diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 34d314c55e..dcf2761983 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -48,9 +48,11 @@ jobs: cd $SOURCE_DIR + BUILD_NUMBER_OFFSET="$(cat build_number_offset)" + export APP_VERSION=$(cat versions.json | python3 -c 'import json,sys;obj=json.load(sys.stdin);print(obj["app"]);') export COMMIT_COUNT=$(git rev-list --count HEAD) - export COMMIT_COUNT="$(($COMMIT_COUNT+2000))" + export COMMIT_COUNT="$(($COMMIT_COUNT+$BUILD_NUMBER_OFFSET))" export BUILD_NUMBER="$COMMIT_COUNT" echo "BUILD_NUMBER=$(echo $BUILD_NUMBER)" >> $GITHUB_ENV echo "APP_VERSION=$(echo $APP_VERSION)" >> $GITHUB_ENV diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index b8cad4dbc1..153e1667a4 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -70,6 +70,7 @@ beta_testflight: stage: build only: - beta + - hotfix except: - tags script: @@ -87,6 +88,7 @@ deploy_beta_testflight: stage: deploy only: - beta + - hotfix except: - tags script: @@ -100,6 +102,7 @@ verifysanity_beta_testflight: stage: verifysanity only: - beta + - hotfix except: - tags script: @@ -118,6 +121,7 @@ verify_beta_testflight: stage: verify only: - beta + - hotfix except: - tags script: diff --git a/Random.txt b/Random.txt index 3e815e3094..86eb369131 100644 --- a/Random.txt +++ b/Random.txt @@ -1 +1 @@ -E65Wt9QZyVD8tvGhCJD3My6x57eDORYaiYh6HR7T3fK= +4f0d2d13a70664d3029d9b97935089df0426fe53745965d175408752838b80dd \ No newline at end of file diff --git a/Telegram/BUILD b/Telegram/BUILD index 5fabcd142c..5c375fe768 100644 --- a/Telegram/BUILD +++ b/Telegram/BUILD @@ -277,6 +277,9 @@ official_apple_pay_merchants = [ "merchant.sberbank.test.ph.telegra.Telegraph", "merchant.privatbank.test.telergramios", "merchant.privatbank.prod.telergram", + "merchant.paymaster.test.telegramios", + "merchant.smartglocal.prod.telegramios", + "merchant.smartglocal.test.telegramios", ] official_bundle_ids = [ @@ -1430,6 +1433,96 @@ ios_extension( ], ) +plist_fragment( + name = "BroadcastUploadInfoPlist", + extension = "plist", + template = + """ + CFBundleDevelopmentRegion + en + CFBundleIdentifier + {telegram_bundle_id}.BroadcastUpload + CFBundleName + Telegram + CFBundlePackageType + XPC! + NSExtension + + NSExtensionPointIdentifier + com.apple.broadcast-services-upload + NSExtensionPrincipalClass + BroadcastUploadSampleHandler + RPBroadcastProcessMode + RPBroadcastProcessModeSampleBuffer + + """.format( + telegram_bundle_id = telegram_bundle_id, + ) +) + +swift_library( + name = "BroadcastUploadExtensionLib", + module_name = "BroadcastUploadExtensionLib", + srcs = glob([ + "BroadcastUpload/**/*.swift", + ]), + deps = [ + "//submodules/TelegramUI:TelegramUI", + "//submodules/TelegramVoip:TelegramVoip", + "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", + "//submodules/BuildConfig:BuildConfig", + "//submodules/WidgetItems:WidgetItems", + "//submodules/BroadcastUploadHelpers:BroadcastUploadHelpers", + ], +) + +genrule( + name = "SetMinOsVersionBroadcastUploadExtension", + cmd_bash = +""" + name=BroadcastUploadExtension.appex + cat $(location PatchMinOSVersion.source.sh) | sed -e "s/<<>>/11\\.0/g" | sed -e "s/<<>>/$$name/g" > $(location SetMinOsVersionBroadcastUploadExtension.sh) +""", + srcs = [ + "PatchMinOSVersion.source.sh", + ], + outs = [ + "SetMinOsVersionBroadcastUploadExtension.sh", + ], + executable = True, + visibility = [ + "//visibility:public", + ] +) + +ios_extension( + name = "BroadcastUploadExtension", + bundle_id = "{telegram_bundle_id}.BroadcastUpload".format( + telegram_bundle_id = telegram_bundle_id, + ), + families = [ + "iphone", + "ipad", + ], + infoplists = [ + ":BroadcastUploadInfoPlist", + ":VersionInfoPlist", + ":BuildNumberInfoPlist", + ":AppNameInfoPlist", + ], + minimum_os_version = "9.0", # maintain the same minimum OS version across extensions + ipa_post_processor = ":SetMinOsVersionBroadcastUploadExtension", + provisioning_profile = select({ + ":disableProvisioningProfilesSetting": None, + "//conditions:default": "@build_configuration//provisioning:BroadcastUpload.mobileprovision", + }), + deps = [":BroadcastUploadExtensionLib"], + frameworks = [ + ":TelegramUIFramework", + ":SwiftSignalKitFramework", + ], +) + plist_fragment( name = "NotificationServiceInfoPlist", extension = "plist", @@ -1718,6 +1811,7 @@ ios_application( ":NotificationServiceExtension", ":IntentsExtension", ":WidgetExtension", + ":BroadcastUploadExtension", ], }), watch_application = select({ @@ -1729,3 +1823,51 @@ ios_application( ":Lib", ], ) + +# Temporary targets used to simplify webrtc build tests + +ios_application( + name = "webrtc_build_test", + bundle_id = "{telegram_bundle_id}".format( + telegram_bundle_id = telegram_bundle_id, + ), + families = ["iphone", "ipad"], + minimum_os_version = "9.0", + provisioning_profile = select({ + ":disableProvisioningProfilesSetting": None, + "//conditions:default": "@build_configuration//provisioning:Telegram.mobileprovision", + }), + entitlements = ":TelegramEntitlements.entitlements", + infoplists = [ + ":TelegramInfoPlist", + ":BuildNumberInfoPlist", + ":VersionInfoPlist", + ":UrlTypesInfoPlist", + ], + deps = [ + "//third-party/webrtc:webrtc_lib", + ], +) + +ios_application( + name = "libvpx_build_test", + bundle_id = "{telegram_bundle_id}".format( + telegram_bundle_id = telegram_bundle_id, + ), + families = ["iphone", "ipad"], + minimum_os_version = "9.0", + provisioning_profile = select({ + ":disableProvisioningProfilesSetting": None, + "//conditions:default": "@build_configuration//provisioning:Telegram.mobileprovision", + }), + entitlements = ":TelegramEntitlements.entitlements", + infoplists = [ + ":TelegramInfoPlist", + ":BuildNumberInfoPlist", + ":VersionInfoPlist", + ":UrlTypesInfoPlist", + ], + deps = [ + "//third-party/libvpx:vpx", + ], +) diff --git a/Telegram/BroadcastUpload/BroadcastUploadExtension.swift b/Telegram/BroadcastUpload/BroadcastUploadExtension.swift new file mode 100644 index 0000000000..d1c0696c6a --- /dev/null +++ b/Telegram/BroadcastUpload/BroadcastUploadExtension.swift @@ -0,0 +1,282 @@ +import Foundation +import ReplayKit +import CoreVideo +import TelegramVoip +import SwiftSignalKit +import BuildConfig +import BroadcastUploadHelpers +import AudioToolbox + +private func rootPathForBasePath(_ appGroupPath: String) -> String { + return appGroupPath + "/telegram-data" +} + +@available(iOS 10.0, *) +@objc(BroadcastUploadSampleHandler) class BroadcastUploadSampleHandler: RPBroadcastSampleHandler { + private var screencastBufferClientContext: IpcGroupCallBufferBroadcastContext? + private var statusDisposable: Disposable? + private var audioConverter: CustomAudioConverter? + + deinit { + self.statusDisposable?.dispose() + } + + public override func beginRequest(with context: NSExtensionContext) { + super.beginRequest(with: context) + } + + private func finish(with reason: IpcGroupCallBufferBroadcastContext.Status.FinishReason) { + var errorString: String? + switch reason { + case .callEnded: + errorString = "You're not in a voice chat" + case .error: + errorString = "Finished" + case .screencastEnded: + break + } + if let errorString = errorString { + let error = NSError(domain: "BroadcastUploadExtension", code: 1, userInfo: [ + NSLocalizedDescriptionKey: errorString + ]) + finishBroadcastWithError(error) + } else { + finishBroadcastGracefully(self) + } + } + + + override public func broadcastStarted(withSetupInfo setupInfo: [String : NSObject]?) { + guard let appBundleIdentifier = Bundle.main.bundleIdentifier, let lastDotRange = appBundleIdentifier.range(of: ".", options: [.backwards]) else { + self.finish(with: .error) + return + } + + let baseAppBundleId = String(appBundleIdentifier[.. deliverOnMainQueue).start(next: { [weak self] status in + guard let strongSelf = self else { + return + } + switch status { + case let .finished(reason): + strongSelf.finish(with: reason) + } + }) + } + + override public func broadcastPaused() { + } + + override public func broadcastResumed() { + } + + override public func broadcastFinished() { + } + + override public func processSampleBuffer(_ sampleBuffer: CMSampleBuffer, with sampleBufferType: RPSampleBufferType) { + switch sampleBufferType { + case RPSampleBufferType.video: + processVideoSampleBuffer(sampleBuffer: sampleBuffer) + case RPSampleBufferType.audioApp: + processAudioSampleBuffer(sampleBuffer: sampleBuffer) + case RPSampleBufferType.audioMic: + break + @unknown default: + break + } + } + + private func processVideoSampleBuffer(sampleBuffer: CMSampleBuffer) { + guard let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else { + return + } + var orientation = CGImagePropertyOrientation.up + if #available(iOS 11.0, *) { + if let orientationAttachment = CMGetAttachment(sampleBuffer, key: RPVideoSampleOrientationKey as CFString, attachmentModeOut: nil) as? NSNumber { + orientation = CGImagePropertyOrientation(rawValue: orientationAttachment.uint32Value) ?? .up + } + } + if let data = serializePixelBuffer(buffer: pixelBuffer) { + self.screencastBufferClientContext?.setCurrentFrame(data: data, orientation: orientation) + } + } + + private func processAudioSampleBuffer(sampleBuffer: CMSampleBuffer) { + guard let formatDescription = CMSampleBufferGetFormatDescription(sampleBuffer) else { + return + } + guard let asbd = CMAudioFormatDescriptionGetStreamBasicDescription(formatDescription) else { + return + } + /*guard let blockBuffer = CMSampleBufferGetDataBuffer(sampleBuffer) else { + return + }*/ + + let format = CustomAudioConverter.Format( + numChannels: Int(asbd.pointee.mChannelsPerFrame), + sampleRate: Int(asbd.pointee.mSampleRate) + ) + if self.audioConverter?.format != format { + self.audioConverter = CustomAudioConverter(asbd: asbd) + } + if let audioConverter = self.audioConverter { + if let data = audioConverter.convert(sampleBuffer: sampleBuffer), !data.isEmpty { + self.screencastBufferClientContext?.writeAudioData(data: data) + } + } + } +} + +private final class CustomAudioConverter { + struct Format: Equatable { + let numChannels: Int + let sampleRate: Int + } + + let format: Format + + var currentInputDescription: UnsafePointer? + var currentBuffer: AudioBuffer? + var currentBufferOffset: UInt32 = 0 + + init(asbd: UnsafePointer) { + self.format = Format( + numChannels: Int(asbd.pointee.mChannelsPerFrame), + sampleRate: Int(asbd.pointee.mSampleRate) + ) + } + + func convert(sampleBuffer: CMSampleBuffer) -> Data? { + guard let formatDescription = CMSampleBufferGetFormatDescription(sampleBuffer) else { + return nil + } + guard let asbd = CMAudioFormatDescriptionGetStreamBasicDescription(formatDescription) else { + return nil + } + + var bufferList = AudioBufferList() + var blockBuffer: CMBlockBuffer? = nil + CMSampleBufferGetAudioBufferListWithRetainedBlockBuffer( + sampleBuffer, + bufferListSizeNeededOut: nil, + bufferListOut: &bufferList, + bufferListSize: MemoryLayout.size, + blockBufferAllocator: nil, + blockBufferMemoryAllocator: nil, + flags: kCMSampleBufferFlag_AudioBufferList_Assure16ByteAlignment, + blockBufferOut: &blockBuffer + ) + let size = bufferList.mBuffers.mDataByteSize + guard size != 0, let mData = bufferList.mBuffers.mData else { + return nil + } + + var outputDescription = AudioStreamBasicDescription( + mSampleRate: 48000.0, + mFormatID: kAudioFormatLinearPCM, + mFormatFlags: kAudioFormatFlagIsSignedInteger | kAudioFormatFlagsNativeEndian | kAudioFormatFlagIsPacked, + mBytesPerPacket: 2, + mFramesPerPacket: 1, + mBytesPerFrame: 2, + mChannelsPerFrame: 1, + mBitsPerChannel: 16, + mReserved: 0 + ) + var maybeAudioConverter: AudioConverterRef? + let _ = AudioConverterNew(asbd, &outputDescription, &maybeAudioConverter) + guard let audioConverter = maybeAudioConverter else { + return nil + } + + self.currentBuffer = AudioBuffer( + mNumberChannels: asbd.pointee.mChannelsPerFrame, + mDataByteSize: UInt32(size), + mData: mData + ) + self.currentBufferOffset = 0 + self.currentInputDescription = asbd + + var numPackets: UInt32? + let outputSize = 32768 * 2 + var outputBuffer = Data(count: outputSize) + outputBuffer.withUnsafeMutableBytes { (outputBytes: UnsafeMutableRawBufferPointer) -> Void in + var outputBufferList = AudioBufferList() + outputBufferList.mNumberBuffers = 1 + outputBufferList.mBuffers.mNumberChannels = outputDescription.mChannelsPerFrame + outputBufferList.mBuffers.mDataByteSize = UInt32(outputSize) + outputBufferList.mBuffers.mData = outputBytes.baseAddress! + + var outputDataPacketSize = UInt32(outputSize) / outputDescription.mBytesPerPacket + + let result = AudioConverterFillComplexBuffer( + audioConverter, + converterComplexInputDataProc, + Unmanaged.passUnretained(self).toOpaque(), + &outputDataPacketSize, + &outputBufferList, + nil + ) + if result == noErr { + numPackets = outputDataPacketSize + } + } + + AudioConverterDispose(audioConverter) + + if let numPackets = numPackets { + outputBuffer.count = Int(numPackets * outputDescription.mBytesPerPacket) + return outputBuffer + } else { + return nil + } + } +} + +private func converterComplexInputDataProc(inAudioConverter: AudioConverterRef, ioNumberDataPackets: UnsafeMutablePointer, ioData: UnsafeMutablePointer, ioDataPacketDescription: UnsafeMutablePointer?>?, inUserData: UnsafeMutableRawPointer?) -> Int32 { + guard let inUserData = inUserData else { + ioNumberDataPackets.pointee = 0 + return 0 + } + let instance = Unmanaged.fromOpaque(inUserData).takeUnretainedValue() + guard let currentBuffer = instance.currentBuffer else { + ioNumberDataPackets.pointee = 0 + return 0 + } + guard let currentInputDescription = instance.currentInputDescription else { + ioNumberDataPackets.pointee = 0 + return 0 + } + + let numPacketsInBuffer = currentBuffer.mDataByteSize / currentInputDescription.pointee.mBytesPerPacket + let numPacketsAvailable = numPacketsInBuffer - instance.currentBufferOffset / currentInputDescription.pointee.mBytesPerPacket + + let numPacketsToRead = min(ioNumberDataPackets.pointee, numPacketsAvailable) + ioNumberDataPackets.pointee = numPacketsToRead + + ioData.pointee.mNumberBuffers = 1 + ioData.pointee.mBuffers.mData = currentBuffer.mData?.advanced(by: Int(instance.currentBufferOffset)) + ioData.pointee.mBuffers.mDataByteSize = currentBuffer.mDataByteSize - instance.currentBufferOffset + ioData.pointee.mBuffers.mNumberChannels = currentBuffer.mNumberChannels + + instance.currentBufferOffset += numPacketsToRead * currentInputDescription.pointee.mBytesPerPacket + + return 0 +} diff --git a/Telegram/NotificationContent/NotificationViewController.swift b/Telegram/NotificationContent/NotificationViewController.swift index d61ef5ffa2..56301732f9 100644 --- a/Telegram/NotificationContent/NotificationViewController.swift +++ b/Telegram/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, apiHash: buildConfig.apiHash, languagesCategory: languagesCategory, encryptionParameters: encryptionParameters, appVersion: appVersion, bundleData: buildConfig.bundleData(withAppToken: nil, signatureDict: nil)), setPreferredContentSize: { [weak self] size in + self.impl = NotificationViewControllerImpl(initializationData: NotificationViewControllerInitializationData(appBundleId: baseAppBundleId, 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/Telegram/NotificationService/NotificationServiceObjC/Sources/Serialization.m b/Telegram/NotificationService/NotificationServiceObjC/Sources/Serialization.m index 4b9dce740b..05d22b5f91 100644 --- a/Telegram/NotificationService/NotificationServiceObjC/Sources/Serialization.m +++ b/Telegram/NotificationService/NotificationServiceObjC/Sources/Serialization.m @@ -3,7 +3,7 @@ @implementation Serialization - (NSUInteger)currentLayer { - return 125; + return 131; } - (id _Nullable)parseMessage:(NSData * _Nullable)data { diff --git a/Telegram/Share/ShareRootController.swift b/Telegram/Share/ShareRootController.swift index c0da95155d..1c53d48b3e 100644 --- a/Telegram/Share/ShareRootController.swift +++ b/Telegram/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, apiHash: buildConfig.apiHash, languagesCategory: languagesCategory, encryptionParameters: encryptionParameters, appVersion: appVersion, bundleData: buildConfig.bundleData(withAppToken: nil, signatureDict: nil)), getExtensionContext: { [weak self] in + self.impl = ShareRootControllerImpl(initializationData: ShareRootControllerInitializationData(appBundleId: baseAppBundleId, 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/Telegram/SiriIntents/Info.plist b/Telegram/SiriIntents/Info.plist deleted file mode 100644 index d77820f815..0000000000 --- a/Telegram/SiriIntents/Info.plist +++ /dev/null @@ -1,46 +0,0 @@ - - - - - CFBundleDevelopmentRegion - en - CFBundleDisplayName - ${APP_NAME} - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - XPC! - CFBundleShortVersionString - $(PRODUCT_BUNDLE_SHORT_VERSION) - CFBundleVersion - ${BUILD_NUMBER} - NSExtension - - NSExtensionAttributes - - IntentsRestrictedWhileLocked - - IntentsRestrictedWhileProtectedDataUnavailable - - IntentsSupported - - INSendMessageIntent - INStartAudioCallIntent - INSearchForMessagesIntent - INSetMessageAttributeIntent - INSearchCallHistoryIntent - - - NSExtensionPointIdentifier - com.apple.intents-service - NSExtensionPrincipalClass - IntentHandler - - - diff --git a/Telegram/SiriIntents/IntentContacts.swift b/Telegram/SiriIntents/IntentContacts.swift index 836fc29984..cb0fca310e 100644 --- a/Telegram/SiriIntents/IntentContacts.swift +++ b/Telegram/SiriIntents/IntentContacts.swift @@ -35,7 +35,7 @@ private func parseAppSpecificContactReference(_ value: String) -> PeerId? { } let idString = String(value[value.index(value.startIndex, offsetBy: phonebookUsernamePrefix.count)...]) if let id = Int32(idString) { - return PeerId(namespace: Namespaces.Peer.CloudUser, id: id) + return PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt32Value(id)) } return nil } diff --git a/Telegram/SiriIntents/IntentHandler.swift b/Telegram/SiriIntents/IntentHandler.swift index 2d833a6c71..9429b3d762 100644 --- a/Telegram/SiriIntents/IntentHandler.swift +++ b/Telegram/SiriIntents/IntentHandler.swift @@ -114,7 +114,7 @@ class DefaultIntentHandler: INExtension, INSendMessageIntentHandling, INSearchFo self.rootPath = rootPath - TempBox.initializeShared(basePath: rootPath, processType: "siri", launchSpecificId: arc4random64()) + TempBox.initializeShared(basePath: rootPath, processType: "siri", launchSpecificId: Int64.random(in: Int64.min ... Int64.max)) let logsPath = rootPath + "/siri-logs" let _ = try? FileManager.default.createDirectory(atPath: logsPath, withIntermediateDirectories: true, attributes: nil) @@ -619,7 +619,7 @@ class DefaultIntentHandler: INExtension, INSendMessageIntentHandling, INSearchFo } for (_, messageId) in maxMessageIdsToApply { - signals.append(applyMaxReadIndexInteractively(postbox: account.postbox, stateManager: account.stateManager, index: MessageIndex(id: messageId, timestamp: 0)) + signals.append(TelegramEngine(account: account).messages.applyMaxReadIndexInteractively(index: MessageIndex(id: messageId, timestamp: 0)) |> castError(IntentHandlingError.self)) } @@ -793,7 +793,7 @@ class DefaultIntentHandler: INExtension, INSendMessageIntentHandling, INSearchFo var accountResults: [Signal, Error>] = [] for (accountId, accountPeerId, _) in accounts { - accountResults.append(accountTransaction(rootPath: rootPath, id: accountId, encryptionParameters: encryptionParameters, isReadOnly: true, useCopy: true, transaction: { postbox, transaction -> INObjectSection in + accountResults.append(accountTransaction(rootPath: rootPath, id: accountId, encryptionParameters: encryptionParameters, isReadOnly: true, useCopy: false, transaction: { postbox, transaction -> INObjectSection in var accountTitle: String = "" if let peer = transaction.getPeer(accountPeerId) as? TelegramUser { if let username = peer.username, !username.isEmpty { @@ -884,7 +884,7 @@ private final class WidgetIntentHandler { self.rootPath = rootPath - TempBox.initializeShared(basePath: rootPath, processType: "siri", launchSpecificId: arc4random64()) + TempBox.initializeShared(basePath: rootPath, processType: "siri", launchSpecificId: Int64.random(in: Int64.min ... Int64.max)) let logsPath = rootPath + "/siri-logs" let _ = try? FileManager.default.createDirectory(atPath: logsPath, withIntermediateDirectories: true, attributes: nil) @@ -962,7 +962,7 @@ private final class WidgetIntentHandler { var accountResults: [Signal, Error>] = [] for (accountId, accountPeerId, _) in accounts { - accountResults.append(accountTransaction(rootPath: rootPath, id: accountId, encryptionParameters: encryptionParameters, isReadOnly: true, useCopy: true, transaction: { postbox, transaction -> INObjectSection in + accountResults.append(accountTransaction(rootPath: rootPath, id: accountId, encryptionParameters: encryptionParameters, isReadOnly: true, useCopy: false, transaction: { postbox, transaction -> INObjectSection in var accountTitle: String = "" if let peer = transaction.getPeer(accountPeerId) as? TelegramUser { if let username = peer.username, !username.isEmpty { @@ -1045,10 +1045,10 @@ private final class WidgetIntentHandler { if !isActive { continue } - accountResults.append(accountTransaction(rootPath: rootPath, id: accountId, encryptionParameters: encryptionParameters, isReadOnly: true, useCopy: true, transaction: { postbox, transaction -> [Friend] in + accountResults.append(accountTransaction(rootPath: rootPath, id: accountId, encryptionParameters: encryptionParameters, isReadOnly: true, useCopy: false, transaction: { postbox, transaction -> [Friend] in var peers: [Peer] = [] - for id in getRecentPeers(transaction: transaction) { + for id in _internal_getRecentPeers(transaction: transaction) { if let peer = transaction.getPeer(id), !(peer is TelegramSecretChat), !peer.isDeleted { peers.append(peer) } diff --git a/Telegram/SiriIntents/IntentMessages.swift b/Telegram/SiriIntents/IntentMessages.swift index 9661264889..1fddc10935 100644 --- a/Telegram/SiriIntents/IntentMessages.swift +++ b/Telegram/SiriIntents/IntentMessages.swift @@ -166,7 +166,7 @@ private func callWithTelegramMessage(_ telegramMessage: Message, account: Accoun @available(iOSApplicationExtension 10.0, iOS 10.0, *) private func messageWithTelegramMessage(_ telegramMessage: Message) -> INMessage? { - guard let author = telegramMessage.author, let user = telegramMessage.peers[author.id] as? TelegramUser, user.id.id != 777000 else { + guard let author = telegramMessage.author, let user = telegramMessage.peers[author.id] as? TelegramUser, user.id.id._internalGetInt32Value() != 777000 else { return nil } diff --git a/Telegram/Telegram-iOS/AlternateIcons-iPad.plist b/Telegram/Telegram-iOS/AlternateIcons-iPad.plist index 2d3b5e8409..a9ebd9173d 100644 --- a/Telegram/Telegram-iOS/AlternateIcons-iPad.plist +++ b/Telegram/Telegram-iOS/AlternateIcons-iPad.plist @@ -72,4 +72,44 @@ UIPrerenderedIcon + New1 + + CFBundleIconFiles + + New1_20x20 + New1_29x29 + New1_40x40 + New1_58x58 + New1_60x60 + New1_76x76 + New1_80x80 + New1_87x87 + New1_120x120 + New1_152x152 + New1_167x167 + New1_180x180 + + UIPrerenderedIcon + + + New2 + + CFBundleIconFiles + + New2_20x20 + New2_29x29 + New2_40x40 + New2_58x58 + New2_60x60 + New2_76x76 + New2_80x80 + New2_87x87 + New2_120x120 + New2_152x152 + New2_167x167 + New2_180x180 + + UIPrerenderedIcon + + \ No newline at end of file diff --git a/Telegram/Telegram-iOS/AlternateIcons.plist b/Telegram/Telegram-iOS/AlternateIcons.plist index 99a0e7039a..d97e8c714a 100644 --- a/Telegram/Telegram-iOS/AlternateIcons.plist +++ b/Telegram/Telegram-iOS/AlternateIcons.plist @@ -66,4 +66,44 @@ UIPrerenderedIcon + New1 + + CFBundleIconFiles + + New1_20x20 + New1_29x29 + New1_40x40 + New1_58x58 + New1_60x60 + New1_76x76 + New1_80x80 + New1_87x87 + New1_120x120 + New1_152x152 + New1_167x167 + New1_180x180 + + UIPrerenderedIcon + + + New2 + + CFBundleIconFiles + + New2_20x20 + New2_29x29 + New2_40x40 + New2_58x58 + New2_60x60 + New2_76x76 + New2_80x80 + New2_87x87 + New2_120x120 + New2_152x152 + New2_167x167 + New2_180x180 + + UIPrerenderedIcon + + \ No newline at end of file diff --git a/Telegram/Telegram-iOS/New1_120x120.png b/Telegram/Telegram-iOS/New1_120x120.png new file mode 100644 index 0000000000..70ddc32cbe Binary files /dev/null and b/Telegram/Telegram-iOS/New1_120x120.png differ diff --git a/Telegram/Telegram-iOS/New1_152x152.png b/Telegram/Telegram-iOS/New1_152x152.png new file mode 100644 index 0000000000..32adc011d1 Binary files /dev/null and b/Telegram/Telegram-iOS/New1_152x152.png differ diff --git a/Telegram/Telegram-iOS/New1_167x167.png b/Telegram/Telegram-iOS/New1_167x167.png new file mode 100644 index 0000000000..93238e0c7f Binary files /dev/null and b/Telegram/Telegram-iOS/New1_167x167.png differ diff --git a/Telegram/Telegram-iOS/New1_180x180.png b/Telegram/Telegram-iOS/New1_180x180.png new file mode 100644 index 0000000000..ced492fd40 Binary files /dev/null and b/Telegram/Telegram-iOS/New1_180x180.png differ diff --git a/Telegram/Telegram-iOS/New1_20x20.png b/Telegram/Telegram-iOS/New1_20x20.png new file mode 100644 index 0000000000..34afc4fbec Binary files /dev/null and b/Telegram/Telegram-iOS/New1_20x20.png differ diff --git a/Telegram/Telegram-iOS/New1_29x29.png b/Telegram/Telegram-iOS/New1_29x29.png new file mode 100644 index 0000000000..6387cb01bd Binary files /dev/null and b/Telegram/Telegram-iOS/New1_29x29.png differ diff --git a/Telegram/Telegram-iOS/New1_40x40.png b/Telegram/Telegram-iOS/New1_40x40.png new file mode 100644 index 0000000000..e29005ac4e Binary files /dev/null and b/Telegram/Telegram-iOS/New1_40x40.png differ diff --git a/Telegram/Telegram-iOS/New1_58x58.png b/Telegram/Telegram-iOS/New1_58x58.png new file mode 100644 index 0000000000..93c34d10c7 Binary files /dev/null and b/Telegram/Telegram-iOS/New1_58x58.png differ diff --git a/Telegram/Telegram-iOS/New1_60x60.png b/Telegram/Telegram-iOS/New1_60x60.png new file mode 100644 index 0000000000..54a04f2c27 Binary files /dev/null and b/Telegram/Telegram-iOS/New1_60x60.png differ diff --git a/Telegram/Telegram-iOS/New1_76x76.png b/Telegram/Telegram-iOS/New1_76x76.png new file mode 100644 index 0000000000..c85f9bc45a Binary files /dev/null and b/Telegram/Telegram-iOS/New1_76x76.png differ diff --git a/Telegram/Telegram-iOS/New1_80x80.png b/Telegram/Telegram-iOS/New1_80x80.png new file mode 100644 index 0000000000..fb4f4a6122 Binary files /dev/null and b/Telegram/Telegram-iOS/New1_80x80.png differ diff --git a/Telegram/Telegram-iOS/New1_87x87.png b/Telegram/Telegram-iOS/New1_87x87.png new file mode 100644 index 0000000000..1b3e74dfa6 Binary files /dev/null and b/Telegram/Telegram-iOS/New1_87x87.png differ diff --git a/Telegram/Telegram-iOS/New2_120x120.png b/Telegram/Telegram-iOS/New2_120x120.png new file mode 100644 index 0000000000..85de9e3fab Binary files /dev/null and b/Telegram/Telegram-iOS/New2_120x120.png differ diff --git a/Telegram/Telegram-iOS/New2_152x152.png b/Telegram/Telegram-iOS/New2_152x152.png new file mode 100644 index 0000000000..d94dc86c61 Binary files /dev/null and b/Telegram/Telegram-iOS/New2_152x152.png differ diff --git a/Telegram/Telegram-iOS/New2_167x167.png b/Telegram/Telegram-iOS/New2_167x167.png new file mode 100644 index 0000000000..813a39a5bd Binary files /dev/null and b/Telegram/Telegram-iOS/New2_167x167.png differ diff --git a/Telegram/Telegram-iOS/New2_180x180.png b/Telegram/Telegram-iOS/New2_180x180.png new file mode 100644 index 0000000000..a083c562d4 Binary files /dev/null and b/Telegram/Telegram-iOS/New2_180x180.png differ diff --git a/Telegram/Telegram-iOS/New2_20x20.png b/Telegram/Telegram-iOS/New2_20x20.png new file mode 100644 index 0000000000..d2d9d52e92 Binary files /dev/null and b/Telegram/Telegram-iOS/New2_20x20.png differ diff --git a/Telegram/Telegram-iOS/New2_29x29.png b/Telegram/Telegram-iOS/New2_29x29.png new file mode 100644 index 0000000000..4e0265fa69 Binary files /dev/null and b/Telegram/Telegram-iOS/New2_29x29.png differ diff --git a/Telegram/Telegram-iOS/New2_40x40.png b/Telegram/Telegram-iOS/New2_40x40.png new file mode 100644 index 0000000000..fe2d70eada Binary files /dev/null and b/Telegram/Telegram-iOS/New2_40x40.png differ diff --git a/Telegram/Telegram-iOS/New2_58x58.png b/Telegram/Telegram-iOS/New2_58x58.png new file mode 100644 index 0000000000..37e2e15d18 Binary files /dev/null and b/Telegram/Telegram-iOS/New2_58x58.png differ diff --git a/Telegram/Telegram-iOS/New2_60x60.png b/Telegram/Telegram-iOS/New2_60x60.png new file mode 100644 index 0000000000..caab8e9d8a Binary files /dev/null and b/Telegram/Telegram-iOS/New2_60x60.png differ diff --git a/Telegram/Telegram-iOS/New2_76x76.png b/Telegram/Telegram-iOS/New2_76x76.png new file mode 100644 index 0000000000..ca043ed339 Binary files /dev/null and b/Telegram/Telegram-iOS/New2_76x76.png differ diff --git a/Telegram/Telegram-iOS/New2_80x80.png b/Telegram/Telegram-iOS/New2_80x80.png new file mode 100644 index 0000000000..6750299df3 Binary files /dev/null and b/Telegram/Telegram-iOS/New2_80x80.png differ diff --git a/Telegram/Telegram-iOS/New2_87x87.png b/Telegram/Telegram-iOS/New2_87x87.png new file mode 100644 index 0000000000..1815681047 Binary files /dev/null and b/Telegram/Telegram-iOS/New2_87x87.png differ diff --git a/Telegram/Telegram-iOS/Resources/Channel.tgs b/Telegram/Telegram-iOS/Resources/Channel.tgs index db71858d35..be8dd016f4 100644 Binary files a/Telegram/Telegram-iOS/Resources/Channel.tgs and b/Telegram/Telegram-iOS/Resources/Channel.tgs differ diff --git a/Telegram/Telegram-iOS/Resources/VoiceCancelReminder.tgs b/Telegram/Telegram-iOS/Resources/VoiceCancelReminder.tgs new file mode 100644 index 0000000000..133d47e1e1 Binary files /dev/null and b/Telegram/Telegram-iOS/Resources/VoiceCancelReminder.tgs differ diff --git a/Telegram/Telegram-iOS/Resources/VoiceCancelReminderToMute.tgs b/Telegram/Telegram-iOS/Resources/VoiceCancelReminderToMute.tgs new file mode 100644 index 0000000000..dbd7cd7e93 Binary files /dev/null and b/Telegram/Telegram-iOS/Resources/VoiceCancelReminderToMute.tgs differ diff --git a/Telegram/Telegram-iOS/Resources/VoiceCancelReminderToRaiseHand.tgs b/Telegram/Telegram-iOS/Resources/VoiceCancelReminderToRaiseHand.tgs new file mode 100644 index 0000000000..bc8c45ab81 Binary files /dev/null and b/Telegram/Telegram-iOS/Resources/VoiceCancelReminderToRaiseHand.tgs differ diff --git a/Telegram/Telegram-iOS/Resources/VoiceHandOff.tgs b/Telegram/Telegram-iOS/Resources/VoiceHandOff.tgs deleted file mode 100644 index 3e20f03395..0000000000 Binary files a/Telegram/Telegram-iOS/Resources/VoiceHandOff.tgs and /dev/null differ diff --git a/Telegram/Telegram-iOS/Resources/VoiceHandOff2.tgs b/Telegram/Telegram-iOS/Resources/VoiceHandOff2.tgs deleted file mode 100644 index ed44efc0ba..0000000000 Binary files a/Telegram/Telegram-iOS/Resources/VoiceHandOff2.tgs and /dev/null differ diff --git a/Telegram/Telegram-iOS/Resources/VoiceHandOn.tgs b/Telegram/Telegram-iOS/Resources/VoiceHandOn.tgs deleted file mode 100644 index f0db083d4f..0000000000 Binary files a/Telegram/Telegram-iOS/Resources/VoiceHandOn.tgs and /dev/null differ diff --git a/Telegram/Telegram-iOS/Resources/VoiceHand_1.tgs b/Telegram/Telegram-iOS/Resources/VoiceHand_1.tgs index 09612a30a1..8a7845594e 100644 Binary files a/Telegram/Telegram-iOS/Resources/VoiceHand_1.tgs and b/Telegram/Telegram-iOS/Resources/VoiceHand_1.tgs differ diff --git a/Telegram/Telegram-iOS/Resources/VoiceHand_10.tgs b/Telegram/Telegram-iOS/Resources/VoiceHand_10.tgs new file mode 100644 index 0000000000..f49670a841 Binary files /dev/null and b/Telegram/Telegram-iOS/Resources/VoiceHand_10.tgs differ diff --git a/Telegram/Telegram-iOS/Resources/VoiceHand_2.tgs b/Telegram/Telegram-iOS/Resources/VoiceHand_2.tgs index 5664e4df5b..0fd6090d7d 100644 Binary files a/Telegram/Telegram-iOS/Resources/VoiceHand_2.tgs and b/Telegram/Telegram-iOS/Resources/VoiceHand_2.tgs differ diff --git a/Telegram/Telegram-iOS/Resources/VoiceHand_3.tgs b/Telegram/Telegram-iOS/Resources/VoiceHand_3.tgs index ea6710d819..3ef3620507 100644 Binary files a/Telegram/Telegram-iOS/Resources/VoiceHand_3.tgs and b/Telegram/Telegram-iOS/Resources/VoiceHand_3.tgs differ diff --git a/Telegram/Telegram-iOS/Resources/VoiceHand_4.tgs b/Telegram/Telegram-iOS/Resources/VoiceHand_4.tgs index 49e54790cf..0c20c2a4e8 100644 Binary files a/Telegram/Telegram-iOS/Resources/VoiceHand_4.tgs and b/Telegram/Telegram-iOS/Resources/VoiceHand_4.tgs differ diff --git a/Telegram/Telegram-iOS/Resources/VoiceHand_5.tgs b/Telegram/Telegram-iOS/Resources/VoiceHand_5.tgs index 396e572397..0675d255d6 100644 Binary files a/Telegram/Telegram-iOS/Resources/VoiceHand_5.tgs and b/Telegram/Telegram-iOS/Resources/VoiceHand_5.tgs differ diff --git a/Telegram/Telegram-iOS/Resources/VoiceHand_6.tgs b/Telegram/Telegram-iOS/Resources/VoiceHand_6.tgs index 04a797f9b4..fdadd74a8e 100644 Binary files a/Telegram/Telegram-iOS/Resources/VoiceHand_6.tgs and b/Telegram/Telegram-iOS/Resources/VoiceHand_6.tgs differ diff --git a/Telegram/Telegram-iOS/Resources/VoiceHand_7.tgs b/Telegram/Telegram-iOS/Resources/VoiceHand_7.tgs index dd03e566ba..74a6db1b1f 100644 Binary files a/Telegram/Telegram-iOS/Resources/VoiceHand_7.tgs and b/Telegram/Telegram-iOS/Resources/VoiceHand_7.tgs differ diff --git a/Telegram/Telegram-iOS/Resources/VoiceHand_8.tgs b/Telegram/Telegram-iOS/Resources/VoiceHand_8.tgs new file mode 100644 index 0000000000..b4ef9e3e02 Binary files /dev/null and b/Telegram/Telegram-iOS/Resources/VoiceHand_8.tgs differ diff --git a/Telegram/Telegram-iOS/Resources/VoiceHand_9.tgs b/Telegram/Telegram-iOS/Resources/VoiceHand_9.tgs new file mode 100644 index 0000000000..e1d4e9965b Binary files /dev/null and b/Telegram/Telegram-iOS/Resources/VoiceHand_9.tgs differ diff --git a/Telegram/Telegram-iOS/Resources/VoiceMute.tgs b/Telegram/Telegram-iOS/Resources/VoiceMute.tgs index a6d20bddcc..533a7a6a2e 100644 Binary files a/Telegram/Telegram-iOS/Resources/VoiceMute.tgs and b/Telegram/Telegram-iOS/Resources/VoiceMute.tgs differ diff --git a/Telegram/Telegram-iOS/Resources/VoiceMuteToRaiseHand.tgs b/Telegram/Telegram-iOS/Resources/VoiceMuteToRaiseHand.tgs new file mode 100644 index 0000000000..6424171527 Binary files /dev/null and b/Telegram/Telegram-iOS/Resources/VoiceMuteToRaiseHand.tgs differ diff --git a/Telegram/Telegram-iOS/Resources/VoiceRaiseHandToMute.tgs b/Telegram/Telegram-iOS/Resources/VoiceRaiseHandToMute.tgs new file mode 100644 index 0000000000..ac08b6eb7f Binary files /dev/null and b/Telegram/Telegram-iOS/Resources/VoiceRaiseHandToMute.tgs differ diff --git a/Telegram/Telegram-iOS/Resources/VoiceSetReminder.tgs b/Telegram/Telegram-iOS/Resources/VoiceSetReminder.tgs new file mode 100644 index 0000000000..fe22c2c4ea Binary files /dev/null and b/Telegram/Telegram-iOS/Resources/VoiceSetReminder.tgs differ diff --git a/Telegram/Telegram-iOS/Resources/VoiceSetReminderToMute.tgs b/Telegram/Telegram-iOS/Resources/VoiceSetReminderToMute.tgs new file mode 100644 index 0000000000..6edae3e5c2 Binary files /dev/null and b/Telegram/Telegram-iOS/Resources/VoiceSetReminderToMute.tgs differ diff --git a/Telegram/Telegram-iOS/Resources/VoiceSetReminderToRaiseHand.tgs b/Telegram/Telegram-iOS/Resources/VoiceSetReminderToRaiseHand.tgs new file mode 100644 index 0000000000..802ca82bde Binary files /dev/null and b/Telegram/Telegram-iOS/Resources/VoiceSetReminderToRaiseHand.tgs differ diff --git a/Telegram/Telegram-iOS/Resources/VoiceStart.tgs b/Telegram/Telegram-iOS/Resources/VoiceStart.tgs new file mode 100644 index 0000000000..de31e33e5b Binary files /dev/null and b/Telegram/Telegram-iOS/Resources/VoiceStart.tgs differ diff --git a/Telegram/Telegram-iOS/Resources/VoiceUnmute.tgs b/Telegram/Telegram-iOS/Resources/VoiceUnmute.tgs index 2f38fb678f..57d546d4d2 100644 Binary files a/Telegram/Telegram-iOS/Resources/VoiceUnmute.tgs and b/Telegram/Telegram-iOS/Resources/VoiceUnmute.tgs differ diff --git a/Telegram/Telegram-iOS/Resources/VoiceUnmuteToRaiseHand.tgs b/Telegram/Telegram-iOS/Resources/VoiceUnmuteToRaiseHand.tgs new file mode 100644 index 0000000000..0c0d781c7b Binary files /dev/null and b/Telegram/Telegram-iOS/Resources/VoiceUnmuteToRaiseHand.tgs differ diff --git a/Telegram/Telegram-iOS/Resources/begin_record.caf b/Telegram/Telegram-iOS/Resources/begin_record.caf deleted file mode 100644 index 3d2e9c4813..0000000000 Binary files a/Telegram/Telegram-iOS/Resources/begin_record.caf and /dev/null differ diff --git a/Telegram/Telegram-iOS/Resources/begin_record.mp3 b/Telegram/Telegram-iOS/Resources/begin_record.mp3 new file mode 100644 index 0000000000..8a5cf9fb82 Binary files /dev/null and b/Telegram/Telegram-iOS/Resources/begin_record.mp3 differ diff --git a/Telegram/Telegram-iOS/Resources/voip_busy.caf b/Telegram/Telegram-iOS/Resources/voip_busy.caf deleted file mode 100644 index 7a6d839506..0000000000 Binary files a/Telegram/Telegram-iOS/Resources/voip_busy.caf and /dev/null differ diff --git a/Telegram/Telegram-iOS/Resources/voip_busy.mp3 b/Telegram/Telegram-iOS/Resources/voip_busy.mp3 new file mode 100644 index 0000000000..08f9897a25 Binary files /dev/null and b/Telegram/Telegram-iOS/Resources/voip_busy.mp3 differ diff --git a/Telegram/Telegram-iOS/Resources/voip_end.caf b/Telegram/Telegram-iOS/Resources/voip_end.caf deleted file mode 100644 index c0a22b1389..0000000000 Binary files a/Telegram/Telegram-iOS/Resources/voip_end.caf and /dev/null differ diff --git a/Telegram/Telegram-iOS/Resources/voip_end.mp3 b/Telegram/Telegram-iOS/Resources/voip_end.mp3 new file mode 100644 index 0000000000..2bf50b2f22 Binary files /dev/null and b/Telegram/Telegram-iOS/Resources/voip_end.mp3 differ diff --git a/Telegram/Telegram-iOS/Resources/voip_fail.caf b/Telegram/Telegram-iOS/Resources/voip_fail.caf deleted file mode 100644 index 17e0da3de4..0000000000 Binary files a/Telegram/Telegram-iOS/Resources/voip_fail.caf and /dev/null differ diff --git a/Telegram/Telegram-iOS/Resources/voip_fail.mp3 b/Telegram/Telegram-iOS/Resources/voip_fail.mp3 new file mode 100644 index 0000000000..fc87e864a0 Binary files /dev/null and b/Telegram/Telegram-iOS/Resources/voip_fail.mp3 differ diff --git a/Telegram/Telegram-iOS/Resources/voip_ringback.caf b/Telegram/Telegram-iOS/Resources/voip_ringback.caf deleted file mode 100644 index af7253776c..0000000000 Binary files a/Telegram/Telegram-iOS/Resources/voip_ringback.caf and /dev/null differ diff --git a/Telegram/Telegram-iOS/Resources/voip_ringback.mp3 b/Telegram/Telegram-iOS/Resources/voip_ringback.mp3 new file mode 100644 index 0000000000..ff955a38a6 Binary files /dev/null and b/Telegram/Telegram-iOS/Resources/voip_ringback.mp3 differ diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 38f0ddf643..1192cce349 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -104,9 +104,8 @@ "PUSH_MESSAGES_1" = "%1$@|sent you a message"; "PUSH_MESSAGES_any" = "%1$@|sent you %2$d messages"; "PUSH_ALBUM" = "%1$@|sent you an album"; -"PUSH_MESSAGE_DOCS" = "%1$@|sent you %2$d files"; -"PUSH_MESSAGE_DOCS_1" = "%1$@|sent you a file"; -"PUSH_MESSAGE_DOCS_any" = "%1$@|sent you %2$d files"; +"PUSH_MESSAGE_FILES_1" = "%1$@|sent you a file"; +"PUSH_MESSAGE_FILES_any" = "%1$@|sent you %2$d files"; "PUSH_CHANNEL_MESSAGE_TEXT" = "%1$@|%2$@"; @@ -2552,7 +2551,6 @@ Unused sets are archived when you add more."; "Message.ForwardedMessageShort" = "Forwarded From\n%@"; "Checkout.LiabilityAlertTitle" = "Warning"; -"Checkout.LiabilityAlert" = "Neither Telegram, nor %1$@ will have access to your credit card information. Credit card details will be handled only by the payment system, %2$@.\n\nPayments will go directly to the developer of %1$@. Telegram cannot provide any guarantees, so proceed at your own risk. In case of problems, please contact the developer of %1$@ or your bank."; "Settings.AppLanguage" = "Language"; "Settings.AppLanguage.Unofficial" = "UNOFFICIAL"; @@ -3950,6 +3948,7 @@ Unused sets are archived when you add more."; "WallpaperPreview.Title" = "Background Preview"; "WallpaperPreview.PreviewTopText" = "Press Set to apply the background"; "WallpaperPreview.PreviewBottomText" = "Enjoy the view"; + "WallpaperPreview.SwipeTopText" = "Swipe left or right to preview more backgrounds"; "WallpaperPreview.SwipeBottomText" = "Backgrounds for the god of backgrounds!"; "WallpaperPreview.SwipeColorsTopText" = "Swipe left or right to see more colors"; @@ -4430,6 +4429,8 @@ Sorry for the inconvenience."; "Appearance.AppIconClassicX" = "Classic X"; "Appearance.AppIconFilled" = "Filled"; "Appearance.AppIconFilledX" = "Filled X"; +"Appearance.AppIconNew1" = "Sunset"; +"Appearance.AppIconNew2" = "Aqua"; "Appearance.ThemeCarouselClassic" = "Classic"; "Appearance.ThemeCarouselDay" = "Day"; @@ -5742,6 +5743,7 @@ Sorry for the inconvenience."; "Notification.VoiceChatStarted" = "%1$@ started a voice chat"; "Notification.VoiceChatEnded" = "Voice chat ended (%@)"; +"Notification.VoiceChatEndedGroup" = "%1$@ ended the voice chat (%2$@)"; "VoiceChat.Panel.TapToJoin" = "Tap to join"; "VoiceChat.Panel.Members_0" = "%@ participants"; @@ -5774,10 +5776,13 @@ Sorry for the inconvenience."; "VoiceChat.CreateNewVoiceChatText" = "Voice chat ended. Start a new one?"; "VoiceChat.CreateNewVoiceChatStart" = "Start"; +"VoiceChat.CreateNewVoiceChatStartNow" = "Start Now"; +"VoiceChat.CreateNewVoiceChatSchedule" = "Schedule"; "PUSH_CHAT_VOICECHAT_START" = "%2$@|%1$@ started a voice chat"; "PUSH_CHAT_VOICECHAT_INVITE" = "%2$@|%1$@ invited %3$@ to the voice chat"; -"PUSH_CHAT_VOICECHAT_INVITE_YOU" = "%2$|@%1$@ invited you to the voice chat"; +"PUSH_CHAT_VOICECHAT_INVITE_YOU" = "%2$@|%1$@ invited you to the voice chat"; +"PUSH_CHAT_VOICECHAT_END" = "%2$@|%1$@ has ended the voice chat"; "Call.VoiceChatInProgressTitle" = "Voice Chat in Progress"; "Call.VoiceChatInProgressMessageCall" = "Leave voice chat in %1$@ and start a call with %2$@?"; @@ -6037,7 +6042,6 @@ Sorry for the inconvenience."; "Conversation.ForwardTooltip.TwoChats.Many" = "Messages forwarded to **%@** and **%@**"; "Conversation.ForwardTooltip.ManyChats.One" = "Message forwarded to **%@** and %@ others"; "Conversation.ForwardTooltip.ManyChats.Many" = "Messages forwarded to **%@** and %@ others"; - "Conversation.ForwardTooltip.SavedMessages.One" = "Message forwarded to **Saved Messages**"; "Conversation.ForwardTooltip.SavedMessages.Many" = "Messages forwarded to **Saved Messages**"; @@ -6255,6 +6259,7 @@ Sorry for the inconvenience."; "VoiceChat.YouCanNowSpeak" = "You can now speak"; "VoiceChat.YouCanNowSpeakIn" = "You can now speak in **%@**"; +"VoiceChat.UserCanNowSpeak" = "**%@** can now speak"; "VoiceChat.MutedByAdmin" = "Muted by Admin"; "VoiceChat.MutedByAdminHelp" = "Tap if you want to speak"; @@ -6292,7 +6297,277 @@ Sorry for the inconvenience."; "VoiceChat.LeaveConfirmation" = "Are you sure you want to leave this voice chat?"; "VoiceChat.LeaveVoiceChat" = "Leave Voice Chat"; "VoiceChat.LeaveAndEndVoiceChat" = "End Voice Chat"; +"VoiceChat.LeaveAndCancelVoiceChat" = "Abort Voice Chat"; "VoiceChat.ForwardTooltip.Chat" = "Invite link forwarded to **%@**"; "VoiceChat.ForwardTooltip.TwoChats" = "Invite link forwarded to **%@** and **%@**"; "VoiceChat.ForwardTooltip.ManyChats" = "Invite link forwarded to **%@** and %@ others"; + +"GroupRemoved.ViewChannelInfo" = "View Channel"; + +"UserInfo.ContactForwardTooltip.Chat.One" = "Contact forwarded to **%@**"; +"UserInfo.ContactForwardTooltip.TwoChats.One" = "Contact forwarded to **%@** and **%@**"; +"UserInfo.ContactForwardTooltip.ManyChats.One" = "Contact forwarded to **%@** and %@ others"; +"UserInfo.ContactForwardTooltip.SavedMessages.One" = "Contact forwarded to **Saved Messages**"; + +"UserInfo.LinkForwardTooltip.Chat.One" = "Link forwarded to **%@**"; +"UserInfo.LinkForwardTooltip.TwoChats.One" = "Link forwarded to **%@** and **%@**"; +"UserInfo.LinkForwardTooltip.ManyChats.One" = "Link forwarded to **%@** and %@ others"; +"UserInfo.LinkForwardTooltip.SavedMessages.One" = "Link forwarded to **Saved Messages**"; + +"VoiceChat.You" = "this is you"; +"VoiceChat.ChangePhoto" = "Change Photo"; +"VoiceChat.EditBio" = "Edit Bio"; +"VoiceChat.EditBioTitle" = "Bio"; +"VoiceChat.EditBioText" = "Any details such as age, occupation or city."; +"VoiceChat.EditBioPlaceholder" = "Bio"; +"VoiceChat.EditBioSave" = "Save"; +"VoiceChat.EditBioSuccess" = "Your bio is changed."; + +"VoiceChat.EditDescription" = "Edit Description"; +"VoiceChat.EditDescriptionTitle" = "Description"; +"VoiceChat.EditDescriptionText" = "Any details such as age, occupation or city."; +"VoiceChat.EditDescriptionPlaceholder" = "Description"; +"VoiceChat.EditDescriptionSave" = "Save"; +"VoiceChat.EditDescriptionSuccess" = "Description is changed."; + +"VoiceChat.SendPublicLinkText" = "%1$@ isn't a member of \"%2$@\" yet. Send them a public invite link instead?"; +"VoiceChat.SendPublicLinkSend" = "Send"; + +"VoiceChat.TapToAddPhotoOrBio" = "tap to add photo or bio"; +"VoiceChat.TapToAddPhoto" = "tap to add photo"; +"VoiceChat.TapToAddBio" = "tap to add bio"; +"VoiceChat.ImproveYourProfileText" = "You can improve your profile by adding missing information."; + +"VoiceChat.AddPhoto" = "Add Photo"; +"VoiceChat.AddBio" = "Add Bio"; +"VoiceChat.ChangeName" = "Change Name"; +"VoiceChat.ChangeNameTitle" = "Change Name"; +"VoiceChat.EditNameSuccess" = "Your name is changed."; + +"VoiceChat.Video" = "video"; + +"VoiceChat.PinVideo" = "Pin Video"; +"VoiceChat.UnpinVideo" = "Unpin Video"; + +"Notification.VoiceChatScheduledChannel" = "Voice chat scheduled for %@"; +"Notification.VoiceChatScheduled" = "%1$@ scheduled a voice chat for %2$@"; + +"Notification.VoiceChatScheduledTodayChannel" = "Voice chat scheduled for today at %@"; +"Notification.VoiceChatScheduledToday" = "%1$@ scheduled a voice chat for today at %2$@"; + +"Notification.VoiceChatScheduledTomorrowChannel" = "Voice chat scheduled for tomorrow at %@"; +"Notification.VoiceChatScheduledTomorrow" = "%1$@ scheduled a voice chat for tomorrow at %2$@"; + +"VoiceChat.StartsIn" = "Starts in"; +"VoiceChat.LateBy" = "Late by"; + +"VoiceChat.StatusStartsIn" = "starts in %@"; +"VoiceChat.StatusLateBy" = "late by %@"; + +"VoiceChat.Scheduled" = "Scheduled"; + +"VoiceChat.StartNow" = "Start Now"; +"VoiceChat.SetReminder" = "Set Reminder"; +"VoiceChat.CancelReminder" = "Cancel Reminder"; + +"VoiceChat.ShareShort" = "share"; +"VoiceChat.TapToEditTitle" = "Tap to edit title"; + +"ChannelInfo.ScheduleVoiceChat" = "Schedule Voice Chat"; + +"ScheduleVoiceChat.Title" = "Schedule Voice Chat"; +"ScheduleVoiceChat.GroupText" = "The members of the group will be notified that the voice chat will start in %@."; +"ScheduleVoiceChat.ChannelText" = "The members of the channel will be notified that the voice chat will start in %@."; + +"ScheduleVoiceChat.ScheduleToday" = "Start today at %@"; +"ScheduleVoiceChat.ScheduleTomorrow" = "Start tomorrow at %@"; +"ScheduleVoiceChat.ScheduleOn" = "Start on %@ at %@"; + +"Conversation.ScheduledVoiceChat" = "Scheduled Voice Chat"; + +"Conversation.ScheduledVoiceChatStartsOn" = "Voice chat starts on %@"; +"Conversation.ScheduledVoiceChatStartsOnShort" = "Starts on %@"; +"Conversation.ScheduledVoiceChatStartsToday" = "Voice chat starts today at %@"; +"Conversation.ScheduledVoiceChatStartsTodayShort" = "Starts today at %@"; +"Conversation.ScheduledVoiceChatStartsTomorrow" = "Voice chat starts tomorrow at %@"; +"Conversation.ScheduledVoiceChatStartsTomorrowShort" = "Starts tomorrow at %@"; + +"VoiceChat.CancelVoiceChat" = "Abort Voice Chat"; +"VoiceChat.CancelConfirmationTitle" = "Abort Voice Chat"; +"VoiceChat.CancelConfirmationText" = "Do you want to abort the scheduled voice chat?"; +"VoiceChat.CancelConfirmationEnd" = "Abort"; + +"ScheduledIn.Seconds_1" = "%@ second"; +"ScheduledIn.Seconds_2" = "%@ seconds"; +"ScheduledIn.Seconds_3_10" = "%@ seconds"; +"ScheduledIn.Seconds_any" = "%@ seconds"; +"ScheduledIn.Seconds_many" = "%@ seconds"; +"ScheduledIn.Seconds_0" = "%@ seconds"; +"ScheduledIn.Minutes_1" = "%@ minute"; +"ScheduledIn.Minutes_2" = "%@ minutes"; +"ScheduledIn.Minutes_3_10" = "%@ minutes"; +"ScheduledIn.Minutes_any" = "%@ minutes"; +"ScheduledIn.Minutes_many" = "%@ minutes"; +"ScheduledIn.Minutes_0" = "%@ minutes"; +"ScheduledIn.Hours_1" = "%@ hour"; +"ScheduledIn.Hours_2" = "%@ hours"; +"ScheduledIn.Hours_3_10" = "%@ hours"; +"ScheduledIn.Hours_any" = "%@ hours"; +"ScheduledIn.Hours_many" = "%@ hours"; +"ScheduledIn.Hours_0" = "%@ hours"; +"ScheduledIn.Days_1" = "%@ day"; +"ScheduledIn.Days_2" = "%@ days"; +"ScheduledIn.Days_3_10" = "%@ days"; +"ScheduledIn.Days_any" = "%@ days"; +"ScheduledIn.Days_many" = "%@ days"; +"ScheduledIn.Days_0" = "%@ days"; +"ScheduledIn.Weeks_1" = "%@ week"; +"ScheduledIn.Weeks_2" = "%@ weeks"; +"ScheduledIn.Weeks_3_10" = "%@ weeks"; +"ScheduledIn.Weeks_any" = "%@ weeks"; +"ScheduledIn.Weeks_many" = "%@ weeks"; +"ScheduledIn.Weeks_0" = "%@ weeks"; +"ScheduledIn.Months_1" = "%@ month"; +"ScheduledIn.Months_2" = "%@ months"; +"ScheduledIn.Months_3_10" = "%@ months"; +"ScheduledIn.Months_any" = "%@ months"; +"ScheduledIn.Months_many" = "%@ months"; +"ScheduledIn.Months_0" = "%@ months"; +"ScheduledIn.Years_1" = "%@ year"; +"ScheduledIn.Years_2" = "%@ years"; +"ScheduledIn.Years_3_10" = "%@ years"; +"ScheduledIn.Years_any" = "%@ years"; +"ScheduledIn.Months_many" = "%@ years"; + +"Checkout.PaymentLiabilityAlert" = "Neither Telegram, nor {target} will have access to your credit card information. Credit card details will be handled only by the payment system, {payment_system}.\n\nPayments will go directly to the developer of {target}. Telegram cannot provide any guarantees, so proceed at your own risk. In case of problems, please contact the developer of {target} or your bank."; + +"Checkout.OptionalTipItem" = "Tip (Optional)"; +"Checkout.TipItem" = "Tip"; +"Checkout.OptionalTipItemPlaceholder" = "Enter Custom"; + +"VoiceChat.ReminderNotify" = "We will notify you when it starts."; + +"Checkout.SuccessfulTooltip" = "You paid %1$@ for %2$@."; + +"Privacy.ContactsReset.ContactsDeleted" = "All synced contacts deleted."; + +"Privacy.DeleteDrafts.DraftsDeleted" = "All cloud drafts deleted."; + +"Privacy.PaymentsClear.PaymentInfoCleared" = "Payment info cleared."; +"Privacy.PaymentsClear.ShippingInfoCleared" = "Shipping info cleared."; +"Privacy.PaymentsClear.AllInfoCleared" = "Payment and shipping info cleared."; + +"Settings.Tips" = "Telegram Features"; +"Settings.TipsUsername" = "TelegramTips"; + +"Calls.NoVoiceAndVideoCallsPlaceholder" = "Your recent voice and video calls will appear here."; +"Calls.StartNewCall" = "Start New Call"; + +"VoiceChat.VideoPreviewTitle" = "Video Preview"; +"VoiceChat.VideoPreviewDescription" = "Are you sure you want to share your video?"; +"VoiceChat.VideoPreviewShareCamera" = "Share Camera Video"; +"VoiceChat.VideoPreviewShareScreen" = "Share Screen"; +"VoiceChat.VideoPreviewStopScreenSharing" = "Stop Screen Sharing"; + +"VoiceChat.TapToViewCameraVideo" = "Tap to view camera video"; +"VoiceChat.TapToViewScreenVideo" = "Tap to view screen sharing"; + +"VoiceChat.ShareScreen" = "Share Screen"; +"VoiceChat.StopScreenSharing" = "Stop Screen Sharing"; +"VoiceChat.ParticipantIsSpeaking" = "%1$@ is speaking"; + +"WallpaperPreview.WallpaperColors" = "Colors"; + +"VoiceChat.UnmuteSuggestion" = "You are on mute. Tap here to speak."; + +"VoiceChat.ContextAudio" = "Audio"; + +"VoiceChat.VideoPaused" = "Video is paused"; +"VoiceChat.YouAreSharingScreen" = "You are sharing your screen"; +"VoiceChat.StopScreenSharingShort" = "Stop Sharing"; + +"VoiceChat.OpenGroup" = "Open Group"; + +"VoiceChat.NoiseSuppression" = "Noise Suppression"; +"VoiceChat.NoiseSuppressionEnabled" = "Enabled"; +"VoiceChat.NoiseSuppressionDisabled" = "Disabled"; + +"VoiceChat.Unpin" = "Unpin"; + +"VoiceChat.VideoParticipantsLimitExceeded" = "Video is only available\nfor the first %@ members"; + +"ImportStickerPack.StickerCount_1" = "1 Sticker"; +"ImportStickerPack.StickerCount_2" = "2 Stickers"; +"ImportStickerPack.StickerCount_3_10" = "%@ Stickers"; +"ImportStickerPack.StickerCount_any" = "%@ Stickers"; +"ImportStickerPack.StickerCount_many" = "%@ Stickers"; +"ImportStickerPack.StickerCount_0" = "%@ Stickers"; +"ImportStickerPack.CreateStickerSet" = "Create Sticker Set"; +"ImportStickerPack.CreateNewStickerSet" = "Create a New Sticker Set"; +"ImportStickerPack.AddToExistingStickerSet" = "Add to an Existing Sticker Set"; +"ImportStickerPack.ChooseStickerSet" = "Choose Sticker Set"; +"ImportStickerPack.RemoveFromImport" = "Remove From Import"; +"ImportStickerPack.ChooseName" = "Choose Name"; +"ImportStickerPack.ChooseNameDescription" = "Please choose a name for your set."; +"ImportStickerPack.NamePlaceholder" = "Name"; +"ImportStickerPack.GeneratingLink" = "generating link..."; +"ImportStickerPack.CheckingLink" = "checking availability..."; +"ImportStickerPack.ChooseLink" = "Choose Link"; +"ImportStickerPack.ChooseLinkDescription" = "You can use a-z, 0-9 and underscores."; +"ImportStickerPack.LinkTaken" = "Sorry, this link is already taken."; +"ImportStickerPack.LinkAvailable" = "Link is available."; +"ImportStickerPack.ImportingStickers" = "Importing Stickers"; +"ImportStickerPack.Of" = "%1$@ of %2$@ Imported"; +"ImportStickerPack.InProgress" = "Please keep this window open\nuntil the import is completed."; +"ImportStickerPack.Create" = "Create"; + +"WallpaperPreview.PreviewBottomTextAnimatable" = "Tap the play button to view the background animation."; + +"Conversation.InputMenu" = "Menu"; +"Conversation.MessageDoesntExist" = "Message doesn't exist"; + +"Settings.CheckPasswordTitle" = "Your Password"; +"Settings.CheckPasswordText" = "Your account is protected by 2-Step Verification. Do you still remember your password?"; +"Settings.KeepPassword" = "Yes, definitely"; +"Settings.TryEnterPassword" = "Not sure, let me try"; + +"TwoFactorSetup.PasswordRecovery.Title" = "Create New Password"; +"TwoFactorSetup.PasswordRecovery.Text" = "You can now set a new password that will be used to log into your account."; +"TwoFactorSetup.PasswordRecovery.PlaceholderPassword" = "New Password"; +"TwoFactorSetup.PasswordRecovery.PlaceholderConfirmPassword" = "Re-enter New Password"; +"TwoFactorSetup.PasswordRecovery.Action" = "Continue"; +"TwoFactorSetup.PasswordRecovery.Skip" = "Skip"; +"TwoFactorSetup.PasswordRecovery.SkipAlertTitle" = "Attention!"; +"TwoFactorSetup.PasswordRecovery.SkipAlertText" = "Skipping this step will disable 2-step verification for your account. Are you sure you want to skip?"; +"TwoFactorSetup.PasswordRecovery.SkipAlertAction" = "Skip"; + +"TwoStepAuth.RecoveryUnavailableResetTitle" = "Reset Password"; +"TwoStepAuth.RecoveryUnavailableResetText" = "Since you didn’t provide a recovery email when setting up your password, your remaining options are either to remember your password or wait 7 days until your password is reset."; +"TwoStepAuth.RecoveryEmailResetText" = "If you don't have access to your recovery email, your remaining options are either to remember your password or wait 7 days until your password resets."; +"TwoStepAuth.RecoveryUnavailableResetAction" = "Reset"; +"TwoStepAuth.ResetPendingText" = "You can reset your password in %@."; +"TwoStepAuth.CancelResetTitle" = "Cancel Reset"; +"TwoStepAuth.ResetAction" = "Reset Password"; +"TwoStepAuth.CancelResetText" = "Cancel the password reset process? If you request a new reset later, it will take another 7 days."; +"TwoStepAuth.RecoveryEmailResetNoAccess" = "Can’t access your email?"; + +"TwoFactorSetup.ResetDone.Title" = "New Password Set!"; +"TwoFactorSetup.ResetDone.Text" = "This password will be required when you log in on a new device in addition to the code you get via SMS."; +"TwoFactorSetup.ResetDone.Action" = "Continue"; + +"TwoFactorSetup.ResetDone.TitleNoPassword" = "Password Removed"; +"TwoFactorSetup.ResetDone.TextNoPassword" = "You can always set a new password in\n\n\nSettings>Privacy & Security>Two-Step Verification"; + +"TwoFactorSetup.ResetFloodWait" = "You recently requested a password reset that was cancelled. Please wait %@ before making a new request."; +"TwoFactorSetup.ResetFloodWait" = "You have recently requested a password reset that was canceled. Please wait for %@ before making a new request."; + +"TwoFactorRemember.Title" = "Enter Your Password"; +"TwoFactorRemember.Text" = "Do you still remeber your password?"; +"TwoFactorRemember.Placeholder" = "Password"; +"TwoFactorRemember.Forgot" = "Forgot Password?"; +"TwoFactorRemember.CheckPassword" = "Check Password"; +"TwoFactorRemember.WrongPassword" = "This password is incorrect."; +"TwoFactorRemember.Done.Title" = "Perfect!"; +"TwoFactorRemember.Done.Text" = "You still remember your password."; +"TwoFactorRemember.Done.Action" = "Back to Settings"; diff --git a/Telegram/Widget/Info.plist b/Telegram/Widget/Info.plist deleted file mode 100644 index 6917b6ac76..0000000000 --- a/Telegram/Widget/Info.plist +++ /dev/null @@ -1,31 +0,0 @@ - - - - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleDisplayName - ${APP_NAME} - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - XPC! - CFBundleShortVersionString - $(PRODUCT_BUNDLE_SHORT_VERSION) - CFBundleVersion - ${BUILD_NUMBER} - NSExtension - - NSExtensionPointIdentifier - com.apple.widget-extension - NSExtensionPrincipalClass - TodayViewController - - - diff --git a/Telegram/Widget/PeerNode.swift b/Telegram/Widget/PeerNode.swift deleted file mode 100644 index b89deba9e6..0000000000 --- a/Telegram/Widget/PeerNode.swift +++ /dev/null @@ -1,172 +0,0 @@ -import Foundation -import UIKit -import WidgetItems - -private extension UIColor { - convenience init(rgb: UInt32) { - self.init(red: CGFloat((rgb >> 16) & 0xff) / 255.0, green: CGFloat((rgb >> 8) & 0xff) / 255.0, blue: CGFloat(rgb & 0xff) / 255.0, alpha: 1.0) - } -} - -private let UIScreenScale = UIScreen.main.scale -private func floorToScreenPixels(_ value: CGFloat) -> CGFloat { - return floor(value * UIScreenScale) / UIScreenScale -} - -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 avatarRoundImage(size: CGSize, source: UIImage) -> UIImage? { - UIGraphicsBeginImageContextWithOptions(size, false, 0.0) - let context = UIGraphicsGetCurrentContext() - - context?.beginPath() - context?.addEllipse(in: CGRect(x: 0.0, y: 0.0, width: size.width, height: size.height)) - context?.clip() - - source.draw(in: CGRect(origin: CGPoint(), size: size)) - - let image = UIGraphicsGetImageFromCurrentImageContext() - UIGraphicsEndImageContext() - return image -} - -private let deviceColorSpace: CGColorSpace = { - if #available(iOSApplicationExtension 9.3, *) { - if let colorSpace = CGColorSpace(name: CGColorSpace.displayP3) { - return colorSpace - } else { - return CGColorSpaceCreateDeviceRGB() - } - } else { - return CGColorSpaceCreateDeviceRGB() - } -}() - -private func avatarViewLettersImage(size: CGSize, peerId: Int64, accountPeerId: Int64, letters: [String]) -> UIImage? { - UIGraphicsBeginImageContextWithOptions(size, false, 0.0) - let context = UIGraphicsGetCurrentContext() - - context?.beginPath() - context?.addEllipse(in: CGRect(x: 0.0, y: 0.0, width: size.width, height: size.height)) - context?.clip() - - let colorIndex = abs(Int(accountPeerId + peerId)) - - let colorsArray = gradientColors[colorIndex % gradientColors.count] - var locations: [CGFloat] = [1.0, 0.0] - let gradient = CGGradient(colorsSpace: deviceColorSpace, colors: colorsArray, locations: &locations)! - - context?.drawLinearGradient(gradient, start: CGPoint(), end: CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions()) - - context?.setBlendMode(.normal) - - let string = letters.count == 0 ? "" : (letters[0] + (letters.count == 1 ? "" : letters[1])) - let attributedString = NSAttributedString(string: string, attributes: [NSAttributedString.Key.font: UIFont.systemFont(ofSize: 20.0), NSAttributedString.Key.foregroundColor: UIColor.white]) - - let line = CTLineCreateWithAttributedString(attributedString) - let lineBounds = CTLineGetBoundsWithOptions(line, .useGlyphPathBounds) - - let lineOffset = CGPoint(x: string == "B" ? 1.0 : 0.0, y: 0.0) - let lineOrigin = CGPoint(x: floorToScreenPixels(-lineBounds.origin.x + (size.width - lineBounds.size.width) / 2.0) + lineOffset.x, y: floorToScreenPixels(-lineBounds.origin.y + (size.height - lineBounds.size.height) / 2.0)) - - 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?.translateBy(x: lineOrigin.x, y: lineOrigin.y) - if let context = context { - CTLineDraw(line, context) - } - context?.translateBy(x: -lineOrigin.x, y: -lineOrigin.y) - - let image = UIGraphicsGetImageFromCurrentImageContext() - UIGraphicsEndImageContext() - return image -} - -private let avatarSize = CGSize(width: 50.0, height: 50.0) - -private final class AvatarView: UIImageView { - init(accountPeerId: Int64, peer: WidgetDataPeer, size: CGSize) { - super.init(frame: CGRect()) - - if let path = peer.avatarPath, let image = UIImage(contentsOfFile: path), let roundImage = avatarRoundImage(size: size, source: image) { - self.image = roundImage - } else { - self.image = avatarViewLettersImage(size: size, peerId: peer.id, accountPeerId: accountPeerId, letters: peer.letters) - } - } - - required init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } -} - -final class PeerView: UIView { - let peer: WidgetDataPeer - private let avatarView: AvatarView - private let titleLabel: UILabel - - private let tapped: () -> Void - - init(primaryColor: UIColor, accountPeerId: Int64, peer: WidgetDataPeer, tapped: @escaping () -> Void) { - self.peer = peer - self.tapped = tapped - self.avatarView = AvatarView(accountPeerId: accountPeerId, peer: peer, size: avatarSize) - - self.titleLabel = UILabel() - 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 - 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()) - - self.addSubview(self.avatarView) - self.addSubview(self.titleLabel) - - self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) - } - - required init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - func updateLayout(size: CGSize) { - self.avatarView.frame = CGRect(origin: CGPoint(x: floor((size.width - avatarSize.width) / 2.0), y: 0.0), size: avatarSize) - - var titleSize = self.titleLabel.sizeThatFits(size) - titleSize.width = min(size.width - 6.0, ceil(titleSize.width)) - titleSize.height = ceil(titleSize.height) - self.titleLabel.frame = CGRect(origin: CGPoint(x: floor((size.width - titleSize.width) / 2.0), y: avatarSize.height + 5.0), size: titleSize) - } - - @objc func tapGesture(_ recognizer: UITapGestureRecognizer) { - if case .ended = recognizer.state { - self.tapped() - } - } -} diff --git a/Telegram/Widget/TodayViewController.swift b/Telegram/Widget/TodayViewController.swift deleted file mode 100644 index 454691e89d..0000000000 --- a/Telegram/Widget/TodayViewController.swift +++ /dev/null @@ -1,171 +0,0 @@ -import UIKit -import NotificationCenter -import BuildConfig -import WidgetItems -import AppLockState - -private func rootPathForBasePath(_ appGroupPath: String) -> String { - return appGroupPath + "/telegram-data" -} - -@objc(TodayViewController) -class TodayViewController: UIViewController, NCWidgetProviding { - private var initializedInterface = false - - private var buildConfig: BuildConfig? - - private var primaryColor: UIColor = .black - private var placeholderLabel: UILabel? - - override func viewDidLoad() { - super.viewDidLoad() - - let appBundleIdentifier = Bundle.main.bundleIdentifier! - guard let lastDotRange = appBundleIdentifier.range(of: ".", options: [.backwards]) else { - return - } - let baseAppBundleId = String(appBundleIdentifier[.. Void)) { - completionHandler(.newData) - } - - @available(iOSApplicationExtension 10.0, *) - func widgetActiveDisplayModeDidChange(_ activeDisplayMode: NCWidgetDisplayMode, withMaximumSize maxSize: CGSize) { - - } - - private var widgetData: WidgetData? - - private func setWidgetData(widgetData: WidgetData, presentationData: WidgetPresentationData) { - self.widgetData = widgetData - self.peerViews.forEach { - $0.removeFromSuperview() - } - self.peerViews = [] - switch widgetData { - case .notAuthorized, .disabled: - break - case let .peers(peers): - for peer in peers.peers { - let peerView = PeerView(primaryColor: self.primaryColor, accountPeerId: peers.accountPeerId, peer: peer, tapped: { [weak self] in - if let strongSelf = self, let buildConfig = strongSelf.buildConfig { - if let url = URL(string: "\(buildConfig.appSpecificUrlScheme)://localpeer?id=\(peer.id)") { - strongSelf.extensionContext?.open(url, completionHandler: nil) - } - } - }) - self.view.addSubview(peerView) - self.peerViews.append(peerView) - } - } - - if self.peerViews.isEmpty { - self.setPlaceholderText(presentationData.applicationStartRequiredString) - } else { - self.placeholderLabel?.removeFromSuperview() - self.placeholderLabel = nil - } - - if let size = self.validLayout { - self.updateLayout(size: size) - } - } - - private var validLayout: CGSize? - - private var peerViews: [PeerView] = [] - - override func viewDidLayoutSubviews() { - super.viewDidLayoutSubviews() - - self.updateLayout(size: self.view.bounds.size) - } - - private func updateLayout(size: CGSize) { - self.validLayout = size - - if let placeholderLabel = self.placeholderLabel { - placeholderLabel.frame = CGRect(origin: CGPoint(x: floor((size.width - placeholderLabel.bounds.width) / 2.0), y: floor((size.height - placeholderLabel.bounds.height) / 2.0)), size: placeholderLabel.bounds.size) - } - - let peerSize = CGSize(width: 70.0, height: 100.0) - - var peerFrames: [CGRect] = [] - - var offset: CGFloat = 0.0 - for _ in self.peerViews { - let peerFrame = CGRect(origin: CGPoint(x: offset, y: 10.0), size: peerSize) - offset += peerFrame.size.width - if peerFrame.maxX > size.width { - break - } - peerFrames.append(peerFrame) - } - - var totalSize: CGFloat = 0.0 - for i in 0 ..< peerFrames.count { - totalSize += peerFrames[i].width - } - - let spacing: CGFloat = floor((size.width - totalSize) / CGFloat(peerFrames.count)) - offset = floor(spacing / 2.0) - for i in 0 ..< peerFrames.count { - let peerView = self.peerViews[i] - 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/Telegram/Widget/Widget-Bridging-Header.h b/Telegram/Widget/Widget-Bridging-Header.h deleted file mode 100644 index 16747def3f..0000000000 --- a/Telegram/Widget/Widget-Bridging-Header.h +++ /dev/null @@ -1,4 +0,0 @@ -#ifndef Widget_Bridging_Header_h -#define Widget_Bridging_Header_h - -#endif diff --git a/Telegram/Widget/ar.lproj/InfoPlist.strings b/Telegram/Widget/ar.lproj/InfoPlist.strings deleted file mode 100644 index 07394dd6c9..0000000000 --- a/Telegram/Widget/ar.lproj/InfoPlist.strings +++ /dev/null @@ -1 +0,0 @@ -"CFBundleDisplayName" = "الأشخاص"; diff --git a/Telegram/Widget/de.lproj/InfoPlist.strings b/Telegram/Widget/de.lproj/InfoPlist.strings deleted file mode 100644 index 1ad24433c2..0000000000 --- a/Telegram/Widget/de.lproj/InfoPlist.strings +++ /dev/null @@ -1 +0,0 @@ -"CFBundleDisplayName" = "Leute"; diff --git a/Telegram/Widget/en.lproj/InfoPlist.strings b/Telegram/Widget/en.lproj/InfoPlist.strings deleted file mode 100644 index b1ddd1ac39..0000000000 --- a/Telegram/Widget/en.lproj/InfoPlist.strings +++ /dev/null @@ -1 +0,0 @@ -"CFBundleDisplayName" = "People"; diff --git a/Telegram/Widget/en.lproj/Localizable.strings b/Telegram/Widget/en.lproj/Localizable.strings deleted file mode 100644 index c90696d0fb..0000000000 --- a/Telegram/Widget/en.lproj/Localizable.strings +++ /dev/null @@ -1,2 +0,0 @@ -"Widget.NoUsers" = "No users here yet..."; -"Widget.AuthRequired" = "Open Telegram and log in."; diff --git a/Telegram/Widget/es.lproj/InfoPlist.strings b/Telegram/Widget/es.lproj/InfoPlist.strings deleted file mode 100644 index 3d5094963a..0000000000 --- a/Telegram/Widget/es.lproj/InfoPlist.strings +++ /dev/null @@ -1 +0,0 @@ -"CFBundleDisplayName" = "Personas"; diff --git a/Telegram/Widget/it.lproj/InfoPlist.strings b/Telegram/Widget/it.lproj/InfoPlist.strings deleted file mode 100644 index f118d25a4d..0000000000 --- a/Telegram/Widget/it.lproj/InfoPlist.strings +++ /dev/null @@ -1 +0,0 @@ -"CFBundleDisplayName" = "Persone"; diff --git a/Telegram/Widget/ko.lproj/InfoPlist.strings b/Telegram/Widget/ko.lproj/InfoPlist.strings deleted file mode 100644 index e1bc831c53..0000000000 --- a/Telegram/Widget/ko.lproj/InfoPlist.strings +++ /dev/null @@ -1 +0,0 @@ -"CFBundleDisplayName" = "사람"; diff --git a/Telegram/Widget/nl.lproj/InfoPlist.strings b/Telegram/Widget/nl.lproj/InfoPlist.strings deleted file mode 100644 index a23cbfc4a2..0000000000 --- a/Telegram/Widget/nl.lproj/InfoPlist.strings +++ /dev/null @@ -1 +0,0 @@ -"CFBundleDisplayName" = "Mensen"; diff --git a/Telegram/Widget/pt.lproj/InfoPlist.strings b/Telegram/Widget/pt.lproj/InfoPlist.strings deleted file mode 100644 index a6c032d0ed..0000000000 --- a/Telegram/Widget/pt.lproj/InfoPlist.strings +++ /dev/null @@ -1 +0,0 @@ -"CFBundleDisplayName" = "Pessoas"; diff --git a/Telegram/Widget/ru.lproj/InfoPlist.strings b/Telegram/Widget/ru.lproj/InfoPlist.strings deleted file mode 100644 index 689e714f47..0000000000 --- a/Telegram/Widget/ru.lproj/InfoPlist.strings +++ /dev/null @@ -1 +0,0 @@ -"CFBundleDisplayName" = "Люди"; diff --git a/Telegram/WidgetKitWidget/TodayViewController.swift b/Telegram/WidgetKitWidget/TodayViewController.swift index efa8b4798f..943953cce4 100644 --- a/Telegram/WidgetKitWidget/TodayViewController.swift +++ b/Telegram/WidgetKitWidget/TodayViewController.swift @@ -92,7 +92,7 @@ private func getCommonTimeline(friends: [Friend]?, in context: TimelineProviderC let rootPath = rootPathForBasePath(appGroupUrl.path) - TempBox.initializeShared(basePath: rootPath, processType: "widget", launchSpecificId: arc4random64()) + TempBox.initializeShared(basePath: rootPath, processType: "widget", launchSpecificId: Int64.random(in: Int64.min ... Int64.max)) let logsPath = rootPath + "/widget-logs" let _ = try? FileManager.default.createDirectory(atPath: logsPath, withIntermediateDirectories: true, attributes: nil) @@ -130,7 +130,7 @@ private func getCommonTimeline(friends: [Friend]?, in context: TimelineProviderC var friendsByAccount: [Signal<[ParsedPeer], NoError>] = [] for (accountId, items) in itemsByAccount { - friendsByAccount.append(accountTransaction(rootPath: rootPath, id: AccountRecordId(rawValue: accountId), encryptionParameters: encryptionParameters, isReadOnly: true, useCopy: true, transaction: { postbox, transaction -> [ParsedPeer] in + friendsByAccount.append(accountTransaction(rootPath: rootPath, id: AccountRecordId(rawValue: accountId), encryptionParameters: encryptionParameters, isReadOnly: true, useCopy: false, transaction: { postbox, transaction -> [ParsedPeer] in guard let state = transaction.getState() as? AuthorizedAccountState else { return [] } diff --git a/WORKSPACE b/WORKSPACE index a270359be7..a41ed5cce6 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -56,3 +56,10 @@ http_file( urls = ["https://github.com/Kitware/CMake/releases/download/v3.19.2/cmake-3.19.2-macos-universal.tar.gz"], sha256 = "50afa2cb66bea6a0314ef28034f3ff1647325e30cf5940f97906a56fd9640bd8", ) + +http_archive( + name = "appcenter_sdk", + urls = ["https://github.com/microsoft/appcenter-sdk-apple/releases/download/4.1.1/AppCenter-SDK-Apple-4.1.1.zip"], + sha256 = "032907801dc7784744a1ca8fd40d3eecc34a2e27a93a4b3993f617cca204a9f3", + build_file = "@//third-party/AppCenter:AppCenter.BUILD", +) diff --git a/build-system/Make/Make.py b/build-system/Make/Make.py index 20ad3024c3..1ab6a9176e 100644 --- a/build-system/Make/Make.py +++ b/build-system/Make/Make.py @@ -106,6 +106,15 @@ class BazelCommandLine: def set_build_number(self, build_number): self.build_number = build_number + def set_custom_target(self, target_name): + self.custom_target = target_name + + def set_continue_on_error(self, continue_on_error): + self.continue_on_error = continue_on_error + + def set_enable_sandbox(self, enable_sandbox): + self.enable_sandbox = enable_sandbox + def set_split_swiftmodules(self, value): self.split_submodules = value @@ -260,10 +269,18 @@ class BazelCommandLine: self.build_environment.bazel_path ] combined_arguments += self.get_startup_bazel_arguments() - combined_arguments += [ - 'build', - 'Telegram/Telegram' - ] + combined_arguments += ['build'] + + if self.custom_target is not None: + combined_arguments += [self.custom_target] + else: + combined_arguments += ['Telegram/Telegram'] + + if self.continue_on_error: + combined_arguments += ['--keep_going'] + + if self.enable_sandbox: + combined_arguments += ['--spawn_strategy=sandboxed'] if self.configuration_path is None: raise Exception('configuration_path is not defined') @@ -353,10 +370,15 @@ def generate_project(arguments): bazel_command_line.set_build_number(arguments.buildNumber) disable_extensions = False + disable_provisioning_profiles = False + generate_dsym = False + if arguments.disableExtensions is not None: disable_extensions = arguments.disableExtensions if arguments.disableProvisioningProfiles is not None: disable_provisioning_profiles = arguments.disableProvisioningProfiles + if arguments.generateDsym is not None: + generate_dsym = arguments.generateDsym call_executable(['killall', 'Xcode'], check_result=False) @@ -364,6 +386,7 @@ def generate_project(arguments): build_environment=bazel_command_line.build_environment, disable_extensions=disable_extensions, disable_provisioning_profiles=disable_provisioning_profiles, + generate_dsym=generate_dsym, configuration_path=bazel_command_line.configuration_path, bazel_app_arguments=bazel_command_line.get_project_generation_arguments() ) @@ -386,6 +409,9 @@ def build(arguments): bazel_command_line.set_configuration(arguments.configuration) bazel_command_line.set_build_number(arguments.buildNumber) + bazel_command_line.set_custom_target(arguments.target) + bazel_command_line.set_continue_on_error(arguments.continueOnError) + bazel_command_line.set_enable_sandbox(arguments.sandbox) bazel_command_line.set_split_swiftmodules(not arguments.disableParallelSwiftmoduleGeneration) @@ -512,6 +538,15 @@ if __name__ == '__main__': ''' ) + generateProjectParser.add_argument( + '--generateDsym', + action='store_true', + default=False, + help=''' + This improves profiling experinence by generating DSYM files. Keep disabled for better build performance. + ''' + ) + buildParser = subparsers.add_parser('build', help='Build the app') buildParser.add_argument( '--buildNumber', @@ -540,6 +575,24 @@ if __name__ == '__main__': default=False, help='Generate .swiftmodule files in parallel to building modules, can speed up compilation on multi-core systems.' ) + buildParser.add_argument( + '--target', + type=str, + help='A custom bazel target name to build.', + metavar='target_name' + ) + buildParser.add_argument( + '--continueOnError', + action='store_true', + default=False, + help='Continue build process after an error.', + ) + buildParser.add_argument( + '--sandbox', + action='store_true', + default=False, + help='Enable sandbox.', + ) if len(sys.argv) < 2: parser.print_help() diff --git a/build-system/Make/ProjectGeneration.py b/build-system/Make/ProjectGeneration.py index 44629edbfb..8c8fab3ba8 100644 --- a/build-system/Make/ProjectGeneration.py +++ b/build-system/Make/ProjectGeneration.py @@ -10,7 +10,7 @@ def remove_directory(path): shutil.rmtree(path) -def generate(build_environment: BuildEnvironment, disable_extensions, disable_provisioning_profiles, configuration_path, bazel_app_arguments): +def generate(build_environment: BuildEnvironment, disable_extensions, disable_provisioning_profiles, generate_dsym, configuration_path, bazel_app_arguments): project_path = os.path.join(build_environment.base_path, 'build-input/gen/project') app_target = 'Telegram' @@ -81,6 +81,8 @@ def generate(build_environment: BuildEnvironment, disable_extensions, disable_pr bazel_build_arguments += ['--//Telegram:disableExtensions'] if disable_provisioning_profiles: bazel_build_arguments += ['--//Telegram:disableProvisioningProfiles'] + if generate_dsym: + bazel_build_arguments += ['--apple_generate_dsym'] call_executable([ tulsi_path, diff --git a/build-system/example-configuration/provisioning/BUILD b/build-system/example-configuration/provisioning/BUILD index 9abb88a19d..8da09832d1 100644 --- a/build-system/example-configuration/provisioning/BUILD +++ b/build-system/example-configuration/provisioning/BUILD @@ -8,4 +8,5 @@ exports_files([ "WatchApp.mobileprovision", "WatchExtension.mobileprovision", "Widget.mobileprovision", + "BroadcastUpload.mobileprovision", ]) diff --git a/build-system/example-configuration/provisioning/BroadcastUpload.mobileprovision b/build-system/example-configuration/provisioning/BroadcastUpload.mobileprovision new file mode 100644 index 0000000000..1d29a80307 Binary files /dev/null and b/build-system/example-configuration/provisioning/BroadcastUpload.mobileprovision differ diff --git a/build-system/fake-codesigning/profiles/appstore/AppStore_ph.telegra.Telegraph.BroadcastUpload.mobileprovision b/build-system/fake-codesigning/profiles/appstore/AppStore_ph.telegra.Telegraph.BroadcastUpload.mobileprovision new file mode 100644 index 0000000000..1d29a80307 Binary files /dev/null and b/build-system/fake-codesigning/profiles/appstore/AppStore_ph.telegra.Telegraph.BroadcastUpload.mobileprovision differ diff --git a/build_number_offset b/build_number_offset new file mode 100644 index 0000000000..0c80f59274 --- /dev/null +++ b/build_number_offset @@ -0,0 +1 @@ +2300 diff --git a/buildbox/build-telegram.sh b/buildbox/build-telegram.sh index 70b300f4ae..f2bc29956c 100644 --- a/buildbox/build-telegram.sh +++ b/buildbox/build-telegram.sh @@ -79,7 +79,8 @@ 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) - COMMIT_COUNT="$(($COMMIT_COUNT+2000))" + BUILD_NUMBER_OFFSET="$(cat build_number_offset)" + COMMIT_COUNT="$(($COMMIT_COUNT+$BUILD_NUMBER_OFFSET))" BUILD_NUMBER="$COMMIT_COUNT" else BUILD_NUMBER="$2" diff --git a/buildbox/deploy-appcenter.sh b/buildbox/deploy-appcenter.sh index 7519ef5b18..141a43bf48 100644 --- a/buildbox/deploy-appcenter.sh +++ b/buildbox/deploy-appcenter.sh @@ -3,84 +3,11 @@ 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" -} - APPCENTER="/usr/local/bin/appcenter" $APPCENTER login --token "$API_TOKEN" $APPCENTER distribute release --app "$API_USER_NAME/$API_APP_NAME" -f "$IPA_PATH" -g Internal +$APPCENTER crashes upload-symbols --app "$API_USER_NAME/$API_APP_NAME" --symbol "$DSYM_PATH" diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index ec99a82e9a..abbaafa7c3 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -30,6 +30,7 @@ public enum AccessType { public final class TelegramApplicationBindings { public let isMainApp: Bool + public let appBundleId: String public let containerPath: String public let appSpecificScheme: String public let openUrl: (String) -> Void @@ -52,9 +53,11 @@ public final class TelegramApplicationBindings { public let getAvailableAlternateIcons: () -> [PresentationAppIcon] public let getAlternateIconName: () -> String? public let requestSetAlternateIconName: (String?, @escaping (Bool) -> Void) -> Void + public let forceOrientation: (UIInterfaceOrientation) -> Void - public init(isMainApp: Bool, containerPath: String, appSpecificScheme: String, openUrl: @escaping (String) -> Void, openUniversalUrl: @escaping (String, TelegramApplicationOpenUrlCompletion) -> Void, canOpenUrl: @escaping (String) -> Bool, getTopWindow: @escaping () -> UIWindow?, displayNotification: @escaping (String) -> Void, applicationInForeground: Signal, applicationIsActive: Signal, clearMessageNotifications: @escaping ([MessageId]) -> Void, pushIdleTimerExtension: @escaping () -> Disposable, openSettings: @escaping () -> Void, openAppStorePage: @escaping () -> Void, registerForNotifications: @escaping (@escaping (Bool) -> Void) -> Void, requestSiriAuthorization: @escaping (@escaping (Bool) -> Void) -> Void, siriAuthorization: @escaping () -> AccessType, getWindowHost: @escaping () -> WindowHost?, presentNativeController: @escaping (UIViewController) -> Void, dismissNativeController: @escaping () -> Void, getAvailableAlternateIcons: @escaping () -> [PresentationAppIcon], getAlternateIconName: @escaping () -> String?, requestSetAlternateIconName: @escaping (String?, @escaping (Bool) -> Void) -> Void) { + public init(isMainApp: Bool, appBundleId: String, containerPath: String, appSpecificScheme: String, openUrl: @escaping (String) -> Void, openUniversalUrl: @escaping (String, TelegramApplicationOpenUrlCompletion) -> Void, canOpenUrl: @escaping (String) -> Bool, getTopWindow: @escaping () -> UIWindow?, displayNotification: @escaping (String) -> Void, applicationInForeground: Signal, applicationIsActive: Signal, clearMessageNotifications: @escaping ([MessageId]) -> Void, pushIdleTimerExtension: @escaping () -> Disposable, openSettings: @escaping () -> Void, openAppStorePage: @escaping () -> Void, registerForNotifications: @escaping (@escaping (Bool) -> Void) -> Void, requestSiriAuthorization: @escaping (@escaping (Bool) -> Void) -> Void, siriAuthorization: @escaping () -> AccessType, getWindowHost: @escaping () -> WindowHost?, presentNativeController: @escaping (UIViewController) -> Void, dismissNativeController: @escaping () -> Void, getAvailableAlternateIcons: @escaping () -> [PresentationAppIcon], getAlternateIconName: @escaping () -> String?, requestSetAlternateIconName: @escaping (String?, @escaping (Bool) -> Void) -> Void, forceOrientation: @escaping (UIInterfaceOrientation) -> Void) { self.isMainApp = isMainApp + self.appBundleId = appBundleId self.containerPath = containerPath self.appSpecificScheme = appSpecificScheme self.openUrl = openUrl @@ -77,6 +80,7 @@ public final class TelegramApplicationBindings { self.getAvailableAlternateIcons = getAvailableAlternateIcons self.getAlternateIconName = getAlternateIconName self.requestSetAlternateIconName = requestSetAlternateIconName + self.forceOrientation = forceOrientation } } @@ -150,9 +154,9 @@ public struct ChatAvailableMessageActions { } public enum WallpaperUrlParameter { - case slug(String, WallpaperPresentationOptions, UIColor?, UIColor?, Int32?, Int32?) + case slug(String, WallpaperPresentationOptions, [UInt32], Int32?, Int32?) case color(UIColor) - case gradient(UIColor, UIColor, Int32?) + case gradient([UInt32], Int32?) } public enum ResolvedUrlSettingsSection { @@ -167,7 +171,7 @@ public enum ResolvedUrl { case inaccessiblePeer case botStart(peerId: PeerId, payload: String) case groupBotStart(peerId: PeerId, payload: String) - case channelMessage(peerId: PeerId, messageId: MessageId) + case channelMessage(peerId: PeerId, messageId: MessageId, timecode: Double?) case replyThreadMessage(replyThreadMessage: ChatReplyThreadMessage, messageId: MessageId) case stickerPack(name: String) case instantView(TelegramMediaWebpage, String?) @@ -184,6 +188,7 @@ public enum ResolvedUrl { #endif case settings(ResolvedUrlSettingsSection) case joinVoiceChat(PeerId, String?) + case importStickers } public enum NavigateToChatKeepStack { @@ -216,17 +221,14 @@ public final class ChatPeerNearbyData: Equatable { public final class ChatGreetingData: Equatable { public static func == (lhs: ChatGreetingData, rhs: ChatGreetingData) -> Bool { - if let lhsSticker = lhs.sticker, let rhsSticker = rhs.sticker, !lhsSticker.isEqual(to: rhsSticker) { - return false - } else if (lhs.sticker == nil) != (rhs.sticker == nil) { - return false - } - return true + return lhs.uuid == rhs.uuid } - public let sticker: TelegramMediaFile? + public let uuid: UUID + public let sticker: Signal - public init(sticker: TelegramMediaFile?) { + public init(uuid: UUID, sticker: Signal) { + self.uuid = uuid self.sticker = sticker } } @@ -282,14 +284,13 @@ public final class NavigateToChatControllerParams { public let activateMessageSearch: (ChatSearchDomain, String)? public let peekData: ChatPeekTimeout? public let peerNearbyData: ChatPeerNearbyData? - public let greetingData: ChatGreetingData? public let reportReason: ReportReason? public let animated: Bool public let options: NavigationAnimationOptions public let parentGroupId: PeerGroupId? public let completion: (ChatController) -> Void - public init(navigationController: NavigationController, chatController: ChatController? = nil, context: AccountContext, chatLocation: ChatLocation, chatLocationContextHolder: Atomic = Atomic(value: nil), subject: ChatControllerSubject? = nil, botStart: ChatControllerInitialBotStart? = nil, updateTextInputState: ChatTextInputState? = nil, activateInput: Bool = false, keepStack: NavigateToChatKeepStack = .default, useExisting: Bool = true, purposefulAction: (() -> Void)? = nil, scrollToEndIfExists: Bool = false, activateMessageSearch: (ChatSearchDomain, String)? = nil, peekData: ChatPeekTimeout? = nil, peerNearbyData: ChatPeerNearbyData? = nil, greetingData: ChatGreetingData? = nil, reportReason: ReportReason? = nil, animated: Bool = true, options: NavigationAnimationOptions = [], parentGroupId: PeerGroupId? = nil, completion: @escaping (ChatController) -> Void = { _ in }) { + public init(navigationController: NavigationController, chatController: ChatController? = nil, context: AccountContext, chatLocation: ChatLocation, chatLocationContextHolder: Atomic = Atomic(value: nil), subject: ChatControllerSubject? = nil, botStart: ChatControllerInitialBotStart? = nil, updateTextInputState: ChatTextInputState? = nil, activateInput: Bool = false, keepStack: NavigateToChatKeepStack = .default, useExisting: Bool = true, purposefulAction: (() -> Void)? = nil, scrollToEndIfExists: Bool = false, activateMessageSearch: (ChatSearchDomain, String)? = nil, peekData: ChatPeekTimeout? = nil, peerNearbyData: ChatPeerNearbyData? = nil, reportReason: ReportReason? = nil, animated: Bool = true, options: NavigationAnimationOptions = [], parentGroupId: PeerGroupId? = nil, completion: @escaping (ChatController) -> Void = { _ in }) { self.navigationController = navigationController self.chatController = chatController self.chatLocationContextHolder = chatLocationContextHolder @@ -306,7 +307,6 @@ public final class NavigateToChatControllerParams { self.activateMessageSearch = activateMessageSearch self.peekData = peekData self.peerNearbyData = peerNearbyData - self.greetingData = greetingData self.reportReason = reportReason self.animated = animated self.options = options @@ -471,15 +471,17 @@ public final class ContactSelectionControllerParams { public let options: [ContactListAdditionalOption] public let displayDeviceContacts: Bool public let displayCallIcons: Bool + public let multipleSelection: Bool public let confirmation: (ContactListPeer) -> Signal - public init(context: AccountContext, autoDismiss: Bool = true, title: @escaping (PresentationStrings) -> String, options: [ContactListAdditionalOption] = [], displayDeviceContacts: Bool = false, displayCallIcons: Bool = false, confirmation: @escaping (ContactListPeer) -> Signal = { _ in .single(true) }) { + public init(context: AccountContext, autoDismiss: Bool = true, title: @escaping (PresentationStrings) -> String, options: [ContactListAdditionalOption] = [], displayDeviceContacts: Bool = false, displayCallIcons: Bool = false, multipleSelection: Bool = false, confirmation: @escaping (ContactListPeer) -> Signal = { _ in .single(true) }) { self.context = context self.autoDismiss = autoDismiss self.title = title self.options = options self.displayDeviceContacts = displayDeviceContacts self.displayCallIcons = displayCallIcons + self.multipleSelection = multipleSelection self.confirmation = confirmation } } @@ -509,7 +511,7 @@ public enum ChatListSearchFilter: Equatable { case .voice: return 5 case let .peer(peerId, _, _, _): - return peerId.id + return peerId.id._internalGetInt32Value() case let .date(_, date, _): return date } @@ -544,6 +546,7 @@ public protocol RecentSessionsController: class { } public protocol SharedAccountContext: class { + var sharedContainerPath: String { get } var basePath: String { get } var mainWindow: Window1? { get } var accountManager: AccountManager { get } @@ -588,7 +591,7 @@ public protocol SharedAccountContext: class { 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, messages: [Message], theme: PresentationTheme, strings: PresentationStrings, wallpaper: TelegramWallpaper, fontSize: PresentationFontSize, chatBubbleCorners: PresentationChatBubbleCorners, dateTimeFormat: PresentationDateTimeFormat, nameOrder: PresentationPersonNameOrder, forcedResourceStatus: FileMediaResourceStatus?, tapMessage: ((Message) -> Void)?, clickThroughMessage: (() -> Void)?) -> ListViewItem + func makeChatMessagePreviewItem(context: AccountContext, messages: [Message], theme: PresentationTheme, strings: PresentationStrings, wallpaper: TelegramWallpaper, fontSize: PresentationFontSize, chatBubbleCorners: PresentationChatBubbleCorners, dateTimeFormat: PresentationDateTimeFormat, nameOrder: PresentationPersonNameOrder, forcedResourceStatus: FileMediaResourceStatus?, tapMessage: ((Message) -> Void)?, clickThroughMessage: (() -> Void)?, backgroundNode: ASDisplayNode?) -> 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 @@ -604,7 +607,7 @@ 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 chatAvailableMessageActions(postbox: Postbox, accountPeerId: PeerId, messageIds: Set, messages: [MessageId: Message], peers: [PeerId: Peer]) -> Signal - func resolveUrl(account: Account, url: String, skipUrlAuth: Bool) -> Signal + func resolveUrl(context: AccountContext, peerId: PeerId?, url: String, skipUrlAuth: Bool) -> Signal func openResolvedUrl(_ resolvedUrl: ResolvedUrl, context: AccountContext, urlContext: OpenURLContext, navigationController: NavigationController?, openPeer: @escaping (PeerId, ChatControllerInteractionNavigateToPeer) -> Void, sendFile: ((FileMediaReference) -> Void)?, sendSticker: ((FileMediaReference, ASDisplayNode, CGRect) -> Bool)?, requestMessageActionUrlAuth: ((MessageActionUrlSubject) -> Void)?, joinVoiceChat: ((PeerId, String?, CachedChannelData.ActiveCall) -> Void)?, 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) @@ -709,10 +712,12 @@ public protocol AccountGroupCallContextCache: class { public protocol AccountContext: class { var sharedContext: SharedAccountContext { get } var account: Account { get } + var engine: TelegramEngine { get } var liveLocationManager: LiveLocationManager? { get } var peersNearbyManager: PeersNearbyManager? { get } var fetchManager: FetchManager { get } + var prefetchManager: PrefetchManager? { get } var downloadedMediaStoreManager: DownloadedMediaStoreManager { get } var peerChannelMemberCategoriesContextsManager: PeerChannelMemberCategoriesContextsManager { get } var wallpaperUploadManager: WallpaperUploadManager? { get } @@ -731,6 +736,7 @@ public protocol AccountContext: class { func chatLocationOutgoingReadState(for location: ChatLocation, contextHolder: Atomic) -> Signal func applyMaxReadIndex(for location: ChatLocation, contextHolder: Atomic, messageIndex: MessageIndex) + func scheduleGroupCall(peerId: PeerId) func joinGroupCall(peerId: PeerId, invite: String?, requestJoinAsPeerId: ((@escaping (PeerId?) -> Void) -> Void)?, activeCall: CachedChannelData.ActiveCall) func requestCall(peerId: PeerId, isVideo: Bool, completion: @escaping () -> Void) } diff --git a/submodules/AccountContext/Sources/ChatController.swift b/submodules/AccountContext/Sources/ChatController.swift index 1ddd5bbe7f..d0cf354fd6 100644 --- a/submodules/AccountContext/Sources/ChatController.swift +++ b/submodules/AccountContext/Sources/ChatController.swift @@ -24,8 +24,9 @@ public final class ChatMessageItemAssociatedData: Equatable { public let channelDiscussionGroup: ChannelDiscussionGroupStatus public let animatedEmojiStickers: [String: [StickerPackItem]] public let forcedResourceStatus: FileMediaResourceStatus? + public let currentlyPlayingMessageId: MessageIndex? - public init(automaticDownloadPeerType: MediaAutoDownloadPeerType, automaticDownloadNetworkType: MediaAutoDownloadNetworkType, isRecentActions: Bool = false, subject: ChatControllerSubject? = nil, contactsPeerIds: Set = Set(), channelDiscussionGroup: ChannelDiscussionGroupStatus = .unknown, animatedEmojiStickers: [String: [StickerPackItem]] = [:], forcedResourceStatus: FileMediaResourceStatus? = nil) { + public init(automaticDownloadPeerType: MediaAutoDownloadPeerType, automaticDownloadNetworkType: MediaAutoDownloadNetworkType, isRecentActions: Bool = false, subject: ChatControllerSubject? = nil, contactsPeerIds: Set = Set(), channelDiscussionGroup: ChannelDiscussionGroupStatus = .unknown, animatedEmojiStickers: [String: [StickerPackItem]] = [:], forcedResourceStatus: FileMediaResourceStatus? = nil, currentlyPlayingMessageId: MessageIndex? = nil) { self.automaticDownloadPeerType = automaticDownloadPeerType self.automaticDownloadNetworkType = automaticDownloadNetworkType self.isRecentActions = isRecentActions @@ -34,6 +35,7 @@ public final class ChatMessageItemAssociatedData: Equatable { self.channelDiscussionGroup = channelDiscussionGroup self.animatedEmojiStickers = animatedEmojiStickers self.forcedResourceStatus = forcedResourceStatus + self.currentlyPlayingMessageId = currentlyPlayingMessageId } public static func == (lhs: ChatMessageItemAssociatedData, rhs: ChatMessageItemAssociatedData) -> Bool { @@ -339,7 +341,7 @@ public struct ChatTextInputStateText: PostboxCoding, Equatable { } public enum ChatControllerSubject: Equatable { - case message(id: MessageId, highlight: Bool) + case message(id: MessageId, highlight: Bool, timecode: Double?) case scheduledMessages case pinnedMessages(id: MessageId?) } diff --git a/submodules/AccountContext/Sources/ContactSelectionController.swift b/submodules/AccountContext/Sources/ContactSelectionController.swift index b99e4c7252..1e5e694f28 100644 --- a/submodules/AccountContext/Sources/ContactSelectionController.swift +++ b/submodules/AccountContext/Sources/ContactSelectionController.swift @@ -3,7 +3,7 @@ import Display import SwiftSignalKit public protocol ContactSelectionController: ViewController { - var result: Signal<(ContactListPeer, ContactListAction)?, NoError> { get } + var result: Signal<([ContactListPeer], ContactListAction)?, NoError> { get } var displayProgress: Bool { get set } var dismissed: (() -> Void)? { get set } diff --git a/submodules/AccountContext/Sources/DeviceContactData.swift b/submodules/AccountContext/Sources/DeviceContactData.swift index 674b035809..3111c3bd5a 100644 --- a/submodules/AccountContext/Sources/DeviceContactData.swift +++ b/submodules/AccountContext/Sources/DeviceContactData.swift @@ -202,7 +202,7 @@ public func parseAppSpecificContactReference(_ value: String) -> PeerId? { } let idString = String(value[value.index(value.startIndex, offsetBy: phonebookUsernamePrefix.count)...]) if let id = Int32(idString) { - return PeerId(namespace: Namespaces.Peer.CloudUser, id: id) + return PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt32Value(id)) } return nil } diff --git a/submodules/AccountContext/Sources/FetchManager.swift b/submodules/AccountContext/Sources/FetchManager.swift index c70576facd..ad4db3b593 100644 --- a/submodules/AccountContext/Sources/FetchManager.swift +++ b/submodules/AccountContext/Sources/FetchManager.swift @@ -158,3 +158,8 @@ public protocol FetchManager { func cancelInteractiveFetches(category: FetchManagerCategory, location: FetchManagerLocation, locationKey: FetchManagerLocationKey, resource: MediaResource) func fetchStatus(category: FetchManagerCategory, location: FetchManagerLocation, locationKey: FetchManagerLocationKey, resource: MediaResource) -> Signal } + +public protocol PrefetchManager { + var preloadedGreetingSticker: ChatGreetingData { get } + func prepareNextGreetingSticker() +} diff --git a/submodules/AccountContext/Sources/FetchMediaUtils.swift b/submodules/AccountContext/Sources/FetchMediaUtils.swift index 52b7487363..f3dc6c4eee 100644 --- a/submodules/AccountContext/Sources/FetchMediaUtils.swift +++ b/submodules/AccountContext/Sources/FetchMediaUtils.swift @@ -13,7 +13,7 @@ public func freeMediaFileInteractiveFetched(account: Account, fileReference: Fil public func freeMediaFileInteractiveFetched(fetchManager: FetchManager, fileReference: FileMediaReference, priority: FetchManagerPriority) -> Signal { let file = fileReference.media let mediaReference = AnyMediaReference.standalone(media: fileReference.media) - return fetchManager.interactivelyFetched(category: fetchCategoryForFile(file), location: .chat(PeerId(namespace: 0, id: 0)), locationKey: .free, mediaReference: mediaReference, resourceReference: mediaReference.resourceReference(file.resource), ranges: IndexSet(integersIn: 0 ..< Int(Int32.max) as Range), statsCategory: statsCategoryForFileWithAttributes(file.attributes), elevatedPriority: false, userInitiated: false, priority: priority, storeToDownloadsPeerType: nil) + return fetchManager.interactivelyFetched(category: fetchCategoryForFile(file), location: .chat(PeerId(0)), locationKey: .free, mediaReference: mediaReference, resourceReference: mediaReference.resourceReference(file.resource), ranges: IndexSet(integersIn: 0 ..< Int(Int32.max) as Range), statsCategory: statsCategoryForFileWithAttributes(file.attributes), elevatedPriority: false, userInitiated: false, priority: priority, storeToDownloadsPeerType: nil) } public func freeMediaFileResourceInteractiveFetched(account: Account, fileReference: FileMediaReference, resource: MediaResource) -> Signal { diff --git a/submodules/AccountContext/Sources/MediaManager.swift b/submodules/AccountContext/Sources/MediaManager.swift index b9c754ec2f..7f5ef063b0 100644 --- a/submodules/AccountContext/Sources/MediaManager.swift +++ b/submodules/AccountContext/Sources/MediaManager.swift @@ -119,11 +119,11 @@ public func peerMessageMediaPlayerType(_ message: Message) -> MediaManagerPlayer public func peerMessagesMediaPlaylistAndItemId(_ message: Message, isRecentActions: Bool, isGlobalSearch: Bool) -> (SharedMediaPlaylistId, SharedMediaPlaylistItemId)? { if isGlobalSearch { - return (PeerMessagesMediaPlaylistId.custom, PeerMessagesMediaPlaylistItemId(messageId: message.id)) + return (PeerMessagesMediaPlaylistId.custom, PeerMessagesMediaPlaylistItemId(messageId: message.id, messageIndex: message.index)) } else if isRecentActions { - return (PeerMessagesMediaPlaylistId.recentActions(message.id.peerId), PeerMessagesMediaPlaylistItemId(messageId: message.id)) + return (PeerMessagesMediaPlaylistId.recentActions(message.id.peerId), PeerMessagesMediaPlaylistItemId(messageId: message.id, messageIndex: message.index)) } else { - return (PeerMessagesMediaPlaylistId.peer(message.id.peerId), PeerMessagesMediaPlaylistItemId(messageId: message.id)) + return (PeerMessagesMediaPlaylistId.peer(message.id.peerId), PeerMessagesMediaPlaylistItemId(messageId: message.id, messageIndex: message.index)) } } diff --git a/submodules/AccountContext/Sources/OpenChatMessage.swift b/submodules/AccountContext/Sources/OpenChatMessage.swift index be3fcfd330..58b24cd349 100644 --- a/submodules/AccountContext/Sources/OpenChatMessage.swift +++ b/submodules/AccountContext/Sources/OpenChatMessage.swift @@ -41,6 +41,7 @@ public final class OpenChatMessageParams { public let actionInteraction: GalleryControllerActionInteraction? public let playlistLocation: PeerMessagesPlaylistLocation? public let gallerySource: GalleryControllerItemSource? + public let centralItemUpdated: ((MessageId) -> Void)? public init( context: AccountContext, @@ -65,7 +66,8 @@ public final class OpenChatMessageParams { chatAvatarHiddenMedia: @escaping (Signal, Media) -> Void, actionInteraction: GalleryControllerActionInteraction? = nil, playlistLocation: PeerMessagesPlaylistLocation? = nil, - gallerySource: GalleryControllerItemSource? = nil + gallerySource: GalleryControllerItemSource? = nil, + centralItemUpdated: ((MessageId) -> Void)? = nil ) { self.context = context self.chatLocation = chatLocation @@ -90,5 +92,6 @@ public final class OpenChatMessageParams { self.actionInteraction = actionInteraction self.playlistLocation = playlistLocation self.gallerySource = gallerySource + self.centralItemUpdated = centralItemUpdated } } diff --git a/submodules/AccountContext/Sources/PeerSelectionController.swift b/submodules/AccountContext/Sources/PeerSelectionController.swift index 3c6686a600..90794e83dd 100644 --- a/submodules/AccountContext/Sources/PeerSelectionController.swift +++ b/submodules/AccountContext/Sources/PeerSelectionController.swift @@ -39,8 +39,9 @@ public final class PeerSelectionControllerParams { public let attemptSelection: ((Peer) -> Void)? public let createNewGroup: (() -> Void)? public let pretendPresentedInModal: Bool + public let multipleSelection: Bool - public init(context: AccountContext, filter: ChatListNodePeersFilter = [.onlyWriteable], hasChatListSelector: Bool = true, hasContactSelector: Bool = true, hasGlobalSearch: Bool = true, title: String? = nil, attemptSelection: ((Peer) -> Void)? = nil, createNewGroup: (() -> Void)? = nil, pretendPresentedInModal: Bool = false) { + public init(context: AccountContext, filter: ChatListNodePeersFilter = [.onlyWriteable], hasChatListSelector: Bool = true, hasContactSelector: Bool = true, hasGlobalSearch: Bool = true, title: String? = nil, attemptSelection: ((Peer) -> Void)? = nil, createNewGroup: (() -> Void)? = nil, pretendPresentedInModal: Bool = false, multipleSelection: Bool = false) { self.context = context self.filter = filter self.hasChatListSelector = hasChatListSelector @@ -50,11 +51,13 @@ public final class PeerSelectionControllerParams { self.attemptSelection = attemptSelection self.createNewGroup = createNewGroup self.pretendPresentedInModal = pretendPresentedInModal + self.multipleSelection = multipleSelection } } public protocol PeerSelectionController: ViewController { var peerSelected: ((Peer) -> Void)? { get set } + var multiplePeersSelected: (([Peer], NSAttributedString) -> Void)? { get set } var inProgress: Bool { get set } var customDismiss: (() -> Void)? { get set } } diff --git a/submodules/AccountContext/Sources/PresentationCallManager.swift b/submodules/AccountContext/Sources/PresentationCallManager.swift index 44602ffedb..d954c7b175 100644 --- a/submodules/AccountContext/Sources/PresentationCallManager.swift +++ b/submodules/AccountContext/Sources/PresentationCallManager.swift @@ -17,6 +17,11 @@ public enum JoinGroupCallManagerResult { case alreadyInProgress(PeerId?) } +public enum RequestScheduleGroupCallResult { + case success + case alreadyInProgress(PeerId?) +} + public struct CallAuxiliaryServer { public enum Connection { case stun @@ -104,6 +109,7 @@ public final class PresentationCallVideoView { public let getAspect: () -> CGFloat public let setOnOrientationUpdated: (((Orientation, CGFloat) -> Void)?) -> Void public let setOnIsMirroredUpdated: (((Bool) -> Void)?) -> Void + public let updateIsEnabled: (Bool) -> Void public init( holder: AnyObject, @@ -112,7 +118,8 @@ public final class PresentationCallVideoView { getOrientation: @escaping () -> Orientation, getAspect: @escaping () -> CGFloat, setOnOrientationUpdated: @escaping (((Orientation, CGFloat) -> Void)?) -> Void, - setOnIsMirroredUpdated: @escaping (((Bool) -> Void)?) -> Void + setOnIsMirroredUpdated: @escaping (((Bool) -> Void)?) -> Void, + updateIsEnabled: @escaping (Bool) -> Void ) { self.holder = holder self.view = view @@ -121,6 +128,7 @@ public final class PresentationCallVideoView { self.getAspect = getAspect self.setOnOrientationUpdated = setOnOrientationUpdated self.setOnIsMirroredUpdated = setOnIsMirroredUpdated + self.updateIsEnabled = updateIsEnabled } } @@ -181,6 +189,9 @@ public struct PresentationGroupCallState: Equatable { public var recordingStartTimestamp: Int32? public var title: String? public var raisedHand: Bool + public var scheduleTimestamp: Int32? + public var subscribedToScheduled: Bool + public var isVideoEnabled: Bool public init( myPeerId: PeerId, @@ -191,7 +202,10 @@ public struct PresentationGroupCallState: Equatable { defaultParticipantMuteState: DefaultParticipantMuteState?, recordingStartTimestamp: Int32?, title: String?, - raisedHand: Bool + raisedHand: Bool, + scheduleTimestamp: Int32?, + subscribedToScheduled: Bool, + isVideoEnabled: Bool ) { self.myPeerId = myPeerId self.networkState = networkState @@ -202,6 +216,9 @@ public struct PresentationGroupCallState: Equatable { self.recordingStartTimestamp = recordingStartTimestamp self.title = title self.raisedHand = raisedHand + self.scheduleTimestamp = scheduleTimestamp + self.subscribedToScheduled = subscribedToScheduled + self.isVideoEnabled = isVideoEnabled } } @@ -278,10 +295,16 @@ public struct PresentationGroupCallMembers: Equatable { public final class PresentationGroupCallMemberEvent { public let peer: Peer + public let isContact: Bool + public let isInChatList: Bool + public let canUnmute: Bool public let joined: Bool - public init(peer: Peer, joined: Bool) { + public init(peer: Peer, isContact: Bool, isInChatList: Bool, canUnmute: Bool, joined: Bool) { self.peer = peer + self.isContact = isContact + self.isInChatList = isInChatList + self.canUnmute = canUnmute self.joined = joined } } @@ -291,40 +314,108 @@ public enum PresentationGroupCallTone { case recordingStarted } +public struct PresentationGroupCallRequestedVideo { + public enum Quality { + case thumbnail + case medium + case full + } + + public struct SsrcGroup { + public var semantics: String + public var ssrcs: [UInt32] + } + + public var audioSsrc: UInt32 + public var endpointId: String + public var ssrcGroups: [SsrcGroup] + public var minQuality: Quality + public var maxQuality: Quality +} + +public extension GroupCallParticipantsContext.Participant { + var videoEndpointId: String? { + return self.videoDescription?.endpointId + } + + var presentationEndpointId: String? { + return self.presentationDescription?.endpointId + } +} + +public extension GroupCallParticipantsContext.Participant { + func requestedVideoChannel(minQuality: PresentationGroupCallRequestedVideo.Quality, maxQuality: PresentationGroupCallRequestedVideo.Quality) -> PresentationGroupCallRequestedVideo? { + guard let audioSsrc = self.ssrc else { + return nil + } + guard let videoDescription = self.videoDescription else { + return nil + } + return PresentationGroupCallRequestedVideo(audioSsrc: audioSsrc, endpointId: videoDescription.endpointId, ssrcGroups: videoDescription.ssrcGroups.map { group in + PresentationGroupCallRequestedVideo.SsrcGroup(semantics: group.semantics, ssrcs: group.ssrcs) + }, minQuality: minQuality, maxQuality: maxQuality) + } + + func requestedPresentationVideoChannel(minQuality: PresentationGroupCallRequestedVideo.Quality, maxQuality: PresentationGroupCallRequestedVideo.Quality) -> PresentationGroupCallRequestedVideo? { + guard let audioSsrc = self.ssrc else { + return nil + } + guard let presentationDescription = self.presentationDescription else { + return nil + } + return PresentationGroupCallRequestedVideo(audioSsrc: audioSsrc, endpointId: presentationDescription.endpointId, ssrcGroups: presentationDescription.ssrcGroups.map { group in + PresentationGroupCallRequestedVideo.SsrcGroup(semantics: group.semantics, ssrcs: group.ssrcs) + }, minQuality: minQuality, maxQuality: maxQuality) + } +} + public protocol PresentationGroupCall: class { var account: Account { get } var accountContext: AccountContext { get } var internalId: CallSessionInternalId { get } var peerId: PeerId { get } - var isVideo: Bool { get } + var hasVideo: Bool { get } + var hasScreencast: Bool { get } + + var schedulePending: Bool { get } var audioOutputState: Signal<([AudioSessionOutput], AudioSessionOutput?), NoError> { get } + var isSpeaking: Signal { get } var canBeRemoved: Signal { get } var state: Signal { get } + var stateVersion: Signal { get } var summaryState: Signal { get } var members: Signal { get } var audioLevels: Signal<[(PeerId, UInt32, Float, Bool)], NoError> { get } var myAudioLevel: Signal { get } var isMuted: Signal { get } + var isNoiseSuppressionEnabled: Signal { get } var memberEvents: Signal { get } var reconnectedAsEvents: Signal { get } + func toggleScheduledSubscription(_ subscribe: Bool) + func schedule(timestamp: Int32) + func startScheduled() + func reconnect(with invite: String) func reconnect(as peerId: PeerId) func leave(terminateIfPossible: Bool) -> Signal func toggleIsMuted() func setIsMuted(action: PresentationGroupCallMuteAction) + func setIsNoiseSuppressionEnabled(_ isNoiseSuppressionEnabled: Bool) func raiseHand() func lowerHand() func requestVideo() func disableVideo() + func disableScreencast() + func switchVideoCamera() func updateDefaultParticipantsAreMuted(isMuted: Bool) func setVolume(peerId: PeerId, volume: Int32, sync: Bool) - func setFullSizeVideo(peerId: PeerId?) + func setRequestedVideoList(items: [PresentationGroupCallRequestedVideo]) func setCurrentAudioOutput(_ output: AudioSessionOutput) func playTone(_ tone: PresentationGroupCallTone) @@ -340,9 +431,8 @@ public protocol PresentationGroupCall: class { var inviteLinks: Signal { get } - var incomingVideoSources: Signal<[PeerId: UInt32], NoError> { get } - - func makeIncomingVideoView(source: UInt32, completion: @escaping (PresentationCallVideoView?) -> Void) + func makeIncomingVideoView(endpointId: String, requestClone: Bool, completion: @escaping (PresentationCallVideoView?, PresentationCallVideoView?) -> Void) + func makeOutgoingVideoView(requestClone: Bool, completion: @escaping (PresentationCallVideoView?, PresentationCallVideoView?) -> Void) func loadMoreMembers(token: String) } @@ -353,4 +443,5 @@ public protocol PresentationCallManager: class { func requestCall(context: AccountContext, peerId: PeerId, isVideo: Bool, endCurrentIfAny: Bool) -> RequestCallResult func joinGroupCall(context: AccountContext, peerId: PeerId, invite: String?, requestJoinAsPeerId: ((@escaping (PeerId?) -> Void) -> Void)?, initialCall: CachedChannelData.ActiveCall, endCurrentIfAny: Bool) -> JoinGroupCallManagerResult + func scheduleGroupCall(context: AccountContext, peerId: PeerId, endCurrentIfAny: Bool) -> RequestScheduleGroupCallResult } diff --git a/submodules/AccountContext/Sources/SharedMediaPlayer.swift b/submodules/AccountContext/Sources/SharedMediaPlayer.swift index 4140e3345d..e71f6843b1 100644 --- a/submodules/AccountContext/Sources/SharedMediaPlayer.swift +++ b/submodules/AccountContext/Sources/SharedMediaPlayer.swift @@ -135,14 +135,16 @@ public func areSharedMediaPlaylistItemIdsEqual(_ lhs: SharedMediaPlaylistItemId? public struct PeerMessagesMediaPlaylistItemId: SharedMediaPlaylistItemId { public let messageId: MessageId + public let messageIndex: MessageIndex - public init(messageId: MessageId) { + public init(messageId: MessageId, messageIndex: MessageIndex) { self.messageId = messageId + self.messageIndex = messageIndex } public func isEqual(to: SharedMediaPlaylistItemId) -> Bool { if let to = to as? PeerMessagesMediaPlaylistItemId { - if self.messageId != to.messageId { + if self.messageId != to.messageId || self.messageIndex != to.messageIndex { return false } return true diff --git a/submodules/AnimatedNavigationStripeNode/Sources/AnimatedNavigationStripeNode.swift b/submodules/AnimatedNavigationStripeNode/Sources/AnimatedNavigationStripeNode.swift index c95452e7e4..be5b3498a8 100644 --- a/submodules/AnimatedNavigationStripeNode/Sources/AnimatedNavigationStripeNode.swift +++ b/submodules/AnimatedNavigationStripeNode/Sources/AnimatedNavigationStripeNode.swift @@ -61,26 +61,35 @@ public final class AnimatedNavigationStripeNode: ASDisplayNode { private let foregroundLineNode: ASImageNode private var backgroundLineNodes: [Int: BackgroundLineNode] = [:] private var removingBackgroundLineNodes: [BackgroundLineNode] = [] - + + private let maskContainerNode: ASDisplayNode private let topShadowNode: ASImageNode private let bottomShadowNode: ASImageNode + private let middleShadowNode: ASDisplayNode private var currentForegroundImage: UIImage? private var currentBackgroundImage: UIImage? private var currentClearBackgroundImage: UIImage? override public init() { + self.maskContainerNode = ASDisplayNode() + self.foregroundLineNode = ASImageNode() self.topShadowNode = ASImageNode() self.bottomShadowNode = ASImageNode() + self.middleShadowNode = ASDisplayNode() + self.middleShadowNode.backgroundColor = .white super.init() self.clipsToBounds = true - + + self.addSubnode(self.maskContainerNode) self.addSubnode(self.foregroundLineNode) - self.addSubnode(self.topShadowNode) - self.addSubnode(self.bottomShadowNode) + self.maskContainerNode.addSubnode(self.topShadowNode) + self.maskContainerNode.addSubnode(self.bottomShadowNode) + self.maskContainerNode.addSubnode(self.middleShadowNode) + self.layer.mask = self.maskContainerNode.layer } public func update(colors: Colors, configuration: Configuration, transition: ContainedViewLayoutTransition) { @@ -123,7 +132,7 @@ public final class AnimatedNavigationStripeNode: ASDisplayNode { context.clear(CGRect(origin: CGPoint(), size: size)) var locations: [CGFloat] = [1.0, 0.0] - let colors: [CGColor] = [colors.clearBackground.cgColor, colors.clearBackground.withAlphaComponent(0.0).cgColor] + let colors: [CGColor] = [UIColor.white.withAlphaComponent(0.0).cgColor, UIColor.white.cgColor] let colorSpace = CGColorSpaceCreateDeviceRGB() let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)! @@ -135,7 +144,7 @@ public final class AnimatedNavigationStripeNode: ASDisplayNode { context.clear(CGRect(origin: CGPoint(), size: size)) var locations: [CGFloat] = [1.0, 0.0] - let colors: [CGColor] = [colors.clearBackground.cgColor, colors.clearBackground.withAlphaComponent(0.0).cgColor] + let colors: [CGColor] = [UIColor.white.withAlphaComponent(0.0).cgColor, UIColor.white.cgColor] let colorSpace = CGColorSpaceCreateDeviceRGB() let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)! @@ -161,6 +170,8 @@ public final class AnimatedNavigationStripeNode: ASDisplayNode { transition.updateFrame(node: self.topShadowNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: 2.0, height: defaultVerticalInset))) transition.updateFrame(node: self.bottomShadowNode, frame: CGRect(origin: CGPoint(x: 0.0, y: configuration.height - defaultVerticalInset), size: CGSize(width: 2.0, height: defaultVerticalInset))) + transition.updateFrame(node: self.middleShadowNode, frame: CGRect(origin: CGPoint(x: 0.0, y: defaultVerticalInset), size: CGSize(width: 2.0, height: configuration.height - defaultVerticalInset * 2.0))) + transition.updateFrame(node: self.maskContainerNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: 2.0, height: configuration.height))) let availableVerticalHeight: CGFloat = configuration.height - defaultVerticalInset * 2.0 @@ -214,7 +225,7 @@ public final class AnimatedNavigationStripeNode: ASDisplayNode { itemNode.overlayNode.image = self.currentClearBackgroundImage self.backgroundLineNodes[index] = itemNode self.insertSubnode(itemNode.lineNode, belowSubnode: self.foregroundLineNode) - self.insertSubnode(itemNode.overlayNode, belowSubnode: self.topShadowNode) + self.topShadowNode.supernode?.insertSubnode(itemNode.overlayNode, belowSubnode: self.topShadowNode) backgroundItemNodesToOffset.append(itemNode) } itemNodeTransition.updateFrame(node: itemNode.lineNode, frame: itemFrame, beginWithCurrentState: true) diff --git a/submodules/AnimatedStickerNode/Sources/AnimatedStickerNode.swift b/submodules/AnimatedStickerNode/Sources/AnimatedStickerNode.swift index 889af3cae8..9606e718e1 100644 --- a/submodules/AnimatedStickerNode/Sources/AnimatedStickerNode.swift +++ b/submodules/AnimatedStickerNode/Sources/AnimatedStickerNode.swift @@ -56,6 +56,7 @@ public enum AnimatedStickerPlaybackPosition { case start case end case timestamp(Double) + case frameIndex(Int) } public enum AnimatedStickerPlaybackMode { @@ -94,6 +95,7 @@ public protocol AnimatedStickerFrameSource: class { func takeFrame(draw: Bool) -> AnimatedStickerFrame? func skipToEnd() + func skipToFrameIndex(_ index: Int) } private final class AnimatedStickerFrameSourceWrapper { @@ -271,6 +273,9 @@ public final class AnimatedStickerCachedFrameSource: AnimatedStickerFrameSource public func skipToEnd() { } + + public func skipToFrameIndex(_ index: Int) { + } } private func wrappedWrite(_ fd: Int32, _ data: UnsafeRawPointer, _ count: Int) -> Int { @@ -609,7 +614,7 @@ private final class AnimatedStickerDirectFrameSource: AnimatedStickerFrameSource self.data = data self.width = width self.height = height - self.bytesPerRow = (4 * Int(width) + 15) & (~15) + self.bytesPerRow = DeviceGraphicsContextSettings.shared.bytesPerRow(forWidth: Int(width)) self.currentFrame = 0 let rawData = TGGUnzipData(data, 8 * 1024 * 1024) ?? data let decompressedData = transformedWithFitzModifier(data: rawData, fitzModifier: fitzModifier) @@ -656,6 +661,10 @@ private final class AnimatedStickerDirectFrameSource: AnimatedStickerFrameSource func skipToEnd() { self.currentFrame = self.frameCount - 1 } + + func skipToFrameIndex(_ index: Int) { + self.currentFrame = index + } } public final class AnimatedStickerFrameQueue { @@ -748,6 +757,8 @@ public final class AnimatedStickerNode: ASDisplayNode { public var completed: (Bool) -> Void = { _ in } public var frameUpdated: (Int, Int) -> Void = { _, _ in } + public private(set) var currentFrameIndex: Int = 0 + private var playFromIndex: Int? private let timer = Atomic(value: nil) private let frameSource = Atomic?>(value: nil) @@ -842,7 +853,9 @@ public final class AnimatedStickerNode: ASDisplayNode { strongSelf.isSetUpForPlayback = false strongSelf.isPlaying = true } - strongSelf.play() + var fromIndex = strongSelf.playFromIndex + strongSelf.playFromIndex = nil + strongSelf.play(fromIndex: fromIndex) } else if strongSelf.canDisplayFirstFrame { strongSelf.play(firstFrame: true) } @@ -911,7 +924,7 @@ public final class AnimatedStickerNode: ASDisplayNode { private var isSetUpForPlayback = false - public func play(firstFrame: Bool = false) { + public func play(firstFrame: Bool = false, fromIndex: Int? = nil) { switch self.playbackMode { case .once: self.isPlaying = true @@ -949,6 +962,9 @@ public final class AnimatedStickerNode: ASDisplayNode { guard let frameSource = maybeFrameSource else { return } + if let fromIndex = fromIndex { + frameSource.skipToFrameIndex(fromIndex) + } let frameQueue = QueueLocalObject(queue: queue, generate: { return AnimatedStickerFrameQueue(queue: queue, length: 1, source: frameSource) }) @@ -978,6 +994,7 @@ public final class AnimatedStickerNode: ASDisplayNode { }) strongSelf.frameUpdated(frame.index, frame.totalFrames) + strongSelf.currentFrameIndex = frame.index if frame.isLastFrame { var stopped = false @@ -1016,6 +1033,9 @@ public final class AnimatedStickerNode: ASDisplayNode { self.isSetUpForPlayback = true let directData = self.directData let cachedData = self.cachedData + if directData == nil && cachedData == nil { + self.playFromIndex = fromIndex + } let queue = self.queue let timerHolder = self.timer let frameSourceHolder = self.frameSource @@ -1039,6 +1059,9 @@ public final class AnimatedStickerNode: ASDisplayNode { guard let frameSource = maybeFrameSource else { return } + if let fromIndex = fromIndex { + frameSource.skipToFrameIndex(fromIndex) + } let frameQueue = QueueLocalObject(queue: queue, generate: { return AnimatedStickerFrameQueue(queue: queue, length: 1, source: frameSource) }) @@ -1068,6 +1091,7 @@ public final class AnimatedStickerNode: ASDisplayNode { }) strongSelf.frameUpdated(frame.index, frame.totalFrames) + strongSelf.currentFrameIndex = frame.index if frame.isLastFrame { var stopped = false @@ -1163,6 +1187,21 @@ public final class AnimatedStickerNode: ASDisplayNode { return } + var delta = targetFrame - frameSource.frameIndex + if delta < 0 { + delta = frameSource.frameCount + delta + } + for i in 0 ..< delta { + maybeFrame = frameQueue.syncWith { frameQueue in + return frameQueue.take(draw: i == delta - 1) + } + } + } else if case let .frameIndex(frameIndex) = position { + let targetFrame = frameIndex + if targetFrame == frameSource.frameIndex { + return + } + var delta = targetFrame - frameSource.frameIndex if delta < 0 { delta = frameSource.frameCount + delta diff --git a/submodules/AnimatedStickerNode/Sources/SoftwareAnimationRenderer.swift b/submodules/AnimatedStickerNode/Sources/SoftwareAnimationRenderer.swift index 6dffe0ed40..5b23eb8b1d 100644 --- a/submodules/AnimatedStickerNode/Sources/SoftwareAnimationRenderer.swift +++ b/submodules/AnimatedStickerNode/Sources/SoftwareAnimationRenderer.swift @@ -13,7 +13,7 @@ final class SoftwareAnimationRenderer: ASDisplayNode, AnimationRenderer { queue.async { [weak self] in switch type { case .argb: - let calculatedBytesPerRow = (4 * Int(width) + 15) & (~15) + let calculatedBytesPerRow = DeviceGraphicsContextSettings.shared.bytesPerRow(forWidth: Int(width)) assert(bytesPerRow == calculatedBytesPerRow) case .yuva: break diff --git a/submodules/AnimationUI/Sources/AnimationNode.swift b/submodules/AnimationUI/Sources/AnimationNode.swift index edc556d626..a034b855d5 100644 --- a/submodules/AnimationUI/Sources/AnimationNode.swift +++ b/submodules/AnimationUI/Sources/AnimationNode.swift @@ -18,6 +18,7 @@ public final class AnimationNode : ASDisplayNode { public var didPlay = false public var completion: (() -> Void)? + private var internalCompletion: (() -> Void)? public var isPlaying: Bool { return self.animationView()?.isAnimationPlaying ?? false @@ -79,10 +80,22 @@ public final class AnimationNode : ASDisplayNode { }) } - public func setAnimation(name: String) { + public func seekToEnd() { + self.animationView()?.animationProgress = 1.0 + } + + public func setAnimation(name: String, colors: [String: UIColor]? = nil) { if let url = getAppBundle().url(forResource: name, withExtension: "json"), let composition = LOTComposition(filePath: url.path) { self.didPlay = false self.animationView()?.sceneModel = composition + + if let colors = colors { + for (key, value) in colors { + let colorCallback = LOTColorValueCallback(color: value.cgColor) + self.colorCallbacks.append(colorCallback) + self.animationView()?.setValueDelegate(colorCallback, for: LOTKeypath(string: "\(key).Color")) + } + } } } @@ -95,6 +108,7 @@ public final class AnimationNode : ASDisplayNode { } public func setAnimation(json: [AnyHashable: Any]) { + self.didPlay = false self.animationView()?.setAnimation(json: json) } @@ -103,7 +117,7 @@ public final class AnimationNode : ASDisplayNode { } public func play() { - if let animationView = animationView(), !animationView.isAnimationPlaying && !self.didPlay { + if let animationView = self.animationView(), !animationView.isAnimationPlaying && !self.didPlay { self.didPlay = true animationView.play { [weak self] _ in self?.completion?() @@ -111,8 +125,20 @@ public final class AnimationNode : ASDisplayNode { } } + public func playOnce() { + if let animationView = self.animationView(), !animationView.isAnimationPlaying && !self.didPlay { + self.didPlay = true + self.internalCompletion = { [weak self] in + self?.didPlay = false + } + animationView.play { [weak self] _ in + self?.internalCompletion?() + } + } + } + public func loop() { - if let animationView = animationView() { + if let animationView = self.animationView() { animationView.loopAnimation = true animationView.play() } diff --git a/submodules/AppLock/Sources/AppLock.swift b/submodules/AppLock/Sources/AppLock.swift index b771fc82de..993663bce7 100644 --- a/submodules/AppLock/Sources/AppLock.swift +++ b/submodules/AppLock/Sources/AppLock.swift @@ -184,7 +184,7 @@ public final class AppLockContextImpl: AppLockContext { } passcodeController.ensureInputFocused() } else { - let passcodeController = PasscodeEntryController(applicationBindings: strongSelf.applicationBindings, accountManager: strongSelf.accountManager, appLockContext: strongSelf, presentationData: presentationData, presentationDataSignal: strongSelf.presentationDataSignal, challengeData: accessChallengeData.data, biometrics: biometrics, arguments: PasscodeEntryControllerPresentationArguments(animated: !becameActiveRecently, lockIconInitialFrame: { [weak self] in + let passcodeController = PasscodeEntryController(applicationBindings: strongSelf.applicationBindings, accountManager: strongSelf.accountManager, appLockContext: strongSelf, presentationData: presentationData, presentationDataSignal: strongSelf.presentationDataSignal, statusBarHost: window?.statusBarHost, challengeData: accessChallengeData.data, biometrics: biometrics, arguments: PasscodeEntryControllerPresentationArguments(animated: !becameActiveRecently, lockIconInitialFrame: { [weak self] in if let lockViewFrame = lockIconInitialFrame() { return lockViewFrame } else { diff --git a/submodules/AsyncDisplayKit/Source/ASCGImageBuffer.mm b/submodules/AsyncDisplayKit/Source/ASCGImageBuffer.mm index 6f05300e23..24ab628d1b 100644 --- a/submodules/AsyncDisplayKit/Source/ASCGImageBuffer.mm +++ b/submodules/AsyncDisplayKit/Source/ASCGImageBuffer.mm @@ -6,7 +6,7 @@ // Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 // -#import "ASCGImageBuffer.h" +#import #import #import @@ -32,7 +32,7 @@ { if (self = [super init]) { _length = length; - _isVM = (length >= vm_page_size); + _isVM = false;//(length >= vm_page_size); if (_isVM) { _mutableBytes = mmap(NULL, length, PROT_WRITE | PROT_READ, MAP_ANONYMOUS | MAP_PRIVATE, VM_MAKE_TAG(VM_MEMORY_COREGRAPHICS_DATA), 0); if (_mutableBytes == MAP_FAILED) { @@ -43,7 +43,7 @@ // Check the VM flag again because we may have failed above. if (!_isVM) { - _mutableBytes = calloc(1, length); + _mutableBytes = malloc(length); } } return self; diff --git a/submodules/AsyncDisplayKit/Source/ASEditableTextNode.mm b/submodules/AsyncDisplayKit/Source/ASEditableTextNode.mm index 87baeb09ac..fdd08f0929 100644 --- a/submodules/AsyncDisplayKit/Source/ASEditableTextNode.mm +++ b/submodules/AsyncDisplayKit/Source/ASEditableTextNode.mm @@ -733,9 +733,6 @@ range.location = range.location + range.length - 1; range.length = 1; [self.textView scrollRangeToVisible:range]; - - CGPoint bottomOffset = CGPointMake(0, self.textView.contentSize.height - self.textView.bounds.size.height); - //[self.textView setContentOffset:bottomOffset animated:NO]; } #pragma mark - Keyboard @@ -1045,7 +1042,7 @@ CGFloat baselineNudge = (lineHeight - fontLineHeight) * 0.6f; CGRect rect = *lineFragmentRect; - rect.size.height = lineHeight; + rect.size.height = lineHeight + 2.0f; CGRect usedRect = *lineFragmentUsedRect; usedRect.size.height = MAX(lineHeight, usedRect.size.height); diff --git a/submodules/AsyncDisplayKit/Source/ASGraphicsContext.mm b/submodules/AsyncDisplayKit/Source/ASGraphicsContext.mm index b181ee2728..b950613d0d 100644 --- a/submodules/AsyncDisplayKit/Source/ASGraphicsContext.mm +++ b/submodules/AsyncDisplayKit/Source/ASGraphicsContext.mm @@ -7,7 +7,7 @@ // #import -#import "ASCGImageBuffer.h" +#import #import #import #import diff --git a/submodules/AsyncDisplayKit/Source/ASCGImageBuffer.h b/submodules/AsyncDisplayKit/Source/PublicHeaders/AsyncDisplayKit/ASCGImageBuffer.h similarity index 100% rename from submodules/AsyncDisplayKit/Source/ASCGImageBuffer.h rename to submodules/AsyncDisplayKit/Source/PublicHeaders/AsyncDisplayKit/ASCGImageBuffer.h diff --git a/submodules/AudioBlob/Sources/BlobView.swift b/submodules/AudioBlob/Sources/BlobView.swift index dc7e9823c3..64ade477e5 100644 --- a/submodules/AudioBlob/Sources/BlobView.swift +++ b/submodules/AudioBlob/Sources/BlobView.swift @@ -1,19 +1,51 @@ import Foundation import UIKit +import AsyncDisplayKit import Display import LegacyComponents +public final class VoiceBlobNode: ASDisplayNode { + public init( + maxLevel: CGFloat, + smallBlobRange: VoiceBlobView.BlobRange, + mediumBlobRange: VoiceBlobView.BlobRange, + bigBlobRange: VoiceBlobView.BlobRange + ) { + super.init() + + self.setViewBlock({ + return VoiceBlobView(frame: CGRect(), maxLevel: maxLevel, smallBlobRange: smallBlobRange, mediumBlobRange: mediumBlobRange, bigBlobRange: bigBlobRange) + }) + } + + public func startAnimating(immediately: Bool = false) { + (self.view as? VoiceBlobView)?.startAnimating(immediately: immediately) + } + + public func stopAnimating(duration: Double = 0.15) { + (self.view as? VoiceBlobView)?.stopAnimating(duration: duration) + } + + public func setColor(_ color: UIColor, animated: Bool = false) { + (self.view as? VoiceBlobView)?.setColor(color, animated: animated) + } + + public func updateLevel(_ level: CGFloat, immediately: Bool = false) { + (self.view as? VoiceBlobView)?.updateLevel(level, immediately: immediately) + } +} + public final class VoiceBlobView: UIView, TGModernConversationInputMicButtonDecoration { - private let smallBlob: BlobView - private let mediumBlob: BlobView - private let bigBlob: BlobView + private let smallBlob: BlobNode + private let mediumBlob: BlobNode + private let bigBlob: BlobNode private let maxLevel: CGFloat private var displayLinkAnimator: ConstantDisplayLinkAnimator? private var audioLevel: CGFloat = 0 - private var presentationAudioLevel: CGFloat = 0 + public var presentationAudioLevel: CGFloat = 0 private(set) var isAnimating = false @@ -28,7 +60,7 @@ public final class VoiceBlobView: UIView, TGModernConversationInputMicButtonDeco ) { self.maxLevel = maxLevel - self.smallBlob = BlobView( + self.smallBlob = BlobNode( pointsCount: 8, minRandomness: 0.1, maxRandomness: 0.5, @@ -39,23 +71,23 @@ public final class VoiceBlobView: UIView, TGModernConversationInputMicButtonDeco scaleSpeed: 0.2, isCircle: true ) - self.mediumBlob = BlobView( + self.mediumBlob = BlobNode( pointsCount: 8, minRandomness: 1, maxRandomness: 1, - minSpeed: 1.5, - maxSpeed: 7, + minSpeed: 0.9, + maxSpeed: 4, minScale: mediumBlobRange.min, maxScale: mediumBlobRange.max, scaleSpeed: 0.2, isCircle: false ) - self.bigBlob = BlobView( + self.bigBlob = BlobNode( pointsCount: 8, minRandomness: 1, maxRandomness: 1, - minSpeed: 1.5, - maxSpeed: 7, + minSpeed: 0.9, + maxSpeed: 4, minScale: bigBlobRange.min, maxScale: bigBlobRange.max, scaleSpeed: 0.2, @@ -64,9 +96,9 @@ public final class VoiceBlobView: UIView, TGModernConversationInputMicButtonDeco super.init(frame: frame) - addSubview(bigBlob) - addSubview(mediumBlob) - addSubview(smallBlob) + self.addSubnode(self.bigBlob) + self.addSubnode(self.mediumBlob) + self.addSubnode(self.smallBlob) displayLinkAnimator = ConstantDisplayLinkAnimator() { [weak self] in guard let strongSelf = self else { return } @@ -94,6 +126,10 @@ public final class VoiceBlobView: UIView, TGModernConversationInputMicButtonDeco } public func updateLevel(_ level: CGFloat) { + self.updateLevel(level, immediately: false) + } + + public func updateLevel(_ level: CGFloat, immediately: Bool = false) { let normalizedLevel = min(1, max(level / maxLevel, 0)) smallBlob.updateSpeedLevel(to: normalizedLevel) @@ -101,14 +137,26 @@ public final class VoiceBlobView: UIView, TGModernConversationInputMicButtonDeco bigBlob.updateSpeedLevel(to: normalizedLevel) audioLevel = normalizedLevel + if immediately { + presentationAudioLevel = normalizedLevel + } } public func startAnimating() { + self.startAnimating(immediately: false) + } + + public func startAnimating(immediately: Bool = false) { guard !isAnimating else { return } isAnimating = true - mediumBlob.layer.animateScale(from: 0.5, to: 1, duration: 0.15, removeOnCompletion: false) - bigBlob.layer.animateScale(from: 0.5, to: 1, duration: 0.15, removeOnCompletion: false) + if !immediately { + mediumBlob.layer.animateScale(from: 0.75, to: 1, duration: 0.35, removeOnCompletion: false) + bigBlob.layer.animateScale(from: 0.75, to: 1, duration: 0.35, removeOnCompletion: false) + } else { + mediumBlob.layer.removeAllAnimations() + bigBlob.layer.removeAllAnimations() + } updateBlobsState() @@ -123,8 +171,8 @@ public final class VoiceBlobView: UIView, TGModernConversationInputMicButtonDeco guard isAnimating else { return } isAnimating = false - mediumBlob.layer.animateScale(from: 1.0, to: 0.5, duration: duration, removeOnCompletion: false) - bigBlob.layer.animateScale(from: 1.0, to: 0.5, duration: duration, removeOnCompletion: false) + mediumBlob.layer.animateScale(from: 1.0, to: 0.75, duration: duration, removeOnCompletion: false) + bigBlob.layer.animateScale(from: 1.0, to: 0.75, duration: duration, removeOnCompletion: false) updateBlobsState() @@ -132,8 +180,8 @@ public final class VoiceBlobView: UIView, TGModernConversationInputMicButtonDeco } private func updateBlobsState() { - if isAnimating { - if smallBlob.frame.size != .zero { + if self.isAnimating { + if self.smallBlob.frame.size != .zero { smallBlob.startAnimating() mediumBlob.startAnimating() bigBlob.startAnimating() @@ -148,15 +196,15 @@ public final class VoiceBlobView: UIView, TGModernConversationInputMicButtonDeco override public func layoutSubviews() { super.layoutSubviews() - smallBlob.frame = bounds - mediumBlob.frame = bounds - bigBlob.frame = bounds + self.smallBlob.frame = bounds + self.mediumBlob.frame = bounds + self.bigBlob.frame = bounds - updateBlobsState() + self.updateBlobsState() } } -final class BlobView: UIView { +final class BlobNode: ASDisplayNode { let pointsCount: Int let smoothness: CGFloat @@ -170,17 +218,17 @@ final class BlobView: UIView { let maxScale: CGFloat let scaleSpeed: CGFloat - var scaleLevelsToBalance = [CGFloat]() - let isCircle: Bool var level: CGFloat = 0 { didSet { - CATransaction.begin() - CATransaction.setDisableActions(true) - let lv = minScale + (maxScale - minScale) * level - shapeLayer.transform = CATransform3DMakeScale(lv, lv, 1) - CATransaction.commit() + if abs(self.level - oldValue) > 0.01 { + CATransaction.begin() + CATransaction.setDisableActions(true) + let lv = self.minScale + (self.maxScale - self.minScale) * self.level + self.shapeLayer.transform = CATransform3DMakeScale(lv, lv, 1) + CATransaction.commit() + } } } @@ -243,11 +291,11 @@ final class BlobView: UIView { let angle = (CGFloat.pi * 2) / CGFloat(pointsCount) self.smoothness = ((4 / 3) * tan(angle / 4)) / sin(angle / 2) / 2 - super.init(frame: .zero) + super.init() - layer.addSublayer(shapeLayer) + self.layer.addSublayer(self.shapeLayer) - shapeLayer.transform = CATransform3DMakeScale(minScale, minScale, 1) + self.shapeLayer.transform = CATransform3DMakeScale(minScale, minScale, 1) } required init?(coder: NSCoder) { @@ -255,75 +303,55 @@ final class BlobView: UIView { } func setColor(_ color: UIColor, animated: Bool) { - let previousColor = shapeLayer.fillColor - shapeLayer.fillColor = color.cgColor + let previousColor = self.shapeLayer.fillColor + self.shapeLayer.fillColor = color.cgColor if animated, let previousColor = previousColor { - shapeLayer.animate(from: previousColor, to: color.cgColor, keyPath: "fillColor", timingFunction: CAMediaTimingFunctionName.linear.rawValue, duration: 0.3) + self.shapeLayer.animate(from: previousColor, to: color.cgColor, keyPath: "fillColor", timingFunction: CAMediaTimingFunctionName.linear.rawValue, duration: 0.3) } } func updateSpeedLevel(to newSpeedLevel: CGFloat) { - speedLevel = max(speedLevel, newSpeedLevel) - - if abs(lastSpeedLevel - newSpeedLevel) > 0.5 { - animateToNewShape() - } + self.speedLevel = max(self.speedLevel, newSpeedLevel) } func startAnimating() { - animateToNewShape() + self.animateToNewShape() } func stopAnimating() { - fromPoints = currentPoints - toPoints = nil - pop_removeAnimation(forKey: "blob") + self.shapeLayer.removeAnimation(forKey: "path") } private func animateToNewShape() { guard !isCircle else { return } - if pop_animation(forKey: "blob") != nil { - fromPoints = currentPoints - toPoints = nil - pop_removeAnimation(forKey: "blob") + if self.shapeLayer.path == nil { + let points = generateNextBlob(for: self.bounds.size) + self.shapeLayer.path = UIBezierPath.smoothCurve(through: points, length: bounds.width, smoothness: smoothness).cgPath } - if fromPoints == nil { - fromPoints = generateNextBlob(for: bounds.size) - } - if toPoints == nil { - toPoints = generateNextBlob(for: bounds.size) - } + let nextPoints = generateNextBlob(for: self.bounds.size) + let nextPath = UIBezierPath.smoothCurve(through: nextPoints, length: bounds.width, smoothness: smoothness).cgPath - let animation = POPBasicAnimation() - animation.property = POPAnimatableProperty.property(withName: "blob.transition", initializer: { property in - property?.readBlock = { blobView, values in - guard let blobView = blobView as? BlobView, let values = values else { return } - - values.pointee = blobView.transition - } - property?.writeBlock = { blobView, values in - guard let blobView = blobView as? BlobView, let values = values else { return } - - blobView.transition = values.pointee - } - }) as? POPAnimatableProperty - animation.completionBlock = { [weak self] animation, finished in + let animation = CABasicAnimation(keyPath: "path") + let previousPath = self.shapeLayer.path + self.shapeLayer.path = nextPath + animation.duration = CFTimeInterval(1 / (self.minSpeed + (self.maxSpeed - self.minSpeed) * self.speedLevel)) + animation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut) + animation.fromValue = previousPath + animation.toValue = nextPath + animation.isRemovedOnCompletion = false + animation.fillMode = .forwards + animation.completion = { [weak self] finished in if finished { - self?.fromPoints = self?.currentPoints - self?.toPoints = nil self?.animateToNewShape() } } - animation.duration = CFTimeInterval(1 / (minSpeed + (maxSpeed - minSpeed) * speedLevel)) - animation.timingFunction = CAMediaTimingFunction(name: .linear) - animation.fromValue = 0 - animation.toValue = 1 - pop_add(animation, forKey: "blob") - - lastSpeedLevel = speedLevel - speedLevel = 0 + + self.shapeLayer.add(animation, forKey: "path") + + self.lastSpeedLevel = self.speedLevel + self.speedLevel = 0 } // MARK: Helpers @@ -366,16 +394,16 @@ final class BlobView: UIView { return points } - override func layoutSubviews() { - super.layoutSubviews() + override func layout() { + super.layout() CATransaction.begin() CATransaction.setDisableActions(true) - shapeLayer.position = CGPoint(x: bounds.midX, y: bounds.midY) - if isCircle { - let halfWidth = bounds.width * 0.5 - shapeLayer.path = UIBezierPath( - roundedRect: bounds.offsetBy(dx: -halfWidth, dy: -halfWidth), + self.shapeLayer.position = CGPoint(x: bounds.midX, y: bounds.midY) + if self.isCircle { + let halfWidth = self.bounds.width * 0.5 + self.shapeLayer.path = UIBezierPath( + roundedRect: self.bounds.offsetBy(dx: -halfWidth, dy: -halfWidth), cornerRadius: halfWidth ).cgPath } @@ -384,7 +412,6 @@ final class BlobView: UIView { } private extension UIBezierPath { - static func smoothCurve( through points: [CGPoint], length: CGFloat, @@ -437,7 +464,6 @@ private extension UIBezierPath { } struct SmoothPoint { - let point: CGPoint let inAngle: CGFloat @@ -462,4 +488,3 @@ private extension UIBezierPath { } } } - diff --git a/submodules/AuthTransferUI/Sources/AuthTransferConfirmationScreen.swift b/submodules/AuthTransferUI/Sources/AuthTransferConfirmationScreen.swift index 284622fae1..d5b76addce 100644 --- a/submodules/AuthTransferUI/Sources/AuthTransferConfirmationScreen.swift +++ b/submodules/AuthTransferUI/Sources/AuthTransferConfirmationScreen.swift @@ -30,7 +30,7 @@ public final class AuthDataTransferSplashScreen: ViewController { 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) + let navigationBarTheme = NavigationBarTheme(buttonColor: defaultTheme.buttonColor, disabledButtonColor: defaultTheme.disabledButtonColor, primaryTextColor: defaultTheme.primaryTextColor, backgroundColor: .clear, enableBackgroundBlur: false, 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))) @@ -80,7 +80,7 @@ public final class AuthDataTransferSplashScreen: ViewController { 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) + (self.displayNode as! AuthDataTransferSplashScreenNode).containerLayoutUpdated(layout: layout, navigationHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition) } } @@ -118,7 +118,7 @@ private final class AuthDataTransferSplashScreenNode: ViewControllerTracingNode let buttonText: String - let badgeFont = Font.with(size: 13.0, design: .round, traits: [.bold]) + let badgeFont = Font.with(size: 13.0, design: .round, weight: .bold) let textFont = Font.regular(16.0) let textColor = self.presentationData.theme.list.itemPrimaryTextColor diff --git a/submodules/AuthTransferUI/Sources/AuthTransferScanScreen.swift b/submodules/AuthTransferUI/Sources/AuthTransferScanScreen.swift index c06059be3f..5fbd877788 100644 --- a/submodules/AuthTransferUI/Sources/AuthTransferScanScreen.swift +++ b/submodules/AuthTransferUI/Sources/AuthTransferScanScreen.swift @@ -90,7 +90,7 @@ public final class AuthTransferScanScreen: ViewController { 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) + let navigationBarTheme = NavigationBarTheme(buttonColor: .white, disabledButtonColor: .white, primaryTextColor: .white, backgroundColor: .clear, enableBackgroundBlur: false, 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))) @@ -205,7 +205,7 @@ public final class AuthTransferScanScreen: ViewController { 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) + (self.displayNode as! AuthTransferScanScreenNode).containerLayoutUpdated(layout: layout, navigationHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition) } } diff --git a/submodules/AvatarNode/BUILD b/submodules/AvatarNode/BUILD index 2afd25db42..d601aef087 100644 --- a/submodules/AvatarNode/BUILD +++ b/submodules/AvatarNode/BUILD @@ -16,6 +16,8 @@ swift_library( "//submodules/AppBundle:AppBundle", "//submodules/AccountContext:AccountContext", "//submodules/Emoji:Emoji", + "//submodules/TinyThumbnail:TinyThumbnail", + "//submodules/FastBlur:FastBlur", ], visibility = [ "//visibility:public", diff --git a/submodules/AvatarNode/Sources/AvatarNode.swift b/submodules/AvatarNode/Sources/AvatarNode.swift index c874fa9ea9..22b15b4753 100644 --- a/submodules/AvatarNode/Sources/AvatarNode.swift +++ b/submodules/AvatarNode/Sources/AvatarNode.swift @@ -19,7 +19,7 @@ private let archivedChatsIcon = UIImage(bundleImageName: "Avatar/ArchiveAvatarIc private let repliesIcon = generateTintedImage(image: UIImage(bundleImageName: "Avatar/RepliesMessagesIcon"), color: .white) public func avatarPlaceholderFont(size: CGFloat) -> UIFont { - return Font.with(size: size, design: .round, traits: [.bold]) + return Font.with(size: size, design: .round, weight: .bold) } public enum AvatarNodeClipStyle { @@ -224,7 +224,7 @@ public final class AvatarNode: ASDisplayNode { public init(font: UIFont) { self.font = font - self.imageNode = ImageNode(enableHasImage: true) + self.imageNode = ImageNode(enableHasImage: true, enableAnimatedTransition: true) super.init() @@ -337,7 +337,7 @@ public final class AvatarNode: ASDisplayNode { } 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) + let updatedState: AvatarNodeState = .peerAvatar(peer?.id ?? PeerId(0), peer?.displayLetters ?? [], representation) if updatedState != self.state || overrideImage != self.overrideImage || theme !== self.theme { self.state = updatedState self.overrideImage = overrideImage @@ -381,7 +381,7 @@ public final class AvatarNode: ASDisplayNode { } self.editOverlayNode?.isHidden = true - 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) + parameters = AvatarNodeParameters(theme: theme, accountPeerId: context.account.peerId, peerId: peer?.id ?? PeerId(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 @@ -453,10 +453,10 @@ public final class AvatarNode: ASDisplayNode { colorIndex = explicitColorIndex } else { if let peerId = parameters.peerId { - if peerId.namespace == -1 { + if peerId.namespace == .max { colorIndex = -1 } else { - colorIndex = abs(Int(clamping: peerId.id)) + colorIndex = abs(Int(clamping: peerId.id._internalGetInt32Value())) } } else { colorIndex = -1 @@ -623,17 +623,19 @@ public final class AvatarNode: ASDisplayNode { } } -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)) - context.clip() +public func drawPeerAvatarLetters(context: CGContext, size: CGSize, round: Bool = true, font: UIFont, letters: [String], peerId: PeerId) { + if round { + context.beginPath() + context.addEllipse(in: CGRect(x: 0.0, y: 0.0, width: size.width, height: + size.height)) + context.clip() + } let colorIndex: Int - if peerId.namespace == -1 { + if peerId.namespace == .max { colorIndex = -1 } else { - colorIndex = Int(abs(peerId.id)) + colorIndex = Int(abs(peerId.id._internalGetInt32Value())) } let colorsArray: NSArray diff --git a/submodules/AvatarNode/Sources/PeerAvatar.swift b/submodules/AvatarNode/Sources/PeerAvatar.swift index 9fa6c87d07..e58b3ab6fc 100644 --- a/submodules/AvatarNode/Sources/PeerAvatar.swift +++ b/submodules/AvatarNode/Sources/PeerAvatar.swift @@ -6,6 +6,8 @@ import Display import ImageIO import TelegramCore import SyncCore +import TinyThumbnail +import FastBlur private let roundCorners = { () -> UIImage in let diameter: CGFloat = 60.0 @@ -21,22 +23,43 @@ private let roundCorners = { () -> UIImage in return image }() -public func peerAvatarImageData(account: Account, peerReference: PeerReference?, authorOfMessage: MessageReference?, representation: TelegramMediaImageRepresentation?, synchronousLoad: Bool) -> Signal? { +public enum PeerAvatarImageType { + case blurred + case complete +} + +public func peerAvatarImageData(account: Account, peerReference: PeerReference?, authorOfMessage: MessageReference?, representation: TelegramMediaImageRepresentation?, synchronousLoad: Bool) -> Signal<(Data, PeerAvatarImageType)?, NoError>? { if let smallProfileImage = representation { let resourceData = account.postbox.mediaBox.resourceData(smallProfileImage.resource, attemptSynchronously: synchronousLoad) let imageData = resourceData |> take(1) - |> mapToSignal { maybeData -> Signal in + |> mapToSignal { maybeData -> Signal<(Data, PeerAvatarImageType)?, NoError> in if maybeData.complete { - return .single(try? Data(contentsOf: URL(fileURLWithPath: maybeData.path))) + if let data = try? Data(contentsOf: URL(fileURLWithPath: maybeData.path)) { + return .single((data, .complete)) + } else { + return .single(nil) + } } else { return Signal { subscriber in + var emittedFirstData = false + if let miniData = representation?.immediateThumbnailData, let decodedData = decodeTinyThumbnail(data: miniData) { + emittedFirstData = true + subscriber.putNext((decodedData, .blurred)) + } + let resourceDataDisposable = resourceData.start(next: { data in if data.complete { - subscriber.putNext(try? Data(contentsOf: URL(fileURLWithPath: maybeData.path))) + if let dataValue = try? Data(contentsOf: URL(fileURLWithPath: maybeData.path)) { + subscriber.putNext((dataValue, .complete)) + } else { + subscriber.putNext(nil) + } subscriber.putCompletion() } else { - subscriber.putNext(nil) + if !emittedFirstData { + subscriber.putNext(nil) + } } }, error: { error in subscriber.putError(error) @@ -64,12 +87,25 @@ public func peerAvatarImageData(account: Account, peerReference: PeerReference?, } } -public func peerAvatarCompleteImage(account: Account, peer: Peer, size: CGSize) -> Signal { +public func peerAvatarCompleteImage(account: Account, peer: Peer, size: CGSize, round: Bool = true, font: UIFont = avatarPlaceholderFont(size: 13.0), drawLetters: Bool = true, fullSize: Bool = false, blurred: Bool = false) -> Signal { let iconSignal: Signal - if let signal = peerAvatarImage(account: account, peerReference: PeerReference(peer), authorOfMessage: nil, representation: peer.profileImageRepresentations.first, displayDimensions: size, inset: 0.0, emptyColor: nil, synchronousLoad: false) { - iconSignal = signal + if let signal = peerAvatarImage(account: account, peerReference: PeerReference(peer), authorOfMessage: nil, representation: peer.profileImageRepresentations.first, displayDimensions: size, round: round, blurred: blurred, inset: 0.0, emptyColor: nil, synchronousLoad: fullSize) { + if fullSize, let fullSizeSignal = peerAvatarImage(account: account, peerReference: PeerReference(peer), authorOfMessage: nil, representation: peer.profileImageRepresentations.last, displayDimensions: size, emptyColor: nil, synchronousLoad: true) { + iconSignal = combineLatest(.single(nil) |> then(signal), .single(nil) |> then(fullSizeSignal)) + |> mapToSignal { thumbnailImage, fullSizeImage -> Signal in + if let fullSizeImage = fullSizeImage { + return .single(fullSizeImage.0) + } else if let thumbnailImage = thumbnailImage { + return .single(thumbnailImage.0) + } else { + return .complete() + } + } + } else { + iconSignal = signal |> map { imageVersions -> UIImage? in return imageVersions?.0 + } } } else { let peerId = peer.id @@ -77,10 +113,17 @@ public func peerAvatarCompleteImage(account: Account, peer: Peer, size: CGSize) if displayLetters.count == 2 && displayLetters[0].isSingleEmoji && displayLetters[1].isSingleEmoji { displayLetters = [displayLetters[0]] } + if !drawLetters { + displayLetters = [] + } iconSignal = Signal { subscriber in let image = generateImage(size, rotatedContext: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) - drawPeerAvatarLetters(context: context, size: CGSize(width: size.width, height: size.height), font: avatarPlaceholderFont(size: 13.0), letters: displayLetters, peerId: peerId) + drawPeerAvatarLetters(context: context, size: CGSize(width: size.width, height: size.height), round: round, font: font, letters: displayLetters, peerId: peerId) + if blurred { + context.setFillColor(UIColor(rgb: 0x000000, alpha: 0.5).cgColor) + context.fill(CGRect(origin: CGPoint(), size: size)) + } })?.withRenderingMode(.alwaysOriginal) subscriber.putNext(image) @@ -91,7 +134,7 @@ public func peerAvatarCompleteImage(account: Account, peer: Peer, size: CGSize) return iconSignal } -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>? { +public func peerAvatarImage(account: Account, peerReference: PeerReference?, authorOfMessage: MessageReference?, representation: TelegramMediaImageRepresentation?, displayDimensions: CGSize = CGSize(width: 60.0, height: 60.0), round: Bool = true, blurred: Bool = false, 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<(UIImage, UIImage)?, NoError> in @@ -100,8 +143,8 @@ public func peerAvatarImage(account: Account, peerReference: PeerReference?, aut return .single(nil) } 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) { + if let (data, dataType) = data { + if let imageSource = CGImageSourceCreateWithData(data as CFData, nil), var dataImage = CGImageSourceCreateImageAtIndex(imageSource, 0, nil) { context.clear(CGRect(origin: CGPoint(), size: displayDimensions)) context.setBlendMode(.copy) @@ -109,8 +152,43 @@ public func peerAvatarImage(account: Account, peerReference: PeerReference?, aut context.addEllipse(in: CGRect(origin: CGPoint(), size: displayDimensions).insetBy(dx: inset, dy: inset)) context.clip() } + + var shouldBlur = false + if case .blurred = dataType { + shouldBlur = true + } else if blurred { + shouldBlur = true + } + if shouldBlur { + let imageContextSize = size.width > 200.0 ? CGSize(width: 192.0, height: 192.0) : CGSize(width: 64.0, height: 64.0) + let imageContext = DrawingContext(size: imageContextSize, scale: 1.0, clear: true) + imageContext.withFlippedContext { c in + c.draw(dataImage, in: CGRect(origin: CGPoint(), size: imageContextSize)) + + context.setBlendMode(.saturation) + context.setFillColor(UIColor(rgb: 0xffffff, alpha: 1.0).cgColor) + context.fill(CGRect(origin: CGPoint(), size: size)) + context.setBlendMode(.copy) + } + + telegramFastBlurMore(Int32(imageContext.size.width * imageContext.scale), Int32(imageContext.size.height * imageContext.scale), Int32(imageContext.bytesPerRow), imageContext.bytes) + if size.width > 200.0 { + telegramFastBlurMore(Int32(imageContext.size.width * imageContext.scale), Int32(imageContext.size.height * imageContext.scale), Int32(imageContext.bytesPerRow), imageContext.bytes) + telegramFastBlurMore(Int32(imageContext.size.width * imageContext.scale), Int32(imageContext.size.height * imageContext.scale), Int32(imageContext.bytesPerRow), imageContext.bytes) + telegramFastBlurMore(Int32(imageContext.size.width * imageContext.scale), Int32(imageContext.size.height * imageContext.scale), Int32(imageContext.bytesPerRow), imageContext.bytes) + telegramFastBlurMore(Int32(imageContext.size.width * imageContext.scale), Int32(imageContext.size.height * imageContext.scale), Int32(imageContext.bytesPerRow), imageContext.bytes) + } + + dataImage = imageContext.generateImage()!.cgImage! + } context.draw(dataImage, in: CGRect(origin: CGPoint(), size: displayDimensions).insetBy(dx: inset, dy: inset)) + if blurred { + context.setBlendMode(.normal) + context.setFillColor(UIColor(rgb: 0x000000, alpha: 0.45).cgColor) + context.fill(CGRect(origin: CGPoint(), size: size)) + context.setBlendMode(.copy) + } if round { if displayDimensions.width == 60.0 { context.setBlendMode(.destinationOut) @@ -141,7 +219,7 @@ public func peerAvatarImage(account: Account, peerReference: PeerReference?, aut let unroundedImage: UIImage? if provideUnrounded { unroundedImage = generateImage(displayDimensions, contextGenerator: { size, context -> Void in - if let data = data { + 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) diff --git a/submodules/BotPaymentsUI/BUILD b/submodules/BotPaymentsUI/BUILD index f7213427bf..d917b67fe4 100644 --- a/submodules/BotPaymentsUI/BUILD +++ b/submodules/BotPaymentsUI/BUILD @@ -22,6 +22,8 @@ swift_library( "//submodules/CountrySelectionUI:CountrySelectionUI", "//submodules/AppBundle:AppBundle", "//submodules/PresentationDataUtils:PresentationDataUtils", + "//submodules/OverlayStatusController:OverlayStatusController", + "//submodules/ShimmerEffect:ShimmerEffect", ], visibility = [ "//visibility:public", diff --git a/submodules/BotPaymentsUI/Sources/BotCheckoutActionButton.swift b/submodules/BotPaymentsUI/Sources/BotCheckoutActionButton.swift index 70ccb3b58e..e784f933b6 100644 --- a/submodules/BotPaymentsUI/Sources/BotCheckoutActionButton.swift +++ b/submodules/BotPaymentsUI/Sources/BotCheckoutActionButton.swift @@ -3,103 +3,49 @@ import UIKit import AsyncDisplayKit import Display import PassKit +import ShimmerEffect enum BotCheckoutActionButtonState: Equatable { - case loading case active(String) - case inactive(String) case applePay - - static func ==(lhs: BotCheckoutActionButtonState, rhs: BotCheckoutActionButtonState) -> Bool { - switch lhs { - case .loading: - if case .loading = rhs { - return true - } else { - return false - } - case let .active(title): - if case .active(title) = rhs { - return true - } else { - return false - } - case let .inactive(title): - if case .inactive(title) = rhs { - return true - } else { - return false - } - case .applePay: - if case .applePay = rhs { - return true - } else { - return false - } - } - } + case placeholder } private let titleFont = Font.semibold(17.0) final class BotCheckoutActionButton: HighlightableButtonNode { - static var diameter: CGFloat = 48.0 - - private var inactiveFillColor: UIColor + static var height: CGFloat = 52.0 + private var activeFillColor: UIColor private var foregroundColor: UIColor - - private let progressBackgroundNode: ASImageNode - private let inactiveBackgroundNode: ASImageNode + private let activeBackgroundNode: ASImageNode private var applePayButton: UIButton? private let labelNode: TextNode private var state: BotCheckoutActionButtonState? - private var validLayout: CGSize? + private var validLayout: (CGRect, CGSize)? + + private var placeholderNode: ShimmerEffectNode? - init(inactiveFillColor: UIColor, activeFillColor: UIColor, foregroundColor: UIColor) { - self.inactiveFillColor = inactiveFillColor + init(activeFillColor: UIColor, foregroundColor: UIColor) { self.activeFillColor = activeFillColor self.foregroundColor = foregroundColor - - self.progressBackgroundNode = ASImageNode() - self.progressBackgroundNode.displaysAsynchronously = false - self.progressBackgroundNode.displayWithoutProcessing = true - self.progressBackgroundNode.isLayerBacked = true - self.progressBackgroundNode.image = generateImage(CGSize(width: BotCheckoutActionButton.diameter, height: BotCheckoutActionButton.diameter), rotatedContext: { size, context in - context.clear(CGRect(origin: CGPoint(), size: size)) - let strokeWidth: CGFloat = 2.0 - context.setFillColor(activeFillColor.cgColor) - context.fillEllipse(in: CGRect(origin: CGPoint(), size: size)) - - context.setFillColor(inactiveFillColor.cgColor) - context.fillEllipse(in: CGRect(origin: CGPoint(x: strokeWidth, y: strokeWidth), size: CGSize(width: size.width - strokeWidth * 2.0, height: size.height - strokeWidth * 2.0))) - let cutout: CGFloat = 10.0 - context.fill(CGRect(origin: CGPoint(x: floor((size.width - cutout) / 2.0), y: 0.0), size: CGSize(width: cutout, height: cutout))) - }) - - self.inactiveBackgroundNode = ASImageNode() - self.inactiveBackgroundNode.displaysAsynchronously = false - self.inactiveBackgroundNode.displayWithoutProcessing = true - self.inactiveBackgroundNode.isLayerBacked = true - self.inactiveBackgroundNode.image = generateStretchableFilledCircleImage(diameter: BotCheckoutActionButton.diameter, color: self.foregroundColor, strokeColor: activeFillColor, strokeWidth: 2.0) - self.inactiveBackgroundNode.alpha = 0.0 + + let diameter: CGFloat = 20.0 self.activeBackgroundNode = ASImageNode() self.activeBackgroundNode.displaysAsynchronously = false self.activeBackgroundNode.displayWithoutProcessing = true self.activeBackgroundNode.isLayerBacked = true - self.activeBackgroundNode.image = generateStretchableFilledCircleImage(diameter: BotCheckoutActionButton.diameter, color: activeFillColor) + self.activeBackgroundNode.image = generateStretchableFilledCircleImage(diameter: diameter, color: activeFillColor) self.labelNode = TextNode() self.labelNode.displaysAsynchronously = false self.labelNode.isUserInteractionEnabled = false super.init() - - self.addSubnode(self.progressBackgroundNode) - self.addSubnode(self.inactiveBackgroundNode) + self.addSubnode(self.activeBackgroundNode) self.addSubnode(self.labelNode) } @@ -109,151 +55,91 @@ final class BotCheckoutActionButton: HighlightableButtonNode { let previousState = self.state self.state = state - if let validLayout = self.validLayout, let previousState = previousState { - switch state { - case .loading: - self.inactiveBackgroundNode.layer.animateFrame(from: self.inactiveBackgroundNode.frame, to: self.progressBackgroundNode.frame, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring) - if !self.inactiveBackgroundNode.alpha.isZero { - self.inactiveBackgroundNode.alpha = 0.0 - self.inactiveBackgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3) - } - self.activeBackgroundNode.layer.animateFrame(from: self.activeBackgroundNode.frame, to: self.progressBackgroundNode.frame, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring) - self.activeBackgroundNode.alpha = 0.0 - self.activeBackgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3) - self.labelNode.alpha = 0.0 - self.labelNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3) - - self.progressBackgroundNode.alpha = 1.0 - self.progressBackgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) - - let basicAnimation = CABasicAnimation(keyPath: "transform.rotation.z") - basicAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut) - basicAnimation.duration = 0.8 - basicAnimation.fromValue = NSNumber(value: Float(0.0)) - basicAnimation.toValue = NSNumber(value: Float.pi * 2.0) - basicAnimation.repeatCount = Float.infinity - basicAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear) - - self.progressBackgroundNode.layer.add(basicAnimation, forKey: "progressRotation") - case let .active(title): - if case .active = previousState { - let makeLayout = TextNode.asyncLayout(self.labelNode) - let (labelLayout, labelApply) = makeLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: title, font: titleFont, textColor: self.foregroundColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: validLayout, alignment: .natural, cutout: nil, insets: UIEdgeInsets())) - self.labelNode.frame = CGRect(origin: CGPoint(x: floor((validLayout.width - labelLayout.size.width) / 2.0), y: floor((validLayout.height - labelLayout.size.height) / 2.0)), size: labelLayout.size) - let _ = labelApply() - } else { - self.inactiveBackgroundNode.layer.animateFrame(from: self.progressBackgroundNode.frame, to: self.activeBackgroundNode.frame, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring) - self.inactiveBackgroundNode.alpha = 1.0 - self.progressBackgroundNode.alpha = 0.0 - - self.activeBackgroundNode.layer.animateFrame(from: self.progressBackgroundNode.frame, to: self.activeBackgroundNode.frame, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring) - self.activeBackgroundNode.alpha = 1.0 - self.activeBackgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) - - let makeLayout = TextNode.asyncLayout(self.labelNode) - let (labelLayout, labelApply) = makeLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: title, font: titleFont, textColor: self.foregroundColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: validLayout, alignment: .natural, cutout: nil, insets: UIEdgeInsets())) - self.labelNode.frame = CGRect(origin: CGPoint(x: floor((validLayout.width - labelLayout.size.width) / 2.0), y: floor((validLayout.height - labelLayout.size.height) / 2.0)), size: labelLayout.size) - let _ = labelApply() - self.labelNode.alpha = 1.0 - self.labelNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) - } - case let .inactive(title): - if case .inactive = previousState { - let makeLayout = TextNode.asyncLayout(self.labelNode) - let (labelLayout, labelApply) = makeLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: title, font: titleFont, textColor: self.activeFillColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: validLayout, alignment: .natural, cutout: nil, insets: UIEdgeInsets())) - self.labelNode.frame = CGRect(origin: CGPoint(x: floor((validLayout.width - labelLayout.size.width) / 2.0), y: floor((validLayout.height - labelLayout.size.height) / 2.0)), size: labelLayout.size) - let _ = labelApply() - } else { - self.inactiveBackgroundNode.layer.animateFrame(from: self.inactiveBackgroundNode.frame, to: self.activeBackgroundNode.frame, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring) - self.inactiveBackgroundNode.alpha = 1.0 - self.progressBackgroundNode.alpha = 0.0 - - self.activeBackgroundNode.alpha = 0.0 - - let makeLayout = TextNode.asyncLayout(self.labelNode) - let (labelLayout, labelApply) = makeLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: title, font: titleFont, textColor: self.foregroundColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: validLayout, alignment: .natural, cutout: nil, insets: UIEdgeInsets())) - self.labelNode.frame = CGRect(origin: CGPoint(x: floor((validLayout.width - labelLayout.size.width) / 2.0), y: floor((validLayout.height - labelLayout.size.height) / 2.0)), size: labelLayout.size) - let _ = labelApply() - self.labelNode.alpha = 1.0 - self.labelNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) - } - case .applePay: - if case .applePay = previousState { - - } else { - - } - } - } else { - switch state { - case .loading: - self.labelNode.alpha = 0.0 - self.progressBackgroundNode.alpha = 1.0 - self.activeBackgroundNode.alpha = 0.0 - - let basicAnimation = CABasicAnimation(keyPath: "transform.rotation.z") - basicAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut) - basicAnimation.duration = 0.8 - basicAnimation.fromValue = NSNumber(value: Float(0.0)) - basicAnimation.toValue = NSNumber(value: Float.pi * 2.0) - basicAnimation.repeatCount = Float.infinity - basicAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear) - - self.progressBackgroundNode.layer.add(basicAnimation, forKey: "progressRotation") - case .active: - self.labelNode.alpha = 1.0 - self.progressBackgroundNode.alpha = 0.0 - self.inactiveBackgroundNode.alpha = 0.0 - self.activeBackgroundNode.alpha = 1.0 - case .inactive: - self.labelNode.alpha = 1.0 - self.progressBackgroundNode.alpha = 0.0 - self.inactiveBackgroundNode.alpha = 1.0 - self.activeBackgroundNode.alpha = 0.0 - case .applePay: - self.labelNode.alpha = 0.0 - self.progressBackgroundNode.alpha = 0.0 - self.inactiveBackgroundNode.alpha = 0.0 - self.activeBackgroundNode.alpha = 0.0 - if self.applePayButton == nil { - if #available(iOSApplicationExtension 9.0, iOS 9.0, *) { - let applePayButton = PKPaymentButton(paymentButtonType: .buy, paymentButtonStyle: .black) - self.view.addSubview(applePayButton) - self.applePayButton = applePayButton - } - } - } + if let (absoluteRect, containerSize) = self.validLayout, let previousState = previousState { + self.updateLayout(absoluteRect: absoluteRect, containerSize: containerSize, transition: .immediate) } } } + + @objc private func applePayButtonPressed() { + self.sendActions(forControlEvents: .touchUpInside, with: nil) + } - func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) { - self.validLayout = size - - transition.updateFrame(node: self.progressBackgroundNode, frame: CGRect(origin: CGPoint(x: floor((size.width - BotCheckoutActionButton.diameter) / 2.0), y: 0.0), size: CGSize(width: BotCheckoutActionButton.diameter, height: BotCheckoutActionButton.diameter))) - transition.updateFrame(node: self.inactiveBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: BotCheckoutActionButton.diameter))) - transition.updateFrame(node: self.activeBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: BotCheckoutActionButton.diameter))) - if let applePayButton = self.applePayButton { - applePayButton.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: BotCheckoutActionButton.diameter)) - } + func updateLayout(absoluteRect: CGRect, containerSize: CGSize, transition: ContainedViewLayoutTransition) { + let size = absoluteRect.size + + self.validLayout = (absoluteRect, containerSize) + + transition.updateFrame(node: self.activeBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: BotCheckoutActionButton.height))) var labelSize = self.labelNode.bounds.size if let state = self.state { switch state { - case let .active(title): - let makeLayout = TextNode.asyncLayout(self.labelNode) - let (labelLayout, labelApply) = makeLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: title, font: titleFont, textColor: self.foregroundColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: size, alignment: .natural, cutout: nil, insets: UIEdgeInsets())) - let _ = labelApply() - labelSize = labelLayout.size - case let .inactive(title): - let makeLayout = TextNode.asyncLayout(self.labelNode) - let (labelLayout, labelApply) = makeLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: title, font: titleFont, textColor: self.activeFillColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: size, alignment: .natural, cutout: nil, insets: UIEdgeInsets())) - let _ = labelApply() - labelSize = labelLayout.size - default: - break + case let .active(title): + if let applePayButton = self.applePayButton { + self.applePayButton = nil + applePayButton.removeFromSuperview() + } + + if let placeholderNode = self.placeholderNode { + self.placeholderNode = nil + placeholderNode.removeFromSupernode() + } + + let makeLayout = TextNode.asyncLayout(self.labelNode) + let (labelLayout, labelApply) = makeLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: title, font: titleFont, textColor: self.foregroundColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: size, alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + let _ = labelApply() + labelSize = labelLayout.size + case .applePay: + if self.applePayButton == nil { + if #available(iOSApplicationExtension 9.0, iOS 9.0, *) { + let applePayButton: PKPaymentButton + if #available(iOS 14.0, *) { + applePayButton = PKPaymentButton(paymentButtonType: .buy, paymentButtonStyle: .black) + } else { + applePayButton = PKPaymentButton(paymentButtonType: .buy, paymentButtonStyle: .black) + } + applePayButton.addTarget(self, action: #selector(self.applePayButtonPressed), for: .touchUpInside) + self.view.addSubview(applePayButton) + self.applePayButton = applePayButton + } + } + + if let placeholderNode = self.placeholderNode { + self.placeholderNode = nil + placeholderNode.removeFromSupernode() + } + + if let applePayButton = self.applePayButton { + applePayButton.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: BotCheckoutActionButton.height)) + } + case .placeholder: + if let applePayButton = self.applePayButton { + self.applePayButton = nil + applePayButton.removeFromSuperview() + } + + let contentSize = CGSize(width: 80.0, height: 8.0) + + let shimmerNode: ShimmerEffectNode + if let current = self.placeholderNode { + shimmerNode = current + } else { + shimmerNode = ShimmerEffectNode() + self.placeholderNode = shimmerNode + self.addSubnode(shimmerNode) + } + shimmerNode.frame = CGRect(origin: CGPoint(x: floor((size.width - contentSize.width) / 2.0), y: floor((size.height - contentSize.height) / 2.0)), size: contentSize) + shimmerNode.updateAbsoluteRect(CGRect(origin: CGPoint(x: absoluteRect.minX + shimmerNode.frame.minX, y: absoluteRect.minY + shimmerNode.frame.minY), size: contentSize), within: containerSize) + + var shapes: [ShimmerEffectNode.Shape] = [] + + shapes.append(.roundedRectLine(startPoint: CGPoint(x: 0.0, y: 0.0), width: contentSize.width, diameter: contentSize.height)) + + shimmerNode.update(backgroundColor: self.activeFillColor, foregroundColor: self.activeFillColor.mixedWith(UIColor.white, alpha: 0.25), shimmeringColor: self.activeFillColor.mixedWith(UIColor.white, alpha: 0.15), shapes: shapes, size: contentSize) } } + transition.updateFrame(node: self.labelNode, frame: CGRect(origin: CGPoint(x: floor((size.width - labelSize.width) / 2.0), y: floor((size.height - labelSize.height) / 2.0)), size: labelSize)) } } diff --git a/submodules/BotPaymentsUI/Sources/BotCheckoutController.swift b/submodules/BotPaymentsUI/Sources/BotCheckoutController.swift index c79efa6d0a..0c67b6e05c 100644 --- a/submodules/BotPaymentsUI/Sources/BotCheckoutController.swift +++ b/submodules/BotPaymentsUI/Sources/BotCheckoutController.swift @@ -10,6 +10,64 @@ import TelegramPresentationData import AccountContext public final class BotCheckoutController: ViewController { + public final class InputData { + public enum FetchError { + case generic + } + + let form: BotPaymentForm + let validatedFormInfo: BotPaymentValidatedFormInfo? + + private init( + form: BotPaymentForm, + validatedFormInfo: BotPaymentValidatedFormInfo? + ) { + self.form = form + self.validatedFormInfo = validatedFormInfo + } + + public static func fetch(context: AccountContext, messageId: MessageId) -> Signal { + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + let themeParams: [String: Any] = [ + "bg_color": Int32(bitPattern: presentationData.theme.list.plainBackgroundColor.argb), + "text_color": Int32(bitPattern: presentationData.theme.list.itemPrimaryTextColor.argb), + "link_color": Int32(bitPattern: presentationData.theme.list.itemAccentColor.argb), + "button_color": Int32(bitPattern: presentationData.theme.list.itemCheckColors.fillColor.argb), + "button_text_color": Int32(bitPattern: presentationData.theme.list.itemCheckColors.foregroundColor.argb) + ] + + return context.engine.payments.fetchBotPaymentForm(messageId: messageId, themeParams: themeParams) + |> mapError { _ -> FetchError in + return .generic + } + |> mapToSignal { paymentForm -> Signal in + if let current = paymentForm.savedInfo { + return context.engine.payments.validateBotPaymentForm(saveInfo: true, messageId: messageId, formInfo: current) + |> mapError { _ -> FetchError in + return .generic + } + |> map { result -> InputData in + return InputData( + form: paymentForm, + validatedFormInfo: result + ) + } + |> `catch` { _ -> Signal in + return .single(InputData( + form: paymentForm, + validatedFormInfo: nil + )) + } + } else { + return .single(InputData( + form: paymentForm, + validatedFormInfo: nil + )) + } + } + } + } + private var controllerNode: BotCheckoutControllerNode { return self.displayNode as! BotCheckoutControllerNode } @@ -22,15 +80,20 @@ public final class BotCheckoutController: ViewController { private let context: AccountContext private let invoice: TelegramMediaInvoice private let messageId: MessageId + private let completed: (String, MessageId?) -> Void private var presentationData: PresentationData private var didPlayPresentationAnimation = false + + private let inputData: Promise - public init(context: AccountContext, invoice: TelegramMediaInvoice, messageId: MessageId) { + public init(context: AccountContext, invoice: TelegramMediaInvoice, messageId: MessageId, inputData: Promise, completed: @escaping (String, MessageId?) -> Void) { self.context = context self.invoice = invoice self.messageId = messageId + self.inputData = inputData + self.completed = completed self.presentationData = context.sharedContext.currentPresentationData.with { $0 } @@ -52,15 +115,11 @@ public final class BotCheckoutController: ViewController { } override public func loadDisplayNode() { - let displayNode = BotCheckoutControllerNode(controller: nil, navigationBar: self.navigationBar!, updateNavigationOffset: { [weak self] offset in - if let strongSelf = self { - strongSelf.navigationOffset = offset - } - }, context: self.context, invoice: self.invoice, messageId: self.messageId, present: { [weak self] c, a in + let displayNode = BotCheckoutControllerNode(controller: self, navigationBar: self.navigationBar!, context: self.context, invoice: self.invoice, messageId: self.messageId, inputData: self.inputData, present: { [weak self] c, a in self?.present(c, in: .window(.root), with: a) }, dismissAnimated: { [weak self] in self?.dismiss() - }) + }, completed: self.completed) //displayNode.enableInteractiveDismiss = true @@ -87,7 +146,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, additionalInsets: UIEdgeInsets()) + self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition, additionalInsets: UIEdgeInsets()) } @objc private func cancelPressed() { diff --git a/submodules/BotPaymentsUI/Sources/BotCheckoutControllerNode.swift b/submodules/BotPaymentsUI/Sources/BotCheckoutControllerNode.swift index c8b6f77fa2..2f44761fc7 100644 --- a/submodules/BotPaymentsUI/Sources/BotCheckoutControllerNode.swift +++ b/submodules/BotPaymentsUI/Sources/BotCheckoutControllerNode.swift @@ -18,18 +18,23 @@ import TelegramStringFormatting import PasswordSetupUI import Stripe import LocalAuth +import OverlayStatusController final class BotCheckoutControllerArguments { fileprivate let account: Account fileprivate let openInfo: (BotCheckoutInfoControllerFocus) -> Void fileprivate let openPaymentMethod: () -> Void fileprivate let openShippingMethod: () -> Void + fileprivate let updateTip: (Int64) -> Void + fileprivate let ensureTipInputVisible: () -> Void - fileprivate init(account: Account, openInfo: @escaping (BotCheckoutInfoControllerFocus) -> Void, openPaymentMethod: @escaping () -> Void, openShippingMethod: @escaping () -> Void) { + fileprivate init(account: Account, openInfo: @escaping (BotCheckoutInfoControllerFocus) -> Void, openPaymentMethod: @escaping () -> Void, openShippingMethod: @escaping () -> Void, updateTip: @escaping (Int64) -> Void, ensureTipInputVisible: @escaping () -> Void) { self.account = account self.openInfo = openInfo self.openPaymentMethod = openPaymentMethod self.openShippingMethod = openShippingMethod + self.updateTip = updateTip + self.ensureTipInputVisible = ensureTipInputVisible } } @@ -40,44 +45,88 @@ private enum BotCheckoutSection: Int32 { } enum BotCheckoutEntry: ItemListNodeEntry { + enum StableId: Hashable { + case header + case price(Int) + case actionPlaceholder(Int) + case tip + case paymentMethod + case shippingInfo + case shippingMethod + case nameInfo + case emailInfo + case phoneInfo + } + case header(PresentationTheme, TelegramMediaInvoice, String) - case price(Int, PresentationTheme, String, String, Bool) + case price(Int, PresentationTheme, String, String, Bool, Bool, Int?) + case tip(Int, PresentationTheme, String, String, String, Int64, Int64, [(String, Int64)]) case paymentMethod(PresentationTheme, String, String) case shippingInfo(PresentationTheme, String, String) case shippingMethod(PresentationTheme, String, String) case nameInfo(PresentationTheme, String, String) case emailInfo(PresentationTheme, String, String) case phoneInfo(PresentationTheme, String, String) + case actionPlaceholder(Int, Int) var section: ItemListSectionId { switch self { case .header: - return BotCheckoutSection.header.rawValue - case .price: + return BotCheckoutSection.prices.rawValue + case .price, .tip: return BotCheckoutSection.prices.rawValue default: return BotCheckoutSection.info.rawValue } } - var stableId: Int32 { + var sortId: Int32 { switch self { case .header: return 0 - case let .price(index, _, _, _, _): + case let .price(index, _, _, _, _, _, _): + return 1 + Int32(index) + case let .tip(index, _, _, _, _, _, _, _): + return 1 + Int32(index) + case let .actionPlaceholder(index, _): return 1 + Int32(index) case .paymentMethod: - return 10000 + 0 - case .shippingInfo: - return 10000 + 1 - case .shippingMethod: return 10000 + 2 - case .nameInfo: + case .shippingInfo: return 10000 + 3 - case .emailInfo: + case .shippingMethod: return 10000 + 4 - case .phoneInfo: + case .nameInfo: return 10000 + 5 + case .emailInfo: + return 10000 + 6 + case .phoneInfo: + return 10000 + 7 + } + } + + var stableId: StableId { + switch self { + case .header: + return .header + case let .price(index, _, _, _, _, _, _): + return .price(index) + case .tip: + return .tip + case let .actionPlaceholder(index, _): + return .actionPlaceholder(index) + case .paymentMethod: + return .paymentMethod + case .shippingInfo: + return .shippingInfo + case .shippingMethod: + return .shippingMethod + case .nameInfo: + return .nameInfo + case .emailInfo: + return .emailInfo + case .phoneInfo: + return .phoneInfo } } @@ -98,8 +147,8 @@ enum BotCheckoutEntry: ItemListNodeEntry { } else { return false } - case let .price(lhsIndex, lhsTheme, lhsText, lhsValue, lhsFinal): - if case let .price(rhsIndex, rhsTheme, rhsText, rhsValue, rhsFinal) = rhs { + case let .price(lhsIndex, lhsTheme, lhsText, lhsValue, lhsFinal, lhsHasSeparator, lhsShimmeringIndex): + if case let .price(rhsIndex, rhsTheme, rhsText, rhsValue, rhsFinal, rhsHasSeparator, rhsShimmeringIndex) = rhs { if lhsIndex != rhsIndex { return false } @@ -115,6 +164,29 @@ enum BotCheckoutEntry: ItemListNodeEntry { if lhsFinal != rhsFinal { return false } + if lhsHasSeparator != rhsHasSeparator { + return false + } + if lhsShimmeringIndex != rhsShimmeringIndex { + return false + } + return true + } else { + return false + } + case let .tip(lhsIndex, lhsTheme, lhsText, lhsCurrency, lhsValue, lhsNumericValue, lhsMaxValue, lhsVariants): + if case let .tip(rhsIndex, rhsTheme, rhsText, rhsCurrency, rhsValue, rhsNumericValue, rhsMaxValue, rhsVariants) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsText == rhsText, lhsCurrency == rhsCurrency, lhsValue == rhsValue, lhsNumericValue == rhsNumericValue, lhsMaxValue == rhsMaxValue { + if lhsVariants.count != rhsVariants.count { + return false + } + for i in 0 ..< lhsVariants.count { + if lhsVariants[i].0 != rhsVariants[i].0 { + return false + } + if lhsVariants[i].1 != rhsVariants[i].1 { + return false + } + } return true } else { return false @@ -155,11 +227,17 @@ enum BotCheckoutEntry: ItemListNodeEntry { } else { return false } + case let .actionPlaceholder(index, shimmeringIndex): + if case .actionPlaceholder(index, shimmeringIndex) = rhs { + return true + } else { + return false + } } } static func <(lhs: BotCheckoutEntry, rhs: BotCheckoutEntry) -> Bool { - return lhs.stableId < rhs.stableId + return lhs.sortId < rhs.sortId } func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { @@ -167,32 +245,43 @@ enum BotCheckoutEntry: ItemListNodeEntry { switch self { case let .header(theme, invoice, botName): return BotCheckoutHeaderItem(account: arguments.account, theme: theme, invoice: invoice, botName: botName, sectionId: self.section) - 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): + case let .price(_, theme, text, value, isFinal, hasSeparator, shimmeringIndex): + return BotCheckoutPriceItem(theme: theme, title: text, label: value, isFinal: isFinal, hasSeparator: hasSeparator, shimmeringIndex: shimmeringIndex, sectionId: self.section) + case let .tip(_, _, text, currency, value, numericValue, maxValue, variants): + return BotCheckoutTipItem(theme: presentationData.theme, strings: presentationData.strings, title: text, currency: currency, value: value, numericValue: numericValue, maxValue: maxValue, availableVariants: variants, sectionId: self.section, updateValue: { value in + arguments.updateTip(value) + }, updatedFocus: { isFocused in + if isFocused { + arguments.ensureTipInputVisible() + } + }) + case let .paymentMethod(_, text, value): return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: { arguments.openPaymentMethod() }) - case let .shippingInfo(theme, text, value): + case let .shippingInfo(_, text, value): 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): + case let .shippingMethod(_, text, value): return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: { arguments.openShippingMethod() }) - case let .nameInfo(theme, text, value): + case let .nameInfo(_, text, value): 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): + case let .emailInfo(_, text, value): 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): + case let .phoneInfo(_, text, value): return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: { arguments.openInfo(.phone) }) + case let .actionPlaceholder(_, shimmeringIndex): + return ItemListDisclosureItem(presentationData: presentationData, title: " ", label: " ", sectionId: self.section, style: .blocks, disclosureStyle: .none, action: { + }, shimmeringIndex: shimmeringIndex) } } } @@ -206,12 +295,16 @@ private struct BotCheckoutControllerState: Equatable { } } -private func currentTotalPrice(paymentForm: BotPaymentForm?, validatedFormInfo: BotPaymentValidatedFormInfo?, currentShippingOptionId: String?) -> Int64 { +private func currentTotalPrice(paymentForm: BotPaymentForm?, validatedFormInfo: BotPaymentValidatedFormInfo?, currentShippingOptionId: String?, currentTip: Int64?) -> Int64 { guard let paymentForm = paymentForm else { return 0 } var totalPrice: Int64 = 0 + + if let currentTip = currentTip { + totalPrice += currentTip + } var index = 0 for price in paymentForm.invoice.prices { @@ -235,7 +328,7 @@ private func currentTotalPrice(paymentForm: BotPaymentForm?, validatedFormInfo: return totalPrice } -private func botCheckoutControllerEntries(presentationData: PresentationData, state: BotCheckoutControllerState, invoice: TelegramMediaInvoice, paymentForm: BotPaymentForm?, formInfo: BotPaymentRequestedInfo?, validatedFormInfo: BotPaymentValidatedFormInfo?, currentShippingOptionId: String?, currentPaymentMethod: BotCheckoutPaymentMethod?, botPeer: Peer?) -> [BotCheckoutEntry] { +private func botCheckoutControllerEntries(presentationData: PresentationData, state: BotCheckoutControllerState, invoice: TelegramMediaInvoice, paymentForm: BotPaymentForm?, formInfo: BotPaymentRequestedInfo?, validatedFormInfo: BotPaymentValidatedFormInfo?, currentShippingOptionId: String?, currentPaymentMethod: BotCheckoutPaymentMethod?, currentTip: Int64?, botPeer: Peer?) -> [BotCheckoutEntry] { var entries: [BotCheckoutEntry] = [] var botName = "" @@ -246,10 +339,14 @@ private func botCheckoutControllerEntries(presentationData: PresentationData, st if let paymentForm = paymentForm { var totalPrice: Int64 = 0 + + if let currentTip = currentTip { + totalPrice += currentTip + } var index = 0 for price in paymentForm.invoice.prices { - entries.append(.price(index, presentationData.theme, price.label, formatCurrencyAmount(price.amount, currency: paymentForm.invoice.currency), false)) + entries.append(.price(index, presentationData.theme, price.label, formatCurrencyAmount(price.amount, currency: paymentForm.invoice.currency), false, index == 0, nil)) totalPrice += price.amount index += 1 } @@ -263,7 +360,7 @@ private func botCheckoutControllerEntries(presentationData: PresentationData, st shippingOptionString = option.title for price in option.prices { - entries.append(.price(index, presentationData.theme, price.label, formatCurrencyAmount(price.amount, currency: paymentForm.invoice.currency), false)) + entries.append(.price(index, presentationData.theme, price.label, formatCurrencyAmount(price.amount, currency: paymentForm.invoice.currency), false, false, nil)) totalPrice += price.amount index += 1 } @@ -273,8 +370,26 @@ private func botCheckoutControllerEntries(presentationData: PresentationData, st } } } + + if !entries.isEmpty { + switch entries[entries.count - 1] { + case let .price(index, theme, title, value, _, _, _): + entries[entries.count - 1] = .price(index, theme, title, value, false, index == 0, nil) + default: + break + } + } + + if let tip = paymentForm.invoice.tip { + let tipTitle: String + tipTitle = presentationData.strings.Checkout_OptionalTipItem + entries.append(.tip(index, presentationData.theme, tipTitle, paymentForm.invoice.currency, "\(formatCurrencyAmount(currentTip ?? 0, currency: paymentForm.invoice.currency))", currentTip ?? 0, tip.max, tip.suggested.map { item -> (String, Int64) in + return ("\(formatCurrencyAmount(item, currency: paymentForm.invoice.currency))", item) + })) + index += 1 + } - entries.append(.price(index, presentationData.theme, presentationData.strings.Checkout_TotalAmount, formatCurrencyAmount(totalPrice, currency: paymentForm.invoice.currency), true)) + entries.append(.price(index, presentationData.theme, presentationData.strings.Checkout_TotalAmount, formatCurrencyAmount(totalPrice, currency: paymentForm.invoice.currency), true, true, nil)) var paymentMethodTitle = "" if let currentPaymentMethod = currentPaymentMethod { @@ -317,6 +432,15 @@ private func botCheckoutControllerEntries(presentationData: PresentationData, st if paymentForm.invoice.requestedFields.contains(.phone) { entries.append(.phoneInfo(presentationData.theme, presentationData.strings.Checkout_Phone, formInfo?.phone ?? "")) } + } else { + let numItems = 4 + for index in 0 ..< numItems { + entries.append(.price(index, presentationData.theme, " ", " ", false, index == 0, index)) + } + + for index in numItems ..< numItems + 2 { + entries.append(.actionPlaceholder(index, index - numItems)) + } } return entries @@ -336,7 +460,8 @@ private func formSupportApplePay(_ paymentForm: BotPaymentForm) -> Bool { "sberbank", "yandex", "privatbank", - "tranzzo" + "tranzzo", + "paymaster" ]) if !applePayProviders.contains(nativeProvider.name) { return false @@ -368,14 +493,21 @@ private func availablePaymentMethods(form: BotPaymentForm, current: BotCheckoutP methods.append(current) } } + if let savedCredentials = form.savedCredentials { + if !methods.contains(.savedCredentials(savedCredentials)) { + methods.append(.savedCredentials(savedCredentials)) + } + } return methods } final class BotCheckoutControllerNode: ItemListControllerNode, PKPaymentAuthorizationViewControllerDelegate { + private weak var controller: BotCheckoutController? private let context: AccountContext private let messageId: MessageId private let present: (ViewController, Any?) -> Void private let dismissAnimated: () -> Void + private let completed: (String, MessageId?) -> Void private var stateValue = BotCheckoutControllerState() private let state = ValuePromise(BotCheckoutControllerState(), ignoreRepeated: true) @@ -383,33 +515,44 @@ final class BotCheckoutControllerNode: ItemListControllerNode, PKPaymentAuthoriz private var presentationData: PresentationData - private let paymentFormAndInfo = Promise<(BotPaymentForm, BotPaymentRequestedInfo, BotPaymentValidatedFormInfo?, String?, BotCheckoutPaymentMethod?)?>(nil) + private let paymentFormAndInfo = Promise<(BotPaymentForm, BotPaymentRequestedInfo, BotPaymentValidatedFormInfo?, String?, BotCheckoutPaymentMethod?, Int64?)?>(nil) private var paymentFormValue: BotPaymentForm? private var currentFormInfo: BotPaymentRequestedInfo? private var currentValidatedFormInfo: BotPaymentValidatedFormInfo? private var currentShippingOptionId: String? private var currentPaymentMethod: BotCheckoutPaymentMethod? + private var currentTipAmount: Int64? private var formRequestDisposable: Disposable? - + + private let actionButtonPanelNode: ASDisplayNode + private let actionButtonPanelSeparator: ASDisplayNode private let actionButton: BotCheckoutActionButton private let inProgressDimNode: ASDisplayNode + private var statusController: ViewController? private let payDisposable = MetaDisposable() private let paymentAuthDisposable = MetaDisposable() private var applePayAuthrorizationCompletion: ((PKPaymentAuthorizationStatus) -> Void)? private var applePayController: PKPaymentAuthorizationViewController? + + private var passwordTip: String? + private var passwordTipDisposable: Disposable? - init(controller: ItemListController?, navigationBar: NavigationBar, updateNavigationOffset: @escaping (CGFloat) -> Void, context: AccountContext, invoice: TelegramMediaInvoice, messageId: MessageId, present: @escaping (ViewController, Any?) -> Void, dismissAnimated: @escaping () -> Void) { + init(controller: BotCheckoutController?, navigationBar: NavigationBar, context: AccountContext, invoice: TelegramMediaInvoice, messageId: MessageId, inputData: Promise, present: @escaping (ViewController, Any?) -> Void, dismissAnimated: @escaping () -> Void, completed: @escaping (String, MessageId?) -> Void) { + self.controller = controller self.context = context self.messageId = messageId self.present = present self.dismissAnimated = dismissAnimated + self.completed = completed self.presentationData = context.sharedContext.currentPresentationData.with { $0 } var openInfoImpl: ((BotCheckoutInfoControllerFocus) -> Void)? + var updateTipImpl: ((Int64) -> Void)? var openPaymentMethodImpl: (() -> Void)? var openShippingMethodImpl: (() -> Void)? + var ensureTipInputVisibleImpl: (() -> Void)? let arguments = BotCheckoutControllerArguments(account: context.account, openInfo: { item in openInfoImpl?(item) @@ -417,29 +560,51 @@ final class BotCheckoutControllerNode: ItemListControllerNode, PKPaymentAuthoriz openPaymentMethodImpl?() }, openShippingMethod: { openShippingMethodImpl?() + }, updateTip: { value in + updateTipImpl?(value) + }, ensureTipInputVisible: { + ensureTipInputVisibleImpl?() }) - - 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)) + + let paymentBotPeer = paymentFormAndInfo.get() + |> map { paymentFormAndInfo -> PeerId? in + return paymentFormAndInfo?.0.paymentBotId + } + |> distinctUntilChanged + |> mapToSignal { peerId -> Signal in + return context.account.postbox.transaction { transaction -> Peer? in + return peerId.flatMap(transaction.getPeer) + } } - 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) + let signal: Signal<(ItemListPresentationData, (ItemListNodeState, Any)), NoError> = combineLatest(queue: .mainQueue(), context.sharedContext.presentationData, self.state.get(), paymentFormAndInfo.get(), paymentBotPeer) + |> 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, currentTip: paymentFormAndInfo?.5, botPeer: botPeer), style: .blocks, focusItemTag: nil, emptyStateItem: nil, animateChanges: false) + + return (ItemListPresentationData(presentationData), (nodeState, arguments)) + } + + self.actionButtonPanelNode = ASDisplayNode() + self.actionButtonPanelNode.backgroundColor = self.presentationData.theme.rootController.navigationBar.opaqueBackgroundColor + + self.actionButtonPanelSeparator = ASDisplayNode() + self.actionButtonPanelSeparator.backgroundColor = self.presentationData.theme.rootController.navigationBar.separatorColor + + self.actionButton = BotCheckoutActionButton(activeFillColor: self.presentationData.theme.list.itemAccentColor, foregroundColor: self.presentationData.theme.list.itemCheckColors.foregroundColor) + self.actionButton.setState(.placeholder) self.inProgressDimNode = ASDisplayNode() self.inProgressDimNode.alpha = 0.0 self.inProgressDimNode.isUserInteractionEnabled = false self.inProgressDimNode.backgroundColor = self.presentationData.theme.list.plainBackgroundColor.withAlphaComponent(0.5) - super.init(controller: controller, navigationBar: navigationBar, updateNavigationOffset: updateNavigationOffset, state: signal) + super.init(controller: nil, navigationBar: navigationBar, state: signal) self.arguments = arguments openInfoImpl = { [weak self] focus in if let strongSelf = self, let paymentFormValue = strongSelf.paymentFormValue, let currentFormInfo = strongSelf.currentFormInfo { + strongSelf.controller?.view.endEditing(true) strongSelf.present(BotCheckoutInfoController(context: context, invoice: paymentFormValue.invoice, messageId: messageId, initialFormInfo: currentFormInfo, focus: focus, formInfoUpdated: { formInfo, validatedInfo in if let strongSelf = self, let paymentFormValue = strongSelf.paymentFormValue { strongSelf.currentFormInfo = formInfo @@ -450,8 +615,9 @@ final class BotCheckoutControllerNode: ItemListControllerNode, PKPaymentAuthoriz updatedCurrentShippingOptionId = currentShippingOptionId } } - strongSelf.paymentFormAndInfo.set(.single((paymentFormValue, formInfo, validatedInfo, updatedCurrentShippingOptionId, strongSelf.currentPaymentMethod))) - + + strongSelf.paymentFormAndInfo.set(.single((paymentFormValue, formInfo, validatedInfo, updatedCurrentShippingOptionId, strongSelf.currentPaymentMethod, strongSelf.currentTipAmount))) + strongSelf.updateActionButton() } }), ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) @@ -461,7 +627,8 @@ final class BotCheckoutControllerNode: ItemListControllerNode, PKPaymentAuthoriz let applyPaymentMethod: (BotCheckoutPaymentMethod) -> Void = { [weak self] method in if let strongSelf = self, let paymentFormValue = strongSelf.paymentFormValue, let currentFormInfo = strongSelf.currentFormInfo { strongSelf.currentPaymentMethod = method - strongSelf.paymentFormAndInfo.set(.single((paymentFormValue, currentFormInfo, strongSelf.currentValidatedFormInfo, strongSelf.currentShippingOptionId, strongSelf.currentPaymentMethod))) + strongSelf.paymentFormAndInfo.set(.single((paymentFormValue, currentFormInfo, strongSelf.currentValidatedFormInfo, strongSelf.currentShippingOptionId, strongSelf.currentPaymentMethod, strongSelf.currentTipAmount))) + strongSelf.updateActionButton() } } @@ -491,7 +658,7 @@ final class BotCheckoutControllerNode: ItemListControllerNode, PKPaymentAuthoriz var dismissImpl: (() -> Void)? let canSave = paymentForm.canSaveCredentials || paymentForm.passwordMissing - let controller = BotCheckoutNativeCardEntryController(context: strongSelf.context, additionalFields: additionalFields, publishableKey: publishableKey, completion: { method in + let controller = BotCheckoutNativeCardEntryController(context: strongSelf.context, provider: .stripe(additionalFields: additionalFields, publishableKey: publishableKey), completion: { method in guard let strongSelf = self else { return } @@ -519,7 +686,7 @@ final class BotCheckoutControllerNode: ItemListControllerNode, PKPaymentAuthoriz controller.dismiss() } switch update { - case .noPassword, .awaitingEmailConfirmation: + case .noPassword, .awaitingEmailConfirmation, .pendingPasswordReset: break case .passwordSet: var updatedToken = webToken @@ -535,7 +702,75 @@ final class BotCheckoutControllerNode: ItemListControllerNode, PKPaymentAuthoriz } })]), nil) default: - break + applyPaymentMethod(method) + } + } else { + applyPaymentMethod(method) + } + dismissImpl?() + }) + dismissImpl = { [weak controller] in + controller?.dismiss() + } + strongSelf.present(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + } else if let nativeProvider = paymentForm.nativeProvider, nativeProvider.name == "smartglocal" { + guard let paramsData = nativeProvider.params.data(using: .utf8) else { + return + } + guard let nativeParams = (try? JSONSerialization.jsonObject(with: paramsData)) as? [String: Any] else { + return + } + guard let publicToken = nativeParams["public_token"] as? String else { + return + } + + var dismissImpl: (() -> Void)? + let canSave = paymentForm.canSaveCredentials || paymentForm.passwordMissing + let controller = BotCheckoutNativeCardEntryController(context: strongSelf.context, provider: .smartglobal(isTesting: paymentForm.invoice.isTest, publicToken: publicToken), completion: { method in + guard let strongSelf = self else { + return + } + if canSave && paymentForm.passwordMissing { + switch method { + case let .webToken(webToken) where webToken.saveOnServer: + var text = strongSelf.presentationData.strings.Checkout_NewCard_SaveInfoEnableHelp + text = text.replacingOccurrences(of: "[", with: "") + text = text.replacingOccurrences(of: "]", with: "") + present(textAlertController(context: strongSelf.context, title: nil, text: text, actions: [TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_NotNow, action: { + var updatedToken = webToken + updatedToken.saveOnServer = false + applyPaymentMethod(.webToken(updatedToken)) + }), TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_Yes, action: { + guard let strongSelf = self else { + return + } + if paymentForm.passwordMissing { + var updatedToken = webToken + updatedToken.saveOnServer = false + applyPaymentMethod(.webToken(updatedToken)) + + let controller = SetupTwoStepVerificationController(context: strongSelf.context, initialState: .automatic, stateUpdated: { update, shouldDismiss, controller in + if shouldDismiss { + controller.dismiss() + } + switch update { + case .noPassword, .awaitingEmailConfirmation, .pendingPasswordReset: + break + case .passwordSet: + var updatedToken = webToken + updatedToken.saveOnServer = true + applyPaymentMethod(.webToken(updatedToken)) + } + }) + strongSelf.present(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + } else { + var updatedToken = webToken + updatedToken.saveOnServer = true + applyPaymentMethod(.webToken(updatedToken)) + } + })]), nil) + default: + applyPaymentMethod(method) } } else { applyPaymentMethod(method) @@ -575,7 +810,7 @@ final class BotCheckoutControllerNode: ItemListControllerNode, PKPaymentAuthoriz controller.dismiss() } switch update { - case .noPassword, .awaitingEmailConfirmation: + case .noPassword, .awaitingEmailConfirmation, .pendingPasswordReset: break case .passwordSet: var updatedToken = token @@ -608,9 +843,43 @@ final class BotCheckoutControllerNode: ItemListControllerNode, PKPaymentAuthoriz } } } + + updateTipImpl = { [weak self] value in + guard let strongSelf = self, let paymentFormValue = strongSelf.paymentFormValue, let currentFormInfo = strongSelf.currentFormInfo else { + return + } + + if strongSelf.currentTipAmount == value { + return + } + + strongSelf.currentTipAmount = value + + strongSelf.paymentFormAndInfo.set(.single((paymentFormValue, currentFormInfo, strongSelf.currentValidatedFormInfo, strongSelf.currentShippingOptionId, strongSelf.currentPaymentMethod, strongSelf.currentTipAmount))) + + strongSelf.updateActionButton() + } + + ensureTipInputVisibleImpl = { [weak self] in + self?.afterLayout({ + guard let strongSelf = self else { + return + } + var selectedItemNode: ListViewItemNode? + strongSelf.listNode.forEachItemNode { itemNode in + if let itemNode = itemNode as? BotCheckoutTipItemNode { + selectedItemNode = itemNode + } + } + if let selectedItemNode = selectedItemNode { + strongSelf.listNode.ensureItemNodeVisible(selectedItemNode, atTop: true) + } + }) + } openPaymentMethodImpl = { [weak self] in if let strongSelf = self, let paymentForm = strongSelf.paymentFormValue { + strongSelf.controller?.view.endEditing(true) let methods = availablePaymentMethods(form: paymentForm, current: strongSelf.currentPaymentMethod) if methods.isEmpty { openNewCard() @@ -626,10 +895,11 @@ final class BotCheckoutControllerNode: ItemListControllerNode, PKPaymentAuthoriz openShippingMethodImpl = { [weak self] in if let strongSelf = self, let paymentFormValue = strongSelf.paymentFormValue, let shippingOptions = strongSelf.currentValidatedFormInfo?.shippingOptions, !shippingOptions.isEmpty { + strongSelf.controller?.view.endEditing(true) strongSelf.present(BotCheckoutPaymentShippingOptionSheetController(context: strongSelf.context, currency: paymentFormValue.invoice.currency, options: shippingOptions, currentId: strongSelf.currentShippingOptionId, applyValue: { id in if let strongSelf = self, let paymentFormValue = strongSelf.paymentFormValue, let currentFormInfo = strongSelf.currentFormInfo { strongSelf.currentShippingOptionId = id - strongSelf.paymentFormAndInfo.set(.single((paymentFormValue, currentFormInfo, strongSelf.currentValidatedFormInfo, strongSelf.currentShippingOptionId, strongSelf.currentPaymentMethod))) + strongSelf.paymentFormAndInfo.set(.single((paymentFormValue, currentFormInfo, strongSelf.currentValidatedFormInfo, strongSelf.currentShippingOptionId, strongSelf.currentPaymentMethod, strongSelf.currentTipAmount))) strongSelf.updateActionButton() } @@ -637,83 +907,117 @@ final class BotCheckoutControllerNode: ItemListControllerNode, PKPaymentAuthoriz } } - let formAndMaybeValidatedInfo = fetchBotPaymentForm(postbox: context.account.postbox, network: context.account.network, messageId: messageId) - |> mapToSignal { paymentForm -> Signal<(BotPaymentForm, BotPaymentValidatedFormInfo?), BotPaymentFormRequestError> in - if let current = paymentForm.savedInfo { - return validateBotPaymentForm(network: context.account.network, saveInfo: true, messageId: messageId, formInfo: current) - |> mapError { _ -> BotPaymentFormRequestError in - return .generic - } - |> map { result -> (BotPaymentForm, BotPaymentValidatedFormInfo?) in - return (paymentForm, result) - } - |> `catch` { _ -> Signal<(BotPaymentForm, BotPaymentValidatedFormInfo?), BotPaymentFormRequestError> in - return .single((paymentForm, nil)) - } - } else { - return .single((paymentForm, nil)) - } - } - - self.formRequestDisposable = (formAndMaybeValidatedInfo |> deliverOnMainQueue).start(next: { [weak self] form, validatedInfo in + self.formRequestDisposable = (inputData.get() |> deliverOnMainQueue).start(next: { [weak self] formAndValidatedInfo in if let strongSelf = self { + guard let formAndValidatedInfo = formAndValidatedInfo else { + strongSelf.controller?.dismiss() + return + } + UIView.transition(with: strongSelf.view, duration: 0.25, options: UIView.AnimationOptions.transitionCrossDissolve, animations: { + }, completion: nil) + let savedInfo: BotPaymentRequestedInfo - if let current = form.savedInfo { + if let current = formAndValidatedInfo.form.savedInfo { savedInfo = current } else { savedInfo = BotPaymentRequestedInfo(name: nil, phone: nil, email: nil, shippingAddress: nil) } - strongSelf.paymentFormValue = form + strongSelf.paymentFormValue = formAndValidatedInfo.form strongSelf.currentFormInfo = savedInfo - strongSelf.currentValidatedFormInfo = validatedInfo - if let savedCredentials = form.savedCredentials { + strongSelf.currentValidatedFormInfo = formAndValidatedInfo.validatedFormInfo + if let savedCredentials = formAndValidatedInfo.form.savedCredentials { strongSelf.currentPaymentMethod = .savedCredentials(savedCredentials) } strongSelf.actionButton.isEnabled = true - strongSelf.paymentFormAndInfo.set(.single((form, savedInfo, validatedInfo, nil, strongSelf.currentPaymentMethod))) + strongSelf.paymentFormAndInfo.set(.single((formAndValidatedInfo.form, savedInfo, formAndValidatedInfo.validatedFormInfo, nil, strongSelf.currentPaymentMethod, strongSelf.currentTipAmount))) strongSelf.updateActionButton() } - }, error: { _ in - }) + + self.addSubnode(self.actionButtonPanelNode) + self.actionButtonPanelNode.addSubnode(self.actionButtonPanelSeparator) + self.actionButtonPanelNode.addSubnode(self.actionButton) self.actionButton.addTarget(self, action: #selector(self.actionButtonPressed), forControlEvents: .touchUpInside) self.actionButton.isEnabled = false - self.addSubnode(self.actionButton) self.listNode.supernode?.insertSubnode(self.inProgressDimNode, aboveSubnode: self.listNode) + + self.passwordTipDisposable = (self.context.engine.auth.twoStepVerificationConfiguration() + |> deliverOnMainQueue).start(next: { [weak self] value in + guard let strongSelf = self else { + return + } + switch value { + case .notSet: + break + case let .set(hint, _, _, _, _): + if !hint.isEmpty { + strongSelf.passwordTip = hint + } + } + }) } deinit { self.formRequestDisposable?.dispose() self.payDisposable.dispose() self.paymentAuthDisposable.dispose() + self.passwordTipDisposable?.dispose() } private func updateActionButton() { - let totalAmount = currentTotalPrice(paymentForm: self.paymentFormValue, validatedFormInfo: self.currentValidatedFormInfo, currentShippingOptionId: self.currentShippingOptionId) + let totalAmount = currentTotalPrice(paymentForm: self.paymentFormValue, validatedFormInfo: self.currentValidatedFormInfo, currentShippingOptionId: self.currentShippingOptionId, currentTip: self.currentTipAmount) let payString: String if let paymentForm = self.paymentFormValue, totalAmount > 0 { payString = self.presentationData.strings.Checkout_PayPrice(formatCurrencyAmount(totalAmount, currency: paymentForm.invoice.currency)).0 } else { payString = self.presentationData.strings.CheckoutInfo_Pay } - if self.actionButton.isEnabled { - self.actionButton.setState(.active(payString)) + if let currentPaymentMethod = self.currentPaymentMethod { + switch currentPaymentMethod { + case .applePay: + self.actionButton.setState(.applePay) + default: + self.actionButton.setState(.active(payString)) + } } else { - self.actionButton.setState(.loading) + self.actionButton.setState(.active(payString)) + } + self.actionButtonPanelNode.isHidden = false + } + + private func updateIsInProgress(_ value: Bool) { + if value { + if self.statusController == nil { + let statusController = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: nil)) + self.statusController = statusController + self.controller?.present(statusController, in: .window(.root)) + } + } else if let statusController = self.statusController { + self.statusController = nil + statusController.dismiss() } } 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, additionalInsets: layout.additionalInsets, 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)) + + let bottomPanelHorizontalInset: CGFloat = 16.0 + let bottomPanelVerticalInset: CGFloat = 16.0 + let bottomPanelHeight = max(updatedInsets.bottom, layout.inputHeight ?? 0.0) + bottomPanelVerticalInset * 2.0 + BotCheckoutActionButton.height + + transition.updateFrame(node: self.actionButtonPanelNode, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - bottomPanelHeight), size: CGSize(width: layout.size.width, height: bottomPanelHeight))) + transition.updateFrame(node: self.actionButtonPanelSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: layout.size.width, height: UIScreenPixel))) + + let actionButtonFrame = CGRect(origin: CGPoint(x: bottomPanelHorizontalInset, y: bottomPanelVerticalInset), size: CGSize(width: layout.size.width - bottomPanelHorizontalInset * 2.0, height: BotCheckoutActionButton.height)) transition.updateFrame(node: self.actionButton, frame: actionButtonFrame) - self.actionButton.updateLayout(size: actionButtonFrame.size, transition: transition) + self.actionButton.updateLayout(absoluteRect: actionButtonFrame.offsetBy(dx: self.actionButtonPanelNode.frame.minX, dy: self.actionButtonPanelNode.frame.minY), containerSize: layout.size, transition: transition) + + updatedInsets.bottom = bottomPanelHeight + + super.containerLayoutUpdated(ContainerViewLayout(size: layout.size, metrics: layout.metrics, deviceMetrics: layout.deviceMetrics, intrinsicInsets: updatedInsets, safeInsets: layout.safeInsets, additionalInsets: layout.additionalInsets, statusBarHeight: layout.statusBarHeight, inputHeight: layout.inputHeight, inputHeightIsInteractivellyChanging: layout.inputHeightIsInteractivellyChanging, inVoiceOver: layout.inVoiceOver), navigationBarHeight: navigationBarHeight, transition: transition, additionalInsets: additionalInsets) transition.updateFrame(node: self.inProgressDimNode, frame: self.listNode.frame) } @@ -765,7 +1069,7 @@ final class BotCheckoutControllerNode: ItemListControllerNode, PKPaymentAuthoriz if let savedCredentialsToken = savedCredentialsToken { credentials = .saved(id: id, tempPassword: savedCredentialsToken.token) } else { - let _ = (cachedTwoStepPasswordToken(postbox: self.context.account.postbox) + let _ = (self.context.engine.auth.cachedTwoStepPasswordToken() |> deliverOnMainQueue).start(next: { [weak self] token in if let strongSelf = self { let timestamp = strongSelf.context.account.network.getApproximateRemoteTimestamp() @@ -835,11 +1139,14 @@ final class BotCheckoutControllerNode: ItemListControllerNode, PKPaymentAuthoriz var items: [PKPaymentSummaryItem] = [] var totalAmount: Int64 = 0 + for price in paymentForm.invoice.prices { totalAmount += price.amount - - let amount = NSDecimalNumber(value: Double(price.amount) * 0.01) - items.append(PKPaymentSummaryItem(label: price.label, amount: amount)) + + if let fractional = currencyToFractionalAmount(value: price.amount, currency: paymentForm.invoice.currency) { + let amount = NSDecimalNumber(value: fractional) + items.append(PKPaymentSummaryItem(label: price.label, amount: amount)) + } } if let shippingOptions = strongSelf.currentValidatedFormInfo?.shippingOptions, let shippingOptionId = strongSelf.currentShippingOptionId { @@ -852,9 +1159,20 @@ final class BotCheckoutControllerNode: ItemListControllerNode, PKPaymentAuthoriz } } } - - let amount = NSDecimalNumber(value: Double(totalAmount) * 0.01) - items.append(PKPaymentSummaryItem(label: botPeer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder), amount: amount)) + + if let tipAmount = strongSelf.currentTipAmount { + totalAmount += tipAmount + + if let fractional = currencyToFractionalAmount(value: tipAmount, currency: paymentForm.invoice.currency) { + let amount = NSDecimalNumber(value: fractional) + items.append(PKPaymentSummaryItem(label: strongSelf.presentationData.strings.Checkout_TipItem, amount: amount)) + } + } + + if let fractionalTotal = currencyToFractionalAmount(value: totalAmount, currency: paymentForm.invoice.currency) { + let amount = NSDecimalNumber(value: fractionalTotal) + items.append(PKPaymentSummaryItem(label: botPeer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder), amount: amount)) + } request.paymentSummaryItems = items @@ -874,20 +1192,20 @@ final class BotCheckoutControllerNode: ItemListControllerNode, PKPaymentAuthoriz } if !liabilityNoticeAccepted { - let messageId = self.messageId let botPeer: Signal = self.context.account.postbox.transaction { transaction -> Peer? in - if let message = transaction.getMessage(messageId) { - return message.author - } - return nil + return transaction.getPeer(paymentForm.paymentBotId) } - let _ = (combineLatest(ApplicationSpecificNotice.getBotPaymentLiability(accountManager: self.context.sharedContext.accountManager, peerId: self.messageId.peerId), botPeer, self.context.account.postbox.loadedPeerWithId(paymentForm.providerId)) + let _ = (combineLatest(ApplicationSpecificNotice.getBotPaymentLiability(accountManager: self.context.sharedContext.accountManager, peerId: paymentForm.paymentBotId), botPeer, self.context.account.postbox.loadedPeerWithId(paymentForm.providerId)) |> deliverOnMainQueue).start(next: { [weak self] value, botPeer, providerPeer in if let strongSelf = self, let botPeer = botPeer { if value { strongSelf.pay(savedCredentialsToken: savedCredentialsToken, liabilityNoticeAccepted: true) } else { - strongSelf.present(textAlertController(context: strongSelf.context, title: strongSelf.presentationData.strings.Checkout_LiabilityAlertTitle, text: strongSelf.presentationData.strings.Checkout_LiabilityAlert(botPeer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder), providerPeer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).0, actions: [TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: { }), TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: { + let paymentText = strongSelf.presentationData.strings.Checkout_PaymentLiabilityAlert + .replacingOccurrences(of: "{target}", with: botPeer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)) + .replacingOccurrences(of: "{payment_system}", with: providerPeer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)) + + strongSelf.present(textAlertController(context: strongSelf.context, title: strongSelf.presentationData.strings.Checkout_LiabilityAlertTitle, text: paymentText, actions: [TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: { }), TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: { if let strongSelf = self { let _ = ApplicationSpecificNotice.setBotPaymentLiability(accountManager: strongSelf.context.sharedContext.accountManager, peerId: strongSelf.messageId.peerId).start() strongSelf.pay(savedCredentialsToken: savedCredentialsToken, liabilityNoticeAccepted: true) @@ -901,11 +1219,22 @@ final class BotCheckoutControllerNode: ItemListControllerNode, PKPaymentAuthoriz self.inProgressDimNode.alpha = 1.0 self.actionButton.isEnabled = false self.updateActionButton() - self.payDisposable.set((sendBotPaymentForm(account: self.context.account, messageId: self.messageId, validatedInfoId: self.currentValidatedFormInfo?.id, shippingOptionId: self.currentShippingOptionId, credentials: credentials) |> deliverOnMainQueue).start(next: { [weak self] result in + self.updateIsInProgress(true) + + var tipAmount = self.currentTipAmount + if tipAmount == nil, let _ = paymentForm.invoice.tip { + tipAmount = 0 + } + + let totalAmount = currentTotalPrice(paymentForm: paymentForm, validatedFormInfo: self.currentValidatedFormInfo, currentShippingOptionId: self.currentShippingOptionId, currentTip: self.currentTipAmount) + let currencyValue = formatCurrencyAmount(totalAmount, currency: paymentForm.invoice.currency) + + self.payDisposable.set((self.context.engine.payments.sendBotPaymentForm(messageId: self.messageId, formId: paymentForm.id, validatedInfoId: self.currentValidatedFormInfo?.id, shippingOptionId: self.currentShippingOptionId, tipAmount: tipAmount, credentials: credentials) |> deliverOnMainQueue).start(next: { [weak self] result in if let strongSelf = self { strongSelf.inProgressDimNode.isUserInteractionEnabled = false strongSelf.inProgressDimNode.alpha = 0.0 strongSelf.actionButton.isEnabled = true + strongSelf.updateIsInProgress(false) if let applePayAuthrorizationCompletion = strongSelf.applePayAuthrorizationCompletion { strongSelf.applePayAuthrorizationCompletion = nil applePayAuthrorizationCompletion(.success) @@ -914,19 +1243,32 @@ final class BotCheckoutControllerNode: ItemListControllerNode, PKPaymentAuthoriz strongSelf.applePayController = nil applePayController.presentingViewController?.dismiss(animated: true, completion: nil) } + + let proceedWithCompletion: (Bool, MessageId?) -> Void = { success, receiptMessageId in + guard let strongSelf = self else { + return + } + + if success { + strongSelf.dismissAnimated() + strongSelf.completed(currencyValue, receiptMessageId) + } else { + strongSelf.dismissAnimated() + } + } switch result { - case .done: - strongSelf.dismissAnimated() + case let .done(receiptMessageId): + proceedWithCompletion(true, receiptMessageId) case let .externalVerificationRequired(url): strongSelf.updateActionButton() - var dismissImpl: (() -> Void)? - let controller = BotCheckoutWebInteractionController(context: strongSelf.context, url: url, intent: .externalVerification({ _ in - dismissImpl?() + var dismissImpl: ((Bool) -> Void)? + let controller = BotCheckoutWebInteractionController(context: strongSelf.context, url: url, intent: .externalVerification({ success in + dismissImpl?(success) })) - dismissImpl = { [weak controller] in + dismissImpl = { [weak controller] success in controller?.dismiss() - self?.dismissAnimated() + proceedWithCompletion(success, nil) } strongSelf.present(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) } @@ -937,6 +1279,7 @@ final class BotCheckoutControllerNode: ItemListControllerNode, PKPaymentAuthoriz strongSelf.inProgressDimNode.alpha = 0.0 strongSelf.actionButton.isEnabled = true strongSelf.updateActionButton() + strongSelf.updateIsInProgress(false) if let applePayAuthrorizationCompletion = strongSelf.applePayAuthrorizationCompletion { strongSelf.applePayAuthrorizationCompletion = nil applePayAuthrorizationCompletion(.failure) @@ -974,7 +1317,7 @@ final class BotCheckoutControllerNode: ItemListControllerNode, PKPaymentAuthoriz period = 1 * 60 * 60 requiresBiometrics = false } - self.present(botCheckoutPasswordEntryController(context: self.context, strings: self.presentationData.strings, cartTitle: cardTitle, period: period, requiresBiometrics: requiresBiometrics, completion: { [weak self] token in + self.present(botCheckoutPasswordEntryController(context: self.context, strings: self.presentationData.strings, passwordTip: self.passwordTip, cartTitle: cardTitle, period: period, requiresBiometrics: requiresBiometrics, completion: { [weak self] token in if let strongSelf = self { let durationString = timeIntervalString(strings: strongSelf.presentationData.strings, value: period) @@ -997,7 +1340,7 @@ final class BotCheckoutControllerNode: ItemListControllerNode, PKPaymentAuthoriz }), TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Yes, action: { if let strongSelf = self { - let _ = cacheTwoStepPasswordToken(postbox: strongSelf.context.account.postbox, token: token).start() + let _ = strongSelf.context.engine.auth.cacheTwoStepPasswordToken(token: token).start() strongSelf.pay(savedCredentialsToken: token) } }) diff --git a/submodules/BotPaymentsUI/Sources/BotCheckoutHeaderItem.swift b/submodules/BotPaymentsUI/Sources/BotCheckoutHeaderItem.swift index ada9ca2d90..2946d5ca0b 100644 --- a/submodules/BotPaymentsUI/Sources/BotCheckoutHeaderItem.swift +++ b/submodules/BotPaymentsUI/Sources/BotCheckoutHeaderItem.swift @@ -80,7 +80,6 @@ class BotCheckoutHeaderItemNode: ListViewItemNode { init() { self.backgroundNode = ASDisplayNode() self.backgroundNode.isLayerBacked = true - self.backgroundNode.backgroundColor = .white self.topStripeNode = ASDisplayNode() self.topStripeNode.isLayerBacked = true @@ -109,7 +108,8 @@ class BotCheckoutHeaderItemNode: ListViewItemNode { self.highlightedBackgroundNode.isLayerBacked = true super.init(layerBacked: false, dynamicBounce: false) - + + self.addSubnode(self.backgroundNode) self.addSubnode(self.imageNode) self.addSubnode(self.titleNode) self.addSubnode(self.textNode) @@ -209,9 +209,9 @@ class BotCheckoutHeaderItemNode: ListViewItemNode { } strongSelf.imageNode.frame = CGRect(origin: CGPoint(x: contentInsets.left, y: contentInsets.top), size: imageSize) - if strongSelf.backgroundNode.supernode != nil { + /*if strongSelf.backgroundNode.supernode != nil { strongSelf.backgroundNode.removeFromSupernode() - } + }*/ if strongSelf.topStripeNode.supernode != nil { strongSelf.topStripeNode.removeFromSupernode() } @@ -231,7 +231,8 @@ class BotCheckoutHeaderItemNode: ListViewItemNode { strongSelf.textNode.frame = textFrame strongSelf.botNameNode.frame = CGRect(origin: CGPoint(x: textFrame.minX, y: textFrame.maxY + textBotNameSpacing), size: botNameLayout.size) - + + strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -1000.0), size: CGSize(width: params.width, height: contentSize.height + 1000.0)) strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: 44.0 + UIScreenPixel + UIScreenPixel)) } }) diff --git a/submodules/BotPaymentsUI/Sources/BotCheckoutInfoController.swift b/submodules/BotPaymentsUI/Sources/BotCheckoutInfoController.swift index 64d7b0cf5f..a1f316815e 100644 --- a/submodules/BotPaymentsUI/Sources/BotCheckoutInfoController.swift +++ b/submodules/BotPaymentsUI/Sources/BotCheckoutInfoController.swift @@ -144,7 +144,7 @@ final class BotCheckoutInfoController: 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.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition) } @objc func cancelPressed() { diff --git a/submodules/BotPaymentsUI/Sources/BotCheckoutInfoControllerNode.swift b/submodules/BotPaymentsUI/Sources/BotCheckoutInfoControllerNode.swift index 03df37910f..034628432e 100644 --- a/submodules/BotPaymentsUI/Sources/BotCheckoutInfoControllerNode.swift +++ b/submodules/BotPaymentsUI/Sources/BotCheckoutInfoControllerNode.swift @@ -338,7 +338,7 @@ final class BotCheckoutInfoControllerNode: ViewControllerTracingNode, UIScrollVi func verify() { self.isVerifying = true let formInfo = self.collectFormInfo() - self.verifyDisposable.set((validateBotPaymentForm(network: self.context.account.network, saveInfo: self.saveInfoItem.isOn, messageId: self.messageId, formInfo: formInfo) |> deliverOnMainQueue).start(next: { [weak self] result in + self.verifyDisposable.set((self.context.engine.payments.validateBotPaymentForm(saveInfo: self.saveInfoItem.isOn, messageId: self.messageId, formInfo: formInfo) |> deliverOnMainQueue).start(next: { [weak self] result in if let strongSelf = self { strongSelf.formInfoUpdated(formInfo, result) } diff --git a/submodules/BotPaymentsUI/Sources/BotCheckoutNativeCardEntryController.swift b/submodules/BotPaymentsUI/Sources/BotCheckoutNativeCardEntryController.swift index c0449e73e2..a5d7cb506d 100644 --- a/submodules/BotPaymentsUI/Sources/BotCheckoutNativeCardEntryController.swift +++ b/submodules/BotPaymentsUI/Sources/BotCheckoutNativeCardEntryController.swift @@ -30,13 +30,17 @@ struct BotCheckoutNativeCardEntryAdditionalFields: OptionSet { } final class BotCheckoutNativeCardEntryController: ViewController { + enum Provider { + case stripe(additionalFields: BotCheckoutNativeCardEntryAdditionalFields, publishableKey: String) + case smartglobal(isTesting: Bool, publicToken: String) + } + private var controllerNode: BotCheckoutNativeCardEntryControllerNode { return super.displayNode as! BotCheckoutNativeCardEntryControllerNode } private let context: AccountContext - private let additionalFields: BotCheckoutNativeCardEntryAdditionalFields - private let publishableKey: String + private let provider: Provider private let completion: (BotCheckoutPaymentMethod) -> Void private var presentationData: PresentationData @@ -46,10 +50,9 @@ final class BotCheckoutNativeCardEntryController: ViewController { private var doneItem: UIBarButtonItem? private var activityItem: UIBarButtonItem? - public init(context: AccountContext, additionalFields: BotCheckoutNativeCardEntryAdditionalFields, publishableKey: String, completion: @escaping (BotCheckoutPaymentMethod) -> Void) { + public init(context: AccountContext, provider: Provider, completion: @escaping (BotCheckoutPaymentMethod) -> Void) { self.context = context - self.additionalFields = additionalFields - self.publishableKey = publishableKey + self.provider = provider self.completion = completion self.presentationData = context.sharedContext.currentPresentationData.with { $0 } @@ -71,7 +74,7 @@ final class BotCheckoutNativeCardEntryController: ViewController { } override public func loadDisplayNode() { - self.displayNode = BotCheckoutNativeCardEntryControllerNode(additionalFields: self.additionalFields, publishableKey: self.publishableKey, theme: self.presentationData.theme, strings: self.presentationData.strings, present: { [weak self] c, a in + self.displayNode = BotCheckoutNativeCardEntryControllerNode(context: self.context, provider: self.provider, theme: self.presentationData.theme, strings: self.presentationData.strings, present: { [weak self] c, a in self?.present(c, in: .window(.root), with: a) }, dismiss: { [weak self] in self?.presentingViewController?.dismiss(animated: false, completion: nil) @@ -139,7 +142,7 @@ final class BotCheckoutNativeCardEntryController: 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.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition) } @objc func cancelPressed() { diff --git a/submodules/BotPaymentsUI/Sources/BotCheckoutNativeCardEntryControllerNode.swift b/submodules/BotPaymentsUI/Sources/BotCheckoutNativeCardEntryControllerNode.swift index d0b2b3e9d5..0072d16381 100644 --- a/submodules/BotPaymentsUI/Sources/BotCheckoutNativeCardEntryControllerNode.swift +++ b/submodules/BotPaymentsUI/Sources/BotCheckoutNativeCardEntryControllerNode.swift @@ -9,6 +9,8 @@ import SwiftSignalKit import TelegramPresentationData import Stripe import CountrySelectionUI +import PresentationDataUtils +import AccountContext private final class BotCheckoutNativeCardEntryScrollerNodeView: UIScrollView { var ignoreUpdateBounds = false @@ -42,7 +44,8 @@ private final class BotCheckoutNativeCardEntryScrollerNode: ASDisplayNode { } final class BotCheckoutNativeCardEntryControllerNode: ViewControllerTracingNode, UIScrollViewDelegate { - private let publishableKey: String + private let context: AccountContext + private let provider: BotCheckoutNativeCardEntryController.Provider private let present: (ViewController, Any?) -> Void private let dismiss: () -> Void @@ -70,9 +73,12 @@ final class BotCheckoutNativeCardEntryControllerNode: ViewControllerTracingNode, private var currentCardData: BotPaymentCardInputData? private var currentCountryIso2: String? + + private var dataTask: URLSessionDataTask? - init(additionalFields: BotCheckoutNativeCardEntryAdditionalFields, publishableKey: String, theme: PresentationTheme, strings: PresentationStrings, present: @escaping (ViewController, Any?) -> Void, dismiss: @escaping () -> Void, openCountrySelection: @escaping () -> Void, updateStatus: @escaping (BotCheckoutNativeCardEntryStatus) -> Void, completion: @escaping (BotCheckoutPaymentMethod) -> Void) { - self.publishableKey = publishableKey + init(context: AccountContext, provider: BotCheckoutNativeCardEntryController.Provider, theme: PresentationTheme, strings: PresentationStrings, present: @escaping (ViewController, Any?) -> Void, dismiss: @escaping () -> Void, openCountrySelection: @escaping () -> Void, updateStatus: @escaping (BotCheckoutNativeCardEntryStatus) -> Void, completion: @escaping (BotCheckoutPaymentMethod) -> Void) { + self.context = context + self.provider = provider self.present = present self.dismiss = dismiss @@ -95,46 +101,53 @@ final class BotCheckoutNativeCardEntryControllerNode: ViewControllerTracingNode, cardUpdatedImpl?(data) } itemNodes.append([BotPaymentHeaderItemNode(text: strings.Checkout_NewCard_PaymentCard), self.cardItem]) - - if additionalFields.contains(.cardholderName) { - var sectionItems: [BotPaymentItemNode] = [] - - sectionItems.append(BotPaymentHeaderItemNode(text: strings.Checkout_NewCard_CardholderNameTitle)) - - let cardholderItem = BotPaymentFieldItemNode(title: "", placeholder: strings.Checkout_NewCard_CardholderNamePlaceholder, contentType: .name) - self.cardholderItem = cardholderItem - sectionItems.append(cardholderItem) - - itemNodes.append(sectionItems) - } else { - self.cardholderItem = nil - } - - if additionalFields.contains(.country) || additionalFields.contains(.zipCode) { - var sectionItems: [BotPaymentItemNode] = [] - - sectionItems.append(BotPaymentHeaderItemNode(text: strings.Checkout_NewCard_PostcodeTitle)) - - if additionalFields.contains(.country) { - let countryItem = BotPaymentDisclosureItemNode(title: "", placeholder: strings.CheckoutInfo_ShippingInfoCountryPlaceholder, text: "") - countryItem.action = { - openCountrySelectionImpl?() + + switch provider { + case let .stripe(additionalFields, _): + if additionalFields.contains(.cardholderName) { + var sectionItems: [BotPaymentItemNode] = [] + + sectionItems.append(BotPaymentHeaderItemNode(text: strings.Checkout_NewCard_CardholderNameTitle)) + + let cardholderItem = BotPaymentFieldItemNode(title: "", placeholder: strings.Checkout_NewCard_CardholderNamePlaceholder, contentType: .name) + self.cardholderItem = cardholderItem + sectionItems.append(cardholderItem) + + itemNodes.append(sectionItems) + } else { + self.cardholderItem = nil + } + + if additionalFields.contains(.country) || additionalFields.contains(.zipCode) { + var sectionItems: [BotPaymentItemNode] = [] + + sectionItems.append(BotPaymentHeaderItemNode(text: strings.Checkout_NewCard_PostcodeTitle)) + + if additionalFields.contains(.country) { + let countryItem = BotPaymentDisclosureItemNode(title: "", placeholder: strings.CheckoutInfo_ShippingInfoCountryPlaceholder, text: "") + countryItem.action = { + openCountrySelectionImpl?() + } + self.countryItem = countryItem + sectionItems.append(countryItem) + } else { + self.countryItem = nil } - self.countryItem = countryItem - sectionItems.append(countryItem) + if additionalFields.contains(.zipCode) { + let zipCodeItem = BotPaymentFieldItemNode(title: "", placeholder: strings.Checkout_NewCard_PostcodePlaceholder, contentType: .address) + self.zipCodeItem = zipCodeItem + sectionItems.append(zipCodeItem) + } else { + self.zipCodeItem = nil + } + + itemNodes.append(sectionItems) } else { self.countryItem = nil - } - if additionalFields.contains(.zipCode) { - let zipCodeItem = BotPaymentFieldItemNode(title: "", placeholder: strings.Checkout_NewCard_PostcodePlaceholder, contentType: .address) - self.zipCodeItem = zipCodeItem - sectionItems.append(zipCodeItem) - } else { self.zipCodeItem = nil } - - itemNodes.append(sectionItems) - } else { + case .smartglobal: + self.cardholderItem = nil self.countryItem = nil self.zipCodeItem = nil } @@ -214,6 +227,7 @@ final class BotCheckoutNativeCardEntryControllerNode: ViewControllerTracingNode, deinit { self.verifyDisposable.dispose() + self.dataTask?.cancel() } func updateCountry(_ iso2: String) { @@ -232,53 +246,163 @@ final class BotCheckoutNativeCardEntryControllerNode: ViewControllerTracingNode, guard let cardData = self.currentCardData else { return } - - let configuration = STPPaymentConfiguration.shared().copy() as! STPPaymentConfiguration - configuration.smsAutofillDisabled = true - configuration.publishableKey = self.publishableKey - configuration.appleMerchantIdentifier = "merchant.ph.telegra.Telegraph" - - let apiClient = STPAPIClient(configuration: configuration) - - let card = STPCardParams() - card.number = cardData.number - card.cvc = cardData.code - card.expYear = cardData.year - card.expMonth = cardData.month - card.name = self.cardholderItem?.text - card.addressCountry = self.currentCountryIso2 - card.addressZip = self.zipCodeItem?.text - - let createToken: Signal = Signal { subscriber in - apiClient.createToken(withCard: card, completion: { token, error in - if let error = error { - subscriber.putError(error) - } else if let token = token { - subscriber.putNext(token) - subscriber.putCompletion() + + switch self.provider { + case let .stripe(_, publishableKey): + let configuration = STPPaymentConfiguration.shared().copy() as! STPPaymentConfiguration + configuration.smsAutofillDisabled = true + configuration.publishableKey = publishableKey + configuration.appleMerchantIdentifier = "merchant.ph.telegra.Telegraph" + + let apiClient = STPAPIClient(configuration: configuration) + + let card = STPCardParams() + card.number = cardData.number + card.cvc = cardData.code + card.expYear = cardData.year + card.expMonth = cardData.month + card.name = self.cardholderItem?.text + card.addressCountry = self.currentCountryIso2 + card.addressZip = self.zipCodeItem?.text + + let createToken: Signal = Signal { subscriber in + apiClient.createToken(withCard: card, completion: { token, error in + if let error = error { + subscriber.putError(error) + } else if let token = token { + subscriber.putNext(token) + subscriber.putCompletion() + } + }) + + return ActionDisposable { + let _ = apiClient.publishableKey + } + } + + self.isVerifying = true + self.verifyDisposable.set((createToken |> deliverOnMainQueue).start(next: { [weak self] token in + if let strongSelf = self, let card = token.card { + let last4 = card.last4() + let brand = STPAPIClient.string(with: card.brand) + strongSelf.completion(.webToken(BotCheckoutPaymentWebToken(title: "\(brand)*\(last4)", data: "{\"type\": \"card\", \"id\": \"\(token.tokenId)\"}", saveOnServer: strongSelf.saveInfoItem.isOn))) + } + }, error: { [weak self] error in + if let strongSelf = self { + strongSelf.isVerifying = false + strongSelf.updateDone() + } + })) + + self.updateDone() + case let .smartglobal(isTesting, publicToken): + let url: String + if isTesting { + url = "https://tgb-playground.smart-glocal.com/cds/v1/tokenize/card" + } else { + url = "https://tgb.smart-glocal.com/cds/v1/tokenize/card" + } + + let jsonPayload: [String: Any] = [ + "card": [ + "number": cardData.number, + "expiration_month": String(format: "%02d", cardData.month), + "expiration_year": String(format: "%02d", cardData.year), + "security_code": "\(cardData.code)" + ] as [String: Any] + ] + + guard let parsedUrl = URL(string: url) else { + return + } + + var request = URLRequest(url: parsedUrl) + request.httpMethod = "POST" + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + request.setValue(publicToken, forHTTPHeaderField: "X-PUBLIC-TOKEN") + guard let requestBody = try? JSONSerialization.data(withJSONObject: jsonPayload, options: []) else { + return + } + request.httpBody = requestBody + + let session = URLSession.shared + let dataTask = session.dataTask(with: request, completionHandler: { [weak self] data, response, error in + Queue.mainQueue().async { + guard let strongSelf = self else { + return + } + + enum ReponseError: Error { + case generic + } + + do { + guard let data = data else { + throw ReponseError.generic + } + + let jsonRaw = try JSONSerialization.jsonObject(with: data, options: []) + guard let json = jsonRaw as? [String: Any] else { + throw ReponseError.generic + } + guard let resultData = json["data"] as? [String: Any] else { + throw ReponseError.generic + } + guard let resultInfo = resultData["info"] as? [String: Any] else { + throw ReponseError.generic + } + guard let token = resultData["token"] as? String else { + throw ReponseError.generic + } + guard let maskedCardNumber = resultInfo["masked_card_number"] as? String else { + throw ReponseError.generic + } + guard let cardType = resultInfo["card_type"] as? String else { + throw ReponseError.generic + } + + var last4 = maskedCardNumber + if last4.count > 4 { + let lastDigits = String(maskedCardNumber[maskedCardNumber.index(maskedCardNumber.endIndex, offsetBy: -4)...]) + if lastDigits.allSatisfy(\.isNumber) { + last4 = "\(cardType) *\(lastDigits)" + } + } + + let responseJson: [String: Any] = [ + "type": "card", + "token": "\(token)" + ] + + let serializedResponseJson = try JSONSerialization.data(withJSONObject: responseJson, options: []) + + guard let serializedResponseString = String(data: serializedResponseJson, encoding: .utf8) else { + throw ReponseError.generic + } + + strongSelf.completion(.webToken(BotCheckoutPaymentWebToken( + title: last4, + data: serializedResponseString, + saveOnServer: strongSelf.saveInfoItem.isOn + ))) + } catch { + strongSelf.isVerifying = false + strongSelf.updateDone() + + strongSelf.present(textAlertController(context: strongSelf.context, title: nil, text: strongSelf.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.strings.Common_OK, action: { + })]), nil) + } } }) - - return ActionDisposable { - let _ = apiClient.publishableKey - } + self.dataTask = dataTask + + self.isVerifying = true + self.updateDone() + + dataTask.resume() + + break } - - self.isVerifying = true - self.verifyDisposable.set((createToken |> deliverOnMainQueue).start(next: { [weak self] token in - if let strongSelf = self, let card = token.card { - let last4 = card.last4() - let brand = STPAPIClient.string(with: card.brand) - strongSelf.completion(.webToken(BotCheckoutPaymentWebToken(title: "\(brand)*\(last4)", data: "{\"type\": \"card\", \"id\": \"\(token.tokenId)\"}", saveOnServer: strongSelf.saveInfoItem.isOn))) - } - }, error: { [weak self] error in - if let strongSelf = self { - strongSelf.isVerifying = false - strongSelf.updateDone() - } - })) - - self.updateDone() } private func updateDone() { diff --git a/submodules/BotPaymentsUI/Sources/BotCheckoutPasswordEntryController.swift b/submodules/BotPaymentsUI/Sources/BotCheckoutPasswordEntryController.swift index 13f395f368..c2c0a5733e 100644 --- a/submodules/BotPaymentsUI/Sources/BotCheckoutPasswordEntryController.swift +++ b/submodules/BotPaymentsUI/Sources/BotCheckoutPasswordEntryController.swift @@ -94,7 +94,7 @@ private final class BotCheckoutPasswordAlertContentNode: AlertContentNode { private let hapticFeedback = HapticFeedback() - init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, cardTitle: String, period: Int32, requiresBiometrics: Bool, cancel: @escaping () -> Void, completion: @escaping (TemporaryTwoStepPasswordToken) -> Void) { + init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, passwordTip: String?, cardTitle: String, period: Int32, requiresBiometrics: Bool, cancel: @escaping () -> Void, completion: @escaping (TemporaryTwoStepPasswordToken) -> Void) { self.context = context self.period = period self.requiresBiometrics = requiresBiometrics @@ -156,6 +156,8 @@ private final class BotCheckoutPasswordAlertContentNode: AlertContentNode { self.textFieldNode.textField.keyboardAppearance = theme.rootController.keyboardColor.keyboardAppearance self.textFieldNode.textField.isSecureTextEntry = true self.textFieldNode.textField.tintColor = theme.list.itemAccentColor + self.textFieldNode.textField.placeholder = passwordTip + super.init() @@ -218,7 +220,7 @@ private final class BotCheckoutPasswordAlertContentNode: AlertContentNode { let textFieldBackgroundFrame = CGRect(origin: CGPoint(x: insets.left, y: resultSize.height - inputHeight + 12.0 - actionsHeight - insets.bottom), size: CGSize(width: resultSize.width - insets.left - insets.right, height: 25.0)) self.textFieldNodeBackground.frame = textFieldBackgroundFrame - self.textFieldNode.frame = textFieldBackgroundFrame.offsetBy(dx: 0.0, dy: 1.0).insetBy(dx: 4.0, dy: 0.0) + self.textFieldNode.frame = textFieldBackgroundFrame.offsetBy(dx: 0.0, dy: 0.0).insetBy(dx: 4.0, dy: 0.0) self.actionNodesSeparator.frame = CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel)) @@ -283,7 +285,7 @@ private final class BotCheckoutPasswordAlertContentNode: AlertContentNode { } self.isVerifying = true - self.disposable.set((requestTemporaryTwoStepPasswordToken(account: self.context.account, password: text, period: self.period, requiresBiometrics: self.requiresBiometrics) |> deliverOnMainQueue).start(next: { [weak self] token in + self.disposable.set((self.context.engine.auth.requestTemporaryTwoStepPasswordToken(password: text, period: self.period, requiresBiometrics: self.requiresBiometrics) |> deliverOnMainQueue).start(next: { [weak self] token in if let strongSelf = self { strongSelf.completion(token) } @@ -300,10 +302,10 @@ private final class BotCheckoutPasswordAlertContentNode: AlertContentNode { } } -func botCheckoutPasswordEntryController(context: AccountContext, strings: PresentationStrings, cartTitle: String, period: Int32, requiresBiometrics: Bool, completion: @escaping (TemporaryTwoStepPasswordToken) -> Void) -> AlertController { +func botCheckoutPasswordEntryController(context: AccountContext, strings: PresentationStrings, passwordTip: String?, 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(presentationData: presentationData), 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, passwordTip: passwordTip, cardTitle: cartTitle, period: period, requiresBiometrics: requiresBiometrics, cancel: { dismissImpl?() }, completion: { token in completion(token) diff --git a/submodules/BotPaymentsUI/Sources/BotCheckoutPriceItem.swift b/submodules/BotPaymentsUI/Sources/BotCheckoutPriceItem.swift index 46df1456df..2e33d49e95 100644 --- a/submodules/BotPaymentsUI/Sources/BotCheckoutPriceItem.swift +++ b/submodules/BotPaymentsUI/Sources/BotCheckoutPriceItem.swift @@ -6,28 +6,33 @@ import SwiftSignalKit import TelegramPresentationData import ItemListUI import PresentationDataUtils +import ShimmerEffect class BotCheckoutPriceItem: ListViewItem, ItemListItem { let theme: PresentationTheme let title: String let label: String let isFinal: Bool + let hasSeparator: Bool + let shimmeringIndex: Int? let sectionId: ItemListSectionId let requestsNoInset: Bool = true - init(theme: PresentationTheme, title: String, label: String, isFinal: Bool, sectionId: ItemListSectionId) { + init(theme: PresentationTheme, title: String, label: String, isFinal: Bool, hasSeparator: Bool, shimmeringIndex: Int?, sectionId: ItemListSectionId) { self.theme = theme self.title = title self.label = label self.isFinal = isFinal + self.hasSeparator = hasSeparator + self.shimmeringIndex = shimmeringIndex 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 = BotCheckoutPriceItemNode() - 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), previousItem, nextItem) node.contentSize = layout.contentSize node.insets = layout.insets @@ -46,7 +51,7 @@ class BotCheckoutPriceItem: ListViewItem, ItemListItem { let makeLayout = nodeValue.asyncLayout() 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), previousItem, nextItem) Queue.mainQueue().async { completion(layout, { _ in apply() @@ -67,13 +72,13 @@ private func priceItemInsets(_ neighbors: ItemListNeighbors) -> UIEdgeInsets { var insets = UIEdgeInsets() switch neighbors.top { case .otherSection: - insets.top += 8.0 + insets.top += 24.0 case .none, .sameSection: break } switch neighbors.bottom { case .none, .otherSection: - insets.bottom += 8.0 + insets.bottom += 24.0 case .sameSection: break } @@ -83,6 +88,13 @@ private func priceItemInsets(_ neighbors: ItemListNeighbors) -> UIEdgeInsets { class BotCheckoutPriceItemNode: ListViewItemNode { let titleNode: TextNode let labelNode: TextNode + + let backgroundNode: ASDisplayNode + let separatorNode: ASDisplayNode + let bottomSeparatorNode: ASDisplayNode + + private var placeholderNode: ShimmerEffectNode? + private var absoluteLocation: (CGRect, CGSize)? private var item: BotCheckoutPriceItem? @@ -92,21 +104,58 @@ class BotCheckoutPriceItemNode: ListViewItemNode { self.labelNode = TextNode() self.labelNode.isUserInteractionEnabled = false + + self.backgroundNode = ASDisplayNode() + self.separatorNode = ASDisplayNode() + self.bottomSeparatorNode = ASDisplayNode() super.init(layerBacked: false, dynamicBounce: false) - + + self.addSubnode(self.backgroundNode) self.addSubnode(self.titleNode) self.addSubnode(self.labelNode) + self.addSubnode(self.separatorNode) + self.addSubnode(self.bottomSeparatorNode) + } + + 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.placeholderNode { + shimmerNode.updateAbsoluteRect(rect, within: containerSize) + } } - func asyncLayout() -> (_ item: BotCheckoutPriceItem, _ params: ListViewItemLayoutParams, _ insets: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { + func asyncLayout() -> (_ item: BotCheckoutPriceItem, _ params: ListViewItemLayoutParams, _ insets: ItemListNeighbors, _ previousItem: ListViewItem?, _ nextItem: ListViewItem?) -> (ListViewItemNodeLayout, () -> Void) { let makeTitleLayout = TextNode.asyncLayout(self.titleNode) let makeLabelLayout = TextNode.asyncLayout(self.labelNode) - return { item, params, neighbors in + return { item, params, neighbors, previousItem, nextItem in let rightInset: CGFloat = 16.0 + params.rightInset + + let naturalContentHeight: CGFloat + var verticalOffset: CGFloat = 0.0 + if item.isFinal { + naturalContentHeight = 44.0 + } else { + switch neighbors.bottom { + case .otherSection, .none: + naturalContentHeight = 44.0 + default: + naturalContentHeight = 34.0 + } + } + if let _ = previousItem as? BotCheckoutHeaderItem { + verticalOffset += 8.0 + } - let contentSize = CGSize(width: params.width, height: 34.0) + var contentSize = CGSize(width: params.width, height: naturalContentHeight + verticalOffset) + if let nextItem = nextItem as? BotCheckoutPriceItem { + if nextItem.isFinal { + contentSize.height += 8.0 + } + } let insets = priceItemInsets(neighbors) let textFont: UIFont @@ -130,9 +179,58 @@ class BotCheckoutPriceItemNode: ListViewItemNode { let _ = labelApply() let leftInset: CGFloat = 16.0 + params.leftInset + + strongSelf.separatorNode.isHidden = !item.hasSeparator + strongSelf.separatorNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor + strongSelf.separatorNode.frame = CGRect(origin: CGPoint(x: leftInset, y: 0.0), size: CGSize(width: params.width - leftInset, height: UIScreenPixel)) + + switch neighbors.bottom { + case .otherSection, .none: + strongSelf.bottomSeparatorNode.isHidden = false + default: + strongSelf.bottomSeparatorNode.isHidden = !item.isFinal + } + + strongSelf.bottomSeparatorNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor + strongSelf.bottomSeparatorNode.frame = CGRect(origin: CGPoint(x: 0.0, y: contentSize.height), size: CGSize(width: params.width, height: UIScreenPixel)) + + strongSelf.backgroundNode.backgroundColor = item.theme.list.itemBlocksBackgroundColor + strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: params.width, height: contentSize.height)) - strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: leftInset, y: floor((contentSize.height - titleLayout.size.height) / 2.0)), size: titleLayout.size) - strongSelf.labelNode.frame = CGRect(origin: CGPoint(x: params.width - rightInset - labelLayout.size.width, y: floor((contentSize.height - labelLayout.size.height) / 2.0)), size: labelLayout.size) + strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: leftInset, y: verticalOffset + floor((naturalContentHeight - titleLayout.size.height) / 2.0)), size: titleLayout.size) + strongSelf.labelNode.frame = CGRect(origin: CGPoint(x: params.width - rightInset - labelLayout.size.width, y: verticalOffset + floor((naturalContentHeight - labelLayout.size.height) / 2.0)), size: labelLayout.size) + + if let shimmeringIndex = item.shimmeringIndex { + let shimmerNode: ShimmerEffectNode + if let current = strongSelf.placeholderNode { + shimmerNode = current + } else { + shimmerNode = ShimmerEffectNode() + strongSelf.placeholderNode = shimmerNode + if strongSelf.separatorNode.supernode != nil { + strongSelf.insertSubnode(shimmerNode, belowSubnode: strongSelf.separatorNode) + } else { + strongSelf.addSubnode(shimmerNode) + } + } + shimmerNode.frame = CGRect(origin: CGPoint(), size: contentSize) + if let (rect, size) = strongSelf.absoluteLocation { + shimmerNode.updateAbsoluteRect(rect, within: size) + } + + var shapes: [ShimmerEffectNode.Shape] = [] + + let titleLineWidth: CGFloat = (shimmeringIndex % 2 == 0) ? 120.0 : 80.0 + let lineDiameter: CGFloat = 8.0 + + let titleFrame = strongSelf.titleNode.frame + shapes.append(.roundedRectLine(startPoint: CGPoint(x: titleFrame.minX, y: titleFrame.minY + floor((titleFrame.height - lineDiameter) / 2.0)), width: titleLineWidth, diameter: lineDiameter)) + + shimmerNode.update(backgroundColor: item.theme.list.itemBlocksBackgroundColor, foregroundColor: item.theme.list.mediaPlaceholderColor, shimmeringColor: item.theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.4), shapes: shapes, size: contentSize) + } else if let shimmerNode = strongSelf.placeholderNode { + strongSelf.placeholderNode = nil + shimmerNode.removeFromSupernode() + } } }) } diff --git a/submodules/BotPaymentsUI/Sources/BotCheckoutTipItem.swift b/submodules/BotPaymentsUI/Sources/BotCheckoutTipItem.swift new file mode 100644 index 0000000000..5c8a7c620a --- /dev/null +++ b/submodules/BotPaymentsUI/Sources/BotCheckoutTipItem.swift @@ -0,0 +1,782 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import SwiftSignalKit +import TelegramPresentationData +import ItemListUI +import PresentationDataUtils +import TelegramStringFormatting + +class BotCheckoutTipItem: ListViewItem, ItemListItem { + let theme: PresentationTheme + let strings: PresentationStrings + let title: String + let currency: String + let value: String + let numericValue: Int64 + let maxValue: Int64 + let availableVariants: [(String, Int64)] + let updateValue: (Int64) -> Void + let updatedFocus: (Bool) -> Void + + let sectionId: ItemListSectionId + + let requestsNoInset: Bool = true + + init(theme: PresentationTheme, strings: PresentationStrings, title: String, currency: String, value: String, numericValue: Int64, maxValue: Int64, availableVariants: [(String, Int64)], sectionId: ItemListSectionId, updateValue: @escaping (Int64) -> Void, updatedFocus: @escaping (Bool) -> Void) { + self.theme = theme + self.strings = strings + self.title = title + self.currency = currency + self.value = value + self.numericValue = numericValue + self.maxValue = maxValue + self.availableVariants = availableVariants + self.updateValue = updateValue + self.updatedFocus = updatedFocus + 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 = BotCheckoutTipItemNode() + 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? BotCheckoutTipItemNode { + 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() + }) + } + } + } + } + } + + let selectable: Bool = false +} + +private let titleFont = Font.regular(17.0) +private let finalFont = Font.semibold(17.0) + +private func priceItemInsets(_ neighbors: ItemListNeighbors) -> UIEdgeInsets { + var insets = UIEdgeInsets() + switch neighbors.top { + case .otherSection: + insets.top += 8.0 + case .none, .sameSection: + break + } + switch neighbors.bottom { + case .none, .otherSection: + insets.bottom += 8.0 + case .sameSection: + break + } + return insets +} + +private final class TipValueNode: ASDisplayNode { + private let backgroundNode: ASImageNode + private let titleNode: ImmediateTextNode + + private let button: HighlightTrackingButtonNode + + private var currentBackgroundColor: UIColor? + + var action: (() -> Void)? + + override init() { + self.backgroundNode = ASImageNode() + self.titleNode = ImmediateTextNode() + + self.button = HighlightTrackingButtonNode() + + super.init() + + self.addSubnode(self.backgroundNode) + self.addSubnode(self.titleNode) + self.addSubnode(self.button) + self.button.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside) + } + + @objc private func buttonPressed() { + self.action?() + } + + func update(theme: PresentationTheme, text: String, isHighlighted: Bool, height: CGFloat) -> (CGFloat, (CGFloat) -> Void) { + var updateBackground = false + let backgroundColor = isHighlighted ? theme.list.paymentOption.activeFillColor : theme.list.paymentOption.inactiveFillColor + if let currentBackgroundColor = self.currentBackgroundColor { + if !currentBackgroundColor.isEqual(backgroundColor) { + updateBackground = true + } + } else { + updateBackground = true + } + if updateBackground { + self.currentBackgroundColor = backgroundColor + self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 20.0, color: backgroundColor) + } + + self.titleNode.attributedText = NSAttributedString(string: text, font: Font.semibold(15.0), textColor: isHighlighted ? theme.list.paymentOption.activeForegroundColor : theme.list.paymentOption.inactiveForegroundColor) + let titleSize = self.titleNode.updateLayout(CGSize(width: 200.0, height: height)) + + let minWidth: CGFloat = 80.0 + + let calculatedWidth = max(titleSize.width + 16.0 * 2.0, minWidth) + + return (calculatedWidth, { calculatedWidth in + self.titleNode.frame = CGRect(origin: CGPoint(x: floor((calculatedWidth - titleSize.width) / 2.0), y: floor((height - titleSize.height) / 2.0)), size: titleSize) + + let size = CGSize(width: calculatedWidth, height: height) + self.backgroundNode.frame = CGRect(origin: CGPoint(), size: size) + + self.button.frame = CGRect(origin: CGPoint(), size: size) + }) + } +} + +private final class FormatterImpl: NSObject, UITextFieldDelegate { + private struct Representation { + private let format: CurrencyFormat + private var caretIndex: Int = 0 + private var wholePart: [Int] = [] + private var decimalPart: [Int] = [] + + init(string: String, format: CurrencyFormat) { + self.format = format + + var isDecimalPart = false + for c in string { + if c.isNumber { + if let value = Int(String(c)) { + if isDecimalPart { + self.decimalPart.append(value) + } else { + self.wholePart.append(value) + } + } + } else if String(c) == format.decimalSeparator { + isDecimalPart = true + } + } + + while self.wholePart.count > 1 { + if self.wholePart[0] != 0 { + break + } else { + self.wholePart.removeFirst() + } + } + if self.wholePart.isEmpty { + self.wholePart = [0] + } + + while self.decimalPart.count > 1 { + if self.decimalPart[self.decimalPart.count - 1] != 0 { + break + } else { + self.decimalPart.removeLast() + } + } + while self.decimalPart.count < format.decimalDigits { + self.decimalPart.append(0) + } + + self.caretIndex = self.wholePart.count + } + + var minCaretIndex: Int { + for i in 0 ..< self.wholePart.count { + if self.wholePart[i] != 0 { + return i + } + } + return self.wholePart.count + } + + mutating func moveCaret(offset: Int) { + self.caretIndex = max(self.minCaretIndex, min(self.caretIndex + offset, self.wholePart.count + self.decimalPart.count)) + } + + mutating func normalize() { + while self.wholePart.count > 1 { + if self.wholePart[0] != 0 { + break + } else { + self.wholePart.removeFirst() + self.moveCaret(offset: -1) + } + } + if self.wholePart.isEmpty { + self.wholePart = [0] + } + + while self.decimalPart.count < format.decimalDigits { + self.decimalPart.append(0) + } + while self.decimalPart.count > format.decimalDigits { + self.decimalPart.removeLast() + } + + self.caretIndex = max(self.minCaretIndex, min(self.caretIndex, self.wholePart.count + self.decimalPart.count)) + } + + mutating func backspace() { + if self.caretIndex > self.wholePart.count { + let decimalIndex = self.caretIndex - self.wholePart.count + if decimalIndex > 0 { + self.decimalPart.remove(at: decimalIndex - 1) + + self.moveCaret(offset: -1) + self.normalize() + } + } else { + if self.caretIndex > 0 { + self.wholePart.remove(at: self.caretIndex - 1) + + self.moveCaret(offset: -1) + self.normalize() + } + } + } + + mutating func insert(letter: String) { + if letter == "." || letter == "," { + if self.caretIndex == self.wholePart.count { + return + } else if self.caretIndex < self.wholePart.count { + for i in (self.caretIndex ..< self.wholePart.count).reversed() { + self.decimalPart.insert(self.wholePart[i], at: 0) + self.wholePart.remove(at: i) + } + } + + self.normalize() + } else if letter.count == 1 && letter[letter.startIndex].isNumber { + if let value = Int(letter) { + if self.caretIndex <= self.wholePart.count { + self.wholePart.insert(value, at: self.caretIndex) + } else { + let decimalIndex = self.caretIndex - self.wholePart.count + self.decimalPart.insert(value, at: decimalIndex) + } + self.moveCaret(offset: 1) + self.normalize() + } + } + } + + var string: String { + var result = "" + + for digit in self.wholePart { + result.append("\(digit)") + } + result.append(self.format.decimalSeparator) + for digit in self.decimalPart { + result.append("\(digit)") + } + + return result + } + + var stringCaretIndex: Int { + var logicalIndex = 0 + var resolvedIndex = 0 + + if logicalIndex == self.caretIndex { + return resolvedIndex + } + + for _ in self.wholePart { + logicalIndex += 1 + resolvedIndex += 1 + + if logicalIndex == self.caretIndex { + return resolvedIndex + } + } + + resolvedIndex += 1 + + for _ in self.decimalPart { + logicalIndex += 1 + resolvedIndex += 1 + + if logicalIndex == self.caretIndex { + return resolvedIndex + } + } + + return resolvedIndex + } + + var numericalValue: Int64 { + var result: Int64 = 0 + + for digit in self.wholePart { + result *= 10 + result += Int64(digit) + } + for digit in self.decimalPart { + result *= 10 + result += Int64(digit) + } + + return result + } + } + + private let format: CurrencyFormat + private let currency: String + private let maxNumericalValue: Int64 + private let updated: (Int64) -> Void + private let focusUpdated: (Bool) -> Void + + private var representation: Representation + + private var previousResolvedCaretIndex: Int = 0 + private var ignoreTextSelection: Bool = false + private var enableTextSelectionProcessing: Bool = false + + init?(textField: UITextField, currency: String, maxNumericalValue: Int64, initialValue: String, updated: @escaping (Int64) -> Void, focusUpdated: @escaping (Bool) -> Void) { + guard let format = CurrencyFormat(currency: currency) else { + return nil + } + self.format = format + self.currency = currency + self.maxNumericalValue = maxNumericalValue + self.updated = updated + self.focusUpdated = focusUpdated + + self.representation = Representation(string: initialValue, format: format) + + super.init() + + textField.text = self.representation.string + self.previousResolvedCaretIndex = self.representation.stringCaretIndex + } + + func reset(textField: UITextField, initialValue: String) { + self.representation = Representation(string: initialValue, format: self.format) + self.resetFromRepresentation(textField: textField, notifyUpdated: false) + } + + private func resetFromRepresentation(textField: UITextField, notifyUpdated: Bool) { + self.ignoreTextSelection = true + + if self.representation.numericalValue > self.maxNumericalValue { + self.representation = Representation(string: formatCurrencyAmountCustom(self.maxNumericalValue, currency: self.currency).0, format: self.format) + } + + textField.text = self.representation.string + self.previousResolvedCaretIndex = self.representation.stringCaretIndex + + if self.enableTextSelectionProcessing { + let stringCaretIndex = self.representation.stringCaretIndex + if let caretPosition = textField.position(from: textField.beginningOfDocument, offset: stringCaretIndex) { + textField.selectedTextRange = textField.textRange(from: caretPosition, to: caretPosition) + } + } + self.ignoreTextSelection = false + + if notifyUpdated { + self.updated(self.representation.numericalValue) + } + } + + @objc public func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { + if string.count == 1 { + self.representation.insert(letter: string) + self.resetFromRepresentation(textField: textField, notifyUpdated: true) + } else if string.count == 0 { + self.representation.backspace() + self.resetFromRepresentation(textField: textField, notifyUpdated: true) + } + + return false + } + + @objc public func textFieldShouldReturn(_ textField: UITextField) -> Bool { + return false + } + + @objc public func textFieldDidBeginEditing(_ textField: UITextField) { + self.enableTextSelectionProcessing = true + self.focusUpdated(true) + + let stringCaretIndex = self.representation.stringCaretIndex + self.previousResolvedCaretIndex = stringCaretIndex + if let caretPosition = textField.position(from: textField.beginningOfDocument, offset: stringCaretIndex) { + self.ignoreTextSelection = true + textField.selectedTextRange = textField.textRange(from: caretPosition, to: caretPosition) + DispatchQueue.main.async { + textField.selectedTextRange = textField.textRange(from: caretPosition, to: caretPosition) + self.ignoreTextSelection = false + } + } + } + + @objc public func textFieldDidChangeSelection(_ textField: UITextField) { + if self.ignoreTextSelection { + return + } + if !self.enableTextSelectionProcessing { + return + } + + if let selectedTextRange = textField.selectedTextRange { + let index = textField.offset(from: textField.beginningOfDocument, to: selectedTextRange.end) + if self.previousResolvedCaretIndex != index { + self.representation.moveCaret(offset: self.previousResolvedCaretIndex < index ? 1 : -1) + + let stringCaretIndex = self.representation.stringCaretIndex + self.previousResolvedCaretIndex = stringCaretIndex + if let caretPosition = textField.position(from: textField.beginningOfDocument, offset: stringCaretIndex) { + textField.selectedTextRange = textField.textRange(from: caretPosition, to: caretPosition) + } + } + } + } + + @objc public func textFieldDidEndEditing(_ textField: UITextField) { + self.enableTextSelectionProcessing = false + self.focusUpdated(false) + } +} + +class BotCheckoutTipItemNode: ListViewItemNode, UITextFieldDelegate { + private let backgroundNode: ASDisplayNode + let titleNode: TextNode + let labelNode: TextNode + let tipMeasurementNode: ImmediateTextNode + let tipCurrencyNode: ImmediateTextNode + private let textNode: TextFieldNode + + private let scrollNode: ASScrollNode + private var valueNodes: [TipValueNode] = [] + + private var item: BotCheckoutTipItem? + private var formatter: FormatterImpl? + + init() { + self.backgroundNode = ASDisplayNode() + + self.titleNode = TextNode() + self.titleNode.isUserInteractionEnabled = false + + self.labelNode = TextNode() + self.labelNode.isUserInteractionEnabled = false + self.labelNode.isHidden = true + + self.tipMeasurementNode = ImmediateTextNode() + self.tipCurrencyNode = ImmediateTextNode() + + self.textNode = TextFieldNode() + + self.scrollNode = ASScrollNode() + self.scrollNode.view.disablesInteractiveTransitionGestureRecognizer = true + self.scrollNode.view.showsVerticalScrollIndicator = false + self.scrollNode.view.showsHorizontalScrollIndicator = false + self.scrollNode.view.scrollsToTop = false + self.scrollNode.view.delaysContentTouches = false + self.scrollNode.view.canCancelContentTouches = true + if #available(iOS 11.0, *) { + self.scrollNode.view.contentInsetAdjustmentBehavior = .never + } + + super.init(layerBacked: false, dynamicBounce: false) + + self.addSubnode(self.backgroundNode) + + self.addSubnode(self.titleNode) + self.addSubnode(self.labelNode) + self.addSubnode(self.textNode) + self.addSubnode(self.tipCurrencyNode) + self.addSubnode(self.scrollNode) + + self.textNode.clipsToBounds = true + self.textNode.textField.addTarget(self, action: #selector(self.textFieldTextChanged(_:)), for: .editingChanged) + self.textNode.hitTestSlop = UIEdgeInsets(top: -5.0, left: -5.0, bottom: -5.0, right: -5.0) + } + + func asyncLayout() -> (_ item: BotCheckoutTipItem, _ params: ListViewItemLayoutParams, _ insets: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { + let makeTitleLayout = TextNode.asyncLayout(self.titleNode) + let makeLabelLayout = TextNode.asyncLayout(self.labelNode) + + return { item, params, neighbors in + //let rightInset: CGFloat = 16.0 + params.rightInset + + let labelsContentHeight: CGFloat = 34.0 + + var contentSize = CGSize(width: params.width, height: labelsContentHeight) + if !item.availableVariants.isEmpty { + contentSize.height += 75.0 + } + + let insets = priceItemInsets(neighbors) + + let textFont: UIFont + let textColor: UIColor + + textFont = titleFont + textColor = item.theme.list.itemSecondaryTextColor + + let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.title, font: textFont, 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())) + + let (labelLayout, labelApply) = makeLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.strings.Checkout_OptionalTipItemPlaceholder, font: textFont, textColor: textColor.withMultipliedAlpha(0.8)), 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())) + + return (ListViewItemNodeLayout(contentSize: contentSize, insets: insets), { [weak self] in + if let strongSelf = self { + strongSelf.item = item + + let _ = titleApply() + let _ = labelApply() + + let leftInset: CGFloat = 16.0 + params.leftInset + + strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: leftInset, y: floor((labelsContentHeight - titleLayout.size.height) / 2.0)), size: titleLayout.size) + strongSelf.labelNode.frame = CGRect(origin: CGPoint(x: params.width - leftInset - labelLayout.size.width, y: floor((labelsContentHeight - labelLayout.size.height) / 2.0)), size: labelLayout.size) + + if strongSelf.formatter == nil { + strongSelf.formatter = FormatterImpl(textField: strongSelf.textNode.textField, currency: item.currency, maxNumericalValue: item.maxValue, initialValue: item.value, updated: { value in + guard let strongSelf = self, let item = strongSelf.item else { + return + } + if item.numericValue != value { + item.updateValue(value) + } + }, focusUpdated: { value in + guard let strongSelf = self else { + return + } + if value { + strongSelf.item?.updatedFocus(true) + } + }) + strongSelf.textNode.textField.delegate = strongSelf.formatter + + /*strongSelf.formatterDelegate = CurrencyUITextFieldDelegate(formatter: CurrencyFormatter(currency: item.currency, { formatter in + formatter.maxValue = currencyToFractionalAmount(value: item.maxValue, currency: item.currency) ?? 10000.0 + formatter.minValue = 0.0 + formatter.hasDecimals = true + })) + strongSelf.formatterDelegate?.passthroughDelegate = strongSelf + + strongSelf.formatterDelegate?.textUpdated = { + guard let strongSelf = self else { + return + } + strongSelf.textFieldTextChanged(strongSelf.textNode.textField) + } + + strongSelf.textNode.textField.delegate = strongSelf.formatterDelegate*/ + + strongSelf.textNode.clipsToBounds = true + //strongSelf.textNode.textField.delegate = strongSelf + } + + strongSelf.textNode.textField.typingAttributes = [NSAttributedString.Key.font: titleFont] + strongSelf.textNode.textField.font = titleFont + + strongSelf.textNode.textField.textColor = textColor + strongSelf.textNode.textField.textAlignment = .right + strongSelf.textNode.textField.keyboardAppearance = item.theme.rootController.keyboardColor.keyboardAppearance + strongSelf.textNode.textField.keyboardType = .decimalPad + strongSelf.textNode.textField.returnKeyType = .next + strongSelf.textNode.textField.tintColor = item.theme.list.itemAccentColor + + var textInputFrame = CGRect(origin: CGPoint(x: params.width - leftInset - 150.0, y: -2.0), size: CGSize(width: 150.0, height: labelsContentHeight)) + + let currencyText: (String, String, Bool) = formatCurrencyAmountCustom(item.numericValue, currency: item.currency) + + let currencySymbolOnTheLeft = currencyText.2 + //let currencySymbolOnTheLeft = true + + if strongSelf.textNode.textField.text ?? "" != currencyText.0 { + strongSelf.formatter?.reset(textField: strongSelf.textNode.textField, initialValue: currencyText.0) + } + + strongSelf.tipMeasurementNode.attributedText = NSAttributedString(string: currencyText.0, font: titleFont, textColor: textColor) + let inputTextSize = strongSelf.tipMeasurementNode.updateLayout(textInputFrame.size) + + let spaceRect = NSAttributedString(string: " ", font: titleFont, textColor: textColor).boundingRect(with: CGSize(width: 100.0, height: 100.0), options: .usesLineFragmentOrigin, context: nil) + + strongSelf.tipCurrencyNode.attributedText = NSAttributedString(string: "\(currencyText.1)", font: titleFont, textColor: textColor) + let currencySize = strongSelf.tipCurrencyNode.updateLayout(CGSize(width: 100.0, height: .greatestFiniteMagnitude)) + if currencySymbolOnTheLeft { + strongSelf.tipCurrencyNode.frame = CGRect(origin: CGPoint(x: textInputFrame.maxX - currencySize.width - inputTextSize.width - spaceRect.width, y: floor((labelsContentHeight - currencySize.height) / 2.0) - 1.0), size: currencySize) + } else { + strongSelf.tipCurrencyNode.frame = CGRect(origin: CGPoint(x: textInputFrame.maxX - currencySize.width, y: floor((labelsContentHeight - currencySize.height) / 2.0) - 1.0), size: currencySize) + textInputFrame.origin.x -= currencySize.width + spaceRect.width + } + + strongSelf.textNode.frame = textInputFrame + + let valueHeight: CGFloat = 52.0 + let valueY: CGFloat = labelsContentHeight + 9.0 + + var index = 0 + var variantLayouts: [(CGFloat, (CGFloat) -> Void)] = [] + var totalMinWidth: CGFloat = 0.0 + for (variantText, variantValue) in item.availableVariants { + let valueNode: TipValueNode + if strongSelf.valueNodes.count > index { + valueNode = strongSelf.valueNodes[index] + } else { + valueNode = TipValueNode() + strongSelf.valueNodes.append(valueNode) + strongSelf.scrollNode.addSubnode(valueNode) + } + let (nodeMinWidth, nodeApply) = valueNode.update(theme: item.theme, text: variantText, isHighlighted: item.value == variantText, height: valueHeight) + valueNode.action = { + guard let strongSelf = self, let item = strongSelf.item else { + return + } + if item.numericValue == variantValue { + item.updateValue(0) + } else { + item.updateValue(variantValue) + } + } + totalMinWidth += nodeMinWidth + variantLayouts.append((nodeMinWidth, nodeApply)) + index += 1 + } + + let sideInset: CGFloat = params.leftInset + 16.0 + var scaleFactor: CGFloat = 1.0 + let availableWidth = params.width - sideInset * 2.0 - CGFloat(max(0, item.availableVariants.count - 1)) * 12.0 + if totalMinWidth < availableWidth { + scaleFactor = availableWidth / totalMinWidth + } + + var variantsOffset: CGFloat = sideInset + for index in 0 ..< item.availableVariants.count { + if index != 0 { + variantsOffset += 12.0 + } + + let valueNode: TipValueNode = strongSelf.valueNodes[index] + let (minWidth, nodeApply) = variantLayouts[index] + + let nodeWidth = floor(scaleFactor * minWidth) + + var valueFrame = CGRect(origin: CGPoint(x: variantsOffset, y: 0.0), size: CGSize(width: nodeWidth, height: valueHeight)) + if scaleFactor > 1.0 && index == item.availableVariants.count - 1 { + valueFrame.size.width = params.width - sideInset - valueFrame.minX + } + + valueNode.frame = valueFrame + nodeApply(nodeWidth) + variantsOffset += nodeWidth + } + + variantsOffset += 16.0 + + strongSelf.scrollNode.frame = CGRect(origin: CGPoint(x: 0.0, y: valueY), size: CGSize(width: params.width, height: max(0.0, contentSize.height - valueY))) + strongSelf.scrollNode.view.contentSize = CGSize(width: variantsOffset, height: strongSelf.scrollNode.frame.height) + + strongSelf.backgroundNode.backgroundColor = item.theme.list.itemBlocksBackgroundColor + strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: params.width, height: contentSize.height)) + } + }) + } + } + + @objc private func dismissKeyboard() { + self.textNode.textField.resignFirstResponder() + } + + @objc private func textFieldTextChanged(_ textField: UITextField) { + let text = textField.text ?? "" + //self.labelNode.isHidden = !text.isEmpty + + guard let item = self.item else { + return + } + + if text.isEmpty { + item.updateValue(0) + return + } + + /*var cleanText = "" + for c in text { + if c.isNumber { + cleanText.append(c) + } else if c == "," { + cleanText.append(".") + } + } + + guard let doubleValue = Double(cleanText) else { + return + } + + if var value = fractionalToCurrencyAmount(value: doubleValue, currency: item.currency) { + if value > item.maxValue { + value = item.maxValue + + let currencyText = formatCurrencyAmountCustom(value, currency: item.currency) + if self.textNode.textField.text ?? "" != currencyText.0 { + self.textNode.textField.text = currencyText.0 + } + } + item.updateValue(value) + }*/ + } + + @objc public func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { + return true + } + + @objc public func textFieldShouldReturn(_ textField: UITextField) -> Bool { + return false + } + + @objc public func textFieldDidBeginEditing(_ textField: UITextField) { + textField.selectedTextRange = textField.textRange(from: textField.endOfDocument, to: textField.endOfDocument) + + self.item?.updatedFocus(true) + } + + @objc public func textFieldDidChangeSelection(_ textField: UITextField) { + textField.selectedTextRange = textField.textRange(from: textField.endOfDocument, to: textField.endOfDocument) + } + + @objc public func textFieldDidEndEditing(_ textField: UITextField) { + } + + 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/BotPaymentsUI/Sources/BotCheckoutWebInteractionController.swift b/submodules/BotPaymentsUI/Sources/BotCheckoutWebInteractionController.swift index 6a0fcc693b..8f555ac9ac 100644 --- a/submodules/BotPaymentsUI/Sources/BotCheckoutWebInteractionController.swift +++ b/submodules/BotPaymentsUI/Sources/BotCheckoutWebInteractionController.swift @@ -75,7 +75,7 @@ final class BotCheckoutWebInteractionController: ViewController { override 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.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition) } override var presentationController: UIPresentationController? { diff --git a/submodules/BotPaymentsUI/Sources/BotPaymentFieldItemNode.swift b/submodules/BotPaymentsUI/Sources/BotPaymentFieldItemNode.swift index 615488fc73..947331fc9c 100644 --- a/submodules/BotPaymentsUI/Sources/BotPaymentFieldItemNode.swift +++ b/submodules/BotPaymentsUI/Sources/BotPaymentFieldItemNode.swift @@ -117,7 +117,7 @@ final class BotPaymentFieldItemNode: BotPaymentItemNode, UITextFieldDelegate { textInset = max(measuredInset, textInset) - transition.updateFrame(node: self.textField, frame: CGRect(origin: CGPoint(x: textInset, y: 3.0), size: CGSize(width: max(1.0, width - textInset - 8.0), height: 40.0))) + transition.updateFrame(node: self.textField, frame: CGRect(origin: CGPoint(x: textInset, y: 0.0), size: CGSize(width: max(1.0, width - textInset - 8.0), height: 40.0))) return 44.0 } diff --git a/submodules/BotPaymentsUI/Sources/BotReceiptController.swift b/submodules/BotPaymentsUI/Sources/BotReceiptController.swift index 35522c55cb..b8b2036b2b 100644 --- a/submodules/BotPaymentsUI/Sources/BotReceiptController.swift +++ b/submodules/BotPaymentsUI/Sources/BotReceiptController.swift @@ -20,16 +20,14 @@ public final class BotReceiptController: ViewController { } private let context: AccountContext - private let invoice: TelegramMediaInvoice private let messageId: MessageId private var presentationData: PresentationData private var didPlayPresentationAnimation = false - public init(context: AccountContext, invoice: TelegramMediaInvoice, messageId: MessageId) { + public init(context: AccountContext, messageId: MessageId) { self.context = context - self.invoice = invoice self.messageId = messageId self.presentationData = context.sharedContext.currentPresentationData.with { $0 } @@ -38,10 +36,10 @@ public final class BotReceiptController: ViewController { self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style - var title = self.presentationData.strings.Checkout_Receipt_Title - if invoice.flags.contains(.isTest) { + let title = self.presentationData.strings.Checkout_Receipt_Title + /*if invoice.flags.contains(.isTest) { title += " (Test)" - } + }*/ self.title = title } @@ -50,11 +48,7 @@ public final class BotReceiptController: ViewController { } override public func loadDisplayNode() { - let displayNode = BotReceiptControllerNode(controller: nil, navigationBar: self.navigationBar!, updateNavigationOffset: { [weak self] offset in - if let strongSelf = self { - strongSelf.navigationOffset = offset - } - }, context: self.context, invoice: self.invoice, messageId: self.messageId, dismissAnimated: { [weak self] in + let displayNode = BotReceiptControllerNode(controller: nil, navigationBar: self.navigationBar!, context: self.context, messageId: self.messageId, dismissAnimated: { [weak self] in self?.dismiss() }) @@ -81,7 +75,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, additionalInsets: UIEdgeInsets()) + self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition, additionalInsets: UIEdgeInsets()) } @objc private func cancelPressed() { diff --git a/submodules/BotPaymentsUI/Sources/BotReceiptControllerNode.swift b/submodules/BotPaymentsUI/Sources/BotReceiptControllerNode.swift index 22cf99bc09..5a038bfb0c 100644 --- a/submodules/BotPaymentsUI/Sources/BotReceiptControllerNode.swift +++ b/submodules/BotPaymentsUI/Sources/BotReceiptControllerNode.swift @@ -28,7 +28,7 @@ private enum BotReceiptSection: Int32 { enum BotReceiptEntry: ItemListNodeEntry { case header(PresentationTheme, TelegramMediaInvoice, String) - case price(Int, PresentationTheme, String, String, Bool) + case price(Int, PresentationTheme, String, String, Bool, Bool) case paymentMethod(PresentationTheme, String, String) case shippingInfo(PresentationTheme, String, String) case shippingMethod(PresentationTheme, String, String) @@ -39,7 +39,7 @@ enum BotReceiptEntry: ItemListNodeEntry { var section: ItemListSectionId { switch self { case .header: - return BotReceiptSection.header.rawValue + return BotReceiptSection.prices.rawValue case .price: return BotReceiptSection.prices.rawValue default: @@ -51,7 +51,7 @@ enum BotReceiptEntry: ItemListNodeEntry { switch self { case .header: return 0 - case let .price(index, _, _, _, _): + case let .price(index, _, _, _, _, _): return 1 + Int32(index) case .paymentMethod: return 10000 + 0 @@ -85,8 +85,8 @@ enum BotReceiptEntry: ItemListNodeEntry { } else { return false } - case let .price(lhsIndex, lhsTheme, lhsText, lhsValue, lhsFinal): - if case let .price(rhsIndex, rhsTheme, rhsText, rhsValue, rhsFinal) = rhs { + case let .price(lhsIndex, lhsTheme, lhsText, lhsValue, lhsHasSeparator, lhsFinal): + if case let .price(rhsIndex, rhsTheme, rhsText, rhsValue, rhsHasSeparator, rhsFinal) = rhs { if lhsIndex != rhsIndex { return false } @@ -99,6 +99,9 @@ enum BotReceiptEntry: ItemListNodeEntry { if lhsValue != rhsValue { return false } + if lhsHasSeparator != rhsHasSeparator { + return false + } if lhsFinal != rhsFinal { return false } @@ -154,39 +157,41 @@ enum BotReceiptEntry: ItemListNodeEntry { switch self { case let .header(theme, invoice, botName): return BotCheckoutHeaderItem(account: arguments.account, theme: theme, invoice: invoice, botName: botName, sectionId: self.section) - 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): + case let .price(_, theme, text, value, hasSeparator, isFinal): + return BotCheckoutPriceItem(theme: theme, title: text, label: value, isFinal: isFinal, hasSeparator: hasSeparator, shimmeringIndex: nil, sectionId: self.section) + case let .paymentMethod(_, text, value): return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .none, action: nil) - case let .shippingInfo(theme, text, value): + case let .shippingInfo(_, text, value): return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .none, action: nil) - case let .shippingMethod(theme, text, value): + case let .shippingMethod(_, text, value): return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .none, action: nil) - case let .nameInfo(theme, text, value): + case let .nameInfo(_, text, value): return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .none, action: nil) - case let .emailInfo(theme, text, value): + case let .emailInfo(_, text, value): return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .none, action: nil) - case let .phoneInfo(theme, text, value): + case let .phoneInfo(_, text, value): return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .none, action: nil) } } } -private func botReceiptControllerEntries(presentationData: PresentationData, invoice: TelegramMediaInvoice, formInvoice: BotPaymentInvoice?, formInfo: BotPaymentRequestedInfo?, shippingOption: BotPaymentShippingOption?, paymentMethodTitle: String?, botPeer: Peer?) -> [BotReceiptEntry] { +private func botReceiptControllerEntries(presentationData: PresentationData, invoice: TelegramMediaInvoice?, formInvoice: BotPaymentInvoice?, formInfo: BotPaymentRequestedInfo?, shippingOption: BotPaymentShippingOption?, paymentMethodTitle: String?, botPeer: Peer?, tipAmount: Int64?) -> [BotReceiptEntry] { var entries: [BotReceiptEntry] = [] var botName = "" if let botPeer = botPeer { botName = botPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) } - entries.append(.header(presentationData.theme, invoice, botName)) + if let invoice = invoice { + entries.append(.header(presentationData.theme, invoice, botName)) + } if let formInvoice = formInvoice { var totalPrice: Int64 = 0 var index = 0 for price in formInvoice.prices { - entries.append(.price(index, presentationData.theme, price.label, formatCurrencyAmount(price.amount, currency: formInvoice.currency), false)) + entries.append(.price(index, presentationData.theme, price.label, formatCurrencyAmount(price.amount, currency: formInvoice.currency), index == 0, false)) totalPrice += price.amount index += 1 } @@ -196,13 +201,19 @@ private func botReceiptControllerEntries(presentationData: PresentationData, inv shippingOptionString = shippingOption.title for price in shippingOption.prices { - entries.append(.price(index, presentationData.theme, price.label, formatCurrencyAmount(price.amount, currency: formInvoice.currency), false)) + entries.append(.price(index, presentationData.theme, price.label, formatCurrencyAmount(price.amount, currency: formInvoice.currency), index == 0, false)) totalPrice += price.amount index += 1 } } + + if let tipAmount = tipAmount, tipAmount != 0 { + entries.append(.price(index, presentationData.theme, presentationData.strings.Checkout_TipItem, formatCurrencyAmount(tipAmount, currency: formInvoice.currency), index == 0, false)) + totalPrice += tipAmount + index += 1 + } - entries.append(.price(index, presentationData.theme, presentationData.strings.Checkout_TotalAmount, formatCurrencyAmount(totalPrice, currency: formInvoice.currency), true)) + entries.append(.price(index, presentationData.theme, presentationData.strings.Checkout_TotalAmount, formatCurrencyAmount(totalPrice, currency: formInvoice.currency), true, true)) if let paymentMethodTitle = paymentMethodTitle { entries.append(.paymentMethod(presentationData.theme, presentationData.strings.Checkout_PaymentMethod, paymentMethodTitle)) @@ -262,12 +273,14 @@ final class BotReceiptControllerNode: ItemListControllerNode { private var presentationData: PresentationData - private let receiptData = Promise<(BotPaymentInvoice, BotPaymentRequestedInfo?, BotPaymentShippingOption?, String?)?>(nil) + private let receiptData = Promise<(BotPaymentInvoice, BotPaymentRequestedInfo?, BotPaymentShippingOption?, String?, TelegramMediaInvoice, Int64?)?>(nil) private var dataRequestDisposable: Disposable? - + + private let actionButtonPanelNode: ASDisplayNode + private let actionButtonPanelSeparator: ASDisplayNode private let actionButton: BotCheckoutActionButton - init(controller: ItemListController?, navigationBar: NavigationBar, updateNavigationOffset: @escaping (CGFloat) -> Void, context: AccountContext, invoice: TelegramMediaInvoice, messageId: MessageId, dismissAnimated: @escaping () -> Void) { + init(controller: ItemListController?, navigationBar: NavigationBar, context: AccountContext, messageId: MessageId, dismissAnimated: @escaping () -> Void) { self.context = context self.dismissAnimated = dismissAnimated @@ -277,24 +290,36 @@ final class BotReceiptControllerNode: ItemListControllerNode { 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) + let nodeState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: botReceiptControllerEntries(presentationData: presentationData, invoice: receiptData?.4, formInvoice: receiptData?.0, formInfo: receiptData?.1, shippingOption: receiptData?.2, paymentMethodTitle: receiptData?.3, botPeer: botPeer, tipAmount: receiptData?.5), style: .blocks, focusItemTag: nil, emptyStateItem: nil, animateChanges: false) return (ItemListPresentationData(presentationData), (nodeState, arguments)) } + + self.actionButtonPanelNode = ASDisplayNode() + self.actionButtonPanelNode.backgroundColor = self.presentationData.theme.rootController.navigationBar.opaqueBackgroundColor + + self.actionButtonPanelSeparator = ASDisplayNode() + self.actionButtonPanelSeparator.backgroundColor = self.presentationData.theme.rootController.navigationBar.separatorColor - self.actionButton = BotCheckoutActionButton(inactiveFillColor: self.presentationData.theme.list.plainBackgroundColor, activeFillColor: self.presentationData.theme.list.itemAccentColor, foregroundColor: self.presentationData.theme.list.plainBackgroundColor) - self.actionButton.setState(.inactive(self.presentationData.strings.Common_Done)) + self.actionButton = BotCheckoutActionButton(activeFillColor: self.presentationData.theme.list.itemAccentColor, foregroundColor: self.presentationData.theme.list.plainBackgroundColor) + self.actionButton.setState(.active(self.presentationData.strings.Common_Done)) - super.init(controller: controller, navigationBar: navigationBar, updateNavigationOffset: updateNavigationOffset, state: signal) + super.init(controller: controller, navigationBar: navigationBar, state: signal) - self.dataRequestDisposable = (requestBotPaymentReceipt(network: context.account.network, messageId: messageId) |> deliverOnMainQueue).start(next: { [weak self] receipt in + self.dataRequestDisposable = (context.engine.payments.requestBotPaymentReceipt(messageId: messageId) |> deliverOnMainQueue).start(next: { [weak self] receipt in if let strongSelf = self { - strongSelf.receiptData.set(.single((receipt.invoice, receipt.info, receipt.shippingOption, receipt.credentialsTitle))) + UIView.transition(with: strongSelf.view, duration: 0.25, options: UIView.AnimationOptions.transitionCrossDissolve, animations: { + }, completion: nil) + + strongSelf.receiptData.set(.single((receipt.invoice, receipt.info, receipt.shippingOption, receipt.credentialsTitle, receipt.invoiceMedia, receipt.tipAmount))) } }) self.actionButton.addTarget(self, action: #selector(self.actionButtonPressed), forControlEvents: .touchUpInside) - self.addSubnode(self.actionButton) + + self.addSubnode(self.actionButtonPanelNode) + self.actionButtonPanelNode.addSubnode(self.actionButtonPanelSeparator) + self.actionButtonPanelNode.addSubnode(self.actionButton) } deinit { @@ -303,12 +328,21 @@ final class BotReceiptControllerNode: ItemListControllerNode { 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, additionalInsets: layout.additionalInsets, 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)) + + let bottomPanelHorizontalInset: CGFloat = 16.0 + let bottomPanelVerticalInset: CGFloat = 16.0 + let bottomPanelHeight = max(updatedInsets.bottom, layout.inputHeight ?? 0.0) + bottomPanelVerticalInset * 2.0 + BotCheckoutActionButton.height + + transition.updateFrame(node: self.actionButtonPanelNode, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - bottomPanelHeight), size: CGSize(width: layout.size.width, height: bottomPanelHeight))) + transition.updateFrame(node: self.actionButtonPanelSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: layout.size.width, height: UIScreenPixel))) + + let actionButtonFrame = CGRect(origin: CGPoint(x: bottomPanelHorizontalInset, y: bottomPanelVerticalInset), size: CGSize(width: layout.size.width - bottomPanelHorizontalInset * 2.0, height: BotCheckoutActionButton.height)) transition.updateFrame(node: self.actionButton, frame: actionButtonFrame) - self.actionButton.updateLayout(size: actionButtonFrame.size, transition: transition) + self.actionButton.updateLayout(absoluteRect: actionButtonFrame.offsetBy(dx: self.actionButtonPanelNode.frame.minX, dy: self.actionButtonPanelNode.frame.minY), containerSize: layout.size, transition: transition) + + updatedInsets.bottom = bottomPanelHeight + + super.containerLayoutUpdated(ContainerViewLayout(size: layout.size, metrics: layout.metrics, deviceMetrics: layout.deviceMetrics, intrinsicInsets: updatedInsets, safeInsets: layout.safeInsets, additionalInsets: layout.additionalInsets, statusBarHeight: layout.statusBarHeight, inputHeight: layout.inputHeight, inputHeightIsInteractivellyChanging: layout.inputHeightIsInteractivellyChanging, inVoiceOver: layout.inVoiceOver), navigationBarHeight: navigationBarHeight, transition: transition, additionalInsets: additionalInsets) } @objc func actionButtonPressed() { diff --git a/submodules/BotPaymentsUI/Sources/Formatter/Currency.swift b/submodules/BotPaymentsUI/Sources/Formatter/Currency.swift new file mode 100644 index 0000000000..de6e7c03d2 --- /dev/null +++ b/submodules/BotPaymentsUI/Sources/Formatter/Currency.swift @@ -0,0 +1,178 @@ +// +// CurrencyCode.swift +// CurrencyText +// +// Created by Felipe Lefèvre Marino on 1/26/19. +// + +import Foundation + +/// Currency wraps all availabe currencies that can represented as formatted monetary values +/// A currency code is a three-letter code that is, in most cases, +/// composed of a country’s two-character Internet country code plus an extra character +/// to denote the currency unit. For example, the currency code for the Australian +/// dollar is “AUD”. Currency codes are based on the ISO 4217 standard +public enum Currency: String { + case afghani = "AFN", + algerianDinar = "DZD", + argentinePeso = "ARS", + armenianDram = "AMD", + arubanFlorin = "AWG", + australianDollar = "AUD", + azerbaijanManat = "AZN", + bahamianDollar = "BSD", + bahrainiDinar = "BHD", + baht = "THB", + balboa = "PAB", + barbadosDollar = "BBD", + belarusianRuble = "BYN", + belizeDollar = "BZD", + bermudianDollar = "BMD", + boliviano = "BOB", + bolívar = "VEF", + brazilianReal = "BRL", + bruneiDollar = "BND", + bulgarianLev = "BGN", + burundiFranc = "BIF", + caboVerdeEscudo = "CVE", + canadianDollar = "CAD", + caymanIslandsDollar = "KYD", + chileanPeso = "CLP", + colombianPeso = "COP", + comorianFranc = "KMF", + congoleseFranc = "CDF", + convertibleMark = "BAM", + cordobaOro = "NIO", + costaRicanColon = "CRC", + cubanPeso = "CUP", + czechKoruna = "CZK", + dalasi = "GMD", + danishKrone = "DKK", + denar = "MKD", + djiboutiFranc = "DJF", + dobra = "STN", + dollar = "USD", + dominicanPeso = "DOP", + dong = "VND", + eastCaribbeanDollar = "XCD", + egyptianPound = "EGP", + elSalvadorColon = "SVC", + ethiopianBirr = "ETB", + euro = "EUR", + falklandIslandsPound = "FKP", + fijiDollar = "FJD", + forint = "HUF", + ghanaCedi = "GHS", + gibraltarPound = "GIP", + gourde = "HTG", + guarani = "PYG", + guineanFranc = "GNF", + guyanaDollar = "GYD", + hongKongDollar = "HKD", + hryvnia = "UAH", + icelandKrona = "ISK", + indianRupee = "INR", + iranianRial = "IRR", + iraqiDinar = "IQD", + jamaicanDollar = "JMD", + jordanianDinar = "JOD", + kenyanShilling = "KES", + kina = "PGK", + kuna = "HRK", + kuwaitiDinar = "KWD", + kwanza = "AOA", + kyat = "MMK", + laoKip = "LAK", + lari = "GEL", + lebanesePound = "LBP", + lek = "ALL", + lempira = "HNL", + leone = "SLL", + liberianDollar = "LRD", + libyanDinar = "LYD", + lilangeni = "SZL", + loti = "LSL", + malagasyAriary = "MGA", + malawiKwacha = "MWK", + malaysianRinggit = "MYR", + mauritiusRupee = "MUR", + mexicanPeso = "MXN", + mexicanUnidadDeInversion = "MXV", + moldovanLeu = "MDL", + moroccanDirham = "MAD", + mozambiqueMetical = "MZN", + mvdol = "BOV", + naira = "NGN", + nakfa = "ERN", + namibiaDollar = "NAD", + nepaleseRupee = "NPR", + netherlandsAntilleanGuilder = "ANG", + newIsraeliSheqel = "ILS", + newTaiwanDollar = "TWD", + newZealandDollar = "NZD", + ngultrum = "BTN", + northKoreanWon = "KPW", + norwegianKrone = "NOK", + ouguiya = "MRU", + paanga = "TOP", + pakistanRupee = "PKR", + pataca = "MOP", + pesoConvertible = "CUC", + pesoUruguayo = "UYU", + philippinePiso = "PHP", + poundSterling = "GBP", + pula = "BWP", + qatariRial = "QAR", + quetzal = "GTQ", + rand = "ZAR", + rialOmani = "OMR", + riel = "KHR", + romanianLeu = "RON", + rufiyaa = "MVR", + rupiah = "IDR", + russianRuble = "RUB", + rwandaFranc = "RWF", + saintHelenaPound = "SHP", + saudiRiyal = "SAR", + serbianDinar = "RSD", + seychellesRupee = "SCR", + singaporeDollar = "SGD", + sol = "PEN", + solomonIslandsDollar = "SBD", + som = "KGS", + somaliShilling = "SOS", + somoni = "TJS", + southSudanesePound = "SSP", + sriLankaRupee = "LKR", + sudanesePound = "SDG", + surinamDollar = "SRD", + swedishKrona = "SEK", + swissFranc = "CHF", + syrianPound = "SYP", + taka = "BDT", + tala = "WST", + tanzanianShilling = "TZS", + tenge = "KZT", + trinidadAndTobagoDollar = "TTD", + tugrik = "MNT", + tunisianDinar = "TND", + turkishLira = "TRY", + turkmenistanNewManat = "TMT", + uaeDirham = "AED", + ugandaShilling = "UGX", + unidadDeFomento = "CLF", + unidadDeValorReal = "COU", + uruguayPesoEnUnidadesIndexadas = "UYI", + uzbekistanSum = "UZS", + vatu = "VUV", + wirEuro = "CHE", + wirFranc = "CHW", + won = "KRW", + yemeniRial = "YER", + yen = "JPY", + yuanRenminbi = "CNY", + zambianKwacha = "ZMW", + zimbabweDollar = "ZWL", + zloty = "PLN", + none +} diff --git a/submodules/BotPaymentsUI/Sources/Formatter/CurrencyFormatter.swift b/submodules/BotPaymentsUI/Sources/Formatter/CurrencyFormatter.swift new file mode 100644 index 0000000000..ef732a1874 --- /dev/null +++ b/submodules/BotPaymentsUI/Sources/Formatter/CurrencyFormatter.swift @@ -0,0 +1,345 @@ +// +// CurrencyFormatter.swift +// CurrencyText +// +// Created by Felipe Lefèvre Marino on 1/27/19. +// + +import Foundation + +import TelegramStringFormatting + +// MARK: - Currency protocols + +public protocol CurrencyFormatting { + var maxDigitsCount: Int { get } + var decimalDigits: Int { get set } + var maxValue: Double? { get set } + var minValue: Double? { get set } + var initialText: String { get } + var currencySymbol: String { get set } + + func string(from double: Double) -> String? + func unformatted(string: String) -> String? + func double(from string: String) -> Double? +} + +public protocol CurrencyAdjusting { + func formattedStringWithAdjustedDecimalSeparator(from string: String) -> String? + func formattedStringAdjustedToFitAllowedValues(from string: String) -> String? +} + +// MARK: - Currency formatter + +public class CurrencyFormatter: CurrencyFormatting { + + /// Set the locale to retrieve the currency from + /// You can pass a Swift type Locale or one of the + /// Locales enum options - that encapsulates all available locales. + public var locale: LocaleConvertible { + set { self.numberFormatter.locale = newValue.locale } + get { self.numberFormatter.locale } + } + + /// Set the desired currency type + /// * Note: The currency take effetcs above the displayed currency symbol, + /// however details such as decimal separators, grouping separators and others + /// will be set based on the defined locale. So for a precise experience, please + /// preferarbly setup both, when you are setting a currency that does not match the + /// default/current user locale. + public var currency: Currency { + set { numberFormatter.currencyCode = newValue.rawValue } + get { Currency(rawValue: numberFormatter.currencyCode) ?? .dollar } + } + + /// Define if currency symbol should be presented or not. + /// Note: when set to false the current currency symbol is removed + public var showCurrencySymbol: Bool = true { + didSet { + numberFormatter.currencySymbol = showCurrencySymbol ? numberFormatter.currencySymbol : "" + } + } + + /// The currency's symbol. + /// Can be used to read or set a custom symbol. + /// Note: showCurrencySymbol must be set to true for + /// the currencySymbol to be correctly changed. + public var currencySymbol: String { + set { + guard showCurrencySymbol else { return } + numberFormatter.currencySymbol = newValue + } + get { numberFormatter.currencySymbol } + } + + /// The lowest number allowed as input. + /// This value is initially set to the text field text + /// when defined. + public var minValue: Double? { + set { + guard let newValue = newValue else { return } + numberFormatter.minimum = NSNumber(value: newValue) + } + get { + if let minValue = numberFormatter.minimum { + return Double(truncating: minValue) + } + return nil + } + } + + /// The highest number allowed as input. + /// The text field will not allow the user to increase the input + /// value beyond it, when defined. + public var maxValue: Double? { + set { + guard let newValue = newValue else { return } + numberFormatter.maximum = NSNumber(value: newValue) + } + get { + if let maxValue = numberFormatter.maximum { + return Double(truncating: maxValue) + } + return nil + } + } + + /// The number of decimal digits shown. + /// default is set to zero. + /// * Example: With decimal digits set to 3, if the value to represent is "1", + /// the formatted text in the fractions will be ",001". + /// Other than that with the value as 1, the formatted text fractions will be ",1". + public var decimalDigits: Int { + set { + numberFormatter.minimumFractionDigits = newValue + numberFormatter.maximumFractionDigits = newValue + } + get { numberFormatter.minimumFractionDigits } + } + + /// Set decimal numbers behavior. + /// When set to true decimalDigits are automatically set to 2 (most currencies pattern), + /// and the decimal separator is presented. Otherwise decimal digits are not shown and + /// the separator gets hidden as well + /// When reading it returns the current pattern based on the setup. + /// Note: Setting decimal digits after, or alwaysShowsDecimalSeparator can overlap this definitios, + /// and should be only done if you need specific cases + public var hasDecimals: Bool { + set { + self.decimalDigits = newValue ? 2 : 0 + self.numberFormatter.alwaysShowsDecimalSeparator = newValue ? true : false + } + get { decimalDigits != 0 } + } + + /// Defines the string that is the decimal separator + /// Note: only presented when hasDecimals is true OR decimalDigits + /// is greater than 0. + public var decimalSeparator: String { + set { self.numberFormatter.currencyDecimalSeparator = newValue } + get { numberFormatter.currencyDecimalSeparator } + } + + /// Can be used to set a custom currency code string + public var currencyCode: String { + set { self.numberFormatter.currencyCode = newValue } + get { numberFormatter.currencyCode } + } + + /// Sets if decimal separator should always be presented, + /// even when decimal digits are disabled + public var alwaysShowsDecimalSeparator: Bool { + set { self.numberFormatter.alwaysShowsDecimalSeparator = newValue } + get { numberFormatter.alwaysShowsDecimalSeparator } + } + + /// The amount of grouped numbers. This definition is fixed for at least + /// the first non-decimal group of numbers, and is applied to all other + /// groups if secondaryGroupingSize does not have another value. + public var groupingSize: Int { + set { self.numberFormatter.groupingSize = newValue } + get { numberFormatter.groupingSize } + } + + /// The amount of grouped numbers after the first group. + /// Example: for the given value of 99999999999, when grouping size + /// is set to 3 and secondaryGroupingSize has 4 as value, + /// the number is represented as: (9999) (9999) [999]. + /// Beign [] grouping size and () secondary grouping size. + public var secondaryGroupingSize: Int { + set { self.numberFormatter.secondaryGroupingSize = newValue } + get { numberFormatter.secondaryGroupingSize } + } + + /// Defines the string that is shown between groups of numbers + /// * Example: a monetary value of a thousand (1000) with a grouping + /// separator == "." is represented as `1.000` *. + /// Note: It automatically sets hasGroupingSeparator to true. + public var groupingSeparator: String { + set { + self.numberFormatter.currencyGroupingSeparator = newValue + self.numberFormatter.usesGroupingSeparator = true + } + get { self.numberFormatter.currencyGroupingSeparator } + } + + /// Sets if has separator between all group of numbers. + /// * Example: when set to false, a bug number such as a million + /// is represented by tight numbers "1000000". Otherwise if set + /// to true each group is separated by the defined `groupingSeparator`. * + /// Note: When set to true only works by defining a grouping separator. + public var hasGroupingSeparator: Bool { + set { self.numberFormatter.usesGroupingSeparator = newValue } + get { self.numberFormatter.usesGroupingSeparator } + } + + /// Value that will be presented when the text field + /// text values matches zero (0) + public var zeroSymbol: String? { + set { numberFormatter.zeroSymbol = newValue } + get { numberFormatter.zeroSymbol } + } + + /// Value that will be presented when the text field + /// is empty. The default is "" - empty string + public var nilSymbol: String { + set { numberFormatter.nilSymbol = newValue } + get { return numberFormatter.nilSymbol } + } + + /// Encapsulated Number formatter + let numberFormatter: NumberFormatter + + /// Maximum allowed number of integers + public var maxIntegers: Int? { + set { + guard let maxIntegers = newValue else { return } + numberFormatter.maximumIntegerDigits = maxIntegers + } + get { return numberFormatter.maximumIntegerDigits } + } + + /// Returns the maximum allowed number of numerical characters + public var maxDigitsCount: Int { + numberFormatter.maximumIntegerDigits + numberFormatter.maximumFractionDigits + } + + /// The value zero formatted to serve as initial text. + public var initialText: String { + numberFormatter.string(from: 0) ?? "0.0" + } + + //MARK: - INIT + + /// Handler to initialize a new style. + public typealias InitHandler = ((CurrencyFormatter) -> (Void)) + + /// Initialize a new currency formatter with optional configuration handler callback. + /// + /// - Parameter handler: configuration handler callback. + + public init(currency: String, _ handler: InitHandler? = nil) { + numberFormatter = setupCurrencyNumberFormatter(currency: currency) + + numberFormatter.alwaysShowsDecimalSeparator = false + /*numberFormatter.numberStyle = .currency + + numberFormatter.minimumFractionDigits = 2 + numberFormatter.maximumFractionDigits = 2 + numberFormatter.minimumIntegerDigits = 1*/ + + handler?(self) + } +} + +// MARK: Format +extension CurrencyFormatter { + + /// Returns a currency string from a given double value. + /// + /// - Parameter double: the monetary amount. + /// - Returns: formatted currency string. + public func string(from double: Double) -> String? { + let validValue = valueAdjustedToFitAllowedValues(from: double) + return numberFormatter.string(from: validValue) + } + + /// Returns a double from a string that represents a numerical value. + /// + /// - Parameter string: string that describes the numerical value. + /// - Returns: the value as a Double. + public func double(from string: String) -> Double? { + Double(string) + } + + /// Receives a currency formatted string and returns its + /// numerical/unformatted representation. + /// + /// - Parameter string: currency formatted string + /// - Returns: numerical representation + public func unformatted(string: String) -> String? { + string.numeralFormat() + } +} + +// MARK: - Currency adjusting conformance + +extension CurrencyFormatter: CurrencyAdjusting { + + /// Receives a currency formatted String, and returns it with its decimal separator adjusted. + /// + /// _Note_: Useful when appending values to a currency formatted String. + /// E.g. "$ 23.24" after users taps an additional number, is equal = "$ 23.247". + /// Which gets updated to "$ 232.47". + /// + /// - Parameter string: The currency formatted String + /// - Returns: The currency formatted received String with its decimal separator adjusted + public func formattedStringWithAdjustedDecimalSeparator(from string: String) -> String? { + let adjustedString = numeralStringWithAdjustedDecimalSeparator(from: string) + guard let value = double(from: adjustedString) else { return nil } + + return self.numberFormatter.string(from: value) + } + + /// Receives a currency formatted String, and returns it to fit the formatter's min and max values, when needed. + /// + /// - Parameter string: The currency formatted String + /// - Returns: The currency formatted String, or the formatted version of its closes allowed value, min or max, depending on the closest boundary. + public func formattedStringAdjustedToFitAllowedValues(from string: String) -> String? { + let adjustedString = numeralStringWithAdjustedDecimalSeparator(from: string) + guard let originalValue = double(from: adjustedString) else { return nil } + + return self.string(from: originalValue) + } + + /// Receives a currency formatted String, and returns a numeral version of it with its decimal separator adjusted. + /// + /// E.g. "$ 23.24", after users taps an additional number, get equal as "$ 23.247". The returned value would be "232.47". + /// + /// - Parameter string: The currency formatted String + /// - Returns: The received String with numeral format and with its decimal separator adjusted + private func numeralStringWithAdjustedDecimalSeparator(from string: String) -> String { + var updatedString = string.numeralFormat() + let isNegative: Bool = string.contains(String.negativeSymbol) + + updatedString = isNegative ? .negativeSymbol + updatedString : updatedString + updatedString.updateDecimalSeparator(decimalDigits: decimalDigits) + + return updatedString + } + + /// Receives a Double value, and returns it adjusted to fit min and max allowed values, when needed. + /// If the value respect number formatter's min and max, it will be returned without changes. + /// + /// - Parameter value: The value to be adjusted if needed + /// - Returns: The value updated or not, depending on the formatter's settings + private func valueAdjustedToFitAllowedValues(from value: Double) -> Double { + if let minValue = minValue, value < minValue { + return minValue + } else if let maxValue = maxValue, value > maxValue { + return maxValue + } + + return value + } +} diff --git a/submodules/BotPaymentsUI/Sources/Formatter/CurrencyLocale.swift b/submodules/BotPaymentsUI/Sources/Formatter/CurrencyLocale.swift new file mode 100644 index 0000000000..e9af7b2f76 --- /dev/null +++ b/submodules/BotPaymentsUI/Sources/Formatter/CurrencyLocale.swift @@ -0,0 +1,755 @@ +// +// CurrencyLocale.swift +// CurrencyText +// +// Created by Felipe Lefèvre Marino on 1/26/19. +// + +import Foundation + +/// All locales were extracted from: +/// jacobbubu/ioslocaleidentifiers.csv - https://gist.github.com/jacobbubu/1836273 + +/// The LocaleConvertible pattern is inspired in SwiftDate by malcommac +/// https://github.com/malcommac/SwiftDate + +/// LocaleConvertible defines the behavior to convert locale info to system Locale type +public protocol LocaleConvertible { + var locale: Locale { get } +} + +extension Locale: LocaleConvertible { + public var locale: Locale { return self } +} + +/// Defines locales available in system +public enum CurrencyLocale: String, LocaleConvertible { + + case current = "current" + case autoUpdating = "currentAutoUpdating" + + case afrikaans = "af" + case afrikaansNamibia = "af_NA" + case afrikaansSouthAfrica = "af_ZA" + case aghem = "agq" + case aghemCameroon = "agq_CM" + case akan = "ak" + case akanGhana = "ak_GH" + case albanian = "sq" + case albanianAlbania = "sq_AL" + case albanianKosovo = "sq_XK" + case albanianMacedonia = "sq_MK" + case amharic = "am" + case amharicEthiopia = "am_ET" + case arabic = "ar" + case arabicAlgeria = "ar_DZ" + case arabicBahrain = "ar_BH" + case arabicChad = "ar_TD" + case arabicComoros = "ar_KM" + case arabicDjibouti = "ar_DJ" + case arabicEgypt = "ar_EG" + case arabicEritrea = "ar_ER" + case arabicIraq = "ar_IQ" + case arabicIsrael = "ar_IL" + case arabicJordan = "ar_JO" + case arabicKuwait = "ar_KW" + case arabicLebanon = "ar_LB" + case arabicLibya = "ar_LY" + case arabicMauritania = "ar_MR" + case arabicMorocco = "ar_MA" + case arabicOman = "ar_OM" + case arabicPalestinianTerritories = "ar_PS" + case arabicQatar = "ar_QA" + case arabicSaudiArabia = "ar_SA" + case arabicSomalia = "ar_SO" + case arabicSouthSudan = "ar_SS" + case arabicSudan = "ar_SD" + case arabicSyria = "ar_SY" + case arabicTunisia = "ar_TN" + case arabicUnitedArabEmirates = "ar_AE" + case arabicWesternSahara = "ar_EH" + case arabicWorld = "ar_001" + case arabicYemen = "ar_YE" + case armenian = "hy" + case armenianArmenia = "hy_AM" + case assamese = "as" + case assameseIndia = "as_IN" + case asu = "asa" + case asuTanzania = "asa_TZ" + case azerbaijani = "az_Latn" + case azerbaijaniAzerbaijan = "az_Latn_AZ" + case azerbaijaniCyrillic = "az_Cyrl" + case azerbaijaniCyrillicAzerbaijan = "az_Cyrl_AZ" + case bafia = "ksf" + case bafiaCameroon = "ksf_CM" + case bambara = "bm_Latn" + case bambaraMali = "bm_Latn_ML" + case basaa = "bas" + case basaaCameroon = "bas_CM" + case basque = "eu" + case basqueSpain = "eu_ES" + case belarusian = "be" + case belarusianBelarus = "be_BY" + case bemba = "bem" + case bembaZambia = "bem_ZM" + case bena = "bez" + case benaTanzania = "bez_TZ" + case bengali = "bn" + case bengaliBangladesh = "bn_BD" + case engaliIndia = "bn_IN" + case bodo = "brx" + case bodoIndia = "brx_IN" + case bosnian = "bs_Latn" + case bosnianBosniaHerzegovina = "bs_Latn_BA" + case bosnianCyrillic = "bs_Cyrl" + case bosnianCyrillicBosniaHerzegovina = "bs_Cyrl_BA" + case breton = "br" + case bretonFrance = "br_FR" + case bulgarian = "bg" + case bulgarianBulgaria = "bg_BG" + case burmese = "my" + case burmeseMyanmarBurma = "my_MM" + case catalan = "ca" + case catalanAndorra = "ca_AD" + case catalanFrance = "ca_FR" + case catalanItaly = "ca_IT" + case catalanSpain = "ca_ES" + case centralAtlasTamazight = "tzm_Latn" + case centralAtlasTamazightMorocco = "tzm_Latn_MA" + case centralKurdish = "ckb" + case centralKurdishIran = "ckb_IR" + case centralKurdishIraq = "ckb_IQ" + case cherokee = "chr" + case cherokeeUnitedStates = "chr_US" + case chiga = "cgg" + case chigaUganda = "cgg_UG" + case chinese = "zh" + case chineseChina = "zh_Hans_CN" + case chineseHongKongSarChina = "zh_Hant_HK" + case chineseMacauSarChina = "zh_Hant_MO" + case chineseSimplified = "zh_Hans" + case chineseSimplifiedHongKongSarChina = "zh_Hans_HK" + case chineseSimplifiedMacauSarChina = "zh_Hans_MO" + case chineseSingapore = "zh_Hans_SG" + case chineseTaiwan = "zh_Hant_TW" + case chineseTraditional = "zh_Hant" + case colognian = "ksh" + case colognianGermany = "ksh_DE" + case cornish = "kw" + case cornishUnitedKingdom = "kw_GB" + case croatian = "hr" + case croatianBosniaHerzegovina = "hr_BA" + case croatianCroatia = "hr_HR" + case czech = "cs" + case czechCzechRepublic = "cs_CZ" + case danish = "da" + case danishDenmark = "da_DK" + case danishGreenland = "da_GL" + case duala = "dua" + case dualaCameroon = "dua_CM" + case dutch = "nl" + case dutchAruba = "nl_AW" + case dutchBelgium = "nl_BE" + case dutchCaribbeanNetherlands = "nl_BQ" + case dutchCuraao = "nl_CW" + case dutchNetherlands = "nl_NL" + case dutchSintMaarten = "nl_SX" + case dutchSuriname = "nl_SR" + case dzongkha = "dz" + case dzongkhaBhutan = "dz_BT" + case embu = "ebu" + case embuKenya = "ebu_KE" + case english = "en" + case englishAlbania = "en_AL" + case englishAmericanSamoa = "en_AS" + case englishAndorra = "en_AD" + case englishAnguilla = "en_AI" + case englishAntiguaBarbuda = "en_AG" + case englishAustralia = "en_AU" + case englishAustria = "en_AT" + case englishBahamas = "en_BS" + case englishBarbados = "en_BB" + case englishBelgium = "en_BE" + case englishBelize = "en_BZ" + case englishBermuda = "en_BM" + case englishBosniaHerzegovina = "en_BA" + case englishBotswana = "en_BW" + case englishBritishIndianOceanTerritory = "en_IO" + case englishBritishVirginIslands = "en_VG" + case englishCameroon = "en_CM" + case englishCanada = "en_CA" + case englishCaymanIslands = "en_KY" + case englishChristmasIsland = "en_CX" + case englishCocosKeelingIslands = "en_CC" + case englishCookIslands = "en_CK" + case englishCroatia = "en_HR" + case englishCyprus = "en_CY" + case englishCzechRepublic = "en_CZ" + case englishDenmark = "en_DK" + case englishDiegoGarcia = "en_DG" + case englishDominica = "en_DM" + case englishEritrea = "en_ER" + case englishEstonia = "en_EE" + case englishEurope = "en_150" + case englishFalklandIslands = "en_FK" + case englishFiji = "en_FJ" + case englishFinland = "en_FI" + case englishFrance = "en_FR" + case englishGambia = "en_GM" + case englishGermany = "en_DE" + case englishGhana = "en_GH" + case englishGibraltar = "en_GI" + case englishGreece = "en_GR" + case englishGrenada = "en_GD" + case englishGuam = "en_GU" + case englishGuernsey = "en_GG" + case englishGuyana = "en_GY" + case englishHongKongSarChina = "en_HK" + case englishHungary = "en_HU" + case englishIceland = "en_IS" + case englishIndia = "en_IN" + case englishIreland = "en_IE" + case englishIsleOfMan = "en_IM" + case englishIsrael = "en_IL" + case englishItaly = "en_IT" + case englishJamaica = "en_JM" + case englishJersey = "en_JE" + case englishKenya = "en_KE" + case englishKiribati = "en_KI" + case englishLatvia = "en_LV" + case englishLesotho = "en_LS" + case englishLiberia = "en_LR" + case englishLithuania = "en_LT" + case englishLuxembourg = "en_LU" + case englishMacauSarChina = "en_MO" + case englishMadagascar = "en_MG" + case englishMalawi = "en_MW" + case englishMalaysia = "en_MY" + case englishMalta = "en_MT" + case englishMarshallIslands = "en_MH" + case englishMauritius = "en_MU" + case englishMicronesia = "en_FM" + case englishMontenegro = "en_ME" + case englishMontserrat = "en_MS" + case englishNamibia = "en_NA" + case englishNauru = "en_NR" + case englishNetherlands = "en_NL" + case englishNewZealand = "en_NZ" + case englishNigeria = "en_NG" + case englishNiue = "en_NU" + case englishNorfolkIsland = "en_NF" + case englishNorthernMarianaIslands = "en_MP" + case englishNorway = "en_NO" + case englishPakistan = "en_PK" + case englishPalau = "en_PW" + case englishPapuaNewGuinea = "en_PG" + case englishPhilippines = "en_PH" + case englishPitcairnIslands = "en_PN" + case englishPoland = "en_PL" + case englishPortugal = "en_PT" + case englishPuertoRico = "en_PR" + case englishRomania = "en_RO" + case englishRussia = "en_RU" + case englishRwanda = "en_RW" + case englishSamoa = "en_WS" + case englishSeychelles = "en_SC" + case englishSierraLeone = "en_SL" + case englishSingapore = "en_SG" + case englishSintMaarten = "en_SX" + case englishSlovakia = "en_SK" + case englishSlovenia = "en_SI" + case englishSolomonIslands = "en_SB" + case englishSouthAfrica = "en_ZA" + case englishSouthSudan = "en_SS" + case englishSpain = "en_ES" + case englishStHelena = "en_SH" + case englishStKittsNevis = "en_KN" + case englishStLucia = "en_LC" + case englishStVincentGrenadines = "en_VC" + case englishSudan = "en_SD" + case englishSwaziland = "en_SZ" + case englishSweden = "en_SE" + case englishSwitzerland = "en_CH" + case englishTanzania = "en_TZ" + case englishTokelau = "en_TK" + case englishTonga = "en_TO" + case englishTrinidadTobago = "en_TT" + case englishTurkey = "en_TR" + case englishTurksCaicosIslands = "en_TC" + case englishTuvalu = "en_TV" + case englishUSOutlyingIslands = "en_UM" + case englishUSVirginIslands = "en_VI" + case englishUganda = "en_UG" + case englishUnitedKingdom = "en_GB" + case englishUnitedStates = "en_US" + case englishUnitedStatesComputer = "en_US_POSIX" + case englishVanuatu = "en_VU" + case englishWorld = "en_001" + case englishZambia = "en_ZM" + case englishZimbabwe = "en_ZW" + case esperanto = "eo" + case estonian = "et" + case estonianEstonia = "et_EE" + case ewe = "ee" + case eweGhana = "ee_GH" + case eweTogo = "ee_TG" + case ewondo = "ewo" + case ewondoCameroon = "ewo_CM" + case faroese = "fo" + case faroeseFaroeIslands = "fo_FO" + case filipino = "fil" + case filipinoPhilippines = "fil_PH" + case finnish = "fi" + case finnishFinland = "fi_FI" + case french = "fr" + case frenchAlgeria = "fr_DZ" + case frenchBelgium = "fr_BE" + case frenchBenin = "fr_BJ" + case frenchBurkinaFaso = "fr_BF" + case frenchBurundi = "fr_BI" + case frenchCameroon = "fr_CM" + case frenchCanada = "fr_CA" + case frenchCentralAfricanRepublic = "fr_CF" + case frenchChad = "fr_TD" + case frenchComoros = "fr_KM" + case frenchCongoBrazzaville = "fr_CG" + case frenchCongoKinshasa = "fr_CD" + case frenchCteDivoire = "fr_CI" + case frenchDjibouti = "fr_DJ" + case frenchEquatorialGuinea = "fr_GQ" + case frenchFrance = "fr_FR" + case frenchFrenchGuiana = "fr_GF" + case frenchFrenchPolynesia = "fr_PF" + case frenchGabon = "fr_GA" + case frenchGuadeloupe = "fr_GP" + case frenchGuinea = "fr_GN" + case frenchHaiti = "fr_HT" + case frenchLuxembourg = "fr_LU" + case frenchMadagascar = "fr_MG" + case frenchMali = "fr_ML" + case frenchMartinique = "fr_MQ" + case frenchMauritania = "fr_MR" + case frenchMauritius = "fr_MU" + case frenchMayotte = "fr_YT" + case frenchMonaco = "fr_MC" + case frenchMorocco = "fr_MA" + case frenchNewCaledonia = "fr_NC" + case frenchNiger = "fr_NE" + case frenchRunion = "fr_RE" + case frenchRwanda = "fr_RW" + case frenchSenegal = "fr_SN" + case frenchSeychelles = "fr_SC" + case frenchStBarthlemy = "fr_BL" + case frenchStMartin = "fr_MF" + case frenchStPierreMiquelon = "fr_PM" + case frenchSwitzerland = "fr_CH" + case frenchSyria = "fr_SY" + case frenchTogo = "fr_TG" + case frenchTunisia = "fr_TN" + case frenchVanuatu = "fr_VU" + case frenchWallisFutuna = "fr_WF" + case friulian = "fur" + case friulianItaly = "fur_IT" + case fulah = "ff" + case fulahCameroon = "ff_CM" + case fulahGuinea = "ff_GN" + case fulahMauritania = "ff_MR" + case fulahSenegal = "ff_SN" + case galician = "gl" + case galicianSpain = "gl_ES" + case ganda = "lg" + case gandaUganda = "lg_UG" + case georgian = "ka" + case georgianGeorgia = "ka_GE" + case german = "de" + case germanAustria = "de_AT" + case germanBelgium = "de_BE" + case germanGermany = "de_DE" + case germanLiechtenstein = "de_LI" + case germanLuxembourg = "de_LU" + case germanSwitzerland = "de_CH" + case greek = "el" + case greekCyprus = "el_CY" + case greekGreece = "el_GR" + case gujarati = "gu" + case gujaratiIndia = "gu_IN" + case gusii = "guz" + case gusiiKenya = "guz_KE" + case hausa = "ha_Latn" + case hausaGhana = "ha_Latn_GH" + case hausaNiger = "ha_Latn_NE" + case hausaNigeria = "ha_Latn_NG" + case hawaiian = "haw" + case hawaiianUnitedStates = "haw_US" + case hebrew = "he" + case hebrewIsrael = "he_IL" + case hindi = "hi" + case hindiIndia = "hi_IN" + case hungarian = "hu" + case hungarianHungary = "hu_HU" + case icelandic = "is" + case icelandicIceland = "is_IS" + case igbo = "ig" + case igboNigeria = "ig_NG" + case inariSami = "smn" + case inariSamiFinland = "smn_FI" + case indonesian = "id" + case indonesianIndonesia = "id_ID" + case inuktitut = "iu" + case inuktitutUnifiedCanadianAboriginalSyllabics = "iu_Cans" + case inuktitutUnifiedCanadianAboriginalSyllabicsCanada = "iu_Cans_CA" + case irish = "ga" + case irishIreland = "ga_IE" + case italian = "it" + case italianItaly = "it_IT" + case italianSanMarino = "it_SM" + case italianSwitzerland = "it_CH" + case japanese = "ja" + case japaneseJapan = "ja_JP" + case jolaFonyi = "dyo" + case jolaFonyiSenegal = "dyo_SN" + case kabuverdianu = "kea" + case kabuverdianuCapeVerde = "kea_CV" + case kabyle = "kab" + case kabyleAlgeria = "kab_DZ" + case kako = "kkj" + case kakoCameroon = "kkj_CM" + case kalaallisut = "kl" + case kalaallisutGreenland = "kl_GL" + case kalenjin = "kln" + case kalenjinKenya = "kln_KE" + case kamba = "kam" + case kambaKenya = "kam_KE" + case kannada = "kn" + case kannadaIndia = "kn_IN" + case kashmiri = "ks" + case kashmiriArabic = "ks_Arab" + case kashmiriArabicIndia = "ks_Arab_IN" + case kazakh = "kk_Cyrl" + case kazakhKazakhstan = "kk_Cyrl_KZ" + case khmer = "km" + case khmerCambodia = "km_KH" + case kikuyu = "ki" + case kikuyuKenya = "ki_KE" + case kinyarwanda = "rw" + case kinyarwandaRwanda = "rw_RW" + case konkani = "kok" + case konkaniIndia = "kok_IN" + case korean = "ko" + case koreanNorthKorea = "ko_KP" + case koreanSouthKorea = "ko_KR" + case koyraChiini = "khq" + case koyraChiiniMali = "khq_ML" + case koyraboroSenni = "ses" + case koyraboroSenniMali = "ses_ML" + case kwasio = "nmg" + case kwasioCameroon = "nmg_CM" + case kyrgyz = "ky_Cyrl" + case kyrgyzKyrgyzstan = "ky_Cyrl_KG" + case lakota = "lkt" + case lakotaUnitedStates = "lkt_US" + case langi = "lag" + case langiTanzania = "lag_TZ" + case lao = "lo" + case laoLaos = "lo_LA" + case latvian = "lv" + case latvianLatvia = "lv_LV" + case lingala = "ln" + case lingalaAngola = "ln_AO" + case lingalaCentralAfricanRepublic = "ln_CF" + case lingalaCongoBrazzaville = "ln_CG" + case lingalaCongoKinshasa = "ln_CD" + case lithuanian = "lt" + case lithuanianLithuania = "lt_LT" + case lowerSorbian = "dsb" + case lowerSorbianGermany = "dsb_DE" + case lubaKatanga = "lu" + case lubaKatangaCongoKinshasa = "lu_CD" + case luo = "luo" + case luoKenya = "luo_KE" + case luxembourgish = "lb" + case luxembourgishLuxembourg = "lb_LU" + case luyia = "luy" + case luyiaKenya = "luy_KE" + case macedonian = "mk" + case macedonianMacedonia = "mk_MK" + case machame = "jmc" + case machameTanzania = "jmc_TZ" + case makhuwaMeetto = "mgh" + case makhuwaMeettoMozambique = "mgh_MZ" + case makonde = "kde" + case makondeTanzania = "kde_TZ" + case malagasy = "mg" + case malagasyMadagascar = "mg_MG" + case malay = "ms_Latn" + case malayArabic = "ms_Arab" + case malayArabicBrunei = "ms_Arab_BN" + case malayArabicMalaysia = "ms_Arab_MY" + case malayBrunei = "ms_Latn_BN" + case malayMalaysia = "ms_Latn_MY" + case malaySingapore = "ms_Latn_SG" + case malayalam = "ml" + case malayalamIndia = "ml_IN" + case maltese = "mt" + case malteseMalta = "mt_MT" + case manx = "gv" + case manxIsleOfMan = "gv_IM" + case marathi = "mr" + case marathiIndia = "mr_IN" + case masai = "mas" + case masaiKenya = "mas_KE" + case masaiTanzania = "mas_TZ" + case meru = "mer" + case meruKenya = "mer_KE" + case meta = "mgo" + case metaCameroon = "mgo_CM" + case mongolian = "mn_Cyrl" + case mongolianMongolia = "mn_Cyrl_MN" + case morisyen = "mfe" + case morisyenMauritius = "mfe_MU" + case mundang = "mua" + case mundangCameroon = "mua_CM" + case nama = "naq" + case namaNamibia = "naq_NA" + case nepali = "ne" + case nepaliIndia = "ne_IN" + case nepaliNepal = "ne_NP" + case ngiemboon = "nnh" + case ngiemboonCameroon = "nnh_CM" + case ngomba = "jgo" + case ngombaCameroon = "jgo_CM" + case northNdebele = "nd" + case northNdebeleZimbabwe = "nd_ZW" + case northernSami = "se" + case northernSamiFinland = "se_FI" + case northernSamiNorway = "se_NO" + case northernSamiSweden = "se_SE" + case norwegianBokml = "nb" + case norwegianBokmlNorway = "nb_NO" + case norwegianBokmlSvalbardJanMayen = "nb_SJ" + case norwegianNynorsk = "nn" + case norwegianNynorskNorway = "nn_NO" + case nuer = "nus" + case nuerSudan = "nus_SD" + case nyankole = "nyn" + case nyankoleUganda = "nyn_UG" + case oriya = "or" + case oriyaIndia = "or_IN" + case oromo = "om" + case oromoEthiopia = "om_ET" + case oromoKenya = "om_KE" + case ossetic = "os" + case osseticGeorgia = "os_GE" + case osseticRussia = "os_RU" + case pashto = "ps" + case pashtoAfghanistan = "ps_AF" + case persian = "fa" + case persianAfghanistan = "fa_AF" + case persianIran = "fa_IR" + case polish = "pl" + case polishPoland = "pl_PL" + case portuguese = "pt" + case portugueseAngola = "pt_AO" + case portugueseBrazil = "pt_BR" + case portugueseCapeVerde = "pt_CV" + case portugueseGuineaBissau = "pt_GW" + case portugueseMacauSarChina = "pt_MO" + case portugueseMozambique = "pt_MZ" + case portuguesePortugal = "pt_PT" + case portugueseSoTomPrncipe = "pt_ST" + case portugueseTimorLeste = "pt_TL" + case punjabi = "pa_Guru" + case punjabiArabic = "pa_Arab" + case punjabiArabicPakistan = "pa_Arab_PK" + case punjabiIndia = "pa_Guru_IN" + case quechua = "qu" + case quechuaBolivia = "qu_BO" + case quechuaEcuador = "qu_EC" + case quechuaPeru = "qu_PE" + case romanian = "ro" + case romanianMoldova = "ro_MD" + case romanianRomania = "ro_RO" + case romansh = "rm" + case romanshSwitzerland = "rm_CH" + case rombo = "rof" + case romboTanzania = "rof_TZ" + case rundi = "rn" + case rundiBurundi = "rn_BI" + case russian = "ru" + case russianBelarus = "ru_BY" + case russianKazakhstan = "ru_KZ" + case russianKyrgyzstan = "ru_KG" + case russianMoldova = "ru_MD" + case russianRussia = "ru_RU" + case russianUkraine = "ru_UA" + case rwa = "rwk" + case rwaTanzania = "rwk_TZ" + case sakha = "sah" + case sakhaRussia = "sah_RU" + case samburu = "saq" + case samburuKenya = "saq_KE" + case sango = "sg" + case sangoCentralAfricanRepublic = "sg_CF" + case sangu = "sbp" + case sanguTanzania = "sbp_TZ" + case scottishGaelic = "gd" + case scottishGaelicUnitedKingdom = "gd_GB" + case sena = "seh" + case senaMozambique = "seh_MZ" + case serbian = "sr_Cyrl" + case serbianBosniaHerzegovina = "sr_Cyrl_BA" + case serbianKosovo = "sr_Cyrl_XK" + case serbianLatin = "sr_Latn" + case serbianLatinBosniaHerzegovina = "sr_Latn_BA" + case serbianLatinKosovo = "sr_Latn_XK" + case serbianLatinMontenegro = "sr_Latn_ME" + case serbianLatinSerbia = "sr_Latn_RS" + case serbianMontenegro = "sr_Cyrl_ME" + case serbianSerbia = "sr_Cyrl_RS" + case shambala = "ksb" + case shambalaTanzania = "ksb_TZ" + case shona = "sn" + case shonaZimbabwe = "sn_ZW" + case sichuanYi = "ii" + case sichuanYiChina = "ii_CN" + case sinhala = "si" + case sinhalaSriLanka = "si_LK" + case slovak = "sk" + case slovakSlovakia = "sk_SK" + case slovenian = "sl" + case slovenianSlovenia = "sl_SI" + case soga = "xog" + case sogaUganda = "xog_UG" + case somali = "so" + case somaliDjibouti = "so_DJ" + case somaliEthiopia = "so_ET" + case somaliKenya = "so_KE" + case somaliSomalia = "so_SO" + case spanish = "es" + case spanishArgentina = "es_AR" + case spanishBolivia = "es_BO" + case spanishCanaryIslands = "es_IC" + case spanishCeutaMelilla = "es_EA" + case spanishChile = "es_CL" + case spanishColombia = "es_CO" + case spanishCostaRica = "es_CR" + case spanishCuba = "es_CU" + case spanishDominicanRepublic = "es_DO" + case spanishEcuador = "es_EC" + case spanishElSalvador = "es_SV" + case spanishEquatorialGuinea = "es_GQ" + case spanishGuatemala = "es_GT" + case spanishHonduras = "es_HN" + case spanishLatinAmerica = "es_419" + case spanishMexico = "es_MX" + case spanishNicaragua = "es_NI" + case spanishPanama = "es_PA" + case spanishParaguay = "es_PY" + case spanishPeru = "es_PE" + case spanishPhilippines = "es_PH" + case spanishPuertoRico = "es_PR" + case spanishSpain = "es_ES" + case spanishUnitedStates = "es_US" + case spanishUruguay = "es_UY" + case spanishVenezuela = "es_VE" + case standardMoroccanTamazight = "zgh" + case standardMoroccanTamazightMorocco = "zgh_MA" + case swahili = "sw" + case swahiliCongoKinshasa = "sw_CD" + case swahiliKenya = "sw_KE" + case swahiliTanzania = "sw_TZ" + case swahiliUganda = "sw_UG" + case swedish = "sv" + case swedishlandIslands = "sv_AX" + case swedishFinland = "sv_FI" + case swedishSweden = "sv_SE" + case swissGerman = "gsw" + case swissGermanFrance = "gsw_FR" + case swissGermanLiechtenstein = "gsw_LI" + case swissGermanSwitzerland = "gsw_CH" + case tachelhit = "shi_Latn" + case tachelhitMorocco = "shi_Latn_MA" + case tachelhitTifinagh = "shi_Tfng" + case tachelhitTifinaghMorocco = "shi_Tfng_MA" + case taita = "dav" + case taitaKenya = "dav_KE" + case tajik = "tg_Cyrl" + case tajikTajikistan = "tg_Cyrl_TJ" + case tamil = "ta" + case tamilIndia = "ta_IN" + case tamilMalaysia = "ta_MY" + case tamilSingapore = "ta_SG" + case tamilSriLanka = "ta_LK" + case tasawaq = "twq" + case tasawaqNiger = "twq_NE" + case telugu = "te" + case teluguIndia = "te_IN" + case teso = "teo" + case tesoKenya = "teo_KE" + case tesoUganda = "teo_UG" + case thai = "th" + case thaiThailand = "th_TH" + case tibetan = "bo" + case tibetanChina = "bo_CN" + case tibetanIndia = "bo_IN" + case tigrinya = "ti" + case tigrinyaEritrea = "ti_ER" + case tigrinyaEthiopia = "ti_ET" + case tongan = "to" + case tonganTonga = "to_TO" + case turkish = "tr" + case turkishCyprus = "tr_CY" + case turkishTurkey = "tr_TR" + case turkmen = "tk_Latn" + case turkmenTurkmenistan = "tk_Latn_TM" + case ukrainian = "uk" + case ukrainianUkraine = "uk_UA" + case upperSorbian = "hsb" + case upperSorbianGermany = "hsb_DE" + case urdu = "ur" + case urduIndia = "ur_IN" + case urduPakistan = "ur_PK" + case uyghur = "ug" + case uyghurArabic = "ug_Arab" + case uyghurArabicChina = "ug_Arab_CN" + case uzbek = "uz_Cyrl" + case uzbekArabic = "uz_Arab" + case uzbekArabicAfghanistan = "uz_Arab_AF" + case uzbekLatin = "uz_Latn" + case uzbekLatinUzbekistan = "uz_Latn_UZ" + case uzbekUzbekistan = "uz_Cyrl_UZ" + case vai = "vai_Vaii" + case vaiLatin = "vai_Latn" + case vaiLatinLiberia = "vai_Latn_LR" + case vaiLiberia = "vai_Vaii_LR" + case vietnamese = "vi" + case vietnameseVietnam = "vi_VN" + case vunjo = "vun" + case vunjoTanzania = "vun_TZ" + case walser = "wae" + case walserSwitzerland = "wae_CH" + case welsh = "cy" + case welshUnitedKingdom = "cy_GB" + case westernFrisian = "fy" + case westernFrisianNetherlands = "fy_NL" + case yangben = "yav" + case yangbenCameroon = "yav_CM" + case yiddish = "yi" + case yiddishWorld = "yi_001" + case yoruba = "yo" + case yorubaBenin = "yo_BJ" + case yorubaNigeria = "yo_NG" + case zarma = "dje" + case zarmaNiger = "dje_NE" + case zulu = "zu" + case zuluSouthAfrica = "zu_ZA" + + /// Return a valid `Locale` instance from currency locale enum + public var locale: Locale { + switch self { + case .current: return Locale.current + case .autoUpdating: return Locale.autoupdatingCurrent + default: return Locale(identifier: rawValue) + } + } +} diff --git a/submodules/BotPaymentsUI/Sources/Formatter/NumberFormatter.swift b/submodules/BotPaymentsUI/Sources/Formatter/NumberFormatter.swift new file mode 100644 index 0000000000..0103b6989b --- /dev/null +++ b/submodules/BotPaymentsUI/Sources/Formatter/NumberFormatter.swift @@ -0,0 +1,18 @@ +// +// NumberFormatter.swift +// CurrencyText +// +// Created by Felipe Lefèvre Marino on 12/27/18. +// + +import Foundation + +public extension NumberFormatter { + + func string(from doubleValue: Double?) -> String? { + if let doubleValue = doubleValue { + return string(from: NSNumber(value: doubleValue)) + } + return nil + } +} diff --git a/submodules/BotPaymentsUI/Sources/Formatter/String.swift b/submodules/BotPaymentsUI/Sources/Formatter/String.swift new file mode 100644 index 0000000000..eabb906e25 --- /dev/null +++ b/submodules/BotPaymentsUI/Sources/Formatter/String.swift @@ -0,0 +1,69 @@ +// +// String.swift +// CurrencyText +// +// Created by Felipe Lefèvre Marino on 4/3/18. +// Copyright © 2018 Felipe Lefèvre Marino. All rights reserved. +// + +import Foundation + +public protocol CurrencyString { + var representsZero: Bool { get } + var hasNumbers: Bool { get } + var lastNumberOffsetFromEnd: Int? { get } + func numeralFormat() -> String + mutating func updateDecimalSeparator(decimalDigits: Int) +} + +//Currency String Extension +extension String: CurrencyString { + + // MARK: Properties + + /// Informs with the string represents the value of zero + public var representsZero: Bool { + return numeralFormat().replacingOccurrences(of: "0", with: "").count == 0 + } + + /// Returns if the string does have any character that represents numbers + public var hasNumbers: Bool { + return numeralFormat().count > 0 + } + + /// The offset from end index to the index _right after_ the last number in the String. + /// e.g. For the String "123some", the last number position is 4, because from the _end index_ to the index of _3_ + /// there is an offset of 4, "e, m, o and s". + public var lastNumberOffsetFromEnd: Int? { + guard let indexOfLastNumber = lastIndex(where: { $0.isNumber }) else { return nil } + let indexAfterLastNumber = index(after: indexOfLastNumber) + return distance(from: endIndex, to: indexAfterLastNumber) + } + + // MARK: Functions + + /// Updates a currency string decimal separator position based on + /// the amount of decimal digits desired + /// + /// - Parameter decimalDigits: The amount of decimal digits of the currency formatted string + public mutating func updateDecimalSeparator(decimalDigits: Int) { + guard decimalDigits != 0 && count >= decimalDigits else { return } + let decimalsRange = index(endIndex, offsetBy: -decimalDigits).. String { + return replacingOccurrences(of:"[^0-9]", with: "", options: .regularExpression) + } +} + +// MARK: - Static constants + +extension String { + public static let negativeSymbol = "-" +} diff --git a/submodules/BotPaymentsUI/Sources/TipEditController.swift b/submodules/BotPaymentsUI/Sources/TipEditController.swift new file mode 100644 index 0000000000..d785c6d17e --- /dev/null +++ b/submodules/BotPaymentsUI/Sources/TipEditController.swift @@ -0,0 +1,461 @@ +import Foundation +import UIKit +import SwiftSignalKit +import AsyncDisplayKit +import Display +import Postbox +import TelegramCore +import SyncCore +import TelegramPresentationData +import AccountContext + +private final class TipEditInputFieldNode: ASDisplayNode, ASEditableTextNodeDelegate { + private var theme: PresentationTheme + private let backgroundNode: ASImageNode + private let textInputNode: EditableTextNode + private let placeholderNode: ASTextNode + private let clearButton: HighlightableButtonNode + + var updateHeight: (() -> Void)? + var complete: (() -> Void)? + var textChanged: ((String) -> Void)? + + private let backgroundInsets = UIEdgeInsets(top: 8.0, left: 16.0, bottom: 15.0, right: 16.0) + private let inputInsets = UIEdgeInsets(top: 5.0, left: 12.0, bottom: 5.0, right: 12.0) + + var text: String { + get { + return self.textInputNode.attributedText?.string ?? "" + } + set { + self.textInputNode.attributedText = NSAttributedString(string: newValue, font: Font.regular(17.0), textColor: self.theme.actionSheet.inputTextColor) + self.placeholderNode.isHidden = !newValue.isEmpty + self.clearButton.isHidden = newValue.isEmpty + } + } + + var placeholder: String = "" { + didSet { + self.placeholderNode.attributedText = NSAttributedString(string: self.placeholder, font: Font.regular(17.0), textColor: self.theme.actionSheet.inputPlaceholderColor) + } + } + + init(theme: PresentationTheme, placeholder: String) { + self.theme = theme + + self.backgroundNode = ASImageNode() + self.backgroundNode.isLayerBacked = true + self.backgroundNode.displaysAsynchronously = false + self.backgroundNode.displayWithoutProcessing = true + self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 12.0, color: theme.actionSheet.inputHollowBackgroundColor, strokeColor: theme.actionSheet.inputBorderColor, strokeWidth: 1.0) + + self.textInputNode = EditableTextNode() + self.textInputNode.typingAttributes = [NSAttributedString.Key.font.rawValue: Font.regular(17.0), NSAttributedString.Key.foregroundColor.rawValue: theme.actionSheet.inputTextColor] + self.textInputNode.clipsToBounds = true + self.textInputNode.hitTestSlop = UIEdgeInsets(top: -5.0, left: -5.0, bottom: -5.0, right: -5.0) + self.textInputNode.textContainerInset = UIEdgeInsets(top: self.inputInsets.top, left: 0.0, bottom: self.inputInsets.bottom, right: 0.0) + self.textInputNode.keyboardAppearance = theme.rootController.keyboardColor.keyboardAppearance + self.textInputNode.keyboardType = .default + self.textInputNode.autocapitalizationType = .sentences + self.textInputNode.returnKeyType = .done + self.textInputNode.autocorrectionType = .default + self.textInputNode.tintColor = theme.actionSheet.controlAccentColor + + self.placeholderNode = ASTextNode() + self.placeholderNode.isUserInteractionEnabled = false + self.placeholderNode.displaysAsynchronously = false + self.placeholderNode.attributedText = NSAttributedString(string: placeholder, font: Font.regular(17.0), textColor: self.theme.actionSheet.inputPlaceholderColor) + + self.clearButton = HighlightableButtonNode() + self.clearButton.imageNode.displaysAsynchronously = false + self.clearButton.imageNode.displayWithoutProcessing = true + self.clearButton.displaysAsynchronously = false + self.clearButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Components/Search Bar/Clear"), color: theme.actionSheet.inputClearButtonColor), for: []) + self.clearButton.isHidden = true + + super.init() + + self.textInputNode.delegate = self + + self.addSubnode(self.backgroundNode) + self.addSubnode(self.textInputNode) + self.addSubnode(self.placeholderNode) + self.addSubnode(self.clearButton) + + self.clearButton.addTarget(self, action: #selector(self.clearPressed), forControlEvents: .touchUpInside) + } + + func updateTheme(_ theme: PresentationTheme) { + self.theme = theme + + self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 12.0, color: self.theme.actionSheet.inputHollowBackgroundColor, strokeColor: self.theme.actionSheet.inputBorderColor, strokeWidth: 1.0) + self.textInputNode.keyboardAppearance = self.theme.rootController.keyboardColor.keyboardAppearance + self.placeholderNode.attributedText = NSAttributedString(string: self.placeholderNode.attributedText?.string ?? "", font: Font.regular(17.0), textColor: self.theme.actionSheet.inputPlaceholderColor) + self.textInputNode.tintColor = self.theme.actionSheet.controlAccentColor + self.clearButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Components/Search Bar/Clear"), color: theme.actionSheet.inputClearButtonColor), for: []) + } + + func updateLayout(width: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat { + let backgroundInsets = self.backgroundInsets + let inputInsets = self.inputInsets + + let textFieldHeight = self.calculateTextFieldMetrics(width: width) + let panelHeight = textFieldHeight + backgroundInsets.top + backgroundInsets.bottom + + let backgroundFrame = CGRect(origin: CGPoint(x: backgroundInsets.left, y: backgroundInsets.top), size: CGSize(width: width - backgroundInsets.left - backgroundInsets.right, height: panelHeight - backgroundInsets.top - backgroundInsets.bottom)) + transition.updateFrame(node: self.backgroundNode, frame: backgroundFrame) + + let placeholderSize = self.placeholderNode.measure(backgroundFrame.size) + transition.updateFrame(node: self.placeholderNode, frame: CGRect(origin: CGPoint(x: backgroundFrame.minX + inputInsets.left, y: backgroundFrame.minY + floor((backgroundFrame.size.height - placeholderSize.height) / 2.0)), size: placeholderSize)) + + transition.updateFrame(node: self.textInputNode, frame: CGRect(origin: CGPoint(x: backgroundFrame.minX + inputInsets.left, y: backgroundFrame.minY), size: CGSize(width: backgroundFrame.size.width - inputInsets.left - inputInsets.right - 20.0, height: backgroundFrame.size.height))) + + if let image = self.clearButton.image(for: []) { + transition.updateFrame(node: self.clearButton, frame: CGRect(origin: CGPoint(x: backgroundFrame.maxX - 8.0 - image.size.width, y: backgroundFrame.minY + floor((backgroundFrame.size.height - image.size.height) / 2.0)), size: image.size)) + } + + return panelHeight + } + + func activateInput() { + self.textInputNode.becomeFirstResponder() + } + + func deactivateInput() { + self.textInputNode.resignFirstResponder() + } + + @objc func editableTextNodeDidUpdateText(_ editableTextNode: ASEditableTextNode) { + self.updateTextNodeText(animated: true) + self.textChanged?(editableTextNode.textView.text) + self.placeholderNode.isHidden = !(editableTextNode.textView.text ?? "").isEmpty + self.clearButton.isHidden = !self.placeholderNode.isHidden + } + + func editableTextNode(_ editableTextNode: ASEditableTextNode, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool { + let updatedText = (editableTextNode.textView.text as NSString).replacingCharacters(in: range, with: text) + if updatedText.count > 40 { + self.textInputNode.layer.addShakeAnimation() + return false + } + if text == "\n" { + self.complete?() + return false + } + return true + } + + private func calculateTextFieldMetrics(width: CGFloat) -> CGFloat { + let backgroundInsets = self.backgroundInsets + let inputInsets = self.inputInsets + + let unboundTextFieldHeight = max(33.0, ceil(self.textInputNode.measure(CGSize(width: width - backgroundInsets.left - backgroundInsets.right - inputInsets.left - inputInsets.right - 20.0, height: CGFloat.greatestFiniteMagnitude)).height)) + + return min(61.0, max(33.0, unboundTextFieldHeight)) + } + + private func updateTextNodeText(animated: Bool) { + let backgroundInsets = self.backgroundInsets + + let textFieldHeight = self.calculateTextFieldMetrics(width: self.bounds.size.width) + + let panelHeight = textFieldHeight + backgroundInsets.top + backgroundInsets.bottom + if !self.bounds.size.height.isEqual(to: panelHeight) { + self.updateHeight?() + } + } + + @objc func clearPressed() { + self.placeholderNode.isHidden = false + self.clearButton.isHidden = true + + self.textInputNode.attributedText = nil + self.deactivateInput() + self.updateHeight?() + } +} + +private final class TipEditAlertContentNode: AlertContentNode { + private let strings: PresentationStrings + private let title: String + private let text: String + + private let titleNode: ASTextNode + private let textNode: ASTextNode + let inputFieldNode: TipEditInputFieldNode + + private let actionNodesSeparator: ASDisplayNode + private let actionNodes: [TextAlertContentActionNode] + private let actionVerticalSeparators: [ASDisplayNode] + + private let disposable = MetaDisposable() + + private var validLayout: CGSize? + + private let hapticFeedback = HapticFeedback() + + var complete: (() -> Void)? { + didSet { + self.inputFieldNode.complete = self.complete + } + } + + override var dismissOnOutsideTap: Bool { + return self.isUserInteractionEnabled + } + + init(theme: AlertControllerTheme, ptheme: PresentationTheme, strings: PresentationStrings, actions: [TextAlertAction], title: String, text: String, placeholder: String, value: String?) { + self.strings = strings + self.title = title + self.text = text + + self.titleNode = ASTextNode() + self.titleNode.maximumNumberOfLines = 2 + self.textNode = ASTextNode() + self.textNode.maximumNumberOfLines = 8 + + self.inputFieldNode = TipEditInputFieldNode(theme: ptheme, placeholder: placeholder) + self.inputFieldNode.text = value ?? "" + + 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.titleNode) + self.addSubnode(self.textNode) + + self.addSubnode(self.inputFieldNode) + + self.addSubnode(self.actionNodesSeparator) + + for actionNode in self.actionNodes { + self.addSubnode(actionNode) + } + + for separatorNode in self.actionVerticalSeparators { + self.addSubnode(separatorNode) + } + + self.inputFieldNode.updateHeight = { [weak self] in + if let strongSelf = self { + if let _ = strongSelf.validLayout { + strongSelf.requestLayout?(.animated(duration: 0.15, curve: .spring)) + } + } + } + + self.updateTheme(theme) + } + + deinit { + self.disposable.dispose() + } + + var value: String { + return self.inputFieldNode.text + } + + override func updateTheme(_ theme: AlertControllerTheme) { + self.titleNode.attributedText = NSAttributedString(string: self.title, font: Font.bold(17.0), textColor: theme.primaryColor, paragraphAlignment: .center) + self.textNode.attributedText = NSAttributedString(string: self.text, 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 spacing: CGFloat = 5.0 + + let titleSize = self.titleNode.measure(measureSize) + transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - titleSize.width) / 2.0), y: origin.y), size: titleSize)) + origin.y += titleSize.height + 4.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 + 6.0 + spacing + + 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.updateLayout(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: 9.0, right: 18.0) + + var contentWidth = max(titleSize.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 inputFieldWidth = resultWidth + let inputFieldHeight = self.inputFieldNode.updateLayout(width: inputFieldWidth, transition: transition) + let inputHeight = inputFieldHeight + transition.updateFrame(node: self.inputFieldNode, frame: CGRect(x: 0.0, y: origin.y, width: resultWidth, height: inputFieldHeight)) + transition.updateAlpha(node: self.inputFieldNode, alpha: inputHeight > 0.0 ? 1.0 : 0.0) + + let resultSize = CGSize(width: resultWidth, height: titleSize.height + textSize.height + spacing + inputHeight + actionsHeight + 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 { + self.inputFieldNode.activateInput() + } + + return resultSize + } + + func animateError() { + self.inputFieldNode.layer.addShakeAnimation() + self.hapticFeedback.error() + } +} + +func tipEditController(sharedContext: SharedAccountContext, account: Account, forceTheme: PresentationTheme?, title: String, text: String, placeholder: String, doneButtonTitle: String? = nil, value: String?, apply: @escaping (String?) -> Void) -> AlertController { + var presentationData = sharedContext.currentPresentationData.with { $0 } + if let forceTheme = forceTheme { + presentationData = presentationData.withUpdated(theme: forceTheme) + } + + var dismissImpl: ((Bool) -> Void)? + var applyImpl: (() -> Void)? + + let actions: [TextAlertAction] = [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: { + dismissImpl?(true) + }), TextAlertAction(type: .defaultAction, title: doneButtonTitle ?? presentationData.strings.Common_Done, action: { + applyImpl?() + })] + + let contentNode = TipEditAlertContentNode(theme: AlertControllerTheme(presentationData: presentationData), ptheme: presentationData.theme, strings: presentationData.strings, actions: actions, title: title, text: text, placeholder: placeholder, value: value) + contentNode.complete = { + applyImpl?() + } + applyImpl = { [weak contentNode] in + guard let contentNode = contentNode else { + return + } + dismissImpl?(true) + + let previousValue = value ?? "" + let newValue = contentNode.value.trimmingCharacters(in: .whitespacesAndNewlines) + apply(previousValue != newValue || value == nil ? newValue : nil) + } + + let controller = AlertController(theme: AlertControllerTheme(presentationData: presentationData), contentNode: contentNode) + let presentationDataDisposable = sharedContext.presentationData.start(next: { [weak controller, weak contentNode] presentationData in + var presentationData = presentationData + if let forceTheme = forceTheme { + presentationData = presentationData.withUpdated(theme: forceTheme) + } + controller?.theme = AlertControllerTheme(presentationData: presentationData) + contentNode?.inputFieldNode.updateTheme(presentationData.theme) + }) + controller.dismissed = { + presentationDataDisposable.dispose() + } + dismissImpl = { [weak controller] animated in + contentNode.inputFieldNode.deactivateInput() + if animated { + controller?.dismissAnimated() + } else { + controller?.dismiss() + } + } + return controller +} diff --git a/submodules/BotPaymentsUI/Sources/UITextFieldDelegate/CurrencyUITextFieldDelegate.swift b/submodules/BotPaymentsUI/Sources/UITextFieldDelegate/CurrencyUITextFieldDelegate.swift new file mode 100644 index 0000000000..5eeb602a6e --- /dev/null +++ b/submodules/BotPaymentsUI/Sources/UITextFieldDelegate/CurrencyUITextFieldDelegate.swift @@ -0,0 +1,188 @@ +// +// CurrencyUITextFieldDelegate.swift +// CurrencyText +// +// Created by Felipe Lefèvre Marino on 12/26/18. +// Copyright © 2018 Felipe Lefèvre Marino. All rights reserved. +// + +import UIKit + +/// Custom text field delegate, that formats user inputs based on a given currency formatter. +public class CurrencyUITextFieldDelegate: NSObject { + + public var formatter: (CurrencyFormatting & CurrencyAdjusting)! + + public var textUpdated: (() -> Void)? + + /// Text field clears its text when value value is equal to zero. + public var clearsWhenValueIsZero: Bool = false + + /// A delegate object to receive and potentially handle `UITextFieldDelegate events` that are sent to `CurrencyUITextFieldDelegate`. + /// + /// Note: Make sure the implementation of this object does not wrongly interfere with currency formatting. + /// + /// By returning `false` on`textField(textField:shouldChangeCharactersIn:replacementString:)` no currency formatting is done. + public var passthroughDelegate: UITextFieldDelegate? { + get { return _passthroughDelegate } + set { + guard newValue !== self else { return } + _passthroughDelegate = newValue + } + } + weak private(set) var _passthroughDelegate: UITextFieldDelegate? + + public init(formatter: CurrencyFormatter) { + self.formatter = formatter + } +} + +// MARK: - UITextFieldDelegate + +extension CurrencyUITextFieldDelegate: UITextFieldDelegate { + + @discardableResult + open func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool { + return passthroughDelegate?.textFieldShouldBeginEditing?(textField) ?? true + } + + public func textFieldDidBeginEditing(_ textField: UITextField) { + textField.setInitialSelectedTextRange() + passthroughDelegate?.textFieldDidBeginEditing?(textField) + } + + @discardableResult + public func textFieldShouldEndEditing(_ textField: UITextField) -> Bool { + if let text = textField.text, text.representsZero && clearsWhenValueIsZero { + textField.text = "" + } + else if let text = textField.text, let updated = formatter.formattedStringAdjustedToFitAllowedValues(from: text), updated != text { + textField.text = updated + } + return passthroughDelegate?.textFieldShouldEndEditing?(textField) ?? true + } + + open func textFieldDidEndEditing(_ textField: UITextField) { + passthroughDelegate?.textFieldDidEndEditing?(textField) + } + + @discardableResult + open func textFieldShouldClear(_ textField: UITextField) -> Bool { + return passthroughDelegate?.textFieldShouldClear?(textField) ?? true + } + + @discardableResult + open func textFieldShouldReturn(_ textField: UITextField) -> Bool { + return passthroughDelegate?.textFieldShouldReturn?(textField) ?? true + } + + @discardableResult + public func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { + let shouldChangeCharactersInRange = passthroughDelegate?.textField?(textField, + shouldChangeCharactersIn: range, + replacementString: string) ?? true + guard shouldChangeCharactersInRange else { + return false + } + + // Store selected text range offset from end, before updating and reformatting the currency string. + let lastSelectedTextRangeOffsetFromEnd = textField.selectedTextRangeOffsetFromEnd + + // Before leaving the scope, update selected text range, + // respecting previous selected text range offset from end. + defer { + textField.updateSelectedTextRange(lastOffsetFromEnd: lastSelectedTextRangeOffsetFromEnd) + textUpdated?() + } + + guard !string.isEmpty else { + handleDeletion(in: textField, at: range) + return false + } + guard string.hasNumbers else { + addNegativeSymbolIfNeeded(in: textField, at: range, replacementString: string) + return false + } + + setFormattedText(in: textField, inputString: string, range: range) + + return false + } + + public func textFieldDidChangeSelection(_ textField: UITextField) { + if #available(iOSApplicationExtension 13.0, iOS 13.0, *) { + passthroughDelegate?.textFieldDidChangeSelection?(textField) + } + } +} + +// MARK: - Private + +extension CurrencyUITextFieldDelegate { + + /// Verifies if user inputed a negative symbol at the first lowest + /// bound of the text field and add it. + /// + /// - Parameters: + /// - textField: text field that user interacted with + /// - range: user input range + /// - string: user input string + private func addNegativeSymbolIfNeeded(in textField: UITextField, at range: NSRange, replacementString string: String) { + guard textField.keyboardType == .numbersAndPunctuation else { return } + + if string == .negativeSymbol && textField.text?.isEmpty == true { + textField.text = .negativeSymbol + } else if range.lowerBound == 0 && string == .negativeSymbol && + textField.text?.contains(String.negativeSymbol) == false { + + textField.text = .negativeSymbol + (textField.text ?? "") + } + } + + /// Correctly delete characters when user taps remove key. + /// + /// - Parameters: + /// - textField: text field that user interacted with + /// - range: range to be removed + private func handleDeletion(in textField: UITextField, at range: NSRange) { + if var text = textField.text { + if let textRange = Range(range, in: text) { + text.removeSubrange(textRange) + } else { + text.removeLast() + } + + if text.isEmpty { + textField.text = text + } else { + textField.text = formatter.formattedStringWithAdjustedDecimalSeparator(from: text) + } + } + } + + /// Formats text field's text with new input string and changed range + /// + /// - Parameters: + /// - textField: text field that user interacted with + /// - inputString: typed string + /// - range: range where the string should be added + private func setFormattedText(in textField: UITextField, inputString: String, range: NSRange) { + var updatedText = "" + + if let text = textField.text { + if text.isEmpty { + updatedText = formatter.initialText + inputString + } else if let range = Range(range, in: text) { + updatedText = text.replacingCharacters(in: range, with: inputString) + } else { + updatedText = text.appending(inputString) + } + } + + if updatedText.numeralFormat().count > formatter.maxDigitsCount { + updatedText.removeLast() + } + + textField.text = formatter.formattedStringWithAdjustedDecimalSeparator(from: updatedText) + } +} diff --git a/submodules/BotPaymentsUI/Sources/UITextFieldDelegate/UITextField.swift b/submodules/BotPaymentsUI/Sources/UITextFieldDelegate/UITextField.swift new file mode 100644 index 0000000000..cabd313e2f --- /dev/null +++ b/submodules/BotPaymentsUI/Sources/UITextFieldDelegate/UITextField.swift @@ -0,0 +1,61 @@ +// +// UITextField.swift +// CurrencyText +// +// Created by Felipe Lefèvre Marino on 12/26/18. +// + +import UIKit + +public extension UITextField { + + // MARK: Public + + var selectedTextRangeOffsetFromEnd: Int { + return offset(from: endOfDocument, to: selectedTextRange?.end ?? endOfDocument) + } + + /// Sets the selected text range when the text field is starting to be edited. + /// _Should_ be called when text field start to be the first responder. + func setInitialSelectedTextRange() { + // update selected text range if needed + adjustSelectedTextRange(lastOffsetFromEnd: 0) // at the end when first selected + } + + /// Interface to update the selected text range as expected. + /// - Parameter lastOffsetFromEnd: The last stored selected text range offset from end. Used to keep it concise with pre-formatting. + func updateSelectedTextRange(lastOffsetFromEnd: Int) { + adjustSelectedTextRange(lastOffsetFromEnd: lastOffsetFromEnd) + } + + // MARK: Private + + /// Adjust the selected text range to match the best position. + private func adjustSelectedTextRange(lastOffsetFromEnd: Int) { + /// If text is empty the offset is set to zero, the selected text range does need to be changed. + if let text = text, text.isEmpty { + return + } + + var offsetFromEnd = lastOffsetFromEnd + + /// Adjust offset if needed. When the last number character offset from end is less than the current offset, + /// or in other words, is more distant to the end of the string, the offset is readjusted to it, + /// so the selected text range is correctly set to the last index with a number. + if let lastNumberOffsetFromEnd = text?.lastNumberOffsetFromEnd, + case let shouldOffsetBeAdjusted = lastNumberOffsetFromEnd < offsetFromEnd, + shouldOffsetBeAdjusted { + + offsetFromEnd = lastNumberOffsetFromEnd + } + + updateSelectedTextRange(offsetFromEnd: offsetFromEnd) + } + + /// Update the selected text range with given offset from end. + private func updateSelectedTextRange(offsetFromEnd: Int) { + if let updatedCursorPosition = position(from: endOfDocument, offset: offsetFromEnd) { + selectedTextRange = textRange(from: updatedCursorPosition, to: updatedCursorPosition) + } + } +} diff --git a/submodules/BroadcastUploadHelpers/BUILD b/submodules/BroadcastUploadHelpers/BUILD new file mode 100644 index 0000000000..276b7c9756 --- /dev/null +++ b/submodules/BroadcastUploadHelpers/BUILD @@ -0,0 +1,22 @@ + +objc_library( + name = "BroadcastUploadHelpers", + enable_modules = True, + module_name = "BroadcastUploadHelpers", + srcs = glob([ + "Sources/**/*.m", + "Sources/**/*.h", + ]), + hdrs = glob([ + "PublicHeaders/**/*.h", + ]), + includes = [ + "PublicHeaders", + ], + sdk_frameworks = [ + "Foundation", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/BroadcastUploadHelpers/PublicHeaders/BroadcastUploadHelpers/BroadcastUploadHelpers.h b/submodules/BroadcastUploadHelpers/PublicHeaders/BroadcastUploadHelpers/BroadcastUploadHelpers.h new file mode 100755 index 0000000000..a1b5fe7b6e --- /dev/null +++ b/submodules/BroadcastUploadHelpers/PublicHeaders/BroadcastUploadHelpers/BroadcastUploadHelpers.h @@ -0,0 +1,8 @@ +#ifndef BroadcastUploadHelpers_h +#define BroadcastUploadHelpers_h + +#import + +void finishBroadcastGracefully(RPBroadcastSampleHandler * _Nonnull broadcastSampleHandler); + +#endif /* BroadcastUploadHelpers_h */ diff --git a/submodules/BroadcastUploadHelpers/Sources/BroadcastUploadHelpers.m b/submodules/BroadcastUploadHelpers/Sources/BroadcastUploadHelpers.m new file mode 100755 index 0000000000..fbb5705b47 --- /dev/null +++ b/submodules/BroadcastUploadHelpers/Sources/BroadcastUploadHelpers.m @@ -0,0 +1,8 @@ +#import + +void finishBroadcastGracefully(RPBroadcastSampleHandler * _Nonnull broadcastSampleHandler) { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wnonnull" + [broadcastSampleHandler finishBroadcastWithError:nil]; +#pragma clang diagnostic pop +} diff --git a/submodules/CallListUI/BUILD b/submodules/CallListUI/BUILD index b10f67b243..65d5b40bb9 100644 --- a/submodules/CallListUI/BUILD +++ b/submodules/CallListUI/BUILD @@ -26,6 +26,8 @@ swift_library( "//submodules/ChatListSearchItemHeader:ChatListSearchItemHeader", "//submodules/PeerOnlineMarkerNode:PeerOnlineMarkerNode", "//submodules/ContextUI:ContextUI", + "//submodules/TelegramBaseController:TelegramBaseController", + "//submodules/AnimatedStickerNode:AnimatedStickerNode", ], visibility = [ "//visibility:public", diff --git a/submodules/CallListUI/Sources/CallListCallItem.swift b/submodules/CallListUI/Sources/CallListCallItem.swift index 1394f62608..72ad5c7f11 100644 --- a/submodules/CallListUI/Sources/CallListCallItem.swift +++ b/submodules/CallListUI/Sources/CallListCallItem.swift @@ -650,9 +650,9 @@ class CallListCallItemNode: ItemListRevealOptionsItemNode { self.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration * 0.3, removeOnCompletion: false) } - override public func header() -> ListViewItemHeader? { + override public func headers() -> [ListViewItemHeader]? { if let (item, _, _, _, _) = self.layoutParams { - return item.header + return item.header.flatMap { [$0] } } else { return nil } diff --git a/submodules/CallListUI/Sources/CallListController.swift b/submodules/CallListUI/Sources/CallListController.swift index fec07a9481..2f9faa7e85 100644 --- a/submodules/CallListUI/Sources/CallListController.swift +++ b/submodules/CallListUI/Sources/CallListController.swift @@ -14,6 +14,7 @@ import AlertUI import AppBundle import LocalizedPeerData import ContextUI +import TelegramBaseController public enum CallListControllerMode { case tab @@ -65,7 +66,7 @@ private final class DeleteAllButtonNode: ASDisplayNode { } } -public final class CallListController: ViewController { +public final class CallListController: TelegramBaseController { private var controllerNode: CallListControllerNode { return self.displayNode as! CallListControllerNode } @@ -98,7 +99,7 @@ public final class CallListController: ViewController { self.segmentedTitleView = ItemListControllerSegmentedTitleView(theme: self.presentationData.theme, segments: [self.presentationData.strings.Calls_All, self.presentationData.strings.Calls_Missed], selectedIndex: 0) - super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationData: self.presentationData)) + super.init(context: context, navigationBarPresentationData: NavigationBarPresentationData(presentationData: self.presentationData), mediaAccessoryPanelVisibility: .none, locationBroadcastPanelSource: .none, groupCallPanelSource: .none) self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style @@ -199,6 +200,10 @@ public final class CallListController: ViewController { if let strongSelf = self { strongSelf.call(peerId, isVideo: isVideo) } + }, joinGroupCall: { [weak self] peerId, activeCall in + if let strongSelf = self { + strongSelf.joinGroupCall(peerId: peerId, invite: nil, activeCall: activeCall) + } }, openInfo: { [weak self] peerId, messages in if let strongSelf = self { let _ = (strongSelf.context.account.postbox.loadedPeerWithId(peerId) @@ -226,12 +231,12 @@ public final class CallListController: ViewController { switch strongSelf.mode { case .tab: if strongSelf.editingMode { - strongSelf.navigationItem.leftBarButtonItem = UIBarButtonItem(title: strongSelf.presentationData.strings.Common_Done, style: .done, target: strongSelf, action: #selector(strongSelf.donePressed)) + strongSelf.navigationItem.setLeftBarButton(UIBarButtonItem(title: strongSelf.presentationData.strings.Common_Done, style: .done, target: strongSelf, action: #selector(strongSelf.donePressed)), animated: true) var pressedImpl: (() -> Void)? let buttonNode = DeleteAllButtonNode(presentationData: strongSelf.presentationData, pressed: { pressedImpl?() }) - strongSelf.navigationItem.rightBarButtonItem = UIBarButtonItem(customDisplayNode: buttonNode) + strongSelf.navigationItem.setRightBarButton(UIBarButtonItem(customDisplayNode: buttonNode), animated: true) strongSelf.navigationItem.rightBarButtonItem?.setCustomAction({ pressedImpl?() }) @@ -244,20 +249,23 @@ public final class CallListController: ViewController { //strongSelf.navigationItem.rightBarButtonItem = UIBarButtonItem(title: strongSelf.presentationData.strings.Notification_Exceptions_DeleteAll, style: .plain, target: strongSelf, action: #selector(strongSelf.deleteAllPressed)) } else { - strongSelf.navigationItem.leftBarButtonItem = UIBarButtonItem(title: strongSelf.presentationData.strings.Common_Edit, style: .plain, target: strongSelf, action: #selector(strongSelf.editPressed)) - strongSelf.navigationItem.rightBarButtonItem = UIBarButtonItem(image: PresentationResourcesRootController.navigationCallIcon(strongSelf.presentationData.theme), style: .plain, target: self, action: #selector(strongSelf.callPressed)) + strongSelf.navigationItem.setLeftBarButton(UIBarButtonItem(title: strongSelf.presentationData.strings.Common_Edit, style: .plain, target: strongSelf, action: #selector(strongSelf.editPressed)), animated: true) + strongSelf.navigationItem.setRightBarButton(UIBarButtonItem(image: PresentationResourcesRootController.navigationCallIcon(strongSelf.presentationData.theme), style: .plain, target: self, action: #selector(strongSelf.callPressed)), animated: true) } case .navigation: if strongSelf.editingMode { - strongSelf.navigationItem.rightBarButtonItem = UIBarButtonItem(title: strongSelf.presentationData.strings.Common_Done, style: .done, target: strongSelf, action: #selector(strongSelf.donePressed)) + strongSelf.navigationItem.setRightBarButton(UIBarButtonItem(title: strongSelf.presentationData.strings.Common_Done, style: .done, target: strongSelf, action: #selector(strongSelf.donePressed)), animated: true) } else { - strongSelf.navigationItem.rightBarButtonItem = UIBarButtonItem(title: strongSelf.presentationData.strings.Common_Edit, style: .plain, target: strongSelf, action: #selector(strongSelf.editPressed)) + strongSelf.navigationItem.setRightBarButton(UIBarButtonItem(title: strongSelf.presentationData.strings.Common_Edit, style: .plain, target: strongSelf, action: #selector(strongSelf.editPressed)), animated: true) } } } } } }) + self.controllerNode.startNewCall = { [weak self] in + self?.beginCallImpl() + } self._ready.set(self.controllerNode.ready) self.displayNodeDidLoad() } @@ -265,7 +273,7 @@ public final class CallListController: 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.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition) } @objc func callPressed() { @@ -280,7 +288,7 @@ public final class CallListController: ViewController { return } - var signal = clearCallHistory(account: strongSelf.context.account, forEveryone: forEveryone) + var signal = strongSelf.context.engine.messages.clearCallHistory(forEveryone: forEveryone) var cancelImpl: (() -> Void)? let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } @@ -360,9 +368,9 @@ public final class CallListController: ViewController { controller.navigationPresentation = .modal self.createActionDisposable.set((controller.result |> take(1) - |> deliverOnMainQueue).start(next: { [weak controller, weak self] peer in + |> deliverOnMainQueue).start(next: { [weak controller, weak self] result in controller?.dismissSearch() - if let strongSelf = self, let (contactPeer, action) = peer, case let .peer(peer, _, _) = contactPeer { + if let strongSelf = self, let (contactPeers, action) = result, let contactPeer = contactPeers.first, case let .peer(peer, _, _) = contactPeer { strongSelf.call(peer.id, isVideo: action == .videoCall, began: { if let strongSelf = self { let _ = (strongSelf.context.sharedContext.hasOngoingCall.get() @@ -389,12 +397,12 @@ public final class CallListController: ViewController { switch self.mode { case .tab: - self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Done, style: .done, target: self, action: #selector(self.donePressed)) + self.navigationItem.setLeftBarButton(UIBarButtonItem(title: self.presentationData.strings.Common_Done, style: .done, target: self, action: #selector(self.donePressed)), animated: true) var pressedImpl: (() -> Void)? let buttonNode = DeleteAllButtonNode(presentationData: self.presentationData, pressed: { pressedImpl?() }) - self.navigationItem.rightBarButtonItem = UIBarButtonItem(customDisplayNode: buttonNode) + self.navigationItem.setRightBarButton(UIBarButtonItem(customDisplayNode: buttonNode), animated: true) self.navigationItem.rightBarButtonItem?.setCustomAction({ pressedImpl?() }) @@ -406,7 +414,7 @@ public final class CallListController: ViewController { } //self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Notification_Exceptions_DeleteAll, style: .plain, target: self, action: #selector(self.deleteAllPressed)) case .navigation: - self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Done, style: .done, target: self, action: #selector(self.donePressed)) + self.navigationItem.setRightBarButton(UIBarButtonItem(title: self.presentationData.strings.Common_Done, style: .done, target: self, action: #selector(self.donePressed)), animated: true) } self.controllerNode.updateState { state in @@ -418,10 +426,10 @@ public final class CallListController: ViewController { self.editingMode = false switch self.mode { case .tab: - self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Edit, style: .plain, target: self, action: #selector(self.editPressed)) - self.navigationItem.rightBarButtonItem = UIBarButtonItem(image: PresentationResourcesRootController.navigationCallIcon(self.presentationData.theme), style: .plain, target: self, action: #selector(self.callPressed)) + self.navigationItem.setLeftBarButton(UIBarButtonItem(title: self.presentationData.strings.Common_Edit, style: .plain, target: self, action: #selector(self.editPressed)), animated: true) + self.navigationItem.setRightBarButton(UIBarButtonItem(image: PresentationResourcesRootController.navigationCallIcon(self.presentationData.theme), style: .plain, target: self, action: #selector(self.callPressed)), animated: true) case .navigation: - self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Edit, style: .plain, target: self, action: #selector(self.editPressed)) + self.navigationItem.setRightBarButton(UIBarButtonItem(title: self.presentationData.strings.Common_Edit, style: .plain, target: self, action: #selector(self.editPressed)), animated: true) } self.controllerNode.updateState { state in diff --git a/submodules/CallListUI/Sources/CallListControllerNode.swift b/submodules/CallListUI/Sources/CallListControllerNode.swift index 6b93e23a68..88354bfed8 100644 --- a/submodules/CallListUI/Sources/CallListControllerNode.swift +++ b/submodules/CallListUI/Sources/CallListControllerNode.swift @@ -13,6 +13,8 @@ import PresentationDataUtils import AccountContext import TelegramNotices import ChatListSearchItemHeader +import AnimatedStickerNode +import AppBundle private struct CallListNodeListViewTransition { let callListView: CallListNodeView @@ -179,6 +181,7 @@ final class CallListControllerNode: ASDisplayNode { var peerSelected: ((PeerId) -> Void)? var activateSearch: (() -> Void)? var deletePeerChat: ((PeerId) -> Void)? + var startNewCall: (() -> Void)? private let viewProcessingQueue = Queue() private var callListView: CallListNodeView? @@ -196,9 +199,15 @@ final class CallListControllerNode: ASDisplayNode { private let listNode: ListView private let leftOverlayNode: ASDisplayNode private let rightOverlayNode: ASDisplayNode - private let emptyTextNode: ASTextNode + private let emptyTextNode: ImmediateTextNode + private let emptyAnimationNode: AnimatedStickerNode + private var emptyAnimationSize = CGSize() + private let emptyButtonNode: HighlightTrackingButtonNode + private let emptyButtonIconNode: ASImageNode + private let emptyButtonTextNode: ImmediateTextNode private let call: (PeerId, Bool) -> Void + private let joinGroupCall: (PeerId, CachedChannelData.ActiveCall) -> Void private let openInfo: (PeerId, [Message]) -> Void private let emptyStateUpdated: (Bool) -> Void @@ -207,16 +216,17 @@ final class CallListControllerNode: ASDisplayNode { private let openGroupCallDisposable = MetaDisposable() - init(controller: CallListController, context: AccountContext, mode: CallListControllerMode, presentationData: PresentationData, call: @escaping (PeerId, Bool) -> Void, openInfo: @escaping (PeerId, [Message]) -> Void, emptyStateUpdated: @escaping (Bool) -> Void) { + init(controller: CallListController, context: AccountContext, mode: CallListControllerMode, presentationData: PresentationData, call: @escaping (PeerId, Bool) -> Void, joinGroupCall: @escaping (PeerId, CachedChannelData.ActiveCall) -> Void, openInfo: @escaping (PeerId, [Message]) -> Void, emptyStateUpdated: @escaping (Bool) -> Void) { self.controller = controller self.context = context self.mode = mode self.presentationData = presentationData self.call = call + self.joinGroupCall = joinGroupCall self.openInfo = openInfo self.emptyStateUpdated = emptyStateUpdated - self.currentState = CallListNodeState(presentationData: ItemListPresentationData(presentationData), dateTimeFormat: presentationData.dateTimeFormat, disableAnimations: presentationData.disableAnimations, editing: false, messageIdWithRevealedOptions: nil) + self.currentState = CallListNodeState(presentationData: ItemListPresentationData(presentationData), dateTimeFormat: presentationData.dateTimeFormat, disableAnimations: true, editing: false, messageIdWithRevealedOptions: nil) self.statePromise = ValuePromise(self.currentState, ignoreRepeated: true) self.listNode = ListView() @@ -230,10 +240,26 @@ final class CallListControllerNode: ASDisplayNode { self.rightOverlayNode = ASDisplayNode() self.rightOverlayNode.backgroundColor = self.presentationData.theme.list.blocksBackgroundColor - self.emptyTextNode = ASTextNode() + self.emptyTextNode = ImmediateTextNode() self.emptyTextNode.alpha = 0.0 self.emptyTextNode.isUserInteractionEnabled = false self.emptyTextNode.displaysAsynchronously = false + self.emptyTextNode.textAlignment = .center + self.emptyTextNode.maximumNumberOfLines = 3 + + self.emptyAnimationNode = AnimatedStickerNode() + self.emptyAnimationNode.alpha = 0.0 + self.emptyAnimationNode.isUserInteractionEnabled = false + + self.emptyButtonNode = HighlightTrackingButtonNode() + self.emptyButtonNode.isUserInteractionEnabled = false + + self.emptyButtonTextNode = ImmediateTextNode() + self.emptyButtonTextNode.isUserInteractionEnabled = false + + self.emptyButtonIconNode = ASImageNode() + self.emptyButtonIconNode.displaysAsynchronously = false + self.emptyButtonIconNode.isUserInteractionEnabled = false super.init() @@ -243,6 +269,10 @@ final class CallListControllerNode: ASDisplayNode { self.addSubnode(self.listNode) self.addSubnode(self.emptyTextNode) + self.addSubnode(self.emptyAnimationNode) + self.addSubnode(self.emptyButtonTextNode) + self.addSubnode(self.emptyButtonIconNode) + self.addSubnode(self.emptyButtonNode) switch self.mode { case .tab: @@ -253,6 +283,31 @@ final class CallListControllerNode: ASDisplayNode { self.listNode.backgroundColor = presentationData.theme.list.blocksBackgroundColor } + + if let path = getAppBundle().path(forResource: "CallsPlaceholder", ofType: "tgs") { + self.emptyAnimationNode.setup(source: AnimatedStickerNodeLocalFileSource(path: path), width: 256, height: 256, playbackMode: .loop, mode: .direct(cachePathPrefix: nil)) + self.emptyAnimationSize = CGSize(width: 148.0, height: 148.0) + } + + self.emptyButtonIconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Call List/CallIcon"), color: presentationData.theme.list.itemAccentColor) + + self.emptyButtonNode.highligthedChanged = { [weak self] highlighted in + if let strongSelf = self { + if highlighted { + strongSelf.emptyButtonIconNode.layer.removeAnimation(forKey: "opacity") + strongSelf.emptyButtonIconNode.alpha = 0.4 + strongSelf.emptyButtonTextNode.layer.removeAnimation(forKey: "opacity") + strongSelf.emptyButtonTextNode.alpha = 0.4 + } else { + strongSelf.emptyButtonIconNode.alpha = 1.0 + strongSelf.emptyButtonIconNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) + strongSelf.emptyButtonTextNode.alpha = 1.0 + strongSelf.emptyButtonTextNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) + } + } + } + self.emptyButtonNode.addTarget(self, action: #selector(self.emptyButtonPressed), forControlEvents: .touchUpInside) + let nodeInteraction = CallListNodeInteraction(setMessageIdWithRevealedOptions: { [weak self] messageId, fromMessageId in if let strongSelf = self { strongSelf.updateState { state in @@ -288,7 +343,7 @@ final class CallListControllerNode: ASDisplayNode { guard let strongSelf = self else { return } - let _ = deleteMessagesInteractively(account: strongSelf.context.account, messageIds: messageIds, type: .forEveryone).start() + let _ = strongSelf.context.engine.messages.deleteMessagesInteractively(messageIds: messageIds, type: .forEveryone).start() })) items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_DeleteMessagesForMe, color: .destructive, action: { [weak actionSheet] in @@ -298,7 +353,7 @@ final class CallListControllerNode: ASDisplayNode { return } - let _ = deleteMessagesInteractively(account: strongSelf.context.account, messageIds: messageIds, type: .forLocalPeer).start() + let _ = strongSelf.context.engine.messages.deleteMessagesInteractively(messageIds: messageIds, type: .forLocalPeer).start() })) actionSheet.setItemGroups([ @@ -329,14 +384,21 @@ final class CallListControllerNode: ASDisplayNode { let disposable = strongSelf.openGroupCallDisposable let account = strongSelf.context.account + let engine = strongSelf.context.engine var signal: Signal = strongSelf.context.account.postbox.transaction { transaction -> CachedChannelData.ActiveCall? in - return (transaction.getPeerCachedData(peerId: peerId) as? CachedChannelData)?.activeCall + let cachedData = transaction.getPeerCachedData(peerId: peerId) + if let cachedData = cachedData as? CachedChannelData { + return cachedData.activeCall + } else if let cachedData = cachedData as? CachedGroupData { + return cachedData.activeCall + } + return nil } |> mapToSignal { activeCall -> Signal in if let activeCall = activeCall { return .single(activeCall) } else { - return updatedCurrentPeerGroupCall(account: account, peerId: peerId) + return engine.calls.updatedCurrentPeerGroupCall(peerId: peerId) } } @@ -375,7 +437,7 @@ final class CallListControllerNode: ASDisplayNode { } if let activeCall = activeCall { - strongSelf.context.joinGroupCall(peerId: peerId, invite: nil, requestJoinAsPeerId: nil, activeCall: activeCall) + strongSelf.joinGroupCall(peerId, activeCall) } })) }) @@ -560,7 +622,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.presentationData.theme, strings: state.presentationData.strings, type: type, hidden: !isEmpty) + strongSelf.updateEmptyPlaceholder(theme: state.presentationData.theme, strings: state.presentationData.strings, type: type, isHidden: !isEmpty) } })) } @@ -572,7 +634,7 @@ final class CallListControllerNode: ASDisplayNode { } func updateThemeAndStrings(presentationData: PresentationData) { - if presentationData.theme !== self.currentState.presentationData.theme || presentationData.strings !== self.currentState.presentationData.strings || presentationData.disableAnimations != self.currentState.disableAnimations { + if presentationData.theme !== self.currentState.presentationData.theme || presentationData.strings !== self.currentState.presentationData.strings { self.presentationData = presentationData self.leftOverlayNode.backgroundColor = presentationData.theme.list.blocksBackgroundColor @@ -586,10 +648,12 @@ final class CallListControllerNode: ASDisplayNode { self.listNode.backgroundColor = presentationData.theme.list.blocksBackgroundColor } - self.updateEmptyPlaceholder(theme: presentationData.theme, strings: presentationData.strings, type: self.currentLocationAndType.type, hidden: self.emptyTextNode.isHidden) + self.emptyButtonIconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Call List/CallIcon"), color: presentationData.theme.list.itemAccentColor) + + self.updateEmptyPlaceholder(theme: presentationData.theme, strings: presentationData.strings, type: self.currentLocationAndType.type, isHidden: self.emptyTextNode.alpha.isZero) self.updateState { - return $0.withUpdatedPresentationData(presentationData: ItemListPresentationData(presentationData), dateTimeFormat: presentationData.dateTimeFormat, disableAnimations: presentationData.disableAnimations) + return $0.withUpdatedPresentationData(presentationData: ItemListPresentationData(presentationData), dateTimeFormat: presentationData.dateTimeFormat, disableAnimations: true) } self.listNode.forEachItemHeaderNode({ itemHeaderNode in @@ -601,20 +665,40 @@ final class CallListControllerNode: ASDisplayNode { } private let textFont = Font.regular(16.0) + private let buttonFont = Font.regular(17.0) - func updateEmptyPlaceholder(theme: PresentationTheme, strings: PresentationStrings, type: CallListViewType, hidden: Bool) { - let alpha: CGFloat = hidden ? 0.0 : 1.0 + func updateEmptyPlaceholder(theme: PresentationTheme, strings: PresentationStrings, type: CallListViewType, isHidden: Bool) { + let alpha: CGFloat = isHidden ? 0.0 : 1.0 let previousAlpha = self.emptyTextNode.alpha self.emptyTextNode.alpha = alpha self.emptyTextNode.layer.animateAlpha(from: previousAlpha, to: alpha, duration: 0.2) - if !hidden { + if previousAlpha.isZero && !alpha.isZero { + self.emptyAnimationNode.visibility = true + } + self.emptyAnimationNode.alpha = alpha + self.emptyAnimationNode.layer.animateAlpha(from: previousAlpha, to: alpha, duration: 0.2, completion: { [weak self] _ in + if let strongSelf = self { + if !previousAlpha.isZero && strongSelf.emptyAnimationNode.alpha.isZero { + strongSelf.emptyAnimationNode.visibility = false + } + } + }) + + self.emptyButtonIconNode.alpha = alpha + self.emptyButtonIconNode.layer.animateAlpha(from: previousAlpha, to: alpha, duration: 0.2) + self.emptyButtonTextNode.alpha = alpha + self.emptyButtonTextNode.layer.animateAlpha(from: previousAlpha, to: alpha, duration: 0.2) + self.emptyButtonNode.isUserInteractionEnabled = !isHidden + + if !isHidden { let type = self.currentLocationAndType.type - let string: String + let emptyText: String + let buttonText = strings.Calls_StartNewCall if type == .missed { - string = strings.Calls_NoMissedCallsPlacehoder + emptyText = strings.Calls_NoMissedCallsPlacehoder } else { - string = strings.Calls_NoCallsPlaceholder + emptyText = strings.Calls_NoVoiceAndVideoCallsPlaceholder } let color: UIColor @@ -629,7 +713,10 @@ final class CallListControllerNode: ASDisplayNode { color = theme.list.freeTextColor } - self.emptyTextNode.attributedText = NSAttributedString(string: string, font: textFont, textColor: color, paragraphAlignment: .center) + self.emptyTextNode.attributedText = NSAttributedString(string: emptyText, font: textFont, textColor: color, paragraphAlignment: .center) + + self.emptyButtonTextNode.attributedText = NSAttributedString(string: buttonText, font: buttonFont, textColor: theme.list.itemAccentColor, paragraphAlignment: .center) + if let layout = self.containerLayout { self.updateLayout(layout.0, navigationBarHeight: layout.1, transition: .immediate) } @@ -722,6 +809,10 @@ final class CallListControllerNode: ASDisplayNode { } } + @objc private func emptyButtonPressed() { + self.startNewCall?() + } + func updateLayout(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { var insets = layout.insets(options: [.input]) insets.top += max(navigationBarHeight, layout.insets(options: [.statusBar]).top) @@ -734,8 +825,30 @@ final class CallListControllerNode: ASDisplayNode { let size = layout.size let contentRect = CGRect(origin: CGPoint(x: 0.0, y: insets.top), size: CGSize(width: size.width, height: size.height - insets.top - insets.bottom)) - let textSize = self.emptyTextNode.measure(CGSize(width: size.width - 20.0, height: size.height)) - transition.updateFrame(node: self.emptyTextNode, frame: CGRect(origin: CGPoint(x: contentRect.minX + floor((contentRect.width - textSize.width) / 2.0), y: contentRect.minY + floor((contentRect.height - textSize.height) / 2.0)), size: textSize)) + let sideInset: CGFloat = 64.0 + + let emptyAnimationHeight = self.emptyAnimationSize.height + let emptyAnimationSpacing: CGFloat = 13.0 + let emptyTextSpacing: CGFloat = 23.0 + let emptyTextSize = self.emptyTextNode.updateLayout(CGSize(width: contentRect.width - sideInset * 2.0, height: size.height)) + let emptyButtonSize = self.emptyButtonTextNode.updateLayout(CGSize(width: contentRect.width - sideInset * 2.0, height: size.height)) + let emptyTotalHeight = emptyAnimationHeight + emptyAnimationSpacing + emptyTextSize.height + emptyTextSpacing + emptyButtonSize.height + let emptyAnimationY = contentRect.minY + floorToScreenPixels((contentRect.height - emptyTotalHeight) / 2.0) + + let textTransition = ContainedViewLayoutTransition.immediate + textTransition.updateFrame(node: self.emptyAnimationNode, frame: CGRect(origin: CGPoint(x: contentRect.minX + (contentRect.width - self.emptyAnimationSize.width) / 2.0, y: emptyAnimationY), size: self.emptyAnimationSize)) + textTransition.updateFrame(node: self.emptyTextNode, frame: CGRect(origin: CGPoint(x: contentRect.minX + (contentRect.width - emptyTextSize.width) / 2.0, y: emptyAnimationY + emptyAnimationHeight + emptyAnimationSpacing), size: emptyTextSize)) + + let emptyButtonSpacing: CGFloat = 14.0 + let emptyButtonIconSize = (self.emptyButtonIconNode.image?.size ?? CGSize()) + let emptyButtonWidth = emptyButtonIconSize.width + emptyButtonSpacing + emptyButtonSize.width + let emptyButtonX = floor(contentRect.width - emptyButtonWidth) / 2.0 + textTransition.updateFrame(node: self.emptyButtonIconNode, frame: CGRect(origin: CGPoint(x: emptyButtonX, y: emptyAnimationY + emptyAnimationHeight + emptyAnimationSpacing + emptyTextSize.height + emptyTextSpacing), size: emptyButtonIconSize)) + textTransition.updateFrame(node: self.emptyButtonTextNode, frame: CGRect(origin: CGPoint(x: emptyButtonX + emptyButtonIconSize.width + emptyButtonSpacing, y: emptyAnimationY + emptyAnimationHeight + emptyAnimationSpacing + emptyTextSize.height + emptyTextSpacing + 4.0), size: emptyButtonSize)) + + textTransition.updateFrame(node: self.emptyButtonNode, frame: CGRect(origin: CGPoint(x: emptyButtonX, y: emptyAnimationY + emptyAnimationHeight + emptyAnimationSpacing + emptyTextSize.height + emptyTextSpacing), size: CGSize(width: emptyButtonWidth, height: 44.0))) + + self.emptyAnimationNode.updateLayout(size: self.emptyAnimationSize) } func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { diff --git a/submodules/CallListUI/Sources/CallListGroupCallItem.swift b/submodules/CallListUI/Sources/CallListGroupCallItem.swift index 88c0d17abb..6d1af1b65d 100644 --- a/submodules/CallListUI/Sources/CallListGroupCallItem.swift +++ b/submodules/CallListUI/Sources/CallListGroupCallItem.swift @@ -467,9 +467,9 @@ class CallListGroupCallItemNode: ItemListRevealOptionsItemNode { self.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration * 0.3, removeOnCompletion: false) } - override public func header() -> ListViewItemHeader? { + override public func headers() -> [ListViewItemHeader]? { if let (item, _, _, _, _) = self.layoutParams { - return item.header + return item.header.flatMap { [$0] } } else { return nil } diff --git a/submodules/ChatImportUI/Sources/ChatImportActivityScreen.swift b/submodules/ChatImportUI/Sources/ChatImportActivityScreen.swift index 794d0c709d..5cefdf43e0 100644 --- a/submodules/ChatImportUI/Sources/ChatImportActivityScreen.swift +++ b/submodules/ChatImportUI/Sources/ChatImportActivityScreen.swift @@ -68,16 +68,16 @@ private final class ImportManager { private let account: Account private let archivePath: String? - private let entries: [(SSZipEntry, String, ChatHistoryImport.MediaType)] + private let entries: [(SSZipEntry, String, TelegramEngine.HistoryImport.MediaType)] - private var session: ChatHistoryImport.Session? + private var session: TelegramEngine.HistoryImport.Session? private let disposable = MetaDisposable() private let totalBytes: Int private let totalMediaBytes: Int private let mainFileSize: Int - private var pendingEntries: [(SSZipEntry, String, ChatHistoryImport.MediaType)] + private var pendingEntries: [(SSZipEntry, String, TelegramEngine.HistoryImport.MediaType)] private var entryProgress: [String: (Int, Int)] = [:] private var activeEntries: [String: Disposable] = [:] @@ -91,7 +91,7 @@ private final class ImportManager { return self.statePromise.get() } - init(account: Account, peerId: PeerId, mainFile: TempBoxFile, archivePath: String?, entries: [(SSZipEntry, String, ChatHistoryImport.MediaType)]) { + init(account: Account, peerId: PeerId, mainFile: TempBoxFile, archivePath: String?, entries: [(SSZipEntry, String, TelegramEngine.HistoryImport.MediaType)]) { self.account = account self.archivePath = archivePath self.entries = entries @@ -114,7 +114,7 @@ private final class ImportManager { Logger.shared.log("ChatImportScreen", " \(entry.1)") } - self.disposable.set((ChatHistoryImport.initSession(account: self.account, peerId: peerId, file: mainFile, mediaCount: Int32(entries.count)) + self.disposable.set((TelegramEngine(account: self.account).historyImport.initSession(peerId: peerId, file: mainFile, mediaCount: Int32(entries.count)) |> mapError { error -> ImportError in switch error { case .chatAdminRequired: @@ -180,7 +180,7 @@ private final class ImportManager { self.failWithError(.generic) return } - self.disposable.set((ChatHistoryImport.startImport(account: self.account, session: session) + self.disposable.set((TelegramEngine(account: self.account).historyImport.startImport(session: session) |> deliverOnMainQueue).start(error: { [weak self] _ in guard let strongSelf = self else { return @@ -258,7 +258,7 @@ private final class ImportManager { if !pathExtension.isEmpty, let value = TGMimeTypeMap.mimeType(forExtension: pathExtension) { mimeType = value } - return ChatHistoryImport.uploadMedia(account: account, session: session, file: tempFile, disposeFileAfterDone: true, fileName: entry.0.path, mimeType: mimeType, type: entry.2) + return TelegramEngine(account: account).historyImport.uploadMedia(session: session, file: tempFile, disposeFileAfterDone: true, fileName: entry.0.path, mimeType: mimeType, type: entry.2) |> mapError { error -> ImportError in switch error { case .chatAdminRequired: @@ -534,7 +534,7 @@ public final class ChatImportActivityScreen: ViewController { self.radialStatusText.attributedText = NSAttributedString(string: "\(Int(effectiveProgress * 100.0))%", font: Font.with(size: floor(36.0 * maxK), design: .round, weight: .semibold), textColor: self.presentationData.theme.list.itemPrimaryTextColor) let radialStatusTextSize = self.radialStatusText.updateLayout(CGSize(width: 200.0, height: .greatestFiniteMagnitude)) - self.progressText.attributedText = NSAttributedString(string: "\(dataSizeString(Int(effectiveProgress * CGFloat(self.totalBytes)))) of \(dataSizeString(Int(1.0 * CGFloat(self.totalBytes))))", font: Font.semibold(17.0), textColor: self.presentationData.theme.list.itemPrimaryTextColor) + self.progressText.attributedText = NSAttributedString(string: "\(dataSizeString(Int(effectiveProgress * CGFloat(self.totalBytes)), formatting: DataSizeStringFormatting(presentationData: self.presentationData))) of \(dataSizeString(Int(1.0 * CGFloat(self.totalBytes)), formatting: DataSizeStringFormatting(presentationData: self.presentationData)))", font: Font.semibold(17.0), textColor: self.presentationData.theme.list.itemPrimaryTextColor) let progressTextSize = self.progressText.updateLayout(CGSize(width: layout.size.width - 16.0 * 2.0, height: .greatestFiniteMagnitude)) switch self.state { @@ -732,7 +732,7 @@ public final class ChatImportActivityScreen: ViewController { private let mainEntry: TempBoxFile private let totalBytes: Int private let totalMediaBytes: Int - private let otherEntries: [(SSZipEntry, String, ChatHistoryImport.MediaType)] + private let otherEntries: [(SSZipEntry, String, TelegramEngine.HistoryImport.MediaType)] private var importManager: ImportManager? private var progressEstimator: ProgressEstimator? @@ -749,14 +749,14 @@ public final class ChatImportActivityScreen: ViewController { } } - public init(context: AccountContext, cancel: @escaping () -> Void, peerId: PeerId, archivePath: String?, mainEntry: TempBoxFile, otherEntries: [(SSZipEntry, String, ChatHistoryImport.MediaType)]) { + public init(context: AccountContext, cancel: @escaping () -> Void, peerId: PeerId, archivePath: String?, mainEntry: TempBoxFile, otherEntries: [(SSZipEntry, String, TelegramEngine.HistoryImport.MediaType)]) { self.context = context self.cancel = cancel self.peerId = peerId self.archivePath = archivePath self.mainEntry = mainEntry - self.otherEntries = otherEntries.map { entry -> (SSZipEntry, String, ChatHistoryImport.MediaType) in + self.otherEntries = otherEntries.map { entry -> (SSZipEntry, String, TelegramEngine.HistoryImport.MediaType) in return (entry.0, entry.1, entry.2) } @@ -814,7 +814,7 @@ public final class ChatImportActivityScreen: ViewController { override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { super.containerLayoutUpdated(layout, transition: transition) - self.controllerNode.containerLayoutUpdated(layout, navigationHeight: self.navigationHeight, transition: transition) + self.controllerNode.containerLayoutUpdated(layout, navigationHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition) } private func beginImport() { @@ -823,7 +823,7 @@ public final class ChatImportActivityScreen: ViewController { let resolvedPeerId: Signal if self.peerId.namespace == Namespaces.Peer.CloudGroup { - resolvedPeerId = convertGroupToSupergroup(account: self.context.account, peerId: self.peerId) + resolvedPeerId = self.context.engine.peers.convertGroupToSupergroup(peerId: self.peerId) |> mapError { _ -> ImportManager.ImportError in return .generic } diff --git a/submodules/ChatInterfaceState/Sources/ChatInterfaceState.swift b/submodules/ChatInterfaceState/Sources/ChatInterfaceState.swift index bcc798afd9..830419853e 100644 --- a/submodules/ChatInterfaceState/Sources/ChatInterfaceState.swift +++ b/submodules/ChatInterfaceState/Sources/ChatInterfaceState.swift @@ -93,25 +93,28 @@ public struct ChatEditMessageState: PostboxCoding, Equatable { public struct ChatInterfaceMessageActionsState: PostboxCoding, Equatable { public var closedButtonKeyboardMessageId: MessageId? + public var dismissedButtonKeyboardMessageId: MessageId? public var processedSetupReplyMessageId: MessageId? public var closedPinnedMessageId: MessageId? public var closedPeerSpecificPackSetup: Bool = false public var dismissedAddContactPhoneNumber: String? public var isEmpty: Bool { - return self.closedButtonKeyboardMessageId == nil && self.processedSetupReplyMessageId == nil && self.closedPinnedMessageId == nil && self.closedPeerSpecificPackSetup == false && self.dismissedAddContactPhoneNumber == nil + return self.closedButtonKeyboardMessageId == nil && self.dismissedButtonKeyboardMessageId == nil && self.processedSetupReplyMessageId == nil && self.closedPinnedMessageId == nil && self.closedPeerSpecificPackSetup == false && self.dismissedAddContactPhoneNumber == nil } public init() { self.closedButtonKeyboardMessageId = nil + self.dismissedButtonKeyboardMessageId = nil self.processedSetupReplyMessageId = nil self.closedPinnedMessageId = nil self.closedPeerSpecificPackSetup = false self.dismissedAddContactPhoneNumber = nil } - public init(closedButtonKeyboardMessageId: MessageId?, processedSetupReplyMessageId: MessageId?, closedPinnedMessageId: MessageId?, closedPeerSpecificPackSetup: Bool, dismissedAddContactPhoneNumber: String?) { + public init(closedButtonKeyboardMessageId: MessageId?, dismissedButtonKeyboardMessageId: MessageId?, processedSetupReplyMessageId: MessageId?, closedPinnedMessageId: MessageId?, closedPeerSpecificPackSetup: Bool, dismissedAddContactPhoneNumber: String?) { self.closedButtonKeyboardMessageId = closedButtonKeyboardMessageId + self.dismissedButtonKeyboardMessageId = dismissedButtonKeyboardMessageId self.processedSetupReplyMessageId = processedSetupReplyMessageId self.closedPinnedMessageId = closedPinnedMessageId self.closedPeerSpecificPackSetup = closedPeerSpecificPackSetup @@ -124,6 +127,12 @@ public struct ChatInterfaceMessageActionsState: PostboxCoding, Equatable { } else { self.closedButtonKeyboardMessageId = nil } + + if let messageIdPeerId = decoder.decodeOptionalInt64ForKey("dismissedbuttons.p"), let messageIdNamespace = decoder.decodeOptionalInt32ForKey("dismissedbuttons.n"), let messageIdId = decoder.decodeOptionalInt32ForKey("dismissedbuttons.i") { + self.dismissedButtonKeyboardMessageId = MessageId(peerId: PeerId(messageIdPeerId), namespace: messageIdNamespace, id: messageIdId) + } else { + self.dismissedButtonKeyboardMessageId = nil + } if let processedMessageIdPeerId = decoder.decodeOptionalInt64ForKey("pb.p"), let processedMessageIdNamespace = decoder.decodeOptionalInt32ForKey("pb.n"), let processedMessageIdId = decoder.decodeOptionalInt32ForKey("pb.i") { self.processedSetupReplyMessageId = MessageId(peerId: PeerId(processedMessageIdPeerId), namespace: processedMessageIdNamespace, id: processedMessageIdId) @@ -150,6 +159,16 @@ public struct ChatInterfaceMessageActionsState: PostboxCoding, Equatable { encoder.encodeNil(forKey: "cb.n") encoder.encodeNil(forKey: "cb.i") } + + if let dismissedButtonKeyboardMessageId = self.dismissedButtonKeyboardMessageId { + encoder.encodeInt64(dismissedButtonKeyboardMessageId.peerId.toInt64(), forKey: "dismissedbuttons.p") + encoder.encodeInt32(dismissedButtonKeyboardMessageId.namespace, forKey: "dismissedbuttons.n") + encoder.encodeInt32(dismissedButtonKeyboardMessageId.id, forKey: "dismissedbuttons.i") + } else { + encoder.encodeNil(forKey: "dismissedbuttons.p") + encoder.encodeNil(forKey: "dismissedbuttons.n") + encoder.encodeNil(forKey: "dismissedbuttons.i") + } if let processedSetupReplyMessageId = self.processedSetupReplyMessageId { encoder.encodeInt64(processedSetupReplyMessageId.peerId.toInt64(), forKey: "pb.p") diff --git a/submodules/ChatListSearchItemHeader/Sources/ChatListSearchItemHeader.swift b/submodules/ChatListSearchItemHeader/Sources/ChatListSearchItemHeader.swift index 966fd6fa88..ab2515d3ed 100644 --- a/submodules/ChatListSearchItemHeader/Sources/ChatListSearchItemHeader.swift +++ b/submodules/ChatListSearchItemHeader/Sources/ChatListSearchItemHeader.swift @@ -151,9 +151,10 @@ private enum ChatListSearchItemHeaderId: Int32 { } public final class ChatListSearchItemHeader: ListViewItemHeader { - public let id: Int64 + public let id: ListViewItemNode.HeaderId public let type: ChatListSearchItemHeaderType public let stickDirection: ListViewItemHeaderStickDirection = .top + public let stickOverInsets: Bool = true public let theme: PresentationTheme public let strings: PresentationStrings public let actionTitle: String? @@ -163,14 +164,14 @@ public final class ChatListSearchItemHeader: ListViewItemHeader { public init(type: ChatListSearchItemHeaderType, theme: PresentationTheme, strings: PresentationStrings, actionTitle: String? = nil, action: (() -> Void)? = nil) { self.type = type - self.id = Int64(self.type.id.rawValue) + self.id = ListViewItemNode.HeaderId(space: 0, id: Int64(self.type.id.rawValue)) self.theme = theme self.strings = strings self.actionTitle = actionTitle self.action = action } - public func node() -> ListViewItemHeaderNode { + public func node(synchronousLoad: Bool) -> ListViewItemHeaderNode { return ChatListSearchItemHeaderNode(type: self.type, theme: self.theme, strings: self.strings, actionTitle: self.actionTitle, action: self.action) } diff --git a/submodules/ChatListSearchRecentPeersNode/Sources/ChatListSearchRecentPeersNode.swift b/submodules/ChatListSearchRecentPeersNode/Sources/ChatListSearchRecentPeersNode.swift index eef6e3b9ab..5cc3470630 100644 --- a/submodules/ChatListSearchRecentPeersNode/Sources/ChatListSearchRecentPeersNode.swift +++ b/submodules/ChatListSearchRecentPeersNode/Sources/ChatListSearchRecentPeersNode.swift @@ -150,7 +150,7 @@ public final class ChatListSearchRecentPeersNode: ASDisplayNode { let peersDisposable = DisposableSet() - let recent: Signal<([Peer], [PeerId: (Int32, Bool)], [PeerId : PeerPresence]), NoError> = recentPeers(account: context.account) + let recent: Signal<([Peer], [PeerId: (Int32, Bool)], [PeerId : PeerPresence]), NoError> = context.engine.peers.recentPeers() |> filter { value -> Bool in switch value { case .disabled: @@ -224,7 +224,7 @@ public final class ChatListSearchRecentPeersNode: ASDisplayNode { } })) if case .actionSheet = mode { - peersDisposable.add(managedUpdatedRecentPeers(accountPeerId: context.account.peerId, postbox: context.account.postbox, network: context.account.network).start()) + peersDisposable.add(context.engine.peers.managedUpdatedRecentPeers().start()) } self.disposable.set(peersDisposable) } diff --git a/submodules/ChatListUI/Sources/ChatContextMenus.swift b/submodules/ChatListUI/Sources/ChatContextMenus.swift index f2c4bee0a4..8c353d57e2 100644 --- a/submodules/ChatListUI/Sources/ChatContextMenus.swift +++ b/submodules/ChatListUI/Sources/ChatContextMenus.swift @@ -49,141 +49,162 @@ enum ChatContextMenuSource { func chatContextMenuItems(context: AccountContext, peerId: PeerId, promoInfo: ChatListNodeEntryPromoInfo?, source: ChatContextMenuSource, chatListController: ChatListControllerImpl?, joined: Bool) -> Signal<[ContextMenuItem], NoError> { let presentationData = context.sharedContext.currentPresentationData.with({ $0 }) let strings = presentationData.strings - return context.account.postbox.transaction { [weak chatListController] transaction -> [ContextMenuItem] in - if promoInfo != nil { - return [] - } - - var items: [ContextMenuItem] = [] - - if case let .search(search) = source { - switch search { - case .recentPeers: - items.append(.action(ContextMenuActionItem(text: strings.ChatList_Context_RemoveFromRecents, textColor: .destructive, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Clear"), color: theme.contextMenu.destructiveColor) }, action: { _, f in - let _ = (removeRecentPeer(account: context.account, peerId: peerId) - |> deliverOnMainQueue).start(completed: { - f(.default) - }) - }))) - items.append(.separator) - case .recentSearch: - items.append(.action(ContextMenuActionItem(text: strings.ChatList_Context_RemoveFromRecents, textColor: .destructive, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Clear"), color: theme.contextMenu.destructiveColor) }, action: { _, f in - let _ = (removeRecentlySearchedPeer(postbox: context.account.postbox, peerId: peerId) - |> deliverOnMainQueue).start(completed: { - f(.default) - }) - }))) - items.append(.separator) - case .search: - break - } - } - - let isSavedMessages = peerId == context.account.peerId - - let chatPeer = transaction.getPeer(peerId) - var maybePeer: Peer? - if let chatPeer = chatPeer { - if let chatPeer = chatPeer as? TelegramSecretChat { - maybePeer = transaction.getPeer(chatPeer.regularPeerId) + + return context.account.postbox.transaction { transaction -> (PeerGroupId, ChatListIndex)? in + transaction.getPeerChatListIndex(peerId) + } + |> mapToSignal { groupAndIndex -> Signal<[ContextMenuItem], NoError> in + let location: TogglePeerChatPinnedLocation + var chatListFilter: ChatListFilter? + if case let .chatList(filter) = source, let chatFilter = filter { + chatListFilter = chatFilter + location = .filter(chatFilter.id) + } else { + if let (group, _) = groupAndIndex { + location = .group(group) } else { - maybePeer = chatPeer - } - } - - guard let peer = maybePeer else { - return [] - } - - if !isSavedMessages, let peer = peer as? TelegramUser, !peer.flags.contains(.isSupport) && peer.botInfo == nil && !peer.isDeleted { - if !transaction.isPeerContact(peerId: peer.id) { - items.append(.action(ContextMenuActionItem(text: strings.ChatList_Context_AddToContacts, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/AddUser"), color: theme.contextMenu.primaryColor) }, action: { _, f in - context.sharedContext.openAddPersonContact(context: context, peerId: peerId, pushController: { controller in - if let navigationController = chatListController?.navigationController as? NavigationController { - navigationController.pushViewController(controller) - } - }, present: { c, a in - if let chatListController = chatListController { - chatListController.present(c, in: .window(.root), with: a) - } - }) - f(.default) - }))) - items.append(.separator) - } - } - - var isMuted = false - if let notificationSettings = transaction.getPeerNotificationSettings(peerId) as? TelegramPeerNotificationSettings { - if case .muted = notificationSettings.muteState { - isMuted = true + location = .group(.root) } } - var isUnread = false - if let readState = transaction.getCombinedPeerReadState(peerId), readState.isUnread { - isUnread = true - } - - let isContact = transaction.isPeerContact(peerId: peerId) - - if case let .chatList(currentFilter) = source { - if let currentFilter = currentFilter { - items.append(.action(ContextMenuActionItem(text: strings.ChatList_Context_RemoveFromFolder, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/RemoveFromFolder"), color: theme.contextMenu.primaryColor) }, action: { c, _ in - let _ = (context.account.postbox.transaction { transaction -> Void in - updateChatListFiltersInteractively(transaction: transaction, { filters in - var filters = filters - for i in 0 ..< filters.count { - if filters[i].id == currentFilter.id { - let _ = filters[i].data.addExcludePeer(peerId: peer.id) - break + return combineLatest( + context.engine.peers.updatedChatListFilters() + |> take(1), + context.engine.peers.getPinnedItemIds(location: location) + ) + |> mapToSignal { filters, pinnedItemIds -> Signal<[ContextMenuItem], NoError> in + let isPinned = pinnedItemIds.contains(.peer(peerId)) + + return context.account.postbox.transaction { [weak chatListController] transaction -> [ContextMenuItem] in + if promoInfo != nil { + return [] + } + + var items: [ContextMenuItem] = [] + + if case let .search(search) = source { + switch search { + case .recentPeers: + items.append(.action(ContextMenuActionItem(text: strings.ChatList_Context_RemoveFromRecents, textColor: .destructive, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Clear"), color: theme.contextMenu.destructiveColor) }, action: { _, f in + let _ = (context.engine.peers.removeRecentPeer(peerId: peerId) + |> deliverOnMainQueue).start(completed: { + f(.default) + }) + }))) + items.append(.separator) + case .recentSearch: + items.append(.action(ContextMenuActionItem(text: strings.ChatList_Context_RemoveFromRecents, textColor: .destructive, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Clear"), color: theme.contextMenu.destructiveColor) }, action: { _, f in + let _ = (context.engine.peers.removeRecentlySearchedPeer(peerId: peerId) + |> deliverOnMainQueue).start(completed: { + f(.default) + }) + }))) + items.append(.separator) + case .search: + break + } + } + + let isSavedMessages = peerId == context.account.peerId + + let chatPeer = transaction.getPeer(peerId) + var maybePeer: Peer? + if let chatPeer = chatPeer { + if let chatPeer = chatPeer as? TelegramSecretChat { + maybePeer = transaction.getPeer(chatPeer.regularPeerId) + } else { + maybePeer = chatPeer + } + } + + guard let peer = maybePeer else { + return [] + } + + if !isSavedMessages, let peer = peer as? TelegramUser, !peer.flags.contains(.isSupport) && peer.botInfo == nil && !peer.isDeleted { + if !transaction.isPeerContact(peerId: peer.id) { + items.append(.action(ContextMenuActionItem(text: strings.ChatList_Context_AddToContacts, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/AddUser"), color: theme.contextMenu.primaryColor) }, action: { _, f in + context.sharedContext.openAddPersonContact(context: context, peerId: peerId, pushController: { controller in + if let navigationController = chatListController?.navigationController as? NavigationController { + navigationController.pushViewController(controller) } + }, present: { c, a in + if let chatListController = chatListController { + chatListController.present(c, in: .window(.root), with: a) + } + }) + f(.default) + }))) + items.append(.separator) + } + } + + var isMuted = false + if let notificationSettings = transaction.getPeerNotificationSettings(peerId) as? TelegramPeerNotificationSettings { + if case .muted = notificationSettings.muteState { + isMuted = true + } + } + + var isUnread = false + if let readState = transaction.getCombinedPeerReadState(peerId), readState.isUnread { + isUnread = true + } + + let isContact = transaction.isPeerContact(peerId: peerId) + + if case let .chatList(currentFilter) = source { + if let currentFilter = currentFilter { + items.append(.action(ContextMenuActionItem(text: strings.ChatList_Context_RemoveFromFolder, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/RemoveFromFolder"), color: theme.contextMenu.primaryColor) }, action: { c, _ in + let _ = (context.engine.peers.updateChatListFiltersInteractively { filters in + var filters = filters + for i in 0 ..< filters.count { + if filters[i].id == currentFilter.id { + let _ = filters[i].data.addExcludePeer(peerId: peer.id) + break + } + } + return filters + } + |> deliverOnMainQueue).start(completed: { + c.dismiss(completion: { + chatListController?.present(UndoOverlayController(presentationData: presentationData, content: .chatRemovedFromFolder(chatTitle: peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), folderTitle: currentFilter.title), elevatedLayout: false, animateInAsReplacement: true, action: { _ in + return false + }), in: .current) + }) + }) + }))) + } else { + var hasFolders = false + + for filter in filters { + let predicate = chatListFilterPredicate(filter: filter.data) + if predicate.includes(peer: peer, groupId: .root, isRemovedFromTotalUnreadCount: isMuted, isUnread: isUnread, isContact: isContact, messageTagSummaryResult: false) { + continue + } + + var data = filter.data + if data.addIncludePeer(peerId: peer.id) { + hasFolders = true + break } - return filters - }) - } - |> deliverOnMainQueue).start(completed: { - c.dismiss(completion: { - chatListController?.present(UndoOverlayController(presentationData: presentationData, content: .chatRemovedFromFolder(chatTitle: peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), folderTitle: currentFilter.title), elevatedLayout: false, animateInAsReplacement: true, action: { _ in - return false - }), in: .current) - }) - }) - }))) - } else { - var hasFolders = false - updateChatListFiltersInteractively(transaction: transaction, { filters in - for filter in filters { - let predicate = chatListFilterPredicate(filter: filter.data) - if predicate.includes(peer: peer, groupId: .root, isRemovedFromTotalUnreadCount: isMuted, isUnread: isUnread, isContact: isContact, messageTagSummaryResult: false) { - continue } - - var data = filter.data - if data.addIncludePeer(peerId: peer.id) { - hasFolders = true - break - } - } - return filters - }) - - if hasFolders { - items.append(.action(ContextMenuActionItem(text: strings.ChatList_Context_AddToFolder, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Folder"), color: theme.contextMenu.primaryColor) }, action: { c, _ in - let _ = (context.account.postbox.transaction { transaction -> [ContextMenuItem] in - var updatedItems: [ContextMenuItem] = [] - updateChatListFiltersInteractively(transaction: transaction, { filters in + + if hasFolders { + items.append(.action(ContextMenuActionItem(text: strings.ChatList_Context_AddToFolder, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Folder"), color: theme.contextMenu.primaryColor) }, action: { c, _ in + var updatedItems: [ContextMenuItem] = [] + for filter in filters { let predicate = chatListFilterPredicate(filter: filter.data) if predicate.includes(peer: peer, groupId: .root, isRemovedFromTotalUnreadCount: isMuted, isUnread: isUnread, isContact: isContact, messageTagSummaryResult: false) { continue } - + var data = filter.data if !data.addIncludePeer(peerId: peer.id) { continue } - + let filterType = chatListFilterType(filter) updatedItems.append(.action(ContextMenuActionItem(text: filter.title, icon: { theme in let imageName: String @@ -208,7 +229,7 @@ func chatContextMenuItems(context: AccountContext, peerId: PeerId, promoInfo: Ch return generateTintedImage(image: UIImage(bundleImageName: imageName), color: theme.contextMenu.primaryColor) }, action: { c, f in c.dismiss(completion: { - let _ = (updateChatListFiltersInteractively(postbox: context.account.postbox, { filters in + let _ = (context.engine.peers.updateChatListFiltersInteractively { filters in var filters = filters for i in 0 ..< filters.count { if filters[i].id == filter.id { @@ -217,180 +238,160 @@ func chatContextMenuItems(context: AccountContext, peerId: PeerId, promoInfo: Ch } } return filters - })).start() - + }).start() + chatListController?.present(UndoOverlayController(presentationData: presentationData, content: .chatAddedToFolder(chatTitle: peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), folderTitle: filter.title), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), in: .current) }) }))) } - - return filters - }) - - updatedItems.append(.separator) - updatedItems.append(.action(ContextMenuActionItem(text: strings.ChatList_Context_Back, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: theme.contextMenu.primaryColor) - }, action: { c, _ in - c.setItems(chatContextMenuItems(context: context, peerId: peerId, promoInfo: promoInfo, source: source, chatListController: chatListController, joined: joined)) + + updatedItems.append(.separator) + updatedItems.append(.action(ContextMenuActionItem(text: strings.ChatList_Context_Back, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: theme.contextMenu.primaryColor) + }, action: { c, _ in + c.setItems(chatContextMenuItems(context: context, peerId: peerId, promoInfo: promoInfo, source: source, chatListController: chatListController, joined: joined)) + }))) + + c.setItems(.single(updatedItems)) }))) - - return updatedItems } - |> deliverOnMainQueue).start(next: { updatedItems in - c.setItems(.single(updatedItems)) - }) + } + } + + if isUnread { + items.append(.action(ContextMenuActionItem(text: strings.ChatList_Context_MarkAsRead, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/MarkAsRead"), color: theme.contextMenu.primaryColor) }, action: { _, f in + let _ = togglePeerUnreadMarkInteractively(postbox: context.account.postbox, viewTracker: context.account.viewTracker, peerId: peerId).start() + f(.default) + }))) + } else { + items.append(.action(ContextMenuActionItem(text: strings.ChatList_Context_MarkAsUnread, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/MarkAsUnread"), color: theme.contextMenu.primaryColor) }, action: { _, f in + let _ = togglePeerUnreadMarkInteractively(postbox: context.account.postbox, viewTracker: context.account.viewTracker, peerId: peerId).start() + f(.default) }))) } - } - } - - if isUnread { - items.append(.action(ContextMenuActionItem(text: strings.ChatList_Context_MarkAsRead, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/MarkAsRead"), color: theme.contextMenu.primaryColor) }, action: { _, f in - let _ = togglePeerUnreadMarkInteractively(postbox: context.account.postbox, viewTracker: context.account.viewTracker, peerId: peerId).start() - f(.default) - }))) - } else { - items.append(.action(ContextMenuActionItem(text: strings.ChatList_Context_MarkAsUnread, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/MarkAsUnread"), color: theme.contextMenu.primaryColor) }, action: { _, f in - let _ = togglePeerUnreadMarkInteractively(postbox: context.account.postbox, viewTracker: context.account.viewTracker, peerId: peerId).start() - f(.default) - }))) - } - - let groupAndIndex = transaction.getPeerChatListIndex(peerId) - - let archiveEnabled = !isSavedMessages && peerId != PeerId(namespace: Namespaces.Peer.CloudUser, id: 777000) && peerId == context.account.peerId - if let (group, index) = groupAndIndex { - if archiveEnabled { - let isArchived = group == Namespaces.PeerGroup.archive - items.append(.action(ContextMenuActionItem(text: isArchived ? strings.ChatList_Context_Unarchive : strings.ChatList_Context_Archive, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: isArchived ? "Chat/Context Menu/Unarchive" : "Chat/Context Menu/Archive"), color: theme.contextMenu.primaryColor) }, action: { _, f in - if isArchived { - let _ = (context.account.postbox.transaction { transaction -> Void in - updatePeerGroupIdInteractively(transaction: transaction, peerId: peerId, groupId: .root) - } - |> deliverOnMainQueue).start(completed: { - f(.default) - }) - } else { - if let chatListController = chatListController { - chatListController.archiveChats(peerIds: [peerId]) - f(.default) - } else { - let _ = (context.account.postbox.transaction { transaction -> Void in - updatePeerGroupIdInteractively(transaction: transaction, peerId: peerId, groupId: Namespaces.PeerGroup.archive) + + let archiveEnabled = !isSavedMessages && peerId != PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt32Value(777000)) && peerId == context.account.peerId + if let (group, index) = groupAndIndex { + if archiveEnabled { + let isArchived = group == Namespaces.PeerGroup.archive + items.append(.action(ContextMenuActionItem(text: isArchived ? strings.ChatList_Context_Unarchive : strings.ChatList_Context_Archive, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: isArchived ? "Chat/Context Menu/Unarchive" : "Chat/Context Menu/Archive"), color: theme.contextMenu.primaryColor) }, action: { _, f in + if isArchived { + let _ = (context.account.postbox.transaction { transaction -> Void in + updatePeerGroupIdInteractively(transaction: transaction, peerId: peerId, groupId: .root) + } + |> deliverOnMainQueue).start(completed: { + f(.default) + }) + } else { + if let chatListController = chatListController { + chatListController.archiveChats(peerIds: [peerId]) + f(.default) + } else { + let _ = (context.account.postbox.transaction { transaction -> Void in + updatePeerGroupIdInteractively(transaction: transaction, peerId: peerId, groupId: Namespaces.PeerGroup.archive) + } + |> deliverOnMainQueue).start(completed: { + f(.default) + }) + } } + }))) + } + + if isPinned || chatListFilter == nil || peerId.namespace != Namespaces.Peer.SecretChat { + items.append(.action(ContextMenuActionItem(text: isPinned ? strings.ChatList_Context_Unpin : strings.ChatList_Context_Pin, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: isPinned ? "Chat/Context Menu/Unpin" : "Chat/Context Menu/Pin"), color: theme.contextMenu.primaryColor) }, action: { _, f in + let _ = (context.engine.peers.toggleItemPinned(location: location, itemId: .peer(peerId)) + |> deliverOnMainQueue).start(next: { result in + switch result { + case .done: + break + case .limitExceeded: + break + } + f(.default) + }) + }))) + } + + if !isSavedMessages, let notificationSettings = transaction.getPeerNotificationSettings(peerId) as? TelegramPeerNotificationSettings { + var isMuted = false + if case .muted = notificationSettings.muteState { + isMuted = true + } + items.append(.action(ContextMenuActionItem(text: isMuted ? strings.ChatList_Context_Unmute : strings.ChatList_Context_Mute, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: isMuted ? "Chat/Context Menu/Unmute" : "Chat/Context Menu/Muted"), color: theme.contextMenu.primaryColor) }, action: { _, f in + let _ = (context.engine.peers.togglePeerMuted(peerId: peerId) |> deliverOnMainQueue).start(completed: { f(.default) }) + }))) + } + } else { + if case .search = source { + if let _ = 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 = context.peerChannelMemberCategoriesContextsManager.join(engine: context.engine, peerId: peerId, hash: nil) + 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?() + })) + chatListController?.present(controller, in: .window(.root)) + return ActionDisposable { [weak controller] in + Queue.mainQueue().async() { + controller?.dismiss() + } + } + } + |> runOn(Queue.mainQueue()) + |> delay(0.15, queue: Queue.mainQueue()) + let progressDisposable = progressSignal.start() + + createSignal = createSignal + |> afterDisposed { + Queue.mainQueue().async { + progressDisposable.dispose() + } + } + let joinChannelDisposable = MetaDisposable() + cancelImpl = { + joinChannelDisposable.set(nil) + } + + joinChannelDisposable.set((createSignal + |> deliverOnMainQueue).start(next: { _ in + if let navigationController = (chatListController?.navigationController as? NavigationController) { + context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peerId))) + } + }, error: { _ in + if let chatListController = chatListController { + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + chatListController.present(textAlertController(context: context, title: nil, text: presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + } + })) + f(.default) + }))) } } - }))) - } - -// if { - let location: TogglePeerChatPinnedLocation - var chatListFilter: ChatListFilter? - if case let .chatList(filter) = source, let chatFilter = filter { - chatListFilter = chatFilter - location = .filter(chatFilter.id) - } else { - location = .group(group) } - - let isPinned = getPinnedItemIds(transaction: transaction, location: location).contains(.peer(peerId)) - - if isPinned || chatListFilter == nil || peerId.namespace != Namespaces.Peer.SecretChat { - items.append(.action(ContextMenuActionItem(text: isPinned ? strings.ChatList_Context_Unpin : strings.ChatList_Context_Pin, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: isPinned ? "Chat/Context Menu/Unpin" : "Chat/Context Menu/Pin"), color: theme.contextMenu.primaryColor) }, action: { _, f in - let _ = (toggleItemPinned(postbox: context.account.postbox, location: location, itemId: .peer(peerId)) - |> deliverOnMainQueue).start(next: { result in - switch result { - case .done: - break - case .limitExceeded: - break - } - f(.default) - }) - }))) - } -// } - - if !isSavedMessages, let notificationSettings = transaction.getPeerNotificationSettings(peerId) as? TelegramPeerNotificationSettings { - var isMuted = false - if case .muted = notificationSettings.muteState { - isMuted = true - } - items.append(.action(ContextMenuActionItem(text: isMuted ? strings.ChatList_Context_Unmute : strings.ChatList_Context_Mute, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: isMuted ? "Chat/Context Menu/Unmute" : "Chat/Context Menu/Muted"), color: theme.contextMenu.primaryColor) }, action: { _, f in - let _ = (togglePeerMuted(account: context.account, peerId: peerId) - |> deliverOnMainQueue).start(completed: { - f(.default) - }) - }))) - } - } else { - if case .search = source { - if let _ = 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 = context.peerChannelMemberCategoriesContextsManager.join(account: context.account, peerId: peerId, hash: nil) - 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?() - })) - chatListController?.present(controller, in: .window(.root)) - return ActionDisposable { [weak controller] in - Queue.mainQueue().async() { - controller?.dismiss() - } - } + + if case .chatList = source, groupAndIndex != nil { + items.append(.action(ContextMenuActionItem(text: strings.ChatList_Context_Delete, textColor: .destructive, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor) }, action: { _, f in + if let chatListController = chatListController { + chatListController.deletePeerChat(peerId: peerId, joined: joined) } - |> runOn(Queue.mainQueue()) - |> delay(0.15, queue: Queue.mainQueue()) - let progressDisposable = progressSignal.start() - - createSignal = createSignal - |> afterDisposed { - Queue.mainQueue().async { - progressDisposable.dispose() - } - } - let joinChannelDisposable = MetaDisposable() - cancelImpl = { - joinChannelDisposable.set(nil) - } - - joinChannelDisposable.set((createSignal - |> deliverOnMainQueue).start(next: { _ in - if let navigationController = (chatListController?.navigationController as? NavigationController) { - context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peerId))) - } - }, error: { _ in - if let chatListController = chatListController { - let presentationData = context.sharedContext.currentPresentationData.with { $0 } - chatListController.present(textAlertController(context: context, title: nil, text: presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root)) - } - })) f(.default) }))) } + + if let item = items.last, case .separator = item { + items.removeLast() + } + + return items } } - - if case .chatList = source, groupAndIndex != nil { - items.append(.action(ContextMenuActionItem(text: strings.ChatList_Context_Delete, textColor: .destructive, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor) }, action: { _, f in - if let chatListController = chatListController { - chatListController.deletePeerChat(peerId: peerId, joined: joined) - } - f(.default) - }))) - } - - if let item = items.last, case .separator = item { - items.removeLast() - } - - return items } } diff --git a/submodules/ChatListUI/Sources/ChatListAdditionalCategoryItem.swift b/submodules/ChatListUI/Sources/ChatListAdditionalCategoryItem.swift index d8912b3ab7..c55120dd20 100644 --- a/submodules/ChatListUI/Sources/ChatListAdditionalCategoryItem.swift +++ b/submodules/ChatListUI/Sources/ChatListAdditionalCategoryItem.swift @@ -363,9 +363,9 @@ public class ChatListAdditionalCategoryItemNode: ItemListRevealOptionsItemNode { self.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration * 0.5, removeOnCompletion: false) } - override public func header() -> ListViewItemHeader? { + override public func headers() -> [ListViewItemHeader]? { if let item = self.item { - return item.header + return item.header.flatMap { [$0] } } else { return nil } diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index ac1af87bd4..6baa6ea005 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -144,9 +144,6 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController private let featuredFiltersDisposable = MetaDisposable() private var processedFeaturedFilters = false - private let preloadedSticker = Promise(nil) - private let preloadStickerDisposable = MetaDisposable() - private let isReorderingTabsValue = ValuePromise(false) private var searchContentNode: NavigationBarSearchContentNode? @@ -154,6 +151,8 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController private let tabContainerNode: ChatListFilterTabContainerNode private var tabContainerData: ([ChatListFilterTabEntry], Bool)? + private var didSetupTabs = false + public override func updateNavigationCustomData(_ data: Any?, progress: CGFloat, transition: ContainedViewLayoutTransition) { if self.isNodeLoaded { self.chatListDisplayNode.containerNode.updateSelectedChatLocation(data: data as? ChatLocation, progress: progress, transition: transition) @@ -187,7 +186,6 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController title = filter.title } else if self.groupId == .root { title = self.presentationData.strings.DialogList_Title - self.navigationBar?.item = nil } else { title = self.presentationData.strings.ChatList_ArchivedChatsTitle } @@ -281,6 +279,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController return (data.isLockable, false) } + let previousEditingAndNetworkStateValue = Atomic<(Bool, AccountNetworkState)?>(value: nil) if !self.hideNetworkActivityStatus { self.titleDisposable = combineLatest(queue: .mainQueue(), context.account.networkState, @@ -296,19 +295,26 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController } else { defaultTitle = strongSelf.presentationData.strings.ChatList_ArchivedChatsTitle } + let previousEditingAndNetworkState = previousEditingAndNetworkStateValue.swap((stateAndFilterId.state.editing, networkState)) if stateAndFilterId.state.editing { if strongSelf.groupId == .root { - strongSelf.navigationItem.rightBarButtonItem = nil + strongSelf.navigationItem.setRightBarButton(nil, animated: true) } - let title = !stateAndFilterId.state.selectedPeerIds.isEmpty ? strongSelf.presentationData.strings.ChatList_SelectedChats(Int32(stateAndFilterId.state.selectedPeerIds.count)) : defaultTitle - strongSelf.titleView.title = NetworkStatusTitle(text: title, activity: false, hasProxy: false, connectsViaProxy: false, isPasscodeSet: false, isManuallyLocked: false) + + var animated = false + if let (previousEditing, previousNetworkState) = previousEditingAndNetworkState { + if previousEditing != stateAndFilterId.state.editing, previousNetworkState == networkState, case .online = networkState { + animated = true + } + } + strongSelf.titleView.setTitle(NetworkStatusTitle(text: title, activity: false, hasProxy: false, connectsViaProxy: false, isPasscodeSet: false, isManuallyLocked: false), animated: animated) } else if isReorderingTabs { if strongSelf.groupId == .root { - strongSelf.navigationItem.rightBarButtonItem = nil + strongSelf.navigationItem.setRightBarButton(nil, animated: true) } let leftBarButtonItem = UIBarButtonItem(title: strongSelf.presentationData.strings.Common_Done, style: .done, target: strongSelf, action: #selector(strongSelf.reorderingDonePressed)) - strongSelf.navigationItem.leftBarButtonItem = leftBarButtonItem + strongSelf.navigationItem.setLeftBarButton(leftBarButtonItem, animated: true) let (_, connectsViaProxy) = proxy switch networkState { @@ -331,16 +337,21 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController isRoot = true if isReorderingTabs { - strongSelf.navigationItem.rightBarButtonItem = nil + strongSelf.navigationItem.setRightBarButton(nil, animated: true) } else { 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.navigationItem.rightBarButtonItem?.accessibilityLabel != rightBarButtonItem.accessibilityLabel { + strongSelf.navigationItem.setRightBarButton(rightBarButtonItem, animated: true) + } } if isReorderingTabs { let leftBarButtonItem = UIBarButtonItem(title: strongSelf.presentationData.strings.Common_Done, style: .done, target: strongSelf, action: #selector(strongSelf.reorderingDonePressed)) - strongSelf.navigationItem.leftBarButtonItem = leftBarButtonItem + leftBarButtonItem.accessibilityLabel = strongSelf.presentationData.strings.Common_Done + if strongSelf.navigationItem.leftBarButtonItem?.accessibilityLabel != leftBarButtonItem.accessibilityLabel { + strongSelf.navigationItem.setLeftBarButton(leftBarButtonItem, animated: true) + } } else { let editItem: UIBarButtonItem if stateAndFilterId.state.editing { @@ -350,8 +361,14 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController editItem = UIBarButtonItem(title: strongSelf.presentationData.strings.Common_Edit, style: .plain, target: self, action: #selector(strongSelf.editPressed)) editItem.accessibilityLabel = strongSelf.presentationData.strings.Common_Edit } - strongSelf.navigationItem.leftBarButtonItem = editItem + if strongSelf.navigationItem.leftBarButtonItem?.accessibilityLabel != editItem.accessibilityLabel { + strongSelf.navigationItem.setLeftBarButton(editItem, animated: true) + } } + } else { + let editItem = UIBarButtonItem(title: strongSelf.presentationData.strings.Common_Edit, style: .plain, target: self, action: #selector(strongSelf.editPressed)) + editItem.accessibilityLabel = strongSelf.presentationData.strings.Common_Edit + strongSelf.navigationItem.setRightBarButton(editItem, animated: true) } let (hasProxy, connectsViaProxy) = proxy @@ -372,7 +389,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController case .updating: strongSelf.titleView.title = NetworkStatusTitle(text: strongSelf.presentationData.strings.State_Updating, activity: true, hasProxy: isRoot && hasProxy, connectsViaProxy: connectsViaProxy, isPasscodeSet: isRoot && isPasscodeSet, isManuallyLocked: isRoot && isManuallyLocked) case .online: - strongSelf.titleView.title = NetworkStatusTitle(text: defaultTitle, activity: false, hasProxy: isRoot && hasProxy, connectsViaProxy: connectsViaProxy, isPasscodeSet: isRoot && isPasscodeSet, isManuallyLocked: isRoot && isManuallyLocked) + strongSelf.titleView.setTitle(NetworkStatusTitle(text: defaultTitle, activity: false, hasProxy: isRoot && hasProxy, connectsViaProxy: connectsViaProxy, isPasscodeSet: isRoot && isPasscodeSet, isManuallyLocked: isRoot && isManuallyLocked), animated: (previousEditingAndNetworkState?.0 ?? false) != stateAndFilterId.state.editing) } if groupId == .root && filter == nil && checkProxy { if strongSelf.proxyUnavailableTooltipController == nil && !strongSelf.didShowProxyUnavailableTooltipController && strongSelf.isNodeLoaded && strongSelf.displayNode.view.window != nil && strongSelf.navigationController?.topViewController === self { @@ -596,53 +613,43 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController scrollToEndIfExists = true } - let _ = (strongSelf.preloadedSticker.get() - |> take(1) - |> deliverOnMainQueue).start(next: { [weak self] greetingSticker in - if let strongSelf = self { - strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(peer.id), activateInput: activateInput && !peer.isDeleted, scrollToEndIfExists: scrollToEndIfExists, greetingData: greetingSticker.flatMap({ ChatGreetingData(sticker: $0) }), animated: !scrollToEndIfExists, options: strongSelf.groupId == PeerGroupId.root ? [.removeOnMasterDetails] : [], parentGroupId: strongSelf.groupId, completion: { [weak self] controller in - self?.chatListDisplayNode.containerNode.currentItemNode.clearHighlightAnimated(true) - if let promoInfo = promoInfo { - switch promoInfo { - case .proxy: - let _ = (ApplicationSpecificNotice.getProxyAdsAcknowledgment(accountManager: strongSelf.context.sharedContext.accountManager) - |> deliverOnMainQueue).start(next: { value in - guard let strongSelf = self else { - return - } - if !value { - controller.displayPromoAnnouncement(text: strongSelf.presentationData.strings.DialogList_AdNoticeAlert) - let _ = ApplicationSpecificNotice.setProxyAdsAcknowledgment(accountManager: strongSelf.context.sharedContext.accountManager).start() - } - }) - case let .psa(type, _): - let _ = (ApplicationSpecificNotice.getPsaAcknowledgment(accountManager: strongSelf.context.sharedContext.accountManager, peerId: peer.id) - |> deliverOnMainQueue).start(next: { value in - guard let strongSelf = self else { - return - } - if !value { - var text = strongSelf.presentationData.strings.ChatList_GenericPsaAlert - let key = "ChatList.PsaAlert.\(type)" - if let string = strongSelf.presentationData.strings.primaryComponent.dict[key] { - text = string - } else if let string = strongSelf.presentationData.strings.secondaryComponent?.dict[key] { - text = string - } - - controller.displayPromoAnnouncement(text: text) - let _ = ApplicationSpecificNotice.setPsaAcknowledgment(accountManager: strongSelf.context.sharedContext.accountManager, peerId: peer.id).start() - } - }) + strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(peer.id), activateInput: activateInput && !peer.isDeleted, scrollToEndIfExists: scrollToEndIfExists, animated: !scrollToEndIfExists, options: strongSelf.groupId == PeerGroupId.root ? [.removeOnMasterDetails] : [], parentGroupId: strongSelf.groupId, completion: { [weak self] controller in + self?.chatListDisplayNode.containerNode.currentItemNode.clearHighlightAnimated(true) + if let promoInfo = promoInfo { + switch promoInfo { + case .proxy: + let _ = (ApplicationSpecificNotice.getProxyAdsAcknowledgment(accountManager: strongSelf.context.sharedContext.accountManager) + |> deliverOnMainQueue).start(next: { value in + guard let strongSelf = self else { + return } - } - })) - - if activateInput { - strongSelf.prepareRandomGreetingSticker() + if !value { + controller.displayPromoAnnouncement(text: strongSelf.presentationData.strings.DialogList_AdNoticeAlert) + let _ = ApplicationSpecificNotice.setProxyAdsAcknowledgment(accountManager: strongSelf.context.sharedContext.accountManager).start() + } + }) + case let .psa(type, _): + let _ = (ApplicationSpecificNotice.getPsaAcknowledgment(accountManager: strongSelf.context.sharedContext.accountManager, peerId: peer.id) + |> deliverOnMainQueue).start(next: { value in + guard let strongSelf = self else { + return + } + if !value { + var text = strongSelf.presentationData.strings.ChatList_GenericPsaAlert + let key = "ChatList.PsaAlert.\(type)" + if let string = strongSelf.presentationData.strings.primaryComponent.dict[key] { + text = string + } else if let string = strongSelf.presentationData.strings.secondaryComponent?.dict[key] { + text = string + } + + controller.displayPromoAnnouncement(text: text) + let _ = ApplicationSpecificNotice.setPsaAcknowledgment(accountManager: strongSelf.context.sharedContext.accountManager, peerId: peer.id).start() + } + }) } } - }) + })) } } } @@ -685,7 +692,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController if let layout = strongSelf.validLayout, case .regular = layout.metrics.widthClass { scrollToEndIfExists = true } - strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(actualPeerId), subject: .message(id: messageId, highlight: true), purposefulAction: { + strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(actualPeerId), subject: .message(id: messageId, highlight: true, timecode: nil), purposefulAction: { if deactivateOnAction { self?.deactivateSearch(animated: false) } @@ -737,7 +744,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController actionSheet?.dismissAnimated() if let strongSelf = self { - let _ = removeRecentPeer(account: strongSelf.context.account, peerId: peer.id).start() + let _ = strongSelf.context.engine.peers.removeRecentPeer(peerId: peer.id).start() } }) ]), @@ -855,7 +862,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController } else { var subject: ChatControllerSubject? if case let .search(messageId) = source, let id = messageId { - subject = .message(id: id, highlight: false) + subject = .message(id: id, highlight: false, timecode: nil) } let chatController = strongSelf.context.sharedContext.makeChatController(context: strongSelf.context, chatLocation: .peer(peer.id), subject: subject, botStart: nil, mode: .standard(previewing: true)) chatController.canReadHistory.set(false) @@ -885,7 +892,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController }) |> mapToSignal { selectedPeerIdsAndFilterId -> Signal<(ChatListSelectionOptions, Set)?, NoError> in if let (selectedPeerIds, filterId) = selectedPeerIdsAndFilterId { - return chatListSelectionOptions(postbox: context.account.postbox, peerIds: selectedPeerIds, filterId: filterId) + return chatListSelectionOptions(context: context, peerIds: selectedPeerIds, filterId: filterId) |> map { options -> (ChatListSelectionOptions, Set)? in return (options, selectedPeerIds) } @@ -894,6 +901,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController } } + let previousToolbarValue = Atomic(value: nil) self.stateDisposable.set(combineLatest(queue: .mainQueue(), self.presentationDataValue.get(), peerIdsAndOptions @@ -920,7 +928,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController } if archiveEnabled { for peerId in peerIds { - if peerId == PeerId(namespace: Namespaces.Peer.CloudUser, id: 777000) { + if peerId == PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt32Value(777000)) { archiveEnabled = false break } else if peerId == strongSelf.context.account.peerId { @@ -944,7 +952,12 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController toolbar = Toolbar(leftAction: leftAction, rightAction: ToolbarAction(title: presentationData.strings.Common_Delete, isEnabled: options.delete), middleAction: middleAction) } } - strongSelf.setToolbar(toolbar, transition: .animated(duration: 0.3, curve: .easeInOut)) + var transition: ContainedViewLayoutTransition = .immediate + let previousToolbar = previousToolbarValue.swap(toolbar) + if (previousToolbar == nil) != (toolbar == nil) { + transition = .animated(duration: 0.3, curve: .easeInOut) + } + strongSelf.setToolbar(toolbar, transition: transition) })) self.tabContainerNode.tabSelected = { [weak self] id in @@ -969,7 +982,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController guard let strongSelf = self else { return } - let _ = (currentChatListFilters(postbox: strongSelf.context.account.postbox) + let _ = (strongSelf.context.engine.peers.currentChatListFilters() |> deliverOnMainQueue).start(next: { [weak self] filters in guard let strongSelf = self else { return @@ -983,7 +996,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController guard let strongSelf = self else { return } - let _ = (currentChatListFilters(postbox: strongSelf.context.account.postbox) + let _ = (strongSelf.context.engine.peers.currentChatListFilters() |> deliverOnMainQueue).start(next: { presetList in guard let strongSelf = self else { return @@ -1011,7 +1024,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController guard let strongSelf = self else { return } - let _ = (currentChatListFilters(postbox: strongSelf.context.account.postbox) + let _ = (strongSelf.context.engine.peers.currentChatListFilters() |> deliverOnMainQueue).start(next: { presetList in guard let strongSelf = self else { return @@ -1019,7 +1032,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController var found = false for filter in presetList { if filter.id == id { - let _ = (currentChatListFilters(postbox: strongSelf.context.account.postbox) + let _ = (strongSelf.context.engine.peers.currentChatListFilters() |> deliverOnMainQueue).start(next: { filters in guard let strongSelf = self else { return @@ -1103,7 +1116,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController super.displayNodeDidLoad() Queue.mainQueue().after(1.0) { - self.prepareRandomGreetingSticker() + self.context.prefetchManager?.prepareNextGreetingSticker() } } @@ -1124,7 +1137,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController return } let count = ChatControllerCount.with({ $0 }) - if count != 0 { + if count > 1 { strongSelf.present(textAlertController(context: strongSelf.context, title: "", text: "ChatControllerCount \(count)", actions: [TextAlertAction(type: .defaultAction, title: "OK", action: {})]), in: .window(.root)) } }) @@ -1157,7 +1170,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController if !self.didSuggestLocalization { self.didSuggestLocalization = true - let network = self.context.account.network + let context = self.context let signal = combineLatest(self.context.sharedContext.accountManager.transaction { transaction -> String in let languageCode: String if let current = transaction.getSharedData(SharedDataKeys.localizationSettings) as? LocalizationSettings { @@ -1183,7 +1196,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController guard let suggestedLocalization = value.1, !suggestedLocalization.isSeen && suggestedLocalization.languageCode != "en" && suggestedLocalization.languageCode != value.0 else { return .single(nil) } - return suggestedLocalizationInfo(network: network, languageCode: suggestedLocalization.languageCode, extractKeys: LanguageSuggestionControllerStrings.keys) + return context.engine.localization.suggestedLocalizationInfo(languageCode: suggestedLocalization.languageCode, extractKeys: LanguageSuggestionControllerStrings.keys) |> map({ suggestedLocalization -> (String, SuggestedLocalizationInfo)? in return (value.0, suggestedLocalization) }) @@ -1200,11 +1213,11 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController } }) { strongSelf.present(controller, in: .window(.root)) - _ = markSuggestedLocalizationAsSeenInteractively(postbox: strongSelf.context.account.postbox, languageCode: suggestedLocalization.languageCode).start() + _ = strongSelf.context.engine.localization.markSuggestedLocalizationAsSeenInteractively(languageCode: suggestedLocalization.languageCode).start() } })) - self.suggestAutoarchiveDisposable.set((getServerProvidedSuggestions(postbox: self.context.account.postbox) + self.suggestAutoarchiveDisposable.set((getServerProvidedSuggestions(account: self.context.account) |> deliverOnMainQueue).start(next: { [weak self] values in guard let strongSelf = self else { return @@ -1259,6 +1272,14 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController return } if !strongSelf.chatListDisplayNode.didBeginSelectingChatsWhileEditing { + var isEditing = false + strongSelf.chatListDisplayNode.containerNode.updateState { state in + isEditing = state.editing + return state + } + if !isEditing { + strongSelf.editPressed() + } strongSelf.chatListDisplayNode.didBeginSelectingChatsWhileEditing = true if let layout = strongSelf.validLayout { strongSelf.updateLayout(layout: layout, transition: .animated(duration: 0.2, curve: .easeInOut)) @@ -1279,7 +1300,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController } |> take(1) - let initializedFilters = updatedChatListFiltersInfo(postbox: self.context.account.postbox) + let initializedFilters = self.context.engine.peers.updatedChatListFiltersInfo() |> mapToSignal { (filters, isInitialized) -> Signal in if isInitialized { return .single(!filters.isEmpty) @@ -1315,7 +1336,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController let text: String if hasFilters { text = strongSelf.presentationData.strings.ChatList_TabIconFoldersTooltipNonEmptyFolders - let _ = markChatListFeaturedFiltersAsSeen(postbox: strongSelf.context.account.postbox).start() + let _ = strongSelf.context.engine.peers.markChatListFeaturedFiltersAsSeen().start() return } else { text = strongSelf.presentationData.strings.ChatList_TabIconFoldersTooltipEmptyFolders @@ -1390,8 +1411,10 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController tabContainerOffset += layout.statusBarHeight ?? 0.0 tabContainerOffset += 44.0 + 20.0 } + + let navigationBarHeight = self.navigationBar?.frame.maxY ?? 0.0 - transition.updateFrame(node: self.tabContainerNode, frame: CGRect(origin: CGPoint(x: 0.0, y: self.visualNavigationInsetHeight - self.additionalHeight - 46.0 + tabContainerOffset), size: CGSize(width: layout.size.width, height: 46.0))) + transition.updateFrame(node: self.tabContainerNode, frame: CGRect(origin: CGPoint(x: 0.0, y: navigationBarHeight - self.additionalNavigationBarHeight - 46.0 + tabContainerOffset), size: CGSize(width: layout.size.width, height: 46.0))) self.tabContainerNode.update(size: CGSize(width: layout.size.width, height: 46.0), sideInset: layout.safeInsets.left, filters: self.tabContainerData?.0 ?? [], selectedFilter: self.chatListDisplayNode.containerNode.currentItemFilter, isReordering: self.chatListDisplayNode.isReorderingFilters || (self.chatListDisplayNode.containerNode.currentItemNode.currentState.editing && !self.chatListDisplayNode.didBeginSelectingChatsWhileEditing), isEditing: false, transitionFraction: self.chatListDisplayNode.containerNode.transitionFraction, presentationData: self.presentationData, transition: .animated(duration: 0.4, curve: .spring)) if let tabContainerData = self.tabContainerData { self.chatListDisplayNode.inlineTabContainerNode.isHidden = !tabContainerData.1 || tabContainerData.0.count <= 1 @@ -1400,7 +1423,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController } self.chatListDisplayNode.inlineTabContainerNode.update(size: CGSize(width: layout.size.width, height: 40.0), sideInset: layout.safeInsets.left, filters: self.tabContainerData?.0 ?? [], selectedFilter: self.chatListDisplayNode.containerNode.currentItemFilter, isReordering: self.chatListDisplayNode.isReorderingFilters || (self.chatListDisplayNode.containerNode.currentItemNode.currentState.editing && !self.chatListDisplayNode.didBeginSelectingChatsWhileEditing), isEditing: false, transitionFraction: self.chatListDisplayNode.containerNode.transitionFraction, presentationData: self.presentationData, transition: .animated(duration: 0.4, curve: .spring)) - self.chatListDisplayNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationInsetHeight, visualNavigationHeight: self.visualNavigationInsetHeight, cleanNavigationBarHeight: self.cleanNavigationHeight, transition: transition) + self.chatListDisplayNode.containerLayoutUpdated(layout, navigationBarHeight: self.cleanNavigationHeight, visualNavigationHeight: navigationBarHeight, cleanNavigationBarHeight: self.cleanNavigationHeight, transition: transition) } override public func navigationStackConfigurationUpdated(next: [ViewController]) { @@ -1411,10 +1434,10 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController let editItem = UIBarButtonItem(title: self.presentationData.strings.Common_Done, style: .done, target: self, action: #selector(self.donePressed)) editItem.accessibilityLabel = self.presentationData.strings.Common_Done if case .root = self.groupId, self.filter == nil { - self.navigationItem.leftBarButtonItem = editItem + self.navigationItem.setLeftBarButton(editItem, animated: true) (self.navigationController as? NavigationController)?.updateMasterDetailsBlackout(.details, transition: .animated(duration: 0.5, curve: .spring)) } else { - self.navigationItem.rightBarButtonItem = editItem + self.navigationItem.setRightBarButton(editItem, animated: true) (self.navigationController as? NavigationController)?.updateMasterDetailsBlackout(.master, transition: .animated(duration: 0.5, curve: .spring)) } self.searchContentNode?.setIsEnabled(false, animated: true) @@ -1435,13 +1458,6 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController @objc private func donePressed() { self.reorderingDonePressed() - let 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, self.filter == nil { - self.navigationItem.leftBarButtonItem = editItem - } else { - self.navigationItem.rightBarButtonItem = editItem - } (self.navigationController as? NavigationController)?.updateMasterDetailsBlackout(nil, transition: .animated(duration: 0.4, curve: .spring)) self.searchContentNode?.setIsEnabled(true, animated: true) self.chatListDisplayNode.didBeginSelectingChatsWhileEditing = false @@ -1479,7 +1495,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController } if let reorderedFilterIds = reorderedFilterIdsValue { - let _ = (updateChatListFiltersInteractively(postbox: self.context.account.postbox, { stateFilters in + let _ = (self.context.engine.peers.updateChatListFiltersInteractively { stateFilters in var updatedFilters: [ChatListFilter] = [] for id in reorderedFilterIds { if let index = stateFilters.firstIndex(where: { $0.id == id }) { @@ -1494,7 +1510,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController } }) return updatedFilters - }) + } |> deliverOnMainQueue).start(completed: { [weak self] in guard let strongSelf = self else { return @@ -1527,7 +1543,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController } |> distinctUntilChanged - let filterItems = chatListFilterItems(postbox: self.context.account.postbox) + let filterItems = chatListFilterItems(context: self.context) var notifiedFirstUpdate = false self.filterDisposable.set((combineLatest(queue: .mainQueue(), context.account.postbox.combinedView(keys: [ @@ -1590,17 +1606,21 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController let isEmpty = resolvedItems.count <= 1 || displayTabsAtBottom + let animated = strongSelf.didSetupTabs + strongSelf.didSetupTabs = true + if wasEmpty != isEmpty, strongSelf.displayNavigationBar { - strongSelf.navigationBar?.setSecondaryContentNode(isEmpty ? nil : strongSelf.tabContainerNode) + strongSelf.navigationBar?.setSecondaryContentNode(isEmpty ? nil : strongSelf.tabContainerNode, animated: false) if let parentController = strongSelf.parent as? TabBarController { - parentController.navigationBar?.setSecondaryContentNode(isEmpty ? nil : strongSelf.tabContainerNode) + parentController.navigationBar?.setSecondaryContentNode(isEmpty ? nil : strongSelf.tabContainerNode, animated: animated) } } if let layout = strongSelf.validLayout { if wasEmpty != isEmpty { - strongSelf.containerLayoutUpdated(layout, transition: .immediate) - (strongSelf.parent as? TabBarController)?.updateLayout() + let transition: ContainedViewLayoutTransition = animated ? .animated(duration: 0.2, curve: .easeInOut) : .immediate + strongSelf.containerLayoutUpdated(layout, transition: transition) + (strongSelf.parent as? TabBarController)?.updateLayout(transition: transition) } else { strongSelf.tabContainerNode.update(size: CGSize(width: layout.size.width, height: 46.0), sideInset: layout.safeInsets.left, filters: resolvedItems, selectedFilter: selectedEntryId, isReordering: strongSelf.chatListDisplayNode.isReorderingFilters || (strongSelf.chatListDisplayNode.containerNode.currentItemNode.currentState.editing && !strongSelf.chatListDisplayNode.didBeginSelectingChatsWhileEditing), isEditing: false, transitionFraction: strongSelf.chatListDisplayNode.containerNode.transitionFraction, presentationData: strongSelf.presentationData, transition: .animated(duration: 0.4, curve: .spring)) strongSelf.chatListDisplayNode.inlineTabContainerNode.update(size: CGSize(width: layout.size.width, height: 40.0), sideInset: layout.safeInsets.left, filters: resolvedItems, selectedFilter: selectedEntryId, isReordering: strongSelf.chatListDisplayNode.isReorderingFilters || (strongSelf.chatListDisplayNode.containerNode.currentItemNode.currentState.editing && !strongSelf.chatListDisplayNode.didBeginSelectingChatsWhileEditing), isEditing: false, transitionFraction: strongSelf.chatListDisplayNode.containerNode.transitionFraction, presentationData: strongSelf.presentationData, transition: .animated(duration: 0.4, curve: .spring)) @@ -1632,7 +1652,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController } } - let _ = (currentChatListFilters(postbox: self.context.account.postbox) + let _ = (self.context.engine.peers.currentChatListFilters() |> deliverOnMainQueue).start(next: { [weak self] filters in guard let strongSelf = self else { return @@ -1689,7 +1709,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController } } - let _ = updateChatListFiltersInteractively(postbox: strongSelf.context.account.postbox, { filters in + let _ = (strongSelf.context.engine.peers.updateChatListFiltersInteractively { filters in return filters.filter({ $0.id != id }) }).start() } @@ -1992,11 +2012,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController |> delay(0.8, queue: Queue.mainQueue()) let progressDisposable = progressSignal.start() - let signal: Signal = strongSelf.context.account.postbox.transaction { transaction -> Void in - for peerId in peerIds { - removePeerChat(account: context.account, transaction: transaction, mediaBox: context.account.postbox.mediaBox, peerId: peerId, reportChatSpam: false, deleteGloballyIfPossible: peerId.namespace == Namespaces.Peer.SecretChat) - } - } + let signal: Signal = strongSelf.context.engine.peers.removePeerChats(peerIds: Array(peerIds)) |> afterDisposed { Queue.mainQueue().async { progressDisposable.dispose() @@ -2275,7 +2291,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController return false } if value == .commit { - let _ = clearHistoryInteractively(postbox: strongSelf.context.account.postbox, peerId: peerId, type: type).start(completed: { + let _ = strongSelf.context.engine.messages.clearHistoryInteractively(peerId: peerId, type: type).start(completed: { guard let strongSelf = self else { return } @@ -2436,7 +2452,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController guard let strongSelf = self else { return } - let _ = requestUpdatePeerIsBlocked(account: strongSelf.context.account, peerId: peer.peerId, isBlocked: true).start() + let _ = strongSelf.context.engine.privacy.requestUpdatePeerIsBlocked(peerId: peer.peerId, isBlocked: true).start() }) } })) @@ -2681,7 +2697,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController if let channel = chatPeer as? TelegramChannel { strongSelf.context.peerChannelMemberCategoriesContextsManager.externallyRemoved(peerId: channel.id, memberId: strongSelf.context.account.peerId) } - let _ = removePeerChat(account: strongSelf.context.account, peerId: peerId, reportChatSpam: false, deleteGloballyIfPossible: deleteGloballyIfPossible).start(completed: { + let _ = strongSelf.context.engine.peers.removePeerChat(peerId: peerId, reportChatSpam: false, deleteGloballyIfPossible: deleteGloballyIfPossible).start(completed: { guard let strongSelf = self else { return } @@ -2743,36 +2759,14 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController } } - private func prepareRandomGreetingSticker() { - let context = self.context - self.preloadedSticker.set(.single(nil) - |> then(randomGreetingSticker(account: context.account) - |> map { item in - return item?.file - })) - - self.preloadStickerDisposable.set((self.preloadedSticker.get() - |> mapToSignal { sticker -> Signal in - if let sticker = sticker { - let _ = freeMediaFileInteractiveFetched(account: context.account, fileReference: .standalone(media: sticker)).start() - return chatMessageAnimationData(postbox: context.account.postbox, resource: sticker.resource, fitzModifier: nil, width: 384, height: 384, synchronousLoad: false) - |> mapToSignal { _ -> Signal in - return .complete() - } - } else { - return .complete() - } - }).start()) - } - override public func tabBarDisabledAction() { self.donePressed() } override public func tabBarItemContextAction(sourceNode: ContextExtractedContentContainingNode, gesture: ContextGesture) { let _ = (combineLatest(queue: .mainQueue(), - currentChatListFilters(postbox: self.context.account.postbox), - chatListFilterItems(postbox: self.context.account.postbox) + self.context.engine.peers.currentChatListFilters(), + chatListFilterItems(context: self.context) |> take(1) ) |> deliverOnMainQueue).start(next: { [weak self] presetList, filterItemsAndTotalCount in @@ -2780,7 +2774,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController return } - let _ = markChatListFeaturedFiltersAsSeen(postbox: strongSelf.context.account.postbox).start() + let _ = strongSelf.context.engine.peers.markChatListFeaturedFiltersAsSeen().start() let (_, filterItems) = filterItemsAndTotalCount diff --git a/submodules/ChatListUI/Sources/ChatListControllerNode.swift b/submodules/ChatListUI/Sources/ChatListControllerNode.swift index e12e902723..ae585ccff7 100644 --- a/submodules/ChatListUI/Sources/ChatListControllerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListControllerNode.swift @@ -179,10 +179,10 @@ private final class ChatListShimmerNode: ASDisplayNode { let chatListPresentationData = ChatListPresentationData(theme: presentationData.theme, fontSize: presentationData.chatFontSize, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameSortOrder: presentationData.nameSortOrder, nameDisplayOrder: presentationData.nameDisplayOrder, disableAnimations: true) - let peer1 = TelegramUser(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: 1), accessHash: nil, firstName: "FirstName", lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: []) + let peer1 = TelegramUser(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt32Value(0)), accessHash: nil, firstName: "FirstName", lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: []) let timestamp1: Int32 = 100000 let peers = SimpleDictionary() - let interaction = ChatListNodeInteraction(activateSearch: {}, peerSelected: { _, _ in }, disabledPeerSelected: { _ in }, togglePeerSelected: { _ in }, additionalCategorySelected: { _ in + let interaction = ChatListNodeInteraction(activateSearch: {}, peerSelected: { _, _ in }, disabledPeerSelected: { _ in }, togglePeerSelected: { _ in }, togglePeersSelection: { _, _ in }, additionalCategorySelected: { _ in }, messageSelected: { _, _, _ in}, groupSelected: { _ in }, addContact: { _ in }, setPeerIdWithRevealedOptions: { _, _ in }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, deletePeer: { _, _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in}, toggleArchivedFolderHiddenByDefault: {}, hidePsa: { _ in }, activateChatPreview: { _, _, gesture in gesture?.cancel() }, present: { _ in }) @@ -275,7 +275,7 @@ private final class ChatListContainerItemNode: ASDisplayNode { self.becameEmpty = becameEmpty self.emptyAction = emptyAction - self.listNode = ChatListNode(context: context, groupId: groupId, chatListFilter: filter, previewing: previewing, fillPreloadItems: 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.listNode = ChatListNode(context: context, groupId: groupId, chatListFilter: filter, previewing: previewing, fillPreloadItems: controlsHistoryPreload, mode: .chatList, theme: presentationData.theme, fontSize: presentationData.listsFontSize, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameSortOrder: presentationData.nameSortOrder, nameDisplayOrder: presentationData.nameDisplayOrder, disableAnimations: true) super.init() @@ -372,7 +372,7 @@ private final class ChatListContainerItemNode: ASDisplayNode { return presentationData.strings.VoiceOver_ScrollStatus(row, count).0 } - self.listNode.updateThemeAndStrings(theme: presentationData.theme, fontSize: presentationData.listsFontSize, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameSortOrder: presentationData.nameSortOrder, nameDisplayOrder: presentationData.nameDisplayOrder, disableAnimations: presentationData.disableAnimations) + self.listNode.updateThemeAndStrings(theme: presentationData.theme, fontSize: presentationData.listsFontSize, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameSortOrder: presentationData.nameSortOrder, nameDisplayOrder: presentationData.nameDisplayOrder, disableAnimations: true) self.emptyNode?.updateThemeAndStrings(theme: presentationData.theme, strings: presentationData.strings) } diff --git a/submodules/ChatListUI/Sources/ChatListFilterPresetCategoryItem.swift b/submodules/ChatListUI/Sources/ChatListFilterPresetCategoryItem.swift index 52751ffef9..95eafaf35f 100644 --- a/submodules/ChatListUI/Sources/ChatListFilterPresetCategoryItem.swift +++ b/submodules/ChatListUI/Sources/ChatListFilterPresetCategoryItem.swift @@ -465,7 +465,7 @@ class ChatListFilterPresetCategoryItemNode: ItemListRevealOptionsItemNode, ItemL } } - override func header() -> ListViewItemHeader? { + override func headers() -> [ListViewItemHeader]? { return nil } } diff --git a/submodules/ChatListUI/Sources/ChatListFilterPresetController.swift b/submodules/ChatListUI/Sources/ChatListFilterPresetController.swift index 1a4932dd6b..aedd7d3f15 100644 --- a/submodules/ChatListUI/Sources/ChatListFilterPresetController.swift +++ b/submodules/ChatListUI/Sources/ChatListFilterPresetController.swift @@ -624,7 +624,7 @@ private func internalChatListFilterAddChatsController(context: AccountContext, f } if applyAutomatically { - let _ = (updateChatListFiltersInteractively(postbox: context.account.postbox, { filters in + let _ = (context.engine.peers.updateChatListFiltersInteractively { filters in var filters = filters for i in 0 ..< filters.count { if filters[i].id == filter.id { @@ -634,7 +634,7 @@ private func internalChatListFilterAddChatsController(context: AccountContext, f } } return filters - }) + } |> deliverOnMainQueue).start(next: { _ in controller?.dismiss() }) @@ -702,7 +702,7 @@ private func internalChatListFilterExcludeChatsController(context: AccountContex excludePeers.sort() if applyAutomatically { - let _ = (updateChatListFiltersInteractively(postbox: context.account.postbox, { filters in + let _ = (context.engine.peers.updateChatListFiltersInteractively { filters in var filters = filters for i in 0 ..< filters.count { if filters[i].id == filter.id { @@ -714,7 +714,7 @@ private func internalChatListFilterExcludeChatsController(context: AccountContex } } return filters - }) + } |> deliverOnMainQueue).start(next: { _ in controller?.dismiss() }) @@ -835,7 +835,7 @@ func chatListFilterPresetController(context: AccountContext, currentPreset: Chat includePeers.setPeers(state.additionallyIncludePeers) let filter = ChatListFilter(id: currentPreset?.id ?? -1, title: state.name, emoticon: currentPreset?.emoticon, data: ChatListFilterData(categories: state.includeCategories, excludeMuted: state.excludeMuted, excludeRead: state.excludeRead, excludeArchived: state.excludeArchived, includePeers: includePeers, excludePeers: state.additionallyExcludePeers)) - let _ = (currentChatListFilters(postbox: context.account.postbox) + let _ = (context.engine.peers.currentChatListFilters() |> deliverOnMainQueue).start(next: { filters in let controller = internalChatListFilterAddChatsController(context: context, filter: filter, allFilters: filters, applyAutomatically: false, updated: { filter in skipStateAnimation = true @@ -856,7 +856,7 @@ func chatListFilterPresetController(context: AccountContext, currentPreset: Chat includePeers.setPeers(state.additionallyIncludePeers) let filter = ChatListFilter(id: currentPreset?.id ?? -1, title: state.name, emoticon: currentPreset?.emoticon, data: ChatListFilterData(categories: state.includeCategories, excludeMuted: state.excludeMuted, excludeRead: state.excludeRead, excludeArchived: state.excludeArchived, includePeers: includePeers, excludePeers: state.additionallyExcludePeers)) - let _ = (currentChatListFilters(postbox: context.account.postbox) + let _ = (context.engine.peers.currentChatListFilters() |> deliverOnMainQueue).start(next: { filters in let controller = internalChatListFilterExcludeChatsController(context: context, filter: filter, allFilters: filters, applyAutomatically: false, updated: { filter in skipStateAnimation = true @@ -985,12 +985,12 @@ func chatListFilterPresetController(context: AccountContext, currentPreset: Chat var attemptNavigationImpl: (() -> Bool)? let applyImpl: (() -> Void)? = { let state = stateValue.with { $0 } - let _ = (updateChatListFiltersInteractively(postbox: context.account.postbox, { filters in + let _ = (context.engine.peers.updateChatListFiltersInteractively { filters in var includePeers = ChatListFilterIncludePeers() includePeers.setPeers(state.additionallyIncludePeers) var updatedFilter = ChatListFilter(id: currentPreset?.id ?? -1, title: state.name, emoticon: currentPreset?.emoticon, data: ChatListFilterData(categories: state.includeCategories, excludeMuted: state.excludeMuted, excludeRead: state.excludeRead, excludeArchived: state.excludeArchived, includePeers: includePeers, excludePeers: state.additionallyExcludePeers)) if currentPreset == nil { - updatedFilter.id = generateNewChatListFilterId(filters: filters) + updatedFilter.id = context.engine.peers.generateNewChatListFilterId(filters: filters) } var filters = filters if let _ = currentPreset { @@ -1017,7 +1017,7 @@ func chatListFilterPresetController(context: AccountContext, currentPreset: Chat filters.append(updatedFilter) } return filters - }) + } |> deliverOnMainQueue).start(next: { filters in updated(filters) dismissImpl?() diff --git a/submodules/ChatListUI/Sources/ChatListFilterPresetListController.swift b/submodules/ChatListUI/Sources/ChatListFilterPresetListController.swift index 9142a50166..75cb00bd1f 100644 --- a/submodules/ChatListUI/Sources/ChatListFilterPresetListController.swift +++ b/submodules/ChatListUI/Sources/ChatListFilterPresetListController.swift @@ -249,12 +249,12 @@ public func chatListFilterPresetListController(context: AccountContext, mode: Ch let arguments = ChatListFilterPresetListControllerArguments(context: context, addSuggestedPresed: { title, data in - let _ = (updateChatListFiltersInteractively(postbox: context.account.postbox, { filters in + let _ = (context.engine.peers.updateChatListFiltersInteractively { filters in var filters = filters - let id = generateNewChatListFilterId(filters: filters) + let id = context.engine.peers.generateNewChatListFilterId(filters: filters) filters.insert(ChatListFilter(id: id, title: title, emoticon: nil, data: data), at: 0) return filters - }) + } |> deliverOnMainQueue).start(next: { _ in }) }, openPreset: { preset in @@ -279,13 +279,13 @@ public func chatListFilterPresetListController(context: AccountContext, mode: Ch ActionSheetButtonItem(title: presentationData.strings.ChatList_RemoveFolderAction, color: .destructive, action: { [weak actionSheet] in actionSheet?.dismissAnimated() - let _ = (updateChatListFiltersInteractively(postbox: context.account.postbox, { filters in + let _ = (context.engine.peers.updateChatListFiltersInteractively { filters in var filters = filters if let index = filters.firstIndex(where: { $0.id == id }) { filters.remove(at: index) } return filters - }) + } |> deliverOnMainQueue).start() }) ]), @@ -300,30 +300,12 @@ public func chatListFilterPresetListController(context: AccountContext, mode: Ch let chatCountCache = Atomic<[ChatListFilterData: Int]>(value: [:]) - let filtersWithCountsSignal = updatedChatListFilters(postbox: context.account.postbox) + let filtersWithCountsSignal = context.engine.peers.updatedChatListFilters() |> distinctUntilChanged |> mapToSignal { filters -> Signal<[(ChatListFilter, Int)], NoError> in return .single(filters.map { filter -> (ChatListFilter, Int) in return (filter, 0) }) - /*return context.account.postbox.transaction { transaction -> [(ChatListFilter, Int)] in - return filters.map { filter -> (ChatListFilter, Int) in - let count: Int - if let cachedValue = chatCountCache.with({ dict -> Int? in - return dict[filter.data] - }) { - count = cachedValue - } else { - count = transaction.getChatCountMatchingPredicate(chatListFilterPredicate(filter: filter.data)) - let _ = chatCountCache.modify { dict in - var dict = dict - dict[filter.data] = count - return dict - } - } - return (filter, count) - } - }*/ } let featuredFilters = context.account.postbox.preferencesView(keys: [PreferencesKeys.chatListFiltersFeaturedState]) @@ -368,7 +350,7 @@ public func chatListFilterPresetListController(context: AccountContext, mode: Ch |> take(1) |> deliverOnMainQueue).start(next: { [weak updatedFilterOrder] updatedFilterOrderValue in if let updatedFilterOrderValue = updatedFilterOrderValue { - let _ = (updateChatListFiltersInteractively(postbox: context.account.postbox, { filters in + let _ = (context.engine.peers.updateChatListFiltersInteractively { filters in var updatedFilters: [ChatListFilter] = [] for id in updatedFilterOrderValue { if let index = filters.firstIndex(where: { $0.id == id }) { @@ -382,7 +364,7 @@ public func chatListFilterPresetListController(context: AccountContext, mode: Ch } return updatedFilters - }) + } |> deliverOnMainQueue).start(next: { _ in filtersWithCounts.set(filtersWithCountsSignal) let _ = (filtersWithCounts.get() diff --git a/submodules/ChatListUI/Sources/ChatListRecentPeersListItem.swift b/submodules/ChatListUI/Sources/ChatListRecentPeersListItem.swift index 175f6f607d..aceec993d0 100644 --- a/submodules/ChatListUI/Sources/ChatListRecentPeersListItem.swift +++ b/submodules/ChatListUI/Sources/ChatListRecentPeersListItem.swift @@ -154,9 +154,9 @@ class ChatListRecentPeersListItemNode: ListViewItemNode { self.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration * 0.5, removeOnCompletion: false) } - override public func header() -> ListViewItemHeader? { + override public func headers() -> [ListViewItemHeader]? { if let item = self.item { - return item.header + return item.header.flatMap { [$0] } } else { return nil } diff --git a/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift b/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift index 05631316a2..041708249f 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift @@ -30,6 +30,7 @@ import InstantPageUI import ChatInterfaceState import ShareController import UndoUI +import TextFormat private enum ChatListTokenId: Int32 { case filter @@ -107,6 +108,8 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo private var suggestedFilters: [ChatListSearchFilter]? private let suggestedFiltersDisposable = MetaDisposable() + private var shareStatusDisposable: MetaDisposable? + private var stateValue = ChatListSearchContainerNodeSearchState() private let statePromise = ValuePromise() @@ -160,11 +163,11 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo addAppLogEvent(postbox: context.account.postbox, type: "search_global_open_message", peerId: peer.id, data: .dictionary(["msg_id": .number(Double(messageId.id))])) } }, openUrl: { [weak self] url in - openUserGeneratedUrl(context: context, url: url, concealed: false, present: { c in + openUserGeneratedUrl(context: context, peerId: nil, url: url, concealed: false, present: { c in present(c, nil) }, openResolved: { [weak self] resolved in context.sharedContext.openResolvedUrl(resolved, context: context, urlContext: .generic, navigationController: navigationController, openPeer: { peerId, navigation in - // self?.openPeer(peerId: peerId, navigation: navigation) + }, sendFile: nil, sendSticker: nil, requestMessageActionUrlAuth: nil, @@ -188,7 +191,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo guard let strongSelf = self else { return } - let _ = (clearRecentlySearchedPeers(postbox: strongSelf.context.account.postbox) + let _ = (strongSelf.context.engine.peers.clearRecentlySearchedPeers() |> deliverOnMainQueue).start() }) ]), ActionSheetItemGroup(items: [ @@ -415,6 +418,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo self.activeActionDisposable.dispose() self.presentationDataDisposable?.dispose() self.suggestedFiltersDisposable.dispose() + self.shareStatusDisposable?.dispose() } private func updateState(_ f: (ChatListSearchContainerNodeSearchState) -> ChatListSearchContainerNodeSearchState) { @@ -844,7 +848,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo items.append(ActionSheetButtonItem(title: globalTitle, color: .destructive, action: { [weak actionSheet] in actionSheet?.dismissAnimated() if let strongSelf = self { - let _ = deleteMessagesInteractively(account: strongSelf.context.account, messageIds: Array(messageIds), type: .forEveryone).start() + let _ = strongSelf.context.engine.messages.deleteMessagesInteractively(messageIds: Array(messageIds), type: .forEveryone).start() strongSelf.updateState { state in return state.withUpdatedSelectedMessageIds(nil) @@ -867,7 +871,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo items.append(ActionSheetButtonItem(title: localOptionText, color: .destructive, action: { [weak actionSheet] in actionSheet?.dismissAnimated() if let strongSelf = self { - let _ = deleteMessagesInteractively(account: strongSelf.context.account, messageIds: Array(messageIds), type: .forLocalPeer).start() + let _ = strongSelf.context.engine.messages.deleteMessagesInteractively(messageIds: Array(messageIds), type: .forLocalPeer).start() strongSelf.updateState { state in return state.withUpdatedSelectedMessageIds(nil) @@ -902,7 +906,83 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo } }).start() - let peerSelectionController = self.context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: self.context, filter: [.onlyWriteable, .excludeDisabled])) + let peerSelectionController = self.context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: self.context, filter: [.onlyWriteable, .excludeDisabled], multipleSelection: true)) + peerSelectionController.multiplePeersSelected = { [weak self, weak peerSelectionController] peers, messageText in + guard let strongSelf = self, let strongController = peerSelectionController else { + return + } + strongController.dismiss() + + for peer in peers { + var result: [EnqueueMessage] = [] + if messageText.string.count > 0 { + let inputText = convertMarkdownToAttributes(messageText) + for text in breakChatInputText(trimChatInputText(inputText)) { + if text.length != 0 { + var attributes: [MessageAttribute] = [] + let entities = generateTextEntities(text.string, enabledTypes: .all, currentEntities: generateChatInputTextEntities(text)) + if !entities.isEmpty { + attributes.append(TextEntitiesMessageAttribute(entities: entities)) + } + result.append(.message(text: text.string, attributes: attributes, mediaReference: nil, replyToMessageId: nil, localGroupingKey: nil, correlationId: nil)) + } + } + } + + result.append(contentsOf: messageIds.map { messageId -> EnqueueMessage in + return .forward(source: messageId, grouping: .auto, attributes: [], correlationId: nil) + }) + + let _ = (enqueueMessages(account: strongSelf.context.account, peerId: peer.id, messages: result) + |> deliverOnMainQueue).start(next: { 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) + }) + if strongSelf.shareStatusDisposable == nil { + strongSelf.shareStatusDisposable = MetaDisposable() + } + strongSelf.shareStatusDisposable?.set((combineLatest(signals) + |> deliverOnMainQueue).start()) + } + }) + + let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } + let text: String + var savedMessages = false + if peers.count == 1, let peerId = peers.first?.id, peerId == strongSelf.context.account.peerId { + text = messages.count == 1 ? presentationData.strings.Conversation_ForwardTooltip_SavedMessages_One : presentationData.strings.Conversation_ForwardTooltip_SavedMessages_Many + savedMessages = true + } else { + if peers.count == 1, let peer = peers.first { + let peerName = peer.id == strongSelf.context.account.peerId ? presentationData.strings.DialogList_SavedMessages : peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + text = messages.count == 1 ? presentationData.strings.Conversation_ForwardTooltip_Chat_One(peerName).0 : presentationData.strings.Conversation_ForwardTooltip_Chat_Many(peerName).0 + } else if peers.count == 2, let firstPeer = peers.first, let secondPeer = peers.last { + let firstPeerName = firstPeer.id == strongSelf.context.account.peerId ? presentationData.strings.DialogList_SavedMessages : firstPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + let secondPeerName = secondPeer.id == strongSelf.context.account.peerId ? presentationData.strings.DialogList_SavedMessages : secondPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + text = messages.count == 1 ? presentationData.strings.Conversation_ForwardTooltip_TwoChats_One(firstPeerName, secondPeerName).0 : presentationData.strings.Conversation_ForwardTooltip_TwoChats_Many(firstPeerName, secondPeerName).0 + } else if let peer = peers.first { + let peerName = peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + text = messages.count == 1 ? presentationData.strings.Conversation_ForwardTooltip_ManyChats_One(peerName, "\(peers.count - 1)").0 : presentationData.strings.Conversation_ForwardTooltip_ManyChats_Many(peerName, "\(peers.count - 1)").0 + } else { + text = "" + } + } + + (strongSelf.navigationController?.topViewController as? ViewController)?.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: savedMessages, text: text), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), in: .current) + } + } peerSelectionController.peerSelected = { [weak self, weak peerSelectionController] peer in let peerId = peer.id if let strongSelf = self, let _ = peerSelectionController { @@ -911,7 +991,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo (strongSelf.navigationController?.topViewController as? ViewController)?.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: true, text: messages.count == 1 ? presentationData.strings.Conversation_ForwardTooltip_SavedMessages_One : presentationData.strings.Conversation_ForwardTooltip_SavedMessages_Many), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), in: .window(.root)) let _ = (enqueueMessages(account: strongSelf.context.account, peerId: peerId, messages: messageIds.map { id -> EnqueueMessage in - return .forward(source: id, grouping: .auto, attributes: []) + return .forward(source: id, grouping: .auto, attributes: [], correlationId: nil) }) |> deliverOnMainQueue).start(next: { [weak self] messageIds in if let strongSelf = self { diff --git a/submodules/ChatListUI/Sources/ChatListSearchFiltersContainerNode.swift b/submodules/ChatListUI/Sources/ChatListSearchFiltersContainerNode.swift index 2224d24af0..fe11a7cf60 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchFiltersContainerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchFiltersContainerNode.swift @@ -211,7 +211,7 @@ final class ChatListSearchFiltersContainerNode: ASDisplayNode { let previousContentWidth = self.scrollNode.view.contentSize.width if self.currentParams?.presentationData.theme !== presentationData.theme { - self.backgroundColor = presentationData.theme.rootController.navigationBar.backgroundColor + //self.backgroundColor = presentationData.theme.rootController.navigationBar.opaqueBackgroundColor self.selectedLineNode.image = generateImage(CGSize(width: 5.0, height: 3.0), rotatedContext: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) context.setFillColor(presentationData.theme.list.itemAccentColor.cgColor) diff --git a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift index ef8bf065b5..969b115076 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift @@ -515,7 +515,7 @@ public enum ChatListSearchEntry: Comparable, Identifiable { let header = ChatListSearchItemHeader(type: .messages, theme: presentationData.theme, strings: presentationData.strings, actionTitle: nil, action: nil) let selection: ChatHistoryMessageSelection = selected.flatMap { .selectable(selected: $0) } ?? .none if let tagMask = tagMask, tagMask != .photoOrVideo { - return ListMessageItem(presentationData: ChatPresentationData(theme: ChatPresentationThemeData(theme: presentationData.theme, wallpaper: .builtin(WallpaperSettings())), fontSize: presentationData.fontSize, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, disableAnimations: presentationData.disableAnimations, largeEmoji: false, chatBubbleCorners: PresentationChatBubbleCorners(mainRadius: 0.0, auxiliaryRadius: 0.0, mergeBubbleCorners: false)), context: context, chatLocation: .peer(peer.peerId), interaction: listInteraction, message: message, selection: selection, displayHeader: enableHeaders && !displayCustomHeader, customHeader: nil, hintIsLink: tagMask == .webPage, isGlobalSearchResult: true) + return ListMessageItem(presentationData: ChatPresentationData(theme: ChatPresentationThemeData(theme: presentationData.theme, wallpaper: .builtin(WallpaperSettings())), fontSize: presentationData.fontSize, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, disableAnimations: true, largeEmoji: false, chatBubbleCorners: PresentationChatBubbleCorners(mainRadius: 0.0, auxiliaryRadius: 0.0, mergeBubbleCorners: false)), context: context, chatLocation: .peer(peer.peerId), interaction: listInteraction, message: message, selection: selection, displayHeader: enableHeaders && !displayCustomHeader, customHeader: nil, hintIsLink: tagMask == .webPage, isGlobalSearchResult: true) } else { return ChatListItem(presentationData: presentationData, context: context, peerGroupId: .root, filterData: nil, index: ChatListIndex(pinningIndex: nil, messageIndex: message.index), content: .peer(messages: [message], peer: peer, combinedReadState: readState, isRemovedFromTotalUnreadCount: false, presence: nil, summaryInfo: ChatListMessageTagSummaryInfo(), embeddedState: nil, inputActivities: nil, promoInfo: nil, ignoreUnreadBadge: true, displayAsMessage: false, hasFailedMessages: false), editing: false, hasActiveRevealControls: false, selected: false, header: tagMask == nil ? header : nil, enableContextActions: false, hiddenOffset: false, interaction: interaction) } @@ -689,7 +689,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { private let emptyResultsTitleNode: ImmediateTextNode private let emptyResultsTextNode: ImmediateTextNode private let emptyResultsAnimationNode: AnimatedStickerNode - private var animationSize: CGSize = CGSize() + private var emptyResultsAnimationSize: CGSize = CGSize() private var currentParams: (size: CGSize, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, presentationData: PresentationData)? @@ -737,7 +737,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { let presentationData = context.sharedContext.currentPresentationData.with { $0 } self.presentationData = presentationData - 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.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: true))) self.searchStatePromise.set(self.searchStateValue) self.selectedMessages = interaction.getSelectedMessageIds() @@ -792,8 +792,8 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { super.init() if let path = getAppBundle().path(forResource: "ChatListNoResults", ofType: "tgs") { - self.emptyResultsAnimationNode.setup(source: AnimatedStickerNodeLocalFileSource(path: path), width: 248, height: 248, playbackMode: .once, mode: .direct(cachePathPrefix: nil)) - self.animationSize = CGSize(width: 124.0, height: 124.0) + self.emptyResultsAnimationNode.setup(source: AnimatedStickerNodeLocalFileSource(path: path), width: 256, height: 256, playbackMode: .once, mode: .direct(cachePathPrefix: nil)) + self.emptyResultsAnimationSize = CGSize(width: 148.0, height: 148.0) } self.addSubnode(self.recentListNode) @@ -841,9 +841,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { } let accountPeer = context.account.postbox.loadedPeerWithId(context.account.peerId) |> take(1) - let foundLocalPeers: Signal<(peers: [RenderedPeer], unread: [PeerId: (Int32, Bool)]), NoError> - if let query = query { foundLocalPeers = context.account.postbox.searchPeers(query: query.lowercased()) |> mapToSignal { local -> Signal<([PeerView], [RenderedPeer]), NoError> in @@ -883,7 +881,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { foundRemotePeers = ( .single((currentRemotePeersValue.0, currentRemotePeersValue.1, true)) |> then( - searchPeers(account: context.account, query: query) + context.engine.peers.searchPeers(query: query) |> map { ($0.0, $0.1, false) } |> delay(0.2, queue: Queue.concurrentDefaultQueue()) ) @@ -922,7 +920,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { addAppLogEvent(postbox: context.account.postbox, type: "search_global_query") } - let searchSignal = searchMessages(account: context.account, location: location, query: finalQuery, state: nil, limit: 50) + let searchSignal = context.engine.messages.searchMessages(location: location, query: finalQuery, state: nil, limit: 50) |> map { result, updatedState -> ChatListSearchMessagesResult in return ChatListSearchMessagesResult(query: finalQuery, messages: result.messages.sorted(by: { $0.index > $1.index }), readStates: result.readStates, hasMore: !result.completed, totalCount: result.totalCount, state: updatedState) } @@ -931,7 +929,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { |> mapToSignal { searchContext -> Signal<(([Message], [PeerId: CombinedPeerReadState], Int32), Bool), NoError> in if let searchContext = searchContext, searchContext.result.hasMore { if let _ = searchContext.loadMoreIndex { - return searchMessages(account: context.account, location: location, query: finalQuery, state: searchContext.result.state, limit: 80) + return context.engine.messages.searchMessages(location: location, query: finalQuery, state: searchContext.result.state, limit: 80) |> map { result, updatedState -> ChatListSearchMessagesResult in return ChatListSearchMessagesResult(query: finalQuery, messages: result.messages.sorted(by: { $0.index > $1.index }), readStates: result.readStates, hasMore: !result.completed, totalCount: result.totalCount, state: updatedState) } @@ -965,10 +963,10 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { } let resolvedMessage = .single(nil) - |> then(context.sharedContext.resolveUrl(account: context.account, url: finalQuery, skipUrlAuth: true) + |> then(context.sharedContext.resolveUrl(context: context, peerId: nil, url: finalQuery, skipUrlAuth: true) |> mapToSignal { resolvedUrl -> Signal in - if case let .channelMessage(_, messageId) = resolvedUrl { - return downloadMessage(postbox: context.account.postbox, network: context.account.network, messageId: messageId) + if case let .channelMessage(_, messageId, _) = resolvedUrl { + return context.engine.messages.downloadMessage(messageId: messageId) } else { return .single(nil) } @@ -1215,10 +1213,11 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { }, peerSelected: { [weak self] peer, _ in interaction.dismissInput() interaction.openPeer(peer, false) - let _ = addRecentlySearchedPeer(postbox: context.account.postbox, peerId: peer.id).start() + let _ = context.engine.peers.addRecentlySearchedPeer(peerId: peer.id).start() self?.listNode.clearHighlightAnimated(true) }, disabledPeerSelected: { _ in }, togglePeerSelected: { _ in + }, togglePeersSelection: { _, _ in }, additionalCategorySelected: { _ in }, messageSelected: { [weak self] peer, message, _ in interaction.dismissInput() @@ -1279,7 +1278,6 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { }, openUrl: { url in interaction.openUrl(url) }, openPeer: { peer, navigation in -// interaction.openPeer(peer.id, navigation) }, callPeer: { _, _ in }, enqueueMessage: { _ in }, sendSticker: nil, setupTemporaryHiddenMedia: { _, _, _ in }, chatAvatarHiddenMedia: { _, _ in }, playlistLocation: .custom(messages: foundMessages, at: message.id, loadMore: { @@ -1398,7 +1396,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { })) let previousRecentItems = Atomic<[ChatListRecentEntry]?>(value: nil) - let hasRecentPeers = recentPeers(account: context.account) + let hasRecentPeers = context.engine.peers.recentPeers() |> map { value -> Bool in switch value { case let .peers(peers): @@ -1410,7 +1408,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { |> distinctUntilChanged let previousRecentlySearchedPeerOrder = Atomic<[PeerId]>(value: []) - let fixedRecentlySearchedPeers = recentlySearchedPeers(postbox: context.account.postbox) + let fixedRecentlySearchedPeers = context.engine.peers.recentlySearchedPeers() |> map { peers -> [RecentlySearchedPeer] in var result: [RecentlySearchedPeer] = [] let _ = previousRecentlySearchedPeerOrder.modify { current in @@ -1468,7 +1466,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { } if tagMask == nil && !peersFilter.contains(.excludeRecent) { - self.updatedRecentPeersDisposable.set(managedUpdatedRecentPeers(accountPeerId: context.account.peerId, postbox: context.account.postbox, network: context.account.network).start()) + self.updatedRecentPeersDisposable.set(context.engine.peers.managedUpdatedRecentPeers().start()) } self.recentDisposable.set((combineLatest(queue: .mainQueue(), @@ -1482,7 +1480,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { let firstTime = previousEntries == nil let transition = chatListSearchContainerPreparedRecentTransition(from: previousEntries ?? [], to: entries, context: context, presentationData: presentationData, filter: peersFilter, peerSelected: { peer in interaction.openPeer(peer, true) - let _ = addRecentlySearchedPeer(postbox: context.account.postbox, peerId: peer.id).start() + let _ = context.engine.peers.addRecentlySearchedPeer(peerId: peer.id).start() self?.recentListNode.clearHighlightAnimated(true) }, disabledPeerSelected: { peer in interaction.openDisabledPeer(peer) @@ -1495,7 +1493,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { }, clearRecentlySearchedPeers: { interaction.clearRecentSearch() }, deletePeer: { peerId in - let _ = removeRecentlySearchedPeer(postbox: context.account.postbox, peerId: peerId).start() + let _ = context.engine.peers.removeRecentlySearchedPeer(peerId: peerId).start() }) strongSelf.enqueueRecentTransition(transition, firstTime: firstTime) } @@ -1505,7 +1503,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { |> deliverOnMainQueue).start(next: { [weak self] presentationData in if let strongSelf = self { strongSelf.presentationData = presentationData - 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))) + strongSelf.presentationDataPromise.set(.single(ChatListPresentationData(theme: presentationData.theme, fontSize: presentationData.listsFontSize, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameSortOrder: presentationData.nameSortOrder, nameDisplayOrder: presentationData.nameDisplayOrder, disableAnimations: true))) strongSelf.listNode.forEachItemHeaderNode({ itemHeaderNode in if let itemHeaderNode = itemHeaderNode as? ChatListSearchItemHeaderNode { @@ -1521,11 +1519,11 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { } }) - self.recentListNode.beganInteractiveDragging = { + self.recentListNode.beganInteractiveDragging = { _ in interaction.dismissInput() } - self.listNode.beganInteractiveDragging = { + self.listNode.beganInteractiveDragging = { _ in interaction.dismissInput() } @@ -1724,7 +1722,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { }) } - let mediaAccessoryPanel = MediaNavigationAccessoryPanel(context: self.context) + let mediaAccessoryPanel = MediaNavigationAccessoryPanel(context: self.context, displayBackground: true) mediaAccessoryPanel.containerNode.headerNode.displayScrubber = item.playbackData?.type != .instantVideo mediaAccessoryPanel.close = { [weak self] in if let strongSelf = self, let (_, _, _, _, type, _) = strongSelf.playlistStateAndType { @@ -1938,21 +1936,17 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { let emptyTitleSize = self.emptyResultsTitleNode.updateLayout(CGSize(width: size.width - sideInset * 2.0 - padding * 2.0, height: CGFloat.greatestFiniteMagnitude)) let emptyTextSize = self.emptyResultsTextNode.updateLayout(CGSize(width: size.width - sideInset * 2.0 - padding * 2.0, height: CGFloat.greatestFiniteMagnitude)) - let emptyAnimationHeight = self.animationSize.height + let emptyAnimationHeight = self.emptyResultsAnimationSize.height let emptyAnimationSpacing: CGFloat = 8.0 -// if case .landscape = layout.orientation, case .compact = layout.metrics.widthClass { -// emptyAnimationHeight = 0.0 -// emptyAnimationSpacing = 0.0 -// } let emptyTextSpacing: CGFloat = 8.0 let emptyTotalHeight = emptyAnimationHeight + emptyAnimationSpacing + emptyTitleSize.height + emptyTextSize.height + emptyTextSpacing let emptyAnimationY = topInset + floorToScreenPixels((visibleHeight - topInset - bottomInset - emptyTotalHeight) / 2.0) let textTransition = ContainedViewLayoutTransition.immediate - textTransition.updateFrame(node: self.emptyResultsAnimationNode, frame: CGRect(origin: CGPoint(x: sideInset + padding + (size.width - sideInset * 2.0 - padding * 2.0 - self.animationSize.width) / 2.0, y: emptyAnimationY), size: self.animationSize)) + textTransition.updateFrame(node: self.emptyResultsAnimationNode, frame: CGRect(origin: CGPoint(x: sideInset + padding + (size.width - sideInset * 2.0 - padding * 2.0 - self.emptyResultsAnimationSize.width) / 2.0, y: emptyAnimationY), size: self.emptyResultsAnimationSize)) textTransition.updateFrame(node: self.emptyResultsTitleNode, frame: CGRect(origin: CGPoint(x: sideInset + padding + (size.width - sideInset * 2.0 - padding * 2.0 - emptyTitleSize.width) / 2.0, y: emptyAnimationY + emptyAnimationHeight + emptyAnimationSpacing), size: emptyTitleSize)) textTransition.updateFrame(node: self.emptyResultsTextNode, frame: CGRect(origin: CGPoint(x: sideInset + padding + (size.width - sideInset * 2.0 - padding * 2.0 - emptyTextSize.width) / 2.0, y: emptyAnimationY + emptyAnimationHeight + emptyAnimationSpacing + emptyTitleSize.height + emptyTextSpacing), size: emptyTextSize)) - self.emptyResultsAnimationNode.updateLayout(size: self.animationSize) + self.emptyResultsAnimationNode.updateLayout(size: self.emptyResultsAnimationSize) if !hadValidLayout { while !self.enqueuedRecentTransitions.isEmpty { @@ -2327,11 +2321,11 @@ private final class ChatListSearchShimmerNode: ASDisplayNode { let chatListPresentationData = ChatListPresentationData(theme: presentationData.theme, fontSize: presentationData.chatFontSize, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameSortOrder: presentationData.nameSortOrder, nameDisplayOrder: presentationData.nameDisplayOrder, disableAnimations: true) - let peer1 = TelegramUser(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: 1), accessHash: nil, firstName: "FirstName", lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: []) + let peer1 = TelegramUser(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt32Value(0)), accessHash: nil, firstName: "FirstName", lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: []) let timestamp1: Int32 = 100000 var peers = SimpleDictionary() peers[peer1.id] = peer1 - let interaction = ChatListNodeInteraction(activateSearch: {}, peerSelected: { _, _ in }, disabledPeerSelected: { _ in }, togglePeerSelected: { _ in }, additionalCategorySelected: { _ in + let interaction = ChatListNodeInteraction(activateSearch: {}, peerSelected: { _, _ in }, disabledPeerSelected: { _ in }, togglePeerSelected: { _ in }, togglePeersSelection: { _, _ in }, additionalCategorySelected: { _ in }, messageSelected: { _, _, _ in}, groupSelected: { _ in }, addContact: { _ in }, setPeerIdWithRevealedOptions: { _, _ in }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, deletePeer: { _, _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in}, toggleArchivedFolderHiddenByDefault: {}, hidePsa: { _ in }, activateChatPreview: { _, _, gesture in gesture?.cancel() }, present: { _ in }) diff --git a/submodules/ChatListUI/Sources/ChatListSearchMediaNode.swift b/submodules/ChatListUI/Sources/ChatListSearchMediaNode.swift index d74c1208e9..7979476472 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchMediaNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchMediaNode.swift @@ -320,21 +320,6 @@ private final class VisualMediaItemNode: ASDisplayNode { func updateIsVisible(_ isVisible: Bool) { self.hasVisibility = isVisible -// if let _ = self.videoLayerFrameManager { -// let displayLink: ConstantDisplayLinkAnimator -// if let current = self.displayLink { -// displayLink = current -// } else { -// displayLink = ConstantDisplayLinkAnimator { [weak self] in -// guard let strongSelf = self else { -// return -// } -// strongSelf.displayLinkTimestamp += 1.0 / 30.0 -// } -// displayLink.frameInterval = 2 -// self.displayLink = displayLink -// } -// } self.displayLink?.isPaused = !self.hasVisibility || self.isHidden } @@ -422,8 +407,8 @@ private final class VisualMediaItem { let dimensions: CGSize let aspectRatio: CGFloat - init(message: Message) { - self.index = nil + init(message: Message, index: UInt32?) { + self.index = index self.message = message var aspectRatio: CGFloat = 1.0 @@ -441,10 +426,10 @@ private final class VisualMediaItem { } var stableId: UInt32 { - if let message = self.message { - return message.stableId - } else if let index = self.index { + if let index = self.index { return index + } else if let message = self.message { + return message.stableId } else { return 0 } @@ -708,7 +693,6 @@ final class ChatListSearchMediaNode: ASDisplayNode, UIScrollViewDelegate { self.animationTimer?.invalidate() } - func updateHistory(entries: [ChatListSearchEntry]?, totalCount: Int32, updateType: ViewUpdateType) { switch updateType { case .FillHole: @@ -716,11 +700,13 @@ final class ChatListSearchMediaNode: ASDisplayNode, UIScrollViewDelegate { default: self.mediaItems.removeAll() + var index: UInt32 = 0 if let entries = entries { for entry in entries { if case let .message(message, _, _, _, _, _, _) = entry { - self.mediaItems.append(VisualMediaItem(message: message)) + self.mediaItems.append(VisualMediaItem(message: message, index: nil)) } + index += 1 } } self.itemsLayout = nil diff --git a/submodules/ChatListUI/Sources/ChatListSearchMessageSelectionPanelNode.swift b/submodules/ChatListUI/Sources/ChatListSearchMessageSelectionPanelNode.swift index d126d37553..765d79ab65 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchMessageSelectionPanelNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchMessageSelectionPanelNode.swift @@ -121,7 +121,7 @@ final class ChatListSearchMessageSelectionPanelNode: ASDisplayNode { if presentationData.theme !== self.theme { self.theme = presentationData.theme - self.backgroundNode.backgroundColor = presentationData.theme.rootController.navigationBar.backgroundColor + self.backgroundNode.backgroundColor = presentationData.theme.rootController.navigationBar.opaqueBackgroundColor self.separatorNode.backgroundColor = presentationData.theme.rootController.navigationBar.separatorColor self.deleteButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionTrash"), color: presentationData.theme.chat.inputPanel.panelControlAccentColor), for: [.normal]) diff --git a/submodules/ChatListUI/Sources/ChatListSelection.swift b/submodules/ChatListUI/Sources/ChatListSelection.swift index 869a2fae27..3c0c533c59 100644 --- a/submodules/ChatListUI/Sources/ChatListSelection.swift +++ b/submodules/ChatListUI/Sources/ChatListSelection.swift @@ -4,6 +4,7 @@ import SwiftSignalKit import Postbox import TelegramCore import SyncCore +import AccountContext enum ChatListSelectionReadOption: Equatable { case all(enabled: Bool) @@ -15,10 +16,10 @@ struct ChatListSelectionOptions: Equatable { let delete: Bool } -func chatListSelectionOptions(postbox: Postbox, peerIds: Set, filterId: Int32?) -> Signal { +func chatListSelectionOptions(context: AccountContext, peerIds: Set, filterId: Int32?) -> Signal { if peerIds.isEmpty { if let filterId = filterId { - return chatListFilterItems(postbox: postbox) + return chatListFilterItems(context: context) |> map { filterItems -> ChatListSelectionOptions in for (filter, unreadCount, _) in filterItems.1 { if filter.id == filterId { @@ -30,7 +31,7 @@ func chatListSelectionOptions(postbox: Postbox, peerIds: Set, filterId: |> distinctUntilChanged } else { let key = PostboxViewKey.unreadCounts(items: [.total(nil)]) - return postbox.combinedView(keys: [key]) + return context.account.postbox.combinedView(keys: [key]) |> map { view -> ChatListSelectionOptions in var hasUnread = false if let unreadCounts = view.views[key] as? UnreadMessageCountsView, let total = unreadCounts.total() { @@ -48,7 +49,7 @@ func chatListSelectionOptions(postbox: Postbox, peerIds: Set, filterId: } else { let items: [UnreadMessageCountsItem] = peerIds.map(UnreadMessageCountsItem.peer) let key = PostboxViewKey.unreadCounts(items: items) - return postbox.combinedView(keys: [key]) + return context.account.postbox.combinedView(keys: [key]) |> map { view -> ChatListSelectionOptions in var hasUnread = false if let unreadCounts = view.views[key] as? UnreadMessageCountsView { diff --git a/submodules/ChatListUI/Sources/ChatListTitleView.swift b/submodules/ChatListUI/Sources/ChatListTitleView.swift index b2fac18119..4de4c02f1d 100644 --- a/submodules/ChatListUI/Sources/ChatListTitleView.swift +++ b/submodules/ChatListUI/Sources/ChatListTitleView.swift @@ -5,6 +5,8 @@ import Display import TelegramPresentationData import ActivityIndicator +private let titleFont = Font.with(size: 17.0, design: .regular, weight: .semibold, traits: [.monospacedNumbers]) + struct NetworkStatusTitle: Equatable { let text: String let activity: Bool @@ -17,6 +19,7 @@ struct NetworkStatusTitle: Equatable { final class ChatListTitleView: UIView, NavigationBarTitleView, NavigationBarTitleTransitionNode { private let titleNode: ImmediateTextNode private let lockView: ChatListTitleLockView + private weak var lockSnapshotView: UIView? private let activityIndicator: ActivityIndicator private let buttonView: HighlightTrackingButton private let proxyNode: ChatTitleProxyNode @@ -24,30 +27,71 @@ final class ChatListTitleView: UIView, NavigationBarTitleView, NavigationBarTitl private var validLayout: (CGSize, CGRect)? - var title: NetworkStatusTitle = NetworkStatusTitle(text: "", activity: false, hasProxy: false, connectsViaProxy: false, isPasscodeSet: false, isManuallyLocked: false) { - didSet { - if self.title != oldValue { - self.titleNode.attributedText = NSAttributedString(string: self.title.text, font: Font.bold(17.0), textColor: self.theme.rootController.navigationBar.primaryTextColor) - self.buttonView.accessibilityLabel = self.title.text - self.activityIndicator.isHidden = !self.title.activity - if self.title.connectsViaProxy { - self.proxyNode.status = self.title.activity ? .connecting : .connected - } else { - self.proxyNode.status = .available - } - self.proxyNode.isHidden = !self.title.hasProxy - self.proxyButton.isHidden = !self.title.hasProxy - - self.buttonView.isHidden = !self.title.isPasscodeSet - if self.title.isPasscodeSet && !self.title.activity { - self.lockView.isHidden = false - } else { - self.lockView.isHidden = true - } - self.lockView.updateTheme(self.theme) - - self.setNeedsLayout() + private var _title: NetworkStatusTitle = NetworkStatusTitle(text: "", activity: false, hasProxy: false, connectsViaProxy: false, isPasscodeSet: false, isManuallyLocked: false) + var title: NetworkStatusTitle { + get { + return self._title + } + set { + self.setTitle(newValue, animated: false) + } + } + + func setTitle(_ title: NetworkStatusTitle, animated: Bool) { + let oldValue = self._title + self._title = title + + if self._title != oldValue { + self.titleNode.attributedText = NSAttributedString(string: self.title.text, font: titleFont, textColor: self.theme.rootController.navigationBar.primaryTextColor) + self.buttonView.accessibilityLabel = self.title.text + self.activityIndicator.isHidden = !self.title.activity + + self.proxyButton.isHidden = !self.title.hasProxy + if self.title.connectsViaProxy { + self.proxyNode.status = self.title.activity ? .connecting : .connected + } else { + self.proxyNode.status = .available } + + let proxyIsHidden = !self.title.hasProxy + let previousProxyIsHidden = self.proxyNode.isHidden + if proxyIsHidden != previousProxyIsHidden { + if proxyIsHidden { + if let snapshotView = self.proxyNode.view.snapshotContentTree() { + snapshotView.frame = self.proxyNode.frame + self.proxyNode.view.superview?.insertSubview(snapshotView, aboveSubview: self.proxyNode.view) + snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false, completion: { [weak snapshotView] _ in + snapshotView?.removeFromSuperview() + }) + } + } else { + self.proxyNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) + } + } + self.proxyNode.isHidden = !self.title.hasProxy + + self.buttonView.isHidden = !self.title.isPasscodeSet + if self.title.isPasscodeSet && !self.title.activity { + if self.lockView.isHidden && animated { + self.lockView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) + } + self.lockView.isHidden = false + } else { + if !self.lockView.isHidden && animated { + if let snapshotView = self.lockView.snapshotContentTree() { + self.lockSnapshotView = snapshotView + snapshotView.frame = self.lockView.frame + self.lockView.superview?.insertSubview(snapshotView, aboveSubview: self.lockView) + snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false, completion: { [weak snapshotView] _ in + snapshotView?.removeFromSuperview() + }) + } + } + self.lockView.isHidden = true + } + self.lockView.updateTheme(self.theme) + + self.setNeedsLayout() } } @@ -59,7 +103,7 @@ final class ChatListTitleView: UIView, NavigationBarTitleView, NavigationBarTitl var theme: PresentationTheme { didSet { - self.titleNode.attributedText = NSAttributedString(string: self.title.text, font: Font.bold(17.0), textColor: self.theme.rootController.navigationBar.primaryTextColor) + self.titleNode.attributedText = NSAttributedString(string: self.title.text, font: titleFont, textColor: self.theme.rootController.navigationBar.primaryTextColor) self.lockView.updateTheme(self.theme) @@ -178,11 +222,9 @@ final class ChatListTitleView: UIView, NavigationBarTitleView, NavigationBarTitl indicatorPadding = indicatorSize.width + 6.0 } var maxTitleWidth = clearBounds.size.width - indicatorPadding - var alignedTitleWidth = size.width - indicatorPadding var proxyPadding: CGFloat = 0.0 if !self.proxyNode.isHidden { maxTitleWidth -= 25.0 - alignedTitleWidth -= 20.0 proxyPadding += 39.0 } if !self.lockView.isHidden { @@ -197,18 +239,24 @@ final class ChatListTitleView: UIView, NavigationBarTitleView, NavigationBarTitl titleContentRect.origin.x = min(titleContentRect.origin.x, clearBounds.maxX - proxyPadding - titleContentRect.width) let titleFrame = titleContentRect - self.titleNode.frame = titleFrame + transition.updateFrame(node: self.titleNode, frame: titleFrame) - let proxyFrame = CGRect(origin: CGPoint(x: clearBounds.maxX - 9.0 - self.proxyNode.bounds.width, y: floor((size.height - proxyNode.bounds.height) / 2.0)), size: proxyNode.bounds.size) + let proxyFrame = CGRect(origin: CGPoint(x: clearBounds.maxX - 9.0 - self.proxyNode.bounds.width, y: floor((size.height - self.proxyNode.bounds.height) / 2.0)), size: self.proxyNode.bounds.size) self.proxyNode.frame = proxyFrame + self.proxyButton.frame = proxyFrame.insetBy(dx: -2.0, dy: -2.0) let buttonX = max(0.0, titleFrame.minX - 10.0) self.buttonView.frame = CGRect(origin: CGPoint(x: buttonX, y: 0.0), size: CGSize(width: min(titleFrame.maxX + 28.0, size.width) - buttonX, height: size.height)) - self.lockView.frame = CGRect(x: titleFrame.maxX + 6.0, y: titleFrame.minY + 2.0, width: 2.0, height: 2.0) + let lockFrame = CGRect(x: titleFrame.maxX + 6.0, y: titleFrame.minY + 2.0, width: 2.0, height: 2.0) + transition.updateFrame(view: self.lockView, frame: lockFrame) + if let lockSnapshotView = self.lockSnapshotView { + transition.updateFrame(view: lockSnapshotView, frame: lockFrame) + } - self.activityIndicator.frame = CGRect(origin: CGPoint(x: titleFrame.minX - indicatorSize.width - 4.0, y: titleFrame.minY - 1.0), size: indicatorSize) + let activityIndicatorFrame = CGRect(origin: CGPoint(x: titleFrame.minX - indicatorSize.width - 4.0, y: titleFrame.minY - 1.0), size: indicatorSize) + transition.updateFrame(node: self.activityIndicator, frame: activityIndicatorFrame) } @objc private func buttonPressed() { diff --git a/submodules/ChatListUI/Sources/Node/ChatListItem.swift b/submodules/ChatListUI/Sources/Node/ChatListItem.swift index 9f429feaec..c4f36a0073 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListItem.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListItem.swift @@ -196,7 +196,7 @@ private enum RevealOptionKey: Int32 { } private func canArchivePeer(id: PeerId, accountPeerId: PeerId) -> Bool { - if id.namespace == Namespaces.Peer.CloudUser && id.id == 777000 { + if id.namespace == Namespaces.Peer.CloudUser && id.id._internalGetInt32Value() == 777000 { return false } if id == accountPeerId { @@ -518,7 +518,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { } else { result += item.presentationData.strings.VoiceOver_ChatList_OutgoingMessage } - let (_, initialHideAuthor, messageText) = chatListItemStrings(strings: item.presentationData.strings, nameDisplayOrder: item.presentationData.nameDisplayOrder, messages: messages, chatPeer: peer, accountPeerId: item.context.account.peerId, isPeerGroup: false) + let (_, initialHideAuthor, messageText) = chatListItemStrings(strings: item.presentationData.strings, nameDisplayOrder: item.presentationData.nameDisplayOrder, dateTimeFormat: item.presentationData.dateTimeFormat, messages: messages, chatPeer: peer, accountPeerId: item.context.account.peerId, isPeerGroup: false) if message.flags.contains(.Incoming), !initialHideAuthor, let author = message.author, author is TelegramUser { result += "\n\(item.presentationData.strings.VoiceOver_ChatList_MessageFrom(author.displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder)).0)" } @@ -552,7 +552,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { } else { result += item.presentationData.strings.VoiceOver_ChatList_OutgoingMessage } - let (_, initialHideAuthor, messageText) = chatListItemStrings(strings: item.presentationData.strings, nameDisplayOrder: item.presentationData.nameDisplayOrder, messages: messages, chatPeer: peer, accountPeerId: item.context.account.peerId, isPeerGroup: false) + let (_, initialHideAuthor, messageText) = chatListItemStrings(strings: item.presentationData.strings, nameDisplayOrder: item.presentationData.nameDisplayOrder, dateTimeFormat: item.presentationData.dateTimeFormat, messages: messages, chatPeer: peer, accountPeerId: item.context.account.peerId, isPeerGroup: false) if message.flags.contains(.Incoming), !initialHideAuthor, let author = message.author, author is TelegramUser { result += "\n\(item.presentationData.strings.VoiceOver_ChatList_MessageFrom(author.displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder)).0)" } @@ -696,7 +696,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { 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 + self.contextContainer.isGestureEnabled = enablePreview && !item.editing } override func layoutForParams(_ params: ListViewItemLayoutParams, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) { @@ -773,9 +773,9 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { guard let item = self.item, item.editing else { return } - if case let .peer(_, _, _, _, _, _, _, _, promoInfo, _, _, _) = item.content { - if promoInfo == nil { - item.interaction.togglePeerSelected(item.index.messageIndex.id.peerId) + if case let .peer(_, peer, _, _, _, _, _, _, promoInfo, _, _, _) = item.content { + if promoInfo == nil, let mainPeer = peer.chatMainPeer { + item.interaction.togglePeerSelected(mainPeer) } } } @@ -801,7 +801,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { 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 badgeFont = Font.with(size: floor(item.presentationData.fontSize.itemListBaseFontSize * 14.0 / 17.0), design: .regular, weight: .regular, traits: [.monospacedNumbers]) let account = item.context.account var messages: [Message] @@ -958,7 +958,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { var hideAuthor = false switch contentPeer { case let .chat(itemPeer): - var (peer, initialHideAuthor, messageText) = chatListItemStrings(strings: item.presentationData.strings, nameDisplayOrder: item.presentationData.nameDisplayOrder, messages: messages, chatPeer: itemPeer, accountPeerId: item.context.account.peerId, enableMediaEmoji: !enableChatListPhotos, isPeerGroup: isPeerGroup) + var (peer, initialHideAuthor, messageText) = chatListItemStrings(strings: item.presentationData.strings, nameDisplayOrder: item.presentationData.nameDisplayOrder, dateTimeFormat: item.presentationData.dateTimeFormat, messages: messages, chatPeer: itemPeer, accountPeerId: item.context.account.peerId, enableMediaEmoji: !enableChatListPhotos, isPeerGroup: isPeerGroup) if case let .psa(_, maybePsaText) = promoInfo, let psaText = maybePsaText { initialHideAuthor = true @@ -1509,7 +1509,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { strongSelf.onlineIsVoiceChat = onlineIsVoiceChat strongSelf.contextContainer.frame = CGRect(origin: CGPoint(), size: layout.contentSize) - + if case .groupReference = item.content { strongSelf.layer.sublayerTransform = CATransform3DMakeTranslation(0.0, layout.contentSize.height - itemHeight, 0.0) } @@ -1853,7 +1853,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: layoutOffset - separatorHeight - topNegativeInset), size: CGSize(width: layout.contentSize.width, height: layout.contentSize.height + separatorHeight + topNegativeInset)) if let peerPresence = peerPresence as? TelegramUserPresence { - strongSelf.peerPresenceManager?.reset(presence: TelegramUserPresence(status: peerPresence.status, lastActivity: 0)) + strongSelf.peerPresenceManager?.reset(presence: TelegramUserPresence(status: peerPresence.status, lastActivity: 0), isOnline: online) } strongSelf.updateLayout(size: layout.contentSize, leftInset: params.leftInset, rightInset: params.rightInset) @@ -1893,9 +1893,9 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { } } - override public func header() -> ListViewItemHeader? { + override public func headers() -> [ListViewItemHeader]? { if let item = self.layoutParams?.0 { - return item.header + return item.header.flatMap { [$0] } } else { return nil } diff --git a/submodules/ChatListUI/Sources/Node/ChatListItemStrings.swift b/submodules/ChatListUI/Sources/Node/ChatListItemStrings.swift index f30ee72089..c7e3c3b503 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListItemStrings.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListItemStrings.swift @@ -46,7 +46,7 @@ private func messageGroupType(messages: [Message]) -> MessageGroupType { return currentType } -public func chatListItemStrings(strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, messages: [Message], chatPeer: RenderedPeer, accountPeerId: PeerId, enableMediaEmoji: Bool = true, isPeerGroup: Bool = false) -> (peer: Peer?, hideAuthor: Bool, messageText: String) { +public func chatListItemStrings(strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, dateTimeFormat: PresentationDateTimeFormat, messages: [Message], chatPeer: RenderedPeer, accountPeerId: PeerId, enableMediaEmoji: Bool = true, isPeerGroup: Bool = false) -> (peer: Peer?, hideAuthor: Bool, messageText: String) { let peer: Peer? let message = messages.last @@ -262,12 +262,12 @@ public func chatListItemStrings(strings: PresentationStrings, nameDisplayOrder: } default: hideAuthor = true - if let text = plainServiceMessageString(strings: strings, nameDisplayOrder: nameDisplayOrder, message: message, accountPeerId: accountPeerId, forChatList: true) { + if let text = plainServiceMessageString(strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: dateTimeFormat, message: message, accountPeerId: accountPeerId, forChatList: true) { messageText = text } } case _ as TelegramMediaExpiredContent: - if let text = plainServiceMessageString(strings: strings, nameDisplayOrder: nameDisplayOrder, message: message, accountPeerId: accountPeerId, forChatList: true) { + if let text = plainServiceMessageString(strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: dateTimeFormat, message: message, accountPeerId: accountPeerId, forChatList: true) { messageText = text } case let poll as TelegramMediaPoll: diff --git a/submodules/ChatListUI/Sources/Node/ChatListNode.swift b/submodules/ChatListUI/Sources/Node/ChatListNode.swift index f86624bf5b..8f430c32b2 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListNode.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListNode.swift @@ -48,10 +48,16 @@ final class ChatListHighlightedLocation { } public final class ChatListNodeInteraction { + public enum PeerEntry { + case peerId(PeerId) + case peer(Peer) + } + let activateSearch: () -> Void let peerSelected: (Peer, ChatListNodeEntryPromoInfo?) -> Void let disabledPeerSelected: (Peer) -> Void - let togglePeerSelected: (PeerId) -> Void + let togglePeerSelected: (Peer) -> Void + let togglePeersSelection: ([PeerEntry], Bool) -> Void let additionalCategorySelected: (Int) -> Void let messageSelected: (Peer, Message, ChatListNodeEntryPromoInfo?) -> Void let groupSelected: (PeerGroupId) -> Void @@ -70,11 +76,12 @@ public final class ChatListNodeInteraction { public var searchTextHighightState: String? var highlightedChatLocation: ChatListHighlightedLocation? - public init(activateSearch: @escaping () -> Void, peerSelected: @escaping (Peer, ChatListNodeEntryPromoInfo?) -> Void, disabledPeerSelected: @escaping (Peer) -> Void, togglePeerSelected: @escaping (PeerId) -> Void, additionalCategorySelected: @escaping (Int) -> Void, messageSelected: @escaping (Peer, Message, ChatListNodeEntryPromoInfo?) -> 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, Bool) -> Void, updatePeerGrouping: @escaping (PeerId, Bool) -> Void, togglePeerMarkedUnread: @escaping (PeerId, Bool) -> Void, toggleArchivedFolderHiddenByDefault: @escaping () -> Void, hidePsa: @escaping (PeerId) -> Void, activateChatPreview: @escaping (ChatListItem, ASDisplayNode, ContextGesture?) -> Void, present: @escaping (ViewController) -> Void) { + public init(activateSearch: @escaping () -> Void, peerSelected: @escaping (Peer, ChatListNodeEntryPromoInfo?) -> Void, disabledPeerSelected: @escaping (Peer) -> Void, togglePeerSelected: @escaping (Peer) -> Void, togglePeersSelection: @escaping ([PeerEntry], Bool) -> Void, additionalCategorySelected: @escaping (Int) -> Void, messageSelected: @escaping (Peer, Message, ChatListNodeEntryPromoInfo?) -> 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, Bool) -> Void, updatePeerGrouping: @escaping (PeerId, Bool) -> Void, togglePeerMarkedUnread: @escaping (PeerId, Bool) -> Void, toggleArchivedFolderHiddenByDefault: @escaping () -> Void, hidePsa: @escaping (PeerId) -> Void, activateChatPreview: @escaping (ChatListItem, ASDisplayNode, ContextGesture?) -> Void, present: @escaping (ViewController) -> Void) { self.activateSearch = activateSearch self.peerSelected = peerSelected self.disabledPeerSelected = disabledPeerSelected self.togglePeerSelected = togglePeerSelected + self.togglePeersSelection = togglePeersSelection self.additionalCategorySelected = additionalCategorySelected self.messageSelected = messageSelected self.groupSelected = groupSelected @@ -111,13 +118,17 @@ public struct ChatListNodeState: Equatable { public var archiveShouldBeTemporaryRevealed: Bool public var selectedAdditionalCategoryIds: Set public var hiddenPsaPeerId: PeerId? + public var foundPeers: [Peer] + public var selectedPeerMap: [PeerId: Peer] - public init(presentationData: ChatListPresentationData, editing: Bool, peerIdWithRevealedOptions: PeerId?, selectedPeerIds: Set, selectedAdditionalCategoryIds: Set, peerInputActivities: ChatListNodePeerInputActivities?, pendingRemovalPeerIds: Set, pendingClearHistoryPeerIds: Set, archiveShouldBeTemporaryRevealed: Bool, hiddenPsaPeerId: PeerId?) { + public init(presentationData: ChatListPresentationData, editing: Bool, peerIdWithRevealedOptions: PeerId?, selectedPeerIds: Set, foundPeers: [Peer], selectedPeerMap: [PeerId: Peer], selectedAdditionalCategoryIds: Set, peerInputActivities: ChatListNodePeerInputActivities?, pendingRemovalPeerIds: Set, pendingClearHistoryPeerIds: Set, archiveShouldBeTemporaryRevealed: Bool, hiddenPsaPeerId: PeerId?) { self.presentationData = presentationData self.editing = editing self.peerIdWithRevealedOptions = peerIdWithRevealedOptions self.selectedPeerIds = selectedPeerIds self.selectedAdditionalCategoryIds = selectedAdditionalCategoryIds + self.foundPeers = foundPeers + self.selectedPeerMap = selectedPeerMap self.peerInputActivities = peerInputActivities self.pendingRemovalPeerIds = pendingRemovalPeerIds self.pendingClearHistoryPeerIds = pendingClearHistoryPeerIds @@ -138,6 +149,12 @@ public struct ChatListNodeState: Equatable { if lhs.selectedPeerIds != rhs.selectedPeerIds { return false } + if arePeerArraysEqual(lhs.foundPeers, rhs.foundPeers) { + return false + } + if arePeerDictionariesEqual(lhs.selectedPeerMap, rhs.selectedPeerMap) { + return false + } if lhs.selectedAdditionalCategoryIds != rhs.selectedAdditionalCategoryIds { return false } @@ -283,7 +300,11 @@ private func mappedInsertEntries(context: AccountContext, nodeInteraction: ChatL 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: status, enabled: enabled, selection: editing ? .selectable(selected: selected) : .none, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: header, action: { _ in if let chatPeer = chatPeer { - nodeInteraction.peerSelected(chatPeer, nil) + if editing { + nodeInteraction.togglePeerSelected(chatPeer) + } else { + nodeInteraction.peerSelected(chatPeer, nil) + } } }, disabledAction: { _ in if let chatPeer = chatPeer { @@ -360,7 +381,11 @@ private func mappedUpdateEntries(context: AccountContext, nodeInteraction: ChatL 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: status, enabled: enabled, selection: editing ? .selectable(selected: selected) : .none, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: header, action: { _ in if let chatPeer = chatPeer { - nodeInteraction.peerSelected(chatPeer, nil) + if editing { + nodeInteraction.togglePeerSelected(chatPeer) + } else { + nodeInteraction.peerSelected(chatPeer, nil) + } } }, disabledAction: { _ in if let chatPeer = chatPeer { @@ -545,6 +570,9 @@ public final class ChatListNode: ListView { let preloadItems = Promise<[ChatHistoryPreloadItem]>([]) var didBeginSelectingChats: (() -> Void)? + public var selectionCountChanged: ((Int) -> Void)? + + var isSelectionGestureEnabled = true public init(context: AccountContext, groupId: PeerGroupId, chatListFilter: ChatListFilter? = nil, previewing: Bool, fillPreloadItems: Bool, mode: ChatListNodeMode, theme: PresentationTheme, fontSize: PresentationFontSize, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, nameSortOrder: PresentationPersonNameOrder, nameDisplayOrder: PresentationPersonNameOrder, disableAnimations: Bool) { self.context = context @@ -559,7 +587,7 @@ public final class ChatListNode: ListView { isSelecting = true } - self.currentState = ChatListNodeState(presentationData: ChatListPresentationData(theme: theme, fontSize: fontSize, strings: strings, dateTimeFormat: dateTimeFormat, nameSortOrder: nameSortOrder, nameDisplayOrder: nameDisplayOrder, disableAnimations: disableAnimations), editing: isSelecting, peerIdWithRevealedOptions: nil, selectedPeerIds: Set(), selectedAdditionalCategoryIds: Set(), peerInputActivities: nil, pendingRemovalPeerIds: Set(), pendingClearHistoryPeerIds: Set(), archiveShouldBeTemporaryRevealed: false, hiddenPsaPeerId: nil) + self.currentState = ChatListNodeState(presentationData: ChatListPresentationData(theme: theme, fontSize: fontSize, strings: strings, dateTimeFormat: dateTimeFormat, nameSortOrder: nameSortOrder, nameDisplayOrder: nameDisplayOrder, disableAnimations: disableAnimations), editing: isSelecting, peerIdWithRevealedOptions: nil, selectedPeerIds: Set(), foundPeers: [], selectedPeerMap: [:], selectedAdditionalCategoryIds: Set(), peerInputActivities: nil, pendingRemovalPeerIds: Set(), pendingClearHistoryPeerIds: Set(), archiveShouldBeTemporaryRevealed: false, hiddenPsaPeerId: nil) self.statePromise = ValuePromise(self.currentState, ignoreRepeated: true) self.theme = theme @@ -583,23 +611,55 @@ public final class ChatListNode: ListView { if let strongSelf = self, let disabledPeerSelected = strongSelf.disabledPeerSelected { disabledPeerSelected(peer) } - }, togglePeerSelected: { [weak self] peerId in + }, togglePeerSelected: { [weak self] peer in var didBeginSelecting = false + var count = 0 self?.updateState { state in var state = state - if state.selectedPeerIds.contains(peerId) { - state.selectedPeerIds.remove(peerId) + if state.selectedPeerIds.contains(peer.id) { + state.selectedPeerIds.remove(peer.id) } else { if state.selectedPeerIds.count < 100 { if state.selectedPeerIds.isEmpty { didBeginSelecting = true } - state.selectedPeerIds.insert(peerId) + state.selectedPeerIds.insert(peer.id) + state.selectedPeerMap[peer.id] = peer + } + } + count = state.selectedPeerIds.count + return state + } + self?.selectionCountChanged?(count) + if didBeginSelecting { + self?.didBeginSelectingChats?() + } + }, togglePeersSelection: { [weak self] peers, selected in + self?.updateState { state in + var state = state + if selected { + for peerEntry in peers { + switch peerEntry { + case let .peer(peer): + state.selectedPeerIds.insert(peer.id) + state.selectedPeerMap[peer.id] = peer + case let .peerId(peerId): + state.selectedPeerIds.insert(peerId) + } + } + } else { + for peerEntry in peers { + switch peerEntry { + case let .peer(peer): + state.selectedPeerIds.remove(peer.id) + case let .peerId(peerId): + state.selectedPeerIds.remove(peerId) + } } } return state } - if didBeginSelecting { + if selected && !peers.isEmpty { self?.didBeginSelectingChats?() } }, additionalCategorySelected: { [weak self] id in @@ -643,7 +703,7 @@ public final class ChatListNode: ListView { } else { location = .group(groupId) } - let _ = (toggleItemPinned(postbox: context.account.postbox, location: location, itemId: itemId) + let _ = (context.engine.peers.toggleItemPinned(location: location, itemId: itemId) |> deliverOnMainQueue).start(next: { result in if let strongSelf = self { switch result { @@ -665,7 +725,7 @@ public final class ChatListNode: ListView { return } strongSelf.setCurrentRemovingPeerId(peerId) - let _ = (togglePeerMuted(account: context.account, peerId: peerId) + let _ = (context.engine.peers.togglePeerMuted(peerId: peerId) |> deliverOnMainQueue).start(completed: { self?.updateState { state in var state = state @@ -772,7 +832,7 @@ public final class ChatListNode: ListView { let previousHideArchivedFolderByDefaultValue = previousHideArchivedFolderByDefault.swap(hideArchivedFolderByDefault) - let (rawEntries, isLoading) = chatListNodeEntriesForView(update.view, state: state, savedMessagesPeer: savedMessagesPeer, hideArchivedFolderByDefault: hideArchivedFolderByDefault, displayArchiveIntro: displayArchiveIntro, mode: mode) + let (rawEntries, isLoading) = chatListNodeEntriesForView(update.view, state: state, savedMessagesPeer: savedMessagesPeer, foundPeers: state.foundPeers, hideArchivedFolderByDefault: hideArchivedFolderByDefault, displayArchiveIntro: displayArchiveIntro, mode: mode) let entries = rawEntries.filter { entry in switch entry { case let .PeerEntry(_, _, _, _, _, _, peer, _, _, _, _, _, _, _, _, _): @@ -875,7 +935,7 @@ public final class ChatListNode: ListView { let removingPeerId = currentRemovingPeerId.with { $0 } - var disableAnimations = state.presentationData.disableAnimations + var disableAnimations = true if previousState.editing != state.editing { disableAnimations = false } else { @@ -1144,16 +1204,18 @@ public final class ChatListNode: ListView { } if case let .index(index) = fromEntry.sortIndex, let _ = index.pinningIndex { - return strongSelf.context.account.postbox.transaction { transaction -> Bool in - let location: TogglePeerChatPinnedLocation - if let chatListFilter = chatListFilter { - location = .filter(chatListFilter.id) - } else { - location = .group(groupId) - } - - var itemIds = getPinnedItemIds(transaction: transaction, location: location) - + let location: TogglePeerChatPinnedLocation + if let chatListFilter = chatListFilter { + location = .filter(chatListFilter.id) + } else { + location = .group(groupId) + } + + let engine = strongSelf.context.engine + return engine.peers.getPinnedItemIds(location: location) + |> mapToSignal { itemIds -> Signal in + var itemIds = itemIds + var itemId: PinnedItemId? switch fromEntry { case let .PeerEntry(index, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _): @@ -1161,7 +1223,7 @@ public final class ChatListNode: ListView { default: break } - + if let itemId = itemId { itemIds = itemIds.filter({ $0 != itemId }) if let referenceId = referenceId { @@ -1185,9 +1247,9 @@ public final class ChatListNode: ListView { } else { itemIds.append(itemId) } - return reorderPinnedItemIds(transaction: transaction, location: location, itemIds: itemIds) + return engine.peers.reorderPinnedItemIds(location: location, itemIds: itemIds) } else { - return false + return .single(false) } } } @@ -1197,7 +1259,7 @@ public final class ChatListNode: ListView { } var startedScrollingAtUpperBound = false - self.beganInteractiveDragging = { [weak self] in + self.beganInteractiveDragging = { [weak self] _ in guard let strongSelf = self else { return } @@ -1292,6 +1354,15 @@ public final class ChatListNode: ListView { } self.resetFilter() + + let selectionRecognizer = ChatHistoryListSelectionRecognizer(target: self, action: #selector(self.selectionPanGesture(_:))) + selectionRecognizer.shouldBegin = { [weak self] in + guard let strongSelf = self else { + return false + } + return strongSelf.isSelectionGestureEnabled + } + self.view.addGestureRecognizer(selectionRecognizer) } deinit { @@ -1309,7 +1380,7 @@ public final class ChatListNode: ListView { private func resetFilter() { if let chatListFilter = self.chatListFilter { - self.updatedFilterDisposable.set((updatedChatListFilters(postbox: self.context.account.postbox) + self.updatedFilterDisposable.set((self.context.engine.peers.updatedChatListFilters() |> map { filters -> ChatListFilter? in for filter in filters { if filter.id == chatListFilter.id { @@ -1332,7 +1403,7 @@ public final class ChatListNode: ListView { } 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 { + if theme !== self.currentState.presentationData.theme || strings !== self.currentState.presentationData.strings || dateTimeFormat != self.currentState.presentationData.dateTimeFormat { self.theme = theme if self.keepTopItemOverscrollBackground != nil { self.keepTopItemOverscrollBackground = ListViewKeepTopItemOverscrollBackground(color: theme.chatList.pinnedItemBackgroundColor, direction: true) @@ -1875,6 +1946,140 @@ public final class ChatListNode: ListView { } return nil } + + private func peerAtPoint(_ point: CGPoint) -> Peer? { + var resultPeer: Peer? + self.forEachVisibleItemNode { itemNode in + if resultPeer == nil, let itemNode = itemNode as? ListViewItemNode, itemNode.frame.contains(point) { + if let itemNode = itemNode as? ChatListItemNode, let item = itemNode.item { + switch item.content { + case let .peer(_, peer, _, _, _, _, _, _, _, _, _, _): + resultPeer = peer.peer + default: + break + } + } + } + } + return resultPeer + } + + private var selectionPanState: (selecting: Bool, initialPeerId: PeerId, toggledPeerIds: [[PeerId]])? + 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 peer = self.peerAtPoint(location) { + let selecting = !self.currentState.selectedPeerIds.contains(peer.id) + self.selectionPanState = (selecting, peer.id, []) + self.interaction?.togglePeersSelection([.peer(peer)], 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 + @unknown default: + fatalError() + } + } + + private func handlePanSelection(location: CGPoint) { + if let state = self.selectionPanState { + if let peer = self.peerAtPoint(location) { + if peer.id == state.initialPeerId { + if !state.toggledPeerIds.isEmpty { + self.interaction?.togglePeersSelection(state.toggledPeerIds.flatMap { $0.compactMap({ .peerId($0) }) }, !state.selecting) + self.selectionPanState = (state.selecting, state.initialPeerId, []) + } + } else if state.toggledPeerIds.last?.first != peer.id { + var updatedToggledPeerIds: [[PeerId]] = [] + var previouslyToggled = false + for i in (0 ..< state.toggledPeerIds.count) { + if let peerId = state.toggledPeerIds[i].first { + if peerId == peer.id { + previouslyToggled = true + updatedToggledPeerIds = Array(state.toggledPeerIds.prefix(i + 1)) + + let peerIdsToToggle = Array(state.toggledPeerIds.suffix(state.toggledPeerIds.count - i - 1)).flatMap { $0 } + self.interaction?.togglePeersSelection(peerIdsToToggle.compactMap { .peerId($0) }, !state.selecting) + break + } + } + } + + if !previouslyToggled { + updatedToggledPeerIds = state.toggledPeerIds + let isSelected = self.currentState.selectedPeerIds.contains(peer.id) + if state.selecting != isSelected { + updatedToggledPeerIds.append([peer.id]) + self.interaction?.togglePeersSelection([.peer(peer)], state.selecting) + } + } + + self.selectionPanState = (state.selecting, state.initialPeerId, updatedToggledPeerIds) + } + } + + 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 + let _ = 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 + } } private func statusStringForPeerType(accountPeerId: PeerId, strings: PresentationStrings, peer: Peer, isMuted: Bool, isUnread: Bool, isContact: Bool, hasUnseenMentions: Bool, chatListFilters: [ChatListFilter]?) -> (String, Bool)? { @@ -1928,3 +2133,65 @@ private func statusStringForPeerType(accountPeerId: PeerId, strings: Presentatio } return (strings.ChatList_PeerTypeNonContact, false) } + +public class ChatHistoryListSelectionRecognizer: UIPanGestureRecognizer { + private let selectionGestureActivationThreshold: CGFloat = 5.0 + + var recognized: Bool? = nil + var initialLocation: CGPoint = CGPoint() + + public var shouldBegin: (() -> Bool)? + + public override init(target: Any?, action: Selector?) { + super.init(target: target, action: action) + + self.minimumNumberOfTouches = 2 + self.maximumNumberOfTouches = 2 + } + + public override func reset() { + super.reset() + + self.recognized = nil + } + + public 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) + } + } + + public 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) + + let touchesArray = Array(touches) + if self.recognized == nil, touchesArray.count == 2 { + if let firstTouch = touchesArray.first, let secondTouch = touchesArray.last { + let firstLocation = firstTouch.location(in: self.view) + let secondLocation = secondTouch.location(in: self.view) + + func distance(_ v1: CGPoint, _ v2: CGPoint) -> CGFloat { + let dx = v1.x - v2.x + let dy = v1.y - v2.y + return sqrt(dx * dx + dy * dy) + } + if distance(firstLocation, secondLocation) > 200.0 { + self.state = .failed + } + } + if self.state != .failed && (abs(translation.y) >= selectionGestureActivationThreshold) { + self.recognized = true + } + } + + if let recognized = self.recognized, recognized { + super.touchesMoved(touches, with: event) + } + } +} diff --git a/submodules/ChatListUI/Sources/Node/ChatListNodeEntries.swift b/submodules/ChatListUI/Sources/Node/ChatListNodeEntries.swift index 7004665711..96eb178e95 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListNodeEntries.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListNodeEntries.swift @@ -282,7 +282,7 @@ private func offsetPinnedIndex(_ index: ChatListIndex, offset: UInt16) -> ChatLi } } -func chatListNodeEntriesForView(_ view: ChatListView, state: ChatListNodeState, savedMessagesPeer: Peer?, hideArchivedFolderByDefault: Bool, displayArchiveIntro: Bool, mode: ChatListNodeMode) -> (entries: [ChatListNodeEntry], loading: Bool) { +func chatListNodeEntriesForView(_ view: ChatListView, state: ChatListNodeState, savedMessagesPeer: Peer?, foundPeers: [Peer], hideArchivedFolderByDefault: Bool, displayArchiveIntro: Bool, mode: ChatListNodeMode) -> (entries: [ChatListNodeEntry], loading: Bool) { var result: [ChatListNodeEntry] = [] var pinnedIndexOffset: UInt16 = 0 @@ -299,6 +299,11 @@ func chatListNodeEntriesForView(_ view: ChatListView, state: ChatListNodeState, return item.info.peerId != state.hiddenPsaPeerId } + var foundPeerIds = Set() + for peer in foundPeers { + foundPeerIds.insert(peer.id) + } + if view.laterIndex == nil && savedMessagesPeer == nil { pinnedIndexOffset += UInt16(filteredAdditionalItemEntries.count) } @@ -306,7 +311,7 @@ func chatListNodeEntriesForView(_ view: ChatListView, state: ChatListNodeState, loop: for entry in view.entries { switch entry { case let .MessageEntry(index, messages, combinedReadState, isRemovedFromTotalUnreadCount, embeddedState, peer, peerPresence, summaryInfo, hasFailed, isContact): - if let savedMessagesPeer = savedMessagesPeer, savedMessagesPeer.id == index.messageIndex.id.peerId { + if let savedMessagesPeer = savedMessagesPeer, savedMessagesPeer.id == index.messageIndex.id.peerId || foundPeerIds.contains(index.messageIndex.id.peerId) { continue loop } if state.pendingRemovalPeerIds.contains(index.messageIndex.id.peerId) { @@ -331,6 +336,17 @@ func chatListNodeEntriesForView(_ view: ChatListView, state: ChatListNodeState, var pinningIndex: UInt16 = UInt16(pinnedIndexOffset == 0 ? 0 : (pinnedIndexOffset - 1)) if let savedMessagesPeer = savedMessagesPeer { + if !foundPeers.isEmpty { + var foundPinningIndex: UInt16 = UInt16(foundPeers.count) + for peer in foundPeers.reversed() { + let messageIndex = MessageIndex(id: MessageId(peerId: peer.id, namespace: 0, id: 0), timestamp: 1) + result.append(.PeerEntry(index: ChatListIndex(pinningIndex: foundPinningIndex, messageIndex: messageIndex), presentationData: state.presentationData, messages: [], readState: nil, isRemovedFromTotalUnreadCount: false, embeddedInterfaceState: nil, peer: RenderedPeer(peerId: peer.id, peers: SimpleDictionary([peer.id: peer])), presence: nil, summaryInfo: ChatListMessageTagSummaryInfo(), editing: state.editing, hasActiveRevealControls: false, selected: state.selectedPeerIds.contains(peer.id), inputActivities: nil, promoInfo: nil, hasFailedMessages: false, isContact: false)) + if foundPinningIndex != 0 { + foundPinningIndex -= 1 + } + } + } + result.append(.PeerEntry(index: ChatListIndex.absoluteUpperBound.predecessor, presentationData: state.presentationData, messages: [], readState: nil, isRemovedFromTotalUnreadCount: false, 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, promoInfo: nil, hasFailedMessages: false, isContact: false)) } else { if !filteredAdditionalItemEntries.isEmpty { @@ -360,7 +376,7 @@ func chatListNodeEntriesForView(_ view: ChatListView, state: ChatListNodeState, if view.laterIndex == nil, case .chatList = mode { for groupReference in view.groupEntries { - let messageIndex = MessageIndex(id: MessageId(peerId: PeerId(namespace: 0, id: 0), namespace: 0, id: 0), timestamp: 1) + let messageIndex = MessageIndex(id: MessageId(peerId: PeerId(0), namespace: 0, id: 0), timestamp: 1) result.append(.GroupReferenceEntry(index: ChatListIndex(pinningIndex: pinningIndex, messageIndex: messageIndex), presentationData: state.presentationData, groupId: groupReference.groupId, peers: groupReference.renderedPeers, message: groupReference.message, editing: state.editing, unreadState: groupReference.unreadState, revealed: state.archiveShouldBeTemporaryRevealed, hiddenByDefault: hideArchivedFolderByDefault)) if pinningIndex != 0 { pinningIndex -= 1 diff --git a/submodules/ChatListUI/Sources/TabBarChatListFilterController.swift b/submodules/ChatListUI/Sources/TabBarChatListFilterController.swift index ab390011a9..e930adb883 100644 --- a/submodules/ChatListUI/Sources/TabBarChatListFilterController.swift +++ b/submodules/ChatListUI/Sources/TabBarChatListFilterController.swift @@ -10,8 +10,8 @@ import Postbox import TelegramUIPreferences import TelegramCore -func chatListFilterItems(postbox: Postbox) -> Signal<(Int, [(ChatListFilter, Int, Bool)]), NoError> { - return updatedChatListFilters(postbox: postbox) +func chatListFilterItems(context: AccountContext) -> Signal<(Int, [(ChatListFilter, Int, Bool)]), NoError> { + return context.engine.peers.updatedChatListFilters() |> distinctUntilChanged |> mapToSignal { filters -> Signal<(Int, [(ChatListFilter, Int, Bool)]), NoError> in var unreadCountItems: [UnreadMessageCountsItem] = [] @@ -40,8 +40,8 @@ func chatListFilterItems(postbox: Postbox) -> Signal<(Int, [(ChatListFilter, Int keys.append(.basicPeer(peerId)) } - return combineLatest(queue: postbox.queue, - postbox.combinedView(keys: keys), + return combineLatest(queue: context.account.postbox.queue, + context.account.postbox.combinedView(keys: keys), Signal.single(true) ) |> map { view, _ -> (Int, [(ChatListFilter, Int, Bool)]) in @@ -63,7 +63,7 @@ func chatListFilterItems(postbox: Postbox) -> Signal<(Int, [(ChatListFilter, Int 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 = postbox.seedConfiguration.peerSummaryCounterTags(peer, peerView.isContact) + let tag = context.account.postbox.seedConfiguration.peerSummaryCounterTags(peer, peerView.isContact) var peerCount = Int(state.count) if state.isUnread { diff --git a/submodules/ChatMessageInteractiveMediaBadge/Sources/ChatMessageInteractiveMediaBadge.swift b/submodules/ChatMessageInteractiveMediaBadge/Sources/ChatMessageInteractiveMediaBadge.swift index eb0d2b272e..417499b57e 100644 --- a/submodules/ChatMessageInteractiveMediaBadge/Sources/ChatMessageInteractiveMediaBadge.swift +++ b/submodules/ChatMessageInteractiveMediaBadge/Sources/ChatMessageInteractiveMediaBadge.swift @@ -7,8 +7,8 @@ import TextFormat import RadialStatusNode import AppBundle -private let font = Font.regular(11.0) -private let boldFont = Font.semibold(11.0) +private let font = Font.with(size: 11.0, design: .regular, weight: .regular, traits: [.monospacedNumbers]) +private let boldFont = Font.with(size: 11.0, design: .regular, weight: .semibold, traits: [.monospacedNumbers]) public enum ChatMessageInteractiveMediaDownloadState: Equatable { case remote @@ -188,7 +188,7 @@ public final class ChatMessageInteractiveMediaBadge: ASDisplayNode { transition.updateAlpha(node: statusNode, alpha: active ? 1.0 : 0.0) } - let durationFrame = CGRect(x: active ? 42.0 : 7.0, y: active ? 6.0 : 3.0, width: durationSize.width, height: durationSize.height) + let durationFrame = CGRect(x: active ? 42.0 : 7.0, y: active ? 6.0 : 2.0 + UIScreenPixel, width: durationSize.width, height: durationSize.height) self.durationNode.bounds = CGRect(origin: CGPoint(), size: durationFrame.size) textTransition.updatePosition(node: self.durationNode, position: durationFrame.center) diff --git a/submodules/ChatTitleActivityNode/Sources/ChatPlayingActivityContentNode.swift b/submodules/ChatTitleActivityNode/Sources/ChatPlayingActivityContentNode.swift index d2d707ed8e..4dea30d2bf 100644 --- a/submodules/ChatTitleActivityNode/Sources/ChatPlayingActivityContentNode.swift +++ b/submodules/ChatTitleActivityNode/Sources/ChatPlayingActivityContentNode.swift @@ -100,7 +100,7 @@ class ChatPlayingActivityContentNode: ChatTitleActivityContentNode { self.addSubnode(self.indicatorNode) } - override func updateLayout(_ constrainedSize: CGSize, alignment: NSTextAlignment) -> CGSize { + override func updateLayout(_ constrainedSize: CGSize, offset: CGFloat, alignment: NSTextAlignment) -> CGSize { let size = self.textNode.updateLayout(constrainedSize) let indicatorSize = CGSize(width: 24.0, height: 16.0) let originX: CGFloat diff --git a/submodules/ChatTitleActivityNode/Sources/ChatRecordingVideoActivityContentNode.swift b/submodules/ChatTitleActivityNode/Sources/ChatRecordingVideoActivityContentNode.swift index 6fc499972a..1ff4e9a4c9 100644 --- a/submodules/ChatTitleActivityNode/Sources/ChatRecordingVideoActivityContentNode.swift +++ b/submodules/ChatTitleActivityNode/Sources/ChatRecordingVideoActivityContentNode.swift @@ -72,7 +72,7 @@ class ChatRecordingVideoActivityContentNode: ChatTitleActivityContentNode { self.addSubnode(self.indicatorNode) } - override func updateLayout(_ constrainedSize: CGSize, alignment: NSTextAlignment) -> CGSize { + override func updateLayout(_ constrainedSize: CGSize, offset: CGFloat, alignment: NSTextAlignment) -> CGSize { let size = self.textNode.updateLayout(constrainedSize) let indicatorSize = CGSize(width: 24.0, height: 16.0) let originX: CGFloat diff --git a/submodules/ChatTitleActivityNode/Sources/ChatRecordingVoiceActivityContentNode.swift b/submodules/ChatTitleActivityNode/Sources/ChatRecordingVoiceActivityContentNode.swift index 768d827f63..a5d1d03a12 100644 --- a/submodules/ChatTitleActivityNode/Sources/ChatRecordingVoiceActivityContentNode.swift +++ b/submodules/ChatTitleActivityNode/Sources/ChatRecordingVoiceActivityContentNode.swift @@ -90,7 +90,7 @@ class ChatRecordingVoiceActivityContentNode: ChatTitleActivityContentNode { self.addSubnode(self.indicatorNode) } - override func updateLayout(_ constrainedSize: CGSize, alignment: NSTextAlignment) -> CGSize { + override func updateLayout(_ constrainedSize: CGSize, offset: CGFloat, alignment: NSTextAlignment) -> CGSize { let size = self.textNode.updateLayout(constrainedSize) let indicatorSize = CGSize(width: 24.0, height: 16.0) let originX: CGFloat @@ -99,7 +99,7 @@ class ChatRecordingVoiceActivityContentNode: ChatTitleActivityContentNode { } else { originX = indicatorSize.width } - self.textNode.frame = CGRect(origin: CGPoint(x: originX, y: 0.0), size: size) + self.textNode.frame = CGRect(origin: CGPoint(x: originX, y: offset), size: size) self.indicatorNode.frame = CGRect(origin: CGPoint(x: self.textNode.frame.minX - indicatorSize.width, y: 0.0), size: indicatorSize) return CGSize(width: size.width + indicatorSize.width, height: size.height) } diff --git a/submodules/ChatTitleActivityNode/Sources/ChatTitleActivityContentNode.swift b/submodules/ChatTitleActivityNode/Sources/ChatTitleActivityContentNode.swift index 3578b12212..7bef4a850c 100644 --- a/submodules/ChatTitleActivityNode/Sources/ChatTitleActivityContentNode.swift +++ b/submodules/ChatTitleActivityNode/Sources/ChatTitleActivityContentNode.swift @@ -122,13 +122,13 @@ public class ChatTitleActivityContentNode: ASDisplayNode { } } - public func updateLayout(_ constrainedSize: CGSize, alignment: NSTextAlignment) -> CGSize { + public func updateLayout(_ constrainedSize: CGSize, offset: CGFloat, alignment: NSTextAlignment) -> CGSize { let size = self.textNode.updateLayout(constrainedSize) self.textNode.bounds = CGRect(origin: CGPoint(), size: size) if case .center = alignment { - self.textNode.position = CGPoint(x: 0.0, y: size.height / 2.0) + self.textNode.position = CGPoint(x: 0.0, y: size.height / 2.0 + offset) } else { - self.textNode.position = CGPoint(x: size.width / 2.0, y: size.height / 2.0) + self.textNode.position = CGPoint(x: size.width / 2.0, y: size.height / 2.0 + offset) } return size } diff --git a/submodules/ChatTitleActivityNode/Sources/ChatTitleActivityNode.swift b/submodules/ChatTitleActivityNode/Sources/ChatTitleActivityNode.swift index 2ccf9a29f5..d07bd226a4 100644 --- a/submodules/ChatTitleActivityNode/Sources/ChatTitleActivityNode.swift +++ b/submodules/ChatTitleActivityNode/Sources/ChatTitleActivityNode.swift @@ -123,7 +123,7 @@ public class ChatTitleActivityNode: ASDisplayNode { } } - public func updateLayout(_ constrainedSize: CGSize, alignment: NSTextAlignment) -> CGSize { - return CGSize(width: 0.0, height: self.contentNode?.updateLayout(constrainedSize, alignment: alignment).height ?? 0.0) + public func updateLayout(_ constrainedSize: CGSize, offset: CGFloat = 0.0, alignment: NSTextAlignment) -> CGSize { + return CGSize(width: 0.0, height: self.contentNode?.updateLayout(constrainedSize, offset: offset, alignment: alignment).height ?? 0.0) } } diff --git a/submodules/ChatTitleActivityNode/Sources/ChatTypingActivityContentNode.swift b/submodules/ChatTitleActivityNode/Sources/ChatTypingActivityContentNode.swift index 40ce563d6e..0581cf999c 100644 --- a/submodules/ChatTitleActivityNode/Sources/ChatTypingActivityContentNode.swift +++ b/submodules/ChatTitleActivityNode/Sources/ChatTypingActivityContentNode.swift @@ -108,7 +108,7 @@ class ChatTypingActivityContentNode: ChatTitleActivityContentNode { self.addSubnode(self.indicatorNode) } - override func updateLayout(_ constrainedSize: CGSize, alignment: NSTextAlignment) -> CGSize { + override func updateLayout(_ constrainedSize: CGSize, offset: CGFloat, alignment: NSTextAlignment) -> CGSize { let indicatorSize = CGSize(width: 24.0, height: 16.0) let size = self.textNode.updateLayout(CGSize(width: constrainedSize.width - indicatorSize.width, height: constrainedSize.height)) var originX: CGFloat diff --git a/submodules/ChatTitleActivityNode/Sources/ChatUploadingActivityContentNode.swift b/submodules/ChatTitleActivityNode/Sources/ChatUploadingActivityContentNode.swift index a5cebb8109..829ff60db5 100644 --- a/submodules/ChatTitleActivityNode/Sources/ChatUploadingActivityContentNode.swift +++ b/submodules/ChatTitleActivityNode/Sources/ChatUploadingActivityContentNode.swift @@ -80,7 +80,7 @@ class ChatUploadingActivityContentNode: ChatTitleActivityContentNode { self.addSubnode(self.indicatorNode) } - override func updateLayout(_ constrainedSize: CGSize, alignment: NSTextAlignment) -> CGSize { + override func updateLayout(_ constrainedSize: CGSize, offset: CGFloat, alignment: NSTextAlignment) -> CGSize { let size = self.textNode.updateLayout(constrainedSize) let indicatorSize = CGSize(width: 24.0, height: 16.0) let originX: CGFloat diff --git a/submodules/ComposePollUI/Sources/CreatePollController.swift b/submodules/ComposePollUI/Sources/CreatePollController.swift index 1ea4d85aa7..4c93ee41c6 100644 --- a/submodules/ComposePollUI/Sources/CreatePollController.swift +++ b/submodules/ComposePollUI/Sources/CreatePollController.swift @@ -825,7 +825,7 @@ public func createPollController(context: AccountContext, peer: Peer, isQuiz: Bo 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: [], solution: resolvedSolution), isClosed: false, deadlineTimeout: deadlineTimeout)), replyToMessageId: nil, localGroupingKey: nil)) + completion(.message(text: "", attributes: [], mediaReference: .standalone(media: TelegramMediaPoll(pollId: MediaId(namespace: Namespaces.Media.LocalPoll, id: Int64.random(in: Int64.min ... Int64.max)), publicity: publicity, kind: kind, text: processPollText(state.text), options: options, correctAnswers: correctAnswers, results: TelegramMediaPollResults(voters: nil, totalVoters: nil, recentVoters: [], solution: resolvedSolution), isClosed: false, deadlineTimeout: deadlineTimeout)), replyToMessageId: nil, localGroupingKey: nil, correlationId: nil)) }) let leftNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Cancel), style: .regular, enabled: true, action: { diff --git a/submodules/ContactListUI/Sources/ContactAddItem.swift b/submodules/ContactListUI/Sources/ContactAddItem.swift index 7a8699cefc..f3c9093614 100644 --- a/submodules/ContactListUI/Sources/ContactAddItem.swift +++ b/submodules/ContactListUI/Sources/ContactAddItem.swift @@ -245,9 +245,9 @@ class ContactsAddItemNode: ListViewItemNode { self.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration * 0.5, removeOnCompletion: false) } - override public func header() -> ListViewItemHeader? { + override public func headers() -> [ListViewItemHeader]? { if let (item, _, _, _, _) = self.layoutParams { - return item.header + return item.header.flatMap { [$0] } } else { return nil } diff --git a/submodules/ContactListUI/Sources/ContactContextMenus.swift b/submodules/ContactListUI/Sources/ContactContextMenus.swift index 2a525d8e60..17dbcc8682 100644 --- a/submodules/ContactListUI/Sources/ContactContextMenus.swift +++ b/submodules/ContactListUI/Sources/ContactContextMenus.swift @@ -61,7 +61,7 @@ func contactContextMenuItems(context: AccountContext, peerId: PeerId, contactsCo context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(currentPeerId), peekData: nil)) } } else { - var createSignal = createSecretChat(account: context.account, peerId: peerId) + var createSignal = context.engine.peers.createSecretChat(peerId: peerId) var cancelImpl: (() -> Void)? let progressSignal = Signal { subscriber in let presentationData = context.sharedContext.currentPresentationData.with { $0 } diff --git a/submodules/ContactListUI/Sources/ContactListActionItem.swift b/submodules/ContactListUI/Sources/ContactListActionItem.swift index f0e708ef16..f27118bdab 100644 --- a/submodules/ContactListUI/Sources/ContactListActionItem.swift +++ b/submodules/ContactListUI/Sources/ContactListActionItem.swift @@ -20,16 +20,18 @@ public class ContactListActionItem: ListViewItem, ListViewItemWithHeader { let icon: ContactListActionItemIcon let highlight: ContactListActionItemHighlight let clearHighlightAutomatically: Bool + let accessible: Bool let action: () -> Void public let header: ListViewItemHeader? - public init(presentationData: ItemListPresentationData, title: String, icon: ContactListActionItemIcon, highlight: ContactListActionItemHighlight = .cell, clearHighlightAutomatically: Bool = true, header: ListViewItemHeader?, action: @escaping () -> Void) { + public init(presentationData: ItemListPresentationData, title: String, icon: ContactListActionItemIcon, highlight: ContactListActionItemHighlight = .cell, clearHighlightAutomatically: Bool = true, accessible: Bool = true, header: ListViewItemHeader?, action: @escaping () -> Void) { self.presentationData = presentationData self.title = title self.icon = icon self.highlight = highlight self.header = header self.clearHighlightAutomatically = clearHighlightAutomatically + self.accessible = accessible self.action = action } @@ -205,6 +207,14 @@ class ContactListActionItemNode: ListViewItemNode { strongSelf.iconNode.image = generateTintedImage(image: item.icon.image, color: item.presentationData.theme.list.itemAccentColor) } + if item.accessible && strongSelf.activateArea.supernode == nil { + strongSelf.view.accessibilityElementsHidden = false + strongSelf.addSubnode(strongSelf.activateArea) + } else if !item.accessible && strongSelf.activateArea.supernode != nil { + strongSelf.view.accessibilityElementsHidden = true + strongSelf.activateArea.removeFromSupernode() + } + let _ = titleApply() var titleOffset = leftInset @@ -315,9 +325,9 @@ class ContactListActionItemNode: ListViewItemNode { self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false) } - override public func header() -> ListViewItemHeader? { + override public func headers() -> [ListViewItemHeader]? { if let item = self.item { - return item.header + return item.header.flatMap { [$0] } } else { return nil } diff --git a/submodules/ContactListUI/Sources/ContactListNameIndexHeader.swift b/submodules/ContactListUI/Sources/ContactListNameIndexHeader.swift index 788c81b52d..7390365843 100644 --- a/submodules/ContactListUI/Sources/ContactListNameIndexHeader.swift +++ b/submodules/ContactListUI/Sources/ContactListNameIndexHeader.swift @@ -5,20 +5,21 @@ import TelegramPresentationData import ListSectionHeaderNode final class ContactListNameIndexHeader: Equatable, ListViewItemHeader { - let id: Int64 + let id: ListViewItemNode.HeaderId let theme: PresentationTheme let letter: unichar let stickDirection: ListViewItemHeaderStickDirection = .top + public let stickOverInsets: Bool = true let height: CGFloat = 29.0 init(theme: PresentationTheme, letter: unichar) { self.theme = theme self.letter = letter - self.id = Int64(letter) + self.id = ListViewItemNode.HeaderId(space: 0, id: Int64(letter)) } - func node() -> ListViewItemHeaderNode { + func node(synchronousLoad: Bool) -> ListViewItemHeaderNode { return ContactListNameIndexHeaderNode(theme: self.theme, letter: self.letter) } diff --git a/submodules/ContactListUI/Sources/ContactListNode.swift b/submodules/ContactListUI/Sources/ContactListNode.swift index da49058cd0..0149b4b795 100644 --- a/submodules/ContactListUI/Sources/ContactListNode.swift +++ b/submodules/ContactListUI/Sources/ContactListNode.swift @@ -163,7 +163,7 @@ private enum ContactListNodeEntry: Comparable, Identifiable { if case .presence = sortOrder { text = strings.Contacts_SortedByPresence } - return ContactListActionItem(presentationData: ItemListPresentationData(presentationData), title: text, icon: .inline(dropDownIcon, .right), highlight: .alpha, header: nil, action: { + return ContactListActionItem(presentationData: ItemListPresentationData(presentationData), title: text, icon: .inline(dropDownIcon, .right), highlight: .alpha, accessible: false, header: nil, action: { interaction.openSortMenu() }) case let .permissionInfo(_, title, text, suppressed): @@ -240,7 +240,7 @@ private enum ContactListNodeEntry: Comparable, Identifiable { })] } - return ContactsPeerItem(presentationData: ItemListPresentationData(presentationData), sortOrder: nameSortOrder, displayOrder: nameDisplayOrder, context: context, peerMode: isSearch ? .generalSearch : .peer, peer: itemPeer, status: status, enabled: enabled, selection: selection, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), additionalActions: additionalActions, index: nil, header: header, action: { _ in + return ContactsPeerItem(presentationData: ItemListPresentationData(presentationData), sortOrder: nameSortOrder, displayOrder: nameDisplayOrder, context: context, peerMode: isSearch ? .generalSearch : .peer, peer: itemPeer, status: status, enabled: enabled, selection: selection, selectionPosition: .left, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), additionalActions: additionalActions, index: nil, header: header, action: { _ in interaction.openPeer(peer, .generic) }, itemHighlighting: interaction.itemHighlighting, contextAction: itemContextAction) } @@ -594,10 +594,46 @@ private func contactListNodeEntries(accountPeer: Peer?, peers: [ContactListPeer] } } + + var index: Int = 0 + + var existingPeerIds = Set() + if let selectionState = selectionState { + for peer in selectionState.foundPeers { + if existingPeerIds.contains(peer.id) { + continue + } + existingPeerIds.insert(peer.id) + + let selection: ContactsPeerItemSelection = .selectable(selected: selectionState.selectedPeerIndices[peer.id] != nil) + + var presence: PeerPresence? + if case let .peer(peer, _, _) = peer { + presence = presences[peer.id] + } + let enabled: Bool + switch peer { + case let .peer(peer, _, _): + enabled = !disabledPeerIds.contains(peer.id) + default: + enabled = true + } + + entries.append(.peer(index, peer, presence, nil, selection, theme, strings, dateTimeFormat, sortOrder, displayOrder, displayCallIcons, enabled)) + index += 1 + } + } + for i in 0 ..< orderedPeers.count { + let peer = orderedPeers[i] + if existingPeerIds.contains(peer.id) { + continue + } + existingPeerIds.insert(peer.id) + let selection: ContactsPeerItemSelection if let selectionState = selectionState { - selection = .selectable(selected: selectionState.selectedPeerIndices[orderedPeers[i].id] != nil) + selection = .selectable(selected: selectionState.selectedPeerIndices[peer.id] != nil) } else { selection = .none } @@ -606,20 +642,21 @@ private func contactListNodeEntries(accountPeer: Peer?, peers: [ContactListPeer] case .orderedByPresence: header = commonHeader default: - header = headers[orderedPeers[i].id] + header = headers[peer.id] } var presence: PeerPresence? - if case let .peer(peer, _, _) = orderedPeers[i] { + if case let .peer(peer, _, _) = peer { presence = presences[peer.id] } let enabled: Bool - switch orderedPeers[i] { + switch peer { case let .peer(peer, _, _): enabled = !disabledPeerIds.contains(peer.id) default: enabled = true } - entries.append(.peer(i, orderedPeers[i], presence, header, selection, theme, strings, dateTimeFormat, sortOrder, displayOrder, displayCallIcons, enabled)) + entries.append(.peer(index, peer, presence, header, selection, theme, strings, dateTimeFormat, sortOrder, displayOrder, displayCallIcons, enabled)) + index += 1 } return entries } @@ -706,15 +743,21 @@ public enum ContactListPresentation { public struct ContactListNodeGroupSelectionState: Equatable { public let selectedPeerIndices: [ContactListPeerId: Int] + public let foundPeers: [ContactListPeer] + public let selectedPeerMap: [ContactListPeerId: ContactListPeer] public let nextSelectionIndex: Int - private init(selectedPeerIndices: [ContactListPeerId: Int], nextSelectionIndex: Int) { + private init(selectedPeerIndices: [ContactListPeerId: Int], foundPeers: [ContactListPeer], selectedPeerMap: [ContactListPeerId: ContactListPeer], nextSelectionIndex: Int) { self.selectedPeerIndices = selectedPeerIndices + self.foundPeers = foundPeers + self.selectedPeerMap = selectedPeerMap self.nextSelectionIndex = nextSelectionIndex } public init() { self.selectedPeerIndices = [:] + self.foundPeers = [] + self.selectedPeerMap = [:] self.nextSelectionIndex = 0 } @@ -722,13 +765,21 @@ public struct ContactListNodeGroupSelectionState: Equatable { var updatedIndices = self.selectedPeerIndices if let _ = updatedIndices[peerId] { updatedIndices.removeValue(forKey: peerId) - return ContactListNodeGroupSelectionState(selectedPeerIndices: updatedIndices, nextSelectionIndex: self.nextSelectionIndex) + return ContactListNodeGroupSelectionState(selectedPeerIndices: updatedIndices, foundPeers: self.foundPeers, selectedPeerMap: self.selectedPeerMap, nextSelectionIndex: self.nextSelectionIndex) } else { updatedIndices[peerId] = self.nextSelectionIndex - return ContactListNodeGroupSelectionState(selectedPeerIndices: updatedIndices, nextSelectionIndex: self.nextSelectionIndex + 1) + return ContactListNodeGroupSelectionState(selectedPeerIndices: updatedIndices, foundPeers: self.foundPeers, selectedPeerMap: self.selectedPeerMap, nextSelectionIndex: self.nextSelectionIndex + 1) } } -} + + public func withFoundPeers(_ foundPeers: [ContactListPeer]) -> ContactListNodeGroupSelectionState { + return ContactListNodeGroupSelectionState(selectedPeerIndices: self.selectedPeerIndices, foundPeers: foundPeers, selectedPeerMap: self.selectedPeerMap, nextSelectionIndex: self.nextSelectionIndex) + } + + public func withSelectedPeerMap(_ selectedPeerMap: [ContactListPeerId: ContactListPeer]) -> ContactListNodeGroupSelectionState { + return ContactListNodeGroupSelectionState(selectedPeerIndices: self.selectedPeerIndices, foundPeers: self.foundPeers, selectedPeerMap: selectedPeerMap, nextSelectionIndex: self.nextSelectionIndex) + } + } public final class ContactListNode: ASDisplayNode { private let context: AccountContext @@ -754,11 +805,35 @@ public final class ContactListNode: ASDisplayNode { private var selectionStateValue: ContactListNodeGroupSelectionState? { didSet { self.selectionStatePromise.set(.single(self.selectionStateValue)) + self.selectionStateUpdated?(self.selectionStateValue) } } public var selectionState: ContactListNodeGroupSelectionState? { return self.selectionStateValue } + public var selectionStateUpdated: ((ContactListNodeGroupSelectionState?) -> Void)? + + public var selectedPeers: [ContactListPeer] { + if let selectionState = self.selectionState { + var selectedPeers: [ContactListPeer] = [] + var selectedIndices: [(Int, ContactListPeerId)] = [] + for (id, index) in selectionState.selectedPeerIndices { + selectedIndices.append((index, id)) + } + selectedIndices.sort(by: { lhs, rhs in + return lhs.0 < rhs.0 + }) + for (_, id) in selectedIndices { + if let peer = selectionState.selectedPeerMap[id] { + selectedPeers.append(peer) + } + } + return selectedPeers + } else { + return [] + } + } + private var interaction: ContactListNodeInteraction? private var enableUpdatesValue = false @@ -799,17 +874,20 @@ public final class ContactListNode: ASDisplayNode { private var authorizationNode: PermissionContentNode private let displayPermissionPlaceholder: Bool - public init(context: AccountContext, presentation: Signal, filters: [ContactListFilter] = [.excludeSelf], selectionState: ContactListNodeGroupSelectionState? = nil, displayPermissionPlaceholder: Bool = true, displaySortOptions: Bool = false, displayCallIcons: Bool = false, contextAction: ((Peer, ASDisplayNode, ContextGesture?) -> Void)? = nil, isSearch: Bool = false) { + public var multipleSelection = false + + public init(context: AccountContext, presentation: Signal, filters: [ContactListFilter] = [.excludeSelf], selectionState: ContactListNodeGroupSelectionState? = nil, displayPermissionPlaceholder: Bool = true, displaySortOptions: Bool = false, displayCallIcons: Bool = false, contextAction: ((Peer, ASDisplayNode, ContextGesture?) -> Void)? = nil, isSearch: Bool = false, multipleSelection: Bool = false) { self.context = context self.filters = filters self.displayPermissionPlaceholder = displayPermissionPlaceholder self.contextAction = contextAction + self.multipleSelection = multipleSelection let presentationData = context.sharedContext.currentPresentationData.with { $0 } self.presentationData = presentationData self.listNode = ListView() - self.listNode.dynamicBounceEnabled = !self.presentationData.disableAnimations + self.listNode.dynamicBounceEnabled = false self.listNode.accessibilityPageScrolledString = { row, count in return presentationData.strings.VoiceOver_ScrollStatus(row, count).0 } @@ -864,6 +942,7 @@ public final class ContactListNode: ASDisplayNode { let processingQueue = Queue() let previousEntries = Atomic<[ContactListNodeEntry]?>(value: nil) + let previousSelectionState = Atomic(value: nil) let interaction = ContactListNodeInteraction(activateSearch: { [weak self] in self?.activateSearch?() @@ -874,7 +953,26 @@ public final class ContactListNode: ASDisplayNode { }, suppressWarning: { [weak self] in self?.suppressPermissionWarning?() }, openPeer: { [weak self] peer, action in - self?.openPeer?(peer, action) + if let strongSelf = self { + if multipleSelection { + var updated = false + strongSelf.updateSelectionState({ state in + if let state = state { + updated = true + var selectedPeerMap = state.selectedPeerMap + selectedPeerMap[peer.id] = peer + return state.withToggledPeerId(peer.id).withSelectedPeerMap(selectedPeerMap) + } else { + return nil + } + }) + if !updated { + strongSelf.openPeer?(peer, action) + } + } else { + strongSelf.openPeer?(peer, action) + } + } }, contextAction: contextAction) self.indexNode.indexSelected = { [weak self] section in @@ -1000,7 +1098,7 @@ public final class ContactListNode: ASDisplayNode { if globalSearch { foundRemoteContacts = foundRemoteContacts |> then( - searchPeers(account: context.account, query: query) + context.engine.peers.searchPeers(query: query) |> map { ($0.0, $0.1) } |> delay(0.2, queue: Queue.concurrentDefaultQueue()) ) @@ -1037,6 +1135,15 @@ public final class ContactListNode: ASDisplayNode { var peers: [ContactListPeer] = [] + if let selectionState = selectionState { + for peer in selectionState.foundPeers { + if case let .peer(peer, _, _) = peer { + existingPeerIds.insert(peer.id) + } + peers.append(peer) + } + } + if !excludeSelf && !existingPeerIds.contains(accountPeer.id) { let lowercasedQuery = query.lowercased() if presentationData.strings.DialogList_SavedMessages.lowercased().hasPrefix(lowercasedQuery) || "saved messages".hasPrefix(lowercasedQuery) { @@ -1210,6 +1317,7 @@ public final class ContactListNode: ASDisplayNode { } 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, displayCallIcons: displayCallIcons) let previous = previousEntries.swap(entries) + let previousSelection = previousSelectionState.swap(selectionState) var hadPermissionInfo = false if let previous = previous { @@ -1229,11 +1337,10 @@ public final class ContactListNode: ASDisplayNode { } let animation: ContactListAnimation - if hadPermissionInfo != hasPermissionInfo { + if (previousSelection == nil) != (selectionState == nil) { + animation = .insertion + } else if hadPermissionInfo != hasPermissionInfo { animation = .insertion - } - else if let previous = previous, !presentationData.disableAnimations, (entries.count - previous.count) < 20 { - animation = .default } else { animation = .none } @@ -1260,11 +1367,10 @@ public final class ContactListNode: ASDisplayNode { if let strongSelf = self { let previousTheme = strongSelf.presentationData.theme let previousStrings = strongSelf.presentationData.strings - let previousDisableAnimations = strongSelf.presentationData.disableAnimations strongSelf.presentationData = presentationData - if previousTheme !== presentationData.theme || previousStrings !== presentationData.strings || previousDisableAnimations != presentationData.disableAnimations { + if previousTheme !== presentationData.theme || previousStrings !== presentationData.strings { strongSelf.backgroundColor = presentationData.theme.chatList.backgroundColor strongSelf.listNode.verticalScrollIndicatorColor = presentationData.theme.list.scrollIndicatorColor strongSelf.presentationDataPromise.set(.single(presentationData)) @@ -1279,7 +1385,7 @@ public final class ContactListNode: ASDisplayNode { strongSelf.authorizationNode.isHidden = authorizationPreviousHidden strongSelf.addSubnode(strongSelf.authorizationNode) - strongSelf.listNode.dynamicBounceEnabled = !presentationData.disableAnimations + strongSelf.listNode.dynamicBounceEnabled = false strongSelf.listNode.forEachAccessoryItemNode({ accessoryItemNode in if let accessoryItemNode = accessoryItemNode as? ContactsSectionHeaderAccessoryItemNode { @@ -1337,7 +1443,6 @@ public final class ContactListNode: ASDisplayNode { } public func updateSelectedChatLocation(_ chatLocation: ChatLocation?, progress: CGFloat, transition: ContainedViewLayoutTransition) { - self.interaction?.itemHighlighting.chatLocation = chatLocation self.interaction?.itemHighlighting.progress = progress @@ -1352,7 +1457,7 @@ public final class ContactListNode: ASDisplayNode { self.disposable.dispose() self.presentationDataDisposable?.dispose() } - + public func updateSelectionState(_ f: (ContactListNodeGroupSelectionState?) -> ContactListNodeGroupSelectionState?) { let updatedSelectionState = f(self.selectionStateValue) if updatedSelectionState != self.selectionStateValue { diff --git a/submodules/ContactListUI/Sources/ContactsController.swift b/submodules/ContactListUI/Sources/ContactsController.swift index 1ec9005311..df4c849526 100644 --- a/submodules/ContactListUI/Sources/ContactsController.swift +++ b/submodules/ContactListUI/Sources/ContactsController.swift @@ -89,9 +89,6 @@ public class ContactsController: ViewController { public var switchToChatsController: (() -> Void)? - private let preloadedSticker = Promise(nil) - private let preloadStickerDisposable = MetaDisposable() - public override func updateNavigationCustomData(_ data: Any?, progress: CGFloat, transition: ContainedViewLayoutTransition) { if self.isNodeLoaded { self.contactsNode.contactListNode.updateSelectedChatLocation(data as? ChatLocation, progress: progress, transition: transition) @@ -235,24 +232,16 @@ public class ContactsController: ViewController { scrollToEndIfExists = true } - let _ = (strongSelf.preloadedSticker.get() - |> take(1) - |> deliverOnMainQueue).start(next: { [weak self] greetingSticker in - if let strongSelf = self { - strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(peer.id), purposefulAction: { [weak self] in - if fromSearch { - self?.deactivateSearch(animated: false) - self?.switchToChatsController?() - } - }, scrollToEndIfExists: scrollToEndIfExists, greetingData: greetingSticker.flatMap({ ChatGreetingData(sticker: $0) }), options: [.removeOnMasterDetails], completion: { [weak self] _ in - if let strongSelf = self { - strongSelf.contactsNode.contactListNode.listNode.clearHighlightAnimated(true) - } - })) - - strongSelf.prepareRandomGreetingSticker() + strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(peer.id), purposefulAction: { [weak self] in + if fromSearch { + self?.deactivateSearch(animated: false) + self?.switchToChatsController?() } - }) + }, scrollToEndIfExists: scrollToEndIfExists, options: [.removeOnMasterDetails], completion: { [weak self] _ in + if let strongSelf = self { + strongSelf.contactsNode.contactListNode.listNode.clearHighlightAnimated(true) + } + })) } case let .deviceContact(id, _): let _ = ((strongSelf.context.sharedContext.contactDataManager?.extendedData(stableId: id) ?? .single(nil)) @@ -423,14 +412,6 @@ public class ContactsController: ViewController { self.contactsNode.contactListNode.enableUpdates = false } - public override func displayNodeDidLoad() { - super.displayNodeDidLoad() - - Queue.mainQueue().after(1.0) { - self.prepareRandomGreetingSticker() - } - } - override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { super.containerLayoutUpdated(layout, transition: transition) @@ -438,7 +419,7 @@ public class ContactsController: ViewController { self.validLayout = layout - self.contactsNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationInsetHeight, actualNavigationBarHeight: self.navigationHeight, transition: transition) + self.contactsNode.containerLayoutUpdated(layout, navigationBarHeight: self.cleanNavigationHeight, actualNavigationBarHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition) } private func activateSearch() { @@ -525,27 +506,4 @@ public class ContactsController: ViewController { } }) } - - - private func prepareRandomGreetingSticker() { - let context = self.context - self.preloadedSticker.set(.single(nil) - |> then(randomGreetingSticker(account: context.account) - |> map { item in - return item?.file - })) - - self.preloadStickerDisposable.set((self.preloadedSticker.get() - |> mapToSignal { sticker -> Signal in - if let sticker = sticker { - let _ = freeMediaFileInteractiveFetched(account: context.account, fileReference: .standalone(media: sticker)).start() - return chatMessageAnimationData(postbox: context.account.postbox, resource: sticker.resource, fitzModifier: nil, width: 384, height: 384, synchronousLoad: false) - |> mapToSignal { _ -> Signal in - return .complete() - } - } else { - return .complete() - } - }).start()) - } } diff --git a/submodules/ContactListUI/Sources/ContactsSearchContainerNode.swift b/submodules/ContactListUI/Sources/ContactsSearchContainerNode.swift index 0c727b739b..d337d880b6 100644 --- a/submodules/ContactListUI/Sources/ContactsSearchContainerNode.swift +++ b/submodules/ContactListUI/Sources/ContactsSearchContainerNode.swift @@ -269,7 +269,7 @@ public final class ContactsSearchContainerNode: SearchDisplayControllerContentNo if categories.contains(.global) { foundRemoteContacts = .single(previousFoundRemoteContacts.with({ $0 })) |> then( - searchPeers(account: context.account, query: query) + context.engine.peers.searchPeers(query: query) |> map { ($0.0, $0.1) } |> delay(0.2, queue: Queue.concurrentDefaultQueue()) ) @@ -414,7 +414,7 @@ public final class ContactsSearchContainerNode: SearchDisplayControllerContentNo } })) - self.listNode.beganInteractiveDragging = { [weak self] in + self.listNode.beganInteractiveDragging = { [weak self] _ in self?.dismissInput?() } } diff --git a/submodules/ContactListUI/Sources/InviteContactsController.swift b/submodules/ContactListUI/Sources/InviteContactsController.swift index bf20344656..4a62814ed5 100644 --- a/submodules/ContactListUI/Sources/InviteContactsController.swift +++ b/submodules/ContactListUI/Sources/InviteContactsController.swift @@ -199,7 +199,7 @@ public class InviteContactsController: ViewController, MFMessageComposeViewContr override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { super.containerLayoutUpdated(layout, transition: transition) - self.contactsNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationInsetHeight, actualNavigationBarHeight: self.navigationHeight, transition: transition) + self.contactsNode.containerLayoutUpdated(layout, navigationBarHeight: self.cleanNavigationHeight, actualNavigationBarHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition) } private func activateSearch() { diff --git a/submodules/ContactListUI/Sources/InviteContactsControllerNode.swift b/submodules/ContactListUI/Sources/InviteContactsControllerNode.swift index 0523dd25e8..13e4f3283a 100644 --- a/submodules/ContactListUI/Sources/InviteContactsControllerNode.swift +++ b/submodules/ContactListUI/Sources/InviteContactsControllerNode.swift @@ -58,7 +58,7 @@ private enum InviteContactsEntry: Comparable, Identifiable { } else { 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: []) + let peer = TelegramUser(id: PeerId(namespace: .max, id: PeerId.Id._internalFromInt32Value(0)), accessHash: nil, firstName: contact.firstName, lastName: contact.lastName, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: []) 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) }) @@ -365,7 +365,7 @@ final class InviteContactsControllerNode: ASDisplayNode { return DeviceContactNormalizedPhoneNumber(rawValue: formatPhoneNumber(phoneNumber.value)) }))) } - return deviceContactsImportedByCount(postbox: context.account.postbox, contacts: mappedContacts) + return context.engine.contacts.deviceContactsImportedByCount(contacts: mappedContacts) |> map { counts -> [(DeviceContactStableId, DeviceContactBasicData, Int32)]? in var result: [(DeviceContactStableId, DeviceContactBasicData, Int32)] = [] var contactValues: [DeviceContactStableId: DeviceContactBasicData] = [:] diff --git a/submodules/ContactListUI/Sources/InviteContactsCountPanelNode.swift b/submodules/ContactListUI/Sources/InviteContactsCountPanelNode.swift index 47d68ed1af..2abe674765 100644 --- a/submodules/ContactListUI/Sources/InviteContactsCountPanelNode.swift +++ b/submodules/ContactListUI/Sources/InviteContactsCountPanelNode.swift @@ -37,7 +37,7 @@ final class InviteContactsCountPanelNode: ASDisplayNode { super.init() - self.backgroundColor = theme.rootController.navigationBar.backgroundColor + self.backgroundColor = theme.rootController.navigationBar.opaqueBackgroundColor self.addSubnode(self.button) self.addSubnode(self.separatorNode) diff --git a/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift b/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift index 5db2eab9ea..1d5c1f259e 100644 --- a/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift +++ b/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift @@ -40,6 +40,11 @@ public enum ContactsPeerItemSelection: Equatable { case selectable(selected: Bool) } +public enum ContactsPeerItemSelectionPosition: Equatable { + case left + case right +} + public struct ContactsPeerItemEditing: Equatable { public var editable: Bool public var editing: Bool @@ -130,6 +135,7 @@ public class ContactsPeerItem: ItemListItem, ListViewItemWithHeader { let badge: ContactsPeerItemBadge? let enabled: Bool let selection: ContactsPeerItemSelection + let selectionPosition: ContactsPeerItemSelectionPosition let editing: ContactsPeerItemEditing let options: [ItemListPeerItemRevealOption] let additionalActions: [ContactsPeerItemAction] @@ -148,7 +154,7 @@ public class ContactsPeerItem: ItemListItem, ListViewItemWithHeader { public let header: ListViewItemHeader? - 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] = [], additionalActions: [ContactsPeerItemAction] = [], 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, arrowAction: (() -> Void)? = nil) { + 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, selectionPosition: ContactsPeerItemSelectionPosition = .right, editing: ContactsPeerItemEditing, options: [ItemListPeerItemRevealOption] = [], additionalActions: [ContactsPeerItemAction] = [], 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, arrowAction: (() -> Void)? = nil) { self.presentationData = presentationData self.style = style self.sectionId = sectionId @@ -161,6 +167,7 @@ public class ContactsPeerItem: ItemListItem, ListViewItemWithHeader { self.badge = badge self.enabled = enabled self.selection = selection + self.selectionPosition = selectionPosition self.editing = editing self.options = options self.additionalActions = additionalActions @@ -518,7 +525,7 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { if currentItem?.presentationData.theme !== item.presentationData.theme { updatedTheme = item.presentationData.theme } - let leftInset: CGFloat = 65.0 + params.leftInset + var leftInset: CGFloat = 65.0 + params.leftInset var rightInset: CGFloat = 10.0 + params.rightInset let updatedSelectionNode: CheckNode? @@ -527,7 +534,12 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { case .none: updatedSelectionNode = nil case let .selectable(selected): - rightInset += 38.0 + switch item.selectionPosition { + case .left: + leftInset += 38.0 + case .right: + rightInset += 38.0 + } isSelected = selected let selectionNode: CheckNode @@ -999,14 +1011,29 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { } if let updatedSelectionNode = updatedSelectionNode { + let hadSelectionNode = strongSelf.selectionNode != nil if strongSelf.selectionNode !== updatedSelectionNode { strongSelf.selectionNode?.removeFromSupernode() strongSelf.selectionNode = updatedSelectionNode strongSelf.addSubnode(updatedSelectionNode) } - updatedSelectionNode.setSelected(isSelected, animated: animated) + updatedSelectionNode.setSelected(isSelected, animated: true) - updatedSelectionNode.frame = CGRect(origin: CGPoint(x: params.width - params.rightInset - 22.0 - 17.0, y: floor((nodeLayout.contentSize.height - 22.0) / 2.0)), size: CGSize(width: 22.0, height: 22.0)) + switch item.selectionPosition { + case .left: + updatedSelectionNode.frame = CGRect(origin: CGPoint(x: params.leftInset + 17.0, y: floor((nodeLayout.contentSize.height - 22.0) / 2.0)), size: CGSize(width: 22.0, height: 22.0)) + case .right: + updatedSelectionNode.frame = CGRect(origin: CGPoint(x: params.width - params.rightInset - 22.0 - 17.0, y: floor((nodeLayout.contentSize.height - 22.0) / 2.0)), size: CGSize(width: 22.0, height: 22.0)) + } + + if !hadSelectionNode { + switch item.selectionPosition { + case .left: + transition.animateFrame(node: updatedSelectionNode, from: updatedSelectionNode.frame.offsetBy(dx: -38.0, dy: 0.0)) + case .right: + transition.animateFrame(node: updatedSelectionNode, from: updatedSelectionNode.frame.offsetBy(dx: 38.0, dy: 0.0)) + } + } } else if let selectionNode = strongSelf.selectionNode { selectionNode.removeFromSupernode() strongSelf.selectionNode = nil @@ -1155,9 +1182,9 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { self.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration * 0.5, removeOnCompletion: false) } - override public func header() -> ListViewItemHeader? { + override public func headers() -> [ListViewItemHeader]? { if let (item, _, _, _, _, _) = self.layoutParams { - return item.header + return item.header.flatMap { [$0] } } else { return nil } diff --git a/submodules/ContextUI/Sources/ContextActionNode.swift b/submodules/ContextUI/Sources/ContextActionNode.swift index 03bcaea17c..cafd458e1c 100644 --- a/submodules/ContextUI/Sources/ContextActionNode.swift +++ b/submodules/ContextUI/Sources/ContextActionNode.swift @@ -17,7 +17,7 @@ public protocol ContextActionNodeProtocol: ASDisplayNode { final class ContextActionNode: ASDisplayNode, ContextActionNodeProtocol { private let action: ContextMenuActionItem - private let getController: () -> ContextController? + private let getController: () -> ContextControllerProtocol? private let actionSelected: (ContextMenuActionResult) -> Void private let backgroundNode: ASDisplayNode @@ -33,7 +33,7 @@ final class ContextActionNode: ASDisplayNode, ContextActionNodeProtocol { private var pointerInteraction: PointerInteraction? - init(presentationData: PresentationData, action: ContextMenuActionItem, getController: @escaping () -> ContextController?, actionSelected: @escaping (ContextMenuActionResult) -> Void) { + init(presentationData: PresentationData, action: ContextMenuActionItem, getController: @escaping () -> ContextControllerProtocol?, actionSelected: @escaping (ContextMenuActionResult) -> Void) { self.action = action self.getController = getController self.actionSelected = actionSelected diff --git a/submodules/ContextUI/Sources/ContextActionsContainerNode.swift b/submodules/ContextUI/Sources/ContextActionsContainerNode.swift index 70837d63f4..2325b2bd92 100644 --- a/submodules/ContextUI/Sources/ContextActionsContainerNode.swift +++ b/submodules/ContextUI/Sources/ContextActionsContainerNode.swift @@ -69,7 +69,7 @@ private final class InnerActionsContainerNode: ASDisplayNode { } } - init(presentationData: PresentationData, items: [ContextMenuItem], getController: @escaping () -> ContextController?, actionSelected: @escaping (ContextMenuActionResult) -> Void, feedbackTap: @escaping () -> Void, blurBackground: Bool) { + init(presentationData: PresentationData, items: [ContextMenuItem], getController: @escaping () -> ContextControllerProtocol?, actionSelected: @escaping (ContextMenuActionResult) -> Void, feedbackTap: @escaping () -> Void, blurBackground: Bool) { self.presentationData = presentationData self.feedbackTap = feedbackTap self.blurBackground = blurBackground @@ -165,8 +165,11 @@ private final class InnerActionsContainerNode: ASDisplayNode { gesture.isEnabled = self.panSelectionGestureEnabled } - func updateLayout(widthClass: ContainerViewLayoutSizeClass, constrainedWidth: CGFloat, transition: ContainedViewLayoutTransition) -> CGSize { + func updateLayout(widthClass: ContainerViewLayoutSizeClass, constrainedWidth: CGFloat, minimalWidth: CGFloat?, transition: ContainedViewLayoutTransition) -> CGSize { var minActionsWidth: CGFloat = 250.0 + if let minimalWidth = minimalWidth, minimalWidth > minActionsWidth { + minActionsWidth = minimalWidth + } switch widthClass { case .compact: @@ -457,7 +460,7 @@ final class ContextActionsContainerNode: ASDisplayNode { return self.additionalActionsNode != nil } - init(presentationData: PresentationData, items: [ContextMenuItem], getController: @escaping () -> ContextController?, actionSelected: @escaping (ContextMenuActionResult) -> Void, feedbackTap: @escaping () -> Void, displayTextSelectionTip: Bool, blurBackground: Bool) { + init(presentationData: PresentationData, items: [ContextMenuItem], getController: @escaping () -> ContextControllerProtocol?, actionSelected: @escaping (ContextMenuActionResult) -> Void, feedbackTap: @escaping () -> Void, displayTextSelectionTip: Bool, blurBackground: Bool) { self.blurBackground = blurBackground self.shadowNode = ASImageNode() self.shadowNode.displaysAsynchronously = false @@ -517,10 +520,10 @@ final class ContextActionsContainerNode: ASDisplayNode { } var contentSize = CGSize() - let actionsSize = self.actionsNode.updateLayout(widthClass: widthClass, constrainedWidth: constrainedWidth, transition: transition) + let actionsSize = self.actionsNode.updateLayout(widthClass: widthClass, constrainedWidth: constrainedWidth, minimalWidth: nil, transition: transition) if let additionalActionsNode = self.additionalActionsNode, let additionalShadowNode = self.additionalShadowNode { - let additionalActionsSize = additionalActionsNode.updateLayout(widthClass: widthClass, constrainedWidth: actionsSize.width, transition: transition) + let additionalActionsSize = additionalActionsNode.updateLayout(widthClass: widthClass, constrainedWidth: actionsSize.width, minimalWidth: actionsSize.width, transition: transition) contentSize = additionalActionsSize let bounds = CGRect(origin: CGPoint(), size: additionalActionsSize) @@ -569,12 +572,15 @@ final class ContextActionsContainerNode: ASDisplayNode { } func animateOut(offset: CGFloat, transition: ContainedViewLayoutTransition) { - guard let additionalActionsNode = self.additionalActionsNode else { + guard let additionalActionsNode = self.additionalActionsNode, let additionalShadowNode = self.additionalShadowNode else { return } transition.animatePosition(node: additionalActionsNode, to: CGPoint(x: 0.0, y: offset / 2.0), additive: true) + transition.animatePosition(node: additionalShadowNode, to: CGPoint(x: 0.0, y: offset / 2.0), additive: true) additionalActionsNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false) + additionalShadowNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false) additionalActionsNode.layer.animateScale(from: 1.0, to: 0.75, duration: 0.15, removeOnCompletion: false) + additionalShadowNode.layer.animateScale(from: 1.0, to: 0.75, duration: 0.15, removeOnCompletion: false) } } diff --git a/submodules/ContextUI/Sources/ContextController.swift b/submodules/ContextUI/Sources/ContextController.swift index cf9037222a..95b1ebabd7 100644 --- a/submodules/ContextUI/Sources/ContextController.swift +++ b/submodules/ContextUI/Sources/ContextController.swift @@ -11,6 +11,13 @@ import SwiftSignalKit private let animationDurationFactor: Double = 1.0 +public protocol ContextControllerProtocol { + var useComplexItemsTransitionAnimation: Bool { get set } + + func setItems(_ items: Signal<[ContextMenuItem], NoError>) + func dismiss(completion: (() -> Void)?) +} + public enum ContextMenuActionItemTextLayout { case singleLine case twoLinesMax @@ -66,9 +73,9 @@ public final class ContextMenuActionItem { public let badge: ContextMenuActionBadge? public let icon: (PresentationTheme) -> UIImage? public let iconSource: ContextMenuActionItemIconSource? - public let action: (ContextController, @escaping (ContextMenuActionResult) -> Void) -> Void + public let action: (ContextControllerProtocol, @escaping (ContextMenuActionResult) -> Void) -> Void - public init(text: String, textColor: ContextMenuActionItemTextColor = .primary, textLayout: ContextMenuActionItemTextLayout = .twoLinesMax, textFont: ContextMenuActionItemFont = .regular, badge: ContextMenuActionBadge? = nil, icon: @escaping (PresentationTheme) -> UIImage?, iconSource: ContextMenuActionItemIconSource? = nil, action: @escaping (ContextController, @escaping (ContextMenuActionResult) -> Void) -> Void) { + public init(text: String, textColor: ContextMenuActionItemTextColor = .primary, textLayout: ContextMenuActionItemTextLayout = .twoLinesMax, textFont: ContextMenuActionItemFont = .regular, badge: ContextMenuActionBadge? = nil, icon: @escaping (PresentationTheme) -> UIImage?, iconSource: ContextMenuActionItemIconSource? = nil, action: @escaping (ContextControllerProtocol, @escaping (ContextMenuActionResult) -> Void) -> Void) { self.text = text self.textColor = textColor self.textFont = textFont @@ -86,7 +93,7 @@ public protocol ContextMenuCustomNode: ASDisplayNode { } public protocol ContextMenuCustomItem { - func node(presentationData: PresentationData, getController: @escaping () -> ContextController?, actionSelected: @escaping (ContextMenuActionResult) -> Void) -> ContextMenuCustomNode + func node(presentationData: PresentationData, getController: @escaping () -> ContextControllerProtocol?, actionSelected: @escaping (ContextMenuActionResult) -> Void) -> ContextMenuCustomNode } public enum ContextMenuItem { @@ -113,7 +120,7 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi private let reactionSelected: (ReactionContextItem.Reaction) -> Void private let beganAnimatingOut: () -> Void private let attemptTransitionControllerIntoNavigation: () -> Void - private let getController: () -> ContextController? + private let getController: () -> ContextControllerProtocol? private weak var gesture: ContextGesture? private var displayTextSelectionTip: Bool @@ -490,7 +497,8 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi if let takenViewInfo = takenViewInfo, let parentSupernode = takenViewInfo.contentContainingNode.supernode { self.contentContainerNode.contentNode = .extracted(node: takenViewInfo.contentContainingNode, keepInPlace: source.keepInPlace) - if source.keepInPlace { + if source.keepInPlace || takenViewInfo.maskView != nil { + self.clippingNode.view.mask = takenViewInfo.maskView self.clippingNode.addSubnode(self.contentContainerNode) } else { self.scrollNode.addSubnode(self.contentContainerNode) @@ -646,14 +654,26 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi } case let .extracted(extracted, keepInPlace): let springDuration: Double = 0.42 * animationDurationFactor - let springDamping: CGFloat = 104.0 + var springDamping: CGFloat = 104.0 + if case let .extracted(source) = self.source, source.centerVertically { + springDamping = 124.0 + } self.actionsContainerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2 * animationDurationFactor) self.actionsContainerNode.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: springDuration, initialVelocity: 0.0, damping: springDamping) - + if let originalProjectedContentViewFrame = self.originalProjectedContentViewFrame { let contentParentNode = extracted let localSourceFrame = self.view.convert(originalProjectedContentViewFrame.1, to: self.scrollNode.view) + + var actionsDuration = springDuration + var actionsOffset: CGFloat = 0.0 + var contentDuration = springDuration + if case let .extracted(source) = self.source, source.centerVertically { + actionsOffset = -(originalProjectedContentViewFrame.1.height - originalProjectedContentViewFrame.0.height) * 0.57 + actionsDuration *= 1.0 + contentDuration *= 0.9 + } let localContentSourceFrame: CGRect if keepInPlace { @@ -666,9 +686,11 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi reactionContextNode.animateIn(from: CGRect(origin: CGPoint(x: originalProjectedContentViewFrame.1.minX, y: originalProjectedContentViewFrame.1.minY), size: contentParentNode.contentRect.size)) } - self.actionsContainerNode.layer.animateSpring(from: NSValue(cgPoint: CGPoint(x: localSourceFrame.center.x - self.actionsContainerNode.position.x, y: localSourceFrame.center.y - self.actionsContainerNode.position.y)), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: springDuration, initialVelocity: 0.0, damping: springDamping, additive: true) + self.actionsContainerNode.layer.animateSpring(from: NSValue(cgPoint: CGPoint(x: localSourceFrame.center.x - self.actionsContainerNode.position.x, y: localSourceFrame.center.y - self.actionsContainerNode.position.y + actionsOffset)), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: actionsDuration, initialVelocity: 0.0, damping: springDamping, additive: true) let contentContainerOffset = CGPoint(x: localContentSourceFrame.center.x - self.contentContainerNode.frame.center.x - contentParentNode.contentRect.minX, y: localContentSourceFrame.center.y - self.contentContainerNode.frame.center.y - contentParentNode.contentRect.minY) - self.contentContainerNode.layer.animateSpring(from: NSValue(cgPoint: contentContainerOffset), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: springDuration, initialVelocity: 0.0, damping: springDamping, additive: true) + self.contentContainerNode.layer.animateSpring(from: NSValue(cgPoint: contentContainerOffset), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: contentDuration, initialVelocity: 0.0, damping: springDamping, additive: true, completion: { [weak self] _ in + self?.clippingNode.view.mask = nil + }) contentParentNode.applyAbsoluteOffsetSpring?(-contentContainerOffset.y, springDuration, springDamping) } @@ -718,6 +740,8 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi } func animateOut(result initialResult: ContextMenuActionResult, completion: @escaping () -> Void) { + self.isUserInteractionEnabled = false + self.beganAnimatingOut() var transitionDuration: Double = 0.2 @@ -828,11 +852,12 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi updatedContentAreaInScreenSpace.origin.x = 0.0 updatedContentAreaInScreenSpace.size.width = self.bounds.width + self.clippingNode.view.mask = putBackInfo.maskView self.clippingNode.layer.animateFrame(from: self.clippingNode.frame, to: updatedContentAreaInScreenSpace, duration: transitionDuration * animationDurationFactor, timingFunction: transitionCurve.timingFunction, removeOnCompletion: false) self.clippingNode.layer.animateBoundsOriginYAdditive(from: 0.0, to: updatedContentAreaInScreenSpace.minY, duration: transitionDuration * animationDurationFactor, timingFunction: transitionCurve.timingFunction, removeOnCompletion: false) } - let intermediateCompletion: () -> Void = { [weak contentParentNode] in + let intermediateCompletion: () -> Void = { [weak self, weak contentParentNode] in if completedEffect && completedContentNode && completedActionsNode { switch result { case .default, .custom: @@ -845,6 +870,8 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi break } + self?.clippingNode.view.mask = nil + completion() } } @@ -911,14 +938,19 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi localContentSourceFrame = localSourceFrame } - self.actionsContainerNode.layer.animatePosition(from: CGPoint(), to: CGPoint(x: localSourceFrame.center.x - self.actionsContainerNode.position.x, y: localSourceFrame.center.y - self.actionsContainerNode.position.y), duration: transitionDuration * animationDurationFactor, timingFunction: transitionCurve.timingFunction, removeOnCompletion: false, additive: true) + var actionsOffset: CGFloat = 0.0 + if case let .extracted(source) = self.source, source.centerVertically { + actionsOffset = -localSourceFrame.width * 0.6 + } + + self.actionsContainerNode.layer.animatePosition(from: CGPoint(), to: CGPoint(x: localSourceFrame.center.x - self.actionsContainerNode.position.x, y: localSourceFrame.center.y - self.actionsContainerNode.position.y + actionsOffset), duration: transitionDuration * animationDurationFactor, timingFunction: transitionCurve.timingFunction, removeOnCompletion: false, additive: true) let contentContainerOffset = CGPoint(x: localContentSourceFrame.center.x - self.contentContainerNode.frame.center.x - contentParentNode.contentRect.minX, y: localContentSourceFrame.center.y - self.contentContainerNode.frame.center.y - contentParentNode.contentRect.minY) self.contentContainerNode.layer.animatePosition(from: CGPoint(), to: contentContainerOffset, duration: transitionDuration * animationDurationFactor, timingFunction: transitionCurve.timingFunction, removeOnCompletion: false, additive: true, completion: { _ in completedContentNode = true intermediateCompletion() }) contentParentNode.updateAbsoluteRect?(self.contentContainerNode.frame.offsetBy(dx: 0.0, dy: -self.scrollNode.view.contentOffset.y + contentContainerOffset.y), self.bounds.size) - contentParentNode.applyAbsoluteOffset?(-contentContainerOffset.y, transitionCurve, transitionDuration) + contentParentNode.applyAbsoluteOffset?(CGPoint(x: 0.0, y: -contentContainerOffset.y), transitionCurve, transitionDuration) if let reactionContextNode = self.reactionContextNode { reactionContextNode.animateOut(to: CGRect(origin: CGPoint(x: originalProjectedContentViewFrame.1.minX, y: originalProjectedContentViewFrame.1.minY), size: contentParentNode.contentRect.size), animatingOutToReaction: self.reactionContextNodeIsAnimatingOut) @@ -1157,7 +1189,6 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi if let layout = self.validLayout { self.updateLayout(layout: layout, transition: .animated(duration: 0.3, curve: .spring), previousActionsContainerNode: previousActionsContainerNode) - } else { previousActionsContainerNode.removeFromSupernode() } @@ -1271,33 +1302,29 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi var overflowOffset: CGFloat var contentContainerFrame: CGRect -// if keepInPlace { - overflowOffset = min(0.0, originalActionsFrame.minY - contentTopInset) + + overflowOffset = min(0.0, originalActionsFrame.minY - contentTopInset) let contentParentNode = referenceNode - contentContainerFrame = originalContentFrame - if !overflowOffset.isZero { - let offsetDelta = contentParentNode.frame.height + 4.0 - overflowOffset += offsetDelta - overflowOffset = min(0.0, overflowOffset) - - originalActionsFrame.origin.x -= contentParentNode.frame.width + 14.0 - originalActionsFrame.origin.x = max(actionsSideInset, originalActionsFrame.origin.x) - //originalActionsFrame.origin.y += contentParentNode.contentRect.height - if originalActionsFrame.minX < contentContainerFrame.minX { - contentContainerFrame.origin.x = min(originalActionsFrame.maxX + 14.0, layout.size.width - actionsSideInset) - } - originalActionsFrame.origin.y += offsetDelta - if originalActionsFrame.maxY < originalContentFrame.maxY { - originalActionsFrame.origin.y += contentParentNode.frame.height - originalActionsFrame.origin.y = min(originalActionsFrame.origin.y, layout.size.height - originalActionsFrame.height - actionsBottomInset) - } - contentHeight -= offsetDelta + contentContainerFrame = originalContentFrame + if !overflowOffset.isZero { + let offsetDelta = contentParentNode.frame.height + 4.0 + overflowOffset += offsetDelta + overflowOffset = min(0.0, overflowOffset) + + originalActionsFrame.origin.x -= contentParentNode.frame.width + 14.0 + originalActionsFrame.origin.x = max(actionsSideInset, originalActionsFrame.origin.x) + + if originalActionsFrame.minX < contentContainerFrame.minX { + contentContainerFrame.origin.x = min(originalActionsFrame.maxX + 14.0, layout.size.width - actionsSideInset) } -// } else { -// overflowOffset = min(0.0, originalContentFrame.minY - contentTopInset) -// contentContainerFrame = originalContentFrame.offsetBy(dx: -contentParentNode.contentRect.minX, dy: -overflowOffset - contentParentNode.contentRect.minY) -// } - + originalActionsFrame.origin.y += offsetDelta + if originalActionsFrame.maxY < originalContentFrame.maxY { + originalActionsFrame.origin.y += contentParentNode.frame.height + originalActionsFrame.origin.y = min(originalActionsFrame.origin.y, layout.size.height - originalActionsFrame.height - actionsBottomInset) + } + contentHeight -= offsetDelta + } + let scrollContentSize = CGSize(width: layout.size.width, height: contentHeight) if self.scrollNode.view.contentSize != scrollContentSize { self.scrollNode.view.contentSize = scrollContentSize @@ -1315,6 +1342,10 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi } } case let .extracted(contentParentNode, keepInPlace): + var centerVertically = false + if case let .extracted(source) = self.source, source.centerVertically { + centerVertically = true + } let contentActionsSpacing: CGFloat = keepInPlace ? 16.0 : 8.0 if let originalProjectedContentViewFrame = self.originalProjectedContentViewFrame { let isInitialLayout = self.actionsContainerNode.frame.size.width.isZero @@ -1328,13 +1359,17 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi let maximumActionsFrameOrigin = max(60.0, layout.size.height - layout.intrinsicInsets.bottom - actionsBottomInset - actionsSize.height) let preferredActionsX: CGFloat let originalActionsY: CGFloat - if keepInPlace { + if centerVertically { + originalActionsY = min(originalProjectedContentViewFrame.1.maxY + contentActionsSpacing, maximumActionsFrameOrigin) + preferredActionsX = originalProjectedContentViewFrame.1.maxX - actionsSize.width + } else if keepInPlace { originalActionsY = originalProjectedContentViewFrame.1.minY - contentActionsSpacing - actionsSize.height preferredActionsX = max(actionsSideInset, originalProjectedContentViewFrame.1.maxX - actionsSize.width) } else { originalActionsY = min(originalProjectedContentViewFrame.1.maxY + contentActionsSpacing, maximumActionsFrameOrigin) preferredActionsX = originalProjectedContentViewFrame.1.minX } + var originalActionsFrame = CGRect(origin: CGPoint(x: max(actionsSideInset, min(layout.size.width - actionsSize.width - actionsSideInset, preferredActionsX)), y: originalActionsY), size: actionsSize) let originalContentX: CGFloat = originalProjectedContentViewFrame.1.minX let originalContentY: CGFloat @@ -1362,7 +1397,20 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi var overflowOffset: CGFloat var contentContainerFrame: CGRect - if keepInPlace { + if centerVertically { + overflowOffset = 0.0 + if layout.size.width > layout.size.height, case .compact = layout.metrics.widthClass { + let totalWidth = originalContentFrame.width + originalActionsFrame.width + contentActionsSpacing + contentContainerFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - totalWidth) / 2.0 + originalContentFrame.width * 0.1), y: floor((layout.size.height - originalContentFrame.height) / 2.0)), size: originalContentFrame.size) + originalActionsFrame.origin.x = contentContainerFrame.maxX + contentActionsSpacing + 14.0 + originalActionsFrame.origin.y = contentContainerFrame.origin.y + contentHeight = layout.size.height + } else { + let totalHeight = originalContentFrame.height + originalActionsFrame.height + contentContainerFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - originalContentFrame.width) / 2.0), y: floor((layout.size.height - totalHeight) / 2.0)), size: originalContentFrame.size) + originalActionsFrame.origin.y = contentContainerFrame.maxY + contentActionsSpacing + } + } else if keepInPlace { overflowOffset = min(0.0, originalActionsFrame.minY - contentTopInset) contentContainerFrame = originalContentFrame.offsetBy(dx: -contentParentNode.contentRect.minX, dy: -contentParentNode.contentRect.minY) if !overflowOffset.isZero { @@ -1386,6 +1434,10 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi } else { overflowOffset = min(0.0, originalContentFrame.minY - contentTopInset) contentContainerFrame = originalContentFrame.offsetBy(dx: -contentParentNode.contentRect.minX, dy: -overflowOffset - contentParentNode.contentRect.minY) + + if contentContainerFrame.maxX > layout.size.width { + contentContainerFrame = CGRect(origin: CGPoint(x: layout.size.width - contentContainerFrame.width - 11.0, y: contentContainerFrame.minY), size: contentContainerFrame.size) + } } let scrollContentSize = CGSize(width: layout.size.width, height: contentHeight) @@ -1557,11 +1609,10 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi } } } - - + if let previousActionsContainerNode = previousActionsContainerNode { if transition.isAnimated { - if previousActionsContainerNode.hasAdditionalActions && !self.actionsContainerNode.hasAdditionalActions { + if previousActionsContainerNode.hasAdditionalActions && !self.actionsContainerNode.hasAdditionalActions && self.getController()?.useComplexItemsTransitionAnimation == true { var initialFrame = self.actionsContainerNode.frame let delta = (previousActionsContainerNode.frame.height - self.actionsContainerNode.frame.height) initialFrame.origin.y = self.actionsContainerNode.frame.minY + previousActionsContainerNode.frame.height - self.actionsContainerNode.frame.height @@ -1625,7 +1676,9 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi if case let .extracted(source) = self.source { if !source.ignoreContentTouches { let contentPoint = self.view.convert(point, to: contentParentNode.contentNode.view) - if let result = contentParentNode.contentNode.hitTest(contentPoint, with: event) { + if let result = contentParentNode.contentNode.customHitTest?(contentPoint) { + return result + } else if let result = contentParentNode.contentNode.hitTest(contentPoint, with: event) { if result is TextSelectionNodeView { return result } else if contentParentNode.contentRect.contains(contentPoint) { @@ -1679,22 +1732,27 @@ public protocol ContextReferenceContentSource: class { public final class ContextControllerTakeViewInfo { public let contentContainingNode: ContextExtractedContentContainingNode public let contentAreaInScreenSpace: CGRect + public let maskView: UIView? - public init(contentContainingNode: ContextExtractedContentContainingNode, contentAreaInScreenSpace: CGRect) { + public init(contentContainingNode: ContextExtractedContentContainingNode, contentAreaInScreenSpace: CGRect, maskView: UIView? = nil) { self.contentContainingNode = contentContainingNode self.contentAreaInScreenSpace = contentAreaInScreenSpace + self.maskView = maskView } } public final class ContextControllerPutBackViewInfo { public let contentAreaInScreenSpace: CGRect + public let maskView: UIView? - public init(contentAreaInScreenSpace: CGRect) { + public init(contentAreaInScreenSpace: CGRect, maskView: UIView? = nil) { self.contentAreaInScreenSpace = contentAreaInScreenSpace + self.maskView = maskView } } public protocol ContextExtractedContentSource: class { + var centerVertically: Bool { get } var keepInPlace: Bool { get } var ignoreContentTouches: Bool { get } var blurBackground: Bool { get } @@ -1705,6 +1763,10 @@ public protocol ContextExtractedContentSource: class { } public extension ContextExtractedContentSource { + var centerVertically: Bool { + return false + } + var shouldBeDismissed: Signal { return .single(false) } @@ -1736,7 +1798,7 @@ public enum ContextContentSource { case controller(ContextControllerContentSource) } -public final class ContextController: ViewController, StandalonePresentableController { +public final class ContextController: ViewController, StandalonePresentableController, ContextControllerProtocol { private let account: Account private var presentationData: PresentationData private let source: ContextContentSource @@ -1762,6 +1824,8 @@ public final class ContextController: ViewController, StandalonePresentableContr public var reactionSelected: ((ReactionContextItem.Reaction) -> Void)? public var dismissed: (() -> Void)? + public var useComplexItemsTransitionAnimation = false + private var shouldBeDismissedDisposable: Disposable? public init(account: Account, presentationData: PresentationData, source: ContextContentSource, items: Signal<[ContextMenuItem], NoError>, reactionItems: [ReactionContextItem], recognizer: TapLongTapOrDoubleTapGestureRecognizer? = nil, gesture: ContextGesture? = nil, displayTextSelectionTip: Bool = false) { diff --git a/submodules/Display/Source/PeekController.swift b/submodules/ContextUI/Sources/PeekController.swift similarity index 68% rename from submodules/Display/Source/PeekController.swift rename to submodules/ContextUI/Sources/PeekController.swift index 69af3b1505..43d8c9f45f 100644 --- a/submodules/Display/Source/PeekController.swift +++ b/submodules/ContextUI/Sources/PeekController.swift @@ -1,6 +1,9 @@ import Foundation import UIKit import AsyncDisplayKit +import Display +import SwiftSignalKit +import TelegramPresentationData public final class PeekControllerTheme { public let isDark: Bool @@ -20,12 +23,29 @@ public final class PeekControllerTheme { } } -public final class PeekController: ViewController { +extension PeekControllerTheme { + convenience public init(presentationTheme: PresentationTheme) { + let actionSheet = presentationTheme.actionSheet + self.init(isDark: actionSheet.backgroundType == .dark, menuBackgroundColor: actionSheet.opaqueItemBackgroundColor, menuItemHighligtedColor: actionSheet.opaqueItemHighlightedBackgroundColor, menuItemSeparatorColor: actionSheet.opaqueItemSeparatorColor, accentColor: actionSheet.controlAccentColor, destructiveColor: actionSheet.destructiveActionTextColor) + } +} + +public final class PeekController: ViewController, ContextControllerProtocol { + public var useComplexItemsTransitionAnimation: Bool = false + + public func setItems(_ items: Signal<[ContextMenuItem], NoError>) { + + } + private var controllerNode: PeekControllerNode { return self.displayNode as! PeekControllerNode } - private let theme: PeekControllerTheme + public var contentNode: PeekControllerContentNode & ASDisplayNode { + return self.controllerNode.contentNode + } + + private let presentationData: PresentationData private let content: PeekControllerContent var sourceNode: () -> ASDisplayNode? @@ -33,8 +53,8 @@ public final class PeekController: ViewController { private var animatedIn = false - public init(theme: PeekControllerTheme, content: PeekControllerContent, sourceNode: @escaping () -> ASDisplayNode?) { - self.theme = theme + public init(presentationData: PresentationData, content: PeekControllerContent, sourceNode: @escaping () -> ASDisplayNode?) { + self.presentationData = presentationData self.content = content self.sourceNode = sourceNode @@ -48,7 +68,7 @@ public final class PeekController: ViewController { } override public func loadDisplayNode() { - self.displayNode = PeekControllerNode(theme: self.theme, content: self.content, requestDismiss: { [weak self] in + self.displayNode = PeekControllerNode(presentationData: self.presentationData, controller: self, content: self.content, requestDismiss: { [weak self] in self?.dismiss() }) self.displayNodeDidLoad() diff --git a/submodules/Display/Source/PeekControllerContent.swift b/submodules/ContextUI/Sources/PeekControllerContent.swift similarity index 78% rename from submodules/Display/Source/PeekControllerContent.swift rename to submodules/ContextUI/Sources/PeekControllerContent.swift index c2767e87c6..e1be451d53 100644 --- a/submodules/Display/Source/PeekControllerContent.swift +++ b/submodules/ContextUI/Sources/PeekControllerContent.swift @@ -1,21 +1,22 @@ import Foundation import UIKit import AsyncDisplayKit +import Display public enum PeekControllerContentPresentation { case contained case freeform } -public enum PeerkControllerMenuActivation { +public enum PeerControllerMenuActivation { case drag case press } public protocol PeekControllerContent { func presentation() -> PeekControllerContentPresentation - func menuActivation() -> PeerkControllerMenuActivation - func menuItems() -> [PeekControllerMenuItem] + func menuActivation() -> PeerControllerMenuActivation + func menuItems() -> [ContextMenuItem] func node() -> PeekControllerContentNode & ASDisplayNode func topAccessoryNode() -> ASDisplayNode? diff --git a/submodules/Display/Source/PeekControllerGestureRecognizer.swift b/submodules/ContextUI/Sources/PeekControllerGestureRecognizer.swift similarity index 93% rename from submodules/Display/Source/PeekControllerGestureRecognizer.swift rename to submodules/ContextUI/Sources/PeekControllerGestureRecognizer.swift index abaa6c0201..ab4a8a323f 100644 --- a/submodules/Display/Source/PeekControllerGestureRecognizer.swift +++ b/submodules/ContextUI/Sources/PeekControllerGestureRecognizer.swift @@ -2,6 +2,7 @@ import Foundation import UIKit import SwiftSignalKit import AsyncDisplayKit +import Display private func traceDeceleratingScrollView(_ view: UIView, at point: CGPoint) -> Bool { if view.bounds.contains(point), let view = view as? UIScrollView, view.isDecelerating { @@ -33,7 +34,7 @@ public final class PeekControllerGestureRecognizer: UIPanGestureRecognizer { } } - private var menuActivation: PeerkControllerMenuActivation? + private var menuActivation: PeerControllerMenuActivation? private weak var presentedController: PeekController? public init(contentAtPoint: @escaping (CGPoint) -> Signal<(ASDisplayNode, PeekControllerContent)?, NoError>?, present: @escaping (PeekControllerContent, ASDisplayNode) -> ViewController?, updateContent: @escaping (PeekControllerContent?) -> Void = { _ in }, activateBySingleTap: Bool = false) { @@ -105,8 +106,8 @@ public final class PeekControllerGestureRecognizer: UIPanGestureRecognizer { (presentedController.displayNode as? PeekControllerNode)?.activateMenu() } self.menuActivation = nil - self.presentedController = nil - self.state = .ended +// self.presentedController = nil +// self.state = .ended } } } @@ -136,10 +137,8 @@ public final class PeekControllerGestureRecognizer: UIPanGestureRecognizer { } self.state = .ended } else { - let velocity = self.velocity(in: self.view) - - if let presentedController = self.presentedController, presentedController.isNodeLoaded { - (presentedController.displayNode as? PeekControllerNode)?.endDraggingWithVelocity(velocity.y) + if let presentedController = self.presentedController, presentedController.isNodeLoaded, let location = touches.first?.location(in: presentedController.view) { + (presentedController.displayNode as? PeekControllerNode)?.endDragging(location) self.presentedController = nil self.menuActivation = nil } @@ -172,7 +171,12 @@ public final class PeekControllerGestureRecognizer: UIPanGestureRecognizer { if let touch = touches.first, let initialTapLocation = self.tapLocation { let touchLocation = touch.location(in: self.view) - if let menuActivation = self.menuActivation, let presentedController = self.presentedController { + if let presentedController = self.presentedController, self.menuActivation == nil { + if presentedController.isNodeLoaded { + let touchLocation = touch.location(in: presentedController.view) + (presentedController.displayNode as? PeekControllerNode)?.applyDraggingOffset(touchLocation) + } + } else if let menuActivation = self.menuActivation, let presentedController = self.presentedController { switch menuActivation { case .drag: var offset = touchLocation.y - initialTapLocation.y @@ -181,7 +185,7 @@ public final class PeekControllerGestureRecognizer: UIPanGestureRecognizer { offset = (-((1.0 - (1.0 / (((delta) * 0.55 / (factor)) + 1.0))) * factor)) * (offset < 0.0 ? 1.0 : -1.0) if presentedController.isNodeLoaded { - (presentedController.displayNode as? PeekControllerNode)?.applyDraggingOffset(offset) +// (presentedController.displayNode as? PeekControllerNode)?.applyDraggingOffset(offset) } case .press: if #available(iOSApplicationExtension 9.0, iOS 9.0, *) { diff --git a/submodules/ContextUI/Sources/PeekControllerNode.swift b/submodules/ContextUI/Sources/PeekControllerNode.swift new file mode 100644 index 0000000000..6ad81cb574 --- /dev/null +++ b/submodules/ContextUI/Sources/PeekControllerNode.swift @@ -0,0 +1,351 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display +import TelegramPresentationData + +private let animationDurationFactor: Double = 1.0 + +final class PeekControllerNode: ViewControllerTracingNode { + private let requestDismiss: () -> Void + + private let presentationData: PresentationData + private let theme: PeekControllerTheme + + private weak var controller: PeekController? + + private let blurView: UIView + private let dimNode: ASDisplayNode + private let containerBackgroundNode: ASImageNode + private let containerNode: ASDisplayNode + private let darkDimNode: ASDisplayNode + + private var validLayout: ContainerViewLayout? + + private var content: PeekControllerContent + var contentNode: PeekControllerContentNode & ASDisplayNode + private var contentNodeHasValidLayout = false + + private var topAccessoryNode: ASDisplayNode? + + private var actionsContainerNode: ContextActionsContainerNode + + private var hapticFeedback = HapticFeedback() + + private var initialContinueGesturePoint: CGPoint? + private var didMoveFromInitialGesturePoint = false + private var highlightedActionNode: ContextActionNodeProtocol? + + init(presentationData: PresentationData, controller: PeekController, content: PeekControllerContent, requestDismiss: @escaping () -> Void) { + self.presentationData = presentationData + self.requestDismiss = requestDismiss + self.theme = PeekControllerTheme(presentationTheme: presentationData.theme) + self.controller = controller + + self.dimNode = ASDisplayNode() + self.blurView = UIVisualEffectView(effect: UIBlurEffect(style: self.theme.isDark ? .dark : .light)) + self.blurView.isUserInteractionEnabled = false + + self.darkDimNode = ASDisplayNode() + self.darkDimNode.alpha = 0.0 + self.darkDimNode.backgroundColor = presentationData.theme.contextMenu.dimColor + self.darkDimNode.isUserInteractionEnabled = false + + switch content.menuActivation() { + case .drag: + self.dimNode.backgroundColor = nil + self.blurView.alpha = 1.0 + case .press: + self.dimNode.backgroundColor = UIColor(white: self.theme.isDark ? 0.0 : 1.0, alpha: 0.5) + self.blurView.alpha = 0.0 + } + + self.containerBackgroundNode = ASImageNode() + self.containerBackgroundNode.isLayerBacked = true + self.containerBackgroundNode.displaysAsynchronously = false + + self.containerNode = ASDisplayNode() + + self.content = content + self.contentNode = content.node() + self.topAccessoryNode = content.topAccessoryNode() + + var feedbackTapImpl: (() -> Void)? + var activatedActionImpl: (() -> Void)? + self.actionsContainerNode = ContextActionsContainerNode(presentationData: presentationData, items: content.menuItems(), getController: { [weak controller] in + return controller + }, actionSelected: { result in + activatedActionImpl?() + }, feedbackTap: { + feedbackTapImpl?() + }, displayTextSelectionTip: false, blurBackground: true) + self.actionsContainerNode.alpha = 0.0 + + super.init() + + feedbackTapImpl = { [weak self] in + self?.hapticFeedback.tap() + } + + if content.presentation() == .freeform { + self.containerNode.isUserInteractionEnabled = false + } else { + self.containerNode.clipsToBounds = true + self.containerNode.cornerRadius = 16.0 + } + + self.addSubnode(self.dimNode) + self.view.addSubview(self.blurView) + self.addSubnode(self.darkDimNode) + self.containerNode.addSubnode(self.contentNode) + + self.addSubnode(self.actionsContainerNode) + self.addSubnode(self.containerNode) + + activatedActionImpl = { [weak self] in + self?.requestDismiss() + } + + self.hapticFeedback.prepareTap() + } + + deinit { + } + + override func didLoad() { + super.didLoad() + + self.dimNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimNodeTap(_:)))) + self.view.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:)))) + } + + func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + self.validLayout = layout + + transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(), size: layout.size)) + transition.updateFrame(node: self.darkDimNode, frame: CGRect(origin: CGPoint(), size: layout.size)) + transition.updateFrame(view: self.blurView, frame: CGRect(origin: CGPoint(), size: layout.size)) + + var layoutInsets = layout.insets(options: []) + let containerWidth = horizontalContainerFillingSizeForLayout(layout: layout, sideInset: layout.safeInsets.left) + + layoutInsets.left = floor((layout.size.width - containerWidth) / 2.0) + layoutInsets.right = layoutInsets.left + if !layoutInsets.bottom.isZero { + layoutInsets.bottom -= 12.0 + } + + let maxContainerSize = CGSize(width: layout.size.width - 14.0 * 2.0, height: layout.size.height - layoutInsets.top - layoutInsets.bottom - 90.0) + + let contentSize = self.contentNode.updateLayout(size: maxContainerSize, transition: self.contentNodeHasValidLayout ? transition : .immediate) + if self.contentNodeHasValidLayout { + transition.updateFrame(node: self.contentNode, frame: CGRect(origin: CGPoint(), size: contentSize)) + } else { + self.contentNode.frame = CGRect(origin: CGPoint(), size: contentSize) + } + + let actionsSideInset: CGFloat = layout.safeInsets.left + 11.0 + let actionsSize = self.actionsContainerNode.updateLayout(widthClass: layout.metrics.widthClass, constrainedWidth: layout.size.width - actionsSideInset * 2.0, transition: .immediate) + + let containerFrame: CGRect + let actionsFrame: CGRect + if layout.size.width > layout.size.height { + if self.actionsContainerNode.alpha.isZero { + containerFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - contentSize.width) / 2.0), y: floor((layout.size.height - contentSize.height) / 2.0)), size: contentSize) + } else { + containerFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - contentSize.width) / 3.0), y: floor((layout.size.height - contentSize.height) / 2.0)), size: contentSize) + } + actionsFrame = CGRect(origin: CGPoint(x: containerFrame.maxX + 32.0, y: floor((layout.size.height - actionsSize.height) / 2.0)), size: actionsSize) + } else { + switch self.content.presentation() { + case .contained: + containerFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - contentSize.width) / 2.0), y: floor((layout.size.height - contentSize.height) / 2.0)), size: contentSize) + case .freeform: + containerFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - contentSize.width) / 2.0), y: floor((layout.size.height - contentSize.height) / 3.0)), size: contentSize) + } + actionsFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - actionsSize.width) / 2.0), y: containerFrame.maxY + 32.0), size: actionsSize) + } + transition.updateFrame(node: self.containerNode, frame: containerFrame) + + self.actionsContainerNode.updateSize(containerSize: actionsSize, contentSize: actionsSize) + transition.updateFrame(node: self.actionsContainerNode, frame: actionsFrame) + + self.contentNodeHasValidLayout = true + } + + func animateIn(from rect: CGRect) { + self.dimNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + self.blurView.layer.animateAlpha(from: 0.0, to: self.blurView.alpha, duration: 0.3) + + let offset = CGPoint(x: rect.midX - self.containerNode.position.x, y: rect.midY - self.containerNode.position.y) + self.containerNode.layer.animateSpring(from: NSValue(cgPoint: offset), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: 0.4, initialVelocity: 0.0, damping: 110.0, additive: true) + self.containerNode.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.4, initialVelocity: 0.0, damping: 110.0) + self.containerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) + + if let topAccessoryNode = self.topAccessoryNode { + topAccessoryNode.layer.animateSpring(from: NSValue(cgPoint: offset), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: 0.4, initialVelocity: 0.0, damping: 110.0, additive: true) + topAccessoryNode.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.4, initialVelocity: 0.0, damping: 110.0) + topAccessoryNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) + } + + if case .press = self.content.menuActivation() { + self.hapticFeedback.tap() + } else { + self.hapticFeedback.impact() + } + } + + func animateOut(to rect: CGRect, completion: @escaping () -> Void) { + self.dimNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) + self.blurView.layer.animateAlpha(from: self.blurView.alpha, to: 0.0, duration: 0.25, removeOnCompletion: false) + self.darkDimNode.layer.animateAlpha(from: self.darkDimNode.alpha, to: 0.0, duration: 0.2, removeOnCompletion: false) + + let springDuration: Double = 0.42 * animationDurationFactor + let springDamping: CGFloat = 104.0 + + let offset = CGPoint(x: rect.midX - self.containerNode.position.x, y: rect.midY - self.containerNode.position.y) + self.containerNode.layer.animateSpring(from: NSValue(cgPoint: CGPoint()), to: NSValue(cgPoint: offset), keyPath: "position", duration: springDuration, initialVelocity: 0.0, damping: springDamping, additive: true, completion: { _ in + completion() + }) + self.containerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) + self.containerNode.layer.animateScale(from: 1.0, to: 0.1, duration: 0.25, removeOnCompletion: false) + + if !self.actionsContainerNode.alpha.isZero { + let actionsOffset = CGPoint(x: rect.midX - self.actionsContainerNode.position.x, y: rect.midY - self.actionsContainerNode.position.y) + self.actionsContainerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2 * animationDurationFactor, removeOnCompletion: false) + self.actionsContainerNode.layer.animateSpring(from: 1.0 as NSNumber, to: 0.0 as NSNumber, keyPath: "transform.scale", duration: springDuration, initialVelocity: 0.0, damping: springDamping, removeOnCompletion: false) + self.actionsContainerNode.layer.animateSpring(from: NSValue(cgPoint: CGPoint()), to: NSValue(cgPoint: actionsOffset), keyPath: "position", duration: springDuration, initialVelocity: 0.0, damping: springDamping, additive: true) + } + } + + @objc func dimNodeTap(_ recognizer: UITapGestureRecognizer) { + if case .ended = recognizer.state { + self.requestDismiss() + } + } + + @objc func panGesture(_ recognizer: UIPanGestureRecognizer) { + guard case .drag = self.content.menuActivation() else { + return + } + + let location = recognizer.location(in: self.view) + switch recognizer.state { + case .began: + break + case .changed: + self.applyDraggingOffset(location) + case .cancelled, .ended: + self.endDragging(location) + default: + break + } + } + + func applyDraggingOffset(_ offset: CGPoint) { + let localPoint = offset + let initialPoint: CGPoint + if let current = self.initialContinueGesturePoint { + initialPoint = current + } else { + initialPoint = localPoint + self.initialContinueGesturePoint = localPoint + } + if !self.actionsContainerNode.alpha.isZero { + if !self.didMoveFromInitialGesturePoint { + let distance = abs(localPoint.y - initialPoint.y) + if distance > 12.0 { + self.didMoveFromInitialGesturePoint = true + } + } + if self.didMoveFromInitialGesturePoint { + let actionPoint = self.view.convert(localPoint, to: self.actionsContainerNode.view) + let actionNode = self.actionsContainerNode.actionNode(at: actionPoint) + if self.highlightedActionNode !== actionNode { + self.highlightedActionNode?.setIsHighlighted(false) + self.highlightedActionNode = actionNode + if let actionNode = actionNode { + actionNode.setIsHighlighted(true) + self.hapticFeedback.tap() + } + } + } + } + } + + func activateMenu() { + if self.content.menuItems().isEmpty { + return + } + if case .press = self.content.menuActivation() { + self.hapticFeedback.impact() + } + + let springDuration: Double = 0.42 * animationDurationFactor + let springDamping: CGFloat = 104.0 + + let previousBlurAlpha = self.blurView.alpha + self.blurView.alpha = 1.0 + self.blurView.layer.animateAlpha(from: previousBlurAlpha, to: self.blurView.alpha, duration: 0.3) + + let previousDarkDimAlpha = self.darkDimNode.alpha + self.darkDimNode.alpha = 1.0 + self.darkDimNode.layer.animateAlpha(from: previousDarkDimAlpha, to: 1.0, duration: 0.3) + + self.actionsContainerNode.alpha = 1.0 + self.actionsContainerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2 * animationDurationFactor) + self.actionsContainerNode.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: springDuration, initialVelocity: 0.0, damping: springDamping) + + let localContentSourceFrame = self.containerNode.frame + self.actionsContainerNode.layer.animateSpring(from: NSValue(cgPoint: CGPoint(x: localContentSourceFrame.center.x - self.actionsContainerNode.position.x, y: localContentSourceFrame.center.y - self.actionsContainerNode.position.y)), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: springDuration, initialVelocity: 0.0, damping: springDamping, additive: true) + + if let layout = self.validLayout { + self.containerLayoutUpdated(layout, transition: .animated(duration: springDuration, curve: .spring)) + } + } + + func endDragging(_ location: CGPoint) { + if self.didMoveFromInitialGesturePoint { + if let highlightedActionNode = self.highlightedActionNode { + self.highlightedActionNode = nil + highlightedActionNode.performAction() + } + } else if self.actionsContainerNode.alpha.isZero { + self.requestDismiss() + } + } + + func updateContent(content: PeekControllerContent) { + let contentNode = self.contentNode + contentNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false, completion: { [weak contentNode] _ in + contentNode?.removeFromSupernode() + }) + contentNode.layer.animateScale(from: 1.0, to: 0.2, duration: 0.15, removeOnCompletion: false) + + self.content = content + self.contentNode = content.node() + self.containerNode.addSubnode(self.contentNode) + self.contentNodeHasValidLayout = false + + let previousActionsContainerNode = self.actionsContainerNode + self.actionsContainerNode = ContextActionsContainerNode(presentationData: self.presentationData, items: content.menuItems(), getController: { [weak self] in + return self?.controller + }, actionSelected: { [weak self] result in + self?.requestDismiss() + }, feedbackTap: { [weak self] in + self?.hapticFeedback.tap() + }, displayTextSelectionTip: false, blurBackground: true) + self.actionsContainerNode.alpha = 0.0 + self.insertSubnode(self.actionsContainerNode, aboveSubnode: previousActionsContainerNode) + previousActionsContainerNode.removeFromSupernode() + + self.contentNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1) + self.contentNode.layer.animateSpring(from: 0.35 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.5) + + if let layout = self.validLayout { + self.containerLayoutUpdated(layout, transition: .animated(duration: 0.15, curve: .easeInOut)) + } + + self.hapticFeedback.tap() + } +} diff --git a/submodules/ContextUI/Sources/PinchController.swift b/submodules/ContextUI/Sources/PinchController.swift new file mode 100644 index 0000000000..473031f90a --- /dev/null +++ b/submodules/ContextUI/Sources/PinchController.swift @@ -0,0 +1,489 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display +import TelegramPresentationData +import TextSelectionNode +import ReactionSelectionNode +import TelegramCore +import SyncCore +import SwiftSignalKit + +private func convertFrame(_ frame: CGRect, from fromView: UIView, to toView: UIView) -> CGRect { + let sourceWindowFrame = fromView.convert(frame, to: nil) + var targetWindowFrame = toView.convert(sourceWindowFrame, from: nil) + + if let fromWindow = fromView.window, let toWindow = toView.window { + targetWindowFrame.origin.x += toWindow.bounds.width - fromWindow.bounds.width + } + return targetWindowFrame +} + +final class PinchSourceGesture: UIPinchGestureRecognizer { + private final class Target { + var updated: (() -> Void)? + + @objc func onGesture(_ gesture: UIPinchGestureRecognizer) { + self.updated?() + } + } + + private let target: Target + + private(set) var currentTransform: (CGFloat, CGPoint, CGPoint)? + + var began: (() -> Void)? + var updated: ((CGFloat, CGPoint, CGPoint) -> Void)? + var ended: (() -> Void)? + + private var initialLocation: CGPoint? + private var pinchLocation = CGPoint() + private var currentOffset = CGPoint() + + private var currentNumberOfTouches = 0 + + init() { + self.target = Target() + + super.init(target: self.target, action: #selector(self.target.onGesture(_:))) + + self.target.updated = { [weak self] in + self?.gestureUpdated() + } + } + + override func reset() { + super.reset() + + self.currentNumberOfTouches = 0 + self.initialLocation = nil + } + + override func touchesBegan(_ touches: Set, with event: UIEvent) { + super.touchesBegan(touches, with: event) + + //self.currentTouches.formUnion(touches) + } + + override func touchesEnded(_ touches: Set, with event: UIEvent) { + super.touchesEnded(touches, with: event) + } + + override func touchesCancelled(_ touches: Set, with event: UIEvent) { + super.touchesCancelled(touches, with: event) + } + + override func touchesMoved(_ touches: Set, with event: UIEvent) { + super.touchesMoved(touches, with: event) + } + + private func gestureUpdated() { + switch self.state { + case .began: + self.currentOffset = CGPoint() + + let pinchLocation = self.location(in: self.view) + self.pinchLocation = pinchLocation + self.initialLocation = pinchLocation + let scale = max(1.0, self.scale) + self.currentTransform = (scale, self.pinchLocation, self.currentOffset) + + self.currentNumberOfTouches = self.numberOfTouches + + self.began?() + case .changed: + let locationSum = self.location(in: self.view) + + if self.numberOfTouches < 2 && self.currentNumberOfTouches >= 2 { + self.initialLocation = CGPoint(x: locationSum.x - self.currentOffset.x, y: locationSum.y - self.currentOffset.y) + } + self.currentNumberOfTouches = self.numberOfTouches + + if let initialLocation = self.initialLocation { + self.currentOffset = CGPoint(x: locationSum.x - initialLocation.x, y: locationSum.y - initialLocation.y) + } + if let (scale, pinchLocation, _) = self.currentTransform { + self.currentTransform = (scale, pinchLocation, self.currentOffset) + self.updated?(scale, pinchLocation, self.currentOffset) + } + + let scale = max(1.0, self.scale) + self.currentTransform = (scale, self.pinchLocation, self.currentOffset) + self.updated?(scale, self.pinchLocation, self.currentOffset) + case .ended, .cancelled: + self.ended?() + default: + break + } + } +} + +private func cancelContextGestures(node: ASDisplayNode) { + if let node = node as? ContextControllerSourceNode { + node.cancelGesture() + } + + if let supernode = node.supernode { + cancelContextGestures(node: supernode) + } +} + +private func cancelContextGestures(view: UIView) { + if let gestureRecognizers = view.gestureRecognizers { + for recognizer in gestureRecognizers { + if let recognizer = recognizer as? InteractiveTransitionGestureRecognizer { + recognizer.cancel() + } else if let recognizer = recognizer as? WindowPanRecognizer { + recognizer.cancel() + } + } + } + + if let superview = view.superview { + cancelContextGestures(view: superview) + } +} + +public final class PinchSourceContainerNode: ASDisplayNode, UIGestureRecognizerDelegate { + public let contentNode: ASDisplayNode + public var contentRect: CGRect = CGRect() + private(set) var naturalContentFrame: CGRect? + + fileprivate let gesture: PinchSourceGesture + fileprivate var panGesture: UIPanGestureRecognizer? + + public var isPinchGestureEnabled: Bool = true { + didSet { + if self.isPinchGestureEnabled != oldValue { + self.gesture.isEnabled = self.isPinchGestureEnabled + } + } + } + + public var maxPinchScale: CGFloat = 10.0 + + private var isActive: Bool = false + + public var activate: ((PinchSourceContainerNode) -> Void)? + public var scaleUpdated: ((CGFloat, ContainedViewLayoutTransition) -> Void)? + public var animatedOut: (() -> Void)? + var deactivate: (() -> Void)? + public var deactivated: (() -> Void)? + var updated: ((CGFloat, CGPoint, CGPoint) -> Void)? + + override public init() { + self.gesture = PinchSourceGesture() + self.contentNode = ASDisplayNode() + + super.init() + + self.addSubnode(self.contentNode) + + self.gesture.began = { [weak self] in + guard let strongSelf = self else { + return + } + cancelContextGestures(node: strongSelf) + cancelContextGestures(view: strongSelf.view) + strongSelf.isActive = true + + strongSelf.activate?(strongSelf) + } + + self.gesture.ended = { [weak self] in + guard let strongSelf = self else { + return + } + + strongSelf.isActive = false + strongSelf.deactivate?() + strongSelf.deactivated?() + } + + self.gesture.updated = { [weak self] scale, pinchLocation, offset in + guard let strongSelf = self else { + return + } + strongSelf.updated?(min(scale, strongSelf.maxPinchScale), pinchLocation, offset) + strongSelf.scaleUpdated?(min(scale, strongSelf.maxPinchScale), .immediate) + } + } + + override public func didLoad() { + super.didLoad() + + self.view.addGestureRecognizer(self.gesture) + self.view.disablesInteractiveTransitionGestureRecognizerNow = { [weak self] in + guard let strongSelf = self else { + return false + } + return strongSelf.isActive + } + } + + @objc private func panGestureRecognized(_ recognizer: UIPanGestureRecognizer) { + } + + public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { + return false + } + + public func update(size: CGSize, transition: ContainedViewLayoutTransition) { + let contentFrame = CGRect(origin: CGPoint(), size: size) + self.naturalContentFrame = contentFrame + if !self.isActive { + transition.updateFrame(node: self.contentNode, frame: contentFrame) + } + } + + func restoreToNaturalSize() { + guard let naturalContentFrame = self.naturalContentFrame else { + return + } + self.contentNode.frame = naturalContentFrame + } +} + +private final class PinchControllerNode: ViewControllerTracingNode { + private weak var controller: PinchController? + + private var initialSourceFrame: CGRect? + + private let clippingNode: ASDisplayNode + private let scrollingContainer: ASDisplayNode + + private let sourceNode: PinchSourceContainerNode + private let getContentAreaInScreenSpace: () -> CGRect + + private let dimNode: ASDisplayNode + + private var validLayout: ContainerViewLayout? + private var isAnimatingOut: Bool = false + + private var hapticFeedback: HapticFeedback? + + init(controller: PinchController, sourceNode: PinchSourceContainerNode, getContentAreaInScreenSpace: @escaping () -> CGRect) { + self.controller = controller + self.sourceNode = sourceNode + self.getContentAreaInScreenSpace = getContentAreaInScreenSpace + + self.dimNode = ASDisplayNode() + self.dimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.5) + self.dimNode.alpha = 0.0 + + self.clippingNode = ASDisplayNode() + self.clippingNode.clipsToBounds = true + + self.scrollingContainer = ASDisplayNode() + + super.init() + + self.addSubnode(self.dimNode) + self.addSubnode(self.clippingNode) + self.clippingNode.addSubnode(self.scrollingContainer) + + self.sourceNode.deactivate = { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.controller?.dismiss() + } + + self.sourceNode.updated = { [weak self] scale, pinchLocation, offset in + guard let strongSelf = self, let initialSourceFrame = strongSelf.initialSourceFrame else { + return + } + strongSelf.dimNode.alpha = max(0.0, min(1.0, scale - 1.0)) + + let pinchOffset = CGPoint( + x: pinchLocation.x - initialSourceFrame.width / 2.0, + y: pinchLocation.y - initialSourceFrame.height / 2.0 + ) + + var transform = CATransform3DIdentity + transform = CATransform3DTranslate(transform, offset.x - pinchOffset.x * (scale - 1.0), offset.y - pinchOffset.y * (scale - 1.0), 0.0) + transform = CATransform3DScale(transform, scale, scale, 0.0) + + strongSelf.sourceNode.contentNode.transform = transform + } + } + + deinit { + } + + override func didLoad() { + super.didLoad() + } + + func updateLayout(layout: ContainerViewLayout, transition: ContainedViewLayoutTransition, previousActionsContainerNode: ContextActionsContainerNode?) { + if self.isAnimatingOut { + return + } + + self.validLayout = layout + + transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(), size: layout.size)) + transition.updateFrame(node: self.clippingNode, frame: CGRect(origin: CGPoint(), size: layout.size)) + } + + func animateIn() { + let convertedFrame = convertFrame(self.sourceNode.bounds, from: self.sourceNode.view, to: self.view) + self.sourceNode.contentNode.frame = convertedFrame + self.initialSourceFrame = convertedFrame + self.scrollingContainer.addSubnode(self.sourceNode.contentNode) + + var updatedContentAreaInScreenSpace = self.getContentAreaInScreenSpace() + updatedContentAreaInScreenSpace.origin.x = 0.0 + updatedContentAreaInScreenSpace.size.width = self.bounds.width + + self.clippingNode.layer.animateFrame(from: updatedContentAreaInScreenSpace, to: self.clippingNode.frame, duration: 0.18 * 1.0, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue) + self.clippingNode.layer.animateBoundsOriginYAdditive(from: updatedContentAreaInScreenSpace.minY, to: 0.0, duration: 0.18 * 1.0, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue) + } + + func animateOut(completion: @escaping () -> Void) { + self.isAnimatingOut = true + + let performCompletion: () -> Void = { [weak self] in + guard let strongSelf = self else { + return + } + + strongSelf.isAnimatingOut = false + + strongSelf.sourceNode.restoreToNaturalSize() + strongSelf.sourceNode.addSubnode(strongSelf.sourceNode.contentNode) + + strongSelf.sourceNode.animatedOut?() + + completion() + } + + let convertedFrame = convertFrame(self.sourceNode.bounds, from: self.sourceNode.view, to: self.view) + self.sourceNode.contentNode.frame = convertedFrame + self.initialSourceFrame = convertedFrame + + if let (scale, pinchLocation, offset) = self.sourceNode.gesture.currentTransform, let initialSourceFrame = self.initialSourceFrame { + let duration = 0.3 + let transitionCurve: ContainedViewLayoutTransitionCurve = .easeInOut + + var updatedContentAreaInScreenSpace = self.getContentAreaInScreenSpace() + updatedContentAreaInScreenSpace.origin.x = 0.0 + updatedContentAreaInScreenSpace.size.width = self.bounds.width + + self.clippingNode.layer.animateFrame(from: self.clippingNode.frame, to: updatedContentAreaInScreenSpace, duration: duration * 1.0, timingFunction: transitionCurve.timingFunction, removeOnCompletion: false) + self.clippingNode.layer.animateBoundsOriginYAdditive(from: 0.0, to: updatedContentAreaInScreenSpace.minY, duration: duration * 1.0, timingFunction: transitionCurve.timingFunction, removeOnCompletion: false) + + let transition: ContainedViewLayoutTransition = .animated(duration: duration, curve: .spring) + if self.hapticFeedback == nil { + self.hapticFeedback = HapticFeedback() + } + self.hapticFeedback?.prepareImpact(.light) + self.hapticFeedback?.impact(.light) + + self.sourceNode.scaleUpdated?(1.0, transition) + + let pinchOffset = CGPoint( + x: pinchLocation.x - initialSourceFrame.width / 2.0, + y: pinchLocation.y - initialSourceFrame.height / 2.0 + ) + + var transform = CATransform3DIdentity + transform = CATransform3DScale(transform, scale, scale, 0.0) + + self.sourceNode.contentNode.transform = CATransform3DIdentity + self.sourceNode.contentNode.position = CGPoint(x: initialSourceFrame.midX, y: initialSourceFrame.midY) + self.sourceNode.contentNode.layer.animateSpring(from: scale as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: duration * 1.2, damping: 110.0) + self.sourceNode.contentNode.layer.animatePosition(from: CGPoint(x: offset.x - pinchOffset.x * (scale - 1.0), y: offset.y - pinchOffset.y * (scale - 1.0)), to: CGPoint(), duration: duration, timingFunction: kCAMediaTimingFunctionSpring, additive: true, force: true, completion: { _ in + performCompletion() + }) + + let dimNodeTransition: ContainedViewLayoutTransition = .animated(duration: 0.3, curve: transitionCurve) + dimNodeTransition.updateAlpha(node: self.dimNode, alpha: 0.0) + } else { + performCompletion() + } + } + + func addRelativeContentOffset(_ offset: CGPoint, transition: ContainedViewLayoutTransition) { + if self.isAnimatingOut { + self.scrollingContainer.bounds = self.scrollingContainer.bounds.offsetBy(dx: 0.0, dy: offset.y) + transition.animateOffsetAdditive(node: self.scrollingContainer, offset: -offset.y) + } + } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + return nil + } +} + +public final class PinchController: ViewController, StandalonePresentableController { + private let _ready = Promise() + override public var ready: Promise { + return self._ready + } + + private let sourceNode: PinchSourceContainerNode + private let getContentAreaInScreenSpace: () -> CGRect + + private var wasDismissed = false + + private var controllerNode: PinchControllerNode { + return self.displayNode as! PinchControllerNode + } + + public init(sourceNode: PinchSourceContainerNode, getContentAreaInScreenSpace: @escaping () -> CGRect) { + self.sourceNode = sourceNode + self.getContentAreaInScreenSpace = getContentAreaInScreenSpace + + super.init(navigationBarPresentationData: nil) + + self.statusBar.statusBarStyle = .Ignore + + self.lockOrientation = true + self.blocksBackgroundWhenInOverlay = true + } + + required init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + } + + override public func loadDisplayNode() { + self.displayNode = PinchControllerNode(controller: self, sourceNode: self.sourceNode, getContentAreaInScreenSpace: self.getContentAreaInScreenSpace) + + self.displayNodeDidLoad() + + self._ready.set(.single(true)) + } + + override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + super.containerLayoutUpdated(layout, transition: transition) + + self.controllerNode.updateLayout(layout: layout, transition: transition, previousActionsContainerNode: nil) + } + + override public func viewDidAppear(_ animated: Bool) { + if self.ignoreAppearanceMethodInvocations() { + return + } + super.viewDidAppear(animated) + + self.controllerNode.animateIn() + } + + override public func dismiss(completion: (() -> Void)? = nil) { + if !self.wasDismissed { + self.wasDismissed = true + self.controllerNode.animateOut(completion: { [weak self] in + self?.presentingViewController?.dismiss(animated: false, completion: nil) + completion?() + }) + } + } + + public func addRelativeContentOffset(_ offset: CGPoint, transition: ContainedViewLayoutTransition) { + self.controllerNode.addRelativeContentOffset(offset, transition: transition) + } +} diff --git a/submodules/CounterContollerTitleView/Sources/CounterContollerTitleView.swift b/submodules/CounterContollerTitleView/Sources/CounterContollerTitleView.swift index 2fd165435e..3e6872815e 100644 --- a/submodules/CounterContollerTitleView/Sources/CounterContollerTitleView.swift +++ b/submodules/CounterContollerTitleView/Sources/CounterContollerTitleView.swift @@ -22,7 +22,7 @@ public final class CounterContollerTitleView: UIView { public var title: CounterContollerTitle = CounterContollerTitle(title: "", counter: "") { didSet { if self.title != oldValue { - self.titleNode.attributedText = NSAttributedString(string: self.title.title, font: Font.bold(17.0), textColor: self.theme.rootController.navigationBar.primaryTextColor) + self.titleNode.attributedText = NSAttributedString(string: self.title.title, font: Font.semibold(17.0), textColor: self.theme.rootController.navigationBar.primaryTextColor) self.subtitleNode.attributedText = NSAttributedString(string: self.title.counter, font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor) self.accessibilityLabel = self.title.title diff --git a/submodules/CountrySelectionUI/Sources/AuthorizationSequenceCountrySelectionController.swift b/submodules/CountrySelectionUI/Sources/AuthorizationSequenceCountrySelectionController.swift index 653cf32759..a71f96627e 100644 --- a/submodules/CountrySelectionUI/Sources/AuthorizationSequenceCountrySelectionController.swift +++ b/submodules/CountrySelectionUI/Sources/AuthorizationSequenceCountrySelectionController.swift @@ -69,8 +69,8 @@ private func loadCountryCodes() -> [Country] { private var countryCodes: [Country] = loadCountryCodes() private var countryCodesByPrefix: [String: (Country, Country.CountryCode)] = [:] -public func loadServerCountryCodes(accountManager: AccountManager, network: Network, completion: @escaping () -> Void) { - let _ = (getCountriesList(accountManager: accountManager, network: network, langCode: nil) +public func loadServerCountryCodes(accountManager: AccountManager, engine: TelegramEngineUnauthorized, completion: @escaping () -> Void) { + let _ = (engine.localization.getCountriesList(accountManager: accountManager, langCode: nil) |> deliverOnMainQueue).start(next: { countries in countryCodes = countries @@ -93,6 +93,30 @@ public func loadServerCountryCodes(accountManager: AccountManager, network: Netw }) } +public func loadServerCountryCodes(accountManager: AccountManager, engine: TelegramEngine, completion: @escaping () -> Void) { + let _ = (engine.localization.getCountriesList(accountManager: accountManager, langCode: nil) + |> deliverOnMainQueue).start(next: { countries in + countryCodes = countries + + var countriesByPrefix: [String: (Country, Country.CountryCode)] = [:] + for country in countries { + for code in country.countryCodes { + if !code.prefixes.isEmpty { + for prefix in code.prefixes { + countriesByPrefix["\(code.code)\(prefix)"] = (country, code) + } + } else { + countriesByPrefix[code.code] = (country, code) + } + } + } + countryCodesByPrefix = countriesByPrefix + Queue.mainQueue().async { + completion() + } + }) +} + private final class AuthorizationSequenceCountrySelectionNavigationContentNode: NavigationBarContentNode { private let theme: PresentationTheme private let strings: PresentationStrings @@ -315,7 +339,7 @@ public final class AuthorizationSequenceCountrySelectionController: ViewControll 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.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition) } private func cancelPressed() { diff --git a/submodules/DatePickerNode/Sources/DatePickerNode.swift b/submodules/DatePickerNode/Sources/DatePickerNode.swift index 9f1c168462..6e9a596c78 100644 --- a/submodules/DatePickerNode/Sources/DatePickerNode.swift +++ b/submodules/DatePickerNode/Sources/DatePickerNode.swift @@ -68,7 +68,7 @@ private let upperLimitDate = Date(timeIntervalSince1970: Double(Int32.max - 1)) private let controlFont = Font.regular(17.0) private let dayFont = Font.regular(13.0) private let dateFont = Font.with(size: 17.0, design: .regular, traits: .monospacedNumbers) -private let selectedDateFont = Font.with(size: 17.0, design: .regular, traits: [.bold, .monospacedNumbers]) +private let selectedDateFont = Font.with(size: 17.0, design: .regular, weight: .bold, traits: .monospacedNumbers) private var calendar: Calendar = { var calendar = Calendar(identifier: .gregorian) diff --git a/submodules/DebugSettingsUI/BUILD b/submodules/DebugSettingsUI/BUILD new file mode 100644 index 0000000000..21c301e2de --- /dev/null +++ b/submodules/DebugSettingsUI/BUILD @@ -0,0 +1,27 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "DebugSettingsUI", + module_name = "DebugSettingsUI", + srcs = glob([ + "Sources/**/*.swift", + ]), + deps = [ + "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", + "//submodules/Display:Display", + "//submodules/Postbox:Postbox", + "//submodules/TelegramCore:TelegramCore", + "//submodules/SyncCore:SyncCore", + "//submodules/MtProtoKit:MtProtoKit", + "//submodules/TelegramPresentationData:TelegramPresentationData", + "//submodules/TelegramUIPreferences:TelegramUIPreferences", + "//submodules/ItemListUI:ItemListUI", + "//submodules/PresentationDataUtils:PresentationDataUtils", + "//submodules/OverlayStatusController:OverlayStatusController", + "//submodules/AccountContext:AccountContext", + "//submodules/AppBundle:AppBundle", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/SettingsUI/Sources/DebugAccountsController.swift b/submodules/DebugSettingsUI/Sources/DebugAccountsController.swift similarity index 100% rename from submodules/SettingsUI/Sources/DebugAccountsController.swift rename to submodules/DebugSettingsUI/Sources/DebugAccountsController.swift diff --git a/submodules/SettingsUI/Sources/DebugController.swift b/submodules/DebugSettingsUI/Sources/DebugController.swift similarity index 86% rename from submodules/SettingsUI/Sources/DebugController.swift rename to submodules/DebugSettingsUI/Sources/DebugController.swift index 9882fcd31b..8a796df36a 100644 --- a/submodules/SettingsUI/Sources/DebugController.swift +++ b/submodules/DebugSettingsUI/Sources/DebugController.swift @@ -13,7 +13,7 @@ import ItemListUI import PresentationDataUtils import OverlayStatusController import AccountContext -import TelegramCallsUI +import AppBundle @objc private final class DebugControllerMailComposeDelegate: NSObject, MFMailComposeViewControllerDelegate { public func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith result: MFMailComposeResult, error: Error?) { @@ -28,18 +28,21 @@ private final class DebugControllerArguments { let presentController: (ViewController, ViewControllerPresentationArguments?) -> Void let pushController: (ViewController) -> Void let getRootController: () -> UIViewController? + let getNavigationController: () -> NavigationController? - init(sharedContext: SharedAccountContext, context: AccountContext?, mailComposeDelegate: DebugControllerMailComposeDelegate, presentController: @escaping (ViewController, ViewControllerPresentationArguments?) -> Void, pushController: @escaping (ViewController) -> Void, getRootController: @escaping () -> UIViewController?) { + init(sharedContext: SharedAccountContext, context: AccountContext?, mailComposeDelegate: DebugControllerMailComposeDelegate, presentController: @escaping (ViewController, ViewControllerPresentationArguments?) -> Void, pushController: @escaping (ViewController) -> Void, getRootController: @escaping () -> UIViewController?, getNavigationController: @escaping () -> NavigationController?) { self.sharedContext = sharedContext self.context = context self.mailComposeDelegate = mailComposeDelegate self.presentController = presentController self.pushController = pushController self.getRootController = getRootController + self.getNavigationController = getNavigationController } } private enum DebugControllerSection: Int32 { + case sticker case logs case logging case experiments @@ -49,6 +52,7 @@ private enum DebugControllerSection: Int32 { } private enum DebugControllerEntry: ItemListNodeEntry { + case testStickerImport(PresentationTheme) case sendLogs(PresentationTheme) case sendOneLog(PresentationTheme) case sendShareLogs @@ -63,7 +67,7 @@ private enum DebugControllerEntry: ItemListNodeEntry { case skipReadHistory(PresentationTheme, Bool) case crashOnSlowQueries(PresentationTheme, Bool) case clearTips(PresentationTheme) - case reimport(PresentationTheme) + case crash(PresentationTheme) case resetData(PresentationTheme) case resetDatabase(PresentationTheme) case resetDatabaseAndCache(PresentationTheme) @@ -73,8 +77,9 @@ private enum DebugControllerEntry: ItemListNodeEntry { case optimizeDatabase(PresentationTheme) case photoPreview(PresentationTheme, Bool) case knockoutWallpaper(PresentationTheme, Bool) - case demoAudioStream(Bool) - case snapPinListToTop(Bool) + case demoVideoChats(Bool) + case experimentalCompatibility(Bool) + case enableNoiseSuppression(Bool) case playerEmbedding(Bool) case playlistPlayback(Bool) case voiceConference @@ -86,6 +91,8 @@ private enum DebugControllerEntry: ItemListNodeEntry { var section: ItemListSectionId { switch self { + case .testStickerImport: + return DebugControllerSection.sticker.rawValue case .sendLogs, .sendOneLog, .sendShareLogs, .sendNotificationLogs, .sendCriticalLogs: return DebugControllerSection.logs.rawValue case .accounts: @@ -94,7 +101,7 @@ private enum DebugControllerEntry: ItemListNodeEntry { return DebugControllerSection.logging.rawValue case .enableRaiseToSpeak, .keepChatNavigationStack, .skipReadHistory, .crashOnSlowQueries: return DebugControllerSection.experiments.rawValue - case .clearTips, .reimport, .resetData, .resetDatabase, .resetDatabaseAndCache, .resetHoles, .reindexUnread, .resetBiometricsData, .optimizeDatabase, .photoPreview, .knockoutWallpaper, .demoAudioStream, .snapPinListToTop, .playerEmbedding, .playlistPlayback, .voiceConference: + case .clearTips, .crash, .resetData, .resetDatabase, .resetDatabaseAndCache, .resetHoles, .reindexUnread, .resetBiometricsData, .optimizeDatabase, .photoPreview, .knockoutWallpaper, .demoVideoChats, .playerEmbedding, .playlistPlayback, .voiceConference, .experimentalCompatibility, .enableNoiseSuppression: return DebugControllerSection.experiments.rawValue case .preferredVideoCodec: return DebugControllerSection.videoExperiments.rawValue @@ -107,66 +114,70 @@ private enum DebugControllerEntry: ItemListNodeEntry { var stableId: Int { switch self { - case .sendLogs: + case .testStickerImport: return 0 - case .sendOneLog: + case .sendLogs: return 1 - case .sendShareLogs: + case .sendOneLog: return 2 - case .sendNotificationLogs: + case .sendShareLogs: return 3 - case .sendCriticalLogs: + case .sendNotificationLogs: return 4 - case .accounts: + case .sendCriticalLogs: return 5 - case .logToFile: + case .accounts: return 6 - case .logToConsole: + case .logToFile: return 7 - case .redactSensitiveData: + case .logToConsole: return 8 - case .enableRaiseToSpeak: + case .redactSensitiveData: return 9 - case .keepChatNavigationStack: + case .enableRaiseToSpeak: return 10 - case .skipReadHistory: + case .keepChatNavigationStack: return 11 - case .crashOnSlowQueries: + case .skipReadHistory: return 12 - case .clearTips: + case .crashOnSlowQueries: return 13 - case .reimport: + case .clearTips: return 14 - case .resetData: + case .crash: return 15 - case .resetDatabase: + case .resetData: return 16 - case .resetDatabaseAndCache: + case .resetDatabase: return 17 - case .resetHoles: + case .resetDatabaseAndCache: return 18 - case .reindexUnread: + case .resetHoles: return 19 - case .resetBiometricsData: + case .reindexUnread: return 20 - case .optimizeDatabase: + case .resetBiometricsData: return 21 - case .photoPreview: + case .optimizeDatabase: return 22 - case .knockoutWallpaper: + case .photoPreview: return 23 - case .demoAudioStream: + case .knockoutWallpaper: return 24 - case .snapPinListToTop: + case .demoVideoChats: return 25 - case .playerEmbedding: + case .experimentalCompatibility: return 26 - case .playlistPlayback: + case .enableNoiseSuppression: return 27 - case .voiceConference: + case .playerEmbedding: return 28 + case .playlistPlayback: + return 29 + case .voiceConference: + return 30 case let .preferredVideoCodec(index, _, _, _): - return 29 + index + return 31 + index case .disableVideoAspectScaling: return 100 case .enableVoipTcp: @@ -185,7 +196,22 @@ private enum DebugControllerEntry: ItemListNodeEntry { func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { let arguments = arguments as! DebugControllerArguments switch self { - case let .sendLogs(theme): + case .testStickerImport: + return ItemListActionItem(presentationData: presentationData, title: "Simulate Stickers Import", kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: { + guard let context = arguments.context else { + return + } + if let url = getAppBundle().url(forResource: "importstickers", withExtension: "json"), let data = try? Data(contentsOf: url) { + let dataType = "org.telegram.third-party.stickerset" + if #available(iOS 10.0, *) { + UIPasteboard.general.setItems([[dataType: data]], options: [UIPasteboard.OptionsKey.localOnly: true, UIPasteboard.OptionsKey.expirationDate: NSDate(timeIntervalSinceNow: 60)]) + } else { + UIPasteboard.general.setData(data, forPasteboardType: dataType) + } + context.sharedContext.openResolvedUrl(.importStickers, context: context, urlContext: .generic, navigationController: arguments.getNavigationController(), openPeer: { _, _ in }, sendFile: nil, sendSticker: nil, requestMessageActionUrlAuth: nil, joinVoiceChat: nil, present: { c, a in arguments.presentController(c, a as? ViewControllerPresentationArguments) }, dismissInput: {}, contentContext: nil) + } + }) + case .sendLogs: return ItemListDisclosureItem(presentationData: presentationData, title: "Send Logs (Up to 40 MB)", label: "", sectionId: self.section, style: .blocks, action: { let _ = (Logger.shared.collectLogs() |> deliverOnMainQueue).start(next: { logs in @@ -194,7 +220,7 @@ private enum DebugControllerEntry: ItemListNodeEntry { var items: [ActionSheetButtonItem] = [] - if let context = arguments.context { + if let context = arguments.context, context.sharedContext.applicationBindings.isMainApp { items.append(ActionSheetButtonItem(title: "Via Telegram", color: .accent, action: { [weak actionSheet] in actionSheet?.dismissAnimated() @@ -220,12 +246,12 @@ private enum DebugControllerEntry: ItemListNodeEntry { } } - let id = arc4random64() + let id = Int64.random(in: Int64.min ... Int64.max) let fileResource = LocalFileMediaResource(fileId: id, size: logData.count, isSecretRelated: false) context.account.postbox.mediaBox.storeResourceData(fileResource.id, data: logData) let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: id), partialReference: nil, resource: fileResource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "application/text", size: logData.count, attributes: [.FileName(fileName: "Log-iOS-Full.txt")]) - let message: EnqueueMessage = .message(text: "", attributes: [], mediaReference: .standalone(media: file), replyToMessageId: nil, localGroupingKey: nil) + let message: EnqueueMessage = .message(text: "", attributes: [], mediaReference: .standalone(media: file), replyToMessageId: nil, localGroupingKey: nil, correlationId: nil) let _ = enqueueMessages(account: context.account, peerId: peerId, messages: [message]).start() } @@ -255,7 +281,7 @@ private enum DebugControllerEntry: ItemListNodeEntry { arguments.presentController(actionSheet, nil) }) }) - case let .sendOneLog(theme): + case .sendOneLog: return ItemListDisclosureItem(presentationData: presentationData, title: "Send Latest Logs (Up to 4 MB)", label: "", sectionId: self.section, style: .blocks, action: { let _ = (Logger.shared.collectLogs() |> deliverOnMainQueue).start(next: { logs in @@ -264,7 +290,7 @@ private enum DebugControllerEntry: ItemListNodeEntry { var items: [ActionSheetButtonItem] = [] - if let context = arguments.context { + if let context = arguments.context, context.sharedContext.applicationBindings.isMainApp { items.append(ActionSheetButtonItem(title: "Via Telegram", color: .accent, action: { [weak actionSheet] in actionSheet?.dismissAnimated() @@ -300,12 +326,12 @@ private enum DebugControllerEntry: ItemListNodeEntry { } } - let id = arc4random64() + let id = Int64.random(in: Int64.min ... Int64.max) let fileResource = LocalFileMediaResource(fileId: id, size: logData.count, isSecretRelated: false) context.account.postbox.mediaBox.storeResourceData(fileResource.id, data: logData) let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: id), partialReference: nil, resource: fileResource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "application/text", size: logData.count, attributes: [.FileName(fileName: "Log-iOS-Short.txt")]) - let message: EnqueueMessage = .message(text: "", attributes: [], mediaReference: .standalone(media: file), replyToMessageId: nil, localGroupingKey: nil) + let message: EnqueueMessage = .message(text: "", attributes: [], mediaReference: .standalone(media: file), replyToMessageId: nil, localGroupingKey: nil, correlationId: nil) let _ = enqueueMessages(account: context.account, peerId: peerId, messages: [message]).start() } @@ -337,7 +363,7 @@ private enum DebugControllerEntry: ItemListNodeEntry { arguments.presentController(actionSheet, nil) }) }) - case let .sendShareLogs: + case .sendShareLogs: return ItemListDisclosureItem(presentationData: presentationData, title: "Send Share Logs (Up to 40 MB)", label: "", sectionId: self.section, style: .blocks, action: { let _ = (Logger.shared.collectLogs(prefix: "/share-logs") |> deliverOnMainQueue).start(next: { logs in @@ -346,7 +372,7 @@ private enum DebugControllerEntry: ItemListNodeEntry { var items: [ActionSheetButtonItem] = [] - if let context = arguments.context { + if let context = arguments.context, context.sharedContext.applicationBindings.isMainApp { items.append(ActionSheetButtonItem(title: "Via Telegram", color: .accent, action: { [weak actionSheet] in actionSheet?.dismissAnimated() @@ -372,12 +398,12 @@ private enum DebugControllerEntry: ItemListNodeEntry { } } - let id = arc4random64() + let id = Int64.random(in: Int64.min ... Int64.max) let fileResource = LocalFileMediaResource(fileId: id, size: logData.count, isSecretRelated: false) context.account.postbox.mediaBox.storeResourceData(fileResource.id, data: logData) let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: id), partialReference: nil, resource: fileResource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "application/text", size: logData.count, attributes: [.FileName(fileName: "Log-iOS-Full.txt")]) - let message: EnqueueMessage = .message(text: "", attributes: [], mediaReference: .standalone(media: file), replyToMessageId: nil, localGroupingKey: nil) + let message: EnqueueMessage = .message(text: "", attributes: [], mediaReference: .standalone(media: file), replyToMessageId: nil, localGroupingKey: nil, correlationId: nil) let _ = enqueueMessages(account: context.account, peerId: peerId, messages: [message]).start() } @@ -407,7 +433,7 @@ private enum DebugControllerEntry: ItemListNodeEntry { arguments.presentController(actionSheet, nil) }) }) - case let .sendNotificationLogs(theme): + case .sendNotificationLogs: return ItemListDisclosureItem(presentationData: presentationData, title: "Send Notification Logs", label: "", sectionId: self.section, style: .blocks, action: { let _ = (Logger(rootPath: arguments.sharedContext.basePath, basePath: arguments.sharedContext.basePath + "/notificationServiceLogs").collectLogs() |> deliverOnMainQueue).start(next: { logs in @@ -422,9 +448,9 @@ private enum DebugControllerEntry: ItemListNodeEntry { strongController.dismiss() let messages = logs.map { (name, path) -> EnqueueMessage in - let id = arc4random64() + let id = Int64.random(in: Int64.min ... Int64.max) let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: id), partialReference: nil, resource: LocalFileReferenceMediaResource(localFilePath: path, randomId: id), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "application/text", size: nil, attributes: [.FileName(fileName: name)]) - return .message(text: "", attributes: [], mediaReference: .standalone(media: file), replyToMessageId: nil, localGroupingKey: nil) + return .message(text: "", attributes: [], mediaReference: .standalone(media: file), replyToMessageId: nil, localGroupingKey: nil, correlationId: nil) } let _ = enqueueMessages(account: context.account, peerId: peerId, messages: messages).start() } @@ -432,7 +458,7 @@ private enum DebugControllerEntry: ItemListNodeEntry { arguments.pushController(controller) }) }) - case let .sendCriticalLogs(theme): + case .sendCriticalLogs: return ItemListDisclosureItem(presentationData: presentationData, title: "Send Critical Logs", label: "", sectionId: self.section, style: .blocks, action: { let _ = (Logger.shared.collectShortLogFiles() |> deliverOnMainQueue).start(next: { logs in @@ -441,7 +467,7 @@ private enum DebugControllerEntry: ItemListNodeEntry { var items: [ActionSheetButtonItem] = [] - if let context = arguments.context { + if let context = arguments.context, context.sharedContext.applicationBindings.isMainApp { items.append(ActionSheetButtonItem(title: "Via Telegram", color: .accent, action: { [weak actionSheet] in actionSheet?.dismissAnimated() @@ -453,9 +479,9 @@ private enum DebugControllerEntry: ItemListNodeEntry { strongController.dismiss() let messages = logs.map { (name, path) -> EnqueueMessage in - let id = arc4random64() + let id = Int64.random(in: Int64.min ... Int64.max) let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: id), partialReference: nil, resource: LocalFileReferenceMediaResource(localFilePath: path, randomId: id), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "application/text", size: nil, attributes: [.FileName(fileName: name)]) - return .message(text: "", attributes: [], mediaReference: .standalone(media: file), replyToMessageId: nil, localGroupingKey: nil) + return .message(text: "", attributes: [], mediaReference: .standalone(media: file), replyToMessageId: nil, localGroupingKey: nil, correlationId: nil) } let _ = enqueueMessages(account: context.account, peerId: peerId, messages: messages).start() } @@ -485,38 +511,38 @@ private enum DebugControllerEntry: ItemListNodeEntry { arguments.presentController(actionSheet, nil) }) }) - case let .accounts(theme): + case .accounts: 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): + case let .logToFile(_, value): 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): + case let .logToConsole(_, value): 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): + case let .redactSensitiveData(_, value): 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): + case let .enableRaiseToSpeak(_, value): 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): + case let .keepChatNavigationStack(_, value): 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 @@ -524,7 +550,7 @@ private enum DebugControllerEntry: ItemListNodeEntry { return settings }).start() }) - case let .skipReadHistory(theme, value): + case let .skipReadHistory(_, value): 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 @@ -532,7 +558,7 @@ private enum DebugControllerEntry: ItemListNodeEntry { return settings }).start() }) - case let .crashOnSlowQueries(theme, value): + case let .crashOnSlowQueries(_, value): 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 @@ -540,7 +566,7 @@ private enum DebugControllerEntry: ItemListNodeEntry { return settings }).start() }) - case let .clearTips(theme): + case .clearTips: 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() @@ -548,28 +574,18 @@ private enum DebugControllerEntry: ItemListNodeEntry { if let context = arguments.context { let _ = (context.account.postbox.transaction { transaction -> Void in transaction.clearItemCacheCollection(collectionId: Namespaces.CachedItemCollection.cachedPollResults) - unmarkChatListFeaturedFiltersAsSeen(transaction: transaction) transaction.clearItemCacheCollection(collectionId: Namespaces.CachedItemCollection.cachedStickerPacks) }).start() + + let _ = context.engine.peers.unmarkChatListFeaturedFiltersAsSeen() } }) - case let .reimport(theme): - 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) - - guard let appGroupUrl = maybeAppGroupUrl else { - return - } - - let statusPath = appGroupUrl.path + "/Documents/importcompleted" - if FileManager.default.fileExists(atPath: statusPath) { - let _ = try? FileManager.default.removeItem(at: URL(fileURLWithPath: statusPath)) - exit(0) - } + case .crash: + return ItemListActionItem(presentationData: presentationData, title: "Crash", kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: { + preconditionFailure() }) - case let .resetData(theme): + case .resetData: 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(presentationData: presentationData) @@ -588,7 +604,7 @@ private enum DebugControllerEntry: ItemListNodeEntry { ])]) arguments.presentController(actionSheet, nil) }) - case let .resetDatabase(theme): + case .resetDatabase: return ItemListActionItem(presentationData: presentationData, title: "Clear Database", kind: .destructive, alignment: .natural, sectionId: self.section, style: .blocks, action: { guard let context = arguments.context else { return @@ -611,7 +627,7 @@ private enum DebugControllerEntry: ItemListNodeEntry { ])]) arguments.presentController(actionSheet, nil) }) - case let .resetDatabaseAndCache(theme): + case .resetDatabaseAndCache: return ItemListActionItem(presentationData: presentationData, title: "Clear Database and Cache", kind: .destructive, alignment: .natural, sectionId: self.section, style: .blocks, action: { guard let context = arguments.context else { return @@ -634,7 +650,7 @@ private enum DebugControllerEntry: ItemListNodeEntry { ])]) arguments.presentController(actionSheet, nil) }) - case let .resetHoles(theme): + case .resetHoles: return ItemListActionItem(presentationData: presentationData, title: "Reset Holes", kind: .destructive, alignment: .natural, sectionId: self.section, style: .blocks, action: { guard let context = arguments.context else { return @@ -649,7 +665,7 @@ private enum DebugControllerEntry: ItemListNodeEntry { controller.dismiss() }) }) - case let .reindexUnread(theme): + case .reindexUnread: 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 @@ -664,13 +680,13 @@ private enum DebugControllerEntry: ItemListNodeEntry { controller.dismiss() }) }) - case let .resetBiometricsData(theme): + case .resetBiometricsData: 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): + case .optimizeDatabase: return ItemListActionItem(presentationData: presentationData, title: "Optimize Database", kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: { guard let context = arguments.context else { return @@ -686,7 +702,7 @@ private enum DebugControllerEntry: ItemListNodeEntry { arguments.presentController(controller, nil) }) }) - case let .photoPreview(theme, value): + case let .photoPreview(_, value): return ItemListSwitchItem(presentationData: presentationData, title: "Media Preview (Updated)", value: value, sectionId: self.section, style: .blocks, updated: { value in let _ = arguments.sharedContext.accountManager.transaction ({ transaction in transaction.updateSharedData(ApplicationSpecificSharedDataKeys.experimentalUISettings, { settings in @@ -706,22 +722,32 @@ private enum DebugControllerEntry: ItemListNodeEntry { }) }).start() }) - case let .demoAudioStream(value): - return ItemListSwitchItem(presentationData: presentationData, title: "Demo Audio Stream", value: value, sectionId: self.section, style: .blocks, updated: { value in + case let .demoVideoChats(value): + return ItemListSwitchItem(presentationData: presentationData, title: "Demo Video", 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.demoAudioStream = value + settings.demoVideoChats = value return settings }) }).start() }) - case let .snapPinListToTop(value): - return ItemListSwitchItem(presentationData: presentationData, title: "Pin List Top Edge", value: value, sectionId: self.section, style: .blocks, updated: { value in + case let .experimentalCompatibility(value): + return ItemListSwitchItem(presentationData: presentationData, title: "Experimental Compatibility", 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.snapPinListToTop = value + settings.experimentalCompatibility = value + return settings + }) + }).start() + }) + case let .enableNoiseSuppression(value): + return ItemListSwitchItem(presentationData: presentationData, title: "Noise Suppression", 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.enableNoiseSuppression = value return settings }) }).start() @@ -782,9 +808,9 @@ private enum DebugControllerEntry: ItemListNodeEntry { }) }).start() }) - case let .hostInfo(theme, string): + case let .hostInfo(_, string): return ItemListTextItem(presentationData: presentationData, text: .plain(string), sectionId: self.section) - case let .versionInfo(theme): + case .versionInfo: let bundle = Bundle.main let bundleId = bundle.bundleIdentifier ?? "" let bundleVersion = bundle.infoDictionary?["CFBundleShortVersionString"] ?? "" @@ -794,44 +820,53 @@ private enum DebugControllerEntry: ItemListNodeEntry { } } -private func debugControllerEntries(presentationData: PresentationData, loggingSettings: LoggingSettings, mediaInputSettings: MediaInputSettings, experimentalSettings: ExperimentalUISettings, networkSettings: NetworkSettings?, hasLegacyAppData: Bool) -> [DebugControllerEntry] { +private func debugControllerEntries(sharedContext: SharedAccountContext, presentationData: PresentationData, loggingSettings: LoggingSettings, mediaInputSettings: MediaInputSettings, experimentalSettings: ExperimentalUISettings, networkSettings: NetworkSettings?, hasLegacyAppData: Bool) -> [DebugControllerEntry] { var entries: [DebugControllerEntry] = [] + + let isMainApp = sharedContext.applicationBindings.isMainApp +// entries.append(.testStickerImport(presentationData.theme)) entries.append(.sendLogs(presentationData.theme)) entries.append(.sendOneLog(presentationData.theme)) entries.append(.sendShareLogs) entries.append(.sendNotificationLogs(presentationData.theme)) entries.append(.sendCriticalLogs(presentationData.theme)) - entries.append(.accounts(presentationData.theme)) + if isMainApp { + entries.append(.accounts(presentationData.theme)) + } entries.append(.logToFile(presentationData.theme, loggingSettings.logToFile)) entries.append(.logToConsole(presentationData.theme, loggingSettings.logToConsole)) entries.append(.redactSensitiveData(presentationData.theme, loggingSettings.redactSensitiveData)) - - entries.append(.enableRaiseToSpeak(presentationData.theme, mediaInputSettings.enableRaiseToSpeak)) - entries.append(.keepChatNavigationStack(presentationData.theme, experimentalSettings.keepChatNavigationStack)) - #if DEBUG - entries.append(.skipReadHistory(presentationData.theme, experimentalSettings.skipReadHistory)) - #endif - entries.append(.crashOnSlowQueries(presentationData.theme, experimentalSettings.crashOnLongQueries)) - entries.append(.clearTips(presentationData.theme)) - if hasLegacyAppData { - entries.append(.reimport(presentationData.theme)) + + if isMainApp { + entries.append(.enableRaiseToSpeak(presentationData.theme, mediaInputSettings.enableRaiseToSpeak)) + entries.append(.keepChatNavigationStack(presentationData.theme, experimentalSettings.keepChatNavigationStack)) + #if DEBUG + entries.append(.skipReadHistory(presentationData.theme, experimentalSettings.skipReadHistory)) + #endif } + entries.append(.crashOnSlowQueries(presentationData.theme, experimentalSettings.crashOnLongQueries)) + if isMainApp { + entries.append(.clearTips(presentationData.theme)) + } + entries.append(.crash(presentationData.theme)) entries.append(.resetData(presentationData.theme)) entries.append(.resetDatabase(presentationData.theme)) entries.append(.resetDatabaseAndCache(presentationData.theme)) entries.append(.resetHoles(presentationData.theme)) - entries.append(.reindexUnread(presentationData.theme)) + if isMainApp { + 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(.demoAudioStream(experimentalSettings.demoAudioStream)) - entries.append(.snapPinListToTop(experimentalSettings.snapPinListToTop)) - entries.append(.playerEmbedding(experimentalSettings.playerEmbedding)) - entries.append(.playlistPlayback(experimentalSettings.playlistPlayback)) - - entries.append(.voiceConference) + if isMainApp { + entries.append(.knockoutWallpaper(presentationData.theme, experimentalSettings.knockoutWallpaper)) + entries.append(.demoVideoChats(experimentalSettings.demoVideoChats)) + entries.append(.experimentalCompatibility(experimentalSettings.experimentalCompatibility)) + entries.append(.enableNoiseSuppression(experimentalSettings.enableNoiseSuppression)) + entries.append(.playerEmbedding(experimentalSettings.playerEmbedding)) + entries.append(.playlistPlayback(experimentalSettings.playlistPlayback)) + } let codecs: [(String, String?)] = [ ("No Preference", nil), @@ -844,9 +879,11 @@ private func debugControllerEntries(presentationData: PresentationData, loggingS for i in 0 ..< codecs.count { entries.append(.preferredVideoCodec(i, codecs[i].0, codecs[i].1, experimentalSettings.preferredVideoCodec == codecs[i].1)) } - - entries.append(.disableVideoAspectScaling(experimentalSettings.disableVideoAspectScaling)) - entries.append(.enableVoipTcp(experimentalSettings.enableVoipTcp)) + + if isMainApp { + entries.append(.disableVideoAspectScaling(experimentalSettings.disableVideoAspectScaling)) + entries.append(.enableVoipTcp(experimentalSettings.enableVoipTcp)) + } if let backupHostOverride = networkSettings?.backupHostOverride { entries.append(.hostInfo(presentationData.theme, "Host: \(backupHostOverride)")) @@ -861,6 +898,7 @@ public func debugController(sharedContext: SharedAccountContext, context: Accoun var pushControllerImpl: ((ViewController) -> Void)? var dismissImpl: (() -> Void)? var getRootControllerImpl: (() -> UIViewController?)? + var getNavigationControllerImpl: (() -> NavigationController?)? let arguments = DebugControllerArguments(sharedContext: sharedContext, context: context, mailComposeDelegate: DebugControllerMailComposeDelegate(), presentController: { controller, arguments in presentControllerImpl?(controller, arguments) @@ -868,6 +906,8 @@ public func debugController(sharedContext: SharedAccountContext, context: Accoun pushControllerImpl?(controller) }, getRootController: { return getRootControllerImpl?() + }, getNavigationController: { + return getNavigationControllerImpl?() }) let appGroupName = "group.\(Bundle.main.bundleIdentifier!)" @@ -915,7 +955,7 @@ public func debugController(sharedContext: SharedAccountContext, context: Accoun } 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) + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: debugControllerEntries(sharedContext: sharedContext, presentationData: presentationData, loggingSettings: loggingSettings, mediaInputSettings: mediaInputSettings, experimentalSettings: experimentalSettings, networkSettings: networkSettings, hasLegacyAppData: hasLegacyAppData), style: .blocks) return (controllerState, (listState, arguments)) } @@ -934,5 +974,8 @@ public func debugController(sharedContext: SharedAccountContext, context: Accoun getRootControllerImpl = { [weak controller] in return controller?.view.window?.rootViewController } + getNavigationControllerImpl = { [weak controller] in + return controller?.navigationController as? NavigationController + } return controller } diff --git a/submodules/DeviceAccess/Sources/DeviceAccess.swift b/submodules/DeviceAccess/Sources/DeviceAccess.swift index d598849965..9c67ad7c0c 100644 --- a/submodules/DeviceAccess/Sources/DeviceAccess.swift +++ b/submodules/DeviceAccess/Sources/DeviceAccess.swift @@ -87,6 +87,10 @@ public final class DeviceAccess { return AVAudioSession.sharedInstance().recordPermission == .granted } + public static func isCameraAccessAuthorized() -> Bool { + return PGCamera.cameraAuthorizationStatus() == PGCameraAuthorizationStatusAuthorized + } + public static func authorizationStatus(applicationInForeground: Signal? = nil, siriAuthorization: (() -> AccessType)? = nil, subject: DeviceAccessSubject) -> Signal { switch subject { case .notifications: @@ -250,27 +254,31 @@ public final class DeviceAccess { } } - 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 }) { + public static func authorizeAccess(to subject: DeviceAccessSubject, onlyCheck: Bool = false, 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 let .camera(cameraSubject): let status = PGCamera.cameraAuthorizationStatus() if status == PGCameraAuthorizationStatusNotDetermined { - AVCaptureDevice.requestAccess(for: AVMediaType.video) { response in - Queue.mainQueue().async { - completion(response) - if !response, let presentationData = presentationData { - let text: String - switch cameraSubject { - case .video: - text = presentationData.strings.AccessDenied_Camera - case .videoCall: - text = presentationData.strings.AccessDenied_VideoCallCamera + if !onlyCheck { + AVCaptureDevice.requestAccess(for: AVMediaType.video) { response in + Queue.mainQueue().async { + completion(response) + if !response, let presentationData = presentationData { + let text: String + switch cameraSubject { + case .video: + text = presentationData.strings.AccessDenied_Camera + case .videoCall: + text = presentationData.strings.AccessDenied_VideoCallCamera + } + 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) } - 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 { + completion(true) } } else if status == PGCameraAuthorizationStatusRestricted || status == PGCameraAuthorizationStatusDenied, let presentationData = presentationData { let text: String @@ -353,7 +361,7 @@ public final class DeviceAccess { switch status { case .restricted, .denied, .notDetermined: value = false - case .authorized: + case .authorized, .limited: value = true @unknown default: fatalError() @@ -408,8 +416,6 @@ public final class DeviceAccess { locationManager?.requestAlwaysAuthorization(completion: { status in completion(status == .authorizedAlways) }) - default: - break } @unknown default: fatalError() diff --git a/submodules/Display/Source/AlertController.swift b/submodules/Display/Source/AlertController.swift index 7179032570..f57b2359ec 100644 --- a/submodules/Display/Source/AlertController.swift +++ b/submodules/Display/Source/AlertController.swift @@ -86,11 +86,15 @@ open class AlertController: ViewController, StandalonePresentableController { private let contentNode: AlertContentNode private let allowInputInset: Bool + private weak var existingAlertController: AlertController? + + public var willDismiss: (() -> Void)? public var dismissed: (() -> Void)? - public init(theme: AlertControllerTheme, contentNode: AlertContentNode, allowInputInset: Bool = true) { + public init(theme: AlertControllerTheme, contentNode: AlertContentNode, existingAlertController: AlertController? = nil, allowInputInset: Bool = true) { self.theme = theme self.contentNode = contentNode + self.existingAlertController = existingAlertController self.allowInputInset = allowInputInset super.init(navigationBarPresentationData: nil) @@ -108,8 +112,11 @@ open class AlertController: ViewController, StandalonePresentableController { self.displayNode = AlertControllerNode(contentNode: self.contentNode, theme: self.theme, allowInputInset: self.allowInputInset) self.displayNodeDidLoad() + self.controllerNode.existingAlertControllerNode = self.existingAlertController?.controllerNode + self.controllerNode.dismiss = { [weak self] in if let strongSelf = self, strongSelf.contentNode.dismissOnOutsideTap { + strongSelf.willDismiss?() strongSelf.controllerNode.animateOut { self?.dismiss() } @@ -120,6 +127,9 @@ open class AlertController: ViewController, StandalonePresentableController { override open func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) + self.existingAlertController?.dismiss(completion: nil) + self.existingAlertController = nil + self.controllerNode.animateIn() } diff --git a/submodules/Display/Source/AlertControllerNode.swift b/submodules/Display/Source/AlertControllerNode.swift index e532753a68..e2973469aa 100644 --- a/submodules/Display/Source/AlertControllerNode.swift +++ b/submodules/Display/Source/AlertControllerNode.swift @@ -3,6 +3,8 @@ import UIKit import AsyncDisplayKit final class AlertControllerNode: ASDisplayNode { + var existingAlertControllerNode: AlertControllerNode? + private let centerDimView: UIImageView private let topDimView: UIView private let bottomDimView: UIView @@ -90,18 +92,35 @@ final class AlertControllerNode: ASDisplayNode { } func animateIn() { - self.centerDimView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) - self.topDimView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) - self.bottomDimView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) - self.leftDimView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) - self.rightDimView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) - self.containerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25, completion: { [weak self] finished in - if finished { - self?.centerDimView.backgroundColor = nil - self?.centerDimView.image = generateStretchableFilledCircleImage(radius: 16.0, color: nil, backgroundColor: UIColor(white: 0.0, alpha: 0.5)) + if let previousNode = self.existingAlertControllerNode { + let transition = ContainedViewLayoutTransition.animated(duration: 0.3, curve: .spring) + + previousNode.position = previousNode.position.offsetBy(dx: -previousNode.frame.width, dy: 0.0) + self.addSubnode(previousNode) + + let position = self.position + self.position = position.offsetBy(dx: self.frame.width, dy: 0.0) + transition.animateView { + self.position = position + } completion: { _ in + previousNode.removeFromSupernode() } - }) - self.containerNode.layer.animateSpring(from: 0.8 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.5, initialVelocity: 0.0, removeOnCompletion: true, additive: false, completion: nil) + + self.existingAlertControllerNode = nil + } else { + self.centerDimView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + self.topDimView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + self.bottomDimView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + self.leftDimView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + self.rightDimView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + self.containerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25, completion: { [weak self] finished in + if finished { + self?.centerDimView.backgroundColor = nil + self?.centerDimView.image = generateStretchableFilledCircleImage(radius: 16.0, color: nil, backgroundColor: UIColor(white: 0.0, alpha: 0.5)) + } + }) + self.containerNode.layer.animateSpring(from: 0.8 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.5, initialVelocity: 0.0, removeOnCompletion: true, additive: false, completion: nil) + } } func animateOut(completion: @escaping () -> Void) { diff --git a/submodules/Display/Source/CAAnimationUtils.swift b/submodules/Display/Source/CAAnimationUtils.swift index 4aecee5b73..9c51d016d1 100644 --- a/submodules/Display/Source/CAAnimationUtils.swift +++ b/submodules/Display/Source/CAAnimationUtils.swift @@ -31,6 +31,7 @@ import UIKitRuntimeUtils private let completionKey = "CAAnimationUtils_completion" public let kCAMediaTimingFunctionSpring = "CAAnimationUtilsSpringCurve" +public let kCAMediaTimingFunctionCustomSpringPrefix = "CAAnimationUtilsSpringCustomCurve" public extension CAAnimation { var completion: ((Bool) -> Void)? { @@ -52,7 +53,38 @@ public extension CAAnimation { public extension CALayer { func makeAnimation(from: AnyObject, to: AnyObject, keyPath: String, timingFunction: String, duration: Double, delay: Double = 0.0, mediaTimingFunction: CAMediaTimingFunction? = nil, removeOnCompletion: Bool = true, additive: Bool = false, completion: ((Bool) -> Void)? = nil) -> CAAnimation { - if timingFunction == kCAMediaTimingFunctionSpring { + if timingFunction.hasPrefix(kCAMediaTimingFunctionCustomSpringPrefix) { + let components = timingFunction.components(separatedBy: "_") + let damping = Float(components[1]) ?? 100.0 + let initialVelocity = Float(components[2]) ?? 0.0 + + let animation = CASpringAnimation(keyPath: keyPath) + animation.fromValue = from + animation.toValue = to + animation.isRemovedOnCompletion = removeOnCompletion + animation.fillMode = .forwards + if let completion = completion { + animation.delegate = CALayerAnimationDelegate(animation: animation, completion: completion) + } + animation.damping = CGFloat(damping) + animation.initialVelocity = CGFloat(initialVelocity) + animation.mass = 5.0 + animation.stiffness = 900.0 + animation.duration = animation.settlingDuration + animation.timingFunction = CAMediaTimingFunction.init(name: .linear) + let k = Float(UIView.animationDurationFactor()) + var speed: Float = 1.0 + if k != 0 && k != 1 { + speed = Float(1.0) / k + } + animation.speed = speed * Float(animation.duration / duration) + animation.isAdditive = additive + if !delay.isZero { + animation.beginTime = self.convertTime(CACurrentMediaTime(), from: nil) + delay * UIView.animationDurationFactor() + animation.fillMode = .both + } + return animation + } else if timingFunction == kCAMediaTimingFunctionSpring { let animation = makeSpringAnimation(keyPath) animation.fromValue = from animation.toValue = to @@ -72,7 +104,7 @@ public extension CALayer { animation.isAdditive = additive if !delay.isZero { - animation.beginTime = CACurrentMediaTime() + delay * UIView.animationDurationFactor() + animation.beginTime = self.convertTime(CACurrentMediaTime(), from: nil) + delay * UIView.animationDurationFactor() animation.fillMode = .both } @@ -102,7 +134,7 @@ public extension CALayer { } if !delay.isZero { - animation.beginTime = CACurrentMediaTime() + delay * UIView.animationDurationFactor() + animation.beginTime = self.convertTime(CACurrentMediaTime(), from: nil) + delay * UIView.animationDurationFactor() animation.fillMode = .both } @@ -229,6 +261,10 @@ public extension CALayer { func animateScale(from: CGFloat, to: CGFloat, duration: Double, delay: Double = 0.0, timingFunction: String = CAMediaTimingFunctionName.easeInEaseOut.rawValue, mediaTimingFunction: CAMediaTimingFunction? = nil, removeOnCompletion: Bool = true, additive: Bool = false, completion: ((Bool) -> Void)? = nil) { self.animate(from: NSNumber(value: Float(from)), to: NSNumber(value: Float(to)), keyPath: "transform.scale", timingFunction: timingFunction, duration: duration, delay: delay, mediaTimingFunction: mediaTimingFunction, removeOnCompletion: removeOnCompletion, additive: additive, completion: completion) } + + func animateScaleX(from: CGFloat, to: CGFloat, duration: Double, delay: Double = 0.0, timingFunction: String = CAMediaTimingFunctionName.easeInEaseOut.rawValue, mediaTimingFunction: CAMediaTimingFunction? = nil, removeOnCompletion: Bool = true, completion: ((Bool) -> Void)? = nil) { + self.animate(from: NSNumber(value: Float(from)), to: NSNumber(value: Float(to)), keyPath: "transform.scale.x", timingFunction: timingFunction, duration: duration, delay: delay, mediaTimingFunction: mediaTimingFunction, removeOnCompletion: removeOnCompletion, completion: completion) + } func animateScaleY(from: CGFloat, to: CGFloat, duration: Double, delay: Double = 0.0, timingFunction: String = CAMediaTimingFunctionName.easeInEaseOut.rawValue, mediaTimingFunction: CAMediaTimingFunction? = nil, removeOnCompletion: Bool = true, completion: ((Bool) -> Void)? = nil) { self.animate(from: NSNumber(value: Float(from)), to: NSNumber(value: Float(to)), keyPath: "transform.scale.y", timingFunction: timingFunction, duration: duration, delay: delay, mediaTimingFunction: mediaTimingFunction, removeOnCompletion: removeOnCompletion, completion: completion) @@ -257,6 +293,26 @@ public extension CALayer { } self.animate(from: NSValue(cgRect: from), to: NSValue(cgRect: to), keyPath: "bounds", timingFunction: timingFunction, duration: duration, delay: delay, mediaTimingFunction: mediaTimingFunction, removeOnCompletion: removeOnCompletion, additive: additive, completion: completion) } + + func animateWidth(from: CGFloat, to: CGFloat, duration: Double, delay: Double = 0.0, timingFunction: String, mediaTimingFunction: CAMediaTimingFunction? = nil, removeOnCompletion: Bool = true, additive: Bool = false, force: Bool = false, completion: ((Bool) -> Void)? = nil) { + if from == to && !force { + if let completion = completion { + completion(true) + } + return + } + self.animate(from: from as NSNumber, to: to as NSNumber, keyPath: "bounds.size.width", timingFunction: timingFunction, duration: duration, delay: delay, mediaTimingFunction: mediaTimingFunction, removeOnCompletion: removeOnCompletion, additive: additive, completion: completion) + } + + func animateHeight(from: CGFloat, to: CGFloat, duration: Double, delay: Double = 0.0, timingFunction: String, mediaTimingFunction: CAMediaTimingFunction? = nil, removeOnCompletion: Bool = true, additive: Bool = false, force: Bool = false, completion: ((Bool) -> Void)? = nil) { + if from == to && !force { + if let completion = completion { + completion(true) + } + return + } + self.animate(from: from as NSNumber, to: to as NSNumber, keyPath: "bounds.size.height", timingFunction: timingFunction, duration: duration, delay: delay, mediaTimingFunction: mediaTimingFunction, removeOnCompletion: removeOnCompletion, additive: additive, completion: completion) + } func animateBoundsOriginXAdditive(from: CGFloat, to: CGFloat, duration: Double, timingFunction: String = CAMediaTimingFunctionName.easeInEaseOut.rawValue, mediaTimingFunction: CAMediaTimingFunction? = nil, removeOnCompletion: Bool = true, completion: ((Bool) -> Void)? = nil) { self.animate(from: from as NSNumber, to: to as NSNumber, keyPath: "bounds.origin.x", timingFunction: timingFunction, duration: duration, mediaTimingFunction: mediaTimingFunction, removeOnCompletion: removeOnCompletion, additive: true, completion: completion) @@ -278,7 +334,7 @@ public extension CALayer { self.animateKeyframes(values: values.map { NSValue(cgPoint: $0) }, duration: duration, keyPath: "position") } - func animateFrame(from: CGRect, to: CGRect, duration: Double, delay: Double = 0.0, timingFunction: String, mediaTimingFunction: CAMediaTimingFunction? = nil, removeOnCompletion: Bool = true, additive: Bool = false, force: Bool = false, completion: ((Bool) -> Void)? = nil) { + func animateFrame(from: CGRect, to: CGRect, duration: Double, delay: Double = 0.0, timingFunction: String = CAMediaTimingFunctionName.easeInEaseOut.rawValue, mediaTimingFunction: CAMediaTimingFunction? = nil, removeOnCompletion: Bool = true, additive: Bool = false, force: Bool = false, completion: ((Bool) -> Void)? = nil) { if from == to && !force { if let completion = completion { completion(true) diff --git a/submodules/Display/Source/ContainedViewLayoutTransition.swift b/submodules/Display/Source/ContainedViewLayoutTransition.swift index bfdcc179a1..019a41615b 100644 --- a/submodules/Display/Source/ContainedViewLayoutTransition.swift +++ b/submodules/Display/Source/ContainedViewLayoutTransition.swift @@ -2,10 +2,11 @@ import Foundation import UIKit import AsyncDisplayKit -public enum ContainedViewLayoutTransitionCurve { +public enum ContainedViewLayoutTransitionCurve: Equatable, Hashable { case linear case easeInOut case spring + case customSpring(damping: CGFloat, initialVelocity: CGFloat) case custom(Float, Float, Float, Float) public static var slide: ContainedViewLayoutTransitionCurve { @@ -13,6 +14,23 @@ public enum ContainedViewLayoutTransitionCurve { } } +public extension ContainedViewLayoutTransitionCurve { + func solve(at offset: CGFloat) -> CGFloat { + switch self { + case .linear: + return offset + case .easeInOut: + return listViewAnimationCurveEaseInOut(offset) + case .spring: + return listViewAnimationCurveSystem(offset) + case .customSpring: + return listViewAnimationCurveSystem(offset) + case let .custom(c1x, c1y, c2x, c2y): + return bezierPoint(CGFloat(c1x), CGFloat(c1y), CGFloat(c2x), CGFloat(c2y), offset) + } + } +} + public extension ContainedViewLayoutTransitionCurve { var timingFunction: String { switch self { @@ -22,6 +40,8 @@ public extension ContainedViewLayoutTransitionCurve { return CAMediaTimingFunctionName.easeInEaseOut.rawValue case .spring: return kCAMediaTimingFunctionSpring + case let .customSpring(damping, initialVelocity): + return "\(kCAMediaTimingFunctionCustomSpringPrefix)_\(damping)_\(initialVelocity)" case .custom: return CAMediaTimingFunctionName.easeInEaseOut.rawValue } @@ -35,6 +55,8 @@ public extension ContainedViewLayoutTransitionCurve { return nil case .spring: return nil + case .customSpring: + return nil case let .custom(p1, p2, p3, p4): return CAMediaTimingFunction(controlPoints: p1, p2, p3, p4) } @@ -49,6 +71,8 @@ public extension ContainedViewLayoutTransitionCurve { return [.curveEaseInOut] case .spring: return UIView.AnimationOptions(rawValue: 7 << 16) + case .customSpring: + return UIView.AnimationOptions(rawValue: 7 << 16) case .custom: return [] } @@ -167,6 +191,39 @@ public extension ContainedViewLayoutTransition { } } + func updateFrameAsPositionAndBounds(layer: CALayer, frame: CGRect, force: Bool = false, beginWithCurrentState: Bool = false, completion: ((Bool) -> Void)? = nil) { + if layer.frame.equalTo(frame) && !force { + completion?(true) + } else { + switch self { + case .immediate: + layer.position = frame.center + layer.bounds = CGRect(origin: CGPoint(), size: frame.size) + if let completion = completion { + completion(true) + } + case let .animated(duration, curve): + let previousPosition: CGPoint + let previousBounds: CGRect + if beginWithCurrentState, let presentation = layer.presentation() { + previousPosition = presentation.position + previousBounds = presentation.bounds + } else { + previousPosition = layer.position + previousBounds = layer.bounds + } + layer.position = frame.center + layer.bounds = CGRect(origin: CGPoint(), size: frame.size) + layer.animateFrame(from: + CGRect(origin: CGPoint(x: previousPosition.x - previousBounds.width / 2.0, y: previousPosition.y - previousBounds.height / 2.0), size: previousBounds.size), to: frame, duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, force: force, completion: { result in + if let completion = completion { + completion(result) + } + }) + } + } + } + func updateFrameAdditive(node: ASDisplayNode, frame: CGRect, force: Bool = false, completion: ((Bool) -> Void)? = nil) { if node.frame.equalTo(frame) && !force { completion?(true) @@ -276,8 +333,8 @@ public extension ContainedViewLayoutTransition { } } - func updatePosition(layer: CALayer, position: CGPoint, completion: ((Bool) -> Void)? = nil) { - if layer.position.equalTo(position) { + func updatePosition(layer: CALayer, position: CGPoint, force: Bool = false, completion: ((Bool) -> Void)? = nil) { + if layer.position.equalTo(position) && !force { completion?(true) } else { switch self { @@ -346,6 +403,21 @@ public extension ContainedViewLayoutTransition { }) } } + + func animateFrame(layer: CALayer, from frame: CGRect, to toFrame: CGRect? = nil, removeOnCompletion: Bool = true, additive: Bool = false, completion: ((Bool) -> Void)? = nil) { + switch self { + case .immediate: + if let completion = completion { + completion(true) + } + case let .animated(duration, curve): + layer.animateFrame(from: frame, to: toFrame ?? layer.frame, duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, removeOnCompletion: removeOnCompletion, additive: additive, completion: { result in + if let completion = completion { + completion(result) + } + }) + } + } func animateBounds(layer: CALayer, from bounds: CGRect, removeOnCompletion: Bool = true, completion: ((Bool) -> Void)? = nil) { switch self { @@ -361,6 +433,36 @@ public extension ContainedViewLayoutTransition { }) } } + + func animateWidthAdditive(layer: CALayer, value: CGFloat, removeOnCompletion: Bool = true, completion: ((Bool) -> Void)? = nil) { + switch self { + case .immediate: + if let completion = completion { + completion(true) + } + case let .animated(duration, curve): + layer.animateWidth(from: value, to: 0.0, duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, removeOnCompletion: removeOnCompletion, additive: true, completion: { result in + if let completion = completion { + completion(result) + } + }) + } + } + + func animateHeightAdditive(layer: CALayer, value: CGFloat, removeOnCompletion: Bool = true, completion: ((Bool) -> Void)? = nil) { + switch self { + case .immediate: + if let completion = completion { + completion(true) + } + case let .animated(duration, curve): + layer.animateHeight(from: value, to: 0.0, duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, removeOnCompletion: removeOnCompletion, additive: true, completion: { result in + if let completion = completion { + completion(result) + } + }) + } + } func animateOffsetAdditive(node: ASDisplayNode, offset: CGFloat) { switch self { @@ -381,6 +483,17 @@ public extension ContainedViewLayoutTransition { }) } } + + func animateHorizontalOffsetAdditive(layer: CALayer, offset: CGFloat, completion: (() -> Void)? = nil) { + switch self { + case .immediate: + break + case let .animated(duration, curve): + layer.animateBoundsOriginXAdditive(from: offset, to: 0.0, duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, completion: { _ in + completion?() + }) + } + } func animateOffsetAdditive(layer: CALayer, offset: CGFloat, completion: (() -> Void)? = nil) { switch self { @@ -422,18 +535,52 @@ public extension ContainedViewLayoutTransition { } } - func animatePositionAdditive(layer: CALayer, offset: CGPoint, to toOffset: CGPoint = CGPoint(), removeOnCompletion: Bool = true, completion: (() -> Void)? = nil) { + func animatePositionAdditive(layer: CALayer, offset: CGPoint, to toOffset: CGPoint = CGPoint(), removeOnCompletion: Bool = true, completion: ((Bool) -> Void)? = nil) { switch self { case .immediate: - completion?() + completion?(true) 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?() + layer.animatePosition(from: offset, to: toOffset, duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, removeOnCompletion: removeOnCompletion, additive: true, completion: { result in + completion?(result) }) } } + + func animateContentsRectPositionAdditive(layer: CALayer, offset: CGPoint, removeOnCompletion: Bool = true, completion: ((Bool) -> Void)? = nil) { + switch self { + case .immediate: + completion?(true) + case let .animated(duration, curve): + layer.animate(from: NSValue(cgPoint: offset), to: NSValue(cgPoint: CGPoint()), keyPath: "contentsRect.origin", timingFunction: curve.timingFunction, duration: duration, delay: 0.0, mediaTimingFunction: curve.mediaTimingFunction, removeOnCompletion: removeOnCompletion, additive: true, completion: completion) + } + } - func updateFrame(view: UIView, frame: CGRect, force: Bool = false, completion: ((Bool) -> Void)? = nil) { + func updateFrame(view: UIView, frame: CGRect, force: Bool = false, beginWithCurrentState: Bool = false, delay: Double = 0.0, completion: ((Bool) -> Void)? = nil) { + if frame.origin.x.isNaN { + return + } + if frame.origin.y.isNaN { + return + } + if frame.size.width.isNaN { + return + } + if frame.size.width < 0.0 { + return + } + if frame.size.height.isNaN { + return + } + if frame.size.height < 0.0 { + return + } + if !ASIsCGRectValidForLayout(CGRect(origin: CGPoint(), size: frame.size)) { + return + } + if !ASIsCGPositionValidForLayout(frame.origin) { + return + } + if view.frame.equalTo(frame) && !force { completion?(true) } else { @@ -444,9 +591,14 @@ public extension ContainedViewLayoutTransition { completion(true) } case let .animated(duration, curve): - let previousFrame = view.frame + let previousFrame: CGRect + if beginWithCurrentState, let presentation = view.layer.presentation() { + previousFrame = presentation.frame + } else { + previousFrame = view.frame + } view.frame = frame - view.layer.animateFrame(from: previousFrame, to: frame, duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, force: force, completion: { result in + view.layer.animateFrame(from: previousFrame, to: frame, duration: duration, delay: delay, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, force: force, completion: { result in if let completion = completion { completion(result) } @@ -454,7 +606,7 @@ public extension ContainedViewLayoutTransition { } } } - + func updateFrame(layer: CALayer, frame: CGRect, completion: ((Bool) -> Void)? = nil) { if layer.frame.equalTo(frame) { completion?(true) @@ -645,6 +797,52 @@ public extension ContainedViewLayoutTransition { }) } } + + func animateTransformScale(node: ASDisplayNode, from fromScale: CGPoint, completion: ((Bool) -> Void)? = nil) { + let t = node.layer.transform + + switch self { + case .immediate: + if let completion = completion { + completion(true) + } + case let .animated(duration, curve): + let calculatedFrom: CGPoint + let calculatedTo: CGPoint + + calculatedFrom = fromScale + calculatedTo = CGPoint(x: 1.0, y: 1.0) + + node.layer.animateScaleX(from: calculatedFrom.x, to: calculatedTo.x, duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, completion: { result in + if let completion = completion { + completion(result) + } + }) + node.layer.animateScaleY(from: calculatedFrom.y, to: calculatedTo.y, duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction) + } + } + + func animateTransformScale(layer: CALayer, from fromScale: CGPoint, completion: ((Bool) -> Void)? = nil) { + switch self { + case .immediate: + if let completion = completion { + completion(true) + } + case let .animated(duration, curve): + let calculatedFrom: CGPoint + let calculatedTo: CGPoint + + calculatedFrom = fromScale + calculatedTo = CGPoint(x: 1.0, y: 1.0) + + layer.animateScaleX(from: calculatedFrom.x, to: calculatedTo.x, duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, completion: { result in + if let completion = completion { + completion(result) + } + }) + layer.animateScaleY(from: calculatedFrom.y, to: calculatedTo.y, duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction) + } + } func animateTransformScale(view: UIView, from fromScale: CGFloat, completion: ((Bool) -> Void)? = nil) { let t = view.layer.transform @@ -890,7 +1088,12 @@ public extension ContainedViewLayoutTransition { completion?(true) return } - let t = node.layer.transform + + self.updateTransformScale(layer: node.layer, scale: scale, completion: completion) + } + + func updateTransformScale(layer: CALayer, scale: CGPoint, completion: ((Bool) -> Void)? = nil) { + let t = layer.transform let currentScaleX = sqrt((t.m11 * t.m11) + (t.m12 * t.m12) + (t.m13 * t.m13)) var currentScaleY = sqrt((t.m21 * t.m21) + (t.m22 * t.m22) + (t.m23 * t.m23)) if t.m22 < 0.0 { @@ -902,16 +1105,16 @@ public extension ContainedViewLayoutTransition { } return } - + switch self { case .immediate: - node.layer.transform = CATransform3DMakeScale(scale.x, scale.y, 1.0) + layer.transform = CATransform3DMakeScale(scale.x, scale.y, 1.0) if let completion = completion { completion(true) } case let .animated(duration, curve): - node.layer.transform = CATransform3DMakeScale(scale.x, scale.y, 1.0) - node.layer.animate(from: NSValue(caTransform3D: t), to: NSValue(caTransform3D: node.layer.transform), keyPath: "transform", timingFunction: curve.timingFunction, duration: duration, delay: 0.0, mediaTimingFunction: curve.mediaTimingFunction, removeOnCompletion: true, additive: false, completion: { + layer.transform = CATransform3DMakeScale(scale.x, scale.y, 1.0) + layer.animate(from: NSValue(caTransform3D: t), to: NSValue(caTransform3D: layer.transform), keyPath: "transform", timingFunction: curve.timingFunction, duration: duration, delay: 0.0, mediaTimingFunction: curve.mediaTimingFunction, removeOnCompletion: true, additive: false, completion: { result in if let completion = completion { completion(result) @@ -1048,7 +1251,119 @@ public extension ContainedViewLayoutTransition { } } -#if os(iOS) +public struct CombinedTransition { + public var horizontal: ContainedViewLayoutTransition + public var vertical: ContainedViewLayoutTransition + + public var isAnimated: Bool { + return self.horizontal.isAnimated || self.vertical.isAnimated + } + + public init(horizontal: ContainedViewLayoutTransition, vertical: ContainedViewLayoutTransition) { + self.horizontal = horizontal + self.vertical = vertical + } + + public func animateFrame(layer: CALayer, from fromFrame: CGRect, completion: ((Bool) -> Void)? = nil) { + //self.horizontal.animateFrame(layer: layer, from: fromFrame, completion: completion) + //return; + + let toFrame = layer.frame + + enum Keys: CaseIterable { + case positionX, positionY + case sizeWidth, sizeHeight + } + var remainingKeys = Keys.allCases + var completedValue = true + let completeKey: (Keys, Bool) -> Void = { key, completed in + remainingKeys.removeAll(where: { $0 == key }) + if !completed { + completedValue = false + } + if remainingKeys.isEmpty { + completion?(completedValue) + } + } + + self.horizontal.animatePositionAdditive(layer: layer, offset: CGPoint(x: fromFrame.midX - toFrame.midX, y: 0.0), completion: { result in + completeKey(.positionX, result) + }) + self.vertical.animatePositionAdditive(layer: layer, offset: CGPoint(x: 0.0, y: fromFrame.midY - toFrame.midY), completion: { result in + completeKey(.positionY, result) + }) + + self.horizontal.animateWidthAdditive(layer: layer, value: fromFrame.width - toFrame.width, completion: { result in + completeKey(.sizeWidth, result) + }) + self.vertical.animateHeightAdditive(layer: layer, value: fromFrame.height - toFrame.height, completion: { result in + completeKey(.sizeHeight, result) + }) + } + + public func updateFrame(layer: CALayer, frame: CGRect, completion: ((Bool) -> Void)? = nil) { + let fromFrame = layer.frame + layer.frame = frame + self.animateFrame(layer: layer, from: fromFrame, completion: completion) + } + + public func updateFrame(node: ASDisplayNode, frame: CGRect, completion: ((Bool) -> Void)? = nil) { + let fromFrame = node.frame + node.frame = frame + self.animateFrame(layer: node.layer, from: fromFrame, completion: completion) + } + + public func updatePosition(layer: CALayer, position: CGPoint, completion: ((Bool) -> Void)? = nil) { + let fromPosition = layer.position + layer.position = position + + enum Keys: CaseIterable { + case positionX, positionY + } + var remainingKeys = Keys.allCases + var completedValue = true + let completeKey: (Keys, Bool) -> Void = { key, completed in + remainingKeys.removeAll(where: { $0 == key }) + if !completed { + completedValue = false + } + if remainingKeys.isEmpty { + completion?(completedValue) + } + } + + self.horizontal.animatePositionAdditive(layer: layer, offset: CGPoint(x: fromPosition.x - position.x, y: 0.0), completion: { result in + completeKey(.positionX, result) + }) + self.vertical.animatePositionAdditive(layer: layer, offset: CGPoint(x: 0.0, y: fromPosition.y - position.y), completion: { result in + completeKey(.positionY, result) + }) + } + + public func animatePositionAdditive(layer: CALayer, offset: CGPoint, to toOffset: CGPoint = CGPoint(), removeOnCompletion: Bool = true, completion: ((Bool) -> Void)? = nil) { + enum Keys: CaseIterable { + case positionX, positionY + } + var remainingKeys = Keys.allCases + var completedValue = true + let completeKey: (Keys, Bool) -> Void = { key, completed in + remainingKeys.removeAll(where: { $0 == key }) + if !completed { + completedValue = false + } + if remainingKeys.isEmpty { + completion?(completedValue) + } + } + + self.horizontal.animatePositionAdditive(layer: layer, offset: CGPoint(x: offset.x, y: 0.0), to: CGPoint(x: toOffset.x, y: 0.0), completion: { result in + completeKey(.positionX, result) + }) + self.vertical.animatePositionAdditive(layer: layer, offset: CGPoint(x: 0.0, y: offset.y), to: CGPoint(x: 0.0, y: toOffset.y), completion: { result in + completeKey(.positionY, result) + }) + } +} public extension ContainedViewLayoutTransition { func animateView(_ f: @escaping () -> Void, completion: ((Bool) -> Void)? = nil) { @@ -1063,5 +1378,3 @@ public extension ContainedViewLayoutTransition { } } } - -#endif diff --git a/submodules/Display/Source/ContextContentSourceNode.swift b/submodules/Display/Source/ContextContentSourceNode.swift index 9553404a3e..5eb8d82223 100644 --- a/submodules/Display/Source/ContextContentSourceNode.swift +++ b/submodules/Display/Source/ContextContentSourceNode.swift @@ -11,10 +11,11 @@ public final class ContextExtractedContentContainingNode: ASDisplayNode { public var willUpdateIsExtractedToContextPreview: ((Bool, ContainedViewLayoutTransition) -> Void)? public var isExtractedToContextPreviewUpdated: ((Bool) -> Void)? public var updateAbsoluteRect: ((CGRect, CGSize) -> Void)? - public var applyAbsoluteOffset: ((CGFloat, ContainedViewLayoutTransitionCurve, Double) -> Void)? + public var applyAbsoluteOffset: ((CGPoint, ContainedViewLayoutTransitionCurve, Double) -> Void)? public var applyAbsoluteOffsetSpring: ((CGFloat, Double, CGFloat) -> Void)? public var layoutUpdated: ((CGSize) -> Void)? public var updateDistractionFreeMode: ((Bool) -> Void)? + public var requestDismiss: (() -> Void)? public override init() { self.contentNode = ContextExtractedContentNode() @@ -23,9 +24,27 @@ public final class ContextExtractedContentContainingNode: ASDisplayNode { self.addSubnode(self.contentNode) } + + public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if self.contentNode.supernode === self { + return self.contentNode.hitTest(self.view.convert(point, to: self.contentNode.view), with: event) + } else { + return nil + } + } } public final class ContextExtractedContentNode: ASDisplayNode { + public var customHitTest: ((CGPoint) -> UIView?)? + + public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + let result = self.view.hitTest(point, with: event) + if result === self.view { + return nil + } else { + return result + } + } } public final class ContextControllerContentNode: ASDisplayNode { diff --git a/submodules/Display/Source/ContextGesture.swift b/submodules/Display/Source/ContextGesture.swift index 79a767a1a6..542cfeba7f 100644 --- a/submodules/Display/Source/ContextGesture.swift +++ b/submodules/Display/Source/ContextGesture.swift @@ -41,6 +41,8 @@ private func cancelOtherGestures(gesture: ContextGesture, view: UIView) { for recognizer in gestureRecognizers { if let recognizer = recognizer as? ContextGesture, recognizer !== gesture { recognizer.cancel() + } else if let recognizer = recognizer as? ListViewTapGestureRecognizer { + recognizer.cancel() } } } diff --git a/submodules/Display/Source/Font.swift b/submodules/Display/Source/Font.swift index 46787501bf..873a367e5d 100644 --- a/submodules/Display/Source/Font.swift +++ b/submodules/Display/Source/Font.swift @@ -20,9 +20,8 @@ public struct Font { self.rawValue = 0 } - public static let bold = Traits(rawValue: 1 << 0) - public static let italic = Traits(rawValue: 1 << 1) - public static let monospacedNumbers = Traits(rawValue: 1 << 2) + public static let italic = Traits(rawValue: 1 << 0) + public static let monospacedNumbers = Traits(rawValue: 1 << 1) } public enum Weight { @@ -31,15 +30,42 @@ public struct Font { case medium case semibold case bold + + var isBold: Bool { + switch self { + case .medium, .semibold, .bold: + return true + default: + return false + } + } + + var weight: UIFont.Weight { + switch self { + case .light: + return .light + case .medium: + return .medium + case .semibold: + return .semibold + case .bold: + return .bold + default: + return .regular + } + } } public static func with(size: CGFloat, design: Design = .regular, weight: Weight = .regular, traits: Traits = []) -> UIFont { if #available(iOS 13.0, *) { - let descriptor = UIFont.systemFont(ofSize: size).fontDescriptor - var symbolicTraits = descriptor.symbolicTraits - if traits.contains(.bold) { - symbolicTraits.insert(.traitBold) + let descriptor: UIFontDescriptor + if #available(iOS 14.0, *) { + descriptor = UIFont.systemFont(ofSize: size).fontDescriptor + } else { + descriptor = UIFont.systemFont(ofSize: size, weight: weight.weight).fontDescriptor } + + var symbolicTraits = descriptor.symbolicTraits if traits.contains(.italic) { symbolicTraits.insert(.traitItalic) } @@ -63,23 +89,12 @@ public struct Font { default: updatedDescriptor = updatedDescriptor?.withDesign(.default) } - if weight != .regular { - let fontWeight: UIFont.Weight - switch weight { - case .light: - fontWeight = .light - case .medium: - fontWeight = .medium - case .semibold: - fontWeight = .semibold - case .bold: - fontWeight = .bold - default: - fontWeight = .regular + if #available(iOS 14.0, *) { + if weight != .regular { + updatedDescriptor = updatedDescriptor?.addingAttributes([ + UIFontDescriptor.AttributeName.traits: [UIFontDescriptor.TraitKey.weight: weight.weight] + ]) } - updatedDescriptor = updatedDescriptor?.addingAttributes([ - UIFontDescriptor.AttributeName.traits: [UIFontDescriptor.TraitKey.weight: fontWeight] - ]) } if let updatedDescriptor = updatedDescriptor { @@ -90,27 +105,19 @@ public struct Font { } else { switch design { case .regular: - if traits.contains(.bold) && traits.contains(.italic) { - if let descriptor = UIFont.systemFont(ofSize: size).fontDescriptor.withSymbolicTraits([.traitBold, .traitItalic]) { + if traits.contains(.italic) { + if let descriptor = UIFont.systemFont(ofSize: size, weight: weight.weight).fontDescriptor.withSymbolicTraits([.traitItalic]) { return UIFont(descriptor: descriptor, size: size) } else { return UIFont.italicSystemFont(ofSize: size) } - } else if traits.contains(.bold) { - if #available(iOS 8.2, *) { - return UIFont.boldSystemFont(ofSize: size) - } else { - return CTFontCreateWithName("HelveticaNeue-Bold" as CFString, size, nil) - } - } else if traits.contains(.italic) { - return UIFont.italicSystemFont(ofSize: size) } else { - return UIFont.systemFont(ofSize: size) + return UIFont.systemFont(ofSize: size, weight: weight.weight) } case .serif: - if traits.contains(.bold) && traits.contains(.italic) { + if weight.isBold && traits.contains(.italic) { return UIFont(name: "Georgia-BoldItalic", size: size - 1.0) ?? UIFont.systemFont(ofSize: size) - } else if traits.contains(.bold) { + } else if weight.isBold { return UIFont(name: "Georgia-Bold", size: size - 1.0) ?? UIFont.systemFont(ofSize: size) } else if traits.contains(.italic) { return UIFont(name: "Georgia-Italic", size: size - 1.0) ?? UIFont.systemFont(ofSize: size) @@ -118,9 +125,9 @@ public struct Font { return UIFont(name: "Georgia", size: size - 1.0) ?? UIFont.systemFont(ofSize: size) } case .monospace: - if traits.contains(.bold) && traits.contains(.italic) { + if weight.isBold && traits.contains(.italic) { return UIFont(name: "Menlo-BoldItalic", size: size - 1.0) ?? UIFont.systemFont(ofSize: size) - } else if traits.contains(.bold) { + } else if weight.isBold { return UIFont(name: "Menlo-Bold", size: size - 1.0) ?? UIFont.systemFont(ofSize: size) } else if traits.contains(.italic) { return UIFont(name: "Menlo-Italic", size: size - 1.0) ?? UIFont.systemFont(ofSize: size) diff --git a/submodules/Display/Source/GenerateImage.swift b/submodules/Display/Source/GenerateImage.swift index e2d8433351..0606e987ef 100644 --- a/submodules/Display/Source/GenerateImage.swift +++ b/submodules/Display/Source/GenerateImage.swift @@ -1,6 +1,7 @@ import Foundation import UIKit import Accelerate +import AsyncDisplayKit public let deviceColorSpace: CGColorSpace = { if #available(iOSApplicationExtension 9.3, iOS 9.3, *) { @@ -19,38 +20,20 @@ private let grayscaleColorSpace = CGColorSpaceCreateDeviceGray() let deviceScale = UIScreen.main.scale public func generateImagePixel(_ size: CGSize, scale: CGFloat, pixelGenerator: (CGSize, UnsafeMutablePointer, Int) -> Void) -> UIImage? { - let scaledSize = CGSize(width: size.width * scale, height: size.height * scale) - let bytesPerRow = (4 * Int(scaledSize.width) + 15) & (~15) - let length = bytesPerRow * Int(scaledSize.height) - let bytes = malloc(length)!.assumingMemoryBound(to: UInt8.self) - guard let provider = CGDataProvider(dataInfo: bytes, data: bytes, size: length, releaseData: { bytes, _, _ in - free(bytes) - }) - else { - return nil - } - - pixelGenerator(scaledSize, bytes, bytesPerRow) - - let bitmapInfo = CGBitmapInfo(rawValue: CGBitmapInfo.byteOrder32Little.rawValue | CGImageAlphaInfo.premultipliedFirst.rawValue) - - guard let image = CGImage(width: Int(scaledSize.width), height: Int(scaledSize.height), bitsPerComponent: 8, bitsPerPixel: 32, bytesPerRow: bytesPerRow, space: deviceColorSpace, bitmapInfo: bitmapInfo, provider: provider, decode: nil, shouldInterpolate: false, intent: .defaultIntent) - else { - return nil - } - - return UIImage(cgImage: image, scale: scale, orientation: .up) + let context = DrawingContext(size: size, scale: scale, opaque: false, clear: false) + pixelGenerator(CGSize(width: size.width * scale, height: size.height * scale), context.bytes.assumingMemoryBound(to: UInt8.self), context.bytesPerRow) + return context.generateImage() } private func withImageBytes(image: UIImage, _ f: (UnsafePointer, Int, Int, Int) -> Void) { let selectedScale = image.scale let scaledSize = CGSize(width: image.size.width * selectedScale, height: image.size.height * selectedScale) - let bytesPerRow = (4 * Int(scaledSize.width) + 15) & (~15) + let bytesPerRow = DeviceGraphicsContextSettings.shared.bytesPerRow(forWidth: Int(scaledSize.width)) let length = bytesPerRow * Int(scaledSize.height) let bytes = malloc(length)!.assumingMemoryBound(to: UInt8.self) memset(bytes, 0, length) - let bitmapInfo = CGBitmapInfo(rawValue: CGBitmapInfo.byteOrder32Little.rawValue | CGImageAlphaInfo.premultipliedFirst.rawValue) + let bitmapInfo = DeviceGraphicsContextSettings.shared.transparentBitmapInfo guard let context = CGContext(data: bytes, width: Int(scaledSize.width), height: Int(scaledSize.height), bitsPerComponent: 8, bytesPerRow: bytesPerRow, space: deviceColorSpace, bitmapInfo: bitmapInfo.rawValue) else { return @@ -65,7 +48,7 @@ private func withImageBytes(image: UIImage, _ f: (UnsafePointer, Int, Int public func generateGrayscaleAlphaMaskImage(image: UIImage) -> UIImage? { let selectedScale = image.scale let scaledSize = CGSize(width: image.size.width * selectedScale, height: image.size.height * selectedScale) - let bytesPerRow = (1 * Int(scaledSize.width) + 15) & (~15) + let bytesPerRow = (1 * Int(scaledSize.width) + 31) & (~31) let length = bytesPerRow * Int(scaledSize.height) let bytes = malloc(length)!.assumingMemoryBound(to: UInt8.self) memset(bytes, 0, length) @@ -111,69 +94,25 @@ public func generateGrayscaleAlphaMaskImage(image: UIImage) -> UIImage? { } public func generateImage(_ size: CGSize, contextGenerator: (CGSize, CGContext) -> Void, opaque: Bool = false, scale: CGFloat? = nil) -> UIImage? { - let selectedScale = scale ?? deviceScale - let scaledSize = CGSize(width: size.width * selectedScale, height: size.height * selectedScale) - let bytesPerRow = (4 * Int(scaledSize.width) + 15) & (~15) - let length = bytesPerRow * Int(scaledSize.height) - let bytes = malloc(length)!.assumingMemoryBound(to: Int8.self) - - guard let provider = CGDataProvider(dataInfo: bytes, data: bytes, size: length, releaseData: { bytes, _, _ in - free(bytes) - }) - else { + if size.width.isZero || size.height.isZero { return nil } - - let bitmapInfo = CGBitmapInfo(rawValue: CGBitmapInfo.byteOrder32Little.rawValue | (opaque ? CGImageAlphaInfo.noneSkipFirst.rawValue : CGImageAlphaInfo.premultipliedFirst.rawValue)) - - guard let context = CGContext(data: bytes, width: Int(scaledSize.width), height: Int(scaledSize.height), bitsPerComponent: 8, bytesPerRow: bytesPerRow, space: deviceColorSpace, bitmapInfo: bitmapInfo.rawValue) else { - return nil + let context = DrawingContext(size: size, scale: scale ?? 0.0, opaque: opaque, clear: false) + context.withFlippedContext { c in + contextGenerator(context.size, c) } - - context.scaleBy(x: selectedScale, y: selectedScale) - - contextGenerator(size, context) - - guard let image = CGImage(width: Int(scaledSize.width), height: Int(scaledSize.height), bitsPerComponent: 8, bitsPerPixel: 32, bytesPerRow: bytesPerRow, space: deviceColorSpace, bitmapInfo: bitmapInfo, provider: provider, decode: nil, shouldInterpolate: false, intent: .defaultIntent) - else { - return nil - } - - return UIImage(cgImage: image, scale: selectedScale, orientation: .up) + return context.generateImage() } public func generateImage(_ size: CGSize, opaque: Bool = false, scale: CGFloat? = nil, rotatedContext: (CGSize, CGContext) -> Void) -> UIImage? { - let selectedScale = scale ?? deviceScale - let scaledSize = CGSize(width: size.width * selectedScale, height: size.height * selectedScale) - let bytesPerRow = (4 * Int(scaledSize.width) + 15) & (~15) - let length = bytesPerRow * Int(scaledSize.height) - let bytes = malloc(length)!.assumingMemoryBound(to: Int8.self) - - guard let provider = CGDataProvider(dataInfo: bytes, data: bytes, size: length, releaseData: { bytes, _, _ in - free(bytes) - }) else { + if size.width.isZero || size.height.isZero { return nil } - - let bitmapInfo = CGBitmapInfo(rawValue: CGBitmapInfo.byteOrder32Little.rawValue | (opaque ? CGImageAlphaInfo.noneSkipFirst.rawValue : CGImageAlphaInfo.premultipliedFirst.rawValue)) - - guard let context = CGContext(data: bytes, width: Int(scaledSize.width), height: Int(scaledSize.height), bitsPerComponent: 8, bytesPerRow: bytesPerRow, space: deviceColorSpace, bitmapInfo: bitmapInfo.rawValue) else { - return nil + let context = DrawingContext(size: size, scale: scale ?? 0.0, opaque: opaque, clear: false) + context.withContext { c in + rotatedContext(context.size, c) } - - context.scaleBy(x: selectedScale, y: selectedScale) - 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) - - rotatedContext(size, context) - - guard let image = CGImage(width: Int(scaledSize.width), height: Int(scaledSize.height), bitsPerComponent: 8, bitsPerPixel: 32, bytesPerRow: bytesPerRow, space: deviceColorSpace, bitmapInfo: bitmapInfo, provider: provider, decode: nil, shouldInterpolate: false, intent: .defaultIntent) - else { - return nil - } - - return UIImage(cgImage: image, scale: selectedScale, orientation: .up) + return context.generateImage() } public func generateFilledCircleImage(diameter: CGFloat, color: UIColor?, strokeColor: UIColor? = nil, strokeWidth: CGFloat? = nil, backgroundColor: UIColor? = nil) -> UIImage? { @@ -383,7 +322,12 @@ public func generateGradientTintedImage(image: UIImage?, colors: [UIColor]) -> U return tintedImage } -public func generateGradientImage(size: CGSize, colors: [UIColor], locations: [CGFloat]) -> UIImage? { +public enum GradientImageDirection { + case vertical + case horizontal +} + +public func generateGradientImage(size: CGSize, colors: [UIColor], locations: [CGFloat], direction: GradientImageDirection = .vertical) -> UIImage? { guard colors.count == locations.count else { return nil } @@ -395,7 +339,7 @@ public func generateGradientImage(size: CGSize, colors: [UIColor], locations: [C var locations = locations 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: size.height), options: CGGradientDrawingOptions()) + context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: direction == .horizontal ? CGPoint(x: size.width, y: 0.0) : CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions()) } let image = UIGraphicsGetImageFromCurrentImageContext()! @@ -428,6 +372,102 @@ public enum DrawingContextBltMode { case Alpha } +public func getSharedDevideGraphicsContextSettings() -> DeviceGraphicsContextSettings { + struct OpaqueSettings { + let rowAlignment: Int + let bitsPerPixel: Int + let bitsPerComponent: Int + let opaqueBitmapInfo: CGBitmapInfo + let colorSpace: CGColorSpace + + init(context: CGContext) { + self.rowAlignment = context.bytesPerRow + self.bitsPerPixel = context.bitsPerPixel + self.bitsPerComponent = context.bitsPerComponent + self.opaqueBitmapInfo = context.bitmapInfo + if #available(iOS 10.0, *) { + if UIScreen.main.traitCollection.displayGamut == .P3 { + self.colorSpace = CGColorSpace(name: CGColorSpace.displayP3) ?? context.colorSpace! + } else { + self.colorSpace = context.colorSpace! + } + } else { + self.colorSpace = context.colorSpace! + } + assert(self.rowAlignment == 32) + assert(self.bitsPerPixel == 32) + assert(self.bitsPerComponent == 8) + } + } + + struct TransparentSettings { + let transparentBitmapInfo: CGBitmapInfo + + init(context: CGContext) { + self.transparentBitmapInfo = context.bitmapInfo + } + } + + var opaqueSettings: OpaqueSettings? + var transparentSettings: TransparentSettings? + + if #available(iOS 10.0, *) { + let opaqueFormat = UIGraphicsImageRendererFormat() + let transparentFormat = UIGraphicsImageRendererFormat() + if #available(iOS 12.0, *) { + opaqueFormat.preferredRange = .standard + transparentFormat.preferredRange = .standard + } + opaqueFormat.opaque = true + transparentFormat.opaque = false + + let opaqueRenderer = UIGraphicsImageRenderer(bounds: CGRect(origin: CGPoint(), size: CGSize(width: 1.0, height: 1.0)), format: opaqueFormat) + let _ = opaqueRenderer.image(actions: { context in + opaqueSettings = OpaqueSettings(context: context.cgContext) + }) + + let transparentRenderer = UIGraphicsImageRenderer(bounds: CGRect(origin: CGPoint(), size: CGSize(width: 1.0, height: 1.0)), format: transparentFormat) + let _ = transparentRenderer.image(actions: { context in + transparentSettings = TransparentSettings(context: context.cgContext) + }) + } else { + UIGraphicsBeginImageContextWithOptions(CGSize(width: 1.0, height: 1.0), true, 1.0) + let refContext = UIGraphicsGetCurrentContext()! + opaqueSettings = OpaqueSettings(context: refContext) + UIGraphicsEndImageContext() + + UIGraphicsBeginImageContextWithOptions(CGSize(width: 1.0, height: 1.0), false, 1.0) + let refCtxTransparent = UIGraphicsGetCurrentContext()! + transparentSettings = TransparentSettings(context: refCtxTransparent) + UIGraphicsEndImageContext() + } + + return DeviceGraphicsContextSettings( + rowAlignment: opaqueSettings!.rowAlignment, + bitsPerPixel: opaqueSettings!.bitsPerPixel, + bitsPerComponent: opaqueSettings!.bitsPerComponent, + opaqueBitmapInfo: opaqueSettings!.opaqueBitmapInfo, + transparentBitmapInfo: transparentSettings!.transparentBitmapInfo, + colorSpace: opaqueSettings!.colorSpace + ) +} + +public struct DeviceGraphicsContextSettings { + public static let shared: DeviceGraphicsContextSettings = getSharedDevideGraphicsContextSettings() + + public let rowAlignment: Int + public let bitsPerPixel: Int + public let bitsPerComponent: Int + public let opaqueBitmapInfo: CGBitmapInfo + public let transparentBitmapInfo: CGBitmapInfo + public let colorSpace: CGColorSpace + + public func bytesPerRow(forWidth width: Int) -> Int { + let baseValue = self.bitsPerPixel * width / 8 + return (baseValue + 31) & ~0x1F + } +} + public class DrawingContext { public let size: CGSize public let scale: CGFloat @@ -435,46 +475,39 @@ public class DrawingContext { public let bytesPerRow: Int private let bitmapInfo: CGBitmapInfo public let length: Int - public let bytes: UnsafeMutableRawPointer - let provider: CGDataProvider? - - private var _context: CGContext? + private let imageBuffer: ASCGImageBuffer + public var bytes: UnsafeMutableRawPointer { + if self.hasGeneratedImage { + preconditionFailure() + } + return self.imageBuffer.mutableBytes + } + private let context: CGContext + + private var hasGeneratedImage = false public func withContext(_ f: (CGContext) -> ()) { - if self._context == nil { - if let c = CGContext(data: bytes, width: Int(scaledSize.width), height: Int(scaledSize.height), bitsPerComponent: 8, bytesPerRow: bytesPerRow, space: deviceColorSpace, bitmapInfo: self.bitmapInfo.rawValue) { - c.scaleBy(x: scale, y: scale) - self._context = c - } - } - - if let _context = self._context { - _context.translateBy(x: self.size.width / 2.0, y: self.size.height / 2.0) - _context.scaleBy(x: 1.0, y: -1.0) - _context.translateBy(x: -self.size.width / 2.0, y: -self.size.height / 2.0) - - f(_context) - - _context.translateBy(x: self.size.width / 2.0, y: self.size.height / 2.0) - _context.scaleBy(x: 1.0, y: -1.0) - _context.translateBy(x: -self.size.width / 2.0, y: -self.size.height / 2.0) - } + let context = self.context + + context.translateBy(x: self.size.width / 2.0, y: self.size.height / 2.0) + context.scaleBy(x: 1.0, y: -1.0) + context.translateBy(x: -self.size.width / 2.0, y: -self.size.height / 2.0) + + f(context) + + context.translateBy(x: self.size.width / 2.0, y: self.size.height / 2.0) + context.scaleBy(x: 1.0, y: -1.0) + context.translateBy(x: -self.size.width / 2.0, y: -self.size.height / 2.0) } public func withFlippedContext(_ f: (CGContext) -> ()) { - if self._context == nil { - if let c = CGContext(data: bytes, width: Int(scaledSize.width), height: Int(scaledSize.height), bitsPerComponent: 8, bytesPerRow: bytesPerRow, space: deviceColorSpace, bitmapInfo: self.bitmapInfo.rawValue) { - c.scaleBy(x: scale, y: scale) - self._context = c - } - } - - if let _context = self._context { - f(_context) - } + f(self.context) } - public init(size: CGSize, scale: CGFloat = 0.0, premultiplied: Bool = true, clear: Bool = false) { + public init(size: CGSize, scale: CGFloat = 0.0, opaque: Bool = false, clear: Bool = false) { + assert(!size.width.isZero && !size.height.isZero) + let size: CGSize = CGSize(width: max(1.0, size.width), height: max(1.0, size.height)) + let actualScale: CGFloat if scale.isZero { actualScale = deviceScale @@ -485,33 +518,61 @@ public class DrawingContext { self.scale = actualScale self.scaledSize = CGSize(width: size.width * actualScale, height: size.height * actualScale) - self.bytesPerRow = (4 * Int(scaledSize.width) + 15) & (~15) + self.bytesPerRow = DeviceGraphicsContextSettings.shared.bytesPerRow(forWidth: Int(scaledSize.width)) self.length = bytesPerRow * Int(scaledSize.height) - - if premultiplied { - self.bitmapInfo = CGBitmapInfo(rawValue: CGBitmapInfo.byteOrder32Little.rawValue | CGImageAlphaInfo.premultipliedFirst.rawValue) + + self.imageBuffer = ASCGImageBuffer(length: UInt(self.length)) + + if opaque { + self.bitmapInfo = DeviceGraphicsContextSettings.shared.opaqueBitmapInfo } else { - self.bitmapInfo = CGBitmapInfo(rawValue: CGBitmapInfo.byteOrder32Little.rawValue | CGImageAlphaInfo.first.rawValue) + self.bitmapInfo = DeviceGraphicsContextSettings.shared.transparentBitmapInfo } - - self.bytes = malloc(length)! + + self.context = CGContext( + data: self.imageBuffer.mutableBytes, + width: Int(self.scaledSize.width), + height: Int(self.scaledSize.height), + bitsPerComponent: DeviceGraphicsContextSettings.shared.bitsPerComponent, + bytesPerRow: self.bytesPerRow, + space: DeviceGraphicsContextSettings.shared.colorSpace, + bitmapInfo: self.bitmapInfo.rawValue, + releaseCallback: nil, + releaseInfo: nil + )! + self.context.scaleBy(x: self.scale, y: self.scale) + if clear { memset(self.bytes, 0, self.length) } - self.provider = CGDataProvider(dataInfo: bytes, data: bytes, size: length, releaseData: { bytes, _, _ in - free(bytes) - }) - - assert(self.bytesPerRow % 16 == 0) - assert(Int64(Int(bitPattern: self.bytes)) % 16 == 0) } public func generateImage() -> UIImage? { if self.scaledSize.width.isZero || self.scaledSize.height.isZero { return nil } - if let image = CGImage(width: Int(scaledSize.width), height: Int(scaledSize.height), bitsPerComponent: 8, bitsPerPixel: 32, bytesPerRow: bytesPerRow, space: deviceColorSpace, bitmapInfo: bitmapInfo, provider: provider!, decode: nil, shouldInterpolate: false, intent: .defaultIntent) { - return UIImage(cgImage: image, scale: scale, orientation: .up) + if self.hasGeneratedImage { + preconditionFailure() + return nil + } + self.hasGeneratedImage = true + + let dataProvider = self.imageBuffer.createDataProviderAndInvalidate() + + if let image = CGImage( + width: Int(self.scaledSize.width), + height: Int(self.scaledSize.height), + bitsPerComponent: self.context.bitsPerComponent, + bitsPerPixel: self.context.bitsPerPixel, + bytesPerRow: self.context.bytesPerRow, + space: DeviceGraphicsContextSettings.shared.colorSpace, + bitmapInfo: self.context.bitmapInfo, + provider: dataProvider, + decode: nil, + shouldInterpolate: true, + intent: .defaultIntent + ) { + return UIImage(cgImage: image, scale: self.scale, orientation: .up) } else { return nil } diff --git a/submodules/Display/Source/ImageNode.swift b/submodules/Display/Source/ImageNode.swift index 3522fad829..ecf6f87aea 100644 --- a/submodules/Display/Source/ImageNode.swift +++ b/submodules/Display/Source/ImageNode.swift @@ -126,6 +126,7 @@ public class ImageNode: ASDisplayNode { private let hasImage: ValuePromise? private var first = true private let enableEmpty: Bool + public var enableAnimatedTransition: Bool private let _contentReady = Promise() private var didSetReady: Bool = false @@ -141,13 +142,14 @@ public class ImageNode: ASDisplayNode { } } - public init(enableHasImage: Bool = false, enableEmpty: Bool = false) { + public init(enableHasImage: Bool = false, enableEmpty: Bool = false, enableAnimatedTransition: Bool = false) { if enableHasImage { self.hasImage = ValuePromise(false, ignoreRepeated: true) } else { self.hasImage = nil } self.enableEmpty = enableEmpty + self.enableAnimatedTransition = enableAnimatedTransition super.init() } @@ -160,17 +162,33 @@ public class ImageNode: ASDisplayNode { self.disposable.set((signal |> deliverOnMainQueue).start(next: {[weak self] next in dispatcher.dispatch { if let strongSelf = self { - if let image = next?.cgImage { - strongSelf.contents = image - } else if strongSelf.enableEmpty { - strongSelf.contents = nil - } + var animate = strongSelf.enableAnimatedTransition if strongSelf.first && next != nil { strongSelf.first = false + animate = false if strongSelf.isNodeLoaded { strongSelf.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.18) } } + if let image = next?.cgImage { + if animate, let previousContents = strongSelf.contents { + strongSelf.contents = image + let tempLayer = CALayer() + tempLayer.contents = previousContents + tempLayer.frame = strongSelf.layer.bounds + strongSelf.layer.addSublayer(tempLayer) + tempLayer.opacity = 0.0 + tempLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: true, completion: { [weak tempLayer] _ in + tempLayer?.removeFromSuperlayer() + }) + + //strongSelf.layer.animate(from: previousContents as! CGImage, to: image, keyPath: "contents", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, duration: 0.2) + } else { + strongSelf.contents = image + } + } else if strongSelf.enableEmpty { + strongSelf.contents = nil + } if !reportedHasImage { if let hasImage = strongSelf.hasImage { reportedHasImage = true diff --git a/submodules/Display/Source/InteractiveTransitionGestureRecognizer.swift b/submodules/Display/Source/InteractiveTransitionGestureRecognizer.swift index 48761f12a1..e0b2655301 100644 --- a/submodules/Display/Source/InteractiveTransitionGestureRecognizer.swift +++ b/submodules/Display/Source/InteractiveTransitionGestureRecognizer.swift @@ -82,6 +82,10 @@ public class InteractiveTransitionGestureRecognizer: UIPanGestureRecognizer { self.validatedGesture = false self.currentAllowedDirections = [] } + + public func cancel() { + self.state = .cancelled + } override public func touchesBegan(_ touches: Set, with event: UIEvent) { let touch = touches.first! diff --git a/submodules/Display/Source/Keyboard.swift b/submodules/Display/Source/Keyboard.swift index 53c3b5d048..d2699a42e6 100644 --- a/submodules/Display/Source/Keyboard.swift +++ b/submodules/Display/Source/Keyboard.swift @@ -2,7 +2,7 @@ import Foundation import UIKitRuntimeUtils public enum Keyboard { - public static func applyAutocorrection() { - applyKeyboardAutocorrection() + public static func applyAutocorrection(textView: UITextView) { + applyKeyboardAutocorrection(textView) } } diff --git a/submodules/Display/Source/LegacyPresentedController.swift b/submodules/Display/Source/LegacyPresentedController.swift index 6403a8088a..9d81b122c0 100644 --- a/submodules/Display/Source/LegacyPresentedController.swift +++ b/submodules/Display/Source/LegacyPresentedController.swift @@ -122,7 +122,7 @@ open class LegacyPresentedController: ViewController { override open 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.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition) } override open func dismiss(completion: (() -> Void)? = nil) { diff --git a/submodules/Display/Source/LinkHighlightingNode.swift b/submodules/Display/Source/LinkHighlightingNode.swift index 26efc7afff..ac51ca4afe 100644 --- a/submodules/Display/Source/LinkHighlightingNode.swift +++ b/submodules/Display/Source/LinkHighlightingNode.swift @@ -157,7 +157,7 @@ private func generateRectsImage(color: UIColor, rects: [CGRect], inset: CGFloat, public final class LinkHighlightingNode: ASDisplayNode { private var rects: [CGRect] = [] - private let imageNode: ASImageNode + public let imageNode: ASImageNode public var innerRadius: CGFloat = 4.0 public var outerRadius: CGFloat = 4.0 @@ -196,7 +196,7 @@ public final class LinkHighlightingNode: ASDisplayNode { } private func updateImage() { - if rects.isEmpty { + if self.rects.isEmpty { self.imageNode.image = nil } let (offset, image) = generateRectsImage(color: self.color, rects: self.rects, inset: self.inset, outerRadius: self.outerRadius, innerRadius: self.innerRadius) @@ -206,6 +206,19 @@ public final class LinkHighlightingNode: ASDisplayNode { self.imageNode.frame = CGRect(origin: offset, size: image.size) } } + + public static func generateImage(color: UIColor, inset: CGFloat, innerRadius: CGFloat, outerRadius: CGFloat, rects: [CGRect]) -> (CGPoint, UIImage)? { + if rects.isEmpty { + return nil + } + let (offset, image) = generateRectsImage(color: color, rects: rects, inset: inset, outerRadius: outerRadius, innerRadius: innerRadius) + + if let image = image { + return (offset, image) + } else { + return nil + } + } public func asyncLayout() -> (UIColor, [CGRect], CGFloat, CGFloat, CGFloat) -> () -> Void { let currentRects = self.rects diff --git a/submodules/Display/Source/ListView.swift b/submodules/Display/Source/ListView.swift index 5b28121d9b..7221fe1e86 100644 --- a/submodules/Display/Source/ListView.swift +++ b/submodules/Display/Source/ListView.swift @@ -6,6 +6,11 @@ import UIKitRuntimeUtils private let infiniteScrollSize: CGFloat = 10000.0 private let insertionAnimationDuration: Double = 0.4 +private struct VisibleHeaderNodeId: Hashable { + var id: ListViewItemNode.HeaderId + var affinity: Int +} + private final class ListViewBackingLayer: CALayer { override func setNeedsLayout() { } @@ -214,6 +219,7 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture } public final var didScrollWithOffset: ((CGFloat, ContainedViewLayoutTransition, ListViewItemNode?) -> Void)? + public final var addContentOffset: ((CGFloat, ListViewItemNode?) -> Void)? private var topItemOverscrollBackground: ListViewOverscrollBackgroundNode? private var bottomItemOverscrollBackground: ASDisplayNode? @@ -274,7 +280,7 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture private final var items: [ListViewItem] = [] private final var itemNodes: [ListViewItemNode] = [] - private final var itemHeaderNodes: [Int64: ListViewItemHeaderNode] = [:] + private final var itemHeaderNodes: [VisibleHeaderNodeId: ListViewItemHeaderNode] = [:] public final var itemHeaderNodesAlpha: CGFloat = 1.0 @@ -285,7 +291,7 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture public final var visibleContentOffsetChanged: (ListViewVisibleContentOffset) -> Void = { _ in } public final var visibleBottomContentOffsetChanged: (ListViewVisibleContentOffset) -> Void = { _ in } - public final var beganInteractiveDragging: () -> Void = { } + public final var beganInteractiveDragging: (CGPoint) -> Void = { _ in } public final var endedInteractiveDragging: () -> Void = { } public final var didEndScrolling: (() -> Void)? @@ -682,7 +688,7 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture } self.scrolledToItem = nil - self.beganInteractiveDragging() + self.beganInteractiveDragging(self.touchesPosition) for itemNode in self.itemNodes { if !itemNode.isLayerBacked { @@ -845,7 +851,7 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture } self.updateScroller(transition: .immediate) - self.updateItemHeaders(leftInset: self.insets.left, rightInset: self.insets.right) + self.updateItemHeaders(leftInset: self.insets.left, rightInset: self.insets.right, synchronousLoad: false) for (_, headerNode) in self.itemHeaderNodes { if self.dynamicBounceEnabled && headerNode.wantsScrollDynamics { @@ -1027,6 +1033,8 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture transition = .animated(duration: duration, curve: .spring) case let .Default(duration): transition = .animated(duration: max(updateSizeAndInsets.duration, duration ?? 0.3), curve: .easeInOut) + case let .Custom(duration, cp1x, cp1y, cp2x, cp2y): + transition = .animated(duration: duration, curve: .custom(cp1x, cp1y, cp2x, cp2y)) } } } else if let scrollToItem = scrollToItem { @@ -1040,6 +1048,8 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture } else { transition = .animated(duration: duration ?? 0.3, curve: .easeInOut) } + case let .Custom(duration, cp1x, cp1y, cp2x, cp2y): + transition = .animated(duration: duration, curve: .custom(cp1x, cp1y, cp2x, cp2y)) } } } @@ -1361,13 +1371,6 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture } } else if let itemHighlightOverlayBackground = self.itemHighlightOverlayBackground { self.itemHighlightOverlayBackground = nil - for (_, _) in self.itemHeaderNodes { - //self.view.bringSubview(toFront: headerNode.view) - } - //self.view.bringSubview(toFront: itemHighlightOverlayBackground.view) - for _ in self.itemNodes { - //self.view.bringSubview(toFront: itemNode.view) - } transition.updateAlpha(node: itemHighlightOverlayBackground, alpha: 0.0, completion: { [weak itemHighlightOverlayBackground] _ in itemHighlightOverlayBackground?.removeFromSupernode() }) @@ -1864,7 +1867,7 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture let beginReplay = { [weak self] in if let strongSelf = self { - strongSelf.replayOperations(animated: animated, animateAlpha: options.contains(.AnimateAlpha), animateCrossfade: options.contains(.AnimateCrossfade), synchronous: options.contains(.Synchronous), animateTopItemVerticalOrigin: options.contains(.AnimateTopItemPosition), operations: updatedOperations, requestItemInsertionAnimationsIndices: options.contains(.RequestItemInsertionAnimations) ? insertedIndexSet : Set(), scrollToItem: scrollToItem, additionalScrollDistance: additionalScrollDistance, updateSizeAndInsets: updateSizeAndInsets, stationaryItemIndex: stationaryItemIndex, updateOpaqueState: updateOpaqueState, completion: { + strongSelf.replayOperations(animated: animated, animateAlpha: options.contains(.AnimateAlpha), animateCrossfade: options.contains(.AnimateCrossfade), synchronous: options.contains(.Synchronous), synchronousLoads: options.contains(.PreferSynchronousResourceLoading), animateTopItemVerticalOrigin: options.contains(.AnimateTopItemPosition), operations: updatedOperations, requestItemInsertionAnimationsIndices: options.contains(.RequestItemInsertionAnimations) ? insertedIndexSet : Set(), scrollToItem: scrollToItem, additionalScrollDistance: additionalScrollDistance, updateSizeAndInsets: updateSizeAndInsets, stationaryItemIndex: stationaryItemIndex, updateOpaqueState: updateOpaqueState, completion: { if options.contains(.PreferSynchronousDrawing) { self?.recursivelyEnsureDisplaySynchronously(true) } @@ -2125,6 +2128,7 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture offsetHeight = 0.0 nextNode.updateFrame(nextNode.frame.offsetBy(dx: 0.0, dy: nextHeight), within: self.visibleSize) + self.didScrollWithOffset?(nextHeight, .immediate, nextNode) nextNode.apparentHeight = 0.0 @@ -2234,6 +2238,7 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture var frame = self.itemNodes[i].frame frame.origin.y -= offsetHeight self.itemNodes[i].updateFrame(frame, within: self.visibleSize) + //self.didScrollWithOffset?(offsetHeight, .immediate, self.itemNodes[i]) if let accessoryItemNode = self.itemNodes[i].accessoryItemNode { self.itemNodes[i].layoutAccessoryItemNode(accessoryItemNode, leftInset: listInsets.left, rightInset: listInsets.right) } @@ -2245,6 +2250,7 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture var frame = self.itemNodes[i].frame frame.origin.y += offsetHeight self.itemNodes[i].updateFrame(frame, within: self.visibleSize) + //self.didScrollWithOffset?(-offsetHeight, .immediate, self.itemNodes[i]) if let accessoryItemNode = self.itemNodes[i].accessoryItemNode { self.itemNodes[i].layoutAccessoryItemNode(accessoryItemNode, leftInset: listInsets.left, rightInset: listInsets.right) } @@ -2310,7 +2316,7 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture } } - private func replayOperations(animated: Bool, animateAlpha: Bool, animateCrossfade: Bool, synchronous: Bool, animateTopItemVerticalOrigin: Bool, operations: [ListViewStateOperation], requestItemInsertionAnimationsIndices: Set, scrollToItem originalScrollToItem: ListViewScrollToItem?, additionalScrollDistance: CGFloat, updateSizeAndInsets: ListViewUpdateSizeAndInsets?, stationaryItemIndex: Int?, updateOpaqueState: Any?, completion: () -> Void) { + private func replayOperations(animated: Bool, animateAlpha: Bool, animateCrossfade: Bool, synchronous: Bool, synchronousLoads: Bool, animateTopItemVerticalOrigin: Bool, operations: [ListViewStateOperation], requestItemInsertionAnimationsIndices: Set, scrollToItem originalScrollToItem: ListViewScrollToItem?, additionalScrollDistance: CGFloat, updateSizeAndInsets: ListViewUpdateSizeAndInsets?, stationaryItemIndex: Int?, updateOpaqueState: Any?, completion: () -> Void) { var scrollToItem: ListViewScrollToItem? var isExperimentalSnapToScrollToItem = false if let originalScrollToItem = originalScrollToItem { @@ -2665,10 +2671,12 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture scrollToItemTransition = .animated(duration: duration, curve: .spring) case let .Default(duration): scrollToItemTransition = .animated(duration: duration ?? 0.3, curve: .easeInOut) + case let .Custom(duration, cp1x, cp1y, cp2x, cp2y): + scrollToItemTransition = .animated(duration: duration, curve: .custom(cp1x, cp1y, cp2x, cp2y)) } } - self.didScrollWithOffset?(-offset, scrollToItemTransition, nil) + //self.didScrollWithOffset?(-offset, scrollToItemTransition, nil) } for itemNode in self.itemNodes { @@ -2751,10 +2759,14 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture updateSizeAndInsetsTransition = .animated(duration: duration, curve: .spring) case let .Default(duration): updateSizeAndInsetsTransition = .animated(duration: duration ?? 0.3, curve: .easeInOut) + case let .Custom(duration, cp1x, cp1y, cp2x, cp2y): + updateSizeAndInsetsTransition = .animated(duration: duration, curve: .custom(cp1x, cp1y, cp2x, cp2y)) } } - - self.didScrollWithOffset?(-offsetFix, updateSizeAndInsetsTransition, nil) + + if !offsetFix.isZero { + //self.didScrollWithOffset?(-offsetFix, updateSizeAndInsetsTransition, nil) + } for itemNode in self.itemNodes { itemNode.updateFrame(itemNode.frame.offsetBy(dx: 0.0, dy: offsetFix), within: self.visibleSize) @@ -2765,7 +2777,7 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture if !snappedTopInset.isZero && (previousVisibleSize.height.isZero || previousApparentFrames.isEmpty) { offsetFix += snappedTopInset - self.didScrollWithOffset?(-offsetFix, .immediate, nil) + //self.didScrollWithOffset?(-snappedTopInset, .immediate, nil) for itemNode in self.itemNodes { itemNode.updateFrame(itemNode.frame.offsetBy(dx: 0.0, dy: snappedTopInset), within: self.visibleSize) @@ -2804,6 +2816,20 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture } animationDuration = duration + springAnimation.isAdditive = true + animation = springAnimation + case let .Custom(duration, cp1x, cp1y, cp2x, cp2y): + headerNodesTransition = (.animated(duration: duration, curve: .custom(cp1x, cp1y, cp2x, cp2y)), false, -completeOffset) + animationCurve = .custom(cp1x, cp1y, cp2x, cp2y) + let springAnimation = CABasicAnimation(keyPath: "sublayerTransform") + springAnimation.timingFunction = CAMediaTimingFunction(controlPoints: cp1x, cp1y, cp2x, cp2y) + springAnimation.duration = duration * UIView.animationDurationFactor() + springAnimation.fromValue = NSValue(caTransform3D: CATransform3DMakeTranslation(0.0, -completeOffset, 0.0)) + springAnimation.toValue = NSValue(caTransform3D: CATransform3DIdentity) + springAnimation.isRemovedOnCompletion = true + + animationDuration = duration + springAnimation.isAdditive = true animation = springAnimation case let .Default(duration): @@ -2825,9 +2851,14 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture self?.updateItemNodesVisibilities(onlyPositive: false) } self.layer.add(animation, forKey: nil) - for itemNode in self.itemNodes { - itemNode.applyAbsoluteOffset(value: -completeOffset, animationCurve: animationCurve, duration: animationDuration) + if !completeOffset.isZero { + for itemNode in self.itemNodes { + itemNode.applyAbsoluteOffset(value: CGPoint(x: 0.0, y: -completeOffset), animationCurve: animationCurve, duration: animationDuration) + } + self.didScrollWithOffset?(-completeOffset, ContainedViewLayoutTransition.animated(duration: animationDuration, curve: animationCurve), nil) } + } else { + self.didScrollWithOffset?(-completeOffset, .immediate, nil) } } else { self.visibleSize = updateSizeAndInsets.size @@ -2865,8 +2896,12 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture springAnimation.isAdditive = true self.layer.add(springAnimation, forKey: nil) - for itemNode in self.itemNodes { - itemNode.applyAbsoluteOffset(value: -completeOffset, animationCurve: .spring, duration: duration) + + if !completeOffset.isZero { + for itemNode in self.itemNodes { + itemNode.applyAbsoluteOffset(value: CGPoint(x: 0.0, y: -completeOffset), animationCurve: .spring, duration: duration) + } + self.didScrollWithOffset?(-completeOffset, .animated(duration: duration, curve: .spring), nil) } } else { if let snapshotView = snapshotView { @@ -2975,7 +3010,9 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture switch scrollToItem.directionHint { case .Up: - offset = updatedLowerBound - (previousUpperBound ?? 0.0) + if let previousUpperBound = previousUpperBound { + offset = updatedLowerBound - previousUpperBound + } case .Down: offset = updatedUpperBound - (previousLowerBound ?? self.visibleSize.height) } @@ -2993,15 +3030,17 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture headerNodesTransition = (.animated(duration: duration, curve: .spring), headerNodesTransition.1, headerNodesTransition.2 - offsetOrZero) case let .Default(duration): headerNodesTransition = (.animated(duration: duration ?? 0.3, curve: .easeInOut), true, headerNodesTransition.2 - offsetOrZero) + case let .Custom(duration, cp1x, cp1y, cp2x, cp2y): + headerNodesTransition = (.animated(duration: duration, curve: .custom(cp1x, cp1y, cp2x, cp2y)), headerNodesTransition.1, headerNodesTransition.2 - offsetOrZero) } for (_, headerNode) in self.itemHeaderNodes { previousItemHeaderNodes.append(headerNode) } - self.updateItemHeaders(leftInset: listInsets.left, rightInset: listInsets.right, transition: headerNodesTransition, animateInsertion: animated || !requestItemInsertionAnimationsIndices.isEmpty) + self.updateItemHeaders(leftInset: listInsets.left, rightInset: listInsets.right, synchronousLoad: synchronousLoads, transition: headerNodesTransition, animateInsertion: animated || !requestItemInsertionAnimationsIndices.isEmpty) if let offset = offset, !offset.isZero { - self.didScrollWithOffset?(-offset, headerNodesTransition.0, nil) + //self.didScrollWithOffset?(-offset, headerNodesTransition.0, nil) let lowestNodeToInsertBelow = self.lowestNodeToInsertBelow() for itemNode in temporaryPreviousNodes { itemNode.updateFrame(itemNode.frame.offsetBy(dx: 0.0, dy: offset), within: self.visibleSize) @@ -3065,6 +3104,27 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture animation = springAnimation reverseAnimation = reverseSpringAnimation + case let .Custom(duration, cp1x, cp1y, cp2x, cp2y): + animationCurve = .custom(cp1x, cp1y, cp2x, cp2y) + animationDuration = duration + let basicAnimation = CABasicAnimation(keyPath: "sublayerTransform") + basicAnimation.timingFunction = CAMediaTimingFunction(controlPoints: cp1x, cp1y, cp2x, cp2y) + basicAnimation.duration = duration * UIView.animationDurationFactor() + basicAnimation.fromValue = NSValue(caTransform3D: CATransform3DMakeTranslation(0.0, -offset, 0.0)) + basicAnimation.toValue = NSValue(caTransform3D: CATransform3DIdentity) + basicAnimation.isRemovedOnCompletion = true + basicAnimation.isAdditive = true + + let reverseBasicAnimation = CABasicAnimation(keyPath: "sublayerTransform") + reverseBasicAnimation.timingFunction = CAMediaTimingFunction(controlPoints: cp1x, cp1y, cp2x, cp2y) + reverseBasicAnimation.duration = duration * UIView.animationDurationFactor() + reverseBasicAnimation.fromValue = NSValue(caTransform3D: CATransform3DMakeTranslation(0.0, offset, 0.0)) + reverseBasicAnimation.toValue = NSValue(caTransform3D: CATransform3DIdentity) + reverseBasicAnimation.isRemovedOnCompletion = true + reverseBasicAnimation.isAdditive = true + + animation = basicAnimation + reverseAnimation = reverseBasicAnimation case let .Default(duration): if let duration = duration { animationCurve = .easeInOut @@ -3122,11 +3182,12 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture } self.layer.add(animation, forKey: nil) for itemNode in self.itemNodes { - itemNode.applyAbsoluteOffset(value: -offset, animationCurve: animationCurve, duration: animationDuration) + itemNode.applyAbsoluteOffset(value: CGPoint(x: 0.0, y: -offset), animationCurve: animationCurve, duration: animationDuration) } for itemNode in temporaryPreviousNodes { - itemNode.applyAbsoluteOffset(value: -offset, animationCurve: animationCurve, duration: animationDuration) + itemNode.applyAbsoluteOffset(value: CGPoint(x: 0.0, y: -offset), animationCurve: animationCurve, duration: animationDuration) } + self.didScrollWithOffset?(-offset, .animated(duration: animationDuration, curve: animationCurve), nil) if let verticalScrollIndicator = self.verticalScrollIndicator { verticalScrollIndicator.layer.add(reverseAnimation, forKey: nil) } @@ -3152,7 +3213,7 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture completion() } else { - self.updateItemHeaders(leftInset: listInsets.left, rightInset: listInsets.right, transition: headerNodesTransition, animateInsertion: animated || !requestItemInsertionAnimationsIndices.isEmpty) + self.updateItemHeaders(leftInset: listInsets.left, rightInset: listInsets.right, synchronousLoad: synchronousLoads, transition: headerNodesTransition, animateInsertion: animated || !requestItemInsertionAnimationsIndices.isEmpty) self.updateItemNodesVisibilities(onlyPositive: deferredUpdateVisible) if animated { @@ -3199,15 +3260,88 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture node.headerAccessoryItemNode?.removeFromSupernode() node.headerAccessoryItemNode = nil } + + private var nextHeaderSpaceAffinity: Int = 0 + + private func assignHeaderSpaceAffinities() { + var nextTempAffinity = 0 + + var existingAffinityIdByAffinity: [Int: Int] = [:] + + for i in 0 ..< self.itemNodes.count { + let currentItemNode = self.itemNodes[i] + + if let currentItemHeaders = currentItemNode.headers() { + currentHeadersLoop: for currentHeader in currentItemHeaders { + let currentId = currentHeader.id + if let currentAffinity = currentItemNode.tempHeaderSpaceAffinities[currentId] { + if let existingAffinity = currentItemNode.headerSpaceAffinities[currentId] { + existingAffinityIdByAffinity[currentAffinity] = existingAffinity + } + + continue currentHeadersLoop + } + + let currentAffinity = nextTempAffinity + nextTempAffinity += 1 + + currentItemNode.tempHeaderSpaceAffinities[currentId] = currentAffinity + + if let existingAffinity = currentItemNode.headerSpaceAffinities[currentId] { + existingAffinityIdByAffinity[currentAffinity] = existingAffinity + } + + groupSearch: for nextIndex in (i + 1) ..< self.itemNodes.count { + let nextItemNode = self.itemNodes[nextIndex] + + var containsSameHeader = false + if let nextHeaders = nextItemNode.headers() { + nextHeaderSearch: for nextHeader in nextHeaders { + if nextHeader.id == currentId { + containsSameHeader = true + break nextHeaderSearch + } + } + } + + if containsSameHeader { + nextItemNode.tempHeaderSpaceAffinities[currentId] = currentAffinity + } else { + break groupSearch + } + } + } + } + } + + for i in 0 ..< self.itemNodes.count { + let itemNode = self.itemNodes[i] + for (headerId, tempAffinity) in itemNode.tempHeaderSpaceAffinities { + let affinity: Int + if let existing = existingAffinityIdByAffinity[tempAffinity] { + affinity = existing + } else { + affinity = self.nextHeaderSpaceAffinity + existingAffinityIdByAffinity[tempAffinity] = affinity + self.nextHeaderSpaceAffinity += 1 + } + + itemNode.headerSpaceAffinities[headerId] = affinity + itemNode.tempHeaderSpaceAffinities = [:] + } + } + } - private func updateItemHeaders(leftInset: CGFloat, rightInset: CGFloat, transition: (ContainedViewLayoutTransition, Bool, CGFloat) = (.immediate, false, 0.0), animateInsertion: Bool = false) { + private func updateItemHeaders(leftInset: CGFloat, rightInset: CGFloat, synchronousLoad: Bool, transition: (ContainedViewLayoutTransition, Bool, CGFloat) = (.immediate, false, 0.0), animateInsertion: Bool = false) { + self.assignHeaderSpaceAffinities() + let upperDisplayBound = self.headerInsets.top let lowerDisplayBound = self.visibleSize.height - self.insets.bottom - var visibleHeaderNodes = Set() + var visibleHeaderNodes = Set() let flashing = self.headerItemsAreFlashing() - func addHeader(id: Int64, upperBound: CGFloat, upperBoundEdge: CGFloat, lowerBound: CGFloat, item: ListViewItemHeader, hasValidNodes: Bool) { + func addHeader(id: VisibleHeaderNodeId, upperBound: CGFloat, upperBoundEdge: CGFloat, lowerBound: CGFloat, item: ListViewItemHeader, hasValidNodes: Bool) { let itemHeaderHeight: CGFloat = item.height let headerFrame: CGRect @@ -3244,7 +3378,7 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture switch curve { case .linear: headerNode.layer.animateBoundsOriginYAdditive(from: offset, to: 0.0, duration: duration, mediaTimingFunction: CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear)) - case .spring: + case .spring, .customSpring: transition.0.animateOffsetAdditive(node: headerNode, offset: offset) case let .custom(p1, p2, p3, p4): headerNode.layer.animateBoundsOriginYAdditive(from: offset, to: 0.0, duration: duration, mediaTimingFunction: CAMediaTimingFunction(controlPoints: p1, p2, p3, p4)) @@ -3271,13 +3405,12 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture } else if hasValidNodes && headerNode.alpha.isZero { headerNode.alpha = initialHeaderNodeAlpha if animateInsertion { - headerNode.layer.animateAlpha(from: 0.0, to: initialHeaderNodeAlpha, duration: 0.2) - headerNode.layer.animateScale(from: 0.2, to: 1.0, duration: 0.2) + headerNode.animateAdded(duration: 0.2) } } headerNode.updateStickDistanceFactor(stickLocationDistanceFactor, transition: transition.0) } else { - let headerNode = item.node() + let headerNode = item.node(synchronousLoad: synchronousLoad) headerNode.alpha = initialHeaderNodeAlpha if headerNode.item !== item { item.updateNode(headerNode, previous: nil, next: nil) @@ -3294,39 +3427,70 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture self.addSubnode(headerNode) } if animateInsertion { - headerNode.layer.animateAlpha(from: 0.0, to: initialHeaderNodeAlpha, duration: 0.3) - headerNode.layer.animateScale(from: 0.2, to: 1.0, duration: 0.3) + headerNode.alpha = initialHeaderNodeAlpha + headerNode.animateAdded(duration: 0.2) } headerNode.updateStickDistanceFactor(stickLocationDistanceFactor, transition: .immediate) } } + + var previousHeaderBySpace: [AnyHashable: (id: VisibleHeaderNodeId, upperBound: CGFloat, upperBoundEdge: CGFloat, lowerBound: CGFloat, item: ListViewItemHeader, hasValidNodes: 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, previousUpperBoundEdge, previousLowerBound, previousHeaderItem, hasValidNodes) = previousHeader { - if previousHeaderId == itemHeader.id { - previousHeader = (previousHeaderId, previousUpperBound, previousUpperBoundEdge, itemFrame.maxY, previousHeaderItem, hasValidNodes || itemNode.index != nil) - } else { - addHeader(id: previousHeaderId, upperBound: previousUpperBound, upperBoundEdge: previousUpperBoundEdge, lowerBound: previousLowerBound, item: previousHeaderItem, hasValidNodes: hasValidNodes) - - previousHeader = (itemHeader.id, itemFrame.minY, itemFrame.minY + itemTopInset, itemFrame.maxY, itemHeader, itemNode.index != nil) + var validItemHeaderSpaces: [AnyHashable] = [] + if let itemHeaders = itemNode.headers() { + for itemHeader in itemHeaders { + guard let affinity = itemNode.headerSpaceAffinities[itemHeader.id] else { + assertionFailure() + continue + } + + let headerId = VisibleHeaderNodeId(id: itemHeader.id, affinity: affinity) + + validItemHeaderSpaces.append(itemHeader.id.space) + + let itemMaxY: CGFloat + if itemHeader.stickOverInsets { + itemMaxY = itemFrame.maxY + } else { + itemMaxY = itemFrame.maxY - (self.rotated ? itemNode.insets.top : itemNode.insets.bottom) + } + + if let (previousHeaderId, previousUpperBound, previousUpperBoundEdge, previousLowerBound, previousHeaderItem, hasValidNodes) = previousHeaderBySpace[itemHeader.id.space] { + if previousHeaderId == headerId { + previousHeaderBySpace[itemHeader.id.space] = (previousHeaderId, previousUpperBound, previousUpperBoundEdge, itemMaxY, previousHeaderItem, hasValidNodes || itemNode.index != nil) + } else { + addHeader(id: previousHeaderId, upperBound: previousUpperBound, upperBoundEdge: previousUpperBoundEdge, lowerBound: previousLowerBound, item: previousHeaderItem, hasValidNodes: hasValidNodes) + + previousHeaderBySpace[itemHeader.id.space] = (headerId, itemFrame.minY, itemFrame.minY + itemTopInset, itemMaxY, itemHeader, itemNode.index != nil) + } + } else { + previousHeaderBySpace[itemHeader.id.space] = (headerId, itemFrame.minY, itemFrame.minY + itemTopInset, itemMaxY, itemHeader, itemNode.index != nil) } - } else { - previousHeader = (itemHeader.id, itemFrame.minY, itemFrame.minY + itemTopInset, itemFrame.maxY, itemHeader, itemNode.index != nil) } - } else { - if let (previousHeaderId, previousUpperBound, previousUpperBoundEdge, previousLowerBound, previousHeaderItem, hasValidNodes) = previousHeader { - addHeader(id: previousHeaderId, upperBound: previousUpperBound, upperBoundEdge: previousUpperBoundEdge, lowerBound: previousLowerBound, item: previousHeaderItem, hasValidNodes: hasValidNodes) + } + + for (space, previousHeader) in previousHeaderBySpace { + if validItemHeaderSpaces.contains(space) { + continue } - previousHeader = nil + + let (previousHeaderId, previousUpperBound, previousUpperBoundEdge, previousLowerBound, previousHeaderItem, hasValidNodes) = previousHeader + + addHeader(id: previousHeaderId, upperBound: previousUpperBound, upperBoundEdge: previousUpperBoundEdge, lowerBound: previousLowerBound, item: previousHeaderItem, hasValidNodes: hasValidNodes) + + previousHeaderBySpace.removeValue(forKey: space) } } - - if let (previousHeaderId, previousUpperBound, previousUpperBoundEdge, previousLowerBound, previousHeaderItem, hasValidNodes) = previousHeader { + + for (space, previousHeader) in previousHeaderBySpace { + let (previousHeaderId, previousUpperBound, previousUpperBoundEdge, previousLowerBound, previousHeaderItem, hasValidNodes) = previousHeader + addHeader(id: previousHeaderId, upperBound: previousUpperBound, upperBoundEdge: previousUpperBoundEdge, lowerBound: previousLowerBound, item: previousHeaderItem, hasValidNodes: hasValidNodes) + + previousHeaderBySpace.removeValue(forKey: space) } let currentIds = Set(self.itemHeaderNodes.keys) @@ -3646,10 +3810,10 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture var updatedOperations = operations updatedState.removeInvisibleNodes(&updatedOperations) if synchronous { - self.replayOperations(animated: false, animateAlpha: false, animateCrossfade: false, synchronous: false, animateTopItemVerticalOrigin: false, operations: updatedOperations, requestItemInsertionAnimationsIndices: Set(), scrollToItem: nil, additionalScrollDistance: 0.0, updateSizeAndInsets: nil, stationaryItemIndex: nil, updateOpaqueState: nil, completion: completion) + self.replayOperations(animated: false, animateAlpha: false, animateCrossfade: false, synchronous: false, synchronousLoads: false, animateTopItemVerticalOrigin: false, operations: updatedOperations, requestItemInsertionAnimationsIndices: Set(), scrollToItem: nil, additionalScrollDistance: 0.0, updateSizeAndInsets: nil, stationaryItemIndex: nil, updateOpaqueState: nil, completion: completion) } else { self.dispatchOnVSync { - self.replayOperations(animated: false, animateAlpha: false, animateCrossfade: false, synchronous: false, animateTopItemVerticalOrigin: false, operations: updatedOperations, requestItemInsertionAnimationsIndices: Set(), scrollToItem: nil, additionalScrollDistance: 0.0, updateSizeAndInsets: nil, stationaryItemIndex: nil, updateOpaqueState: nil, completion: completion) + self.replayOperations(animated: false, animateAlpha: false, animateCrossfade: false, synchronous: false, synchronousLoads: false, animateTopItemVerticalOrigin: false, operations: updatedOperations, requestItemInsertionAnimationsIndices: Set(), scrollToItem: nil, additionalScrollDistance: 0.0, updateSizeAndInsets: nil, stationaryItemIndex: nil, updateOpaqueState: nil, completion: completion) } } } @@ -3976,7 +4140,7 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture self.updateOverlayHighlight(transition: transition) } - private func itemIndexAtPoint(_ point: CGPoint) -> Int? { + public func itemIndexAtPoint(_ point: CGPoint) -> Int? { for itemNode in self.itemNodes { if itemNode.apparentContentFrame.contains(point) { return itemNode.index @@ -3994,6 +4158,15 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture return nil } + public func indexOf(itemNode: ListViewItemNode) -> Int? { + for listItemNode in self.itemNodes { + if itemNode === listItemNode { + return listItemNode.index + } + } + return nil + } + public func forEachItemNode(_ f: (ASDisplayNode) -> Void) { for itemNode in self.itemNodes { if itemNode.index != nil { @@ -4024,13 +4197,21 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture } } - public func ensureItemNodeVisible(_ node: ListViewItemNode, animated: Bool = true, overflow: CGFloat = 0.0, allowIntersection: Bool = false, curve: ListViewAnimationCurve = .Default(duration: 0.25)) { + public func ensureItemNodeVisible(_ node: ListViewItemNode, animated: Bool = true, overflow: CGFloat = 0.0, allowIntersection: Bool = false, atTop: Bool = false, 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: 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 }) + if atTop { + if node.frame.maxY > self.visibleSize.height - self.insets.bottom { + self.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: ListViewDeleteAndInsertOptions(), scrollToItem: ListViewScrollToItem(index: index, position: ListViewScrollPosition.top(-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 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: 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 { diff --git a/submodules/Display/Source/ListViewAnimation.swift b/submodules/Display/Source/ListViewAnimation.swift index aed2d242c3..482f466d3d 100644 --- a/submodules/Display/Source/ListViewAnimation.swift +++ b/submodules/Display/Source/ListViewAnimation.swift @@ -183,27 +183,19 @@ public final class ListViewAnimation { } public func listViewAnimationDurationAndCurve(transition: ContainedViewLayoutTransition) -> (Double, ListViewAnimationCurve) { - var duration: Double = 0.0 - var curve: UInt = 0 switch transition { case .immediate: - break + return (0.0, .Default(duration: 0.0)) case let .animated(animationDuration, animationCurve): - duration = animationDuration switch animationCurve { - case .linear, .easeInOut, .custom: - break - case .spring: - curve = 7 + case .linear: + return (animationDuration, .Default(duration: animationDuration)) + case .easeInOut: + return (animationDuration, .Default(duration: animationDuration)) + case .spring, .customSpring: + return (animationDuration, .Spring(duration: animationDuration)) + case let .custom(c1x, c1y, c2x, c2y): + return (animationDuration, .Custom(duration: animationDuration, c1x, c1y, c2x, c2y)) } } - - 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 ccc1344711..56d0e73bec 100644 --- a/submodules/Display/Source/ListViewIntermediateState.swift +++ b/submodules/Display/Source/ListViewIntermediateState.swift @@ -22,6 +22,7 @@ public enum ListViewScrollToItemDirectionHint { public enum ListViewAnimationCurve { case Spring(duration: Double) case Default(duration: Double?) + case Custom(duration: Double, Float, Float, Float, Float) } public struct ListViewScrollToItem { diff --git a/submodules/Display/Source/ListViewItemHeader.swift b/submodules/Display/Source/ListViewItemHeader.swift index e81635ae8f..dcbbffb286 100644 --- a/submodules/Display/Source/ListViewItemHeader.swift +++ b/submodules/Display/Source/ListViewItemHeader.swift @@ -8,14 +8,13 @@ public enum ListViewItemHeaderStickDirection { case bottom } -public typealias ListViewItemHeaderId = Int64 - -public protocol ListViewItemHeader: class { - var id: ListViewItemHeaderId { get } +public protocol ListViewItemHeader: AnyObject { + var id: ListViewItemNode.HeaderId { get } var stickDirection: ListViewItemHeaderStickDirection { get } var height: CGFloat { get } + var stickOverInsets: Bool { get } - func node() -> ListViewItemHeaderNode + func node(synchronousLoad: Bool) -> ListViewItemHeaderNode func updateNode(_ node: ListViewItemHeaderNode, previous: ListViewItemHeader?, next: ListViewItemHeader?) } @@ -114,6 +113,11 @@ open class ListViewItemHeaderNode: ASDisplayNode { self.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration, removeOnCompletion: false) self.layer.animateScale(from: 1.0, to: 0.2, duration: duration, removeOnCompletion: false) } + + open func animateAdded(duration: Double) { + self.layer.animateAlpha(from: 0.0, to: self.alpha, duration: 0.2) + self.layer.animateScale(from: 0.2, to: 1.0, duration: 0.2) + } private var cachedLayout: (CGSize, CGFloat, CGFloat)? diff --git a/submodules/Display/Source/ListViewItemNode.swift b/submodules/Display/Source/ListViewItemNode.swift index d57694a912..0322282174 100644 --- a/submodules/Display/Source/ListViewItemNode.swift +++ b/submodules/Display/Source/ListViewItemNode.swift @@ -84,6 +84,16 @@ public struct ListViewItemLayoutParams { } open class ListViewItemNode: ASDisplayNode, AccessibilityFocusableNode { + public struct HeaderId: Hashable { + public var space: AnyHashable + public var id: AnyHashable + + public init(space: AnyHashable, id: AnyHashable) { + self.space = space + self.id = id + } + } + let rotated: Bool final var index: Int? @@ -116,6 +126,9 @@ open class ListViewItemNode: ASDisplayNode, AccessibilityFocusableNode { private final var spring: ListViewItemSpring? private final var animations: [(String, ListViewAnimation)] = [] + + final var tempHeaderSpaceAffinities: [ListViewItemNode.HeaderId: Int] = [:] + final var headerSpaceAffinities: [ListViewItemNode.HeaderId: Int] = [:] final let wantsScrollDynamics: Bool @@ -221,6 +234,7 @@ open class ListViewItemNode: ASDisplayNode, AccessibilityFocusableNode { } var apparentHeight: CGFloat = 0.0 + public private(set) var apparentHeightTransition: (CGFloat, CGFloat)? private var _bounds: CGRect = CGRect() private var _position: CGPoint = CGPoint() @@ -465,6 +479,7 @@ open class ListViewItemNode: ASDisplayNode, AccessibilityFocusableNode { } public func addApparentHeightAnimation(_ value: CGFloat, duration: Double, beginAt: Double, update: ((CGFloat, CGFloat) -> Void)? = nil) { + self.apparentHeightTransition = (self.apparentHeight, value) let animation = ListViewAnimation(from: self.apparentHeight, to: value, duration: duration, curve: self.preferredAnimationCurve, beginAt: beginAt, update: { [weak self] progress, currentValue in if let strongSelf = self { strongSelf.apparentHeight = currentValue @@ -533,7 +548,7 @@ open class ListViewItemNode: ASDisplayNode, AccessibilityFocusableNode { return false } - open func header() -> ListViewItemHeader? { + open func headers() -> [ListViewItemHeader]? { return nil } @@ -547,7 +562,10 @@ open class ListViewItemNode: ASDisplayNode, AccessibilityFocusableNode { public func updateFrame(_ frame: CGRect, within containerSize: CGSize) { self.frame = frame - self.updateAbsoluteRect(frame, within: containerSize) + if frame.maxY < 0.0 || frame.minY > containerSize.height { + } else { + self.updateAbsoluteRect(frame, within: containerSize) + } if let extractedBackgroundNode = self.extractedBackgroundNode { extractedBackgroundNode.frame = frame.offsetBy(dx: 0.0, dy: -self.insets.top) } @@ -556,10 +574,10 @@ open class ListViewItemNode: ASDisplayNode, AccessibilityFocusableNode { open func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) { } - open func applyAbsoluteOffset(value: CGFloat, animationCurve: ContainedViewLayoutTransitionCurve, duration: Double) { + open func applyAbsoluteOffset(value: CGPoint, 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)) + transition.animatePositionAdditive(node: extractedBackgroundNode, offset: CGPoint(x: -value.x, y: -value.y)) } } diff --git a/submodules/Display/Source/ListViewTapGestureRecognizer.swift b/submodules/Display/Source/ListViewTapGestureRecognizer.swift index f5090f99d6..2501325652 100644 --- a/submodules/Display/Source/ListViewTapGestureRecognizer.swift +++ b/submodules/Display/Source/ListViewTapGestureRecognizer.swift @@ -2,5 +2,7 @@ import Foundation import UIKit public final class ListViewTapGestureRecognizer: UITapGestureRecognizer { - + public func cancel() { + self.state = .failed + } } diff --git a/submodules/Display/Source/ListViewTransactionQueue.swift b/submodules/Display/Source/ListViewTransactionQueue.swift index a790dc1e82..af78b3f10e 100644 --- a/submodules/Display/Source/ListViewTransactionQueue.swift +++ b/submodules/Display/Source/ListViewTransactionQueue.swift @@ -32,6 +32,8 @@ public final class ListViewTransactionQueue { } } }) + } else { + assert(true) } } diff --git a/submodules/Display/Source/NavigationBar.swift b/submodules/Display/Source/NavigationBar.swift index 7308bd7c54..8f677e6c73 100644 --- a/submodules/Display/Source/NavigationBar.swift +++ b/submodules/Display/Source/NavigationBar.swift @@ -1,10 +1,18 @@ import UIKit import AsyncDisplayKit +private let titleFont = Font.with(size: 17.0, design: .regular, weight: .semibold, traits: [.monospacedNumbers]) + private var backArrowImageCache: [Int32: UIImage] = [:] -class SparseNode: ASDisplayNode { - override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { +public final class SparseNode: ASDisplayNode { + override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + for view in self.view.subviews { + if let result = view.hitTest(self.view.convert(point, to: view), with: event) { + return result + } + } + let result = super.hitTest(point, with: event) if result != self.view { return result @@ -30,16 +38,18 @@ public final class NavigationBarTheme { public let disabledButtonColor: UIColor public let primaryTextColor: UIColor public let backgroundColor: UIColor + public let enableBackgroundBlur: Bool public let separatorColor: UIColor public let badgeBackgroundColor: UIColor public let badgeStrokeColor: UIColor public let badgeTextColor: UIColor - public init(buttonColor: UIColor, disabledButtonColor: UIColor, primaryTextColor: UIColor, backgroundColor: UIColor, separatorColor: UIColor, badgeBackgroundColor: UIColor, badgeStrokeColor: UIColor, badgeTextColor: UIColor) { + public init(buttonColor: UIColor, disabledButtonColor: UIColor, primaryTextColor: UIColor, backgroundColor: UIColor, enableBackgroundBlur: Bool, separatorColor: UIColor, badgeBackgroundColor: UIColor, badgeStrokeColor: UIColor, badgeTextColor: UIColor) { self.buttonColor = buttonColor self.disabledButtonColor = disabledButtonColor self.primaryTextColor = primaryTextColor self.backgroundColor = backgroundColor + self.enableBackgroundBlur = enableBackgroundBlur self.separatorColor = separatorColor self.badgeBackgroundColor = badgeBackgroundColor self.badgeStrokeColor = badgeStrokeColor @@ -47,7 +57,7 @@ public final class NavigationBarTheme { } public func withUpdatedSeparatorColor(_ color: UIColor) -> NavigationBarTheme { - return NavigationBarTheme(buttonColor: self.buttonColor, disabledButtonColor: self.disabledButtonColor, primaryTextColor: self.primaryTextColor, backgroundColor: self.backgroundColor, separatorColor: color, badgeBackgroundColor: self.badgeBackgroundColor, badgeStrokeColor: self.badgeStrokeColor, badgeTextColor: self.badgeTextColor) + return NavigationBarTheme(buttonColor: self.buttonColor, disabledButtonColor: self.disabledButtonColor, primaryTextColor: self.primaryTextColor, backgroundColor: self.backgroundColor, enableBackgroundBlur: self.enableBackgroundBlur, separatorColor: color, badgeBackgroundColor: self.badgeBackgroundColor, badgeStrokeColor: self.badgeStrokeColor, badgeTextColor: self.badgeTextColor) } } @@ -113,6 +123,115 @@ enum NavigationPreviousAction: Equatable { } } +private var sharedIsReduceTransparencyEnabled = UIAccessibility.isReduceTransparencyEnabled + +public final class NavigationBackgroundNode: ASDisplayNode { + private var _color: UIColor + + private var enableBlur: Bool + + private var effectView: UIVisualEffectView? + private let backgroundNode: ASDisplayNode + + private var validLayout: (CGSize, CGFloat)? + + public init(color: UIColor, enableBlur: Bool = true) { + self._color = .clear + self.enableBlur = enableBlur + + self.backgroundNode = ASDisplayNode() + + super.init() + + self.addSubnode(self.backgroundNode) + + self.updateColor(color: color, transition: .immediate) + } + + private func updateBackgroundBlur(forceKeepBlur: Bool) { + if self.enableBlur && !sharedIsReduceTransparencyEnabled && ((self._color.alpha > .ulpOfOne && self._color.alpha < 0.95) || forceKeepBlur) { + if self.effectView == nil { + let effectView = UIVisualEffectView(effect: UIBlurEffect(style: .light)) + + for subview in effectView.subviews { + if subview.description.contains("VisualEffectSubview") { + subview.isHidden = true + } + } + + if let sublayer = effectView.layer.sublayers?[0], let filters = sublayer.filters { + sublayer.backgroundColor = nil + sublayer.isOpaque = false + let allowedKeys: [String] = [ + "colorSaturate", + "gaussianBlur" + ] + sublayer.filters = filters.filter { filter in + guard let filter = filter as? NSObject else { + return true + } + let filterName = String(describing: filter) + if !allowedKeys.contains(filterName) { + return false + } + return true + } + } + + if let (size, cornerRadius) = self.validLayout { + effectView.frame = CGRect(origin: CGPoint(), size: size) + ContainedViewLayoutTransition.immediate.updateCornerRadius(layer: effectView.layer, cornerRadius: cornerRadius) + effectView.clipsToBounds = !cornerRadius.isZero + } + self.effectView = effectView + self.view.insertSubview(effectView, at: 0) + } + } else if let effectView = self.effectView { + self.effectView = nil + effectView.removeFromSuperview() + } + } + + public func updateColor(color: UIColor, enableBlur: Bool? = nil, forceKeepBlur: Bool = false, transition: ContainedViewLayoutTransition) { + let effectiveEnableBlur = enableBlur ?? self.enableBlur + + if self._color.isEqual(color) && self.enableBlur == effectiveEnableBlur { + return + } + self._color = color + self.enableBlur = effectiveEnableBlur + + if sharedIsReduceTransparencyEnabled { + transition.updateBackgroundColor(node: self.backgroundNode, color: self._color.withAlphaComponent(1.0)) + } else { + transition.updateBackgroundColor(node: self.backgroundNode, color: self._color) + } + + self.updateBackgroundBlur(forceKeepBlur: forceKeepBlur) + } + + public func update(size: CGSize, cornerRadius: CGFloat = 0.0, transition: ContainedViewLayoutTransition) { + self.validLayout = (size, cornerRadius) + + let contentFrame = CGRect(origin: CGPoint(), size: size) + transition.updateFrame(node: self.backgroundNode, frame: contentFrame) + if let effectView = self.effectView, effectView.frame != contentFrame { + transition.updateFrame(layer: effectView.layer, frame: contentFrame) + if let sublayers = effectView.layer.sublayers { + for sublayer in sublayers { + transition.updateFrame(layer: sublayer, frame: contentFrame) + } + } + } + + transition.updateCornerRadius(node: self.backgroundNode, cornerRadius: cornerRadius) + if let effectView = self.effectView { + transition.updateCornerRadius(layer: effectView.layer, cornerRadius: cornerRadius) + effectView.clipsToBounds = !cornerRadius.isZero + } + } +} + open class NavigationBar: ASDisplayNode { public static var defaultSecondaryContentHeight: CGFloat { return 38.0 @@ -120,7 +239,7 @@ open class NavigationBar: ASDisplayNode { var presentationData: NavigationBarPresentationData - private var validLayout: (CGSize, CGFloat, CGFloat, CGFloat, CGFloat, Bool)? + private var validLayout: (size: CGSize, defaultHeight: CGFloat, additionalTopHeight: CGFloat, additionalContentHeight: CGFloat, additionalBackgroundHeight: CGFloat, leftInset: CGFloat, rightInset: CGFloat, appearsHidden: Bool)? private var requestedLayout: Bool = false var requestContainerLayout: (ContainedViewLayoutTransition) -> Void = { _ in } @@ -269,7 +388,7 @@ open class NavigationBar: ASDisplayNode { private var title: String? { didSet { if let title = self.title { - self.titleNode.attributedText = NSAttributedString(string: title, font: Font.semibold(17.0), textColor: self.presentationData.theme.primaryTextColor) + self.titleNode.attributedText = NSAttributedString(string: title, font: titleFont, textColor: self.presentationData.theme.primaryTextColor) self.titleNode.accessibilityLabel = title if self.titleNode.supernode == nil { self.buttonsContainerNode.addSubnode(self.titleNode) @@ -307,40 +426,6 @@ open class NavigationBar: ASDisplayNode { var previousItemBackListenerKey: Int? private func updateAccessibilityElements() { - /*if !self.isNodeLoaded { - return - } - var accessibilityElements: [AnyObject] = [] - - if self.leftButtonNode.supernode != nil { - accessibilityElements.append(self.leftButtonNode) - } - if self.titleNode.supernode != nil { - accessibilityElements.append(self.titleNode) - } - if let titleView = self.titleView, titleView.superview != nil { - accessibilityElements.append(titleView) - } - if self.rightButtonNode.supernode != nil { - accessibilityElements.append(self.rightButtonNode) - } - - var updated = false - if let currentAccessibilityElements = self.accessibilityElements { - if currentAccessibilityElements.count != accessibilityElements.count { - updated = true - } else { - for i in 0 ..< accessibilityElements.count { - let element = currentAccessibilityElements[i] as AnyObject - if element !== accessibilityElements[i] { - updated = true - } - } - } - } - if updated { - self.accessibilityElements = accessibilityElements - }*/ } override open var accessibilityElements: [Any]? { @@ -518,15 +603,13 @@ open class NavigationBar: ASDisplayNode { self.leftButtonNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) } } else { - if animated { - if self.leftButtonNode.view.superview != nil { - if let snapshotView = self.leftButtonNode.view.snapshotContentTree() { - snapshotView.frame = self.leftButtonNode.frame - self.leftButtonNode.view.superview?.insertSubview(snapshotView, aboveSubview: self.leftButtonNode.view) - snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false, completion: { [weak snapshotView] _ in - snapshotView?.removeFromSuperview() - }) - } + if animated, self.leftButtonNode.view.superview != nil { + if let snapshotView = self.leftButtonNode.view.snapshotContentTree() { + snapshotView.frame = self.leftButtonNode.frame + self.leftButtonNode.view.superview?.insertSubview(snapshotView, aboveSubview: self.leftButtonNode.view) + snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false, completion: { [weak snapshotView] _ in + snapshotView?.removeFromSuperview() + }) } } self.leftButtonNode.removeFromSupernode() @@ -606,9 +689,27 @@ open class NavigationBar: ASDisplayNode { self.rightButtonNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) } } else { + if animated, self.rightButtonNode.view.superview != nil { + if let snapshotView = self.rightButtonNode.view.snapshotContentTree() { + snapshotView.frame = self.rightButtonNode.frame + self.rightButtonNode.view.superview?.insertSubview(snapshotView, aboveSubview: self.rightButtonNode.view) + snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false, completion: { [weak snapshotView] _ in + snapshotView?.removeFromSuperview() + }) + } + } self.rightButtonNode.removeFromSupernode() } } else { + if animated, self.rightButtonNode.view.superview != nil { + if let snapshotView = self.rightButtonNode.view.snapshotContentTree() { + snapshotView.frame = self.rightButtonNode.frame + self.rightButtonNode.view.superview?.insertSubview(snapshotView, aboveSubview: self.rightButtonNode.view) + snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false, completion: { [weak snapshotView] _ in + snapshotView?.removeFromSuperview() + }) + } + } self.rightButtonNode.removeFromSupernode() } @@ -617,12 +718,20 @@ open class NavigationBar: ASDisplayNode { } self.updateAccessibilityElements() } - + + public let backgroundNode: NavigationBackgroundNode public let backButtonNode: NavigationButtonNode public let badgeNode: NavigationBarBadgeNode public let backButtonArrow: ASImageNode public let leftButtonNode: NavigationButtonNode public let rightButtonNode: NavigationButtonNode + public let additionalContentNode: SparseNode + + public func reattachAdditionalContentNode() { + if self.additionalContentNode.supernode !== self { + self.insertSubnode(self.additionalContentNode, aboveSubnode: self.clippingNode) + } + } private var _transitionState: NavigationBarTransitionState? var transitionState: NavigationBarTransitionState? { @@ -728,17 +837,23 @@ open class NavigationBar: ASDisplayNode { 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) + self.titleNode.attributedText = NSAttributedString(string: title, font: titleFont, textColor: self.presentationData.theme.primaryTextColor) self.titleNode.accessibilityLabel = title } self.stripeNode.backgroundColor = self.presentationData.theme.separatorColor + + self.backgroundNode = NavigationBackgroundNode(color: self.presentationData.theme.backgroundColor, enableBlur: self.presentationData.theme.enableBackgroundBlur) + self.additionalContentNode = SparseNode() super.init() - + + self.addSubnode(self.backgroundNode) self.addSubnode(self.buttonsContainerNode) self.addSubnode(self.clippingNode) - - self.backgroundColor = self.presentationData.theme.backgroundColor + self.addSubnode(self.additionalContentNode) + + self.backgroundColor = nil + self.isOpaque = false self.stripeNode.isLayerBacked = true self.stripeNode.displaysAsynchronously = false @@ -793,7 +908,7 @@ open class NavigationBar: ASDisplayNode { if presentationData.theme !== self.presentationData.theme || presentationData.strings !== self.presentationData.strings { self.presentationData = presentationData - self.backgroundColor = self.presentationData.theme.backgroundColor + self.backgroundNode.updateColor(color: self.presentationData.theme.backgroundColor, transition: .immediate) self.backButtonNode.color = self.presentationData.theme.buttonColor self.backButtonNode.disabledColor = self.presentationData.theme.disabledButtonColor @@ -804,7 +919,7 @@ open class NavigationBar: ASDisplayNode { 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) + self.titleNode.attributedText = NSAttributedString(string: title, font: titleFont, textColor: self.presentationData.theme.primaryTextColor) self.titleNode.accessibilityLabel = title } self.stripeNode.backgroundColor = self.presentationData.theme.separatorColor @@ -825,16 +940,22 @@ open class NavigationBar: ASDisplayNode { if let validLayout = self.validLayout, self.requestedLayout { self.requestedLayout = false - self.updateLayout(size: validLayout.0, defaultHeight: validLayout.1, additionalHeight: validLayout.2, leftInset: validLayout.3, rightInset: validLayout.4, appearsHidden: validLayout.5, transition: .immediate) + self.updateLayout(size: validLayout.size, defaultHeight: validLayout.defaultHeight, additionalTopHeight: validLayout.additionalTopHeight, additionalContentHeight: validLayout.additionalContentHeight, additionalBackgroundHeight: validLayout.additionalBackgroundHeight, leftInset: validLayout.leftInset, rightInset: validLayout.rightInset, appearsHidden: validLayout.appearsHidden, transition: .immediate) } } - func updateLayout(size: CGSize, defaultHeight: CGFloat, additionalHeight: CGFloat, leftInset: CGFloat, rightInset: CGFloat, appearsHidden: Bool, transition: ContainedViewLayoutTransition) { + func updateLayout(size: CGSize, defaultHeight: CGFloat, additionalTopHeight: CGFloat, additionalContentHeight: CGFloat, additionalBackgroundHeight: CGFloat, leftInset: CGFloat, rightInset: CGFloat, appearsHidden: Bool, transition: ContainedViewLayoutTransition) { if self.layoutSuspended { return } - self.validLayout = (size, defaultHeight, additionalHeight, leftInset, rightInset, appearsHidden) + self.validLayout = (size, defaultHeight, additionalTopHeight, additionalContentHeight, additionalBackgroundHeight, leftInset, rightInset, appearsHidden) + + let backgroundFrame = CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.height + additionalBackgroundHeight)) + if self.backgroundNode.frame != backgroundFrame { + transition.updateFrame(node: self.backgroundNode, frame: backgroundFrame) + self.backgroundNode.update(size: backgroundFrame.size, transition: transition) + } let apparentAdditionalHeight: CGFloat = self.secondaryContentNode != nil ? NavigationBar.defaultSecondaryContentHeight : 0.0 @@ -842,6 +963,7 @@ open class NavigationBar: ASDisplayNode { let backButtonInset: CGFloat = leftInset + 27.0 transition.updateFrame(node: self.clippingNode, frame: CGRect(origin: CGPoint(), size: size)) + transition.updateFrame(node: self.additionalContentNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.height + additionalBackgroundHeight))) transition.updateFrame(node: self.buttonsContainerNode, frame: CGRect(origin: CGPoint(), size: size)) var expansionHeight: CGFloat = 0.0 if let contentNode = self.contentNode { @@ -849,12 +971,12 @@ open class NavigationBar: ASDisplayNode { switch contentNode.mode { case .replacement: expansionHeight = contentNode.height - defaultHeight - contentNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height)) + contentNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height - additionalContentHeight)) case .expansion: expansionHeight = contentNode.height let additionalExpansionHeight: CGFloat = self.secondaryContentNode != nil && appearsHidden ? NavigationBar.defaultSecondaryContentHeight : 0.0 - contentNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: size.height - expansionHeight - apparentAdditionalHeight - additionalExpansionHeight), size: CGSize(width: size.width, height: expansionHeight)) + contentNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: size.height - (appearsHidden ? 0.0 : additionalContentHeight) - expansionHeight - apparentAdditionalHeight - additionalExpansionHeight), size: CGSize(width: size.width, height: expansionHeight)) if appearsHidden { if self.secondaryContentNode != nil { contentNodeFrame.origin.y += NavigationBar.defaultSecondaryContentHeight @@ -865,10 +987,10 @@ open class NavigationBar: ASDisplayNode { contentNode.updateLayout(size: contentNodeFrame.size, leftInset: leftInset, rightInset: rightInset, transition: transition) } - transition.updateFrame(node: self.stripeNode, frame: CGRect(x: 0.0, y: size.height, width: size.width, height: UIScreenPixel)) + transition.updateFrame(node: self.stripeNode, frame: CGRect(x: 0.0, y: size.height + additionalBackgroundHeight, width: size.width, height: UIScreenPixel)) - let nominalHeight: CGFloat = defaultHeight - additionalHeight - let contentVerticalOrigin = size.height - nominalHeight - expansionHeight - additionalHeight - apparentAdditionalHeight + let nominalHeight: CGFloat = defaultHeight + let contentVerticalOrigin = additionalTopHeight var leftTitleInset: CGFloat = leftInset + 1.0 var rightTitleInset: CGFloat = rightInset + 1.0 @@ -1012,12 +1134,18 @@ 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: floor((size.width - titleSize.width) / 2.0), y: contentVerticalOrigin + floorToScreenPixels((nominalHeight - titleSize.height) / 2.0)), size: titleSize) - transition.updateFrame(view: titleView, frame: titleFrame) + var titleViewTransition = transition + if titleView.frame.isEmpty { + titleViewTransition = .immediate + titleView.frame = titleFrame + } + + titleViewTransition.updateFrame(view: titleView, frame: titleFrame) if let titleView = titleView as? NavigationBarTitleView { let titleWidth = size.width - (leftTitleInset > 0.0 ? leftTitleInset : rightTitleInset) - (rightTitleInset > 0.0 ? rightTitleInset : leftTitleInset) - titleView.updateLayout(size: titleFrame.size, clearBounds: CGRect(origin: CGPoint(x: leftTitleInset - titleFrame.minX, y: 0.0), size: CGSize(width: titleWidth, height: titleFrame.height)), transition: transition) + titleView.updateLayout(size: titleFrame.size, clearBounds: CGRect(origin: CGPoint(x: leftTitleInset - titleFrame.minX, y: 0.0), size: CGSize(width: titleWidth, height: titleFrame.height)), transition: titleViewTransition) } if let transitionState = self.transitionState, let otherNavigationBar = transitionState.navigationBar { @@ -1063,7 +1191,7 @@ open class NavigationBar: ASDisplayNode { } } else if let title = self.title { let node = ImmediateTextNode() - node.attributedText = NSAttributedString(string: title, font: Font.semibold(17.0), textColor: foregroundColor) + node.attributedText = NSAttributedString(string: title, font: titleFont, textColor: foregroundColor) return node } else { return nil @@ -1075,8 +1203,8 @@ open class NavigationBar: ASDisplayNode { let node = NavigationButtonNode() node.updateManualText(self.backButtonNode.manualText) node.color = accentColor - if let (size, defaultHeight, _, _, _, _) = self.validLayout { - let _ = node.updateLayout(constrainedSize: CGSize(width: size.width, height: defaultHeight)) + if let validLayout = self.validLayout { + let _ = node.updateLayout(constrainedSize: CGSize(width: validLayout.size.width, height: validLayout.defaultHeight)) node.frame = self.backButtonNode.frame } return node @@ -1098,8 +1226,8 @@ open class NavigationBar: ASDisplayNode { } node.updateItems(items) node.color = accentColor - if let (size, defaultHeight, _, _, _, _) = self.validLayout { - let _ = node.updateLayout(constrainedSize: CGSize(width: size.width, height: defaultHeight)) + if let validLayout = self.validLayout { + let _ = node.updateLayout(constrainedSize: CGSize(width: validLayout.size.width, height: validLayout.defaultHeight)) node.frame = self.backButtonNode.frame } return node @@ -1263,15 +1391,10 @@ 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) - } - } - }*/ - + if let result = self.additionalContentNode.view.hitTest(self.view.convert(point, to: self.additionalContentNode.view), with: event) { + return result + } + guard let result = super.hitTest(point, with: event) else { return nil } diff --git a/submodules/Display/Source/NavigationButtonNode.swift b/submodules/Display/Source/NavigationButtonNode.swift index 43b7f473c2..4ad1cd4318 100644 --- a/submodules/Display/Source/NavigationButtonNode.swift +++ b/submodules/Display/Source/NavigationButtonNode.swift @@ -225,7 +225,7 @@ private final class NavigationButtonItemNode: ImmediateTextNode { return size } 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)) + let size = CGSize(width: max(nodeSize.width, superSize.width), height: max(44.0, max(nodeSize.height, superSize.height))) 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 @@ -266,7 +266,11 @@ 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 { + var touchInside = true + if let touch = touches.first { + touchInside = self.touchInsideApparentBounds(touch) + } + if previousTouchCount != 0 && self.touchCount == 0 && self.isEnabled && touchInside { self.pressed() } } diff --git a/submodules/Display/Source/PeekControllerMenuItemNode.swift b/submodules/Display/Source/PeekControllerMenuItemNode.swift deleted file mode 100644 index c1c872986e..0000000000 --- a/submodules/Display/Source/PeekControllerMenuItemNode.swift +++ /dev/null @@ -1,108 +0,0 @@ -import Foundation -import UIKit -import AsyncDisplayKit - -public enum PeekControllerMenuItemColor { - case accent - case destructive -} - -public enum PeekControllerMenuItemFont { - case `default` - case bold -} - -public struct PeekControllerMenuItem { - public let title: String - public let color: PeekControllerMenuItemColor - public let font: PeekControllerMenuItemFont - public let action: (ASDisplayNode, CGRect) -> Bool - - public init(title: String, color: PeekControllerMenuItemColor, font: PeekControllerMenuItemFont = .default, action: @escaping (ASDisplayNode, CGRect) -> Bool) { - self.title = title - self.color = color - self.font = font - self.action = action - } -} - -final class PeekControllerMenuItemNode: HighlightTrackingButtonNode { - private let item: PeekControllerMenuItem - private let activatedAction: () -> Void - - private let separatorNode: ASDisplayNode - private let highlightedBackgroundNode: ASDisplayNode - private let textNode: ImmediateTextNode - - init(theme: PeekControllerTheme, item: PeekControllerMenuItem, activatedAction: @escaping () -> Void) { - self.item = item - self.activatedAction = activatedAction - - self.separatorNode = ASDisplayNode() - self.separatorNode.isLayerBacked = true - self.separatorNode.backgroundColor = theme.menuItemSeparatorColor - - self.highlightedBackgroundNode = ASDisplayNode() - self.highlightedBackgroundNode.isLayerBacked = true - self.highlightedBackgroundNode.backgroundColor = theme.menuItemHighligtedColor - self.highlightedBackgroundNode.alpha = 0.0 - - self.textNode = ImmediateTextNode() - self.textNode.isUserInteractionEnabled = false - self.textNode.displaysAsynchronously = false - - let textColor: UIColor - let textFont: UIFont - switch item.color { - case .accent: - textColor = theme.accentColor - case .destructive: - textColor = theme.destructiveColor - } - switch item.font { - case .default: - textFont = Font.regular(20.0) - case .bold: - textFont = Font.medium(20.0) - } - self.textNode.attributedText = NSAttributedString(string: item.title, font: textFont, textColor: textColor) - - super.init() - - self.addSubnode(self.separatorNode) - self.addSubnode(self.highlightedBackgroundNode) - self.addSubnode(self.textNode) - - self.highligthedChanged = { [weak self] highlighted in - if let strongSelf = self { - if highlighted { - strongSelf.view.superview?.bringSubviewToFront(strongSelf.view) - strongSelf.highlightedBackgroundNode.alpha = 1.0 - } else { - strongSelf.highlightedBackgroundNode.alpha = 0.0 - strongSelf.highlightedBackgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3) - } - } - } - - self.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside) - } - - func updateLayout(width: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat { - let height: CGFloat = 57.0 - transition.updateFrame(node: self.highlightedBackgroundNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: width, height: height))) - transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: height), size: CGSize(width: width, height: UIScreenPixel))) - - let textSize = self.textNode.updateLayout(CGSize(width: width - 10.0, height: height)) - transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: floor((width - textSize.width) / 2.0), y: floor((height - textSize.height) / 2.0)), size: textSize)) - - return height - } - - @objc func buttonPressed() { - self.activatedAction() - if self.item.action(self, self.bounds) { - - } - } -} diff --git a/submodules/Display/Source/PeekControllerMenuNode.swift b/submodules/Display/Source/PeekControllerMenuNode.swift deleted file mode 100644 index 6b7e5c4fdb..0000000000 --- a/submodules/Display/Source/PeekControllerMenuNode.swift +++ /dev/null @@ -1,31 +0,0 @@ -import Foundation -import UIKit -import AsyncDisplayKit - -final class PeekControllerMenuNode: ASDisplayNode { - private let itemNodes: [PeekControllerMenuItemNode] - - init(theme: PeekControllerTheme, items: [PeekControllerMenuItem], activatedAction: @escaping () -> Void) { - self.itemNodes = items.map { PeekControllerMenuItemNode(theme: theme, item: $0, activatedAction: activatedAction) } - - super.init() - - self.backgroundColor = theme.menuBackgroundColor - self.cornerRadius = 16.0 - self.clipsToBounds = true - - for itemNode in self.itemNodes { - self.addSubnode(itemNode) - } - } - - func updateLayout(width: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat { - var verticalOffset: CGFloat = 0.0 - for itemNode in self.itemNodes { - let itemHeight = itemNode.updateLayout(width: width, transition: transition) - transition.updateFrame(node: itemNode, frame: CGRect(origin: CGPoint(x: 0.0, y: verticalOffset), size: CGSize(width: width, height: itemHeight))) - verticalOffset += itemHeight - } - return verticalOffset - UIScreenPixel - } -} diff --git a/submodules/Display/Source/PeekControllerNode.swift b/submodules/Display/Source/PeekControllerNode.swift deleted file mode 100644 index fabe9c53b9..0000000000 --- a/submodules/Display/Source/PeekControllerNode.swift +++ /dev/null @@ -1,357 +0,0 @@ -import Foundation -import UIKit -import AsyncDisplayKit - -final class PeekControllerNode: ViewControllerTracingNode { - private let requestDismiss: () -> Void - - private let theme: PeekControllerTheme - - private let blurView: UIView - private let dimNode: ASDisplayNode - private let containerBackgroundNode: ASImageNode - private let containerNode: ASDisplayNode - - private var validLayout: ContainerViewLayout? - private var containerOffset: CGFloat = 0.0 - private var panInitialContainerOffset: CGFloat? - - private var content: PeekControllerContent - private var contentNode: PeekControllerContentNode & ASDisplayNode - private var contentNodeHasValidLayout = false - - private var topAccessoryNode: ASDisplayNode? - - private var menuNode: PeekControllerMenuNode? - private var displayingMenu = false - - private var hapticFeedback: HapticFeedback? - - init(theme: PeekControllerTheme, content: PeekControllerContent, requestDismiss: @escaping () -> Void) { - self.theme = theme - self.requestDismiss = requestDismiss - - self.dimNode = ASDisplayNode() - self.blurView = UIVisualEffectView(effect: UIBlurEffect(style: theme.isDark ? .dark : .light)) - self.blurView.isUserInteractionEnabled = false - - switch content.menuActivation() { - case .drag: - self.dimNode.backgroundColor = nil - self.blurView.alpha = 1.0 - case .press: - self.dimNode.backgroundColor = UIColor(white: theme.isDark ? 0.0 : 1.0, alpha: 0.5) - self.blurView.alpha = 0.0 - } - - self.containerBackgroundNode = ASImageNode() - self.containerBackgroundNode.isLayerBacked = true - self.containerBackgroundNode.displaysAsynchronously = false - - self.containerNode = ASDisplayNode() - - self.content = content - self.contentNode = content.node() - self.topAccessoryNode = content.topAccessoryNode() - - var activatedActionImpl: (() -> Void)? - let menuItems = content.menuItems() - if menuItems.isEmpty { - self.menuNode = nil - } else { - self.menuNode = PeekControllerMenuNode(theme: theme, items: menuItems, activatedAction: { - activatedActionImpl?() - }) - } - - super.init() - - if content.presentation() == .freeform { - self.containerNode.isUserInteractionEnabled = false - } else { - self.containerNode.clipsToBounds = true - self.containerNode.cornerRadius = 16.0 - } - - self.addSubnode(self.dimNode) - self.view.addSubview(self.blurView) - self.containerNode.addSubnode(self.contentNode) - self.addSubnode(self.containerNode) - - if let topAccessoryNode = self.topAccessoryNode { - self.addSubnode(topAccessoryNode) - } - - if let menuNode = self.menuNode { - self.addSubnode(menuNode) - } - - activatedActionImpl = { [weak self] in - self?.requestDismiss() - } - - self.hapticFeedback = HapticFeedback() - self.hapticFeedback?.prepareTap() - } - - deinit { - } - - override func didLoad() { - super.didLoad() - - self.dimNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimNodeTap(_:)))) - self.view.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:)))) - } - - func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { - self.validLayout = layout - - transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(), size: layout.size)) - transition.updateFrame(view: self.blurView, frame: CGRect(origin: CGPoint(), size: layout.size)) - - var layoutInsets = layout.insets(options: []) - let containerWidth = horizontalContainerFillingSizeForLayout(layout: layout, sideInset: layout.safeInsets.left) - - layoutInsets.left = floor((layout.size.width - containerWidth) / 2.0) - layoutInsets.right = layoutInsets.left - if !layoutInsets.bottom.isZero { - layoutInsets.bottom -= 12.0 - } - - let maxContainerSize = CGSize(width: layout.size.width - 14.0 * 2.0, height: layout.size.height - layoutInsets.top - layoutInsets.bottom - 90.0) - - var menuSize: CGSize? - - let contentSize = self.contentNode.updateLayout(size: maxContainerSize, transition: self.contentNodeHasValidLayout ? transition : .immediate) - if self.contentNodeHasValidLayout { - transition.updateFrame(node: self.contentNode, frame: CGRect(origin: CGPoint(), size: contentSize)) - } else { - self.contentNode.frame = CGRect(origin: CGPoint(), size: contentSize) - } - - var containerFrame: CGRect - switch self.content.presentation() { - case .contained: - containerFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - contentSize.width) / 2.0), y: floor((layout.size.height - contentSize.height) / 2.0)), size: contentSize) - case .freeform: - containerFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - contentSize.width) / 2.0), y: floor((layout.size.height - contentSize.height) / 4.0)), size: contentSize) - } - - if let menuNode = self.menuNode { - let menuWidth = layout.size.width - layoutInsets.left - layoutInsets.right - 14.0 * 2.0 - let menuHeight = menuNode.updateLayout(width: menuWidth, transition: transition) - menuSize = CGSize(width: menuWidth, height: menuHeight) - - if self.displayingMenu { - let upperBound = layout.size.height - layoutInsets.bottom - menuHeight - 14.0 * 2.0 - containerFrame.height - if containerFrame.origin.y > upperBound { - containerFrame.origin.y = upperBound - } - - transition.updateAlpha(layer: self.blurView.layer, alpha: 1.0) - } - } - - if self.displayingMenu { - var offset = self.containerOffset - let delta = abs(offset) - let factor: CGFloat = 60.0 - offset = (-((1.0 - (1.0 / (((delta) * 0.55 / (factor)) + 1.0))) * factor)) * (offset < 0.0 ? 1.0 : -1.0) - containerFrame = containerFrame.offsetBy(dx: 0.0, dy: offset) - } else { - containerFrame = containerFrame.offsetBy(dx: 0.0, dy: self.containerOffset) - } - - transition.updateFrame(node: self.containerNode, frame: containerFrame) - - if let topAccessoryNode = self.topAccessoryNode { - let accessorySize = topAccessoryNode.frame.size - let accessoryFrame = CGRect(origin: CGPoint(x: floorToScreenPixels(containerFrame.midX - accessorySize.width / 2.0), y: containerFrame.minY - accessorySize.height - 16.0), size: accessorySize) - transition.updateFrame(node: topAccessoryNode, frame: accessoryFrame) - transition.updateAlpha(node: topAccessoryNode, alpha: self.displayingMenu ? 0.0 : 1.0) - } - - if let menuNode = self.menuNode, let menuSize = menuSize { - let menuY: CGFloat - if self.displayingMenu { - menuY = max(containerFrame.maxY + 14.0, layout.size.height - layoutInsets.bottom - 14.0 - menuSize.height) - } else { - menuY = layout.size.height + 14.0 - } - - let menuFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - menuSize.width) / 2.0), y: menuY), size: menuSize) - - if self.contentNodeHasValidLayout { - transition.updateFrame(node: menuNode, frame: menuFrame) - } else { - menuNode.frame = menuFrame - } - } - - self.contentNodeHasValidLayout = true - } - - func animateIn(from rect: CGRect) { - self.dimNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) - self.blurView.layer.animateAlpha(from: 0.0, to: self.blurView.alpha, duration: 0.3) - - let offset = CGPoint(x: rect.midX - self.containerNode.position.x, y: rect.midY - self.containerNode.position.y) - self.containerNode.layer.animateSpring(from: NSValue(cgPoint: offset), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: 0.4, initialVelocity: 0.0, damping: 110.0, additive: true) - self.containerNode.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.4, initialVelocity: 0.0, damping: 110.0) - self.containerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) - - if let topAccessoryNode = self.topAccessoryNode { - topAccessoryNode.layer.animateSpring(from: NSValue(cgPoint: offset), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: 0.4, initialVelocity: 0.0, damping: 110.0, additive: true) - topAccessoryNode.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.4, initialVelocity: 0.0, damping: 110.0) - topAccessoryNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) - } - - if case .press = self.content.menuActivation() { - self.hapticFeedback?.tap() - } else { - self.hapticFeedback?.impact() - } - } - - func animateOut(to rect: CGRect, completion: @escaping () -> Void) { - self.dimNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) - self.blurView.layer.animateAlpha(from: self.blurView.alpha, to: 0.0, duration: 0.25, removeOnCompletion: false) - - let offset = CGPoint(x: rect.midX - self.containerNode.position.x, y: rect.midY - self.containerNode.position.y) - self.containerNode.layer.animatePosition(from: CGPoint(), to: offset, duration: 0.25, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true, force: true, completion: { _ in - completion() - }) - self.containerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) - self.containerNode.layer.animateScale(from: 1.0, to: 0.1, duration: 0.25, removeOnCompletion: false) - - if let topAccessoryNode = self.topAccessoryNode { - topAccessoryNode.layer.animatePosition(from: CGPoint(), to: offset, duration: 0.25, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true, force: true, completion: { _ in - completion() - }) - topAccessoryNode.layer.animateAlpha(from: topAccessoryNode.alpha, to: 0.0, duration: 0.2, removeOnCompletion: false) - topAccessoryNode.layer.animateScale(from: 1.0, to: 0.1, duration: 0.25, removeOnCompletion: false) - } - - if let menuNode = self.menuNode { - menuNode.layer.animatePosition(from: menuNode.position, to: CGPoint(x: menuNode.position.x, y: self.bounds.size.height + menuNode.bounds.size.height / 2.0), duration: 0.25, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false) - } - } - - @objc func dimNodeTap(_ recognizer: UITapGestureRecognizer) { - if case .ended = recognizer.state { - self.requestDismiss() - } - } - - @objc func panGesture(_ recognizer: UIPanGestureRecognizer) { - guard case .drag = self.content.menuActivation() else { - return - } - - switch recognizer.state { - case .began: - self.panInitialContainerOffset = self.containerOffset - case .changed: - if let panInitialContainerOffset = self.panInitialContainerOffset { - let translation = recognizer.translation(in: self.view) - var offset = panInitialContainerOffset + translation.y - if offset < 0.0 { - let delta = abs(offset) - let factor: CGFloat = 60.0 - offset = (-((1.0 - (1.0 / (((delta) * 0.55 / (factor)) + 1.0))) * factor)) * (offset < 0.0 ? 1.0 : -1.0) - } - self.applyDraggingOffset(offset) - } - case .cancelled, .ended: - if let _ = self.panInitialContainerOffset { - self.panInitialContainerOffset = nil - if self.containerOffset < 0.0 { - self.activateMenu() - } else { - self.requestDismiss() - } - } - default: - break - } - } - - func applyDraggingOffset(_ offset: CGFloat) { - self.containerOffset = offset - if self.containerOffset < -25.0 { - //self.displayingMenu = true - } else { - //self.displayingMenu = false - } - if let layout = self.validLayout { - self.containerLayoutUpdated(layout, transition: .immediate) - } - } - - func activateMenu() { - if case .press = self.content.menuActivation() { - self.hapticFeedback?.impact() - } - if let layout = self.validLayout { - self.displayingMenu = true - self.containerOffset = 0.0 - self.containerLayoutUpdated(layout, transition: .animated(duration: 0.18, curve: .spring)) - } - } - - func endDraggingWithVelocity(_ velocity: CGFloat) { - if let _ = self.menuNode, velocity < -600.0 || self.containerOffset < -38.0 { - if let layout = self.validLayout { - self.displayingMenu = true - self.containerOffset = 0.0 - self.containerLayoutUpdated(layout, transition: .animated(duration: 0.18, curve: .spring)) - } - } else { - self.requestDismiss() - } - } - - func updateContent(content: PeekControllerContent) { - let contentNode = self.contentNode - contentNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false, completion: { [weak contentNode] _ in - contentNode?.removeFromSupernode() - }) - contentNode.layer.animateScale(from: 1.0, to: 0.2, duration: 0.15, removeOnCompletion: false) - - self.menuNode?.removeFromSupernode() - self.menuNode = nil - - self.content = content - self.contentNode = content.node() - self.containerNode.addSubnode(self.contentNode) - self.contentNodeHasValidLayout = false - - var activatedActionImpl: (() -> Void)? - let menuItems = content.menuItems() - if menuItems.isEmpty { - self.menuNode = nil - } else { - self.menuNode = PeekControllerMenuNode(theme: self.theme, items: menuItems, activatedAction: { - activatedActionImpl?() - }) - } - - if let menuNode = self.menuNode { - self.addSubnode(menuNode) - } - - activatedActionImpl = { [weak self] in - self?.requestDismiss() - } - - self.contentNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1) - self.contentNode.layer.animateSpring(from: 0.35 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.5) - - if let layout = self.validLayout { - self.containerLayoutUpdated(layout, transition: .animated(duration: 0.15, curve: .easeInOut)) - } - - self.hapticFeedback?.tap() - } -} diff --git a/submodules/Display/Source/StatusBarHost.swift b/submodules/Display/Source/StatusBarHost.swift index be038af9fb..8c34f16999 100644 --- a/submodules/Display/Source/StatusBarHost.swift +++ b/submodules/Display/Source/StatusBarHost.swift @@ -12,4 +12,6 @@ public protocol StatusBarHost { func setStatusBarStyle(_ style: UIStatusBarStyle, animated: Bool) func setStatusBarHidden(_ value: Bool, animated: Bool) + + var shouldChangeStatusBarStyle: ((UIStatusBarStyle) -> Bool)? { get set } } diff --git a/submodules/Display/Source/TabBarContollerNode.swift b/submodules/Display/Source/TabBarContollerNode.swift index 50757aa762..93bf14dac1 100644 --- a/submodules/Display/Source/TabBarContollerNode.swift +++ b/submodules/Display/Source/TabBarContollerNode.swift @@ -12,7 +12,6 @@ final class TabBarControllerNode: ASDisplayNode { private var theme: TabBarControllerTheme let tabBarNode: TabBarNode private let disabledOverlayNode: ASDisplayNode - private let navigationBar: NavigationBar? private var toolbarNode: ToolbarNode? private let toolbarActionSelected: (ToolbarActionOption) -> Void private let disabledPressed: () -> Void @@ -27,9 +26,8 @@ final class TabBarControllerNode: ASDisplayNode { } } - init(theme: TabBarControllerTheme, navigationBar: NavigationBar?, itemSelected: @escaping (Int, Bool, [ASDisplayNode]) -> Void, contextAction: @escaping (Int, ContextExtractedContentContainingNode, ContextGesture) -> Void, swipeAction: @escaping (Int, TabBarItemSwipeDirection) -> Void, toolbarActionSelected: @escaping (ToolbarActionOption) -> Void, disabledPressed: @escaping () -> Void) { + init(theme: TabBarControllerTheme, itemSelected: @escaping (Int, Bool, [ASDisplayNode]) -> Void, contextAction: @escaping (Int, ContextExtractedContentContainingNode, ContextGesture) -> Void, swipeAction: @escaping (Int, TabBarItemSwipeDirection) -> Void, toolbarActionSelected: @escaping (ToolbarActionOption) -> Void, disabledPressed: @escaping () -> Void) { self.theme = theme - self.navigationBar = navigationBar self.tabBarNode = TabBarNode(theme: theme, itemSelected: itemSelected, contextAction: contextAction, swipeAction: swipeAction) self.disabledOverlayNode = ASDisplayNode() self.disabledOverlayNode.backgroundColor = theme.backgroundColor.withAlphaComponent(0.5) diff --git a/submodules/Display/Source/TabBarController.swift b/submodules/Display/Source/TabBarController.swift index 6adad68208..64665cd01b 100644 --- a/submodules/Display/Source/TabBarController.swift +++ b/submodules/Display/Source/TabBarController.swift @@ -87,7 +87,6 @@ open class TabBarController: ViewController { } } - public private(set) var controllers: [ViewController] = [] private let _ready = Promise() @@ -115,14 +114,6 @@ open class TabBarController: ViewController { var currentController: ViewController? - open override var navigationBarRequiresEntireLayoutUpdate: Bool { - if let currentController = currentController { - return currentController.navigationBarRequiresEntireLayoutUpdate - } else { - return false - } - } - private let pendingControllerDisposable = MetaDisposable() private var theme: TabBarControllerTheme @@ -130,7 +121,7 @@ open class TabBarController: ViewController { public init(navigationBarPresentationData: NavigationBarPresentationData, theme: TabBarControllerTheme) { self.theme = theme - super.init(navigationBarPresentationData: navigationBarPresentationData) + super.init(navigationBarPresentationData: nil) self.scrollToTop = { [weak self] in guard let strongSelf = self else { @@ -151,7 +142,6 @@ open class TabBarController: ViewController { } public func updateTheme(navigationBarPresentationData: NavigationBarPresentationData, theme: TabBarControllerTheme) { - self.navigationBar?.updatePresentationData(navigationBarPresentationData) if self.theme !== theme { self.theme = theme if self.isNodeLoaded { @@ -193,7 +183,7 @@ open class TabBarController: ViewController { } override open func loadDisplayNode() { - self.displayNode = TabBarControllerNode(theme: self.theme, navigationBar: self.navigationBar, itemSelected: { [weak self] index, longTap, itemNodes in + self.displayNode = TabBarControllerNode(theme: self.theme, itemSelected: { [weak self] index, longTap, itemNodes in if let strongSelf = self { if longTap, let controller = strongSelf.controllers[index] as? TabBarContainedController { controller.presentTabBarPreviewingController(sourceNodes: itemNodes) @@ -302,37 +292,16 @@ open class TabBarController: ViewController { if let _selectedIndex = self._selectedIndex, _selectedIndex < self.controllers.count { self.currentController = self.controllers[_selectedIndex] } - - var displayNavigationBar = false + if let currentController = self.currentController { currentController.willMove(toParent: self) self.tabBarControllerNode.currentControllerNode = currentController.displayNode - currentController.navigationBar?.isHidden = true self.addChild(currentController) currentController.didMove(toParent: self) - - currentController.navigationBar?.layoutSuspended = true - currentController.navigationItem.setTarget(self.navigationItem) - displayNavigationBar = currentController.displayNavigationBar - self.navigationBar?.setContentNode(currentController.navigationBar?.contentNode, animated: false) - self.navigationBar?.setSecondaryContentNode(currentController.navigationBar?.secondaryContentNode) + currentController.displayNode.recursivelyEnsureDisplaySynchronously(true) self.statusBar.statusBarStyle = currentController.statusBar.statusBarStyle - if let navigationBarPresentationData = currentController.navigationBar?.presentationData { - self.navigationBar?.updatePresentationData(navigationBarPresentationData) - } } else { - self.navigationItem.title = nil - self.navigationItem.leftBarButtonItem = nil - self.navigationItem.rightBarButtonItem = nil - self.navigationItem.titleView = nil - self.navigationItem.backBarButtonItem = nil - self.navigationBar?.setContentNode(nil, animated: false) - self.navigationBar?.setSecondaryContentNode(nil) - displayNavigationBar = false - } - if self.displayNavigationBar != displayNavigationBar { - self.setDisplayNavigationBar(displayNavigationBar) } if let layout = self.validLayout { diff --git a/submodules/Display/Source/TabBarNode.swift b/submodules/Display/Source/TabBarNode.swift index 5e6372d208..291333a8e4 100644 --- a/submodules/Display/Source/TabBarNode.swift +++ b/submodules/Display/Source/TabBarNode.swift @@ -331,7 +331,8 @@ class TabBarNode: ASDisplayNode { private var centered: Bool = false private var badgeImage: UIImage - + + let backgroundNode: NavigationBackgroundNode let separatorNode: ASDisplayNode private var tabBarNodeContainers: [TabBarNodeContainer] = [] @@ -342,6 +343,8 @@ class TabBarNode: ASDisplayNode { self.contextAction = contextAction self.swipeAction = swipeAction self.theme = theme + + self.backgroundNode = NavigationBackgroundNode(color: theme.tabBarBackgroundColor) self.separatorNode = ASDisplayNode() self.separatorNode.backgroundColor = theme.tabBarSeparatorColor @@ -354,9 +357,10 @@ class TabBarNode: ASDisplayNode { self.isAccessibilityContainer = false - self.isOpaque = true - self.backgroundColor = theme.tabBarBackgroundColor - + self.isOpaque = false + self.backgroundColor = nil + + self.addSubnode(self.backgroundNode) self.addSubnode(self.separatorNode) } @@ -389,7 +393,7 @@ class TabBarNode: ASDisplayNode { self.theme = theme self.separatorNode.backgroundColor = theme.tabBarSeparatorColor - self.backgroundColor = theme.tabBarBackgroundColor + self.backgroundNode.updateColor(color: theme.tabBarBackgroundColor, transition: .immediate) self.badgeImage = generateStretchableFilledCircleImage(diameter: 18.0, color: theme.tabBarBadgeBackgroundColor, strokeColor: theme.tabBarBadgeStrokeColor, strokeWidth: 1.0, backgroundColor: nil)! for container in self.tabBarNodeContainers { @@ -539,6 +543,9 @@ class TabBarNode: ASDisplayNode { func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat, additionalSideInsets: UIEdgeInsets, bottomInset: CGFloat, transition: ContainedViewLayoutTransition) { self.validLayout = (size, leftInset, rightInset, additionalSideInsets, bottomInset) + + transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(), size: size)) + self.backgroundNode.update(size: size, transition: transition) transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -separatorHeight), size: CGSize(width: size.width, height: separatorHeight))) diff --git a/submodules/Display/Source/TextNode.swift b/submodules/Display/Source/TextNode.swift index e010783205..3d36be6ece 100644 --- a/submodules/Display/Source/TextNode.swift +++ b/submodules/Display/Source/TextNode.swift @@ -242,6 +242,14 @@ public final class TextNodeLayout: NSObject { return 0.0 } } + + public var trailingLineIsRTL: Bool { + if let lastLine = self.lines.last { + return lastLine.isRTL + } else { + return false + } + } public func attributesAtPoint(_ point: CGPoint, orNearest: Bool) -> (Int, [NSAttributedString.Key: Any])? { if let attributedString = self.attributedString { @@ -1017,7 +1025,14 @@ public class TextNode: ASDisplayNode { layoutSize.height += fontLineSpacing } - let lineRange = CFRangeMake(lastLineCharacterIndex, lineCharacterCount) + var lineRange = CFRangeMake(lastLineCharacterIndex, lineCharacterCount) + if lineRange.location + lineRange.length > attributedString.length { + lineRange.length = attributedString.length - lineRange.location + } + if lineRange.length < 0 { + break + } + let coreTextLine = CTTypesetterCreateLineWithOffset(typesetter, lineRange, 100.0) lastLineCharacterIndex += lineCharacterCount diff --git a/submodules/Display/Source/ToolbarNode.swift b/submodules/Display/Source/ToolbarNode.swift index fa4dda6a2c..775fe381bd 100644 --- a/submodules/Display/Source/ToolbarNode.swift +++ b/submodules/Display/Source/ToolbarNode.swift @@ -8,7 +8,8 @@ public final class ToolbarNode: ASDisplayNode { public var left: () -> Void public var right: () -> Void public var middle: () -> Void - + + private let backgroundNode: NavigationBackgroundNode private let separatorNode: ASDisplayNode private let leftTitle: ImmediateTextNode private let leftButton: HighlightTrackingButtonNode @@ -23,6 +24,8 @@ public final class ToolbarNode: ASDisplayNode { self.left = left self.right = right self.middle = middle + + self.backgroundNode = NavigationBackgroundNode(color: theme.tabBarBackgroundColor) self.separatorNode = ASDisplayNode() self.separatorNode.isLayerBacked = true @@ -40,6 +43,8 @@ public final class ToolbarNode: ASDisplayNode { super.init() self.isAccessibilityContainer = false + + self.addSubnode(self.backgroundNode) self.addSubnode(self.leftTitle) self.addSubnode(self.leftButton) @@ -47,6 +52,7 @@ public final class ToolbarNode: ASDisplayNode { self.addSubnode(self.rightButton) self.addSubnode(self.middleTitle) self.addSubnode(self.middleButton) + if self.displaySeparator { self.addSubnode(self.separatorNode) } @@ -96,10 +102,12 @@ public final class ToolbarNode: ASDisplayNode { public func updateTheme(_ theme: TabBarControllerTheme) { self.separatorNode.backgroundColor = theme.tabBarSeparatorColor - self.backgroundColor = theme.tabBarBackgroundColor + self.backgroundNode.updateColor(color: theme.tabBarBackgroundColor, transition: .immediate) } public func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat, additionalSideInsets: UIEdgeInsets, bottomInset: CGFloat, toolbar: Toolbar, transition: ContainedViewLayoutTransition) { + transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(), size: size)) + self.backgroundNode.update(size: size, transition: transition) transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: UIScreenPixel))) var sideInset: CGFloat = 16.0 diff --git a/submodules/Display/Source/TransformImageArguments.swift b/submodules/Display/Source/TransformImageArguments.swift index fd3b359d7e..d874eb56de 100644 --- a/submodules/Display/Source/TransformImageArguments.swift +++ b/submodules/Display/Source/TransformImageArguments.swift @@ -12,15 +12,15 @@ public protocol TransformImageCustomArguments { } public struct TransformImageArguments: Equatable { - public let corners: ImageCorners + public var corners: ImageCorners - public let imageSize: CGSize - public let boundingSize: CGSize - public let intrinsicInsets: UIEdgeInsets - public let resizeMode: TransformImageResizeMode - public let emptyColor: UIColor? - public let custom: TransformImageCustomArguments? - public let scale: CGFloat? + public var imageSize: CGSize + public var boundingSize: CGSize + public var intrinsicInsets: UIEdgeInsets + public var resizeMode: TransformImageResizeMode + public var emptyColor: UIColor? + public var custom: TransformImageCustomArguments? + public var scale: CGFloat? 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 diff --git a/submodules/Display/Source/UIKitUtils.swift b/submodules/Display/Source/UIKitUtils.swift index bc40ac4391..bb2f3d0333 100644 --- a/submodules/Display/Source/UIKitUtils.swift +++ b/submodules/Display/Source/UIKitUtils.swift @@ -133,7 +133,7 @@ public extension UIColor { } } - var hsb: (CGFloat, CGFloat, CGFloat) { + var hsb: (h: CGFloat, s: CGFloat, b: CGFloat) { var hue: CGFloat = 0.0 var saturation: CGFloat = 0.0 var brightness: CGFloat = 0.0 @@ -203,7 +203,6 @@ public extension UIColor { func blitOver(_ other: UIColor, alpha: CGFloat) -> UIColor { let alpha = min(1.0, max(0.0, alpha)) - let oneMinusAlpha = 1.0 - alpha var r1: CGFloat = 0.0 var r2: CGFloat = 0.0 @@ -284,6 +283,27 @@ public extension UIColor { let b = e1.b - e2.b return ((512 + rMean) * r * r) >> 8 + 4 * g * g + ((767 - rMean) * b * b) >> 8 } + + static func average(of colors: [UIColor]) -> UIColor { + var sr: CGFloat = 0.0 + var sg: CGFloat = 0.0 + var sb: CGFloat = 0.0 + var sa: CGFloat = 0.0 + + for color in colors { + var r: CGFloat = 0.0 + var g: CGFloat = 0.0 + var b: CGFloat = 0.0 + var a: CGFloat = 0.0 + color.getRed(&r, green: &g, blue: &b, alpha: &a) + sr += r + sg += g + sb += b + sa += a + } + + return UIColor(red: sr / CGFloat(colors.count), green: sg / CGFloat(colors.count), blue: sb / CGFloat(colors.count), alpha: sa / CGFloat(colors.count)) + } } public extension CGSize { diff --git a/submodules/Display/Source/ViewController.swift b/submodules/Display/Source/ViewController.swift index d4e36bbd54..924cd07fe6 100644 --- a/submodules/Display/Source/ViewController.swift +++ b/submodules/Display/Source/ViewController.swift @@ -74,6 +74,16 @@ public enum TabBarItemContextActionType { } @objc open class ViewController: UIViewController, ContainableController { + public struct NavigationLayout { + public var navigationFrame: CGRect + public var defaultContentHeight: CGFloat + + public init(navigationFrame: CGRect, defaultContentHeight: CGFloat) { + self.navigationFrame = navigationFrame + self.defaultContentHeight = defaultContentHeight + } + } + private var validLayout: ContainerViewLayout? public var currentlyAppliedLayout: ContainerViewLayout? { return self.validLayout @@ -183,8 +193,6 @@ public enum TabBarItemContextActionType { public let navigationBar: NavigationBar? private(set) var toolbar: Toolbar? - private var previewingContext: Any? - public var displayNavigationBar = true open var navigationBarRequiresEntireLayoutUpdate: Bool { return true @@ -196,35 +204,28 @@ public enum TabBarItemContextActionType { open var hasActiveInput: Bool = false private var navigationBarOrigin: CGFloat = 0.0 - - public var navigationOffset: CGFloat = 0.0 { - didSet { - if let navigationBar = self.navigationBar { - var navigationBarFrame = navigationBar.frame - navigationBarFrame.origin.y = self.navigationBarOrigin + self.navigationOffset - navigationBar.frame = navigationBarFrame - } - } - } - - open var navigationHeight: CGFloat { - if let navigationBar = self.navigationBar { - return navigationBar.frame.maxY + + open func navigationLayout(layout: ContainerViewLayout) -> NavigationLayout { + let statusBarHeight: CGFloat = layout.statusBarHeight ?? 0.0 + var defaultNavigationBarHeight: CGFloat + if self._presentedInModal { + defaultNavigationBarHeight = 56.0 } else { - return 0.0 + defaultNavigationBarHeight = 44.0 } - } - - open var navigationInsetHeight: CGFloat { - if let navigationBar = self.navigationBar { - var height = navigationBar.frame.maxY - if let contentNode = navigationBar.contentNode, case .expansion = contentNode.mode { - height += contentNode.nominalHeight - contentNode.height - } - return height - } else { - return 0.0 + let navigationBarHeight: CGFloat = statusBarHeight + (self.navigationBar?.contentHeight(defaultHeight: defaultNavigationBarHeight) ?? defaultNavigationBarHeight) + + var navigationBarFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: layout.size.width, height: navigationBarHeight)) + + navigationBarFrame.size.height += self.additionalNavigationBarHeight + + if !self.displayNavigationBar { + navigationBarFrame.origin.y = -navigationBarFrame.size.height } + + self.navigationBarOrigin = navigationBarFrame.origin.y + + return NavigationLayout(navigationFrame: navigationBarFrame, defaultContentHeight: defaultNavigationBarHeight) } open var cleanNavigationHeight: CGFloat { @@ -238,21 +239,11 @@ public enum TabBarItemContextActionType { return 0.0 } } - - open var visualNavigationInsetHeight: CGFloat { - if let navigationBar = self.navigationBar { - let height = navigationBar.frame.maxY - if let contentNode = navigationBar.contentNode, case .expansion = contentNode.mode { - //height += contentNode.height - } - return height - } else { - return 0.0 - } + + open var additionalNavigationBarHeight: CGFloat { + return 0.0 } - public var additionalNavigationBarHeight: CGFloat = 0.0 - public var additionalSideInsets: UIEdgeInsets = UIEdgeInsets() private let _ready = Promise(true) @@ -375,40 +366,33 @@ public enum TabBarItemContextActionType { deinit { } - - private func updateNavigationBarLayout(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { - let statusBarHeight: CGFloat = layout.statusBarHeight ?? 0.0 - var defaultNavigationBarHeight: CGFloat - if self._presentedInModal { - defaultNavigationBarHeight = 56.0 - } else { - defaultNavigationBarHeight = 44.0 - } - let navigationBarHeight: CGFloat = statusBarHeight + (self.navigationBar?.contentHeight(defaultHeight: defaultNavigationBarHeight) ?? defaultNavigationBarHeight) - let navigationBarOffset: CGFloat - if statusBarHeight.isZero { - navigationBarOffset = 0.0 - } else { - navigationBarOffset = 0.0 - } - var navigationBarFrame = CGRect(origin: CGPoint(x: 0.0, y: navigationBarOffset), size: CGSize(width: layout.size.width, height: navigationBarHeight)) + open func updateNavigationBarLayout(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + self.applyNavigationBarLayout(layout, navigationLayout: self.navigationLayout(layout: layout), additionalBackgroundHeight: 0.0, transition: transition) + } + + public func applyNavigationBarLayout(_ layout: ContainerViewLayout, navigationLayout: NavigationLayout, additionalBackgroundHeight: CGFloat, transition: ContainedViewLayoutTransition) { + let statusBarHeight: CGFloat = layout.statusBarHeight ?? 0.0 + + var navigationBarFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: layout.size.width, height: navigationLayout.navigationFrame.maxY)) if !self.displayNavigationBar { navigationBarFrame.origin.y = -navigationBarFrame.size.height } self.navigationBarOrigin = navigationBarFrame.origin.y - navigationBarFrame.origin.y += self.navigationOffset if let navigationBar = self.navigationBar { if let contentNode = navigationBar.contentNode, case .expansion = contentNode.mode, !self.displayNavigationBar { - navigationBarFrame.origin.y += contentNode.height + statusBarHeight + navigationBarFrame.origin.y -= navigationLayout.defaultContentHeight + navigationBarFrame.size.height += contentNode.height + navigationLayout.defaultContentHeight + statusBarHeight + //navigationBarFrame.origin.y += contentNode.height + statusBarHeight } if let _ = navigationBar.contentNode, let _ = navigationBar.secondaryContentNode, !self.displayNavigationBar { - navigationBarFrame.origin.y += NavigationBar.defaultSecondaryContentHeight + navigationBarFrame.size.height += NavigationBar.defaultSecondaryContentHeight + //navigationBarFrame.origin.y += NavigationBar.defaultSecondaryContentHeight } - navigationBar.updateLayout(size: navigationBarFrame.size, defaultHeight: defaultNavigationBarHeight, additionalHeight: 0.0, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, appearsHidden: !self.displayNavigationBar, transition: transition) + navigationBar.updateLayout(size: navigationBarFrame.size, defaultHeight: navigationLayout.defaultContentHeight, additionalTopHeight: statusBarHeight, additionalContentHeight: self.additionalNavigationBarHeight, additionalBackgroundHeight: additionalBackgroundHeight, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, appearsHidden: !self.displayNavigationBar, transition: transition) if !transition.isAnimated { navigationBar.layer.cancelAnimationsRecursive(key: "bounds") navigationBar.layer.cancelAnimationsRecursive(key: "position") @@ -612,33 +596,6 @@ public enum TabBarItemContextActionType { } } - @available(iOSApplicationExtension 9.0, iOS 9.0, *) - open func registerForPreviewing(with delegate: UIViewControllerPreviewingDelegate, sourceView: UIView, theme: PeekControllerTheme, onlyNative: Bool) { - } - - @available(iOSApplicationExtension 9.0, iOS 9.0, *) - public func registerForPreviewingNonNative(with delegate: UIViewControllerPreviewingDelegate, sourceView: UIView, theme: PeekControllerTheme) { - if true || self.traitCollection.forceTouchCapability != .available { - if self.previewingContext == nil { - let previewingContext = SimulatedViewControllerPreviewing(theme: theme, delegate: delegate, sourceView: sourceView, node: self.displayNode, present: { [weak self] c, a in - self?.presentInGlobalOverlay(c, with: a) - }, customPresent: { [weak self] c, n in - return self?.customPresentPreviewingController?(c, n) - }) - self.previewingContext = previewingContext - } - } - } - - @available(iOSApplicationExtension 9.0, iOS 9.0, *) - open override func unregisterForPreviewing(withContext previewing: UIViewControllerPreviewing) { - if self.previewingContext != nil { - self.previewingContext = nil - } else { - super.unregisterForPreviewing(withContext: previewing) - } - } - public final func navigationNextSibling() -> UIViewController? { if let navigationController = self.navigationController as? NavigationController { if let index = navigationController.viewControllers.firstIndex(where: { $0 === self }) { diff --git a/submodules/Display/Source/ViewControllerPreviewing.swift b/submodules/Display/Source/ViewControllerPreviewing.swift deleted file mode 100644 index f9f8072015..0000000000 --- a/submodules/Display/Source/ViewControllerPreviewing.swift +++ /dev/null @@ -1,137 +0,0 @@ -import Foundation -import UIKit -import AsyncDisplayKit -import SwiftSignalKit - -@available(iOSApplicationExtension 9.0, iOS 9.0, *) -private final class ViewControllerPeekContent: PeekControllerContent { - let controller: ViewController - private let menu: [PeekControllerMenuItem] - - init(controller: ViewController) { - self.controller = controller - var menu: [PeekControllerMenuItem] = [] - for item in controller.previewActionItems { - menu.append(PeekControllerMenuItem(title: item.title, color: .accent, action: { [weak controller] _, _ in - if let controller = controller, let item = item as? UIPreviewAction { - item.handler(item, controller) - } - return true - })) - } - self.menu = menu - } - - func presentation() -> PeekControllerContentPresentation { - return .contained - } - - func menuActivation() -> PeerkControllerMenuActivation { - return .drag - } - - func menuItems() -> [PeekControllerMenuItem] { - return self.menu - } - - func node() -> PeekControllerContentNode & ASDisplayNode { - return ViewControllerPeekContentNode(controller: self.controller) - } - - func topAccessoryNode() -> ASDisplayNode? { - return nil - } - - func isEqual(to: PeekControllerContent) -> Bool { - if let to = to as? ViewControllerPeekContent { - return self.controller === to.controller - } else { - return false - } - } -} - -private final class ViewControllerPeekContentNode: ASDisplayNode, PeekControllerContentNode { - private let controller: ViewController - private var hasValidLayout = false - - init(controller: ViewController) { - self.controller = controller - - super.init() - } - - func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize { - if !self.hasValidLayout { - self.hasValidLayout = true - self.controller.view.frame = CGRect(origin: CGPoint(), size: size) - self.controller.containerLayoutUpdated(ContainerViewLayout(size: size, metrics: LayoutMetrics(), deviceMetrics: .unknown(screenSize: size, statusBarHeight: 20.0, onScreenNavigationHeight: nil), intrinsicInsets: UIEdgeInsets(), safeInsets: UIEdgeInsets(), additionalInsets: UIEdgeInsets(), statusBarHeight: nil, inputHeight: nil, inputHeightIsInteractivellyChanging: false, inVoiceOver: false), transition: .immediate) - self.controller.setIgnoreAppearanceMethodInvocations(true) - self.view.addSubview(self.controller.view) - self.controller.setIgnoreAppearanceMethodInvocations(false) - self.controller.viewWillAppear(false) - self.controller.viewDidAppear(false) - } else { - self.controller.containerLayoutUpdated(ContainerViewLayout(size: size, metrics: LayoutMetrics(), deviceMetrics: .unknown(screenSize: size, statusBarHeight: 20.0, onScreenNavigationHeight: nil), intrinsicInsets: UIEdgeInsets(), safeInsets: UIEdgeInsets(), additionalInsets: UIEdgeInsets(), statusBarHeight: nil, inputHeight: nil, inputHeightIsInteractivellyChanging: false, inVoiceOver: false), transition: transition) - } - - return size - } - - override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { - if self.bounds.contains(point) { - return self.view - } - return nil - } -} - -@available(iOSApplicationExtension 9.0, iOS 9.0, *) -final class SimulatedViewControllerPreviewing: NSObject, UIViewControllerPreviewing { - weak var delegateImpl: UIViewControllerPreviewingDelegate? - var delegate: UIViewControllerPreviewingDelegate { - return self.delegateImpl! - } - let recognizer: PeekControllerGestureRecognizer - var previewingGestureRecognizerForFailureRelationship: UIGestureRecognizer { - return self.recognizer - } - let sourceView: UIView - let node: ASDisplayNode - - var sourceRect: CGRect = CGRect() - - init(theme: PeekControllerTheme, delegate: UIViewControllerPreviewingDelegate, sourceView: UIView, node: ASDisplayNode, present: @escaping (ViewController, Any?) -> Void, customPresent: ((ViewController, ASDisplayNode) -> ViewController?)?) { - self.delegateImpl = delegate - self.sourceView = sourceView - self.node = node - var contentAtPointImpl: ((CGPoint) -> Signal<(ASDisplayNode, PeekControllerContent)?, NoError>?)? - self.recognizer = PeekControllerGestureRecognizer(contentAtPoint: { point in - return contentAtPointImpl?(point) - }, present: { content, sourceNode in - if let content = content as? ViewControllerPeekContent, let controller = customPresent?(content.controller, sourceNode) { - present(controller, nil) - return controller - } else { - let controller = PeekController(theme: theme, content: content, sourceNode: { - return sourceNode - }) - present(controller, nil) - return controller - } - }) - - node.view.addGestureRecognizer(self.recognizer) - - super.init() - - contentAtPointImpl = { [weak self] point in - if let strongSelf = self, let delegate = strongSelf.delegateImpl { - if let controller = delegate.previewingContext(strongSelf, viewControllerForLocation: point) as? ViewController { - return .single((strongSelf.node, ViewControllerPeekContent(controller: controller))) - } - } - return nil - } - } -} diff --git a/submodules/Display/Source/WallpaperBackgroundNode.swift b/submodules/Display/Source/WallpaperBackgroundNode.swift deleted file mode 100644 index 65de835a13..0000000000 --- a/submodules/Display/Source/WallpaperBackgroundNode.swift +++ /dev/null @@ -1,91 +0,0 @@ -import Foundation -import UIKit -import AsyncDisplayKit - -private let motionAmount: CGFloat = 32.0 - -public final class WallpaperBackgroundNode: ASDisplayNode { - let contentNode: ASDisplayNode - - public var motionEnabled: Bool = false { - didSet { - if oldValue != self.motionEnabled { - if self.motionEnabled { - 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.contentNode.view.addMotionEffect(group) - } else { - for effect in self.contentNode.view.motionEffects { - self.contentNode.view.removeMotionEffect(effect) - } - } - if !self.frame.isEmpty { - self.updateScale() - } - } - } - } - - public var image: UIImage? { - didSet { - self.contentNode.contents = self.image?.cgImage - } - } - - public var rotation: CGFloat = 0.0 { - didSet { - 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 - self.contentNode.transform = CATransform3DMakeScale(scale, scale, 1.0) - } else { - self.contentNode.transform = CATransform3DIdentity - } - } - - public override init() { - self.imageContentMode = .scaleAspectFill - - self.contentNode = ASDisplayNode() - self.contentNode.contentMode = self.imageContentMode - - super.init() - - self.clipsToBounds = true - self.contentNode.frame = self.bounds - self.addSubnode(self.contentNode) - } - - 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 df3eec9703..34f98e1b9b 100644 --- a/submodules/Display/Source/WindowContent.swift +++ b/submodules/Display/Source/WindowContent.swift @@ -117,14 +117,6 @@ private func containedLayoutForWindowLayout(_ layout: WindowLayout, deviceMetric return ContainerViewLayout(size: layout.size, metrics: layout.metrics, deviceMetrics: deviceMetrics, intrinsicInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: layout.onScreenNavigationHeight ?? 0.0, right: 0.0), safeInsets: resolvedSafeInsets, additionalInsets: UIEdgeInsets(), statusBarHeight: resolvedStatusBarHeight, inputHeight: updatedInputHeight, inputHeightIsInteractivellyChanging: layout.upperKeyboardInputPositionBound != nil && layout.upperKeyboardInputPositionBound != layout.size.height && layout.inputHeight != nil, inVoiceOver: layout.inVoiceOver) } -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 -} - public func doesViewTreeDisableInteractiveTransitionGestureRecognizer(_ view: UIView, keyboardOnly: Bool = false) -> Bool { if view.disablesInteractiveTransitionGestureRecognizer && !keyboardOnly { return true @@ -237,7 +229,7 @@ public class Window1 { private var deviceMetrics: DeviceMetrics - private let statusBarHost: StatusBarHost? + public let statusBarHost: StatusBarHost? private let keyboardManager: KeyboardManager? private let keyboardViewManager: KeyboardViewManager? private var statusBarChangeObserver: AnyObject? @@ -270,8 +262,42 @@ public class Window1 { private var shouldNotAnimateLikelyKeyboardAutocorrectionSwitch: Bool = false public private(set) var forceInCallStatusBarText: String? = nil - public var inCallNavigate: (() -> Void)? { + public var inCallNavigate: (() -> Void)? + + private var debugTapCounter: (Double, Int) = (0.0, 0) + private var debugTapRecognizer: UITapGestureRecognizer? + public var debugAction: (() -> Void)? { didSet { + if self.debugAction != nil { + if self.debugTapRecognizer == nil { + let debugTapRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.debugTapGesture(_:))) + self.debugTapRecognizer = debugTapRecognizer + self.hostView.containerView.addGestureRecognizer(debugTapRecognizer) + } + } else if let debugTapRecognizer = self.debugTapRecognizer { + self.debugTapRecognizer = nil + self.hostView.containerView.removeGestureRecognizer(debugTapRecognizer) + } + } + } + @objc private func debugTapGesture(_ recognizer: UITapGestureRecognizer) { + if case .ended = recognizer.state { + let timestamp = CACurrentMediaTime() + if self.debugTapCounter.0 < timestamp - 0.4 { + self.debugTapCounter.0 = timestamp + self.debugTapCounter.1 = 0 + } + + if self.debugTapCounter.0 >= timestamp - 0.4 { + self.debugTapCounter.0 = timestamp + self.debugTapCounter.1 += 1 + } + + if self.debugTapCounter.1 >= 10 { + self.debugTapCounter.1 = 0 + + self.debugAction?() + } } } @@ -750,12 +776,11 @@ public class Window1 { self?.inCallNavigate?() } } + self.hostView.containerView.insertSubview(rootController.view, at: 0) if !self.windowLayout.size.width.isZero && !self.windowLayout.size.height.isZero { rootController.displayNode.frame = CGRect(origin: CGPoint(), size: self.windowLayout.size) rootController.containerLayoutUpdated(containedLayoutForWindowLayout(self.windowLayout, deviceMetrics: self.deviceMetrics), transition: .immediate) } - - self.hostView.containerView.insertSubview(rootController.view, at: 0) } self.hostView.eventView.setNeedsLayout() diff --git a/submodules/Display/Source/WindowPanRecognizer.swift b/submodules/Display/Source/WindowPanRecognizer.swift index d9180edbaa..53ed394912 100644 --- a/submodules/Display/Source/WindowPanRecognizer.swift +++ b/submodules/Display/Source/WindowPanRecognizer.swift @@ -13,6 +13,10 @@ public final class WindowPanRecognizer: UIGestureRecognizer { self.previousPoints.removeAll() } + + public func cancel() { + self.state = .cancelled + } private func addPoint(_ point: CGPoint) { self.previousPoints.append((point, CACurrentMediaTime())) diff --git a/submodules/FFMpegBinding/Sources/FFMpegSWResample.m b/submodules/FFMpegBinding/Sources/FFMpegSWResample.m index fbecf0555b..16c2a888dc 100644 --- a/submodules/FFMpegBinding/Sources/FFMpegSWResample.m +++ b/submodules/FFMpegBinding/Sources/FFMpegSWResample.m @@ -6,6 +6,7 @@ #import "libswresample/swresample.h" @interface FFMpegSWResample () { + int _sourceChannelCount; SwrContext *_context; NSUInteger _ratio; NSInteger _destinationChannelCount; @@ -21,6 +22,7 @@ - (instancetype)initWithSourceChannelCount:(NSInteger)sourceChannelCount sourceSampleRate:(NSInteger)sourceSampleRate sourceSampleFormat:(enum FFMpegAVSampleFormat)sourceSampleFormat destinationChannelCount:(NSInteger)destinationChannelCount destinationSampleRate:(NSInteger)destinationSampleRate destinationSampleFormat:(enum FFMpegAVSampleFormat)destinationSampleFormat { self = [super init]; if (self != nil) { + _sourceChannelCount = sourceChannelCount; _destinationChannelCount = destinationChannelCount; _destinationSampleFormat = destinationSampleFormat; _context = swr_alloc_set_opts(NULL, @@ -47,6 +49,12 @@ - (NSData * _Nullable)resample:(FFMpegAVFrame *)frame { AVFrame *frameImpl = (AVFrame *)[frame impl]; + + int numChannels = frameImpl->channels; + if (numChannels != _sourceChannelCount) { + return nil; + } + int bufSize = av_samples_get_buffer_size(NULL, (int)_destinationChannelCount, frameImpl->nb_samples * (int)_ratio, diff --git a/submodules/GalleryUI/BUILD b/submodules/GalleryUI/BUILD index 06170f2d03..6a06062b21 100644 --- a/submodules/GalleryUI/BUILD +++ b/submodules/GalleryUI/BUILD @@ -28,6 +28,7 @@ swift_library( "//submodules/OverlayStatusController:OverlayStatusController", "//submodules/PresentationDataUtils:PresentationDataUtils", "//submodules/UrlEscaping:UrlEscaping", + "//submodules/ManagedAnimationNode:ManagedAnimationNode" ], visibility = [ "//visibility:public", diff --git a/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift b/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift index 3eca056b51..d099fe3d7b 100644 --- a/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift +++ b/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift @@ -20,6 +20,7 @@ import LocalizedPeerData import TextSelectionNode import UrlEscaping import UndoUI +import ManagedAnimationNode private let deleteImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionTrash"), color: .white) private let actionImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionForward"), color: .white) @@ -27,8 +28,6 @@ private let editImage = generateTintedImage(image: UIImage(bundleImageName: "Med private let backwardImage = generateTintedImage(image: UIImage(bundleImageName: "Media Gallery/BackwardButton"), color: .white) private let forwardImage = generateTintedImage(image: UIImage(bundleImageName: "Media Gallery/ForwardButton"), color: .white) -private let pauseImage = generateTintedImage(image: UIImage(bundleImageName: "Media Gallery/PauseButton"), color: .white) -private let playImage = generateTintedImage(image: UIImage(bundleImageName: "Media Gallery/PlayButton"), color: .white) private let cloudFetchIcon = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/FileCloudFetch"), color: UIColor.white) @@ -108,6 +107,8 @@ class CaptionScrollWrapperNode: ASDisplayNode { } } + + final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScrollViewDelegate { private let context: AccountContext private var presentationData: PresentationData @@ -127,9 +128,10 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll private let textNode: ImmediateTextNode private let authorNameNode: ASTextNode private let dateNode: ASTextNode - private let backwardButton: HighlightableButtonNode - private let forwardButton: HighlightableButtonNode + private let backwardButton: PlaybackButtonNode + private let forwardButton: PlaybackButtonNode private let playbackControlButton: HighlightableButtonNode + private let playPauseIconNode: PlayPauseIconNode private let statusButtonNode: HighlightTrackingButtonNode private let statusNode: RadialStatusNode @@ -152,6 +154,8 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll var setPlayRate: ((Double) -> Void)? var fetchControl: (() -> Void)? + var interacting: ((Bool) -> Void)? + private var seekTimer: SwiftSignalKit.Timer? private var currentIsPaused: Bool = true private var seekRate: Double = 1.0 @@ -179,7 +183,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll self.forwardButton.isHidden = !seekable if status == .Local { self.playbackControlButton.isHidden = false - self.playbackControlButton.setImage(playImage, for: []) + self.playPauseIconNode.enqueueState(.play, animated: true) } else { self.playbackControlButton.isHidden = true } @@ -188,7 +192,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll var statusState: RadialStatusNodeState = .none switch status { - case let .Fetching(isActive, progress): + case let .Fetching(_, progress): let adjustedProgress = max(progress, 0.027) statusState = .cloudProgress(color: UIColor.white, strokeBackgroundColor: UIColor.white.withAlphaComponent(0.5), lineWidth: 2.0, value: CGFloat(adjustedProgress)) case .Local: @@ -207,7 +211,14 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll self.backwardButton.isHidden = !seekable self.forwardButton.isHidden = !seekable self.playbackControlButton.isHidden = false - self.playbackControlButton.setImage(paused ? playImage : pauseImage, for: []) + + let icon: PlayPauseIconNodeState + if let wasPlaying = self.wasPlaying { + icon = wasPlaying ? .pause : .play + } else { + icon = paused ? .play : .pause + } + self.playPauseIconNode.enqueueState(icon, animated: true) self.statusButtonNode.isHidden = true self.statusNode.isHidden = true } @@ -303,17 +314,20 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll self.dateNode.isUserInteractionEnabled = false self.dateNode.displaysAsynchronously = false - self.backwardButton = HighlightableButtonNode() + self.backwardButton = PlaybackButtonNode() self.backwardButton.isHidden = true - self.backwardButton.setImage(backwardImage, for: []) + self.backwardButton.backgroundIconNode.image = backwardImage - self.forwardButton = HighlightableButtonNode() + self.forwardButton = PlaybackButtonNode() self.forwardButton.isHidden = true - self.forwardButton.setImage(forwardImage, for: []) + self.forwardButton.forward = true + self.forwardButton.backgroundIconNode.image = forwardImage self.playbackControlButton = HighlightableButtonNode() self.playbackControlButton.isHidden = true + self.playPauseIconNode = PlayPauseIconNode() + self.statusButtonNode = HighlightTrackingButtonNode() self.statusNode = RadialStatusNode(backgroundNodeColor: .clear) self.statusNode.isUserInteractionEnabled = false @@ -361,6 +375,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll self.contentNode.addSubnode(self.backwardButton) self.contentNode.addSubnode(self.forwardButton) self.contentNode.addSubnode(self.playbackControlButton) + self.playbackControlButton.addSubnode(self.playPauseIconNode) self.contentNode.addSubnode(self.statusNode) self.contentNode.addSubnode(self.statusButtonNode) @@ -405,12 +420,14 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll self.forwardButton.view.addGestureRecognizer(forwardLongPressGestureRecognizer) } - private var wasPlaying = false + private var wasPlaying: Bool? @objc private func seekBackwardLongPress(_ gestureRecognizer: UILongPressGestureRecognizer) { switch gestureRecognizer.state { case .began: + self.interacting?(true) + self.backwardButton.isPressing = true self.wasPlaying = !self.currentIsPaused - if self.wasPlaying { + if self.wasPlaying == true { self.playbackControl?() } @@ -431,12 +448,14 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll self.seekTimer = seekTimer seekTimer.start() case .ended, .cancelled: + self.interacting?(false) + self.backwardButton.isPressing = false self.seekTimer?.invalidate() self.seekTimer = nil - if self.wasPlaying { + if self.wasPlaying == true { self.playbackControl?() - self.wasPlaying = false } + self.wasPlaying = nil default: break } @@ -445,8 +464,10 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll @objc private func seekForwardLongPress(_ gestureRecognizer: UILongPressGestureRecognizer) { switch gestureRecognizer.state { case .began: + self.interacting?(true) + self.forwardButton.isPressing = true self.wasPlaying = !self.currentIsPaused - if !self.wasPlaying { + if self.wasPlaying == false { self.playbackControl?() } @@ -467,13 +488,16 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll self.seekTimer = seekTimer seekTimer.start() case .ended, .cancelled: + self.interacting?(false) + self.forwardButton.isPressing = false self.setPlayRate?(1.0) self.seekTimer?.invalidate() self.seekTimer = nil - if !self.wasPlaying { + if self.wasPlaying == false { self.playbackControl?() } + self.wasPlaying = nil default: break } @@ -503,7 +527,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll func setup(origin: GalleryItemOriginData?, caption: NSAttributedString) { let titleText = origin?.title - let dateText = origin?.timestamp.flatMap { humanReadableStringForTimestamp(strings: self.strings, dateTimeFormat: self.dateTimeFormat, timestamp: $0) } + let dateText = origin?.timestamp.flatMap { humanReadableStringForTimestamp(strings: self.strings, dateTimeFormat: self.dateTimeFormat, timestamp: $0).0 } if self.currentMessageText != caption || self.currentAuthorNameText != titleText || self.currentDateText != dateText { self.currentMessageText = caption @@ -586,8 +610,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll authorNameText = peer.displayTitle(strings: self.strings, displayOrder: self.nameOrder) } - var dateText = humanReadableStringForTimestamp(strings: self.strings, dateTimeFormat: self.dateTimeFormat, timestamp: message.timestamp) - + var dateText = humanReadableStringForTimestamp(strings: self.strings, dateTimeFormat: self.dateTimeFormat, timestamp: message.timestamp).0 if !displayInfo { authorNameText = "" dateText = "" @@ -759,22 +782,24 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll self.deleteButton.frame = deleteFrame self.editButton.frame = editFrame - if let image = self.backwardButton.image(for: .normal) { + if let image = self.backwardButton.backgroundIconNode.image { self.backwardButton.frame = CGRect(origin: CGPoint(x: floor((width - image.size.width) / 2.0) - 66.0, y: panelHeight - bottomInset - 44.0 + 7.0), size: image.size) } - if let image = self.forwardButton.image(for: .normal) { + if let image = self.forwardButton.backgroundIconNode.image { self.forwardButton.frame = CGRect(origin: CGPoint(x: floor((width - image.size.width) / 2.0) + 66.0, y: panelHeight - bottomInset - 44.0 + 7.0), size: image.size) } self.playbackControlButton.frame = CGRect(origin: CGPoint(x: floor((width - 44.0) / 2.0), y: panelHeight - bottomInset - 44.0), size: CGSize(width: 44.0, height: 44.0)) + self.playPauseIconNode.frame = self.playbackControlButton.bounds.offsetBy(dx: 2.0, dy: 2.0) let statusSize = CGSize(width: 28.0, height: 28.0) transition.updateFrame(node: self.statusNode, frame: CGRect(origin: CGPoint(x: floor((width - statusSize.width) / 2.0), y: panelHeight - bottomInset - statusSize.height - 8.0), size: statusSize)) self.statusButtonNode.frame = CGRect(origin: CGPoint(x: floor((width - 44.0) / 2.0), y: panelHeight - bottomInset - 44.0), size: CGSize(width: 44.0, height: 44.0)) - let authorNameSize = self.authorNameNode.measure(CGSize(width: width - 44.0 * 2.0 - 8.0 * 2.0 - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude)) - let dateSize = self.dateNode.measure(CGSize(width: width - 44.0 * 2.0 - 8.0 * 2.0, height: CGFloat.greatestFiniteMagnitude)) + let buttonsSideInset: CGFloat = !self.editButton.isHidden ? 88.0 : 44.0 + let authorNameSize = self.authorNameNode.measure(CGSize(width: width - buttonsSideInset * 2.0 - 8.0 * 2.0 - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude)) + let dateSize = self.dateNode.measure(CGSize(width: width - buttonsSideInset * 2.0 - 8.0 * 2.0, height: CGFloat.greatestFiniteMagnitude)) if authorNameSize.height.isZero { self.dateNode.frame = CGRect(origin: CGPoint(x: floor((width - dateSize.width) / 2.0), y: panelHeight - bottomInset - 44.0 + floor((44.0 - dateSize.height) / 2.0)), size: dateSize) @@ -873,10 +898,16 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll if messages.count == 1 { strongSelf.commitDeleteMessages(messages, ask: true) } else { - let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } + strongSelf.interacting?(true) + + var presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } + if !presentationData.theme.overallDarkAppearance { + presentationData = presentationData.withUpdated(theme: defaultDarkColorPresentationTheme) + } + var generalMessageContentKind: MessageContentKind? for message in messages { - let currentKind = messageContentKind(contentSettings: strongSelf.context.currentContentSettings.with { $0 }, message: 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, dateTimeFormat: presentationData.dateTimeFormat, accountPeerId: strongSelf.context.account.peerId) if generalMessageContentKind == nil || generalMessageContentKind == currentKind { generalMessageContentKind = currentKind } else { @@ -907,7 +938,10 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll } } - let actionSheet = ActionSheetController(presentationData: presentationData.withUpdated(theme: defaultDarkColorPresentationTheme)) + let actionSheet = ActionSheetController(presentationData: presentationData) + actionSheet.dismissed = { [weak self] _ in + self?.interacting?(false) + } let items: [ActionSheetItem] = [ ActionSheetButtonItem(title: singleText, color: .destructive, action: { [weak actionSheet] in actionSheet?.dismissAnimated() @@ -937,7 +971,11 @@ 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(presentationData: strongSelf.presentationData.withUpdated(theme: defaultDarkColorPresentationTheme)) + var presentationData = strongSelf.presentationData + if !presentationData.theme.overallDarkAppearance { + presentationData = presentationData.withUpdated(theme: defaultDarkColorPresentationTheme) + } + let actionSheet = ActionSheetController(presentationData: presentationData) var items: [ActionSheetItem] = [] var personalPeerName: String? var isChannel = false @@ -960,7 +998,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(account: strongSelf.context.account, messageIds: messages.map { $0.id }, type: .forEveryone).start() + let _ = strongSelf.context.engine.messages.deleteMessagesInteractively(messageIds: messages.map { $0.id }, type: .forEveryone).start() strongSelf.controllerInteraction?.dismissController() } })) @@ -975,15 +1013,19 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll items.append(ActionSheetButtonItem(title: localOptionText, color: .destructive, action: { [weak actionSheet] in actionSheet?.dismissAnimated() if let strongSelf = self { - let _ = deleteMessagesInteractively(account: strongSelf.context.account, messageIds: messages.map { $0.id }, type: .forLocalPeer).start() + let _ = strongSelf.context.engine.messages.deleteMessagesInteractively(messageIds: messages.map { $0.id }, type: .forLocalPeer).start() strongSelf.controllerInteraction?.dismissController() } })) } if !ask && items.count == 1 { - let _ = deleteMessagesInteractively(account: strongSelf.context.account, messageIds: messages.map { $0.id }, type: .forEveryone).start() + let _ = strongSelf.context.engine.messages.deleteMessagesInteractively(messageIds: messages.map { $0.id }, type: .forEveryone).start() strongSelf.controllerInteraction?.dismissController() } else if !items.isEmpty { + strongSelf.interacting?(true) + actionSheet.dismissed = { [weak self] _ in + self?.interacting?(false) + } actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ ActionSheetButtonItem(title: strongSelf.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in actionSheet?.dismissAnimated() @@ -996,18 +1038,25 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll } @objc func actionButtonPressed() { + self.interacting?(true) + if let currentMessage = self.currentMessage { let _ = (self.context.account.postbox.transaction { transaction -> [Message] in return transaction.getMessageGroup(currentMessage.id) ?? [] } |> deliverOnMainQueue).start(next: { [weak self] messages in if let strongSelf = self, !messages.isEmpty { - let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } + var presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } + var forceTheme: PresentationTheme? + if !presentationData.theme.overallDarkAppearance { + forceTheme = defaultDarkColorPresentationTheme + presentationData = presentationData.withUpdated(theme: defaultDarkColorPresentationTheme) + } var generalMessageContentKind: MessageContentKind? var beganContentKindScanning = false var messageContentKinds = Set() for message in messages { - let currentKind = messageContentKind(contentSettings: strongSelf.context.currentContentSettings.with { $0 }, message: 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, dateTimeFormat: presentationData.dateTimeFormat, accountPeerId: strongSelf.context.account.peerId) if beganContentKindScanning && currentKind != generalMessageContentKind { generalMessageContentKind = nil } else if !beganContentKindScanning || currentKind == generalMessageContentKind { @@ -1078,7 +1127,10 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll } } } - let shareController = ShareController(context: strongSelf.context, subject: subject, preferredAction: preferredAction, forcedTheme: defaultDarkColorPresentationTheme) + let shareController = ShareController(context: strongSelf.context, subject: subject, preferredAction: preferredAction, forceTheme: forceTheme) + shareController.dismissed = { [weak self] _ in + self?.interacting?(false) + } shareController.completed = { [weak self] peerIds in if let strongSelf = self { let _ = (strongSelf.context.account.postbox.transaction { transaction -> [Peer] in @@ -1139,7 +1191,10 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll let shareAction: ([Message]) -> Void = { messages in if let strongSelf = self { - let shareController = ShareController(context: strongSelf.context, subject: .messages(messages), preferredAction: preferredAction, forcedTheme: defaultDarkColorPresentationTheme) + let shareController = ShareController(context: strongSelf.context, subject: .messages(messages), preferredAction: preferredAction, forceTheme: forceTheme) + shareController.dismissed = { [weak self] _ in + self?.interacting?(false) + } shareController.completed = { [weak self] peerIds in if let strongSelf = self { let _ = (strongSelf.context.account.postbox.transaction { transaction -> [Peer] in @@ -1184,7 +1239,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll } } - let actionSheet = ActionSheetController(presentationData: presentationData.withUpdated(theme: defaultDarkColorPresentationTheme)) + let actionSheet = ActionSheetController(presentationData: presentationData) let items: [ActionSheetItem] = [ ActionSheetButtonItem(title: singleText, color: .accent, action: { [weak actionSheet] in actionSheet?.dismissAnimated() @@ -1208,7 +1263,12 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll } }) } else if let (webPage, media) = self.currentWebPageAndMedia { - let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } + var presentationData = self.context.sharedContext.currentPresentationData.with { $0 } + var forceTheme: PresentationTheme? + if !presentationData.theme.overallDarkAppearance { + forceTheme = defaultDarkColorPresentationTheme + presentationData = presentationData.withUpdated(theme: defaultDarkColorPresentationTheme) + } var preferredAction = ShareControllerPreferredAction.default var subject = ShareControllerSubject.media(.webPage(webPage: WebpageReference(webPage), media: media)) @@ -1231,7 +1291,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll if availableOpenInOptions(context: self.context, item: item).count > 1 { preferredAction = .custom(action: ShareControllerAction(title: presentationData.strings.Conversation_FileOpenIn, action: { [weak self] in if let strongSelf = self { - let openInController = OpenInActionSheetController(context: strongSelf.context, forceTheme: defaultDarkColorPresentationTheme, item: item, additionalAction: nil, openUrl: { [weak self] url in + let openInController = OpenInActionSheetController(context: strongSelf.context, forceTheme: forceTheme, item: item, additionalAction: nil, openUrl: { [weak self] url in if let strongSelf = self { strongSelf.context.sharedContext.openExternalUrl(context: strongSelf.context, urlContext: .generic, url: url, forceExternal: true, presentationData: presentationData, navigationController: nil, dismissInput: {}) } @@ -1256,7 +1316,10 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll } } } - let shareController = ShareController(context: self.context, subject: subject, preferredAction: preferredAction, forcedTheme: defaultDarkColorPresentationTheme) + let shareController = ShareController(context: self.context, subject: subject, preferredAction: preferredAction, forceTheme: forceTheme) + shareController.dismissed = { [weak self] _ in + self?.interacting?(false) + } shareController.completed = { [weak self] peerIds in if let strongSelf = self { let _ = (strongSelf.context.account.postbox.transaction { transaction -> [Peer] in @@ -1313,11 +1376,15 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll } @objc func backwardButtonPressed() { + self.interacting?(true) self.seekBackward?(15.0) + self.interacting?(false) } @objc func forwardButtonPressed() { + self.interacting?(true) self.seekForward?(15.0) + self.interacting?(false) } @objc private func statusPressed() { @@ -1381,3 +1448,119 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll } } } + +private enum PlayPauseIconNodeState: Equatable { + case play + case pause +} + +private final class PlayPauseIconNode: ManagedAnimationNode { + private let duration: Double = 0.35 + private var iconState: PlayPauseIconNodeState = .pause + + init() { + super.init(size: CGSize(width: 40.0, height: 40.0)) + + self.trackTo(item: ManagedAnimationItem(source: .local("anim_playpause"), frames: .range(startFrame: 41, endFrame: 41), duration: 0.01)) + } + + func enqueueState(_ state: PlayPauseIconNodeState, animated: Bool) { + guard self.iconState != state else { + return + } + + let previousState = self.iconState + self.iconState = state + + switch previousState { + case .pause: + switch state { + case .play: + if animated { + self.trackTo(item: ManagedAnimationItem(source: .local("anim_playpause"), frames: .range(startFrame: 41, endFrame: 83), duration: self.duration)) + } else { + self.trackTo(item: ManagedAnimationItem(source: .local("anim_playpause"), frames: .range(startFrame: 0, endFrame: 0), duration: 0.01)) + } + case .pause: + break + } + case .play: + switch state { + case .pause: + if animated { + self.trackTo(item: ManagedAnimationItem(source: .local("anim_playpause"), frames: .range(startFrame: 0, endFrame: 41), duration: self.duration)) + } else { + self.trackTo(item: ManagedAnimationItem(source: .local("anim_playpause"), frames: .range(startFrame: 41, endFrame: 41), duration: 0.01)) + } + case .play: + break + } + } + } +} + +private let circleDiameter: CGFloat = 80.0 + +private final class PlaybackButtonNode: HighlightTrackingButtonNode { + let backgroundIconNode: ASImageNode + let textNode: ImmediateTextNode + + var forward: Bool = false + + var isPressing = false { + didSet { + if self.isPressing != oldValue && !self.isPressing { + self.highligthedChanged(false) + } + } + } + + init() { + self.backgroundIconNode = ASImageNode() + self.backgroundIconNode.isLayerBacked = true + self.backgroundIconNode.displaysAsynchronously = false + self.backgroundIconNode.displayWithoutProcessing = true + + self.textNode = ImmediateTextNode() + self.textNode.attributedText = NSAttributedString(string: "15", font: Font.with(size: 11.0, design: .round, weight: .semibold, traits: []), textColor: .white) + + super.init(pointerStyle: .circle) + + self.addSubnode(self.backgroundIconNode) + self.addSubnode(self.textNode) + + self.highligthedChanged = { [weak self] highlighted in + if let strongSelf = self { + if highlighted { + strongSelf.backgroundIconNode.layer.removeAnimation(forKey: "opacity") + strongSelf.backgroundIconNode.alpha = 0.4 + + strongSelf.textNode.layer.removeAnimation(forKey: "opacity") + strongSelf.textNode.alpha = 0.4 + + let transition: ContainedViewLayoutTransition = .animated(duration: 0.18, curve: .linear) + let angle = CGFloat.pi / 4.0 + 0.226 + transition.updateTransformRotation(node: strongSelf.backgroundIconNode, angle: strongSelf.forward ? angle : -angle) + } else if !strongSelf.isPressing { + strongSelf.backgroundIconNode.alpha = 1.0 + strongSelf.backgroundIconNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) + + strongSelf.textNode.alpha = 1.0 + strongSelf.textNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) + + let transition: ContainedViewLayoutTransition = .animated(duration: 0.2, curve: .linear) + transition.updateTransformRotation(node: strongSelf.backgroundIconNode, angle: 0.0) + } + } + } + } + + override func layout() { + super.layout() + self.backgroundIconNode.frame = self.bounds + + let size = self.bounds.size + let textSize = self.textNode.updateLayout(size) + self.textNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - textSize.width) / 2.0), y: floorToScreenPixels((size.height - textSize.height) / 2.0) + UIScreenPixel), size: textSize) + } +} diff --git a/submodules/GalleryUI/Sources/ChatVideoGalleryItemScrubberView.swift b/submodules/GalleryUI/Sources/ChatVideoGalleryItemScrubberView.swift index 97e49dded0..dcb3b6d743 100644 --- a/submodules/GalleryUI/Sources/ChatVideoGalleryItemScrubberView.swift +++ b/submodules/GalleryUI/Sources/ChatVideoGalleryItemScrubberView.swift @@ -9,7 +9,7 @@ import Display import UniversalMediaPlayer import TelegramPresentationData -private let textFont = Font.regular(13.0) +private let textFont = Font.with(size: 13.0, design: .regular, weight: .regular, traits: [.monospacedNumbers]) private let scrubberBackgroundColor = UIColor(white: 1.0, alpha: 0.42) private let scrubberForegroundColor = UIColor.white @@ -120,7 +120,7 @@ final class ChatVideoGalleryItemScrubberView: UIView { self.fetchStatusDisposable.dispose() } - var collapsed: Bool = false + var collapsed: Bool? func setCollapsed(_ collapsed: Bool, animated: Bool) { guard self.collapsed != collapsed else { return @@ -128,15 +128,14 @@ final class ChatVideoGalleryItemScrubberView: UIView { self.collapsed = collapsed - guard let (size, _, _) = self.containerLayout else { - return - } - let alpha: CGFloat = collapsed ? 0.0 : 1.0 self.leftTimestampNode.alpha = alpha self.rightTimestampNode.alpha = alpha - self.infoNode.alpha = size.width < size.height && !self.collapsed ? 1.0 : 0.0 self.updateScrubberVisibility(animated: animated) + + if let (size, _, _) = self.containerLayout { + self.infoNode.alpha = size.width < size.height && !collapsed ? 1.0 : 0.0 + } } private func updateScrubberVisibility(animated: Bool) { @@ -144,10 +143,10 @@ final class ChatVideoGalleryItemScrubberView: UIView { var alpha: CGFloat = 1.0 if let playbackStatus = self.playbackStatus, playbackStatus.duration <= 30.0 { } else { - alpha = self.collapsed ? 0.0 : 1.0 + alpha = self.collapsed == true ? 0.0 : 1.0 collapsed = false } - self.scrubberNode.setCollapsed(collapsed, animated: animated) + self.scrubberNode.setCollapsed(collapsed == true, animated: animated) let transition: ContainedViewLayoutTransition = animated ? .animated(duration: 0.3, curve: .linear) : .immediate transition.updateAlpha(node: self.scrubberNode, alpha: alpha) } @@ -226,6 +225,7 @@ final class ChatVideoGalleryItemScrubberView: UIView { } func setFetchStatusSignal(_ fetchStatus: Signal?, strings: PresentationStrings, decimalSeparator: String, fileSize: Int?) { + let formatting = DataSizeStringFormatting(strings: strings, decimalSeparator: decimalSeparator) if let fileSize = fileSize { if let fetchStatus = fetchStatus { self.fetchStatusDisposable.set((fetchStatus @@ -234,9 +234,9 @@ final class ChatVideoGalleryItemScrubberView: UIView { var text: String switch status { case .Remote: - text = dataSizeString(fileSize, forceDecimal: true, decimalSeparator: decimalSeparator) + text = dataSizeString(fileSize, forceDecimal: true, formatting: formatting) case let .Fetching(_, progress): - text = strings.DownloadingStatus(dataSizeString(Int64(Float(fileSize) * progress), forceDecimal: true, decimalSeparator: decimalSeparator), dataSizeString(fileSize, forceDecimal: true, decimalSeparator: decimalSeparator)).0 + text = strings.DownloadingStatus(dataSizeString(Int64(Float(fileSize) * progress), forceDecimal: true, formatting: formatting), dataSizeString(fileSize, forceDecimal: true, formatting: formatting)).0 default: text = "" } @@ -248,7 +248,7 @@ final class ChatVideoGalleryItemScrubberView: UIView { } })) } else { - self.infoNode.attributedText = NSAttributedString(string: dataSizeString(fileSize, forceDecimal: true, decimalSeparator: decimalSeparator), font: textFont, textColor: .white) + self.infoNode.attributedText = NSAttributedString(string: dataSizeString(fileSize, forceDecimal: true, formatting: formatting), font: textFont, textColor: .white) } } else { self.infoNode.attributedText = nil @@ -284,7 +284,7 @@ final class ChatVideoGalleryItemScrubberView: UIView { let infoSize = self.infoNode.measure(infoConstrainedSize) self.infoNode.bounds = CGRect(origin: CGPoint(), size: infoSize) transition.updatePosition(node: self.infoNode, position: CGPoint(x: size.width / 2.0, y: infoOffset + infoSize.height / 2.0)) - self.infoNode.alpha = size.width < size.height && !self.collapsed ? 1.0 : 0.0 + self.infoNode.alpha = size.width < size.height && self.collapsed == false ? 1.0 : 0.0 self.scrubberNode.frame = CGRect(origin: CGPoint(x: scrubberInset, y: 6.0), size: CGSize(width: size.width - leftInset - rightInset - scrubberInset * 2.0, height: scrubberHeight)) } diff --git a/submodules/GalleryUI/Sources/GalleryController.swift b/submodules/GalleryUI/Sources/GalleryController.swift index a35425a0fe..d55e5ff53f 100644 --- a/submodules/GalleryUI/Sources/GalleryController.swift +++ b/submodules/GalleryUI/Sources/GalleryController.swift @@ -323,8 +323,8 @@ public struct GalleryConfiguration { } public class GalleryController: ViewController, StandalonePresentableController { - public static let darkNavigationTheme = NavigationBarTheme(buttonColor: .white, disabledButtonColor: UIColor(rgb: 0x525252), primaryTextColor: .white, backgroundColor: UIColor(white: 0.0, alpha: 0.6), separatorColor: UIColor(white: 0.0, alpha: 0.8), badgeBackgroundColor: .clear, badgeStrokeColor: .clear, badgeTextColor: .clear) - public static let lightNavigationTheme = NavigationBarTheme(buttonColor: UIColor(rgb: 0x007ee5), disabledButtonColor: UIColor(rgb: 0xd0d0d0), primaryTextColor: .black, backgroundColor: UIColor(red: 0.968626451, green: 0.968626451, blue: 0.968626451, alpha: 1.0), separatorColor: UIColor(red: 0.6953125, green: 0.6953125, blue: 0.6953125, alpha: 1.0), badgeBackgroundColor: .clear, badgeStrokeColor: .clear, badgeTextColor: .clear) + public static let darkNavigationTheme = NavigationBarTheme(buttonColor: .white, disabledButtonColor: UIColor(rgb: 0x525252), primaryTextColor: .white, backgroundColor: UIColor(white: 0.0, alpha: 0.6), enableBackgroundBlur: false, separatorColor: UIColor(white: 0.0, alpha: 0.8), badgeBackgroundColor: .clear, badgeStrokeColor: .clear, badgeTextColor: .clear) + public static let lightNavigationTheme = NavigationBarTheme(buttonColor: UIColor(rgb: 0x007ee5), disabledButtonColor: UIColor(rgb: 0xd0d0d0), primaryTextColor: .black, backgroundColor: UIColor(red: 0.968626451, green: 0.968626451, blue: 0.968626451, alpha: 1.0), enableBackgroundBlur: false, separatorColor: UIColor(red: 0.6953125, green: 0.6953125, blue: 0.6953125, alpha: 1.0), badgeBackgroundColor: .clear, badgeStrokeColor: .clear, badgeTextColor: .clear) private var galleryNode: GalleryControllerNode { return self.displayNode as! GalleryControllerNode @@ -384,6 +384,10 @@ public class GalleryController: ViewController, StandalonePresentableController private var screenCaptureEventsDisposable: Disposable? + public var centralItemUpdated: ((MessageId) -> Void)? + + private var initialOrientation: UIInterfaceOrientation? + 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, Promise?) -> Void, baseNavigationController: NavigationController?, actionInteraction: GalleryControllerActionInteraction? = nil) { self.context = context self.source = source @@ -665,7 +669,10 @@ public class GalleryController: ViewController, StandalonePresentableController openActionOptionsImpl = { [weak self] action in if let strongSelf = self { - let presentationData = strongSelf.presentationData + var presentationData = strongSelf.presentationData + if !presentationData.theme.overallDarkAppearance { + presentationData = presentationData.withUpdated(theme: defaultDarkColorPresentationTheme) + } switch action { case let .url(url, _): var cleanUrl = url @@ -673,7 +680,7 @@ public class GalleryController: ViewController, StandalonePresentableController let canOpenIn = availableOpenInOptions(context: strongSelf.context, item: .url(url: url)).count > 1 let mailtoString = "mailto:" let telString = "tel:" - var openText = strongSelf.presentationData.strings.Conversation_LinkDialogOpen + var openText = presentationData.strings.Conversation_LinkDialogOpen var phoneNumber: String? var isEmail = false @@ -686,12 +693,12 @@ public class GalleryController: ViewController, StandalonePresentableController canAddToReadingList = false phoneNumber = String(cleanUrl[cleanUrl.index(cleanUrl.startIndex, offsetBy: telString.distance(from: telString.startIndex, to: telString.endIndex))...]) cleanUrl = phoneNumber! - openText = strongSelf.presentationData.strings.UserInfo_PhoneCall + openText = presentationData.strings.UserInfo_PhoneCall isPhoneNumber = true } else if canOpenIn { - openText = strongSelf.presentationData.strings.Conversation_FileOpenIn + openText = presentationData.strings.Conversation_FileOpenIn } - let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData.withUpdated(theme: defaultDarkColorPresentationTheme)) + let actionSheet = ActionSheetController(presentationData: presentationData) var items: [ActionSheetItem] = [] items.append(ActionSheetTextItem(title: cleanUrl)) @@ -707,7 +714,7 @@ public class GalleryController: ViewController, StandalonePresentableController } })) if let phoneNumber = phoneNumber { - items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_AddContact, color: .accent, action: { [weak actionSheet] in + items.append(ActionSheetButtonItem(title: presentationData.strings.Conversation_AddContact, color: .accent, action: { [weak actionSheet] in actionSheet?.dismissAnimated() if let strongSelf = self { strongSelf.dismiss(forceAway: false) @@ -715,7 +722,7 @@ public class GalleryController: ViewController, StandalonePresentableController } })) } - items.append(ActionSheetButtonItem(title: canAddToReadingList ? strongSelf.presentationData.strings.ShareMenu_CopyShareLink : strongSelf.presentationData.strings.Conversation_ContextMenuCopy, color: .accent, action: { [weak actionSheet, weak self] in + items.append(ActionSheetButtonItem(title: canAddToReadingList ? presentationData.strings.ShareMenu_CopyShareLink : presentationData.strings.Conversation_ContextMenuCopy, color: .accent, action: { [weak actionSheet, weak self] in actionSheet?.dismissAnimated() UIPasteboard.general.string = cleanUrl @@ -732,7 +739,7 @@ public class GalleryController: ViewController, StandalonePresentableController self?.present(UndoOverlayController(presentationData: presentationData, content: content, elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root)) })) if canAddToReadingList { - items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_AddToReadingList, color: .accent, action: { [weak actionSheet] in + items.append(ActionSheetButtonItem(title: 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) @@ -740,13 +747,13 @@ public class GalleryController: ViewController, StandalonePresentableController })) } actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in + ActionSheetButtonItem(title: 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(presentationData: strongSelf.presentationData.withUpdated(theme: defaultDarkColorPresentationTheme)) + let actionSheet = ActionSheetController(presentationData: presentationData) var items: [ActionSheetItem] = [] if !mention.isEmpty { items.append(ActionSheetTextItem(title: mention)) @@ -774,7 +781,7 @@ public class GalleryController: ViewController, StandalonePresentableController ])]) strongSelf.present(actionSheet, in: .window(.root)) case let .textMention(mention): - let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData.withUpdated(theme: defaultDarkColorPresentationTheme)) + let actionSheet = ActionSheetController(presentationData: presentationData) actionSheet.setItemGroups([ActionSheetItemGroup(items: [ ActionSheetTextItem(title: mention), ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogOpen, color: .accent, action: { [weak actionSheet] in @@ -798,7 +805,7 @@ public class GalleryController: ViewController, StandalonePresentableController ])]) strongSelf.present(actionSheet, in: .window(.root)) case let .botCommand(command): - let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData.withUpdated(theme: defaultDarkColorPresentationTheme)) + let actionSheet = ActionSheetController(presentationData: presentationData) var items: [ActionSheetItem] = [] items.append(ActionSheetTextItem(title: command)) items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogCopy, color: .accent, action: { [weak actionSheet, weak self] in @@ -815,7 +822,7 @@ public class GalleryController: ViewController, StandalonePresentableController ])]) strongSelf.present(actionSheet, in: .window(.root)) case let .hashtag(peerName, hashtag): - let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData.withUpdated(theme: defaultDarkColorPresentationTheme)) + let actionSheet = ActionSheetController(presentationData: presentationData) actionSheet.setItemGroups([ActionSheetItemGroup(items: [ ActionSheetTextItem(title: hashtag), ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogOpen, color: .accent, action: { [weak actionSheet] in @@ -840,7 +847,7 @@ public class GalleryController: ViewController, StandalonePresentableController ]) strongSelf.present(actionSheet, in: .window(.root)) case let .timecode(timecode, text): - let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData.withUpdated(theme: defaultDarkColorPresentationTheme)) + let actionSheet = ActionSheetController(presentationData: presentationData) actionSheet.setItemGroups([ActionSheetItemGroup(items: [ ActionSheetTextItem(title: text), ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogOpen, color: .accent, action: { [weak actionSheet] in @@ -878,7 +885,7 @@ public class GalleryController: ViewController, StandalonePresentableController self.screenCaptureEventsDisposable = (screenCaptureEvents() |> deliverOnMainQueue).start(next: { [weak self] _ in if let strongSelf = self, strongSelf.traceVisibility() { - let _ = addSecretChatMessageScreenshot(account: strongSelf.context.account, peerId: id.peerId).start() + let _ = strongSelf.context.engine.messages.addSecretChatMessageScreenshot(peerId: id.peerId).start() } }) } @@ -892,6 +899,10 @@ public class GalleryController: ViewController, StandalonePresentableController } deinit { + if let initialOrientation = self.initialOrientation { + self.context.sharedContext.applicationBindings.forceOrientation(initialOrientation) + } + self.accountInUseDisposable.dispose() self.disposable.dispose() self.centralItemAttributesDisposable.dispose() @@ -1014,6 +1025,17 @@ public class GalleryController: ViewController, StandalonePresentableController self?.galleryNode.pager.centralItemNode()?.controlsVisibilityUpdated(isVisible: visible) } + self.galleryNode.updateOrientation = { [weak self] orientation in + if let strongSelf = self { + if strongSelf.initialOrientation == nil { + strongSelf.initialOrientation = orientation == .portrait ? .landscapeRight : .portrait + } else if strongSelf.initialOrientation == orientation { + strongSelf.initialOrientation = nil + } + strongSelf.context.sharedContext.applicationBindings.forceOrientation(orientation) + } + } + let baseNavigationController = self.baseNavigationController self.galleryNode.baseNavigationController = { [weak baseNavigationController] in return baseNavigationController @@ -1189,6 +1211,9 @@ public class GalleryController: ViewController, StandalonePresentableController } if strongSelf.didSetReady { strongSelf._hiddenMedia.set(.single(hiddenItem)) + if let hiddenItem = hiddenItem { + strongSelf.centralItemUpdated?(hiddenItem.0) + } } } } @@ -1283,7 +1308,7 @@ public class GalleryController: ViewController, StandalonePresentableController super.containerLayoutUpdated(layout, transition: transition) self.galleryNode.frame = CGRect(origin: CGPoint(), size: layout.size) - self.galleryNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationHeight, transition: transition) + self.galleryNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition) if !self.adjustedForInitialPreviewingLayout && self.isPresentedInPreviewingContext() { self.adjustedForInitialPreviewingLayout = true diff --git a/submodules/GalleryUI/Sources/GalleryControllerNode.swift b/submodules/GalleryUI/Sources/GalleryControllerNode.swift index 314dbb9b5d..06780433d2 100644 --- a/submodules/GalleryUI/Sources/GalleryControllerNode.swift +++ b/submodules/GalleryUI/Sources/GalleryControllerNode.swift @@ -30,6 +30,8 @@ open class GalleryControllerNode: ASDisplayNode, UIScrollViewDelegate, UIGesture public var areControlsHidden = false public var controlsVisibilityChanged: ((Bool) -> Void)? + public var updateOrientation: ((UIInterfaceOrientation) -> Void)? + public var isBackgroundExtendedOverNavigationBar = true { didSet { if let (navigationBarHeight, layout) = self.containerLayout { @@ -38,7 +40,7 @@ open class GalleryControllerNode: ASDisplayNode, UIScrollViewDelegate, UIGesture } } - public init(controllerInteraction: GalleryControllerInteraction, pageGap: CGFloat = 20.0) { + public init(controllerInteraction: GalleryControllerInteraction, pageGap: CGFloat = 20.0, disableTapNavigation: Bool = false) { self.backgroundNode = ASDisplayNode() self.backgroundNode.backgroundColor = UIColor.black self.scrollView = UIScrollView() @@ -48,7 +50,7 @@ open class GalleryControllerNode: ASDisplayNode, UIScrollViewDelegate, UIGesture self.scrollView.contentInsetAdjustmentBehavior = .never } - self.pager = GalleryPagerNode(pageGap: pageGap) + self.pager = GalleryPagerNode(pageGap: pageGap, disableTapNavigation: disableTapNavigation) self.footerNode = GalleryFooterNode(controllerInteraction: controllerInteraction) super.init() @@ -69,6 +71,12 @@ open class GalleryControllerNode: ASDisplayNode, UIScrollViewDelegate, UIGesture } } + self.pager.updateOrientation = { [weak self] orientation in + if let strongSelf = self { + strongSelf.updateOrientation?(orientation) + } + } + self.pager.dismiss = { [weak self] in if let strongSelf = self { var interfaceAnimationCompleted = false @@ -275,6 +283,7 @@ open class GalleryControllerNode: ASDisplayNode, UIScrollViewDelegate, UIGesture } self.pager.frame = CGRect(origin: CGPoint(x: 0.0, y: layout.size.height), size: layout.size) + self.pager.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition) } diff --git a/submodules/GalleryUI/Sources/GalleryFooterNode.swift b/submodules/GalleryUI/Sources/GalleryFooterNode.swift index 57ef587554..95acee8794 100644 --- a/submodules/GalleryUI/Sources/GalleryFooterNode.swift +++ b/submodules/GalleryUI/Sources/GalleryFooterNode.swift @@ -6,6 +6,7 @@ import Display public final class GalleryFooterNode: ASDisplayNode { private let backgroundNode: ASDisplayNode + private var currentThumbnailPanelHeight: CGFloat? private var currentFooterContentNode: GalleryFooterContentNode? private var currentOverlayContentNode: GalleryOverlayContentNode? private var currentLayout: (ContainerViewLayout, CGFloat, Bool)? @@ -36,11 +37,16 @@ public final class GalleryFooterNode: ASDisplayNode { let cleanInsets = layout.insets(options: []) var dismissedCurrentFooterContentNode: GalleryFooterContentNode? + var dismissedThumbnailPanelHeight: CGFloat? if self.currentFooterContentNode !== footerContentNode { if let currentFooterContentNode = self.currentFooterContentNode { currentFooterContentNode.requestLayout = nil dismissedCurrentFooterContentNode = currentFooterContentNode } + if let currentThumbnailPanelHeight = self.currentThumbnailPanelHeight { + dismissedThumbnailPanelHeight = currentThumbnailPanelHeight + } + self.currentThumbnailPanelHeight = thumbnailPanelHeight self.currentFooterContentNode = footerContentNode if let footerContentNode = footerContentNode { footerContentNode.setVisibilityAlpha(self.visibilityAlpha, animated: transition.isAnimated) @@ -54,22 +60,25 @@ public final class GalleryFooterNode: ASDisplayNode { } } + var animateOverlayIn = false var dismissedCurrentOverlayContentNode: GalleryOverlayContentNode? if self.currentOverlayContentNode !== overlayContentNode { if let currentOverlayContentNode = self.currentOverlayContentNode { dismissedCurrentOverlayContentNode = currentOverlayContentNode } self.currentOverlayContentNode = overlayContentNode + animateOverlayIn = true if let overlayContentNode = overlayContentNode { overlayContentNode.setVisibilityAlpha(self.visibilityAlpha) self.addSubnode(overlayContentNode) } } + let effectiveThumbnailPanelHeight = self.currentThumbnailPanelHeight ?? thumbnailPanelHeight var backgroundHeight: CGFloat = 0.0 - let verticalOffset: CGFloat = isHidden ? (layout.size.width > layout.size.height ? 44.0 : (thumbnailPanelHeight > 0.0 ? 106.0 : 54.0)) : 0.0 + let verticalOffset: CGFloat = isHidden ? (layout.size.width > layout.size.height ? 44.0 : (effectiveThumbnailPanelHeight > 0.0 ? 106.0 : 54.0)) : 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) + backgroundHeight = footerContentNode.updateLayout(size: layout.size, metrics: layout.metrics, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, bottomInset: cleanInsets.bottom, contentInset: effectiveThumbnailPanelHeight, transition: transition) transition.updateFrame(node: footerContentNode, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - backgroundHeight + verticalOffset), size: CGSize(width: layout.size.width, height: backgroundHeight))) if let dismissedCurrentFooterContentNode = dismissedCurrentFooterContentNode { let contentTransition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring) @@ -96,7 +105,9 @@ public final class GalleryFooterNode: ASDisplayNode { 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 animateOverlayIn { + 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 { diff --git a/submodules/GalleryUI/Sources/GalleryItemNode.swift b/submodules/GalleryUI/Sources/GalleryItemNode.swift index 7f90361440..6903789863 100644 --- a/submodules/GalleryUI/Sources/GalleryItemNode.swift +++ b/submodules/GalleryUI/Sources/GalleryItemNode.swift @@ -22,6 +22,7 @@ open class GalleryItemNode: ASDisplayNode { public var toggleControlsVisibility: () -> Void = { } public var updateControlsVisibility: (Bool) -> Void = { _ in } + public var updateOrientation: (UIInterfaceOrientation) -> Void = { _ in } public var dismiss: () -> Void = { } public var beginCustomDismiss: () -> Void = { } public var completeCustomDismiss: () -> Void = { } diff --git a/submodules/GalleryUI/Sources/GalleryPagerNode.swift b/submodules/GalleryUI/Sources/GalleryPagerNode.swift index 199d822c0b..b6d9e6b6fa 100644 --- a/submodules/GalleryUI/Sources/GalleryPagerNode.swift +++ b/submodules/GalleryUI/Sources/GalleryPagerNode.swift @@ -9,7 +9,8 @@ private func edgeWidth(width: CGFloat) -> CGFloat { return min(44.0, floor(width / 6.0)) } -private let leftFadeImage = generateImage(CGSize(width: 64.0, height: 1.0), opaque: false, rotatedContext: { size, context in +let fadeWidth: CGFloat = 70.0 +private let leftFadeImage = generateImage(CGSize(width: fadeWidth, height: 32.0), opaque: false, rotatedContext: { size, context in let bounds = CGRect(origin: CGPoint(), size: size) context.clear(bounds) @@ -19,10 +20,10 @@ private let leftFadeImage = generateImage(CGSize(width: 64.0, height: 1.0), opaq 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()) + context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: size.width, y: 0.0), options: CGGradientDrawingOptions()) }) -private let rightFadeImage = generateImage(CGSize(width: 64.0, height: 1.0), opaque: false, rotatedContext: { size, context in +private let rightFadeImage = generateImage(CGSize(width: fadeWidth, height: 32.0), opaque: false, rotatedContext: { size, context in let bounds = CGRect(origin: CGPoint(), size: size) context.clear(bounds) @@ -32,7 +33,7 @@ private let rightFadeImage = generateImage(CGSize(width: 64.0, height: 1.0), opa 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()) + context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: size.width, y: 0.0), options: CGGradientDrawingOptions()) }) public struct GalleryPagerInsertItem { @@ -77,11 +78,12 @@ public struct GalleryPagerTransaction { public final class GalleryPagerNode: ASDisplayNode, UIScrollViewDelegate, UIGestureRecognizerDelegate { private let pageGap: CGFloat + private let disableTapNavigation: Bool private let scrollView: UIScrollView - private let leftFadeNode: ASImageNode - private let rightFadeNode: ASImageNode + private let leftFadeNode: ASDisplayNode + private let rightFadeNode: ASDisplayNode private var highlightedSide: Bool? private var tapRecognizer: TapLongTapOrDoubleTapGestureRecognizer? @@ -105,27 +107,28 @@ public final class GalleryPagerNode: ASDisplayNode, UIScrollViewDelegate, UIGest public var centralItemIndexOffsetUpdated: (([GalleryItem]?, Int, CGFloat)?) -> Void = { _ in } public var toggleControlsVisibility: () -> Void = { } public var updateControlsVisibility: (Bool) -> Void = { _ in } + public var updateOrientation: (UIInterfaceOrientation) -> Void = { _ in } public var dismiss: () -> Void = { } public var beginCustomDismiss: () -> Void = { } public var completeCustomDismiss: () -> Void = { } public var baseNavigationController: () -> NavigationController? = { return nil } - public init(pageGap: CGFloat) { + public init(pageGap: CGFloat, disableTapNavigation: Bool) { self.pageGap = pageGap + self.disableTapNavigation = disableTapNavigation + self.scrollView = UIScrollView() if #available(iOSApplicationExtension 11.0, iOS 11.0, *) { self.scrollView.contentInsetAdjustmentBehavior = .never } - self.leftFadeNode = ASImageNode() - self.leftFadeNode.contentMode = .scaleToFill - self.leftFadeNode.image = leftFadeImage + self.leftFadeNode = ASDisplayNode() self.leftFadeNode.alpha = 0.0 + self.leftFadeNode.backgroundColor = leftFadeImage.flatMap { UIColor(patternImage: $0) } - self.rightFadeNode = ASImageNode() - self.rightFadeNode.contentMode = .scaleToFill - self.rightFadeNode.image = rightFadeImage + self.rightFadeNode = ASDisplayNode() self.rightFadeNode.alpha = 0.0 + self.rightFadeNode.backgroundColor = rightFadeImage.flatMap { UIColor(patternImage: $0) } super.init() @@ -293,7 +296,6 @@ public final class GalleryPagerNode: ASDisplayNode, UIScrollViewDelegate, UIGest transition.animatePosition(node: centralItemNode, from: centralItemNode.position.offsetBy(dx: -updatedCentralPoint.x + centralPoint.x, dy: -updatedCentralPoint.y + centralPoint.y)) } - 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) } @@ -436,6 +438,9 @@ public final class GalleryPagerNode: ASDisplayNode, UIScrollViewDelegate, UIGest } private func canGoToPreviousItem() -> Bool { + if self.disableTapNavigation { + return false + } if let index = self.centralItemIndex, index > 0 { return true } else { @@ -444,6 +449,9 @@ public final class GalleryPagerNode: ASDisplayNode, UIScrollViewDelegate, UIGest } private func canGoToNextItem() -> Bool { + if self.disableTapNavigation { + return false + } if let index = self.centralItemIndex, index < self.items.count - 1 { return true } else { @@ -467,6 +475,7 @@ public final class GalleryPagerNode: ASDisplayNode, UIScrollViewDelegate, UIGest let node = self.items[index].node(synchronous: synchronous) node.toggleControlsVisibility = self.toggleControlsVisibility node.updateControlsVisibility = self.updateControlsVisibility + node.updateOrientation = self.updateOrientation node.dismiss = self.dismiss node.beginCustomDismiss = self.beginCustomDismiss node.completeCustomDismiss = self.completeCustomDismiss diff --git a/submodules/GalleryUI/Sources/GalleryTitleView.swift b/submodules/GalleryUI/Sources/GalleryTitleView.swift index 89ceb199e5..475efaa33d 100644 --- a/submodules/GalleryUI/Sources/GalleryTitleView.swift +++ b/submodules/GalleryUI/Sources/GalleryTitleView.swift @@ -34,7 +34,7 @@ final class GalleryTitleView: UIView, NavigationBarTitleView { func setMessage(_ message: Message, presentationData: PresentationData, accountPeerId: PeerId) { let authorNameText = stringForFullAuthorName(message: message, strings: presentationData.strings, nameDisplayOrder: presentationData.nameDisplayOrder, accountPeerId: accountPeerId) - let dateText = humanReadableStringForTimestamp(strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, timestamp: message.timestamp) + let dateText = humanReadableStringForTimestamp(strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, timestamp: message.timestamp).0 self.authorNameNode.attributedText = NSAttributedString(string: authorNameText, font: titleFont, textColor: .white) self.dateNode.attributedText = NSAttributedString(string: dateText, font: dateFont, textColor: .white) diff --git a/submodules/GalleryUI/Sources/Items/ChatAnimationGalleryItem.swift b/submodules/GalleryUI/Sources/Items/ChatAnimationGalleryItem.swift index 90c12dedaa..3deeab5929 100644 --- a/submodules/GalleryUI/Sources/Items/ChatAnimationGalleryItem.swift +++ b/submodules/GalleryUI/Sources/Items/ChatAnimationGalleryItem.swift @@ -171,7 +171,7 @@ final class ChatAnimationGalleryItemNode: ZoomableContentGalleryItemNode { self.setupStatus(resource: fileReference.media.resource) - self._title.set(.single("\(fileReference.media.fileName ?? "") - \(dataSizeString(fileReference.media.size ?? 0, forceDecimal: false, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator))")) + self._title.set(.single("\(fileReference.media.fileName ?? "") - \(dataSizeString(fileReference.media.size ?? 0, forceDecimal: false, formatting: DataSizeStringFormatting(presentationData: self.presentationData)))")) let speedItem = UIBarButtonItem(image: UIImage(bundleImageName: "Media Gallery/SlowDown"), style: .plain, target: self, action: #selector(self.toggleSpeedButtonPressed)) let backgroundItem = UIBarButtonItem(image: backgroundButtonIcon, style: .plain, target: self, action: #selector(self.toggleBackgroundButtonPressed)) diff --git a/submodules/GalleryUI/Sources/Items/ChatImageGalleryItem.swift b/submodules/GalleryUI/Sources/Items/ChatImageGalleryItem.swift index 8e3bab9eb4..4ffe741eb3 100644 --- a/submodules/GalleryUI/Sources/Items/ChatImageGalleryItem.swift +++ b/submodules/GalleryUI/Sources/Items/ChatImageGalleryItem.swift @@ -353,7 +353,7 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode { |> delay(0.15, queue: Queue.mainQueue()) let progressDisposable = progressSignal.start() - let signal = stickerPacksAttachedToMedia(account: context.account, media: media) + let signal = context.engine.stickers.stickerPacksAttachedToMedia(media: media) |> afterDisposed { Queue.mainQueue().async { progressDisposable.dispose() @@ -487,7 +487,7 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode { surfaceCopyView.frame = transformedSurfaceFrame } - self.view.insertSubview(copyView, belowSubview: self.scrollNode.view) + //self.view.insertSubview(copyView, belowSubview: self.scrollNode.view) copyView.frame = transformedSelfFrame copyView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) diff --git a/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift b/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift index fe7bd8b2cf..217fbbd6ee 100644 --- a/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift +++ b/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift @@ -164,81 +164,82 @@ 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 let fullscreenImage = generateTintedImage(image: UIImage(bundleImageName: "Media Gallery/Fullscreen"), color: .white) +private let minimizeImage = generateTintedImage(image: UIImage(bundleImageName: "Media Gallery/Minimize"), color: .white) private final class UniversalVideoGalleryItemOverlayNode: GalleryOverlayContentNode { - private let soundButtonNode: HighlightableButtonNode + private let wrapperNode: ASDisplayNode + private let fullscreenNode: HighlightableButtonNode private var validLayout: (CGSize, LayoutMetrics, CGFloat, CGFloat, CGFloat)? + var action: ((Bool) -> Void)? + 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]) + self.wrapperNode = ASDisplayNode() + self.wrapperNode.alpha = 0.0 + self.fullscreenNode = HighlightableButtonNode() + self.fullscreenNode.setImage(fullscreenImage, for: .normal) + self.fullscreenNode.setImage(minimizeImage, for: .selected) + self.fullscreenNode.setImage(minimizeImage, 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 + self.addSubnode(self.wrapperNode) + self.wrapperNode.addSubnode(self.fullscreenNode) + + self.fullscreenNode.addTarget(self, action: #selector(self.toggleFullscreenPressed), forControlEvents: .touchUpInside) } 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) + let isLandscape = size.width > size.height + self.fullscreenNode.isSelected = isLandscape + + let iconSize: CGFloat = 42.0 + let inset: CGFloat = 4.0 + let buttonFrame = CGRect(origin: CGPoint(x: size.width - iconSize - inset - rightInset, y: size.height - iconSize - inset - bottomInset), size: CGSize(width: iconSize, height: iconSize)) + transition.updateFrame(node: self.wrapperNode, frame: buttonFrame) + transition.updateFrame(node: self.fullscreenNode, frame: CGRect(origin: CGPoint(), size: buttonFrame.size)) } override func animateIn(previousContentNode: GalleryOverlayContentNode?, transition: ContainedViewLayoutTransition) { - transition.updateAlpha(node: self.soundButtonNode, alpha: 1.0) + if !self.visibilityAlpha.isZero { + transition.updateAlpha(node: self.wrapperNode, alpha: 1.0) + } } override func animateOut(nextContentNode: GalleryOverlayContentNode?, transition: ContainedViewLayoutTransition, completion: @escaping () -> Void) { - transition.updateAlpha(node: self.soundButtonNode, alpha: 0.0) + transition.updateAlpha(node: self.wrapperNode, alpha: 0.0) } override func setVisibilityAlpha(_ alpha: CGFloat) { super.setVisibilityAlpha(alpha) - self.updateSoundButtonVisibility() + self.updateFullscreenButtonVisibility() } - func updateSoundButtonVisibility() { - if self.soundButtonNode.isSelected { - self.soundButtonNode.alpha = self.visibilityAlpha - } else { - self.soundButtonNode.alpha = 1.0 - } + func updateFullscreenButtonVisibility() { + self.wrapperNode.alpha = self.visibilityAlpha 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() + @objc func toggleFullscreenPressed() { + var toLandscape = false + if let (size, _, _, _ ,_) = self.validLayout, size.width < size.height { + toLandscape = true + } + if toLandscape { + self.wrapperNode.alpha = 0.0 + } + self.action?(toLandscape) } override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { - if !self.soundButtonNode.frame.contains(point) { + if !self.wrapperNode.frame.contains(point) { return nil } return super.hitTest(point, with: event) @@ -272,12 +273,13 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { private let statusNode: RadialStatusNode private var statusNodeShouldBeHidden = true - private var isCentral = false + private var isCentral: Bool? private var _isVisible: Bool? private var initiallyActivated = false private var hideStatusNodeUntilCentrality = false private var playOnContentOwnership = false private var skipInitialPause = false + private var ignorePauseStatus = false private var validLayout: (ContainerViewLayout, CGFloat)? private var didPause = false private var isPaused = true @@ -300,6 +302,11 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { private var scrubbingFrames = false private var scrubbingFrameDisposable: Disposable? + private let isPlayingPromise = ValuePromise(false, ignoreRepeated: true) + private let isInteractingPromise = ValuePromise(false, ignoreRepeated: true) + private let controlsVisiblePromise = ValuePromise(true, ignoreRepeated: true) + private var hideControlsDisposable: Disposable? + var playbackCompleted: (() -> Void)? private var customUnembedWhenPortrait: ((OverlayMediaItemNode) -> Bool)? @@ -324,25 +331,40 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { super.init() + self.footerContentNode.interacting = { [weak self] value in + self?.isInteractingPromise.set(value) + } + + self.overlayContentNode.action = { [weak self] toLandscape in + self?.updateControlsVisibility(!toLandscape) + self?.updateOrientation(toLandscape ? .landscapeRight : .portrait) + } + self.scrubberView.seek = { [weak self] timecode in self?.videoNode?.seek(timecode) } self.scrubberView.updateScrubbing = { [weak self] timecode in - guard let strongSelf = self, let videoFramePreview = strongSelf.videoFramePreview else { + guard let strongSelf = self else { return } - if let timecode = timecode { - if !strongSelf.scrubbingFrames { - strongSelf.scrubbingFrames = true - strongSelf.scrubbingFrame.set(videoFramePreview.generatedFrames - |> map(Optional.init)) + + strongSelf.isInteractingPromise.set(timecode != nil) + + if let videoFramePreview = strongSelf.videoFramePreview { + if let timecode = timecode { + if !strongSelf.scrubbingFrames { + strongSelf.scrubbingFrames = true + strongSelf.scrubbingFrame.set(videoFramePreview.generatedFrames + |> map(Optional.init)) + } + videoFramePreview.generateFrame(at: timecode) + } else { + strongSelf.isInteractingPromise.set(false) + strongSelf.scrubbingFrame.set(.single(nil)) + videoFramePreview.cancelPendingFrames() + strongSelf.scrubbingFrames = false } - videoFramePreview.generateFrame(at: timecode) - } else { - strongSelf.scrubbingFrame.set(.single(nil)) - videoFramePreview.cancelPendingFrames() - strongSelf.scrubbingFrames = false } } @@ -432,12 +454,30 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { self.titleContentView = GalleryTitleView(frame: CGRect()) self._titleView.set(.single(self.titleContentView)) + + let shouldHideControlsSignal: Signal = combineLatest(self.isPlayingPromise.get(), self.isInteractingPromise.get(), self.controlsVisiblePromise.get()) + |> mapToSignal { isPlaying, isIntracting, controlsVisible -> Signal in + if isPlaying && !isIntracting && controlsVisible { + return .single(Void()) + |> delay(4.0, queue: Queue.mainQueue()) + } else { + return .complete() + } + } + + self.hideControlsDisposable = (shouldHideControlsSignal + |> deliverOnMainQueue).start(next: { [weak self] _ in + if let strongSelf = self { + strongSelf.updateControlsVisibility(false) + } + }) } deinit { self.statusDisposable.dispose() self.mediaPlaybackStateDisposable.dispose() self.scrubbingFrameDisposable?.dispose() + self.hideControlsDisposable?.dispose() } override func ready() -> Signal { @@ -474,27 +514,20 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { pictureInPictureNode.updateLayout(placeholderSize, transition: transition) } } - + if dismiss { self.dismiss() } } - private var controlsTimer: SwiftSignalKit.Timer? - private var previousPlaying: Bool? - - private func setupControlsTimer() { - - } - func setupItem(_ item: UniversalVideoGalleryItem) { if self.item?.content.id != item.content.id { - self.previousPlaying = nil + self.isPlayingPromise.set(false) if item.hideControls { self.statusButtonNode.isHidden = true } - + self.dismissOnOrientationChange = item.landscape var hasLinkedStickers = false @@ -529,6 +562,13 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { forceEnablePiP = true } + let dimensions = item.content.dimensions + if dimensions.height > 0.0 { + if dimensions.width / dimensions.height < 1.33 || isAnimated { + self.overlayContentNode.isHidden = true + } + } + if let videoNode = self.videoNode { videoNode.canAttachContent = false videoNode.removeFromSupernode() @@ -571,7 +611,6 @@ 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) } @@ -600,7 +639,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { } 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 * 10.0 { var timestamp: Double? if status.timestamp > 5.0 && status.timestamp < status.duration - 5.0 { timestamp = status.timestamp @@ -650,7 +689,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { |> deliverOnMainQueue).start(next: { [weak self] value, fetchStatus in if let strongSelf = self { var initialBuffering = false - var playing = false + var isPlaying = false var isPaused = true var seekable = hintSeekable var hasStarted = false @@ -668,7 +707,8 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { switch value.status { case .playing: isPaused = false - playing = true + isPlaying = true + strongSelf.ignorePauseStatus = false case let .buffering(_, whilePlaying, _, display): displayProgress = display initialBuffering = !whilePlaying @@ -701,9 +741,10 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { isPaused = false } } else if strongSelf.actionAtEnd == .stop { - strongSelf.updateControlsVisibility(true) - strongSelf.controlsTimer?.invalidate() - strongSelf.controlsTimer = nil + strongSelf.isPlayingPromise.set(false) + if strongSelf.isCentral == true { + strongSelf.updateControlsVisibility(true) + } } } if !value.duration.isZero { @@ -711,20 +752,11 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { } } - if strongSelf.isCentral && playing && strongSelf.previousPlaying != true && !disablePlayerControls { - strongSelf.controlsTimer?.invalidate() - - let timer = SwiftSignalKit.Timer(timeout: 3.0, repeat: false, completion: { [weak self] in - self?.updateControlsVisibility(false) - self?.controlsTimer = nil - }, queue: Queue.mainQueue()) - timer.start() - strongSelf.controlsTimer = timer - } else if !playing { - strongSelf.controlsTimer?.invalidate() - strongSelf.controlsTimer = nil + if !disablePlayerControls && strongSelf.isCentral == true && isPlaying { + strongSelf.isPlayingPromise.set(true) + } else if !isPlaying { + strongSelf.isPlayingPromise.set(false) } - strongSelf.previousPlaying = playing var fetching = false if initialBuffering { @@ -742,7 +774,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { case .Remote: state = .download(.white) case let .Fetching(_, progress): - if !playing { + if !isPlaying { fetching = true isPaused = true } @@ -759,13 +791,13 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { strongSelf.fetchStatus = fetchStatus if !item.hideControls { - strongSelf.statusNodeShouldBeHidden = (!initialBuffering && (strongSelf.didPause || !isPaused) && !fetching) + strongSelf.statusNodeShouldBeHidden = strongSelf.ignorePauseStatus || (!initialBuffering && (strongSelf.didPause || !isPaused) && !fetching) strongSelf.statusButtonNode.isHidden = strongSelf.hideStatusNodeUntilCentrality || strongSelf.statusNodeShouldBeHidden } if isAnimated || disablePlayerControls { strongSelf.footerContentNode.content = .info - } else if isPaused { + } else if isPaused && !strongSelf.ignorePauseStatus { if hasStarted || strongSelf.didPause { strongSelf.footerContentNode.content = .playback(paused: true, seekable: seekable) } else if let fetchStatus = fetchStatus, !strongSelf.requiresDownload { @@ -799,10 +831,9 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { if let strongSelf = self, !isAnimated { videoNode?.seek(0.0) - if strongSelf.actionAtEnd == .stop && strongSelf.isCentral { + if strongSelf.actionAtEnd == .stop && strongSelf.isCentral == true { + strongSelf.isPlayingPromise.set(false) strongSelf.updateControlsVisibility(true) - strongSelf.controlsTimer?.invalidate() - strongSelf.controlsTimer = nil } } } @@ -825,8 +856,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { } override func controlsVisibilityUpdated(isVisible: Bool) { - self.controlsTimer?.invalidate() - self.controlsTimer = nil + self.controlsVisiblePromise.set(isVisible) self.videoNode?.isUserInteractionEnabled = isVisible ? self.videoNodeUserInteractionEnabled : false self.videoNode?.notifyPlaybackControlsHidden(!isVisible) @@ -906,8 +936,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { } } } else { - self.controlsTimer?.invalidate() - self.controlsTimer = nil + self.isPlayingPromise.set(false) self.dismissOnOrientationChange = false if videoNode.ownsContentNode { @@ -932,6 +961,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { if self.skipInitialPause { self.skipInitialPause = false } else { + self.ignorePauseStatus = true videoNode.pause() videoNode.seek(0.0) } @@ -964,7 +994,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { } override func activateAsInitial() { - if let videoNode = self.videoNode, self.isCentral { + if let videoNode = self.videoNode, self.isCentral == true { self.initiallyActivated = true var isAnimated = false @@ -1605,7 +1635,9 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { |> delay(0.15, queue: Queue.mainQueue()) let progressDisposable = progressSignal.start() - let signal = stickerPacksAttachedToMedia(account: self.context.account, media: media) + self.isInteractingPromise.set(true) + + let signal = self.context.engine.stickers.stickerPacksAttachedToMedia(media: media) |> afterDisposed { Queue.mainQueue().async { progressDisposable.dispose() @@ -1618,7 +1650,9 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { } let baseNavigationController = strongSelf.baseNavigationController() baseNavigationController?.view.endEditing(true) - let controller = StickerPackScreen(context: strongSelf.context, mainStickerPack: packs[0], stickerPacks: packs, sendSticker: nil) + let controller = StickerPackScreen(context: strongSelf.context, mainStickerPack: packs[0], stickerPacks: packs, sendSticker: nil, dismissed: { [weak self] in + self?.isInteractingPromise.set(false) + }) (baseNavigationController?.topViewController as? ViewController)?.present(controller, in: .window(.root), with: nil) }) } @@ -1631,6 +1665,6 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { } override func footerContent() -> Signal<(GalleryFooterContentNode?, GalleryOverlayContentNode?), NoError> { - return .single((self.footerContentNode, nil)) + return .single((self.footerContentNode, self.overlayContentNode)) } } diff --git a/submodules/GalleryUI/Sources/SecretMediaPreviewController.swift b/submodules/GalleryUI/Sources/SecretMediaPreviewController.swift index 8d23425be3..ba498852d8 100644 --- a/submodules/GalleryUI/Sources/SecretMediaPreviewController.swift +++ b/submodules/GalleryUI/Sources/SecretMediaPreviewController.swift @@ -349,9 +349,9 @@ public final class SecretMediaPreviewController: ViewController { |> 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() + 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, correlationId: nil)]).start() } else if strongSelf.messageId.peerId.namespace == Namespaces.Peer.SecretChat { - let _ = addSecretChatMessageScreenshot(account: strongSelf.context.account, peerId: strongSelf.messageId.peerId).start() + let _ = strongSelf.context.engine.messages.addSecretChatMessageScreenshot(peerId: strongSelf.messageId.peerId).start() } } }) @@ -450,7 +450,7 @@ public final class SecretMediaPreviewController: ViewController { self?.didSetReady = true } self._ready.set(ready |> map { true }) - self.markMessageAsConsumedDisposable.set(markMessageContentAsConsumedInteractively(postbox: self.context.account.postbox, messageId: message.id).start()) + self.markMessageAsConsumedDisposable.set(self.context.engine.messages.markMessageContentAsConsumedInteractively(messageId: message.id).start()) } else { var beginTimeAndTimeout: (Double, Double)? var videoDuration: Int32? @@ -497,7 +497,7 @@ public final class SecretMediaPreviewController: ViewController { super.containerLayoutUpdated(layout, transition: transition) self.controllerNode.frame = CGRect(origin: CGPoint(), size: layout.size) - self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationHeight, transition: transition) + self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition) } override public func dismiss(completion: (() -> Void)? = nil) { diff --git a/submodules/GameUI/Sources/GameController.swift b/submodules/GameUI/Sources/GameController.swift index d9cbd217e1..5b77a2a9f8 100644 --- a/submodules/GameUI/Sources/GameController.swift +++ b/submodules/GameUI/Sources/GameController.swift @@ -79,7 +79,7 @@ public final class GameController: 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.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition) } override public var presentationController: UIPresentationController? { diff --git a/submodules/GameUI/Sources/GameControllerNode.swift b/submodules/GameUI/Sources/GameControllerNode.swift index 51be76923e..02ef5c9839 100644 --- a/submodules/GameUI/Sources/GameControllerNode.swift +++ b/submodules/GameUI/Sources/GameControllerNode.swift @@ -147,7 +147,7 @@ final class GameControllerNode: ViewControllerTracingNode { if eventName == "share_score" { self.present(ShareController(context: self.context, subject: .fromExternal({ [weak self] peerIds, text, account in if let strongSelf = self { - let signals = peerIds.map { forwardGameWithScore(account: account, messageId: strongSelf.message.id, to: $0) } + let signals = peerIds.map { TelegramEngine(account: account).messages.forwardGameWithScore(messageId: strongSelf.message.id, to: $0) } return .single(.preparing) |> then( combineLatest(signals) diff --git a/submodules/GameUI/Sources/GameControllerTitleView.swift b/submodules/GameUI/Sources/GameControllerTitleView.swift index ec012f2fae..6c565228a9 100644 --- a/submodules/GameUI/Sources/GameControllerTitleView.swift +++ b/submodules/GameUI/Sources/GameControllerTitleView.swift @@ -40,7 +40,7 @@ final class GameControllerTitleView: UIView { } func set(title: String, subtitle: String) { - self.titleNode.attributedText = NSAttributedString(string: title, font: Font.medium(17.0), textColor: self.theme.rootController.navigationBar.primaryTextColor) + self.titleNode.attributedText = NSAttributedString(string: title, font: Font.semibold(17.0), textColor: self.theme.rootController.navigationBar.primaryTextColor) self.infoNode.attributedText = NSAttributedString(string: subtitle, font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor) } diff --git a/submodules/GradientBackground/BUILD b/submodules/GradientBackground/BUILD new file mode 100644 index 0000000000..f216710644 --- /dev/null +++ b/submodules/GradientBackground/BUILD @@ -0,0 +1,20 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "GradientBackground", + module_name = "GradientBackground", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-O", + ], + deps = [ + "//submodules/AsyncDisplayKit:AsyncDisplayKit", + "//submodules/Display:Display", + "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/GradientBackground/Sources/GradientBackground.swift b/submodules/GradientBackground/Sources/GradientBackground.swift new file mode 100644 index 0000000000..352d1aacf4 --- /dev/null +++ b/submodules/GradientBackground/Sources/GradientBackground.swift @@ -0,0 +1,8 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit + +public func createGradientBackgroundNode(colors: [UIColor]? = nil, useSharedAnimationPhase: Bool = false) -> GradientBackgroundNode { + return GradientBackgroundNode(colors: colors, useSharedAnimationPhase: useSharedAnimationPhase) +} diff --git a/submodules/GradientBackground/Sources/SoftwareGradientBackground.swift b/submodules/GradientBackground/Sources/SoftwareGradientBackground.swift new file mode 100644 index 0000000000..21fcde858a --- /dev/null +++ b/submodules/GradientBackground/Sources/SoftwareGradientBackground.swift @@ -0,0 +1,464 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import SwiftSignalKit +import Accelerate + +private func shiftArray(array: [CGPoint], offset: Int) -> [CGPoint] { + var newArray = array + var offset = offset + while offset > 0 { + let element = newArray.removeFirst() + newArray.append(element) + offset -= 1 + } + return newArray +} + +private func gatherPositions(_ list: [CGPoint]) -> [CGPoint] { + var result: [CGPoint] = [] + for i in 0 ..< list.count / 2 { + result.append(list[i * 2]) + } + return result +} + +private func interpolateFloat(_ value1: CGFloat, _ value2: CGFloat, at factor: CGFloat) -> CGFloat { + return value1 * (1.0 - factor) + value2 * factor +} + +private func interpolatePoints(_ point1: CGPoint, _ point2: CGPoint, at factor: CGFloat) -> CGPoint { + return CGPoint(x: interpolateFloat(point1.x, point2.x, at: factor), y: interpolateFloat(point1.y, point2.y, at: factor)) +} + +public func adjustSaturationInContext(context: DrawingContext, saturation: CGFloat) { + var buffer = vImage_Buffer() + buffer.data = context.bytes + buffer.width = UInt(context.size.width * context.scale) + buffer.height = UInt(context.size.height * context.scale) + buffer.rowBytes = context.bytesPerRow + + let divisor: Int32 = 0x1000 + + let rwgt: CGFloat = 0.3086 + let gwgt: CGFloat = 0.6094 + let bwgt: CGFloat = 0.0820 + + let adjustSaturation = saturation + + let a = (1.0 - adjustSaturation) * rwgt + adjustSaturation + let b = (1.0 - adjustSaturation) * rwgt + let c = (1.0 - adjustSaturation) * rwgt + let d = (1.0 - adjustSaturation) * gwgt + let e = (1.0 - adjustSaturation) * gwgt + adjustSaturation + let f = (1.0 - adjustSaturation) * gwgt + let g = (1.0 - adjustSaturation) * bwgt + let h = (1.0 - adjustSaturation) * bwgt + let i = (1.0 - adjustSaturation) * bwgt + adjustSaturation + + let satMatrix: [CGFloat] = [ + a, b, c, 0, + d, e, f, 0, + g, h, i, 0, + 0, 0, 0, 1 + ] + + var matrix: [Int16] = satMatrix.map { value in + return Int16(value * CGFloat(divisor)) + } + + vImageMatrixMultiply_ARGB8888(&buffer, &buffer, &matrix, divisor, nil, nil, vImage_Flags(kvImageDoNotTile)) +} + +private func generateGradient(size: CGSize, colors inputColors: [UIColor], positions: [CGPoint], adjustSaturation: CGFloat = 1.0) -> UIImage { + let colors: [UIColor] = inputColors.count == 1 ? [inputColors[0], inputColors[0], inputColors[0]] : inputColors + + let width = Int(size.width) + let height = Int(size.height) + + let rgbData = malloc(MemoryLayout.size * colors.count * 3)! + defer { + free(rgbData) + } + let rgb = rgbData.assumingMemoryBound(to: Float.self) + for i in 0 ..< colors.count { + var r: CGFloat = 0.0 + var g: CGFloat = 0.0 + var b: CGFloat = 0.0 + colors[i].getRed(&r, green: &g, blue: &b, alpha: nil) + + rgb.advanced(by: i * 3 + 0).pointee = Float(r) + rgb.advanced(by: i * 3 + 1).pointee = Float(g) + rgb.advanced(by: i * 3 + 2).pointee = Float(b) + } + + let positionData = malloc(MemoryLayout.size * positions.count * 2)! + defer { + free(positionData) + } + let positionFloats = positionData.assumingMemoryBound(to: Float.self) + for i in 0 ..< positions.count { + positionFloats.advanced(by: i * 2 + 0).pointee = Float(positions[i].x) + positionFloats.advanced(by: i * 2 + 1).pointee = Float(1.0 - positions[i].y) + } + + let context = DrawingContext(size: CGSize(width: CGFloat(width), height: CGFloat(height)), scale: 1.0, opaque: true, clear: false) + let imageBytes = context.bytes.assumingMemoryBound(to: UInt8.self) + + for y in 0 ..< height { + let directPixelY = Float(y) / Float(height) + let centerDistanceY = directPixelY - 0.5 + let centerDistanceY2 = centerDistanceY * centerDistanceY + + let lineBytes = imageBytes.advanced(by: context.bytesPerRow * y) + for x in 0 ..< width { + let directPixelX = Float(x) / Float(width) + + let centerDistanceX = directPixelX - 0.5 + let centerDistance = sqrt(centerDistanceX * centerDistanceX + centerDistanceY2) + + let swirlFactor = 0.35 * centerDistance + let theta = swirlFactor * swirlFactor * 0.8 * 8.0 + let sinTheta = sin(theta) + let cosTheta = cos(theta) + + let pixelX = max(0.0, min(1.0, 0.5 + centerDistanceX * cosTheta - centerDistanceY * sinTheta)) + let pixelY = max(0.0, min(1.0, 0.5 + centerDistanceX * sinTheta + centerDistanceY * cosTheta)) + + var distanceSum: Float = 0.0 + + var r: Float = 0.0 + var g: Float = 0.0 + var b: Float = 0.0 + + for i in 0 ..< colors.count { + let colorX = positionFloats[i * 2 + 0] + let colorY = positionFloats[i * 2 + 1] + + let distanceX = pixelX - colorX + let distanceY = pixelY - colorY + + var distance = max(0.0, 0.92 - sqrt(distanceX * distanceX + distanceY * distanceY)) + distance = distance * distance * distance + distanceSum += distance + + r = r + distance * rgb[i * 3 + 0] + g = g + distance * rgb[i * 3 + 1] + b = b + distance * rgb[i * 3 + 2] + } + + if distanceSum < 0.00001 { + distanceSum = 0.00001 + } + + var pixelB = b / distanceSum * 255.0 + if pixelB > 255.0 { + pixelB = 255.0 + } + + var pixelG = g / distanceSum * 255.0 + if pixelG > 255.0 { + pixelG = 255.0 + } + + var pixelR = r / distanceSum * 255.0 + if pixelR > 255.0 { + pixelR = 255.0 + } + + let pixelBytes = lineBytes.advanced(by: x * 4) + pixelBytes.advanced(by: 0).pointee = UInt8(pixelB) + pixelBytes.advanced(by: 1).pointee = UInt8(pixelG) + pixelBytes.advanced(by: 2).pointee = UInt8(pixelR) + pixelBytes.advanced(by: 3).pointee = 0xff + } + } + + if abs(adjustSaturation - 1.0) > .ulpOfOne { + adjustSaturationInContext(context: context, saturation: adjustSaturation) + } + + return context.generateImage()! +} + +public final class GradientBackgroundNode: ASDisplayNode { + public final class CloneNode: ASImageNode { + private weak var parentNode: GradientBackgroundNode? + private var index: SparseBag>.Index? + + public init(parentNode: GradientBackgroundNode) { + self.parentNode = parentNode + + super.init() + + self.index = parentNode.cloneNodes.add(Weak(self)) + self.image = parentNode.dimmedImage + } + + deinit { + if let parentNode = self.parentNode, let index = self.index { + parentNode.cloneNodes.remove(index) + } + } + } + + private static let basePositions: [CGPoint] = [ + CGPoint(x: 0.80, y: 0.10), + CGPoint(x: 0.60, y: 0.20), + CGPoint(x: 0.35, y: 0.25), + CGPoint(x: 0.25, y: 0.60), + CGPoint(x: 0.20, y: 0.90), + CGPoint(x: 0.40, y: 0.80), + CGPoint(x: 0.65, y: 0.75), + CGPoint(x: 0.75, y: 0.40) + ] + + public static func generatePreview(size: CGSize, colors: [UIColor]) -> UIImage { + let positions = gatherPositions(shiftArray(array: GradientBackgroundNode.basePositions, offset: 0)) + return generateGradient(size: size, colors: colors, positions: positions) + } + + private var colors: [UIColor] + private var phase: Int = 0 + + public let contentView: UIImageView + private var validPhase: Int? + private var invalidated: Bool = false + + private var dimmedImageParams: (size: CGSize, colors: [UIColor], positions: [CGPoint])? + private var _dimmedImage: UIImage? + private var dimmedImage: UIImage? { + if let current = self._dimmedImage { + return current + } else if let (size, colors, positions) = self.dimmedImageParams { + self._dimmedImage = generateGradient(size: size, colors: colors, positions: positions, adjustSaturation: 1.7) + return self._dimmedImage + } else { + return nil + } + } + + private var validLayout: CGSize? + private let cloneNodes = SparseBag>() + + private let useSharedAnimationPhase: Bool + static var sharedPhase: Int = 0 + + public init(colors: [UIColor]? = nil, useSharedAnimationPhase: Bool = false) { + self.useSharedAnimationPhase = useSharedAnimationPhase + self.contentView = UIImageView() + let defaultColors: [UIColor] = [ + UIColor(rgb: 0x7FA381), + UIColor(rgb: 0xFFF5C5), + UIColor(rgb: 0x336F55), + UIColor(rgb: 0xFBE37D) + ] + self.colors = colors ?? defaultColors + + super.init() + + self.view.addSubview(self.contentView) + + if useSharedAnimationPhase { + self.phase = GradientBackgroundNode.sharedPhase + } else { + self.phase = 0 + } + } + + deinit { + } + + public func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition, extendAnimation: Bool = false, backwards: Bool = false) { + let sizeUpdated = self.validLayout != size + self.validLayout = size + + let imageSize = size.fitted(CGSize(width: 80.0, height: 80.0)).integralFloor + + let positions = gatherPositions(shiftArray(array: GradientBackgroundNode.basePositions, offset: self.phase % 8)) + + if let validPhase = self.validPhase { + if validPhase != self.phase || self.invalidated { + self.validPhase = self.phase + self.invalidated = false + + var steps: [[CGPoint]] = [] + if backwards { + let phaseCount = extendAnimation ? 6 : 1 + self.phase = (self.phase + phaseCount) % 8 + self.validPhase = self.phase + + var stepPhase = self.phase - phaseCount + if stepPhase < 0 { + stepPhase = 8 + stepPhase + } + for _ in 0 ... phaseCount { + steps.append(gatherPositions(shiftArray(array: GradientBackgroundNode.basePositions, offset: stepPhase))) + stepPhase = (stepPhase + 1) % 8 + } + } else if extendAnimation { + let phaseCount = 4 + var stepPhase = (self.phase + phaseCount) % 8 + for _ in 0 ... phaseCount { + steps.append(gatherPositions(shiftArray(array: GradientBackgroundNode.basePositions, offset: stepPhase))) + stepPhase = stepPhase - 1 + if stepPhase < 0 { + stepPhase = 7 + } + } + } else { + steps.append(gatherPositions(shiftArray(array: GradientBackgroundNode.basePositions, offset: validPhase % 8))) + steps.append(positions) + } + + if case let .animated(duration, curve) = transition, duration > 0.001 { + var images: [UIImage] = [] + + var dimmedImages: [UIImage] = [] + let needDimmedImages = !self.cloneNodes.isEmpty + + let stepCount = steps.count - 1 + + let fps: Double = extendAnimation ? 60 : 30 + let maxFrame = Int(duration * fps) + let framesPerAnyStep = maxFrame / stepCount + + for frameIndex in 0 ..< maxFrame { + let t = curve.solve(at: CGFloat(frameIndex) / CGFloat(maxFrame - 1)) + let globalStep = Int(t * CGFloat(maxFrame)) + let stepIndex = min(stepCount - 1, globalStep / framesPerAnyStep) + + let stepFrameIndex = globalStep - stepIndex * framesPerAnyStep + let stepFrames: Int + if stepIndex == stepCount - 1 { + stepFrames = maxFrame - framesPerAnyStep * (stepCount - 1) + } else { + stepFrames = framesPerAnyStep + } + let stepT = CGFloat(stepFrameIndex) / CGFloat(stepFrames - 1) + + var morphedPositions: [CGPoint] = [] + for i in 0 ..< steps[0].count { + morphedPositions.append(interpolatePoints(steps[stepIndex][i], steps[stepIndex + 1][i], at: stepT)) + } + + images.append(generateGradient(size: imageSize, colors: self.colors, positions: morphedPositions)) + if needDimmedImages { + dimmedImages.append(generateGradient(size: imageSize, colors: self.colors, positions: morphedPositions, adjustSaturation: 1.7)) + } + } + + self.dimmedImageParams = (imageSize, self.colors, gatherPositions(shiftArray(array: GradientBackgroundNode.basePositions, offset: self.phase % 8))) + + self.contentView.image = images.last + + let animation = CAKeyframeAnimation(keyPath: "contents") + animation.values = images.map { $0.cgImage! } + animation.duration = duration * UIView.animationDurationFactor() + if backwards || extendAnimation { + animation.calculationMode = .discrete + } else { + animation.calculationMode = .linear + } + animation.isRemovedOnCompletion = true + if extendAnimation && !backwards { + animation.fillMode = .backwards + animation.beginTime = self.contentView.layer.convertTime(CACurrentMediaTime(), from: nil) + 0.25 + } + + self.contentView.layer.removeAnimation(forKey: "contents") + self.contentView.layer.add(animation, forKey: "contents") + + if !self.cloneNodes.isEmpty { + let cloneAnimation = CAKeyframeAnimation(keyPath: "contents") + cloneAnimation.values = dimmedImages.map { $0.cgImage! } + cloneAnimation.duration = animation.duration + cloneAnimation.calculationMode = animation.calculationMode + cloneAnimation.isRemovedOnCompletion = animation.isRemovedOnCompletion + cloneAnimation.fillMode = animation.fillMode + cloneAnimation.beginTime = animation.beginTime + + self._dimmedImage = dimmedImages.last + + for cloneNode in self.cloneNodes { + if let value = cloneNode.value { + value.image = dimmedImages.last + value.layer.removeAnimation(forKey: "contents") + value.layer.add(cloneAnimation, forKey: "contents") + } + } + } + } else { + let image = generateGradient(size: imageSize, colors: self.colors, positions: positions) + self.contentView.image = image + + let dimmedImage = generateGradient(size: imageSize, colors: self.colors, positions: positions, adjustSaturation: 1.7) + self._dimmedImage = dimmedImage + self.dimmedImageParams = (imageSize, self.colors, positions) + + for cloneNode in self.cloneNodes { + cloneNode.value?.image = dimmedImage + } + } + } + } else if sizeUpdated { + let image = generateGradient(size: imageSize, colors: self.colors, positions: positions) + self.contentView.image = image + + let dimmedImage = generateGradient(size: imageSize, colors: self.colors, positions: positions, adjustSaturation: 1.7) + self.dimmedImageParams = (imageSize, self.colors, positions) + + for cloneNode in self.cloneNodes { + cloneNode.value?.image = dimmedImage + } + + self.validPhase = self.phase + } + + transition.updateFrame(view: self.contentView, frame: CGRect(origin: CGPoint(), size: size)) + } + + public func updateColors(colors: [UIColor]) { + var updated = false + if self.colors.count != colors.count { + updated = true + } else { + for i in 0 ..< self.colors.count { + if !self.colors[i].isEqual(colors[i]) { + updated = true + break + } + } + } + if updated { + self.colors = colors + self.invalidated = true + if let size = self.validLayout { + self.updateLayout(size: size, transition: .immediate) + } + } + } + + public func animateEvent(transition: ContainedViewLayoutTransition, extendAnimation: Bool = false, backwards: Bool = false) { + guard case let .animated(duration, _) = transition, duration > 0.001 else { + return + } + + if extendAnimation || backwards { + self.invalidated = true + } else { + if self.phase == 0 { + self.phase = 7 + } else { + self.phase = self.phase - 1 + } + } + if self.useSharedAnimationPhase { + GradientBackgroundNode.sharedPhase = self.phase + } + if let size = self.validLayout { + self.updateLayout(size: size, transition: transition, extendAnimation: extendAnimation, backwards: backwards) + } + } +} diff --git a/submodules/GraphCore/Sources/Helpers/UIImage+Utils.swift b/submodules/GraphCore/Sources/Helpers/UIImage+Utils.swift index 76390fe4da..6408a97414 100644 --- a/submodules/GraphCore/Sources/Helpers/UIImage+Utils.swift +++ b/submodules/GraphCore/Sources/Helpers/UIImage+Utils.swift @@ -56,7 +56,7 @@ var deviceScale: CGFloat { func generateImage(_ size: CGSize, contextGenerator: (CGSize, CGContext) -> Void, opaque: Bool = false, scale: CGFloat? = nil) -> GImage? { let selectedScale = scale ?? deviceScale let scaledSize = CGSize(width: size.width * selectedScale, height: size.height * selectedScale) - let bytesPerRow = (4 * Int(scaledSize.width) + 15) & (~15) + let bytesPerRow = (4 * Int(scaledSize.width) + 31) & (~31) let length = bytesPerRow * Int(scaledSize.height) let bytes = malloc(length)!.assumingMemoryBound(to: Int8.self) diff --git a/submodules/HashtagSearchUI/Sources/HashtagSearchController.swift b/submodules/HashtagSearchUI/Sources/HashtagSearchController.swift index 03b576262f..982a409f23 100644 --- a/submodules/HashtagSearchUI/Sources/HashtagSearchController.swift +++ b/submodules/HashtagSearchUI/Sources/HashtagSearchController.swift @@ -40,10 +40,10 @@ 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, 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 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 location: SearchMessagesLocation = .general(tags: nil, minDate: nil, maxDate: nil) - let search = searchMessages(account: context.account, location: location, query: query, state: nil) + let search = context.engine.messages.searchMessages(location: location, query: query, state: nil) let foundMessages: Signal<[ChatListSearchEntry], NoError> = search |> map { result, _ in return result.messages.map({ .message($0, RenderedPeer(message: $0), result.readStates[$0.id.peerId], chatListPresentationData, result.totalCount, nil, false) }) @@ -52,12 +52,13 @@ public final class HashtagSearchController: TelegramBaseController { }, peerSelected: { _, _ in }, disabledPeerSelected: { _ in }, togglePeerSelected: { _ in + }, togglePeersSelection: { _, _ in }, additionalCategorySelected: { _ in }, messageSelected: { [weak self] peer, message, _ in if let strongSelf = self { strongSelf.openMessageFromSearchDisposable.set((storedMessageFromSearchPeer(account: strongSelf.context.account, peer: peer) |> deliverOnMainQueue).start(next: { actualPeerId in if let strongSelf = self, let navigationController = strongSelf.navigationController as? NavigationController { - strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(actualPeerId), subject: message.id.peerId == actualPeerId ? .message(id: message.id, highlight: true) : nil, keepStack: .always)) + strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(actualPeerId), subject: message.id.peerId == actualPeerId ? .message(id: message.id, highlight: true, timecode: nil) : nil, keepStack: .always)) } })) strongSelf.controllerNode.listNode.clearHighlightAnimated(true) @@ -115,17 +116,37 @@ public final class HashtagSearchController: TelegramBaseController { } override public func loadDisplayNode() { - self.displayNode = HashtagSearchControllerNode(context: self.context, peer: self.peer, query: self.query, theme: self.presentationData.theme, strings: self.presentationData.strings, navigationController: self.navigationController as? NavigationController) + self.displayNode = HashtagSearchControllerNode(context: self.context, peer: self.peer, query: self.query, theme: self.presentationData.theme, strings: self.presentationData.strings, navigationBar: self.navigationBar, navigationController: self.navigationController as? NavigationController) if let chatController = self.controllerNode.chatController { chatController.parentController = self } self.displayNodeDidLoad() } + + private var suspendNavigationBarLayout: Bool = false + private var suspendedNavigationBarLayout: ContainerViewLayout? + private var additionalNavigationBarBackgroundHeight: CGFloat = 0.0 + + override public func updateNavigationBarLayout(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + if self.suspendNavigationBarLayout { + self.suspendedNavigationBarLayout = layout + return + } + self.applyNavigationBarLayout(layout, navigationLayout: self.navigationLayout(layout: layout), additionalBackgroundHeight: self.additionalNavigationBarBackgroundHeight, transition: transition) + } override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + self.suspendNavigationBarLayout = true + super.containerLayoutUpdated(layout, transition: transition) - self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationHeight, transition: transition) + self.additionalNavigationBarBackgroundHeight = self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition) + + self.suspendNavigationBarLayout = false + if let suspendedNavigationBarLayout = self.suspendedNavigationBarLayout { + self.suspendedNavigationBarLayout = suspendedNavigationBarLayout + self.applyNavigationBarLayout(suspendedNavigationBarLayout, navigationLayout: self.navigationLayout(layout: layout), additionalBackgroundHeight: self.additionalNavigationBarBackgroundHeight, transition: transition) + } } } diff --git a/submodules/HashtagSearchUI/Sources/HashtagSearchControllerNode.swift b/submodules/HashtagSearchUI/Sources/HashtagSearchControllerNode.swift index fe178a5bab..b7b0f16a86 100644 --- a/submodules/HashtagSearchUI/Sources/HashtagSearchControllerNode.swift +++ b/submodules/HashtagSearchUI/Sources/HashtagSearchControllerNode.swift @@ -10,12 +10,14 @@ import ChatListUI import SegmentedControlNode final class HashtagSearchControllerNode: ASDisplayNode { - private let toolbarBackgroundNode: ASDisplayNode + private let navigationBar: NavigationBar? + + private let toolbarBackgroundNode: NavigationBackgroundNode private let toolbarSeparatorNode: ASDisplayNode private let segmentedControlNode: SegmentedControlNode let listNode: ListView - var chatController: ChatController? + let chatController: ChatController? private let context: AccountContext private let query: String @@ -24,9 +26,9 @@ final class HashtagSearchControllerNode: ASDisplayNode { private var enqueuedTransitions: [(ChatListSearchContainerTransition, Bool)] = [] private var hasValidLayout = false - var navigationBar: NavigationBar? - - init(context: AccountContext, peer: Peer?, query: String, theme: PresentationTheme, strings: PresentationStrings, navigationController: NavigationController?) { + init(context: AccountContext, peer: Peer?, query: String, theme: PresentationTheme, strings: PresentationStrings, navigationBar: NavigationBar?, navigationController: NavigationController?) { + self.navigationBar = navigationBar + self.context = context self.query = query self.listNode = ListView() @@ -34,8 +36,7 @@ final class HashtagSearchControllerNode: ASDisplayNode { return strings.VoiceOver_ScrollStatus(row, count).0 } - self.toolbarBackgroundNode = ASDisplayNode() - self.toolbarBackgroundNode.backgroundColor = theme.rootController.navigationBar.backgroundColor + self.toolbarBackgroundNode = NavigationBackgroundNode(color: theme.rootController.navigationBar.blurredBackgroundColor) self.toolbarSeparatorNode = ASDisplayNode() self.toolbarSeparatorNode.backgroundColor = theme.rootController.navigationBar.separatorColor @@ -102,13 +103,13 @@ final class HashtagSearchControllerNode: ASDisplayNode { } } - func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { + func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat { self.containerLayout = (layout, navigationBarHeight) - if self.chatController != nil && self.toolbarBackgroundNode.supernode == nil { - self.addSubnode(self.toolbarBackgroundNode) - self.addSubnode(self.toolbarSeparatorNode) - self.addSubnode(self.segmentedControlNode) + if self.chatController != nil && self.toolbarSeparatorNode.supernode == nil { + //self.addSubnode(self.toolbarBackgroundNode) + self.navigationBar?.additionalContentNode.addSubnode(self.toolbarSeparatorNode) + self.navigationBar?.additionalContentNode.addSubnode(self.segmentedControlNode) } var insets = layout.insets(options: [.input]) @@ -118,10 +119,11 @@ final class HashtagSearchControllerNode: ASDisplayNode { let panelY: CGFloat = insets.top - UIScreenPixel - 4.0 transition.updateFrame(node: self.toolbarBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: panelY), size: CGSize(width: layout.size.width, height: toolbarHeight))) - transition.updateFrame(node: self.toolbarSeparatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: panelY + toolbarHeight), size: CGSize(width: layout.size.width, height: UIScreenPixel))) + self.toolbarBackgroundNode.update(size: self.toolbarBackgroundNode.bounds.size, transition: transition) + transition.updateFrame(node: self.toolbarSeparatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: panelY), size: CGSize(width: layout.size.width, height: UIScreenPixel))) let controlSize = self.segmentedControlNode.updateLayout(.stretchToFill(width: layout.size.width - 14.0 * 2.0), transition: transition) - transition.updateFrame(node: self.segmentedControlNode, frame: CGRect(origin: CGPoint(x: floor((layout.size.width - controlSize.width) / 2.0), y: panelY + floor((toolbarHeight - controlSize.height) / 2.0)), size: controlSize)) + transition.updateFrame(node: self.segmentedControlNode, frame: CGRect(origin: CGPoint(x: floor((layout.size.width - controlSize.width) / 2.0), y: panelY + 2.0 + floor((toolbarHeight - controlSize.height) / 2.0)), size: controlSize)) if let chatController = self.chatController { insets.top += toolbarHeight - 4.0 @@ -152,5 +154,11 @@ final class HashtagSearchControllerNode: ASDisplayNode { self.dequeueTransition() } } + + if self.chatController != nil { + return toolbarHeight + } else { + return 0.0 + } } } diff --git a/submodules/ImageCompression/Sources/ImageCompression.swift b/submodules/ImageCompression/Sources/ImageCompression.swift index 0d123acb2f..8988c56557 100644 --- a/submodules/ImageCompression/Sources/ImageCompression.swift +++ b/submodules/ImageCompression/Sources/ImageCompression.swift @@ -58,6 +58,31 @@ public func compressImage(_ image: UIImage, quality: Float) -> Data? { return data as Data } -public func compressImageMiniThumbnail(_ image: UIImage) -> Data? { - return compressMiniThumbnail(image) +public enum MiniThumbnailType { + case image + case avatar +} + +public func compressImageMiniThumbnail(_ image: UIImage, type: MiniThumbnailType = .image) -> Data? { + switch type { + case .image: + return compressMiniThumbnail(image, CGSize(width: 40.0, height: 40.0)) + case .avatar: + var size: CGFloat = 8.0 + var data = compressMiniThumbnail(image, CGSize(width: size, height: size)) + while true { + size += 1.0 + if let candidateData = compressMiniThumbnail(image, CGSize(width: size, height: size)) { + if candidateData.count >= 32 { + break + } else { + data = candidateData + } + } else { + break + } + } + + return data + } } diff --git a/submodules/ImageTransparency/Sources/ImageTransparency.swift b/submodules/ImageTransparency/Sources/ImageTransparency.swift index 3c0e71187e..3283567641 100644 --- a/submodules/ImageTransparency/Sources/ImageTransparency.swift +++ b/submodules/ImageTransparency/Sources/ImageTransparency.swift @@ -92,14 +92,19 @@ public func imageRequiresInversion(_ cgImage: CGImage) -> Bool { } if hasAlpha { + let probingContext = DrawingContext(size: CGSize(width: cgImage.width, height: cgImage.height)) + probingContext.withContext { c in + c.draw(cgImage, in: CGRect(origin: CGPoint(), size: probingContext.size)) + } + var matching: Int = 0 var total: Int = 0 - for y in 0 ..< Int(context.size.height) { - for x in 0 ..< Int(context.size.width) { + for y in 0 ..< Int(probingContext.size.height) { + for x in 0 ..< Int(probingContext.size.width) { var saturation: CGFloat = 0.0 var brightness: CGFloat = 0.0 var alpha: CGFloat = 0.0 - if context.colorAt(CGPoint(x: x, y: y)).getHue(nil, saturation: &saturation, brightness: &brightness, alpha: &alpha) { + if probingContext.colorAt(CGPoint(x: x, y: y)).getHue(nil, saturation: &saturation, brightness: &brightness, alpha: &alpha) { if alpha < 1.0 { hasAlpha = true } diff --git a/submodules/ImportStickerPackUI/BUILD b/submodules/ImportStickerPackUI/BUILD new file mode 100644 index 0000000000..dda51cbc8b --- /dev/null +++ b/submodules/ImportStickerPackUI/BUILD @@ -0,0 +1,38 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ImportStickerPackUI", + module_name = "ImportStickerPackUI", + srcs = glob([ + "Sources/**/*.swift", + ]), + deps = [ + "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", + "//submodules/AsyncDisplayKit:AsyncDisplayKit", + "//submodules/Display:Display", + "//submodules/Postbox:Postbox", + "//submodules/TelegramCore:TelegramCore", + "//submodules/SyncCore:SyncCore", + "//submodules/TelegramPresentationData:TelegramPresentationData", + "//submodules/AccountContext:AccountContext", + "//submodules/TelegramUIPreferences:TelegramUIPreferences", + "//submodules/ShareController:ShareController", + "//submodules/ItemListUI:ItemListUI", + "//submodules/StickerResources:StickerResources", + "//submodules/AlertUI:AlertUI", + "//submodules/PresentationDataUtils:PresentationDataUtils", + "//submodules/TextFormat:TextFormat", + "//submodules/MergeLists:MergeLists", + "//submodules/ActivityIndicator:ActivityIndicator", + "//submodules/AnimatedStickerNode:AnimatedStickerNode", + "//submodules/TelegramAnimatedStickerNode:TelegramAnimatedStickerNode", + "//submodules/ShimmerEffect:ShimmerEffect", + "//submodules/UndoUI:UndoUI", + "//submodules/ContextUI:ContextUI", + "//submodules/RadialStatusNode:RadialStatusNode", + "//submodules/StickerPackPreviewUI:StickerPackPreviewUI", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/ImportStickerPackUI/Sources/ImportStickerPack.swift b/submodules/ImportStickerPackUI/Sources/ImportStickerPack.swift new file mode 100644 index 0000000000..dadfc898be --- /dev/null +++ b/submodules/ImportStickerPackUI/Sources/ImportStickerPack.swift @@ -0,0 +1,102 @@ +import Foundation +import UIKit +import Postbox + +enum StickerVerificationStatus { + case loading + case verified + case declined +} + +public class ImportStickerPack { + public class Sticker: Equatable { + public enum Content { + case image(Data) + case animation(Data) + } + + public static func == (lhs: ImportStickerPack.Sticker, rhs: ImportStickerPack.Sticker) -> Bool { + return lhs.uuid == rhs.uuid + } + + let content: Content + let emojis: [String] + let uuid: UUID + var resource: MediaResource? + + init(content: Content, emojis: [String], uuid: UUID = UUID()) { + self.content = content + self.emojis = emojis + self.uuid = uuid + } + + var data: Data { + switch self.content { + case let .image(data): + return data + case let .animation(data): + return data + } + } + + var isAnimated: Bool { + if case .animation = self.content { + return true + } else { + return false + } + } + } + + public let software: String + public let isAnimated: Bool + public let thumbnail: Sticker? + public let stickers: [Sticker] + + public init?(data: Data) { + guard let json = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] else { + return nil + } + self.software = json["software"] as? String ?? "" + let isAnimated = json["isAnimated"] as? Bool ?? false + self.isAnimated = isAnimated + + func parseSticker(_ sticker: [String: Any]) -> Sticker? { + if let dataString = sticker["data"] as? String, let mimeType = sticker["mimeType"] as? String, let data = Data(base64Encoded: dataString) { + var content: Sticker.Content? + switch mimeType.lowercased() { + case "image/png": + if !isAnimated { + content = .image(data) + } + case "application/x-tgsticker": + if isAnimated { + content = .animation(data) + } + default: + break + } + if let content = content { + return Sticker(content: content, emojis: sticker["emojis"] as? [String] ?? []) + } + } + return nil + } + + if let thumbnail = json["thumbnail"] as? [String: Any], let parsedSticker = parseSticker(thumbnail) { + self.thumbnail = parsedSticker + } else { + self.thumbnail = nil + } + + var stickers: [Sticker] = [] + if let stickersArray = json["stickers"] as? [[String: Any]] { + for sticker in stickersArray { + if let parsedSticker = parseSticker(sticker) { + stickers.append(parsedSticker) + } + } + } + self.stickers = stickers + } +} diff --git a/submodules/ImportStickerPackUI/Sources/ImportStickerPackController.swift b/submodules/ImportStickerPackUI/Sources/ImportStickerPackController.swift new file mode 100644 index 0000000000..b49309f8bd --- /dev/null +++ b/submodules/ImportStickerPackUI/Sources/ImportStickerPackController.swift @@ -0,0 +1,169 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import Postbox +import TelegramCore +import SyncCore +import SwiftSignalKit +import TelegramUIPreferences +import AccountContext +import ShareController +import StickerResources +import AlertUI +import PresentationDataUtils +import UndoUI + +public final class ImportStickerPackController: ViewController, StandalonePresentableController { + private var controllerNode: ImportStickerPackControllerNode { + return self.displayNode as! ImportStickerPackControllerNode + } + + private var animatedIn = false + private var isDismissed = false + + public var dismissed: (() -> Void)? + + private let context: AccountContext + private weak var parentNavigationController: NavigationController? + + private let stickerPack: ImportStickerPack + private var presentationDataDisposable: Disposable? + private var verificationDisposable: Disposable? + + public init(context: AccountContext, stickerPack: ImportStickerPack, parentNavigationController: NavigationController?) { + self.context = context + self.parentNavigationController = parentNavigationController + + self.stickerPack = stickerPack + + super.init(navigationBarPresentationData: nil) + + self.blocksBackgroundWhenInOverlay = true + self.acceptsFocusWhenInOverlay = true + self.statusBar.statusBarStyle = .Ignore + + self.presentationDataDisposable = (context.sharedContext.presentationData + |> deliverOnMainQueue).start(next: { [weak self] presentationData in + if let strongSelf = self, strongSelf.isNodeLoaded { + strongSelf.controllerNode.updatePresentationData(presentationData) + } + }) + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.presentationDataDisposable?.dispose() + self.verificationDisposable?.dispose() + } + + override public func loadDisplayNode() { + self.displayNode = ImportStickerPackControllerNode(context: self.context) + self.controllerNode.dismiss = { [weak self] in + self?.dismissed?() + self?.presentingViewController?.dismiss(animated: false, completion: nil) + } + self.controllerNode.cancel = { [weak self] in + self?.dismiss() + } + self.controllerNode.present = { [weak self] controller, arguments in + self?.present(controller, in: .window(.root), with: arguments) + } + self.controllerNode.presentInGlobalOverlay = { [weak self] controller, arguments in + self?.presentInGlobalOverlay(controller, with: arguments) + } + self.controllerNode.navigationController = self.parentNavigationController + + Queue.mainQueue().after(0.1) { + self.controllerNode.updateStickerPack(self.stickerPack, verifiedStickers: Set(), declinedStickers: Set(), uploadedStickerResources: [:]) + + if self.stickerPack.isAnimated { + let _ = (self.context.account.postbox.loadedPeerWithId(self.context.account.peerId) + |> deliverOnMainQueue).start(next: { [weak self] peer in + guard let strongSelf = self else { + return + } + + var signals: [Signal<(UUID, StickerVerificationStatus, MediaResource?), NoError>] = [] + for sticker in strongSelf.stickerPack.stickers { + if let resource = strongSelf.controllerNode.stickerResources[sticker.uuid] { + signals.append(strongSelf.context.engine.stickers.uploadSticker(peer: peer, resource: resource, alt: sticker.emojis.first ?? "", dimensions: PixelDimensions(width: 512, height: 512), isAnimated: true) + |> map { result -> (UUID, StickerVerificationStatus, MediaResource?) in + switch result { + case .progress: + return (sticker.uuid, .loading, nil) + case let .complete(resource, mimeType): + if mimeType == "application/x-tgsticker" { + return (sticker.uuid, .verified, resource) + } else { + return (sticker.uuid, .declined, nil) + } + } + } + |> `catch` { _ -> Signal<(UUID, StickerVerificationStatus, MediaResource?), NoError> in + return .single((sticker.uuid, .declined, nil)) + }) + } + } + strongSelf.verificationDisposable = (combineLatest(signals) + |> deliverOnMainQueue).start(next: { [weak self] results in + guard let strongSelf = self else { + return + } + var verifiedStickers = Set() + var declinedStickers = Set() + var uploadedStickerResources: [UUID: MediaResource] = [:] + for (uuid, result, resource) in results { + switch result { + case .verified: + if let resource = resource { + verifiedStickers.insert(uuid) + uploadedStickerResources[uuid] = resource + } else { + declinedStickers.insert(uuid) + } + case .declined: + declinedStickers.insert(uuid) + case .loading: + break + } + } + strongSelf.controllerNode.updateStickerPack(strongSelf.stickerPack, verifiedStickers: verifiedStickers, declinedStickers: declinedStickers, uploadedStickerResources: uploadedStickerResources) + }) + }) + } + } + + self.ready.set(self.controllerNode.ready.get()) + } + + override public func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + if !self.animatedIn { + self.animatedIn = true + self.controllerNode.animateIn() + } + } + + override public func dismiss(completion: (() -> Void)? = nil) { + if !self.isDismissed { + self.isDismissed = true + } else { + return + } + self.acceptsFocusWhenInOverlay = false + self.requestUpdateParameters() + self.controllerNode.animateOut(completion: completion) + } + + override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + super.containerLayoutUpdated(layout, transition: transition) + + self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition) + } +} + diff --git a/submodules/ImportStickerPackUI/Sources/ImportStickerPackControllerNode.swift b/submodules/ImportStickerPackUI/Sources/ImportStickerPackControllerNode.swift new file mode 100644 index 0000000000..c2e27298a8 --- /dev/null +++ b/submodules/ImportStickerPackUI/Sources/ImportStickerPackControllerNode.swift @@ -0,0 +1,863 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import SwiftSignalKit +import Postbox +import TelegramCore +import SyncCore +import TelegramPresentationData +import TelegramUIPreferences +import MergeLists +import ActivityIndicator +import TextFormat +import AccountContext +import ContextUI +import RadialStatusNode +import UndoUI +import StickerPackPreviewUI + +private struct StickerPackPreviewGridEntry: Comparable, Equatable, Identifiable { + let index: Int + let stickerItem: ImportStickerPack.Sticker + let isVerified: Bool + + var stableId: Int { + return self.index + } + + static func <(lhs: StickerPackPreviewGridEntry, rhs: StickerPackPreviewGridEntry) -> Bool { + return lhs.index < rhs.index + } + + func item(account: Account, interaction: StickerPackPreviewInteraction, theme: PresentationTheme) -> StickerPackPreviewGridItem { + return StickerPackPreviewGridItem(account: account, stickerItem: self.stickerItem, interaction: interaction, theme: theme, isVerified: self.isVerified) + } + + +} + +private struct StickerPackPreviewGridTransaction { + let deletions: [Int] + let insertions: [GridNodeInsertItem] + let updates: [GridNodeUpdateItem] + + init(previousList: [StickerPackPreviewGridEntry], list: [StickerPackPreviewGridEntry], account: Account, interaction: StickerPackPreviewInteraction, theme: PresentationTheme) { + 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, theme: theme), previousIndex: $0.2) } + self.updates = updateIndices.map { GridNodeUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, interaction: interaction, theme: theme)) } + } +} + +final class ImportStickerPackControllerNode: ViewControllerTracingNode, UIScrollViewDelegate { + private let context: AccountContext + private var presentationData: PresentationData + private var stickerPack: ImportStickerPack? + var stickerResources: [UUID: MediaResource] = [:] + private var uploadedStickerResources: [UUID: MediaResource] = [:] + private var stickerPackReady = true + + private var containerLayout: (ContainerViewLayout, CGFloat)? + + private let dimNode: ASDisplayNode + + private let wrappingScrollNode: ASScrollNode + private let cancelButtonNode: ASButtonNode + + private let contentContainerNode: ASDisplayNode + private let contentBackgroundNode: ASImageNode + private let contentGridNode: GridNode + private let createActionButtonNode: ASButtonNode + private let createActionSeparatorNode: ASDisplayNode + private let addToExistingActionButtonNode: ASButtonNode + private let addToExistingActionSeparatorNode: ASDisplayNode + private let contentTitleNode: ImmediateTextNode + private let contentSeparatorNode: ASDisplayNode + + private let radialStatus: RadialStatusNode + private let radialCheck: RadialStatusNode + private let radialStatusBackground: ASImageNode + private let radialStatusText: ImmediateTextNode + private let progressText: ImmediateTextNode + private let infoText: ImmediateTextNode + + private var interaction: StickerPackPreviewInteraction! + + weak var navigationController: NavigationController? + + var present: ((ViewController, Any?) -> Void)? + var presentInGlobalOverlay: ((ViewController, Any?) -> Void)? + var dismiss: (() -> Void)? + var cancel: (() -> Void)? + + let ready = Promise() + private var didSetReady = false + + private var pendingItems: [StickerPackPreviewGridEntry] = [] + private var currentItems: [StickerPackPreviewGridEntry] = [] + + private var hapticFeedback: HapticFeedback? + + private let disposable = MetaDisposable() + private let shortNameSuggestionDisposable = MetaDisposable() + + private var progress: (CGFloat, Int32, Int32)? + + init(context: AccountContext) { + self.context = context + self.presentationData = context.sharedContext.currentPresentationData.with { $0 } + + self.wrappingScrollNode = ASScrollNode() + self.wrappingScrollNode.view.alwaysBounceVertical = true + self.wrappingScrollNode.view.delaysContentTouches = false + self.wrappingScrollNode.view.canCancelContentTouches = true + + self.dimNode = ASDisplayNode() + self.dimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.5) + + self.cancelButtonNode = ASButtonNode() + self.cancelButtonNode.displaysAsynchronously = false + + self.contentContainerNode = ASDisplayNode() + self.contentContainerNode.isOpaque = false + self.contentContainerNode.clipsToBounds = true + + self.contentBackgroundNode = ASImageNode() + self.contentBackgroundNode.displaysAsynchronously = false + self.contentBackgroundNode.displayWithoutProcessing = true + + self.contentGridNode = GridNode() + + self.createActionButtonNode = HighlightTrackingButtonNode() + self.createActionButtonNode.displaysAsynchronously = false + self.createActionButtonNode.titleNode.displaysAsynchronously = false + + self.addToExistingActionButtonNode = HighlightTrackingButtonNode() + self.addToExistingActionButtonNode.displaysAsynchronously = false + self.addToExistingActionButtonNode.titleNode.displaysAsynchronously = false + + self.contentTitleNode = ImmediateTextNode() + self.contentTitleNode.displaysAsynchronously = false + self.contentTitleNode.maximumNumberOfLines = 1 + self.contentTitleNode.alpha = 0.0 + + self.contentSeparatorNode = ASDisplayNode() + self.contentSeparatorNode.isLayerBacked = true + + self.createActionSeparatorNode = ASDisplayNode() + self.createActionSeparatorNode.isLayerBacked = true + self.createActionSeparatorNode.displaysAsynchronously = false + + self.addToExistingActionSeparatorNode = ASDisplayNode() + self.addToExistingActionSeparatorNode.isLayerBacked = true + self.addToExistingActionSeparatorNode.displaysAsynchronously = false + + self.radialStatus = RadialStatusNode(backgroundNodeColor: .clear) + self.radialStatus.alpha = 0.0 + self.radialCheck = RadialStatusNode(backgroundNodeColor: .clear) + self.radialCheck.alpha = 0.0 + + self.radialStatusBackground = ASImageNode() + self.radialStatusBackground.isUserInteractionEnabled = false + self.radialStatusBackground.displaysAsynchronously = false + self.radialStatusBackground.image = generateCircleImage(diameter: 180.0, lineWidth: 6.0, color: self.presentationData.theme.list.itemAccentColor.withMultipliedAlpha(0.2)) + self.radialStatusBackground.alpha = 0.0 + + self.radialStatusText = ImmediateTextNode() + self.radialStatusText.isUserInteractionEnabled = false + self.radialStatusText.displaysAsynchronously = false + self.radialStatusText.maximumNumberOfLines = 1 + self.radialStatusText.isAccessibilityElement = false + self.radialStatusText.alpha = 0.0 + + self.progressText = ImmediateTextNode() + self.progressText.isUserInteractionEnabled = false + self.progressText.displaysAsynchronously = false + self.progressText.maximumNumberOfLines = 1 + self.progressText.isAccessibilityElement = false + self.progressText.alpha = 0.0 + + self.infoText = ImmediateTextNode() + self.infoText.isUserInteractionEnabled = false + self.infoText.displaysAsynchronously = false + self.infoText.maximumNumberOfLines = 4 + self.infoText.isAccessibilityElement = false + self.infoText.textAlignment = .center + self.infoText.alpha = 0.0 + + super.init() + + self.interaction = StickerPackPreviewInteraction(playAnimatedStickers: false) + + self.backgroundColor = nil + self.isOpaque = false + + self.dimNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:)))) + self.addSubnode(self.dimNode) + + self.wrappingScrollNode.view.delegate = self + self.addSubnode(self.wrappingScrollNode) + + self.wrappingScrollNode.addSubnode(self.cancelButtonNode) + self.cancelButtonNode.addTarget(self, action: #selector(self.cancelButtonPressed), forControlEvents: .touchUpInside) + + self.createActionButtonNode.addTarget(self, action: #selector(self.createActionButtonPressed), forControlEvents: .touchUpInside) + self.addToExistingActionButtonNode.addTarget(self, action: #selector(self.addToExistingActionButtonPressed), forControlEvents: .touchUpInside) + + self.wrappingScrollNode.addSubnode(self.contentBackgroundNode) + + self.wrappingScrollNode.addSubnode(self.contentContainerNode) + self.contentContainerNode.addSubnode(self.contentGridNode) + self.contentContainerNode.addSubnode(self.createActionSeparatorNode) + self.contentContainerNode.addSubnode(self.createActionButtonNode) +// self.contentContainerNode.addSubnode(self.addToExistingActionSeparatorNode) +// self.contentContainerNode.addSubnode(self.addToExistingActionButtonNode) + self.wrappingScrollNode.addSubnode(self.contentTitleNode) + self.wrappingScrollNode.addSubnode(self.contentSeparatorNode) + + self.wrappingScrollNode.addSubnode(self.radialStatusBackground) + self.wrappingScrollNode.addSubnode(self.radialStatus) + self.wrappingScrollNode.addSubnode(self.radialCheck) + self.wrappingScrollNode.addSubnode(self.radialStatusText) + self.wrappingScrollNode.addSubnode(self.progressText) + self.wrappingScrollNode.addSubnode(self.infoText) + + self.createActionButtonNode.setTitle(self.presentationData.strings.ImportStickerPack_CreateNewStickerSet, with: Font.regular(20.0), with: self.presentationData.theme.actionSheet.controlAccentColor, for: .normal) + self.addToExistingActionButtonNode.setTitle(self.presentationData.strings.ImportStickerPack_AddToExistingStickerSet, with: Font.regular(20.0), with: self.presentationData.theme.actionSheet.controlAccentColor, for: .normal) + + self.contentGridNode.presentationLayoutUpdated = { [weak self] presentationLayout, transition in + self?.gridPresentationLayoutUpdated(presentationLayout, transition: transition) + } + } + + deinit { + self.disposable.dispose() + self.shortNameSuggestionDisposable.dispose() + } + + override func didLoad() { + super.didLoad() + + if #available(iOSApplicationExtension 11.0, iOS 11.0, *) { + self.wrappingScrollNode.view.contentInsetAdjustmentBehavior = .never + } + self.contentGridNode.view.addGestureRecognizer(PeekControllerGestureRecognizer(contentAtPoint: { [weak self] point -> Signal<(ASDisplayNode, PeekControllerContent)?, NoError>? in + if let strongSelf = self { + if let itemNode = strongSelf.contentGridNode.itemNodeAtPoint(point) as? StickerPackPreviewGridItemNode, let item = itemNode.stickerPackItem { + var menuItems: [ContextMenuItem] = [] + if strongSelf.currentItems.count > 1 { + menuItems.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.ImportStickerPack_RemoveFromImport, textColor: .destructive, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor) }, action: { [weak self] c, f in + f(.dismissWithoutContent) + + if let strongSelf = self { + var updatedItems = strongSelf.currentItems + updatedItems.removeAll(where: { $0.stickerItem.uuid == item.uuid }) + strongSelf.pendingItems = updatedItems + + if let (layout, navigationHeight) = strongSelf.containerLayout { + strongSelf.containerLayoutUpdated(layout, navigationBarHeight: navigationHeight, transition: .animated(duration: 0.3, curve: .easeInOut)) + } + } + }))) + } + return .single((itemNode, StickerPreviewPeekContent(account: strongSelf.context.account, item: item, menu: menuItems))) + } + } + return nil + }, present: { [weak self] content, sourceNode in + if let strongSelf = self { + let controller = PeekController(presentationData: strongSelf.presentationData, content: content, sourceNode: { + return sourceNode + }) + controller.visibilityUpdated = { [weak self] visible in + if let strongSelf = self { + strongSelf.contentGridNode.forceHidden = visible + } + } + strongSelf.presentInGlobalOverlay?(controller, nil) + return controller + } + return nil + }, updateContent: { [weak self] content in + if let strongSelf = self { + var item: ImportStickerPack.Sticker? + if let content = content as? StickerPreviewPeekContent { + item = content.item + } + strongSelf.updatePreviewingItem(item: item, animated: true) + } + }, activateBySingleTap: true)) + + self.updatePresentationData(self.presentationData) + } + + func updatePresentationData(_ presentationData: PresentationData) { + self.presentationData = presentationData + + let theme = presentationData.theme + let solidBackground = generateImage(CGSize(width: 1.0, height: 1.0), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(theme.actionSheet.opaqueItemBackgroundColor.cgColor) + context.fill(CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.height))) + })?.stretchableImage(withLeftCapWidth: 16, topCapHeight: 1) + + let highlightedSolidBackground = generateImage(CGSize(width: 1.0, height: 1.0), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(theme.actionSheet.opaqueItemHighlightedBackgroundColor.cgColor) + context.fill(CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.height))) + })?.stretchableImage(withLeftCapWidth: 16, topCapHeight: 1) + + let halfRoundedBackground = generateImage(CGSize(width: 32.0, height: 32.0), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(theme.actionSheet.opaqueItemBackgroundColor.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.height))) + context.fill(CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.height / 2.0))) + })?.stretchableImage(withLeftCapWidth: 16, topCapHeight: 1) + + let highlightedHalfRoundedBackground = generateImage(CGSize(width: 32.0, height: 32.0), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(theme.actionSheet.opaqueItemHighlightedBackgroundColor.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.height))) + context.fill(CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.height / 2.0))) + })?.stretchableImage(withLeftCapWidth: 16, topCapHeight: 1) + + let roundedBackground = generateStretchableFilledCircleImage(radius: 16.0, color: presentationData.theme.actionSheet.opaqueItemBackgroundColor) + let highlightedRoundedBackground = generateStretchableFilledCircleImage(radius: 16.0, color: presentationData.theme.actionSheet.opaqueItemHighlightedBackgroundColor) + + self.contentBackgroundNode.image = roundedBackground + + self.cancelButtonNode.setBackgroundImage(roundedBackground, for: .normal) + self.cancelButtonNode.setBackgroundImage(highlightedRoundedBackground, for: .highlighted) + + if self.addToExistingActionButtonNode.supernode != nil { + self.createActionButtonNode.setBackgroundImage(solidBackground, for: .normal) + self.createActionButtonNode.setBackgroundImage(highlightedSolidBackground, for: .highlighted) + } else { + self.createActionButtonNode.setBackgroundImage(halfRoundedBackground, for: .normal) + self.createActionButtonNode.setBackgroundImage(highlightedHalfRoundedBackground, for: .highlighted) + } + + self.addToExistingActionButtonNode.setBackgroundImage(halfRoundedBackground, for: .normal) + self.addToExistingActionButtonNode.setBackgroundImage(highlightedHalfRoundedBackground, for: .highlighted) + + self.contentSeparatorNode.backgroundColor = presentationData.theme.actionSheet.opaqueItemSeparatorColor + self.createActionSeparatorNode.backgroundColor = presentationData.theme.actionSheet.opaqueItemSeparatorColor + self.addToExistingActionSeparatorNode.backgroundColor = presentationData.theme.actionSheet.opaqueItemSeparatorColor + + self.cancelButtonNode.setTitle(presentationData.strings.Common_Cancel, with: Font.medium(20.0), with: presentationData.theme.actionSheet.standardActionTextColor, for: .normal) + + self.contentTitleNode.linkHighlightColor = presentationData.theme.actionSheet.controlAccentColor.withAlphaComponent(0.5) + + if let (layout, navigationBarHeight) = self.containerLayout { + self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate) + } + } + + private var hadProgress = false + func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { + self.containerLayout = (layout, navigationBarHeight) + + transition.updateFrame(node: self.wrappingScrollNode, frame: CGRect(origin: CGPoint(), size: layout.size)) + + var insets = layout.insets(options: [.statusBar]) + insets.top = max(10.0, insets.top) + let cleanInsets = layout.insets(options: [.statusBar]) + + let hasAddToExistingButton = self.addToExistingActionButtonNode.supernode != nil + + transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(), size: layout.size)) + + var bottomInset: CGFloat = 10.0 + cleanInsets.bottom + if insets.bottom > 0 { + bottomInset -= 12.0 + } + + let buttonHeight: CGFloat = 57.0 + let sectionSpacing: CGFloat = 8.0 + let titleAreaHeight: CGFloat = 51.0 + + let width = horizontalContainerFillingSizeForLayout(layout: layout, sideInset: 10.0 + layout.safeInsets.left) + + let sideInset = floor((layout.size.width - width) / 2.0) + + transition.updateFrame(node: self.cancelButtonNode, frame: CGRect(origin: CGPoint(x: sideInset, y: layout.size.height - bottomInset - buttonHeight), size: CGSize(width: width, height: buttonHeight))) + + let maximumContentHeight = layout.size.height - insets.top - bottomInset - buttonHeight - sectionSpacing + + let contentContainerFrame = CGRect(origin: CGPoint(x: sideInset, y: insets.top), size: CGSize(width: width, height: maximumContentHeight)) + let contentFrame = contentContainerFrame.insetBy(dx: 12.0, dy: 0.0) + + var transaction: StickerPackPreviewGridTransaction? + + var forceTitleUpdate = false + if self.progress != nil && !self.hadProgress { + self.hadProgress = true + forceTitleUpdate = true + } + + if let _ = self.stickerPack, self.currentItems.isEmpty || self.currentItems.count != self.pendingItems.count || self.pendingItems != self.currentItems || forceTitleUpdate { + let previousItems = self.currentItems + self.currentItems = self.pendingItems + + let titleFont = Font.medium(20.0) + let title: String + if let _ = self.progress { + title = self.presentationData.strings.ImportStickerPack_ImportingStickers + } else { + title = self.presentationData.strings.ImportStickerPack_StickerCount(Int32(self.currentItems.count)) + } + self.contentTitleNode.attributedText = stringWithAppliedEntities(title, entities: [], baseColor: self.presentationData.theme.actionSheet.primaryTextColor, linkColor: self.presentationData.theme.actionSheet.controlAccentColor, baseFont: titleFont, linkFont: titleFont, boldFont: titleFont, italicFont: titleFont, boldItalicFont: titleFont, fixedFont: titleFont, blockQuoteFont: titleFont) + + if !forceTitleUpdate { + transaction = StickerPackPreviewGridTransaction(previousList: previousItems, list: self.currentItems, account: self.context.account, interaction: self.interaction, theme: self.presentationData.theme) + } + } + let itemCount = self.currentItems.count + + let titleSize = self.contentTitleNode.updateLayout(CGSize(width: contentContainerFrame.size.width - 24.0, height: CGFloat.greatestFiniteMagnitude)) + let titleFrame = CGRect(origin: CGPoint(x: contentContainerFrame.minX + floor((contentContainerFrame.size.width - titleSize.width) / 2.0), y: self.contentBackgroundNode.frame.minY + 15.0), size: titleSize) + let deltaTitlePosition = CGPoint(x: titleFrame.midX - self.contentTitleNode.frame.midX, y: titleFrame.midY - self.contentTitleNode.frame.midY) + self.contentTitleNode.frame = titleFrame + transition.animatePosition(node: self.contentTitleNode, from: CGPoint(x: titleFrame.midX + deltaTitlePosition.x, y: titleFrame.midY + deltaTitlePosition.y)) + + transition.updateFrame(node: self.contentTitleNode, frame: titleFrame) + transition.updateFrame(node: self.contentSeparatorNode, frame: CGRect(origin: CGPoint(x: contentContainerFrame.minX, y: self.contentBackgroundNode.frame.minY + titleAreaHeight), size: CGSize(width: contentContainerFrame.size.width, height: UIScreenPixel))) + + let itemsPerRow = 4 + let itemWidth = floor(contentFrame.size.width / CGFloat(itemsPerRow)) + let rowCount = itemCount / itemsPerRow + (itemCount % itemsPerRow != 0 ? 1 : 0) + + let minimallyRevealedRowCount: CGFloat = 3.5 + let initiallyRevealedRowCount = min(minimallyRevealedRowCount, CGFloat(rowCount)) + + var bottomGridInset = hasAddToExistingButton ? 2.0 * buttonHeight : buttonHeight + if let _ = self.progress { + bottomGridInset += 210.0 + } + let topInset = max(0.0, contentFrame.size.height - initiallyRevealedRowCount * itemWidth - titleAreaHeight - bottomGridInset) + transition.updateFrame(node: self.contentContainerNode, frame: contentContainerFrame) + + let createButtonOffset = hasAddToExistingButton ? 2.0 * buttonHeight : buttonHeight + transition.updateFrame(node: self.createActionButtonNode, frame: CGRect(origin: CGPoint(x: 0.0, y: contentContainerFrame.size.height - createButtonOffset), size: CGSize(width: contentContainerFrame.size.width, height: buttonHeight))) + transition.updateFrame(node: self.createActionSeparatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: contentContainerFrame.size.height - createButtonOffset - UIScreenPixel), size: CGSize(width: contentContainerFrame.size.width, height: UIScreenPixel))) + + transition.updateFrame(node: self.addToExistingActionButtonNode, frame: CGRect(origin: CGPoint(x: 0.0, y: contentContainerFrame.size.height - buttonHeight), size: CGSize(width: contentContainerFrame.size.width, height: buttonHeight))) + transition.updateFrame(node: self.addToExistingActionSeparatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: contentContainerFrame.size.height - buttonHeight - UIScreenPixel), size: CGSize(width: contentContainerFrame.size.width, height: UIScreenPixel))) + + let gridSize = CGSize(width: contentFrame.size.width, height: max(32.0, contentFrame.size.height - titleAreaHeight)) + + self.contentGridNode.transaction(GridNodeTransaction(deleteItems: transaction?.deletions ?? [], insertItems: transaction?.insertions ?? [], updateItems: transaction?.updates ?? [], scrollToItem: nil, updateLayout: GridNodeUpdateLayout(layout: GridNodeLayout(size: gridSize, insets: UIEdgeInsets(top: topInset, left: 0.0, bottom: bottomGridInset, right: 0.0), preloadSize: 80.0, type: .fixed(itemSize: CGSize(width: itemWidth, height: itemWidth), fillWidth: nil, lineSpacing: 0.0, itemSpacing: nil)), transition: transition), itemTransition: .immediate, stationaryItems: .none, updateFirstIndexInSectionOffset: nil), completion: { _ in }) + transition.updateFrame(node: self.contentGridNode, frame: CGRect(origin: CGPoint(x: floor((contentContainerFrame.size.width - contentFrame.size.width) / 2.0), y: titleAreaHeight), size: gridSize)) + + transition.updateAlpha(node: self.contentGridNode, alpha: self.progress == nil ? 1.0 : 0.0) + + var effectiveProgress: CGFloat = 0.0 + var count: Int32 = 0 + var total: Int32 = 0 + + var hasProgress = false + if let (progress, progressCount, progressTotal) = self.progress { + effectiveProgress = progress + count = progressCount + total = progressTotal + hasProgress = true + } + + let availableHeight: CGFloat = 330.0 + var radialStatusSize = CGSize(width: 186.0, height: 186.0) + var maxIconStatusSpacing: CGFloat = 46.0 + var maxProgressTextSpacing: CGFloat = 33.0 + var progressStatusSpacing: CGFloat = 14.0 + var statusButtonSpacing: CGFloat = 19.0 + + var maxK: CGFloat = availableHeight / (30.0 + maxProgressTextSpacing + 320.0) + maxK = max(0.5, min(1.0, maxK)) + + radialStatusSize.width = floor(radialStatusSize.width * maxK) + radialStatusSize.height = floor(radialStatusSize.height * maxK) + maxIconStatusSpacing = floor(maxIconStatusSpacing * maxK) + maxProgressTextSpacing = floor(maxProgressTextSpacing * maxK) + progressStatusSpacing = floor(progressStatusSpacing * maxK) + statusButtonSpacing = floor(statusButtonSpacing * maxK) + + var updateRadialBackround = false + if let width = self.radialStatusBackground.image?.size.width { + if abs(width - radialStatusSize.width) > 0.01 { + updateRadialBackround = true + } + } else { + updateRadialBackround = true + } + + if updateRadialBackround { + self.radialStatusBackground.image = generateCircleImage(diameter: radialStatusSize.width, lineWidth: 6.0, color: self.presentationData.theme.list.itemAccentColor.withMultipliedAlpha(0.2)) + } + + let contentOrigin = self.contentBackgroundNode.frame.minY + 72.0 + if hasProgress { + transition.updateAlpha(node: self.radialStatusText, alpha: 1.0) + transition.updateAlpha(node: self.progressText, alpha: 1.0) + transition.updateAlpha(node: self.radialStatus, alpha: 1.0) + transition.updateAlpha(node: self.infoText, alpha: 1.0) + transition.updateAlpha(node: self.radialCheck, alpha: 1.0) + transition.updateAlpha(node: self.radialStatusBackground, alpha: 1.0) + transition.updateAlpha(node: self.createActionButtonNode, alpha: 0.0) + transition.updateAlpha(node: self.contentSeparatorNode, alpha: 0.0) + transition.updateAlpha(node: self.createActionSeparatorNode, alpha: 0.0) + transition.updateAlpha(node: self.addToExistingActionButtonNode, alpha: 0.0) + transition.updateAlpha(node: self.addToExistingActionSeparatorNode, alpha: 0.0) + } + + self.radialStatusText.attributedText = NSAttributedString(string: "\(Int(effectiveProgress * 100.0))%", font: Font.with(size: floor(36.0 * maxK), design: .round, weight: .semibold), textColor: self.presentationData.theme.list.itemPrimaryTextColor) + let radialStatusTextSize = self.radialStatusText.updateLayout(CGSize(width: 200.0, height: .greatestFiniteMagnitude)) + + self.progressText.attributedText = NSAttributedString(string: self.presentationData.strings.ImportStickerPack_Of(String(count), String(total)).0, font: Font.semibold(17.0), textColor: self.presentationData.theme.list.itemPrimaryTextColor) + let progressTextSize = self.progressText.updateLayout(CGSize(width: layout.size.width - 16.0 * 2.0, height: .greatestFiniteMagnitude)) + + self.infoText.attributedText = NSAttributedString(string: self.presentationData.strings.ImportStickerPack_InProgress, font: Font.regular(17.0), textColor: self.presentationData.theme.list.itemSecondaryTextColor) + let infoTextSize = self.infoText.updateLayout(CGSize(width: layout.size.width - 16.0 * 2.0, height: .greatestFiniteMagnitude)) + + transition.updateFrame(node: self.radialStatus, frame: CGRect(origin: CGPoint(x: floor((layout.size.width - radialStatusSize.width) / 2.0), y: contentOrigin), size: radialStatusSize)) + let checkSize: CGFloat = 130.0 + transition.updateFrame(node: self.radialCheck, frame: CGRect(origin: CGPoint(x: self.radialStatus.frame.minX + floor((self.radialStatus.frame.width - checkSize) / 2.0), y: self.radialStatus.frame.minY + floor((self.radialStatus.frame.height - checkSize) / 2.0)), size: CGSize(width: checkSize, height: checkSize))) + transition.updateFrame(node: self.radialStatusBackground, frame: self.radialStatus.frame) + + transition.updateFrame(node: self.radialStatusText, frame: CGRect(origin: CGPoint(x: self.radialStatus.frame.minX + floor((self.radialStatus.frame.width - radialStatusTextSize.width) / 2.0), y: self.radialStatus.frame.minY + floor((self.radialStatus.frame.height - radialStatusTextSize.height) / 2.0)), size: radialStatusTextSize)) + + transition.updateFrame(node: self.progressText, frame: CGRect(origin: CGPoint(x: floor((layout.size.width - progressTextSize.width) / 2.0), y: (self.radialStatus.frame.maxY + maxProgressTextSpacing)), size: progressTextSize)) + + transition.updateFrame(node: self.infoText, frame: CGRect(origin: CGPoint(x: floor((layout.size.width - infoTextSize.width) / 2.0), y: (self.progressText.frame.maxY + maxProgressTextSpacing) + 10.0), size: infoTextSize)) + } + + private func gridPresentationLayoutUpdated(_ presentationLayout: GridNodeCurrentPresentationLayout, transition: ContainedViewLayoutTransition) { + if let (layout, _) = self.containerLayout { + var insets = layout.insets(options: [.statusBar]) + insets.top = max(10.0, insets.top) + let cleanInsets = layout.insets(options: [.statusBar]) + + var bottomInset: CGFloat = 10.0 + cleanInsets.bottom + if insets.bottom > 0 { + bottomInset -= 12.0 + } + + let buttonHeight: CGFloat = 57.0 + let sectionSpacing: CGFloat = 8.0 + let titleAreaHeight: CGFloat = 51.0 + + let width = horizontalContainerFillingSizeForLayout(layout: layout, sideInset: 10.0 + layout.safeInsets.left) + + let sideInset = floor((layout.size.width - width) / 2.0) + + let maximumContentHeight = layout.size.height - insets.top - bottomInset - buttonHeight - sectionSpacing + let contentFrame = CGRect(origin: CGPoint(x: sideInset, y: insets.top), size: CGSize(width: width, height: maximumContentHeight)) + + var backgroundFrame = CGRect(origin: CGPoint(x: contentFrame.minX, y: contentFrame.minY - presentationLayout.contentOffset.y), size: contentFrame.size) + if backgroundFrame.minY < contentFrame.minY { + backgroundFrame.origin.y = contentFrame.minY + } + if backgroundFrame.maxY > contentFrame.maxY { + backgroundFrame.size.height += contentFrame.maxY - backgroundFrame.maxY + } + if backgroundFrame.size.height < buttonHeight + 32.0 { + backgroundFrame.origin.y -= buttonHeight + 32.0 - backgroundFrame.size.height + backgroundFrame.size.height = buttonHeight + 32.0 + } + let backgroundDeltaY = backgroundFrame.minY - self.contentBackgroundNode.frame.minY + transition.updateFrame(node: self.contentBackgroundNode, frame: backgroundFrame) + transition.animatePositionAdditive(node: self.contentGridNode, offset: CGPoint(x: 0.0, y: -backgroundDeltaY)) + + let titleSize = self.contentTitleNode.bounds.size + let titleFrame = CGRect(origin: CGPoint(x: contentFrame.minX + floor((contentFrame.size.width - titleSize.width) / 2.0), y: backgroundFrame.minY + 15.0), size: titleSize) + transition.updateFrame(node: self.contentTitleNode, frame: titleFrame) + + transition.updateFrame(node: self.contentSeparatorNode, frame: CGRect(origin: CGPoint(x: contentFrame.minX, y: backgroundFrame.minY + titleAreaHeight), size: CGSize(width: contentFrame.size.width, height: UIScreenPixel))) + + if CGFloat(0.0).isLessThanOrEqualTo(presentationLayout.contentOffset.y) { + self.contentSeparatorNode.alpha = 1.0 + } else { + self.contentSeparatorNode.alpha = 0.0 + } + } + } + + @objc func dimTapGesture(_ recognizer: UITapGestureRecognizer) { + if case .ended = recognizer.state { + self.cancelButtonPressed() + } + } + + @objc func cancelButtonPressed() { + self.disposable.set(nil) + self.cancel?() + } + + private func createStickerSet(title: String, shortName: String) { + guard let stickerPack = self.stickerPack else { + return + } + + var stickers: [ImportSticker] = [] + for item in self.currentItems { + var dimensions = PixelDimensions(width: 512, height: 512) + if case let .image(data) = item.stickerItem.content, let image = UIImage(data: data) { + dimensions = PixelDimensions(image.size) + } + if let resource = self.uploadedStickerResources[item.stickerItem.uuid] { + if let localResource = item.stickerItem.resource { + self.context.account.postbox.mediaBox.copyResourceData(from: localResource.id, to: resource.id) + } + stickers.append(ImportSticker(resource: resource, emojis: item.stickerItem.emojis, dimensions: dimensions)) + } else if let resource = item.stickerItem.resource { + stickers.append(ImportSticker(resource: resource, emojis: item.stickerItem.emojis, dimensions: dimensions)) + } + } + var thumbnailSticker: ImportSticker? + if let thumbnail = stickerPack.thumbnail { + var dimensions = PixelDimensions(width: 512, height: 512) + if case let .image(data) = thumbnail.content, let image = UIImage(data: data) { + dimensions = PixelDimensions(image.size) + } + let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) + self.context.account.postbox.mediaBox.storeResourceData(resource.id, data: thumbnail.data) + thumbnailSticker = ImportSticker(resource: resource, emojis: [], dimensions: dimensions) + } + + let firstStickerItem = thumbnailSticker ?? stickers.first + + self.progress = (0.0, 0, Int32(stickers.count)) + self.radialStatus.transitionToState(.progress(color: self.presentationData.theme.list.itemAccentColor, lineWidth: 6.0, value: max(0.01, 0.0), cancelEnabled: false, animateRotation: false), animated: false, synchronous: true, completion: {}) + if let (layout, navigationBarHeight) = self.containerLayout { + self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .animated(duration: 0.2, curve: .easeInOut)) + } + + self.disposable.set((self.context.engine.stickers.createStickerSet(title: title, shortName: shortName, stickers: stickers, thumbnail: thumbnailSticker, isAnimated: stickerPack.isAnimated, software: stickerPack.software) + |> deliverOnMainQueue).start(next: { [weak self] status in + if let strongSelf = self { + if case let .complete(info, items) = status { + if let (_, _, count) = strongSelf.progress { + strongSelf.progress = (1.0, count, count) + strongSelf.radialStatus.transitionToState(.progress(color: strongSelf.presentationData.theme.list.itemAccentColor, lineWidth: 6.0, value: 1.0, cancelEnabled: false, animateRotation: false), animated: !stickerPack.isAnimated, synchronous: true, completion: {}) + if let (layout, navigationBarHeight) = strongSelf.containerLayout { + strongSelf.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate) + } + } + let _ = strongSelf.context.engine.stickers.addStickerPackInteractively(info: info, items: items).start() + + strongSelf.radialCheck.transitionToState(.progress(color: .clear, lineWidth: 6.0, value: 1.0, cancelEnabled: false, animateRotation: false), animated: false, synchronous: true, completion: {}) + strongSelf.radialCheck.transitionToState(.check(strongSelf.presentationData.theme.list.itemAccentColor), animated: true, synchronous: true, completion: {}) + strongSelf.radialStatus.layer.animateScale(from: 1.0, to: 1.05, duration: 0.07, delay: 0.0, timingFunction: CAMediaTimingFunctionName.linear.rawValue, removeOnCompletion: false, additive: false, completion: { [weak self] _ in + guard let strongSelf = self else { + return + } + strongSelf.radialStatus.layer.animateScale(from: 1.05, to: 1.0, duration: 0.07, delay: 0.0, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, removeOnCompletion: false, additive: false) + }) + strongSelf.radialStatusBackground.layer.animateScale(from: 1.0, to: 1.05, duration: 0.07, delay: 0.0, timingFunction: CAMediaTimingFunctionName.linear.rawValue, removeOnCompletion: false, additive: false, completion: { [weak self] _ in + guard let strongSelf = self else { + return + } + strongSelf.radialStatusBackground.layer.animateScale(from: 1.05, to: 1.0, duration: 0.07, delay: 0.0, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, removeOnCompletion: false, additive: false) + }) + strongSelf.radialCheck.layer.animateScale(from: 1.0, to: 1.05, duration: 0.07, delay: 0.0, timingFunction: CAMediaTimingFunctionName.linear.rawValue, removeOnCompletion: false, additive: false, completion: { [weak self] _ in + guard let strongSelf = self else { + return + } + strongSelf.radialCheck.layer.animateScale(from: 1.05, to: 1.0, duration: 0.07, delay: 0.0, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, removeOnCompletion: false, additive: false) + }) + strongSelf.radialStatusText.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) + strongSelf.radialStatusText.layer.animateScale(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) + + strongSelf.cancelButtonNode.isUserInteractionEnabled = false + + let navigationController = strongSelf.navigationController + let context = strongSelf.context + + Queue.mainQueue().after(1.0) { + var firstItem: StickerPackItem? + if let firstStickerItem = firstStickerItem, let resource = firstStickerItem.resource as? TelegramMediaResource { + firstItem = StickerPackItem(index: ItemCollectionItemIndex(index: 0, id: 0), file: TelegramMediaFile(fileId: MediaId(namespace: 0, id: 0), partialReference: nil, resource: resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: stickerPack.isAnimated ? "application/x-tgsticker": "image/png", size: nil, attributes: [.FileName(fileName: stickerPack.isAnimated ? "sticker.tgs" : "sticker.png"), .ImageSize(size: firstStickerItem.dimensions)]), indexKeys: []) + } + strongSelf.presentInGlobalOverlay?(UndoOverlayController(presentationData: strongSelf.presentationData, content: .stickersModified(title: strongSelf.presentationData.strings.StickerPackActionInfo_AddedTitle, text: strongSelf.presentationData.strings.StickerPackActionInfo_AddedText(info.title).0, undo: false, info: info, topItem: firstItem ?? items.first, context: strongSelf.context), elevatedLayout: false, action: { action in + if case .info = action { + (navigationController?.viewControllers.last as? ViewController)?.present(StickerPackScreen(context: context, mode: .settings, mainStickerPack: .id(id: info.id.id, accessHash: info.accessHash), stickerPacks: [], parentNavigationController: navigationController, actionPerformed: { _, _, _ in + }), in: .window(.root)) + } + return true + }), nil) + strongSelf.cancel?() + } + } else if case let .progress(progress, count, total) = status { + strongSelf.progress = (CGFloat(progress), count, total) + strongSelf.radialStatus.transitionToState(.progress(color: strongSelf.presentationData.theme.list.itemAccentColor, lineWidth: 6.0, value: max(0.01, CGFloat(progress)), cancelEnabled: false, animateRotation: false), animated: true, synchronous: true, completion: {}) + if let (layout, navigationBarHeight) = strongSelf.containerLayout { + strongSelf.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate) + } + } + } + }, error: { [weak self] error in + if let strongSelf = self { + + } + })) + } + + @objc private func createActionButtonPressed() { + var proceedImpl: ((String, String?) -> Void)? + let titleController = importStickerPackTitleController(context: self.context, title: self.presentationData.strings.ImportStickerPack_ChooseName, text: self.presentationData.strings.ImportStickerPack_ChooseNameDescription, placeholder: self.presentationData.strings.ImportStickerPack_NamePlaceholder, value: nil, maxLength: 128, apply: { [weak self] title in + if let strongSelf = self, let title = title { + strongSelf.shortNameSuggestionDisposable.set((strongSelf.context.engine.stickers.getStickerSetShortNameSuggestion(title: title) + |> deliverOnMainQueue).start(next: { suggestedShortName in + proceedImpl?(title, suggestedShortName) + })) + } + }, cancel: { [weak self] in + if let strongSelf = self { + strongSelf.shortNameSuggestionDisposable.set(nil) + } + }) + proceedImpl = { [weak titleController, weak self] title, suggestedShortName in + guard let strongSelf = self else { + return + } + let controller = importStickerPackShortNameController(context: strongSelf.context, title: strongSelf.presentationData.strings.ImportStickerPack_ChooseLink, text: strongSelf.presentationData.strings.ImportStickerPack_ChooseLinkDescription, placeholder: "", value: suggestedShortName, maxLength: 60, existingAlertController: titleController, apply: { [weak self] shortName in + if let shortName = shortName { + self?.createStickerSet(title: title, shortName: shortName) + } + }) + strongSelf.present?(controller, nil) + } + self.present?(titleController, nil) + } + + @objc private func addToExistingActionButtonPressed() { + + } + + func animateIn() { + self.dimNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) + + let offset = self.bounds.size.height - self.contentBackgroundNode.frame.minY + + let dimPosition = self.dimNode.layer.position + self.dimNode.layer.animatePosition(from: CGPoint(x: dimPosition.x, y: dimPosition.y - offset), to: dimPosition, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring) + self.layer.animateBoundsOriginYAdditive(from: -offset, to: 0.0, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring) + } + + func animateOut(completion: (() -> Void)? = nil) { + var dimCompleted = false + var offsetCompleted = false + + let internalCompletion: () -> Void = { [weak self] in + if let strongSelf = self, dimCompleted && offsetCompleted { + strongSelf.dismiss?() + } + completion?() + } + + self.dimNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { _ in + dimCompleted = true + internalCompletion() + }) + + let offset = self.bounds.size.height - self.contentBackgroundNode.frame.minY + let dimPosition = self.dimNode.layer.position + self.dimNode.layer.animatePosition(from: dimPosition, to: CGPoint(x: dimPosition.x, y: dimPosition.y - offset), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) + self.layer.animateBoundsOriginYAdditive(from: 0.0, to: -offset, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in + offsetCompleted = true + internalCompletion() + }) + } + + func updateStickerPack(_ stickerPack: ImportStickerPack, verifiedStickers: Set, declinedStickers: Set, uploadedStickerResources: [UUID: MediaResource]) { + self.stickerPack = stickerPack + self.uploadedStickerResources = uploadedStickerResources + var updatedItems: [StickerPackPreviewGridEntry] = [] + for item in stickerPack.stickers { + if declinedStickers.contains(item.uuid) { + continue + } + if let resource = self.stickerResources[item.uuid] { + item.resource = resource + } else { + let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) + self.context.account.postbox.mediaBox.storeResourceData(resource.id, data: item.data) + item.resource = resource + self.stickerResources[item.uuid] = resource + } + updatedItems.append(StickerPackPreviewGridEntry(index: updatedItems.count, stickerItem: item, isVerified: !item.isAnimated || verifiedStickers.contains(item.uuid))) + } + self.pendingItems = updatedItems + + if stickerPack.isAnimated { + self.stickerPackReady = stickerPack.stickers.count == (verifiedStickers.count + declinedStickers.count) && updatedItems.count > 0 + } + + self.interaction.playAnimatedStickers = true + + if let _ = self.containerLayout { + self.dequeueUpdateStickerPack() + } + } + + func dequeueUpdateStickerPack() { + if let (layout, navigationBarHeight) = self.containerLayout, let _ = self.stickerPack { + let transition: ContainedViewLayoutTransition + if self.didSetReady { + transition = .animated(duration: 0.4, curve: .spring) + } else { + transition = .immediate + } + + self.contentTitleNode.alpha = 1.0 + self.contentTitleNode.layer.animateAlpha(from: self.contentTitleNode.alpha, to: 1.0, duration: 0.2) + + self.contentGridNode.alpha = 1.0 + self.contentGridNode.layer.animateAlpha(from: self.contentGridNode.alpha, to: 1.0, duration: 0.2) + + let buttonTransition: ContainedViewLayoutTransition = .animated(duration: 0.3, curve: .easeInOut) + buttonTransition.updateAlpha(node: self.createActionButtonNode, alpha: self.stickerPackReady ? 1.0 : 0.3) + buttonTransition.updateAlpha(node: self.addToExistingActionButtonNode, alpha: self.stickerPackReady ? 1.0 : 0.3) + + self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition) + + if !self.didSetReady { + self.didSetReady = true + self.ready.set(.single(true)) + } + } + } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if let result = self.createActionButtonNode.hitTest(self.createActionButtonNode.convert(point, from: self), with: event) { + return result + } + if self.bounds.contains(point) { + if !self.contentBackgroundNode.bounds.contains(self.convert(point, to: self.contentBackgroundNode)) && !self.cancelButtonNode.bounds.contains(self.convert(point, to: self.cancelButtonNode)) { + return self.dimNode.view + } + } + return super.hitTest(point, with: event) + } + + func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { + let contentOffset = scrollView.contentOffset + let additionalTopHeight = max(0.0, -contentOffset.y) + + if additionalTopHeight >= 30.0 { + self.cancelButtonPressed() + } + } + + private func updatePreviewingItem(item: ImportStickerPack.Sticker?, animated: Bool) { + if self.interaction.previewedItem !== item { + self.interaction.previewedItem = item + + self.contentGridNode.forEachItemNode { itemNode in + if let itemNode = itemNode as? StickerPackPreviewGridItemNode { + itemNode.updatePreviewing(animated: animated) + } + } + } + } +} diff --git a/submodules/ImportStickerPackUI/Sources/ImportStickerPackTitleController.swift b/submodules/ImportStickerPackUI/Sources/ImportStickerPackTitleController.swift new file mode 100644 index 0000000000..d07f6b3078 --- /dev/null +++ b/submodules/ImportStickerPackUI/Sources/ImportStickerPackTitleController.swift @@ -0,0 +1,870 @@ +import Foundation +import UIKit +import SwiftSignalKit +import AsyncDisplayKit +import Display +import Postbox +import TelegramCore +import SyncCore +import TelegramPresentationData +import AccountContext +import UrlEscaping +import ActivityIndicator + +private class TextField: UITextField, UIScrollViewDelegate { + let placeholderLabel: ImmediateTextNode + var placeholderString: NSAttributedString? { + didSet { + self.placeholderLabel.attributedText = self.placeholderString + self.setNeedsLayout() + } + } + + fileprivate func updatePrefixWidth(_ prefixWidth: CGFloat) { + let previousPrefixWidth = self.prefixWidth + guard previousPrefixWidth != prefixWidth else { + return + } + self.prefixWidth = prefixWidth + if let scrollView = self.scrollView { + if scrollView.contentInset.left != prefixWidth { + scrollView.contentInset = UIEdgeInsets(top: 0.0, left: prefixWidth, bottom: 0.0, right: 0.0) + } + if prefixWidth.isZero { + scrollView.contentOffset = CGPoint() + } else if prefixWidth != previousPrefixWidth { + scrollView.contentOffset = CGPoint(x: -prefixWidth, y: 0.0) + } + self.updatePrefixPosition(transition: .immediate) + } + } + + private var prefixWidth: CGFloat = 0.0 + + let prefixLabel: ImmediateTextNode + var prefixString: NSAttributedString? { + didSet { + self.prefixLabel.attributedText = self.prefixString + self.setNeedsLayout() + } + } + + init() { + self.prefixLabel = ImmediateTextNode() + self.prefixLabel.isUserInteractionEnabled = false + self.prefixLabel.displaysAsynchronously = false + self.prefixLabel.maximumNumberOfLines = 1 + self.prefixLabel.truncationMode = .byTruncatingTail + + self.placeholderLabel = ImmediateTextNode() + self.placeholderLabel.isUserInteractionEnabled = false + self.placeholderLabel.displaysAsynchronously = false + self.placeholderLabel.maximumNumberOfLines = 1 + self.placeholderLabel.truncationMode = .byTruncatingTail + + super.init(frame: CGRect()) + + self.addSubnode(self.prefixLabel) + self.addSubnode(self.placeholderLabel) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func addSubview(_ view: UIView) { + super.addSubview(view) + + if let scrollView = view as? UIScrollView { + scrollView.delegate = self + } + } + + private weak var _scrollView: UIScrollView? + var scrollView: UIScrollView? { + if let scrollView = self._scrollView { + return scrollView + } + for view in self.subviews { + if let scrollView = view as? UIScrollView { + _scrollView = scrollView + return scrollView + } + } + return nil + } + + override func deleteBackward() { + super.deleteBackward() + + if let scrollView = self.scrollView { + if scrollView.contentSize.width <= scrollView.frame.width && scrollView.contentOffset.x > -scrollView.contentInset.left { + scrollView.contentOffset = CGPoint(x: max(scrollView.contentOffset.x - 5.0, -scrollView.contentInset.left), y: 0.0) + self.updatePrefixPosition() + } + } + } + + func selectWhole() { + guard let scrollView = self.scrollView else { + return + } +// if scrollView.contentSize.width > scrollView.frame.width - scrollView.contentInset.left { +// scrollView.contentOffset = CGPoint(x: -scrollView.contentInset.left + scrollView.contentSize.width - (scrollView.frame.width - scrollView.contentInset.left), y: 0.0) +// self.updatePrefixPosition() +// } + self.selectAll(nil) + } + + var fixAutoScroll: CGPoint? + func scrollViewDidScroll(_ scrollView: UIScrollView) { + if let fixAutoScroll = self.fixAutoScroll { + self.scrollView?.setContentOffset(fixAutoScroll, animated: true) + self.scrollView?.setContentOffset(fixAutoScroll, animated: false) + self.fixAutoScroll = nil + } else { + self.updatePrefixPosition() + } + } + + override func becomeFirstResponder() -> Bool { + if let contentOffset = self.scrollView?.contentOffset { + self.fixAutoScroll = contentOffset + Queue.mainQueue().after(0.1) { + self.fixAutoScroll = nil + } + } + return super.becomeFirstResponder() + } + + private func updatePrefixPosition(transition: ContainedViewLayoutTransition = .immediate) { + if let scrollView = self.scrollView { + transition.updateFrame(node: self.prefixLabel, frame: CGRect(origin: CGPoint(x: -scrollView.contentOffset.x - scrollView.contentInset.left, y: self.prefixLabel.frame.minY), size: self.prefixLabel.frame.size)) + } + } + + override var keyboardAppearance: UIKeyboardAppearance { + get { + return super.keyboardAppearance + } + set { + let resigning = self.isFirstResponder + if resigning { + self.resignFirstResponder() + } + super.keyboardAppearance = newValue + if resigning { + let _ = self.becomeFirstResponder() + } + } + } + + override func textRect(forBounds bounds: CGRect) -> CGRect { + if bounds.size.width.isZero { + return CGRect(origin: CGPoint(), size: CGSize()) + } + var rect = bounds.insetBy(dx: 0.0, dy: 4.0) + if #available(iOS 14.0, *) { + } else { + rect.origin.y += 1.0 + } + if !self.prefixWidth.isZero && self.scrollView?.superview == nil { + var offset = self.prefixWidth + if let scrollView = self.scrollView { + offset = scrollView.contentOffset.x * -1.0 + } + rect.origin.x += offset + rect.size.width -= offset + } + rect.size.width = max(rect.size.width, 10.0) + return rect + } + + override func editingRect(forBounds bounds: CGRect) -> CGRect { + return self.textRect(forBounds: bounds) + } + + override func layoutSubviews() { + super.layoutSubviews() + + let bounds = self.bounds + if bounds.size.width.isZero { + return + } + + let textRect = self.textRect(forBounds: bounds) + + let labelSize = self.placeholderLabel.updateLayout(textRect.size) + self.placeholderLabel.frame = CGRect(origin: CGPoint(x: textRect.minX + 3.0, y: floorToScreenPixels((bounds.height - labelSize.height) / 2.0)), size: labelSize) + + let prefixSize = self.prefixLabel.updateLayout(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: floorToScreenPixels((bounds.height - prefixSize.height) / 2.0)), size: prefixSize) + self.updatePrefixWidth(prefixSize.width + 3.0) + } +} + +private let validIdentifierSet: CharacterSet = { + var set = CharacterSet(charactersIn: "a".unicodeScalars.first! ... "z".unicodeScalars.first!) + set.insert(charactersIn: "A".unicodeScalars.first! ... "Z".unicodeScalars.first!) + set.insert(charactersIn: "0".unicodeScalars.first! ... "9".unicodeScalars.first!) + set.insert("_") + return set +}() + +private final class ImportStickerPackTitleInputFieldNode: ASDisplayNode, UITextFieldDelegate { + private var theme: PresentationTheme + private let backgroundNode: ASImageNode + private let textInputNode: TextField + private let clearButton: HighlightableButtonNode + + var updateHeight: (() -> Void)? + var complete: (() -> Void)? + var textChanged: ((String) -> Void)? + + private let backgroundInsets = UIEdgeInsets(top: 8.0, left: 16.0, bottom: 15.0, right: 16.0) + private let inputInsets = UIEdgeInsets(top: 8.0, left: 8.0, bottom: 8.0, right: 8.0) + + var text: String { + get { + return self.textInputNode.attributedText?.string ?? "" + } + set { + self.textInputNode.attributedText = NSAttributedString(string: newValue, font: Font.regular(14.0), textColor: self.theme.actionSheet.inputTextColor) + self.textInputNode.placeholderLabel.isHidden = !newValue.isEmpty + if self.textInputNode.isFirstResponder { + self.clearButton.isHidden = newValue.isEmpty + } else { + self.clearButton.isHidden = true + } + } + } + + var prefix: String = "" { + didSet { + self.textInputNode.prefixString = NSAttributedString(string: self.prefix, font: Font.regular(14.0), textColor: self.theme.actionSheet.inputTextColor) + } + } + + var disabled: Bool = false { + didSet { + self.clearButton.isHidden = true + } + } + + private let maxLength: Int + + init(theme: PresentationTheme, placeholder: String, maxLength: Int, keyboardType: UIKeyboardType = .default, returnKeyType: UIReturnKeyType = .done) { + self.theme = theme + self.maxLength = maxLength + + self.backgroundNode = ASImageNode() + self.backgroundNode.displaysAsynchronously = false + self.backgroundNode.displayWithoutProcessing = true + self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 12.0, color: theme.actionSheet.inputHollowBackgroundColor, strokeColor: theme.actionSheet.inputBorderColor, strokeWidth: 1.0) + + self.textInputNode = TextField() + self.textInputNode.font = Font.regular(14.0) + self.textInputNode.typingAttributes = [NSAttributedString.Key.font: Font.regular(14.0), NSAttributedString.Key.foregroundColor: theme.actionSheet.inputTextColor] + self.textInputNode.clipsToBounds = true + self.textInputNode.placeholderString = NSAttributedString(string: placeholder, font: Font.regular(14.0), textColor: theme.actionSheet.secondaryTextColor) + self.textInputNode.keyboardAppearance = theme.rootController.keyboardColor.keyboardAppearance + self.textInputNode.keyboardType = keyboardType + self.textInputNode.autocapitalizationType = .sentences + self.textInputNode.returnKeyType = returnKeyType + self.textInputNode.autocorrectionType = .default + self.textInputNode.tintColor = theme.actionSheet.controlAccentColor + + self.clearButton = HighlightableButtonNode() + self.clearButton.imageNode.displaysAsynchronously = false + self.clearButton.imageNode.displayWithoutProcessing = true + self.clearButton.displaysAsynchronously = false + self.clearButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Components/Search Bar/Clear"), color: theme.actionSheet.inputClearButtonColor), for: []) + self.clearButton.isHidden = true + + super.init() + + self.addSubnode(self.backgroundNode) + self.addSubnode(self.clearButton) + + self.clearButton.addTarget(self, action: #selector(self.clearPressed), forControlEvents: .touchUpInside) + } + + override func didLoad() { + super.didLoad() + + self.textInputNode.delegate = self + self.view.insertSubview(self.textInputNode, aboveSubview: self.backgroundNode.view) + } + + func selectAll() { + self.textInputNode.selectWhole() + } + + func updateTheme(_ theme: PresentationTheme) { + self.theme = theme + + self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 12.0, color: self.theme.actionSheet.inputHollowBackgroundColor, strokeColor: self.theme.actionSheet.inputBorderColor, strokeWidth: 1.0) + self.textInputNode.keyboardAppearance = self.theme.rootController.keyboardColor.keyboardAppearance + self.textInputNode.tintColor = self.theme.actionSheet.controlAccentColor + self.clearButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Components/Search Bar/Clear"), color: theme.actionSheet.inputClearButtonColor), for: []) + } + + func updateLayout(width: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat { + let backgroundInsets = self.backgroundInsets + let inputInsets = self.inputInsets + + let textFieldHeight = self.calculateTextFieldMetrics(width: width) + let panelHeight = textFieldHeight + backgroundInsets.top + backgroundInsets.bottom + + let backgroundFrame = CGRect(origin: CGPoint(x: backgroundInsets.left, y: backgroundInsets.top), size: CGSize(width: width - backgroundInsets.left - backgroundInsets.right, height: panelHeight - backgroundInsets.top - backgroundInsets.bottom)) + transition.updateFrame(node: self.backgroundNode, frame: backgroundFrame) + + transition.updateFrame(view: self.textInputNode, frame: CGRect(origin: CGPoint(x: backgroundFrame.minX + inputInsets.left, y: backgroundFrame.minY), size: CGSize(width: backgroundFrame.size.width - inputInsets.left - inputInsets.right - 22.0, height: backgroundFrame.size.height))) + + if let image = self.clearButton.image(for: []) { + transition.updateFrame(node: self.clearButton, frame: CGRect(origin: CGPoint(x: backgroundFrame.maxX - 8.0 - image.size.width, y: backgroundFrame.minY + floor((backgroundFrame.size.height - image.size.height) / 2.0)), size: image.size)) + } + + return panelHeight + } + + func activateInput() { + let _ = self.textInputNode.becomeFirstResponder() + } + + func deactivateInput() { + self.textInputNode.resignFirstResponder() + } + + func textFieldDidBeginEditing(_ textField: UITextField) { + self.clearButton.isHidden = (textField.text ?? "").isEmpty + } + + func textFieldDidEndEditing(_ textField: UITextField) { + self.clearButton.isHidden = true + } + + func textFieldDidUpdateText(_ text: String) { + self.updateTextNodeText(animated: true) + self.textChanged?(text) + self.clearButton.isHidden = text.isEmpty + self.textInputNode.placeholderLabel.isHidden = !text.isEmpty + } + + func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { + if self.disabled { + return false + } + let updatedText = ((textField.text ?? "") as NSString).replacingCharacters(in: range, with: string) + if updatedText.count > maxLength { + self.textInputNode.layer.addShakeAnimation() + return false + } + if string == "\n" { + self.complete?() + return false + } + + if self.textInputNode.keyboardType == .asciiCapable { + var cleanString = string.folding(options: .diacriticInsensitive, locale: .current).replacingOccurrences(of: " ", with: "_") + + let filtered = cleanString.unicodeScalars.filter { validIdentifierSet.contains($0) } + let filteredString = String(String.UnicodeScalarView(filtered)) + + if cleanString != filteredString { + cleanString = filteredString + + self.textInputNode.layer.addShakeAnimation() + let hapticFeedback = HapticFeedback() + hapticFeedback.error() + DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 1.0, execute: { + let _ = hapticFeedback + }) + } + + if cleanString != string { + var text = textField.text ?? "" + text.replaceSubrange(text.index(text.startIndex, offsetBy: range.lowerBound) ..< text.index(text.startIndex, offsetBy: range.upperBound), with: cleanString) + textField.text = text + if let startPosition = textField.position(from: textField.beginningOfDocument, offset: range.lowerBound + cleanString.count) { + let selectionRange = textField.textRange(from: startPosition, to: startPosition) + DispatchQueue.main.async { + textField.selectedTextRange = selectionRange + } + } + self.textFieldDidUpdateText(text) + return false + } + } + + self.textFieldDidUpdateText(updatedText) + return true + } + + private func calculateTextFieldMetrics(width: CGFloat) -> CGFloat { + return 33.0 + } + + private func updateTextNodeText(animated: Bool) { + let backgroundInsets = self.backgroundInsets + + let textFieldHeight = self.calculateTextFieldMetrics(width: self.bounds.size.width) + + let panelHeight = textFieldHeight + backgroundInsets.top + backgroundInsets.bottom + if !self.bounds.size.height.isEqual(to: panelHeight) { + self.updateHeight?() + } + } + + @objc func clearPressed() { + self.clearButton.isHidden = true + + self.textInputNode.attributedText = nil + self.updateHeight?() + self.textChanged?("") + } +} + +private final class ImportStickerPackTitleAlertContentNode: AlertContentNode { + enum InfoText { + case info + case checking + case available + case taken + case generating + } + private var theme: PresentationTheme + private var alertTheme: AlertControllerTheme + private let strings: PresentationStrings + private let title: String + private let text: String + + var infoText: InfoText? { + didSet { + let text: String + let color: UIColor + var activity = false + if let infoText = self.infoText { + switch infoText { + case .info: + text = self.strings.ImportStickerPack_ChooseLinkDescription + color = self.alertTheme.primaryColor + case .checking: + text = self.strings.ImportStickerPack_CheckingLink + color = self.alertTheme.secondaryColor + activity = true + case .available: + text = self.strings.ImportStickerPack_LinkAvailable + color = self.theme.list.freeTextSuccessColor + case .taken: + text = self.strings.ImportStickerPack_LinkTaken + color = self.theme.list.freeTextErrorColor + case .generating: + text = self.strings.ImportStickerPack_GeneratingLink + color = self.alertTheme.secondaryColor + activity = true + } + self.activityIndicator.isHidden = !activity + } else { + text = self.text + color = self.alertTheme.primaryColor + } + self.textNode.attributedText = NSAttributedString(string: text, font: Font.regular(13.0), textColor: color) + if let size = self.validLayout { + _ = self.updateLayout(size: size, transition: .immediate) + } + } + } + + private let titleNode: ASTextNode + private let textNode: ASTextNode + private let activityIndicator: ActivityIndicator + let inputFieldNode: ImportStickerPackTitleInputFieldNode + + private let actionNodesSeparator: ASDisplayNode + fileprivate let actionNodes: [TextAlertContentActionNode] + private let actionVerticalSeparators: [ASDisplayNode] + + private let disposable = MetaDisposable() + + private var validLayout: CGSize? + + private let hapticFeedback = HapticFeedback() + + var complete: (() -> Void)? { + didSet { + self.inputFieldNode.complete = self.complete + } + } + + override var dismissOnOutsideTap: Bool { + return self.isUserInteractionEnabled + } + + init(theme: AlertControllerTheme, ptheme: PresentationTheme, strings: PresentationStrings, actions: [TextAlertAction], title: String, text: String, placeholder: String, value: String?, maxLength: Int, asciiOnly: Bool = false) { + self.strings = strings + self.alertTheme = theme + self.theme = ptheme + self.title = title + self.text = text + + self.titleNode = ASTextNode() + self.titleNode.maximumNumberOfLines = 2 + self.textNode = ASTextNode() + self.textNode.maximumNumberOfLines = 8 + + self.activityIndicator = ActivityIndicator(type: .custom(ptheme.rootController.navigationBar.secondaryTextColor, 20.0, 1.5, false), speed: .slow) + self.activityIndicator.isHidden = true + + self.inputFieldNode = ImportStickerPackTitleInputFieldNode(theme: ptheme, placeholder: placeholder, maxLength: maxLength, keyboardType: asciiOnly ? .asciiCapable : .default, returnKeyType: asciiOnly ? .done : .next) + if asciiOnly { + self.inputFieldNode.prefix = "t.me/addstickers/" + } + self.inputFieldNode.text = value ?? "" + + 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.titleNode) + self.addSubnode(self.textNode) + self.addSubnode(self.activityIndicator) + + self.addSubnode(self.inputFieldNode) + + self.addSubnode(self.actionNodesSeparator) + + for actionNode in self.actionNodes { + self.addSubnode(actionNode) + } + + for separatorNode in self.actionVerticalSeparators { + self.addSubnode(separatorNode) + } + + self.inputFieldNode.updateHeight = { [weak self] in + if let strongSelf = self { + if let _ = strongSelf.validLayout { + strongSelf.requestLayout?(.animated(duration: 0.15, curve: .spring)) + } + } + } + + self.updateTheme(theme) + } + + deinit { + self.disposable.dispose() + } + + var value: String { + return self.inputFieldNode.text + } + + override func updateTheme(_ theme: AlertControllerTheme) { + self.alertTheme = theme + + self.titleNode.attributedText = NSAttributedString(string: self.title, font: Font.bold(17.0), textColor: theme.primaryColor, paragraphAlignment: .center) + self.textNode.attributedText = NSAttributedString(string: self.text, 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 spacing: CGFloat = 5.0 + + let titleSize = self.titleNode.measure(measureSize) + transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - titleSize.width) / 2.0), y: origin.y), size: titleSize)) + origin.y += titleSize.height + 4.0 + + let activitySize = CGSize(width: 20.0, height: 20.0) + let textSize = self.textNode.measure(measureSize) + let activityInset: CGFloat = self.activityIndicator.isHidden ? 0.0 : activitySize.width + 5.0 + let totalWidth = textSize.width + activityInset + transition.updateFrame(node: self.activityIndicator, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - totalWidth) / 2.0), y: origin.y - 1.0), size: activitySize)) + transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - totalWidth) / 2.0) + activityInset, y: origin.y), size: textSize)) + + origin.y += textSize.height + 6.0 + spacing + + 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.updateLayout(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: 9.0, right: 18.0) + + var contentWidth = max(titleSize.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 inputFieldWidth = resultWidth + let inputFieldHeight = self.inputFieldNode.updateLayout(width: inputFieldWidth, transition: transition) + let inputHeight = inputFieldHeight + transition.updateFrame(node: self.inputFieldNode, frame: CGRect(x: 0.0, y: origin.y, width: resultWidth, height: inputFieldHeight)) + transition.updateAlpha(node: self.inputFieldNode, alpha: inputHeight > 0.0 ? 1.0 : 0.0) + + let resultSize = CGSize(width: resultWidth, height: titleSize.height + textSize.height + spacing + inputHeight + actionsHeight + 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 { + self.inputFieldNode.activateInput() + } + + return resultSize + } + + func animateError() { + self.inputFieldNode.layer.addShakeAnimation() + self.hapticFeedback.error() + } +} + +func importStickerPackTitleController(context: AccountContext, title: String, text: String, placeholder: String, value: String?, maxLength: Int, apply: @escaping (String?) -> Void, cancel: @escaping () -> Void) -> AlertController { + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + var dismissImpl: ((Bool) -> Void)? + var applyImpl: (() -> Void)? + + let actions: [TextAlertAction] = [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: { + dismissImpl?(true) + cancel() + }), TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Next, action: { + applyImpl?() + })] + + let contentNode = ImportStickerPackTitleAlertContentNode(theme: AlertControllerTheme(presentationData: presentationData), ptheme: presentationData.theme, strings: presentationData.strings, actions: actions, title: title, text: text, placeholder: placeholder, value: value, maxLength: maxLength) + contentNode.complete = { + applyImpl?() + } + applyImpl = { [weak contentNode] in + guard let contentNode = contentNode else { + return + } + let newValue = contentNode.value.trimmingCharacters(in: .whitespacesAndNewlines) + guard !newValue.isEmpty else { + return + } + + contentNode.infoText = .generating + contentNode.inputFieldNode.disabled = true + contentNode.actionNodes.last?.actionEnabled = false + + apply(newValue) + } + + 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) + contentNode?.inputFieldNode.updateTheme(presentationData.theme) + }) + contentNode.actionNodes.last?.actionEnabled = false + contentNode.inputFieldNode.textChanged = { [weak contentNode] title in + contentNode?.actionNodes.last?.actionEnabled = !title.trimmingTrailingSpaces().isEmpty + } + controller.willDismiss = { [weak contentNode] in + contentNode?.inputFieldNode.deactivateInput() + } + controller.dismissed = { + presentationDataDisposable.dispose() + } + dismissImpl = { [weak controller, weak contentNode] animated in + contentNode?.inputFieldNode.deactivateInput() + if animated { + controller?.dismissAnimated() + } else { + controller?.dismiss() + } + } + return controller +} + + +func importStickerPackShortNameController(context: AccountContext, title: String, text: String, placeholder: String, value: String?, maxLength: Int, existingAlertController: AlertController?, apply: @escaping (String?) -> Void) -> AlertController { + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + var dismissImpl: ((Bool) -> Void)? + var applyImpl: (() -> Void)? + + let actions: [TextAlertAction] = [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: { + dismissImpl?(true) + }), TextAlertAction(type: .defaultAction, title: presentationData.strings.ImportStickerPack_Create, action: { + applyImpl?() + })] + + let contentNode = ImportStickerPackTitleAlertContentNode(theme: AlertControllerTheme(presentationData: presentationData), ptheme: presentationData.theme, strings: presentationData.strings, actions: actions, title: title, text: text, placeholder: placeholder, value: value, maxLength: maxLength, asciiOnly: true) + contentNode.complete = { + applyImpl?() + } + applyImpl = { [weak contentNode] in + guard let contentNode = contentNode else { + return + } + let newValue = contentNode.value.trimmingCharacters(in: .whitespacesAndNewlines) + guard !newValue.isEmpty else { + return + } + + dismissImpl?(true) + apply(newValue) + } + + let controller = AlertController(theme: AlertControllerTheme(presentationData: presentationData), contentNode: contentNode, existingAlertController: existingAlertController) + let presentationDataDisposable = context.sharedContext.presentationData.start(next: { [weak controller, weak contentNode] presentationData in + controller?.theme = AlertControllerTheme(presentationData: presentationData) + contentNode?.inputFieldNode.updateTheme(presentationData.theme) + }) + let checkDisposable = MetaDisposable() + var value = value ?? "" + contentNode.actionNodes.last?.actionEnabled = !value.isEmpty + if !value.isEmpty { + Queue.mainQueue().after(0.25) { + contentNode.inputFieldNode.selectAll() + } + } + contentNode.inputFieldNode.textChanged = { [weak contentNode] value in + if value.isEmpty { + checkDisposable.set(nil) + contentNode?.infoText = .info + contentNode?.actionNodes.last?.actionEnabled = false + } else { + checkDisposable.set((context.engine.stickers.validateStickerSetShortNameInteractive(shortName: value) + |> deliverOnMainQueue).start(next: { [weak contentNode] result in + switch result { + case .checking: + contentNode?.infoText = .checking + contentNode?.actionNodes.last?.actionEnabled = false + case let .availability(availability): + switch availability { + case .available: + contentNode?.infoText = .available + contentNode?.actionNodes.last?.actionEnabled = true + case .taken: + contentNode?.infoText = .taken + contentNode?.actionNodes.last?.actionEnabled = false + case .invalid: + contentNode?.infoText = .info + contentNode?.actionNodes.last?.actionEnabled = false + } + case .invalidFormat: + contentNode?.infoText = .info + contentNode?.actionNodes.last?.actionEnabled = false + } + })) + } + } + controller.willDismiss = { [weak contentNode] in + contentNode?.inputFieldNode.deactivateInput() + } + controller.dismissed = { + presentationDataDisposable.dispose() + } + dismissImpl = { [weak controller, weak contentNode] animated in + contentNode?.inputFieldNode.deactivateInput() + if animated { + controller?.dismissAnimated() + } else { + controller?.dismiss() + } + } + return controller +} diff --git a/submodules/ImportStickerPackUI/Sources/StickerPackPreviewGridItem.swift b/submodules/ImportStickerPackUI/Sources/StickerPackPreviewGridItem.swift new file mode 100644 index 0000000000..cf334e4a36 --- /dev/null +++ b/submodules/ImportStickerPackUI/Sources/StickerPackPreviewGridItem.swift @@ -0,0 +1,223 @@ +import Foundation +import UIKit +import Display +import TelegramCore +import SyncCore +import SwiftSignalKit +import AsyncDisplayKit +import Postbox +import StickerResources +import AccountContext +import AnimatedStickerNode +import TelegramAnimatedStickerNode +import TelegramPresentationData +import ShimmerEffect + +final class StickerPackPreviewInteraction { + var previewedItem: ImportStickerPack.Sticker? + var playAnimatedStickers: Bool + + init(playAnimatedStickers: Bool) { + self.playAnimatedStickers = playAnimatedStickers + } +} + +final class StickerPackPreviewGridItem: GridItem { + let account: Account + let stickerItem: ImportStickerPack.Sticker + let interaction: StickerPackPreviewInteraction + let theme: PresentationTheme + let isVerified: Bool + + let section: GridSection? = nil + + init(account: Account, stickerItem: ImportStickerPack.Sticker, interaction: StickerPackPreviewInteraction, theme: PresentationTheme, isVerified: Bool) { + self.account = account + self.stickerItem = stickerItem + self.interaction = interaction + self.theme = theme + self.isVerified = isVerified + } + + func node(layout: GridNodeLayout, synchronousLoad: Bool) -> GridItemNode { + let node = StickerPackPreviewGridItemNode() + node.setup(account: self.account, stickerItem: self.stickerItem, interaction: self.interaction, theme: self.theme, isVerified: self.isVerified) + return node + } + + func update(node: GridItemNode) { + guard let node = node as? StickerPackPreviewGridItemNode else { + assertionFailure() + return + } + node.setup(account: self.account, stickerItem: self.stickerItem, interaction: self.interaction, theme: self.theme, isVerified: self.isVerified) + } +} + +private let textFont = Font.regular(20.0) + +final class StickerPackPreviewGridItemNode: GridItemNode { + private var currentState: (Account, ImportStickerPack.Sticker?, CGSize)? + private var isVerified: Bool? + private let imageNode: ASImageNode + private var animationNode: AnimatedStickerNode? + private var placeholderNode: ShimmerEffectNode? + + private var theme: PresentationTheme? + + override var isVisibleInGrid: Bool { + didSet { + self.animationNode?.visibility = self.isVisibleInGrid && self.interaction?.playAnimatedStickers ?? true + } + } + + private var currentIsPreviewing = false + + private let stickerFetchedDisposable = MetaDisposable() + + var interaction: StickerPackPreviewInteraction? + + var selected: (() -> Void)? + + var stickerPackItem: ImportStickerPack.Sticker? { + return self.currentState?.1 + } + + override init() { + self.imageNode = ASImageNode() + + super.init() + + self.addSubnode(self.imageNode) + } + + deinit { + self.stickerFetchedDisposable.dispose() + } + + override func didLoad() { + super.didLoad() + + self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.imageNodeTap(_:)))) + } + + func setup(account: Account, stickerItem: ImportStickerPack.Sticker?, interaction: StickerPackPreviewInteraction, theme: PresentationTheme, isVerified: Bool) { + self.interaction = interaction + self.theme = theme + + if self.currentState == nil || self.currentState!.0 !== account || self.currentState!.1 !== stickerItem || self.isVerified != isVerified { + var dimensions = CGSize(width: 512.0, height: 512.0) + if let stickerItem = stickerItem { + switch stickerItem.content { + case let .image(data): + if let animationNode = self.animationNode { + animationNode.visibility = false + self.animationNode = nil + animationNode.removeFromSupernode() + } + self.imageNode.isHidden = false + if let image = UIImage(data: data) { + self.imageNode.image = image + dimensions = image.size + } + case .animation: + self.imageNode.isHidden = true + + if isVerified { + let animationNode = AnimatedStickerNode() + self.animationNode = animationNode + + if let placeholderNode = self.placeholderNode { + self.placeholderNode = nil + placeholderNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak placeholderNode] _ in + placeholderNode?.removeFromSupernode() + }) + self.insertSubnode(animationNode, belowSubnode: placeholderNode) + } else { + self.addSubnode(animationNode) + } + + let fittedDimensions = dimensions.aspectFitted(CGSize(width: 160.0, height: 160.0)) + if let resource = stickerItem.resource { + animationNode.setup(source: AnimatedStickerResourceSource(account: account, resource: resource), width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), mode: .direct(cachePathPrefix: nil)) + } + animationNode.visibility = self.isVisibleInGrid && self.interaction?.playAnimatedStickers ?? true + } else { + let placeholderNode = ShimmerEffectNode() + self.placeholderNode = placeholderNode + + self.addSubnode(placeholderNode) + if let (absoluteRect, containerSize) = self.absoluteLocation { + placeholderNode.updateAbsoluteRect(absoluteRect, within: containerSize) + } + } + } + } else { + dimensions = CGSize() + } + self.currentState = (account, stickerItem, dimensions) + self.setNeedsLayout() + } + self.isVerified = isVerified + } + + override func layout() { + super.layout() + + let bounds = self.bounds + let boundsSide = min(bounds.size.width - 14.0, bounds.size.height - 14.0) + let boundingSize = CGSize(width: boundsSide, height: boundsSide) + + if let (_, _, dimensions) = self.currentState { + let imageSize = dimensions.aspectFitted(boundingSize) + self.imageNode.frame = CGRect(origin: CGPoint(x: floor((bounds.size.width - imageSize.width) / 2.0), y: (bounds.size.height - imageSize.height) / 2.0), size: imageSize) + if let animationNode = self.animationNode { + animationNode.frame = CGRect(origin: CGPoint(x: floor((bounds.size.width - imageSize.width) / 2.0), y: (bounds.size.height - imageSize.height) / 2.0), size: imageSize) + animationNode.updateLayout(size: imageSize) + } + + if let placeholderNode = self.placeholderNode, let theme = self.theme { + placeholderNode.update(backgroundColor: theme.list.itemBlocksBackgroundColor, foregroundColor: theme.list.mediaPlaceholderColor, shimmeringColor: theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.4), shapes: [.roundedRect(rect: CGRect(origin: CGPoint(), size: imageSize), cornerRadius: 11.0)], horizontal: true, size: imageSize) + placeholderNode.frame = self.imageNode.frame + } + } + } + + func transitionNode() -> ASDisplayNode? { + return self + } + + @objc func imageNodeTap(_ recognizer: UITapGestureRecognizer) { + } + + func updatePreviewing(animated: Bool) { + var isPreviewing = false + if let (_, maybeItem, _) = self.currentState, let interaction = self.interaction, let item = maybeItem { + isPreviewing = interaction.previewedItem === item + } + if self.currentIsPreviewing != isPreviewing { + self.currentIsPreviewing = isPreviewing + + if isPreviewing { + self.layer.sublayerTransform = CATransform3DMakeScale(0.8, 0.8, 1.0) + if animated { + self.layer.animateSpring(from: 1.0 as NSNumber, to: 0.8 as NSNumber, keyPath: "sublayerTransform.scale", duration: 0.4) + } + } else { + self.layer.sublayerTransform = CATransform3DIdentity + if animated { + self.layer.animateSpring(from: 0.8 as NSNumber, to: 1.0 as NSNumber, keyPath: "sublayerTransform.scale", duration: 0.5) + } + } + } + } + + var absoluteLocation: (CGRect, CGSize)? + override func updateAbsoluteRect(_ absoluteRect: CGRect, within containerSize: CGSize) { + self.absoluteLocation = (absoluteRect, containerSize) + if let placeholderNode = self.placeholderNode { + placeholderNode.updateAbsoluteRect(absoluteRect, within: containerSize) + } + } +} + diff --git a/submodules/ImportStickerPackUI/Sources/StickerPreviewPeekContent.swift b/submodules/ImportStickerPackUI/Sources/StickerPreviewPeekContent.swift new file mode 100644 index 0000000000..b0add8eb54 --- /dev/null +++ b/submodules/ImportStickerPackUI/Sources/StickerPreviewPeekContent.swift @@ -0,0 +1,118 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import Postbox +import TelegramCore +import SyncCore +import SwiftSignalKit +import StickerResources +import AnimatedStickerNode +import TelegramAnimatedStickerNode +import ContextUI + +public final class StickerPreviewPeekContent: PeekControllerContent { + let account: Account + public let item: ImportStickerPack.Sticker + let menu: [ContextMenuItem] + + public init(account: Account, item: ImportStickerPack.Sticker, menu: [ContextMenuItem]) { + self.account = account + self.item = item + self.menu = menu + } + + public func presentation() -> PeekControllerContentPresentation { + return .freeform + } + + public func menuActivation() -> PeerControllerMenuActivation { + return .press + } + + public func menuItems() -> [ContextMenuItem] { + return self.menu + } + + public func node() -> PeekControllerContentNode & ASDisplayNode { + return StickerPreviewPeekContentNode(account: self.account, item: self.item) + } + + public func topAccessoryNode() -> ASDisplayNode? { + return nil + } + + public func isEqual(to: PeekControllerContent) -> Bool { + if let to = to as? StickerPreviewPeekContent { + return self.item === to.item + } else { + return false + } + } +} + +private final class StickerPreviewPeekContentNode: ASDisplayNode, PeekControllerContentNode { + private let account: Account + private let item: ImportStickerPack.Sticker + + private var textNode: ASTextNode + private var imageNode: ASImageNode + private var animationNode: AnimatedStickerNode? + + private var containerLayout: (ContainerViewLayout, CGFloat)? + + init(account: Account, item: ImportStickerPack.Sticker) { + self.account = account + self.item = item + + self.textNode = ASTextNode() + self.imageNode = ASImageNode() + self.imageNode.displaysAsynchronously = false + switch item.content { + case let .image(data): + self.imageNode.image = UIImage(data: data) + case .animation: + let animationNode = AnimatedStickerNode() + self.animationNode = animationNode + let dimensions = PixelDimensions(width: 512, height: 512) + let fittedDimensions = dimensions.cgSize.aspectFitted(CGSize(width: 400.0, height: 400.0)) + if let resource = item.resource { + self.animationNode?.setup(source: AnimatedStickerResourceSource(account: account, resource: resource), width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), mode: .direct(cachePathPrefix: nil)) + } + self.animationNode?.visibility = true + } + if case let .image(data) = item.content, let image = UIImage(data: data) { + self.imageNode.image = image + } + self.textNode.attributedText = NSAttributedString(string: item.emojis.joined(separator: " "), font: Font.regular(32.0), textColor: .black) + + super.init() + + self.isUserInteractionEnabled = false + + if let animationNode = self.animationNode { + self.addSubnode(animationNode) + } else { + self.addSubnode(self.imageNode) + } + + self.addSubnode(self.textNode) + } + + func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize { + let boundingSize = CGSize(width: 180.0, height: 180.0).fitted(size) + let imageFrame = CGRect(origin: CGPoint(), size: boundingSize) + + let textSpacing: CGFloat = 10.0 + let textSize = self.textNode.measure(CGSize(width: 100.0, height: 100.0)) + self.textNode.frame = CGRect(origin: CGPoint(x: floor((imageFrame.size.width - textSize.width) / 2.0), y: -textSize.height - textSpacing), size: textSize) + + self.imageNode.frame = imageFrame + + if let animationNode = self.animationNode { + animationNode.frame = imageFrame + animationNode.updateLayout(size: imageFrame.size) + } + return boundingSize + } +} diff --git a/submodules/InstantPageUI/Sources/InstantPageAnchorItem.swift b/submodules/InstantPageUI/Sources/InstantPageAnchorItem.swift index 7ff05d9c24..9dc2305790 100644 --- a/submodules/InstantPageUI/Sources/InstantPageAnchorItem.swift +++ b/submodules/InstantPageUI/Sources/InstantPageAnchorItem.swift @@ -7,6 +7,7 @@ import AsyncDisplayKit import TelegramPresentationData import TelegramUIPreferences import AccountContext +import ContextUI final class InstantPageAnchorItem: InstantPageItem { let wantsNode: Bool = false @@ -28,7 +29,7 @@ final class InstantPageAnchorItem: InstantPageItem { func drawInTile(context: CGContext) { } - 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 node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourcePeerType: MediaAutoDownloadPeerType, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, activatePinchPreview: ((PinchSourceContainerNode) -> Void)?, pinchPreviewFinished: ((InstantPageNode) -> Void)?, openPeer: @escaping (PeerId) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> InstantPageNode? { return nil } diff --git a/submodules/InstantPageUI/Sources/InstantPageArticleItem.swift b/submodules/InstantPageUI/Sources/InstantPageArticleItem.swift index efc678f687..c188042ead 100644 --- a/submodules/InstantPageUI/Sources/InstantPageArticleItem.swift +++ b/submodules/InstantPageUI/Sources/InstantPageArticleItem.swift @@ -7,6 +7,7 @@ import AsyncDisplayKit import TelegramPresentationData import TelegramUIPreferences import AccountContext +import ContextUI final class InstantPageArticleItem: InstantPageItem { var frame: CGRect @@ -35,7 +36,7 @@ final class InstantPageArticleItem: InstantPageItem { self.hasRTL = hasRTL } - 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 node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourcePeerType: MediaAutoDownloadPeerType, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, activatePinchPreview: ((PinchSourceContainerNode) -> Void)?, pinchPreviewFinished: ((InstantPageNode) -> Void)?, openPeer: @escaping (PeerId) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> InstantPageNode? { 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, openUrl: openUrl) } diff --git a/submodules/InstantPageUI/Sources/InstantPageAudioItem.swift b/submodules/InstantPageUI/Sources/InstantPageAudioItem.swift index 8516284d54..024b3148f7 100644 --- a/submodules/InstantPageUI/Sources/InstantPageAudioItem.swift +++ b/submodules/InstantPageUI/Sources/InstantPageAudioItem.swift @@ -7,6 +7,7 @@ import AsyncDisplayKit import TelegramPresentationData import TelegramUIPreferences import AccountContext +import ContextUI final class InstantPageAudioItem: InstantPageItem { var frame: CGRect @@ -24,7 +25,7 @@ final class InstantPageAudioItem: InstantPageItem { self.medias = [media] } - 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 node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourcePeerType: MediaAutoDownloadPeerType, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, activatePinchPreview: ((PinchSourceContainerNode) -> Void)?, pinchPreviewFinished: ((InstantPageNode) -> Void)?, openPeer: @escaping (PeerId) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> InstantPageNode? { return InstantPageAudioNode(context: context, strings: strings, theme: theme, webPage: self.webpage, media: self.media, openMedia: openMedia) } diff --git a/submodules/InstantPageUI/Sources/InstantPageContentNode.swift b/submodules/InstantPageUI/Sources/InstantPageContentNode.swift index be14889554..897fba5a63 100644 --- a/submodules/InstantPageUI/Sources/InstantPageContentNode.swift +++ b/submodules/InstantPageUI/Sources/InstantPageContentNode.swift @@ -193,7 +193,10 @@ final class InstantPageContentNode : ASDisplayNode { self?.openMedia(media) }, longPressMedia: { [weak self] media in self?.longPressMedia(media) - }, openPeer: { [weak self] peerId in + }, + activatePinchPreview: nil, + pinchPreviewFinished: nil, + openPeer: { [weak self] peerId in self?.openPeer(peerId) }, openUrl: { [weak self] url in self?.openUrl(url) diff --git a/submodules/InstantPageUI/Sources/InstantPageController.swift b/submodules/InstantPageUI/Sources/InstantPageController.swift index 5bbb5f9c17..a4502426d2 100644 --- a/submodules/InstantPageUI/Sources/InstantPageController.swift +++ b/submodules/InstantPageUI/Sources/InstantPageController.swift @@ -146,7 +146,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, autoNightModeTriggered: self.presentationData.autoNightModeTriggered, statusBar: self.statusBar, sourcePeerType: self.sourcePeerType, getNavigationController: { [weak self] in + self.displayNode = InstantPageControllerNode(controller: self, 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) @@ -182,6 +182,6 @@ public final class InstantPageController: 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.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition) } } diff --git a/submodules/InstantPageUI/Sources/InstantPageControllerNode.swift b/submodules/InstantPageUI/Sources/InstantPageControllerNode.swift index 8dbd7a8a65..19e09fbc4f 100644 --- a/submodules/InstantPageUI/Sources/InstantPageControllerNode.swift +++ b/submodules/InstantPageUI/Sources/InstantPageControllerNode.swift @@ -16,8 +16,10 @@ import GalleryUI import OpenInExternalAppUI import LocationUI import UndoUI +import ContextUI final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate { + private weak var controller: InstantPageController? private let context: AccountContext private var settings: InstantPagePresentationSettings? private var themeSettings: PresentationThemeSettings? @@ -89,7 +91,8 @@ 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, 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) { + init(controller: InstantPageController, 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.controller = controller self.context = context self.presentationTheme = presentationTheme self.dateTimeFormat = dateTimeFormat @@ -556,6 +559,31 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate { self?.openMedia(media) }, longPressMedia: { [weak self] media in self?.longPressMedia(media) + }, activatePinchPreview: { [weak self] sourceNode in + guard let strongSelf = self, let controller = strongSelf.controller else { + return + } + let pinchController = PinchController(sourceNode: sourceNode, getContentAreaInScreenSpace: { + guard let strongSelf = self else { + return CGRect() + } + + let localRect = CGRect(origin: CGPoint(x: 0.0, y: strongSelf.navigationBar.frame.maxY), size: CGSize(width: strongSelf.bounds.width, height: strongSelf.bounds.height - strongSelf.navigationBar.frame.maxY)) + return strongSelf.view.convert(localRect, to: nil) + }) + controller.window?.presentInGlobalOverlay(pinchController) + }, pinchPreviewFinished: { [weak self] itemNode in + guard let strongSelf = self else { + return + } + for (_, listItemNode) in strongSelf.visibleItemsWithNodes { + if let listItemNode = listItemNode as? InstantPagePeerReferenceNode { + if listItemNode.frame.intersects(itemNode.frame) && listItemNode.frame.maxY <= itemNode.frame.maxY + 2.0 { + listItemNode.layer.animateAlpha(from: 0.0, to: listItemNode.alpha, duration: 0.25) + break + } + } + } }, openPeer: { [weak self] peerId in self?.openPeer(peerId) }, openUrl: { [weak self] url in @@ -1147,7 +1175,7 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate { self.loadProgress.set(0.02) self.loadWebpageDisposable.set(nil) - self.resolveUrlDisposable.set((self.context.sharedContext.resolveUrl(account: self.context.account, url: url.url, skipUrlAuth: true) + self.resolveUrlDisposable.set((self.context.sharedContext.resolveUrl(context: self.context, peerId: nil, url: url.url, skipUrlAuth: true) |> deliverOnMainQueue).start(next: { [weak self] result in if let strongSelf = self { strongSelf.loadProgress.set(0.07) @@ -1250,7 +1278,7 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate { }, openUrl: { _ in }, openPeer: { _ in }, showAll: false) - let peer = TelegramUser(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: 1), accessHash: nil, firstName: "", lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: []) + let peer = TelegramUser(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt32Value(0)), accessHash: nil, firstName: "", lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: []) let message = Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: 0, id: 0), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 0, flags: [], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peer, text: "", attributes: [], media: [map], peers: SimpleDictionary(), associatedMessages: SimpleDictionary(), associatedMessageIds: []) let controller = LocationViewController(context: self.context, subject: message, params: controllerParams) diff --git a/submodules/InstantPageUI/Sources/InstantPageDetailsItem.swift b/submodules/InstantPageUI/Sources/InstantPageDetailsItem.swift index 4dfef89b24..681f993b8e 100644 --- a/submodules/InstantPageUI/Sources/InstantPageDetailsItem.swift +++ b/submodules/InstantPageUI/Sources/InstantPageDetailsItem.swift @@ -8,6 +8,7 @@ import Display import TelegramPresentationData import TelegramUIPreferences import AccountContext +import ContextUI final class InstantPageDetailsItem: InstantPageItem { var frame: CGRect @@ -40,7 +41,7 @@ final class InstantPageDetailsItem: InstantPageItem { self.index = index } - 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 node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourcePeerType: MediaAutoDownloadPeerType, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, activatePinchPreview: ((PinchSourceContainerNode) -> Void)?, pinchPreviewFinished: ((InstantPageNode) -> Void)?, openPeer: @escaping (PeerId) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> InstantPageNode? { var expanded: Bool? if let expandedDetails = currentExpandedDetails, let currentlyExpanded = expandedDetails[self.index] { expanded = currentlyExpanded diff --git a/submodules/InstantPageUI/Sources/InstantPageFeedbackItem.swift b/submodules/InstantPageUI/Sources/InstantPageFeedbackItem.swift index 4bcf2e92e9..6d7092f015 100644 --- a/submodules/InstantPageUI/Sources/InstantPageFeedbackItem.swift +++ b/submodules/InstantPageUI/Sources/InstantPageFeedbackItem.swift @@ -7,6 +7,7 @@ import AsyncDisplayKit import TelegramPresentationData import TelegramUIPreferences import AccountContext +import ContextUI final class InstantPageFeedbackItem: InstantPageItem { var frame: CGRect @@ -21,7 +22,7 @@ final class InstantPageFeedbackItem: InstantPageItem { self.webPage = webPage } - 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 node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourcePeerType: MediaAutoDownloadPeerType, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, activatePinchPreview: ((PinchSourceContainerNode) -> Void)?, pinchPreviewFinished: ((InstantPageNode) -> Void)?, openPeer: @escaping (PeerId) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> InstantPageNode? { 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 da2ce07137..02f155b17c 100644 --- a/submodules/InstantPageUI/Sources/InstantPageFeedbackNode.swift +++ b/submodules/InstantPageUI/Sources/InstantPageFeedbackNode.swift @@ -73,7 +73,7 @@ final class InstantPageFeedbackNode: ASDisplayNode, InstantPageNode { } @objc func buttonPressed() { - self.resolveDisposable.set((resolvePeerByName(account: self.context.account, name: "previews") |> deliverOnMainQueue).start(next: { [weak self] peerId in + self.resolveDisposable.set((self.context.engine.peers.resolvePeerByName(name: "previews") |> deliverOnMainQueue).start(next: { [weak self] peerId in if let strongSelf = self, let _ = peerId, let webPageId = strongSelf.webPage.id?.id { strongSelf.openUrl(InstantPageUrlItem(url: "https://t.me/previews?start=webpage\(webPageId)", webpageId: nil)) } diff --git a/submodules/InstantPageUI/Sources/InstantPageGalleryController.swift b/submodules/InstantPageUI/Sources/InstantPageGalleryController.swift index 8c4754b2f1..fa8909089b 100644 --- a/submodules/InstantPageUI/Sources/InstantPageGalleryController.swift +++ b/submodules/InstantPageUI/Sources/InstantPageGalleryController.swift @@ -118,7 +118,7 @@ public struct InstantPageGalleryEntry: Equatable { var representations: [TelegramMediaImageRepresentation] = [] representations.append(contentsOf: file.previewRepresentations) if let dimensions = file.dimensions { - representations.append(TelegramMediaImageRepresentation(dimensions: dimensions, resource: file.resource, progressiveSizes: [])) + representations.append(TelegramMediaImageRepresentation(dimensions: dimensions, resource: file.resource, progressiveSizes: [], immediateThumbnailData: nil)) } 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) @@ -282,9 +282,14 @@ public class InstantPageGalleryController: ViewController, StandalonePresentable openLinkOptionsImpl = { [weak self] url in if let strongSelf = self { + var presentationData = strongSelf.presentationData + if !presentationData.theme.overallDarkAppearance { + presentationData = presentationData.withUpdated(theme: defaultDarkColorPresentationTheme) + } + 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(presentationData: strongSelf.presentationData.withUpdated(theme: defaultDarkColorPresentationTheme)) + let actionSheet = ActionSheetController(presentationData: presentationData) actionSheet.setItemGroups([ActionSheetItemGroup(items: [ ActionSheetTextItem(title: url.url), ActionSheetButtonItem(title: openText, color: .accent, action: { [weak actionSheet] in @@ -457,6 +462,6 @@ public class InstantPageGalleryController: ViewController, StandalonePresentable super.containerLayoutUpdated(layout, transition: transition) self.galleryNode.frame = CGRect(origin: CGPoint(), size: layout.size) - self.galleryNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationHeight, transition: transition) + self.galleryNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition) } } diff --git a/submodules/InstantPageUI/Sources/InstantPageImageItem.swift b/submodules/InstantPageUI/Sources/InstantPageImageItem.swift index 6bed10a920..3a6d1da89a 100644 --- a/submodules/InstantPageUI/Sources/InstantPageImageItem.swift +++ b/submodules/InstantPageUI/Sources/InstantPageImageItem.swift @@ -7,6 +7,7 @@ import AsyncDisplayKit import TelegramPresentationData import TelegramUIPreferences import AccountContext +import ContextUI protocol InstantPageImageAttribute { } @@ -45,8 +46,8 @@ final class InstantPageImageItem: InstantPageItem { self.fit = fit } - 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 node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourcePeerType: MediaAutoDownloadPeerType, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, activatePinchPreview: ((PinchSourceContainerNode) -> Void)?, pinchPreviewFinished: ((InstantPageNode) -> Void)?, openPeer: @escaping (PeerId) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> InstantPageNode? { + 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, activatePinchPreview: activatePinchPreview, pinchPreviewFinished: pinchPreviewFinished) } func matchesAnchor(_ anchor: String) -> Bool { diff --git a/submodules/InstantPageUI/Sources/InstantPageImageNode.swift b/submodules/InstantPageUI/Sources/InstantPageImageNode.swift index 73e634010e..a63f6dbc28 100644 --- a/submodules/InstantPageUI/Sources/InstantPageImageNode.swift +++ b/submodules/InstantPageUI/Sources/InstantPageImageNode.swift @@ -15,6 +15,7 @@ import LocationResources import LiveLocationPositionNode import AppBundle import TelegramUIPreferences +import ContextUI private struct FetchControls { let fetch: (Bool) -> Void @@ -34,7 +35,8 @@ final class InstantPageImageNode: ASDisplayNode, InstantPageNode { private let longPressMedia: (InstantPageMedia) -> Void private var fetchControls: FetchControls? - + + private let pinchContainerNode: PinchSourceContainerNode private let imageNode: TransformImageNode private let statusNode: RadialStatusNode private let linkIconNode: ASImageNode @@ -48,7 +50,7 @@ final class InstantPageImageNode: ASDisplayNode, InstantPageNode { private var themeUpdated: Bool = false - 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) { + 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, activatePinchPreview: ((PinchSourceContainerNode) -> Void)?, pinchPreviewFinished: ((InstantPageNode) -> Void)?) { self.context = context self.theme = theme self.webPage = webPage @@ -59,15 +61,17 @@ final class InstantPageImageNode: ASDisplayNode, InstantPageNode { self.fit = fit self.openMedia = openMedia self.longPressMedia = longPressMedia - + + self.pinchContainerNode = PinchSourceContainerNode() self.imageNode = TransformImageNode() self.statusNode = RadialStatusNode(backgroundNodeColor: UIColor(white: 0.0, alpha: 0.6)) self.linkIconNode = ASImageNode() self.pinNode = ChatMessageLiveLocationPositionNode() super.init() - - self.addSubnode(self.imageNode) + + self.pinchContainerNode.contentNode.addSubnode(self.imageNode) + self.addSubnode(self.pinchContainerNode) if let image = media.media as? TelegramMediaImage, let largest = largestImageRepresentation(image.representations) { let imageReference = ImageMediaReference.webPage(webPage: WebpageReference(webPage), media: image) @@ -97,10 +101,10 @@ final class InstantPageImageNode: ASDisplayNode, InstantPageNode { if media.url != nil { self.linkIconNode.image = UIImage(bundleImageName: "Instant View/ImageLink") - self.addSubnode(self.linkIconNode) + self.pinchContainerNode.contentNode.addSubnode(self.linkIconNode) } - self.addSubnode(self.statusNode) + self.pinchContainerNode.contentNode.addSubnode(self.statusNode) } } else if let file = media.media as? TelegramMediaFile { let fileReference = FileMediaReference.webPage(webPage: WebpageReference(webPage), media: file) @@ -114,16 +118,14 @@ final class InstantPageImageNode: ASDisplayNode, InstantPageNode { } if file.isVideo { self.statusNode.transitionToState(.play(.white), animated: false, completion: {}) - self.addSubnode(self.statusNode) + self.pinchContainerNode.contentNode.addSubnode(self.statusNode) } } else if let map = media.media as? TelegramMediaMap { self.addSubnode(self.pinNode) - - var zoom: Int32 = 12 + var dimensions = CGSize(width: 200.0, height: 100.0) for attribute in self.attributes { if let mapAttribute = attribute as? InstantPageMapAttribute { - zoom = mapAttribute.zoom dimensions = mapAttribute.dimensions break } @@ -135,7 +137,19 @@ final class InstantPageImageNode: ASDisplayNode, InstantPageNode { self.imageNode.setSignal(chatMessagePhoto(postbox: context.account.postbox, photoReference: imageReference)) self.fetchedDisposable.set(chatMessagePhotoInteractiveFetched(context: context, photoReference: imageReference, displayAtSize: nil, storeToDownloadsPeerType: nil).start()) self.statusNode.transitionToState(.play(.white), animated: false, completion: {}) - self.addSubnode(self.statusNode) + self.pinchContainerNode.contentNode.addSubnode(self.statusNode) + } + + if let activatePinchPreview = activatePinchPreview { + self.pinchContainerNode.activate = { sourceNode in + activatePinchPreview(sourceNode) + } + self.pinchContainerNode.animatedOut = { [weak self] in + guard let strongSelf = self else { + return + } + pinchPreviewFinished?(strongSelf) + } } } @@ -198,7 +212,9 @@ final class InstantPageImageNode: ASDisplayNode, InstantPageNode { if self.currentSize != size || self.themeUpdated { self.currentSize = size self.themeUpdated = false - + + self.pinchContainerNode.frame = CGRect(origin: CGPoint(), size: size) + self.pinchContainerNode.update(size: size, transition: .immediate) self.imageNode.frame = CGRect(origin: CGPoint(), size: size) let radialStatusSize: CGFloat = 50.0 diff --git a/submodules/InstantPageUI/Sources/InstantPageItem.swift b/submodules/InstantPageUI/Sources/InstantPageItem.swift index c60bd39cf0..436678bf5a 100644 --- a/submodules/InstantPageUI/Sources/InstantPageItem.swift +++ b/submodules/InstantPageUI/Sources/InstantPageItem.swift @@ -7,6 +7,7 @@ import AsyncDisplayKit import TelegramPresentationData import TelegramUIPreferences import AccountContext +import ContextUI protocol InstantPageItem { var frame: CGRect { get set } @@ -16,7 +17,7 @@ protocol InstantPageItem { func matchesAnchor(_ anchor: String) -> Bool func drawInTile(context: CGContext) - 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 node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourcePeerType: MediaAutoDownloadPeerType, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, activatePinchPreview: ((PinchSourceContainerNode) -> Void)?, pinchPreviewFinished: ((InstantPageNode) -> Void)?, openPeer: @escaping (PeerId) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> InstantPageNode? func matchesNode(_ node: InstantPageNode) -> Bool func linkSelectionRects(at point: CGPoint) -> [CGRect] diff --git a/submodules/InstantPageUI/Sources/InstantPageNode.swift b/submodules/InstantPageUI/Sources/InstantPageNode.swift index 3eb643e1b4..591d8693cc 100644 --- a/submodules/InstantPageUI/Sources/InstantPageNode.swift +++ b/submodules/InstantPageUI/Sources/InstantPageNode.swift @@ -4,7 +4,7 @@ import AsyncDisplayKit import Display import TelegramPresentationData -protocol InstantPageNode { +protocol InstantPageNode: ASDisplayNode { func updateIsVisible(_ isVisible: Bool) func transitionNode(media: InstantPageMedia) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? diff --git a/submodules/InstantPageUI/Sources/InstantPagePeerReferenceItem.swift b/submodules/InstantPageUI/Sources/InstantPagePeerReferenceItem.swift index 3b5e71ccc4..86075eba4e 100644 --- a/submodules/InstantPageUI/Sources/InstantPagePeerReferenceItem.swift +++ b/submodules/InstantPageUI/Sources/InstantPagePeerReferenceItem.swift @@ -7,6 +7,7 @@ import AsyncDisplayKit import TelegramPresentationData import TelegramUIPreferences import AccountContext +import ContextUI final class InstantPagePeerReferenceItem: InstantPageItem { var frame: CGRect @@ -27,7 +28,7 @@ final class InstantPagePeerReferenceItem: InstantPageItem { self.rtl = rtl } - 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 node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourcePeerType: MediaAutoDownloadPeerType, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, activatePinchPreview: ((PinchSourceContainerNode) -> Void)?, pinchPreviewFinished: ((InstantPageNode) -> Void)?, openPeer: @escaping (PeerId) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> InstantPageNode? { 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 264b1c1117..642c6b5c51 100644 --- a/submodules/InstantPageUI/Sources/InstantPagePeerReferenceNode.swift +++ b/submodules/InstantPageUI/Sources/InstantPagePeerReferenceNode.swift @@ -147,10 +147,11 @@ final class InstantPagePeerReferenceNode: ASDisplayNode, InstantPageNode { self.joinNode.addTarget(self, action: #selector(self.joinPressed), forControlEvents: .touchUpInside) let account = self.context.account + let context = self.context let signal = actualizedPeer(postbox: account.postbox, network: account.network, peer: initialPeer) |> mapToSignal({ peer -> Signal in if let peer = peer as? TelegramChannel, let username = peer.username, peer.accessHash == nil { - return .single(peer) |> then(resolvePeerByName(account: account, name: username) + return .single(peer) |> then(context.engine.peers.resolvePeerByName(name: username) |> mapToSignal({ peerId -> Signal in if let peerId = peerId { return account.postbox.transaction({ transaction -> Peer in @@ -309,7 +310,7 @@ final class InstantPagePeerReferenceNode: ASDisplayNode, InstantPageNode { @objc func joinPressed() { if let peer = self.peer, case .notJoined = self.joinState { self.updateJoinState(.inProgress) - self.joinDisposable.set((joinChannel(account: self.context.account, peerId: peer.id, hash: nil) |> deliverOnMainQueue).start(error: { [weak self] _ in + self.joinDisposable.set((self.context.engine.peers.joinChannel(peerId: peer.id, hash: nil) |> deliverOnMainQueue).start(error: { [weak self] _ in if let strongSelf = self { if case .inProgress = strongSelf.joinState { strongSelf.updateJoinState(.notJoined) diff --git a/submodules/InstantPageUI/Sources/InstantPagePlayableVideoItem.swift b/submodules/InstantPageUI/Sources/InstantPagePlayableVideoItem.swift index d43d84cc0c..82d0b9efeb 100644 --- a/submodules/InstantPageUI/Sources/InstantPagePlayableVideoItem.swift +++ b/submodules/InstantPageUI/Sources/InstantPagePlayableVideoItem.swift @@ -7,6 +7,7 @@ import AsyncDisplayKit import TelegramPresentationData import TelegramUIPreferences import AccountContext +import ContextUI final class InstantPagePlayableVideoItem: InstantPageItem { var frame: CGRect @@ -29,7 +30,7 @@ final class InstantPagePlayableVideoItem: InstantPageItem { self.interactive = interactive } - 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 node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourcePeerType: MediaAutoDownloadPeerType, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, activatePinchPreview: ((PinchSourceContainerNode) -> Void)?, pinchPreviewFinished: ((InstantPageNode) -> Void)?, openPeer: @escaping (PeerId) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> InstantPageNode? { return InstantPagePlayableVideoNode(context: context, webPage: self.webPage, theme: theme, media: self.media, interactive: self.interactive, openMedia: openMedia) } diff --git a/submodules/InstantPageUI/Sources/InstantPageReferenceController.swift b/submodules/InstantPageUI/Sources/InstantPageReferenceController.swift index 4768118441..fbf69ad4c7 100644 --- a/submodules/InstantPageUI/Sources/InstantPageReferenceController.swift +++ b/submodules/InstantPageUI/Sources/InstantPageReferenceController.swift @@ -74,6 +74,6 @@ final class InstantPageReferenceController: 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.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition) } } diff --git a/submodules/InstantPageUI/Sources/InstantPageShapeItem.swift b/submodules/InstantPageUI/Sources/InstantPageShapeItem.swift index 3d22f56dd6..fe506f8626 100644 --- a/submodules/InstantPageUI/Sources/InstantPageShapeItem.swift +++ b/submodules/InstantPageUI/Sources/InstantPageShapeItem.swift @@ -7,6 +7,7 @@ import AsyncDisplayKit import TelegramPresentationData import TelegramUIPreferences import AccountContext +import ContextUI enum InstantPageShape { case rect @@ -62,7 +63,7 @@ final class InstantPageShapeItem: InstantPageItem { return false } - 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 node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourcePeerType: MediaAutoDownloadPeerType, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, activatePinchPreview: ((PinchSourceContainerNode) -> Void)?, pinchPreviewFinished: ((InstantPageNode) -> Void)?, openPeer: @escaping (PeerId) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> InstantPageNode? { return nil } diff --git a/submodules/InstantPageUI/Sources/InstantPageSlideshowItem.swift b/submodules/InstantPageUI/Sources/InstantPageSlideshowItem.swift index 9ce77fc9a6..cf0f8a9d49 100644 --- a/submodules/InstantPageUI/Sources/InstantPageSlideshowItem.swift +++ b/submodules/InstantPageUI/Sources/InstantPageSlideshowItem.swift @@ -7,6 +7,7 @@ import AsyncDisplayKit import TelegramPresentationData import TelegramUIPreferences import AccountContext +import ContextUI final class InstantPageSlideshowItem: InstantPageItem { var frame: CGRect @@ -21,7 +22,7 @@ final class InstantPageSlideshowItem: InstantPageItem { self.medias = medias } - 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 node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourcePeerType: MediaAutoDownloadPeerType, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, activatePinchPreview: ((PinchSourceContainerNode) -> Void)?, pinchPreviewFinished: ((InstantPageNode) -> Void)?, openPeer: @escaping (PeerId) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> InstantPageNode? { return InstantPageSlideshowNode(context: context, sourcePeerType: sourcePeerType, theme: theme, webPage: webPage, medias: self.medias, openMedia: openMedia, longPressMedia: longPressMedia) } diff --git a/submodules/InstantPageUI/Sources/InstantPageSlideshowItemNode.swift b/submodules/InstantPageUI/Sources/InstantPageSlideshowItemNode.swift index d545caade5..624bf4501f 100644 --- a/submodules/InstantPageUI/Sources/InstantPageSlideshowItemNode.swift +++ b/submodules/InstantPageUI/Sources/InstantPageSlideshowItemNode.swift @@ -183,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, sourcePeerType: self.sourcePeerType, 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, activatePinchPreview: nil, pinchPreviewFinished: nil) } else if let file = media.media as? TelegramMediaFile { contentNode = ASDisplayNode() } else { diff --git a/submodules/InstantPageUI/Sources/InstantPageSubContentNode.swift b/submodules/InstantPageUI/Sources/InstantPageSubContentNode.swift new file mode 100644 index 0000000000..1b48fd5f8c --- /dev/null +++ b/submodules/InstantPageUI/Sources/InstantPageSubContentNode.swift @@ -0,0 +1,447 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display +import Postbox +import TelegramCore +import SyncCore +import SwiftSignalKit +import TelegramPresentationData +import TelegramUIPreferences +import AccountContext + +final class InstantPageSubContentNode : 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 + private let longPressMedia: (InstantPageMedia) -> Void + private let openPeer: (PeerId) -> Void + private let openUrl: (InstantPageUrlItem) -> Void + + var currentLayoutTiles: [InstantPageTile] = [] + var currentLayoutItemsWithNodes: [InstantPageItem] = [] + var distanceThresholdGroupCount: [Int: Int] = [:] + + var visibleTiles: [Int: InstantPageTileNode] = [:] + var visibleItemsWithNodes: [Int: InstantPageNode] = [:] + + var currentWebEmbedHeights: [Int : CGFloat] = [:] + var currentExpandedDetails: [Int : Bool]? + var currentDetailsItems: [InstantPageDetailsItem] = [] + + var requestLayoutUpdate: ((Bool) -> Void)? + + var currentLayout: InstantPageLayout + let contentSize: CGSize + let inOverlayPanel: Bool + + private var previousVisibleBounds: CGRect? + + 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 + self.longPressMedia = longPressMedia + self.openPeer = openPeer + self.openUrl = openUrl + + self.currentLayout = InstantPageLayout(origin: CGPoint(), contentSize: contentSize, items: items) + self.contentSize = contentSize + self.inOverlayPanel = inOverlayPanel + + super.init() + + self.updateLayout() + } + + private func updateLayout() { + for (_, tileNode) in self.visibleTiles { + tileNode.removeFromSupernode() + } + self.visibleTiles.removeAll() + + let currentLayoutTiles = instantPageTilesFromLayout(currentLayout, boundingWidth: contentSize.width) + + var currentDetailsItems: [InstantPageDetailsItem] = [] + var currentLayoutItemsWithViews: [InstantPageItem] = [] + var distanceThresholdGroupCount: [Int: Int] = [:] + + var expandedDetails: [Int: Bool] = [:] + + var detailsIndex = -1 + for item in self.currentLayout.items { + if item.wantsNode { + currentLayoutItemsWithViews.append(item) + if let group = item.distanceThresholdGroup() { + let count: Int + if let currentCount = distanceThresholdGroupCount[Int(group)] { + count = currentCount + } else { + count = 0 + } + distanceThresholdGroupCount[Int(group)] = count + 1 + } + if let detailsItem = item as? InstantPageDetailsItem { + detailsIndex += 1 + expandedDetails[detailsIndex] = detailsItem.initiallyExpanded + currentDetailsItems.append(detailsItem) + } + } + } + + if self.currentExpandedDetails == nil { + self.currentExpandedDetails = expandedDetails + } + + self.currentLayoutTiles = currentLayoutTiles + self.currentLayoutItemsWithNodes = currentLayoutItemsWithViews + self.currentDetailsItems = currentDetailsItems + self.distanceThresholdGroupCount = distanceThresholdGroupCount + } + + var effectiveContentSize: CGSize { + var contentSize = self.contentSize + for item in self.currentDetailsItems { + let expanded = self.currentExpandedDetails?[item.index] ?? item.initiallyExpanded + contentSize.height += -item.frame.height + (expanded ? self.effectiveSizeForDetails(item).height : item.titleHeight) + } + return contentSize + } + + func updateVisibleItems(visibleBounds: CGRect, animated: Bool = false) { + var visibleTileIndices = Set() + var visibleItemIndices = Set() + + self.previousVisibleBounds = visibleBounds + + var topNode: ASDisplayNode? + let topTileNode = topNode + if let scrollSubnodes = self.subnodes { + for node in scrollSubnodes.reversed() { + if let node = node as? InstantPageTileNode { + topNode = node + break + } + } + } + + var collapseOffset: CGFloat = 0.0 + let transition: ContainedViewLayoutTransition + if animated { + transition = .animated(duration: 0.3, curve: .spring) + } else { + transition = .immediate + } + + var itemIndex = -1 + var embedIndex = -1 + var detailsIndex = -1 + + for item in self.currentLayoutItemsWithNodes { + itemIndex += 1 + if item is InstantPageWebEmbedItem { + embedIndex += 1 + } + if item is InstantPageDetailsItem { + detailsIndex += 1 + } + + var itemThreshold: CGFloat = 0.0 + if let group = item.distanceThresholdGroup() { + var count: Int = 0 + if let currentCount = self.distanceThresholdGroupCount[group] { + count = currentCount + } + itemThreshold = item.distanceThresholdWithGroupCount(count) + } + + var itemFrame = item.frame.offsetBy(dx: 0.0, dy: -collapseOffset) + var thresholdedItemFrame = itemFrame + thresholdedItemFrame.origin.y -= itemThreshold + thresholdedItemFrame.size.height += itemThreshold * 2.0 + + if let detailsItem = item as? InstantPageDetailsItem, let expanded = self.currentExpandedDetails?[detailsIndex] { + let height = expanded ? self.effectiveSizeForDetails(detailsItem).height : detailsItem.titleHeight + collapseOffset += itemFrame.height - height + itemFrame = CGRect(origin: itemFrame.origin, size: CGSize(width: itemFrame.width, height: height)) + } + + if visibleBounds.intersects(thresholdedItemFrame) { + visibleItemIndices.insert(itemIndex) + + var itemNode = self.visibleItemsWithNodes[itemIndex] + if let currentItemNode = itemNode { + if !item.matchesNode(currentItemNode) { + (currentItemNode as! ASDisplayNode).removeFromSupernode() + self.visibleItemsWithNodes.removeValue(forKey: itemIndex) + itemNode = nil + } + } + + 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, sourcePeerType: self.sourcePeerType, openMedia: { [weak self] media in + self?.openMedia(media) + }, longPressMedia: { [weak self] media in + self?.longPressMedia(media) + }, activatePinchPreview: nil, pinchPreviewFinished: nil, openPeer: { [weak self] peerId in + self?.openPeer(peerId) + }, openUrl: { [weak self] url in + self?.openUrl(url) + }, updateWebEmbedHeight: { _ in + }, updateDetailsExpanded: { [weak self] expanded in + self?.updateDetailsExpanded(detailsIndex, expanded) + }, currentExpandedDetails: self.currentExpandedDetails) { + newNode.frame = itemFrame + newNode.updateLayout(size: itemFrame.size, transition: transition) + if let topNode = topNode { + self.insertSubnode(newNode, aboveSubnode: topNode) + } else { + self.insertSubnode(newNode, at: 0) + } + topNode = newNode + self.visibleItemsWithNodes[itemIndex] = newNode + itemNode = newNode + + if let itemNode = itemNode as? InstantPageDetailsNode { + itemNode.requestLayoutUpdate = { [weak self] animated in + self?.requestLayoutUpdate?(animated) + } + } + } + } else { + if (itemNode as! ASDisplayNode).frame != itemFrame { + transition.updateFrame(node: (itemNode as! ASDisplayNode), frame: itemFrame) + itemNode?.updateLayout(size: itemFrame.size, transition: transition) + } + } + + if let itemNode = itemNode as? InstantPageDetailsNode { + itemNode.updateVisibleItems(visibleBounds: visibleBounds.offsetBy(dx: -itemNode.frame.minX, dy: -itemNode.frame.minY), animated: animated) + } + } + } + + topNode = topTileNode + + var tileIndex = -1 + for tile in self.currentLayoutTiles { + tileIndex += 1 + + let tileFrame = effectiveFrameForTile(tile) + var tileVisibleFrame = tileFrame + tileVisibleFrame.origin.y -= 400.0 + tileVisibleFrame.size.height += 400.0 * 2.0 + if tileVisibleFrame.intersects(visibleBounds) { + visibleTileIndices.insert(tileIndex) + + if self.visibleTiles[tileIndex] == nil { + let tileNode = InstantPageTileNode(tile: tile, backgroundColor: self.inOverlayPanel ? self.theme.overlayPanelColor : self.theme.pageBackgroundColor) + tileNode.frame = tileFrame + if let topNode = topNode { + self.insertSubnode(tileNode, aboveSubnode: topNode) + } else { + self.insertSubnode(tileNode, at: 0) + } + topNode = tileNode + self.visibleTiles[tileIndex] = tileNode + } else { + if visibleTiles[tileIndex]!.frame != tileFrame { + transition.updateFrame(node: self.visibleTiles[tileIndex]!, frame: tileFrame) + } + } + } + } + + var removeTileIndices: [Int] = [] + for (index, tileNode) in self.visibleTiles { + if !visibleTileIndices.contains(index) { + removeTileIndices.append(index) + tileNode.removeFromSupernode() + } + } + for index in removeTileIndices { + self.visibleTiles.removeValue(forKey: index) + } + + var removeItemIndices: [Int] = [] + for (index, itemNode) in self.visibleItemsWithNodes { + if !visibleItemIndices.contains(index) { + removeItemIndices.append(index) + (itemNode as! ASDisplayNode).removeFromSupernode() + } else { + var itemFrame = (itemNode as! ASDisplayNode).frame + let itemThreshold: CGFloat = 200.0 + itemFrame.origin.y -= itemThreshold + itemFrame.size.height += itemThreshold * 2.0 + itemNode.updateIsVisible(visibleBounds.intersects(itemFrame)) + } + } + for index in removeItemIndices { + self.visibleItemsWithNodes.removeValue(forKey: index) + } + } + + private func updateWebEmbedHeight(_ index: Int, _ height: CGFloat) { + // let currentHeight = self.currentWebEmbedHeights[index] + // if height != currentHeight { + // if let currentHeight = currentHeight, currentHeight > height { + // return + // } + // self.currentWebEmbedHeights[index] = height + // + // let signal: Signal = (.complete() |> delay(0.08, queue: Queue.mainQueue())) + // self.updateLayoutDisposable.set(signal.start(completed: { [weak self] in + // if let strongSelf = self { + // strongSelf.updateLayout() + // strongSelf.updateVisibleItems() + // } + // })) + // } + } + + func updateDetailsExpanded(_ index: Int, _ expanded: Bool, animated: Bool = true, requestLayout: Bool = true) { + if var currentExpandedDetails = self.currentExpandedDetails { + currentExpandedDetails[index] = expanded + self.currentExpandedDetails = currentExpandedDetails + } + self.requestLayoutUpdate?(animated) + } + + func transitionNode(media: InstantPageMedia) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? { + for (_, itemNode) in self.visibleItemsWithNodes { + if let transitionNode = itemNode.transitionNode(media: media) { + return transitionNode + } + } + return nil + } + + func updateHiddenMedia(media: InstantPageMedia?) { + for (_, itemNode) in self.visibleItemsWithNodes { + itemNode.updateHiddenMedia(media: media) + } + } + + func scrollableContentOffset(item: InstantPageScrollableItem) -> CGPoint { + var contentOffset = CGPoint() + for (_, itemNode) in self.visibleItemsWithNodes { + if let itemNode = itemNode as? InstantPageScrollableNode, itemNode.item === item { + contentOffset = itemNode.contentOffset + break + } + } + return contentOffset + } + + func nodeForDetailsItem(_ item: InstantPageDetailsItem) -> InstantPageDetailsNode? { + for (_, itemNode) in self.visibleItemsWithNodes { + if let detailsNode = itemNode as? InstantPageDetailsNode, detailsNode.item === item { + return detailsNode + } + } + return nil + } + + private func effectiveSizeForDetails(_ item: InstantPageDetailsItem) -> CGSize { + if let node = nodeForDetailsItem(item) { + return CGSize(width: item.frame.width, height: node.effectiveContentSize.height + item.titleHeight) + } else { + return item.frame.size + } + } + + private func effectiveFrameForTile(_ tile: InstantPageTile) -> CGRect { + let layoutOrigin = tile.frame.origin + var origin = layoutOrigin + for item in self.currentDetailsItems { + let expanded = self.currentExpandedDetails?[item.index] ?? item.initiallyExpanded + if layoutOrigin.y >= item.frame.maxY { + let height = expanded ? self.effectiveSizeForDetails(item).height : item.titleHeight + origin.y += height - item.frame.height + } + } + return CGRect(origin: origin, size: tile.frame.size) + } + + func effectiveFrameForItem(_ item: InstantPageItem) -> CGRect { + let layoutOrigin = item.frame.origin + var origin = layoutOrigin + + for item in self.currentDetailsItems { + let expanded = self.currentExpandedDetails?[item.index] ?? item.initiallyExpanded + if layoutOrigin.y >= item.frame.maxY { + let height = expanded ? self.effectiveSizeForDetails(item).height : item.titleHeight + origin.y += height - item.frame.height + } + } + + if let item = item as? InstantPageDetailsItem { + let expanded = self.currentExpandedDetails?[item.index] ?? item.initiallyExpanded + let height = expanded ? self.effectiveSizeForDetails(item).height : item.titleHeight + return CGRect(origin: origin, size: CGSize(width: item.frame.width, height: height)) + } else { + return CGRect(origin: origin, size: item.frame.size) + } + } + + func textItemAtLocation(_ location: CGPoint) -> (InstantPageTextItem, CGPoint)? { + for item in self.currentLayout.items { + let itemFrame = self.effectiveFrameForItem(item) + if itemFrame.contains(location) { + if let item = item as? InstantPageTextItem, item.selectable { + return (item, CGPoint(x: itemFrame.minX - item.frame.minX, y: itemFrame.minY - item.frame.minY)) + } else if let item = item as? InstantPageScrollableItem { + let contentOffset = scrollableContentOffset(item: item) + if let (textItem, parentOffset) = item.textItemAtLocation(location.offsetBy(dx: -itemFrame.minX + contentOffset.x, dy: -itemFrame.minY)) { + return (textItem, itemFrame.origin.offsetBy(dx: parentOffset.x - contentOffset.x, dy: parentOffset.y)) + } + } else if let item = item as? InstantPageDetailsItem { + for (_, itemNode) in self.visibleItemsWithNodes { + if let itemNode = itemNode as? InstantPageDetailsNode, itemNode.item === item { + if let (textItem, parentOffset) = itemNode.textItemAtLocation(location.offsetBy(dx: -itemFrame.minX, dy: -itemFrame.minY)) { + return (textItem, itemFrame.origin.offsetBy(dx: parentOffset.x, dy: parentOffset.y)) + } + } + } + } + } + } + return nil + } + + + func tapActionAtPoint(_ point: CGPoint) -> TapLongTapOrDoubleTapGestureRecognizerAction { + for item in self.currentLayout.items { + let frame = self.effectiveFrameForItem(item) + if frame.contains(point) { + if item is InstantPagePeerReferenceItem { + return .fail + } else if item is InstantPageAudioItem { + return .fail + } else if item is InstantPageArticleItem { + return .fail + } else if item is InstantPageFeedbackItem { + return .fail + } else if let item = item as? InstantPageDetailsItem { + for (_, itemNode) in self.visibleItemsWithNodes { + if let itemNode = itemNode as? InstantPageDetailsNode, itemNode.item === item { + return itemNode.tapActionAtPoint(point.offsetBy(dx: -itemNode.frame.minX, dy: -itemNode.frame.minY)) + } + } + } + break + } + } + return .waitForSingleTap + } +} diff --git a/submodules/InstantPageUI/Sources/InstantPageTableItem.swift b/submodules/InstantPageUI/Sources/InstantPageTableItem.swift index b496136023..8dd07125ee 100644 --- a/submodules/InstantPageUI/Sources/InstantPageTableItem.swift +++ b/submodules/InstantPageUI/Sources/InstantPageTableItem.swift @@ -8,6 +8,7 @@ import Display import TelegramPresentationData import TelegramUIPreferences import AccountContext +import ContextUI private struct TableSide: OptionSet { var rawValue: Int32 = 0 @@ -200,12 +201,12 @@ final class InstantPageTableItem: InstantPageScrollableItem { return false } - 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 node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourcePeerType: MediaAutoDownloadPeerType, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, activatePinchPreview: ((PinchSourceContainerNode) -> Void)?, pinchPreviewFinished: ((InstantPageNode) -> Void)?, openPeer: @escaping (PeerId) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> InstantPageNode? { 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, sourcePeerType: sourcePeerType, 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 }, activatePinchPreview: nil, pinchPreviewFinished: nil, 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 c9483f1fba..60e725ca0f 100644 --- a/submodules/InstantPageUI/Sources/InstantPageTextItem.swift +++ b/submodules/InstantPageUI/Sources/InstantPageTextItem.swift @@ -9,6 +9,7 @@ import TelegramPresentationData import TelegramUIPreferences import TextFormat import AccountContext +import ContextUI public final class InstantPageUrlItem: Equatable { public let url: String @@ -436,7 +437,7 @@ final class InstantPageTextItem: InstantPageItem { return false } - 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 node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourcePeerType: MediaAutoDownloadPeerType, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, activatePinchPreview: ((PinchSourceContainerNode) -> Void)?, pinchPreviewFinished: ((InstantPageNode) -> Void)?, openPeer: @escaping (PeerId) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> InstantPageNode? { return nil } @@ -485,11 +486,11 @@ final class InstantPageScrollableTextItem: InstantPageScrollableItem { context.restoreGState() } - 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 node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourcePeerType: MediaAutoDownloadPeerType, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, activatePinchPreview: ((PinchSourceContainerNode) -> Void)?, pinchPreviewFinished: ((InstantPageNode) -> Void)?, openPeer: @escaping (PeerId) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> InstantPageNode? { var additionalNodes: [InstantPageNode] = [] for item in additionalItems { if item.wantsNode { - 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) { + if let node = item.node(context: context, strings: strings, nameDisplayOrder: nameDisplayOrder, theme: theme, sourcePeerType: sourcePeerType, openMedia: { _ in }, longPressMedia: { _ in }, activatePinchPreview: nil, pinchPreviewFinished: nil, openPeer: { _ in }, openUrl: { _ in}, updateWebEmbedHeight: { _ in }, updateDetailsExpanded: { _ in }, currentExpandedDetails: nil) { node.frame = item.frame additionalNodes.append(node) } diff --git a/submodules/InstantPageUI/Sources/InstantPageWebEmbedItem.swift b/submodules/InstantPageUI/Sources/InstantPageWebEmbedItem.swift index f432f660ba..14f06fad3f 100644 --- a/submodules/InstantPageUI/Sources/InstantPageWebEmbedItem.swift +++ b/submodules/InstantPageUI/Sources/InstantPageWebEmbedItem.swift @@ -7,6 +7,7 @@ import AsyncDisplayKit import TelegramPresentationData import TelegramUIPreferences import AccountContext +import ContextUI final class InstantPageWebEmbedItem: InstantPageItem { var frame: CGRect @@ -25,7 +26,7 @@ final class InstantPageWebEmbedItem: InstantPageItem { self.enableScrolling = enableScrolling } - 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 node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourcePeerType: MediaAutoDownloadPeerType, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, activatePinchPreview: ((PinchSourceContainerNode) -> Void)?, pinchPreviewFinished: ((InstantPageNode) -> Void)?, openPeer: @escaping (PeerId) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> InstantPageNode? { return InstantPageWebEmbedNode(frame: self.frame, url: self.url, html: self.html, enableScrolling: self.enableScrolling, updateWebEmbedHeight: updateWebEmbedHeight) } diff --git a/submodules/InviteLinksUI/Sources/InviteLinkEditController.swift b/submodules/InviteLinksUI/Sources/InviteLinkEditController.swift index 08e410893a..2502a6a12c 100644 --- a/submodules/InviteLinksUI/Sources/InviteLinkEditController.swift +++ b/submodules/InviteLinksUI/Sources/InviteLinkEditController.swift @@ -387,7 +387,7 @@ public func inviteLinkEditController(context: AccountContext, peerId: PeerId, in dismissAction() dismissImpl?() - let _ = (revokePeerExportedInvitation(account: context.account, peerId: peerId, link: invite.link) + let _ = (context.engine.peers.revokePeerExportedInvitation(peerId: peerId, link: invite.link) |> timeout(10, queue: Queue.mainQueue(), alternate: .fail(.generic)) |> deliverOnMainQueue).start(next: { invite in switch invite { @@ -444,7 +444,7 @@ public func inviteLinkEditController(context: AccountContext, peerId: PeerId, in let usageLimit = state.usage.value if invite == nil { - let _ = (createPeerExportedInvitation(account: context.account, peerId: peerId, expireDate: expireDate, usageLimit: usageLimit) + let _ = (context.engine.peers.createPeerExportedInvitation(peerId: peerId, expireDate: expireDate, usageLimit: usageLimit) |> timeout(10, queue: Queue.mainQueue(), alternate: .fail(.generic)) |> deliverOnMainQueue).start(next: { invite in completion?(invite) @@ -458,7 +458,7 @@ public func inviteLinkEditController(context: AccountContext, peerId: PeerId, in presentControllerImpl?(textAlertController(context: context, title: nil, text: presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil) }) } else if let invite = invite { - let _ = (editPeerExportedInvitation(account: context.account, peerId: peerId, link: invite.link, expireDate: expireDate, usageLimit: usageLimit) + let _ = (context.engine.peers.editPeerExportedInvitation(peerId: peerId, link: invite.link, expireDate: expireDate, usageLimit: usageLimit) |> timeout(10, queue: Queue.mainQueue(), alternate: .fail(.generic)) |> deliverOnMainQueue).start(next: { invite in completion?(invite) diff --git a/submodules/InviteLinksUI/Sources/InviteLinkInviteController.swift b/submodules/InviteLinksUI/Sources/InviteLinkInviteController.swift index 68d2764e4b..02985f64fe 100644 --- a/submodules/InviteLinksUI/Sources/InviteLinkInviteController.swift +++ b/submodules/InviteLinksUI/Sources/InviteLinkInviteController.swift @@ -285,7 +285,7 @@ public final class InviteLinkInviteController: ViewController { self.presentationDataPromise = Promise(self.presentationData) self.controller = controller - self.invitesContext = PeerExportedInvitationsContext(account: context.account, peerId: peerId, adminId: nil, revoked: false, forceUpdate: false) + self.invitesContext = context.engine.peers.peerExportedInvitations(peerId: peerId, adminId: nil, revoked: false, forceUpdate: false) self.dimNode = ASDisplayNode() self.dimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.5) @@ -396,7 +396,7 @@ public final class InviteLinkInviteController: ViewController { dismissAction() if let invite = invite { - let _ = (revokePeerExportedInvitation(account: context.account, peerId: peerId, link: invite.link) |> deliverOnMainQueue).start(next: { result in + let _ = (context.engine.peers.revokePeerExportedInvitation(peerId: peerId, link: invite.link) |> deliverOnMainQueue).start(next: { result in if let result = result, case let .replace(_, invite) = result { mainInvitePromise.set(invite) } diff --git a/submodules/InviteLinksUI/Sources/InviteLinkListController.swift b/submodules/InviteLinksUI/Sources/InviteLinkListController.swift index 9314f186b7..973d154cd1 100644 --- a/submodules/InviteLinksUI/Sources/InviteLinkListController.swift +++ b/submodules/InviteLinksUI/Sources/InviteLinkListController.swift @@ -418,12 +418,12 @@ public func inviteLinkListController(context: AccountContext, peerId: PeerId, ad var getControllerImpl: (() -> ViewController?)? let adminId = admin?.peer.peer?.id - let invitesContext = PeerExportedInvitationsContext(account: context.account, peerId: peerId, adminId: adminId, revoked: false, forceUpdate: true) - let revokedInvitesContext = PeerExportedInvitationsContext(account: context.account, peerId: peerId, adminId: adminId, revoked: true, forceUpdate: true) + let invitesContext = context.engine.peers.peerExportedInvitations(peerId: peerId, adminId: adminId, revoked: false, forceUpdate: true) + let revokedInvitesContext = context.engine.peers.peerExportedInvitations(peerId: peerId, adminId: adminId, revoked: true, forceUpdate: true) let creators: Signal<[ExportedInvitationCreator], NoError> if adminId == nil { - creators = .single([]) |> then(peerExportedInvitationsCreators(account: context.account, peerId: peerId)) + creators = .single([]) |> then(context.engine.peers.peerExportedInvitationsCreators(peerId: peerId)) } else { creators = .single([]) } @@ -520,7 +520,7 @@ public func inviteLinkListController(context: AccountContext, peerId: PeerId, ad } } if revoke { - revokeLinkDisposable.set((revokePeerExportedInvitation(account: context.account, peerId: peerId, link: invite.link) |> deliverOnMainQueue).start(next: { result in + revokeLinkDisposable.set((context.engine.peers.revokePeerExportedInvitation(peerId: peerId, link: invite.link) |> deliverOnMainQueue).start(next: { result in updateState { state in var updatedState = state updatedState.revokingPrivateLink = false @@ -661,7 +661,7 @@ public func inviteLinkListController(context: AccountContext, peerId: PeerId, ad ActionSheetButtonItem(title: presentationData.strings.InviteLink_DeleteLinkAlert_Action, color: .destructive, action: { dismissAction() - revokeLinkDisposable.set((deletePeerExportedInvitation(account: context.account, peerId: peerId, link: invite.link) |> deliverOnMainQueue).start(completed: { + revokeLinkDisposable.set((context.engine.peers.deletePeerExportedInvitation(peerId: peerId, link: invite.link) |> deliverOnMainQueue).start(completed: { })) revokedInvitesContext.remove(invite) @@ -695,7 +695,7 @@ public func inviteLinkListController(context: AccountContext, peerId: PeerId, ad ActionSheetButtonItem(title: presentationData.strings.GroupInfo_InviteLink_RevokeLink, color: .destructive, action: { dismissAction() - revokeLinkDisposable.set((revokePeerExportedInvitation(account: context.account, peerId: peerId, link: invite.link) |> deliverOnMainQueue).start(next: { result in + revokeLinkDisposable.set((context.engine.peers.revokePeerExportedInvitation(peerId: peerId, link: invite.link) |> deliverOnMainQueue).start(next: { result in if case let .replace(_, newInvite) = result { invitesContext.add(newInvite) } @@ -732,7 +732,7 @@ public func inviteLinkListController(context: AccountContext, peerId: PeerId, ad ActionSheetButtonItem(title: presentationData.strings.InviteLink_DeleteAllRevokedLinksAlert_Action, color: .destructive, action: { dismissAction() - deleteAllRevokedLinksDisposable.set((deleteAllRevokedPeerExportedInvitations(account: context.account, peerId: peerId, adminId: adminId ?? context.account.peerId) |> deliverOnMainQueue).start(completed: { + deleteAllRevokedLinksDisposable.set((context.engine.peers.deleteAllRevokedPeerExportedInvitations(peerId: peerId, adminId: adminId ?? context.account.peerId) |> deliverOnMainQueue).start(completed: { })) revokedInvitesContext.clear() @@ -770,7 +770,7 @@ public func inviteLinkListController(context: AccountContext, peerId: PeerId, ad |> distinctUntilChanged |> deliverOnMainQueue |> map { invite -> PeerInvitationImportersContext? in - return invite.flatMap { PeerInvitationImportersContext(account: context.account, peerId: peerId, invite: $0) } + return invite.flatMap { context.engine.peers.peerInvitationImporters(peerId: peerId, invite: $0) } } |> afterNext { context in if let context = context { importersState.set(context.state |> map(Optional.init)) diff --git a/submodules/InviteLinksUI/Sources/InviteLinkQRCodeController.swift b/submodules/InviteLinksUI/Sources/InviteLinkQRCodeController.swift index aec9ecee79..a13cdeb9a7 100644 --- a/submodules/InviteLinksUI/Sources/InviteLinkQRCodeController.swift +++ b/submodules/InviteLinksUI/Sources/InviteLinkQRCodeController.swift @@ -143,7 +143,7 @@ public final class InviteLinkQRCodeController: 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.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition) } class Node: ViewControllerTracingNode, UIScrollViewDelegate { diff --git a/submodules/InviteLinksUI/Sources/InviteLinkViewController.swift b/submodules/InviteLinksUI/Sources/InviteLinkViewController.swift index a6483cda91..db9cef882d 100644 --- a/submodules/InviteLinksUI/Sources/InviteLinkViewController.swift +++ b/submodules/InviteLinksUI/Sources/InviteLinkViewController.swift @@ -374,7 +374,7 @@ public final class InviteLinkViewController: ViewController { self.presentationDataPromise = Promise(self.presentationData) self.controller = controller - self.importersContext = importersContext ?? PeerInvitationImportersContext(account: context.account, peerId: peerId, invite: invite) + self.importersContext = importersContext ?? context.engine.peers.peerInvitationImporters(peerId: peerId, invite: invite) self.dimNode = ASDisplayNode() self.dimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.5) @@ -483,7 +483,7 @@ public final class InviteLinkViewController: ViewController { dismissAction() self?.controller?.dismiss() - let _ = (deletePeerExportedInvitation(account: context.account, peerId: peerId, link: invite.link) |> deliverOnMainQueue).start(completed: { + let _ = (context.engine.peers.deletePeerExportedInvitation(peerId: peerId, link: invite.link) |> deliverOnMainQueue).start(completed: { }) self?.controller?.revokedInvitationsContext?.remove(invite) @@ -537,7 +537,7 @@ public final class InviteLinkViewController: ViewController { dismissAction() self?.controller?.dismiss() - let _ = (revokePeerExportedInvitation(account: context.account, peerId: peerId, link: invite.link) |> deliverOnMainQueue).start(next: { result in + let _ = (context.engine.peers.revokePeerExportedInvitation(peerId: peerId, link: invite.link) |> deliverOnMainQueue).start(next: { result in if case let .replace(_, newInvite) = result { self?.controller?.invitationsContext?.add(newInvite) } @@ -598,7 +598,7 @@ public final class InviteLinkViewController: ViewController { if state.importers.isEmpty && state.isLoadingMore { count = min(4, state.count) loading = true - let fakeUser = TelegramUser(id: PeerId(namespace: -1, id: 0), accessHash: nil, firstName: "", lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: []) + let fakeUser = TelegramUser(id: PeerId(namespace: .max, id: PeerId.Id._internalFromInt32Value(0)), accessHash: nil, firstName: "", lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: []) for i in 0 ..< count { entries.append(.importer(Int32(i), presentationData.theme, presentationData.dateTimeFormat, fakeUser, 0, true)) } @@ -839,7 +839,7 @@ public final class InviteLinkViewController: ViewController { } else { let elapsedTime = expireDate - currentTime if elapsedTime >= 86400 { - subtitleText = self.presentationData.strings.InviteLink_ExpiresIn(timeIntervalString(strings: self.presentationData.strings, value: elapsedTime)).0 + subtitleText = self.presentationData.strings.InviteLink_ExpiresIn(scheduledTimeIntervalString(strings: self.presentationData.strings, value: elapsedTime)).0 } else { subtitleText = self.presentationData.strings.InviteLink_ExpiresIn(textForTimeout(value: elapsedTime)).0 if self.countdownTimer == nil { diff --git a/submodules/InviteLinksUI/Sources/ItemListInviteLinkItem.swift b/submodules/InviteLinksUI/Sources/ItemListInviteLinkItem.swift index be3a22af2b..ee217b013e 100644 --- a/submodules/InviteLinksUI/Sources/ItemListInviteLinkItem.swift +++ b/submodules/InviteLinksUI/Sources/ItemListInviteLinkItem.swift @@ -392,7 +392,7 @@ public class ItemListInviteLinkItemNode: ListViewItemNode, ItemListItemNode { } let elapsedTime = expireDate - currentTime if elapsedTime >= 86400 { - subtitleText += item.presentationData.strings.InviteLink_ExpiresIn(timeIntervalString(strings: item.presentationData.strings, value: elapsedTime)).0 + subtitleText += item.presentationData.strings.InviteLink_ExpiresIn(scheduledTimeIntervalString(strings: item.presentationData.strings, value: elapsedTime)).0 } else { subtitleText += item.presentationData.strings.InviteLink_ExpiresIn(textForTimeout(value: elapsedTime)).0 } diff --git a/submodules/ItemListAvatarAndNameInfoItem/Sources/ItemListAvatarAndNameItem.swift b/submodules/ItemListAvatarAndNameInfoItem/Sources/ItemListAvatarAndNameItem.swift index 5393572d5c..f924c08da9 100644 --- a/submodules/ItemListAvatarAndNameInfoItem/Sources/ItemListAvatarAndNameItem.swift +++ b/submodules/ItemListAvatarAndNameInfoItem/Sources/ItemListAvatarAndNameItem.swift @@ -531,7 +531,7 @@ public class ItemListAvatarAndNameInfoItemNode: ListViewItemNode, ItemListItemNo } var updateAvatarOverlayImage: UIImage? - if item.updatingImage != nil && item.peer?.id.namespace != -1 && currentOverlayImage == nil { + if item.updatingImage != nil && item.peer?.id.namespace != .max && currentOverlayImage == nil { updateAvatarOverlayImage = updatingAvatarOverlayImage } diff --git a/submodules/ItemListPeerItem/Sources/ItemListPeerItem.swift b/submodules/ItemListPeerItem/Sources/ItemListPeerItem.swift index 404f42cc07..5e11e84832 100644 --- a/submodules/ItemListPeerItem/Sources/ItemListPeerItem.swift +++ b/submodules/ItemListPeerItem/Sources/ItemListPeerItem.swift @@ -1328,8 +1328,12 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo } } - override public func header() -> ListViewItemHeader? { - return self.layoutParams?.0.header + override public func headers() -> [ListViewItemHeader]? { + if let item = self.layoutParams?.0 { + return item.header.flatMap { [$0] } + } else { + return nil + } } override public func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) { @@ -1350,10 +1354,11 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo } public final class ItemListPeerItemHeader: ListViewItemHeader { - public let id: Int64 + public let id: ListViewItemNode.HeaderId public let text: String public let additionalText: String public let stickDirection: ListViewItemHeaderStickDirection = .topEdge + public let stickOverInsets: Bool = true public let theme: PresentationTheme public let strings: PresentationStrings public let actionTitle: String? @@ -1364,14 +1369,14 @@ public final class ItemListPeerItemHeader: ListViewItemHeader { 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.id = ListViewItemNode.HeaderId(space: 0, id: id) self.theme = theme self.strings = strings self.actionTitle = actionTitle self.action = action } - public func node() -> ListViewItemHeaderNode { + public func node(synchronousLoad: Bool) -> ListViewItemHeaderNode { return ItemListPeerItemHeaderNode(theme: self.theme, strings: self.strings, text: self.text, additionalText: self.additionalText, actionTitle: self.actionTitle, action: self.action) } @@ -1408,7 +1413,7 @@ public final class ItemListPeerItemHeaderNode: ListViewItemHeaderNode, ItemListH self.backgroundNode.backgroundColor = theme.list.blocksBackgroundColor self.snappedBackgroundNode = ASDisplayNode() - self.snappedBackgroundNode.backgroundColor = theme.rootController.navigationBar.backgroundColor + self.snappedBackgroundNode.backgroundColor = theme.rootController.navigationBar.opaqueBackgroundColor self.snappedBackgroundNode.alpha = 0.0 self.separatorNode = ASDisplayNode() @@ -1467,7 +1472,7 @@ public final class ItemListPeerItemHeaderNode: ListViewItemHeaderNode, ItemListH self.theme = theme self.backgroundNode.backgroundColor = theme.list.blocksBackgroundColor - self.snappedBackgroundNode.backgroundColor = theme.rootController.navigationBar.backgroundColor + self.snappedBackgroundNode.backgroundColor = theme.rootController.navigationBar.opaqueBackgroundColor self.separatorNode.backgroundColor = theme.list.itemBlocksSeparatorColor let titleFont = Font.regular(13.0) diff --git a/submodules/ItemListStickerPackItem/Sources/ItemListStickerPackItem.swift b/submodules/ItemListStickerPackItem/Sources/ItemListStickerPackItem.swift index d7462bab91..85ad2f2bc4 100644 --- a/submodules/ItemListStickerPackItem/Sources/ItemListStickerPackItem.swift +++ b/submodules/ItemListStickerPackItem/Sources/ItemListStickerPackItem.swift @@ -451,7 +451,7 @@ class ItemListStickerPackItemNode: ItemListRevealOptionsItemNode { 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, progressiveSizes: [])) + thumbnailItem = .still(TelegramMediaImageRepresentation(dimensions: dimensions, resource: resource, progressiveSizes: [], immediateThumbnailData: nil)) resourceReference = MediaResourceReference.media(media: .standalone(media: item.file), resource: resource) } } diff --git a/submodules/ItemListUI/BUILD b/submodules/ItemListUI/BUILD index 68433a99c3..9a38bf8d03 100644 --- a/submodules/ItemListUI/BUILD +++ b/submodules/ItemListUI/BUILD @@ -22,6 +22,7 @@ swift_library( "//submodules/SegmentedControlNode:SegmentedControlNode", "//submodules/AccountContext:AccountContext", "//submodules/AnimationUI:AnimationUI", + "//submodules/ShimmerEffect:ShimmerEffect", ], visibility = [ "//visibility:public", diff --git a/submodules/ItemListUI/Sources/ItemListController.swift b/submodules/ItemListUI/Sources/ItemListController.swift index 73a09858ce..8f57656f5d 100644 --- a/submodules/ItemListUI/Sources/ItemListController.swift +++ b/submodules/ItemListUI/Sources/ItemListController.swift @@ -444,11 +444,7 @@ open class ItemListController: ViewController, KeyShortcutResponder, Presentable } |> map { ($0.presentationData, $1) } - let displayNode = ItemListControllerNode(controller: self, navigationBar: self.navigationBar!, updateNavigationOffset: { [weak self] offset in - if let strongSelf = self { - strongSelf.navigationOffset = offset - } - }, state: nodeState) + let displayNode = ItemListControllerNode(controller: self, navigationBar: self.navigationBar!, state: nodeState) displayNode.dismiss = { [weak self] in self?.presentingViewController?.dismiss(animated: true, completion: nil) } @@ -476,7 +472,7 @@ open class ItemListController: ViewController, KeyShortcutResponder, Presentable self.validLayout = layout - (self.displayNode as! ItemListControllerNode).containerLayoutUpdated(layout, navigationBarHeight: self.navigationInsetHeight, transition: transition, additionalInsets: self.additionalInsets) + (self.displayNode as! ItemListControllerNode).containerLayoutUpdated(layout, navigationBarHeight: self.cleanNavigationHeight, transition: transition, additionalInsets: self.additionalInsets) } @objc func leftNavigationButtonPressed() { diff --git a/submodules/ItemListUI/Sources/ItemListControllerNode.swift b/submodules/ItemListUI/Sources/ItemListControllerNode.swift index d988e238ba..1fc89122e8 100644 --- a/submodules/ItemListUI/Sources/ItemListControllerNode.swift +++ b/submodules/ItemListUI/Sources/ItemListControllerNode.swift @@ -227,7 +227,7 @@ public final class ItemListControllerNodeView: UITracingLayerView { weak var controller: ItemListController? } -open class ItemListControllerNode: ASDisplayNode, UIScrollViewDelegate { +open class ItemListControllerNode: ASDisplayNode { private var _ready = ValuePromise() open var ready: Signal { return self._ready.get() @@ -261,8 +261,7 @@ open class ItemListControllerNode: ASDisplayNode, UIScrollViewDelegate { private var appliedEnsureVisibleItemTag: ItemListItemTag? private var afterLayoutActions: [() -> Void] = [] - - public let updateNavigationOffset: (CGFloat) -> Void + public var dismiss: (() -> Void)? public var visibleEntriesUpdated: ((ItemListNodeVisibleEntries) -> Void)? @@ -282,9 +281,8 @@ open class ItemListControllerNode: ASDisplayNode, UIScrollViewDelegate { var alwaysSynchronous = false - public init(controller: ItemListController?, navigationBar: NavigationBar, updateNavigationOffset: @escaping (CGFloat) -> Void, state: Signal<(ItemListPresentationData, (ItemListNodeState, Any)), NoError>) { + public init(controller: ItemListController?, navigationBar: NavigationBar, state: Signal<(ItemListPresentationData, (ItemListNodeState, Any)), NoError>) { self.navigationBar = navigationBar - self.updateNavigationOffset = updateNavigationOffset self.listNode = ListView() self.leftOverlayNode = ASDisplayNode() @@ -349,7 +347,7 @@ open class ItemListControllerNode: ASDisplayNode, UIScrollViewDelegate { self?.contentOffsetChanged?(offset, inVoiceOver) } - self.listNode.beganInteractiveDragging = { [weak self] in + self.listNode.beganInteractiveDragging = { [weak self] _ in if let strongSelf = self { strongSelf.beganInteractiveDragging?() } @@ -820,11 +818,6 @@ open class ItemListControllerNode: ASDisplayNode, UIScrollViewDelegate { self.searchNode?.scrollToTop() } - open func scrollViewDidScroll(_ scrollView: UIScrollView) { - let distanceFromEquilibrium = scrollView.contentOffset.y - scrollView.contentSize.height / 3.0 - self.updateNavigationOffset(-distanceFromEquilibrium) - } - open func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer) { targetContentOffset.pointee = scrollView.contentOffset diff --git a/submodules/ItemListUI/Sources/Items/ItemListDisclosureItem.swift b/submodules/ItemListUI/Sources/Items/ItemListDisclosureItem.swift index 585b750ae1..1c58b50e54 100644 --- a/submodules/ItemListUI/Sources/Items/ItemListDisclosureItem.swift +++ b/submodules/ItemListUI/Sources/Items/ItemListDisclosureItem.swift @@ -4,6 +4,7 @@ import Display import AsyncDisplayKit import SwiftSignalKit import TelegramPresentationData +import ShimmerEffect public enum ItemListDisclosureItemTitleColor { case primary @@ -38,8 +39,9 @@ public class ItemListDisclosureItem: ListViewItem, ItemListItem { let action: (() -> Void)? let clearHighlightAutomatically: Bool public let tag: ItemListItemTag? + public let shimmeringIndex: Int? - 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) { + 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, shimmeringIndex: Int? = nil) { self.presentationData = presentationData self.icon = icon self.title = title @@ -53,6 +55,7 @@ public class ItemListDisclosureItem: ListViewItem, ItemListItem { self.action = action self.clearHighlightAutomatically = clearHighlightAutomatically self.tag = tag + self.shimmeringIndex = shimmeringIndex } public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) { @@ -131,6 +134,9 @@ public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode { public var tag: ItemListItemTag? { return self.item?.tag } + + private var placeholderNode: ShimmerEffectNode? + private var absoluteLocation: (CGRect, CGSize)? public init() { self.backgroundNode = ASDisplayNode() @@ -179,6 +185,15 @@ public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode { self.addSubnode(self.activateArea) } + + 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.placeholderNode { + shimmerNode.updateAbsoluteRect(rect, within: containerSize) + } + } public func asyncLayout() -> (_ item: ItemListDisclosureItem, _ params: ListViewItemLayoutParams, _ insets: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { let makeTitleLayout = TextNode.asyncLayout(self.titleNode) @@ -479,6 +494,38 @@ public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode { } strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: height + UIScreenPixel)) + + if let shimmeringIndex = item.shimmeringIndex { + let shimmerNode: ShimmerEffectNode + if let current = strongSelf.placeholderNode { + shimmerNode = current + } else { + shimmerNode = ShimmerEffectNode() + strongSelf.placeholderNode = shimmerNode + if strongSelf.backgroundNode.supernode != nil { + strongSelf.insertSubnode(shimmerNode, aboveSubnode: strongSelf.backgroundNode) + } else { + strongSelf.addSubnode(shimmerNode) + } + } + shimmerNode.frame = CGRect(origin: CGPoint(), size: contentSize) + if let (rect, size) = strongSelf.absoluteLocation { + shimmerNode.updateAbsoluteRect(rect, within: size) + } + + var shapes: [ShimmerEffectNode.Shape] = [] + + let titleLineWidth: CGFloat = (shimmeringIndex % 2 == 0) ? 120.0 : 80.0 + let lineDiameter: CGFloat = 8.0 + + let titleFrame = strongSelf.titleNode.frame + shapes.append(.roundedRectLine(startPoint: CGPoint(x: titleFrame.minX, y: titleFrame.minY + floor((titleFrame.height - lineDiameter) / 2.0)), width: titleLineWidth, diameter: 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: contentSize) + } else if let shimmerNode = strongSelf.placeholderNode { + strongSelf.placeholderNode = nil + shimmerNode.removeFromSupernode() + } } }) } diff --git a/submodules/ItemListUI/Sources/Items/ItemListInfoItem.swift b/submodules/ItemListUI/Sources/Items/ItemListInfoItem.swift index 717b8c2b51..ca15e82aa4 100644 --- a/submodules/ItemListUI/Sources/Items/ItemListInfoItem.swift +++ b/submodules/ItemListUI/Sources/Items/ItemListInfoItem.swift @@ -111,7 +111,7 @@ public class ItemListInfoItem: InfoListItem, ItemListItem { } } -class InfoItemNode: ListViewItemNode { +public class InfoItemNode: ListViewItemNode { private let backgroundNode: ASDisplayNode private let topStripeNode: ASDisplayNode private let bottomStripeNode: ASDisplayNode @@ -128,7 +128,7 @@ class InfoItemNode: ListViewItemNode { private var item: InfoListItem? - override var canBeSelected: Bool { + public override var canBeSelected: Bool { return false } @@ -178,7 +178,7 @@ class InfoItemNode: ListViewItemNode { self.closeButton.addTarget(self, action: #selector(self.closeButtonPressed), forControlEvents: .touchUpInside) } - override func didLoad() { + public override func didLoad() { super.didLoad() let recognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.tapLongTapOrDoubleTapGesture(_:))) @@ -204,12 +204,12 @@ class InfoItemNode: ListViewItemNode { let currentItem = self.item return { item, params, neighbors in - let leftInset: CGFloat = 15.0 + params.leftInset - let rightInset: CGFloat = 15.0 + params.rightInset + let leftInset: CGFloat = 16.0 + params.leftInset + let rightInset: CGFloat = 16.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 titleFont = Font.medium(item.presentationData.fontSize.itemListBaseFontSize) + let textFont = Font.regular(item.presentationData.fontSize.itemListBaseLabelFontSize / 14.0 * 16.0) + let textBoldFont = Font.semibold(item.presentationData.fontSize.itemListBaseLabelFontSize / 14.0 * 16.0) let badgeFont = Font.regular(15.0) var updatedTheme: PresentationTheme? @@ -217,7 +217,7 @@ class InfoItemNode: ListViewItemNode { var updatedCloseIcon: UIImage? - let badgeDiameter: CGFloat = 20.0 + let badgeDiameter: CGFloat = 22.0 if currentItem?.presentationData.theme !== item.presentationData.theme { updatedTheme = item.presentationData.theme updatedBadgeImage = generateStretchableFilledCircleImage(diameter: badgeDiameter, color: item.presentationData.theme.list.itemDestructiveColor) @@ -254,11 +254,11 @@ class InfoItemNode: ListViewItemNode { })) } - 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 (labelLayout, labelApply) = makeLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: "!", font: badgeFont, textColor: item.presentationData.theme.list.itemCheckColors.foregroundColor), backgroundColor: nil, maximumNumberOfLines: 3, 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 contentSize = CGSize(width: params.width, height: titleLayout.size.height + textLayout.size.height + 38.0) let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) return (layout, { [weak self] in @@ -338,11 +338,11 @@ class InfoItemNode: ListViewItemNode { strongSelf.closeButton.setImage(updatedCloseIcon, for: []) } - 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.badgeNode.frame = CGRect(origin: CGPoint(x: leftInset, y: 15.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) + strongSelf.labelNode.frame = CGRect(origin: CGPoint(x: strongSelf.badgeNode.frame.midX - labelLayout.size.width / 2.0, y: strongSelf.badgeNode.frame.minY + 2.0 + UIScreenPixel), size: labelLayout.size) - strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: strongSelf.badgeNode.frame.maxX + 8.0, y: 15.0), size: titleLayout.size) + strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: strongSelf.badgeNode.frame.maxX + 8.0, y: 16.0), size: titleLayout.size) strongSelf.textNode.frame = CGRect(origin: CGPoint(x: leftInset, y: strongSelf.titleNode.frame.maxY + 9.0), size: textLayout.size) @@ -352,15 +352,15 @@ class InfoItemNode: ListViewItemNode { } } - override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + public 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) { + public 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) { + public 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/ItemListVenueItem/BUILD b/submodules/ItemListVenueItem/BUILD index f80b0d9376..7ca5b05b58 100644 --- a/submodules/ItemListVenueItem/BUILD +++ b/submodules/ItemListVenueItem/BUILD @@ -18,6 +18,7 @@ swift_library( "//submodules/TelegramUIPreferences:TelegramUIPreferences", "//submodules/PresentationDataUtils:PresentationDataUtils", "//submodules/LocationResources:LocationResources", + "//submodules/ShimmerEffect:ShimmerEffect", ], visibility = [ "//visibility:public", diff --git a/submodules/ItemListVenueItem/Sources/ItemListVenueItem.swift b/submodules/ItemListVenueItem/Sources/ItemListVenueItem.swift index e4f4921b56..9b4765bbdd 100644 --- a/submodules/ItemListVenueItem/Sources/ItemListVenueItem.swift +++ b/submodules/ItemListVenueItem/Sources/ItemListVenueItem.swift @@ -9,11 +9,12 @@ import SyncCore import TelegramPresentationData import ItemListUI import LocationResources +import ShimmerEffect public final class ItemListVenueItem: ListViewItem, ItemListItem { let presentationData: ItemListPresentationData let account: Account - let venue: TelegramMediaMap + let venue: TelegramMediaMap? let title: String? let subtitle: String? let style: ItemListStyle @@ -23,7 +24,7 @@ public final class ItemListVenueItem: ListViewItem, ItemListItem { 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) { + 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 @@ -117,6 +118,9 @@ public class ItemListVenueItemNode: ListViewItemNode, ItemListItemNode { private let addressNode: TextNode private let infoButton: HighlightableButtonNode + private var placeholderNode: ShimmerEffectNode? + private var absoluteLocation: (CGRect, CGSize)? + private var item: ItemListVenueItem? private var layoutParams: (ItemListVenueItem, ListViewItemLayoutParams, ItemListNeighbors, Bool, Bool)? @@ -170,6 +174,15 @@ public class ItemListVenueItemNode: ListViewItemNode, ItemListItemNode { self.infoButton.addTarget(self, action: #selector(self.infoPressed), forControlEvents: .touchUpInside) } + 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.placeholderNode { + shimmerNode.updateAbsoluteRect(rect, within: containerSize) + } + } + 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) @@ -188,27 +201,27 @@ public class ItemListVenueItemNode: ListViewItemNode, ItemListItemNode { updatedTheme = item.presentationData.theme } - let venueType = item.venue.venue?.type ?? "" - if currentItem?.venue.venue?.type != venueType { + let venueType = item.venue?.venue?.type ?? "" + if currentItem?.venue?.venue?.type != venueType { updatedVenueType = venueType } let title: String - if let venueTitle = item.venue.venue?.title { + if let venueTitle = item.venue?.venue?.title { title = venueTitle } else if let customTitle = item.title { title = customTitle } else { - title = "" + title = " " } let subtitle: String - if let address = item.venue.venue?.address { + if let address = item.venue?.venue?.address { subtitle = address } else if let customSubtitle = item.subtitle { subtitle = customSubtitle } else { - subtitle = "" + subtitle = " " } let titleAttributedString = NSAttributedString(string: title, font: titleFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor) @@ -350,6 +363,45 @@ public class ItemListVenueItemNode: ListViewItemNode, ItemListItemNode { 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)) + + if item.venue == nil { + let shimmerNode: ShimmerEffectNode + if let current = strongSelf.placeholderNode { + shimmerNode = current + } else { + shimmerNode = ShimmerEffectNode() + strongSelf.placeholderNode = shimmerNode + if strongSelf.bottomStripeNode.supernode != nil { + strongSelf.insertSubnode(shimmerNode, belowSubnode: strongSelf.bottomStripeNode) + } else { + strongSelf.addSubnode(shimmerNode) + } + } + shimmerNode.frame = CGRect(origin: CGPoint(), size: layout.contentSize) + if let (rect, size) = strongSelf.absoluteLocation { + shimmerNode.updateAbsoluteRect(rect, within: size) + } + + var shapes: [ShimmerEffectNode.Shape] = [] + + let titleLineWidth: CGFloat = 180.0 + let subtitleLineWidth: CGFloat = 90.0 + let lineDiameter: CGFloat = 10.0 + + let iconFrame = strongSelf.iconNode.frame + shapes.append(.circle(iconFrame)) + + let titleFrame = strongSelf.titleNode.frame + shapes.append(.roundedRectLine(startPoint: CGPoint(x: titleFrame.minX, y: titleFrame.minY + floor((titleFrame.height - lineDiameter) / 2.0)), width: titleLineWidth, diameter: lineDiameter)) + + let subtitleFrame = strongSelf.addressNode.frame + shapes.append(.roundedRectLine(startPoint: CGPoint(x: subtitleFrame.minX, y: subtitleFrame.minY + floor((subtitleFrame.height - lineDiameter) / 2.0)), width: subtitleLineWidth, diameter: 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.placeholderNode { + strongSelf.placeholderNode = nil + shimmerNode.removeFromSupernode() + } } }) } @@ -405,7 +457,11 @@ public class ItemListVenueItemNode: ListViewItemNode, ItemListItemNode { self.item?.infoAction?() } - override public func header() -> ListViewItemHeader? { - return self.item?.header + override public func headers() -> [ListViewItemHeader]? { + if let item = self.item { + return item.header.flatMap { [$0] } + } else { + return nil + } } } diff --git a/submodules/JoinLinkPreviewUI/BUILD b/submodules/JoinLinkPreviewUI/BUILD index cb02124a89..a7fb398ee0 100644 --- a/submodules/JoinLinkPreviewUI/BUILD +++ b/submodules/JoinLinkPreviewUI/BUILD @@ -20,6 +20,7 @@ swift_library( "//submodules/ShareController:ShareController", "//submodules/SelectablePeerNode:SelectablePeerNode", "//submodules/PeerInfoUI:PeerInfoUI", + "//submodules/UndoUI:UndoUI", ], visibility = [ "//visibility:public", diff --git a/submodules/JoinLinkPreviewUI/Sources/JoinLinkPreviewController.swift b/submodules/JoinLinkPreviewUI/Sources/JoinLinkPreviewController.swift index 4701e24a22..d7cbb6817b 100644 --- a/submodules/JoinLinkPreviewUI/Sources/JoinLinkPreviewController.swift +++ b/submodules/JoinLinkPreviewUI/Sources/JoinLinkPreviewController.swift @@ -11,6 +11,7 @@ import AccountContext import AlertUI import PresentationDataUtils import PeerInfoUI +import UndoUI public final class JoinLinkPreviewController: ViewController { private var controllerNode: JoinLinkPreviewControllerNode { @@ -69,7 +70,7 @@ public final class JoinLinkPreviewController: ViewController { if let resolvedState = self.resolvedState { signal = .single(resolvedState) } else { - signal = joinLinkInformation(self.link, account: self.context.account) + signal = self.context.engine.peers.joinLinkInformation(self.link) } self.disposable.set((signal @@ -87,7 +88,8 @@ public final class JoinLinkPreviewController: ViewController { strongSelf.navigateToPeer(peerId, ChatPeekTimeout(deadline: deadline, linkData: strongSelf.link)) strongSelf.dismiss() case .invalidHash: - strongSelf.present(textAlertController(context: strongSelf.context, title: nil, text: strongSelf.presentationData.strings.InviteLinks_InviteLinkExpired, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } + strongSelf.present(UndoOverlayController(presentationData: presentationData, content: .linkRevoked(text: presentationData.strings.InviteLinks_InviteLinkExpired), elevatedLayout: true, animateInAsReplacement: true, action: { _ in return false }), in: .window(.root)) strongSelf.dismiss() } } @@ -115,11 +117,11 @@ public final class JoinLinkPreviewController: 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.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition) } private func join() { - self.disposable.set((joinChatInteractively(with: self.link, account: self.context.account) |> deliverOnMainQueue).start(next: { [weak self] peerId in + self.disposable.set((self.context.engine.peers.joinChatInteractively(with: self.link) |> deliverOnMainQueue).start(next: { [weak self] peerId in if let strongSelf = self { if let peerId = peerId { strongSelf.navigateToPeer(peerId, nil) diff --git a/submodules/JoinLinkPreviewUI/Sources/JoinLinkPreviewPeerContentNode.swift b/submodules/JoinLinkPreviewUI/Sources/JoinLinkPreviewPeerContentNode.swift index 3937a380fd..0d95a46cb5 100644 --- a/submodules/JoinLinkPreviewUI/Sources/JoinLinkPreviewPeerContentNode.swift +++ b/submodules/JoinLinkPreviewUI/Sources/JoinLinkPreviewPeerContentNode.swift @@ -63,7 +63,7 @@ final class JoinLinkPreviewPeerContentNode: ASDisplayNode, ShareContentContainer super.init() - 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) + let peer = TelegramGroup(id: PeerId(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(context: context, theme: theme, peer: peer, emptyColor: theme.list.mediaPlaceholderColor) diff --git a/submodules/LanguageLinkPreviewUI/Sources/LanguageLinkPreviewController.swift b/submodules/LanguageLinkPreviewUI/Sources/LanguageLinkPreviewController.swift index 82c76ccd0c..56d168908f 100644 --- a/submodules/LanguageLinkPreviewUI/Sources/LanguageLinkPreviewController.swift +++ b/submodules/LanguageLinkPreviewUI/Sources/LanguageLinkPreviewController.swift @@ -65,7 +65,7 @@ public final class LanguageLinkPreviewController: ViewController { } self.displayNodeDidLoad() - self.disposable.set((requestLocalizationPreview(network: self.context.account.network, identifier: self.identifier) + self.disposable.set((self.context.engine.localization.requestLocalizationPreview(identifier: self.identifier) |> deliverOnMainQueue).start(next: { [weak self] result in guard let strongSelf = self else { return @@ -107,7 +107,7 @@ public final class LanguageLinkPreviewController: 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.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition) } private func activate() { @@ -115,7 +115,7 @@ public final class LanguageLinkPreviewController: ViewController { return } self.controllerNode.setInProgress(true) - self.disposable.set((downloadAndApplyLocalization(accountManager: self.context.sharedContext.accountManager, postbox: self.context.account.postbox, network: self.context.account.network, languageCode: localizationInfo.languageCode) + self.disposable.set((self.context.engine.localization.downloadAndApplyLocalization(accountManager: self.context.sharedContext.accountManager, languageCode: localizationInfo.languageCode) |> deliverOnMainQueue).start(error: { [weak self] _ in guard let strongSelf = self else { return diff --git a/submodules/LanguageSuggestionUI/Sources/LanguageSuggestionController.swift b/submodules/LanguageSuggestionUI/Sources/LanguageSuggestionController.swift index bdde36d467..0635c5b176 100644 --- a/submodules/LanguageSuggestionUI/Sources/LanguageSuggestionController.swift +++ b/submodules/LanguageSuggestionUI/Sources/LanguageSuggestionController.swift @@ -353,7 +353,7 @@ public func languageSuggestionController(context: AccountContext, suggestedLocal dismissImpl?(true) } else { startActivity() - disposable.set((downloadAndApplyLocalization(accountManager: context.sharedContext.accountManager, postbox: context.account.postbox, network: context.account.network, languageCode: languageCode) + disposable.set((context.engine.localization.downloadAndApplyLocalization(accountManager: context.sharedContext.accountManager, languageCode: languageCode) |> deliverOnMainQueue).start(completed: { dismissImpl?(true) })) diff --git a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/HPTextViewInternal.h b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/HPTextViewInternal.h index f837bd957e..0ad1e8080e 100644 --- a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/HPTextViewInternal.h +++ b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/HPTextViewInternal.h @@ -46,8 +46,6 @@ - (instancetype)initWithKeyCommandController:(TGKeyCommandController *)keyCommandController; -+ (void)addTextViewMethods; - - (void)textViewEnsureSelectionVisible; @end diff --git a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/LegacyComponentsAccessChecker.h b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/LegacyComponentsAccessChecker.h index 68cebadbfa..5770ecd099 100644 --- a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/LegacyComponentsAccessChecker.h +++ b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/LegacyComponentsAccessChecker.h @@ -26,14 +26,10 @@ typedef enum { @protocol LegacyComponentsAccessChecker -- (bool)checkAddressBookAuthorizationStatusWithAlertDismissComlpetion:(void (^)(void))alertDismissCompletion; - - (bool)checkPhotoAuthorizationStatusForIntent:(TGPhotoAccessIntent)intent alertDismissCompletion:(void (^)(void))alertDismissCompletion; - (bool)checkMicrophoneAuthorizationStatusForIntent:(TGMicrophoneAccessIntent)intent alertDismissCompletion:(void (^)(void))alertDismissCompletion; -- (bool)checkCameraAuthorizationStatusForIntent:(TGCameraAccessIntent)intent alertDismissCompletion:(void (^)(void))alertDismissCompletion; - -- (bool)checkLocationAuthorizationStatusForIntent:(TGLocationAccessIntent)intent alertDismissComlpetion:(void (^)(void))alertDismissCompletion; +- (bool)checkCameraAuthorizationStatusForIntent:(TGCameraAccessIntent)intent completion:(void (^)(BOOL))completion alertDismissCompletion:(void (^)(void))alertDismissCompletion; @end diff --git a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/LegacyComponentsGlobals.h b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/LegacyComponentsGlobals.h index ee8dcc1e23..eaa49d13ee 100644 --- a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/LegacyComponentsGlobals.h +++ b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/LegacyComponentsGlobals.h @@ -29,6 +29,8 @@ typedef enum { @protocol LegacyComponentsGlobalsProvider +- (void)makeViewDisableInteractiveKeyboardGestureRecognizer:(UIView *)view; + - (TGLocalization *)effectiveLocalization; - (void)log:(NSString *)string; - (NSArray *)applicationWindows; @@ -73,6 +75,7 @@ typedef enum { - (TGNavigationBarPallete *)navigationBarPallete; - (TGMenuSheetPallete *)menuSheetPallete; +- (TGMenuSheetPallete *)darkMenuSheetPallete; - (TGMediaAssetsPallete *)mediaAssetsPallete; - (TGCheckButtonPallete *)checkButtonPallete; diff --git a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGAttachmentCarouselItemView.h b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGAttachmentCarouselItemView.h index 9346362f3d..ed3b3f3ae3 100644 --- a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGAttachmentCarouselItemView.h +++ b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGAttachmentCarouselItemView.h @@ -43,7 +43,7 @@ @property (nonatomic, assign) bool openEditor; @property (nonatomic, copy) void (^cameraPressed)(TGAttachmentCameraView *cameraView); -@property (nonatomic, copy) void (^sendPressed)(TGMediaAsset *currentItem, bool asFiles, bool silentPosting, int32_t scheduleTime); +@property (nonatomic, copy) void (^sendPressed)(TGMediaAsset *currentItem, bool asFiles, bool silentPosting, int32_t scheduleTime, bool isFromPicker); @property (nonatomic, copy) void (^avatarCompletionBlock)(UIImage *image); @property (nonatomic, copy) void (^avatarVideoCompletionBlock)(UIImage *image, AVAsset *asset, TGVideoEditAdjustments *adjustments); @@ -62,4 +62,6 @@ - (instancetype)initWithContext:(id)context camera:(bool)hasCamera selfPortrait:(bool)selfPortrait forProfilePhoto:(bool)forProfilePhoto assetType:(TGMediaAssetType)assetType saveEditedPhotos:(bool)saveEditedPhotos allowGrouping:(bool)allowGrouping allowSelection:(bool)allowSelection allowEditing:(bool)allowEditing document:(bool)document selectionLimit:(int)selectionLimit; +- (UIView *)getItemSnapshot:(NSString *)uniqueId; + @end diff --git a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGHacks.h b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGHacks.h index e12bf35732..916a57346c 100644 --- a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGHacks.h +++ b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGHacks.h @@ -30,16 +30,9 @@ typedef enum { + (void)setSecondaryAnimationDurationFactor:(float)factor; + (void)setForceSystemCurve:(bool)forceSystemCurve; -+ (CGFloat)applicationStatusBarOffset; -+ (void)setApplicationStatusBarOffset:(CGFloat)offset; -+ (void)animateApplicationStatusBarStyleTransitionWithDuration:(NSTimeInterval)duration; -+ (CGFloat)statusBarHeightForOrientation:(UIInterfaceOrientation)orientation; - + (bool)isKeyboardVisible; -+ (CGFloat)keyboardHeightForOrientation:(UIInterfaceOrientation)orientation; -+ (void)applyCurrentKeyboardAutocorrectionVariant; ++ (void)applyCurrentKeyboardAutocorrectionVariant:(UITextView *)textView; + (UIWindow *)applicationKeyboardWindow; -+ (UIView *)applicationKeyboardView; + (void)setApplicationKeyboardOffset:(CGFloat)offset; + (void)forcePerformWithAnimation:(dispatch_block_t)block; diff --git a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMediaAssetsController.h b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMediaAssetsController.h index 9f16354018..5d94419416 100644 --- a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMediaAssetsController.h +++ b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMediaAssetsController.h @@ -74,7 +74,7 @@ typedef enum @property (nonatomic, strong) NSString *recipientName; -@property (nonatomic, copy) NSDictionary *(^descriptionGenerator)(id, NSString *, NSArray *, NSString *); +@property (nonatomic, copy) NSDictionary *(^descriptionGenerator)(id, NSString *, NSArray *, NSString *, NSString *); @property (nonatomic, copy) void (^avatarCompletionBlock)(UIImage *image); @property (nonatomic, copy) void (^completionBlock)(NSArray *signals, bool silentPosting, int32_t scheduleTime); @property (nonatomic, copy) void (^avatarVideoCompletionBlock)(UIImage *image, AVAsset *asset, TGVideoEditAdjustments *adjustments); @@ -92,7 +92,7 @@ typedef enum - (UIBarButtonItem *)rightBarButtonItem; -- (NSArray *)resultSignalsWithCurrentItem:(TGMediaAsset *)currentItem descriptionGenerator:(id (^)(id, NSString *, NSArray *, NSString *))descriptionGenerator; +- (NSArray *)resultSignalsWithCurrentItem:(TGMediaAsset *)currentItem descriptionGenerator:(id (^)(id, NSString *, NSArray *, NSString *, NSString *))descriptionGenerator; - (void)completeWithAvatarImage:(UIImage *)image; - (void)completeWithAvatarVideo:(AVAsset *)asset adjustments:(TGVideoEditAdjustments *)adjustments image:(UIImage *)image; @@ -105,6 +105,6 @@ typedef enum + (TGMediaAssetType)assetTypeForIntent:(TGMediaAssetsControllerIntent)intent; -+ (NSArray *)resultSignalsForSelectionContext:(TGMediaSelectionContext *)selectionContext editingContext:(TGMediaEditingContext *)editingContext intent:(TGMediaAssetsControllerIntent)intent currentItem:(TGMediaAsset *)currentItem storeAssets:(bool)storeAssets useMediaCache:(bool)useMediaCache descriptionGenerator:(id (^)(id, NSString *, NSArray *, NSString *))descriptionGenerator saveEditedPhotos:(bool)saveEditedPhotos; ++ (NSArray *)resultSignalsForSelectionContext:(TGMediaSelectionContext *)selectionContext editingContext:(TGMediaEditingContext *)editingContext intent:(TGMediaAssetsControllerIntent)intent currentItem:(TGMediaAsset *)currentItem storeAssets:(bool)storeAssets useMediaCache:(bool)useMediaCache descriptionGenerator:(id (^)(id, NSString *, NSArray *, NSString *, NSString *))descriptionGenerator saveEditedPhotos:(bool)saveEditedPhotos; @end diff --git a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMediaAvatarMenuMixin.h b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMediaAvatarMenuMixin.h index 158da0dcee..d4273ef481 100644 --- a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMediaAvatarMenuMixin.h +++ b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMediaAvatarMenuMixin.h @@ -13,6 +13,8 @@ typedef void (^TGMediaAvatarPresentImpl)(id, void (^)(U @interface TGMediaAvatarMenuMixin : NSObject +@property (nonatomic, assign) bool forceDark; + @property (nonatomic, copy) void (^didFinishWithImage)(UIImage *image); @property (nonatomic, copy) void (^didFinishWithVideo)(UIImage *image, AVAsset *asset, TGVideoEditAdjustments *adjustments); @property (nonatomic, copy) void (^didFinishWithDelete)(void); diff --git a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMenuSheetController.h b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMenuSheetController.h index 38ea38aeb8..0aad93b6da 100644 --- a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMenuSheetController.h +++ b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMenuSheetController.h @@ -34,6 +34,8 @@ @property (nonatomic, assign) bool dismissesByOutsideTap; @property (nonatomic, assign) bool hasSwipeGesture; +@property (nonatomic, assign) bool forceDark; + @property (nonatomic, assign) bool followsKeyboard; @property (nonatomic, assign) bool ignoreNextDismissal; diff --git a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoEditorSliderView.h b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoEditorSliderView.h index 514aad3bda..319a69076e 100644 --- a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoEditorSliderView.h +++ b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoEditorSliderView.h @@ -35,6 +35,7 @@ @property (nonatomic, strong) UIImage *knobImage; @property (nonatomic, readonly) UIImageView *knobView; +@property (nonatomic, assign) bool disableSnapToPositions; @property (nonatomic, assign) NSInteger positionsCount; @property (nonatomic, assign) CGFloat dotSize; diff --git a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGVideoMessageCaptureController.h b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGVideoMessageCaptureController.h index 73b0488a07..1a522e7eb0 100644 --- a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGVideoMessageCaptureController.h +++ b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGVideoMessageCaptureController.h @@ -36,7 +36,7 @@ - (CGRect)frameForSendButton; - (void)complete; -- (void)dismiss; +- (void)dismiss:(bool)cancelled; - (bool)stop; + (void)clearStartImage; @@ -44,4 +44,7 @@ + (void)requestCameraAccess:(void (^)(bool granted, bool wasNotDetermined))resultBlock; + (void)requestMicrophoneAccess:(void (^)(bool granted, bool wasNotDetermined))resultBlock; +- (UIView *)extractVideoContent; +- (void)hideVideoContent; + @end diff --git a/submodules/LegacyComponents/Sources/HPGrowingTextView.m b/submodules/LegacyComponents/Sources/HPGrowingTextView.m index c31e3cbc5e..99e53f2370 100644 --- a/submodules/LegacyComponents/Sources/HPGrowingTextView.m +++ b/submodules/LegacyComponents/Sources/HPGrowingTextView.m @@ -63,12 +63,6 @@ NSString *TGMentionBoldAttributeName = @"TGMentionBoldAttributeName"; - (void)commonInitialiser { - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^ - { - [HPTextViewInternal addTextViewMethods]; - }); - CGRect frame = self.frame; frame.origin = CGPointZero; _internalTextView = [[HPTextViewInternal alloc] initWithKeyCommandController:_keyCommandController]; diff --git a/submodules/LegacyComponents/Sources/HPTextViewInternal.m b/submodules/LegacyComponents/Sources/HPTextViewInternal.m index 7515e8a770..a7a02ae7bd 100644 --- a/submodules/LegacyComponents/Sources/HPTextViewInternal.m +++ b/submodules/LegacyComponents/Sources/HPTextViewInternal.m @@ -29,15 +29,6 @@ return self; } -+ (void)addTextViewMethods -{ - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^ - { - InjectInstanceMethodFromAnotherClass([HPTextViewInternal class], [HPTextViewInternal class], @selector(textViewAdjustScrollRange:animated:), NSSelectorFromString(TGEncodeText(@"`tdspmmSbohfUpWjtjcmf;bojnbufe;", -1))); - }); -} - - (void)setText:(NSString *)text { BOOL originalValue = self.scrollEnabled; @@ -64,25 +55,6 @@ [super setScrollEnabled:isScrollable]; } -- (void)textViewAdjustScrollRange:(NSRange)range animated:(BOOL)animated -{ - static SEL selector = NULL; - static void (*impl)(id, SEL, NSRange, BOOL) = NULL; - - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^ - { - Method method = class_getInstanceMethod([UITextView class], selector); - if (method != NULL) - impl = (void (*)(id, SEL, NSRange, BOOL))method_getImplementation(method); - }); - - animated = false; - - if (impl != NULL) - impl(self, selector, range, animated); -} - - (void)scrollRectToVisible:(CGRect)__unused rect animated:(BOOL)__unused animated { diff --git a/submodules/LegacyComponents/Sources/LegacyComponentsInternal.h b/submodules/LegacyComponents/Sources/LegacyComponentsInternal.h index 0fac5b8aa4..7feaa28fb6 100644 --- a/submodules/LegacyComponents/Sources/LegacyComponentsInternal.h +++ b/submodules/LegacyComponents/Sources/LegacyComponentsInternal.h @@ -14,8 +14,6 @@ void TGLegacyLog(NSString *format, ...); int iosMajorVersion(); int iosMinorVersion(); -NSString *TGEncodeText(NSString *string, int key); - void TGDispatchOnMainThread(dispatch_block_t block); void TGDispatchAfter(double delay, dispatch_queue_t queue, dispatch_block_t block); diff --git a/submodules/LegacyComponents/Sources/LegacyComponentsInternal.m b/submodules/LegacyComponents/Sources/LegacyComponentsInternal.m index 494622075b..a79b0e7f37 100644 --- a/submodules/LegacyComponents/Sources/LegacyComponentsInternal.m +++ b/submodules/LegacyComponents/Sources/LegacyComponentsInternal.m @@ -70,20 +70,6 @@ int iosMinorVersion() return version; } -NSString *TGEncodeText(NSString *string, int key) -{ - NSMutableString *result = [[NSMutableString alloc] init]; - - for (int i = 0; i < (int)[string length]; i++) - { - unichar c = [string characterAtIndex:i]; - c += key; - [result appendString:[NSString stringWithCharacters:&c length:1]]; - } - - return result; -} - int deviceMemorySize() { static int memorySize = 0; diff --git a/submodules/LegacyComponents/Sources/PGVideoMovie.m b/submodules/LegacyComponents/Sources/PGVideoMovie.m index 0d781fbbde..274ec0605c 100755 --- a/submodules/LegacyComponents/Sources/PGVideoMovie.m +++ b/submodules/LegacyComponents/Sources/PGVideoMovie.m @@ -1,5 +1,6 @@ #import "PGVideoMovie.h" #import "GPUImageFilter.h" +#import "LegacyComponentsInternal.h" GLfloat kColorConversion601Default[] = { 1.164, 1.164, 1.164, @@ -226,7 +227,20 @@ NSString *const kYUVVideoRangeConversionForLAFragmentShaderString = SHADER_STRIN else { [pixBuffAttributes setObject:@(kCVPixelFormatType_32BGRA) forKey:(id)kCVPixelBufferPixelFormatTypeKey]; } - playerItemOutput = [[AVPlayerItemVideoOutput alloc] initWithPixelBufferAttributes:pixBuffAttributes]; + if (iosMajorVersion() >= 10) { + NSDictionary *hdVideoProperties = @ + { + AVVideoColorPrimariesKey: AVVideoColorPrimaries_ITU_R_709_2, + AVVideoTransferFunctionKey: AVVideoTransferFunction_ITU_R_709_2, + AVVideoYCbCrMatrixKey: AVVideoYCbCrMatrix_ITU_R_709_2, + }; + [pixBuffAttributes setObject:hdVideoProperties forKey:AVVideoColorPropertiesKey]; + playerItemOutput = [[AVPlayerItemVideoOutput alloc] initWithOutputSettings:pixBuffAttributes]; + + + } else { + playerItemOutput = [[AVPlayerItemVideoOutput alloc] initWithPixelBufferAttributes:pixBuffAttributes]; + } [playerItemOutput setDelegate:self queue:videoProcessingQueue]; [_playerItem addOutput:playerItemOutput]; diff --git a/submodules/LegacyComponents/Sources/TGAttachmentCarouselItemView.m b/submodules/LegacyComponents/Sources/TGAttachmentCarouselItemView.m index 3aa9bcda19..cc036da905 100644 --- a/submodules/LegacyComponents/Sources/TGAttachmentCarouselItemView.m +++ b/submodules/LegacyComponents/Sources/TGAttachmentCarouselItemView.m @@ -270,7 +270,7 @@ const NSUInteger TGAttachmentDisplayedAssetLimit = 500; { if (strongSelf->_selectionContext.allowGrouping) [[NSUserDefaults standardUserDefaults] setObject:@(!strongSelf->_selectionContext.grouping) forKey:@"TG_mediaGroupingDisabled_v0"]; - strongSelf.sendPressed(nil, false, false, 0); + strongSelf.sendPressed(nil, false, false, 0, false); } }]; [_sendMediaItemView setHidden:true animated:false]; @@ -282,7 +282,7 @@ const NSUInteger TGAttachmentDisplayedAssetLimit = 500; { __strong TGAttachmentCarouselItemView *strongSelf = weakSelf; if (strongSelf != nil && strongSelf.sendPressed != nil) - strongSelf.sendPressed(nil, true, false, 0); + strongSelf.sendPressed(nil, true, false, 0, false); }]; _sendFileItemView.requiresDivider = false; [_sendFileItemView setHidden:true animated:false]; @@ -346,6 +346,21 @@ const NSUInteger TGAttachmentDisplayedAssetLimit = 500; [_itemsSizeChangedDisposable dispose]; } +- (UIView *)getItemSnapshot:(NSString *)uniqueId { + for (UIView *cell in _collectionView.visibleCells) { + if ([cell isKindOfClass:[TGAttachmentAssetCell class]]) { + TGAttachmentAssetCell *assetCell = (TGAttachmentAssetCell *)cell; + if (assetCell.asset.identifier != nil && [assetCell.asset.identifier isEqualToString:uniqueId]) { + UIView *snapshotView = [assetCell snapshotViewAfterScreenUpdates:false]; + snapshotView.frame = [assetCell convertRect:assetCell.bounds toView:nil]; + assetCell.alpha = 0.01f; + return snapshotView; + } + } + } + return nil; +} + - (void)setPallete:(TGMenuSheetPallete *)pallete { _pallete = pallete; @@ -792,7 +807,7 @@ const NSUInteger TGAttachmentDisplayedAssetLimit = 500; { if (strongSelf->_selectionContext.allowGrouping) [[NSUserDefaults standardUserDefaults] setObject:@(!strongSelf->_selectionContext.grouping) forKey:@"TG_mediaGroupingDisabled_v0"]; - strongSelf.sendPressed(item.asset, strongSelf.asFile, silentPosting, scheduleTime); + strongSelf.sendPressed(item.asset, strongSelf.asFile, silentPosting, scheduleTime, true); } }; diff --git a/submodules/LegacyComponents/Sources/TGGradientLabel.m b/submodules/LegacyComponents/Sources/TGGradientLabel.m index 317e9111ac..ca5f4212dd 100644 --- a/submodules/LegacyComponents/Sources/TGGradientLabel.m +++ b/submodules/LegacyComponents/Sources/TGGradientLabel.m @@ -113,7 +113,7 @@ _offscreenContextWidth = offscreenWidth; _offscreenContextHeight = offscreenHeight; - _offscreenContextStride = ((4 * _offscreenContextWidth + 15) & (~15)); + _offscreenContextStride = ((4 * _offscreenContextWidth + 31) & (~31)); _offscreenMemory = malloc(_offscreenContextStride * _offscreenContextHeight); CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); diff --git a/submodules/LegacyComponents/Sources/TGHacks.m b/submodules/LegacyComponents/Sources/TGHacks.m index 0b1f88a6bc..e8211c63a8 100644 --- a/submodules/LegacyComponents/Sources/TGHacks.m +++ b/submodules/LegacyComponents/Sources/TGHacks.m @@ -215,209 +215,6 @@ void InjectInstanceMethodFromAnotherClass(Class toClass, Class fromClass, SEL fr window.alpha = alpha; } -+ (CGFloat)applicationStatusBarOffset -{ - UIWindow *window = [[LegacyComponentsGlobals provider] applicationStatusBarWindow]; - return window.bounds.origin.y; -} - -+ (void)setApplicationStatusBarOffset:(CGFloat)offset { - UIWindow *window = [[LegacyComponentsGlobals provider] applicationStatusBarWindow]; - CGRect bounds = window.bounds; - bounds.origin = CGPointMake(0.0f, -offset); - window.bounds = bounds; -} - -static UIView *findStatusBarView() -{ - static Class viewClass = nil; - static SEL selector = NULL; - if (selector == NULL) - { - NSString *str1 = @"rs`str"; - NSString *str2 = @"A`qVhmcnv"; - - selector = NSSelectorFromString([[NSString alloc] initWithFormat:@"%@%@", TGEncodeText(str1, 1), TGEncodeText(str2, 1)]); - - viewClass = NSClassFromString(TGEncodeText(@"VJTubuvtCbs", -1)); - } - - UIWindow *window = [[LegacyComponentsGlobals provider] applicationStatusBarWindow]; - - for (UIView *subview in window.subviews) - { - if ([subview isKindOfClass:viewClass]) - { - return subview; - } - } - - return nil; -} - -+ (void)animateApplicationStatusBarAppearance:(int)statusBarAnimation duration:(NSTimeInterval)duration completion:(void (^)())completion -{ - [self animateApplicationStatusBarAppearance:statusBarAnimation delay:0.0 duration:duration completion:completion]; -} - -+ (void)animateApplicationStatusBarAppearance:(int)statusBarAnimation delay:(NSTimeInterval)delay duration:(NSTimeInterval)duration completion:(void (^)())completion -{ - UIView *view = findStatusBarView(); - - if (view != nil) - { - if ((statusBarAnimation & TGStatusBarAppearanceAnimationSlideDown) || (statusBarAnimation & TGStatusBarAppearanceAnimationSlideUp)) - { - CGPoint startPosition = view.layer.position; - CGPoint position = view.layer.position; - - CGPoint normalPosition = CGPointMake(CGFloor(view.frame.size.width / 2), CGFloor(view.frame.size.height / 2)); - - CGFloat viewHeight = view.frame.size.height; - - if (statusBarAnimation & TGStatusBarAppearanceAnimationSlideDown) - { - startPosition = CGPointMake(CGFloor(view.frame.size.width / 2), CGFloor(view.frame.size.height / 2) - viewHeight); - position = CGPointMake(CGFloor(view.frame.size.width / 2), CGFloor(view.frame.size.height / 2)); - } - else if (statusBarAnimation & TGStatusBarAppearanceAnimationSlideUp) - { - startPosition = CGPointMake(CGFloor(view.frame.size.width / 2), CGFloor(view.frame.size.height / 2)); - position = CGPointMake(CGFloor(view.frame.size.width / 2), CGFloor(view.frame.size.height / 2) - viewHeight); - } - - CABasicAnimation *animation = [[CABasicAnimation alloc] init]; - animation.duration = duration; - animation.fromValue = [NSValue valueWithCGPoint:startPosition]; - animation.toValue = [NSValue valueWithCGPoint:position]; - animation.removedOnCompletion = true; - animation.fillMode = kCAFillModeForwards; - animation.beginTime = delay; - animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]; - - TGAnimationBlockDelegate *delegate = [[TGAnimationBlockDelegate alloc] initWithLayer:view.layer]; - delegate.completion = ^(BOOL finished) - { - if (finished) - view.layer.position = normalPosition; - if (completion) - completion(); - }; - animation.delegate = delegate; - [view.layer addAnimation:animation forKey:@"position"]; - - view.layer.position = position; - } - else if ((statusBarAnimation & TGStatusBarAppearanceAnimationFadeIn) || (statusBarAnimation & TGStatusBarAppearanceAnimationFadeOut)) - { - float startOpacity = view.layer.opacity; - float opacity = view.layer.opacity; - - if (statusBarAnimation & TGStatusBarAppearanceAnimationFadeIn) - { - startOpacity = 0.0f; - opacity = 1.0f; - } - else if (statusBarAnimation & TGStatusBarAppearanceAnimationFadeOut) - { - startOpacity = 1.0f; - opacity = 0.0f; - } - - CABasicAnimation *animation = [[CABasicAnimation alloc] init]; - animation.duration = duration; - animation.fromValue = @(startOpacity); - animation.toValue = @(opacity); - animation.removedOnCompletion = true; - animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]; - TGAnimationBlockDelegate *delegate = [[TGAnimationBlockDelegate alloc] initWithLayer:view.layer]; - delegate.completion = ^(__unused BOOL finished) - { - if (completion) - completion(); - }; - animation.delegate = delegate; - - [view.layer addAnimation:animation forKey:@"opacity"]; - } - } - else - { - if (completion) - completion(); - } -} - -+ (void)animateApplicationStatusBarStyleTransitionWithDuration:(NSTimeInterval)duration -{ - UIView *view = findStatusBarView(); - - if (view != nil) - { - UIView *snapshotView = [view snapshotViewAfterScreenUpdates:false]; - [view addSubview:snapshotView]; - - [UIView animateWithDuration:duration animations:^ - { - snapshotView.alpha = 0.0f; - } completion:^(__unused BOOL finished) - { - [snapshotView removeFromSuperview]; - }]; - } -} - -+ (CGFloat)statusBarHeightForOrientation:(UIInterfaceOrientation)orientation -{ - UIWindow *window = [[LegacyComponentsGlobals provider] applicationStatusBarWindow]; - - Class statusBarClass = NSClassFromString(TGEncodeText(@"VJTubuvtCbs", -1)); - - for (UIView *view in window.subviews) - { - if ([view isKindOfClass:statusBarClass]) - { - SEL selector = NSSelectorFromString(TGEncodeText(@"dvssfouTuzmf", -1)); - NSMethodSignature *signature = [statusBarClass instanceMethodSignatureForSelector:selector]; - if (signature == nil) - { - TGLegacyLog(@"***** Method not found"); - return 20.0f; - } - - NSInvocation *inv = [NSInvocation invocationWithMethodSignature:signature]; - [inv setSelector:selector]; - [inv setTarget:view]; - [inv invoke]; - - NSInteger result = 0; - [inv getReturnValue:&result]; - - SEL selector2 = NSSelectorFromString(TGEncodeText(@"ifjhiuGpsTuzmf;psjfoubujpo;", -1)); - NSMethodSignature *signature2 = [statusBarClass methodSignatureForSelector:selector2]; - if (signature2 == nil) - { - TGLegacyLog(@"***** Method not found"); - return 20.0f; - } - NSInvocation *inv2 = [NSInvocation invocationWithMethodSignature:signature2]; - [inv2 setSelector:selector2]; - [inv2 setTarget:[view class]]; - [inv2 setArgument:&result atIndex:2]; - NSInteger argOrientation = orientation; - [inv2 setArgument:&argOrientation atIndex:3]; - [inv2 invoke]; - - CGFloat result2 = 0; - [inv2 getReturnValue:&result2]; - - return result2; - } - } - - return 20.0f; -} - + (bool)isKeyboardVisible { return [self isKeyboardVisibleAlt]; @@ -445,66 +242,9 @@ static bool keyboardHidden = true; return !keyboardHidden; } -+ (CGFloat)keyboardHeightForOrientation:(UIInterfaceOrientation)orientation ++ (void)applyCurrentKeyboardAutocorrectionVariant:(UITextView *)textView { - static NSInvocation *invocation = nil; - static Class keyboardClass = NULL; - - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^ - { - keyboardClass = NSClassFromString(TGEncodeText(@"VJLfzcpbse", -1)); - - SEL selector = NSSelectorFromString(TGEncodeText(@"tj{fGpsJoufsgbdfPsjfoubujpo;", -1)); - NSMethodSignature *signature = [keyboardClass methodSignatureForSelector:selector]; - if (signature == nil) - TGLegacyLog(@"***** Method not found"); - else - { - invocation = [NSInvocation invocationWithMethodSignature:signature]; - [invocation setSelector:selector]; - } - }); - - if (invocation != nil) - { - [invocation setTarget:[keyboardClass class]]; - [invocation setArgument:&orientation atIndex:2]; - [invocation invoke]; - - CGSize result = CGSizeZero; - [invocation getReturnValue:&result]; - - return MIN(result.width, result.height); - } - - return 0.0f; -} - -+ (void)applyCurrentKeyboardAutocorrectionVariant -{ - static Class keyboardClass = NULL; - static SEL currentInstanceSelector = NULL; - static SEL applyVariantSelector = NULL; - - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^ - { - keyboardClass = NSClassFromString(TGEncodeText(@"VJLfzcpbse", -1)); - - currentInstanceSelector = NSSelectorFromString(TGEncodeText(@"bdujwfLfzcpbse", -1)); - applyVariantSelector = NSSelectorFromString(TGEncodeText(@"bddfquBvupdpssfdujpo", -1)); - }); - - if ([keyboardClass respondsToSelector:currentInstanceSelector]) - { -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Warc-performSelector-leaks" - id currentInstance = [keyboardClass performSelector:currentInstanceSelector]; - if ([currentInstance respondsToSelector:applyVariantSelector]) - [currentInstance performSelector:applyVariantSelector]; -#pragma clang diagnostic pop - } + [textView unmarkText]; } + (UIWindow *)applicationKeyboardWindow @@ -518,32 +258,6 @@ static bool keyboardHidden = true; keyboardWindow.frame = CGRectOffset(keyboardWindow.bounds, 0.0f, offset); } -+ (UIView *)applicationKeyboardView -{ - static Class keyboardViewClass = Nil; - static Class keyboardViewContainerClass = Nil; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^ - { - keyboardViewClass = NSClassFromString(TGEncodeText(@"VJJoqvuTfuIptuWjfx", -1)); - keyboardViewContainerClass = NSClassFromString(TGEncodeText(@"VJJoqvuTfuDpoubjofsWjfx", -1)); - }); - - for (UIView *view in [self applicationKeyboardWindow].subviews) - { - if ([view isKindOfClass:keyboardViewContainerClass]) - { - for (UIView *subview in view.subviews) - { - if ([subview isKindOfClass:keyboardViewClass]) - return subview; - } - } - } - - return nil; -} - + (void)setForceMovieAnimatedScaleMode:(bool)force { forceMovieAnimatedScaleMode = force; diff --git a/submodules/LegacyComponents/Sources/TGMediaAssetsController.m b/submodules/LegacyComponents/Sources/TGMediaAssetsController.m index 7ab5b91ab6..37058dca22 100644 --- a/submodules/LegacyComponents/Sources/TGMediaAssetsController.m +++ b/submodules/LegacyComponents/Sources/TGMediaAssetsController.m @@ -444,7 +444,7 @@ bool onlyGroupableMedia = true; for (TGMediaAsset *item in strongSelf->_selectionContext.selectedItems) { - TGMediaAsset *asset = asset; + TGMediaAsset *asset = item; if ([asset isKindOfClass:[TGCameraCapturedVideo class]]) { asset = [(TGCameraCapturedVideo *)item originalAsset]; } @@ -578,8 +578,11 @@ [super loadView]; bool hasOnScreenNavigation = false; - if (iosMajorVersion() >= 11) - hasOnScreenNavigation = (self.viewLoaded && self.view.safeAreaInsets.bottom > FLT_EPSILON) || _context.safeAreaInset.bottom > FLT_EPSILON; + if (iosMajorVersion() >= 11) { + if (@available(iOS 11.0, *)) { + hasOnScreenNavigation = (self.viewLoaded && self.view.safeAreaInsets.bottom > FLT_EPSILON) || _context.safeAreaInset.bottom > FLT_EPSILON; + } + } CGFloat inset = [TGViewController safeAreaInsetForOrientation:self.interfaceOrientation hasOnScreenNavigation:hasOnScreenNavigation].bottom; _toolbarView = [[TGMediaPickerToolbarView alloc] initWithFrame:CGRectMake(0, self.view.frame.size.height - TGMediaPickerToolbarHeight - inset, self.view.frame.size.width, TGMediaPickerToolbarHeight + inset)]; @@ -648,7 +651,9 @@ return; [strongController dismissAnimated:true manual:false completion:nil]; + if (@available(iOS 14, *)) { [[PHPhotoLibrary sharedPhotoLibrary] presentLimitedLibraryPickerFromViewController:strongSelf]; + } }], [[TGMenuSheetButtonItemView alloc] initWithTitle:TGLocalized(@"Media.LimitedAccessChangeSettings") type:TGMenuSheetButtonTypeDefault fontSize:20.0 action:^ { @@ -725,8 +730,11 @@ orientation = UIInterfaceOrientationLandscapeLeft; bool hasOnScreenNavigation = false; - if (iosMajorVersion() >= 11) - hasOnScreenNavigation = (self.viewLoaded && self.view.safeAreaInsets.bottom > FLT_EPSILON) || _context.safeAreaInset.bottom > FLT_EPSILON; + if (iosMajorVersion() >= 11) { + if (@available(iOS 11.0, *)) { + hasOnScreenNavigation = (self.viewLoaded && self.view.safeAreaInsets.bottom > FLT_EPSILON) || _context.safeAreaInset.bottom > FLT_EPSILON; + } + } _toolbarView.safeAreaInset = [TGViewController safeAreaInsetForOrientation:orientation hasOnScreenNavigation:hasOnScreenNavigation]; _accessView.safeAreaInset = [TGViewController safeAreaInsetForOrientation:orientation hasOnScreenNavigation:hasOnScreenNavigation]; @@ -810,7 +818,7 @@ } } -- (NSArray *)resultSignalsWithCurrentItem:(TGMediaAsset *)currentItem descriptionGenerator:(id (^)(id, NSString *, NSArray *, NSString *))descriptionGenerator +- (NSArray *)resultSignalsWithCurrentItem:(TGMediaAsset *)currentItem descriptionGenerator:(id (^)(id, NSString *, NSArray *, NSString *, NSString *))descriptionGenerator { bool storeAssets = (_editingContext != nil) && self.shouldStoreAssets; @@ -827,7 +835,7 @@ return value; } -+ (NSArray *)resultSignalsForSelectionContext:(TGMediaSelectionContext *)selectionContext editingContext:(TGMediaEditingContext *)editingContext intent:(TGMediaAssetsControllerIntent)intent currentItem:(TGMediaAsset *)currentItem storeAssets:(bool)storeAssets useMediaCache:(bool)__unused useMediaCache descriptionGenerator:(id (^)(id, NSString *, NSArray *, NSString *))descriptionGenerator saveEditedPhotos:(bool)saveEditedPhotos ++ (NSArray *)resultSignalsForSelectionContext:(TGMediaSelectionContext *)selectionContext editingContext:(TGMediaEditingContext *)editingContext intent:(TGMediaAssetsControllerIntent)intent currentItem:(TGMediaAsset *)currentItem storeAssets:(bool)storeAssets useMediaCache:(bool)__unused useMediaCache descriptionGenerator:(id (^)(id, NSString *, NSArray *, NSString *, NSString *))descriptionGenerator saveEditedPhotos:(bool)saveEditedPhotos { NSMutableArray *signals = [[NSMutableArray alloc] init]; NSMutableArray *selectedItems = selectionContext.selectedItems ? [selectionContext.selectedItems mutableCopy] : [[NSMutableArray alloc] init]; @@ -945,7 +953,7 @@ if (groupedId != nil) dict[@"groupedId"] = groupedId; - id generatedItem = descriptionGenerator(dict, caption, entities, nil); + id generatedItem = descriptionGenerator(dict, caption, entities, nil, asset.identifier); return generatedItem; }] catch:^SSignal *(id error) { @@ -971,7 +979,7 @@ if (groupedId != nil) dict[@"groupedId"] = groupedId; - id generatedItem = descriptionGenerator(dict, caption, entities, nil); + id generatedItem = descriptionGenerator(dict, caption, entities, nil, asset.identifier); return generatedItem; }]; }]]; @@ -998,7 +1006,7 @@ else if (groupedId != nil && !hasAnyTimers) dict[@"groupedId"] = groupedId; - id generatedItem = descriptionGenerator(dict, caption, entities, nil); + id generatedItem = descriptionGenerator(dict, caption, entities, nil, asset.identifier); return generatedItem; }]; @@ -1074,7 +1082,7 @@ else if (groupedId != nil && !hasAnyTimers) dict[@"groupedId"] = groupedId; - id generatedItem = descriptionGenerator(dict, caption, entities, nil); + id generatedItem = descriptionGenerator(dict, caption, entities, nil, asset.identifier); return generatedItem; }]; }]]; @@ -1156,7 +1164,7 @@ else if (groupedId != nil && !hasAnyTimers) dict[@"groupedId"] = groupedId; - id generatedItem = descriptionGenerator(dict, caption, entities, nil); + id generatedItem = descriptionGenerator(dict, caption, entities, nil, asset.identifier); return generatedItem; }] catch:^SSignal *(__unused id error) { @@ -1197,7 +1205,7 @@ if (groupedId != nil) dict[@"groupedId"] = groupedId; - id generatedItem = descriptionGenerator(dict, caption, entities, nil); + id generatedItem = descriptionGenerator(dict, caption, entities, nil, asset.identifier); return generatedItem; }]]; @@ -1267,7 +1275,7 @@ else if (groupedId != nil && !hasAnyTimers) dict[@"groupedId"] = groupedId; - id generatedItem = descriptionGenerator(dict, caption, entities, nil); + id generatedItem = descriptionGenerator(dict, caption, entities, nil, asset.identifier); return generatedItem; }]]; @@ -1345,7 +1353,7 @@ if (timer != nil) dict[@"timer"] = timer; - id generatedItem = descriptionGenerator(dict, caption, entities, nil); + id generatedItem = descriptionGenerator(dict, caption, entities, nil, asset.identifier); return generatedItem; }]]; @@ -1399,8 +1407,8 @@ if (_searchController == nil) return; - UIView *backArrow = [self _findBackArrow:self.navigationBar]; - UIView *backButton = [self _findBackButton:self.navigationBar parentView:self.navigationBar]; + UIView *backArrow = nil; + UIView *backButton = nil; if ([viewController isKindOfClass:[TGPhotoEditorController class]]) { @@ -1432,50 +1440,13 @@ _searchSnapshotView = nil; _searchController.view.hidden = false; - UIView *backArrow = [self _findBackArrow:self.navigationBar]; - UIView *backButton = [self _findBackButton:self.navigationBar parentView:self.navigationBar]; + UIView *backArrow = nil; + UIView *backButton = nil; backArrow.alpha = 1.0f; backButton.alpha = 1.0f; } } -- (UIView *)_findBackArrow:(UIView *)view -{ - Class backArrowClass = NSClassFromString(TGEncodeText(@"`VJObwjhbujpoCbsCbdlJoejdbupsWjfx", -1)); - - if ([view isKindOfClass:backArrowClass]) - return view; - - for (UIView *subview in view.subviews) - { - UIView *result = [self _findBackArrow:subview]; - if (result != nil) - return result; - } - - return nil; -} - -- (UIView *)_findBackButton:(UIView *)view parentView:(UIView *)parentView -{ - Class backButtonClass = NSClassFromString(TGEncodeText(@"VJObwjhbujpoJufnCvuupoWjfx", -1)); - - if ([view isKindOfClass:backButtonClass]) - { - if (view.center.x < parentView.frame.size.width / 2.0f) - return view; - } - - for (UIView *subview in view.subviews) - { - UIView *result = [self _findBackButton:subview parentView:parentView]; - if (result != nil) - return result; - } - - return nil; -} - #pragma mark - + (TGMediaAssetType)assetTypeForIntent:(TGMediaAssetsControllerIntent)intent diff --git a/submodules/LegacyComponents/Sources/TGMediaAvatarMenuMixin.m b/submodules/LegacyComponents/Sources/TGMediaAvatarMenuMixin.m index 1169d65208..12b4f56a8a 100644 --- a/submodules/LegacyComponents/Sources/TGMediaAvatarMenuMixin.m +++ b/submodules/LegacyComponents/Sources/TGMediaAvatarMenuMixin.m @@ -73,7 +73,7 @@ - (TGMenuSheetController *)_presentAvatarMenu { __weak TGMediaAvatarMenuMixin *weakSelf = self; - TGMenuSheetController *controller = [[TGMenuSheetController alloc] initWithContext:_context dark:false]; + TGMenuSheetController *controller = [[TGMenuSheetController alloc] initWithContext:_context dark:self.forceDark]; controller.dismissesByOutsideTap = true; controller.hasSwipeGesture = true; controller.didDismiss = ^(bool manual) @@ -270,101 +270,102 @@ - (void)_displayCameraWithView:(TGAttachmentCameraView *)cameraView menuController:(TGMenuSheetController *)menuController { - if (![[[LegacyComponentsGlobals provider] accessChecker] checkCameraAuthorizationStatusForIntent:TGCameraAccessIntentDefault alertDismissCompletion:nil]) - return; - - if ([_context currentlyInSplitView]) - return; - - TGCameraController *controller = nil; - CGSize screenSize = TGScreenSize(); - - id windowManager = [_context makeOverlayWindowManager]; - - if (cameraView.previewView != nil) - controller = [[TGCameraController alloc] initWithContext:[windowManager context] saveEditedPhotos:_saveEditedPhotos saveCapturedMedia:_saveCapturedMedia camera:cameraView.previewView.camera previewView:cameraView.previewView intent:_signup ? TGCameraControllerSignupAvatarIntent : TGCameraControllerAvatarIntent]; - else - controller = [[TGCameraController alloc] initWithContext:[windowManager context] saveEditedPhotos:_saveEditedPhotos saveCapturedMedia:_saveCapturedMedia intent:_signup ? TGCameraControllerSignupAvatarIntent : TGCameraControllerAvatarIntent]; - controller.stickersContext = _stickersContext; - controller.shouldStoreCapturedAssets = true; - - TGCameraControllerWindow *controllerWindow = [[TGCameraControllerWindow alloc] initWithManager:windowManager parentController:_parentController contentController:controller]; - controllerWindow.hidden = false; - controllerWindow.clipsToBounds = true; - - if ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPhone) - controllerWindow.frame = CGRectMake(0, 0, screenSize.width, screenSize.height); - else - controllerWindow.frame = [_context fullscreenBounds]; - - bool standalone = true; - CGRect startFrame = CGRectMake(0, screenSize.height, screenSize.width, screenSize.height); - if (cameraView != nil) - { - standalone = false; - if ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPad) - startFrame = CGRectZero; - else - startFrame = [controller.view convertRect:cameraView.previewView.frame fromView:cameraView]; - } - - [cameraView detachPreviewView]; - [controller beginTransitionInFromRect:startFrame]; - - __weak TGMediaAvatarMenuMixin *weakSelf = self; - __weak TGCameraController *weakCameraController = controller; - __weak TGAttachmentCameraView *weakCameraView = cameraView; - - controller.beginTransitionOut = ^CGRect - { - __strong TGCameraController *strongCameraController = weakCameraController; - if (strongCameraController == nil) - return CGRectZero; - - __strong TGAttachmentCameraView *strongCameraView = weakCameraView; - if (strongCameraView != nil) - { - [strongCameraView willAttachPreviewView]; - if ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPad) - return CGRectZero; + [[[LegacyComponentsGlobals provider] accessChecker] checkCameraAuthorizationStatusForIntent:TGCameraAccessIntentDefault completion:^(BOOL allowed) { + if (!allowed) + return; + if ([_context currentlyInSplitView]) + return; - return [strongCameraController.view convertRect:strongCameraView.frame fromView:strongCameraView.superview]; + TGCameraController *controller = nil; + CGSize screenSize = TGScreenSize(); + + id windowManager = [_context makeOverlayWindowManager]; + + if (cameraView.previewView != nil) + controller = [[TGCameraController alloc] initWithContext:[windowManager context] saveEditedPhotos:_saveEditedPhotos saveCapturedMedia:_saveCapturedMedia camera:cameraView.previewView.camera previewView:cameraView.previewView intent:_signup ? TGCameraControllerSignupAvatarIntent : TGCameraControllerAvatarIntent]; + else + controller = [[TGCameraController alloc] initWithContext:[windowManager context] saveEditedPhotos:_saveEditedPhotos saveCapturedMedia:_saveCapturedMedia intent:_signup ? TGCameraControllerSignupAvatarIntent : TGCameraControllerAvatarIntent]; + controller.stickersContext = _stickersContext; + controller.shouldStoreCapturedAssets = true; + + TGCameraControllerWindow *controllerWindow = [[TGCameraControllerWindow alloc] initWithManager:windowManager parentController:_parentController contentController:controller]; + controllerWindow.hidden = false; + controllerWindow.clipsToBounds = true; + + if ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPhone) + controllerWindow.frame = CGRectMake(0, 0, screenSize.width, screenSize.height); + else + controllerWindow.frame = [_context fullscreenBounds]; + + bool standalone = true; + CGRect startFrame = CGRectMake(0, screenSize.height, screenSize.width, screenSize.height); + if (cameraView != nil) + { + standalone = false; + if ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPad) + startFrame = CGRectZero; + else + startFrame = [controller.view convertRect:cameraView.previewView.frame fromView:cameraView]; } - return CGRectZero; - }; - - controller.finishedTransitionOut = ^ - { - __strong TGAttachmentCameraView *strongCameraView = weakCameraView; - if (strongCameraView == nil) - return; + [cameraView detachPreviewView]; + [controller beginTransitionInFromRect:startFrame]; - [strongCameraView attachPreviewViewAnimated:true]; - }; - - controller.finishedWithPhoto = ^(__unused TGOverlayController *controller, UIImage *resultImage, __unused NSString *caption, __unused NSArray *entities, __unused NSArray *stickers, __unused NSNumber *timer) - { - __strong TGMediaAvatarMenuMixin *strongSelf = weakSelf; - if (strongSelf == nil) - return; + __weak TGMediaAvatarMenuMixin *weakSelf = self; + __weak TGCameraController *weakCameraController = controller; + __weak TGAttachmentCameraView *weakCameraView = cameraView; - if (strongSelf.didFinishWithImage != nil) - strongSelf.didFinishWithImage(resultImage); + controller.beginTransitionOut = ^CGRect + { + __strong TGCameraController *strongCameraController = weakCameraController; + if (strongCameraController == nil) + return CGRectZero; + + __strong TGAttachmentCameraView *strongCameraView = weakCameraView; + if (strongCameraView != nil) + { + [strongCameraView willAttachPreviewView]; + if ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPad) + return CGRectZero; + + return [strongCameraController.view convertRect:strongCameraView.frame fromView:strongCameraView.superview]; + } + + return CGRectZero; + }; - [menuController dismissAnimated:false]; - }; - - controller.finishedWithVideo = ^(__unused TGOverlayController *controller, NSURL *url, UIImage *previewImage, __unused NSTimeInterval duration, __unused CGSize dimensions, TGVideoEditAdjustments *adjustments, __unused NSString *caption, __unused NSArray *entities, __unused NSArray *stickers, __unused NSNumber *timer){ - __strong TGMediaAvatarMenuMixin *strongSelf = weakSelf; - if (strongSelf == nil) - return; + controller.finishedTransitionOut = ^ + { + __strong TGAttachmentCameraView *strongCameraView = weakCameraView; + if (strongCameraView == nil) + return; + + [strongCameraView attachPreviewViewAnimated:true]; + }; - if (strongSelf.didFinishWithVideo != nil) - strongSelf.didFinishWithVideo(previewImage, [[AVURLAsset alloc] initWithURL:url options:nil], adjustments); + controller.finishedWithPhoto = ^(__unused TGOverlayController *controller, UIImage *resultImage, __unused NSString *caption, __unused NSArray *entities, __unused NSArray *stickers, __unused NSNumber *timer) + { + __strong TGMediaAvatarMenuMixin *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + if (strongSelf.didFinishWithImage != nil) + strongSelf.didFinishWithImage(resultImage); + + [menuController dismissAnimated:false]; + }; - [menuController dismissAnimated:false]; - }; + controller.finishedWithVideo = ^(__unused TGOverlayController *controller, NSURL *url, UIImage *previewImage, __unused NSTimeInterval duration, __unused CGSize dimensions, TGVideoEditAdjustments *adjustments, __unused NSString *caption, __unused NSArray *entities, __unused NSArray *stickers, __unused NSNumber *timer){ + __strong TGMediaAvatarMenuMixin *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + if (strongSelf.didFinishWithVideo != nil) + strongSelf.didFinishWithVideo(previewImage, [[AVURLAsset alloc] initWithURL:url options:nil], adjustments); + + [menuController dismissAnimated:false]; + }; + } alertDismissCompletion:nil]; } - (void)_displayMediaPicker diff --git a/submodules/LegacyComponents/Sources/TGMediaEditingContext.m b/submodules/LegacyComponents/Sources/TGMediaEditingContext.m index 97970434ef..367eeaf715 100644 --- a/submodules/LegacyComponents/Sources/TGMediaEditingContext.m +++ b/submodules/LegacyComponents/Sources/TGMediaEditingContext.m @@ -185,7 +185,10 @@ - (void)cleanup { - [_diskCache cleanup]; + TGModernCache *diskCache = _diskCache; + TGDispatchAfter(10.0, dispatch_get_main_queue(), ^{ + [diskCache cleanup]; + }); [[NSFileManager defaultManager] removeItemAtPath:_fullSizeResultsUrl.path error:nil]; [[NSFileManager defaultManager] removeItemAtPath:_paintingImagesUrl.path error:nil]; @@ -991,7 +994,7 @@ + (NSUInteger)diskMemoryLimit { - return 64 * 1024 * 1024; + return 512 * 1024 * 1024; } + (NSUInteger)imageSoftMemoryLimit diff --git a/submodules/LegacyComponents/Sources/TGMediaPickerCaptionInputPanel.m b/submodules/LegacyComponents/Sources/TGMediaPickerCaptionInputPanel.m index 9f861df438..c253a45589 100644 --- a/submodules/LegacyComponents/Sources/TGMediaPickerCaptionInputPanel.m +++ b/submodules/LegacyComponents/Sources/TGMediaPickerCaptionInputPanel.m @@ -252,7 +252,7 @@ static void setViewFrame(UIView *view, CGRect frame) } if (_inputField.internalTextView.isFirstResponder) - [TGHacks applyCurrentKeyboardAutocorrectionVariant]; + [TGHacks applyCurrentKeyboardAutocorrectionVariant:_inputField.internalTextView]; NSMutableAttributedString *text = [[NSMutableAttributedString alloc] initWithAttributedString:_inputField.text == nil ? [[NSAttributedString alloc] initWithString:@""] : _inputField.attributedText]; NSMutableString *usualString = [text.string mutableCopy]; @@ -1028,13 +1028,19 @@ static void setViewFrame(UIView *view, CGRect frame) _associatedPanel.frame = associatedPanelFrame; } - UIEdgeInsets inputFieldInsets = [self _inputFieldInsets]; + UIEdgeInsets visibleInputFieldInsets = [self _inputFieldInsets]; if (self.isFirstResponder) { - inputFieldInsets.right += 41.0; + visibleInputFieldInsets.right += 41.0; } + UIEdgeInsets actualInputFieldInsets = [self _inputFieldInsets]; + actualInputFieldInsets.right += 41.0; CGFloat inputContainerHeight = [self heightForInputFieldHeight:self.isFirstResponder ? _inputField.frame.size.height : 0]; - setViewFrame(_fieldBackground, CGRectMake(inputFieldInsets.left, inputFieldInsets.top, frame.size.width - inputFieldInsets.left - inputFieldInsets.right, inputContainerHeight - inputFieldInsets.top - inputFieldInsets.bottom)); + CGRect fieldBackgroundFrame = CGRectMake(visibleInputFieldInsets.left, visibleInputFieldInsets.top, frame.size.width - visibleInputFieldInsets.left - visibleInputFieldInsets.right, inputContainerHeight - visibleInputFieldInsets.top - visibleInputFieldInsets.bottom); + + CGRect actualFieldBackgroundFrame = CGRectMake(actualInputFieldInsets.left, actualInputFieldInsets.top, frame.size.width - actualInputFieldInsets.left - actualInputFieldInsets.right, inputContainerHeight - actualInputFieldInsets.top - actualInputFieldInsets.bottom); + + setViewFrame(_fieldBackground, fieldBackgroundFrame); UIEdgeInsets inputFieldInternalEdgeInsets = [self _inputFieldInternalEdgeInsets]; CGRect onelineFrame = _fieldBackground.frame; @@ -1049,7 +1055,7 @@ static void setViewFrame(UIView *view, CGRect frame) placeholderFrame.origin.x = onelineFrame.origin.x; setViewFrame(_placeholderLabel, placeholderFrame); - CGRect inputFieldClippingFrame = _fieldBackground.frame; + CGRect inputFieldClippingFrame = actualFieldBackgroundFrame; setViewFrame(_inputFieldClippingContainer, inputFieldClippingFrame); CGFloat inputFieldWidth = _inputFieldClippingContainer.frame.size.width - inputFieldInternalEdgeInsets.left - 36; diff --git a/submodules/LegacyComponents/Sources/TGMediaPickerGalleryInterfaceView.m b/submodules/LegacyComponents/Sources/TGMediaPickerGalleryInterfaceView.m index a9b1e07199..f5cd71d600 100644 --- a/submodules/LegacyComponents/Sources/TGMediaPickerGalleryInterfaceView.m +++ b/submodules/LegacyComponents/Sources/TGMediaPickerGalleryInterfaceView.m @@ -108,6 +108,8 @@ self = [super initWithFrame:CGRectZero]; if (self != nil) { + [[LegacyComponentsGlobals provider] makeViewDisableInteractiveKeyboardGestureRecognizer:self]; + _actionHandle = [[ASHandle alloc] initWithDelegate:self releaseOnMainThread:true]; _context = context; diff --git a/submodules/LegacyComponents/Sources/TGMediaPickerModernGalleryMixin.m b/submodules/LegacyComponents/Sources/TGMediaPickerModernGalleryMixin.m index 1c66ba5015..702201f77c 100644 --- a/submodules/LegacyComponents/Sources/TGMediaPickerModernGalleryMixin.m +++ b/submodules/LegacyComponents/Sources/TGMediaPickerModernGalleryMixin.m @@ -330,8 +330,13 @@ - (void)updateWithFetchResult:(TGMediaAssetFetchResult *)fetchResult { TGMediaAsset *currentAsset = ((TGMediaPickerGalleryItem *)_galleryController.currentItem).asset; - bool exists = ([fetchResult indexOfAsset:currentAsset] != NSNotFound); + bool exists; + if ([currentAsset isKindOfClass:[TGCameraCapturedVideo class]]) { + exists = [fetchResult indexOfAsset:((TGCameraCapturedVideo *)currentAsset).originalAsset] != NSNotFound; + } else { + exists = ([fetchResult indexOfAsset:currentAsset] != NSNotFound); + } if (!exists) { _galleryModel.dismiss(true, false); diff --git a/submodules/LegacyComponents/Sources/TGMediaVideoConverter.m b/submodules/LegacyComponents/Sources/TGMediaVideoConverter.m index 35ecd6a849..65b20a3dd2 100644 --- a/submodules/LegacyComponents/Sources/TGMediaVideoConverter.m +++ b/submodules/LegacyComponents/Sources/TGMediaVideoConverter.m @@ -362,14 +362,19 @@ AVMutableCompositionTrack *compositionTrack = [composition addMutableTrackWithMediaType:AVMediaTypeVideo preferredTrackID:kCMPersistentTrackID_Invalid]; [compositionTrack insertTimeRange:timeRange ofTrack:videoTrack atTime:kCMTimeZero error:NULL]; - CMTime frameDuration = CMTimeMake(1, 30); + CMTime frameDuration30FPS = CMTimeMake(1, 30); + CMTime frameDuration = frameDuration30FPS; if (videoTrack.nominalFrameRate > 0) frameDuration = CMTimeMake(1, (int32_t)videoTrack.nominalFrameRate); else if (CMTimeCompare(videoTrack.minFrameDuration, kCMTimeZero) == 1) frameDuration = videoTrack.minFrameDuration; if (CMTimeCompare(frameDuration, kCMTimeZero) != 1 || !CMTIME_IS_VALID(frameDuration) || image != nil || entityRenderer != nil || adjustments.toolsApplied) - frameDuration = CMTimeMake(1, 30); + frameDuration = frameDuration30FPS; + + if (CMTimeCompare(frameDuration, frameDuration30FPS)) { + frameDuration = frameDuration30FPS; + } NSInteger fps = (NSInteger)(1.0 / CMTimeGetSeconds(frameDuration)); @@ -889,7 +894,7 @@ + (NSURL *)_randomTemporaryURL { - return [NSURL fileURLWithPath:[NSTemporaryDirectory() stringByAppendingPathComponent:[[NSString alloc] initWithFormat:@"%x.tmp", (int)arc4random()]]]; + return [NSURL fileURLWithPath:[NSTemporaryDirectory() stringByAppendingPathComponent:[[NSString alloc] initWithFormat:@"%x.mp4", (int)arc4random()]]]; } + (NSUInteger)estimatedSizeForPreset:(TGMediaVideoConversionPreset)preset duration:(NSTimeInterval)duration hasAudio:(bool)hasAudio @@ -1320,7 +1325,8 @@ static CGFloat progressOfSampleBufferInTimeRange(CMSampleBufferRef sampleBuffer, { AVVideoAverageBitRateKey: @([self _videoBitrateKbpsForPreset:preset] * 1000), AVVideoCleanApertureKey: videoCleanApertureSettings, - AVVideoPixelAspectRatioKey: videoAspectRatioSettings + AVVideoPixelAspectRatioKey: videoAspectRatioSettings, + AVVideoExpectedSourceFrameRateKey: @30 }; NSDictionary *hdVideoProperties = @ @@ -1408,7 +1414,7 @@ static CGFloat progressOfSampleBufferInTimeRange(CMSampleBufferRef sampleBuffer, return 64; case TGMediaVideoConversionPresetVideoMessage: - return 32; + return 64; case TGMediaVideoConversionPresetAnimation: case TGMediaVideoConversionPresetProfile: diff --git a/submodules/LegacyComponents/Sources/TGMenuSheetController.m b/submodules/LegacyComponents/Sources/TGMenuSheetController.m index 66b125cefd..8e10f799dc 100644 --- a/submodules/LegacyComponents/Sources/TGMenuSheetController.m +++ b/submodules/LegacyComponents/Sources/TGMenuSheetController.m @@ -95,7 +95,9 @@ typedef enum _permittedArrowDirections = UIPopoverArrowDirectionDown; _requiuresDimView = true; - if (!dark && [[LegacyComponentsGlobals provider] respondsToSelector:@selector(menuSheetPallete)]) + if (dark && [[LegacyComponentsGlobals provider] respondsToSelector:@selector(darkMenuSheetPallete)]) + self.pallete = [[LegacyComponentsGlobals provider] darkMenuSheetPallete]; + else if (!dark && [[LegacyComponentsGlobals provider] respondsToSelector:@selector(menuSheetPallete)]) self.pallete = [[LegacyComponentsGlobals provider] menuSheetPallete]; self.wantsFullScreenLayout = true; diff --git a/submodules/LegacyComponents/Sources/TGModernConversationInputMicButton.m b/submodules/LegacyComponents/Sources/TGModernConversationInputMicButton.m index 34099398e4..0b7799d810 100644 --- a/submodules/LegacyComponents/Sources/TGModernConversationInputMicButton.m +++ b/submodules/LegacyComponents/Sources/TGModernConversationInputMicButton.m @@ -249,7 +249,7 @@ static const CGFloat outerCircleMinScale = innerCircleRadius / outerCircleRadius _innerCircleView.center = centerPoint; _outerCircleView.center = centerPoint; _decoration.center = centerPoint; - _innerIconWrapperView.center = centerPoint; + _innerIconWrapperView.center = CGPointMake(_decoration.frame.size.width / 2.0f, _decoration.frame.size.height / 2.0f); _lockPanelWrapperView.frame = CGRectMake(floor(centerPoint.x - _lockPanelWrapperView.frame.size.width / 2.0f), floor(centerPoint.y - 122.0f - _lockPanelWrapperView.frame.size.height / 2.0f), _lockPanelWrapperView.frame.size.width, _lockPanelWrapperView.frame.size.height); @@ -420,8 +420,8 @@ static const CGFloat outerCircleMinScale = innerCircleRadius / outerCircleRadius _innerIconWrapperView.alpha = 0.0f; _innerIconWrapperView.userInteractionEnabled = false; [_innerIconWrapperView addSubview:_innerIconView]; - - [[_presentation view] addSubview:_innerIconWrapperView]; + + [_decoration addSubview:_innerIconWrapperView]; if (_lock == nil) { _stopButton = [[TGModernButton alloc] initWithFrame:CGRectMake(0.0f, 0.0f, 40.0f, 40.0f)]; @@ -448,7 +448,7 @@ static const CGFloat outerCircleMinScale = innerCircleRadius / outerCircleRadius block(); dispatch_async(dispatch_get_main_queue(), block); - _innerIconWrapperView.transform = CGAffineTransformIdentity; + //_innerIconWrapperView.transform = CGAffineTransformIdentity; _innerCircleView.transform = CGAffineTransformMakeScale(0.2f, 0.2f); _outerCircleView.transform = CGAffineTransformMakeScale(0.2f, 0.2f); _decoration.transform = CGAffineTransformMakeScale(0.2f, 0.2f); @@ -515,11 +515,11 @@ static const CGFloat outerCircleMinScale = innerCircleRadius / outerCircleRadius _outerCircleView.transform = CGAffineTransformMakeScale(0.2f, 0.2f); if (toSmallSize) { _decoration.transform = CGAffineTransformConcat(CGAffineTransformMakeScale(0.33f, 0.33f), CGAffineTransformMakeTranslation(0, 2 - TGScreenPixel)); - _innerIconWrapperView.transform = CGAffineTransformConcat(CGAffineTransformMakeScale(0.492f, 0.492f), CGAffineTransformMakeTranslation(-TGScreenPixel, 1)); + //_innerIconWrapperView.transform = CGAffineTransformConcat(CGAffineTransformMakeScale(0.492f, 0.492f), CGAffineTransformMakeTranslation(-TGScreenPixel, 1)); } else { _decoration.transform = CGAffineTransformMakeScale(0.2f, 0.2f); _decoration.alpha = 0.0; - _innerIconWrapperView.transform = CGAffineTransformMakeScale(0.2f, 0.2f); + //_innerIconWrapperView.transform = CGAffineTransformMakeScale(0.2f, 0.2f); _innerIconWrapperView.alpha = 0.0f; } _innerCircleView.alpha = 0.0f; @@ -864,7 +864,7 @@ static const CGFloat outerCircleMinScale = innerCircleRadius / outerCircleRadius transform = CGAffineTransformTranslate(transform, _cancelTranslation, 0); _innerCircleView.transform = transform; - _innerIconWrapperView.transform = transform; + //_innerIconWrapperView.transform = transform; _decoration.transform = transform; } } diff --git a/submodules/LegacyComponents/Sources/TGNavigationController.m b/submodules/LegacyComponents/Sources/TGNavigationController.m index 440d7325f3..d3bb25fb1b 100644 --- a/submodules/LegacyComponents/Sources/TGNavigationController.m +++ b/submodules/LegacyComponents/Sources/TGNavigationController.m @@ -352,40 +352,6 @@ } } -- (void)setShowCallStatusBar:(bool)showCallStatusBar -{ - if (_showCallStatusBar == showCallStatusBar) - return; - - _showCallStatusBar = showCallStatusBar; - - int screenHeight = (int)TGScreenSize().height; - CGFloat statusBarHeight = (screenHeight == 812 || screenHeight == 896) ? 0.0f : 20.0f; - - _currentAdditionalStatusBarHeight = _showCallStatusBar ? statusBarHeight : 0.0f; - [(TGNavigationBar *)self.navigationBar setVerticalOffset:_currentAdditionalStatusBarHeight]; - - [UIView animateWithDuration:0.25 animations:^ - { - static SEL selector = NULL; - static void (*impl)(id, SEL) = NULL; - - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^ - { - selector = NSSelectorFromString(TGEncodeText(@"`vqebufCbstGpsDvssfouJoufsgbdfPsjfoubujpo", -1)); - Method method = class_getInstanceMethod([UINavigationController class], selector); - impl = (void (*)(id, SEL))method_getImplementation(method); - }); - - if (impl != NULL) - impl(self, selector); - - [self updateStatusBarOnControllers]; - }]; -} - - - (void)setupStatusBarOnControllers:(NSArray *)controllers { if ([[self navigationBar] isKindOfClass:[TGNavigationBar class]]) @@ -416,11 +382,6 @@ TGViewController *viewController = (TGViewController *)maybeController; [viewController setAdditionalStatusBarHeight:_currentAdditionalStatusBarHeight]; [viewController setNeedsStatusBarAppearanceUpdate]; - - if ([viewController.presentedViewController isKindOfClass:[TGNavigationController class]] && viewController.presentedViewController.modalPresentationStyle != UIModalPresentationPopover) - { - [(TGNavigationController *)viewController.presentedViewController setShowCallStatusBar:_showCallStatusBar]; - } } else if ([maybeController isKindOfClass:[UITabBarController class]] && [maybeController conformsToProtocol:@protocol(TGNavigationControllerTabsController)]) { @@ -438,54 +399,9 @@ } } -static UIView *findDimmingView(UIView *view) -{ - static NSString *encodedString = nil; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^ - { - encodedString = TGEncodeText(@"VJEjnnjohWjfx", -1); - }); - - if ([NSStringFromClass(view.class) isEqualToString:encodedString]) - return view; - - for (UIView *subview in view.subviews) - { - UIView *result = findDimmingView(subview); - if (result != nil) - return result; - } - - return nil; -} - - (void)viewDidAppear:(BOOL)animated { [super viewDidAppear:animated]; - - if (self.modalPresentationStyle == UIModalPresentationFormSheet) - { - UIView *dimmingView = findDimmingView(self.view.window); - bool tapSetup = false; - if (_dimmingTapRecognizer != nil) - { - for (UIGestureRecognizer *recognizer in dimmingView.gestureRecognizers) - { - if (recognizer == _dimmingTapRecognizer) - { - tapSetup = true; - break; - } - } - } - - if (!tapSetup) - { - _dimmingTapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(dimmingViewTapped:)]; - [dimmingView addGestureRecognizer:_dimmingTapRecognizer]; - } - } } - (void)dimmingViewTapped:(UITapGestureRecognizer *)recognizer @@ -931,74 +847,16 @@ TGNavigationController *findNavigationController() - (void)updateInteractiveTransition:(CGFloat)percentComplete { - TGNavigationController *navigationController = findNavigationController(); - if (navigationController != nil) - { - if (!navigationController.disableInteractiveKeyboardTransition && [TGHacks applicationKeyboardWindow] != nil && ![TGHacks applicationKeyboardWindow].hidden) - { - CGSize screenSize = [TGViewController screenSizeForInterfaceOrientation:navigationController.interfaceOrientation]; - CGFloat keyboardOffset = MAX(0.0f, percentComplete * screenSize.width); - - UIView *keyboardView = [TGHacks applicationKeyboardView]; - CGRect keyboardViewFrame = keyboardView.frame; - keyboardViewFrame.origin.x = keyboardOffset; - - keyboardView.frame = keyboardViewFrame; - } - } - [super updateInteractiveTransition:percentComplete]; } - (void)finishInteractiveTransition { - CGFloat value = self.percentComplete; - UIView *keyboardView = [TGHacks applicationKeyboardView]; - CGRect keyboardViewFrame = keyboardView.frame; - [super finishInteractiveTransition]; - - TGNavigationController *navigationController = findNavigationController(); - if (navigationController != nil) - { - if (!navigationController.disableInteractiveKeyboardTransition) - { - keyboardView.frame = keyboardViewFrame; - - CGSize screenSize = [TGViewController screenSizeForInterfaceOrientation:navigationController.interfaceOrientation]; - CGFloat keyboardOffset = 1.0f * screenSize.width; - - keyboardViewFrame.origin.x = keyboardOffset; - NSTimeInterval duration = (1.0 - value) * [navigationController myNominalTransitionAnimationDuration]; - [UIView animateWithDuration:duration delay:0 options:UIViewAnimationOptionCurveLinear animations:^ - { - keyboardView.frame = keyboardViewFrame; - } completion:nil]; - } - } } - (void)cancelInteractiveTransition { - CGFloat value = self.percentComplete; - - TGNavigationController *navigationController = findNavigationController(); - if (navigationController != nil) - { - if (!navigationController.disableInteractiveKeyboardTransition && [TGHacks applicationKeyboardWindow] != nil && ![TGHacks applicationKeyboardWindow].hidden) - { - UIView *keyboardView = [TGHacks applicationKeyboardView]; - CGRect keyboardViewFrame = keyboardView.frame; - keyboardViewFrame.origin.x = 0.0f; - - NSTimeInterval duration = value * [navigationController myNominalTransitionAnimationDuration]; - [UIView animateWithDuration:duration delay:0 options:UIViewAnimationOptionCurveLinear animations:^ - { - keyboardView.frame = keyboardViewFrame; - } completion:nil]; - } - } - [super cancelInteractiveTransition]; } diff --git a/submodules/LegacyComponents/Sources/TGOverlayControllerWindow.m b/submodules/LegacyComponents/Sources/TGOverlayControllerWindow.m index eb45317107..1d7d18f920 100644 --- a/submodules/LegacyComponents/Sources/TGOverlayControllerWindow.m +++ b/submodules/LegacyComponents/Sources/TGOverlayControllerWindow.m @@ -68,28 +68,7 @@ } - (BOOL)shouldAutorotate -{ - static NSArray *nonRotateableWindowClasses = nil; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^ - { - NSMutableArray *array = [[NSMutableArray alloc] init]; - Class alertClass = NSClassFromString(TGEncodeText(@"`VJBmfsuPwfsmbzXjoepx", -1)); - if (alertClass != nil) - [array addObject:alertClass]; - - nonRotateableWindowClasses = array; - }); - - for (UIWindow *window in [[LegacyComponentsGlobals provider] applicationWindows].reverseObjectEnumerator) - { - for (Class classInfo in nonRotateableWindowClasses) - { - if ([window isKindOfClass:classInfo]) - return false; - } - } - +{ UIViewController *rootController = [[LegacyComponentsGlobals provider] applicationWindows].firstObject.rootViewController; if (rootController.presentedViewController != nil) diff --git a/submodules/LegacyComponents/Sources/TGPassportAttachMenu.m b/submodules/LegacyComponents/Sources/TGPassportAttachMenu.m index f083293dec..6d168caa5b 100644 --- a/submodules/LegacyComponents/Sources/TGPassportAttachMenu.m +++ b/submodules/LegacyComponents/Sources/TGPassportAttachMenu.m @@ -66,7 +66,7 @@ [TGPassportAttachMenu _displayCameraWithView:cameraView menuController:strongController parentController:strongParentController context:context intent:intent uploadAction:uploadAction]; }; - carouselItem.sendPressed = ^(TGMediaAsset *currentItem, __unused bool asFiles, __unused bool silentPosting, __unused int32_t scheduleTime) + carouselItem.sendPressed = ^(TGMediaAsset *currentItem, __unused bool asFiles, __unused bool silentPosting, __unused int32_t scheduleTime, __unused bool fromPicker) { __strong TGMenuSheetController *strongController = weakController; if (strongController == nil) @@ -271,7 +271,7 @@ + (void)_displayCameraWithView:(TGAttachmentCameraView *)cameraView menuController:(TGMenuSheetController *)menuController parentController:(TGViewController *)parentController context:(id)context intent:(TGPassportAttachIntent)intent uploadAction:(void (^)(SSignal *, void (^)(void)))uploadAction { - if (![[[LegacyComponentsGlobals provider] accessChecker] checkCameraAuthorizationStatusForIntent:TGCameraAccessIntentDefault alertDismissCompletion:nil]) + if (![[[LegacyComponentsGlobals provider] accessChecker] checkCameraAuthorizationStatusForIntent:TGCameraAccessIntentDefault completion:^(BOOL allowed) { } alertDismissCompletion:nil]) return; if ([context currentlyInSplitView]) diff --git a/submodules/LegacyComponents/Sources/TGPhotoCropController.m b/submodules/LegacyComponents/Sources/TGPhotoCropController.m index 9e497a5ed4..e64c049c3d 100644 --- a/submodules/LegacyComponents/Sources/TGPhotoCropController.m +++ b/submodules/LegacyComponents/Sources/TGPhotoCropController.m @@ -502,13 +502,10 @@ NSString * const TGPhotoCropOriginalAspectRatio = @"original"; - (id)currentResultRepresentation { - if (_transitionOutView != nil && [_transitionOutView isKindOfClass:[UIImageView class]]) - { + if (_transitionOutView != nil && [_transitionOutView isKindOfClass:[UIImageView class]]) { return ((UIImageView *)_transitionOutView).image; - } - else - { - return [_cropView croppedImageWithMaxSize:CGSizeMake(750, 750)]; + } else { + return [_cropView croppedImageWithMaxSize:TGPhotoEditorScreenImageMaxSize()]; } } diff --git a/submodules/LegacyComponents/Sources/TGPhotoEditorController.m b/submodules/LegacyComponents/Sources/TGPhotoEditorController.m index cbf564209e..166715d56b 100644 --- a/submodules/LegacyComponents/Sources/TGPhotoEditorController.m +++ b/submodules/LegacyComponents/Sources/TGPhotoEditorController.m @@ -146,7 +146,6 @@ { _context = context; _actionHandle = [[ASHandle alloc] initWithDelegate:self releaseOnMainThread:true]; - _standaloneEditingContext = [[TGMediaEditingContext alloc] init]; self.automaticallyManageScrollViewInsets = false; self.autoManageStatusBarBackground = false; @@ -2182,10 +2181,14 @@ - (TGMediaEditingContext *)editingContext { - if (_editingContext) + if (_editingContext) { return _editingContext; - else + } else { + if (_standaloneEditingContext == nil) { + _standaloneEditingContext = [[TGMediaEditingContext alloc] init]; + } return _standaloneEditingContext; + } } - (void)doneButtonLongPressed:(UIButton *)sender diff --git a/submodules/LegacyComponents/Sources/TGPhotoEditorSliderView.m b/submodules/LegacyComponents/Sources/TGPhotoEditorSliderView.m index f5210889db..96f847a378 100644 --- a/submodules/LegacyComponents/Sources/TGPhotoEditorSliderView.m +++ b/submodules/LegacyComponents/Sources/TGPhotoEditorSliderView.m @@ -89,7 +89,7 @@ const CGFloat TGPhotoEditorSliderViewInternalMargin = 7.0f; - (void)setPositionsCount:(NSInteger)positionsCount { _positionsCount = positionsCount; - _tapGestureRecognizer.enabled = _positionsCount > 1; + _tapGestureRecognizer.enabled = !_disableSnapToPositions && _positionsCount > 1; _doubleTapGestureRecognizer.enabled = !_tapGestureRecognizer.enabled; } @@ -158,7 +158,6 @@ const CGFloat TGPhotoEditorSliderViewInternalMargin = 7.0f; knobFrame = CGRectMake(knobFrame.origin.y, knobFrame.origin.x, knobFrame.size.width, knobFrame.size.height); } - CGFloat markPosition = visualMargin + visualTotalLength / (_maximumValue - _minimumValue) * (ABS(_minimumValue) + _startValue); if (_markValue > FLT_EPSILON) { CGContextSetFillColorWithColor(context, _backColor.CGColor); @@ -174,60 +173,14 @@ const CGFloat TGPhotoEditorSliderViewInternalMargin = 7.0f; CGContextSetBlendMode(context, kCGBlendModeCopy); } - - if (false && _minimumUndottedValue > -1 && self.positionsCount > 1) { - CGContextSetLineWidth(context, backFrame.size.height); - CGContextSetLineCap(context, kCGLineCapRound); - - for (NSInteger i = 1; i < self.positionsCount; i++) - { - CGFloat previousX = margin + totalLength / (self.positionsCount - 1) * (i - 1); - CGFloat currentX = margin + totalLength / (self.positionsCount - 1) * i; - - if (_minimumUndottedValue < i) { - CGFloat normalDashWidth = 16.0f; - CGFloat dashFraction = 0.6f; - CGFloat totalLineWidth = currentX - previousX; - int numberOfDashes = (int)floor((double)(totalLineWidth / normalDashWidth)); - CGFloat dashWidth = (totalLineWidth / (CGFloat)numberOfDashes); - - CGFloat innerWidth = dashWidth * dashFraction - 2.0f; - CGFloat innerOffset = (dashWidth - innerWidth) / 2.0f; - - CGFloat dottedX = previousX; - - while (dottedX + innerWidth < currentX) { - bool highlighted = dottedX + dashWidth / 2.0f < CGRectGetMaxX(trackFrame); - - CGContextSetStrokeColorWithColor(context, highlighted ? _trackColor.CGColor : _backColor.CGColor); - - CGContextMoveToPoint(context, dottedX + innerOffset, CGRectGetMidY(backFrame)); - CGContextAddLineToPoint(context, dottedX + innerOffset + innerWidth, CGRectGetMidY(backFrame)); - CGContextStrokePath(context); - - dottedX += dashWidth; - } - } else { - bool highlighted = (previousX + (currentX - previousX) / 2.0f) < CGRectGetMaxX(trackFrame); - CGContextSetStrokeColorWithColor(context, highlighted ? _trackColor.CGColor : _backColor.CGColor); - - CGContextMoveToPoint(context, previousX, CGRectGetMidY(backFrame)); - CGContextAddLineToPoint(context, currentX, CGRectGetMidY(backFrame)); - CGContextStrokePath(context); - } - } - } else { - CGContextSetFillColorWithColor(context, _backColor.CGColor); - [self drawRectangle:backFrame cornerRadius:self.trackCornerRadius context:context]; - } + + CGContextSetFillColorWithColor(context, _backColor.CGColor); + [self drawRectangle:backFrame cornerRadius:self.trackCornerRadius context:context]; CGContextSetBlendMode(context, kCGBlendModeNormal); - - if (false && _minimumUndottedValue > -1) { - } else { - CGContextSetFillColorWithColor(context, _trackColor.CGColor); - [self drawRectangle:trackFrame cornerRadius:self.trackCornerRadius context:context]; - } + + CGContextSetFillColorWithColor(context, _trackColor.CGColor); + [self drawRectangle:trackFrame cornerRadius:self.trackCornerRadius context:context]; if (!_startHidden || self.displayEdges) { @@ -644,14 +597,14 @@ const CGFloat TGPhotoEditorSliderViewInternalMargin = 7.0f; totalLength -= _knobPadding * 2; CGFloat previousValue = self.value; - if (self.positionsCount > 1) + if (self.positionsCount > 1 && !self.disableSnapToPositions) { NSInteger position = (NSInteger)round((_knobDragCenter / totalLength) * (self.positionsCount - 1)); _knobDragCenter = position * totalLength / (self.positionsCount - 1); } [self setValue:[self valueForCenterPosition:_knobDragCenter totalLength:totalLength knobSize:_knobView.image.size.width vertical:vertical]]; - if (previousValue != self.value && (self.positionsCount > 1 || self.value == self.minimumValue || self.value == self.maximumValue || (self.minimumValue != self.startValue && self.value == self.startValue))) + if (previousValue != self.value && !self.disableSnapToPositions && (self.positionsCount > 1 || self.value == self.minimumValue || self.value == self.maximumValue || (self.minimumValue != self.startValue && self.value == self.startValue))) { [_feedbackGenerator selectionChanged]; [_feedbackGenerator prepare]; diff --git a/submodules/LegacyComponents/Sources/TGPhotoEditorUtils.m b/submodules/LegacyComponents/Sources/TGPhotoEditorUtils.m index 2cadc81c9e..fd5465eefa 100644 --- a/submodules/LegacyComponents/Sources/TGPhotoEditorUtils.m +++ b/submodules/LegacyComponents/Sources/TGPhotoEditorUtils.m @@ -7,14 +7,14 @@ #import const CGSize TGPhotoEditorResultImageMaxSize = { 1280, 1280 }; -const CGSize TGPhotoEditorScreenImageHardLimitSize = { 750, 750 }; +const CGSize TGPhotoEditorScreenImageHardLimitSize = { 1280, 1280 }; +const CGSize TGPhotoEditorScreenImageHardLimitLegacySize = { 750, 750 }; CGSize TGPhotoEditorScreenImageMaxSize() { CGSize screenSize = TGScreenSize(); - CGFloat maxSide = MIN(TGPhotoEditorScreenImageHardLimitSize.width, TGScreenScaling() * MIN(screenSize.width, screenSize.height)); - - return CGSizeMake(maxSide, maxSide); + CGSize limitSize = screenSize.width == 320 ? TGPhotoEditorScreenImageHardLimitLegacySize : TGPhotoEditorScreenImageHardLimitSize; + return limitSize; } CGSize TGPhotoThumbnailSizeForCurrentScreen() diff --git a/submodules/LegacyComponents/Sources/TGPhotoPaintController.m b/submodules/LegacyComponents/Sources/TGPhotoPaintController.m index c0ba0e2c91..fa503a47a3 100644 --- a/submodules/LegacyComponents/Sources/TGPhotoPaintController.m +++ b/submodules/LegacyComponents/Sources/TGPhotoPaintController.m @@ -1414,7 +1414,7 @@ const CGFloat TGPhotoPaintStickerKeyboardSize = 260.0f; - (CGFloat)_brushWeightForSize:(CGFloat)size { - return [self _brushBaseWeightForCurrentPainting] + [self _brushWeightRangeForCurrentPainting] * size; + return ([self _brushBaseWeightForCurrentPainting] + [self _brushWeightRangeForCurrentPainting] * size) / _scrollView.zoomScale; } + (CGSize)maximumPaintingSize @@ -1739,6 +1739,9 @@ const CGFloat TGPhotoPaintStickerKeyboardSize = 260.0f; { [self adjustZoom]; + TGPaintSwatch *currentSwatch = _portraitSettingsView.swatch; + [_canvasView setBrushWeight:[self _brushWeightForSize:currentSwatch.brushWeight]]; + if (_scrollView.zoomScale < _scrollView.normalZoomScale - FLT_EPSILON) { [TGHacks setAnimationDurationFactor:0.5f]; diff --git a/submodules/LegacyComponents/Sources/TGVideoCameraPipeline.h b/submodules/LegacyComponents/Sources/TGVideoCameraPipeline.h index 6d79b12348..796a49c812 100644 --- a/submodules/LegacyComponents/Sources/TGVideoCameraPipeline.h +++ b/submodules/LegacyComponents/Sources/TGVideoCameraPipeline.h @@ -15,13 +15,16 @@ @property (nonatomic, copy) void (^micLevel)(CGFloat); +@property (nonatomic, readonly) bool isZoomAvailable; +@property (nonatomic, assign) CGFloat zoomLevel; + - (instancetype)initWithDelegate:(id)delegate position:(AVCaptureDevicePosition)position callbackQueue:(dispatch_queue_t)queue liveUploadInterface:(id)liveUploadInterface; - (void)startRunning; - (void)stopRunning; - (void)startRecording:(NSURL *)url preset:(TGMediaVideoConversionPreset)preset liveUpload:(bool)liveUpload; -- (void)stopRecording:(void (^)())completed; +- (void)stopRecording:(void (^)(bool))completed; - (CGAffineTransform)transformForOrientation:(AVCaptureVideoOrientation)orientation; diff --git a/submodules/LegacyComponents/Sources/TGVideoCameraPipeline.m b/submodules/LegacyComponents/Sources/TGVideoCameraPipeline.m index 304099a130..fde3cd666c 100644 --- a/submodules/LegacyComponents/Sources/TGVideoCameraPipeline.m +++ b/submodules/LegacyComponents/Sources/TGVideoCameraPipeline.m @@ -135,7 +135,7 @@ const NSInteger TGVideoCameraRetainedBufferCount = 16; { _running = false; - [self stopRecording:^{}]; + [self stopRecording:^(__unused bool success) {}]; [_captureSession stopRunning]; [self captureSessionDidStopRunning]; @@ -300,7 +300,7 @@ const NSInteger TGVideoCameraRetainedBufferCount = 16; - (void)captureSessionDidStopRunning { - [self stopRecording:^{}]; + [self stopRecording:^(__unused bool success) {}]; [self destroyVideoPipeline]; } @@ -701,7 +701,7 @@ const NSInteger TGVideoCameraRetainedBufferCount = 16; [recorder prepareToRecord]; } -- (void)stopRecording:(void (^)())completed +- (void)stopRecording:(void (^)(bool))completed { [[TGVideoCameraPipeline cameraQueue] dispatch:^ { @@ -709,7 +709,7 @@ const NSInteger TGVideoCameraRetainedBufferCount = 16; { if (_recordingStatus != TGVideoCameraRecordingStatusRecording) { if (completed) { - completed(); + completed(false); } return; } @@ -721,7 +721,7 @@ const NSInteger TGVideoCameraRetainedBufferCount = 16; [_recorder finishRecording:^{ __unused __auto_type description = [self description]; if (completed) { - completed(); + completed(true); } }]; }]; @@ -864,6 +864,52 @@ static CGFloat angleOffsetFromPortraitOrientationToOrientation(AVCaptureVideoOri return _recorder.videoDuration; } +- (CGFloat)zoomLevel +{ + if (![_videoDevice respondsToSelector:@selector(videoZoomFactor)]) + return 1.0f; + + return (_videoDevice.videoZoomFactor - 1.0f) / ([self _maximumZoomFactor] - 1.0f); +} + +- (CGFloat)_maximumZoomFactor +{ + return MIN(5.0f, _videoDevice.activeFormat.videoMaxZoomFactor); +} + +- (void)setZoomLevel:(CGFloat)zoomLevel +{ + zoomLevel = MAX(0.0f, MIN(1.0f, zoomLevel)); + + __weak TGVideoCameraPipeline *weakSelf = self; + [[TGVideoCameraPipeline cameraQueue] dispatch:^ + { + __strong TGVideoCameraPipeline *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + [self _reconfigureDevice:_videoDevice withBlock:^(AVCaptureDevice *device) { + device.videoZoomFactor = MAX(1.0f, MIN([strongSelf _maximumZoomFactor], 1.0f + ([strongSelf _maximumZoomFactor] - 1.0f) * zoomLevel)); + }]; + }]; +} + +- (bool)isZoomAvailable +{ + return [TGVideoCameraPipeline _isZoomAvailableForDevice:_videoDevice]; +} + ++ (bool)_isZoomAvailableForDevice:(AVCaptureDevice *)device +{ + if (![device respondsToSelector:@selector(setVideoZoomFactor:)]) + return false; + + if (device.position == AVCaptureDevicePositionFront) + return false; + + return true; +} + - (void)setCameraPosition:(AVCaptureDevicePosition)position { @synchronized (self) diff --git a/submodules/LegacyComponents/Sources/TGVideoMessageCaptureController.m b/submodules/LegacyComponents/Sources/TGVideoMessageCaptureController.m index cf05bf2cba..ea744f75fc 100644 --- a/submodules/LegacyComponents/Sources/TGVideoMessageCaptureController.m +++ b/submodules/LegacyComponents/Sources/TGVideoMessageCaptureController.m @@ -91,6 +91,8 @@ typedef enum TGVideoCameraGLView *_previewView; TGVideoMessageRingView *_ringView; + UIPinchGestureRecognizer *_pinchGestureRecognizer; + UIView *_separatorView; UIImageView *_placeholderView; @@ -344,7 +346,6 @@ typedef enum [_circleWrapperView addSubview:_ringView]; CGRect controlsFrame = _controlsFrame; -// controlsFrame.size.width = _wrapperView.frame.size.width; _controlsView = [[TGVideoMessageControls alloc] initWithFrame:controlsFrame assets:_assets slowmodeTimestamp:_slowmodeTimestamp slowmodeView:_slowmodeView]; _controlsView.pallete = self.pallete; @@ -417,12 +418,43 @@ typedef enum [self.view addSubview:_switchButton]; } + _pinchGestureRecognizer = [[UIPinchGestureRecognizer alloc] initWithTarget:self action:@selector(handlePinch:)]; + _pinchGestureRecognizer.delegate = self; + [self.view addGestureRecognizer:_pinchGestureRecognizer]; + void (^voidBlock)(void) = ^{}; _buttonHandler = [[PGCameraVolumeButtonHandler alloc] initWithUpButtonPressedBlock:voidBlock upButtonReleasedBlock:voidBlock downButtonPressedBlock:voidBlock downButtonReleasedBlock:voidBlock]; [self configureCamera]; } +- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer +{ + if (gestureRecognizer == _pinchGestureRecognizer) + return _capturePipeline.isZoomAvailable; + + return true; +} + +- (void)handlePinch:(UIPinchGestureRecognizer *)gestureRecognizer +{ + switch (gestureRecognizer.state) + { + case UIGestureRecognizerStateChanged: + { + CGFloat delta = (gestureRecognizer.scale - 1.0f) / 1.5f; + CGFloat value = MAX(0.0f, MIN(1.0f, _capturePipeline.zoomLevel + delta)); + + [_capturePipeline setZoomLevel:value]; + + gestureRecognizer.scale = 1.0f; + } + break; + default: + break; + } +} + - (TGVideoMessageTransitionType)_transitionType { static dispatch_once_t onceToken; @@ -658,9 +690,10 @@ typedef enum return; [_activityDisposable dispose]; - [self stopRecording:^{ + [self stopRecording:^() { TGDispatchOnMainThread(^{ - [self dismiss:false]; + //[self dismiss:false]; + [self description]; }); }]; } @@ -955,7 +988,20 @@ typedef enum - (void)stopRecording:(void (^)())completed { - [_capturePipeline stopRecording:completed]; + __weak TGVideoMessageCaptureController *weakSelf = self; + [_capturePipeline stopRecording:^(bool success) { + TGDispatchOnMainThread(^{ + __strong TGVideoMessageCaptureController *strongSelf = weakSelf; + if (strongSelf == nil) { + return; + } + if (!success) { + if (!strongSelf->_dismissed && strongSelf.finishedWithVideo != nil) { + strongSelf.finishedWithVideo(nil, nil, 0, 0.0, CGSizeZero, nil, nil, false, 0); + } + } + }); + }]; [_buttonHandler ignoreEventsFor:1.0f andDisable:true]; [_capturePipeline stopRunning]; } @@ -1015,10 +1061,14 @@ typedef enum } } - if (!_dismissed && self.finishedWithVideo != nil) + if (!_dismissed) { self.finishedWithVideo(url, image, fileSize, duration, dimensions, liveUploadData, adjustments, isSilent, scheduleTimestamp); - else + } else { [[NSFileManager defaultManager] removeItemAtURL:url error:NULL]; + if (self.finishedWithVideo != nil) { + self.finishedWithVideo(nil, nil, 0, 0.0, CGSizeZero, nil, nil, false, 0); + } + } } - (UIImageOrientation)orientationForThumbnailWithTransform:(CGAffineTransform)transform mirrored:(bool)mirrored @@ -1501,6 +1551,16 @@ static UIImage *startImage = nil; return CGSizeMake(240.0f, 240.0f); } +- (UIView *)extractVideoContent { + UIView *result = [_circleView snapshotViewAfterScreenUpdates:false]; + result.frame = [_circleView convertRect:_circleView.bounds toView:nil]; + return result; +} + +- (void)hideVideoContent { + _circleWrapperView.alpha = 0.02f; +} + @end diff --git a/submodules/LegacyComponents/Sources/TGVideoMessageControls.m b/submodules/LegacyComponents/Sources/TGVideoMessageControls.m index c4b6fc917c..d7e68bb320 100644 --- a/submodules/LegacyComponents/Sources/TGVideoMessageControls.m +++ b/submodules/LegacyComponents/Sources/TGVideoMessageControls.m @@ -445,7 +445,7 @@ static CGRect viewFrame(UIView *view) [UIView animateWithDuration:0.2 delay:0.0 options:UIViewAnimationOptionBeginFromCurrentState | animationCurveOption animations:^ { - CGAffineTransform transform = CGAffineTransformScale(transform, 0.25f, 0.25f); + CGAffineTransform transform = CGAffineTransformMakeScale(0.25, 0.25); _cancelButton.transform = transform; _cancelButton.alpha = 0.0f; } completion:nil]; diff --git a/submodules/LegacyComponents/Sources/TGVideoMessageScrubber.m b/submodules/LegacyComponents/Sources/TGVideoMessageScrubber.m index fb0efcbba8..66a0dd2036 100644 --- a/submodules/LegacyComponents/Sources/TGVideoMessageScrubber.m +++ b/submodules/LegacyComponents/Sources/TGVideoMessageScrubber.m @@ -70,6 +70,9 @@ typedef enum if (self != nil) { _allowsTrimming = true; + + self.clipsToBounds = true; + self.layer.cornerRadius = 16.0f; _wrapperView = [[UIControl alloc] initWithFrame:CGRectMake(0, 0, 0, 33)]; _wrapperView.hitTestEdgeInsets = UIEdgeInsetsMake(-5, -10, -5, -10); diff --git a/submodules/LegacyComponents/Sources/TGViewController.mm b/submodules/LegacyComponents/Sources/TGViewController.mm index 4ec8d72d20..0a35d3fb26 100644 --- a/submodules/LegacyComponents/Sources/TGViewController.mm +++ b/submodules/LegacyComponents/Sources/TGViewController.mm @@ -695,7 +695,7 @@ static id _defaultContext = nil; { float additionalKeyboardHeight = [self _keyboardAdditionalDeltaHeightWhenRotatingFrom:_viewControllerRotatingFromOrientation toOrientation:toInterfaceOrientation]; - CGFloat statusBarHeight = [TGHacks statusBarHeightForOrientation:toInterfaceOrientation]; + CGFloat statusBarHeight = [[UIApplication sharedApplication] statusBarFrame].size.height; [self _updateControllerInsetForOrientation:toInterfaceOrientation statusBarHeight:statusBarHeight keyboardHeight:[self _currentKeyboardHeight:toInterfaceOrientation] + additionalKeyboardHeight force:false notify:true]; } @@ -768,9 +768,6 @@ static id _defaultContext = nil; if ([self isViewLoaded] && !_viewControllerHasEverAppeared && ([self findFirstResponder:self.view] == nil && ![self willCaptureInputShortly])) return 0.0f; - if ([TGHacks isKeyboardVisible]) - return [TGHacks keyboardHeightForOrientation:orientation]; - return 0.0f; } @@ -1264,7 +1261,7 @@ static id _defaultContext = nil; if (navigationBarHidden != self.navigationController.navigationBarHidden) { CGFloat barHeight = [self navigationBarHeightForInterfaceOrientation:self.interfaceOrientation]; - CGFloat statusBarHeight = [TGHacks statusBarHeightForOrientation:self.interfaceOrientation]; + CGFloat statusBarHeight = [[UIApplication sharedApplication] statusBarFrame].size.height; if ([self shouldIgnoreStatusBarInOrientation:self.interfaceOrientation]) statusBarHeight = 0.0f; @@ -1435,13 +1432,6 @@ static id _defaultContext = nil; if (TGIsPad() && iosMajorVersion() >= 7) viewControllerToPresent.preferredContentSize = [self.navigationController preferredContentSize]; - if ([viewControllerToPresent isKindOfClass:[TGNavigationController class]]) - { - TGNavigationController *navController = (TGNavigationController *)self.navigationController; - if (navController.showCallStatusBar) - [(TGNavigationController *)viewControllerToPresent setShowCallStatusBar:true]; - } - if (iosMajorVersion() >= 8 && self.presentedViewController != nil && [self.presentedViewController isKindOfClass:[UIAlertController class]]) { dispatch_async(dispatch_get_main_queue(), ^ diff --git a/submodules/LegacyMediaPickerUI/Sources/LegacyAttachmentMenu.swift b/submodules/LegacyMediaPickerUI/Sources/LegacyAttachmentMenu.swift index 775c7fc6ca..d6ee786284 100644 --- a/submodules/LegacyMediaPickerUI/Sources/LegacyAttachmentMenu.swift +++ b/submodules/LegacyMediaPickerUI/Sources/LegacyAttachmentMenu.swift @@ -106,7 +106,10 @@ public func legacyMediaEditor(context: AccountContext, peer: Peer, media: AnyMed TGPhotoVideoEditor.present(with: legacyController.context, controller: emptyController, caption: initialCaption, entities: [], withItem: item, paint: true, recipientName: recipientName, stickersContext: paintStickersContext, snapshots: snapshots as? [Any], immediate: transitionCompletion != nil, appeared: { transitionCompletion?() }, completion: { result, editingContext in - let signals = TGCameraController.resultSignals(for: nil, editingContext: editingContext, currentItem: result as! TGMediaSelectableItem, storeAssets: false, saveEditedPhotos: false, descriptionGenerator: legacyAssetPickerItemGenerator()) + let nativeGenerator = legacyAssetPickerItemGenerator() + let signals = TGCameraController.resultSignals(for: nil, editingContext: editingContext, currentItem: result as! TGMediaSelectableItem, storeAssets: false, saveEditedPhotos: false, descriptionGenerator: { _1, _2, _3, _4 in + nativeGenerator(_1, _2, _3, _4, nil) + }) sendMessagesWithSignals(signals, false, 0) }, dismissed: { [weak legacyController] in legacyController?.dismiss() @@ -114,7 +117,7 @@ public func legacyMediaEditor(context: AccountContext, peer: Peer, media: AnyMed }) } -public func legacyAttachmentMenu(context: AccountContext, peer: Peer, chatLocation: ChatLocation, 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, presentTimerPicker: @escaping (@escaping (Int32) -> Void) -> Void, sendMessagesWithSignals: @escaping ([Any]?, Bool, Int32) -> Void, selectRecentlyUsedInlineBot: @escaping (Peer) -> Void, presentStickers: @escaping (@escaping (TelegramMediaFile, Bool, UIView, CGRect) -> Void) -> TGPhotoPaintStickersScreen?, present: @escaping (ViewController, Any?) -> Void) -> TGMenuSheetController { +public func legacyAttachmentMenu(context: AccountContext, peer: Peer, chatLocation: ChatLocation, 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, presentTimerPicker: @escaping (@escaping (Int32) -> Void) -> Void, sendMessagesWithSignals: @escaping ([Any]?, Bool, Int32, ((String) -> UIView?)?, @escaping () -> Void) -> Void, selectRecentlyUsedInlineBot: @escaping (Peer) -> Void, presentStickers: @escaping (@escaping (TelegramMediaFile, Bool, UIView, CGRect) -> Void) -> TGPhotoPaintStickersScreen?, present: @escaping (ViewController, Any?) -> Void) -> TGMenuSheetController { let defaultVideoPreset = defaultVideoPresetForContext(context) UserDefaults.standard.set(defaultVideoPreset.rawValue as NSNumber, forKey: "TG_preferredVideoPreset_v0") @@ -210,15 +213,21 @@ public func legacyAttachmentMenu(context: AccountContext, peer: Peer, chatLocati done?(time) } } - carouselItem.sendPressed = { [weak controller, weak carouselItem] currentItem, asFiles, silentPosting, scheduleTime in + carouselItem.sendPressed = { [weak controller, weak carouselItem] currentItem, asFiles, silentPosting, scheduleTime, isFromPicker in if let controller = controller, let carouselItem = carouselItem { let intent: TGMediaAssetsControllerIntent = asFiles ? TGMediaAssetsControllerSendFileIntent : TGMediaAssetsControllerSendMediaIntent let signals = TGMediaAssetsController.resultSignals(for: carouselItem.selectionContext, editingContext: carouselItem.editingContext, intent: intent, currentItem: currentItem, storeAssets: true, useMediaCache: false, descriptionGenerator: legacyAssetPickerItemGenerator(), saveEditedPhotos: saveEditedPhotos) if slowModeEnabled, let signals = signals, signals.count > 1 { presentCantSendMultipleFiles() } else { - controller.dismiss(animated: true) - sendMessagesWithSignals(signals, silentPosting, scheduleTime) + sendMessagesWithSignals(signals, silentPosting, scheduleTime, isFromPicker ? nil : { [weak carouselItem] uniqueId in + if let carouselItem = carouselItem { + return carouselItem.getItemSnapshot(uniqueId) + } + return nil + }, { [weak controller] in + controller?.dismiss(animated: true) + }) } } }; @@ -301,7 +310,7 @@ public func legacyAttachmentMenu(context: AccountContext, peer: Peer, chatLocati 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) + controller.pallete = legacyMenuPaletteFromTheme(presentationData.theme, forceDark: false) } }) legacyController.disposables.add(presentationDisposable) @@ -310,8 +319,11 @@ public func legacyAttachmentMenu(context: AccountContext, peer: Peer, chatLocati TGPhotoVideoEditor.present(with: legacyController.context, controller: emptyController, caption: "", entities: [], withItem: item, paint: false, recipientName: recipientName, stickersContext: paintStickersContext, snapshots: [], immediate: false, appeared: { }, completion: { result, editingContext in - let signals = TGCameraController.resultSignals(for: nil, editingContext: editingContext, currentItem: result as! TGMediaSelectableItem, storeAssets: false, saveEditedPhotos: false, descriptionGenerator: legacyAssetPickerItemGenerator()) - sendMessagesWithSignals(signals, false, 0) + let nativeGenerator = legacyAssetPickerItemGenerator() + let signals = TGCameraController.resultSignals(for: nil, editingContext: editingContext, currentItem: result as! TGMediaSelectableItem, storeAssets: false, saveEditedPhotos: false, descriptionGenerator: { _1, _2, _3, _4 in + nativeGenerator(_1, _2, _3, _4, nil) + }) + sendMessagesWithSignals(signals, false, 0, { _ in nil}, {}) }, dismissed: { [weak legacyController] in legacyController?.dismiss() }) @@ -378,9 +390,14 @@ public func legacyAttachmentMenu(context: AccountContext, peer: Peer, chatLocati return controller } -public func legacyMenuPaletteFromTheme(_ theme: PresentationTheme) -> TGMenuSheetPallete { - let sheetTheme = theme.actionSheet - return TGMenuSheetPallete(dark: theme.overallDarkAppearance, backgroundColor: sheetTheme.opaqueItemBackgroundColor, selectionColor: sheetTheme.opaqueItemHighlightedBackgroundColor, separatorColor: sheetTheme.opaqueItemSeparatorColor, accentColor: sheetTheme.controlAccentColor, destructiveColor: sheetTheme.destructiveActionTextColor, textColor: sheetTheme.primaryTextColor, secondaryTextColor: sheetTheme.secondaryTextColor, spinnerColor: sheetTheme.secondaryTextColor, badgeTextColor: sheetTheme.controlAccentColor, badgeImage: nil, cornersImage: generateStretchableFilledCircleImage(diameter: 11.0, color: nil, strokeColor: nil, strokeWidth: nil, backgroundColor: sheetTheme.opaqueItemBackgroundColor)) +public func legacyMenuPaletteFromTheme(_ theme: PresentationTheme, forceDark: Bool) -> TGMenuSheetPallete { + let sheetTheme: PresentationThemeActionSheet + if forceDark && !theme.overallDarkAppearance { + sheetTheme = defaultDarkColorPresentationTheme.actionSheet + } else { + sheetTheme = theme.actionSheet + } + return TGMenuSheetPallete(dark: forceDark || theme.overallDarkAppearance, backgroundColor: sheetTheme.opaqueItemBackgroundColor, selectionColor: sheetTheme.opaqueItemHighlightedBackgroundColor, separatorColor: sheetTheme.opaqueItemSeparatorColor, accentColor: sheetTheme.controlAccentColor, destructiveColor: sheetTheme.destructiveActionTextColor, textColor: sheetTheme.primaryTextColor, secondaryTextColor: sheetTheme.secondaryTextColor, spinnerColor: sheetTheme.secondaryTextColor, badgeTextColor: sheetTheme.controlAccentColor, badgeImage: nil, cornersImage: generateStretchableFilledCircleImage(diameter: 11.0, color: nil, strokeColor: nil, strokeWidth: nil, backgroundColor: sheetTheme.opaqueItemBackgroundColor)) } public func presentLegacyPasteMenu(context: AccountContext, peer: Peer, chatLocation: ChatLocation, saveEditedPhotos: Bool, allowGrouping: Bool, hasSchedule: Bool, presentationData: PresentationData, images: [UIImage], presentSchedulePicker: @escaping (@escaping (Int32) -> Void) -> Void, presentTimerPicker: @escaping (@escaping (Int32) -> Void) -> Void, sendMessagesWithSignals: @escaping ([Any]?, Bool, Int32) -> Void, presentStickers: @escaping (@escaping (TelegramMediaFile, Bool, UIView, CGRect) -> Void) -> TGPhotoPaintStickersScreen?, present: (ViewController, Any?) -> Void, initialLayout: ContainerViewLayout? = nil) -> ViewController { @@ -430,7 +447,10 @@ public func presentLegacyPasteMenu(context: AccountContext, peer: Peer, chatLoca done?(time) } }, completed: { selectionContext, editingContext, currentItem, silentPosting, scheduleTime in - let signals = TGClipboardMenu.resultSignals(for: selectionContext, editingContext: editingContext, currentItem: currentItem, descriptionGenerator: legacyAssetPickerItemGenerator()) + let nativeGenerator = legacyAssetPickerItemGenerator() + let signals = TGClipboardMenu.resultSignals(for: selectionContext, editingContext: editingContext, currentItem: currentItem, descriptionGenerator: { _1, _2, _3, _4 in + nativeGenerator(_1, _2, _3, _4, nil) + }) sendMessagesWithSignals(signals, silentPosting, scheduleTime) }, dismissed: { [weak legacyController] in legacyController?.dismiss() @@ -441,7 +461,7 @@ public func presentLegacyPasteMenu(context: AccountContext, peer: Peer, chatLoca 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) + controller.pallete = legacyMenuPaletteFromTheme(presentationData.theme, forceDark: false) } }) legacyController.disposables.add(presentationDisposable) diff --git a/submodules/LegacyMediaPickerUI/Sources/LegacyLiveUploadInterface.swift b/submodules/LegacyMediaPickerUI/Sources/LegacyLiveUploadInterface.swift index f07d6629c0..dd9295eceb 100644 --- a/submodules/LegacyMediaPickerUI/Sources/LegacyLiveUploadInterface.swift +++ b/submodules/LegacyMediaPickerUI/Sources/LegacyLiveUploadInterface.swift @@ -5,6 +5,7 @@ import TelegramCore import SyncCore import LegacyComponents import SwiftSignalKit +import AccountContext public class VideoConversionWatcher: TGMediaVideoFileWatcher { private let update: (String, Int) -> Void @@ -44,7 +45,7 @@ public final class LegacyLiveUploadInterfaceResult: NSObject { } public final class LegacyLiveUploadInterface: VideoConversionWatcher, TGLiveUploadInterface { - private let account: Account + private let context: AccountContext private let id: Int64 private var path: String? private var size: Int? @@ -52,9 +53,9 @@ public final class LegacyLiveUploadInterface: VideoConversionWatcher, TGLiveUplo private let data = Promise() private let dataValue = Atomic(value: nil) - public init(account: Account) { - self.account = account - self.id = arc4random64() + public init(context: AccountContext) { + self.context = context + self.id = Int64.random(in: Int64.min ... Int64.max) var updateImpl: ((String, Int) -> Void)? super.init(update: { path, size in @@ -65,7 +66,7 @@ public final class LegacyLiveUploadInterface: VideoConversionWatcher, TGLiveUplo if let strongSelf = self { if strongSelf.path == nil { strongSelf.path = path - strongSelf.account.messageMediaPreuploadManager.add(network: strongSelf.account.network, postbox: strongSelf.account.postbox, id: strongSelf.id, encrypt: false, tag: nil, source: strongSelf.data.get()) + strongSelf.context.engine.resources.preUpload(id: strongSelf.id, encrypt: false, tag: nil, source: strongSelf.data.get()) } strongSelf.size = size diff --git a/submodules/LegacyMediaPickerUI/Sources/LegacyMediaPickers.swift b/submodules/LegacyMediaPickerUI/Sources/LegacyMediaPickers.swift index 68fd2ba850..9860b34a9f 100644 --- a/submodules/LegacyMediaPickerUI/Sources/LegacyMediaPickers.swift +++ b/submodules/LegacyMediaPickerUI/Sources/LegacyMediaPickers.swift @@ -20,8 +20,6 @@ public func guessMimeTypeByFileExtension(_ ext: String) -> String { } public func configureLegacyAssetPicker(_ controller: TGMediaAssetsController, context: AccountContext, peer: Peer, chatLocation: ChatLocation, captionsEnabled: Bool = true, storeCreatedAssets: Bool = true, showFileTooltip: Bool = false, initialCaption: String, hasSchedule: Bool, presentWebSearch: (() -> Void)?, presentSelectionLimitExceeded: @escaping () -> Void, presentSchedulePicker: @escaping (@escaping (Int32) -> Void) -> Void, presentTimerPicker: @escaping (@escaping (Int32) -> Void) -> Void, presentStickers: @escaping (@escaping (TelegramMediaFile, Bool, UIView, CGRect) -> Void) -> TGPhotoPaintStickersScreen?) { - let isSecretChat = peer.id.namespace == Namespaces.Peer.SecretChat - let paintStickersContext = LegacyPaintStickersContext(context: context) paintStickersContext.presentStickersController = { completion in return presentStickers({ file, animated, view, rect in @@ -67,7 +65,7 @@ public func configureLegacyAssetPicker(_ controller: TGMediaAssetsController, co } public func legacyAssetPicker(context: AccountContext, presentationData: PresentationData, editingMedia: Bool, fileMode: Bool, peer: Peer?, saveEditedPhotos: Bool, allowGrouping: Bool, selectionLimit: Int) -> Signal<(LegacyComponentsContext) -> TGMediaAssetsController, Void> { - let isSecretChat = (peer?.id.namespace ?? 0) == Namespaces.Peer.SecretChat + let isSecretChat = (peer?.id.namespace._internalGetInt32Value() ?? 0) == Namespaces.Peer.SecretChat._internalGetInt32Value() return Signal { subscriber in let intent = fileMode ? TGMediaAssetsControllerSendFileIntent : TGMediaAssetsControllerSendMediaIntent @@ -130,18 +128,20 @@ private final class LegacyAssetItemWrapper: NSObject { let item: LegacyAssetItem let timer: Int? let groupedId: Int64? + let uniqueId: String? - init(item: LegacyAssetItem, timer: Int?, groupedId: Int64?) { + init(item: LegacyAssetItem, timer: Int?, groupedId: Int64?, uniqueId: String?) { self.item = item self.timer = timer self.groupedId = groupedId + self.uniqueId = uniqueId super.init() } } -public func legacyAssetPickerItemGenerator() -> ((Any?, String?, [Any]?, String?) -> [AnyHashable : Any]?) { - return { anyDict, caption, entities, hash in +public func legacyAssetPickerItemGenerator() -> ((Any?, String?, [Any]?, String?, String?) -> [AnyHashable : Any]?) { + return { anyDict, caption, entities, hash, uniqueId in let dict = anyDict as! NSDictionary let stickers = (dict["stickers"] as? [Data])?.compactMap { data -> FileMediaReference? in let decoder = PostboxDecoder(buffer: MemoryBuffer(data: data)) @@ -160,10 +160,10 @@ public func legacyAssetPickerItemGenerator() -> ((Any?, String?, [Any]?, String? let url: String? = (dict["url"] as? String) ?? (dict["url"] as? URL)?.path if let url = url { let dimensions = image.size - result["item" as NSString] = LegacyAssetItemWrapper(item: .video(data: .tempFile(path: url, dimensions: dimensions, duration: 4.0), thumbnail: thumbnail, adjustments: dict["adjustments"] as? TGVideoEditAdjustments, caption: caption, asFile: false, asAnimation: true, stickers: stickers), timer: (dict["timer"] as? NSNumber)?.intValue, groupedId: (dict["groupedId"] as? NSNumber)?.int64Value) + result["item" as NSString] = LegacyAssetItemWrapper(item: .video(data: .tempFile(path: url, dimensions: dimensions, duration: 4.0), thumbnail: thumbnail, adjustments: dict["adjustments"] as? TGVideoEditAdjustments, caption: caption, asFile: false, asAnimation: true, stickers: stickers), timer: (dict["timer"] as? NSNumber)?.intValue, groupedId: (dict["groupedId"] as? NSNumber)?.int64Value, uniqueId: uniqueId) } } else { - 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) + 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, uniqueId: uniqueId) } return result } else if (dict["type"] as! NSString) == "cloudPhoto" { @@ -184,9 +184,9 @@ public func legacyAssetPickerItemGenerator() -> ((Any?, String?, [Any]?, String? name = customName } - result["item" as NSString] = LegacyAssetItemWrapper(item: .file(data: .asset(asset.backingAsset), thumbnail: thumbnail, mimeType: mimeType, name: name, caption: caption), timer: nil, groupedId: (dict["groupedId"] as? NSNumber)?.int64Value) + result["item" as NSString] = LegacyAssetItemWrapper(item: .file(data: .asset(asset.backingAsset), thumbnail: thumbnail, mimeType: mimeType, name: name, caption: caption), timer: nil, groupedId: (dict["groupedId"] as? NSNumber)?.int64Value, uniqueId: uniqueId) } else { - result["item" as NSString] = LegacyAssetItemWrapper(item: .image(data: .asset(asset.backingAsset), 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: .asset(asset.backingAsset), thumbnail: thumbnail, caption: caption, stickers: []), timer: (dict["timer"] as? NSNumber)?.intValue, groupedId: (dict["groupedId"] as? NSNumber)?.int64Value, uniqueId: uniqueId) } return result } else if (dict["type"] as! NSString) == "file" { @@ -207,12 +207,12 @@ public func legacyAssetPickerItemGenerator() -> ((Any?, String?, [Any]?, String? let dimensions = (dict["dimensions"]! as AnyObject).cgSizeValue! let duration = (dict["duration"]! as AnyObject).doubleValue! - result["item" as NSString] = LegacyAssetItemWrapper(item: .video(data: .tempFile(path: tempFileUrl.path, dimensions: dimensions, duration: duration), thumbnail: thumbnail, adjustments: nil, caption: caption, asFile: false, asAnimation: true, stickers: []), timer: (dict["timer"] as? NSNumber)?.intValue, groupedId: (dict["groupedId"] as? NSNumber)?.int64Value) + result["item" as NSString] = LegacyAssetItemWrapper(item: .video(data: .tempFile(path: tempFileUrl.path, dimensions: dimensions, duration: duration), thumbnail: thumbnail, adjustments: nil, caption: caption, asFile: false, asAnimation: true, stickers: []), timer: (dict["timer"] as? NSNumber)?.intValue, groupedId: (dict["groupedId"] as? NSNumber)?.int64Value, uniqueId: uniqueId) return result } var result: [AnyHashable: Any] = [:] - result["item" as NSString] = LegacyAssetItemWrapper(item: .file(data: .tempFile(tempFileUrl.path), thumbnail: thumbnail, mimeType: mimeType, name: name, caption: caption), timer: (dict["timer"] as? NSNumber)?.intValue, groupedId: (dict["groupedId"] as? NSNumber)?.int64Value) + result["item" as NSString] = LegacyAssetItemWrapper(item: .file(data: .tempFile(tempFileUrl.path), thumbnail: thumbnail, mimeType: mimeType, name: name, caption: caption), timer: (dict["timer"] as? NSNumber)?.intValue, groupedId: (dict["groupedId"] as? NSNumber)?.int64Value, uniqueId: uniqueId) return result } } else if (dict["type"] as! NSString) == "video" { @@ -224,13 +224,13 @@ public func legacyAssetPickerItemGenerator() -> ((Any?, String?, [Any]?, String? if let asset = dict["asset"] as? TGMediaAsset { var result: [AnyHashable: Any] = [:] - result["item" as NSString] = LegacyAssetItemWrapper(item: .video(data: .asset(asset), thumbnail: thumbnail, adjustments: dict["adjustments"] as? TGVideoEditAdjustments, caption: caption, asFile: asFile, asAnimation: false, stickers: stickers), timer: (dict["timer"] as? NSNumber)?.intValue, groupedId: (dict["groupedId"] as? NSNumber)?.int64Value) + result["item" as NSString] = LegacyAssetItemWrapper(item: .video(data: .asset(asset), thumbnail: thumbnail, adjustments: dict["adjustments"] as? TGVideoEditAdjustments, caption: caption, asFile: asFile, asAnimation: false, stickers: stickers), timer: (dict["timer"] as? NSNumber)?.intValue, groupedId: (dict["groupedId"] as? NSNumber)?.int64Value, uniqueId: uniqueId) return result } else if let url = (dict["url"] as? String) ?? (dict["url"] as? URL)?.absoluteString { let dimensions = (dict["dimensions"]! as AnyObject).cgSizeValue! let duration = (dict["duration"]! as AnyObject).doubleValue! var result: [AnyHashable: Any] = [:] - result["item" as NSString] = LegacyAssetItemWrapper(item: .video(data: .tempFile(path: url, dimensions: dimensions, duration: duration), thumbnail: thumbnail, adjustments: dict["adjustments"] as? TGVideoEditAdjustments, caption: caption, asFile: asFile, asAnimation: false, stickers: stickers), timer: (dict["timer"] as? NSNumber)?.intValue, groupedId: (dict["groupedId"] as? NSNumber)?.int64Value) + result["item" as NSString] = LegacyAssetItemWrapper(item: .video(data: .tempFile(path: url, dimensions: dimensions, duration: duration), thumbnail: thumbnail, adjustments: dict["adjustments"] as? TGVideoEditAdjustments, caption: caption, asFile: asFile, asAnimation: false, stickers: stickers), timer: (dict["timer"] as? NSNumber)?.intValue, groupedId: (dict["groupedId"] as? NSNumber)?.int64Value, uniqueId: uniqueId) return result } } else if (dict["type"] as! NSString) == "cameraVideo" { @@ -246,7 +246,7 @@ public func legacyAssetPickerItemGenerator() -> ((Any?, String?, [Any]?, String? let dimensions = previewImage.pixelSize() let duration = (dict["duration"]! as AnyObject).doubleValue! var result: [AnyHashable: Any] = [:] - result["item" as NSString] = LegacyAssetItemWrapper(item: .video(data: .tempFile(path: url, dimensions: dimensions, duration: duration), thumbnail: thumbnail, adjustments: dict["adjustments"] as? TGVideoEditAdjustments, caption: caption, asFile: asFile, asAnimation: false, stickers: stickers), timer: (dict["timer"] as? NSNumber)?.intValue, groupedId: (dict["groupedId"] as? NSNumber)?.int64Value) + result["item" as NSString] = LegacyAssetItemWrapper(item: .video(data: .tempFile(path: url, dimensions: dimensions, duration: duration), thumbnail: thumbnail, adjustments: dict["adjustments"] as? TGVideoEditAdjustments, caption: caption, asFile: asFile, asAnimation: false, stickers: stickers), timer: (dict["timer"] as? NSNumber)?.intValue, groupedId: (dict["groupedId"] as? NSNumber)?.int64Value, uniqueId: uniqueId) return result } } @@ -254,7 +254,7 @@ public func legacyAssetPickerItemGenerator() -> ((Any?, String?, [Any]?, String? } } -public func legacyEnqueueGifMessage(account: Account, data: Data) -> Signal { +public func legacyEnqueueGifMessage(account: Account, data: Data, correlationId: Int64? = nil) -> Signal { return Signal { subscriber in if let previewImage = UIImage(data: data) { let dimensions = previewImage.size @@ -263,9 +263,9 @@ public func legacyEnqueueGifMessage(account: Account, data: Data) -> Signal Signal Signal Signal runOn(Queue.concurrentDefaultQueue()) } -public func legacyEnqueueVideoMessage(account: Account, data: Data) -> Signal { +public func legacyEnqueueVideoMessage(account: Account, data: Data, correlationId: Int64? = nil) -> Signal { return Signal { subscriber in if let previewImage = UIImage(data: data) { let dimensions = previewImage.size @@ -305,9 +305,9 @@ public func legacyEnqueueVideoMessage(account: Account, data: Data) -> Signal Signal Signal Signal runOn(Queue.concurrentDefaultQueue()) } -public func legacyAssetPickerEnqueueMessages(account: Account, signals: [Any]) -> Signal<[EnqueueMessage], Void> { +public struct LegacyAssetPickerEnqueueMessage { + public var message: EnqueueMessage + public var uniqueId: String? +} + +public func legacyAssetPickerEnqueueMessages(account: Account, signals: [Any]) -> Signal<[LegacyAssetPickerEnqueueMessage], Void> { return Signal { subscriber in let disposable = SSignal.combineSignals(signals).start(next: { anyValues in - var messages: [EnqueueMessage] = [] + var messages: [LegacyAssetPickerEnqueueMessage] = [] outer: for item in (anyValues as! NSArray) { if let item = (item as? NSDictionary)?.object(forKey: "item") as? LegacyAssetItemWrapper { @@ -349,12 +354,12 @@ public func legacyAssetPickerEnqueueMessages(account: Account, signals: [Any]) - case let .image(data, thumbnail, caption, stickers): var representations: [TelegramMediaImageRepresentation] = [] if let thumbnail = thumbnail { - let resource = LocalFileMediaResource(fileId: arc4random64()) + let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) let thumbnailSize = thumbnail.size.aspectFitted(CGSize(width: 320.0, height: 320.0)) let thumbnailImage = TGScaleImageToPixelSize(thumbnail, thumbnailSize)! if let thumbnailData = thumbnailImage.jpegData(compressionQuality: 0.4) { account.postbox.mediaBox.storeResourceData(resource.id, data: thumbnailData) - representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(thumbnailSize), resource: resource, progressiveSizes: [])) + representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(thumbnailSize), resource: resource, progressiveSizes: [], immediateThumbnailData: nil)) } } switch data { @@ -368,7 +373,7 @@ public func legacyAssetPickerEnqueueMessages(account: Account, signals: [Any]) - let _ = try? scaledImageData.write(to: URL(fileURLWithPath: tempFilePath)) let resource = LocalFileReferenceMediaResource(localFilePath: tempFilePath, randomId: randomId) - representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(scaledSize), resource: resource, progressiveSizes: [])) + representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(scaledSize), resource: resource, progressiveSizes: [], immediateThumbnailData: nil)) var imageFlags: TelegramMediaImageFlags = [] @@ -391,7 +396,7 @@ public func legacyAssetPickerEnqueueMessages(account: Account, signals: [Any]) - } var text = caption ?? "" - messages.append(.message(text: text, attributes: attributes, mediaReference: .standalone(media: media), replyToMessageId: nil, localGroupingKey: item.groupedId)) + messages.append(LegacyAssetPickerEnqueueMessage(message: .message(text: text, attributes: attributes, mediaReference: .standalone(media: media), replyToMessageId: nil, localGroupingKey: item.groupedId, correlationId: nil), uniqueId: item.uniqueId)) } } case let .asset(asset): @@ -399,15 +404,15 @@ public func legacyAssetPickerEnqueueMessages(account: Account, signals: [Any]) - arc4random_buf(&randomId, 8) let size = CGSize(width: CGFloat(asset.pixelWidth), height: CGFloat(asset.pixelHeight)) let scaledSize = size.aspectFittedOrSmaller(CGSize(width: 1280.0, height: 1280.0)) - let resource = PhotoLibraryMediaResource(localIdentifier: asset.localIdentifier, uniqueId: arc4random64()) - representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(scaledSize), resource: resource, progressiveSizes: [])) + let resource = PhotoLibraryMediaResource(localIdentifier: asset.localIdentifier, uniqueId: Int64.random(in: Int64.min ... Int64.max)) + representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(scaledSize), resource: resource, progressiveSizes: [], immediateThumbnailData: 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)) } - messages.append(.message(text: caption ?? "", attributes: attributes, mediaReference: .standalone(media: media), replyToMessageId: nil, localGroupingKey: item.groupedId)) + messages.append(LegacyAssetPickerEnqueueMessage(message: .message(text: caption ?? "", attributes: attributes, mediaReference: .standalone(media: media), replyToMessageId: nil, localGroupingKey: item.groupedId, correlationId: nil), uniqueId: item.uniqueId)) case .tempFile: break } @@ -418,13 +423,13 @@ public func legacyAssetPickerEnqueueMessages(account: Account, signals: [Any]) - arc4random_buf(&randomId, 8) let resource = LocalFileReferenceMediaResource(localFilePath: path, randomId: randomId) let media = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: randomId), partialReference: nil, resource: resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: mimeType, size: nil, attributes: [.FileName(fileName: name)]) - messages.append(.message(text: caption ?? "", attributes: [], mediaReference: .standalone(media: media), replyToMessageId: nil, localGroupingKey: item.groupedId)) + messages.append(LegacyAssetPickerEnqueueMessage(message: .message(text: caption ?? "", attributes: [], mediaReference: .standalone(media: media), replyToMessageId: nil, localGroupingKey: item.groupedId, correlationId: nil), uniqueId: item.uniqueId)) case let .asset(asset): var randomId: Int64 = 0 arc4random_buf(&randomId, 8) - let resource = PhotoLibraryMediaResource(localIdentifier: asset.localIdentifier, uniqueId: arc4random64()) + let resource = PhotoLibraryMediaResource(localIdentifier: asset.localIdentifier, uniqueId: Int64.random(in: Int64.min ... Int64.max)) let media = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: randomId), partialReference: nil, resource: resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: mimeType, size: nil, attributes: [.FileName(fileName: name)]) - messages.append(.message(text: caption ?? "", attributes: [], mediaReference: .standalone(media: media), replyToMessageId: nil, localGroupingKey: item.groupedId)) + messages.append(LegacyAssetPickerEnqueueMessage(message: .message(text: caption ?? "", attributes: [], mediaReference: .standalone(media: media), replyToMessageId: nil, localGroupingKey: item.groupedId, correlationId: nil), uniqueId: item.uniqueId)) default: break } @@ -462,12 +467,12 @@ public func legacyAssetPickerEnqueueMessages(account: Account, signals: [Any]) - var previewRepresentations: [TelegramMediaImageRepresentation] = [] if let thumbnail = thumbnail { - let resource = LocalFileMediaResource(fileId: arc4random64()) + let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) let thumbnailSize = finalDimensions.aspectFitted(CGSize(width: 320.0, height: 320.0)) let thumbnailImage = TGScaleImageToPixelSize(thumbnail, thumbnailSize)! if let thumbnailData = thumbnailImage.jpegData(compressionQuality: 0.4) { account.postbox.mediaBox.storeResourceData(resource.id, data: thumbnailData) - previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(thumbnailSize), resource: resource, progressiveSizes: [])) + previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(thumbnailSize), resource: resource, progressiveSizes: [], immediateThumbnailData: nil)) } } @@ -505,13 +510,13 @@ public func legacyAssetPickerEnqueueMessages(account: Account, signals: [Any]) - case let .tempFile(path, _, _): if asFile || (asAnimation && !path.contains(".jpg")) { if let size = fileSize(path) { - resource = LocalFileMediaResource(fileId: arc4random64(), size: size) + resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max), size: size) account.postbox.mediaBox.moveResourceData(resource.id, fromTempPath: path) } else { continue outer } } else { - resource = LocalFileVideoMediaResource(randomId: arc4random64(), path: path, adjustments: resourceAdjustments) + resource = LocalFileVideoMediaResource(randomId: Int64.random(in: Int64.min ... Int64.max), path: path, adjustments: resourceAdjustments) } } @@ -547,12 +552,12 @@ public func legacyAssetPickerEnqueueMessages(account: Account, signals: [Any]) - fileAttributes.append(.HasLinkedStickers) } - let media = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: arc4random64()), partialReference: nil, resource: resource, previewRepresentations: previewRepresentations, videoThumbnails: [], immediateThumbnailData: nil, mimeType: "video/mp4", size: nil, attributes: fileAttributes) + let media = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: Int64.random(in: Int64.min ... Int64.max)), partialReference: nil, resource: resource, previewRepresentations: previewRepresentations, videoThumbnails: [], immediateThumbnailData: nil, mimeType: "video/mp4", size: nil, attributes: fileAttributes) if let timer = item.timer, timer > 0 && timer <= 60 { attributes.append(AutoremoveTimeoutMessageAttribute(timeout: Int32(timer), countdownBeginTime: nil)) } - messages.append(.message(text: caption ?? "", attributes: attributes, mediaReference: .standalone(media: media), replyToMessageId: nil, localGroupingKey: item.groupedId)) + messages.append(LegacyAssetPickerEnqueueMessage(message: .message(text: caption ?? "", attributes: attributes, mediaReference: .standalone(media: media), replyToMessageId: nil, localGroupingKey: item.groupedId, correlationId: nil), uniqueId: item.uniqueId)) } } } diff --git a/submodules/LegacyMediaPickerUI/Sources/LegacyPaintStickersContext.swift b/submodules/LegacyMediaPickerUI/Sources/LegacyPaintStickersContext.swift index dbaebce170..78fd73e01b 100644 --- a/submodules/LegacyMediaPickerUI/Sources/LegacyPaintStickersContext.swift +++ b/submodules/LegacyMediaPickerUI/Sources/LegacyPaintStickersContext.swift @@ -21,7 +21,7 @@ protocol LegacyPaintEntity { } private func render(width: Int, height: Int, bytesPerRow: Int, data: Data, type: AnimationRendererFrameType) -> CIImage? { - let calculatedBytesPerRow = (4 * Int(width) + 15) & (~15) + let calculatedBytesPerRow = (4 * Int(width) + 31) & (~31) assert(bytesPerRow == calculatedBytesPerRow) let image = generateImagePixel(CGSize(width: CGFloat(width), height: CGFloat(height)), scale: 1.0, pixelGenerator: { _, pixelData, bytesPerRow in @@ -328,7 +328,10 @@ public final class LegacyPaintEntityRenderer: NSObject, TGPhotoPaintEntityRender var result: Double let minDuration: Double = 3.0 if durations.count > 1 { - result = min(6.0, Double(durations.reduce(1.0) { Double(lcm(Int32($0 * 10.0), Int32($1 * 10.0))) }) / 10.0) + let reduced = durations.reduce(1.0) { lhs, rhs -> Double in + return Double(lcm(Int32(lhs * 10.0), Int32(rhs * 10.0))) + } + result = min(6.0, Double(reduced) / 10.0) } else if let duration = durations.first { result = duration } else { diff --git a/submodules/LegacyMediaPickerUI/Sources/LegacySuggestionContext.swift b/submodules/LegacyMediaPickerUI/Sources/LegacySuggestionContext.swift index 53258c65b3..0f247d9fd5 100644 --- a/submodules/LegacyMediaPickerUI/Sources/LegacySuggestionContext.swift +++ b/submodules/LegacyMediaPickerUI/Sources/LegacySuggestionContext.swift @@ -19,7 +19,7 @@ public func legacySuggestionContext(context: AccountContext, peerId: PeerId, cha for peer in peers { if let peer = peer as? TelegramUser { let user = TGUser() - user.uid = peer.id.id + user.uid = peer.id.id._internalGetInt32Value() user.firstName = peer.firstName user.lastName = peer.lastName user.userName = peer.addressName @@ -46,7 +46,7 @@ public func legacySuggestionContext(context: AccountContext, peerId: PeerId, cha } suggestionContext.hashtagListSignal = { query in return SSignal { subscriber in - let disposable = (recentlyUsedHashtags(postbox: context.account.postbox) |> map { hashtags -> [String] in + let disposable = (context.engine.messages.recentlyUsedHashtags() |> map { hashtags -> [String] in let normalizedQuery = query?.lowercased() var result: [String] = [] if let normalizedQuery = normalizedQuery { @@ -74,7 +74,7 @@ public func legacySuggestionContext(context: AccountContext, peerId: PeerId, cha return SSignal.complete() } return SSignal { subscriber in - let disposable = (searchEmojiKeywords(postbox: context.account.postbox, inputLanguageCode: inputLanguageCode, query: query, completeMatch: query.count < 3) + let disposable = (context.engine.stickers.searchEmojiKeywords(inputLanguageCode: inputLanguageCode, query: query, completeMatch: query.count < 3) |> map { keywords -> [TGAlphacodeEntry] in var result: [TGAlphacodeEntry] = [] for keyword in keywords { diff --git a/submodules/LegacyUI/Sources/LegacyController.swift b/submodules/LegacyUI/Sources/LegacyController.swift index df58f2cbe3..5fdeaa4f3a 100644 --- a/submodules/LegacyUI/Sources/LegacyController.swift +++ b/submodules/LegacyUI/Sources/LegacyController.swift @@ -478,7 +478,7 @@ open class LegacyController: ViewController, PresentableController { super.containerLayoutUpdated(layout, transition: transition) - self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationHeight, transition: transition) + self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition) if let legacyTelegramController = self.legacyController as? TGViewController { var duration: TimeInterval = 0.0 if case let .animated(transitionDuration, _) = transition { diff --git a/submodules/LegacyUI/Sources/LegacyMediaLocations.swift b/submodules/LegacyUI/Sources/LegacyMediaLocations.swift index ca8b0fecc8..92d1d6cf68 100644 --- a/submodules/LegacyUI/Sources/LegacyMediaLocations.swift +++ b/submodules/LegacyUI/Sources/LegacyMediaLocations.swift @@ -11,7 +11,7 @@ public func legacyImageLocationUri(resource: MediaResource) -> String? { return nil } -private let legacyImageUriExpr = try? NSRegularExpression(pattern: "telegram-peer-photo-size-([-\\d]+)-([-\\d]+)-([-\\d]+)-([-\\d]+)", options: []) +private let legacyImageUriExpr = try? NSRegularExpression(pattern: "telegram-peer-photo-size-([-\\d]+)-([-\\d]+)-([-\\d]+)-([-\\d]+)-([-\\d]+)", options: []) public func resourceFromLegacyImageUri(_ uri: String) -> MediaResource? { guard let legacyImageUriExpr = legacyImageUriExpr else { @@ -21,13 +21,17 @@ public func resourceFromLegacyImageUri(_ uri: String) -> MediaResource? { if let match = matches.first { let nsString = uri as NSString let datacenterId = nsString.substring(with: match.range(at: 1)) - let size = nsString.substring(with: match.range(at: 2)) - let volumeId = nsString.substring(with: match.range(at: 3)) - let localId = nsString.substring(with: match.range(at: 4)) + let photoId = nsString.substring(with: match.range(at: 2)) + let size = nsString.substring(with: match.range(at: 3)) + let volumeId = nsString.substring(with: match.range(at: 4)) + let localId = nsString.substring(with: match.range(at: 5)) guard let nDatacenterId = Int32(datacenterId) else { return nil } + guard let nPhotoId = Int64(photoId) else { + return nil + } guard let nSizeSpec = Int32(size), let sizeSpec = CloudPeerPhotoSizeSpec(rawValue: nSizeSpec) else { return nil } @@ -38,7 +42,7 @@ public func resourceFromLegacyImageUri(_ uri: String) -> MediaResource? { return nil } - return CloudPeerPhotoSizeMediaResource(datacenterId: nDatacenterId, sizeSpec: sizeSpec, volumeId: nVolumeId, localId: nLocalId) + return CloudPeerPhotoSizeMediaResource(datacenterId: nDatacenterId, photoId: nPhotoId, sizeSpec: sizeSpec, volumeId: nVolumeId, localId: nLocalId) } return nil } diff --git a/submodules/LegacyUI/Sources/LegacyPeerAvatarPlaceholderDataSource.swift b/submodules/LegacyUI/Sources/LegacyPeerAvatarPlaceholderDataSource.swift index 54fb835456..cce9f64a41 100644 --- a/submodules/LegacyUI/Sources/LegacyPeerAvatarPlaceholderDataSource.swift +++ b/submodules/LegacyUI/Sources/LegacyPeerAvatarPlaceholderDataSource.swift @@ -61,12 +61,12 @@ final class LegacyPeerAvatarPlaceholderDataSource: TGImageDataSource { return EmptyDisposable } - var peerId = PeerId(namespace: 0, id: 0) + var peerId = PeerId(0) if let uid = args["uid"] as? String, let nUid = Int32(uid) { - peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: nUid) + peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt32Value(nUid)) } else if let cid = args["cid"] as? String, let nCid = Int32(cid) { - peerId = PeerId(namespace: Namespaces.Peer.CloudGroup, id: nCid) + peerId = PeerId(namespace: Namespaces.Peer.CloudGroup, id: PeerId.Id._internalFromInt32Value(nCid)) } let image = generateImage(CGSize(width: CGFloat(width), height: CGFloat(height)), rotatedContext: { size, context in @@ -77,10 +77,10 @@ final class LegacyPeerAvatarPlaceholderDataSource: TGImageDataSource { context.clip() let colorIndex: Int - if peerId.id == 0 { + if peerId.id._internalGetInt32Value() == 0 { colorIndex = -1 } else { - colorIndex = abs(Int(account.peerId.id + peerId.id)) + colorIndex = abs(Int(account.peerId.id._internalGetInt32Value() &+ peerId.id._internalGetInt32Value())) } let colorsArray: NSArray diff --git a/submodules/LegacyUI/Sources/TelegramInitializeLegacyComponents.swift b/submodules/LegacyUI/Sources/TelegramInitializeLegacyComponents.swift index 16a8c05cab..79984fa147 100644 --- a/submodules/LegacyUI/Sources/TelegramInitializeLegacyComponents.swift +++ b/submodules/LegacyUI/Sources/TelegramInitializeLegacyComponents.swift @@ -39,10 +39,6 @@ private final class LegacyComponentsAccessCheckerImpl: NSObject, LegacyComponent self.context = context } - public func checkAddressBookAuthorizationStatus(alertDismissComlpetion alertDismissCompletion: (() -> Void)!) -> Bool { - return true - } - 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 @@ -58,24 +54,10 @@ private final class LegacyComponentsAccessCheckerImpl: NSObject, LegacyComponent return true } - public func checkCameraAuthorizationStatus(for intent: TGCameraAccessIntent, alertDismissCompletion: (() -> Void)!) -> Bool { - return true - } - - public func checkLocationAuthorizationStatus(for intent: TGLocationAccessIntent, alertDismissComlpetion alertDismissCompletion: (() -> Void)!) -> Bool { - let subject: DeviceAccessLocationSubject - if intent == TGLocationAccessIntentSend { - subject = .send - } else if intent == TGLocationAccessIntentLiveLocation { - subject = .live - } else if intent == TGLocationAccessIntentTracking { - subject = .tracking - } else { - assertionFailure() - subject = .send - } + public func checkCameraAuthorizationStatus(for intent: TGCameraAccessIntent, completion: ((Bool) -> Void)!, alertDismissCompletion: (() -> Void)!) -> Bool { if let context = self.context { - DeviceAccess.authorizeAccess(to: .location(subject), presentationData: context.sharedContext.currentPresentationData.with { $0 }, present: context.sharedContext.presentGlobalController, openSettings: context.sharedContext.applicationBindings.openSettings, { value in + DeviceAccess.authorizeAccess(to: .camera(.video), presentationData: context.sharedContext.currentPresentationData.with { $0 }, present: context.sharedContext.presentGlobalController, openSettings: context.sharedContext.applicationBindings.openSettings, { value in + completion(value) if !value { alertDismissCompletion?() } @@ -85,25 +67,25 @@ private final class LegacyComponentsAccessCheckerImpl: NSObject, LegacyComponent } } -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 let keyboardWindowClass: AnyClass? = { +private func isKeyboardWindow(window: NSObject) -> Bool { + let typeName = NSStringFromClass(type(of: window)) if #available(iOS 9.0, *) { - return NSClassFromString(encodeText("VJSfnpufLfzcpbseXjoepx", -1)) + if typeName.hasPrefix("UI") && typeName.hasSuffix("RemoteKeyboardWindow") { + return true + } } else { - return NSClassFromString(encodeText("VJUfyuFggfdutXjoepx", -1)) + if typeName.hasPrefix("UI") && typeName.hasSuffix("TextEffectsWindow") { + return true + } } -}() + return false +} private final class LegacyComponentsGlobalsProviderImpl: NSObject, LegacyComponentsGlobalsProvider { func log(_ string: String!) { - print(string) + if let string = string { + print("\(string)") + } } public func effectiveLocalization() -> TGLocalization! { @@ -119,12 +101,8 @@ private final class LegacyComponentsGlobalsProviderImpl: NSObject, LegacyCompone } public func applicationKeyboardWindow() -> UIWindow! { - guard let keyboardWindowClass = keyboardWindowClass else { - return nil - } - for window in legacyComponentsApplication?.windows ?? [] { - if window.isKind(of: keyboardWindowClass) { + if isKeyboardWindow(window: window) { return window } } @@ -174,6 +152,10 @@ private final class LegacyComponentsGlobalsProviderImpl: NSObject, LegacyCompone legacyOpenUrl(url) } + public func makeViewDisableInteractiveKeyboardGestureRecognizer(_ view: UIView!) { + view.disablesInteractiveKeyboardGestureRecognizer = true + } + public func disableUserInteraction(for timeInterval: TimeInterval) { } @@ -259,7 +241,7 @@ private final class LegacyComponentsGlobalsProviderImpl: NSObject, LegacyCompone theme = defaultPresentationTheme } let barTheme = theme.rootController.navigationBar - return TGNavigationBarPallete(backgroundColor: barTheme.backgroundColor, separatorColor: barTheme.separatorColor, titleColor: barTheme.primaryTextColor, tintColor: barTheme.accentTextColor) + return TGNavigationBarPallete(backgroundColor: barTheme.opaqueBackgroundColor, separatorColor: barTheme.separatorColor, titleColor: barTheme.primaryTextColor, tintColor: barTheme.accentTextColor) } func menuSheetPallete() -> TGMenuSheetPallete! { @@ -275,6 +257,22 @@ private final class LegacyComponentsGlobalsProviderImpl: NSObject, LegacyCompone return TGMenuSheetPallete(dark: theme.overallDarkAppearance, backgroundColor: sheetTheme.opaqueItemBackgroundColor, selectionColor: sheetTheme.opaqueItemHighlightedBackgroundColor, separatorColor: sheetTheme.opaqueItemSeparatorColor, accentColor: sheetTheme.controlAccentColor, destructiveColor: sheetTheme.destructiveActionTextColor, textColor: sheetTheme.primaryTextColor, secondaryTextColor: sheetTheme.secondaryTextColor, spinnerColor: sheetTheme.secondaryTextColor, badgeTextColor: sheetTheme.controlAccentColor, badgeImage: nil, cornersImage: generateStretchableFilledCircleImage(diameter: 11.0, color: nil, strokeColor: nil, strokeWidth: nil, backgroundColor: sheetTheme.opaqueItemBackgroundColor)) } + func darkMenuSheetPallete() -> TGMenuSheetPallete! { + let theme: PresentationTheme + if let legacyContext = legacyContext { + let presentationData = legacyContext.sharedContext.currentPresentationData.with { $0 } + if presentationData.theme.overallDarkAppearance { + theme = presentationData.theme + } else { + theme = defaultDarkColorPresentationTheme + } + } else { + theme = defaultDarkColorPresentationTheme + } + let sheetTheme = theme.actionSheet + return TGMenuSheetPallete(dark: theme.overallDarkAppearance, backgroundColor: sheetTheme.opaqueItemBackgroundColor, selectionColor: sheetTheme.opaqueItemHighlightedBackgroundColor, separatorColor: sheetTheme.opaqueItemSeparatorColor, accentColor: sheetTheme.controlAccentColor, destructiveColor: sheetTheme.destructiveActionTextColor, textColor: sheetTheme.primaryTextColor, secondaryTextColor: sheetTheme.secondaryTextColor, spinnerColor: sheetTheme.secondaryTextColor, badgeTextColor: sheetTheme.controlAccentColor, badgeImage: nil, cornersImage: generateStretchableFilledCircleImage(diameter: 11.0, color: nil, strokeColor: nil, strokeWidth: nil, backgroundColor: sheetTheme.opaqueItemBackgroundColor)) + } + func mediaAssetsPallete() -> TGMediaAssetsPallete! { let presentationTheme: PresentationTheme if let legacyContext = legacyContext { @@ -288,7 +286,7 @@ private final class LegacyComponentsGlobalsProviderImpl: NSObject, LegacyCompone let navigationBar = presentationTheme.rootController.navigationBar let tabBar = presentationTheme.rootController.tabBar - return TGMediaAssetsPallete(dark: presentationTheme.overallDarkAppearance, backgroundColor: theme.plainBackgroundColor, selectionColor: theme.itemHighlightedBackgroundColor, separatorColor: theme.itemPlainSeparatorColor, textColor: theme.itemPrimaryTextColor, secondaryTextColor: theme.controlSecondaryColor, accentColor: theme.itemAccentColor, destructiveColor: theme.itemDestructiveColor, barBackgroundColor: tabBar.backgroundColor, barSeparatorColor: tabBar.separatorColor, navigationTitleColor: navigationBar.primaryTextColor, badge: generateStretchableFilledCircleImage(diameter: 22.0, color: navigationBar.accentTextColor), badgeTextColor: navigationBar.backgroundColor, sendIconImage: PresentationResourcesChat.chatInputPanelSendButtonImage(presentationTheme), doneIconImage: PresentationResourcesChat.chatInputPanelApplyButtonImage(presentationTheme), maybeAccentColor: navigationBar.accentTextColor) + return TGMediaAssetsPallete(dark: presentationTheme.overallDarkAppearance, backgroundColor: theme.plainBackgroundColor, selectionColor: theme.itemHighlightedBackgroundColor, separatorColor: theme.itemPlainSeparatorColor, textColor: theme.itemPrimaryTextColor, secondaryTextColor: theme.controlSecondaryColor, accentColor: theme.itemAccentColor, destructiveColor: theme.itemDestructiveColor, barBackgroundColor: navigationBar.opaqueBackgroundColor, barSeparatorColor: tabBar.separatorColor, navigationTitleColor: navigationBar.primaryTextColor, badge: generateStretchableFilledCircleImage(diameter: 22.0, color: navigationBar.accentTextColor), badgeTextColor: navigationBar.opaqueBackgroundColor, sendIconImage: PresentationResourcesChat.chatInputPanelSendButtonImage(presentationTheme), doneIconImage: PresentationResourcesChat.chatInputPanelApplyButtonImage(presentationTheme), maybeAccentColor: navigationBar.accentTextColor) } func checkButtonPallete() -> TGCheckButtonPallete! { diff --git a/submodules/ListMessageItem/BUILD b/submodules/ListMessageItem/BUILD index 55edc54d95..46c2804980 100644 --- a/submodules/ListMessageItem/BUILD +++ b/submodules/ListMessageItem/BUILD @@ -31,6 +31,8 @@ swift_library( "//submodules/MediaPlayer:UniversalMediaPlayer", "//submodules/ContextUI:ContextUI", "//submodules/FileMediaResourceStatus:FileMediaResourceStatus", + "//submodules/ManagedAnimationNode:ManagedAnimationNode", + "//submodules/WallpaperResources:WallpaperResources", ], visibility = [ "//visibility:public", diff --git a/submodules/ListMessageItem/Sources/ListMessageDateHeader.swift b/submodules/ListMessageItem/Sources/ListMessageDateHeader.swift index 9700c275bb..99e90d504e 100644 --- a/submodules/ListMessageItem/Sources/ListMessageDateHeader.swift +++ b/submodules/ListMessageItem/Sources/ListMessageDateHeader.swift @@ -42,7 +42,7 @@ final class ListMessageDateHeader: ListViewItemHeader { private let month: Int32 private let year: Int32 - let id: Int64 + let id: ListViewItemNode.HeaderId let theme: PresentationTheme let strings: PresentationStrings let fontSize: PresentationFontSize @@ -61,14 +61,15 @@ final class ListMessageDateHeader: ListViewItemHeader { self.month = timeinfo.tm_mon self.year = timeinfo.tm_year - self.id = Int64(self.roundedTimestamp) + self.id = ListViewItemNode.HeaderId(space: 0, id: Int64(self.roundedTimestamp)) } let stickDirection: ListViewItemHeaderStickDirection = .top + let stickOverInsets: Bool = true let height: CGFloat = 28.0 - func node() -> ListViewItemHeaderNode { + func node(synchronousLoad: Bool) -> ListViewItemHeaderNode { return ListMessageDateHeaderNode(theme: self.theme, strings: self.strings, fontSize: self.fontSize, roundedTimestamp: self.roundedTimestamp, month: self.month, year: self.year) } diff --git a/submodules/ListMessageItem/Sources/ListMessageFileItemNode.swift b/submodules/ListMessageItem/Sources/ListMessageFileItemNode.swift index 9484e73626..846e318a9c 100644 --- a/submodules/ListMessageItem/Sources/ListMessageFileItemNode.swift +++ b/submodules/ListMessageItem/Sources/ListMessageFileItemNode.swift @@ -19,6 +19,7 @@ import MusicAlbumArtResources import UniversalMediaPlayer import ContextUI import FileMediaResourceStatus +import ManagedAnimationNode private let extensionImageCache = Atomic<[UInt32: UIImage]>(value: [:]) @@ -87,7 +88,7 @@ private func extensionImage(fileExtension: String?) -> UIImage? { return nil } } -private let extensionFont = Font.with(size: 15.0, design: .round, traits: [.bold]) +private let extensionFont = Font.with(size: 15.0, design: .round, weight: .bold) private struct FetchControls { let fetch: () -> Void @@ -184,7 +185,7 @@ public final class ListMessageFileItemNode: ListMessageNode { private let playbackStatusDisposable = MetaDisposable() private let playbackStatus = Promise() - private var downloadStatusIconNode: ASImageNode + private var downloadStatusIconNode: DownloadIconNode private var linearProgressNode: LinearProgressNode? private var context: AccountContext? @@ -246,10 +247,7 @@ public final class ListMessageFileItemNode: ListMessageNode { self.iconStatusNode = SemanticStatusNode(backgroundNodeColor: .clear, foregroundNodeColor: .white) self.iconStatusNode.isUserInteractionEnabled = false - self.downloadStatusIconNode = ASImageNode() - self.downloadStatusIconNode.isLayerBacked = true - self.downloadStatusIconNode.displaysAsynchronously = false - self.downloadStatusIconNode.displayWithoutProcessing = true + self.downloadStatusIconNode = DownloadIconNode() self.restrictionNode = ASDisplayNode() self.restrictionNode.isHidden = true @@ -422,7 +420,7 @@ public final class ListMessageFileItemNode: ListMessageNode { descriptionString = "\(stringForDuration(Int32(duration))) • \(performer)" } } else if let size = file.size { - descriptionString = dataSizeString(size, decimalSeparator: item.presentationData.dateTimeFormat.decimalSeparator) + descriptionString = dataSizeString(size, formatting: DataSizeStringFormatting(chatPresentationData: item.presentationData)) } else { descriptionString = "" } @@ -514,9 +512,9 @@ public final class ListMessageFileItemNode: ListMessageNode { var descriptionString: String = "" if let size = file.size { if item.isGlobalSearchResult { - descriptionString = (dataSizeString(size, decimalSeparator: item.presentationData.dateTimeFormat.decimalSeparator)) + descriptionString = dataSizeString(size, formatting: DataSizeStringFormatting(chatPresentationData: item.presentationData)) } else { - descriptionString = "\(dataSizeString(size, decimalSeparator: item.presentationData.dateTimeFormat.decimalSeparator)) • \(dateString)" + descriptionString = "\(dataSizeString(size, formatting: DataSizeStringFormatting(chatPresentationData: item.presentationData))) • \(dateString)" } } else { if !item.isGlobalSearchResult { @@ -577,6 +575,13 @@ public final class ListMessageFileItemNode: ListMessageNode { if statusUpdated { updatedStatusSignal = messageFileMediaResourceStatus(context: item.context, file: selectedMedia, message: message, isRecentActions: false, isSharedMedia: true, isGlobalSearch: item.isGlobalSearchResult) + |> mapToSignal { value -> Signal in + if case .Fetching = value.fetchStatus { + return .single(value) |> delay(0.1, queue: Queue.concurrentDefaultQueue()) + } else { + return .single(value) + } + } if isAudio || isInstantVideo { if let currentUpdatedStatusSignal = updatedStatusSignal { @@ -738,6 +743,8 @@ public final class ListMessageFileItemNode: ListMessageNode { strongSelf.linearProgressNode?.updateTheme(theme: item.presentationData.theme.theme) strongSelf.restrictionNode.backgroundColor = item.presentationData.theme.theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.6) + + strongSelf.downloadStatusIconNode.customColor = item.presentationData.theme.theme.list.itemAccentColor } if let (selectionWidth, selectionApply) = selectionNodeWidthAndApply { @@ -849,7 +856,7 @@ public final class ListMessageFileItemNode: ListMessageNode { })) } - transition.updateFrame(node: strongSelf.downloadStatusIconNode, frame: CGRect(origin: CGPoint(x: leftOffset + leftInset, y: strongSelf.descriptionNode.frame.minY + floor((strongSelf.descriptionNode.frame.height - 12.0) / 2.0)), size: CGSize(width: 12.0, height: 12.0))) + transition.updateFrame(node: strongSelf.downloadStatusIconNode, frame: CGRect(origin: CGPoint(x: leftOffset + leftInset - 3.0, y: strongSelf.descriptionNode.frame.minY + floor((strongSelf.descriptionNode.frame.height - 18.0) / 2.0)), size: CGSize(width: 18.0, height: 18.0))) if let updatedFetchControls = updatedFetchControls { let _ = strongSelf.fetchControls.swap(updatedFetchControls) @@ -873,7 +880,7 @@ public final class ListMessageFileItemNode: ListMessageNode { } private func updateStatus(transition: ContainedViewLayoutTransition) { - guard let item = self.item, let media = self.currentMedia, let fetchStatus = self.fetchStatus, let status = self.resourceStatus, let layoutParams = self.layoutParams, let contentSize = self.contentSizeValue else { + guard let item = self.item, let media = self.currentMedia, let _ = self.fetchStatus, let status = self.resourceStatus, let layoutParams = self.layoutParams, let contentSize = self.contentSizeValue else { return } @@ -990,7 +997,7 @@ public final class ListMessageFileItemNode: ListMessageNode { switch fetchStatus { case let .Fetching(_, progress): if let file = self.currentMedia as? TelegramMediaFile, let size = file.size { - downloadingString = "\(dataSizeString(Int(Float(size) * progress), forceDecimal: true, decimalSeparator: item.presentationData.dateTimeFormat.decimalSeparator)) / \(dataSizeString(size, forceDecimal: true, decimalSeparator: item.presentationData.dateTimeFormat.decimalSeparator))" + downloadingString = "\(dataSizeString(Int(Float(size) * progress), forceDecimal: true, formatting: DataSizeStringFormatting(chatPresentationData: item.presentationData))) / \(dataSizeString(size, forceDecimal: true, formatting: DataSizeStringFormatting(chatPresentationData: item.presentationData)))" } descriptionOffset = 14.0 case .Remote: @@ -1015,10 +1022,12 @@ public final class ListMessageFileItemNode: ListMessageNode { transition.updateFrame(node: linearProgressNode, frame: progressFrame) linearProgressNode.updateProgress(value: CGFloat(progress), completion: {}) + var animated = true if self.downloadStatusIconNode.supernode == nil { + animated = false self.offsetContainerNode.addSubnode(self.downloadStatusIconNode) } - self.downloadStatusIconNode.image = PresentationResourcesChat.sharedMediaFileDownloadPauseIcon(item.presentationData.theme.theme) + self.downloadStatusIconNode.enqueueState(.pause, animated: animated) case .Local: if let linearProgressNode = self.linearProgressNode { self.linearProgressNode = nil @@ -1031,7 +1040,6 @@ public final class ListMessageFileItemNode: ListMessageNode { if self.downloadStatusIconNode.supernode != nil { self.downloadStatusIconNode.removeFromSupernode() } - self.downloadStatusIconNode.image = nil case .Remote: if let linearProgressNode = self.linearProgressNode { self.linearProgressNode = nil @@ -1039,10 +1047,12 @@ public final class ListMessageFileItemNode: ListMessageNode { linearProgressNode?.removeFromSupernode() }) } + var animated = true if self.downloadStatusIconNode.supernode == nil { + animated = false self.offsetContainerNode.addSubnode(self.downloadStatusIconNode) } - self.downloadStatusIconNode.image = PresentationResourcesChat.sharedMediaFileDownloadStartIcon(item.presentationData.theme.theme) + self.downloadStatusIconNode.enqueueState(.download, animated: animated) } } else { if let linearProgressNode = self.linearProgressNode { @@ -1063,18 +1073,19 @@ public final class ListMessageFileItemNode: ListMessageNode { transition.updateFrame(node: self.descriptionNode, frame: descriptionFrame) } + let alphaTransition = ContainedViewLayoutTransition.animated(duration: 0.3, curve: .easeInOut) if downloadingString != nil { - self.descriptionProgressNode.isHidden = false - self.descriptionNode.isHidden = true + alphaTransition.updateAlpha(node: self.descriptionProgressNode, alpha: 1.0) + alphaTransition.updateAlpha(node: self.descriptionNode, alpha: 0.0) } else { - self.descriptionProgressNode.isHidden = true - self.descriptionNode.isHidden = false + alphaTransition.updateAlpha(node: self.descriptionProgressNode, alpha: 0.0) + alphaTransition.updateAlpha(node: self.descriptionNode, alpha: 1.0) } - let descriptionFont = Font.regular(floor(item.presentationData.fontSize.baseDisplaySize * 13.0 / 17.0)) + + let descriptionFont = Font.with(size: floor(item.presentationData.fontSize.baseDisplaySize * 13.0 / 17.0), design: .regular, weight: .regular, traits: [.monospacedNumbers]) self.descriptionProgressNode.attributedText = NSAttributedString(string: downloadingString ?? "", font: descriptionFont, textColor: item.presentationData.theme.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)) - } func activateMedia() { @@ -1107,8 +1118,8 @@ public final class ListMessageFileItemNode: ListMessageNode { } } - override public func header() -> ListViewItemHeader? { - return self.item?.header + override public func headers() -> [ListViewItemHeader]? { + return self.item?.header.flatMap { [$0] } } override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { @@ -1269,3 +1280,53 @@ private final class LinearProgressNode: ASDisplayNode { self.shimmerNode.frame = CGRect(origin: CGPoint(x: shimmerOffset - shimmerWidth / 2.0, y: 0.0), size: CGSize(width: shimmerWidth, height: 3.0)) } } + +private enum DownloadIconNodeState: Equatable { + case download + case pause +} + +private final class DownloadIconNode: ManagedAnimationNode { + private let duration: Double = 0.3 + private var iconState: DownloadIconNodeState = .download + + init() { + super.init(size: CGSize(width: 18.0, height: 18.0)) + + self.trackTo(item: ManagedAnimationItem(source: .local("anim_shareddownload"), frames: .range(startFrame: 0, endFrame: 0), duration: 0.01)) + } + + func enqueueState(_ state: DownloadIconNodeState, animated: Bool) { + guard self.iconState != state else { + return + } + + let previousState = self.iconState + self.iconState = state + + switch previousState { + case .pause: + switch state { + case .download: + if animated { + self.trackTo(item: ManagedAnimationItem(source: .local("anim_shareddownload"), frames: .range(startFrame: 100, endFrame: 120), duration: self.duration)) + } else { + self.trackTo(item: ManagedAnimationItem(source: .local("anim_shareddownload"), frames: .range(startFrame: 0, endFrame: 0), duration: 0.01)) + } + case .pause: + break + } + case .download: + switch state { + case .pause: + if animated { + self.trackTo(item: ManagedAnimationItem(source: .local("anim_shareddownload"), frames: .range(startFrame: 0, endFrame: 20), duration: self.duration)) + } else { + self.trackTo(item: ManagedAnimationItem(source: .local("anim_shareddownload"), frames: .range(startFrame: 60, endFrame: 60), duration: 0.01)) + } + case .download: + break + } + } + } +} diff --git a/submodules/ListMessageItem/Sources/ListMessageSnippetItemNode.swift b/submodules/ListMessageItem/Sources/ListMessageSnippetItemNode.swift index 9c55e664a3..209da15c40 100644 --- a/submodules/ListMessageItem/Sources/ListMessageSnippetItemNode.swift +++ b/submodules/ListMessageItem/Sources/ListMessageSnippetItemNode.swift @@ -16,8 +16,9 @@ import UrlHandling import UrlWhitelist import AccountContext import TelegramStringFormatting +import WallpaperResources -private let iconFont = Font.with(size: 30.0, design: .round, traits: [.bold]) +private let iconFont = Font.with(size: 30.0, design: .round, weight: .bold) private let iconTextBackgroundImage = generateStretchableFilledCircleImage(radius: 6.0, color: UIColor(rgb: 0xFF9500)) @@ -253,6 +254,9 @@ public final class ListMessageSnippetItemNode: ListMessageNode { var primaryUrl: String? var isInstantView = false + + var previewWallpaper: TelegramWallpaper? + var previewWallpaperFileReference: FileMediaReference? var selectedMedia: TelegramMediaWebpage? var processed = false @@ -283,6 +287,17 @@ public final class ListMessageSnippetItemNode: ListMessageNode { iconImageReferenceAndRepresentation = (.message(message: MessageReference(item.message), media: image), representation) } } else if let file = content.file { + if content.type == "telegram_background" { + if let wallpaper = parseWallpaperUrl(content.url) { + switch wallpaper { + case let .slug(slug, _, colors, intensity, angle): + previewWallpaperFileReference = .message(message: MessageReference(item.message), media: file) + previewWallpaper = .file(id: file.fileId.id, accessHash: 0, isCreator: false, isDefault: false, isPattern: true, isDark: false, slug: slug, file: file, settings: WallpaperSettings(blur: false, motion: false, colors: colors, intensity: intensity, rotation: angle)) + default: + break + } + } + } if let representation = smallestImageRepresentation(file.previewRepresentations) { iconImageReferenceAndRepresentation = (.message(message: MessageReference(item.message), media: file), representation) } @@ -508,7 +523,9 @@ public final class ListMessageSnippetItemNode: ListMessageNode { } if currentIconImageRepresentation != iconImageReferenceAndRepresentation?.1 { - if let iconImageReferenceAndRepresentation = iconImageReferenceAndRepresentation { + if let previewWallpaper = previewWallpaper, let fileReference = previewWallpaperFileReference { + updateIconImageSignal = wallpaperThumbnail(account: item.context.account, accountManager: item.context.sharedContext.accountManager, fileReference: fileReference, wallpaper: previewWallpaper, synchronousLoad: false) + } else if let iconImageReferenceAndRepresentation = iconImageReferenceAndRepresentation { if let imageReference = iconImageReferenceAndRepresentation.0.concrete(TelegramMediaImage.self) { updateIconImageSignal = chatWebpageSnippetPhoto(account: item.context.account, photoReference: imageReference) } else if let fileReference = iconImageReferenceAndRepresentation.0.concrete(TelegramMediaFile.self) { @@ -742,8 +759,12 @@ public final class ListMessageSnippetItemNode: ListMessageNode { } } - override public func header() -> ListViewItemHeader? { - return self.item?.header + override public func headers() -> [ListViewItemHeader]? { + if let item = self.item { + return item.header.flatMap { [$0] } + } else { + return nil + } } override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { diff --git a/submodules/LiveLocationManager/Sources/LiveLocationManager.swift b/submodules/LiveLocationManager/Sources/LiveLocationManager.swift index 0cb7e547ea..14e1fa6448 100644 --- a/submodules/LiveLocationManager/Sources/LiveLocationManager.swift +++ b/submodules/LiveLocationManager/Sources/LiveLocationManager.swift @@ -10,9 +10,7 @@ import AccountContext public final class LiveLocationManagerImpl: LiveLocationManager { private let queue = Queue.mainQueue() - private let postbox: Postbox - private let network: Network - private let stateManager: AccountStateManager + private let account: Account private let locationManager: DeviceLocationManager private let summaryManagerImpl: LiveLocationSummaryManagerImpl @@ -46,16 +44,14 @@ public final class LiveLocationManagerImpl: LiveLocationManager { private var invalidationTimer: (SwiftSignalKit.Timer, Int32)? - public init(postbox: Postbox, network: Network, accountPeerId: PeerId, viewTracker: AccountViewTracker, stateManager: AccountStateManager, locationManager: DeviceLocationManager, inForeground: Signal) { - self.postbox = postbox - self.network = network - self.stateManager = stateManager + public init(engine: TelegramEngine, account: Account, locationManager: DeviceLocationManager, inForeground: Signal) { + self.account = account self.locationManager = locationManager - self.summaryManagerImpl = LiveLocationSummaryManagerImpl(queue: self.queue, postbox: postbox, accountPeerId: accountPeerId, viewTracker: viewTracker) + self.summaryManagerImpl = LiveLocationSummaryManagerImpl(queue: self.queue, engine: engine, postbox: account.postbox, accountPeerId: account.peerId, viewTracker: account.viewTracker) let viewKey: PostboxViewKey = .localMessageTag(.OutgoingLiveLocation) - self.messagesDisposable = (postbox.combinedView(keys: [viewKey]) + self.messagesDisposable = (account.postbox.combinedView(keys: [viewKey]) |> deliverOn(self.queue)).start(next: { [weak self] view in if let strongSelf = self { let timestamp = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) @@ -175,7 +171,7 @@ public final class LiveLocationManagerImpl: LiveLocationManager { let addedStopped = stopMessageIds.subtracting(self.stopMessageIds) self.stopMessageIds = stopMessageIds for id in addedStopped { - self.editMessageDisposables.set((requestEditLiveLocation(postbox: self.postbox, network: self.network, stateManager: self.stateManager, messageId: id, stop: true, coordinate: nil, heading: nil, proximityNotificationRadius: nil) + self.editMessageDisposables.set((TelegramEngine(account: self.account).messages.requestEditLiveLocation(messageId: id, stop: true, coordinate: nil, heading: nil, proximityNotificationRadius: nil) |> deliverOn(self.queue)).start(completed: { [weak self] in if let strongSelf = self { strongSelf.editMessageDisposables.set(nil, forKey: id) @@ -230,7 +226,7 @@ public final class LiveLocationManagerImpl: LiveLocationManager { let ids = self.broadcastToMessageIds let remainingIds = Atomic>(value: Set(ids.keys)) for id in ids.keys { - self.editMessageDisposables.set((requestEditLiveLocation(postbox: self.postbox, network: self.network, stateManager: self.stateManager, messageId: id, stop: false, coordinate: (latitude: coordinate.latitude, longitude: coordinate.longitude, accuracyRadius: Int32(accuracyRadius)), heading: heading.flatMap { Int32($0) }, proximityNotificationRadius: nil) + self.editMessageDisposables.set((TelegramEngine(account: self.account).messages.requestEditLiveLocation(messageId: id, stop: false, coordinate: (latitude: coordinate.latitude, longitude: coordinate.longitude, accuracyRadius: Int32(accuracyRadius)), heading: heading.flatMap { Int32($0) }, proximityNotificationRadius: nil) |> deliverOn(self.queue)).start(completed: { [weak self] in if let strongSelf = self { strongSelf.editMessageDisposables.set(nil, forKey: id) @@ -253,7 +249,7 @@ public final class LiveLocationManagerImpl: LiveLocationManager { let ids = self.broadcastToMessageIds.keys.filter({ $0.peerId == peerId }) if !ids.isEmpty { - let _ = self.postbox.transaction({ transaction -> Void in + let _ = self.account.postbox.transaction({ transaction -> Void in for id in ids { transaction.updateMessage(id, update: { currentMessage in var storeForwardInfo: StoreMessageForwardInfo? diff --git a/submodules/LiveLocationManager/Sources/LiveLocationSummaryManager.swift b/submodules/LiveLocationManager/Sources/LiveLocationSummaryManager.swift index 93440b5d88..db91916199 100644 --- a/submodules/LiveLocationManager/Sources/LiveLocationSummaryManager.swift +++ b/submodules/LiveLocationManager/Sources/LiveLocationSummaryManager.swift @@ -79,6 +79,7 @@ private final class LiveLocationSummaryContext { private final class LiveLocationPeerSummaryContext { private let queue: Queue + private let engine: TelegramEngine private let accountPeerId: PeerId private let viewTracker: AccountViewTracker private let peerId: PeerId @@ -116,8 +117,9 @@ private final class LiveLocationPeerSummaryContext { private let peerDisposable = MetaDisposable() - init(queue: Queue, accountPeerId: PeerId, viewTracker: AccountViewTracker, peerId: PeerId, becameEmpty: @escaping () -> Void) { + init(queue: Queue, engine: TelegramEngine, accountPeerId: PeerId, viewTracker: AccountViewTracker, peerId: PeerId, becameEmpty: @escaping () -> Void) { self.queue = queue + self.engine = engine self.accountPeerId = accountPeerId self.viewTracker = viewTracker self.peerId = peerId @@ -160,7 +162,7 @@ private final class LiveLocationPeerSummaryContext { private func updateSubscription() { if self.isActive || !self.subscribers.isEmpty { - self.peerDisposable.set((topPeerActiveLiveLocationMessages(viewTracker: self.viewTracker, accountPeerId: self.accountPeerId, peerId: self.peerId) + self.peerDisposable.set((self.engine.messages.topPeerActiveLiveLocationMessages(peerId: self.peerId) |> deliverOn(self.queue)).start(next: { [weak self] accountPeer, messages in if let strongSelf = self { var peersAndMessages: [(Peer, Message)] = [] @@ -187,6 +189,7 @@ private final class LiveLocationPeerSummaryContext { public final class LiveLocationSummaryManagerImpl: LiveLocationSummaryManager { private let queue: Queue + private let engine: TelegramEngine private let postbox: Postbox private let accountPeerId: PeerId private let viewTracker: AccountViewTracker @@ -194,9 +197,10 @@ public final class LiveLocationSummaryManagerImpl: LiveLocationSummaryManager { private let globalContext: LiveLocationSummaryContext private var peerContexts: [PeerId: LiveLocationPeerSummaryContext] = [:] - init(queue: Queue, postbox: Postbox, accountPeerId: PeerId, viewTracker: AccountViewTracker) { + init(queue: Queue, engine: TelegramEngine, postbox: Postbox, accountPeerId: PeerId, viewTracker: AccountViewTracker) { assert(queue.isCurrent()) self.queue = queue + self.engine = engine self.postbox = postbox self.accountPeerId = accountPeerId self.viewTracker = viewTracker @@ -212,7 +216,7 @@ public final class LiveLocationSummaryManagerImpl: LiveLocationSummaryManager { for peerId in peerIds { if self.peerContexts[peerId] == nil { - let context = LiveLocationPeerSummaryContext(queue: self.queue, accountPeerId: self.accountPeerId, viewTracker: self.viewTracker, peerId: peerId, becameEmpty: { [weak self] in + let context = LiveLocationPeerSummaryContext(queue: self.queue, engine: self.engine, accountPeerId: self.accountPeerId, viewTracker: self.viewTracker, peerId: peerId, becameEmpty: { [weak self] in if let strongSelf = self, let context = strongSelf.peerContexts[peerId], context.isEmpty { strongSelf.peerContexts.removeValue(forKey: peerId) } @@ -242,7 +246,7 @@ public final class LiveLocationSummaryManagerImpl: LiveLocationSummaryManager { if let current = strongSelf.peerContexts[peerId] { context = current } else { - context = LiveLocationPeerSummaryContext(queue: strongSelf.queue, accountPeerId: strongSelf.accountPeerId, viewTracker: strongSelf.viewTracker, peerId: peerId, becameEmpty: { + context = LiveLocationPeerSummaryContext(queue: strongSelf.queue, engine: strongSelf.engine, accountPeerId: strongSelf.accountPeerId, viewTracker: strongSelf.viewTracker, peerId: peerId, becameEmpty: { if let strongSelf = self, let context = strongSelf.peerContexts[peerId], context.isEmpty { strongSelf.peerContexts.removeValue(forKey: peerId) } diff --git a/submodules/LiveLocationTimerNode/Sources/ChatMessageLiveLocationTimerNode.swift b/submodules/LiveLocationTimerNode/Sources/ChatMessageLiveLocationTimerNode.swift index 2294ce1298..16ae49897a 100644 --- a/submodules/LiveLocationTimerNode/Sources/ChatMessageLiveLocationTimerNode.swift +++ b/submodules/LiveLocationTimerNode/Sources/ChatMessageLiveLocationTimerNode.swift @@ -4,8 +4,8 @@ import AsyncDisplayKit import Display import TelegramPresentationData -private let textFont = Font.with(size: 13.0, design: .round, traits: [.bold]) -private let smallTextFont = Font.with(size: 11.0, design: .round, traits: [.bold]) +private let textFont = Font.with(size: 13.0, design: .round, weight: .bold) +private let smallTextFont = Font.with(size: 11.0, design: .round, weight: .bold) private class ChatMessageLiveLocationTimerNodeParams: NSObject { let backgroundColor: UIColor diff --git a/submodules/LocationUI/Sources/LocationDistancePickerScreen.swift b/submodules/LocationUI/Sources/LocationDistancePickerScreen.swift index 78ce35ae8e..bb338c6e11 100644 --- a/submodules/LocationUI/Sources/LocationDistancePickerScreen.swift +++ b/submodules/LocationUI/Sources/LocationDistancePickerScreen.swift @@ -115,7 +115,7 @@ final class LocationDistancePickerScreen: 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.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition) } } @@ -552,7 +552,12 @@ class LocationDistancePickerScreenNode: ViewControllerTracingNode, UIScrollViewD self.dimNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) let offset = self.contentContainerNode.frame.height - self.wrappingScrollNode.layer.animatePosition(from: CGPoint(x: 0.0, y: offset), to: CGPoint(), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, additive: true) + let position = self.wrappingScrollNode.position + let transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring) + self.wrappingScrollNode.position = CGPoint(x: position.x, y: position.y + offset) + transition.animateView({ + self.wrappingScrollNode.position = position + }) } func animateOut(completion: (() -> Void)? = nil) { diff --git a/submodules/LocationUI/Sources/LocationMapHeaderNode.swift b/submodules/LocationUI/Sources/LocationMapHeaderNode.swift index 50b7f715c1..e267dedc67 100644 --- a/submodules/LocationUI/Sources/LocationMapHeaderNode.swift +++ b/submodules/LocationUI/Sources/LocationMapHeaderNode.swift @@ -15,7 +15,7 @@ private func generateBackgroundImage(theme: PresentationTheme) -> UIImage? { 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) + context.setFillColor(theme.rootController.navigationBar.opaqueBackgroundColor.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() diff --git a/submodules/LocationUI/Sources/LocationOptionsNode.swift b/submodules/LocationUI/Sources/LocationOptionsNode.swift index 9aee841c66..950f0df748 100644 --- a/submodules/LocationUI/Sources/LocationOptionsNode.swift +++ b/submodules/LocationUI/Sources/LocationOptionsNode.swift @@ -19,7 +19,7 @@ final class LocationOptionsNode: ASDisplayNode { self.presentationData = presentationData self.backgroundNode = ASDisplayNode() - self.backgroundNode.backgroundColor = self.presentationData.theme.rootController.navigationBar.backgroundColor + self.backgroundNode.backgroundColor = self.presentationData.theme.rootController.navigationBar.opaqueBackgroundColor self.separatorNode = ASDisplayNode() self.separatorNode.backgroundColor = self.presentationData.theme.rootController.navigationBar.separatorColor @@ -47,7 +47,7 @@ final class LocationOptionsNode: ASDisplayNode { func updatePresentationData(_ presentationData: PresentationData) { self.presentationData = presentationData - self.backgroundNode.backgroundColor = self.presentationData.theme.rootController.navigationBar.backgroundColor + self.backgroundNode.backgroundColor = self.presentationData.theme.rootController.navigationBar.opaqueBackgroundColor self.separatorNode.backgroundColor = self.presentationData.theme.rootController.navigationBar.separatorColor self.segmentedControlNode.updateTheme(SegmentedControlTheme(theme: self.presentationData.theme)) } diff --git a/submodules/LocationUI/Sources/LocationPickerController.swift b/submodules/LocationUI/Sources/LocationPickerController.swift index 0d9fb08ea1..cabb04b08d 100644 --- a/submodules/LocationUI/Sources/LocationPickerController.swift +++ b/submodules/LocationUI/Sources/LocationPickerController.swift @@ -319,7 +319,7 @@ public final class LocationPickerController: ViewController { override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { super.containerLayoutUpdated(layout, transition: transition) - self.controllerNode.containerLayoutUpdated(layout, navigationHeight: self.navigationHeight, transition: transition) + self.controllerNode.containerLayoutUpdated(layout, navigationHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition) } @objc private func cancelPressed() { diff --git a/submodules/LocationUI/Sources/LocationPickerControllerNode.swift b/submodules/LocationUI/Sources/LocationPickerControllerNode.swift index e1b9e6cbd9..e43a09a232 100644 --- a/submodules/LocationUI/Sources/LocationPickerControllerNode.swift +++ b/submodules/LocationUI/Sources/LocationPickerControllerNode.swift @@ -10,7 +10,6 @@ import SwiftSignalKit import MergeLists import ItemListUI import ItemListVenueItem -import ActivityIndicator import TelegramPresentationData import TelegramStringFormatting import AccountContext @@ -41,7 +40,7 @@ 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 venue(PresentationTheme, TelegramMediaMap?, Int) case attribution(PresentationTheme, LocationAttribution) var stableId: LocationPickerEntryId { @@ -52,8 +51,8 @@ private enum LocationPickerEntry: Comparable, Identifiable { return .liveLocation case .header: return .header - case let .venue(_, venue, _): - return .venue(venue.venue?.id ?? "") + case let .venue(_, venue, index): + return .venue(venue?.venue?.id ?? "\(index)") case .attribution: return .attribution } @@ -80,7 +79,7 @@ private enum LocationPickerEntry: Comparable, Identifiable { 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 { + if case let .venue(rhsTheme, rhsVenue, rhsIndex) = rhs, lhsTheme === rhsTheme, lhsVenue?.venue?.id == rhsVenue?.venue?.id, lhsIndex == rhsIndex { return true } else { return false @@ -158,9 +157,9 @@ private enum LocationPickerEntry: Comparable, Identifiable { case let .header(_, title): return LocationSectionHeaderItem(presentationData: ItemListPresentationData(presentationData), title: title) case let .venue(_, venue, _): - let venueType = venue.venue?.type ?? "" - return ItemListVenueItem(presentationData: ItemListPresentationData(presentationData), account: account, venue: venue, style: .plain, action: { - interaction?.sendVenue(venue) + let venueType = venue?.venue?.type ?? "" + return ItemListVenueItem(presentationData: ItemListPresentationData(presentationData), account: account, venue: venue, style: .plain, action: venue.flatMap { venue in + return { interaction?.sendVenue(venue) } }, infoAction: ["home", "work"].contains(venueType) ? { interaction?.openHomeWorkInfo() } : nil) @@ -253,7 +252,6 @@ final class LocationPickerControllerNode: ViewControllerTracingNode, CLLocationM private let listNode: ListView private let emptyResultsTextNode: ImmediateTextNode private let headerNode: LocationMapHeaderNode - private let activityIndicator: ActivityIndicator private let shadeNode: ASDisplayNode private let innerShadeNode: ASDisplayNode @@ -301,8 +299,6 @@ final class LocationPickerControllerNode: ViewControllerTracingNode, CLLocationM 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 @@ -316,7 +312,6 @@ final class LocationPickerControllerNode: ViewControllerTracingNode, CLLocationM 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) @@ -408,7 +403,7 @@ final class LocationPickerControllerNode: ViewControllerTracingNode, CLLocationM 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) + return combineLatest(nearbyVenues(context: context, latitude: location.coordinate.latitude, longitude: location.coordinate.longitude), personalVenues) |> map { nearbyVenues, personalVenues -> [TelegramMediaMap]? in var resultVenues: [TelegramMediaMap] = [] if let personalVenues = personalVenues { @@ -436,7 +431,7 @@ final class LocationPickerControllerNode: ViewControllerTracingNode, CLLocationM if let coordinate = coordinate { return (.single(nil) |> then( - nearbyVenues(account: context.account, latitude: coordinate.latitude, longitude: coordinate.longitude) + nearbyVenues(context: context, latitude: coordinate.latitude, longitude: coordinate.longitude) |> map { venues -> ([TelegramMediaMap], CLLocation)? in return (venues, CLLocation(latitude: coordinate.latitude, longitude: coordinate.longitude)) } @@ -504,8 +499,8 @@ final class LocationPickerControllerNode: ViewControllerTracingNode, CLLocationM entries.append(.header(presentationData.theme, presentationData.strings.Map_ChooseAPlace.uppercased())) let displayedVenues = foundVenues != nil || state.searchingVenuesAround ? foundVenues : venues + var index: Int = 0 if let venues = displayedVenues { - var index: Int = 0 var attribution: LocationAttribution? for venue in venues { if venue.venue?.provider == "foursquare" { @@ -519,6 +514,11 @@ final class LocationPickerControllerNode: ViewControllerTracingNode, CLLocationM if let attribution = attribution { entries.append(.attribution(presentationData.theme, attribution)) } + } else { + for i in 0 ..< 8 { + entries.append(.venue(presentationData.theme, nil, index)) + index += 1 + } } let previousEntries = previousEntries.swap(entries) let previousState = previousState.swap(state) @@ -637,10 +637,10 @@ final class LocationPickerControllerNode: ViewControllerTracingNode, CLLocationM 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) + strongSelf.layoutEmptyResultsPlaceholder(transition: listTransition) } - self.listNode.beganInteractiveDragging = { [weak self] in + self.listNode.beganInteractiveDragging = { [weak self] _ in guard let strongSelf = self else { return } @@ -755,12 +755,11 @@ final class LocationPickerControllerNode: ViewControllerTracingNode, CLLocationM 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) + strongSelf.layoutEmptyResultsPlaceholder(transition: .immediate) } }) } @@ -799,7 +798,7 @@ final class LocationPickerControllerNode: ViewControllerTracingNode, CLLocationM } } - private func layoutActivityIndicator(transition: ContainedViewLayoutTransition) { + private func layoutEmptyResultsPlaceholder(transition: ContainedViewLayoutTransition) { guard let (layout, navigationHeight) = self.validLayout else { return } @@ -812,10 +811,7 @@ final class LocationPickerControllerNode: ViewControllerTracingNode, CLLocationM 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)) @@ -875,7 +871,7 @@ final class LocationPickerControllerNode: ViewControllerTracingNode, CLLocationM 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) + self.layoutEmptyResultsPlaceholder(transition: transition) if isFirstLayout { while !self.enqueuedTransitions.isEmpty { diff --git a/submodules/LocationUI/Sources/LocationSearchContainerNode.swift b/submodules/LocationUI/Sources/LocationSearchContainerNode.swift index cadd155df2..75fe5a5e7c 100644 --- a/submodules/LocationUI/Sources/LocationSearchContainerNode.swift +++ b/submodules/LocationUI/Sources/LocationSearchContainerNode.swift @@ -165,7 +165,7 @@ final class LocationSearchContainerNode: ASDisplayNode { } |> 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) + let foundVenues = nearbyVenues(context: context, latitude: coordinate.latitude, longitude: coordinate.longitude, query: query) |> afterCompleted { isSearching.set(false) } @@ -224,7 +224,7 @@ final class LocationSearchContainerNode: ASDisplayNode { } })) - self.listNode.beganInteractiveDragging = { [weak self] in + self.listNode.beganInteractiveDragging = { [weak self] _ in self?.interaction.dismissInput() } } diff --git a/submodules/LocationUI/Sources/LocationUtils.swift b/submodules/LocationUI/Sources/LocationUtils.swift index 7d658b057c..604c803418 100644 --- a/submodules/LocationUI/Sources/LocationUtils.swift +++ b/submodules/LocationUI/Sources/LocationUtils.swift @@ -5,6 +5,7 @@ import TelegramCore import TelegramPresentationData import TelegramStringFormatting import MapKit +import AccountContext extension TelegramMediaMap { convenience init(coordinate: CLLocationCoordinate2D, liveBroadcastingTimeout: Int32? = nil, proximityNotificationRadius: Int32? = nil) { @@ -32,17 +33,17 @@ 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 account.postbox.transaction { transaction -> SearchBotsConfiguration in +public func nearbyVenues(context: AccountContext, latitude: Double, longitude: Double, query: String? = nil) -> Signal<[TelegramMediaMap], NoError> { + return context.account.postbox.transaction { transaction -> SearchBotsConfiguration in return currentSearchBotsConfiguration(transaction: transaction) } |> mapToSignal { searchBotsConfiguration in - return resolvePeerByName(account: account, name: searchBotsConfiguration.venueBotUsername ?? "foursquare") + return context.engine.peers.resolvePeerByName(name: searchBotsConfiguration.venueBotUsername ?? "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: "") + return context.engine.messages.requestChatContextResults(botId: peerId, peerId: context.account.peerId, query: query ?? "", location: .single((latitude, longitude)), offset: "") |> map { results -> ChatContextResultCollection? in return results?.results } diff --git a/submodules/LocationUI/Sources/LocationViewController.swift b/submodules/LocationUI/Sources/LocationViewController.swift index 14920a2593..54fcf2468a 100644 --- a/submodules/LocationUI/Sources/LocationViewController.swift +++ b/submodules/LocationUI/Sources/LocationViewController.swift @@ -192,7 +192,7 @@ public final class LocationViewController: ViewController { return state } - let _ = requestEditLiveLocation(postbox: context.account.postbox, network: context.account.network, stateManager: context.account.stateManager, messageId: messageId, stop: false, coordinate: nil, heading: nil, proximityNotificationRadius: 0).start(completed: { [weak self] in + let _ = context.engine.messages.requestEditLiveLocation(messageId: messageId, stop: false, coordinate: nil, heading: nil, proximityNotificationRadius: 0).start(completed: { [weak self] in guard let strongSelf = self else { return } @@ -261,7 +261,7 @@ public final class LocationViewController: ViewController { return state } - let _ = requestEditLiveLocation(postbox: context.account.postbox, network: context.account.network, stateManager: context.account.stateManager, messageId: messageId, stop: false, coordinate: nil, heading: nil, proximityNotificationRadius: distance).start(completed: { [weak self] in + let _ = context.engine.messages.requestEditLiveLocation(messageId: messageId, stop: false, coordinate: nil, heading: nil, proximityNotificationRadius: distance).start(completed: { [weak self] in guard let strongSelf = self else { return } @@ -495,7 +495,7 @@ public final class LocationViewController: ViewController { override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { super.containerLayoutUpdated(layout, transition: transition) - self.controllerNode.containerLayoutUpdated(layout, navigationHeight: self.navigationHeight, transition: transition) + self.controllerNode.containerLayoutUpdated(layout, navigationHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition) } @objc private func cancelPressed() { diff --git a/submodules/LocationUI/Sources/LocationViewControllerNode.swift b/submodules/LocationUI/Sources/LocationViewControllerNode.swift index d2bb4fa7cb..179270366f 100644 --- a/submodules/LocationUI/Sources/LocationViewControllerNode.swift +++ b/submodules/LocationUI/Sources/LocationViewControllerNode.swift @@ -279,7 +279,7 @@ final class LocationViewControllerNode: ViewControllerTracingNode, CLLocationMan } } - let liveLocations = topPeerActiveLiveLocationMessages(viewTracker: context.account.viewTracker, accountPeerId: context.account.peerId, peerId: subject.id.peerId) + let liveLocations = context.engine.messages.topPeerActiveLiveLocationMessages(peerId: subject.id.peerId) |> map { _, messages -> [Message] in return messages } @@ -537,7 +537,7 @@ final class LocationViewControllerNode: ViewControllerTracingNode, CLLocationMan 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 + self.listNode.beganInteractiveDragging = { [weak self] _ in guard let strongSelf = self else { return } diff --git a/submodules/ManagedAnimationNode/Sources/ManagedAnimationNode.swift b/submodules/ManagedAnimationNode/Sources/ManagedAnimationNode.swift index 50f8a1402b..1dde44442a 100644 --- a/submodules/ManagedAnimationNode/Sources/ManagedAnimationNode.swift +++ b/submodules/ManagedAnimationNode/Sources/ManagedAnimationNode.swift @@ -12,6 +12,8 @@ import SwiftSignalKit public final class ManagedAnimationState { public let item: ManagedAnimationItem private let instance: LottieInstance + + private let displaySize: CGSize let frameCount: Int let fps: Double @@ -21,15 +23,11 @@ public final class ManagedAnimationState { public var executedCallbacks = Set() - private let renderContext: DrawingContext - public init?(displaySize: CGSize, item: ManagedAnimationItem, current: ManagedAnimationState?) { let resolvedInstance: LottieInstance - let renderContext: DrawingContext if let current = current { resolvedInstance = current.instance - renderContext = current.renderContext } else { guard let path = item.source.path else { return nil @@ -44,20 +42,21 @@ public final class ManagedAnimationState { return nil } resolvedInstance = instance - renderContext = DrawingContext(size: displaySize, scale: UIScreenScale, premultiplied: true, clear: true) } - + + self.displaySize = displaySize self.item = item self.instance = resolvedInstance - self.renderContext = renderContext self.frameCount = Int(self.instance.frameCount) self.fps = Double(self.instance.frameRate) } func draw() -> UIImage? { - self.instance.renderFrame(with: Int32(self.frameIndex ?? 0), into: self.renderContext.bytes.assumingMemoryBound(to: UInt8.self), width: Int32(self.renderContext.size.width * self.renderContext.scale), height: Int32(self.renderContext.size.height * self.renderContext.scale), bytesPerRow: Int32(self.renderContext.bytesPerRow)) - return self.renderContext.generateImage() + let renderContext = DrawingContext(size: self.displaySize, scale: UIScreenScale, clear: true) + + self.instance.renderFrame(with: Int32(self.frameIndex ?? 0), into: renderContext.bytes.assumingMemoryBound(to: UInt8.self), width: Int32(renderContext.size.width * renderContext.scale), height: Int32(renderContext.size.height * renderContext.scale), bytesPerRow: Int32(renderContext.bytesPerRow)) + return renderContext.generateImage() } } @@ -133,10 +132,29 @@ open class ManagedAnimationNode: ASDisplayNode { private let imageNode: ASImageNode private let displayLink: CADisplayLink + public var imageUpdated: ((UIImage) -> Void)? + public var image: UIImage? { + return self.imageNode.image + } + public var state: ManagedAnimationState? public var trackStack: [ManagedAnimationItem] = [] public var didTryAdvancingState = false + public var customColor: UIColor? { + didSet { + if let customColor = self.customColor, oldValue?.rgb != customColor.rgb { + self.imageNode.image = generateTintedImage(image: self.imageNode.image, color: customColor) + } + } + } + + public var scale: CGFloat = 1.0 { + didSet { + self.imageNode.transform = CATransform3DMakeScale(self.scale, self.scale, 1.0) + } + } + public init(size: CGSize) { self.intrinsicSize = size @@ -242,7 +260,12 @@ open class ManagedAnimationNode: ASDisplayNode { if state.frameIndex != frameIndex { state.frameIndex = frameIndex if let image = state.draw() { - self.imageNode.image = image + if let customColor = self.customColor { + self.imageNode.image = generateTintedImage(image: image, color: customColor) + } else { + self.imageNode.image = image + } + self.imageUpdated?(image) } for (callbackFrame, callback) in state.item.callbacks { @@ -274,4 +297,11 @@ open class ManagedAnimationNode: ASDisplayNode { self.didTryAdvancingState = false self.updateAnimation() } + + open override func layout() { + super.layout() + + self.imageNode.bounds = self.bounds + self.imageNode.position = CGPoint(x: self.bounds.width / 2.0, y: self.bounds.height / 2.0) + } } diff --git a/submodules/MediaPlayer/Sources/FFMpegMediaFrameSource.swift b/submodules/MediaPlayer/Sources/FFMpegMediaFrameSource.swift index 3c86dc17ca..83fb2ab4e5 100644 --- a/submodules/MediaPlayer/Sources/FFMpegMediaFrameSource.swift +++ b/submodules/MediaPlayer/Sources/FFMpegMediaFrameSource.swift @@ -283,6 +283,9 @@ public final class FFMpegMediaFrameSource: NSObject, MediaFrameSource { let _ = currentSemaphore.swap(nil) subscriber.putError(.generic) } + } else { + let _ = currentSemaphore.swap(nil) + subscriber.putError(.generic) } } }) diff --git a/submodules/MediaPlayer/Sources/FFMpegMediaVideoFrameDecoder.swift b/submodules/MediaPlayer/Sources/FFMpegMediaVideoFrameDecoder.swift index 14c0330554..83e84af1f3 100644 --- a/submodules/MediaPlayer/Sources/FFMpegMediaVideoFrameDecoder.swift +++ b/submodules/MediaPlayer/Sources/FFMpegMediaVideoFrameDecoder.swift @@ -1,10 +1,30 @@ + +#if !os(macOS) import UIKit +#else +import AppKit +#endif import CoreMedia import Accelerate import FFMpegBinding private let bufferCount = 32 + + +#if os(macOS) +private let deviceColorSpace: CGColorSpace = { + if #available(OSX 10.11.2, *) { + if let colorSpace = CGColorSpace(name: CGColorSpace.displayP3) { + return colorSpace + } else { + return CGColorSpaceCreateDeviceRGB() + } + } else { + return CGColorSpaceCreateDeviceRGB() + } +}() +#else private let deviceColorSpace: CGColorSpace = { if #available(iOSApplicationExtension 9.3, iOS 9.3, *) { if let colorSpace = CGColorSpace(name: CGColorSpace.displayP3) { @@ -16,7 +36,7 @@ private let deviceColorSpace: CGColorSpace = { return CGColorSpaceCreateDeviceRGB() } }() - +#endif public final class FFMpegMediaVideoFrameDecoder: MediaTrackFrameDecoder { public enum ReceiveResult { case error @@ -202,7 +222,7 @@ public final class FFMpegMediaVideoFrameDecoder: MediaTrackFrameDecoder { var srcCb = vImage_Buffer(data: frame.data[1], height: vImagePixelCount(frame.height), width: vImagePixelCount(frame.width / 2), rowBytes: Int(frame.lineSize[1])) var srcCr = vImage_Buffer(data: frame.data[2], height: vImagePixelCount(frame.height), width: vImagePixelCount(frame.width / 2), rowBytes: Int(frame.lineSize[2])) - let argbBytesPerRow = (4 * Int(frame.width) + 15) & (~15) + let argbBytesPerRow = (4 * Int(frame.width) + 31) & (~31) let argbLength = argbBytesPerRow * Int(frame.height) let argb = malloc(argbLength)! guard let provider = CGDataProvider(dataInfo: argb, data: argb, size: argbLength, releaseData: { bytes, _, _ in diff --git a/submodules/MediaPlayer/Sources/MediaPlayer.swift b/submodules/MediaPlayer/Sources/MediaPlayer.swift index 315f8c85c3..72ea0fed6c 100644 --- a/submodules/MediaPlayer/Sources/MediaPlayer.swift +++ b/submodules/MediaPlayer/Sources/MediaPlayer.swift @@ -284,15 +284,11 @@ private final class MediaPlayerContext { } let currentTimestamp = CMTimeGetSeconds(CMTimebaseGetTime(loadedState.controlTimebase.timebase)) var duration: Double = 0.0 - var videoStatus: MediaTrackFrameBufferStatus? if let videoTrackFrameBuffer = loadedState.mediaBuffers.videoBuffer { - videoStatus = videoTrackFrameBuffer.status(at: currentTimestamp) duration = max(duration, CMTimeGetSeconds(videoTrackFrameBuffer.duration)) } - - var audioStatus: MediaTrackFrameBufferStatus? + if let audioTrackFrameBuffer = loadedState.mediaBuffers.audioBuffer { - audioStatus = audioTrackFrameBuffer.status(at: currentTimestamp) duration = max(duration, CMTimeGetSeconds(audioTrackFrameBuffer.duration)) } loadedDuration = duration @@ -447,6 +443,7 @@ private final class MediaPlayerContext { switch self.state { case .empty: + self.stoppedAtEnd = false self.lastStatusUpdateTimestamp = nil if self.enableSound { let queue = self.queue @@ -476,6 +473,7 @@ private final class MediaPlayerContext { } self.seek(timestamp: 0.0, action: .play) case let .seeking(frameSource, timestamp, seekState, disposable, _, enableSound): + self.stoppedAtEnd = false self.state = .seeking(frameSource: frameSource, timestamp: timestamp, seekState: seekState, disposable: disposable, action: .play, enableSound: enableSound) self.lastStatusUpdateTimestamp = nil case let .paused(loadedState): @@ -499,12 +497,14 @@ private final class MediaPlayerContext { fadeTimer.start() } - if loadedState.lostAudioSession { + if loadedState.lostAudioSession && !self.stoppedAtEnd { + self.stoppedAtEnd = false let timestamp = CMTimeGetSeconds(CMTimebaseGetTime(loadedState.controlTimebase.timebase)) self.seek(timestamp: timestamp, action: .play) } else { self.lastStatusUpdateTimestamp = nil if self.stoppedAtEnd { + self.stoppedAtEnd = false self.seek(timestamp: 0.0, action: .play) } else { self.state = .playing(loadedState) @@ -512,10 +512,8 @@ private final class MediaPlayerContext { } } case .playing: - break + self.stoppedAtEnd = false } - - self.stoppedAtEnd = false } fileprivate func playOnceWithSound(playAndRecord: Bool, seek: MediaPlayerSeek = .start) { @@ -962,28 +960,28 @@ private final class MediaPlayerContext { self.playerStatus.set(.single(status)) let _ = self.playerStatusValue.swap(status) } - - if performActionAtEndNow && !self.stoppedAtEnd { - switch self.actionAtEnd { - case let .loop(f): - self.stoppedAtEnd = false - self.seek(timestamp: 0.0, action: .play) - f?() - case .stop: - self.stoppedAtEnd = true - self.pause(lostAudioSession: false) - case let .action(f): - self.stoppedAtEnd = true - self.pause(lostAudioSession: false) - f() - case let .loopDisablingSound(f): - self.stoppedAtEnd = false - self.enableSound = false - self.seek(timestamp: 0.0, action: .play) - f() + + if performActionAtEndNow { + if !self.stoppedAtEnd { + switch self.actionAtEnd { + case let .loop(f): + self.stoppedAtEnd = false + self.seek(timestamp: 0.0, action: .play) + f?() + case .stop: + self.stoppedAtEnd = true + self.pause(lostAudioSession: false) + case let .action(f): + self.stoppedAtEnd = true + self.pause(lostAudioSession: false) + f() + case let .loopDisablingSound(f): + self.stoppedAtEnd = false + self.enableSound = false + self.seek(timestamp: 0.0, action: .play) + f() + } } - } else { - self.stoppedAtEnd = false } } } diff --git a/submodules/MediaPlayer/Sources/MediaPlayerScrubbingNode.swift b/submodules/MediaPlayer/Sources/MediaPlayerScrubbingNode.swift index b7ce647975..b97a260cb9 100644 --- a/submodules/MediaPlayer/Sources/MediaPlayerScrubbingNode.swift +++ b/submodules/MediaPlayer/Sources/MediaPlayerScrubbingNode.swift @@ -317,13 +317,17 @@ public final class MediaPlayerScrubbingNode: ASDisplayNode { if value != self._statusValue { if let value = value, value.seekId == self.ignoreSeekId { } else { + let previousStatusValue = self._statusValue self._statusValue = value self.updateProgressAnimations() - let playbackStatus = value?.status + var playbackStatus = value?.status if self.playbackStatusValue != playbackStatus { self.playbackStatusValue = playbackStatus if let playbackStatusUpdated = self.playbackStatusUpdated { + if playbackStatus == .paused, previousStatusValue?.status == .playing, let value = value, value.timestamp > value.duration - 0.1 { + playbackStatus = .playing + } playbackStatusUpdated(playbackStatus) } } diff --git a/submodules/TelegramUI/Sources/TimeBasedVideoPreload.swift b/submodules/MediaPlayer/Sources/TimeBasedVideoPreload.swift similarity index 90% rename from submodules/TelegramUI/Sources/TimeBasedVideoPreload.swift rename to submodules/MediaPlayer/Sources/TimeBasedVideoPreload.swift index 2015895dab..bc6a8bc7ef 100644 --- a/submodules/TelegramUI/Sources/TimeBasedVideoPreload.swift +++ b/submodules/MediaPlayer/Sources/TimeBasedVideoPreload.swift @@ -5,9 +5,8 @@ import Postbox import TelegramCore import SyncCore import FFMpegBinding -import UniversalMediaPlayer -func preloadVideoResource(postbox: Postbox, resourceReference: MediaResourceReference, duration: Double) -> Signal { +public func preloadVideoResource(postbox: Postbox, resourceReference: MediaResourceReference, duration: Double) -> Signal { return Signal { subscriber in let queue = Queue() let disposable = MetaDisposable() diff --git a/submodules/MediaResources/Sources/CachedResourceRepresentations.swift b/submodules/MediaResources/Sources/CachedResourceRepresentations.swift index ea1cfbccc7..54f11eb402 100644 --- a/submodules/MediaResources/Sources/CachedResourceRepresentations.swift +++ b/submodules/MediaResources/Sources/CachedResourceRepresentations.swift @@ -117,70 +117,6 @@ public final class CachedBlurredWallpaperRepresentation: CachedMediaResourceRepr } } -public final class CachedPatternWallpaperMaskRepresentation: CachedMediaResourceRepresentation { - public let keepDuration: CachedMediaRepresentationKeepDuration = .general - - public let size: CGSize? - - public var uniqueId: String { - if let size = self.size { - return "pattern-wallpaper-mask-\(Int(size.width))x\(Int(size.height))" - } else { - return "pattern-wallpaper-mask" - } - } - - public init(size: CGSize?) { - self.size = size - } - - public func isEqual(to: CachedMediaResourceRepresentation) -> Bool { - if let to = to as? CachedPatternWallpaperMaskRepresentation { - return self.size == to.size - } else { - return false - } - } -} - - -public final class CachedPatternWallpaperRepresentation: CachedMediaResourceRepresentation { - public let keepDuration: CachedMediaRepresentationKeepDuration = .general - - public let color: UInt32 - public let bottomColor: UInt32? - public let intensity: Int32 - public let rotation: Int32? - - public var uniqueId: String { - 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: 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.bottomColor == to.bottomColor && self.intensity == intensity && self.rotation == to.rotation - } else { - return false - } - } -} - public final class CachedAlbumArtworkRepresentation: CachedMediaResourceRepresentation { public let keepDuration: CachedMediaRepresentationKeepDuration = .general diff --git a/submodules/MessageReactionListUI/Sources/MessageReactionCategoryNode.swift b/submodules/MessageReactionListUI/Sources/MessageReactionCategoryNode.swift deleted file mode 100644 index a71caccd9c..0000000000 --- a/submodules/MessageReactionListUI/Sources/MessageReactionCategoryNode.swift +++ /dev/null @@ -1,104 +0,0 @@ -import Foundation -import AsyncDisplayKit -import Display -import TelegramPresentationData -import TelegramCore -import SyncCore - -final class MessageReactionCategoryNode: ASDisplayNode { - let category: MessageReactionListCategory - private let action: () -> Void - - private let buttonNode: HighlightableButtonNode - private let highlightedBackgroundNode: ASImageNode - private let iconNode: ASImageNode - private let emojiNode: ImmediateTextNode - private let countNode: ImmediateTextNode - - var isSelected = false { - didSet { - self.highlightedBackgroundNode.alpha = self.isSelected ? 1.0 : 0.0 - } - } - - init(theme: PresentationTheme, category: MessageReactionListCategory, count: Int, action: @escaping () -> Void) { - self.category = category - self.action = action - - self.buttonNode = HighlightableButtonNode() - - self.highlightedBackgroundNode = ASImageNode() - self.highlightedBackgroundNode.displaysAsynchronously = false - self.highlightedBackgroundNode.displayWithoutProcessing = true - self.highlightedBackgroundNode.image = generateStretchableFilledCircleImage(diameter: 18.0, color: UIColor(rgb: 0xe6e6e8)) - self.highlightedBackgroundNode.alpha = 1.0 - - self.iconNode = ASImageNode() - - self.emojiNode = ImmediateTextNode() - self.emojiNode.displaysAsynchronously = false - let emojiText: String - switch category { - case .all: - emojiText = "" - self.iconNode.image = PresentationResourcesChat.chatInputTextFieldTimerImage(theme) - case let .reaction(value): - emojiText = value - } - self.emojiNode.attributedText = NSAttributedString(string: emojiText, font: Font.regular(18.0), textColor: .black) - - self.countNode = ImmediateTextNode() - self.countNode.displaysAsynchronously = false - self.countNode.attributedText = NSAttributedString(string: "\(count)", font: Font.regular(16.0), textColor: .black) - - super.init() - - self.addSubnode(self.highlightedBackgroundNode) - self.addSubnode(self.iconNode) - self.addSubnode(self.emojiNode) - self.addSubnode(self.countNode) - self.addSubnode(self.buttonNode) - - self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside) - } - - func updateLayout() -> CGSize { - let sideInset: CGFloat = 6.0 - let spacing: CGFloat = 2.0 - let emojiSize = self.emojiNode.updateLayout(CGSize(width: 100.0, height: 100.0)) - let iconSize = self.iconNode.image?.size ?? CGSize() - let countSize = self.countNode.updateLayout(CGSize(width: 100.0, height: 100.0)) - - let height: CGFloat = 60.0 - let backgroundHeight: CGFloat = 36.0 - - self.emojiNode.frame = CGRect(origin: CGPoint(x: sideInset, y: floor((height - emojiSize.height) / 2.0)), size: emojiSize) - self.iconNode.frame = CGRect(origin: CGPoint(x: sideInset, y: floor((height - iconSize.height) / 2.0)), size: iconSize) - - let iconFrame: CGRect - if self.iconNode.image != nil { - iconFrame = self.iconNode.frame - } else { - iconFrame = self.emojiNode.frame - } - - self.countNode.frame = CGRect(origin: CGPoint(x: iconFrame.maxX + spacing, y: floor((height - countSize.height) / 2.0)), size: countSize) - let contentWidth = sideInset * 2.0 + spacing + iconFrame.width + countSize.width - self.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: floor((height - backgroundHeight) / 2.0)), size: CGSize(width: contentWidth, height: backgroundHeight)) - - let size = CGSize(width: contentWidth, height: height) - self.buttonNode.frame = CGRect(origin: CGPoint(), size: size) - return size - } - - @objc private func buttonPressed() { - self.action() - } - - override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { - if self.buttonNode.frame.contains(point) { - return self.buttonNode.view - } - return nil - } -} diff --git a/submodules/MessageReactionListUI/Sources/MessageReactionListController.swift b/submodules/MessageReactionListUI/Sources/MessageReactionListController.swift deleted file mode 100644 index 94374463e3..0000000000 --- a/submodules/MessageReactionListUI/Sources/MessageReactionListController.swift +++ /dev/null @@ -1,447 +0,0 @@ -import Foundation -import UIKit -import AsyncDisplayKit -import Display -import AccountContext -import TelegramPresentationData -import Postbox -import TelegramCore -import SyncCore -import SwiftSignalKit -import MergeLists -import ItemListPeerItem -import ItemListUI - -public final class MessageReactionListController: ViewController { - private let context: AccountContext - private let messageId: MessageId - private let presentationData: PresentationData - private let initialReactions: [MessageReaction] - - private var controllerNode: MessageReactionListControllerNode { - return self.displayNode as! MessageReactionListControllerNode - } - - private var animatedIn: Bool = false - - private let _ready = Promise() - override public var ready: Promise { - return self._ready - } - - public init(context: AccountContext, messageId: MessageId, initialReactions: [MessageReaction]) { - self.context = context - self.messageId = messageId - self.presentationData = context.sharedContext.currentPresentationData.with { $0 } - self.initialReactions = initialReactions - - super.init(navigationBarPresentationData: nil) - - self.statusBar.statusBarStyle = .Ignore - } - - required public init(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override public func loadDisplayNode() { - self.displayNode = MessageReactionListControllerNode(context: self.context, presentationData: self.presentationData, messageId: messageId, initialReactions: initialReactions, dismiss: { [weak self] in - self?.dismiss() - }) - - super.displayNodeDidLoad() - - self._ready.set(self.controllerNode.isReady.get()) - } - - override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { - super.containerLayoutUpdated(layout, transition: transition) - - self.controllerNode.containerLayoutUpdated(layout: layout, transition: transition) - } - - override public func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - - if !self.animatedIn { - self.animatedIn = true - self.controllerNode.animateIn() - } - } - - override public func dismiss(completion: (() -> Void)? = nil) { - self.controllerNode.animateOut(completion: { [weak self] in - self?.presentingViewController?.dismiss(animated: false, completion: nil) - completion?() - }) - } -} - -private struct MessageReactionListTransaction { - let deletions: [ListViewDeleteItem] - let insertions: [ListViewInsertItem] - let updates: [ListViewUpdateItem] -} - -private struct MessageReactionListEntry: Comparable, Identifiable { - let index: Int - let item: MessageReactionListCategoryItem - - var stableId: PeerId { - return self.item.peer.id - } - - static func <(lhs: MessageReactionListEntry, rhs: MessageReactionListEntry) -> Bool { - return lhs.index < rhs.index - } - - func item(context: AccountContext, presentationData: PresentationData) -> ListViewItem { - 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) - } -} - -private func preparedTransition(from fromEntries: [MessageReactionListEntry], to toEntries: [MessageReactionListEntry], context: AccountContext, presentationData: PresentationData) -> MessageReactionListTransaction { - 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), directionHint: nil) } - let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData), directionHint: nil) } - - return MessageReactionListTransaction(deletions: deletions, insertions: insertions, updates: updates) -} - -private let headerHeight: CGFloat = 60.0 -private let itemHeight: CGFloat = 50.0 - -private func topInsetForLayout(layout: ContainerViewLayout, itemCount: Int) -> CGFloat { - let contentHeight = CGFloat(itemCount) * itemHeight - let minimumItemHeights: CGFloat = max(contentHeight, itemHeight * 5.0) - - return max(layout.size.height - layout.intrinsicInsets.bottom - minimumItemHeights, headerHeight) -} - -private final class MessageReactionListControllerNode: ViewControllerTracingNode { - private let context: AccountContext - private let presentationData: PresentationData - private let dismiss: () -> Void - - private let listContext: MessageReactionListContext - - private let dimNode: ASDisplayNode - private let backgroundNode: ASDisplayNode - private let contentHeaderContainerNode: ASDisplayNode - private let contentHeaderContainerBackgroundNode: ASImageNode - private let contentHeaderContainerSeparatorNode: ASDisplayNode - private var categoryItemNodes: [MessageReactionCategoryNode] = [] - private let categoryScrollNode: ASScrollNode - private let listNode: ListView - private var placeholderNode: MessageReactionListLoadingPlaceholder? - private var placeholderNodeIsAnimatingOut = false - - private var validLayout: ContainerViewLayout? - - private var currentCategory: MessageReactionListCategory = .all - private var currentState: MessageReactionListState? - - private var enqueuedTransactions: [MessageReactionListTransaction] = [] - - private let disposable = MetaDisposable() - - let isReady = Promise() - - private var forceHeaderTransition: ContainedViewLayoutTransition? - - init(context: AccountContext, presentationData: PresentationData, messageId: MessageId, initialReactions: [MessageReaction], dismiss: @escaping () -> Void) { - self.context = context - self.presentationData = presentationData - self.dismiss = dismiss - - self.dimNode = ASDisplayNode() - self.dimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.5) - - self.backgroundNode = ASDisplayNode() - self.backgroundNode.backgroundColor = self.presentationData.theme.actionSheet.opaqueItemBackgroundColor - - self.contentHeaderContainerNode = ASDisplayNode() - self.contentHeaderContainerBackgroundNode = ASImageNode() - self.contentHeaderContainerBackgroundNode.displaysAsynchronously = false - - self.contentHeaderContainerSeparatorNode = ASDisplayNode() - self.contentHeaderContainerSeparatorNode.backgroundColor = self.presentationData.theme.list.itemPlainSeparatorColor - - self.categoryScrollNode = ASScrollNode() - self.contentHeaderContainerBackgroundNode.displayWithoutProcessing = true - self.contentHeaderContainerBackgroundNode.image = generateImage(CGSize(width: 10.0, height: 10.0), rotatedContext: { size, context in - context.clear(CGRect(origin: CGPoint(), size: size)) - context.setFillColor(presentationData.theme.rootController.navigationBar.backgroundColor.cgColor) - context.fillEllipse(in: CGRect(origin: CGPoint(), size: size)) - context.fill(CGRect(origin: CGPoint(x: 0.0, y: size.height / 2.0), size: CGSize(width: size.width, height: size.height / 2.0))) - })?.stretchableImage(withLeftCapWidth: 5, topCapHeight: 5) - - self.listNode = ListView() - self.listNode.limitHitTestToNodes = true - self.listNode.accessibilityPageScrolledString = { row, count in - return presentationData.strings.VoiceOver_ScrollStatus(row, count).0 - } - - self.placeholderNode = MessageReactionListLoadingPlaceholder(theme: presentationData.theme, itemHeight: itemHeight) - self.placeholderNode?.isUserInteractionEnabled = false - - self.listContext = MessageReactionListContext(postbox: self.context.account.postbox, network: self.context.account.network, messageId: messageId, initialReactions: initialReactions) - - super.init() - - self.addSubnode(self.dimNode) - self.addSubnode(self.backgroundNode) - - self.listNode.stackFromBottom = false - self.addSubnode(self.listNode) - self.placeholderNode.flatMap(self.addSubnode) - - self.addSubnode(self.contentHeaderContainerNode) - self.contentHeaderContainerNode.addSubnode(self.contentHeaderContainerBackgroundNode) - self.contentHeaderContainerNode.addSubnode(self.contentHeaderContainerSeparatorNode) - self.contentHeaderContainerNode.addSubnode(self.categoryScrollNode) - - self.listNode.updateFloatingHeaderOffset = { [weak self] offset, listTransition in - guard let strongSelf = self, let layout = strongSelf.validLayout else { - return - } - - let transition = strongSelf.forceHeaderTransition ?? listTransition - strongSelf.forceHeaderTransition = nil - - let topOffset = offset - transition.updateFrame(node: strongSelf.contentHeaderContainerNode, frame: CGRect(origin: CGPoint(x: 0.0, y: topOffset - headerHeight), size: CGSize(width: layout.size.width, height: headerHeight))) - transition.updateFrame(node: strongSelf.contentHeaderContainerBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: layout.size.width, height: headerHeight))) - transition.updateFrame(node: strongSelf.contentHeaderContainerSeparatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: headerHeight), size: CGSize(width: layout.size.width, height: UIScreenPixel))) - if let placeholderNode = strongSelf.placeholderNode { - transition.updateFrame(node: placeholderNode, frame: CGRect(origin: CGPoint(x: 0.0, y: topOffset), size: placeholderNode.bounds.size)) - } - transition.updateFrame(node: strongSelf.backgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: topOffset - headerHeight / 2.0), size: CGSize(width: layout.size.width, height: layout.size.height + 300.0))) - } - - self.disposable.set((self.listContext.state - |> deliverOnMainQueue).start(next: { [weak self] state in - self?.updateState(state) - })) - } - - deinit { - self.disposable.dispose() - } - - override func didLoad() { - super.didLoad() - - self.dimNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimNodeTapGesture))) - } - - func containerLayoutUpdated(layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { - let isFirstLayout = self.validLayout == nil - self.validLayout = layout - - transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(), size: layout.size)) - - transition.updateBounds(node: self.listNode, bounds: CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: layout.size.height)) - transition.updatePosition(node: self.listNode, position: CGPoint(x: layout.size.width / 2.0, y: layout.size.height / 2.0)) - - var currentCategoryItemCount = 0 - if let currentState = self.currentState { - for (category, categoryState) in currentState.states { - if category == self.currentCategory { - currentCategoryItemCount = categoryState.count - break - } - } - } - - var insets = UIEdgeInsets() - insets.top = topInsetForLayout(layout: layout, itemCount: currentCategoryItemCount) - insets.bottom = layout.intrinsicInsets.bottom - - if let placeholderNode = self.placeholderNode, !self.placeholderNodeIsAnimatingOut { - let placeholderHeight = min(CGFloat(currentCategoryItemCount) * itemHeight, layout.size.height) + UIScreenPixel - placeholderNode.frame = CGRect(origin: placeholderNode.frame.origin, size: CGSize(width: layout.size.width, height: placeholderHeight)) - placeholderNode.updateLayout(size: CGSize(width: layout.size.width, height: placeholderHeight)) - } - - 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 - var leftX = sideInset - for itemNode in self.categoryItemNodes { - let itemSize = itemNode.updateLayout() - itemNode.frame = CGRect(origin: CGPoint(x: leftX, y: 0.0), size: itemSize) - leftX += spacing + itemSize.width - } - leftX += sideInset - self.categoryScrollNode.view.contentSize = CGSize(width: leftX, height: 60.0) - self.categoryScrollNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: layout.size.width, height: 60.0)) - - if isFirstLayout { - while !self.enqueuedTransactions.isEmpty { - self.dequeueTransaction() - } - } - } - - func animateIn() { - self.dimNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) - self.dimNode.layer.animatePosition(from: CGPoint(x: self.dimNode.position.x, y: self.dimNode.position.y - self.layer.bounds.size.height), to: self.layer.position, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, completion: { _ in - }) - 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, completion: { _ in - }) - } - - func animateOut(completion: @escaping () -> Void) { - self.dimNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) - self.dimNode.layer.animatePosition(from: self.dimNode.position, to: CGPoint(x: self.dimNode.position.x, y: self.dimNode.position.y - self.layer.bounds.size.height), duration: 0.2, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false) - 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() - }) - } - - func updateState(_ state: MessageReactionListState) { - if self.currentState != state { - self.currentState = state - - self.updateItems() - - if let validLayout = self.validLayout { - self.containerLayoutUpdated(layout: validLayout, transition: .immediate) - } - } - } - - private var currentEntries: [MessageReactionListEntry]? - private func updateItems() { - var entries: [MessageReactionListEntry] = [] - - var index = 0 - let states = self.currentState?.states ?? [] - for (category, categoryState) in states { - if self.categoryItemNodes.count <= index { - let itemNode = MessageReactionCategoryNode(theme: self.presentationData.theme, category: category, count: categoryState.count, action: { [weak self] in - self?.setCategory(category) - }) - self.categoryItemNodes.append(itemNode) - self.categoryScrollNode.addSubnode(itemNode) - if category == self.currentCategory { - itemNode.isSelected = true - } else { - itemNode.isSelected = false - } - } - - if category == self.currentCategory { - for item in categoryState.items { - entries.append(MessageReactionListEntry(index: entries.count, item: item)) - } - } - index += 1 - } - let transaction = preparedTransition(from: self.currentEntries ?? [], to: entries, context: self.context, presentationData: self.presentationData) - let previousWasEmpty = self.currentEntries == nil || self.currentEntries?.count == 0 - let isEmpty = entries.isEmpty - self.currentEntries = entries - - self.enqueuedTransactions.append(transaction) - self.dequeueTransaction() - - if previousWasEmpty && !isEmpty { - if let placeholderNode = self.placeholderNode { - self.placeholderNodeIsAnimatingOut = true - placeholderNode.allowsGroupOpacity = true - placeholderNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.18, removeOnCompletion: false, completion: { [weak self] _ in - guard let strongSelf = self else { - return - } - strongSelf.placeholderNode?.removeFromSupernode() - strongSelf.placeholderNode = nil - }) - } - self.listNode.forEachItemNode({ itemNode in - itemNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.18) - }) - } - } - - func setCategory(_ category: MessageReactionListCategory) { - if self.currentCategory != category { - self.currentCategory = category - - for itemNode in self.categoryItemNodes { - itemNode.isSelected = category == itemNode.category - } - - //self.forceHeaderTransition = .animated(duration: 0.3, curve: .spring) - if let validLayout = self.validLayout { - self.containerLayoutUpdated(layout: validLayout, transition: .animated(duration: 0.3, curve: .spring)) - } - - self.updateItems() - } - } - - private func dequeueTransaction() { - guard let layout = self.validLayout, let transaction = self.enqueuedTransactions.first else { - return - } - - self.enqueuedTransactions.remove(at: 0) - - var options = ListViewDeleteAndInsertOptions() - options.insert(.Synchronous) - options.insert(.PreferSynchronousResourceLoading) - options.insert(.PreferSynchronousDrawing) - - var currentCategoryItemCount = 0 - if let currentState = self.currentState { - for (category, categoryState) in currentState.states { - if category == self.currentCategory { - currentCategoryItemCount = categoryState.count - break - } - } - } - - var insets = UIEdgeInsets() - insets.top = topInsetForLayout(layout: layout, itemCount: currentCategoryItemCount) - insets.bottom = layout.intrinsicInsets.bottom - - let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: self.listNode.bounds.size, insets: insets, duration: 0.3, curve: .Default(duration: 0.3)) - - self.listNode.transaction(deleteIndices: transaction.deletions, insertIndicesAndItems: transaction.insertions, updateIndicesAndItems: transaction.updates, options: options, updateSizeAndInsets: updateSizeAndInsets, updateOpaqueState: nil, completion: { [weak self] _ in - self?.isReady.set(.single(true)) - }) - } - - @objc private func dimNodeTapGesture(_ recognizer: UITapGestureRecognizer) { - if case .ended = recognizer.state { - self.dismiss() - } - } - - override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { - for itemNode in self.categoryItemNodes { - if let result = itemNode.hitTest(self.view.convert(point, to: itemNode.view), with: event) { - return result - } - } - if let result = self.listNode.hitTest(self.view.convert(point, to: self.listNode.view), with: event) { - return result - } - if point.y >= self.contentHeaderContainerNode.frame.minY && point.y < self.bounds.height { - return self.listNode.view - } - if point.y >= 0 && point.y < self.contentHeaderContainerNode.frame.minY { - return self.dimNode.view - } - return nil - } -} diff --git a/submodules/MessageReactionListUI/Sources/MessageReactionListLoadingPlaceholder.swift b/submodules/MessageReactionListUI/Sources/MessageReactionListLoadingPlaceholder.swift deleted file mode 100644 index 7c5eebf198..0000000000 --- a/submodules/MessageReactionListUI/Sources/MessageReactionListLoadingPlaceholder.swift +++ /dev/null @@ -1,81 +0,0 @@ -import Foundation -import AsyncDisplayKit -import Display -import TelegramPresentationData -import TelegramCore -import SyncCore - -final class MessageReactionListLoadingPlaceholder: ASDisplayNode { - private let theme: PresentationTheme - private let itemHeight: CGFloat - private let itemImage: UIImage? - - private let backgroundNode: ASDisplayNode - private let separatorNode: ASDisplayNode - private let highlightNode: ASImageNode - private var itemNodes: [ASImageNode] = [] - - init(theme: PresentationTheme, itemHeight: CGFloat) { - self.theme = theme - self.itemHeight = itemHeight - - self.backgroundNode = ASDisplayNode() - self.backgroundNode.backgroundColor = UIColor(white: 0.92, alpha: 1.0) - - self.separatorNode = ASDisplayNode() - self.separatorNode.backgroundColor = theme.list.itemPlainSeparatorColor - - self.highlightNode = ASImageNode() - self.highlightNode.displaysAsynchronously = false - self.highlightNode.displayWithoutProcessing = true - - let leftInset: CGFloat = 15.0 - let avatarSize: CGFloat = 40.0 - let avatarSpacing: CGFloat = 11.0 - let contentWidth: CGFloat = 4.0 - let contentHeight: CGFloat = 14.0 - let rightInset: CGFloat = 54.0 - self.itemImage = generateImage(CGSize(width: leftInset + avatarSize + avatarSpacing + contentWidth + rightInset, height: itemHeight), rotatedContext: { size, context in - context.setFillColor(theme.actionSheet.opaqueItemBackgroundColor.cgColor) - context.fill(CGRect(origin: CGPoint(), size: size)) - context.setFillColor(theme.list.itemPlainSeparatorColor.cgColor) - context.fill(CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: UIScreenPixel))) - context.setBlendMode(.copy) - context.setFillColor(UIColor.clear.cgColor) - context.fillEllipse(in: CGRect(origin: CGPoint(x: leftInset, y: floor((itemHeight - avatarSize) / 2.0)), size: CGSize(width: avatarSize, height: avatarSize))) - let contentOrigin = leftInset + avatarSize + avatarSpacing - context.fill(CGRect(origin: CGPoint(x: contentOrigin, y: floor((size.height - contentHeight) / 2.0)), size: CGSize(width: size.width - contentOrigin - rightInset, height: contentHeight))) - })?.stretchableImage(withLeftCapWidth: Int(leftInset + avatarSize + avatarSpacing + 1), topCapHeight: 0) - - super.init() - - self.addSubnode(self.backgroundNode) - self.addSubnode(self.highlightNode) - self.addSubnode(self.separatorNode) - } - - func updateLayout(size: CGSize) { - self.backgroundNode.frame = CGRect(origin: CGPoint(), size: size) - - var verticalOffset: CGFloat = 0.0 - var index = 0 - while verticalOffset < size.height - 1.0 { - if self.itemNodes.count >= index { - let itemNode = ASImageNode() - itemNode.image = self.itemImage - self.itemNodes.append(itemNode) - self.addSubnode(itemNode) - } - self.itemNodes[index].frame = CGRect(origin: CGPoint(x: 0.0, y: verticalOffset), size: CGSize(width: size.width, height: self.itemHeight)) - verticalOffset += self.itemHeight - index += 1 - } - self.separatorNode.frame = CGRect(origin: CGPoint(x: 0.0, y: verticalOffset), size: CGSize(width: size.width, height: UIScreenPixel)) - if index < self.itemNodes.count { - for i in index ..< self.itemNodes.count { - self.itemNodes[i].removeFromSupernode() - } - self.itemNodes.removeLast(self.itemNodes.count - index) - } - } -} diff --git a/submodules/MozjpegBinding/Public/MozjpegBinding/MozjpegBinding.h b/submodules/MozjpegBinding/Public/MozjpegBinding/MozjpegBinding.h index 3e7ec4696f..c7142e4e5e 100644 --- a/submodules/MozjpegBinding/Public/MozjpegBinding/MozjpegBinding.h +++ b/submodules/MozjpegBinding/Public/MozjpegBinding/MozjpegBinding.h @@ -2,4 +2,4 @@ NSData * _Nullable compressJPEGData(UIImage * _Nonnull sourceImage); NSArray * _Nonnull extractJPEGDataScans(NSData * _Nonnull data); -NSData * _Nullable compressMiniThumbnail(UIImage * _Nonnull image); +NSData * _Nullable compressMiniThumbnail(UIImage * _Nonnull image, CGSize size); diff --git a/submodules/MozjpegBinding/Sources/MozjpegBinding.m b/submodules/MozjpegBinding/Sources/MozjpegBinding.m index fa2dd964e2..8138b309ad 100644 --- a/submodules/MozjpegBinding/Sources/MozjpegBinding.m +++ b/submodules/MozjpegBinding/Sources/MozjpegBinding.m @@ -70,7 +70,7 @@ NSData * _Nullable compressJPEGData(UIImage * _Nonnull sourceImage) { int width = (int)(sourceImage.size.width * sourceImage.scale); int height = (int)(sourceImage.size.height * sourceImage.scale); - int targetBytesPerRow = ((4 * (int)width) + 15) & (~15); + int targetBytesPerRow = ((4 * (int)width) + 31) & (~31); uint8_t *targetMemory = malloc((int)(targetBytesPerRow * height)); CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); @@ -86,7 +86,7 @@ NSData * _Nullable compressJPEGData(UIImage * _Nonnull sourceImage) { UIGraphicsPopContext(); - int bufferBytesPerRow = ((3 * (int)width) + 15) & (~15); + int bufferBytesPerRow = ((3 * (int)width) + 31) & (~31); uint8_t *buffer = malloc(bufferBytesPerRow * height); for (int y = 0; y < height; y++) { @@ -146,8 +146,7 @@ NSData * _Nullable compressJPEGData(UIImage * _Nonnull sourceImage) { return result; } -NSData * _Nullable compressMiniThumbnail(UIImage * _Nonnull image) { - CGSize size = CGSizeMake(40.0f, 40.0f); +NSData * _Nullable compressMiniThumbnail(UIImage * _Nonnull image, CGSize size) { CGSize fittedSize = image.size; if (fittedSize.width > size.width) { fittedSize = CGSizeMake(size.width, (int)((fittedSize.height * size.width / MAX(fittedSize.width, 1.0f)))); @@ -159,7 +158,7 @@ NSData * _Nullable compressMiniThumbnail(UIImage * _Nonnull image) { int width = (int)fittedSize.width; int height = (int)fittedSize.height; - int targetBytesPerRow = ((4 * (int)width) + 15) & (~15); + int targetBytesPerRow = ((4 * (int)width) + 31) & (~31); uint8_t *targetMemory = malloc((int)(targetBytesPerRow * height)); CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); @@ -175,7 +174,7 @@ NSData * _Nullable compressMiniThumbnail(UIImage * _Nonnull image) { UIGraphicsPopContext(); - int bufferBytesPerRow = ((3 * (int)width) + 15) & (~15); + int bufferBytesPerRow = ((3 * (int)width) + 31) & (~31); uint8_t *buffer = malloc(bufferBytesPerRow * height); for (int y = 0; y < height; y++) { diff --git a/submodules/MtProtoKit/PublicHeaders/MtProtoKit/MTRequestErrorContext.h b/submodules/MtProtoKit/PublicHeaders/MtProtoKit/MTRequestErrorContext.h index fc190383be..b1e9361e08 100644 --- a/submodules/MtProtoKit/PublicHeaders/MtProtoKit/MTRequestErrorContext.h +++ b/submodules/MtProtoKit/PublicHeaders/MtProtoKit/MTRequestErrorContext.h @@ -10,5 +10,6 @@ @property (nonatomic) NSUInteger floodWaitSeconds; @property (nonatomic) bool waitingForTokenExport; +@property (nonatomic, strong) id waitingForRequestToComplete; @end diff --git a/submodules/MtProtoKit/PublicHeaders/MtProtoKit/MTSessionInfo.h b/submodules/MtProtoKit/PublicHeaders/MtProtoKit/MTSessionInfo.h index ec32fbbce6..a3a3f2f262 100644 --- a/submodules/MtProtoKit/PublicHeaders/MtProtoKit/MTSessionInfo.h +++ b/submodules/MtProtoKit/PublicHeaders/MtProtoKit/MTSessionInfo.h @@ -19,6 +19,8 @@ - (bool)messageProcessed:(int64_t)messageId; - (void)setMessageProcessed:(int64_t)messageId; +- (bool)wasMessageSentOnce:(int64_t)messageId; +- (void)setMessageWasSentOnce:(int64_t)messageId; - (void)scheduleMessageConfirmation:(int64_t)messageId size:(NSInteger)size; - (NSArray *)scheduledMessageConfirmations; - (bool)scheduledMessageConfirmationsExceedSize:(NSInteger)sizeLimit orCount:(NSUInteger)countLimit; diff --git a/submodules/MtProtoKit/Sources/GCDAsyncSocket.m b/submodules/MtProtoKit/Sources/GCDAsyncSocket.m index b3668549b3..0670de7879 100755 --- a/submodules/MtProtoKit/Sources/GCDAsyncSocket.m +++ b/submodules/MtProtoKit/Sources/GCDAsyncSocket.m @@ -2402,22 +2402,21 @@ enum GCDAsyncSocketConfig int nosigpipe = 1; setsockopt(socketFD, SOL_SOCKET, SO_NOSIGPIPE, &nosigpipe, sizeof(nosigpipe)); - /*int32_t rcvBuf = 400 * 1024; + int32_t rcvBuf = 1024 * 1024; setsockopt(socketFD, SOL_SOCKET, SO_RCVBUF, &rcvBuf, 4); int32_t checkRcvBuf = 0; unsigned int checkRcvBufLen = sizeof(checkRcvBuf); getsockopt(socketFD, SOL_SOCKET, SO_RCVBUF, &checkRcvBuf, &checkRcvBufLen); - int32_t sndBuf = 400 * 1024; + int32_t sndBuf = 1024 * 1024; setsockopt(socketFD, SOL_SOCKET, SO_SNDBUF, &sndBuf, 4); int32_t checkSndBuf = 0; unsigned int checkSndBufLen = sizeof(checkSndBuf); getsockopt(socketFD, SOL_SOCKET, SO_SNDBUF, &checkSndBuf, &checkSndBufLen); - */ - if (_useTcpNodelay) + if (_useTcpNodelay || true) { int flag = 1; setsockopt(socketFD, SOL_SOCKET, TCP_NODELAY, &flag, sizeof(flag)); diff --git a/submodules/MtProtoKit/Sources/MTApiEnvironment.m b/submodules/MtProtoKit/Sources/MTApiEnvironment.m index f8dd947b67..bcab215524 100644 --- a/submodules/MtProtoKit/Sources/MTApiEnvironment.m +++ b/submodules/MtProtoKit/Sources/MTApiEnvironment.m @@ -657,7 +657,7 @@ NSString *suffix = @""; [platform isEqualToString:@"iPad8,2"] || [platform isEqualToString:@"iPad8,3"] || [platform isEqualToString:@"iPad8,4"]) - return @"iPad Pro 11 inch (3rd gen)"; + return @"iPad Pro 11 inch"; if ([platform isEqualToString:@"iPad8,5"] || [platform isEqualToString:@"iPad8,6"] || @@ -667,7 +667,7 @@ NSString *suffix = @""; if ([platform isEqualToString:@"iPad8,9"] || [platform isEqualToString:@"iPad8,10"]) - return @"iPad Pro 11 inch (4th gen)"; + return @"iPad Pro 11 inch (2th gen)"; if ([platform isEqualToString:@"iPad8,11"] || [platform isEqualToString:@"iPad8,12"]) @@ -688,6 +688,18 @@ NSString *suffix = @""; if ([platform isEqualToString:@"iPad13,1"] || [platform isEqualToString:@"iPad13,2"]) return @"iPad Air (4th gen)"; + + if ([platform isEqualToString:@"iPad13,4"] || + [platform isEqualToString:@"iPad13,5"] || + [platform isEqualToString:@"iPad13,6"] || + [platform isEqualToString:@"iPad13,7"]) + return @"iPad Pro 11 inch (3th gen)"; + + if ([platform isEqualToString:@"iPad13,8"] || + [platform isEqualToString:@"iPad13,9"] || + [platform isEqualToString:@"iPad13,10"] || + [platform isEqualToString:@"iPad13,11"]) + return @"iPad Pro 12.9 inch (5th gen)"; if ([platform hasPrefix:@"iPhone"]) return @"Unknown iPhone"; diff --git a/submodules/MtProtoKit/Sources/MTContext.m b/submodules/MtProtoKit/Sources/MTContext.m index b394ac923c..bde013a54e 100644 --- a/submodules/MtProtoKit/Sources/MTContext.m +++ b/submodules/MtProtoKit/Sources/MTContext.m @@ -379,14 +379,14 @@ static int32_t fixedTimeDifferenceValue = 0; NSDictionary *datacenterAuthInfoById = [keychain objectForKey:@"datacenterAuthInfoById" group:@"persistent"]; if (datacenterAuthInfoById != nil) { _datacenterAuthInfoById = [[NSMutableDictionary alloc] initWithDictionary:datacenterAuthInfoById]; -/*#if DEBUG +#if DEBUG NSArray *keys = [_datacenterAuthInfoById allKeys]; for (NSNumber *key in keys) { if (parseAuthInfoMapKeyInteger(key).selector != MTDatacenterAuthInfoSelectorPersistent) { [_datacenterAuthInfoById removeObjectForKey:key]; } } -#endif*/ +#endif } NSDictionary *datacenterPublicKeysById = [keychain objectForKey:@"datacenterPublicKeysById" group:@"ephemeral"]; diff --git a/submodules/MtProtoKit/Sources/MTDatacenterAuthMessageService.m b/submodules/MtProtoKit/Sources/MTDatacenterAuthMessageService.m index da1ca4e98a..ac200f686b 100644 --- a/submodules/MtProtoKit/Sources/MTDatacenterAuthMessageService.m +++ b/submodules/MtProtoKit/Sources/MTDatacenterAuthMessageService.m @@ -13,6 +13,7 @@ #import #import "MTBuffer.h" #import +#import #import "MTInternalMessageParser.h" #import "MTServerDhInnerDataMessage.h" @@ -20,99 +21,70 @@ #import "MTServerDhParamsMessage.h" #import "MTSetClientDhParamsResponseMessage.h" -static NSArray *defaultPublicKeys() { - static NSArray *serverPublicKeys = nil; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^ - { - serverPublicKeys = [[NSArray alloc] initWithObjects: -[[NSDictionary alloc] initWithObjectsAndKeys:@"-----BEGIN RSA PUBLIC KEY-----\n" -"MIIBCgKCAQEAxq7aeLAqJR20tkQQMfRn+ocfrtMlJsQ2Uksfs7Xcoo77jAid0bRt\n" -"ksiVmT2HEIJUlRxfABoPBV8wY9zRTUMaMA654pUX41mhyVN+XoerGxFvrs9dF1Ru\n" -"vCHbI02dM2ppPvyytvvMoefRoL5BTcpAihFgm5xCaakgsJ/tH5oVl74CdhQw8J5L\n" -"xI/K++KJBUyZ26Uba1632cOiq05JBUW0Z2vWIOk4BLysk7+U9z+SxynKiZR3/xdi\n" -"XvFKk01R3BHV+GUKM2RYazpS/P8v7eyKhAbKxOdRcFpHLlVwfjyM1VlDQrEZxsMp\n" -"NTLYXb6Sce1Uov0YtNx5wEowlREH1WOTlwIDAQAB\n" -"-----END RSA PUBLIC KEY-----", @"key", [[NSNumber alloc] initWithUnsignedLongLong:0x9a996a1db11c729bUL], @"fingerprint", nil], -[[NSDictionary alloc] initWithObjectsAndKeys:@"-----BEGIN RSA PUBLIC KEY-----\n" -"MIIBCgKCAQEAsQZnSWVZNfClk29RcDTJQ76n8zZaiTGuUsi8sUhW8AS4PSbPKDm+\n" -"DyJgdHDWdIF3HBzl7DHeFrILuqTs0vfS7Pa2NW8nUBwiaYQmPtwEa4n7bTmBVGsB\n" -"1700/tz8wQWOLUlL2nMv+BPlDhxq4kmJCyJfgrIrHlX8sGPcPA4Y6Rwo0MSqYn3s\n" -"g1Pu5gOKlaT9HKmE6wn5Sut6IiBjWozrRQ6n5h2RXNtO7O2qCDqjgB2vBxhV7B+z\n" -"hRbLbCmW0tYMDsvPpX5M8fsO05svN+lKtCAuz1leFns8piZpptpSCFn7bWxiA9/f\n" -"x5x17D7pfah3Sy2pA+NDXyzSlGcKdaUmwQIDAQAB\n" -"-----END RSA PUBLIC KEY-----", @"key", [[NSNumber alloc] initWithUnsignedLongLong:0xb05b2a6f70cdea78UL], @"fingerprint", nil], -[[NSDictionary alloc] initWithObjectsAndKeys:@"-----BEGIN RSA PUBLIC KEY-----\n" -"MIIBCgKCAQEAwVACPi9w23mF3tBkdZz+zwrzKOaaQdr01vAbU4E1pvkfj4sqDsm6\n" -"lyDONS789sVoD/xCS9Y0hkkC3gtL1tSfTlgCMOOul9lcixlEKzwKENj1Yz/s7daS\n" -"an9tqw3bfUV/nqgbhGX81v/+7RFAEd+RwFnK7a+XYl9sluzHRyVVaTTveB2GazTw\n" -"Efzk2DWgkBluml8OREmvfraX3bkHZJTKX4EQSjBbbdJ2ZXIsRrYOXfaA+xayEGB+\n" -"8hdlLmAjbCVfaigxX0CDqWeR1yFL9kwd9P0NsZRPsmoqVwMbMu7mStFai6aIhc3n\n" -"Slv8kg9qv1m6XHVQY3PnEw+QQtqSIXklHwIDAQAB\n" -"-----END RSA PUBLIC KEY-----", @"key", [[NSNumber alloc] initWithUnsignedLongLong:0xc3b42b026ce86b21UL], @"fingerprint", nil], -[[NSDictionary alloc] initWithObjectsAndKeys:@"-----BEGIN RSA PUBLIC KEY-----\n" -"MIIBCgKCAQEAwqjFW0pi4reKGbkc9pK83Eunwj/k0G8ZTioMMPbZmW99GivMibwa\n" -"xDM9RDWabEMyUtGoQC2ZcDeLWRK3W8jMP6dnEKAlvLkDLfC4fXYHzFO5KHEqF06i\n" -"qAqBdmI1iBGdQv/OQCBcbXIWCGDY2AsiqLhlGQfPOI7/vvKc188rTriocgUtoTUc\n" -"/n/sIUzkgwTqRyvWYynWARWzQg0I9olLBBC2q5RQJJlnYXZwyTL3y9tdb7zOHkks\n" -"WV9IMQmZmyZh/N7sMbGWQpt4NMchGpPGeJ2e5gHBjDnlIf2p1yZOYeUYrdbwcS0t\n" -"UiggS4UeE8TzIuXFQxw7fzEIlmhIaq3FnwIDAQAB\n" -"-----END RSA PUBLIC KEY-----", @"key", [[NSNumber alloc] initWithUnsignedLongLong:0x71e025b6c76033e3UL], @"fingerprint", nil], -[[NSDictionary alloc] initWithObjectsAndKeys:@"-----BEGIN RSA PUBLIC KEY-----\n" -"MIIBCgKCAQEAruw2yP/BCcsJliRoW5eBVBVle9dtjJw+OYED160Wybum9SXtBBLX\n" -"riwt4rROd9csv0t0OHCaTmRqBcQ0J8fxhN6/cpR1GWgOZRUAiQxoMnlt0R93LCX/\n" -"j1dnVa/gVbCjdSxpbrfY2g2L4frzjJvdl84Kd9ORYjDEAyFnEA7dD556OptgLQQ2\n" -"e2iVNq8NZLYTzLp5YpOdO1doK+ttrltggTCy5SrKeLoCPPbOgGsdxJxyz5KKcZnS\n" -"Lj16yE5HvJQn0CNpRdENvRUXe6tBP78O39oJ8BTHp9oIjd6XWXAsp2CvK45Ol8wF\n" -"XGF710w9lwCGNbmNxNYhtIkdqfsEcwR5JwIDAQAB\n" -"-----END RSA PUBLIC KEY-----", @"key", [[NSNumber alloc] initWithUnsignedLongLong:0xbc35f3509f7b7a5UL], @"fingerprint", nil], -[[NSDictionary alloc] initWithObjectsAndKeys:@"-----BEGIN RSA PUBLIC KEY-----\n" -"MIIBCgKCAQEAvfLHfYH2r9R70w8prHblWt/nDkh+XkgpflqQVcnAfSuTtO05lNPs\n" -"pQmL8Y2XjVT4t8cT6xAkdgfmmvnvRPOOKPi0OfJXoRVylFzAQG/j83u5K3kRLbae\n" -"7fLccVhKZhY46lvsueI1hQdLgNV9n1cQ3TDS2pQOCtovG4eDl9wacrXOJTG2990V\n" -"jgnIKNA0UMoP+KF03qzryqIt3oTvZq03DyWdGK+AZjgBLaDKSnC6qD2cFY81UryR\n" -"WOab8zKkWAnhw2kFpcqhI0jdV5QaSCExvnsjVaX0Y1N0870931/5Jb9ICe4nweZ9\n" -"kSDF/gip3kWLG0o8XQpChDfyvsqB9OLV/wIDAQAB\n" -"-----END RSA PUBLIC KEY-----", @"key", [[NSNumber alloc] initWithUnsignedLongLong:0x15ae5fa8b5529542UL], @"fingerprint", nil], -[[NSDictionary alloc] initWithObjectsAndKeys:@"-----BEGIN RSA PUBLIC KEY-----\n" -"MIIBCgKCAQEAs/ditzm+mPND6xkhzwFIz6J/968CtkcSE/7Z2qAJiXbmZ3UDJPGr\n" -"zqTDHkO30R8VeRM/Kz2f4nR05GIFiITl4bEjvpy7xqRDspJcCFIOcyXm8abVDhF+\n" -"th6knSU0yLtNKuQVP6voMrnt9MV1X92LGZQLgdHZbPQz0Z5qIpaKhdyA8DEvWWvS\n" -"Uwwc+yi1/gGaybwlzZwqXYoPOhwMebzKUk0xW14htcJrRrq+PXXQbRzTMynseCoP\n" -"Ioke0dtCodbA3qQxQovE16q9zz4Otv2k4j63cz53J+mhkVWAeWxVGI0lltJmWtEY\n" -"K6er8VqqWot3nqmWMXogrgRLggv/NbbooQIDAQAB\n" -"-----END RSA PUBLIC KEY-----", @"key", [[NSNumber alloc] initWithUnsignedLongLong:0xaeae98e13cd7f94fUL], @"fingerprint", nil], -[[NSDictionary alloc] initWithObjectsAndKeys:@"-----BEGIN RSA PUBLIC KEY-----\n" -"MIIBCgKCAQEAvmpxVY7ld/8DAjz6F6q05shjg8/4p6047bn6/m8yPy1RBsvIyvuD\n" -"uGnP/RzPEhzXQ9UJ5Ynmh2XJZgHoE9xbnfxL5BXHplJhMtADXKM9bWB11PU1Eioc\n" -"3+AXBB8QiNFBn2XI5UkO5hPhbb9mJpjA9Uhw8EdfqJP8QetVsI/xrCEbwEXe0xvi\n" -"fRLJbY08/Gp66KpQvy7g8w7VB8wlgePexW3pT13Ap6vuC+mQuJPyiHvSxjEKHgqe\n" -"Pji9NP3tJUFQjcECqcm0yV7/2d0t/pbCm+ZH1sadZspQCEPPrtbkQBlvHb4OLiIW\n" -"PGHKSMeRFvp3IWcmdJqXahxLCUS1Eh6MAQIDAQAB\n" -"-----END RSA PUBLIC KEY-----", @"key", [[NSNumber alloc] initWithUnsignedLongLong:0x5a181b2235057d98UL], @"fingerprint", nil], -[[NSDictionary alloc] initWithObjectsAndKeys:@"-----BEGIN RSA PUBLIC KEY-----\n" -"MIIBCgKCAQEAr4v4wxMDXIaMOh8bayF/NyoYdpcysn5EbjTIOZC0RkgzsRj3SGlu\n" -"52QSz+ysO41dQAjpFLgxPVJoOlxXokaOq827IfW0bGCm0doT5hxtedu9UCQKbE8j\n" -"lDOk+kWMXHPZFJKWRgKgTu9hcB3y3Vk+JFfLpq3d5ZB48B4bcwrRQnzkx5GhWOFX\n" -"x73ZgjO93eoQ2b/lDyXxK4B4IS+hZhjzezPZTI5upTRbs5ljlApsddsHrKk6jJNj\n" -"8Ygs/ps8e6ct82jLXbnndC9s8HjEvDvBPH9IPjv5JUlmHMBFZ5vFQIfbpo0u0+1P\n" -"n6bkEi5o7/ifoyVv2pAZTRwppTz0EuXD8QIDAQAB\n" -"-----END RSA PUBLIC KEY-----", @"key", [[NSNumber alloc] initWithUnsignedLongLong:0x9692106da14b9f02UL], @"fingerprint", nil], -nil]; - }); - return serverPublicKeys; +@interface MTDatacenterAuthPublicKey : NSObject + +@property (nonatomic, strong, readonly) NSString *publicKey; + +@end + +@implementation MTDatacenterAuthPublicKey + +- (instancetype)initWithPublicKey:(NSString *)publicKey { + self = [super init]; + if (self != nil) { + _publicKey = publicKey; + } + return self; } -static NSDictionary *selectPublicKey(NSArray *fingerprints, NSArray *publicKeys) -{ - for (NSNumber *nFingerprint in fingerprints) - { - for (NSDictionary *keyDesc in publicKeys) - { - uint64_t keyFingerprint = [[keyDesc objectForKey:@"fingerprint"] unsignedLongLongValue]; +- (uint64_t)fingerprintWithEncryptionProvider:(id)encryptionProvider { + return MTRsaFingerprint(encryptionProvider, _publicKey); +} + +@end + +static NSArray *defaultPublicKeys(bool isProduction) { + static NSArray *testingPublicKeys = nil; + static NSArray *productionPublicKeys = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + testingPublicKeys = @[ + [[MTDatacenterAuthPublicKey alloc] initWithPublicKey:@"-----BEGIN RSA PUBLIC KEY-----\n" + "MIIBCgKCAQEAyMEdY1aR+sCR3ZSJrtztKTKqigvO/vBfqACJLZtS7QMgCGXJ6XIR\n" + "yy7mx66W0/sOFa7/1mAZtEoIokDP3ShoqF4fVNb6XeqgQfaUHd8wJpDWHcR2OFwv\n" + "plUUI1PLTktZ9uW2WE23b+ixNwJjJGwBDJPQEQFBE+vfmH0JP503wr5INS1poWg/\n" + "j25sIWeYPHYeOrFp/eXaqhISP6G+q2IeTaWTXpwZj4LzXq5YOpk4bYEQ6mvRq7D1\n" + "aHWfYmlEGepfaYR8Q0YqvvhYtMte3ITnuSJs171+GDqpdKcSwHnd6FudwGO4pcCO\n" + "j4WcDuXc2CTHgH8gFTNhp/Y8/SpDOhvn9QIDAQAB\n" + "-----END RSA PUBLIC KEY-----"] + ]; + + productionPublicKeys = @[ + [[MTDatacenterAuthPublicKey alloc] initWithPublicKey:@"-----BEGIN RSA PUBLIC KEY-----\n" + "MIIBCgKCAQEA6LszBcC1LGzyr992NzE0ieY+BSaOW622Aa9Bd4ZHLl+TuFQ4lo4g\n" + "5nKaMBwK/BIb9xUfg0Q29/2mgIR6Zr9krM7HjuIcCzFvDtr+L0GQjae9H0pRB2OO\n" + "62cECs5HKhT5DZ98K33vmWiLowc621dQuwKWSQKjWf50XYFw42h21P2KXUGyp2y/\n" + "+aEyZ+uVgLLQbRA1dEjSDZ2iGRy12Mk5gpYc397aYp438fsJoHIgJ2lgMv5h7WY9\n" + "t6N/byY9Nw9p21Og3AoXSL2q/2IJ1WRUhebgAdGVMlV1fkuOQoEzR7EdpqtQD9Cs\n" + "5+bfo3Nhmcyvk5ftB0WkJ9z6bNZ7yxrP8wIDAQAB\n" + "-----END RSA PUBLIC KEY-----"] + ]; + }); + if (isProduction) { + return productionPublicKeys; + } else { + return testingPublicKeys; + } +} + +static MTDatacenterAuthPublicKey *selectPublicKey(id encryptionProvider, NSArray *fingerprints, NSArray *publicKeys) { + for (NSNumber *nFingerprint in fingerprints) { + for (MTDatacenterAuthPublicKey *key in publicKeys) { + uint64_t keyFingerprint = [key fingerprintWithEncryptionProvider:encryptionProvider]; - if ([nFingerprint unsignedLongLongValue] == keyFingerprint) - return keyDesc; + if ([nFingerprint unsignedLongLongValue] == keyFingerprint) { + return key; + } } } @@ -150,7 +122,7 @@ typedef enum { MTDatacenterAuthKey *_authKey; NSData *_encryptedClientData; - NSArray *_publicKeys; + NSArray *_publicKeys; } @end @@ -168,6 +140,17 @@ typedef enum { return self; } +- (NSArray *)convertPublicKeysFromDictionaries:(NSArray *)list { + NSMutableArray *cdnKeys = [[NSMutableArray alloc] init]; + for (NSDictionary *dict in list) { + NSString *key = dict[@"key"]; + if ([key isKindOfClass:[NSString class]]) { + [cdnKeys addObject:[[MTDatacenterAuthPublicKey alloc] initWithPublicKey:key]]; + } + } + return cdnKeys; +} + - (void)reset:(MTProto *)mtProto { _currentStageMessageId = 0; @@ -187,15 +170,15 @@ typedef enum { _encryptedClientData = nil; if (mtProto.cdn) { - _publicKeys = [mtProto.context publicKeysForDatacenterWithId:mtProto.datacenterId]; - if (_publicKeys == nil) { + _publicKeys = [self convertPublicKeysFromDictionaries:[mtProto.context publicKeysForDatacenterWithId:mtProto.datacenterId]]; + if (_publicKeys.count == 0) { _stage = MTDatacenterAuthStageWaitingForPublicKeys; [mtProto.context publicKeysForDatacenterWithIdRequired:mtProto.datacenterId]; } else { _stage = MTDatacenterAuthStagePQ; } } else { - _publicKeys = defaultPublicKeys(); + _publicKeys = defaultPublicKeys(!mtProto.context.isTestingEnvironment); _stage = MTDatacenterAuthStagePQ; } @@ -209,9 +192,13 @@ typedef enum { } - (void)mtProtoPublicKeysUpdated:(MTProto *)mtProto datacenterId:(NSInteger)datacenterId publicKeys:(NSArray *)publicKeys { + if (!mtProto.cdn) { + return; + } + if (_stage == MTDatacenterAuthStageWaitingForPublicKeys) { if (mtProto.datacenterId == datacenterId) { - _publicKeys = publicKeys; + _publicKeys = [self convertPublicKeysFromDictionaries:publicKeys]; if (_publicKeys != nil && _publicKeys.count != 0) { _stage = MTDatacenterAuthStagePQ; [mtProto requestTransportTransaction]; @@ -238,7 +225,7 @@ typedef enum { } MTBuffer *reqPqBuffer = [[MTBuffer alloc] init]; - [reqPqBuffer appendInt32:(int32_t)0x60469778]; + [reqPqBuffer appendInt32:(int32_t)0xbe7e8ef1]; [reqPqBuffer appendBytes:_nonce.bytes length:_nonce.length]; NSString *messageDescription = [NSString stringWithFormat:@"reqPq nonce:%@", _nonce]; @@ -306,6 +293,113 @@ typedef enum { return nil; } +static NSData *reversedBytes(NSData *data) { + NSMutableData *result = [[NSMutableData alloc] initWithLength:data.length]; + for (NSUInteger i = 0; i < result.length; i++) { + ((uint8_t *)result.mutableBytes)[i] = ((uint8_t *)data.bytes)[result.length - i - 1]; + } + return result; +} + +static NSData *encryptRSAModernPadding(id encryptionProvider, NSData *pqInnerData, NSString *publicKey) { + NSMutableData *dataWithPadding = [[NSMutableData alloc] init]; + [dataWithPadding appendData:pqInnerData]; + if (dataWithPadding.length > 144) { + return nil; + } + if (dataWithPadding.length != 192) { + int originalLength = (int)dataWithPadding.length; + int numPaddingBytes = 192 - originalLength; + [dataWithPadding setLength:192]; + int randomResult = SecRandomCopyBytes(kSecRandomDefault, numPaddingBytes, ((uint8_t *)dataWithPadding.mutableBytes) + originalLength); + if (randomResult != errSecSuccess) { + return nil; + } + } + + NSData *dataWithPaddingReversed = reversedBytes(dataWithPadding); + + while (true) { + int randomResult = 0; + NSMutableData *tempKey = [[NSMutableData alloc] initWithLength:32]; + randomResult = SecRandomCopyBytes(kSecRandomDefault, tempKey.length, tempKey.mutableBytes); + if (randomResult != errSecSuccess) { + return nil; + } + + NSMutableData *tempKeyAndDataWithPadding = [[NSMutableData alloc] init]; + [tempKeyAndDataWithPadding appendData:tempKey]; + [tempKeyAndDataWithPadding appendData:dataWithPadding]; + + NSMutableData *dataWithHash = [[NSMutableData alloc] init]; + [dataWithHash appendData:dataWithPaddingReversed]; + [dataWithHash appendData:MTSha256(tempKeyAndDataWithPadding)]; + if (dataWithHash.length != 224) { + return nil; + } + + NSMutableData *zeroIv = [[NSMutableData alloc] initWithLength:32]; + memset(zeroIv.mutableBytes, 0, zeroIv.length); + + NSData *aesEncrypted = MTAesEncrypt(dataWithHash, tempKey, zeroIv); + if (aesEncrypted == nil) { + return nil; + } + NSData *shaAesEncrypted = MTSha256(aesEncrypted); + + NSMutableData *tempKeyXor = [[NSMutableData alloc] initWithLength:tempKey.length]; + if (tempKeyXor.length != shaAesEncrypted.length) { + return nil; + } + for (NSUInteger i = 0; i < tempKey.length; i++) { + ((uint8_t *)tempKeyXor.mutableBytes)[i] = ((uint8_t *)tempKey.bytes)[i] ^ ((uint8_t *)shaAesEncrypted.bytes)[i]; + } + + NSMutableData *keyAesEncrypted = [[NSMutableData alloc] init]; + [keyAesEncrypted appendData:tempKeyXor]; + [keyAesEncrypted appendData:aesEncrypted]; + if (keyAesEncrypted.length != 256) { + return nil; + } + + id bignumContext = [encryptionProvider createBignumContext]; + if (bignumContext == nil) { + return nil; + } + id rsaPublicKey = [encryptionProvider parseRSAPublicKey:publicKey]; + if (rsaPublicKey == nil) { + return nil; + } + id rsaModule = [bignumContext rsaGetN:rsaPublicKey]; + if (rsaModule == nil) { + return nil; + } + id bignumKeyAesEncrypted = [bignumContext create]; + if (bignumKeyAesEncrypted == nil) { + return nil; + } + [bignumContext assignBinTo:bignumKeyAesEncrypted value:keyAesEncrypted]; + int compareResult = [bignumContext compare:rsaModule with:bignumKeyAesEncrypted]; + if (compareResult <= 0) { + continue; + } + + NSData *encryptedData = [encryptionProvider rsaEncryptWithPublicKey:publicKey data:keyAesEncrypted]; + NSMutableData *paddedEncryptedData = [[NSMutableData alloc] init]; + [paddedEncryptedData appendData:encryptedData]; + while (paddedEncryptedData.length < 256) { + uint8_t zero = 0; + [paddedEncryptedData replaceBytesInRange:NSMakeRange(0, 0) withBytes:&zero length:1]; + } + + if (paddedEncryptedData.length != 256) { + return nil; + } + + return paddedEncryptedData; + } +} + - (void)mtProto:(MTProto *)mtProto receivedMessage:(MTIncomingMessage *)message authInfoSelector:(MTDatacenterAuthInfoSelector)authInfoSelector { if (_stage == MTDatacenterAuthStagePQ && [message.body isKindOfClass:[MTResPqMessage class]]) @@ -314,10 +408,10 @@ typedef enum { if ([_nonce isEqualToData:resPqMessage.nonce]) { - NSDictionary *publicKey = selectPublicKey(resPqMessage.serverPublicKeyFingerprints, _publicKeys); + MTDatacenterAuthPublicKey *publicKey = selectPublicKey(_encryptionProvider, resPqMessage.serverPublicKeyFingerprints, _publicKeys); if (publicKey == nil && mtProto.cdn && resPqMessage.serverPublicKeyFingerprints.count == 1 && _publicKeys.count == 1) { - publicKey = @{@"key": _publicKeys[0][@"key"], @"fingerprint": resPqMessage.serverPublicKeyFingerprints[0]}; + publicKey = _publicKeys[0]; } if (publicKey == nil) @@ -367,7 +461,7 @@ typedef enum { } while (q > 0); _dhQ = qBytes; - _dhPublicKeyFingerprint = [[publicKey objectForKey:@"fingerprint"] longLongValue]; + _dhPublicKeyFingerprint = [publicKey fingerprintWithEncryptionProvider:_encryptionProvider]; uint8_t nonceBytes[32]; __unused int result = SecRandomCopyBytes(kSecRandomDefault, 32, nonceBytes); @@ -390,37 +484,14 @@ typedef enum { [innerDataBuffer appendInt32:mtProto.context.tempKeyExpiration]; NSData *innerDataBytes = innerDataBuffer.data; - - NSMutableData *dataWithHash = [[NSMutableData alloc] init]; - [dataWithHash appendData:MTSha1(innerDataBytes)]; - [dataWithHash appendData:innerDataBytes]; - while (dataWithHash.length < 255) - { - uint8_t random = 0; - arc4random_buf(&random, 1); - [dataWithHash appendBytes:&random length:1]; - } - - NSData *encryptedData = MTRsaEncrypt(_encryptionProvider, [publicKey objectForKey:@"key"], dataWithHash); + NSData *encryptedData = nil; + + encryptedData = encryptRSAModernPadding(_encryptionProvider, innerDataBytes, publicKey.publicKey); + if (MTLogEnabled()) { - MTLog(@"[MTDatacenterAuthMessageService#%p encryptedData length %d dataWithHash length %d]", self, (int)encryptedData.length, (int)dataWithHash.length); + MTLog(@"[MTDatacenterAuthMessageService#%p encryptedData length %d]", self, (int)encryptedData.length); } - if (encryptedData.length < 256) - { - NSMutableData *newEncryptedData = [[NSMutableData alloc] init]; - for (int i = 0; i < 256 - (int)encryptedData.length; i++) - { - uint8_t random = 0; - arc4random_buf(&random, 1); - [newEncryptedData appendBytes:&random length:1]; - } - [newEncryptedData appendData:encryptedData]; - encryptedData = newEncryptedData; - } - #if DEBUG - assert(encryptedData.length == 256); - #endif - + _dhEncryptedData = encryptedData; } else { MTBuffer *innerDataBuffer = [[MTBuffer alloc] init]; @@ -433,42 +504,27 @@ typedef enum { [innerDataBuffer appendBytes:_newNonce.bytes length:_newNonce.length]; NSData *innerDataBytes = innerDataBuffer.data; - - NSMutableData *dataWithHash = [[NSMutableData alloc] init]; - [dataWithHash appendData:MTSha1(innerDataBytes)]; - [dataWithHash appendData:innerDataBytes]; - while (dataWithHash.length < 255) - { - uint8_t random = 0; - arc4random_buf(&random, 1); - [dataWithHash appendBytes:&random length:1]; - } - NSData *encryptedData = MTRsaEncrypt(_encryptionProvider, [publicKey objectForKey:@"key"], dataWithHash); - if (MTLogEnabled()) { - MTLog(@"[MTDatacenterAuthMessageService#%p encryptedData length %d dataWithHash length %d]", self, (int)encryptedData.length, (int)dataWithHash.length); - } - if (encryptedData.length < 256) - { - NSMutableData *newEncryptedData = [[NSMutableData alloc] init]; - for (int i = 0; i < 256 - (int)encryptedData.length; i++) - { - uint8_t random = 0; - arc4random_buf(&random, 1); - [newEncryptedData appendBytes:&random length:1]; - } - [newEncryptedData appendData:encryptedData]; - encryptedData = newEncryptedData; - } + NSData *encryptedData = nil; + + encryptedData = encryptRSAModernPadding(_encryptionProvider, innerDataBytes, publicKey.publicKey); _dhEncryptedData = encryptedData; } - - _stage = MTDatacenterAuthStageReqDH; - _currentStageMessageId = 0; - _currentStageMessageSeqNo = 0; - _currentStageTransactionId = nil; - [mtProto requestTransportTransaction]; + + if (_dhEncryptedData == nil) { + _stage = MTDatacenterAuthStagePQ; + _currentStageMessageId = 0; + _currentStageMessageSeqNo = 0; + _currentStageTransactionId = nil; + [mtProto requestTransportTransaction]; + } else { + _stage = MTDatacenterAuthStageReqDH; + _currentStageMessageId = 0; + _currentStageMessageSeqNo = 0; + _currentStageTransactionId = nil; + [mtProto requestTransportTransaction]; + } } } } diff --git a/submodules/MtProtoKit/Sources/MTProto.m b/submodules/MtProtoKit/Sources/MTProto.m index c0c1d8177b..fd0dc1ce8e 100644 --- a/submodules/MtProtoKit/Sources/MTProto.m +++ b/submodules/MtProtoKit/Sources/MTProto.m @@ -1148,7 +1148,7 @@ static const NSUInteger MTMaxUnacknowledgedMessageCount = 64; { if (!_useUnauthorizedMode) { - NSMutableArray *currentContainerMessages = [[NSMutableArray alloc] init]; + NSMutableArray *currentContainerMessages = [[NSMutableArray alloc] init]; NSUInteger currentContainerSize = 0; for (NSUInteger j = i; j < transactionMessageList.count; j++, i++) @@ -1186,8 +1186,9 @@ static const NSUInteger MTMaxUnacknowledgedMessageCount = 64; } } - if (currentContainerMessages.count == 1) + if (currentContainerMessages.count == 1 && ![transactionSessionInfo wasMessageSentOnce:currentContainerMessages[0].messageId]) { + [transactionSessionInfo setMessageWasSentOnce:currentContainerMessages[0].messageId]; int32_t quickAckId = 0; NSData *messageData = [self _dataForEncryptedMessage:currentContainerMessages[0] authKey:authKey sessionInfo:transactionSessionInfo quickAckId:&quickAckId address:scheme.address extendedPadding:extendedPadding]; if (messageData != nil) @@ -2054,7 +2055,7 @@ static NSString *dumpHexString(NSData *data, int maxLength) { [self getAuthKeyForCurrentScheme:scheme createIfNeeded:false authInfoSelector:&authInfoSelector]; if (MTLogEnabled()) { - MTLog(@"[MTProto#%p@%p missing key %lld selector %d]", self, _context, _validAuthInfo.authInfo.authKeyId, authInfoSelector); + MTLog(@"[MTProto#%p@%p missing key %lld selector %d useExplicitAuthKey: %lld, canResetAuthData: %s]", self, _context, _validAuthInfo.authInfo.authKeyId, authInfoSelector, _useExplicitAuthKey.authKeyId, _canResetAuthData ? "true" : "false"); } if (_useExplicitAuthKey != nil) { @@ -2112,6 +2113,19 @@ static NSString *dumpHexString(NSData *data, int maxLength) { } } +static bool isDataEqualToDataConstTime(NSData *data1, NSData *data2) { + if (data1.length != data2.length) { + return false; + } + uint8_t const *bytes1 = data1.bytes; + uint8_t const *bytes2 = data2.bytes; + int result = 0; + for (int i = 0; i < data1.length; i++) { + result |= bytes1[i] != bytes2[i]; + } + return result == 0; +} + - (NSData *)_decryptIncomingTransportData:(NSData *)transportData address:(MTDatacenterAddress *)address authKey:(MTDatacenterAuthKey *)authKey { MTDatacenterAuthKey *effectiveAuthKey = authKey; @@ -2138,20 +2152,6 @@ static NSString *dumpHexString(NSData *data, int maxLength) { NSData *decryptedData = MTAesDecrypt(dataToDecrypt, encryptionKey.key, encryptionKey.iv); - int32_t messageDataLength = 0; - [decryptedData getBytes:&messageDataLength range:NSMakeRange(28, 4)]; - - int32_t paddingLength = ((int32_t)decryptedData.length) - messageDataLength; - if (paddingLength < 12 || paddingLength > 1024) { - __unused NSData *result = MTSha256(decryptedData); - return nil; - } - - if (messageDataLength < 0 || messageDataLength > (int32_t)decryptedData.length) { - __unused NSData *result = MTSha256(decryptedData); - return nil; - } - int xValue = 8; NSMutableData *msgKeyLargeData = [[NSMutableData alloc] init]; [msgKeyLargeData appendBytes:effectiveAuthKey.authKey.bytes + 88 + xValue length:32]; @@ -2160,8 +2160,21 @@ static NSString *dumpHexString(NSData *data, int maxLength) { NSData *msgKeyLarge = MTSha256(msgKeyLargeData); NSData *messageKey = [msgKeyLarge subdataWithRange:NSMakeRange(8, 16)]; - if (![messageKey isEqualToData:embeddedMessageKey]) + if (!isDataEqualToDataConstTime(messageKey, embeddedMessageKey)) { return nil; + } + + int32_t messageDataLength = 0; + [decryptedData getBytes:&messageDataLength range:NSMakeRange(28, 4)]; + + int32_t paddingLength = ((int32_t)decryptedData.length) - messageDataLength; + if (paddingLength < 12 || paddingLength > 1024) { + return nil; + } + + if (messageDataLength < 0 || messageDataLength > (int32_t)decryptedData.length) { + return nil; + } return decryptedData; } diff --git a/submodules/MtProtoKit/Sources/MTRequestMessageService.m b/submodules/MtProtoKit/Sources/MTRequestMessageService.m index 2a3d4963d6..0b91e6d40e 100644 --- a/submodules/MtProtoKit/Sources/MTRequestMessageService.m +++ b/submodules/MtProtoKit/Sources/MTRequestMessageService.m @@ -196,6 +196,20 @@ { if (request.errorContext != nil) { + if (request.errorContext.waitingForRequestToComplete != nil) { + bool foundDependency = false; + for (MTRequest *anotherRequest in _requests) { + if (request.errorContext.waitingForRequestToComplete == anotherRequest.internalId) { + foundDependency = true; + break; + } + } + + if (!foundDependency) { + needTransaction = true; + } + } + if (request.requestContext == nil) { if (request.errorContext.minimalExecuteTime > currentTime + DBL_EPSILON) @@ -407,10 +421,23 @@ if (request.errorContext != nil) { - if (request.errorContext.minimalExecuteTime > currentTime) + if (request.errorContext.minimalExecuteTime > currentTime) { continue; - if (request.errorContext.waitingForTokenExport) + } + if (request.errorContext.waitingForTokenExport) { continue; + } + + bool foundDependency = false; + for (MTRequest *anotherRequest in _requests) { + if (request.errorContext.waitingForRequestToComplete == anotherRequest.internalId) { + foundDependency = true; + break; + } + } + if (foundDependency) { + continue; + } } if (request.requestContext == nil || (!request.requestContext.waitingForMessageId && !request.requestContext.delivered && request.requestContext.transactionId == nil)) @@ -690,6 +717,28 @@ request.errorContext.minimalExecuteTime = MAX(request.errorContext.minimalExecuteTime, MTAbsoluteSystemTime() + 2.0); } } + else if ( + ( + rpcError.errorCode == 400 && + [rpcError.errorDescription isEqualToString:@"MSG_WAIT_TIMEOUT"] + ) || + ( + rpcError.errorCode == 500 && + [rpcError.errorDescription isEqualToString:@"MSG_WAIT_FAILED"] + ) + ) { + if (request.errorContext == nil) { + request.errorContext = [[MTRequestErrorContext alloc] init]; + } + + for (MTRequest *anotherRequest in _requests) { + if (request.shouldDependOnRequest != nil && request.shouldDependOnRequest(anotherRequest)) { + request.errorContext.waitingForRequestToComplete = anotherRequest.internalId; + break; + } + } + restartRequest = true; + } else if (rpcError.errorCode == 420 || [rpcError.errorDescription rangeOfString:@"FLOOD_WAIT_"].location != NSNotFound) { if (request.errorContext == nil) diff --git a/submodules/MtProtoKit/Sources/MTResPqMessage.m b/submodules/MtProtoKit/Sources/MTResPqMessage.m index 6156721256..2aa73435cd 100644 --- a/submodules/MtProtoKit/Sources/MTResPqMessage.m +++ b/submodules/MtProtoKit/Sources/MTResPqMessage.m @@ -16,7 +16,14 @@ } - (NSString *)description { - return [NSString stringWithFormat:@"res_pq nonce:%@ serverNonce:%@ pq:%@ fingerprints:%@", _nonce, _serverNonce, _pq, _serverPublicKeyFingerprints]; + NSMutableString *fingerprintsString = [[NSMutableString alloc] init]; + for (NSNumber *value in _serverPublicKeyFingerprints) { + if (fingerprintsString.length != 0) { + [fingerprintsString appendString:@"\n"]; + } + [fingerprintsString appendFormat:@"%llx", [value longLongValue]]; + } + return [NSString stringWithFormat:@"res_pq nonce:%@ serverNonce:%@ pq:%@ fingerprints:%@", _nonce, _serverNonce, _pq, fingerprintsString]; } @end diff --git a/submodules/MtProtoKit/Sources/MTSessionInfo.m b/submodules/MtProtoKit/Sources/MTSessionInfo.m index 1691592fa0..25d8e0dd07 100644 --- a/submodules/MtProtoKit/Sources/MTSessionInfo.m +++ b/submodules/MtProtoKit/Sources/MTSessionInfo.m @@ -52,6 +52,7 @@ NSMutableSet *_processedMessageIdsSet; NSMutableArray *_scheduledMessageConfirmations; NSMutableDictionary *_containerMessagesMappingDict; + NSMutableSet *_sentMessageIdsSet; } @end @@ -76,6 +77,7 @@ _scheduledMessageConfirmations = [[NSMutableArray alloc] init]; _processedMessageIdsSet = [[NSMutableSet alloc] init]; + _sentMessageIdsSet = [[NSMutableSet alloc] init]; _containerMessagesMappingDict = [[NSMutableDictionary alloc] init]; } return self; @@ -138,6 +140,14 @@ [_processedMessageIdsSet addObject:@(messageId)]; } +- (bool)wasMessageSentOnce:(int64_t)messageId { + return [_sentMessageIdsSet containsObject:@(messageId)]; +} + +- (void)setMessageWasSentOnce:(int64_t)messageId { + [_sentMessageIdsSet addObject:@(messageId)]; +} + - (void)scheduleMessageConfirmation:(int64_t)messageId size:(NSInteger)size { bool found = false; diff --git a/submodules/MusicAlbumArtResources/Sources/ExternalMusicAlbumArtResources.swift b/submodules/MusicAlbumArtResources/Sources/ExternalMusicAlbumArtResources.swift index 81e63879cc..3f45e62f37 100644 --- a/submodules/MusicAlbumArtResources/Sources/ExternalMusicAlbumArtResources.swift +++ b/submodules/MusicAlbumArtResources/Sources/ExternalMusicAlbumArtResources.swift @@ -104,6 +104,9 @@ public func fetchExternalMusicAlbumArtResource(account: Account, resource: Exter let metaUrl = "https://itunes.apple.com/search?term=\(urlEncodedStringFromString("\(performer) \(resource.title)"))&entity=song&limit=4" + let title = resource.title.lowercased() + let isMix = title.contains("remix") || title.contains("mixed") + let fetchDisposable = MetaDisposable() let disposable = fetchHttpResource(url: metaUrl).start(next: { result in @@ -120,7 +123,23 @@ public func fetchExternalMusicAlbumArtResource(account: Account, resource: Exter return } - guard let result = results.first as? [String: Any] else { + var matchingResult: Any? + for result in results { + if let result = result as? [String: Any] { + let title = ((result["trackCensoredName"] as? String) ?? (result["trackName"] as? String))?.lowercased() ?? "" + let resultIsMix = title.contains("remix") || title.contains("mixed") + if isMix == resultIsMix { + matchingResult = result + break + } + } + } + + if matchingResult == nil { + matchingResult = results.first + } + + guard let result = matchingResult as? [String: Any] else { subscriber.putNext(.dataPart(resourceOffset: 0, data: Data(), range: 0 ..< 0, complete: true)) subscriber.putCompletion() return diff --git a/submodules/OpenInExternalAppUI/Sources/OpenInOptions.swift b/submodules/OpenInExternalAppUI/Sources/OpenInOptions.swift index e855eae738..21117128d1 100644 --- a/submodules/OpenInExternalAppUI/Sources/OpenInOptions.swift +++ b/submodules/OpenInExternalAppUI/Sources/OpenInOptions.swift @@ -239,6 +239,15 @@ private func allOpenInOptions(context: AccountContext, item: OpenInItem) -> [Ope })) } + options.append(OpenInOption(identifier: "2gis", application: .other(title: "2GIS", identifier: 481627348, scheme: "dgis", store: nil), action: { + let coordinates = "\(lon),\(lat)" + if withDirections { + return .openUrl(url: "dgis://2gis.ru/routeSearch/to/\(coordinates)/go") + } else { + return .openUrl(url: "dgis://2gis.ru/geo/\(coordinates)") + } + })) + options.append(OpenInOption(identifier: "moovit", application: .other(title: "Moovit", identifier: 498477945, scheme: "moovit", store: nil), action: { if withDirections { let destName: String diff --git a/submodules/PasscodeUI/BUILD b/submodules/PasscodeUI/BUILD index 60825d4832..cadf3b48ea 100644 --- a/submodules/PasscodeUI/BUILD +++ b/submodules/PasscodeUI/BUILD @@ -23,6 +23,7 @@ swift_library( "//submodules/AppBundle:AppBundle", "//submodules/PasscodeInputFieldNode:PasscodeInputFieldNode", "//submodules/MonotonicTime:MonotonicTime", + "//submodules/GradientBackground:GradientBackground", ], visibility = [ "//visibility:public", diff --git a/submodules/PasscodeUI/Sources/PasscodeBackground.swift b/submodules/PasscodeUI/Sources/PasscodeBackground.swift index 41bed65bbe..aff1c5812a 100644 --- a/submodules/PasscodeUI/Sources/PasscodeBackground.swift +++ b/submodules/PasscodeUI/Sources/PasscodeBackground.swift @@ -1,19 +1,53 @@ import Foundation import UIKit +import AsyncDisplayKit import Display import ImageBlur import FastBlur +import GradientBackground protocol PasscodeBackground { var size: CGSize { get } - var backgroundImage: UIImage { get } - var foregroundImage: UIImage { get } + var backgroundImage: UIImage? { get } + var foregroundImage: UIImage? { get } + + func makeBackgroundNode() -> ASDisplayNode? + func makeForegroundNode(backgroundNode: ASDisplayNode?) -> ASDisplayNode? +} + +final class CustomPasscodeBackground: PasscodeBackground { + private let colors: [UIColor] + private let backgroundNode: GradientBackgroundNode + let inverted: Bool + + public private(set) var size: CGSize + public private(set) var backgroundImage: UIImage? = nil + public private(set) var foregroundImage: UIImage? = nil + + init(size: CGSize, colors: [UIColor], inverted: Bool) { + self.size = size + self.colors = colors + self.inverted = inverted + self.backgroundNode = createGradientBackgroundNode(colors: self.colors) + } + + func makeBackgroundNode() -> ASDisplayNode? { + return self.backgroundNode + } + + func makeForegroundNode(backgroundNode: ASDisplayNode?) -> ASDisplayNode? { + if self.inverted, let backgroundNode = backgroundNode as? GradientBackgroundNode { + return GradientBackgroundNode.CloneNode(parentNode: backgroundNode) + } else { + return nil + } + } } final class GradientPasscodeBackground: PasscodeBackground { public private(set) var size: CGSize - public private(set) var backgroundImage: UIImage - public private(set) var foregroundImage: UIImage + public private(set) var backgroundImage: UIImage? + public private(set) var foregroundImage: UIImage? init(size: CGSize, backgroundColors: (UIColor, UIColor), buttonColor: UIColor) { self.size = size @@ -35,12 +69,20 @@ final class GradientPasscodeBackground: PasscodeBackground { context.fill(bounds) })! } + + func makeBackgroundNode() -> ASDisplayNode? { + return nil + } + + func makeForegroundNode(backgroundNode: ASDisplayNode?) -> ASDisplayNode? { + return nil + } } final class ImageBasedPasscodeBackground: PasscodeBackground { public private(set) var size: CGSize - public private(set) var backgroundImage: UIImage - public private(set) var foregroundImage: UIImage + public private(set) var backgroundImage: UIImage? + public private(set) var foregroundImage: UIImage? init(image: UIImage, size: CGSize) { self.size = size @@ -74,7 +116,7 @@ final class ImageBasedPasscodeBackground: PasscodeBackground { } telegramFastBlurMore(Int32(contextSize.width), Int32(contextSize.height), Int32(backgroundContext.bytesPerRow), backgroundContext.bytes) telegramFastBlurMore(Int32(contextSize.width), Int32(contextSize.height), Int32(backgroundContext.bytesPerRow), backgroundContext.bytes) - telegramFastBlurMore(Int32(contextSize.width), Int32(contextSize.height), Int32(foregroundContext.bytesPerRow), foregroundContext.bytes) + telegramFastBlurMore(Int32(contextSize.width), Int32(contextSize.height), Int32(foregroundContext.bytesPerRow), backgroundContext.bytes) backgroundContext.withFlippedContext { context in context.setFillColor(UIColor(white: 0.0, alpha: 0.35).cgColor) @@ -82,4 +124,12 @@ final class ImageBasedPasscodeBackground: PasscodeBackground { } self.backgroundImage = backgroundContext.generateImage()! } + + func makeBackgroundNode() -> ASDisplayNode? { + return nil + } + + func makeForegroundNode(backgroundNode: ASDisplayNode?) -> ASDisplayNode? { + return nil + } } diff --git a/submodules/PasscodeUI/Sources/PasscodeEntryController.swift b/submodules/PasscodeUI/Sources/PasscodeEntryController.swift index ad96c44ada..54ceb4e794 100644 --- a/submodules/PasscodeUI/Sources/PasscodeEntryController.swift +++ b/submodules/PasscodeUI/Sources/PasscodeEntryController.swift @@ -58,7 +58,10 @@ public final class PasscodeEntryController: ViewController { private var inBackground: Bool = false private var inBackgroundDisposable: Disposable? - public init(applicationBindings: TelegramApplicationBindings, accountManager: AccountManager, appLockContext: AppLockContext, presentationData: PresentationData, presentationDataSignal: Signal, challengeData: PostboxAccessChallengeData, biometrics: PasscodeEntryControllerBiometricsMode, arguments: PasscodeEntryControllerPresentationArguments) { + private var statusBarHost: StatusBarHost? + private var previousStatusBarStyle: UIStatusBarStyle? + + public init(applicationBindings: TelegramApplicationBindings, accountManager: AccountManager, appLockContext: AppLockContext, presentationData: PresentationData, presentationDataSignal: Signal, statusBarHost: StatusBarHost?, challengeData: PostboxAccessChallengeData, biometrics: PasscodeEntryControllerBiometricsMode, arguments: PasscodeEntryControllerPresentationArguments) { self.applicationBindings = applicationBindings self.accountManager = accountManager self.appLockContext = appLockContext @@ -68,10 +71,19 @@ public final class PasscodeEntryController: ViewController { self.biometrics = biometrics self.arguments = arguments + self.statusBarHost = statusBarHost + self.previousStatusBarStyle = statusBarHost?.statusBarStyle super.init(navigationBarPresentationData: nil) self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait) - self.statusBar.statusBarStyle = .White + self.statusBarHost?.setStatusBarStyle(.lightContent, animated: true) + self.statusBarHost?.shouldChangeStatusBarStyle = { [weak self] style in + if let strongSelf = self { + strongSelf.previousStatusBarStyle = style + return false + } + return true + } self.presentationDataDisposable = (presentationDataSignal |> deliverOnMainQueue).start(next: { [weak self] presentationData in @@ -128,7 +140,7 @@ public final class PasscodeEntryController: ViewController { } else { biometricsType = nil } - self.displayNode = PasscodeEntryControllerNode(accountManager: self.accountManager, theme: self.presentationData.theme, strings: self.presentationData.strings, wallpaper: self.presentationData.chatWallpaper, passcodeType: passcodeType, biometricsType: biometricsType, arguments: self.arguments, statusBar: self.statusBar, modalPresentation: self.arguments.modalPresentation) + self.displayNode = PasscodeEntryControllerNode(accountManager: self.accountManager, presentationData: self.presentationData, theme: self.presentationData.theme, strings: self.presentationData.strings, wallpaper: self.presentationData.chatWallpaper, passcodeType: passcodeType, biometricsType: biometricsType, arguments: self.arguments, modalPresentation: self.arguments.modalPresentation) self.displayNodeDidLoad() let _ = (self.appLockContext.invalidAttempts @@ -267,10 +279,14 @@ public final class PasscodeEntryController: 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.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition) } public override func dismiss(completion: (() -> Void)? = nil) { + self.statusBarHost?.shouldChangeStatusBarStyle = nil + if let statusBarHost = self.statusBarHost, let previousStatusBarStyle = self.previousStatusBarStyle { + statusBarHost.setStatusBarStyle(previousStatusBarStyle, animated: true) + } self.view.endEditing(true) self.controllerNode.animateOut { [weak self] in guard let strongSelf = self else { diff --git a/submodules/PasscodeUI/Sources/PasscodeEntryControllerNode.swift b/submodules/PasscodeUI/Sources/PasscodeEntryControllerNode.swift index 6272a4c17b..0050da048a 100644 --- a/submodules/PasscodeUI/Sources/PasscodeEntryControllerNode.swift +++ b/submodules/PasscodeUI/Sources/PasscodeEntryControllerNode.swift @@ -12,6 +12,7 @@ import LocalAuth import AppBundle import PasscodeInputFieldNode import MonotonicTime +import GradientBackground private let titleFont = Font.regular(20.0) private let subtitleFont = Font.regular(15.0) @@ -19,6 +20,7 @@ private let buttonFont = Font.regular(17.0) final class PasscodeEntryControllerNode: ASDisplayNode { private let accountManager: AccountManager + private var presentationData: PresentationData private var theme: PresentationTheme private var strings: PresentationStrings private var wallpaper: TelegramWallpaper @@ -27,11 +29,11 @@ final class PasscodeEntryControllerNode: ASDisplayNode { private let arguments: PasscodeEntryControllerPresentationArguments private var background: PasscodeBackground? - private let statusBar: StatusBar - private let modalPresentation: Bool - private let backgroundNode: ASImageNode + private var backgroundCustomNode: ASDisplayNode? + private let backgroundDimNode: ASDisplayNode + private let backgroundImageNode: ASImageNode private let iconNode: PasscodeLockIconNode private let titleNode: PasscodeEntryLabelNode private let inputFieldNode: PasscodeInputFieldNode @@ -52,20 +54,24 @@ final class PasscodeEntryControllerNode: ASDisplayNode { var checkPasscode: ((String) -> Void)? var requestBiometrics: (() -> Void)? - init(accountManager: AccountManager, theme: PresentationTheme, strings: PresentationStrings, wallpaper: TelegramWallpaper, passcodeType: PasscodeEntryFieldType, biometricsType: LocalAuthBiometricAuthentication?, arguments: PasscodeEntryControllerPresentationArguments, statusBar: StatusBar, modalPresentation: Bool) { + init(accountManager: AccountManager, presentationData: PresentationData, theme: PresentationTheme, strings: PresentationStrings, wallpaper: TelegramWallpaper, passcodeType: PasscodeEntryFieldType, biometricsType: LocalAuthBiometricAuthentication?, arguments: PasscodeEntryControllerPresentationArguments, modalPresentation: Bool) { self.accountManager = accountManager + self.presentationData = presentationData self.theme = theme self.strings = strings self.wallpaper = wallpaper self.passcodeType = passcodeType self.biometricsType = biometricsType self.arguments = arguments - self.statusBar = statusBar self.modalPresentation = modalPresentation - self.backgroundNode = ASImageNode() - self.backgroundNode.contentMode = .scaleToFill + self.backgroundImageNode = ASImageNode() + self.backgroundImageNode.contentMode = .scaleToFill + self.backgroundDimNode = ASDisplayNode() + self.backgroundDimNode.backgroundColor = UIColor(rgb: 0x000000, alpha: 0.15) + self.backgroundDimNode.isHidden = true + self.iconNode = PasscodeLockIconNode() self.titleNode = PasscodeEntryLabelNode() self.inputFieldNode = PasscodeInputFieldNode(color: .white, accentColor: .white, fieldType: passcodeType, keyboardAppearance: .dark, useCustomNumpad: true) @@ -86,7 +92,20 @@ final class PasscodeEntryControllerNode: ASDisplayNode { self.iconNode.unlockedColor = theme.rootController.navigationBar.primaryTextColor self.keyboardNode.charactedEntered = { [weak self] character in - self?.inputFieldNode.append(character) + if let strongSelf = self { + strongSelf.inputFieldNode.append(character) + if let gradientNode = strongSelf.backgroundCustomNode as? GradientBackgroundNode { + gradientNode.animateEvent(transition: .animated(duration: 0.55, curve: .spring)) + } + } + } + self.keyboardNode.backspace = { [weak self] in + if let strongSelf = self { + let _ = strongSelf.inputFieldNode.delete() + if let gradientNode = strongSelf.backgroundCustomNode as? GradientBackgroundNode { + gradientNode.animateEvent(transition: .animated(duration: 0.55, curve: .spring), backwards: true) + } + } } self.inputFieldNode.complete = { [weak self] passcode in guard let strongSelf = self else { @@ -111,7 +130,8 @@ final class PasscodeEntryControllerNode: ASDisplayNode { } } - self.addSubnode(self.backgroundNode) + self.addSubnode(self.backgroundImageNode) + self.addSubnode(self.backgroundDimNode) self.addSubnode(self.iconNode) self.addSubnode(self.titleNode) self.addSubnode(self.inputFieldNode) @@ -146,7 +166,10 @@ final class PasscodeEntryControllerNode: ASDisplayNode { @objc private func deletePressed() { self.hapticFeedback.tap() - self.inputFieldNode.delete() + let result = self.inputFieldNode.delete() + if result, let gradientNode = self.backgroundCustomNode as? GradientBackgroundNode { + gradientNode.animateEvent(transition: .animated(duration: 0.55, curve: .spring), backwards: true) + } } @objc private func biometricsPressed() { @@ -158,6 +181,7 @@ final class PasscodeEntryControllerNode: ASDisplayNode { } func updatePresentationData(_ presentationData: PresentationData) { + self.presentationData = presentationData self.theme = presentationData.theme self.strings = presentationData.strings self.wallpaper = presentationData.chatWallpaper @@ -173,26 +197,73 @@ final class PasscodeEntryControllerNode: ASDisplayNode { return } - var size = validLayout.size + let size = validLayout.size if let background = self.background, background.size == size { return } switch self.wallpaper { + case let .color(colorValue): + let color = UIColor(argb: colorValue) + let baseColor: UIColor + let lightness = color.lightness + if lightness < 0.1 || lightness > 0.9 { + baseColor = self.theme.chat.message.outgoing.bubble.withoutWallpaper.fill + } else{ + baseColor = color + } + + let color1: UIColor + let color2: UIColor + let color3: UIColor + let color4: UIColor + if self.theme.overallDarkAppearance { + color1 = baseColor.withMultiplied(hue: 1.034, saturation: 0.819, brightness: 0.214) + color2 = baseColor.withMultiplied(hue: 1.029, saturation: 0.77, brightness: 0.132) + color3 = color1 + color4 = color2 + } else { + color1 = baseColor.withMultiplied(hue: 1.029, saturation: 0.312, brightness: 1.26) + color2 = baseColor.withMultiplied(hue: 1.034, saturation: 0.729, brightness: 0.942) + color3 = baseColor.withMultiplied(hue: 1.029, saturation: 0.729, brightness: 1.231) + color4 = baseColor.withMultiplied(hue: 1.034, saturation: 0.583, brightness: 1.043) + } + self.background = CustomPasscodeBackground(size: size, colors: [color1, color2, color3, color4], inverted: false) + case let .gradient(_, colors, settings): + self.background = CustomPasscodeBackground(size: size, colors: colors.compactMap { UIColor(rgb: $0) }, inverted: (settings.intensity ?? 0) < 0) case .image, .file: if let image = chatControllerBackgroundImage(theme: self.theme, wallpaper: self.wallpaper, mediaBox: self.accountManager.mediaBox, composed: false, knockoutMode: false) { self.background = ImageBasedPasscodeBackground(image: image, size: size) } else { - self.background = GradientPasscodeBackground(size: size, backgroundColors: self.theme.passcode.backgroundColors.colors, buttonColor: self.theme.passcode.buttonColor) + if case let .file(file) = self.wallpaper, !file.settings.colors.isEmpty { + self.background = CustomPasscodeBackground(size: size, colors: file.settings.colors.compactMap { UIColor(rgb: $0) }, inverted: (file.settings.intensity ?? 0) < 0) + } else { + self.background = GradientPasscodeBackground(size: size, backgroundColors: self.theme.passcode.backgroundColors.colors, buttonColor: self.theme.passcode.buttonColor) + } } default: self.background = GradientPasscodeBackground(size: size, backgroundColors: self.theme.passcode.backgroundColors.colors, buttonColor: self.theme.passcode.buttonColor) } if let background = self.background { - self.backgroundNode.image = background.backgroundImage - self.keyboardNode.updateBackground(background) - self.inputFieldNode.updateBackground(background.foregroundImage, size: background.size) + self.backgroundCustomNode?.removeFromSupernode() + self.backgroundCustomNode = nil + + if let backgroundImage = background.backgroundImage { + self.backgroundImageNode.image = backgroundImage + self.backgroundDimNode.isHidden = true + } else if let customBackgroundNode = background.makeBackgroundNode() { + self.backgroundCustomNode = customBackgroundNode + self.insertSubnode(customBackgroundNode, aboveSubnode: self.backgroundImageNode) + if let background = background as? CustomPasscodeBackground, background.inverted { + self.backgroundDimNode.backgroundColor = UIColor(rgb: 0x000000, alpha: 0.75) + } else { + self.backgroundDimNode.backgroundColor = UIColor(rgb: 0x000000, alpha: 0.15) + } + self.backgroundDimNode.isHidden = false + } + self.keyboardNode.updateBackground(self.presentationData, background) + self.inputFieldNode.updateBackground(background) } } @@ -263,7 +334,12 @@ final class PasscodeEntryControllerNode: ASDisplayNode { self.effectView.alpha = 1.0 } }) - self.backgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + self.backgroundImageNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + if let gradientNode = self.backgroundCustomNode as? GradientBackgroundNode { + gradientNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + self.backgroundDimNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + gradientNode.animateEvent(transition: .animated(duration: 1.0, curve: .spring), extendAnimation: true) + } } self.titleNode.setAttributedText(NSAttributedString(string: self.strings.EnterPasscode_EnterPasscode, font: titleFont, textColor: .white), animation: .none) } @@ -277,15 +353,21 @@ final class PasscodeEntryControllerNode: ASDisplayNode { self.effectView.alpha = 1.0 } }) - self.backgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + self.backgroundImageNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + if let gradientNode = self.backgroundCustomNode as? GradientBackgroundNode { + gradientNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + gradientNode.animateEvent(transition: .animated(duration: 0.35, curve: .spring)) + self.backgroundDimNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } if !iconFrame.isEmpty { self.iconNode.animateIn(fromScale: 0.416) self.iconNode.layer.animatePosition(from: iconFrame.center.offsetBy(dx: 6.0, dy: 6.0), to: self.iconNode.layer.position, duration: 0.45) + + Queue.mainQueue().after(0.45) { + self.hapticFeedback.impact(.medium) + } } - self.statusBar.layer.removeAnimation(forKey: "opacity") - self.statusBar.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) - self.subtitleNode.isHidden = true self.inputFieldNode.isHidden = true self.keyboardNode.isHidden = true @@ -303,6 +385,9 @@ final class PasscodeEntryControllerNode: ASDisplayNode { self.subtitleNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) + if let gradientNode = self.backgroundCustomNode as? GradientBackgroundNode { + gradientNode.animateEvent(transition: .animated(duration: 1.0, curve: .spring)) + } self.inputFieldNode.animateIn() self.keyboardNode.animateIn() var biometricDelay = 0.3 @@ -323,7 +408,6 @@ final class PasscodeEntryControllerNode: ASDisplayNode { } func animateOut(down: Bool = false, completion: @escaping () -> Void = {}) { - self.statusBar.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false) self.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: down ? self.bounds.size.height : -self.bounds.size.height), duration: 0.2, removeOnCompletion: false, additive: true, completion: { _ in completion() }) @@ -340,6 +424,10 @@ final class PasscodeEntryControllerNode: ASDisplayNode { self.iconNode.layer.addShakeAnimation(amplitude: -8.0, duration: 0.5, count: 6, decay: true) self.hapticFeedback.error() + + if let gradientNode = self.backgroundCustomNode as? GradientBackgroundNode { + gradientNode.animateEvent(transition: .animated(duration: 1.5, curve: .spring), extendAnimation: true, backwards: true) + } } func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { @@ -348,7 +436,14 @@ final class PasscodeEntryControllerNode: ASDisplayNode { self.updateBackground() let bounds = CGRect(origin: CGPoint(), size: layout.size) - transition.updateFrame(node: self.backgroundNode, frame: bounds) + transition.updateFrame(node: self.backgroundImageNode, frame: bounds) + transition.updateFrame(node: self.backgroundDimNode, frame: bounds) + if let backgroundCustomNode = self.backgroundCustomNode { + transition.updateFrame(node: backgroundCustomNode, frame: bounds) + if let gradientBackgroundNode = backgroundCustomNode as? GradientBackgroundNode { + gradientBackgroundNode.updateLayout(size: bounds.size, transition: transition) + } + } transition.updateFrame(view: self.effectView, frame: bounds) switch self.passcodeType { diff --git a/submodules/PasscodeUI/Sources/PasscodeEntryKeyboardNode.swift b/submodules/PasscodeUI/Sources/PasscodeEntryKeyboardNode.swift index 0154e44451..83b322eb0e 100644 --- a/submodules/PasscodeUI/Sources/PasscodeEntryKeyboardNode.swift +++ b/submodules/PasscodeUI/Sources/PasscodeEntryKeyboardNode.swift @@ -3,6 +3,8 @@ import UIKit import Display import AsyncDisplayKit import SwiftSignalKit +import TelegramPresentationData +import GradientBackground private let regularTitleFont = Font.regular(36.0) private let regularSubtitleFont: UIFont = { @@ -35,8 +37,9 @@ private func generateButtonImage(background: PasscodeBackground, frame: CGRect, context.clip() context.setAlpha(0.8) - context.draw(background.foregroundImage.cgImage!, in: relativeFrame) - + if let foregroundImage = background.foregroundImage { + context.draw(foregroundImage.cgImage!, in: relativeFrame) + } if highlighted { context.setFillColor(UIColor(white: 1.0, alpha: 0.65).cgColor) context.fillEllipse(in: bounds) @@ -98,6 +101,7 @@ private func generateButtonImage(background: PasscodeBackground, frame: CGRect, } final class PasscodeEntryButtonNode: HighlightTrackingButtonNode { + private var presentationData: PresentationData private var background: PasscodeBackground let title: String private let subtitle: String @@ -106,15 +110,30 @@ final class PasscodeEntryButtonNode: HighlightTrackingButtonNode { private var regularImage: UIImage? private var highlightedImage: UIImage? + private var blurredBackgroundNode: NavigationBackgroundNode? + private var gradientBackgroundNode: GradientBackgroundNode.CloneNode? private let backgroundNode: ASImageNode var action: (() -> Void)? + var cancelAction: (() -> Void)? - init(background: PasscodeBackground, title: String, subtitle: String) { + init(presentationData: PresentationData, background: PasscodeBackground, title: String, subtitle: String) { + self.presentationData = presentationData self.background = background self.title = title self.subtitle = subtitle + if let background = background as? CustomPasscodeBackground { + if false, background.inverted { + let gradientBackgroundNode = background.makeForegroundNode(backgroundNode: background.makeBackgroundNode()) + self.gradientBackgroundNode = gradientBackgroundNode as? GradientBackgroundNode.CloneNode + } else { + let blurredBackgroundColor = (background.inverted ? UIColor(rgb: 0xffffff, alpha: 0.1) : UIColor(rgb: 0x000000, alpha: 0.2), dateFillNeedsBlur(theme: presentationData.theme, wallpaper: presentationData.chatWallpaper)) + let blurredBackgroundNode = NavigationBackgroundNode(color: blurredBackgroundColor.0, enableBlur: blurredBackgroundColor.1) + self.blurredBackgroundNode = blurredBackgroundNode + } + } + self.backgroundNode = ASImageNode() self.backgroundNode.displaysAsynchronously = false self.backgroundNode.displayWithoutProcessing = true @@ -122,6 +141,12 @@ final class PasscodeEntryButtonNode: HighlightTrackingButtonNode { super.init() + if let gradientBackgroundNode = self.gradientBackgroundNode { + self.addSubnode(gradientBackgroundNode) + } + if let blurredBackgroundNode = self.blurredBackgroundNode { + self.addSubnode(blurredBackgroundNode) + } self.addSubnode(self.backgroundNode) self.highligthedChanged = { [weak self] highlighted in @@ -146,7 +171,8 @@ final class PasscodeEntryButtonNode: HighlightTrackingButtonNode { } } - func updateBackground(_ background: PasscodeBackground) { + func updateBackground(_ presentationData: PresentationData, _ background: PasscodeBackground) { + self.presentationData = presentationData self.background = background self.updateGraphics() } @@ -155,6 +181,12 @@ final class PasscodeEntryButtonNode: HighlightTrackingButtonNode { self.regularImage = generateButtonImage(background: self.background, frame: self.frame, title: self.title, subtitle: self.subtitle, highlighted: false) self.highlightedImage = generateButtonImage(background: self.background, frame: self.frame, title: self.title, subtitle: self.subtitle, highlighted: true) self.updateState(highlighted: self.isHighlighted) + + if let gradientBackgroundNode = self.gradientBackgroundNode { + let containerSize = self.background.size + let shiftedContentsRect = CGRect(origin: CGPoint(x: self.frame.minX / containerSize.width, y: self.frame.minY / containerSize.height), size: CGSize(width: self.frame.width / containerSize.width, height: self.frame.height / containerSize.height)) + gradientBackgroundNode.layer.contentsRect = shiftedContentsRect + } } private func updateState(highlighted: Bool) { @@ -175,6 +207,13 @@ final class PasscodeEntryButtonNode: HighlightTrackingButtonNode { override func layout() { super.layout() + if let gradientBackgroundNode = self.gradientBackgroundNode { + gradientBackgroundNode.frame = self.bounds + } + if let blurredBackgroundNode = self.blurredBackgroundNode { + blurredBackgroundNode.frame = self.bounds + blurredBackgroundNode.update(size: blurredBackgroundNode.bounds.size, cornerRadius: blurredBackgroundNode.bounds.height / 2.0, transition: .immediate) + } self.backgroundNode.frame = self.bounds } @@ -183,6 +222,20 @@ final class PasscodeEntryButtonNode: HighlightTrackingButtonNode { self.action?() } + + override func touchesEnded(_ touches: Set, with event: UIEvent?) { + super.touchesEnded(touches, with: event) + + if let touchPosition = touches.first?.location(in: self.view), !self.view.bounds.contains(touchPosition) { + self.cancelAction?() + } + } + + override func touchesCancelled(_ touches: Set?, with event: UIEvent?) { + super.touchesCancelled(touches, with: event) + + self.cancelAction?() + } } private let buttonsData = [ @@ -199,31 +252,37 @@ private let buttonsData = [ ] final class PasscodeEntryKeyboardNode: ASDisplayNode { + private var presentationData: PresentationData? private var background: PasscodeBackground? var charactedEntered: ((String) -> Void)? + var backspace: (() -> Void)? private func updateButtons() { - guard let background = self.background else { + guard let presentationData = self.presentationData, let background = self.background else { return } if let subnodes = self.subnodes, !subnodes.isEmpty { for case let button as PasscodeEntryButtonNode in subnodes { - button.updateBackground(background) + button.updateBackground(presentationData, background) } } else { for (title, subtitle) in buttonsData { - let buttonNode = PasscodeEntryButtonNode(background: background, title: title, subtitle: subtitle) + let buttonNode = PasscodeEntryButtonNode(presentationData: presentationData, background: background, title: title, subtitle: subtitle) buttonNode.action = { [weak self] in self?.charactedEntered?(title) } + buttonNode.cancelAction = { [weak self] in + self?.backspace?() + } self.addSubnode(buttonNode) } } } - func updateBackground(_ background: PasscodeBackground) { + func updateBackground(_ presentationData: PresentationData, _ background: PasscodeBackground) { + self.presentationData = presentationData self.background = background self.updateButtons() } diff --git a/submodules/PasscodeUI/Sources/PasscodeEntryLabelNode.swift b/submodules/PasscodeUI/Sources/PasscodeEntryLabelNode.swift index 51db01bf6e..d5b1783263 100644 --- a/submodules/PasscodeUI/Sources/PasscodeEntryLabelNode.swift +++ b/submodules/PasscodeUI/Sources/PasscodeEntryLabelNode.swift @@ -22,6 +22,7 @@ final class PasscodeEntryLabelNode: ASDisplayNode { self.textNode = ASTextNode() self.textNode.isLayerBacked = false self.textNode.textAlignment = .center + self.textNode.displaysAsynchronously = false super.init() diff --git a/submodules/PasscodeUI/Sources/PasscodeInputFieldNode.swift b/submodules/PasscodeUI/Sources/PasscodeInputFieldNode.swift index dd2ce400ac..f6ea4d47e5 100644 --- a/submodules/PasscodeUI/Sources/PasscodeInputFieldNode.swift +++ b/submodules/PasscodeUI/Sources/PasscodeInputFieldNode.swift @@ -24,20 +24,23 @@ private func generateDotImage(color: UIColor, filled: Bool) -> UIImage? { }) } -private func generateFieldBackgroundImage(backgroundImage: UIImage, backgroundSize: CGSize, frame: CGRect) -> UIImage? { +private func generateFieldBackgroundImage(backgroundImage: UIImage?, backgroundSize: CGSize?, frame: CGRect) -> UIImage? { return generateImage(frame.size, contextGenerator: { size, context in let bounds = CGRect(origin: CGPoint(), size: size) context.clear(bounds) - let relativeFrame = CGRect(x: -frame.minX, y: frame.minY - backgroundSize.height + frame.size.height - , width: backgroundSize.width, height: backgroundSize.height) - let path = UIBezierPath(roundedRect: CGRect(x: 0.0, y: 0.0, width: size.width, height: size.height), cornerRadius: 6.0) context.addPath(path.cgPath) context.clip() - context.draw(backgroundImage.cgImage!, in: relativeFrame) - + if let backgroundImage = backgroundImage, let backgroundSize = backgroundSize { + let relativeFrame = CGRect(x: -frame.minX, y: frame.minY - backgroundSize.height + frame.size.height + , width: backgroundSize.width, height: backgroundSize.height) + context.draw(backgroundImage.cgImage!, in: relativeFrame) + } else { + context.setFillColor(UIColor(rgb: 0xffffff, alpha: 1.0).cgColor) + context.fill(bounds) + } context.setBlendMode(.clear) context.setFillColor(UIColor.clear.cgColor) @@ -129,7 +132,7 @@ private class PasscodeEntryDotNode: ASImageNode { } public final class PasscodeInputFieldNode: ASDisplayNode, UITextFieldDelegate { - private var background: (UIImage, CGSize)? + private var background: PasscodeBackground? private var color: UIColor private var accentColor: UIColor private var fieldType: PasscodeEntryFieldType @@ -207,8 +210,8 @@ public final class PasscodeInputFieldNode: ASDisplayNode, UITextFieldDelegate { } } - func updateBackground(_ image: UIImage, size: CGSize) { - self.background = (image, size) + func updateBackground(_ background: PasscodeBackground) { + self.background = background if let (size, topOffset) = self.validLayout { let _ = self.updateLayout(size: size, topOffset: topOffset, transition: .immediate) } @@ -276,14 +279,15 @@ public final class PasscodeInputFieldNode: ASDisplayNode, UITextFieldDelegate { } } - func delete() { + func delete() -> Bool { var text = self.textFieldNode.textField.text ?? "" guard !text.isEmpty else { - return + return false } text = String(text[text.startIndex ..< text.index(text.endIndex, offsetBy: -1)]) self.textFieldNode.textField.text = text self.updateDots(count: text.count, animated: true) + return true } func updateDots(count: Int, animated: Bool) { @@ -346,9 +350,8 @@ public final class PasscodeInputFieldNode: ASDisplayNode, UITextFieldDelegate { let fieldFrame = CGRect(x: inset, y: origin.y, width: size.width - inset * 2.0, height: fieldHeight) transition.updateFrame(node: self.borderNode, frame: fieldFrame) transition.updateFrame(node: self.textFieldNode, frame: fieldFrame.insetBy(dx: 13.0, dy: 0.0)) - if let (backgroundImage, backgroundSize) = self.background { - self.borderNode.image = generateFieldBackgroundImage(backgroundImage: backgroundImage, backgroundSize: backgroundSize, frame: fieldFrame) - } + + self.borderNode.image = generateFieldBackgroundImage(backgroundImage: self.background?.foregroundImage, backgroundSize: self.background?.size, frame: fieldFrame) return fieldFrame } diff --git a/submodules/PasscodeUI/Sources/PasscodeSetupController.swift b/submodules/PasscodeUI/Sources/PasscodeSetupController.swift index 33a9f3cb4a..47873beb62 100644 --- a/submodules/PasscodeUI/Sources/PasscodeSetupController.swift +++ b/submodules/PasscodeUI/Sources/PasscodeSetupController.swift @@ -143,7 +143,7 @@ public final class PasscodeSetupController: 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.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition) } @objc private func nextPressed() { diff --git a/submodules/PassportUI/Sources/Form/FormController.swift b/submodules/PassportUI/Sources/Form/FormController.swift index d0ce8c2905..aebd1cea99 100644 --- a/submodules/PassportUI/Sources/Form/FormController.swift +++ b/submodules/PassportUI/Sources/Form/FormController.swift @@ -45,6 +45,6 @@ public class FormController deliverOnMainQueue).start(next: { [weak self] data in if let strongSelf = self { let storedPassword = context.getStoredSecureIdPassword() if data.currentPasswordDerivation != nil, let storedPassword = storedPassword { - strongSelf.authenthicateDisposable.set((accessSecureId(network: strongSelf.context.account.network, password: storedPassword) + strongSelf.authenthicateDisposable.set((strongSelf.context.engine.secureId.accessSecureId(password: storedPassword) |> deliverOnMainQueue).start(next: { context in guard let strongSelf = self, strongSelf.state.verificationState == nil else { return @@ -366,7 +366,7 @@ public final class SecureIdAuthController: ViewController, StandalonePresentable 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.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition) } override public func dismiss(completion: (() -> Void)? = nil) { @@ -423,8 +423,8 @@ public final class SecureIdAuthController: ViewController, StandalonePresentable @objc private func cancelPressed() { self.dismiss() - if case let .form(reqForm) = self.mode { - self.openUrl(secureIdCallbackUrl(with: reqForm.callbackUrl, peerId: reqForm.peerId, result: .cancel, parameters: [:])) + if case let .form(peerId, _, _, maybeCallbackUrl, _, _) = self.mode, let callbackUrl = maybeCallbackUrl { + self.openUrl(secureIdCallbackUrl(with: callbackUrl, peerId: peerId, result: .cancel, parameters: [:])) } } @@ -441,7 +441,7 @@ public final class SecureIdAuthController: ViewController, StandalonePresentable state.verificationState = .passwordChallenge(hint: hint, state: .checking, hasRecoveryEmail: hasRecoveryEmail) return state }) - self.challengeDisposable.set((accessSecureId(network: self.context.account.network, password: password) + self.challengeDisposable.set((self.context.engine.secureId.accessSecureId(password: password) |> deliverOnMainQueue).start(next: { [weak self] context in guard let strongSelf = self, let verificationState = strongSelf.state.verificationState, case .passwordChallenge(_, .checking, _) = verificationState else { return @@ -512,13 +512,13 @@ public final class SecureIdAuthController: ViewController, StandalonePresentable guard let strongSelf = self else { return } - strongSelf.recoveryDisposable.set((requestTwoStepVerificationPasswordRecoveryCode(network: strongSelf.context.account.network) + strongSelf.recoveryDisposable.set((strongSelf.context.engine.auth.requestTwoStepVerificationPasswordRecoveryCode() |> deliverOnMainQueue).start(next: { emailPattern in guard let strongSelf = self else { return } var completionImpl: (() -> Void)? - let controller = resetPasswordController(context: strongSelf.context, emailPattern: emailPattern, completion: { + let controller = resetPasswordController(context: strongSelf.context, emailPattern: emailPattern, completion: { _ in completionImpl?() }) completionImpl = { [weak controller] in @@ -558,7 +558,7 @@ public final class SecureIdAuthController: ViewController, StandalonePresentable return } switch update { - case .noPassword: + case .noPassword, .pendingPasswordReset: strongSelf.updateState(animated: false, { state in var state = state if let verificationState = state.verificationState, case .noChallenge = verificationState { @@ -639,13 +639,15 @@ public final class SecureIdAuthController: ViewController, StandalonePresentable @objc private func grantAccess() { switch self.state { case let .form(form): - if case let .form(reqForm) = self.mode, let encryptedFormData = form.encryptedFormData, let formData = form.formData { + if case let .form(peerId, scope, publicKey, callbackUrl, opaquePayload, opaqueNonce) = self.mode, let encryptedFormData = form.encryptedFormData, let formData = form.formData { let values = parseRequestedFormFields(formData.requestedFields, values: formData.values, primaryLanguageByCountry: encryptedFormData.primaryLanguageByCountry).map({ $0.1 }).flatMap({ $0 }) - let _ = (grantSecureIdAccess(network: self.context.account.network, peerId: encryptedFormData.servicePeer.id, publicKey: reqForm.publicKey, scope: reqForm.scope, opaquePayload: reqForm.opaquePayload, opaqueNonce: reqForm.opaqueNonce, values: values, requestedFields: formData.requestedFields) + let _ = (grantSecureIdAccess(network: self.context.account.network, peerId: encryptedFormData.servicePeer.id, publicKey: publicKey, scope: scope, opaquePayload: opaquePayload, opaqueNonce: opaqueNonce, values: values, requestedFields: formData.requestedFields) |> deliverOnMainQueue).start(completed: { [weak self] in self?.dismiss() - self?.openUrl(secureIdCallbackUrl(with: reqForm.callbackUrl, peerId: reqForm.peerId, result: .success, parameters: [:])) + if let callbackUrl = callbackUrl { + self?.openUrl(secureIdCallbackUrl(with: callbackUrl, peerId: peerId, result: .success, parameters: [:])) + } }) } case .list: diff --git a/submodules/PassportUI/Sources/SecureIdDocumentFormControllerNode.swift b/submodules/PassportUI/Sources/SecureIdDocumentFormControllerNode.swift index e0f44699b3..5ec8b56f37 100644 --- a/submodules/PassportUI/Sources/SecureIdDocumentFormControllerNode.swift +++ b/submodules/PassportUI/Sources/SecureIdDocumentFormControllerNode.swift @@ -2601,7 +2601,7 @@ final class SecureIdDocumentFormControllerNode: FormControllerNode Void) -> ViewController { +public func resetPasswordController(context: AccountContext, emailPattern: String, completion: @escaping (Bool) -> Void) -> ViewController { let statePromise = ValuePromise(ResetPasswordControllerState(), ignoreRepeated: true) let stateValue = Atomic(value: ResetPasswordControllerState()) let updateState: ((ResetPasswordControllerState) -> ResetPasswordControllerState) -> Void = { f in @@ -138,7 +138,20 @@ public func resetPasswordController(context: AccountContext, emailPattern: Strin } }, openHelp: { let presentationData = context.sharedContext.currentPresentationData.with { $0 } - presentControllerImpl?(textAlertController(context: context, title: nil, text: presentationData.strings.TwoStepAuth_RecoveryFailed, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil) + + presentControllerImpl?(textAlertController(context: context, title: presentationData.strings.TwoStepAuth_RecoveryUnavailableResetTitle, text: presentationData.strings.TwoStepAuth_RecoveryEmailResetText, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: presentationData.strings.TwoStepAuth_RecoveryUnavailableResetAction, action: { + let _ = (context.engine.auth.requestTwoStepPasswordReset() + |> deliverOnMainQueue).start(next: { result in + switch result { + case .done, .waitingForReset: + completion(false) + case .declined: + break + case let .error(reason): + break + } + }) + })]), ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) }) var initialFocusImpl: (() -> Void)? @@ -166,7 +179,7 @@ public func resetPasswordController(context: AccountContext, emailPattern: Strin state.checking = true return state } - saveDisposable.set((recoverTwoStepVerificationPassword(network: context.account.network, code: state.code) + saveDisposable.set((context.engine.auth.performPasswordRecovery(code: state.code, updatedPassword: .none) |> deliverOnMainQueue).start(error: { error in updateState { state in var state = state @@ -177,7 +190,7 @@ public func resetPasswordController(context: AccountContext, emailPattern: Strin switch error { case .invalidCode: text = presentationData.strings.TwoStepAuth_RecoveryCodeInvalid - case .codeExpired: + case .expired: text = presentationData.strings.TwoStepAuth_RecoveryCodeExpired case .limitExceeded: text = presentationData.strings.TwoStepAuth_FloodError @@ -187,7 +200,7 @@ public func resetPasswordController(context: AccountContext, emailPattern: Strin let presentationData = context.sharedContext.currentPresentationData.with { $0 } presentControllerImpl?(textAlertController(context: context, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil) }, completed: { - completion() + completion(true) })) } }) diff --git a/submodules/PasswordSetupUI/Sources/SetupTwoStepVerificationController.swift b/submodules/PasswordSetupUI/Sources/SetupTwoStepVerificationController.swift index 9760ad783d..478e8abc9d 100644 --- a/submodules/PasswordSetupUI/Sources/SetupTwoStepVerificationController.swift +++ b/submodules/PasswordSetupUI/Sources/SetupTwoStepVerificationController.swift @@ -12,6 +12,7 @@ import AccountContext public class SetupTwoStepVerificationController: ViewController { private let context: AccountContext + private let initialState: SetupTwoStepVerificationInitialState private let stateUpdated: (SetupTwoStepVerificationStateUpdate, Bool, SetupTwoStepVerificationController) -> Void @@ -31,21 +32,22 @@ public class SetupTwoStepVerificationController: ViewController { private var presentationData: PresentationData private var presentationDataDisposable: Disposable? - + public init(context: AccountContext, initialState: SetupTwoStepVerificationInitialState, stateUpdated: @escaping (SetupTwoStepVerificationStateUpdate, Bool, SetupTwoStepVerificationController) -> Void) { self.context = context + self.initialState = initialState self.stateUpdated = stateUpdated - self.presentationData = context.sharedContext.currentPresentationData.with { $0 } + self.presentationData = self.context.sharedContext.currentPresentationData.with { $0 } - super.init(navigationBarPresentationData: NavigationBarPresentationData(theme: NavigationBarTheme(buttonColor: self.presentationData.theme.rootController.navigationBar.accentTextColor, disabledButtonColor: self.presentationData.theme.rootController.navigationBar.disabledButtonColor, primaryTextColor: self.presentationData.theme.rootController.navigationBar.primaryTextColor, backgroundColor: .clear, separatorColor: .clear, badgeBackgroundColor: .clear, badgeStrokeColor: .clear, badgeTextColor: .clear), strings: NavigationBarStrings(presentationStrings: self.presentationData.strings))) + super.init(navigationBarPresentationData: NavigationBarPresentationData(theme: NavigationBarTheme(buttonColor: self.presentationData.theme.rootController.navigationBar.accentTextColor, disabledButtonColor: self.presentationData.theme.rootController.navigationBar.disabledButtonColor, primaryTextColor: self.presentationData.theme.rootController.navigationBar.primaryTextColor, backgroundColor: .clear, enableBackgroundBlur: false, separatorColor: .clear, badgeBackgroundColor: .clear, badgeStrokeColor: .clear, badgeTextColor: .clear), strings: NavigationBarStrings(presentationStrings: self.presentationData.strings))) self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style self.navigationItem.setLeftBarButton(UIBarButtonItem(title: self.presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(self.cancelPressed)), animated: false) - self.presentationDataDisposable = (context.sharedContext.presentationData + self.presentationDataDisposable = (self.context.sharedContext.presentationData |> deliverOnMainQueue).start(next: { [weak self] presentationData in if let strongSelf = self { let previousTheme = strongSelf.presentationData.theme @@ -151,7 +153,7 @@ public class SetupTwoStepVerificationController: 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.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition) } @objc private func nextPressed() { diff --git a/submodules/PasswordSetupUI/Sources/SetupTwoStepVerificationControllerNode.swift b/submodules/PasswordSetupUI/Sources/SetupTwoStepVerificationControllerNode.swift index 550b0f781c..8fe2f2b704 100644 --- a/submodules/PasswordSetupUI/Sources/SetupTwoStepVerificationControllerNode.swift +++ b/submodules/PasswordSetupUI/Sources/SetupTwoStepVerificationControllerNode.swift @@ -138,6 +138,7 @@ public enum SetupTwoStepVerificationStateUpdate { case noPassword case awaitingEmailConfirmation(password: String, pattern: String, codeLength: Int32?) case passwordSet(password: String?, hasRecoveryEmail: Bool, hasSecureValues: Bool) + case pendingPasswordReset } final class SetupTwoStepVerificationControllerNode: ViewControllerTracingNode { @@ -161,7 +162,7 @@ final class SetupTwoStepVerificationControllerNode: ViewControllerTracingNode { self.stateUpdated = stateUpdated self.present = present self.dismiss = dismiss - self.presentationData = context.sharedContext.currentPresentationData.with { $0 } + self.presentationData = self.context.sharedContext.currentPresentationData.with { $0 } self.innerState = SetupTwoStepVerificationControllerInnerState(layout: nil, data: SetupTwoStepVerificationControllerDataState(activity: false, state: SetupTwoStepVerificationState(initialState: initialState))) self.activityIndicator = ActivityIndicator(type: .custom(self.presentationData.theme.list.itemAccentColor, 22.0, 2.0, false)) @@ -171,7 +172,7 @@ final class SetupTwoStepVerificationControllerNode: ViewControllerTracingNode { self.processStateUpdated() if self.innerState.data.state == nil { - self.actionDisposable.set((twoStepAuthData(context.account.network) + self.actionDisposable.set((self.context.engine.auth.twoStepAuthData() |> deliverOnMainQueue).start(next: { [weak self] data in guard let strongSelf = self else { return @@ -340,7 +341,7 @@ final class SetupTwoStepVerificationControllerNode: ViewControllerTracingNode { state.data.activity = true return state }, transition: .animated(duration: 0.5, curve: .spring)) - strongSelf.actionDisposable.set((updateTwoStepVerificationPassword(network: strongSelf.context.account.network, currentPassword: nil, updatedPassword: .none) + strongSelf.actionDisposable.set((strongSelf.context.engine.auth.updateTwoStepVerificationPassword(currentPassword: nil, updatedPassword: .none) |> deliverOnMainQueue).start(next: { _ in guard let strongSelf = self else { return @@ -356,7 +357,7 @@ final class SetupTwoStepVerificationControllerNode: ViewControllerTracingNode { guard let strongSelf = self else { return } - strongSelf.present(textAlertController(context: strongSelf.context, title: nil, text: strongSelf.presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), nil) + strongSelf.present(textAlertController(sharedContext: strongSelf.context.sharedContext, title: nil, text: strongSelf.presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), nil) strongSelf.updateState({ state in var state = state state.data.activity = false @@ -393,7 +394,7 @@ final class SetupTwoStepVerificationControllerNode: ViewControllerTracingNode { state.data.activity = true return state }, transition: .animated(duration: 0.5, curve: .spring)) - strongSelf.actionDisposable.set((resendTwoStepRecoveryEmail(network: strongSelf.context.account.network) + strongSelf.actionDisposable.set((strongSelf.context.engine.auth.resendTwoStepRecoveryEmail() |> deliverOnMainQueue).start(error: { error in guard let strongSelf = self else { return @@ -405,7 +406,7 @@ final class SetupTwoStepVerificationControllerNode: ViewControllerTracingNode { case .generic: text = strongSelf.presentationData.strings.Login_UnknownError } - strongSelf.present(textAlertController(context: strongSelf.context, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), nil) + strongSelf.present(textAlertController(sharedContext: strongSelf.context.sharedContext, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), nil) strongSelf.updateState({ state in var state = state state.data.activity = false @@ -526,7 +527,7 @@ final class SetupTwoStepVerificationControllerNode: ViewControllerTracingNode { if password == confirmation { state.data.state = .enterHint(mode: mode, password: password, hint: "") } else { - strongSelf.present(textAlertController(context: strongSelf.context, title: nil, text: strongSelf.presentationData.strings.TwoStepAuth_SetupPasswordConfirmFailed, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), nil) + strongSelf.present(textAlertController(sharedContext: strongSelf.context.sharedContext, title: nil, text: strongSelf.presentationData.strings.TwoStepAuth_SetupPasswordConfirmFailed, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), nil) } case let .enterHint(mode, password, hint): switch mode { @@ -534,7 +535,7 @@ final class SetupTwoStepVerificationControllerNode: ViewControllerTracingNode { state.data.state = .enterEmail(state: .create(password: password, hint: hint), email: "") case let .update(current, hasRecoveryEmail, hasSecureValues): state.data.activity = true - strongSelf.actionDisposable.set((updateTwoStepVerificationPassword(network: strongSelf.context.account.network, currentPassword: current, updatedPassword: .password(password: password, hint: hint, email: nil)) + strongSelf.actionDisposable.set((strongSelf.context.engine.auth.updateTwoStepVerificationPassword(currentPassword: current, updatedPassword: .password(password: password, hint: hint, email: nil)) |> deliverOnMainQueue).start(next: { result in guard let strongSelf = self else { return @@ -558,7 +559,7 @@ final class SetupTwoStepVerificationControllerNode: ViewControllerTracingNode { guard let strongSelf = self else { return } - strongSelf.present(textAlertController(context: strongSelf.context, title: nil, text: strongSelf.presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), nil) + strongSelf.present(textAlertController(sharedContext: strongSelf.context.sharedContext, title: nil, text: strongSelf.presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), nil) strongSelf.updateState({ state in var state = state state.data.activity = false @@ -570,7 +571,7 @@ final class SetupTwoStepVerificationControllerNode: ViewControllerTracingNode { state.data.activity = true switch enterState { case let .create(password, hint): - strongSelf.actionDisposable.set((updateTwoStepVerificationPassword(network: strongSelf.context.account.network, currentPassword: nil, updatedPassword: .password(password: password, hint: hint, email: email)) + strongSelf.actionDisposable.set((strongSelf.context.engine.auth.updateTwoStepVerificationPassword(currentPassword: nil, updatedPassword: .password(password: password, hint: hint, email: email)) |> deliverOnMainQueue).start(next: { result in guard let strongSelf = self else { return @@ -602,7 +603,7 @@ final class SetupTwoStepVerificationControllerNode: ViewControllerTracingNode { case .generic: text = strongSelf.presentationData.strings.Login_UnknownError } - strongSelf.present(textAlertController(context: strongSelf.context, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), nil) + strongSelf.present(textAlertController(sharedContext: strongSelf.context.sharedContext, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), nil) strongSelf.updateState({ state in var state = state state.data.activity = false @@ -618,7 +619,7 @@ final class SetupTwoStepVerificationControllerNode: ViewControllerTracingNode { state.data.activity = true return state }, transition: .animated(duration: 0.5, curve: .spring)) - strongSelf.actionDisposable.set((updateTwoStepVerificationEmail(network: strongSelf.context.account.network, currentPassword: password, updatedEmail: email) + strongSelf.actionDisposable.set((strongSelf.context.engine.auth.updateTwoStepVerificationEmail(currentPassword: password, updatedEmail: email) |> deliverOnMainQueue).start(next: { result in guard let strongSelf = self else { return @@ -644,7 +645,7 @@ final class SetupTwoStepVerificationControllerNode: ViewControllerTracingNode { guard let strongSelf = self else { return } - strongSelf.present(textAlertController(context: strongSelf.context, title: nil, text: strongSelf.presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), nil) + strongSelf.present(textAlertController(sharedContext: strongSelf.context.sharedContext, title: nil, text: strongSelf.presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), nil) strongSelf.updateState({ state in var state = state state.data.activity = false @@ -654,7 +655,7 @@ final class SetupTwoStepVerificationControllerNode: ViewControllerTracingNode { } case let .confirmEmail(confirmState, _, _, code): state.data.activity = true - strongSelf.actionDisposable.set((confirmTwoStepRecoveryEmail(network: strongSelf.context.account.network, code: code) + strongSelf.actionDisposable.set((strongSelf.context.engine.auth.confirmTwoStepRecoveryEmail(code: code) |> deliverOnMainQueue).start(error: { error in guard let strongSelf = self else { return @@ -673,7 +674,7 @@ final class SetupTwoStepVerificationControllerNode: ViewControllerTracingNode { case .generic: text = strongSelf.presentationData.strings.Login_UnknownError } - strongSelf.present(textAlertController(context: strongSelf.context, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), nil) + strongSelf.present(textAlertController(sharedContext: strongSelf.context.sharedContext, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), nil) strongSelf.updateState({ state in var state = state @@ -697,8 +698,8 @@ final class SetupTwoStepVerificationControllerNode: ViewControllerTracingNode { return state }, 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: { + if case let .enterEmail(enterEmailState, enterEmailEmail)? = self.innerState.data.state, case .create = enterEmailState, enterEmailEmail.isEmpty { + self.present(textAlertController(sharedContext: self.context.sharedContext, 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) } else { diff --git a/submodules/PasswordSetupUI/Sources/TwoFactorAuthDataInputScreen.swift b/submodules/PasswordSetupUI/Sources/TwoFactorAuthDataInputScreen.swift index a4f3dcc72a..92e4fa5323 100644 --- a/submodules/PasswordSetupUI/Sources/TwoFactorAuthDataInputScreen.swift +++ b/submodules/PasswordSetupUI/Sources/TwoFactorAuthDataInputScreen.swift @@ -11,35 +11,71 @@ import TelegramPresentationData import PresentationDataUtils import TelegramCore import AnimatedStickerNode +import ActivityIndicator public enum TwoFactorDataInputMode { + public struct Recovery { + public enum Mode { + case notAuthorized(syncContacts: Bool) + case authorized + } + + public var code: String + public var mode: Mode + + public init(code: String, mode: Mode) { + self.code = code + self.mode = mode + } + } + + public enum PasswordRecoveryEmailMode { + case notAuthorized(syncContacts: Bool) + case authorized + } + case password + case passwordRecoveryEmail(emailPattern: String, mode: PasswordRecoveryEmailMode) + case passwordRecovery(Recovery) case emailAddress(password: String, hint: String) case updateEmailAddress(password: String) case emailConfirmation(passwordAndHint: (String, String)?, emailPattern: String, codeLength: Int?) - case passwordHint(password: String) + case passwordHint(recovery: Recovery?, password: String) + case rememberPassword } public final class TwoFactorDataInputScreen: ViewController { - private let context: AccountContext + private let sharedContext: SharedAccountContext + private let engine: SomeTelegramEngine private var presentationData: PresentationData private let mode: TwoFactorDataInputMode private let stateUpdated: (SetupTwoStepVerificationStateUpdate) -> Void + private let actionDisposable = MetaDisposable() - public init(context: AccountContext, mode: TwoFactorDataInputMode, stateUpdated: @escaping (SetupTwoStepVerificationStateUpdate) -> Void) { - self.context = context + private let forgotDataDisposable = MetaDisposable() + private let forgotDataPromise = Promise(nil) + private var didSetupForgotData = false + + public var passwordRecoveryFailed: (() -> Void)? + + public var passwordRemembered: (() -> Void)? + public var twoStepAuthSettingsController: ((TwoStepVerificationConfiguration) -> ViewController)? + + public init(sharedContext: SharedAccountContext, engine: SomeTelegramEngine, mode: TwoFactorDataInputMode, stateUpdated: @escaping (SetupTwoStepVerificationStateUpdate) -> Void, presentation: ViewControllerNavigationPresentation = .modalInLargeLayout) { + self.sharedContext = sharedContext + self.engine = engine self.mode = mode self.stateUpdated = stateUpdated - self.presentationData = context.sharedContext.currentPresentationData.with { $0 } + self.presentationData = self.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) + let navigationBarTheme = NavigationBarTheme(buttonColor: defaultTheme.buttonColor, disabledButtonColor: defaultTheme.disabledButtonColor, primaryTextColor: defaultTheme.primaryTextColor, backgroundColor: .clear, enableBackgroundBlur: false, 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.navigationPresentation = presentation self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait) self.navigationBar?.intrinsicCanTransitionInline = false @@ -49,11 +85,24 @@ public final class TwoFactorDataInputScreen: ViewController { required init(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } + + deinit { + self.actionDisposable.dispose() + self.forgotDataDisposable.dispose() + } @objc private func backPressed() { self.dismiss() } + public override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + if case .rememberPassword = self.mode { + (self.displayNode as? TwoFactorDataInputScreenNode)?.focus() + } + } + override public func loadDisplayNode() { self.displayNode = TwoFactorDataInputScreenNode(presentationData: self.presentationData, mode: self.mode, action: { [weak self] in guard let strongSelf = self else { @@ -86,8 +135,75 @@ public final class TwoFactorDataInputScreen: ViewController { } return true } - controllers.append(TwoFactorDataInputScreen(context: strongSelf.context, mode: .passwordHint(password: values[0]), stateUpdated: strongSelf.stateUpdated)) + controllers.append(TwoFactorDataInputScreen(sharedContext: strongSelf.sharedContext, engine: strongSelf.engine, mode: .passwordHint(recovery: nil, password: values[0]), stateUpdated: strongSelf.stateUpdated, presentation: strongSelf.navigationPresentation)) navigationController.setViewControllers(controllers, animated: true) + case let .passwordRecoveryEmail(_, mode): + guard let text = (strongSelf.displayNode as! TwoFactorDataInputScreenNode).inputText.first, !text.isEmpty else { + return + } + let statusController = OverlayStatusController(theme: strongSelf.presentationData.theme, type: .loading(cancelled: nil)) + strongSelf.present(statusController, in: .window(.root)) + + strongSelf.actionDisposable.set((strongSelf.engine.auth.checkPasswordRecoveryCode(code: text) + |> deliverOnMainQueue).start(error: { [weak statusController] error in + statusController?.dismiss() + guard let strongSelf = self else { + return + } + + let text: String + switch error { + case .limitExceeded: + text = strongSelf.presentationData.strings.LoginPassword_FloodError + case .invalidCode: + text = strongSelf.presentationData.strings.Login_InvalidCodeError + case .expired: + text = strongSelf.presentationData.strings.Login_CodeExpiredError + case .generic: + text = strongSelf.presentationData.strings.Login_UnknownError + } + + 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)) + }, completed: { [weak statusController] in + statusController?.dismiss() + + guard let strongSelf = self else { + return + } + + let mappedMode: TwoFactorDataInputMode.Recovery.Mode + switch mode { + case .authorized: + mappedMode = .authorized + case let .notAuthorized(syncContacts): + mappedMode = .notAuthorized(syncContacts: syncContacts) + } + + let setupController = TwoFactorDataInputScreen(sharedContext: strongSelf.sharedContext, engine: strongSelf.engine, mode: .passwordRecovery(TwoFactorDataInputMode.Recovery(code: text, mode: mappedMode)), stateUpdated: strongSelf.stateUpdated, presentation: strongSelf.navigationPresentation) + + guard let navigationController = strongSelf.navigationController as? NavigationController else { + return + } + navigationController.replaceController(strongSelf, with: setupController, animated: true) + })) + case let .passwordRecovery(recovery): + let values = (strongSelf.displayNode as! TwoFactorDataInputScreenNode).inputText + if values.count != 2 { + return + } + if values[0] != values[1] { + 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 + } + if values[0].isEmpty { + return + } + guard let navigationController = strongSelf.navigationController as? NavigationController else { + return + } + navigationController.replaceController(strongSelf, with: TwoFactorDataInputScreen(sharedContext: strongSelf.sharedContext, engine: strongSelf.engine, mode: .passwordHint(recovery: recovery, password: values[0]), stateUpdated: strongSelf.stateUpdated, presentation: strongSelf.navigationPresentation), animated: true) case let .emailAddress(password, hint): guard let text = (strongSelf.displayNode as! TwoFactorDataInputScreenNode).inputText.first, !text.isEmpty else { return @@ -95,7 +211,7 @@ public final class TwoFactorDataInputScreen: ViewController { let statusController = OverlayStatusController(theme: strongSelf.presentationData.theme, type: .loading(cancelled: nil)) strongSelf.present(statusController, in: .window(.root)) - let _ = (updateTwoStepVerificationPassword(network: strongSelf.context.account.network, currentPassword: "", updatedPassword: .password(password: password, hint: hint, email: text)) + let _ = (strongSelf.engine.auth.updateTwoStepVerificationPassword(currentPassword: "", updatedPassword: .password(password: password, hint: hint, email: text)) |> deliverOnMainQueue).start(next: { [weak statusController] result in statusController?.dismiss() @@ -120,7 +236,7 @@ public final class TwoFactorDataInputScreen: ViewController { } return true } - controllers.append(TwoFactorDataInputScreen(context: strongSelf.context, mode: .emailConfirmation(passwordAndHint: (password, hint), emailPattern: text, codeLength: pendingEmail.codeLength.flatMap(Int.init)), stateUpdated: strongSelf.stateUpdated)) + controllers.append(TwoFactorDataInputScreen(sharedContext: strongSelf.sharedContext, engine: strongSelf.engine, mode: .emailConfirmation(passwordAndHint: (password, hint), emailPattern: text, codeLength: pendingEmail.codeLength.flatMap(Int.init)), stateUpdated: strongSelf.stateUpdated, presentation: strongSelf.navigationPresentation)) navigationController.setViewControllers(controllers, animated: true) } else { guard let navigationController = strongSelf.navigationController as? NavigationController else { @@ -135,7 +251,7 @@ public final class TwoFactorDataInputScreen: ViewController { } return true } - controllers.append(TwoFactorAuthSplashScreen(context: strongSelf.context, mode: .done)) + controllers.append(TwoFactorAuthSplashScreen(sharedContext: strongSelf.sharedContext, engine: strongSelf.engine, mode: .done)) navigationController.setViewControllers(controllers, animated: true) } } @@ -154,7 +270,7 @@ public final class TwoFactorDataInputScreen: ViewController { case .invalidEmail: alertText = presentationData.strings.TwoStepAuth_EmailInvalid } - strongSelf.present(textAlertController(context: strongSelf.context, title: nil, text: alertText, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + strongSelf.present(textAlertController(sharedContext: strongSelf.sharedContext, title: nil, text: alertText, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root)) }) case let .updateEmailAddress(password): guard let text = (strongSelf.displayNode as! TwoFactorDataInputScreenNode).inputText.first, !text.isEmpty else { @@ -162,105 +278,182 @@ public final class TwoFactorDataInputScreen: ViewController { } let statusController = OverlayStatusController(theme: strongSelf.presentationData.theme, type: .loading(cancelled: nil)) strongSelf.present(statusController, in: .window(.root)) - - let _ = (updateTwoStepVerificationEmail(network: strongSelf.context.account.network, currentPassword: password, updatedEmail: text) - |> deliverOnMainQueue).start(next: { [weak statusController] result in - statusController?.dismiss() - - guard let strongSelf = self else { - return - } - - switch result { - case .none: - break - case let .password(_, pendingEmail): - if let pendingEmail = pendingEmail { - guard let navigationController = strongSelf.navigationController as? NavigationController else { - return - } - var controllers = navigationController.viewControllers.filter { controller in - if controller is TwoFactorAuthSplashScreen { - return false - } - if controller is TwoFactorDataInputScreen { - return false - } - return true - } - controllers.append(TwoFactorDataInputScreen(context: strongSelf.context, mode: .emailConfirmation(passwordAndHint: (password, ""), emailPattern: text, codeLength: pendingEmail.codeLength.flatMap(Int.init)), stateUpdated: strongSelf.stateUpdated)) - navigationController.setViewControllers(controllers, animated: true) - } else { - guard let navigationController = strongSelf.navigationController as? NavigationController else { - return - } - var controllers = navigationController.viewControllers.filter { controller in - if controller is TwoFactorAuthSplashScreen { - return false - } - if controller is TwoFactorDataInputScreen { - return false - } - return true - } - controllers.append(TwoFactorAuthSplashScreen(context: strongSelf.context, mode: .done)) - navigationController.setViewControllers(controllers, animated: true) + + switch strongSelf.engine { + case let .authorized(engine): + let _ = (engine.auth.updateTwoStepVerificationEmail(currentPassword: password, updatedEmail: text) + |> deliverOnMainQueue).start(next: { [weak statusController] result in + statusController?.dismiss() + + guard let strongSelf = self else { + return } - } - }, error: { [weak statusController] error in - statusController?.dismiss() - - guard let strongSelf = self else { - return - } - - let presentationData = strongSelf.presentationData - let alertText: String - switch error { - case .generic: - alertText = presentationData.strings.Login_UnknownError - case .invalidEmail: - alertText = presentationData.strings.TwoStepAuth_EmailInvalid - } - strongSelf.present(textAlertController(context: strongSelf.context, title: nil, text: alertText, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root)) - }) + + switch result { + case .none: + break + case let .password(_, pendingEmail): + if let pendingEmail = pendingEmail { + guard let navigationController = strongSelf.navigationController as? NavigationController else { + return + } + var controllers = navigationController.viewControllers.filter { controller in + if controller is TwoFactorAuthSplashScreen { + return false + } + if controller is TwoFactorDataInputScreen { + return false + } + return true + } + controllers.append(TwoFactorDataInputScreen(sharedContext: strongSelf.sharedContext, engine: strongSelf.engine, mode: .emailConfirmation(passwordAndHint: (password, ""), emailPattern: text, codeLength: pendingEmail.codeLength.flatMap(Int.init)), stateUpdated: strongSelf.stateUpdated)) + navigationController.setViewControllers(controllers, animated: true) + } else { + guard let navigationController = strongSelf.navigationController as? NavigationController else { + return + } + var controllers = navigationController.viewControllers.filter { controller in + if controller is TwoFactorAuthSplashScreen { + return false + } + if controller is TwoFactorDataInputScreen { + return false + } + return true + } + controllers.append(TwoFactorAuthSplashScreen(sharedContext: strongSelf.sharedContext, engine: strongSelf.engine, mode: .done)) + navigationController.setViewControllers(controllers, animated: true) + } + } + }, error: { [weak statusController] error in + statusController?.dismiss() + + guard let strongSelf = self else { + return + } + + let presentationData = strongSelf.presentationData + let alertText: String + switch error { + case .generic: + alertText = presentationData.strings.Login_UnknownError + case .invalidEmail: + alertText = presentationData.strings.TwoStepAuth_EmailInvalid + } + strongSelf.present(textAlertController(sharedContext: strongSelf.sharedContext, title: nil, text: alertText, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + }) + case .unauthorized: + break + } case .emailConfirmation: guard let text = (strongSelf.displayNode as! TwoFactorDataInputScreenNode).inputText.first, !text.isEmpty else { return } let statusController = OverlayStatusController(theme: strongSelf.presentationData.theme, type: .loading(cancelled: nil)) strongSelf.present(statusController, in: .window(.root)) + + switch strongSelf.engine { + case let .authorized(engine): + let _ = (engine.auth.confirmTwoStepRecoveryEmail(code: text) + |> deliverOnMainQueue).start(error: { [weak statusController] error in + statusController?.dismiss() + + guard let strongSelf = self else { + return + } + + let presentationData = strongSelf.presentationData + let text: String + switch error { + case .invalidEmail: + text = presentationData.strings.TwoStepAuth_EmailInvalid + case .invalidCode: + text = presentationData.strings.Login_InvalidCodeError + case .expired: + text = presentationData.strings.TwoStepAuth_EmailCodeExpired + case .flood: + text = presentationData.strings.TwoStepAuth_FloodError + case .generic: + text = presentationData.strings.Login_UnknownError + } + strongSelf.present(textAlertController(sharedContext: strongSelf.sharedContext, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + }, completed: { [weak statusController] in + statusController?.dismiss() + + guard let strongSelf = self else { + return + } + + guard let navigationController = strongSelf.navigationController as? NavigationController else { + return + } + var controllers = navigationController.viewControllers.filter { controller in + if controller is TwoFactorAuthSplashScreen { + return false + } + if controller is TwoFactorDataInputScreen { + return false + } + return true + } + controllers.append(TwoFactorAuthSplashScreen(sharedContext: strongSelf.sharedContext, engine: strongSelf.engine, mode: .done)) + navigationController.setViewControllers(controllers, animated: true) + }) + case .unauthorized: + break + } + case let .passwordHint(recovery, password): + guard let value = (strongSelf.displayNode as! TwoFactorDataInputScreenNode).inputText.first, !value.isEmpty else { + return + } + + if let recovery = recovery { + strongSelf.performRecovery(recovery: recovery, password: password, hint: value) + } else { + strongSelf.push(TwoFactorDataInputScreen(sharedContext: strongSelf.sharedContext, engine: strongSelf.engine, mode: .emailAddress(password: password, hint: value), stateUpdated: strongSelf.stateUpdated, presentation: strongSelf.navigationPresentation)) + } + case .rememberPassword: + guard case let .authorized(engine) = strongSelf.engine else { + return + } - let _ = (confirmTwoStepRecoveryEmail(network: strongSelf.context.account.network, code: text) - |> deliverOnMainQueue).start(error: { [weak statusController] error in - statusController?.dismiss() - + guard let value = (strongSelf.displayNode as! TwoFactorDataInputScreenNode).inputText.first, !value.isEmpty else { + return + } + + (strongSelf.displayNode as? TwoFactorDataInputScreenNode)?.isLoading = true + + let _ = (engine.auth.requestTwoStepVerifiationSettings(password: value) + |> deliverOnMainQueue).start(error: { [weak self] error in guard let strongSelf = self else { return } + (strongSelf.displayNode as? TwoFactorDataInputScreenNode)?.isLoading = false + let presentationData = strongSelf.presentationData - let text: String + let text: String? switch error { - case .invalidEmail: - text = presentationData.strings.TwoStepAuth_EmailInvalid - case .invalidCode: - text = presentationData.strings.Login_InvalidCodeError - case .expired: - text = presentationData.strings.TwoStepAuth_EmailCodeExpired - case .flood: - text = presentationData.strings.TwoStepAuth_FloodError - case .generic: - text = presentationData.strings.Login_UnknownError + case .limitExceeded: + text = presentationData.strings.LoginPassword_FloodError + case .invalidPassword: + text = nil + case .generic: + text = presentationData.strings.Login_UnknownError } - strongSelf.present(textAlertController(context: strongSelf.context, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root)) - }, completed: { [weak statusController] in - statusController?.dismiss() - + if let text = text { + strongSelf.present(textAlertController(sharedContext: strongSelf.sharedContext, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + } else { + (strongSelf.displayNode as? TwoFactorDataInputScreenNode)?.onAction(success: false) + } + }, completed: { [weak self] in guard let strongSelf = self else { return } + (strongSelf.displayNode as? TwoFactorDataInputScreenNode)?.isLoading = false + strongSelf.passwordRemembered?() + guard let navigationController = strongSelf.navigationController as? NavigationController else { return } @@ -273,15 +466,9 @@ public final class TwoFactorDataInputScreen: ViewController { } return true } - controllers.append(TwoFactorAuthSplashScreen(context: strongSelf.context, mode: .done)) + controllers.append(TwoFactorAuthSplashScreen(sharedContext: strongSelf.sharedContext, engine: strongSelf.engine, mode: .remember)) navigationController.setViewControllers(controllers, animated: true) }) - case let .passwordHint(password): - guard let value = (strongSelf.displayNode as! TwoFactorDataInputScreenNode).inputText.first, !value.isEmpty else { - return - } - - strongSelf.push(TwoFactorDataInputScreen(context: strongSelf.context, mode: .emailAddress(password: password, hint: value), stateUpdated: strongSelf.stateUpdated)) } }, skipAction: { [weak self] in guard let strongSelf = self else { @@ -297,7 +484,7 @@ public final class TwoFactorDataInputScreen: ViewController { let statusController = OverlayStatusController(theme: strongSelf.presentationData.theme, type: .loading(cancelled: nil)) strongSelf.present(statusController, in: .window(.root)) - let _ = (updateTwoStepVerificationPassword(network: strongSelf.context.account.network, currentPassword: "", updatedPassword: .password(password: password, hint: hint, email: nil)) + let _ = (strongSelf.engine.auth.updateTwoStepVerificationPassword(currentPassword: "", updatedPassword: .password(password: password, hint: hint, email: nil)) |> deliverOnMainQueue).start(next: { [weak statusController] result in statusController?.dismiss() @@ -321,7 +508,7 @@ public final class TwoFactorDataInputScreen: ViewController { } return true } - controllers.append(TwoFactorAuthSplashScreen(context: strongSelf.context, mode: .done)) + controllers.append(TwoFactorAuthSplashScreen(sharedContext: strongSelf.sharedContext, engine: strongSelf.engine, mode: .done)) navigationController.setViewControllers(controllers, animated: true) } }, error: { [weak statusController] error in @@ -339,13 +526,177 @@ public final class TwoFactorDataInputScreen: ViewController { case .invalidEmail: alertText = presentationData.strings.TwoStepAuth_EmailInvalid } - strongSelf.present(textAlertController(context: strongSelf.context, title: nil, text: alertText, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + strongSelf.present(textAlertController(sharedContext: strongSelf.sharedContext, title: nil, text: alertText, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root)) }) }), TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {}) ]), in: .window(.root)) - case let .passwordHint(password): - strongSelf.push(TwoFactorDataInputScreen(context: strongSelf.context, mode: .emailAddress(password: password, hint: ""), stateUpdated: strongSelf.stateUpdated)) + case let .passwordHint(recovery, password): + if let recovery = recovery { + strongSelf.performRecovery(recovery: recovery, password: password, hint: "") + } else { + strongSelf.push(TwoFactorDataInputScreen(sharedContext: strongSelf.sharedContext, engine: strongSelf.engine, mode: .emailAddress(password: password, hint: ""), stateUpdated: strongSelf.stateUpdated, presentation: strongSelf.navigationPresentation)) + } + case let .passwordRecovery(recovery): + strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: strongSelf.presentationData.strings.TwoFactorSetup_PasswordRecovery_SkipAlertTitle, text: strongSelf.presentationData.strings.TwoFactorSetup_PasswordRecovery_SkipAlertText, actions: [ + TextAlertAction(type: .destructiveAction, title: strongSelf.presentationData.strings.TwoFactorSetup_PasswordRecovery_SkipAlertAction, action: { + guard let strongSelf = self else { + return + } + strongSelf.performRecovery(recovery: recovery, password: "", hint: "") + }), + TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {}) + ]), in: .window(.root)) + case .rememberPassword: + guard case let .authorized(engine) = strongSelf.engine else { + return + } + + strongSelf.view.endEditing(true) + + let sharedContext = strongSelf.sharedContext + let presentationData = sharedContext.currentPresentationData.with { $0 } + let navigationController = strongSelf.navigationController as? NavigationController + + if !strongSelf.didSetupForgotData { + strongSelf.didSetupForgotData = true + strongSelf.forgotDataPromise.set(engine.auth.twoStepVerificationConfiguration() |> map(Optional.init)) + } + let disposable = strongSelf.forgotDataDisposable + + disposable.set((strongSelf.forgotDataPromise.get() + |> take(1) + |> deliverOnMainQueue).start(next: { [weak self] configuration in + if let strongSelf = self, let configuration = configuration { + switch configuration { + case let .set(_, hasRecoveryEmail, _, _, pendingResetTimestamp): + if hasRecoveryEmail { + disposable.set((engine.auth.requestTwoStepVerificationPasswordRecoveryCode() + |> deliverOnMainQueue).start(next: { emailPattern in + var stateUpdated: ((SetupTwoStepVerificationStateUpdate) -> Void)? + let controller = TwoFactorDataInputScreen(sharedContext: sharedContext, engine: .authorized(engine), mode: .passwordRecoveryEmail(emailPattern: emailPattern, mode: .authorized), stateUpdated: { state in + stateUpdated?(state) + }) + stateUpdated = { [weak controller] state in + controller?.view.endEditing(true) + controller?.dismiss() + + switch state { + case .noPassword, .awaitingEmailConfirmation, .passwordSet: + controller?.dismiss() + + navigationController?.filterController(strongSelf, animated: true) + case .pendingPasswordReset: + let _ = (engine.auth.twoStepVerificationConfiguration() + |> deliverOnMainQueue).start(next: { [weak self] configuration in + if let strongSelf = self { + if let navigationController = navigationController, let twoStepAuthSettingsController = strongSelf.twoStepAuthSettingsController?(configuration) { + var controllers = navigationController.viewControllers.filter { controller in + if controller is TwoFactorDataInputScreen { + return false + } + return true + } + controllers.append(twoStepAuthSettingsController) + navigationController.setViewControllers(controllers, animated: true) + } + } + }) + } + } + strongSelf.push(controller) + }, error: { _ in + strongSelf.present(textAlertController(sharedContext: sharedContext, title: nil, text: presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + })) + } else { + if let pendingResetTimestamp = pendingResetTimestamp { + let remainingSeconds = pendingResetTimestamp - Int32(Date().timeIntervalSince1970) + if remainingSeconds <= 0 { + let _ = (engine.auth.requestTwoStepPasswordReset() + |> deliverOnMainQueue).start(next: { result in + switch result { + case .done, .waitingForReset: + let _ = (engine.auth.twoStepVerificationConfiguration() + |> deliverOnMainQueue).start(next: { [weak self] configuration in + if let strongSelf = self { + if let navigationController = navigationController, let twoStepAuthSettingsController = strongSelf.twoStepAuthSettingsController?(configuration) { + var controllers = navigationController.viewControllers.filter { controller in + if controller is TwoFactorDataInputScreen { + return false + } + return true + } + controllers.append(twoStepAuthSettingsController) + navigationController.setViewControllers(controllers, animated: true) + } + } + }) + case .declined: + break + case let .error(reason): + let text: String + switch reason { + case let .limitExceeded(retryAtTimestamp): + if let retryAtTimestamp = retryAtTimestamp { + let remainingSeconds = retryAtTimestamp - Int32(Date().timeIntervalSince1970) + text = presentationData.strings.TwoFactorSetup_ResetFloodWait(timeIntervalString(strings: presentationData.strings, value: remainingSeconds)).0 + } else { + text = presentationData.strings.TwoStepAuth_FloodError + } + case .generic: + text = presentationData.strings.Login_UnknownError + } + strongSelf.present(textAlertController(sharedContext: sharedContext, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + } + }) + } + } else { + strongSelf.present(textAlertController(sharedContext: sharedContext, title: presentationData.strings.TwoStepAuth_RecoveryUnavailableResetTitle, text: presentationData.strings.TwoStepAuth_RecoveryUnavailableResetText, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: presentationData.strings.TwoStepAuth_RecoveryUnavailableResetAction, action: { + let _ = (engine.auth.requestTwoStepPasswordReset() + |> deliverOnMainQueue).start(next: { result in + switch result { + case .done, .waitingForReset: + let _ = (engine.auth.twoStepVerificationConfiguration() + |> deliverOnMainQueue).start(next: { [weak self] configuration in + if let strongSelf = self { + if let navigationController = navigationController, let twoStepAuthSettingsController = strongSelf.twoStepAuthSettingsController?(configuration) { + var controllers = navigationController.viewControllers.filter { controller in + if controller is TwoFactorDataInputScreen { + return false + } + return true + } + controllers.append(twoStepAuthSettingsController) + navigationController.setViewControllers(controllers, animated: true) + } + } + }) + case .declined: + break + case let .error(reason): + let text: String + switch reason { + case let .limitExceeded(retryAtTimestamp): + if let retryAtTimestamp = retryAtTimestamp { + let remainingSeconds = retryAtTimestamp - Int32(Date().timeIntervalSince1970) + text = presentationData.strings.TwoFactorSetup_ResetFloodWait(timeIntervalString(strings: presentationData.strings, value: remainingSeconds)).0 + } else { + text = presentationData.strings.TwoStepAuth_FloodError + } + case .generic: + text = presentationData.strings.Login_UnknownError + } + strongSelf.present(textAlertController(sharedContext: sharedContext, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + } + }) + })]), in: .window(.root)) + } + } + case .notSet: + break + } + } + })) default: break } @@ -368,10 +719,49 @@ public final class TwoFactorDataInputScreen: ViewController { } return true } - controllers.append(TwoFactorDataInputScreen(context: strongSelf.context, mode: .emailAddress(password: password, hint: hint), stateUpdated: strongSelf.stateUpdated)) + controllers.append(TwoFactorDataInputScreen(sharedContext: strongSelf.sharedContext, engine: strongSelf.engine, mode: .emailAddress(password: password, hint: hint), stateUpdated: strongSelf.stateUpdated, presentation: strongSelf.navigationPresentation)) navigationController.setViewControllers(controllers, animated: true) } else { } + case .passwordRecoveryEmail: + switch strongSelf.engine { + case let .authorized(engine): + strongSelf.present(textAlertController(sharedContext: strongSelf.sharedContext, title: strongSelf.presentationData.strings.TwoStepAuth_RecoveryUnavailableResetTitle, text: strongSelf.presentationData.strings.TwoStepAuth_RecoveryEmailResetText, actions: [TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.TwoStepAuth_RecoveryUnavailableResetAction, action: { + let _ = (engine.auth.requestTwoStepPasswordReset() + |> deliverOnMainQueue).start(next: { result in + guard let strongSelf = self else { + return + } + switch result { + case .done, .waitingForReset: + strongSelf.stateUpdated(.pendingPasswordReset) + case .declined: + break + case let .error(reason): + let text: String + switch reason { + case let .limitExceeded(retryAtTimestamp): + if let retryAtTimestamp = retryAtTimestamp { + let remainingSeconds = retryAtTimestamp - Int32(Date().timeIntervalSince1970) + text = strongSelf.presentationData.strings.TwoFactorSetup_ResetFloodWait(timeIntervalString(strings: strongSelf.presentationData.strings, value: remainingSeconds)).0 + } else { + text = strongSelf.presentationData.strings.TwoStepAuth_FloodError + } + case .generic: + text = strongSelf.presentationData.strings.Login_UnknownError + } + strongSelf.present(textAlertController(sharedContext: strongSelf.sharedContext, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + } + }) + })]), in: .window(.root)) + case .unauthorized: + strongSelf.present(textAlertController(sharedContext: strongSelf.sharedContext, title: nil, text: strongSelf.presentationData.strings.TwoStepAuth_RecoveryFailed, actions: [TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_OK, action: { + guard let strongSelf = self else { + return + } + strongSelf.passwordRecoveryFailed?() + })]), in: .window(.root)) + } default: break } @@ -379,29 +769,57 @@ public final class TwoFactorDataInputScreen: ViewController { guard let strongSelf = self else { return } - - let statusController = OverlayStatusController(theme: strongSelf.presentationData.theme, type: .loading(cancelled: nil)) - strongSelf.present(statusController, in: .window(.root)) - - let _ = (resendTwoStepRecoveryEmail(network: strongSelf.context.account.network) - |> deliverOnMainQueue).start(error: { [weak statusController] error in - statusController?.dismiss() - - guard let strongSelf = self else { + switch strongSelf.mode { + case .passwordRecoveryEmail: + let statusController = OverlayStatusController(theme: strongSelf.presentationData.theme, type: .loading(cancelled: nil)) + strongSelf.present(statusController, in: .window(.root)) + + let _ = (strongSelf.engine.auth.requestTwoStepVerificationPasswordRecoveryCode() + |> deliverOnMainQueue).start(error: { [weak statusController] error in + statusController?.dismiss() + + guard let strongSelf = self else { + return + } + + let text: String + switch error { + case .limitExceeded: + text = strongSelf.presentationData.strings.TwoStepAuth_FloodError + case .generic: + text = strongSelf.presentationData.strings.Login_UnknownError + } + strongSelf.present(textAlertController(sharedContext: strongSelf.sharedContext, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + }, completed: { [weak statusController] in + statusController?.dismiss() + }) + default: + guard case let .authorized(engine) = strongSelf.engine else { return } - - let text: String - switch error { - case .flood: - text = strongSelf.presentationData.strings.TwoStepAuth_FloodError - case .generic: - text = strongSelf.presentationData.strings.Login_UnknownError - } - strongSelf.present(textAlertController(context: strongSelf.context, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) - }, completed: { [weak statusController] in - statusController?.dismiss() - }) + let statusController = OverlayStatusController(theme: strongSelf.presentationData.theme, type: .loading(cancelled: nil)) + strongSelf.present(statusController, in: .window(.root)) + + let _ = (engine.auth.resendTwoStepRecoveryEmail() + |> deliverOnMainQueue).start(error: { [weak statusController] error in + statusController?.dismiss() + + guard let strongSelf = self else { + return + } + + let text: String + switch error { + case .flood: + text = strongSelf.presentationData.strings.TwoStepAuth_FloodError + case .generic: + text = strongSelf.presentationData.strings.Login_UnknownError + } + strongSelf.present(textAlertController(sharedContext: strongSelf.sharedContext, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + }, completed: { [weak statusController] in + statusController?.dismiss() + }) + } }) self.displayNodeDidLoad() @@ -410,7 +828,97 @@ public final class TwoFactorDataInputScreen: ViewController { override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { super.containerLayoutUpdated(layout, transition: transition) - (self.displayNode as! TwoFactorDataInputScreenNode).containerLayoutUpdated(layout: layout, navigationHeight: self.navigationHeight, transition: transition) + (self.displayNode as! TwoFactorDataInputScreenNode).containerLayoutUpdated(layout: layout, navigationHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition) + } + + private func performRecovery(recovery: TwoFactorDataInputMode.Recovery, password: String, hint: String) { + let statusController = OverlayStatusController(theme: self.presentationData.theme, type: .loading(cancelled: nil)) + self.present(statusController, in: .window(.root)) + + switch self.engine { + case let .unauthorized(engine): + var syncContacts = false + switch recovery.mode { + case let .notAuthorized(syncContactsValue): + syncContacts = syncContactsValue + case .authorized: + break + } + let _ = (engine.auth.performPasswordRecovery(code: recovery.code, updatedPassword: password.isEmpty ? .none : .password(password: password, hint: hint, email: nil)) + |> deliverOnMainQueue).start(next: { [weak self, weak statusController] recoveredAccountData in + statusController?.dismiss() + + guard let strongSelf = self else { + return + } + + if password.isEmpty { + strongSelf.stateUpdated(.noPassword) + } else { + strongSelf.stateUpdated(.passwordSet(password: password, hasRecoveryEmail: true, hasSecureValues: false)) + } + + (strongSelf.navigationController as? NavigationController)?.replaceController(strongSelf, with: TwoFactorAuthSplashScreen(sharedContext: strongSelf.sharedContext, engine: strongSelf.engine, mode: .recoveryDone(recoveredAccountData: recoveredAccountData, syncContacts: syncContacts, isPasswordSet: !password.isEmpty)), animated: true) + }, error: { [weak self, weak statusController] error in + statusController?.dismiss() + + guard let strongSelf = self else { + return + } + + let text: String + switch error { + case .limitExceeded: + text = strongSelf.presentationData.strings.LoginPassword_FloodError + case .invalidCode: + text = strongSelf.presentationData.strings.Login_InvalidCodeError + case .expired: + text = strongSelf.presentationData.strings.Login_CodeExpiredError + case .generic: + text = strongSelf.presentationData.strings.Login_UnknownError + } + + strongSelf.present(textAlertController(sharedContext: strongSelf.sharedContext, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + }, completed: { [weak statusController] in + statusController?.dismiss() + }) + case let .authorized(engine): + let _ = (engine.auth.performPasswordRecovery(code: recovery.code, updatedPassword: password.isEmpty ? .none : .password(password: password, hint: hint, email: nil)) + |> deliverOnMainQueue).start(error: { [weak self, weak statusController] error in + statusController?.dismiss() + + guard let strongSelf = self else { + return + } + + let text: String + switch error { + case .limitExceeded: + text = strongSelf.presentationData.strings.LoginPassword_FloodError + case .invalidCode: + text = strongSelf.presentationData.strings.Login_InvalidCodeError + case .expired: + text = strongSelf.presentationData.strings.Login_CodeExpiredError + case .generic: + text = strongSelf.presentationData.strings.Login_UnknownError + } + + strongSelf.present(textAlertController(sharedContext: strongSelf.sharedContext, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + }, completed: { [weak self, weak statusController] in + statusController?.dismiss() + + guard let strongSelf = self else { + return + } + + if password.isEmpty { + strongSelf.stateUpdated(.noPassword) + } else { + strongSelf.stateUpdated(.passwordSet(password: password, hasRecoveryEmail: true, hasSecureValues: false)) + } + strongSelf.dismiss() + }) + } } } @@ -479,10 +987,17 @@ private final class TwoFactorDataInputTextNode: ASDisplayNode, UITextFieldDelega private let hideButtonNode: HighlightableButtonNode private let clearButtonNode: HighlightableButtonNode + private var activityIndicator: ActivityIndicator? + fileprivate var ignoreTextChanged: Bool = false var isFocused: Bool { - return self.inputNode.textField.isFirstResponder + get { + return self.inputNode.textField.isFirstResponder + } + set { + let _ = self.inputNode.textField.becomeFirstResponder() + } } var text: String { @@ -494,6 +1009,43 @@ private final class TwoFactorDataInputTextNode: ASDisplayNode, UITextFieldDelega } } + var isFailed: Bool = false { + didSet { + if self.isFailed != oldValue { + UIView.transition(with: self.view, duration: 0.2, options: [.transitionCrossDissolve, .curveEaseInOut]) { + self.inputNode.textField.textColor = self.isFailed ? self.theme.list.itemDestructiveColor : self.theme.list.freePlainInputField.primaryColor + self.hideButtonNode.setImage(generateTextHiddenImage(color: self.isFailed ? self.theme.list.itemDestructiveColor : self.theme.list.freePlainInputField.controlColor, on: !self.inputNode.textField.isSecureTextEntry), for: []) + self.backgroundNode.image = self.isFailed ? generateStretchableFilledCircleImage(diameter: 20.0, color: self.theme.list.itemDestructiveColor.withAlphaComponent(0.1)) : generateStretchableFilledCircleImage(diameter: 20.0, color: self.theme.list.freePlainInputField.backgroundColor) + } completion: { _ in + + } + } + } + } + + var isLoading: Bool = false { + didSet { + if self.isLoading != oldValue { + if self.isLoading { + if self.activityIndicator == nil { + let activityIndicator = ActivityIndicator(type: .custom(self.theme.actionSheet.inputClearButtonColor, 24.0, 1.0, false)) + self.activityIndicator = activityIndicator + self.addSubnode(activityIndicator) + if let size = self.validLayout { + self.updateLayout(size: size, transition: .immediate) + } + } + } else if let activityIndicator = self.activityIndicator { + self.activityIndicator = nil + activityIndicator.removeFromSupernode() + } + self.hideButtonNode.isHidden = self.isLoading + } + } + } + + private var validLayout: CGSize? + 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 @@ -591,6 +1143,9 @@ private final class TwoFactorDataInputTextNode: ASDisplayNode, UITextFieldDelega } func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { + if self.isLoading { + return false + } return true } @@ -598,7 +1153,9 @@ private final class TwoFactorDataInputTextNode: ASDisplayNode, UITextFieldDelega if !self.ignoreTextChanged { switch self.mode { case .password: - break + if self.isFailed { + self.isFailed = false + } default: self.clearButtonNode.isHidden = self.text.isEmpty } @@ -616,6 +1173,8 @@ private final class TwoFactorDataInputTextNode: ASDisplayNode, UITextFieldDelega } func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) { + self.validLayout = size + let leftInset: CGFloat = 16.0 let rightInset: CGFloat = 38.0 @@ -623,6 +1182,11 @@ private final class TwoFactorDataInputTextNode: ASDisplayNode, UITextFieldDelega transition.updateFrame(node: self.inputNode, frame: CGRect(origin: CGPoint(x: leftInset, y: 0.0), size: CGSize(width: size.width - leftInset - rightInset, height: size.height))) transition.updateFrame(node: self.hideButtonNode, frame: CGRect(origin: CGPoint(x: size.width - rightInset - 4.0, y: 0.0), size: CGSize(width: rightInset + 4.0, height: size.height))) transition.updateFrame(node: self.clearButtonNode, frame: CGRect(origin: CGPoint(x: size.width - rightInset - 4.0, y: 0.0), size: CGSize(width: rightInset + 4.0, height: size.height))) + + if let activityIndicator = self.activityIndicator { + let indicatorSize = activityIndicator.measure(CGSize(width: 24.0, height: 24.0)) + transition.updateFrame(node: activityIndicator, frame: CGRect(origin: CGPoint(x: size.width - rightInset + 6.0, y: floorToScreenPixels((size.height - indicatorSize.height) / 2.0) + 1.0), size: indicatorSize)) + } } func focus() { @@ -630,7 +1194,7 @@ private final class TwoFactorDataInputTextNode: ASDisplayNode, UITextFieldDelega } func updateTextHidden(_ value: Bool) { - self.hideButtonNode.setImage(generateTextHiddenImage(color: self.theme.actionSheet.inputClearButtonColor, on: !value), for: []) + self.hideButtonNode.setImage(generateTextHiddenImage(color: self.isFailed ? self.theme.list.itemDestructiveColor : self.theme.list.freePlainInputField.controlColor, on: !value), for: []) let text = self.inputNode.textField.text ?? "" self.inputNode.textField.isSecureTextEntry = value if value { @@ -671,12 +1235,18 @@ private final class TwoFactorDataInputScreenNode: ViewControllerTracingNode, UIS private let inputNodes: [TwoFactorDataInputTextNode] private let buttonNode: SolidRoundedButtonNode - private var navigationHeight: CGFloat? + private var validLayout: (ContainerViewLayout, CGFloat)? var inputText: [String] { return self.inputNodes.map { $0.text } } + var isLoading: Bool = false { + didSet { + self.inputNodes.first?.isLoading = self.isLoading + } + } + init(presentationData: PresentationData, mode: TwoFactorDataInputMode, action: @escaping () -> Void, skipAction: @escaping () -> Void, changeEmailAction: @escaping () -> Void, resendCodeAction: @escaping () -> Void) { self.presentationData = presentationData self.mode = mode @@ -686,7 +1256,7 @@ private final class TwoFactorDataInputScreenNode: ViewControllerTracingNode, UIS self.resendCodeAction = resendCodeAction self.navigationBackgroundNode = ASDisplayNode() - self.navigationBackgroundNode.backgroundColor = self.presentationData.theme.rootController.navigationBar.backgroundColor + self.navigationBackgroundNode.backgroundColor = self.presentationData.theme.rootController.navigationBar.opaqueBackgroundColor self.navigationBackgroundNode.alpha = 0.0 self.navigationSeparatorNode = ASDisplayNode() self.navigationSeparatorNode.backgroundColor = self.presentationData.theme.rootController.navigationBar.separatorColor @@ -695,9 +1265,9 @@ private final class TwoFactorDataInputScreenNode: ViewControllerTracingNode, UIS self.scrollNode.canCancelAllTouchesInViews = true switch mode { - case .password, .emailAddress, .updateEmailAddress: + case .password, .passwordRecovery, .emailAddress, .updateEmailAddress: self.monkeyNode = ManagedMonkeyAnimationNode() - case .emailConfirmation: + case .emailConfirmation, .passwordRecoveryEmail: if let path = getAppBundle().path(forResource: "TwoFactorSetupMail", ofType: "tgs") { let animatedStickerNode = AnimatedStickerNode() animatedStickerNode.setup(source: AnimatedStickerNodeLocalFileSource(path: path), width: 272, height: 272, playbackMode: .once, mode: .direct(cachePathPrefix: nil)) @@ -711,6 +1281,13 @@ private final class TwoFactorDataInputScreenNode: ViewControllerTracingNode, UIS animatedStickerNode.visibility = true self.animatedStickerNode = animatedStickerNode } + case .rememberPassword: + if let path = getAppBundle().path(forResource: "TwoFactorSetupRemember", ofType: "tgs") { + let animatedStickerNode = AnimatedStickerNode() + animatedStickerNode.setup(source: AnimatedStickerNodeLocalFileSource(path: path), width: 272, height: 272, playbackMode: .count(3), mode: .direct(cachePathPrefix: nil)) + animatedStickerNode.visibility = true + self.animatedStickerNode = animatedStickerNode + } } let title: String @@ -754,6 +1331,33 @@ private final class TwoFactorDataInputScreenNode: ViewControllerTracingNode, UIS toggleTextHidden?(node) }) ] + case .passwordRecovery: + title = presentationData.strings.TwoFactorSetup_PasswordRecovery_Title + text = NSAttributedString(string: presentationData.strings.TwoFactorSetup_PasswordRecovery_Text, font: Font.regular(16.0), textColor: presentationData.theme.list.itemPrimaryTextColor) + buttonText = presentationData.strings.TwoFactorSetup_PasswordRecovery_Action + skipActionText = presentationData.strings.TwoFactorSetup_PasswordRecovery_Skip + changeEmailActionText = "" + resendCodeActionText = "" + inputNodes = [ + TwoFactorDataInputTextNode(theme: presentationData.theme, mode: .password(confirmation: false), placeholder: presentationData.strings.TwoFactorSetup_PasswordRecovery_PlaceholderPassword, focusUpdated: { node, focused in + focusUpdated?(node, focused) + }, next: { node in + next?(node) + }, updated: { node in + updated?(node) + }, toggleTextHidden: { node in + toggleTextHidden?(node) + }), + TwoFactorDataInputTextNode(theme: presentationData.theme, mode: .password(confirmation: true), placeholder: presentationData.strings.TwoFactorSetup_PasswordRecovery_PlaceholderConfirmPassword, focusUpdated: { node, focused in + focusUpdated?(node, focused) + }, next: { node in + next?(node) + }, updated: { node in + updated?(node) + }, toggleTextHidden: { node in + toggleTextHidden?(node) + }) + ] case .emailAddress, .updateEmailAddress: title = presentationData.strings.TwoFactorSetup_Email_Title text = NSAttributedString(string: presentationData.strings.TwoFactorSetup_Email_Text, font: Font.regular(16.0), textColor: presentationData.theme.list.itemPrimaryTextColor) @@ -772,6 +1376,33 @@ private final class TwoFactorDataInputScreenNode: ViewControllerTracingNode, UIS toggleTextHidden?(node) }), ] + case let .passwordRecoveryEmail(emailPattern, _): + title = presentationData.strings.TwoFactorSetup_EmailVerification_Title + let (rawText, ranges) = presentationData.strings.TwoFactorSetup_EmailVerification_Text(emailPattern) + + let string = NSMutableAttributedString() + string.append(NSAttributedString(string: rawText, font: Font.regular(16.0), textColor: presentationData.theme.list.itemPrimaryTextColor)) + for (_, range) in ranges { + string.addAttribute(.font, value: Font.semibold(16.0), range: range) + } + + text = string + + buttonText = presentationData.strings.TwoFactorSetup_EmailVerification_Action + skipActionText = "" + changeEmailActionText = presentationData.strings.TwoStepAuth_RecoveryEmailResetNoAccess + resendCodeActionText = presentationData.strings.TwoFactorSetup_EmailVerification_ResendAction + inputNodes = [ + 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 + updated?(node) + }, toggleTextHidden: { node in + toggleTextHidden?(node) + }), + ] case let .emailConfirmation(_, emailPattern, _): title = presentationData.strings.TwoFactorSetup_EmailVerification_Title let (rawText, ranges) = presentationData.strings.TwoFactorSetup_EmailVerification_Text(emailPattern) @@ -819,6 +1450,24 @@ private final class TwoFactorDataInputScreenNode: ViewControllerTracingNode, UIS toggleTextHidden?(node) }), ] + case .rememberPassword: + title = presentationData.strings.TwoFactorRemember_Title + text = NSAttributedString(string: presentationData.strings.TwoFactorRemember_Text, font: Font.regular(16.0), textColor: presentationData.theme.list.itemPrimaryTextColor) + buttonText = presentationData.strings.TwoFactorRemember_CheckPassword + skipActionText = presentationData.strings.TwoFactorRemember_Forgot + changeEmailActionText = "" + resendCodeActionText = "" + inputNodes = [ + TwoFactorDataInputTextNode(theme: presentationData.theme, mode: .password(confirmation: false), placeholder: presentationData.strings.TwoFactorRemember_Placeholder, focusUpdated: { node, focused in + focusUpdated?(node, focused) + }, next: { node in + next?(node) + }, updated: { node in + updated?(node) + }, toggleTextHidden: { node in + toggleTextHidden?(node) + }) + ] } self.titleNode = ImmediateTextNode() @@ -848,7 +1497,6 @@ private final class TwoFactorDataInputScreenNode: ViewControllerTracingNode, UIS self.changeEmailActionTitleNode.attributedText = NSAttributedString(string: changeEmailActionText, font: Font.regular(16.0), textColor: self.presentationData.theme.list.itemAccentColor) self.changeEmailActionButtonNode = HighlightTrackingButtonNode() self.changeEmailActionButtonNode.isHidden = changeEmailActionText.isEmpty - self.changeEmailActionButtonNode.isHidden = changeEmailActionText.isEmpty self.resendCodeActionTitleNode = ImmediateTextNode() self.resendCodeActionTitleNode.isUserInteractionEnabled = false @@ -864,6 +1512,10 @@ private final class TwoFactorDataInputScreenNode: ViewControllerTracingNode, UIS super.init() + if case .rememberPassword = mode { + self.buttonNode.alpha = 0.0 + } + self.backgroundColor = self.presentationData.theme.list.plainBackgroundColor self.addSubnode(self.scrollNode) @@ -937,7 +1589,7 @@ private final class TwoFactorDataInputScreenNode: ViewControllerTracingNode, UIS guard let strongSelf = self else { return } - if let index = strongSelf.inputNodes.index(where: { $0 === node }) { + if let index = strongSelf.inputNodes.firstIndex(where: { $0 === node }) { if index == strongSelf.inputNodes.count - 1 { strongSelf.action() } else if strongSelf.buttonNode.isUserInteractionEnabled { @@ -951,16 +1603,14 @@ private final class TwoFactorDataInputScreenNode: ViewControllerTracingNode, UIS return } switch strongSelf.mode { - case .password: + case .password, .passwordRecovery: 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)) @@ -978,14 +1628,12 @@ private final class TwoFactorDataInputScreenNode: ViewControllerTracingNode, UIS } case .emailAddress: 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 + var trackingOffset = textSize.width / maxWidth trackingOffset = max(0.0, min(1.0, trackingOffset)) strongSelf.monkeyNode?.setState(.tracking(trackingOffset)) @@ -996,11 +1644,8 @@ private final class TwoFactorDataInputScreenNode: ViewControllerTracingNode, UIS break } } - focusUpdated = { [weak self] node, _ in + focusUpdated = { node, _ in DispatchQueue.main.async { - guard let strongSelf = self else { - return - } updateAnimations() } } @@ -1009,7 +1654,7 @@ private final class TwoFactorDataInputScreenNode: ViewControllerTracingNode, UIS return } switch strongSelf.mode { - case .emailAddress, .updateEmailAddress: + case .emailAddress, .updateEmailAddress, .passwordRecovery: let hasText = strongSelf.inputNodes.contains(where: { !$0.text.isEmpty }) strongSelf.buttonNode.isHidden = !hasText strongSelf.skipActionTitleNode.isHidden = hasText @@ -1026,6 +1671,18 @@ private final class TwoFactorDataInputScreenNode: ViewControllerTracingNode, UIS if let codeLength = codeLength, text.count == codeLength { action() } + case .passwordRecoveryEmail: + let text = strongSelf.inputNodes[0].text + let hasText = !text.isEmpty + strongSelf.buttonNode.isHidden = !hasText + strongSelf.changeEmailActionTitleNode.isHidden = hasText + strongSelf.changeEmailActionButtonNode.isHidden = hasText + strongSelf.resendCodeActionTitleNode.isHidden = hasText + strongSelf.resendCodeActionButtonNode.isHidden = hasText + + if text.count == 6 { + action() + } case .passwordHint: let hasText = strongSelf.inputNodes.contains(where: { !$0.text.isEmpty }) strongSelf.buttonNode.isHidden = !hasText @@ -1033,6 +1690,30 @@ private final class TwoFactorDataInputScreenNode: ViewControllerTracingNode, UIS strongSelf.skipActionButtonNode.isHidden = hasText case .password: break + case .rememberPassword: + let hasText = strongSelf.inputNodes.contains(where: { !$0.text.isEmpty }) + let transition = ContainedViewLayoutTransition.animated(duration: 0.2, curve: .easeInOut) + transition.updateAlpha(node: strongSelf.buttonNode, alpha: hasText ? 1.0 : 0.0) + transition.updateAlpha(node: strongSelf.skipActionTitleNode, alpha: hasText ? 0.0 : 1.0) + strongSelf.skipActionButtonNode.isHidden = hasText + + if strongSelf.textNode.attributedText?.string != strongSelf.presentationData.strings.TwoFactorRemember_Text { + if let snapshotView = strongSelf.textNode.view.snapshotContentTree() { + snapshotView.frame = strongSelf.textNode.view.frame + strongSelf.textNode.view.superview?.addSubview(snapshotView) + + snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak snapshotView] _ in + snapshotView?.removeFromSuperview() + }) + + strongSelf.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } + + strongSelf.textNode.attributedText = NSAttributedString(string: strongSelf.presentationData.strings.TwoFactorRemember_Text, font: Font.regular(16.0), textColor: presentationData.theme.list.itemPrimaryTextColor) + if let (layout, navigationHeight) = strongSelf.validLayout { + strongSelf.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: .immediate) + } + } } updateAnimations() } @@ -1041,7 +1722,7 @@ private final class TwoFactorDataInputScreenNode: ViewControllerTracingNode, UIS return } switch strongSelf.mode { - case .password: + case .password, .passwordRecovery, .rememberPassword: textHidden = !textHidden for node in strongSelf.inputNodes { node.updateTextHidden(textHidden) @@ -1054,6 +1735,10 @@ private final class TwoFactorDataInputScreenNode: ViewControllerTracingNode, UIS self.inputNodes.first.flatMap { updated?($0) } } + func focus() { + self.inputNodes.first?.isFocused = true + } + @objc private func skipActionPressed() { self.skipAction() } @@ -1085,8 +1770,42 @@ private final class TwoFactorDataInputScreenNode: ViewControllerTracingNode, UIS func scrollViewDidScroll(_ scrollView: UIScrollView) { } + func onAction(success: Bool) { + switch self.mode { + case .rememberPassword: + if !success { + self.skipActionTitleNode.isHidden = false + self.skipActionButtonNode.isHidden = false + + let transition = ContainedViewLayoutTransition.animated(duration: 0.2, curve: .easeInOut) + transition.updateAlpha(node: self.buttonNode, alpha: 0.0) + transition.updateAlpha(node: self.skipActionTitleNode, alpha: 1.0) + + if let snapshotView = self.textNode.view.snapshotContentTree() { + snapshotView.frame = self.textNode.view.frame + self.textNode.view.superview?.addSubview(snapshotView) + + snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak snapshotView] _ in + snapshotView?.removeFromSuperview() + }) + + self.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } + + self.textNode.attributedText = NSAttributedString(string: self.presentationData.strings.TwoFactorRemember_WrongPassword, font: Font.regular(16.0), textColor: self.presentationData.theme.list.itemDestructiveColor) + self.inputNodes.first?.isFailed = true + + if let (layout, navigationHeight) = self.validLayout { + self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: .immediate) + } + } + default: + break + } + } + func containerLayoutUpdated(layout: ContainerViewLayout, navigationHeight: CGFloat, transition: ContainedViewLayoutTransition) { - self.navigationHeight = navigationHeight + self.validLayout = (layout, navigationHeight) let contentAreaSize = layout.size let availableAreaSize = CGSize(width: layout.size.width, height: layout.size.height - layout.insets(options: [.input]).bottom) @@ -1113,7 +1832,7 @@ private final class TwoFactorDataInputScreenNode: ViewControllerTracingNode, UIS transition.updateFrame(node: self.scrollNode, frame: CGRect(origin: CGPoint(), size: contentAreaSize)) let iconSize: CGSize - if let animatedStickerNode = self.animatedStickerNode { + if let _ = self.animatedStickerNode { iconSize = CGSize(width: 136.0, height: 136.0) } else if let monkeyNode = self.monkeyNode { iconSize = monkeyNode.intrinsicSize @@ -1190,19 +1909,26 @@ private final class TwoFactorDataInputScreenNode: ViewControllerTracingNode, UIS let buttonWidth = contentAreaSize.width - buttonSideInset * 2.0 - let maxButtonY = min(areaHeight - buttonSpacing, layout.size.height - buttonBottomInset) - buttonHeight + let maxButtonY = min(areaHeight - buttonSpacing, layout.size.height - buttonBottomInset) - buttonHeight * 2.0 let buttonFrame = CGRect(origin: CGPoint(x: floor((contentAreaSize.width - buttonWidth) / 2.0), y: max(contentHeight + buttonSpacing, maxButtonY)), size: CGSize(width: buttonWidth, height: buttonHeight)) transition.updateFrame(node: self.buttonNode, frame: buttonFrame) - self.buttonNode.updateLayout(width: buttonFrame.width, transition: transition) - 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)) + let _ = self.buttonNode.updateLayout(width: buttonFrame.width, transition: transition) + + var skipButtonFrame = buttonFrame + if case .rememberPassword = self.mode { + } else if !self.buttonNode.isHidden { + skipButtonFrame.origin.y += skipButtonFrame.height + } + + transition.updateFrame(node: self.skipActionButtonNode, frame: skipButtonFrame) + transition.updateFrame(node: self.skipActionTitleNode, frame: CGRect(origin: CGPoint(x: skipButtonFrame.minX + floor((skipButtonFrame.width - skipActionSize.width) / 2.0), y: skipButtonFrame.minY + floor((skipButtonFrame.height - skipActionSize.height) / 2.0)), size: skipActionSize)) let changeEmailActionFrame: CGRect let changeEmailActionButtonFrame: CGRect let resendCodeActionFrame: CGRect let resendCodeActionButtonFrame: CGRect - if changeEmailActionSize.width + resendCodeActionSize.width > layout.size.width - 24.0 { + if changeEmailActionSize.width + resendCodeActionSize.width > layout.size.width - buttonFrame.minX * 2.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)) @@ -1222,7 +1948,7 @@ private final class TwoFactorDataInputScreenNode: ViewControllerTracingNode, UIS transition.animateView { self.scrollNode.view.contentInset = UIEdgeInsets(top: 0.0, left: 0.0, bottom: layout.insets(options: [.input]).bottom, right: 0.0) - self.scrollNode.view.contentSize = CGSize(width: contentAreaSize.width, height: max(availableAreaSize.height, buttonFrame.maxY + bottomInset)) + self.scrollNode.view.contentSize = CGSize(width: contentAreaSize.width, height: max(availableAreaSize.height, skipButtonFrame.maxY + bottomInset)) } } } diff --git a/submodules/PasswordSetupUI/Sources/TwoFactorAuthSplashScreen.swift b/submodules/PasswordSetupUI/Sources/TwoFactorAuthSplashScreen.swift index af6154e147..64074345a2 100644 --- a/submodules/PasswordSetupUI/Sources/TwoFactorAuthSplashScreen.swift +++ b/submodules/PasswordSetupUI/Sources/TwoFactorAuthSplashScreen.swift @@ -10,34 +10,51 @@ import AnimatedStickerNode import AccountContext import TelegramPresentationData import PresentationDataUtils +import TelegramCore public enum TwoFactorAuthSplashMode { case intro case done + case recoveryDone(recoveredAccountData: RecoveredAccountData?, syncContacts: Bool, isPasswordSet: Bool) + case remember } public final class TwoFactorAuthSplashScreen: ViewController { - private let context: AccountContext + private let sharedContext: SharedAccountContext + private let engine: SomeTelegramEngine private var presentationData: PresentationData private var mode: TwoFactorAuthSplashMode - public init(context: AccountContext, mode: TwoFactorAuthSplashMode) { - self.context = context + public init(sharedContext: SharedAccountContext, engine: SomeTelegramEngine, mode: TwoFactorAuthSplashMode, presentation: ViewControllerNavigationPresentation = .modalInLargeLayout) { + self.sharedContext = sharedContext + self.engine = engine self.mode = mode - self.presentationData = context.sharedContext.currentPresentationData.with { $0 } + self.presentationData = self.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) + let navigationBarTheme = NavigationBarTheme(buttonColor: defaultTheme.buttonColor, disabledButtonColor: defaultTheme.disabledButtonColor, primaryTextColor: defaultTheme.primaryTextColor, backgroundColor: .clear, enableBackgroundBlur: false, 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.navigationPresentation = presentation 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) + let hasBackButton: Bool + switch mode { + case .done, .remember: + hasBackButton = false + default: + hasBackButton = true + } + if hasBackButton { + self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Back, style: .plain, target: nil, action: nil) + } else { + self.navigationItem.leftBarButtonItem = UIBarButtonItem(customDisplayNode: ASDisplayNode()) + } } required init(coder aDecoder: NSCoder) { @@ -48,19 +65,31 @@ public final class TwoFactorAuthSplashScreen: ViewController { } override public func loadDisplayNode() { - self.displayNode = TwoFactorAuthSplashScreenNode(context: self.context, presentationData: self.presentationData, mode: self.mode, action: { [weak self] in + self.displayNode = TwoFactorAuthSplashScreenNode(sharedContext: self.sharedContext, presentationData: self.presentationData, mode: self.mode, action: { [weak self] in guard let strongSelf = self else { return } switch strongSelf.mode { case .intro: - strongSelf.push(TwoFactorDataInputScreen(context: strongSelf.context, mode: .password, stateUpdated: { _ in - })) - case .done: + strongSelf.push(TwoFactorDataInputScreen(sharedContext: strongSelf.sharedContext, engine: strongSelf.engine, mode: .password, stateUpdated: { _ in + }, presentation: strongSelf.navigationPresentation)) + case .done, .remember: guard let navigationController = strongSelf.navigationController as? NavigationController else { return } navigationController.filterController(strongSelf, animated: true) + case let .recoveryDone(recoveredAccountData, syncContacts, _): + guard let navigationController = strongSelf.navigationController as? NavigationController else { + return + } + switch strongSelf.engine { + case let .unauthorized(engine): + if let recoveredAccountData = recoveredAccountData { + let _ = loginWithRecoveredAccountData(accountManager: strongSelf.sharedContext.accountManager, account: engine.account, recoveredAccountData: recoveredAccountData, syncContacts: syncContacts).start() + } + case .authorized: + navigationController.filterController(strongSelf, animated: true) + } } }) @@ -70,7 +99,7 @@ public final class TwoFactorAuthSplashScreen: ViewController { override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { super.containerLayoutUpdated(layout, transition: transition) - (self.displayNode as! TwoFactorAuthSplashScreenNode).containerLayoutUpdated(layout: layout, navigationHeight: self.navigationHeight, transition: transition) + (self.displayNode as! TwoFactorAuthSplashScreenNode).containerLayoutUpdated(layout: layout, navigationHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition) } } @@ -82,7 +111,8 @@ private final class TwoFactorAuthSplashScreenNode: ViewControllerTracingNode { private var animationOffset: CGPoint = CGPoint() private let animationNode: AnimatedStickerNode private let titleNode: ImmediateTextNode - private let textNode: ImmediateTextNode + private let textNodes: [ImmediateTextNode] + private let textArrowNodes: [ASImageNode] let buttonNode: SolidRoundedButtonNode var inProgress: Bool = false { @@ -92,14 +122,14 @@ private final class TwoFactorAuthSplashScreenNode: ViewControllerTracingNode { } } - init(context: AccountContext, presentationData: PresentationData, mode: TwoFactorAuthSplashMode, action: @escaping () -> Void) { + init(sharedContext: SharedAccountContext, presentationData: PresentationData, mode: TwoFactorAuthSplashMode, action: @escaping () -> Void) { self.presentationData = presentationData self.mode = mode self.animationNode = AnimatedStickerNode() let title: String - let text: NSAttributedString + let texts: [NSAttributedString] let buttonText: String let textFont = Font.regular(16.0) @@ -108,7 +138,7 @@ private final class TwoFactorAuthSplashScreenNode: ViewControllerTracingNode { switch mode { case .intro: title = self.presentationData.strings.TwoFactorSetup_Intro_Title - text = NSAttributedString(string: self.presentationData.strings.TwoFactorSetup_Intro_Text, font: textFont, textColor: textColor) + texts = [NSAttributedString(string: self.presentationData.strings.TwoFactorSetup_Intro_Text, font: textFont, textColor: textColor)] buttonText = self.presentationData.strings.TwoFactorSetup_Intro_Action if let path = getAppBundle().path(forResource: "TwoFactorSetupIntro", ofType: "tgs") { @@ -118,7 +148,7 @@ private final class TwoFactorAuthSplashScreenNode: ViewControllerTracingNode { } case .done: title = self.presentationData.strings.TwoFactorSetup_Done_Title - text = NSAttributedString(string: self.presentationData.strings.TwoFactorSetup_Done_Text, font: textFont, textColor: textColor) + texts = [NSAttributedString(string: self.presentationData.strings.TwoFactorSetup_Done_Text, font: textFont, textColor: textColor)] buttonText = self.presentationData.strings.TwoFactorSetup_Done_Action if let path = getAppBundle().path(forResource: "TwoFactorSetupDone", ofType: "tgs") { @@ -126,6 +156,41 @@ private final class TwoFactorAuthSplashScreenNode: ViewControllerTracingNode { self.animationSize = CGSize(width: 124.0, height: 124.0) self.animationNode.visibility = true } + case let .recoveryDone(_, _, isPasswordSet): + title = isPasswordSet ? self.presentationData.strings.TwoFactorSetup_ResetDone_Title : self.presentationData.strings.TwoFactorSetup_ResetDone_TitleNoPassword + + let rawText = isPasswordSet ? self.presentationData.strings.TwoFactorSetup_ResetDone_Text : self.presentationData.strings.TwoFactorSetup_ResetDone_TextNoPassword + + var splitTexts: [String] = [""] + var index = rawText.startIndex + while index != rawText.endIndex { + let c = rawText[index] + if c == ">" { + splitTexts.append("") + } else { + splitTexts[splitTexts.count - 1].append(c) + } + index = rawText.index(after: index) + } + + texts = splitTexts.map { NSAttributedString(string: $0, font: textFont, textColor: textColor) } + buttonText = self.presentationData.strings.TwoFactorSetup_ResetDone_Action + + if let path = getAppBundle().path(forResource: isPasswordSet ? "TwoFactorSetupDone" : "TwoFactorRemovePasswordDone", ofType: "tgs") { + self.animationNode.setup(source: AnimatedStickerNodeLocalFileSource(path: path), width: 248, height: 248, playbackMode: isPasswordSet ? .loop : .once, mode: .direct(cachePathPrefix: nil)) + self.animationSize = CGSize(width: 124.0, height: 124.0) + self.animationNode.visibility = true + } + case .remember: + title = self.presentationData.strings.TwoFactorRemember_Done_Title + texts = [NSAttributedString(string: self.presentationData.strings.TwoFactorRemember_Done_Text, font: textFont, textColor: textColor)] + buttonText = self.presentationData.strings.TwoFactorRemember_Done_Action + + if let path = getAppBundle().path(forResource: "TwoFactorSetupRememberSuccess", ofType: "tgs") { + self.animationNode.setup(source: AnimatedStickerNodeLocalFileSource(path: path), width: 248, height: 248, mode: .direct(cachePathPrefix: nil)) + self.animationSize = CGSize(width: 124.0, height: 124.0) + self.animationNode.visibility = true + } } self.titleNode = ImmediateTextNode() @@ -134,12 +199,27 @@ private final class TwoFactorAuthSplashScreenNode: ViewControllerTracingNode { self.titleNode.maximumNumberOfLines = 0 self.titleNode.textAlignment = .center - self.textNode = ImmediateTextNode() - self.textNode.displaysAsynchronously = false - self.textNode.attributedText = text - self.textNode.maximumNumberOfLines = 0 - self.textNode.lineSpacing = 0.1 - self.textNode.textAlignment = .center + self.textNodes = texts.map { text in + let textNode = ImmediateTextNode() + + textNode.displaysAsynchronously = false + textNode.attributedText = text + textNode.maximumNumberOfLines = 0 + textNode.lineSpacing = 0.1 + textNode.textAlignment = .center + + return textNode + } + + let arrowImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Search/DownButton"), color: presentationData.theme.list.itemPrimaryTextColor) + self.textArrowNodes = (0 ..< self.textNodes.count - 1).map { _ in + let iconNode = ASImageNode() + + iconNode.image = arrowImage + iconNode.alpha = 0.34 + + return iconNode + } 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 @@ -150,7 +230,8 @@ private final class TwoFactorAuthSplashScreenNode: ViewControllerTracingNode { self.addSubnode(self.animationNode) self.addSubnode(self.titleNode) - self.addSubnode(self.textNode) + self.textNodes.forEach(self.addSubnode) + self.textArrowNodes.forEach(self.addSubnode) self.addSubnode(self.buttonNode) self.buttonNode.pressed = { @@ -179,9 +260,17 @@ private final class TwoFactorAuthSplashScreenNode: ViewControllerTracingNode { } let titleSize = self.titleNode.updateLayout(CGSize(width: layout.size.width - sideInset * 2.0, height: layout.size.height)) - let textSize = self.textNode.updateLayout(CGSize(width: layout.size.width - sideInset * 2.0, height: layout.size.height)) + let textSizes = self.textNodes.map { + $0.updateLayout(CGSize(width: layout.size.width - sideInset * 2.0, height: layout.size.height)) + } + var combinedTextHeight: CGFloat = 0.0 + let textSpacing: CGFloat = 32.0 + for textSize in textSizes { + combinedTextHeight += textSize.height + } + combinedTextHeight += CGFloat(max(0, textSizes.count - 1)) * textSpacing - let contentHeight = iconSize.height + iconSpacing + titleSize.height + titleSpacing + textSize.height + let contentHeight = iconSize.height + iconSpacing + titleSize.height + titleSpacing + combinedTextHeight var contentVerticalOrigin = floor((layout.size.height - contentHeight - iconSize.height / 2.0) / 2.0) let minimalBottomInset: CGFloat = 60.0 @@ -191,9 +280,9 @@ private final class TwoFactorAuthSplashScreenNode: ViewControllerTracingNode { 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) + let _ = self.buttonNode.updateLayout(width: buttonFrame.width, transition: transition) - var maxContentVerticalOrigin = buttonFrame.minY - 12.0 - contentHeight + let maxContentVerticalOrigin = buttonFrame.minY - 12.0 - contentHeight contentVerticalOrigin = min(contentVerticalOrigin, maxContentVerticalOrigin) @@ -202,7 +291,20 @@ private final class TwoFactorAuthSplashScreenNode: ViewControllerTracingNode { transition.updateFrameAdditive(node: self.animationNode, frame: iconFrame) let titleFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - titleSize.width) / 2.0), y: iconFrame.maxY + iconSpacing), size: titleSize) transition.updateFrameAdditive(node: self.titleNode, frame: titleFrame) - let textFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - textSize.width) / 2.0), y: titleFrame.maxY + titleSpacing), size: textSize) - transition.updateFrameAdditive(node: self.textNode, frame: textFrame) + + var nextTextOrigin: CGFloat = titleFrame.maxY + titleSpacing + for i in 0 ..< self.textNodes.count { + let textFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - textSizes[i].width) / 2.0), y: nextTextOrigin), size: textSizes[i]) + transition.updateFrameAdditive(node: self.textNodes[i], frame: textFrame) + + if i != 0 { + if let image = self.textArrowNodes[i - 1].image { + let scaledImageSize = CGSize(width: floor(image.size.width * 0.7), height: floor(image.size.height * 0.7)) + self.textArrowNodes[i - 1].frame = CGRect(origin: CGPoint(x: floor((layout.size.width - scaledImageSize.width) / 2.0), y: nextTextOrigin - textSpacing + floor((textSpacing - scaledImageSize.height) / 2.0)), size: scaledImageSize) + } + } + + nextTextOrigin = textFrame.maxY + textSpacing + } } } diff --git a/submodules/PeerAvatarGalleryUI/Sources/AvatarGalleryController.swift b/submodules/PeerAvatarGalleryUI/Sources/AvatarGalleryController.swift index 0a278cd06e..89171cffca 100644 --- a/submodules/PeerAvatarGalleryUI/Sources/AvatarGalleryController.swift +++ b/submodules/PeerAvatarGalleryUI/Sources/AvatarGalleryController.swift @@ -22,10 +22,57 @@ public enum AvatarGalleryEntryId: Hashable { case resource(String) } +public func peerInfoProfilePhotos(context: AccountContext, peerId: PeerId) -> Signal { + return context.account.postbox.combinedView(keys: [.basicPeer(peerId)]) + |> mapToSignal { view -> Signal<[AvatarGalleryEntry]?, NoError> in + guard let peer = (view.views[.basicPeer(peerId)] as? BasicPeerView)?.peer else { + return .single(nil) + } + return initialAvatarGalleryEntries(account: context.account, peer: peer) + } + |> distinctUntilChanged + |> mapToSignal { entries -> Signal<(Bool, [AvatarGalleryEntry])?, NoError> in + if let entries = entries { + if let firstEntry = entries.first { + return context.account.postbox.loadedPeerWithId(peerId) + |> mapToSignal { peer -> Signal<(Bool, [AvatarGalleryEntry])?, NoError>in + return fetchedAvatarGalleryEntries(engine: context.engine, account: context.account, peer: peer, firstEntry: firstEntry) + |> map(Optional.init) + } + } else { + return .single((true, [])) + } + } else { + return context.engine.peers.fetchAndUpdateCachedPeerData(peerId: peerId) + |> map { _ -> (Bool, [AvatarGalleryEntry])? in + return nil + } + } + } + |> map { items -> Any in + if let items = items { + return items + } else { + return peerInfoProfilePhotos(context: context, peerId: peerId) + } + } +} + +public func peerInfoProfilePhotosWithCache(context: AccountContext, peerId: PeerId) -> Signal<(Bool, [AvatarGalleryEntry]), NoError> { + return context.peerChannelMemberCategoriesContextsManager.profilePhotos(postbox: context.account.postbox, network: context.account.network, peerId: peerId, fetch: peerInfoProfilePhotos(context: context, peerId: peerId)) + |> map { items -> (Bool, [AvatarGalleryEntry]) in + return items as? (Bool, [AvatarGalleryEntry]) ?? (true, []) + } +} + public enum AvatarGalleryEntry: Equatable { case topImage([ImageRepresentationWithReference], [VideoRepresentationWithReference], Peer?, GalleryItemIndexData?, Data?, String?) case image(MediaId, TelegramMediaImageReference?, [ImageRepresentationWithReference], [VideoRepresentationWithReference], Peer?, Int32?, GalleryItemIndexData?, MessageId?, Data?, String?) + public init(representation: TelegramMediaImageRepresentation, peer: Peer) { + self = .topImage([ImageRepresentationWithReference(representation: representation, reference: MediaResourceReference.standalone(resource: representation.resource))], [], peer, nil, nil, nil) + } + public var id: AvatarGalleryEntryId { switch self { case let .topImage(representations, _, _, _, _, _): @@ -115,22 +162,22 @@ public final class AvatarGalleryControllerPresentationArguments { } public func normalizeEntries(_ entries: [AvatarGalleryEntry]) -> [AvatarGalleryEntry] { - var updatedEntries: [AvatarGalleryEntry] = [] - let count: Int32 = Int32(entries.count) - var index: Int32 = 0 - for entry in entries { - let indexData = GalleryItemIndexData(position: index, totalCount: count) - if case let .topImage(representations, videoRepresentations, peer, _, immediateThumbnailData, category) = entry { - updatedEntries.append(.topImage(representations, videoRepresentations, peer, indexData, immediateThumbnailData, category)) - } else if case let .image(id, reference, representations, videoRepresentations, peer, date, _, messageId, immediateThumbnailData, category) = entry { - updatedEntries.append(.image(id, reference, representations, videoRepresentations, peer, date, indexData, messageId, immediateThumbnailData, category)) - } - index += 1 + var updatedEntries: [AvatarGalleryEntry] = [] + let count: Int32 = Int32(entries.count) + var index: Int32 = 0 + for entry in entries { + let indexData = GalleryItemIndexData(position: index, totalCount: count) + if case let .topImage(representations, videoRepresentations, peer, _, immediateThumbnailData, category) = entry { + updatedEntries.append(.topImage(representations, videoRepresentations, peer, indexData, immediateThumbnailData, category)) + } else if case let .image(id, reference, representations, videoRepresentations, peer, date, _, messageId, immediateThumbnailData, category) = entry { + updatedEntries.append(.image(id, reference, representations, videoRepresentations, peer, date, indexData, messageId, immediateThumbnailData, category)) } - return updatedEntries + index += 1 } + return updatedEntries +} -public func initialAvatarGalleryEntries(account: Account, peer: Peer) -> Signal<[AvatarGalleryEntry], NoError> { +public func initialAvatarGalleryEntries(account: Account, peer: Peer) -> Signal<[AvatarGalleryEntry]?, NoError> { 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)) }), [], peer, nil, nil, nil)) @@ -155,7 +202,7 @@ public func initialAvatarGalleryEntries(account: Account, peer: Peer) -> Signal< } return [.image(photo.imageId, photo.reference, representations, photo.videoRepresentations.map({ VideoRepresentationWithReference(representation: $0, reference: MediaResourceReference.avatarList(peer: peerReference, resource: $0.resource)) }), peer, nil, nil, nil, photo.immediateThumbnailData, nil)] } else { - return [] + return cachedData != nil ? [] : nil } } } else { @@ -163,12 +210,15 @@ public func initialAvatarGalleryEntries(account: Account, peer: Peer) -> Signal< } } -public func fetchedAvatarGalleryEntries(account: Account, peer: Peer) -> Signal<[AvatarGalleryEntry], NoError> { +public func fetchedAvatarGalleryEntries(engine: TelegramEngine, account: Account, peer: Peer) -> Signal<[AvatarGalleryEntry], NoError> { return initialAvatarGalleryEntries(account: account, peer: peer) + |> map { entries -> [AvatarGalleryEntry] in + return entries ?? [] + } |> mapToSignal { initialEntries in return .single(initialEntries) |> then( - requestPeerPhotos(postbox: account.postbox, network: account.network, peerId: peer.id) + engine.peers.requestPeerPhotos(peerId: peer.id) |> map { photos -> [AvatarGalleryEntry] in var result: [AvatarGalleryEntry] = [] if photos.isEmpty { @@ -219,11 +269,11 @@ public func fetchedAvatarGalleryEntries(account: Account, peer: Peer) -> Signal< } } -public func fetchedAvatarGalleryEntries(account: Account, peer: Peer, firstEntry: AvatarGalleryEntry) -> Signal<(Bool, [AvatarGalleryEntry]), NoError> { +public func fetchedAvatarGalleryEntries(engine: TelegramEngine, account: Account, peer: Peer, firstEntry: AvatarGalleryEntry) -> Signal<(Bool, [AvatarGalleryEntry]), NoError> { let initialEntries = [firstEntry] return Signal<(Bool, [AvatarGalleryEntry]), NoError>.single((false, initialEntries)) |> then( - requestPeerPhotos(postbox: account.postbox, network: account.network, peerId: peer.id) + engine.peers.requestPeerPhotos(peerId: peer.id) |> map { photos -> (Bool, [AvatarGalleryEntry]) in var result: [AvatarGalleryEntry] = [] let initialEntries = [firstEntry] @@ -353,10 +403,15 @@ public class AvatarGalleryController: ViewController, StandalonePresentableContr if let remoteEntries = remoteEntries { remoteEntriesSignal = remoteEntries.get() } else { - remoteEntriesSignal = fetchedAvatarGalleryEntries(account: context.account, peer: peer) + remoteEntriesSignal = fetchedAvatarGalleryEntries(engine: context.engine, account: context.account, peer: peer) } - let entriesSignal: Signal<[AvatarGalleryEntry], NoError> = skipInitial ? remoteEntriesSignal : (initialAvatarGalleryEntries(account: context.account, peer: peer) |> then(remoteEntriesSignal)) + let initialSignal = initialAvatarGalleryEntries(account: context.account, peer: peer) + |> map { entries -> [AvatarGalleryEntry] in + return entries ?? [] + } + + let entriesSignal: Signal<[AvatarGalleryEntry], NoError> = skipInitial ? remoteEntriesSignal : (initialSignal |> then(remoteEntriesSignal)) let presentationData = self.presentationData @@ -634,7 +689,7 @@ public class AvatarGalleryController: ViewController, StandalonePresentableContr super.containerLayoutUpdated(layout, transition: transition) self.galleryNode.frame = CGRect(origin: CGPoint(), size: layout.size) - self.galleryNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationHeight, transition: transition) + self.galleryNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition) if !self.adjustedForInitialPreviewingLayout && self.isPresentedInPreviewingContext() { self.adjustedForInitialPreviewingLayout = true @@ -693,7 +748,7 @@ public class AvatarGalleryController: ViewController, StandalonePresentableContr case let .image(_, reference, _, _, _, _, _, _, _, _): if self.peer.id == self.context.account.peerId, let peerReference = PeerReference(self.peer) { if let reference = reference { - let _ = (updatePeerPhotoExisting(network: self.context.account.network, reference: reference) + let _ = (self.context.engine.accountData.updatePeerPhotoExisting(reference: reference) |> deliverOnMainQueue).start(next: { [weak self] photo in if let strongSelf = self, let photo = photo, let firstEntry = strongSelf.entries.first, case let .image(image) = firstEntry { let updatedEntry = AvatarGalleryEntry.image(photo.imageId, photo.reference, photo.representations.map({ ImageRepresentationWithReference(representation: $0, reference: MediaResourceReference.avatar(peer: peerReference, resource: $0.resource)) }), photo.videoRepresentations.map({ VideoRepresentationWithReference(representation: $0, reference: MediaResourceReference.avatarList(peer: peerReference, resource: $0.resource)) }), strongSelf.peer, image.5, image.6, image.7, photo.immediateThumbnailData, image.9) @@ -805,7 +860,7 @@ public class AvatarGalleryController: ViewController, StandalonePresentableContr if self.peer.id == self.context.account.peerId { } else { if entry == self.entries.first { - let _ = updatePeerPhoto(postbox: self.context.account.postbox, network: self.context.account.network, stateManager: self.context.account.stateManager, accountPeerId: self.context.account.peerId, peerId: self.peer.id, photo: nil, mapResourceToAvatarSizes: { _, _ in .single([:]) }).start() + let _ = self.context.engine.peers.updatePeerPhoto(peerId: self.peer.id, photo: nil, mapResourceToAvatarSizes: { _, _ in .single([:]) }).start() dismiss = true } else { if let index = self.entries.firstIndex(of: entry) { @@ -817,7 +872,7 @@ public class AvatarGalleryController: ViewController, StandalonePresentableContr 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() + let _ = self.context.engine.accountData.removeAccountPhoto(reference: reference).start() } if entry == self.entries.first { dismiss = true @@ -830,11 +885,11 @@ public class AvatarGalleryController: ViewController, StandalonePresentableContr } } else { if let messageId = messageId { - let _ = deleteMessagesInteractively(account: self.context.account, messageIds: [messageId], type: .forEveryone).start() + let _ = self.context.engine.messages.deleteMessagesInteractively(messageIds: [messageId], type: .forEveryone).start() } if entry == self.entries.first { - let _ = updatePeerPhoto(postbox: self.context.account.postbox, network: self.context.account.network, stateManager: self.context.account.stateManager, accountPeerId: self.context.account.peerId, peerId: self.peer.id, photo: nil, mapResourceToAvatarSizes: { _, _ in .single([:]) }).start() + let _ = self.context.engine.peers.updatePeerPhoto(peerId: self.peer.id, photo: nil, mapResourceToAvatarSizes: { _, _ in .single([:]) }).start() dismiss = true } else { if let index = self.entries.firstIndex(of: entry) { diff --git a/submodules/PeerAvatarGalleryUI/Sources/AvatarGalleryItemFooterContentNode.swift b/submodules/PeerAvatarGalleryUI/Sources/AvatarGalleryItemFooterContentNode.swift index 0b63aef373..55dec58bb0 100644 --- a/submodules/PeerAvatarGalleryUI/Sources/AvatarGalleryItemFooterContentNode.swift +++ b/submodules/PeerAvatarGalleryUI/Sources/AvatarGalleryItemFooterContentNode.swift @@ -110,7 +110,7 @@ final class AvatarGalleryItemFooterContentNode: GalleryFooterContentNode { case let .image(_, _, _, videoRepresentations, peer, date, _, _, _, _): nameText = peer?.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder) ?? "" if let date = date { - dateText = humanReadableStringForTimestamp(strings: self.strings, dateTimeFormat: self.dateTimeFormat, timestamp: date) + dateText = humanReadableStringForTimestamp(strings: self.strings, dateTimeFormat: self.dateTimeFormat, timestamp: date).0 } if (!videoRepresentations.isEmpty) { diff --git a/submodules/PeerAvatarGalleryUI/Sources/PeerAvatarImageGalleryItem.swift b/submodules/PeerAvatarGalleryUI/Sources/PeerAvatarImageGalleryItem.swift index 2345f79d4d..7a9a47e931 100644 --- a/submodules/PeerAvatarGalleryUI/Sources/PeerAvatarImageGalleryItem.swift +++ b/submodules/PeerAvatarGalleryUI/Sources/PeerAvatarImageGalleryItem.swift @@ -257,7 +257,7 @@ final class PeerAvatarImageGalleryItemNode: ZoomableContentGalleryItemNode { id = image.0.id category = image.9 } else { - id = Int64(entry.peer?.id.id ?? 0) + id = Int64(entry.peer?.id.id._internalGetInt32Value() ?? 0) if let resource = entry.videoRepresentations.first?.representation.resource as? CloudPhotoSizeMediaResource { id = id &+ resource.photoId } diff --git a/submodules/PeerInfoAvatarListNode/BUILD b/submodules/PeerInfoAvatarListNode/BUILD new file mode 100644 index 0000000000..0eaab52961 --- /dev/null +++ b/submodules/PeerInfoAvatarListNode/BUILD @@ -0,0 +1,30 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "PeerInfoAvatarListNode", + module_name = "PeerInfoAvatarListNode", + srcs = glob([ + "Sources/**/*.swift", + ]), + deps = [ + "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", + "//submodules/AsyncDisplayKit:AsyncDisplayKit", + "//submodules/Display:Display", + "//submodules/Postbox:Postbox", + "//submodules/TelegramCore:TelegramCore", + "//submodules/SyncCore:SyncCore", + "//submodules/TelegramPresentationData:TelegramPresentationData", + "//submodules/AvatarNode:AvatarNode", + "//submodules/PhotoResources:PhotoResources", + "//submodules/RadialStatusNode:RadialStatusNode", + "//submodules/PeerAvatarGalleryUI:PeerAvatarGalleryUI", + "//submodules/TelegramStringFormatting:TelegramStringFormatting", + "//submodules/TelegramUniversalVideoContent:TelegramUniversalVideoContent", + "//submodules/GalleryUI:GalleryUI", + "//submodules/MediaPlayer:UniversalMediaPlayer", + "//submodules/AccountContext:AccountContext", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/PeerInfoAvatarListNode/Sources/PeerInfoAvatarListNode.swift b/submodules/PeerInfoAvatarListNode/Sources/PeerInfoAvatarListNode.swift new file mode 100644 index 0000000000..7677c2afd9 --- /dev/null +++ b/submodules/PeerInfoAvatarListNode/Sources/PeerInfoAvatarListNode.swift @@ -0,0 +1,1211 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display +import SwiftSignalKit +import SyncCore +import Postbox +import TelegramCore +import AccountContext +import TelegramPresentationData +import PhotoResources +import PeerAvatarGalleryUI +import TelegramStringFormatting +import TelegramUniversalVideoContent +import GalleryUI +import UniversalMediaPlayer +import RadialStatusNode +import TelegramUIPreferences + +private class PeerInfoAvatarListLoadingStripNode: ASImageNode { + private var currentInHierarchy = false + + let imageNode = ASImageNode() + + override init() { + super.init() + + self.addSubnode(self.imageNode) + } + + override public var isHidden: Bool { + didSet { + self.updateAnimation() + } + } + private var isAnimating = false { + didSet { + if self.isAnimating != oldValue { + if self.isAnimating { + let basicAnimation = CABasicAnimation(keyPath: "opacity") + basicAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut) + basicAnimation.duration = 0.45 + basicAnimation.fromValue = 0.1 + basicAnimation.toValue = 0.75 + basicAnimation.repeatCount = Float.infinity + basicAnimation.autoreverses = true + + self.imageNode.layer.add(basicAnimation, forKey: "loading") + } else { + self.imageNode.layer.removeAnimation(forKey: "loading") + } + } + } + } + + private func updateAnimation() { + self.isAnimating = !self.isHidden && self.currentInHierarchy + } + + override public func willEnterHierarchy() { + super.willEnterHierarchy() + + self.currentInHierarchy = true + self.updateAnimation() + } + + override public func didExitHierarchy() { + super.didExitHierarchy() + + self.currentInHierarchy = false + self.updateAnimation() + } + + override func layout() { + super.layout() + + self.imageNode.frame = self.bounds + } +} + +private struct CustomListItemResourceId: MediaResourceId { + public var uniqueId: String { + return "customNode" + } + + public var hashValue: Int { + return 0 + } + + public func isEqual(to: MediaResourceId) -> Bool { + if to is CustomListItemResourceId { + return true + } else { + return false + } + } +} + +public enum PeerInfoAvatarListItem: Equatable { + case custom(ASDisplayNode) + case topImage([ImageRepresentationWithReference], [VideoRepresentationWithReference], Data?) + case image(TelegramMediaImageReference?, [ImageRepresentationWithReference], [VideoRepresentationWithReference], Data?) + + var id: WrappedMediaResourceId { + switch self { + case .custom: + return WrappedMediaResourceId(CustomListItemResourceId()) + 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) + } + } + + var representations: [ImageRepresentationWithReference] { + switch self { + case .custom: + return [] + case let .topImage(representations, _, _): + return representations + case let .image(_, representations, _, _): + return representations + } + } + + + var videoRepresentations: [VideoRepresentationWithReference] { + switch self { + case .custom: + return [] + case let .topImage(_, videoRepresentations, _): + return videoRepresentations + case let .image(_, _, videoRepresentations, _): + return videoRepresentations + } + } + + public init?(entry: AvatarGalleryEntry) { + switch entry { + case let .topImage(representations, videoRepresentations, _, _, immediateThumbnailData, _): + self = .topImage(representations, videoRepresentations, immediateThumbnailData) + case let .image(_, reference, representations, videoRepresentations, _, _, _, _, immediateThumbnailData, _): + if representations.isEmpty { + return nil + } + self = .image(reference, representations, videoRepresentations, immediateThumbnailData) + } + } +} + +public final class PeerInfoAvatarListItemNode: ASDisplayNode { + private let context: AccountContext + private let peer: Peer + public let imageNode: TransformImageNode + private var videoNode: UniversalVideoNode? + private var videoContent: NativeVideoContent? + private var videoStartTimestamp: Double? + private let playbackStartDisposable = MetaDisposable() + private let statusDisposable = MetaDisposable() + private let preloadDisposable = MetaDisposable() + private let statusNode: RadialStatusNode + + private var playerStatus: MediaPlayerStatus? + private var isLoading = Promise(false) + private var loadingProgress = Promise(nil) + private var progress: Signal? + private var loadingProgressDisposable = MetaDisposable() + private var hasProgress = false + + public let isReady = Promise() + private var didSetReady: Bool = false + + public var item: PeerInfoAvatarListItem? + + private var statusPromise = Promise<(MediaPlayerStatus?, Double?)?>() + var mediaStatus: Signal<(MediaPlayerStatus?, Double?)?, NoError> { + get { + return self.statusPromise.get() + } + } + + var delayCentralityLose = false + var isCentral: Bool? = nil { + didSet { + guard self.isCentral != oldValue, let isCentral = self.isCentral else { + return + } + if isCentral { + self.setupVideoPlayback() + self.preloadDisposable.set(nil) + } else { + if let videoNode = self.videoNode { + self.playbackStartDisposable.set(nil) + self.statusPromise.set(.single(nil)) + self.videoNode = nil + if self.delayCentralityLose { + Queue.mainQueue().after(0.5) { + videoNode.removeFromSupernode() + } + } else { + videoNode.removeFromSupernode() + } + } + if let videoContent = self.videoContent { + let duration: Double = (self.videoStartTimestamp ?? 0.0) + 4.0 + self.preloadDisposable.set(preloadVideoResource(postbox: self.context.account.postbox, resourceReference: videoContent.fileReference.resourceReference(videoContent.fileReference.media.resource), duration: duration).start()) + } + } + } + } + + init(context: AccountContext, peer: Peer) { + self.context = context + self.peer = peer + self.imageNode = TransformImageNode() + + self.statusNode = RadialStatusNode(backgroundNodeColor: UIColor(rgb: 0x000000, alpha: 0.3)) + self.statusNode.isUserInteractionEnabled = false + + super.init() + + self.clipsToBounds = true + + self.imageNode.contentAnimations = [.firstUpdate, .subsequentUpdates] + self.addSubnode(self.imageNode) + self.addSubnode(self.statusNode) + + self.loadingProgressDisposable.set((combineLatest(self.isLoading.get() + |> mapToSignal { value -> Signal in + if value { + return .single(value) |> delay(0.5, queue: Queue.mainQueue()) + } else { + return .single(value) + } + } |> distinctUntilChanged, self.loadingProgress.get() |> distinctUntilChanged)).start(next: { [weak self] isLoading, progress in + guard let strongSelf = self else { + return + } + if isLoading, let progress = progress { + strongSelf.hasProgress = true + strongSelf.statusNode.transitionToState(.progress(color: .white, lineWidth: nil, value: CGFloat(max(0.027, progress)), cancelEnabled: false, animateRotation: true), completion: {}) + } else if strongSelf.hasProgress { + strongSelf.hasProgress = false + strongSelf.statusNode.transitionToState(.progress(color: .white, lineWidth: nil, value: 1.0, cancelEnabled: false, animateRotation: true), completion: { [weak self] in + guard let strongSelf = self else { + return + } + if !strongSelf.hasProgress { + Queue.mainQueue().after(0.3) { + strongSelf.statusNode.transitionToState(.none, completion: {}) + } + } + }) + } + })) + } + + deinit { + self.statusDisposable.dispose() + self.playbackStartDisposable.dispose() + self.preloadDisposable.dispose() + } + + private func updateStatus() { + guard let videoContent = self.videoContent else { + return + } + + var bufferingProgress: Float? + if isMediaStreamable(resource: videoContent.fileReference.media.resource) { + if let playerStatus = self.playerStatus { + if case let .buffering(_, _, progress, _) = playerStatus.status { + bufferingProgress = progress + } else if case .playing = playerStatus.status { + bufferingProgress = nil + } + } else { + bufferingProgress = nil + } + } + + if self.progress == nil { + self.loadingProgress.set(.single(bufferingProgress)) + self.isLoading.set(.single(bufferingProgress != nil)) + } + } + + public func updateTransitionFraction(_ fraction: CGFloat, transition: ContainedViewLayoutTransition) { + if let videoNode = self.videoNode { + if case .immediate = transition, fraction == 1.0 { + return + } + transition.updateAlpha(node: videoNode, alpha: 1.0 - fraction) + } + } + + private func setupVideoPlayback() { + guard let videoContent = self.videoContent, let isCentral = self.isCentral, isCentral, self.videoNode == nil else { + return + } + + let mediaManager = self.context.sharedContext.mediaManager + let videoNode = UniversalVideoNode(postbox: self.context.account.postbox, audioSession: mediaManager.audioSession, manager: mediaManager.universalVideoManager, decoration: GalleryVideoDecoration(), content: videoContent, priority: .secondaryOverlay) + videoNode.isUserInteractionEnabled = false + videoNode.canAttachContent = true + videoNode.isHidden = true + + if let _ = self.videoStartTimestamp { + self.playbackStartDisposable.set((videoNode.status + |> map { status -> Bool in + if let status = status, case .playing = status.status { + return true + } else { + return false + } + } + |> filter { playing in + return playing + } + |> take(1) + |> deliverOnMainQueue).start(completed: { [weak self] in + if let strongSelf = self { + Queue.mainQueue().after(0.1) { + strongSelf.videoNode?.isHidden = false + } + } + })) + } else { + self.playbackStartDisposable.set(nil) + videoNode.isHidden = false + } + videoNode.play() + + self.videoNode = videoNode + let videoStartTimestamp = self.videoStartTimestamp + self.statusPromise.set(videoNode.status |> map { ($0, videoStartTimestamp) }) + + self.statusDisposable.set((self.mediaStatus + |> deliverOnMainQueue).start(next: { [weak self] mediaStatus in + if let strongSelf = self { + if let mediaStatusAndStartTimestamp = mediaStatus { + strongSelf.playerStatus = mediaStatusAndStartTimestamp.0 + } + strongSelf.updateStatus() + } + })) + + self.insertSubnode(videoNode, belowSubnode: self.statusNode) + + self.isReady.set(videoNode.ready |> map { return true }) + } + + func setup(item: PeerInfoAvatarListItem, progress: Signal? = nil, synchronous: Bool, fullSizeOnly: Bool = false) { + self.item = item + self.progress = progress + + if let progress = progress { + self.loadingProgress.set((progress + |> beforeNext { [weak self] next in + self?.isLoading.set(.single(next != nil)) + })) + } + + let representations: [ImageRepresentationWithReference] + let videoRepresentations: [VideoRepresentationWithReference] + let immediateThumbnailData: Data? + var id: Int64 + switch item { + case let .custom(node): + id = 0 + representations = [] + videoRepresentations = [] + immediateThumbnailData = nil + if !synchronous { + self.addSubnode(node) + } + case let .topImage(topRepresentations, videoRepresentationsValue, immediateThumbnail): + representations = topRepresentations + videoRepresentations = videoRepresentationsValue + immediateThumbnailData = immediateThumbnail + id = Int64(self.peer.id.id._internalGetInt32Value()) + if let resource = videoRepresentations.first?.representation.resource as? CloudPhotoSizeMediaResource { + id = id &+ resource.photoId + } + case let .image(reference, imageRepresentations, videoRepresentationsValue, immediateThumbnail): + representations = imageRepresentations + videoRepresentations = videoRepresentationsValue + immediateThumbnailData = immediateThumbnail + if case let .cloud(imageId, _, _) = reference { + id = imageId + } else { + id = Int64(self.peer.id.id._internalGetInt32Value()) + } + } + self.imageNode.setSignal(chatAvatarGalleryPhoto(account: self.context.account, representations: representations, immediateThumbnailData: immediateThumbnailData, autoFetchFullSize: true, attemptSynchronously: synchronous, skipThumbnail: fullSizeOnly), attemptSynchronously: synchronous, dispatchOnDisplayLink: false) + + if let video = videoRepresentations.last, let peerReference = PeerReference(self.peer) { + let videoFileReference = FileMediaReference.avatarList(peer: peerReference, media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: video.representation.resource, previewRepresentations: representations.map { $0.representation }, videoThumbnails: [], immediateThumbnailData: immediateThumbnailData, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: video.representation.dimensions, flags: [])])) + let videoContent = NativeVideoContent(id: .profileVideo(id, nil), fileReference: videoFileReference, streamVideo: isMediaStreamable(resource: video.representation.resource) ? .conservative : .none, loopVideo: true, enableSound: false, fetchAutomatically: true, onlyFullSizeThumbnail: fullSizeOnly, useLargeThumbnail: true, autoFetchFullSizeThumbnail: true, startTimestamp: video.representation.startTimestamp, continuePlayingWithoutSoundOnLostAudioSession: false, placeholderColor: .clear) + + if videoContent.id != self.videoContent?.id { + self.videoContent = videoContent + self.videoStartTimestamp = video.representation.startTimestamp + self.setupVideoPlayback() + } + } else { + if let videoNode = self.videoNode { + self.videoContent = nil + self.videoStartTimestamp = nil + self.videoNode = nil + + videoNode.removeFromSupernode() + } + + self.statusPromise.set(.single(nil)) + + self.statusDisposable.set(nil) + + self.imageNode.imageUpdated = { [weak self] _ in + guard let strongSelf = self else { + return + } + if !strongSelf.didSetReady { + strongSelf.didSetReady = true + strongSelf.isReady.set(.single(true)) + } + } + } + } + + 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() + let imageFrame = CGRect(origin: CGPoint(x: floor((size.width - imageSize.width) / 2.0), y: floor((size.height - imageSize.height) / 2.0)), size: imageSize) + transition.updateFrame(node: self.imageNode, frame: imageFrame) + + transition.updateFrame(node: self.statusNode, frame: CGRect(origin: CGPoint(x: floor((size.width - 50.0) / 2.0), y: floor((size.height - 50.0) / 2.0)), size: CGSize(width: 50.0, height: 50.0))) + + if let videoNode = self.videoNode { + videoNode.updateLayout(size: imageSize, transition: .immediate) + videoNode.frame = imageFrame + } + } +} + +private let fadeWidth: CGFloat = 70.0 + +public final class PeerInfoAvatarListContainerNode: ASDisplayNode { + private let context: AccountContext + public var peer: Peer? + + public let controlsContainerNode: ASDisplayNode + public let controlsClippingNode: ASDisplayNode + public let controlsClippingOffsetNode: ASDisplayNode + public let shadowNode: ASImageNode + + public let contentNode: ASDisplayNode + let leftHighlightNode: ASDisplayNode + let rightHighlightNode: ASDisplayNode + var highlightedSide: Bool? + public let stripContainerNode: ASDisplayNode + public let highlightContainerNode: ASDisplayNode + public private(set) var galleryEntries: [AvatarGalleryEntry] = [] + private var items: [PeerInfoAvatarListItem] = [] + private var itemNodes: [WrappedMediaResourceId: PeerInfoAvatarListItemNode] = [:] + private var stripNodes: [ASImageNode] = [] + private var activeStripNode: ASImageNode + private var loadingStripNode: PeerInfoAvatarListLoadingStripNode + private let activeStripImage: UIImage + private var appliedStripNodeCurrentIndex: Int? + var currentIndex: Int = 0 + private var transitionFraction: CGFloat = 0.0 + + private var validLayout: CGSize? + public var isCollapsing = false + private var isExpanded = false + + public var firstFullSizeOnly = false + public var customCenterTapAction: (() -> Void)? + + private let disposable = MetaDisposable() + private let positionDisposable = MetaDisposable() + private var initializedList = false + private var ignoreNextProfilePhotoUpdate = false + public var itemsUpdated: (([PeerInfoAvatarListItem]) -> Void)? + public var currentIndexUpdated: (() -> Void)? + + public let isReady = Promise() + private var didSetReady = false + + public var currentItemNode: PeerInfoAvatarListItemNode? { + if self.currentIndex >= 0 && self.currentIndex < self.items.count { + return self.itemNodes[self.items[self.currentIndex].id] + } else { + return nil + } + } + + public var currentEntry: AvatarGalleryEntry? { + if self.currentIndex >= 0 && self.currentIndex < self.galleryEntries.count { + return self.galleryEntries[self.currentIndex] + } else { + return nil + } + } + + private var playerUpdateTimer: SwiftSignalKit.Timer? + private var playerStatus: (MediaPlayerStatus?, Double?)? { + didSet { + if self.playerStatus?.0 != oldValue?.0 || self.playerStatus?.1 != oldValue?.1 { + if let (playerStatus, _) = self.playerStatus, let status = playerStatus, case .playing = status.status { + self.ensureHasTimer() + } else { + self.stopTimer() + } + self.updateStatus() + } + } + } + + private func ensureHasTimer() { + if self.playerUpdateTimer == nil { + let timer = SwiftSignalKit.Timer(timeout: 0.016, repeat: true, completion: { [weak self] in + self?.updateStatus() + }, queue: Queue.mainQueue()) + self.playerUpdateTimer = timer + timer.start() + } + } + + private var playbackProgress: CGFloat? + private var loading: Bool = false + private func updateStatus() { + var position: CGFloat = 1.0 + var loading = false + if let (status, videoStartTimestamp) = self.playerStatus, let playerStatus = status { + var playerPosition: Double + if case .buffering = playerStatus.status { + loading = true + } + if !playerStatus.generationTimestamp.isZero, case .playing = playerStatus.status { + playerPosition = playerStatus.timestamp + (CACurrentMediaTime() - playerStatus.generationTimestamp) + } else { + playerPosition = playerStatus.timestamp + } + + if let videoStartTimestamp = videoStartTimestamp, false { + playerPosition -= videoStartTimestamp + if playerPosition < 0.0 { + playerPosition = playerStatus.duration + playerPosition + } + } + + if playerStatus.duration.isZero { + position = 0.0 + } else { + position = CGFloat(playerPosition / playerStatus.duration) + } + } else { + self.playbackProgress = nil + } + + if let size = self.validLayout { + self.playbackProgress = position + self.loading = loading + self.updateStrips(size: size, itemsAdded: false, stripTransition: .animated(duration: 0.3, curve: .spring)) + } + } + + private func stopTimer() { + self.playerUpdateTimer?.invalidate() + self.playerUpdateTimer = nil + } + + public init(context: AccountContext) { + self.context = context + + self.contentNode = ASDisplayNode() + + self.leftHighlightNode = ASDisplayNode() + self.leftHighlightNode.displaysAsynchronously = false + self.leftHighlightNode.backgroundColor = generateImage(CGSize(width: fadeWidth, height: 24.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]) + }).flatMap { UIColor(patternImage: $0) } + self.leftHighlightNode.alpha = 0.0 + + self.rightHighlightNode = ASDisplayNode() + self.rightHighlightNode.displaysAsynchronously = false + self.rightHighlightNode.backgroundColor = generateImage(CGSize(width: fadeWidth, height: 24.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]) + }).flatMap { UIColor(patternImage: $0) } + self.rightHighlightNode.alpha = 0.0 + + self.stripContainerNode = ASDisplayNode() + self.contentNode.addSubnode(self.stripContainerNode) + self.activeStripImage = generateSmallHorizontalStretchableFilledCircleImage(diameter: 2.0, color: .white)! + + self.activeStripNode = ASImageNode() + self.activeStripNode.image = self.activeStripImage + + self.loadingStripNode = PeerInfoAvatarListLoadingStripNode() + self.loadingStripNode.imageNode.image = self.activeStripImage + + 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() + self.positionDisposable.dispose() + } + + public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + return super.hitTest(point, with: event) + } + + public func selectFirstItem() { + let previousIndex = self.currentIndex + self.currentIndex = 0 + if self.currentIndex != previousIndex { + self.currentIndexUpdated?() + } + if let size = self.validLayout { + self.updateItems(size: size, transition: .immediate, stripTransition: .immediate) + } + } + + public func updateEntryIsHidden(entry: AvatarGalleryEntry?) { + if let entry = entry, let index = self.galleryEntries.firstIndex(of: entry) { + self.currentItemNode?.isHidden = index == self.currentIndex + } else { + self.currentItemNode?.isHidden = false + } + } + + public var offsetLocation = 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 { + var location = location + if self.offsetLocation { + location.x += size.width / 2.0 + } + if location.x < size.width * 1.0 / 5.0 { + if self.currentIndex != 0 { + let previousIndex = self.currentIndex + self.currentIndex -= 1 + if self.currentIndex != previousIndex { + self.currentIndexUpdated?() + } + self.updateItems(size: size, transition: .immediate, stripTransition: .animated(duration: 0.3, curve: .spring), synchronous: true) + } else if self.items.count > 1 { + let previousIndex = self.currentIndex + self.currentIndex = self.items.count - 1 + if self.currentIndex != previousIndex { + self.currentIndexUpdated?() + } + self.updateItems(size: size, transition: .immediate, stripTransition: .animated(duration: 0.3, curve: .spring), synchronous: true) + } + } else { + if let customAction = self.customCenterTapAction, location.x < size.width - size.width * 1.0 / 5.0 { + customAction() + return + } + if self.currentIndex < self.items.count - 1 { + let previousIndex = self.currentIndex + self.currentIndex += 1 + if self.currentIndex != previousIndex { + self.currentIndexUpdated?() + } + self.updateItems(size: size, transition: .immediate, stripTransition: .animated(duration: 0.3, curve: .spring), synchronous: true) + } else if self.items.count > 1 { + let previousIndex = self.currentIndex + self.currentIndex = 0 + if self.currentIndex != previousIndex { + self.currentIndexUpdated?() + } + self.updateItems(size: size, transition: .immediate, stripTransition: .animated(duration: 0.3, curve: .spring), synchronous: true) + } + } + } + } + default: + break + } + } + + private var pageChangedByPan = false + @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: Bool? + if abs(velocity.x) > 10.0 { + directionIsToRight = velocity.x < 0.0 + } else if abs(self.transitionFraction) > 0.5 { + directionIsToRight = self.transitionFraction < 0.0 + } + var updatedIndex = self.currentIndex + if let directionIsToRight = directionIsToRight { + if directionIsToRight { + updatedIndex = min(updatedIndex + 1, self.items.count - 1) + } else { + updatedIndex = max(updatedIndex - 1, 0) + } + } + let previousIndex = self.currentIndex + self.currentIndex = updatedIndex + if self.currentIndex != previousIndex { + self.pageChangedByPan = true + self.currentIndexUpdated?() + } + 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)) + self.pageChangedByPan = false + } + default: + break + } + } + + func setMainItem(_ item: PeerInfoAvatarListItem) { + guard case let .image(image) = item else { + return + } + var items: [PeerInfoAvatarListItem] = [] + var entries: [AvatarGalleryEntry] = [] + for entry in self.galleryEntries { + switch entry { + case let .topImage(representations, videoRepresentations, _, _, immediateThumbnailData, _): + entries.append(entry) + items.append(.topImage(representations, videoRepresentations, immediateThumbnailData)) + case let .image(_, reference, representations, videoRepresentations, _, _, _, _, immediateThumbnailData, _): + if representations.isEmpty { + continue + } + if image.0 == reference { + entries.insert(entry, at: 0) + items.insert(.image(reference, representations, videoRepresentations, immediateThumbnailData), at: 0) + } else { + entries.append(entry) + items.append(.image(reference, representations, videoRepresentations, immediateThumbnailData)) + } + } + } + self.galleryEntries = normalizeEntries(entries) + self.items = items + self.itemsUpdated?(items) + let previousIndex = self.currentIndex + self.currentIndex = 0 + if self.currentIndex != previousIndex { + self.currentIndexUpdated?() + } + self.ignoreNextProfilePhotoUpdate = true + if let size = self.validLayout { + self.updateItems(size: size, update: true, transition: .immediate, stripTransition: .immediate, synchronous: true) + } + } + + public func deleteItem(_ item: PeerInfoAvatarListItem) -> Bool { + guard case let .image(image) = item else { + return false + } + + var items: [PeerInfoAvatarListItem] = [] + var entries: [AvatarGalleryEntry] = [] + let previousIndex = self.currentIndex + + var index = 0 + var deletedIndex: Int? + for entry in self.galleryEntries { + switch entry { + case let .topImage(representations, videoRepresentations, _, _, immediateThumbnailData, _): + entries.append(entry) + items.append(.topImage(representations, videoRepresentations, immediateThumbnailData)) + case let .image(_, reference, representations, videoRepresentations, _, _, _, _, immediateThumbnailData, _): + if representations.isEmpty { + continue + } + if image.0 != reference { + entries.append(entry) + items.append(.image(reference, representations, videoRepresentations, immediateThumbnailData)) + } else { + deletedIndex = index + } + } + index += 1 + } + + + if let peer = self.peer, peer is TelegramGroup || peer is TelegramChannel, deletedIndex == 0 { + self.galleryEntries = [] + self.items = [] + self.itemsUpdated?([]) + self.currentIndex = 0 + if let size = self.validLayout { + self.updateItems(size: size, update: true, transition: .immediate, stripTransition: .immediate, synchronous: true) + } + return true + } + + self.galleryEntries = normalizeEntries(entries) + self.items = items + self.itemsUpdated?(items) + self.currentIndex = max(0, previousIndex - 1) + if self.currentIndex != previousIndex { + self.currentIndexUpdated?() + } + self.ignoreNextProfilePhotoUpdate = true + if let size = self.validLayout { + self.updateItems(size: size, update: true, transition: .immediate, stripTransition: .immediate, synchronous: true) + } + + return items.count == 0 + } + + private var additionalEntryProgress: Signal? = nil + public func update(size: CGSize, peer: Peer?, customNode: ASDisplayNode? = nil, additionalEntry: Signal<(TelegramMediaImageRepresentation, Float)?, NoError> = .single(nil), isExpanded: Bool, transition: ContainedViewLayoutTransition) { + self.validLayout = size + let previousExpanded = self.isExpanded + self.isExpanded = isExpanded + if !isExpanded && previousExpanded { + self.isCollapsing = true + } + self.leftHighlightNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: fadeWidth, height: size.height)) + self.rightHighlightNode.frame = CGRect(origin: CGPoint(x: size.width - fadeWidth, y: 0.0), size: CGSize(width: fadeWidth, height: size.height)) + + if let peer = peer, !self.initializedList { + self.initializedList = true + + let entry = additionalEntry + |> map { representation -> AvatarGalleryEntry? in + return representation.flatMap { AvatarGalleryEntry(representation: $0.0, peer: peer) } + } + + self.disposable.set(combineLatest(queue: Queue.mainQueue(), peerInfoProfilePhotosWithCache(context: self.context, peerId: peer.id), entry).start(next: { [weak self] completeAndEntries, entry in + guard let strongSelf = self else { + return + } + + var (complete, entries) = completeAndEntries + + if strongSelf.galleryEntries.count > 1, entries.count == 1 && !complete { + return + } + + var synchronous = false + if !strongSelf.galleryEntries.isEmpty, let updated = entries.first, case let .image(image) = updated, !image.3.isEmpty, let previous = strongSelf.galleryEntries.first, case let .topImage(topImage) = previous { + let firstEntry = AvatarGalleryEntry.image(image.0, image.1, topImage.0, image.3, image.4, image.5, image.6, image.7, image.8, image.9) + entries.remove(at: 0) + entries.insert(firstEntry, at: 0) + synchronous = true + } + + if let entry = entry { + entries.insert(entry, at: 0) + + strongSelf.additionalEntryProgress = additionalEntry + |> map { value -> Float? in + return value?.1 + } + } + + if strongSelf.ignoreNextProfilePhotoUpdate { + if entries.count == 1, let first = entries.first, case .topImage = first { + return + } else { + strongSelf.ignoreNextProfilePhotoUpdate = false + synchronous = true + } + } + + var items: [PeerInfoAvatarListItem] = [] + if let customNode = customNode { + items.append(.custom(customNode)) + } + for entry in entries { + if let item = PeerInfoAvatarListItem(entry: entry) { + items.append(item) + } + } + strongSelf.galleryEntries = entries + strongSelf.items = items + strongSelf.itemsUpdated?(items) + if let size = strongSelf.validLayout { + strongSelf.updateItems(size: size, update: true, transition: .immediate, stripTransition: .immediate, synchronous: synchronous) + } + if items.isEmpty { + if !strongSelf.didSetReady { + strongSelf.didSetReady = true + strongSelf.isReady.set(.single(true)) + } + } + })) + } + self.updateItems(size: size, transition: transition, stripTransition: transition) + } + + private func updateStrips(size: CGSize, itemsAdded: Bool, stripTransition: ContainedViewLayoutTransition) { + 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 + 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) + } + } + self.stripContainerNode.addSubnode(self.activeStripNode) + self.stripContainerNode.addSubnode(self.loadingStripNode) + } + if self.appliedStripNodeCurrentIndex != self.currentIndex || itemsAdded { + if !self.itemNodes.isEmpty { + self.appliedStripNodeCurrentIndex = self.currentIndex + } + + if let currentItemNode = self.currentItemNode { + self.positionDisposable.set((currentItemNode.mediaStatus + |> deliverOnMainQueue).start(next: { [weak self] statusAndVideoStartTimestamp in + if let strongSelf = self { + strongSelf.playerStatus = statusAndVideoStartTimestamp + } + })) + } else { + self.positionDisposable.set(nil) + } + } + 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, floorToScreenPixels((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 = floorToScreenPixels(currentStripMinX + stripWidth / 2.0) + let lastStripMaxX = stripInset + CGFloat(self.stripNodes.count - 1) * (stripWidth + stripSpacing) + stripWidth + 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 self.currentIndex >= 0 && self.currentIndex < self.stripNodes.count { + var frame = self.stripNodes[self.currentIndex].frame + stripTransition.updateFrame(node: self.loadingStripNode, frame: frame) + if let playbackProgress = self.playbackProgress { + frame.size.width = max(frame.size.height, frame.size.width * playbackProgress) + } + stripTransition.updateFrameAdditive(node: self.activeStripNode, frame: frame) + stripTransition.updateAlpha(node: self.activeStripNode, alpha: self.loading ? 0.0 : 1.0) + stripTransition.updateAlpha(node: self.loadingStripNode, alpha: self.loading ? 1.0 : 0.0) + + self.activeStripNode.isHidden = self.stripNodes.count < 2 + self.loadingStripNode.isHidden = self.stripNodes.count < 2 || !self.loading + } + } + + public var updateCustomItemsOnlySynchronously = false + + private func updateItems(size: CGSize, update: Bool = false, transition: ContainedViewLayoutTransition, stripTransition: ContainedViewLayoutTransition, synchronous: Bool = false) { + var validIds: [WrappedMediaResourceId] = [] + var addedItemNodesForAdditiveTransition: [PeerInfoAvatarListItemNode] = [] + var additiveTransitionOffset: CGFloat = 0.0 + var itemsAdded = false + if self.currentIndex >= 0 && self.currentIndex < self.items.count { + let preloadSpan: Int = 2 + for i in max(0, self.currentIndex - preloadSpan) ... min(self.currentIndex + preloadSpan, self.items.count - 1) { + if self.items[i].representations.isEmpty { + continue + } + validIds.append(self.items[i].id) + var itemNode: PeerInfoAvatarListItemNode? + var wasAdded = false + if let current = self.itemNodes[self.items[i].id] { + itemNode = current + if update { + var synchronous = synchronous && i == self.currentIndex + if case .custom = self.items[i], self.updateCustomItemsOnlySynchronously { + synchronous = true + } + current.setup(item: self.items[i], synchronous: synchronous && i == self.currentIndex, fullSizeOnly: self.firstFullSizeOnly && i == 0) + } + } else if let peer = self.peer { + wasAdded = true + let addedItemNode = PeerInfoAvatarListItemNode(context: self.context, peer: peer) + itemNode = addedItemNode + addedItemNode.setup(item: self.items[i], progress: i == 0 ? self.additionalEntryProgress : nil, synchronous: (i == 0 && i == self.currentIndex) || (synchronous && i == self.currentIndex), fullSizeOnly: self.firstFullSizeOnly && i == 0) + self.itemNodes[self.items[i].id] = addedItemNode + self.contentNode.addSubnode(addedItemNode) + } + if let itemNode = itemNode { + itemNode.delayCentralityLose = self.pageChangedByPan + itemNode.isCentral = i == self.currentIndex + itemNode.delayCentralityLose = false + + 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 { + itemsAdded = true + 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: .immediate) + } + } + } + } + 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() + } + } + + self.updateStrips(size: size, itemsAdded: itemsAdded, stripTransition: stripTransition) + + if let item = self.items.first, let itemNode = self.itemNodes[item.id] { + if !self.didSetReady { + self.didSetReady = true + self.isReady.set(itemNode.isReady.get()) + } + } + } +} diff --git a/submodules/PeerInfoUI/Sources/ChannelAdminController.swift b/submodules/PeerInfoUI/Sources/ChannelAdminController.swift index aa6e27d728..2c3c31104b 100644 --- a/submodules/PeerInfoUI/Sources/ChannelAdminController.swift +++ b/submodules/PeerInfoUI/Sources/ChannelAdminController.swift @@ -889,7 +889,7 @@ public func channelAdminController(context: AccountContext, peerId: PeerId, admi return } - transferOwnershipDisposable.set((checkOwnershipTranfserAvailability(postbox: context.account.postbox, network: context.account.network, accountStateManager: context.account.stateManager, memberId: adminId) |> deliverOnMainQueue).start(error: { error in + transferOwnershipDisposable.set((context.engine.peers.checkOwnershipTranfserAvailability(memberId: adminId) |> deliverOnMainQueue).start(error: { error in let controller = channelOwnershipTransferController(context: context, peer: peer, member: member, initialError: error, present: { c, a in presentControllerImpl?(c, a) }, completion: { upgradedPeerId in @@ -927,14 +927,14 @@ public func channelAdminController(context: AccountContext, peerId: PeerId, admi return current.withUpdatedUpdating(true) } if peerId.namespace == Namespaces.Peer.CloudGroup { - updateRightsDisposable.set((removeGroupAdmin(account: context.account, peerId: peerId, adminId: adminId) + updateRightsDisposable.set((context.engine.peers.removeGroupAdmin(peerId: peerId, adminId: adminId) |> deliverOnMainQueue).start(error: { _ in }, completed: { updated(nil) dismissImpl?() })) } else { - updateRightsDisposable.set((context.peerChannelMemberCategoriesContextsManager.updateMemberAdminRights(account: context.account, peerId: peerId, memberId: adminId, adminRights: nil, rank: nil) |> deliverOnMainQueue).start(error: { _ in + updateRightsDisposable.set((context.peerChannelMemberCategoriesContextsManager.updateMemberAdminRights(engine: context.engine, peerId: peerId, memberId: adminId, adminRights: nil, rank: nil) |> deliverOnMainQueue).start(error: { _ in }, completed: { updated(nil) @@ -1023,6 +1023,8 @@ public func channelAdminController(context: AccountContext, peerId: PeerId, admi } else { updateFlags = [] } + } else { + updateFlags = adminInfo?.rights.rights } } currentRank = rank @@ -1039,7 +1041,7 @@ public func channelAdminController(context: AccountContext, peerId: PeerId, admi updateState { current in return current.withUpdatedUpdating(true) } - updateRightsDisposable.set((context.peerChannelMemberCategoriesContextsManager.updateMemberAdminRights(account: context.account, peerId: peerId, memberId: adminId, adminRights: TelegramChatAdminRights(rights: updateFlags ?? []), rank: effectiveRank) |> deliverOnMainQueue).start(error: { error in + updateRightsDisposable.set((context.peerChannelMemberCategoriesContextsManager.updateMemberAdminRights(engine: context.engine, peerId: peerId, memberId: adminId, adminRights: TelegramChatAdminRights(rights: updateFlags ?? []), rank: effectiveRank) |> deliverOnMainQueue).start(error: { error in updateState { current in return current.withUpdatedUpdating(false) } @@ -1087,7 +1089,7 @@ public func channelAdminController(context: AccountContext, peerId: PeerId, admi updateState { current in return current.withUpdatedUpdating(true) } - updateRightsDisposable.set((context.peerChannelMemberCategoriesContextsManager.updateMemberAdminRights(account: context.account, peerId: peerId, memberId: adminId, adminRights: TelegramChatAdminRights(rights: currentFlags), rank: updateRank) |> deliverOnMainQueue).start(error: { _ in + updateRightsDisposable.set((context.peerChannelMemberCategoriesContextsManager.updateMemberAdminRights(engine: context.engine, peerId: peerId, memberId: adminId, adminRights: TelegramChatAdminRights(rights: currentFlags), rank: updateRank) |> deliverOnMainQueue).start(error: { _ in }, completed: { updated(TelegramChatAdminRights(rights: currentFlags)) @@ -1132,7 +1134,7 @@ public func channelAdminController(context: AccountContext, peerId: PeerId, admi updateState { current in return current.withUpdatedUpdating(true) } - updateRightsDisposable.set((context.peerChannelMemberCategoriesContextsManager.updateMemberAdminRights(account: context.account, peerId: peerId, memberId: adminId, adminRights: TelegramChatAdminRights(rights: updateFlags), rank: updateRank) |> deliverOnMainQueue).start(error: { error in + updateRightsDisposable.set((context.peerChannelMemberCategoriesContextsManager.updateMemberAdminRights(engine: context.engine, peerId: peerId, memberId: adminId, adminRights: TelegramChatAdminRights(rights: updateFlags), rank: updateRank) |> deliverOnMainQueue).start(error: { error in if case let .addMemberError(addMemberError) = error, let admin = adminView.peers[adminView.peerId] { var text = presentationData.strings.Login_UnknownError switch addMemberError { @@ -1199,7 +1201,7 @@ public func channelAdminController(context: AccountContext, peerId: PeerId, admi updateState { current in return current.withUpdatedUpdating(true) } - updateRightsDisposable.set((addGroupAdmin(account: context.account, peerId: peerId, adminId: adminId) + updateRightsDisposable.set((context.engine.peers.addGroupAdmin(peerId: peerId, adminId: adminId) |> deliverOnMainQueue).start(error: { error in if case let .addMemberError(error) = error, case .privacy = error, let admin = adminView.peers[adminView.peerId] { presentControllerImpl?(textAlertController(context: context, title: nil, text: presentationData.strings.Privacy_GroupsAndChannels_InviteToGroupError(admin.compactDisplayTitle, admin.compactDisplayTitle).0, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil) @@ -1218,7 +1220,7 @@ public func channelAdminController(context: AccountContext, peerId: PeerId, admi case conversionFailed } - let signal = convertGroupToSupergroup(account: context.account, peerId: peerId) + let signal = context.engine.peers.convertGroupToSupergroup(peerId: peerId) |> map(Optional.init) |> `catch` { error -> Signal in switch error { @@ -1232,7 +1234,7 @@ public func channelAdminController(context: AccountContext, peerId: PeerId, admi guard let upgradedPeerId = upgradedPeerId else { return .fail(.conversionFailed) } - return context.peerChannelMemberCategoriesContextsManager.updateMemberAdminRights(account: context.account, peerId: upgradedPeerId, memberId: adminId, adminRights: TelegramChatAdminRights(rights: updateFlags), rank: updateRank) + return context.peerChannelMemberCategoriesContextsManager.updateMemberAdminRights(engine: context.engine, peerId: upgradedPeerId, memberId: adminId, adminRights: TelegramChatAdminRights(rights: updateFlags), rank: updateRank) |> mapError { error -> WrappedUpdateChannelAdminRightsError in return .direct(error) } diff --git a/submodules/PeerInfoUI/Sources/ChannelAdminsController.swift b/submodules/PeerInfoUI/Sources/ChannelAdminsController.swift index ae17167501..d0b16d3986 100644 --- a/submodules/PeerInfoUI/Sources/ChannelAdminsController.swift +++ b/submodules/PeerInfoUI/Sources/ChannelAdminsController.swift @@ -389,8 +389,16 @@ private func channelAdminsControllerEntries(presentationData: PresentationData, if id == accountPeerId { canEdit = false } else if let adminInfo = adminInfo { - if peer.flags.contains(.isCreator) || adminInfo.promotedBy == accountPeerId { + if peer.flags.contains(.isCreator) { canEdit = true + canOpen = true + } else if adminInfo.promotedBy == accountPeerId { + canEdit = true + if let adminRights = peer.adminRights { + if adminRights.rights.isEmpty { + canOpen = false + } + } } else { canEdit = false } @@ -575,14 +583,14 @@ public func channelAdminsController(context: AccountContext, peerId initialPeerI return $0.withUpdatedRemovingPeerId(adminId) } if peerId.namespace == Namespaces.Peer.CloudGroup { - removeAdminDisposable.set((removeGroupAdmin(account: context.account, peerId: peerId, adminId: adminId) + removeAdminDisposable.set((context.engine.peers.removeGroupAdmin(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: nil, rank: nil) + removeAdminDisposable.set((context.peerChannelMemberCategoriesContextsManager.updateMemberAdminRights(engine: context.engine, peerId: peerId, memberId: adminId, adminRights: nil, rank: nil) |> deliverOnMainQueue).start(completed: { updateState { return $0.withUpdatedRemovingPeerId(nil) @@ -657,7 +665,7 @@ public func channelAdminsController(context: AccountContext, peerId initialPeerI |> 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 + let membersAndLoadMoreControl: (Disposable, PeerChannelMemberCategoryControl?) = context.peerChannelMemberCategoriesContextsManager.admins(engine: context.engine, 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 { diff --git a/submodules/PeerInfoUI/Sources/ChannelBannedMemberController.swift b/submodules/PeerInfoUI/Sources/ChannelBannedMemberController.swift index 872d4bea6a..6b5a47e44b 100644 --- a/submodules/PeerInfoUI/Sources/ChannelBannedMemberController.swift +++ b/submodules/PeerInfoUI/Sources/ChannelBannedMemberController.swift @@ -506,7 +506,7 @@ public func channelBannedMemberController(context: AccountContext, peerId: PeerI state.updating = true return state } - updateRightsDisposable.set((context.peerChannelMemberCategoriesContextsManager.updateMemberBannedRights(account: context.account, peerId: peerId, memberId: memberId, bannedRights: nil) + updateRightsDisposable.set((context.peerChannelMemberCategoriesContextsManager.updateMemberBannedRights(engine: context.engine, peerId: peerId, memberId: memberId, bannedRights: nil) |> deliverOnMainQueue).start(error: { _ in }, completed: { @@ -650,7 +650,7 @@ public func channelBannedMemberController(context: AccountContext, peerId: PeerI } if peerId.namespace == Namespaces.Peer.CloudGroup { - let signal = convertGroupToSupergroup(account: context.account, peerId: peerId) + let signal = context.engine.peers.convertGroupToSupergroup(peerId: peerId) |> map(Optional.init) |> `catch` { error -> Signal in switch error { @@ -667,7 +667,7 @@ public func channelBannedMemberController(context: AccountContext, peerId: PeerI guard let upgradedPeerId = upgradedPeerId else { return .single(nil) } - return context.peerChannelMemberCategoriesContextsManager.updateMemberBannedRights(account: context.account, peerId: upgradedPeerId, memberId: memberId, bannedRights: cleanResolvedRights) + return context.peerChannelMemberCategoriesContextsManager.updateMemberBannedRights(engine: context.engine, peerId: upgradedPeerId, memberId: memberId, bannedRights: cleanResolvedRights) |> mapToSignal { _ -> Signal in return .complete() } @@ -700,7 +700,7 @@ public func channelBannedMemberController(context: AccountContext, peerId: PeerI } })) } else { - updateRightsDisposable.set((context.peerChannelMemberCategoriesContextsManager.updateMemberBannedRights(account: context.account, peerId: peerId, memberId: memberId, bannedRights: cleanResolvedRights) + updateRightsDisposable.set((context.peerChannelMemberCategoriesContextsManager.updateMemberBannedRights(engine: context.engine, peerId: peerId, memberId: memberId, bannedRights: cleanResolvedRights) |> deliverOnMainQueue).start(error: { _ in }, completed: { diff --git a/submodules/PeerInfoUI/Sources/ChannelBlacklistController.swift b/submodules/PeerInfoUI/Sources/ChannelBlacklistController.swift index b343ba58ac..9a180f0474 100644 --- a/submodules/PeerInfoUI/Sources/ChannelBlacklistController.swift +++ b/submodules/PeerInfoUI/Sources/ChannelBlacklistController.swift @@ -274,7 +274,8 @@ public func channelBlacklistController(context: AccountContext, peerId: PeerId) let updateState: ((ChannelBlacklistControllerState) -> ChannelBlacklistControllerState) -> Void = { f in statePromise.set(stateValue.modify { f($0) }) } - + + var getNavigationControllerImpl: (() -> NavigationController?)? var presentControllerImpl: ((ViewController, Any?) -> Void)? var pushControllerImpl: ((ViewController) -> Void)? var dismissInputImpl: (() -> Void)? @@ -323,7 +324,7 @@ public func channelBlacklistController(context: AccountContext, peerId: PeerId) let presentationData = context.sharedContext.currentPresentationData.with { $0 } let progress = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: nil)) presentControllerImpl?(progress, nil) - removePeerDisposable.set((context.peerChannelMemberCategoriesContextsManager.updateMemberBannedRights(account: context.account, peerId: peerId, memberId: peer.id, bannedRights: TelegramChatBannedRights(flags: [.banReadMessages], untilDate: Int32.max)) + removePeerDisposable.set((context.peerChannelMemberCategoriesContextsManager.updateMemberBannedRights(engine: context.engine, peerId: peerId, memberId: peer.id, bannedRights: TelegramChatBannedRights(flags: [.banReadMessages], untilDate: Int32.max)) |> deliverOnMainQueue).start(error: { [weak progress] _ in progress?.dismiss() dismissController?() @@ -342,7 +343,7 @@ public func channelBlacklistController(context: AccountContext, peerId: PeerId) return $0.withUpdatedRemovingPeerId(memberId) } - removePeerDisposable.set((context.peerChannelMemberCategoriesContextsManager.updateMemberBannedRights(account: context.account, peerId: peerId, memberId: memberId, bannedRights: nil) |> deliverOnMainQueue).start(error: { _ in + removePeerDisposable.set((context.peerChannelMemberCategoriesContextsManager.updateMemberBannedRights(engine: context.engine, peerId: peerId, memberId: memberId, bannedRights: nil) |> deliverOnMainQueue).start(error: { _ in updateState { return $0.withUpdatedRemovingPeerId(nil) } @@ -364,9 +365,19 @@ public func channelBlacklistController(context: AccountContext, peerId: PeerId) 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 + let viewInfoTitle: String + if participant.peer is TelegramChannel { + viewInfoTitle = presentationData.strings.GroupRemoved_ViewChannelInfo + } else { + viewInfoTitle = presentationData.strings.GroupRemoved_ViewUserInfo + } + items.append(ActionSheetButtonItem(title: viewInfoTitle, action: { [weak actionSheet] in actionSheet?.dismissAnimated() - if let infoController = context.sharedContext.makePeerInfoController(context: context, peer: participant.peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false) { + if participant.peer is TelegramChannel { + if let navigationController = getNavigationControllerImpl?() { + context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(participant.peer.id))) + } + } else if let infoController = context.sharedContext.makePeerInfoController(context: context, peer: participant.peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false) { pushControllerImpl?(infoController) } })) @@ -377,10 +388,10 @@ public func channelBlacklistController(context: AccountContext, peerId: PeerId) updateState { return $0.withUpdatedRemovingPeerId(memberId) } - let signal = context.peerChannelMemberCategoriesContextsManager.updateMemberBannedRights(account: context.account, peerId: peerId, memberId: memberId, bannedRights: nil) + let signal = context.peerChannelMemberCategoriesContextsManager.updateMemberBannedRights(engine: context.engine, peerId: peerId, memberId: memberId, bannedRights: nil) |> ignoreValues |> then( - context.peerChannelMemberCategoriesContextsManager.addMember(account: context.account, peerId: peerId, memberId: memberId) + context.peerChannelMemberCategoriesContextsManager.addMember(engine: context.engine, peerId: peerId, memberId: memberId) |> map { _ -> Void in return Void() } @@ -407,7 +418,7 @@ public func channelBlacklistController(context: AccountContext, peerId: PeerId) return $0.withUpdatedRemovingPeerId(memberId) } - removePeerDisposable.set((context.peerChannelMemberCategoriesContextsManager.updateMemberBannedRights(account: context.account, peerId: peerId, memberId: memberId, bannedRights: nil) |> deliverOnMainQueue).start(error: { _ in + removePeerDisposable.set((context.peerChannelMemberCategoriesContextsManager.updateMemberBannedRights(engine: context.engine, peerId: peerId, memberId: memberId, bannedRights: nil) |> deliverOnMainQueue).start(error: { _ in updateState { return $0.withUpdatedRemovingPeerId(nil) } @@ -426,7 +437,7 @@ public func channelBlacklistController(context: AccountContext, peerId: PeerId) }) }) - let (listDisposable, loadMoreControl) = context.peerChannelMemberCategoriesContextsManager.banned(postbox: context.account.postbox, network: context.account.network, accountPeerId: context.account.peerId, peerId: peerId, updated: { listState in + let (listDisposable, loadMoreControl) = context.peerChannelMemberCategoriesContextsManager.banned(engine: context.engine, postbox: context.account.postbox, network: context.account.network, accountPeerId: context.account.peerId, peerId: peerId, updated: { listState in if case .loading(true) = listState.loadingState, listState.list.isEmpty { blacklistPromise.set(.single(nil)) } else { @@ -520,6 +531,9 @@ public func channelBlacklistController(context: AccountContext, peerId: PeerId) (controller.navigationController as? NavigationController)?.pushViewController(c) } } + getNavigationControllerImpl = { [weak controller] in + return controller?.navigationController as? NavigationController + } dismissInputImpl = { [weak controller] in controller?.view.endEditing(true) } diff --git a/submodules/PeerInfoUI/Sources/ChannelDiscussionGroupSearchContainerNode.swift b/submodules/PeerInfoUI/Sources/ChannelDiscussionGroupSearchContainerNode.swift index 352be4ea7c..37ce4689be 100644 --- a/submodules/PeerInfoUI/Sources/ChannelDiscussionGroupSearchContainerNode.swift +++ b/submodules/PeerInfoUI/Sources/ChannelDiscussionGroupSearchContainerNode.swift @@ -219,7 +219,7 @@ final class ChannelDiscussionGroupSearchContainerNode: SearchDisplayControllerCo } }) - self.listNode.beganInteractiveDragging = { [weak self] in + self.listNode.beganInteractiveDragging = { [weak self] _ in self?.dismissInput?() } } diff --git a/submodules/PeerInfoUI/Sources/ChannelDiscussionGroupSetupController.swift b/submodules/PeerInfoUI/Sources/ChannelDiscussionGroupSetupController.swift index 7c16bdf96b..dc2e738bf6 100644 --- a/submodules/PeerInfoUI/Sources/ChannelDiscussionGroupSetupController.swift +++ b/submodules/PeerInfoUI/Sources/ChannelDiscussionGroupSetupController.swift @@ -230,7 +230,7 @@ public func channelDiscussionGroupSetupController(context: AccountContext, peerI let groupPeers = Promise<[Peer]?>() groupPeers.set(.single(nil) |> then( - availableGroupsForChannelDiscussion(postbox: context.account.postbox, network: context.account.network) + context.engine.peers.availableGroupsForChannelDiscussion() |> map(Optional.init) |> `catch` { _ -> Signal<[Peer]?, NoError> in return .single(nil) @@ -260,7 +260,7 @@ public func channelDiscussionGroupSetupController(context: AccountContext, peerI } let presentationData = context.sharedContext.currentPresentationData.with { $0 } pushControllerImpl?(context.sharedContext.makeCreateGroupController(context: context, peerIds: [], initialTitle: peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + " Chat", mode: .supergroup, completion: { groupId, dismiss in - var applySignal = updateGroupDiscussionForChannel(network: context.account.network, postbox: context.account.postbox, channelId: peerId, groupId: groupId) + var applySignal = context.engine.peers.updateGroupDiscussionForChannel(channelId: peerId, groupId: groupId) var cancelImpl: (() -> Void)? let progressSignal = Signal { subscriber in let presentationData = context.sharedContext.currentPresentationData.with { $0 } @@ -327,7 +327,7 @@ public func channelDiscussionGroupSetupController(context: AccountContext, peerI var applySignal: Signal var updatedPeerId: PeerId? = nil if let legacyGroup = groupPeer as? TelegramGroup { - applySignal = convertGroupToSupergroup(account: context.account, peerId: legacyGroup.id) + applySignal = context.engine.peers.convertGroupToSupergroup(peerId: legacyGroup.id) |> mapError { error -> ChannelDiscussionGroupError in switch error { case .tooManyChannels: @@ -358,13 +358,13 @@ public func channelDiscussionGroupSetupController(context: AccountContext, peerI }) } - return updateGroupDiscussionForChannel(network: context.account.network, postbox: context.account.postbox, channelId: peerId, groupId: resultPeerId) + return context.engine.peers.updateGroupDiscussionForChannel(channelId: peerId, groupId: resultPeerId) } |> castError(ChannelDiscussionGroupError.self) |> switchToLatest } } else { - applySignal = updateGroupDiscussionForChannel(network: context.account.network, postbox: context.account.postbox, channelId: peerId, groupId: groupId) + applySignal = context.engine.peers.updateGroupDiscussionForChannel(channelId: peerId, groupId: groupId) } var cancelImpl: (() -> Void)? let progressSignal = Signal { subscriber in @@ -413,7 +413,7 @@ public func channelDiscussionGroupSetupController(context: AccountContext, peerI case .groupHistoryIsCurrentlyPrivate: let presentationData = context.sharedContext.currentPresentationData.with { $0 } presentControllerImpl?(textAlertController(context: context, title: nil, text: presentationData.strings.Channel_DiscussionGroup_MakeHistoryPublic, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: presentationData.strings.Channel_DiscussionGroup_MakeHistoryPublicProceed, action: { - var applySignal: Signal = updateChannelHistoryAvailabilitySettingsInteractively(postbox: context.account.postbox, network: context.account.network, accountStateManager: context.account.stateManager, peerId: updatedPeerId ?? groupId, historyAvailableForNewMembers: true) + var applySignal: Signal = context.engine.peers.updateChannelHistoryAvailabilitySettingsInteractively(peerId: updatedPeerId ?? groupId, historyAvailableForNewMembers: true) |> mapError { _ -> ChannelDiscussionGroupError in return .generic } @@ -421,7 +421,7 @@ public func channelDiscussionGroupSetupController(context: AccountContext, peerI return .complete() } |> then( - updateGroupDiscussionForChannel(network: context.account.network, postbox: context.account.postbox, channelId: peerId, groupId: updatedPeerId ?? groupId) + context.engine.peers.updateGroupDiscussionForChannel(channelId: peerId, groupId: updatedPeerId ?? groupId) ) var cancelImpl: (() -> Void)? let progressSignal = Signal { subscriber in @@ -502,7 +502,7 @@ public func channelDiscussionGroupSetupController(context: AccountContext, peerI return } - var applySignal = updateGroupDiscussionForChannel(network: context.account.network, postbox: context.account.postbox, channelId: applyPeerId, groupId: nil) + var applySignal = context.engine.peers.updateGroupDiscussionForChannel(channelId: applyPeerId, groupId: nil) var cancelImpl: (() -> Void)? let progressSignal = Signal { subscriber in let presentationData = context.sharedContext.currentPresentationData.with { $0 } diff --git a/submodules/PeerInfoUI/Sources/ChannelInfoController.swift b/submodules/PeerInfoUI/Sources/ChannelInfoController.swift index ae74d55458..ce61260895 100644 --- a/submodules/PeerInfoUI/Sources/ChannelInfoController.swift +++ b/submodules/PeerInfoUI/Sources/ChannelInfoController.swift @@ -732,13 +732,13 @@ public func channelInfoController(context: AccountContext, peerId: PeerId) -> Vi let completedImpl: (UIImage) -> Void = { image in if let data = image.jpegData(compressionQuality: 0.6) { - let resource = LocalFileMediaResource(fileId: arc4random64()) + let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) context.account.postbox.mediaBox.storeResourceData(resource.id, data: data) - let representation = TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 640, height: 640), resource: resource, progressiveSizes: []) + let representation = TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 640, height: 640), resource: resource, progressiveSizes: [], immediateThumbnailData: nil) updateState { $0.withUpdatedUpdatingAvatar(.image(representation, true)) } - updateAvatarDisposable.set((updatePeerPhoto(postbox: context.account.postbox, network: context.account.network, stateManager: context.account.stateManager, accountPeerId: context.account.peerId, peerId: peerId, photo: uploadedPeerPhoto(postbox: context.account.postbox, network: context.account.network, resource: resource), mapResourceToAvatarSizes: { resource, representations in + updateAvatarDisposable.set((context.engine.peers.updatePeerPhoto(peerId: peerId, photo: context.engine.peers.uploadedPeerPhoto(resource: resource), mapResourceToAvatarSizes: { resource, representations in return mapResourceToAvatarSizes(postbox: context.account.postbox, resource: resource, representations: representations) }) |> deliverOnMainQueue).start(next: { result in @@ -777,7 +777,7 @@ public func channelInfoController(context: AccountContext, peerId: PeerId) -> Vi return $0.withUpdatedUpdatingAvatar(ItemListAvatarAndNameInfoItemUpdatingAvatar.none) } } - updateAvatarDisposable.set((updatePeerPhoto(postbox: context.account.postbox, network: context.account.network, stateManager: context.account.stateManager, accountPeerId: context.account.peerId, peerId: peerId, photo: nil, mapResourceToAvatarSizes: { resource, representations in + updateAvatarDisposable.set((context.engine.peers.updatePeerPhoto(peerId: peerId, photo: nil, mapResourceToAvatarSizes: { resource, representations in return mapResourceToAvatarSizes(postbox: context.account.postbox, resource: resource, representations: representations) }) |> deliverOnMainQueue).start(next: { result in switch result { @@ -836,11 +836,11 @@ public func channelInfoController(context: AccountContext, peerId: PeerId) -> Vi } let controller = notificationMuteSettingsController(presentationData: presentationData, notificationSettings: globalSettings.effective.groupChats, soundSettings: soundSettings, openSoundSettings: { let controller = notificationSoundSelectionController(context: context, isModal: true, currentSound: peerSettings.messageSound, defaultSound: globalSettings.effective.groupChats.sound, completion: { sound in - let _ = updatePeerNotificationSoundInteractive(account: context.account, peerId: peerId, sound: sound).start() + let _ = context.engine.peers.updatePeerNotificationSoundInteractive(peerId: peerId, sound: sound).start() }) presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) }, updateSettings: { value in - changeMuteSettingsDisposable.set(updatePeerMuteSetting(account: context.account, peerId: peerId, muteInterval: value).start()) + changeMuteSettingsDisposable.set(context.engine.peers.updatePeerMuteSetting(peerId: peerId, muteInterval: value).start()) }) presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) }) @@ -918,7 +918,7 @@ public func channelInfoController(context: AccountContext, peerId: PeerId) -> Vi }, aboutLinkAction: { action, itemLink in aboutLinkActionImpl?(action, itemLink) }, toggleSignatures: { enabled in - actionsDisposable.add(toggleShouldChannelMessagesSignatures(account: context.account, peerId: peerId, enabled: enabled).start()) + actionsDisposable.add(context.engine.peers.toggleShouldChannelMessagesSignatures(peerId: peerId, enabled: enabled).start()) }) var wasEditing: Bool? @@ -993,7 +993,7 @@ public func channelInfoController(context: AccountContext, peerId: PeerId) -> Vi let updateTitle: Signal if let titleValue = updateValues.title { - updateTitle = updatePeerTitle(account: context.account, peerId: peerId, title: titleValue) + updateTitle = context.engine.peers.updatePeerTitle(peerId: peerId, title: titleValue) |> mapError { _ in return Void() } } else { updateTitle = .complete() @@ -1001,7 +1001,7 @@ public func channelInfoController(context: AccountContext, peerId: PeerId) -> Vi let updateDescription: Signal if let descriptionValue = updateValues.description { - updateDescription = updatePeerDescription(account: context.account, peerId: peerId, description: descriptionValue.isEmpty ? nil : descriptionValue) + updateDescription = context.engine.peers.updatePeerDescription(peerId: peerId, description: descriptionValue.isEmpty ? nil : descriptionValue) |> mapError { _ in return Void() } } else { updateDescription = .complete() diff --git a/submodules/PeerInfoUI/Sources/ChannelMembersController.swift b/submodules/PeerInfoUI/Sources/ChannelMembersController.swift index 9e1de8dd89..dc424cb6ad 100644 --- a/submodules/PeerInfoUI/Sources/ChannelMembersController.swift +++ b/submodules/PeerInfoUI/Sources/ChannelMembersController.swift @@ -281,7 +281,13 @@ private func channelMembersControllerEntries(context: AccountContext, presentati if let peer = view.peers[view.peerId] as? TelegramChannel, peer.addressName == nil { entries.append(.inviteLink(presentationData.theme, presentationData.strings.Channel_Members_InviteLink)) } - entries.append(.addMemberInfo(presentationData.theme, isGroup ? presentationData.strings.Group_Members_AddMembersHelp : presentationData.strings.Channel_Members_AddMembersHelp)) + if let peer = view.peers[view.peerId] as? TelegramChannel { + if peer.flags.contains(.isGigagroup) { + entries.append(.addMemberInfo(presentationData.theme, presentationData.strings.Group_Members_AddMembersHelp)) + } else if case .broadcast = peer.info { + entries.append(.addMemberInfo(presentationData.theme, presentationData.strings.Channel_Members_AddMembersHelp)) + } + } } @@ -353,7 +359,7 @@ public func channelMembersController(context: AccountContext, peerId: PeerId) -> contacts = peerIdsValue } - let signal = context.peerChannelMemberCategoriesContextsManager.addMembers(account: context.account, peerId: peerId, memberIds: contacts.compactMap({ contact -> PeerId? in + let signal = context.peerChannelMemberCategoriesContextsManager.addMembers(engine: context.engine, peerId: peerId, memberIds: contacts.compactMap({ contact -> PeerId? in switch contact { case let .peer(contactId): return contactId @@ -437,7 +443,7 @@ public func channelMembersController(context: AccountContext, peerId: PeerId) -> return $0.withUpdatedRemovingPeerId(memberId) } - removePeerDisposable.set((context.peerChannelMemberCategoriesContextsManager.updateMemberBannedRights(account: context.account, peerId: peerId, memberId: memberId, bannedRights: TelegramChatBannedRights(flags: [.banReadMessages], untilDate: Int32.max)) + removePeerDisposable.set((context.peerChannelMemberCategoriesContextsManager.updateMemberBannedRights(engine: context.engine, peerId: peerId, memberId: memberId, bannedRights: TelegramChatBannedRights(flags: [.banReadMessages], untilDate: Int32.max)) |> deliverOnMainQueue).start(completed: { updateState { return $0.withUpdatedRemovingPeerId(nil) @@ -456,7 +462,7 @@ public func channelMembersController(context: AccountContext, peerId: PeerId) -> let peerView = context.account.viewTracker.peerView(peerId) - let (disposable, loadMoreControl) = context.peerChannelMemberCategoriesContextsManager.recent(postbox: context.account.postbox, network: context.account.network, accountPeerId: context.account.peerId, peerId: peerId, updated: { state in + let (disposable, loadMoreControl) = context.peerChannelMemberCategoriesContextsManager.recent(engine: context.engine, postbox: context.account.postbox, network: context.account.network, accountPeerId: context.account.peerId, peerId: peerId, updated: { state in peersPromise.set(.single(state.list)) }) actionsDisposable.add(disposable) diff --git a/submodules/PeerInfoUI/Sources/ChannelMembersSearchContainerNode.swift b/submodules/PeerInfoUI/Sources/ChannelMembersSearchContainerNode.swift index 9e43448063..fe3b052e30 100644 --- a/submodules/PeerInfoUI/Sources/ChannelMembersSearchContainerNode.swift +++ b/submodules/PeerInfoUI/Sources/ChannelMembersSearchContainerNode.swift @@ -231,13 +231,13 @@ private func categorySignal(context: AccountContext, peerId: PeerId, category: G } switch category { case .admins: - disposableAndLoadMoreControl = context.peerChannelMemberCategoriesContextsManager.admins(postbox: context.account.postbox, network: context.account.network, accountPeerId: context.account.peerId, peerId: peerId, searchQuery: nil, updated: processListState) + disposableAndLoadMoreControl = context.peerChannelMemberCategoriesContextsManager.admins(engine: context.engine, postbox: context.account.postbox, network: context.account.network, accountPeerId: context.account.peerId, peerId: peerId, searchQuery: nil, updated: processListState) case .contacts: - disposableAndLoadMoreControl = context.peerChannelMemberCategoriesContextsManager.contacts(postbox: context.account.postbox, network: context.account.network, accountPeerId: context.account.peerId, peerId: peerId, searchQuery: nil, updated: processListState) + disposableAndLoadMoreControl = context.peerChannelMemberCategoriesContextsManager.contacts(engine: context.engine, postbox: context.account.postbox, network: context.account.network, accountPeerId: context.account.peerId, peerId: peerId, searchQuery: nil, updated: processListState) case .bots: - disposableAndLoadMoreControl = context.peerChannelMemberCategoriesContextsManager.bots(postbox: context.account.postbox, network: context.account.network, accountPeerId: context.account.peerId, peerId: peerId, searchQuery: nil, updated: processListState) + disposableAndLoadMoreControl = context.peerChannelMemberCategoriesContextsManager.bots(engine: context.engine, postbox: context.account.postbox, network: context.account.network, accountPeerId: context.account.peerId, peerId: peerId, searchQuery: nil, updated: processListState) case .members: - disposableAndLoadMoreControl = context.peerChannelMemberCategoriesContextsManager.recent(postbox: context.account.postbox, network: context.account.network, accountPeerId: context.account.peerId, peerId: peerId, searchQuery: nil, updated: processListState) + disposableAndLoadMoreControl = context.peerChannelMemberCategoriesContextsManager.recent(engine: context.engine, postbox: context.account.postbox, network: context.account.network, accountPeerId: context.account.peerId, peerId: peerId, searchQuery: nil, updated: processListState) } let (disposable, _) = disposableAndLoadMoreControl @@ -437,7 +437,7 @@ public final class ChannelMembersSearchContainerNode: SearchDisplayControllerCon if peerId.namespace == Namespaces.Peer.CloudChannel { if case .searchAdmins = mode { - return context.peerChannelMemberCategoriesContextsManager.updateMemberAdminRights(account: context.account, peerId: peerId, memberId: memberId, adminRights: nil, rank: nil) + return context.peerChannelMemberCategoriesContextsManager.updateMemberAdminRights(engine: context.engine, peerId: peerId, memberId: memberId, adminRights: nil, rank: nil) |> `catch` { _ -> Signal in return .complete() } @@ -452,7 +452,7 @@ public final class ChannelMembersSearchContainerNode: SearchDisplayControllerCon } } - return context.peerChannelMemberCategoriesContextsManager.updateMemberBannedRights(account: context.account, peerId: peerId, memberId: memberId, bannedRights: TelegramChatBannedRights(flags: [.banReadMessages], untilDate: Int32.max)) + return context.peerChannelMemberCategoriesContextsManager.updateMemberBannedRights(engine: context.engine, peerId: peerId, memberId: memberId, bannedRights: TelegramChatBannedRights(flags: [.banReadMessages], untilDate: Int32.max)) |> afterDisposed { Queue.mainQueue().async { updateState { state in @@ -465,7 +465,7 @@ public final class ChannelMembersSearchContainerNode: SearchDisplayControllerCon } if case .searchAdmins = mode { - return removeGroupAdmin(account: context.account, peerId: peerId, adminId: memberId) + return context.engine.peers.removeGroupAdmin(peerId: peerId, adminId: memberId) |> `catch` { _ -> Signal in return .complete() } @@ -479,7 +479,7 @@ public final class ChannelMembersSearchContainerNode: SearchDisplayControllerCon } } - return removePeerMember(account: context.account, peerId: peerId, memberId: memberId) + return context.engine.peers.removePeerMember(peerId: peerId, memberId: memberId) |> deliverOnMainQueue |> afterDisposed { updateState { state in @@ -608,7 +608,7 @@ public final class ChannelMembersSearchContainerNode: SearchDisplayControllerCon switch mode { case .searchMembers, .banAndPromoteActions: foundGroupMembers = Signal { subscriber in - let (disposable, _) = context.peerChannelMemberCategoriesContextsManager.recent(postbox: context.account.postbox, network: context.account.network, accountPeerId: context.account.peerId, peerId: peerId, searchQuery: query, updated: { state in + let (disposable, _) = context.peerChannelMemberCategoriesContextsManager.recent(engine: context.engine, postbox: context.account.postbox, network: context.account.network, accountPeerId: context.account.peerId, peerId: peerId, searchQuery: query, updated: { state in if case .ready = state.loadingState { subscriber.putNext(state.list) } @@ -619,11 +619,11 @@ public final class ChannelMembersSearchContainerNode: SearchDisplayControllerCon foundMembers = .single([]) case .inviteActions: foundGroupMembers = .single([]) - foundMembers = channelMembers(postbox: context.account.postbox, network: context.account.network, accountPeerId: context.account.peerId, peerId: peerId, category: .recent(.search(query))) + foundMembers = context.engine.peers.channelMembers(peerId: peerId, category: .recent(.search(query))) |> map { $0 ?? [] } case .searchAdmins: foundGroupMembers = Signal { subscriber in - let (disposable, _) = context.peerChannelMemberCategoriesContextsManager.admins(postbox: context.account.postbox, network: context.account.network, accountPeerId: context.account.peerId, peerId: peerId, searchQuery: query, updated: { state in + let (disposable, _) = context.peerChannelMemberCategoriesContextsManager.admins(engine: context.engine, postbox: context.account.postbox, network: context.account.network, accountPeerId: context.account.peerId, peerId: peerId, searchQuery: query, updated: { state in if case .ready = state.loadingState { subscriber.putNext(state.list) } @@ -633,7 +633,7 @@ public final class ChannelMembersSearchContainerNode: SearchDisplayControllerCon foundMembers = .single([]) case .searchBanned: foundGroupMembers = Signal { subscriber in - let (disposable, _) = context.peerChannelMemberCategoriesContextsManager.restricted(postbox: context.account.postbox, network: context.account.network, accountPeerId: context.account.peerId, peerId: peerId, searchQuery: query, updated: { state in + let (disposable, _) = context.peerChannelMemberCategoriesContextsManager.restricted(engine: context.engine, postbox: context.account.postbox, network: context.account.network, accountPeerId: context.account.peerId, peerId: peerId, searchQuery: query, updated: { state in if case .ready = state.loadingState { subscriber.putNext(state.list) subscriber.putCompletion() @@ -643,7 +643,7 @@ public final class ChannelMembersSearchContainerNode: SearchDisplayControllerCon } |> runOn(Queue.mainQueue()) foundMembers = Signal { subscriber in - let (disposable, _) = context.peerChannelMemberCategoriesContextsManager.recent(postbox: context.account.postbox, network: context.account.network, accountPeerId: context.account.peerId, peerId: peerId, searchQuery: query, updated: { state in + let (disposable, _) = context.peerChannelMemberCategoriesContextsManager.recent(engine: context.engine, postbox: context.account.postbox, network: context.account.network, accountPeerId: context.account.peerId, peerId: peerId, searchQuery: query, updated: { state in if case .ready = state.loadingState { subscriber.putNext(state.list.filter({ participant in return participant.peer.id != context.account.peerId @@ -655,7 +655,7 @@ public final class ChannelMembersSearchContainerNode: SearchDisplayControllerCon |> runOn(Queue.mainQueue()) case .searchKicked: foundGroupMembers = Signal { subscriber in - let (disposable, _) = context.peerChannelMemberCategoriesContextsManager.banned(postbox: context.account.postbox, network: context.account.network, accountPeerId: context.account.peerId, peerId: peerId, searchQuery: query, updated: { state in + let (disposable, _) = context.peerChannelMemberCategoriesContextsManager.banned(engine: context.engine, postbox: context.account.postbox, network: context.account.network, accountPeerId: context.account.peerId, peerId: peerId, searchQuery: query, updated: { state in if case .ready = state.loadingState { subscriber.putNext(state.list) subscriber.putCompletion() @@ -682,7 +682,7 @@ public final class ChannelMembersSearchContainerNode: SearchDisplayControllerCon foundRemotePeers = .single(([], [])) } else { foundContacts = context.account.postbox.searchContacts(query: query.lowercased()) - foundRemotePeers = .single(([], [])) |> then(searchPeers(account: context.account, query: query) + foundRemotePeers = .single(([], [])) |> then(context.engine.peers.searchPeers(query: query) |> delay(0.2, queue: Queue.concurrentDefaultQueue())) } case .searchMembers, .searchBanned, .searchKicked, .searchAdmins: @@ -996,7 +996,7 @@ public final class ChannelMembersSearchContainerNode: SearchDisplayControllerCon } if mode == .banAndPromoteActions || mode == .inviteActions { - foundRemotePeers = .single(([], [])) |> then(searchPeers(account: context.account, query: query) + foundRemotePeers = .single(([], [])) |> then(context.engine.peers.searchPeers(query: query) |> delay(0.2, queue: Queue.concurrentDefaultQueue())) } else { foundRemotePeers = .single(([], [])) @@ -1279,10 +1279,10 @@ public final class ChannelMembersSearchContainerNode: SearchDisplayControllerCon } }) - self.emptyQueryListNode.beganInteractiveDragging = { [weak self] in + self.emptyQueryListNode.beganInteractiveDragging = { [weak self] _ in self?.dismissInput?() } - self.listNode.beganInteractiveDragging = { [weak self] in + self.listNode.beganInteractiveDragging = { [weak self] _ in self?.dismissInput?() } } diff --git a/submodules/PeerInfoUI/Sources/ChannelMembersSearchController.swift b/submodules/PeerInfoUI/Sources/ChannelMembersSearchController.swift index d93591f208..e0b9298316 100644 --- a/submodules/PeerInfoUI/Sources/ChannelMembersSearchController.swift +++ b/submodules/PeerInfoUI/Sources/ChannelMembersSearchController.swift @@ -132,7 +132,7 @@ public final class ChannelMembersSearchController: ViewController { override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { super.containerLayoutUpdated(layout, transition: transition) - self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationInsetHeight, actualNavigationBarHeight: self.navigationHeight, transition: transition) + self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.cleanNavigationHeight, actualNavigationBarHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition) } private func activateSearch() { diff --git a/submodules/PeerInfoUI/Sources/ChannelMembersSearchControllerNode.swift b/submodules/PeerInfoUI/Sources/ChannelMembersSearchControllerNode.swift index 9078871922..0ae15a0ed8 100644 --- a/submodules/PeerInfoUI/Sources/ChannelMembersSearchControllerNode.swift +++ b/submodules/PeerInfoUI/Sources/ChannelMembersSearchControllerNode.swift @@ -419,20 +419,38 @@ class ChannelMembersSearchControllerNode: ASDisplayNode { } else { let membersState = Promise() - disposableAndLoadMoreControl = context.peerChannelMemberCategoriesContextsManager.recent(postbox: context.account.postbox, network: context.account.network, accountPeerId: context.account.peerId, peerId: peerId, updated: { state in + disposableAndLoadMoreControl = context.peerChannelMemberCategoriesContextsManager.recent(engine: context.engine, postbox: context.account.postbox, network: context.account.network, accountPeerId: context.account.peerId, peerId: peerId, updated: { state in membersState.set(.single(state)) }) additionalDisposable.set((combineLatest(queue: .mainQueue(), membersState.get(), + context.account.postbox.peerView(id: peerId), context.account.postbox.contactPeersView(accountPeerId: context.account.peerId, includePresences: true) - ).start(next: { [weak self] state, contactsView in + ).start(next: { [weak self] state, peerView, contactsView in guard let strongSelf = self else { return } var entries: [ChannelMembersSearchEntry] = [] - if case .inviteToCall = mode, !filters.contains(where: { filter in + var canInviteByLink = false + if let peer = peerViewMainPeer(peerView) { + if !(peer.addressName?.isEmpty ?? true) { + canInviteByLink = true + } else if let peer = peer as? TelegramChannel { + if peer.flags.contains(.isCreator) || (peer.adminRights?.rights.contains(.canInviteUsers) == true) { + canInviteByLink = true + } + } else if let peer = peer as? TelegramGroup { + if case .creator = peer.role { + canInviteByLink = true + } else if case let .admin(rights, _) = peer.role, rights.rights.contains(.canInviteUsers) { + canInviteByLink = true + } + } + } + + if case .inviteToCall = mode, canInviteByLink, !filters.contains(where: { filter in if case .excludeNonMembers = filter { return true } else { @@ -563,7 +581,7 @@ class ChannelMembersSearchControllerNode: ASDisplayNode { } } - self.listNode.beganInteractiveDragging = { [weak self] in + self.listNode.beganInteractiveDragging = { [weak self] _ in self?.view.endEditing(true) } } diff --git a/submodules/PeerInfoUI/Sources/ChannelOwnershipTransferController.swift b/submodules/PeerInfoUI/Sources/ChannelOwnershipTransferController.swift index 2dc4cd0c0f..bd93b2c704 100644 --- a/submodules/PeerInfoUI/Sources/ChannelOwnershipTransferController.swift +++ b/submodules/PeerInfoUI/Sources/ChannelOwnershipTransferController.swift @@ -455,12 +455,12 @@ private func commitChannelOwnershipTransferController(context: AccountContext, p let signal: Signal if let peer = peer as? TelegramChannel { - signal = context.peerChannelMemberCategoriesContextsManager.transferOwnership(account: context.account, peerId: peer.id, memberId: member.id, password: contentNode.password) |> mapToSignal { _ in + signal = context.peerChannelMemberCategoriesContextsManager.transferOwnership(engine: context.engine, peerId: peer.id, memberId: member.id, password: contentNode.password) |> mapToSignal { _ in return .complete() } |> then(.single(nil)) } else if let peer = peer as? TelegramGroup { - signal = convertGroupToSupergroup(account: context.account, peerId: peer.id) + signal = context.engine.peers.convertGroupToSupergroup(peerId: peer.id) |> map(Optional.init) |> mapError { error -> ChannelOwnershipTransferError in switch error { @@ -475,7 +475,7 @@ private func commitChannelOwnershipTransferController(context: AccountContext, p guard let upgradedPeerId = upgradedPeerId else { return .fail(.generic) } - return context.peerChannelMemberCategoriesContextsManager.transferOwnership(account: context.account, peerId: upgradedPeerId, memberId: member.id, password: contentNode.password) |> mapToSignal { _ in + return context.peerChannelMemberCategoriesContextsManager.transferOwnership(engine: context.engine, peerId: upgradedPeerId, memberId: member.id, password: contentNode.password) |> mapToSignal { _ in return .complete() } |> then(.single(upgradedPeerId)) diff --git a/submodules/PeerInfoUI/Sources/ChannelPermissionsController.swift b/submodules/PeerInfoUI/Sources/ChannelPermissionsController.swift index 42ecaef5e9..b2b2d60479 100644 --- a/submodules/PeerInfoUI/Sources/ChannelPermissionsController.swift +++ b/submodules/PeerInfoUI/Sources/ChannelPermissionsController.swift @@ -536,7 +536,7 @@ public func channelPermissionsController(context: AccountContext, peerId origina peersPromise.set(.single((peerId, nil))) } else { var loadCompletedCalled = false - let disposableAndLoadMoreControl = context.peerChannelMemberCategoriesContextsManager.restricted(postbox: context.account.postbox, network: context.account.network, accountPeerId: context.account.peerId, peerId: peerId, updated: { state in + let disposableAndLoadMoreControl = context.peerChannelMemberCategoriesContextsManager.restricted(engine: context.engine, postbox: context.account.postbox, network: context.account.network, accountPeerId: context.account.peerId, peerId: peerId, updated: { state in if case .loading(true) = state.loadingState, !updated { peersPromise.set(.single((peerId, nil))) } else { @@ -594,7 +594,7 @@ public func channelPermissionsController(context: AccountContext, peerId origina } let state = stateValue.with { $0 } if let modifiedRightsFlags = state.modifiedRightsFlags { - updateDefaultRightsDisposable.set((updateDefaultChannelMemberBannedRights(account: context.account, peerId: view.peerId, rights: TelegramChatBannedRights(flags: completeRights(modifiedRightsFlags), untilDate: Int32.max)) + updateDefaultRightsDisposable.set((context.engine.peers.updateDefaultChannelMemberBannedRights(peerId: view.peerId, rights: TelegramChatBannedRights(flags: completeRights(modifiedRightsFlags), untilDate: Int32.max)) |> deliverOnMainQueue).start()) } } else if let group = view.peers[view.peerId] as? TelegramGroup, let _ = view.cachedData as? CachedGroupData { @@ -624,7 +624,7 @@ public func channelPermissionsController(context: AccountContext, peerId origina } let state = stateValue.with { $0 } if let modifiedRightsFlags = state.modifiedRightsFlags { - updateDefaultRightsDisposable.set((updateDefaultChannelMemberBannedRights(account: context.account, peerId: view.peerId, rights: TelegramChatBannedRights(flags: completeRights(modifiedRightsFlags), untilDate: Int32.max)) + updateDefaultRightsDisposable.set((context.engine.peers.updateDefaultChannelMemberBannedRights(peerId: view.peerId, rights: TelegramChatBannedRights(flags: completeRights(modifiedRightsFlags), untilDate: Int32.max)) |> deliverOnMainQueue).start()) } } @@ -679,7 +679,7 @@ public func channelPermissionsController(context: AccountContext, peerId origina return state } - removePeerDisposable.set((context.peerChannelMemberCategoriesContextsManager.updateMemberBannedRights(account: context.account, peerId: peerId, memberId: memberId, bannedRights: nil) + removePeerDisposable.set((context.peerChannelMemberCategoriesContextsManager.updateMemberBannedRights(engine: context.engine, peerId: peerId, memberId: memberId, bannedRights: nil) |> deliverOnMainQueue).start(error: { _ in updateState { state in var state = state @@ -762,7 +762,7 @@ public func channelPermissionsController(context: AccountContext, peerId origina } pushControllerImpl?(controller) }, openChannelExample: { - resolveDisposable.set((resolvePeerByName(account: context.account, name: "durov") |> deliverOnMainQueue).start(next: { peerId in + resolveDisposable.set((context.engine.peers.resolvePeerByName(name: "durov") |> deliverOnMainQueue).start(next: { peerId in if let peerId = peerId { navigateToChatControllerImpl?(peerId) } @@ -779,7 +779,7 @@ public func channelPermissionsController(context: AccountContext, peerId origina } let state = stateValue.with { $0 } if let modifiedSlowmodeTimeout = state.modifiedSlowmodeTimeout { - updateDefaultRightsDisposable.set(updateChannelSlowModeInteractively(postbox: context.account.postbox, network: context.account.network, accountStateManager: context.account.stateManager, peerId: view.peerId, timeout: modifiedSlowmodeTimeout == 0 ? nil : value).start()) + updateDefaultRightsDisposable.set(context.engine.peers.updateChannelSlowModeInteractively(peerId: view.peerId, timeout: modifiedSlowmodeTimeout == 0 ? nil : value).start()) } } else if let _ = view.peers[view.peerId] as? TelegramGroup, let _ = view.cachedData as? CachedGroupData { updateState { state in @@ -797,7 +797,7 @@ public func channelPermissionsController(context: AccountContext, peerId origina let progress = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: nil)) presentControllerImpl?(progress, nil) - let signal = convertGroupToSupergroup(account: context.account, peerId: view.peerId) + let signal = context.engine.peers.convertGroupToSupergroup(peerId: view.peerId) |> mapError { error -> UpdateChannelSlowModeError in switch error { case .tooManyChannels: @@ -815,7 +815,7 @@ public func channelPermissionsController(context: AccountContext, peerId origina } } |> mapToSignal { upgradedPeerId -> Signal in - return updateChannelSlowModeInteractively(postbox: context.account.postbox, network: context.account.network, accountStateManager: context.account.stateManager, peerId: upgradedPeerId, timeout: modifiedSlowmodeTimeout == 0 ? nil : value) + return context.engine.peers.updateChannelSlowModeInteractively(peerId: upgradedPeerId, timeout: modifiedSlowmodeTimeout == 0 ? nil : value) |> mapToSignal { _ -> Signal in return .complete() } diff --git a/submodules/PeerInfoUI/Sources/ChannelVisibilityController.swift b/submodules/PeerInfoUI/Sources/ChannelVisibilityController.swift index f523a1e944..d73f4ef204 100644 --- a/submodules/PeerInfoUI/Sources/ChannelVisibilityController.swift +++ b/submodules/PeerInfoUI/Sources/ChannelVisibilityController.swift @@ -499,8 +499,8 @@ private func channelVisibilityControllerEntries(presentationData: PresentationDa break case .initialSetup, .generic: entries.append(.typeHeader(presentationData.theme, isGroup ? presentationData.strings.Group_Setup_TypeHeader.uppercased() : presentationData.strings.Channel_Edit_LinkItem.uppercased())) - entries.append(.typePublic(presentationData.theme, isGroup ? presentationData.strings.Channel_Setup_TypePublic : presentationData.strings.Channel_Setup_LinkTypePublic, selectedType == .publicChannel)) - entries.append(.typePrivate(presentationData.theme, isGroup ? presentationData.strings.Channel_Setup_TypePrivate : presentationData.strings.Channel_Setup_LinkTypePrivate, selectedType == .privateChannel)) + entries.append(.typePublic(presentationData.theme, isGroup ? presentationData.strings.Group_Setup_TypePublic : presentationData.strings.Channel_Setup_LinkTypePublic, selectedType == .publicChannel)) + entries.append(.typePrivate(presentationData.theme, isGroup ? presentationData.strings.Group_Setup_TypePrivate : presentationData.strings.Channel_Setup_LinkTypePrivate, selectedType == .privateChannel)) switch selectedType { case .publicChannel: @@ -818,9 +818,9 @@ public func channelVisibilityController(context: AccountContext, peerId: PeerId, } let peersDisablingAddressNameAssignment = Promise<[Peer]?>() - peersDisablingAddressNameAssignment.set(.single(nil) |> then(channelAddressNameAssignmentAvailability(account: context.account, peerId: peerId.namespace == Namespaces.Peer.CloudChannel ? peerId : nil) |> mapToSignal { result -> Signal<[Peer]?, NoError> in + peersDisablingAddressNameAssignment.set(.single(nil) |> then(context.engine.peers.channelAddressNameAssignmentAvailability(peerId: peerId.namespace == Namespaces.Peer.CloudChannel ? peerId : nil) |> mapToSignal { result -> Signal<[Peer]?, NoError> in if case .addressNameLimitReached = result { - return adminedPublicChannels(account: context.account, scope: .all) + return context.engine.peers.adminedPublicChannels(scope: .all) |> map(Optional.init) } else { return .single([]) @@ -871,7 +871,7 @@ public func channelVisibilityController(context: AccountContext, peerId: PeerId, return state.withUpdatedEditingPublicLinkText(text) } - checkAddressNameDisposable.set((validateAddressNameInteractive(account: context.account, domain: .peer(peerId), name: text) + checkAddressNameDisposable.set((context.engine.peers.validateAddressNameInteractive(domain: .peer(peerId), name: text) |> deliverOnMainQueue).start(next: { result in updateState { state in return state.withUpdatedAddressNameValidationStatus(result) @@ -893,7 +893,7 @@ public func channelVisibilityController(context: AccountContext, peerId: PeerId, return state.withUpdatedRevokingPeerId(peerId) } - revokeAddressNameDisposable.set((updateAddressName(account: context.account, domain: .peer(peerId), name: nil) |> deliverOnMainQueue).start(error: { _ in + revokeAddressNameDisposable.set((context.engine.peers.updateAddressName(domain: .peer(peerId), name: nil) |> deliverOnMainQueue).start(error: { _ in updateState { state in return state.withUpdatedRevokingPeerId(nil) } @@ -1027,7 +1027,7 @@ public func channelVisibilityController(context: AccountContext, peerId: PeerId, } } if revoke { - revokeLinkDisposable.set((revokePeerExportedInvitation(account: context.account, peerId: peerId, link: link) |> deliverOnMainQueue).start(completed: { + revokeLinkDisposable.set((context.engine.peers.revokePeerExportedInvitation(peerId: peerId, link: link) |> deliverOnMainQueue).start(completed: { updateState { $0.withUpdatedRevokingPrivateLink(false) } @@ -1101,7 +1101,7 @@ public func channelVisibilityController(context: AccountContext, peerId: PeerId, } _ = ApplicationSpecificNotice.markAsSeenSetPublicChannelLink(accountManager: context.sharedContext.accountManager).start() - updateAddressNameDisposable.set((updateAddressName(account: context.account, domain: .peer(peerId), name: updatedAddressNameValue.isEmpty ? nil : updatedAddressNameValue) |> timeout(10, queue: Queue.mainQueue(), alternate: .fail(.generic)) + updateAddressNameDisposable.set((context.engine.peers.updateAddressName(domain: .peer(peerId), name: updatedAddressNameValue.isEmpty ? nil : updatedAddressNameValue) |> timeout(10, queue: Queue.mainQueue(), alternate: .fail(.generic)) |> deliverOnMainQueue).start(error: { _ in updateState { state in return state.withUpdatedUpdatingAddressName(false) @@ -1171,9 +1171,9 @@ public func channelVisibilityController(context: AccountContext, peerId: PeerId, } _ = ApplicationSpecificNotice.markAsSeenSetPublicChannelLink(accountManager: context.sharedContext.accountManager).start() - let signal = convertGroupToSupergroup(account: context.account, peerId: peerId) + let signal = context.engine.peers.convertGroupToSupergroup(peerId: peerId) |> mapToSignal { upgradedPeerId -> Signal in - return updateAddressName(account: context.account, domain: .peer(upgradedPeerId), name: updatedAddressNameValue.isEmpty ? nil : updatedAddressNameValue) + return context.engine.peers.updateAddressName(domain: .peer(upgradedPeerId), name: updatedAddressNameValue.isEmpty ? nil : updatedAddressNameValue) |> `catch` { _ -> Signal in return .complete() } @@ -1357,7 +1357,7 @@ public func channelVisibilityController(context: AccountContext, peerId: PeerId, context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, chatController: nil, context: context, chatLocation: .peer(peerId), keepStack: .never, animated: true)) } else { selectionController.displayProgress = true - let _ = (addChannelMembers(account: context.account, peerId: peerId, memberIds: filteredPeerIds) + let _ = (context.engine.peers.addChannelMembers(peerId: peerId, memberIds: filteredPeerIds) |> deliverOnMainQueue).start(error: { [weak selectionController] _ in guard let selectionController = selectionController, let navigationController = selectionController.navigationController as? NavigationController else { return diff --git a/submodules/PeerInfoUI/Sources/ConvertToSupergroupController.swift b/submodules/PeerInfoUI/Sources/ConvertToSupergroupController.swift index 494e9537c2..b1dbe193b9 100644 --- a/submodules/PeerInfoUI/Sources/ConvertToSupergroupController.swift +++ b/submodules/PeerInfoUI/Sources/ConvertToSupergroupController.swift @@ -149,7 +149,7 @@ public func convertToSupergroupController(context: AccountContext, peerId: PeerI } if !alreadyConverting { - convertDisposable.set((convertGroupToSupergroup(account: context.account, peerId: peerId) + convertDisposable.set((context.engine.peers.convertGroupToSupergroup(peerId: peerId) |> deliverOnMainQueue).start(next: { createdPeerId in replaceControllerImpl?(context.sharedContext.makeChatController(context: context, chatLocation: .peer(createdPeerId), subject: nil, botStart: nil, mode: .standard(previewing: false))) })) diff --git a/submodules/PeerInfoUI/Sources/DeviceContactInfoController.swift b/submodules/PeerInfoUI/Sources/DeviceContactInfoController.swift index 2fa79dc0ca..eba9713cee 100644 --- a/submodules/PeerInfoUI/Sources/DeviceContactInfoController.swift +++ b/submodules/PeerInfoUI/Sources/DeviceContactInfoController.swift @@ -396,7 +396,7 @@ private enum DeviceContactInfoEntry: ItemListNodeEntry { func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { let arguments = arguments as! DeviceContactInfoControllerArguments switch self { - case let .info(_, _, strings, dateTimeFormat, peer, state, jobSummary, isPlain): + case let .info(_, _, _, dateTimeFormat, peer, state, jobSummary, _): 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: { @@ -647,8 +647,13 @@ private func deviceContactInfoEntries(account: Account, presentationData: Presen let jobSummary = jobComponents.joined(separator: " — ") let isOrganization = personName.0.isEmpty && personName.1.isEmpty && !contactData.organization.isEmpty + + var firstName: String = isOrganization ? contactData.organization : personName.0 + if firstName.isEmpty { + firstName = presentationData.strings.Message_Contact + } - entries.append(.info(entries.count, presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, peer: peer ?? TelegramUser(id: PeerId(namespace: -1, id: 0), accessHash: nil, firstName: isOrganization ? contactData.organization : personName.0, lastName: isOrganization ? nil : personName.1, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: []), state: ItemListAvatarAndNameInfoItemState(editingName: editingName, updatingName: nil), job: isOrganization ? nil : jobSummary, isPlain: !isShare)) + entries.append(.info(entries.count, presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, peer: peer ?? TelegramUser(id: PeerId(namespace: .max, id: PeerId.Id._internalFromInt32Value(0)), accessHash: nil, firstName: firstName, lastName: isOrganization ? nil : personName.1, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: []), state: ItemListAvatarAndNameInfoItemState(editingName: editingName, updatingName: nil), job: isOrganization ? nil : jobSummary, isPlain: !isShare)) if !selecting { if let _ = peer { @@ -1099,7 +1104,7 @@ public func deviceContactInfoController(context: AccountContext, subject: Device switch subject { case let .create(peer, _, share, shareViaException, _): if share, filteredPhoneNumbers.count <= 1, let peer = peer { - addContactDisposable.set((addContactInteractively(account: context.account, peerId: peer.id, firstName: composedContactData.basicData.firstName, lastName: composedContactData.basicData.lastName, phoneNumber: filteredPhoneNumbers.first?.value ?? "", addToPrivacyExceptions: shareViaException && addToPrivacyExceptions) + addContactDisposable.set((context.engine.contacts.addContactInteractively(peerId: peer.id, firstName: composedContactData.basicData.firstName, lastName: composedContactData.basicData.lastName, phoneNumber: filteredPhoneNumbers.first?.value ?? "", addToPrivacyExceptions: shareViaException && addToPrivacyExceptions) |> deliverOnMainQueue).start(error: { _ in presentControllerImpl?(textAlertController(context: context, title: nil, text: presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil) }, completed: { @@ -1133,7 +1138,7 @@ public func deviceContactInfoController(context: AccountContext, subject: Device switch subject { case let .create(peer, _, share, shareViaException, _): if share, let peer = peer { - return addContactInteractively(account: context.account, peerId: peer.id, firstName: composedContactData.basicData.firstName, lastName: composedContactData.basicData.lastName, phoneNumber: filteredPhoneNumbers.first?.value ?? "", addToPrivacyExceptions: shareViaException && addToPrivacyExceptions) + return context.engine.contacts.addContactInteractively(peerId: peer.id, firstName: composedContactData.basicData.firstName, lastName: composedContactData.basicData.lastName, phoneNumber: filteredPhoneNumbers.first?.value ?? "", addToPrivacyExceptions: shareViaException && addToPrivacyExceptions) |> mapToSignal { _ -> Signal<(DeviceContactStableId, DeviceContactExtendedData, Peer?)?, AddContactError> in return .complete() } @@ -1148,7 +1153,7 @@ public func deviceContactInfoController(context: AccountContext, subject: Device break } - return importContact(account: context.account, firstName: composedContactData.basicData.firstName, lastName: composedContactData.basicData.lastName, phoneNumber: filteredPhoneNumbers[0].value) + return context.engine.contacts.importContact(firstName: composedContactData.basicData.firstName, lastName: composedContactData.basicData.lastName, phoneNumber: filteredPhoneNumbers[0].value) |> castError(AddContactError.self) |> mapToSignal { peerId -> Signal<(DeviceContactStableId, DeviceContactExtendedData, Peer?)?, AddContactError> in if let peerId = peerId { @@ -1322,8 +1327,8 @@ private func addContactToExisting(context: AccountContext, parentController: Vie contactsController.navigationPresentation = .modal (parentController.navigationController as? NavigationController)?.pushViewController(contactsController) let _ = (contactsController.result - |> deliverOnMainQueue).start(next: { peer in - if let (peer, _) = peer { + |> deliverOnMainQueue).start(next: { result in + if let (peers, _) = result, let peer = peers.first { let dataSignal: Signal<(Peer?, DeviceContactStableId?), NoError> switch peer { case let .peer(contact, _, _): diff --git a/submodules/PeerInfoUI/Sources/GroupInfoSearchNavigationContentNode.swift b/submodules/PeerInfoUI/Sources/GroupInfoSearchNavigationContentNode.swift index 5a1a9ede36..42168c45cc 100644 --- a/submodules/PeerInfoUI/Sources/GroupInfoSearchNavigationContentNode.swift +++ b/submodules/PeerInfoUI/Sources/GroupInfoSearchNavigationContentNode.swift @@ -34,7 +34,7 @@ final class GroupInfoSearchNavigationContentNode: NavigationBarContentNode, Item self.cancel = cancel - self.searchBar = SearchBarNode(theme: SearchBarNodeTheme(theme: theme, hasSeparator: false), strings: strings, fieldStyle: .modern) + self.searchBar = SearchBarNode(theme: SearchBarNodeTheme(theme: theme, hasSeparator: false), strings: strings, fieldStyle: .modern, displayBackground: false) super.init() diff --git a/submodules/PeerInfoUI/Sources/GroupPreHistorySetupController.swift b/submodules/PeerInfoUI/Sources/GroupPreHistorySetupController.swift index f16c20505d..7468d566f6 100644 --- a/submodules/PeerInfoUI/Sources/GroupPreHistorySetupController.swift +++ b/submodules/PeerInfoUI/Sources/GroupPreHistorySetupController.swift @@ -161,9 +161,9 @@ 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) + let signal = context.engine.peers.convertGroupToSupergroup(peerId: peerId) |> mapToSignal { upgradedPeerId -> Signal in - return updateChannelHistoryAvailabilitySettingsInteractively(postbox: context.account.postbox, network: context.account.network, accountStateManager: context.account.stateManager, peerId: upgradedPeerId, historyAvailableForNewMembers: value) + return context.engine.peers.updateChannelHistoryAvailabilitySettingsInteractively(peerId: upgradedPeerId, historyAvailableForNewMembers: value) |> `catch` { _ -> Signal in return .complete() } @@ -190,7 +190,7 @@ public func groupPreHistorySetupController(context: AccountContext, peerId: Peer } })) } else { - applyDisposable.set((updateChannelHistoryAvailabilitySettingsInteractively(postbox: context.account.postbox, network: context.account.network, accountStateManager: context.account.stateManager, peerId: peerId, historyAvailableForNewMembers: value) + applyDisposable.set((context.engine.peers.updateChannelHistoryAvailabilitySettingsInteractively(peerId: peerId, historyAvailableForNewMembers: value) |> deliverOnMainQueue).start(completed: { dismissImpl?() })) diff --git a/submodules/PeerInfoUI/Sources/GroupStickerPackSetupController.swift b/submodules/PeerInfoUI/Sources/GroupStickerPackSetupController.swift index c79387453e..8c465147ca 100644 --- a/submodules/PeerInfoUI/Sources/GroupStickerPackSetupController.swift +++ b/submodules/PeerInfoUI/Sources/GroupStickerPackSetupController.swift @@ -323,7 +323,7 @@ public func groupStickerPackSetupController(context: AccountContext, peerId: Pee let initialData = Promise() if let currentPackInfo = currentPackInfo { - initialData.set(cachedStickerPack(postbox: context.account.postbox, network: context.account.network, reference: .id(id: currentPackInfo.id.id, accessHash: currentPackInfo.accessHash), forceRemote: false) + initialData.set(context.engine.stickers.cachedStickerPack(reference: .id(id: currentPackInfo.id.id, accessHash: currentPackInfo.accessHash), forceRemote: false) |> map { result -> InitialStickerPackData? in switch result { case .none: @@ -363,7 +363,7 @@ public func groupStickerPackSetupController(context: AccountContext, peerId: Pee } } return .single((searchText, .searching)) - |> then((loadedStickerPack(postbox: context.account.postbox, network: context.account.network, reference: .name(searchText.lowercased()), forceActualized: false) |> delay(0.3, queue: Queue.concurrentDefaultQueue())) + |> then((context.engine.stickers.loadedStickerPack(reference: .name(searchText.lowercased()), forceActualized: false) |> delay(0.3, queue: Queue.concurrentDefaultQueue())) |> mapToSignal { value -> Signal<(String, GroupStickerPackSearchState), NoError> in switch value { case .fetching: @@ -402,7 +402,7 @@ public func groupStickerPackSetupController(context: AccountContext, peerId: Pee }, updateSearchText: { text in searchText.set(text) }, openStickersBot: { - resolveDisposable.set((resolvePeerByName(account: context.account, name: "stickers") |> deliverOnMainQueue).start(next: { peerId in + resolveDisposable.set((context.engine.peers.resolvePeerByName(name: "stickers") |> deliverOnMainQueue).start(next: { peerId in if let peerId = peerId { dismissImpl?() navigateToChatControllerImpl?(peerId) @@ -448,7 +448,7 @@ public func groupStickerPackSetupController(context: AccountContext, peerId: Pee state.isSaving = true return state } - saveDisposable.set((updateGroupSpecificStickerset(postbox: context.account.postbox, network: context.account.network, peerId: peerId, info: info) + saveDisposable.set((context.engine.peers.updateGroupSpecificStickerset(peerId: peerId, info: info) |> deliverOnMainQueue).start(error: { _ in updateState { state in var state = state diff --git a/submodules/PeerInfoUI/Sources/GroupsInCommonController.swift b/submodules/PeerInfoUI/Sources/GroupsInCommonController.swift deleted file mode 100644 index 70c6a25db9..0000000000 --- a/submodules/PeerInfoUI/Sources/GroupsInCommonController.swift +++ /dev/null @@ -1,244 +0,0 @@ -import Foundation -import UIKit -import AsyncDisplayKit -import Display -import SwiftSignalKit -import Postbox -import TelegramCore -import SyncCore -import TelegramPresentationData -import TelegramUIPreferences -import ItemListUI -import PresentationDataUtils -import AccountContext -import ItemListPeerItem -import ContextUI - -private final class GroupsInCommonControllerArguments { - let context: AccountContext - - let openPeer: (PeerId) -> Void - let contextAction: (Peer, ASDisplayNode, ContextGesture?) -> Void - - init(context: AccountContext, openPeer: @escaping (PeerId) -> Void, contextAction: @escaping (Peer, ASDisplayNode, ContextGesture?) -> Void) { - self.context = context - self.openPeer = openPeer - self.contextAction = contextAction - } -} - -private enum GroupsInCommonSection: Int32 { - case peers -} - -private enum GroupsInCommonEntryStableId: Hashable { - case peer(PeerId) -} - -private enum GroupsInCommonEntry: ItemListNodeEntry { - case peerItem(Int32, PresentationTheme, PresentationStrings, PresentationDateTimeFormat, PresentationPersonNameOrder, Peer) - - var section: ItemListSectionId { - switch self { - case .peerItem: - return GroupsInCommonSection.peers.rawValue - } - } - - var stableId: GroupsInCommonEntryStableId { - switch self { - case let .peerItem(_, _, _, _, _, peer): - return .peer(peer.id) - } - } - - static func ==(lhs: GroupsInCommonEntry, rhs: GroupsInCommonEntry) -> Bool { - switch lhs { - case let .peerItem(lhsIndex, lhsTheme, lhsStrings, lhsDateTimeFormat, lhsNameOrder, lhsPeer): - if case let .peerItem(rhsIndex, rhsTheme, rhsStrings, rhsDateTimeFormat, rhsNameOrder, rhsPeer) = rhs { - if lhsIndex != rhsIndex { - return false - } - if lhsTheme !== rhsTheme { - return false - } - if lhsStrings !== rhsStrings { - return false - } - if lhsDateTimeFormat != rhsDateTimeFormat { - return false - } - if !lhsPeer.isEqual(rhsPeer) { - return false - } - if lhsNameOrder != rhsNameOrder { - return false - } - return true - } else { - return false - } - } - } - - static func <(lhs: GroupsInCommonEntry, rhs: GroupsInCommonEntry) -> Bool { - switch lhs { - case let .peerItem(index, _, _, _, _, _): - switch rhs { - case let .peerItem(rhsIndex, _, _, _, _, _): - return index < rhsIndex - } - } - } - - func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { - let arguments = arguments as! GroupsInCommonControllerArguments - switch self { - case let .peerItem(_, theme, strings, dateTimeFormat, nameDisplayOrder, peer): - 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 - }, contextAction: { node, gesture in - arguments.contextAction(peer, node, gesture) - }) - } - } -} - -private struct GroupsInCommonControllerState: Equatable { - static func ==(lhs: GroupsInCommonControllerState, rhs: GroupsInCommonControllerState) -> Bool { - return true - } -} - -private func groupsInCommonControllerEntries(presentationData: PresentationData, state: GroupsInCommonControllerState, peers: [Peer]?) -> [GroupsInCommonEntry] { - var entries: [GroupsInCommonEntry] = [] - - if let peers = peers { - var index: Int32 = 0 - for peer in peers { - entries.append(.peerItem(index, presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, presentationData.nameDisplayOrder, peer)) - index += 1 - } - } - - return entries -} - -public func groupsInCommonController(context: AccountContext, peerId: PeerId) -> ViewController { - let statePromise = ValuePromise(GroupsInCommonControllerState(), ignoreRepeated: true) - let stateValue = Atomic(value: GroupsInCommonControllerState()) - let updateState: ((GroupsInCommonControllerState) -> GroupsInCommonControllerState) -> Void = { f in - statePromise.set(stateValue.modify { f($0) }) - } - - let actionsDisposable = DisposableSet() - - let peersPromise = Promise<[Peer]?>(nil) - - var pushControllerImpl: ((ViewController) -> Void)? - var getNavigationControllerImpl: (() -> NavigationController?)? - - var contextActionImpl: ((Peer, ASDisplayNode, ContextGesture?) -> Void)? - - let arguments = GroupsInCommonControllerArguments(context: context, openPeer: { memberId in - guard let navigationController = getNavigationControllerImpl?() else { - return - } - context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(memberId), animated: true)) - }, contextAction: { peer, node, gesture in - contextActionImpl?(peer, node, gesture) - }) - - let peersSignal: Signal<[Peer]?, NoError> = .single(nil) |> then(groupsInCommon(account: context.account, peerId: peerId) |> mapToSignal { peerIds -> Signal<[Peer], NoError> in - return context.account.postbox.transaction { transaction -> [Peer] in - var result: [Peer] = [] - for id in peerIds { - if let peer = transaction.getPeer(id.id) { - result.append(peer) - } - } - return result - } - } - |> map(Optional.init)) - - peersPromise.set(peersSignal) - - var previousPeers: [Peer]? - - let signal = combineLatest(context.sharedContext.presentationData, statePromise.get(), peersPromise.get()) - |> deliverOnMainQueue - |> map { presentationData, state, peers -> (ItemListControllerState, (ItemListNodeState, Any)) in - var emptyStateItem: ItemListControllerEmptyStateItem? - if peers == nil { - emptyStateItem = ItemListLoadingIndicatorEmptyStateItem(theme: presentationData.theme) - } - - let previous = previousPeers - previousPeers = peers - - 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 { - actionsDisposable.dispose() - } - - let controller = ItemListController(context: context, state: signal) - pushControllerImpl = { [weak controller] c in - if let controller = controller { - (controller.navigationController as? NavigationController)?.pushViewController(c) - } - } - getNavigationControllerImpl = { [weak controller] in - return controller?.navigationController as? NavigationController - } - contextActionImpl = { [weak controller] peer, node, gesture in - guard let controller = controller else { - return - } - 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 items: [ContextMenuItem] = [ - .action(ContextMenuActionItem(text: presentationData.strings.Conversation_LinkDialogOpen, icon: { _ in nil }, action: { _, f in - f(.dismissWithoutContent) - arguments.openPeer(peer.id) - })) - ] - 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 -} - -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/PeerInfoUI/Sources/OldChannelsController.swift b/submodules/PeerInfoUI/Sources/OldChannelsController.swift index 8b1248bb1a..3a4edeb23e 100644 --- a/submodules/PeerInfoUI/Sources/OldChannelsController.swift +++ b/submodules/PeerInfoUI/Sources/OldChannelsController.swift @@ -221,7 +221,7 @@ private final class OldChannelsActionPanelNode: ASDisplayNode { super.init() - self.backgroundColor = presentationData.theme.rootController.navigationBar.backgroundColor + self.backgroundColor = presentationData.theme.rootController.navigationBar.opaqueBackgroundColor self.addSubnode(self.separatorNode) self.addSubnode(self.buttonNode) @@ -233,7 +233,7 @@ private final class OldChannelsActionPanelNode: ASDisplayNode { func updatePresentationData(_ presentationData: ItemListPresentationData) { self.separatorNode.backgroundColor = presentationData.theme.rootController.navigationBar.separatorColor - self.backgroundColor = presentationData.theme.rootController.navigationBar.backgroundColor + self.backgroundColor = presentationData.theme.rootController.navigationBar.opaqueBackgroundColor } func updateLayout(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) -> CGFloat { @@ -384,7 +384,7 @@ public func oldChannelsController(context: AccountContext, intent: OldChannelsCo let peersSignal: Signal<[InactiveChannel]?, NoError> = .single(nil) |> then( - inactiveChannelList(network: context.account.network) + context.engine.peers.inactiveChannelList() |> map { peers -> [InactiveChannel]? in return peers.sorted(by: { lhs, rhs in return lhs.lastActivityDate < rhs.lastActivityDate @@ -455,21 +455,21 @@ public func oldChannelsController(context: AccountContext, intent: OldChannelsCo let state = stateValue.with { $0 } let _ = (peersPromise.get() |> take(1) - |> mapToSignal { peers in + |> mapToSignal { peers -> Signal in + let peers = peers ?? [] 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) + 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 + }) } } } } + |> ignoreValues + |> then(context.engine.peers.removePeerChats(peerIds: Array(peers.map(\.peer.id)))) } |> deliverOnMainQueue).start(completed: { completed(true) diff --git a/submodules/PeerInfoUI/Sources/OldChannelsSearch.swift b/submodules/PeerInfoUI/Sources/OldChannelsSearch.swift index 4e27823a2f..685ca8cfc0 100644 --- a/submodules/PeerInfoUI/Sources/OldChannelsSearch.swift +++ b/submodules/PeerInfoUI/Sources/OldChannelsSearch.swift @@ -263,7 +263,7 @@ private final class OldChannelsSearchContainerNode: SearchDisplayControllerConte } }) - self.listNode.beganInteractiveDragging = { [weak self] in + self.listNode.beganInteractiveDragging = { [weak self] _ in self?.dismissInput?() } } diff --git a/submodules/PeerInfoUI/Sources/PeerAutoremoveSetupScreen.swift b/submodules/PeerInfoUI/Sources/PeerAutoremoveSetupScreen.swift index 3840254013..6767eab567 100644 --- a/submodules/PeerInfoUI/Sources/PeerAutoremoveSetupScreen.swift +++ b/submodules/PeerInfoUI/Sources/PeerAutoremoveSetupScreen.swift @@ -212,7 +212,7 @@ public func peerAutoremoveSetupScreen(context: AccountContext, peerId: PeerId, c } if updated { - let signal = setChatMessageAutoremoveTimeoutInteractively(account: context.account, peerId: peerId, timeout: resolvedValue) + let signal = context.engine.peers.setChatMessageAutoremoveTimeoutInteractively(peerId: peerId, timeout: resolvedValue) |> deliverOnMainQueue applyDisposable.set((signal diff --git a/submodules/PeerInfoUI/Sources/PeerReportController.swift b/submodules/PeerInfoUI/Sources/PeerReportController.swift index 146ce5ddbe..d13701fb43 100644 --- a/submodules/PeerInfoUI/Sources/PeerReportController.swift +++ b/submodules/PeerInfoUI/Sources/PeerReportController.swift @@ -33,7 +33,7 @@ public enum PeerReportOption { case other } -public func presentPeerReportOptions(context: AccountContext, parent: ViewController, contextController: ContextController?, backAction: ((ContextController) -> Void)? = nil, subject: PeerReportSubject, options: [PeerReportOption] = [.spam, .violence, .pornography, .childAbuse, .copyright, .other], passthrough: Bool = false, completion: @escaping (ReportReason?, Bool) -> Void) { +public func presentPeerReportOptions(context: AccountContext, parent: ViewController, contextController: ContextControllerProtocol?, backAction: ((ContextControllerProtocol) -> Void)? = nil, subject: PeerReportSubject, options: [PeerReportOption] = [.spam, .violence, .pornography, .childAbuse, .copyright, .other], passthrough: Bool = false, completion: @escaping (ReportReason?, Bool) -> Void) { if let contextController = contextController { let presentationData = context.sharedContext.currentPresentationData.with { $0 } var items: [ContextMenuItem] = [] @@ -107,19 +107,19 @@ public func presentPeerReportOptions(context: AccountContext, parent: ViewContro } else { switch subject { case let .peer(peerId): - let _ = (reportPeer(account: context.account, peerId: peerId, reason: reportReason, message: "") + let _ = (context.engine.peers.reportPeer(peerId: peerId, reason: reportReason, message: "") |> deliverOnMainQueue).start(completed: { displaySuccess() completion(nil, false) }) case let .messages(messageIds): - let _ = (reportPeerMessages(account: context.account, messageIds: messageIds, reason: reportReason, message: "") + let _ = (context.engine.peers.reportPeerMessages(messageIds: messageIds, reason: reportReason, message: "") |> deliverOnMainQueue).start(completed: { displaySuccess() completion(nil, false) }) case let .profilePhoto(peerId, photoId): - let _ = (reportPeerPhoto(account: context.account, peerId: peerId, reason: reportReason, message: "") + let _ = (context.engine.peers.reportPeerPhoto(peerId: peerId, reason: reportReason, message: "") |> deliverOnMainQueue).start(completed: { displaySuccess() completion(nil, false) @@ -163,7 +163,7 @@ public func presentPeerReportOptions(context: AccountContext, parent: ViewContro } contextController.setItems(.single(items)) } else { - contextController?.dismiss() + contextController?.dismiss(completion: nil) parent.view.endEditing(true) parent.present(peerReportOptionsController(context: context, subject: subject, passthrough: passthrough, present: { [weak parent] c, a in parent?.present(c, in: .window(.root), with: a) @@ -233,19 +233,19 @@ public func peerReportOptionsController(context: AccountContext, subject: PeerRe } else { switch subject { case let .peer(peerId): - let _ = (reportPeer(account: context.account, peerId: peerId, reason: reportReason, message: message) + let _ = (context.engine.peers.reportPeer(peerId: peerId, reason: reportReason, message: message) |> deliverOnMainQueue).start(completed: { displaySuccess() completion(nil, true) }) case let .messages(messageIds): - let _ = (reportPeerMessages(account: context.account, messageIds: messageIds, reason: reportReason, message: message) + let _ = (context.engine.peers.reportPeerMessages(messageIds: messageIds, reason: reportReason, message: message) |> deliverOnMainQueue).start(completed: { displaySuccess() completion(nil, true) }) case let .profilePhoto(peerId, photoId): - let _ = (reportPeerPhoto(account: context.account, peerId: peerId, reason: reportReason, message: message) + let _ = (context.engine.peers.reportPeerPhoto(peerId: peerId, reason: reportReason, message: message) |> deliverOnMainQueue).start(completed: { displaySuccess() completion(nil, true) diff --git a/submodules/PeerInfoUI/Sources/SecretChatKeyController.swift b/submodules/PeerInfoUI/Sources/SecretChatKeyController.swift index fb309cd499..a5a15bbc79 100644 --- a/submodules/PeerInfoUI/Sources/SecretChatKeyController.swift +++ b/submodules/PeerInfoUI/Sources/SecretChatKeyController.swift @@ -48,6 +48,6 @@ public final class SecretChatKeyController: 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.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition) } } diff --git a/submodules/PeerInfoUI/Sources/UserInfoController.swift b/submodules/PeerInfoUI/Sources/UserInfoController.swift index 1c8c012ebc..07faaa9665 100644 --- a/submodules/PeerInfoUI/Sources/UserInfoController.swift +++ b/submodules/PeerInfoUI/Sources/UserInfoController.swift @@ -29,744 +29,6 @@ import LocalizedPeerData import PhoneNumberFormat import TelegramIntents -private final class UserInfoControllerArguments { - let context: AccountContext - let avatarAndNameInfoContext: ItemListAvatarAndNameInfoItemContext - let updateEditingName: (ItemListAvatarAndNameInfoItemName) -> Void - let tapAvatarAction: () -> Void - let openChat: () -> Void - let addContact: () -> Void - let shareContact: () -> Void - let shareMyContact: () -> Void - let startSecretChat: () -> Void - let changeNotificationMuteSettings: () -> Void - let openSharedMedia: () -> Void - let openGroupsInCommon: () -> Void - let updatePeerBlocked: (Bool) -> Void - let deleteContact: () -> Void - let displayUsernameContextMenu: (String) -> Void - let displayCopyContextMenu: (UserInfoEntryTag, String) -> Void - let call: () -> Void - let openCallMenu: (String) -> Void - let requestPhoneNumber: () -> Void - let aboutLinkAction: (TextLinkItemActionType, TextLinkItem) -> Void - let displayAboutContextMenu: (String) -> Void - let openEncryptionKey: (SecretChatKeyFingerprint) -> Void - let addBotToGroup: () -> Void - let shareBot: () -> Void - let botSettings: () -> Void - let botHelp: () -> Void - let botPrivacy: () -> Void - let report: () -> Void - - 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 - self.openChat = openChat - self.addContact = addContact - - self.shareContact = shareContact - self.shareMyContact = shareMyContact - self.startSecretChat = startSecretChat - self.changeNotificationMuteSettings = changeNotificationMuteSettings - self.openSharedMedia = openSharedMedia - self.openGroupsInCommon = openGroupsInCommon - self.updatePeerBlocked = updatePeerBlocked - self.deleteContact = deleteContact - self.displayUsernameContextMenu = displayUsernameContextMenu - self.displayCopyContextMenu = displayCopyContextMenu - self.call = call - self.openCallMenu = openCallMenu - self.requestPhoneNumber = requestPhoneNumber - self.aboutLinkAction = aboutLinkAction - self.displayAboutContextMenu = displayAboutContextMenu - self.openEncryptionKey = openEncryptionKey - self.addBotToGroup = addBotToGroup - self.shareBot = shareBot - self.botSettings = botSettings - self.botHelp = botHelp - self.botPrivacy = botPrivacy - self.report = report - } -} - -private enum UserInfoSection: ItemListSectionId { - case info - case actions - case sharedMediaAndNotifications - case bot - case block -} - -private enum UserInfoEntryTag { - case about - case phoneNumber - case username -} - -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 enum UserInfoEntry: ItemListNodeEntry { - case info(PresentationTheme, PresentationStrings, PresentationDateTimeFormat, peer: Peer?, presence: PeerPresence?, cachedData: CachedPeerData?, state: ItemListAvatarAndNameInfoItemState, displayCall: Bool) - case calls(PresentationTheme, PresentationStrings, PresentationDateTimeFormat, messages: [Message]) - case about(PresentationTheme, Peer, String, String) - case phoneNumber(PresentationTheme, Int, String, String, Bool) - case requestPhoneNumber(PresentationTheme, String, String) - case userName(PresentationTheme, String, String) - case sendMessage(PresentationTheme, String) - case addContact(PresentationTheme, String) - case shareContact(PresentationTheme, String) - case shareMyContact(PresentationTheme, String) - case startSecretChat(PresentationTheme, String) - case sharedMedia(PresentationTheme, String) - case notifications(PresentationTheme, String, String) - case groupsInCommon(PresentationTheme, String, String) - case secretEncryptionKey(PresentationTheme, String, SecretChatKeyFingerprint) - case botAddToGroup(PresentationTheme, String) - case botShare(PresentationTheme, String) - case botSettings(PresentationTheme, String) - case botHelp(PresentationTheme, String) - case botPrivacy(PresentationTheme, String) - case botReport(PresentationTheme, String) - case block(PresentationTheme, String, DestructiveUserInfoAction) - - var section: ItemListSectionId { - switch self { - case .info, .calls, .about, .phoneNumber, .requestPhoneNumber, .userName: - return UserInfoSection.info.rawValue - case .sendMessage, .addContact, .shareContact, .shareMyContact, .startSecretChat, .botAddToGroup, .botShare: - return UserInfoSection.actions.rawValue - case .botSettings, .botHelp, .botPrivacy: - return UserInfoSection.bot.rawValue - case .sharedMedia, .notifications, .groupsInCommon, .secretEncryptionKey: - return UserInfoSection.sharedMediaAndNotifications.rawValue - case .botReport, .block: - return UserInfoSection.block.rawValue - } - } - - var stableId: Int { - return self.sortIndex - } - - static func ==(lhs: UserInfoEntry, rhs: UserInfoEntry) -> Bool { - switch lhs { - case let .info(lhsTheme, lhsStrings, lhsDateTimeFormat, lhsPeer, lhsPresence, lhsCachedData, lhsState, lhsDisplayCall): - switch rhs { - case let .info(rhsTheme, rhsStrings, rhsDateTimeFormat, rhsPeer, rhsPresence, rhsCachedData, rhsState, rhsDisplayCall): - if lhsTheme !== rhsTheme { - return false - } - if lhsStrings !== rhsStrings { - return false - } - if lhsDateTimeFormat != rhsDateTimeFormat { - return false - } - if let lhsPeer = lhsPeer, let rhsPeer = rhsPeer { - if !lhsPeer.isEqual(rhsPeer) { - return false - } - } else if (lhsPeer != nil) != (rhsPeer != nil) { - return false - } - if let lhsPresence = lhsPresence, let rhsPresence = rhsPresence { - if !lhsPresence.isEqual(to: rhsPresence) { - return false - } - } else if (lhsPresence != nil) != (rhsPresence != nil) { - return false - } - if let lhsCachedData = lhsCachedData, let rhsCachedData = rhsCachedData { - if !lhsCachedData.isEqual(to: rhsCachedData) { - return false - } - } else if (lhsCachedData != nil) != (rhsCachedData != nil) { - return false - } - if lhsState != rhsState { - return false - } - if lhsDisplayCall != rhsDisplayCall { - return false - } - return true - default: - return false - } - case let .calls(lhsTheme, lhsStrings, lhsDateTimeFormat, lhsMessages): - if case let .calls(rhsTheme, rhsStrings, rhsDateTimeFormat, rhsMessages) = rhs, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsDateTimeFormat == rhsDateTimeFormat { - if lhsMessages.count != rhsMessages.count { - return false - } - for i in 0 ..< lhsMessages.count { - if !areMessagesEqual(lhsMessages[i], rhsMessages[i]) { - return false - } - } - return true - } else { - return false - } - case let .about(lhsTheme, lhsPeer, lhsText, lhsValue): - if case let .about(rhsTheme, rhsPeer, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsPeer.isEqual(rhsPeer), lhsText == rhsText, lhsValue == rhsValue { - return true - } else { - return false - } - case let .phoneNumber(lhsTheme, lhsIndex, lhsLabel, lhsValue, lhsMain): - if case let .phoneNumber(rhsTheme, rhsIndex, rhsLabel, rhsValue, rhsMain) = rhs, lhsTheme === rhsTheme, lhsIndex == rhsIndex, lhsLabel == rhsLabel, lhsValue == rhsValue, lhsMain == rhsMain { - return true - } else { - return false - } - case let .requestPhoneNumber(lhsTheme, lhsLabel, lhsValue): - if case let .requestPhoneNumber(rhsTheme, rhsLabel, rhsValue) = rhs, lhsTheme === rhsTheme, lhsLabel == rhsLabel, lhsValue == rhsValue { - return true - } else { - return false - } - case let .userName(lhsTheme, lhsText, lhsValue): - if case let .userName(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { - return true - } else { - return false - } - case let .sendMessage(lhsTheme, lhsText): - if case let .sendMessage(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { - return true - } else { - return false - } - case let .addContact(lhsTheme, lhsText): - if case let .addContact(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { - return true - } else { - return false - } - case let .shareContact(lhsTheme, lhsText): - if case let .shareContact(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { - return true - } else { - return false - } - case let .shareMyContact(lhsTheme, lhsText): - if case let .shareMyContact(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { - return true - } else { - return false - } - case let .startSecretChat(lhsTheme, lhsText): - if case let .startSecretChat(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { - return true - } else { - return false - } - case let .sharedMedia(lhsTheme, lhsText): - if case let .sharedMedia(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { - return true - } else { - return false - } - case let .notifications(lhsTheme, lhsText, lhsValue): - if case let .notifications(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { - return true - } else { - return false - } - case let .groupsInCommon(lhsTheme, lhsText, lhsValue): - if case let .groupsInCommon(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { - return true - } else { - return false - } - case let .secretEncryptionKey(lhsTheme, lhsText, lhsValue): - if case let .secretEncryptionKey(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { - return true - } else { - return false - } - case let .botAddToGroup(lhsTheme, lhsText): - if case let .botAddToGroup(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { - return true - } else { - return false - } - case let .botShare(lhsTheme, lhsText): - if case let .botShare(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { - return true - } else { - return false - } - case let .botSettings(lhsTheme, lhsText): - if case let .botSettings(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { - return true - } else { - return false - } - case let .botHelp(lhsTheme, lhsText): - if case let .botHelp(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { - return true - } else { - return false - } - case let .botPrivacy(lhsTheme, lhsText): - if case let .botPrivacy(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { - return true - } else { - return false - } - case let .botReport(lhsTheme, lhsText): - if case let .botReport(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { - return true - } else { - return false - } - case let .block(lhsTheme, lhsText, lhsAction): - if case let .block(rhsTheme, rhsText, rhsAction) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsAction == rhsAction { - return true - } else { - return false - } - } - } - - private var sortIndex: Int { - switch self { - case .info: - return 0 - case .calls: - return 1 - case let .phoneNumber(_, index, _, _, _): - return 2 + index - case .requestPhoneNumber: - return 998 - case .about: - return 999 - case .userName: - return 1000 - case .sendMessage: - return 1001 - case .addContact: - return 1002 - case .shareContact: - return 1003 - case .shareMyContact: - return 1004 - case .startSecretChat: - return 1005 - case .botAddToGroup: - return 1006 - case .botShare: - return 1007 - case .botSettings: - return 1008 - case .botHelp: - return 1009 - case .botPrivacy: - return 1010 - case .sharedMedia: - return 1011 - case .notifications: - return 1012 - case .secretEncryptionKey: - return 1014 - case .groupsInCommon: - return 1015 - case .botReport: - return 1016 - case .block: - return 1017 - } - } - - static func <(lhs: UserInfoEntry, rhs: UserInfoEntry) -> Bool { - return lhs.sortIndex < rhs.sortIndex - } - - 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(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() - }, context: arguments.avatarAndNameInfoContext, call: displayCall ? { - arguments.call() - } : nil) - case let .calls(theme, strings, dateTimeFormat, messages): - 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 = [.allUrl, .mention, .hashtag] - } - 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(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(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(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(presentationData: presentationData, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .plain, action: { - arguments.openChat() - }) - case let .addContact(theme, text): - 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(presentationData: presentationData, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .plain, action: { - arguments.shareContact() - }) - case let .shareMyContact(theme, text): - 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(presentationData: presentationData, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .plain, action: { - arguments.startSecretChat() - }) - case let .sharedMedia(theme, text): - return ItemListDisclosureItem(presentationData: presentationData, title: text, label: "", sectionId: self.section, style: .plain, action: { - arguments.openSharedMedia() - }) - case let .notifications(theme, text, value): - return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .plain, action: { - arguments.changeNotificationMuteSettings() - }) - case let .groupsInCommon(theme, text, value): - return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .plain, action: { - arguments.openGroupsInCommon() - }) - case let .secretEncryptionKey(theme, text, fingerprint): - return ItemListSecretChatKeyItem(presentationData: presentationData, title: text, fingerprint: fingerprint, sectionId: self.section, style: .plain, action: { - arguments.openEncryptionKey(fingerprint) - }) - case let .botAddToGroup(theme, text): - 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(presentationData: presentationData, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .plain, action: { - arguments.shareBot() - }) - case let .botSettings(theme, text): - 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(presentationData: presentationData, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .plain, action: { - arguments.botHelp() - }) - case let .botPrivacy(theme, text): - 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(presentationData: presentationData, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .plain, action: { - arguments.report() - }) - case let .block(theme, text, action): - return ItemListActionItem(presentationData: presentationData, title: text, kind: .destructive, alignment: .natural, sectionId: self.section, style: .plain, action: { - switch action { - case .block: - arguments.updatePeerBlocked(true) - case .unblock: - arguments.updatePeerBlocked(false) - case .removeContact: - arguments.deleteContact() - } - }) - } - } -} - -private enum DestructiveUserInfoAction { - case block - case removeContact - case unblock -} - -private struct UserInfoEditingState: Equatable { - let editingName: ItemListAvatarAndNameInfoItemName? - - static func ==(lhs: UserInfoEditingState, rhs: UserInfoEditingState) -> Bool { - if lhs.editingName != rhs.editingName { - return false - } - return true - } -} - -private struct UserInfoState: Equatable { - let savingData: Bool - let editingState: UserInfoEditingState? - - init() { - self.savingData = false - self.editingState = nil - } - - init(savingData: Bool, editingState: UserInfoEditingState?) { - self.savingData = savingData - self.editingState = editingState - } - - static func ==(lhs: UserInfoState, rhs: UserInfoState) -> Bool { - if lhs.savingData != rhs.savingData { - return false - } - if lhs.editingState != rhs.editingState { - return false - } - return true - } - - func withUpdatedSavingData(_ savingData: Bool) -> UserInfoState { - return UserInfoState(savingData: savingData, editingState: self.editingState) - } - - func withUpdatedEditingState(_ editingState: UserInfoEditingState?) -> UserInfoState { - return UserInfoState(savingData: self.savingData, editingState: editingState) - } -} - -private func stringForBlockAction(strings: PresentationStrings, action: DestructiveUserInfoAction, peer: Peer) -> String { - switch action { - case .block: - if let user = peer as? TelegramUser, user.botInfo != nil { - return strings.Bot_Stop - } else { - return strings.Conversation_BlockUser - } - case .unblock: - if let user = peer as? TelegramUser, user.botInfo != nil { - return strings.Bot_Unblock - } else { - return strings.Conversation_UnblockUser - } - case .removeContact: - return strings.UserInfo_DeleteContact - } -} - -private func userInfoEntries(account: Account, presentationData: PresentationData, view: PeerView, cachedPeerData: CachedPeerData?, deviceContacts: [(DeviceContactStableId, DeviceContactBasicData)], mode: PeerInfoControllerMode, state: UserInfoState, peerChatState: PostboxCoding?, globalNotificationSettings: GlobalNotificationSettings) -> [UserInfoEntry] { - var entries: [UserInfoEntry] = [] - - guard let peer = view.peers[view.peerId], let user = peerViewMainPeer(view) as? TelegramUser else { - return [] - } - - var editingName: ItemListAvatarAndNameInfoItemName? - - var isEditing = false - if let editingState = state.editingState { - isEditing = true - - if view.peerIsContact { - editingName = editingState.editingName - } - } - - var callsAvailable = true - if let cachedUserData = cachedPeerData as? CachedUserData { - callsAvailable = cachedUserData.voiceCallsAvailable - } - - entries.append(UserInfoEntry.info(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, peer: user, presence: view.peerPresences[user.id], cachedData: cachedPeerData, state: ItemListAvatarAndNameInfoItemState(editingName: editingName, updatingName: nil), displayCall: user.botInfo == nil && callsAvailable)) - - if case let .calls(messages) = mode, !isEditing { - entries.append(UserInfoEntry.calls(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, messages: messages)) - } - - if let phoneNumber = user.phone, !phoneNumber.isEmpty { - let formattedNumber = formatPhoneNumber(phoneNumber) - let normalizedNumber = DeviceContactNormalizedPhoneNumber(rawValue: formattedNumber) - - var index = 0 - var found = false - - var existingNumbers = Set() - var phoneNumbers: [(String, DeviceContactNormalizedPhoneNumber, Bool)] = [] - - for (_, contact) in deviceContacts { - inner: for number in contact.phoneNumbers { - var isMain = false - let normalizedContactNumber = DeviceContactNormalizedPhoneNumber(rawValue: formatPhoneNumber(number.value)) - if !existingNumbers.contains(normalizedContactNumber) { - existingNumbers.insert(normalizedContactNumber) - } else { - continue inner - } - if normalizedContactNumber == normalizedNumber { - found = true - isMain = true - } - - phoneNumbers.append((number.label, normalizedContactNumber, isMain)) - } - } - if !found { - entries.append(UserInfoEntry.phoneNumber(presentationData.theme, index, presentationData.strings.ContactInfo_PhoneLabelMobile, formattedNumber, false)) - index += 1 - } else { - for (label, number, isMain) in phoneNumbers { - entries.append(UserInfoEntry.phoneNumber(presentationData.theme, index, localizedPhoneNumberLabel(label: label, strings: presentationData.strings), number.rawValue, isMain && phoneNumbers.count != 1)) - index += 1 - } - } - } else { - //entries.append(UserInfoEntry.requestPhoneNumber(presentationData.theme, "phone", "Request Number")) - } - - let aboutTitle: String - if let _ = user.botInfo { - aboutTitle = presentationData.strings.Profile_BotInfo - } else { - aboutTitle = presentationData.strings.Profile_About - } - if user.isFake { - let aboutValue: String - if let _ = user.botInfo { - aboutValue = presentationData.strings.UserInfo_FakeBotWarning - } else { - aboutValue = presentationData.strings.UserInfo_FakeUserWarning - } - entries.append(UserInfoEntry.about(presentationData.theme, peer, aboutTitle, aboutValue)) - } else if user.isScam { - let aboutValue: String - if let _ = user.botInfo { - aboutValue = presentationData.strings.UserInfo_ScamBotWarning - } else { - aboutValue = presentationData.strings.UserInfo_ScamUserWarning - } - entries.append(UserInfoEntry.about(presentationData.theme, peer, aboutTitle, aboutValue)) - } else if let cachedUserData = cachedPeerData as? CachedUserData, let about = cachedUserData.about, !about.isEmpty { - entries.append(UserInfoEntry.about(presentationData.theme, peer, aboutTitle, about)) - } - - if !isEditing { - if let username = user.username, !username.isEmpty { - entries.append(UserInfoEntry.userName(presentationData.theme, presentationData.strings.Profile_Username, username)) - } - - if !(peer is TelegramSecretChat) { - entries.append(UserInfoEntry.sendMessage(presentationData.theme, presentationData.strings.UserInfo_SendMessage)) - } - - if user.botInfo == nil { - if view.peerIsContact { - if let phone = user.phone, !phone.isEmpty { - entries.append(UserInfoEntry.shareContact(presentationData.theme, presentationData.strings.UserInfo_ShareContact)) - } - } else { - entries.append(UserInfoEntry.addContact(presentationData.theme, presentationData.strings.Conversation_AddToContacts)) - } - } - - if let cachedUserData = cachedPeerData as? CachedUserData, let peerStatusSettings = cachedUserData.peerStatusSettings, peerStatusSettings.contains(.canShareContact) { - entries.append(UserInfoEntry.shareMyContact(presentationData.theme, presentationData.strings.UserInfo_ShareMyContactInfo)) - } - - if let peer = peer as? TelegramUser, peer.botInfo == nil { - entries.append(UserInfoEntry.startSecretChat(presentationData.theme, presentationData.strings.UserInfo_StartSecretChat)) - } - - if let peer = peer as? TelegramUser, let botInfo = peer.botInfo { - if botInfo.flags.contains(.worksWithGroups) { - entries.append(UserInfoEntry.botAddToGroup(presentationData.theme, presentationData.strings.UserInfo_InviteBotToGroup)) - } - entries.append(UserInfoEntry.botShare(presentationData.theme, presentationData.strings.UserInfo_ShareBot)) - - if let cachedUserData = cachedPeerData as? CachedUserData, let botInfo = cachedUserData.botInfo { - for command in botInfo.commands { - if command.text == "settings" { - entries.append(UserInfoEntry.botSettings(presentationData.theme, presentationData.strings.UserInfo_BotSettings)) - } else if command.text == "help" { - entries.append(UserInfoEntry.botHelp(presentationData.theme, presentationData.strings.UserInfo_BotHelp)) - } else if command.text == "privacy" { - entries.append(UserInfoEntry.botPrivacy(presentationData.theme, presentationData.strings.UserInfo_BotPrivacy)) - } - } - } - } - - entries.append(UserInfoEntry.sharedMedia(presentationData.theme, presentationData.strings.GroupInfo_SharedMedia)) - } - let notificationsLabel: String - let notificationSettings = view.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 if case .default = notificationSettings.messageSound { - notificationsLabel = presentationData.strings.UserInfo_NotificationsEnabled - } else { - notificationsLabel = localizedPeerNotificationSoundString(strings: presentationData.strings, sound: notificationSettings.messageSound, default: globalNotificationSettings.effective.channels.sound) - } - entries.append(UserInfoEntry.notifications(presentationData.theme, presentationData.strings.GroupInfo_Notifications, notificationsLabel)) - - if isEditing { - if view.peerIsContact { - entries.append(UserInfoEntry.block(presentationData.theme, stringForBlockAction(strings: presentationData.strings, action: .removeContact, peer: user), .removeContact)) - } - } else { - if peer is TelegramSecretChat, let peerChatState = peerChatState as? SecretChatKeyState, let keyFingerprint = peerChatState.keyFingerprint { - entries.append(UserInfoEntry.secretEncryptionKey(presentationData.theme, presentationData.strings.Profile_EncryptionKey, keyFingerprint)) - } - - if let groupsInCommon = (cachedPeerData as? CachedUserData)?.commonGroupCount, groupsInCommon != 0 { - entries.append(UserInfoEntry.groupsInCommon(presentationData.theme, presentationData.strings.UserInfo_GroupsInCommon, presentationStringsFormattedNumber(groupsInCommon, presentationData.dateTimeFormat.groupingSeparator))) - } - - if let peer = peer as? TelegramUser, let _ = peer.botInfo { - entries.append(UserInfoEntry.botReport(presentationData.theme, presentationData.strings.ReportPeer_Report)) - } - - if let cachedData = cachedPeerData as? CachedUserData { - if cachedData.isBlocked { - entries.append(UserInfoEntry.block(presentationData.theme, stringForBlockAction(strings: presentationData.strings, action: .unblock, peer: user), .unblock)) - } else { - if let peer = peer as? TelegramUser, peer.flags.contains(.isSupport) { - } else { - entries.append(UserInfoEntry.block(presentationData.theme, stringForBlockAction(strings: presentationData.strings, action: .block, peer: user), .block)) - } - } - } - } - - return entries -} - 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 { @@ -805,803 +67,3 @@ public func openAddPersonContactImpl(context: AccountContext, peerId: PeerId, pu }), completed: nil, cancelled: nil)) }) } - -public func userInfoController(context: AccountContext, peerId: PeerId, mode: PeerInfoControllerMode = .generic) -> ViewController { - let statePromise = ValuePromise(UserInfoState(), ignoreRepeated: true) - let stateValue = Atomic(value: UserInfoState()) - let updateState: ((UserInfoState) -> UserInfoState) -> Void = { f in - statePromise.set(stateValue.modify { f($0) }) - } - - var pushControllerImpl: ((ViewController) -> Void)? - var presentControllerImpl: ((ViewController, Any?) -> Void)? - var openChatImpl: (() -> Void)? - var shareContactImpl: (() -> Void)? - var shareMyContactImpl: (() -> Void)? - var startSecretChatImpl: (() -> Void)? - var botAddToGroupImpl: (() -> Void)? - var shareBotImpl: (() -> Void)? - var dismissInputImpl: (() -> Void)? - var dismissImpl: (() -> Void)? - - let actionsDisposable = DisposableSet() - - let updatePeerNameDisposable = MetaDisposable() - actionsDisposable.add(updatePeerNameDisposable) - - let updatePeerBlockedDisposable = MetaDisposable() - actionsDisposable.add(updatePeerBlockedDisposable) - - let changeMuteSettingsDisposable = MetaDisposable() - actionsDisposable.add(changeMuteSettingsDisposable) - - let hiddenAvatarRepresentationDisposable = MetaDisposable() - actionsDisposable.add(hiddenAvatarRepresentationDisposable) - - let createSecretChatDisposable = MetaDisposable() - actionsDisposable.add(createSecretChatDisposable) - - let navigateDisposable = MetaDisposable() - actionsDisposable.add(navigateDisposable) - - var avatarGalleryTransitionArguments: ((AvatarGalleryEntry) -> GalleryTransitionArguments?)? - let avatarAndNameInfoContext = ItemListAvatarAndNameInfoItemContext() - var updateHiddenAvatarImpl: (() -> Void)? - - var aboutLinkActionImpl: ((TextLinkItemActionType, TextLinkItem) -> Void)? - var displayAboutContextMenuImpl: ((String) -> Void)? - var displayCopyContextMenuImpl: ((UserInfoEntryTag, String) -> Void)? - var popToRootImpl: (() -> Void)? - - let cachedAvatarEntries = Atomic?>(value: nil) - - let peerView = Promise<(PeerView, CachedPeerData?)>() - peerView.set(context.account.viewTracker.peerView(peerId, updateData: true) |> mapToSignal({ view -> Signal<(PeerView, CachedPeerData?), NoError> in - if peerId.namespace == Namespaces.Peer.SecretChat { - if let peer = peerViewMainPeer(view) { - return context.account.viewTracker.peerView(peer.id) |> map({ secretChatView -> (PeerView, CachedPeerData?) in - return (view, secretChatView.cachedData) - }) - } - } - return .single((view, view.cachedData)) - })) - - let requestCallImpl: (Bool) -> Void = { isVideo in - let _ = (peerView.get() - |> take(1) - |> deliverOnMainQueue).start(next: { view in - guard let peer = peerViewMainPeer(view.0) else { - return - } - - if let cachedUserData = view.1 as? CachedUserData, cachedUserData.callsPrivate { - let presentationData = context.sharedContext.currentPresentationData.with { $0 } - presentControllerImpl?(textAlertController(context: context, title: presentationData.strings.Call_ConnectionErrorTitle, text: presentationData.strings.Call_PrivacyErrorMessage(peer.compactDisplayTitle).0, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil) - return - } - - context.requestCall(peerId: peer.id, isVideo: isVideo, completion: {}) - }) - } - - let arguments = UserInfoControllerArguments(context: context, avatarAndNameInfoContext: avatarAndNameInfoContext, updateEditingName: { editingName in - updateState { state in - if let _ = state.editingState { - return state.withUpdatedEditingState(UserInfoEditingState(editingName: editingName)) - } else { - return state - } - } - }, tapAvatarAction: { - let _ = (getUserPeer(postbox: context.account.postbox, peerId: peerId) |> deliverOnMainQueue).start(next: { peer, _ in - guard let peer = peer else { - return - } - - if peer.profileImageRepresentations.isEmpty { - return - } - - let galleryController = AvatarGalleryController(context: context, peer: peer, remoteEntries: cachedAvatarEntries.with { $0 }, replaceRootController: { controller, ready in - }) - hiddenAvatarRepresentationDisposable.set((galleryController.hiddenMedia |> deliverOnMainQueue).start(next: { entry in - avatarAndNameInfoContext.hiddenAvatarRepresentation = entry?.representations.first?.representation - updateHiddenAvatarImpl?() - })) - presentControllerImpl?(galleryController, AvatarGalleryControllerPresentationArguments(transitionArguments: { entry in - return avatarGalleryTransitionArguments?(entry) - })) - }) - }, openChat: { - openChatImpl?() - }, addContact: { - openAddPersonContactImpl(context: context, peerId: peerId, pushController: { c in - pushControllerImpl?(c) - }, present: { c, a in - presentControllerImpl?(c, a) - }) - }, shareContact: { - shareContactImpl?() - }, shareMyContact: { - shareMyContactImpl?() - }, startSecretChat: { - startSecretChatImpl?() - }, changeNotificationMuteSettings: { - let presentationData = context.sharedContext.currentPresentationData.with { $0 } - let _ = (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: { peerSettings, globalSettings in - let soundSettings: NotificationSoundSettings? - if case .default = peerSettings.messageSound { - soundSettings = NotificationSoundSettings(value: nil) - } else { - soundSettings = NotificationSoundSettings(value: peerSettings.messageSound) - } - let controller = notificationMuteSettingsController(presentationData: presentationData, notificationSettings: globalSettings.effective.groupChats, soundSettings: soundSettings, openSoundSettings: { - let controller = notificationSoundSelectionController(context: context, isModal: true, currentSound: peerSettings.messageSound, defaultSound: globalSettings.effective.groupChats.sound, completion: { sound in - let _ = updatePeerNotificationSoundInteractive(account: context.account, peerId: peerId, sound: sound).start() - }) - presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) - }, updateSettings: { value in - changeMuteSettingsDisposable.set(updatePeerMuteSetting(account: context.account, peerId: peerId, muteInterval: value).start()) - }) - presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) - }) - }, openSharedMedia: { - if let controller = context.sharedContext.makePeerSharedMediaController(context: context, peerId: peerId) { - pushControllerImpl?(controller) - } - }, openGroupsInCommon: { - let _ = (getUserPeer(postbox: context.account.postbox, peerId: peerId) - |> take(1) - |> deliverOnMainQueue).start(next: { peer, _ in - guard let peer = peer else { - return - } - - pushControllerImpl?(groupsInCommonController(context: context, peerId: peer.id)) - }) - }, updatePeerBlocked: { value in - let _ = (getUserPeer(postbox: context.account.postbox, peerId: peerId) - |> take(1) - |> deliverOnMainQueue).start(next: { peer, _ in - guard let peer = peer else { - return - } - - let presentationData = context.sharedContext.currentPresentationData.with { $0 } - if let peer = peer as? TelegramUser, let _ = peer.botInfo { - updatePeerBlockedDisposable.set(requestUpdatePeerIsBlocked(account: context.account, peerId: peer.id, isBlocked: value).start()) - if !value { - let _ = enqueueMessages(account: context.account, peerId: peer.id, messages: [.message(text: "/start", attributes: [], mediaReference: nil, replyToMessageId: nil, localGroupingKey: nil)]).start() - openChatImpl?() - } - } else { - if value { - let presentationData = context.sharedContext.currentPresentationData.with { $0 } - let controller = ActionSheetController(presentationData: presentationData) - let dismissAction: () -> Void = { [weak controller] in - controller?.dismissAnimated() - } - var reportSpam = false - var deleteChat = false - 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: { - dismissAction() - updatePeerBlockedDisposable.set(requestUpdatePeerIsBlocked(account: context.account, peerId: peer.id, isBlocked: true).start()) - if deleteChat { - let _ = removePeerChat(account: context.account, peerId: peerId, reportChatSpam: reportSpam).start() - popToRootImpl?() - } else if reportSpam { - let _ = reportPeer(account: context.account, peerId: peerId, reason: .spam, message: "").start() - } - - deleteSendMessageIntents(peerId: peerId) - }) - ]), - ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })]) - ]) - presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) - } else { - let text: String - if value { - 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 - } - presentControllerImpl?(textAlertController(context: context, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_No, action: {}), TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Yes, action: { - updatePeerBlockedDisposable.set(requestUpdatePeerIsBlocked(account: context.account, peerId: peer.id, isBlocked: value).start()) - })]), nil) - } - } - }) - }, deleteContact: { - let presentationData = context.sharedContext.currentPresentationData.with { $0 } - let controller = ActionSheetController(presentationData: presentationData) - let dismissAction: () -> Void = { [weak controller] in - controller?.dismissAnimated() - } - controller.setItemGroups([ - ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: presentationData.strings.UserInfo_DeleteContact, color: .destructive, action: { - dismissAction() - let _ = (getUserPeer(postbox: context.account.postbox, peerId: peerId) - |> deliverOnMainQueue).start(next: { peer, _ in - guard let peer = peer else { - return - } - let deleteContactFromDevice: Signal - if let contactDataManager = context.sharedContext.contactDataManager { - deleteContactFromDevice = contactDataManager.deleteContactWithAppSpecificReference(peerId: peer.id) - } else { - deleteContactFromDevice = .complete() - } - - var deleteSignal = deleteContactPeerInteractively(account: context.account, peerId: peer.id) - |> then(deleteContactFromDevice) - - let progressSignal = Signal { subscriber in - let presentationData = context.sharedContext.currentPresentationData.with { $0 } - let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: nil)) - presentControllerImpl?(controller, nil) - return ActionDisposable { [weak controller] in - Queue.mainQueue().async() { - controller?.dismiss() - } - } - } - |> runOn(Queue.mainQueue()) - |> delay(0.15, queue: Queue.mainQueue()) - let progressDisposable = progressSignal.start() - - deleteSignal = deleteSignal - |> afterDisposed { - Queue.mainQueue().async { - progressDisposable.dispose() - } - } - - updatePeerBlockedDisposable.set((deleteSignal - |> deliverOnMainQueue).start(completed: { - dismissImpl?() - })) - - deleteSendMessageIntents(peerId: peerId) - }) - }) - ]), - ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })]) - ]) - presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) - }, displayUsernameContextMenu: { text in - let shareController = ShareController(context: context, subject: .url("\(text)")) - presentControllerImpl?(shareController, nil) - }, displayCopyContextMenu: { tag, phone in - displayCopyContextMenuImpl?(tag, phone) - }, call: { - requestCallImpl(false) - }, openCallMenu: { number in - let _ = (getUserPeer(postbox: context.account.postbox, peerId: peerId) - |> 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(presentationData: presentationData) - let dismissAction: () -> Void = { [weak controller] in - controller?.dismissAnimated() - } - controller.setItemGroups([ - ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: presentationData.strings.UserInfo_TelegramCall, action: { - dismissAction() - requestCallImpl(false) - }), - ActionSheetButtonItem(title: presentationData.strings.UserInfo_PhoneCall, action: { - dismissAction() - context.sharedContext.applicationBindings.openUrl("tel:\(formatPhoneNumber(number).replacingOccurrences(of: " ", with: ""))") - }), - ]), - ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })]) - ]) - presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) - } else { - context.sharedContext.applicationBindings.openUrl("tel:\(formatPhoneNumber(number).replacingOccurrences(of: " ", with: ""))") - } - }) - }, requestPhoneNumber: { - let _ = (requestPhoneNumber(account: context.account, peerId: peerId) - |> deliverOnMainQueue).start(completed: { - - }) - }, aboutLinkAction: { action, itemLink in - aboutLinkActionImpl?(action, itemLink) - }, displayAboutContextMenu: { text in - displayAboutContextMenuImpl?(text) - }, openEncryptionKey: { fingerprint in - let _ = (context.account.postbox.transaction { transaction -> Peer? in - if let peer = transaction.getPeer(peerId) as? TelegramSecretChat { - if let userPeer = transaction.getPeer(peer.regularPeerId) { - return userPeer - } - } - return nil - } |> deliverOnMainQueue).start(next: { peer in - if let peer = peer { - pushControllerImpl?(SecretChatKeyController(context: context, fingerprint: fingerprint, peer: peer)) - } - }) - }, addBotToGroup: { - botAddToGroupImpl?() - }, shareBot: { - shareBotImpl?() - }, botSettings: { - let _ = (context.account.postbox.loadedPeerWithId(peerId) - |> deliverOnMainQueue).start(next: { peer in - let _ = enqueueMessages(account: context.account, peerId: peer.id, messages: [.message(text: "/settings", attributes: [], mediaReference: nil, replyToMessageId: nil, localGroupingKey: nil)]).start() - openChatImpl?() - }) - }, botHelp: { - let _ = (context.account.postbox.loadedPeerWithId(peerId) - |> deliverOnMainQueue).start(next: { peer in - let _ = enqueueMessages(account: context.account, peerId: peer.id, messages: [.message(text: "/help", attributes: [], mediaReference: nil, replyToMessageId: nil, localGroupingKey: nil)]).start() - openChatImpl?() - }) - }, botPrivacy: { - let _ = (context.account.postbox.loadedPeerWithId(peerId) - |> deliverOnMainQueue).start(next: { peer in - let _ = enqueueMessages(account: context.account, peerId: peer.id, messages: [.message(text: "/privacy", attributes: [], mediaReference: nil, replyToMessageId: nil, localGroupingKey: nil)]).start() - openChatImpl?() - }) - }, report: { - presentControllerImpl?(peerReportOptionsController(context: context, subject: .peer(peerId), passthrough: false, present: { c, a in - presentControllerImpl?(c, a) - }, push: { c in - pushControllerImpl?(c) - }, completion: { _, _ in }), nil) - }) - - let deviceContacts: Signal<[(DeviceContactStableId, DeviceContactBasicData)], NoError> = peerView.get() - |> map { peerView -> String in - if let peer = peerView.0.peers[peerId] as? TelegramUser { - return peer.phone ?? "" - } - return "" - } - |> distinctUntilChanged - |> mapToSignal { number -> Signal<[(DeviceContactStableId, DeviceContactBasicData)], NoError> in - if number.isEmpty { - return .single([]) - } else { - return context.sharedContext.contactDataManager?.basicDataForNormalizedPhoneNumber(DeviceContactNormalizedPhoneNumber(rawValue: formatPhoneNumber(number))) ?? .single([]) - } - } - - let globalNotificationsKey: PostboxViewKey = .preferences(keys: Set([PreferencesKeys.globalNotifications])) - let signal = combineLatest(context.sharedContext.presentationData, statePromise.get(), peerView.get(), deviceContacts, context.account.postbox.combinedView(keys: [.peerChatState(peerId: peerId), globalNotificationsKey])) - |> map { presentationData, state, view, deviceContacts, combinedView -> (ItemListControllerState, (ItemListNodeState, Any)) in - let peer = peerViewMainPeer(view.0) - - var globalNotificationSettings: GlobalNotificationSettings = .defaultSettings - if let preferencesView = combinedView.views[globalNotificationsKey] as? PreferencesView { - if let settings = preferencesView.values[PreferencesKeys.globalNotifications] as? GlobalNotificationSettings { - globalNotificationSettings = settings - } - } - - if let peer = peer { - let _ = cachedAvatarEntries.modify { value in - if value != nil { - return value - } else { - let promise = Promise<[AvatarGalleryEntry]>() - promise.set(fetchedAvatarGalleryEntries(account: context.account, peer: peer)) - return promise - } - } - } - var leftNavigationButton: ItemListNavigationButton? - let rightNavigationButton: ItemListNavigationButton - if let editingState = state.editingState { - leftNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Cancel), style: .regular, enabled: true, action: { - updateState { - $0.withUpdatedEditingState(nil) - } - }) - - var doneEnabled = true - if let editingName = editingState.editingName, editingName.isEmpty { - doneEnabled = false - } - - if state.savingData { - rightNavigationButton = ItemListNavigationButton(content: .none, style: .activity, enabled: doneEnabled, action: {}) - } else { - rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Done), style: .bold, enabled: doneEnabled, action: { - var updateName: ItemListAvatarAndNameInfoItemName? - updateState { state in - if let editingState = state.editingState, let editingName = editingState.editingName { - if let user = peer { - if ItemListAvatarAndNameInfoItemName(user) != editingName { - updateName = editingName - } - } - } - if updateName != nil { - return state.withUpdatedSavingData(true) - } else { - return state.withUpdatedEditingState(nil) - } - } - - if let updateName = updateName, case let .personName(firstName, lastName, _) = updateName { - updatePeerNameDisposable.set((updateContactName(account: context.account, peerId: peerId, firstName: firstName, lastName: lastName) - |> deliverOnMainQueue).start(error: { _ in - updateState { state in - return state.withUpdatedSavingData(false) - } - }, completed: { - updateState { state in - return state.withUpdatedSavingData(false).withUpdatedEditingState(nil) - } - - let _ = (getUserPeer(postbox: context.account.postbox, peerId: peerId) - |> 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() - })) - } - }) - } - } else { - rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Edit), style: .regular, enabled: true, action: { - if let user = peer { - updateState { state in - return state.withUpdatedEditingState(UserInfoEditingState(editingName: ItemListAvatarAndNameInfoItemName(user))) - } - } - }) - } - - 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)) - } - |> afterDisposed { - actionsDisposable.dispose() - } - - let controller = ItemListController(context: context, state: signal) - - pushControllerImpl = { [weak controller] value in - (controller?.navigationController as? NavigationController)?.pushViewController(value) - } - dismissImpl = { [weak controller] in - guard let controller = controller else { - return - } - (controller.navigationController as? NavigationController)?.filterController(controller, animated: true) - } - presentControllerImpl = { [weak controller] value, presentationArguments in - controller?.present(value, in: .window(.root), with: presentationArguments, blockInteraction: true) - } - dismissInputImpl = { [weak controller] in - controller?.view.endEditing(true) - } - openChatImpl = { [weak controller] in - if let navigationController = (controller?.navigationController as? NavigationController) { - context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peerId))) - } - } - shareContactImpl = { [weak controller] in - let _ = (getUserPeer(postbox: context.account.postbox, peerId: peerId) - |> deliverOnMainQueue).start(next: { peer, _ in - if let peer = 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: context, subject: .media(.standalone(media: contact))) - controller?.present(shareController, in: .window(.root)) - } - }) - } - shareMyContactImpl = { [weak controller] in - let _ = (getUserPeer(postbox: context.account.postbox, peerId: context.account.peerId) - |> deliverOnMainQueue).start(next: { peer, _ in - guard let peer = peer as? TelegramUser, let phone = peer.phone else { - return - } - let contact = TelegramMediaContact(firstName: peer.firstName ?? "", lastName: peer.lastName ?? "", phoneNumber: phone, peerId: peer.id, vCardData: nil) - - let _ = (enqueueMessages(account: context.account, peerId: peerId, messages: [.message(text: "", attributes: [], mediaReference: .standalone(media: contact), replyToMessageId: nil, localGroupingKey: nil)]) - |> deliverOnMainQueue).start(next: { [weak controller] _ in - let presentationData = context.sharedContext.currentPresentationData.with { $0 } - controller?.present(OverlayStatusController(theme: presentationData.theme, type: .success), in: .window(.root)) - }) - }) - } - startSecretChatImpl = { [weak controller] in - let _ = (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: { peer, currentPeerId in - if let currentPeerId = currentPeerId { - if let navigationController = (controller?.navigationController as? NavigationController) { - context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(currentPeerId))) - } - } else if let controller = controller { - let presentationData = context.sharedContext.currentPresentationData.with { $0 } - let displayTitle = peer?.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) ?? "" - controller.present(textAlertController(context: context, title: nil, text: presentationData.strings.UserInfo_StartSecretChatConfirmation(displayTitle).0, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: presentationData.strings.UserInfo_StartSecretChatStart, action: { - var createSignal = createSecretChat(account: context.account, peerId: peerId) - var cancelImpl: (() -> Void)? - let progressSignal = Signal { subscriber in - let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: { - cancelImpl?() - })) - presentControllerImpl?(controller, nil) - return ActionDisposable { [weak controller] in - Queue.mainQueue().async() { - controller?.dismiss() - } - } - } - |> runOn(Queue.mainQueue()) - |> delay(0.15, queue: Queue.mainQueue()) - let progressDisposable = progressSignal.start() - - createSignal = createSignal - |> afterDisposed { - Queue.mainQueue().async { - progressDisposable.dispose() - } - } - cancelImpl = { - createSecretChatDisposable.set(nil) - } - - createSecretChatDisposable.set((createSignal |> deliverOnMainQueue).start(next: { [weak controller] peerId in - if let navigationController = (controller?.navigationController as? NavigationController) { - context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peerId))) - } - }, error: { [weak controller] error in - if let controller = controller { - let presentationData = context.sharedContext.currentPresentationData.with { $0 } - let text: String - switch error { - case .limitExceeded: - text = presentationData.strings.TwoStepAuth_FloodError - default: - text = presentationData.strings.Login_UnknownError - } - controller.present(textAlertController(context: context, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root)) - } - })) - })]), in: .window(.root)) - } - }) - } - botAddToGroupImpl = { [weak controller] in - guard let controller = controller else { - return - } - context.sharedContext.openResolvedUrl(.groupBotStart(peerId: peerId, payload: ""), context: context, urlContext: .generic, navigationController: controller.navigationController as? NavigationController, openPeer: { id, navigation in - - }, sendFile: nil, - sendSticker: nil, - requestMessageActionUrlAuth: nil, - joinVoiceChat: nil, - present: { c, a in - presentControllerImpl?(c, a) - }, dismissInput: { - dismissInputImpl?() - }, contentContext: nil) - } - shareBotImpl = { [weak controller] in - let _ = (getUserPeer(postbox: context.account.postbox, peerId: peerId) - |> deliverOnMainQueue).start(next: { peer, _ in - if let peer = peer as? TelegramUser, let username = peer.username { - let shareController = ShareController(context: context, subject: .url("https://t.me/\(username)")) - controller?.present(shareController, in: .window(.root)) - } - }) - } - avatarGalleryTransitionArguments = { [weak controller] entry in - if let controller = controller { - var result: ((ASDisplayNode, CGRect, () -> (UIView?, UIView?)), CGRect)? - controller.forEachItemNode { itemNode in - if let itemNode = itemNode as? ItemListAvatarAndNameInfoItemNode { - result = itemNode.avatarTransitionNode() - } - } - if let (node, _) = result { - return GalleryTransitionArguments(transitionNode: node, addToTransitionSurface: { _ in - }) - } - } - return nil - } - updateHiddenAvatarImpl = { [weak controller] in - if let controller = controller { - controller.forEachItemNode { itemNode in - if let itemNode = itemNode as? ItemListAvatarAndNameInfoItemNode { - itemNode.updateAvatarHidden() - } - } - } - } - aboutLinkActionImpl = { [weak context, weak controller] action, itemLink in - if let controller = controller, let context = context { - context.sharedContext.handleTextLinkAction(context: context, peerId: peerId, navigateDisposable: navigateDisposable, controller: controller, action: action, itemLink: itemLink) - } - } - displayAboutContextMenuImpl = { [weak controller] text in - if let strongController = controller { - let presentationData = context.sharedContext.currentPresentationData.with { $0 } - var resultItemNode: ListViewItemNode? - let _ = strongController.frameForItemNode({ itemNode in - if let itemNode = itemNode as? ItemListTextWithLabelItemNode { - if let tag = itemNode.tag as? UserInfoEntryTag { - if tag == .about { - resultItemNode = itemNode - return true - } - } - } - return false - }) - if let resultItemNode = resultItemNode { - let contextMenuController = ContextMenuController(actions: [ContextMenuAction(content: .text(title: presentationData.strings.Conversation_ContextMenuCopy, accessibilityLabel: presentationData.strings.Conversation_ContextMenuCopy), action: { - UIPasteboard.general.string = text - })]) - strongController.present(contextMenuController, in: .window(.root), with: ContextMenuControllerPresentationArguments(sourceNodeAndRect: { [weak resultItemNode] in - if let strongController = controller, let resultItemNode = resultItemNode { - return (resultItemNode, resultItemNode.contentBounds.insetBy(dx: 0.0, dy: -2.0), strongController.displayNode, strongController.view.bounds) - } else { - return nil - } - })) - - } - } - } - - displayCopyContextMenuImpl = { [weak controller] tag, value in - if let strongController = controller { - let presentationData = context.sharedContext.currentPresentationData.with { $0 } - var resultItemNode: ListViewItemNode? - let _ = strongController.frameForItemNode({ itemNode in - if let itemNode = itemNode as? ItemListTextWithLabelItemNode { - if let itemTag = itemNode.tag as? UserInfoEntryTag { - if itemTag == tag && itemNode.item?.text == value { - resultItemNode = itemNode - return true - } - } - } - return false - }) - if let resultItemNode = resultItemNode { - let contextMenuController = ContextMenuController(actions: [ContextMenuAction(content: .text(title: presentationData.strings.Conversation_ContextMenuCopy, accessibilityLabel: presentationData.strings.Conversation_ContextMenuCopy), action: { - UIPasteboard.general.string = value - })]) - strongController.present(contextMenuController, in: .window(.root), with: ContextMenuControllerPresentationArguments(sourceNodeAndRect: { [weak resultItemNode] in - if let strongController = controller, let resultItemNode = resultItemNode { - return (resultItemNode, resultItemNode.contentBounds.insetBy(dx: 0.0, dy: -2.0), strongController.displayNode, strongController.view.bounds) - } else { - return nil - } - })) - } - } - } - - popToRootImpl = { [weak controller] in - (controller?.navigationController as? NavigationController)?.popToRoot(animated: true) - } - - controller.didAppear = { [weak controller] firstTime in - guard let controller = controller, firstTime else { - return - } - - var resultItemNode: ItemListAvatarAndNameInfoItemNode? - let _ = controller.frameForItemNode({ itemNode in - if let itemNode = itemNode as? ItemListAvatarAndNameInfoItemNode { - resultItemNode = itemNode - return true - } - return false - }) - if let resultItemNode = resultItemNode, let callButtonFrame = resultItemNode.callButtonFrame { - let _ = (ApplicationSpecificNotice.getProfileCallTips(accountManager: context.sharedContext.accountManager) - |> deliverOnMainQueue).start(next: { [weak controller] counter in - guard let controller = controller else { - return - } - - var displayTip = false - if counter == 0 { - displayTip = true - } else if counter < 3 && arc4random_uniform(4) == 1 { - displayTip = true - } - if !displayTip { - return - } - let _ = ApplicationSpecificNotice.incrementProfileCallTips(accountManager: context.sharedContext.accountManager).start() - - let presentationData = context.sharedContext.currentPresentationData.with { $0 } - let text: String = presentationData.strings.UserInfo_TapToCall - - 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) - } - return nil - })) - }) - } - } - - controller.navigationItem.backBarButtonItem = UIBarButtonItem(title: context.sharedContext.currentPresentationData.with{ $0 }.strings.Common_Back, style: .plain, target: nil, action: nil) - - return controller -} diff --git a/submodules/PeerPresenceStatusManager/Sources/PeerPresenceStatusManager.swift b/submodules/PeerPresenceStatusManager/Sources/PeerPresenceStatusManager.swift index af518befa8..1389239a93 100644 --- a/submodules/PeerPresenceStatusManager/Sources/PeerPresenceStatusManager.swift +++ b/submodules/PeerPresenceStatusManager/Sources/PeerPresenceStatusManager.swift @@ -4,19 +4,23 @@ import TelegramCore import SyncCore import SyncCore -private func suggestedUserPresenceStringRefreshTimeout(_ presence: TelegramUserPresence, relativeTo timestamp: Int32) -> Double { +private func suggestedUserPresenceStringRefreshTimeout(_ presence: TelegramUserPresence, relativeTo timestamp: Int32, isOnline: Bool?) -> Double { switch presence.status { case let .present(statusTimestamp): if statusTimestamp >= timestamp { return Double(statusTimestamp - timestamp) } else { - let difference = timestamp - statusTimestamp - if difference < 30 { - return Double((30 - difference) + 1) - } else if difference < 60 * 60 { - return Double((difference % 60) + 1) + if let isOnline = isOnline, isOnline { + return 1.0 } else { - return Double.infinity + let difference = timestamp - statusTimestamp + if difference < 30 { + return Double((30 - difference) + 1) + } else if difference < 60 * 60 { + return Double((difference % 60) + 1) + } else { + return Double.infinity + } } } case .recently: @@ -43,12 +47,12 @@ public final class PeerPresenceStatusManager { self.timer?.invalidate() } - public func reset(presence: TelegramUserPresence) { + public func reset(presence: TelegramUserPresence, isOnline: Bool? = nil) { self.timer?.invalidate() self.timer = nil let timestamp = CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970 - let timeout = suggestedUserPresenceStringRefreshTimeout(presence, relativeTo: Int32(timestamp)) + let timeout = suggestedUserPresenceStringRefreshTimeout(presence, relativeTo: Int32(timestamp), isOnline: isOnline) if timeout.isFinite { self.timer = SwiftSignalKit.Timer(timeout: timeout, repeat: false, completion: { [weak self] in if let strongSelf = self { diff --git a/submodules/PeersNearbyUI/Sources/PeersNearbyController.swift b/submodules/PeersNearbyUI/Sources/PeersNearbyController.swift index db5f2c0b36..25e2a0983a 100644 --- a/submodules/PeersNearbyUI/Sources/PeersNearbyController.swift +++ b/submodules/PeersNearbyUI/Sources/PeersNearbyController.swift @@ -229,7 +229,7 @@ private enum PeersNearbyEntry: ItemListNodeEntry { var text = strings.Map_DistanceAway(shortStringForDistance(strings: strings, distance: 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 + text = strings.PeopleNearby_VisibleUntil(humanReadableStringForTimestamp(strings: strings, dateTimeFormat: dateTimeFormat, timestamp: peer.expires).0).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, .secondary), label: .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), revealOptions: nil, switchValue: nil, enabled: true, selectable: !isSelfPeer, sectionId: self.section, action: { if !isSelfPeer { @@ -478,14 +478,14 @@ public func peersNearbyController(context: AccountContext) -> ViewController { let _ = (coordinatePromise.get() |> deliverOnMainQueue).start(next: { coordinate in if let coordinate = coordinate { - let _ = updatePeersNearbyVisibility(account: context.account, update: .visible(latitude: coordinate.latitude, longitude: coordinate.longitude), background: false).start() + let _ = context.engine.peersNearby.updatePeersNearbyVisibility(update: .visible(latitude: coordinate.latitude, longitude: coordinate.longitude), background: false).start() } }) })]), nil) } else { - let _ = updatePeersNearbyVisibility(account: context.account, update: .invisible, background: false).start() + let _ = context.engine.peersNearby.updatePeersNearbyVisibility(update: .invisible, background: false).start() } }, openProfile: { peer, distance in navigateToProfileImpl?(peer, distance) @@ -512,7 +512,7 @@ public func peersNearbyController(context: AccountContext) -> ViewController { cancelImpl = { checkCreationAvailabilityDisposable.set(nil) } - checkCreationAvailabilityDisposable.set((checkPublicChannelCreationAvailability(account: context.account, location: true) + checkCreationAvailabilityDisposable.set((context.engine.peers.checkPublicChannelCreationAvailability(location: true) |> afterDisposed { Queue.mainQueue().async { progressDisposable.dispose() diff --git a/submodules/PhotoResources/Sources/PhotoResources.swift b/submodules/PhotoResources/Sources/PhotoResources.swift index 91ec1a3454..d56431a40b 100644 --- a/submodules/PhotoResources/Sources/PhotoResources.swift +++ b/submodules/PhotoResources/Sources/PhotoResources.swift @@ -32,8 +32,8 @@ public func largestRepresentationForPhoto(_ photo: TelegramMediaImage) -> Telegr private let progressiveRangeMap: [(Int, [Int])] = [ (100, [0]), - (400, [1]), - (600, [2, 3]), + (400, [3]), + (600, [4]), (Int(Int32.max), [2, 3, 4]) ] @@ -59,9 +59,6 @@ public func chatMessagePhotoDatas(postbox: Postbox, photoReference: ImageMediaRe } var sources: [SizeSource] = [] - if let miniThumbnail = photoReference.media.immediateThumbnailData.flatMap(decodeTinyThumbnail) { - sources.append(.miniThumbnail(data: miniThumbnail)) - } let thumbnailByteSize = Int(progressiveRepresentation.progressiveSizes[0]) var largestByteSize = Int(progressiveRepresentation.progressiveSizes[0]) for (maxDimension, byteSizes) in progressiveRangeMap { @@ -79,6 +76,12 @@ public func chatMessagePhotoDatas(postbox: Postbox, photoReference: ImageMediaRe break } } + if sources.isEmpty { + sources.append(.image(size: largestByteSize)) + } + if let miniThumbnail = photoReference.media.immediateThumbnailData.flatMap(decodeTinyThumbnail) { + sources.insert(.miniThumbnail(data: miniThumbnail), at: 0) + } return Signal { subscriber in let signals: [Signal<(SizeSource, Data?), NoError>] = sources.map { source -> Signal<(SizeSource, Data?), NoError> in @@ -565,7 +568,7 @@ public func rawMessagePhoto(postbox: Postbox, photoReference: ImageMediaReferenc } } -public func chatMessagePhoto(postbox: Postbox, photoReference: ImageMediaReference, synchronousLoad: Bool = false) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { +public func chatMessagePhoto(postbox: Postbox, photoReference: ImageMediaReference, synchronousLoad: Bool = false, highQuality: Bool = false) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { return chatMessagePhotoInternal(photoData: chatMessagePhotoDatas(postbox: postbox, photoReference: photoReference, tryAdditionalRepresentations: true, synchronousLoad: synchronousLoad), synchronousLoad: synchronousLoad) |> map { _, _, generate in return generate @@ -684,7 +687,7 @@ public func chatMessagePhotoInternal(photoData: Signal Signal<(TransformImageArguments) -> DrawingContext?, NoError> { +public func chatAvatarGalleryPhoto(account: Account, representations: [ImageRepresentationWithReference], immediateThumbnailData: Data?, autoFetchFullSize: Bool = false, attemptSynchronously: Bool = false, skipThumbnail: Bool = false) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { let signal = avatarGalleryPhotoDatas(account: account, representations: representations, immediateThumbnailData: immediateThumbnailData, autoFetchFullSize: autoFetchFullSize, attemptSynchronously: attemptSynchronously) return signal @@ -2352,8 +2355,8 @@ public func chatAvatarGalleryPhoto(account: Account, representations: [ImageRepr } var blurredThumbnailImage: UIImage? - if let thumbnailImage = thumbnailImage { - if max(thumbnailImage.width, thumbnailImage.height) > 200 { + if let thumbnailImage = thumbnailImage, !skipThumbnail { + if max(thumbnailImage.width, thumbnailImage.height) > 200 { blurredThumbnailImage = UIImage(cgImage: thumbnailImage) } else { let thumbnailSize = CGSize(width: thumbnailImage.width, height: thumbnailImage.height) diff --git a/submodules/Postbox/Sources/AccountManager.swift b/submodules/Postbox/Sources/AccountManager.swift index 3da8424277..1025cbe4cb 100644 --- a/submodules/Postbox/Sources/AccountManager.swift +++ b/submodules/Postbox/Sources/AccountManager.swift @@ -124,7 +124,7 @@ final class AccountManagerImpl { self.tables.append(self.sharedDataTable) self.tables.append(self.noticeTable) - print("AccountManager initialization took \((CFAbsoluteTimeGetCurrent() - startTime) * 1000.0) ms") + postboxLog("AccountManager initialization took \((CFAbsoluteTimeGetCurrent() - startTime) * 1000.0) ms") } deinit { diff --git a/submodules/Postbox/Sources/ChatListIndexTable.swift b/submodules/Postbox/Sources/ChatListIndexTable.swift index c6c17af690..f44ade7157 100644 --- a/submodules/Postbox/Sources/ChatListIndexTable.swift +++ b/submodules/Postbox/Sources/ChatListIndexTable.swift @@ -62,8 +62,7 @@ final class ChatListIndexTable: Table { } private func key(_ peerId: PeerId) -> ValueBoxKey { - self.sharedKey.setInt32(0, value: peerId.namespace) - self.sharedKey.setInt32(4, value: peerId.id) + self.sharedKey.setInt64(0, value: peerId.toInt64()) assert(self.sharedKey.getInt64(0) == peerId.toInt64()) return self.sharedKey } @@ -648,7 +647,7 @@ final class ChatListIndexTable: Table { var summary = PeerGroupUnreadCountersCombinedSummary(namespaces: [:]) postbox.chatListTable.forEachPeer(groupId: groupId, { peerId in - if peerId.namespace == Int32.max { + if peerId.namespace == .max { return } guard let combinedState = postbox.readStateTable.getCombinedState(peerId) else { diff --git a/submodules/Postbox/Sources/ChatListViewState.swift b/submodules/Postbox/Sources/ChatListViewState.swift index ef08231ff7..7e8d365f44 100644 --- a/submodules/Postbox/Sources/ChatListViewState.swift +++ b/submodules/Postbox/Sources/ChatListViewState.swift @@ -242,11 +242,13 @@ private final class ChatListViewSpaceState { let allIndices = (lowerOrAtAnchorMessages + higherThanAnchorMessages).map { $0.entryIndex } let allEntityIds = (lowerOrAtAnchorMessages + higherThanAnchorMessages).map { $0.entityId } if Set(allIndices).count != allIndices.count { + var debugRepeatedIndices = Set() var existingIndices = Set() for i in (0 ..< lowerOrAtAnchorMessages.count).reversed() { if !existingIndices.contains(lowerOrAtAnchorMessages[i].entryIndex) { existingIndices.insert(lowerOrAtAnchorMessages[i].entryIndex) } else { + debugRepeatedIndices.insert(lowerOrAtAnchorMessages[i].entryIndex) lowerOrAtAnchorMessages.remove(at: i) } } @@ -254,10 +256,11 @@ private final class ChatListViewSpaceState { if !existingIndices.contains(higherThanAnchorMessages[i].entryIndex) { existingIndices.insert(higherThanAnchorMessages[i].entryIndex) } else { + debugRepeatedIndices.insert(higherThanAnchorMessages[i].entryIndex) higherThanAnchorMessages.remove(at: i) } } - postboxLog("allIndices not unique: \(allIndices)") + postboxLog("allIndices not unique, repeated: \(debugRepeatedIndices)") assert(false) //preconditionFailure() diff --git a/submodules/Postbox/Sources/ContactTable.swift b/submodules/Postbox/Sources/ContactTable.swift index 8be82c6de4..e236a3879a 100644 --- a/submodules/Postbox/Sources/ContactTable.swift +++ b/submodules/Postbox/Sources/ContactTable.swift @@ -22,11 +22,11 @@ final class ContactTable: Table { } private func lowerBound() -> ValueBoxKey { - return self.key(PeerId(namespace: 0, id: 0)) + return self.key(PeerId(0)) } private func upperBound() -> ValueBoxKey { - return self.key(PeerId(namespace: Int32.max, id: Int32.max)) + return self.key(PeerId.max) } func isContact(peerId: PeerId) -> Bool { @@ -81,7 +81,7 @@ final class ContactTable: Table { let removedPeerIds = peerIdsBeforeModification.subtracting(peerIds) let addedPeerIds = peerIds.subtracting(peerIdsBeforeModification) - let sharedKey = self.key(PeerId(namespace: 0, id: 0)) + let sharedKey = self.key(PeerId(0)) for peerId in removedPeerIds { self.valueBox.remove(self.table, key: self.key(peerId, sharedKey: sharedKey), secure: false) diff --git a/submodules/Postbox/Sources/GlobalMessageIdsTable.swift b/submodules/Postbox/Sources/GlobalMessageIdsTable.swift index a5665f3e16..800ddf9ffa 100644 --- a/submodules/Postbox/Sources/GlobalMessageIdsTable.swift +++ b/submodules/Postbox/Sources/GlobalMessageIdsTable.swift @@ -23,7 +23,7 @@ final class GlobalMessageIdsTable: Table { func set(_ globalId: Int32, id: MessageId) { assert(id.namespace == 0) - assert(id.peerId.namespace == 0 || id.peerId.namespace == 1) + assert(id.peerId.namespace._internalGetInt32Value() == 0 || id.peerId.namespace._internalGetInt32Value() == 1) assert(self.seedConfiguration.globalMessageIdsPeerIdNamespaces.contains(GlobalMessageIdsNamespace(peerIdNamespace: id.peerId.namespace, messageIdNamespace: id.namespace))) self.sharedBuffer.reset() diff --git a/submodules/Postbox/Sources/GlobalMessageTagsView.swift b/submodules/Postbox/Sources/GlobalMessageTagsView.swift index 51bd14f19a..7e272d24f1 100644 --- a/submodules/Postbox/Sources/GlobalMessageTagsView.swift +++ b/submodules/Postbox/Sources/GlobalMessageTagsView.swift @@ -348,7 +348,7 @@ final class MutableGlobalMessageTagsView: MutablePostboxView { } if let later = self.later { - addedEntries += postbox.messageHistoryTable.laterEntries(globalTagMask: self.globalTag, index: later.predecessor(), count: self.count).map { entry -> InternalGlobalMessageTagsEntry in + addedEntries += postbox.messageHistoryTable.laterEntries(globalTagMask: self.globalTag, index: later.globalPredecessor(), count: self.count).map { entry -> InternalGlobalMessageTagsEntry in switch entry { case let .message(message): return .intermediateMessage(message) @@ -358,7 +358,7 @@ final class MutableGlobalMessageTagsView: MutablePostboxView { } } if let earlier = self.earlier { - addedEntries += postbox.messageHistoryTable.earlierEntries(globalTagMask: self.globalTag, index: earlier.successor(), count: self.count).map { entry -> InternalGlobalMessageTagsEntry in + addedEntries += postbox.messageHistoryTable.earlierEntries(globalTagMask: self.globalTag, index: earlier.globalSuccessor(), count: self.count).map { entry -> InternalGlobalMessageTagsEntry in switch entry { case let .message(message): return .intermediateMessage(message) diff --git a/submodules/Postbox/Sources/MediaBox.swift b/submodules/Postbox/Sources/MediaBox.swift index e7188645ed..f0abfe8281 100644 --- a/submodules/Postbox/Sources/MediaBox.swift +++ b/submodules/Postbox/Sources/MediaBox.swift @@ -104,9 +104,10 @@ private struct CachedMediaResourceRepresentationKey: Hashable { static func ==(lhs: CachedMediaResourceRepresentationKey, rhs: CachedMediaResourceRepresentationKey) -> Bool { return lhs.resourceId.isEqual(to: rhs.resourceId) && lhs.representation.isEqual(to: rhs.representation) } - - var hashValue: Int { - return self.resourceId.hashValue + + func hash(into hasher: inout Hasher) { + hasher.combine(self.resourceId.hashValue) + hasher.combine(self.representation.uniqueId) } } @@ -1109,15 +1110,17 @@ public final class MediaBox { } } - public func removeCachedResources(_ ids: Set) -> Signal { + public func removeCachedResources(_ ids: Set, force: Bool = false) -> Signal { return Signal { subscriber in self.dataQueue.async { for id in ids { - if self.fileContexts[id] != nil { - continue - } - if self.keepResourceContexts[id] != nil { - continue + if !force { + if self.fileContexts[id] != nil { + continue + } + if self.keepResourceContexts[id] != nil { + continue + } } let paths = self.storePathsForId(id.id) unlink(paths.complete) diff --git a/submodules/Postbox/Sources/MediaResource.swift b/submodules/Postbox/Sources/MediaResource.swift index bd87e21e24..db25d91a3d 100644 --- a/submodules/Postbox/Sources/MediaResource.swift +++ b/submodules/Postbox/Sources/MediaResource.swift @@ -22,7 +22,7 @@ public struct WrappedMediaResourceId: Hashable { // } public func hash(into hasher: inout Hasher) { - hasher.combine(id.hashValue) + hasher.combine(self.id.hashValue) } } diff --git a/submodules/Postbox/Sources/Message.swift b/submodules/Postbox/Sources/Message.swift index 540b072f43..85b35fefca 100644 --- a/submodules/Postbox/Sources/Message.swift +++ b/submodules/Postbox/Sources/Message.swift @@ -10,7 +10,7 @@ public struct MessageId: Hashable, Comparable, CustomStringConvertible, PostboxC public var description: String { get { - return "\(namespace)_\(id)" + return "\(peerId):\(namespace)_\(id)" } } @@ -18,14 +18,16 @@ public struct MessageId: Hashable, Comparable, CustomStringConvertible, PostboxC self.peerId = peerId self.namespace = namespace self.id = id + if namespace == 0 && id == 0 { + assert(true) + } } public init(_ buffer: ReadBuffer) { - var peerIdNamespaceValue: Int32 = 0 - memcpy(&peerIdNamespaceValue, buffer.memory + buffer.offset, 4) - var peerIdIdValue: Int32 = 0 - memcpy(&peerIdIdValue, buffer.memory + (buffer.offset + 4), 4) - self.peerId = PeerId(namespace: peerIdNamespaceValue, id: peerIdIdValue) + var peerIdInt64Value: Int64 = 0 + memcpy(&peerIdInt64Value, buffer.memory + buffer.offset, 8) + + self.peerId = PeerId(peerIdInt64Value) var namespaceValue: Int32 = 0 memcpy(&namespaceValue, buffer.memory + (buffer.offset + 8), 4) @@ -50,14 +52,12 @@ public struct MessageId: Hashable, Comparable, CustomStringConvertible, PostboxC } public func encodeToBuffer(_ buffer: WriteBuffer) { - var peerIdNamespace = self.peerId.namespace - var peerIdId = self.peerId.id + var peerIdValue = self.peerId.toInt64() var namespace = self.namespace var id = self.id - buffer.write(&peerIdNamespace, offset: 0, length: 4); - buffer.write(&peerIdId, offset: 0, length: 4); - buffer.write(&namespace, offset: 0, length: 4); - buffer.write(&id, offset: 0, length: 4); + buffer.write(&peerIdValue, offset: 0, length: 8) + buffer.write(&namespace, offset: 0, length: 4) + buffer.write(&id, offset: 0, length: 4) } public static func encodeArrayToBuffer(_ array: [MessageId], buffer: WriteBuffer) { @@ -103,7 +103,22 @@ public struct MessageIndex: Comparable, Hashable { self.timestamp = timestamp } - public func predecessor() -> MessageIndex { + public func globalPredecessor() -> MessageIndex { + let previousPeerId = self.id.peerId.predecessor + if previousPeerId != self.id.peerId { + return MessageIndex(id: MessageId(peerId: previousPeerId, namespace: self.id.namespace, id: self.id.id), timestamp: self.timestamp) + } else if self.id.id != 0 { + return MessageIndex(id: MessageId(peerId: self.id.peerId, namespace: self.id.namespace, id: self.id.id - 1), timestamp: self.timestamp) + } else if self.id.namespace != 0 { + return MessageIndex(id: MessageId(peerId: self.id.peerId, namespace: self.id.namespace - 1, id: Int32.max - 1), timestamp: self.timestamp) + } else if self.timestamp != 0 { + return MessageIndex(id: MessageId(peerId: self.id.peerId, namespace: Int32(Int8.max) - 1, id: Int32.max - 1), timestamp: self.timestamp - 1) + } else { + return self + } + } + + public func peerLocalPredecessor() -> MessageIndex { if self.id.id != 0 { return MessageIndex(id: MessageId(peerId: self.id.peerId, namespace: self.id.namespace, id: self.id.id - 1), timestamp: self.timestamp) } else if self.id.namespace != 0 { @@ -115,16 +130,25 @@ public struct MessageIndex: Comparable, Hashable { } } - public func successor() -> MessageIndex { + public func globalSuccessor() -> MessageIndex { + let nextPeerId = self.id.peerId.successor + if nextPeerId != self.id.peerId { + return MessageIndex(id: MessageId(peerId: nextPeerId, namespace: self.id.namespace, id: self.id.id), timestamp: self.timestamp) + } else { + return MessageIndex(id: MessageId(peerId: self.id.peerId, namespace: self.id.namespace, id: self.id.id == Int32.max ? self.id.id : (self.id.id + 1)), timestamp: self.timestamp) + } + } + + public func peerLocalSuccessor() -> MessageIndex { return MessageIndex(id: MessageId(peerId: self.id.peerId, namespace: self.id.namespace, id: self.id.id == Int32.max ? self.id.id : (self.id.id + 1)), timestamp: self.timestamp) } public static func absoluteUpperBound() -> MessageIndex { - return MessageIndex(id: MessageId(peerId: PeerId(namespace: Int32(Int8.max), id: Int32.max), namespace: Int32(Int8.max), id: Int32.max), timestamp: Int32.max) + return MessageIndex(id: MessageId(peerId: PeerId.max, namespace: Int32(Int8.max), id: Int32.max), timestamp: Int32.max) } public static func absoluteLowerBound() -> MessageIndex { - return MessageIndex(id: MessageId(peerId: PeerId(namespace: 0, id: 0), namespace: 0, id: 0), timestamp: 0) + return MessageIndex(id: MessageId(peerId: PeerId(0), namespace: 0, id: 0), timestamp: 0) } public static func lowerBound(peerId: PeerId) -> MessageIndex { @@ -209,11 +233,11 @@ public struct ChatListIndex: Comparable, Hashable { } public var predecessor: ChatListIndex { - return ChatListIndex(pinningIndex: self.pinningIndex, messageIndex: self.messageIndex.predecessor()) + return ChatListIndex(pinningIndex: self.pinningIndex, messageIndex: self.messageIndex.globalPredecessor()) } public var successor: ChatListIndex { - return ChatListIndex(pinningIndex: self.pinningIndex, messageIndex: self.messageIndex.successor()) + return ChatListIndex(pinningIndex: self.pinningIndex, messageIndex: self.messageIndex.globalSuccessor()) } } diff --git a/submodules/Postbox/Sources/MessageHistoryReadStateTable.swift b/submodules/Postbox/Sources/MessageHistoryReadStateTable.swift index fbbd8ffd59..15f2fb9af5 100644 --- a/submodules/Postbox/Sources/MessageHistoryReadStateTable.swift +++ b/submodules/Postbox/Sources/MessageHistoryReadStateTable.swift @@ -315,7 +315,7 @@ final class MessageHistoryReadStateTable: Table { readPastTopIndex = true } if maxIncomingReadIndex < messageIndex || markedUnread || readPastTopIndex { - let (realDeltaCount, holes, messageIds) = incomingStatsInRange(maxIncomingReadIndex.successor(), messageIndex) + let (realDeltaCount, holes, messageIds) = incomingStatsInRange(maxIncomingReadIndex.peerLocalSuccessor(), messageIndex) var deltaCount = realDeltaCount if readPastTopIndex { deltaCount = max(Int(count), deltaCount) @@ -366,7 +366,7 @@ final class MessageHistoryReadStateTable: Table { break case let .indexBased(maxIncomingReadIndex, maxOutgoingReadIndex, count, markedUnread): if maxOutgoingReadIndex < messageIndex { - let messageIds: [MessageId] = outgoingIndexStatsInRange(maxOutgoingReadIndex.successor(), messageIndex) + let messageIds: [MessageId] = outgoingIndexStatsInRange(maxOutgoingReadIndex.peerLocalSuccessor(), messageIndex) self.markReadStatesAsUpdated(messageIndex.id.peerId, namespaces: states.namespaces) states.namespaces[messageIndex.id.namespace] = .indexBased(maxIncomingReadIndex: maxIncomingReadIndex, maxOutgoingReadIndex: messageIndex, count: count, markedUnread: markedUnread) diff --git a/submodules/Postbox/Sources/MessageHistoryTable.swift b/submodules/Postbox/Sources/MessageHistoryTable.swift index 203849fc24..35f7affa81 100644 --- a/submodules/Postbox/Sources/MessageHistoryTable.swift +++ b/submodules/Postbox/Sources/MessageHistoryTable.swift @@ -223,7 +223,7 @@ final class MessageHistoryTable: Table { } private func processIndexOperations(_ peerId: PeerId, operations: [MessageHistoryIndexOperation], processedOperationsByPeerId: inout [PeerId: [MessageHistoryOperation]], updatedMedia: inout [MediaId: Media?], unsentMessageOperations: inout [IntermediateMessageHistoryUnsentOperation], updatedPeerReadStateOperations: inout [PeerId: PeerReadStateSynchronizationOperation?], globalTagsOperations: inout [GlobalMessageHistoryTagsOperation], pendingActionsOperations: inout [PendingMessageActionsOperation], updatedMessageActionsSummaries: inout [PendingMessageActionsSummaryKey: Int32], updatedMessageTagSummaries: inout [MessageHistoryTagsSummaryKey: MessageHistoryTagNamespaceSummary], invalidateMessageTagSummaries: inout [InvalidatedMessageHistoryTagsSummaryEntryOperation], localTagsOperations: inout [IntermediateMessageHistoryLocalTagsOperation], timestampBasedMessageAttributesOperations: inout [TimestampBasedMessageAttributesOperation]) { - let sharedKey = self.key(MessageIndex(id: MessageId(peerId: PeerId(namespace: 0, id: 0), namespace: 0, id: 0), timestamp: 0)) + let sharedKey = self.key(MessageIndex(id: MessageId(peerId: PeerId(0), namespace: 0, id: 0), timestamp: 0)) let sharedBuffer = WriteBuffer() let sharedEncoder = PostboxEncoder() diff --git a/submodules/Postbox/Sources/MessageHistoryView.swift b/submodules/Postbox/Sources/MessageHistoryView.swift index 71b6869b36..4966a9dea8 100644 --- a/submodules/Postbox/Sources/MessageHistoryView.swift +++ b/submodules/Postbox/Sources/MessageHistoryView.swift @@ -904,20 +904,20 @@ public final class MessageHistoryView { public let isLoading: Bool public let isAddedToChatList: Bool - public init(tagMask: MessageTags?, namespaces: MessageIdNamespaces, entries: [MessageHistoryEntry], holeEarlier: Bool) { + public init(tagMask: MessageTags?, namespaces: MessageIdNamespaces, entries: [MessageHistoryEntry], holeEarlier: Bool, holeLater: Bool, isLoading: Bool) { self.tagMask = tagMask self.namespaces = namespaces self.anchorIndex = .lowerBound self.earlierId = nil self.laterId = nil self.holeEarlier = holeEarlier - self.holeLater = false + self.holeLater = holeLater self.entries = entries self.maxReadIndex = nil self.fixedReadStates = nil self.topTaggedMessages = [] self.additionalData = [] - self.isLoading = false + self.isLoading = isLoading self.isAddedToChatList = false } @@ -1053,7 +1053,7 @@ public final class MessageHistoryView { index = 0 for entry in entries { if entry.index.id.peerId == peerId && entry.index.id.namespace == namespace { - maxNamespaceIndex = entry.index.predecessor() + maxNamespaceIndex = entry.index.peerLocalPredecessor() break } index += 1 @@ -1109,7 +1109,7 @@ public final class MessageHistoryView { index = 0 for entry in entries { if entry.index.id.peerId == peerId && entry.index.id.namespace == namespace { - maxNamespaceIndex = entry.index.predecessor() + maxNamespaceIndex = entry.index.peerLocalPredecessor() break } index += 1 diff --git a/submodules/Postbox/Sources/MessageHistoryViewState.swift b/submodules/Postbox/Sources/MessageHistoryViewState.swift index 98de702b7b..7257692472 100644 --- a/submodules/Postbox/Sources/MessageHistoryViewState.swift +++ b/submodules/Postbox/Sources/MessageHistoryViewState.swift @@ -468,11 +468,11 @@ private func sampleHoleRanges(input: MessageHistoryInput, orderedEntriesBySpace: if items.higherThanAnchor.count == 0 { clipRanges.append(MessageIndex.absoluteLowerBound() ... MessageIndex.absoluteUpperBound()) } else { - let clipIndex = items.higherThanAnchor[0].index.predecessor() + let clipIndex = items.higherThanAnchor[0].index.peerLocalPredecessor() clipRanges.append(MessageIndex.absoluteLowerBound() ... clipIndex) } } else { - let clipIndex = items.lowerOrAtAnchor[0].index.predecessor() + let clipIndex = items.lowerOrAtAnchor[0].index.peerLocalPredecessor() clipRanges.append(MessageIndex.absoluteLowerBound() ... clipIndex) } } else { @@ -480,7 +480,7 @@ private func sampleHoleRanges(input: MessageHistoryInput, orderedEntriesBySpace: if items.higherThanAnchor.count == 0 { clipRanges.append(MessageIndex.absoluteLowerBound() ... MessageIndex.absoluteUpperBound()) } else { - let clipIndex = items.higherThanAnchor[0].index.predecessor() + let clipIndex = items.higherThanAnchor[0].index.peerLocalPredecessor() clipRanges.append(MessageIndex.absoluteLowerBound() ... clipIndex) } } else { @@ -488,7 +488,7 @@ private func sampleHoleRanges(input: MessageHistoryInput, orderedEntriesBySpace: if indices.contains(Int(items.lowerOrAtAnchor[i + 1].index.id.id)) { clipIndex = items.lowerOrAtAnchor[i + 1].index } else { - clipIndex = items.lowerOrAtAnchor[i + 1].index.predecessor() + clipIndex = items.lowerOrAtAnchor[i + 1].index.peerLocalPredecessor() } clipRanges.append(MessageIndex.absoluteLowerBound() ... clipIndex) } @@ -536,11 +536,11 @@ private func sampleHoleRanges(input: MessageHistoryInput, orderedEntriesBySpace: if items.lowerOrAtAnchor.count == 0 { clipRanges.append(MessageIndex.absoluteLowerBound() ... MessageIndex.absoluteUpperBound()) } else { - let clipIndex = items.lowerOrAtAnchor[items.lowerOrAtAnchor.count - 1].index.successor() + let clipIndex = items.lowerOrAtAnchor[items.lowerOrAtAnchor.count - 1].index.peerLocalSuccessor() clipRanges.append(clipIndex ... MessageIndex.absoluteUpperBound()) } } else { - let clipIndex = items.higherThanAnchor[items.higherThanAnchor.count - 1].index.successor() + let clipIndex = items.higherThanAnchor[items.higherThanAnchor.count - 1].index.peerLocalSuccessor() clipRanges.append(clipIndex ... MessageIndex.absoluteUpperBound()) } } else { @@ -548,7 +548,7 @@ private func sampleHoleRanges(input: MessageHistoryInput, orderedEntriesBySpace: if items.lowerOrAtAnchor.count == 0 { clipRanges.append(MessageIndex.absoluteLowerBound() ... MessageIndex.absoluteUpperBound()) } else { - let clipIndex = items.lowerOrAtAnchor[items.lowerOrAtAnchor.count - 1].index.successor() + let clipIndex = items.lowerOrAtAnchor[items.lowerOrAtAnchor.count - 1].index.peerLocalSuccessor() clipRanges.append(clipIndex ... MessageIndex.absoluteUpperBound()) } } else { @@ -556,7 +556,7 @@ private func sampleHoleRanges(input: MessageHistoryInput, orderedEntriesBySpace: if indices.contains(Int(items.higherThanAnchor[i - 1].index.id.id)) { clipIndex = items.higherThanAnchor[i - 1].index } else { - clipIndex = items.higherThanAnchor[i - 1].index.successor() + clipIndex = items.higherThanAnchor[i - 1].index.peerLocalSuccessor() } clipRanges.append(clipIndex ... MessageIndex.absoluteUpperBound()) } diff --git a/submodules/Postbox/Sources/OrderedItemListTable.swift b/submodules/Postbox/Sources/OrderedItemListTable.swift index 7299cc8994..66c90f3f3d 100644 --- a/submodules/Postbox/Sources/OrderedItemListTable.swift +++ b/submodules/Postbox/Sources/OrderedItemListTable.swift @@ -29,17 +29,45 @@ final class OrderedItemListTable: Table { let key = ValueBoxKey(length: 1 + 4 + 4) key.setUInt8(0, value: OrderedItemListKeyNamespace.indexToId.rawValue) key.setInt32(1, value: collectionId) - key.setUInt32(5, value: itemIndex) + key.setUInt32(1 + 4, value: itemIndex) return key } + + private func keyIndexToIdLowerBound(collectionId: Int32) -> ValueBoxKey { + let key = ValueBoxKey(length: 1 + 4 + 4) + key.setUInt8(0, value: OrderedItemListKeyNamespace.indexToId.rawValue) + key.setInt32(1, value: collectionId) + return key + } + + private func keyIndexToIdUpperBound(collectionId: Int32) -> ValueBoxKey { + let key = ValueBoxKey(length: 1 + 4 + 4) + key.setUInt8(0, value: OrderedItemListKeyNamespace.indexToId.rawValue) + key.setInt32(1, value: collectionId) + return key.successor + } private func keyIdToIndex(collectionId: Int32, id: MemoryBuffer) -> ValueBoxKey { let key = ValueBoxKey(length: 1 + 4 + id.length) key.setUInt8(0, value: OrderedItemListKeyNamespace.idToIndex.rawValue) key.setInt32(1, value: collectionId) - memcpy(key.memory.advanced(by: 5), id.memory, id.length) + memcpy(key.memory.advanced(by: 1 + 4), id.memory, id.length) return key } + + private func keyIdToIndexLowerBound(collectionId: Int32) -> ValueBoxKey { + let key = ValueBoxKey(length: 1 + 4) + key.setUInt8(0, value: OrderedItemListKeyNamespace.idToIndex.rawValue) + key.setInt32(1, value: collectionId) + return key + } + + private func keyIdToIndexUpperBound(collectionId: Int32) -> ValueBoxKey { + let key = ValueBoxKey(length: 1 + 4) + key.setUInt8(0, value: OrderedItemListKeyNamespace.idToIndex.rawValue) + key.setInt32(1, value: collectionId) + return key.successor + } func getItemIds(collectionId: Int32) -> [MemoryBuffer] { var itemIds: [MemoryBuffer] = [] @@ -119,6 +147,8 @@ final class OrderedItemListTable: Table { self.indexTable.remove(collectionId: collectionId, id: id) } + + assert(Set(items.map({ $0.id.makeData() })).count == items.count) for i in 0 ..< items.count { self.valueBox.set(self.table, key: self.keyIndexToId(collectionId: collectionId, itemIndex: UInt32(i)), value: items[i].id) var indexValue: UInt32 = UInt32(i) diff --git a/submodules/Postbox/Sources/Peer.swift b/submodules/Postbox/Sources/Peer.swift index 2f6dc2f3ca..48943109a3 100644 --- a/submodules/Postbox/Sources/Peer.swift +++ b/submodules/Postbox/Sources/Peer.swift @@ -1,11 +1,136 @@ import Foundation public struct PeerId: Hashable, CustomStringConvertible, Comparable, Codable { - public typealias Namespace = Int32 - public typealias Id = Int32 + public struct Namespace: Comparable, Hashable, Codable, CustomStringConvertible { + public static var max: Namespace { + return Namespace(rawValue: 0x7) + } + + fileprivate var rawValue: UInt32 + + var predecessor: Namespace { + if self.rawValue != 0 { + return Namespace(rawValue: self.rawValue - 1) + } else { + return self + } + } + + var successor: Namespace { + if self.rawValue != Namespace.max.rawValue { + return Namespace(rawValue: self.rawValue + 1) + } else { + return self + } + } + + public var description: String { + return "\(self.rawValue)" + } + + fileprivate init(rawValue: UInt32) { + precondition((rawValue | 0x7) == 0x7) + + self.rawValue = rawValue + } + + public static func _internalFromInt32Value(_ value: Int32) -> Namespace { + return Namespace(rawValue: UInt32(bitPattern: value)) + } + + public func _internalGetInt32Value() -> Int32 { + return Int32(bitPattern: self.rawValue) + } + + public static func <(lhs: Namespace, rhs: Namespace) -> Bool { + return lhs.rawValue < rhs.rawValue + } + } + + public struct Id: Comparable, Hashable, Codable { + public static var min: Id { + return Id(rawValue: 0) + } + + public static var max: Id { + return Id(rawValue: 0x000000007fffffff) + } + + fileprivate var rawValue: Int32 + + var predecessor: Id { + if self.rawValue != 0 { + return Id(rawValue: self.rawValue - 1) + } else { + return self + } + } + + var successor: Id { + if self.rawValue != Id.max.rawValue { + return Id(rawValue: self.rawValue + 1) + } else { + return self + } + } + + public var description: String { + return "\(self.rawValue)" + } + + fileprivate init(rawValue: Int32) { + //precondition((rawValue | 0x000FFFFFFFFFFFFF) == 0x000FFFFFFFFFFFFF) + + self.rawValue = rawValue + } + + public static func _internalFromInt32Value(_ value: Int32) -> Id { + return Id(rawValue: value) + } + + public func _internalGetInt32Value() -> Int32 { + return self.rawValue + } + + public static func <(lhs: Id, rhs: Id) -> Bool { + return lhs.rawValue < rhs.rawValue + } + } + + public static var max: PeerId { + return PeerId(namespace: .max, id: .max) + } public let namespace: Namespace public let id: Id + + var predecessor: PeerId { + let previousId = self.id.predecessor + if previousId != self.id { + return PeerId(namespace: self.namespace, id: previousId) + } else { + let previousNamespace = self.namespace.predecessor + if previousNamespace != self.namespace { + return PeerId(namespace: previousNamespace, id: .max) + } else { + return self + } + } + } + + var successor: PeerId { + let nextId = self.id.successor + if nextId != self.id { + return PeerId(namespace: self.namespace, id: nextId) + } else { + let nextNamespace = self.namespace.successor + if nextNamespace != self.namespace { + return PeerId(namespace: nextNamespace, id: .min) + } else { + return self + } + } + } public init(namespace: Namespace, id: Id) { self.namespace = namespace @@ -13,12 +138,54 @@ public struct PeerId: Hashable, CustomStringConvertible, Comparable, Codable { } public init(_ n: Int64) { - self.namespace = Int32((n >> 32) & 0x7fffffff) - self.id = Int32(bitPattern: UInt32(n & 0xffffffff)) + let data = UInt64(bitPattern: n) + + let legacyNamespaceBits = ((data >> 32) & 0xffffffff) + let idLowBits = data & 0xffffffff + + if legacyNamespaceBits == 0x7fffffff && idLowBits == 0 { + self.namespace = .max + self.id = Id(rawValue: Int32(bitPattern: UInt32(clamping: idLowBits))) + } else { + let namespaceBits = ((data >> 32) & 0x7) + self.namespace = Namespace(rawValue: UInt32(namespaceBits)) + + let idHighBits = (data >> (32 + 3)) & 0xffffffff + //assert(idHighBits == 0) + + self.id = Id(rawValue: Int32(bitPattern: UInt32(clamping: idLowBits))) + } } public func toInt64() -> Int64 { - return (Int64(self.namespace) << 32) | Int64(bitPattern: UInt64(UInt32(bitPattern: self.id))) + let idLowBits = UInt32(bitPattern: self.id.rawValue) + + let result: Int64 + if self.namespace == .max && self.id.rawValue == 0 { + var data: UInt64 = 0 + + let namespaceBits: UInt64 = 0x7fffffff + data |= namespaceBits << 32 + + data |= UInt64(idLowBits) + + result = Int64(bitPattern: data) + } else { + var data: UInt64 = 0 + data |= UInt64(self.namespace.rawValue) << 32 + + let idValue = UInt32(bitPattern: self.id.rawValue) + let idHighBits = (idValue >> 32) & 0x3FFFFFFF + assert(idHighBits == 0) + + data |= UInt64(idLowBits) + + result = Int64(bitPattern: data) + } + + assert(PeerId(result) == self) + + return result } public static func encodeArrayToBuffer(_ array: [PeerId], buffer: WriteBuffer) { @@ -46,32 +213,23 @@ public struct PeerId: Hashable, CustomStringConvertible, Comparable, Codable { return array } - public var hashValue: Int { - get { - return Int(self.id) - } - } - public var description: String { get { return "\(namespace):\(id)" } } - public init(_ buffer: ReadBuffer) { - var namespace: Int32 = 0 - var id: Int32 = 0 - memcpy(&namespace, buffer.memory, 4) - self.namespace = namespace - memcpy(&id, buffer.memory + 4, 4) - self.id = id - } + /*public init(_ buffer: ReadBuffer) { + var value: Int64 = 0 + memcpy(&value, buffer.memory, 8) + buffer.offset += 8 + + self.init(value) + }*/ public func encodeToBuffer(_ buffer: WriteBuffer) { - var namespace = self.namespace - var id = self.id - buffer.write(&namespace, offset: 0, length: 4); - buffer.write(&id, offset: 0, length: 4); + var value = self.toInt64() + buffer.write(&value, offset: 0, length: 8); } public static func <(lhs: PeerId, rhs: PeerId) -> Bool { diff --git a/submodules/Postbox/Sources/Postbox.swift b/submodules/Postbox/Sources/Postbox.swift index 5b0f799b5f..1a93b8aee3 100644 --- a/submodules/Postbox/Sources/Postbox.swift +++ b/submodules/Postbox/Sources/Postbox.swift @@ -985,6 +985,11 @@ public final class Transaction { assert(!self.disposed) self.postbox?.scanMessages(peerId: peerId, namespace: namespace, tag: tag, f) } + + public func scanTopMessages(peerId: PeerId, namespace: MessageId.Namespace, limit: Int, _ f: (Message) -> Bool) { + assert(!self.disposed) + self.postbox?.scanTopMessages(peerId: peerId, namespace: namespace, limit: limit, f) + } public func scanMessageAttributes(peerId: PeerId, namespace: MessageId.Namespace, limit: Int, _ f: (MessageId, [MessageAttribute]) -> Bool) { self.postbox?.scanMessageAttributes(peerId: peerId, namespace: namespace, limit: limit, f) @@ -1091,6 +1096,11 @@ public final class Transaction { assert(!self.disposed) return self.postbox?.searchPeers(query: query) ?? [] } + + public func clearTimestampBasedAttribute(id: MessageId, tag: UInt16) { + assert(!self.disposed) + self.postbox?.clearTimestampBasedAttribute(id: id, tag: tag) + } } public enum PostboxResult { @@ -1129,6 +1139,8 @@ public func openPostbox(basePath: String, seedConfiguration: SeedConfiguration, let queue = sharedQueue return Signal { subscriber in queue.async { + postboxLog("openPostbox, basePath: \(basePath), useCopy: \(useCopy)") + let _ = try? FileManager.default.createDirectory(atPath: basePath, withIntermediateDirectories: true, attributes: nil) var tempDir: TempBoxDirectory? @@ -1149,6 +1161,7 @@ public func openPostbox(basePath: String, seedConfiguration: SeedConfiguration, return } } else { + postboxLog("openPostbox, error1") subscriber.putNext(.error) return } @@ -1163,10 +1176,14 @@ public func openPostbox(basePath: String, seedConfiguration: SeedConfiguration, #endif let startTime = CFAbsoluteTimeGetCurrent() + + postboxLog("openPostbox, initialize SqliteValueBox") guard var valueBox = SqliteValueBox(basePath: dbBasePath, queue: queue, isTemporary: isTemporary, isReadOnly: isReadOnly, encryptionParameters: encryptionParameters, upgradeProgress: { progress in + postboxLog("openPostbox, SqliteValueBox upgrading progress \(progress)") subscriber.putNext(.upgrading(progress)) }) else { + postboxLog("openPostbox, SqliteValueBox open error") subscriber.putNext(.error) return } @@ -1176,10 +1193,13 @@ public func openPostbox(basePath: String, seedConfiguration: SeedConfiguration, let userVersion: Int32? = metadataTable.userVersion() let currentUserVersion: Int32 = 25 + + postboxLog("openPostbox, current userVersion: \(userVersion ?? nil)") if let userVersion = userVersion { if userVersion != currentUserVersion { if isTemporary { + postboxLog("openPostbox, isTemporary = true, not upgrading") subscriber.putNext(.error) return } else { @@ -1241,9 +1261,12 @@ public func openPostbox(basePath: String, seedConfiguration: SeedConfiguration, } let endTime = CFAbsoluteTimeGetCurrent() - print("Postbox load took \((endTime - startTime) * 1000.0) ms") + postboxLog("Postbox load took \((endTime - startTime) * 1000.0) ms") subscriber.putNext(.postbox(Postbox(queue: queue, basePath: basePath, seedConfiguration: seedConfiguration, valueBox: valueBox, timestampForAbsoluteTimeBasedOperations: timestampForAbsoluteTimeBasedOperations, isTemporary: isTemporary, tempDir: tempDir))) + + postboxLog("openPostbox, putCompletion") + subscriber.putCompletion() break } @@ -1407,22 +1430,11 @@ public final class Postbox { self.seedConfiguration = seedConfiguration self.tempDir = tempDir - print("MediaBox path: \(self.basePath + "/media")") + postboxLog("MediaBox path: \(basePath + "/media")") self.mediaBox = MediaBox(basePath: self.basePath + "/media") self.valueBox = valueBox - /*self.pipeNotifier = PipeNotifier(basePath: basePath, notify: { [weak self] in - //if let strongSelf = self { - /*strongSelf.queue.async { - if strongSelf.valueBox != nil { - let _ = strongSelf.transaction({ _ -> Void in - }).start() - } - }*/ - //} - })*/ - self.metadataTable = MetadataTable(valueBox: self.valueBox, table: MetadataTable.tableSpec(0)) self.keychainTable = KeychainTable(valueBox: self.valueBox, table: KeychainTable.tableSpec(1)) @@ -1557,7 +1569,7 @@ public final class Postbox { }) ) - print("(Postbox initialization took \((CFAbsoluteTimeGetCurrent() - startTime) * 1000.0) ms") + postboxLog("(Postbox initialization took \((CFAbsoluteTimeGetCurrent() - startTime) * 1000.0) ms") let _ = self.transaction({ transaction -> Void in let reindexUnreadVersion: Int32 = 2 @@ -3409,6 +3421,26 @@ public final class Postbox { } } } + + fileprivate func scanTopMessages(peerId: PeerId, namespace: MessageId.Namespace, limit: Int, _ f: (Message) -> Bool) { + let lowerBound = MessageIndex.lowerBound(peerId: peerId, namespace: namespace) + var index = MessageIndex.upperBound(peerId: peerId, namespace: namespace) + var remainingLimit = limit + while remainingLimit > 0 { + let messages = self.messageHistoryTable.fetch(peerId: peerId, namespace: namespace, tag: nil, threadId: nil, from: index, includeFrom: false, to: lowerBound, limit: 10) + remainingLimit -= 10 + for message in messages { + if !f(self.renderIntermediateMessage(message)) { + break + } + } + if let last = messages.last { + index = last.index + } else { + break + } + } + } fileprivate func scanMessageAttributes(peerId: PeerId, namespace: MessageId.Namespace, limit: Int, _ f: (MessageId, [MessageAttribute]) -> Bool) { var remainingLimit = limit @@ -3538,6 +3570,10 @@ public final class Postbox { } } } + + fileprivate func clearTimestampBasedAttribute(id: MessageId, tag: UInt16) { + self.timestampBasedMessageAttributesTable.remove(tag: tag, id: id, operations: &self.currentTimestampBasedMessageAttributesOperations) + } fileprivate func reindexUnreadCounters() { self.groupMessageStatsTable.removeAll() diff --git a/submodules/Postbox/Sources/PostboxUpgrade_15to16.swift b/submodules/Postbox/Sources/PostboxUpgrade_15to16.swift index 62a4714b43..e8d8d9334f 100644 --- a/submodules/Postbox/Sources/PostboxUpgrade_15to16.swift +++ b/submodules/Postbox/Sources/PostboxUpgrade_15to16.swift @@ -211,10 +211,8 @@ func postboxUpgrade_15to16(metadataTable: MetadataTable, valueBox: ValueBox, pro valueBox.scanInt64(chatListIndexTable, values: { key, value in let peerId = PeerId(key) - if peerId.namespace != Int32.max { - if parseInclusionIndex(peerId: peerId, value: value) { - includedPeerIds.append(peerId) - } + if parseInclusionIndex(peerId: peerId, value: value) { + includedPeerIds.append(peerId) } return true }) diff --git a/submodules/Postbox/Sources/PostboxUpgrade_16to17.swift b/submodules/Postbox/Sources/PostboxUpgrade_16to17.swift index 3bdbfea670..030c1fce46 100644 --- a/submodules/Postbox/Sources/PostboxUpgrade_16to17.swift +++ b/submodules/Postbox/Sources/PostboxUpgrade_16to17.swift @@ -215,10 +215,8 @@ func postboxUpgrade_16to17(metadataTable: MetadataTable, valueBox: ValueBox, pro valueBox.scanInt64(chatListIndexTable, values: { key, value in let peerId = PeerId(key) - if peerId.namespace != Int32.max { - if parseInclusionIndex(peerId: peerId, value: value) { - includedPeerIds.append(peerId) - } + if parseInclusionIndex(peerId: peerId, value: value) { + includedPeerIds.append(peerId) } return true }) diff --git a/submodules/Postbox/Sources/PostboxUpgrade_19to20.swift b/submodules/Postbox/Sources/PostboxUpgrade_19to20.swift index 2faa64bec9..074275cd5b 100644 --- a/submodules/Postbox/Sources/PostboxUpgrade_19to20.swift +++ b/submodules/Postbox/Sources/PostboxUpgrade_19to20.swift @@ -424,7 +424,7 @@ func postboxUpgrade_19to20(metadataTable: MetadataTable, valueBox: ValueBox, pro var removeChatListKeys: [ValueBoxKey] = [] valueBox.scan(chatListTable, keys: { key in let (_, _, index, type) = extractChatListKey(key) - if index.id.peerId.namespace != 3 { // Secret Chat + if index.id.peerId.namespace._internalGetInt32Value() != 3 { // Secret Chat sharedChatListIndexKey.setInt64(0, value: index.id.peerId.toInt64()) valueBox.remove(chatListIndexTable, key: sharedChatListIndexKey, secure: false) diff --git a/submodules/Postbox/Sources/PostboxUpgrade_20to21.swift b/submodules/Postbox/Sources/PostboxUpgrade_20to21.swift index f33b8459a2..00fd244141 100644 --- a/submodules/Postbox/Sources/PostboxUpgrade_20to21.swift +++ b/submodules/Postbox/Sources/PostboxUpgrade_20to21.swift @@ -426,7 +426,7 @@ func postboxUpgrade_20to21(metadataTable: MetadataTable, valueBox: ValueBox, pro var removeChatListKeys: [ValueBoxKey] = [] valueBox.scan(chatListTable, keys: { key in let (_, _, index, type) = extractChatListKey(key) - if index.id.peerId.namespace != 3 { // Secret Chat + if index.id.peerId.namespace._internalGetInt32Value() != 3 { // Secret Chat sharedChatListIndexKey.setInt64(0, value: index.id.peerId.toInt64()) valueBox.remove(chatListIndexTable, key: sharedChatListIndexKey, secure: false) diff --git a/submodules/Postbox/Sources/SqliteValueBox.swift b/submodules/Postbox/Sources/SqliteValueBox.swift index 0ae732a92a..b9409cecc4 100644 --- a/submodules/Postbox/Sources/SqliteValueBox.swift +++ b/submodules/Postbox/Sources/SqliteValueBox.swift @@ -65,7 +65,7 @@ struct SqlitePreparedStatement { } return res == SQLITE_ROW } - + struct SqlError: Error { var code: Int32 } @@ -234,10 +234,8 @@ public final class SqliteValueBox: ValueBox { let _ = try? FileManager.default.createDirectory(atPath: basePath, withIntermediateDirectories: true, attributes: nil) let path = basePath + "/db_sqlite" - - #if DEBUG - print("Instance \(self) opening sqlite at \(path)") - #endif + + postboxLog("Instance \(self) opening sqlite at \(path)") #if DEBUG let exists = FileManager.default.fileExists(atPath: path) @@ -297,8 +295,10 @@ public final class SqliteValueBox: ValueBox { let _ = try? FileManager.default.removeItem(atPath: path) preconditionFailure("Couldn't open database") } - - sqlite3_busy_timeout(database.handle, 1000 * 10000) + + postboxLog("Did open DB at \(path)") + + sqlite3_busy_timeout(database.handle, 5 * 1000) var resultCode: Bool = true @@ -306,8 +306,12 @@ public final class SqliteValueBox: ValueBox { assert(resultCode) resultCode = database.execute("PRAGMA cipher_default_plaintext_header_size=32") assert(resultCode) + + postboxLog("Did set up cipher") if self.isEncrypted(database) { + postboxLog("Database is encrypted") + if let encryptionParameters = encryptionParameters { precondition(encryptionParameters.salt.data.count == 16) precondition(encryptionParameters.key.data.count == 32) @@ -316,12 +320,15 @@ public final class SqliteValueBox: ValueBox { resultCode = database.execute("PRAGMA key=\"x'\(hexKey)'\"") assert(resultCode) + + postboxLog("Setting encryption key") if self.isEncrypted(database) { + postboxLog("Encryption key is invalid") + if isTemporary || isReadOnly { return nil } - postboxLog("Encryption key is invalid") for fileName in dabaseFileNames { let _ = try? FileManager.default.removeItem(atPath: basePath + "/\(fileName)") @@ -354,6 +361,8 @@ public final class SqliteValueBox: ValueBox { assert(resultCode) } } else if let encryptionParameters = encryptionParameters, encryptionParameters.forceEncryptionIfNoSet { + postboxLog("Not encrypted") + let hexKey = hexString(encryptionParameters.key.data + encryptionParameters.salt.data) if FileManager.default.fileExists(atPath: path) { @@ -409,6 +418,8 @@ public final class SqliteValueBox: ValueBox { } } } + + postboxLog("Did set up encryption") //database.execute("PRAGMA cache_size=-2097152") resultCode = database.execute("PRAGMA mmap_size=0") @@ -421,6 +432,9 @@ public final class SqliteValueBox: ValueBox { assert(resultCode) resultCode = database.execute("PRAGMA cipher_memory_security = OFF") assert(resultCode) + + postboxLog("Did set up pragmas") + //resultCode = database.execute("PRAGMA wal_autocheckpoint=500") //database.execute("PRAGMA journal_size_limit=1536") @@ -441,8 +455,12 @@ public final class SqliteValueBox: ValueBox { let _ = self.runPragma(database, "checkpoint_fullfsync = 1") assert(self.runPragma(database, "checkpoint_fullfsync") == "1") + + postboxLog("Did set up checkpoint_fullfsync") self.beginInternal(database: database) + + postboxLog("Did begin transaction") let result = self.getUserVersion(database) @@ -462,8 +480,12 @@ public final class SqliteValueBox: ValueBox { for table in self.listFullTextTables(database) { self.fullTextTables[table.id] = table } + + postboxLog("Did load tables") self.commitInternal(database: database) + + postboxLog("Did commit final") lock.unlock() @@ -518,7 +540,21 @@ public final class SqliteValueBox: ValueBox { private func isEncrypted(_ database: Database) -> Bool { var statement: OpaquePointer? = nil + postboxLog("isEncrypted prepare...") + + let allIsOk = Atomic(value: false) + let databasePath = self.databasePath + DispatchQueue.global().asyncAfter(deadline: .now() + 5.0, execute: { + if allIsOk.with({ $0 }) == false { + postboxLog("Timeout reached, discarding database") + try? FileManager.default.removeItem(atPath: databasePath) + + exit(0) + } + }) let status = sqlite3_prepare_v2(database.handle, "SELECT * FROM sqlite_master LIMIT 1", -1, &statement, nil) + let _ = allIsOk.swap(true) + postboxLog("isEncrypted prepare done") if statement == nil { postboxLog("isEncrypted: sqlite3_prepare_v2 status = \(status) [\(self.databasePath)]") return true @@ -536,6 +572,7 @@ public final class SqliteValueBox: ValueBox { preparedStatement.destroy() return true } + postboxLog("isEncrypted step done") preparedStatement.destroy() return status == SQLITE_NOTADB } diff --git a/submodules/Postbox/Sources/TimeBasedCleanup.swift b/submodules/Postbox/Sources/TimeBasedCleanup.swift index f8b020c907..0230cca372 100644 --- a/submodules/Postbox/Sources/TimeBasedCleanup.swift +++ b/submodules/Postbox/Sources/TimeBasedCleanup.swift @@ -170,7 +170,7 @@ private final class TimeBasedCleanupImpl { let generalPaths = self.generalPaths let shortLivedPaths = self.shortLivedPaths let scanOnce = Signal { subscriber in - DispatchQueue.global(qos: .utility).async { + DispatchQueue.global(qos: .background).async { var removedShortLivedCount: Int = 0 var removedGeneralCount: Int = 0 var removedGeneralLimitCount: Int = 0 diff --git a/submodules/Postbox/Sources/TimestampBasedMessageAttributesTable.swift b/submodules/Postbox/Sources/TimestampBasedMessageAttributesTable.swift index 8285b09ee2..541cf987e4 100644 --- a/submodules/Postbox/Sources/TimestampBasedMessageAttributesTable.swift +++ b/submodules/Postbox/Sources/TimestampBasedMessageAttributesTable.swift @@ -1,6 +1,6 @@ import Foundation -public struct TimestampBasedMessageAttributesEntry { +public struct TimestampBasedMessageAttributesEntry: CustomStringConvertible { public let tag: UInt16 public let timestamp: Int32 public let messageId: MessageId @@ -8,6 +8,10 @@ public struct TimestampBasedMessageAttributesEntry { public var index: MessageIndex { return MessageIndex(id: self.messageId, timestamp: timestamp) } + + public var description: String { + return "(tag: \(self.tag), timestamp: \(self.timestamp), messageId: \(self.messageId))" + } } enum TimestampBasedMessageAttributesOperation { @@ -53,7 +57,11 @@ final class TimestampBasedMessageAttributesTable: Table { } func set(tag: UInt16, id: MessageId, timestamp: Int32, operations: inout [TimestampBasedMessageAttributesOperation]) { - if let previousTimestamp = self.indexTable.get(tag: tag, id: id) { + let previousTimestamp = self.indexTable.get(tag: tag, id: id) + + postboxLog("TimestampBasedMessageAttributesTable set(tag: \(tag), id: \(id), timestamp: \(timestamp)) previousTimestamp: \(String(describing: previousTimestamp))") + + if let previousTimestamp = previousTimestamp { if previousTimestamp == timestamp { return } else { @@ -67,7 +75,11 @@ final class TimestampBasedMessageAttributesTable: Table { } func remove(tag: UInt16, id: MessageId, operations: inout [TimestampBasedMessageAttributesOperation]) { - if let previousTimestamp = self.indexTable.get(tag: tag, id: id) { + let previousTimestamp = self.indexTable.get(tag: tag, id: id) + + postboxLog("TimestampBasedMessageAttributesTable remove(tag: \(tag), id: \(id)) previousTimestamp: \(String(describing: previousTimestamp))") + + if let previousTimestamp = previousTimestamp { self.valueBox.remove(self.table, key: self.key(tag: tag, timestamp: previousTimestamp, id: id), secure: false) self.indexTable.remove(tag: tag, id: id) operations.append(.remove(TimestampBasedMessageAttributesEntry(tag: tag, timestamp: previousTimestamp, messageId: id))) diff --git a/submodules/Postbox/Sources/TimestampBasedMessageAttributesView.swift b/submodules/Postbox/Sources/TimestampBasedMessageAttributesView.swift index bbff66b6d0..a253f1ba23 100644 --- a/submodules/Postbox/Sources/TimestampBasedMessageAttributesView.swift +++ b/submodules/Postbox/Sources/TimestampBasedMessageAttributesView.swift @@ -7,6 +7,8 @@ final class MutableTimestampBasedMessageAttributesView { init(postbox: Postbox, tag: UInt16) { self.tag = tag self.head = postbox.timestampBasedMessageAttributesTable.head(tag: tag) + + postboxLog("MutableTimestampBasedMessageAttributesView: tag: \(tag) head: \(String(describing: self.head))") } func replay(postbox: Postbox, operations: [TimestampBasedMessageAttributesOperation]) -> Bool { diff --git a/submodules/PresentationDataUtils/Sources/AlertTheme.swift b/submodules/PresentationDataUtils/Sources/AlertTheme.swift index a2d4ddb5a2..9fbfb15c0f 100644 --- a/submodules/PresentationDataUtils/Sources/AlertTheme.swift +++ b/submodules/PresentationDataUtils/Sources/AlertTheme.swift @@ -6,11 +6,15 @@ import SwiftSignalKit import TelegramPresentationData public func textAlertController(context: AccountContext, forceTheme: PresentationTheme? = nil, title: String?, text: String, actions: [TextAlertAction], actionLayout: TextAlertContentActionLayout = .horizontal, allowInputInset: Bool = true, dismissOnOutsideTap: Bool = true) -> AlertController { - var presentationData = context.sharedContext.currentPresentationData.with { $0 } + return textAlertController(sharedContext: context.sharedContext, forceTheme: forceTheme, title: title, text: text, actions: actions, actionLayout: actionLayout, allowInputInset: allowInputInset, dismissOnOutsideTap: dismissOnOutsideTap) +} + +public func textAlertController(sharedContext: SharedAccountContext, forceTheme: PresentationTheme? = nil, title: String?, text: String, actions: [TextAlertAction], actionLayout: TextAlertContentActionLayout = .horizontal, allowInputInset: Bool = true, dismissOnOutsideTap: Bool = true) -> AlertController { + var presentationData = sharedContext.currentPresentationData.with { $0 } if let forceTheme = forceTheme { presentationData = presentationData.withUpdated(theme: forceTheme) } - return textAlertController(alertContext: AlertControllerContext(theme: AlertControllerTheme(presentationData: presentationData), themeSignal: context.sharedContext.presentationData |> map { + return textAlertController(alertContext: AlertControllerContext(theme: AlertControllerTheme(presentationData: presentationData), themeSignal: sharedContext.presentationData |> map { presentationData in var presentationData = presentationData if let forceTheme = forceTheme { diff --git a/submodules/PresentationDataUtils/Sources/OpenUrl.swift b/submodules/PresentationDataUtils/Sources/OpenUrl.swift index 25b894efca..8ab09feb64 100644 --- a/submodules/PresentationDataUtils/Sources/OpenUrl.swift +++ b/submodules/PresentationDataUtils/Sources/OpenUrl.swift @@ -1,11 +1,12 @@ import Foundation import Display import SwiftSignalKit +import Postbox import AccountContext import OverlayStatusController import UrlWhitelist -public func openUserGeneratedUrl(context: AccountContext, url: String, concealed: Bool, skipUrlAuth: Bool = false, present: @escaping (ViewController) -> Void, openResolved: @escaping (ResolvedUrl) -> Void) { +public func openUserGeneratedUrl(context: AccountContext, peerId: PeerId?, url: String, concealed: Bool, skipUrlAuth: Bool = false, present: @escaping (ViewController) -> Void, openResolved: @escaping (ResolvedUrl) -> Void) { var concealed = concealed let presentationData = context.sharedContext.currentPresentationData.with { $0 } @@ -31,7 +32,7 @@ public func openUserGeneratedUrl(context: AccountContext, url: String, concealed cancelImpl = { disposable.dispose() } - disposable.set((context.sharedContext.resolveUrl(account: context.account, url: url, skipUrlAuth: skipUrlAuth) + disposable.set((context.sharedContext.resolveUrl(context: context, peerId: peerId, url: url, skipUrlAuth: skipUrlAuth) |> afterDisposed { Queue.mainQueue().async { progressDisposable.dispose() diff --git a/submodules/QrCode/Sources/QrCode.swift b/submodules/QrCode/Sources/QrCode.swift index 5e26ae2357..eb3cb3c082 100644 --- a/submodules/QrCode/Sources/QrCode.swift +++ b/submodules/QrCode/Sources/QrCode.swift @@ -46,7 +46,7 @@ public func qrCode(string: String, color: UIColor, backgroundColor: UIColor? = n if let output = filter.outputImage { let size = Int(output.extent.width) - let bytesPerRow = (4 * Int(size) + 15) & (~15) + let bytesPerRow = DeviceGraphicsContextSettings.shared.bytesPerRow(forWidth: Int(size)) let length = bytesPerRow * size let bitmapInfo = CGBitmapInfo(rawValue: CGBitmapInfo.byteOrder32Little.rawValue | CGImageAlphaInfo.noneSkipFirst.rawValue) diff --git a/submodules/RadialStatusNode/Sources/RadialStatusBackgroundNode.swift b/submodules/RadialStatusNode/Sources/RadialStatusBackgroundNode.swift index d32ae107a7..8dad789975 100644 --- a/submodules/RadialStatusNode/Sources/RadialStatusBackgroundNode.swift +++ b/submodules/RadialStatusNode/Sources/RadialStatusBackgroundNode.swift @@ -1,48 +1,6 @@ import Foundation import UIKit import AsyncDisplayKit +import Display -private final class RadialStatusBackgroundNodeParameters: NSObject { - let color: UIColor - - init(color: UIColor) { - self.color = color - } -} -final class RadialStatusBackgroundNode: ASDisplayNode { - var color: UIColor { - didSet { - self.setNeedsDisplay() - } - } - - init(color: UIColor, synchronous: Bool) { - self.color = color - - super.init() - - self.displaysAsynchronously = !synchronous - self.isLayerBacked = true - self.isOpaque = false - } - - override func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol? { - return RadialStatusBackgroundNodeParameters(color: self.color) - } - - @objc override 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) - } - - if let parameters = parameters as? RadialStatusBackgroundNodeParameters { - context.setFillColor(parameters.color.cgColor) - context.fillEllipse(in: bounds) - } - } -} diff --git a/submodules/RadialStatusNode/Sources/RadialStatusNode.swift b/submodules/RadialStatusNode/Sources/RadialStatusNode.swift index 9bfbcbb715..28670b84b2 100644 --- a/submodules/RadialStatusNode/Sources/RadialStatusNode.swift +++ b/submodules/RadialStatusNode/Sources/RadialStatusNode.swift @@ -1,6 +1,7 @@ import Foundation import UIKit import AsyncDisplayKit +import Display public enum RadialStatusNodeState: Equatable { case none @@ -190,14 +191,19 @@ public final class RadialStatusNode: ASControlNode { self.transitionToBackgroundColor(self.state.backgroundColor(color: self.backgroundNodeColor), previousContentNode: nil, animated: false, synchronous: false, completion: {}) } } - + + private let enableBlur: Bool + public private(set) var state: RadialStatusNodeState = .none - private var backgroundNode: RadialStatusBackgroundNode? + private var backgroundNode: NavigationBackgroundNode? + private var currentBackgroundNodeColor: UIColor? + private var contentNode: RadialStatusContentNode? private var nextContentNode: RadialStatusContentNode? - public init(backgroundNodeColor: UIColor) { + public init(backgroundNodeColor: UIColor, enableBlur: Bool = false) { + self.enableBlur = enableBlur self.backgroundNodeColor = backgroundNodeColor super.init() @@ -287,7 +293,7 @@ public final class RadialStatusNode: ASControlNode { } private func transitionToBackgroundColor(_ color: UIColor?, previousContentNode: RadialStatusContentNode?, animated: Bool, synchronous: Bool, completion: @escaping () -> Void) { - let currentColor = self.backgroundNode?.color + let currentColor = self.currentBackgroundNodeColor var updated = false if let color = color, let currentColor = currentColor { @@ -299,11 +305,16 @@ public final class RadialStatusNode: ASControlNode { if updated { if let color = color { if let backgroundNode = self.backgroundNode { - backgroundNode.color = color + backgroundNode.updateColor(color: color, transition: .immediate) + self.currentBackgroundNodeColor = color + completion() } else { - let backgroundNode = RadialStatusBackgroundNode(color: color, synchronous: synchronous) + let backgroundNode = NavigationBackgroundNode(color: color, enableBlur: self.enableBlur) + self.currentBackgroundNodeColor = color + backgroundNode.frame = self.bounds + backgroundNode.update(size: backgroundNode.bounds.size, cornerRadius: backgroundNode.bounds.size.height / 2.0, transition: .immediate) self.backgroundNode = backgroundNode self.insertSubnode(backgroundNode, at: 0) @@ -318,6 +329,7 @@ public final class RadialStatusNode: ASControlNode { } } else if let backgroundNode = self.backgroundNode { self.backgroundNode = nil + self.currentBackgroundNodeColor = nil if animated { backgroundNode.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false) previousContentNode?.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false) @@ -336,7 +348,10 @@ public final class RadialStatusNode: ASControlNode { } override public func layout() { - self.backgroundNode?.frame = self.bounds + if let backgroundNode = self.backgroundNode { + backgroundNode.frame = self.bounds + backgroundNode.update(size: backgroundNode.bounds.size, cornerRadius: backgroundNode.bounds.size.height / 2.0, transition: .immediate) + } if let contentNode = self.contentNode { contentNode.frame = self.bounds } diff --git a/submodules/SSignalKit/SwiftSignalKit/Source/Bag.swift b/submodules/SSignalKit/SwiftSignalKit/Source/Bag.swift index 08d5aef3a4..59d287b874 100644 --- a/submodules/SSignalKit/SwiftSignalKit/Source/Bag.swift +++ b/submodules/SSignalKit/SwiftSignalKit/Source/Bag.swift @@ -1,5 +1,17 @@ import Foundation +public final class Weak { + private weak var _value: T? + + public var value: T? { + return self._value + } + + public init(_ value: T) { + self._value = value + } +} + public final class Bag { public typealias Index = Int private var nextIndex: Index = 0 @@ -73,6 +85,46 @@ public final class Bag { } } +public final class SparseBag: Sequence { + public typealias Index = Int + private var nextIndex: Index = 0 + private var items: [Index: T] = [:] + + public init() { + } + + public func add(_ item: T) -> Index { + let key = self.nextIndex + self.nextIndex += 1 + self.items[key] = item + + return key + } + + public func get(_ index: Index) -> T? { + return self.items[index] + } + + public func remove(_ index: Index) { + self.items.removeValue(forKey: index) + } + + public func removeAll() { + self.items.removeAll() + } + + public var isEmpty: Bool { + return self.items.isEmpty + } + + public func makeIterator() -> AnyIterator { + var iterator = self.items.makeIterator() + return AnyIterator { () -> T? in + return iterator.next()?.value + } + } +} + public final class CounterBag { private var nextIndex: Int = 1 private var items = Set() diff --git a/submodules/SSignalKit/SwiftSignalKit/Source/Signal_Combine.swift b/submodules/SSignalKit/SwiftSignalKit/Source/Signal_Combine.swift index 8dc1fed2ed..6f569eac70 100644 --- a/submodules/SSignalKit/SwiftSignalKit/Source/Signal_Combine.swift +++ b/submodules/SSignalKit/SwiftSignalKit/Source/Signal_Combine.swift @@ -148,12 +148,18 @@ 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> { +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, _ s1: Signal, _ s2: Signal, _ s3: Signal, _ s4: Signal, _ s5: Signal, _ s6: Signal, _ s7: Signal, _ s8: Signal, _ s9: Signal, _ s10: Signal, _ s11: Signal, _ s12: Signal) -> Signal<(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12), 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), signalOfAny(s12)], 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, values[11] as! T12) + }, 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/Sources/SaveToCameraRoll.swift b/submodules/SaveToCameraRoll/Sources/SaveToCameraRoll.swift index 7e72afbe0d..b9a2d40f90 100644 --- a/submodules/SaveToCameraRoll/Sources/SaveToCameraRoll.swift +++ b/submodules/SaveToCameraRoll/Sources/SaveToCameraRoll.swift @@ -96,7 +96,7 @@ public func saveToCameraRoll(context: AccountContext, postbox: Postbox, mediaRef return } - let tempVideoPath = NSTemporaryDirectory() + "\(arc4random64()).mp4" + let tempVideoPath = NSTemporaryDirectory() + "\(Int64.random(in: Int64.min ... Int64.max)).mp4" PHPhotoLibrary.shared().performChanges({ if isImage { if let fileData = try? Data(contentsOf: URL(fileURLWithPath: data.path)) { diff --git a/submodules/SearchBarNode/Sources/SearchBarNode.swift b/submodules/SearchBarNode/Sources/SearchBarNode.swift index 94ffecf976..2756d839c7 100644 --- a/submodules/SearchBarNode/Sources/SearchBarNode.swift +++ b/submodules/SearchBarNode/Sources/SearchBarNode.swift @@ -26,7 +26,6 @@ private func generateBackground(foregroundColor: UIColor, diameter: CGFloat) -> }, opaque: false)?.stretchableImage(withLeftCapWidth: Int(diameter / 2.0), topCapHeight: Int(diameter / 2.0)) } - public struct SearchBarToken { public struct Style { public let backgroundColor: UIColor @@ -620,9 +619,9 @@ public final class SearchBarNodeTheme: Equatable { self.keyboard = keyboard } - public init(theme: PresentationTheme, hasSeparator: Bool = true) { - self.background = theme.rootController.navigationBar.backgroundColor - self.separator = hasSeparator ? theme.rootController.navigationBar.separatorColor : theme.rootController.navigationBar.backgroundColor + public init(theme: PresentationTheme, hasBackground: Bool = true, hasSeparator: Bool = true) { + self.background = hasBackground ? theme.rootController.navigationBar.blurredBackgroundColor : .clear + self.separator = hasSeparator ? theme.rootController.navigationBar.separatorColor : theme.rootController.navigationBar.blurredBackgroundColor self.inputFill = theme.rootController.navigationSearchBar.inputFillColor self.placeholder = theme.rootController.navigationSearchBar.inputPlaceholderTextColor self.primaryText = theme.rootController.navigationSearchBar.inputTextColor @@ -714,7 +713,7 @@ public class SearchBarNode: ASDisplayNode, UITextFieldDelegate { public var tokensUpdated: (([SearchBarToken]) -> Void)? - private let backgroundNode: ASDisplayNode + private let backgroundNode: NavigationBackgroundNode private let separatorNode: ASDisplayNode private let textBackgroundNode: ASDisplayNode private var activityIndicator: ActivityIndicator? @@ -808,13 +807,14 @@ public class SearchBarNode: ASDisplayNode, UITextFieldDelegate { private var strings: PresentationStrings? private let cancelText: String? - public init(theme: SearchBarNodeTheme, strings: PresentationStrings, fieldStyle: SearchBarStyle = .legacy, forceSeparator: Bool = false, cancelText: String? = nil) { + public init(theme: SearchBarNodeTheme, strings: PresentationStrings, fieldStyle: SearchBarStyle = .legacy, forceSeparator: Bool = false, displayBackground: Bool = true, cancelText: String? = nil) { self.fieldStyle = fieldStyle self.forceSeparator = forceSeparator self.cancelText = cancelText - self.backgroundNode = ASDisplayNode() - self.backgroundNode.isLayerBacked = true + self.backgroundNode = NavigationBackgroundNode(color: theme.background) + self.backgroundNode.isUserInteractionEnabled = false + self.backgroundNode.isHidden = !displayBackground self.separatorNode = ASDisplayNode() self.separatorNode.isLayerBacked = true @@ -887,7 +887,7 @@ public class SearchBarNode: ASDisplayNode, UITextFieldDelegate { self.cancelButton.setAttributedTitle(NSAttributedString(string: self.cancelText ?? strings.Common_Cancel, font: self.cancelText != nil ? Font.semibold(17.0) : Font.regular(17.0), textColor: theme.accent), for: []) } if self.theme != theme { - self.backgroundNode.backgroundColor = theme.background + self.backgroundNode.updateColor(color: theme.background, transition: .immediate) if self.fieldStyle != .modern || self.forceSeparator { self.separatorNode.backgroundColor = theme.separator } @@ -914,6 +914,7 @@ public class SearchBarNode: ASDisplayNode, UITextFieldDelegate { self.validLayout = (boundingSize, leftInset, rightInset) self.backgroundNode.frame = self.bounds + self.backgroundNode.update(size: self.backgroundNode.bounds.size, transition: .immediate) transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: self.bounds.size.height), size: CGSize(width: self.bounds.size.width, height: UIScreenPixel))) let verticalOffset: CGFloat = boundingSize.height - 82.0 @@ -928,7 +929,7 @@ public class SearchBarNode: ASDisplayNode, UITextFieldDelegate { let textBackgroundFrame = CGRect(origin: CGPoint(x: contentFrame.minX + padding, y: verticalOffset + textBackgroundHeight), size: CGSize(width: contentFrame.width - padding * 2.0 - (self.hasCancelButton ? cancelButtonSize.width + 11.0 : 0.0), height: textBackgroundHeight)) transition.updateFrame(node: self.textBackgroundNode, frame: textBackgroundFrame) - let textFrame = CGRect(origin: CGPoint(x: textBackgroundFrame.minX + 24.0, y: textBackgroundFrame.minY), size: CGSize(width: max(1.0, textBackgroundFrame.size.width - 24.0 - 20.0), height: textBackgroundFrame.size.height)) + let textFrame = CGRect(origin: CGPoint(x: textBackgroundFrame.minX + 24.0, y: textBackgroundFrame.minY), size: CGSize(width: max(1.0, textBackgroundFrame.size.width - 24.0 - 27.0), height: textBackgroundFrame.size.height)) if let iconImage = self.iconNode.image { let iconSize = iconImage.size @@ -1044,7 +1045,9 @@ public class SearchBarNode: ASDisplayNode, UITextFieldDelegate { separatorCompleted = true intermediateCompletion() }) - + + self.textBackgroundNode.isHidden = true + self.textBackgroundNode.layer.animateFrame(from: self.textBackgroundNode.frame, to: targetTextBackgroundFrame, duration: duration, timingFunction: timingFunction, removeOnCompletion: false, completion: { _ in textBackgroundCompleted = true intermediateCompletion() @@ -1056,7 +1059,7 @@ public class SearchBarNode: ASDisplayNode, UITextFieldDelegate { transitionBackgroundNode.backgroundColor = node.backgroundNode.backgroundColor transitionBackgroundNode.cornerRadius = node.backgroundNode.cornerRadius self.insertSubnode(transitionBackgroundNode, aboveSubnode: self.textBackgroundNode) - transitionBackgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration / 2.0, removeOnCompletion: false) + //transitionBackgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration / 2.0, removeOnCompletion: false) transitionBackgroundNode.layer.animateFrame(from: self.textBackgroundNode.frame, to: targetTextBackgroundFrame, duration: duration, timingFunction: timingFunction, removeOnCompletion: false) let textFieldFrame = self.textField.frame diff --git a/submodules/SearchPeerMembers/Sources/SearchPeerMembers.swift b/submodules/SearchPeerMembers/Sources/SearchPeerMembers.swift index 05c001fd06..23329b5e41 100644 --- a/submodules/SearchPeerMembers/Sources/SearchPeerMembers.swift +++ b/submodules/SearchPeerMembers/Sources/SearchPeerMembers.swift @@ -18,7 +18,7 @@ public func searchPeerMembers(context: AccountContext, peerId: PeerId, chatLocat |> mapToSignal { cachedData -> Signal<([Peer], Bool), NoError> in if case .peer = chatLocation, let cachedData = cachedData, let memberCount = cachedData.participantsSummary.memberCount, memberCount <= 64 { return Signal { subscriber in - let (disposable, _) = context.peerChannelMemberCategoriesContextsManager.recent(postbox: context.account.postbox, network: context.account.network, accountPeerId: context.account.peerId, peerId: peerId, searchQuery: nil, requestUpdate: false, updated: { state in + let (disposable, _) = context.peerChannelMemberCategoriesContextsManager.recent(engine: context.engine, postbox: context.account.postbox, network: context.account.network, accountPeerId: context.account.peerId, peerId: peerId, searchQuery: nil, requestUpdate: false, updated: { state in if case .ready = state.loadingState { let normalizedQuery = query.lowercased() subscriber.putNext((state.list.compactMap { participant -> Peer? in @@ -54,7 +54,7 @@ public func searchPeerMembers(context: AccountContext, peerId: PeerId, chatLocat return Signal { subscriber in switch chatLocation { case let .peer(peerId): - let (disposable, _) = context.peerChannelMemberCategoriesContextsManager.recent(postbox: context.account.postbox, network: context.account.network, accountPeerId: context.account.peerId, peerId: peerId, searchQuery: query.isEmpty ? nil : query, updated: { state in + let (disposable, _) = context.peerChannelMemberCategoriesContextsManager.recent(engine: context.engine, postbox: context.account.postbox, network: context.account.network, accountPeerId: context.account.peerId, peerId: peerId, searchQuery: query.isEmpty ? nil : query, updated: { state in if case .ready = state.loadingState { subscriber.putNext((state.list.compactMap { participant in if participant.peer.isDeleted { @@ -69,7 +69,7 @@ public func searchPeerMembers(context: AccountContext, peerId: PeerId, chatLocat disposable.dispose() } case let .replyThread(replyThreadMessage): - let (disposable, _) = context.peerChannelMemberCategoriesContextsManager.mentions(postbox: context.account.postbox, network: context.account.network, accountPeerId: context.account.peerId, peerId: peerId, threadMessageId: replyThreadMessage.messageId, searchQuery: query.isEmpty ? nil : query, updated: { state in + let (disposable, _) = context.peerChannelMemberCategoriesContextsManager.mentions(engine: context.engine, postbox: context.account.postbox, network: context.account.network, accountPeerId: context.account.peerId, peerId: peerId, threadMessageId: replyThreadMessage.messageId, searchQuery: query.isEmpty ? nil : query, updated: { state in if case .ready = state.loadingState { subscriber.putNext((state.list.compactMap { participant in if participant.peer.isDeleted { @@ -117,6 +117,6 @@ public func searchPeerMembers(context: AccountContext, peerId: PeerId, chatLocat } } } else { - return searchGroupMembers(postbox: context.account.postbox, network: context.account.network, accountPeerId: context.account.peerId, peerId: peerId, query: query) + return context.engine.peers.searchGroupMembers(peerId: peerId, query: query) } } diff --git a/submodules/SearchUI/Sources/NavigationBarSearchContentNode.swift b/submodules/SearchUI/Sources/NavigationBarSearchContentNode.swift index 6c1ab5d9be..a2bcfca7b0 100644 --- a/submodules/SearchUI/Sources/NavigationBarSearchContentNode.swift +++ b/submodules/SearchUI/Sources/NavigationBarSearchContentNode.swift @@ -41,7 +41,7 @@ public class NavigationBarSearchContentNode: NavigationBarContentNode { self.placeholder = placeholder self.placeholderNode.accessibilityLabel = placeholder if let disabledOverlay = self.disabledOverlay { - disabledOverlay.backgroundColor = theme.rootController.navigationBar.backgroundColor.withAlphaComponent(0.5) + disabledOverlay.backgroundColor = theme.rootController.navigationBar.opaqueBackgroundColor.withAlphaComponent(0.5) } if let validLayout = self.validLayout { self.updatePlaceholder(self.expansionProgress, size: validLayout.0, leftInset: validLayout.1, rightInset: validLayout.2, transition: .immediate) @@ -88,34 +88,9 @@ public class NavigationBarSearchContentNode: NavigationBarContentNode { } public func setIsEnabled(_ enabled: Bool, animated: Bool = false) { - if !enabled { - if self.disabledOverlay == nil { - let disabledOverlay = ASDisplayNode() - self.addSubnode(disabledOverlay) - self.disabledOverlay = disabledOverlay - if animated { - disabledOverlay.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) - } - } - if let disabledOverlay = self.disabledOverlay { - disabledOverlay.backgroundColor = self.theme?.rootController.navigationBar.backgroundColor.withAlphaComponent(0.4) - - var disabledOverlayFrame = self.placeholderNode.frame - if let searchBarHeight = self.placeholderHeight { - disabledOverlayFrame.size.height = searchBarHeight - } - disabledOverlay.frame = disabledOverlayFrame - } - } else if let disabledOverlay = self.disabledOverlay { - self.disabledOverlay = nil - if animated { - disabledOverlay.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak disabledOverlay] _ in - disabledOverlay?.removeFromSupernode() - }) - } else { - disabledOverlay.removeFromSupernode() - } - } + let transition: ContainedViewLayoutTransition = animated ? .animated(duration: 0.25, curve: .easeInOut) : .immediate + transition.updateAlpha(node: self.placeholderNode, alpha: enabled ? 1.0 : 0.6) + self.placeholderNode.isUserInteractionEnabled = enabled } private func updatePlaceholder(_ progress: CGFloat, size: CGSize, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition) { @@ -130,7 +105,7 @@ public class NavigationBarSearchContentNode: NavigationBarContentNode { let overscrollProgress = max(0.0, max(0.0, self.expansionProgress - 1.0 + fraction) / fraction - visibleProgress) let searchBarNodeLayout = self.placeholderNode.asyncLayout() - let (searchBarHeight, searchBarApply) = searchBarNodeLayout(NSAttributedString(string: self.placeholder, font: searchBarFont, textColor: self.theme?.rootController.navigationSearchBar.inputPlaceholderTextColor ?? UIColor(rgb: 0x8e8e93)), CGSize(width: baseWidth, height: fieldHeight), visibleProgress, self.theme?.rootController.navigationSearchBar.inputPlaceholderTextColor ?? UIColor(rgb: 0x8e8e93), self.theme?.rootController.navigationSearchBar.inputFillColor ?? .clear, self.theme?.rootController.navigationBar.backgroundColor ?? .clear, transition) + let (searchBarHeight, searchBarApply) = searchBarNodeLayout(NSAttributedString(string: self.placeholder, font: searchBarFont, textColor: self.theme?.rootController.navigationSearchBar.inputPlaceholderTextColor ?? UIColor(rgb: 0x8e8e93)), CGSize(width: baseWidth, height: fieldHeight), visibleProgress, self.theme?.rootController.navigationSearchBar.inputPlaceholderTextColor ?? UIColor(rgb: 0x8e8e93), self.theme?.rootController.navigationSearchBar.inputFillColor ?? .clear, self.theme?.rootController.navigationBar.opaqueBackgroundColor ?? .clear, transition) searchBarApply() let searchBarFrame = CGRect(origin: CGPoint(x: padding + leftInset, y: 8.0 + overscrollProgress * fieldHeight), size: CGSize(width: baseWidth, height: fieldHeight)) diff --git a/submodules/SearchUI/Sources/SearchDisplayController.swift b/submodules/SearchUI/Sources/SearchDisplayController.swift index e176758e03..341bebff44 100644 --- a/submodules/SearchUI/Sources/SearchDisplayController.swift +++ b/submodules/SearchUI/Sources/SearchDisplayController.swift @@ -37,8 +37,8 @@ public final class SearchDisplayController { private var isSearchingDisposable: Disposable? - public init(presentationData: PresentationData, mode: SearchDisplayControllerMode = .navigation, placeholder: String? = nil, hasSeparator: Bool = false, contentNode: SearchDisplayControllerContentNode, cancel: @escaping () -> Void) { - self.searchBar = SearchBarNode(theme: SearchBarNodeTheme(theme: presentationData.theme, hasSeparator: hasSeparator), strings: presentationData.strings, fieldStyle: .modern, forceSeparator: hasSeparator) + public init(presentationData: PresentationData, mode: SearchDisplayControllerMode = .navigation, placeholder: String? = nil, hasBackground: Bool = false, hasSeparator: Bool = false, contentNode: SearchDisplayControllerContentNode, cancel: @escaping () -> Void) { + self.searchBar = SearchBarNode(theme: SearchBarNodeTheme(theme: presentationData.theme, hasBackground: hasBackground, hasSeparator: hasSeparator), strings: presentationData.strings, fieldStyle: .modern, forceSeparator: hasSeparator, displayBackground: hasBackground) self.backgroundNode = BackgroundNode() self.backgroundNode.backgroundColor = presentationData.theme.chatList.backgroundColor self.backgroundNode.allowsGroupOpacity = true @@ -231,6 +231,9 @@ public final class SearchDisplayController { self.searchBar.activate() if let placeholder = placeholder { self.searchBar.animateIn(from: placeholder, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring) + if self.contentNode.hasDim { + self.contentNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue) + } } 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) @@ -255,17 +258,6 @@ public final class SearchDisplayController { let backgroundNode = self.backgroundNode let contentNode = self.contentNode if animated { - if let placeholder = placeholder, let (layout, navigationBarHeight) = self.containerLayout { - let contentNodePosition = self.backgroundNode.layer.position - let targetTextBackgroundFrame = placeholder.convert(placeholder.backgroundNode.frame, to: nil) - - var contentNavigationBarHeight = navigationBarHeight - if layout.statusBarHeight == nil { - contentNavigationBarHeight += 28.0 - } - - self.backgroundNode.layer.animatePosition(from: contentNodePosition, to: CGPoint(x: contentNodePosition.x, y: contentNodePosition.y + (targetTextBackgroundFrame.maxY + 8.0 - contentNavigationBarHeight)), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) - } backgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak backgroundNode] _ in backgroundNode?.removeFromSupernode() }) diff --git a/submodules/SelectablePeerNode/Sources/SelectablePeerNode.swift b/submodules/SelectablePeerNode/Sources/SelectablePeerNode.swift index 02a8516c32..5042a4a69b 100644 --- a/submodules/SelectablePeerNode/Sources/SelectablePeerNode.swift +++ b/submodules/SelectablePeerNode/Sources/SelectablePeerNode.swift @@ -138,6 +138,7 @@ public final class SelectablePeerNode: ASDisplayNode { } public func setup(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, peer: RenderedPeer, online: Bool = false, numberOfLines: Int = 2, synchronousLoad: Bool) { + let isFirstTime = self.peer == nil self.peer = peer guard let mainPeer = peer.chatMainPeer else { return @@ -165,7 +166,7 @@ public final class SelectablePeerNode: ASDisplayNode { let onlineLayout = self.onlineNode.asyncLayout() let (onlineSize, onlineApply) = onlineLayout(online, false) - let _ = onlineApply(false) + let _ = onlineApply(!isFirstTime) self.onlineNode.setImage(PresentationResourcesChatList.recentStatusOnlineIcon(theme, state: .panel), color: nil, transition: .immediate) self.onlineNode.frame = CGRect(origin: CGPoint(), size: onlineSize) diff --git a/submodules/SemanticStatusNode/BUILD b/submodules/SemanticStatusNode/BUILD index 9f6c4667f7..43a601eeee 100644 --- a/submodules/SemanticStatusNode/BUILD +++ b/submodules/SemanticStatusNode/BUILD @@ -10,6 +10,10 @@ swift_library( "//submodules/Display:Display", "//submodules/AsyncDisplayKit:AsyncDisplayKit", "//submodules/LegacyComponents:LegacyComponents", + "//submodules/GZip:GZip", + "//submodules/rlottie:RLottieBinding", + "//submodules/AppBundle:AppBundle", + "//submodules/ManagedAnimationNode:ManagedAnimationNode" ], visibility = [ "//visibility:public", diff --git a/submodules/SemanticStatusNode/Sources/SemanticStatusNode.swift b/submodules/SemanticStatusNode/Sources/SemanticStatusNode.swift index f0ece247d4..dcf38e6d72 100644 --- a/submodules/SemanticStatusNode/Sources/SemanticStatusNode.swift +++ b/submodules/SemanticStatusNode/Sources/SemanticStatusNode.swift @@ -3,6 +3,10 @@ import UIKit import AsyncDisplayKit import Display import SwiftSignalKit +import RLottieBinding +import GZip +import AppBundle +import ManagedAnimationNode public enum SemanticStatusNodeState: Equatable { public struct ProgressAppearance: Equatable { @@ -38,6 +42,7 @@ private protocol SemanticStatusNodeStateDrawingState: NSObjectProtocol { private protocol SemanticStatusNodeStateContext: class { var isAnimating: Bool { get } + var requestUpdate: () -> Void { get set } func drawingState(transitionFraction: CGFloat) -> SemanticStatusNodeStateDrawingState } @@ -87,10 +92,12 @@ private final class SemanticStatusNodeIconContext: SemanticStatusNodeStateContex final class DrawingState: NSObject, SemanticStatusNodeStateDrawingState { let transitionFraction: CGFloat let icon: SemanticStatusNodeIcon - - init(transitionFraction: CGFloat, icon: SemanticStatusNodeIcon) { + let iconImage: UIImage? + + init(transitionFraction: CGFloat, icon: SemanticStatusNodeIcon, iconImage: UIImage?) { self.transitionFraction = transitionFraction self.icon = icon + self.iconImage = iconImage super.init() } @@ -116,38 +123,65 @@ private final class SemanticStatusNodeIconContext: SemanticStatusNodeStateContex 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) + + + let size: CGSize + var offset: CGFloat = 0.0 + if let iconImage = self.iconImage { + size = iconImage.size + } else { + offset = 1.5 + size = CGSize(width: 15.0, height: 18.0) + } + context.translateBy(x: (diameter - size.width) / 2.0 + offset, 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 let iconImage = self.iconImage { + context.saveGState() + let iconRect = CGRect(origin: CGPoint(), size: iconImage.size) + context.clip(to: iconRect, mask: iconImage.cgImage!) + context.fill(iconRect) + context.restoreGState() + } else { + 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) + context.translateBy(x: -(diameter - size.width) / 2.0 - offset, 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) + let size: CGSize + if let iconImage = self.iconImage { + size = iconImage.size + } else { + 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 let iconImage = self.iconImage { + context.saveGState() + let iconRect = CGRect(origin: CGPoint(), size: iconImage.size) + context.clip(to: iconRect, mask: iconImage.cgImage!) + context.fill(iconRect) + context.restoreGState() + } else { + 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) @@ -156,7 +190,6 @@ private final class SemanticStatusNodeIconContext: SemanticStatusNodeStateContex 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() @@ -207,18 +240,36 @@ private final class SemanticStatusNodeIconContext: SemanticStatusNodeStateContex } } - let icon: SemanticStatusNodeIcon + var icon: SemanticStatusNodeIcon { + didSet { + self.animationNode?.enqueueState(self.icon == .play ? .play : .pause, animated: self.iconImage != nil) + } + } + + var animationNode: PlayPauseIconNode? + var iconImage: UIImage? init(icon: SemanticStatusNodeIcon) { self.icon = icon + + if [.play, .pause].contains(icon) { + self.animationNode = PlayPauseIconNode() + self.animationNode?.imageUpdated = { [weak self] image in + self?.iconImage = image + self?.requestUpdate() + } + self.iconImage = self.animationNode?.image + } } var isAnimating: Bool { return false } + var requestUpdate: () -> Void = {} + func drawingState(transitionFraction: CGFloat) -> SemanticStatusNodeStateDrawingState { - return DrawingState(transitionFraction: transitionFraction, icon: self.icon) + return DrawingState(transitionFraction: transitionFraction, icon: self.icon, iconImage: self.iconImage) } } @@ -231,11 +282,11 @@ private final class SemanticStatusNodeProgressTransition { self.initialValue = initialValue } - func valueAt(timestamp: Double, actualValue: CGFloat) -> CGFloat { + func valueAt(timestamp: Double, actualValue: CGFloat) -> (CGFloat, Bool) { 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 + return (t * actualValue + (1.0 - t) * self.initialValue, t >= 1.0 - 0.001) } } @@ -373,6 +424,8 @@ private final class SemanticStatusNodeProgressContext: SemanticStatusNodeStateCo return true } + var requestUpdate: () -> Void = {} + init(value: CGFloat?, displayCancel: Bool, appearance: SemanticStatusNodeState.ProgressAppearance?) { self.value = value self.displayCancel = displayCancel @@ -385,7 +438,11 @@ private final class SemanticStatusNodeProgressContext: SemanticStatusNodeStateCo let resolvedValue: CGFloat? if let value = self.value { if let transition = self.transition { - resolvedValue = transition.valueAt(timestamp: timestamp, actualValue: value) + let (v, isCompleted) = transition.valueAt(timestamp: timestamp, actualValue: value) + resolvedValue = v + if isCompleted { + self.transition = nil + } } else { resolvedValue = value } @@ -395,14 +452,18 @@ private final class SemanticStatusNodeProgressContext: SemanticStatusNodeStateCo return DrawingState(transitionFraction: transitionFraction, value: resolvedValue, displayCancel: self.displayCancel, appearance: self.appearance, timestamp: timestamp) } + func maskView() -> UIView? { + return nil + } + func updateValue(value: CGFloat?) { if value != self.value { - let previousValue = value + let previousValue = self.value self.value = value let timestamp = CACurrentMediaTime() if let _ = value, let previousValue = previousValue { if let transition = self.transition { - self.transition = SemanticStatusNodeProgressTransition(beginTime: timestamp, initialValue: transition.valueAt(timestamp: timestamp, actualValue: previousValue)) + self.transition = SemanticStatusNodeProgressTransition(beginTime: timestamp, initialValue: transition.valueAt(timestamp: timestamp, actualValue: previousValue).0) } else { self.transition = SemanticStatusNodeProgressTransition(beginTime: timestamp, initialValue: previousValue) } @@ -494,6 +555,8 @@ private final class SemanticStatusNodeCheckContext: SemanticStatusNodeStateConte return true } + var requestUpdate: () -> Void = {} + init(value: CGFloat, appearance: SemanticStatusNodeState.CheckAppearance?) { self.value = value self.appearance = appearance @@ -506,13 +569,21 @@ private final class SemanticStatusNodeCheckContext: SemanticStatusNodeStateConte let resolvedValue: CGFloat if let transition = self.transition { - resolvedValue = transition.valueAt(timestamp: timestamp, actualValue: value) + let (v, isCompleted) = transition.valueAt(timestamp: timestamp, actualValue: value) + resolvedValue = v + if isCompleted { + self.transition = nil + } } else { resolvedValue = value } return DrawingState(transitionFraction: transitionFraction, value: resolvedValue, appearance: self.appearance) } + func maskView() -> UIView? { + return nil + } + func animate() { guard self.value < 1.0 else { return @@ -542,8 +613,15 @@ private extension SemanticStatusNodeState { default: preconditionFailure() } - if let current = current as? SemanticStatusNodeIconContext, current.icon == icon { - return current + if let current = current as? SemanticStatusNodeIconContext { + if current.icon == icon { + return current + } else if (current.icon == .play && icon == .pause) || (current.icon == .pause && icon == .play) { + current.icon = icon + return current + } else { + return SemanticStatusNodeIconContext(icon: icon) + } } else { return SemanticStatusNodeIconContext(icon: icon) } @@ -774,6 +852,16 @@ public final class SemanticStatusNode: ASControlNode { private var disposable: Disposable? private var backgroundNodeImage: UIImage? + private let hasLayoutPromise = ValuePromise(false, ignoreRepeated: true) + + public override func layout() { + super.layout() + + if !self.bounds.width.isZero { + self.hasLayoutPromise.set(true) + } + } + public init(backgroundNodeColor: UIColor, foregroundNodeColor: UIColor, image: Signal<(TransformImageArguments) -> DrawingContext?, NoError>? = nil, overlayForegroundNodeColor: UIColor? = nil, cutout: CGRect? = nil) { self.state = .none self.stateContext = self.state.context(current: nil) @@ -786,9 +874,8 @@ public final class SemanticStatusNode: ASControlNode { if let image = image { let start = CACurrentMediaTime() - self.disposable = (image - |> deliverOnMainQueue).start(next: { [weak self] transform in - guard let strongSelf = self else { + self.disposable = combineLatest(queue: Queue.mainQueue(), image, self.hasLayoutPromise.get()).start(next: { [weak self] transform, ready in + guard let strongSelf = self, ready else { return } let context = transform(TransformImageArguments(corners: ImageCorners(radius: strongSelf.bounds.width / 2.0), imageSize: strongSelf.bounds.size, boundingSize: strongSelf.bounds.size, intrinsicInsets: UIEdgeInsets())) @@ -854,6 +941,9 @@ public final class SemanticStatusNode: ASControlNode { self.state = state let previousStateContext = self.stateContext self.stateContext = self.state.context(current: self.stateContext) + self.stateContext.requestUpdate = { [weak self] in + self?.setNeedsDisplay() + } if animated && previousStateContext !== self.stateContext { self.transitionContext = SemanticStatusNodeTransitionContext(startTime: CACurrentMediaTime(), duration: 0.18, previousStateContext: previousStateContext, previousAppearanceContext: nil, completion: completion) @@ -927,3 +1017,53 @@ public final class SemanticStatusNode: ASControlNode { parameters.appearanceState.drawForeground(context: context, size: bounds.size) } } + +private enum PlayPauseIconNodeState: Equatable { + case play + case pause +} + +private final class PlayPauseIconNode: ManagedAnimationNode { + private let duration: Double = 0.35 + private var iconState: PlayPauseIconNodeState = .play + + init() { + super.init(size: CGSize(width: 36.0, height: 36.0)) + + self.trackTo(item: ManagedAnimationItem(source: .local("anim_playpause"), frames: .range(startFrame: 0, endFrame: 0), duration: 0.01)) + } + + func enqueueState(_ state: PlayPauseIconNodeState, animated: Bool) { + guard self.iconState != state else { + return + } + + let previousState = self.iconState + self.iconState = state + + switch previousState { + case .pause: + switch state { + case .play: + if animated { + self.trackTo(item: ManagedAnimationItem(source: .local("anim_playpause"), frames: .range(startFrame: 41, endFrame: 83), duration: self.duration)) + } else { + self.trackTo(item: ManagedAnimationItem(source: .local("anim_playpause"), frames: .range(startFrame: 0, endFrame: 0), duration: 0.01)) + } + case .pause: + break + } + case .play: + switch state { + case .pause: + if animated { + self.trackTo(item: ManagedAnimationItem(source: .local("anim_playpause"), frames: .range(startFrame: 0, endFrame: 41), duration: self.duration)) + } else { + self.trackTo(item: ManagedAnimationItem(source: .local("anim_playpause"), frames: .range(startFrame: 41, endFrame: 41), duration: 0.01)) + } + case .play: + break + } + } + } +} diff --git a/submodules/SettingsUI/BUILD b/submodules/SettingsUI/BUILD index 97155853ad..aef7b6fda1 100644 --- a/submodules/SettingsUI/BUILD +++ b/submodules/SettingsUI/BUILD @@ -88,6 +88,8 @@ swift_library( "//submodules/AuthTransferUI:AuthTransferUI", "//submodules/WidgetSetupScreen:WidgetSetupScreen", "//submodules/UIKitRuntimeUtils:UIKitRuntimeUtils", + "//submodules/DebugSettingsUI:DebugSettingsUI", + "//submodules/WallpaperBackgroundNode:WallpaperBackgroundNode", ], visibility = [ "//visibility:public", diff --git a/submodules/SettingsUI/Sources/BubbleSettings/BubbleSettingsController.swift b/submodules/SettingsUI/Sources/BubbleSettings/BubbleSettingsController.swift index 94145bbdb7..06bce910f7 100644 --- a/submodules/SettingsUI/Sources/BubbleSettings/BubbleSettingsController.swift +++ b/submodules/SettingsUI/Sources/BubbleSettings/BubbleSettingsController.swift @@ -13,6 +13,7 @@ import ChatListUI import WallpaperResources import LegacyComponents import ItemListUI +import WallpaperBackgroundNode private func generateMaskImage(color: UIColor) -> UIImage? { return generateImage(CGSize(width: 1.0, height: 80.0), opaque: false, rotatedContext: { size, context in @@ -62,18 +63,15 @@ private final class BubbleSettingsControllerNode: ASDisplayNode, UIScrollViewDel self.scrollNode = ASScrollNode() - self.chatBackgroundNode = WallpaperBackgroundNode() + self.chatBackgroundNode = WallpaperBackgroundNode(context: context) 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.chatBackgroundNode.update(wallpaper: self.presentationData.chatWallpaper) + self.chatBackgroundNode.updateBubbleTheme(bubbleTheme: self.presentationData.theme, bubbleCorners: self.presentationData.chatBubbleCorners) self.toolbarNode = BubbleSettingsToolbarNode(presentationThemeSettings: self.presentationThemeSettings, presentationData: self.presentationData) @@ -162,7 +160,7 @@ private final class BubbleSettingsControllerNode: ASDisplayNode, UIScrollViewDel 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 peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt32Value(1)) let otherPeerId = self.context.account.peerId var peers = SimpleDictionary() var messages = SimpleDictionary() @@ -173,20 +171,20 @@ private final class BubbleSettingsControllerNode: ASDisplayNode, UIScrollViewDel messages[replyMessageId] = Message(stableId: 3, stableVersion: 0, id: replyMessageId, globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: 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, threadId: 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, messages: [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)) + items.append(self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: [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, backgroundNode: self.chatBackgroundNode)) let message2 = Message(stableId: 3, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 3), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: 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, threadMessageId: nil)], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: []) - items.append(self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: [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)) + items.append(self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: [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, backgroundNode: self.chatBackgroundNode)) 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: [], videoThumbnails: [], 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, threadId: 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, messages: [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)) + items.append(self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: [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, backgroundNode: self.chatBackgroundNode)) let message4 = Message(stableId: 2, stableVersion: 0, id: MessageId(peerId: otherPeerId, namespace: 0, id: 2), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: 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, messages: [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)) + items.append(self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: [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, backgroundNode: self.chatBackgroundNode)) let width: CGFloat if case .regular = layout.metrics.widthClass { @@ -249,7 +247,7 @@ private final class BubbleSettingsControllerNode: ASDisplayNode, UIScrollViewDel dateHeaderNode = currentDateHeaderNode headerItem.updateNode(dateHeaderNode, previous: nil, next: headerItem) } else { - dateHeaderNode = headerItem.node() + dateHeaderNode = headerItem.node(synchronousLoad: true) dateHeaderNode.subnodeTransform = CATransform3DMakeScale(-1.0, 1.0, 1.0) self.messagesContainerNode.addSubnode(dateHeaderNode) self.dateHeaderNode = dateHeaderNode @@ -394,7 +392,7 @@ final class BubbleSettingsController: 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.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition) } } diff --git a/submodules/SettingsUI/Sources/ChangePhoneNumberCodeController.swift b/submodules/SettingsUI/Sources/ChangePhoneNumberCodeController.swift index 3f37c4c2a7..0dd32295e6 100644 --- a/submodules/SettingsUI/Sources/ChangePhoneNumberCodeController.swift +++ b/submodules/SettingsUI/Sources/ChangePhoneNumberCodeController.swift @@ -230,7 +230,7 @@ func changePhoneNumberCodeController(context: AccountContext, phoneNumber: Strin |> take(1) |> mapToSignal { _ -> Signal in return Signal { subscriber in - return requestNextChangeAccountPhoneNumberVerification(account: context.account, phoneNumber: phoneNumber, phoneCodeHash: data.hash).start(next: { next in + return context.engine.accountData.requestNextChangeAccountPhoneNumberVerification(phoneNumber: phoneNumber, phoneCodeHash: data.hash).start(next: { next in currentDataPromise?.set(.single(next)) }, error: { error in @@ -254,7 +254,7 @@ func changePhoneNumberCodeController(context: AccountContext, phoneNumber: Strin } } if let code = code { - changePhoneDisposable.set((requestChangeAccountPhoneNumber(account: context.account, phoneNumber: phoneNumber, phoneCodeHash: codeData.hash, phoneCode: code) |> deliverOnMainQueue).start(error: { error in + changePhoneDisposable.set((context.engine.accountData.requestChangeAccountPhoneNumber(phoneNumber: phoneNumber, phoneCodeHash: codeData.hash, phoneCode: code) |> deliverOnMainQueue).start(error: { error in updateState { return $0.withUpdatedChecking(false) } @@ -277,6 +277,9 @@ func changePhoneNumberCodeController(context: AccountContext, phoneNumber: Strin } let presentationData = context.sharedContext.currentPresentationData.with { $0 } presentControllerImpl?(OverlayStatusController(theme: presentationData.theme, type: .success), nil) + + let _ = dismissServerProvidedSuggestion(account: context.account, suggestion: .validatePhoneNumber).start() + dismissImpl?() })) } diff --git a/submodules/SettingsUI/Sources/ChangePhoneNumberController.swift b/submodules/SettingsUI/Sources/ChangePhoneNumberController.swift index f7b4f026b9..05aac53984 100644 --- a/submodules/SettingsUI/Sources/ChangePhoneNumberController.swift +++ b/submodules/SettingsUI/Sources/ChangePhoneNumberController.swift @@ -12,8 +12,10 @@ import AlertUI import PresentationDataUtils import CountrySelectionUI import PhoneNumberFormat +import CoreTelephony +import MessageUI -final class ChangePhoneNumberController: ViewController { +final class ChangePhoneNumberController: ViewController, MFMailComposeViewControllerDelegate { private var controllerNode: ChangePhoneNumberControllerNode { return self.displayNode as! ChangePhoneNumberControllerNode } @@ -88,7 +90,7 @@ final class ChangePhoneNumberController: ViewController { } } - loadServerCountryCodes(accountManager: self.context.sharedContext.accountManager, network: self.context.account.network, completion: { [weak self] in + loadServerCountryCodes(accountManager: self.context.sharedContext.accountManager, engine: self.context.engine, completion: { [weak self] in if let strongSelf = self { strongSelf.controllerNode.updateCountryCode() } @@ -121,7 +123,7 @@ final class ChangePhoneNumberController: ViewController { } if !number.isEmpty { self.inProgress = true - self.requestDisposable.set((requestChangeAccountPhoneNumberVerification(account: self.context.account, phoneNumber: self.controllerNode.currentNumber) |> deliverOnMainQueue).start(next: { [weak self] next in + self.requestDisposable.set((self.context.engine.accountData.requestChangeAccountPhoneNumberVerification(phoneNumber: self.controllerNode.currentNumber) |> deliverOnMainQueue).start(next: { [weak self] next in if let strongSelf = self { strongSelf.inProgress = false (strongSelf.navigationController as? NavigationController)?.pushViewController(changePhoneNumberCodeController(context: strongSelf.context, phoneNumber: strongSelf.controllerNode.currentNumber, codeData: next)) @@ -133,18 +135,39 @@ final class ChangePhoneNumberController: ViewController { let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } let text: String + var actions: [TextAlertAction] = [] switch error { case .limitExceeded: text = presentationData.strings.Login_CodeFloodError + actions.append(TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})) case .invalidPhoneNumber: text = presentationData.strings.Login_InvalidPhoneError + actions.append(TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})) case .phoneNumberOccupied: text = presentationData.strings.ChangePhone_ErrorOccupied(formatPhoneNumber(phoneNumber)).0 + actions.append(TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})) + case .phoneBanned: + text = presentationData.strings.Login_PhoneBannedError + actions.append(TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_OK, action: {})) + actions.append(TextAlertAction(type: .genericAction, title: presentationData.strings.Login_PhoneNumberHelp, action: { [weak self] in + guard let strongSelf = self else { + return + } + let formattedNumber = formatPhoneNumber(number) + let appVersion = (Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String) ?? "unknown" + let systemVersion = UIDevice.current.systemVersion + let locale = Locale.current.identifier + let carrier = CTCarrier() + let mnc = carrier.mobileNetworkCode ?? "none" + + strongSelf.presentEmailComposeController(address: "login@stel.com", subject: presentationData.strings.Login_PhoneBannedEmailSubject(formattedNumber).0, body: presentationData.strings.Login_PhoneBannedEmailBody(formattedNumber, appVersion, systemVersion, locale, mnc).0) + })) case .generic: text = presentationData.strings.Login_UnknownError + actions.append(TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})) } - strongSelf.present(textAlertController(context: strongSelf.context, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + strongSelf.present(textAlertController(context: strongSelf.context, title: nil, text: text, actions: actions), in: .window(.root)) } })) } else { @@ -152,4 +175,22 @@ final class ChangePhoneNumberController: ViewController { self.controllerNode.animateError() } } + + private func presentEmailComposeController(address: String, subject: String, body: String) { + if MFMailComposeViewController.canSendMail() { + let composeController = MFMailComposeViewController() + composeController.setToRecipients([address]) + composeController.setSubject(subject) + composeController.setMessageBody(body, isHTML: false) + composeController.mailComposeDelegate = self + + self.view.window?.rootViewController?.present(composeController, animated: true, completion: nil) + } else { + self.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)) + } + } + + public func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith result: MFMailComposeResult, error: Error?) { + controller.dismiss(animated: true, completion: nil) + } } diff --git a/submodules/SettingsUI/Sources/ChangePhoneNumberIntroController.swift b/submodules/SettingsUI/Sources/ChangePhoneNumberIntroController.swift index b01109917d..06b2801b5a 100644 --- a/submodules/SettingsUI/Sources/ChangePhoneNumberIntroController.swift +++ b/submodules/SettingsUI/Sources/ChangePhoneNumberIntroController.swift @@ -11,6 +11,7 @@ import AlertUI import PresentationDataUtils import AppBundle import Markdown +import PhoneNumberFormat private final class ChangePhoneNumberIntroControllerNode: ASDisplayNode { var presentationData: PresentationData @@ -101,7 +102,8 @@ public final class ChangePhoneNumberIntroController: ViewController { self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style - self.title = phoneNumber + let formattedPhone = formatPhoneNumber(phoneNumber) + self.title = formattedPhone self.navigationItem.backBarButtonItem = UIBarButtonItem(title: presentationData.strings.Common_Back, style: .plain, target: nil, action: nil) //self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: "Cancel", style: .plain, target: self, action: #selector(self.cancelPressed)) } @@ -133,7 +135,7 @@ public final class ChangePhoneNumberIntroController: ViewController { override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { super.containerLayoutUpdated(layout, transition: transition) - (self.displayNode as! ChangePhoneNumberIntroControllerNode).containerLayoutUpdated(layout, navigationBarHeight: self.navigationHeight, transition: transition) + (self.displayNode as! ChangePhoneNumberIntroControllerNode).containerLayoutUpdated(layout, navigationBarHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition) } @objc func cancelPressed() { @@ -141,7 +143,7 @@ public final class ChangePhoneNumberIntroController: ViewController { } func proceed() { - self.present(textAlertController(context: self.context, title: nil, text: self.presentationData.strings.PhoneNumberHelp_Alert, actions: [TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .genericAction, title: self.presentationData.strings.Common_OK, action: { [weak self] in + self.present(textAlertController(context: self.context, title: nil, text: self.presentationData.strings.PhoneNumberHelp_Alert, actions: [TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .genericAction, title: self.presentationData.strings.TwoFactorSetup_Email_Action, action: { [weak self] in if let strongSelf = self { (strongSelf.navigationController as? NavigationController)?.replaceTopController(ChangePhoneNumberController(context: strongSelf.context), animated: true) } diff --git a/submodules/SettingsUI/Sources/Data and Storage/AutodownloadMediaCategoryController.swift b/submodules/SettingsUI/Sources/Data and Storage/AutodownloadMediaCategoryController.swift index 222efd9093..c0054bdbfc 100644 --- a/submodules/SettingsUI/Sources/Data and Storage/AutodownloadMediaCategoryController.swift +++ b/submodules/SettingsUI/Sources/Data and Storage/AutodownloadMediaCategoryController.swift @@ -71,7 +71,7 @@ private enum AutodownloadMediaCategoryEntry: ItemListNodeEntry { case peerChannels(PresentationTheme, String, Bool) case sizeHeader(PresentationTheme, String) - case sizeItem(PresentationTheme, String, String, Int32) + case sizeItem(PresentationTheme, PresentationStrings, String, String, Int32) case sizePreload(PresentationTheme, String, Bool, Bool) case sizePreloadInfo(PresentationTheme, String) @@ -145,8 +145,8 @@ private enum AutodownloadMediaCategoryEntry: ItemListNodeEntry { } else { return false } - case let .sizeItem(lhsTheme, lhsDecimalSeparator, lhsText, lhsValue): - if case let .sizeItem(rhsTheme, rhsDecimalSeparator, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsDecimalSeparator == rhsDecimalSeparator, lhsText == rhsText, lhsValue == rhsValue { + case let .sizeItem(lhsTheme, lhsStrings, lhsDecimalSeparator, lhsText, lhsValue): + if case let .sizeItem(rhsTheme, rhsStrings, rhsDecimalSeparator, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsDecimalSeparator == rhsDecimalSeparator, lhsText == rhsText, lhsValue == rhsValue { return true } else { return false @@ -173,35 +173,35 @@ private enum AutodownloadMediaCategoryEntry: ItemListNodeEntry { func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { let arguments = arguments as! AutodownloadMediaCategoryControllerArguments switch self { - case let .peerHeader(theme, text): + case let .peerHeader(_, text): return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) - case let .peerContacts(theme, text, value): + case let .peerContacts(_, text, value): 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): + case let .peerOtherPrivate(_, text, value): 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): + case let .peerGroups(_, text, value): 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): + case let .peerChannels(_, text, value): 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): + case let .sizeHeader(_, text): 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 + case let .sizeItem(theme, strings, decimalSeparator, text, value): + return AutodownloadSizeLimitItem(theme: theme, strings: strings, decimalSeparator: decimalSeparator, text: text, value: value, sectionId: self.section, updated: { value in arguments.adjustSize(value) }) - case let .sizePreload(theme, text, value, enabled): + case let .sizePreload(_, text, value, enabled): 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): + case let .sizePreloadInfo(_, text): return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) } } @@ -277,7 +277,7 @@ private func autodownloadMediaCategoryControllerEntries(presentationData: Presen sizeText = autodownloadDataSizeString(Int64(size), decimalSeparator: presentationData.dateTimeFormat.decimalSeparator) } let text = presentationData.strings.AutoDownloadSettings_UpTo(sizeText).0 - entries.append(.sizeItem(presentationData.theme, presentationData.dateTimeFormat.decimalSeparator, text, size)) + entries.append(.sizeItem(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat.decimalSeparator, text, size)) if #available(iOSApplicationExtension 10.3, *), category == .video { entries.append(.sizePreload(presentationData.theme, presentationData.strings.AutoDownloadSettings_PreloadVideo, predownload, size > 2 * 1024 * 1024)) entries.append(.sizePreloadInfo(presentationData.theme, presentationData.strings.AutoDownloadSettings_PreloadVideoInfo(sizeText).0)) diff --git a/submodules/SettingsUI/Sources/Data and Storage/AutodownloadSizeLimitItem.swift b/submodules/SettingsUI/Sources/Data and Storage/AutodownloadSizeLimitItem.swift index af3fe51a26..fdd1b17419 100644 --- a/submodules/SettingsUI/Sources/Data and Storage/AutodownloadSizeLimitItem.swift +++ b/submodules/SettingsUI/Sources/Data and Storage/AutodownloadSizeLimitItem.swift @@ -51,14 +51,16 @@ private func sizeValue(for sliderValue: CGFloat) -> Int32 { final class AutodownloadSizeLimitItem: ListViewItem, ItemListItem { let theme: PresentationTheme + let strings: PresentationStrings let decimalSeparator: String let text: String let value: Int32 let sectionId: ItemListSectionId let updated: (Int32) -> Void - init(theme: PresentationTheme, decimalSeparator: String, text: String, value: Int32, sectionId: ItemListSectionId, updated: @escaping (Int32) -> Void) { + init(theme: PresentationTheme, strings: PresentationStrings, decimalSeparator: String, text: String, value: Int32, sectionId: ItemListSectionId, updated: @escaping (Int32) -> Void) { self.theme = theme + self.strings = strings self.decimalSeparator = decimalSeparator self.text = text self.value = value @@ -201,9 +203,11 @@ private final class AutodownloadSizeLimitItemNode: ListViewItemNode { let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.text, font: Font.regular(17.0), textColor: item.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .center, lineSpacing: 0.0, cutout: nil, insets: UIEdgeInsets())) - let (minTextLayout, minTextApply) = makeMinTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: dataSizeString(512 * 1024, decimalSeparator: item.decimalSeparator), font: Font.regular(13.0), textColor: item.theme.list.itemSecondaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .center, lineSpacing: 0.0, cutout: nil, insets: UIEdgeInsets())) + let formatting = DataSizeStringFormatting(strings: item.strings, decimalSeparator: item.decimalSeparator) - let (maxTextLayout, maxTextApply) = makeMaxTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: dataSizeString(1536 * 1024 * 1024, decimalSeparator: item.decimalSeparator), font: Font.regular(13.0), textColor: item.theme.list.itemSecondaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .center, lineSpacing: 0.0, cutout: nil, insets: UIEdgeInsets())) + let (minTextLayout, minTextApply) = makeMinTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: dataSizeString(512 * 1024, formatting: formatting), font: Font.regular(13.0), textColor: item.theme.list.itemSecondaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .center, lineSpacing: 0.0, cutout: nil, insets: UIEdgeInsets())) + + let (maxTextLayout, maxTextApply) = makeMaxTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: dataSizeString(1536 * 1024 * 1024, formatting: formatting), font: Font.regular(13.0), textColor: item.theme.list.itemSecondaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .center, lineSpacing: 0.0, cutout: nil, insets: UIEdgeInsets())) contentSize = CGSize(width: params.width, height: 88.0) insets = itemListNeighborsGroupedInsets(neighbors) diff --git a/submodules/SettingsUI/Sources/Data and Storage/MaximumCacheSizePickerItem.swift b/submodules/SettingsUI/Sources/Data and Storage/MaximumCacheSizePickerItem.swift index bb511ddb3a..7b26b8b274 100644 --- a/submodules/SettingsUI/Sources/Data and Storage/MaximumCacheSizePickerItem.swift +++ b/submodules/SettingsUI/Sources/Data and Storage/MaximumCacheSizePickerItem.swift @@ -24,7 +24,7 @@ private func stringForCacheSize(strings: PresentationStrings, size: Int32) -> St if size > 100 { return strings.Cache_NoLimit } else { - return dataSizeString(Int64(size) * 1024 * 1024 * 1024) + return dataSizeString(Int64(size) * 1024 * 1024 * 1024, formatting: DataSizeStringFormatting(strings: strings, decimalSeparator: ".")) } } diff --git a/submodules/SettingsUI/Sources/Data and Storage/NetworkUsageStatsController.swift b/submodules/SettingsUI/Sources/Data and Storage/NetworkUsageStatsController.swift index 87744afa82..df38d5e13d 100644 --- a/submodules/SettingsUI/Sources/Data and Storage/NetworkUsageStatsController.swift +++ b/submodules/SettingsUI/Sources/Data and Storage/NetworkUsageStatsController.swift @@ -258,47 +258,47 @@ private enum NetworkUsageStatsEntry: ItemListNodeEntry { func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { let arguments = arguments as! NetworkUsageStatsControllerArguments switch self { - case let .messagesHeader(theme, text): + case let .messagesHeader(_, text): return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) - case let .messagesSent(theme, text, value): + case let .messagesSent(_, text, value): return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .none , action: nil) - case let .messagesReceived(theme, text, value): + case let .messagesReceived(_, text, value): return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .none , action: nil) - case let .imageHeader(theme, text): + case let .imageHeader(_, text): return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) - case let .imageSent(theme, text, value): + case let .imageSent(_, text, value): return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .none , action: nil) - case let .imageReceived(theme, text, value): + case let .imageReceived(_, text, value): return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .none , action: nil) - case let .videoHeader(theme, text): + case let .videoHeader(_, text): return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) - case let .videoSent(theme, text, value): + case let .videoSent(_, text, value): return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .none , action: nil) - case let .videoReceived(theme, text, value): + case let .videoReceived(_, text, value): return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .none , action: nil) - case let .audioHeader(theme, text): + case let .audioHeader(_, text): return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) - case let .audioSent(theme, text, value): + case let .audioSent(_, text, value): return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .none , action: nil) - case let .audioReceived(theme, text, value): + case let .audioReceived(_, text, value): return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .none , action: nil) - case let .fileHeader(theme, text): + case let .fileHeader(_, text): return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) - case let .fileSent(theme, text, value): + case let .fileSent(_, text, value): return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .none , action: nil) - case let .fileReceived(theme, text, value): + case let .fileReceived(_, text, value): return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .none , action: nil) - case let .callHeader(theme, text): + case let .callHeader(_, text): return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) - case let .callSent(theme, text, value): + case let .callSent(_, text, value): return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .none , action: nil) - case let .callReceived(theme, text, value): + case let .callReceived(_, text, value): return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .none , action: nil) - case let .reset(theme, section, text): + case let .reset(_, section, text): return ItemListActionItem(presentationData: presentationData, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: { arguments.resetStatistics(section) }) - case let .resetTimestamp(theme, text): + case let .resetTimestamp(_, text): return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) } } @@ -307,31 +307,32 @@ private enum NetworkUsageStatsEntry: ItemListNodeEntry { private func networkUsageStatsControllerEntries(presentationData: PresentationData, section: NetworkUsageControllerSection, stats: NetworkUsageStats) -> [NetworkUsageStatsEntry] { var entries: [NetworkUsageStatsEntry] = [] + let formatting = DataSizeStringFormatting(presentationData: presentationData) switch section { case .cellular: entries.append(.messagesHeader(presentationData.theme, presentationData.strings.NetworkUsageSettings_GeneralDataSection)) - entries.append(.messagesSent(presentationData.theme, presentationData.strings.NetworkUsageSettings_BytesSent, dataSizeString(stats.generic.cellular.outgoing, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator))) - entries.append(.messagesReceived(presentationData.theme, presentationData.strings.NetworkUsageSettings_BytesReceived, dataSizeString(stats.generic.cellular.incoming, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator))) + entries.append(.messagesSent(presentationData.theme, presentationData.strings.NetworkUsageSettings_BytesSent, dataSizeString(stats.generic.cellular.outgoing, formatting: formatting))) + entries.append(.messagesReceived(presentationData.theme, presentationData.strings.NetworkUsageSettings_BytesReceived, dataSizeString(stats.generic.cellular.incoming, formatting: formatting))) entries.append(.imageHeader(presentationData.theme, presentationData.strings.NetworkUsageSettings_MediaImageDataSection)) - entries.append(.imageSent(presentationData.theme, presentationData.strings.NetworkUsageSettings_BytesSent, dataSizeString(stats.image.cellular.outgoing, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator))) - entries.append(.imageReceived(presentationData.theme, presentationData.strings.NetworkUsageSettings_BytesReceived, dataSizeString(stats.image.cellular.incoming, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator))) + entries.append(.imageSent(presentationData.theme, presentationData.strings.NetworkUsageSettings_BytesSent, dataSizeString(stats.image.cellular.outgoing, formatting: formatting))) + entries.append(.imageReceived(presentationData.theme, presentationData.strings.NetworkUsageSettings_BytesReceived, dataSizeString(stats.image.cellular.incoming, formatting: formatting))) entries.append(.videoHeader(presentationData.theme, presentationData.strings.NetworkUsageSettings_MediaVideoDataSection)) - entries.append(.videoSent(presentationData.theme, presentationData.strings.NetworkUsageSettings_BytesSent, dataSizeString(stats.video.cellular.outgoing, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator))) - entries.append(.videoReceived(presentationData.theme, presentationData.strings.NetworkUsageSettings_BytesReceived, dataSizeString(stats.video.cellular.incoming, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator))) + entries.append(.videoSent(presentationData.theme, presentationData.strings.NetworkUsageSettings_BytesSent, dataSizeString(stats.video.cellular.outgoing, formatting: formatting))) + entries.append(.videoReceived(presentationData.theme, presentationData.strings.NetworkUsageSettings_BytesReceived, dataSizeString(stats.video.cellular.incoming, formatting: formatting))) entries.append(.audioHeader(presentationData.theme, presentationData.strings.NetworkUsageSettings_MediaAudioDataSection)) - entries.append(.audioSent(presentationData.theme, presentationData.strings.NetworkUsageSettings_BytesSent, dataSizeString(stats.audio.cellular.outgoing, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator))) - entries.append(.audioReceived(presentationData.theme, presentationData.strings.NetworkUsageSettings_BytesReceived, dataSizeString(stats.audio.cellular.incoming, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator))) + entries.append(.audioSent(presentationData.theme, presentationData.strings.NetworkUsageSettings_BytesSent, dataSizeString(stats.audio.cellular.outgoing, formatting: formatting))) + entries.append(.audioReceived(presentationData.theme, presentationData.strings.NetworkUsageSettings_BytesReceived, dataSizeString(stats.audio.cellular.incoming, formatting: formatting))) entries.append(.fileHeader(presentationData.theme, presentationData.strings.NetworkUsageSettings_MediaDocumentDataSection)) - entries.append(.fileSent(presentationData.theme, presentationData.strings.NetworkUsageSettings_BytesSent, dataSizeString(stats.file.cellular.outgoing, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator))) - entries.append(.fileReceived(presentationData.theme, presentationData.strings.NetworkUsageSettings_BytesReceived, dataSizeString(stats.file.cellular.incoming, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator))) + entries.append(.fileSent(presentationData.theme, presentationData.strings.NetworkUsageSettings_BytesSent, dataSizeString(stats.file.cellular.outgoing, formatting: formatting))) + entries.append(.fileReceived(presentationData.theme, presentationData.strings.NetworkUsageSettings_BytesReceived, dataSizeString(stats.file.cellular.incoming, formatting: formatting))) entries.append(.callHeader(presentationData.theme, presentationData.strings.NetworkUsageSettings_CallDataSection)) - entries.append(.callSent(presentationData.theme, presentationData.strings.NetworkUsageSettings_BytesSent, dataSizeString(stats.call.cellular.outgoing, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator))) - entries.append(.callReceived(presentationData.theme, presentationData.strings.NetworkUsageSettings_BytesReceived, dataSizeString(stats.call.cellular.incoming, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator))) + entries.append(.callSent(presentationData.theme, presentationData.strings.NetworkUsageSettings_BytesSent, dataSizeString(stats.call.cellular.outgoing, formatting: formatting))) + entries.append(.callReceived(presentationData.theme, presentationData.strings.NetworkUsageSettings_BytesReceived, dataSizeString(stats.call.cellular.incoming, formatting: formatting))) entries.append(.reset(presentationData.theme, section, presentationData.strings.NetworkUsageSettings_ResetStats)) @@ -344,28 +345,28 @@ private func networkUsageStatsControllerEntries(presentationData: PresentationDa } case .wifi: entries.append(.messagesHeader(presentationData.theme, presentationData.strings.NetworkUsageSettings_GeneralDataSection)) - entries.append(.messagesSent(presentationData.theme, presentationData.strings.NetworkUsageSettings_BytesSent, dataSizeString(stats.generic.wifi.outgoing, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator))) - entries.append(.messagesReceived(presentationData.theme, presentationData.strings.NetworkUsageSettings_BytesReceived, dataSizeString(stats.generic.wifi.incoming, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator))) + entries.append(.messagesSent(presentationData.theme, presentationData.strings.NetworkUsageSettings_BytesSent, dataSizeString(stats.generic.wifi.outgoing, formatting: formatting))) + entries.append(.messagesReceived(presentationData.theme, presentationData.strings.NetworkUsageSettings_BytesReceived, dataSizeString(stats.generic.wifi.incoming, formatting: formatting))) entries.append(.imageHeader(presentationData.theme, presentationData.strings.NetworkUsageSettings_MediaImageDataSection)) - entries.append(.imageSent(presentationData.theme, presentationData.strings.NetworkUsageSettings_BytesSent, dataSizeString(stats.image.wifi.outgoing, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator))) - entries.append(.imageReceived(presentationData.theme, presentationData.strings.NetworkUsageSettings_BytesReceived, dataSizeString(stats.image.wifi.incoming, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator))) + entries.append(.imageSent(presentationData.theme, presentationData.strings.NetworkUsageSettings_BytesSent, dataSizeString(stats.image.wifi.outgoing, formatting: formatting))) + entries.append(.imageReceived(presentationData.theme, presentationData.strings.NetworkUsageSettings_BytesReceived, dataSizeString(stats.image.wifi.incoming, formatting: formatting))) entries.append(.videoHeader(presentationData.theme, presentationData.strings.NetworkUsageSettings_MediaVideoDataSection)) - entries.append(.videoSent(presentationData.theme, presentationData.strings.NetworkUsageSettings_BytesSent, dataSizeString(stats.video.wifi.outgoing, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator))) - entries.append(.videoReceived(presentationData.theme, presentationData.strings.NetworkUsageSettings_BytesReceived, dataSizeString(stats.video.wifi.incoming, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator))) + entries.append(.videoSent(presentationData.theme, presentationData.strings.NetworkUsageSettings_BytesSent, dataSizeString(stats.video.wifi.outgoing, formatting: formatting))) + entries.append(.videoReceived(presentationData.theme, presentationData.strings.NetworkUsageSettings_BytesReceived, dataSizeString(stats.video.wifi.incoming, formatting: formatting))) entries.append(.audioHeader(presentationData.theme, presentationData.strings.NetworkUsageSettings_MediaAudioDataSection)) - entries.append(.audioSent(presentationData.theme, presentationData.strings.NetworkUsageSettings_BytesSent, dataSizeString(stats.audio.wifi.outgoing, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator))) - entries.append(.audioReceived(presentationData.theme, presentationData.strings.NetworkUsageSettings_BytesReceived, dataSizeString(stats.audio.wifi.incoming, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator))) + entries.append(.audioSent(presentationData.theme, presentationData.strings.NetworkUsageSettings_BytesSent, dataSizeString(stats.audio.wifi.outgoing, formatting: formatting))) + entries.append(.audioReceived(presentationData.theme, presentationData.strings.NetworkUsageSettings_BytesReceived, dataSizeString(stats.audio.wifi.incoming, formatting: formatting))) entries.append(.fileHeader(presentationData.theme, presentationData.strings.NetworkUsageSettings_MediaDocumentDataSection)) - entries.append(.fileSent(presentationData.theme, presentationData.strings.NetworkUsageSettings_BytesSent, dataSizeString(stats.file.wifi.outgoing, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator))) - entries.append(.fileReceived(presentationData.theme, presentationData.strings.NetworkUsageSettings_BytesReceived, dataSizeString(stats.file.wifi.incoming, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator))) + entries.append(.fileSent(presentationData.theme, presentationData.strings.NetworkUsageSettings_BytesSent, dataSizeString(stats.file.wifi.outgoing, formatting: formatting))) + entries.append(.fileReceived(presentationData.theme, presentationData.strings.NetworkUsageSettings_BytesReceived, dataSizeString(stats.file.wifi.incoming, formatting: formatting))) entries.append(.callHeader(presentationData.theme, presentationData.strings.NetworkUsageSettings_CallDataSection)) - entries.append(.callSent(presentationData.theme, presentationData.strings.NetworkUsageSettings_BytesSent, dataSizeString(stats.call.wifi.outgoing, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator))) - entries.append(.callReceived(presentationData.theme, presentationData.strings.NetworkUsageSettings_BytesReceived, dataSizeString(stats.call.wifi.incoming, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator))) + entries.append(.callSent(presentationData.theme, presentationData.strings.NetworkUsageSettings_BytesSent, dataSizeString(stats.call.wifi.outgoing, formatting: formatting))) + entries.append(.callReceived(presentationData.theme, presentationData.strings.NetworkUsageSettings_BytesReceived, dataSizeString(stats.call.wifi.incoming, formatting: formatting))) entries.append(.reset(presentationData.theme, section, presentationData.strings.NetworkUsageSettings_ResetStats)) if stats.resetWifiTimestamp != 0 { diff --git a/submodules/SettingsUI/Sources/Data and Storage/ProxyServerActionSheetController.swift b/submodules/SettingsUI/Sources/Data and Storage/ProxyServerActionSheetController.swift index 610a18a8e9..48b0ac5ebf 100644 --- a/submodules/SettingsUI/Sources/Data and Storage/ProxyServerActionSheetController.swift +++ b/submodules/SettingsUI/Sources/Data and Storage/ProxyServerActionSheetController.swift @@ -12,6 +12,7 @@ import ActivityIndicator import OverlayStatusController import AccountContext import PresentationDataUtils +import UrlEscaping public final class ProxyServerActionSheetController: ActionSheetController { private var presentationDisposable: Disposable? @@ -131,7 +132,7 @@ private final class ProxyServerInfoItemNode: ActionSheetItemNode { let serverTextNode = ImmediateTextNode() serverTextNode.isUserInteractionEnabled = false serverTextNode.displaysAsynchronously = false - serverTextNode.attributedText = NSAttributedString(string: server.host, font: textFont, textColor: theme.primaryTextColor) + serverTextNode.attributedText = NSAttributedString(string: urlEncodedStringFromString(server.host), font: textFont, textColor: theme.primaryTextColor) fieldNodes.append((serverTitleNode, serverTextNode)) let portTitleNode = ImmediateTextNode() diff --git a/submodules/SettingsUI/Sources/Data and Storage/ProxySettingsServerItem.swift b/submodules/SettingsUI/Sources/Data and Storage/ProxySettingsServerItem.swift index 5d7e756311..3e68942278 100644 --- a/submodules/SettingsUI/Sources/Data and Storage/ProxySettingsServerItem.swift +++ b/submodules/SettingsUI/Sources/Data and Storage/ProxySettingsServerItem.swift @@ -10,6 +10,7 @@ import TelegramPresentationData import ItemListUI import PresentationDataUtils import ActivityIndicator +import UrlEscaping private let activitySize = CGSize(width: 24.0, height: 24.0) @@ -236,7 +237,7 @@ private final class ProxySettingsServerItemNode: ItemListRevealOptionsItemNode { } let titleAttributedString = NSMutableAttributedString() - titleAttributedString.append(NSAttributedString(string: item.server.host, font: titleFont, textColor: item.theme.list.itemPrimaryTextColor)) + titleAttributedString.append(NSAttributedString(string: urlEncodedStringFromString(item.server.host), font: titleFont, textColor: item.theme.list.itemPrimaryTextColor)) 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) diff --git a/submodules/SettingsUI/Sources/Data and Storage/StorageUsageController.swift b/submodules/SettingsUI/Sources/Data and Storage/StorageUsageController.swift index 9b9882a2b2..2b63878ba5 100644 --- a/submodules/SettingsUI/Sources/Data and Storage/StorageUsageController.swift +++ b/submodules/SettingsUI/Sources/Data and Storage/StorageUsageController.swift @@ -71,7 +71,7 @@ private enum StorageUsageEntry: ItemListNodeEntry { case maximumSizeInfo(PresentationTheme, String) case storageHeader(PresentationTheme, String) - case storageUsage(PresentationTheme, PresentationDateTimeFormat, [StorageUsageCategory]) + case storageUsage(PresentationTheme, PresentationStrings, PresentationDateTimeFormat, [StorageUsageCategory]) case collecting(PresentationTheme, String) case clearAll(PresentationTheme, String, Bool) @@ -164,8 +164,8 @@ private enum StorageUsageEntry: ItemListNodeEntry { } else { return false } - case let .storageUsage(lhsTheme, lhsDateTimeFormat, lhsCategories): - if case let .storageUsage(rhsTheme, rhsDateTimeFormat, rhsCategories) = rhs, lhsTheme === rhsTheme, lhsDateTimeFormat == rhsDateTimeFormat, lhsCategories == rhsCategories { + case let .storageUsage(lhsTheme, lhsStrings, lhsDateTimeFormat, lhsCategories): + if case let .storageUsage(rhsTheme, rhsStrings, rhsDateTimeFormat, rhsCategories) = rhs, lhsTheme === rhsTheme, lhsStrings == rhsStrings, lhsDateTimeFormat == rhsDateTimeFormat, lhsCategories == rhsCategories { return true } else { return false @@ -249,8 +249,8 @@ private enum StorageUsageEntry: ItemListNodeEntry { return ItemListTextItem(presentationData: presentationData, text: .markdown(text), sectionId: self.section) case let .storageHeader(_, text): 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 .storageUsage(theme, strings, dateTimeFormat, categories): + return StorageUsageItem(theme: theme, strings: strings, 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(_, text, enabled): @@ -262,7 +262,7 @@ private enum StorageUsageEntry: ItemListNodeEntry { case let .peersHeader(_, text): return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) case let .peer(_, _, strings, dateTimeFormat, nameDisplayOrder, peer, chatPeer, value, revealed): - var options: [ItemListPeerItemRevealOption] = [ItemListPeerItemRevealOption(type: .destructive, title: strings.ClearCache_Clear, action: { + let options: [ItemListPeerItemRevealOption] = [ItemListPeerItemRevealOption(type: .destructive, title: strings.ClearCache_Clear, action: { arguments.clearPeerMedia(peer.id) })] 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: { @@ -340,7 +340,7 @@ private func storageUsageControllerEntries(presentationData: PresentationData, c categories.append(StorageUsageCategory(title: presentationData.strings.ClearCache_StorageOtherApps, size: otherAppsSpace, fraction: CGFloat(otherAppsSpace) / totalSpaceValue, color: presentationData.theme.list.itemBarChart.color2)) categories.append(StorageUsageCategory(title: presentationData.strings.ClearCache_StorageFree, size: freeSpace, fraction: CGFloat(freeSpace) / totalSpaceValue, color: presentationData.theme.list.itemBarChart.color3)) - entries.append(.storageUsage(presentationData.theme, presentationData.dateTimeFormat, categories)) + entries.append(.storageUsage(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, categories)) entries.append(.clearAll(presentationData.theme, presentationData.strings.ClearCache_ClearCache, telegramCacheSize > 0)) @@ -358,7 +358,7 @@ private func storageUsageControllerEntries(presentationData: PresentationData, c chatPeer = mainPeer mainPeer = associatedPeer } - entries.append(.peer(index, presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, presentationData.nameDisplayOrder, mainPeer, chatPeer, dataSizeString(size, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator), state.peerIdWithRevealedOptions == peer.id)) + entries.append(.peer(index, presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, presentationData.nameDisplayOrder, mainPeer, chatPeer, dataSizeString(size, formatting: DataSizeStringFormatting(presentationData: presentationData)), state.peerIdWithRevealedOptions == peer.id)) index += 1 } } @@ -394,7 +394,7 @@ func cacheUsageStats(context: AccountContext) -> Signal then(collectCacheUsageStats(account: context.account, additionalCachePaths: additionalPaths, logFilesPath: context.sharedContext.applicationBindings.containerPath + "/telegram-data/logs") + |> then(context.engine.resources.collectCacheUsageStats(additionalCachePaths: additionalPaths, logFilesPath: context.sharedContext.applicationBindings.containerPath + "/telegram-data/logs") |> map(Optional.init)) } @@ -490,7 +490,7 @@ public func storageUsageController(context: AccountContext, cacheUsagePromise: P if filteredSize == 0 { title = presentationData.strings.Cache_ClearNone } else { - title = presentationData.strings.Cache_Clear("\(dataSizeString(filteredSize, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator))").0 + title = presentationData.strings.Cache_Clear("\(dataSizeString(filteredSize, formatting: DataSizeStringFormatting(presentationData: presentationData)))").0 } if let item = item as? ActionSheetButtonItem { @@ -527,7 +527,7 @@ public func storageUsageController(context: AccountContext, cacheUsagePromise: P let categorySize: Int64 = size totalSize += categorySize let index = itemIndex - items.append(ActionSheetCheckboxItem(title: stringForCategory(strings: presentationData.strings, category: categoryId), label: dataSizeString(categorySize, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator), value: true, action: { value in + items.append(ActionSheetCheckboxItem(title: stringForCategory(strings: presentationData.strings, category: categoryId), label: dataSizeString(categorySize, formatting: DataSizeStringFormatting(presentationData: presentationData)), value: true, action: { value in toggleCheck(categoryId, index) })) itemIndex += 1 @@ -537,7 +537,7 @@ public func storageUsageController(context: AccountContext, cacheUsagePromise: P if otherSize.1 != 0 { totalSize += otherSize.1 let index = itemIndex - items.append(ActionSheetCheckboxItem(title: presentationData.strings.Localization_LanguageOther, label: dataSizeString(otherSize.1, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator), value: true, action: { value in + items.append(ActionSheetCheckboxItem(title: presentationData.strings.Localization_LanguageOther, label: dataSizeString(otherSize.1, formatting: DataSizeStringFormatting(presentationData: presentationData)), value: true, action: { value in toggleCheck(nil, index) })) itemIndex += 1 @@ -545,7 +545,7 @@ public func storageUsageController(context: AccountContext, cacheUsagePromise: P selectedSize = totalSize if !items.isEmpty { - items.append(ActionSheetButtonItem(title: presentationData.strings.Cache_Clear("\(dataSizeString(totalSize, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator))").0, action: { + items.append(ActionSheetButtonItem(title: presentationData.strings.Cache_Clear("\(dataSizeString(totalSize, formatting: DataSizeStringFormatting(presentationData: presentationData)))").0, action: { if let statsPromise = statsPromise { let clearCategories = sizeIndex.keys.filter({ sizeIndex[$0]!.0 }) @@ -581,7 +581,7 @@ public func storageUsageController(context: AccountContext, cacheUsagePromise: P var updatedTempPaths = stats.tempPaths var updatedTempSize = stats.tempSize - var signal: Signal = clearCachedMediaResources(account: context.account, mediaResourceIds: clearResourceIds) + var signal: Signal = context.engine.resources.clearCachedMediaResources(mediaResourceIds: clearResourceIds) if otherSize.0 { let removeTempFiles: Signal = Signal { subscriber in let fileManager = FileManager.default @@ -637,7 +637,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 return false }), .current, nil) + presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .succeed(text: presentationData.strings.ClearCache_Success("\(dataSizeString(selectedSize, formatting: DataSizeStringFormatting(presentationData: presentationData)))", stringForDeviceType()).0), elevatedLayout: false, action: { _ in return false }), .current, nil) })) } @@ -692,7 +692,7 @@ public func storageUsageController(context: AccountContext, cacheUsagePromise: P if filteredSize == 0 { title = presentationData.strings.Cache_ClearNone } else { - title = presentationData.strings.Cache_Clear("\(dataSizeString(filteredSize, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator))").0 + title = presentationData.strings.Cache_Clear("\(dataSizeString(filteredSize, formatting: DataSizeStringFormatting(presentationData: presentationData)))").0 } if let item = item as? ActionSheetButtonItem { @@ -732,7 +732,7 @@ public func storageUsageController(context: AccountContext, cacheUsagePromise: P totalSize += categorySize if categorySize > 1024 { let index = itemIndex - items.append(ActionSheetCheckboxItem(title: stringForCategory(strings: presentationData.strings, category: categoryId), label: dataSizeString(categorySize, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator), value: true, action: { value in + items.append(ActionSheetCheckboxItem(title: stringForCategory(strings: presentationData.strings, category: categoryId), label: dataSizeString(categorySize, formatting: DataSizeStringFormatting(presentationData: presentationData)), value: true, action: { value in toggleCheck(categoryId, index) })) itemIndex += 1 @@ -742,7 +742,7 @@ public func storageUsageController(context: AccountContext, cacheUsagePromise: P selectedSize = totalSize if !items.isEmpty { - items.append(ActionSheetButtonItem(title: presentationData.strings.Cache_Clear("\(dataSizeString(totalSize, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator))").0, action: { + items.append(ActionSheetButtonItem(title: presentationData.strings.Cache_Clear("\(dataSizeString(totalSize, formatting: DataSizeStringFormatting(presentationData: presentationData)))").0, action: { if let statsPromise = statsPromise { let clearCategories = sizeIndex.keys.filter({ sizeIndex[$0]!.0 }) var clearMediaIds = Set() @@ -784,7 +784,7 @@ public func storageUsageController(context: AccountContext, cacheUsagePromise: P } } - var signal = clearCachedMediaResources(account: context.account, mediaResourceIds: clearResourceIds) + var signal = context.engine.resources.clearCachedMediaResources(mediaResourceIds: clearResourceIds) let resultStats = CacheUsageStats(media: media, mediaResourceIds: stats.mediaResourceIds, peers: stats.peers, otherSize: stats.otherSize, otherPaths: stats.otherPaths, cacheSize: stats.cacheSize, tempPaths: stats.tempPaths, tempSize: stats.tempSize, immutableSize: stats.immutableSize) @@ -818,7 +818,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 return false }), .current, nil) + presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .succeed(text: presentationData.strings.ClearCache_Success("\(dataSizeString(selectedSize, formatting: DataSizeStringFormatting(presentationData: presentationData)))", stringForDeviceType()).0), elevatedLayout: false, action: { _ in return false }), .current, nil) })) } @@ -911,7 +911,7 @@ public func storageUsageController(context: AccountContext, cacheUsagePromise: P } } - var signal = clearCachedMediaResources(account: context.account, mediaResourceIds: clearResourceIds) + var signal = context.engine.resources.clearCachedMediaResources(mediaResourceIds: clearResourceIds) let resultStats = CacheUsageStats(media: media, mediaResourceIds: stats.mediaResourceIds, peers: stats.peers, otherSize: stats.otherSize, otherPaths: stats.otherPaths, cacheSize: stats.cacheSize, tempPaths: stats.tempPaths, tempSize: stats.tempSize, immutableSize: stats.immutableSize) @@ -945,7 +945,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 return false }), .current, nil) + presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .succeed(text: presentationData.strings.ClearCache_Success("\(dataSizeString(totalSize, formatting: DataSizeStringFormatting(presentationData: presentationData)))", stringForDeviceType()).0), elevatedLayout: false, action: { _ in return false }), .current, nil) })) } } diff --git a/submodules/SettingsUI/Sources/Data and Storage/StorageUsageItem.swift b/submodules/SettingsUI/Sources/Data and Storage/StorageUsageItem.swift index 03ca00282f..3c4d8770d6 100644 --- a/submodules/SettingsUI/Sources/Data and Storage/StorageUsageItem.swift +++ b/submodules/SettingsUI/Sources/Data and Storage/StorageUsageItem.swift @@ -19,12 +19,14 @@ struct StorageUsageCategory: Equatable { final class StorageUsageItem: ListViewItem, ItemListItem { let theme: PresentationTheme + let strings: PresentationStrings let dateTimeFormat: PresentationDateTimeFormat let categories: [StorageUsageCategory] let sectionId: ItemListSectionId - init(theme: PresentationTheme, dateTimeFormat: PresentationDateTimeFormat, categories: [StorageUsageCategory], sectionId: ItemListSectionId) { + init(theme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, categories: [StorageUsageCategory], sectionId: ItemListSectionId) { self.theme = theme + self.strings = strings self.dateTimeFormat = dateTimeFormat self.categories = categories self.sectionId = sectionId @@ -187,7 +189,7 @@ private final class StorageUsageItemNode: ListViewItemNode { let category = item.categories[i] let attributedString = NSMutableAttributedString(string: category.title, font: Font.regular(14.0), textColor: item.theme.list.itemPrimaryTextColor, paragraphAlignment: .natural) - attributedString.append(NSAttributedString(string: " • \(dataSizeString(category.size, forceDecimal: true, decimalSeparator: item.dateTimeFormat.decimalSeparator))", font: Font.bold(14.0), textColor: item.theme.list.itemPrimaryTextColor)) + attributedString.append(NSAttributedString(string: " • \(dataSizeString(category.size, forceDecimal: true, formatting: DataSizeStringFormatting(strings: item.strings, decimalSeparator: item.dateTimeFormat.decimalSeparator)))", font: Font.bold(14.0), textColor: item.theme.list.itemPrimaryTextColor)) let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: attributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - params.leftInset - params.rightInset - 60.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) diff --git a/submodules/SettingsUI/Sources/Language Selection/LocalizationListController.swift b/submodules/SettingsUI/Sources/Language Selection/LocalizationListController.swift index dfaca96c32..f96efea62d 100644 --- a/submodules/SettingsUI/Sources/Language Selection/LocalizationListController.swift +++ b/submodules/SettingsUI/Sources/Language Selection/LocalizationListController.swift @@ -145,7 +145,7 @@ public class LocalizationListController: ViewController { override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { super.containerLayoutUpdated(layout, transition: transition) - self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationInsetHeight, transition: transition) + self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.cleanNavigationHeight, transition: transition) } @objc private func editPressed() { diff --git a/submodules/SettingsUI/Sources/Language Selection/LocalizationListControllerNode.swift b/submodules/SettingsUI/Sources/Language Selection/LocalizationListControllerNode.swift index aa17f128bf..0328661f80 100644 --- a/submodules/SettingsUI/Sources/Language Selection/LocalizationListControllerNode.swift +++ b/submodules/SettingsUI/Sources/Language Selection/LocalizationListControllerNode.swift @@ -14,7 +14,6 @@ import AccountContext import ShareController import SearchBarNode import SearchUI -import ActivityIndicator import UndoUI private enum LanguageListSection: ItemListSectionId { @@ -33,12 +32,12 @@ private enum LanguageListEntryType { } private enum LanguageListEntry: Comparable, Identifiable { - case localization(index: Int, info: LocalizationInfo, type: LanguageListEntryType, selected: Bool, activity: Bool, revealed: Bool, editing: Bool) + case localization(index: Int, info: LocalizationInfo?, type: LanguageListEntryType, selected: Bool, activity: Bool, revealed: Bool, editing: Bool) var stableId: LanguageListEntryId { switch self { - case let .localization(_, info, _, _, _, _, _): - return .localization(info.languageCode) + case let .localization(index, info, _, _, _, _, _): + return .localization(info?.languageCode ?? "\(index)") } } @@ -56,8 +55,10 @@ private enum LanguageListEntry: Comparable, Identifiable { 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(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) + return LocalizationListItem(presentationData: ItemListPresentationData(presentationData), id: info?.languageCode ?? "", title: info?.title ?? " ", subtitle: info?.localizedTitle ?? " ", checked: selected, activity: activity, loading: info == nil, editing: LocalizationListItemEditing(editable: !selected && !searchMode && !(info?.isOfficial ?? true), editing: editing, revealed: !selected && revealed, reorderable: false), sectionId: type == .official ? LanguageListSection.official.rawValue : LanguageListSection.unofficial.rawValue, alwaysPlain: searchMode, action: { + if let info = info { + selectLocalization(info) + } }, setItemWithRevealedOptions: setItemWithRevealedOptions, removeItem: removeItem) } } @@ -173,7 +174,7 @@ private final class LocalizationListSearchContainerNode: SearchDisplayController } }) - self.listNode.beganInteractiveDragging = { [weak self] in + self.listNode.beganInteractiveDragging = { [weak self] _ in self?.dismissInput?() } } @@ -259,16 +260,17 @@ private struct LanguageListNodeTransition { let firstTime: Bool let isLoading: Bool let animated: Bool + let crossfade: Bool } -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 { +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, crossfade: 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(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) + return LanguageListNodeTransition(deletions: deletions, insertions: insertions, updates: updates, firstTime: firstTime, isLoading: isLoading, animated: animated, crossfade: crossfade) } final class LocalizationListControllerNode: ViewControllerTracingNode { @@ -285,7 +287,6 @@ final class LocalizationListControllerNode: ViewControllerTracingNode { private var containerLayout: (ContainerViewLayout, CGFloat)? let listNode: ListView private var queuedTransitions: [LanguageListNodeTransition] = [] - private var activityIndicator: ActivityIndicator? private var searchDisplayController: SearchDisplayController? private let presentationDataValue = Promise() @@ -365,19 +366,22 @@ final class LocalizationListControllerNode: ViewControllerTracingNode { } let preferencesKey: PostboxViewKey = .preferences(keys: Set([PreferencesKeys.localizationListState])) + let previousState = Atomic(value: nil) let previousEntriesHolder = Atomic<([LanguageListEntry], PresentationTheme, PresentationStrings)?>(value: nil) self.listDisposable = combineLatest(queue: .mainQueue(), context.account.postbox.combinedView(keys: [preferencesKey]), context.sharedContext.accountManager.sharedData(keys: [SharedDataKeys.localizationSettings]), self.presentationDataValue.get(), self.applyingCode.get(), revealedCode.get(), self.isEditing.get()).start(next: { [weak self] view, sharedData, presentationData, applyingCode, revealedCode, isEditing in guard let strongSelf = self else { return } - + var entries: [LanguageListEntry] = [] var activeLanguageCode: String? if let localizationSettings = sharedData.entries[SharedDataKeys.localizationSettings] as? LocalizationSettings { activeLanguageCode = localizationSettings.primaryComponent.languageCode } var existingIds = Set() - if let localizationListState = (view.views[preferencesKey] as? PreferencesView)?.values[PreferencesKeys.localizationListState] as? LocalizationListState, !localizationListState.availableOfficialLocalizations.isEmpty { + + let localizationListState = (view.views[preferencesKey] as? PreferencesView)?.values[PreferencesKeys.localizationListState] as? LocalizationListState + if let localizationListState = localizationListState, !localizationListState.availableOfficialLocalizations.isEmpty { strongSelf.currentListState = localizationListState let availableSavedLocalizations = localizationListState.availableSavedLocalizations.filter({ info in !localizationListState.availableOfficialLocalizations.contains(where: { $0.languageCode == info.languageCode }) }) @@ -402,12 +406,19 @@ final class LocalizationListControllerNode: ViewControllerTracingNode { existingIds.insert(info.languageCode) entries.append(.localization(index: entries.count, info: info, type: .official, selected: info.languageCode == activeLanguageCode, activity: applyingCode == info.languageCode, revealed: revealedCode == info.languageCode, editing: false)) } + } else { + for _ in 0 ..< 15 { + entries.append(.localization(index: entries.count, info: nil, type: .official, selected: false, activity: false, revealed: false, editing: false)) + } } + + let previousState = previousState.swap(localizationListState) + 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) + 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, crossfade: (previousState == nil) != (localizationListState == nil)) strongSelf.enqueueTransition(transition) }) - self.updatedDisposable = synchronizedLocalizationListState(postbox: context.account.postbox, network: context.account.network).start() + self.updatedDisposable = context.engine.localization.synchronizedLocalizationListState().start() } deinit { @@ -444,11 +455,6 @@ final class LocalizationListControllerNode: ViewControllerTracingNode { self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: nil, updateSizeAndInsets: updateSizeAndInsets, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) - if let activityIndicator = self.activityIndicator { - let indicatorSize = activityIndicator.measure(CGSize(width: 100.0, height: 100.0)) - transition.updateFrame(node: activityIndicator, frame: CGRect(origin: CGPoint(x: floor((layout.size.width - indicatorSize.width) / 2.0), y: updateSizeAndInsets.insets.top + 50.0 + floor((layout.size.height - updateSizeAndInsets.insets.top - updateSizeAndInsets.insets.bottom - indicatorSize.height - 50.0) / 2.0)), size: indicatorSize)) - } - if !hadValidLayout { self.dequeueTransitions() } @@ -463,7 +469,7 @@ final class LocalizationListControllerNode: ViewControllerTracingNode { } private func dequeueTransitions() { - guard let (layout, navigationBarHeight) = self.containerLayout else { + guard let _ = self.containerLayout else { return } while !self.queuedTransitions.isEmpty { @@ -473,6 +479,8 @@ final class LocalizationListControllerNode: ViewControllerTracingNode { if transition.firstTime { options.insert(.Synchronous) options.insert(.LowLatency) + } else if transition.crossfade { + options.insert(.AnimateCrossfade) } else if transition.animated { options.insert(.AnimateInsertion) } @@ -482,17 +490,6 @@ final class LocalizationListControllerNode: ViewControllerTracingNode { strongSelf.didSetReady = true strongSelf._ready.set(true) } - - if transition.isLoading, strongSelf.activityIndicator == nil { - let activityIndicator = ActivityIndicator(type: .custom(strongSelf.presentationData.theme.list.itemAccentColor, 22.0, 1.0, false)) - strongSelf.activityIndicator = activityIndicator - strongSelf.insertSubnode(activityIndicator, aboveSubnode: strongSelf.listNode) - - strongSelf.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate) - } else if !transition.isLoading, let activityIndicator = strongSelf.activityIndicator { - strongSelf.activityIndicator = nil - activityIndicator.removeFromSupernode() - } } }) } @@ -504,7 +501,7 @@ final class LocalizationListControllerNode: ViewControllerTracingNode { return } strongSelf.applyingCode.set(.single(info.languageCode)) - strongSelf.applyDisposable.set((downloadAndApplyLocalization(accountManager: strongSelf.context.sharedContext.accountManager, postbox: strongSelf.context.account.postbox, network: strongSelf.context.account.network, languageCode: info.languageCode) + strongSelf.applyDisposable.set((strongSelf.context.engine.localization.downloadAndApplyLocalization(accountManager: strongSelf.context.sharedContext.accountManager, languageCode: info.languageCode) |> deliverOnMainQueue).start(completed: { self?.applyingCode.set(.single(nil)) })) diff --git a/submodules/SettingsUI/Sources/Language Selection/LocalizationListItem.swift b/submodules/SettingsUI/Sources/Language Selection/LocalizationListItem.swift index c00aef3a25..ba852bc7ef 100644 --- a/submodules/SettingsUI/Sources/Language Selection/LocalizationListItem.swift +++ b/submodules/SettingsUI/Sources/Language Selection/LocalizationListItem.swift @@ -8,6 +8,7 @@ import ItemListUI import PresentationDataUtils import ActivityIndicator import ChatListSearchItemNode +import ShimmerEffect struct LocalizationListItemEditing: Equatable { let editable: Bool @@ -23,6 +24,7 @@ class LocalizationListItem: ListViewItem, ItemListItem { let subtitle: String let checked: Bool let activity: Bool + let loading: Bool let editing: LocalizationListItemEditing let sectionId: ItemListSectionId let alwaysPlain: Bool @@ -30,13 +32,14 @@ class LocalizationListItem: ListViewItem, ItemListItem { let setItemWithRevealedOptions: (String?, String?) -> Void let removeItem: (String) -> Void - 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) { + init(presentationData: ItemListPresentationData, id: String, title: String, subtitle: String, checked: Bool, activity: Bool, loading: 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 self.checked = checked self.activity = activity + self.loading = loading self.editing = editing self.sectionId = sectionId self.alwaysPlain = alwaysPlain @@ -108,6 +111,9 @@ class LocalizationListItemNode: ItemListRevealOptionsItemNode { private var item: LocalizationListItem? private var layoutParams: (ListViewItemLayoutParams, ItemListNeighbors)? + private var placeholderNode: ShimmerEffectNode? + private var absoluteLocation: (CGRect, CGSize)? + private var editableControlNode: ItemListEditableControlNode? private var reorderControlNode: ItemListEditableReorderControlNode? @@ -117,7 +123,7 @@ class LocalizationListItemNode: ItemListRevealOptionsItemNode { if self.editableControlNode != nil { return false } - if let _ = self.layoutParams?.0 { + if let _ = self.layoutParams?.0, let item = self.item, !item.loading { return super.canBeSelected } else { return false @@ -167,6 +173,15 @@ class LocalizationListItemNode: ItemListRevealOptionsItemNode { self.addSubnode(self.activateArea) } + 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.placeholderNode { + shimmerNode.updateAbsoluteRect(rect, within: containerSize) + } + } + func asyncLayout() -> (_ item: LocalizationListItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, (Bool) -> Void) { let makeTitleLayout = TextNode.asyncLayout(self.titleNode) let makeSubtitleLayout = TextNode.asyncLayout(self.subtitleNode) @@ -322,6 +337,42 @@ class LocalizationListItemNode: ItemListRevealOptionsItemNode { strongSelf.setRevealOptions((left: [], right: [])) } strongSelf.setRevealOptionsOpened(item.editing.revealed, animated: animated) + + if item.loading { + let shimmerNode: ShimmerEffectNode + if let current = strongSelf.placeholderNode { + shimmerNode = current + } else { + shimmerNode = ShimmerEffectNode() + strongSelf.placeholderNode = shimmerNode + if strongSelf.bottomStripeNode.supernode != nil { + strongSelf.insertSubnode(shimmerNode, belowSubnode: strongSelf.bottomStripeNode) + } else { + strongSelf.addSubnode(shimmerNode) + } + } + shimmerNode.frame = CGRect(origin: CGPoint(), size: layout.contentSize) + if let (rect, size) = strongSelf.absoluteLocation { + shimmerNode.updateAbsoluteRect(rect, within: size) + } + + var shapes: [ShimmerEffectNode.Shape] = [] + + let titleLineWidth: CGFloat = 80.0 + let subtitleLineWidth: CGFloat = 50.0 + let lineDiameter: CGFloat = 10.0 + + let titleFrame = strongSelf.titleNode.frame + shapes.append(.roundedRectLine(startPoint: CGPoint(x: titleFrame.minX, y: titleFrame.minY + floor((titleFrame.height - lineDiameter) / 2.0)), width: titleLineWidth, diameter: lineDiameter)) + + let subtitleFrame = strongSelf.subtitleNode.frame + shapes.append(.roundedRectLine(startPoint: CGPoint(x: subtitleFrame.minX, y: subtitleFrame.minY + floor((subtitleFrame.height - lineDiameter) / 2.0)), width: subtitleLineWidth, diameter: 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.placeholderNode { + strongSelf.placeholderNode = nil + shimmerNode.removeFromSupernode() + } } }) } diff --git a/submodules/SettingsUI/Sources/LogoutOptionsController.swift b/submodules/SettingsUI/Sources/LogoutOptionsController.swift index a9443cd2a2..7a68f36c76 100644 --- a/submodules/SettingsUI/Sources/LogoutOptionsController.swift +++ b/submodules/SettingsUI/Sources/LogoutOptionsController.swift @@ -157,7 +157,7 @@ public func logoutOptionsController(context: AccountContext, navigationControlle dismissImpl?() }, contactSupport: { [weak navigationController] in let supportPeer = Promise() - supportPeer.set(supportPeerId(account: context.account)) + supportPeer.set(context.engine.peers.supportPeerId()) let presentationData = context.sharedContext.currentPresentationData.with { $0 } var faqUrl = presentationData.strings.Settings_FAQ_URL diff --git a/submodules/SettingsUI/Sources/Notifications/Exceptions/NotificationExceptionControllerNode.swift b/submodules/SettingsUI/Sources/Notifications/Exceptions/NotificationExceptionControllerNode.swift index 66a5dfe0ae..7689a657cd 100644 --- a/submodules/SettingsUI/Sources/Notifications/Exceptions/NotificationExceptionControllerNode.swift +++ b/submodules/SettingsUI/Sources/Notifications/Exceptions/NotificationExceptionControllerNode.swift @@ -780,16 +780,16 @@ final class NotificationExceptionsControllerNode: ViewControllerTracingNode { let presentationData = context.sharedContext.currentPresentationData.modify {$0} let updatePeerSound: (PeerId, PeerMessageSound) -> Signal = { peerId, sound in - return updatePeerNotificationSoundInteractive(account: context.account, peerId: peerId, sound: sound) |> deliverOnMainQueue + return context.engine.peers.updatePeerNotificationSoundInteractive(peerId: peerId, sound: sound) |> deliverOnMainQueue } - let updatePeerNotificationInterval:(PeerId, Int32?) -> Signal = { peerId, muteInterval in - return updatePeerMuteSetting(account: context.account, peerId: peerId, muteInterval: muteInterval) |> deliverOnMainQueue + let updatePeerNotificationInterval: (PeerId, Int32?) -> Signal = { peerId, muteInterval in + return context.engine.peers.updatePeerMuteSetting(peerId: peerId, muteInterval: muteInterval) |> deliverOnMainQueue } let updatePeerDisplayPreviews:(PeerId, PeerNotificationDisplayPreviews) -> Signal = { peerId, displayPreviews in - return updatePeerDisplayPreviewsSetting(account: context.account, peerId: peerId, displayPreviews: displayPreviews) |> deliverOnMainQueue + return context.engine.peers.updatePeerDisplayPreviewsSetting(peerId: peerId, displayPreviews: displayPreviews) |> deliverOnMainQueue } self.backgroundColor = presentationData.theme.list.blocksBackgroundColor @@ -842,13 +842,11 @@ final class NotificationExceptionsControllerNode: ViewControllerTracingNode { updateNotificationsView({}) }) }, removePeerFromExceptions: { - let _ = (context.account.postbox.transaction { transaction -> Peer? in - updatePeerMuteSetting(transaction: transaction, peerId: peerId, muteInterval: nil) - updatePeerDisplayPreviewsSetting(transaction: transaction, peerId: peerId, displayPreviews: .default) - updatePeerNotificationSoundInteractive(transaction: transaction, peerId: peerId, sound: .default) + let _ = (context.engine.peers.removeCustomNotificationSettings(peerIds: [peerId]) + |> map { _ -> Peer? in } + |> then(context.account.postbox.transaction { transaction -> Peer? in return transaction.getPeer(peerId) - } - |> deliverOnMainQueue).start(next: { peer in + })).start(next: { peer in guard let peer = peer else { return } @@ -917,11 +915,7 @@ final class NotificationExceptionsControllerNode: ViewControllerTracingNode { updateState { value in return value.withUpdatedPeerMuteInterval(peer, nil).withUpdatedPeerSound(peer, .default).withUpdatedPeerDisplayPreviews(peer, .default) } - _ = (context.account.postbox.transaction { transaction in - updatePeerNotificationSoundInteractive(transaction: transaction, peerId: peer.id, sound: .default) - updatePeerMuteSetting(transaction: transaction, peerId: peer.id, muteInterval: nil) - updatePeerDisplayPreviewsSetting(transaction: transaction, peerId: peer.id, displayPreviews: .default) - } + let _ = (context.engine.peers.removeCustomNotificationSettings(peerIds: [peer.id]) |> deliverOnMainQueue).start(completed: { updateNotificationsView({}) }) @@ -953,13 +947,7 @@ final class NotificationExceptionsControllerNode: ViewControllerTracingNode { } return state } - let _ = (context.account.postbox.transaction { transaction -> Void in - for value in values { - updatePeerNotificationSoundInteractive(transaction: transaction, peerId: value.peer.id, sound: .default) - updatePeerMuteSetting(transaction: transaction, peerId: value.peer.id, muteInterval: nil) - updatePeerDisplayPreviewsSetting(transaction: transaction, peerId: value.peer.id, displayPreviews: .default) - } - } + let _ = (context.engine.peers.removeCustomNotificationSettings(peerIds: values.map(\.peer.id)) |> deliverOnMainQueue).start(completed: { updateNotificationsView({}) }) @@ -1279,7 +1267,7 @@ private final class NotificationExceptionsSearchContainerNode: SearchDisplayCont } }) - self.listNode.beganInteractiveDragging = { [weak self] in + self.listNode.beganInteractiveDragging = { [weak self] _ in self?.dismissInput?() } } diff --git a/submodules/SettingsUI/Sources/Notifications/Exceptions/NotificationExceptions.swift b/submodules/SettingsUI/Sources/Notifications/Exceptions/NotificationExceptions.swift index e56e371194..566da4ec52 100644 --- a/submodules/SettingsUI/Sources/Notifications/Exceptions/NotificationExceptions.swift +++ b/submodules/SettingsUI/Sources/Notifications/Exceptions/NotificationExceptions.swift @@ -157,7 +157,7 @@ public class NotificationExceptionsController: ViewController { override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { super.containerLayoutUpdated(layout, transition: transition) - self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationInsetHeight, actualNavigationBarHeight: self.navigationHeight, transition: transition) + self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.cleanNavigationHeight, actualNavigationBarHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition) } @objc private func removeAllPressed() { diff --git a/submodules/SettingsUI/Sources/Notifications/NotificationsAndSounds.swift b/submodules/SettingsUI/Sources/Notifications/NotificationsAndSounds.swift index b34263b70d..545d26e11e 100644 --- a/submodules/SettingsUI/Sources/Notifications/NotificationsAndSounds.swift +++ b/submodules/SettingsUI/Sources/Notifications/NotificationsAndSounds.swift @@ -1008,7 +1008,7 @@ public func notificationsAndSoundsController(context: AccountContext, exceptions let sharedData = context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.inAppNotificationSettings]) let preferences = context.account.postbox.preferencesView(keys: [PreferencesKeys.globalNotifications]) - let exceptionsSignal = Signal.single(exceptionsList) |> then(notificationExceptionsList(postbox: context.account.postbox, network: context.account.network) |> map(Optional.init)) + let exceptionsSignal = Signal.single(exceptionsList) |> then(context.engine.peers.notificationExceptionsList() |> map(Optional.init)) notificationExceptions.set(exceptionsSignal |> map { list -> (NotificationExceptionMode, NotificationExceptionMode, NotificationExceptionMode) in var users:[PeerId : NotificationExceptionWrapper] = [:] diff --git a/submodules/SettingsUI/Sources/Privacy and Security/ConfirmPhoneNumberController.swift b/submodules/SettingsUI/Sources/Privacy and Security/ConfirmPhoneNumberController.swift index ac27bd6795..e0b7c6dee5 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/ConfirmPhoneNumberController.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/ConfirmPhoneNumberController.swift @@ -216,7 +216,7 @@ public func confirmPhoneNumberCodeController(context: AccountContext, phoneNumbe |> take(1) |> mapToSignal { _ -> Signal in return Signal { subscriber in - return requestNextCancelAccountResetOption(network: context.account.network, phoneNumber: phoneNumber, phoneCodeHash: data.hash).start(next: { next in + return context.engine.auth.requestNextCancelAccountResetOption(phoneNumber: phoneNumber, phoneCodeHash: data.hash).start(next: { next in currentDataPromise?.set(.single(next)) }, error: { error in @@ -242,7 +242,7 @@ public func confirmPhoneNumberCodeController(context: AccountContext, phoneNumbe } } if let code = code { - confirmPhoneDisposable.set((requestCancelAccountReset(network: context.account.network, phoneCodeHash: codeData.hash, phoneCode: code) + confirmPhoneDisposable.set((context.engine.auth.requestCancelAccountReset(phoneCodeHash: codeData.hash, phoneCode: code) |> deliverOnMainQueue).start(error: { error in updateState { state in var state = state diff --git a/submodules/SettingsUI/Sources/Privacy and Security/CreatePasswordController.swift b/submodules/SettingsUI/Sources/Privacy and Security/CreatePasswordController.swift index 5be4d7c79c..f8d3dc0936 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/CreatePasswordController.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/CreatePasswordController.swift @@ -274,7 +274,7 @@ func createPasswordController(context: AccountContext, createPasswordContext: Cr state.saving = true return state } - saveDisposable.set((updateTwoStepVerificationPassword(network: context.account.network, currentPassword: currentPassword, updatedPassword: .password(password: state.passwordText, hint: state.hintText, email: email)) + saveDisposable.set((context.engine.auth.updateTwoStepVerificationPassword(currentPassword: currentPassword, updatedPassword: .password(password: state.passwordText, hint: state.hintText, email: email)) |> deliverOnMainQueue).start(next: { update in switch update { case .none: @@ -356,7 +356,7 @@ func createPasswordController(context: AccountContext, createPasswordContext: Cr return state } - saveDisposable.set((updateTwoStepVerificationPassword(network: context.account.network, currentPassword: currentPassword, updatedPassword: .none) + saveDisposable.set((context.engine.auth.updateTwoStepVerificationPassword(currentPassword: currentPassword, updatedPassword: .none) |> deliverOnMainQueue).start(next: { _ in updateState { state in var state = state diff --git a/submodules/SettingsUI/Sources/Privacy and Security/DataPrivacySettingsController.swift b/submodules/SettingsUI/Sources/Privacy and Security/DataPrivacySettingsController.swift index 2372c84b27..f8803407b2 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/DataPrivacySettingsController.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/DataPrivacySettingsController.swift @@ -9,11 +9,11 @@ import TelegramPresentationData import TelegramUIPreferences import ItemListUI import PresentationDataUtils -import OverlayStatusController import AccountContext import AlertUI import PresentationDataUtils import TelegramNotices +import UndoUI private final class DataPrivacyControllerArguments { let account: Account @@ -360,7 +360,7 @@ public func dataPrivacyController(context: AccountContext) -> ViewController { info.insert(.shippingInfo) } - clearPaymentInfoDisposable.set((clearBotPaymentInfo(network: context.account.network, info: info) + clearPaymentInfoDisposable.set((context.engine.payments.clearBotPaymentInfo(info: info) |> deliverOnMainQueue).start(completed: { updateState { state in var state = state @@ -368,7 +368,19 @@ public func dataPrivacyController(context: AccountContext) -> ViewController { return state } let presentationData = context.sharedContext.currentPresentationData.with { $0 } - presentControllerImpl?(OverlayStatusController(theme: presentationData.theme, type: .success)) + let text: String? + if info.contains([.paymentInfo, .shippingInfo]) { + text = presentationData.strings.Privacy_PaymentsClear_AllInfoCleared + } else if info.contains(.paymentInfo) { + text = presentationData.strings.Privacy_PaymentsClear_PaymentInfoCleared + } else if info.contains(.shippingInfo) { + text = presentationData.strings.Privacy_PaymentsClear_ShippingInfoCleared + } else { + text = nil + } + if let text = text { + presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .succeed(text: text), elevatedLayout: false, action: { _ in return false })) + } })) } dismissAction() @@ -414,7 +426,7 @@ public func dataPrivacyController(context: AccountContext) -> ViewController { }) }).start() - actionsDisposable.add((deleteAllContacts(account: context.account) + actionsDisposable.add((context.engine.contacts.deleteAllContacts() |> deliverOnMainQueue).start(completed: { updateState { state in var state = state @@ -422,7 +434,7 @@ public func dataPrivacyController(context: AccountContext) -> ViewController { return state } let presentationData = context.sharedContext.currentPresentationData.with { $0 } - presentControllerImpl?(OverlayStatusController(theme: presentationData.theme, type: .success)) + presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .succeed(text: presentationData.strings.Privacy_ContactsReset_ContactsDeleted), elevatedLayout: false, action: { _ in return false })) })) }), TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Cancel, action: {})])) } @@ -441,7 +453,7 @@ public func dataPrivacyController(context: AccountContext) -> ViewController { state.updatedSuggestFrequentContacts = value return state } - let _ = updateRecentPeersEnabled(postbox: context.account.postbox, network: context.account.network, enabled: value).start() + let _ = context.engine.peers.updateRecentPeersEnabled(enabled: value).start() } if !value { let presentationData = context.sharedContext.currentPresentationData.with { $0 } @@ -470,7 +482,7 @@ public func dataPrivacyController(context: AccountContext) -> ViewController { return state } if clear { - clearPaymentInfoDisposable.set((clearCloudDraftsInteractively(postbox: context.account.postbox, network: context.account.network, accountPeerId: context.account.peerId) + clearPaymentInfoDisposable.set((context.engine.messages.clearCloudDraftsInteractively() |> deliverOnMainQueue).start(completed: { updateState { state in var state = state @@ -478,7 +490,7 @@ public func dataPrivacyController(context: AccountContext) -> ViewController { return state } let presentationData = context.sharedContext.currentPresentationData.with { $0 } - presentControllerImpl?(OverlayStatusController(theme: presentationData.theme, type: .success)) + presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .succeed(text: presentationData.strings.Privacy_DeleteDrafts_DraftsDeleted), elevatedLayout: false, action: { _ in return false })) })) } dismissAction() @@ -491,9 +503,9 @@ public func dataPrivacyController(context: AccountContext) -> ViewController { let previousState = Atomic(value: nil) - actionsDisposable.add(managedUpdatedRecentPeers(accountPeerId: context.account.peerId, postbox: context.account.postbox, network: context.account.network).start()) + actionsDisposable.add(context.engine.peers.managedUpdatedRecentPeers().start()) - let signal = combineLatest(queue: .mainQueue(), context.sharedContext.presentationData, statePromise.get(), context.sharedContext.accountManager.noticeEntry(key: ApplicationSpecificNotice.secretChatLinkPreviewsKey()), context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.contactSynchronizationSettings]), context.account.postbox.preferencesView(keys: [PreferencesKeys.contactsSettings]), recentPeers(account: context.account)) + let signal = combineLatest(queue: .mainQueue(), context.sharedContext.presentationData, statePromise.get(), context.sharedContext.accountManager.noticeEntry(key: ApplicationSpecificNotice.secretChatLinkPreviewsKey()), context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.contactSynchronizationSettings]), context.account.postbox.preferencesView(keys: [PreferencesKeys.contactsSettings]), context.engine.peers.recentPeers()) |> map { presentationData, state, noticeView, sharedData, preferences, recentPeers -> (ItemListControllerState, (ItemListNodeState, Any)) in let secretChatLinkPreviews = noticeView.value.flatMap({ ApplicationSpecificNotice.getSecretChatLinkPreviews($0) }) @@ -530,7 +542,7 @@ public func dataPrivacyController(context: AccountContext) -> ViewController { let controller = ItemListController(context: context, state: signal) presentControllerImpl = { [weak controller] c in - controller?.present(c, in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + controller?.present(c, in: .window(.root)) } return controller diff --git a/submodules/SettingsUI/Sources/Privacy and Security/ForwardPrivacyChatPreviewItem.swift b/submodules/SettingsUI/Sources/Privacy and Security/ForwardPrivacyChatPreviewItem.swift index 3ac27c2c04..000b2d8d36 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/ForwardPrivacyChatPreviewItem.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/ForwardPrivacyChatPreviewItem.swift @@ -150,7 +150,7 @@ class ForwardPrivacyChatPreviewItemNode: ListViewItemNode { let insets: UIEdgeInsets let separatorHeight = UIScreenPixel - let peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: 1) + let peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt32Value(1)) var peers = SimpleDictionary() let messages = SimpleDictionary() @@ -159,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, psaType: nil, flags: []) - let messageItem = item.context.sharedContext.makeChatMessagePreviewItem(context: item.context, messages: [Message(stableId: 1, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: 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) + let messageItem = item.context.sharedContext.makeChatMessagePreviewItem(context: item.context, messages: [Message(stableId: 1, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: 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, backgroundNode: nil) var node: ListViewItemNode? if let current = currentNode { diff --git a/submodules/SettingsUI/Sources/Privacy and Security/PasscodeOptionsController.swift b/submodules/SettingsUI/Sources/Privacy and Security/PasscodeOptionsController.swift index 750686026c..7b4c6980d7 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/PasscodeOptionsController.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/PasscodeOptionsController.swift @@ -469,7 +469,7 @@ public func passcodeEntryController(context: AccountContext, animateIn: Bool = t biometrics = .none } #endif - let controller = PasscodeEntryController(applicationBindings: context.sharedContext.applicationBindings, accountManager: context.sharedContext.accountManager, appLockContext: context.sharedContext.appLockContext, presentationData: context.sharedContext.currentPresentationData.with { $0 }, presentationDataSignal: context.sharedContext.presentationData, challengeData: challenge, biometrics: biometrics, arguments: PasscodeEntryControllerPresentationArguments(animated: false, fadeIn: true, cancel: { + let controller = PasscodeEntryController(applicationBindings: context.sharedContext.applicationBindings, accountManager: context.sharedContext.accountManager, appLockContext: context.sharedContext.appLockContext, presentationData: context.sharedContext.currentPresentationData.with { $0 }, presentationDataSignal: context.sharedContext.presentationData, statusBarHost: context.sharedContext.mainWindow?.statusBarHost, challengeData: challenge, biometrics: biometrics, arguments: PasscodeEntryControllerPresentationArguments(animated: false, fadeIn: true, cancel: { completion(false) }, modalPresentation: modalPresentation)) controller.presentationCompleted = { [weak controller] in diff --git a/submodules/SettingsUI/Sources/Privacy and Security/PrivacyAndSecurityController.swift b/submodules/SettingsUI/Sources/Privacy and Security/PrivacyAndSecurityController.swift index 5e332cc700..ab2508baa9 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/PrivacyAndSecurityController.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/PrivacyAndSecurityController.swift @@ -506,11 +506,11 @@ public func privacyAndSecurityController(context: AccountContext, initialSetting actionsDisposable.add(updateAutoArchiveDisposable) let privacySettingsPromise = Promise() - privacySettingsPromise.set(.single(initialSettings) |> then(requestAccountPrivacySettings(account: context.account) |> map(Optional.init))) + privacySettingsPromise.set(.single(initialSettings) |> then(context.engine.privacy.requestAccountPrivacySettings() |> 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 activeSessionsContext = activeSessionsContext ?? context.engine.privacy.activeSessions() + let webSessionsContext = webSessionsContext ?? context.engine.privacy.webSessions() let blockedPeersState = Promise() blockedPeersState.set(blockedPeersContext.state) @@ -542,7 +542,7 @@ public func privacyAndSecurityController(context: AccountContext, initialSetting } let updateHasTwoStepAuth: () -> Void = { - let signal = twoStepVerificationConfiguration(account: context.account) + let signal = context.engine.auth.twoStepVerificationConfiguration() |> map { value -> TwoStepVerificationAccessConfiguration? in return TwoStepVerificationAccessConfiguration(configuration: value, password: nil) } @@ -727,15 +727,15 @@ public func privacyAndSecurityController(context: AccountContext, initialSetting } }) }, openTwoStepVerification: { data in - var intro = false + let intro = false if let data = data { switch data { case .set: break case let .notSet(pendingEmail): - //intro = pendingEmail == nil if pendingEmail == nil { - let controller = TwoFactorAuthSplashScreen(context: context, mode: .intro) + let controller = TwoFactorAuthSplashScreen(sharedContext: context.sharedContext, engine: .authorized(context.engine), mode: .intro) + pushControllerImpl?(controller, true) return } else { @@ -779,7 +779,7 @@ public func privacyAndSecurityController(context: AccountContext, initialSetting return .complete() } - updateAutoArchiveDisposable.set((updateAccountAutoArchiveChats(account: context.account, value: archiveValue) + updateAutoArchiveDisposable.set((context.engine.privacy.updateAccountAutoArchiveChats(value: archiveValue) |> mapToSignal { _ -> Signal in } |> then(applyTimeout) |> deliverOnMainQueue).start(completed: { @@ -817,7 +817,7 @@ public func privacyAndSecurityController(context: AccountContext, initialSetting } return .complete() } - updateAccountTimeoutDisposable.set((updateAccountRemovalTimeout(account: context.account, timeout: timeout) + updateAccountTimeoutDisposable.set((context.engine.privacy.updateAccountRemovalTimeout(timeout: timeout) |> then(applyTimeout) |> deliverOnMainQueue).start(completed: { updateState { state in @@ -851,7 +851,7 @@ public func privacyAndSecurityController(context: AccountContext, initialSetting pushControllerImpl?(dataPrivacyController(context: context), true) }) - actionsDisposable.add(managedUpdatedRecentPeers(accountPeerId: context.account.peerId, postbox: context.account.postbox, network: context.account.network).start()) + actionsDisposable.add(context.engine.peers.managedUpdatedRecentPeers().start()) actionsDisposable.add((privacySettingsPromise.get() |> deliverOnMainQueue).start(next: { settings in @@ -865,7 +865,7 @@ public func privacyAndSecurityController(context: AccountContext, initialSetting let preferencesKey: PostboxViewKey = .preferences(keys: Set([PreferencesKeys.appConfiguration])) - 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()), context.account.postbox.combinedView(keys: [preferencesKey])) + 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]), context.engine.peers.recentPeers(), blockedPeersState.get(), webSessionsContext.state, context.sharedContext.accountManager.accessChallengeData(), combineLatest(twoStepAuth.get(), twoStepAuthDataValue.get()), context.account.postbox.combinedView(keys: [preferencesKey])) |> map { presentationData, state, privacySettings, noticeView, sharedData, recentPeers, blockedPeersState, activeWebsitesState, accessChallengeData, twoStepAuth, preferences -> (ItemListControllerState, (ItemListNodeState, Any)) in var canAutoarchive = false if let view = preferences.views[preferencesKey] as? PreferencesView, let appConfiguration = view.values[PreferencesKeys.appConfiguration] as? AppConfiguration, let data = appConfiguration.data, let hasAutoarchive = data["autoarchive_setting_available"] as? Bool { diff --git a/submodules/SettingsUI/Sources/Privacy and Security/PrivacyIntroController.swift b/submodules/SettingsUI/Sources/Privacy and Security/PrivacyIntroController.swift index 4de91ff54d..76c2e339a9 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/PrivacyIntroController.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/PrivacyIntroController.swift @@ -178,6 +178,6 @@ final class PrivacyIntroController: 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.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition) } } diff --git a/submodules/SettingsUI/Sources/Privacy and Security/SelectivePrivacySettingsController.swift b/submodules/SettingsUI/Sources/Privacy and Security/SelectivePrivacySettingsController.swift index 1ba0704ebf..9d71c8b993 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/SelectivePrivacySettingsController.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/SelectivePrivacySettingsController.swift @@ -1038,14 +1038,14 @@ func selectivePrivacySettingsController(context: AccountContext, kind: Selective type = .phoneNumber } - let updateSettingsSignal = updateSelectiveAccountPrivacySettings(account: context.account, type: type, settings: settings) + let updateSettingsSignal = context.engine.privacy.updateSelectiveAccountPrivacySettings(type: type, settings: settings) var updateCallP2PSettingsSignal: Signal = Signal.complete() if let callP2PSettings = callP2PSettings { - updateCallP2PSettingsSignal = updateSelectiveAccountPrivacySettings(account: context.account, type: .voiceCallsP2P, settings: callP2PSettings) + updateCallP2PSettingsSignal = context.engine.privacy.updateSelectiveAccountPrivacySettings(type: .voiceCallsP2P, settings: callP2PSettings) } var updatePhoneDiscoverySignal: Signal = Signal.complete() if let phoneDiscoveryEnabled = phoneDiscoveryEnabled { - updatePhoneDiscoverySignal = updatePhoneNumberDiscovery(account: context.account, value: phoneDiscoveryEnabled) + updatePhoneDiscoverySignal = context.engine.privacy.updatePhoneNumberDiscovery(value: phoneDiscoveryEnabled) } let _ = (combineLatest(updateSettingsSignal, updateCallP2PSettingsSignal, updatePhoneDiscoverySignal) diff --git a/submodules/SettingsUI/Sources/Privacy and Security/TwoStepVerificationPasswordEntryController.swift b/submodules/SettingsUI/Sources/Privacy and Security/TwoStepVerificationPasswordEntryController.swift deleted file mode 100644 index 6a4ab372c1..0000000000 --- a/submodules/SettingsUI/Sources/Privacy and Security/TwoStepVerificationPasswordEntryController.swift +++ /dev/null @@ -1,456 +0,0 @@ -import Foundation -import UIKit -import Display -import SwiftSignalKit -import Postbox -import TelegramCore -import SyncCore -import TelegramPresentationData -import ItemListUI -import PresentationDataUtils -import AccountContext -import AlertUI -import PresentationDataUtils - -private final class TwoStepVerificationPasswordEntryControllerArguments { - let updateEntryText: (String) -> Void - let next: () -> Void - - init(updateEntryText: @escaping (String) -> Void, next: @escaping () -> Void) { - self.updateEntryText = updateEntryText - self.next = next - } -} - -private enum TwoStepVerificationPasswordEntrySection: Int32 { - case password -} - -private enum TwoStepVerificationPasswordEntryTag: ItemListItemTag { - case input - - func isEqual(to other: ItemListItemTag) -> Bool { - if let other = other as? TwoStepVerificationPasswordEntryTag { - switch self { - case .input: - if case .input = other { - return true - } else { - return false - } - } - } else { - return false - } - } -} - -private enum TwoStepVerificationPasswordEntryEntry: ItemListNodeEntry { - case passwordEntryTitle(PresentationTheme, String) - case passwordEntry(PresentationTheme, PresentationStrings, String) - - case hintTitle(PresentationTheme, String) - case hintEntry(PresentationTheme, PresentationStrings, String) - - case emailEntry(PresentationTheme, PresentationStrings, String) - case emailInfo(PresentationTheme, String) - - var section: ItemListSectionId { - return TwoStepVerificationPasswordEntrySection.password.rawValue - } - - var stableId: Int32 { - switch self { - case .passwordEntryTitle: - return 0 - case .passwordEntry: - return 1 - case .hintTitle: - return 2 - case .hintEntry: - return 3 - case .emailEntry: - return 5 - case .emailInfo: - return 6 - } - } - - static func ==(lhs: TwoStepVerificationPasswordEntryEntry, rhs: TwoStepVerificationPasswordEntryEntry) -> Bool { - switch lhs { - case let .passwordEntryTitle(lhsTheme, lhsText): - if case let .passwordEntryTitle(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { - return true - } else { - return false - } - case let .passwordEntry(lhsTheme, lhsStrings, lhsText): - if case let .passwordEntry(rhsTheme, rhsStrings, rhsText) = rhs, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsText == rhsText { - return true - } else { - return false - } - case let .hintTitle(lhsTheme, lhsText): - if case let .hintTitle(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { - return true - } else { - return false - } - case let .hintEntry(lhsTheme, lhsStrings, lhsText): - if case let .hintEntry(rhsTheme, rhsStrings, rhsText) = rhs, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsText == rhsText { - return true - } else { - return false - } - case let .emailEntry(lhsTheme, lhsStrings, lhsText): - if case let .emailEntry(rhsTheme, rhsStrings, rhsText) = rhs, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsText == rhsText { - return true - } else { - return false - } - case let .emailInfo(lhsTheme, lhsText): - if case let .emailInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { - return true - } else { - return false - } - } - } - - static func <(lhs: TwoStepVerificationPasswordEntryEntry, rhs: TwoStepVerificationPasswordEntryEntry) -> Bool { - return lhs.stableId < rhs.stableId - } - - func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { - let arguments = arguments as! TwoStepVerificationPasswordEntryControllerArguments - switch self { - case let .passwordEntryTitle(theme, text): - return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) - case let .passwordEntry(theme, strings, text): - 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(presentationData: presentationData, text: text, sectionId: self.section) - case let .hintEntry(theme, strings, text): - 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(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(presentationData: presentationData, text: .plain(text), sectionId: self.section) - } - } -} - -private enum PasswordEntryStage: Equatable { - case entry(text: String) - case reentry(first: String, text: String) - case hint(password: String, text: String) - case email(password: String, hint: String, text: String) - - func updateCurrentText(_ text: String) -> PasswordEntryStage { - switch self { - case .entry: - return .entry(text: text) - case let .reentry(first, _): - return .reentry(first: first, text: text) - case let .hint(password, _): - return .hint(password: password, text: text) - case let .email(password, hint, _): - return .email(password: password, hint: hint, text: text) - } - } - - static func ==(lhs: PasswordEntryStage, rhs: PasswordEntryStage) -> Bool { - switch lhs { - case let .entry(text): - if case .entry(text) = rhs { - return true - } else { - return false - } - case let .reentry(first, text): - if case .reentry(first, text) = rhs { - return true - } else { - return false - } - case let .hint(password, text): - if case .hint(password, text) = rhs { - return true - } else { - return false - } - case let .email(password, hint, text): - if case .email(password, hint, text) = rhs { - return true - } else { - return false - } - } - } -} - -private struct TwoStepVerificationPasswordEntryControllerState: Equatable { - let stage: PasswordEntryStage - let updating: Bool - - init(stage: PasswordEntryStage, updating: Bool) { - self.stage = stage - self.updating = updating - } - - static func ==(lhs: TwoStepVerificationPasswordEntryControllerState, rhs: TwoStepVerificationPasswordEntryControllerState) -> Bool { - if lhs.stage != rhs.stage { - return false - } - if lhs.updating != rhs.updating { - return false - } - - return true - } - - func withUpdatedStage(_ stage: PasswordEntryStage) -> TwoStepVerificationPasswordEntryControllerState { - return TwoStepVerificationPasswordEntryControllerState(stage: stage, updating: self.updating) - } - - func withUpdatedUpdating(_ updating: Bool) -> TwoStepVerificationPasswordEntryControllerState { - return TwoStepVerificationPasswordEntryControllerState(stage: self.stage, updating: updating) - } -} - -private func twoStepVerificationPasswordEntryControllerEntries(presentationData: PresentationData, state: TwoStepVerificationPasswordEntryControllerState, mode: TwoStepVerificationPasswordEntryMode) -> [TwoStepVerificationPasswordEntryEntry] { - var entries: [TwoStepVerificationPasswordEntryEntry] = [] - - switch state.stage { - case let .entry(text): - entries.append(.passwordEntryTitle(presentationData.theme, presentationData.strings.TwoStepAuth_SetupPasswordEnterPasswordNew)) - entries.append(.passwordEntry(presentationData.theme, presentationData.strings, text)) - case let .reentry(_, text): - entries.append(.passwordEntryTitle(presentationData.theme, presentationData.strings.TwoStepAuth_SetupPasswordConfirmPassword)) - entries.append(.passwordEntry(presentationData.theme, presentationData.strings, text)) - case let .hint(_, text): - entries.append(.hintTitle(presentationData.theme, presentationData.strings.TwoStepAuth_SetupHint)) - entries.append(.hintEntry(presentationData.theme, presentationData.strings, text)) - case let .email(_, _, text): - entries.append(.emailEntry(presentationData.theme, presentationData.strings, text)) - entries.append(.emailInfo(presentationData.theme, presentationData.strings.TwoStepAuth_EmailHelp)) - } - - return entries -} - -enum TwoStepVerificationPasswordEntryMode { - case setup - case change(current: String) - case setupEmail(password: String) -} - -struct TwoStepVerificationPasswordEntryResult { - let password: String - let pendingEmail: TwoStepVerificationPendingEmail? -} - -func twoStepVerificationPasswordEntryController(context: AccountContext, mode: TwoStepVerificationPasswordEntryMode, result: Promise) -> ViewController { - let initialStage: PasswordEntryStage - switch mode { - case .setup, .change: - initialStage = .entry(text: "") - case .setupEmail: - initialStage = .email(password: "", hint: "", text: "") - } - let initialState = TwoStepVerificationPasswordEntryControllerState(stage: initialStage, updating: false) - - let statePromise = ValuePromise(initialState, ignoreRepeated: true) - let stateValue = Atomic(value: initialState) - let updateState: ((TwoStepVerificationPasswordEntryControllerState) -> TwoStepVerificationPasswordEntryControllerState) -> Void = { f in - statePromise.set(stateValue.modify { f($0) }) - } - - var dismissImpl: (() -> Void)? - var presentControllerImpl: ((ViewController, ViewControllerPresentationArguments) -> Void)? - - let actionsDisposable = DisposableSet() - - let updatePasswordDisposable = MetaDisposable() - actionsDisposable.add(updatePasswordDisposable) - - let checkPassword: () -> Void = { - var passwordHintEmail: (String, String, String)? - var invalidReentry = false - updateState { state in - if state.updating { - return state - } else { - switch state.stage { - case let .entry(text): - if text.isEmpty { - return state - } else { - return state.withUpdatedStage(.reentry(first: text, text: "")) - } - case let .reentry(first, text): - if text.isEmpty { - return state - } else if text != first { - invalidReentry = true - return state.withUpdatedStage(.entry(text: "")) - } else { - return state.withUpdatedStage(.hint(password: text, text: "")) - } - case let .hint(password, text): - switch mode { - case .setup: - return state.withUpdatedStage(.email(password: password, hint: text, text: "")) - case .change: - passwordHintEmail = (password, text, "") - return state.withUpdatedUpdating(true) - case .setupEmail: - preconditionFailure() - } - case let .email(password, hint, text): - passwordHintEmail = (password, hint, text) - return state.withUpdatedUpdating(true) - } - } - } - if let (password, hint, email) = passwordHintEmail { - switch mode { - case .setup, .change: - var currentPassword: String? - if case let .change(current) = mode { - currentPassword = current - } - updatePasswordDisposable.set((updateTwoStepVerificationPassword(network: context.account.network, currentPassword: currentPassword, updatedPassword: .password(password: password, hint: hint, email: email)) |> deliverOnMainQueue).start(next: { update in - updateState { - $0.withUpdatedUpdating(false) - } - switch update { - case let .password(password, pendingEmail): - result.set(.single(TwoStepVerificationPasswordEntryResult(password: password, pendingEmail: pendingEmail))) - case .none: - break - } - }, error: { error in - updateState { - $0.withUpdatedUpdating(false) - } - let presentationData = context.sharedContext.currentPresentationData.with { $0 } - let alertText: String - switch error { - case .generic: - alertText = presentationData.strings.Login_UnknownError - case .invalidEmail: - alertText = presentationData.strings.TwoStepAuth_EmailInvalid - } - presentControllerImpl?(textAlertController(context: context, title: nil, text: alertText, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) - })) - case let .setupEmail(password): - updatePasswordDisposable.set((updateTwoStepVerificationEmail(network: context.account.network, currentPassword: password, updatedEmail: email) |> deliverOnMainQueue).start(next: { update in - updateState { - $0.withUpdatedUpdating(false) - } - switch update { - case let .password(password, pendingEmail): - result.set(.single(TwoStepVerificationPasswordEntryResult(password: password, pendingEmail: pendingEmail))) - case .none: - break - } - }, error: { error in - updateState { - $0.withUpdatedUpdating(false) - } - let presentationData = context.sharedContext.currentPresentationData.with { $0 } - let alertText: String - switch error { - case .generic: - alertText = presentationData.strings.Login_UnknownError - case .invalidEmail: - alertText = presentationData.strings.TwoStepAuth_EmailInvalid - } - presentControllerImpl?(textAlertController(context: context, title: nil, text: alertText, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) - })) - } - } else if invalidReentry { - let presentationData = context.sharedContext.currentPresentationData.with { $0 } - presentControllerImpl?(textAlertController(context: context, title: nil, text: presentationData.strings.TwoStepAuth_SetupPasswordConfirmFailed, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) - } - } - - let arguments = TwoStepVerificationPasswordEntryControllerArguments(updateEntryText: { updatedText in - updateState { - $0.withUpdatedStage($0.stage.updateCurrentText(updatedText)) - } - }, next: { - checkPassword() - }) - - let signal = combineLatest(context.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?() - }) - - var rightNavigationButton: ItemListNavigationButton? - if state.updating { - rightNavigationButton = ItemListNavigationButton(content: .none, style: .activity, enabled: true, action: {}) - } else { - var nextEnabled = true - switch state.stage { - case let .entry(text): - if text.isEmpty { - nextEnabled = false - } - case let.reentry(_, text): - if text.isEmpty { - nextEnabled = false - } - case .hint, .email: - break - } - rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Next), style: .bold, enabled: nextEnabled, action: { - checkPassword() - }) - } - - let title: String - switch mode { - case .setup, .change: - title = presentationData.strings.TwoStepAuth_EnterPasswordTitle - case .setupEmail: - title = presentationData.strings.TwoStepAuth_EmailTitle - } - - 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 { - actionsDisposable.dispose() - } - - let controller = ItemListController(context: context, state: signal) - presentControllerImpl = { [weak controller] c, p in - if let controller = controller { - controller.present(c, in: .window(.root), with: p) - } - } - dismissImpl = { [weak controller] in - controller?.view.endEditing(true) - controller?.dismiss() - } - - return controller -} diff --git a/submodules/SettingsUI/Sources/Privacy and Security/TwoStepVerificationResetController.swift b/submodules/SettingsUI/Sources/Privacy and Security/TwoStepVerificationResetController.swift deleted file mode 100644 index 2f6091ea8e..0000000000 --- a/submodules/SettingsUI/Sources/Privacy and Security/TwoStepVerificationResetController.swift +++ /dev/null @@ -1,247 +0,0 @@ -import Foundation -import UIKit -import Display -import SwiftSignalKit -import Postbox -import TelegramCore -import SyncCore -import TelegramPresentationData -import ItemListUI -import PresentationDataUtils -import TextFormat -import AccountContext -import AlertUI -import PresentationDataUtils -import Markdown - -private final class TwoStepVerificationResetControllerArguments { - let updateEntryText: (String) -> Void - let next: () -> Void - let openEmailInaccessible: () -> Void - - init(updateEntryText: @escaping (String) -> Void, next: @escaping () -> Void, openEmailInaccessible: @escaping () -> Void) { - self.updateEntryText = updateEntryText - self.next = next - self.openEmailInaccessible = openEmailInaccessible - } -} - -private enum TwoStepVerificationResetSection: Int32 { - case password -} - -private enum TwoStepVerificationResetTag: ItemListItemTag { - case input - - func isEqual(to other: ItemListItemTag) -> Bool { - if let other = other as? TwoStepVerificationResetTag { - switch self { - case .input: - if case .input = other { - return true - } else { - return false - } - } - } else { - return false - } - } -} - -private enum TwoStepVerificationResetEntry: ItemListNodeEntry { - case codeEntry(PresentationTheme, PresentationStrings, String, String) - case codeInfo(PresentationTheme, String) - - var section: ItemListSectionId { - return TwoStepVerificationResetSection.password.rawValue - } - - var stableId: Int32 { - switch self { - case .codeEntry: - return 0 - case .codeInfo: - return 1 - } - } - - static func ==(lhs: TwoStepVerificationResetEntry, rhs: TwoStepVerificationResetEntry) -> Bool { - switch lhs { - case let .codeEntry(lhsTheme, lhsStrings, lhsPlaceholder, lhsText): - if case let .codeEntry(rhsTheme, rhsStrings, rhsPlaceholder, rhsText) = rhs, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsPlaceholder == rhsPlaceholder, lhsText == rhsText { - return true - } else { - return false - } - case let .codeInfo(lhsTheme, lhsText): - if case let .codeInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { - return true - } else { - return false - } - } - } - - static func <(lhs: TwoStepVerificationResetEntry, rhs: TwoStepVerificationResetEntry) -> Bool { - return lhs.stableId < rhs.stableId - } - - func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { - let arguments = arguments as! TwoStepVerificationResetControllerArguments - switch self { - case let .codeEntry(theme, strings, placeholder, text): - 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(presentationData: presentationData, text: text, sectionId: self.section) - } - } -} - -private struct TwoStepVerificationResetControllerState: Equatable { - let codeText: String - let checking: Bool - - init(codeText: String, checking: Bool) { - self.codeText = codeText - self.checking = checking - } - - static func ==(lhs: TwoStepVerificationResetControllerState, rhs: TwoStepVerificationResetControllerState) -> Bool { - if lhs.codeText != rhs.codeText { - return false - } - if lhs.checking != rhs.checking { - return false - } - - return true - } - - func withUpdatedCodeText(_ codeText: String) -> TwoStepVerificationResetControllerState { - return TwoStepVerificationResetControllerState(codeText: codeText, checking: self.checking) - } - - func withUpdatedChecking(_ checking: Bool) -> TwoStepVerificationResetControllerState { - return TwoStepVerificationResetControllerState(codeText: self.codeText, checking: checking) - } -} - -private func twoStepVerificationResetControllerEntries(presentationData: PresentationData, state: TwoStepVerificationResetControllerState, emailPattern: String) -> [TwoStepVerificationResetEntry] { - var entries: [TwoStepVerificationResetEntry] = [] - - entries.append(.codeEntry(presentationData.theme, presentationData.strings, presentationData.strings.TwoStepAuth_RecoveryCode, state.codeText)) - entries.append(.codeInfo(presentationData.theme, "\(presentationData.strings.TwoStepAuth_RecoveryCodeHelp)\n\n[\(presentationData.strings.TwoStepAuth_RecoveryEmailUnavailable(escapedPlaintextForMarkdown(emailPattern)).0)]()")) - - return entries -} - -func twoStepVerificationResetController(context: AccountContext, emailPattern: String, result: Promise) -> ViewController { - let initialState = TwoStepVerificationResetControllerState(codeText: "", checking: false) - - let statePromise = ValuePromise(initialState, ignoreRepeated: true) - let stateValue = Atomic(value: initialState) - let updateState: ((TwoStepVerificationResetControllerState) -> TwoStepVerificationResetControllerState) -> Void = { f in - statePromise.set(stateValue.modify { f($0) }) - } - - var dismissImpl: (() -> Void)? - var presentControllerImpl: ((ViewController, ViewControllerPresentationArguments) -> Void)? - - let actionsDisposable = DisposableSet() - - let resetPasswordDisposable = MetaDisposable() - actionsDisposable.add(resetPasswordDisposable) - - let checkCode: () -> Void = { - var code: String? - updateState { state in - if state.checking || state.codeText.isEmpty { - return state - } else { - code = state.codeText - return state.withUpdatedChecking(true) - } - } - if let code = code { - resetPasswordDisposable.set((recoverTwoStepVerificationPassword(network: context.account.network, code: code) |> deliverOnMainQueue).start(error: { error in - updateState { - return $0.withUpdatedChecking(false) - } - let presentationData = context.sharedContext.currentPresentationData.with { $0 } - let alertText: String - switch error { - case .generic: - alertText = presentationData.strings.Login_UnknownError - case .invalidCode: - alertText = presentationData.strings.Login_InvalidCodeError - case .codeExpired: - alertText = presentationData.strings.Login_CodeExpiredError - case .limitExceeded: - alertText = presentationData.strings.Login_CodeFloodError - } - presentControllerImpl?(textAlertController(context: context, title: nil, text: alertText, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) - }, completed: { - updateState { - return $0.withUpdatedChecking(false) - } - result.set(.single(true)) - })) - } - } - - let arguments = TwoStepVerificationResetControllerArguments(updateEntryText: { updatedText in - updateState { - $0.withUpdatedCodeText(updatedText) - } - }, next: { - checkCode() - }, openEmailInaccessible: { - let presentationData = context.sharedContext.currentPresentationData.with { $0 } - presentControllerImpl?(textAlertController(context: context, title: nil, text: presentationData.strings.TwoStepAuth_RecoveryFailed, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) - }) - - let signal = combineLatest(context.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?() - }) - - var rightNavigationButton: ItemListNavigationButton? - if state.checking { - rightNavigationButton = ItemListNavigationButton(content: .none, style: .activity, enabled: true, action: {}) - } else { - var nextEnabled = true - if state.codeText.isEmpty { - nextEnabled = false - } - rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Next), style: .bold, enabled: nextEnabled, action: { - checkCode() - }) - } - - 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 { - actionsDisposable.dispose() - } - - let controller = ItemListController(context: context, state: signal) - presentControllerImpl = { [weak controller] c, p in - if let controller = controller { - controller.present(c, in: .window(.root), with: p) - } - } - dismissImpl = { [weak controller] in - controller?.dismiss() - } - - return controller -} diff --git a/submodules/SettingsUI/Sources/Privacy and Security/TwoStepVerificationUnlockController.swift b/submodules/SettingsUI/Sources/Privacy and Security/TwoStepVerificationUnlockController.swift index 71419adcbb..dd2af7c141 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/TwoStepVerificationUnlockController.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/TwoStepVerificationUnlockController.swift @@ -26,8 +26,10 @@ private final class TwoStepVerificationUnlockSettingsControllerArguments { let openResetPendingEmail: () -> Void let updateEmailCode: (String) -> Void let openConfirmEmail: () -> Void + let declinePasswordReset: () -> Void + let resetPassword: () -> Void - init(updatePasswordText: @escaping (String) -> Void, checkPassword: @escaping () -> Void, openForgotPassword: @escaping () -> Void, openSetupPassword: @escaping () -> Void, openDisablePassword: @escaping () -> Void, openSetupEmail: @escaping () -> Void, openResetPendingEmail: @escaping () -> Void, updateEmailCode: @escaping (String) -> Void, openConfirmEmail: @escaping () -> Void) { + init(updatePasswordText: @escaping (String) -> Void, checkPassword: @escaping () -> Void, openForgotPassword: @escaping () -> Void, openSetupPassword: @escaping () -> Void, openDisablePassword: @escaping () -> Void, openSetupEmail: @escaping () -> Void, openResetPendingEmail: @escaping () -> Void, updateEmailCode: @escaping (String) -> Void, openConfirmEmail: @escaping () -> Void, declinePasswordReset: @escaping () -> Void, resetPassword: @escaping () -> Void) { self.updatePasswordText = updatePasswordText self.checkPassword = checkPassword self.openForgotPassword = openForgotPassword @@ -37,6 +39,8 @@ private final class TwoStepVerificationUnlockSettingsControllerArguments { self.openResetPendingEmail = openResetPendingEmail self.updateEmailCode = updateEmailCode self.openConfirmEmail = openConfirmEmail + self.declinePasswordReset = declinePasswordReset + self.resetPassword = resetPassword } } @@ -132,47 +136,53 @@ private enum TwoStepVerificationUnlockSettingsEntry: ItemListNodeEntry { }, action: { arguments.checkPassword() }) - case let .passwordEntryInfo(theme, text): + case let .passwordEntryInfo(_, text): return ItemListTextItem(presentationData: presentationData, text: .markdown(text), sectionId: self.section, linkAction: { action in switch action { - case .tap: - arguments.openForgotPassword() + case let .tap(item): + if item == "forgot" { + arguments.openForgotPassword() + } else if item == "declineReset" { + arguments.declinePasswordReset() + } else if item == "reset" { + arguments.resetPassword() + } } }) - case let .passwordSetup(theme, text): + case let .passwordSetup(_, text): return ItemListActionItem(presentationData: presentationData, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: { arguments.openSetupPassword() }) - case let .passwordSetupInfo(theme, text): + case let .passwordSetupInfo(_, text): return ItemListTextItem(presentationData: presentationData, text: .markdown(text), sectionId: self.section) - case let .changePassword(theme, text): + case let .changePassword(_, text): return ItemListActionItem(presentationData: presentationData, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: { arguments.openSetupPassword() }) - case let .turnPasswordOff(theme, text): + case let .turnPasswordOff(_, text): return ItemListActionItem(presentationData: presentationData, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: { arguments.openDisablePassword() }) - case let .setupRecoveryEmail(theme, text): + case let .setupRecoveryEmail(_, text): return ItemListActionItem(presentationData: presentationData, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: { arguments.openSetupEmail() }) - case let .passwordInfo(theme, text): + case let .passwordInfo(_, text): return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) - case let .pendingEmailConfirmInfo(theme, text): + case let .pendingEmailConfirmInfo(_, text): return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) - case let .pendingEmailConfirmCode(theme, strings, title, text): + case let .pendingEmailConfirmCode(_, _, title, text): 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): + case let .pendingEmailInfo(_, text): return ItemListTextItem(presentationData: presentationData, text: .markdown(text), sectionId: self.section, linkAction: { action in switch action { case .tap: arguments.openResetPendingEmail() } }) - case let .pendingEmailOpenConfirm(theme, text): + case let .pendingEmailOpenConfirm(_, text): return ItemListActionItem(presentationData: presentationData, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: { arguments.openConfirmEmail() }) @@ -204,13 +214,28 @@ private func twoStepVerificationUnlockSettingsControllerEntries(presentationData entries.append(.passwordSetup(presentationData.theme, presentationData.strings.TwoStepAuth_SetPassword)) entries.append(.passwordSetupInfo(presentationData.theme, presentationData.strings.TwoStepAuth_SetPasswordHelp)) } - case let .set(hint, _, _): + case let .set(hint, _, _, pendingResetTimestamp): entries.append(.passwordEntry(presentationData.theme, presentationData.strings, presentationData.strings.TwoStepAuth_EnterPasswordPassword, state.passwordText)) - if hint.isEmpty { - entries.append(.passwordEntryInfo(presentationData.theme, presentationData.strings.TwoStepAuth_EnterPasswordHelp + "\n\n[" + presentationData.strings.TwoStepAuth_EnterPasswordForgot + "](forgot)")) - } else { - entries.append(.passwordEntryInfo(presentationData.theme, presentationData.strings.TwoStepAuth_EnterPasswordHint(escapedPlaintextForMarkdown(hint)).0 + "\n\n" + presentationData.strings.TwoStepAuth_EnterPasswordHelp + "\n\n[" + presentationData.strings.TwoStepAuth_EnterPasswordForgot + "](forgot)")) + var text: String = "" + if !hint.isEmpty { + text += presentationData.strings.TwoStepAuth_EnterPasswordHint(escapedPlaintextForMarkdown(hint)).0 } + + if let pendingResetTimestamp = pendingResetTimestamp { + text += "\n\n" + let remainingSeconds = pendingResetTimestamp - Int32(Date().timeIntervalSince1970) + if remainingSeconds <= 0 { + text += "[" + presentationData.strings.TwoStepAuth_ResetAction + "](reset)" + } else { + text.append(presentationData.strings.TwoStepAuth_ResetPendingText(timeIntervalString(strings: presentationData.strings, value: remainingSeconds)).0) + text.append("\n[\(presentationData.strings.TwoStepAuth_CancelResetTitle)](declineReset)") + } + } else { + text += "\n\n" + text += presentationData.strings.TwoStepAuth_EnterPasswordHelp + "\n\n[" + presentationData.strings.TwoStepAuth_EnterPasswordForgot + "](forgot)" + } + + entries.append(.passwordEntryInfo(presentationData.theme, text)) } } case let .manage(_, emailSet, pendingEmail, _): @@ -228,36 +253,36 @@ private func twoStepVerificationUnlockSettingsControllerEntries(presentationData return entries } -enum TwoStepVerificationUnlockSettingsControllerMode { +public enum TwoStepVerificationUnlockSettingsControllerMode { case access(intro: Bool, data: Signal?) case manage(password: String, email: String, pendingEmail: TwoStepVerificationPendingEmail?, hasSecureValues: Bool) } -struct TwoStepVerificationPendingEmailState: Equatable { +public struct TwoStepVerificationPendingEmailState: Equatable { let password: String? let email: TwoStepVerificationPendingEmail } -enum TwoStepVerificationAccessConfiguration: Equatable { +public enum TwoStepVerificationAccessConfiguration: Equatable { case notSet(pendingEmail: TwoStepVerificationPendingEmailState?) - case set(hint: String, hasRecoveryEmail: Bool, hasSecureValues: Bool) + case set(hint: String, hasRecoveryEmail: Bool, hasSecureValues: Bool, pendingResetTimestamp: Int32?) - init(configuration: TwoStepVerificationConfiguration, password: String?) { + public init(configuration: TwoStepVerificationConfiguration, password: String?) { switch configuration { case let .notSet(pendingEmail): self = .notSet(pendingEmail: pendingEmail.flatMap({ TwoStepVerificationPendingEmailState(password: password, email: $0) })) - case let .set(hint, hasRecoveryEmail, _, hasSecureValues): - self = .set(hint: hint, hasRecoveryEmail: hasRecoveryEmail, hasSecureValues: hasSecureValues) + case let .set(hint, hasRecoveryEmail, _, hasSecureValues, pendingResetTimestamp): + self = .set(hint: hint, hasRecoveryEmail: hasRecoveryEmail, hasSecureValues: hasSecureValues, pendingResetTimestamp: pendingResetTimestamp) } } } -enum TwoStepVerificationUnlockSettingsControllerData: Equatable { +public enum TwoStepVerificationUnlockSettingsControllerData: Equatable { case access(configuration: TwoStepVerificationAccessConfiguration?) case manage(password: String, emailSet: Bool, pendingEmail: TwoStepVerificationPendingEmail?, hasSecureValues: Bool) } -func twoStepVerificationUnlockSettingsController(context: AccountContext, mode: TwoStepVerificationUnlockSettingsControllerMode, openSetupPasswordImmediately: Bool = false) -> ViewController { +public func twoStepVerificationUnlockSettingsController(context: AccountContext, mode: TwoStepVerificationUnlockSettingsControllerMode, openSetupPasswordImmediately: Bool = false) -> ViewController { let initialState = TwoStepVerificationUnlockSettingsControllerState() let statePromise = ValuePromise(initialState, ignoreRepeated: true) @@ -268,6 +293,7 @@ func twoStepVerificationUnlockSettingsController(context: AccountContext, mode: var replaceControllerImpl: ((ViewController, Bool) -> Void)? var presentControllerImpl: ((ViewController, ViewControllerPresentationArguments?) -> Void)? + var pushControllerImpl: ((ViewController) -> Void)? var dismissImpl: (() -> Void)? let actionsDisposable = DisposableSet() @@ -291,7 +317,7 @@ func twoStepVerificationUnlockSettingsController(context: AccountContext, mode: } else { dataPromise.set(.single(TwoStepVerificationUnlockSettingsControllerData.access(configuration: nil)) |> then(remoteDataPromise.get())) - remoteDataPromise.set(twoStepVerificationConfiguration(account: context.account) + remoteDataPromise.set(context.engine.auth.twoStepVerificationConfiguration() |> map { TwoStepVerificationUnlockSettingsControllerData.access(configuration: TwoStepVerificationAccessConfiguration(configuration: $0, password: nil)) }) } case let .manage(password, email, pendingEmail, hasSecureValues): @@ -330,7 +356,7 @@ func twoStepVerificationUnlockSettingsController(context: AccountContext, mode: return state } if let code = code { - setupDisposable.set((confirmTwoStepRecoveryEmail(network: context.account.network, code: code) + setupDisposable.set((context.engine.auth.confirmTwoStepRecoveryEmail(code: code) |> deliverOnMainQueue).start(error: { error in updateState { state in var state = state @@ -379,7 +405,7 @@ func twoStepVerificationUnlockSettingsController(context: AccountContext, mode: dataPromise.set(.single(TwoStepVerificationUnlockSettingsControllerData.manage(password: password, emailSet: true, pendingEmail: nil, hasSecureValues: false))) } else { dataPromise.set(.single(.access(configuration: nil)) - |> then(twoStepVerificationConfiguration(account: context.account) |> map { TwoStepVerificationUnlockSettingsControllerData.access(configuration: TwoStepVerificationAccessConfiguration(configuration: $0, password: pendingEmail.password)) })) + |> then(context.engine.auth.twoStepVerificationConfiguration() |> map { TwoStepVerificationUnlockSettingsControllerData.access(configuration: TwoStepVerificationAccessConfiguration(configuration: $0, password: pendingEmail.password)) })) } case let .manage(manage): dataPromise.set(.single(TwoStepVerificationUnlockSettingsControllerData.manage(password: manage.password, emailSet: true, pendingEmail: nil, hasSecureValues: manage.hasSecureValues))) @@ -416,11 +442,10 @@ func twoStepVerificationUnlockSettingsController(context: AccountContext, mode: } if let password = password, !password.isEmpty, !wasChecking { - checkDisposable.set((requestTwoStepVerifiationSettings(network: context.account.network, password: password) + checkDisposable.set((context.engine.auth.requestTwoStepVerifiationSettings(password: password) |> mapToSignal { settings -> Signal<(TwoStepVerificationSettings, TwoStepVerificationPendingEmail?), AuthorizationPasswordVerificationError> in - return twoStepVerificationConfiguration(account: context.account) + return context.engine.auth.twoStepVerificationConfiguration() |> mapError { _ -> AuthorizationPasswordVerificationError in - return .generic } |> map { configuration in var pendingEmail: TwoStepVerificationPendingEmail? @@ -469,34 +494,42 @@ func twoStepVerificationUnlockSettingsController(context: AccountContext, mode: if let configuration = configuration { let presentationData = context.sharedContext.currentPresentationData.with { $0 } switch configuration { - case let .set(_, hasRecoveryEmail, _): + case let .set(_, hasRecoveryEmail, _, pendingResetTimestamp): if hasRecoveryEmail { updateState { state in var state = state state.checking = true return state } - setupResultDisposable.set((requestTwoStepVerificationPasswordRecoveryCode(network: context.account.network) + setupResultDisposable.set((context.engine.auth.requestTwoStepVerificationPasswordRecoveryCode() |> deliverOnMainQueue).start(next: { emailPattern in updateState { state in var state = state state.checking = false return state } - - var completionImpl: (() -> Void)? - let controller = resetPasswordController(context: context, emailPattern: emailPattern, completion: { - completionImpl?() + + var stateUpdated: ((SetupTwoStepVerificationStateUpdate) -> Void)? + let controller = TwoFactorDataInputScreen(sharedContext: context.sharedContext, engine: .authorized(context.engine), mode: .passwordRecoveryEmail(emailPattern: emailPattern, mode: .authorized), stateUpdated: { state in + stateUpdated?(state) }) - completionImpl = { [weak controller] in - dataPromise.set(.single(TwoStepVerificationUnlockSettingsControllerData.access(configuration: .notSet(pendingEmail: nil)))) + stateUpdated = { [weak controller] state in controller?.view.endEditing(true) controller?.dismiss() - let presentationData = context.sharedContext.currentPresentationData.with { $0 } - presentControllerImpl?(OverlayStatusController(theme: presentationData.theme, type: .genericSuccess(presentationData.strings.TwoStepAuth_DisableSuccess, false)), nil) + switch state { + case .noPassword, .awaitingEmailConfirmation, .passwordSet: + controller?.dismiss() + + dismissImpl?() + case .pendingPasswordReset: + dataPromise.set(context.engine.auth.twoStepVerificationConfiguration() + |> map { TwoStepVerificationUnlockSettingsControllerData.access(configuration: TwoStepVerificationAccessConfiguration(configuration: $0, password: nil)) + }) + } } - presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + + pushControllerImpl?(controller) }, error: { _ in updateState { state in var state = state @@ -506,7 +539,64 @@ func twoStepVerificationUnlockSettingsController(context: AccountContext, mode: presentControllerImpl?(textAlertController(context: context, title: nil, text: presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) })) } else { - presentControllerImpl?(textAlertController(context: context, title: nil, text: presentationData.strings.TwoStepAuth_RecoveryUnavailable, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + if let pendingResetTimestamp = pendingResetTimestamp { + let remainingSeconds = pendingResetTimestamp - Int32(Date().timeIntervalSince1970) + if remainingSeconds <= 0 { + let _ = (context.engine.auth.requestTwoStepPasswordReset() + |> deliverOnMainQueue).start(next: { result in + switch result { + case .done, .waitingForReset: + dataPromise.set(context.engine.auth.twoStepVerificationConfiguration() + |> map { TwoStepVerificationUnlockSettingsControllerData.access(configuration: TwoStepVerificationAccessConfiguration(configuration: $0, password: nil)) + }) + case .declined: + break + case let .error(reason): + let text: String + switch reason { + case let .limitExceeded(retryAtTimestamp): + if let retryAtTimestamp = retryAtTimestamp { + let remainingSeconds = retryAtTimestamp - Int32(Date().timeIntervalSince1970) + text = presentationData.strings.TwoFactorSetup_ResetFloodWait(timeIntervalString(strings: presentationData.strings, value: remainingSeconds)).0 + } else { + text = presentationData.strings.TwoStepAuth_FloodError + } + case .generic: + text = presentationData.strings.Login_UnknownError + } + presentControllerImpl?(textAlertController(sharedContext: context.sharedContext, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil) + } + }) + } + } else { + presentControllerImpl?(textAlertController(context: context, title: presentationData.strings.TwoStepAuth_RecoveryUnavailableResetTitle, text: presentationData.strings.TwoStepAuth_RecoveryUnavailableResetText, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: presentationData.strings.TwoStepAuth_RecoveryUnavailableResetAction, action: { + let _ = (context.engine.auth.requestTwoStepPasswordReset() + |> deliverOnMainQueue).start(next: { result in + switch result { + case .done, .waitingForReset: + dataPromise.set(context.engine.auth.twoStepVerificationConfiguration() + |> map { TwoStepVerificationUnlockSettingsControllerData.access(configuration: TwoStepVerificationAccessConfiguration(configuration: $0, password: nil)) + }) + case .declined: + break + case let .error(reason): + let text: String + switch reason { + case let .limitExceeded(retryAtTimestamp): + if let retryAtTimestamp = retryAtTimestamp { + let remainingSeconds = retryAtTimestamp - Int32(Date().timeIntervalSince1970) + text = presentationData.strings.TwoFactorSetup_ResetFloodWait(timeIntervalString(strings: presentationData.strings, value: remainingSeconds)).0 + } else { + text = presentationData.strings.TwoStepAuth_FloodError + } + case .generic: + text = presentationData.strings.Login_UnknownError + } + presentControllerImpl?(textAlertController(sharedContext: context.sharedContext, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil) + } + }) + })]), ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + } } case .notSet: break @@ -527,6 +617,8 @@ func twoStepVerificationUnlockSettingsController(context: AccountContext, mode: case .notSet: let controller = SetupTwoStepVerificationController(context: context, initialState: .createPassword, stateUpdated: { update, shouldDismiss, controller in switch update { + case .pendingPasswordReset: + break case .noPassword: dataPromise.set(.single(.access(configuration: .notSet(pendingEmail: nil)))) case let .awaitingEmailConfirmation(password, pattern, codeLength): @@ -538,7 +630,7 @@ func twoStepVerificationUnlockSettingsController(context: AccountContext, mode: presentControllerImpl?(OverlayStatusController(theme: presentationData.theme, type: .genericSuccess(presentationData.strings.TwoStepAuth_EnabledSuccess, false)), nil) } else { dataPromise.set(.single(.access(configuration: nil)) - |> then(twoStepVerificationConfiguration(account: context.account) |> map { TwoStepVerificationUnlockSettingsControllerData.access(configuration: TwoStepVerificationAccessConfiguration(configuration: $0, password: password)) })) + |> then(context.engine.auth.twoStepVerificationConfiguration() |> map { TwoStepVerificationUnlockSettingsControllerData.access(configuration: TwoStepVerificationAccessConfiguration(configuration: $0, password: password)) })) } } if shouldDismiss { @@ -558,6 +650,8 @@ func twoStepVerificationUnlockSettingsController(context: AccountContext, mode: case let .manage(password, hasRecovery, pendingEmail, hasSecureValues): let controller = SetupTwoStepVerificationController(context: context, initialState: .updatePassword(current: password, hasRecoveryEmail: hasRecovery, hasSecureValues: hasSecureValues), stateUpdated: { update, shouldDismiss, controller in switch update { + case .pendingPasswordReset: + break case .noPassword: dataPromise.set(.single(.access(configuration: .notSet(pendingEmail: nil)))) case .awaitingEmailConfirmation: @@ -570,7 +664,7 @@ func twoStepVerificationUnlockSettingsController(context: AccountContext, mode: presentControllerImpl?(OverlayStatusController(theme: presentationData.theme, type: .genericSuccess(presentationData.strings.TwoStepAuth_PasswordChangeSuccess, false)), nil) } else { dataPromise.set(.single(.access(configuration: nil)) - |> then(twoStepVerificationConfiguration(account: context.account) |> map { TwoStepVerificationUnlockSettingsControllerData.access(configuration: TwoStepVerificationAccessConfiguration(configuration: $0, password: password)) })) + |> then(context.engine.auth.twoStepVerificationConfiguration() |> map { TwoStepVerificationUnlockSettingsControllerData.access(configuration: TwoStepVerificationAccessConfiguration(configuration: $0, password: password)) })) } } if shouldDismiss { @@ -612,7 +706,7 @@ func twoStepVerificationUnlockSettingsController(context: AccountContext, mode: case let .manage(password, _, _, _): let presentationData = context.sharedContext.currentPresentationData.with { $0 } presentControllerImpl?(OverlayStatusController(theme: presentationData.theme, type: .genericSuccess(presentationData.strings.TwoStepAuth_DisableSuccess, false)), nil) - return updateTwoStepVerificationPassword(network: context.account.network, currentPassword: password, updatedPassword: .none) + return context.engine.auth.updateTwoStepVerificationPassword(currentPassword: password, updatedPassword: .none) |> mapToSignal { _ -> Signal in return .complete() } @@ -647,10 +741,10 @@ func twoStepVerificationUnlockSettingsController(context: AccountContext, mode: case .access: break case let .manage(password, emailSet, _, hasSecureValues): - //let controller = TwoFactorDataInputScreen(context: context, mode: .updateEmailAddress(password: password)) - let controller = SetupTwoStepVerificationController(context: context, initialState: .addEmail(hadRecoveryEmail: emailSet, hasSecureValues: hasSecureValues, password: password), stateUpdated: { update, shouldDismiss, controller in switch update { + case .pendingPasswordReset: + break case .noPassword: assertionFailure() break @@ -665,7 +759,7 @@ func twoStepVerificationUnlockSettingsController(context: AccountContext, mode: presentControllerImpl?(OverlayStatusController(theme: presentationData.theme, type: .genericSuccess(emailSet ? presentationData.strings.TwoStepAuth_EmailChangeSuccess : presentationData.strings.TwoStepAuth_EmailAddSuccess, false)), nil) } else { dataPromise.set(.single(.access(configuration: nil)) - |> then(twoStepVerificationConfiguration(account: context.account) |> map { TwoStepVerificationUnlockSettingsControllerData.access(configuration: TwoStepVerificationAccessConfiguration(configuration: $0, password: password)) })) + |> then(context.engine.auth.twoStepVerificationConfiguration() |> map { TwoStepVerificationUnlockSettingsControllerData.access(configuration: TwoStepVerificationAccessConfiguration(configuration: $0, password: password)) })) } } if shouldDismiss { @@ -681,7 +775,7 @@ func twoStepVerificationUnlockSettingsController(context: AccountContext, mode: state.checking = true return state } - setupDisposable.set((updateTwoStepVerificationPassword(network: context.account.network, currentPassword: nil, updatedPassword: .none) + setupDisposable.set((context.engine.auth.updateTwoStepVerificationPassword(currentPassword: nil, updatedPassword: .none) |> deliverOnMainQueue).start(next: { _ in updateState { state in var state = state @@ -738,6 +832,8 @@ func twoStepVerificationUnlockSettingsController(context: AccountContext, mode: } let controller = SetupTwoStepVerificationController(context: context, initialState: .confirmEmail(password: password, hasSecureValues: hasSecureValues, pattern: pendingEmail.pattern, codeLength: pendingEmail.codeLength), stateUpdated: { update, shouldDismiss, controller in switch update { + case .pendingPasswordReset: + break case .noPassword: assertionFailure() break @@ -752,7 +848,7 @@ func twoStepVerificationUnlockSettingsController(context: AccountContext, mode: dataPromise.set(.single(data)) } else { dataPromise.set(.single(.access(configuration: nil)) - |> then(twoStepVerificationConfiguration(account: context.account) |> map { TwoStepVerificationUnlockSettingsControllerData.access(configuration: TwoStepVerificationAccessConfiguration(configuration: $0, password: password)) })) + |> then(context.engine.auth.twoStepVerificationConfiguration() |> map { TwoStepVerificationUnlockSettingsControllerData.access(configuration: TwoStepVerificationAccessConfiguration(configuration: $0, password: password)) })) } } if shouldDismiss { @@ -762,12 +858,75 @@ func twoStepVerificationUnlockSettingsController(context: AccountContext, mode: presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) } }) + }, declinePasswordReset: { + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + presentControllerImpl?(textAlertController(context: context, title: presentationData.strings.TwoStepAuth_CancelResetTitle, text: presentationData.strings.TwoStepAuth_CancelResetText, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Yes, action: { + let _ = (context.engine.auth.declineTwoStepPasswordReset() + |> deliverOnMainQueue).start(completed: { + dataPromise.set(context.engine.auth.twoStepVerificationConfiguration() + |> map { TwoStepVerificationUnlockSettingsControllerData.access(configuration: TwoStepVerificationAccessConfiguration(configuration: $0, password: nil)) + }) + }) + }), TextAlertAction(type: .genericAction, title: presentationData.strings.Common_No, action: { + })]), ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + }, resetPassword: { + let _ = (context.engine.auth.requestTwoStepPasswordReset() + |> deliverOnMainQueue).start(next: { result in + switch result { + case .done: + dismissImpl?() + case .waitingForReset: + dataPromise.set(context.engine.auth.twoStepVerificationConfiguration() + |> map { TwoStepVerificationUnlockSettingsControllerData.access(configuration: TwoStepVerificationAccessConfiguration(configuration: $0, password: nil)) + }) + case .declined: + break + case let .error(reason): + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + let text: String + switch reason { + case let .limitExceeded(retryAtTimestamp): + if let retryAtTimestamp = retryAtTimestamp { + let remainingSeconds = retryAtTimestamp - Int32(Date().timeIntervalSince1970) + text = presentationData.strings.TwoFactorSetup_ResetFloodWait(timeIntervalString(strings: presentationData.strings, value: remainingSeconds)).0 + } else { + text = presentationData.strings.TwoStepAuth_FloodError + } + case .generic: + text = presentationData.strings.Login_UnknownError + } + presentControllerImpl?(textAlertController(sharedContext: context.sharedContext, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil) + } + }) }) var initialFocusImpl: (() -> Void)? var didAppear = false + + let dataWithTimer = dataPromise.get() + |> distinctUntilChanged + |> mapToSignal { data -> Signal in + switch data { + case let .access(configuration): + if let configuration = configuration { + switch configuration { + case let .set(_, _, _, pendingResetTimestamp): + if pendingResetTimestamp != nil { + return .single(data) + |> then(.complete() |> delay(0.5, queue: .mainQueue())) + |> restart + } + default: + break + } + } + default: + break + } + return .single(data) + } - let signal = combineLatest(context.sharedContext.presentationData, statePromise.get(), dataPromise.get() |> deliverOnMainQueue) |> deliverOnMainQueue + let signal = combineLatest(context.sharedContext.presentationData, statePromise.get(), dataWithTimer |> deliverOnMainQueue) |> deliverOnMainQueue |> map { presentationData, state, data -> (ItemListControllerState, (ItemListNodeState, Any)) in var rightNavigationButton: ItemListNavigationButton? var emptyStateItem: ItemListControllerEmptyStateItem? @@ -826,6 +985,9 @@ func twoStepVerificationUnlockSettingsController(context: AccountContext, mode: controller.present(c, in: .window(.root), with: p) } } + pushControllerImpl = { [weak controller] c in + controller?.push(c) + } dismissImpl = { [weak controller] in controller?.dismiss() } diff --git a/submodules/SettingsUI/Sources/Search/SettingsSearchItem.swift b/submodules/SettingsUI/Sources/Search/SettingsSearchItem.swift index 862cb1755b..4c1d7bb5b8 100644 --- a/submodules/SettingsUI/Sources/Search/SettingsSearchItem.swift +++ b/submodules/SettingsUI/Sources/Search/SettingsSearchItem.swift @@ -535,11 +535,11 @@ public final class SettingsSearchContainerNode: SearchDisplayControllerContentNo } }) - self.listNode.beganInteractiveDragging = { [weak self] in + self.listNode.beganInteractiveDragging = { [weak self] _ in self?.dismissInput?() } - self.recentListNode.beganInteractiveDragging = { [weak self] in + self.recentListNode.beganInteractiveDragging = { [weak self] _ in self?.dismissInput?() } } diff --git a/submodules/SettingsUI/Sources/Search/SettingsSearchRecentItem.swift b/submodules/SettingsUI/Sources/Search/SettingsSearchRecentItem.swift index e51d602ac6..22dfb3b3d4 100644 --- a/submodules/SettingsUI/Sources/Search/SettingsSearchRecentItem.swift +++ b/submodules/SettingsUI/Sources/Search/SettingsSearchRecentItem.swift @@ -243,9 +243,9 @@ class SettingsSearchRecentItemNode: ItemListRevealOptionsItemNode { self.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration * 0.5, removeOnCompletion: false) } - override public func header() -> ListViewItemHeader? { + override public func headers() -> [ListViewItemHeader]? { if let item = self.item { - return item.header + return item.header.flatMap { [$0] } } else { return nil } diff --git a/submodules/SettingsUI/Sources/Search/SettingsSearchableItems.swift b/submodules/SettingsUI/Sources/Search/SettingsSearchableItems.swift index 045996ac7a..ddb16e8422 100644 --- a/submodules/SettingsUI/Sources/Search/SettingsSearchableItems.swift +++ b/submodules/SettingsUI/Sources/Search/SettingsSearchableItems.swift @@ -430,7 +430,7 @@ private func privacySearchableItems(context: AccountContext, privacySettings: Ac if let privacySettings = privacySettings { privacySignal = .single(privacySettings) } else { - privacySignal = requestAccountPrivacySettings(account: context.account) + privacySignal = context.engine.privacy.requestAccountPrivacySettings() } let callsSignal: Signal<(VoiceCallSettings, VoipConfiguration)?, NoError> if case .voiceCalls = kind { @@ -535,10 +535,10 @@ private func privacySearchableItems(context: AccountContext, privacySettings: Ac present(.push, twoStepVerificationUnlockSettingsController(context: context, mode: .access(intro: true, data: nil))) }), 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)) + present(.push, recentSessionsController(context: context, activeSessionsContext: activeSessionsContext!, webSessionsContext: webSessionsContext ?? context.engine.privacy.webSessions(), websitesOnly: false)) }), 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)) + present(.push, recentSessionsController(context: context, activeSessionsContext: activeSessionsContext ?? context.engine.privacy.activeSessions(), webSessionsContext: webSessionsContext ?? context.engine.privacy.webSessions(), 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) @@ -708,7 +708,7 @@ private func languageSearchableItems(context: AccountContext, localizations: [Lo let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: nil)) present(.immediate, controller) - let _ = (downloadAndApplyLocalization(accountManager: context.sharedContext.accountManager, postbox: context.account.postbox, network: context.account.network, languageCode: languageCode) + let _ = (context.engine.localization.downloadAndApplyLocalization(accountManager: context.sharedContext.accountManager, languageCode: languageCode) |> deliverOnMainQueue).start(completed: { [weak controller] in controller?.dismiss() present(.dismiss, nil) @@ -883,7 +883,7 @@ func settingsSearchableItems(context: AccountContext, notificationExceptionsList } 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) + let _ = (context.engine.peers.supportPeerId() |> deliverOnMainQueue).start(next: { peerId in if let peerId = peerId { present(.push, context.sharedContext.makeChatController(context: context, chatLocation: .peer(peerId), subject: nil, botStart: nil, mode: .standard(previewing: false))) diff --git a/submodules/SettingsUI/Sources/Stickers/ArchivedStickerPacksController.swift b/submodules/SettingsUI/Sources/Stickers/ArchivedStickerPacksController.swift index 84d5b2a096..9ab2596ff6 100644 --- a/submodules/SettingsUI/Sources/Stickers/ArchivedStickerPacksController.swift +++ b/submodules/SettingsUI/Sources/Stickers/ArchivedStickerPacksController.swift @@ -268,7 +268,7 @@ public func archivedStickerPacksController(context: AccountContext, mode: Archiv namespace = .masks } let stickerPacks = Promise<[ArchivedStickerPackItem]?>() - stickerPacks.set(.single(archived) |> then(archivedStickerPacks(account: context.account, namespace: namespace) |> map(Optional.init))) + stickerPacks.set(.single(archived) |> then(context.engine.stickers.archivedStickerPacks(namespace: namespace) |> map(Optional.init))) actionsDisposable.add(stickerPacks.get().start(next: { packs in updatedPacks(packs) @@ -302,17 +302,16 @@ public func archivedStickerPacksController(context: AccountContext, mode: Archiv if !add { return } - let _ = (loadedStickerPack(postbox: context.account.postbox, network: context.account.network, reference: .id(id: info.id.id, accessHash: info.accessHash), forceActualized: false) + let _ = (context.engine.stickers.loadedStickerPack(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 addStickerPackInteractively(postbox: context.account.postbox, info: info, items: items) + return context.engine.stickers.addStickerPackInteractively(info: info, items: items) |> ignoreValues |> mapToSignal { _ -> Signal<(StickerPackCollectionInfo, [ItemCollectionItem]), NoError> in - return .complete() } |> then(.single((info, items))) } @@ -336,7 +335,7 @@ public func archivedStickerPacksController(context: AccountContext, mode: Archiv } } - 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 + 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, context: context), elevatedLayout: false, animateInAsReplacement: animateInAsReplacement, action: { _ in return true }), nil) @@ -390,7 +389,7 @@ public func archivedStickerPacksController(context: AccountContext, mode: Archiv return .complete() } - removePackDisposables.set((removeArchivedStickerPack(account: context.account, info: info) |> then(applyPacks) |> deliverOnMainQueue).start(completed: { + removePackDisposables.set((context.engine.stickers.removeArchivedStickerPack(info: info) |> then(applyPacks) |> deliverOnMainQueue).start(completed: { updateState { state in var removingPackIds = state.removingPackIds removingPackIds.remove(info.id) diff --git a/submodules/SettingsUI/Sources/Stickers/FeaturedStickerPacksController.swift b/submodules/SettingsUI/Sources/Stickers/FeaturedStickerPacksController.swift index f98650abec..b460f6c795 100644 --- a/submodules/SettingsUI/Sources/Stickers/FeaturedStickerPacksController.swift +++ b/submodules/SettingsUI/Sources/Stickers/FeaturedStickerPacksController.swift @@ -180,14 +180,14 @@ public func featuredStickerPacksController(context: AccountContext) -> ViewContr let arguments = FeaturedStickerPacksControllerArguments(account: context.account, openStickerPack: { info in presentStickerPackController?(info) }, addPack: { info in - let _ = (loadedStickerPack(postbox: context.account.postbox, network: context.account.network, reference: .id(id: info.id.id, accessHash: info.accessHash), forceActualized: false) + let _ = (context.engine.stickers.loadedStickerPack(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: context.account.postbox, info: info, items: items) + return context.engine.stickers.addStickerPackInteractively(info: info, items: items) } case .fetching: break @@ -254,7 +254,7 @@ public func featuredStickerPacksController(context: AccountContext) -> ViewContr if !unreadIds.isEmpty { alreadyReadIds.formUnion(Set(unreadIds)) - let _ = markFeaturedStickerPacksAsSeenInteractively(postbox: context.account.postbox, ids: unreadIds).start() + let _ = context.engine.stickers.markFeaturedStickerPacksAsSeenInteractively(ids: unreadIds).start() } } diff --git a/submodules/SettingsUI/Sources/Stickers/InstalledStickerPacksController.swift b/submodules/SettingsUI/Sources/Stickers/InstalledStickerPacksController.swift index 6129ce7dae..d0c6783df5 100644 --- a/submodules/SettingsUI/Sources/Stickers/InstalledStickerPacksController.swift +++ b/submodules/SettingsUI/Sources/Stickers/InstalledStickerPacksController.swift @@ -532,7 +532,7 @@ public func installedStickerPacksController(context: AccountContext, mode: Insta controller?.dismissAnimated() } let removeAction: (RemoveStickerPackOption) -> Void = { action in - let _ = (removeStickerPackInteractively(postbox: context.account.postbox, id: archivedItem.info.id, option: action) + let _ = (context.engine.stickers.removeStickerPackInteractively(id: archivedItem.info.id, option: action) |> deliverOnMainQueue).start(next: { indexAndItems in guard let (positionInList, items) = indexAndItems else { return @@ -548,9 +548,9 @@ public func installedStickerPacksController(context: AccountContext, mode: Insta } } - 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 + 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, context: context), 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() + let _ = context.engine.stickers.addStickerPackInteractively(info: archivedItem.info, items: items, positionInList: positionInList).start() } return true })) @@ -581,7 +581,7 @@ public func installedStickerPacksController(context: AccountContext, mode: Insta ]) presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) }, openStickersBot: { - resolveDisposable.set((resolvePeerByName(account: context.account, name: "stickers") |> deliverOnMainQueue).start(next: { peerId in + resolveDisposable.set((context.engine.peers.resolvePeerByName(name: "stickers") |> deliverOnMainQueue).start(next: { peerId in if let peerId = peerId { navigateToChatControllerImpl?(peerId) } @@ -655,10 +655,10 @@ public func installedStickerPacksController(context: AccountContext, mode: Insta switch mode { case .general, .modal: featured.set(context.account.viewTracker.featuredStickerPacks()) - archivedPromise.set(.single(archivedPacks) |> then(archivedStickerPacks(account: context.account) |> map(Optional.init))) + archivedPromise.set(.single(archivedPacks) |> then(context.engine.stickers.archivedStickerPacks() |> map(Optional.init))) case .masks: featured.set(.single([])) - archivedPromise.set(.single(nil) |> then(archivedStickerPacks(account: context.account, namespace: .masks) |> map(Optional.init))) + archivedPromise.set(.single(nil) |> then(context.engine.stickers.archivedStickerPacks(namespace: .masks) |> map(Optional.init))) } var previousPackCount: Int? @@ -686,10 +686,10 @@ public func installedStickerPacksController(context: AccountContext, mode: Insta var rightNavigationButton: ItemListNavigationButton? var toolbarItem: ItemListToolbarItem? if let packCount = packCount, packCount != 0 { - if case .modal = mode { - rightNavigationButton = nil - } else { - if state.editing { + if state.editing { + if case .modal = mode { + rightNavigationButton = nil + } else { rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Done), style: .bold, enabled: true, action: { updateState { $0.withUpdatedEditing(false).withUpdatedSelectedPackIds(nil) @@ -698,75 +698,97 @@ public func installedStickerPacksController(context: AccountContext, mode: Insta dismissImpl?() } }) - - let selectedCount = Int32(state.selectedPackIds?.count ?? 0) - toolbarItem = StickersToolbarItem(selectedCount: selectedCount, actions: [.init(title: presentationData.strings.StickerPacks_ActionDelete, isEnabled: selectedCount > 0, action: { - let actionSheet = ActionSheetController(presentationData: presentationData) - var items: [ActionSheetItem] = [] - items.append(ActionSheetButtonItem(title: presentationData.strings.StickerPacks_DeleteStickerPacksConfirmation(selectedCount), color: .destructive, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - + } + + let selectedCount = Int32(state.selectedPackIds?.count ?? 0) + toolbarItem = StickersToolbarItem(selectedCount: selectedCount, actions: [.init(title: presentationData.strings.StickerPacks_ActionDelete, isEnabled: selectedCount > 0, action: { + let actionSheet = ActionSheetController(presentationData: presentationData) + var items: [ActionSheetItem] = [] + items.append(ActionSheetButtonItem(title: presentationData.strings.StickerPacks_DeleteStickerPacksConfirmation(selectedCount), color: .destructive, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + + if case .modal = mode { + updateState { + $0.withUpdatedEditing(true).withUpdatedSelectedPackIds(nil) + } + } else { updateState { $0.withUpdatedEditing(false).withUpdatedSelectedPackIds(nil) } - - var packIds: [ItemCollectionId] = [] - for entry in stickerPacks { - if let selectedPackIds = state.selectedPackIds, selectedPackIds.contains(entry.id) { - packIds.append(entry.id) - } + } + + var packIds: [ItemCollectionId] = [] + for entry in stickerPacks { + if let selectedPackIds = state.selectedPackIds, selectedPackIds.contains(entry.id) { + packIds.append(entry.id) } - - let _ = removeStickerPacksInteractively(postbox: context.account.postbox, ids: packIds, option: .delete).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) - }), .init(title: presentationData.strings.StickerPacks_ActionArchive, isEnabled: selectedCount > 0, action: { - let actionSheet = ActionSheetController(presentationData: presentationData) - var items: [ActionSheetItem] = [] - items.append(ActionSheetButtonItem(title: presentationData.strings.StickerPacks_ArchiveStickerPacksConfirmation(selectedCount), color: .destructive, action: { [weak actionSheet] in + } + + let _ = context.engine.stickers.removeStickerPacksInteractively(ids: packIds, option: .delete).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) + }), .init(title: presentationData.strings.StickerPacks_ActionArchive, isEnabled: selectedCount > 0, action: { + let actionSheet = ActionSheetController(presentationData: presentationData) + var items: [ActionSheetItem] = [] + items.append(ActionSheetButtonItem(title: presentationData.strings.StickerPacks_ArchiveStickerPacksConfirmation(selectedCount), color: .destructive, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + + if case .modal = mode { + updateState { + $0.withUpdatedEditing(true).withUpdatedSelectedPackIds(nil) + } + } else { updateState { $0.withUpdatedEditing(false).withUpdatedSelectedPackIds(nil) } - - var packIds: [ItemCollectionId] = [] - for entry in stickerPacks { - if let selectedPackIds = state.selectedPackIds, selectedPackIds.contains(entry.id) { - packIds.append(entry.id) - } + } + + var packIds: [ItemCollectionId] = [] + for entry in stickerPacks { + if let selectedPackIds = state.selectedPackIds, selectedPackIds.contains(entry.id) { + packIds.append(entry.id) } - - let _ = removeStickerPacksInteractively(postbox: context.account.postbox, ids: packIds, option: .archive).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) - }), .init(title: presentationData.strings.StickerPacks_ActionShare, isEnabled: selectedCount > 0, action: { + } + + let _ = context.engine.stickers.removeStickerPacksInteractively(ids: packIds, option: .archive).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) + }), .init(title: presentationData.strings.StickerPacks_ActionShare, isEnabled: selectedCount > 0, action: { + if case .modal = mode { + updateState { + $0.withUpdatedEditing(true).withUpdatedSelectedPackIds(nil) + } + } else { updateState { $0.withUpdatedEditing(false).withUpdatedSelectedPackIds(nil) } - - var packNames: [String] = [] - for entry in stickerPacks { - if let selectedPackIds = state.selectedPackIds, selectedPackIds.contains(entry.id) { - if let info = entry.info as? StickerPackCollectionInfo { - packNames.append(info.shortName) - } + } + + var packNames: [String] = [] + for entry in stickerPacks { + if let selectedPackIds = state.selectedPackIds, selectedPackIds.contains(entry.id) { + if let info = entry.info as? StickerPackCollectionInfo { + packNames.append(info.shortName) } } - let text = packNames.map { "https://t.me/addstickers/\($0)" }.joined(separator: "\n") - let shareController = ShareController(context: context, subject: .text(text), externalShare: true) - presentControllerImpl?(shareController, nil) - })]) + } + let text = packNames.map { "https://t.me/addstickers/\($0)" }.joined(separator: "\n") + let shareController = ShareController(context: context, subject: .text(text), externalShare: true) + presentControllerImpl?(shareController, nil) + })]) + } else { + if case .modal = mode { + rightNavigationButton = nil } else { rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Edit), style: .regular, enabled: true, action: { updateState { @@ -954,13 +976,13 @@ public func installedStickerPacksController(context: AccountContext, mode: Insta } 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 + 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, context: context), 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 + 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, context: context), elevatedLayout: false, animateInAsReplacement: animateInAsReplacement, action: { action in if case .undo = action { - let _ = addStickerPackInteractively(postbox: context.account.postbox, info: info, items: items, positionInList: positionInList).start() + let _ = context.engine.stickers.addStickerPackInteractively(info: info, items: items, positionInList: positionInList).start() } return true })) diff --git a/submodules/SettingsUI/Sources/Terms of Service/TermsOfServiceController.swift b/submodules/SettingsUI/Sources/Terms of Service/TermsOfServiceController.swift index d06773453a..3b10fd385c 100644 --- a/submodules/SettingsUI/Sources/Terms of Service/TermsOfServiceController.swift +++ b/submodules/SettingsUI/Sources/Terms of Service/TermsOfServiceController.swift @@ -36,7 +36,7 @@ public class TermsOfServiceControllerTheme { 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) + self.init(statusBarStyle: presentationTheme.rootController.statusBarStyle.style, navigationBackground: presentationTheme.rootController.navigationBar.opaqueBackgroundColor, 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) } } @@ -145,7 +145,7 @@ public class TermsOfServiceController: ViewController, StandalonePresentableCont 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.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition) } override public func viewDidAppear(_ animated: Bool) { diff --git a/submodules/SettingsUI/Sources/Terms of Service/TermsOfServiceControllerNode.swift b/submodules/SettingsUI/Sources/Terms of Service/TermsOfServiceControllerNode.swift index 6871c9e3da..5a75ca945e 100644 --- a/submodules/SettingsUI/Sources/Terms of Service/TermsOfServiceControllerNode.swift +++ b/submodules/SettingsUI/Sources/Terms of Service/TermsOfServiceControllerNode.swift @@ -79,7 +79,7 @@ final class TermsOfServiceControllerNode: ViewControllerTracingNode { super.init() self.backgroundColor = self.presentationData.theme.list.blocksBackgroundColor - self.toolbarNode.backgroundColor = self.presentationData.theme.rootController.navigationBar.backgroundColor + self.toolbarNode.backgroundColor = self.presentationData.theme.rootController.navigationBar.opaqueBackgroundColor self.toolbarSeparatorNode.backgroundColor = self.presentationData.theme.rootController.navigationBar.separatorColor self.contentBackgroundNode.backgroundColor = self.presentationData.theme.list.itemBlocksBackgroundColor diff --git a/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift b/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift index e98633d1cc..ecff3147ef 100644 --- a/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift +++ b/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift @@ -13,6 +13,7 @@ import ChatListUI import WallpaperResources import LegacyComponents import ItemListUI +import WallpaperBackgroundNode private func generateMaskImage(color: UIColor) -> UIImage? { return generateImage(CGSize(width: 1.0, height: 80.0), opaque: false, rotatedContext: { size, context in @@ -74,18 +75,15 @@ private final class TextSizeSelectionControllerNode: ASDisplayNode, UIScrollView self.pageControlNode = PageControlNode(dotSpacing: 7.0, dotColor: .white, inactiveDotColor: UIColor.white.withAlphaComponent(0.4)) self.chatListBackgroundNode = ASDisplayNode() - self.chatBackgroundNode = WallpaperBackgroundNode() + self.chatBackgroundNode = WallpaperBackgroundNode(context: context) 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.chatBackgroundNode.update(wallpaper: self.presentationData.chatWallpaper) + self.chatBackgroundNode.updateBubbleTheme(bubbleTheme: self.presentationData.theme, bubbleCorners: self.presentationData.chatBubbleCorners) self.toolbarNode = TextSelectionToolbarNode(presentationThemeSettings: self.presentationThemeSettings, presentationData: self.presentationData) @@ -213,7 +211,7 @@ private final class TextSizeSelectionControllerNode: ASDisplayNode, UIScrollView private func updateChatsLayout(layout: ContainerViewLayout, topInset: CGFloat, transition: ContainedViewLayoutTransition) { var items: [ChatListItem] = [] - let interaction = ChatListNodeInteraction(activateSearch: {}, peerSelected: { _, _ in }, disabledPeerSelected: { _ in }, togglePeerSelected: { _ in }, additionalCategorySelected: { _ in + let interaction = ChatListNodeInteraction(activateSearch: {}, peerSelected: { _, _ in }, disabledPeerSelected: { _ in }, togglePeerSelected: { _ in }, togglePeersSelection: { _, _ in }, additionalCategorySelected: { _ in }, messageSelected: { _, _, _ in}, groupSelected: { _ in }, addContact: { _ in }, setPeerIdWithRevealedOptions: { _, _ in }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, deletePeer: { _, _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in}, toggleArchivedFolderHiddenByDefault: {}, hidePsa: { _ in }, activateChatPreview: { _, _, gesture in gesture?.cancel() @@ -223,14 +221,14 @@ private final class TextSizeSelectionControllerNode: ASDisplayNode, UIScrollView 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 peer1 = TelegramUser(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt32Value(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: PeerId.Id._internalFromInt32Value(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: PeerId.Id._internalFromInt32Value(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: PeerId.Id._internalFromInt32Value(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: PeerId.Id._internalFromInt32Value(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: PeerId.Id._internalFromInt32Value(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: PeerId.Id._internalFromInt32Value(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: PeerId.Id._internalFromInt32Value(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 @@ -308,7 +306,7 @@ private final class TextSizeSelectionControllerNode: ASDisplayNode, UIScrollView 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 peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt32Value(1)) let otherPeerId = self.context.account.peerId var peers = SimpleDictionary() var messages = SimpleDictionary() @@ -319,20 +317,20 @@ private final class TextSizeSelectionControllerNode: ASDisplayNode, UIScrollView messages[replyMessageId] = Message(stableId: 3, stableVersion: 0, id: replyMessageId, globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: 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, threadId: 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, messages: [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)) + items.append(self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: [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, backgroundNode: self.chatBackgroundNode)) let message2 = Message(stableId: 3, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 3), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: 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, threadMessageId: nil)], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: []) - items.append(self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: [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)) + items.append(self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: [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, backgroundNode: self.chatBackgroundNode)) 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: [], videoThumbnails: [], 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, threadId: 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, messages: [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)) + items.append(self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: [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, backgroundNode: self.chatBackgroundNode)) let message4 = Message(stableId: 2, stableVersion: 0, id: MessageId(peerId: otherPeerId, namespace: 0, id: 2), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: 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, messages: [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)) + items.append(self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: [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, backgroundNode: self.chatBackgroundNode)) let width: CGFloat if case .regular = layout.metrics.widthClass { @@ -388,7 +386,7 @@ private final class TextSizeSelectionControllerNode: ASDisplayNode, UIScrollView dateHeaderNode = currentDateHeaderNode headerItem.updateNode(dateHeaderNode, previous: nil, next: headerItem) } else { - dateHeaderNode = headerItem.node() + dateHeaderNode = headerItem.node(synchronousLoad: true) dateHeaderNode.subnodeTransform = CATransform3DMakeScale(-1.0, 1.0, 1.0) self.messagesContainerNode.addSubnode(dateHeaderNode) self.dateHeaderNode = dateHeaderNode @@ -569,7 +567,7 @@ final class TextSizeSelectionController: 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.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition) } } diff --git a/submodules/SettingsUI/Sources/Themes/CustomWallpaperPicker.swift b/submodules/SettingsUI/Sources/Themes/CustomWallpaperPicker.swift index 940dc2ce69..c706d19b1a 100644 --- a/submodules/SettingsUI/Sources/Themes/CustomWallpaperPicker.swift +++ b/submodules/SettingsUI/Sources/Themes/CustomWallpaperPicker.swift @@ -130,11 +130,11 @@ func uploadCustomWallpaper(context: AccountContext, wallpaper: WallpaperGalleryE let thumbnailImage = generateScaledImage(image: croppedImage, size: thumbnailDimensions, scale: 1.0) if let data = croppedImage.jpegData(compressionQuality: 0.8), let thumbnailImage = thumbnailImage, let thumbnailData = thumbnailImage.jpegData(compressionQuality: 0.4) { - let thumbnailResource = LocalFileMediaResource(fileId: arc4random64()) + let thumbnailResource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) context.sharedContext.accountManager.mediaBox.storeResourceData(thumbnailResource.id, data: thumbnailData) context.account.postbox.mediaBox.storeResourceData(thumbnailResource.id, data: thumbnailData) - let resource = LocalFileMediaResource(fileId: arc4random64()) + let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) context.sharedContext.accountManager.mediaBox.storeResourceData(resource.id, data: data) context.account.postbox.mediaBox.storeResourceData(resource.id, data: data) @@ -174,8 +174,8 @@ func uploadCustomWallpaper(context: AccountContext, wallpaper: WallpaperGalleryE } let apply: () -> Void = { - let settings = WallpaperSettings(blur: mode.contains(.blur), motion: mode.contains(.motion), color: nil, intensity: nil) - let wallpaper: TelegramWallpaper = .image([TelegramMediaImageRepresentation(dimensions: PixelDimensions(thumbnailDimensions), resource: thumbnailResource, progressiveSizes: []), TelegramMediaImageRepresentation(dimensions: PixelDimensions(croppedImage.size), resource: resource, progressiveSizes: [])], settings) + let settings = WallpaperSettings(blur: mode.contains(.blur), motion: mode.contains(.motion), colors: [], intensity: nil) + let wallpaper: TelegramWallpaper = .image([TelegramMediaImageRepresentation(dimensions: PixelDimensions(thumbnailDimensions), resource: thumbnailResource, progressiveSizes: [], immediateThumbnailData: nil), TelegramMediaImageRepresentation(dimensions: PixelDimensions(croppedImage.size), resource: resource, progressiveSizes: [], immediateThumbnailData: nil)], settings) updateWallpaper(wallpaper) DispatchQueue.main.async { completion() diff --git a/submodules/SettingsUI/Sources/Themes/EditThemeController.swift b/submodules/SettingsUI/Sources/Themes/EditThemeController.swift index 14713d7a87..fc6d82f5d3 100644 --- a/submodules/SettingsUI/Sources/Themes/EditThemeController.swift +++ b/submodules/SettingsUI/Sources/Themes/EditThemeController.swift @@ -359,7 +359,7 @@ 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, progressiveSizes: []), reference: .wallpaper(wallpaper: .slug(file.slug), resource: file.file.resource))) + convertedRepresentations.append(ImageRepresentationWithReference(representation: TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 100, height: 100), resource: file.file.resource, progressiveSizes: [], immediateThumbnailData: nil), 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 { @@ -443,7 +443,7 @@ public func editThemeController(context: AccountContext, mode: EditThemeControll |> 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: [], videoThumbnails: [], 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) + let message = EnqueueMessage.message(text: "", attributes: [], mediaReference: .standalone(media: file), replyToMessageId: nil, localGroupingKey: nil, correlationId: nil) let _ = enqueueMessages(account: context.account, peerId: context.account.peerId, messages: [message]).start() @@ -478,7 +478,7 @@ public func editThemeController(context: AccountContext, mode: EditThemeControll let themeData: Data? let themeThumbnailData: Data? if let theme = theme, let themeString = encodePresentationTheme(theme), let data = themeString.data(using: .utf8) { - let resource = LocalFileMediaResource(fileId: arc4random64()) + let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) context.account.postbox.mediaBox.storeResourceData(resource.id, data: data, synchronous: true) context.sharedContext.accountManager.mediaBox.storeResourceData(resource.id, data: data, synchronous: true) themeResource = resource @@ -514,7 +514,6 @@ public func editThemeController(context: AccountContext, mode: EditThemeControll 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) { @@ -525,13 +524,7 @@ public func editThemeController(context: AccountContext, mode: EditThemeControll 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() - }) + prepare = .complete() } else { prepare = .complete() } @@ -556,7 +549,7 @@ public func editThemeController(context: AccountContext, mode: EditThemeControll 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) + 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, reduceMotion: current.reduceMotion) }) |> deliverOnMainQueue).start(completed: { if !hasCustomFile { saveThemeTemplateFile(state.title, themeResource, { @@ -590,7 +583,7 @@ public func editThemeController(context: AccountContext, mode: EditThemeControll 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) + 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, reduceMotion: current.reduceMotion) }) |> deliverOnMainQueue).start(completed: { if let themeResource = themeResource, !hasCustomFile { saveThemeTemplateFile(state.title, themeResource, { diff --git a/submodules/SettingsUI/Sources/Themes/SettingsThemeWallpaperNode.swift b/submodules/SettingsUI/Sources/Themes/SettingsThemeWallpaperNode.swift index f1fb72e4cf..d4cd1100ed 100644 --- a/submodules/SettingsUI/Sources/Themes/SettingsThemeWallpaperNode.swift +++ b/submodules/SettingsUI/Sources/Themes/SettingsThemeWallpaperNode.swift @@ -10,6 +10,7 @@ import TelegramPresentationData import AccountContext import RadialStatusNode import WallpaperResources +import GradientBackground private func whiteColorImage(theme: PresentationTheme, color: UIColor) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { return .single({ arguments in @@ -29,21 +30,38 @@ private func whiteColorImage(theme: PresentationTheme, color: UIColor) -> Signal }) } +private let blackColorImage: UIImage? = { + let context = DrawingContext(size: CGSize(width: 1.0, height: 1.0), scale: 1.0, opaque: true, clear: false) + context.withContext { c in + c.setFillColor(UIColor.black.cgColor) + c.fill(CGRect(origin: CGPoint(), size: CGSize(width: 1.0, height: 1.0))) + } + return context.generateImage() +}() + final class SettingsThemeWallpaperNode: ASDisplayNode { var wallpaper: TelegramWallpaper? private var arguments: PatternWallpaperArguments? let buttonNode = HighlightTrackingButtonNode() - let backgroundNode = ASDisplayNode() + let backgroundNode = ASImageNode() let imageNode = TransformImageNode() + private var gradientNode: GradientBackgroundNode? private let statusNode: RadialStatusNode var pressed: (() -> Void)? + + private let displayLoading: Bool + private var isSelected: Bool = false + private var isLoaded: Bool = false + + private let isLoadedDisposable = MetaDisposable() - init(overlayBackgroundColor: UIColor = UIColor(white: 0.0, alpha: 0.3)) { + init(displayLoading: Bool = false, overlayBackgroundColor: UIColor = UIColor(white: 0.0, alpha: 0.3)) { + self.displayLoading = displayLoading self.imageNode.contentAnimations = [.subsequentUpdates] - self.statusNode = RadialStatusNode(backgroundNodeColor: overlayBackgroundColor) + self.statusNode = RadialStatusNode(backgroundNodeColor: UIColor(white: 0.0, alpha: 0.2), enableBlur: true) let progressDiameter: CGFloat = 50.0 self.statusNode.frame = CGRect(x: 0.0, y: 0.0, width: progressDiameter, height: progressDiameter) self.statusNode.isUserInteractionEnabled = false @@ -57,10 +75,36 @@ final class SettingsThemeWallpaperNode: ASDisplayNode { self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside) } + + deinit { + self.isLoadedDisposable.dispose() + } func setSelected(_ selected: Bool, animated: Bool = false) { - let state: RadialStatusNodeState = selected ? .check(.white) : .none - self.statusNode.transitionToState(state, animated: animated, completion: {}) + if self.isSelected != selected { + self.isSelected = selected + + self.updateStatus(animated: animated) + } + } + + private func updateIsLoaded(isLoaded: Bool, animated: Bool) { + if self.isLoaded != isLoaded { + self.isLoaded = isLoaded + self.updateStatus(animated: animated) + } + } + + private func updateStatus(animated: Bool) { + if self.isSelected { + if self.isLoaded || !displayLoading { + self.statusNode.transitionToState(.check(.white), animated: animated, completion: {}) + } else { + self.statusNode.transitionToState(.progress(color: .white, lineWidth: nil, value: nil, cancelEnabled: false, animateRotation: true), animated: animated, completion: {}) + } + } else { + self.statusNode.transitionToState(.none, animated: animated, completion: {}) + } } func setOverlayBackgroundColor(_ color: UIColor) { @@ -71,9 +115,68 @@ final class SettingsThemeWallpaperNode: ASDisplayNode { self.buttonNode.frame = CGRect(origin: CGPoint(), size: size) self.backgroundNode.frame = CGRect(origin: CGPoint(), size: size) self.imageNode.frame = CGRect(origin: CGPoint(), size: size) - - let state: RadialStatusNodeState = selected ? .check(.white) : .none - self.statusNode.transitionToState(state, animated: false, completion: {}) + + var colors: [UInt32] = [] + var intensity: CGFloat = 0.5 + if case let .gradient(_, value, _) = wallpaper { + colors = value + } else if case let .file(file) = wallpaper { + colors = file.settings.colors + intensity = CGFloat(file.settings.intensity ?? 50) / 100.0 + } else if case let .color(color) = wallpaper { + colors = [color] + } + let isBlack = UIColor.average(of: colors.map(UIColor.init(rgb:))).hsb.b <= 0.01 + if colors.count >= 3 { + if let gradientNode = self.gradientNode { + gradientNode.updateColors(colors: colors.map { UIColor(rgb: $0) }) + } else { + let gradientNode = createGradientBackgroundNode() + gradientNode.isUserInteractionEnabled = false + self.gradientNode = gradientNode + gradientNode.updateColors(colors: colors.map { UIColor(rgb: $0) }) + self.insertSubnode(gradientNode, belowSubnode: self.imageNode) + } + + if intensity < 0.0 { + self.imageNode.layer.compositingFilter = nil + } else { + if isBlack { + self.imageNode.layer.compositingFilter = nil + } else { + self.imageNode.layer.compositingFilter = "softLightBlendMode" + } + } + self.backgroundNode.image = nil + } else { + if let gradientNode = self.gradientNode { + self.gradientNode = nil + gradientNode.removeFromSupernode() + } + + if intensity < 0.0 { + self.imageNode.layer.compositingFilter = nil + } else { + if isBlack { + self.imageNode.layer.compositingFilter = nil + } else { + self.imageNode.layer.compositingFilter = "softLightBlendMode" + } + } + + if colors.count >= 2 { + self.backgroundNode.image = generateGradientImage(size: CGSize(width: 80.0, height: 80.0), colors: colors.map(UIColor.init(rgb:)), locations: [0.0, 1.0], direction: .vertical) + self.backgroundNode.backgroundColor = nil + } else if colors.count >= 1 { + self.backgroundNode.image = nil + self.backgroundNode.backgroundColor = UIColor(rgb: colors[0]) + } + } + + if let gradientNode = self.gradientNode { + gradientNode.frame = CGRect(origin: CGPoint(), size: size) + gradientNode.updateLayout(size: size, transition: .immediate) + } let progressDiameter: CGFloat = 50.0 self.statusNode.frame = CGRect(x: floorToScreenPixels((size.width - progressDiameter) / 2.0), y: floorToScreenPixels((size.height - progressDiameter) / 2.0), width: progressDiameter, height: progressDiameter) @@ -84,79 +187,101 @@ final class SettingsThemeWallpaperNode: ASDisplayNode { self.wallpaper = wallpaper switch wallpaper { case .builtin: - self.imageNode.isHidden = false - self.backgroundNode.isHidden = true - self.imageNode.setSignal(settingsBuiltinWallpaperImage(account: context.account)) - let apply = self.imageNode.asyncLayout()(TransformImageArguments(corners: corners, imageSize: CGSize(), boundingSize: size, intrinsicInsets: UIEdgeInsets())) - apply() - case let .color(color): - let theme = context.sharedContext.currentPresentationData.with { $0 }.theme - let uiColor = UIColor(rgb: color) - if uiColor.distance(to: theme.list.itemBlocksBackgroundColor) < 200 { - self.imageNode.isHidden = false - self.backgroundNode.isHidden = true - self.imageNode.setSignal(whiteColorImage(theme: theme, color: uiColor)) - let apply = self.imageNode.asyncLayout()(TransformImageArguments(corners: corners, imageSize: CGSize(), boundingSize: size, intrinsicInsets: UIEdgeInsets())) - apply() - } else { - self.imageNode.isHidden = true - self.backgroundNode.isHidden = false - 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)])) + self.imageNode.alpha = 1.0 + self.imageNode.setSignal(settingsBuiltinWallpaperImage(account: context.account, thumbnail: true)) let apply = self.imageNode.asyncLayout()(TransformImageArguments(corners: corners, imageSize: CGSize(), boundingSize: size, intrinsicInsets: UIEdgeInsets())) apply() + self.isLoadedDisposable.set(nil) + self.updateIsLoaded(isLoaded: true, animated: false) case let .image(representations, _): - self.imageNode.isHidden = false - self.backgroundNode.isHidden = true - let convertedRepresentations: [ImageRepresentationWithReference] = representations.map({ ImageRepresentationWithReference(representation: $0, reference: .wallpaper(wallpaper: nil, resource: $0.resource)) }) + self.imageNode.alpha = 1.0 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())) apply() + self.isLoadedDisposable.set(nil) + self.updateIsLoaded(isLoaded: true, animated: false) case let .file(file): - self.imageNode.isHidden = false - let convertedRepresentations : [ImageRepresentationWithReference] = file.file.previewRepresentations.map { ImageRepresentationWithReference(representation: $0, reference: .wallpaper(wallpaper: .slug(file.slug), resource: $0.resource)) } + + let fullDimensions = file.file.dimensions ?? PixelDimensions(width: 2000, height: 4000) + let convertedFullRepresentations = [ImageRepresentationWithReference(representation: .init(dimensions: fullDimensions, resource: file.file.resource, progressiveSizes: [], immediateThumbnailData: nil), reference: .wallpaper(wallpaper: .slug(file.slug), resource: file.file.resource))] let imageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError> + var placeholder: UIImage? 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 !file.settings.colors.isEmpty { if let intensity = file.settings.intensity { patternIntensity = CGFloat(intensity) / 100.0 } - patternColor = UIColor(rgb: color, alpha: patternIntensity) - patternColors.append(patternColor) - - if let bottomColor = file.settings.bottomColor { - patternColors.append(UIColor(rgb: bottomColor, alpha: patternIntensity)) + } + + if patternIntensity < 0.0 { + placeholder = blackColorImage + self.imageNode.alpha = 1.0 + self.arguments = PatternWallpaperArguments(colors: [.clear], rotation: nil, customPatternColor: UIColor(white: 0.0, alpha: 1.0 + patternIntensity)) + } else { + self.imageNode.alpha = CGFloat(file.settings.intensity ?? 50) / 100.0 + let isLight = UIColor.average(of: file.settings.colors.map(UIColor.init(rgb:))).hsb.b > 0.3 + self.arguments = PatternWallpaperArguments(colors: [.clear], rotation: nil, customPatternColor: isLight ? .black : .white) + } + imageSignal = patternWallpaperImage(account: context.account, accountManager: context.sharedContext.accountManager, representations: convertedRepresentations, mode: .thumbnail, autoFetchFullSize: true) + |> mapToSignal { value -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> in + if let value = value { + return .single(value) + } else { + return .complete() } } - - self.backgroundNode.backgroundColor = 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) + + let anyStatus = combineLatest(queue: .mainQueue(), + context.account.postbox.mediaBox.resourceStatus(convertedFullRepresentations[0].reference.resource, approximateSynchronousValue: true), + context.sharedContext.accountManager.mediaBox.resourceStatus(convertedFullRepresentations[0].reference.resource, approximateSynchronousValue: true) + ) + |> map { a, b -> Bool in + switch a { + case .Local: + return true + default: + break + } + switch b { + case .Local: + return true + default: + break + } + return false + } + |> distinctUntilChanged + + self.updateIsLoaded(isLoaded: false, animated: false) + self.isLoadedDisposable.set((anyStatus + |> deliverOnMainQueue).start(next: { [weak self] value in + guard let strongSelf = self else { + return + } + strongSelf.updateIsLoaded(isLoaded: value, animated: true) + })) } else { - self.backgroundNode.isHidden = true - - imageSignal = wallpaperImage(account: context.account, accountManager: context.sharedContext.accountManager, fileReference: .standalone(media: file.file), representations: convertedRepresentations, thumbnail: true, autoFetchFullSize: true, synchronousLoad: synchronousLoad) + self.imageNode.alpha = 1.0 + + imageSignal = wallpaperImage(account: context.account, accountManager: context.sharedContext.accountManager, fileReference: .standalone(media: file.file), representations: convertedRepresentations, thumbnail: true, autoFetchFullSize: true, blurred: file.settings.blur, synchronousLoad: synchronousLoad) + + self.updateIsLoaded(isLoaded: true, animated: false) + self.isLoadedDisposable.set(nil) } 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(), custom: self.arguments)) apply() + default: + break } } else if let wallpaper = self.wallpaper { switch wallpaper { @@ -172,6 +297,8 @@ final class SettingsThemeWallpaperNode: ASDisplayNode { apply() } } + + self.setSelected(selected, animated: false) } @objc func buttonPressed() { diff --git a/submodules/SettingsUI/Sources/Themes/ThemeAccentColorController.swift b/submodules/SettingsUI/Sources/Themes/ThemeAccentColorController.swift index 21c9950124..b193e7982a 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemeAccentColorController.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemeAccentColorController.swift @@ -69,10 +69,7 @@ final class ThemeAccentColorController: ViewController { self.mode = mode self.presentationData = context.sharedContext.currentPresentationData.with { $0 } - var section: ThemeColorSection = .accent - if case .background = mode { - section = .background - } + var section: ThemeColorSection = .background self.section = section self.segmentedTitleView = ThemeColorSegmentedTitleView(theme: self.presentationData.theme, strings: self.presentationData.strings, selectedSection: section) @@ -133,6 +130,12 @@ final class ThemeAccentColorController: ViewController { deinit { self.applyDisposable.dispose() } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + self.controllerNode.animateWallpaperAppeared() + } override func loadDisplayNode() { super.loadDisplayNode() @@ -154,20 +157,16 @@ final class ThemeAccentColorController: ViewController { }, apply: { [weak self] state, serviceBackgroundColor in if let strongSelf = self { let context = strongSelf.context - 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 } - + if !state.backgroundColors.isEmpty { 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)) + coloredWallpaper = patternWallpaper.withUpdatedSettings(WallpaperSettings(colors: state.backgroundColors, intensity: state.patternIntensity, rotation: state.rotation)) + } else if state.backgroundColors.count >= 2 { + coloredWallpaper = .gradient(nil, state.backgroundColors, WallpaperSettings(rotation: state.rotation)) } else { - coloredWallpaper = .color(color) + coloredWallpaper = .color(state.backgroundColors[0]) } } @@ -175,10 +174,9 @@ final class ThemeAccentColorController: ViewController { let apply: Signal let prepareWallpaper: Signal - if let patternWallpaper = state.patternWallpaper, case let .file(file) = patternWallpaper, let backgroundColors = state.backgroundColors { + if let patternWallpaper = state.patternWallpaper, case let .file(file) = patternWallpaper, !state.backgroundColors.isEmpty { 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 @@ -188,13 +186,7 @@ final class ThemeAccentColorController: ViewController { 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() - }) + prepareWallpaper = .complete() } else { prepareWallpaper = .complete() } @@ -220,7 +212,7 @@ final class ThemeAccentColorController: ViewController { } 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 + updatedTheme = makePresentationTheme(mediaBox: context.sharedContext.accountManager.mediaBox, themeReference: themeReference, accentColor: state.accentColor, backgroundColors: state.backgroundColors, bubbleColors: state.messagesColors, wallpaper: coloredWallpaper ?? state.initialWallpaper, serviceBackgroundColor: serviceBackgroundColor) ?? defaultPresentationTheme } else { updatedTheme = customizePresentationTheme(theme, editing: false, accentColor: state.accentColor, backgroundColors: state.backgroundColors, bubbleColors: state.messagesColors, wallpaper: state.initialWallpaper ?? coloredWallpaper) } @@ -248,7 +240,7 @@ final class ThemeAccentColorController: ViewController { baseTheme = .classic } - let wallpaper = state.initialWallpaper ?? coloredWallpaper + let wallpaper = coloredWallpaper ?? state.initialWallpaper let settings = TelegramThemeSettings(baseTheme: baseTheme, accentColor: state.accentColor, messageColors: state.messagesColors, wallpaper: wallpaper) let baseThemeReference = PresentationThemeReference.builtin(PresentationBuiltinThemeReference(baseTheme: baseTheme)) @@ -276,7 +268,7 @@ final class ThemeAccentColorController: ViewController { 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) + 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, reduceMotion: current.reduceMotion) }) |> castError(CreateThemeError.self) } else { @@ -305,7 +297,7 @@ final class ThemeAccentColorController: ViewController { 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) + 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, reduceMotion: current.reduceMotion) }) |> castError(CreateThemeError.self) } else { @@ -396,7 +388,7 @@ final class ThemeAccentColorController: ViewController { let accentColor: UIColor var initialWallpaper: TelegramWallpaper? - var backgroundColors: (UIColor, UIColor?)? + var backgroundColors: [UInt32] = [] var patternWallpaper: TelegramWallpaper? var patternIntensity: Int32 = 50 var motion = false @@ -411,27 +403,33 @@ final class ThemeAccentColorController: ViewController { 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 !file.settings.colors.isEmpty { if let intensity = file.settings.intensity { patternIntensity = intensity } - patternColor = UIColor(rgb: color) - if let bottomColorValue = file.settings.bottomColor { - bottomColor = UIColor(rgb: bottomColorValue) + patternColor = UIColor(rgb: file.settings.colors[0]) + if file.settings.colors.count >= 2 { + bottomColor = UIColor(rgb: file.settings.colors[1]) } } patternWallpaper = wallpaper - backgroundColors = (patternColor, bottomColor) + backgroundColors = file.settings.colors 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)) + backgroundColors = [color] + } else if case let .gradient(_, colors, settings) = wallpaper { + backgroundColors = colors motion = settings.motion rotation = settings.rotation ?? 0 } else { - backgroundColors = nil + if let image = chatControllerBackgroundImage(theme: nil, wallpaper: wallpaper, mediaBox: strongSelf.context.sharedContext.accountManager.mediaBox, knockoutMode: false) { + backgroundColors = [averageColor(from: image).rgb] + } else if let image = chatControllerBackgroundImage(theme: nil, wallpaper: wallpaper, mediaBox: strongSelf.context.account.postbox.mediaBox, knockoutMode: false) { + backgroundColors = [averageColor(from: image).rgb] + } else { + backgroundColors = [UIColor.gray.rgb] + } } } @@ -450,7 +448,7 @@ final class ThemeAccentColorController: ViewController { } if let defaultPatternWallpaper = defaultPatternWallpaper { - wallpaper = defaultPatternWallpaper.withUpdatedSettings(WallpaperSettings(blur: settings.blur, motion: settings.motion, color: 0xd6e2ee, bottomColor: nil, intensity: 40, rotation: nil)) + wallpaper = defaultPatternWallpaper.withUpdatedSettings(WallpaperSettings(blur: settings.blur, motion: settings.motion, colors: [0xd6e2ee], intensity: 40, rotation: nil)) } } } @@ -458,13 +456,15 @@ final class ThemeAccentColorController: ViewController { if case .colors(_, true) = strongSelf.mode { let themeSpecificAccentColor = settings.themeSpecificAccentColors[themeReference.index] accentColor = themeSpecificAccentColor?.color ?? defaultDayAccentColor - + + var referenceTheme: PresentationTheme? 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 + let theme = makePresentationTheme(mediaBox: strongSelf.context.sharedContext.accountManager.mediaBox, themeReference: themeReference, accentColor: themeSpecificAccentColor?.color, wallpaper: themeSpecificAccentColor?.wallpaper, baseColor: themeSpecificAccentColor?.baseColor) ?? defaultPresentationTheme + referenceTheme = theme wallpaper = theme.chat.defaultWallpaper } @@ -475,7 +475,7 @@ final class ThemeAccentColorController: ViewController { } if let initialBackgroundColor = strongSelf.initialBackgroundColor { - backgroundColors = (initialBackgroundColor, nil) + backgroundColors = [initialBackgroundColor.rgb] } else { extractWallpaperParameters(wallpaper) } @@ -489,6 +489,8 @@ final class ThemeAccentColorController: ViewController { } else { if let themeReference = strongSelf.mode.themeReference, themeReference == .builtin(.dayClassic), settings.themeSpecificAccentColors[themeReference.index] == nil { messageColors = (UIColor(rgb: 0xe1ffc7), nil) + } else if let referenceTheme = referenceTheme { + messageColors = (referenceTheme.chat.message.outgoing.bubble.withoutWallpaper.fill, referenceTheme.chat.message.outgoing.bubble.withoutWallpaper.gradientFill) } else { messageColors = nil } @@ -539,7 +541,7 @@ final class ThemeAccentColorController: ViewController { } if let initialBackgroundColor = strongSelf.initialBackgroundColor { - backgroundColors = (initialBackgroundColor, nil) + backgroundColors = [initialBackgroundColor.rgb] } else { extractWallpaperParameters(wallpaper) } @@ -601,11 +603,11 @@ final class ThemeAccentColorController: ViewController { } } else { accentColor = defaultDayAccentColor - backgroundColors = nil + backgroundColors = [] 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) + let initialState = ThemeColorState(section: strongSelf.section, accentColor: accentColor, initialWallpaper: initialWallpaper, backgroundColors: backgroundColors, patternWallpaper: patternWallpaper, patternIntensity: patternIntensity, defaultMessagesColor: defaultMessagesColor, messagesColors: messageColors, rotation: rotation) strongSelf.controllerNode.updateState({ _ in return initialState @@ -617,6 +619,6 @@ final class ThemeAccentColorController: ViewController { override 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.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition) } } diff --git a/submodules/SettingsUI/Sources/Themes/ThemeAccentColorControllerNode.swift b/submodules/SettingsUI/Sources/Themes/ThemeAccentColorControllerNode.swift index 8cd04d4ec1..b27204c058 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemeAccentColorControllerNode.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemeAccentColorControllerNode.swift @@ -12,6 +12,7 @@ import ChatListUI import AccountContext import WallpaperResources import PresentationDataUtils +import WallpaperBackgroundNode private func generateMaskImage(color: UIColor) -> UIImage? { return generateImage(CGSize(width: 1.0, height: 80.0), opaque: false, rotatedContext: { size, context in @@ -29,8 +30,8 @@ private func generateMaskImage(color: UIColor) -> UIImage? { } enum ThemeColorSection: Int { - case accent case background + case accent case messages } @@ -41,13 +42,12 @@ struct ThemeColorState { var accentColor: UIColor var initialWallpaper: TelegramWallpaper? - var backgroundColors: (UIColor, UIColor?)? + var backgroundColors: [UInt32] fileprivate var preview: Bool fileprivate var previousPatternWallpaper: TelegramWallpaper? var patternWallpaper: TelegramWallpaper? var patternIntensity: Int32 - var motion: Bool var defaultMessagesColor: UIColor? var messagesColors: (UIColor, UIColor?)? @@ -60,18 +60,17 @@ struct ThemeColorState { self.displayPatternPanel = false self.accentColor = .clear self.initialWallpaper = nil - self.backgroundColors = nil + self.backgroundColors = [] 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) { + init(section: ThemeColorSection, accentColor: UIColor, initialWallpaper: TelegramWallpaper?, backgroundColors: [UInt32], patternWallpaper: TelegramWallpaper?, patternIntensity: Int32, defaultMessagesColor: UIColor?, messagesColors: (UIColor, UIColor?)?, rotation: Int32 = 0) { self.section = section self.colorPanelCollapsed = false self.displayPatternPanel = false @@ -82,7 +81,6 @@ struct ThemeColorState { self.previousPatternWallpaper = nil self.patternWallpaper = patternWallpaper self.patternIntensity = patternIntensity - self.motion = motion self.defaultMessagesColor = defaultMessagesColor self.messagesColors = messagesColors self.rotation = rotation @@ -104,20 +102,10 @@ struct ThemeColorState { 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) { + if self.backgroundColors != otherState.backgroundColors { return false } + if let lhsMessagesColors = self.messagesColors, let rhsMessagesColors = otherState.messagesColors { if lhsMessagesColors.0 != rhsMessagesColors.0 { return false @@ -137,11 +125,11 @@ struct ThemeColorState { } private func calcPatternColors(for state: ThemeColorState) -> [UIColor] { - if let backgroundColors = state.backgroundColors { + if state.backgroundColors.count >= 1 { let patternIntensity = CGFloat(state.patternIntensity) / 100.0 - let topPatternColor = backgroundColors.0.withAlphaComponent(patternIntensity) - if let bottomColor = backgroundColors.1 { - let bottomPatternColor = bottomColor.withAlphaComponent(patternIntensity) + let topPatternColor = UIColor(rgb: state.backgroundColors[0]).withAlphaComponent(patternIntensity) + if state.backgroundColors.count >= 2 { + let bottomPatternColor = UIColor(rgb: state.backgroundColors[1]).withAlphaComponent(patternIntensity) return [topPatternColor, bottomPatternColor] } else { return [topPatternColor, topPatternColor] @@ -168,15 +156,21 @@ final class ThemeAccentColorControllerNode: ASDisplayNode, UIScrollViewDelegate private let scrollNode: ASScrollNode private let pageControlBackgroundNode: ASDisplayNode private let pageControlNode: PageControlNode - private var motionButtonNode: WallpaperOptionButtonNode + private var patternButtonNode: WallpaperOptionButtonNode + private var colorsButtonNode: WallpaperOptionButtonNode + + private var playButtonNode: HighlightableButtonNode + private let playButtonBackgroundNode: NavigationBackgroundNode + private let playButtonPlayImage: UIImage? + private let playButtonRotateImage: UIImage? + private let chatListBackgroundNode: ASDisplayNode private var chatNodes: [ListViewItemNode]? private let maskNode: ASImageNode private let backgroundContainerNode: ASDisplayNode private let backgroundWrapperNode: ASDisplayNode - private let immediateBackgroundNode: ASImageNode - private let signalBackgroundNode: TransformImageNode + private let backgroundNode: WallpaperBackgroundNode private let messagesContainerNode: ASDisplayNode private var dateHeaderNode: ListViewItemHeaderNode? private var messageNodes: [ListViewItemNode]? @@ -193,7 +187,7 @@ final class ThemeAccentColorControllerNode: ASDisplayNode, UIScrollViewDelegate private let serviceBackgroundColorPromise = Promise() private var wallpaperDisposable = MetaDisposable() - private var currentBackgroundColors: (UIColor, UIColor?, Int32?)? + private var currentBackgroundColors: ([UInt32], Int32?, Int32?)? private var currentBackgroundPromise = Promise<(UIColor, UIColor?)?>() private var patternWallpaper: TelegramWallpaper? @@ -243,22 +237,53 @@ final class ThemeAccentColorControllerNode: ASDisplayNode, UIScrollViewDelegate self.pageControlBackgroundNode.cornerRadius = 10.5 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.colorsButtonNode = WallpaperOptionButtonNode(title: self.presentationData.strings.WallpaperPreview_WallpaperColors, value: .colors(false, [])) + + self.playButtonBackgroundNode = NavigationBackgroundNode(color: UIColor(white: 0.0, alpha: 0.3)) + self.playButtonNode = HighlightableButtonNode() + self.playButtonNode.insertSubnode(self.playButtonBackgroundNode, at: 0) + + self.playButtonPlayImage = generateImage(CGSize(width: 48.0, height: 48.0), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(UIColor.white.cgColor) + + 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) + }) + + self.playButtonRotateImage = generateTintedImage(image: UIImage(bundleImageName: "Settings/ThemeColorRotateIcon"), color: .white) + + self.playButtonNode.setImage(self.playButtonPlayImage, for: []) self.chatListBackgroundNode = ASDisplayNode() self.backgroundContainerNode = ASDisplayNode() self.backgroundContainerNode.clipsToBounds = true self.backgroundWrapperNode = ASDisplayNode() - self.immediateBackgroundNode = ASImageNode() - self.signalBackgroundNode = TransformImageNode() - self.signalBackgroundNode.displaysAsynchronously = false + self.backgroundNode = WallpaperBackgroundNode(context: context) self.messagesContainerNode = ASDisplayNode() self.messagesContainerNode.clipsToBounds = true - self.messagesContainerNode.transform = CATransform3DMakeScale(1.0, -1.0, 1.0) + self.messagesContainerNode.transform = CATransform3DMakeScale(-1.0, -1.0, 1.0) self.colorPanelNode = WallpaperColorPanelNode(theme: self.theme, strings: self.presentationData.strings) self.patternPanelNode = WallpaperPatternPanelNode(context: self.context, theme: self.theme, strings: self.presentationData.strings) @@ -292,8 +317,9 @@ 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.colorsButtonNode) + self.addSubnode(self.playButtonNode) self.addSubnode(self.colorPanelNode) self.addSubnode(self.patternPanelNode) self.addSubnode(self.toolbarNode) @@ -303,50 +329,27 @@ final class ThemeAccentColorControllerNode: ASDisplayNode, UIScrollViewDelegate self.scrollNode.addSubnode(self.messagesContainerNode) 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.ready.set(.single(true)) - strongSelf.signalBackgroundNode.contentAnimations = [] - } - } - - self.motionButtonNode.addTarget(self, action: #selector(self.toggleMotion), forControlEvents: .touchUpInside) + self.backgroundWrapperNode.addSubnode(self.backgroundNode) + 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.colorsButtonNode.addTarget(self, action: #selector(self.toggleColors), forControlEvents: .touchUpInside) + self.playButtonNode.addTarget(self, action: #selector(self.playPressed), forControlEvents: .touchUpInside) - self.colorPanelNode.colorRemoved = { [weak self] in - if let strongSelf = self { - strongSelf.signalBackgroundNode.contentAnimations = [.subsequentUpdates] - } - } - - self.colorPanelNode.colorsChanged = { [weak self] firstColor, secondColor, ended in + self.colorPanelNode.colorsChanged = { [weak self] colors, 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 + if let firstColor = colors.first { + updated.accentColor = UIColor(rgb: firstColor) } case .background: - if let firstColor = firstColor { - updated.backgroundColors = (firstColor, secondColor) - } else { - updated.backgroundColors = nil - } + updated.backgroundColors = colors case .messages: - if let firstColor = firstColor { - updated.messagesColors = (firstColor, secondColor) + if colors.count >= 1 { + updated.messagesColors = (UIColor(rgb: colors[0]), colors.count >= 2 ? UIColor(rgb: colors[1]) : nil) } else { updated.messagesColors = nil } @@ -423,47 +426,41 @@ final class ThemeAccentColorControllerNode: ASDisplayNode, UIScrollViewDelegate } } } - + + self.backgroundNode.update(wallpaper: self.wallpaper) + self.backgroundNode.updateBubbleTheme(bubbleTheme: self.theme, bubbleCorners: self.presentationData.chatBubbleCorners) + 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 + |> map { state -> (PresentationTheme?, TelegramWallpaper, UIColor, [UInt32], Int32, 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 wallpaper: TelegramWallpaper + var updateOnlyWallpaper = false if state.section == .background && state.preview { updateOnlyWallpaper = true } - - if let backgroundColors = backgroundColors { + + if !backgroundColors.isEmpty { 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)) - + wallpaper = patternWallpaper.withUpdatedSettings(WallpaperSettings(colors: backgroundColors, 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, progressiveSizes: []), 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) + convertedRepresentations.append(ImageRepresentationWithReference(representation: .init(dimensions: dimensions, resource: file.file.resource, progressiveSizes: [], immediateThumbnailData: nil), reference: .wallpaper(wallpaper: .slug(file.slug), resource: file.file.resource))) + } else if backgroundColors.count >= 2 { + wallpaper = .gradient(nil, backgroundColors, WallpaperSettings(rotation: state.rotation)) } else { - wallpaper = .color(backgroundColors.0.argb) + wallpaper = .color(backgroundColors.first ?? 0xffffff) } } else if let themeReference = mode.themeReference, case let .builtin(theme) = themeReference, state.initialWallpaper == nil { var suggestedWallpaper: TelegramWallpaper @@ -471,25 +468,23 @@ final class ThemeAccentColorControllerNode: ASDisplayNode, UIScrollViewDelegate 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) + suggestedWallpaper = .gradient(nil, [topColor.rgb, bottomColor.rgb], WallpaperSettings()) + backgroundColors = [topColor.rgb, bottomColor.rgb] case .nightAccent: let color = accentColor.withMultiplied(hue: 1.024, saturation: 0.573, brightness: 0.18) - suggestedWallpaper = .color(color.argb) - backgroundColors = (color, nil) + suggestedWallpaper = .color(color.rgb) + backgroundColors = [color.rgb] 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 serviceBackgroundColor = serviceColor(for: (wallpaper, nil)) 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 @@ -498,29 +493,22 @@ final class ThemeAccentColorControllerNode: ASDisplayNode, UIScrollViewDelegate } else { updatedTheme = theme } - - let _ = PresentationResourcesChat.principalGraphics(mediaBox: context.account.postbox.mediaBox, knockoutWallpaper: context.sharedContext.immediateExperimentalUISettings.knockoutWallpaper, theme: updatedTheme!, wallpaper: wallpaper, bubbleCorners: bubbleCorners) + + let _ = PresentationResourcesChat.principalGraphics(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) + + return (updatedTheme, wallpaper, serviceBackgroundColor, backgroundColors, state.rotation, patternArguments, state.preview) } - |> deliverOnMainQueue).start(next: { [weak self] theme, wallpaperImageAndSignal, serviceBackgroundColor, backgroundColors, patternArguments, preview in + |> deliverOnMainQueue).start(next: { [weak self] theme, wallpaper, serviceBackgroundColor, backgroundColors, rotation, patternArguments, preview in guard let strongSelf = self else { return } - let (wallpaper, wallpaperImage, wallpaperSignal, wallpaperApply) = wallpaperImageAndSignal - - if let theme = theme { + + if let theme = theme { strongSelf.theme = theme strongSelf.themeUpdated?(theme) strongSelf.themePromise.set(.single(theme)) @@ -528,56 +516,45 @@ final class ThemeAccentColorControllerNode: ASDisplayNode, UIScrollViewDelegate strongSelf.toolbarNode.updateThemeAndStrings(theme: theme, strings: strongSelf.presentationData.strings) strongSelf.chatListBackgroundNode.backgroundColor = theme.chatList.backgroundColor strongSelf.maskNode.image = generateMaskImage(color: theme.chatList.backgroundColor) + + strongSelf.backgroundNode.updateBubbleTheme(bubbleTheme: theme, bubbleCorners: strongSelf.presentationData.chatBubbleCorners) } - + 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 .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.backgroundNode.update(wallpaper: wallpaper) + strongSelf.backgroundNode.updateBubbleTheme(bubbleTheme: strongSelf.theme, bubbleCorners: strongSelf.presentationData.chatBubbleCorners) + + strongSelf.ready.set(.single(true)) + strongSelf.wallpaper = wallpaper strongSelf.patternArguments = patternArguments - + + strongSelf.colorsButtonNode.colors = backgroundColors.map(UIColor.init(rgb:)) + if !preview { - if let backgroundColors = backgroundColors { - strongSelf.currentBackgroundColors = (backgroundColors.0, backgroundColors.1, strongSelf.state.rotation) + if !backgroundColors.isEmpty { + strongSelf.currentBackgroundColors = (backgroundColors, strongSelf.state.rotation, strongSelf.state.patternIntensity) } else { strongSelf.currentBackgroundColors = nil } strongSelf.patternPanelNode.backgroundColors = strongSelf.currentBackgroundColors + } else { + let previousIntensity = strongSelf.patternPanelNode.backgroundColors?.2 + let updatedIntensity = strongSelf.state.patternIntensity + if let previousIntensity = previousIntensity { + if (previousIntensity < 0) != (updatedIntensity < 0) { + if !backgroundColors.isEmpty { + strongSelf.currentBackgroundColors = (backgroundColors, strongSelf.state.rotation, strongSelf.state.patternIntensity) + } 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) @@ -595,7 +572,8 @@ final class ThemeAccentColorControllerNode: ASDisplayNode, UIScrollViewDelegate strongSelf.patternPanelNode.serviceBackgroundColor = color strongSelf.pageControlBackgroundNode.backgroundColor = color strongSelf.patternButtonNode.buttonColor = color - strongSelf.motionButtonNode.buttonColor = color + strongSelf.colorsButtonNode.buttonColor = color + strongSelf.playButtonBackgroundNode.updateColor(color: color, transition: .immediate) } }) } @@ -641,27 +619,26 @@ final class ThemeAccentColorControllerNode: ASDisplayNode, UIScrollViewDelegate } let colorPanelCollapsed = self.state.colorPanelCollapsed + + if colorPanelCollapsed != previousState.colorPanelCollapsed { + Queue.mainQueue().justDispatch { + self.colorPanelNode.view.endEditing(true) + } + } 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 colors: [UInt32] var defaultColor: UIColor? switch section { case .accent: - firstColor = self.state.accentColor ?? defaultDayAccentColor - secondColor = nil + colors = [self.state.accentColor.rgb] case .background: if let themeReference = self.mode.themeReference, case let .builtin(theme) = themeReference { switch theme { @@ -673,15 +650,10 @@ final class ThemeAccentColorControllerNode: ASDisplayNode, UIScrollViewDelegate 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 + if !self.state.backgroundColors.isEmpty { + colors = self.state.backgroundColors } else { - firstColor = nil - secondColor = nil + colors = [] } case .messages: if let defaultMessagesColor = self.state.defaultMessagesColor { @@ -692,16 +664,42 @@ final class ThemeAccentColorControllerNode: ASDisplayNode, UIScrollViewDelegate defaultColor = self.state.accentColor } if let messagesColors = self.state.messagesColors { - firstColor = messagesColors.0 - secondColor = messagesColors.1 + if let second = messagesColors.1 { + colors = [messagesColors.0.rgb, second.rgb] + } else { + colors = [messagesColors.0.rgb] + } } else { - firstColor = nil - secondColor = nil + colors = [] } } + if colors.isEmpty, let defaultColor = defaultColor { + colors = [defaultColor.rgb] + } + + let maximumNumberOfColors: Int + switch self.state.section { + case .accent: + maximumNumberOfColors = 1 + case .background: + maximumNumberOfColors = 4 + case .messages: + maximumNumberOfColors = 2 + default: + maximumNumberOfColors = 1 + } + 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) + return WallpaperColorPanelNodeState( + selection: colorPanelCollapsed ? nil : 0, + colors: colors, + maximumNumberOfColors: maximumNumberOfColors, + rotateAvailable: self.state.section == .background, + rotation: self.state.rotation, + preview: false, + simpleGradientGeneration: self.state.section == .messages + ) }, animated: animated) needsLayout = true @@ -714,7 +712,7 @@ final class ThemeAccentColorControllerNode: ASDisplayNode, UIScrollViewDelegate self.colorPanelNode.updateState({ current in var updated = current - updated.selection = colorPanelCollapsed ? .none : .first + updated.selection = colorPanelCollapsed ? nil : 0 return updated }, animated: animated) } @@ -745,6 +743,20 @@ final class ThemeAccentColorControllerNode: ASDisplayNode, UIScrollViewDelegate if (previousState.patternWallpaper == nil) != (self.state.patternWallpaper == nil) { needsLayout = true } + + if (previousState.backgroundColors.count >= 2) != (self.state.backgroundColors.count >= 2) { + needsLayout = true + } + + if previousState.backgroundColors.count != self.state.backgroundColors.count { + if self.state.backgroundColors.count <= 2 { + self.playButtonNode.setImage(self.playButtonRotateImage, for: []) + } else { + self.playButtonNode.setImage(self.playButtonPlayImage, for: []) + } + } + + self.colorsButtonNode.isSelected = !self.state.colorPanelCollapsed if needsLayout, let (layout, navigationBarHeight, _) = self.validLayout { self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: animated ? .animated(duration: animationDuration, curve: animationCurve) : .immediate) @@ -759,6 +771,7 @@ final class ThemeAccentColorControllerNode: ASDisplayNode, UIScrollViewDelegate } updated.section = section updated.displayPatternPanel = false + updated.colorPanelCollapsed = false return updated }, animated: true) } @@ -766,7 +779,7 @@ final class ThemeAccentColorControllerNode: ASDisplayNode, UIScrollViewDelegate private func updateChatsLayout(layout: ContainerViewLayout, topInset: CGFloat, transition: ContainedViewLayoutTransition) { var items: [ChatListItem] = [] - let interaction = ChatListNodeInteraction(activateSearch: {}, peerSelected: { _, _ in }, disabledPeerSelected: { _ in }, togglePeerSelected: { _ in }, additionalCategorySelected: { _ in + let interaction = ChatListNodeInteraction(activateSearch: {}, peerSelected: { _, _ in }, disabledPeerSelected: { _ in }, togglePeerSelected: { _ in }, togglePeersSelection: { _, _ in }, additionalCategorySelected: { _ in }, messageSelected: { _, _, _ in}, groupSelected: { _ in }, addContact: { _ in }, setPeerIdWithRevealedOptions: { _, _ in }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, deletePeer: { _, _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in}, toggleArchivedFolderHiddenByDefault: {}, hidePsa: { _ in }, activateChatPreview: { _, _, gesture in gesture?.cancel() @@ -777,11 +790,11 @@ final class ThemeAccentColorControllerNode: ASDisplayNode, UIScrollViewDelegate 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.SecretChat, 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 peer1 = TelegramUser(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt32Value(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: PeerId.Id._internalFromInt32Value(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: PeerId.Id._internalFromInt32Value(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: PeerId.Id._internalFromInt32Value(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.SecretChat, id: PeerId.Id._internalFromInt32Value(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 timestamp = self.referenceTimestamp @@ -843,7 +856,7 @@ final class ThemeAccentColorControllerNode: ASDisplayNode, UIScrollViewDelegate 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 peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt32Value(1)) let otherPeerId = self.context.account.peerId var peers = SimpleDictionary() var messages = SimpleDictionary() @@ -883,17 +896,28 @@ final class ThemeAccentColorControllerNode: ASDisplayNode, UIScrollViewDelegate items = sampleMessages.reversed().map { message in let item = self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: [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) { + guard let strongSelf = self else { + return + } + strongSelf.updateState({ state in + var state = state + if state.section == .background { + state.colorPanelCollapsed = true + state.displayPatternPanel = false + } + return state + }, animated: true) + /*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) - }) + }, backgroundNode: self.backgroundNode) return item } @@ -921,7 +945,7 @@ final class ThemeAccentColorControllerNode: ASDisplayNode, UIScrollViewDelegate itemNode = node apply().1(ListViewItemApply(isOnScreen: true)) }) - itemNode!.subnodeTransform = CATransform3DMakeScale(-1.0, 1.0, 1.0) + //itemNode!.subnodeTransform = CATransform3DMakeScale(-1.0, 1.0, 1.0) messageNodes.append(itemNode!) self.messagesContainerNode.addSubnode(itemNode!) } @@ -931,9 +955,13 @@ final class ThemeAccentColorControllerNode: ASDisplayNode, UIScrollViewDelegate var bottomOffset: CGFloat = 9.0 + bottomInset if let messageNodes = self.messageNodes { for itemNode in messageNodes { + let previousFrame = itemNode.frame 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) + if case let .animated(duration, curve) = transition { + itemNode.applyAbsoluteOffset(value: CGPoint(x: 0.0, y: -itemNode.frame.minY + previousFrame.minY), animationCurve: curve, duration: duration) + } } } @@ -942,8 +970,8 @@ final class ThemeAccentColorControllerNode: ASDisplayNode, UIScrollViewDelegate dateHeaderNode = currentDateHeaderNode headerItem.updateNode(dateHeaderNode, previous: nil, next: headerItem) } else { - dateHeaderNode = headerItem.node() - dateHeaderNode.subnodeTransform = CATransform3DMakeScale(-1.0, 1.0, 1.0) + dateHeaderNode = headerItem.node(synchronousLoad: true) + //dateHeaderNode.subnodeTransform = CATransform3DMakeScale(-1.0, 1.0, 1.0) self.messagesContainerNode.addSubnode(dateHeaderNode) self.dateHeaderNode = dateHeaderNode } @@ -983,7 +1011,7 @@ final class ThemeAccentColorControllerNode: ASDisplayNode, UIScrollViewDelegate var colorPanelOffset: CGFloat = 0.0 if self.state.colorPanelCollapsed { - colorPanelOffset = colorPanelHeight - inputFieldPanelHeight + colorPanelOffset = colorPanelHeight } 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) @@ -1004,28 +1032,26 @@ final class ThemeAccentColorControllerNode: ASDisplayNode, UIScrollViewDelegate 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)) + transition.updateBounds(node: self.messagesContainerNode, bounds: CGRect(x: 0.0, y: 0.0, width: bounds.width, height: bounds.height)) + transition.updatePosition(node: self.messagesContainerNode, position: CGRect(x: 0.0, y: 0.0, width: bounds.width, height: bounds.height).center) 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.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(), size: layout.size)) + self.backgroundNode.updateLayout(size: self.backgroundNode.bounds.size, transition: transition) 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 + var messagesBottomInset: CGFloat = bottomInset if displayOptionButtons { - messagesBottomInset = 46.0 + messagesBottomInset += 56.0 } else if chatListPreviewAvailable { - messagesBottomInset = 37.0 + messagesBottomInset += 37.0 } self.updateChatsLayout(layout: layout, topInset: navigationBarHeight, transition: transition) self.updateMessagesLayout(layout: layout, bottomInset: messagesBottomInset, transition: messagesTransition) @@ -1043,39 +1069,42 @@ final class ThemeAccentColorControllerNode: ASDisplayNode, UIScrollViewDelegate 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 colorsButtonSize = self.colorsButtonNode.measure(layout.size) + let maxButtonWidth = max(patternButtonSize.width, colorsButtonSize.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 + let patternAlpha: CGFloat = displayOptionButtons ? 1.0 : 0.0 + let colorsAlpha: CGFloat = displayOptionButtons ? 1.0 : 0.0 - 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 + let patternFrame: CGRect + let colorsFrame: CGRect + + let playButtonSize = CGSize(width: 48.0, height: 48.0) + var centerDistance: CGFloat = 40.0 + let buttonsVerticalOffset: CGFloat = 5.0 + + let playFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - playButtonSize.width) / 2.0), y: layout.size.height - bottomInset - 44.0 - buttonsVerticalOffset + floor((buttonSize.height - playButtonSize.height) / 2.0)), size: playButtonSize) + + let playAlpha: CGFloat + if self.state.backgroundColors.count >= 2 { + playAlpha = displayOptionButtons ? 1.0 : 0.0 + centerDistance += playButtonSize.width + } else { + playAlpha = 0.0 + } + + patternFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - buttonSize.width * 2.0 - centerDistance) / 2.0), y: layout.size.height - bottomInset - 44.0 - buttonsVerticalOffset), size: buttonSize) + colorsFrame = CGRect(origin: CGPoint(x: patternFrame.maxX + centerDistance, y: layout.size.height - bottomInset - 44.0 - buttonsVerticalOffset), size: buttonSize) 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) + transition.updateFrame(node: self.colorsButtonNode, frame: colorsFrame) + transition.updateAlpha(node: self.colorsButtonNode, alpha: colorsAlpha) + + transition.updateFrame(node: self.playButtonNode, frame: playFrame) + transition.updateFrame(node: self.playButtonBackgroundNode, frame: CGRect(origin: CGPoint(), size: playFrame.size)) + self.playButtonBackgroundNode.update(size: playFrame.size, cornerRadius: playFrame.size.height / 2.0, transition: transition) + transition.updateAlpha(node: self.playButtonNode, alpha: playAlpha) } @objc private func togglePattern() { @@ -1087,24 +1116,41 @@ final class ThemeAccentColorControllerNode: ASDisplayNode, UIScrollViewDelegate 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 { + if !updated.displayPatternPanel { updated.colorPanelCollapsed = false updated.displayPatternPanel = true if current.patternWallpaper == nil, let wallpaper = wallpaper { updated.patternWallpaper = wallpaper - if updated.backgroundColors == nil { + if updated.backgroundColors.isEmpty { if let backgroundColors = backgroundColors { - updated.backgroundColors = (backgroundColors.0, backgroundColors.1) + updated.backgroundColors = backgroundColors.0 } else { - updated.backgroundColors = nil + updated.backgroundColors = [] } } appeared = true } + } else { + updated.colorPanelCollapsed = true + 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.isEmpty { + if let backgroundColors = backgroundColors { + updated.backgroundColors = backgroundColors.0 + } else { + updated.backgroundColors = [] + } + } + appeared = true + } + } } return updated }, animated: true) @@ -1113,50 +1159,38 @@ final class ThemeAccentColorControllerNode: ASDisplayNode, UIScrollViewDelegate 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) + + @objc private func toggleColors() { + self.updateState({ current in + var updated = current + if updated.displayPatternPanel { + updated.displayPatternPanel = false + updated.colorPanelCollapsed = false } 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) + if updated.colorPanelCollapsed { + updated.colorPanelCollapsed = false + } else { + updated.colorPanelCollapsed = true } - } else { - self.backgroundWrapperNode.transform = CATransform3DIdentity } + updated.displayPatternPanel = false + return updated + }, animated: true) + } + + @objc private func playPressed() { + if self.state.backgroundColors.count >= 3 { + self.backgroundNode.animateEvent(transition: .animated(duration: 0.5, curve: .spring)) + } else { + self.updateState({ state in + var state = state + state.rotation = (state.rotation + 90) % 360 + return state + }, animated: true) } } + + func animateWallpaperAppeared() { + self.backgroundNode.animateEvent(transition: .animated(duration: 2.0, curve: .spring), extendAnimation: true) + } } diff --git a/submodules/SettingsUI/Sources/Themes/ThemeColorPresets.swift b/submodules/SettingsUI/Sources/Themes/ThemeColorPresets.swift index ceb0836926..98410c1f7b 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemeColorPresets.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemeColorPresets.swift @@ -2,19 +2,33 @@ import Foundation import Postbox import SyncCore import TelegramUIPreferences +import TelegramPresentationData -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: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "", size: nil, attributes: []), settings: WallpaperSettings(color: topColor, bottomColor: bottomColor, intensity: intensity ?? 50, rotation: rotation)) +private func patternWallpaper(data: BuiltinWallpaperData, colors: [UInt32], intensity: Int32?, rotation: Int32?) -> TelegramWallpaper { + return defaultBuiltinWallpaper(data: data, colors: colors, 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)) + // Pink with Blue + PresentationThemeAccentColor(index: 106, baseColor: .preset, accentColor: 0xfff55783, bubbleColors: (0xffd6f5ff, 0xffc9fdfe), wallpaper: patternWallpaper(data: .default, colors: [0x8dc0eb, 0xb9d1ea, 0xc6b1ef, 0xebd7ef], intensity: 50, rotation: nil)), + + // Pink with Gold + PresentationThemeAccentColor(index: 102, baseColor: .preset, accentColor: 0xFFFF5FA9, bubbleColors: (0xFFFFF4D7, nil), wallpaper: patternWallpaper(data: .variant12, colors: [0xeaa36e, 0xf0e486, 0xf29ebf, 0xe8c06e], intensity: 50, rotation: nil)), + + // Green + PresentationThemeAccentColor(index: 104, baseColor: .preset, accentColor: 0xFF5A9E29, bubbleColors: (0xffFFF8DF, nil), wallpaper: patternWallpaper(data: .variant13, colors: [0x7fc289, 0xe4d573, 0xafd677, 0xf0c07a], intensity: 50, rotation: nil)), + + // Purple + PresentationThemeAccentColor(index: 101, baseColor: .preset, accentColor: 0xFF7E5FE5, bubbleColors: (0xFFF5e2FF, nil), wallpaper: patternWallpaper(data: .variant14, colors: [0xe4b2ea, 0x8376c2, 0xeab9d9, 0xb493e6], intensity: 50, rotation: nil)), + + // Light Blue + PresentationThemeAccentColor(index: 107, baseColor: .preset, accentColor: 0xFF2CB9ED, bubbleColors: (0xFFADF7B5, 0xFFFCFF8B), wallpaper: patternWallpaper(data: .variant3, colors: [0x1a2e1a, 0x47623c, 0x222e24, 0x314429], intensity: 50, rotation: nil)), + + // Mint + PresentationThemeAccentColor(index: 103, baseColor: .preset, accentColor: 0xFF199972, bubbleColors: (0xFFFFFEC7, nil), wallpaper: patternWallpaper(data: .variant3, colors: [0xdceb92, 0x8fe1d6, 0x67a3f2, 0x85d685], intensity: 50, rotation: nil)), + + // Pink with Green + PresentationThemeAccentColor(index: 105, baseColor: .preset, accentColor: 0xFFDA90D9, bubbleColors: (0xFF94FFF9, 0xFFCCFFC7), wallpaper: patternWallpaper(data: .variant9, colors: [0xffc3b2, 0xe2c0ff, 0xffe7b2], intensity: 50, rotation: nil)) ] var dayColorPresets: [PresentationThemeAccentColor] = [ @@ -25,8 +39,8 @@ var dayColorPresets: [PresentationThemeAccentColor] = [ ] 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) + PresentationThemeAccentColor(index: 101, baseColor: .preset, accentColor: 0x007aff, bubbleColors: (0x007aff, 0xff53f4), wallpaper: patternWallpaper(data: .variant4, colors: [0xe4b2ea, 0x8376c2, 0xeab9d9, 0xb493e6], intensity: -35, rotation: nil)), + PresentationThemeAccentColor(index: 102, baseColor: .preset, accentColor: 0x00b09b, bubbleColors: (0xaee946, 0x00b09b), wallpaper: patternWallpaper(data: .variant9, colors: [0xe4b2ea, 0x8376c2, 0xeab9d9, 0xb493e6], intensity: -35, rotation: nil)), + PresentationThemeAccentColor(index: 103, baseColor: .preset, accentColor: 0xd33213, bubbleColors: (0xf9db00, 0xd33213), wallpaper: patternWallpaper(data: .variant2, colors: [0xfec496, 0xdd6cb9, 0x962fbf, 0x4f5bd5], intensity: -40, rotation: nil)), + PresentationThemeAccentColor(index: 104, baseColor: .preset, accentColor: 0xea8ced, bubbleColors: (0xea8ced, 0x00c2ed), wallpaper: patternWallpaper(data: .variant6, colors: [0x8adbf2, 0x888dec, 0xe39fea, 0x679ced], intensity: -30, rotation: nil)) ] diff --git a/submodules/SettingsUI/Sources/Themes/ThemeColorSegmentedTitleView.swift b/submodules/SettingsUI/Sources/Themes/ThemeColorSegmentedTitleView.swift index 37cdcf7299..a8d182994d 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemeColorSegmentedTitleView.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemeColorSegmentedTitleView.swift @@ -31,7 +31,7 @@ final class ThemeColorSegmentedTitleView: UIView { init(theme: PresentationTheme, strings: PresentationStrings, selectedSection: ThemeColorSection) { self.theme = theme - let sections = [strings.Theme_Colors_Accent, strings.Theme_Colors_Background, strings.Theme_Colors_Messages] + let sections = [strings.Theme_Colors_Background, strings.Theme_Colors_Accent, strings.Theme_Colors_Messages] self.segmentedControlNode = SegmentedControlNode(theme: SegmentedControlTheme(theme: theme), items: sections.map { SegmentedControlItem(title: $0) }, selectedIndex: selectedSection.rawValue) super.init(frame: CGRect()) diff --git a/submodules/SettingsUI/Sources/Themes/ThemeColorsGridController.swift b/submodules/SettingsUI/Sources/Themes/ThemeColorsGridController.swift index 99d25dbc70..1d7670f305 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemeColorsGridController.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemeColorsGridController.swift @@ -11,47 +11,96 @@ import TelegramPresentationData import TelegramUIPreferences import AccountContext -private func availableColors() -> [UInt32] { - return [ - 0xffffff, - 0xd4dfea, - 0xb3cde1, - 0x6ab7ea, - 0x008dd0, - 0xd3e2da, - 0xc8e6c9, - 0xc5e1a5, - 0x61b06e, - 0xcdcfaf, - 0xa7a895, - 0x7c6f72, - 0xffd7ae, - 0xffb66d, - 0xde8751, - 0xefd5e0, - 0xdba1b9, - 0xffafaf, - 0xf16a60, - 0xe8bcea, - 0x9592ed, - 0xd9bc60, - 0xb17e49, - 0xd5cef7, - 0xdf506b, - 0x8bd2cc, - 0x3c847e, - 0x22612c, - 0x244d7c, - 0x3d3b85, - 0x65717d, - 0x18222d, - 0x000000 - ] +private func availableGradients(theme: PresentationTheme) -> [[UInt32]] { + if theme.overallDarkAppearance { + return [ + [0x1e3557, 0x151a36, 0x1c4352, 0x2a4541] as [UInt32], + [0x1d223f, 0x1d1832, 0x1b2943, 0x141631] as [UInt32], + [0x203439, 0x102028, 0x1d3c3a, 0x172635] as [UInt32], + [0x1c2731, 0x1a1c25, 0x27303b, 0x1b1b21] as [UInt32], + [0x3a1c3a, 0x24193c, 0x392e3e, 0x1a1632] as [UInt32], + [0x2c211b, 0x44332a, 0x22191f, 0x3b2d36] as [UInt32], + [0x1e3557, 0x182036, 0x1c4352, 0x16263a] as [UInt32], + [0x111236, 0x14424f, 0x0b2334, 0x3b315d] as [UInt32], + [0x2d4836, 0x172b19, 0x364331, 0x103231] as [UInt32] + ] + } else { + return [ + [0xdbddbb, 0x6ba587, 0xd5d88d, 0x88b884] as [UInt32], + [0x8dc0eb, 0xb9d1ea, 0xc6b1ef, 0xebd7ef] as [UInt32], + [0x97beeb, 0xb1e9ea, 0xc6b1ef, 0xefb7dc] as [UInt32], + [0x8adbf2, 0x888dec, 0xe39fea, 0x679ced] as [UInt32], + [0xb0cdeb, 0x9fb0ea, 0xbbead5, 0xb2e3dd] as [UInt32], + [0xdaeac8, 0xa2b4ff, 0xeccbff, 0xb9e2ff] as [UInt32], + [0xdceb92, 0x8fe1d6, 0x67a3f2, 0x85d685] as [UInt32], + [0xeaa36e, 0xf0e486, 0xf29ebf, 0xe8c06e] as [UInt32], + [0xffc3b2, 0xe2c0ff, 0xffe7b2, 0xf8cece] as [UInt32] + ] + } } -private func randomColor() -> UInt32 { - let colors = availableColors() - return colors[1 ..< colors.count - 1].randomElement() ?? 0x000000 +private func availableColors(theme: PresentationTheme) -> [UInt32] { + if theme.overallDarkAppearance { + return [ + 0x1D2D3C, + 0x111B26, + 0x0B141E, + 0x1F361F, + 0x131F15, + 0x0E1710, + 0x2F2E27, + 0x2A261F, + 0x191817, + 0x432E30, + 0x2E1C1E, + 0x1F1314, + 0x432E3C, + 0x2E1C28, + 0x1F131B, + 0x3C2E43, + 0x291C2E, + 0x1D1221, + 0x312E43, + 0x1E1C2E, + 0x141221, + 0x2F3F3F, + 0x212D30, + 0x141E20, + 0x272524, + 0x191716, + 0x000000 + ] + } else { + return [ + 0xD3DFEA, + 0xA5C5DB, + 0x6F99C8, + 0xD2E3A9, + 0xA4D48E, + 0x7DBB6E, + 0xE6DDAE, + 0xD5BE91, + 0xCBA479, + 0xEBC0B9, + 0xE0A79D, + 0xC97870, + 0xEBB9C8, + 0xE09DB7, + 0xD27593, + 0xDAC2ED, + 0xD3A5E7, + 0xB587D2, + 0xC2C2ED, + 0xA5A5E7, + 0x7F7FD0, + 0xC2E2ED, + 0xA5D6E7, + 0x7FBAD0, + 0xD6C2B9, + 0x9C8882, + 0x000000 + ] + } } final class ThemeColorsGridController: ViewController { @@ -120,7 +169,7 @@ final class ThemeColorsGridController: ViewController { } override func loadDisplayNode() { - self.displayNode = ThemeColorsGridControllerNode(context: self.context, presentationData: self.presentationData, colors: availableColors(), present: { [weak self] controller, arguments in + self.displayNode = ThemeColorsGridControllerNode(context: self.context, presentationData: self.presentationData, gradients: availableGradients(theme: self.presentationData.theme), colors: availableColors(theme: self.presentationData.theme), present: { [weak self] controller, arguments in self?.present(controller, in: .window(.root), with: arguments, blockInteraction: true) }, pop: { [weak self] in if let strongSelf = self, let navigationController = strongSelf.navigationController as? NavigationController { @@ -177,6 +226,6 @@ final class ThemeColorsGridController: ViewController { override 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.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition) } } diff --git a/submodules/SettingsUI/Sources/Themes/ThemeColorsGridControllerItem.swift b/submodules/SettingsUI/Sources/Themes/ThemeColorsGridControllerItem.swift index fecf396153..b2c1cbcdb1 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemeColorsGridControllerItem.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemeColorsGridControllerItem.swift @@ -47,7 +47,7 @@ final class ThemeColorsGridControllerItemNode: GridItemNode { private var interaction: ThemeColorsGridControllerInteraction? override init() { - self.wallpaperNode = SettingsThemeWallpaperNode() + self.wallpaperNode = SettingsThemeWallpaperNode(displayLoading: false) super.init() self.addSubnode(self.wallpaperNode) diff --git a/submodules/SettingsUI/Sources/Themes/ThemeColorsGridControllerNode.swift b/submodules/SettingsUI/Sources/Themes/ThemeColorsGridControllerNode.swift index e1b471550c..f0206397e9 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: [UInt32], present: @escaping (ViewController, Any?) -> Void, pop: @escaping () -> Void, presentColorPicker: @escaping () -> Void) { + init(context: AccountContext, presentationData: PresentationData, gradients: [[UInt32]], colors: [UInt32], present: @escaping (ViewController, Any?) -> Void, pop: @escaping () -> Void, presentColorPicker: @escaping () -> Void) { self.context = context self.presentationData = presentationData self.present = present @@ -142,7 +142,9 @@ final class ThemeColorsGridControllerNode: ASDisplayNode { }) self.controllerInteraction = interaction - let wallpapers = colors.map { TelegramWallpaper.color($0) } + var wallpapers: [TelegramWallpaper] = [] + wallpapers.append(contentsOf: gradients.map { TelegramWallpaper.gradient(nil, $0, WallpaperSettings()) }) + wallpapers.append(contentsOf: colors.map { TelegramWallpaper.color($0) }) let transition = context.sharedContext.presentationData |> map { presentationData -> (ThemeColorsGridEntryTransition, Bool) in var entries: [ThemeColorsGridControllerEntry] = [] diff --git a/submodules/SettingsUI/Sources/Themes/ThemeGridController.swift b/submodules/SettingsUI/Sources/Themes/ThemeGridController.swift index 9444ef01e6..8f6be20568 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemeGridController.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemeGridController.swift @@ -149,6 +149,47 @@ final class ThemeGridController: ViewController { let controller = ThemeColorsGridController(context: strongSelf.context) (strongSelf.navigationController as? NavigationController)?.pushViewController(controller) } + + /*if let strongSelf = self { + let _ = (strongSelf.context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.presentationThemeSettings]) + |> take(1) + |> deliverOnMainQueue).start(next: { [weak self] sharedData in + guard let strongSelf = self else { + return + } + 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) + }) + }*/ }, emptyStateUpdated: { [weak self] empty in if let strongSelf = self { if empty != strongSelf.isEmpty { @@ -239,10 +280,17 @@ final class ThemeGridController: ViewController { strongSelf.present(controller, in: .window(.root)) let _ = resetWallpapers(account: strongSelf.context.account).start(completed: { [weak self, weak controller] in - let presentationData = strongSelf.presentationData let _ = updatePresentationThemeSettingsInteractively(accountManager: strongSelf.context.sharedContext.accountManager, { current in return current.withUpdatedThemeSpecificChatWallpapers([:]) }).start() + + let _ = (strongSelf.context.sharedContext.accountManager.transaction { transaction in + WallpapersState.update(transaction: transaction, { state in + var state = state + state.wallpapers.removeAll() + return state + }) + }).start() let _ = (telegramWallpapers(postbox: strongSelf.context.account.postbox, network: strongSelf.context.account.network) |> deliverOnMainQueue).start(completed: { [weak self, weak controller] in @@ -299,8 +347,8 @@ final class ThemeGridController: ViewController { case let .file(_, _, _, _, isPattern, _, slug, _, settings): var options: [String] = [] if isPattern { - if let color = settings.color { - options.append("bg_color=\(UIColor(rgb: color).hexString)") + if settings.colors.count >= 1 { + options.append("bg_color=\(UIColor(rgb: settings.colors[0]).hexString)") } if let intensity = settings.intensity { options.append("intensity=\(intensity)") @@ -339,7 +387,7 @@ final class ThemeGridController: ViewController { override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { super.containerLayoutUpdated(layout, transition: transition) - self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationInsetHeight, transition: transition) + self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.cleanNavigationHeight, transition: transition) } func activateSearch() { @@ -375,7 +423,9 @@ final class ThemeGridController: ViewController { self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Done, style: .done, target: self, action: #selector(self.donePressed)) self.searchContentNode?.setIsEnabled(false, animated: true) self.controllerNode.updateState { state in - return state.withUpdatedEditing(true) + var state = state + state.editing = true + return state } } @@ -384,7 +434,9 @@ final class ThemeGridController: ViewController { self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Edit, style: .plain, target: self, action: #selector(self.editPressed)) self.searchContentNode?.setIsEnabled(true, animated: true) self.controllerNode.updateState { state in - return state.withUpdatedEditing(false) + var state = state + state.editing = false + return state } } } diff --git a/submodules/SettingsUI/Sources/Themes/ThemeGridControllerItem.swift b/submodules/SettingsUI/Sources/Themes/ThemeGridControllerItem.swift index 5f38860949..0652c21f76 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemeGridControllerItem.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemeGridControllerItem.swift @@ -12,6 +12,7 @@ import GridMessageSelectionNode final class ThemeGridControllerItem: GridItem { let context: AccountContext let wallpaper: TelegramWallpaper + let wallpaperId: ThemeGridControllerEntry.StableId let index: Int let editable: Bool let selected: Bool @@ -19,9 +20,10 @@ final class ThemeGridControllerItem: GridItem { let section: GridSection? = nil - init(context: AccountContext, wallpaper: TelegramWallpaper, index: Int, editable: Bool, selected: Bool, interaction: ThemeGridControllerInteraction) { + init(context: AccountContext, wallpaper: TelegramWallpaper, wallpaperId: ThemeGridControllerEntry.StableId, index: Int, editable: Bool, selected: Bool, interaction: ThemeGridControllerInteraction) { self.context = context self.wallpaper = wallpaper + self.wallpaperId = wallpaperId self.index = index self.editable = editable self.selected = selected @@ -30,7 +32,7 @@ final class ThemeGridControllerItem: GridItem { func node(layout: GridNodeLayout, synchronousLoad: Bool) -> GridItemNode { let node = ThemeGridControllerItemNode() - node.setup(context: self.context, wallpaper: self.wallpaper, editable: self.editable, selected: self.selected, interaction: self.interaction, synchronousLoad: synchronousLoad) + node.setup(item: self, synchronousLoad: synchronousLoad) return node } @@ -39,7 +41,7 @@ final class ThemeGridControllerItem: GridItem { assertionFailure() return } - node.setup(context: self.context, wallpaper: self.wallpaper, editable: self.editable, selected: self.selected, interaction: self.interaction, synchronousLoad: false) + node.setup(item: self, synchronousLoad: false) } } @@ -47,11 +49,11 @@ final class ThemeGridControllerItemNode: GridItemNode { private let wallpaperNode: SettingsThemeWallpaperNode private var selectionNode: GridMessageSelectionNode? - private var currentState: (AccountContext, TelegramWallpaper, Bool, Bool, Bool)? - private var interaction: ThemeGridControllerInteraction? + private var item: ThemeGridControllerItem? override init() { - self.wallpaperNode = SettingsThemeWallpaperNode() + self.wallpaperNode = SettingsThemeWallpaperNode(displayLoading: false) + super.init() self.addSubnode(self.wallpaperNode) @@ -64,50 +66,35 @@ final class ThemeGridControllerItemNode: GridItemNode { self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) } - func setup(context: AccountContext, wallpaper: TelegramWallpaper, editable: Bool, selected: Bool, interaction: ThemeGridControllerInteraction, synchronousLoad: Bool) { - self.interaction = interaction - - if self.currentState == nil || self.currentState!.0 !== context || wallpaper != self.currentState!.1 || selected != self.currentState!.2 || synchronousLoad != self.currentState!.3 || editable != self.currentState!.4 { - self.currentState = (context, wallpaper, selected, synchronousLoad, editable) - self.updateSelectionState(animated: false) - self.setNeedsLayout() - } + func setup(item: ThemeGridControllerItem, synchronousLoad: Bool) { + self.item = item + self.updateSelectionState(animated: false) + self.setNeedsLayout() } @objc func tapGesture(_ recognizer: UITapGestureRecognizer) { if case .ended = recognizer.state { - if let (_, wallpaper, _, _, _) = self.currentState { - self.interaction?.openWallpaper(wallpaper) + if let item = self.item { + item.interaction.openWallpaper(item.wallpaper) } } } func updateSelectionState(animated: Bool) { - if let (context, wallpaper, _, _, editable) = self.currentState { - var editing = false - var id: Int64? - if case let .file(file) = wallpaper { - id = file.id - } else if case .image = wallpaper { - id = 0 - } - var selectedIndices = Set() - if let interaction = self.interaction { - let (active, indices) = interaction.selectionState - editing = active - selectedIndices = indices - } - if let id = id, editing && editable { - let selected = selectedIndices.contains(id) + if let item = self.item { + let (editing, selectedIds) = item.interaction.selectionState + + if editing && item.editable { + let selected = selectedIds.contains(item.wallpaperId) if let selectionNode = self.selectionNode { selectionNode.updateSelected(selected, animated: animated) selectionNode.frame = CGRect(origin: CGPoint(), size: self.bounds.size) } else { - let theme = context.sharedContext.currentPresentationData.with { $0 }.theme + let theme = item.context.sharedContext.currentPresentationData.with { $0 }.theme let selectionNode = GridMessageSelectionNode(theme: theme, toggle: { [weak self] value in if let strongSelf = self { - strongSelf.interaction?.toggleWallpaperSelection(id, value) + strongSelf.item?.interaction.toggleWallpaperSelection(item.wallpaperId, value) } }) @@ -139,8 +126,8 @@ final class ThemeGridControllerItemNode: GridItemNode { super.layout() let bounds = self.bounds - if let (context, wallpaper, selected, synchronousLoad, _) = self.currentState { - self.wallpaperNode.setWallpaper(context: context, wallpaper: wallpaper, selected: selected, size: bounds.size, synchronousLoad: synchronousLoad) + if let item = self.item { + self.wallpaperNode.setWallpaper(context: item.context, wallpaper: item.wallpaper, selected: item.selected, size: bounds.size, synchronousLoad: false) self.selectionNode?.frame = CGRect(origin: CGPoint(), size: bounds.size) } } diff --git a/submodules/SettingsUI/Sources/Themes/ThemeGridControllerNode.swift b/submodules/SettingsUI/Sources/Themes/ThemeGridControllerNode.swift index 75476d20eb..0e39a2108a 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemeGridControllerNode.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemeGridControllerNode.swift @@ -18,36 +18,18 @@ import SearchUI import WallpaperResources struct ThemeGridControllerNodeState: Equatable { - let editing: Bool - var selectedIndices: Set - - func withUpdatedEditing(_ editing: Bool) -> ThemeGridControllerNodeState { - return ThemeGridControllerNodeState(editing: editing, selectedIndices: editing ? self.selectedIndices : Set()) - } - - func withUpdatedSelectedIndices(_ selectedIndices: Set) -> ThemeGridControllerNodeState { - return ThemeGridControllerNodeState(editing: self.editing, selectedIndices: selectedIndices) - } - - static func ==(lhs: ThemeGridControllerNodeState, rhs: ThemeGridControllerNodeState) -> Bool { - if lhs.editing != rhs.editing { - return false - } - if lhs.selectedIndices != rhs.selectedIndices { - return false - } - return true - } + var editing: Bool + var selectedIds: Set } final class ThemeGridControllerInteraction { let openWallpaper: (TelegramWallpaper) -> Void - let toggleWallpaperSelection: (Int64, Bool) -> Void + let toggleWallpaperSelection: (ThemeGridControllerEntry.StableId, Bool) -> Void let deleteSelectedWallpapers: () -> Void let shareSelectedWallpapers: () -> Void - var selectionState: (Bool, Set) = (false, Set()) + var selectionState: (Bool, Set) = (false, Set()) - init(openWallpaper: @escaping (TelegramWallpaper) -> Void, toggleWallpaperSelection: @escaping (Int64, Bool) -> Void, deleteSelectedWallpapers: @escaping () -> Void, shareSelectedWallpapers: @escaping () -> Void) { + init(openWallpaper: @escaping (TelegramWallpaper) -> Void, toggleWallpaperSelection: @escaping (ThemeGridControllerEntry.StableId, Bool) -> Void, deleteSelectedWallpapers: @escaping () -> Void, shareSelectedWallpapers: @escaping () -> Void) { self.openWallpaper = openWallpaper self.toggleWallpaperSelection = toggleWallpaperSelection self.deleteSelectedWallpapers = deleteSelectedWallpapers @@ -55,46 +37,45 @@ final class ThemeGridControllerInteraction { } } -private struct ThemeGridControllerEntry: Comparable, Identifiable { - let index: Int - let wallpaper: TelegramWallpaper - let isEditable: Bool - let isSelected: Bool - - static func ==(lhs: ThemeGridControllerEntry, rhs: ThemeGridControllerEntry) -> Bool { - return lhs.index == rhs.index && lhs.wallpaper == rhs.wallpaper && lhs.isEditable == rhs.isEditable && lhs.isSelected == rhs.isSelected +struct ThemeGridControllerEntry: Comparable, Identifiable { + enum StableId: Hashable { + case builtin + case color(UInt32) + case gradient([UInt32]) + case file(Int64, [UInt32], Int32) + case image(String) } + + var index: Int + var wallpaper: TelegramWallpaper + var isEditable: Bool + var isSelected: Bool static func <(lhs: ThemeGridControllerEntry, rhs: ThemeGridControllerEntry) -> Bool { return lhs.index < rhs.index } - var stableId: Int64 { + var stableId: StableId { switch self.wallpaper { - case .builtin: - return 0 - case let .color(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(3) << 32) | Int64(hash) - case let .image(representations, _): - if let largest = largestImageRepresentation(representations) { - return (Int64(4) << 32) | Int64(largest.resource.id.hashValue) - } else { - return 0 - } + case .builtin: + return .builtin + case let .color(color): + return .color(color) + case let .gradient(_, colors, _): + return .gradient(colors) + case let .file(id, _, _, _, _, _, _, _, settings): + return .file(id, settings.colors, settings.intensity ?? 0) + case let .image(representations, _): + if let largest = largestImageRepresentation(representations) { + return .image(largest.resource.id.uniqueId) + } else { + return .image("") + } } } func item(context: AccountContext, interaction: ThemeGridControllerInteraction) -> ThemeGridControllerItem { - return ThemeGridControllerItem(context: context, wallpaper: self.wallpaper, index: self.index, editable: self.isEditable, selected: self.isSelected, interaction: interaction) + return ThemeGridControllerItem(context: context, wallpaper: self.wallpaper, wallpaperId: self.stableId, index: self.index, editable: self.isEditable, selected: self.isSelected, interaction: interaction) } } @@ -146,20 +127,19 @@ private func selectedWallpapers(entries: [ThemeGridControllerEntry]?, state: The } var wallpapers: [TelegramWallpaper] = [] for entry in entries { - if case let .file(file) = entry.wallpaper { - if state.selectedIndices.contains(file.id) { - wallpapers.append(entry.wallpaper) - } - } else if case .image = entry.wallpaper { - if state.selectedIndices.contains(0) { - wallpapers.append(entry.wallpaper) - } + if state.selectedIds.contains(entry.stableId) { + wallpapers.append(entry.wallpaper) } } return wallpapers } final class ThemeGridControllerNode: ASDisplayNode { + private struct Wallpaper: Equatable { + var wallpaper: TelegramWallpaper + var isLocal: Bool + } + private let context: AccountContext private var presentationData: PresentationData private var controllerInteraction: ThemeGridControllerInteraction? @@ -173,7 +153,7 @@ final class ThemeGridControllerNode: ASDisplayNode { var requestDeactivateSearch: (() -> Void)? let ready = ValuePromise() - let wallpapersPromise: Promise<[TelegramWallpaper]> + private let wallpapersPromise: Promise<[Wallpaper]> private var backgroundNode: ASDisplayNode private var separatorNode: ASDisplayNode @@ -193,7 +173,7 @@ final class ThemeGridControllerNode: ASDisplayNode { private var selectionPanel: ThemeGridSelectionPanelNode? private var selectionPanelSeparatorNode: ASDisplayNode? - private var selectionPanelBackgroundNode: ASDisplayNode? + private var selectionPanelBackgroundNode: NavigationBackgroundNode? let gridNode: GridNode private let leftOverlayNode: ASDisplayNode @@ -258,15 +238,16 @@ final class ThemeGridControllerNode: ASDisplayNode { self.resetDescriptionItemNode = ItemListTextItemNode() self.resetDescriptionItem = ItemListTextItem(presentationData: ItemListPresentationData(presentationData), text: .plain(presentationData.strings.Wallpaper_ResetWallpapersInfo), sectionId: 0) - self.currentState = ThemeGridControllerNodeState(editing: false, selectedIndices: Set()) + self.currentState = ThemeGridControllerNodeState(editing: false, selectedIds: Set()) self.statePromise = ValuePromise(self.currentState, ignoreRepeated: true) - - let wallpapersPromise = Promise<[TelegramWallpaper]>() - wallpapersPromise.set(telegramWallpapers(postbox: context.account.postbox, network: context.account.network)) + + let defaultWallpaper = presentationData.theme.chat.defaultWallpaper + + let wallpapersPromise = Promise<[Wallpaper]>() self.wallpapersPromise = wallpapersPromise - let deletedWallpaperSlugsValue = Atomic>(value: Set()) - let deletedWallpaperSlugsPromise = ValuePromise>(Set()) + let deletedWallpaperIdsValue = Atomic>(value: Set()) + let deletedWallpaperIdsPromise = ValuePromise>(Set()) super.init() @@ -308,31 +289,47 @@ final class ThemeGridControllerNode: ASDisplayNode { } }, toggleWallpaperSelection: { [weak self] id, value in if let strongSelf = self { - strongSelf.updateState { current in - var updated = current.selectedIndices + strongSelf.updateState { state in + var state = state if value { - updated.insert(id) + state.selectedIds.insert(id) } else { - updated.remove(id) + state.selectedIds.remove(id) } - return current.withUpdatedSelectedIndices(updated) + return state } } }, deleteSelectedWallpapers: { [weak self] in let entries = previousEntries.with { $0 } if let strongSelf = self, let entries = entries { - deleteWallpapers(selectedWallpapers(entries: entries, state: strongSelf.currentState), { [weak self] in + let wallpapers = selectedWallpapers(entries: entries, state: strongSelf.currentState) + + deleteWallpapers(wallpapers, { [weak self] in if let strongSelf = self { - var updatedDeletedSlugs = deletedWallpaperSlugsValue.with { $0 } + var updatedDeletedIds = deletedWallpaperIdsValue.with { $0 } for entry in entries { - if case let .file(file) = entry.wallpaper, strongSelf.currentState.selectedIndices.contains(file.id) { - updatedDeletedSlugs.insert(file.slug) + if strongSelf.currentState.selectedIds.contains(entry.stableId) { + updatedDeletedIds.insert(entry.stableId) } } - let _ = deletedWallpaperSlugsValue.swap(updatedDeletedSlugs) - deletedWallpaperSlugsPromise.set(updatedDeletedSlugs) + let _ = deletedWallpaperIdsValue.swap(updatedDeletedIds) + deletedWallpaperIdsPromise.set(updatedDeletedIds) + + let _ = (strongSelf.context.sharedContext.accountManager.transaction { transaction in + WallpapersState.update(transaction: transaction, { state in + var state = state + for wallpaper in wallpapers { + if let index = state.wallpapers.firstIndex(where: { + $0.isBasicallyEqual(to: wallpaper) + }) { + state.wallpapers.remove(at: index) + } + } + return state + }) + }).start() } }) } @@ -344,8 +341,8 @@ final class ThemeGridControllerNode: ASDisplayNode { }) self.controllerInteraction = interaction - let transition = combineLatest(wallpapersPromise.get(), deletedWallpaperSlugsPromise.get(), context.sharedContext.presentationData) - |> map { wallpapers, deletedWallpaperSlugs, presentationData -> (ThemeGridEntryTransition, Bool) in + let transition = combineLatest(self.wallpapersPromise.get(), deletedWallpaperIdsPromise.get(), context.sharedContext.presentationData) + |> map { wallpapers, deletedWallpaperIds, presentationData -> (ThemeGridEntryTransition, Bool) in var entries: [ThemeGridControllerEntry] = [] var index = 1 @@ -355,35 +352,40 @@ final class ThemeGridControllerNode: ASDisplayNode { } 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) + entries.insert(ThemeGridControllerEntry(index: 0, wallpaper: presentationData.chatWallpaper, isEditable: false, isSelected: true), at: 0) var defaultWallpaper: TelegramWallpaper? if !presentationData.chatWallpaper.isBasicallyEqual(to: presentationData.theme.chat.defaultWallpaper) { - if case .builtin = presentationData.theme.chat.defaultWallpaper { - } else { + let entry = ThemeGridControllerEntry(index: 1, wallpaper: presentationData.theme.chat.defaultWallpaper, isEditable: false, isSelected: false) + if !entries.contains(where: { $0.stableId == entry.stableId }) { defaultWallpaper = presentationData.theme.chat.defaultWallpaper - entries.insert(ThemeGridControllerEntry(index: 1, wallpaper: presentationData.theme.chat.defaultWallpaper, isEditable: false, isSelected: false), at: 1) + entries.insert(entry, at: index) index += 1 } } var sortedWallpapers: [TelegramWallpaper] = [] if presentationData.theme.overallDarkAppearance { + var localWallpapers: [TelegramWallpaper] = [] var darkWallpapers: [TelegramWallpaper] = [] for wallpaper in wallpapers { - if case let .file(file) = wallpaper, file.isDark { - darkWallpapers.append(wallpaper) + if wallpaper.isLocal { + localWallpapers.append(wallpaper.wallpaper) } else { - sortedWallpapers.append(wallpaper) + if case let .file(file) = wallpaper.wallpaper, file.isDark { + darkWallpapers.append(wallpaper.wallpaper) + } else { + sortedWallpapers.append(wallpaper.wallpaper) + } } } - sortedWallpapers = darkWallpapers + sortedWallpapers + sortedWallpapers = localWallpapers + darkWallpapers + sortedWallpapers } else { - sortedWallpapers = wallpapers + sortedWallpapers = wallpapers.map(\.wallpaper) } for wallpaper in sortedWallpapers { - if case let .file(file) = wallpaper, deletedWallpaperSlugs.contains(file.slug) || (wallpaper.isPattern && file.settings.color == nil) { + if case let .file(file) = wallpaper, (wallpaper.isPattern && file.settings.colors.isEmpty) { continue } let selected = presentationData.chatWallpaper.isBasicallyEqual(to: wallpaper) @@ -395,11 +397,24 @@ final class ThemeGridControllerNode: ASDisplayNode { if case .builtin = wallpaper { isEditable = false } - if !selected && !isDefault { - entries.append(ThemeGridControllerEntry(index: index, wallpaper: wallpaper, isEditable: isEditable, isSelected: false)) + if isDefault || presentationData.chatWallpaper.isBasicallyEqual(to: wallpaper) { + isEditable = false + } + if !selected && !isDefault { + let entry = ThemeGridControllerEntry(index: index, wallpaper: wallpaper, isEditable: isEditable, isSelected: false) + if deletedWallpaperIds.contains(entry.stableId) { + continue + } + if !entries.contains(where: { $0.stableId == entry.stableId }) { + entries.append(entry) + index += 1 + } } - index += 1 } + + /*if !entries.isEmpty { + entries = [entries[0]] + }*/ let previous = previousEntries.swap(entries) return (preparedThemeGridEntryTransition(context: context, from: previous ?? [], to: entries, interaction: interaction), previous == nil) @@ -409,6 +424,8 @@ final class ThemeGridControllerNode: ASDisplayNode { strongSelf.enqueueTransition(transition) } }) + + self.updateWallpapers() } deinit { @@ -491,7 +508,31 @@ final class ThemeGridControllerNode: ASDisplayNode { } func updateWallpapers() { - self.wallpapersPromise.set(telegramWallpapers(postbox: self.context.account.postbox, network: self.context.account.network)) + self.wallpapersPromise.set(combineLatest(queue: .mainQueue(), + telegramWallpapers(postbox: self.context.account.postbox, network: self.context.account.network), + self.context.sharedContext.accountManager.sharedData(keys: [SharedDataKeys.wallapersState]) + ) + |> map { remoteWallpapers, sharedData -> [Wallpaper] in + let localState = (sharedData.entries[SharedDataKeys.wallapersState] as? WallpapersState) ?? WallpapersState.default + + var wallpapers: [Wallpaper] = [] + for wallpaper in localState.wallpapers { + if !wallpapers.contains(where: { + $0.wallpaper.isBasicallyEqual(to: wallpaper) + }) { + wallpapers.append(Wallpaper(wallpaper: wallpaper, isLocal: true)) + } + } + for wallpaper in remoteWallpapers { + if !wallpapers.contains(where: { + $0.wallpaper.isBasicallyEqual(to: wallpaper) + }) { + wallpapers.append(Wallpaper(wallpaper: wallpaper, isLocal: false)) + } + } + + return wallpapers + }) } func updatePresentationData(_ presentationData: PresentationData) { @@ -531,7 +572,7 @@ final class ThemeGridControllerNode: ASDisplayNode { self.statePromise.set(state) } - let selectionState = (self.currentState.editing, self.currentState.selectedIndices) + let selectionState = (self.currentState.editing, self.currentState.selectedIds) if let interaction = self.controllerInteraction, interaction.selectionState != selectionState { let requestLayout = interaction.selectionState.0 != self.currentState.editing self.controllerInteraction?.selectionState = selectionState @@ -545,7 +586,7 @@ final class ThemeGridControllerNode: ASDisplayNode { if requestLayout, let (containerLayout, navigationBarHeight) = self.validLayout { self.containerLayoutUpdated(containerLayout, navigationBarHeight: navigationBarHeight, transition: .animated(duration: 0.4, curve: .spring)) } - self.selectionPanel?.selectedIndices = selectionState.1 + self.selectionPanel?.selectedIds = selectionState.1 } } @@ -647,7 +688,7 @@ final class ThemeGridControllerNode: ASDisplayNode { if self.currentState.editing { let panelHeight: CGFloat if let selectionPanel = self.selectionPanel { - selectionPanel.selectedIndices = self.currentState.selectedIndices + selectionPanel.selectedIds = self.currentState.selectedIds panelHeight = selectionPanel.updateLayout(width: layout.size.width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, maxHeight: 0.0, transition: transition, 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 { @@ -655,24 +696,21 @@ final class ThemeGridControllerNode: ASDisplayNode { } if let selectionPanelBackgroundNode = self.selectionPanelBackgroundNode { transition.updateFrame(node: selectionPanelBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - insets.bottom - panelHeight), size: CGSize(width: layout.size.width, height: insets.bottom + panelHeight))) + selectionPanelBackgroundNode.update(size: selectionPanelBackgroundNode.bounds.size, transition: transition) } } else { - let selectionPanelBackgroundNode = ASDisplayNode() - selectionPanelBackgroundNode.isLayerBacked = true - selectionPanelBackgroundNode.backgroundColor = self.presentationData.theme.chat.inputPanel.panelBackgroundColor + let selectionPanelBackgroundNode = NavigationBackgroundNode(color: self.presentationData.theme.rootController.navigationBar.blurredBackgroundColor) self.addSubnode(selectionPanelBackgroundNode) self.selectionPanelBackgroundNode = selectionPanelBackgroundNode let selectionPanel = ThemeGridSelectionPanelNode(theme: self.presentationData.theme) - selectionPanel.backgroundColor = self.presentationData.theme.chat.inputPanel.panelBackgroundColor selectionPanel.controllerInteraction = self.controllerInteraction - selectionPanel.selectedIndices = self.currentState.selectedIndices + selectionPanel.selectedIds = self.currentState.selectedIds panelHeight = selectionPanel.updateLayout(width: layout.size.width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, maxHeight: 0.0, transition: .immediate, metrics: layout.metrics) self.selectionPanel = selectionPanel self.addSubnode(selectionPanel) let selectionPanelSeparatorNode = ASDisplayNode() - selectionPanelSeparatorNode.isLayerBacked = true selectionPanelSeparatorNode.backgroundColor = self.presentationData.theme.chat.inputPanel.panelSeparatorColor self.addSubnode(selectionPanelSeparatorNode) self.selectionPanelSeparatorNode = selectionPanelSeparatorNode @@ -682,6 +720,7 @@ final class ThemeGridControllerNode: ASDisplayNode { selectionPanelSeparatorNode.frame = CGRect(origin: CGPoint(x: 0.0, y: layout.size.height), size: CGSize(width: layout.size.width, height: UIScreenPixel)) 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))) transition.updateFrame(node: selectionPanelBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - insets.bottom - panelHeight), size: CGSize(width: layout.size.width, height: insets.bottom + panelHeight))) + selectionPanelBackgroundNode.update(size: selectionPanelBackgroundNode.bounds.size, transition: .immediate) 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))) } @@ -701,6 +740,7 @@ final class ThemeGridControllerNode: ASDisplayNode { transition.updateFrame(node: selectionPanelBackgroundNode, frame: selectionPanelBackgroundNode.frame.offsetBy(dx: 0.0, dy: selectionPanel.bounds.size.height + insets.bottom), completion: { [weak selectionPanelSeparatorNode] _ in selectionPanelSeparatorNode?.removeFromSupernode() }) + selectionPanelBackgroundNode.update(size: selectionPanelBackgroundNode.bounds.size, transition: transition) } } diff --git a/submodules/SettingsUI/Sources/Themes/ThemeGridSearchColorsItem.swift b/submodules/SettingsUI/Sources/Themes/ThemeGridSearchColorsItem.swift index c0dcf33d56..4f2c87afe8 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemeGridSearchColorsItem.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemeGridSearchColorsItem.swift @@ -93,7 +93,11 @@ final class ThemeGridSearchColorsNode: ASDisplayNode { return CGSize(width: constrainedSize.width, height: 100.0) } + private var validLayout: (CGSize, CGFloat, CGFloat)? func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat) { + let hadLayout = self.validLayout != nil + self.validLayout = (size, leftInset, rightInset) + self.sectionHeaderNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: 29.0)) self.sectionHeaderNode.updateLayout(size: CGSize(width: size.width, height: 29.0), leftInset: leftInset, rightInset: rightInset) @@ -103,6 +107,9 @@ final class ThemeGridSearchColorsNode: ASDisplayNode { self.scrollNode.frame = CGRect(x: 0.0, y: 29.0, width: size.width, height: size.height - 29.0) self.scrollNode.view.contentInset = insets + if !hadLayout { + self.scrollNode.view.contentOffset = CGPoint(x: -leftInset, y: 0.0) + } var offset: CGFloat = inset if let subnodes = self.scrollNode.subnodes { @@ -242,9 +249,9 @@ class ThemeGridSearchColorsItemNode: ListViewItemNode { self.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration * 0.5, removeOnCompletion: false) } - override public func header() -> ListViewItemHeader? { + override public func headers() -> [ListViewItemHeader]? { if let item = self.item { - return item.header + return item.header.flatMap { [$0] } } else { return nil } diff --git a/submodules/SettingsUI/Sources/Themes/ThemeGridSearchContentNode.swift b/submodules/SettingsUI/Sources/Themes/ThemeGridSearchContentNode.swift index 39f33e0555..0862763d18 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemeGridSearchContentNode.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemeGridSearchContentNode.swift @@ -497,7 +497,7 @@ final class ThemeGridSearchContentNode: SearchDisplayControllerContentNode { guard let name = configuration.imageBotUsername else { return .single(nil) } - return resolvePeerByName(account: context.account, name: name) + return context.engine.peers.resolvePeerByName(name: name) |> mapToSignal { peerId -> Signal in if let peerId = peerId { return context.account.postbox.loadedPeerWithId(peerId) @@ -520,7 +520,7 @@ final class ThemeGridSearchContentNode: SearchDisplayControllerContentNode { let geoPoint = collection.geoPoint.flatMap { geoPoint -> (Double, Double) in return (geoPoint.latitude, geoPoint.longitude) } - return requestChatContextResults(account: self.context.account, botId: collection.botId, peerId: collection.peerId, query: searchContext.result.query, location: .single(geoPoint), offset: nextOffset) + return self.context.engine.messages.requestChatContextResults(botId: collection.botId, peerId: collection.peerId, query: searchContext.result.query, location: .single(geoPoint), offset: nextOffset) |> map { results -> ChatContextResultCollection? in return results?.results } @@ -572,7 +572,7 @@ final class ThemeGridSearchContentNode: SearchDisplayControllerContentNode { return (.complete() |> delay(0.1, queue: Queue.concurrentDefaultQueue())) |> then( - requestContextResults(account: context.account, botId: user.id, query: wallpaperQuery, peerId: context.account.peerId, limit: 16) + requestContextResults(context: context, botId: user.id, query: wallpaperQuery, peerId: context.account.peerId, limit: 16) |> map { results -> ChatContextResultCollection? in return results?.results } @@ -654,7 +654,7 @@ final class ThemeGridSearchContentNode: SearchDisplayControllerContentNode { } }) - self.recentListNode.beganInteractiveDragging = { [weak self] in + self.recentListNode.beganInteractiveDragging = { [weak self] _ in self?.dismissInput?() } @@ -848,7 +848,7 @@ final class ThemeGridSearchContentNode: SearchDisplayControllerContentNode { } private func clearRecentSearch() { - let _ = (clearRecentlySearchedPeers(postbox: self.context.account.postbox) |> deliverOnMainQueue).start() + let _ = (self.context.engine.peers.clearRecentlySearchedPeers() |> deliverOnMainQueue).start() } override func scrollToTop() { diff --git a/submodules/SettingsUI/Sources/Themes/ThemeGridSearchItem.swift b/submodules/SettingsUI/Sources/Themes/ThemeGridSearchItem.swift index 04d8256dce..4d4383553f 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemeGridSearchItem.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemeGridSearchItem.swift @@ -109,10 +109,10 @@ final class ThemeGridSearchItemNode: GridItemNode { var representations: [TelegramMediaImageRepresentation] = [] if let thumbnailResource = thumbnailResource, let thumbnailDimensions = thumbnailDimensions { - representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(thumbnailDimensions), resource: thumbnailResource, progressiveSizes: [])) + representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(thumbnailDimensions), resource: thumbnailResource, progressiveSizes: [], immediateThumbnailData: nil)) } if let imageResource = imageResource, let imageDimensions = imageDimensions { - representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(imageDimensions), resource: imageResource, progressiveSizes: [])) + representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(imageDimensions), resource: imageResource, progressiveSizes: [], immediateThumbnailData: nil)) } if !representations.isEmpty { let tmpImage = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: representations, immediateThumbnailData: immediateThumbnailData, reference: nil, partialReference: nil, flags: []) diff --git a/submodules/SettingsUI/Sources/Themes/ThemeGridSelectionPanelNode.swift b/submodules/SettingsUI/Sources/Themes/ThemeGridSelectionPanelNode.swift index 78a1c64556..0d7500fbf6 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemeGridSelectionPanelNode.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemeGridSelectionPanelNode.swift @@ -17,11 +17,11 @@ final class ThemeGridSelectionPanelNode: ASDisplayNode { private var theme: PresentationTheme - var selectedIndices = Set() { + var selectedIds = Set() { didSet { - if oldValue != self.selectedIndices { - self.deleteButton.isEnabled = !self.selectedIndices.isEmpty - self.shareButton.isEnabled = !self.selectedIndices.isEmpty + if oldValue != self.selectedIds { + self.deleteButton.isEnabled = !self.selectedIds.isEmpty + self.shareButton.isEnabled = !self.selectedIds.isEmpty } } } diff --git a/submodules/SettingsUI/Sources/Themes/ThemePreviewController.swift b/submodules/SettingsUI/Sources/Themes/ThemePreviewController.swift index 923279b761..dd72374307 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemePreviewController.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemePreviewController.swift @@ -245,7 +245,7 @@ public final class ThemePreviewController: ViewController { } case .media: if let strings = encodePresentationTheme(previewTheme), let data = strings.data(using: .utf8) { - let resource = LocalFileMediaResource(fileId: arc4random64()) + let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) context.account.postbox.mediaBox.storeResourceData(resource.id, data: data) context.sharedContext.accountManager.mediaBox.storeResourceData(resource.id, data: data) theme = .single(.local(PresentationLocalTheme(title: previewTheme.name.string, resource: resource, resolvedWallpaper: nil))) @@ -479,7 +479,7 @@ public final class ThemePreviewController: ViewController { self.validLayout = layout - self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationHeight, transition: transition) + self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition) } @objc private func actionPressed() { diff --git a/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift b/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift index 13125e369d..82d7f5f0a0 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift @@ -12,6 +12,7 @@ import AccountContext import ChatListUI import WallpaperResources import LegacyComponents +import WallpaperBackgroundNode private func generateMaskImage(color: UIColor) -> UIImage? { return generateImage(CGSize(width: 1.0, height: 80.0), opaque: false, rotatedContext: { size, context in @@ -55,6 +56,7 @@ final class ThemePreviewControllerNode: ASDisplayNode, UIScrollViewDelegate { private let instantChatBackgroundNode: WallpaperBackgroundNode private let remoteChatBackgroundNode: TransformImageNode private let blurredNode: BlurredImageNode + private let wallpaperNode: WallpaperBackgroundNode private var dateHeaderNode: ListViewItemHeaderNode? private var messageNodes: [ListViewItemNode]? @@ -68,11 +70,15 @@ final class ThemePreviewControllerNode: ASDisplayNode, UIScrollViewDelegate { private var fetchDisposable = MetaDisposable() private var dismissed = false + + private var wallpaper: TelegramWallpaper 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.wallpaper = initialWallpaper ?? previewTheme.chat.defaultWallpaper self.ready = ready @@ -102,18 +108,12 @@ final class ThemePreviewControllerNode: ASDisplayNode, UIScrollViewDelegate { self.messagesContainerNode.clipsToBounds = true self.messagesContainerNode.transform = CATransform3DMakeScale(1.0, -1.0, 1.0) - self.instantChatBackgroundNode = WallpaperBackgroundNode() + self.instantChatBackgroundNode = WallpaperBackgroundNode(context: context) self.instantChatBackgroundNode.displaysAsynchronously = false - - 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.ready.set(.single(true)) + self.instantChatBackgroundNode.update(wallpaper: wallpaper) + self.instantChatBackgroundNode.view.contentMode = .scaleAspectFill self.remoteChatBackgroundNode = TransformImageNode() @@ -121,6 +121,8 @@ final class ThemePreviewControllerNode: ASDisplayNode, UIScrollViewDelegate { self.blurredNode = BlurredImageNode() self.blurredNode.blurView.contentMode = .scaleAspectFill + + self.wallpaperNode = WallpaperBackgroundNode(context: context) self.toolbarNode = WallpaperGalleryToolbarNode(theme: self.previewTheme, strings: self.presentationData.strings, doneButtonType: .set) @@ -147,7 +149,7 @@ final class ThemePreviewControllerNode: ASDisplayNode, UIScrollViewDelegate { 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 { + if case let .color(value) = self.wallpaper { self.instantChatBackgroundNode.backgroundColor = UIColor(rgb: value) } @@ -182,13 +184,25 @@ final class ThemePreviewControllerNode: ASDisplayNode, UIScrollViewDelegate { } } } - - if case let .file(file) = self.previewTheme.chat.defaultWallpaper { + + var gradientColors: [UInt32] = [] + if case let .file(file) = self.wallpaper { + gradientColors = file.settings.colors + if file.settings.blur { self.chatContainerNode.insertSubnode(self.blurredNode, belowSubnode: self.messagesContainerNode) } + } else if case let .gradient(_, colors, _) = self.wallpaper { + gradientColors = colors } - + + if gradientColors.count >= 3 { + self.chatContainerNode.insertSubnode(self.wallpaperNode, belowSubnode: self.messagesContainerNode) + } + + self.wallpaperNode.update(wallpaper: self.wallpaper) + self.wallpaperNode.updateBubbleTheme(bubbleTheme: self.previewTheme, bubbleCorners: self.presentationData.chatBubbleCorners) + self.remoteChatBackgroundNode.imageUpdated = { [weak self] image in if let strongSelf = self, strongSelf.blurredNode.supernode != nil { var image = image @@ -231,20 +245,12 @@ final class ThemePreviewControllerNode: ASDisplayNode, UIScrollViewDelegate { 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, progressiveSizes: []), reference: .wallpaper(wallpaper: .slug(file.slug), resource: file.file.resource))) + convertedRepresentations.append(ImageRepresentationWithReference(representation: .init(dimensions: dimensions, resource: file.file.resource, progressiveSizes: [], immediateThumbnailData: nil), reference: .wallpaper(wallpaper: .slug(file.slug), resource: file.file.resource))) let signal: Signal<(TransformImageArguments) -> DrawingContext?, NoError> let fileReference = FileMediaReference.standalone(media: file.file) if wallpaper.isPattern { - signal = patternWallpaperImage(account: context.account, accountManager: context.sharedContext.accountManager, representations: convertedRepresentations, mode: .screen, autoFetchFullSize: false) - } 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) { - } else if let path = context.account.postbox.mediaBox.completedResourcePath(file.file.resource), let data = try? Data(contentsOf: URL(fileURLWithPath: path)) { - context.sharedContext.accountManager.mediaBox.storeResourceData(file.file.resource.id, data: data) - } - } + signal = .complete() } else { signal = .complete() } @@ -271,14 +277,14 @@ final class ThemePreviewControllerNode: ASDisplayNode, UIScrollViewDelegate { }) var patternArguments: PatternWallpaperArguments? - if let color = file.settings.color { + if !file.settings.colors.isEmpty { var patternIntensity: CGFloat = 0.5 if let intensity = file.settings.intensity { patternIntensity = CGFloat(intensity) / 100.0 } - var patternColors = [UIColor(rgb: color, alpha: patternIntensity)] - if let bottomColor = file.settings.bottomColor { - patternColors.append(UIColor(rgb: bottomColor, alpha: patternIntensity)) + var patternColors = [UIColor(rgb: file.settings.colors[0], alpha: patternIntensity)] + if file.settings.colors.count >= 2 { + patternColors.append(UIColor(rgb: file.settings.colors[1], alpha: patternIntensity)) } patternArguments = PatternWallpaperArguments(colors: patternColors, rotation: file.settings.rotation) } @@ -313,7 +319,7 @@ final class ThemePreviewControllerNode: ASDisplayNode, UIScrollViewDelegate { 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 { + if case let .color(value) = self.wallpaper { self.instantChatBackgroundNode.backgroundColor = UIColor(rgb: value) } @@ -350,7 +356,7 @@ final class ThemePreviewControllerNode: ASDisplayNode, UIScrollViewDelegate { private func updateChatsLayout(layout: ContainerViewLayout, topInset: CGFloat, transition: ContainedViewLayoutTransition) { var items: [ChatListItem] = [] - let interaction = ChatListNodeInteraction(activateSearch: {}, peerSelected: { _, _ in }, disabledPeerSelected: { _ in }, togglePeerSelected: { _ in }, additionalCategorySelected: { _ in + let interaction = ChatListNodeInteraction(activateSearch: {}, peerSelected: { _, _ in }, disabledPeerSelected: { _ in }, togglePeerSelected: { _ in }, togglePeersSelection: { _, _ in }, additionalCategorySelected: { _ in }, messageSelected: { _, _, _ in}, groupSelected: { _ in }, addContact: { _ in }, setPeerIdWithRevealedOptions: { _, _ in }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, deletePeer: { _, _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in}, toggleArchivedFolderHiddenByDefault: {}, hidePsa: { _ in }, activateChatPreview: { _, _, gesture in gesture?.cancel() @@ -361,14 +367,14 @@ final class ThemePreviewControllerNode: ASDisplayNode, UIScrollViewDelegate { 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 peer1 = TelegramUser(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt32Value(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: PeerId.Id._internalFromInt32Value(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: PeerId.Id._internalFromInt32Value(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: PeerId.Id._internalFromInt32Value(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: PeerId.Id._internalFromInt32Value(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: PeerId.Id._internalFromInt32Value(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: PeerId.Id._internalFromInt32Value(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: PeerId.Id._internalFromInt32Value(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 @@ -448,7 +454,7 @@ final class ThemePreviewControllerNode: ASDisplayNode, UIScrollViewDelegate { 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 peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt32Value(1)) let otherPeerId = self.context.account.peerId var peers = SimpleDictionary() var messages = SimpleDictionary() @@ -487,7 +493,7 @@ final class ThemePreviewControllerNode: ASDisplayNode, UIScrollViewDelegate { sampleMessages.append(message8) items = sampleMessages.reversed().map { message in - self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: [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) + self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: [message], theme: self.previewTheme, 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: nil, clickThroughMessage: nil, backgroundNode: self.wallpaperNode) } let width: CGFloat @@ -544,7 +550,7 @@ final class ThemePreviewControllerNode: ASDisplayNode, UIScrollViewDelegate { dateHeaderNode = currentDateHeaderNode headerItem.updateNode(dateHeaderNode, previous: nil, next: headerItem) } else { - dateHeaderNode = headerItem.node() + dateHeaderNode = headerItem.node(synchronousLoad: true) dateHeaderNode.subnodeTransform = CATransform3DMakeScale(-1.0, 1.0, 1.0) self.messagesContainerNode.addSubnode(dateHeaderNode) self.dateHeaderNode = dateHeaderNode @@ -594,6 +600,8 @@ final class ThemePreviewControllerNode: ASDisplayNode, UIScrollViewDelegate { self.instantChatBackgroundNode.updateLayout(size: self.instantChatBackgroundNode.bounds.size, transition: .immediate) self.remoteChatBackgroundNode.frame = self.chatContainerNode.bounds self.blurredNode.frame = self.chatContainerNode.bounds + self.wallpaperNode.frame = self.chatContainerNode.bounds + self.wallpaperNode.updateLayout(size: self.wallpaperNode.bounds.size, transition: .immediate) 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) diff --git a/submodules/SettingsUI/Sources/Themes/ThemeSettingsAppIconItem.swift b/submodules/SettingsUI/Sources/Themes/ThemeSettingsAppIconItem.swift index 1f5fd063ac..d88914ce70 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemeSettingsAppIconItem.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemeSettingsAppIconItem.swift @@ -319,6 +319,10 @@ class ThemeSettingsAppIconItemNode: ListViewItemNode, ItemListItemNode { bordered = false case "WhiteFilled": name = "⍺ White" + case "New1": + name = item.strings.Appearance_AppIconNew1 + case "New2": + name = item.strings.Appearance_AppIconNew2 default: break } diff --git a/submodules/SettingsUI/Sources/Themes/ThemeSettingsChatPreviewItem.swift b/submodules/SettingsUI/Sources/Themes/ThemeSettingsChatPreviewItem.swift index 2a24f6b579..5a07d05a5e 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemeSettingsChatPreviewItem.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemeSettingsChatPreviewItem.swift @@ -11,6 +11,7 @@ import TelegramUIPreferences import ItemListUI import PresentationDataUtils import AccountContext +import WallpaperBackgroundNode struct ChatPreviewMessageItem: Equatable { static func == (lhs: ChatPreviewMessageItem, rhs: ChatPreviewMessageItem) -> Bool { @@ -95,7 +96,7 @@ class ThemeSettingsChatPreviewItem: ListViewItem, ItemListItem { } class ThemeSettingsChatPreviewItemNode: ListViewItemNode { - private let backgroundNode: ASImageNode + private var backgroundNode: WallpaperBackgroundNode? private let topStripeNode: ASDisplayNode private let bottomStripeNode: ASDisplayNode private let maskNode: ASImageNode @@ -109,12 +110,6 @@ class ThemeSettingsChatPreviewItemNode: ListViewItemNode { private let disposable = MetaDisposable() init() { - self.backgroundNode = ASImageNode() - self.backgroundNode.isLayerBacked = true - self.backgroundNode.displaysAsynchronously = false - self.backgroundNode.displayWithoutProcessing = true - self.backgroundNode.contentMode = .scaleAspectFill - self.topStripeNode = ASDisplayNode() self.topStripeNode.isLayerBacked = true @@ -138,21 +133,22 @@ class ThemeSettingsChatPreviewItemNode: ListViewItemNode { } func asyncLayout() -> (_ item: ThemeSettingsChatPreviewItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { - let currentItem = self.item - let currentNodes = self.messageNodes + + var currentBackgroundNode = self.backgroundNode return { item, params, neighbors in - var updatedBackgroundSignal: Signal<(UIImage?, Bool)?, NoError>? - if currentItem?.wallpaper != item.wallpaper { - updatedBackgroundSignal = chatControllerBackgroundImageSignal(wallpaper: item.wallpaper, mediaBox: item.context.sharedContext.accountManager.mediaBox, accountMediaBox: item.context.account.postbox.mediaBox) + if currentBackgroundNode == nil { + currentBackgroundNode = WallpaperBackgroundNode(context: item.context) } + currentBackgroundNode?.update(wallpaper: item.wallpaper) + currentBackgroundNode?.updateBubbleTheme(bubbleTheme: item.theme, bubbleCorners: item.chatBubbleCorners) let insets: UIEdgeInsets let separatorHeight = UIScreenPixel - let peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: 1) - let otherPeerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: 2) + let peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt32Value(1)) + let otherPeerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt32Value(2)) var items: [ListViewItem] = [] for messageItem in item.messageItems.reversed() { var peers = SimpleDictionary() @@ -165,7 +161,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, threadId: 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, threadMessageId: nil)] : [], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: []) - items.append(item.context.sharedContext.makeChatMessagePreviewItem(context: item.context, messages: [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)) + items.append(item.context.sharedContext.makeChatMessagePreviewItem(context: item.context, messages: [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, backgroundNode: currentBackgroundNode)) } var nodes: [ListViewItemNode] = [] @@ -224,32 +220,34 @@ class ThemeSettingsChatPreviewItemNode: ListViewItemNode { node.updateFrame(CGRect(origin: CGPoint(x: 0.0, y: topOffset), size: node.frame.size), within: layoutSize) topOffset += node.frame.size.height } + + if let currentBackgroundNode = currentBackgroundNode, strongSelf.backgroundNode !== currentBackgroundNode { + strongSelf.backgroundNode = currentBackgroundNode + strongSelf.insertSubnode(currentBackgroundNode, at: 0) + } - if let updatedBackgroundSignal = updatedBackgroundSignal { + /*if let updatedBackgroundSignal = updatedBackgroundSignal { strongSelf.disposable.set((updatedBackgroundSignal |> deliverOnMainQueue).start(next: { [weak self] image in - if let strongSelf = self, let (image, final) = image { + if let strongSelf = self, let (image, final) = image, let backgroundNode = strongSelf.backgroundNode { if final && !strongSelf.finalImage { let tempLayer = CALayer() - tempLayer.frame = strongSelf.backgroundNode.bounds - tempLayer.contentsGravity = strongSelf.backgroundNode.layer.contentsGravity + tempLayer.frame = backgroundNode.bounds + tempLayer.contentsGravity = 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 + backgroundNode.image = image strongSelf.finalImage = final } })) - } + }*/ 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) - } + if strongSelf.topStripeNode.supernode == nil { strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1) } @@ -286,7 +284,12 @@ class ThemeSettingsChatPreviewItemNode: ListViewItemNode { strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil 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) + if let backgroundNode = strongSelf.backgroundNode { + backgroundNode.frame = backgroundFrame.insetBy(dx: 0.0, dy: -100.0) + backgroundNode.update(wallpaper: item.wallpaper) + backgroundNode.updateBubbleTheme(bubbleTheme: item.theme, bubbleCorners: item.chatBubbleCorners) + backgroundNode.updateLayout(size: backgroundNode.bounds.size, transition: .immediate) + } 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 c1bb641a8e..7f7047442f 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemeSettingsController.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemeSettingsController.swift @@ -374,6 +374,19 @@ private enum ThemeSettingsControllerEntry: ItemListNodeEntry { } } colorItems.append(contentsOf: colors.map { .color($0) }) + if let index = colorItems.firstIndex(where: { item in + if case .default = item { + return true + } else { + return false + } + }) { + if index > 0 { + let item = colorItems[index] + colorItems.remove(at: index) + colorItems.insert(item, at: 1) + } + } return ThemeSettingsAccentColorItem(theme: theme, sectionId: self.section, generalThemeReference: generalThemeReference, themeReference: currentTheme, colors: colorItems, currentColor: currentColor, updated: { color in if let color = color { @@ -522,7 +535,7 @@ private func themeSettingsControllerEntries(presentationData: PresentationData, entries.append(.otherHeader(presentationData.theme, strings.Appearance_Other.uppercased())) entries.append(.largeEmoji(presentationData.theme, strings.Appearance_LargeEmoji, presentationData.largeEmoji)) - entries.append(.animations(presentationData.theme, strings.Appearance_ReduceMotion, presentationData.disableAnimations)) + entries.append(.animations(presentationData.theme, strings.Appearance_ReduceMotion, presentationData.reduceMotion)) entries.append(.animationsInfo(presentationData.theme, strings.Appearance_ReduceMotionInfo)) return entries @@ -536,6 +549,10 @@ private final class ThemeSettingsControllerImpl: ItemListController, ThemeSettin } public func themeSettingsController(context: AccountContext, focusOnItemTag: ThemeSettingsEntryTag? = nil) -> ViewController { + #if DEBUG + BuiltinWallpaperData.generate(account: context.account) + #endif + var pushControllerImpl: ((ViewController) -> Void)? var presentControllerImpl: ((ViewController, Any?) -> Void)? var updateControllersImpl: ((([UIViewController]) -> [UIViewController]) -> Void)? @@ -598,9 +615,9 @@ public func themeSettingsController(context: AccountContext, focusOnItemTag: The let _ = updatePresentationThemeSettingsInteractively(accountManager: context.sharedContext.accountManager, { current in return current.withUpdatedLargeEmoji(largeEmoji) }).start() - }, disableAnimations: { disableAnimations in + }, disableAnimations: { reduceMotion in let _ = updatePresentationThemeSettingsInteractively(accountManager: context.sharedContext.accountManager, { current in - return current.withUpdatedDisableAnimations(disableAnimations) + return current.withUpdatedReduceMotion(reduceMotion) }).start() }, selectAppIcon: { name in currentAppIconName.set(name) @@ -831,7 +848,14 @@ public func themeSettingsController(context: AccountContext, focusOnItemTag: The 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) + var baseColor: PresentationThemeBaseColor? + switch accentColor { + case let .accentColor(value): + baseColor = value.baseColor + default: + break + } + theme = makePresentationTheme(mediaBox: context.sharedContext.accountManager.mediaBox, themeReference: generalThemeReference, accentColor: accentColor?.accentColor, bubbleColors: accentColor?.customBubbleColors, wallpaper: accentColor?.wallpaper, baseColor: baseColor) } effectiveWallpaper = theme?.chat.defaultWallpaper ?? .builtin(WallpaperSettings()) } @@ -1016,17 +1040,10 @@ public func themeSettingsController(context: AccountContext, focusOnItemTag: The presentInGlobalOverlayImpl?(contextController, nil) }) }) - - 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 dateTimeFormat = presentationData.dateTimeFormat - let largeEmoji = presentationData.largeEmoji - let disableAnimations = presentationData.disableAnimations let themeReference: PresentationThemeReference if presentationData.autoNightModeTriggered { @@ -1035,18 +1052,21 @@ public func themeSettingsController(context: AccountContext, focusOnItemTag: The themeReference = settings.theme } - let accentColor = settings.themeSpecificAccentColors[themeReference.index] - let rightNavigationButton = ItemListNavigationButton(content: .icon(.add), style: .regular, enabled: true, action: { moreImpl?() }) var defaultThemes: [PresentationThemeReference] = [] if presentationData.autoNightModeTriggered { + defaultThemes.append(contentsOf: [.builtin(.nightAccent), .builtin(.night)]) } else { - defaultThemes.append(contentsOf: [.builtin(.dayClassic), .builtin(.day)]) + defaultThemes.append(contentsOf: [ + .builtin(.dayClassic), + .builtin(.nightAccent), + .builtin(.day), + .builtin(.night) + ]) } - defaultThemes.append(contentsOf: [.builtin(.night), .builtin(.nightAccent)]) let cloudThemes: [PresentationThemeReference] = cloudThemes.map { .cloud(PresentationCloudTheme(theme: $0, resolvedWallpaper: nil, creatorAccountId: $0.isCreator ? context.account.id : nil)) }.filter { !removedThemeIndexes.contains($0.index) } @@ -1236,21 +1256,9 @@ public func themeSettingsController(context: AccountContext, focusOnItemTag: The |> 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 { @@ -1285,7 +1293,7 @@ public func themeSettingsController(context: AccountContext, focusOnItemTag: The updatedTheme = generalThemeReference } - guard let theme = makePresentationTheme(mediaBox: context.sharedContext.accountManager.mediaBox, themeReference: generalThemeReference, accentColor: accentColor?.color, wallpaper: presetWallpaper) else { + guard let theme = makePresentationTheme(mediaBox: context.sharedContext.accountManager.mediaBox, themeReference: generalThemeReference, accentColor: accentColor?.color, wallpaper: presetWallpaper, baseColor: accentColor?.baseColor) else { return current } @@ -1304,7 +1312,7 @@ public func themeSettingsController(context: AccountContext, focusOnItemTag: The } } - 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) + 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, reduceMotion: current.reduceMotion) }).start() presentCrossfadeControllerImpl?(true) diff --git a/submodules/SettingsUI/Sources/Themes/ThemeSettingsThemeItem.swift b/submodules/SettingsUI/Sources/Themes/ThemeSettingsThemeItem.swift index 67f48a9bc6..db8b0f5436 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemeSettingsThemeItem.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemeSettingsThemeItem.swift @@ -249,11 +249,15 @@ private final class ThemeSettingsThemeItemIconNode : ListViewItemNode { var updatedThemeReference = false var updatedAccentColor = false var updatedTheme = false + var updatedWallpaper = false var updatedSelected = false if currentItem?.themeReference != item.themeReference { updatedThemeReference = true } + if currentItem?.wallpaper != item.wallpaper { + updatedWallpaper = true + } if currentItem == nil || currentItem?.accentColor != item.accentColor { updatedAccentColor = true } @@ -278,7 +282,7 @@ private final class ThemeSettingsThemeItemIconNode : ListViewItemNode { } strongSelf.containerNode.isGestureEnabled = false } else { - if updatedThemeReference || updatedAccentColor { + if updatedThemeReference || updatedAccentColor || updatedWallpaper { 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 @@ -545,8 +549,6 @@ class ThemeSettingsThemeItemNode: ListViewItemNode, ItemListItemNode { return (layout, { [weak self] in if let strongSelf = self { - let isFirstLayout = currentItem == nil - strongSelf.item = item strongSelf.layoutParams = params @@ -614,27 +616,37 @@ class ThemeSettingsThemeItemNode: ListViewItemNode, ItemListItemNode { 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 + if case let .cloud(theme) = theme { + if !item.displayUnsupported && theme.theme.file == nil { + continue + } } let title = themeDisplayName(strings: item.strings, reference: theme) var accentColor = item.themeSpecificAccentColors[theme.generalThemeReference.index] - if let customThemeIndex = accentColor?.themeIndex { + /*if let customThemeIndex = accentColor?.themeIndex { if let customTheme = themes[customThemeIndex] { theme = customTheme } accentColor = nil + }*/ + + var themeWallpaper: TelegramWallpaper? + if case let .cloud(theme) = theme { + themeWallpaper = theme.resolvedWallpaper ?? theme.theme.settings?.wallpaper } + + let customWallpaper = item.themeSpecificChatWallpapers[theme.generalThemeReference.index] - let wallpaper = accentColor?.wallpaper + let wallpaper = accentColor?.wallpaper ?? customWallpaper ?? themeWallpaper + entries.append(ThemeSettingsThemeEntry(index: index, themeReference: theme, title: title, accentColor: accentColor, selected: item.currentTheme.index == theme.index, theme: item.theme, wallpaper: wallpaper)) index += 1 } - let action: (PresentationThemeReference) -> Void = { [weak self, weak item] themeReference in + let action: (PresentationThemeReference) -> Void = { [weak self] themeReference in if let strongSelf = self { strongSelf.item?.updatedTheme(themeReference) - ensureThemeVisible(listNode: strongSelf.listNode, themeReference: themeReference, animated: true) + let _ = ensureThemeVisible(listNode: strongSelf.listNode, themeReference: themeReference, animated: true) } } let previousEntries = strongSelf.entries ?? [] diff --git a/submodules/SettingsUI/Sources/Themes/WallpaperColorPanelNode.swift b/submodules/SettingsUI/Sources/Themes/WallpaperColorPanelNode.swift index 3bb307b4ab..7d1f6b29c5 100644 --- a/submodules/SettingsUI/Sources/Themes/WallpaperColorPanelNode.swift +++ b/submodules/SettingsUI/Sources/Themes/WallpaperColorPanelNode.swift @@ -33,13 +33,6 @@ private func textInputBackgroundImage(fieldColor: UIColor, strokeColor: UIColor, 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 { @@ -61,7 +54,7 @@ private class ColorInputFieldNode: ASDisplayNode, UITextFieldDelegate { var colorSelected: (() -> Void)? private var color: UIColor? - + private var isDefault = false { didSet { self.updateSelectionVisibility() @@ -73,25 +66,19 @@ private class ColorInputFieldNode: ASDisplayNode, UITextFieldDelegate { 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 + + private let displaySwatch: Bool - init(theme: PresentationTheme) { + init(theme: PresentationTheme, displaySwatch: Bool = true) { self.theme = theme + + self.displaySwatch = displaySwatch self.textBackgroundNode = ASImageNode() self.textBackgroundNode.image = textInputBackgroundImage(fieldColor: theme.chat.inputPanel.inputBackgroundColor, strokeColor: theme.chat.inputPanel.inputStrokeColor, diameter: 33.0) @@ -148,7 +135,7 @@ private class ColorInputFieldNode: ASDisplayNode, UITextFieldDelegate { 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)) + let gestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.tapped(_:))) self.view.addGestureRecognizer(gestureRecognizer) self.gestureRecognizer = gestureRecognizer } @@ -201,12 +188,12 @@ private class ColorInputFieldNode: ASDisplayNode, UITextFieldDelegate { } self.colorRemoved?() - self.removeButton.layer.removeAnimation(forKey: "opacity") - self.removeButton.alpha = 1.0 } - @objc private func tapped() { - self.colorSelected?() + @objc private func tapped(_ recognizer: UITapGestureRecognizer) { + if case .ended = recognizer.state { + self.colorSelected?() + } } @objc internal func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { @@ -249,18 +236,13 @@ private class ColorInputFieldNode: ASDisplayNode, UITextFieldDelegate { } 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 - } + self.skipEndEditing = false + self.previousColor = self.color + self.previousIsDefault = self.isDefault + + textField.textColor = self.theme.chat.inputPanel.inputTextColor + + return true } @objc func textFieldDidEndEditing(_ textField: UITextField) { @@ -294,7 +276,7 @@ private class ColorInputFieldNode: ASDisplayNode, UITextFieldDelegate { } private func updateSelectionVisibility() { - self.selectionNode.isHidden = !self.isSelected || self.isDefault + self.selectionNode.isHidden = true } func updateLayout(size: CGSize, condensed: Bool, transition: ContainedViewLayoutTransition) { @@ -303,8 +285,15 @@ private class ColorInputFieldNode: ASDisplayNode, UITextFieldDelegate { 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) + + self.swatchNode.isHidden = !self.displaySwatch - let textPadding: CGFloat = condensed ? 31.0 : 37.0 + let textPadding: CGFloat + if self.displaySwatch { + textPadding = condensed ? 31.0 : 37.0 + } else { + textPadding = 12.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)) @@ -317,45 +306,91 @@ private class ColorInputFieldNode: ASDisplayNode, UITextFieldDelegate { 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) + 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 +struct WallpaperColorPanelNodeState: Equatable { + var selection: Int? + var colors: [UInt32] + var maximumNumberOfColors: Int var rotateAvailable: Bool var rotation: Int32 var preview: Bool var simpleGradientGeneration: Bool } +private final class ColorSampleItemNode: ASImageNode { + private struct State: Equatable { + var color: UInt32 + var size: CGSize + var isSelected: Bool + } + + private var action: () -> Void + private var validState: State? + + init(action: @escaping () -> Void) { + self.action = action + + super.init() + + self.isUserInteractionEnabled = true + self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) + } + + @objc private func tapGesture(_ recognizer: UITapGestureRecognizer) { + if case .ended = recognizer.state { + self.action() + } + } + + func update(size: CGSize, color: UIColor, isSelected: Bool) { + let state = State(color: color.rgb, size: size, isSelected: isSelected) + if self.validState != state { + self.validState = state + + self.image = generateImage(CGSize(width: size.width, height: size.height), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(color.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(), size: size)) + + context.setBlendMode(.softLight) + context.setStrokeColor(UIColor(white: 0.0, alpha: 0.3).cgColor) + context.setLineWidth(UIScreenPixel) + context.strokeEllipse(in: CGRect(origin: CGPoint(), size: size).insetBy(dx: UIScreenPixel, dy: UIScreenPixel)) + + if isSelected { + context.setBlendMode(.copy) + context.setStrokeColor(UIColor.clear.cgColor) + let lineWidth: CGFloat = 2.0 + context.setLineWidth(lineWidth) + let inset: CGFloat = 2.0 + lineWidth / 2.0 + context.strokeEllipse(in: CGRect(origin: CGPoint(), size: size).insetBy(dx: inset, dy: inset)) + } + }) + } + } +} + final class WallpaperColorPanelNode: ASDisplayNode { private var theme: PresentationTheme private var state: WallpaperColorPanelNodeState - private let backgroundNode: ASDisplayNode + private let backgroundNode: NavigationBackgroundNode 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)? + private var sampleItemNodes: [ColorSampleItemNode] = [] + private let multiColorFieldNode: ColorInputFieldNode + + var colorsChanged: (([UInt32], Bool) -> Void)? var colorSelected: (() -> Void)? var rotate: (() -> Void)? @@ -367,8 +402,7 @@ final class WallpaperColorPanelNode: ASDisplayNode { init(theme: PresentationTheme, strings: PresentationStrings) { self.theme = theme - self.backgroundNode = ASDisplayNode() - self.backgroundNode.backgroundColor = theme.chat.inputPanel.panelBackgroundColor + self.backgroundNode = NavigationBackgroundNode(color: theme.chat.inputPanel.panelBackgroundColor) self.topSeparatorNode = ASDisplayNode() self.topSeparatorNode.backgroundColor = theme.chat.inputPanel.panelSeparatorColor @@ -386,19 +420,25 @@ final class WallpaperColorPanelNode: ASDisplayNode { 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.multiColorFieldNode = ColorInputFieldNode(theme: theme, displaySwatch: false) - 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) + self.state = WallpaperColorPanelNodeState( + selection: 0, + colors: [], + maximumNumberOfColors: 1, + 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.multiColorFieldNode) self.addSubnode(self.doneButton) self.addSubnode(self.colorPickerNode) @@ -409,79 +449,34 @@ final class WallpaperColorPanelNode: ASDisplayNode { 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 + + self.multiColorFieldNode.colorChanged = { [weak self] color, ended in if let strongSelf = self { strongSelf.updateState({ current in var updated = current - updated.firstColor = color + updated.preview = !ended + if let index = strongSelf.state.selection { + updated.colors[index] = color.rgb + } return updated }) } } - self.firstColorFieldNode.colorRemoved = { [weak self] in + self.multiColorFieldNode.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 + if let index = strongSelf.state.selection { + updated.colors.remove(at: index) + if updated.colors.isEmpty { + updated.selection = nil + } else { + updated.selection = max(0, min(index - 1, updated.colors.count - 1)) + } } 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?() + }, animated: strongSelf.state.colors.count >= 2) } } @@ -490,13 +485,8 @@ final class WallpaperColorPanelNode: ASDisplayNode { 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 + if let index = strongSelf.state.selection { + updated.colors[index] = color.rgb } return updated }, updateLayout: false) @@ -507,13 +497,8 @@ final class WallpaperColorPanelNode: ASDisplayNode { 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 + if let index = strongSelf.state.selection { + updated.colors[index] = color.rgb } return updated }, updateLayout: false) @@ -523,125 +508,49 @@ final class WallpaperColorPanelNode: ASDisplayNode { func updateTheme(_ theme: PresentationTheme) { self.theme = theme - self.backgroundNode.backgroundColor = self.theme.chat.inputPanel.panelBackgroundColor + self.backgroundNode.updateColor(color: self.theme.chat.inputPanel.panelBackgroundColor, transition: .immediate) self.topSeparatorNode.backgroundColor = self.theme.chat.inputPanel.panelSeparatorColor self.bottomSeparatorNode.backgroundColor = self.theme.chat.inputPanel.panelSeparatorColor - self.firstColorFieldNode.updateTheme(theme) - self.secondColorFieldNode.updateTheme(theme) + self.multiColorFieldNode.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 previousColors = self.state.colors let previousPreview = self.state.preview - let previousRotation = self.state.rotation self.state = f(self.state) - 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 { + let colorWasRemovable = self.multiColorFieldNode.isRemovable + self.multiColorFieldNode.isRemovable = self.state.colors.count > 1 + if colorWasRemovable != self.multiColorFieldNode.isRemovable { updateLayout = true } + + if let index = self.state.selection { + if self.state.colors.count > index { + self.colorPickerNode.color = UIColor(rgb: self.state.colors[index]) + } + } 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) - } - } - - 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() + + if let index = state.selection { + if self.state.colors.count > index { + self.multiColorFieldNode.setColor(UIColor(rgb: self.state.colors[index]), update: false) } } - - 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() + + for i in 0 ..< state.colors.count { + if i < self.sampleItemNodes.count { + self.sampleItemNodes[i].update(size: self.sampleItemNodes[i].bounds.size, color: UIColor(rgb: state.colors[i]), isSelected: state.selection == i) } } - - 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 + + if self.state.colors != previousColors || self.state.preview != previousPreview { + self.colorsChanged?(self.state.colors, !self.state.preview) + } } func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) { @@ -651,6 +560,7 @@ final class WallpaperColorPanelNode: ASDisplayNode { 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)) + self.backgroundNode.update(size: self.backgroundNode.bounds.size, transition: transition) transition.updateFrame(node: self.topSeparatorNode, frame: CGRect(x: 0.0, y: 0.0, width: size.width, height: separatorHeight)) transition.updateFrame(node: self.bottomSeparatorNode, frame: CGRect(x: 0.0, y: topPanelHeight, width: size.width, height: separatorHeight)) @@ -671,13 +581,18 @@ final class WallpaperColorPanelNode: ASDisplayNode { 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 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) - transition.updateFrame(node: self.rotateButton, frame: middleButtonFrame) - transition.updateFrame(node: self.swapButton, frame: middleButtonFrame) - transition.updateFrame(node: self.addButton, frame: middleButtonFrame) + //transition.updateFrame(node: self.rotateButton, frame: middleButtonFrame) + //transition.updateFrame(node: self.swapButton, frame: middleButtonFrame) + + let canAddColors = self.state.colors.count < self.state.maximumNumberOfColors + + transition.updateFrame(node: self.addButton, frame: CGRect(origin: CGPoint(x: size.width - rightInset - buttonSize.width, y: floor((topPanelHeight - buttonSize.height) / 2.0)), size: buttonSize)) + transition.updateAlpha(node: self.addButton, alpha: canAddColors ? 1.0 : 0.0) + transition.updateSublayerTransformScale(node: self.addButton, scale: canAddColors ? 1.0 : 0.1) - let rotateButtonAlpha: CGFloat + /*let rotateButtonAlpha: CGFloat let swapButtonAlpha: CGFloat let addButtonAlpha: CGFloat if let _ = self.state.secondColor { @@ -700,10 +615,9 @@ final class WallpaperColorPanelNode: ASDisplayNode { } transition.updateAlpha(node: self.rotateButton, alpha: rotateButtonAlpha) transition.updateAlpha(node: self.swapButton, alpha: swapButtonAlpha) - transition.updateAlpha(node: self.addButton, alpha: addButtonAlpha) + transition.updateAlpha(node: self.addButton, alpha: addButtonAlpha)*/ - func degreesToRadians(_ degrees: CGFloat) -> CGFloat - { + func degreesToRadians(_ degrees: CGFloat) -> CGFloat { var degrees = degrees if degrees >= 270.0 { degrees = degrees - 360.0 @@ -712,20 +626,67 @@ final class WallpaperColorPanelNode: ASDisplayNode { } 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) + + self.rotateButton.isHidden = true + self.swapButton.isHidden = true + self.multiColorFieldNode.isHidden = false + + let sampleItemSize: CGFloat = 32.0 + let sampleItemSpacing: CGFloat = 15.0 + + var nextSampleX = leftInset + + for i in 0 ..< self.state.colors.count { + var animateIn = false + let itemNode: ColorSampleItemNode + if self.sampleItemNodes.count > i { + itemNode = self.sampleItemNodes[i] + } else { + itemNode = ColorSampleItemNode(action: { [weak self] in + guard let strongSelf = self else { + return + } + let index = i + strongSelf.updateState({ state in + var state = state + state.selection = index + return state + }) + }) + self.sampleItemNodes.append(itemNode) + self.insertSubnode(itemNode, aboveSubnode: self.multiColorFieldNode) + animateIn = true + } + + if i != 0 { + nextSampleX += sampleItemSpacing + } + itemNode.frame = CGRect(origin: CGPoint(x: nextSampleX, y: (topPanelHeight - sampleItemSize) / 2.0), size: CGSize(width: sampleItemSize, height: sampleItemSize)) + nextSampleX += sampleItemSize + itemNode.update(size: itemNode.bounds.size, color: UIColor(rgb: self.state.colors[i]), isSelected: self.state.selection == i) + + if animateIn { + transition.animateTransformScale(node: itemNode, from: 0.1) + itemNode.alpha = 0.0 + transition.updateAlpha(node: itemNode, alpha: 1.0) + } + } + if self.sampleItemNodes.count > self.state.colors.count { + for i in self.state.colors.count ..< self.sampleItemNodes.count { + let itemNode = self.sampleItemNodes[i] + transition.updateTransformScale(node: itemNode, scale: 0.1) + transition.updateAlpha(node: itemNode, alpha: 0.0, completion: { [weak itemNode] _ in + itemNode?.removeFromSupernode() + }) + } + self.sampleItemNodes.removeSubrange(self.state.colors.count ..< self.sampleItemNodes.count) + } + + let fieldX = nextSampleX + sampleItemSpacing + + let fieldFrame = CGRect(x: fieldX, y: (topPanelHeight - fieldHeight) / 2.0, width: size.width - fieldX - leftInset - (canAddColors ? (buttonSize.width + sampleItemSpacing) : 0.0), height: fieldHeight) + transition.updateFrame(node: self.multiColorFieldNode, frame: fieldFrame) + self.multiColorFieldNode.updateLayout(size: fieldFrame.size, condensed: false, 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)) @@ -746,25 +707,53 @@ final class WallpaperColorPanelNode: ASDisplayNode { } @objc private func swapPressed() { - self.updateState({ current in + /*self.updateState({ current in var updated = current if let secondColor = current.secondColor { updated.firstColor = secondColor updated.secondColor = current.firstColor } return updated - }) + })*/ } @objc private func addPressed() { self.colorSelected?() self.colorAdded?() + + self.multiColorFieldNode.setSkipEndEditingIfNeeded() + + self.updateState({ current in + var current = current + if current.colors.count < current.maximumNumberOfColors { + if current.colors.isEmpty { + current.colors.append(0xffffff) + } else if current.simpleGradientGeneration { + var hsb = UIColor(rgb: current.colors[0]).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 + } + current.colors.append(UIColor(hue: hsb.0, saturation: hsb.1, brightness: hsb.2, alpha: 1.0).rgb) + } else { + current.colors.append(current.colors[current.colors.count - 1]) + } + current.selection = current.colors.count - 1 + } + return current + }) - self.firstColorFieldNode.setSkipEndEditingIfNeeded() + /*self.firstColorFieldNode.setSkipEndEditingIfNeeded() self.updateState({ current in var updated = current - updated.selection = .second + updated.selection = .index(1) let firstColor = current.firstColor ?? current.defaultColor if let color = firstColor { @@ -790,6 +779,13 @@ final class WallpaperColorPanelNode: ASDisplayNode { } return updated - }) + })*/ + } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if let result = super.hitTest(point, with: event) { + return result + } + return nil } } diff --git a/submodules/SettingsUI/Sources/Themes/WallpaperColorPickerNode.swift b/submodules/SettingsUI/Sources/Themes/WallpaperColorPickerNode.swift index cf89d9b0d7..48b935af57 100644 --- a/submodules/SettingsUI/Sources/Themes/WallpaperColorPickerNode.swift +++ b/submodules/SettingsUI/Sources/Themes/WallpaperColorPickerNode.swift @@ -238,6 +238,7 @@ final class WallpaperColorPickerNode: ASDisplayNode { self.brightnessNode.hitTestSlop = UIEdgeInsets(top: -16.0, left: -16.0, bottom: -16.0, right: -16.0) self.brightnessKnobNode = ASImageNode() self.brightnessKnobNode.image = pointerImage + self.brightnessKnobNode.isUserInteractionEnabled = false self.colorNode = WallpaperColorHueSaturationNode() self.colorNode.hitTestSlop = UIEdgeInsets(top: -16.0, left: -16.0, bottom: -16.0, right: -16.0) self.colorKnobNode = WallpaperColorKnobNode() diff --git a/Telegram/SupportFiles/Empty.swift b/submodules/SettingsUI/Sources/Themes/WallpaperColorsPanelNode.swift similarity index 100% rename from Telegram/SupportFiles/Empty.swift rename to submodules/SettingsUI/Sources/Themes/WallpaperColorsPanelNode.swift diff --git a/submodules/SettingsUI/Sources/Themes/WallpaperGalleryController.swift b/submodules/SettingsUI/Sources/Themes/WallpaperGalleryController.swift index 7924a034f8..9973dfafe9 100644 --- a/submodules/SettingsUI/Sources/Themes/WallpaperGalleryController.swift +++ b/submodules/SettingsUI/Sources/Themes/WallpaperGalleryController.swift @@ -25,8 +25,8 @@ public enum WallpaperListType { public enum WallpaperListSource { case list(wallpapers: [TelegramWallpaper], central: TelegramWallpaper, type: WallpaperListType) - case wallpaper(TelegramWallpaper, WallpaperPresentationOptions?, UIColor?, UIColor?, Int32?, Int32?, Message?) - case slug(String, TelegramMediaFile?, WallpaperPresentationOptions?, UIColor?, UIColor?, Int32?, Int32?, Message?) + case wallpaper(TelegramWallpaper, WallpaperPresentationOptions?, [UInt32], Int32?, Int32?, Message?) + case slug(String, TelegramMediaFile?, WallpaperPresentationOptions?, [UInt32], Int32?, Int32?, Message?) case asset(PHAsset) case contextResult(ChatContextResult) case customColor(UInt32?) @@ -89,6 +89,8 @@ class WallpaperGalleryOverlayNode: ASDisplayNode { } class WallpaperGalleryControllerNode: GalleryControllerNode { + var nativeStatusBar: StatusBar? + override func updateDistanceFromEquilibrium(_ value: CGFloat) { guard let itemNode = self.pager.centralItemNode() as? WallpaperGalleryItemNode else { return @@ -96,36 +98,63 @@ class WallpaperGalleryControllerNode: GalleryControllerNode { itemNode.updateDismissTransition(value) } + + override func didLoad() { + super.didLoad() + + //self.view.addGestureRecognizer(UILongPressGestureRecognizer(target: self, action: #selector(self.longPressGesture(_:)))) + } + + @objc private func longPressGesture(_ recognizer: UILongPressGestureRecognizer) { + switch recognizer.state { + case .began: + self.setControlsHidden(true, animated: false) + + self.overlayNode?.alpha = 0.0 + self.nativeStatusBar?.updateAlpha(0.0, transition: .immediate) + + if let itemNode = self.pager.centralItemNode() as? WallpaperGalleryItemNode { + itemNode.updateDismissTransition(self.bounds.size.height) + } + case .ended, .cancelled: + self.setControlsHidden(false, animated: false) + + self.overlayNode?.alpha = 1.0 + self.nativeStatusBar?.updateAlpha(1.0, transition: .immediate) + + if let itemNode = self.pager.centralItemNode() as? WallpaperGalleryItemNode { + itemNode.updateDismissTransition(0.0) + } + default: + break + } + } } -private func updatedFileWallpaper(wallpaper: TelegramWallpaper, firstColor: UIColor?, secondColor: UIColor?, intensity: Int32?, rotation: Int32?) -> TelegramWallpaper { +private func updatedFileWallpaper(wallpaper: TelegramWallpaper, colors: [UInt32], 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, firstColor: firstColor, secondColor: secondColor, intensity: intensity, rotation: rotation) + return updatedFileWallpaper(id: file.id, accessHash: file.accessHash, slug: file.slug, file: file.file, colors: colors, intensity: intensity, rotation: rotation) } else { return wallpaper } } -private func updatedFileWallpaper(id: Int64? = nil, accessHash: Int64? = nil, slug: String, file: TelegramMediaFile, firstColor: UIColor?, secondColor: UIColor?, intensity: Int32?, rotation: Int32?) -> TelegramWallpaper { +private func updatedFileWallpaper(id: Int64? = nil, accessHash: Int64? = nil, slug: String, file: TelegramMediaFile, colors: [UInt32], 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 colorValues: [UInt32] = [] var intensityValue: Int32? - if let firstColor = firstColor { - firstColorValue = firstColor.argb - intensityValue = intensity - } else if isPattern { - firstColorValue = 0xd6e2ee + if !colors.isEmpty { + colorValues = colors + intensityValue = intensity ?? 50 + } else { + colorValues = [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(color: firstColorValue, bottomColor: secondColorValue, intensity: intensityValue, rotation: rotation)) + return .file(id: id ?? 0, accessHash: accessHash ?? 0, isCreator: false, isDefault: false, isPattern: isPattern, isDark: false, slug: slug, file: file, settings: WallpaperSettings(colors: colorValues, intensity: intensityValue, rotation: rotation)) } public class WallpaperGalleryController: ViewController { @@ -142,6 +171,7 @@ public class WallpaperGalleryController: ViewController { return self._ready } private var didSetReady = false + private var didBeginSettingReady = false private let disposable = MetaDisposable() @@ -165,8 +195,14 @@ public class WallpaperGalleryController: ViewController { private var overlayNode: WallpaperGalleryOverlayNode? private var toolbarNode: WallpaperGalleryToolbarNode? private var patternPanelNode: WallpaperPatternPanelNode? - + private var colorsPanelNode: WallpaperColorPanelNode? + + private var patternInitialWallpaper: TelegramWallpaper? private var patternPanelEnabled = false + private var colorsPanelEnabled = false + + private var savedPatternWallpaper: TelegramWallpaper? + private var savedPatternIntensity: Int32? public init(context: AccountContext, source: WallpaperListSource) { self.context = context @@ -190,15 +226,15 @@ public class WallpaperGalleryController: ViewController { if case let .wallpapers(wallpaperOptions) = type, let options = wallpaperOptions { self.initialOptions = options } - case let .slug(slug, file, options, firstColor, secondColor, intensity, rotation, message): + case let .slug(slug, file, options, colors, intensity, rotation, message): if let file = file { - let wallpaper = updatedFileWallpaper(slug: slug, file: file, firstColor: firstColor, secondColor: secondColor, intensity: intensity, rotation: rotation) + let wallpaper = updatedFileWallpaper(slug: slug, file: file, colors: colors, intensity: intensity, rotation: rotation) entries = [.wallpaper(wallpaper, message)] centralEntryIndex = 0 self.initialOptions = options } - case let .wallpaper(wallpaper, options, firstColor, secondColor, intensity, rotation, message): - let wallpaper = updatedFileWallpaper(wallpaper: wallpaper, firstColor: firstColor, secondColor: secondColor, intensity: intensity, rotation: rotation) + case let .wallpaper(wallpaper, options, colors, intensity, rotation, message): + let wallpaper = updatedFileWallpaper(wallpaper: wallpaper, colors: colors, intensity: intensity, rotation: rotation) entries = [.wallpaper(wallpaper, message)] centralEntryIndex = 0 self.initialOptions = options @@ -283,7 +319,8 @@ public class WallpaperGalleryController: ViewController { 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 + + self.colorsPanelNode?.updateTheme(self.presentationData.theme) } func dismiss(forceAway: Bool) { @@ -304,6 +341,17 @@ public class WallpaperGalleryController: ViewController { } return GalleryPagerTransaction(deleteItems: [], insertItems: [], updateItems: updateItems, focusOnItem: self.galleryNode.pager.centralItemNode()?.index, synchronous: false) } + + private func updateCurrentEntryTransaction(entry: WallpaperGalleryEntry, arguments: WallpaperGalleryItemArguments, index: Int) -> GalleryPagerTransaction { + var updateItems: [GalleryPagerUpdateItem] = [] + for i in 0 ..< self.entries.count { + if i == index { + let item = GalleryPagerUpdateItem(index: index, previousIndex: index, item: WallpaperGalleryItem(context: self.context, index: index, entry: entry, arguments: arguments, source: self.source)) + updateItems.append(item) + } + } + return GalleryPagerTransaction(deleteItems: [], insertItems: [], updateItems: updateItems, focusOnItem: self.galleryNode.pager.centralItemNode()?.index, synchronous: false) + } override public func loadDisplayNode() { let controllerInteraction = GalleryControllerInteraction(presentController: { [weak self] controller, arguments in @@ -315,17 +363,22 @@ public class WallpaperGalleryController: ViewController { }, replaceRootController: { controller, ready in }, editMedia: { _ in }) - self.displayNode = WallpaperGalleryControllerNode(controllerInteraction: controllerInteraction, pageGap: 0.0) + self.displayNode = WallpaperGalleryControllerNode(controllerInteraction: controllerInteraction, pageGap: 0.0, disableTapNavigation: true) self.displayNodeDidLoad() + + (self.displayNode as? WallpaperGalleryControllerNode)?.nativeStatusBar = self.statusBar self.galleryNode.navigationBar = self.navigationBar self.galleryNode.dismiss = { [weak self] in self?.presentingViewController?.dismiss(animated: false, completion: nil) } - + + var currentCentralItemIndex: Int? self.galleryNode.pager.centralItemIndexUpdated = { [weak self] index in if let strongSelf = self { - strongSelf.bindCentralItemNode(animated: true) + let updated = currentCentralItemIndex != index + currentCentralItemIndex = index + strongSelf.bindCentralItemNode(animated: true, updated: updated) } } @@ -374,6 +427,7 @@ public class WallpaperGalleryController: ViewController { dismissed = true if let centralItemNode = strongSelf.galleryNode.pager.centralItemNode() as? WallpaperGalleryItemNode { let options = centralItemNode.options + let gradientColors = centralItemNode.colors if !strongSelf.entries.isEmpty { let entry = strongSelf.entries[centralItemNode.index] switch entry { @@ -392,13 +446,13 @@ 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, bottomColor: baseSettings?.bottomColor, intensity: baseSettings?.intensity) + let updatedSettings = WallpaperSettings(blur: options.contains(.blur), motion: options.contains(.motion), colors: baseSettings?.colors ?? [], intensity: baseSettings?.intensity, rotation: baseSettings?.rotation) let wallpaper = wallpaper.withUpdatedSettings(updatedSettings) let autoNightModeTriggered = strongSelf.presentationData.autoNightModeTriggered let _ = (updatePresentationThemeSettingsInteractively(accountManager: strongSelf.context.sharedContext.accountManager, { current in var themeSpecificChatWallpapers = current.themeSpecificChatWallpapers - var wallpaper = wallpaper.isBasicallyEqual(to: strongSelf.presentationData.theme.chat.defaultWallpaper) ? nil : wallpaper + let wallpaper = wallpaper.isBasicallyEqual(to: strongSelf.presentationData.theme.chat.defaultWallpaper) ? nil : wallpaper let themeReference: PresentationThemeReference if autoNightModeTriggered { themeReference = current.automaticThemeSwitchSetting.theme @@ -424,6 +478,18 @@ public class WallpaperGalleryController: ViewController { break } let _ = installWallpaper(account: strongSelf.context.account, wallpaper: wallpaper).start() + let _ = (strongSelf.context.sharedContext.accountManager.transaction { transaction in + WallpapersState.update(transaction: transaction, { state in + var state = state + if let index = state.wallpapers.firstIndex(where: { + $0.isBasicallyEqual(to: wallpaper) + }) { + state.wallpapers.remove(at: index) + } + state.wallpapers.insert(wallpaper, at: 0) + return state + }) + }).start() } let applyWallpaper: (TelegramWallpaper) -> Void = { wallpaper in @@ -449,24 +515,31 @@ public class WallpaperGalleryController: ViewController { } } } else if case let .file(file) = wallpaper, let resource = resource { - 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) - + if wallpaper.isPattern, !file.settings.colors.isEmpty, let intensity = file.settings.intensity { var data: Data? + var thumbnailData: 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 } + + let thumbnailResource = file.file.previewRepresentations.first?.resource + + if let resource = thumbnailResource { + if let path = strongSelf.context.account.postbox.mediaBox.completedResourcePath(resource), let maybeData = try? Data(contentsOf: URL(fileURLWithPath: path), options: .mappedRead) { + thumbnailData = maybeData + } else if let path = strongSelf.context.sharedContext.accountManager.mediaBox.completedResourcePath(resource), let maybeData = try? Data(contentsOf: URL(fileURLWithPath: path), options: .mappedRead) { + thumbnailData = maybeData + } + } if let data = data { strongSelf.context.sharedContext.accountManager.mediaBox.storeResourceData(resource.id, data: data, synchronous: true) - let _ = (strongSelf.context.sharedContext.accountManager.mediaBox.cachedResourceRepresentation(resource, representation: representation, complete: true, fetch: true) - |> filter({ $0.complete }) - |> take(1) - |> deliverOnMainQueue).start(next: { _ in - completion(wallpaper) - }) + if let thumbnailResource = thumbnailResource, let thumbnailData = thumbnailData { + strongSelf.context.sharedContext.accountManager.mediaBox.storeResourceData(thumbnailResource.id, data: thumbnailData, synchronous: true) + } + completion(wallpaper) } } else if let path = strongSelf.context.account.postbox.mediaBox.completedResourcePath(file.file.resource), let data = try? Data(contentsOf: URL(fileURLWithPath: path), options: .mappedRead) { strongSelf.context.sharedContext.accountManager.mediaBox.storeResourceData(file.file.resource.id, data: data) @@ -500,7 +573,12 @@ public class WallpaperGalleryController: ViewController { applyWallpaper(wallpaper) }) } else { - applyWallpaper(wallpaper) + var updatedWallpaper = wallpaper + if var settings = wallpaper.settings { + settings.motion = options.contains(.motion) + updatedWallpaper = updatedWallpaper.withUpdatedSettings(settings) + } + applyWallpaper(updatedWallpaper) } default: break @@ -511,11 +589,6 @@ public class WallpaperGalleryController: ViewController { } } } - - let ready = self.galleryNode.pager.ready() |> timeout(2.0, queue: Queue.mainQueue(), alternate: .single(Void())) |> afterNext { [weak self] _ in - self?.didSetReady = true - } - self._ready.set(ready |> map { true }) } private func currentEntry() -> WallpaperGalleryEntry? { @@ -532,10 +605,14 @@ public class WallpaperGalleryController: ViewController { super.viewDidAppear(animated) self.galleryNode.modalAnimateIn() - self.bindCentralItemNode(animated: false) + self.bindCentralItemNode(animated: false, updated: false) + + if let centralItemNode = self.galleryNode.pager.centralItemNode() as? WallpaperGalleryItemNode { + centralItemNode.animateWallpaperAppeared() + } } - private func bindCentralItemNode(animated: Bool) { + private func bindCentralItemNode(animated: Bool, updated: Bool) { if let node = self.galleryNode.pager.centralItemNode() as? WallpaperGalleryItemNode { self.centralItemSubtitle.set(node.subtitle.get()) self.centralItemStatus.set(node.status.get()) @@ -543,27 +620,119 @@ public class WallpaperGalleryController: ViewController { node.action = { [weak self] in self?.actionPressed() } - node.requestPatternPanel = { [weak self] enabled in + node.requestPatternPanel = { [weak self] enabled, initialWallpaper in if let strongSelf = self, let (layout, _) = strongSelf.validLayout { + strongSelf.colorsPanelEnabled = false + strongSelf.colorsPanelNode?.view.endEditing(true) + + if !enabled { + strongSelf.savedPatternWallpaper = initialWallpaper + strongSelf.savedPatternIntensity = initialWallpaper.settings?.intensity + } + + strongSelf.patternInitialWallpaper = enabled ? initialWallpaper : nil + switch initialWallpaper { + case let .color(color): + strongSelf.patternPanelNode?.backgroundColors = ([color], nil, nil) + case let .gradient(_, colors, settings): + strongSelf.patternPanelNode?.backgroundColors = (colors, settings.rotation, nil) + case let .file(file) where file.isPattern: + strongSelf.patternPanelNode?.backgroundColors = (file.settings.colors, file.settings.rotation, file.settings.intensity) + default: + break + } + strongSelf.patternPanelNode?.serviceBackgroundColor = serviceColor(for: (initialWallpaper, nil)) strongSelf.patternPanelEnabled = enabled strongSelf.galleryNode.scrollView.isScrollEnabled = !enabled if enabled { - strongSelf.patternPanelNode?.didAppear() + strongSelf.patternPanelNode?.updateWallpapers() + strongSelf.patternPanelNode?.didAppear(initialWallpaper: strongSelf.savedPatternWallpaper, intensity: strongSelf.savedPatternIntensity) } else { - strongSelf.updateEntries(pattern: .color(0), preview: false) + switch initialWallpaper { + case .color, .gradient: + strongSelf.updateEntries(wallpaper: initialWallpaper) + case let .file(file): + if !file.settings.colors.isEmpty { + if file.settings.colors.count >= 2 { + strongSelf.updateEntries(wallpaper: .gradient(nil, file.settings.colors, WallpaperSettings(rotation: file.settings.rotation))) + } else { + strongSelf.updateEntries(wallpaper: .color(file.settings.colors[0])) + } + } + default: + break + } } strongSelf.containerLayoutUpdated(layout, transition: .animated(duration: 0.3, curve: .spring)) } } + + node.toggleColorsPanel = { [weak self] colors in + if let strongSelf = self, let (layout, _) = strongSelf.validLayout, let colors = colors, let itemNode = strongSelf.galleryNode.pager.centralItemNode() as? WallpaperGalleryItemNode { + strongSelf.patternPanelEnabled = false + strongSelf.colorsPanelEnabled = !strongSelf.colorsPanelEnabled + strongSelf.galleryNode.scrollView.isScrollEnabled = !strongSelf.colorsPanelEnabled + if !strongSelf.colorsPanelEnabled { + strongSelf.colorsPanelNode?.view.endEditing(true) + } + + if strongSelf.colorsPanelEnabled { + strongSelf.colorsPanelNode?.updateState({ _ in + return WallpaperColorPanelNodeState( + selection: 0, + colors: colors.map(\.rgb), + maximumNumberOfColors: 4, + rotateAvailable: false, + rotation: 0, + preview: false, + simpleGradientGeneration: false + ) + }, animated: false) + } + + itemNode.updateIsColorsPanelActive(strongSelf.colorsPanelEnabled, animated: true) + + strongSelf.containerLayoutUpdated(layout, transition: .animated(duration: 0.3, curve: .spring)) + } + } + + node.requestRotateGradient = { [weak self] angle in + guard let strongSelf = self, let _ = strongSelf.validLayout, let entry = strongSelf.currentEntry(), case let .wallpaper(wallpaper, _) = entry else { + return + } + var settings = wallpaper.settings ?? WallpaperSettings() + settings.rotation = angle + strongSelf.updateEntries(wallpaper: wallpaper.withUpdatedSettings(settings)) + } - if let entry = self.currentEntry(), case let .wallpaper(wallpaper, _) = entry, case let .file(_, _, _, _, true, _, _, _ , settings) = wallpaper, let color = settings.color { - if self.patternPanelNode?.backgroundColors != nil, let snapshotView = self.patternPanelNode?.scrollNode.view.snapshotContentTree() { + if let entry = self.currentEntry(), case let .wallpaper(wallpaper, _) = entry, case let .file(_, _, _, _, true, _, _, _ , settings) = wallpaper, !settings.colors.isEmpty { + /*if self.patternPanelNode?.backgroundColors != nil, let snapshotView = self.patternPanelNode?.scrollNode.view.snapshotContentTree() { self.patternPanelNode?.view.addSubview(snapshotView) snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false) { [weak snapshotView] _ in snapshotView?.removeFromSuperview() } + }*/ + //self.patternPanelNode?.backgroundColors = ([settings.colors[0]], nil) + } + + if updated { + if self.colorsPanelEnabled || self.patternPanelEnabled { + self.colorsPanelEnabled = false + self.colorsPanelNode?.view.endEditing(true) + self.patternPanelEnabled = false + + if let (layout, _) = self.validLayout { + self.containerLayoutUpdated(layout, transition: .animated(duration: 0.3, curve: .spring)) + } } - self.patternPanelNode?.backgroundColors = (UIColor(rgb: color), nil, nil) + } + if !self.didBeginSettingReady { + self.didBeginSettingReady = true + + let ready = self.galleryNode.pager.ready() |> timeout(2.0, queue: Queue.mainQueue(), alternate: .single(Void())) |> afterNext { [weak self] _ in + self?.didSetReady = true + } + self._ready.set(ready |> map { true }) } } } @@ -591,27 +760,53 @@ public class WallpaperGalleryController: ViewController { self.galleryNode.pager.transaction(self.updateTransaction(entries: entries, arguments: WallpaperGalleryItemArguments(colorPreview: preview, isColorsList: false, patternEnabled: self.patternPanelEnabled))) } + + private func updateEntries(wallpaper: TelegramWallpaper, preview: Bool = false) { + guard self.validLayout != nil, let centralEntryIndex = self.galleryNode.pager.centralItemNode()?.index else { + return + } + + var entries = self.entries + var currentEntry = entries[centralEntryIndex] + switch currentEntry { + case .wallpaper: + currentEntry = .wallpaper(wallpaper, nil) + default: + break + } + entries[centralEntryIndex] = currentEntry + self.entries = entries + + self.galleryNode.pager.transaction(self.updateCurrentEntryTransaction(entry: currentEntry, arguments: WallpaperGalleryItemArguments(colorPreview: preview, isColorsList: false, patternEnabled: self.patternPanelEnabled), index: centralEntryIndex)) + } private func updateEntries(pattern: TelegramWallpaper?, intensity: Int32? = nil, preview: Bool = false) { var updatedEntries: [WallpaperGalleryEntry] = [] for entry in self.entries { - var entryColor: UInt32? + var entryColors: [UInt32] = [] if case let .wallpaper(wallpaper, _) = entry { if case let .color(color) = wallpaper { - entryColor = color - } else if case let .file(file) = wallpaper { - entryColor = file.settings.color + entryColors = [color] + } else if case let .file(file) = wallpaper, file.isPattern { + entryColors = file.settings.colors + } else if case let .gradient(_, colors, _) = wallpaper { + entryColors = colors } } - if let entryColor = entryColor { + if !entryColors.isEmpty { 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 newSettings = WallpaperSettings(blur: file.settings.blur, motion: file.settings.motion, colors: entryColors, intensity: intensity) 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) - updatedEntries.append(.wallpaper(newWallpaper, nil)) + if entryColors.count == 1 { + let newWallpaper = TelegramWallpaper.color(entryColors[0]) + updatedEntries.append(.wallpaper(newWallpaper, nil)) + } else { + let newWallpaper = TelegramWallpaper.gradient(nil, entryColors, WallpaperSettings(rotation: nil)) + updatedEntries.append(.wallpaper(newWallpaper, nil)) + } } } } @@ -626,17 +821,23 @@ public class WallpaperGalleryController: ViewController { let hadLayout = self.validLayout != nil super.containerLayoutUpdated(layout, transition: transition) + + let panelHeight: CGFloat = 235.0 + + var pagerLayout = layout + if self.patternPanelEnabled || self.colorsPanelEnabled { + pagerLayout.intrinsicInsets.bottom += panelHeight + } + pagerLayout.intrinsicInsets.bottom = max(pagerLayout.intrinsicInsets.bottom, layout.inputHeight ?? 0.0) self.galleryNode.frame = CGRect(origin: CGPoint(), size: layout.size) - self.galleryNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationHeight, transition: transition) + self.galleryNode.containerLayoutUpdated(pagerLayout, navigationBarHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition) self.overlayNode?.frame = self.galleryNode.bounds transition.updateFrame(node: self.toolbarNode!, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - 49.0 - layout.intrinsicInsets.bottom), 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 = layout.intrinsicInsets.bottom + 49.0 - let standardInputHeight = layout.deviceMetrics.keyboardHeight(inLandscape: false) - let height = max(standardInputHeight, layout.inputHeight ?? 0.0) - bottomInset + 47.0 let currentPatternPanelNode: WallpaperPatternPanelNode if let patternPanelNode = self.patternPanelNode { @@ -644,26 +845,90 @@ public class WallpaperGalleryController: ViewController { } else { let patternPanelNode = WallpaperPatternPanelNode(context: self.context, theme: presentationData.theme, strings: presentationData.strings) patternPanelNode.patternChanged = { [weak self] pattern, intensity, preview in - if let strongSelf = self, strongSelf.validLayout != nil { - strongSelf.updateEntries(pattern: pattern, intensity: intensity, preview: preview) + if let strongSelf = self, strongSelf.validLayout != nil, let patternInitialWallpaper = strongSelf.patternInitialWallpaper { + var colors: [UInt32] = [] + var rotation: Int32? + switch patternInitialWallpaper { + case let .color(color): + colors = [color] + case let .file(file): + colors = file.settings.colors + rotation = file.settings.rotation + case let .gradient(_, colorsValue, settings): + colors = colorsValue + rotation = settings.rotation + default: + break + } + switch patternInitialWallpaper { + case .color, .file, .gradient: + if let pattern = pattern, case let .file(file) = pattern { + let newSettings = WallpaperSettings(blur: file.settings.blur, motion: file.settings.motion, colors: colors, intensity: intensity) + 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) + + strongSelf.savedPatternWallpaper = newWallpaper + strongSelf.savedPatternIntensity = intensity + + strongSelf.updateEntries(wallpaper: newWallpaper, preview: preview) + } + default: + break + } + + strongSelf.patternPanelNode?.backgroundColors = (colors, rotation, intensity) } } - 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 = 235.0 + + let currentColorsPanelNode: WallpaperColorPanelNode + if let current = self.colorsPanelNode { + currentColorsPanelNode = current + } else { + let colorsPanelNode = WallpaperColorPanelNode(theme: self.presentationData.theme, strings: self.presentationData.strings) + self.colorsPanelNode = colorsPanelNode + currentColorsPanelNode = colorsPanelNode + self.overlayNode?.insertSubnode(colorsPanelNode, belowSubnode: self.toolbarNode!) + + colorsPanelNode.colorsChanged = { [weak self] colors, _ in + guard let strongSelf = self else { + return + } + guard let entry = strongSelf.currentEntry(), case let .wallpaper(currentWallpaper, _) = entry else { + return + } + + var wallpaper: TelegramWallpaper = .gradient(nil, colors, WallpaperSettings(blur: false, motion: false, colors: [], intensity: nil, rotation: nil)) + + if case let .file(file) = currentWallpaper { + wallpaper = currentWallpaper.withUpdatedSettings(WallpaperSettings(blur: false, motion: false, colors: colors, intensity: file.settings.intensity, rotation: file.settings.rotation)) + } + + strongSelf.updateEntries(wallpaper: wallpaper) + } + } + 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) + patternPanelFrame.origin = CGPoint(x: 0.0, y: layout.size.height - max((layout.inputHeight ?? 0.0) - panelHeight + 44.0, bottomInset) - panelHeight) bottomInset += panelHeight } - bottomInset += 66.0 transition.updateFrame(node: currentPatternPanelNode, frame: patternPanelFrame) currentPatternPanelNode.updateLayout(size: patternPanelFrame.size, transition: transition) + + var colorsPanelFrame = CGRect(x: 0.0, y: layout.size.height, width: layout.size.width, height: panelHeight) + if self.colorsPanelEnabled { + colorsPanelFrame.origin = CGPoint(x: 0.0, y: layout.size.height - max((layout.inputHeight ?? 0.0) - panelHeight + 44.0, bottomInset) - panelHeight) + bottomInset += panelHeight + } + + transition.updateFrame(node: currentColorsPanelNode, frame: colorsPanelFrame) + currentColorsPanelNode.updateLayout(size: colorsPanelFrame.size, transition: transition) + + bottomInset += 66.0 self.validLayout = (layout, bottomInset) if !hadLayout { @@ -721,11 +986,20 @@ public class WallpaperGalleryController: ViewController { }) case let .file(_, _, _, _, isPattern, _, slug, _, settings): if isPattern { - if let color = settings.color { - if let bottomColor = settings.bottomColor { - options.append("bg_color=\(UIColor(rgb: color).hexString)-\(UIColor(rgb: bottomColor).hexString)") + if !settings.colors.isEmpty { + if settings.colors.count == 2 { + options.append("bg_color=\(UIColor(rgb: settings.colors[0]).hexString)-\(UIColor(rgb: settings.colors[1]).hexString)") + } else if settings.colors.count >= 3 { + var colorsString = "" + for color in settings.colors { + if !colorsString.isEmpty { + colorsString.append("~") + } + colorsString.append(UIColor(rgb: color).hexString) + } + options.append("bg_color=\(colorsString)") } else { - options.append("bg_color=\(UIColor(rgb: color).hexString)") + options.append("bg_color=\(UIColor(rgb: settings.colors[0]).hexString)") } } if let intensity = settings.intensity { @@ -744,8 +1018,21 @@ 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: 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)")) + case let .gradient(_, colors, _): + var colorsString = "" + + for color in colors { + if !colorsString.isEmpty { + if colors.count >= 3 { + colorsString.append("~") + } else { + colorsString.append("-") + } + } + colorsString.append(UIColor(rgb: color).hexString) + } + + controller = ShareController(context: context, subject:. url("https://t.me/bg/\(colorsString)")) default: break } diff --git a/submodules/SettingsUI/Sources/Themes/WallpaperGalleryItem.swift b/submodules/SettingsUI/Sources/Themes/WallpaperGalleryItem.swift index 7e93b9ff75..e9cc0bce08 100644 --- a/submodules/SettingsUI/Sources/Themes/WallpaperGalleryItem.swift +++ b/submodules/SettingsUI/Sources/Themes/WallpaperGalleryItem.swift @@ -18,6 +18,7 @@ import GalleryUI import LocalMediaResources import WallpaperResources import AppBundle +import WallpaperBackgroundNode struct WallpaperGalleryItemArguments { let colorPreview: Bool @@ -71,11 +72,11 @@ class WallpaperGalleryItem: GalleryItem { private let progressDiameter: CGFloat = 50.0 private let motionAmount: CGFloat = 32.0 -private func reference(for resource: MediaResource, media: Media, message: Message?) -> MediaResourceReference { +private func reference(for resource: MediaResource, media: Media, message: Message?, slug: String?) -> MediaResourceReference { if let message = message { return .media(media: .message(message: MessageReference(message), media: media), resource: resource) } - return .wallpaper(wallpaper: nil, resource: resource) + return .wallpaper(wallpaper: slug.flatMap(WallpaperReference.slug), resource: resource) } final class WallpaperGalleryItemNode: GalleryItemNode { @@ -90,6 +91,7 @@ final class WallpaperGalleryItemNode: GalleryItemNode { let wrapperNode: ASDisplayNode let imageNode: TransformImageNode + let nativeNode: WallpaperBackgroundNode private let statusNode: RadialStatusNode private let blurredNode: BlurredImageNode let cropNode: WallpaperCropNode @@ -97,9 +99,13 @@ final class WallpaperGalleryItemNode: GalleryItemNode { private var blurButtonNode: WallpaperOptionButtonNode private var motionButtonNode: WallpaperOptionButtonNode private var patternButtonNode: WallpaperOptionButtonNode + private var colorsButtonNode: WallpaperOptionButtonNode + private var playButtonNode: HighlightableButtonNode + private let playButtonBackgroundNode: NavigationBackgroundNode private let messagesContainerNode: ASDisplayNode private var messageNodes: [ListViewItemNode]? + private var validMessages: [String]? fileprivate let _ready = Promise() private let fetchDisposable = MetaDisposable() @@ -110,10 +116,19 @@ final class WallpaperGalleryItemNode: GalleryItemNode { let status = Promise(.Local) let actionButton = Promise(nil) var action: (() -> Void)? - var requestPatternPanel: ((Bool) -> Void)? + var requestPatternPanel: ((Bool, TelegramWallpaper) -> Void)? + var toggleColorsPanel: (([UIColor]?) -> Void)? + var requestRotateGradient: ((Int32) -> Void)? - private var validLayout: ContainerViewLayout? + private var validLayout: (ContainerViewLayout, CGFloat)? private var validOffset: CGFloat? + + private var initialWallpaper: TelegramWallpaper? + + private let playButtonPlayImage: UIImage? + private let playButtonRotateImage: UIImage? + + private var isReadyDisposable: Disposable? init(context: AccountContext) { self.context = context @@ -122,6 +137,7 @@ final class WallpaperGalleryItemNode: GalleryItemNode { self.wrapperNode = ASDisplayNode() self.imageNode = TransformImageNode() self.imageNode.contentAnimations = .subsequentUpdates + self.nativeNode = WallpaperBackgroundNode(context: context) self.cropNode = WallpaperCropNode() self.statusNode = RadialStatusNode(backgroundNodeColor: UIColor(white: 0.0, alpha: 0.6)) self.statusNode.frame = CGRect(x: 0.0, y: 0.0, width: progressDiameter, height: progressDiameter) @@ -139,36 +155,84 @@ final class WallpaperGalleryItemNode: GalleryItemNode { self.motionButtonNode.setEnabled(false) self.patternButtonNode = WallpaperOptionButtonNode(title: self.presentationData.strings.WallpaperPreview_Pattern, value: .check(false)) self.patternButtonNode.setEnabled(false) + + self.colorsButtonNode = WallpaperOptionButtonNode(title: self.presentationData.strings.WallpaperPreview_WallpaperColors, value: .colors(false, [.clear])) + + self.playButtonBackgroundNode = NavigationBackgroundNode(color: UIColor(white: 0.0, alpha: 0.3)) + self.playButtonNode = HighlightableButtonNode() + self.playButtonNode.insertSubnode(self.playButtonBackgroundNode, at: 0) + + self.playButtonPlayImage = generateImage(CGSize(width: 48.0, height: 48.0), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(UIColor.white.cgColor) + + 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) + }) + + self.playButtonRotateImage = generateTintedImage(image: UIImage(bundleImageName: "Settings/ThemeColorRotateIcon"), color: .white) + + self.playButtonNode.setImage(self.playButtonPlayImage, for: []) super.init() self.clipsToBounds = true self.backgroundColor = .black - self.imageNode.imageUpdated = { [weak self] _ in - self?._ready.set(.single(Void())) + self.imageNode.imageUpdated = { [weak self] image in + if image != nil { + self?._ready.set(.single(Void())) + } } + self.isReadyDisposable = (self.nativeNode.isReady + |> filter { $0 } + |> take(1) + |> deliverOnMainQueue).start(next: { [weak self] _ in + self?._ready.set(.single(Void())) + }) self.imageNode.view.contentMode = .scaleAspectFill self.imageNode.clipsToBounds = true self.addSubnode(self.wrapperNode) - self.addSubnode(self.statusNode) + //self.addSubnode(self.statusNode) self.addSubnode(self.messagesContainerNode) self.addSubnode(self.blurButtonNode) self.addSubnode(self.motionButtonNode) self.addSubnode(self.patternButtonNode) + self.addSubnode(self.colorsButtonNode) + self.addSubnode(self.playButtonNode) self.blurButtonNode.addTarget(self, action: #selector(self.toggleBlur), forControlEvents: .touchUpInside) self.motionButtonNode.addTarget(self, action: #selector(self.toggleMotion), forControlEvents: .touchUpInside) self.patternButtonNode.addTarget(self, action: #selector(self.togglePattern), forControlEvents: .touchUpInside) + self.colorsButtonNode.addTarget(self, action: #selector(self.toggleColors), forControlEvents: .touchUpInside) + self.playButtonNode.addTarget(self, action: #selector(self.togglePlay), forControlEvents: .touchUpInside) } deinit { self.fetchDisposable.dispose() self.statusDisposable.dispose() self.colorDisposable.dispose() + self.isReadyDisposable?.dispose() } var cropRect: CGRect? { @@ -210,8 +274,8 @@ final class WallpaperGalleryItemNode: GalleryItemNode { if previousEntry != entry { self.preparePatternEditing() } - - self.patternButtonNode.isSelected = self.arguments.patternEnabled + + self.colorsButtonNode.colors = self.calculateGradientColors() ?? defaultBuiltinWallpaperGradientColors let imagePromise = Promise() @@ -231,14 +295,61 @@ final class WallpaperGalleryItemNode: GalleryItemNode { let progressAction = UIBarButtonItem(customDisplayNode: ProgressNavigationButtonNode(color: presentationData.theme.rootController.navigationBar.accentTextColor)) var isBlurrable = true + + self.nativeNode.updateBubbleTheme(bubbleTheme: presentationData.theme, bubbleCorners: presentationData.chatBubbleCorners) + + switch entry { + case let .wallpaper(wallpaper, _): + self.nativeNode.update(wallpaper: wallpaper) + + if case let .file(_, _, _, _, isPattern, _, _, _, settings) = wallpaper, isPattern { + self.nativeNode.isHidden = false + self.patternButtonNode.isSelected = isPattern + + if isPattern && settings.colors.count >= 3 { + self.playButtonNode.setImage(self.playButtonPlayImage, for: []) + } else { + self.playButtonNode.setImage(self.playButtonRotateImage, for: []) + } + } else if case let .gradient(_, colors, _) = wallpaper { + self.nativeNode.isHidden = false + self.nativeNode.update(wallpaper: wallpaper) + self.patternButtonNode.isSelected = false + + if colors.count >= 3 { + self.playButtonNode.setImage(self.playButtonPlayImage, for: []) + } else { + self.playButtonNode.setImage(self.playButtonRotateImage, for: []) + } + } else if case .color = wallpaper { + self.nativeNode.isHidden = false + self.nativeNode.update(wallpaper: wallpaper) + self.patternButtonNode.isSelected = false + } else { + self.nativeNode.isHidden = true + self.patternButtonNode.isSelected = false + self.playButtonNode.setImage(self.playButtonRotateImage, for: []) + } + case .asset: + self.nativeNode._internalUpdateIsSettingUpWallpaper() + self.nativeNode.isHidden = true + self.patternButtonNode.isSelected = false + self.playButtonNode.setImage(self.playButtonRotateImage, for: []) + default: + self.nativeNode.isHidden = true + self.patternButtonNode.isSelected = false + self.playButtonNode.setImage(self.playButtonRotateImage, for: []) + } switch entry { case let .wallpaper(wallpaper, message): + self.initialWallpaper = wallpaper + switch wallpaper { case .builtin: displaySize = CGSize(width: 1308.0, height: 2688.0).fitted(CGSize(width: 1280.0, height: 1280.0)).dividedByScreenScale().integralFloor contentSize = displaySize - signal = settingsBuiltinWallpaperImage(account: context.account) + signal = settingsBuiltinWallpaperImage(account: self.context.account) fetchSignal = .complete() statusSignal = .single(.Local) subtitleSignal = .single(nil) @@ -247,17 +358,17 @@ final class WallpaperGalleryItemNode: GalleryItemNode { case let .color(color): displaySize = CGSize(width: 1.0, height: 1.0) contentSize = displaySize - signal = solidColorImage(UIColor(rgb: color)) + signal = .single({ _ in nil }) 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): + case let .gradient(_, colors, settings): displaySize = CGSize(width: 1.0, height: 1.0) contentSize = displaySize - signal = gradientImage([UIColor(rgb: topColor), UIColor(rgb: bottomColor)], rotation: settings.rotation) + signal = .single({ _ in nil }) fetchSignal = .complete() statusSignal = .single(.Local) subtitleSignal = .single(nil) @@ -271,24 +382,24 @@ final class WallpaperGalleryItemNode: GalleryItemNode { var convertedRepresentations: [ImageRepresentationWithReference] = [] for representation in file.file.previewRepresentations { - convertedRepresentations.append(ImageRepresentationWithReference(representation: representation, reference: reference(for: representation.resource, media: file.file, message: message))) + convertedRepresentations.append(ImageRepresentationWithReference(representation: representation, reference: reference(for: representation.resource, media: file.file, message: message, slug: file.slug))) } - convertedRepresentations.append(ImageRepresentationWithReference(representation: .init(dimensions: dimensions, resource: file.file.resource, progressiveSizes: []), reference: reference(for: file.file.resource, media: file.file, message: message))) + convertedRepresentations.append(ImageRepresentationWithReference(representation: .init(dimensions: dimensions, resource: file.file.resource, progressiveSizes: [], immediateThumbnailData: nil), reference: reference(for: file.file.resource, media: file.file, message: message, slug: file.slug))) 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 !file.settings.colors.isEmpty { if let intensity = file.settings.intensity { patternIntensity = CGFloat(intensity) / 100.0 } - patternColor = UIColor(rgb: color, alpha: patternIntensity) + patternColor = UIColor(rgb: file.settings.colors[0], alpha: patternIntensity) patternColors.append(patternColor) - if let bottomColor = file.settings.bottomColor { - patternColors.append(UIColor(rgb: bottomColor, alpha: patternIntensity)) + if file.settings.colors.count >= 2 { + patternColors.append(UIColor(rgb: file.settings.colors[1], alpha: patternIntensity)) } } @@ -296,28 +407,11 @@ final class WallpaperGalleryItemNode: GalleryItemNode { self.backgroundColor = patternColor.withAlphaComponent(1.0) - if let previousEntry = previousEntry, case let .wallpaper(wallpaper, _) = previousEntry, case let .file(previousFile) = wallpaper, file.id == previousFile.id && (file.settings.color != previousFile.settings.color || file.settings.intensity != previousFile.settings.intensity) && self.colorPreview == self.arguments.colorPreview { - - let makeImageLayout = self.imageNode.asyncLayout() - Queue.concurrentDefaultQueue().async { - let apply = makeImageLayout(TransformImageArguments(corners: ImageCorners(), imageSize: displaySize, boundingSize: displaySize, intrinsicInsets: UIEdgeInsets(), custom: patternArguments)) - Queue.mainQueue().async { - if self.colorPreview { - apply() - } - } - } - return - } else if let offset = self.validOffset, self.arguments.colorPreview && abs(offset) > 0.0 { - return - } 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: .screen, autoFetchFullSize: true) - colorSignal = chatServiceBackgroundColor(wallpaper: wallpaper, mediaBox: context.account.postbox.mediaBox) + + signal = .single({ _ in nil }) + + colorSignal = chatServiceBackgroundColor(wallpaper: wallpaper, mediaBox: self.context.account.postbox.mediaBox) isBlurrable = false } else { @@ -341,11 +435,11 @@ final class WallpaperGalleryItemNode: GalleryItemNode { } } if let fileSize = file.file.size { - subtitleSignal = .single(dataSizeString(fileSize)) + subtitleSignal = .single(dataSizeString(fileSize, formatting: DataSizeStringFormatting(presentationData: self.presentationData))) } else { subtitleSignal = .single(nil) } - if file.id == 0 { + if file.slug.isEmpty { actionSignal = .single(nil) } else { actionSignal = .single(defaultAction) @@ -374,7 +468,7 @@ final class WallpaperGalleryItemNode: GalleryItemNode { } } if let fileSize = largestSize.resource.size { - subtitleSignal = .single(dataSizeString(fileSize)) + subtitleSignal = .single(dataSizeString(fileSize, formatting: DataSizeStringFormatting(presentationData: self.presentationData))) } else { subtitleSignal = .single(nil) } @@ -407,7 +501,7 @@ final class WallpaperGalleryItemNode: GalleryItemNode { let dimensions = CGSize(width: asset.pixelWidth, height: asset.pixelHeight) contentSize = dimensions displaySize = dimensions.dividedByScreenScale().integralFloor - signal = photoWallpaper(postbox: context.account.postbox, photoLibraryResource: PhotoLibraryMediaResource(localIdentifier: asset.localIdentifier, uniqueId: arc4random64())) + signal = photoWallpaper(postbox: context.account.postbox, photoLibraryResource: PhotoLibraryMediaResource(localIdentifier: asset.localIdentifier, uniqueId: Int64.random(in: Int64.min ... Int64.max))) fetchSignal = .complete() statusSignal = .single(.Local) subtitleSignal = .single(nil) @@ -448,9 +542,9 @@ final class WallpaperGalleryItemNode: GalleryItemNode { var representations: [TelegramMediaImageRepresentation] = [] if let thumbnailResource = thumbnailResource, let thumbnailDimensions = thumbnailDimensions { - representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(thumbnailDimensions), resource: thumbnailResource, progressiveSizes: [])) + representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(thumbnailDimensions), resource: thumbnailResource, progressiveSizes: [], immediateThumbnailData: nil)) } - representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(imageDimensions), resource: imageResource, progressiveSizes: [])) + representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(imageDimensions), resource: imageResource, progressiveSizes: [], immediateThumbnailData: 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)) @@ -471,6 +565,7 @@ final class WallpaperGalleryItemNode: GalleryItemNode { if self.cropNode.supernode == nil { self.imageNode.contentMode = .scaleAspectFill self.wrapperNode.addSubnode(self.imageNode) + self.wrapperNode.addSubnode(self.nativeNode) } else { self.imageNode.contentMode = .scaleToFill } @@ -479,6 +574,9 @@ final class WallpaperGalleryItemNode: GalleryItemNode { 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 { + if image != nil { + strongSelf._ready.set(.single(Void())) + } var image = isBlurrable ? image : nil if let imageToScale = image { let actualSize = CGSize(width: imageToScale.size.width * imageToScale.scale, height: imageToScale.size.height * imageToScale.scale) @@ -522,22 +620,24 @@ final class WallpaperGalleryItemNode: GalleryItemNode { self.colorDisposable.set((colorSignal |> deliverOnMainQueue).start(next: { [weak self] color in - self?.statusNode.backgroundNodeColor = color - self?.patternButtonNode.buttonColor = color - self?.blurButtonNode.buttonColor = color - self?.motionButtonNode.buttonColor = color + guard let strongSelf = self else { + return + } + strongSelf.statusNode.backgroundNodeColor = color + strongSelf.patternButtonNode.buttonColor = color + strongSelf.blurButtonNode.buttonColor = color + strongSelf.motionButtonNode.buttonColor = color + strongSelf.colorsButtonNode.buttonColor = color + + strongSelf.playButtonBackgroundNode.updateColor(color: color, transition: .immediate) })) - - if let layout = self.validLayout { - //self.updateButtonsLayout(layout: layout, offset: CGPoint(), transition: animated ? .animated(duration: 0.2, curve: .easeInOut) : .immediate) - } } else if self.arguments.patternEnabled != previousArguments.patternEnabled { self.patternButtonNode.isSelected = self.arguments.patternEnabled - - if let layout = self.validLayout { - self.updateButtonsLayout(layout: layout, offset: CGPoint(), transition: .immediate) - self.updateMessagesLayout(layout: layout, offset: CGPoint(), transition: .immediate) - } + } + + if let (layout, _) = self.validLayout { + self.updateButtonsLayout(layout: layout, offset: CGPoint(), transition: .immediate) + self.updateMessagesLayout(layout: layout, offset: CGPoint(), transition: .immediate) } } @@ -547,7 +647,7 @@ final class WallpaperGalleryItemNode: GalleryItemNode { return } self.validOffset = offset - if let layout = self.validLayout { + 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) @@ -555,7 +655,7 @@ final class WallpaperGalleryItemNode: GalleryItemNode { } func updateDismissTransition(_ value: CGFloat) { - if let layout = self.validLayout { + 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) } @@ -578,8 +678,20 @@ final class WallpaperGalleryItemNode: GalleryItemNode { self.setMotionEnabled(newValue.contains(.motion), animated: false) self.motionButtonNode.isSelected = newValue.contains(.motion) + + if let (layout, _) = self.validLayout { + self.updateButtonsLayout(layout: layout, offset: CGPoint(), transition: .immediate) + } } } + + var colors: [UInt32]? { + return self.calculateGradientColors()?.map({ $0.rgb }) + } + + func updateIsColorsPanelActive(_ value: Bool, animated: Bool) { + self.colorsButtonNode.setSelected(value, animated: false) + } @objc func toggleBlur() { let value = !self.blurButtonNode.isSelected @@ -591,7 +703,7 @@ final class WallpaperGalleryItemNode: GalleryItemNode { let blurRadius: CGFloat = 45.0 var animated = animated - if animated, let layout = self.validLayout { + if animated, let (layout, _) = self.validLayout { animated = min(layout.size.width, layout.size.height) > 321.0 } else { animated = false @@ -637,6 +749,10 @@ final class WallpaperGalleryItemNode: GalleryItemNode { let value = !self.motionButtonNode.isSelected self.motionButtonNode.setSelected(value, animated: true) self.setMotionEnabled(value, animated: true) + + if let (layout, navigationBarHeight) = self.validLayout { + self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .animated(duration: 0.2, curve: .easeInOut)) + } } var isPatternEnabled: Bool { @@ -644,19 +760,79 @@ final class WallpaperGalleryItemNode: GalleryItemNode { } @objc private func togglePattern() { + guard let initialWallpaper = self.initialWallpaper else { + return + } + let value = !self.patternButtonNode.isSelected self.patternButtonNode.setSelected(value, animated: false) - self.requestPatternPanel?(value) + self.requestPatternPanel?(value, initialWallpaper) + } + + func calculateGradientColors() -> [UIColor]? { + guard let entry = self.entry else { + return nil + } + switch entry { + case let .wallpaper(wallpaper, _): + switch wallpaper { + case let .file(_, _, _, _, isPattern, _, _, _, settings): + if isPattern { + if settings.colors.isEmpty { + return nil + } else { + return settings.colors.map(UIColor.init(rgb:)) + } + } else { + return nil + } + case let .gradient(_, colors, _): + return colors.map(UIColor.init(rgb:)) + case let .color(color): + return [UIColor(rgb: color)] + default: + return nil + } + default: + return nil + } + } + + @objc private func toggleColors() { + guard let currentGradientColors = self.calculateGradientColors() else { + return + } + self.toggleColorsPanel?(currentGradientColors) + } + + @objc private func togglePlay() { + guard let entry = self.entry, case let .wallpaper(wallpaper, _) = entry else { + return + } + switch wallpaper { + case let .gradient(_, colors, settings): + if colors.count >= 3 { + self.nativeNode.animateEvent(transition: .animated(duration: 0.5, curve: .spring)) + } else { + let rotation = settings.rotation ?? 0 + self.requestRotateGradient?((rotation + 90) % 360) + } + case let .file(file): + if file.isPattern { + if file.settings.colors.count >= 3 { + self.nativeNode.animateEvent(transition: .animated(duration: 0.5, curve: .spring)) + } else { + let rotation = file.settings.rotation ?? 0 + self.requestRotateGradient?((rotation + 90) % 360) + } + } + default: + break + } } private func preparePatternEditing() { - if let entry = self.entry, case let .wallpaper(wallpaper, _) = entry, case let .file(file) = wallpaper { - 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() - } } func setMotionEnabled(_ enabled: Bool, animated: Bool) { @@ -710,19 +886,25 @@ final class WallpaperGalleryItemNode: GalleryItemNode { let patternButtonSize = self.patternButtonNode.measure(layout.size) let blurButtonSize = self.blurButtonNode.measure(layout.size) let motionButtonSize = self.motionButtonNode.measure(layout.size) + let colorsButtonSize = self.colorsButtonNode.measure(layout.size) + let playButtonSize = CGSize(width: 48.0, height: 48.0) let maxButtonWidth = max(patternButtonSize.width, max(blurButtonSize.width, motionButtonSize.width)) let buttonSize = CGSize(width: maxButtonWidth, height: 30.0) let alpha = 1.0 - min(1.0, max(0.0, abs(offset.y) / 50.0)) - var additionalYOffset: CGFloat = 0.0 - if self.patternButtonNode.isSelected { + let additionalYOffset: CGFloat = 0.0 + /*if self.patternButtonNode.isSelected { additionalYOffset = -235.0 - } + } else if self.colorsButtonNode.isSelected { + additionalYOffset = -235.0 + }*/ + + let buttonSpacing: CGFloat = 18.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) + let leftButtonFrame = CGRect(origin: CGPoint(x: floor(layout.size.width / 2.0 - buttonSize.width - buttonSpacing) + offset.x, y: layout.size.height - 49.0 - layout.intrinsicInsets.bottom - 54.0 + offset.y + additionalYOffset), size: buttonSize) let centerButtonFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - buttonSize.width) / 2.0) + offset.x, y: layout.size.height - 49.0 - layout.intrinsicInsets.bottom - 54.0 + offset.y + additionalYOffset), size: buttonSize) - let rightButtonFrame = CGRect(origin: CGPoint(x: ceil(layout.size.width / 2.0 + 10.0) + offset.x, y: layout.size.height - 49.0 - layout.intrinsicInsets.bottom - 54.0 + offset.y + additionalYOffset), size: buttonSize) + let rightButtonFrame = CGRect(origin: CGPoint(x: ceil(layout.size.width / 2.0 + buttonSpacing) + offset.x, y: layout.size.height - 49.0 - layout.intrinsicInsets.bottom - 54.0 + offset.y + additionalYOffset), size: buttonSize) var patternAlpha: CGFloat = 0.0 var patternFrame = centerButtonFrame @@ -732,6 +914,14 @@ final class WallpaperGalleryItemNode: GalleryItemNode { var motionFrame = centerButtonFrame var motionAlpha: CGFloat = 0.0 + + var colorsFrame = CGRect(origin: CGPoint(x: rightButtonFrame.maxX - colorsButtonSize.width, y: rightButtonFrame.minY), size: colorsButtonSize) + var colorsAlpha: CGFloat = 0.0 + + let playFrame = CGRect(origin: CGPoint(x: centerButtonFrame.midX - playButtonSize.width / 2.0, y: centerButtonFrame.midY - playButtonSize.height / 2.0), size: playButtonSize) + var playAlpha: CGFloat = 0.0 + + let centerOffset: CGFloat = 32.0 if let entry = self.entry { switch entry { @@ -749,35 +939,65 @@ final class WallpaperGalleryItemNode: GalleryItemNode { switch wallpaper { case .builtin: motionAlpha = 1.0 + motionFrame = centerButtonFrame case .color: + motionAlpha = 0.0 patternAlpha = 1.0 - if self.patternButtonNode.isSelected { - patternFrame = leftButtonFrame - motionAlpha = 1.0 - motionFrame = rightButtonFrame - } + + patternFrame = leftButtonFrame + playAlpha = 0.0 + + colorsAlpha = 1.0 case .image: blurAlpha = 1.0 blurFrame = leftButtonFrame motionAlpha = 1.0 motionFrame = rightButtonFrame - case .gradient: - motionAlpha = 1.0 + case let .gradient(_, colors, _): + motionAlpha = 0.0 + patternAlpha = 1.0 + + if colors.count >= 2 { + playAlpha = 1.0 + patternFrame = leftButtonFrame.offsetBy(dx: -centerOffset, dy: 0.0) + colorsFrame = colorsFrame.offsetBy(dx: centerOffset, dy: 0.0) + } else { + playAlpha = 0.0 + patternFrame = leftButtonFrame + } + + colorsAlpha = 1.0 case let .file(file): - if wallpaper.isPattern { - motionAlpha = 1.0 - if self.arguments.isColorsList { - patternAlpha = 1.0 - if self.patternButtonNode.isSelected { - patternFrame = leftButtonFrame + if file.isPattern { + motionAlpha = 0.0 + patternAlpha = 1.0 + + if file.settings.colors.count >= 2 { + playAlpha = 1.0 + patternFrame = leftButtonFrame.offsetBy(dx: -centerOffset, dy: 0.0) + colorsFrame = colorsFrame.offsetBy(dx: centerOffset, dy: 0.0) + } else { + playAlpha = 0.0 + patternFrame = leftButtonFrame + } + + colorsAlpha = 1.0 + } else { + if wallpaper.isPattern { + motionAlpha = 1.0 + if self.arguments.isColorsList { + patternAlpha = 1.0 + if self.patternButtonNode.isSelected { + patternFrame = leftButtonFrame + } + motionFrame = rightButtonFrame } + } else { + blurAlpha = 1.0 + blurFrame = leftButtonFrame + motionAlpha = 1.0 motionFrame = rightButtonFrame } - } else { - blurAlpha = 1.0 - blurFrame = leftButtonFrame - motionAlpha = 1.0 - motionFrame = rightButtonFrame } } } @@ -791,16 +1011,26 @@ final class WallpaperGalleryItemNode: GalleryItemNode { transition.updateFrame(node: self.motionButtonNode, frame: motionFrame) transition.updateAlpha(node: self.motionButtonNode, alpha: motionAlpha * alpha) + + transition.updateFrame(node: self.colorsButtonNode, frame: colorsFrame) + transition.updateAlpha(node: self.colorsButtonNode, alpha: colorsAlpha * alpha) + + transition.updateFrame(node: self.playButtonNode, frame: playFrame) + transition.updateFrame(node: self.playButtonBackgroundNode, frame: CGRect(origin: CGPoint(), size: playFrame.size)) + self.playButtonBackgroundNode.update(size: playFrame.size, cornerRadius: playFrame.size.height / 2.0, transition: transition) + transition.updateAlpha(node: self.playButtonNode, alpha: playAlpha * alpha) + transition.updateSublayerTransformScale(node: self.playButtonNode, scale: max(0.1, playAlpha)) } private func updateMessagesLayout(layout: ContainerViewLayout, offset: CGPoint, transition: ContainedViewLayoutTransition) { var bottomInset: CGFloat = 115.0 - if self.patternButtonNode.isSelected { - bottomInset = 350.0 + + if self.patternButtonNode.isSelected || self.colorsButtonNode.isSelected { + //bottomInset = 350.0 } var items: [ListViewItem] = [] - let peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: 1) + let peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt32Value(1)) let otherPeerId = self.context.account.peerId var peers = SimpleDictionary() let messages = SimpleDictionary() @@ -816,14 +1046,48 @@ final class WallpaperGalleryItemNode: GalleryItemNode { if let source = self.source { switch source { - case .wallpaper, .slug: + case .slug, .wallpaper: topMessageText = presentationData.strings.WallpaperPreview_PreviewTopText bottomMessageText = presentationData.strings.WallpaperPreview_PreviewBottomText + + var hasAnimatableGradient = false + switch currentWallpaper { + case let .file(file) where file.isPattern: + if file.settings.colors.count >= 3 { + hasAnimatableGradient = true + } + case let .gradient(_, colors, _): + if colors.count >= 3 { + hasAnimatableGradient = true + } + default: + break + } + if hasAnimatableGradient { + bottomMessageText = presentationData.strings.WallpaperPreview_PreviewBottomTextAnimatable + } case let .list(_, _, type): switch type { case .wallpapers: topMessageText = presentationData.strings.WallpaperPreview_SwipeTopText bottomMessageText = presentationData.strings.WallpaperPreview_SwipeBottomText + + var hasAnimatableGradient = false + switch currentWallpaper { + case let .file(file) where file.isPattern: + if file.settings.colors.count >= 3 { + hasAnimatableGradient = true + } + case let .gradient(_, colors, _): + if colors.count >= 3 { + hasAnimatableGradient = true + } + default: + break + } + if hasAnimatableGradient { + bottomMessageText = presentationData.strings.WallpaperPreview_PreviewBottomTextAnimatable + } case .colors: topMessageText = presentationData.strings.WallpaperPreview_SwipeColorsTopText bottomMessageText = presentationData.strings.WallpaperPreview_SwipeColorsBottomText @@ -840,14 +1104,29 @@ final class WallpaperGalleryItemNode: GalleryItemNode { 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, threadId: 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, messages: [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)) + items.append(self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: [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, backgroundNode: self.nativeNode)) let message2 = Message(stableId: 1, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: 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, messages: [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)) + items.append(self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: [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, backgroundNode: self.nativeNode)) let params = ListViewItemLayoutParams(width: layout.size.width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, availableHeight: layout.size.height) - if let _ = self.messageNodes { + if let messageNodes = self.messageNodes { + if self.validMessages != [topMessageText, bottomMessageText] { + self.validMessages = [topMessageText, bottomMessageText] + for i in 0 ..< items.count { + items[i].updateNode(async: { f in f() }, node: { return messageNodes[i] }, params: params, previousItem: i == 0 ? nil : items[i - 1], nextItem: i == (items.count - 1) ? nil : items[i + 1], animation: .None) { layout, apply in + let nodeFrame = CGRect(origin: messageNodes[i].frame.origin, size: CGSize(width: layout.size.width, height: layout.size.height)) + + messageNodes[i].contentSize = layout.contentSize + messageNodes[i].insets = layout.insets + messageNodes[i].frame = nodeFrame + + apply(ListViewItemApply(isOnScreen: true)) + } + } + } } else { + self.validMessages = [topMessageText, bottomMessageText] var messageNodes: [ListViewItemNode] = [] for i in 0 ..< items.count { var itemNode: ListViewItemNode? @@ -890,6 +1169,8 @@ final class WallpaperGalleryItemNode: GalleryItemNode { if self.cropNode.supernode == nil { self.imageNode.frame = self.wrapperNode.bounds + self.nativeNode.frame = self.wrapperNode.bounds + self.nativeNode.updateLayout(size: self.nativeNode.bounds.size, transition: .immediate) self.blurredNode.frame = self.imageNode.frame } else { self.cropNode.frame = self.wrapperNode.bounds @@ -903,20 +1184,21 @@ final class WallpaperGalleryItemNode: GalleryItemNode { self.blurredNode.frame = self.imageNode.bounds } - var additionalYOffset: CGFloat = 0.0 - if self.patternButtonNode.isSelected { - additionalYOffset = -190.0 - } + let additionalYOffset: CGFloat = 0.0 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 + self.validLayout = (layout, navigationBarHeight) } override func visibilityUpdated(isVisible: Bool) { super.visibilityUpdated(isVisible: isVisible) } + + func animateWallpaperAppeared() { + self.nativeNode.animateEvent(transition: .animated(duration: 2.0, curve: .spring), extendAnimation: true) + } } diff --git a/submodules/SettingsUI/Sources/Themes/WallpaperGalleryToolbarNode.swift b/submodules/SettingsUI/Sources/Themes/WallpaperGalleryToolbarNode.swift index 97e93331b7..ae64944748 100644 --- a/submodules/SettingsUI/Sources/Themes/WallpaperGalleryToolbarNode.swift +++ b/submodules/SettingsUI/Sources/Themes/WallpaperGalleryToolbarNode.swift @@ -35,6 +35,7 @@ final class WallpaperGalleryToolbarNode: ASDisplayNode { private let cancelHighlightBackgroundNode = ASDisplayNode() private let doneButton = HighlightTrackingButtonNode() private let doneHighlightBackgroundNode = ASDisplayNode() + private let backgroundNode = NavigationBackgroundNode(color: .clear) private let separatorNode = ASDisplayNode() private let topSeparatorNode = ASDisplayNode() @@ -51,7 +52,8 @@ final class WallpaperGalleryToolbarNode: ASDisplayNode { self.doneHighlightBackgroundNode.alpha = 0.0 super.init() - + + self.addSubnode(self.backgroundNode) self.addSubnode(self.cancelHighlightBackgroundNode) self.addSubnode(self.cancelButton) self.addSubnode(self.doneHighlightBackgroundNode) @@ -96,7 +98,7 @@ final class WallpaperGalleryToolbarNode: ASDisplayNode { func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) { self.theme = theme - self.backgroundColor = theme.rootController.tabBar.backgroundColor + self.backgroundNode.updateColor(color: theme.rootController.tabBar.backgroundColor, transition: .immediate) self.separatorNode.backgroundColor = theme.rootController.tabBar.separatorColor self.topSeparatorNode.backgroundColor = theme.rootController.tabBar.separatorColor self.cancelHighlightBackgroundNode.backgroundColor = theme.list.itemHighlightedBackgroundColor @@ -132,6 +134,8 @@ final class WallpaperGalleryToolbarNode: ASDisplayNode { 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)) + self.backgroundNode.frame = CGRect(origin: CGPoint(), size: size) + self.backgroundNode.update(size: CGSize(width: size.width, height: size.height + layout.intrinsicInsets.bottom), transition: .immediate) } @objc func cancelPressed() { diff --git a/submodules/SettingsUI/Sources/Themes/WallpaperOptionButtonNode.swift b/submodules/SettingsUI/Sources/Themes/WallpaperOptionButtonNode.swift index b57c724bfc..b0ebf964cb 100644 --- a/submodules/SettingsUI/Sources/Themes/WallpaperOptionButtonNode.swift +++ b/submodules/SettingsUI/Sources/Themes/WallpaperOptionButtonNode.swift @@ -9,10 +9,33 @@ import CheckNode enum WallpaperOptionButtonValue { case check(Bool) case color(Bool, UIColor) + case colors(Bool, [UIColor]) +} + +private func generateColorsImage(diameter: CGFloat, colors: [UIColor]) -> UIImage? { + return generateImage(CGSize(width: diameter, height: diameter), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + + if !colors.isEmpty { + let center = CGPoint(x: size.width / 2.0, y: size.height / 2.0) + var startAngle = -CGFloat.pi * 0.5 + for i in 0 ..< colors.count { + context.setFillColor(colors[i].cgColor) + + let endAngle = startAngle + 2.0 * CGFloat.pi * (1.0 / CGFloat(colors.count)) + + context.move(to: center) + context.addArc(center: center, radius: size.width / 2.0, startAngle: startAngle, endAngle: endAngle, clockwise: false) + context.fillPath() + + startAngle = endAngle + } + } + }) } final class WallpaperOptionButtonNode: HighlightTrackingButtonNode { - private let backgroundNode: ASDisplayNode + private let backgroundNode: NavigationBackgroundNode private let checkNode: CheckNode private let colorNode: ASImageNode private let textNode: ASTextNode @@ -23,16 +46,18 @@ final class WallpaperOptionButtonNode: HighlightTrackingButtonNode { override var isSelected: Bool { get { switch self._value { - case let .check(selected), let .color(selected, _): - return selected + case let .check(selected), let .color(selected, _), let .colors(selected, _): + return selected } } set { switch self._value { - case .check: - self._value = .check(newValue) - case let .color(_, color): - self._value = .color(newValue, color) + case .check: + self._value = .check(newValue) + case let .color(_, color): + self._value = .color(newValue, color) + case let .colors(_, colors): + self._value = .colors(newValue, colors) } self.checkNode.setSelected(newValue, animated: false) } @@ -41,8 +66,7 @@ final class WallpaperOptionButtonNode: HighlightTrackingButtonNode { init(title: String, value: WallpaperOptionButtonValue) { self._value = value - self.backgroundNode = ASDisplayNode() - self.backgroundNode.backgroundColor = UIColor(rgb: 0x000000, alpha: 0.3) + self.backgroundNode = NavigationBackgroundNode(color: UIColor(rgb: 0x000000, alpha: 0.3)) self.backgroundNode.cornerRadius = 14.0 self.checkNode = CheckNode(theme: CheckNodeTheme(backgroundColor: .white, strokeColor: .clear, borderColor: .white, overlayBorder: false, hasInset: false, hasShadow: false, borderWidth: 1.5)) @@ -56,14 +80,18 @@ final class WallpaperOptionButtonNode: HighlightTrackingButtonNode { super.init() switch value { - case let .check(selected): - self.checkNode.isHidden = false - self.colorNode.isHidden = true - self.checkNode.selected = selected - case let .color(_, color): - self.checkNode.isHidden = true - self.colorNode.isHidden = false - self.colorNode.image = generateFilledCircleImage(diameter: 18.0, color: color) + case let .check(selected): + self.checkNode.isHidden = false + self.colorNode.isHidden = true + self.checkNode.selected = selected + case let .color(_, color): + self.checkNode.isHidden = true + self.colorNode.isHidden = false + self.colorNode.image = generateFilledCircleImage(diameter: 18.0, color: color) + case let .colors(_, colors): + self.checkNode.isHidden = true + self.colorNode.isHidden = false + self.colorNode.image = generateColorsImage(diameter: 18.0, colors: colors) } self.addSubnode(self.backgroundNode) @@ -104,7 +132,7 @@ final class WallpaperOptionButtonNode: HighlightTrackingButtonNode { var buttonColor: UIColor = UIColor(rgb: 0x000000, alpha: 0.3) { didSet { - self.backgroundNode.backgroundColor = self.buttonColor + self.backgroundNode.updateColor(color: self.buttonColor, transition: .immediate) } } @@ -129,9 +157,50 @@ final class WallpaperOptionButtonNode: HighlightTrackingButtonNode { } } } + + var colors: [UIColor]? { + get { + switch self._value { + case let .colors(_, colors): + return colors + default: + return nil + } + } + set { + if let colors = newValue { + switch self._value { + case let .colors(selected, current): + if current.count == colors.count { + var updated = false + for i in 0 ..< current.count { + if !current[i].isEqual(colors[i]) { + updated = true + break + } + } + if !updated { + return + } + } + self._value = .colors(selected, colors) + self.colorNode.image = generateColorsImage(diameter: 18.0, colors: colors) + default: + break + } + } + } + } func setSelected(_ selected: Bool, animated: Bool = false) { - self._value = .check(selected) + switch self._value { + case .check: + self._value = .check(selected) + case let .color(_, color): + self._value = .color(selected, color) + case let .colors(_, colors): + self._value = .colors(selected, colors) + } self.checkNode.setSelected(selected, animated: animated) } @@ -152,10 +221,11 @@ final class WallpaperOptionButtonNode: HighlightTrackingButtonNode { override func layout() { super.layout() - + self.backgroundNode.frame = self.bounds + self.backgroundNode.update(size: self.backgroundNode.bounds.size, cornerRadius: 15.0, transition: .immediate) - guard let textSize = self.textSize else { + guard let _ = self.textSize else { return } diff --git a/submodules/SettingsUI/Sources/Themes/WallpaperPatternPanelNode.swift b/submodules/SettingsUI/Sources/Themes/WallpaperPatternPanelNode.swift index 3e00ffa2ce..fa972c2292 100644 --- a/submodules/SettingsUI/Sources/Themes/WallpaperPatternPanelNode.swift +++ b/submodules/SettingsUI/Sources/Themes/WallpaperPatternPanelNode.swift @@ -9,10 +9,34 @@ import TelegramPresentationData import LegacyComponents import AccountContext import MergeLists +import Postbox private let itemSize = CGSize(width: 88.0, height: 88.0) private let inset: CGFloat = 12.0 +private func intensityToSliderValue(_ value: Int32, allowDark: Bool) -> CGFloat { + if allowDark { + if value < 0 { + return max(0.0, min(100.0, CGFloat(abs(value)))) + } else { + return 100.0 + max(0.0, min(100.0, CGFloat(value))) + } + } else { + return CGFloat(max(value, 0)) * 2.0 + } +} + +private func sliderValueToIntensity(_ value: CGFloat, allowDark: Bool) -> Int32 { + if allowDark { + if value < 100.0 { + return -Int32(max(1.0, value)) + } else { + return Int32(value - 100.0) + } + } else { + return Int32(value / 2.0) + } +} private struct WallpaperPatternEntry: Comparable, Identifiable { let index: Int @@ -105,7 +129,7 @@ private final class WallpaperPatternItemNode : ListViewItemNode { var item: WallpaperPatternItem? init() { - self.wallpaperNode = SettingsThemeWallpaperNode() + self.wallpaperNode = SettingsThemeWallpaperNode(displayLoading: true) super.init(layerBacked: false, dynamicBounce: false, rotated: false, seeThrough: false) @@ -165,7 +189,7 @@ final class WallpaperPatternPanelNode: ASDisplayNode { private let context: AccountContext private var theme: PresentationTheme - private let backgroundNode: ASDisplayNode + private let backgroundNode: NavigationBackgroundNode private let topSeparatorNode: ASDisplayNode let scrollNode: ASScrollNode @@ -189,10 +213,22 @@ final class WallpaperPatternPanelNode: ASDisplayNode { } } - var backgroundColors: (UIColor, UIColor?, Int32?)? = nil { + var backgroundColors: ([UInt32], Int32?, Int32?)? = nil { didSet { - if oldValue?.0.rgb != self.backgroundColors?.0.rgb || oldValue?.1?.rgb != self.backgroundColors?.1?.rgb - || oldValue?.2 != self.backgroundColors?.2 { + var updated = false + if oldValue?.0 != self.backgroundColors?.0 || oldValue?.1 != self.backgroundColors?.1 { + updated = true + } else if oldValue?.2 != self.backgroundColors?.2 { + if let oldIntensity = oldValue?.2, let newIntensity = self.backgroundColors?.2 { + if (oldIntensity < 0) != (newIntensity < 0) { + updated = true + } + } else if (oldValue?.2 != nil) != (self.backgroundColors?.2 != nil) { + updated = true + } + } + + if updated { self.updateWallpapers() } } @@ -202,12 +238,14 @@ final class WallpaperPatternPanelNode: ASDisplayNode { var patternChanged: ((TelegramWallpaper?, Int32?, Bool) -> Void)? + private let allowDark: Bool + init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings) { self.context = context self.theme = theme + self.allowDark = theme.overallDarkAppearance - self.backgroundNode = ASDisplayNode() - self.backgroundNode.backgroundColor = self.theme.chat.inputPanel.panelBackgroundColor + self.backgroundNode = NavigationBackgroundNode(color: theme.chat.inputPanel.panelBackgroundColor) self.topSeparatorNode = ASDisplayNode() self.topSeparatorNode.backgroundColor = self.theme.chat.inputPanel.panelSeparatorColor @@ -230,12 +268,21 @@ final class WallpaperPatternPanelNode: ASDisplayNode { self.addSubnode(self.titleNode) self.addSubnode(self.labelNode) - self.disposable = ((telegramWallpapers(postbox: context.account.postbox, network: context.account.network) - |> map { wallpapers in + |> map { wallpapers -> [TelegramWallpaper] in + var existingIds = Set() + return wallpapers.filter { wallpaper in if case let .file(file) = wallpaper, wallpaper.isPattern, file.file.mimeType != "image/webp" { - return true + if file.id == 0 { + return true + } + if existingIds.contains(file.file.fileId) { + return false + } else { + existingIds.insert(file.file.fileId) + return true + } } else { return false } @@ -261,16 +308,25 @@ final class WallpaperPatternPanelNode: ASDisplayNode { self.scrollNode.view.alwaysBounceHorizontal = true let sliderView = TGPhotoEditorSliderView() + sliderView.disableSnapToPositions = true sliderView.trackCornerRadius = 1.0 sliderView.lineSize = 2.0 - sliderView.minimumValue = 0.0 sliderView.startValue = 0.0 - sliderView.maximumValue = 100.0 - sliderView.value = 40.0 + sliderView.minimumValue = 0.0 + sliderView.maximumValue = 200.0 + if self.allowDark { + sliderView.positionsCount = 3 + } + sliderView.useLinesForPositions = true + sliderView.value = intensityToSliderValue(50, allowDark: self.allowDark) sliderView.disablesInteractiveTransitionGestureRecognizer = true sliderView.backgroundColor = .clear sliderView.backColor = self.theme.list.disclosureArrowColor - sliderView.trackColor = self.theme.list.itemAccentColor + if self.allowDark { + sliderView.trackColor = self.theme.list.disclosureArrowColor + } else { + sliderView.trackColor = self.theme.list.itemAccentColor + } self.view.addSubview(sliderView) sliderView.addTarget(self, action: #selector(self.sliderValueChanged), for: .valueChanged) @@ -286,26 +342,35 @@ final class WallpaperPatternPanelNode: ASDisplayNode { node.removeFromSupernode() } - let backgroundColors = self.backgroundColors ?? (UIColor(rgb: 0xd6e2ee), nil, nil) + let backgroundColors = self.backgroundColors ?? ([0xd6e2ee], nil, nil) + let intensity: Int32 = backgroundColors.2.flatMap { value in + if value < 0 { + return -80 + } else { + return 80 + } + } ?? 80 var selectedFileId: Int64? + var selectedSlug: String? if let currentWallpaper = self.currentWallpaper, case let .file(file) = currentWallpaper { selectedFileId = file.id + selectedSlug = file.slug } for wallpaper in self.wallpapers { - let node = SettingsThemeWallpaperNode(overlayBackgroundColor: self.serviceBackgroundColor.withAlphaComponent(0.4)) + let node = SettingsThemeWallpaperNode(displayLoading: true, 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) + let settings = WallpaperSettings(colors: backgroundColors.0, intensity: intensity, rotation: backgroundColors.1) 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 { + if case let .file(file) = wallpaper, (file.id == selectedFileId || file.slug == selectedSlug) { selected = true } @@ -314,7 +379,7 @@ final class WallpaperPatternPanelNode: ASDisplayNode { if let strongSelf = self { strongSelf.currentWallpaper = updatedWallpaper if let sliderView = strongSelf.sliderView { - strongSelf.patternChanged?(updatedWallpaper, Int32(sliderView.value), false) + strongSelf.patternChanged?(updatedWallpaper, sliderValueToIntensity(sliderView.value, allowDark: strongSelf.allowDark), false) } if let subnodes = strongSelf.scrollNode.subnodes { for case let subnode as SettingsThemeWallpaperNode in subnodes { @@ -337,11 +402,15 @@ final class WallpaperPatternPanelNode: ASDisplayNode { func updateTheme(_ theme: PresentationTheme) { self.theme = theme - self.backgroundNode.backgroundColor = self.theme.chat.inputPanel.panelBackgroundColor + self.backgroundNode.updateColor(color: self.theme.chat.inputPanel.panelBackgroundColor, transition: .immediate) self.topSeparatorNode.backgroundColor = self.theme.chat.inputPanel.panelSeparatorColor self.sliderView?.backColor = self.theme.list.disclosureArrowColor - self.sliderView?.trackColor = self.theme.list.itemAccentColor + if self.allowDark { + self.sliderView?.trackColor = self.theme.list.disclosureArrowColor + } else { + 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) @@ -356,12 +425,19 @@ final class WallpaperPatternPanelNode: ASDisplayNode { } if let wallpaper = self.currentWallpaper { - self.patternChanged?(wallpaper, Int32(sliderView.value), sliderView.isTracking) + self.patternChanged?(wallpaper, sliderValueToIntensity(sliderView.value, allowDark: self.allowDark), sliderView.isTracking) } } func didAppear(initialWallpaper: TelegramWallpaper? = nil, intensity: Int32? = nil) { - var wallpaper = initialWallpaper ?? self.wallpapers.first + let wallpaper: TelegramWallpaper? + + switch initialWallpaper { + case let .file(id, accessHash, isCreator, isDefault, isPattern, isDark, slug, file, _): + wallpaper = .file(id: id, accessHash: accessHash, isCreator: isCreator, isDefault: isDefault, isPattern: isPattern, isDark: isDark, slug: slug, file: file, settings: self.wallpapers[0].settings ?? WallpaperSettings()) + default: + wallpaper = self.wallpapers.first + } if let wallpaper = wallpaper { var selectedFileId: Int64? @@ -370,7 +446,7 @@ final class WallpaperPatternPanelNode: ASDisplayNode { } self.currentWallpaper = wallpaper - self.sliderView?.value = CGFloat(intensity ?? 50) + self.sliderView?.value = intensity.flatMap { intensityToSliderValue($0, allowDark: self.allowDark) } ?? intensityToSliderValue(50, allowDark: self.allowDark) self.scrollNode.view.contentOffset = CGPoint() @@ -386,8 +462,8 @@ final class WallpaperPatternPanelNode: ASDisplayNode { } } - if initialWallpaper == nil, let wallpaper = self.currentWallpaper, let sliderView = self.sliderView { - self.patternChanged?(wallpaper, Int32(sliderView.value), false) + if let wallpaper = self.currentWallpaper, let sliderView = self.sliderView { + self.patternChanged?(wallpaper, sliderValueToIntensity(sliderView.value, allowDark: self.allowDark), false) } if let selectedNode = selectedNode { @@ -418,6 +494,7 @@ final class WallpaperPatternPanelNode: ASDisplayNode { self.validLayout = size transition.updateFrame(node: self.backgroundNode, frame: CGRect(x: 0.0, y: 0.0, width: size.width, height: size.height)) + self.backgroundNode.update(size: self.backgroundNode.bounds.size, transition: transition) transition.updateFrame(node: self.topSeparatorNode, frame: CGRect(x: 0.0, y: 0.0, width: size.width, height: UIScreenPixel)) let titleSize = self.titleNode.updateLayout(self.bounds.size) diff --git a/submodules/SettingsUI/Sources/UsernameSetupController.swift b/submodules/SettingsUI/Sources/UsernameSetupController.swift index cede57928c..26789fa576 100644 --- a/submodules/SettingsUI/Sources/UsernameSetupController.swift +++ b/submodules/SettingsUI/Sources/UsernameSetupController.swift @@ -260,7 +260,7 @@ public func usernameSetupController(context: AccountContext) -> ViewController { return state.withUpdatedEditingPublicLinkText(text) } - checkAddressNameDisposable.set((validateAddressNameInteractive(account: context.account, domain: .account, name: text) + checkAddressNameDisposable.set((context.engine.peers.validateAddressNameInteractive(domain: .account, name: text) |> deliverOnMainQueue).start(next: { result in updateState { state in return state.withUpdatedAddressNameValidationStatus(result) @@ -325,7 +325,7 @@ public func usernameSetupController(context: AccountContext) -> ViewController { } if let updatedAddressNameValue = updatedAddressNameValue { - updateAddressNameDisposable.set((updateAddressName(account: context.account, domain: .account, name: updatedAddressNameValue.isEmpty ? nil : updatedAddressNameValue) + updateAddressNameDisposable.set((context.engine.peers.updateAddressName(domain: .account, name: updatedAddressNameValue.isEmpty ? nil : updatedAddressNameValue) |> deliverOnMainQueue).start(error: { _ in updateState { state in return state.withUpdatedUpdatingAddressName(false) diff --git a/submodules/ShareController/BUILD b/submodules/ShareController/BUILD index 0ea81b3ebf..d86123a68d 100644 --- a/submodules/ShareController/BUILD +++ b/submodules/ShareController/BUILD @@ -27,6 +27,7 @@ swift_library( "//submodules/TelegramIntents:TelegramIntents", "//submodules/AccountContext:AccountContext", "//submodules/SegmentedControlNode:SegmentedControlNode", + "//submodules/WallpaperBackgroundNode:WallpaperBackgroundNode", ], visibility = [ "//visibility:public", diff --git a/submodules/ShareController/Sources/ShareController.swift b/submodules/ShareController/Sources/ShareController.swift index 76e8adbeac..b2ebd337a7 100644 --- a/submodules/ShareController/Sources/ShareController.swift +++ b/submodules/ShareController/Sources/ShareController.swift @@ -16,6 +16,7 @@ import UrlEscaping import StickerResources import SaveToCameraRoll import TelegramStringFormatting +import WallpaperBackgroundNode public struct ShareControllerAction { let title: String @@ -299,7 +300,7 @@ public final class ShareController: ViewController { private var currentAccount: Account private var presentationData: PresentationData private var presentationDataDisposable: Disposable? - private let forcedTheme: PresentationTheme? + private let forceTheme: PresentationTheme? private let externalShare: Bool private let immediateExternalShare: Bool @@ -326,12 +327,14 @@ public final class ShareController: ViewController { } } } + + public var debugAction: (() -> Void)? - public convenience init(context: AccountContext, subject: ShareControllerSubject, presetText: String? = nil, preferredAction: ShareControllerPreferredAction = .default, showInChat: ((Message) -> Void)? = nil, fromForeignApp: Bool = false, segmentedValues: [ShareControllerSegmentedValue]? = nil, externalShare: Bool = true, immediateExternalShare: Bool = false, switchableAccounts: [AccountWithInfo] = [], immediatePeerId: PeerId? = nil, forcedTheme: PresentationTheme? = nil, forcedActionTitle: String? = nil) { - self.init(sharedContext: context.sharedContext, currentContext: context, subject: subject, presetText: presetText, preferredAction: preferredAction, showInChat: showInChat, fromForeignApp: fromForeignApp, segmentedValues: segmentedValues, externalShare: externalShare, immediateExternalShare: immediateExternalShare, switchableAccounts: switchableAccounts, immediatePeerId: immediatePeerId, forcedTheme: forcedTheme, forcedActionTitle: forcedActionTitle) + public convenience init(context: AccountContext, subject: ShareControllerSubject, presetText: String? = nil, preferredAction: ShareControllerPreferredAction = .default, showInChat: ((Message) -> Void)? = nil, fromForeignApp: Bool = false, segmentedValues: [ShareControllerSegmentedValue]? = nil, externalShare: Bool = true, immediateExternalShare: Bool = false, switchableAccounts: [AccountWithInfo] = [], immediatePeerId: PeerId? = nil, forceTheme: PresentationTheme? = nil, forcedActionTitle: String? = nil) { + self.init(sharedContext: context.sharedContext, currentContext: context, subject: subject, presetText: presetText, preferredAction: preferredAction, showInChat: showInChat, fromForeignApp: fromForeignApp, segmentedValues: segmentedValues, externalShare: externalShare, immediateExternalShare: immediateExternalShare, switchableAccounts: switchableAccounts, immediatePeerId: immediatePeerId, forceTheme: forceTheme, forcedActionTitle: forcedActionTitle) } - public init(sharedContext: SharedAccountContext, currentContext: AccountContext, subject: ShareControllerSubject, presetText: String? = nil, preferredAction: ShareControllerPreferredAction = .default, showInChat: ((Message) -> Void)? = nil, fromForeignApp: Bool = false, segmentedValues: [ShareControllerSegmentedValue]? = nil, externalShare: Bool = true, immediateExternalShare: Bool = false, switchableAccounts: [AccountWithInfo] = [], immediatePeerId: PeerId? = nil, forcedTheme: PresentationTheme? = nil, forcedActionTitle: String? = nil) { + public init(sharedContext: SharedAccountContext, currentContext: AccountContext, subject: ShareControllerSubject, presetText: String? = nil, preferredAction: ShareControllerPreferredAction = .default, showInChat: ((Message) -> Void)? = nil, fromForeignApp: Bool = false, segmentedValues: [ShareControllerSegmentedValue]? = nil, externalShare: Bool = true, immediateExternalShare: Bool = false, switchableAccounts: [AccountWithInfo] = [], immediatePeerId: PeerId? = nil, forceTheme: PresentationTheme? = nil, forcedActionTitle: String? = nil) { self.sharedContext = sharedContext self.currentContext = currentContext self.currentAccount = currentContext.account @@ -343,11 +346,11 @@ public final class ShareController: ViewController { self.immediatePeerId = immediatePeerId self.fromForeignApp = fromForeignApp self.segmentedValues = segmentedValues - self.forcedTheme = forcedTheme + self.forceTheme = forceTheme self.presentationData = self.sharedContext.currentPresentationData.with { $0 } - if let forcedTheme = self.forcedTheme { - self.presentationData = self.presentationData.withUpdated(theme: forcedTheme) + if let forceTheme = self.forceTheme { + self.presentationData = self.presentationData.withUpdated(theme: forceTheme) } super.init(navigationBarPresentationData: nil) @@ -432,7 +435,7 @@ public final class ShareController: ViewController { guard let strongSelf = self else { return } - let _ = (exportMessageLink(account: strongSelf.currentAccount, peerId: chatPeer.id, messageId: message.id) + let _ = (TelegramEngine(account: strongSelf.currentAccount).messages.exportMessageLink(peerId: chatPeer.id, messageId: message.id) |> map { result -> String? in return result } @@ -487,17 +490,17 @@ public final class ShareController: ViewController { return } 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, fromForeignApp: self.fromForeignApp, forcedTheme: self.forcedTheme, segmentedValues: self.segmentedValues) + }, externalShare: self.externalShare, immediateExternalShare: self.immediateExternalShare, immediatePeerId: self.immediatePeerId, fromForeignApp: self.fromForeignApp, forceTheme: self.forceTheme, segmentedValues: self.segmentedValues) self.controllerNode.completed = self.completed self.controllerNode.dismiss = { [weak self] shared in - self?.presentingViewController?.dismiss(animated: false, completion: nil) self?.dismissed?(shared) + self?.presentingViewController?.dismiss(animated: false, completion: nil) } self.controllerNode.cancel = { [weak self] in self?.controllerNode.view.endEditing(true) self?.controllerNode.animateOut(shared: false, completion: { - self?.presentingViewController?.dismiss(animated: false, completion: nil) self?.dismissed?(false) + self?.presentingViewController?.dismiss(animated: false, completion: nil) }) } self.controllerNode.share = { [weak self] text, peerIds in @@ -517,9 +520,9 @@ public final class ShareController: ViewController { for peerId in peerIds { var messages: [EnqueueMessage] = [] if !text.isEmpty { - messages.append(.message(text: url + "\n\n" + text, attributes: [], mediaReference: nil, replyToMessageId: nil, localGroupingKey: nil)) + messages.append(.message(text: url + "\n\n" + text, attributes: [], mediaReference: nil, replyToMessageId: nil, localGroupingKey: nil, correlationId: nil)) } else { - messages.append(.message(text: url, attributes: [], mediaReference: nil, replyToMessageId: nil, localGroupingKey: nil)) + messages.append(.message(text: url, attributes: [], mediaReference: nil, replyToMessageId: nil, localGroupingKey: nil, correlationId: nil)) } shareSignals.append(enqueueMessages(account: strongSelf.currentAccount, peerId: peerId, messages: messages)) } @@ -527,58 +530,58 @@ public final class ShareController: ViewController { for peerId in peerIds { var messages: [EnqueueMessage] = [] if !text.isEmpty { - messages.append(.message(text: text, attributes: [], mediaReference: nil, replyToMessageId: nil, localGroupingKey: nil)) + messages.append(.message(text: text, attributes: [], mediaReference: nil, replyToMessageId: nil, localGroupingKey: nil, correlationId: nil)) } - messages.append(.message(text: string, attributes: [], mediaReference: nil, replyToMessageId: nil, localGroupingKey: nil)) + messages.append(.message(text: string, attributes: [], mediaReference: nil, replyToMessageId: nil, localGroupingKey: nil, correlationId: nil)) shareSignals.append(enqueueMessages(account: strongSelf.currentAccount, peerId: peerId, messages: messages)) } case let .quote(string, url): for peerId in peerIds { var messages: [EnqueueMessage] = [] if !text.isEmpty { - messages.append(.message(text: text, attributes: [], mediaReference: nil, replyToMessageId: nil, localGroupingKey: nil)) + messages.append(.message(text: text, attributes: [], mediaReference: nil, replyToMessageId: nil, localGroupingKey: nil, correlationId: nil)) } let attributedText = NSMutableAttributedString(string: string, attributes: [ChatTextInputAttributes.italic: true as NSNumber]) attributedText.append(NSAttributedString(string: "\n\n\(url)")) let entities = generateChatInputTextEntities(attributedText) - messages.append(.message(text: attributedText.string, attributes: [TextEntitiesMessageAttribute(entities: entities)], mediaReference: nil, replyToMessageId: nil, localGroupingKey: nil)) + messages.append(.message(text: attributedText.string, attributes: [TextEntitiesMessageAttribute(entities: entities)], mediaReference: nil, replyToMessageId: nil, localGroupingKey: nil, correlationId: nil)) shareSignals.append(enqueueMessages(account: strongSelf.currentAccount, peerId: peerId, messages: messages)) } case let .image(representations): for peerId in peerIds { var messages: [EnqueueMessage] = [] if !text.isEmpty { - messages.append(.message(text: text, attributes: [], mediaReference: nil, replyToMessageId: nil, localGroupingKey: nil)) + messages.append(.message(text: text, attributes: [], mediaReference: nil, replyToMessageId: nil, localGroupingKey: nil, correlationId: 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)) + messages.append(.message(text: "", attributes: [], mediaReference: .standalone(media: TelegramMediaImage(imageId: MediaId(namespace: Namespaces.Media.LocalImage, id: Int64.random(in: Int64.min ... Int64.max)), representations: representations.map({ $0.representation }), immediateThumbnailData: nil, reference: nil, partialReference: nil, flags: [])), replyToMessageId: nil, localGroupingKey: nil, correlationId: nil)) shareSignals.append(enqueueMessages(account: strongSelf.currentAccount, peerId: peerId, messages: messages)) } case let .media(mediaReference): for peerId in peerIds { var messages: [EnqueueMessage] = [] if !text.isEmpty { - messages.append(.message(text: text, attributes: [], mediaReference: nil, replyToMessageId: nil, localGroupingKey: nil)) + messages.append(.message(text: text, attributes: [], mediaReference: nil, replyToMessageId: nil, localGroupingKey: nil, correlationId: nil)) } - messages.append(.message(text: "", attributes: [], mediaReference: mediaReference, replyToMessageId: nil, localGroupingKey: nil)) + messages.append(.message(text: "", attributes: [], mediaReference: mediaReference, replyToMessageId: nil, localGroupingKey: nil, correlationId: nil)) shareSignals.append(enqueueMessages(account: strongSelf.currentAccount, peerId: peerId, messages: messages)) } case let .mapMedia(media): for peerId in peerIds { var messages: [EnqueueMessage] = [] if !text.isEmpty { - messages.append(.message(text: text, attributes: [], mediaReference: nil, replyToMessageId: nil, localGroupingKey: nil)) + messages.append(.message(text: text, attributes: [], mediaReference: nil, replyToMessageId: nil, localGroupingKey: nil, correlationId: nil)) } - messages.append(.message(text: "", attributes: [], mediaReference: .standalone(media: media), replyToMessageId: nil, localGroupingKey: nil)) + messages.append(.message(text: "", attributes: [], mediaReference: .standalone(media: media), replyToMessageId: nil, localGroupingKey: nil, correlationId: nil)) shareSignals.append(enqueueMessages(account: strongSelf.currentAccount, peerId: peerId, messages: messages)) } case let .messages(messages): for peerId in peerIds { var messagesToEnqueue: [EnqueueMessage] = [] if !text.isEmpty { - messagesToEnqueue.append(.message(text: text, attributes: [], mediaReference: nil, replyToMessageId: nil, localGroupingKey: nil)) + messagesToEnqueue.append(.message(text: text, attributes: [], mediaReference: nil, replyToMessageId: nil, localGroupingKey: nil, correlationId: nil)) } for message in messages { - messagesToEnqueue.append(.forward(source: message.id, grouping: .auto, attributes: [])) + messagesToEnqueue.append(.forward(source: message.id, grouping: .auto, attributes: [], correlationId: nil)) } shareSignals.append(enqueueMessages(account: strongSelf.currentAccount, peerId: peerId, messages: messagesToEnqueue)) } @@ -616,7 +619,7 @@ public final class ShareController: ViewController { if let error = error { Queue.mainQueue().async { let _ = (account.postbox.transaction { transaction -> Peer? in - deleteMessages(transaction: transaction, mediaBox: account.postbox.mediaBox, ids: [id]) + TelegramEngine(account: account).messages.deleteMessages(transaction: transaction, ids: [id]) return transaction.getPeer(id.peerId) } |> deliverOnMainQueue).start(next: { peer in @@ -661,7 +664,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, flags: []) + let media = TelegramMediaImage(imageId: MediaId(namespace: Namespaces.Media.LocalImage, id: Int64.random(in: Int64.min ... Int64.max)), 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)) @@ -706,7 +709,19 @@ public final class ShareController: ViewController { } else { authorPeerId = accountPeerId } - collectableItems.append(CollectableExternalShareItem(url: url, text: message.text, author: authorPeerId, timestamp: message.timestamp, mediaReference: selectedMedia.flatMap({ AnyMediaReference.message(message: MessageReference(message), media: $0) }))) + + var restrictedText: String? + for attribute in message.attributes { + if let attribute = attribute as? RestrictedContentMessageAttribute { + restrictedText = attribute.platformText(platform: "ios", contentSettings: strongSelf.currentContext.currentContentSettings.with { $0 }) ?? "" + } + } + + if let restrictedText = restrictedText { + collectableItems.append(CollectableExternalShareItem(url: url, text: restrictedText, author: authorPeerId, timestamp: message.timestamp, mediaReference: nil)) + } else { + collectableItems.append(CollectableExternalShareItem(url: url, text: message.text, author: authorPeerId, timestamp: message.timestamp, mediaReference: selectedMedia.flatMap({ AnyMediaReference.message(message: MessageReference(message), media: $0) }))) + } } case .fromExternal: break @@ -800,6 +815,9 @@ public final class ShareController: ViewController { strongSelf.view.endEditing(true) strongSelf.present(controller, in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) } + self.controllerNode.debugAction = { [weak self] in + self?.debugAction?() + } self.displayNodeDidLoad() self.peersDisposable.set((self.peers.get() @@ -839,7 +857,7 @@ public final class ShareController: 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.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition) } private func saveToCameraRoll(messages: [Message]) { @@ -967,9 +985,8 @@ final class MessageStoryRenderer { self.containerNode = ASDisplayNode() - self.instantChatBackgroundNode = WallpaperBackgroundNode() + self.instantChatBackgroundNode = WallpaperBackgroundNode(context: context) self.instantChatBackgroundNode.displaysAsynchronously = false - self.instantChatBackgroundNode.image = chatControllerBackgroundImage(theme: self.presentationData.theme, wallpaper: self.presentationData.chatWallpaper, mediaBox: context.sharedContext.accountManager.mediaBox, knockoutMode: context.sharedContext.immediateExperimentalUISettings.knockoutWallpaper) self.messagesContainerNode = ASDisplayNode() self.messagesContainerNode.clipsToBounds = true @@ -1012,7 +1029,7 @@ final class MessageStoryRenderer { let theme = self.presentationData.theme.withUpdated(preview: true) let headerItem = self.context.sharedContext.makeChatMessageDateHeaderItem(context: self.context, timestamp: self.messages.first?.timestamp ?? 0, theme: 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) - let items: [ListViewItem] = [self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: self.messages, theme: theme, strings: self.presentationData.strings, wallpaper: self.presentationData.theme.chat.defaultWallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil)] + let items: [ListViewItem] = [self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: self.messages, theme: theme, strings: self.presentationData.strings, wallpaper: self.presentationData.theme.chat.defaultWallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: nil)] let inset: CGFloat = 16.0 let width = layout.size.width - inset * 2.0 @@ -1065,7 +1082,7 @@ final class MessageStoryRenderer { dateHeaderNode = currentDateHeaderNode headerItem.updateNode(dateHeaderNode, previous: nil, next: headerItem) } else { - dateHeaderNode = headerItem.node() + dateHeaderNode = headerItem.node(synchronousLoad: true) dateHeaderNode.subnodeTransform = CATransform3DMakeScale(-1.0, 1.0, 1.0) self.messagesContainerNode.addSubnode(dateHeaderNode) self.dateHeaderNode = dateHeaderNode diff --git a/submodules/ShareController/Sources/ShareControllerNode.swift b/submodules/ShareController/Sources/ShareControllerNode.swift index 4d33c1a50f..9608e6275e 100644 --- a/submodules/ShareController/Sources/ShareControllerNode.swift +++ b/submodules/ShareController/Sources/ShareControllerNode.swift @@ -25,7 +25,7 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate private let sharedContext: SharedAccountContext private var context: AccountContext? private var presentationData: PresentationData - private let forcedTheme: PresentationTheme? + private let forceTheme: PresentationTheme? private let externalShare: Bool private let immediateExternalShare: Bool private var immediatePeerId: PeerId? @@ -61,6 +61,7 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate var share: ((String, [PeerId]) -> Signal)? var shareExternal: (() -> Signal)? var switchToAnotherAccount: (() -> Void)? + var debugAction: (() -> Void)? var openStats: (() -> Void)? var completed: (([PeerId]) -> Void)? @@ -80,10 +81,10 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate private let presetText: String? - init(sharedContext: SharedAccountContext, presetText: String?, defaultAction: ShareControllerAction?, requestLayout: @escaping (ContainedViewLayoutTransition) -> Void, presentError: @escaping (String?, String) -> Void, externalShare: Bool, immediateExternalShare: Bool, immediatePeerId: PeerId?, fromForeignApp: Bool, forcedTheme: PresentationTheme?, segmentedValues: [ShareControllerSegmentedValue]?) { + init(sharedContext: SharedAccountContext, presetText: String?, defaultAction: ShareControllerAction?, requestLayout: @escaping (ContainedViewLayoutTransition) -> Void, presentError: @escaping (String?, String) -> Void, externalShare: Bool, immediateExternalShare: Bool, immediatePeerId: PeerId?, fromForeignApp: Bool, forceTheme: PresentationTheme?, segmentedValues: [ShareControllerSegmentedValue]?) { self.sharedContext = sharedContext self.presentationData = sharedContext.currentPresentationData.with { $0 } - self.forcedTheme = forcedTheme + self.forceTheme = forceTheme self.externalShare = externalShare self.immediateExternalShare = immediateExternalShare self.immediatePeerId = immediatePeerId @@ -96,8 +97,8 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate self.defaultAction = defaultAction self.requestLayout = requestLayout - if let forcedTheme = self.forcedTheme { - self.presentationData = self.presentationData.withUpdated(theme: forcedTheme) + if let forceTheme = self.forceTheme { + self.presentationData = self.presentationData.withUpdated(theme: forceTheme) } let roundedBackground = generateStretchableFilledCircleImage(radius: 16.0, color: self.presentationData.theme.actionSheet.opaqueItemBackgroundColor) @@ -272,8 +273,8 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate return } self.presentationData = presentationData - if let forcedTheme = self.forcedTheme { - self.presentationData = self.presentationData.withUpdated(theme: forcedTheme) + if let forceTheme = self.forceTheme { + self.presentationData = self.presentationData.withUpdated(theme: forceTheme) } let roundedBackground = generateStretchableFilledCircleImage(radius: 16.0, color: self.presentationData.theme.actionSheet.opaqueItemBackgroundColor) @@ -378,7 +379,7 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate let animation = contentNode.layer.makeAnimation(from: 0.0 as NSNumber, to: 1.0 as NSNumber, keyPath: "opacity", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, duration: 0.35) animation.fillMode = .both if !fastOut { - animation.beginTime = CACurrentMediaTime() + 0.1 + animation.beginTime = contentNode.layer.convertTime(CACurrentMediaTime(), from: nil) + 0.1 } contentNode.layer.add(animation, forKey: "opacity") } @@ -594,6 +595,13 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate self.animateOut(shared: true, completion: { }) self.completed?(peerIds) + + Queue.mainQueue().after(0.44) { + if self.hapticFeedback == nil { + self.hapticFeedback = HapticFeedback() + } + self.hapticFeedback?.success() + } } let fromForeignApp = self.fromForeignApp self.shareDisposable.set((signal @@ -730,10 +738,12 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate let animated = self.peersContentNode == nil 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?() + }, debugAction: { [weak self] in + self?.debugAction?() }, extendedInitialReveal: self.presetText != nil, segmentedValues: self.segmentedValues) self.peersContentNode = peersContentNode peersContentNode.openSearch = { [weak self] in - let _ = (recentlySearchedPeers(postbox: context.account.postbox) + let _ = (context.engine.peers.recentlySearchedPeers() |> take(1) |> deliverOnMainQueue).start(next: { peers in if let strongSelf = self { diff --git a/submodules/ShareController/Sources/ShareInputFieldNode.swift b/submodules/ShareController/Sources/ShareInputFieldNode.swift index cc60e09b3b..47de1c6bc3 100644 --- a/submodules/ShareController/Sources/ShareInputFieldNode.swift +++ b/submodules/ShareController/Sources/ShareInputFieldNode.swift @@ -170,10 +170,10 @@ public final class ShareInputFieldNode: ASDisplayNode, ASEditableTextNodeDelegat @objc public func editableTextNodeDidUpdateText(_ editableTextNode: ASEditableTextNode) { self.updateTextNodeText(animated: true) self.updateText?(editableTextNode.attributedText?.string ?? "") + self.placeholderNode.isHidden = !(editableTextNode.textView.text ?? "").isEmpty } public func editableTextNodeDidBeginEditing(_ editableTextNode: ASEditableTextNode) { - self.placeholderNode.isHidden = true self.clearButton.isHidden = false if self.selectTextOnce { diff --git a/submodules/ShareController/Sources/SharePeersContainerNode.swift b/submodules/ShareController/Sources/SharePeersContainerNode.swift index 8b05d91369..381beb78e5 100644 --- a/submodules/ShareController/Sources/SharePeersContainerNode.swift +++ b/submodules/ShareController/Sources/SharePeersContainerNode.swift @@ -81,6 +81,7 @@ final class SharePeersContainerNode: ASDisplayNode, ShareContentContainerNode { private let nameDisplayOrder: PresentationPersonNameOrder private let controllerInteraction: ShareControllerInteraction private let switchToAnotherAccount: () -> Void + private let debugAction: () -> Void private let extendedInitialReveal: Bool let accountPeer: Peer @@ -113,7 +114,7 @@ final class SharePeersContainerNode: ASDisplayNode, ShareContentContainerNode { let peersValue = Promise<[(RenderedPeer, PeerPresence?)]>() - 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, extendedInitialReveal: Bool, segmentedValues: [ShareControllerSegmentedValue]?) { + 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, debugAction: @escaping () -> Void, extendedInitialReveal: Bool, segmentedValues: [ShareControllerSegmentedValue]?) { self.sharedContext = sharedContext self.context = context self.theme = theme @@ -122,6 +123,7 @@ final class SharePeersContainerNode: ASDisplayNode, ShareContentContainerNode { self.controllerInteraction = controllerInteraction self.accountPeer = accountPeer self.switchToAnotherAccount = switchToAnotherAccount + self.debugAction = debugAction self.extendedInitialReveal = extendedInitialReveal self.segmentedValues = segmentedValues @@ -133,14 +135,16 @@ final class SharePeersContainerNode: ASDisplayNode, ShareContentContainerNode { var index: Int32 = 0 var existingPeerIds: Set = Set() - entries.append(SharePeerEntry(index: index, peer: RenderedPeer(peer: accountPeer), presence: nil, theme: theme, strings: strings)) + existingPeerIds.insert(accountPeer.id) index += 1 for peer in foundPeers.reversed() { - entries.append(SharePeerEntry(index: index, peer: peer, presence: nil, theme: theme, strings: strings)) - existingPeerIds.insert(peer.peerId) - index += 1 + if !existingPeerIds.contains(peer.peerId) { + entries.append(SharePeerEntry(index: index, peer: peer, presence: nil, theme: theme, strings: strings)) + existingPeerIds.insert(peer.peerId) + index += 1 + } } for (peer, presence) in initialPeers { @@ -238,6 +242,8 @@ final class SharePeersContainerNode: ASDisplayNode, ShareContentContainerNode { self.segmentedNode.selectedIndexChanged = { [weak self] index in self?.segmentedSelectedIndexUpdated?(index) } + + self.contentTitleNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.debugTapGesture(_:)))) } deinit { @@ -462,4 +468,27 @@ final class SharePeersContainerNode: ASDisplayNode, ShareContentContainerNode { @objc private func accountTapGesture(_ recognizer: UITapGestureRecognizer) { self.switchToAnotherAccount() } + + private var debugTapCounter: (Double, Int) = (0.0, 0) + + @objc private func debugTapGesture(_ recognizer: UITapGestureRecognizer) { + if case .ended = recognizer.state { + let timestamp = CACurrentMediaTime() + if self.debugTapCounter.0 < timestamp - 0.4 { + self.debugTapCounter.0 = timestamp + self.debugTapCounter.1 = 0 + } + + if self.debugTapCounter.0 >= timestamp - 0.4 { + self.debugTapCounter.0 = timestamp + self.debugTapCounter.1 += 1 + } + + if self.debugTapCounter.1 >= 10 { + self.debugTapCounter.1 = 0 + + self.debugAction() + } + } + } } diff --git a/submodules/ShareController/Sources/ShareSearchContainerNode.swift b/submodules/ShareController/Sources/ShareSearchContainerNode.swift index 2cba52dc8b..51dac1c75d 100644 --- a/submodules/ShareController/Sources/ShareSearchContainerNode.swift +++ b/submodules/ShareController/Sources/ShareSearchContainerNode.swift @@ -249,7 +249,7 @@ final class ShareSearchContainerNode: ASDisplayNode, ShareContentContainerNode { let foundLocalPeers = context.account.postbox.searchPeers(query: query.lowercased()) let foundRemotePeers: Signal<([FoundPeer], [FoundPeer]), NoError> = .single(([], [])) |> then( - searchPeers(account: context.account, query: query) + context.engine.peers.searchPeers(query: query) |> delay(0.2, queue: Queue.concurrentDefaultQueue()) ) @@ -333,7 +333,7 @@ final class ShareSearchContainerNode: ASDisplayNode, ShareContentContainerNode { self?.searchQuery.set(text) } - let hasRecentPeers = recentPeers(account: context.account) + let hasRecentPeers = context.engine.peers.recentPeers() |> map { value -> Bool in switch value { case let .peers(peers): diff --git a/submodules/ShareItems/Sources/ShareItems.swift b/submodules/ShareItems/Sources/ShareItems.swift index 48d7040e80..a91ec1dec9 100644 --- a/submodules/ShareItems/Sources/ShareItems.swift +++ b/submodules/ShareItems/Sources/ShareItems.swift @@ -127,7 +127,7 @@ private func preparedShareItem(account: Account, to peerId: PeerId, value: [Stri let estimatedSize = TGMediaVideoConverter.estimatedSize(for: preset, duration: finalDuration, hasAudio: true) - let resource = LocalFileVideoMediaResource(randomId: arc4random64(), path: asset.url.path, adjustments: resourceAdjustments) + let resource = LocalFileVideoMediaResource(randomId: Int64.random(in: Int64.min ... Int64.max), path: asset.url.path, adjustments: resourceAdjustments) return standaloneUploadedFile(account: account, peerId: peerId, text: "", source: .resource(.standalone(resource: resource)), mimeType: "video/mp4", attributes: [.Video(duration: Int(finalDuration), size: PixelDimensions(width: Int32(finalDimensions.width), height: Int32(finalDimensions.height)), flags: flags)], hintFileIsLarge: estimatedSize > 10 * 1024 * 1024) |> mapError { _ -> Void in return Void() @@ -394,26 +394,26 @@ public func sentShareItems(account: Account, to peerIds: [PeerId], items: [Prepa } if ((mediaTypes.photo + mediaTypes.video) > 1) && (mediaTypes.music == 0 && mediaTypes.other == 0) { - groupingKey = arc4random64() + groupingKey = Int64.random(in: Int64.min ... Int64.max) } else if ((mediaTypes.photo + mediaTypes.video) == 0) && ((mediaTypes.music > 1 && mediaTypes.other == 0) || (mediaTypes.music == 0 && mediaTypes.other > 1)) { - groupingKey = arc4random64() + groupingKey = Int64.random(in: Int64.min ... Int64.max) } var mediaMessages: [EnqueueMessage] = [] for item in items { switch item { case let .text(text): - messages.append(.message(text: text, attributes: [], mediaReference: nil, replyToMessageId: nil, localGroupingKey: nil)) + messages.append(.message(text: text, attributes: [], mediaReference: nil, replyToMessageId: nil, localGroupingKey: nil, correlationId: nil)) case let .media(media): switch media { case let .media(reference): - let message: EnqueueMessage = .message(text: "", attributes: [], mediaReference: reference, replyToMessageId: nil, localGroupingKey: groupingKey) + let message: EnqueueMessage = .message(text: "", attributes: [], mediaReference: reference, replyToMessageId: nil, localGroupingKey: groupingKey, correlationId: nil) messages.append(message) mediaMessages.append(message) } if let _ = groupingKey, mediaMessages.count % 10 == 0 { - groupingKey = arc4random64() + groupingKey = Int64.random(in: Int64.min ... Int64.max) } } } diff --git a/submodules/ShimmerEffect/Sources/ShimmerEffect.swift b/submodules/ShimmerEffect/Sources/ShimmerEffect.swift index 3157a60c1a..456806dec7 100644 --- a/submodules/ShimmerEffect/Sources/ShimmerEffect.swift +++ b/submodules/ShimmerEffect/Sources/ShimmerEffect.swift @@ -2,9 +2,10 @@ import UIKit import AsyncDisplayKit import Display -private final class ShimmerEffectForegroundNode: ASDisplayNode { +final class ShimmerEffectForegroundNode: ASDisplayNode { private var currentBackgroundColor: UIColor? private var currentForegroundColor: UIColor? + private var currentHorizontal: Bool? private let imageNodeContainer: ASDisplayNode private let imageNode: ASImageNode @@ -45,31 +46,56 @@ private final class ShimmerEffectForegroundNode: ASDisplayNode { self.updateAnimation() } - func update(backgroundColor: UIColor, foregroundColor: UIColor) { - if let currentBackgroundColor = self.currentBackgroundColor, currentBackgroundColor.isEqual(backgroundColor), let currentForegroundColor = self.currentForegroundColor, currentForegroundColor.isEqual(foregroundColor) { + func update(backgroundColor: UIColor, foregroundColor: UIColor, horizontal: Bool = false) { + if let currentBackgroundColor = self.currentBackgroundColor, currentBackgroundColor.isEqual(backgroundColor), let currentForegroundColor = self.currentForegroundColor, currentForegroundColor.isEqual(foregroundColor), self.currentHorizontal == horizontal { return } self.currentBackgroundColor = backgroundColor self.currentForegroundColor = foregroundColor + self.currentHorizontal = horizontal - self.imageNode.image = generateImage(CGSize(width: 16.0, height: 320.0), opaque: false, scale: 1.0, rotatedContext: { size, context in - context.clear(CGRect(origin: CGPoint(), size: size)) - 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()) - }) + let image: UIImage? + if horizontal { + image = generateImage(CGSize(width: 320.0, height: 16.0), opaque: false, scale: 1.0, rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + 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: size.width, y: 0.0), options: CGGradientDrawingOptions()) + }) + } else { + image = generateImage(CGSize(width: 16.0, height: 320.0), opaque: false, scale: 1.0, rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + 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()) + }) + } + self.imageNode.image = image + self.updateAnimation() } func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) { @@ -95,7 +121,7 @@ private final class ShimmerEffectForegroundNode: ASDisplayNode { } private func updateAnimation() { - let shouldBeAnimating = self.isCurrentlyInHierarchy && self.absoluteLocation != nil + let shouldBeAnimating = self.isCurrentlyInHierarchy && self.absoluteLocation != nil && self.currentHorizontal != nil if shouldBeAnimating != self.shouldBeAnimating { self.shouldBeAnimating = shouldBeAnimating if shouldBeAnimating { @@ -107,15 +133,25 @@ private final class ShimmerEffectForegroundNode: ASDisplayNode { } private func addImageAnimation() { - guard let containerSize = self.absoluteLocation?.1 else { + guard let containerSize = self.absoluteLocation?.1, let horizontal = self.currentHorizontal 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") + + if horizontal { + let gradientHeight: CGFloat = 320.0 + self.imageNode.frame = CGRect(origin: CGPoint(x: -gradientHeight, y: 0.0), size: CGSize(width: gradientHeight, height: containerSize.height)) + let animation = self.imageNode.layer.makeAnimation(from: 0.0 as NSNumber, to: (containerSize.width + gradientHeight) as NSNumber, keyPath: "position.x", 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") + } else { + 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") + } } } @@ -135,6 +171,7 @@ public final class ShimmerEffectNode: ASDisplayNode { private var currentBackgroundColor: UIColor? private var currentForegroundColor: UIColor? private var currentShimmeringColor: UIColor? + private var currentHorizontal: Bool? private var currentSize = CGSize() override public init() { @@ -157,8 +194,8 @@ public final class ShimmerEffectNode: ASDisplayNode { self.effectNode.updateAbsoluteRect(rect, within: containerSize) } - public 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 { + public func update(backgroundColor: UIColor, foregroundColor: UIColor, shimmeringColor: UIColor, shapes: [Shape], horizontal: Bool = false, 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), horizontal == self.currentHorizontal, self.currentSize == size { return } @@ -166,11 +203,12 @@ public final class ShimmerEffectNode: ASDisplayNode { self.currentForegroundColor = foregroundColor self.currentShimmeringColor = shimmeringColor self.currentShapes = shapes + self.currentHorizontal = horizontal self.currentSize = size self.backgroundNode.backgroundColor = foregroundColor - self.effectNode.update(backgroundColor: foregroundColor, foregroundColor: shimmeringColor) + self.effectNode.update(backgroundColor: foregroundColor, foregroundColor: shimmeringColor, horizontal: horizontal) self.foregroundNode.image = generateImage(size, rotatedContext: { size, context in context.setFillColor(backgroundColor.cgColor) diff --git a/submodules/ShimmerEffect/Sources/StickerShimmerEffectNode.swift b/submodules/ShimmerEffect/Sources/StickerShimmerEffectNode.swift index b0d5a129d8..af464d3222 100644 --- a/submodules/ShimmerEffect/Sources/StickerShimmerEffectNode.swift +++ b/submodules/ShimmerEffect/Sources/StickerShimmerEffectNode.swift @@ -2,124 +2,6 @@ import Foundation import AsyncDisplayKit import Display -private final class ShimmerEffectForegroundNode: 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 - - let image = generateImage(CGSize(width: 320.0, height: 16.0), opaque: false, scale: 1.0, rotatedContext: { size, context in - context.clear(CGRect(origin: CGPoint(), size: size)) - 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: size.width, y: 0.0), options: CGGradientDrawingOptions()) - }) - self.imageNode.image = image - } - - 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() - } else { - self.updateAnimation() - } - } - - 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 = 320.0 - self.imageNode.frame = CGRect(origin: CGPoint(x: -gradientHeight, y: 0.0), size: CGSize(width: gradientHeight, height: containerSize.height)) - let animation = self.imageNode.layer.makeAnimation(from: 0.0 as NSNumber, to: (containerSize.width + gradientHeight) as NSNumber, keyPath: "position.x", 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 let decodingMap: [String] = ["A", "A", "C", "A", "A", "A", "A", "H", "A", "A", "A", "L", "M", "A", "A", "A", "Q", "A", "S", "T", "A", "V", "A", "A", "A", "Z", "a", "a", "c", "a", "a", "a", "a", "h", "a", "a", "a", "l", "m", "a", "a", "a", "q", "a", "s", "t", "a", "v", "a", ".", "a", "z", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "-", ","] private func decodeStickerThumbnailData(_ data: Data) -> String { var string = "M" @@ -140,6 +22,7 @@ private func decodeStickerThumbnailData(_ data: Data) -> String { } public class StickerShimmerEffectNode: ASDisplayNode { + private var backdropNode: ASDisplayNode? private let backgroundNode: ASDisplayNode private let effectNode: ShimmerEffectForegroundNode private let foregroundNode: ASImageNode @@ -163,11 +46,21 @@ public class StickerShimmerEffectNode: ASDisplayNode { self.addSubnode(self.effectNode) self.addSubnode(self.foregroundNode) } - + public var isEmpty: Bool { return self.currentData == nil } + public func addBackdropNode(_ backdropNode: ASDisplayNode) { + if let current = self.backdropNode { + current.removeFromSupernode() + } + self.backdropNode = backdropNode + self.insertSubnode(backdropNode, at: 0) + + self.effectNode.layer.compositingFilter = "screenBlendMode" + } + public func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) { self.effectNode.updateAbsoluteRect(rect, within: containerSize) } @@ -188,8 +81,9 @@ public class StickerShimmerEffectNode: ASDisplayNode { self.backgroundNode.backgroundColor = foregroundColor - self.effectNode.update(backgroundColor: backgroundColor == nil ? .clear : foregroundColor, foregroundColor: shimmeringColor) + self.effectNode.update(backgroundColor: backgroundColor == nil ? .clear : foregroundColor, foregroundColor: shimmeringColor, horizontal: true) + let bounds = CGRect(origin: CGPoint(), size: size) let image = generateImage(size, rotatedContext: { size, context in if let backgroundColor = backgroundColor { context.setFillColor(backgroundColor.cgColor) @@ -219,7 +113,7 @@ public class StickerShimmerEffectNode: ASDisplayNode { UIGraphicsPopContext() } }) - + if backgroundColor == nil { self.foregroundNode.image = nil @@ -228,7 +122,7 @@ public class StickerShimmerEffectNode: ASDisplayNode { maskView = current } else { maskView = UIImageView() - maskView.frame = CGRect(origin: CGPoint(), size: size) + maskView.frame = bounds self.maskView = maskView self.view.mask = maskView } @@ -244,9 +138,10 @@ public class StickerShimmerEffectNode: ASDisplayNode { self.maskView?.image = image - self.backgroundNode.frame = CGRect(origin: CGPoint(), size: size) - self.foregroundNode.frame = CGRect(origin: CGPoint(), size: size) - self.effectNode.frame = CGRect(origin: CGPoint(), size: size) + self.backdropNode?.frame = bounds + self.backgroundNode.frame = bounds + self.foregroundNode.frame = bounds + self.effectNode.frame = bounds } } diff --git a/submodules/SolidRoundedButtonNode/Sources/SolidRoundedButtonNode.swift b/submodules/SolidRoundedButtonNode/Sources/SolidRoundedButtonNode.swift index f25d6743ea..2f63f71b6f 100644 --- a/submodules/SolidRoundedButtonNode/Sources/SolidRoundedButtonNode.swift +++ b/submodules/SolidRoundedButtonNode/Sources/SolidRoundedButtonNode.swift @@ -3,8 +3,6 @@ import UIKit import AsyncDisplayKit import Display -private let textFont: UIFont = Font.regular(16.0) - public final class SolidRoundedButtonTheme { public let backgroundColor: UIColor public let foregroundColor: UIColor diff --git a/submodules/StatisticsUI/Sources/ChannelStatsController.swift b/submodules/StatisticsUI/Sources/ChannelStatsController.swift index 2bae41250a..649297977a 100644 --- a/submodules/StatisticsUI/Sources/ChannelStatsController.swift +++ b/submodules/StatisticsUI/Sources/ChannelStatsController.swift @@ -518,7 +518,7 @@ public func channelStatsController(context: AccountContext, peerId: PeerId, cach items.append(.action(ContextMenuActionItem(text: presentationData.strings.SharedMedia_ViewInChat, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/GoToMessage"), color: theme.contextMenu.primaryColor) }, action: { [weak controller] c, _ in c.dismiss(completion: { if let navigationController = controller?.navigationController as? NavigationController { - context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peerId), subject: .message(id: messageId, highlight: true))) + context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peerId), subject: .message(id: messageId, highlight: true, timecode: nil))) } }) }))) diff --git a/submodules/StatisticsUI/Sources/GroupStatsController.swift b/submodules/StatisticsUI/Sources/GroupStatsController.swift index 7370caed78..d2fc365197 100644 --- a/submodules/StatisticsUI/Sources/GroupStatsController.swift +++ b/submodules/StatisticsUI/Sources/GroupStatsController.swift @@ -901,7 +901,7 @@ public func groupStatsController(context: AccountContext, peerId: PeerId, cached } promotePeerImpl = { [weak controller] participantPeerId in if let navigationController = controller?.navigationController as? NavigationController { - let _ = (fetchChannelParticipant(account: context.account, peerId: peerId, participantId: participantPeerId) + let _ = (context.engine.peers.fetchChannelParticipant(peerId: peerId, participantId: participantPeerId) |> take(1) |> deliverOnMainQueue).start(next: { participant in if let participant = participant, let controller = context.sharedContext.makeChannelAdminController(context: context, peerId: peerId, adminId: participantPeerId, initialParticipant: participant) { diff --git a/submodules/StatisticsUI/Sources/MessageStatsController.swift b/submodules/StatisticsUI/Sources/MessageStatsController.swift index 72ce6daff7..3eebe518d1 100644 --- a/submodules/StatisticsUI/Sources/MessageStatsController.swift +++ b/submodules/StatisticsUI/Sources/MessageStatsController.swift @@ -215,13 +215,13 @@ public func messageStatsController(context: AccountContext, messageId: MessageId let previousData = Atomic(value: nil) - let searchSignal = searchMessages(account: context.account, location: .publicForwards(messageId: messageId, datacenterId: Int(datacenterId)), query: "", state: nil) + let searchSignal = context.engine.messages.searchMessages(location: .publicForwards(messageId: messageId, datacenterId: Int(datacenterId)), query: "", state: nil) |> map(Optional.init) |> afterNext { result in if let result = result { for message in result.0.messages { if let peer = message.peers[message.id.peerId], let peerReference = PeerReference(peer) { - let _ = updatedRemotePeer(postbox: context.account.postbox, network: context.account.network, peer: peerReference).start() + let _ = context.engine.peers.updatedRemotePeer(peer: peerReference).start() } } } @@ -264,7 +264,7 @@ public func messageStatsController(context: AccountContext, messageId: MessageId } navigateToMessageImpl = { [weak controller] messageId in if let navigationController = controller?.navigationController as? NavigationController { - context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(messageId.peerId), subject: .message(id: messageId, highlight: true), keepStack: .always, useExisting: false, purposefulAction: {}, peekData: nil)) + context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(messageId.peerId), subject: .message(id: messageId, highlight: true, timecode: nil), keepStack: .always, useExisting: false, purposefulAction: {}, peekData: nil)) } } return controller diff --git a/submodules/StatisticsUI/Sources/MessageStatsOverviewItem.swift b/submodules/StatisticsUI/Sources/MessageStatsOverviewItem.swift index d2bbfdfd00..339fc1a3f7 100644 --- a/submodules/StatisticsUI/Sources/MessageStatsOverviewItem.swift +++ b/submodules/StatisticsUI/Sources/MessageStatsOverviewItem.swift @@ -166,7 +166,7 @@ class MessageStatsOverviewItemNode: ListViewItemNode { centerValueLabelLayoutAndApply = makeCenterValueLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.publicShares.flatMap { compactNumericCountString(Int($0)) } ?? "–", font: valueFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) - rightValueLabelLayoutAndApply = makeRightValueLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.publicShares.flatMap { "≈\( compactNumericCountString(item.stats.forwards - Int($0)))" } ?? "–", font: valueFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + rightValueLabelLayoutAndApply = makeRightValueLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.publicShares.flatMap { "≈\( compactNumericCountString(max(0, item.stats.forwards - Int($0))))" } ?? "–", font: valueFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) leftTitleLabelLayoutAndApply = makeLeftTitleLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.presentationData.strings.Stats_Message_Views, font: titleFont, textColor: item.presentationData.theme.list.sectionHeaderTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) diff --git a/submodules/StatisticsUI/Sources/StatsMessageItem.swift b/submodules/StatisticsUI/Sources/StatsMessageItem.swift index 0851a6a5e8..c097cecdb8 100644 --- a/submodules/StatisticsUI/Sources/StatsMessageItem.swift +++ b/submodules/StatisticsUI/Sources/StatsMessageItem.swift @@ -241,7 +241,8 @@ public class StatsMessageItemNode: ListViewItemNode, ItemListItemNode { let titleFont = Font.regular(item.presentationData.fontSize.itemListBaseFontSize) - let contentKind = messageContentKind(contentSettings: item.context.currentContentSettings.with { $0 }, message: item.message, strings: item.presentationData.strings, nameDisplayOrder: .firstLast, accountPeerId: item.context.account.peerId) + let presentationData = item.context.sharedContext.currentPresentationData.with { $0 } + let contentKind = messageContentKind(contentSettings: item.context.currentContentSettings.with { $0 }, message: item.message, strings: item.presentationData.strings, nameDisplayOrder: .firstLast, dateTimeFormat: presentationData.dateTimeFormat, accountPeerId: item.context.account.peerId) var text = !item.message.text.isEmpty ? item.message.text : stringForMediaKind(contentKind, strings: item.presentationData.strings).0 text = foldLineBreaks(text) @@ -288,7 +289,6 @@ public class StatsMessageItemNode: ListViewItemNode, ItemListItemNode { let labelFont = Font.regular(floor(item.presentationData.fontSize.itemListBaseFontSize * 13.0 / 17.0)) - let presentationData = item.context.sharedContext.currentPresentationData.with { $0 } let label = stringForFullDate(timestamp: item.message.timestamp, strings: item.presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat) let (labelLayout, labelApply) = makeLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: label, font: labelFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - totalLeftInset - rightInset - additionalRightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) diff --git a/submodules/StickerPackPreviewUI/BUILD b/submodules/StickerPackPreviewUI/BUILD index bf34fcb5bb..d844048b4d 100644 --- a/submodules/StickerPackPreviewUI/BUILD +++ b/submodules/StickerPackPreviewUI/BUILD @@ -28,6 +28,7 @@ swift_library( "//submodules/ArchivedStickerPacksNotice:ArchivedStickerPacksNotice", "//submodules/ShimmerEffect:ShimmerEffect", "//submodules/UndoUI:UndoUI", + "//submodules/ContextUI:ContextUI", ], visibility = [ "//visibility:public", diff --git a/submodules/StickerPackPreviewUI/Sources/StickerPackPreviewController.swift b/submodules/StickerPackPreviewUI/Sources/StickerPackPreviewController.swift index 15923c2ae7..4cfa82d782 100644 --- a/submodules/StickerPackPreviewUI/Sources/StickerPackPreviewController.swift +++ b/submodules/StickerPackPreviewUI/Sources/StickerPackPreviewController.swift @@ -25,7 +25,9 @@ public final class StickerPackPreviewController: ViewController, StandalonePrese } private var animatedIn = false - private var dismissed = false + private var isDismissed = false + + public var dismissed: (() -> Void)? private let context: AccountContext private let mode: StickerPackPreviewControllerMode @@ -80,7 +82,7 @@ public final class StickerPackPreviewController: ViewController, StandalonePrese self.acceptsFocusWhenInOverlay = true self.statusBar.statusBarStyle = .Ignore - self.stickerPackContents.set(loadedStickerPack(postbox: context.account.postbox, network: context.account.network, reference: stickerPack, forceActualized: true)) + self.stickerPackContents.set(context.engine.stickers.loadedStickerPack(reference: stickerPack, forceActualized: true)) self.presentationDataDisposable = (context.sharedContext.presentationData |> deliverOnMainQueue).start(next: { [weak self] presentationData in @@ -130,7 +132,7 @@ public final class StickerPackPreviewController: ViewController, StandalonePrese } let account = strongSelf.context.account - strongSelf.openMentionDisposable.set((resolvePeerByName(account: strongSelf.context.account, name: mention) + strongSelf.openMentionDisposable.set((strongSelf.context.engine.peers.resolvePeerByName(name: mention) |> mapToSignal { peerId -> Signal in if let peerId = peerId { return account.postbox.loadedPeerWithId(peerId) @@ -150,6 +152,7 @@ public final class StickerPackPreviewController: ViewController, StandalonePrese })) }, actionPerformed: self.actionPerformed) self.controllerNode.dismiss = { [weak self] in + self?.dismissed?() self?.presentingViewController?.dismiss(animated: false, completion: nil) } self.controllerNode.cancel = { [weak self] in @@ -264,8 +267,8 @@ public final class StickerPackPreviewController: ViewController, StandalonePrese } override public func dismiss(completion: (() -> Void)? = nil) { - if !self.dismissed { - self.dismissed = true + if !self.isDismissed { + self.isDismissed = true } else { return } @@ -277,7 +280,7 @@ public final class StickerPackPreviewController: ViewController, StandalonePrese 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.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition) } } diff --git a/submodules/StickerPackPreviewUI/Sources/StickerPackPreviewControllerNode.swift b/submodules/StickerPackPreviewUI/Sources/StickerPackPreviewControllerNode.swift index e4198bd782..68ddba3a16 100644 --- a/submodules/StickerPackPreviewUI/Sources/StickerPackPreviewControllerNode.swift +++ b/submodules/StickerPackPreviewUI/Sources/StickerPackPreviewControllerNode.swift @@ -12,6 +12,7 @@ import MergeLists import ActivityIndicator import TextFormat import AccountContext +import ContextUI private struct StickerPackPreviewGridEntry: Comparable, Identifiable { let index: Int @@ -88,6 +89,8 @@ final class StickerPackPreviewControllerNode: ViewControllerTracingNode, UIScrol private var hapticFeedback: HapticFeedback? + private weak var peekController: PeekController? + init(context: AccountContext, openShare: (() -> Void)?, openMention: @escaping (String) -> Void, actionPerformed: ((StickerPackCollectionInfo, [ItemCollectionItem], StickerPackScreenPerformedAction) -> Void)?) { self.context = context self.openShare = openShare @@ -204,28 +207,31 @@ final class StickerPackPreviewControllerNode: ViewControllerTracingNode, UIScrol |> deliverOnMainQueue |> map { isStarred -> (ASDisplayNode, PeekControllerContent)? in if let strongSelf = self { - var menuItems: [PeekControllerMenuItem] = [] + var menuItems: [ContextMenuItem] = [] if let stickerPack = strongSelf.stickerPack, case let .result(info, _, _) = stickerPack, 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() + menuItems.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.StickerPack_Send, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Resend"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in + if let strongSelf = self, let peekController = strongSelf.peekController { + if let animationNode = (peekController.contentNode as? StickerPreviewPeekContentNode)?.animationNode { + let _ = strongSelf.sendSticker?(.standalone(media: item.file), animationNode, animationNode.bounds) + } else if let imageNode = (peekController.contentNode as? StickerPreviewPeekContentNode)?.imageNode { + let _ = strongSelf.sendSticker?(.standalone(media: item.file), imageNode, imageNode.bounds) } } - return true - })) - menuItems.append(PeekControllerMenuItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { _, _ in return true })) + f(.default) + }))) + } + menuItems.append(.action(ContextMenuActionItem(text: isStarred ? strongSelf.presentationData.strings.Stickers_RemoveFromFavorites : strongSelf.presentationData.strings.Stickers_AddToFavorites, icon: { theme in generateTintedImage(image: isStarred ? UIImage(bundleImageName: "Chat/Context Menu/Unstar") : UIImage(bundleImageName: "Chat/Context Menu/Rate"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in + f(.default) + + 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 (itemNode, StickerPreviewPeekContent(account: strongSelf.context.account, item: .pack(item), menu: menuItems)) } else { @@ -237,7 +243,7 @@ final class StickerPackPreviewControllerNode: ViewControllerTracingNode, UIScrol return nil }, present: { [weak self] content, sourceNode in if let strongSelf = self { - let controller = PeekController(theme: PeekControllerTheme(presentationTheme: strongSelf.presentationData.theme), content: content, sourceNode: { + let controller = PeekController(presentationData: strongSelf.presentationData, content: content, sourceNode: { return sourceNode }) controller.visibilityUpdated = { [weak self] visible in @@ -245,6 +251,7 @@ final class StickerPackPreviewControllerNode: ViewControllerTracingNode, UIScrol strongSelf.contentGridNode.forceHidden = visible } } + strongSelf.peekController = controller strongSelf.presentInGlobalOverlay?(controller, nil) return controller } @@ -521,7 +528,7 @@ final class StickerPackPreviewControllerNode: ViewControllerTracingNode, UIScrol switch stickerPack { case let .result(info, items, installed): if installed { - let _ = (removeStickerPackInteractively(postbox: self.context.account.postbox, id: info.id, option: .delete) + let _ = (self.context.engine.stickers.removeStickerPackInteractively(id: info.id, option: .delete) |> deliverOnMainQueue).start(next: { [weak self] indexAndItems in guard let strongSelf = self, let (positionInList, _) = indexAndItems else { return @@ -532,7 +539,7 @@ final class StickerPackPreviewControllerNode: ViewControllerTracingNode, UIScrol self.updateStickerPack(.result(info: info, items: items, installed: false), stickerSettings: stickerSettings) } } else { - let _ = addStickerPackInteractively(postbox: self.context.account.postbox, info: info, items: items).start() + let _ = self.context.engine.stickers.addStickerPackInteractively(info: info, items: items).start() if !dismissOnAction { self.updateStickerPack(.result(info: info, items: items, installed: true), stickerSettings: stickerSettings) } @@ -550,11 +557,17 @@ final class StickerPackPreviewControllerNode: ViewControllerTracingNode, UIScrol func animateIn() { self.dimNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) - let offset = self.bounds.size.height - self.contentBackgroundNode.frame.minY - + let offset: CGFloat = 510.0 let dimPosition = self.dimNode.layer.position - self.dimNode.layer.animatePosition(from: CGPoint(x: dimPosition.x, y: dimPosition.y - offset), to: dimPosition, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring) - self.layer.animateBoundsOriginYAdditive(from: -offset, to: 0.0, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring) + + let transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring) + let targetBounds = self.bounds + self.bounds = self.bounds.offsetBy(dx: 0.0, dy: -offset) + self.dimNode.position = CGPoint(x: dimPosition.x, y: dimPosition.y - offset) + transition.animateView({ + self.bounds = targetBounds + self.dimNode.position = dimPosition + }) } func animateOut(completion: (() -> Void)? = nil) { diff --git a/submodules/StickerPackPreviewUI/Sources/StickerPackScreen.swift b/submodules/StickerPackPreviewUI/Sources/StickerPackScreen.swift index 512a1b7bcf..e7ad90f035 100644 --- a/submodules/StickerPackPreviewUI/Sources/StickerPackScreen.swift +++ b/submodules/StickerPackPreviewUI/Sources/StickerPackScreen.swift @@ -11,6 +11,7 @@ import TelegramPresentationData import TelegramUIPreferences import MergeLists import ShimmerEffect +import ContextUI private struct StickerPackPreviewGridEntry: Comparable, Identifiable { let index: Int @@ -100,6 +101,8 @@ private final class StickerPackContainer: ASDisplayNode { private let interaction: StickerPackPreviewInteraction + private weak var peekController: PeekController? + init(index: Int, context: AccountContext, presentationData: PresentationData, stickerPack: StickerPackReference, decideNextAction: @escaping (StickerPackContainer, StickerPackAction) -> StickerPackNextAction, requestDismiss: @escaping () -> Void, expandProgressUpdated: @escaping (StickerPackContainer, ContainedViewLayoutTransition, ContainedViewLayoutTransition) -> Void, presentInGlobalOverlay: @escaping (ViewController, Any?) -> Void, sendSticker: ((FileMediaReference, ASDisplayNode, CGRect) -> Bool)?) { self.index = index self.context = context @@ -238,7 +241,7 @@ private final class StickerPackContainer: ASDisplayNode { return updatedOffset } - self.itemsDisposable = (loadedStickerPack(postbox: context.account.postbox, network: context.account.network, reference: stickerPack, forceActualized: false) + self.itemsDisposable = (context.engine.stickers.loadedStickerPack(reference: stickerPack, forceActualized: false) |> deliverOnMainQueue).start(next: { [weak self] contents in guard let strongSelf = self else { return @@ -276,28 +279,27 @@ private final class StickerPackContainer: ASDisplayNode { |> deliverOnMainQueue |> map { isStarred -> (ASDisplayNode, PeekControllerContent)? in if let strongSelf = self { - var menuItems: [PeekControllerMenuItem] = [] + var menuItems: [ContextMenuItem] = [] 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(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.StickerPack_Send, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Resend"), color: theme.contextMenu.primaryColor) }, action: { _, f in + if let strongSelf = self, let peekController = strongSelf.peekController, let animationNode = (peekController.contentNode as? StickerPreviewPeekContentNode)?.animationNode { + let _ = strongSelf.sendSticker?(.standalone(media: item.file), animationNode, animationNode.bounds) } - })) + f(.default) + }))) } - 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() - } + menuItems.append(.action(ContextMenuActionItem(text: isStarred ? strongSelf.presentationData.strings.Stickers_RemoveFromFavorites : strongSelf.presentationData.strings.Stickers_AddToFavorites, icon: { theme in generateTintedImage(image: isStarred ? UIImage(bundleImageName: "Chat/Context Menu/Unstar") : UIImage(bundleImageName: "Chat/Context Menu/Rate"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in + f(.default) + + 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 { @@ -309,9 +311,10 @@ private final class StickerPackContainer: ASDisplayNode { return nil }, present: { [weak self] content, sourceNode in if let strongSelf = self { - let controller = PeekController(theme: PeekControllerTheme(presentationTheme: strongSelf.presentationData.theme), content: content, sourceNode: { + let controller = PeekController(presentationData: strongSelf.presentationData, content: content, sourceNode: { return sourceNode }) + strongSelf.peekController = controller strongSelf.presentInGlobalOverlay(controller, nil) return controller } @@ -340,9 +343,9 @@ private final class StickerPackContainer: ASDisplayNode { return } if installed { - let _ = removeStickerPackInteractively(postbox: strongSelf.context.account.postbox, id: info.id, option: .delete).start() + let _ = strongSelf.context.engine.stickers.removeStickerPackInteractively(id: info.id, option: .delete).start() } else { - let _ = addStickerPackInteractively(postbox: strongSelf.context.account.postbox, info: info, items: items).start() + let _ = strongSelf.context.engine.stickers.addStickerPackInteractively(info: info, items: items).start() } switch strongSelf.decideNextAction(strongSelf, installed ? .remove : .add) { @@ -988,6 +991,8 @@ public final class StickerPackScreenImpl: ViewController { return self.displayNode as! StickerPackScreenNode } + public var dismissed: (() -> Void)? + private let _ready = Promise() override public var ready: Promise { return self._ready @@ -1020,6 +1025,7 @@ public final class StickerPackScreenImpl: ViewController { strongSelf.updateModalStyleOverlayTransitionFactor(value, transition: transition) } }, dismissed: { [weak self] in + self?.dismissed?() self?.dismiss() }, presentInGlobalOverlay: { [weak self] c, a in self?.presentInGlobalOverlay(c, with: a) @@ -1060,10 +1066,11 @@ public enum StickerPackScreenPerformedAction { 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 { +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, dismissed: (() -> Void)? = nil) -> ViewController { //return StickerPackScreenImpl(context: context, stickerPacks: stickerPacks, selectedStickerPackIndex: stickerPacks.firstIndex(of: mainStickerPack) ?? 0, parentNavigationController: parentNavigationController, sendSticker: sendSticker) let controller = StickerPackPreviewController(context: context, stickerPack: mainStickerPack, mode: mode, parentNavigationController: parentNavigationController, actionPerformed: actionPerformed) + controller.dismissed = dismissed controller.sendSticker = sendSticker return controller } diff --git a/submodules/StickerPackPreviewUI/Sources/StickerPreviewController.swift b/submodules/StickerPackPreviewUI/Sources/StickerPreviewController.swift index d4aad4b91c..7281977304 100644 --- a/submodules/StickerPackPreviewUI/Sources/StickerPreviewController.swift +++ b/submodules/StickerPackPreviewUI/Sources/StickerPreviewController.swift @@ -69,7 +69,7 @@ public final class StickerPreviewController: 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.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition) } public func updateItem(_ item: StickerPackItem) { diff --git a/submodules/StickerPackPreviewUI/Sources/StickerPreviewControllerNode.swift b/submodules/StickerPackPreviewUI/Sources/StickerPreviewControllerNode.swift index 093498780a..edc6b01210 100644 --- a/submodules/StickerPackPreviewUI/Sources/StickerPreviewControllerNode.swift +++ b/submodules/StickerPackPreviewUI/Sources/StickerPreviewControllerNode.swift @@ -138,7 +138,7 @@ final class StickerPreviewControllerNode: ASDisplayNode, UIScrollViewDelegate { self.textNode.attributedText = NSAttributedString(string: text, font: Font.regular(32.0), textColor: .black) break } - self.imageNode.setSignal(chatMessageSticker(account: context.account, file: item.file, small: false)) + self.imageNode.setSignal(chatMessageSticker(account: context.account, file: item.file, small: false, onlyFullSize: false)) if let (layout, navigationBarHeight) = self.containerLayout { self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate) diff --git a/submodules/StickerPackPreviewUI/Sources/StickerPreviewPeekContent.swift b/submodules/StickerPackPreviewUI/Sources/StickerPreviewPeekContent.swift index 6af3b952f5..38ee9a8531 100644 --- a/submodules/StickerPackPreviewUI/Sources/StickerPreviewPeekContent.swift +++ b/submodules/StickerPackPreviewUI/Sources/StickerPreviewPeekContent.swift @@ -9,6 +9,7 @@ import SwiftSignalKit import StickerResources import AnimatedStickerNode import TelegramAnimatedStickerNode +import ContextUI public enum StickerPreviewPeekItem: Equatable { case pack(StickerPackItem) @@ -27,9 +28,9 @@ public enum StickerPreviewPeekItem: Equatable { public final class StickerPreviewPeekContent: PeekControllerContent { let account: Account public let item: StickerPreviewPeekItem - let menu: [PeekControllerMenuItem] + let menu: [ContextMenuItem] - public init(account: Account, item: StickerPreviewPeekItem, menu: [PeekControllerMenuItem]) { + public init(account: Account, item: StickerPreviewPeekItem, menu: [ContextMenuItem]) { self.account = account self.item = item self.menu = menu @@ -39,11 +40,11 @@ public final class StickerPreviewPeekContent: PeekControllerContent { return .freeform } - public func menuActivation() -> PeerkControllerMenuActivation { + public func menuActivation() -> PeerControllerMenuActivation { return .press } - public func menuItems() -> [PeekControllerMenuItem] { + public func menuItems() -> [ContextMenuItem] { return self.menu } @@ -64,13 +65,13 @@ public final class StickerPreviewPeekContent: PeekControllerContent { } } -private final class StickerPreviewPeekContentNode: ASDisplayNode, PeekControllerContentNode { +public final class StickerPreviewPeekContentNode: ASDisplayNode, PeekControllerContentNode { private let account: Account private let item: StickerPreviewPeekItem private var textNode: ASTextNode - private var imageNode: TransformImageNode - private var animationNode: AnimatedStickerNode? + public var imageNode: TransformImageNode + public var animationNode: AnimatedStickerNode? private var containerLayout: (ContainerViewLayout, CGFloat)? @@ -114,7 +115,7 @@ private final class StickerPreviewPeekContentNode: ASDisplayNode, PeekController } } - func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize { + public func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize { let boundingSize = CGSize(width: 180.0, height: 180.0).fitted(size) if let dimensitons = self.item.file.dimensions { @@ -123,7 +124,7 @@ private final class StickerPreviewPeekContentNode: ASDisplayNode, PeekController let imageSize = dimensitons.cgSize.aspectFitted(boundingSize) self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets()))() - let imageFrame = CGRect(origin: CGPoint(x: floor((size.width - imageSize.width) / 2.0), y: textSize.height + textSpacing), size: imageSize) + let imageFrame = CGRect(origin: CGPoint(x: 0.0, y: textSize.height + textSpacing), size: imageSize) self.imageNode.frame = imageFrame if let animationNode = self.animationNode { animationNode.frame = imageFrame @@ -132,7 +133,7 @@ private final class StickerPreviewPeekContentNode: ASDisplayNode, PeekController self.textNode.frame = CGRect(origin: CGPoint(x: floor((imageFrame.size.width - textSize.width) / 2.0), y: -textSize.height - textSpacing), size: textSize) - return CGSize(width: size.width, height: imageFrame.height + textSize.height + textSpacing) + return CGSize(width: imageFrame.width, height: imageFrame.height + textSize.height + textSpacing) } else { return CGSize(width: size.width, height: 10.0) } diff --git a/submodules/StickerResources/Sources/StickerResources.swift b/submodules/StickerResources/Sources/StickerResources.swift index b41cfecd44..7e907abb57 100644 --- a/submodules/StickerResources/Sources/StickerResources.swift +++ b/submodules/StickerResources/Sources/StickerResources.swift @@ -402,7 +402,7 @@ public func chatMessageSticker(postbox: Postbox, file: TelegramMediaFile, small: return nil } - if file.immediateThumbnailData != nil && fullSizeData == nil { + if file.immediateThumbnailData != nil && thumbnailData == nil && fullSizeData == nil { return nil } diff --git a/submodules/Stripe/PublicHeaders/Stripe/STPToken.h b/submodules/Stripe/PublicHeaders/Stripe/STPToken.h index ad44f21ecb..4f6896d968 100755 --- a/submodules/Stripe/PublicHeaders/Stripe/STPToken.h +++ b/submodules/Stripe/PublicHeaders/Stripe/STPToken.h @@ -21,7 +21,7 @@ /** * You cannot directly instantiate an `STPToken`. You should only use one that has been returned from an `STPAPIClient` callback. */ -- (nonnull instancetype) init __attribute__((unavailable("You cannot directly instantiate an STPToken. You should only use one that has been returned from an STPAPIClient callback."))); +- (nonnull instancetype) init; /** * The value of the token. You can store this value on your server and use it to make charges and customers. @see diff --git a/submodules/Svg/Sources/Svg.m b/submodules/Svg/Sources/Svg.m index 099cb481f0..eb56cc50cd 100755 --- a/submodules/Svg/Sources/Svg.m +++ b/submodules/Svg/Sources/Svg.m @@ -104,9 +104,9 @@ UIImage * _Nullable drawSvgImage(NSData * _Nonnull data, CGSize size, UIColor *b [xmlString replaceOccurrencesOfString:[NSString stringWithFormat:@"class=\"%@\"", styleName] withString:[NSString stringWithFormat:@"style=\"%@\"", styleValue] options:0 range:NSMakeRange(0, xmlString.length)]; } - char *zeroTerminatedData = xmlString.UTF8String; + const char *zeroTerminatedData = xmlString.UTF8String; - NSVGimage *image = nsvgParse(zeroTerminatedData, "px", 96); + NSVGimage *image = nsvgParse((char *)zeroTerminatedData, "px", 96); if (image == nil || image->width < 1.0f || image->height < 1.0f) { return nil; } @@ -135,7 +135,6 @@ UIImage * _Nullable drawSvgImage(NSData * _Nonnull data, CGSize size, UIColor *b } 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; @@ -173,7 +172,6 @@ UIImage * _Nullable drawSvgImage(NSData * _Nonnull data, CGSize size, UIColor *b } 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); @@ -196,10 +194,10 @@ UIImage * _Nullable drawSvgImage(NSData * _Nonnull data, CGSize size, UIColor *b CGContextSetLineJoin(context, kCGLineJoinBevel); break; case NSVG_JOIN_MITER: - CGContextSetLineCap(context, kCGLineJoinMiter); + CGContextSetLineJoin(context, kCGLineJoinMiter); break; case NSVG_JOIN_ROUND: - CGContextSetLineCap(context, kCGLineJoinRound); + CGContextSetLineJoin(context, kCGLineJoinRound); break; default: break; diff --git a/submodules/SvgRendering/BUILD b/submodules/SvgRendering/BUILD new file mode 100644 index 0000000000..20e1b4b487 --- /dev/null +++ b/submodules/SvgRendering/BUILD @@ -0,0 +1,14 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "SvgRendering", + module_name = "SvgRendering", + srcs = glob([ + "Sources/**/*.swift", + ]), + deps = [ + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/SvgRendering/Sources/SvgParser.swift b/submodules/SvgRendering/Sources/SvgParser.swift new file mode 100644 index 0000000000..e69de29bb2 diff --git a/submodules/SyncCore/Sources/CachedChannelData.swift b/submodules/SyncCore/Sources/CachedChannelData.swift index 24adca7534..5b994dc253 100644 --- a/submodules/SyncCore/Sources/CachedChannelData.swift +++ b/submodules/SyncCore/Sources/CachedChannelData.swift @@ -159,21 +159,29 @@ public final class CachedChannelData: CachedPeerData { public var id: Int64 public var accessHash: Int64 public var title: String? + public var scheduleTimestamp: Int32? + public var subscribedToScheduled: Bool public init( id: Int64, accessHash: Int64, - title: String? + title: String?, + scheduleTimestamp: Int32?, + subscribedToScheduled: Bool ) { self.id = id self.accessHash = accessHash self.title = title + self.scheduleTimestamp = scheduleTimestamp + self.subscribedToScheduled = subscribedToScheduled } public init(decoder: PostboxDecoder) { self.id = decoder.decodeInt64ForKey("id", orElse: 0) self.accessHash = decoder.decodeInt64ForKey("accessHash", orElse: 0) self.title = decoder.decodeOptionalStringForKey("title") + self.scheduleTimestamp = decoder.decodeOptionalInt32ForKey("scheduleTimestamp") + self.subscribedToScheduled = decoder.decodeBoolForKey("subscribed", orElse: false) } public func encode(_ encoder: PostboxEncoder) { @@ -184,6 +192,12 @@ public final class CachedChannelData: CachedPeerData { } else { encoder.encodeNil(forKey: "title") } + if let scheduleTimestamp = self.scheduleTimestamp { + encoder.encodeInt32(scheduleTimestamp, forKey: "scheduleTimestamp") + } else { + encoder.encodeNil(forKey: "scheduleTimestamp") + } + encoder.encodeBool(self.subscribedToScheduled, forKey: "subscribed") } } diff --git a/submodules/SyncCore/Sources/CloudFileMediaResource.swift b/submodules/SyncCore/Sources/CloudFileMediaResource.swift index adbc601e56..fa65be62e9 100644 --- a/submodules/SyncCore/Sources/CloudFileMediaResource.swift +++ b/submodules/SyncCore/Sources/CloudFileMediaResource.swift @@ -1,7 +1,7 @@ import Foundation import Postbox -public struct CloudFileMediaResourceId: MediaResourceId { +public struct CloudFileMediaResourceId: MediaResourceId, Hashable, Equatable { let datacenterId: Int let volumeId: Int64 let localId: Int32 @@ -18,13 +18,9 @@ public struct CloudFileMediaResourceId: MediaResourceId { return "telegram-cloud-file-\(self.datacenterId)-\(self.volumeId)-\(self.localId)-\(self.secret)" } - public var hashValue: Int { - return self.secret.hashValue - } - public func isEqual(to: MediaResourceId) -> Bool { if let to = to as? CloudFileMediaResourceId { - return self.datacenterId == to.datacenterId && self.volumeId == to.volumeId && self.localId == to.localId && self.secret == to.secret + return self == to } else { return false } @@ -91,7 +87,7 @@ public final class CloudFileMediaResource: TelegramMediaResource { } } -public struct CloudPhotoSizeMediaResourceId: MediaResourceId, Hashable { +public struct CloudPhotoSizeMediaResourceId: MediaResourceId, Hashable, Equatable { let datacenterId: Int32 let photoId: Int64 let sizeSpec: String @@ -108,7 +104,7 @@ public struct CloudPhotoSizeMediaResourceId: MediaResourceId, Hashable { public func isEqual(to: MediaResourceId) -> Bool { if let to = to as? CloudPhotoSizeMediaResourceId { - return self.datacenterId == to.datacenterId && self.photoId == to.photoId && self.sizeSpec == to.sizeSpec + return self == to } else { return false } @@ -120,8 +116,6 @@ public final class CloudPhotoSizeMediaResource: TelegramMediaResource { public let photoId: Int64 public let accessHash: Int64 public let sizeSpec: String - public let volumeId: Int64 - public let localId: Int32 public let size: Int? public let fileReference: Data? @@ -129,13 +123,11 @@ public final class CloudPhotoSizeMediaResource: TelegramMediaResource { return CloudPhotoSizeMediaResourceId(datacenterId: Int32(self.datacenterId), photoId: self.photoId, sizeSpec: self.sizeSpec) } - public init(datacenterId: Int32, photoId: Int64, accessHash: Int64, sizeSpec: String, volumeId: Int64, localId: Int32, size: Int?, fileReference: Data?) { + public init(datacenterId: Int32, photoId: Int64, accessHash: Int64, sizeSpec: String, size: Int?, fileReference: Data?) { self.datacenterId = Int(datacenterId) self.photoId = photoId self.accessHash = accessHash self.sizeSpec = sizeSpec - self.volumeId = volumeId - self.localId = localId self.size = size self.fileReference = fileReference } @@ -145,8 +137,6 @@ public final class CloudPhotoSizeMediaResource: TelegramMediaResource { self.photoId = decoder.decodeInt64ForKey("i", orElse: 0) self.accessHash = decoder.decodeInt64ForKey("h", orElse: 0) self.sizeSpec = decoder.decodeStringForKey("s", orElse: "") - self.volumeId = decoder.decodeInt64ForKey("v", orElse: 0) - self.localId = decoder.decodeInt32ForKey("l", orElse: 0) if let size = decoder.decodeOptionalInt32ForKey("n") { self.size = Int(size) } else { @@ -160,8 +150,6 @@ public final class CloudPhotoSizeMediaResource: TelegramMediaResource { encoder.encodeInt64(self.photoId, forKey: "i") encoder.encodeInt64(self.accessHash, forKey: "h") encoder.encodeString(self.sizeSpec, forKey: "s") - encoder.encodeInt64(self.volumeId, forKey: "v") - encoder.encodeInt32(self.localId, forKey: "l") if let size = self.size { encoder.encodeInt32(Int32(size), forKey: "n") } else { @@ -176,14 +164,14 @@ public final class CloudPhotoSizeMediaResource: TelegramMediaResource { public func isEqual(to: MediaResource) -> Bool { if let to = to as? CloudPhotoSizeMediaResource { - return self.datacenterId == to.datacenterId && self.photoId == to.photoId && self.accessHash == to.accessHash && self.sizeSpec == to.sizeSpec && self.volumeId == to.volumeId && self.localId == to.localId && self.size == to.size && self.fileReference == to.fileReference + return self.datacenterId == to.datacenterId && self.photoId == to.photoId && self.accessHash == to.accessHash && self.sizeSpec == to.sizeSpec && self.size == to.size && self.fileReference == to.fileReference } else { return false } } } -public struct CloudDocumentSizeMediaResourceId: MediaResourceId, Hashable { +public struct CloudDocumentSizeMediaResourceId: MediaResourceId, Hashable, Equatable { let datacenterId: Int32 let documentId: Int64 let sizeSpec: String @@ -200,7 +188,7 @@ public struct CloudDocumentSizeMediaResourceId: MediaResourceId, Hashable { public func isEqual(to: MediaResourceId) -> Bool { if let to = to as? CloudDocumentSizeMediaResourceId { - return self.datacenterId == to.datacenterId && self.documentId == to.documentId && self.sizeSpec == to.sizeSpec + return self == to } else { return false } @@ -212,21 +200,17 @@ public final class CloudDocumentSizeMediaResource: TelegramMediaResource { public let documentId: Int64 public let accessHash: Int64 public let sizeSpec: String - public let volumeId: Int64 - public let localId: Int32 public let fileReference: Data? public var id: MediaResourceId { return CloudDocumentSizeMediaResourceId(datacenterId: Int32(self.datacenterId), documentId: self.documentId, sizeSpec: self.sizeSpec) } - public init(datacenterId: Int32, documentId: Int64, accessHash: Int64, sizeSpec: String, volumeId: Int64, localId: Int32, fileReference: Data?) { + public init(datacenterId: Int32, documentId: Int64, accessHash: Int64, sizeSpec: String, fileReference: Data?) { self.datacenterId = Int(datacenterId) self.documentId = documentId self.accessHash = accessHash self.sizeSpec = sizeSpec - self.volumeId = volumeId - self.localId = localId self.fileReference = fileReference } @@ -235,8 +219,6 @@ public final class CloudDocumentSizeMediaResource: TelegramMediaResource { self.documentId = decoder.decodeInt64ForKey("i", orElse: 0) self.accessHash = decoder.decodeInt64ForKey("h", orElse: 0) self.sizeSpec = decoder.decodeStringForKey("s", orElse: "") - self.volumeId = decoder.decodeInt64ForKey("v", orElse: 0) - self.localId = decoder.decodeInt32ForKey("l", orElse: 0) self.fileReference = decoder.decodeBytesForKey("fr")?.makeData() } @@ -245,8 +227,6 @@ public final class CloudDocumentSizeMediaResource: TelegramMediaResource { encoder.encodeInt64(self.documentId, forKey: "i") encoder.encodeInt64(self.accessHash, forKey: "h") encoder.encodeString(self.sizeSpec, forKey: "s") - encoder.encodeInt64(self.volumeId, forKey: "v") - encoder.encodeInt32(self.localId, forKey: "l") if let fileReference = self.fileReference { encoder.encodeBytes(MemoryBuffer(data: fileReference), forKey: "fr") } else { @@ -256,7 +236,7 @@ public final class CloudDocumentSizeMediaResource: TelegramMediaResource { public func isEqual(to: MediaResource) -> Bool { if let to = to as? CloudDocumentSizeMediaResource { - return self.datacenterId == to.datacenterId && self.documentId == to.documentId && self.accessHash == to.accessHash && self.sizeSpec == to.sizeSpec && self.volumeId == to.volumeId && self.localId == to.localId && self.fileReference == to.fileReference + return self.datacenterId == to.datacenterId && self.documentId == to.documentId && self.accessHash == to.accessHash && self.sizeSpec == to.sizeSpec && self.fileReference == to.fileReference } else { return false } @@ -268,26 +248,32 @@ public enum CloudPeerPhotoSizeSpec: Int32 { case fullSize } -public struct CloudPeerPhotoSizeMediaResourceId: MediaResourceId, Hashable { +public struct CloudPeerPhotoSizeMediaResourceId: MediaResourceId, Hashable, Equatable { let datacenterId: Int32 + let photoId: Int64? let sizeSpec: CloudPeerPhotoSizeSpec - let volumeId: Int64 - let localId: Int32 + let volumeId: Int64? + let localId: Int32? - init(datacenterId: Int32, sizeSpec: CloudPeerPhotoSizeSpec, volumeId: Int64, localId: Int32) { + init(datacenterId: Int32, photoId: Int64?, sizeSpec: CloudPeerPhotoSizeSpec, volumeId: Int64?, localId: Int32?) { self.datacenterId = datacenterId + self.photoId = photoId self.sizeSpec = sizeSpec self.volumeId = volumeId self.localId = localId } public var uniqueId: String { - return "telegram-peer-photo-size-\(self.datacenterId)-\(self.sizeSpec.rawValue)-\(self.volumeId)-\(self.localId)" + if let photoId = self.photoId { + return "telegram-peer-photo-size-\(self.datacenterId)-\(photoId)-\(self.sizeSpec.rawValue)-\(self.volumeId ?? 0)-\(self.localId ?? 0)" + } else { + return "telegram-peer-photo-size-\(self.datacenterId)-\(self.sizeSpec.rawValue)-\(self.volumeId ?? 0)-\(self.localId ?? 0)" + } } public func isEqual(to: MediaResourceId) -> Bool { if let to = to as? CloudPeerPhotoSizeMediaResourceId { - return self.datacenterId == to.datacenterId && self.sizeSpec == to.sizeSpec && self.volumeId == to.volumeId && self.localId == to.localId + return self == to } else { return false } @@ -296,16 +282,18 @@ public struct CloudPeerPhotoSizeMediaResourceId: MediaResourceId, Hashable { public final class CloudPeerPhotoSizeMediaResource: TelegramMediaResource { public let datacenterId: Int + public let photoId: Int64? public let sizeSpec: CloudPeerPhotoSizeSpec - public let volumeId: Int64 - public let localId: Int32 + public let volumeId: Int64? + public let localId: Int32? public var id: MediaResourceId { - return CloudPeerPhotoSizeMediaResourceId(datacenterId: Int32(self.datacenterId), sizeSpec: self.sizeSpec, volumeId: self.volumeId, localId: self.localId) + return CloudPeerPhotoSizeMediaResourceId(datacenterId: Int32(self.datacenterId), photoId: self.photoId, sizeSpec: self.sizeSpec, volumeId: self.volumeId, localId: self.localId) } - public init(datacenterId: Int32, sizeSpec: CloudPeerPhotoSizeSpec, volumeId: Int64, localId: Int32) { + public init(datacenterId: Int32, photoId: Int64?, sizeSpec: CloudPeerPhotoSizeSpec, volumeId: Int64?, localId: Int32?) { self.datacenterId = Int(datacenterId) + self.photoId = photoId self.sizeSpec = sizeSpec self.volumeId = volumeId self.localId = localId @@ -313,45 +301,65 @@ public final class CloudPeerPhotoSizeMediaResource: TelegramMediaResource { public required init(decoder: PostboxDecoder) { self.datacenterId = Int(decoder.decodeInt32ForKey("d", orElse: 0)) + self.photoId = decoder.decodeOptionalInt64ForKey("p") self.sizeSpec = CloudPeerPhotoSizeSpec(rawValue: decoder.decodeInt32ForKey("s", orElse: 0)) ?? .small - self.volumeId = decoder.decodeInt64ForKey("v", orElse: 0) - self.localId = decoder.decodeInt32ForKey("l", orElse: 0) + self.volumeId = decoder.decodeOptionalInt64ForKey("v") + self.localId = decoder.decodeOptionalInt32ForKey("l") } public func encode(_ encoder: PostboxEncoder) { encoder.encodeInt32(Int32(self.datacenterId), forKey: "d") + if let photoId = self.photoId { + encoder.encodeInt64(photoId, forKey: "p") + } else { + encoder.encodeNil(forKey: "p") + } encoder.encodeInt32(self.sizeSpec.rawValue, forKey: "s") - encoder.encodeInt64(self.volumeId, forKey: "v") - encoder.encodeInt32(self.localId, forKey: "l") + if let volumeId = self.volumeId { + encoder.encodeInt64(volumeId, forKey: "v") + } else { + encoder.encodeNil(forKey: "v") + } + if let localId = self.localId { + encoder.encodeInt32(localId, forKey: "l") + } else { + encoder.encodeNil(forKey: "l") + } } public func isEqual(to: MediaResource) -> Bool { if let to = to as? CloudPeerPhotoSizeMediaResource { - return self.datacenterId == to.datacenterId && self.sizeSpec == to.sizeSpec && self.volumeId == to.volumeId && self.localId == to.localId + return self.datacenterId == to.datacenterId && self.photoId == to.photoId && self.sizeSpec == to.sizeSpec && self.volumeId == to.volumeId && self.localId == to.localId } else { return false } } } -public struct CloudStickerPackThumbnailMediaResourceId: MediaResourceId, Hashable { +public struct CloudStickerPackThumbnailMediaResourceId: MediaResourceId, Hashable, Equatable { let datacenterId: Int32 - let volumeId: Int64 - let localId: Int32 + let thumbVersion: Int32? + let volumeId: Int64? + let localId: Int32? - init(datacenterId: Int32, volumeId: Int64, localId: Int32) { + init(datacenterId: Int32, thumbVersion: Int32?, volumeId: Int64?, localId: Int32?) { self.datacenterId = datacenterId + self.thumbVersion = thumbVersion self.volumeId = volumeId self.localId = localId } public var uniqueId: String { - return "telegram-stickerpackthumbnail-\(self.datacenterId)-\(self.volumeId)-\(self.localId)" + if let thumbVersion = self.thumbVersion { + return "telegram-stickerpackthumbnail-\(self.datacenterId)-\(thumbVersion)-\(self.volumeId ?? 0)-\(self.localId ?? 0)" + } else { + return "telegram-stickerpackthumbnail-\(self.datacenterId)-\(self.volumeId ?? 0)-\(self.localId ?? 0)" + } } public func isEqual(to: MediaResourceId) -> Bool { if let to = to as? CloudStickerPackThumbnailMediaResourceId { - return self.datacenterId == to.datacenterId && self.volumeId == to.volumeId && self.localId == to.localId + return self == to } else { return false } @@ -360,41 +368,57 @@ public struct CloudStickerPackThumbnailMediaResourceId: MediaResourceId, Hashabl public final class CloudStickerPackThumbnailMediaResource: TelegramMediaResource { public let datacenterId: Int - public let volumeId: Int64 - public let localId: Int32 + public let thumbVersion: Int32? + public let volumeId: Int64? + public let localId: Int32? public var id: MediaResourceId { - return CloudStickerPackThumbnailMediaResourceId(datacenterId: Int32(self.datacenterId), volumeId: self.volumeId, localId: self.localId) + return CloudStickerPackThumbnailMediaResourceId(datacenterId: Int32(self.datacenterId), thumbVersion: self.thumbVersion, volumeId: self.volumeId, localId: self.localId) } - public init(datacenterId: Int32, volumeId: Int64, localId: Int32) { + public init(datacenterId: Int32, thumbVersion: Int32?, volumeId: Int64?, localId: Int32?) { self.datacenterId = Int(datacenterId) + self.thumbVersion = thumbVersion self.volumeId = volumeId self.localId = localId } public required init(decoder: PostboxDecoder) { self.datacenterId = Int(decoder.decodeInt32ForKey("d", orElse: 0)) - self.volumeId = decoder.decodeInt64ForKey("v", orElse: 0) - self.localId = decoder.decodeInt32ForKey("l", orElse: 0) + self.thumbVersion = decoder.decodeOptionalInt32ForKey("t") + self.volumeId = decoder.decodeOptionalInt64ForKey("v") + self.localId = decoder.decodeOptionalInt32ForKey("l") } public func encode(_ encoder: PostboxEncoder) { encoder.encodeInt32(Int32(self.datacenterId), forKey: "d") - encoder.encodeInt64(self.volumeId, forKey: "v") - encoder.encodeInt32(self.localId, forKey: "l") + if let thumbVersion = self.thumbVersion { + encoder.encodeInt32(thumbVersion, forKey: "t") + } else { + encoder.encodeNil(forKey: "t") + } + if let volumeId = self.volumeId { + encoder.encodeInt64(volumeId, forKey: "v") + } else { + encoder.encodeNil(forKey: "v") + } + if let localId = self.localId { + encoder.encodeInt32(localId, forKey: "l") + } else { + encoder.encodeNil(forKey: "l") + } } public func isEqual(to: MediaResource) -> Bool { if let to = to as? CloudStickerPackThumbnailMediaResource { - return self.datacenterId == to.datacenterId && self.volumeId == to.volumeId && self.localId == to.localId + return self.datacenterId == to.datacenterId && self.thumbVersion == to.thumbVersion && self.volumeId == to.volumeId && self.localId == to.localId } else { return false } } } -public struct CloudDocumentMediaResourceId: MediaResourceId { +public struct CloudDocumentMediaResourceId: MediaResourceId, Hashable, Equatable { public let datacenterId: Int public let fileId: Int64 @@ -407,13 +431,9 @@ public struct CloudDocumentMediaResourceId: MediaResourceId { return "telegram-cloud-document-\(self.datacenterId)-\(self.fileId)" } - public var hashValue: Int { - return self.fileId.hashValue - } - public func isEqual(to: MediaResourceId) -> Bool { if let to = to as? CloudDocumentMediaResourceId { - return self.datacenterId == to.datacenterId && self.fileId == to.fileId + return self == to } else { return false } @@ -484,20 +504,16 @@ public final class CloudDocumentMediaResource: TelegramMediaResource { } } -public struct LocalFileMediaResourceId: MediaResourceId { +public struct LocalFileMediaResourceId: MediaResourceId, Hashable, Equatable { public let fileId: Int64 public var uniqueId: String { return "telegram-local-file-\(self.fileId)" } - public var hashValue: Int { - return self.fileId.hashValue - } - public func isEqual(to: MediaResourceId) -> Bool { if let to = to as? LocalFileMediaResourceId { - return self.fileId == to.fileId + return self == to } else { return false } @@ -549,20 +565,16 @@ public class LocalFileMediaResource: TelegramMediaResource { } } -public struct LocalFileReferenceMediaResourceId: MediaResourceId { +public struct LocalFileReferenceMediaResourceId: MediaResourceId, Hashable, Equatable { public let randomId: Int64 public var uniqueId: String { return "local-file-\(self.randomId)" } - public var hashValue: Int { - return self.randomId.hashValue - } - public func isEqual(to: MediaResourceId) -> Bool { if let to = to as? LocalFileReferenceMediaResourceId { - return self.randomId == to.randomId + return self == to } else { return false } @@ -613,21 +625,17 @@ public class LocalFileReferenceMediaResource: TelegramMediaResource { } } -public struct HttpReferenceMediaResourceId: MediaResourceId { +public struct HttpReferenceMediaResourceId: MediaResourceId, Hashable, Equatable { public let url: String public func isEqual(to: MediaResourceId) -> Bool { if let to = to as? HttpReferenceMediaResourceId { - return self.url == to.url + return self == to } else { return false } } - public var hashValue: Int { - return self.url.hashValue - } - public var uniqueId: String { return "http-\(persistentHash32(self.url))" } @@ -673,23 +681,19 @@ public final class HttpReferenceMediaResource: TelegramMediaResource { } } -public struct WebFileReferenceMediaResourceId: MediaResourceId { +public struct WebFileReferenceMediaResourceId: MediaResourceId, Hashable, Equatable { public let url: String public let accessHash: Int64 public let size: Int32 public func isEqual(to: MediaResourceId) -> Bool { if let to = to as? WebFileReferenceMediaResourceId { - return self.url == to.url && size == to.size && accessHash == to.accessHash + return self == to } else { return false } } - public var hashValue: Int { - return self.url.hashValue - } - public var uniqueId: String { return "proxy-\(persistentHash32(self.url))-\(size)-\(accessHash)" } @@ -732,7 +736,7 @@ public final class WebFileReferenceMediaResource: TelegramMediaResource { } -public struct SecretFileMediaResourceId: MediaResourceId { +public struct SecretFileMediaResourceId: MediaResourceId, Hashable, Equatable { public let fileId: Int64 public let datacenterId: Int32 @@ -745,13 +749,9 @@ public struct SecretFileMediaResourceId: MediaResourceId { self.datacenterId = datacenterId } - public var hashValue: Int { - return self.fileId.hashValue - } - public func isEqual(to: MediaResourceId) -> Bool { if let to = to as? SecretFileMediaResourceId { - return self.fileId == to.fileId && self.datacenterId == to.datacenterId + return self == to } else { return false } @@ -859,3 +859,59 @@ public final class EmptyMediaResource: TelegramMediaResource { return to is EmptyMediaResource } } + +public struct WallpaperDataResourceId: MediaResourceId { + public var uniqueId: String { + return "wallpaper-\(self.slug)" + } + + public var hashValue: Int { + return self.slug.hashValue + } + + public var slug: String + + public init(slug: String) { + self.slug = slug + } + + public func isEqual(to: MediaResourceId) -> Bool { + guard let to = to as? WallpaperDataResourceId else { + return false + } + if self.slug != to.slug { + return false + } + return true + } +} + +public final class WallpaperDataResource: TelegramMediaResource { + public let slug: String + + public init(slug: String) { + self.slug = slug + } + + public init(decoder: PostboxDecoder) { + self.slug = decoder.decodeStringForKey("s", orElse: "") + } + + public func encode(_ encoder: PostboxEncoder) { + encoder.encodeString(self.slug, forKey: "s") + } + + public var id: MediaResourceId { + return WallpaperDataResourceId(slug: self.slug) + } + + public func isEqual(to: MediaResource) -> Bool { + guard let to = to as? WallpaperDataResource else { + return false + } + if self.slug != to.slug { + return false + } + return true + } +} diff --git a/submodules/SyncCore/Sources/Namespaces.swift b/submodules/SyncCore/Sources/Namespaces.swift index bfccdc26ec..61cde874e7 100644 --- a/submodules/SyncCore/Sources/Namespaces.swift +++ b/submodules/SyncCore/Sources/Namespaces.swift @@ -31,11 +31,11 @@ public struct Namespaces { } public struct Peer { - public static let CloudUser: Int32 = 0 - public static let CloudGroup: Int32 = 1 - public static let CloudChannel: Int32 = 2 - public static let SecretChat: Int32 = 3 - public static let Empty: Int32 = Int32.max + public static let CloudUser = PeerId.Namespace._internalFromInt32Value(0) + public static let CloudGroup = PeerId.Namespace._internalFromInt32Value(1) + public static let CloudChannel = PeerId.Namespace._internalFromInt32Value(2) + public static let SecretChat = PeerId.Namespace._internalFromInt32Value(3) + public static let Empty = PeerId.Namespace.max } public struct ItemCollection { @@ -202,7 +202,6 @@ private enum PreferencesKeyValues: Int32 { case globalNotifications = 0 case suggestedLocalization = 3 case limitsConfiguration = 4 - case coreSettings = 7 case contentPrivacySettings = 8 case networkSettings = 9 case remoteStorageConfiguration = 10 @@ -251,12 +250,6 @@ public struct PreferencesKeys { return key }() - public static let coreSettings: ValueBoxKey = { - let key = ValueBoxKey(length: 4) - key.setInt32(0, value: PreferencesKeyValues.coreSettings.rawValue) - return key - }() - public static let contentPrivacySettings: ValueBoxKey = { let key = ValueBoxKey(length: 4) key.setInt32(0, value: PreferencesKeyValues.contentPrivacySettings.rawValue) @@ -356,6 +349,7 @@ private enum SharedDataKeyValues: Int32 { case autodownloadSettings = 5 case themeSettings = 6 case countriesList = 7 + case wallapersState = 8 } public struct SharedDataKeys { @@ -400,6 +394,12 @@ public struct SharedDataKeys { key.setInt32(0, value: SharedDataKeyValues.countriesList.rawValue) return key }() + + public static let wallapersState: ValueBoxKey = { + let key = ValueBoxKey(length: 4) + key.setInt32(0, value: SharedDataKeyValues.wallapersState.rawValue) + return key + }() } public func applicationSpecificItemCacheCollectionId(_ value: Int8) -> Int8 { diff --git a/submodules/SyncCore/Sources/OutgoingMessageInfoAttribute.swift b/submodules/SyncCore/Sources/OutgoingMessageInfoAttribute.swift index 42856b9faf..9426cf01ab 100644 --- a/submodules/SyncCore/Sources/OutgoingMessageInfoAttribute.swift +++ b/submodules/SyncCore/Sources/OutgoingMessageInfoAttribute.swift @@ -19,30 +19,38 @@ public class OutgoingMessageInfoAttribute: MessageAttribute { public let uniqueId: Int64 public let flags: OutgoingMessageInfoFlags public let acknowledged: Bool + public let correlationId: Int64? - public init(uniqueId: Int64, flags: OutgoingMessageInfoFlags, acknowledged: Bool) { + public init(uniqueId: Int64, flags: OutgoingMessageInfoFlags, acknowledged: Bool, correlationId: Int64?) { self.uniqueId = uniqueId self.flags = flags self.acknowledged = acknowledged + self.correlationId = correlationId } required public init(decoder: PostboxDecoder) { self.uniqueId = decoder.decodeInt64ForKey("u", orElse: 0) self.flags = OutgoingMessageInfoFlags(rawValue: decoder.decodeInt32ForKey("f", orElse: 0)) self.acknowledged = decoder.decodeInt32ForKey("ack", orElse: 0) != 0 + self.correlationId = decoder.decodeOptionalInt64ForKey("cid") } public func encode(_ encoder: PostboxEncoder) { encoder.encodeInt64(self.uniqueId, forKey: "u") encoder.encodeInt32(self.flags.rawValue, forKey: "f") encoder.encodeInt32(self.acknowledged ? 1 : 0, forKey: "ack") + if let correlationId = self.correlationId { + encoder.encodeInt64(correlationId, forKey: "cid") + } else { + encoder.encodeNil(forKey: "cid") + } } public func withUpdatedFlags(_ flags: OutgoingMessageInfoFlags) -> OutgoingMessageInfoAttribute { - return OutgoingMessageInfoAttribute(uniqueId: self.uniqueId, flags: flags, acknowledged: self.acknowledged) + return OutgoingMessageInfoAttribute(uniqueId: self.uniqueId, flags: flags, acknowledged: self.acknowledged, correlationId: self.correlationId) } public func withUpdatedAcknowledged(_ acknowledged: Bool) -> OutgoingMessageInfoAttribute { - return OutgoingMessageInfoAttribute(uniqueId: self.uniqueId, flags: self.flags, acknowledged: acknowledged) + return OutgoingMessageInfoAttribute(uniqueId: self.uniqueId, flags: self.flags, acknowledged: acknowledged, correlationId: self.correlationId) } } diff --git a/submodules/SyncCore/Sources/PeerReference.swift b/submodules/SyncCore/Sources/PeerReference.swift index dc50bea070..9b93b4f6e6 100644 --- a/submodules/SyncCore/Sources/PeerReference.swift +++ b/submodules/SyncCore/Sources/PeerReference.swift @@ -39,15 +39,15 @@ public enum PeerReference: PostboxCoding, Hashable, Equatable { switch peer { case let user as TelegramUser: if let accessHash = user.accessHash { - self = .user(id: user.id.id, accessHash: accessHash.value) + self = .user(id: user.id.id._internalGetInt32Value(), accessHash: accessHash.value) } else { return nil } case let group as TelegramGroup: - self = .group(id: group.id.id) + self = .group(id: group.id.id._internalGetInt32Value()) case let channel as TelegramChannel: if let accessHash = channel.accessHash { - self = .channel(id: channel.id.id, accessHash: accessHash.value) + self = .channel(id: channel.id.id._internalGetInt32Value(), accessHash: accessHash.value) } else { return nil } diff --git a/submodules/SyncCore/Sources/ReplyMarkupMessageAttribute.swift b/submodules/SyncCore/Sources/ReplyMarkupMessageAttribute.swift index d1eb42c27c..33e8c4db04 100644 --- a/submodules/SyncCore/Sources/ReplyMarkupMessageAttribute.swift +++ b/submodules/SyncCore/Sources/ReplyMarkupMessageAttribute.swift @@ -150,23 +150,31 @@ public struct ReplyMarkupMessageFlags: OptionSet { public class ReplyMarkupMessageAttribute: MessageAttribute, Equatable { public let rows: [ReplyMarkupRow] public let flags: ReplyMarkupMessageFlags + public let placeholder: String? - public init(rows: [ReplyMarkupRow], flags: ReplyMarkupMessageFlags) { + public init(rows: [ReplyMarkupRow], flags: ReplyMarkupMessageFlags, placeholder: String?) { self.rows = rows self.flags = flags + self.placeholder = placeholder } public required init(decoder: PostboxDecoder) { self.rows = decoder.decodeObjectArrayWithDecoderForKey("r") self.flags = ReplyMarkupMessageFlags(rawValue: decoder.decodeInt32ForKey("f", orElse: 0)) + self.placeholder = decoder.decodeOptionalStringForKey("pl") } public func encode(_ encoder: PostboxEncoder) { encoder.encodeObjectArray(self.rows, forKey: "r") encoder.encodeInt32(self.flags.rawValue, forKey: "f") + if let placeholder = self.placeholder { + encoder.encodeString(placeholder, forKey: "pl") + } else { + encoder.encodeNil(forKey: "pl") + } } public static func ==(lhs: ReplyMarkupMessageAttribute, rhs: ReplyMarkupMessageAttribute) -> Bool { - return lhs.flags == rhs.flags && lhs.rows == rhs.rows + return lhs.flags == rhs.flags && lhs.rows == rhs.rows && lhs.placeholder == rhs.placeholder } } diff --git a/submodules/SyncCore/Sources/SecretChatState.swift b/submodules/SyncCore/Sources/SecretChatState.swift index 26164481a5..48be226cf4 100644 --- a/submodules/SyncCore/Sources/SecretChatState.swift +++ b/submodules/SyncCore/Sources/SecretChatState.swift @@ -249,7 +249,7 @@ public struct SecretChatLayerNegotiationState: PostboxCoding, Equatable { } public init(decoder: PostboxDecoder) { - self.activeLayer = SecretChatSequenceBasedLayer(rawValue: decoder.decodeInt32ForKey("a", orElse: 0)) ?? .layer46 + self.activeLayer = SecretChatSequenceBasedLayer(rawValue: decoder.decodeInt32ForKey("a", orElse: 0)) ?? .layer73 self.locallyRequestedLayer = decoder.decodeOptionalInt32ForKey("lr") self.remotelyRequestedLayer = decoder.decodeOptionalInt32ForKey("rr") } diff --git a/submodules/SyncCore/Sources/StandaloneAccountTransaction.swift b/submodules/SyncCore/Sources/StandaloneAccountTransaction.swift index 843c08ddea..d9b8c07eb7 100644 --- a/submodules/SyncCore/Sources/StandaloneAccountTransaction.swift +++ b/submodules/SyncCore/Sources/StandaloneAccountTransaction.swift @@ -38,7 +38,7 @@ public let telegramPostboxSeedConfiguration: SeedConfiguration = { globalMessageIdsPeerIdNamespaces.insert(GlobalMessageIdsNamespace(peerIdNamespace: peerIdNamespace, messageIdNamespace: Namespaces.Message.Cloud)) } - 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, upgradedMessageHoles: upgradedMessageHoles, messageThreadHoles: messageThreadHoles, existingMessageTags: MessageTags.all, messageTagsWithSummary: [.unseenPersonalMessage, .pinned], existingGlobalMessageTags: GlobalMessageTags.all, peerNamespacesRequiringMessageTextIndex: [Namespaces.Peer.SecretChat], peerSummaryCounterTags: { peer, isContact in + return SeedConfiguration(globalMessageIdsPeerIdNamespaces: globalMessageIdsPeerIdNamespaces, initializeChatListWithHole: (topLevel: ChatListHole(index: MessageIndex(id: MessageId(peerId: PeerId(namespace: Namespaces.Peer.Empty, id: PeerId.Id._internalFromInt32Value(0)), namespace: Namespaces.Message.Cloud, id: 1), timestamp: Int32.max - 1)), groups: ChatListHole(index: MessageIndex(id: MessageId(peerId: PeerId(namespace: Namespaces.Peer.Empty, id: PeerId.Id._internalFromInt32Value(0)), namespace: Namespaces.Message.Cloud, id: 1), timestamp: Int32.max - 1))), messageHoles: messageHoles, upgradedMessageHoles: upgradedMessageHoles, messageThreadHoles: messageThreadHoles, existingMessageTags: MessageTags.all, messageTagsWithSummary: [.unseenPersonalMessage, .pinned], existingGlobalMessageTags: GlobalMessageTags.all, peerNamespacesRequiringMessageTextIndex: [Namespaces.Peer.SecretChat], peerSummaryCounterTags: { peer, isContact in if let peer = peer as? TelegramUser { if peer.botInfo != nil { return .bot diff --git a/submodules/SyncCore/Sources/SynchronizePinnedChatsOperation.swift b/submodules/SyncCore/Sources/SynchronizePinnedChatsOperation.swift index 4f9fd6b5ff..02b21573c2 100644 --- a/submodules/SyncCore/Sources/SynchronizePinnedChatsOperation.swift +++ b/submodules/SyncCore/Sources/SynchronizePinnedChatsOperation.swift @@ -48,7 +48,7 @@ public func addSynchronizePinnedChatsOperation(transaction: Transaction, groupId var previousItemIds = transaction.getPinnedItemIds(groupId: groupId) var updateLocalIndex: Int32? - transaction.operationLogEnumerateEntries(peerId: PeerId(namespace: 0, id: rawId), tag: OperationLogTags.SynchronizePinnedChats, { entry in + transaction.operationLogEnumerateEntries(peerId: PeerId(namespace: PeerId.Namespace._internalFromInt32Value(0), id: PeerId.Id._internalFromInt32Value(rawId)), tag: OperationLogTags.SynchronizePinnedChats, { entry in updateLocalIndex = entry.tagLocalIndex if let contents = entry.contents as? SynchronizePinnedChatsOperation { previousItemIds = contents.previousItemIds @@ -57,7 +57,7 @@ public func addSynchronizePinnedChatsOperation(transaction: Transaction, groupId }) let operationContents = SynchronizePinnedChatsOperation(previousItemIds: previousItemIds) if let updateLocalIndex = updateLocalIndex { - let _ = transaction.operationLogRemoveEntry(peerId: PeerId(namespace: 0, id: rawId), tag: OperationLogTags.SynchronizePinnedChats, tagLocalIndex: updateLocalIndex) + let _ = transaction.operationLogRemoveEntry(peerId: PeerId(namespace: PeerId.Namespace._internalFromInt32Value(0), id: PeerId.Id._internalFromInt32Value(rawId)), tag: OperationLogTags.SynchronizePinnedChats, tagLocalIndex: updateLocalIndex) } - transaction.operationLogAddEntry(peerId: PeerId(namespace: 0, id: rawId), tag: OperationLogTags.SynchronizePinnedChats, tagLocalIndex: .automatic, tagMergedIndex: .automatic, contents: operationContents) + transaction.operationLogAddEntry(peerId: PeerId(namespace: PeerId.Namespace._internalFromInt32Value(0), id: PeerId.Id._internalFromInt32Value(rawId)), tag: OperationLogTags.SynchronizePinnedChats, tagLocalIndex: .automatic, tagMergedIndex: .automatic, contents: operationContents) } diff --git a/submodules/SyncCore/Sources/TelegramMediaAction.swift b/submodules/SyncCore/Sources/TelegramMediaAction.swift index 8da6281da5..7ad29dd436 100644 --- a/submodules/SyncCore/Sources/TelegramMediaAction.swift +++ b/submodules/SyncCore/Sources/TelegramMediaAction.swift @@ -46,7 +46,7 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable { case peerJoined case phoneNumberRequest case geoProximityReached(from: PeerId, to: PeerId, distance: Int32) - case groupPhoneCall(callId: Int64, accessHash: Int64, duration: Int32?) + case groupPhoneCall(callId: Int64, accessHash: Int64, scheduleDate: Int32?, duration: Int32?) case inviteToGroupPhoneCall(callId: Int64, accessHash: Int64, peerIds: [PeerId]) public init(decoder: PostboxDecoder) { @@ -101,7 +101,7 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable { case 21: self = .geoProximityReached(from: PeerId(decoder.decodeInt64ForKey("fromId", orElse: 0)), to: PeerId(decoder.decodeInt64ForKey("toId", orElse: 0)), distance: (decoder.decodeInt32ForKey("dst", orElse: 0))) case 22: - self = .groupPhoneCall(callId: decoder.decodeInt64ForKey("callId", orElse: 0), accessHash: decoder.decodeInt64ForKey("accessHash", orElse: 0), duration: decoder.decodeOptionalInt32ForKey("duration")) + self = .groupPhoneCall(callId: decoder.decodeInt64ForKey("callId", orElse: 0), accessHash: decoder.decodeInt64ForKey("accessHash", orElse: 0), scheduleDate: decoder.decodeOptionalInt32ForKey("scheduleDate"), duration: decoder.decodeOptionalInt32ForKey("duration")) case 23: var peerIds: [PeerId] = [] if let peerId = decoder.decodeOptionalInt64ForKey("peerId") { @@ -200,10 +200,15 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable { encoder.encodeInt64(from.toInt64(), forKey: "fromId") encoder.encodeInt64(to.toInt64(), forKey: "toId") encoder.encodeInt32(distance, forKey: "dst") - case let .groupPhoneCall(callId, accessHash, duration): + case let .groupPhoneCall(callId, accessHash, scheduleDate, duration): encoder.encodeInt32(22, forKey: "_rawValue") encoder.encodeInt64(callId, forKey: "callId") encoder.encodeInt64(accessHash, forKey: "accessHash") + if let scheduleDate = scheduleDate { + encoder.encodeInt32(scheduleDate, forKey: "scheduleDate") + } else { + encoder.encodeNil(forKey: "scheduleDate") + } if let duration = duration { encoder.encodeInt32(duration, forKey: "duration") } else { diff --git a/submodules/SyncCore/Sources/TelegramMediaFile.swift b/submodules/SyncCore/Sources/TelegramMediaFile.swift index 66f8ac0d75..9b27539e27 100644 --- a/submodules/SyncCore/Sources/TelegramMediaFile.swift +++ b/submodules/SyncCore/Sources/TelegramMediaFile.swift @@ -425,6 +425,8 @@ public final class TelegramMediaFile: Media, Equatable, Codable { if case .Sticker = attribute { if let s = self.size, s < 300 * 1024 { return !isAnimatedSticker + } else if self.size == nil { + return !isAnimatedSticker } } } @@ -577,6 +579,10 @@ public final class TelegramMediaFile: Media, Equatable, Codable { public func withUpdatedPartialReference(_ partialReference: PartialMediaReference?) -> TelegramMediaFile { return TelegramMediaFile(fileId: self.fileId, partialReference: partialReference, resource: self.resource, previewRepresentations: self.previewRepresentations, videoThumbnails: self.videoThumbnails, immediateThumbnailData: self.immediateThumbnailData, mimeType: self.mimeType, size: self.size, attributes: self.attributes) } + + public func withUpdatedResource(_ resource: TelegramMediaResource) -> TelegramMediaFile { + return TelegramMediaFile(fileId: self.fileId, partialReference: self.partialReference, resource: resource, previewRepresentations: self.previewRepresentations, videoThumbnails: self.videoThumbnails, immediateThumbnailData: self.immediateThumbnailData, mimeType: self.mimeType, size: self.size, attributes: self.attributes) + } public func withUpdatedSize(_ size: Int?) -> TelegramMediaFile { return TelegramMediaFile(fileId: self.fileId, partialReference: self.partialReference, resource: self.resource, previewRepresentations: self.previewRepresentations, videoThumbnails: self.videoThumbnails, immediateThumbnailData: self.immediateThumbnailData, mimeType: self.mimeType, size: size, attributes: self.attributes) diff --git a/submodules/SyncCore/Sources/TelegramMediaImage.swift b/submodules/SyncCore/Sources/TelegramMediaImage.swift index 644a023398..11c9c10d8c 100644 --- a/submodules/SyncCore/Sources/TelegramMediaImage.swift +++ b/submodules/SyncCore/Sources/TelegramMediaImage.swift @@ -304,17 +304,20 @@ public final class TelegramMediaImageRepresentation: PostboxCoding, Equatable, C public let dimensions: PixelDimensions public let resource: TelegramMediaResource public let progressiveSizes: [Int32] + public let immediateThumbnailData: Data? - public init(dimensions: PixelDimensions, resource: TelegramMediaResource, progressiveSizes: [Int32]) { + public init(dimensions: PixelDimensions, resource: TelegramMediaResource, progressiveSizes: [Int32], immediateThumbnailData: Data?) { self.dimensions = dimensions self.resource = resource self.progressiveSizes = progressiveSizes + self.immediateThumbnailData = immediateThumbnailData } public init(decoder: PostboxDecoder) { self.dimensions = PixelDimensions(width: decoder.decodeInt32ForKey("dx", orElse: 0), height: decoder.decodeInt32ForKey("dy", orElse: 0)) self.resource = decoder.decodeObjectForKey("r") as? TelegramMediaResource ?? EmptyMediaResource() self.progressiveSizes = decoder.decodeInt32ArrayForKey("ps") + self.immediateThumbnailData = decoder.decodeDataForKey("th") } public func encode(_ encoder: PostboxEncoder) { @@ -322,6 +325,11 @@ public final class TelegramMediaImageRepresentation: PostboxCoding, Equatable, C encoder.encodeInt32(self.dimensions.height, forKey: "dy") encoder.encodeObject(self.resource, forKey: "r") encoder.encodeInt32Array(self.progressiveSizes, forKey: "ps") + if let immediateThumbnailData = self.immediateThumbnailData { + encoder.encodeData(immediateThumbnailData, forKey: "th") + } else { + encoder.encodeNil(forKey: "th") + } } public var description: String { @@ -338,6 +346,9 @@ public final class TelegramMediaImageRepresentation: PostboxCoding, Equatable, C if self.progressiveSizes != other.progressiveSizes { return false } + if self.immediateThumbnailData != other.immediateThumbnailData { + return false + } return true } } diff --git a/submodules/SyncCore/Sources/TelegramWallpaper.swift b/submodules/SyncCore/Sources/TelegramWallpaper.swift index 7b500b04de..baf2a7aba0 100644 --- a/submodules/SyncCore/Sources/TelegramWallpaper.swift +++ b/submodules/SyncCore/Sources/TelegramWallpaper.swift @@ -1,18 +1,16 @@ import Postbox public struct WallpaperSettings: PostboxCoding, Equatable { - public let blur: Bool - public let motion: Bool - public let color: UInt32? - public let bottomColor: UInt32? - public let intensity: Int32? - public let rotation: Int32? + public var blur: Bool + public var motion: Bool + public var colors: [UInt32] + public var intensity: Int32? + public var rotation: Int32? - public init(blur: Bool = false, motion: Bool = false, color: UInt32? = nil, bottomColor: UInt32? = nil, intensity: Int32? = nil, rotation: Int32? = nil) { + public init(blur: Bool = false, motion: Bool = false, colors: [UInt32] = [], intensity: Int32? = nil, rotation: Int32? = nil) { self.blur = blur self.motion = motion - self.color = color - self.bottomColor = bottomColor + self.colors = colors self.intensity = intensity self.rotation = rotation } @@ -20,8 +18,16 @@ public struct WallpaperSettings: PostboxCoding, Equatable { 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").flatMap { UInt32(bitPattern: $0) } - self.bottomColor = decoder.decodeOptionalInt32ForKey("bc").flatMap { UInt32(bitPattern: $0) } + if let topColor = decoder.decodeOptionalInt32ForKey("c").flatMap(UInt32.init(bitPattern:)) { + var colors: [UInt32] = [topColor] + if let bottomColor = decoder.decodeOptionalInt32ForKey("bc").flatMap(UInt32.init(bitPattern:)) { + colors.append(bottomColor) + } + self.colors = colors + } else { + self.colors = decoder.decodeInt32ArrayForKey("colors").map(UInt32.init(bitPattern:)) + } + self.intensity = decoder.decodeOptionalInt32ForKey("i") self.rotation = decoder.decodeOptionalInt32ForKey("r") } @@ -29,16 +35,7 @@ public struct WallpaperSettings: PostboxCoding, Equatable { 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(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") - } + encoder.encodeInt32Array(self.colors.map(Int32.init(bitPattern:)), forKey: "colors") if let intensity = self.intensity { encoder.encodeInt32(intensity, forKey: "i") } else { @@ -58,18 +55,15 @@ public struct WallpaperSettings: PostboxCoding, Equatable { if lhs.motion != rhs.motion { return false } - if lhs.color != rhs.color { - return false - } - if lhs.bottomColor != rhs.bottomColor { + if lhs.colors != rhs.colors { return false } if lhs.intensity != rhs.intensity { - return false - } + return false + } if lhs.rotation != rhs.rotation { - return false - } + return false + } return true } } @@ -77,33 +71,45 @@ public struct WallpaperSettings: PostboxCoding, Equatable { public enum TelegramWallpaper: OrderedItemListEntryContents, Equatable { case builtin(WallpaperSettings) case color(UInt32) - case gradient(UInt32, UInt32, WallpaperSettings) + case gradient(Int64?, [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) public init(decoder: PostboxDecoder) { switch decoder.decodeInt32ForKey("v", orElse: 0) { - case 0: - let settings = decoder.decodeObjectForKey("settings", decoder: { WallpaperSettings(decoder: $0) }) as? WallpaperSettings ?? WallpaperSettings() - self = .builtin(settings) - case 1: - 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) - case 3: - let settings = decoder.decodeObjectForKey("settings", decoder: { WallpaperSettings(decoder: $0) }) as? WallpaperSettings ?? WallpaperSettings() - if let file = decoder.decodeObjectForKey("file", decoder: { TelegramMediaFile(decoder: $0) }) as? TelegramMediaFile { - self = .file(id: decoder.decodeInt64ForKey("id", orElse: 0), accessHash: decoder.decodeInt64ForKey("accessHash", orElse: 0), isCreator: decoder.decodeInt32ForKey("isCreator", orElse: 0) != 0, isDefault: decoder.decodeInt32ForKey("isDefault", orElse: 0) != 0, isPattern: decoder.decodeInt32ForKey("isPattern", orElse: 0) != 0, isDark: decoder.decodeInt32ForKey("isDark", orElse: 0) != 0, slug: decoder.decodeStringForKey("slug", orElse: ""), file: file, settings: settings) - } 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() + case 0: + let settings = decoder.decodeObjectForKey("settings", decoder: { WallpaperSettings(decoder: $0) }) as? WallpaperSettings ?? WallpaperSettings() + self = .builtin(settings) + case 1: + 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) + case 3: + let settings = decoder.decodeObjectForKey("settings", decoder: { WallpaperSettings(decoder: $0) }) as? WallpaperSettings ?? WallpaperSettings() + if let file = decoder.decodeObjectForKey("file", decoder: { TelegramMediaFile(decoder: $0) }) as? TelegramMediaFile { + self = .file(id: decoder.decodeInt64ForKey("id", orElse: 0), accessHash: decoder.decodeInt64ForKey("accessHash", orElse: 0), isCreator: decoder.decodeInt32ForKey("isCreator", orElse: 0) != 0, isDefault: decoder.decodeInt32ForKey("isDefault", orElse: 0) != 0, isPattern: decoder.decodeInt32ForKey("isPattern", orElse: 0) != 0, isDark: decoder.decodeInt32ForKey("isDark", orElse: 0) != 0, slug: decoder.decodeStringForKey("slug", orElse: ""), file: file, settings: settings) + } else { self = .color(0xffffff) + } + case 4: + let settings = decoder.decodeObjectForKey("settings", decoder: { WallpaperSettings(decoder: $0) }) as? WallpaperSettings ?? WallpaperSettings() + + var colors: [UInt32] = [] + + if let topColor = decoder.decodeOptionalInt32ForKey("c1").flatMap(UInt32.init(bitPattern:)) { + colors.append(topColor) + if let bottomColor = decoder.decodeOptionalInt32ForKey("c2").flatMap(UInt32.init(bitPattern:)) { + colors.append(bottomColor) + } + } else { + colors = decoder.decodeInt32ArrayForKey("colors").map(UInt32.init(bitPattern:)) + } + + self = .gradient(decoder.decodeOptionalInt64ForKey("id"), colors, settings) + default: + assertionFailure() + self = .color(0xffffff) } } @@ -124,10 +130,14 @@ public enum TelegramWallpaper: OrderedItemListEntryContents, Equatable { case let .color(color): encoder.encodeInt32(1, forKey: "v") encoder.encodeInt32(Int32(bitPattern: color), forKey: "c") - case let .gradient(topColor, bottomColor, settings): + case let .gradient(id, colors, settings): encoder.encodeInt32(4, forKey: "v") - encoder.encodeInt32(Int32(bitPattern: topColor), forKey: "c1") - encoder.encodeInt32(Int32(bitPattern: bottomColor), forKey: "c2") + if let id = id { + encoder.encodeInt64(id, forKey: "id") + } else { + encoder.encodeNil(forKey: "id") + } + encoder.encodeInt32Array(colors.map(Int32.init(bitPattern:)), forKey: "colors") encoder.encodeObject(settings, forKey: "settings") case let .image(representations, settings): encoder.encodeInt32(2, forKey: "v") @@ -161,8 +171,8 @@ public enum TelegramWallpaper: OrderedItemListEntryContents, Equatable { } else { return false } - case let .gradient(topColor, bottomColor, settings): - if case .gradient(topColor, bottomColor, settings) = rhs { + case let .gradient(id, colors, settings): + if case .gradient(id, colors, settings) = rhs { return true } else { return false @@ -196,8 +206,8 @@ public enum TelegramWallpaper: OrderedItemListEntryContents, Equatable { } else { return false } - case let .gradient(topColor, bottomColor, _): - if case .gradient(topColor, bottomColor, _) = wallpaper { + case let .gradient(_, colors, _): + if case .gradient(_, colors, _) = wallpaper { return true } else { return false @@ -209,7 +219,7 @@ public enum TelegramWallpaper: OrderedItemListEntryContents, Equatable { return false } case let .file(_, _, _, _, _, _, lhsSlug, _, lhsSettings): - if case let .file(_, _, _, _, _, _, rhsSlug, _, rhsSettings) = wallpaper, lhsSlug == rhsSlug, lhsSettings.color == rhsSettings.color && lhsSettings.intensity == rhsSettings.intensity { + if case let .file(_, _, _, _, _, _, rhsSlug, _, rhsSettings) = wallpaper, lhsSlug == rhsSlug, lhsSettings.colors == rhsSettings.colors && lhsSettings.intensity == rhsSettings.intensity { return true } else { return false @@ -232,12 +242,12 @@ public enum TelegramWallpaper: OrderedItemListEntryContents, Equatable { return .builtin(settings) case .color: return self - case let .gradient(topColor, bottomColor, _): - return .gradient(topColor, bottomColor, settings) + case let .gradient(id, colors, _): + return .gradient(id, colors, settings) case let .image(representations, _): return .image(representations, settings) case let .file(id, accessHash, isCreator, isDefault, isPattern, isDark, slug, file, _): - return .file(id: id, accessHash: accessHash, isCreator: isCreator, isDefault: isDefault, isPattern: settings.color != nil ? true : isPattern, isDark: isDark, slug: slug, file: file, settings: settings) + return .file(id: id, accessHash: accessHash, isCreator: isCreator, isDefault: isDefault, isPattern: isPattern, isDark: isDark, slug: slug, file: file, settings: settings) } } } diff --git a/submodules/SyncCore/Sources/wallapersState.swift b/submodules/SyncCore/Sources/wallapersState.swift new file mode 100644 index 0000000000..b3117097fa --- /dev/null +++ b/submodules/SyncCore/Sources/wallapersState.swift @@ -0,0 +1,35 @@ +import Postbox +import SwiftSignalKit + +public struct WallpapersState: PreferencesEntry, Equatable { + public var wallpapers: [TelegramWallpaper] + + public static var `default`: WallpapersState { + return WallpapersState(wallpapers: []) + } + + public init(wallpapers: [TelegramWallpaper]) { + self.wallpapers = wallpapers + } + + public init(decoder: PostboxDecoder) { + self.wallpapers = decoder.decodeObjectArrayWithDecoderForKey("wallpapers") + } + + public func encode(_ encoder: PostboxEncoder) { + encoder.encodeObjectArray(self.wallpapers, forKey: "wallpapers") + } + + public func isEqual(to: PreferencesEntry) -> Bool { + return self == (to as? WallpapersState) + } +} + +public extension WallpapersState { + static func update(transaction: AccountManagerModifier, _ f: (WallpapersState) -> WallpapersState) { + transaction.updateSharedData(SharedDataKeys.wallapersState, { current in + let item = (transaction.getSharedData(SharedDataKeys.wallapersState) as? WallpapersState) ?? WallpapersState(wallpapers: []) + return f(item) + }) + } +} diff --git a/submodules/TelegramAnimatedStickerNode/Sources/AnimatedStickerUtils.swift b/submodules/TelegramAnimatedStickerNode/Sources/AnimatedStickerUtils.swift index d39c63244e..a12fc19c98 100644 --- a/submodules/TelegramAnimatedStickerNode/Sources/AnimatedStickerUtils.swift +++ b/submodules/TelegramAnimatedStickerNode/Sources/AnimatedStickerUtils.swift @@ -136,7 +136,7 @@ public func experimentalConvertCompressedLottieToCombinedMp4(data: Data, size: C return } - let bytesPerRow = (4 * Int(size.width) + 15) & (~15) + let bytesPerRow = DeviceGraphicsContextSettings.shared.bytesPerRow(forWidth: Int(size.width)) var currentFrame: Int32 = 0 diff --git a/submodules/TelegramApi/Sources/Api0.swift b/submodules/TelegramApi/Sources/Api0.swift index eb7b7b353d..33901fcd07 100644 --- a/submodules/TelegramApi/Sources/Api0.swift +++ b/submodules/TelegramApi/Sources/Api0.swift @@ -7,7 +7,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-1255641564] = { return parseString($0) } dict[-1240849242] = { return Api.messages.StickerSet.parse_stickerSet($0) } dict[2004925620] = { return Api.GroupCall.parse_groupCallDiscarded($0) } - dict[-1061026514] = { return Api.GroupCall.parse_groupCall($0) } + dict[-711498484] = { return Api.GroupCall.parse_groupCall($0) } dict[-457104426] = { return Api.InputGeoPoint.parse_inputGeoPointEmpty($0) } dict[1210199983] = { return Api.InputGeoPoint.parse_inputGeoPoint($0) } dict[-784000893] = { return Api.payments.ValidatedRequestedInfo.parse_validatedRequestedInfo($0) } @@ -88,7 +88,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-347535331] = { return Api.RecentMeUrl.parse_recentMeUrlChatInvite($0) } dict[-1140172836] = { return Api.RecentMeUrl.parse_recentMeUrlStickerSet($0) } dict[-797791052] = { return Api.RestrictionReason.parse_restrictionReason($0) } - dict[-177282392] = { return Api.channels.ChannelParticipants.parse_channelParticipants($0) } + dict[-1699676497] = { return Api.channels.ChannelParticipants.parse_channelParticipants($0) } dict[-266911767] = { return Api.channels.ChannelParticipants.parse_channelParticipantsNotModified($0) } dict[-599948721] = { return Api.RichText.parse_textEmpty($0) } dict[1950782688] = { return Api.RichText.parse_textPlain($0) } @@ -115,14 +115,14 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-1705233435] = { return Api.account.PasswordSettings.parse_passwordSettings($0) } dict[-1945767479] = { return Api.help.SupportName.parse_supportName($0) } dict[-288727837] = { return Api.LangPackLanguage.parse_langPackLanguage($0) } - dict[-399391402] = { return Api.VideoSize.parse_videoSize($0) } - dict[497489295] = { return Api.help.AppUpdate.parse_appUpdate($0) } + dict[-567037804] = { return Api.VideoSize.parse_videoSize($0) } + dict[-860107216] = { return Api.help.AppUpdate.parse_appUpdate($0) } dict[-1000708810] = { return Api.help.AppUpdate.parse_noAppUpdate($0) } dict[-209337866] = { return Api.LangPackDifference.parse_langPackDifference($0) } - dict[84438264] = { return Api.WallPaperSettings.parse_wallPaperSettings($0) } + dict[499236004] = { return Api.WallPaperSettings.parse_wallPaperSettings($0) } dict[-1519029347] = { return Api.EmojiURL.parse_emojiURL($0) } dict[1611985938] = { return Api.StatsGroupTopAdmin.parse_statsGroupTopAdmin($0) } - dict[-791039645] = { return Api.channels.ChannelParticipant.parse_channelParticipant($0) } + dict[-541588713] = { return Api.channels.ChannelParticipant.parse_channelParticipant($0) } dict[-1736378792] = { return Api.InputCheckPasswordSRP.parse_inputCheckPasswordEmpty($0) } dict[-763367294] = { return Api.InputCheckPasswordSRP.parse_inputCheckPasswordSRP($0) } dict[-1432995067] = { return Api.storage.FileType.parse_fileUnknown($0) } @@ -142,7 +142,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[767652808] = { return Api.InputEncryptedFile.parse_inputEncryptedFileBigUploaded($0) } dict[1304052993] = { return Api.account.Takeout.parse_takeout($0) } dict[-1456996667] = { return Api.messages.InactiveChats.parse_inactiveChats($0) } - dict[430815881] = { return Api.GroupCallParticipant.parse_groupCallParticipant($0) } + dict[-341428482] = { return Api.GroupCallParticipant.parse_groupCallParticipant($0) } dict[1443858741] = { return Api.messages.SentEncryptedMessage.parse_sentEncryptedMessage($0) } dict[-1802240206] = { return Api.messages.SentEncryptedMessage.parse_sentEncryptedFile($0) } dict[289586518] = { return Api.SavedContact.parse_savedPhoneContact($0) } @@ -261,7 +261,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { 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[938909451] = { return Api.Update.parse_updateMessagePollVote($0) } dict[654302845] = { return Api.Update.parse_updateDialogFilter($0) } dict[-1512627963] = { return Api.Update.parse_updateDialogFilterOrder($0) } dict[889491791] = { return Api.Update.parse_updateDialogFilters($0) } @@ -280,15 +280,18 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-206342113] = { return Api.Update.parse_updateChatParticipant($0) } dict[2146218476] = { return Api.Update.parse_updateChannelParticipant($0) } dict[133777546] = { return Api.Update.parse_updateBotStopped($0) } + dict[192428418] = { return Api.Update.parse_updateGroupCallConnection($0) } + dict[-813823885] = { return Api.Update.parse_updateBotCommands($0) } dict[136574537] = { return Api.messages.VotesList.parse_votesList($0) } dict[1558266229] = { return Api.PopularContact.parse_popularContact($0) } + dict[-592373577] = { return Api.GroupCallParticipantVideoSourceGroup.parse_groupCallParticipantVideoSourceGroup($0) } dict[-373643672] = { return Api.FolderPeer.parse_folderPeer($0) } dict[367766557] = { return Api.ChannelParticipant.parse_channelParticipant($0) } dict[-1557620115] = { return Api.ChannelParticipant.parse_channelParticipantSelf($0) } dict[1149094475] = { return Api.ChannelParticipant.parse_channelParticipantCreator($0) } dict[-859915345] = { return Api.ChannelParticipant.parse_channelParticipantAdmin($0) } - dict[470789295] = { return Api.ChannelParticipant.parse_channelParticipantBanned($0) } - dict[-1010402965] = { return Api.ChannelParticipant.parse_channelParticipantLeft($0) } + dict[1352785878] = { return Api.ChannelParticipant.parse_channelParticipantBanned($0) } + dict[453242886] = { return Api.ChannelParticipant.parse_channelParticipantLeft($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) } @@ -313,10 +316,10 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-199313886] = { return Api.account.Themes.parse_themesNotModified($0) } dict[2137482273] = { return Api.account.Themes.parse_themes($0) } dict[236446268] = { return Api.PhotoSize.parse_photoSizeEmpty($0) } - dict[2009052699] = { return Api.PhotoSize.parse_photoSize($0) } - dict[-374917894] = { return Api.PhotoSize.parse_photoCachedSize($0) } + dict[1976012384] = { return Api.PhotoSize.parse_photoSize($0) } + dict[35527382] = { return Api.PhotoSize.parse_photoCachedSize($0) } dict[-525288402] = { return Api.PhotoSize.parse_photoStrippedSize($0) } - dict[1520986705] = { return Api.PhotoSize.parse_photoSizeProgressive($0) } + dict[-96535659] = { return Api.PhotoSize.parse_photoSizeProgressive($0) } dict[-668906175] = { return Api.PhotoSize.parse_photoPathSize($0) } dict[-244016606] = { return Api.messages.Stickers.parse_stickersNotModified($0) } dict[-463889475] = { return Api.messages.Stickers.parse_stickers($0) } @@ -328,7 +331,6 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[1881892265] = { return Api.account.WallPapers.parse_wallPapers($0) } dict[1012306921] = { return Api.InputTheme.parse_inputTheme($0) } dict[-175567375] = { return Api.InputTheme.parse_inputThemeSlug($0) } - dict[-1132476723] = { return Api.FileLocation.parse_fileLocationToBeDeprecated($0) } dict[-2032041631] = { return Api.Poll.parse_poll($0) } dict[-1195615476] = { return Api.InputNotifyPeer.parse_inputNotifyPeer($0) } dict[423314455] = { return Api.InputNotifyPeer.parse_inputNotifyUsers($0) } @@ -354,8 +356,9 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[1098628881] = { return Api.InputBotInlineMessage.parse_inputBotInlineMessageMediaVenue($0) } dict[-1494368259] = { return Api.InputBotInlineMessage.parse_inputBotInlineMessageMediaContact($0) } dict[1262639204] = { return Api.InputBotInlineMessage.parse_inputBotInlineMessageGame($0) } + dict[-672693723] = { return Api.InputBotInlineMessage.parse_inputBotInlineMessageMediaInvoice($0) } dict[2002815875] = { return Api.KeyboardButtonRow.parse_keyboardButtonRow($0) } - dict[1088567208] = { return Api.StickerSet.parse_stickerSet($0) } + dict[-673242758] = { return Api.StickerSet.parse_stickerSet($0) } dict[-1111085620] = { return Api.messages.ExportedChatInvites.parse_exportedChatInvites($0) } dict[354925740] = { return Api.SecureSecretSettings.parse_secureSecretSettings($0) } dict[539045032] = { return Api.photos.Photo.parse_photo($0) } @@ -392,7 +395,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-440664550] = { return Api.InputMedia.parse_inputMediaPhotoExternal($0) } dict[-78455655] = { return Api.InputMedia.parse_inputMediaDocumentExternal($0) } dict[-750828557] = { return Api.InputMedia.parse_inputMediaGame($0) } - dict[-186607933] = { return Api.InputMedia.parse_inputMediaInvoice($0) } + dict[-646342540] = { return Api.InputMedia.parse_inputMediaInvoice($0) } dict[-1759532989] = { return Api.InputMedia.parse_inputMediaGeoLive($0) } dict[261416433] = { return Api.InputMedia.parse_inputMediaPoll($0) } dict[-428884101] = { return Api.InputMedia.parse_inputMediaDice($0) } @@ -527,8 +530,8 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-786326563] = { return Api.InputPrivacyKey.parse_inputPrivacyKeyAddedByPhone($0) } dict[235081943] = { return Api.help.RecentMeUrls.parse_recentMeUrls($0) } dict[-1606526075] = { return Api.ReplyMarkup.parse_replyKeyboardHide($0) } - dict[-200242528] = { return Api.ReplyMarkup.parse_replyKeyboardForceReply($0) } - dict[889353612] = { return Api.ReplyMarkup.parse_replyKeyboardMarkup($0) } + dict[-2035021048] = { return Api.ReplyMarkup.parse_replyKeyboardForceReply($0) } + dict[-2049074735] = { return Api.ReplyMarkup.parse_replyKeyboardMarkup($0) } dict[1218642516] = { return Api.ReplyMarkup.parse_replyInlineMarkup($0) } dict[1556570557] = { return Api.EmojiKeywordsDifference.parse_emojiKeywordsDifference($0) } dict[1493171408] = { return Api.HighScore.parse_highScore($0) } @@ -555,6 +558,9 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[182649427] = { return Api.MessageRange.parse_messageRange($0) } dict[946083368] = { return Api.messages.StickerSetInstallResult.parse_stickerSetInstallResultSuccess($0) } dict[904138920] = { return Api.messages.StickerSetInstallResult.parse_stickerSetInstallResultArchive($0) } + dict[-478701471] = { return Api.account.ResetPasswordResult.parse_resetPasswordFailedWait($0) } + dict[-370148227] = { return Api.account.ResetPasswordResult.parse_resetPasswordRequestedWait($0) } + dict[-383330754] = { return Api.account.ResetPasswordResult.parse_resetPasswordOk($0) } dict[856375399] = { return Api.Config.parse_config($0) } dict[-75283823] = { return Api.TopPeerCategoryPeers.parse_topPeerCategoryPeers($0) } dict[-1107729093] = { return Api.Game.parse_game($0) } @@ -578,16 +584,24 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-1160215659] = { return Api.InputMessage.parse_inputMessageReplyTo($0) } dict[-2037963464] = { return Api.InputMessage.parse_inputMessagePinned($0) } dict[-1392895362] = { return Api.InputMessage.parse_inputMessageCallbackQuery($0) } + dict[1735736008] = { return Api.GroupCallParticipantVideo.parse_groupCallParticipantVideo($0) } dict[-58224696] = { return Api.PhoneCallProtocol.parse_phoneCallProtocol($0) } dict[-1237848657] = { return Api.StatsDateRangeDays.parse_statsDateRangeDays($0) } dict[-275956116] = { return Api.messages.AffectedFoundMessages.parse_affectedFoundMessages($0) } + dict[795652779] = { return Api.BotCommandScope.parse_botCommandScopeDefault($0) } + dict[1011811544] = { return Api.BotCommandScope.parse_botCommandScopeUsers($0) } + dict[1877059713] = { return Api.BotCommandScope.parse_botCommandScopeChats($0) } + dict[-1180016534] = { return Api.BotCommandScope.parse_botCommandScopeChatAdmins($0) } + dict[-610432643] = { return Api.BotCommandScope.parse_botCommandScopePeer($0) } + dict[1071145937] = { return Api.BotCommandScope.parse_botCommandScopePeerAdmins($0) } + dict[169026035] = { return Api.BotCommandScope.parse_botCommandScopePeerUser($0) } dict[-1539849235] = { return Api.WallPaper.parse_wallPaper($0) } - dict[-1963717851] = { return Api.WallPaper.parse_wallPaperNoFile($0) } + dict[-528465642] = { return Api.WallPaper.parse_wallPaperNoFile($0) } dict[-1938715001] = { return Api.messages.Messages.parse_messages($0) } dict[978610270] = { return Api.messages.Messages.parse_messagesSlice($0) } dict[1682413576] = { return Api.messages.Messages.parse_channelMessages($0) } dict[1951620897] = { return Api.messages.Messages.parse_messagesNotModified($0) } - dict[-1022713000] = { return Api.Invoice.parse_invoice($0) } + dict[215516896] = { return Api.Invoice.parse_invoice($0) } dict[1933519201] = { return Api.PeerSettings.parse_peerSettings($0) } dict[1577067778] = { return Api.auth.SentCode.parse_sentCode($0) } dict[480546647] = { return Api.InputChatPhoto.parse_inputChatPhotoEmpty($0) } @@ -635,10 +649,10 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { 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[-770990276] = { return Api.ChatPhoto.parse_chatPhoto($0) } + dict[476978193] = { return Api.ChatPhoto.parse_chatPhoto($0) } dict[1869903447] = { return Api.PageCaption.parse_pageCaption($0) } - dict[1062645411] = { return Api.payments.PaymentForm.parse_paymentForm($0) } - dict[1342771681] = { return Api.payments.PaymentReceipt.parse_paymentReceipt($0) } + dict[-1928649707] = { return Api.payments.PaymentForm.parse_paymentForm($0) } + dict[280319440] = { return Api.payments.PaymentReceipt.parse_paymentReceipt($0) } dict[863093588] = { return Api.messages.PeerDialogs.parse_peerDialogs($0) } dict[-1831650802] = { return Api.UrlAuthResult.parse_urlAuthResultRequest($0) } dict[-1886646706] = { return Api.UrlAuthResult.parse_urlAuthResultAccepted($0) } @@ -650,6 +664,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-427863538] = { return Api.InputStickerSet.parse_inputStickerSetDice($0) } dict[-1231326505] = { return Api.messages.ChatAdminsWithInvites.parse_chatAdminsWithInvites($0) } dict[-1729618630] = { return Api.BotInfo.parse_botInfo($0) } + dict[-2046910401] = { return Api.stickers.SuggestedShortName.parse_suggestedShortName($0) } dict[-1519637954] = { return Api.updates.State.parse_state($0) } dict[537022650] = { return Api.User.parse_userEmpty($0) } dict[-1820043071] = { return Api.User.parse_user($0) } @@ -666,8 +681,8 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[700340377] = { return Api.InputFileLocation.parse_inputTakeoutFileLocation($0) } dict[1075322878] = { return Api.InputFileLocation.parse_inputPhotoFileLocation($0) } dict[-667654413] = { return Api.InputFileLocation.parse_inputPhotoLegacyFileLocation($0) } - dict[668375447] = { return Api.InputFileLocation.parse_inputPeerPhotoFileLocation($0) } - dict[230353641] = { return Api.InputFileLocation.parse_inputStickerSetThumb($0) } + dict[925204121] = { return Api.InputFileLocation.parse_inputPeerPhotoFileLocation($0) } + dict[-1652231205] = { return Api.InputFileLocation.parse_inputStickerSetThumb($0) } dict[-1146808775] = { return Api.InputFileLocation.parse_inputGroupCallStream($0) } dict[286776671] = { return Api.GeoPoint.parse_geoPointEmpty($0) } dict[-1297942941] = { return Api.GeoPoint.parse_geoPoint($0) } @@ -689,7 +704,7 @@ 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[-1770371538] = { 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) } @@ -754,6 +769,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[85477117] = { return Api.BotInlineMessage.parse_botInlineMessageMediaGeo($0) } dict[-1970903652] = { return Api.BotInlineMessage.parse_botInlineMessageMediaVenue($0) } dict[416402882] = { return Api.BotInlineMessage.parse_botInlineMessageMediaContact($0) } + dict[894081801] = { return Api.BotInlineMessage.parse_botInlineMessageMediaInvoice($0) } dict[-1673717362] = { return Api.InputPeerNotifySettings.parse_inputPeerNotifySettings($0) } dict[-1634752813] = { return Api.messages.FavedStickers.parse_favedStickersNotModified($0) } dict[-209768682] = { return Api.messages.FavedStickers.parse_favedStickers($0) } @@ -783,7 +799,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-1908627474] = { return Api.SecureValueType.parse_secureValueTypeEmail($0) } dict[-732254058] = { return Api.PasswordKdfAlgo.parse_passwordKdfAlgoUnknown($0) } dict[982592842] = { return Api.PasswordKdfAlgo.parse_passwordKdfAlgoSHA256SHA256PBKDF2HMACSHA512iter100000SHA256ModPow($0) } - dict[-1390001672] = { return Api.account.Password.parse_password($0) } + dict[408623183] = { return Api.account.Password.parse_password($0) } dict[-2000710887] = { return Api.InputBotInlineResult.parse_inputBotInlineResult($0) } dict[-1462213465] = { return Api.InputBotInlineResult.parse_inputBotInlineResultPhoto($0) } dict[-459324] = { return Api.InputBotInlineResult.parse_inputBotInlineResultDocument($0) } @@ -824,6 +840,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[2047704898] = { return Api.MessageAction.parse_messageActionGroupCall($0) } dict[1991897370] = { return Api.MessageAction.parse_messageActionInviteToGroupCall($0) } dict[-1441072131] = { return Api.MessageAction.parse_messageActionSetMessagesTTL($0) } + dict[-1281329567] = { return Api.MessageAction.parse_messageActionGroupCallScheduled($0) } dict[1399245077] = { return Api.PhoneCall.parse_phoneCallEmpty($0) } dict[462375633] = { return Api.PhoneCall.parse_phoneCallWaiting($0) } dict[-2014659757] = { return Api.PhoneCall.parse_phoneCallRequested($0) } @@ -846,7 +863,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-1282352120] = { return Api.PageRelatedArticle.parse_pageRelatedArticle($0) } dict[313694676] = { return Api.StickerPack.parse_stickerPack($0) } dict[1326562017] = { return Api.UserProfilePhoto.parse_userProfilePhotoEmpty($0) } - dict[1775479590] = { return Api.UserProfilePhoto.parse_userProfilePhoto($0) } + dict[-2100168954] = { return Api.UserProfilePhoto.parse_userProfilePhoto($0) } dict[-74456004] = { return Api.payments.SavedInfo.parse_savedInfo($0) } dict[1041346555] = { return Api.updates.ChannelDifference.parse_channelDifferenceEmpty($0) } dict[-1531132162] = { return Api.updates.ChannelDifference.parse_channelDifferenceTooLong($0) } @@ -1082,6 +1099,8 @@ public struct Api { _1.serialize(buffer, boxed) case let _1 as Api.PopularContact: _1.serialize(buffer, boxed) + case let _1 as Api.GroupCallParticipantVideoSourceGroup: + _1.serialize(buffer, boxed) case let _1 as Api.FolderPeer: _1.serialize(buffer, boxed) case let _1 as Api.ChannelParticipant: @@ -1114,8 +1133,6 @@ public struct Api { _1.serialize(buffer, boxed) case let _1 as Api.InputTheme: _1.serialize(buffer, boxed) - case let _1 as Api.FileLocation: - _1.serialize(buffer, boxed) case let _1 as Api.Poll: _1.serialize(buffer, boxed) case let _1 as Api.InputNotifyPeer: @@ -1300,6 +1317,8 @@ public struct Api { _1.serialize(buffer, boxed) case let _1 as Api.messages.StickerSetInstallResult: _1.serialize(buffer, boxed) + case let _1 as Api.account.ResetPasswordResult: + _1.serialize(buffer, boxed) case let _1 as Api.Config: _1.serialize(buffer, boxed) case let _1 as Api.TopPeerCategoryPeers: @@ -1332,12 +1351,16 @@ public struct Api { _1.serialize(buffer, boxed) case let _1 as Api.InputMessage: _1.serialize(buffer, boxed) + case let _1 as Api.GroupCallParticipantVideo: + _1.serialize(buffer, boxed) case let _1 as Api.PhoneCallProtocol: _1.serialize(buffer, boxed) case let _1 as Api.StatsDateRangeDays: _1.serialize(buffer, boxed) case let _1 as Api.messages.AffectedFoundMessages: _1.serialize(buffer, boxed) + case let _1 as Api.BotCommandScope: + _1.serialize(buffer, boxed) case let _1 as Api.WallPaper: _1.serialize(buffer, boxed) case let _1 as Api.messages.Messages: @@ -1398,6 +1421,8 @@ public struct Api { _1.serialize(buffer, boxed) case let _1 as Api.BotInfo: _1.serialize(buffer, boxed) + case let _1 as Api.stickers.SuggestedShortName: + _1.serialize(buffer, boxed) case let _1 as Api.updates.State: _1.serialize(buffer, boxed) case let _1 as Api.User: diff --git a/submodules/TelegramApi/Sources/Api2.swift b/submodules/TelegramApi/Sources/Api2.swift index bcb055e5d5..c4e4a4df88 100644 --- a/submodules/TelegramApi/Sources/Api2.swift +++ b/submodules/TelegramApi/Sources/Api2.swift @@ -1,7 +1,7 @@ public extension Api { public enum GroupCall: TypeConstructorDescription { case groupCallDiscarded(id: Int64, accessHash: Int64, duration: Int32) - case groupCall(flags: Int32, id: Int64, accessHash: Int64, participantsCount: Int32, params: Api.DataJSON?, title: String?, streamDcId: Int32?, recordStartDate: Int32?, version: Int32) + case groupCall(flags: Int32, id: Int64, accessHash: Int64, participantsCount: Int32, title: String?, streamDcId: Int32?, recordStartDate: Int32?, scheduleDate: Int32?, unmutedVideoCount: Int32?, unmutedVideoLimit: Int32, version: Int32) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { @@ -13,18 +13,20 @@ public extension Api { serializeInt64(accessHash, buffer: buffer, boxed: false) serializeInt32(duration, buffer: buffer, boxed: false) break - case .groupCall(let flags, let id, let accessHash, let participantsCount, let params, let title, let streamDcId, let recordStartDate, let version): + case .groupCall(let flags, let id, let accessHash, let participantsCount, let title, let streamDcId, let recordStartDate, let scheduleDate, let unmutedVideoCount, let unmutedVideoLimit, let version): if boxed { - buffer.appendInt32(-1061026514) + buffer.appendInt32(-711498484) } serializeInt32(flags, buffer: buffer, boxed: false) serializeInt64(id, buffer: buffer, boxed: false) serializeInt64(accessHash, buffer: buffer, boxed: false) serializeInt32(participantsCount, buffer: buffer, boxed: false) - if Int(flags) & Int(1 << 0) != 0 {params!.serialize(buffer, true)} if Int(flags) & Int(1 << 3) != 0 {serializeString(title!, buffer: buffer, boxed: false)} if Int(flags) & Int(1 << 4) != 0 {serializeInt32(streamDcId!, buffer: buffer, boxed: false)} if Int(flags) & Int(1 << 5) != 0 {serializeInt32(recordStartDate!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 7) != 0 {serializeInt32(scheduleDate!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 10) != 0 {serializeInt32(unmutedVideoCount!, buffer: buffer, boxed: false)} + serializeInt32(unmutedVideoLimit, buffer: buffer, boxed: false) serializeInt32(version, buffer: buffer, boxed: false) break } @@ -34,8 +36,8 @@ public extension Api { switch self { case .groupCallDiscarded(let id, let accessHash, let duration): return ("groupCallDiscarded", [("id", id), ("accessHash", accessHash), ("duration", duration)]) - case .groupCall(let flags, let id, let accessHash, let participantsCount, let params, let title, let streamDcId, let recordStartDate, let version): - return ("groupCall", [("flags", flags), ("id", id), ("accessHash", accessHash), ("participantsCount", participantsCount), ("params", params), ("title", title), ("streamDcId", streamDcId), ("recordStartDate", recordStartDate), ("version", version)]) + case .groupCall(let flags, let id, let accessHash, let participantsCount, let title, let streamDcId, let recordStartDate, let scheduleDate, let unmutedVideoCount, let unmutedVideoLimit, let version): + return ("groupCall", [("flags", flags), ("id", id), ("accessHash", accessHash), ("participantsCount", participantsCount), ("title", title), ("streamDcId", streamDcId), ("recordStartDate", recordStartDate), ("scheduleDate", scheduleDate), ("unmutedVideoCount", unmutedVideoCount), ("unmutedVideoLimit", unmutedVideoLimit), ("version", version)]) } } @@ -65,29 +67,33 @@ public extension Api { _3 = reader.readInt64() var _4: Int32? _4 = reader.readInt32() - var _5: Api.DataJSON? - if Int(_1!) & Int(1 << 0) != 0 {if let signature = reader.readInt32() { - _5 = Api.parse(reader, signature: signature) as? Api.DataJSON - } } - var _6: String? - if Int(_1!) & Int(1 << 3) != 0 {_6 = parseString(reader) } + var _5: String? + if Int(_1!) & Int(1 << 3) != 0 {_5 = parseString(reader) } + var _6: Int32? + if Int(_1!) & Int(1 << 4) != 0 {_6 = reader.readInt32() } var _7: Int32? - if Int(_1!) & Int(1 << 4) != 0 {_7 = reader.readInt32() } + if Int(_1!) & Int(1 << 5) != 0 {_7 = reader.readInt32() } var _8: Int32? - if Int(_1!) & Int(1 << 5) != 0 {_8 = reader.readInt32() } + if Int(_1!) & Int(1 << 7) != 0 {_8 = reader.readInt32() } var _9: Int32? - _9 = reader.readInt32() + if Int(_1!) & Int(1 << 10) != 0 {_9 = reader.readInt32() } + var _10: Int32? + _10 = reader.readInt32() + var _11: Int32? + _11 = reader.readInt32() 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 - let _c6 = (Int(_1!) & Int(1 << 3) == 0) || _6 != nil - let _c7 = (Int(_1!) & Int(1 << 4) == 0) || _7 != nil - let _c8 = (Int(_1!) & Int(1 << 5) == 0) || _8 != nil - let _c9 = _9 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 { - return Api.GroupCall.groupCall(flags: _1!, id: _2!, accessHash: _3!, participantsCount: _4!, params: _5, title: _6, streamDcId: _7, recordStartDate: _8, version: _9!) + let _c5 = (Int(_1!) & Int(1 << 3) == 0) || _5 != nil + let _c6 = (Int(_1!) & Int(1 << 4) == 0) || _6 != nil + let _c7 = (Int(_1!) & Int(1 << 5) == 0) || _7 != nil + let _c8 = (Int(_1!) & Int(1 << 7) == 0) || _8 != nil + let _c9 = (Int(_1!) & Int(1 << 10) == 0) || _9 != nil + let _c10 = _10 != nil + let _c11 = _11 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 { + return Api.GroupCall.groupCall(flags: _1!, id: _2!, accessHash: _3!, participantsCount: _4!, title: _5, streamDcId: _6, recordStartDate: _7, scheduleDate: _8, unmutedVideoCount: _9, unmutedVideoLimit: _10!, version: _11!) } else { return nil @@ -3198,17 +3204,16 @@ public extension Api { } public enum VideoSize: TypeConstructorDescription { - case videoSize(flags: Int32, type: String, location: Api.FileLocation, w: Int32, h: Int32, size: Int32, videoStartTs: Double?) + case videoSize(flags: Int32, type: String, w: Int32, h: Int32, size: Int32, videoStartTs: Double?) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { - case .videoSize(let flags, let type, let location, let w, let h, let size, let videoStartTs): + case .videoSize(let flags, let type, let w, let h, let size, let videoStartTs): if boxed { - buffer.appendInt32(-399391402) + buffer.appendInt32(-567037804) } serializeInt32(flags, buffer: buffer, boxed: false) serializeString(type, buffer: buffer, boxed: false) - location.serialize(buffer, true) serializeInt32(w, buffer: buffer, boxed: false) serializeInt32(h, buffer: buffer, boxed: false) serializeInt32(size, buffer: buffer, boxed: false) @@ -3219,8 +3224,8 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { - case .videoSize(let flags, let type, let location, let w, let h, let size, let videoStartTs): - return ("videoSize", [("flags", flags), ("type", type), ("location", location), ("w", w), ("h", h), ("size", size), ("videoStartTs", videoStartTs)]) + case .videoSize(let flags, let type, let w, let h, let size, let videoStartTs): + return ("videoSize", [("flags", flags), ("type", type), ("w", w), ("h", h), ("size", size), ("videoStartTs", videoStartTs)]) } } @@ -3229,27 +3234,22 @@ public extension Api { _1 = reader.readInt32() var _2: String? _2 = parseString(reader) - var _3: Api.FileLocation? - if let signature = reader.readInt32() { - _3 = Api.parse(reader, signature: signature) as? Api.FileLocation - } + var _3: Int32? + _3 = reader.readInt32() var _4: Int32? _4 = reader.readInt32() var _5: Int32? _5 = reader.readInt32() - var _6: Int32? - _6 = reader.readInt32() - var _7: Double? - if Int(_1!) & Int(1 << 0) != 0 {_7 = reader.readDouble() } + var _6: Double? + if Int(_1!) & Int(1 << 0) != 0 {_6 = reader.readDouble() } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil let _c4 = _4 != nil let _c5 = _5 != nil - let _c6 = _6 != nil - let _c7 = (Int(_1!) & Int(1 << 0) == 0) || _7 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 { - return Api.VideoSize.videoSize(flags: _1!, type: _2!, location: _3!, w: _4!, h: _5!, size: _6!, videoStartTs: _7) + let _c6 = (Int(_1!) & Int(1 << 0) == 0) || _6 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { + return Api.VideoSize.videoSize(flags: _1!, type: _2!, w: _3!, h: _4!, size: _5!, videoStartTs: _6) } else { return nil @@ -3310,17 +3310,19 @@ public extension Api { } public enum WallPaperSettings: TypeConstructorDescription { - case wallPaperSettings(flags: Int32, backgroundColor: Int32?, secondBackgroundColor: Int32?, intensity: Int32?, rotation: Int32?) + case wallPaperSettings(flags: Int32, backgroundColor: Int32?, secondBackgroundColor: Int32?, thirdBackgroundColor: Int32?, fourthBackgroundColor: Int32?, intensity: Int32?, rotation: Int32?) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { - case .wallPaperSettings(let flags, let backgroundColor, let secondBackgroundColor, let intensity, let rotation): + case .wallPaperSettings(let flags, let backgroundColor, let secondBackgroundColor, let thirdBackgroundColor, let fourthBackgroundColor, let intensity, let rotation): if boxed { - buffer.appendInt32(84438264) + buffer.appendInt32(499236004) } 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 << 5) != 0 {serializeInt32(thirdBackgroundColor!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 6) != 0 {serializeInt32(fourthBackgroundColor!, 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 @@ -3329,8 +3331,8 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { - case .wallPaperSettings(let flags, let backgroundColor, let secondBackgroundColor, let intensity, let rotation): - return ("wallPaperSettings", [("flags", flags), ("backgroundColor", backgroundColor), ("secondBackgroundColor", secondBackgroundColor), ("intensity", intensity), ("rotation", rotation)]) + case .wallPaperSettings(let flags, let backgroundColor, let secondBackgroundColor, let thirdBackgroundColor, let fourthBackgroundColor, let intensity, let rotation): + return ("wallPaperSettings", [("flags", flags), ("backgroundColor", backgroundColor), ("secondBackgroundColor", secondBackgroundColor), ("thirdBackgroundColor", thirdBackgroundColor), ("fourthBackgroundColor", fourthBackgroundColor), ("intensity", intensity), ("rotation", rotation)]) } } @@ -3342,16 +3344,22 @@ public extension Api { var _3: Int32? if Int(_1!) & Int(1 << 4) != 0 {_3 = reader.readInt32() } var _4: Int32? - if Int(_1!) & Int(1 << 3) != 0 {_4 = reader.readInt32() } + if Int(_1!) & Int(1 << 5) != 0 {_4 = reader.readInt32() } var _5: Int32? - if Int(_1!) & Int(1 << 4) != 0 {_5 = reader.readInt32() } + if Int(_1!) & Int(1 << 6) != 0 {_5 = reader.readInt32() } + var _6: Int32? + if Int(_1!) & Int(1 << 3) != 0 {_6 = reader.readInt32() } + var _7: Int32? + if Int(_1!) & Int(1 << 4) != 0 {_7 = reader.readInt32() } let _c1 = _1 != nil let _c2 = (Int(_1!) & Int(1 << 0) == 0) || _2 != nil 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) + let _c4 = (Int(_1!) & Int(1 << 5) == 0) || _4 != nil + let _c5 = (Int(_1!) & Int(1 << 6) == 0) || _5 != nil + let _c6 = (Int(_1!) & Int(1 << 3) == 0) || _6 != nil + let _c7 = (Int(_1!) & Int(1 << 4) == 0) || _7 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 { + return Api.WallPaperSettings.wallPaperSettings(flags: _1!, backgroundColor: _2, secondBackgroundColor: _3, thirdBackgroundColor: _4, fourthBackgroundColor: _5, intensity: _6, rotation: _7) } else { return nil @@ -3604,13 +3612,13 @@ public extension Api { } public enum GroupCallParticipant: TypeConstructorDescription { - case groupCallParticipant(flags: Int32, peer: Api.Peer, date: Int32, activeDate: Int32?, source: Int32, volume: Int32?, about: String?, raiseHandRating: Int64?) + case groupCallParticipant(flags: Int32, peer: Api.Peer, date: Int32, activeDate: Int32?, source: Int32, volume: Int32?, about: String?, raiseHandRating: Int64?, video: Api.GroupCallParticipantVideo?, presentation: Api.GroupCallParticipantVideo?) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { - case .groupCallParticipant(let flags, let peer, let date, let activeDate, let source, let volume, let about, let raiseHandRating): + case .groupCallParticipant(let flags, let peer, let date, let activeDate, let source, let volume, let about, let raiseHandRating, let video, let presentation): if boxed { - buffer.appendInt32(430815881) + buffer.appendInt32(-341428482) } serializeInt32(flags, buffer: buffer, boxed: false) peer.serialize(buffer, true) @@ -3620,14 +3628,16 @@ public extension Api { if Int(flags) & Int(1 << 7) != 0 {serializeInt32(volume!, buffer: buffer, boxed: false)} if Int(flags) & Int(1 << 11) != 0 {serializeString(about!, buffer: buffer, boxed: false)} if Int(flags) & Int(1 << 13) != 0 {serializeInt64(raiseHandRating!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 6) != 0 {video!.serialize(buffer, true)} + if Int(flags) & Int(1 << 14) != 0 {presentation!.serialize(buffer, true)} break } } public func descriptionFields() -> (String, [(String, Any)]) { switch self { - case .groupCallParticipant(let flags, let peer, let date, let activeDate, let source, let volume, let about, let raiseHandRating): - return ("groupCallParticipant", [("flags", flags), ("peer", peer), ("date", date), ("activeDate", activeDate), ("source", source), ("volume", volume), ("about", about), ("raiseHandRating", raiseHandRating)]) + case .groupCallParticipant(let flags, let peer, let date, let activeDate, let source, let volume, let about, let raiseHandRating, let video, let presentation): + return ("groupCallParticipant", [("flags", flags), ("peer", peer), ("date", date), ("activeDate", activeDate), ("source", source), ("volume", volume), ("about", about), ("raiseHandRating", raiseHandRating), ("video", video), ("presentation", presentation)]) } } @@ -3650,6 +3660,14 @@ public extension Api { if Int(_1!) & Int(1 << 11) != 0 {_7 = parseString(reader) } var _8: Int64? if Int(_1!) & Int(1 << 13) != 0 {_8 = reader.readInt64() } + var _9: Api.GroupCallParticipantVideo? + if Int(_1!) & Int(1 << 6) != 0 {if let signature = reader.readInt32() { + _9 = Api.parse(reader, signature: signature) as? Api.GroupCallParticipantVideo + } } + var _10: Api.GroupCallParticipantVideo? + if Int(_1!) & Int(1 << 14) != 0 {if let signature = reader.readInt32() { + _10 = Api.parse(reader, signature: signature) as? Api.GroupCallParticipantVideo + } } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil @@ -3658,8 +3676,10 @@ public extension Api { let _c6 = (Int(_1!) & Int(1 << 7) == 0) || _6 != nil let _c7 = (Int(_1!) & Int(1 << 11) == 0) || _7 != nil let _c8 = (Int(_1!) & Int(1 << 13) == 0) || _8 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 { - return Api.GroupCallParticipant.groupCallParticipant(flags: _1!, peer: _2!, date: _3!, activeDate: _4, source: _5!, volume: _6, about: _7, raiseHandRating: _8) + let _c9 = (Int(_1!) & Int(1 << 6) == 0) || _9 != nil + let _c10 = (Int(_1!) & Int(1 << 14) == 0) || _10 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 { + return Api.GroupCallParticipant.groupCallParticipant(flags: _1!, peer: _2!, date: _3!, activeDate: _4, source: _5!, volume: _6, about: _7, raiseHandRating: _8, video: _9, presentation: _10) } else { return nil @@ -4658,7 +4678,7 @@ public extension Api { case updateTheme(theme: Api.Theme) case updateGeoLiveViewed(peer: Api.Peer, msgId: Int32) case updateLoginToken - case updateMessagePollVote(pollId: Int64, userId: Int32, options: [Buffer]) + case updateMessagePollVote(pollId: Int64, userId: Int32, options: [Buffer], qts: Int32) case updateDialogFilter(flags: Int32, id: Int32, filter: Api.DialogFilter?) case updateDialogFilterOrder(order: [Int32]) case updateDialogFilters @@ -4677,6 +4697,8 @@ public extension Api { case updateChatParticipant(flags: Int32, chatId: Int32, date: Int32, actorId: Int32, userId: Int32, prevParticipant: Api.ChatParticipant?, newParticipant: Api.ChatParticipant?, invite: Api.ExportedChatInvite?, qts: Int32) case updateChannelParticipant(flags: Int32, channelId: Int32, date: Int32, actorId: Int32, userId: Int32, prevParticipant: Api.ChannelParticipant?, newParticipant: Api.ChannelParticipant?, invite: Api.ExportedChatInvite?, qts: Int32) case updateBotStopped(userId: Int32, date: Int32, stopped: Api.Bool, qts: Int32) + case updateGroupCallConnection(flags: Int32, params: Api.DataJSON) + case updateBotCommands(peer: Api.Peer, botId: Int32, commands: [Api.BotCommand]) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { @@ -5286,9 +5308,9 @@ public extension Api { } break - case .updateMessagePollVote(let pollId, let userId, let options): + case .updateMessagePollVote(let pollId, let userId, let options, let qts): if boxed { - buffer.appendInt32(1123585836) + buffer.appendInt32(938909451) } serializeInt64(pollId, buffer: buffer, boxed: false) serializeInt32(userId, buffer: buffer, boxed: false) @@ -5297,6 +5319,7 @@ public extension Api { for item in options { serializeBytes(item, buffer: buffer, boxed: false) } + serializeInt32(qts, buffer: buffer, boxed: false) break case .updateDialogFilter(let flags, let id, let filter): if boxed { @@ -5471,6 +5494,25 @@ public extension Api { stopped.serialize(buffer, true) serializeInt32(qts, buffer: buffer, boxed: false) break + case .updateGroupCallConnection(let flags, let params): + if boxed { + buffer.appendInt32(192428418) + } + serializeInt32(flags, buffer: buffer, boxed: false) + params.serialize(buffer, true) + break + case .updateBotCommands(let peer, let botId, let commands): + if boxed { + buffer.appendInt32(-813823885) + } + peer.serialize(buffer, true) + serializeInt32(botId, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(commands.count)) + for item in commands { + item.serialize(buffer, true) + } + break } } @@ -5620,8 +5662,8 @@ public extension Api { 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)]) + case .updateMessagePollVote(let pollId, let userId, let options, let qts): + return ("updateMessagePollVote", [("pollId", pollId), ("userId", userId), ("options", options), ("qts", qts)]) case .updateDialogFilter(let flags, let id, let filter): return ("updateDialogFilter", [("flags", flags), ("id", id), ("filter", filter)]) case .updateDialogFilterOrder(let order): @@ -5658,6 +5700,10 @@ public extension Api { return ("updateChannelParticipant", [("flags", flags), ("channelId", channelId), ("date", date), ("actorId", actorId), ("userId", userId), ("prevParticipant", prevParticipant), ("newParticipant", newParticipant), ("invite", invite), ("qts", qts)]) case .updateBotStopped(let userId, let date, let stopped, let qts): return ("updateBotStopped", [("userId", userId), ("date", date), ("stopped", stopped), ("qts", qts)]) + case .updateGroupCallConnection(let flags, let params): + return ("updateGroupCallConnection", [("flags", flags), ("params", params)]) + case .updateBotCommands(let peer, let botId, let commands): + return ("updateBotCommands", [("peer", peer), ("botId", botId), ("commands", commands)]) } } @@ -6890,11 +6936,14 @@ public extension Api { if let _ = reader.readInt32() { _3 = Api.parseVector(reader, elementSignature: -1255641564, elementType: Buffer.self) } + var _4: Int32? + _4 = reader.readInt32() 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!) + let _c4 = _4 != nil + if _c1 && _c2 && _c3 && _c4 { + return Api.Update.updateMessagePollVote(pollId: _1!, userId: _2!, options: _3!, qts: _4!) } else { return nil @@ -7277,6 +7326,43 @@ public extension Api { return nil } } + public static func parse_updateGroupCallConnection(_ reader: BufferReader) -> Update? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Api.DataJSON? + if let signature = reader.readInt32() { + _2 = Api.parse(reader, signature: signature) as? Api.DataJSON + } + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.Update.updateGroupCallConnection(flags: _1!, params: _2!) + } + else { + return nil + } + } + public static func parse_updateBotCommands(_ 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() + var _3: [Api.BotCommand]? + if let _ = reader.readInt32() { + _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.BotCommand.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return Api.Update.updateBotCommands(peer: _1!, botId: _2!, commands: _3!) + } + else { + return nil + } + } } public enum PopularContact: TypeConstructorDescription { @@ -7316,6 +7402,50 @@ public extension Api { } } + } + public enum GroupCallParticipantVideoSourceGroup: TypeConstructorDescription { + case groupCallParticipantVideoSourceGroup(semantics: String, sources: [Int32]) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .groupCallParticipantVideoSourceGroup(let semantics, let sources): + if boxed { + buffer.appendInt32(-592373577) + } + serializeString(semantics, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(sources.count)) + for item in sources { + serializeInt32(item, buffer: buffer, boxed: false) + } + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .groupCallParticipantVideoSourceGroup(let semantics, let sources): + return ("groupCallParticipantVideoSourceGroup", [("semantics", semantics), ("sources", sources)]) + } + } + + public static func parse_groupCallParticipantVideoSourceGroup(_ reader: BufferReader) -> GroupCallParticipantVideoSourceGroup? { + var _1: String? + _1 = parseString(reader) + var _2: [Int32]? + if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: -1471112230, elementType: Int32.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.GroupCallParticipantVideoSourceGroup.groupCallParticipantVideoSourceGroup(semantics: _1!, sources: _2!) + } + else { + return nil + } + } + } public enum FolderPeer: TypeConstructorDescription { case folderPeer(peer: Api.Peer, folderId: Int32) @@ -7362,8 +7492,8 @@ public extension Api { case channelParticipantSelf(userId: Int32, inviterId: Int32, date: Int32) case channelParticipantCreator(flags: Int32, userId: Int32, adminRights: Api.ChatAdminRights, rank: String?) case channelParticipantAdmin(flags: Int32, userId: Int32, inviterId: Int32?, promotedBy: Int32, date: Int32, adminRights: Api.ChatAdminRights, rank: String?) - case channelParticipantBanned(flags: Int32, userId: Int32, kickedBy: Int32, date: Int32, bannedRights: Api.ChatBannedRights) - case channelParticipantLeft(userId: Int32) + case channelParticipantBanned(flags: Int32, peer: Api.Peer, kickedBy: Int32, date: Int32, bannedRights: Api.ChatBannedRights) + case channelParticipantLeft(peer: Api.Peer) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { @@ -7403,21 +7533,21 @@ public extension Api { adminRights.serialize(buffer, true) if Int(flags) & Int(1 << 2) != 0 {serializeString(rank!, buffer: buffer, boxed: false)} break - case .channelParticipantBanned(let flags, let userId, let kickedBy, let date, let bannedRights): + case .channelParticipantBanned(let flags, let peer, let kickedBy, let date, let bannedRights): if boxed { - buffer.appendInt32(470789295) + buffer.appendInt32(1352785878) } serializeInt32(flags, buffer: buffer, boxed: false) - serializeInt32(userId, buffer: buffer, boxed: false) + peer.serialize(buffer, true) serializeInt32(kickedBy, buffer: buffer, boxed: false) serializeInt32(date, buffer: buffer, boxed: false) bannedRights.serialize(buffer, true) break - case .channelParticipantLeft(let userId): + case .channelParticipantLeft(let peer): if boxed { - buffer.appendInt32(-1010402965) + buffer.appendInt32(453242886) } - serializeInt32(userId, buffer: buffer, boxed: false) + peer.serialize(buffer, true) break } } @@ -7432,10 +7562,10 @@ public extension Api { return ("channelParticipantCreator", [("flags", flags), ("userId", userId), ("adminRights", adminRights), ("rank", rank)]) case .channelParticipantAdmin(let flags, let userId, let inviterId, let promotedBy, let date, let adminRights, let rank): return ("channelParticipantAdmin", [("flags", flags), ("userId", userId), ("inviterId", inviterId), ("promotedBy", promotedBy), ("date", date), ("adminRights", adminRights), ("rank", rank)]) - case .channelParticipantBanned(let flags, let userId, let kickedBy, let date, let bannedRights): - return ("channelParticipantBanned", [("flags", flags), ("userId", userId), ("kickedBy", kickedBy), ("date", date), ("bannedRights", bannedRights)]) - case .channelParticipantLeft(let userId): - return ("channelParticipantLeft", [("userId", userId)]) + case .channelParticipantBanned(let flags, let peer, let kickedBy, let date, let bannedRights): + return ("channelParticipantBanned", [("flags", flags), ("peer", peer), ("kickedBy", kickedBy), ("date", date), ("bannedRights", bannedRights)]) + case .channelParticipantLeft(let peer): + return ("channelParticipantLeft", [("peer", peer)]) } } @@ -7526,8 +7656,10 @@ public extension Api { public static func parse_channelParticipantBanned(_ reader: BufferReader) -> ChannelParticipant? { var _1: Int32? _1 = reader.readInt32() - var _2: Int32? - _2 = reader.readInt32() + var _2: Api.Peer? + if let signature = reader.readInt32() { + _2 = Api.parse(reader, signature: signature) as? Api.Peer + } var _3: Int32? _3 = reader.readInt32() var _4: Int32? @@ -7542,18 +7674,20 @@ public extension Api { let _c4 = _4 != nil let _c5 = _5 != nil if _c1 && _c2 && _c3 && _c4 && _c5 { - return Api.ChannelParticipant.channelParticipantBanned(flags: _1!, userId: _2!, kickedBy: _3!, date: _4!, bannedRights: _5!) + return Api.ChannelParticipant.channelParticipantBanned(flags: _1!, peer: _2!, kickedBy: _3!, date: _4!, bannedRights: _5!) } else { return nil } } public static func parse_channelParticipantLeft(_ reader: BufferReader) -> ChannelParticipant? { - var _1: Int32? - _1 = reader.readInt32() + var _1: Api.Peer? + if let signature = reader.readInt32() { + _1 = Api.parse(reader, signature: signature) as? Api.Peer + } let _c1 = _1 != nil if _c1 { - return Api.ChannelParticipant.channelParticipantLeft(userId: _1!) + return Api.ChannelParticipant.channelParticipantLeft(peer: _1!) } else { return nil @@ -8127,10 +8261,10 @@ public extension Api { } public enum PhotoSize: TypeConstructorDescription { case photoSizeEmpty(type: String) - case photoSize(type: String, location: Api.FileLocation, w: Int32, h: Int32, size: Int32) - case photoCachedSize(type: String, location: Api.FileLocation, w: Int32, h: Int32, bytes: Buffer) + case photoSize(type: String, w: Int32, h: Int32, size: Int32) + case photoCachedSize(type: String, w: Int32, h: Int32, bytes: Buffer) case photoStrippedSize(type: String, bytes: Buffer) - case photoSizeProgressive(type: String, location: Api.FileLocation, w: Int32, h: Int32, sizes: [Int32]) + case photoSizeProgressive(type: String, w: Int32, h: Int32, sizes: [Int32]) case photoPathSize(type: String, bytes: Buffer) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { @@ -8141,22 +8275,20 @@ public extension Api { } serializeString(type, buffer: buffer, boxed: false) break - case .photoSize(let type, let location, let w, let h, let size): + case .photoSize(let type, let w, let h, let size): if boxed { - buffer.appendInt32(2009052699) + buffer.appendInt32(1976012384) } serializeString(type, buffer: buffer, boxed: false) - location.serialize(buffer, true) serializeInt32(w, buffer: buffer, boxed: false) serializeInt32(h, buffer: buffer, boxed: false) serializeInt32(size, buffer: buffer, boxed: false) break - case .photoCachedSize(let type, let location, let w, let h, let bytes): + case .photoCachedSize(let type, let w, let h, let bytes): if boxed { - buffer.appendInt32(-374917894) + buffer.appendInt32(35527382) } serializeString(type, buffer: buffer, boxed: false) - location.serialize(buffer, true) serializeInt32(w, buffer: buffer, boxed: false) serializeInt32(h, buffer: buffer, boxed: false) serializeBytes(bytes, buffer: buffer, boxed: false) @@ -8168,12 +8300,11 @@ public extension Api { serializeString(type, buffer: buffer, boxed: false) serializeBytes(bytes, buffer: buffer, boxed: false) break - case .photoSizeProgressive(let type, let location, let w, let h, let sizes): + case .photoSizeProgressive(let type, let w, let h, let sizes): if boxed { - buffer.appendInt32(1520986705) + buffer.appendInt32(-96535659) } serializeString(type, buffer: buffer, boxed: false) - location.serialize(buffer, true) serializeInt32(w, buffer: buffer, boxed: false) serializeInt32(h, buffer: buffer, boxed: false) buffer.appendInt32(481674261) @@ -8196,14 +8327,14 @@ public extension Api { switch self { case .photoSizeEmpty(let type): return ("photoSizeEmpty", [("type", type)]) - case .photoSize(let type, let location, let w, let h, let size): - return ("photoSize", [("type", type), ("location", location), ("w", w), ("h", h), ("size", size)]) - case .photoCachedSize(let type, let location, let w, let h, let bytes): - return ("photoCachedSize", [("type", type), ("location", location), ("w", w), ("h", h), ("bytes", bytes)]) + case .photoSize(let type, let w, let h, let size): + return ("photoSize", [("type", type), ("w", w), ("h", h), ("size", size)]) + case .photoCachedSize(let type, let w, let h, let bytes): + return ("photoCachedSize", [("type", type), ("w", w), ("h", h), ("bytes", bytes)]) case .photoStrippedSize(let type, let bytes): return ("photoStrippedSize", [("type", type), ("bytes", bytes)]) - case .photoSizeProgressive(let type, let location, let w, let h, let sizes): - return ("photoSizeProgressive", [("type", type), ("location", location), ("w", w), ("h", h), ("sizes", sizes)]) + case .photoSizeProgressive(let type, let w, let h, let sizes): + return ("photoSizeProgressive", [("type", type), ("w", w), ("h", h), ("sizes", sizes)]) case .photoPathSize(let type, let bytes): return ("photoPathSize", [("type", type), ("bytes", bytes)]) } @@ -8223,23 +8354,18 @@ public extension Api { public static func parse_photoSize(_ reader: BufferReader) -> PhotoSize? { var _1: String? _1 = parseString(reader) - var _2: Api.FileLocation? - if let signature = reader.readInt32() { - _2 = Api.parse(reader, signature: signature) as? Api.FileLocation - } + var _2: Int32? + _2 = reader.readInt32() var _3: Int32? _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 - let _c5 = _5 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 { - return Api.PhotoSize.photoSize(type: _1!, location: _2!, w: _3!, h: _4!, size: _5!) + if _c1 && _c2 && _c3 && _c4 { + return Api.PhotoSize.photoSize(type: _1!, w: _2!, h: _3!, size: _4!) } else { return nil @@ -8248,23 +8374,18 @@ public extension Api { public static func parse_photoCachedSize(_ reader: BufferReader) -> PhotoSize? { var _1: String? _1 = parseString(reader) - var _2: Api.FileLocation? - if let signature = reader.readInt32() { - _2 = Api.parse(reader, signature: signature) as? Api.FileLocation - } + var _2: Int32? + _2 = reader.readInt32() var _3: Int32? _3 = reader.readInt32() - var _4: Int32? - _4 = reader.readInt32() - var _5: Buffer? - _5 = parseBytes(reader) + var _4: Buffer? + _4 = parseBytes(reader) 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.PhotoSize.photoCachedSize(type: _1!, location: _2!, w: _3!, h: _4!, bytes: _5!) + if _c1 && _c2 && _c3 && _c4 { + return Api.PhotoSize.photoCachedSize(type: _1!, w: _2!, h: _3!, bytes: _4!) } else { return nil @@ -8287,25 +8408,20 @@ public extension Api { public static func parse_photoSizeProgressive(_ reader: BufferReader) -> PhotoSize? { var _1: String? _1 = parseString(reader) - var _2: Api.FileLocation? - if let signature = reader.readInt32() { - _2 = Api.parse(reader, signature: signature) as? Api.FileLocation - } + var _2: Int32? + _2 = reader.readInt32() var _3: Int32? _3 = reader.readInt32() - var _4: Int32? - _4 = reader.readInt32() - var _5: [Int32]? + var _4: [Int32]? if let _ = reader.readInt32() { - _5 = Api.parseVector(reader, elementSignature: -1471112230, elementType: Int32.self) + _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 - let _c5 = _5 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 { - return Api.PhotoSize.photoSizeProgressive(type: _1!, location: _2!, w: _3!, h: _4!, sizes: _5!) + if _c1 && _c2 && _c3 && _c4 { + return Api.PhotoSize.photoSizeProgressive(type: _1!, w: _2!, h: _3!, sizes: _4!) } else { return nil @@ -8462,44 +8578,6 @@ public extension Api { } } - } - public enum FileLocation: TypeConstructorDescription { - case fileLocationToBeDeprecated(volumeId: Int64, localId: Int32) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .fileLocationToBeDeprecated(let volumeId, let localId): - if boxed { - buffer.appendInt32(-1132476723) - } - serializeInt64(volumeId, buffer: buffer, boxed: false) - serializeInt32(localId, buffer: buffer, boxed: false) - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .fileLocationToBeDeprecated(let volumeId, let localId): - return ("fileLocationToBeDeprecated", [("volumeId", volumeId), ("localId", localId)]) - } - } - - public static func parse_fileLocationToBeDeprecated(_ reader: BufferReader) -> FileLocation? { - var _1: Int64? - _1 = reader.readInt64() - var _2: Int32? - _2 = reader.readInt32() - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return Api.FileLocation.fileLocationToBeDeprecated(volumeId: _1!, localId: _2!) - } - else { - return nil - } - } - } public enum Poll: TypeConstructorDescription { case poll(id: Int64, flags: Int32, question: String, answers: [Api.PollAnswer], closePeriod: Int32?, closeDate: Int32?) @@ -9068,6 +9146,7 @@ public extension Api { case inputBotInlineMessageMediaVenue(flags: Int32, geoPoint: Api.InputGeoPoint, title: String, address: String, provider: String, venueId: String, venueType: String, replyMarkup: Api.ReplyMarkup?) case inputBotInlineMessageMediaContact(flags: Int32, phoneNumber: String, firstName: String, lastName: String, vcard: String, replyMarkup: Api.ReplyMarkup?) case inputBotInlineMessageGame(flags: Int32, replyMarkup: Api.ReplyMarkup?) + case inputBotInlineMessageMediaInvoice(flags: Int32, title: String, description: String, photo: Api.InputWebDocument?, invoice: Api.Invoice, payload: Buffer, provider: String, providerData: Api.DataJSON, replyMarkup: Api.ReplyMarkup?) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { @@ -9139,6 +9218,20 @@ public extension Api { serializeInt32(flags, buffer: buffer, boxed: false) if Int(flags) & Int(1 << 2) != 0 {replyMarkup!.serialize(buffer, true)} break + case .inputBotInlineMessageMediaInvoice(let flags, let title, let description, let photo, let invoice, let payload, let provider, let providerData, let replyMarkup): + if boxed { + buffer.appendInt32(-672693723) + } + serializeInt32(flags, buffer: buffer, boxed: false) + serializeString(title, buffer: buffer, boxed: false) + serializeString(description, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 0) != 0 {photo!.serialize(buffer, true)} + invoice.serialize(buffer, true) + serializeBytes(payload, buffer: buffer, boxed: false) + serializeString(provider, buffer: buffer, boxed: false) + providerData.serialize(buffer, true) + if Int(flags) & Int(1 << 2) != 0 {replyMarkup!.serialize(buffer, true)} + break } } @@ -9156,6 +9249,8 @@ public extension Api { return ("inputBotInlineMessageMediaContact", [("flags", flags), ("phoneNumber", phoneNumber), ("firstName", firstName), ("lastName", lastName), ("vcard", vcard), ("replyMarkup", replyMarkup)]) case .inputBotInlineMessageGame(let flags, let replyMarkup): return ("inputBotInlineMessageGame", [("flags", flags), ("replyMarkup", replyMarkup)]) + case .inputBotInlineMessageMediaInvoice(let flags, let title, let description, let photo, let invoice, let payload, let provider, let providerData, let replyMarkup): + return ("inputBotInlineMessageMediaInvoice", [("flags", flags), ("title", title), ("description", description), ("photo", photo), ("invoice", invoice), ("payload", payload), ("provider", provider), ("providerData", providerData), ("replyMarkup", replyMarkup)]) } } @@ -9317,6 +9412,49 @@ public extension Api { return nil } } + public static func parse_inputBotInlineMessageMediaInvoice(_ reader: BufferReader) -> InputBotInlineMessage? { + var _1: Int32? + _1 = reader.readInt32() + var _2: String? + _2 = parseString(reader) + var _3: String? + _3 = parseString(reader) + var _4: Api.InputWebDocument? + if Int(_1!) & Int(1 << 0) != 0 {if let signature = reader.readInt32() { + _4 = Api.parse(reader, signature: signature) as? Api.InputWebDocument + } } + var _5: Api.Invoice? + if let signature = reader.readInt32() { + _5 = Api.parse(reader, signature: signature) as? Api.Invoice + } + var _6: Buffer? + _6 = parseBytes(reader) + var _7: String? + _7 = parseString(reader) + var _8: Api.DataJSON? + if let signature = reader.readInt32() { + _8 = Api.parse(reader, signature: signature) as? Api.DataJSON + } + var _9: Api.ReplyMarkup? + if Int(_1!) & Int(1 << 2) != 0 {if let signature = reader.readInt32() { + _9 = Api.parse(reader, signature: signature) as? Api.ReplyMarkup + } } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = (Int(_1!) & Int(1 << 0) == 0) || _4 != nil + let _c5 = _5 != nil + let _c6 = _6 != nil + let _c7 = _7 != nil + let _c8 = _8 != nil + let _c9 = (Int(_1!) & Int(1 << 2) == 0) || _9 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 { + return Api.InputBotInlineMessage.inputBotInlineMessageMediaInvoice(flags: _1!, title: _2!, description: _3!, photo: _4, invoice: _5!, payload: _6!, provider: _7!, providerData: _8!, replyMarkup: _9) + } + else { + return nil + } + } } public enum KeyboardButtonRow: TypeConstructorDescription { @@ -9360,13 +9498,13 @@ public extension Api { } public enum StickerSet: TypeConstructorDescription { - case stickerSet(flags: Int32, installedDate: Int32?, id: Int64, accessHash: Int64, title: String, shortName: String, thumbs: [Api.PhotoSize]?, thumbDcId: Int32?, count: Int32, hash: Int32) + case stickerSet(flags: Int32, installedDate: Int32?, id: Int64, accessHash: Int64, title: String, shortName: String, thumbs: [Api.PhotoSize]?, thumbDcId: Int32?, thumbVersion: Int32?, count: Int32, hash: Int32) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { - case .stickerSet(let flags, let installedDate, let id, let accessHash, let title, let shortName, let thumbs, let thumbDcId, let count, let hash): + case .stickerSet(let flags, let installedDate, let id, let accessHash, let title, let shortName, let thumbs, let thumbDcId, let thumbVersion, let count, let hash): if boxed { - buffer.appendInt32(1088567208) + buffer.appendInt32(-673242758) } serializeInt32(flags, buffer: buffer, boxed: false) if Int(flags) & Int(1 << 0) != 0 {serializeInt32(installedDate!, buffer: buffer, boxed: false)} @@ -9380,6 +9518,7 @@ public extension Api { item.serialize(buffer, true) }} if Int(flags) & Int(1 << 4) != 0 {serializeInt32(thumbDcId!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 4) != 0 {serializeInt32(thumbVersion!, buffer: buffer, boxed: false)} serializeInt32(count, buffer: buffer, boxed: false) serializeInt32(hash, buffer: buffer, boxed: false) break @@ -9388,8 +9527,8 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { - case .stickerSet(let flags, let installedDate, let id, let accessHash, let title, let shortName, let thumbs, let thumbDcId, let count, let hash): - return ("stickerSet", [("flags", flags), ("installedDate", installedDate), ("id", id), ("accessHash", accessHash), ("title", title), ("shortName", shortName), ("thumbs", thumbs), ("thumbDcId", thumbDcId), ("count", count), ("hash", hash)]) + case .stickerSet(let flags, let installedDate, let id, let accessHash, let title, let shortName, let thumbs, let thumbDcId, let thumbVersion, let count, let hash): + return ("stickerSet", [("flags", flags), ("installedDate", installedDate), ("id", id), ("accessHash", accessHash), ("title", title), ("shortName", shortName), ("thumbs", thumbs), ("thumbDcId", thumbDcId), ("thumbVersion", thumbVersion), ("count", count), ("hash", hash)]) } } @@ -9413,9 +9552,11 @@ public extension Api { var _8: Int32? if Int(_1!) & Int(1 << 4) != 0 {_8 = reader.readInt32() } var _9: Int32? - _9 = reader.readInt32() + if Int(_1!) & Int(1 << 4) != 0 {_9 = reader.readInt32() } var _10: Int32? _10 = reader.readInt32() + var _11: Int32? + _11 = reader.readInt32() let _c1 = _1 != nil let _c2 = (Int(_1!) & Int(1 << 0) == 0) || _2 != nil let _c3 = _3 != nil @@ -9424,10 +9565,11 @@ public extension Api { let _c6 = _6 != nil let _c7 = (Int(_1!) & Int(1 << 4) == 0) || _7 != nil let _c8 = (Int(_1!) & Int(1 << 4) == 0) || _8 != nil - let _c9 = _9 != nil + let _c9 = (Int(_1!) & Int(1 << 4) == 0) || _9 != nil let _c10 = _10 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 { - return Api.StickerSet.stickerSet(flags: _1!, installedDate: _2, id: _3!, accessHash: _4!, title: _5!, shortName: _6!, thumbs: _7, thumbDcId: _8, count: _9!, hash: _10!) + let _c11 = _11 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 { + return Api.StickerSet.stickerSet(flags: _1!, installedDate: _2, id: _3!, accessHash: _4!, title: _5!, shortName: _6!, thumbs: _7, thumbDcId: _8, thumbVersion: _9, count: _10!, hash: _11!) } else { return nil @@ -9919,7 +10061,7 @@ public extension Api { case inputMediaPhotoExternal(flags: Int32, url: String, ttlSeconds: Int32?) case inputMediaDocumentExternal(flags: Int32, url: String, ttlSeconds: Int32?) case inputMediaGame(id: Api.InputGame) - case inputMediaInvoice(flags: Int32, title: String, description: String, photo: Api.InputWebDocument?, invoice: Api.Invoice, payload: Buffer, provider: String, providerData: Api.DataJSON, startParam: String) + case inputMediaInvoice(flags: Int32, title: String, description: String, photo: Api.InputWebDocument?, invoice: Api.Invoice, payload: Buffer, provider: String, providerData: Api.DataJSON, startParam: String?) case inputMediaGeoLive(flags: Int32, geoPoint: Api.InputGeoPoint, heading: Int32?, period: Int32?, proximityNotificationRadius: Int32?) case inputMediaPoll(flags: Int32, poll: Api.Poll, correctAnswers: [Buffer]?, solution: String?, solutionEntities: [Api.MessageEntity]?) case inputMediaDice(emoticon: String) @@ -10032,7 +10174,7 @@ public extension Api { break case .inputMediaInvoice(let flags, let title, let description, let photo, let invoice, let payload, let provider, let providerData, let startParam): if boxed { - buffer.appendInt32(-186607933) + buffer.appendInt32(-646342540) } serializeInt32(flags, buffer: buffer, boxed: false) serializeString(title, buffer: buffer, boxed: false) @@ -10042,7 +10184,7 @@ public extension Api { serializeBytes(payload, buffer: buffer, boxed: false) serializeString(provider, buffer: buffer, boxed: false) providerData.serialize(buffer, true) - serializeString(startParam, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 1) != 0 {serializeString(startParam!, buffer: buffer, boxed: false)} break case .inputMediaGeoLive(let flags, let geoPoint, let heading, let period, let proximityNotificationRadius): if boxed { @@ -10353,7 +10495,7 @@ public extension Api { _8 = Api.parse(reader, signature: signature) as? Api.DataJSON } var _9: String? - _9 = parseString(reader) + if Int(_1!) & Int(1 << 1) != 0 {_9 = parseString(reader) } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil @@ -10362,9 +10504,9 @@ public extension Api { let _c6 = _6 != nil let _c7 = _7 != nil let _c8 = _8 != nil - let _c9 = _9 != nil + let _c9 = (Int(_1!) & Int(1 << 1) == 0) || _9 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 { - return Api.InputMedia.inputMediaInvoice(flags: _1!, title: _2!, description: _3!, photo: _4, invoice: _5!, payload: _6!, provider: _7!, providerData: _8!, startParam: _9!) + return Api.InputMedia.inputMediaInvoice(flags: _1!, title: _2!, description: _3!, photo: _4, invoice: _5!, payload: _6!, provider: _7!, providerData: _8!, startParam: _9) } else { return nil @@ -13209,8 +13351,8 @@ public extension Api { } public enum ReplyMarkup: TypeConstructorDescription { case replyKeyboardHide(flags: Int32) - case replyKeyboardForceReply(flags: Int32) - case replyKeyboardMarkup(flags: Int32, rows: [Api.KeyboardButtonRow]) + case replyKeyboardForceReply(flags: Int32, placeholder: String?) + case replyKeyboardMarkup(flags: Int32, rows: [Api.KeyboardButtonRow], placeholder: String?) case replyInlineMarkup(rows: [Api.KeyboardButtonRow]) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { @@ -13221,15 +13363,16 @@ public extension Api { } serializeInt32(flags, buffer: buffer, boxed: false) break - case .replyKeyboardForceReply(let flags): + case .replyKeyboardForceReply(let flags, let placeholder): if boxed { - buffer.appendInt32(-200242528) + buffer.appendInt32(-2035021048) } serializeInt32(flags, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 3) != 0 {serializeString(placeholder!, buffer: buffer, boxed: false)} break - case .replyKeyboardMarkup(let flags, let rows): + case .replyKeyboardMarkup(let flags, let rows, let placeholder): if boxed { - buffer.appendInt32(889353612) + buffer.appendInt32(-2049074735) } serializeInt32(flags, buffer: buffer, boxed: false) buffer.appendInt32(481674261) @@ -13237,6 +13380,7 @@ public extension Api { for item in rows { item.serialize(buffer, true) } + if Int(flags) & Int(1 << 3) != 0 {serializeString(placeholder!, buffer: buffer, boxed: false)} break case .replyInlineMarkup(let rows): if boxed { @@ -13255,10 +13399,10 @@ public extension Api { switch self { case .replyKeyboardHide(let flags): return ("replyKeyboardHide", [("flags", flags)]) - case .replyKeyboardForceReply(let flags): - return ("replyKeyboardForceReply", [("flags", flags)]) - case .replyKeyboardMarkup(let flags, let rows): - return ("replyKeyboardMarkup", [("flags", flags), ("rows", rows)]) + case .replyKeyboardForceReply(let flags, let placeholder): + return ("replyKeyboardForceReply", [("flags", flags), ("placeholder", placeholder)]) + case .replyKeyboardMarkup(let flags, let rows, let placeholder): + return ("replyKeyboardMarkup", [("flags", flags), ("rows", rows), ("placeholder", placeholder)]) case .replyInlineMarkup(let rows): return ("replyInlineMarkup", [("rows", rows)]) } @@ -13278,9 +13422,12 @@ public extension Api { public static func parse_replyKeyboardForceReply(_ reader: BufferReader) -> ReplyMarkup? { var _1: Int32? _1 = reader.readInt32() + var _2: String? + if Int(_1!) & Int(1 << 3) != 0 {_2 = parseString(reader) } let _c1 = _1 != nil - if _c1 { - return Api.ReplyMarkup.replyKeyboardForceReply(flags: _1!) + let _c2 = (Int(_1!) & Int(1 << 3) == 0) || _2 != nil + if _c1 && _c2 { + return Api.ReplyMarkup.replyKeyboardForceReply(flags: _1!, placeholder: _2) } else { return nil @@ -13293,10 +13440,13 @@ public extension Api { if let _ = reader.readInt32() { _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.KeyboardButtonRow.self) } + var _3: String? + if Int(_1!) & Int(1 << 3) != 0 {_3 = parseString(reader) } let _c1 = _1 != nil let _c2 = _2 != nil - if _c1 && _c2 { - return Api.ReplyMarkup.replyKeyboardMarkup(flags: _1!, rows: _2!) + let _c3 = (Int(_1!) & Int(1 << 3) == 0) || _3 != nil + if _c1 && _c2 && _c3 { + return Api.ReplyMarkup.replyKeyboardMarkup(flags: _1!, rows: _2!, placeholder: _3) } else { return nil @@ -14834,6 +14984,58 @@ public extension Api { } } + } + public enum GroupCallParticipantVideo: TypeConstructorDescription { + case groupCallParticipantVideo(flags: Int32, endpoint: String, sourceGroups: [Api.GroupCallParticipantVideoSourceGroup], audioSource: Int32?) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .groupCallParticipantVideo(let flags, let endpoint, let sourceGroups, let audioSource): + if boxed { + buffer.appendInt32(1735736008) + } + serializeInt32(flags, buffer: buffer, boxed: false) + serializeString(endpoint, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(sourceGroups.count)) + for item in sourceGroups { + item.serialize(buffer, true) + } + if Int(flags) & Int(1 << 1) != 0 {serializeInt32(audioSource!, buffer: buffer, boxed: false)} + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .groupCallParticipantVideo(let flags, let endpoint, let sourceGroups, let audioSource): + return ("groupCallParticipantVideo", [("flags", flags), ("endpoint", endpoint), ("sourceGroups", sourceGroups), ("audioSource", audioSource)]) + } + } + + public static func parse_groupCallParticipantVideo(_ reader: BufferReader) -> GroupCallParticipantVideo? { + var _1: Int32? + _1 = reader.readInt32() + var _2: String? + _2 = parseString(reader) + var _3: [Api.GroupCallParticipantVideoSourceGroup]? + if let _ = reader.readInt32() { + _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.GroupCallParticipantVideoSourceGroup.self) + } + var _4: Int32? + if Int(_1!) & Int(1 << 1) != 0 {_4 = reader.readInt32() } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = (Int(_1!) & Int(1 << 1) == 0) || _4 != nil + if _c1 && _c2 && _c3 && _c4 { + return Api.GroupCallParticipantVideo.groupCallParticipantVideo(flags: _1!, endpoint: _2!, sourceGroups: _3!, audioSource: _4) + } + else { + return nil + } + } + } public enum PhoneCallProtocol: TypeConstructorDescription { case phoneCallProtocol(flags: Int32, minLayer: Int32, maxLayer: Int32, libraryVersions: [String]) @@ -14924,10 +15126,144 @@ public extension Api { } } + } + public enum BotCommandScope: TypeConstructorDescription { + case botCommandScopeDefault + case botCommandScopeUsers + case botCommandScopeChats + case botCommandScopeChatAdmins + case botCommandScopePeer(peer: Api.InputPeer) + case botCommandScopePeerAdmins(peer: Api.InputPeer) + case botCommandScopePeerUser(peer: Api.InputPeer, userId: Api.InputUser) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .botCommandScopeDefault: + if boxed { + buffer.appendInt32(795652779) + } + + break + case .botCommandScopeUsers: + if boxed { + buffer.appendInt32(1011811544) + } + + break + case .botCommandScopeChats: + if boxed { + buffer.appendInt32(1877059713) + } + + break + case .botCommandScopeChatAdmins: + if boxed { + buffer.appendInt32(-1180016534) + } + + break + case .botCommandScopePeer(let peer): + if boxed { + buffer.appendInt32(-610432643) + } + peer.serialize(buffer, true) + break + case .botCommandScopePeerAdmins(let peer): + if boxed { + buffer.appendInt32(1071145937) + } + peer.serialize(buffer, true) + break + case .botCommandScopePeerUser(let peer, let userId): + if boxed { + buffer.appendInt32(169026035) + } + peer.serialize(buffer, true) + userId.serialize(buffer, true) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .botCommandScopeDefault: + return ("botCommandScopeDefault", []) + case .botCommandScopeUsers: + return ("botCommandScopeUsers", []) + case .botCommandScopeChats: + return ("botCommandScopeChats", []) + case .botCommandScopeChatAdmins: + return ("botCommandScopeChatAdmins", []) + case .botCommandScopePeer(let peer): + return ("botCommandScopePeer", [("peer", peer)]) + case .botCommandScopePeerAdmins(let peer): + return ("botCommandScopePeerAdmins", [("peer", peer)]) + case .botCommandScopePeerUser(let peer, let userId): + return ("botCommandScopePeerUser", [("peer", peer), ("userId", userId)]) + } + } + + public static func parse_botCommandScopeDefault(_ reader: BufferReader) -> BotCommandScope? { + return Api.BotCommandScope.botCommandScopeDefault + } + public static func parse_botCommandScopeUsers(_ reader: BufferReader) -> BotCommandScope? { + return Api.BotCommandScope.botCommandScopeUsers + } + public static func parse_botCommandScopeChats(_ reader: BufferReader) -> BotCommandScope? { + return Api.BotCommandScope.botCommandScopeChats + } + public static func parse_botCommandScopeChatAdmins(_ reader: BufferReader) -> BotCommandScope? { + return Api.BotCommandScope.botCommandScopeChatAdmins + } + public static func parse_botCommandScopePeer(_ reader: BufferReader) -> BotCommandScope? { + var _1: Api.InputPeer? + if let signature = reader.readInt32() { + _1 = Api.parse(reader, signature: signature) as? Api.InputPeer + } + let _c1 = _1 != nil + if _c1 { + return Api.BotCommandScope.botCommandScopePeer(peer: _1!) + } + else { + return nil + } + } + public static func parse_botCommandScopePeerAdmins(_ reader: BufferReader) -> BotCommandScope? { + var _1: Api.InputPeer? + if let signature = reader.readInt32() { + _1 = Api.parse(reader, signature: signature) as? Api.InputPeer + } + let _c1 = _1 != nil + if _c1 { + return Api.BotCommandScope.botCommandScopePeerAdmins(peer: _1!) + } + else { + return nil + } + } + public static func parse_botCommandScopePeerUser(_ reader: BufferReader) -> BotCommandScope? { + var _1: Api.InputPeer? + if let signature = reader.readInt32() { + _1 = Api.parse(reader, signature: signature) as? Api.InputPeer + } + var _2: Api.InputUser? + if let signature = reader.readInt32() { + _2 = Api.parse(reader, signature: signature) as? Api.InputUser + } + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.BotCommandScope.botCommandScopePeerUser(peer: _1!, userId: _2!) + } + else { + return nil + } + } + } 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?) + case wallPaperNoFile(id: Int64, flags: Int32, settings: Api.WallPaperSettings?) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { @@ -14942,10 +15278,11 @@ 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): + case .wallPaperNoFile(let id, let flags, let settings): if boxed { - buffer.appendInt32(-1963717851) + buffer.appendInt32(-528465642) } + serializeInt64(id, buffer: buffer, boxed: false) serializeInt32(flags, buffer: buffer, boxed: false) if Int(flags) & Int(1 << 2) != 0 {settings!.serialize(buffer, true)} break @@ -14956,8 +15293,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)]) + case .wallPaperNoFile(let id, let flags, let settings): + return ("wallPaperNoFile", [("id", id), ("flags", flags), ("settings", settings)]) } } @@ -14992,16 +15329,19 @@ public extension Api { } } 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 + var _1: Int64? + _1 = reader.readInt64() + var _2: Int32? + _2 = reader.readInt32() + var _3: Api.WallPaperSettings? + if Int(_2!) & Int(1 << 2) != 0 {if let signature = reader.readInt32() { + _3 = 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) + let _c2 = _2 != nil + let _c3 = (Int(_2!) & Int(1 << 2) == 0) || _3 != nil + if _c1 && _c2 && _c3 { + return Api.WallPaper.wallPaperNoFile(id: _1!, flags: _2!, settings: _3) } else { return nil @@ -15010,13 +15350,13 @@ public extension Api { } public enum Invoice: TypeConstructorDescription { - case invoice(flags: Int32, currency: String, prices: [Api.LabeledPrice]) + case invoice(flags: Int32, currency: String, prices: [Api.LabeledPrice], maxTipAmount: Int64?, suggestedTipAmounts: [Int64]?) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { - case .invoice(let flags, let currency, let prices): + case .invoice(let flags, let currency, let prices, let maxTipAmount, let suggestedTipAmounts): if boxed { - buffer.appendInt32(-1022713000) + buffer.appendInt32(215516896) } serializeInt32(flags, buffer: buffer, boxed: false) serializeString(currency, buffer: buffer, boxed: false) @@ -15025,14 +15365,20 @@ public extension Api { for item in prices { item.serialize(buffer, true) } + if Int(flags) & Int(1 << 8) != 0 {serializeInt64(maxTipAmount!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 8) != 0 {buffer.appendInt32(481674261) + buffer.appendInt32(Int32(suggestedTipAmounts!.count)) + for item in suggestedTipAmounts! { + serializeInt64(item, buffer: buffer, boxed: false) + }} break } } public func descriptionFields() -> (String, [(String, Any)]) { switch self { - case .invoice(let flags, let currency, let prices): - return ("invoice", [("flags", flags), ("currency", currency), ("prices", prices)]) + case .invoice(let flags, let currency, let prices, let maxTipAmount, let suggestedTipAmounts): + return ("invoice", [("flags", flags), ("currency", currency), ("prices", prices), ("maxTipAmount", maxTipAmount), ("suggestedTipAmounts", suggestedTipAmounts)]) } } @@ -15045,11 +15391,19 @@ public extension Api { if let _ = reader.readInt32() { _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.LabeledPrice.self) } + var _4: Int64? + if Int(_1!) & Int(1 << 8) != 0 {_4 = reader.readInt64() } + var _5: [Int64]? + if Int(_1!) & Int(1 << 8) != 0 {if let _ = reader.readInt32() { + _5 = 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.Invoice.invoice(flags: _1!, currency: _2!, prices: _3!) + let _c4 = (Int(_1!) & Int(1 << 8) == 0) || _4 != nil + let _c5 = (Int(_1!) & Int(1 << 8) == 0) || _5 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 { + return Api.Invoice.invoice(flags: _1!, currency: _2!, prices: _3!, maxTipAmount: _4, suggestedTipAmounts: _5) } else { return nil @@ -16315,7 +16669,7 @@ public extension Api { } public enum ChatPhoto: TypeConstructorDescription { case chatPhotoEmpty - case chatPhoto(flags: Int32, photoSmall: Api.FileLocation, photoBig: Api.FileLocation, dcId: Int32) + case chatPhoto(flags: Int32, photoId: Int64, strippedThumb: Buffer?, dcId: Int32) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { @@ -16325,13 +16679,13 @@ public extension Api { } break - case .chatPhoto(let flags, let photoSmall, let photoBig, let dcId): + case .chatPhoto(let flags, let photoId, let strippedThumb, let dcId): if boxed { - buffer.appendInt32(-770990276) + buffer.appendInt32(476978193) } serializeInt32(flags, buffer: buffer, boxed: false) - photoSmall.serialize(buffer, true) - photoBig.serialize(buffer, true) + serializeInt64(photoId, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 1) != 0 {serializeBytes(strippedThumb!, buffer: buffer, boxed: false)} serializeInt32(dcId, buffer: buffer, boxed: false) break } @@ -16341,8 +16695,8 @@ public extension Api { switch self { case .chatPhotoEmpty: return ("chatPhotoEmpty", []) - case .chatPhoto(let flags, let photoSmall, let photoBig, let dcId): - return ("chatPhoto", [("flags", flags), ("photoSmall", photoSmall), ("photoBig", photoBig), ("dcId", dcId)]) + case .chatPhoto(let flags, let photoId, let strippedThumb, let dcId): + return ("chatPhoto", [("flags", flags), ("photoId", photoId), ("strippedThumb", strippedThumb), ("dcId", dcId)]) } } @@ -16352,22 +16706,18 @@ public extension Api { public static func parse_chatPhoto(_ reader: BufferReader) -> ChatPhoto? { var _1: Int32? _1 = reader.readInt32() - var _2: Api.FileLocation? - if let signature = reader.readInt32() { - _2 = Api.parse(reader, signature: signature) as? Api.FileLocation - } - var _3: Api.FileLocation? - if let signature = reader.readInt32() { - _3 = Api.parse(reader, signature: signature) as? Api.FileLocation - } + var _2: Int64? + _2 = reader.readInt64() + var _3: Buffer? + if Int(_1!) & Int(1 << 1) != 0 {_3 = parseBytes(reader) } var _4: Int32? _4 = reader.readInt32() let _c1 = _1 != nil let _c2 = _2 != nil - let _c3 = _3 != nil + let _c3 = (Int(_1!) & Int(1 << 1) == 0) || _3 != nil let _c4 = _4 != nil if _c1 && _c2 && _c3 && _c4 { - return Api.ChatPhoto.chatPhoto(flags: _1!, photoSmall: _2!, photoBig: _3!, dcId: _4!) + return Api.ChatPhoto.chatPhoto(flags: _1!, photoId: _2!, strippedThumb: _3, dcId: _4!) } else { return nil @@ -17023,8 +17373,8 @@ public extension Api { case inputTakeoutFileLocation case inputPhotoFileLocation(id: Int64, accessHash: Int64, fileReference: Buffer, thumbSize: String) case inputPhotoLegacyFileLocation(id: Int64, accessHash: Int64, fileReference: Buffer, volumeId: Int64, localId: Int32, secret: Int64) - case inputPeerPhotoFileLocation(flags: Int32, peer: Api.InputPeer, volumeId: Int64, localId: Int32) - case inputStickerSetThumb(stickerset: Api.InputStickerSet, volumeId: Int64, localId: Int32) + case inputPeerPhotoFileLocation(flags: Int32, peer: Api.InputPeer, photoId: Int64) + case inputStickerSetThumb(stickerset: Api.InputStickerSet, thumbVersion: Int32) case inputGroupCallStream(call: Api.InputGroupCall, timeMs: Int64, scale: Int32) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { @@ -17087,22 +17437,20 @@ public extension Api { serializeInt32(localId, buffer: buffer, boxed: false) serializeInt64(secret, buffer: buffer, boxed: false) break - case .inputPeerPhotoFileLocation(let flags, let peer, let volumeId, let localId): + case .inputPeerPhotoFileLocation(let flags, let peer, let photoId): if boxed { - buffer.appendInt32(668375447) + buffer.appendInt32(925204121) } serializeInt32(flags, buffer: buffer, boxed: false) peer.serialize(buffer, true) - serializeInt64(volumeId, buffer: buffer, boxed: false) - serializeInt32(localId, buffer: buffer, boxed: false) + serializeInt64(photoId, buffer: buffer, boxed: false) break - case .inputStickerSetThumb(let stickerset, let volumeId, let localId): + case .inputStickerSetThumb(let stickerset, let thumbVersion): if boxed { - buffer.appendInt32(230353641) + buffer.appendInt32(-1652231205) } stickerset.serialize(buffer, true) - serializeInt64(volumeId, buffer: buffer, boxed: false) - serializeInt32(localId, buffer: buffer, boxed: false) + serializeInt32(thumbVersion, buffer: buffer, boxed: false) break case .inputGroupCallStream(let call, let timeMs, let scale): if boxed { @@ -17131,10 +17479,10 @@ public extension Api { return ("inputPhotoFileLocation", [("id", id), ("accessHash", accessHash), ("fileReference", fileReference), ("thumbSize", thumbSize)]) case .inputPhotoLegacyFileLocation(let id, let accessHash, let fileReference, let volumeId, let localId, let secret): return ("inputPhotoLegacyFileLocation", [("id", id), ("accessHash", accessHash), ("fileReference", fileReference), ("volumeId", volumeId), ("localId", localId), ("secret", secret)]) - case .inputPeerPhotoFileLocation(let flags, let peer, let volumeId, let localId): - return ("inputPeerPhotoFileLocation", [("flags", flags), ("peer", peer), ("volumeId", volumeId), ("localId", localId)]) - case .inputStickerSetThumb(let stickerset, let volumeId, let localId): - return ("inputStickerSetThumb", [("stickerset", stickerset), ("volumeId", volumeId), ("localId", localId)]) + case .inputPeerPhotoFileLocation(let flags, let peer, let photoId): + return ("inputPeerPhotoFileLocation", [("flags", flags), ("peer", peer), ("photoId", photoId)]) + case .inputStickerSetThumb(let stickerset, let thumbVersion): + return ("inputStickerSetThumb", [("stickerset", stickerset), ("thumbVersion", thumbVersion)]) case .inputGroupCallStream(let call, let timeMs, let scale): return ("inputGroupCallStream", [("call", call), ("timeMs", timeMs), ("scale", scale)]) } @@ -17266,14 +17614,11 @@ public extension Api { } var _3: Int64? _3 = reader.readInt64() - 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.InputFileLocation.inputPeerPhotoFileLocation(flags: _1!, peer: _2!, volumeId: _3!, localId: _4!) + if _c1 && _c2 && _c3 { + return Api.InputFileLocation.inputPeerPhotoFileLocation(flags: _1!, peer: _2!, photoId: _3!) } else { return nil @@ -17284,15 +17629,12 @@ public extension Api { if let signature = reader.readInt32() { _1 = Api.parse(reader, signature: signature) as? Api.InputStickerSet } - var _2: Int64? - _2 = reader.readInt64() - var _3: Int32? - _3 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() let _c1 = _1 != nil let _c2 = _2 != nil - let _c3 = _3 != nil - if _c1 && _c2 && _c3 { - return Api.InputFileLocation.inputStickerSetThumb(stickerset: _1!, volumeId: _2!, localId: _3!) + if _c1 && _c2 { + return Api.InputFileLocation.inputStickerSetThumb(stickerset: _1!, thumbVersion: _2!) } else { return nil @@ -17972,7 +18314,7 @@ public extension Api { public enum InputWallPaper: TypeConstructorDescription { case inputWallPaper(id: Int64, accessHash: Int64) case inputWallPaperSlug(slug: String) - case inputWallPaperNoFile + case inputWallPaperNoFile(id: Int64) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { @@ -17989,11 +18331,11 @@ public extension Api { } serializeString(slug, buffer: buffer, boxed: false) break - case .inputWallPaperNoFile: + case .inputWallPaperNoFile(let id): if boxed { - buffer.appendInt32(-2077770836) + buffer.appendInt32(-1770371538) } - + serializeInt64(id, buffer: buffer, boxed: false) break } } @@ -18004,8 +18346,8 @@ public extension Api { return ("inputWallPaper", [("id", id), ("accessHash", accessHash)]) case .inputWallPaperSlug(let slug): return ("inputWallPaperSlug", [("slug", slug)]) - case .inputWallPaperNoFile: - return ("inputWallPaperNoFile", []) + case .inputWallPaperNoFile(let id): + return ("inputWallPaperNoFile", [("id", id)]) } } @@ -18035,7 +18377,15 @@ public extension Api { } } public static func parse_inputWallPaperNoFile(_ reader: BufferReader) -> InputWallPaper? { - return Api.InputWallPaper.inputWallPaperNoFile + var _1: Int64? + _1 = reader.readInt64() + let _c1 = _1 != nil + if _c1 { + return Api.InputWallPaper.inputWallPaperNoFile(id: _1!) + } + else { + return nil + } } } @@ -19163,6 +19513,7 @@ public extension Api { case botInlineMessageMediaGeo(flags: Int32, geo: Api.GeoPoint, heading: Int32?, period: Int32?, proximityNotificationRadius: Int32?, replyMarkup: Api.ReplyMarkup?) case botInlineMessageMediaVenue(flags: Int32, geo: Api.GeoPoint, title: String, address: String, provider: String, venueId: String, venueType: String, replyMarkup: Api.ReplyMarkup?) case botInlineMessageMediaContact(flags: Int32, phoneNumber: String, firstName: String, lastName: String, vcard: String, replyMarkup: Api.ReplyMarkup?) + case botInlineMessageMediaInvoice(flags: Int32, title: String, description: String, photo: Api.WebDocument?, currency: String, totalAmount: Int64, replyMarkup: Api.ReplyMarkup?) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { @@ -19227,6 +19578,18 @@ public extension Api { serializeString(vcard, buffer: buffer, boxed: false) if Int(flags) & Int(1 << 2) != 0 {replyMarkup!.serialize(buffer, true)} break + case .botInlineMessageMediaInvoice(let flags, let title, let description, let photo, let currency, let totalAmount, let replyMarkup): + if boxed { + buffer.appendInt32(894081801) + } + serializeInt32(flags, buffer: buffer, boxed: false) + serializeString(title, buffer: buffer, boxed: false) + serializeString(description, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 0) != 0 {photo!.serialize(buffer, true)} + serializeString(currency, buffer: buffer, boxed: false) + serializeInt64(totalAmount, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 2) != 0 {replyMarkup!.serialize(buffer, true)} + break } } @@ -19242,6 +19605,8 @@ public extension Api { return ("botInlineMessageMediaVenue", [("flags", flags), ("geo", geo), ("title", title), ("address", address), ("provider", provider), ("venueId", venueId), ("venueType", venueType), ("replyMarkup", replyMarkup)]) case .botInlineMessageMediaContact(let flags, let phoneNumber, let firstName, let lastName, let vcard, let replyMarkup): return ("botInlineMessageMediaContact", [("flags", flags), ("phoneNumber", phoneNumber), ("firstName", firstName), ("lastName", lastName), ("vcard", vcard), ("replyMarkup", replyMarkup)]) + case .botInlineMessageMediaInvoice(let flags, let title, let description, let photo, let currency, let totalAmount, let replyMarkup): + return ("botInlineMessageMediaInvoice", [("flags", flags), ("title", title), ("description", description), ("photo", photo), ("currency", currency), ("totalAmount", totalAmount), ("replyMarkup", replyMarkup)]) } } @@ -19387,6 +19752,39 @@ public extension Api { return nil } } + public static func parse_botInlineMessageMediaInvoice(_ reader: BufferReader) -> BotInlineMessage? { + var _1: Int32? + _1 = reader.readInt32() + var _2: String? + _2 = parseString(reader) + var _3: String? + _3 = parseString(reader) + var _4: Api.WebDocument? + if Int(_1!) & Int(1 << 0) != 0 {if let signature = reader.readInt32() { + _4 = Api.parse(reader, signature: signature) as? Api.WebDocument + } } + var _5: String? + _5 = parseString(reader) + var _6: Int64? + _6 = reader.readInt64() + var _7: Api.ReplyMarkup? + if Int(_1!) & Int(1 << 2) != 0 {if let signature = reader.readInt32() { + _7 = Api.parse(reader, signature: signature) as? Api.ReplyMarkup + } } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = (Int(_1!) & Int(1 << 0) == 0) || _4 != nil + let _c5 = _5 != nil + let _c6 = _6 != nil + let _c7 = (Int(_1!) & Int(1 << 2) == 0) || _7 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 { + return Api.BotInlineMessage.botInlineMessageMediaInvoice(flags: _1!, title: _2!, description: _3!, photo: _4, currency: _5!, totalAmount: _6!, replyMarkup: _7) + } + else { + return nil + } + } } public enum InputPeerNotifySettings: TypeConstructorDescription { @@ -20369,6 +20767,7 @@ public extension Api { case messageActionGroupCall(flags: Int32, call: Api.InputGroupCall, duration: Int32?) case messageActionInviteToGroupCall(call: Api.InputGroupCall, users: [Int32]) case messageActionSetMessagesTTL(period: Int32) + case messageActionGroupCallScheduled(call: Api.InputGroupCall, scheduleDate: Int32) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { @@ -20573,6 +20972,13 @@ public extension Api { } serializeInt32(period, buffer: buffer, boxed: false) break + case .messageActionGroupCallScheduled(let call, let scheduleDate): + if boxed { + buffer.appendInt32(-1281329567) + } + call.serialize(buffer, true) + serializeInt32(scheduleDate, buffer: buffer, boxed: false) + break } } @@ -20632,6 +21038,8 @@ public extension Api { return ("messageActionInviteToGroupCall", [("call", call), ("users", users)]) case .messageActionSetMessagesTTL(let period): return ("messageActionSetMessagesTTL", [("period", period)]) + case .messageActionGroupCallScheduled(let call, let scheduleDate): + return ("messageActionGroupCallScheduled", [("call", call), ("scheduleDate", scheduleDate)]) } } @@ -20969,6 +21377,22 @@ public extension Api { return nil } } + public static func parse_messageActionGroupCallScheduled(_ reader: BufferReader) -> MessageAction? { + var _1: Api.InputGroupCall? + if let signature = reader.readInt32() { + _1 = Api.parse(reader, signature: signature) as? Api.InputGroupCall + } + var _2: Int32? + _2 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.MessageAction.messageActionGroupCallScheduled(call: _1!, scheduleDate: _2!) + } + else { + return nil + } + } } public enum PhoneCall: TypeConstructorDescription { @@ -21769,7 +22193,7 @@ public extension Api { } public enum UserProfilePhoto: TypeConstructorDescription { case userProfilePhotoEmpty - case userProfilePhoto(flags: Int32, photoId: Int64, photoSmall: Api.FileLocation, photoBig: Api.FileLocation, dcId: Int32) + case userProfilePhoto(flags: Int32, photoId: Int64, strippedThumb: Buffer?, dcId: Int32) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { @@ -21779,14 +22203,13 @@ public extension Api { } break - case .userProfilePhoto(let flags, let photoId, let photoSmall, let photoBig, let dcId): + case .userProfilePhoto(let flags, let photoId, let strippedThumb, let dcId): if boxed { - buffer.appendInt32(1775479590) + buffer.appendInt32(-2100168954) } serializeInt32(flags, buffer: buffer, boxed: false) serializeInt64(photoId, buffer: buffer, boxed: false) - photoSmall.serialize(buffer, true) - photoBig.serialize(buffer, true) + if Int(flags) & Int(1 << 1) != 0 {serializeBytes(strippedThumb!, buffer: buffer, boxed: false)} serializeInt32(dcId, buffer: buffer, boxed: false) break } @@ -21796,8 +22219,8 @@ public extension Api { switch self { case .userProfilePhotoEmpty: return ("userProfilePhotoEmpty", []) - case .userProfilePhoto(let flags, let photoId, let photoSmall, let photoBig, let dcId): - return ("userProfilePhoto", [("flags", flags), ("photoId", photoId), ("photoSmall", photoSmall), ("photoBig", photoBig), ("dcId", dcId)]) + case .userProfilePhoto(let flags, let photoId, let strippedThumb, let dcId): + return ("userProfilePhoto", [("flags", flags), ("photoId", photoId), ("strippedThumb", strippedThumb), ("dcId", dcId)]) } } @@ -21809,23 +22232,16 @@ public extension Api { _1 = reader.readInt32() var _2: Int64? _2 = reader.readInt64() - var _3: Api.FileLocation? - if let signature = reader.readInt32() { - _3 = Api.parse(reader, signature: signature) as? Api.FileLocation - } - var _4: Api.FileLocation? - if let signature = reader.readInt32() { - _4 = Api.parse(reader, signature: signature) as? Api.FileLocation - } - var _5: Int32? - _5 = reader.readInt32() + var _3: Buffer? + if Int(_1!) & Int(1 << 1) != 0 {_3 = parseBytes(reader) } + var _4: Int32? + _4 = reader.readInt32() let _c1 = _1 != nil let _c2 = _2 != nil - let _c3 = _3 != nil + let _c3 = (Int(_1!) & Int(1 << 1) == 0) || _3 != nil let _c4 = _4 != nil - let _c5 = _5 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 { - return Api.UserProfilePhoto.userProfilePhoto(flags: _1!, photoId: _2!, photoSmall: _3!, photoBig: _4!, dcId: _5!) + if _c1 && _c2 && _c3 && _c4 { + return Api.UserProfilePhoto.userProfilePhoto(flags: _1!, photoId: _2!, strippedThumb: _3, dcId: _4!) } else { return nil diff --git a/submodules/TelegramApi/Sources/Api3.swift b/submodules/TelegramApi/Sources/Api3.swift index 7f9aceb2f5..da75f10e38 100644 --- a/submodules/TelegramApi/Sources/Api3.swift +++ b/submodules/TelegramApi/Sources/Api3.swift @@ -1,14 +1,14 @@ public extension Api { public struct channels { public enum ChannelParticipants: TypeConstructorDescription { - case channelParticipants(count: Int32, participants: [Api.ChannelParticipant], users: [Api.User]) + case channelParticipants(count: Int32, participants: [Api.ChannelParticipant], chats: [Api.Chat], users: [Api.User]) case channelParticipantsNotModified public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { - case .channelParticipants(let count, let participants, let users): + case .channelParticipants(let count, let participants, let chats, let users): if boxed { - buffer.appendInt32(-177282392) + buffer.appendInt32(-1699676497) } serializeInt32(count, buffer: buffer, boxed: false) buffer.appendInt32(481674261) @@ -17,6 +17,11 @@ public struct channels { 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) @@ -33,8 +38,8 @@ public struct channels { public func descriptionFields() -> (String, [(String, Any)]) { switch self { - case .channelParticipants(let count, let participants, let users): - return ("channelParticipants", [("count", count), ("participants", participants), ("users", users)]) + case .channelParticipants(let count, let participants, let chats, let users): + return ("channelParticipants", [("count", count), ("participants", participants), ("chats", chats), ("users", users)]) case .channelParticipantsNotModified: return ("channelParticipantsNotModified", []) } @@ -47,15 +52,20 @@ public struct channels { if let _ = reader.readInt32() { _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.ChannelParticipant.self) } - var _3: [Api.User]? + var _3: [Api.Chat]? if let _ = reader.readInt32() { - _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + _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 - if _c1 && _c2 && _c3 { - return Api.channels.ChannelParticipants.channelParticipants(count: _1!, participants: _2!, users: _3!) + let _c4 = _4 != nil + if _c1 && _c2 && _c3 && _c4 { + return Api.channels.ChannelParticipants.channelParticipants(count: _1!, participants: _2!, chats: _3!, users: _4!) } else { return nil @@ -67,16 +77,21 @@ public struct channels { } public enum ChannelParticipant: TypeConstructorDescription { - case channelParticipant(participant: Api.ChannelParticipant, users: [Api.User]) + case channelParticipant(participant: Api.ChannelParticipant, chats: [Api.Chat], users: [Api.User]) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { - case .channelParticipant(let participant, let users): + case .channelParticipant(let participant, let chats, let users): if boxed { - buffer.appendInt32(-791039645) + buffer.appendInt32(-541588713) } participant.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) @@ -87,8 +102,8 @@ public struct channels { public func descriptionFields() -> (String, [(String, Any)]) { switch self { - case .channelParticipant(let participant, let users): - return ("channelParticipant", [("participant", participant), ("users", users)]) + case .channelParticipant(let participant, let chats, let users): + return ("channelParticipant", [("participant", participant), ("chats", chats), ("users", users)]) } } @@ -97,14 +112,19 @@ public struct channels { if let signature = reader.readInt32() { _1 = Api.parse(reader, signature: signature) as? Api.ChannelParticipant } - var _2: [Api.User]? + var _2: [Api.Chat]? if let _ = reader.readInt32() { - _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + _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 - if _c1 && _c2 { - return Api.channels.ChannelParticipant.channelParticipant(participant: _1!, users: _2!) + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return Api.channels.ChannelParticipant.channelParticipant(participant: _1!, chats: _2!, users: _3!) } else { return nil @@ -281,15 +301,16 @@ public struct payments { } public enum PaymentForm: TypeConstructorDescription { - case paymentForm(flags: Int32, botId: Int32, invoice: Api.Invoice, providerId: Int32, url: String, nativeProvider: String?, nativeParams: Api.DataJSON?, savedInfo: Api.PaymentRequestedInfo?, savedCredentials: Api.PaymentSavedCredentials?, users: [Api.User]) + case paymentForm(flags: Int32, formId: Int64, botId: Int32, invoice: Api.Invoice, providerId: Int32, url: String, nativeProvider: String?, nativeParams: Api.DataJSON?, savedInfo: Api.PaymentRequestedInfo?, savedCredentials: Api.PaymentSavedCredentials?, users: [Api.User]) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { - case .paymentForm(let flags, let botId, let invoice, let providerId, let url, let nativeProvider, let nativeParams, let savedInfo, let savedCredentials, let users): + case .paymentForm(let flags, let formId, let botId, let invoice, let providerId, let url, let nativeProvider, let nativeParams, let savedInfo, let savedCredentials, let users): if boxed { - buffer.appendInt32(1062645411) + buffer.appendInt32(-1928649707) } serializeInt32(flags, buffer: buffer, boxed: false) + serializeInt64(formId, buffer: buffer, boxed: false) serializeInt32(botId, buffer: buffer, boxed: false) invoice.serialize(buffer, true) serializeInt32(providerId, buffer: buffer, boxed: false) @@ -309,54 +330,57 @@ public struct payments { public func descriptionFields() -> (String, [(String, Any)]) { switch self { - case .paymentForm(let flags, let botId, let invoice, let providerId, let url, let nativeProvider, let nativeParams, let savedInfo, let savedCredentials, let users): - return ("paymentForm", [("flags", flags), ("botId", botId), ("invoice", invoice), ("providerId", providerId), ("url", url), ("nativeProvider", nativeProvider), ("nativeParams", nativeParams), ("savedInfo", savedInfo), ("savedCredentials", savedCredentials), ("users", users)]) + case .paymentForm(let flags, let formId, let botId, let invoice, let providerId, let url, let nativeProvider, let nativeParams, let savedInfo, let savedCredentials, let users): + return ("paymentForm", [("flags", flags), ("formId", formId), ("botId", botId), ("invoice", invoice), ("providerId", providerId), ("url", url), ("nativeProvider", nativeProvider), ("nativeParams", nativeParams), ("savedInfo", savedInfo), ("savedCredentials", savedCredentials), ("users", users)]) } } public static func parse_paymentForm(_ reader: BufferReader) -> PaymentForm? { var _1: Int32? _1 = reader.readInt32() - var _2: Int32? - _2 = reader.readInt32() - var _3: Api.Invoice? + var _2: Int64? + _2 = reader.readInt64() + var _3: Int32? + _3 = reader.readInt32() + var _4: Api.Invoice? if let signature = reader.readInt32() { - _3 = Api.parse(reader, signature: signature) as? Api.Invoice + _4 = Api.parse(reader, signature: signature) as? Api.Invoice } - var _4: Int32? - _4 = reader.readInt32() - var _5: String? - _5 = parseString(reader) + var _5: Int32? + _5 = reader.readInt32() var _6: String? - if Int(_1!) & Int(1 << 4) != 0 {_6 = parseString(reader) } - var _7: Api.DataJSON? + _6 = parseString(reader) + var _7: String? + if Int(_1!) & Int(1 << 4) != 0 {_7 = parseString(reader) } + var _8: Api.DataJSON? if Int(_1!) & Int(1 << 4) != 0 {if let signature = reader.readInt32() { - _7 = Api.parse(reader, signature: signature) as? Api.DataJSON + _8 = Api.parse(reader, signature: signature) as? Api.DataJSON } } - var _8: Api.PaymentRequestedInfo? + var _9: Api.PaymentRequestedInfo? if Int(_1!) & Int(1 << 0) != 0 {if let signature = reader.readInt32() { - _8 = Api.parse(reader, signature: signature) as? Api.PaymentRequestedInfo + _9 = Api.parse(reader, signature: signature) as? Api.PaymentRequestedInfo } } - var _9: Api.PaymentSavedCredentials? + var _10: Api.PaymentSavedCredentials? if Int(_1!) & Int(1 << 1) != 0 {if let signature = reader.readInt32() { - _9 = Api.parse(reader, signature: signature) as? Api.PaymentSavedCredentials + _10 = Api.parse(reader, signature: signature) as? Api.PaymentSavedCredentials } } - var _10: [Api.User]? + var _11: [Api.User]? if let _ = reader.readInt32() { - _10 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + _11 = 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 = (Int(_1!) & Int(1 << 4) == 0) || _6 != nil + let _c6 = _6 != nil let _c7 = (Int(_1!) & Int(1 << 4) == 0) || _7 != nil - let _c8 = (Int(_1!) & Int(1 << 0) == 0) || _8 != nil - let _c9 = (Int(_1!) & Int(1 << 1) == 0) || _9 != nil - let _c10 = _10 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 { - return Api.payments.PaymentForm.paymentForm(flags: _1!, botId: _2!, invoice: _3!, providerId: _4!, url: _5!, nativeProvider: _6, nativeParams: _7, savedInfo: _8, savedCredentials: _9, users: _10!) + let _c8 = (Int(_1!) & Int(1 << 4) == 0) || _8 != nil + let _c9 = (Int(_1!) & Int(1 << 0) == 0) || _9 != nil + let _c10 = (Int(_1!) & Int(1 << 1) == 0) || _10 != nil + let _c11 = _11 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 { + return Api.payments.PaymentForm.paymentForm(flags: _1!, formId: _2!, botId: _3!, invoice: _4!, providerId: _5!, url: _6!, nativeProvider: _7, nativeParams: _8, savedInfo: _9, savedCredentials: _10, users: _11!) } else { return nil @@ -365,21 +389,25 @@ public struct payments { } public enum PaymentReceipt: TypeConstructorDescription { - case paymentReceipt(flags: Int32, date: Int32, botId: Int32, invoice: Api.Invoice, providerId: Int32, info: Api.PaymentRequestedInfo?, shipping: Api.ShippingOption?, currency: String, totalAmount: Int64, credentialsTitle: String, users: [Api.User]) + case paymentReceipt(flags: Int32, date: Int32, botId: Int32, providerId: Int32, title: String, description: String, photo: Api.WebDocument?, invoice: Api.Invoice, info: Api.PaymentRequestedInfo?, shipping: Api.ShippingOption?, tipAmount: Int64?, currency: String, totalAmount: Int64, credentialsTitle: String, users: [Api.User]) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { - case .paymentReceipt(let flags, let date, let botId, let invoice, let providerId, let info, let shipping, let currency, let totalAmount, let credentialsTitle, let users): + case .paymentReceipt(let flags, let date, let botId, let providerId, let title, let description, let photo, let invoice, let info, let shipping, let tipAmount, let currency, let totalAmount, let credentialsTitle, let users): if boxed { - buffer.appendInt32(1342771681) + buffer.appendInt32(280319440) } serializeInt32(flags, buffer: buffer, boxed: false) serializeInt32(date, buffer: buffer, boxed: false) serializeInt32(botId, buffer: buffer, boxed: false) - invoice.serialize(buffer, true) serializeInt32(providerId, buffer: buffer, boxed: false) + serializeString(title, buffer: buffer, boxed: false) + serializeString(description, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 2) != 0 {photo!.serialize(buffer, true)} + invoice.serialize(buffer, true) if Int(flags) & Int(1 << 0) != 0 {info!.serialize(buffer, true)} if Int(flags) & Int(1 << 1) != 0 {shipping!.serialize(buffer, true)} + if Int(flags) & Int(1 << 3) != 0 {serializeInt64(tipAmount!, buffer: buffer, boxed: false)} serializeString(currency, buffer: buffer, boxed: false) serializeInt64(totalAmount, buffer: buffer, boxed: false) serializeString(credentialsTitle, buffer: buffer, boxed: false) @@ -394,8 +422,8 @@ public struct payments { public func descriptionFields() -> (String, [(String, Any)]) { switch self { - case .paymentReceipt(let flags, let date, let botId, let invoice, let providerId, let info, let shipping, let currency, let totalAmount, let credentialsTitle, let users): - return ("paymentReceipt", [("flags", flags), ("date", date), ("botId", botId), ("invoice", invoice), ("providerId", providerId), ("info", info), ("shipping", shipping), ("currency", currency), ("totalAmount", totalAmount), ("credentialsTitle", credentialsTitle), ("users", users)]) + case .paymentReceipt(let flags, let date, let botId, let providerId, let title, let description, let photo, let invoice, let info, let shipping, let tipAmount, let currency, let totalAmount, let credentialsTitle, let users): + return ("paymentReceipt", [("flags", flags), ("date", date), ("botId", botId), ("providerId", providerId), ("title", title), ("description", description), ("photo", photo), ("invoice", invoice), ("info", info), ("shipping", shipping), ("tipAmount", tipAmount), ("currency", currency), ("totalAmount", totalAmount), ("credentialsTitle", credentialsTitle), ("users", users)]) } } @@ -406,43 +434,57 @@ public struct payments { _2 = reader.readInt32() var _3: Int32? _3 = reader.readInt32() - var _4: Api.Invoice? + var _4: Int32? + _4 = reader.readInt32() + var _5: String? + _5 = parseString(reader) + var _6: String? + _6 = parseString(reader) + var _7: Api.WebDocument? + if Int(_1!) & Int(1 << 2) != 0 {if let signature = reader.readInt32() { + _7 = Api.parse(reader, signature: signature) as? Api.WebDocument + } } + var _8: Api.Invoice? if let signature = reader.readInt32() { - _4 = Api.parse(reader, signature: signature) as? Api.Invoice + _8 = Api.parse(reader, signature: signature) as? Api.Invoice } - var _5: Int32? - _5 = reader.readInt32() - var _6: Api.PaymentRequestedInfo? + var _9: Api.PaymentRequestedInfo? if Int(_1!) & Int(1 << 0) != 0 {if let signature = reader.readInt32() { - _6 = Api.parse(reader, signature: signature) as? Api.PaymentRequestedInfo + _9 = Api.parse(reader, signature: signature) as? Api.PaymentRequestedInfo } } - var _7: Api.ShippingOption? + var _10: Api.ShippingOption? if Int(_1!) & Int(1 << 1) != 0 {if let signature = reader.readInt32() { - _7 = Api.parse(reader, signature: signature) as? Api.ShippingOption + _10 = Api.parse(reader, signature: signature) as? Api.ShippingOption } } - var _8: String? - _8 = parseString(reader) - var _9: Int64? - _9 = reader.readInt64() - var _10: String? - _10 = parseString(reader) - var _11: [Api.User]? + var _11: Int64? + if Int(_1!) & Int(1 << 3) != 0 {_11 = reader.readInt64() } + var _12: String? + _12 = parseString(reader) + var _13: Int64? + _13 = reader.readInt64() + var _14: String? + _14 = parseString(reader) + var _15: [Api.User]? if let _ = reader.readInt32() { - _11 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + _15 = 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 = (Int(_1!) & Int(1 << 0) == 0) || _6 != nil - let _c7 = (Int(_1!) & Int(1 << 1) == 0) || _7 != nil + let _c6 = _6 != nil + let _c7 = (Int(_1!) & Int(1 << 2) == 0) || _7 != nil let _c8 = _8 != nil - let _c9 = _9 != nil - let _c10 = _10 != nil - let _c11 = _11 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 { - return Api.payments.PaymentReceipt.paymentReceipt(flags: _1!, date: _2!, botId: _3!, invoice: _4!, providerId: _5!, info: _6, shipping: _7, currency: _8!, totalAmount: _9!, credentialsTitle: _10!, users: _11!) + let _c9 = (Int(_1!) & Int(1 << 0) == 0) || _9 != nil + let _c10 = (Int(_1!) & Int(1 << 1) == 0) || _10 != nil + let _c11 = (Int(_1!) & Int(1 << 3) == 0) || _11 != nil + let _c12 = _12 != nil + let _c13 = _13 != nil + let _c14 = _14 != nil + let _c15 = _15 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 && _c14 && _c15 { + return Api.payments.PaymentReceipt.paymentReceipt(flags: _1!, date: _2!, botId: _3!, providerId: _4!, title: _5!, description: _6!, photo: _7, invoice: _8!, info: _9, shipping: _10, tipAmount: _11, currency: _12!, totalAmount: _13!, credentialsTitle: _14!, users: _15!) } else { return nil @@ -1773,14 +1815,14 @@ public struct help { } public enum AppUpdate: TypeConstructorDescription { - case appUpdate(flags: Int32, id: Int32, version: String, text: String, entities: [Api.MessageEntity], document: Api.Document?, url: String?) + case appUpdate(flags: Int32, id: Int32, version: String, text: String, entities: [Api.MessageEntity], document: Api.Document?, url: String?, sticker: Api.Document?) case noAppUpdate public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { - case .appUpdate(let flags, let id, let version, let text, let entities, let document, let url): + case .appUpdate(let flags, let id, let version, let text, let entities, let document, let url, let sticker): if boxed { - buffer.appendInt32(497489295) + buffer.appendInt32(-860107216) } serializeInt32(flags, buffer: buffer, boxed: false) serializeInt32(id, buffer: buffer, boxed: false) @@ -1793,6 +1835,7 @@ public struct help { } if Int(flags) & Int(1 << 1) != 0 {document!.serialize(buffer, true)} if Int(flags) & Int(1 << 2) != 0 {serializeString(url!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 3) != 0 {sticker!.serialize(buffer, true)} break case .noAppUpdate: if boxed { @@ -1805,8 +1848,8 @@ public struct help { public func descriptionFields() -> (String, [(String, Any)]) { switch self { - case .appUpdate(let flags, let id, let version, let text, let entities, let document, let url): - return ("appUpdate", [("flags", flags), ("id", id), ("version", version), ("text", text), ("entities", entities), ("document", document), ("url", url)]) + case .appUpdate(let flags, let id, let version, let text, let entities, let document, let url, let sticker): + return ("appUpdate", [("flags", flags), ("id", id), ("version", version), ("text", text), ("entities", entities), ("document", document), ("url", url), ("sticker", sticker)]) case .noAppUpdate: return ("noAppUpdate", []) } @@ -1831,6 +1874,10 @@ public struct help { } } var _7: String? if Int(_1!) & Int(1 << 2) != 0 {_7 = parseString(reader) } + var _8: Api.Document? + if Int(_1!) & Int(1 << 3) != 0 {if let signature = reader.readInt32() { + _8 = Api.parse(reader, signature: signature) as? Api.Document + } } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil @@ -1838,8 +1885,9 @@ public struct help { let _c5 = _5 != nil let _c6 = (Int(_1!) & Int(1 << 1) == 0) || _6 != nil let _c7 = (Int(_1!) & Int(1 << 2) == 0) || _7 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 { - return Api.help.AppUpdate.appUpdate(flags: _1!, id: _2!, version: _3!, text: _4!, entities: _5!, document: _6, url: _7) + let _c8 = (Int(_1!) & Int(1 << 3) == 0) || _8 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 { + return Api.help.AppUpdate.appUpdate(flags: _1!, id: _2!, version: _3!, text: _4!, entities: _5!, document: _6, url: _7, sticker: _8) } else { return nil diff --git a/submodules/TelegramApi/Sources/Api4.swift b/submodules/TelegramApi/Sources/Api4.swift index 3e22875d1c..fbbd0ce0df 100644 --- a/submodules/TelegramApi/Sources/Api4.swift +++ b/submodules/TelegramApi/Sources/Api4.swift @@ -621,6 +621,44 @@ public struct upload { } } public extension Api { +public struct stickers { + public enum SuggestedShortName: TypeConstructorDescription { + case suggestedShortName(shortName: String) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .suggestedShortName(let shortName): + if boxed { + buffer.appendInt32(-2046910401) + } + serializeString(shortName, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .suggestedShortName(let shortName): + return ("suggestedShortName", [("shortName", shortName)]) + } + } + + public static func parse_suggestedShortName(_ reader: BufferReader) -> SuggestedShortName? { + var _1: String? + _1 = parseString(reader) + let _c1 = _1 != nil + if _c1 { + return Api.stickers.SuggestedShortName.suggestedShortName(shortName: _1!) + } + else { + return nil + } + } + + } +} +} +public extension Api { public struct storage { public enum FileType: TypeConstructorDescription { case fileUnknown @@ -1133,6 +1171,72 @@ public struct account { } } + } + public enum ResetPasswordResult: TypeConstructorDescription { + case resetPasswordFailedWait(retryDate: Int32) + case resetPasswordRequestedWait(untilDate: Int32) + case resetPasswordOk + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .resetPasswordFailedWait(let retryDate): + if boxed { + buffer.appendInt32(-478701471) + } + serializeInt32(retryDate, buffer: buffer, boxed: false) + break + case .resetPasswordRequestedWait(let untilDate): + if boxed { + buffer.appendInt32(-370148227) + } + serializeInt32(untilDate, buffer: buffer, boxed: false) + break + case .resetPasswordOk: + if boxed { + buffer.appendInt32(-383330754) + } + + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .resetPasswordFailedWait(let retryDate): + return ("resetPasswordFailedWait", [("retryDate", retryDate)]) + case .resetPasswordRequestedWait(let untilDate): + return ("resetPasswordRequestedWait", [("untilDate", untilDate)]) + case .resetPasswordOk: + return ("resetPasswordOk", []) + } + } + + public static func parse_resetPasswordFailedWait(_ reader: BufferReader) -> ResetPasswordResult? { + var _1: Int32? + _1 = reader.readInt32() + let _c1 = _1 != nil + if _c1 { + return Api.account.ResetPasswordResult.resetPasswordFailedWait(retryDate: _1!) + } + else { + return nil + } + } + public static func parse_resetPasswordRequestedWait(_ reader: BufferReader) -> ResetPasswordResult? { + var _1: Int32? + _1 = reader.readInt32() + let _c1 = _1 != nil + if _c1 { + return Api.account.ResetPasswordResult.resetPasswordRequestedWait(untilDate: _1!) + } + else { + return nil + } + } + public static func parse_resetPasswordOk(_ reader: BufferReader) -> ResetPasswordResult? { + return Api.account.ResetPasswordResult.resetPasswordOk + } + } public enum ContentSettings: TypeConstructorDescription { case contentSettings(flags: Int32) @@ -1287,13 +1391,13 @@ public struct account { } public enum Password: TypeConstructorDescription { - case password(flags: Int32, currentAlgo: Api.PasswordKdfAlgo?, srpB: Buffer?, srpId: Int64?, hint: String?, emailUnconfirmedPattern: String?, newAlgo: Api.PasswordKdfAlgo, newSecureAlgo: Api.SecurePasswordKdfAlgo, secureRandom: Buffer) + case password(flags: Int32, currentAlgo: Api.PasswordKdfAlgo?, srpB: Buffer?, srpId: Int64?, hint: String?, emailUnconfirmedPattern: String?, newAlgo: Api.PasswordKdfAlgo, newSecureAlgo: Api.SecurePasswordKdfAlgo, secureRandom: Buffer, pendingResetDate: Int32?) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { - case .password(let flags, let currentAlgo, let srpB, let srpId, let hint, let emailUnconfirmedPattern, let newAlgo, let newSecureAlgo, let secureRandom): + case .password(let flags, let currentAlgo, let srpB, let srpId, let hint, let emailUnconfirmedPattern, let newAlgo, let newSecureAlgo, let secureRandom, let pendingResetDate): if boxed { - buffer.appendInt32(-1390001672) + buffer.appendInt32(408623183) } serializeInt32(flags, buffer: buffer, boxed: false) if Int(flags) & Int(1 << 2) != 0 {currentAlgo!.serialize(buffer, true)} @@ -1304,14 +1408,15 @@ public struct account { newAlgo.serialize(buffer, true) newSecureAlgo.serialize(buffer, true) serializeBytes(secureRandom, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 5) != 0 {serializeInt32(pendingResetDate!, buffer: buffer, boxed: false)} break } } public func descriptionFields() -> (String, [(String, Any)]) { switch self { - case .password(let flags, let currentAlgo, let srpB, let srpId, let hint, let emailUnconfirmedPattern, let newAlgo, let newSecureAlgo, let secureRandom): - return ("password", [("flags", flags), ("currentAlgo", currentAlgo), ("srpB", srpB), ("srpId", srpId), ("hint", hint), ("emailUnconfirmedPattern", emailUnconfirmedPattern), ("newAlgo", newAlgo), ("newSecureAlgo", newSecureAlgo), ("secureRandom", secureRandom)]) + case .password(let flags, let currentAlgo, let srpB, let srpId, let hint, let emailUnconfirmedPattern, let newAlgo, let newSecureAlgo, let secureRandom, let pendingResetDate): + return ("password", [("flags", flags), ("currentAlgo", currentAlgo), ("srpB", srpB), ("srpId", srpId), ("hint", hint), ("emailUnconfirmedPattern", emailUnconfirmedPattern), ("newAlgo", newAlgo), ("newSecureAlgo", newSecureAlgo), ("secureRandom", secureRandom), ("pendingResetDate", pendingResetDate)]) } } @@ -1340,6 +1445,8 @@ public struct account { } var _9: Buffer? _9 = parseBytes(reader) + var _10: Int32? + if Int(_1!) & Int(1 << 5) != 0 {_10 = reader.readInt32() } let _c1 = _1 != nil let _c2 = (Int(_1!) & Int(1 << 2) == 0) || _2 != nil let _c3 = (Int(_1!) & Int(1 << 2) == 0) || _3 != nil @@ -1349,8 +1456,9 @@ public struct account { let _c7 = _7 != nil let _c8 = _8 != nil let _c9 = _9 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 { - return Api.account.Password.password(flags: _1!, currentAlgo: _2, srpB: _3, srpId: _4, hint: _5, emailUnconfirmedPattern: _6, newAlgo: _7!, newSecureAlgo: _8!, secureRandom: _9!) + let _c10 = (Int(_1!) & Int(1 << 5) == 0) || _10 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 { + return Api.account.Password.password(flags: _1!, currentAlgo: _2, srpB: _3, srpId: _4, hint: _5, emailUnconfirmedPattern: _6, newAlgo: _7!, newSecureAlgo: _8!, secureRandom: _9!, pendingResetDate: _10) } else { return nil @@ -4376,12 +4484,12 @@ public extension Api { }) } - public static func getParticipant(channel: Api.InputChannel, userId: Api.InputUser) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + public static func getParticipant(channel: Api.InputChannel, participant: Api.InputPeer) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() - buffer.appendInt32(1416484774) + buffer.appendInt32(-1599378234) channel.serialize(buffer, true) - userId.serialize(buffer, true) - return (FunctionDescription(name: "channels.getParticipant", parameters: [("channel", channel), ("userId", userId)]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.channels.ChannelParticipant? in + participant.serialize(buffer, true) + return (FunctionDescription(name: "channels.getParticipant", parameters: [("channel", channel), ("participant", participant)]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.channels.ChannelParticipant? in let reader = BufferReader(buffer) var result: Api.channels.ChannelParticipant? if let signature = reader.readInt32() { @@ -4624,13 +4732,13 @@ public extension Api { }) } - public static func editBanned(channel: Api.InputChannel, userId: Api.InputUser, bannedRights: Api.ChatBannedRights) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + public static func editBanned(channel: Api.InputChannel, participant: Api.InputPeer, bannedRights: Api.ChatBannedRights) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() - buffer.appendInt32(1920559378) + buffer.appendInt32(-1763259007) channel.serialize(buffer, true) - userId.serialize(buffer, true) + participant.serialize(buffer, true) bannedRights.serialize(buffer, true) - return (FunctionDescription(name: "channels.editBanned", parameters: [("channel", channel), ("userId", userId), ("bannedRights", bannedRights)]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in + return (FunctionDescription(name: "channels.editBanned", parameters: [("channel", channel), ("participant", participant), ("bannedRights", bannedRights)]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in let reader = BufferReader(buffer) var result: Api.Updates? if let signature = reader.readInt32() { @@ -4848,11 +4956,14 @@ public extension Api { } } public struct payments { - public static func getPaymentForm(msgId: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + public static func getPaymentForm(flags: Int32, peer: Api.InputPeer, msgId: Int32, themeParams: Api.DataJSON?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() - buffer.appendInt32(-1712285883) + buffer.appendInt32(-1976353651) + serializeInt32(flags, buffer: buffer, boxed: false) + peer.serialize(buffer, true) serializeInt32(msgId, buffer: buffer, boxed: false) - return (FunctionDescription(name: "payments.getPaymentForm", parameters: [("msgId", msgId)]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.payments.PaymentForm? in + if Int(flags) & Int(1 << 0) != 0 {themeParams!.serialize(buffer, true)} + return (FunctionDescription(name: "payments.getPaymentForm", parameters: [("flags", flags), ("peer", peer), ("msgId", msgId), ("themeParams", themeParams)]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.payments.PaymentForm? in let reader = BufferReader(buffer) var result: Api.payments.PaymentForm? if let signature = reader.readInt32() { @@ -4862,11 +4973,12 @@ public extension Api { }) } - public static func getPaymentReceipt(msgId: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + public static func getPaymentReceipt(peer: Api.InputPeer, msgId: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() - buffer.appendInt32(-1601001088) + buffer.appendInt32(611897804) + peer.serialize(buffer, true) serializeInt32(msgId, buffer: buffer, boxed: false) - return (FunctionDescription(name: "payments.getPaymentReceipt", parameters: [("msgId", msgId)]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.payments.PaymentReceipt? in + return (FunctionDescription(name: "payments.getPaymentReceipt", parameters: [("peer", peer), ("msgId", msgId)]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.payments.PaymentReceipt? in let reader = BufferReader(buffer) var result: Api.payments.PaymentReceipt? if let signature = reader.readInt32() { @@ -4876,13 +4988,14 @@ public extension Api { }) } - public static func validateRequestedInfo(flags: Int32, msgId: Int32, info: Api.PaymentRequestedInfo) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + public static func validateRequestedInfo(flags: Int32, peer: Api.InputPeer, msgId: Int32, info: Api.PaymentRequestedInfo) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() - buffer.appendInt32(1997180532) + buffer.appendInt32(-619695760) serializeInt32(flags, buffer: buffer, boxed: false) + peer.serialize(buffer, true) serializeInt32(msgId, buffer: buffer, boxed: false) info.serialize(buffer, true) - return (FunctionDescription(name: "payments.validateRequestedInfo", parameters: [("flags", flags), ("msgId", msgId), ("info", info)]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.payments.ValidatedRequestedInfo? in + return (FunctionDescription(name: "payments.validateRequestedInfo", parameters: [("flags", flags), ("peer", peer), ("msgId", msgId), ("info", info)]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.payments.ValidatedRequestedInfo? in let reader = BufferReader(buffer) var result: Api.payments.ValidatedRequestedInfo? if let signature = reader.readInt32() { @@ -4892,15 +5005,18 @@ public extension Api { }) } - public static func sendPaymentForm(flags: Int32, msgId: Int32, requestedInfoId: String?, shippingOptionId: String?, credentials: Api.InputPaymentCredentials) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + public static func sendPaymentForm(flags: Int32, formId: Int64, peer: Api.InputPeer, msgId: Int32, requestedInfoId: String?, shippingOptionId: String?, credentials: Api.InputPaymentCredentials, tipAmount: Int64?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() - buffer.appendInt32(730364339) + buffer.appendInt32(818134173) serializeInt32(flags, buffer: buffer, boxed: false) + serializeInt64(formId, buffer: buffer, boxed: false) + peer.serialize(buffer, true) serializeInt32(msgId, buffer: buffer, boxed: false) if Int(flags) & Int(1 << 0) != 0 {serializeString(requestedInfoId!, buffer: buffer, boxed: false)} if Int(flags) & Int(1 << 1) != 0 {serializeString(shippingOptionId!, buffer: buffer, boxed: false)} credentials.serialize(buffer, true) - return (FunctionDescription(name: "payments.sendPaymentForm", parameters: [("flags", flags), ("msgId", msgId), ("requestedInfoId", requestedInfoId), ("shippingOptionId", shippingOptionId), ("credentials", credentials)]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.payments.PaymentResult? in + if Int(flags) & Int(1 << 2) != 0 {serializeInt64(tipAmount!, buffer: buffer, boxed: false)} + return (FunctionDescription(name: "payments.sendPaymentForm", parameters: [("flags", flags), ("formId", formId), ("peer", peer), ("msgId", msgId), ("requestedInfoId", requestedInfoId), ("shippingOptionId", shippingOptionId), ("credentials", credentials), ("tipAmount", tipAmount)]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.payments.PaymentResult? in let reader = BufferReader(buffer) var result: Api.payments.PaymentResult? if let signature = reader.readInt32() { @@ -5204,11 +5320,13 @@ public extension Api { }) } - public static func recoverPassword(code: String) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + public static func recoverPassword(flags: Int32, code: String, newSettings: Api.account.PasswordInputSettings?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() - buffer.appendInt32(1319464594) + buffer.appendInt32(923364464) + serializeInt32(flags, buffer: buffer, boxed: false) serializeString(code, buffer: buffer, boxed: false) - return (FunctionDescription(name: "auth.recoverPassword", parameters: [("code", code)]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.auth.Authorization? in + if Int(flags) & Int(1 << 0) != 0 {newSettings!.serialize(buffer, true)} + return (FunctionDescription(name: "auth.recoverPassword", parameters: [("flags", flags), ("code", code), ("newSettings", newSettings)]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.auth.Authorization? in let reader = BufferReader(buffer) var result: Api.auth.Authorization? if let signature = reader.readInt32() { @@ -5313,6 +5431,20 @@ public extension Api { return result }) } + + public static func checkRecoveryPassword(code: String) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(221691769) + serializeString(code, buffer: buffer, boxed: false) + return (FunctionDescription(name: "auth.checkRecoveryPassword", parameters: [("code", code)]), 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 struct bots { public static func sendCustomRequest(customMethod: String, params: Api.DataJSON) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { @@ -5345,15 +5477,17 @@ public extension Api { }) } - public static func setBotCommands(commands: [Api.BotCommand]) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + public static func setBotCommands(scope: Api.BotCommandScope, langCode: String, commands: [Api.BotCommand]) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() - buffer.appendInt32(-2141370634) + buffer.appendInt32(85399130) + scope.serialize(buffer, true) + serializeString(langCode, buffer: buffer, boxed: false) buffer.appendInt32(481674261) buffer.appendInt32(Int32(commands.count)) for item in commands { item.serialize(buffer, true) } - return (FunctionDescription(name: "bots.setBotCommands", parameters: [("commands", commands)]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in + return (FunctionDescription(name: "bots.setBotCommands", parameters: [("scope", scope), ("langCode", langCode), ("commands", commands)]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in let reader = BufferReader(buffer) var result: Api.Bool? if let signature = reader.readInt32() { @@ -5362,6 +5496,36 @@ public extension Api { return result }) } + + public static func resetBotCommands(scope: Api.BotCommandScope, langCode: String) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(1032708345) + scope.serialize(buffer, true) + serializeString(langCode, buffer: buffer, boxed: false) + return (FunctionDescription(name: "bots.resetBotCommands", parameters: [("scope", scope), ("langCode", langCode)]), 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 getBotCommands(scope: Api.BotCommandScope, langCode: String) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<[Api.BotCommand]>) { + let buffer = Buffer() + buffer.appendInt32(-481554986) + scope.serialize(buffer, true) + serializeString(langCode, buffer: buffer, boxed: false) + return (FunctionDescription(name: "bots.getBotCommands", parameters: [("scope", scope), ("langCode", langCode)]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> [Api.BotCommand]? in + let reader = BufferReader(buffer) + var result: [Api.BotCommand]? + if let _ = reader.readInt32() { + result = Api.parseVector(reader, elementSignature: 0, elementType: Api.BotCommand.self) + } + return result + }) + } } public struct users { public static func getUsers(id: [Api.InputUser]) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<[Api.User]>) { @@ -6270,9 +6434,9 @@ public extension Api { } } public struct stickers { - public static func createStickerSet(flags: Int32, userId: Api.InputUser, title: String, shortName: String, thumb: Api.InputDocument?, stickers: [Api.InputStickerSetItem]) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + public static func createStickerSet(flags: Int32, userId: Api.InputUser, title: String, shortName: String, thumb: Api.InputDocument?, stickers: [Api.InputStickerSetItem], software: String?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() - buffer.appendInt32(-251435136) + buffer.appendInt32(-1876841625) serializeInt32(flags, buffer: buffer, boxed: false) userId.serialize(buffer, true) serializeString(title, buffer: buffer, boxed: false) @@ -6283,7 +6447,8 @@ public extension Api { for item in stickers { item.serialize(buffer, true) } - return (FunctionDescription(name: "stickers.createStickerSet", parameters: [("flags", flags), ("userId", userId), ("title", title), ("shortName", shortName), ("thumb", thumb), ("stickers", stickers)]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.messages.StickerSet? in + if Int(flags) & Int(1 << 3) != 0 {serializeString(software!, buffer: buffer, boxed: false)} + return (FunctionDescription(name: "stickers.createStickerSet", parameters: [("flags", flags), ("userId", userId), ("title", title), ("shortName", shortName), ("thumb", thumb), ("stickers", stickers), ("software", software)]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.messages.StickerSet? in let reader = BufferReader(buffer) var result: Api.messages.StickerSet? if let signature = reader.readInt32() { @@ -6351,6 +6516,34 @@ public extension Api { return result }) } + + public static func checkShortName(shortName: String) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(676017721) + serializeString(shortName, buffer: buffer, boxed: false) + return (FunctionDescription(name: "stickers.checkShortName", parameters: [("shortName", shortName)]), 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 suggestShortName(title: String) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(1303364867) + serializeString(title, buffer: buffer, boxed: false) + return (FunctionDescription(name: "stickers.suggestShortName", parameters: [("title", title)]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.stickers.SuggestedShortName? in + let reader = BufferReader(buffer) + var result: Api.stickers.SuggestedShortName? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.stickers.SuggestedShortName + } + return result + }) + } } public struct account { public static func registerDevice(flags: Int32, tokenType: Int32, token: String, appSandbox: Api.Bool, secret: Buffer, otherUids: [Int32]) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { @@ -7395,6 +7588,34 @@ public extension Api { return result }) } + + public static func resetPassword() -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(-1828139493) + + return (FunctionDescription(name: "account.resetPassword", parameters: []), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.account.ResetPasswordResult? in + let reader = BufferReader(buffer) + var result: Api.account.ResetPasswordResult? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.account.ResetPasswordResult + } + return result + }) + } + + public static func declinePasswordReset() -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(1284770294) + + return (FunctionDescription(name: "account.declinePasswordReset", parameters: []), 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 struct langpack { public static func getLangPack(langPack: String, langCode: String) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { @@ -7689,12 +7910,15 @@ public extension Api { }) } - public static func createGroupCall(peer: Api.InputPeer, randomId: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + public static func createGroupCall(flags: Int32, peer: Api.InputPeer, randomId: Int32, title: String?, scheduleDate: Int32?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() - buffer.appendInt32(-1120031776) + buffer.appendInt32(1221445336) + serializeInt32(flags, buffer: buffer, boxed: false) peer.serialize(buffer, true) serializeInt32(randomId, buffer: buffer, boxed: false) - return (FunctionDescription(name: "phone.createGroupCall", parameters: [("peer", peer), ("randomId", randomId)]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in + if Int(flags) & Int(1 << 0) != 0 {serializeString(title!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 1) != 0 {serializeInt32(scheduleDate!, buffer: buffer, boxed: false)} + return (FunctionDescription(name: "phone.createGroupCall", parameters: [("flags", flags), ("peer", peer), ("randomId", randomId), ("title", title), ("scheduleDate", scheduleDate)]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in let reader = BufferReader(buffer) var result: Api.Updates? if let signature = reader.readInt32() { @@ -7826,16 +8050,20 @@ public extension Api { }) } - public static func checkGroupCall(call: Api.InputGroupCall, source: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + public static func checkGroupCall(call: Api.InputGroupCall, sources: [Int32]) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<[Int32]>) { let buffer = Buffer() - buffer.appendInt32(-1219855382) + buffer.appendInt32(-1248003721) call.serialize(buffer, true) - serializeInt32(source, buffer: buffer, boxed: false) - return (FunctionDescription(name: "phone.checkGroupCall", parameters: [("call", call), ("source", source)]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(sources.count)) + for item in sources { + serializeInt32(item, buffer: buffer, boxed: false) + } + return (FunctionDescription(name: "phone.checkGroupCall", parameters: [("call", call), ("sources", sources)]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> [Int32]? in let reader = BufferReader(buffer) - var result: Api.Bool? - if let signature = reader.readInt32() { - result = Api.parse(reader, signature: signature) as? Api.Bool + var result: [Int32]? + if let _ = reader.readInt32() { + result = Api.parseVector(reader, elementSignature: -1471112230, elementType: Int32.self) } return result }) @@ -7857,15 +8085,19 @@ public extension Api { }) } - public static func editGroupCallParticipant(flags: Int32, call: Api.InputGroupCall, participant: Api.InputPeer, volume: Int32?, raiseHand: Api.Bool?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + public static func editGroupCallParticipant(flags: Int32, call: Api.InputGroupCall, participant: Api.InputPeer, muted: Api.Bool?, volume: Int32?, raiseHand: Api.Bool?, videoStopped: Api.Bool?, videoPaused: Api.Bool?, presentationPaused: Api.Bool?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() - buffer.appendInt32(-646583424) + buffer.appendInt32(-1524155713) serializeInt32(flags, buffer: buffer, boxed: false) call.serialize(buffer, true) participant.serialize(buffer, true) + if Int(flags) & Int(1 << 0) != 0 {muted!.serialize(buffer, true)} if Int(flags) & Int(1 << 1) != 0 {serializeInt32(volume!, buffer: buffer, boxed: false)} if Int(flags) & Int(1 << 2) != 0 {raiseHand!.serialize(buffer, true)} - return (FunctionDescription(name: "phone.editGroupCallParticipant", parameters: [("flags", flags), ("call", call), ("participant", participant), ("volume", volume), ("raiseHand", raiseHand)]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in + if Int(flags) & Int(1 << 3) != 0 {videoStopped!.serialize(buffer, true)} + if Int(flags) & Int(1 << 4) != 0 {videoPaused!.serialize(buffer, true)} + if Int(flags) & Int(1 << 5) != 0 {presentationPaused!.serialize(buffer, true)} + return (FunctionDescription(name: "phone.editGroupCallParticipant", parameters: [("flags", flags), ("call", call), ("participant", participant), ("muted", muted), ("volume", volume), ("raiseHand", raiseHand), ("videoStopped", videoStopped), ("videoPaused", videoPaused), ("presentationPaused", presentationPaused)]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in let reader = BufferReader(buffer) var result: Api.Updates? if let signature = reader.readInt32() { @@ -7918,6 +8150,79 @@ public extension Api { return result }) } + + public static func toggleGroupCallStartSubscription(call: Api.InputGroupCall, subscribed: Api.Bool) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(563885286) + call.serialize(buffer, true) + subscribed.serialize(buffer, true) + return (FunctionDescription(name: "phone.toggleGroupCallStartSubscription", parameters: [("call", call), ("subscribed", subscribed)]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in + let reader = BufferReader(buffer) + var result: Api.Updates? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.Updates + } + return result + }) + } + + public static func startScheduledGroupCall(call: Api.InputGroupCall) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(1451287362) + call.serialize(buffer, true) + return (FunctionDescription(name: "phone.startScheduledGroupCall", parameters: [("call", call)]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in + let reader = BufferReader(buffer) + var result: Api.Updates? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.Updates + } + return result + }) + } + + public static func saveDefaultGroupCallJoinAs(peer: Api.InputPeer, joinAs: Api.InputPeer) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(1465786252) + peer.serialize(buffer, true) + joinAs.serialize(buffer, true) + return (FunctionDescription(name: "phone.saveDefaultGroupCallJoinAs", parameters: [("peer", peer), ("joinAs", joinAs)]), 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 joinGroupCallPresentation(call: Api.InputGroupCall, params: Api.DataJSON) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(-873829436) + call.serialize(buffer, true) + params.serialize(buffer, true) + return (FunctionDescription(name: "phone.joinGroupCallPresentation", parameters: [("call", call), ("params", params)]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in + let reader = BufferReader(buffer) + var result: Api.Updates? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.Updates + } + return result + }) + } + + public static func leaveGroupCallPresentation(call: Api.InputGroupCall) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(475058500) + call.serialize(buffer, true) + return (FunctionDescription(name: "phone.leaveGroupCallPresentation", parameters: [("call", call)]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in + let reader = BufferReader(buffer) + var result: Api.Updates? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.Updates + } + return result + }) + } } } } diff --git a/submodules/TelegramAudio/Sources/ManagedAudioSession.swift b/submodules/TelegramAudio/Sources/ManagedAudioSession.swift index cc431709c8..ffe4017e73 100644 --- a/submodules/TelegramAudio/Sources/ManagedAudioSession.swift +++ b/submodules/TelegramAudio/Sources/ManagedAudioSession.swift @@ -3,6 +3,17 @@ import SwiftSignalKit import AVFoundation import UIKit +private var managedAudioSessionLogger: (String) -> Void = { _ in } + +public func setManagedAudioSessionLogger(_ f: @escaping (String) -> Void) { + managedAudioSessionLogger = f +} + +func managedAudioSessionLog(_ what: @autoclosure () -> String) { + managedAudioSessionLogger(what()) +} + + public enum ManagedAudioSessionType: Equatable { case ambient case play @@ -208,11 +219,15 @@ public final class ManagedAudioSession { }) NotificationCenter.default.addObserver(forName: AVAudioSession.interruptionNotification, object: AVAudioSession.sharedInstance(), queue: nil, using: { [weak self] notification in + managedAudioSessionLog("Interruption received") + guard let info = notification.userInfo, let typeValue = info[AVAudioSessionInterruptionTypeKey] as? UInt, let type = AVAudioSession.InterruptionType(rawValue: typeValue) else { return } + + managedAudioSessionLog("Interruption type: \(type)") queue.async { if let strongSelf = self { @@ -222,6 +237,17 @@ public final class ManagedAudioSession { } } }) + + NotificationCenter.default.addObserver(forName: AVAudioSession.mediaServicesWereLostNotification, object: AVAudioSession.sharedInstance(), queue: nil, using: { [weak self] _ in + managedAudioSessionLog("Media Services were lost") + queue.after(1.0, { + if let strongSelf = self { + if let (type, outputMode) = strongSelf.currentTypeAndOutputMode { + strongSelf.setup(type: type, outputMode: outputMode, activateNow: true) + } + } + }) + }) queue.async { self.isHeadsetPluggedInValue = self.isHeadsetPluggedIn() @@ -524,7 +550,7 @@ public final class ManagedAudioSession { private func updateHolders(interruption: Bool = false) { assert(self.queue.isCurrent()) - print("holder count \(self.holders.count)") + managedAudioSessionLog("holder count \(self.holders.count)") if !self.holders.isEmpty { var activeIndex: Int? @@ -625,7 +651,7 @@ public final class ManagedAudioSession { assert(self.queue.isCurrent()) let route = AVAudioSession.sharedInstance().currentRoute - //print("\(route)") + //managedAudioSessionLog("\(route)") for desc in route.outputs { if desc.portType == .headphones || desc.portType == .bluetoothA2DP || desc.portType == .bluetoothHFP { return true @@ -643,13 +669,13 @@ public final class ManagedAudioSession { let wasPlaybackActive = self.currentTypeAndOutputMode?.0.isPlay ?? false self.currentTypeAndOutputMode = nil - print("ManagedAudioSession setting active false") + managedAudioSessionLog("ManagedAudioSession setting active false") do { try AVAudioSession.sharedInstance().setActive(false, options: [.notifyOthersOnDeactivation]) try AVAudioSession.sharedInstance().overrideOutputAudioPort(.none) try AVAudioSession.sharedInstance().setPreferredInput(nil) } catch let error { - print("ManagedAudioSession applyNone error \(error), waiting") + managedAudioSessionLog("ManagedAudioSession applyNone error \(error), waiting") Thread.sleep(forTimeInterval: 2.0) @@ -658,7 +684,7 @@ public final class ManagedAudioSession { try AVAudioSession.sharedInstance().overrideOutputAudioPort(.none) try AVAudioSession.sharedInstance().setPreferredInput(nil) } catch let error { - print("ManagedAudioSession applyNone repeated error \(error), giving up") + managedAudioSessionLog("ManagedAudioSession applyNone repeated error \(error), giving up") } } @@ -687,7 +713,7 @@ public final class ManagedAudioSession { do { let nativeCategory = nativeCategoryForType(type, headphones: self.isHeadsetPluggedInValue, outputMode: outputMode) - print("ManagedAudioSession setting category for \(type) (native: \(nativeCategory)) activateNow: \(activateNow)") + managedAudioSessionLog("ManagedAudioSession setting category for \(type) (native: \(nativeCategory)) activateNow: \(activateNow)") var options: AVAudioSession.CategoryOptions = [] switch type { case .play, .ambient: @@ -703,13 +729,15 @@ public final class ManagedAudioSession { case .record, .voiceCall, .videoCall: options.insert(.allowBluetooth) } - print("ManagedAudioSession setting active true") + managedAudioSessionLog("ManagedAudioSession setting active true") let mode: AVAudioSession.Mode switch type { case .voiceCall: mode = .voiceChat + options.insert(.mixWithOthers) case .videoCall: mode = .videoChat + options.insert(.mixWithOthers) default: mode = .default } @@ -720,7 +748,7 @@ public final class ManagedAudioSession { try AVAudioSession.sharedInstance().setMode(mode) } } catch let error { - print("ManagedAudioSession setup error \(error)") + managedAudioSessionLog("ManagedAudioSession setup error \(error)") } } @@ -741,7 +769,7 @@ public final class ManagedAudioSession { } private func setupOutputMode(_ outputMode: AudioSessionOutputMode, type: ManagedAudioSessionType) throws { - print("ManagedAudioSession setup \(outputMode) for \(type)") + managedAudioSessionLog("ManagedAudioSession setup \(outputMode) for \(type)") var resetToBuiltin = false switch outputMode { case .system: @@ -831,21 +859,21 @@ public final class ManagedAudioSession { try AVAudioSession.sharedInstance().setActive(true, options: [.notifyOthersOnDeactivation]) - print("\(CFAbsoluteTimeGetCurrent()) AudioSession activate: \((CFAbsoluteTimeGetCurrent() - startTime) * 1000.0) ms") + managedAudioSessionLog("\(CFAbsoluteTimeGetCurrent()) AudioSession activate: \((CFAbsoluteTimeGetCurrent() - startTime) * 1000.0) ms") try self.setupOutputMode(outputMode, type: type) - print("\(CFAbsoluteTimeGetCurrent()) AudioSession setupOutputMode: \((CFAbsoluteTimeGetCurrent() - startTime) * 1000.0) ms") + managedAudioSessionLog("\(CFAbsoluteTimeGetCurrent()) AudioSession setupOutputMode: \((CFAbsoluteTimeGetCurrent() - startTime) * 1000.0) ms") self.updateCurrentAudioRouteInfo() - print("\(CFAbsoluteTimeGetCurrent()) AudioSession updateCurrentAudioRouteInfo: \((CFAbsoluteTimeGetCurrent() - startTime) * 1000.0) ms") + managedAudioSessionLog("\(CFAbsoluteTimeGetCurrent()) AudioSession updateCurrentAudioRouteInfo: \((CFAbsoluteTimeGetCurrent() - startTime) * 1000.0) ms") if case .voiceCall = type { try AVAudioSession.sharedInstance().setPreferredIOBufferDuration(0.005) } } catch let error { - print("ManagedAudioSession activate error \(error)") + managedAudioSessionLog("ManagedAudioSession activate error \(error)") } } } @@ -858,7 +886,7 @@ public final class ManagedAudioSession { public func callKitActivatedAudioSession() { /*self.queue.async { - print("ManagedAudioSession callKitDeactivatedAudioSession") + managedAudioSessionLog("ManagedAudioSession callKitDeactivatedAudioSession") self.callKitAudioSessionIsActive = true self.updateHolders() }*/ @@ -866,7 +894,7 @@ public final class ManagedAudioSession { public func callKitDeactivatedAudioSession() { /*self.queue.async { - print("ManagedAudioSession callKitDeactivatedAudioSession") + managedAudioSessionLog("ManagedAudioSession callKitDeactivatedAudioSession") self.callKitAudioSessionIsActive = false self.updateHolders() }*/ diff --git a/submodules/TelegramBaseController/BUILD b/submodules/TelegramBaseController/BUILD index dfa86f0840..7d1c7d3d24 100644 --- a/submodules/TelegramBaseController/BUILD +++ b/submodules/TelegramBaseController/BUILD @@ -21,6 +21,7 @@ swift_library( "//submodules/PresentationDataUtils:PresentationDataUtils", "//submodules/Markdown:Markdown", "//submodules/TelegramCallsUI:TelegramCallsUI", + "//submodules/ManagedAnimationNode:ManagedAnimationNode", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramBaseController/Sources/LocationBroadcastNavigationAccessoryPanel.swift b/submodules/TelegramBaseController/Sources/LocationBroadcastNavigationAccessoryPanel.swift index fa59e85683..d99edd3a58 100644 --- a/submodules/TelegramBaseController/Sources/LocationBroadcastNavigationAccessoryPanel.swift +++ b/submodules/TelegramBaseController/Sources/LocationBroadcastNavigationAccessoryPanel.swift @@ -49,9 +49,8 @@ final class LocationBroadcastNavigationAccessoryPanel: ASDisplayNode { self.tapAction = tapAction self.close = close - + self.contentNode = ASDisplayNode() - self.contentNode.backgroundColor = self.theme.rootController.navigationBar.backgroundColor self.iconNode = ASImageNode() self.iconNode.isLayerBacked = true @@ -101,7 +100,6 @@ final class LocationBroadcastNavigationAccessoryPanel: ASDisplayNode { self.theme = presentationData.theme self.strings = presentationData.strings - self.contentNode.backgroundColor = self.theme.rootController.navigationBar.backgroundColor self.iconNode.image = PresentationResourcesRootController.navigationLiveLocationIcon(self.theme) self.wavesNode.color = self.theme.rootController.navigationBar.accentTextColor @@ -178,7 +176,7 @@ final class LocationBroadcastNavigationAccessoryPanel: ASDisplayNode { let closeButtonSize = self.closeButton.measure(CGSize(width: 100.0, height: 100.0)) transition.updateFrame(node: self.closeButton, frame: CGRect(origin: CGPoint(x: bounds.size.width - 18.0 - closeButtonSize.width - rightInset, y: minimizedTitleFrame.minY + 8.0), size: closeButtonSize)) - transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: size.height - UIScreenPixel), size: CGSize(width: size.width, height: UIScreenPixel))) + transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: UIScreenPixel))) } func update(peers: [Peer], mode: LocationBroadcastNavigationAccessoryPanelMode, canClose: Bool) { @@ -191,9 +189,16 @@ final class LocationBroadcastNavigationAccessoryPanel: ASDisplayNode { func animateIn(_ transition: ContainedViewLayoutTransition) { self.clipsToBounds = true let contentPosition = self.contentNode.layer.position + transition.animatePosition(node: self.contentNode, from: CGPoint(x: contentPosition.x, y: contentPosition.y - 37.0), completion: { [weak self] _ in self?.clipsToBounds = false }) + + guard let (size, _, _) = self.validLayout else { + return + } + + transition.animatePositionAdditive(node: self.separatorNode, offset: CGPoint(x: 0.0, y: size.height)) } func animateOut(_ transition: ContainedViewLayoutTransition, completion: @escaping () -> Void) { @@ -203,6 +208,12 @@ final class LocationBroadcastNavigationAccessoryPanel: ASDisplayNode { self?.clipsToBounds = false completion() }) + + guard let (size, _, _) = self.validLayout else { + return + } + + transition.updatePosition(node: self.separatorNode, position: self.separatorNode.position.offsetBy(dx: 0.0, dy: size.height)) } @objc func closePressed() { diff --git a/submodules/TelegramBaseController/Sources/MediaNavigationAccessoryContainerNode.swift b/submodules/TelegramBaseController/Sources/MediaNavigationAccessoryContainerNode.swift index c9c981ae06..ee4211ad3a 100644 --- a/submodules/TelegramBaseController/Sources/MediaNavigationAccessoryContainerNode.swift +++ b/submodules/TelegramBaseController/Sources/MediaNavigationAccessoryContainerNode.swift @@ -8,36 +8,59 @@ import TelegramPresentationData import AccountContext public final class MediaNavigationAccessoryContainerNode: ASDisplayNode, UIGestureRecognizerDelegate { + private let displayBackground: Bool + public let backgroundNode: ASDisplayNode + public let separatorNode: ASDisplayNode public let headerNode: MediaNavigationAccessoryHeaderNode private let currentHeaderHeight: CGFloat = MediaNavigationAccessoryHeaderNode.minimizedHeight private var presentationData: PresentationData - init(context: AccountContext) { + init(context: AccountContext, displayBackground: Bool) { + self.displayBackground = displayBackground + self.presentationData = context.sharedContext.currentPresentationData.with { $0 } self.backgroundNode = ASDisplayNode() + self.separatorNode = ASDisplayNode() self.headerNode = MediaNavigationAccessoryHeaderNode(presentationData: self.presentationData) super.init() - - self.backgroundNode.backgroundColor = self.presentationData.theme.rootController.navigationBar.backgroundColor + + if self.displayBackground { + self.backgroundNode.backgroundColor = self.presentationData.theme.rootController.navigationBar.opaqueBackgroundColor + self.separatorNode.backgroundColor = self.presentationData.theme.rootController.navigationBar.separatorColor + } self.addSubnode(self.backgroundNode) + self.addSubnode(self.separatorNode) self.addSubnode(self.headerNode) } func updatePresentationData(_ presentationData: PresentationData) { self.presentationData = presentationData - - self.backgroundNode.backgroundColor = self.presentationData.theme.rootController.navigationBar.backgroundColor + + if self.displayBackground { + self.backgroundNode.backgroundColor = self.presentationData.theme.rootController.navigationBar.opaqueBackgroundColor + self.separatorNode.backgroundColor = self.presentationData.theme.rootController.navigationBar.separatorColor + } + self.headerNode.updatePresentationData(presentationData) } - + + func animateIn(transition: ContainedViewLayoutTransition) { + self.headerNode.animateIn(transition: transition) + } + + func animateOut(transition: ContainedViewLayoutTransition) { + self.headerNode.animateOut(transition: transition) + } + func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition) { transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: self.currentHeaderHeight))) + transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: self.currentHeaderHeight - UIScreenPixel), size: CGSize(width: size.width, height: UIScreenPixel))) let headerHeight = self.currentHeaderHeight transition.updateFrame(node: self.headerNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: headerHeight))) diff --git a/submodules/TelegramBaseController/Sources/MediaNavigationAccessoryHeaderNode.swift b/submodules/TelegramBaseController/Sources/MediaNavigationAccessoryHeaderNode.swift index 31aab50b23..2816162c63 100644 --- a/submodules/TelegramBaseController/Sources/MediaNavigationAccessoryHeaderNode.swift +++ b/submodules/TelegramBaseController/Sources/MediaNavigationAccessoryHeaderNode.swift @@ -10,6 +10,7 @@ import TelegramUIPreferences import UniversalMediaPlayer import AccountContext import TelegramStringFormatting +import ManagedAnimationNode private let titleFont = Font.regular(12.0) private let subtitleFont = Font.regular(10.0) @@ -79,7 +80,7 @@ private class MediaHeaderItemNode: ASDisplayNode { } if titleText == subtitleText { - subtitleText = humanReadableStringForTimestamp(strings: strings, dateTimeFormat: dateTimeFormat, timestamp: timestamp) + subtitleText = humanReadableStringForTimestamp(strings: strings, dateTimeFormat: dateTimeFormat, timestamp: timestamp).0 } titleString = NSAttributedString(string: titleText, font: titleFont, textColor: theme.rootController.navigationBar.primaryTextColor) @@ -147,8 +148,7 @@ public final class MediaNavigationAccessoryHeaderNode: ASDisplayNode, UIScrollVi private let closeButton: HighlightableButtonNode private let actionButton: HighlightTrackingButtonNode - private let actionPauseNode: ASImageNode - private let actionPlayNode: ASImageNode + private let playPauseIconNode: PlayPauseIconNode private let rateButton: HighlightableButtonNode private let accessibilityAreaNode: AccessibilityAreaNode @@ -226,7 +226,7 @@ public final class MediaNavigationAccessoryHeaderNode: ASDisplayNode, UIScrollVi self.rightMaskNode = ASImageNode() self.rightMaskNode.contentMode = .scaleToFill - let maskImage = generateMaskImage(color: self.theme.rootController.navigationBar.backgroundColor) + let maskImage = generateMaskImage(color: self.theme.rootController.navigationBar.opaqueBackgroundColor) self.leftMaskNode.image = maskImage self.rightMaskNode.image = maskImage @@ -248,20 +248,8 @@ public final class MediaNavigationAccessoryHeaderNode: ASDisplayNode, UIScrollVi self.actionButton.hitTestSlop = UIEdgeInsets(top: -8.0, left: -8.0, bottom: -8.0, right: -8.0) self.actionButton.displaysAsynchronously = false - self.actionPauseNode = ASImageNode() - self.actionPauseNode.contentMode = .center - self.actionPauseNode.isLayerBacked = true - self.actionPauseNode.displaysAsynchronously = false - self.actionPauseNode.displayWithoutProcessing = true - self.actionPauseNode.image = PresentationResourcesRootController.navigationPlayerPauseIcon(self.theme) - - self.actionPlayNode = ASImageNode() - self.actionPlayNode.contentMode = .center - self.actionPlayNode.isLayerBacked = true - self.actionPlayNode.displaysAsynchronously = false - self.actionPlayNode.displayWithoutProcessing = true - self.actionPlayNode.image = PresentationResourcesRootController.navigationPlayerPlayIcon(self.theme) - self.actionPlayNode.isHidden = true + self.playPauseIconNode = PlayPauseIconNode() + self.playPauseIconNode.customColor = self.theme.rootController.navigationBar.accentTextColor self.scrubbingNode = MediaPlayerScrubbingNode(content: .standard(lineHeight: 2.0, lineCap: .square, scrubberHandle: .none, backgroundColor: .clear, foregroundColor: self.theme.rootController.navigationBar.accentTextColor, bufferingColor: self.theme.rootController.navigationBar.accentTextColor.withAlphaComponent(0.5), chapters: [])) @@ -278,15 +266,14 @@ public final class MediaNavigationAccessoryHeaderNode: ASDisplayNode, UIScrollVi self.scrollNode.addSubnode(self.previousItemNode) self.scrollNode.addSubnode(self.nextItemNode) - self.addSubnode(self.leftMaskNode) - self.addSubnode(self.rightMaskNode) + //self.addSubnode(self.leftMaskNode) + //self.addSubnode(self.rightMaskNode) self.addSubnode(self.closeButton) self.addSubnode(self.rateButton) self.addSubnode(self.accessibilityAreaNode) - self.actionButton.addSubnode(self.actionPauseNode) - self.actionButton.addSubnode(self.actionPlayNode) + self.actionButton.addSubnode(self.playPauseIconNode) self.addSubnode(self.actionButton) self.closeButton.addTarget(self, action: #selector(self.closeButtonPressed), forControlEvents: .touchUpInside) @@ -341,8 +328,7 @@ public final class MediaNavigationAccessoryHeaderNode: ASDisplayNode, UIScrollVi } else { paused = true } - strongSelf.actionPlayNode.isHidden = !paused - strongSelf.actionPauseNode.isHidden = paused + strongSelf.playPauseIconNode.enqueueState(paused ? .play : .pause, animated: true) strongSelf.actionButton.accessibilityLabel = paused ? strongSelf.strings.VoiceOver_Media_PlaybackPlay : strongSelf.strings.VoiceOver_Media_PlaybackPause } } @@ -369,13 +355,12 @@ public final class MediaNavigationAccessoryHeaderNode: ASDisplayNode, UIScrollVi self.nameDisplayOrder = presentationData.nameDisplayOrder self.dateTimeFormat = presentationData.dateTimeFormat - let maskImage = generateMaskImage(color: self.theme.rootController.navigationBar.backgroundColor) + let maskImage = generateMaskImage(color: self.theme.rootController.navigationBar.opaqueBackgroundColor) self.leftMaskNode.image = maskImage self.rightMaskNode.image = maskImage self.closeButton.setImage(PresentationResourcesRootController.navigationPlayerCloseButton(self.theme), for: []) - self.actionPlayNode.image = PresentationResourcesRootController.navigationPlayerPlayIcon(self.theme) - self.actionPauseNode.image = PresentationResourcesRootController.navigationPlayerPauseIcon(self.theme) + self.playPauseIconNode.customColor = self.theme.rootController.navigationBar.accentTextColor self.separatorNode.backgroundColor = self.theme.rootController.navigationBar.separatorColor self.scrubbingNode.updateContent(.standard(lineHeight: 2.0, lineCap: .square, scrubberHandle: .none, backgroundColor: .clear, foregroundColor: self.theme.rootController.navigationBar.accentTextColor, bufferingColor: self.theme.rootController.navigationBar.accentTextColor.withAlphaComponent(0.5), chapters: [])) @@ -430,6 +415,22 @@ public final class MediaNavigationAccessoryHeaderNode: ASDisplayNode, UIScrollVi self.playNext?() } } + + func animateIn(transition: ContainedViewLayoutTransition) { + guard let (size, _, _) = self.validLayout else { + return + } + + transition.animatePositionAdditive(node: self.separatorNode, offset: CGPoint(x: 0.0, y: size.height)) + } + + func animateOut(transition: ContainedViewLayoutTransition) { + guard let (size, _, _) = self.validLayout else { + return + } + + transition.updatePosition(node: self.separatorNode, position: self.separatorNode.position.offsetBy(dx: 0.0, dy: size.height)) + } public func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition) { self.validLayout = (size, leftInset, rightInset) @@ -477,12 +478,11 @@ public final class MediaNavigationAccessoryHeaderNode: ASDisplayNode, UIScrollVi transition.updateFrame(node: self.closeButton, frame: CGRect(origin: CGPoint(x: bounds.size.width - 44.0 - rightInset, y: 0.0), size: CGSize(width: 44.0, height: minHeight))) let rateButtonSize = CGSize(width: 24.0, height: minHeight) transition.updateFrame(node: self.rateButton, frame: CGRect(origin: CGPoint(x: bounds.size.width - 18.0 - closeButtonSize.width - 17.0 - rateButtonSize.width - rightInset, y: 0.0), size: rateButtonSize)) - transition.updateFrame(node: self.actionPlayNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: 40.0, height: 37.0))) - transition.updateFrame(node: self.actionPauseNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: 40.0, height: 37.0))) + transition.updateFrame(node: self.playPauseIconNode, frame: CGRect(origin: CGPoint(x: 6.0, y: 4.0 + UIScreenPixel), size: CGSize(width: 28.0, height: 28.0))) transition.updateFrame(node: self.actionButton, frame: CGRect(origin: CGPoint(x: leftInset, y: 0.0), size: CGSize(width: 40.0, height: 37.0))) transition.updateFrame(node: self.scrubbingNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 37.0 - 2.0), size: CGSize(width: size.width, height: 2.0))) - transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: minHeight - UIScreenPixel), size: CGSize(width: size.width, height: UIScreenPixel))) + transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: UIScreenPixel))) self.accessibilityAreaNode.frame = CGRect(origin: CGPoint(x: self.actionButton.frame.maxX, y: 0.0), size: CGSize(width: self.rateButton.frame.minX - self.actionButton.frame.maxX, height: minHeight)) } @@ -505,3 +505,53 @@ public final class MediaNavigationAccessoryHeaderNode: ASDisplayNode, UIScrollVi } } } + +private enum PlayPauseIconNodeState: Equatable { + case play + case pause +} + +private final class PlayPauseIconNode: ManagedAnimationNode { + private let duration: Double = 0.35 + private var iconState: PlayPauseIconNodeState = .pause + + init() { + super.init(size: CGSize(width: 28.0, height: 28.0)) + + self.trackTo(item: ManagedAnimationItem(source: .local("anim_playpause"), frames: .range(startFrame: 41, endFrame: 41), duration: 0.01)) + } + + func enqueueState(_ state: PlayPauseIconNodeState, animated: Bool) { + guard self.iconState != state else { + return + } + + let previousState = self.iconState + self.iconState = state + + switch previousState { + case .pause: + switch state { + case .play: + if animated { + self.trackTo(item: ManagedAnimationItem(source: .local("anim_playpause"), frames: .range(startFrame: 41, endFrame: 83), duration: self.duration)) + } else { + self.trackTo(item: ManagedAnimationItem(source: .local("anim_playpause"), frames: .range(startFrame: 0, endFrame: 0), duration: 0.01)) + } + case .pause: + break + } + case .play: + switch state { + case .pause: + if animated { + self.trackTo(item: ManagedAnimationItem(source: .local("anim_playpause"), frames: .range(startFrame: 0, endFrame: 41), duration: self.duration)) + } else { + self.trackTo(item: ManagedAnimationItem(source: .local("anim_playpause"), frames: .range(startFrame: 41, endFrame: 41), duration: 0.01)) + } + case .play: + break + } + } + } +} diff --git a/submodules/TelegramBaseController/Sources/MediaNavigationAccessoryPanel.swift b/submodules/TelegramBaseController/Sources/MediaNavigationAccessoryPanel.swift index 5f6aa9da2b..04ef455bf7 100644 --- a/submodules/TelegramBaseController/Sources/MediaNavigationAccessoryPanel.swift +++ b/submodules/TelegramBaseController/Sources/MediaNavigationAccessoryPanel.swift @@ -16,8 +16,8 @@ public final class MediaNavigationAccessoryPanel: ASDisplayNode { public var playPrevious: (() -> Void)? public var playNext: (() -> Void)? - public init(context: AccountContext) { - self.containerNode = MediaNavigationAccessoryContainerNode(context: context) + public init(context: AccountContext, displayBackground: Bool = false) { + self.containerNode = MediaNavigationAccessoryContainerNode(context: context, displayBackground: displayBackground) super.init() @@ -61,6 +61,9 @@ public final class MediaNavigationAccessoryPanel: ASDisplayNode { public func animateIn(transition: ContainedViewLayoutTransition) { self.clipsToBounds = true let contentPosition = self.containerNode.layer.position + + self.containerNode.animateIn(transition: transition) + transition.animatePosition(node: self.containerNode, from: CGPoint(x: contentPosition.x, y: contentPosition.y - 37.0), completion: { [weak self] _ in self?.clipsToBounds = false }) @@ -69,6 +72,9 @@ public final class MediaNavigationAccessoryPanel: ASDisplayNode { public func animateOut(transition: ContainedViewLayoutTransition, completion: @escaping () -> Void) { self.clipsToBounds = true let contentPosition = self.containerNode.layer.position + + self.containerNode.animateOut(transition: transition) + transition.animatePosition(node: self.containerNode, to: CGPoint(x: contentPosition.x, y: contentPosition.y - 37.0), removeOnCompletion: false, completion: { [weak self] _ in self?.clipsToBounds = false completion() diff --git a/submodules/TelegramBaseController/Sources/TelegramBaseController.swift b/submodules/TelegramBaseController/Sources/TelegramBaseController.swift index 9bdaf8d5e4..16e3e6bd81 100644 --- a/submodules/TelegramBaseController/Sources/TelegramBaseController.swift +++ b/submodules/TelegramBaseController/Sources/TelegramBaseController.swift @@ -77,6 +77,7 @@ open class TelegramBaseController: ViewController, KeyShortcutResponder { public var tempVoicePlaylistEnded: (() -> Void)? public var tempVoicePlaylistItemChanged: ((SharedMediaPlaylistItem?, SharedMediaPlaylistItem?) -> Void)? + public var tempVoicePlaylistCurrentItem: SharedMediaPlaylistItem? public var mediaAccessoryPanel: (MediaNavigationAccessoryPanel, MediaManagerPlayerType)? @@ -94,19 +95,7 @@ open class TelegramBaseController: ViewController, KeyShortcutResponder { private var presentationDataDisposable: Disposable? private var playlistPreloadDisposable: Disposable? - override open var navigationHeight: CGFloat { - return super.navigationHeight + self.additionalHeight - } - - override open var navigationInsetHeight: CGFloat { - return super.navigationInsetHeight + self.additionalHeight - } - - override open var visualNavigationInsetHeight: CGFloat { - return super.visualNavigationInsetHeight + self.additionalHeight - } - - public var additionalHeight: CGFloat { + override open var additionalNavigationBarHeight: CGFloat { var height: CGFloat = 0.0 if let _ = self.groupCallAccessoryPanel { height += 50.0 @@ -120,10 +109,6 @@ open class TelegramBaseController: ViewController, KeyShortcutResponder { return height } - open var primaryNavigationHeight: CGFloat { - return super.navigationHeight - } - public init(context: AccountContext, navigationBarPresentationData: NavigationBarPresentationData?, mediaAccessoryPanelVisibility: MediaAccessoryPanelVisibility, locationBroadcastPanelSource: LocationBroadcastPanelSource, groupCallPanelSource: GroupCallPanelSource) { self.context = context self.presentationData = context.sharedContext.currentPresentationData.with { $0 } @@ -166,6 +151,7 @@ open class TelegramBaseController: ViewController, KeyShortcutResponder { updatedVoiceItem = playlistStateAndType.1.item } + strongSelf.tempVoicePlaylistCurrentItem = updatedVoiceItem strongSelf.tempVoicePlaylistItemChanged?(previousVoiceItem, updatedVoiceItem) if let playlistStateAndType = playlistStateAndType { strongSelf.playlistStateAndType = (playlistStateAndType.1.item, playlistStateAndType.1.previousItem, playlistStateAndType.1.nextItem, playlistStateAndType.1.order, playlistStateAndType.2, playlistStateAndType.0) @@ -303,7 +289,7 @@ open class TelegramBaseController: ViewController, KeyShortcutResponder { let disposable = MetaDisposable() callContextCache.impl.syncWith { impl in - let callContext = impl.get(account: context.account, peerId: peerId, call: activeCall) + let callContext = impl.get(account: context.account, engine: context.engine, peerId: peerId, call: activeCall) disposable.set((callContext.context.panelData |> deliverOnMainQueue).start(next: { panelData in callContext.keep() @@ -333,7 +319,7 @@ open class TelegramBaseController: ViewController, KeyShortcutResponder { if previousCurrentGroupCall != nil && currentGroupCall == nil && availableState?.participantCount == 1 { panelData = nil } else { - panelData = currentGroupCall != nil || availableState?.participantCount == 0 ? nil : availableState + panelData = currentGroupCall != nil || (availableState?.participantCount == 0 && availableState?.info.scheduleTimestamp == nil) ? nil : availableState } let wasEmpty = strongSelf.groupCallPanelData == nil @@ -376,11 +362,25 @@ open class TelegramBaseController: ViewController, KeyShortcutResponder { required public init(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } + + private var suspendNavigationBarLayout: Bool = false + private var suspendedNavigationBarLayout: ContainerViewLayout? + private var additionalNavigationBarBackgroundHeight: CGFloat = 0.0 + + override open func updateNavigationBarLayout(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + if self.suspendNavigationBarLayout { + self.suspendedNavigationBarLayout = layout + return + } + self.applyNavigationBarLayout(layout, navigationLayout: self.navigationLayout(layout: layout), additionalBackgroundHeight: self.additionalNavigationBarBackgroundHeight, transition: transition) + } override open func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + self.suspendNavigationBarLayout = true + super.containerLayoutUpdated(layout, transition: transition) - var navigationHeight = super.navigationHeight + var navigationHeight = super.navigationLayout(layout: layout).navigationFrame.maxY - self.additionalNavigationBarHeight if !self.displayNavigationBar { navigationHeight = 0.0 } @@ -406,14 +406,10 @@ open class TelegramBaseController: ViewController, KeyShortcutResponder { strongSelf.joinGroupCall( peerId: groupCallPanelData.peerId, invite: nil, - activeCall: CachedChannelData.ActiveCall(id: groupCallPanelData.info.id, accessHash: groupCallPanelData.info.accessHash, title: groupCallPanelData.info.title) + activeCall: CachedChannelData.ActiveCall(id: groupCallPanelData.info.id, accessHash: groupCallPanelData.info.accessHash, title: groupCallPanelData.info.title, scheduleTimestamp: groupCallPanelData.info.scheduleTimestamp, subscribedToScheduled: groupCallPanelData.info.subscribedToScheduled) ) }) - if let navigationBar = self.navigationBar { - self.displayNode.insertSubnode(groupCallAccessoryPanel, aboveSubnode: navigationBar) - } else { - self.displayNode.addSubnode(groupCallAccessoryPanel) - } + self.navigationBar?.additionalContentNode.addSubnode(groupCallAccessoryPanel) self.groupCallAccessoryPanel = groupCallAccessoryPanel groupCallAccessoryPanel.frame = panelFrame @@ -560,11 +556,7 @@ open class TelegramBaseController: ViewController, KeyShortcutResponder { strongSelf.present(controller, in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) } }) - if let navigationBar = self.navigationBar { - self.displayNode.insertSubnode(locationBroadcastAccessoryPanel, aboveSubnode: navigationBar) - } else { - self.displayNode.addSubnode(locationBroadcastAccessoryPanel) - } + self.navigationBar?.additionalContentNode.addSubnode(locationBroadcastAccessoryPanel) self.locationBroadcastAccessoryPanel = locationBroadcastAccessoryPanel locationBroadcastAccessoryPanel.frame = panelFrame @@ -806,11 +798,9 @@ open class TelegramBaseController: ViewController, KeyShortcutResponder { } mediaAccessoryPanel.frame = panelFrame if let dismissingPanel = self.dismissingPanel { - self.displayNode.insertSubnode(mediaAccessoryPanel, aboveSubnode: dismissingPanel) - } else if let navigationBar = self.navigationBar { - self.displayNode.insertSubnode(mediaAccessoryPanel, belowSubnode: navigationBar) + self.navigationBar?.additionalContentNode.insertSubnode(mediaAccessoryPanel, aboveSubnode: dismissingPanel) } else { - self.displayNode.addSubnode(mediaAccessoryPanel) + self.navigationBar?.additionalContentNode.addSubnode(mediaAccessoryPanel) } self.mediaAccessoryPanel = (mediaAccessoryPanel, type) mediaAccessoryPanel.updateLayout(size: panelFrame.size, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, transition: .immediate) @@ -842,6 +832,12 @@ open class TelegramBaseController: ViewController, KeyShortcutResponder { } }) } + + self.suspendNavigationBarLayout = false + if let suspendedNavigationBarLayout = self.suspendedNavigationBarLayout { + self.suspendedNavigationBarLayout = suspendedNavigationBarLayout + self.applyNavigationBarLayout(suspendedNavigationBarLayout, navigationLayout: self.navigationLayout(layout: layout), additionalBackgroundHeight: self.additionalNavigationBarBackgroundHeight, transition: transition) + } } open var keyShortcuts: [KeyShortcut] { @@ -856,6 +852,8 @@ open class TelegramBaseController: ViewController, KeyShortcutResponder { let context = self.context let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } + self.view.endEditing(true) + self.context.joinGroupCall(peerId: peerId, invite: invite, requestJoinAsPeerId: { completion in let currentAccountPeer = context.account.postbox.loadedPeerWithId(context.account.peerId) |> map { peer in @@ -865,7 +863,7 @@ open class TelegramBaseController: ViewController, KeyShortcutResponder { return transaction.getPeerCachedData(peerId: peerId) } - let _ = (combineLatest(currentAccountPeer, cachedGroupCallDisplayAsAvailablePeers(account: context.account, peerId: peerId), cachedData) + let _ = (combineLatest(currentAccountPeer, context.engine.calls.cachedGroupCallDisplayAsAvailablePeers(peerId: peerId), cachedData) |> map { currentAccountPeer, availablePeers, cachedData -> ([FoundPeer], CachedPeerData?) in var result = currentAccountPeer result.append(contentsOf: availablePeers) @@ -898,7 +896,10 @@ open class TelegramBaseController: ViewController, KeyShortcutResponder { var items: [ActionSheetItem] = [] var isGroup = false for peer in peers { - if let peer = peer.peer as? TelegramChannel, case .group = peer.info { + if peer.peer is TelegramGroup { + isGroup = true + break + } else if let peer = peer.peer as? TelegramChannel, case .group = peer.info { isGroup = true break } diff --git a/submodules/TelegramCallsUI/BUILD b/submodules/TelegramCallsUI/BUILD index 50825f66db..57dca2b206 100644 --- a/submodules/TelegramCallsUI/BUILD +++ b/submodules/TelegramCallsUI/BUILD @@ -1,4 +1,44 @@ load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") +load( + "@build_bazel_rules_apple//apple:resources.bzl", + "apple_resource_bundle", + "apple_resource_group", +) +load("//build-system/bazel-utils:plist_fragment.bzl", + "plist_fragment", +) + +filegroup( + name = "TelegramCallsUIMetalResources", + srcs = glob([ + "Resources/**/*.metal", + ]), + visibility = ["//visibility:public"], +) + +plist_fragment( + name = "TelegramCallsUIBundleInfoPlist", + extension = "plist", + template = + """ + CFBundleIdentifier + org.telegram.TelegramCallsUI + CFBundleDevelopmentRegion + en + CFBundleName + TelegramCallsUI + """ +) + +apple_resource_bundle( + name = "TelegramCallsUIBundle", + infoplists = [ + ":TelegramCallsUIBundleInfoPlist", + ], + resources = [ + ":TelegramCallsUIMetalResources", + ], +) swift_library( name = "TelegramCallsUI", @@ -6,6 +46,9 @@ swift_library( srcs = glob([ "Sources/**/*.swift", ]), + data = [ + ":TelegramCallsUIBundle", + ], deps = [ "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", "//submodules/Display:Display", @@ -42,6 +85,12 @@ swift_library( "//submodules/DeviceProximity:DeviceProximity", "//submodules/ManagedAnimationNode:ManagedAnimationNode", "//submodules/TemporaryCachedPeerDataManager:TemporaryCachedPeerDataManager", + "//submodules/PeerInfoAvatarListNode:PeerInfoAvatarListNode", + "//submodules/WebSearchUI:WebSearchUI", + "//submodules/MapResourceToAvatarSizes:MapResourceToAvatarSizes", + "//submodules/TextFormat:TextFormat", + "//submodules/Markdown:Markdown", + "//submodules/ChatTitleActivityNode:ChatTitleActivityNode", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramCallsUI/Resources/I420VideoShaders.metal b/submodules/TelegramCallsUI/Resources/I420VideoShaders.metal new file mode 100644 index 0000000000..894d3e520c --- /dev/null +++ b/submodules/TelegramCallsUI/Resources/I420VideoShaders.metal @@ -0,0 +1,49 @@ +#include +using namespace metal; + +typedef struct { + packed_float2 position; + packed_float2 texcoord; +} Vertex; + +typedef struct { + float4 position[[position]]; + float2 texcoord; +} Varyings; + +vertex Varyings i420VertexPassthrough(constant Vertex *verticies[[buffer(0)]], + unsigned int vid[[vertex_id]]) { + Varyings out; + constant Vertex &v = verticies[vid]; + out.position = float4(float2(v.position), 0.0, 1.0); + out.texcoord = v.texcoord; + + return out; +} + +fragment half4 i420FragmentColorConversion( + Varyings in[[stage_in]], + texture2d textureY[[texture(0)]], + texture2d textureU[[texture(1)]], + texture2d textureV[[texture(2)]]) { + constexpr sampler s(address::clamp_to_edge, filter::linear); + float y; + float u; + float v; + float r; + float g; + float b; + // Conversion for YUV to rgb from http://www.fourcc.org/fccyvrgb.php + y = textureY.sample(s, in.texcoord).r; + u = textureU.sample(s, in.texcoord).r; + v = textureV.sample(s, in.texcoord).r; + u = u - 0.5; + v = v - 0.5; + r = y + 1.403 * v; + g = y - 0.344 * u - 0.714 * v; + b = y + 1.770 * u; + + float4 out = float4(r, g, b, 1.0); + + return half4(out); +} \ No newline at end of file diff --git a/submodules/TelegramCallsUI/Resources/NV12VideoShaders.metal b/submodules/TelegramCallsUI/Resources/NV12VideoShaders.metal new file mode 100644 index 0000000000..3f0fca262d --- /dev/null +++ b/submodules/TelegramCallsUI/Resources/NV12VideoShaders.metal @@ -0,0 +1,57 @@ +#include +using namespace metal; + +typedef struct { + packed_float2 position; + packed_float2 texcoord; +} Vertex; + +typedef struct { + float4 position[[position]]; + float2 texcoord; +} Varyings; + +vertex Varyings nv12VertexPassthrough( + constant Vertex *verticies[[buffer(0)]], + unsigned int vid[[vertex_id]] +) { + Varyings out; + constant Vertex &v = verticies[vid]; + out.position = float4(float2(v.position), 0.0, 1.0); + out.texcoord = v.texcoord; + return out; +} + +float4 samplePoint(texture2d textureY, texture2d textureCbCr, sampler s, float2 texcoord) { + float y; + float2 uv; + y = textureY.sample(s, texcoord).r; + uv = textureCbCr.sample(s, texcoord).rg - float2(0.5, 0.5); + + // Conversion for YUV to rgb from http://www.fourcc.org/fccyvrgb.php + float4 out = float4(y + 1.403 * uv.y, y - 0.344 * uv.x - 0.714 * uv.y, y + 1.770 * uv.x, 1.0); + return out; +} + +fragment half4 nv12FragmentColorConversion( + Varyings in[[stage_in]], + texture2d textureY[[texture(0)]], + texture2d textureCbCr[[texture(1)]] +) { + constexpr sampler s(address::clamp_to_edge, filter::linear); + + float4 out = samplePoint(textureY, textureCbCr, s, in.texcoord); + + return half4(out); +} + +fragment half4 blitFragmentColorConversion( + Varyings in[[stage_in]], + texture2d texture[[texture(0)]] +) { + constexpr sampler s(address::clamp_to_edge, filter::linear); + + float4 out = texture.sample(s, in.texcoord); + + return half4(out); +} diff --git a/submodules/TelegramCallsUI/Sources/CallController.swift b/submodules/TelegramCallsUI/Sources/CallController.swift index 64281e9645..3fde19a833 100644 --- a/submodules/TelegramCallsUI/Sources/CallController.swift +++ b/submodules/TelegramCallsUI/Sources/CallController.swift @@ -187,7 +187,9 @@ public final class CallController: ViewController { if port.type == .bluetooth { var image = UIImage(bundleImageName: "Call/CallBluetoothButton") let portName = port.name.lowercased() - if portName.contains("airpods pro") { + if portName.contains("airpods max") { + image = UIImage(bundleImageName: "Call/CallAirpodsMaxButton") + } else if portName.contains("airpods pro") { image = UIImage(bundleImageName: "Call/CallAirpodsProButton") } else if portName.contains("airpods") { image = UIImage(bundleImageName: "Call/CallAirpodsButton") @@ -341,7 +343,7 @@ public final class CallController: 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.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition) } override public func dismiss(completion: (() -> Void)? = nil) { diff --git a/submodules/TelegramCallsUI/Sources/CallControllerButton.swift b/submodules/TelegramCallsUI/Sources/CallControllerButton.swift index 5a0bc5da67..42468535b8 100644 --- a/submodules/TelegramCallsUI/Sources/CallControllerButton.swift +++ b/submodules/TelegramCallsUI/Sources/CallControllerButton.swift @@ -5,6 +5,7 @@ import AsyncDisplayKit import SwiftSignalKit import AppBundle import SemanticStatusNode +import AnimationUI private let labelFont = Font.regular(13.0) @@ -30,6 +31,8 @@ final class CallControllerButtonItemNode: HighlightTrackingButtonNode { } enum Image { + case cameraOff + case cameraOn case camera case mute case flipCamera @@ -37,10 +40,12 @@ final class CallControllerButtonItemNode: HighlightTrackingButtonNode { case speaker case airpods case airpodsPro + case airpodsMax case headphones case accept case end case cancel + case share } var appearance: Appearance @@ -56,20 +61,26 @@ final class CallControllerButtonItemNode: HighlightTrackingButtonNode { } } + private let wrapperNode: ASDisplayNode private let contentContainer: ASDisplayNode private let effectView: UIVisualEffectView private let contentBackgroundNode: ASImageNode private let contentNode: ASImageNode + private var animationNode: AnimationNode? private let overlayHighlightNode: ASImageNode private var statusNode: SemanticStatusNode? let textNode: ImmediateTextNode - private let largeButtonSize: CGFloat = 72.0 + private let largeButtonSize: CGFloat + private var size: CGSize? private(set) var currentContent: Content? private(set) var currentText: String = "" - init() { + init(largeButtonSize: CGFloat = 72.0) { + self.largeButtonSize = largeButtonSize + + self.wrapperNode = ASDisplayNode() self.contentContainer = ASDisplayNode() self.effectView = UIVisualEffectView() @@ -94,10 +105,11 @@ final class CallControllerButtonItemNode: HighlightTrackingButtonNode { super.init(pointerStyle: nil) - self.addSubnode(self.contentContainer) + self.addSubnode(self.wrapperNode) + self.wrapperNode.addSubnode(self.contentContainer) self.contentContainer.frame = CGRect(origin: CGPoint(), size: CGSize(width: self.largeButtonSize, height: self.largeButtonSize)) - self.addSubnode(self.textNode) + self.wrapperNode.addSubnode(self.textNode) self.contentContainer.view.addSubview(self.effectView) self.contentContainer.addSubnode(self.contentBackgroundNode) @@ -121,6 +133,11 @@ final class CallControllerButtonItemNode: HighlightTrackingButtonNode { } } + override func layout() { + super.layout() + self.wrapperNode.frame = self.bounds + } + func update(size: CGSize, content: Content, text: String, transition: ContainedViewLayoutTransition) { let scaleFactor = size.width / self.largeButtonSize @@ -131,9 +148,10 @@ final class CallControllerButtonItemNode: HighlightTrackingButtonNode { self.contentNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: self.largeButtonSize, height: self.largeButtonSize)) self.overlayHighlightNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: self.largeButtonSize, height: self.largeButtonSize)) - if self.currentContent != content { + if self.currentContent != content || self.size != size { let previousContent = self.currentContent self.currentContent = content + self.size = size if content.hasProgress { let statusFrame = CGRect(origin: CGPoint(), size: CGSize(width: self.largeButtonSize, height: self.largeButtonSize)) @@ -164,11 +182,40 @@ final class CallControllerButtonItemNode: HighlightTrackingButtonNode { self.effectView.isHidden = true } - transition.updateAlpha(node: self, alpha: content.isEnabled ? 1.0 : 0.4) - self.isUserInteractionEnabled = content.isEnabled + transition.updateAlpha(node: self.wrapperNode, alpha: content.isEnabled ? 1.0 : 0.4) + self.wrapperNode.isUserInteractionEnabled = content.isEnabled let contentBackgroundImage: UIImage? = nil + var animationName: String? + switch content.image { + case .cameraOff: + animationName = "anim_cameraoff" + case .cameraOn: + animationName = "anim_cameraon" + default: + break + } + + if let animationName = animationName { + let animationFrame = CGRect(origin: CGPoint(), size: CGSize(width: self.largeButtonSize, height: self.largeButtonSize)) + if self.animationNode == nil { + let animationNode = AnimationNode(animation: animationName, colors: nil, scale: 1.0) + self.animationNode = animationNode + self.contentContainer.insertSubnode(animationNode, aboveSubnode: self.contentNode) + } + if let animationNode = self.animationNode { + animationNode.bounds = animationFrame + animationNode.position = CGPoint(x: self.largeButtonSize / 2.0, y: self.largeButtonSize / 2.0) + if previousContent == nil { + animationNode.seekToEnd() + } else if previousContent?.image != content.image { + animationNode.setAnimation(name: animationName) + animationNode.play() + } + } + } + let contentImage = generateImage(CGSize(width: self.largeButtonSize, height: self.largeButtonSize), contextGenerator: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) @@ -209,6 +256,8 @@ final class CallControllerButtonItemNode: HighlightTrackingButtonNode { var image: UIImage? switch content.image { + case .cameraOff, .cameraOn: + image = nil case .camera: image = generateTintedImage(image: UIImage(bundleImageName: "Call/CallCameraButton"), color: imageColor) case .mute: @@ -223,6 +272,8 @@ final class CallControllerButtonItemNode: HighlightTrackingButtonNode { image = generateTintedImage(image: UIImage(bundleImageName: "Call/CallAirpodsButton"), color: imageColor) case .airpodsPro: image = generateTintedImage(image: UIImage(bundleImageName: "Call/CallAirpodsProButton"), color: imageColor) + case .airpodsMax: + image = generateTintedImage(image: UIImage(bundleImageName: "Call/CallAirpodsMaxButton"), color: imageColor) case .headphones: image = generateTintedImage(image: UIImage(bundleImageName: "Call/CallHeadphonesButton"), color: imageColor) case .accept: @@ -246,6 +297,8 @@ final class CallControllerButtonItemNode: HighlightTrackingButtonNode { context.addLine(to: CGPoint(x: 2.0 + UIScreenPixel, y: 26.0 - UIScreenPixel)) context.strokePath() }) + case .share: + image = generateTintedImage(image: UIImage(bundleImageName: "Call/CallShareButton"), color: imageColor) } if let image = image { @@ -323,6 +376,9 @@ final class CallControllerButtonItemNode: HighlightTrackingButtonNode { transition.updatePosition(node: self.contentContainer, position: CGPoint(x: size.width / 2.0, y: size.height / 2.0)) transition.updateSublayerTransformScale(node: self.contentContainer, scale: scaleFactor) + if let animationNode = self.animationNode { + transition.updateTransformScale(node: animationNode, scale: isSmall ? 1.35 : 1.12) + } if self.currentText != text { self.textNode.attributedText = NSAttributedString(string: text, font: labelFont, textColor: .white) diff --git a/submodules/TelegramCallsUI/Sources/CallControllerButtonsNode.swift b/submodules/TelegramCallsUI/Sources/CallControllerButtonsNode.swift index bafe04e8c3..12ad79611d 100644 --- a/submodules/TelegramCallsUI/Sources/CallControllerButtonsNode.swift +++ b/submodules/TelegramCallsUI/Sources/CallControllerButtonsNode.swift @@ -11,6 +11,7 @@ enum CallControllerButtonsSpeakerMode: Equatable { case generic case airpods case airpodsPro + case airpodsMax } case none @@ -51,6 +52,7 @@ private enum ButtonDescription: Equatable { case bluetooth case airpods case airpodsPro + case airpodsMax case headphones } @@ -215,6 +217,8 @@ final class CallControllerButtonsNode: ASDisplayNode { soundOutput = .airpods case .airpodsPro: soundOutput = .airpodsPro + case .airpodsMax: + soundOutput = .airpodsMax } } @@ -306,6 +310,8 @@ final class CallControllerButtonsNode: ASDisplayNode { soundOutput = .airpods case .airpodsPro: soundOutput = .airpodsPro + case .airpodsMax: + soundOutput = .airpodsMax } } @@ -362,6 +368,8 @@ final class CallControllerButtonsNode: ASDisplayNode { soundOutput = .airpods case .airpodsPro: soundOutput = .airpodsPro + case .airpodsMax: + soundOutput = .airpodsMax } } @@ -468,6 +476,9 @@ final class CallControllerButtonsNode: ASDisplayNode { case .airpodsPro: image = .airpodsPro title = strings.Call_Audio + case .airpodsMax: + image = .airpodsMax + title = strings.Call_Audio case .headphones: image = .headphones title = strings.Call_Audio diff --git a/submodules/TelegramCallsUI/Sources/CallControllerNode.swift b/submodules/TelegramCallsUI/Sources/CallControllerNode.swift index 6c5651cc17..ded0d56666 100644 --- a/submodules/TelegramCallsUI/Sources/CallControllerNode.swift +++ b/submodules/TelegramCallsUI/Sources/CallControllerNode.swift @@ -16,6 +16,7 @@ import CallsEmoji import TooltipUI import AlertUI import PresentationDataUtils +import DeviceAccess 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))) @@ -132,6 +133,14 @@ private final class CallVideoNode: ASDisplayNode { self.isReadyTimer?.invalidate() } + override func didLoad() { + super.didLoad() + + if #available(iOS 13.0, *) { + self.layer.cornerCurve = .continuous + } + } + func animateRadialMask(from fromRect: CGRect, to toRect: CGRect) { let maskLayer = CAShapeLayer() maskLayer.frame = fromRect @@ -551,25 +560,36 @@ final class CallControllerNode: ViewControllerTracingNode, CallControllerNodePro switch callState.state { case .active: if strongSelf.outgoingVideoNodeValue == nil { - let proceed = { - strongSelf.displayedCameraConfirmation = true - switch callState.videoState { - case .inactive: - strongSelf.isRequestingVideo = true - strongSelf.updateButtonsMode() - default: - break + DeviceAccess.authorizeAccess(to: .camera(.videoCall), onlyCheck: true, presentationData: strongSelf.presentationData, present: { [weak self] c, a in + if let strongSelf = self { + strongSelf.present?(c) } - strongSelf.call.requestVideo() - } - - if strongSelf.displayedCameraConfirmation { - proceed() - } else { - strongSelf.present?(textAlertController(sharedContext: strongSelf.sharedContext, title: nil, text: strongSelf.presentationData.strings.Call_CameraConfirmationText, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Call_CameraConfirmationConfirm, action: { + }, openSettings: { [weak self] in + self?.sharedContext.applicationBindings.openSettings() + }, _: { [weak self] ready in + guard let strongSelf = self, ready else { + return + } + let proceed = { + strongSelf.displayedCameraConfirmation = true + switch callState.videoState { + case .inactive: + strongSelf.isRequestingVideo = true + strongSelf.updateButtonsMode() + default: + break + } + strongSelf.call.requestVideo() + } + + if strongSelf.displayedCameraConfirmation { proceed() - })])) - } + } else { + strongSelf.present?(textAlertController(sharedContext: strongSelf.sharedContext, title: nil, text: strongSelf.presentationData.strings.Call_CameraConfirmationText, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Call_CameraConfirmationConfirm, action: { + proceed() + })])) + } + }) } else { strongSelf.call.disableVideo() strongSelf.cancelScheduledUIHiding() diff --git a/submodules/TelegramCallsUI/Sources/CallControllerToastNode.swift b/submodules/TelegramCallsUI/Sources/CallControllerToastNode.swift index b8a472ddcd..ddcb9aa279 100644 --- a/submodules/TelegramCallsUI/Sources/CallControllerToastNode.swift +++ b/submodules/TelegramCallsUI/Sources/CallControllerToastNode.swift @@ -222,9 +222,6 @@ private class CallControllerToastItemNode: ASDisplayNode { self.clipNode = ASDisplayNode() self.clipNode.clipsToBounds = true self.clipNode.layer.cornerRadius = 14.0 - if #available(iOS 13.0, *) { - self.clipNode.layer.cornerCurve = .continuous - } self.effectView = UIVisualEffectView() self.effectView.effect = UIBlurEffect(style: .light) @@ -248,6 +245,14 @@ private class CallControllerToastItemNode: ASDisplayNode { self.clipNode.addSubnode(self.textNode) } + override func didLoad() { + super.didLoad() + + if #available(iOS 13.0, *) { + self.clipNode.layer.cornerCurve = .continuous + } + } + func update(width: CGFloat, content: Content, transition: ContainedViewLayoutTransition) -> CGFloat { let inset: CGFloat = 30.0 let isNarrowScreen = width <= 320.0 diff --git a/submodules/TelegramCallsUI/Sources/CallFeedbackController.swift b/submodules/TelegramCallsUI/Sources/CallFeedbackController.swift index 3016ce0f12..d21e7b7cd1 100644 --- a/submodules/TelegramCallsUI/Sources/CallFeedbackController.swift +++ b/submodules/TelegramCallsUI/Sources/CallFeedbackController.swift @@ -314,7 +314,7 @@ public func callFeedbackController(sharedContext: SharedAccountContext, account: } comment.append(hashtags) - let _ = rateCallAndSendLogs(account: account, callId: callId, starsCount: rating, comment: comment, userInitiated: userInitiated, includeLogs: state.includeLogs).start() + let _ = rateCallAndSendLogs(engine: TelegramEngine(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))) diff --git a/submodules/TelegramCallsUI/Sources/CallKitIntegration.swift b/submodules/TelegramCallsUI/Sources/CallKitIntegration.swift index b148c695db..e816f62270 100644 --- a/submodules/TelegramCallsUI/Sources/CallKitIntegration.swift +++ b/submodules/TelegramCallsUI/Sources/CallKitIntegration.swift @@ -169,7 +169,7 @@ class CallKitProviderDelegate: NSObject, CXProviderDelegate { func startCall(account: Account, peerId: PeerId, isVideo: Bool, displayTitle: String) { let uuid = UUID() self.currentStartCallAccount = (uuid, account) - let handle = CXHandle(type: .generic, value: "\(peerId.id)") + let handle = CXHandle(type: .generic, value: "\(peerId.id._internalGetInt32Value())") let startCallAction = CXStartCallAction(call: uuid, handle: handle) startCallAction.contactIdentifier = displayTitle diff --git a/submodules/TelegramCallsUI/Sources/CallRatingController.swift b/submodules/TelegramCallsUI/Sources/CallRatingController.swift index 63103d7e8b..961f02c7a6 100644 --- a/submodules/TelegramCallsUI/Sources/CallRatingController.swift +++ b/submodules/TelegramCallsUI/Sources/CallRatingController.swift @@ -240,24 +240,24 @@ private final class CallRatingAlertContentNode: AlertContentNode { } } -func rateCallAndSendLogs(account: Account, callId: CallId, starsCount: Int, comment: String, userInitiated: Bool, includeLogs: Bool) -> Signal { - let peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: 4244000) +func rateCallAndSendLogs(engine: TelegramEngine, callId: CallId, starsCount: Int, comment: String, userInitiated: Bool, includeLogs: Bool) -> Signal { + let peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt32Value(4244000)) - let rate = rateCall(account: account, callId: callId, starsCount: Int32(starsCount), comment: comment, userInitiated: userInitiated) + let rate = engine.calls.rateCall(callId: callId, starsCount: Int32(starsCount), comment: comment, userInitiated: userInitiated) if includeLogs { - let id = arc4random64() + let id = Int64.random(in: Int64.min ... Int64.max) let name = "\(callId.id)_\(callId.accessHash).log.json" - let path = callLogsPath(account: account) + "/" + name + let path = callLogsPath(account: engine.account) + "/" + name let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: id), partialReference: nil, resource: LocalFileReferenceMediaResource(localFilePath: path, randomId: id), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "application/text", size: nil, attributes: [.FileName(fileName: name)]) - let message = EnqueueMessage.message(text: comment, attributes: [], mediaReference: .standalone(media: file), replyToMessageId: nil, localGroupingKey: nil) + let message = EnqueueMessage.message(text: comment, attributes: [], mediaReference: .standalone(media: file), replyToMessageId: nil, localGroupingKey: nil, correlationId: nil) return rate - |> then(enqueueMessages(account: account, peerId: peerId, messages: [message]) + |> then(enqueueMessages(account: engine.account, peerId: peerId, messages: [message]) |> mapToSignal({ _ -> Signal in return .single(Void()) })) } else if !comment.isEmpty { return rate - |> then(enqueueMessages(account: account, peerId: peerId, messages: [.message(text: comment, attributes: [], mediaReference: nil, replyToMessageId: nil, localGroupingKey: nil)]) + |> then(enqueueMessages(account: engine.account, peerId: peerId, messages: [.message(text: comment, attributes: [], mediaReference: nil, replyToMessageId: nil, localGroupingKey: nil, correlationId: nil)]) |> mapToSignal({ _ -> Signal in return .single(Void()) })) @@ -284,7 +284,7 @@ public func callRatingController(sharedContext: SharedAccountContext, account: A if rating < 4 { push(callFeedbackController(sharedContext: sharedContext, account: account, callId: callId, rating: rating, userInitiated: userInitiated, isVideo: isVideo)) } else { - let _ = rateCallAndSendLogs(account: account, callId: callId, starsCount: rating, comment: "", userInitiated: userInitiated, includeLogs: false).start() + let _ = rateCallAndSendLogs(engine: TelegramEngine(account: account), callId: callId, starsCount: rating, comment: "", userInitiated: userInitiated, includeLogs: false).start() } }) diff --git a/submodules/TelegramCallsUI/Sources/CallStatusBarNode.swift b/submodules/TelegramCallsUI/Sources/CallStatusBarNode.swift index 5dbf6ecaf4..f02ecdc0ad 100644 --- a/submodules/TelegramCallsUI/Sources/CallStatusBarNode.swift +++ b/submodules/TelegramCallsUI/Sources/CallStatusBarNode.swift @@ -8,18 +8,29 @@ import TelegramCore import TelegramPresentationData import TelegramUIPreferences import AccountContext -import LegacyComponents import AnimatedCountLabelNode -private let blue = UIColor(rgb: 0x0078ff) -private let lightBlue = UIColor(rgb: 0x59c7f8) +private let blue = UIColor(rgb: 0x007fff) +private let lightBlue = UIColor(rgb: 0x00affe) private let green = UIColor(rgb: 0x33c659) private let activeBlue = UIColor(rgb: 0x00a0b9) +private let purple = UIColor(rgb: 0x3252ef) +private let pink = UIColor(rgb: 0xef436c) +private let latePurple = UIColor(rgb: 0xaa56a6) +private let latePink = UIColor(rgb: 0xef476f) private class CallStatusBarBackgroundNode: ASDisplayNode { + enum State { + case connecting + case cantSpeak + case late + case active + case speaking + } private let foregroundView: UIView private let foregroundGradientLayer: CAGradientLayer private let maskCurveView: VoiceCurveView + private let initialTimestamp = CACurrentMediaTime() var audioLevel: Float = 0.0 { didSet { @@ -35,9 +46,9 @@ private class CallStatusBarBackgroundNode: ASDisplayNode { } } - var speaking: Bool? = nil { + var state: State = .connecting { didSet { - if self.speaking != oldValue { + if self.state != oldValue { self.updateGradientColors() } } @@ -46,13 +57,28 @@ private class CallStatusBarBackgroundNode: ASDisplayNode { private func updateGradientColors() { let initialColors = self.foregroundGradientLayer.colors let targetColors: [CGColor] - if let speaking = self.speaking { - targetColors = speaking ? [green.cgColor, activeBlue.cgColor] : [blue.cgColor, lightBlue.cgColor] - } else { - targetColors = [connectingColor.cgColor, connectingColor.cgColor] + switch self.state { + case .connecting: + targetColors = [connectingColor.cgColor, connectingColor.cgColor] + case .active: + targetColors = [blue.cgColor, lightBlue.cgColor] + case .speaking: + targetColors = [green.cgColor, activeBlue.cgColor] + case .cantSpeak: + targetColors = [purple.cgColor, pink.cgColor] + case .late: + targetColors = [latePurple.cgColor, latePink.cgColor] + } + + if CACurrentMediaTime() - self.initialTimestamp > 0.1 { + self.foregroundGradientLayer.colors = targetColors + self.foregroundGradientLayer.animate(from: initialColors as AnyObject, to: targetColors as AnyObject, keyPath: "colors", timingFunction: CAMediaTimingFunctionName.linear.rawValue, duration: 0.3) + } else { + CATransaction.begin() + CATransaction.setDisableActions(true) + self.foregroundGradientLayer.colors = targetColors + CATransaction.commit() } - self.foregroundGradientLayer.colors = targetColors - self.foregroundGradientLayer.animate(from: initialColors as AnyObject, to: targetColors as AnyObject, keyPath: "colors", timingFunction: CAMediaTimingFunctionName.linear.rawValue, duration: 0.3) } private let hierarchyTrackingNode: HierarchyTrackingNode @@ -177,6 +203,8 @@ public class CallStatusBarNodeImpl: CallStatusBarNode { private var currentCallState: PresentationCallState? private var currentGroupCallState: PresentationGroupCallSummaryState? private var currentIsMuted = true + private var currentCantSpeak = false + private var currentScheduleTimestamp: Int32? private var currentMembers: PresentationGroupCallMembers? private var currentIsConnected = true @@ -279,16 +307,25 @@ public class CallStatusBarNodeImpl: CallStatusBarNode { strongSelf.currentMembers = members var isMuted = isMuted + var cantSpeak = false if let state = state, let muteState = state.callState.muteState { if !muteState.canUnmute { isMuted = true + cantSpeak = true } } + if state?.callState.scheduleTimestamp != nil { + cantSpeak = true + } strongSelf.currentIsMuted = isMuted + strongSelf.currentCantSpeak = cantSpeak + strongSelf.currentScheduleTimestamp = state?.callState.scheduleTimestamp let currentIsConnected: Bool if let state = state, case .connected = state.callState.networkState { currentIsConnected = true + } else if state?.callState.scheduleTimestamp != nil { + currentIsConnected = true } else { currentIsConnected = false } @@ -316,10 +353,11 @@ public class CallStatusBarNodeImpl: CallStatusBarNode { var title: String = "" var speakerSubtitle: String = "" - let textFont = Font.regular(13.0) + let textFont = Font.with(size: 13.0, design: .regular, weight: .regular, traits: [.monospacedNumbers]) let textColor = UIColor.white var segments: [AnimatedCountLabelNode.Segment] = [] var displaySpeakerSubtitle = false + var isLate = false if let presentationData = self.presentationData { if let voiceChatTitle = self.currentGroupCallState?.info?.title, !voiceChatTitle.isEmpty { @@ -350,7 +388,23 @@ public class CallStatusBarNodeImpl: CallStatusBarNode { } displaySpeakerSubtitle = speakerSubtitle != title && !speakerSubtitle.isEmpty - if let membersCount = membersCount { + var requiresTimer = false + if let scheduleTime = self.currentGroupCallState?.info?.scheduleTimestamp { + requiresTimer = true + + let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) + let elapsedTime = scheduleTime - currentTime + let timerText: String + if elapsedTime >= 86400 { + timerText = presentationData.strings.VoiceChat_StatusStartsIn(scheduledTimeIntervalString(strings: presentationData.strings, value: elapsedTime)).0 + } else if elapsedTime < 0 { + isLate = true + timerText = presentationData.strings.VoiceChat_StatusLateBy(textForTimeout(value: abs(elapsedTime))).0 + } else { + timerText = presentationData.strings.VoiceChat_StatusStartsIn(textForTimeout(value: elapsedTime)).0 + } + segments.append(.text(0, NSAttributedString(string: timerText, font: textFont, textColor: textColor))) + } else if let membersCount = membersCount { var membersPart = presentationData.strings.VoiceChat_Status_Members(membersCount) if membersPart.contains("[") && membersPart.contains("]") { if let startIndex = membersPart.firstIndex(of: "["), let endIndex = membersPart.firstIndex(of: "]") { @@ -402,6 +456,19 @@ public class CallStatusBarNodeImpl: CallStatusBarNode { } self.backgroundNode.connectingColor = color + + if requiresTimer { + if self.currentCallTimer == nil { + let timer = SwiftSignalKit.Timer(timeout: 0.5, repeat: true, completion: { [weak self] in + self?.update() + }, queue: Queue.mainQueue()) + timer.start() + self.currentCallTimer = timer + } + } else if let currentCallTimer = self.currentCallTimer { + self.currentCallTimer = nil + currentCallTimer.invalidate() + } } if self.subtitleNode.segments != segments && !displaySpeakerSubtitle { @@ -439,7 +506,19 @@ public class CallStatusBarNodeImpl: CallStatusBarNode { self.speakerNode.frame = CGRect(origin: CGPoint(x: horizontalOrigin + titleSize.width + spacing, y: verticalOrigin + floor((contentHeight - speakerSize.height) / 2.0)), size: speakerSize) } - self.backgroundNode.speaking = self.currentIsConnected ? !self.currentIsMuted : nil + let state: CallStatusBarBackgroundNode.State + if self.currentIsConnected { + if self.currentCantSpeak { + state = isLate ? .late : .cantSpeak + } else if self.currentIsMuted { + state = .active + } else { + state = .speaking + } + } else { + state = .connecting + } + self.backgroundNode.state = state self.backgroundNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.height + 18.0)) } } @@ -601,8 +680,8 @@ final class CurveView: UIView { } CATransaction.begin() CATransaction.setDisableActions(true) - let lv = minOffset + (maxOffset - minOffset) * level - shapeLayer.transform = CATransform3DMakeTranslation(0.0, lv * 16.0, 0.0) + let lv = self.minOffset + (self.maxOffset - self.minOffset) * self.level + self.shapeLayer.transform = CATransform3DMakeTranslation(0.0, lv * 16.0, 0.0) CATransaction.commit() } } @@ -616,39 +695,16 @@ final class CurveView: UIView { return layer }() - private var transition: CGFloat = 0 { - didSet { - guard let currentPoints = currentPoints else { return } - - shapeLayer.path = UIBezierPath.smoothCurve(through: currentPoints, length: bounds.width, smoothness: smoothness, curve: true).cgPath - } - } override var frame: CGRect { didSet { if self.frame.size != oldValue.size { - self.fromPoints = nil - self.toPoints = nil + self.shapeLayer.path = nil self.animateToNewShape() } } } - private var fromPoints: [CGPoint]? - private var toPoints: [CGPoint]? - - private var currentPoints: [CGPoint]? { - guard let fromPoints = fromPoints, let toPoints = toPoints else { return nil } - - return fromPoints.enumerated().map { offset, fromPoint in - let toPoint = toPoints[offset] - return CGPoint( - x: fromPoint.x + (toPoint.x - fromPoint.x) * transition, - y: fromPoint.y + (toPoint.y - fromPoint.y) * transition - ) - } - } - init( pointsCount: Int, minRandomness: CGFloat, @@ -670,7 +726,7 @@ final class CurveView: UIView { super.init(frame: .zero) - layer.addSublayer(shapeLayer) + self.layer.addSublayer(self.shapeLayer) } required init?(coder: NSCoder) { @@ -678,7 +734,7 @@ final class CurveView: UIView { } func setColor(_ color: UIColor) { - shapeLayer.fillColor = color.cgColor + self.shapeLayer.fillColor = color.cgColor } func updateSpeedLevel(to newSpeedLevel: CGFloat) { @@ -690,57 +746,40 @@ final class CurveView: UIView { } func startAnimating() { - animateToNewShape() + self.animateToNewShape() } func stopAnimating() { - fromPoints = currentPoints - toPoints = nil - pop_removeAnimation(forKey: "curve") + self.shapeLayer.removeAnimation(forKey: "path") } private func animateToNewShape() { - if pop_animation(forKey: "curve") != nil { - fromPoints = currentPoints - toPoints = nil - pop_removeAnimation(forKey: "curve") + if self.shapeLayer.path == nil { + let points = self.generateNextCurve(for: self.bounds.size) + self.shapeLayer.path = UIBezierPath.smoothCurve(through: points, length: bounds.width, smoothness: self.smoothness, curve: true).cgPath } - if fromPoints == nil { - fromPoints = generateNextCurve(for: bounds.size) - } - if toPoints == nil { - toPoints = generateNextCurve(for: bounds.size) - } + let nextPoints = self.generateNextCurve(for: self.bounds.size) + let nextPath = UIBezierPath.smoothCurve(through: nextPoints, length: bounds.width, smoothness: self.smoothness, curve: true).cgPath - let animation = POPBasicAnimation() - animation.property = POPAnimatableProperty.property(withName: "curve.transition", initializer: { property in - property?.readBlock = { curveView, values in - guard let curveView = curveView as? CurveView, let values = values else { return } - - values.pointee = curveView.transition - } - property?.writeBlock = { curveView, values in - guard let curveView = curveView as? CurveView, let values = values else { return } - - curveView.transition = values.pointee - } - }) as? POPAnimatableProperty - animation.completionBlock = { [weak self] animation, finished in + let animation = CABasicAnimation(keyPath: "path") + let previousPath = self.shapeLayer.path + self.shapeLayer.path = nextPath + animation.duration = CFTimeInterval(1 / (self.minSpeed + (self.maxSpeed - self.minSpeed) * self.speedLevel)) + animation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut) + animation.fromValue = previousPath + animation.toValue = nextPath + animation.isRemovedOnCompletion = false + animation.fillMode = .forwards + animation.completion = { [weak self] finished in if finished { - self?.fromPoints = self?.currentPoints - self?.toPoints = nil self?.animateToNewShape() } } - animation.duration = CFTimeInterval(1 / (minSpeed + (maxSpeed - minSpeed) * speedLevel)) - animation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut) - animation.fromValue = 0 - animation.toValue = 1 - pop_add(animation, forKey: "curve") + self.shapeLayer.add(animation, forKey: "path") - lastSpeedLevel = speedLevel - speedLevel = 0 + self.lastSpeedLevel = self.speedLevel + self.speedLevel = 0 } private func generateNextCurve(for size: CGSize) -> [CGPoint] { @@ -792,8 +831,8 @@ final class CurveView: UIView { CATransaction.begin() CATransaction.setDisableActions(true) - shapeLayer.position = CGPoint(x: self.bounds.width / 2.0, y: self.bounds.height / 2.0) - shapeLayer.bounds = self.bounds + self.shapeLayer.position = CGPoint(x: self.bounds.width / 2.0, y: self.bounds.height / 2.0) + self.shapeLayer.bounds = self.bounds CATransaction.commit() } } diff --git a/submodules/TelegramCallsUI/Sources/CallSuggestTabController.swift b/submodules/TelegramCallsUI/Sources/CallSuggestTabController.swift index e3d40c7883..1026980ca3 100644 --- a/submodules/TelegramCallsUI/Sources/CallSuggestTabController.swift +++ b/submodules/TelegramCallsUI/Sources/CallSuggestTabController.swift @@ -11,16 +11,13 @@ import TelegramUIPreferences import AccountContext import AppBundle -private func generateIconImage(theme: AlertControllerTheme) -> UIImage? { - return UIImage(bundleImageName: "Call List/AlertIcon") -} - private final class CallSuggestTabAlertContentNode: AlertContentNode { private let strings: PresentationStrings private let titleNode: ASTextNode private let textNode: ASTextNode private let iconNode: ASImageNode + private let accentIconNode: ASImageNode private let actionNodesSeparator: ASDisplayNode private let actionNodes: [TextAlertContentActionNode] @@ -42,6 +39,12 @@ private final class CallSuggestTabAlertContentNode: AlertContentNode { self.textNode.maximumNumberOfLines = 0 self.iconNode = ASImageNode() + self.iconNode.displaysAsynchronously = false + self.iconNode.displayWithoutProcessing = true + + self.accentIconNode = ASImageNode() + self.accentIconNode.displaysAsynchronously = false + self.accentIconNode.displayWithoutProcessing = true self.actionNodesSeparator = ASDisplayNode() self.actionNodesSeparator.isLayerBacked = true @@ -65,6 +68,7 @@ private final class CallSuggestTabAlertContentNode: AlertContentNode { self.addSubnode(self.titleNode) self.addSubnode(self.textNode) self.addSubnode(self.iconNode) + self.addSubnode(self.accentIconNode) self.addSubnode(self.actionNodesSeparator) @@ -82,7 +86,8 @@ private final class CallSuggestTabAlertContentNode: AlertContentNode { override func updateTheme(_ theme: AlertControllerTheme) { self.titleNode.attributedText = NSAttributedString(string: strings.Calls_CallTabTitle, font: Font.bold(17.0), textColor: theme.primaryColor, paragraphAlignment: .center) self.textNode.attributedText = NSAttributedString(string: strings.Calls_CallTabDescription, font: Font.regular(13.0), textColor: theme.primaryColor, paragraphAlignment: .center) - self.iconNode.image = generateIconImage(theme: theme) + self.iconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Call List/AlertIcon"), color: theme.controlBorderColor) + self.accentIconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Call List/AlertAccentIcon"), color: theme.accentColor) self.actionNodesSeparator.backgroundColor = theme.separatorColor for actionNode in self.actionNodes { @@ -112,7 +117,9 @@ private final class CallSuggestTabAlertContentNode: AlertContentNode { var iconSize = CGSize() if let icon = self.iconNode.image { iconSize = icon.size - transition.updateFrame(node: self.iconNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - iconSize.width) / 2.0), y: origin.y), size: iconSize)) + let iconFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - iconSize.width) / 2.0), y: origin.y), size: iconSize) + transition.updateFrame(node: self.iconNode, frame: iconFrame) + transition.updateFrame(node: self.accentIconNode, frame: iconFrame) origin.y += iconSize.height + 16.0 } diff --git a/submodules/TelegramCallsUI/Sources/GroupCallNavigationAccessoryPanel.swift b/submodules/TelegramCallsUI/Sources/GroupCallNavigationAccessoryPanel.swift index e49dc24222..12e61cca8d 100644 --- a/submodules/TelegramCallsUI/Sources/GroupCallNavigationAccessoryPanel.swift +++ b/submodules/TelegramCallsUI/Sources/GroupCallNavigationAccessoryPanel.swift @@ -7,12 +7,29 @@ import SyncCore import Postbox import TelegramPresentationData import TelegramUIPreferences +import TelegramStringFormatting import AccountContext import AppBundle import SwiftSignalKit import AnimatedAvatarSetNode import AudioBlob +func textForTimeout(value: Int32) -> String { + if value < 3600 { + let minutes = value / 60 + let seconds = value % 60 + let secondsPadding = seconds < 10 ? "0" : "" + return "\(minutes):\(secondsPadding)\(seconds)" + } else { + let hours = value / 3600 + let minutes = (value % 3600) / 60 + let minutesPadding = minutes < 10 ? "0" : "" + let seconds = value % 60 + let secondsPadding = seconds < 10 ? "0" : "" + return "\(hours):\(minutesPadding)\(minutes):\(secondsPadding)\(seconds)" + } +} + private let titleFont = Font.semibold(15.0) private let subtitleFont = Font.regular(13.0) @@ -79,6 +96,7 @@ public final class GroupCallNavigationAccessoryPanel: ASDisplayNode { private let context: AccountContext private var theme: PresentationTheme private var strings: PresentationStrings + private var dateTimeFormat: PresentationDateTimeFormat private let tapAction: () -> Void @@ -102,12 +120,18 @@ public final class GroupCallNavigationAccessoryPanel: ASDisplayNode { private var textIsActive = false private let muteIconNode: ASImageNode + private var isScheduled = false + private var isLate = false + private var currentText: String = "" + private var updateTimer: SwiftSignalKit.Timer? + private let avatarsContext: AnimatedAvatarSetContext private var avatarsContent: AnimatedAvatarSetContext.Content? private let avatarsNode: AnimatedAvatarSetNode private var audioLevelGenerators: [PeerId: FakeAudioLevelGenerator] = [:] private var audioLevelGeneratorTimer: SwiftSignalKit.Timer? - + + private let backgroundNode: ASDisplayNode private let separatorNode: ASDisplayNode private let membersDisposable = MetaDisposable() @@ -125,6 +149,7 @@ public final class GroupCallNavigationAccessoryPanel: ASDisplayNode { self.context = context self.theme = presentationData.theme self.strings = presentationData.strings + self.dateTimeFormat = presentationData.dateTimeFormat self.tapAction = tapAction @@ -135,6 +160,9 @@ public final class GroupCallNavigationAccessoryPanel: ASDisplayNode { self.joinButton = HighlightableButtonNode() self.joinButtonTitleNode = ImmediateTextNode() self.joinButtonBackgroundNode = ASImageNode() + self.joinButtonBackgroundNode.clipsToBounds = true + self.joinButtonBackgroundNode.displaysAsynchronously = false + self.joinButtonBackgroundNode.cornerRadius = 14.0 self.micButton = HighlightTrackingButtonNode() self.micButtonForegroundNode = VoiceChatMicrophoneNode() @@ -147,13 +175,17 @@ public final class GroupCallNavigationAccessoryPanel: ASDisplayNode { self.avatarsContext = AnimatedAvatarSetContext() self.avatarsNode = AnimatedAvatarSetNode() + + self.backgroundNode = ASDisplayNode() self.separatorNode = ASDisplayNode() self.separatorNode.isLayerBacked = true super.init() - + self.addSubnode(self.contentNode) + + self.contentNode.addSubnode(self.backgroundNode) self.tapButton.highligthedChanged = { [weak self] highlighted in if let strongSelf = self { @@ -198,6 +230,7 @@ public final class GroupCallNavigationAccessoryPanel: ASDisplayNode { self.membersDisposable.dispose() self.isMutedDisposable.dispose() self.audioLevelGeneratorTimer?.invalidate() + self.updateTimer?.invalidate() } public override func didLoad() { @@ -250,25 +283,50 @@ public final class GroupCallNavigationAccessoryPanel: ASDisplayNode { public func updatePresentationData(_ presentationData: PresentationData) { self.theme = presentationData.theme self.strings = presentationData.strings - - self.contentNode.backgroundColor = self.theme.rootController.navigationBar.backgroundColor - - self.theme = presentationData.theme - + self.dateTimeFormat = presentationData.dateTimeFormat + self.separatorNode.backgroundColor = presentationData.theme.chat.historyNavigation.strokeColor - self.joinButtonTitleNode.attributedText = NSAttributedString(string: presentationData.strings.VoiceChat_PanelJoin.uppercased(), font: Font.semibold(15.0), textColor: presentationData.theme.chat.inputPanel.actionControlForegroundColor) - self.joinButtonBackgroundNode.image = generateStretchableFilledCircleImage(diameter: 28.0, color: presentationData.theme.chat.inputPanel.actionControlFillColor) - + self.joinButtonTitleNode.attributedText = NSAttributedString(string: self.joinButtonTitleNode.attributedText?.string ?? "", font: Font.with(size: 15.0, design: .round, weight: .semibold, traits: [.monospacedNumbers]), textColor: self.isScheduled ? .white : presentationData.theme.chat.inputPanel.actionControlForegroundColor) self.textNode.attributedText = NSAttributedString(string: self.textNode.attributedText?.string ?? "", font: Font.regular(13.0), textColor: presentationData.theme.chat.inputPanel.secondaryTextColor) self.muteIconNode.image = PresentationResourcesChat.chatTitleMuteIcon(presentationData.theme) + self.updateJoinButton() + if let (size, leftInset, rightInset) = self.validLayout { self.updateLayout(size: size, leftInset: leftInset, rightInset: rightInset, transition: .immediate) } } + private func updateJoinButton() { + if self.isScheduled { + let purple = UIColor(rgb: 0x5d4ed1) + let pink = UIColor(rgb: 0xea436f) + let latePurple = UIColor(rgb: 0xaa56a6) + let latePink = UIColor(rgb: 0xef476f) + let colors: [UIColor] + if self.isLate { + colors = [latePurple, latePink] + } else { + colors = [purple, pink] + } + if self.joinButtonBackgroundNode.image != nil, let snapshotView = self.joinButtonBackgroundNode.view.snapshotContentTree() { + self.joinButtonBackgroundNode.view.superview?.insertSubview(snapshotView, aboveSubview: self.joinButtonBackgroundNode.view) + + snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 1.0, removeOnCompletion: false, completion: { [weak snapshotView] _ in + snapshotView?.removeFromSuperview() + }) + } + + self.joinButtonBackgroundNode.image = generateGradientImage(size: CGSize(width: 100.0, height: 1.0), colors: colors, locations: [0.0, 1.0], direction: .horizontal) + self.joinButtonBackgroundNode.backgroundColor = nil + } else { + self.joinButtonBackgroundNode.image = nil + self.joinButtonBackgroundNode.backgroundColor = self.theme.chat.inputPanel.actionControlFillColor + } + } + private func animateTextChange() { if let snapshotView = self.textNode.view.snapshotContentTree() { let offset: CGFloat = self.textIsActive ? -7.0 : 7.0 @@ -298,6 +356,7 @@ public final class GroupCallNavigationAccessoryPanel: ASDisplayNode { } else { membersText = self.strings.VoiceChat_Panel_Members(Int32(data.participantCount)) } + self.currentText = membersText self.avatarsContent = self.avatarsContext.update(peers: data.topParticipants.map { $0.peer }, animated: false) @@ -321,9 +380,8 @@ public final class GroupCallNavigationAccessoryPanel: ASDisplayNode { } else { membersText = strongSelf.strings.VoiceChat_Panel_Members(Int32(summaryState.participantCount)) } - - strongSelf.textNode.attributedText = NSAttributedString(string: membersText, font: Font.regular(13.0), textColor: strongSelf.theme.chat.inputPanel.secondaryTextColor) - + strongSelf.currentText = membersText + strongSelf.avatarsContent = strongSelf.avatarsContext.update(peers: summaryState.topParticipants.map { $0.peer }, animated: false) if let (size, leftInset, rightInset) = strongSelf.validLayout { @@ -382,7 +440,6 @@ public final class GroupCallNavigationAccessoryPanel: ASDisplayNode { strongSelf.micButton.view.insertSubview(audioLevelView, at: 0) } - let level = min(1.0, max(0.0, CGFloat(value))) strongSelf.audioLevelView?.updateLevel(CGFloat(value) * 2.0) if value > 0.0 { strongSelf.audioLevelView?.startAnimating() @@ -400,9 +457,8 @@ public final class GroupCallNavigationAccessoryPanel: ASDisplayNode { } else { membersText = self.strings.VoiceChat_Panel_Members(Int32(data.participantCount)) } + self.currentText = membersText - self.textNode.attributedText = NSAttributedString(string: membersText, font: Font.regular(13.0), textColor: self.theme.chat.inputPanel.secondaryTextColor) - self.avatarsContent = self.avatarsContext.update(peers: data.topParticipants.map { $0.peer }, animated: false) updateAudioLevels = true @@ -466,6 +522,59 @@ public final class GroupCallNavigationAccessoryPanel: ASDisplayNode { transition.updateFrame(node: self.avatarsNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - avatarsSize.width) / 2.0), y: floor((size.height - avatarsSize.height) / 2.0)), size: avatarsSize)) } + var joinText = self.strings.VoiceChat_PanelJoin + var title = self.strings.VoiceChat_Title + var text = self.currentText + var isScheduled = false + var isLate = false + if let scheduleTime = self.currentData?.info.scheduleTimestamp { + isScheduled = true + if let voiceChatTitle = self.currentData?.info.title { + title = voiceChatTitle + text = humanReadableStringForTimestamp(strings: self.strings, dateTimeFormat: self.dateTimeFormat, timestamp: scheduleTime, alwaysShowTime: true, format: HumanReadableStringFormat(dateFormatString: { self.strings.Conversation_ScheduledVoiceChatStartsOn($0) }, tomorrowFormatString: { self.strings.Conversation_ScheduledVoiceChatStartsTomorrow($0) }, todayFormatString: { self.strings.Conversation_ScheduledVoiceChatStartsToday($0) })).0 + } else { + title = self.strings.Conversation_ScheduledVoiceChat + text = humanReadableStringForTimestamp(strings: self.strings, dateTimeFormat: self.dateTimeFormat, timestamp: scheduleTime, alwaysShowTime: true, format: HumanReadableStringFormat(dateFormatString: { self.strings.Conversation_ScheduledVoiceChatStartsOnShort($0) }, tomorrowFormatString: { self.strings.Conversation_ScheduledVoiceChatStartsTomorrowShort($0) }, todayFormatString: { self.strings.Conversation_ScheduledVoiceChatStartsTodayShort($0) })).0 + } + + let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) + let elapsedTime = scheduleTime - currentTime + if elapsedTime >= 86400 { + joinText = scheduledTimeIntervalString(strings: strings, value: elapsedTime) + } else if elapsedTime < 0 { + joinText = "-\(textForTimeout(value: abs(elapsedTime)))" + isLate = true + } else { + joinText = textForTimeout(value: elapsedTime) + } + + if self.updateTimer == nil { + let timer = SwiftSignalKit.Timer(timeout: 0.5, repeat: true, completion: { [weak self] in + if let strongSelf = self, let (size, leftInset, rightInset) = strongSelf.validLayout { + strongSelf.updateLayout(size: size, leftInset: leftInset, rightInset: rightInset, transition: .immediate) + } + }, queue: Queue.mainQueue()) + self.updateTimer = timer + timer.start() + } + } else { + if let timer = self.updateTimer { + self.updateTimer = nil + timer.invalidate() + } + if let voiceChatTitle = self.currentData?.info.title, voiceChatTitle.count < 15 { + title = voiceChatTitle + } + } + + if self.isScheduled != isScheduled || self.isLate != isLate { + self.isScheduled = isScheduled + self.isLate = isLate + self.updateJoinButton() + } + + self.joinButtonTitleNode.attributedText = NSAttributedString(string: joinText.uppercased(), font: Font.with(size: 15.0, design: .round, weight: .semibold, traits: [.monospacedNumbers]), textColor: isScheduled ? .white : self.theme.chat.inputPanel.actionControlForegroundColor) + let joinButtonTitleSize = self.joinButtonTitleNode.updateLayout(CGSize(width: 150.0, height: .greatestFiniteMagnitude)) let joinButtonSize = CGSize(width: joinButtonTitleSize.width + 20.0, height: 28.0) let joinButtonFrame = CGRect(origin: CGPoint(x: size.width - rightInset - 7.0 - joinButtonSize.width, y: floor((panelHeight - joinButtonSize.height) / 2.0)), size: joinButtonSize) @@ -500,15 +609,17 @@ public final class GroupCallNavigationAccessoryPanel: ASDisplayNode { self.micButtonBackgroundNode.image = updatedImage } } - - var title = self.strings.VoiceChat_Title - if let voiceChatTitle = self.currentData?.info.title, voiceChatTitle.count < 15 { - title = voiceChatTitle - } - + self.titleNode.attributedText = NSAttributedString(string: title, font: Font.semibold(15.0), textColor: self.theme.chat.inputPanel.primaryTextColor) - let titleSize = self.titleNode.updateLayout(CGSize(width: size.width / 2.0 - 56.0, height: .greatestFiniteMagnitude)) + self.textNode.attributedText = NSAttributedString(string: text, font: Font.regular(13.0), textColor: self.theme.chat.inputPanel.secondaryTextColor) + + var constrainedWidth = size.width / 2.0 - 56.0 + if isScheduled { + constrainedWidth = size.width - 100.0 + } + + let titleSize = self.titleNode.updateLayout(CGSize(width: constrainedWidth, height: .greatestFiniteMagnitude)) let textSize = self.textNode.updateLayout(CGSize(width: size.width, height: .greatestFiniteMagnitude)) let titleFrame = CGRect(origin: CGPoint(x: leftInset + 16.0, y: 9.0), size: titleSize) @@ -522,7 +633,8 @@ public final class GroupCallNavigationAccessoryPanel: ASDisplayNode { self.joinButton.isHidden = self.currentData?.groupCall != nil self.micButton.isHidden = self.currentData?.groupCall == nil - transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: panelHeight - UIScreenPixel), size: CGSize(width: size.width, height: UIScreenPixel))) + transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: UIScreenPixel))) + transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: panelHeight))) } public func animateIn(_ transition: ContainedViewLayoutTransition) { @@ -531,6 +643,12 @@ public final class GroupCallNavigationAccessoryPanel: ASDisplayNode { transition.animatePosition(node: self.contentNode, from: CGPoint(x: contentPosition.x, y: contentPosition.y - 50.0), completion: { [weak self] _ in self?.clipsToBounds = false }) + + guard let (size, _, _) = self.validLayout else { + return + } + + transition.animatePositionAdditive(node: self.separatorNode, offset: CGPoint(x: 0.0, y: size.height)) } public func animateOut(_ transition: ContainedViewLayoutTransition, completion: @escaping () -> Void) { @@ -540,6 +658,12 @@ public final class GroupCallNavigationAccessoryPanel: ASDisplayNode { self?.clipsToBounds = false completion() }) + + guard let (size, _, _) = self.validLayout else { + return + } + + transition.updatePosition(node: self.separatorNode, position: self.separatorNode.position.offsetBy(dx: 0.0, dy: size.height)) } func rightButtonSnapshotViews() -> (background: UIView, foreground: UIView)? { diff --git a/submodules/TelegramCallsUI/Sources/GroupVideoNode.swift b/submodules/TelegramCallsUI/Sources/GroupVideoNode.swift new file mode 100644 index 0000000000..3167b142ed --- /dev/null +++ b/submodules/TelegramCallsUI/Sources/GroupVideoNode.swift @@ -0,0 +1,374 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display +import SwiftSignalKit +import AccountContext +import ContextUI + +final class GroupVideoNode: ASDisplayNode { + static let useBlurTransparency: Bool = !UIAccessibility.isReduceTransparencyEnabled + + enum Position { + case tile + case list + case mainstage + } + + enum LayoutMode { + case fillOrFitToSquare + case fillHorizontal + case fillVertical + case fit + } + + let sourceContainerNode: PinchSourceContainerNode + private let containerNode: ASDisplayNode + private let videoViewContainer: UIView + private let videoView: VideoRenderingView + + private let backdropVideoViewContainer: UIView + private let backdropVideoView: VideoRenderingView? + private var backdropEffectView: UIVisualEffectView? + + private var effectView: UIVisualEffectView? + private var isBlurred: Bool = false + + private var isEnabled: Bool = false + private var isBlurEnabled: Bool = false + + private var validLayout: (CGSize, LayoutMode)? + + var tapped: (() -> Void)? + + private let readyPromise = ValuePromise(false) + var ready: Signal { + return self.readyPromise.get() + } + + public var isMainstageExclusive = false + + init(videoView: VideoRenderingView, backdropVideoView: VideoRenderingView?) { + self.sourceContainerNode = PinchSourceContainerNode() + self.containerNode = ASDisplayNode() + self.videoViewContainer = UIView() + self.videoViewContainer.isUserInteractionEnabled = false + self.videoView = videoView + + self.backdropVideoViewContainer = UIView() + self.backdropVideoViewContainer.isUserInteractionEnabled = false + self.backdropVideoView = backdropVideoView + + super.init() + + if let backdropVideoView = backdropVideoView { + self.backdropVideoViewContainer.addSubview(backdropVideoView) + self.view.addSubview(self.backdropVideoViewContainer) + + let effect: UIVisualEffect + if #available(iOS 13.0, *) { + effect = UIBlurEffect(style: .systemThinMaterialDark) + } else { + effect = UIBlurEffect(style: .dark) + } + //let backdropEffectView = UIVisualEffectView(effect: effect) + //self.view.addSubview(backdropEffectView) + //self.backdropEffectView = backdropEffectView + } + + self.videoViewContainer.addSubview(self.videoView) + self.addSubnode(self.sourceContainerNode) + self.containerNode.view.addSubview(self.videoViewContainer) + self.sourceContainerNode.contentNode.addSubnode(self.containerNode) + + self.clipsToBounds = true + + videoView.setOnFirstFrameReceived({ [weak self] _ in + Queue.mainQueue().async { + guard let strongSelf = self else { + return + } + strongSelf.readyPromise.set(true) + if let (size, layoutMode) = strongSelf.validLayout { + strongSelf.updateLayout(size: size, layoutMode: layoutMode, transition: .immediate) + } + } + }) + + videoView.setOnOrientationUpdated({ [weak self] _, _ in + Queue.mainQueue().async { + guard let strongSelf = self else { + return + } + if let (size, layoutMode) = strongSelf.validLayout { + strongSelf.updateLayout(size: size, layoutMode: layoutMode, transition: .immediate) + } + } + }) + + self.containerNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) + } + + func updateIsEnabled(_ isEnabled: Bool) { + self.isEnabled = isEnabled + + self.videoView.updateIsEnabled(isEnabled) + self.backdropVideoView?.updateIsEnabled(isEnabled && self.isBlurEnabled) + } + + func updateIsBlurred(isBlurred: Bool, light: Bool = false, animated: Bool = true) { + if self.isBlurred == isBlurred { + return + } + self.isBlurred = isBlurred + + if isBlurred { + if self.effectView == nil { + let effectView = UIVisualEffectView() + self.effectView = effectView + effectView.frame = self.bounds + self.view.addSubview(effectView) + } + if animated { + UIView.animate(withDuration: 0.3, animations: { + self.effectView?.effect = UIBlurEffect(style: light ? .light : .dark) + }) + } else { + self.effectView?.effect = UIBlurEffect(style: light ? .light : .dark) + } + } else if let effectView = self.effectView { + self.effectView = nil + UIView.animate(withDuration: 0.3, animations: { + effectView.effect = nil + }, completion: { [weak effectView] _ in + effectView?.removeFromSuperview() + }) + } + } + + func flip(withBackground: Bool) { + if withBackground { + self.backgroundColor = .black + } + var snapshotView: UIView? + if let snapshot = self.videoView.snapshotView(afterScreenUpdates: false) { + snapshotView = snapshot + snapshot.transform = self.videoView.transform + snapshot.frame = self.videoView.frame + self.videoView.superview?.insertSubview(snapshot, aboveSubview: self.videoView) + } + UIView.transition(with: withBackground ? self.videoViewContainer : self.view, duration: 0.4, options: [.transitionFlipFromLeft, .curveEaseOut], animations: { + UIView.performWithoutAnimation { + self.updateIsBlurred(isBlurred: true, light: false, animated: false) + } + }) { finished in + self.backgroundColor = nil + if let snapshotView = snapshotView { + Queue.mainQueue().after(0.3) { + snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak snapshotView] _ in + snapshotView?.removeFromSuperview() + }) + self.updateIsBlurred(isBlurred: false) + } + } else { + Queue.mainQueue().after(0.4) { + self.updateIsBlurred(isBlurred: false) + } + } + } + } + + @objc private func tapGesture(_ recognizer: UITapGestureRecognizer) { + if case .ended = recognizer.state { + self.tapped?() + } + } + + var aspectRatio: CGFloat { + let orientation = self.videoView.getOrientation() + var aspect = self.videoView.getAspect() + if aspect <= 0.01 { + aspect = 3.0 / 4.0 + } + let rotatedAspect: CGFloat + switch orientation { + case .rotation0: + rotatedAspect = 1.0 / aspect + case .rotation90: + rotatedAspect = aspect + case .rotation180: + rotatedAspect = 1.0 / aspect + case .rotation270: + rotatedAspect = aspect + } + return rotatedAspect + } + + func updateLayout(size: CGSize, layoutMode: LayoutMode, transition: ContainedViewLayoutTransition) { + self.validLayout = (size, layoutMode) + let bounds = CGRect(origin: CGPoint(), size: size) + self.sourceContainerNode.update(size: size, transition: .immediate) + transition.updateFrameAsPositionAndBounds(node: self.sourceContainerNode, frame: bounds) + transition.updateFrameAsPositionAndBounds(node: self.containerNode, frame: bounds) + transition.updateFrameAsPositionAndBounds(layer: self.videoViewContainer.layer, frame: bounds) + transition.updateFrameAsPositionAndBounds(layer: self.backdropVideoViewContainer.layer, frame: bounds) + + let orientation = self.videoView.getOrientation() + var aspect = self.videoView.getAspect() + if aspect <= 0.01 { + aspect = 3.0 / 4.0 + } + + let rotatedAspect: CGFloat + let angle: CGFloat + let switchOrientation: Bool + switch orientation { + case .rotation0: + angle = 0.0 + rotatedAspect = 1 / aspect + switchOrientation = false + case .rotation90: + angle = CGFloat.pi / 2.0 + rotatedAspect = aspect + switchOrientation = true + case .rotation180: + angle = CGFloat.pi + rotatedAspect = 1 / aspect + switchOrientation = false + case .rotation270: + angle = CGFloat.pi * 3.0 / 2.0 + rotatedAspect = aspect + switchOrientation = true + } + + var rotatedVideoSize = CGSize(width: 100.0, height: rotatedAspect * 100.0) + let videoSize = rotatedVideoSize + + var containerSize = size + if switchOrientation { + rotatedVideoSize = CGSize(width: rotatedVideoSize.height, height: rotatedVideoSize.width) + containerSize = CGSize(width: containerSize.height, height: containerSize.width) + } + + let fittedSize = rotatedVideoSize.aspectFitted(containerSize) + let filledSize = rotatedVideoSize.aspectFilled(containerSize) + var squareSide = size.height + if !size.height.isZero && size.width / size.height < 1.2 { + squareSide = max(size.width, size.height) + } + let filledToSquareSize = rotatedVideoSize.aspectFilled(CGSize(width: squareSide, height: squareSide)) + + switch layoutMode { + case .fit: + rotatedVideoSize = fittedSize + case .fillOrFitToSquare: + rotatedVideoSize = filledToSquareSize + case .fillHorizontal: + if videoSize.width > videoSize.height { + rotatedVideoSize = filledSize + } else { + rotatedVideoSize = fittedSize + } + case .fillVertical: + if videoSize.width < videoSize.height { + rotatedVideoSize = filledSize + } else { + rotatedVideoSize = fittedSize + } + } + + var rotatedVideoFrame = CGRect(origin: CGPoint(x: floor((size.width - rotatedVideoSize.width) / 2.0), y: floor((size.height - rotatedVideoSize.height) / 2.0)), size: rotatedVideoSize) + rotatedVideoFrame.origin.x = floor(rotatedVideoFrame.origin.x) + rotatedVideoFrame.origin.y = floor(rotatedVideoFrame.origin.y) + rotatedVideoFrame.size.width = ceil(rotatedVideoFrame.size.width) + rotatedVideoFrame.size.height = ceil(rotatedVideoFrame.size.height) + + self.videoView.alpha = 0.995 + + let normalizedVideoSize = rotatedVideoFrame.size.aspectFilled(CGSize(width: 1080.0, height: 1080.0)) + transition.updatePosition(layer: self.videoView.layer, position: rotatedVideoFrame.center) + transition.updateBounds(layer: self.videoView.layer, bounds: CGRect(origin: CGPoint(), size: normalizedVideoSize)) + + let transformScale: CGFloat = rotatedVideoFrame.width / normalizedVideoSize.width + transition.updateTransformScale(layer: self.videoViewContainer.layer, scale: transformScale) + + if let backdropVideoView = self.backdropVideoView { + backdropVideoView.alpha = 0.995 + + let topFrame = rotatedVideoFrame + + rotatedVideoSize = filledSize + var rotatedVideoFrame = CGRect(origin: CGPoint(x: floor((size.width - rotatedVideoSize.width) / 2.0), y: floor((size.height - rotatedVideoSize.height) / 2.0)), size: rotatedVideoSize) + rotatedVideoFrame.origin.x = floor(rotatedVideoFrame.origin.x) + rotatedVideoFrame.origin.y = floor(rotatedVideoFrame.origin.y) + rotatedVideoFrame.size.width = ceil(rotatedVideoFrame.size.width) + rotatedVideoFrame.size.height = ceil(rotatedVideoFrame.size.height) + + self.isBlurEnabled = !topFrame.contains(rotatedVideoFrame) + + let normalizedVideoSize = rotatedVideoFrame.size.aspectFilled(CGSize(width: 1080.0, height: 1080.0)) + + self.backdropVideoView?.updateIsEnabled(self.isEnabled && self.isBlurEnabled) + + if self.isBlurEnabled { + self.backdropVideoView?.isHidden = false + self.backdropEffectView?.isHidden = false + } + transition.updatePosition(layer: backdropVideoView.layer, position: rotatedVideoFrame.center, force: true, completion: { [weak self] value in + guard let strongSelf = self, value else { + return + } + if !strongSelf.isBlurEnabled { + strongSelf.backdropVideoView?.updateIsEnabled(false) + strongSelf.backdropVideoView?.isHidden = true + strongSelf.backdropEffectView?.isHidden = false + } + }) + transition.updateBounds(layer: backdropVideoView.layer, bounds: CGRect(origin: CGPoint(), size: normalizedVideoSize)) + + let transformScale: CGFloat = rotatedVideoFrame.width / normalizedVideoSize.width + + transition.updateTransformScale(layer: self.backdropVideoViewContainer.layer, scale: transformScale) + + let transition: ContainedViewLayoutTransition = .immediate + transition.updateTransformRotation(view: backdropVideoView, angle: angle) + } + + if let backdropEffectView = self.backdropEffectView { + let maxSide = max(bounds.width, bounds.height) + 32.0 + let squareBounds = CGRect(x: (bounds.width - maxSide) / 2.0, y: (bounds.height - maxSide) / 2.0, width: maxSide, height: maxSide) + + if case let .animated(duration, .spring) = transition { + UIView.animate(withDuration: duration, delay: 0.0, usingSpringWithDamping: 500.0, initialSpringVelocity: 0.0, options: .layoutSubviews, animations: { + backdropEffectView.frame = squareBounds + }) + } else { + transition.animateView { + backdropEffectView.frame = squareBounds + } + } + } + + if let effectView = self.effectView { + if case let .animated(duration, .spring) = transition { + UIView.animate(withDuration: duration, delay: 0.0, usingSpringWithDamping: 500.0, initialSpringVelocity: 0.0, options: .layoutSubviews, animations: { + effectView.frame = bounds + }) + } else { + transition.animateView { + effectView.frame = bounds + } + } + } + + let transition: ContainedViewLayoutTransition = .immediate + transition.updateTransformRotation(view: self.videoView, angle: angle) + } + + var snapshotView: UIView? + func storeSnapshot() { + if self.frame.size.width == 180.0 { + self.snapshotView = self.view.snapshotView(afterScreenUpdates: false) + } + } +} diff --git a/submodules/TelegramCallsUI/Sources/MetalVideoRenderingView.swift b/submodules/TelegramCallsUI/Sources/MetalVideoRenderingView.swift new file mode 100644 index 0000000000..54545769af --- /dev/null +++ b/submodules/TelegramCallsUI/Sources/MetalVideoRenderingView.swift @@ -0,0 +1,657 @@ +#if targetEnvironment(simulator) +#else + +import Foundation +import UIKit +import AsyncDisplayKit +import Display +import SwiftSignalKit +import AccountContext +import TelegramVoip +import AVFoundation +import Metal +import MetalPerformanceShaders + +private func alignUp(size: Int, align: Int) -> Int { + precondition(((align - 1) & align) == 0, "Align must be a power of two") + + let alignmentMask = align - 1 + return (size + alignmentMask) & ~alignmentMask +} + +private func getCubeVertexData( + cropX: Int, + cropY: Int, + cropWidth: Int, + cropHeight: Int, + frameWidth: Int, + frameHeight: Int, + rotation: Int, + buffer: UnsafeMutablePointer +) { + let cropLeft = Float(cropX) / Float(frameWidth) + let cropRight = Float(cropX + cropWidth) / Float(frameWidth) + let cropTop = Float(cropY) / Float(frameHeight) + let cropBottom = Float(cropY + cropHeight) / Float(frameHeight) + + switch rotation { + default: + var values: [Float] = [ + -1.0, -1.0, cropLeft, cropBottom, + 1.0, -1.0, cropRight, cropBottom, + -1.0, 1.0, cropLeft, cropTop, + 1.0, 1.0, cropRight, cropTop + ] + memcpy(buffer, &values, values.count * MemoryLayout.size(ofValue: values[0])); + } +} + +@available(iOS 13.0, *) +private protocol FrameBufferRenderingState { + var frameSize: CGSize? { get } + + func encode(renderingContext: MetalVideoRenderingContext, vertexBuffer: MTLBuffer, renderEncoder: MTLRenderCommandEncoder) -> Bool +} + +@available(iOS 13.0, *) +private final class BlitRenderingState { + static func encode(renderingContext: MetalVideoRenderingContext, texture: MTLTexture, vertexBuffer: MTLBuffer, renderEncoder: MTLRenderCommandEncoder) -> Bool { + renderEncoder.setRenderPipelineState(renderingContext.blitPipelineState) + + renderEncoder.setVertexBuffer(vertexBuffer, offset: 0, index: 0) + + renderEncoder.setFragmentTexture(texture, index: 0) + + renderEncoder.drawPrimitives(type: .triangleStrip, vertexStart: 0, vertexCount: 4, instanceCount: 1) + + return true + } +} + +@available(iOS 13.0, *) +private final class NV12FrameBufferRenderingState: FrameBufferRenderingState { + private var yTexture: MTLTexture? + private var uvTexture: MTLTexture? + + var frameSize: CGSize? { + if let yTexture = self.yTexture { + return CGSize(width: yTexture.width, height: yTexture.height) + } else { + return nil + } + } + + func updateTextureBuffers(renderingContext: MetalVideoRenderingContext, frameBuffer: OngoingGroupCallContext.VideoFrameData.NativeBuffer) { + let pixelBuffer = frameBuffer.pixelBuffer + + var lumaTexture: MTLTexture? + var chromaTexture: MTLTexture? + var outTexture: CVMetalTexture? + + let lumaWidth = CVPixelBufferGetWidthOfPlane(pixelBuffer, 0) + let lumaHeight = CVPixelBufferGetHeightOfPlane(pixelBuffer, 0) + + var indexPlane = 0 + var result = CVMetalTextureCacheCreateTextureFromImage(kCFAllocatorDefault, renderingContext.textureCache, pixelBuffer, nil, .r8Unorm, lumaWidth, lumaHeight, indexPlane, &outTexture) + if result == kCVReturnSuccess, let outTexture = outTexture { + lumaTexture = CVMetalTextureGetTexture(outTexture) + } + outTexture = nil + + indexPlane = 1 + result = CVMetalTextureCacheCreateTextureFromImage(kCFAllocatorDefault, renderingContext.textureCache, pixelBuffer, nil, .rg8Unorm, lumaWidth / 2, lumaHeight / 2, indexPlane, &outTexture) + if result == kCVReturnSuccess, let outTexture = outTexture { + chromaTexture = CVMetalTextureGetTexture(outTexture) + } + outTexture = nil + + if let lumaTexture = lumaTexture, let chromaTexture = chromaTexture { + self.yTexture = lumaTexture + self.uvTexture = chromaTexture + } else { + self.yTexture = nil + self.uvTexture = nil + } + } + + func encode(renderingContext: MetalVideoRenderingContext, vertexBuffer: MTLBuffer, renderEncoder: MTLRenderCommandEncoder) -> Bool { + guard let yTexture = self.yTexture, let uvTexture = self.uvTexture else { + return false + } + + renderEncoder.setRenderPipelineState(renderingContext.nv12PipelineState) + + renderEncoder.setVertexBuffer(vertexBuffer, offset: 0, index: 0) + + renderEncoder.setFragmentTexture(yTexture, index: 0) + renderEncoder.setFragmentTexture(uvTexture, index: 1) + + renderEncoder.drawPrimitives(type: .triangleStrip, vertexStart: 0, vertexCount: 4, instanceCount: 1) + + return true + } +} + +@available(iOS 13.0, *) +private final class I420FrameBufferRenderingState: FrameBufferRenderingState { + private var yTexture: MTLTexture? + private var uTexture: MTLTexture? + private var vTexture: MTLTexture? + + private var lumaTextureDescriptorSize: CGSize? + private var lumaTextureDescriptor: MTLTextureDescriptor? + private var chromaTextureDescriptor: MTLTextureDescriptor? + + var frameSize: CGSize? { + if let yTexture = self.yTexture { + return CGSize(width: yTexture.width, height: yTexture.height) + } else { + return nil + } + } + + func updateTextureBuffers(renderingContext: MetalVideoRenderingContext, frameBuffer: OngoingGroupCallContext.VideoFrameData.I420Buffer) { + let lumaSize = CGSize(width: frameBuffer.width, height: frameBuffer.height) + + if lumaSize != lumaTextureDescriptorSize || lumaTextureDescriptor == nil || chromaTextureDescriptor == nil { + self.lumaTextureDescriptorSize = lumaSize + + let lumaTextureDescriptor = MTLTextureDescriptor.texture2DDescriptor(pixelFormat: .r8Unorm, width: frameBuffer.width, height: frameBuffer.height, mipmapped: false) + lumaTextureDescriptor.usage = .shaderRead + self.lumaTextureDescriptor = lumaTextureDescriptor + + self.yTexture = renderingContext.device.makeTexture(descriptor: lumaTextureDescriptor) + + let chromaTextureDescriptor = MTLTextureDescriptor.texture2DDescriptor(pixelFormat: .r8Unorm, width: frameBuffer.width / 2, height: frameBuffer.height / 2, mipmapped: false) + chromaTextureDescriptor.usage = .shaderRead + self.chromaTextureDescriptor = chromaTextureDescriptor + + self.uTexture = renderingContext.device.makeTexture(descriptor: chromaTextureDescriptor) + self.vTexture = renderingContext.device.makeTexture(descriptor: chromaTextureDescriptor) + } + + guard let yTexture = self.yTexture, let uTexture = self.uTexture, let vTexture = self.vTexture else { + return + } + + frameBuffer.y.withUnsafeBytes { bufferPointer in + if let baseAddress = bufferPointer.baseAddress { + yTexture.replace(region: MTLRegionMake2D(0, 0, yTexture.width, yTexture.height), mipmapLevel: 0, withBytes: baseAddress, bytesPerRow: frameBuffer.strideY) + } + } + + frameBuffer.u.withUnsafeBytes { bufferPointer in + if let baseAddress = bufferPointer.baseAddress { + uTexture.replace(region: MTLRegionMake2D(0, 0, uTexture.width, uTexture.height), mipmapLevel: 0, withBytes: baseAddress, bytesPerRow: frameBuffer.strideU) + } + } + + frameBuffer.v.withUnsafeBytes { bufferPointer in + if let baseAddress = bufferPointer.baseAddress { + vTexture.replace(region: MTLRegionMake2D(0, 0, vTexture.width, vTexture.height), mipmapLevel: 0, withBytes: baseAddress, bytesPerRow: frameBuffer.strideV) + } + } + } + + func encode(renderingContext: MetalVideoRenderingContext, vertexBuffer: MTLBuffer, renderEncoder: MTLRenderCommandEncoder) -> Bool { + guard let yTexture = self.yTexture, let uTexture = self.uTexture, let vTexture = self.vTexture else { + return false + } + + renderEncoder.setRenderPipelineState(renderingContext.i420PipelineState) + + renderEncoder.setVertexBuffer(vertexBuffer, offset: 0, index: 0) + + renderEncoder.setFragmentTexture(yTexture, index: 0) + renderEncoder.setFragmentTexture(uTexture, index: 1) + renderEncoder.setFragmentTexture(vTexture, index: 2) + + renderEncoder.drawPrimitives(type: .triangleStrip, vertexStart: 0, vertexCount: 4, instanceCount: 1) + + return true + } +} + +@available(iOS 13.0, *) +final class MetalVideoRenderingView: UIView, VideoRenderingView { + static override var layerClass: AnyClass { + return CAMetalLayer.self + } + + private var metalLayer: CAMetalLayer { + return self.layer as! CAMetalLayer + } + + private weak var renderingContext: MetalVideoRenderingContext? + private var renderingContextIndex: Int? + + private let blur: Bool + + private let vertexBuffer: MTLBuffer + + private var frameBufferRenderingState: FrameBufferRenderingState? + private var blurInputTexture: MTLTexture? + private var blurOutputTexture: MTLTexture? + + fileprivate private(set) var isEnabled: Bool = false + fileprivate var needsRedraw: Bool = false + fileprivate let numberOfUsedDrawables = Atomic(value: 0) + + private var onFirstFrameReceived: ((Float) -> Void)? + private var onOrientationUpdated: ((PresentationCallVideoView.Orientation, CGFloat) -> Void)? + private var onIsMirroredUpdated: ((Bool) -> Void)? + + private var didReportFirstFrame: Bool = false + private var currentOrientation: PresentationCallVideoView.Orientation = .rotation0 + private var currentAspect: CGFloat = 1.0 + + private var disposable: Disposable? + + init?(renderingContext: MetalVideoRenderingContext, input: Signal, blur: Bool) { + self.renderingContext = renderingContext + self.blur = blur + + let vertexBufferArray = Array(repeating: 0, count: 16) + guard let vertexBuffer = renderingContext.device.makeBuffer(bytes: vertexBufferArray, length: vertexBufferArray.count * MemoryLayout.size(ofValue: vertexBufferArray[0]), options: [.cpuCacheModeWriteCombined]) else { + return nil + } + self.vertexBuffer = vertexBuffer + + super.init(frame: CGRect()) + + self.renderingContextIndex = renderingContext.add(view: self) + + self.metalLayer.device = renderingContext.device + self.metalLayer.pixelFormat = .bgra8Unorm + self.metalLayer.framebufferOnly = true + self.metalLayer.allowsNextDrawableTimeout = true + + self.disposable = input.start(next: { [weak self] videoFrameData in + Queue.mainQueue().async { + self?.addFrame(videoFrameData) + } + }) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.disposable?.dispose() + if let renderingContext = self.renderingContext, let renderingContextIndex = self.renderingContextIndex { + renderingContext.remove(index: renderingContextIndex) + } + } + + private func addFrame(_ videoFrameData: OngoingGroupCallContext.VideoFrameData) { + let aspect = CGFloat(videoFrameData.width) / CGFloat(videoFrameData.height) + var isAspectUpdated = false + if self.currentAspect != aspect { + self.currentAspect = aspect + isAspectUpdated = true + } + + let videoFrameOrientation = PresentationCallVideoView.Orientation(videoFrameData.orientation) + var isOrientationUpdated = false + if self.currentOrientation != videoFrameOrientation { + self.currentOrientation = videoFrameOrientation + isOrientationUpdated = true + } + + if isAspectUpdated || isOrientationUpdated { + self.onOrientationUpdated?(self.currentOrientation, self.currentAspect) + } + + if !self.didReportFirstFrame { + self.didReportFirstFrame = true + self.onFirstFrameReceived?(Float(self.currentAspect)) + } + + if self.isEnabled, let renderingContext = self.renderingContext { + switch videoFrameData.buffer { + case let .native(buffer): + let renderingState: NV12FrameBufferRenderingState + if let current = self.frameBufferRenderingState as? NV12FrameBufferRenderingState { + renderingState = current + } else { + renderingState = NV12FrameBufferRenderingState() + self.frameBufferRenderingState = renderingState + } + renderingState.updateTextureBuffers(renderingContext: renderingContext, frameBuffer: buffer) + self.needsRedraw = true + case let .i420(buffer): + let renderingState: I420FrameBufferRenderingState + if let current = self.frameBufferRenderingState as? I420FrameBufferRenderingState { + renderingState = current + } else { + renderingState = I420FrameBufferRenderingState() + self.frameBufferRenderingState = renderingState + } + renderingState.updateTextureBuffers(renderingContext: renderingContext, frameBuffer: buffer) + self.needsRedraw = true + default: + break + } + } + } + + fileprivate func encode(commandBuffer: MTLCommandBuffer) -> MTLDrawable? { + guard let renderingContext = self.renderingContext else { + return nil + } + if self.numberOfUsedDrawables.with({ $0 }) >= 2 { + return nil + } + guard let frameBufferRenderingState = self.frameBufferRenderingState else { + return nil + } + + guard let frameSize = frameBufferRenderingState.frameSize else { + return nil + } + + let drawableSize: CGSize + if self.blur { + drawableSize = frameSize.aspectFitted(CGSize(width: 64.0, height: 64.0)) + } else { + drawableSize = frameSize + } + + if self.blur { + if let current = self.blurInputTexture, current.width == Int(drawableSize.width) && current.height == Int(drawableSize.height) { + } else { + let blurTextureDescriptor = MTLTextureDescriptor.texture2DDescriptor(pixelFormat: .bgra8Unorm, width: Int(drawableSize.width), height: Int(drawableSize.height), mipmapped: false) + blurTextureDescriptor.usage = [.shaderRead, .shaderWrite, .renderTarget] + + if let texture = renderingContext.device.makeTexture(descriptor: blurTextureDescriptor) { + self.blurInputTexture = texture + } + } + + if let current = self.blurOutputTexture, current.width == Int(drawableSize.width) && current.height == Int(drawableSize.height) { + } else { + let blurTextureDescriptor = MTLTextureDescriptor.texture2DDescriptor(pixelFormat: .bgra8Unorm, width: Int(drawableSize.width), height: Int(drawableSize.height), mipmapped: false) + blurTextureDescriptor.usage = [.shaderRead, .shaderWrite] + + if let texture = renderingContext.device.makeTexture(descriptor: blurTextureDescriptor) { + self.blurOutputTexture = texture + } + } + } + + if self.metalLayer.drawableSize != drawableSize { + self.metalLayer.drawableSize = drawableSize + + getCubeVertexData( + cropX: 0, + cropY: 0, + cropWidth: Int(drawableSize.width), + cropHeight: Int(drawableSize.height), + frameWidth: Int(drawableSize.width), + frameHeight: Int(drawableSize.height), + rotation: 0, + buffer: self.vertexBuffer.contents().assumingMemoryBound(to: Float.self) + ) + } + + + guard let drawable = self.metalLayer.nextDrawable() else { + return nil + } + + if let blurInputTexture = self.blurInputTexture, let blurOutputTexture = self.blurOutputTexture { + let renderPassDescriptor = MTLRenderPassDescriptor() + renderPassDescriptor.colorAttachments[0].texture = blurInputTexture + renderPassDescriptor.colorAttachments[0].loadAction = .clear + renderPassDescriptor.colorAttachments[0].clearColor = MTLClearColor( + red: 0.0, + green: 0.0, + blue: 0.0, + alpha: 1.0 + ) + + guard let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor) else { + return nil + } + + let _ = frameBufferRenderingState.encode(renderingContext: renderingContext, vertexBuffer: self.vertexBuffer, renderEncoder: renderEncoder) + + renderEncoder.endEncoding() + + renderingContext.blurKernel.encode(commandBuffer: commandBuffer, sourceTexture: blurInputTexture, destinationTexture: blurOutputTexture) + + let blitPassDescriptor = MTLRenderPassDescriptor() + blitPassDescriptor.colorAttachments[0].texture = drawable.texture + blitPassDescriptor.colorAttachments[0].loadAction = .clear + blitPassDescriptor.colorAttachments[0].clearColor = MTLClearColor( + red: 0.0, + green: 0.0, + blue: 0.0, + alpha: 1.0 + ) + + guard let blitEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: blitPassDescriptor) else { + return nil + } + + let _ = BlitRenderingState.encode(renderingContext: renderingContext, texture: blurOutputTexture, vertexBuffer: self.vertexBuffer, renderEncoder: blitEncoder) + + blitEncoder.endEncoding() + } else { + let renderPassDescriptor = MTLRenderPassDescriptor() + renderPassDescriptor.colorAttachments[0].texture = drawable.texture + renderPassDescriptor.colorAttachments[0].loadAction = .clear + renderPassDescriptor.colorAttachments[0].clearColor = MTLClearColor( + red: 0.0, + green: 0.0, + blue: 0.0, + alpha: 1.0 + ) + + guard let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor) else { + return nil + } + + let _ = frameBufferRenderingState.encode(renderingContext: renderingContext, vertexBuffer: self.vertexBuffer, renderEncoder: renderEncoder) + + renderEncoder.endEncoding() + } + + return drawable + } + + func setOnFirstFrameReceived(_ f: @escaping (Float) -> Void) { + self.onFirstFrameReceived = f + self.didReportFirstFrame = false + } + + func setOnOrientationUpdated(_ f: @escaping (PresentationCallVideoView.Orientation, CGFloat) -> Void) { + self.onOrientationUpdated = f + } + + func getOrientation() -> PresentationCallVideoView.Orientation { + return self.currentOrientation + } + + func getAspect() -> CGFloat { + return self.currentAspect + } + + func setOnIsMirroredUpdated(_ f: @escaping (Bool) -> Void) { + self.onIsMirroredUpdated = f + } + + func updateIsEnabled(_ isEnabled: Bool) { + if self.isEnabled != isEnabled { + self.isEnabled = isEnabled + + if self.isEnabled { + self.needsRedraw = true + } + } + } +} + +@available(iOS 13.0, *) +class MetalVideoRenderingContext { + private final class ViewReference { + weak var view: MetalVideoRenderingView? + + init(view: MetalVideoRenderingView) { + self.view = view + } + } + + fileprivate let device: MTLDevice + fileprivate let textureCache: CVMetalTextureCache + fileprivate let blurKernel: MPSImageGaussianBlur + + fileprivate let blitPipelineState: MTLRenderPipelineState + fileprivate let nv12PipelineState: MTLRenderPipelineState + fileprivate let i420PipelineState: MTLRenderPipelineState + + private let commandQueue: MTLCommandQueue + + private var displayLink: ConstantDisplayLinkAnimator? + private var viewReferences = Bag() + + init?() { + guard let device = MTLCreateSystemDefaultDevice() else { + return nil + } + self.device = device + + var textureCache: CVMetalTextureCache? + let _ = CVMetalTextureCacheCreate(kCFAllocatorDefault, nil, self.device, nil, &textureCache) + if let textureCache = textureCache { + self.textureCache = textureCache + } else { + return nil + } + + let mainBundle = Bundle(for: MetalVideoRenderingView.self) + + guard let path = mainBundle.path(forResource: "TelegramCallsUIBundle", ofType: "bundle") else { + return nil + } + guard let bundle = Bundle(path: path) else { + return nil + } + guard let defaultLibrary = try? self.device.makeDefaultLibrary(bundle: bundle) else { + return nil + } + + self.blurKernel = MPSImageGaussianBlur(device: self.device, sigma: 3.0) + + func makePipelineState(vertexProgram: String, fragmentProgram: String) -> MTLRenderPipelineState? { + guard let loadedVertexProgram = defaultLibrary.makeFunction(name: vertexProgram) else { + return nil + } + guard let loadedFragmentProgram = defaultLibrary.makeFunction(name: fragmentProgram) else { + return nil + } + + let pipelineStateDescriptor = MTLRenderPipelineDescriptor() + pipelineStateDescriptor.vertexFunction = loadedVertexProgram + pipelineStateDescriptor.fragmentFunction = loadedFragmentProgram + pipelineStateDescriptor.colorAttachments[0].pixelFormat = .bgra8Unorm + guard let pipelineState = try? device.makeRenderPipelineState(descriptor: pipelineStateDescriptor) else { + return nil + } + + return pipelineState + } + + guard let blitPipelineState = makePipelineState(vertexProgram: "nv12VertexPassthrough", fragmentProgram: "blitFragmentColorConversion") else { + return nil + } + self.blitPipelineState = blitPipelineState + + guard let nv12PipelineState = makePipelineState(vertexProgram: "nv12VertexPassthrough", fragmentProgram: "nv12FragmentColorConversion") else { + return nil + } + self.nv12PipelineState = nv12PipelineState + + guard let i420PipelineState = makePipelineState(vertexProgram: "i420VertexPassthrough", fragmentProgram: "i420FragmentColorConversion") else { + return nil + } + self.i420PipelineState = i420PipelineState + + guard let commandQueue = self.device.makeCommandQueue() else { + return nil + } + self.commandQueue = commandQueue + + self.displayLink = ConstantDisplayLinkAnimator(update: { [weak self] in + self?.redraw() + }) + self.displayLink?.isPaused = false + } + + func updateVisibility(isVisible: Bool) { + self.displayLink?.isPaused = !isVisible + } + + fileprivate func add(view: MetalVideoRenderingView) -> Int { + return self.viewReferences.add(ViewReference(view: view)) + } + + fileprivate func remove(index: Int) { + self.viewReferences.remove(index) + } + + private func redraw() { + guard let commandBuffer = self.commandQueue.makeCommandBuffer() else { + return + } + + var drawables: [MTLDrawable] = [] + var takenViewReferences: [ViewReference] = [] + + for viewReference in self.viewReferences.copyItems() { + guard let videoView = viewReference.view else { + continue + } + + if !videoView.needsRedraw { + continue + } + videoView.needsRedraw = false + + if let drawable = videoView.encode(commandBuffer: commandBuffer) { + let numberOfUsedDrawables = videoView.numberOfUsedDrawables + let _ = numberOfUsedDrawables.modify { + return $0 + 1 + } + takenViewReferences.append(viewReference) + + drawable.addPresentedHandler { _ in + let _ = numberOfUsedDrawables.modify { + return max(0, $0 - 1) + } + } + + drawables.append(drawable) + } + } + + if drawables.isEmpty { + return + } + + if drawables.count > 10 { + print("Schedule \(drawables.count) drawables") + } + + commandBuffer.addScheduledHandler { _ in + for drawable in drawables { + drawable.present() + } + } + + commandBuffer.commit() + } +} + +#endif diff --git a/submodules/TelegramCallsUI/Sources/PresentationCall.swift b/submodules/TelegramCallsUI/Sources/PresentationCall.swift index 110a963ac2..6d1867ab59 100644 --- a/submodules/TelegramCallsUI/Sources/PresentationCall.swift +++ b/submodules/TelegramCallsUI/Sources/PresentationCall.swift @@ -24,7 +24,7 @@ final class PresentationCallToneRenderer { private var toneRendererAudioSessionActivated = false private let audioLevelPipe = ValuePipe() - init(tone: PresentationCallTone) { + init(tone: PresentationCallTone, completed: (() -> Void)? = nil) { let queue = Queue.mainQueue() self.queue = queue @@ -52,6 +52,7 @@ final class PresentationCallToneRenderer { let toneDataOffset = Atomic(value: 0) let toneData = Atomic(value: nil) + let reportedCompletion = Atomic(value: false) self.toneRenderer.beginRequestingFrames(queue: DispatchQueue.global(), takeFrame: { var data = toneData.with { $0 } @@ -63,6 +64,9 @@ final class PresentationCallToneRenderer { } guard let toneData = data else { + if !reportedCompletion.swap(true) { + completed?() + } return .finished } @@ -83,6 +87,11 @@ final class PresentationCallToneRenderer { if let takeOffset = takeOffset { if let toneDataMaxOffset = toneDataMaxOffset, takeOffset >= toneDataMaxOffset { + if !reportedCompletion.swap(true) { + Queue.mainQueue().after(1.0, { + completed?() + }) + } return .finished } @@ -117,6 +126,9 @@ final class PresentationCallToneRenderer { let status = CMBlockBufferCreateWithMemoryBlock(allocator: nil, memoryBlock: bytes, blockLength: frameSize, blockAllocator: nil, customBlockSource: nil, offsetToData: 0, dataLength: frameSize, flags: 0, blockBufferOut: &blockBuffer) if status != noErr { + if !reportedCompletion.swap(true) { + completed?() + } return .finished } @@ -127,15 +139,24 @@ final class PresentationCallToneRenderer { var sampleBuffer: CMSampleBuffer? var sampleSize = frameSize guard CMSampleBufferCreate(allocator: nil, dataBuffer: blockBuffer, dataReady: true, makeDataReadyCallback: nil, refcon: nil, formatDescription: nil, sampleCount: 1, sampleTimingEntryCount: 1, sampleTimingArray: &timingInfo, sampleSizeEntryCount: 1, sampleSizeArray: &sampleSize, sampleBufferOut: &sampleBuffer) == noErr else { + if !reportedCompletion.swap(true) { + completed?() + } return .finished } if let sampleBuffer = sampleBuffer { return .frame(MediaTrackFrame(type: .audio, sampleBuffer: sampleBuffer, resetDecoder: false, decoded: true)) } else { + if !reportedCompletion.swap(true) { + completed?() + } return .finished } } else { + if !reportedCompletion.swap(true) { + completed?() + } return .finished } }) @@ -927,6 +948,7 @@ public final class PresentationCallImpl: PresentationCall { let setOnFirstFrameReceived = view.setOnFirstFrameReceived let setOnOrientationUpdated = view.setOnOrientationUpdated let setOnIsMirroredUpdated = view.setOnIsMirroredUpdated + let updateIsEnabled = view.updateIsEnabled completion(PresentationCallVideoView( holder: view, view: view.view, @@ -978,6 +1000,9 @@ public final class PresentationCallImpl: PresentationCall { setOnIsMirroredUpdated { value in f?(value) } + }, + updateIsEnabled: { value in + updateIsEnabled(value) } )) } else { @@ -992,11 +1017,12 @@ public final class PresentationCallImpl: PresentationCall { self.videoCapturer = videoCapturer } - self.videoCapturer?.makeOutgoingVideoView(completion: { view in + self.videoCapturer?.makeOutgoingVideoView(requestClone: false, completion: { view, _ in if let view = view { let setOnFirstFrameReceived = view.setOnFirstFrameReceived let setOnOrientationUpdated = view.setOnOrientationUpdated let setOnIsMirroredUpdated = view.setOnIsMirroredUpdated + let updateIsEnabled = view.updateIsEnabled completion(PresentationCallVideoView( holder: view, view: view.view, @@ -1048,6 +1074,9 @@ public final class PresentationCallImpl: PresentationCall { setOnIsMirroredUpdated { value in f?(value) } + }, + updateIsEnabled: { value in + updateIsEnabled(value) } )) } else { diff --git a/submodules/TelegramCallsUI/Sources/PresentationCallManager.swift b/submodules/TelegramCallsUI/Sources/PresentationCallManager.swift index 6ffd1d3607..f0d671d9ca 100644 --- a/submodules/TelegramCallsUI/Sources/PresentationCallManager.swift +++ b/submodules/TelegramCallsUI/Sources/PresentationCallManager.swift @@ -206,7 +206,7 @@ public final class PresentationCallManagerImpl: PresentationCallManager { startCallImpl = { [weak self] account, uuid, handle, isVideo in if let strongSelf = self, let userId = Int32(handle) { - return strongSelf.startCall(account: account, peerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: userId), isVideo: isVideo, internalId: uuid) + return strongSelf.startCall(account: account, peerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt32Value(userId)), isVideo: isVideo, internalId: uuid) |> take(1) |> map { result -> Bool in return result @@ -624,6 +624,113 @@ public final class PresentationCallManagerImpl: PresentationCallManager { } } + private func requestScheduleGroupCall(accountContext: AccountContext, peerId: PeerId, internalId: CallSessionInternalId = CallSessionInternalId()) -> Signal { + let (presentationData, present, openSettings) = self.getDeviceAccessData() + + let isVideo = false + + let accessEnabledSignal: Signal = Signal { subscriber in + DeviceAccess.authorizeAccess(to: .microphone(.voiceCall), presentationData: presentationData, present: { c, a in + present(c, a) + }, openSettings: { + openSettings() + }, { value in + if isVideo && value { + DeviceAccess.authorizeAccess(to: .camera(.videoCall), presentationData: presentationData, present: { c, a in + present(c, a) + }, openSettings: { + openSettings() + }, { value in + subscriber.putNext(value) + subscriber.putCompletion() + }) + } else { + subscriber.putNext(value) + subscriber.putCompletion() + } + }) + return EmptyDisposable + } + |> runOn(Queue.mainQueue()) + + return accessEnabledSignal + |> deliverOnMainQueue + |> mapToSignal { [weak self] accessEnabled -> Signal in + guard let strongSelf = self else { + return .single(false) + } + + if !accessEnabled { + return .single(false) + } + + let call = PresentationGroupCallImpl( + accountContext: accountContext, + audioSession: strongSelf.audioSession, + callKitIntegration: nil, + getDeviceAccessData: strongSelf.getDeviceAccessData, + initialCall: nil, + internalId: internalId, + peerId: peerId, + invite: nil, + joinAsPeerId: nil + ) + strongSelf.updateCurrentGroupCall(call) + strongSelf.currentGroupCallPromise.set(.single(call)) + strongSelf.hasActiveGroupCallsPromise.set(true) + strongSelf.removeCurrentGroupCallDisposable.set((call.canBeRemoved + |> filter { $0 } + |> take(1) + |> deliverOnMainQueue).start(next: { [weak call] value in + guard let strongSelf = self, let call = call else { + return + } + if value { + if strongSelf.currentGroupCall === call { + strongSelf.updateCurrentGroupCall(nil) + strongSelf.currentGroupCallPromise.set(.single(nil)) + strongSelf.hasActiveGroupCallsPromise.set(false) + } + } + })) + + return .single(true) + } + } + + public func scheduleGroupCall(context: AccountContext, peerId: PeerId, endCurrentIfAny: Bool) -> RequestScheduleGroupCallResult { + let begin: () -> Void = { [weak self] in + let _ = self?.requestScheduleGroupCall(accountContext: context, peerId: peerId).start() + } + + if let currentGroupCall = self.currentGroupCallValue { + if endCurrentIfAny { + let endSignal = currentGroupCall.leave(terminateIfPossible: false) + |> filter { $0 } + |> take(1) + |> deliverOnMainQueue + self.startCallDisposable.set(endSignal.start(next: { _ in + begin() + })) + } else { + return .alreadyInProgress(currentGroupCall.peerId) + } + } else if let currentCall = self.currentCall { + if endCurrentIfAny { + self.callKitIntegration?.dropCall(uuid: currentCall.internalId) + self.startCallDisposable.set((currentCall.hangUp() + |> deliverOnMainQueue).start(next: { _ in + begin() + })) + } else { + return .alreadyInProgress(currentCall.peerId) + } + } else { + begin() + } + return .success + } + public func joinGroupCall(context: AccountContext, peerId: PeerId, invite: String?, requestJoinAsPeerId: ((@escaping (PeerId?) -> Void) -> Void)?, initialCall: CachedChannelData.ActiveCall, endCurrentIfAny: Bool) -> JoinGroupCallManagerResult { let begin: () -> Void = { [weak self] in if let requestJoinAsPeerId = requestJoinAsPeerId { diff --git a/submodules/TelegramCallsUI/Sources/PresentationCallToneData.swift b/submodules/TelegramCallsUI/Sources/PresentationCallToneData.swift index 065a6939d7..0d4afbbed2 100644 --- a/submodules/TelegramCallsUI/Sources/PresentationCallToneData.swift +++ b/submodules/TelegramCallsUI/Sources/PresentationCallToneData.swift @@ -108,15 +108,15 @@ enum PresentationCallTone: Equatable { func presentationCallToneData(_ tone: PresentationCallTone) -> Data? { switch tone { case .ringing: - return loadToneData(name: "voip_ringback.caf") + return loadToneData(name: "voip_ringback.mp3") case .connecting: return loadToneData(name: "voip_connecting.mp3") case .busy: - return loadToneData(name: "voip_busy.caf") + return loadToneData(name: "voip_busy.mp3") case .failed: - return loadToneData(name: "voip_fail.caf") + return loadToneData(name: "voip_fail.mp3") case .ended: - return loadToneData(name: "voip_end.caf") + return loadToneData(name: "voip_end.mp3") case .groupJoined: return loadToneData(name: "voip_group_joined.mp3") case .groupLeft: diff --git a/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift b/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift index 587eed8c7d..36a3c47ba1 100644 --- a/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift +++ b/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift @@ -24,16 +24,41 @@ private extension GroupCallParticipantsContext.Participant { if let ssrc = self.ssrc { participantSsrcs.insert(ssrc) } - if let jsonParams = self.jsonParams, let jsonData = jsonParams.data(using: .utf8), let json = try? JSONSerialization.jsonObject(with: jsonData, options: []) as? [String: Any] { - if let groups = json["ssrc-groups"] as? [Any] { - for group in groups { - if let group = group as? [String: Any] { - if let groupSources = group["sources"] as? [UInt32] { - for source in groupSources { - participantSsrcs.insert(source) - } - } - } + if let videoDescription = self.videoDescription { + for group in videoDescription.ssrcGroups { + for ssrc in group.ssrcs { + participantSsrcs.insert(ssrc) + } + } + } + if let presentationDescription = self.presentationDescription { + for group in presentationDescription.ssrcGroups { + for ssrc in group.ssrcs { + participantSsrcs.insert(ssrc) + } + } + } + return participantSsrcs + } + + var videoSsrcs: Set { + var participantSsrcs = Set() + if let videoDescription = self.videoDescription { + for group in videoDescription.ssrcGroups { + for ssrc in group.ssrcs { + participantSsrcs.insert(ssrc) + } + } + } + return participantSsrcs + } + + var presentationSsrcs: Set { + var participantSsrcs = Set() + if let presentationDescription = self.presentationDescription { + for group in presentationDescription.ssrcGroups { + for ssrc in group.ssrcs { + participantSsrcs.insert(ssrc) } } } @@ -67,17 +92,22 @@ public final class AccountGroupCallContextImpl: AccountGroupCallContext { return self.panelDataPromise.get() } - public init(account: Account, peerId: PeerId, call: CachedChannelData.ActiveCall) { + public init(account: Account, engine: TelegramEngine, peerId: PeerId, call: CachedChannelData.ActiveCall) { self.panelDataPromise.set(.single(GroupCallPanelData( peerId: peerId, info: GroupCallInfo( id: call.id, accessHash: call.accessHash, participantCount: 0, - clientParams: nil, streamDcId: nil, title: call.title, - recordingStartTimestamp: nil + scheduleTimestamp: call.scheduleTimestamp, + subscribedToScheduled: call.subscribedToScheduled, + recordingStartTimestamp: nil, + sortAscending: true, + defaultParticipantsAreMuted: nil, + isVideoEnabled: false, + unmutedVideoLimit: 0 ), topParticipants: [], participantCount: 0, @@ -85,7 +115,7 @@ public final class AccountGroupCallContextImpl: AccountGroupCallContext { groupCall: nil ))) - self.disposable = (getGroupCallParticipants(account: account, callId: call.id, accessHash: call.accessHash, offset: "", ssrcs: [], limit: 100) + self.disposable = (engine.calls.getGroupCallParticipants(callId: call.id, accessHash: call.accessHash, offset: "", ssrcs: [], limit: 100, sortAscending: nil) |> map(Optional.init) |> `catch` { _ -> Signal in return .single(nil) @@ -94,8 +124,7 @@ public final class AccountGroupCallContextImpl: AccountGroupCallContext { guard let strongSelf = self, let state = state else { return } - let context = GroupCallParticipantsContext( - account: account, + let context = engine.calls.groupCall( peerId: peerId, myPeerId: account.peerId, id: call.id, @@ -119,7 +148,7 @@ public final class AccountGroupCallContextImpl: AccountGroupCallContext { } return GroupCallPanelData( peerId: peerId, - info: GroupCallInfo(id: call.id, accessHash: call.accessHash, participantCount: state.totalCount, clientParams: nil, streamDcId: nil, title: state.title, recordingStartTimestamp: nil), + info: GroupCallInfo(id: call.id, accessHash: call.accessHash, participantCount: state.totalCount, streamDcId: nil, title: state.title, scheduleTimestamp: state.scheduleTimestamp, subscribedToScheduled: state.subscribedToScheduled, recordingStartTimestamp: nil, sortAscending: state.sortAscending, defaultParticipantsAreMuted: state.defaultParticipantsAreMuted, isVideoEnabled: state.isVideoEnabled, unmutedVideoLimit: state.unmutedVideoLimit), topParticipants: topParticipants, participantCount: state.totalCount, activeSpeakers: activeSpeakers, @@ -155,12 +184,12 @@ public final class AccountGroupCallContextCacheImpl: AccountGroupCallContextCach self.queue = queue } - public func get(account: Account, peerId: PeerId, call: CachedChannelData.ActiveCall) -> AccountGroupCallContextImpl.Proxy { + public func get(account: Account, engine: TelegramEngine, peerId: PeerId, call: CachedChannelData.ActiveCall) -> AccountGroupCallContextImpl.Proxy { let result: Record if let current = self.contexts[call.id] { result = current } else { - let context = AccountGroupCallContextImpl(account: account, peerId: peerId, call: call) + let context = AccountGroupCallContextImpl(account: account, engine: engine, peerId: peerId, call: call) result = Record(context: context) self.contexts[call.id] = result } @@ -186,8 +215,8 @@ public final class AccountGroupCallContextCacheImpl: AccountGroupCallContextCach }) } - public func leaveInBackground(account: Account, id: Int64, accessHash: Int64, source: UInt32) { - let disposable = leaveGroupCall(account: account, callId: id, accessHash: accessHash, source: source).start() + public func leaveInBackground(engine: TelegramEngine, id: Int64, accessHash: Int64, source: UInt32) { + let disposable = engine.calls.leaveGroupCall(callId: id, accessHash: accessHash, source: source).start() self.leaveDisposables.add(disposable) } } @@ -204,7 +233,7 @@ public final class AccountGroupCallContextCacheImpl: AccountGroupCallContextCach } private extension PresentationGroupCallState { - static func initialValue(myPeerId: PeerId, title: String?) -> PresentationGroupCallState { + static func initialValue(myPeerId: PeerId, title: String?, scheduleTimestamp: Int32?, subscribedToScheduled: Bool) -> PresentationGroupCallState { return PresentationGroupCallState( myPeerId: myPeerId, networkState: .connecting, @@ -214,7 +243,10 @@ private extension PresentationGroupCallState { defaultParticipantMuteState: nil, recordingStartTimestamp: nil, title: title, - raisedHand: false + raisedHand: false, + scheduleTimestamp: scheduleTimestamp, + subscribedToScheduled: subscribedToScheduled, + isVideoEnabled: false ) } } @@ -358,24 +390,36 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { private var ignorePreviousJoinAsPeerId: (PeerId, UInt32)? private var reconnectingAsPeer: Peer? - public private(set) var isVideo: Bool + public private(set) var hasVideo: Bool + public private(set) var hasScreencast: Bool + private let isVideoEnabled: Bool private var temporaryJoinTimestamp: Int32 private var temporaryActivityTimestamp: Double? private var temporaryActivityRank: Int? private var temporaryRaiseHandRating: Int64? private var temporaryHasRaiseHand: Bool = false + private var temporaryJoinedVideo: Bool = true private var temporaryMuteState: GroupCallParticipantsContext.Participant.MuteState? private var internalState: InternalState = .requesting private let internalStatePromise = Promise(.requesting) private var currentLocalSsrc: UInt32? + private var currentLocalEndpointId: String? - private var callContext: OngoingGroupCallContext? + private var genericCallContext: OngoingGroupCallContext? private var currentConnectionMode: OngoingGroupCallContext.ConnectionMode = .none - private var ssrcMapping: [UInt32: PeerId] = [:] - - private var requestedSsrcs = Set() + private var didInitializeConnectionMode: Bool = false + + private var screencastCallContext: OngoingGroupCallContext? + private var screencastBufferServerContext: IpcGroupCallBufferAppContext? + private var screencastCapturer: OngoingCallVideoCapturer? + + private struct SsrcMapping { + var peerId: PeerId + var isPresentation: Bool + } + private var ssrcMapping: [UInt32: SsrcMapping] = [:] private var summaryInfoState = Promise(nil) private var summaryParticipantsState = Promise(nil) @@ -405,6 +449,15 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { } } } + + private let isNoiseSuppressionEnabledPromise = ValuePromise(true) + public var isNoiseSuppressionEnabled: Signal { + return self.isNoiseSuppressionEnabledPromise.get() + } + private let isNoiseSuppressionEnabledDisposable = MetaDisposable() + + private var isVideoMuted: Bool = false + private let isVideoMutedDisposable = MetaDisposable() private let audioOutputStatePromise = Promise<([AudioSessionOutput], AudioSessionOutput?)>(([], nil)) private var audioOutputStateDisposable: Disposable? @@ -463,6 +516,18 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { public var state: Signal { return self.statePromise.get() } + + private var stateVersionValue: Int = 0 { + didSet { + if self.stateVersionValue != oldValue { + self.stateVersionPromise.set(self.stateVersionValue) + } + } + } + private let stateVersionPromise = ValuePromise(0) + public var stateVersion: Signal { + return self.stateVersionPromise.get() + } private var membersValue: PresentationGroupCallMembers? { didSet { @@ -500,7 +565,10 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { } private let joinDisposable = MetaDisposable() + private let screencastJoinDisposable = MetaDisposable() private let requestDisposable = MetaDisposable() + private let startDisposable = MetaDisposable() + private let subscribeDisposable = MetaDisposable() private var groupCallParticipantUpdatesDisposable: Disposable? private let networkStateDisposable = MetaDisposable() @@ -531,19 +599,23 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { private var toneRenderer: PresentationCallToneRenderer? private var videoCapturer: OngoingCallVideoCapturer? - - private let incomingVideoSourcePromise = Promise<[PeerId: UInt32]>([:]) - public var incomingVideoSources: Signal<[PeerId: UInt32], NoError> { - return self.incomingVideoSourcePromise.get() - } - - private var missingSsrcs = Set() - private var processedMissingSsrcs = Set() - private let missingSsrcsDisposable = MetaDisposable() - private var isRequestingMissingSsrcs: Bool = false + private var useFrontCamera: Bool = true private var peerUpdatesSubscription: Disposable? + public private(set) var schedulePending = false + private var isScheduled = false + private var isScheduledStarted = false + + private let isSpeakingPromise = ValuePromise(false, ignoreRepeated: true) + public var isSpeaking: Signal { + return self.isSpeakingPromise.get() + } + + private var screencastFramesDisposable: Disposable? + private var screencastAudioDataDisposable: Disposable? + private var screencastStateDisposable: Disposable? + init( accountContext: AccountContext, audioSession: ManagedAudioSession, @@ -566,14 +638,17 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { self.peerId = peerId self.invite = invite self.joinAsPeerId = joinAsPeerId ?? accountContext.account.peerId + self.schedulePending = initialCall == nil + self.isScheduled = initialCall == nil || initialCall?.scheduleTimestamp != nil - self.stateValue = PresentationGroupCallState.initialValue(myPeerId: self.joinAsPeerId, title: initialCall?.title) + self.stateValue = PresentationGroupCallState.initialValue(myPeerId: self.joinAsPeerId, title: initialCall?.title, scheduleTimestamp: initialCall?.scheduleTimestamp, subscribedToScheduled: initialCall?.subscribedToScheduled ?? false) self.statePromise = ValuePromise(self.stateValue) self.temporaryJoinTimestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) - - //self.videoCapturer = OngoingCallVideoCapturer(keepLandscape: true) - self.isVideo = self.videoCapturer != nil + + self.isVideoEnabled = true + self.hasVideo = false + self.hasScreencast = false var didReceiveAudioOutputs = false @@ -678,7 +753,6 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { return } if case let .established(callInfo, _, _, _, _) = strongSelf.internalState { - var addedParticipants: [(UInt32, String?)] = [] var removedSsrc: [UInt32] = [] for (callId, update) in updates { if callId == callInfo.id { @@ -713,14 +787,10 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { } } } else if case .joined = participantUpdate.participationStatusChange { - if let ssrc = participantUpdate.ssrc { - addedParticipants.append((ssrc, participantUpdate.jsonParams)) - } } else if let ssrc = participantUpdate.ssrc, strongSelf.ssrcMapping[ssrc] == nil { - addedParticipants.append((ssrc, participantUpdate.jsonParams)) } } - case let .call(isTerminated, _, _, _): + case let .call(isTerminated, _, _, _, _, _): if isTerminated { strongSelf.markAsCanBeRemoved() } @@ -728,9 +798,8 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { } } if !removedSsrc.isEmpty { - strongSelf.callContext?.removeSsrcs(ssrcs: removedSsrc) + strongSelf.genericCallContext?.removeSsrcs(ssrcs: removedSsrc) } - //strongSelf.callContext?.addParticipants(participants: addedParticipants) } }) @@ -753,7 +822,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { }) if let initialCall = initialCall, let temporaryParticipantsContext = (self.accountContext.cachedGroupCallContexts as? AccountGroupCallContextCacheImpl)?.impl.syncWith({ impl in - impl.get(account: accountContext.account, peerId: peerId, call: CachedChannelData.ActiveCall(id: initialCall.id, accessHash: initialCall.accessHash, title: initialCall.title)) + impl.get(account: accountContext.account, engine: accountContext.engine, peerId: peerId, call: CachedChannelData.ActiveCall(id: initialCall.id, accessHash: initialCall.accessHash, title: initialCall.title, scheduleTimestamp: initialCall.scheduleTimestamp, subscribedToScheduled: initialCall.subscribedToScheduled)) }) { self.switchToTemporaryParticipantsContext(sourceContext: temporaryParticipantsContext.context.participantsContext, oldMyPeerId: self.joinAsPeerId) } else { @@ -797,19 +866,67 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { strongSelf.stateValue = updatedValue }) - self.requestCall(movingFromBroadcastToRtc: false) + if let _ = self.initialCall { + self.requestCall(movingFromBroadcastToRtc: false) + } + + let basePath = self.accountContext.sharedContext.basePath + "/broadcast-coordination" + let screencastBufferServerContext = IpcGroupCallBufferAppContext(basePath: basePath) + self.screencastBufferServerContext = screencastBufferServerContext + let screencastCapturer = OngoingCallVideoCapturer(isCustom: true) + self.screencastCapturer = screencastCapturer + self.screencastFramesDisposable = (screencastBufferServerContext.frames + |> deliverOnMainQueue).start(next: { [weak screencastCapturer] screencastFrame in + guard let screencastCapturer = screencastCapturer else { + return + } + screencastCapturer.injectPixelBuffer(screencastFrame.0, rotation: screencastFrame.1) + }) + self.screencastAudioDataDisposable = (screencastBufferServerContext.audioData + |> deliverOnMainQueue).start(next: { [weak self] data in + guard let strongSelf = self else { + return + } + strongSelf.screencastCallContext?.addExternalAudioData(data: data) + }) + self.screencastStateDisposable = (screencastBufferServerContext.isActive + |> distinctUntilChanged + |> deliverOnMainQueue).start(next: { [weak self] isActive in + guard let strongSelf = self else { + return + } + if isActive { + strongSelf.requestScreencast() + } else { + strongSelf.disableScreencast() + } + }) + + /*Queue.mainQueue().after(2.0, { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.screencastBufferClientContext = IpcGroupCallBufferBroadcastContext(basePath: basePath) + })*/ } deinit { + assert(Queue.mainQueue().isCurrent()) + self.audioSessionShouldBeActiveDisposable?.dispose() self.audioSessionActiveDisposable?.dispose() self.summaryStateDisposable?.dispose() self.audioSessionDisposable?.dispose() self.joinDisposable.dispose() + self.screencastJoinDisposable.dispose() self.requestDisposable.dispose() + self.startDisposable.dispose() + self.subscribeDisposable.dispose() self.groupCallParticipantUpdatesDisposable?.dispose() self.leaveDisposable.dispose() self.isMutedDisposable.dispose() + self.isNoiseSuppressionEnabledDisposable.dispose() + self.isVideoMutedDisposable.dispose() self.memberStatesDisposable.dispose() self.networkStateDisposable.dispose() self.checkCallDisposable?.dispose() @@ -817,7 +934,6 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { self.participantsContextStateDisposable.dispose() self.myAudioLevelDisposable.dispose() self.memberEventsPipeDisposable.dispose() - self.missingSsrcsDisposable.dispose() self.myAudioLevelTimer?.invalidate() self.typingDisposable.dispose() @@ -831,10 +947,15 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { self.removedChannelMembersDisposable?.dispose() self.peerUpdatesSubscription?.dispose() + + self.screencastFramesDisposable?.dispose() + self.screencastAudioDataDisposable?.dispose() + self.screencastStateDisposable?.dispose() } private func switchToTemporaryParticipantsContext(sourceContext: GroupCallParticipantsContext?, oldMyPeerId: PeerId) { let myPeerId = self.joinAsPeerId + let accountContext = self.accountContext let myPeer = self.accountContext.account.postbox.transaction { transaction -> (Peer, CachedPeerData?)? in if let peer = transaction.getPeer(myPeerId) { return (peer, transaction.getPeerCachedData(peerId: myPeerId)) @@ -842,8 +963,13 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { return nil } } + |> beforeNext { view in + if let view = view, view.1 == nil { + let _ = accountContext.engine.peers.fetchAndUpdateCachedPeerData(peerId: myPeerId).start() + } + } if let sourceContext = sourceContext, let initialState = sourceContext.immediateState { - let temporaryParticipantsContext = GroupCallParticipantsContext(account: self.account, peerId: self.peerId, myPeerId: myPeerId, id: sourceContext.id, accessHash: sourceContext.accessHash, state: initialState, previousServiceState: sourceContext.serviceState) + let temporaryParticipantsContext = self.accountContext.engine.calls.groupCall(peerId: self.peerId, myPeerId: myPeerId, id: sourceContext.id, accessHash: sourceContext.accessHash, state: initialState, previousServiceState: sourceContext.serviceState) self.temporaryParticipantsContext = temporaryParticipantsContext self.participantsContextStateDisposable.set((combineLatest(queue: .mainQueue(), myPeer, @@ -854,7 +980,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { guard let strongSelf = self else { return } - + var topParticipants: [GroupCallParticipantsContext.Participant] = [] var members = PresentationGroupCallMembers( @@ -886,12 +1012,13 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { } else if let cachedData = cachedData as? CachedUserData { about = cachedData.about } else { - about = nil + about = " " } participants.append(GroupCallParticipantsContext.Participant( peer: myPeer, ssrc: nil, - jsonParams: nil, + videoDescription: nil, + presentationDescription: nil, joinTimestamp: strongSelf.temporaryJoinTimestamp, raiseHandRating: strongSelf.temporaryRaiseHandRating, hasRaiseHand: strongSelf.temporaryHasRaiseHand, @@ -899,9 +1026,10 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { activityRank: strongSelf.temporaryActivityRank, muteState: strongSelf.temporaryMuteState ?? GroupCallParticipantsContext.Participant.MuteState(canUnmute: true, mutedByYou: false), volume: nil, - about: about + about: about, + joinedVideo: strongSelf.temporaryJoinedVideo )) - participants.sort() + participants.sort(by: { GroupCallParticipantsContext.Participant.compare(lhs: $0, rhs: $1, sortAscending: state.sortAscending) }) } } @@ -959,31 +1087,30 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { var participants: [GroupCallParticipantsContext.Participant] = [] - if !participants.contains(where: { $0.peer.id == myPeerId }) { - if let (myPeer, cachedData) = myPeerAndCachedData { - let about: String? - if let cachedData = cachedData as? CachedUserData { - about = cachedData.about - } else if let cachedData = cachedData as? CachedUserData { - about = cachedData.about - } else { - about = nil - } - participants.append(GroupCallParticipantsContext.Participant( - peer: myPeer, - ssrc: nil, - jsonParams: nil, - joinTimestamp: strongSelf.temporaryJoinTimestamp, - raiseHandRating: strongSelf.temporaryRaiseHandRating, - hasRaiseHand: strongSelf.temporaryHasRaiseHand, - activityTimestamp: strongSelf.temporaryActivityTimestamp, - activityRank: strongSelf.temporaryActivityRank, - muteState: strongSelf.temporaryMuteState ?? GroupCallParticipantsContext.Participant.MuteState(canUnmute: true, mutedByYou: false), - volume: nil, - about: about - )) - participants.sort() + if let (myPeer, cachedData) = myPeerAndCachedData { + let about: String? + if let cachedData = cachedData as? CachedUserData { + about = cachedData.about + } else if let cachedData = cachedData as? CachedUserData { + about = cachedData.about + } else { + about = " " } + participants.append(GroupCallParticipantsContext.Participant( + peer: myPeer, + ssrc: nil, + videoDescription: nil, + presentationDescription: nil, + joinTimestamp: strongSelf.temporaryJoinTimestamp, + raiseHandRating: strongSelf.temporaryRaiseHandRating, + hasRaiseHand: strongSelf.temporaryHasRaiseHand, + activityTimestamp: strongSelf.temporaryActivityTimestamp, + activityRank: strongSelf.temporaryActivityRank, + muteState: strongSelf.temporaryMuteState ?? GroupCallParticipantsContext.Participant.MuteState(canUnmute: true, mutedByYou: false), + volume: nil, + about: about, + joinedVideo: strongSelf.temporaryJoinedVideo + )) } for participant in participants { @@ -1004,6 +1131,198 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { } } + private func switchToTemporaryScheduledParticipantsContext() { + guard let callInfo = self.internalState.callInfo, callInfo.scheduleTimestamp != nil else { + return + } + let accountContext = self.accountContext + let peerId = self.peerId + let rawAdminIds: Signal, NoError> + if peerId.namespace == Namespaces.Peer.CloudChannel { + rawAdminIds = Signal { subscriber in + let (disposable, _) = accountContext.peerChannelMemberCategoriesContextsManager.admins(engine: accountContext.engine, postbox: accountContext.account.postbox, network: accountContext.account.network, accountPeerId: accountContext.account.peerId, peerId: peerId, updated: { list in + var peerIds = Set() + for item in list.list { + if let adminInfo = item.participant.adminInfo, adminInfo.rights.rights.contains(.canManageCalls) { + peerIds.insert(item.peer.id) + } + } + subscriber.putNext(peerIds) + }) + return disposable + } + |> distinctUntilChanged + |> runOn(.mainQueue()) + } else { + rawAdminIds = accountContext.account.postbox.combinedView(keys: [.cachedPeerData(peerId: peerId)]) + |> map { views -> Set in + guard let view = views.views[.cachedPeerData(peerId: peerId)] as? CachedPeerDataView else { + return Set() + } + guard let cachedData = view.cachedPeerData as? CachedGroupData, let participants = cachedData.participants else { + return Set() + } + return Set(participants.participants.compactMap { item -> PeerId? in + switch item { + case .creator, .admin: + return item.peerId + default: + return nil + } + }) + } + |> distinctUntilChanged + } + + let adminIds = combineLatest(queue: .mainQueue(), + rawAdminIds, + accountContext.account.postbox.combinedView(keys: [.basicPeer(peerId)]) + ) + |> map { rawAdminIds, view -> Set in + var rawAdminIds = rawAdminIds + if let peerView = view.views[.basicPeer(peerId)] as? BasicPeerView, let peer = peerView.peer as? TelegramChannel { + if peer.hasPermission(.manageCalls) { + rawAdminIds.insert(accountContext.account.peerId) + } else { + rawAdminIds.remove(accountContext.account.peerId) + } + } + return rawAdminIds + } + |> distinctUntilChanged + + let participantsContext = self.accountContext.engine.calls.groupCall( + peerId: self.peerId, + myPeerId: self.joinAsPeerId, + id: callInfo.id, + accessHash: callInfo.accessHash, + state: GroupCallParticipantsContext.State( + participants: [], + nextParticipantsFetchOffset: nil, + adminIds: Set(), + isCreator: false, + defaultParticipantsAreMuted: callInfo.defaultParticipantsAreMuted ?? GroupCallParticipantsContext.State.DefaultParticipantsAreMuted(isMuted: self.stateValue.defaultParticipantMuteState == .muted, canChange: true), + sortAscending: true, + recordingStartTimestamp: nil, + title: self.stateValue.title, + scheduleTimestamp: self.stateValue.scheduleTimestamp, + subscribedToScheduled: self.stateValue.subscribedToScheduled, + totalCount: 0, + isVideoEnabled: callInfo.isVideoEnabled, + unmutedVideoLimit: callInfo.unmutedVideoLimit, + version: 0 + ), + previousServiceState: nil + ) + self.temporaryParticipantsContext = nil + self.participantsContext = participantsContext + + let myPeerId = self.joinAsPeerId + let myPeer = self.accountContext.account.postbox.transaction { transaction -> (Peer, CachedPeerData?)? in + if let peer = transaction.getPeer(myPeerId) { + return (peer, transaction.getPeerCachedData(peerId: myPeerId)) + } else { + return nil + } + } + |> beforeNext { view in + if let view = view, view.1 == nil { + let _ = accountContext.engine.peers.fetchAndUpdateCachedPeerData(peerId: myPeerId).start() + } + } + self.participantsContextStateDisposable.set(combineLatest(queue: .mainQueue(), + participantsContext.state, + adminIds, + myPeer, + accountContext.account.postbox.peerView(id: peerId) + ).start(next: { [weak self] state, adminIds, myPeerAndCachedData, view in + guard let strongSelf = self else { + return + } + + var members = PresentationGroupCallMembers( + participants: [], + speakingParticipants: Set(), + totalCount: state.totalCount, + loadMoreToken: state.nextParticipantsFetchOffset + ) + + strongSelf.stateValue.adminIds = adminIds + let canManageCall = state.isCreator || strongSelf.stateValue.adminIds.contains(strongSelf.accountContext.account.peerId) + + var participants: [GroupCallParticipantsContext.Participant] = [] + var topParticipants: [GroupCallParticipantsContext.Participant] = [] + if let (myPeer, cachedData) = myPeerAndCachedData { + let about: String? + if let cachedData = cachedData as? CachedUserData { + about = cachedData.about + } else if let cachedData = cachedData as? CachedUserData { + about = cachedData.about + } else { + about = " " + } + participants.append(GroupCallParticipantsContext.Participant( + peer: myPeer, + ssrc: nil, + videoDescription: nil, + presentationDescription: nil, + joinTimestamp: strongSelf.temporaryJoinTimestamp, + raiseHandRating: strongSelf.temporaryRaiseHandRating, + hasRaiseHand: strongSelf.temporaryHasRaiseHand, + activityTimestamp: strongSelf.temporaryActivityTimestamp, + activityRank: strongSelf.temporaryActivityRank, + muteState: strongSelf.temporaryMuteState ?? GroupCallParticipantsContext.Participant.MuteState(canUnmute: canManageCall || !state.defaultParticipantsAreMuted.isMuted, mutedByYou: false), + volume: nil, + about: about, + joinedVideo: strongSelf.temporaryJoinedVideo + )) + } + + for participant in participants { + members.participants.append(participant) + + if topParticipants.count < 3 { + topParticipants.append(participant) + } + } + + strongSelf.membersValue = members + strongSelf.stateValue.canManageCall = state.isCreator || adminIds.contains(strongSelf.accountContext.account.peerId) + strongSelf.stateValue.defaultParticipantMuteState = state.defaultParticipantsAreMuted.isMuted ? .muted : .unmuted + + + strongSelf.stateValue.recordingStartTimestamp = state.recordingStartTimestamp + strongSelf.stateValue.title = state.title + strongSelf.stateValue.muteState = GroupCallParticipantsContext.Participant.MuteState(canUnmute: canManageCall || !state.defaultParticipantsAreMuted.isMuted, mutedByYou: false) + + strongSelf.stateValue.scheduleTimestamp = strongSelf.isScheduledStarted ? nil : state.scheduleTimestamp + if state.scheduleTimestamp == nil && !strongSelf.isScheduledStarted { + strongSelf.updateSessionState(internalState: .active(GroupCallInfo(id: callInfo.id, accessHash: callInfo.accessHash, participantCount: state.totalCount, streamDcId: callInfo.streamDcId, title: state.title, scheduleTimestamp: nil, subscribedToScheduled: false, recordingStartTimestamp: nil, sortAscending: true, defaultParticipantsAreMuted: callInfo.defaultParticipantsAreMuted ?? state.defaultParticipantsAreMuted, isVideoEnabled: callInfo.isVideoEnabled, unmutedVideoLimit: callInfo.unmutedVideoLimit)), audioSessionControl: strongSelf.audioSessionControl) + } else { + strongSelf.summaryInfoState.set(.single(SummaryInfoState(info: GroupCallInfo( + id: callInfo.id, + accessHash: callInfo.accessHash, + participantCount: state.totalCount, + streamDcId: nil, + title: state.title, + scheduleTimestamp: state.scheduleTimestamp, + subscribedToScheduled: false, + recordingStartTimestamp: state.recordingStartTimestamp, + sortAscending: state.sortAscending, + defaultParticipantsAreMuted: state.defaultParticipantsAreMuted, + isVideoEnabled: state.isVideoEnabled, + unmutedVideoLimit: state.unmutedVideoLimit + )))) + + strongSelf.summaryParticipantsState.set(.single(SummaryParticipantsState( + participantCount: state.totalCount, + topParticipants: topParticipants, + activeSpeakers: Set() + ))) + } + })) + } + private func updateSessionState(internalState: InternalState, audioSessionControl: ManagedAudioSessionControl?) { let previousControl = self.audioSessionControl self.audioSessionControl = audioSessionControl @@ -1033,259 +1352,312 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { } } + var shouldJoin = false + let activeCallInfo: GroupCallInfo? switch previousInternalState { - case .active: - break - default: - if case let .active(callInfo) = internalState { - let callContext: OngoingGroupCallContext - if let current = self.callContext { - callContext = current + case let .active(previousCallInfo): + if case let .active(callInfo) = internalState { + shouldJoin = previousCallInfo.scheduleTimestamp != nil && callInfo.scheduleTimestamp == nil + self.participantsContext = nil + activeCallInfo = callInfo } else { - callContext = OngoingGroupCallContext(video: self.videoCapturer, participantDescriptionsRequired: { [weak self] ssrcs in - Queue.mainQueue().async { - guard let strongSelf = self else { - return - } - strongSelf.maybeRequestParticipants(ssrcs: ssrcs) - } - }, audioStreamData: OngoingGroupCallContext.AudioStreamData(account: self.accountContext.account, callId: callInfo.id, accessHash: callInfo.accessHash), rejoinNeeded: { [weak self] in - Queue.mainQueue().async { - guard let strongSelf = self else { - return - } - if case .established = strongSelf.internalState { - strongSelf.requestCall(movingFromBroadcastToRtc: false) - } - } - }) - self.incomingVideoSourcePromise.set(callContext.videoSources - |> deliverOnMainQueue - |> map { [weak self] sources -> [PeerId: UInt32] in + activeCallInfo = nil + } + default: + if case let .active(callInfo) = internalState { + shouldJoin = callInfo.scheduleTimestamp == nil + activeCallInfo = callInfo + } else { + activeCallInfo = nil + } + } + if self.leaving { + shouldJoin = false + } + + if shouldJoin, let callInfo = activeCallInfo { + let genericCallContext: OngoingGroupCallContext + if let current = self.genericCallContext { + genericCallContext = current + } else { + var outgoingAudioBitrateKbit: Int32? + let appConfiguration = self.accountContext.currentAppConfiguration.with({ $0 }) + if let data = appConfiguration.data, let value = data["voice_chat_send_bitrate"] as? Int32 { + outgoingAudioBitrateKbit = value + } + + let enableNoiseSuppression = accountContext.sharedContext.immediateExperimentalUISettings.enableNoiseSuppression + + genericCallContext = OngoingGroupCallContext(video: self.videoCapturer, requestMediaChannelDescriptions: { [weak self] ssrcs, completion in + let disposable = MetaDisposable() + Queue.mainQueue().async { guard let strongSelf = self else { - return [:] + return } - var result: [PeerId: UInt32] = [:] - for source in sources { - if let peerId = strongSelf.ssrcMapping[source] { - result[peerId] = source + disposable.set(strongSelf.requestMediaChannelDescriptions(ssrcs: ssrcs, completion: completion)) + } + return disposable + }, audioStreamData: OngoingGroupCallContext.AudioStreamData(engine: self.accountContext.engine, callId: callInfo.id, accessHash: callInfo.accessHash), rejoinNeeded: { [weak self] in + Queue.mainQueue().async { + guard let strongSelf = self else { + return + } + if case .established = strongSelf.internalState { + strongSelf.requestCall(movingFromBroadcastToRtc: false) + } + } + }, outgoingAudioBitrateKbit: outgoingAudioBitrateKbit, videoContentType: self.isVideoEnabled ? .generic : .none, enableNoiseSuppression: enableNoiseSuppression) + + self.genericCallContext = genericCallContext + self.stateVersionValue += 1 + } + self.joinDisposable.set((genericCallContext.joinPayload + |> distinctUntilChanged(isEqual: { lhs, rhs in + if lhs.0 != rhs.0 { + return false + } + if lhs.1 != rhs.1 { + return false + } + return true + }) + |> deliverOnMainQueue).start(next: { [weak self] joinPayload, ssrc in + guard let strongSelf = self else { + return + } + + let peerAdminIds: Signal<[PeerId], NoError> + let peerId = strongSelf.peerId + if strongSelf.peerId.namespace == Namespaces.Peer.CloudChannel { + peerAdminIds = Signal { subscriber in + let (disposable, _) = strongSelf.accountContext.peerChannelMemberCategoriesContextsManager.admins(engine: strongSelf.accountContext.engine, postbox: strongSelf.accountContext.account.postbox, network: strongSelf.accountContext.account.network, accountPeerId: strongSelf.accountContext.account.peerId, peerId: peerId, updated: { list in + var peerIds = Set() + for item in list.list { + if let adminInfo = item.participant.adminInfo, adminInfo.rights.rights.contains(.canManageCalls) { + peerIds.insert(item.peer.id) + } + } + subscriber.putNext(Array(peerIds)) + }) + return disposable + } + |> distinctUntilChanged + |> runOn(.mainQueue()) + } else { + peerAdminIds = strongSelf.account.postbox.transaction { transaction -> [PeerId] in + var result: [PeerId] = [] + if let cachedData = transaction.getPeerCachedData(peerId: peerId) as? CachedGroupData { + if let participants = cachedData.participants { + for participant in participants.participants { + if case .creator = participant { + result.append(participant.peerId) + } else if case .admin = participant { + result.append(participant.peerId) + } + } } } return result - }) - self.callContext = callContext + } } - self.joinDisposable.set((callContext.joinPayload - |> distinctUntilChanged(isEqual: { lhs, rhs in - if lhs.0 != rhs.0 { - return false - } - if lhs.1 != rhs.1 { - return false - } - return true - }) - |> deliverOnMainQueue).start(next: { [weak self] joinPayload, ssrc in + + strongSelf.currentLocalSsrc = ssrc + strongSelf.requestDisposable.set((strongSelf.accountContext.engine.calls.joinGroupCall( + peerId: strongSelf.peerId, + joinAs: strongSelf.joinAsPeerId, + callId: callInfo.id, + accessHash: callInfo.accessHash, + preferMuted: true, + joinPayload: joinPayload, + peerAdminIds: peerAdminIds, + inviteHash: strongSelf.invite + ) + |> deliverOnMainQueue).start(next: { joinCallResult in guard let strongSelf = self else { return } + let clientParams = joinCallResult.jsonParams + if let data = clientParams.data(using: .utf8), let dict = (try? JSONSerialization.jsonObject(with: data, options: [])) as? [String: Any] { + if let video = dict["video"] as? [String: Any] { + if let endpointId = video["endpoint"] as? String { + strongSelf.currentLocalEndpointId = endpointId + } + } + } - let peerAdminIds: Signal<[PeerId], NoError> - let peerId = strongSelf.peerId - if strongSelf.peerId.namespace == Namespaces.Peer.CloudChannel { - peerAdminIds = Signal { subscriber in - let (disposable, _) = strongSelf.accountContext.peerChannelMemberCategoriesContextsManager.admins(postbox: strongSelf.accountContext.account.postbox, network: strongSelf.accountContext.account.network, accountPeerId: strongSelf.accountContext.account.peerId, peerId: peerId, updated: { list in - var peerIds = Set() - for item in list.list { - if let adminInfo = item.participant.adminInfo, adminInfo.rights.rights.contains(.canManageCalls) { - peerIds.insert(item.peer.id) - } + strongSelf.ssrcMapping.removeAll() + for participant in joinCallResult.state.participants { + if let ssrc = participant.ssrc { + strongSelf.ssrcMapping[ssrc] = SsrcMapping(peerId: participant.peer.id, isPresentation: false) + } + if let presentationSsrc = participant.presentationDescription?.audioSsrc { + strongSelf.ssrcMapping[presentationSsrc] = SsrcMapping(peerId: participant.peer.id, isPresentation: true) + } + } + + switch joinCallResult.connectionMode { + case .rtc: + strongSelf.currentConnectionMode = .rtc + strongSelf.genericCallContext?.setConnectionMode(.rtc, keepBroadcastConnectedIfWasEnabled: false) + strongSelf.genericCallContext?.setJoinResponse(payload: clientParams) + case .broadcast: + strongSelf.currentConnectionMode = .broadcast + strongSelf.genericCallContext?.setConnectionMode(.broadcast, keepBroadcastConnectedIfWasEnabled: false) + } + + strongSelf.updateSessionState(internalState: .established(info: joinCallResult.callInfo, connectionMode: joinCallResult.connectionMode, clientParams: clientParams, localSsrc: ssrc, initialState: joinCallResult.state), audioSessionControl: strongSelf.audioSessionControl) + }, error: { error in + guard let strongSelf = self else { + return + } + if case .anonymousNotAllowed = error { + let presentationData = strongSelf.accountContext.sharedContext.currentPresentationData.with { $0 } + strongSelf.accountContext.sharedContext.mainWindow?.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: presentationData.strings.VoiceChat_AnonymousDisabledAlertText, actions: [ + TextAlertAction(type: .genericAction, title: presentationData.strings.Common_OK, action: {}) + ]), on: .root, blockInteraction: false, completion: {}) + } else if case .tooManyParticipants = error { + let presentationData = strongSelf.accountContext.sharedContext.currentPresentationData.with { $0 } + strongSelf.accountContext.sharedContext.mainWindow?.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: presentationData.strings.VoiceChat_ChatFullAlertText, actions: [ + TextAlertAction(type: .genericAction, title: presentationData.strings.Common_OK, action: {}) + ]), on: .root, blockInteraction: false, completion: {}) + } else if case .invalidJoinAsPeer = error { + let peerId = strongSelf.peerId + let _ = strongSelf.accountContext.engine.calls.clearCachedGroupCallDisplayAsAvailablePeers(peerId: peerId).start() + let _ = (strongSelf.accountContext.account.postbox.transaction { transaction -> Void in + transaction.updatePeerCachedData(peerIds: Set([peerId]), update: { _, current in + if let current = current as? CachedChannelData { + return current.withUpdatedCallJoinPeerId(nil) + } else if let current = current as? CachedGroupData { + return current.withUpdatedCallJoinPeerId(nil) + } else { + return current } - subscriber.putNext(Array(peerIds)) }) - return disposable - } - |> distinctUntilChanged - |> runOn(.mainQueue()) - } else { - peerAdminIds = strongSelf.account.postbox.transaction { transaction -> [PeerId] in - var result: [PeerId] = [] - if let cachedData = transaction.getPeerCachedData(peerId: peerId) as? CachedGroupData { - if let participants = cachedData.participants { - for participant in participants.participants { - if case .creator = participant { - result.append(participant.peerId) - } else if case .admin = participant { - result.append(participant.peerId) - } - } - } - } - return result - } + }).start() } - - strongSelf.currentLocalSsrc = ssrc - strongSelf.requestDisposable.set((joinGroupCall( - account: strongSelf.account, - peerId: strongSelf.peerId, - joinAs: strongSelf.joinAsPeerId, - callId: callInfo.id, - accessHash: callInfo.accessHash, - preferMuted: true, - joinPayload: joinPayload, - peerAdminIds: peerAdminIds, - inviteHash: strongSelf.invite - ) - |> deliverOnMainQueue).start(next: { joinCallResult in - guard let strongSelf = self else { - return - } - if let clientParams = joinCallResult.callInfo.clientParams { - strongSelf.ssrcMapping.removeAll() - let addedParticipants: [(UInt32, String?)] = [] - for participant in joinCallResult.state.participants { - if let ssrc = participant.ssrc { - strongSelf.ssrcMapping[ssrc] = participant.peer.id - //addedParticipants.append((participant.ssrc, participant.jsonParams)) - } - } - - switch joinCallResult.connectionMode { - case .rtc: - strongSelf.currentConnectionMode = .rtc - strongSelf.callContext?.setConnectionMode(.rtc, keepBroadcastConnectedIfWasEnabled: false) - strongSelf.callContext?.setJoinResponse(payload: clientParams, participants: addedParticipants) - case .broadcast: - strongSelf.currentConnectionMode = .broadcast - strongSelf.callContext?.setConnectionMode(.broadcast, keepBroadcastConnectedIfWasEnabled: false) - } - - strongSelf.updateSessionState(internalState: .established(info: joinCallResult.callInfo, connectionMode: joinCallResult.connectionMode, clientParams: clientParams, localSsrc: ssrc, initialState: joinCallResult.state), audioSessionControl: strongSelf.audioSessionControl) - } - }, error: { error in - guard let strongSelf = self else { - return - } - if case .anonymousNotAllowed = error { - let presentationData = strongSelf.accountContext.sharedContext.currentPresentationData.with { $0 } - strongSelf.accountContext.sharedContext.mainWindow?.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: presentationData.strings.VoiceChat_AnonymousDisabledAlertText, actions: [ - TextAlertAction(type: .genericAction, title: presentationData.strings.Common_OK, action: {}) - ]), on: .root, blockInteraction: false, completion: {}) - } else if case .tooManyParticipants = error { - let presentationData = strongSelf.accountContext.sharedContext.currentPresentationData.with { $0 } - strongSelf.accountContext.sharedContext.mainWindow?.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: presentationData.strings.VoiceChat_ChatFullAlertText, actions: [ - TextAlertAction(type: .genericAction, title: presentationData.strings.Common_OK, action: {}) - ]), on: .root, blockInteraction: false, completion: {}) - } - strongSelf.markAsCanBeRemoved() - })) + strongSelf.markAsCanBeRemoved() })) + })) + + self.networkStateDisposable.set((genericCallContext.networkState + |> deliverOnMainQueue).start(next: { [weak self] state in + guard let strongSelf = self else { + return + } + let mappedState: PresentationGroupCallState.NetworkState + if state.isConnected { + mappedState = .connected + } else { + mappedState = .connecting + } + + let wasConnecting = strongSelf.stateValue.networkState == .connecting + if strongSelf.stateValue.networkState != mappedState { + strongSelf.stateValue.networkState = mappedState + } + let isConnecting = mappedState == .connecting - self.networkStateDisposable.set((callContext.networkState - |> deliverOnMainQueue).start(next: { [weak self] state in - guard let strongSelf = self else { - return - } - let mappedState: PresentationGroupCallState.NetworkState - if state.isConnected { - mappedState = .connected - } else { - mappedState = .connecting - } - - let wasConnecting = strongSelf.stateValue.networkState == .connecting - if strongSelf.stateValue.networkState != mappedState { - strongSelf.stateValue.networkState = mappedState - } - let isConnecting = mappedState == .connecting - - if strongSelf.isCurrentlyConnecting != isConnecting { - strongSelf.isCurrentlyConnecting = isConnecting - if isConnecting { - strongSelf.startCheckingCallIfNeeded() - } else { - strongSelf.checkCallDisposable?.dispose() - strongSelf.checkCallDisposable = nil - } - } - - strongSelf.isReconnectingAsSpeaker = state.isTransitioningFromBroadcastToRtc - - if (wasConnecting != isConnecting && strongSelf.didConnectOnce) { - if isConnecting { - let toneRenderer = PresentationCallToneRenderer(tone: .groupConnecting) - strongSelf.toneRenderer = toneRenderer - toneRenderer.setAudioSessionActive(strongSelf.isAudioSessionActive) - } else { - strongSelf.toneRenderer = nil - } - } - + if strongSelf.isCurrentlyConnecting != isConnecting { + strongSelf.isCurrentlyConnecting = isConnecting if isConnecting { - strongSelf.didStartConnectingOnce = true + strongSelf.startCheckingCallIfNeeded() + } else { + strongSelf.checkCallDisposable?.dispose() + strongSelf.checkCallDisposable = nil } - - if state.isConnected { - if !strongSelf.didConnectOnce { - strongSelf.didConnectOnce = true - - let toneRenderer = PresentationCallToneRenderer(tone: .groupJoined) - strongSelf.toneRenderer = toneRenderer - toneRenderer.setAudioSessionActive(strongSelf.isAudioSessionActive) - } + } - if let peer = strongSelf.reconnectingAsPeer { - strongSelf.reconnectingAsPeer = nil - strongSelf.reconnectedAsEventsPipe.putNext(peer) + strongSelf.isReconnectingAsSpeaker = state.isTransitioningFromBroadcastToRtc + + if (wasConnecting != isConnecting && strongSelf.didConnectOnce) { + if isConnecting { + strongSelf.beginTone(tone: .groupConnecting) + } else { + strongSelf.toneRenderer = nil + } + } + + if isConnecting { + strongSelf.didStartConnectingOnce = true + } + + if state.isConnected { + if !strongSelf.didConnectOnce { + strongSelf.didConnectOnce = true + + if !strongSelf.isScheduled { + strongSelf.beginTone(tone: .groupJoined) } } - })) - - self.audioLevelsDisposable.set((callContext.audioLevels - |> deliverOnMainQueue).start(next: { [weak self] levels in - guard let strongSelf = self else { - return + + if let peer = strongSelf.reconnectingAsPeer { + strongSelf.reconnectingAsPeer = nil + strongSelf.reconnectedAsEventsPipe.putNext(peer) } - var result: [(PeerId, UInt32, Float, Bool)] = [] - var myLevel: Float = 0.0 - var myLevelHasVoice: Bool = false - var missingSsrcs = Set() - for (ssrcKey, level, hasVoice) in levels { - var peerId: PeerId? - let ssrcValue: UInt32 - switch ssrcKey { - case .local: - peerId = strongSelf.joinAsPeerId - ssrcValue = 0 - case let .source(ssrc): - peerId = strongSelf.ssrcMapping[ssrc] + } + })) + + self.isNoiseSuppressionEnabledDisposable.set((genericCallContext.isNoiseSuppressionEnabled + |> deliverOnMainQueue).start(next: { [weak self] value in + guard let strongSelf = self else { + return + } + strongSelf.isNoiseSuppressionEnabledPromise.set(value) + })) + + self.audioLevelsDisposable.set((genericCallContext.audioLevels + |> deliverOnMainQueue).start(next: { [weak self] levels in + guard let strongSelf = self else { + return + } + var result: [(PeerId, UInt32, Float, Bool)] = [] + var myLevel: Float = 0.0 + var myLevelHasVoice: Bool = false + var orignalMyLevelHasVoice: Bool = false + var missingSsrcs = Set() + for (ssrcKey, level, hasVoice) in levels { + var peerId: PeerId? + let ssrcValue: UInt32 + switch ssrcKey { + case .local: + peerId = strongSelf.joinAsPeerId + ssrcValue = 0 + case let .source(ssrc): + if let mapping = strongSelf.ssrcMapping[ssrc] { + if mapping.isPresentation { + peerId = nil + ssrcValue = 0 + } else { + peerId = mapping.peerId + ssrcValue = ssrc + } + } else { ssrcValue = ssrc } - if let peerId = peerId { - if case .local = ssrcKey { - if !strongSelf.isMutedValue.isEffectivelyMuted { - myLevel = level - myLevelHasVoice = hasVoice - } - } - result.append((peerId, ssrcValue, level, hasVoice)) - } else if ssrcValue != 0 { - missingSsrcs.insert(ssrcValue) + } + if let peerId = peerId { + if case .local = ssrcKey { + orignalMyLevelHasVoice = hasVoice + myLevel = level + myLevelHasVoice = hasVoice } + result.append((peerId, ssrcValue, level, hasVoice)) + } else if ssrcValue != 0 { + missingSsrcs.insert(ssrcValue) } - - strongSelf.speakingParticipantsContext.update(levels: result) - - let mappedLevel = myLevel * 1.5 - strongSelf.myAudioLevelPipe.putNext(mappedLevel) - strongSelf.processMyAudioLevel(level: mappedLevel, hasVoice: myLevelHasVoice) - - if !missingSsrcs.isEmpty { - strongSelf.participantsContext?.ensureHaveParticipants(ssrcs: missingSsrcs) - } - })) - } + } + + strongSelf.speakingParticipantsContext.update(levels: result) + + let mappedLevel = myLevel * 1.5 + strongSelf.myAudioLevelPipe.putNext(mappedLevel) + strongSelf.processMyAudioLevel(level: mappedLevel, hasVoice: myLevelHasVoice) + strongSelf.isSpeakingPromise.set(orignalMyLevelHasVoice) + + if !missingSsrcs.isEmpty { + strongSelf.participantsContext?.ensureHaveParticipants(ssrcs: missingSsrcs) + } + })) } switch previousInternalState { @@ -1305,13 +1677,16 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { if self.stateValue.title != initialState.title { self.stateValue.title = initialState.title } + if self.stateValue.scheduleTimestamp != initialState.scheduleTimestamp { + self.stateValue.scheduleTimestamp = initialState.scheduleTimestamp + } let accountContext = self.accountContext let peerId = self.peerId let rawAdminIds: Signal, NoError> if peerId.namespace == Namespaces.Peer.CloudChannel { rawAdminIds = Signal { subscriber in - let (disposable, _) = accountContext.peerChannelMemberCategoriesContextsManager.admins(postbox: accountContext.account.postbox, network: accountContext.account.network, accountPeerId: accountContext.account.peerId, peerId: peerId, updated: { list in + let (disposable, _) = accountContext.peerChannelMemberCategoriesContextsManager.admins(engine: accountContext.engine, postbox: accountContext.account.postbox, network: accountContext.account.network, accountPeerId: accountContext.account.peerId, peerId: peerId, updated: { list in var peerIds = Set() for item in list.list { if let adminInfo = item.participant.adminInfo, adminInfo.rights.rights.contains(.canManageCalls) { @@ -1367,12 +1742,11 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { var initialState = initialState var serviceState: GroupCallParticipantsContext.ServiceState? if let participantsContext = self.participantsContext, let immediateState = participantsContext.immediateState { - initialState.mergeActivity(from: immediateState, myPeerId: myPeerId, previousMyPeerId: self.ignorePreviousJoinAsPeerId?.0) + initialState.mergeActivity(from: immediateState, myPeerId: myPeerId, previousMyPeerId: self.ignorePreviousJoinAsPeerId?.0, mergeActivityTimestamps: true) serviceState = participantsContext.serviceState } - let participantsContext = GroupCallParticipantsContext( - account: self.accountContext.account, + let participantsContext = self.accountContext.engine.calls.groupCall( peerId: self.peerId, myPeerId: self.joinAsPeerId, id: callInfo.id, @@ -1382,13 +1756,20 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { ) self.temporaryParticipantsContext = nil self.participantsContext = participantsContext - let myPeer = self.accountContext.account.postbox.transaction { transaction -> (Peer, CachedPeerData?)? in - if let peer = transaction.getPeer(myPeerId) { - return (peer, transaction.getPeerCachedData(peerId: myPeerId)) + let myPeer = self.accountContext.account.postbox.peerView(id: myPeerId) + |> map { view -> (Peer, CachedPeerData?)? in + if let peer = peerViewMainPeer(view) { + return (peer, view.cachedData) } else { return nil } } + |> beforeNext { view in + if let view = view, view.1 == nil { + let _ = accountContext.engine.peers.fetchAndUpdateCachedPeerData(peerId: myPeerId).start() + } + } + self.participantsContextStateDisposable.set(combineLatest(queue: .mainQueue(), participantsContext.state, participantsContext.activeSpeakers, @@ -1456,13 +1837,14 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { } else if let cachedData = cachedData as? CachedChannelData { about = cachedData.about } else { - about = nil + about = " " } participants.append(GroupCallParticipantsContext.Participant( peer: myPeer, ssrc: nil, - jsonParams: nil, + videoDescription: nil, + presentationDescription: nil, joinTimestamp: strongSelf.temporaryJoinTimestamp, raiseHandRating: strongSelf.temporaryRaiseHandRating, hasRaiseHand: strongSelf.temporaryHasRaiseHand, @@ -1470,11 +1852,14 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { activityRank: strongSelf.temporaryActivityRank, muteState: strongSelf.temporaryMuteState ?? GroupCallParticipantsContext.Participant.MuteState(canUnmute: true, mutedByYou: false), volume: nil, - about: about + about: about, + joinedVideo: strongSelf.temporaryJoinedVideo )) - participants.sort() + participants.sort(by: { GroupCallParticipantsContext.Participant.compare(lhs: $0, rhs: $1, sortAscending: state.sortAscending) }) } } + + var otherParticipantsWithVideo = 0 for participant in participants { var participant = participant @@ -1484,10 +1869,26 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { } if let ssrc = participant.ssrc { - strongSelf.ssrcMapping[ssrc] = participant.peer.id + strongSelf.ssrcMapping[ssrc] = SsrcMapping(peerId: participant.peer.id, isPresentation: false) + } + if let presentationSsrc = participant.presentationDescription?.audioSsrc { + strongSelf.ssrcMapping[presentationSsrc] = SsrcMapping(peerId: participant.peer.id, isPresentation: true) } if participant.peer.id == strongSelf.joinAsPeerId { + if let (myPeer, cachedData) = myPeerAndCachedData { + let about: String? + if let cachedData = cachedData as? CachedUserData { + about = cachedData.about + } else if let cachedData = cachedData as? CachedChannelData { + about = cachedData.about + } else { + about = " " + } + participant.peer = myPeer + participant.about = about + } + var filteredMuteState = participant.muteState if isReconnectingAsSpeaker || strongSelf.currentConnectionMode != .rtc { filteredMuteState = GroupCallParticipantsContext.Participant.MuteState(canUnmute: false, mutedByYou: false) @@ -1496,7 +1897,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { let previousRaisedHand = strongSelf.stateValue.raisedHand if !(strongSelf.stateValue.muteState?.canUnmute ?? false) { - strongSelf.stateValue.raisedHand = participant.raiseHandRating != nil + strongSelf.stateValue.raisedHand = participant.hasRaiseHand } if let muteState = participant.muteState, muteState.canUnmute && previousRaisedHand { @@ -1534,30 +1935,41 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { switch strongSelf.isMutedValue { case let .muted(isPushToTalkActive): if !isPushToTalkActive { - strongSelf.callContext?.setIsMuted(true) + strongSelf.genericCallContext?.setIsMuted(true) } case .unmuted: strongSelf.isMutedValue = .muted(isPushToTalkActive: false) - strongSelf.callContext?.setIsMuted(true) + strongSelf.genericCallContext?.setIsMuted(true) } } else { strongSelf.isMutedValue = .muted(isPushToTalkActive: false) - strongSelf.callContext?.setIsMuted(true) + strongSelf.genericCallContext?.setIsMuted(true) } strongSelf.stateValue.muteState = muteState } else if let currentMuteState = strongSelf.stateValue.muteState, !currentMuteState.canUnmute { strongSelf.isMutedValue = .muted(isPushToTalkActive: false) strongSelf.stateValue.muteState = GroupCallParticipantsContext.Participant.MuteState(canUnmute: true, mutedByYou: false) - strongSelf.callContext?.setIsMuted(true) + strongSelf.genericCallContext?.setIsMuted(true) } } else { if let ssrc = participant.ssrc { if let volume = participant.volume { - strongSelf.callContext?.setVolume(ssrc: ssrc, volume: Double(volume) / 10000.0) + strongSelf.genericCallContext?.setVolume(ssrc: ssrc, volume: Double(volume) / 10000.0) } else if participant.muteState?.mutedByYou == true { - strongSelf.callContext?.setVolume(ssrc: ssrc, volume: 0.0) + strongSelf.genericCallContext?.setVolume(ssrc: ssrc, volume: 0.0) } } + if let presentationSsrc = participant.presentationDescription?.audioSsrc { + if let volume = participant.volume { + strongSelf.genericCallContext?.setVolume(ssrc: presentationSsrc, volume: Double(volume) / 10000.0) + } else if participant.muteState?.mutedByYou == true { + strongSelf.genericCallContext?.setVolume(ssrc: presentationSsrc, volume: 0.0) + } + } + + if participant.videoDescription != nil || participant.presentationDescription != nil { + otherParticipantsWithVideo += 1 + } } if let index = updatedInvitedPeers.firstIndex(of: participant.peer.id) { @@ -1581,15 +1993,22 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { } strongSelf.stateValue.recordingStartTimestamp = state.recordingStartTimestamp strongSelf.stateValue.title = state.title - + strongSelf.stateValue.scheduleTimestamp = state.scheduleTimestamp + strongSelf.stateValue.isVideoEnabled = state.isVideoEnabled && otherParticipantsWithVideo < state.unmutedVideoLimit + strongSelf.summaryInfoState.set(.single(SummaryInfoState(info: GroupCallInfo( id: callInfo.id, accessHash: callInfo.accessHash, participantCount: state.totalCount, - clientParams: nil, streamDcId: nil, title: state.title, - recordingStartTimestamp: state.recordingStartTimestamp + scheduleTimestamp: state.scheduleTimestamp, + subscribedToScheduled: false, + recordingStartTimestamp: state.recordingStartTimestamp, + sortAscending: state.sortAscending, + defaultParticipantsAreMuted: state.defaultParticipantsAreMuted, + isVideoEnabled: state.isVideoEnabled, + unmutedVideoLimit: state.unmutedVideoLimit )))) strongSelf.summaryParticipantsState.set(.single(SummaryParticipantsState( @@ -1608,7 +2027,9 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { |> mapToSignal { event -> Signal in return postbox.transaction { transaction -> Signal in if let peer = transaction.getPeer(event.peerId) { - return .single(PresentationGroupCallMemberEvent(peer: peer, joined: event.joined)) + let isContact = transaction.isPeerContact(peerId: event.peerId) + let isInChatList = transaction.getPeerChatListIndex(event.peerId) != nil + return .single(PresentationGroupCallMemberEvent(peer: peer, isContact: isContact, isInChatList: isInChatList, canUnmute: event.canUnmute, joined: event.joined)) } else { return .complete() } @@ -1616,90 +2037,79 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { |> switchToLatest } |> deliverOnMainQueue).start(next: { [weak self] event in - guard let strongSelf = self else { + guard let strongSelf = self, event.peer.id != strongSelf.stateValue.myPeerId else { return } - if event.peer.id == strongSelf.stateValue.myPeerId { - return + var skip = false + if let participantsCount = strongSelf.participantsContext?.immediateState?.totalCount, participantsCount >= 250 { + if event.peer.isVerified || event.isContact || event.isInChatList || (strongSelf.stateValue.defaultParticipantMuteState == .muted && event.canUnmute) { + skip = false + } else { + skip = true + } + } + if !skip { + strongSelf.memberEventsPipe.putNext(event) } - strongSelf.memberEventsPipe.putNext(event) })) if let isCurrentlyConnecting = self.isCurrentlyConnecting, isCurrentlyConnecting { self.startCheckingCallIfNeeded() } + } else if case let .active(callInfo) = internalState, callInfo.scheduleTimestamp != nil { + self.switchToTemporaryScheduledParticipantsContext() } } } - private func maybeRequestParticipants(ssrcs: Set) { - var missingSsrcs = ssrcs - missingSsrcs.subtract(self.processedMissingSsrcs) - if missingSsrcs.isEmpty { - return + private func requestMediaChannelDescriptions(ssrcs: Set, completion: @escaping ([OngoingGroupCallContext.MediaChannelDescription]) -> Void) -> Disposable { + func extractMediaChannelDescriptions(remainingSsrcs: inout Set, participants: [GroupCallParticipantsContext.Participant], into result: inout [OngoingGroupCallContext.MediaChannelDescription]) { + for participant in participants { + guard let audioSsrc = participant.ssrc else { + continue + } + + if remainingSsrcs.contains(audioSsrc) { + remainingSsrcs.remove(audioSsrc) + + result.append(OngoingGroupCallContext.MediaChannelDescription( + kind: .audio, + audioSsrc: audioSsrc, + videoDescription: nil + )) + } + + if let screencastSsrc = participant.presentationDescription?.audioSsrc { + if remainingSsrcs.contains(screencastSsrc) { + remainingSsrcs.remove(screencastSsrc) + + result.append(OngoingGroupCallContext.MediaChannelDescription( + kind: .audio, + audioSsrc: screencastSsrc, + videoDescription: nil + )) + } + } + } } - self.processedMissingSsrcs.formUnion(ssrcs) - - var addedParticipants: [(UInt32, String?)] = [] - + + var remainingSsrcs = ssrcs + var result: [OngoingGroupCallContext.MediaChannelDescription] = [] + if let membersValue = self.membersValue { - for participant in membersValue.participants { - let participantSsrcs = participant.allSsrcs - - if !missingSsrcs.intersection(participantSsrcs).isEmpty { - missingSsrcs.subtract(participantSsrcs) - self.processedMissingSsrcs.formUnion(participantSsrcs) - - if let ssrc = participant.ssrc { - addedParticipants.append((ssrc, participant.jsonParams)) - } - } - } + extractMediaChannelDescriptions(remainingSsrcs: &remainingSsrcs, participants: membersValue.participants, into: &result) } - - if !addedParticipants.isEmpty { - self.callContext?.addParticipants(participants: addedParticipants) - } - - if !missingSsrcs.isEmpty { - self.missingSsrcs.formUnion(missingSsrcs) - self.maybeRequestMissingSsrcs() - } - } - - private func maybeRequestMissingSsrcs() { - if self.isRequestingMissingSsrcs { - return - } - if self.missingSsrcs.isEmpty { - return - } - if case let .established(callInfo, _, _, _, _) = self.internalState { - self.isRequestingMissingSsrcs = true - - let requestedSsrcs = self.missingSsrcs - self.missingSsrcsDisposable.set((getGroupCallParticipants(account: self.account, callId: callInfo.id, accessHash: callInfo.accessHash, offset: "", ssrcs: Array(requestedSsrcs), limit: 100) - |> deliverOnMainQueue).start(next: { [weak self] state in - guard let strongSelf = self else { - return - } - strongSelf.isRequestingMissingSsrcs = false - strongSelf.missingSsrcs.subtract(requestedSsrcs) - - var addedParticipants: [(UInt32, String?)] = [] - - for participant in state.participants { - if let ssrc = participant.ssrc { - addedParticipants.append((ssrc, participant.jsonParams)) - } - } - - if !addedParticipants.isEmpty { - strongSelf.callContext?.addParticipants(participants: addedParticipants) - } - - strongSelf.maybeRequestMissingSsrcs() - })) + + if !remainingSsrcs.isEmpty, let callInfo = self.internalState.callInfo { + return (self.accountContext.engine.calls.getGroupCallParticipants(callId: callInfo.id, accessHash: callInfo.accessHash, offset: "", ssrcs: Array(remainingSsrcs), limit: 100, sortAscending: callInfo.sortAscending) + |> deliverOnMainQueue).start(next: { state in + extractMediaChannelDescriptions(remainingSsrcs: &remainingSsrcs, participants: state.participants, into: &result) + + completion(result) + }) + } else { + completion(result) + return EmptyDisposable } } @@ -1708,14 +2118,21 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { return } if case let .established(callInfo, connectionMode, _, ssrc, _) = self.internalState, case .rtc = connectionMode { - let checkSignal = checkGroupCall(account: self.account, callId: callInfo.id, accessHash: callInfo.accessHash, ssrc: Int32(bitPattern: ssrc)) + let checkSignal = self.accountContext.engine.calls.checkGroupCall(callId: callInfo.id, accessHash: callInfo.accessHash, ssrcs: [ssrc]) self.checkCallDisposable = (( checkSignal |> castError(Bool.self) |> delay(4.0, queue: .mainQueue()) |> mapToSignal { result -> Signal in - if case .success = result { + var foundAll = true + for value in [ssrc] { + if !result.contains(value) { + foundAll = false + break + } + } + if foundAll { return .fail(true) } else { return .single(true) @@ -1741,6 +2158,24 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { } } + private func beginTone(tone: PresentationCallTone) { + var completed: (() -> Void)? + let toneRenderer = PresentationCallToneRenderer(tone: tone, completed: { + completed?() + }) + completed = { [weak self, weak toneRenderer] in + Queue.mainQueue().async { + guard let strongSelf = self, let toneRenderer = toneRenderer, toneRenderer === strongSelf.toneRenderer else { + return + } + strongSelf.toneRenderer = nil + } + } + + self.toneRenderer = toneRenderer + toneRenderer.setAudioSessionActive(self.isAudioSessionActive) + } + public func playTone(_ tone: PresentationGroupCallTone) { let name: String switch tone { @@ -1750,9 +2185,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { name = "voip_group_recording_started.mp3" } - let toneRenderer = PresentationCallToneRenderer(tone: .custom(name: name, loopCount: 1)) - self.toneRenderer = toneRenderer - toneRenderer.setAudioSessionActive(self.isAudioSessionActive) + self.beginTone(tone: .custom(name: name, loopCount: 1)) } private func markAsCanBeRemoved() { @@ -1761,7 +2194,11 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { } self.markedAsCanBeRemoved = true - self.callContext?.stop() + self.genericCallContext?.stop() + + //self.screencastIpcContext = nil + self.screencastCallContext?.stop() + self._canBeRemoved.set(.single(true)) if self.didConnectOnce { @@ -1776,10 +2213,8 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { strongSelf.wasRemoved.set(.single(true)) return } - - let toneRenderer = PresentationCallToneRenderer(tone: .groupLeft) - strongSelf.toneRenderer = toneRenderer - toneRenderer.setAudioSessionActive(strongSelf.isAudioSessionActive) + + strongSelf.beginTone(tone: .groupLeft) Queue.mainQueue().after(1.0, { strongSelf.wasRemoved.set(.single(true)) @@ -1802,11 +2237,9 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { return transaction.getPeer(peerId) } |> deliverOnMainQueue).start(next: { [weak self] myPeer in - guard let strongSelf = self, let _ = myPeer else { + guard let strongSelf = self, let myPeer = myPeer else { return } - - strongSelf.reconnectingAsPeer = myPeer let previousPeerId = strongSelf.joinAsPeerId if let localSsrc = strongSelf.currentLocalSsrc { @@ -1814,48 +2247,63 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { } strongSelf.joinAsPeerId = peerId - if let participantsContext = strongSelf.participantsContext, let immediateState = participantsContext.immediateState { - for participant in immediateState.participants { - if participant.peer.id == previousPeerId { - strongSelf.temporaryJoinTimestamp = participant.joinTimestamp - strongSelf.temporaryActivityTimestamp = participant.activityTimestamp - strongSelf.temporaryActivityRank = participant.activityRank - strongSelf.temporaryRaiseHandRating = participant.raiseHandRating - strongSelf.temporaryHasRaiseHand = participant.hasRaiseHand - strongSelf.temporaryMuteState = participant.muteState - } - } - strongSelf.switchToTemporaryParticipantsContext(sourceContext: participantsContext, oldMyPeerId: previousPeerId) - } else { + if strongSelf.stateValue.scheduleTimestamp != nil { strongSelf.stateValue.myPeerId = peerId + strongSelf.reconnectedAsEventsPipe.putNext(myPeer) + strongSelf.switchToTemporaryScheduledParticipantsContext() + } else { + strongSelf.disableVideo() + strongSelf.isMutedValue = .muted(isPushToTalkActive: false) + strongSelf.isMutedPromise.set(strongSelf.isMutedValue) + + strongSelf.reconnectingAsPeer = myPeer + + if let participantsContext = strongSelf.participantsContext, let immediateState = participantsContext.immediateState { + for participant in immediateState.participants { + if participant.peer.id == previousPeerId { + strongSelf.temporaryJoinTimestamp = participant.joinTimestamp + strongSelf.temporaryActivityTimestamp = participant.activityTimestamp + strongSelf.temporaryActivityRank = participant.activityRank + strongSelf.temporaryRaiseHandRating = participant.raiseHandRating + strongSelf.temporaryHasRaiseHand = participant.hasRaiseHand + strongSelf.temporaryMuteState = participant.muteState + strongSelf.temporaryJoinedVideo = participant.joinedVideo + } + } + strongSelf.switchToTemporaryParticipantsContext(sourceContext: participantsContext, oldMyPeerId: previousPeerId) + } else { + strongSelf.stateValue.myPeerId = peerId + } + + strongSelf.requestCall(movingFromBroadcastToRtc: false) } - - strongSelf.requestCall(movingFromBroadcastToRtc: false) }) } public func leave(terminateIfPossible: Bool) -> Signal { self.leaving = true - if let callInfo = self.internalState.callInfo, let localSsrc = self.currentLocalSsrc { + if let callInfo = self.internalState.callInfo { if terminateIfPossible { - self.leaveDisposable.set((stopGroupCall(account: self.account, peerId: self.peerId, callId: callInfo.id, accessHash: callInfo.accessHash) + self.leaveDisposable.set((self.accountContext.engine.calls.stopGroupCall(peerId: self.peerId, callId: callInfo.id, accessHash: callInfo.accessHash) |> deliverOnMainQueue).start(completed: { [weak self] in guard let strongSelf = self else { return } strongSelf.markAsCanBeRemoved() })) - } else { + } else if let localSsrc = self.currentLocalSsrc { if let contexts = self.accountContext.cachedGroupCallContexts as? AccountGroupCallContextCacheImpl { - let account = self.account + let engine = self.accountContext.engine let id = callInfo.id let accessHash = callInfo.accessHash let source = localSsrc contexts.impl.with { impl in - impl.leaveInBackground(account: account, id: id, accessHash: accessHash, source: source) + impl.leaveInBackground(engine: engine, id: id, accessHash: accessHash, source: source) } } self.markAsCanBeRemoved() + } else { + self.markAsCanBeRemoved() } } else { self.markAsCanBeRemoved() @@ -1893,7 +2341,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { isVisuallyMuted = false let _ = self.updateMuteState(peerId: self.joinAsPeerId, isMuted: false) } - self.callContext?.setIsMuted(isEffectivelyMuted) + self.genericCallContext?.setIsMuted(isEffectivelyMuted) if isVisuallyMuted { self.stateValue.muteState = GroupCallParticipantsContext.Participant.MuteState(canUnmute: true, mutedByYou: false) @@ -1901,6 +2349,68 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { self.stateValue.muteState = nil } } + + public func setIsNoiseSuppressionEnabled(_ isNoiseSuppressionEnabled: Bool) { + self.genericCallContext?.setIsNoiseSuppressionEnabled(isNoiseSuppressionEnabled) + } + + public func toggleScheduledSubscription(_ subscribe: Bool) { + guard case let .active(callInfo) = self.internalState, callInfo.scheduleTimestamp != nil else { + return + } + + self.stateValue.subscribedToScheduled = subscribe + + self.subscribeDisposable.set((self.accountContext.engine.calls.toggleScheduledGroupCallSubscription(peerId: self.peerId, callId: callInfo.id, accessHash: callInfo.accessHash, subscribe: subscribe) + |> deliverOnMainQueue).start()) + } + + public func schedule(timestamp: Int32) { + guard self.schedulePending else { + return + } + + self.schedulePending = false + self.stateValue.scheduleTimestamp = timestamp + + self.summaryParticipantsState.set(.single(SummaryParticipantsState( + participantCount: 1, + topParticipants: [], + activeSpeakers: Set() + ))) + + self.startDisposable.set((self.accountContext.engine.calls.createGroupCall(peerId: self.peerId, title: nil, scheduleDate: timestamp) + |> deliverOnMainQueue).start(next: { [weak self] callInfo in + guard let strongSelf = self else { + return + } + strongSelf.updateSessionState(internalState: .active(callInfo), audioSessionControl: strongSelf.audioSessionControl) + }, error: { [weak self] error in + if let strongSelf = self { + strongSelf.markAsCanBeRemoved() + } + })) + } + + + public func startScheduled() { + guard case let .active(callInfo) = self.internalState else { + return + } + + self.isScheduledStarted = true + self.stateValue.scheduleTimestamp = nil + + self.startDisposable.set((self.accountContext.engine.calls.startScheduledGroupCall(peerId: self.peerId, callId: callInfo.id, accessHash: callInfo.accessHash) + |> deliverOnMainQueue).start(next: { [weak self] callInfo in + guard let strongSelf = self else { + return + } + strongSelf.updateSessionState(internalState: .active(callInfo), audioSessionControl: strongSelf.audioSessionControl) + + strongSelf.beginTone(tone: .groupJoined) + })) + } public func raiseHand() { guard let membersValue = self.membersValue else { @@ -1908,7 +2418,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { } for participant in membersValue.participants { if participant.peer.id == self.joinAsPeerId { - if participant.raiseHandRating != nil { + if participant.hasRaiseHand { return } break @@ -1924,7 +2434,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { } for participant in membersValue.participants { if participant.peer.id == self.joinAsPeerId { - if participant.raiseHandRating == nil { + if !participant.hasRaiseHand { return } break @@ -1934,48 +2444,303 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { self.participantsContext?.lowerHand() } + public func makeOutgoingVideoView(requestClone: Bool, completion: @escaping (PresentationCallVideoView?, PresentationCallVideoView?) -> Void) { + if self.videoCapturer == nil { + let videoCapturer = OngoingCallVideoCapturer() + self.videoCapturer = videoCapturer + } + + guard let videoCapturer = self.videoCapturer else { + completion(nil, nil) + return + } + videoCapturer.makeOutgoingVideoView(requestClone: requestClone, completion: { mainView, cloneView in + if let mainView = mainView { + let setOnFirstFrameReceived = mainView.setOnFirstFrameReceived + let setOnOrientationUpdated = mainView.setOnOrientationUpdated + let setOnIsMirroredUpdated = mainView.setOnIsMirroredUpdated + let updateIsEnabled = mainView.updateIsEnabled + let mainVideoView = PresentationCallVideoView( + holder: mainView, + view: mainView.view, + setOnFirstFrameReceived: { f in + setOnFirstFrameReceived(f) + }, + getOrientation: { [weak mainView] in + if let mainView = mainView { + let mappedValue: PresentationCallVideoView.Orientation + switch mainView.getOrientation() { + case .rotation0: + mappedValue = .rotation0 + case .rotation90: + mappedValue = .rotation90 + case .rotation180: + mappedValue = .rotation180 + case .rotation270: + mappedValue = .rotation270 + } + return mappedValue + } else { + return .rotation0 + } + }, + getAspect: { [weak mainView] in + if let mainView = mainView { + return mainView.getAspect() + } else { + return 0.0 + } + }, + setOnOrientationUpdated: { f in + setOnOrientationUpdated { value, aspect in + let mappedValue: PresentationCallVideoView.Orientation + switch value { + case .rotation0: + mappedValue = .rotation0 + case .rotation90: + mappedValue = .rotation90 + case .rotation180: + mappedValue = .rotation180 + case .rotation270: + mappedValue = .rotation270 + } + f?(mappedValue, aspect) + } + }, + setOnIsMirroredUpdated: { f in + setOnIsMirroredUpdated { value in + f?(value) + } + }, + updateIsEnabled: { value in + updateIsEnabled(value) + } + ) + var cloneVideoView: PresentationCallVideoView? + if let cloneView = cloneView { + let setOnFirstFrameReceived = cloneView.setOnFirstFrameReceived + let setOnOrientationUpdated = cloneView.setOnOrientationUpdated + let setOnIsMirroredUpdated = cloneView.setOnIsMirroredUpdated + let updateIsEnabled = cloneView.updateIsEnabled + cloneVideoView = PresentationCallVideoView( + holder: cloneView, + view: cloneView.view, + setOnFirstFrameReceived: { f in + setOnFirstFrameReceived(f) + }, + getOrientation: { [weak cloneView] in + if let cloneView = cloneView { + let mappedValue: PresentationCallVideoView.Orientation + switch cloneView.getOrientation() { + case .rotation0: + mappedValue = .rotation0 + case .rotation90: + mappedValue = .rotation90 + case .rotation180: + mappedValue = .rotation180 + case .rotation270: + mappedValue = .rotation270 + } + return mappedValue + } else { + return .rotation0 + } + }, + getAspect: { [weak cloneView] in + if let cloneView = cloneView { + return cloneView.getAspect() + } else { + return 0.0 + } + }, + setOnOrientationUpdated: { f in + setOnOrientationUpdated { value, aspect in + let mappedValue: PresentationCallVideoView.Orientation + switch value { + case .rotation0: + mappedValue = .rotation0 + case .rotation90: + mappedValue = .rotation90 + case .rotation180: + mappedValue = .rotation180 + case .rotation270: + mappedValue = .rotation270 + } + f?(mappedValue, aspect) + } + }, + setOnIsMirroredUpdated: { f in + setOnIsMirroredUpdated { value in + f?(value) + } + }, + updateIsEnabled: { value in + updateIsEnabled(value) + } + ) + } + completion(mainVideoView, cloneVideoView) + } else { + completion(nil, nil) + } + }) + } + public func requestVideo() { if self.videoCapturer == nil { let videoCapturer = OngoingCallVideoCapturer() self.videoCapturer = videoCapturer } - self.isVideo = true + if let videoCapturer = self.videoCapturer { - self.callContext?.requestVideo(videoCapturer) + self.requestVideo(capturer: videoCapturer) + } + } + + func requestVideo(capturer: OngoingCallVideoCapturer) { + self.videoCapturer = capturer + + self.hasVideo = true + if let videoCapturer = self.videoCapturer { + self.genericCallContext?.requestVideo(videoCapturer) + self.isVideoMuted = false + self.isVideoMutedDisposable.set((videoCapturer.isActive + |> distinctUntilChanged + |> deliverOnMainQueue).start(next: { [weak self] value in + guard let strongSelf = self else { + return + } + strongSelf.isVideoMuted = !value + strongSelf.updateLocalVideoState() + })) + + self.updateLocalVideoState() } } public func disableVideo() { - self.isVideo = false + self.hasVideo = false + self.useFrontCamera = true; if let _ = self.videoCapturer { self.videoCapturer = nil - self.callContext?.disableVideo() + self.isVideoMutedDisposable.set(nil) + self.genericCallContext?.disableVideo() + self.isVideoMuted = true + + self.updateLocalVideoState() + } + } + + private func updateLocalVideoState() { + self.participantsContext?.updateVideoState(peerId: self.joinAsPeerId, isVideoMuted: self.videoCapturer == nil, isVideoPaused: self.isVideoMuted, isPresentationPaused: nil) + } + + public func switchVideoCamera() { + self.useFrontCamera = !self.useFrontCamera + self.videoCapturer?.switchVideoInput(isFront: self.useFrontCamera) + } + + private func requestScreencast() { + guard let callInfo = self.internalState.callInfo, self.screencastCallContext == nil else { + return + } + + self.hasScreencast = true + + let screencastCallContext = OngoingGroupCallContext(video: self.screencastCapturer, requestMediaChannelDescriptions: { _, _ in EmptyDisposable }, audioStreamData: nil, rejoinNeeded: { }, outgoingAudioBitrateKbit: nil, videoContentType: .screencast, enableNoiseSuppression: false) + self.screencastCallContext = screencastCallContext + + self.screencastJoinDisposable.set((screencastCallContext.joinPayload + |> take(1) + |> deliverOnMainQueue).start(next: { [weak self] joinPayload in + guard let strongSelf = self else { + return + } + + strongSelf.requestDisposable.set((strongSelf.accountContext.engine.calls.joinGroupCallAsScreencast( + peerId: strongSelf.peerId, + callId: callInfo.id, + accessHash: callInfo.accessHash, + joinPayload: joinPayload.0 + ) + |> deliverOnMainQueue).start(next: { joinCallResult in + guard let strongSelf = self, let screencastCallContext = strongSelf.screencastCallContext else { + return + } + let clientParams = joinCallResult.jsonParams + + screencastCallContext.setConnectionMode(.rtc, keepBroadcastConnectedIfWasEnabled: false) + screencastCallContext.setJoinResponse(payload: clientParams) + }, error: { error in + guard let _ = self else { + return + } + })) + })) + } + + public func disableScreencast() { + self.hasScreencast = false + if let screencastCallContext = self.screencastCallContext { + self.screencastCallContext = nil + screencastCallContext.stop() + + let maybeCallInfo: GroupCallInfo? = self.internalState.callInfo + + if let callInfo = maybeCallInfo { + self.screencastJoinDisposable.set(self.accountContext.engine.calls.leaveGroupCallAsScreencast( + callId: callInfo.id, + accessHash: callInfo.accessHash + ).start()) + } + + self.screencastBufferServerContext?.stopScreencast() } } public func setVolume(peerId: PeerId, volume: Int32, sync: Bool) { - for (ssrc, id) in self.ssrcMapping { - if id == peerId { - self.callContext?.setVolume(ssrc: ssrc, volume: Double(volume) / 10000.0) - if sync { - self.participantsContext?.updateMuteState(peerId: peerId, muteState: nil, volume: volume, raiseHand: nil) - } - break + var found = false + for (ssrc, mapping) in self.ssrcMapping { + if mapping.peerId == peerId { + self.genericCallContext?.setVolume(ssrc: ssrc, volume: Double(volume) / 10000.0) + found = true } } + if found && sync { + self.participantsContext?.updateMuteState(peerId: peerId, muteState: nil, volume: volume, raiseHand: nil) + } } - public func setFullSizeVideo(peerId: PeerId?) { - var resolvedSsrc: UInt32? - if let peerId = peerId { - for (ssrc, id) in self.ssrcMapping { - if id == peerId { - resolvedSsrc = ssrc - break - } + public func setRequestedVideoList(items: [PresentationGroupCallRequestedVideo]) { + self.genericCallContext?.setRequestedVideoChannels(items.compactMap { item -> OngoingGroupCallContext.VideoChannel in + let mappedMinQuality: OngoingGroupCallContext.VideoChannel.Quality + let mappedMaxQuality: OngoingGroupCallContext.VideoChannel.Quality + switch item.minQuality { + case .thumbnail: + mappedMinQuality = .thumbnail + case .medium: + mappedMinQuality = .medium + case .full: + mappedMinQuality = .full } - } - self.callContext?.setFullSizeVideoSsrc(ssrc: resolvedSsrc) + switch item.maxQuality { + case .thumbnail: + mappedMaxQuality = .thumbnail + case .medium: + mappedMaxQuality = .medium + case .full: + mappedMaxQuality = .full + } + return OngoingGroupCallContext.VideoChannel( + audioSsrc: item.audioSsrc, + endpointId: item.endpointId, + ssrcGroups: item.ssrcGroups.map { group in + return OngoingGroupCallContext.VideoChannel.SsrcGroup(semantics: group.semantics, ssrcs: group.ssrcs) + }, + minQuality: mappedMinQuality, + maxQuality: mappedMaxQuality + ) + }) } public func setCurrentAudioOutput(_ output: AudioSessionOutput) { @@ -2095,12 +2860,11 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { } private func requestCall(movingFromBroadcastToRtc: Bool) { - self.currentConnectionMode = .none - self.callContext?.setConnectionMode(.none, keepBroadcastConnectedIfWasEnabled: movingFromBroadcastToRtc) - - self.missingSsrcsDisposable.set(nil) - self.missingSsrcs.removeAll() - self.processedMissingSsrcs.removeAll() + if !self.didInitializeConnectionMode || self.currentConnectionMode != .none { + self.didInitializeConnectionMode = true + self.currentConnectionMode = .none + self.genericCallContext?.setConnectionMode(.none, keepBroadcastConnectedIfWasEnabled: movingFromBroadcastToRtc) + } self.internalState = .requesting self.internalStatePromise.set(.single(.requesting)) @@ -2111,10 +2875,18 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { } let account = self.account - + let context = self.accountContext let currentCall: Signal if let initialCall = self.initialCall { - currentCall = getCurrentGroupCall(account: account, callId: initialCall.id, accessHash: initialCall.accessHash) + currentCall = context.engine.calls.getCurrentGroupCall(callId: initialCall.id, accessHash: initialCall.accessHash) + |> mapError { _ -> CallError in + return .generic + } + |> map { summary -> GroupCallInfo? in + return summary?.info + } + } else if case let .active(callInfo) = self.internalState { + currentCall = context.engine.calls.getCurrentGroupCall(callId: callInfo.id, accessHash: callInfo.accessHash) |> mapError { _ -> CallError in return .generic } @@ -2153,7 +2925,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { } if let value = value { - strongSelf.initialCall = CachedChannelData.ActiveCall(id: value.id, accessHash: value.accessHash, title: value.title) + strongSelf.initialCall = CachedChannelData.ActiveCall(id: value.id, accessHash: value.accessHash, title: value.title, scheduleTimestamp: nil, subscribedToScheduled: false) strongSelf.updateSessionState(internalState: .active(value), audioSessionControl: strongSelf.audioSessionControl) } else { @@ -2163,7 +2935,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { } public func invitePeer(_ peerId: PeerId) -> Bool { - guard case let .established(callInfo, _, _, _, _) = self.internalState, !self.invitedPeersValue.contains(peerId) else { + guard let callInfo = self.internalState.callInfo, !self.invitedPeersValue.contains(peerId) else { return false } @@ -2171,7 +2943,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { updatedInvitedPeers.insert(peerId, at: 0) self.invitedPeersValue = updatedInvitedPeers - let _ = inviteToGroupCall(account: self.account, callId: callInfo.id, accessHash: callInfo.accessHash, peerId: peerId).start() + let _ = self.accountContext.engine.calls.inviteToGroupCall(callId: callInfo.id, accessHash: callInfo.accessHash, peerId: peerId).start() return true } @@ -2182,15 +2954,17 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { self.invitedPeersValue = updatedInvitedPeers } - public func updateTitle(_ title: String){ - guard case let .established(callInfo, _, _, _, _) = self.internalState else { + public func updateTitle(_ title: String) { + guard let callInfo = self.internalState.callInfo else { return } - - let _ = editGroupCallTitle(account: self.account, callId: callInfo.id, accessHash: callInfo.accessHash, title: title).start() + self.stateValue.title = title.isEmpty ? nil : title + let _ = self.accountContext.engine.calls.editGroupCallTitle(callId: callInfo.id, accessHash: callInfo.accessHash, title: title).start() } public var inviteLinks: Signal { + let engine = self.accountContext.engine + return self.state |> map { state -> PeerId in return state.myPeerId @@ -2207,7 +2981,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { } |> mapToSignal { state in if let callInfo = state.callInfo { - return groupCallInviteLinks(account: self.account, callId: callInfo.id, accessHash: callInfo.accessHash) + return engine.calls.groupCallInviteLinks(callId: callInfo.id, accessHash: callInfo.accessHash) } else { return .complete() } @@ -2269,22 +3043,28 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { self.participantsContext?.updateDefaultParticipantsAreMuted(isMuted: isMuted) } - public func makeIncomingVideoView(source: UInt32, completion: @escaping (PresentationCallVideoView?) -> Void) { - self.callContext?.makeIncomingVideoView(source: source, completion: { view in - if let view = view { - let setOnFirstFrameReceived = view.setOnFirstFrameReceived - let setOnOrientationUpdated = view.setOnOrientationUpdated - let setOnIsMirroredUpdated = view.setOnIsMirroredUpdated - completion(PresentationCallVideoView( - holder: view, - view: view.view, + public func makeIncomingVideoView(endpointId: String, requestClone: Bool, completion: @escaping (PresentationCallVideoView?, PresentationCallVideoView?) -> Void) { + if endpointId == self.currentLocalEndpointId { + self.makeOutgoingVideoView(requestClone: requestClone, completion: completion) + return + } + + self.genericCallContext?.makeIncomingVideoView(endpointId: endpointId, requestClone: requestClone, completion: { mainView, cloneView in + if let mainView = mainView { + let setOnFirstFrameReceived = mainView.setOnFirstFrameReceived + let setOnOrientationUpdated = mainView.setOnOrientationUpdated + let setOnIsMirroredUpdated = mainView.setOnIsMirroredUpdated + let updateIsEnabled = mainView.updateIsEnabled + let mainVideoView = PresentationCallVideoView( + holder: mainView, + view: mainView.view, setOnFirstFrameReceived: { f in setOnFirstFrameReceived(f) }, - getOrientation: { [weak view] in - if let view = view { + getOrientation: { [weak mainView] in + if let mainView = mainView { let mappedValue: PresentationCallVideoView.Orientation - switch view.getOrientation() { + switch mainView.getOrientation() { case .rotation0: mappedValue = .rotation0 case .rotation90: @@ -2299,9 +3079,9 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { return .rotation0 } }, - getAspect: { [weak view] in - if let view = view { - return view.getAspect() + getAspect: { [weak mainView] in + if let mainView = mainView { + return mainView.getAspect() } else { return 0.0 } @@ -2326,13 +3106,86 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { setOnIsMirroredUpdated { value in f?(value) } + }, + updateIsEnabled: { value in + updateIsEnabled(value) } - )) + ) + + var cloneVideoView: PresentationCallVideoView? + if let cloneView = cloneView { + let setOnFirstFrameReceived = cloneView.setOnFirstFrameReceived + let setOnOrientationUpdated = cloneView.setOnOrientationUpdated + let setOnIsMirroredUpdated = cloneView.setOnIsMirroredUpdated + let updateIsEnabled = cloneView.updateIsEnabled + cloneVideoView = PresentationCallVideoView( + holder: cloneView, + view: cloneView.view, + setOnFirstFrameReceived: { f in + setOnFirstFrameReceived(f) + }, + getOrientation: { [weak cloneView] in + if let cloneView = cloneView { + let mappedValue: PresentationCallVideoView.Orientation + switch cloneView.getOrientation() { + case .rotation0: + mappedValue = .rotation0 + case .rotation90: + mappedValue = .rotation90 + case .rotation180: + mappedValue = .rotation180 + case .rotation270: + mappedValue = .rotation270 + } + return mappedValue + } else { + return .rotation0 + } + }, + getAspect: { [weak cloneView] in + if let cloneView = cloneView { + return cloneView.getAspect() + } else { + return 0.0 + } + }, + setOnOrientationUpdated: { f in + setOnOrientationUpdated { value, aspect in + let mappedValue: PresentationCallVideoView.Orientation + switch value { + case .rotation0: + mappedValue = .rotation0 + case .rotation90: + mappedValue = .rotation90 + case .rotation180: + mappedValue = .rotation180 + case .rotation270: + mappedValue = .rotation270 + } + f?(mappedValue, aspect) + } + }, + setOnIsMirroredUpdated: { f in + setOnIsMirroredUpdated { value in + f?(value) + } + }, + updateIsEnabled: { value in + updateIsEnabled(value) + } + ) + } + + completion(mainVideoView, cloneVideoView) } else { - completion(nil) + completion(nil, nil) } }) } + + func video(endpointId: String) -> Signal? { + return self.genericCallContext?.video(endpointId: endpointId) + } public func loadMoreMembers(token: String) { self.participantsContext?.loadMore(token: token) diff --git a/submodules/TelegramCallsUI/Sources/SampleBufferVideoRenderingView.swift b/submodules/TelegramCallsUI/Sources/SampleBufferVideoRenderingView.swift new file mode 100644 index 0000000000..a6dd976c83 --- /dev/null +++ b/submodules/TelegramCallsUI/Sources/SampleBufferVideoRenderingView.swift @@ -0,0 +1,144 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display +import SwiftSignalKit +import AccountContext +import TelegramVoip +import AVFoundation + +private func sampleBufferFromPixelBuffer(pixelBuffer: CVPixelBuffer) -> CMSampleBuffer? { + var maybeFormat: CMVideoFormatDescription? + let status = CMVideoFormatDescriptionCreateForImageBuffer(allocator: kCFAllocatorDefault, imageBuffer: pixelBuffer, formatDescriptionOut: &maybeFormat) + if status != noErr { + return nil + } + guard let format = maybeFormat else { + return nil + } + + var timingInfo = CMSampleTimingInfo( + duration: CMTimeMake(value: 1, timescale: 30), + presentationTimeStamp: CMTimeMake(value: 0, timescale: 30), + decodeTimeStamp: CMTimeMake(value: 0, timescale: 30) + ) + + var maybeSampleBuffer: CMSampleBuffer? + let bufferStatus = CMSampleBufferCreateReadyWithImageBuffer(allocator: kCFAllocatorDefault, imageBuffer: pixelBuffer, formatDescription: format, sampleTiming: &timingInfo, sampleBufferOut: &maybeSampleBuffer) + + if (bufferStatus != noErr) { + return nil + } + guard let sampleBuffer = maybeSampleBuffer else { + return nil + } + + let attachments: NSArray = CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, createIfNecessary: true)! as NSArray + let dict: NSMutableDictionary = attachments[0] as! NSMutableDictionary + dict[kCMSampleAttachmentKey_DisplayImmediately as NSString] = true as NSNumber + + return sampleBuffer +} + +final class SampleBufferVideoRenderingView: UIView, VideoRenderingView { + static override var layerClass: AnyClass { + return AVSampleBufferDisplayLayer.self + } + + private var sampleBufferLayer: AVSampleBufferDisplayLayer { + return self.layer as! AVSampleBufferDisplayLayer + } + + private var isEnabled: Bool = false + + private var onFirstFrameReceived: ((Float) -> Void)? + private var onOrientationUpdated: ((PresentationCallVideoView.Orientation, CGFloat) -> Void)? + private var onIsMirroredUpdated: ((Bool) -> Void)? + + private var didReportFirstFrame: Bool = false + private var currentOrientation: PresentationCallVideoView.Orientation = .rotation0 + private var currentAspect: CGFloat = 1.0 + + private var disposable: Disposable? + + init(input: Signal) { + super.init(frame: CGRect()) + + self.disposable = input.start(next: { [weak self] videoFrameData in + Queue.mainQueue().async { + self?.addFrame(videoFrameData) + } + }) + + self.sampleBufferLayer.videoGravity = .resize + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.disposable?.dispose() + } + + private func addFrame(_ videoFrameData: OngoingGroupCallContext.VideoFrameData) { + let aspect = CGFloat(videoFrameData.width) / CGFloat(videoFrameData.height) + var isAspectUpdated = false + if self.currentAspect != aspect { + self.currentAspect = aspect + isAspectUpdated = true + } + + let videoFrameOrientation = PresentationCallVideoView.Orientation(videoFrameData.orientation) + var isOrientationUpdated = false + if self.currentOrientation != videoFrameOrientation { + self.currentOrientation = videoFrameOrientation + isOrientationUpdated = true + } + + if isAspectUpdated || isOrientationUpdated { + self.onOrientationUpdated?(self.currentOrientation, self.currentAspect) + } + + if !self.didReportFirstFrame { + self.didReportFirstFrame = true + self.onFirstFrameReceived?(Float(self.currentAspect)) + } + + if self.isEnabled { + switch videoFrameData.buffer { + case let .native(buffer): + if let sampleBuffer = sampleBufferFromPixelBuffer(pixelBuffer: buffer.pixelBuffer) { + self.sampleBufferLayer.enqueue(sampleBuffer) + } + default: + break + } + } + } + + func setOnFirstFrameReceived(_ f: @escaping (Float) -> Void) { + self.onFirstFrameReceived = f + self.didReportFirstFrame = false + } + + func setOnOrientationUpdated(_ f: @escaping (PresentationCallVideoView.Orientation, CGFloat) -> Void) { + self.onOrientationUpdated = f + } + + func getOrientation() -> PresentationCallVideoView.Orientation { + return self.currentOrientation + } + + func getAspect() -> CGFloat { + return self.currentAspect + } + + func setOnIsMirroredUpdated(_ f: @escaping (Bool) -> Void) { + self.onIsMirroredUpdated = f + } + + func updateIsEnabled(_ isEnabled: Bool) { + self.isEnabled = isEnabled + } +} diff --git a/submodules/TelegramCallsUI/Sources/VideoRenderingContext.swift b/submodules/TelegramCallsUI/Sources/VideoRenderingContext.swift new file mode 100644 index 0000000000..e9bb3e13cd --- /dev/null +++ b/submodules/TelegramCallsUI/Sources/VideoRenderingContext.swift @@ -0,0 +1,71 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display +import SwiftSignalKit +import AccountContext +import TelegramVoip +import AVFoundation + +protocol VideoRenderingView: UIView { + func setOnFirstFrameReceived(_ f: @escaping (Float) -> Void) + func setOnOrientationUpdated(_ f: @escaping (PresentationCallVideoView.Orientation, CGFloat) -> Void) + func getOrientation() -> PresentationCallVideoView.Orientation + func getAspect() -> CGFloat + func setOnIsMirroredUpdated(_ f: @escaping (Bool) -> Void) + func updateIsEnabled(_ isEnabled: Bool) +} + +class VideoRenderingContext { + private var metalContextImpl: Any? + + #if targetEnvironment(simulator) + #else + @available(iOS 13.0, *) + var metalContext: MetalVideoRenderingContext { + if let value = self.metalContextImpl as? MetalVideoRenderingContext { + return value + } else { + let value = MetalVideoRenderingContext()! + self.metalContextImpl = value + return value + } + } + #endif + + func makeView(input: Signal, blur: Bool) -> VideoRenderingView? { + #if targetEnvironment(simulator) + return SampleBufferVideoRenderingView(input: input) + #else + if #available(iOS 13.0, *) { + return MetalVideoRenderingView(renderingContext: self.metalContext, input: input, blur: blur) + } else { + return SampleBufferVideoRenderingView(input: input) + } + #endif + } + + func updateVisibility(isVisible: Bool) { + #if targetEnvironment(simulator) + #else + if #available(iOS 13.0, *) { + self.metalContext.updateVisibility(isVisible: isVisible) + } + #endif + } +} + +extension PresentationCallVideoView.Orientation { + init(_ orientation: OngoingCallVideoOrientation) { + switch orientation { + case .rotation0: + self = .rotation0 + case .rotation90: + self = .rotation90 + case .rotation180: + self = .rotation180 + case .rotation270: + self = .rotation270 + } + } +} diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatActionButton.swift b/submodules/TelegramCallsUI/Sources/VoiceChatActionButton.swift index d53f1fd35b..2b6b756bbf 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatActionButton.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatActionButton.swift @@ -3,7 +3,6 @@ import UIKit import AsyncDisplayKit import Display import SwiftSignalKit -import LegacyComponents import AnimationUI import AppBundle import ManagedAnimationNode @@ -14,8 +13,8 @@ private let subtitleFont = Font.regular(13.0) private let white = UIColor(rgb: 0xffffff) private let greyColor = UIColor(rgb: 0x2c2c2e) private let secondaryGreyColor = UIColor(rgb: 0x1c1c1e) -private let blue = UIColor(rgb: 0x0078ff) -private let lightBlue = UIColor(rgb: 0x59c7f8) +private let blue = UIColor(rgb: 0x007fff) +private let lightBlue = UIColor(rgb: 0x00affe) private let green = UIColor(rgb: 0x33c659) private let activeBlue = UIColor(rgb: 0x00a0b9) private let purple = UIColor(rgb: 0x3252ef) @@ -24,6 +23,11 @@ private let pink = UIColor(rgb: 0xef436c) private let areaSize = CGSize(width: 300.0, height: 300.0) private let blobSize = CGSize(width: 190.0, height: 190.0) +private let smallScale: CGFloat = 0.48 +private let smallIconScale: CGFloat = 0.69 + +private let buttonHeight: CGFloat = 52.0 + final class VoiceChatActionButton: HighlightTrackingButtonNode { enum State: Equatable { enum ActiveState: Equatable { @@ -31,7 +35,15 @@ final class VoiceChatActionButton: HighlightTrackingButtonNode { case muted case on } + + enum ScheduledState: Equatable { + case start + case subscribe + case unsubscribe + } + case button(text: String) + case scheduled(state: ScheduledState) case connecting case active(state: ActiveState) } @@ -48,15 +60,17 @@ final class VoiceChatActionButton: HighlightTrackingButtonNode { private let containerNode: ASDisplayNode private let backgroundNode: VoiceChatActionButtonBackgroundNode private let iconNode: VoiceChatActionButtonIconNode - private let titleLabel: ImmediateTextNode + private let labelContainerNode: ASDisplayNode + let titleLabel: ImmediateTextNode private let subtitleLabel: ImmediateTextNode + private let buttonTitleLabel: ImmediateTextNode private var currentParams: (size: CGSize, buttonSize: CGSize, state: VoiceChatActionButton.State, dark: Bool, small: Bool, title: String, subtitle: String, snap: Bool)? private var activePromise = ValuePromise(false) - private var outerColorPromise = ValuePromise(nil) - var outerColor: Signal { - return outerColorPromise.get() + private var outerColorPromise = Promise<(UIColor?, UIColor?)>((nil, nil)) + var outerColor: Signal<(UIColor?, UIColor?), NoError> { + return self.outerColorPromise.get() } var connectingColor: UIColor = UIColor(rgb: 0xb6b6bb) { @@ -80,13 +94,18 @@ final class VoiceChatActionButton: HighlightTrackingButtonNode { var wasActiveWhenPressed = false var pressing: Bool = false { didSet { - guard let (_, _, state, _, _, _, _, snap) = self.currentParams, !self.isDisabled else { + guard let (_, _, state, _, small, _, _, snap) = self.currentParams, !self.isDisabled else { return } if self.pressing { let transition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .spring) - transition.updateTransformScale(node: self.iconNode, scale: snap ? 0.5 : 0.9) - + if small { + transition.updateTransformScale(node: self.backgroundNode, scale: smallScale * 0.9) + transition.updateTransformScale(node: self.iconNode, scale: smallIconScale * 0.9) + } else { + transition.updateTransformScale(node: self.iconNode, scale: snap ? 0.5 : 0.9) + } + switch state { case let .active(state): switch state { @@ -95,12 +114,17 @@ final class VoiceChatActionButton: HighlightTrackingButtonNode { default: break } - case .connecting: + case .connecting, .button, .scheduled: break } } else { let transition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .spring) - transition.updateTransformScale(node: self.iconNode, scale: snap ? 0.5 : 1.0) + if small { + transition.updateTransformScale(node: self.backgroundNode, scale: smallScale) + transition.updateTransformScale(node: self.iconNode, scale: smallIconScale) + } else { + transition.updateTransformScale(node: self.iconNode, scale: snap ? 0.5 : 1.0) + } self.wasActiveWhenPressed = false } } @@ -108,34 +132,63 @@ final class VoiceChatActionButton: HighlightTrackingButtonNode { init() { self.bottomNode = ASDisplayNode() + self.bottomNode.isUserInteractionEnabled = false self.containerNode = ASDisplayNode() + self.containerNode.isUserInteractionEnabled = false self.backgroundNode = VoiceChatActionButtonBackgroundNode() self.iconNode = VoiceChatActionButtonIconNode(isColored: false) + self.labelContainerNode = ASDisplayNode() self.titleLabel = ImmediateTextNode() self.subtitleLabel = ImmediateTextNode() + self.buttonTitleLabel = ImmediateTextNode() + self.buttonTitleLabel.isUserInteractionEnabled = false + self.buttonTitleLabel.alpha = 0.0 super.init() self.addSubnode(self.bottomNode) - self.addSubnode(self.titleLabel) - self.addSubnode(self.subtitleLabel) - + self.labelContainerNode.addSubnode(self.titleLabel) + self.labelContainerNode.addSubnode(self.subtitleLabel) + self.addSubnode(self.labelContainerNode) + self.addSubnode(self.containerNode) self.containerNode.addSubnode(self.backgroundNode) self.containerNode.addSubnode(self.iconNode) + self.containerNode.addSubnode(self.buttonTitleLabel) + self.highligthedChanged = { [weak self] pressing in if let strongSelf = self { - guard let (_, _, _, _, _, _, _, snap) = strongSelf.currentParams else { + guard let (_, _, state, _, small, _, _, snap) = strongSelf.currentParams else { return } if pressing { - let transition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .spring) - transition.updateTransformScale(node: strongSelf.iconNode, scale: snap ? 0.5 : 0.9) + if case .button = state { + strongSelf.containerNode.layer.removeAnimation(forKey: "opacity") + strongSelf.containerNode.alpha = 0.4 + } else { + let transition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .spring) + if small { + transition.updateTransformScale(node: strongSelf.backgroundNode, scale: smallScale * 0.9) + transition.updateTransformScale(node: strongSelf.iconNode, scale: smallIconScale * 0.9) + } else { + transition.updateTransformScale(node: strongSelf.iconNode, scale: snap ? 0.5 : 0.9) + } + } } else if !strongSelf.pressing { - let transition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .spring) - transition.updateTransformScale(node: strongSelf.iconNode, scale: snap ? 0.5 : 1.0) + if case .button = state { + strongSelf.containerNode.alpha = 1.0 + strongSelf.containerNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) + } else { + let transition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .spring) + if small { + transition.updateTransformScale(node: strongSelf.backgroundNode, scale: smallScale) + transition.updateTransformScale(node: strongSelf.iconNode, scale: smallIconScale) + } else { + transition.updateTransformScale(node: strongSelf.iconNode, scale: snap ? 0.5 : 1.0) + } + } } } } @@ -144,8 +197,8 @@ final class VoiceChatActionButton: HighlightTrackingButtonNode { self?.activePromise.set(active) } - self.backgroundNode.updatedOuterColor = { [weak self] color in - self?.outerColorPromise.set(color) + self.backgroundNode.updatedColors = { [weak self] outerColor, activeColor in + self?.outerColorPromise.set(.single((outerColor, activeColor))) } } @@ -153,7 +206,7 @@ final class VoiceChatActionButton: HighlightTrackingButtonNode { self.activeDisposable.dispose() } - func updateLevel(_ level: CGFloat) { + func updateLevel(_ level: CGFloat, immediately: Bool = false) { self.backgroundNode.audioLevel = level } @@ -191,9 +244,16 @@ final class VoiceChatActionButton: HighlightTrackingButtonNode { let subtitleSize = self.subtitleLabel.updateLayout(CGSize(width: size.width, height: .greatestFiniteMagnitude)) let totalHeight = titleSize.height + subtitleSize.height + 1.0 - self.titleLabel.frame = CGRect(origin: CGPoint(x: floor((size.width - titleSize.width) / 2.0), y: floor(size.height - totalHeight / 2.0) - 70.0), size: titleSize) - self.subtitleLabel.frame = CGRect(origin: CGPoint(x: floor((size.width - subtitleSize.width) / 2.0), y: self.titleLabel.frame.maxY + 1.0), size: subtitleSize) - + self.labelContainerNode.frame = CGRect(origin: CGPoint(), size: size) + + let titleLabelFrame = CGRect(origin: CGPoint(x: floor((size.width - titleSize.width) / 2.0), y: floor((size.height - totalHeight) / 2.0) + 84.0), size: titleSize) + let subtitleLabelFrame = CGRect(origin: CGPoint(x: floor((size.width - subtitleSize.width) / 2.0), y: titleLabelFrame.maxY + 1.0), size: subtitleSize) + + self.titleLabel.bounds = CGRect(origin: CGPoint(), size: titleLabelFrame.size) + self.titleLabel.position = titleLabelFrame.center + self.subtitleLabel.bounds = CGRect(origin: CGPoint(), size: subtitleLabelFrame.size) + self.subtitleLabel.position = subtitleLabelFrame.center + self.bottomNode.frame = CGRect(origin: CGPoint(), size: size) self.containerNode.frame = CGRect(origin: CGPoint(), size: size) @@ -209,7 +269,7 @@ final class VoiceChatActionButton: HighlightTrackingButtonNode { default: break } - case .connecting: + case .connecting, .button, .scheduled: break } @@ -221,11 +281,24 @@ final class VoiceChatActionButton: HighlightTrackingButtonNode { transition.updateAlpha(node: self.subtitleLabel, alpha: 0.0) transition.updateAlpha(layer: self.backgroundNode.maskProgressLayer, alpha: 0.0) } else { - let transition: ContainedViewLayoutTransition = animated ? .animated(duration: 0.2, curve: .easeInOut) : .immediate - transition.updateTransformScale(node: self.backgroundNode, scale: small ? 0.85 : 1.0, delay: 0.05) - transition.updateTransformScale(node: self.iconNode, scale: self.pressing ? 0.9 : 1.0, delay: 0.05) - transition.updateAlpha(node: self.titleLabel, alpha: 1.0, delay: 0.05) - transition.updateAlpha(node: self.subtitleLabel, alpha: 1.0, delay: 0.05) + let transition: ContainedViewLayoutTransition = animated ? .animated(duration: 0.4, curve: .spring) : .immediate + if small { + transition.updateTransformScale(node: self.backgroundNode, scale: self.pressing ? smallScale * 0.9 : smallScale, delay: 0.0) + transition.updateTransformScale(node: self.iconNode, scale: self.pressing ? smallIconScale * 0.9 : smallIconScale, delay: 0.0) + transition.updateAlpha(node: self.titleLabel, alpha: 0.0) + transition.updateAlpha(node: self.subtitleLabel, alpha: 0.0) + transition.updateSublayerTransformOffset(layer: self.labelContainerNode.layer, offset: CGPoint(x: 0.0, y: -43.0)) + transition.updateTransformScale(node: self.titleLabel, scale: 0.8) + transition.updateTransformScale(node: self.subtitleLabel, scale: 0.8) + } else { + transition.updateTransformScale(node: self.backgroundNode, scale: 1.0, delay: 0.0) + transition.updateTransformScale(node: self.iconNode, scale: self.pressing ? 0.9 : 1.0, delay: 0.0) + transition.updateAlpha(node: self.titleLabel, alpha: 1.0, delay: 0.05) + transition.updateAlpha(node: self.subtitleLabel, alpha: 1.0, delay: 0.05) + transition.updateSublayerTransformOffset(layer: self.labelContainerNode.layer, offset: CGPoint()) + transition.updateTransformScale(node: self.titleLabel, scale: 1.0) + transition.updateTransformScale(node: self.subtitleLabel, scale: 1.0) + } transition.updateAlpha(layer: self.backgroundNode.maskProgressLayer, alpha: 1.0) } @@ -236,12 +309,23 @@ final class VoiceChatActionButton: HighlightTrackingButtonNode { private var previousIcon: VoiceChatActionButtonIconAnimationState? private func applyIconParams() { - guard let (_, _, state, _, _, _, _, snap) = self.currentParams else { + guard let (_, _, state, _, _, _, _, _) = self.currentParams else { return } let icon: VoiceChatActionButtonIconAnimationState switch state { + case .button: + icon = .empty + case let .scheduled(state): + switch state { + case .start: + icon = .start + case .subscribe: + icon = .subscribe + case .unsubscribe: + icon = .unsubscribe + } case let .active(state): switch state { case .on: @@ -261,7 +345,6 @@ final class VoiceChatActionButton: HighlightTrackingButtonNode { self.previousIcon = icon self.iconNode.enqueueState(icon) -// self.iconNode.update(state: VoiceChatMicrophoneNode.State(muted: iconMuted, filled: true, color: iconColor), animated: true) } func update(snap: Bool, animated: Bool) { @@ -269,7 +352,7 @@ final class VoiceChatActionButton: HighlightTrackingButtonNode { self.currentParams = (previous.size, previous.buttonSize, previous.state, previous.dark, previous.small, previous.title, previous.subtitle, snap) self.backgroundNode.isSnap = snap - self.backgroundNode.glowHidden = snap + self.backgroundNode.glowHidden = snap || previous.small self.backgroundNode.updateColors() self.applyParams(animated: animated) self.applyIconParams() @@ -283,8 +366,30 @@ final class VoiceChatActionButton: HighlightTrackingButtonNode { self.statePromise.set(state) + if let previousState = previousState, case .button = previousState, case .scheduled = state { + self.buttonTitleLabel.alpha = 0.0 + self.buttonTitleLabel.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2) + self.buttonTitleLabel.layer.animateScale(from: 1.0, to: 0.001, duration: 0.24) + + self.iconNode.alpha = 1.0 + self.iconNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + self.iconNode.layer.animateSpring(from: 0.01 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.42, damping: 104.0) + } + var backgroundState: VoiceChatActionButtonBackgroundNode.State + var animated = true switch state { + case let .button(text): + backgroundState = .button + self.buttonTitleLabel.alpha = 1.0 + self.buttonTitleLabel.attributedText = NSAttributedString(string: text, font: Font.semibold(17.0), textColor: .white) + let titleSize = self.buttonTitleLabel.updateLayout(CGSize(width: size.width, height: 100.0)) + self.buttonTitleLabel.frame = CGRect(origin: CGPoint(x: floor((self.bounds.width - titleSize.width) / 2.0), y: floor((self.bounds.height - titleSize.height) / 2.0)), size: titleSize) + case .scheduled: + backgroundState = .disabled + if previousState == .connecting { + animated = false + } case let .active(state): switch state { case .on: @@ -299,8 +404,9 @@ final class VoiceChatActionButton: HighlightTrackingButtonNode { } self.applyIconParams() + self.backgroundNode.glowHidden = (self.currentParams?.snap ?? false) || small self.backgroundNode.isDark = dark - self.backgroundNode.update(state: backgroundState, animated: true) + self.backgroundNode.update(state: backgroundState, animated: animated) if case .active = state, let previousState = previousState, case .connecting = previousState, animated { self.activeDisposable.set((self.activePromise.get() @@ -311,14 +417,18 @@ final class VoiceChatActionButton: HighlightTrackingButtonNode { } })) } else { - applyParams(animated: animated) + self.applyParams(animated: animated) } } override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { var hitRect = self.bounds - if let (_, buttonSize, _, _, _, _, _, _) = self.currentParams { - hitRect = self.bounds.insetBy(dx: (self.bounds.width - buttonSize.width) / 2.0, dy: (self.bounds.height - buttonSize.height) / 2.0) + if let (_, buttonSize, state, _, _, _, _, _) = self.currentParams { + if case .button = state { + hitRect = CGRect(x: 0.0, y: floor((self.bounds.height - buttonHeight) / 2.0), width: self.bounds.width, height: buttonHeight) + } else { + hitRect = self.bounds.insetBy(dx: (self.bounds.width - buttonSize.width) / 2.0, dy: (self.bounds.height - buttonSize.height) / 2.0) + } } let result = super.hitTest(point, with: event) if !hitRect.contains(point) { @@ -424,6 +534,7 @@ private final class VoiceChatActionButtonBackgroundNode: ASDisplayNode { enum State: Equatable { case connecting case disabled + case button case blob(Bool) } @@ -434,12 +545,14 @@ private final class VoiceChatActionButtonBackgroundNode: ASDisplayNode { var audioLevel: CGFloat = 0.0 { didSet { - self.maskBlobView.updateLevel(audioLevel) + self.maskBlobView.updateLevel(self.audioLevel, immediately: false) } } + + var updatedActive: ((Bool) -> Void)? - var updatedOuterColor: ((UIColor?) -> Void)? + var updatedColors: ((UIColor?, UIColor?) -> Void)? private let backgroundCircleLayer = CAShapeLayer() private let foregroundCircleLayer = CAShapeLayer() @@ -495,7 +608,7 @@ private final class VoiceChatActionButtonBackgroundNode: ASDisplayNode { self.foregroundGradientLayer.type = .radial self.foregroundGradientLayer.colors = [lightBlue.cgColor, blue.cgColor, blue.cgColor] - self.foregroundGradientLayer.locations = [0.0, 0.85, 1.0] + self.foregroundGradientLayer.locations = [0.0, 0.55, 1.0] self.foregroundGradientLayer.startPoint = CGPoint(x: 1.0, y: 0.0) self.foregroundGradientLayer.endPoint = CGPoint(x: 0.0, y: 1.0) @@ -517,9 +630,11 @@ private final class VoiceChatActionButtonBackgroundNode: ASDisplayNode { self.maskProgressLayer.lineCap = .round self.maskProgressLayer.path = path - let largerCirclePath = UIBezierPath(ovalIn: CGRect(origin: CGPoint(), size: CGSize(width: buttonSize.width + progressLineWidth, height: buttonSize.height + progressLineWidth))).cgPath - self.maskCircleLayer.fillColor = white.cgColor + let circleFrame = CGRect(origin: CGPoint(x: (areaSize.width - buttonSize.width) / 2.0, y: (areaSize.height - buttonSize.height) / 2.0), size: buttonSize).insetBy(dx: -progressLineWidth / 2.0, dy: -progressLineWidth / 2.0) + let largerCirclePath = UIBezierPath(roundedRect: CGRect(x: circleFrame.minX, y: circleFrame.minY, width: circleFrame.width, height: circleFrame.height), cornerRadius: circleFrame.width / 2.0).cgPath + self.maskCircleLayer.path = largerCirclePath + self.maskCircleLayer.fillColor = white.cgColor self.maskCircleLayer.isHidden = true updateInHierarchy = { [weak self] value in @@ -560,11 +675,11 @@ private final class VoiceChatActionButtonBackgroundNode: ASDisplayNode { let previousValue = self.foregroundGradientLayer.startPoint let newValue: CGPoint if self.maskBlobView.presentationAudioLevel > 0.22 { - newValue = CGPoint(x: CGFloat.random(in: 0.9 ..< 1.0), y: CGFloat.random(in: 0.1 ..< 0.35)) + newValue = CGPoint(x: CGFloat.random(in: 0.9 ..< 1.0), y: CGFloat.random(in: 0.15 ..< 0.35)) } else if self.maskBlobView.presentationAudioLevel > 0.01 { - newValue = CGPoint(x: CGFloat.random(in: 0.77 ..< 0.95), y: CGFloat.random(in: 0.1 ..< 0.35)) + newValue = CGPoint(x: CGFloat.random(in: 0.57 ..< 0.85), y: CGFloat.random(in: 0.15 ..< 0.45)) } else { - newValue = CGPoint(x: CGFloat.random(in: 0.65 ..< 0.85), y: CGFloat.random(in: 0.1 ..< 0.45)) + newValue = CGPoint(x: CGFloat.random(in: 0.6 ..< 0.75), y: CGFloat.random(in: 0.25 ..< 0.45)) } self.foregroundGradientLayer.startPoint = newValue @@ -663,7 +778,7 @@ private final class VoiceChatActionButtonBackgroundNode: ASDisplayNode { case muted } - func updateGlowAndGradientAnimations(type: Gradient, previousType: Gradient? = nil) { + func updateGlowAndGradientAnimations(type: Gradient, previousType: Gradient? = nil, animated: Bool = true) { let effectivePreviousTyoe = previousType ?? .active let scale: CGFloat @@ -677,37 +792,44 @@ private final class VoiceChatActionButtonBackgroundNode: ASDisplayNode { let initialColors = self.foregroundGradientLayer.colors let outerColor: UIColor? + let activeColor: UIColor? let targetColors: [CGColor] let targetScale: CGFloat switch type { case .speaking: targetColors = [activeBlue.cgColor, green.cgColor, green.cgColor] targetScale = 0.89 - outerColor = UIColor(rgb: 0x21674f) + outerColor = UIColor(rgb: 0x134b22) + activeColor = green case .active: targetColors = [lightBlue.cgColor, blue.cgColor, blue.cgColor] targetScale = 0.85 - outerColor = UIColor(rgb: 0x1d588d) + outerColor = UIColor(rgb: 0x002e5d) + activeColor = blue case .connecting: targetColors = [lightBlue.cgColor, blue.cgColor, blue.cgColor] targetScale = 0.3 outerColor = nil + activeColor = blue case .muted: targetColors = [pink.cgColor, purple.cgColor, purple.cgColor] targetScale = 0.85 - outerColor = UIColor(rgb: 0x3b3474) + outerColor = UIColor(rgb: 0x24306b) + activeColor = purple } - self.updatedOuterColor?(outerColor) + self.updatedColors?(outerColor, activeColor) self.maskGradientLayer.transform = CATransform3DMakeScale(targetScale, targetScale, 1.0) if let _ = previousType { self.maskGradientLayer.animateScale(from: initialScale, to: targetScale, duration: 0.3) - } else { + } else if animated { self.maskGradientLayer.animateSpring(from: initialScale as NSNumber, to: targetScale as NSNumber, keyPath: "transform.scale", duration: 0.45) } self.foregroundGradientLayer.colors = targetColors - self.foregroundGradientLayer.animate(from: initialColors as AnyObject, to: targetColors as AnyObject, keyPath: "colors", timingFunction: CAMediaTimingFunctionName.linear.rawValue, duration: 0.3) + if animated { + self.foregroundGradientLayer.animate(from: initialColors as AnyObject, to: targetColors as AnyObject, keyPath: "colors", timingFunction: CAMediaTimingFunctionName.linear.rawValue, duration: 0.3) + } } private func playMuteAnimation() { @@ -796,7 +918,7 @@ private final class VoiceChatActionButtonBackgroundNode: ASDisplayNode { self.maskBlobView.startAnimating() self.maskBlobView.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.45) } - + private func playConnectionAnimation(type: Gradient, completion: @escaping () -> Void) { CATransaction.begin() let initialRotation: CGFloat = CGFloat((self.maskProgressLayer.value(forKeyPath: "presentationLayer.transform.rotation.z") as? NSNumber)?.floatValue ?? 0.0) @@ -843,7 +965,8 @@ private final class VoiceChatActionButtonBackgroundNode: ASDisplayNode { self.updateGlowAndGradientAnimations(type: type, previousType: nil) - if case .blob = self.state { + if case .connecting = self.state { + } else { self.maskBlobView.isHidden = false self.maskBlobView.startAnimating() self.maskBlobView.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.45) @@ -878,6 +1001,53 @@ private final class VoiceChatActionButtonBackgroundNode: ASDisplayNode { CATransaction.commit() } + private var maskIsCircle = true + private func setupButtonAnimation() { + CATransaction.begin() + CATransaction.setDisableActions(true) + self.backgroundCircleLayer.isHidden = true + self.foregroundCircleLayer.isHidden = true + self.maskCircleLayer.isHidden = false + self.maskProgressLayer.isHidden = true + self.maskGradientLayer.isHidden = true + + let path = UIBezierPath(roundedRect: CGRect(x: 0.0, y: floor((self.bounds.height - buttonHeight) / 2.0), width: self.bounds.width, height: buttonHeight), cornerRadius: 10.0).cgPath + self.maskCircleLayer.path = path + self.maskIsCircle = false + + CATransaction.commit() + + self.updateGlowAndGradientAnimations(type: .muted, previousType: nil) + + self.updatedActive?(true) + } + + private func playScheduledAnimation() { + CATransaction.begin() + CATransaction.setDisableActions(true) + self.maskGradientLayer.isHidden = false + CATransaction.commit() + + let circleFrame = CGRect(origin: CGPoint(x: (self.bounds.width - buttonSize.width) / 2.0, y: (self.bounds.height - buttonSize.height) / 2.0), size: buttonSize).insetBy(dx: -progressLineWidth / 2.0, dy: -progressLineWidth / 2.0) + let largerCirclePath = UIBezierPath(roundedRect: CGRect(x: circleFrame.minX, y: circleFrame.minY, width: circleFrame.width, height: circleFrame.height), cornerRadius: circleFrame.width / 2.0).cgPath + + let previousPath = self.maskCircleLayer.path + self.maskCircleLayer.path = largerCirclePath + self.maskIsCircle = true + + self.maskCircleLayer.animateSpring(from: previousPath as AnyObject, to: largerCirclePath as AnyObject, keyPath: "path", duration: 0.6, initialVelocity: 0.0, damping: 100.0) + + self.maskBlobView.isHidden = false + self.maskBlobView.startAnimating() + self.maskBlobView.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.6, damping: 100.0) + + self.disableGlowAnimations = true + self.maskGradientLayer.removeAllAnimations() + self.maskGradientLayer.animateSpring(from: 0.3 as NSNumber, to: 0.85 as NSNumber, keyPath: "transform.scale", duration: 0.45, completion: { [weak self] _ in + self?.disableGlowAnimations = false + }) + } + var isActive = false func updateAnimations() { if !self.isCurrentlyInHierarchy { @@ -930,7 +1100,9 @@ private final class VoiceChatActionButtonBackgroundNode: ASDisplayNode { self.isActive = false if let transition = self.transition { - if case .connecting = transition { + if case .button = transition { + self.playScheduledAnimation() + } else if case .connecting = transition { self.playConnectionAnimation(type: .muted) { [weak self] in self?.isActive = false } @@ -939,8 +1111,21 @@ private final class VoiceChatActionButtonBackgroundNode: ASDisplayNode { self.playMuteAnimation() } self.transition = nil + } else { + if self.maskBlobView.isHidden { + self.updateGlowAndGradientAnimations(type: .muted, previousType: nil, animated: false) + self.maskCircleLayer.isHidden = false + self.maskProgressLayer.isHidden = true + self.maskGradientLayer.isHidden = false + self.maskBlobView.isHidden = false + self.maskBlobView.startAnimating() + self.maskBlobView.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.45) + } } - break + case .button: + self.updatedActive?(true) + self.isActive = false + self.setupButtonAnimation() } } @@ -1005,23 +1190,41 @@ private final class VoiceChatActionButtonBackgroundNode: ASDisplayNode { self.updateAnimations() } + var previousSize: CGSize? override func layout() { super.layout() - let center = CGPoint(x: self.bounds.width / 2.0, y: self.bounds.height / 2.0) + let sizeUpdated = self.previousSize != self.bounds.size + self.previousSize = self.bounds.size - let circleFrame = CGRect(origin: CGPoint(x: (self.bounds.width - buttonSize.width) / 2.0, y: (self.bounds.height - buttonSize.height) / 2.0), size: buttonSize) + let bounds = CGRect(x: (self.bounds.width - areaSize.width) / 2.0, y: (self.bounds.height - areaSize.height) / 2.0, width: areaSize.width, height: areaSize.height) + let center = bounds.center + + self.maskBlobView.frame = CGRect(origin: CGPoint(x: bounds.minX + (bounds.width - blobSize.width) / 2.0, y: bounds.minY + (bounds.height - blobSize.height) / 2.0), size: blobSize) + + let circleFrame = CGRect(origin: CGPoint(x: bounds.minX + (bounds.width - buttonSize.width) / 2.0, y: bounds.minY + (bounds.height - buttonSize.height) / 2.0), size: buttonSize) self.backgroundCircleLayer.frame = circleFrame self.foregroundCircleLayer.position = center self.foregroundCircleLayer.bounds = CGRect(origin: CGPoint(), size: CGSize(width: circleFrame.width - progressLineWidth, height: circleFrame.height - progressLineWidth)) self.growingForegroundCircleLayer.position = center self.growingForegroundCircleLayer.bounds = self.foregroundCircleLayer.bounds - self.maskCircleLayer.frame = circleFrame.insetBy(dx: -progressLineWidth / 2.0, dy: -progressLineWidth / 2.0) + self.maskCircleLayer.frame = self.bounds + + if sizeUpdated && self.maskIsCircle { + CATransaction.begin() + CATransaction.setDisableActions(true) + let circleFrame = CGRect(origin: CGPoint(x: (self.bounds.width - buttonSize.width) / 2.0, y: (self.bounds.height - buttonSize.height) / 2.0), size: buttonSize).insetBy(dx: -progressLineWidth / 2.0, dy: -progressLineWidth / 2.0) + let largerCirclePath = UIBezierPath(roundedRect: CGRect(x: circleFrame.minX, y: circleFrame.minY, width: circleFrame.width, height: circleFrame.height), cornerRadius: circleFrame.width / 2.0).cgPath + + self.maskCircleLayer.path = largerCirclePath + CATransaction.commit() + } + self.maskProgressLayer.frame = circleFrame.insetBy(dx: -3.0, dy: -3.0) self.foregroundView.frame = self.bounds self.foregroundGradientLayer.frame = self.bounds self.maskGradientLayer.position = center - self.maskGradientLayer.bounds = self.bounds + self.maskGradientLayer.bounds = bounds self.maskView.frame = self.bounds } } @@ -1094,17 +1297,20 @@ private final class VoiceBlobView: UIView { } public func setColor(_ color: UIColor) { - mediumBlob.setColor(color.withAlphaComponent(0.55)) - bigBlob.setColor(color.withAlphaComponent(0.35)) + mediumBlob.setColor(color.withAlphaComponent(0.5)) + bigBlob.setColor(color.withAlphaComponent(0.21)) } - public func updateLevel(_ level: CGFloat) { + public func updateLevel(_ level: CGFloat, immediately: Bool) { let normalizedLevel = min(1, max(level / maxLevel, 0)) mediumBlob.updateSpeedLevel(to: normalizedLevel) bigBlob.updateSpeedLevel(to: normalizedLevel) audioLevel = normalizedLevel + if immediately { + presentationAudioLevel = normalizedLevel + } } public func startAnimating() { @@ -1168,12 +1374,14 @@ final class BlobView: UIView { var level: CGFloat = 0 { didSet { - CATransaction.begin() - CATransaction.setDisableActions(true) - let lv = minScale + (maxScale - minScale) * level - shapeLayer.transform = CATransform3DMakeScale(lv, lv, 1) - self.scaleUpdated?(level) - CATransaction.commit() + if abs(self.level - oldValue) > 0.01 { + CATransaction.begin() + CATransaction.setDisableActions(true) + let lv = self.minScale + (self.maxScale - self.minScale) * self.level + self.shapeLayer.transform = CATransform3DMakeScale(lv, lv, 1) + self.scaleUpdated?(self.level) + CATransaction.commit() + } } } @@ -1185,30 +1393,7 @@ final class BlobView: UIView { layer.strokeColor = nil return layer }() - - private var transition: CGFloat = 0 { - didSet { - guard let currentPoints = currentPoints else { return } - - shapeLayer.path = UIBezierPath.smoothCurve(through: currentPoints, length: bounds.width, smoothness: smoothness).cgPath - } - } - - private var fromPoints: [CGPoint]? - private var toPoints: [CGPoint]? - - private var currentPoints: [CGPoint]? { - guard let fromPoints = fromPoints, let toPoints = toPoints else { return nil } - return fromPoints.enumerated().map { offset, fromPoint in - let toPoint = toPoints[offset] - return CGPoint( - x: fromPoint.x + (toPoint.x - fromPoint.x) * transition, - y: fromPoint.y + (toPoint.y - fromPoint.y) * transition - ) - } - } - init( pointsCount: Int, minRandomness: CGFloat, @@ -1231,9 +1416,9 @@ final class BlobView: UIView { super.init(frame: .zero) - layer.addSublayer(shapeLayer) + self.layer.addSublayer(self.shapeLayer) - shapeLayer.transform = CATransform3DMakeScale(minScale, minScale, 1) + self.shapeLayer.transform = CATransform3DMakeScale(minScale, minScale, 1) } required init?(coder: NSCoder) { @@ -1241,11 +1426,11 @@ final class BlobView: UIView { } func setColor(_ color: UIColor) { - shapeLayer.fillColor = color.cgColor + self.shapeLayer.fillColor = color.cgColor } func updateSpeedLevel(to newSpeedLevel: CGFloat) { - speedLevel = max(speedLevel, newSpeedLevel) + self.speedLevel = max(self.speedLevel, newSpeedLevel) // if abs(lastSpeedLevel - newSpeedLevel) > 0.45 { // animateToNewShape() @@ -1253,57 +1438,41 @@ final class BlobView: UIView { } func startAnimating() { - animateToNewShape() + self.animateToNewShape() } func stopAnimating() { - fromPoints = currentPoints - toPoints = nil - pop_removeAnimation(forKey: "blob") + self.shapeLayer.removeAnimation(forKey: "path") } private func animateToNewShape() { - if pop_animation(forKey: "blob") != nil { - fromPoints = currentPoints - toPoints = nil - pop_removeAnimation(forKey: "blob") + if self.shapeLayer.path == nil { + let points = generateNextBlob(for: self.bounds.size) + self.shapeLayer.path = UIBezierPath.smoothCurve(through: points, length: bounds.width, smoothness: smoothness).cgPath } - if fromPoints == nil { - fromPoints = generateNextBlob(for: bounds.size) - } - if toPoints == nil { - toPoints = generateNextBlob(for: bounds.size) - } + let nextPoints = generateNextBlob(for: self.bounds.size) + let nextPath = UIBezierPath.smoothCurve(through: nextPoints, length: bounds.width, smoothness: smoothness).cgPath - let animation = POPBasicAnimation() - animation.property = POPAnimatableProperty.property(withName: "blob.transition", initializer: { property in - property?.readBlock = { blobView, values in - guard let blobView = blobView as? BlobView, let values = values else { return } - - values.pointee = blobView.transition - } - property?.writeBlock = { blobView, values in - guard let blobView = blobView as? BlobView, let values = values else { return } - - blobView.transition = values.pointee - } - }) as? POPAnimatableProperty - animation.completionBlock = { [weak self] animation, finished in + let animation = CABasicAnimation(keyPath: "path") + let previousPath = self.shapeLayer.path + self.shapeLayer.path = nextPath + animation.duration = CFTimeInterval(1.0 / (minSpeed + (maxSpeed - minSpeed) * speedLevel)) + animation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut) + animation.fromValue = previousPath + animation.toValue = nextPath + animation.isRemovedOnCompletion = false + animation.fillMode = .forwards + animation.completion = { [weak self] finished in if finished { - self?.fromPoints = self?.currentPoints - self?.toPoints = nil self?.animateToNewShape() } } - animation.duration = CFTimeInterval(1 / (minSpeed + (maxSpeed - minSpeed) * speedLevel)) - animation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut) - animation.fromValue = 0 - animation.toValue = 1 - pop_add(animation, forKey: "blob") + + self.shapeLayer.add(animation, forKey: "path") - lastSpeedLevel = speedLevel - speedLevel = 0 + self.lastSpeedLevel = self.speedLevel + self.speedLevel = 0 } // MARK: Helpers @@ -1357,6 +1526,10 @@ final class BlobView: UIView { } enum VoiceChatActionButtonIconAnimationState: Equatable { + case empty + case start + case subscribe + case unsubscribe case unmute case mute case hand @@ -1381,30 +1554,77 @@ final class VoiceChatActionButtonIconNode: ManagedAnimationNode { let previousState = self.iconState self.iconState = state + if state != .empty { + self.alpha = 1.0 + } switch previousState { + case .empty: + switch state { + case .start: + self.trackTo(item: ManagedAnimationItem(source: .local("VoiceStart"), frames: .range(startFrame: 0, endFrame: 0), duration: 0.001)) + default: + break + } + case .subscribe: + switch state { + case .unsubscribe: + self.trackTo(item: ManagedAnimationItem(source: .local("VoiceCancelReminder"))) + case .mute: + self.trackTo(item: ManagedAnimationItem(source: .local("VoiceSetReminderToMute"))) + case .hand: + self.trackTo(item: ManagedAnimationItem(source: .local("VoiceSetReminderToRaiseHand"))) + default: + break + } + case .unsubscribe: + switch state { + case .subscribe: + self.trackTo(item: ManagedAnimationItem(source: .local("VoiceSetReminder"))) + case .mute: + self.trackTo(item: ManagedAnimationItem(source: .local("VoiceCancelReminderToMute"))) + case .hand: + self.trackTo(item: ManagedAnimationItem(source: .local("VoiceCancelReminderToRaiseHand"))) + default: + break + } + case .start: + switch state { + case .mute: + self.trackTo(item: ManagedAnimationItem(source: .local("VoiceStart"))) + default: + break + } case .unmute: switch state { case .mute: self.trackTo(item: ManagedAnimationItem(source: .local("VoiceMute"))) case .hand: - self.trackTo(item: ManagedAnimationItem(source: .local("VoiceHandOff2"))) - case .unmute: + self.trackTo(item: ManagedAnimationItem(source: .local("VoiceUnmuteToRaiseHand"))) + default: break } case .mute: switch state { + case .start: + self.trackTo(item: ManagedAnimationItem(source: .local("VoiceStart"), frames: .range(startFrame: 0, endFrame: 0), duration: 0.001)) case .unmute: - self.trackTo(item: ManagedAnimationItem(source: .local("VoiceUnmute"), frames: .range(startFrame: 0, endFrame: 12), duration: 0.2)) + self.trackTo(item: ManagedAnimationItem(source: .local("VoiceUnmute"))) case .hand: - self.trackTo(item: ManagedAnimationItem(source: .local("VoiceHandOff"))) - case .mute: + self.trackTo(item: ManagedAnimationItem(source: .local("VoiceMuteToRaiseHand"))) + case .subscribe: + self.trackTo(item: ManagedAnimationItem(source: .local("VoiceSetReminderToRaiseHand"), frames: .range(startFrame: 0, endFrame: 0), duration: 0.001)) + case .unsubscribe: + self.trackTo(item: ManagedAnimationItem(source: .local("VoiceCancelReminderToRaiseHand"), frames: .range(startFrame: 0, endFrame: 0), duration: 0.001)) + case .empty: + self.alpha = 0.0 + default: break } case .hand: switch state { case .mute, .unmute: - self.trackTo(item: ManagedAnimationItem(source: .local("VoiceHandOn"))) - case .hand: + self.trackTo(item: ManagedAnimationItem(source: .local("VoiceRaiseHandToMute"))) + default: break } } @@ -1417,15 +1637,25 @@ final class VoiceChatActionButtonIconNode: ManagedAnimationNode { } var useTiredAnimation = false + var useAngryAnimation = false let val = Float.random(in: 0.0..<1.0) if val <= 0.01 { useTiredAnimation = true + } else if val <= 0.05 { + useAngryAnimation = true } - let normalAnimations = ["VoiceHand_1", "VoiceHand_2", "VoiceHand_3", "VoiceHand_4", "VoiceHand_7"] + let normalAnimations = ["VoiceHand_1", "VoiceHand_2", "VoiceHand_3", "VoiceHand_4", "VoiceHand_7", "VoiceHand_8"] let tiredAnimations = ["VoiceHand_5", "VoiceHand_6"] - let animations = useTiredAnimation ? tiredAnimations : normalAnimations - + let angryAnimations = ["VoiceHand_9", "VoiceHand_10"] + let animations: [String] + if useTiredAnimation { + animations = tiredAnimations + } else if useAngryAnimation { + animations = angryAnimations + } else { + animations = normalAnimations + } if let animationName = animations.randomElement() { self.trackTo(item: ManagedAnimationItem(source: .local(animationName))) } diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatActionItem.swift b/submodules/TelegramCallsUI/Sources/VoiceChatActionItem.swift index 16638681c3..d547845763 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatActionItem.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatActionItem.swift @@ -55,7 +55,7 @@ class VoiceChatActionItem: ListViewItem { 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 = VoiceChatActionItemNode() - let (layout, apply) = node.asyncLayout()(self, params, false, nextItem == nil) + let (layout, apply) = node.asyncLayout()(self, params, previousItem == nil || previousItem is VoiceChatTilesGridItem, nextItem == nil) node.contentSize = layout.contentSize node.insets = layout.insets @@ -74,7 +74,7 @@ class VoiceChatActionItem: ListViewItem { let makeLayout = nodeValue.asyncLayout() async { - let (layout, apply) = makeLayout(self, params, false, nextItem == nil) + let (layout, apply) = makeLayout(self, params, previousItem == nil || previousItem is VoiceChatTilesGridItem, nextItem == nil) Queue.mainQueue().async { completion(layout, { _ in apply() @@ -95,6 +95,7 @@ class VoiceChatActionItem: ListViewItem { class VoiceChatActionItemNode: ListViewItemNode { private let topStripeNode: ASDisplayNode private let bottomStripeNode: ASDisplayNode + private let highlightContainerNode: ASDisplayNode private let highlightedBackgroundNode: ASDisplayNode private let iconNode: ASImageNode @@ -121,13 +122,17 @@ class VoiceChatActionItemNode: ListViewItemNode { self.iconNode.displayWithoutProcessing = true self.iconNode.displaysAsynchronously = false + self.highlightContainerNode = ASDisplayNode() + self.highlightContainerNode.clipsToBounds = true + self.highlightedBackgroundNode = ASDisplayNode() - self.highlightedBackgroundNode.isLayerBacked = true self.activateArea = AccessibilityAreaNode() super.init(layerBacked: false, dynamicBounce: false) + self.highlightContainerNode.addSubnode(self.highlightedBackgroundNode) + self.addSubnode(self.iconNode) self.addSubnode(self.titleNode) self.addSubnode(self.activateArea) @@ -138,30 +143,42 @@ class VoiceChatActionItemNode: ListViewItemNode { } } - func asyncLayout() -> (_ item: VoiceChatActionItem, _ params: ListViewItemLayoutParams, _ firstWithHeader: Bool, _ last: Bool) -> (ListViewItemNodeLayout, () -> Void) { + override func didLoad() { + super.didLoad() + + if #available(iOS 13.0, *) { + self.highlightContainerNode.layer.cornerCurve = .continuous + } + } + + func asyncLayout() -> (_ item: VoiceChatActionItem, _ params: ListViewItemLayoutParams, _ first: Bool, _ last: Bool) -> (ListViewItemNodeLayout, () -> Void) { let makeTitleLayout = TextNode.asyncLayout(self.titleNode) let currentItem = self.item - return { item, params, firstWithHeader, last in + return { item, params, first, last in var updatedTheme: PresentationTheme? + var updatedContent = false if currentItem?.presentationData.theme !== item.presentationData.theme { updatedTheme = item.presentationData.theme } + if currentItem?.title != item.title { + updatedContent = true + } let titleFont = Font.regular(17.0) - var leftInset: CGFloat = 16.0 + params.leftInset + var leftInset: CGFloat = 8.0 + params.leftInset if case .generic = item.icon { leftInset += 49.0 } - let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.title, font: titleFont, textColor: UIColor(rgb: 0xffffff)), 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 contentHeight: CGFloat = 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 insets = UIEdgeInsets() let separatorHeight = UIScreenPixel let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) @@ -170,6 +187,10 @@ class VoiceChatActionItemNode: ListViewItemNode { if let strongSelf = self { strongSelf.item = item + guard params.width > 0.0 else { + return + } + 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)) @@ -178,7 +199,9 @@ class VoiceChatActionItemNode: ListViewItemNode { strongSelf.bottomStripeNode.backgroundColor = UIColor(rgb: 0xffffff, alpha: 0.08) strongSelf.highlightedBackgroundNode.backgroundColor = item.presentationData.theme.list.itemHighlightedBackgroundColor - strongSelf.iconNode.image = generateTintedImage(image: item.icon.image, color: UIColor(rgb: 0xffffff)) + strongSelf.iconNode.image = generateTintedImage(image: item.icon.image, color: item.presentationData.theme.list.itemAccentColor) + } else if updatedContent { + strongSelf.iconNode.image = generateTintedImage(image: item.icon.image, color: item.presentationData.theme.list.itemAccentColor) } let _ = titleApply() @@ -186,7 +209,7 @@ class VoiceChatActionItemNode: ListViewItemNode { let titleOffset = leftInset let hideBottomStripe: Bool = last if let image = item.icon.image { - let 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) + let iconFrame = 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) strongSelf.iconNode.frame = iconFrame } @@ -202,7 +225,11 @@ class VoiceChatActionItemNode: ListViewItemNode { strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: leftInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - leftInset, height: separatorHeight)) - strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: titleOffset, y: floor((contentSize.height - titleLayout.size.height) / 2.0)), size: titleLayout.size) + strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: titleOffset + 1.0, y: floor((contentSize.height - titleLayout.size.height) / 2.0)), size: titleLayout.size) + + strongSelf.highlightContainerNode.frame = CGRect(origin: CGPoint(x: params.leftInset, y: -UIScreenPixel), size: CGSize(width: params.width - params.leftInset - params.rightInset, height: layout.contentSize.height + UIScreenPixel + UIScreenPixel + 11.0)) + + strongSelf.highlightContainerNode.cornerRadius = first ? 11.0 : 0.0 strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: contentSize.height + UIScreenPixel + UIScreenPixel)) } @@ -214,8 +241,8 @@ class VoiceChatActionItemNode: ListViewItemNode { super.setHighlighted(highlighted, at: point, animated: animated) if highlighted { - self.highlightedBackgroundNode.alpha = 1.0 - if self.highlightedBackgroundNode.supernode == nil { + self.highlightContainerNode.alpha = 1.0 + if self.highlightContainerNode.supernode == nil { var anchorNode: ASDisplayNode? if self.bottomStripeNode.supernode != nil { anchorNode = self.bottomStripeNode @@ -223,24 +250,24 @@ class VoiceChatActionItemNode: ListViewItemNode { anchorNode = self.topStripeNode } if let anchorNode = anchorNode { - self.insertSubnode(self.highlightedBackgroundNode, aboveSubnode: anchorNode) + self.insertSubnode(self.highlightContainerNode, aboveSubnode: anchorNode) } else { - self.addSubnode(self.highlightedBackgroundNode) + self.addSubnode(self.highlightContainerNode) } } } else { - if self.highlightedBackgroundNode.supernode != nil { + if self.highlightContainerNode.supernode != nil { if animated { - self.highlightedBackgroundNode.layer.animateAlpha(from: self.highlightedBackgroundNode.alpha, to: 0.0, duration: 0.4, completion: { [weak self] completed in + self.highlightContainerNode.layer.animateAlpha(from: self.highlightContainerNode.alpha, to: 0.0, duration: 0.4, completion: { [weak self] completed in if let strongSelf = self { if completed { - strongSelf.highlightedBackgroundNode.removeFromSupernode() + strongSelf.highlightContainerNode.removeFromSupernode() } } }) - self.highlightedBackgroundNode.alpha = 0.0 + self.highlightContainerNode.alpha = 0.0 } else { - self.highlightedBackgroundNode.removeFromSupernode() + self.highlightContainerNode.removeFromSupernode() } } } @@ -254,7 +281,7 @@ class VoiceChatActionItemNode: ListViewItemNode { self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false) } - override public func header() -> ListViewItemHeader? { + override public func headers() -> [ListViewItemHeader]? { return nil } } diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatCameraPreviewController.swift b/submodules/TelegramCallsUI/Sources/VoiceChatCameraPreviewController.swift new file mode 100644 index 0000000000..05b5d1185a --- /dev/null +++ b/submodules/TelegramCallsUI/Sources/VoiceChatCameraPreviewController.swift @@ -0,0 +1,588 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import Postbox +import TelegramCore +import SyncCore +import SwiftSignalKit +import AccountContext +import TelegramPresentationData +import SolidRoundedButtonNode +import PresentationDataUtils +import UIKitRuntimeUtils +import ReplayKit + +private let accentColor: UIColor = UIColor(rgb: 0x007aff) + +final class VoiceChatCameraPreviewController: ViewController { + private var controllerNode: VoiceChatCameraPreviewControllerNode { + return self.displayNode as! VoiceChatCameraPreviewControllerNode + } + + private let context: AccountContext + + private var animatedIn = false + + private let cameraNode: GroupVideoNode + private let shareCamera: (ASDisplayNode, Bool) -> Void + private let switchCamera: () -> Void + + private var presentationDataDisposable: Disposable? + + init(context: AccountContext, cameraNode: GroupVideoNode, shareCamera: @escaping (ASDisplayNode, Bool) -> Void, switchCamera: @escaping () -> Void) { + self.context = context + self.cameraNode = cameraNode + self.shareCamera = shareCamera + self.switchCamera = switchCamera + + super.init(navigationBarPresentationData: nil) + + self.statusBar.statusBarStyle = .Ignore + + self.blocksBackgroundWhenInOverlay = true + + self.presentationDataDisposable = (context.sharedContext.presentationData + |> deliverOnMainQueue).start(next: { [weak self] presentationData in + if let strongSelf = self { + strongSelf.controllerNode.updatePresentationData(presentationData) + } + }) + + self.statusBar.statusBarStyle = .Ignore + } + + required init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.presentationDataDisposable?.dispose() + } + + override public func loadDisplayNode() { + self.displayNode = VoiceChatCameraPreviewControllerNode(controller: self, context: self.context, cameraNode: self.cameraNode) + self.controllerNode.shareCamera = { [weak self] unmuted in + if let strongSelf = self { + strongSelf.shareCamera(strongSelf.cameraNode, unmuted) + strongSelf.dismiss() + } + } + self.controllerNode.switchCamera = { [weak self] in + self?.switchCamera() + self?.cameraNode.flip(withBackground: false) + } + self.controllerNode.dismiss = { [weak self] in + self?.presentingViewController?.dismiss(animated: false, completion: nil) + } + self.controllerNode.cancel = { [weak self] in + self?.dismiss() + } + } + + override public func loadView() { + super.loadView() + } + + override public func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + if !self.animatedIn { + self.animatedIn = true + self.controllerNode.animateIn() + } + } + + override public func dismiss(completion: (() -> Void)? = nil) { + self.controllerNode.animateOut(completion: completion) + } + + override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + super.containerLayoutUpdated(layout, transition: transition) + + self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition) + } +} + +private class VoiceChatCameraPreviewControllerNode: ViewControllerTracingNode, UIScrollViewDelegate { + private weak var controller: VoiceChatCameraPreviewController? + private let context: AccountContext + private var presentationData: PresentationData + + private let cameraNode: GroupVideoNode + private let dimNode: ASDisplayNode + private let wrappingScrollNode: ASScrollNode + private let contentContainerNode: ASDisplayNode + private let effectNode: ASDisplayNode + private let backgroundNode: ASDisplayNode + private let contentBackgroundNode: ASDisplayNode + private let titleNode: ASTextNode + private let previewContainerNode: ASDisplayNode + private let shimmerNode: ShimmerEffectForegroundNode + private let cameraButton: SolidRoundedButtonNode + private let screenButton: SolidRoundedButtonNode + private var broadcastPickerView: UIView? + private let cancelButton: SolidRoundedButtonNode + + private let microphoneButton: HighlightTrackingButtonNode + private let microphoneEffectView: UIVisualEffectView + private let microphoneIconNode: VoiceChatMicrophoneNode + + private let switchCameraButton: HighlightTrackingButtonNode + private let switchCameraEffectView: UIVisualEffectView + private let switchCameraIconNode: ASImageNode + + private var containerLayout: (ContainerViewLayout, CGFloat)? + + private var applicationStateDisposable: Disposable? + + private let hapticFeedback = HapticFeedback() + + private let readyDisposable = MetaDisposable() + + var shareCamera: ((Bool) -> Void)? + var switchCamera: (() -> Void)? + var dismiss: (() -> Void)? + var cancel: (() -> Void)? + + init(controller: VoiceChatCameraPreviewController, context: AccountContext, cameraNode: GroupVideoNode) { + self.controller = controller + self.context = context + self.presentationData = context.sharedContext.currentPresentationData.with { $0 } + + self.cameraNode = cameraNode + + self.wrappingScrollNode = ASScrollNode() + self.wrappingScrollNode.view.alwaysBounceVertical = true + self.wrappingScrollNode.view.delaysContentTouches = false + self.wrappingScrollNode.view.canCancelContentTouches = true + + self.dimNode = ASDisplayNode() + self.dimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.5) + + self.contentContainerNode = ASDisplayNode() + self.contentContainerNode.isOpaque = false + + self.backgroundNode = ASDisplayNode() + self.backgroundNode.clipsToBounds = true + self.backgroundNode.cornerRadius = 16.0 + + let backgroundColor = UIColor(rgb: 0x1c1c1e) + let textColor: UIColor = .white + let buttonColor: UIColor = UIColor(rgb: 0x2b2b2f) + let buttonTextColor: UIColor = .white + let blurStyle: UIBlurEffect.Style = .dark + + self.effectNode = ASDisplayNode(viewBlock: { + return UIVisualEffectView(effect: UIBlurEffect(style: blurStyle)) + }) + + self.contentBackgroundNode = ASDisplayNode() + self.contentBackgroundNode.backgroundColor = backgroundColor + + let title = self.presentationData.strings.VoiceChat_VideoPreviewTitle + + self.titleNode = ASTextNode() + self.titleNode.attributedText = NSAttributedString(string: title, font: Font.bold(17.0), textColor: textColor) + + self.cameraButton = SolidRoundedButtonNode(theme: SolidRoundedButtonTheme(backgroundColor: accentColor, foregroundColor: .white), font: .bold, height: 52.0, cornerRadius: 11.0, gloss: false) + self.cameraButton.title = self.presentationData.strings.VoiceChat_VideoPreviewShareCamera + + self.screenButton = SolidRoundedButtonNode(theme: SolidRoundedButtonTheme(backgroundColor: buttonColor, foregroundColor: buttonTextColor), font: .bold, height: 52.0, cornerRadius: 11.0, gloss: false) + self.screenButton.title = self.presentationData.strings.VoiceChat_VideoPreviewShareScreen + + if #available(iOS 12.0, *) { + let broadcastPickerView = RPSystemBroadcastPickerView(frame: CGRect(x: 0, y: 0, width: 50, height: 52.0)) + broadcastPickerView.alpha = 0.02 + broadcastPickerView.preferredExtension = "\(self.context.sharedContext.applicationBindings.appBundleId).BroadcastUpload" + broadcastPickerView.showsMicrophoneButton = false + self.broadcastPickerView = broadcastPickerView + } + + self.cancelButton = SolidRoundedButtonNode(theme: SolidRoundedButtonTheme(backgroundColor: buttonColor, foregroundColor: buttonTextColor), font: .regular, height: 52.0, cornerRadius: 11.0, gloss: false) + self.cancelButton.title = self.presentationData.strings.Common_Cancel + + self.previewContainerNode = ASDisplayNode() + self.previewContainerNode.clipsToBounds = true + self.previewContainerNode.cornerRadius = 11.0 + self.previewContainerNode.backgroundColor = UIColor(rgb: 0x2b2b2f) + + self.shimmerNode = ShimmerEffectForegroundNode(size: 200.0) + self.previewContainerNode.addSubnode(self.shimmerNode) + + self.microphoneButton = HighlightTrackingButtonNode() + self.microphoneButton.isSelected = true + self.microphoneEffectView = UIVisualEffectView(effect: UIBlurEffect(style: .dark)) + self.microphoneEffectView.clipsToBounds = true + self.microphoneEffectView.layer.cornerRadius = 24.0 + self.microphoneEffectView.isUserInteractionEnabled = false + + self.microphoneIconNode = VoiceChatMicrophoneNode() + self.microphoneIconNode.update(state: .init(muted: false, filled: true, color: .white), animated: false) + + self.switchCameraButton = HighlightTrackingButtonNode() + self.switchCameraEffectView = UIVisualEffectView(effect: UIBlurEffect(style: .dark)) + self.switchCameraEffectView.clipsToBounds = true + self.switchCameraEffectView.layer.cornerRadius = 24.0 + self.switchCameraEffectView.isUserInteractionEnabled = false + + self.switchCameraIconNode = ASImageNode() + self.switchCameraIconNode.displaysAsynchronously = false + self.switchCameraIconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Call/SwitchCameraIcon"), color: .white) + self.switchCameraIconNode.contentMode = .center + + super.init() + + self.backgroundColor = nil + self.isOpaque = false + + self.dimNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:)))) + self.addSubnode(self.dimNode) + + self.wrappingScrollNode.view.delegate = self + self.addSubnode(self.wrappingScrollNode) + + self.wrappingScrollNode.addSubnode(self.backgroundNode) + self.wrappingScrollNode.addSubnode(self.contentContainerNode) + + self.backgroundNode.addSubnode(self.effectNode) + self.backgroundNode.addSubnode(self.contentBackgroundNode) + self.contentContainerNode.addSubnode(self.titleNode) + self.contentContainerNode.addSubnode(self.cameraButton) + self.contentContainerNode.addSubnode(self.screenButton) + if let broadcastPickerView = self.broadcastPickerView { + self.contentContainerNode.view.addSubview(broadcastPickerView) + } + self.contentContainerNode.addSubnode(self.cancelButton) + + self.contentContainerNode.addSubnode(self.previewContainerNode) + + self.previewContainerNode.addSubnode(self.cameraNode) + self.previewContainerNode.addSubnode(self.microphoneButton) + self.microphoneButton.view.addSubview(self.microphoneEffectView) + self.microphoneButton.addSubnode(self.microphoneIconNode) + self.previewContainerNode.addSubnode(self.switchCameraButton) + self.switchCameraButton.view.addSubview(self.switchCameraEffectView) + self.switchCameraButton.addSubnode(self.switchCameraIconNode) + + self.cameraButton.pressed = { [weak self] in + if let strongSelf = self { + strongSelf.shareCamera?(strongSelf.microphoneButton.isSelected) + } + } + self.cancelButton.pressed = { [weak self] in + if let strongSelf = self { + strongSelf.cancel?() + } + } + + self.microphoneButton.addTarget(self, action: #selector(self.microphonePressed), forControlEvents: .touchUpInside) + self.microphoneButton.highligthedChanged = { [weak self] highlighted in + if let strongSelf = self { + if highlighted { + let transition: ContainedViewLayoutTransition = .animated(duration: 0.3, curve: .spring) + transition.updateSublayerTransformScale(node: strongSelf.microphoneButton, scale: 0.9) + } else { + let transition: ContainedViewLayoutTransition = .animated(duration: 0.5, curve: .spring) + transition.updateSublayerTransformScale(node: strongSelf.microphoneButton, scale: 1.0) + } + } + } + + self.switchCameraButton.addTarget(self, action: #selector(self.switchCameraPressed), forControlEvents: .touchUpInside) + self.switchCameraButton.highligthedChanged = { [weak self] highlighted in + if let strongSelf = self { + if highlighted { + let transition: ContainedViewLayoutTransition = .animated(duration: 0.3, curve: .spring) + transition.updateSublayerTransformScale(node: strongSelf.switchCameraButton, scale: 0.9) + } else { + let transition: ContainedViewLayoutTransition = .animated(duration: 0.5, curve: .spring) + transition.updateSublayerTransformScale(node: strongSelf.switchCameraButton, scale: 1.0) + } + } + } + + self.readyDisposable.set(self.cameraNode.ready.start(next: { [weak self] ready in + if let strongSelf = self, ready { + Queue.mainQueue().after(0.07) { + strongSelf.shimmerNode.alpha = 0.0 + strongSelf.shimmerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3) + } + } + })) + } + + deinit { + self.readyDisposable.dispose() + self.applicationStateDisposable?.dispose() + } + + @objc private func microphonePressed() { + self.hapticFeedback.impact(.light) + self.microphoneButton.isSelected = !self.microphoneButton.isSelected + self.microphoneIconNode.update(state: .init(muted: !self.microphoneButton.isSelected, filled: true, color: .white), animated: true) + } + + @objc private func switchCameraPressed() { + self.hapticFeedback.impact(.light) + self.switchCamera?() + + let springDuration: Double = 0.7 + let springDamping: CGFloat = 100.0 + self.switchCameraButton.isUserInteractionEnabled = false + self.switchCameraIconNode.layer.animateSpring(from: 0.0 as NSNumber, to: CGFloat.pi as NSNumber, keyPath: "transform.rotation.z", duration: springDuration, damping: springDamping, completion: { [weak self] _ in + self?.switchCameraButton.isUserInteractionEnabled = true + }) + } + + func updatePresentationData(_ presentationData: PresentationData) { + self.presentationData = presentationData + } + + override func didLoad() { + super.didLoad() + + if #available(iOSApplicationExtension 11.0, iOS 11.0, *) { + self.wrappingScrollNode.view.contentInsetAdjustmentBehavior = .never + } + } + + @objc func dimTapGesture(_ recognizer: UITapGestureRecognizer) { + if case .ended = recognizer.state { + self.cancel?() + } + } + + func animateIn() { + self.dimNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) + + let offset = self.bounds.size.height - self.contentBackgroundNode.frame.minY + let dimPosition = self.dimNode.layer.position + + let transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring) + let targetBounds = self.bounds + self.bounds = self.bounds.offsetBy(dx: 0.0, dy: -offset) + self.dimNode.position = CGPoint(x: dimPosition.x, y: dimPosition.y - offset) + transition.animateView({ + self.bounds = targetBounds + self.dimNode.position = dimPosition + }) + + self.applicationStateDisposable = (self.context.sharedContext.applicationBindings.applicationIsActive + |> filter { !$0 } + |> take(1) + |> deliverOnMainQueue).start(next: { [weak self] _ in + guard let strongSelf = self else { + return + } + strongSelf.controller?.dismiss() + }) + } + + func animateOut(completion: (() -> Void)? = nil) { + var dimCompleted = false + var offsetCompleted = false + + let internalCompletion: () -> Void = { [weak self] in + if let strongSelf = self, dimCompleted && offsetCompleted { + strongSelf.dismiss?() + } + completion?() + } + + self.dimNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { _ in + dimCompleted = true + internalCompletion() + }) + + let offset = self.bounds.size.height - self.contentBackgroundNode.frame.minY + let dimPosition = self.dimNode.layer.position + self.dimNode.layer.animatePosition(from: dimPosition, to: CGPoint(x: dimPosition.x, y: dimPosition.y - offset), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) + self.layer.animateBoundsOriginYAdditive(from: 0.0, to: -offset, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in + offsetCompleted = true + internalCompletion() + }) + } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if self.bounds.contains(point) { + if !self.contentBackgroundNode.bounds.contains(self.convert(point, to: self.contentBackgroundNode)) { + return self.dimNode.view + } + } + return super.hitTest(point, with: event) + } + + func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { + let contentOffset = scrollView.contentOffset + let additionalTopHeight = max(0.0, -contentOffset.y) + + if additionalTopHeight >= 30.0 { + self.cancel?() + } + } + + func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { + self.containerLayout = (layout, navigationBarHeight) + + let isLandscape: Bool + if layout.size.width > layout.size.height { + isLandscape = true + } else { + isLandscape = false + } + let isTablet: Bool + if case .regular = layout.metrics.widthClass { + isTablet = true + } else { + isTablet = false + } + + var insets = layout.insets(options: [.statusBar, .input]) + let cleanInsets = layout.insets(options: [.statusBar]) + insets.top = max(10.0, insets.top) + + var buttonOffset: CGFloat = 60.0 + if let _ = self.broadcastPickerView { + buttonOffset *= 2.0 + } + let bottomInset: CGFloat = isTablet ? 31.0 : 10.0 + cleanInsets.bottom + let titleHeight: CGFloat = 54.0 + var contentHeight = titleHeight + bottomInset + 52.0 + 17.0 + let innerContentHeight: CGFloat = layout.size.height - contentHeight - 160.0 + var width = horizontalContainerFillingSizeForLayout(layout: layout, sideInset: layout.safeInsets.left) + if isLandscape { + if isTablet { + width = 870.0 + contentHeight = 690.0 + } else { + contentHeight = layout.size.height + width = layout.size.width + } + } else { + if isTablet { + width = 600.0 + contentHeight = 960.0 + } else { + contentHeight = titleHeight + bottomInset + 52.0 + 17.0 + innerContentHeight + buttonOffset + } + } + + let previewInset: CGFloat = 16.0 + let sideInset = floor((layout.size.width - width) / 2.0) + let contentFrame: CGRect + if isTablet { + contentFrame = CGRect(origin: CGPoint(x: sideInset, y: floor((layout.size.height - contentHeight) / 2.0)), size: CGSize(width: width, height: contentHeight)) + } else { + contentFrame = CGRect(origin: CGPoint(x: sideInset, y: layout.size.height - contentHeight), size: CGSize(width: width, height: contentHeight)) + } + var backgroundFrame = CGRect(origin: CGPoint(x: contentFrame.minX, y: contentFrame.minY), size: CGSize(width: contentFrame.width, height: contentFrame.height)) + if !isTablet { + backgroundFrame.size.height += 2000.0 + } + if backgroundFrame.minY < contentFrame.minY { + backgroundFrame.origin.y = contentFrame.minY + } + transition.updateAlpha(node: self.titleNode, alpha: isLandscape && !isTablet ? 0.0 : 1.0) + transition.updateFrame(node: self.backgroundNode, frame: backgroundFrame) + transition.updateFrame(node: self.effectNode, frame: CGRect(origin: CGPoint(), size: backgroundFrame.size)) + transition.updateFrame(node: self.contentBackgroundNode, frame: CGRect(origin: CGPoint(), size: backgroundFrame.size)) + transition.updateFrame(node: self.wrappingScrollNode, frame: CGRect(origin: CGPoint(), size: layout.size)) + transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(), size: layout.size)) + + let titleSize = self.titleNode.measure(CGSize(width: width, height: titleHeight)) + let titleFrame = CGRect(origin: CGPoint(x: floor((contentFrame.width - titleSize.width) / 2.0), y: 18.0), size: titleSize) + transition.updateFrame(node: self.titleNode, frame: titleFrame) + + var previewSize: CGSize + var previewFrame: CGRect + if isLandscape { + let previewHeight = contentHeight - 21.0 - 52.0 - 10.0 + previewSize = CGSize(width: min(contentFrame.width - layout.safeInsets.left - layout.safeInsets.right, previewHeight * 1.7778), height: previewHeight) + if isTablet { + previewSize.width -= previewInset * 2.0 + previewSize.height -= 46.0 + } + previewFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((contentFrame.width - previewSize.width) / 2.0), y: 0.0), size: previewSize) + if isTablet { + previewFrame.origin.y += 56.0 + } + } else { + previewSize = CGSize(width: contentFrame.width - previewInset * 2.0, height: contentHeight - 243.0 - bottomInset + (120.0 - buttonOffset)) + if isTablet { + previewSize.height += 17.0 + } + previewFrame = CGRect(origin: CGPoint(x: previewInset, y: 56.0), size: previewSize) + } + transition.updateFrame(node: self.previewContainerNode, frame: previewFrame) + transition.updateFrame(node: self.shimmerNode, frame: CGRect(origin: CGPoint(), size: previewFrame.size)) + self.shimmerNode.update(foregroundColor: UIColor(rgb: 0xffffff, alpha: 0.07)) + self.shimmerNode.updateAbsoluteRect(previewFrame, within: layout.size) + + self.cameraNode.frame = CGRect(origin: CGPoint(), size: previewSize) + self.cameraNode.updateLayout(size: previewSize, layoutMode: isLandscape ? .fillHorizontal : .fillVertical, transition: .immediate) + + let microphoneFrame = CGRect(x: 16.0, y: previewSize.height - 48.0 - 16.0, width: 48.0, height: 48.0) + transition.updateFrame(node: self.microphoneButton, frame: microphoneFrame) + transition.updateFrame(view: self.microphoneEffectView, frame: CGRect(origin: CGPoint(), size: microphoneFrame.size)) + transition.updateFrameAsPositionAndBounds(node: self.microphoneIconNode, frame: CGRect(origin: CGPoint(x: 1.0, y: 0.0), size: microphoneFrame.size).insetBy(dx: 6.0, dy: 6.0)) + self.microphoneIconNode.transform = CATransform3DMakeScale(1.2, 1.2, 1.0) + + let switchCameraFrame = CGRect(x: previewSize.width - 48.0 - 16.0, y: previewSize.height - 48.0 - 16.0, width: 48.0, height: 48.0) + transition.updateFrame(node: self.switchCameraButton, frame: switchCameraFrame) + transition.updateFrame(view: self.switchCameraEffectView, frame: CGRect(origin: CGPoint(), size: switchCameraFrame.size)) + transition.updateFrame(node: self.switchCameraIconNode, frame: CGRect(origin: CGPoint(), size: switchCameraFrame.size)) + + if isLandscape { + var buttonsCount: Int = 2 + if let _ = self.broadcastPickerView { + buttonsCount += 1 + } else { + self.screenButton.isHidden = true + } + + let buttonInset: CGFloat = 6.0 + var leftButtonInset = buttonInset + let availableWidth: CGFloat + if isTablet { + availableWidth = contentFrame.width - layout.safeInsets.left - layout.safeInsets.right - previewInset * 2.0 + leftButtonInset += previewInset + } else { + availableWidth = contentFrame.width - layout.safeInsets.left - layout.safeInsets.right + } + let buttonWidth = floorToScreenPixels((availableWidth - CGFloat(buttonsCount + 1) * buttonInset) / CGFloat(buttonsCount)) + + let cameraButtonHeight = self.cameraButton.updateLayout(width: buttonWidth, transition: transition) + let screenButtonHeight = self.screenButton.updateLayout(width: buttonWidth, transition: transition) + let cancelButtonHeight = self.cancelButton.updateLayout(width: buttonWidth, transition: transition) + + transition.updateFrame(node: self.cancelButton, frame: CGRect(x: layout.safeInsets.left + leftButtonInset, y: previewFrame.maxY + 10.0, width: buttonWidth, height: cancelButtonHeight)) + if let broadcastPickerView = self.broadcastPickerView { + transition.updateFrame(node: self.screenButton, frame: CGRect(x: layout.safeInsets.left + leftButtonInset + buttonWidth + buttonInset, y: previewFrame.maxY + 10.0, width: buttonWidth, height: screenButtonHeight)) + broadcastPickerView.frame = CGRect(x: layout.safeInsets.left + leftButtonInset + buttonWidth + buttonInset, y: previewFrame.maxY + 10.0, width: buttonWidth, height: screenButtonHeight) + transition.updateFrame(node: self.cameraButton, frame: CGRect(x: layout.safeInsets.left + leftButtonInset + buttonWidth + buttonInset + buttonWidth + buttonInset, y: previewFrame.maxY + 10.0, width: buttonWidth, height: cameraButtonHeight)) + } else { + transition.updateFrame(node: self.cameraButton, frame: CGRect(x: layout.safeInsets.left + leftButtonInset + buttonWidth + buttonInset, y: previewFrame.maxY + 10.0, width: buttonWidth, height: cameraButtonHeight)) + } + + } else { + let bottomInset = isTablet ? 21.0 : insets.bottom + 16.0 + let buttonInset: CGFloat = 16.0 + let cameraButtonHeight = self.cameraButton.updateLayout(width: contentFrame.width - buttonInset * 2.0, transition: transition) + transition.updateFrame(node: self.cameraButton, frame: CGRect(x: buttonInset, y: contentHeight - cameraButtonHeight - bottomInset - buttonOffset, width: contentFrame.width, height: cameraButtonHeight)) + + let screenButtonHeight = self.screenButton.updateLayout(width: contentFrame.width - buttonInset * 2.0, transition: transition) + transition.updateFrame(node: self.screenButton, frame: CGRect(x: buttonInset, y: contentHeight - cameraButtonHeight - 8.0 - screenButtonHeight - bottomInset, width: contentFrame.width, height: screenButtonHeight)) + if let broadcastPickerView = self.broadcastPickerView { + broadcastPickerView.frame = CGRect(x: buttonInset, y: contentHeight - cameraButtonHeight - 8.0 - screenButtonHeight - bottomInset, width: contentFrame.width + 1000.0, height: screenButtonHeight) + } else { + self.screenButton.isHidden = true + } + + let cancelButtonHeight = self.cancelButton.updateLayout(width: contentFrame.width - buttonInset * 2.0, transition: transition) + transition.updateFrame(node: self.cancelButton, frame: CGRect(x: buttonInset, y: contentHeight - cancelButtonHeight - bottomInset, width: contentFrame.width, height: cancelButtonHeight)) + } + + transition.updateFrame(node: self.contentContainerNode, frame: contentFrame) + } +} diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatController.swift b/submodules/TelegramCallsUI/Sources/VoiceChatController.swift index f5fe1cbff4..42249b143f 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatController.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatController.swift @@ -5,6 +5,7 @@ import AsyncDisplayKit import SwiftSignalKit import TelegramPresentationData import TelegramUIPreferences +import TelegramStringFormatting import TelegramVoip import TelegramAudio import AccountContext @@ -24,15 +25,46 @@ import DirectionalPanGesture import PeerInfoUI import AvatarNode import TooltipUI +import LegacyUI +import LegacyComponents +import LegacyMediaPickerUI +import WebSearchUI +import MapResourceToAvatarSizes +import SolidRoundedButtonNode +import AudioBlob +import DeviceAccess -private let panelBackgroundColor = UIColor(rgb: 0x1c1c1e) -private let secondaryPanelBackgroundColor = UIColor(rgb: 0x2c2c2e) -private let fullscreenBackgroundColor = UIColor(rgb: 0x000000) -private let dimColor = UIColor(white: 0.0, alpha: 0.5) +let panelBackgroundColor = UIColor(rgb: 0x1c1c1e) +let secondaryPanelBackgroundColor = UIColor(rgb: 0x2c2c2e) +let fullscreenBackgroundColor = UIColor(rgb: 0x000000) +private let smallButtonSize = CGSize(width: 36.0, height: 36.0) private let sideButtonSize = CGSize(width: 56.0, height: 56.0) -private let bottomAreaHeight: CGFloat = 205.0 +private let topPanelHeight: CGFloat = 63.0 +let bottomAreaHeight: CGFloat = 206.0 +private let fullscreenBottomAreaHeight: CGFloat = 80.0 +private let bottomGradientHeight: CGFloat = 70.0 -private func cornersImage(top: Bool, bottom: Bool, dark: Bool) -> UIImage? { +public struct VoiceChatConfiguration { + static var defaultValue: VoiceChatConfiguration { + return VoiceChatConfiguration(videoParticipantsMaxCount: 30) + } + + public let videoParticipantsMaxCount: Int32 + + fileprivate init(videoParticipantsMaxCount: Int32) { + self.videoParticipantsMaxCount = videoParticipantsMaxCount + } + + static func with(appConfiguration: AppConfiguration) -> VoiceChatConfiguration { + if let data = appConfiguration.data, let value = data["groupcall_video_participants_max"] as? Double { + return VoiceChatConfiguration(videoParticipantsMaxCount: Int32(value)) + } else { + return .defaultValue + } + } +} + +func decorationCornersImage(top: Bool, bottom: Bool, dark: Bool) -> UIImage? { if !top && !bottom { return nil } @@ -58,270 +90,184 @@ private func cornersImage(top: Bool, bottom: Bool, dark: Bool) -> UIImage? { })?.stretchableImage(withLeftCapWidth: 25, topCapHeight: 25) } +func decorationTopCornersImage(dark: Bool) -> UIImage? { + return generateImage(CGSize(width: 50.0, height: 110.0), rotatedContext: { (size, context) in + let bounds = CGRect(origin: CGPoint(), size: size) + context.setFillColor((dark ? fullscreenBackgroundColor : panelBackgroundColor).cgColor) + context.fill(bounds) + + context.setBlendMode(.clear) + + var corners: UIRectCorner = [] + corners.insert(.topLeft) + corners.insert(.topRight) -private final class VoiceChatControllerTitleNode: ASDisplayNode { - private var theme: PresentationTheme - - private let titleNode: ASTextNode - private let infoNode: ASTextNode - fileprivate let recordingIconNode: VoiceChatRecordingIconNode - - public var isRecording: Bool = false { - didSet { - self.recordingIconNode.isHidden = !self.isRecording - } + let path = UIBezierPath(roundedRect: CGRect(x: 0.0, y: 60.0, width: 50.0, height: 50.0), byRoundingCorners: corners, cornerRadii: CGSize(width: 11.0, height: 11.0)) + context.addPath(path.cgPath) + context.fillPath() + })?.stretchableImage(withLeftCapWidth: 25, topCapHeight: 32) +} + +func decorationBottomCornersImage(dark: Bool) -> UIImage? { + return generateImage(CGSize(width: 50.0, height: 110.0), rotatedContext: { (size, context) in + let bounds = CGRect(origin: CGPoint(), size: size) + context.setFillColor((dark ? fullscreenBackgroundColor : panelBackgroundColor).cgColor) + context.fill(bounds) + + context.setBlendMode(.clear) + + var corners: UIRectCorner = [] + corners.insert(.bottomLeft) + corners.insert(.bottomRight) + + let path = UIBezierPath(roundedRect: CGRect(x: 0.0, y: 0.0, width: 50.0, height: 50.0), byRoundingCorners: corners, cornerRadii: CGSize(width: 11.0, height: 11.0)) + context.addPath(path.cgPath) + context.fillPath() + })?.resizableImage(withCapInsets: UIEdgeInsets(top: 25.0, left: 25.0, bottom: 0.0, right: 25.0), resizingMode: .stretch) +} + +private func decorationBottomGradientImage(dark: Bool) -> UIImage? { + return generateImage(CGSize(width: 24.0, height: bottomGradientHeight), rotatedContext: { size, context in + let bounds = CGRect(origin: CGPoint(), size: size) + context.clear(bounds) + + let color = dark ? fullscreenBackgroundColor : panelBackgroundColor + let colorsArray = [color.withAlphaComponent(0.0).cgColor, color.cgColor] as CFArray + var locations: [CGFloat] = [1.0, 0.0] + let gradient = CGGradient(colorsSpace: deviceColorSpace, colors: colorsArray, locations: &locations)! + context.drawLinearGradient(gradient, start: CGPoint(), end: CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions()) + }) +} + +struct VoiceChatPeerEntry: Identifiable { + enum State { + case listening + case speaking + case invited + case raisedHand } - var tapped: (() -> Void)? + var peer: Peer + var about: String? + var isMyPeer: Bool + var videoEndpointId: String? + var videoPaused: Bool + var presentationEndpointId: String? + var presentationPaused: Bool + var effectiveSpeakerVideoEndpointId: String? + var state: State + var muteState: GroupCallParticipantsContext.Participant.MuteState? + var canManageCall: Bool + var volume: Int32? + var raisedHand: Bool + var displayRaisedHandStatus: Bool + var active: Bool + var isLandscape: Bool - init(theme: PresentationTheme) { - self.theme = theme - - self.titleNode = ASTextNode() - self.titleNode.displaysAsynchronously = false - self.titleNode.maximumNumberOfLines = 1 - self.titleNode.truncationMode = .byTruncatingTail - self.titleNode.isOpaque = false - - self.infoNode = ASTextNode() - self.infoNode.displaysAsynchronously = false - self.infoNode.maximumNumberOfLines = 1 - self.infoNode.truncationMode = .byTruncatingTail - self.infoNode.isOpaque = false - - self.recordingIconNode = VoiceChatRecordingIconNode(hasBackground: false) - - super.init() - - self.addSubnode(self.titleNode) - self.addSubnode(self.infoNode) - self.addSubnode(self.recordingIconNode) + var effectiveVideoEndpointId: String? { + return self.presentationEndpointId ?? self.videoEndpointId } - - required init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") + + init( + peer: Peer, + about: String?, + isMyPeer: Bool, + videoEndpointId: String?, + videoPaused: Bool, + presentationEndpointId: String?, + presentationPaused: Bool, + effectiveSpeakerVideoEndpointId: String?, + state: State, + muteState: GroupCallParticipantsContext.Participant.MuteState?, + canManageCall: Bool, + volume: Int32?, + raisedHand: Bool, + displayRaisedHandStatus: Bool, + active: Bool, + isLandscape: Bool + ) { + self.peer = peer + self.about = about + self.isMyPeer = isMyPeer + self.videoEndpointId = videoEndpointId + self.videoPaused = videoPaused + self.presentationEndpointId = presentationEndpointId + self.presentationPaused = presentationPaused + self.effectiveSpeakerVideoEndpointId = effectiveSpeakerVideoEndpointId + self.state = state + self.muteState = muteState + self.canManageCall = canManageCall + self.volume = volume + self.raisedHand = raisedHand + self.displayRaisedHandStatus = displayRaisedHandStatus + self.active = active + self.isLandscape = isLandscape } - override func didLoad() { - super.didLoad() - - self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tap))) + var stableId: PeerId { + return self.peer.id } - override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { - if point.y > 0.0 && point.y < self.frame.size.height && point.x > min(self.titleNode.frame.minX, self.infoNode.frame.minX) && point.x < max(self.recordingIconNode.frame.maxX, self.infoNode.frame.maxX) { - return true - } else { + static func ==(lhs: VoiceChatPeerEntry, rhs: VoiceChatPeerEntry) -> Bool { + if !lhs.peer.isEqual(rhs.peer) { return false } - } - - @objc private func tap() { - self.tapped?() - } - - func update(size: CGSize, title: String, subtitle: String, transition: ContainedViewLayoutTransition) { - var titleUpdated = false - if let previousTitle = self.titleNode.attributedText?.string { - titleUpdated = previousTitle != title + if lhs.about != rhs.about { + return false } - - if titleUpdated, let snapshotView = self.titleNode.view.snapshotContentTree() { - snapshotView.frame = self.titleNode.frame - self.view.addSubview(snapshotView) - - snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak snapshotView] _ in - snapshotView?.removeFromSuperview() - }) - - self.titleNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + if lhs.isMyPeer != rhs.isMyPeer { + return false } - - self.titleNode.attributedText = NSAttributedString(string: title, font: Font.medium(17.0), textColor: UIColor(rgb: 0xffffff)) - self.infoNode.attributedText = NSAttributedString(string: subtitle, font: Font.regular(13.0), textColor: UIColor(rgb: 0xffffff, alpha: 0.5)) - - let constrainedSize = CGSize(width: size.width - 120.0, height: size.height) - let titleSize = self.titleNode.measure(constrainedSize) - let infoSize = self.infoNode.measure(constrainedSize) - let titleInfoSpacing: CGFloat = 0.0 - - let combinedHeight = titleSize.height + infoSize.height + titleInfoSpacing - - let titleFrame = CGRect(origin: CGPoint(x: floor((size.width - titleSize.width) / 2.0), y: floor((size.height - combinedHeight) / 2.0)), size: titleSize) - self.titleNode.frame = titleFrame - self.infoNode.frame = CGRect(origin: CGPoint(x: floor((size.width - infoSize.width) / 2.0), y: floor((size.height - combinedHeight) / 2.0) + titleSize.height + titleInfoSpacing), size: infoSize) - - let iconSide = 16.0 + (1.0 + UIScreenPixel) * 2.0 - let iconSize: CGSize = CGSize(width: iconSide, height: iconSide) - self.recordingIconNode.frame = CGRect(origin: CGPoint(x: titleFrame.maxX + 1.0, y: titleFrame.minY + 1.0), size: iconSize) - } -} - -final class GroupVideoNode: ASDisplayNode { - private let videoViewContainer: UIView - private let videoView: PresentationCallVideoView - - private var validLayout: CGSize? - - var tapped: (() -> Void)? - - init(videoView: PresentationCallVideoView) { - self.videoViewContainer = UIView() - self.videoView = videoView - - super.init() - - self.videoViewContainer.addSubview(self.videoView.view) - self.view.addSubview(self.videoViewContainer) - - videoView.setOnFirstFrameReceived({ [weak self] _ in - Queue.mainQueue().async { - guard let strongSelf = self else { - return - } - if let size = strongSelf.validLayout { - strongSelf.updateLayout(size: size, transition: .immediate) - } - } - }) - - videoView.setOnOrientationUpdated({ [weak self] _, _ in - Queue.mainQueue().async { - guard let strongSelf = self else { - return - } - if let size = strongSelf.validLayout { - strongSelf.updateLayout(size: size, transition: .immediate) - } - } - }) - - self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) - } - - @objc private func tapGesture(_ recognizer: UITapGestureRecognizer) { - if case .ended = recognizer.state { - self.tapped?() + if lhs.videoEndpointId != rhs.videoEndpointId { + return false } - } - - func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) { - self.validLayout = size - self.videoViewContainer.frame = CGRect(origin: CGPoint(), size: size) - - let orientation = self.videoView.getOrientation() - var aspect = self.videoView.getAspect() - if aspect <= 0.01 { - aspect = 3.0 / 4.0 + if lhs.videoPaused != rhs.videoPaused { + return false } - - let rotatedAspect: CGFloat - let angle: CGFloat - let switchOrientation: Bool - switch orientation { - case .rotation0: - angle = 0.0 - rotatedAspect = 1 / aspect - switchOrientation = false - case .rotation90: - angle = CGFloat.pi / 2.0 - rotatedAspect = aspect - switchOrientation = true - case .rotation180: - angle = CGFloat.pi - rotatedAspect = 1 / aspect - switchOrientation = false - case .rotation270: - angle = CGFloat.pi * 3.0 / 2.0 - rotatedAspect = aspect - switchOrientation = true + if lhs.presentationEndpointId != rhs.presentationEndpointId { + return false } - - var rotatedVideoSize = CGSize(width: 100.0, height: rotatedAspect * 100.0) - - if size.width < 100.0 || true { - rotatedVideoSize = rotatedVideoSize.aspectFilled(size) - } else { - rotatedVideoSize = rotatedVideoSize.aspectFitted(size) + if lhs.presentationPaused != rhs.presentationPaused { + return false } - - if switchOrientation { - rotatedVideoSize = CGSize(width: rotatedVideoSize.height, height: rotatedVideoSize.width) + if lhs.effectiveSpeakerVideoEndpointId != rhs.effectiveSpeakerVideoEndpointId { + return false } - var rotatedVideoFrame = CGRect(origin: CGPoint(x: floor((size.width - rotatedVideoSize.width) / 2.0), y: floor((size.height - rotatedVideoSize.height) / 2.0)), size: rotatedVideoSize) - rotatedVideoFrame.origin.x = floor(rotatedVideoFrame.origin.x) - rotatedVideoFrame.origin.y = floor(rotatedVideoFrame.origin.y) - rotatedVideoFrame.size.width = ceil(rotatedVideoFrame.size.width) - rotatedVideoFrame.size.height = ceil(rotatedVideoFrame.size.height) - self.videoView.view.center = rotatedVideoFrame.center - self.videoView.view.bounds = CGRect(origin: CGPoint(), size: rotatedVideoFrame.size) - - let transition: ContainedViewLayoutTransition = .immediate - transition.updateTransformRotation(view: self.videoView.view, angle: angle) - } -} - -private final class MainVideoContainerNode: ASDisplayNode { - private let context: AccountContext - private let call: PresentationGroupCall - - private var currentVideoNode: GroupVideoNode? - private var currentPeer: (PeerId, UInt32)? - - private var validLayout: CGSize? - - init(context: AccountContext, call: PresentationGroupCall) { - self.context = context - self.call = call - - super.init() - - self.backgroundColor = .black - } - - func updatePeer(peer: (peerId: PeerId, source: UInt32)?) { - if self.currentPeer?.0 == peer?.0 && self.currentPeer?.1 == peer?.1 { - return + if lhs.state != rhs.state { + return false } - self.currentPeer = peer - if let (_, source) = peer { - self.call.makeIncomingVideoView(source: source, completion: { [weak self] videoView in - Queue.mainQueue().async { - guard let strongSelf = self, let videoView = videoView else { - return - } - let videoNode = GroupVideoNode(videoView: videoView) - if let currentVideoNode = strongSelf.currentVideoNode { - currentVideoNode.removeFromSupernode() - strongSelf.currentVideoNode = nil - } - strongSelf.currentVideoNode = videoNode - strongSelf.addSubnode(videoNode) - if let size = strongSelf.validLayout { - strongSelf.update(size: size, transition: .immediate) - } - } - }) - } else { - if let currentVideoNode = self.currentVideoNode { - currentVideoNode.removeFromSupernode() - self.currentVideoNode = nil - } + if lhs.muteState != rhs.muteState { + return false } - } - - func update(size: CGSize, transition: ContainedViewLayoutTransition) { - self.validLayout = size - - if let currentVideoNode = self.currentVideoNode { - transition.updateFrame(node: currentVideoNode, frame: CGRect(origin: CGPoint(), size: size)) - currentVideoNode.updateLayout(size: size, transition: .immediate) + if lhs.canManageCall != rhs.canManageCall { + return false } + if lhs.volume != rhs.volume { + return false + } + if lhs.raisedHand != rhs.raisedHand { + return false + } + if lhs.displayRaisedHandStatus != rhs.displayRaisedHandStatus { + return false + } + if lhs.active != rhs.active { + return false + } + if lhs.isLandscape != rhs.isLandscape { + return false + } + return true } } public final class VoiceChatController: ViewController { - private final class Node: ViewControllerTracingNode, UIGestureRecognizerDelegate { + enum DisplayMode { + case modal(isExpanded: Bool, isFilled: Bool) + case fullscreen(controlsHidden: Bool) + } + + fileprivate final class Node: ViewControllerTracingNode, UIGestureRecognizerDelegate { private struct ListTransition { let deletions: [ListViewDeleteItem] let insertions: [ListViewInsertItem] @@ -334,33 +280,29 @@ public final class VoiceChatController: ViewController { let animated: Bool } - private struct State: Equatable { - var revealedPeerId: PeerId? - } - private final class Interaction { let updateIsMuted: (PeerId, Bool) -> Void - let openPeer: (PeerId) -> Void + let switchToPeer: (PeerId, String?, Bool) -> Void let openInvite: () -> Void - let peerContextAction: (PeerEntry, ASDisplayNode, ContextGesture?) -> Void - let setPeerIdWithRevealedOptions: (PeerId?, PeerId?) -> Void - let getPeerVideo: (UInt32) -> GroupVideoNode? + let peerContextAction: (VoiceChatPeerEntry, ASDisplayNode, ContextGesture?, Bool) -> Void + let getPeerVideo: (String, GroupVideoNode.Position) -> GroupVideoNode? + var isExpanded: Bool = false private var audioLevels: [PeerId: ValuePipe] = [:] + var updateAvatarPromise = Promise<(TelegramMediaImageRepresentation, Float)?>(nil) + init( updateIsMuted: @escaping (PeerId, Bool) -> Void, - openPeer: @escaping (PeerId) -> Void, + switchToPeer: @escaping (PeerId, String?, Bool) -> Void, openInvite: @escaping () -> Void, - peerContextAction: @escaping (PeerEntry, ASDisplayNode, ContextGesture?) -> Void, - setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, - getPeerVideo: @escaping (UInt32) -> GroupVideoNode? + peerContextAction: @escaping (VoiceChatPeerEntry, ASDisplayNode, ContextGesture?, Bool) -> Void, + getPeerVideo: @escaping (String, GroupVideoNode.Position) -> GroupVideoNode? ) { self.updateIsMuted = updateIsMuted - self.openPeer = openPeer + self.switchToPeer = switchToPeer self.openInvite = openInvite self.peerContextAction = peerContextAction - self.setPeerIdWithRevealedOptions = setPeerIdWithRevealedOptions self.getPeerVideo = getPeerVideo } @@ -401,84 +343,8 @@ public final class VoiceChatController: ViewController { } } - private struct PeerEntry: Comparable, Identifiable { - enum State { - case listening - case speaking - case invited - case raisedHand - } - - var peer: Peer - var about: String? - var isMyPeer: Bool - var ssrc: UInt32? - var presence: TelegramUserPresence? - var activityTimestamp: Int32 - var state: State - var muteState: GroupCallParticipantsContext.Participant.MuteState? - var revealed: Bool? - var canManageCall: Bool - var volume: Int32? - var raisedHand: Bool - var displayRaisedHandStatus: Bool - - var stableId: PeerId { - return self.peer.id - } - - static func ==(lhs: PeerEntry, rhs: PeerEntry) -> Bool { - if !lhs.peer.isEqual(rhs.peer) { - return false - } - if lhs.about != rhs.about { - return false - } - if lhs.isMyPeer != rhs.isMyPeer { - return false - } - if lhs.ssrc != rhs.ssrc { - return false - } - if lhs.presence != rhs.presence { - return false - } - if lhs.activityTimestamp != rhs.activityTimestamp { - return false - } - if lhs.state != rhs.state { - return false - } - if lhs.muteState != rhs.muteState { - return false - } - if lhs.revealed != rhs.revealed { - return false - } - if lhs.canManageCall != rhs.canManageCall { - return false - } - if lhs.volume != rhs.volume { - return false - } - if lhs.raisedHand != rhs.raisedHand { - return false - } - if lhs.displayRaisedHandStatus != rhs.displayRaisedHandStatus { - return false - } - return true - } - - static func <(lhs: PeerEntry, rhs: PeerEntry) -> Bool { - if lhs.activityTimestamp != rhs.activityTimestamp { - return lhs.activityTimestamp > rhs.activityTimestamp - } - return lhs.peer.id < rhs.peer.id - } - } - private enum EntryId: Hashable { + case tiles case invite case peerId(PeerId) @@ -488,6 +354,13 @@ public final class VoiceChatController: ViewController { static func ==(lhs: EntryId, rhs: EntryId) -> Bool { switch lhs { + case .tiles: + switch rhs { + case .tiles: + return true + default: + return false + } case .invite: switch rhs { case .invite: @@ -507,30 +380,39 @@ public final class VoiceChatController: ViewController { } private enum ListEntry: Comparable, Identifiable { - case invite(PresentationTheme, PresentationStrings, String) - case peer(PeerEntry) + case tiles([VoiceChatTileItem], VoiceChatTileLayoutMode) + case invite(PresentationTheme, PresentationStrings, String, Bool) + case peer(VoiceChatPeerEntry, Int32) var stableId: EntryId { switch self { + case .tiles: + return .tiles case .invite: return .invite - case let .peer(peerEntry): + case let .peer(peerEntry, _): return .peerId(peerEntry.peer.id) } } static func ==(lhs: ListEntry, rhs: ListEntry) -> Bool { switch lhs { - case let .invite(lhsTheme, lhsStrings, lhsText): - if case let .invite(rhsTheme, rhsStrings, rhsText) = rhs, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsText == rhsText { + case let .tiles(lhsTiles, lhsLayoutMode): + if case let .tiles(rhsTiles, rhsLayoutMode) = rhs, lhsTiles == rhsTiles, lhsLayoutMode == rhsLayoutMode { return true } else { return false } - case let .peer(lhsPeerEntry): + case let .invite(lhsTheme, lhsStrings, lhsText, lhsIsLink): + if case let .invite(rhsTheme, rhsStrings, rhsText, rhsIsLink) = rhs, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsText == rhsText, lhsIsLink == rhsIsLink { + return true + } else { + return false + } + case let .peer(lhsPeerEntry, lhsIndex): switch rhs { - case let .peer(rhsPeerEntry): - return lhsPeerEntry == rhsPeerEntry + case let .peer(rhsPeerEntry, rhsIndex): + return lhsPeerEntry == rhsPeerEntry && lhsIndex == rhsIndex default: return false } @@ -539,38 +421,272 @@ public final class VoiceChatController: ViewController { static func <(lhs: ListEntry, rhs: ListEntry) -> Bool { switch lhs { - case .invite: + case .tiles: return true - case let .peer(lhsPeerEntry): + case .invite: + return false + case let .peer(_, lhsIndex): switch rhs { - case .invite: + case .tiles: return false - case let .peer(rhsPeerEntry): - return lhsPeerEntry < rhsPeerEntry + case let .peer(_, rhsIndex): + return lhsIndex < rhsIndex + case .invite: + return true } } } + func tileItem(context: AccountContext, presentationData: PresentationData, interaction: Interaction, isTablet: Bool, videoEndpointId: String, videoReady: Bool, videoTimeouted: Bool, videoIsPaused: Bool, showAsPresentation: Bool, secondary: Bool) -> VoiceChatTileItem? { + guard case let .peer(peerEntry, _) = self else { + return nil + } + let peer = peerEntry.peer + + let icon: VoiceChatTileItem.Icon + var text: VoiceChatParticipantItem.ParticipantText + var additionalText: VoiceChatParticipantItem.ParticipantText? + var speaking = false + + var textIcon = VoiceChatParticipantItem.ParticipantText.TextIcon() + let yourText: String + if (peerEntry.about?.isEmpty ?? true) && peer.smallProfileImage == nil { + yourText = presentationData.strings.VoiceChat_TapToAddPhotoOrBio + } else if peer.smallProfileImage == nil { + yourText = presentationData.strings.VoiceChat_TapToAddPhoto + } else if (peerEntry.about?.isEmpty ?? true) { + yourText = presentationData.strings.VoiceChat_TapToAddBio + } else { + yourText = presentationData.strings.VoiceChat_You + } + + var state = peerEntry.state + if let muteState = peerEntry.muteState, case .speaking = state, muteState.mutedByYou || !muteState.canUnmute { + state = .listening + } + switch state { + case .listening: + if peerEntry.isMyPeer { + text = .text(yourText, textIcon, .accent) + } else if let muteState = peerEntry.muteState, muteState.mutedByYou { + text = .text(presentationData.strings.VoiceChat_StatusMutedForYou, textIcon, .destructive) + } else if let about = peerEntry.about, !about.isEmpty { + text = .text(about, textIcon, .generic) + } else { + text = .text(presentationData.strings.VoiceChat_StatusListening, textIcon, .generic) + } + if let muteState = peerEntry.muteState, muteState.mutedByYou { + icon = .microphone(true) + additionalText = .text(presentationData.strings.VoiceChat_StatusMutedForYou, textIcon, .destructive) + } else { + icon = .microphone(peerEntry.muteState != nil) + } + case .speaking: + if let muteState = peerEntry.muteState, muteState.mutedByYou { + text = .text(presentationData.strings.VoiceChat_StatusMutedForYou, textIcon, .destructive) + icon = .microphone(true) + additionalText = .text(presentationData.strings.VoiceChat_StatusMutedForYou, textIcon, .destructive) + } else { + if peerEntry.volume != nil { + textIcon.insert(.volume) + } + let volumeValue = peerEntry.volume.flatMap { $0 / 100 } + if let volume = volumeValue, volume != 100 { + text = .text( presentationData.strings.VoiceChat_StatusSpeakingVolume("\(volume)%").0, textIcon, .constructive) + } else { + text = .text(presentationData.strings.VoiceChat_StatusSpeaking, textIcon, .constructive) + } + icon = .microphone(false) + speaking = true + } + case .raisedHand, .invited: + text = .none + icon = .none + } + + if let about = peerEntry.about, !about.isEmpty { + textIcon = [] + text = .text(about, textIcon, .generic) + } + + return VoiceChatTileItem(account: context.account, peer: peerEntry.peer, videoEndpointId: videoEndpointId, videoReady: videoReady, videoTimeouted: videoTimeouted, isVideoLimit: false, videoLimit: 0, isPaused: videoIsPaused, isOwnScreencast: peerEntry.presentationEndpointId == videoEndpointId && peerEntry.isMyPeer, strings: presentationData.strings, nameDisplayOrder: presentationData.nameDisplayOrder, speaking: speaking, secondary: secondary, isTablet: isTablet, icon: showAsPresentation ? .presentation : icon, text: text, additionalText: additionalText, action: { + interaction.switchToPeer(peer.id, videoEndpointId, !secondary) + }, contextAction: { node, gesture in + interaction.peerContextAction(peerEntry, node, gesture, false) + }, getVideo: { position in + return interaction.getPeerVideo(videoEndpointId, position) + }, getAudioLevel: { + return interaction.getAudioLevel(peerEntry.peer.id) + }) + } + + func fullscreenItem(context: AccountContext, presentationData: PresentationData, interaction: Interaction) -> ListViewItem { + switch self { + case .tiles: + return VoiceChatActionItem(presentationData: ItemListPresentationData(presentationData), title: "", icon: .none, action: { + }) + case .invite: + return VoiceChatActionItem(presentationData: ItemListPresentationData(presentationData), title: "", icon: .generic(UIImage(bundleImageName: "Chat/Context Menu/AddUser")!), action: { + interaction.openInvite() + }) + case let .peer(peerEntry, _): + let peer = peerEntry.peer + var textColor: VoiceChatFullscreenParticipantItem.Color = .generic + var color: VoiceChatFullscreenParticipantItem.Color = .generic + let icon: VoiceChatFullscreenParticipantItem.Icon + var text: VoiceChatParticipantItem.ParticipantText + + var textIcon = VoiceChatParticipantItem.ParticipantText.TextIcon() + let yourText: String + if (peerEntry.about?.isEmpty ?? true) && peer.smallProfileImage == nil { + yourText = presentationData.strings.VoiceChat_TapToAddPhotoOrBio + } else if peer.smallProfileImage == nil { + yourText = presentationData.strings.VoiceChat_TapToAddPhoto + } else if (peerEntry.about?.isEmpty ?? true) { + yourText = presentationData.strings.VoiceChat_TapToAddBio + } else { + yourText = presentationData.strings.VoiceChat_You + } + + var state = peerEntry.state + if let muteState = peerEntry.muteState, case .speaking = state, muteState.mutedByYou || !muteState.canUnmute { + state = .listening + } + switch state { + case .listening: + if peerEntry.isMyPeer { + text = .text(yourText, textIcon, .accent) + } else if let muteState = peerEntry.muteState, muteState.mutedByYou { + text = .text(presentationData.strings.VoiceChat_StatusMutedForYou, textIcon, .destructive) + } else if let about = peerEntry.about, !about.isEmpty { + text = .text(about, textIcon, .generic) + } else { + text = .text(presentationData.strings.VoiceChat_StatusListening, textIcon, .generic) + } + if let muteState = peerEntry.muteState, muteState.mutedByYou { + textColor = .destructive + color = .destructive + icon = .microphone(true, UIColor(rgb: 0xff3b30)) + } else { + icon = .microphone(peerEntry.muteState != nil, UIColor.white) + color = .accent + } + case .speaking: + if let muteState = peerEntry.muteState, muteState.mutedByYou { + text = .text(presentationData.strings.VoiceChat_StatusMutedForYou, textIcon, .destructive) + textColor = .destructive + color = .destructive + icon = .microphone(true, UIColor(rgb: 0xff3b30)) + } else { + if peerEntry.volume != nil { + textIcon.insert(.volume) + } + let volumeValue = peerEntry.volume.flatMap { $0 / 100 } + if let volume = volumeValue, volume != 100 { + text = .text( presentationData.strings.VoiceChat_StatusSpeakingVolume("\(volume)%").0, textIcon, .constructive) + } else { + text = .text(presentationData.strings.VoiceChat_StatusSpeaking, textIcon, .constructive) + } + icon = .microphone(false, UIColor(rgb: 0x34c759)) + textColor = .constructive + color = .constructive + } + case .raisedHand: + text = .none + textColor = .accent + icon = .wantsToSpeak + case .invited: + text = .none + icon = .none + } + + if let about = peerEntry.about, !about.isEmpty { + textIcon = [] + text = .text(about, textIcon, .generic) + } + + var videoEndpointId = peerEntry.effectiveVideoEndpointId + var otherVideoEndpointId: String? + let hasBothVideos = peerEntry.presentationEndpointId != nil && peerEntry.videoEndpointId != nil + if hasBothVideos { + if let effectiveVideoEndpointId = peerEntry.effectiveSpeakerVideoEndpointId { + if effectiveVideoEndpointId == peerEntry.videoEndpointId { + videoEndpointId = peerEntry.presentationEndpointId + otherVideoEndpointId = videoEndpointId + } else if effectiveVideoEndpointId == peerEntry.presentationEndpointId { + videoEndpointId = peerEntry.videoEndpointId + otherVideoEndpointId = videoEndpointId + } + } + } + + var isPaused = false + if videoEndpointId == peerEntry.videoEndpointId { + isPaused = peerEntry.videoPaused + } else if videoEndpointId == peerEntry.presentationEndpointId { + isPaused = peerEntry.presentationPaused + } + + return VoiceChatFullscreenParticipantItem(presentationData: ItemListPresentationData(presentationData), nameDisplayOrder: presentationData.nameDisplayOrder, context: context, peer: peerEntry.peer, videoEndpointId: videoEndpointId, isPaused: isPaused, icon: icon, text: text, textColor: textColor, color: color, isLandscape: peerEntry.isLandscape, active: peerEntry.active, showVideoWhenActive: otherVideoEndpointId != nil, getAudioLevel: { return interaction.getAudioLevel(peerEntry.peer.id) }, getVideo: { + if let videoEndpointId = videoEndpointId { + return interaction.getPeerVideo(videoEndpointId, .list) + } else { + return nil + } + }, action: { _ in + interaction.switchToPeer(peerEntry.peer.id, otherVideoEndpointId, false) + }, contextAction: { node, gesture in + interaction.peerContextAction(peerEntry, node, gesture, true) + }, getUpdatingAvatar: { + return interaction.updateAvatarPromise.get() + }) + } + } + func item(context: AccountContext, presentationData: PresentationData, interaction: Interaction) -> ListViewItem { switch self { - case let .invite(_, _, text): - return VoiceChatActionItem(presentationData: ItemListPresentationData(presentationData), title: text, icon: .generic(UIImage(bundleImageName: "Chat/Context Menu/AddUser")!), action: { + case let .tiles(tiles, layoutMode): + return VoiceChatTilesGridItem(context: context, tiles: tiles, layoutMode: layoutMode, getIsExpanded: { + return interaction.isExpanded + }) + case let .invite(_, _, text, isLink): + return VoiceChatActionItem(presentationData: ItemListPresentationData(presentationData), title: text, icon: .generic(UIImage(bundleImageName: isLink ? "Chat/Context Menu/Link" : "Chat/Context Menu/AddUser")!), action: { interaction.openInvite() }) - case let .peer(peerEntry): + case let .peer(peerEntry, _): let peer = peerEntry.peer - + var text: VoiceChatParticipantItem.ParticipantText var expandedText: VoiceChatParticipantItem.ParticipantText? let icon: VoiceChatParticipantItem.Icon - switch peerEntry.state { + + var state = peerEntry.state + if let muteState = peerEntry.muteState, case .speaking = state, muteState.mutedByYou || !muteState.canUnmute { + state = .listening + } + + var textIcon = VoiceChatParticipantItem.ParticipantText.TextIcon() + let yourText: String + if (peerEntry.about?.isEmpty ?? true) && peer.smallProfileImage == nil { + yourText = presentationData.strings.VoiceChat_TapToAddPhotoOrBio + } else if peer.smallProfileImage == nil { + yourText = presentationData.strings.VoiceChat_TapToAddPhoto + } else if (peerEntry.about?.isEmpty ?? true) { + yourText = presentationData.strings.VoiceChat_TapToAddBio + } else { + yourText = presentationData.strings.VoiceChat_You + } + + switch state { case .listening: - if let muteState = peerEntry.muteState, muteState.mutedByYou { - text = .text(presentationData.strings.VoiceChat_StatusMutedForYou, .destructive) + if peerEntry.isMyPeer { + text = .text(yourText, textIcon, .accent) + } else if let muteState = peerEntry.muteState, muteState.mutedByYou { + text = .text(presentationData.strings.VoiceChat_StatusMutedForYou, textIcon, .destructive) } else if let about = peerEntry.about, !about.isEmpty { - text = .text(about, .generic) + text = .text(about, textIcon, .generic) } else { - text = .text(presentationData.strings.VoiceChat_StatusListening, .generic) + text = .text(presentationData.strings.VoiceChat_StatusListening, textIcon, .generic) } let microphoneColor: UIColor if let muteState = peerEntry.muteState, !muteState.canUnmute || muteState.mutedByYou { @@ -581,46 +697,50 @@ public final class VoiceChatController: ViewController { icon = .microphone(peerEntry.muteState != nil, microphoneColor) case .speaking: if let muteState = peerEntry.muteState, muteState.mutedByYou { - text = .text(presentationData.strings.VoiceChat_StatusMutedForYou, .destructive) + text = .text(presentationData.strings.VoiceChat_StatusMutedForYou, textIcon, .destructive) icon = .microphone(true, UIColor(rgb: 0xff3b30)) } else { + if peerEntry.volume != nil { + textIcon.insert(.volume) + } let volumeValue = peerEntry.volume.flatMap { $0 / 100 } if let volume = volumeValue, volume != 100 { - text = .text( presentationData.strings.VoiceChat_StatusSpeakingVolume("\(volume)%").0, .constructive) + text = .text( presentationData.strings.VoiceChat_StatusSpeakingVolume("\(volume)%").0, textIcon, .constructive) } else { - text = .text(presentationData.strings.VoiceChat_StatusSpeaking, .constructive) + text = .text(presentationData.strings.VoiceChat_StatusSpeaking, textIcon, .constructive) } icon = .microphone(false, UIColor(rgb: 0x34c759)) } case .invited: - text = .text(presentationData.strings.VoiceChat_StatusInvited, .generic) + text = .text(presentationData.strings.VoiceChat_StatusInvited, textIcon, .generic) icon = .invite(true) case .raisedHand: - if let about = peerEntry.about, !about.isEmpty && !peerEntry.displayRaisedHandStatus { - text = .text(about, .generic) + if peerEntry.isMyPeer && !peerEntry.displayRaisedHandStatus { + text = .text(yourText, textIcon, .accent) + } else if let about = peerEntry.about, !about.isEmpty && !peerEntry.displayRaisedHandStatus { + text = .text(about, textIcon, .generic) } else { - text = .text(presentationData.strings.VoiceChat_StatusWantsToSpeak, .accent) + text = .text(presentationData.strings.VoiceChat_StatusWantsToSpeak, textIcon, .accent) } icon = .wantsToSpeak } if let about = peerEntry.about, !about.isEmpty { - expandedText = .text(about, .generic) + textIcon = [] + expandedText = .text(about, textIcon, .generic) } - - let revealOptions: [VoiceChatParticipantItem.RevealOption] = [] - - return VoiceChatParticipantItem(presentationData: ItemListPresentationData(presentationData), dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, context: context, peer: peer, ssrc: peerEntry.ssrc, presence: peerEntry.presence, text: text, expandedText: expandedText, icon: icon, enabled: true, selectable: !peerEntry.isMyPeer || peerEntry.canManageCall || peerEntry.raisedHand, getAudioLevel: { return interaction.getAudioLevel(peer.id) }, getVideo: { - if let ssrc = peerEntry.ssrc { - return interaction.getPeerVideo(ssrc) - } else { - return nil + + return VoiceChatParticipantItem(presentationData: ItemListPresentationData(presentationData), dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, context: context, peer: peer, text: text, expandedText: expandedText, icon: icon, getAudioLevel: { return interaction.getAudioLevel(peer.id) }, action: { node in + if let node = node { + interaction.peerContextAction(peerEntry, node, nil, false) } - }, revealOptions: revealOptions, revealed: peerEntry.revealed, setPeerIdWithRevealedOptions: { peerId, fromPeerId in - interaction.setPeerIdWithRevealedOptions(peerId, fromPeerId) - }, action: { node in - interaction.peerContextAction(peerEntry, node, nil) - }, contextAction: nil) + }, contextAction: { node, gesture in + interaction.peerContextAction(peerEntry, node, gesture, false) + }, getIsExpanded: { + return interaction.isExpanded + }, getUpdatingAvatar: { + return interaction.updateAvatarPromise.get() + }) } } } @@ -635,6 +755,20 @@ public final class VoiceChatController: ViewController { return ListTransition(deletions: deletions, insertions: insertions, updates: updates, isLoading: isLoading, isEmpty: isEmpty, canInvite: canInvite, crossFade: crossFade, count: toEntries.count, animated: animated) } + private func preparedFullscreenTransition(from fromEntries: [ListEntry], to toEntries: [ListEntry], isLoading: Bool, isEmpty: Bool, canInvite: Bool, crossFade: Bool, animated: Bool, context: AccountContext, presentationData: PresentationData, interaction: Interaction) -> ListTransition { + 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.fullscreenItem(context: context, presentationData: presentationData, interaction: interaction), directionHint: nil) } + let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.fullscreenItem(context: context, presentationData: presentationData, interaction: interaction), directionHint: nil) } + + return ListTransition(deletions: deletions, insertions: insertions, updates: updates, isLoading: isLoading, isEmpty: isEmpty, canInvite: canInvite, crossFade: crossFade, count: toEntries.count, animated: animated) + } + + private let currentAvatarMixin = Atomic(value: nil) + + private var configuration: VoiceChatConfiguration? + private weak var controller: VoiceChatController? private let sharedContext: SharedAccountContext private let context: AccountContext @@ -646,54 +780,99 @@ public final class VoiceChatController: ViewController { private let dimNode: ASDisplayNode private let contentContainer: ASDisplayNode private let backgroundNode: ASDisplayNode - private var mainVideoContainer: MainVideoContainerNode? + private let listContainer: ASDisplayNode private let listNode: ListView + private let fullscreenListContainer: ASDisplayNode + private let fullscreenListNode: ListView + private let tileGridNode: VoiceChatTileGridNode private let topPanelNode: ASDisplayNode private let topPanelEdgeNode: ASDisplayNode private let topPanelBackgroundNode: ASDisplayNode private let optionsButton: VoiceChatHeaderButton - private var optionsButtonIsAvatar = false private let closeButton: VoiceChatHeaderButton + private let panelButton: VoiceChatHeaderButton private let topCornersNode: ASImageNode fileprivate let bottomPanelNode: ASDisplayNode + private let bottomGradientNode: ASDisplayNode private let bottomPanelBackgroundNode: ASDisplayNode private let bottomCornersNode: ASImageNode - fileprivate let audioOutputNode: CallControllerButtonItemNode - fileprivate let cameraButtonNode: CallControllerButtonItemNode - fileprivate let leaveNode: CallControllerButtonItemNode + fileprivate let audioButton: CallControllerButtonItemNode + fileprivate let cameraButton: CallControllerButtonItemNode + fileprivate let switchCameraButton: CallControllerButtonItemNode + fileprivate let leaveButton: CallControllerButtonItemNode fileprivate let actionButton: VoiceChatActionButton private let leftBorderNode: ASDisplayNode private let rightBorderNode: ASDisplayNode + private let mainStageContainerNode: ASDisplayNode + private let mainStageBackgroundNode: ASDisplayNode + private let mainStageNode: VoiceChatMainStageNode + + private let transitionMaskView: UIView + private let transitionMaskTopFillLayer: CALayer + private let transitionMaskFillLayer: CALayer + private let transitionMaskGradientLayer: CAGradientLayer + private let transitionMaskBottomFillLayer: CALayer + private let transitionContainerNode: ASDisplayNode - private let titleNode: VoiceChatControllerTitleNode + private var isScheduling = false + private let timerNode: VoiceChatTimerNode + private var pickerView: UIDatePicker? + private let dateFormatter: DateFormatter + private let scheduleTextNode: ImmediateTextNode + private let scheduleCancelButton: SolidRoundedButtonNode + private var scheduleButtonTitle = "" + + private let titleNode: VoiceChatTitleNode private var enqueuedTransitions: [ListTransition] = [] - private var floatingHeaderOffset: CGFloat? + private var enqueuedFullscreenTransitions: [ListTransition] = [] private var validLayout: (ContainerViewLayout, CGFloat)? private var didSetContentsReady: Bool = false private var didSetDataReady: Bool = false + private var isFirstTime = true + private var topInset: CGFloat? + + private var animatingInsertion = false + private var animatingExpansion = false + private var animatingAppearance = false + private var animatingButtonsSwap = false + private var animatingMainStage = false + private var animatingContextMenu = false + private var panGestureArguments: (topInset: CGFloat, offset: CGFloat)? + private var isPanning = false + private var peer: Peer? private var currentTitle: String = "" private var currentTitleIsCustom = false private var currentSubtitle: String = "" + private var currentSpeakingSubtitle: String? private var currentCallMembers: ([GroupCallParticipantsContext.Participant], String?)? private var currentInvitedPeers: [Peer]? private var currentSpeakingPeers: Set? private var currentContentOffset: CGFloat? - private var ignoreScrolling = false - private var currentAudioButtonColor: UIColor? + private var currentNormalButtonColor: UIColor? + private var currentActiveButtonColor: UIColor? + private var myEntry: VoiceChatPeerEntry? + private var mainEntry: VoiceChatPeerEntry? private var currentEntries: [ListEntry] = [] + private var currentFullscreenEntries: [ListEntry] = [] + private var currentTileItems: [VoiceChatTileItem] = [] + private var displayPanelVideos = false + private var joinedVideo: Bool? private var peerViewDisposable: Disposable? private let leaveDisposable = MetaDisposable() private var isMutedDisposable: Disposable? + private var isNoiseSuppressionEnabled: Bool = true + private var isNoiseSuppressionEnabledDisposable: Disposable? private var callStateDisposable: Disposable? private var pushingToTalk = false + private var temporaryPushingToTalk = false private let hapticFeedback = HapticFeedback() private var callState: PresentationGroupCallState? @@ -713,24 +892,21 @@ public final class VoiceChatController: ViewController { private var audioLevelsDisposable: Disposable? private var myAudioLevelDisposable: Disposable? + private var isSpeakingDisposable: Disposable? private var memberStatesDisposable: Disposable? private var actionButtonColorDisposable: Disposable? private var itemInteraction: Interaction? private let inviteDisposable = MetaDisposable() - private let memberEventsDisposable = MetaDisposable() private let reconnectedAsEventsDisposable = MetaDisposable() - private let voiceSourcesDisposable = MetaDisposable() + private let stateVersionDisposable = MetaDisposable() + private var applicationStateDisposable: Disposable? - private var requestedVideoSources = Set() - private var videoNodes: [(PeerId, UInt32, GroupVideoNode)] = [] - private let displayAsPeersPromise = Promise<[FoundPeer]>([]) private let inviteLinksPromise = Promise(nil) - private var raisedHandDisplayDisposables: [PeerId: Disposable] = [:] private var displayedRaisedHands = Set() { didSet { @@ -739,14 +915,68 @@ public final class VoiceChatController: ViewController { } private let displayedRaisedHandsPromise = ValuePromise>(Set()) - private var currentDominantSpeakerWithVideo: (PeerId, UInt32)? + private var requestedVideoSources = Set() + private var requestedVideoChannels: [PresentationGroupCallRequestedVideo] = [] + + private var videoRenderingContext: VideoRenderingContext + private var videoNodes: [String: GroupVideoNode] = [:] + private var wideVideoNodes = Set() + private var videoOrder: [String] = [] + private var readyVideoEndpointIds = Set() + private var readyVideoEndpointIdsPromise = ValuePromise>(Set()) + private var timeoutedEndpointIds = Set() + private var readyVideoDisposables = DisposableDict() + private var myPeerVideoReadyDisposable = MetaDisposable() + + private var peerIdToEndpointId: [PeerId: String] = [:] + + private var currentSpeakers: [PeerId] = [] + private var currentDominantSpeaker: (PeerId, String?, Double)? + private var currentForcedSpeaker: (PeerId, String?)? + private var effectiveSpeaker: (PeerId, String?, Bool, Bool, Bool)? + + private var updateAvatarDisposable = MetaDisposable() + private let updateAvatarPromise = Promise<(TelegramMediaImageRepresentation, Float)?>(nil) + private var currentUpdatingAvatar: TelegramMediaImageRepresentation? + + private var connectedOnce = false + private var ignoreConnecting = false + private var ignoreConnectingTimer: SwiftSignalKit.Timer? + + private var displayUnmuteTooltipTimer: SwiftSignalKit.Timer? + private var dismissUnmuteTooltipTimer: SwiftSignalKit.Timer? + private var lastUnmuteTooltipDisplayTimestamp: Double? + + private var panelHidden = false + private var displayMode: DisplayMode = .modal(isExpanded: false, isFilled: false) { + didSet { + if case let .modal(isExpanded, _) = self.displayMode { + self.itemInteraction?.isExpanded = isExpanded + } else { + self.itemInteraction?.isExpanded = true + } + } + } + + private var isExpanded: Bool { + switch self.displayMode { + case .modal(true, _), .fullscreen: + return true + default: + return false + } + } init(controller: VoiceChatController, sharedContext: SharedAccountContext, call: PresentationGroupCall) { self.controller = controller self.sharedContext = sharedContext self.context = call.accountContext self.call = call + + self.videoRenderingContext = VideoRenderingContext() + self.isScheduling = call.schedulePending + let presentationData = sharedContext.currentPresentationData.with { $0 } self.presentationData = presentationData @@ -754,20 +984,20 @@ public final class VoiceChatController: ViewController { self.currentSubtitle = self.presentationData.strings.SocksProxySetup_ProxyStatusConnecting self.dimNode = ASDisplayNode() - self.dimNode.backgroundColor = dimColor + self.dimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.5) self.contentContainer = ASDisplayNode() self.contentContainer.isHidden = true self.backgroundNode = ASDisplayNode() - self.backgroundNode.backgroundColor = secondaryPanelBackgroundColor + self.backgroundNode.backgroundColor = self.isScheduling ? panelBackgroundColor : secondaryPanelBackgroundColor self.backgroundNode.clipsToBounds = false - /*if false { - self.mainVideoContainer = MainVideoContainerNode(context: call.accountContext, call: call) - }*/ + self.listContainer = ASDisplayNode() self.listNode = ListView() + self.listNode.alpha = self.isScheduling ? 0.0 : 1.0 + self.listNode.isUserInteractionEnabled = !self.isScheduling self.listNode.verticalScrollIndicatorColor = UIColor(white: 1.0, alpha: 0.3) self.listNode.clipsToBounds = true self.listNode.scroller.bounces = false @@ -775,6 +1005,18 @@ public final class VoiceChatController: ViewController { return presentationData.strings.VoiceOver_ScrollStatus(row, count).0 } + self.fullscreenListContainer = ASDisplayNode() + self.fullscreenListContainer.isHidden = true + + self.fullscreenListNode = ListView() + self.fullscreenListNode.transform = CATransform3DMakeRotation(-CGFloat(CGFloat.pi / 2.0), 0.0, 0.0, 1.0) + self.fullscreenListNode.clipsToBounds = true + self.fullscreenListNode.accessibilityPageScrolledString = { row, count in + return presentationData.strings.VoiceOver_ScrollStatus(row, count).0 + } + + self.tileGridNode = VoiceChatTileGridNode(context: self.context) + self.topPanelNode = ASDisplayNode() self.topPanelNode.clipsToBounds = false @@ -791,31 +1033,53 @@ public final class VoiceChatController: ViewController { } self.optionsButton = VoiceChatHeaderButton(context: self.context) + self.optionsButton.setContent(.more(optionsCircleImage(dark: false))) self.closeButton = VoiceChatHeaderButton(context: self.context) self.closeButton.setContent(.image(closeButtonImage(dark: false))) + self.panelButton = VoiceChatHeaderButton(context: self.context, wide: true) + self.panelButton.setContent(.image(panelButtonImage(dark: false))) - self.titleNode = VoiceChatControllerTitleNode(theme: self.presentationData.theme) + self.titleNode = VoiceChatTitleNode(theme: self.presentationData.theme) self.topCornersNode = ASImageNode() self.topCornersNode.displaysAsynchronously = false self.topCornersNode.displayWithoutProcessing = true - self.topCornersNode.image = cornersImage(top: true, bottom: false, dark: false) - + self.topCornersNode.image = decorationTopCornersImage(dark: false) + self.topCornersNode.isUserInteractionEnabled = false + self.bottomPanelNode = ASDisplayNode() self.bottomPanelNode.clipsToBounds = false self.bottomPanelBackgroundNode = ASDisplayNode() self.bottomPanelBackgroundNode.backgroundColor = panelBackgroundColor + self.bottomPanelBackgroundNode.isUserInteractionEnabled = false + + self.bottomGradientNode = ASDisplayNode() + self.bottomGradientNode.displaysAsynchronously = false + self.bottomGradientNode.backgroundColor = decorationBottomGradientImage(dark: false).flatMap { UIColor(patternImage: $0) } self.bottomCornersNode = ASImageNode() self.bottomCornersNode.displaysAsynchronously = false self.bottomCornersNode.displayWithoutProcessing = true - self.bottomCornersNode.image = cornersImage(top: false, bottom: true, dark: false) + self.bottomCornersNode.image = decorationBottomCornersImage(dark: false) + self.bottomCornersNode.isUserInteractionEnabled = false - self.audioOutputNode = CallControllerButtonItemNode() - self.cameraButtonNode = CallControllerButtonItemNode() - self.leaveNode = CallControllerButtonItemNode() + self.audioButton = CallControllerButtonItemNode() + self.cameraButton = CallControllerButtonItemNode(largeButtonSize: sideButtonSize.width) + self.switchCameraButton = CallControllerButtonItemNode() + self.switchCameraButton.alpha = 0.0 + self.switchCameraButton.isUserInteractionEnabled = false + self.leaveButton = CallControllerButtonItemNode() self.actionButton = VoiceChatActionButton() + + if self.isScheduling { + self.cameraButton.alpha = 0.0 + self.cameraButton.isUserInteractionEnabled = false + self.audioButton.alpha = 0.0 + self.audioButton.isUserInteractionEnabled = false + self.leaveButton.alpha = 0.0 + self.leaveButton.isUserInteractionEnabled = false + } self.leftBorderNode = ASDisplayNode() self.leftBorderNode.backgroundColor = panelBackgroundColor @@ -827,23 +1091,83 @@ public final class VoiceChatController: ViewController { self.rightBorderNode.isUserInteractionEnabled = false self.rightBorderNode.clipsToBounds = false - super.init() + self.mainStageContainerNode = ASDisplayNode() + self.mainStageContainerNode.clipsToBounds = true + self.mainStageContainerNode.isUserInteractionEnabled = false + self.mainStageContainerNode.isHidden = true - let statePromise = ValuePromise(State(), ignoreRepeated: true) - let stateValue = Atomic(value: State()) - let updateState: ((State) -> State) -> Void = { f in - statePromise.set(stateValue.modify { f($0) }) - } + self.mainStageBackgroundNode = ASDisplayNode() + self.mainStageBackgroundNode.backgroundColor = .black + self.mainStageBackgroundNode.alpha = 0.0 + self.mainStageBackgroundNode.isUserInteractionEnabled = false + + self.mainStageNode = VoiceChatMainStageNode(context: self.context, call: self.call) + + self.transitionMaskView = UIView() + self.transitionMaskTopFillLayer = CALayer() + self.transitionMaskTopFillLayer.backgroundColor = UIColor.white.cgColor + self.transitionMaskTopFillLayer.opacity = 0.0 + + self.transitionMaskFillLayer = CALayer() + self.transitionMaskFillLayer.backgroundColor = UIColor.white.cgColor + + self.transitionMaskGradientLayer = CAGradientLayer() + self.transitionMaskGradientLayer.colors = [UIColor.white.cgColor, UIColor.white.withAlphaComponent(0.0).cgColor] + self.transitionMaskGradientLayer.locations = [0.0, 1.0] + self.transitionMaskGradientLayer.startPoint = CGPoint(x: 0.0, y: 0.0) + self.transitionMaskGradientLayer.endPoint = CGPoint(x: 0.0, y: 1.0) + + self.transitionMaskBottomFillLayer = CALayer() + self.transitionMaskBottomFillLayer.backgroundColor = UIColor.white.cgColor + self.transitionMaskBottomFillLayer.opacity = 0.0 + + self.transitionMaskView.layer.addSublayer(self.transitionMaskTopFillLayer) + self.transitionMaskView.layer.addSublayer(self.transitionMaskFillLayer) + self.transitionMaskView.layer.addSublayer(self.transitionMaskGradientLayer) + self.transitionMaskView.layer.addSublayer(self.transitionMaskBottomFillLayer) + + self.transitionContainerNode = ASDisplayNode() + self.transitionContainerNode.clipsToBounds = true + self.transitionContainerNode.isUserInteractionEnabled = false + self.transitionContainerNode.view.mask = self.transitionMaskView +// self.transitionContainerNode.view.addSubview(self.transitionMaskView) + + self.scheduleTextNode = ImmediateTextNode() + self.scheduleTextNode.isHidden = !self.isScheduling + self.scheduleTextNode.isUserInteractionEnabled = false + self.scheduleTextNode.textAlignment = .center + self.scheduleTextNode.maximumNumberOfLines = 4 + + self.scheduleCancelButton = SolidRoundedButtonNode(title: self.presentationData.strings.Common_Cancel, theme: SolidRoundedButtonTheme(backgroundColor: UIColor(rgb: 0x2b2b2f), foregroundColor: .white), height: 52.0, cornerRadius: 10.0) + self.scheduleCancelButton.isHidden = !self.isScheduling + + self.dateFormatter = DateFormatter() + self.dateFormatter.timeStyle = .none + self.dateFormatter.dateStyle = .short + self.dateFormatter.timeZone = TimeZone.current + + self.timerNode = VoiceChatTimerNode(strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat) + self.timerNode.isHidden = true + + super.init() let context = self.context let currentAccountPeer = self.context.account.postbox.loadedPeerWithId(context.account.peerId) |> map { peer in return [FoundPeer(peer: peer, subscribers: nil)] } + + self.isNoiseSuppressionEnabledDisposable = (call.isNoiseSuppressionEnabled + |> deliverOnMainQueue).start(next: { [weak self] value in + guard let strongSelf = self else { + return + } + strongSelf.isNoiseSuppressionEnabled = value + }) let displayAsPeers: Signal<[FoundPeer], NoError> = currentAccountPeer |> then( - combineLatest(currentAccountPeer, cachedGroupCallDisplayAsAvailablePeers(account: context.account, peerId: call.peerId)) + combineLatest(currentAccountPeer, context.engine.calls.cachedGroupCallDisplayAsAvailablePeers(peerId: call.peerId)) |> map { currentAccountPeer, availablePeers -> [FoundPeer] in var result = currentAccountPeer result.append(contentsOf: availablePeers) @@ -857,33 +1181,17 @@ public final class VoiceChatController: ViewController { self.itemInteraction = Interaction(updateIsMuted: { [weak self] peerId, isMuted in let _ = self?.call.updateMuteState(peerId: peerId, isMuted: isMuted) - }, openPeer: { [weak self] peerId in - if let strongSelf = self { - /*let context = strongSelf.context - strongSelf.controller?.dismiss(completion: { - Queue.mainQueue().justDispatch { - context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peerId), keepStack: .always, purposefulAction: {}, peekData: nil)) - } - })*/ - for entry in strongSelf.currentEntries { - switch entry { - case let .peer(peer): - if peer.peer.id == peerId { - if let source = peer.ssrc { - if strongSelf.currentDominantSpeakerWithVideo?.0 != peerId || strongSelf.currentDominantSpeakerWithVideo?.1 != source { - strongSelf.currentDominantSpeakerWithVideo = (peerId, source) - strongSelf.call.setFullSizeVideo(peerId: peerId) - strongSelf.mainVideoContainer?.updatePeer(peer: (peerId: peerId, source: source)) - } else { - strongSelf.currentDominantSpeakerWithVideo = nil - strongSelf.call.setFullSizeVideo(peerId: nil) - strongSelf.mainVideoContainer?.updatePeer(peer: nil) - } - } - } - default: - break + }, switchToPeer: { [weak self] peerId, videoEndpointId, expand in + if let strongSelf = self, strongSelf.connectedOnce { + if expand, let videoEndpointId = videoEndpointId { + strongSelf.currentDominantSpeaker = (peerId, videoEndpointId, CACurrentMediaTime() + 3.0) + strongSelf.updateDisplayMode(.fullscreen(controlsHidden: false)) + } else { + strongSelf.currentForcedSpeaker = nil + if peerId != strongSelf.currentDominantSpeaker?.0 || (videoEndpointId != nil && videoEndpointId != strongSelf.currentDominantSpeaker?.1) { + strongSelf.currentDominantSpeaker = (peerId, videoEndpointId, CACurrentMediaTime()) } + strongSelf.updateMainVideo(waitForFullSize: true, updateMembers: true, force: true) } } }, openInvite: { [weak self] in @@ -905,12 +1213,11 @@ public final class VoiceChatController: ViewController { } if let groupPeer = groupPeer as? TelegramChannel { - var canInvite = true + var canInviteMembers = true if case .broadcast = groupPeer.info, !(groupPeer.addressName?.isEmpty ?? true) { - canInvite = false + canInviteMembers = false } - - if !canInvite { + if !canInviteMembers { if let inviteLinks = inviteLinks { strongSelf.presentShare(inviteLinks) } @@ -923,7 +1230,7 @@ public final class VoiceChatController: ViewController { filters.append(.disable(Array(currentCallMembers.map { $0.peer.id }))) } if let groupPeer = groupPeer as? TelegramChannel { - if !groupPeer.hasPermission(.inviteMembers) { + if !groupPeer.hasPermission(.inviteMembers) && inviteLinks?.listenerLink == nil { filters.append(.excludeNonMembers) } } else if let groupPeer = groupPeer as? TelegramGroup { @@ -941,7 +1248,6 @@ public final class VoiceChatController: ViewController { } let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } - if peer.id == strongSelf.callState?.myPeerId { return } @@ -952,154 +1258,171 @@ public final class VoiceChatController: ViewController { strongSelf.presentUndoOverlay(content: .invitedToVoiceChat(context: strongSelf.context, peer: participant.peer, text: strongSelf.presentationData.strings.VoiceChat_InvitedPeerText(peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).0), action: { _ in return false }) } } else { - let text: String - if let groupPeer = groupPeer as? TelegramChannel, case .broadcast = groupPeer.info { - text = strongSelf.presentationData.strings.VoiceChat_InviteMemberToChannelFirstText(peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder), groupPeer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).0 + if let groupPeer = groupPeer as? TelegramChannel, let listenerLink = inviteLinks?.listenerLink, !groupPeer.hasPermission(.inviteMembers) { + let text = strongSelf.presentationData.strings.VoiceChat_SendPublicLinkText(peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder), groupPeer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).0 + + strongSelf.controller?.present(textAlertController(context: strongSelf.context, forceTheme: strongSelf.darkTheme, title: nil, text: text, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: presentationData.strings.VoiceChat_SendPublicLinkSend, action: { [weak self] in + dismissController?() + + if let strongSelf = self { + let _ = (enqueueMessages(account: strongSelf.context.account, peerId: peer.id, messages: [.message(text: listenerLink, attributes: [], mediaReference: nil, replyToMessageId: nil, localGroupingKey: nil, correlationId: nil)]) + |> deliverOnMainQueue).start(next: { [weak self] _ in + if let strongSelf = self { + strongSelf.presentUndoOverlay(content: .forward(savedMessages: false, text: strongSelf.presentationData.strings.UserInfo_LinkForwardTooltip_Chat_One(peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).0), action: { _ in return true }) + } + }) + } + })]), in: .window(.root)) } else { - text = strongSelf.presentationData.strings.VoiceChat_InviteMemberToGroupFirstText(peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder), groupPeer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).0 - } - - strongSelf.controller?.present(textAlertController(context: strongSelf.context, forceTheme: strongSelf.darkTheme, title: nil, text: text, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: presentationData.strings.VoiceChat_InviteMemberToGroupFirstAdd, action: { - guard let strongSelf = self else { - return + let text: String + if let groupPeer = groupPeer as? TelegramChannel, case .broadcast = groupPeer.info { + text = strongSelf.presentationData.strings.VoiceChat_InviteMemberToChannelFirstText(peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder), groupPeer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).0 + } else { + text = strongSelf.presentationData.strings.VoiceChat_InviteMemberToGroupFirstText(peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder), groupPeer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).0 } - if let groupPeer = groupPeer as? TelegramChannel { - let selfController = strongSelf.controller - let inviteDisposable = strongSelf.inviteDisposable - var inviteSignal = strongSelf.context.peerChannelMemberCategoriesContextsManager.addMembers(account: strongSelf.context.account, peerId: groupPeer.id, memberIds: [peer.id]) - var cancelImpl: (() -> Void)? - let progressSignal = Signal { [weak selfController] subscriber in - let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: { - cancelImpl?() - })) - selfController?.present(controller, in: .window(.root)) - return ActionDisposable { [weak controller] in - Queue.mainQueue().async() { - controller?.dismiss() - } - } - } - |> runOn(Queue.mainQueue()) - |> delay(0.15, queue: Queue.mainQueue()) - let progressDisposable = progressSignal.start() - - inviteSignal = inviteSignal - |> afterDisposed { - Queue.mainQueue().async { - progressDisposable.dispose() - } - } - cancelImpl = { - inviteDisposable.set(nil) + strongSelf.controller?.present(textAlertController(context: strongSelf.context, forceTheme: strongSelf.darkTheme, title: nil, text: text, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: presentationData.strings.VoiceChat_InviteMemberToGroupFirstAdd, action: { + guard let strongSelf = self else { + return } - inviteDisposable.set((inviteSignal |> deliverOnMainQueue).start(error: { error in - dismissController?() - guard let strongSelf = self else { - return - } - let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } - - let text: String - switch error { - case .limitExceeded: - text = presentationData.strings.Channel_ErrorAddTooMuch - case .tooMuchJoined: - text = presentationData.strings.Invite_ChannelsTooMuch - case .generic: - text = presentationData.strings.Login_UnknownError - case .restricted: - text = presentationData.strings.Channel_ErrorAddBlocked - case .notMutualContact: - if case .broadcast = groupPeer.info { - text = presentationData.strings.Channel_AddUserLeftError - } else { - text = presentationData.strings.GroupInfo_AddUserLeftError + if let groupPeer = groupPeer as? TelegramChannel { + let selfController = strongSelf.controller + let inviteDisposable = strongSelf.inviteDisposable + var inviteSignal = strongSelf.context.peerChannelMemberCategoriesContextsManager.addMembers(engine: strongSelf.context.engine, peerId: groupPeer.id, memberIds: [peer.id]) + var cancelImpl: (() -> Void)? + let progressSignal = Signal { [weak selfController] subscriber in + let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: { + cancelImpl?() + })) + selfController?.present(controller, in: .window(.root)) + return ActionDisposable { [weak controller] in + Queue.mainQueue().async() { + controller?.dismiss() } - case .botDoesntSupportGroups: - text = presentationData.strings.Channel_BotDoesntSupportGroups - case .tooMuchBots: - text = presentationData.strings.Channel_TooMuchBots - case .bot: - text = presentationData.strings.Login_UnknownError - } - strongSelf.controller?.present(textAlertController(context: strongSelf.context, forceTheme: strongSelf.darkTheme, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root)) - }, completed: { - guard let strongSelf = self else { - dismissController?() - return - } - dismissController?() - - if strongSelf.call.invitePeer(peer.id) { - strongSelf.presentUndoOverlay(content: .invitedToVoiceChat(context: strongSelf.context, peer: peer, text: strongSelf.presentationData.strings.VoiceChat_InvitedPeerText(peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).0), action: { _ in return false }) - } - })) - } else if let groupPeer = groupPeer as? TelegramGroup { - let selfController = strongSelf.controller - let inviteDisposable = strongSelf.inviteDisposable - var inviteSignal = addGroupMember(account: strongSelf.context.account, peerId: groupPeer.id, memberId: peer.id) - var cancelImpl: (() -> Void)? - let progressSignal = Signal { [weak selfController] subscriber in - let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: { - cancelImpl?() - })) - selfController?.present(controller, in: .window(.root)) - return ActionDisposable { [weak controller] in - Queue.mainQueue().async() { - controller?.dismiss() } } - } - |> runOn(Queue.mainQueue()) - |> delay(0.15, queue: Queue.mainQueue()) - let progressDisposable = progressSignal.start() - - inviteSignal = inviteSignal - |> afterDisposed { - Queue.mainQueue().async { - progressDisposable.dispose() - } - } - cancelImpl = { - inviteDisposable.set(nil) - } - - inviteDisposable.set((inviteSignal |> deliverOnMainQueue).start(error: { error in - dismissController?() - guard let strongSelf = self else { - return - } - let context = strongSelf.context - let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } + |> runOn(Queue.mainQueue()) + |> delay(0.15, queue: Queue.mainQueue()) + let progressDisposable = progressSignal.start() - switch error { - case .privacy: - let _ = (strongSelf.context.account.postbox.loadedPeerWithId(peer.id) - |> 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)) - }) - case .notMutualContact: - strongSelf.controller?.present(textAlertController(context: context, title: nil, text: presentationData.strings.GroupInfo_AddUserLeftError, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root)) - case .tooManyChannels: - strongSelf.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)) - case .groupFull, .generic: - strongSelf.controller?.present(textAlertController(context: strongSelf.context, forceTheme: strongSelf.darkTheme, title: nil, text: presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + inviteSignal = inviteSignal + |> afterDisposed { + Queue.mainQueue().async { + progressDisposable.dispose() + } } - }, completed: { - guard let strongSelf = self else { + cancelImpl = { + inviteDisposable.set(nil) + } + + inviteDisposable.set((inviteSignal |> deliverOnMainQueue).start(error: { error in dismissController?() - return + guard let strongSelf = self else { + return + } + let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } + + let text: String + switch error { + case .limitExceeded: + text = presentationData.strings.Channel_ErrorAddTooMuch + case .tooMuchJoined: + text = presentationData.strings.Invite_ChannelsTooMuch + case .generic: + text = presentationData.strings.Login_UnknownError + case .restricted: + text = presentationData.strings.Channel_ErrorAddBlocked + case .notMutualContact: + if case .broadcast = groupPeer.info { + text = presentationData.strings.Channel_AddUserLeftError + } else { + text = presentationData.strings.GroupInfo_AddUserLeftError + } + case .botDoesntSupportGroups: + text = presentationData.strings.Channel_BotDoesntSupportGroups + case .tooMuchBots: + text = presentationData.strings.Channel_TooMuchBots + case .bot: + text = presentationData.strings.Login_UnknownError + } + strongSelf.controller?.present(textAlertController(context: strongSelf.context, forceTheme: strongSelf.darkTheme, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + }, completed: { + guard let strongSelf = self else { + dismissController?() + return + } + dismissController?() + + if strongSelf.call.invitePeer(peer.id) { + strongSelf.presentUndoOverlay(content: .invitedToVoiceChat(context: strongSelf.context, peer: peer, text: strongSelf.presentationData.strings.VoiceChat_InvitedPeerText(peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).0), action: { _ in return false }) + } + })) + } else if let groupPeer = groupPeer as? TelegramGroup { + let selfController = strongSelf.controller + let inviteDisposable = strongSelf.inviteDisposable + var inviteSignal = strongSelf.context.engine.peers.addGroupMember(peerId: groupPeer.id, memberId: peer.id) + var cancelImpl: (() -> Void)? + let progressSignal = Signal { [weak selfController] subscriber in + let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: { + cancelImpl?() + })) + selfController?.present(controller, in: .window(.root)) + return ActionDisposable { [weak controller] in + Queue.mainQueue().async() { + controller?.dismiss() + } + } } - dismissController?() + |> runOn(Queue.mainQueue()) + |> delay(0.15, queue: Queue.mainQueue()) + let progressDisposable = progressSignal.start() - if strongSelf.call.invitePeer(peer.id) { - strongSelf.presentUndoOverlay(content: .invitedToVoiceChat(context: strongSelf.context, peer: peer, text: strongSelf.presentationData.strings.VoiceChat_InvitedPeerText(peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).0), action: { _ in return false }) + inviteSignal = inviteSignal + |> afterDisposed { + Queue.mainQueue().async { + progressDisposable.dispose() + } } - })) - } - })]), in: .window(.root)) + cancelImpl = { + inviteDisposable.set(nil) + } + + inviteDisposable.set((inviteSignal |> deliverOnMainQueue).start(error: { error in + dismissController?() + guard let strongSelf = self else { + return + } + let context = strongSelf.context + let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } + + switch error { + case .privacy: + let _ = (strongSelf.context.account.postbox.loadedPeerWithId(peer.id) + |> 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)) + }) + case .notMutualContact: + strongSelf.controller?.present(textAlertController(context: context, title: nil, text: presentationData.strings.GroupInfo_AddUserLeftError, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + case .tooManyChannels: + strongSelf.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)) + case .groupFull, .generic: + strongSelf.controller?.present(textAlertController(context: strongSelf.context, forceTheme: strongSelf.darkTheme, title: nil, text: presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + } + }, completed: { + guard let strongSelf = self else { + dismissController?() + return + } + dismissController?() + + if strongSelf.call.invitePeer(peer.id) { + strongSelf.presentUndoOverlay(content: .invitedToVoiceChat(context: strongSelf.context, peer: peer, text: strongSelf.presentationData.strings.VoiceChat_InvitedPeerText(peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).0), action: { _ in return false }) + } + })) + } + })]), in: .window(.root)) + } } }) controller.copyInviteLink = { @@ -1111,7 +1434,9 @@ public final class VoiceChatController: ViewController { let callPeerId = strongSelf.call.peerId let _ = (strongSelf.context.account.postbox.transaction { transaction -> String? in - if let peer = transaction.getPeer(callPeerId), let addressName = peer.addressName, !addressName.isEmpty { + if let link = inviteLinks?.listenerLink { + return link + } else if let peer = transaction.getPeer(callPeerId), let addressName = peer.addressName, !addressName.isEmpty { return "https://t.me/\(addressName)" } else if let cachedData = transaction.getPeerCachedData(peerId: callPeerId) { if let cachedData = cachedData as? CachedChannelData { @@ -1139,46 +1464,46 @@ public final class VoiceChatController: ViewController { } strongSelf.controller?.push(controller) }) - }, peerContextAction: { [weak self] entry, sourceNode, gesture in - guard let strongSelf = self, let controller = strongSelf.controller, let sourceNode = sourceNode as? ContextExtractedContentContainingNode else { + }, peerContextAction: { [weak self] entry, sourceNode, gesture, fullscreen in + guard let strongSelf = self, let sourceNode = sourceNode as? ContextExtractedContentContainingNode else { return } let muteStatePromise = Promise(entry.muteState) - let itemsForEntry: (PeerEntry, GroupCallParticipantsContext.Participant.MuteState?) -> [ContextMenuItem] = { entry, muteState in + let itemsForEntry: (VoiceChatPeerEntry, GroupCallParticipantsContext.Participant.MuteState?) -> [ContextMenuItem] = { entry, muteState in var items: [ContextMenuItem] = [] + var hasVolumeSlider = false let peer = entry.peer if let muteState = muteState, !muteState.canUnmute || muteState.mutedByYou { } else { - let minValue: CGFloat - if let callState = strongSelf.callState, callState.canManageCall && callState.adminIds.contains(peer.id) && muteState != nil { - minValue = 0.01 - } else { - minValue = 0.0 - } - items.append(.custom(VoiceChatVolumeContextItem(minValue: minValue, value: entry.volume.flatMap { CGFloat($0) / 10000.0 } ?? 1.0, valueChanged: { newValue, finished in - if finished && newValue.isZero { - let updatedMuteState = strongSelf.call.updateMuteState(peerId: peer.id, isMuted: true) - muteStatePromise.set(.single(updatedMuteState)) + if entry.canManageCall || !entry.isMyPeer { + hasVolumeSlider = true + + let minValue: CGFloat + if let callState = strongSelf.callState, callState.canManageCall && callState.adminIds.contains(peer.id) && muteState != nil { + minValue = 0.01 } else { - strongSelf.call.setVolume(peerId: peer.id, volume: Int32(newValue * 10000), sync: finished) + minValue = 0.0 } - }), true)) + items.append(.custom(VoiceChatVolumeContextItem(minValue: minValue, value: entry.volume.flatMap { CGFloat($0) / 10000.0 } ?? 1.0, valueChanged: { newValue, finished in + if finished && newValue.isZero { + let updatedMuteState = strongSelf.call.updateMuteState(peerId: peer.id, isMuted: true) + muteStatePromise.set(.single(updatedMuteState)) + } else { + strongSelf.call.setVolume(peerId: peer.id, volume: Int32(newValue * 10000), sync: finished) + } + }), true)) + } } - /*items.append(.action(ContextMenuActionItem(text: "Toggle Full Screen", icon: { theme in - return nil - }, action: { _, f in - guard let strongSelf = self else { - return - } - - strongSelf.itemInteraction?.openPeer(peer.id) - f(.default) - })))*/ - + if entry.isMyPeer && !hasVolumeSlider && ((entry.about?.isEmpty ?? true) || entry.peer.smallProfileImage == nil) { + items.append(.custom(VoiceChatInfoContextItem(text: strongSelf.presentationData.strings.VoiceChat_ImproveYourProfileText, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Tip"), color: theme.actionSheet.primaryTextColor) + }), true)) + } + if peer.id == strongSelf.callState?.myPeerId { if entry.raisedHand { items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_CancelSpeakRequest, icon: { theme in @@ -1192,6 +1517,76 @@ public final class VoiceChatController: ViewController { f(.default) }))) } + items.append(.action(ContextMenuActionItem(text: peer.smallProfileImage == nil ? strongSelf.presentationData.strings.VoiceChat_AddPhoto : strongSelf.presentationData.strings.VoiceChat_ChangePhoto, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Camera"), color: theme.actionSheet.primaryTextColor) + }, action: { _, f in + guard let strongSelf = self else { + return + } + + f(.default) + Queue.mainQueue().after(0.1) { + strongSelf.openAvatarForEditing(fromGallery: false, completion: {}) + } + }))) + + items.append(.action(ContextMenuActionItem(text: (entry.about?.isEmpty ?? true) ? strongSelf.presentationData.strings.VoiceChat_AddBio : strongSelf.presentationData.strings.VoiceChat_EditBio, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Info"), color: theme.actionSheet.primaryTextColor) + }, action: { _, f in + guard let strongSelf = self else { + return + } + f(.default) + + Queue.mainQueue().after(0.1) { + let maxBioLength: Int + if peer.id.namespace == Namespaces.Peer.CloudUser { + maxBioLength = 70 + } else { + maxBioLength = 100 + } + let controller = voiceChatTitleEditController(sharedContext: strongSelf.context.sharedContext, account: strongSelf.context.account, forceTheme: strongSelf.darkTheme, title: presentationData.strings.VoiceChat_EditBioTitle, text: presentationData.strings.VoiceChat_EditBioText, placeholder: presentationData.strings.VoiceChat_EditBioPlaceholder, doneButtonTitle: presentationData.strings.VoiceChat_EditBioSave, value: entry.about, maxLength: maxBioLength, apply: { bio in + if let strongSelf = self, let bio = bio { + if peer.id.namespace == Namespaces.Peer.CloudUser { + let _ = (strongSelf.context.engine.accountData.updateAbout(about: bio) + |> `catch` { _ -> Signal in + return .complete() + }).start() + } else { + let _ = (strongSelf.context.engine.peers.updatePeerDescription(peerId: peer.id, description: bio) + |> `catch` { _ -> Signal in + return .complete() + }).start() + } + + strongSelf.presentUndoOverlay(content: .info(text: strongSelf.presentationData.strings.VoiceChat_EditBioSuccess), action: { _ in return false }) + } + }) + self?.controller?.present(controller, in: .window(.root)) + } + }))) + + if let peer = peer as? TelegramUser { + items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_ChangeName, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Call/Context Menu/ChangeName"), color: theme.actionSheet.primaryTextColor) + }, action: { _, f in + guard let strongSelf = self else { + return + } + f(.default) + + Queue.mainQueue().after(0.1) { + let controller = voiceChatUserNameController(sharedContext: strongSelf.context.sharedContext, account: strongSelf.context.account, forceTheme: strongSelf.darkTheme, title: presentationData.strings.VoiceChat_ChangeNameTitle, firstNamePlaceholder: presentationData.strings.UserInfo_FirstNamePlaceholder, lastNamePlaceholder: presentationData.strings.UserInfo_LastNamePlaceholder, doneButtonTitle: presentationData.strings.VoiceChat_EditBioSave, firstName: peer.firstName, lastName: peer.lastName, maxLength: 128, apply: { firstAndLastName in + if let strongSelf = self, let (firstName, lastName) = firstAndLastName { + let _ = context.engine.accountData.updateAccountPeerName(firstName: firstName, lastName: lastName).start() + + strongSelf.presentUndoOverlay(content: .info(text: strongSelf.presentationData.strings.VoiceChat_EditNameSuccess), action: { _ in return false }) + } + }) + self?.controller?.present(controller, in: .window(.root)) + } + }))) + } } else { if let callState = strongSelf.callState, (callState.canManageCall || callState.adminIds.contains(strongSelf.context.account.peerId)) { if callState.adminIds.contains(peer.id) { @@ -1219,6 +1614,8 @@ public final class VoiceChatController: ViewController { let _ = strongSelf.call.updateMuteState(peerId: peer.id, isMuted: false) f(.default) + + strongSelf.presentUndoOverlay(content: .voiceChatCanSpeak(text: presentationData.strings.VoiceChat_UserCanNowSpeak(entry.peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).0), action: { _ in return true }) }))) } else { items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_MutePeer, icon: { theme in @@ -1261,11 +1658,16 @@ public final class VoiceChatController: ViewController { let openTitle: String let openIcon: UIImage? - if peer.id.namespace == Namespaces.Peer.CloudChannel { - openTitle = strongSelf.presentationData.strings.VoiceChat_OpenChannel - openIcon = UIImage(bundleImageName: "Chat/Context Menu/Channels") + if [Namespaces.Peer.CloudChannel, Namespaces.Peer.CloudGroup].contains(peer.id.namespace) { + if let peer = peer as? TelegramChannel, case .broadcast = peer.info { + openTitle = strongSelf.presentationData.strings.VoiceChat_OpenChannel + openIcon = UIImage(bundleImageName: "Chat/Context Menu/Channels") + } else { + openTitle = strongSelf.presentationData.strings.VoiceChat_OpenGroup + openIcon = UIImage(bundleImageName: "Chat/Context Menu/Groups") + } } else { - openTitle = strongSelf.presentationData.strings.VoiceChat_OpenChat + openTitle = strongSelf.presentationData.strings.Conversation_ContextMenuSendMessage openIcon = UIImage(bundleImageName: "Chat/Context Menu/Message") } items.append(.action(ContextMenuActionItem(text: openTitle, icon: { theme in @@ -1277,15 +1679,15 @@ public final class VoiceChatController: ViewController { let context = strongSelf.context strongSelf.controller?.dismiss(completion: { - Queue.mainQueue().justDispatch { + Queue.mainQueue().after(0.3) { context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer.id), keepStack: .always, purposefulAction: {}, peekData: nil)) } }) - f(.default) + f(.dismissWithoutContent) }))) - if let callState = strongSelf.callState, (callState.canManageCall && !callState.adminIds.contains(peer.id) && peer.id.namespace != Namespaces.Peer.CloudChannel) { + if let callState = strongSelf.callState, (callState.canManageCall && !callState.adminIds.contains(peer.id)), peer.id != strongSelf.call.peerId { items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_RemovePeer, textColor: .destructive, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Clear"), color: theme.actionSheet.destructiveActionTextColor) }, action: { [weak self] c, _ in @@ -1312,7 +1714,7 @@ public final class VoiceChatController: ViewController { return } - let _ = strongSelf.context.peerChannelMemberCategoriesContextsManager.updateMemberBannedRights(account: strongSelf.context.account, peerId: strongSelf.call.peerId, memberId: peer.id, bannedRights: TelegramChatBannedRights(flags: [.banReadMessages], untilDate: Int32.max)).start() + let _ = strongSelf.context.peerChannelMemberCategoriesContextsManager.updateMemberBannedRights(engine: strongSelf.context.engine, peerId: strongSelf.call.peerId, memberId: peer.id, bannedRights: TelegramChatBannedRights(flags: [.banReadMessages], untilDate: Int32.max)).start() strongSelf.call.removedPeer(peer.id) strongSelf.presentUndoOverlay(content: .banned(text: strongSelf.presentationData.strings.VoiceChat_RemovedPeerText(peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).0), action: { _ in return false }) @@ -1340,53 +1742,124 @@ public final class VoiceChatController: ViewController { return itemsForEntry(entry, muteState) } - let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData.withUpdated(theme: strongSelf.darkTheme), source: .extracted(VoiceChatContextExtractedContentSource(controller: controller, sourceNode: sourceNode, keepInPlace: false, blurBackground: true)), items: items, reactionItems: [], gesture: gesture) - strongSelf.controller?.presentInGlobalOverlay(contextController) - }, setPeerIdWithRevealedOptions: { peerId, _ in - updateState { state in - var updated = state - updated.revealedPeerId = peerId - return updated + var centerVertically = entry.peer.smallProfileImage != nil || (!fullscreen && entry.effectiveVideoEndpointId != nil) + if let (layout, _) = strongSelf.validLayout, case .regular = layout.metrics.widthClass { + centerVertically = false } - }, getPeerVideo: { [weak self] ssrc in + + var useMaskView = true + if case .fullscreen = strongSelf.displayMode { + useMaskView = false + } + + let dismissPromise = ValuePromise(false) + let source = VoiceChatContextExtractedContentSource(sourceNode: sourceNode, maskView: useMaskView ? strongSelf.transitionMaskView : nil, keepInPlace: false, blurBackground: true, centerVertically: centerVertically, shouldBeDismissed: dismissPromise.get(), animateTransitionIn: { [weak self] in + if let strongSelf = self { + strongSelf.animatingContextMenu = true + strongSelf.updateDecorationsLayout(transition: .immediate) + if strongSelf.isLandscape { + strongSelf.transitionMaskTopFillLayer.opacity = 1.0 + } + strongSelf.transitionContainerNode.view.mask = nil + strongSelf.transitionMaskBottomFillLayer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4, removeOnCompletion: false, completion: { [weak self] _ in + Queue.mainQueue().after(0.3) { + self?.transitionMaskTopFillLayer.opacity = 0.0 + self?.transitionMaskBottomFillLayer.removeAllAnimations() + self?.animatingContextMenu = false + self?.updateDecorationsLayout(transition: .immediate) + } + }) + } + }, animateTransitionOut: { [weak self] in + if let strongSelf = self { + strongSelf.animatingContextMenu = true + strongSelf.updateDecorationsLayout(transition: .immediate) + strongSelf.transitionMaskTopFillLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.4) + strongSelf.transitionMaskBottomFillLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.4, completion: { [weak self] _ in + self?.animatingContextMenu = false + self?.updateDecorationsLayout(transition: .immediate) + self?.transitionContainerNode.view.mask = self?.transitionMaskView + }) + } + }) + sourceNode.requestDismiss = { + dismissPromise.set(true) + } + + let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData.withUpdated(theme: strongSelf.darkTheme), source: .extracted(source), items: items, reactionItems: [], gesture: gesture) + contextController.useComplexItemsTransitionAnimation = true + strongSelf.controller?.presentInGlobalOverlay(contextController) + }, getPeerVideo: { [weak self] endpointId, position in guard let strongSelf = self else { return nil } - for (_, listSsrc, videoNode) in strongSelf.videoNodes { - if listSsrc == ssrc { + var ignore = false + if case .mainstage = position { + ignore = false + } else if case .fullscreen = strongSelf.displayMode, !strongSelf.isPanning { + ignore = ![.mainstage, .list].contains(position) + } else { + ignore = position != .tile + } + if ignore { + return nil + } + if !strongSelf.readyVideoEndpointIds.contains(endpointId) { + return nil + } + for (listEndpointId, videoNode) in strongSelf.videoNodes { + if listEndpointId == endpointId { + if position != .mainstage && videoNode.isMainstageExclusive { + return nil + } return videoNode } } return nil }) + self.itemInteraction?.updateAvatarPromise = self.updateAvatarPromise self.topPanelNode.addSubnode(self.topPanelEdgeNode) self.topPanelNode.addSubnode(self.topPanelBackgroundNode) self.topPanelNode.addSubnode(self.titleNode) self.topPanelNode.addSubnode(self.optionsButton) self.topPanelNode.addSubnode(self.closeButton) - self.topPanelNode.addSubnode(self.topCornersNode) + self.topPanelNode.addSubnode(self.panelButton) - self.bottomPanelNode.addSubnode(self.bottomCornersNode) - self.bottomPanelNode.addSubnode(self.bottomPanelBackgroundNode) - self.bottomPanelNode.addSubnode(self.audioOutputNode) - //self.bottomPanelNode.addSubnode(self.cameraButtonNode) - self.bottomPanelNode.addSubnode(self.leaveNode) + self.bottomPanelNode.addSubnode(self.cameraButton) + self.bottomPanelNode.addSubnode(self.audioButton) + self.bottomPanelNode.addSubnode(self.switchCameraButton) + self.bottomPanelNode.addSubnode(self.leaveButton) self.bottomPanelNode.addSubnode(self.actionButton) + self.bottomPanelNode.addSubnode(self.scheduleCancelButton) self.addSubnode(self.dimNode) self.addSubnode(self.contentContainer) self.contentContainer.addSubnode(self.backgroundNode) - self.contentContainer.addSubnode(self.listNode) - if let mainVideoContainer = self.mainVideoContainer { - self.contentContainer.addSubnode(mainVideoContainer) - } + self.contentContainer.addSubnode(self.listContainer) self.contentContainer.addSubnode(self.topPanelNode) - self.contentContainer.addSubnode(self.leftBorderNode) - self.contentContainer.addSubnode(self.rightBorderNode) + self.listContainer.addSubnode(self.listNode) + self.listContainer.addSubnode(self.leftBorderNode) + self.listContainer.addSubnode(self.rightBorderNode) + self.listContainer.addSubnode(self.bottomCornersNode) + self.listContainer.addSubnode(self.topCornersNode) + self.contentContainer.addSubnode(self.bottomGradientNode) + self.contentContainer.addSubnode(self.bottomPanelBackgroundNode) + self.contentContainer.addSubnode(self.tileGridNode) + self.contentContainer.addSubnode(self.mainStageContainerNode) + self.contentContainer.addSubnode(self.transitionContainerNode) self.contentContainer.addSubnode(self.bottomPanelNode) + self.contentContainer.addSubnode(self.timerNode) + self.contentContainer.addSubnode(self.scheduleTextNode) + self.contentContainer.addSubnode(self.fullscreenListContainer) + self.fullscreenListContainer.addSubnode(self.fullscreenListNode) + self.mainStageContainerNode.addSubnode(self.mainStageBackgroundNode) + self.mainStageContainerNode.addSubnode(self.mainStageNode) + + self.updateDecorationsColors() + let invitedPeers: Signal<[Peer], NoError> = self.call.invitedPeers |> mapToSignal { ids -> Signal<[Peer], NoError> in return context.account.postbox.transaction { transaction -> [Peer] in @@ -1414,18 +1887,40 @@ public final class VoiceChatController: ViewController { self.call.state, self.call.members, invitedPeers, - self.displayAsPeersPromise.get() + self.displayAsPeersPromise.get(), + self.context.account.postbox.preferencesView(keys: [PreferencesKeys.appConfiguration]) ) |> mapToThrottled { values in return .single(values) |> then(.complete() |> delay(0.1, queue: Queue.mainQueue())) - }).start(next: { [weak self] state, callMembers, invitedPeers, displayAsPeers in + }).start(next: { [weak self] state, callMembers, invitedPeers, displayAsPeers, preferencesView in guard let strongSelf = self else { return } + let appConfiguration: AppConfiguration = preferencesView.values[PreferencesKeys.appConfiguration] as? AppConfiguration ?? .defaultValue + let configuration = VoiceChatConfiguration.with(appConfiguration: appConfiguration) + strongSelf.configuration = configuration + + var animate = false if strongSelf.callState != state { + if let previousCallState = strongSelf.callState { + var networkStateUpdated = false + if case .connecting = previousCallState.networkState, case .connected = state.networkState { + networkStateUpdated = true + strongSelf.connectedOnce = true + } + var canUnmuteUpdated = false + if previousCallState.muteState?.canUnmute != state.muteState?.canUnmute { + canUnmuteUpdated = true + } + if previousCallState.isVideoEnabled != state.isVideoEnabled || (state.isVideoEnabled && networkStateUpdated) || canUnmuteUpdated { + strongSelf.animatingButtonsSwap = true + animate = true + } + } strongSelf.callState = state + strongSelf.mainStageNode.callState = state if let muteState = state.muteState, !muteState.canUnmute { if strongSelf.pushingToTalk { @@ -1442,27 +1937,32 @@ public final class VoiceChatController: ViewController { let subtitle = strongSelf.presentationData.strings.VoiceChat_Panel_Members(Int32(max(1, callMembers?.totalCount ?? 0))) strongSelf.currentSubtitle = subtitle - if let callState = strongSelf.callState, callState.canManageCall { - strongSelf.optionsButtonIsAvatar = false - strongSelf.optionsButton.isUserInteractionEnabled = true - strongSelf.optionsButton.alpha = 1.0 - } else if displayAsPeers.count > 1 { - strongSelf.optionsButtonIsAvatar = true - for peer in displayAsPeers { - if peer.peer.id == state.myPeerId { - strongSelf.optionsButton.setContent(.avatar(peer.peer)) - } - } - strongSelf.optionsButton.isUserInteractionEnabled = true - strongSelf.optionsButton.alpha = 1.0 - } else { - strongSelf.optionsButtonIsAvatar = false + if strongSelf.isScheduling { strongSelf.optionsButton.isUserInteractionEnabled = false strongSelf.optionsButton.alpha = 0.0 + strongSelf.closeButton.isUserInteractionEnabled = false + strongSelf.closeButton.alpha = 0.0 + strongSelf.panelButton.isUserInteractionEnabled = false + strongSelf.panelButton.alpha = 0.0 + } else { + if let (layout, _) = strongSelf.validLayout { + if case .regular = layout.metrics.widthClass, !strongSelf.peerIdToEndpointId.isEmpty { + strongSelf.panelButton.isUserInteractionEnabled = true + } else { + strongSelf.panelButton.isUserInteractionEnabled = false + } + } + if let callState = strongSelf.callState, callState.canManageCall { + strongSelf.optionsButton.isUserInteractionEnabled = true + } else if displayAsPeers.count > 1 { + strongSelf.optionsButton.isUserInteractionEnabled = true + } else { + strongSelf.optionsButton.isUserInteractionEnabled = true + } } if let (layout, navigationHeight) = strongSelf.validLayout { - strongSelf.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: .immediate) + strongSelf.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: animate ? .animated(duration: 0.4, curve: .spring) : .immediate) } }) @@ -1486,9 +1986,8 @@ public final class VoiceChatController: ViewController { strongSelf.titleNode.isRecording = isRecording } if !strongSelf.didSetDataReady { - strongSelf.updateMembers(muteState: strongSelf.effectiveMuteState, callMembers: strongSelf.currentCallMembers ?? ([], nil), invitedPeers: strongSelf.currentInvitedPeers ?? [], speakingPeers: strongSelf.currentSpeakingPeers ?? Set()) - strongSelf.didSetDataReady = true + strongSelf.updateMembers() strongSelf.controller?.dataReady.set(true) } }) @@ -1518,24 +2017,43 @@ public final class VoiceChatController: ViewController { levels = levels.filter { $0.0 != strongSelf.callState?.myPeerId } } - var maxLevelWithVideo: (PeerId, UInt32, Float)? + var maxLevelWithVideo: (PeerId, Float)? for (peerId, source, level, hasSpeech) in levels { - if hasSpeech && source != 0 { - if let (_, _, currentLevel) = maxLevelWithVideo { + let hasVideo = strongSelf.peerIdToEndpointId[peerId] != nil + if hasSpeech && source != 0 && hasVideo { + if let (_, currentLevel) = maxLevelWithVideo { if currentLevel < level { - maxLevelWithVideo = (peerId, source, level) + maxLevelWithVideo = (peerId, level) } } else { - maxLevelWithVideo = (peerId, source, level) + maxLevelWithVideo = (peerId, level) } } } - if let (peerId, source, _) = maxLevelWithVideo { - if strongSelf.currentDominantSpeakerWithVideo?.0 != peerId || strongSelf.currentDominantSpeakerWithVideo?.1 != source { - strongSelf.currentDominantSpeakerWithVideo = (peerId, source) - strongSelf.call.setFullSizeVideo(peerId: peerId) - strongSelf.mainVideoContainer?.updatePeer(peer: (peerId: peerId, source: source)) + if maxLevelWithVideo == nil { + if let (peerId, _, _) = strongSelf.currentDominantSpeaker { + maxLevelWithVideo = (peerId, 0.0) + } else if strongSelf.peerIdToEndpointId.count > 0 { + for entry in strongSelf.currentFullscreenEntries { + if case let .peer(peerEntry, _) = entry { + if let _ = peerEntry.effectiveVideoEndpointId { + maxLevelWithVideo = (peerEntry.peer.id, 0.0) + break + } + } + } + } + } + + if case .fullscreen = strongSelf.displayMode, !strongSelf.mainStageNode.animating && !strongSelf.animatingExpansion { + if let (peerId, _) = maxLevelWithVideo { + if let (currentPeerId, _, timestamp) = strongSelf.currentDominantSpeaker { + if CACurrentMediaTime() - timestamp > 2.5 && peerId != currentPeerId { + strongSelf.currentDominantSpeaker = (peerId, nil, CACurrentMediaTime()) + strongSelf.updateMainVideo(waitForFullSize: true) + } + } } } @@ -1550,39 +2068,110 @@ public final class VoiceChatController: ViewController { var effectiveLevel: Float = 0.0 if let state = strongSelf.callState, state.muteState == nil || strongSelf.pushingToTalk { effectiveLevel = level + } else if level > 0.1 { + effectiveLevel = level * 0.5 } strongSelf.actionButton.updateLevel(CGFloat(effectiveLevel)) }) - self.leaveNode.addTarget(self, action: #selector(self.leavePressed), forControlEvents: .touchUpInside) - - self.actionButton.addTarget(self, action: #selector(self.actionButtonPressed), forControlEvents: .touchUpInside) - - self.audioOutputNode.addTarget(self, action: #selector(self.audioOutputPressed), forControlEvents: .touchUpInside) - - self.cameraButtonNode.addTarget(self, action: #selector(self.cameraPressed), forControlEvents: .touchUpInside) - - self.optionsButton.contextAction = { [weak self] sourceNode, gesture in - self?.openContextMenu(sourceNode: sourceNode, gesture: gesture) - } - - self.optionsButton.addTarget(self, action: #selector(self.optionsPressed), forControlEvents: .touchUpInside) - self.closeButton.addTarget(self, action: #selector(self.closePressed), forControlEvents: .touchUpInside) - - self.actionButtonColorDisposable = (self.actionButton.outerColor - |> deliverOnMainQueue).start(next: { [weak self] color in - if let strongSelf = self { - let animated = strongSelf.currentAudioButtonColor != nil - strongSelf.currentAudioButtonColor = color - strongSelf.updateButtons(animated: animated) + self.isSpeakingDisposable = (self.call.isSpeaking + |> deliverOnMainQueue).start(next: { [weak self] isSpeaking in + guard let strongSelf = self else { + return + } + if let state = strongSelf.callState, state.muteState == nil || strongSelf.pushingToTalk { + strongSelf.displayUnmuteTooltipTimer?.invalidate() + strongSelf.displayUnmuteTooltipTimer = nil + strongSelf.dismissUnmuteTooltipTimer?.invalidate() + strongSelf.dismissUnmuteTooltipTimer = nil + } else { + if isSpeaking { + var shouldDisplayTooltip = false + if let previousTimstamp = strongSelf.lastUnmuteTooltipDisplayTimestamp, CACurrentMediaTime() > previousTimstamp + 45.0 { + shouldDisplayTooltip = true + } else if strongSelf.lastUnmuteTooltipDisplayTimestamp == nil { + shouldDisplayTooltip = true + } + if shouldDisplayTooltip { + strongSelf.dismissUnmuteTooltipTimer?.invalidate() + strongSelf.dismissUnmuteTooltipTimer = nil + + if strongSelf.displayUnmuteTooltipTimer == nil { + let timer = SwiftSignalKit.Timer(timeout: 1.0, repeat: false, completion: { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.lastUnmuteTooltipDisplayTimestamp = CACurrentMediaTime() + strongSelf.displayUnmuteTooltip() + strongSelf.displayUnmuteTooltipTimer?.invalidate() + strongSelf.displayUnmuteTooltipTimer = nil + strongSelf.dismissUnmuteTooltipTimer?.invalidate() + strongSelf.dismissUnmuteTooltipTimer = nil + }, queue: Queue.mainQueue()) + timer.start() + strongSelf.displayUnmuteTooltipTimer = timer + } + } + } else if strongSelf.dismissUnmuteTooltipTimer == nil && strongSelf.displayUnmuteTooltipTimer != nil { + let timer = SwiftSignalKit.Timer(timeout: 0.4, repeat: false, completion: { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.displayUnmuteTooltipTimer?.invalidate() + strongSelf.displayUnmuteTooltipTimer = nil + + strongSelf.dismissUnmuteTooltipTimer?.invalidate() + strongSelf.dismissUnmuteTooltipTimer = nil + }, queue: Queue.mainQueue()) + timer.start() + strongSelf.dismissUnmuteTooltipTimer = timer + } } }) + self.leaveButton.addTarget(self, action: #selector(self.leavePressed), forControlEvents: .touchUpInside) + self.actionButton.addTarget(self, action: #selector(self.actionPressed), forControlEvents: .touchUpInside) + self.audioButton.addTarget(self, action: #selector(self.audioPressed), forControlEvents: .touchUpInside) + self.cameraButton.addTarget(self, action: #selector(self.cameraPressed), forControlEvents: .touchUpInside) + self.switchCameraButton.addTarget(self, action: #selector(self.switchCameraPressed), forControlEvents: .touchUpInside) + self.optionsButton.contextAction = { [weak self] sourceNode, gesture in + self?.openSettingsMenu(sourceNode: sourceNode, gesture: gesture) + } + self.optionsButton.addTarget(self, action: #selector(self.optionsPressed), forControlEvents: .touchUpInside) + self.closeButton.addTarget(self, action: #selector(self.closePressed), forControlEvents: .touchUpInside) + self.panelButton.addTarget(self, action: #selector(self.panelPressed), forControlEvents: .touchUpInside) + + self.actionButtonColorDisposable = (self.actionButton.outerColor + |> deliverOnMainQueue).start(next: { [weak self] normalColor, activeColor in + if let strongSelf = self { + let animated = strongSelf.currentNormalButtonColor != nil || strongSelf.currentActiveButtonColor == nil + strongSelf.currentNormalButtonColor = normalColor + strongSelf.currentActiveButtonColor = activeColor + strongSelf.updateButtons(transition: animated ? .animated(duration: 0.3, curve: .linear) : .immediate) + } + }) + + self.fullscreenListNode.updateFloatingHeaderOffset = { [weak self] _, _ in + guard let strongSelf = self else { + return + } + + var visiblePeerIds = Set() + strongSelf.fullscreenListNode.forEachVisibleItemNode { itemNode in + if let itemNode = itemNode as? VoiceChatFullscreenParticipantItemNode, let item = itemNode.item { + if item.videoEndpointId == nil { + visiblePeerIds.insert(item.peer.id) + } + } + } + strongSelf.mainStageNode.update(visiblePeerIds: visiblePeerIds) + } + self.listNode.updateFloatingHeaderOffset = { [weak self] offset, transition in if let strongSelf = self { strongSelf.currentContentOffset = offset - if strongSelf.animation == nil && !strongSelf.animatingExpansion { - strongSelf.updateFloatingHeaderOffset(offset: offset, transition: transition) + if !(strongSelf.animatingExpansion || strongSelf.animatingInsertion || strongSelf.animatingAppearance) && (strongSelf.panGestureArguments == nil || strongSelf.isExpanded) { + strongSelf.updateDecorationsLayout(transition: transition) } } } @@ -1599,16 +2188,16 @@ public final class VoiceChatController: ViewController { } } -// self.memberEventsDisposable.set((self.call.memberEvents -// |> deliverOnMainQueue).start(next: { [weak self] event in -// guard let strongSelf = self else { -// return -// } -// if event.joined { -// strongSelf.presentUndoOverlay(content: .invitedToVoiceChat(context: strongSelf.context, peer: event.peer, text: strongSelf.presentationData.strings.VoiceChat_PeerJoinedText(event.peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).0), action: { _ in return false }) -// } -// })) - + self.memberEventsDisposable.set((self.call.memberEvents + |> deliverOnMainQueue).start(next: { [weak self] event in + guard let strongSelf = self else { + return + } + if event.joined { + strongSelf.presentUndoOverlay(content: .invitedToVoiceChat(context: strongSelf.context, peer: event.peer, text: strongSelf.presentationData.strings.VoiceChat_PeerJoinedText(event.peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).0), action: { _ in return false }) + } + })) + self.reconnectedAsEventsDisposable.set((self.call.reconnectedAsEvents |> deliverOnMainQueue).start(next: { [weak self] peer in guard let strongSelf = self else { @@ -1616,111 +2205,160 @@ public final class VoiceChatController: ViewController { } strongSelf.presentUndoOverlay(content: .invitedToVoiceChat(context: strongSelf.context, peer: peer, text: strongSelf.presentationData.strings.VoiceChat_DisplayAsSuccess(peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).0), action: { _ in return false }) })) - - self.voiceSourcesDisposable.set((self.call.incomingVideoSources - |> deliverOnMainQueue).start(next: { [weak self] sources in + + self.stateVersionDisposable.set((self.call.stateVersion + |> distinctUntilChanged + |> deliverOnMainQueue).start(next: { [weak self] _ in guard let strongSelf = self else { return } - var validSources = Set() - for (peerId, source) in sources { - validSources.insert(source) - - if !strongSelf.requestedVideoSources.contains(source) { - strongSelf.requestedVideoSources.insert(source) - strongSelf.call.makeIncomingVideoView(source: source, completion: { videoView in - Queue.mainQueue().async { - guard let strongSelf = self, let videoView = videoView else { - return - } - let videoNode = GroupVideoNode(videoView: videoView) - strongSelf.videoNodes.append((peerId, source, videoNode)) - //strongSelf.addSubnode(videoNode) - if let (layout, navigationHeight) = strongSelf.validLayout { - strongSelf.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: .immediate) - - loop: for i in 0 ..< strongSelf.currentEntries.count { - let entry = strongSelf.currentEntries[i] - switch entry { - case let .peer(peerEntry): - if peerEntry.ssrc == source { - let presentationData = strongSelf.presentationData.withUpdated(theme: strongSelf.darkTheme) - strongSelf.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [ListViewUpdateItem(index: i, previousIndex: i, item: entry.item(context: strongSelf.context, presentationData: presentationData, interaction: strongSelf.itemInteraction!), directionHint: nil)], options: [.Synchronous], updateOpaqueState: nil) - break loop - } - default: - break - } - } - } - } - }) - } - } - - var updated = false - for i in (0 ..< strongSelf.videoNodes.count).reversed() { - if !validSources.contains(strongSelf.videoNodes[i].1) { - let ssrc = strongSelf.videoNodes[i].1 - strongSelf.videoNodes.remove(at: i) - - loop: for j in 0 ..< strongSelf.currentEntries.count { - let entry = strongSelf.currentEntries[j] - switch entry { - case let .peer(peerEntry): - if peerEntry.ssrc == ssrc { - let presentationData = strongSelf.presentationData.withUpdated(theme: strongSelf.darkTheme) - strongSelf.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [ListViewUpdateItem(index: i, previousIndex: i, item: entry.item(context: strongSelf.context, presentationData: presentationData, interaction: strongSelf.itemInteraction!), directionHint: nil)], options: [.Synchronous], updateOpaqueState: nil) - break loop - } - default: - break - } - } - - //strongSelf.videoNodes[i].2.removeFromSupernode() - updated = true - } - } - - if let (_, source) = strongSelf.currentDominantSpeakerWithVideo { - if !validSources.contains(source) { - strongSelf.currentDominantSpeakerWithVideo = nil - strongSelf.call.setFullSizeVideo(peerId: nil) - strongSelf.mainVideoContainer?.updatePeer(peer: nil) - } - } - - if updated { - if let (layout, navigationHeight) = strongSelf.validLayout { - strongSelf.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: .immediate) - } - } + strongSelf.callStateDidReset() })) self.titleNode.tapped = { [weak self] in - if let strongSelf = self, !strongSelf.titleNode.recordingIconNode.isHidden { - var ignore = false - strongSelf.controller?.forEachController { controller -> Bool in - if controller is TooltipScreen { - ignore = true + if let strongSelf = self, !strongSelf.isScheduling { + if strongSelf.callState?.canManageCall ?? false { + strongSelf.openTitleEditing() + } else if !strongSelf.titleNode.recordingIconNode.isHidden { + var hasTooltipAlready = false + strongSelf.controller?.forEachController { controller -> Bool in + if controller is TooltipScreen { + hasTooltipAlready = true + } + return true + } + if !hasTooltipAlready { + let location = strongSelf.titleNode.recordingIconNode.convert(strongSelf.titleNode.recordingIconNode.bounds, to: nil) + strongSelf.controller?.present(TooltipScreen(text: presentationData.strings.VoiceChat_RecordingInProgress, icon: nil, location: .point(location.offsetBy(dx: 1.0, dy: 0.0), .top), displayDuration: .custom(3.0), shouldDismissOnTouch: { _ in + return .dismiss(consume: true) + }), in: .window(.root)) } - return true } - - guard !ignore else { - return - } - - let location = strongSelf.titleNode.recordingIconNode.convert(strongSelf.titleNode.recordingIconNode.bounds, to: nil) - strongSelf.controller?.present(TooltipScreen(text: presentationData.strings.VoiceChat_RecordingInProgress, icon: nil, location: .point(location.offsetBy(dx: 1.0, dy: 0.0), .top), displayDuration: .custom(3.0), shouldDismissOnTouch: { _ in - return .dismiss(consume: true) - }), in: .window(.root)) } } - //self.isFullscreen = true - //self.isExpanded = true + self.scheduleCancelButton.pressed = { [weak self] in + if let strongSelf = self { + strongSelf.dismissScheduled() + } + } + + self.mainStageNode.controlsHidden = { [weak self] hidden in + if let strongSelf = self { + if hidden { + strongSelf.fullscreenListNode.alpha = 0.0 + } else { + strongSelf.fullscreenListNode.alpha = 1.0 + strongSelf.fullscreenListNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } + } + } + + self.mainStageNode.tapped = { [weak self] in + if let strongSelf = self, let (layout, navigationHeight) = strongSelf.validLayout, !strongSelf.animatingExpansion && !strongSelf.animatingMainStage && !strongSelf.mainStageNode.animating { + if case .regular = layout.metrics.widthClass { + strongSelf.panelHidden = !strongSelf.panelHidden + + strongSelf.animatingExpansion = true + let transition = ContainedViewLayoutTransition.animated(duration: 0.3, curve: .easeInOut) + strongSelf.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: transition) + strongSelf.updateDecorationsLayout(transition: transition) + } else { + let effectiveDisplayMode = strongSelf.displayMode + let nextDisplayMode: DisplayMode + switch effectiveDisplayMode { + case .modal: + nextDisplayMode = effectiveDisplayMode + case let .fullscreen(controlsHidden): + if controlsHidden { + nextDisplayMode = .fullscreen(controlsHidden: false) + } else { + nextDisplayMode = .fullscreen(controlsHidden: true) + } + } + strongSelf.updateDisplayMode(nextDisplayMode) + } + } + } + + self.mainStageNode.stopScreencast = { [weak self] in + if let strongSelf = self { + strongSelf.call.disableScreencast() + } + } + + self.mainStageNode.back = { [weak self] in + if let strongSelf = self, !strongSelf.isPanning && !strongSelf.animatingExpansion && !strongSelf.mainStageNode.animating { + strongSelf.currentForcedSpeaker = nil + strongSelf.updateDisplayMode(.modal(isExpanded: true, isFilled: true), fromPan: true) + strongSelf.effectiveSpeaker = nil + } + } + + self.mainStageNode.togglePin = { [weak self] in + if let strongSelf = self { + if let (peerId, videoEndpointId, _, _, _) = strongSelf.effectiveSpeaker { + if let _ = strongSelf.currentForcedSpeaker { + strongSelf.currentDominantSpeaker = (peerId, videoEndpointId, CACurrentMediaTime()) + strongSelf.currentForcedSpeaker = nil + } else { + strongSelf.currentForcedSpeaker = (peerId, videoEndpointId) + } + } + strongSelf.updateMembers() + } + } + + self.mainStageNode.switchTo = { [weak self] peerId in + if let strongSelf = self, let interaction = strongSelf.itemInteraction { + interaction.switchToPeer(peerId, nil, false) + } + } + + self.mainStageNode.getAudioLevel = { [weak self] peerId in + return self?.itemInteraction?.getAudioLevel(peerId) ?? .single(0.0) + } + + self.mainStageNode.getVideo = { [weak self] endpointId, isMyPeer, completion in + if let strongSelf = self { + if isMyPeer { + if strongSelf.readyVideoEndpointIds.contains(endpointId) { + completion(strongSelf.itemInteraction?.getPeerVideo(endpointId, .mainstage)) + } else { + strongSelf.myPeerVideoReadyDisposable.set((strongSelf.readyVideoEndpointIdsPromise.get() + |> filter { $0.contains(endpointId) } + |> take(1) + |> deliverOnMainQueue).start(next: { [weak self] _ in + if let strongSelf = self { + completion(strongSelf.itemInteraction?.getPeerVideo(endpointId, .mainstage)) + } + })) + } + } else { + if let input = (strongSelf.call as! PresentationGroupCallImpl).video(endpointId: endpointId) { + if let videoView = strongSelf.videoRenderingContext.makeView(input: input, blur: false) { + completion(GroupVideoNode(videoView: videoView, backdropVideoView: strongSelf.videoRenderingContext.makeView(input: input, blur: true))) + } + } + + /*strongSelf.call.makeIncomingVideoView(endpointId: endpointId, requestClone: GroupVideoNode.useBlurTransparency, completion: { videoView, backdropVideoView in + if let videoView = videoView { + completion(GroupVideoNode(videoView: videoView, backdropVideoView: backdropVideoView)) + } else { + completion(nil) + } + })*/ + } + } + } + + self.applicationStateDisposable = (self.context.sharedContext.applicationBindings.applicationIsActive + |> deliverOnMainQueue).start(next: { [weak self] active in + guard let strongSelf = self else { + return + } + strongSelf.appIsActive = active + }) } deinit { @@ -1728,27 +2366,26 @@ public final class VoiceChatController: ViewController { self.peerViewDisposable?.dispose() self.leaveDisposable.dispose() self.isMutedDisposable?.dispose() + self.isNoiseSuppressionEnabledDisposable?.dispose() self.callStateDisposable?.dispose() self.audioOutputStateDisposable?.dispose() self.memberStatesDisposable?.dispose() self.audioLevelsDisposable?.dispose() self.myAudioLevelDisposable?.dispose() + self.isSpeakingDisposable?.dispose() self.inviteDisposable.dispose() self.memberEventsDisposable.dispose() self.reconnectedAsEventsDisposable.dispose() - self.voiceSourcesDisposable.dispose() + self.stateVersionDisposable.dispose() + self.updateAvatarDisposable.dispose() + self.ignoreConnectingTimer?.invalidate() + self.readyVideoDisposables.dispose() + self.applicationStateDisposable?.dispose() + self.myPeerVideoReadyDisposable.dispose() } - - private func openContextMenu(sourceNode: ASDisplayNode, gesture: ContextGesture?) { - let canManageCall = !self.optionsButtonIsAvatar - - let items: Signal<[ContextMenuItem], NoError> - if canManageCall { - items = self.contextMenuMainItems() - } else { - items = self.contextMenuDisplayAsItems() - } - + + private func openSettingsMenu(sourceNode: ASDisplayNode, gesture: ContextGesture?) { + let items: Signal<[ContextMenuItem], NoError> = self.contextMenuMainItems() if let controller = self.controller { let contextController = ContextController(account: self.context.account, presentationData: self.presentationData.withUpdated(theme: self.darkTheme), source: .reference(VoiceChatContextReferenceContentSource(controller: controller, sourceNode: self.optionsButton.referenceNode)), items: items, reactionItems: [], gesture: gesture) controller.presentInGlobalOverlay(contextController) @@ -1760,9 +2397,9 @@ public final class VoiceChatController: ViewController { return .single([]) } + let canManageCall = self.callState?.canManageCall == true let avatarSize = CGSize(width: 28.0, height: 28.0) - - return combineLatest(self.displayAsPeersPromise.get(), self.context.account.postbox.loadedPeerWithId(call.peerId), self.inviteLinksPromise.get()) + return combineLatest(self.displayAsPeersPromise.get(), self.context.account.postbox.loadedPeerWithId(self.call.peerId), self.inviteLinksPromise.get()) |> take(1) |> deliverOnMainQueue |> map { [weak self] peers, chatPeer, inviteLinks -> [ContextMenuItem] in @@ -1787,45 +2424,68 @@ public final class VoiceChatController: ViewController { } } } - - items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_EditTitle, icon: { theme -> UIImage? in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Pencil"), color: theme.actionSheet.primaryTextColor) - }, action: { _, f in - f(.default) - - guard let strongSelf = self else { - return - } - - let controller = voiceChatTitleEditController(sharedContext: strongSelf.context.sharedContext, account: strongSelf.context.account, forceTheme: strongSelf.darkTheme, title: presentationData.strings.VoiceChat_EditTitleTitle, text: presentationData.strings.VoiceChat_EditTitleText, placeholder: chatPeer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder), value: strongSelf.callState?.title, apply: { title in - if let strongSelf = self, let title = title { - strongSelf.call.updateTitle(title) - - strongSelf.presentUndoOverlay(content: .voiceChatFlag(text: title.isEmpty ? strongSelf.presentationData.strings.VoiceChat_EditTitleRemoveSuccess : strongSelf.presentationData.strings.VoiceChat_EditTitleSuccess(title).0), action: { _ in return false }) + + if let (availableOutputs, currentOutput) = strongSelf.audioOutputState, availableOutputs.count > 1 { + var currentOutputTitle = "" + for output in availableOutputs { + if output == currentOutput { + let title: String + switch output { + case .builtin: + title = UIDevice.current.model + case .speaker: + title = strongSelf.presentationData.strings.Call_AudioRouteSpeaker + case .headphones: + title = strongSelf.presentationData.strings.Call_AudioRouteHeadphones + case let .port(port): + title = port.name + } + currentOutputTitle = title + break } - }) - self?.controller?.present(controller, in: .window(.root)) - }))) - - var hasPermissions = true - if let chatPeer = chatPeer as? TelegramChannel { - if case .broadcast = chatPeer.info { - hasPermissions = false - } else if chatPeer.flags.contains(.isGigagroup) { - hasPermissions = false } - } - if hasPermissions { - items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_EditPermissions, icon: { theme -> UIImage? in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Restrict"), color: theme.actionSheet.primaryTextColor) + items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_ContextAudio, textLayout: .secondLineWithValue(currentOutputTitle), icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Call/Context Menu/Audio"), color: theme.actionSheet.primaryTextColor) }, action: { c, _ in guard let strongSelf = self else { return } - c.setItems(strongSelf.contextMenuPermissionItems()) + c.setItems(strongSelf.contextMenuAudioItems()) }))) } + if canManageCall { + items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_EditTitle, icon: { theme -> UIImage? in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Pencil"), color: theme.actionSheet.primaryTextColor) + }, action: { _, f in + f(.default) + + guard let strongSelf = self else { + return + } + strongSelf.openTitleEditing() + }))) + + var hasPermissions = true + if let chatPeer = chatPeer as? TelegramChannel { + if case .broadcast = chatPeer.info { + hasPermissions = false + } else if chatPeer.flags.contains(.isGigagroup) { + hasPermissions = false + } + } + if hasPermissions { + items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_EditPermissions, icon: { theme -> UIImage? in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Restrict"), color: theme.actionSheet.primaryTextColor) + }, action: { c, _ in + guard let strongSelf = self else { + return + } + c.setItems(strongSelf.contextMenuPermissionItems()) + }))) + } + } + if let inviteLinks = inviteLinks { items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_Share, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Link"), color: theme.actionSheet.primaryTextColor) @@ -1835,60 +2495,95 @@ public final class VoiceChatController: ViewController { self?.presentShare(inviteLinks) }))) } - - if let recordingStartTimestamp = strongSelf.callState?.recordingStartTimestamp { - items.append(.custom(VoiceChatRecordingContextItem(timestamp: recordingStartTimestamp, action: { _, f in - f(.dismissWithoutContent) - - guard let strongSelf = self else { - return - } - - let alertController = textAlertController(context: strongSelf.context, forceTheme: strongSelf.darkTheme, title: nil, text: strongSelf.presentationData.strings.VoiceChat_StopRecordingTitle, actions: [TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.VoiceChat_StopRecordingStop, action: { - if let strongSelf = self { - strongSelf.call.setShouldBeRecording(false, title: nil) - - strongSelf.presentUndoOverlay(content: .forward(savedMessages: true, text: strongSelf.presentationData.strings.VoiceChat_RecordingSaved), action: { [weak self] value in - if case .info = value, let strongSelf = self, let navigationController = strongSelf.controller?.navigationController as? NavigationController { - let context = strongSelf.context - strongSelf.controller?.dismiss(completion: { - Queue.mainQueue().justDispatch { - context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(context.account.peerId), keepStack: .always, purposefulAction: {}, peekData: nil)) - } - }) - - return true - } - return false - }) - } - })]) - self?.controller?.present(alertController, in: .window(.root)) - }), false)) - } else { - items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_StartRecording, icon: { theme -> UIImage? in - return generateStartRecordingIcon(color: theme.actionSheet.primaryTextColor) + + let isScheduled = strongSelf.isScheduled + + if !isScheduled { + items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_NoiseSuppression, textColor: .primary, textLayout: .secondLineWithValue(strongSelf.isNoiseSuppressionEnabled ? strongSelf.presentationData.strings.VoiceChat_NoiseSuppressionEnabled : strongSelf.presentationData.strings.VoiceChat_NoiseSuppressionDisabled), icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Call/Context Menu/Noise"), color: theme.actionSheet.primaryTextColor) }, action: { _, f in f(.dismissWithoutContent) - - guard let strongSelf = self else { - return + if let strongSelf = self { + strongSelf.call.setIsNoiseSuppressionEnabled(!strongSelf.isNoiseSuppressionEnabled) } - - let controller = voiceChatTitleEditController(sharedContext: strongSelf.context.sharedContext, account: strongSelf.context.account, forceTheme: strongSelf.darkTheme, title: presentationData.strings.VoiceChat_StartRecordingTitle, text: presentationData.strings.VoiceChat_StartRecordingText, placeholder: presentationData.strings.VoiceChat_RecordingTitlePlaceholder, value: nil, apply: { title in - if let strongSelf = self, let title = title { - strongSelf.call.setShouldBeRecording(true, title: title) - - strongSelf.presentUndoOverlay(content: .voiceChatRecording(text: strongSelf.presentationData.strings.VoiceChat_RecordingStarted), action: { _ in return false }) - strongSelf.call.playTone(.recordingStarted) - } - }) - self?.controller?.present(controller, in: .window(.root)) }))) } + + if let callState = strongSelf.callState, callState.isVideoEnabled && (callState.muteState?.canUnmute ?? true) { + if #available(iOS 12.0, *) { + if strongSelf.call.hasScreencast { + items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_StopScreenSharing, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Call/Context Menu/ShareScreen"), color: theme.actionSheet.primaryTextColor) + }, action: { _, f in + f(.default) - if let callState = strongSelf.callState, callState.canManageCall { - items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_EndVoiceChat, textColor: .destructive, icon: { theme in + self?.call.disableScreencast() + }))) + } else { + items.append(.custom(VoiceChatShareScreenContextItem(context: strongSelf.context, text: strongSelf.presentationData.strings.VoiceChat_ShareScreen, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Call/Context Menu/ShareScreen"), color: theme.actionSheet.primaryTextColor) + }, action: { _, _ in }), false)) + } + } + } + + if canManageCall { + if let recordingStartTimestamp = strongSelf.callState?.recordingStartTimestamp { + items.append(.custom(VoiceChatRecordingContextItem(timestamp: recordingStartTimestamp, action: { _, f in + f(.dismissWithoutContent) + + guard let strongSelf = self else { + return + } + + let alertController = textAlertController(context: strongSelf.context, forceTheme: strongSelf.darkTheme, title: nil, text: strongSelf.presentationData.strings.VoiceChat_StopRecordingTitle, actions: [TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.VoiceChat_StopRecordingStop, action: { + if let strongSelf = self { + strongSelf.call.setShouldBeRecording(false, title: nil) + + strongSelf.presentUndoOverlay(content: .forward(savedMessages: true, text: strongSelf.presentationData.strings.VoiceChat_RecordingSaved), action: { [weak self] value in + if case .info = value, let strongSelf = self, let navigationController = strongSelf.controller?.navigationController as? NavigationController { + let context = strongSelf.context + strongSelf.controller?.dismiss(completion: { + Queue.mainQueue().justDispatch { + context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(context.account.peerId), keepStack: .always, purposefulAction: {}, peekData: nil)) + } + }) + + return true + } + return false + }) + } + })]) + self?.controller?.present(alertController, in: .window(.root)) + }), false)) + } else { + if strongSelf.callState?.scheduleTimestamp == nil { + items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_StartRecording, icon: { theme -> UIImage? in + return generateStartRecordingIcon(color: theme.actionSheet.primaryTextColor) + }, action: { _, f in + f(.dismissWithoutContent) + + guard let strongSelf = self else { + return + } + + let controller = voiceChatTitleEditController(sharedContext: strongSelf.context.sharedContext, account: strongSelf.context.account, forceTheme: strongSelf.darkTheme, title: presentationData.strings.VoiceChat_StartRecordingTitle, text: presentationData.strings.VoiceChat_StartRecordingText, placeholder: presentationData.strings.VoiceChat_RecordingTitlePlaceholder, value: nil, maxLength: 40, apply: { title in + if let strongSelf = self, let title = title { + strongSelf.call.setShouldBeRecording(true, title: title) + + strongSelf.presentUndoOverlay(content: .voiceChatRecording(text: strongSelf.presentationData.strings.VoiceChat_RecordingStarted), action: { _ in return false }) + strongSelf.call.playTone(.recordingStarted) + } + }) + self?.controller?.present(controller, in: .window(.root)) + }))) + } + } + } + + if canManageCall { + items.append(.action(ContextMenuActionItem(text: isScheduled ? strongSelf.presentationData.strings.VoiceChat_CancelVoiceChat : strongSelf.presentationData.strings.VoiceChat_EndVoiceChat, textColor: .destructive, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Clear"), color: theme.actionSheet.destructiveActionTextColor) }, action: { _, f in f(.dismissWithoutContent) @@ -1910,25 +2605,80 @@ public final class VoiceChatController: ViewController { }) } - let alertController = textAlertController(context: strongSelf.context, forceTheme: strongSelf.darkTheme, title: strongSelf.presentationData.strings.VoiceChat_EndConfirmationTitle, text: strongSelf.presentationData.strings.VoiceChat_EndConfirmationText, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.VoiceChat_EndConfirmationEnd, action: { + let alertController = textAlertController(context: strongSelf.context, forceTheme: strongSelf.darkTheme, title: isScheduled ? strongSelf.presentationData.strings.VoiceChat_CancelConfirmationTitle : strongSelf.presentationData.strings.VoiceChat_EndConfirmationTitle, text: isScheduled ? strongSelf.presentationData.strings.VoiceChat_CancelConfirmationText : strongSelf.presentationData.strings.VoiceChat_EndConfirmationText, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .genericAction, title: isScheduled ? strongSelf.presentationData.strings.VoiceChat_CancelConfirmationEnd : strongSelf.presentationData.strings.VoiceChat_EndConfirmationEnd, action: { action() })]) strongSelf.controller?.present(alertController, in: .window(.root)) }))) + } else { + items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_LeaveVoiceChat, textColor: .destructive, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Clear"), color: theme.actionSheet.destructiveActionTextColor) + }, action: { _, f in + f(.dismissWithoutContent) + + guard let strongSelf = self else { + return + } + + let _ = (strongSelf.call.leave(terminateIfPossible: false) + |> filter { $0 } + |> take(1) + |> deliverOnMainQueue).start(completed: { + self?.controller?.dismiss() + }) + }))) } - - return items } } + private func contextMenuAudioItems() -> Signal<[ContextMenuItem], NoError> { + guard let (availableOutputs, currentOutput) = self.audioOutputState else { + return .single([]) + } + + var items: [ContextMenuItem] = [] + for output in availableOutputs { + let title: String + switch output { + case .builtin: + title = UIDevice.current.model + case .speaker: + title = self.presentationData.strings.Call_AudioRouteSpeaker + case .headphones: + title = self.presentationData.strings.Call_AudioRouteHeadphones + case let .port(port): + title = port.name + } + items.append(.action(ContextMenuActionItem(text: title, icon: { theme in + if output == currentOutput { + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.actionSheet.primaryTextColor) + } else { + return nil + } + }, action: { [weak self] _, f in + f(.default) + self?.call.setCurrentAudioOutput(output) + }))) + } + items.append(.separator) + items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.Common_Back, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: theme.actionSheet.primaryTextColor) + }, action: { [weak self] (c, _) in + guard let strongSelf = self else { + return + } + c.setItems(strongSelf.contextMenuMainItems()) + }))) + return .single(items) + } + private func contextMenuDisplayAsItems() -> Signal<[ContextMenuItem], NoError> { guard let myPeerId = self.callState?.myPeerId else { return .single([]) } let avatarSize = CGSize(width: 28.0, height: 28.0) - let canManageCall = !self.optionsButtonIsAvatar let darkTheme = self.darkTheme return self.displayAsPeersPromise.get() @@ -1942,7 +2692,10 @@ public final class VoiceChatController: ViewController { var isGroup = false for peer in peers { - if let peer = peer.peer as? TelegramChannel, case .group = peer.info { + if peer.peer is TelegramGroup { + isGroup = true + break + } else if let peer = peer.peer as? TelegramChannel, case .group = peer.info { isGroup = true break } @@ -2003,17 +2756,15 @@ public final class VoiceChatController: ViewController { items.append(.separator) } } - if canManageCall { - items.append(.separator) - items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.Common_Back, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: theme.actionSheet.primaryTextColor) - }, action: { (c, _) in - guard let strongSelf = self else { - return - } - c.setItems(strongSelf.contextMenuMainItems()) - }))) - } + items.append(.separator) + items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.Common_Back, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: theme.actionSheet.primaryTextColor) + }, action: { (c, _) in + guard let strongSelf = self else { + return + } + c.setItems(strongSelf.contextMenuMainItems()) + }))) return items } } @@ -2082,12 +2833,205 @@ public final class VoiceChatController: ViewController { panRecognizer.delaysTouchesBegan = false panRecognizer.cancelsTouchesInView = true self.view.addGestureRecognizer(panRecognizer) + + if self.isScheduling { + self.setupSchedulePickerView() + self.updateScheduleButtonTitle() + } + } + + private func updateSchedulePickerLimits() { + let timeZone = TimeZone(secondsFromGMT: 0)! + var calendar = Calendar(identifier: .gregorian) + calendar.timeZone = timeZone + let currentDate = Date() + var components = calendar.dateComponents(Set([.era, .year, .month, .day, .hour, .minute, .second]), from: currentDate) + components.second = 0 + + let roundedDate = calendar.date(from: components)! + let next1MinDate = calendar.date(byAdding: .minute, value: 1, to: roundedDate) + + let minute = components.minute ?? 0 + components.minute = 0 + let roundedToHourDate = calendar.date(from: components)! + components.hour = 0 + + let roundedToMidnightDate = calendar.date(from: components)! + let nextTwoHourDate = calendar.date(byAdding: .hour, value: minute > 30 ? 4 : 3, to: roundedToHourDate) + let maxDate = calendar.date(byAdding: .day, value: 8, to: roundedToMidnightDate) + + if let date = calendar.date(byAdding: .day, value: 365, to: currentDate) { + self.pickerView?.maximumDate = date + } + if let next1MinDate = next1MinDate, let nextTwoHourDate = nextTwoHourDate { + self.pickerView?.minimumDate = next1MinDate + self.pickerView?.maximumDate = maxDate + self.pickerView?.date = nextTwoHourDate + } + } + + private func setupSchedulePickerView() { + var currentDate: Date? + if let pickerView = self.pickerView { + currentDate = pickerView.date + pickerView.removeFromSuperview() + } + + let textColor = UIColor.white + UILabel.setDateLabel(textColor) + + let pickerView = UIDatePicker() + pickerView.timeZone = TimeZone(secondsFromGMT: 0) + pickerView.datePickerMode = .countDownTimer + pickerView.datePickerMode = .dateAndTime + pickerView.locale = Locale.current + pickerView.timeZone = TimeZone.current + pickerView.minuteInterval = 1 + self.contentContainer.view.addSubview(pickerView) + pickerView.addTarget(self, action: #selector(self.scheduleDatePickerUpdated), for: .valueChanged) + if #available(iOS 13.4, *) { + pickerView.preferredDatePickerStyle = .wheels + } + pickerView.setValue(textColor, forKey: "textColor") + self.pickerView = pickerView + + self.updateSchedulePickerLimits() + if let currentDate = currentDate { + pickerView.date = currentDate + } + } + + private let calendar = Calendar(identifier: .gregorian) + private func updateScheduleButtonTitle() { + guard let date = self.pickerView?.date else { + return + } + + let calendar = Calendar(identifier: .gregorian) + let currentTimestamp = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) + let timestamp = Int32(date.timeIntervalSince1970) + let time = stringForMessageTimestamp(timestamp: timestamp, dateTimeFormat: self.presentationData.dateTimeFormat) + let buttonTitle: String + if calendar.isDateInToday(date) { + buttonTitle = self.presentationData.strings.ScheduleVoiceChat_ScheduleToday(time).0 + } else if calendar.isDateInTomorrow(date) { + buttonTitle = self.presentationData.strings.ScheduleVoiceChat_ScheduleTomorrow(time).0 + } else { + buttonTitle = self.presentationData.strings.ScheduleVoiceChat_ScheduleOn(self.dateFormatter.string(from: date), time).0 + } + self.scheduleButtonTitle = buttonTitle + + let delta = timestamp - currentTimestamp + + var isGroup = true + if let peer = self.peer as? TelegramChannel, case .broadcast = peer.info { + isGroup = false + } + let intervalString = scheduledTimeIntervalString(strings: self.presentationData.strings, value: max(60, delta)) + self.scheduleTextNode.attributedText = NSAttributedString(string: isGroup ? self.presentationData.strings.ScheduleVoiceChat_GroupText(intervalString).0 : self.presentationData.strings.ScheduleVoiceChat_ChannelText(intervalString).0, font: Font.regular(14.0), textColor: UIColor(rgb: 0x8e8e93)) + + if let (layout, navigationHeight) = self.validLayout { + self.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: .animated(duration: 0.3, curve: .spring)) + } + } + + @objc private func scheduleDatePickerUpdated() { + self.updateScheduleButtonTitle() + } + + private func schedule() { + if let date = self.pickerView?.date, date > Date() { + self.call.schedule(timestamp: Int32(date.timeIntervalSince1970)) + + self.isScheduling = false + self.transitionToScheduled() + if let (layout, navigationHeight) = self.validLayout { + self.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: .animated(duration: 0.3, curve: .spring)) + } + } + } + + private func dismissScheduled() { + self.leaveDisposable.set((self.call.leave(terminateIfPossible: true) + |> deliverOnMainQueue).start(completed: { [weak self] in + self?.controller?.dismiss(closing: true) + })) + } + + private func transitionToScheduled() { + let springDuration: Double = 0.6 + let springDamping: CGFloat = 100.0 + + self.optionsButton.alpha = 1.0 + self.optionsButton.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + self.optionsButton.layer.animateSpring(from: 0.01 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: springDuration, damping: springDamping) + self.optionsButton.isUserInteractionEnabled = true + + self.closeButton.alpha = 1.0 + self.closeButton.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + self.closeButton.layer.animateSpring(from: 0.01 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: springDuration, damping: springDamping) + self.closeButton.isUserInteractionEnabled = true + + self.audioButton.alpha = 1.0 + self.audioButton.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + self.audioButton.layer.animateSpring(from: 0.01 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: springDuration, damping: springDamping) + self.audioButton.isUserInteractionEnabled = true + + self.leaveButton.alpha = 1.0 + self.leaveButton.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + self.leaveButton.layer.animateSpring(from: 0.01 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: springDuration, damping: springDamping) + self.leaveButton.isUserInteractionEnabled = true + + self.scheduleCancelButton.alpha = 0.0 + self.scheduleCancelButton.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15) + self.scheduleCancelButton.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: 26.0), duration: 0.2, removeOnCompletion: false, additive: true) + + self.actionButton.titleLabel.layer.animatePosition(from: CGPoint(x: 0.0, y: -26.0), to: CGPoint(), duration: 0.2, additive: true) + + if let pickerView = self.pickerView { + self.pickerView = nil + pickerView.alpha = 0.0 + pickerView.layer.animateScale(from: 1.0, to: 0.25, duration: 0.15, removeOnCompletion: false) + pickerView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak pickerView] _ in + pickerView?.removeFromSuperview() + }) + pickerView.isUserInteractionEnabled = false + } + + self.timerNode.isHidden = false + self.timerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) + self.timerNode.animateIn() + + self.scheduleTextNode.alpha = 0.0 + self.scheduleTextNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25) + + self.updateTitle(slide: true, transition: .animated(duration: 0.2, curve: .easeInOut)) + } + + private func transitionToCall() { + self.updateDecorationsColors() + + self.listNode.alpha = 1.0 + self.listNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + self.listNode.isUserInteractionEnabled = true + + self.timerNode.alpha = 0.0 + self.timerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, completion: { [weak self] _ in + self?.timerNode.isHidden = true + }) + + if self.audioButton.isHidden { + self.audioButton.isHidden = false + self.audioButton.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + self.audioButton.layer.animateSpring(from: 0.01 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.6, damping: 100.0) + } + + self.updateTitle(transition: .animated(duration: 0.2, curve: .easeInOut)) } @objc private func optionsPressed() { - if self.optionsButton.isUserInteractionEnabled { - self.optionsButton.contextAction?(self.optionsButton.containerNode, nil) - } + self.optionsButton.play() + self.optionsButton.contextAction?(self.optionsButton.containerNode, nil) } @objc private func closePressed() { @@ -2095,6 +3039,18 @@ public final class VoiceChatController: ViewController { self.controller?.dismissAllTooltips() } + @objc private func panelPressed() { + guard let (layout, navigationHeight) = self.validLayout, !self.animatingExpansion && !self.animatingMainStage && !self.mainStageNode.animating else { + return + } + self.panelHidden = !self.panelHidden + + self.animatingExpansion = true + let transition = ContainedViewLayoutTransition.animated(duration: 0.3, curve: .easeInOut) + self.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: transition) + self.updateDecorationsLayout(transition: transition) + } + @objc private func leavePressed() { self.hapticFeedback.impact(.light) self.controller?.dismissAllTooltips() @@ -2105,18 +3061,30 @@ public final class VoiceChatController: ViewController { return } - let _ = (strongSelf.call.leave(terminateIfPossible: true) - |> filter { $0 } - |> take(1) + strongSelf.leaveDisposable.set((strongSelf.call.leave(terminateIfPossible: true) |> deliverOnMainQueue).start(completed: { self?.controller?.dismiss() - }) + })) } let actionSheet = ActionSheetController(presentationData: self.presentationData.withUpdated(theme: self.darkTheme)) var items: [ActionSheetItem] = [] items.append(ActionSheetTextItem(title: self.presentationData.strings.VoiceChat_LeaveConfirmation)) + items.append(ActionSheetButtonItem(title: self.isScheduled ? self.presentationData.strings.VoiceChat_LeaveAndCancelVoiceChat : self.presentationData.strings.VoiceChat_LeaveAndEndVoiceChat, color: .destructive, action: { [weak self, weak actionSheet] in + actionSheet?.dismissAnimated() + + if let strongSelf = self { + if let (members, _) = strongSelf.currentCallMembers, members.count >= 10 || true { + let alertController = textAlertController(context: strongSelf.context, forceTheme: strongSelf.darkTheme, title: strongSelf.isScheduled ? strongSelf.presentationData.strings.VoiceChat_CancelConfirmationTitle : strongSelf.presentationData.strings.VoiceChat_EndConfirmationTitle, text: strongSelf.isScheduled ? strongSelf.presentationData.strings.VoiceChat_CancelConfirmationText : strongSelf.presentationData.strings.VoiceChat_EndConfirmationText, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .genericAction, title: strongSelf.isScheduled ? strongSelf.presentationData.strings.VoiceChat_CancelConfirmationEnd : strongSelf.presentationData.strings.VoiceChat_EndConfirmationEnd, action: { + action() + })]) + strongSelf.controller?.present(alertController, in: .window(.root)) + } else { + action() + } + } + })) items.append(ActionSheetButtonItem(title: self.presentationData.strings.VoiceChat_LeaveVoiceChat, color: .accent, action: { [weak self, weak actionSheet] in actionSheet?.dismissAnimated() @@ -2130,12 +3098,6 @@ public final class VoiceChatController: ViewController { })) })) - items.append(ActionSheetButtonItem(title: self.presentationData.strings.VoiceChat_LeaveAndEndVoiceChat, color: .destructive, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - - action() - })) - actionSheet.setItemGroups([ ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ @@ -2155,8 +3117,12 @@ public final class VoiceChatController: ViewController { @objc func dimTapGesture(_ recognizer: UITapGestureRecognizer) { if case .ended = recognizer.state { - self.controller?.dismiss(closing: false) - self.controller?.dismissAllTooltips() + if self.isScheduling { + self.dismissScheduled() + } else { + self.controller?.dismiss(closing: false) + self.controller?.dismissAllTooltips() + } } } @@ -2208,7 +3174,7 @@ public final class VoiceChatController: ViewController { return formatSendTitle(presentationData.strings.VoiceChat_InviteLink_InviteListeners(Int32(count))) })] } - let shareController = ShareController(context: strongSelf.context, subject: .url(inviteLinks.listenerLink), segmentedValues: segmentedValues, forcedTheme: strongSelf.darkTheme, forcedActionTitle: presentationData.strings.VoiceChat_CopyInviteLink) + let shareController = ShareController(context: strongSelf.context, subject: .url(inviteLinks.listenerLink), segmentedValues: segmentedValues, forceTheme: strongSelf.darkTheme, forcedActionTitle: presentationData.strings.VoiceChat_CopyInviteLink) shareController.completed = { [weak self] peerIds in if let strongSelf = self { let _ = (strongSelf.context.account.postbox.transaction { transaction -> [Peer] in @@ -2256,23 +3222,25 @@ public final class VoiceChatController: ViewController { }) } - private var pressTimer: SwiftSignalKit.Timer? - private func startPressTimer() { - self.pressTimer?.invalidate() + private var actionButtonPressTimer: SwiftSignalKit.Timer? + private var actionButtonPressedTimestamp: Double? + private func startActionButtonPressTimer() { + self.actionButtonPressTimer?.invalidate() let pressTimer = SwiftSignalKit.Timer(timeout: 0.185, repeat: false, completion: { [weak self] in - self?.pressTimerFired() - self?.pressTimer = nil + self?.actionButtonPressedTimestamp = CACurrentMediaTime() + self?.actionButtonPressTimerFired() + self?.actionButtonPressTimer = nil }, queue: Queue.mainQueue()) - self.pressTimer = pressTimer + self.actionButtonPressTimer = pressTimer pressTimer.start() } - private func stopPressTimer() { - self.pressTimer?.invalidate() - self.pressTimer = nil + private func stopActionButtonPressTimer() { + self.actionButtonPressTimer?.invalidate() + self.actionButtonPressTimer = nil } - private func pressTimerFired() { + private func actionButtonPressTimerFired() { guard let callState = self.callState else { return } @@ -2285,14 +3253,44 @@ public final class VoiceChatController: ViewController { self.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: .animated(duration: 0.3, curve: .spring)) } - self.updateMembers(muteState: self.effectiveMuteState, callMembers: self.currentCallMembers ?? ([], nil), invitedPeers: self.currentInvitedPeers ?? [], speakingPeers: self.currentSpeakingPeers ?? Set()) + self.updateMembers() } @objc private func actionButtonPressGesture(_ gestureRecognizer: UILongPressGestureRecognizer) { guard let callState = self.callState else { return } - if case .connecting = callState.networkState { + if case .connecting = callState.networkState, callState.scheduleTimestamp == nil && !self.isScheduling { + return + } + if callState.scheduleTimestamp != nil || self.isScheduling { + switch gestureRecognizer.state { + case .began: + self.actionButton.pressing = true + self.hapticFeedback.impact(.light) + case .ended, .cancelled: + self.actionButton.pressing = false + + let location = gestureRecognizer.location(in: self.actionButton.view) + if self.actionButton.hitTest(location, with: nil) != nil { + if self.isScheduling { + self.schedule() + } else if callState.canManageCall { + self.call.startScheduled() + } else { + if !callState.subscribedToScheduled { + let location = self.actionButton.view.convert(self.actionButton.bounds, to: self.view).center + let point = CGRect(origin: CGPoint(x: location.x - 5.0, y: location.y - 5.0 - 68.0), size: CGSize(width: 10.0, height: 10.0)) + self.controller?.present(TooltipScreen(text: self.presentationData.strings.VoiceChat_ReminderNotify, style: .gradient(UIColor(rgb: 0x262c5a), UIColor(rgb: 0x5d2835)), icon: nil, location: .point(point, .bottom), displayDuration: .custom(3.0), shouldDismissOnTouch: { _ in + return .dismiss(consume: false) + }), in: .window(.root)) + } + self.call.toggleScheduledSubscription(!callState.subscribedToScheduled) + } + } + default: + break + } return } if let muteState = callState.muteState { @@ -2319,20 +3317,39 @@ public final class VoiceChatController: ViewController { case .began: self.actionButton.pressing = true self.hapticFeedback.impact(.light) - self.startPressTimer() + self.actionButtonPressedTimestamp = nil + self.startActionButtonPressTimer() if let (layout, navigationHeight) = self.validLayout { self.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: .animated(duration: 0.3, curve: .spring)) } case .ended, .cancelled: - self.pushingToTalk = false - self.actionButton.pressing = false - - if self.pressTimer != nil { - self.stopPressTimer() + if self.actionButtonPressTimer != nil { + self.pushingToTalk = false + self.actionButton.pressing = false + + self.stopActionButtonPressTimer() self.call.toggleIsMuted() } else { self.hapticFeedback.impact(.light) - self.call.setIsMuted(action: .muted(isPushToTalkActive: false)) + if self.pushingToTalk, let timestamp = self.actionButtonPressedTimestamp, CACurrentMediaTime() < timestamp + 0.5 { + self.pushingToTalk = false + self.temporaryPushingToTalk = true + self.call.setIsMuted(action: .unmuted) + + Queue.mainQueue().after(0.1) { + self.temporaryPushingToTalk = false + self.actionButton.pressing = false + + if let (layout, navigationHeight) = self.validLayout { + self.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: .animated(duration: 0.3, curve: .spring)) + } + } + } else { + self.pushingToTalk = false + self.actionButton.pressing = false + + self.call.setIsMuted(action: .muted(isPushToTalkActive: false)) + } } if let callState = self.callState { @@ -2342,17 +3359,60 @@ public final class VoiceChatController: ViewController { if let (layout, navigationHeight) = self.validLayout { self.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: .animated(duration: 0.3, curve: .spring)) } - self.updateMembers(muteState: self.effectiveMuteState, callMembers: self.currentCallMembers ?? ([], nil), invitedPeers: self.currentInvitedPeers ?? [], speakingPeers: self.currentSpeakingPeers ?? Set()) + self.updateMembers() default: break } } - @objc private func actionButtonPressed() { + @objc private func actionPressed() { + if self.isScheduling { + self.schedule() + } } - @objc private func audioOutputPressed() { + @objc private func audioPressed() { self.hapticFeedback.impact(.light) + + if let _ = self.callState?.scheduleTimestamp { + if let callState = self.callState, let peer = self.peer, !callState.canManageCall && (peer.addressName?.isEmpty ?? true) { + return + } + + let _ = (self.inviteLinksPromise.get() + |> take(1) + |> deliverOnMainQueue).start(next: { [weak self] inviteLinks in + guard let strongSelf = self else { + return + } + + let callPeerId = strongSelf.call.peerId + let _ = (strongSelf.context.account.postbox.transaction { transaction -> GroupCallInviteLinks? in + if let inviteLinks = inviteLinks { + return inviteLinks + } else if let peer = transaction.getPeer(callPeerId), let addressName = peer.addressName, !addressName.isEmpty { + return GroupCallInviteLinks(listenerLink: "https://t.me/\(addressName)?voicechat", speakerLink: nil) + } else if let cachedData = transaction.getPeerCachedData(peerId: callPeerId) { + if let cachedData = cachedData as? CachedChannelData, let link = cachedData.exportedInvitation?.link { + return GroupCallInviteLinks(listenerLink: link, speakerLink: nil) + } else if let cachedData = cachedData as? CachedGroupData, let link = cachedData.exportedInvitation?.link { + return GroupCallInviteLinks(listenerLink: link, speakerLink: nil) + } + } + return nil + } + |> deliverOnMainQueue).start(next: { links in + guard let strongSelf = self else { + return + } + + if let links = links { + strongSelf.presentShare(links) + } + }) + }) + return + } guard let (availableOutputs, currentOutput) = self.audioOutputState else { return @@ -2360,8 +3420,7 @@ public final class VoiceChatController: ViewController { guard availableOutputs.count >= 2 else { return } - let hasMute = false - + if availableOutputs.count == 2 { for output in availableOutputs { if output != currentOutput { @@ -2370,12 +3429,9 @@ public final class VoiceChatController: ViewController { } } } else { - let actionSheet = ActionSheetController(presentationData: self.presentationData) + let actionSheet = ActionSheetController(presentationData: self.presentationData.withUpdated(theme: self.darkTheme)) var items: [ActionSheetItem] = [] for output in availableOutputs { - if hasMute, case .builtin = output { - continue - } let title: String var icon: UIImage? switch output { @@ -2391,7 +3447,9 @@ public final class VoiceChatController: ViewController { if port.type == .bluetooth { var image = UIImage(bundleImageName: "Call/CallBluetoothButton") let portName = port.name.lowercased() - if portName.contains("airpods pro") { + if portName.contains("airpods max") { + image = UIImage(bundleImageName: "Call/CallAirpodsMaxButton") + } else if portName.contains("airpods pro") { image = UIImage(bundleImageName: "Call/CallAirpodsProButton") } else if portName.contains("airpods") { image = UIImage(bundleImageName: "Call/CallAirpodsButton") @@ -2416,29 +3474,156 @@ public final class VoiceChatController: ViewController { } @objc private func cameraPressed() { - if self.call.isVideo { + self.hapticFeedback.impact(.light) + if self.call.hasVideo { self.call.disableVideo() + + if let (layout, navigationHeight) = self.validLayout { + self.animatingButtonsSwap = true + self.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: .animated(duration: 0.4, curve: .spring)) + } } else { - self.call.requestVideo() + DeviceAccess.authorizeAccess(to: .camera(.videoCall), onlyCheck: true, presentationData: self.presentationData.withUpdated(theme: self.darkTheme), present: { [weak self] c, a in + self?.controller?.present(c, in: .window(.root), with: a) + }, openSettings: { [weak self] in + self?.context.sharedContext.applicationBindings.openSettings() + }, _: { [weak self] ready in + guard let strongSelf = self, ready else { + return + } + let videoCapturer = OngoingCallVideoCapturer() + let input = videoCapturer.video() + if let videoView = strongSelf.videoRenderingContext.makeView(input: input, blur: false) { + let cameraNode = GroupVideoNode(videoView: videoView, backdropVideoView: nil) + let controller = VoiceChatCameraPreviewController(context: strongSelf.context, cameraNode: cameraNode, shareCamera: { [weak self] _, unmuted in + if let strongSelf = self { + strongSelf.call.setIsMuted(action: unmuted ? .unmuted : .muted(isPushToTalkActive: false)) + (strongSelf.call as! PresentationGroupCallImpl).requestVideo(capturer: videoCapturer) + + if let (layout, navigationHeight) = strongSelf.validLayout { + strongSelf.animatingButtonsSwap = true + strongSelf.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: .animated(duration: 0.4, curve: .spring)) + } + } + }, switchCamera: { [weak self] in + Queue.mainQueue().after(0.1) { + self?.call.switchVideoCamera() + } + }) + strongSelf.controller?.present(controller, in: .window(.root)) + } + + /*strongSelf.call.makeOutgoingVideoView(requestClone: false, completion: { [weak self] view, _ in + guard let strongSelf = self, let view = view else { + return + } + let cameraNode = GroupVideoNode(videoView: view, backdropVideoView: nil) + let controller = VoiceChatCameraPreviewController(context: strongSelf.context, cameraNode: cameraNode, shareCamera: { [weak self] videoNode, unmuted in + if let strongSelf = self { + strongSelf.call.setIsMuted(action: unmuted ? .unmuted : .muted(isPushToTalkActive: false)) + strongSelf.call.requestVideo() + + if let (layout, navigationHeight) = strongSelf.validLayout { + strongSelf.animatingButtonsSwap = true + strongSelf.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: .animated(duration: 0.4, curve: .spring)) + } + } + }, switchCamera: { [weak self] in + Queue.mainQueue().after(0.1) { + self?.call.switchVideoCamera() + } + }) + strongSelf.controller?.present(controller, in: .window(.root)) + })*/ + }) } } - private func updateFloatingHeaderOffset(offset: CGFloat, transition: ContainedViewLayoutTransition, completion: (() -> Void)? = nil) { + @objc private func switchCameraPressed() { + self.hapticFeedback.impact(.light) + Queue.mainQueue().after(0.1) { + self.call.switchVideoCamera() + } + + if let callState = self.callState { + for entry in self.currentFullscreenEntries { + if case let .peer(peerEntry, _) = entry { + if peerEntry.peer.id == callState.myPeerId { + if let videoEndpointId = peerEntry.videoEndpointId, let videoNode = self.videoNodes[videoEndpointId] { + videoNode.flip(withBackground: false) + } + break + } + } + } + } + self.mainStageNode.flipVideoIfNeeded() + + let springDuration: Double = 0.7 + let springDamping: CGFloat = 100.0 + self.switchCameraButton.isUserInteractionEnabled = false + self.switchCameraButton.layer.animateSpring(from: 0.0 as NSNumber, to: CGFloat.pi as NSNumber, keyPath: "transform.rotation.z", duration: springDuration, damping: springDamping, completion: { [weak self] _ in + self?.switchCameraButton.isUserInteractionEnabled = true + }) + } + + private var isLandscape: Bool { + if let (layout, _) = self.validLayout, layout.size.width > layout.size.height, case .compact = layout.metrics.widthClass { + return true + } else { + return false + } + } + + private var effectiveBottomAreaHeight: CGFloat { + if let (layout, _) = self.validLayout, case .regular = layout.metrics.widthClass { + return bottomAreaHeight + } + switch self.displayMode { + case .modal: + return bottomAreaHeight + case let .fullscreen(controlsHidden): + return controlsHidden ? 0.0 : fullscreenBottomAreaHeight + } + } + + private var isFullscreen: Bool { + switch self.displayMode { + case .fullscreen(_), .modal(_, true): + return true + default: + return false + } + } + + private func updateDecorationsLayout(transition: ContainedViewLayoutTransition, completion: (() -> Void)? = nil) { guard let (layout, _) = self.validLayout else { return } - - let layoutTopInset: CGFloat = max(layout.statusBarHeight ?? 0.0, layout.safeInsets.top) - let topPanelHeight: CGFloat = 63.0 - let listTopInset = layoutTopInset + topPanelHeight - let bottomPanelHeight = bottomAreaHeight + layout.intrinsicInsets.bottom + + let isLandscape = self.isLandscape - var size = layout.size + let layoutTopInset: CGFloat = max(layout.statusBarHeight ?? 0.0, layout.safeInsets.top) + let listTopInset = isLandscape ? topPanelHeight : layoutTopInset + topPanelHeight + let bottomPanelHeight = isLandscape ? layout.intrinsicInsets.bottom : bottomAreaHeight + layout.intrinsicInsets.bottom + + let size = layout.size + let contentWidth: CGFloat + var contentLeftInset: CGFloat = 0.0 + var forceUpdate = false if case .regular = layout.metrics.widthClass { - size.width = floor(min(size.width, size.height) * 0.5) + contentWidth = max(320.0, min(375.0, floor(size.width * 0.3))) + if self.peerIdToEndpointId.isEmpty { + contentLeftInset = 0.0 + } else { + contentLeftInset = self.panelHidden ? layout.size.width : layout.size.width - contentWidth + } + forceUpdate = true + } else { + contentWidth = isLandscape ? min(530.0, size.width - 210.0) : size.width } - let listSize = CGSize(width: size.width, height: layout.size.height - listTopInset - bottomPanelHeight) + let listSize = CGSize(width: contentWidth, height: layout.size.height - listTopInset - bottomPanelHeight + bottomGradientHeight) let topInset: CGFloat if let (panInitialTopInset, panOffset) = self.panGestureArguments { if self.isExpanded { @@ -2446,105 +3631,195 @@ public final class VoiceChatController: ViewController { } else { topInset = max(0.0, panInitialTopInset + min(0.0, panOffset)) } - } else if let _ = self.animation { - topInset = self.listNode.frame.minY - listTopInset + } else if case .regular = layout.metrics.widthClass { + topInset = 0.0 } else if let currentTopInset = self.topInset { topInset = self.isExpanded ? 0.0 : currentTopInset } else { - topInset = listSize.height + topInset = listSize.height - 46.0 - floor(56.0 * 3.5) } - let offset = offset + topInset - self.floatingHeaderOffset = offset - - let rawPanelOffset = offset + listTopInset - topPanelHeight + var bottomEdge: CGFloat = 0.0 + if case .regular = layout.metrics.widthClass { + bottomEdge = size.height + } else { + self.listNode.forEachItemNode { itemNode in + if let itemNode = itemNode as? ListViewItemNode { + let convertedFrame = self.listNode.view.convert(itemNode.frame, to: self.contentContainer.view) + if convertedFrame.maxY > bottomEdge { + bottomEdge = convertedFrame.maxY + } + } + } + if bottomEdge.isZero { + bottomEdge = self.listNode.frame.minY + 46.0 + 56.0 + } + } + + let rawPanelOffset = topInset + listTopInset - topPanelHeight let panelOffset = max(layoutTopInset, rawPanelOffset) - let topPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: panelOffset), size: CGSize(width: size.width, height: topPanelHeight)) - - if let mainVideoContainer = self.mainVideoContainer { - let videoContainerFrame = CGRect(origin: CGPoint(x: 0.0, y: topPanelFrame.maxY), size: CGSize(width: layout.size.width, height: 200.0)) - transition.updateFrameAdditive(node: mainVideoContainer, frame: videoContainerFrame) - mainVideoContainer.update(size: videoContainerFrame.size, transition: transition) + let topPanelFrame: CGRect + if isLandscape { + topPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: topPanelHeight)) + } else { + topPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: panelOffset), size: CGSize(width: size.width, height: topPanelHeight)) } + let sideInset: CGFloat = 14.0 + + let bottomPanelCoverHeight = bottomAreaHeight + layout.intrinsicInsets.bottom + var bottomGradientFrame = CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - bottomPanelCoverHeight), size: CGSize(width: size.width, height: bottomGradientHeight)) + if isLandscape { + bottomGradientFrame.origin.y = layout.size.height + } + + let transitionContainerFrame = CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: layout.size.height) + transition.updateFrame(node: self.transitionContainerNode, frame: transitionContainerFrame) + transition.updateFrame(view: self.transitionMaskView, frame: CGRect(x: 0.0, y: 0.0, width: transitionContainerFrame.width, height: transitionContainerFrame.height)) + let updateMaskLayers = { + var topPanelFrame = topPanelFrame + if self.animatingContextMenu { + topPanelFrame.origin.y = 0.0 + } + transition.updateFrame(layer: self.transitionMaskTopFillLayer, frame: CGRect(x: 0.0, y: 0.0, width: transitionContainerFrame.width, height: topPanelFrame.maxY)) + transition.updateFrame(layer: self.transitionMaskFillLayer, frame: CGRect(x: 0.0, y: topPanelFrame.maxY, width: transitionContainerFrame.width, height: bottomGradientFrame.minY - topPanelFrame.maxY)) + transition.updateFrame(layer: self.transitionMaskGradientLayer, frame: CGRect(x: 0.0, y: bottomGradientFrame.minY, width: transitionContainerFrame.width, height: bottomGradientFrame.height)) + transition.updateFrame(layer: self.transitionMaskBottomFillLayer, frame: CGRect(x: 0.0, y: bottomGradientFrame.minY, width: transitionContainerFrame.width, height: max(0.0, transitionContainerFrame.height - bottomGradientFrame.minY))) + } + if transition.isAnimated { + updateMaskLayers() + } else { + CATransaction.begin() + CATransaction.setDisableActions(true) + updateMaskLayers() + CATransaction.commit() + } + + var bottomInset: CGFloat = 0.0 + if case .compact = layout.metrics.widthClass, case let .fullscreen(controlsHidden) = self.displayMode { + if !controlsHidden { + bottomInset = 80.0 + } + } + transition.updateAlpha(node: self.bottomGradientNode, alpha: self.isLandscape ? 0.0 : 1.0) + + var isTablet = false + let videoFrame: CGRect + let videoContainerFrame: CGRect + if case .regular = layout.metrics.widthClass { + isTablet = true + let videoTopEdgeY = topPanelFrame.maxY + let videoBottomEdgeY = layout.size.height - layout.intrinsicInsets.bottom + videoFrame = CGRect(x: sideInset, y: 0.0, width: contentLeftInset - sideInset, height: videoBottomEdgeY - videoTopEdgeY) + videoContainerFrame = CGRect(origin: CGPoint(x: 0.0, y: videoTopEdgeY), size: CGSize(width: contentLeftInset, height: layout.size.height)) + } else { + let videoTopEdgeY = isLandscape ? 0.0 : layoutTopInset + let videoBottomEdgeY = self.isLandscape ? layout.size.height : layout.size.height - layout.intrinsicInsets.bottom - 92.0 + videoFrame = CGRect(x: 0.0, y: videoTopEdgeY, width: isLandscape ? max(0.0, layout.size.width - layout.safeInsets.right - 92.0) : layout.size.width, height: videoBottomEdgeY - videoTopEdgeY) + videoContainerFrame = CGRect(origin: CGPoint(), size: layout.size) + } + transition.updateFrame(node: self.mainStageContainerNode, frame: videoContainerFrame) + transition.updateFrame(node: self.mainStageBackgroundNode, frame: videoFrame) + if !self.mainStageNode.animating { + transition.updateFrame(node: self.mainStageNode, frame: videoFrame) + } + self.mainStageNode.update(size: videoFrame.size, sideInset: layout.safeInsets.left, bottomInset: self.isLandscape ? 0.0 : bottomInset, isLandscape: videoFrame.width > videoFrame.height, isTablet: isTablet, transition: transition) + let backgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: topPanelFrame.maxY), size: CGSize(width: size.width, height: layout.size.height)) - let sideInset: CGFloat = 16.0 - let leftBorderFrame = CGRect(origin: CGPoint(x: 0.0, y: topPanelFrame.maxY - 16.0), size: CGSize(width: sideInset, height: layout.size.height)) - let rightBorderFrame = CGRect(origin: CGPoint(x: size.width - sideInset, y: topPanelFrame.maxY - 16.0), size: CGSize(width: sideInset, height: layout.size.height)) + + let leftBorderFrame: CGRect + let rightBorderFrame: CGRect + let additionalInset: CGFloat = 60.0 + let additionalSideInset = (size.width - contentWidth) / 2.0 + let additionalLeftInset = size.width / 2.0 + if isLandscape { + leftBorderFrame = CGRect(origin: CGPoint(x: 0.0, y: topPanelFrame.maxY - additionalInset), size: CGSize(width: (size.width - contentWidth) / 2.0 + sideInset, height: layout.size.height)) + rightBorderFrame = CGRect(origin: CGPoint(x: size.width - (size.width - contentWidth) / 2.0 - sideInset, y: topPanelFrame.maxY - additionalInset), size: CGSize(width: layout.safeInsets.right + (size.width - contentWidth) / 2.0 + sideInset, height: layout.size.height)) + } else { + var isFullscreen = false + if case .fullscreen = self.displayMode { + isFullscreen = true + forceUpdate = true + } + leftBorderFrame = CGRect(origin: CGPoint(x: -additionalInset - additionalLeftInset, y: topPanelFrame.maxY - additionalInset * (isFullscreen ? 0.95 : 0.8)), size: CGSize(width: sideInset + additionalInset + additionalLeftInset + (contentLeftInset.isZero ? additionalSideInset : contentLeftInset), height: layout.size.height)) + rightBorderFrame = CGRect(origin: CGPoint(x: size.width - sideInset - (contentLeftInset.isZero ? additionalSideInset : 0.0), y: topPanelFrame.maxY - additionalInset * (isFullscreen ? 0.95 : 0.8)), size: CGSize(width: sideInset + additionalInset + additionalLeftInset + additionalSideInset, height: layout.size.height)) + } + + let topCornersFrame = CGRect(x: sideInset + (contentLeftInset.isZero ? floorToScreenPixels((size.width - contentWidth) / 2.0) : contentLeftInset), y: topPanelFrame.maxY - 60.0, width: contentWidth - sideInset * 2.0, height: 50.0 + 60.0) let previousTopPanelFrame = self.topPanelNode.frame let previousBackgroundFrame = self.backgroundNode.frame let previousLeftBorderFrame = self.leftBorderNode.frame let previousRightBorderFrame = self.rightBorderNode.frame - if !topPanelFrame.equalTo(previousTopPanelFrame) { - self.topPanelNode.frame = topPanelFrame - let positionDelta = CGPoint(x: 0.0, y: topPanelFrame.minY - previousTopPanelFrame.minY) - transition.animateOffsetAdditive(layer: self.topPanelNode.layer, offset: positionDelta.y, completion: completion) + if !topPanelFrame.equalTo(previousTopPanelFrame) || forceUpdate { + if topPanelFrame.width != previousTopPanelFrame.width { + transition.updateFrame(node: self.topPanelNode, frame: topPanelFrame) + transition.updateFrame(node: self.topCornersNode, frame: topCornersFrame) + transition.updateFrame(node: self.backgroundNode, frame: backgroundFrame) + transition.updateFrame(node: self.leftBorderNode, frame: leftBorderFrame) + transition.updateFrame(node: self.rightBorderNode, frame: rightBorderFrame) + } else { + self.topPanelNode.frame = topPanelFrame + let positionDelta = CGPoint(x: 0.0, y: topPanelFrame.minY - previousTopPanelFrame.minY) + transition.animateOffsetAdditive(layer: self.topPanelNode.layer, offset: positionDelta.y, completion: completion) - self.backgroundNode.frame = backgroundFrame - let backgroundPositionDelta = CGPoint(x: 0.0, y: previousBackgroundFrame.minY - backgroundFrame.minY) - transition.animatePositionAdditive(node: self.backgroundNode, offset: backgroundPositionDelta) - - self.leftBorderNode.frame = leftBorderFrame - let leftBorderPositionDelta = CGPoint(x: 0.0, y: previousLeftBorderFrame.minY - leftBorderFrame.minY) - transition.animatePositionAdditive(node: self.leftBorderNode, offset: leftBorderPositionDelta) - - self.rightBorderNode.frame = rightBorderFrame - let rightBorderPositionDelta = CGPoint(x: 0.0, y: previousRightBorderFrame.minY - rightBorderFrame.minY) - transition.animatePositionAdditive(node: self.rightBorderNode, offset: rightBorderPositionDelta) + transition.updateFrame(node: self.topCornersNode, frame: topCornersFrame) + + self.backgroundNode.frame = backgroundFrame + let backgroundPositionDelta = CGPoint(x: 0.0, y: previousBackgroundFrame.minY - backgroundFrame.minY) + transition.animatePositionAdditive(node: self.backgroundNode, offset: backgroundPositionDelta) + + self.leftBorderNode.frame = leftBorderFrame + let leftBorderPositionDelta = CGPoint(x: previousLeftBorderFrame.maxX - leftBorderFrame.maxX, y: previousLeftBorderFrame.minY - leftBorderFrame.minY) + transition.animatePositionAdditive(node: self.leftBorderNode, offset: leftBorderPositionDelta) + + self.rightBorderNode.frame = rightBorderFrame + let rightBorderPositionDelta = CGPoint(x: previousRightBorderFrame.minX - rightBorderFrame.minX, y: previousRightBorderFrame.minY - rightBorderFrame.minY) + transition.animatePositionAdditive(node: self.rightBorderNode, offset: rightBorderPositionDelta) + } } else { completion?() } - self.topPanelBackgroundNode.frame = CGRect(x: 0.0, y: topPanelHeight - 24.0, width: size.width, height: 24.0) - - var bottomEdge: CGFloat = 0.0 - self.listNode.forEachItemNode { itemNode in - if let itemNode = itemNode as? ListViewItemNode { - let convertedFrame = self.listNode.view.convert(itemNode.frame, to: self.view) - if convertedFrame.maxY > bottomEdge { - bottomEdge = convertedFrame.maxY - } - } - } - - let listMaxY = listTopInset + listSize.height - if bottomEdge.isZero { - bottomEdge = listMaxY - } + + self.topPanelBackgroundNode.frame = CGRect(x: 0.0, y: topPanelHeight - 24.0, width: size.width, height: min(topPanelFrame.height, 24.0)) - var bottomOffset: CGFloat = 0.0 - if bottomEdge < listMaxY && (self.panGestureArguments != nil || self.isExpanded) { - bottomOffset = bottomEdge - listMaxY - } - - let bottomCornersFrame = CGRect(origin: CGPoint(x: sideInset, y: -50.0 + bottomOffset), size: CGSize(width: size.width - sideInset * 2.0, height: 50.0)) + let listMaxY = listTopInset + listSize.height + let bottomOffset = min(0.0, bottomEdge - listMaxY) + layout.size.height - bottomPanelHeight + + let bottomCornersFrame = CGRect(origin: CGPoint(x: sideInset + floorToScreenPixels((size.width - contentWidth) / 2.0), y: -50.0 + bottomOffset + bottomGradientHeight), size: CGSize(width: contentWidth - sideInset * 2.0, height: 50.0 + 60.0)) + let bottomPanelBackgroundFrame = CGRect(x: 0.0, y: bottomOffset + bottomGradientHeight, width: size.width, height: 2000.0) let previousBottomCornersFrame = self.bottomCornersNode.frame if !bottomCornersFrame.equalTo(previousBottomCornersFrame) { - self.bottomCornersNode.frame = bottomCornersFrame - self.bottomPanelBackgroundNode.frame = CGRect(x: 0.0, y: bottomOffset, width: size.width, height: 2000.0) - - let positionDelta = CGPoint(x: 0.0, y: previousBottomCornersFrame.minY - bottomCornersFrame.minY) - transition.animatePositionAdditive(node: self.bottomCornersNode, offset: positionDelta) - transition.animatePositionAdditive(node: self.bottomPanelBackgroundNode, offset: positionDelta) + if bottomCornersFrame.width != previousBottomCornersFrame.width { + transition.updateFrame(node: self.bottomCornersNode, frame: bottomCornersFrame) + transition.updateFrame(node: self.bottomPanelBackgroundNode, frame: bottomPanelBackgroundFrame) + } else { + self.bottomCornersNode.frame = bottomCornersFrame + self.bottomPanelBackgroundNode.frame = bottomPanelBackgroundFrame + + let positionDelta = CGPoint(x: 0.0, y: previousBottomCornersFrame.minY - bottomCornersFrame.minY) + transition.animatePositionAdditive(node: self.bottomCornersNode, offset: positionDelta) + transition.animatePositionAdditive(node: self.bottomPanelBackgroundNode, offset: positionDelta) + } } } - var isFullscreen = false - func updateIsFullscreen(_ isFullscreen: Bool) { - guard self.isFullscreen != isFullscreen, let (layout, _) = self.validLayout else { + private var decorationsAreDark: Bool? + private var ignoreLayout = false + private func updateDecorationsColors() { + guard let (layout, _) = self.validLayout else { return } - self.isFullscreen = isFullscreen - - self.controller?.statusBar.updateStatusBarStyle(isFullscreen ? .White : .Ignore, animated: true) - - var size = layout.size - if case .regular = layout.metrics.widthClass { - size.width = floor(min(size.width, size.height) * 0.5) - } - let topPanelHeight: CGFloat = 63.0 + let isFullscreen = self.isFullscreen + let effectiveDisplayMode = self.displayMode + + self.ignoreLayout = true + self.controller?.statusBar.updateStatusBarStyle(isFullscreen ? .White : .Ignore, animated: true) + self.ignoreLayout = false + + let size = layout.size let topEdgeFrame: CGRect if isFullscreen { let offset: CGFloat @@ -2558,51 +3833,93 @@ public final class VoiceChatController: ViewController { topEdgeFrame = CGRect(x: 0.0, y: 0.0, width: size.width, height: topPanelHeight) } + let backgroundColor: UIColor + if case .fullscreen = effectiveDisplayMode { + backgroundColor = isFullscreen ? panelBackgroundColor : secondaryPanelBackgroundColor + } else if self.isScheduling || self.callState?.scheduleTimestamp != nil { + backgroundColor = panelBackgroundColor + } else { + backgroundColor = isFullscreen ? panelBackgroundColor : secondaryPanelBackgroundColor + } + let transition: ContainedViewLayoutTransition = .animated(duration: 0.3, curve: .linear) transition.updateFrame(node: self.topPanelEdgeNode, frame: topEdgeFrame) transition.updateCornerRadius(node: self.topPanelEdgeNode, cornerRadius: isFullscreen ? layout.deviceMetrics.screenCornerRadius - 0.5 : 12.0) transition.updateBackgroundColor(node: self.topPanelBackgroundNode, color: isFullscreen ? fullscreenBackgroundColor : panelBackgroundColor) transition.updateBackgroundColor(node: self.topPanelEdgeNode, color: isFullscreen ? fullscreenBackgroundColor : panelBackgroundColor) - transition.updateBackgroundColor(node: self.backgroundNode, color: isFullscreen ? panelBackgroundColor : secondaryPanelBackgroundColor) + transition.updateBackgroundColor(node: self.backgroundNode, color: backgroundColor) transition.updateBackgroundColor(node: self.bottomPanelBackgroundNode, color: isFullscreen ? fullscreenBackgroundColor : panelBackgroundColor) transition.updateBackgroundColor(node: self.leftBorderNode, color: isFullscreen ? fullscreenBackgroundColor : panelBackgroundColor) transition.updateBackgroundColor(node: self.rightBorderNode, color: isFullscreen ? fullscreenBackgroundColor : panelBackgroundColor) - transition.updateBackgroundColor(node: self.rightBorderNode, color: isFullscreen ? fullscreenBackgroundColor : panelBackgroundColor) - if let snapshotView = self.topCornersNode.view.snapshotContentTree() { - snapshotView.frame = self.topCornersNode.frame - self.topPanelNode.view.addSubview(snapshotView) - - snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak snapshotView] _ in - snapshotView?.removeFromSuperview() - }) + var gridNode: VoiceChatTilesGridItemNode? + self.listNode.forEachItemNode { itemNode in + if let itemNode = itemNode as? VoiceChatTilesGridItemNode { + gridNode = itemNode + } + } + if let gridNode = gridNode { + transition.updateBackgroundColor(node: gridNode.backgroundNode, color: isFullscreen ? fullscreenBackgroundColor : panelBackgroundColor) } - self.topCornersNode.image = cornersImage(top: true, bottom: false, dark: isFullscreen) - if let snapshotView = self.bottomCornersNode.view.snapshotContentTree() { - snapshotView.frame = self.bottomCornersNode.bounds - self.bottomCornersNode.view.addSubview(snapshotView) + let previousDark = self.decorationsAreDark + self.decorationsAreDark = isFullscreen + if previousDark != self.decorationsAreDark { + if let snapshotView = self.topCornersNode.view.snapshotContentTree() { + snapshotView.frame = self.topCornersNode.bounds + self.topCornersNode.view.addSubview(snapshotView) + + snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, timingFunction: CAMediaTimingFunctionName.linear.rawValue, removeOnCompletion: false, completion: { [weak snapshotView] _ in + snapshotView?.removeFromSuperview() + }) + } + self.topCornersNode.image = decorationTopCornersImage(dark: isFullscreen) - snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak snapshotView] _ in - snapshotView?.removeFromSuperview() - }) - } - self.bottomCornersNode.image = cornersImage(top: false, bottom: true, dark: isFullscreen) + if let snapshotView = self.bottomCornersNode.view.snapshotContentTree() { + snapshotView.frame = self.bottomCornersNode.bounds + self.bottomCornersNode.view.addSubview(snapshotView) + + snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, timingFunction: CAMediaTimingFunctionName.linear.rawValue, removeOnCompletion: false, completion: { [weak snapshotView] _ in + snapshotView?.removeFromSuperview() + }) + } + self.bottomCornersNode.image = decorationBottomCornersImage(dark: isFullscreen) + + if let gridNode = gridNode { + if let snapshotView = gridNode.cornersNode.view.snapshotContentTree() { + snapshotView.frame = gridNode.cornersNode.bounds + gridNode.cornersNode.view.addSubview(snapshotView) + + snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, timingFunction: CAMediaTimingFunctionName.linear.rawValue, removeOnCompletion: false, completion: { [weak snapshotView] _ in + snapshotView?.removeFromSuperview() + }) + } + gridNode.cornersNode.image = decorationCornersImage(top: true, bottom: false, dark: isFullscreen) + gridNode.supernode?.addSubnode(gridNode) + } + + UIView.transition(with: self.bottomGradientNode.view, duration: 0.3, options: [.transitionCrossDissolve, .curveLinear]) { + self.bottomGradientNode.backgroundColor = decorationBottomGradientImage(dark: isFullscreen).flatMap { UIColor(patternImage: $0) } + } completion: { _ in + } - if !self.optionsButtonIsAvatar { - self.optionsButton.setContent(.image(optionsButtonImage(dark: isFullscreen)), animated: transition.isAnimated) + self.closeButton.setContent(.image(closeButtonImage(dark: isFullscreen)), animated: transition.isAnimated) + self.optionsButton.setContent(.more(optionsCircleImage(dark: isFullscreen)), animated: transition.isAnimated) + self.panelButton.setContent(.image(panelButtonImage(dark: isFullscreen)), animated: transition.isAnimated) } - self.closeButton.setContent(.image(closeButtonImage(dark: isFullscreen)), animated: transition.isAnimated) - + self.updateTitle(transition: transition) } - private func updateTitle(transition: ContainedViewLayoutTransition) { - guard let (layout, _) = self.validLayout else { + private func updateTitle(slide: Bool = false, transition: ContainedViewLayoutTransition) { + guard let _ = self.validLayout else { return } + var title = self.currentTitle - if !self.isFullscreen && !self.currentTitleIsCustom { + if self.isScheduling { + title = self.presentationData.strings.ScheduleVoiceChat_Title + } else if case .modal(_, false) = self.displayMode, !self.currentTitleIsCustom { if let navigationController = self.controller?.navigationController as? NavigationController { for controller in navigationController.viewControllers.reversed() { if let controller = controller as? ChatController, case let .peer(peerId) = controller.chatLocation, peerId == self.call.peerId { @@ -2612,22 +3929,27 @@ public final class VoiceChatController: ViewController { } } - var size = layout.size - if case .regular = layout.metrics.widthClass { - size.width = floor(min(size.width, size.height) * 0.5) + var subtitle = self.currentSpeakingSubtitle ?? self.currentSubtitle + var speaking = self.currentSpeakingSubtitle != nil + if self.isScheduling { + subtitle = "" + speaking = false + } else if self.callState?.scheduleTimestamp != nil { + if self.callState?.canManageCall ?? false { + subtitle = self.presentationData.strings.VoiceChat_TapToEditTitle + } else { + subtitle = self.presentationData.strings.VoiceChat_Scheduled + } + speaking = false } - self.titleNode.update(size: CGSize(width: size.width, height: 44.0), title: title, subtitle: self.currentSubtitle, transition: transition) + self.titleNode.update(size: CGSize(width: self.titleNode.bounds.width, height: 44.0), title: title, subtitle: subtitle, speaking: speaking, slide: slide, transition: transition) } - private func updateButtons(animated: Bool) { - let audioButtonAppearance: CallControllerButtonItemNode.Content.Appearance - if let color = self.currentAudioButtonColor { - audioButtonAppearance = .color(.custom(color.rgb, 1.0)) - } else { - audioButtonAppearance = .color(.custom(self.isFullscreen ? 0x1c1c1e : 0x2c2c2e, 1.0)) + private func updateButtons(transition: ContainedViewLayoutTransition) { + guard let (layout, _) = self.validLayout else { + return } - var audioMode: CallControllerButtonsSpeakerMode = .none //var hasAudioRouteMenu: Bool = false if let (availableOutputs, maybeCurrentOutput) = self.audioOutputState, let currentOutput = maybeCurrentOutput { @@ -2642,7 +3964,9 @@ public final class VoiceChatController: ViewController { case let .port(port): var type: CallControllerButtonsSpeakerMode.BluetoothType = .generic let portName = port.name.lowercased() - if portName.contains("airpods pro") { + if portName.contains("airpods max") { + type = .airpodsMax + } else if portName.contains("airpods pro") { type = .airpodsPro } else if portName.contains("airpods") { type = .airpods @@ -2654,15 +3978,28 @@ public final class VoiceChatController: ViewController { } } - let soundImage: CallControllerButtonItemNode.Content.Image - var soundAppearance: CallControllerButtonItemNode.Content.Appearance = audioButtonAppearance + let normalButtonAppearance: CallControllerButtonItemNode.Content.Appearance + let activeButtonAppearance: CallControllerButtonItemNode.Content.Appearance + if let color = self.currentNormalButtonColor { + normalButtonAppearance = .color(.custom(color.rgb, 1.0)) + } else { + normalButtonAppearance = .color(.custom(self.isFullscreen ? 0x1c1c1e : 0x2c2c2e, 1.0)) + } + if let color = self.currentActiveButtonColor { + activeButtonAppearance = .color(.custom(color.rgb, 1.0)) + } else { + activeButtonAppearance = .color(.custom(self.isFullscreen ? 0x1c1c1e : 0x2c2c2e, 1.0)) + } + + var soundImage: CallControllerButtonItemNode.Content.Image + var soundAppearance: CallControllerButtonItemNode.Content.Appearance = normalButtonAppearance var soundTitle: String = self.presentationData.strings.Call_Speaker switch audioMode { case .none, .builtin: soundImage = .speaker case .speaker: soundImage = .speaker - soundAppearance = .blurred(isFilled: true) + soundAppearance = activeButtonAppearance case .headphones: soundImage = .headphones soundTitle = self.presentationData.strings.Call_Audio @@ -2674,69 +4011,168 @@ public final class VoiceChatController: ViewController { soundImage = .airpods case .airpodsPro: soundImage = .airpodsPro + case .airpodsMax: + soundImage = .airpodsMax } soundTitle = self.presentationData.strings.Call_Audio } + + let isScheduled = self.isScheduling || self.callState?.scheduleTimestamp != nil - self.audioOutputNode.update(size: sideButtonSize, content: CallControllerButtonItemNode.Content(appearance: soundAppearance, image: soundImage), text: soundTitle, transition: animated ? .animated(duration: 0.3, curve: .linear) : .immediate) + var isSoundEnabled = true + if isScheduled { + if let callState = self.callState, let peer = self.peer, !callState.canManageCall && (peer.addressName?.isEmpty ?? true) { + isSoundEnabled = false + } else { + soundImage = .share + soundTitle = self.presentationData.strings.VoiceChat_ShareShort + soundAppearance = normalButtonAppearance + } + } - let cameraButtonSize = CGSize(width: 40.0, height: 40.0) + let audioButtonSize: CGSize + var buttonsTitleAlpha: CGFloat + let effectiveDisplayMode = self.displayMode - self.cameraButtonNode.update(size: cameraButtonSize, content: CallControllerButtonItemNode.Content(appearance: CallControllerButtonItemNode.Content.Appearance.blurred(isFilled: false), image: .camera), text: " ", transition: animated ? .animated(duration: 0.3, curve: .linear) : .immediate) + let hasCameraButton = self.cameraButton.isUserInteractionEnabled + let hasVideo = self.call.hasVideo + switch effectiveDisplayMode { + case .modal: + audioButtonSize = hasCameraButton ? smallButtonSize : sideButtonSize + buttonsTitleAlpha = 1.0 + case .fullscreen: + if case .regular = layout.metrics.widthClass { + audioButtonSize = hasCameraButton ? smallButtonSize : sideButtonSize + } else { + audioButtonSize = sideButtonSize + } + if case .regular = layout.metrics.widthClass { + buttonsTitleAlpha = 1.0 + } else { + buttonsTitleAlpha = 0.0 + } + } - self.leaveNode.update(size: sideButtonSize, content: CallControllerButtonItemNode.Content(appearance: .color(.custom(0xff3b30, 0.3)), image: .cancel), text: self.presentationData.strings.VoiceChat_Leave, transition: .immediate) + self.cameraButton.update(size: sideButtonSize, content: CallControllerButtonItemNode.Content(appearance: hasVideo ? activeButtonAppearance : normalButtonAppearance, image: hasVideo ? .cameraOn : .cameraOff), text: self.presentationData.strings.VoiceChat_Video, transition: transition) + + self.switchCameraButton.update(size: audioButtonSize, content: CallControllerButtonItemNode.Content(appearance: normalButtonAppearance, image: .flipCamera), text: "", transition: transition) + + transition.updateAlpha(node: self.switchCameraButton, alpha: hasCameraButton && hasVideo ? 1.0 : 0.0) + transition.updateTransformScale(node: self.switchCameraButton, scale: hasCameraButton && hasVideo ? 1.0 : 0.0) + + transition.updateTransformScale(node: self.cameraButton, scale: hasCameraButton ? 1.0 : 0.0) + + let hasAudioButton = !self.isScheduling + transition.updateAlpha(node: self.audioButton, alpha: hasCameraButton || !hasAudioButton ? 0.0 : 1.0) + transition.updateTransformScale(node: self.audioButton, scale: hasCameraButton || !hasAudioButton ? 0.0 : 1.0) + + self.audioButton.update(size: audioButtonSize, content: CallControllerButtonItemNode.Content(appearance: soundAppearance, image: soundImage, isEnabled: isSoundEnabled), text: soundTitle, transition: transition) + self.audioButton.isUserInteractionEnabled = isSoundEnabled + + self.leaveButton.update(size: sideButtonSize, content: CallControllerButtonItemNode.Content(appearance: .color(.custom(0xff3b30, 0.3)), image: .cancel), text: self.presentationData.strings.VoiceChat_Leave, transition: .immediate) + + transition.updateAlpha(node: self.cameraButton.textNode, alpha: buttonsTitleAlpha) + transition.updateAlpha(node: self.switchCameraButton.textNode, alpha: buttonsTitleAlpha) + transition.updateAlpha(node: self.audioButton.textNode, alpha: buttonsTitleAlpha) + transition.updateAlpha(node: self.leaveButton.textNode, alpha: buttonsTitleAlpha) } func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationHeight: CGFloat, transition: ContainedViewLayoutTransition) { + guard !self.ignoreLayout else { + return + } let isFirstTime = self.validLayout == nil + let previousLayout = self.validLayout?.0 self.validLayout = (layout, navigationHeight) - - var size = layout.size + + let size = layout.size + let contentWidth: CGFloat + let headerWidth: CGFloat + let contentLeftInset: CGFloat if case .regular = layout.metrics.widthClass { - size.width = floor(min(size.width, size.height) * 0.5) + contentWidth = max(320.0, min(375.0, floor(size.width * 0.3))) + headerWidth = size.width + if self.peerIdToEndpointId.isEmpty { + contentLeftInset = 0.0 + } else { + contentLeftInset = self.panelHidden ? layout.size.width : layout.size.width - contentWidth + } + } else { + contentWidth = self.isLandscape ? min(530.0, size.width - 210.0) : size.width + headerWidth = contentWidth + contentLeftInset = 0.0 } + var previousIsLandscape = false + if let previousLayout = previousLayout, case .compact = previousLayout.metrics.widthClass, previousLayout.size.width > previousLayout.size.height { + previousIsLandscape = true + } + var shouldSwitchToExpanded = false + if case let .modal(isExpanded, _) = self.displayMode { + if previousIsLandscape != self.isLandscape && !isExpanded { + shouldSwitchToExpanded = true + } else if case .regular = layout.metrics.widthClass, !isExpanded { + shouldSwitchToExpanded = true + } + } + if shouldSwitchToExpanded { + self.displayMode = .modal(isExpanded: true, isFilled: true) + self.updateDecorationsColors() + self.updateDecorationsLayout(transition: transition) + self.updateMembers() + } else if case .fullscreen = self.displayMode, previousIsLandscape != self.isLandscape { + self.updateMembers() + } + + let effectiveDisplayMode = self.displayMode + + transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - headerWidth) / 2.0), y: 10.0), size: CGSize(width: headerWidth, height: 44.0))) self.updateTitle(transition: transition) - transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 10.0), size: CGSize(width: size.width, height: 44.0))) - transition.updateFrame(node: self.optionsButton, frame: CGRect(origin: CGPoint(x: 20.0, y: 18.0), size: CGSize(width: 28.0, height: 28.0))) - transition.updateFrame(node: self.closeButton, frame: CGRect(origin: CGPoint(x: size.width - 20.0 - 28.0, y: 18.0), size: CGSize(width: 28.0, height: 28.0))) + + transition.updateFrame(node: self.optionsButton, frame: CGRect(origin: CGPoint(x: 20.0 + floorToScreenPixels((size.width - headerWidth) / 2.0), y: 18.0), size: CGSize(width: 28.0, height: 28.0))) + transition.updateFrame(node: self.panelButton, frame: CGRect(origin: CGPoint(x: size.width - floorToScreenPixels((size.width - headerWidth) / 2.0) - 20.0 - 28.0 - 38.0 - 24.0, y: 18.0), size: CGSize(width: 38.0, height: 28.0))) + transition.updateFrame(node: self.closeButton, frame: CGRect(origin: CGPoint(x: size.width - floorToScreenPixels((size.width - headerWidth) / 2.0) - 20.0 - 28.0, y: 18.0), size: CGSize(width: 28.0, height: 28.0))) + + transition.updateAlpha(node: self.optionsButton, alpha: self.optionsButton.isUserInteractionEnabled ? 1.0 : 0.0) + transition.updateAlpha(node: self.panelButton, alpha: self.panelButton.isUserInteractionEnabled ? 1.0 : 0.0) transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(), size: layout.size)) - transition.updateFrame(node: self.contentContainer, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((layout.size.width - size.width) / 2.0), y: 0.0), size: size)) let layoutTopInset: CGFloat = max(layout.statusBarHeight ?? 0.0, layout.safeInsets.top) + let sideInset: CGFloat = 14.0 - let sideInset: CGFloat = 16.0 - var insets = UIEdgeInsets() - insets.left = layout.safeInsets.left + sideInset - insets.right = layout.safeInsets.right + sideInset + var listInsets = UIEdgeInsets() + listInsets.left = sideInset + (self.isLandscape ? 0.0 : layout.safeInsets.left) + listInsets.right = sideInset + (self.isLandscape ? 0.0 : layout.safeInsets.right) - let topPanelHeight: CGFloat = 63.0 - if let _ = self.panGestureArguments { + let topEdgeOffset: CGFloat + if let statusBarHeight = layout.statusBarHeight { + topEdgeOffset = statusBarHeight + } else { + topEdgeOffset = 44.0 + } + + if self.isLandscape { + transition.updateFrame(node: self.topPanelEdgeNode, frame: CGRect(x: 0.0, y: -topEdgeOffset, width: size.width, height: topPanelHeight + topEdgeOffset)) + } else if let _ = self.panGestureArguments { } else { let topEdgeFrame: CGRect if self.isFullscreen { - let offset: CGFloat - if let statusBarHeight = layout.statusBarHeight { - offset = statusBarHeight - } else { - offset = 44.0 - } - topEdgeFrame = CGRect(x: 0.0, y: -offset, width: size.width, height: topPanelHeight + offset) + topEdgeFrame = CGRect(x: 0.0, y: -topEdgeOffset, width: size.width, height: topPanelHeight + topEdgeOffset) } else { topEdgeFrame = CGRect(x: 0.0, y: 0.0, width: size.width, height: topPanelHeight) } transition.updateFrame(node: self.topPanelEdgeNode, frame: topEdgeFrame) } - let bottomPanelHeight = bottomAreaHeight + layout.intrinsicInsets.bottom + let bottomPanelHeight = self.effectiveBottomAreaHeight + layout.intrinsicInsets.bottom var listTopInset = layoutTopInset + topPanelHeight - if self.mainVideoContainer != nil { - listTopInset += 200.0 + if self.isLandscape { + listTopInset = topPanelHeight } - let listSize = CGSize(width: size.width, height: layout.size.height - listTopInset - bottomPanelHeight) - + + let listSize = CGSize(width: contentWidth, height: layout.size.height - listTopInset - (self.isLandscape ? layout.intrinsicInsets.bottom : bottomPanelHeight) + bottomGradientHeight) let topInset: CGFloat if let (panInitialTopInset, panOffset) = self.panGestureArguments { if self.isExpanded { @@ -2744,154 +4180,475 @@ public final class VoiceChatController: ViewController { } else { topInset = max(0.0, panInitialTopInset + min(0.0, panOffset)) } + } else if case .regular = layout.metrics.widthClass { + topInset = 0.0 } else if let currentTopInset = self.topInset { topInset = self.isExpanded ? 0.0 : currentTopInset } else { - topInset = listSize.height + topInset = listSize.height - 46.0 - floor(56.0 * 3.5) - bottomGradientHeight } - if self.animation == nil { - transition.updateFrame(node: self.listNode, frame: CGRect(origin: CGPoint(x: 0.0, y: listTopInset + topInset), size: listSize)) + transition.updateFrameAsPositionAndBounds(node: self.listContainer, frame: CGRect(origin: CGPoint(), size: size)) + transition.updateFrame(node: self.listNode, frame: CGRect(origin: CGPoint(x: contentLeftInset.isZero ? floorToScreenPixels((size.width - contentWidth) / 2.0) : contentLeftInset, y: listTopInset + topInset), size: listSize)) + + let tileGridSize = CGSize(width: max(0.0, contentLeftInset - sideInset), height: size.height - layout.intrinsicInsets.bottom - listTopInset - topInset) + + if contentLeftInset > 0.0 { + self.tileGridNode.isHidden = false } + if !self.tileGridNode.isHidden { + let _ = self.tileGridNode.update(size: tileGridSize, layoutMode: .grid, items: self.currentTileItems, transition: transition, completion: { [weak self] in + if contentLeftInset.isZero && transition.isAnimated { + self?.tileGridNode.isHidden = true + } + }) + } + transition.updateFrame(node: self.tileGridNode, frame: CGRect(origin: CGPoint(x: sideInset, y: listTopInset + topInset), size: tileGridSize)) + self.tileGridNode.updateAbsoluteRect(CGRect(origin: CGPoint(), size: tileGridSize), within: tileGridSize) + + listInsets.bottom = bottomGradientHeight let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition) - let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: listSize, insets: insets, duration: duration, curve: curve) + self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: listSize, insets: listInsets, duration: duration, curve: curve), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) - self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: nil, updateSizeAndInsets: updateSizeAndInsets, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) + let fullscreenListWidth: CGFloat + let fullscreenListHeight: CGFloat = 84.0 + let fullscreenListTransform: CATransform3D + let fullscreenListInset: CGFloat = 14.0 + let fullscreenListUpdateSizeAndInsets: ListViewUpdateSizeAndInsets + let fullscreenListContainerFrame: CGRect + if self.isLandscape { + fullscreenListWidth = layout.size.height + fullscreenListTransform = CATransform3DIdentity + fullscreenListUpdateSizeAndInsets = ListViewUpdateSizeAndInsets(size: CGSize(width: fullscreenListHeight, height: layout.size.height), insets: UIEdgeInsets(top: fullscreenListInset, left: 0.0, bottom: fullscreenListInset, right: 0.0), duration: duration, curve: curve) + fullscreenListContainerFrame = CGRect(x: layout.size.width - min(self.effectiveBottomAreaHeight, fullscreenBottomAreaHeight) - layout.safeInsets.right - fullscreenListHeight - 4.0, y: 0.0, width: fullscreenListHeight, height: layout.size.height) + } else { + fullscreenListWidth = layout.size.width + fullscreenListTransform = CATransform3DMakeRotation(-CGFloat(CGFloat.pi / 2.0), 0.0, 0.0, 1.0) + fullscreenListUpdateSizeAndInsets = ListViewUpdateSizeAndInsets(size: CGSize(width: fullscreenListHeight, height: layout.size.width), insets: UIEdgeInsets(top: fullscreenListInset + layout.safeInsets.left, left: 0.0, bottom: fullscreenListInset + layout.safeInsets.left, right: 0.0), duration: duration, curve: curve) + fullscreenListContainerFrame = CGRect(x: 0.0, y: layout.size.height - min(bottomPanelHeight, fullscreenBottomAreaHeight + layout.intrinsicInsets.bottom) - fullscreenListHeight - 4.0, width: layout.size.width, height: fullscreenListHeight) + } - transition.updateFrame(node: self.topCornersNode, frame: CGRect(origin: CGPoint(x: sideInset, y: 63.0), size: CGSize(width: size.width - sideInset * 2.0, height: 50.0))) + transition.updateFrame(node: self.fullscreenListContainer, frame: fullscreenListContainerFrame) - let bottomPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - bottomPanelHeight), size: CGSize(width: size.width, height: bottomPanelHeight)) + self.fullscreenListNode.bounds = CGRect(x: 0.0, y: 0.0, width: fullscreenListHeight, height: fullscreenListWidth) + transition.updatePosition(node: self.fullscreenListNode, position: CGPoint(x: fullscreenListContainerFrame.width / 2.0, y: fullscreenListContainerFrame.height / 2.0)) + + self.fullscreenListNode.transform = fullscreenListTransform + self.fullscreenListNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: nil, updateSizeAndInsets: fullscreenListUpdateSizeAndInsets, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) + + if case .regular = layout.metrics.widthClass { + self.transitionContainerNode.view.mask = nil + } else { + self.transitionContainerNode.view.mask = self.transitionMaskView + } + + var childrenLayout = layout + var childrenInsets = childrenLayout.intrinsicInsets + var childrenSafeInsets = childrenLayout.safeInsets + if case .regular = layout.metrics.widthClass { + let childrenLayoutWidth: CGFloat = 375.0 + if contentLeftInset.isZero { + childrenSafeInsets.left = floorToScreenPixels((size.width - childrenLayoutWidth) / 2.0) + childrenSafeInsets.right = floorToScreenPixels((size.width - childrenLayoutWidth) / 2.0) + } else { + childrenSafeInsets.left = floorToScreenPixels((contentLeftInset - childrenLayoutWidth) / 2.0) + childrenSafeInsets.right = childrenSafeInsets.left + (size.width - contentLeftInset) + } + } else if !self.isLandscape, case .fullscreen = effectiveDisplayMode { + childrenInsets.bottom += self.effectiveBottomAreaHeight + fullscreenListHeight + 30.0 + } + childrenLayout.safeInsets = childrenSafeInsets + childrenLayout.intrinsicInsets = childrenInsets + self.controller?.presentationContext.containerLayoutUpdated(childrenLayout, transition: transition) + + var bottomPanelLeftInset = contentLeftInset + var bottomPanelWidth = size.width - contentLeftInset + if case .regular = layout.metrics.widthClass, bottomPanelLeftInset.isZero { + bottomPanelLeftInset = floorToScreenPixels((layout.size.width - contentWidth) / 2.0) + bottomPanelWidth = contentWidth + } + + var bottomPanelFrame = CGRect(origin: CGPoint(x: bottomPanelLeftInset, y: layout.size.height - bottomPanelHeight), size: CGSize(width: bottomPanelWidth, height: bottomPanelHeight)) + let bottomPanelCoverHeight = bottomAreaHeight + layout.intrinsicInsets.bottom + if self.isLandscape { + bottomPanelFrame = CGRect(origin: CGPoint(x: layout.size.width - fullscreenBottomAreaHeight - layout.safeInsets.right, y: 0.0), size: CGSize(width: fullscreenBottomAreaHeight + layout.safeInsets.right, height: layout.size.height)) + } + let bottomGradientFrame = CGRect(origin: CGPoint(x: bottomPanelLeftInset, y: layout.size.height - bottomPanelCoverHeight), size: CGSize(width: bottomPanelWidth, height: bottomGradientHeight)) + transition.updateFrame(node: self.bottomGradientNode, frame: bottomGradientFrame) transition.updateFrame(node: self.bottomPanelNode, frame: bottomPanelFrame) - let cameraButtonSize = CGSize(width: 40.0, height: 40.0) - let centralButtonSize = CGSize(width: 300.0, height: 300.0) - - let actionButtonFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - centralButtonSize.width) / 2.0), y: floorToScreenPixels((bottomAreaHeight - centralButtonSize.height) / 2.0)), size: centralButtonSize) + if let pickerView = self.pickerView { + transition.updateFrame(view: pickerView, frame: CGRect(x: 0.0, y: layout.size.height - bottomPanelHeight - 216.0, width: size.width, height: 216.0)) + } + + let timerFrame = CGRect(x: 0.0, y: layout.size.height - bottomPanelHeight - 216.0, width: size.width, height: 216.0) + transition.updateFrame(node: self.timerNode, frame: timerFrame) + self.timerNode.update(size: timerFrame.size, scheduleTime: self.callState?.scheduleTimestamp, transition: .immediate) + let scheduleTextSize = self.scheduleTextNode.updateLayout(CGSize(width: size.width - sideInset * 2.0, height: CGFloat.greatestFiniteMagnitude)) + self.scheduleTextNode.frame = CGRect(origin: CGPoint(x: floor((size.width - scheduleTextSize.width) / 2.0), y: layout.size.height - layout.intrinsicInsets.bottom - scheduleTextSize.height - 145.0), size: scheduleTextSize) + + let centralButtonSide = min(contentWidth, size.height) - 32.0 + let centralButtonSize = CGSize(width: centralButtonSide, height: centralButtonSide) + let cameraButtonSize = smallButtonSize + let sideButtonMinimalInset: CGFloat = 16.0 + let sideButtonOffset = min(42.0, floor((((contentWidth - 112.0) / 2.0) - sideButtonSize.width) / 2.0)) + let sideButtonOrigin = max(sideButtonMinimalInset, floor((contentWidth - 112.0) / 2.0) - sideButtonOffset - sideButtonSize.width) + + let smallButtons: Bool + if case .regular = layout.metrics.widthClass { + smallButtons = false + } else { + switch effectiveDisplayMode { + case .modal: + smallButtons = self.isLandscape + case .fullscreen: + smallButtons = true + } + } let actionButtonState: VoiceChatActionButton.State let actionButtonTitle: String let actionButtonSubtitle: String var actionButtonEnabled = true - if let callState = self.callState { - switch callState.networkState { - case .connecting: + if let callState = self.callState, !self.isScheduling { + if callState.scheduleTimestamp != nil { + self.ignoreConnecting = true + if callState.canManageCall { + actionButtonState = .scheduled(state: .start) + actionButtonTitle = self.presentationData.strings.VoiceChat_StartNow + actionButtonSubtitle = "" + } else { + if callState.subscribedToScheduled { + actionButtonState = .scheduled(state: .unsubscribe) + actionButtonTitle = self.presentationData.strings.VoiceChat_CancelReminder + } else { + actionButtonState = .scheduled(state: .subscribe) + actionButtonTitle = self.presentationData.strings.VoiceChat_SetReminder + } + actionButtonSubtitle = "" + } + } else { + let connected = self.ignoreConnecting || callState.networkState == .connected + if case .connected = callState.networkState { + self.ignoreConnecting = false + self.ignoreConnectingTimer?.invalidate() + self.ignoreConnectingTimer = nil + } else if self.ignoreConnecting { + if self.ignoreConnectingTimer == nil { + let timer = SwiftSignalKit.Timer(timeout: 3.0, repeat: false, completion: { [weak self] in + if let strongSelf = self { + strongSelf.ignoreConnecting = false + strongSelf.ignoreConnectingTimer?.invalidate() + strongSelf.ignoreConnectingTimer = nil + + if let (layout, navigationHeight) = strongSelf.validLayout { + strongSelf.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: .immediate) + } + } + }, queue: Queue.mainQueue()) + self.ignoreConnectingTimer = timer + timer.start() + } + } + + if connected { + if let muteState = callState.muteState, !self.pushingToTalk && !self.temporaryPushingToTalk { + if muteState.canUnmute { + actionButtonState = .active(state: .muted) + + actionButtonTitle = self.presentationData.strings.VoiceChat_Unmute + actionButtonSubtitle = "" + } else { + actionButtonState = .active(state: .cantSpeak) + + if callState.raisedHand { + actionButtonTitle = self.presentationData.strings.VoiceChat_AskedToSpeak + actionButtonSubtitle = self.presentationData.strings.VoiceChat_AskedToSpeakHelp + } else { + actionButtonTitle = self.presentationData.strings.VoiceChat_MutedByAdmin + actionButtonSubtitle = self.presentationData.strings.VoiceChat_MutedByAdminHelp + } + } + } else { + actionButtonState = .active(state: .on) + + actionButtonTitle = self.pushingToTalk ? self.presentationData.strings.VoiceChat_Live : self.presentationData.strings.VoiceChat_Mute + actionButtonSubtitle = "" + } + } else { + actionButtonState = .connecting + actionButtonTitle = self.presentationData.strings.VoiceChat_Connecting + actionButtonSubtitle = "" + actionButtonEnabled = false + } + } + } else { + if self.isScheduling { + actionButtonState = .button(text: self.scheduleButtonTitle) + actionButtonTitle = "" + actionButtonSubtitle = "" + actionButtonEnabled = true + } else { actionButtonState = .connecting actionButtonTitle = self.presentationData.strings.VoiceChat_Connecting actionButtonSubtitle = "" actionButtonEnabled = false - case .connected: - if let muteState = callState.muteState, !self.pushingToTalk { - if muteState.canUnmute { - actionButtonState = .active(state: .muted) - - actionButtonTitle = self.presentationData.strings.VoiceChat_Unmute - actionButtonSubtitle = "" - } else { - actionButtonState = .active(state: .cantSpeak) - - if callState.raisedHand { - actionButtonTitle = self.presentationData.strings.VoiceChat_AskedToSpeak - actionButtonSubtitle = self.presentationData.strings.VoiceChat_AskedToSpeakHelp - } else { - actionButtonTitle = self.presentationData.strings.VoiceChat_MutedByAdmin - actionButtonSubtitle = self.presentationData.strings.VoiceChat_MutedByAdminHelp - } - } - } else { - actionButtonState = .active(state: .on) - - actionButtonTitle = self.pushingToTalk ? self.presentationData.strings.VoiceChat_Live : self.presentationData.strings.VoiceChat_Mute - actionButtonSubtitle = "" - } } - } else { - actionButtonState = .connecting - actionButtonTitle = self.presentationData.strings.VoiceChat_Connecting - actionButtonSubtitle = "" - actionButtonEnabled = false } self.actionButton.isDisabled = !actionButtonEnabled - self.actionButton.update(size: centralButtonSize, buttonSize: CGSize(width: 112.0, height: 112.0), state: actionButtonState, title: actionButtonTitle, subtitle: actionButtonSubtitle, dark: self.isFullscreen, small: false, animated: true) + self.actionButton.update(size: centralButtonSize, buttonSize: CGSize(width: 112.0, height: 112.0), state: actionButtonState, title: actionButtonTitle, subtitle: actionButtonSubtitle, dark: self.isFullscreen, small: smallButtons, animated: true) + + let isVideoEnabled = self.callState?.isVideoEnabled ?? false + var hasCameraButton = isVideoEnabled + if let joinedVideo = self.joinedVideo { + hasCameraButton = joinedVideo + } + if !isVideoEnabled { + hasCameraButton = false + } + switch actionButtonState { + case let .active(state): + switch state { + case .cantSpeak: + hasCameraButton = false + case .on, .muted: + break + } + case .connecting: + if !self.connectedOnce { + hasCameraButton = false + } + case .scheduled, .button: + hasCameraButton = false + } + let hasVideo = hasCameraButton && self.call.hasVideo + + let upperButtonDistance: CGFloat = 12.0 + let firstButtonFrame: CGRect + let secondButtonFrame: CGRect + let thirdButtonFrame: CGRect + let forthButtonFrame: CGRect + + let leftButtonFrame: CGRect + if self.isScheduled || !hasVideo { + leftButtonFrame = CGRect(origin: CGPoint(x: sideButtonOrigin, y: floor((self.effectiveBottomAreaHeight - sideButtonSize.height) / 2.0)), size: sideButtonSize) + } else { + leftButtonFrame = CGRect(origin: CGPoint(x: sideButtonOrigin, y: floor((self.effectiveBottomAreaHeight - sideButtonSize.height - upperButtonDistance - cameraButtonSize.height) / 2.0) + upperButtonDistance + cameraButtonSize.height), size: sideButtonSize) + } + let rightButtonFrame = CGRect(origin: CGPoint(x: contentWidth - sideButtonOrigin - sideButtonSize.width, y: floor((self.effectiveBottomAreaHeight - sideButtonSize.height) / 2.0)), size: sideButtonSize) + var centerButtonFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - centralButtonSize.width) / 2.0), y: floor((self.effectiveBottomAreaHeight - centralButtonSize.height) / 2.0) - 3.0), size: centralButtonSize) + + if case .regular = layout.metrics.widthClass { + centerButtonFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((contentWidth - centralButtonSize.width) / 2.0), y: floor((self.effectiveBottomAreaHeight - centralButtonSize.height) / 2.0) - 3.0), size: centralButtonSize) + + if hasCameraButton { + firstButtonFrame = CGRect(origin: CGPoint(x: floor(leftButtonFrame.midX - cameraButtonSize.width / 2.0), y: leftButtonFrame.minY - upperButtonDistance - cameraButtonSize.height), size: cameraButtonSize) + } else { + firstButtonFrame = CGRect(origin: CGPoint(x: leftButtonFrame.center.x - cameraButtonSize.width / 2.0, y: leftButtonFrame.center.y - cameraButtonSize.height / 2.0), size: cameraButtonSize) + } + secondButtonFrame = leftButtonFrame + thirdButtonFrame = centerButtonFrame + forthButtonFrame = rightButtonFrame + } else { + switch effectiveDisplayMode { + case .modal: + if self.isLandscape { + let sideInset: CGFloat + let buttonsCount: Int + if hasVideo { + sideInset = 26.0 + buttonsCount = 4 + } else { + sideInset = 42.0 + buttonsCount = 3 + } + let spacing = floor((layout.size.height - sideInset * 2.0 - sideButtonSize.height * CGFloat(buttonsCount)) / (CGFloat(buttonsCount - 1))) + let x = floor((fullscreenBottomAreaHeight - sideButtonSize.width) / 2.0) + forthButtonFrame = CGRect(origin: CGPoint(x: x, y: sideInset), size: sideButtonSize) + let thirdButtonPreFrame = CGRect(origin: CGPoint(x: x, y: sideInset + sideButtonSize.height + spacing), size: sideButtonSize) + thirdButtonFrame = CGRect(origin: CGPoint(x: floor(thirdButtonPreFrame.midX - centralButtonSize.width / 2.0), y: floor(thirdButtonPreFrame.midY - centralButtonSize.height / 2.0)), size: centralButtonSize) + secondButtonFrame = CGRect(origin: CGPoint(x: x, y: thirdButtonPreFrame.maxY + spacing), size: sideButtonSize) + if hasCameraButton { + firstButtonFrame = CGRect(origin: CGPoint(x: x, y: layout.size.height - sideInset - sideButtonSize.height), size: sideButtonSize) + } else { + firstButtonFrame = secondButtonFrame + } + } else { + if hasCameraButton { + firstButtonFrame = CGRect(origin: CGPoint(x: floor(leftButtonFrame.midX - cameraButtonSize.width / 2.0), y: leftButtonFrame.minY - upperButtonDistance - cameraButtonSize.height), size: cameraButtonSize) + } else { + firstButtonFrame = CGRect(origin: CGPoint(x: leftButtonFrame.center.x - cameraButtonSize.width / 2.0, y: leftButtonFrame.center.y - cameraButtonSize.height / 2.0), size: cameraButtonSize) + } + secondButtonFrame = leftButtonFrame + thirdButtonFrame = centerButtonFrame + forthButtonFrame = rightButtonFrame + } + case let .fullscreen(controlsHidden): + if self.isLandscape { + let sideInset: CGFloat + let buttonsCount: Int + if hasVideo { + sideInset = 26.0 + buttonsCount = 4 + } else { + sideInset = 42.0 + buttonsCount = 3 + } + let spacing = floor((layout.size.height - sideInset * 2.0 - sideButtonSize.height * CGFloat(buttonsCount)) / (CGFloat(buttonsCount - 1))) + let x = controlsHidden ? fullscreenBottomAreaHeight + layout.safeInsets.right + 30.0 : floor((fullscreenBottomAreaHeight - sideButtonSize.width) / 2.0) + forthButtonFrame = CGRect(origin: CGPoint(x: x, y: sideInset), size: sideButtonSize) + let thirdButtonPreFrame = CGRect(origin: CGPoint(x: x, y: sideInset + sideButtonSize.height + spacing), size: sideButtonSize) + thirdButtonFrame = CGRect(origin: CGPoint(x: floor(thirdButtonPreFrame.midX - centralButtonSize.width / 2.0), y: floor(thirdButtonPreFrame.midY - centralButtonSize.height / 2.0)), size: centralButtonSize) + secondButtonFrame = CGRect(origin: CGPoint(x: x, y: thirdButtonPreFrame.maxY + spacing), size: sideButtonSize) + if hasVideo { + firstButtonFrame = CGRect(origin: CGPoint(x: x, y: layout.size.height - sideInset - sideButtonSize.height), size: sideButtonSize) + } else { + firstButtonFrame = secondButtonFrame + } + } else { + let sideInset: CGFloat + let buttonsCount: Int + if hasVideo { + sideInset = 26.0 + buttonsCount = 4 + } else { + sideInset = 42.0 + buttonsCount = 3 + } + let spacing = floor((layout.size.width - sideInset * 2.0 - sideButtonSize.width * CGFloat(buttonsCount)) / (CGFloat(buttonsCount - 1))) + let y = controlsHidden ? self.effectiveBottomAreaHeight + layout.intrinsicInsets.bottom + 30.0: floor((self.effectiveBottomAreaHeight - sideButtonSize.height) / 2.0) + if hasVideo { + firstButtonFrame = CGRect(origin: CGPoint(x: sideInset, y: y), size: sideButtonSize) + secondButtonFrame = CGRect(origin: CGPoint(x: firstButtonFrame.maxX + spacing, y: y), size: sideButtonSize) + } else { + firstButtonFrame = CGRect(origin: CGPoint(x: sideInset, y: y), size: sideButtonSize) + secondButtonFrame = firstButtonFrame + } + let thirdButtonPreFrame = CGRect(origin: CGPoint(x: secondButtonFrame.maxX + spacing, y: y), size: sideButtonSize) + thirdButtonFrame = CGRect(origin: CGPoint(x: floor(thirdButtonPreFrame.midX - centralButtonSize.width / 2.0), y: floor(thirdButtonPreFrame.midY - centralButtonSize.height / 2.0)), size: centralButtonSize) + forthButtonFrame = CGRect(origin: CGPoint(x: thirdButtonPreFrame.maxX + spacing, y: y), size: sideButtonSize) + } + } + } + + let buttonHeight = self.scheduleCancelButton.updateLayout(width: size.width - 32.0, transition: .immediate) + self.scheduleCancelButton.frame = CGRect(x: 16.0, y: 137.0, width: size.width - 32.0, height: buttonHeight) if self.actionButton.supernode === self.bottomPanelNode { - transition.updateFrame(node: self.actionButton, frame: actionButtonFrame) + transition.updateFrame(node: self.actionButton, frame: thirdButtonFrame, completion: transition.isAnimated ? { [weak self] _ in + self?.animatingExpansion = false + } : nil) } - self.updateButtons(animated: !isFirstTime) + self.cameraButton.isUserInteractionEnabled = hasCameraButton - /*var currentVideoOrigin = CGPoint(x: 4.0, y: (layout.statusBarHeight ?? 0.0) + 4.0) - for (_, _, videoNode) in self.videoNodes { - let videoSize = CGSize(width: 300.0, height: 500.0) - if currentVideoOrigin.x + videoSize.width > layout.size.width { - currentVideoOrigin.x = 0.0 - currentVideoOrigin.y += videoSize.height - } - - videoNode.frame = CGRect(origin: currentVideoOrigin, size: videoSize) - videoNode.updateLayout(size: videoSize, transition: .immediate) - if videoNode.supernode == nil { - self.contentContainer.addSubnode(videoNode) - } - - currentVideoOrigin.x += videoSize.width + 4.0 - }*/ - - let sideButtonMinimalInset: CGFloat = 16.0 - let sideButtonOffset = min(42.0, floor((((size.width - 112.0) / 2.0) - sideButtonSize.width) / 2.0)) - let sideButtonOrigin = max(sideButtonMinimalInset, floor((size.width - 112.0) / 2.0) - sideButtonOffset - sideButtonSize.width) - - if self.audioOutputNode.supernode === self.bottomPanelNode { - if true { - let audioOutputFrame = CGRect(origin: CGPoint(x: sideButtonOrigin, y: floor((bottomAreaHeight - sideButtonSize.height) / 2.0)), size: sideButtonSize) - transition.updateFrame(node: self.audioOutputNode, frame: audioOutputFrame) + var buttonsTransition: ContainedViewLayoutTransition = .immediate + if !isFirstTime { + if case .animated(_, .spring) = transition { + buttonsTransition = transition } else { - let cameraButtonDistance: CGFloat = 4.0 - - let audioOutputFrame = CGRect(origin: CGPoint(x: sideButtonOrigin, y: floor((bottomAreaHeight - sideButtonSize.height - cameraButtonDistance - cameraButtonSize.height) / 2.0) + cameraButtonDistance + cameraButtonSize.height), size: sideButtonSize) - - transition.updateFrame(node: self.audioOutputNode, frame: audioOutputFrame) - - transition.updateFrame(node: self.cameraButtonNode, frame: CGRect(origin: CGPoint(x: floor(audioOutputFrame.midX - cameraButtonSize.width / 2.0), y: audioOutputFrame.minY - cameraButtonDistance - cameraButtonSize.height), size: cameraButtonSize)) + buttonsTransition = .animated(duration: 0.3, curve: .linear) } + } + self.updateButtons(transition: buttonsTransition) + + if self.audioButton.supernode === self.bottomPanelNode { + transition.updateAlpha(node: self.cameraButton, alpha: hasCameraButton ? 1.0 : 0.0) + transition.updateFrameAsPositionAndBounds(node: self.switchCameraButton, frame: firstButtonFrame) - transition.updateFrame(node: self.leaveNode, frame: CGRect(origin: CGPoint(x: size.width - sideButtonOrigin - sideButtonSize.width, y: floor((bottomAreaHeight - sideButtonSize.height) / 2.0)), size: sideButtonSize)) + if !self.animatingButtonsSwap || transition.isAnimated { + transition.updateFrameAsPositionAndBounds(node: self.audioButton, frame: secondButtonFrame, completion: { [weak self] _ in + self?.animatingButtonsSwap = false + }) + transition.updateFrameAsPositionAndBounds(node: self.cameraButton, frame: secondButtonFrame) + } + transition.updateFrameAsPositionAndBounds(node: self.leaveButton, frame: forthButtonFrame) } if isFirstTime { while !self.enqueuedTransitions.isEmpty { self.dequeueTransition() } + while !self.enqueuedFullscreenTransitions.isEmpty { + self.dequeueFullscreenTransition() + } } } + private var appIsActive = true { + didSet { + if self.appIsActive != oldValue { + self.updateVisibility() + self.updateRequestedVideoChannels() + } + } + } + private var visibility = false { + didSet { + if self.visibility != oldValue { + self.updateVisibility() + self.updateRequestedVideoChannels() + } + } + } + + private func updateVisibility() { + let visible = self.appIsActive && self.visibility + if self.tileGridNode.isHidden { + self.tileGridNode.visibility = false + } else { + self.tileGridNode.visibility = visible + } + self.mainStageNode.visibility = visible + self.listNode.forEachItemNode { itemNode in + if let itemNode = itemNode as? VoiceChatTilesGridItemNode { + itemNode.gridVisibility = visible + } + } + self.fullscreenListNode.forEachItemNode { itemNode in + if let itemNode = itemNode as? VoiceChatFullscreenParticipantItemNode { + itemNode.gridVisibility = visible + } + } + + self.videoRenderingContext.updateVisibility(isVisible: visible) + } + func animateIn() { guard let (layout, navigationHeight) = self.validLayout else { return } - let transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring) - let topPanelFrame = self.topPanelNode.view.convert(self.topPanelNode.bounds, to: self.view) - + self.visibility = true + + self.updateDecorationsLayout(transition: .immediate) + + self.animatingAppearance = true + let initialBounds = self.contentContainer.bounds + let topPanelFrame = self.topPanelNode.view.convert(self.topPanelNode.bounds, to: self.view) self.contentContainer.bounds = initialBounds.offsetBy(dx: 0.0, dy: -(layout.size.height - topPanelFrame.minY)) self.contentContainer.isHidden = false + + let transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring) transition.animateView({ self.contentContainer.view.bounds = initialBounds }, completion: { _ in + self.animatingAppearance = false if self.actionButton.supernode !== self.bottomPanelNode { self.actionButton.ignoreHierarchyChanges = true - self.audioOutputNode.isHidden = false - self.cameraButtonNode.isHidden = false - self.leaveNode.isHidden = false - self.audioOutputNode.layer.removeAllAnimations() - self.cameraButtonNode.layer.removeAllAnimations() - self.leaveNode.layer.removeAllAnimations() - self.bottomPanelNode.addSubnode(self.audioOutputNode) - //self.bottomPanelNode.addSubnode(self.cameraButtonNode) - self.bottomPanelNode.addSubnode(self.leaveNode) + self.audioButton.isHidden = false + self.cameraButton.isHidden = false + self.leaveButton.isHidden = false + self.audioButton.layer.removeAllAnimations() + self.cameraButton.layer.removeAllAnimations() + self.leaveButton.layer.removeAllAnimations() + self.bottomPanelNode.addSubnode(self.cameraButton) + self.bottomPanelNode.addSubnode(self.audioButton) + self.bottomPanelNode.addSubnode(self.leaveButton) self.bottomPanelNode.addSubnode(self.actionButton) - self.containerLayoutUpdated(layout, navigationHeight :navigationHeight, transition: .immediate) + self.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: .immediate) self.actionButton.ignoreHierarchyChanges = false } @@ -2915,6 +4672,8 @@ public final class VoiceChatController: ViewController { var bounds = strongSelf.contentContainer.bounds bounds.origin.y = 0.0 strongSelf.contentContainer.bounds = bounds + + strongSelf.visibility = false } completion?() } @@ -2938,14 +4697,36 @@ public final class VoiceChatController: ViewController { } } - private var topInset: CGFloat? - private var isFirstTime = true + private func enqueueFullscreenTransition(_ transition: ListTransition) { + self.enqueuedFullscreenTransitions.append(transition) + + if let _ = self.validLayout { + while !self.enqueuedFullscreenTransitions.isEmpty { + self.dequeueFullscreenTransition() + } + } + } + private func dequeueTransition() { guard let (layout, _) = self.validLayout, let transition = self.enqueuedTransitions.first else { return } self.enqueuedTransitions.remove(at: 0) + if let callState = self.callState { + if callState.scheduleTimestamp != nil && self.listNode.alpha > 0.0 { + self.timerNode.isHidden = false + self.cameraButton.alpha = 0.0 + self.cameraButton.isUserInteractionEnabled = false + self.listNode.alpha = 0.0 + self.listNode.isUserInteractionEnabled = false + self.backgroundNode.backgroundColor = panelBackgroundColor + self.updateDecorationsColors() + } else if callState.scheduleTimestamp == nil && !self.isScheduling && self.listNode.alpha == 0.0 { + self.transitionToCall() + } + } + var options = ListViewDeleteAndInsertOptions() let isFirstTime = self.isFirstTime if isFirstTime { @@ -2954,140 +4735,115 @@ public final class VoiceChatController: ViewController { if transition.crossFade { options.insert(.AnimateCrossfade) } - if transition.animated && self.animation == nil { + if transition.animated { options.insert(.AnimateInsertion) } } options.insert(.LowLatency) options.insert(.PreferSynchronousResourceLoading) - - var itemsHeight: CGFloat = 0.0 - var itemsCount = transition.count - if transition.canInvite { - itemsHeight += 46.0 - itemsCount -= 1 - } - itemsHeight += CGFloat(itemsCount) * 56.0 - - let layoutTopInset: CGFloat = max(layout.statusBarHeight ?? 0.0, layout.safeInsets.top) - - let sideInset: CGFloat = 16.0 - var insets = UIEdgeInsets() - insets.left = layout.safeInsets.left + sideInset - insets.right = layout.safeInsets.right + sideInset - + var size = layout.size if case .regular = layout.metrics.widthClass { size.width = floor(min(size.width, size.height) * 0.5) } - let bottomPanelHeight = bottomAreaHeight + layout.intrinsicInsets.bottom - let listTopInset = layoutTopInset + 63.0 - let listSize = CGSize(width: size.width, height: layout.size.height - listTopInset - bottomPanelHeight) + let bottomPanelHeight = self.isLandscape ? layout.intrinsicInsets.bottom : bottomAreaHeight + layout.intrinsicInsets.bottom + let layoutTopInset: CGFloat = max(layout.statusBarHeight ?? 0.0, layout.safeInsets.top) + let listTopInset = layoutTopInset + topPanelHeight + let listSize = CGSize(width: size.width, height: layout.size.height - listTopInset - bottomPanelHeight + bottomGradientHeight) - self.topInset = max(0.0, max(listSize.height - itemsHeight, listSize.height - 46.0 - floor(56.0 * 3.5))) + self.topInset = listSize.height - 46.0 - floor(56.0 * 3.5) - bottomGradientHeight - let targetY = listTopInset + (self.topInset ?? listSize.height) - - if isFirstTime { - var frame = self.listNode.frame - frame.origin.y = targetY - self.listNode.frame = frame - } else if !self.isExpanded { - if self.listNode.frame.minY != targetY && !self.animatingExpansion && self.panGestureArguments == nil { - self.animation = ListViewAnimation(from: self.listNode.frame.minY, to: targetY, duration: 0.4, curve: listViewAnimationCurveSystem, beginAt: CACurrentMediaTime(), update: { [weak self] _, currentValue in - if let strongSelf = self { - var frame = strongSelf.listNode.frame - frame.origin.y = currentValue - strongSelf.listNode.frame = frame - strongSelf.updateFloatingHeaderOffset(offset: strongSelf.currentContentOffset ?? 0.0, transition: .immediate) - } - }) - self.updateAnimation() - } + if transition.animated { + self.animatingInsertion = true } - self.listNode.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, scrollToItem: nil, updateSizeAndInsets: nil, updateOpaqueState: nil, completion: { [weak self] _ in guard let strongSelf = self else { return } + if isFirstTime { + strongSelf.updateDecorationsLayout(transition: .immediate) + } else if strongSelf.animatingInsertion { + strongSelf.updateDecorationsLayout(transition: .animated(duration: 0.2, curve: .easeInOut)) + } + strongSelf.animatingInsertion = false if !strongSelf.didSetContentsReady { strongSelf.didSetContentsReady = true strongSelf.controller?.contentsReady.set(true) } + strongSelf.updateVisibility() + }) + } + + private func dequeueFullscreenTransition() { + guard let _ = self.validLayout, let transition = self.enqueuedFullscreenTransitions.first else { + return + } + self.enqueuedFullscreenTransitions.remove(at: 0) + + var options = ListViewDeleteAndInsertOptions() + let isFirstTime = self.isFirstTime + if !isFirstTime { + if transition.animated { + options.insert(.AnimateInsertion) + } + } + + self.fullscreenListNode.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, scrollToItem: nil, updateSizeAndInsets: nil, updateOpaqueState: nil, completion: { _ in }) } - - private var animator: ConstantDisplayLinkAnimator? - private var animation: ListViewAnimation? - private func updateAnimation() { - var animate = false - let timestamp = CACurrentMediaTime() - - if let animation = self.animation { - animation.applyAt(timestamp) - - if animation.completeAt(timestamp) { - self.animation = nil - } else { - animate = true - } - } - - if animate { - let animator: ConstantDisplayLinkAnimator - if let current = self.animator { - animator = current - } else { - animator = ConstantDisplayLinkAnimator(update: { [weak self] in - self?.updateAnimation() - }) - self.animator = animator - } - animator.isPaused = false - } else { - self.animator?.isPaused = true - } + private func updateMembers(maybeUpdateVideo: Bool = true, force: Bool = false) { + self.updateMembers(muteState: self.effectiveMuteState, callMembers: self.currentCallMembers ?? ([], nil), invitedPeers: self.currentInvitedPeers ?? [], speakingPeers: self.currentSpeakingPeers ?? Set(), maybeUpdateVideo: maybeUpdateVideo, force: force) } - private func updateMembers(muteState: GroupCallParticipantsContext.Participant.MuteState?, callMembers: ([GroupCallParticipantsContext.Participant], String?), invitedPeers: [Peer], speakingPeers: Set) { + private func updateMembers(muteState: GroupCallParticipantsContext.Participant.MuteState?, callMembers: ([GroupCallParticipantsContext.Participant], String?), invitedPeers: [Peer], speakingPeers: Set, maybeUpdateVideo: Bool = true, force: Bool = false) { var disableAnimation = false if self.currentCallMembers?.1 != callMembers.1 { disableAnimation = true } + let speakingPeersUpdated = self.currentSpeakingPeers != speakingPeers self.currentCallMembers = callMembers self.currentSpeakingPeers = speakingPeers self.currentInvitedPeers = invitedPeers - let previousEntries = self.currentEntries var entries: [ListEntry] = [] - + var fullscreenEntries: [ListEntry] = [] var index: Int32 = 0 - + var fullscreenIndex: Int32 = 0 var processedPeerIds = Set() + var processedFullscreenPeerIds = Set() + + var peerIdToCameraEndpointId: [PeerId: String] = [:] + var peerIdToEndpointId: [PeerId: String] = [:] + + var requestedVideoChannels: [PresentationGroupCallRequestedVideo] = [] + var gridTileItems: [VoiceChatTileItem] = [] + var tileItems: [VoiceChatTileItem] = [] + var gridTileByVideoEndpoint: [String: VoiceChatTileItem] = [:] + var tileByVideoEndpoint: [String: VoiceChatTileItem] = [:] + var entryByPeerId: [PeerId: VoiceChatPeerEntry] = [:] + var latestWideVideo: String? = nil - var canInvite = true - if let peer = self.peer as? TelegramChannel { - if peer.flags.contains(.isGigagroup) || (peer.addressName?.isEmpty ?? true) { - if peer.flags.contains(.isCreator) || peer.adminRights != nil { - } else { - canInvite = false - } - } - } - if canInvite { - entries.append(.invite(self.presentationData.theme, self.presentationData.strings, self.presentationData.strings.VoiceChat_InviteMember)) + var isTablet = false + var displayPanelVideos = false + if let (layout, _) = self.validLayout, case .regular = layout.metrics.widthClass { + isTablet = true + displayPanelVideos = self.displayPanelVideos } + var joinedVideo = self.joinedVideo ?? true + + var myEntry: VoiceChatPeerEntry? + var mainEntry: VoiceChatPeerEntry? for member in callMembers.0 { if processedPeerIds.contains(member.peer.id) { continue } processedPeerIds.insert(member.peer.id) - let memberState: PeerEntry.State + let memberState: VoiceChatPeerEntry.State var memberMuteState: GroupCallParticipantsContext.Participant.MuteState? if member.hasRaiseHand && !(member.muteState?.canUnmute ?? false) { memberState = .raisedHand @@ -3104,8 +4860,7 @@ public final class VoiceChatController: ViewController { var displayedRaisedHands = strongSelf.displayedRaisedHands displayedRaisedHands.remove(member.peer.id) strongSelf.displayedRaisedHands = displayedRaisedHands - - strongSelf.updateMembers(muteState: strongSelf.effectiveMuteState, callMembers: strongSelf.currentCallMembers ?? ([], nil), invitedPeers: strongSelf.currentInvitedPeers ?? [], speakingPeers: strongSelf.currentSpeakingPeers ?? Set()) + strongSelf.updateMembers() } }) } @@ -3128,53 +4883,279 @@ public final class VoiceChatController: ViewController { } } - entries.append(.peer(PeerEntry( - peer: member.peer, + var memberPeer = member.peer + if member.peer.id == self.callState?.myPeerId { + joinedVideo = member.joinedVideo + if let user = memberPeer as? TelegramUser, let photo = self.currentUpdatingAvatar { + memberPeer = user.withUpdatedPhoto([photo]) + } + } + + if let videoEndpointId = member.videoEndpointId { + peerIdToCameraEndpointId[member.peer.id] = videoEndpointId + } + if let anyEndpointId = member.presentationEndpointId ?? member.videoEndpointId { + peerIdToEndpointId[member.peer.id] = anyEndpointId + } + + let peerEntry = VoiceChatPeerEntry( + peer: memberPeer, about: member.about, isMyPeer: self.callState?.myPeerId == member.peer.id, - ssrc: member.ssrc, - presence: nil, - activityTimestamp: Int32.max - 1 - index, + videoEndpointId: member.videoEndpointId, + videoPaused: member.videoDescription?.isPaused ?? false, + presentationEndpointId: member.presentationEndpointId, + presentationPaused: member.presentationDescription?.isPaused ?? false, + effectiveSpeakerVideoEndpointId: self.effectiveSpeaker?.1, state: memberState, muteState: memberMuteState, canManageCall: self.callState?.canManageCall ?? false, volume: member.volume, - raisedHand: member.raiseHandRating != nil, - displayRaisedHandStatus: self.displayedRaisedHands.contains(member.peer.id) - ))) + raisedHand: member.hasRaiseHand, + displayRaisedHandStatus: self.displayedRaisedHands.contains(member.peer.id), + active: memberPeer.id == self.effectiveSpeaker?.0, + isLandscape: self.isLandscape + ) + if peerEntry.isMyPeer { + myEntry = peerEntry + } + if peerEntry.active { + mainEntry = peerEntry + } + entryByPeerId[peerEntry.peer.id] = peerEntry + + var isTile = false + if let interaction = self.itemInteraction { + if let videoEndpointId = member.presentationEndpointId { + if !self.videoOrder.contains(videoEndpointId) { + if peerEntry.isMyPeer { + self.videoOrder.insert(videoEndpointId, at: 0) + } else { + self.videoOrder.append(videoEndpointId) + } + } + if isTablet { + if let tileItem = ListEntry.peer(peerEntry, 0).tileItem(context: self.context, presentationData: self.presentationData, interaction: interaction, isTablet: isTablet, videoEndpointId: videoEndpointId, videoReady: self.readyVideoEndpointIds.contains(videoEndpointId), videoTimeouted: self.timeoutedEndpointIds.contains(videoEndpointId), videoIsPaused: member.presentationDescription?.isPaused ?? false, showAsPresentation: peerIdToCameraEndpointId[peerEntry.peer.id] != nil, secondary: false) { + isTile = true + gridTileByVideoEndpoint[videoEndpointId] = tileItem + } + } + if let tileItem = ListEntry.peer(peerEntry, 0).tileItem(context: self.context, presentationData: self.presentationData, interaction: interaction, isTablet: isTablet, videoEndpointId: videoEndpointId, videoReady: self.readyVideoEndpointIds.contains(videoEndpointId), videoTimeouted: self.timeoutedEndpointIds.contains(videoEndpointId), videoIsPaused: member.presentationDescription?.isPaused ?? false, showAsPresentation: peerIdToCameraEndpointId[peerEntry.peer.id] != nil, secondary: displayPanelVideos) { + isTile = true + tileByVideoEndpoint[videoEndpointId] = tileItem + } + if self.wideVideoNodes.contains(videoEndpointId) { + latestWideVideo = videoEndpointId + } + } + if let videoEndpointId = member.videoEndpointId { + if !self.videoOrder.contains(videoEndpointId) { + if peerEntry.isMyPeer { + self.videoOrder.insert(videoEndpointId, at: 0) + } else { + self.videoOrder.append(videoEndpointId) + } + } + if isTablet { + if let tileItem = ListEntry.peer(peerEntry, 0).tileItem(context: self.context, presentationData: self.presentationData, interaction: interaction, isTablet: isTablet, videoEndpointId: videoEndpointId, videoReady: self.readyVideoEndpointIds.contains(videoEndpointId), videoTimeouted: self.timeoutedEndpointIds.contains(videoEndpointId), videoIsPaused: member.videoDescription?.isPaused ?? false, showAsPresentation: false, secondary: false) { + isTile = true + gridTileByVideoEndpoint[videoEndpointId] = tileItem + } + } + if let tileItem = ListEntry.peer(peerEntry, 0).tileItem(context: self.context, presentationData: self.presentationData, interaction: interaction, isTablet: isTablet, videoEndpointId: videoEndpointId, videoReady: self.readyVideoEndpointIds.contains(videoEndpointId), videoTimeouted: self.timeoutedEndpointIds.contains(videoEndpointId), videoIsPaused: member.videoDescription?.isPaused ?? false, showAsPresentation: false, secondary: displayPanelVideos) { + isTile = true + tileByVideoEndpoint[videoEndpointId] = tileItem + } + if self.wideVideoNodes.contains(videoEndpointId) { + latestWideVideo = videoEndpointId + } + } + } + + if !isTile || isTablet || !joinedVideo { + entries.append(.peer(peerEntry, index)) + } + index += 1 + + if self.callState?.networkState == .connecting { + } else { + if var videoChannel = member.requestedVideoChannel(minQuality: .thumbnail, maxQuality: .medium) { + if self.effectiveSpeaker?.1 == videoChannel.endpointId { + videoChannel.maxQuality = .full + } + requestedVideoChannels.append(videoChannel) + } + if member.peer.id != self.callState?.myPeerId { + if var presentationChannel = member.requestedPresentationVideoChannel(minQuality: .thumbnail, maxQuality: .thumbnail) { + if self.effectiveSpeaker?.1 == presentationChannel.endpointId { + presentationChannel.minQuality = .full + presentationChannel.maxQuality = .full + } + requestedVideoChannels.append(presentationChannel) + } + } + } } + var temporaryList: [String] = [] + for tileVideoEndpoint in self.videoOrder { + if let _ = tileByVideoEndpoint[tileVideoEndpoint] { + temporaryList.append(tileVideoEndpoint) + } + } + + if (tileByVideoEndpoint.count % 2) != 0, let last = temporaryList.last, !self.wideVideoNodes.contains(last), let latestWide = latestWideVideo { + self.videoOrder.removeAll(where: { $0 == latestWide }) + self.videoOrder.append(latestWide) + } + + for tileVideoEndpoint in self.videoOrder { + if let tileItem = gridTileByVideoEndpoint[tileVideoEndpoint] { + gridTileItems.append(tileItem) + } + if let tileItem = tileByVideoEndpoint[tileVideoEndpoint] { + if displayPanelVideos && tileItem.peer.id == self.effectiveSpeaker?.0 { + } else { + tileItems.append(tileItem) + } + if let fullscreenEntry = entryByPeerId[tileItem.peer.id] { + if processedFullscreenPeerIds.contains(tileItem.peer.id) { + continue + } + fullscreenEntries.append(.peer(fullscreenEntry, fullscreenIndex)) + processedFullscreenPeerIds.insert(fullscreenEntry.peer.id) + fullscreenIndex += 1 + } + } + } + + self.joinedVideo = joinedVideo + + if !joinedVideo && (!tileItems.isEmpty || !gridTileItems.isEmpty), let peer = self.peer { + tileItems.removeAll() + gridTileItems.removeAll() + + let configuration = self.configuration ?? VoiceChatConfiguration.defaultValue + tileItems.append(VoiceChatTileItem(account: self.context.account, peer: peer, videoEndpointId: "", videoReady: false, videoTimeouted: true, isVideoLimit: true, videoLimit: configuration.videoParticipantsMaxCount, isPaused: false, isOwnScreencast: false, strings: self.presentationData.strings, nameDisplayOrder: self.presentationData.nameDisplayOrder, speaking: false, secondary: false, isTablet: false, icon: .none, text: .none, additionalText: nil, action: {}, contextAction: nil, getVideo: { _ in return nil }, getAudioLevel: nil)) + } + + for member in callMembers.0 { + if processedFullscreenPeerIds.contains(member.peer.id) { + continue + } + processedFullscreenPeerIds.insert(member.peer.id) + if let peerEntry = entryByPeerId[member.peer.id] { + fullscreenEntries.append(.peer(peerEntry, fullscreenIndex)) + fullscreenIndex += 1 + } + } + for peer in invitedPeers { if processedPeerIds.contains(peer.id) { continue } processedPeerIds.insert(peer.id) - entries.append(.peer(PeerEntry( + entries.append(.peer(VoiceChatPeerEntry( peer: peer, about: nil, isMyPeer: false, - ssrc: nil, - presence: nil, - activityTimestamp: Int32.max - 1 - index, + videoEndpointId: nil, + videoPaused: false, + presentationEndpointId: nil, + presentationPaused: false, + effectiveSpeakerVideoEndpointId: nil, state: .invited, muteState: nil, canManageCall: false, volume: nil, raisedHand: false, - displayRaisedHandStatus: false - ))) + displayRaisedHandStatus: false, + active: false, + isLandscape: false + ), index)) index += 1 } + self.requestedVideoChannels = requestedVideoChannels + + var myVideoUpdated = false + if let previousMyEntry = self.myEntry, let myEntry = myEntry, previousMyEntry.effectiveVideoEndpointId == nil && myEntry.effectiveVideoEndpointId != nil && self.currentForcedSpeaker == nil { + self.currentDominantSpeaker = (myEntry.peer.id, myEntry.effectiveVideoEndpointId, CACurrentMediaTime()) + myVideoUpdated = true + } + self.myEntry = myEntry + + guard self.didSetDataReady && (force || (!self.isPanning && !self.animatingExpansion && !self.animatingMainStage)) else { + return + } + + let previousMainEntry = self.mainEntry + self.mainEntry = mainEntry + if let mainEntry = mainEntry { + self.mainStageNode.update(peerEntry: mainEntry, pinned: self.currentForcedSpeaker != nil) + + if let previousMainEntry = previousMainEntry, maybeUpdateVideo { + if previousMainEntry.effectiveVideoEndpointId != mainEntry.effectiveVideoEndpointId || previousMainEntry.videoPaused != mainEntry.videoPaused || myVideoUpdated { + self.updateMainVideo(waitForFullSize: true, entries: fullscreenEntries, force: true) + return + } + } + } else if self.effectiveSpeaker != nil, !fullscreenEntries.isEmpty { + self.updateMainVideo(waitForFullSize: true, entries: fullscreenEntries, force: true) + return + } + + self.updateRequestedVideoChannels() + + self.peerIdToEndpointId = peerIdToEndpointId + + var updateLayout = false + var animatingLayout = false + if self.currentTileItems.isEmpty != gridTileItems.isEmpty { + animatingLayout = true + updateLayout = true + } + if isTablet { + updateLayout = true + self.currentTileItems = gridTileItems + if displayPanelVideos && !tileItems.isEmpty { + entries.insert(.tiles(tileItems, .pairs), at: 0) + } + } else { + if !tileItems.isEmpty { + entries.insert(.tiles(tileItems, .pairs), at: 0) + } + } + + var canInvite = true + var inviteIsLink = false + if let peer = self.peer as? TelegramChannel { + if peer.flags.contains(.isGigagroup) { + if peer.flags.contains(.isCreator) || peer.adminRights != nil { + } else { + canInvite = false + } + } + if case .broadcast = peer.info, !(peer.addressName?.isEmpty ?? true) { + inviteIsLink = true + } + } + if canInvite { + entries.append(.invite(self.presentationData.theme, self.presentationData.strings, inviteIsLink ? self.presentationData.strings.VoiceChat_Share : self.presentationData.strings.VoiceChat_InviteMember, inviteIsLink)) + } + + let previousEntries = self.currentEntries + let previousFullscreenEntries = self.currentFullscreenEntries self.currentEntries = entries + self.currentFullscreenEntries = fullscreenEntries if previousEntries.count == entries.count { var allEqual = true for i in 0 ..< previousEntries.count { if previousEntries[i].stableId != entries[i].stableId { - if case let .peer(lhsPeer) = previousEntries[i], case let .peer(rhsPeer) = entries[i] { + if case let .peer(lhsPeer, _) = previousEntries[i], case let .peer(rhsPeer, _) = entries[i] { if lhsPeer.isMyPeer != rhsPeer.isMyPeer { allEqual = false break @@ -3188,17 +5169,322 @@ public final class VoiceChatController: ViewController { if allEqual { disableAnimation = true } + } else if abs(previousEntries.count - entries.count) > 10 { + disableAnimation = true + } + + let presentationData = self.presentationData.withUpdated(theme: self.darkTheme) + let transition = self.preparedTransition(from: previousEntries, to: entries, isLoading: false, isEmpty: false, canInvite: canInvite, crossFade: false, animated: !disableAnimation, context: self.context, presentationData: presentationData, interaction: self.itemInteraction!) + self.enqueueTransition(transition) + + let fullscreenTransition = self.preparedFullscreenTransition(from: previousFullscreenEntries, to: fullscreenEntries, isLoading: false, isEmpty: false, canInvite: canInvite, crossFade: false, animated: true, context: self.context, presentationData: presentationData, interaction: self.itemInteraction!) + if !isTablet { + self.enqueueFullscreenTransition(fullscreenTransition) } - let presentationData = self.presentationData.withUpdated(theme: self.darkTheme) - let transition = preparedTransition(from: previousEntries, to: entries, isLoading: false, isEmpty: false, canInvite: canInvite, crossFade: false, animated: !disableAnimation, context: self.context, presentationData: presentationData, interaction: self.itemInteraction!) - self.enqueueTransition(transition) + if speakingPeersUpdated { + var speakingPeers = speakingPeers + var updatedSpeakers: [PeerId] = [] + for peerId in self.currentSpeakers { + if speakingPeers.contains(peerId) { + updatedSpeakers.append(peerId) + speakingPeers.remove(peerId) + } + } + + var currentSpeakingSubtitle = "" + for peerId in Array(speakingPeers) { + updatedSpeakers.append(peerId) + if let peer = entryByPeerId[peerId]?.peer { + let displayName = speakingPeers.count == 1 ? peer.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder) : peer.compactDisplayTitle + if currentSpeakingSubtitle.isEmpty { + currentSpeakingSubtitle.append(displayName) + } else { + currentSpeakingSubtitle.append(", \(displayName)") + } + } + } + self.currentSpeakers = updatedSpeakers + self.currentSpeakingSubtitle = currentSpeakingSubtitle.isEmpty ? nil : currentSpeakingSubtitle + self.updateTitle(transition: .immediate) + } + + if case .fullscreen = self.displayMode, !self.mainStageNode.animating { + if speakingPeersUpdated { + self.mainStageNode.update(speakingPeerId: self.currentSpeakers.first) + } + } else { + self.mainStageNode.update(speakingPeerId: nil) + } + + if updateLayout, let (layout, navigationHeight) = self.validLayout { + let transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .easeInOut) + if animatingLayout { + self.animatingExpansion = true + } + self.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: transition) + self.updateDecorationsLayout(transition: transition) + } + } + + private func callStateDidReset() { + self.requestedVideoSources.removeAll() + self.filterRequestedVideoChannels(channels: []) + self.updateRequestedVideoChannels() + } + + private func filterRequestedVideoChannels(channels: [PresentationGroupCallRequestedVideo]) { + var validSources = Set() + for channel in channels { + validSources.insert(channel.endpointId) + + if !self.requestedVideoSources.contains(channel.endpointId) { + self.requestedVideoSources.insert(channel.endpointId) + + let input = (self.call as! PresentationGroupCallImpl).video(endpointId: channel.endpointId) + if let input = input, let videoView = self.videoRenderingContext.makeView(input: input, blur: false) { + let videoNode = GroupVideoNode(videoView: videoView, backdropVideoView: self.videoRenderingContext.makeView(input: input, blur: true)) + + self.readyVideoDisposables.set((combineLatest(videoNode.ready, .single(false) |> then(.single(true) |> delay(10.0, queue: Queue.mainQueue()))) + |> deliverOnMainQueue + ).start(next: { [weak self, weak videoNode] ready, timeouted in + if let strongSelf = self, let videoNode = videoNode { + Queue.mainQueue().after(0.1) { + if timeouted && !ready { + strongSelf.timeoutedEndpointIds.insert(channel.endpointId) + strongSelf.readyVideoEndpointIds.remove(channel.endpointId) + strongSelf.readyVideoEndpointIdsPromise.set(strongSelf.readyVideoEndpointIds) + strongSelf.wideVideoNodes.remove(channel.endpointId) + + strongSelf.updateMembers() + } else if ready { + strongSelf.readyVideoEndpointIds.insert(channel.endpointId) + strongSelf.readyVideoEndpointIdsPromise.set(strongSelf.readyVideoEndpointIds) + strongSelf.timeoutedEndpointIds.remove(channel.endpointId) + if videoNode.aspectRatio <= 0.77 { + strongSelf.wideVideoNodes.insert(channel.endpointId) + } else { + strongSelf.wideVideoNodes.remove(channel.endpointId) + } + strongSelf.updateMembers() + + if let (layout, _) = strongSelf.validLayout, case .compact = layout.metrics.widthClass { + if let interaction = strongSelf.itemInteraction { + loop: for i in 0 ..< strongSelf.currentFullscreenEntries.count { + let entry = strongSelf.currentFullscreenEntries[i] + switch entry { + case let .peer(peerEntry, _): + if peerEntry.effectiveVideoEndpointId == channel.endpointId { + let presentationData = strongSelf.presentationData.withUpdated(theme: strongSelf.darkTheme) + strongSelf.fullscreenListNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [ListViewUpdateItem(index: i, previousIndex: i, item: entry.fullscreenItem(context: strongSelf.context, presentationData: presentationData, interaction: interaction), directionHint: nil)], options: [.Synchronous], updateOpaqueState: nil) + break loop + } + default: + break + } + } + } + } + } + } + } + }), forKey: channel.endpointId) + self.videoNodes[channel.endpointId] = videoNode + + if let _ = self.validLayout { + self.updateMembers() + } + } + + /*self.call.makeIncomingVideoView(endpointId: channel.endpointId, requestClone: GroupVideoNode.useBlurTransparency, completion: { [weak self] videoView, backdropVideoView in + Queue.mainQueue().async { + guard let strongSelf = self, let videoView = videoView else { + return + } + let videoNode = GroupVideoNode(videoView: videoView, backdropVideoView: backdropVideoView) + + strongSelf.readyVideoDisposables.set((combineLatest(videoNode.ready, .single(false) |> then(.single(true) |> delay(10.0, queue: Queue.mainQueue()))) + |> deliverOnMainQueue + ).start(next: { [weak self, weak videoNode] ready, timeouted in + if let strongSelf = self, let videoNode = videoNode { + Queue.mainQueue().after(0.1) { + if timeouted && !ready { + strongSelf.timeoutedEndpointIds.insert(channel.endpointId) + strongSelf.readyVideoEndpointIds.remove(channel.endpointId) + strongSelf.readyVideoEndpointIdsPromise.set(strongSelf.readyVideoEndpointIds) + strongSelf.wideVideoNodes.remove(channel.endpointId) + + strongSelf.updateMembers() + } else if ready { + strongSelf.readyVideoEndpointIds.insert(channel.endpointId) + strongSelf.readyVideoEndpointIdsPromise.set(strongSelf.readyVideoEndpointIds) + strongSelf.timeoutedEndpointIds.remove(channel.endpointId) + if videoNode.aspectRatio <= 0.77 { + strongSelf.wideVideoNodes.insert(channel.endpointId) + } else { + strongSelf.wideVideoNodes.remove(channel.endpointId) + } + strongSelf.updateMembers() + + if let (layout, _) = strongSelf.validLayout, case .compact = layout.metrics.widthClass { + if let interaction = strongSelf.itemInteraction { + loop: for i in 0 ..< strongSelf.currentFullscreenEntries.count { + let entry = strongSelf.currentFullscreenEntries[i] + switch entry { + case let .peer(peerEntry, _): + if peerEntry.effectiveVideoEndpointId == channel.endpointId { + let presentationData = strongSelf.presentationData.withUpdated(theme: strongSelf.darkTheme) + strongSelf.fullscreenListNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [ListViewUpdateItem(index: i, previousIndex: i, item: entry.fullscreenItem(context: strongSelf.context, presentationData: presentationData, interaction: interaction), directionHint: nil)], options: [.Synchronous], updateOpaqueState: nil) + break loop + } + default: + break + } + } + } + } + } + } + } + }), forKey: channel.endpointId) + strongSelf.videoNodes[channel.endpointId] = videoNode + + if let _ = strongSelf.validLayout { + strongSelf.updateMembers() + } + } + })*/ + } + } + + var removeRequestedVideoSources: [String] = [] + for source in self.requestedVideoSources { + if !validSources.contains(source) { + removeRequestedVideoSources.append(source) + } + } + for source in removeRequestedVideoSources { + self.requestedVideoSources.remove(source) + } + + for (videoEndpointId, _) in self.videoNodes { + if !validSources.contains(videoEndpointId) { + self.videoNodes[videoEndpointId] = nil + self.videoOrder.removeAll(where: { $0 == videoEndpointId }) + self.readyVideoEndpointIds.remove(videoEndpointId) + self.readyVideoEndpointIdsPromise.set(self.readyVideoEndpointIds) + self.readyVideoDisposables.set(nil, forKey: videoEndpointId) + } + } } + private func updateMainVideo(waitForFullSize: Bool, entries: [ListEntry]? = nil, updateMembers: Bool = true, force: Bool = false, completion: (() -> Void)? = nil) { + let effectiveMainSpeaker = self.currentForcedSpeaker ?? self.currentDominantSpeaker.flatMap { ($0.0, $0.1) } + guard effectiveMainSpeaker?.0 != self.effectiveSpeaker?.0 || effectiveMainSpeaker?.1 != self.effectiveSpeaker?.1 || force else { + return + } + + let currentEntries = entries ?? self.currentFullscreenEntries + var effectiveSpeaker: (PeerId, String?, Bool, Bool, Bool)? = nil + var anySpeakerWithVideo: (PeerId, String?, Bool, Bool, Bool)? = nil + var anySpeaker: (PeerId, Bool)? = nil + if let (peerId, preferredVideoEndpointId) = effectiveMainSpeaker { + for entry in currentEntries { + switch entry { + case let .peer(peer, _): + if peer.peer.id == peerId { + if let preferredVideoEndpointId = preferredVideoEndpointId, peer.videoEndpointId == preferredVideoEndpointId || peer.presentationEndpointId == preferredVideoEndpointId { + var isPaused = false + if peer.presentationEndpointId != nil && preferredVideoEndpointId == peer.presentationEndpointId { + isPaused = peer.presentationPaused + } else if peer.videoEndpointId != nil && preferredVideoEndpointId == peer.videoEndpointId { + isPaused = peer.videoPaused + } + effectiveSpeaker = (peerId, preferredVideoEndpointId, peer.isMyPeer, peer.presentationEndpointId != nil && preferredVideoEndpointId == peer.presentationEndpointId, isPaused) + } else { + var isPaused = false + if peer.effectiveVideoEndpointId != nil && peer.effectiveVideoEndpointId == peer.presentationEndpointId { + isPaused = peer.presentationPaused + } else if peer.effectiveVideoEndpointId != nil && peer.effectiveVideoEndpointId == peer.videoEndpointId { + isPaused = peer.videoPaused + } + effectiveSpeaker = (peerId, peer.effectiveVideoEndpointId, peer.isMyPeer, peer.presentationEndpointId != nil && peer.effectiveVideoEndpointId == peer.presentationEndpointId, isPaused) + } + } else if anySpeakerWithVideo == nil, let videoEndpointId = peer.effectiveVideoEndpointId { + var isPaused = false + if videoEndpointId == peer.presentationEndpointId { + isPaused = peer.presentationPaused + } else if videoEndpointId == peer.videoEndpointId { + isPaused = peer.videoPaused + } + anySpeakerWithVideo = (peer.peer.id, videoEndpointId, peer.isMyPeer, peer.presentationEndpointId != nil && videoEndpointId == peer.presentationEndpointId, isPaused) + } else if anySpeaker == nil { + anySpeaker = (peer.peer.id, peer.isMyPeer) + } + default: + break + } + } + } + + if effectiveSpeaker == nil { + self.currentForcedSpeaker = nil + effectiveSpeaker = anySpeakerWithVideo ?? anySpeaker.flatMap { ($0.0, nil, $0.1, false, false) } + if let (peerId, videoEndpointId, _, _, _) = effectiveSpeaker { + self.currentDominantSpeaker = (peerId, videoEndpointId, CACurrentMediaTime()) + } else { + self.currentDominantSpeaker = nil + } + } + + self.effectiveSpeaker = effectiveSpeaker + if updateMembers { + self.updateMembers(maybeUpdateVideo: false, force: force) + } + + var waitForFullSize = waitForFullSize + var isReady = false + if let (_, maybeVideoEndpointId, _, _, _) = effectiveSpeaker, let videoEndpointId = maybeVideoEndpointId { + isReady = true + if !self.readyVideoEndpointIds.contains(videoEndpointId) { + isReady = false + if entries == nil { + waitForFullSize = false + } + } + } + + self.mainStageNode.update(peer: effectiveSpeaker, isReady: isReady, waitForFullSize: waitForFullSize, completion: { + completion?() + }) + } + + private func updateRequestedVideoChannels() { + Queue.mainQueue().after(0.3) { + let enableVideo = self.appIsActive && self.visibility + + self.call.setRequestedVideoList(items: enableVideo ? self.requestedVideoChannels : []) + self.filterRequestedVideoChannels(channels: self.requestedVideoChannels) + } + } + override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { - if gestureRecognizer is DirectionalPanGestureRecognizer { - let location = gestureRecognizer.location(in: self.bottomPanelNode.view) - if self.audioOutputNode.frame.contains(location) || (!self.cameraButtonNode.isHidden && self.cameraButtonNode.frame.contains(location)) || self.leaveNode.frame.contains(location) { + if gestureRecognizer is UILongPressGestureRecognizer { + return !self.isScheduling + } else if gestureRecognizer is DirectionalPanGestureRecognizer { + if self.mainStageNode.animating || self.animatingMainStage { + return false + } + + let bottomPanelLocation = gestureRecognizer.location(in: self.bottomPanelNode.view) + let containerLocation = gestureRecognizer.location(in: self.contentContainer.view) + let mainStageLocation = gestureRecognizer.location(in: self.mainStageNode.view) + + if self.isLandscape && self.mainStageContainerNode.isUserInteractionEnabled && mainStageLocation.x > self.mainStageNode.frame.width - 80.0 { + return false + } + + if self.audioButton.frame.contains(bottomPanelLocation) || (!self.cameraButton.isHidden && self.cameraButton.frame.contains(bottomPanelLocation)) || self.leaveButton.frame.contains(bottomPanelLocation) || self.pickerView?.frame.contains(containerLocation) == true || (self.mainStageContainerNode.isUserInteractionEnabled && (mainStageLocation.y < 44.0 || mainStageLocation.y > self.mainStageNode.frame.height - 100.0)) { return false } } @@ -3212,16 +5498,17 @@ public final class VoiceChatController: ViewController { return false } - private var isExpanded = false - private var animatingExpansion = false - private var panGestureArguments: (topInset: CGFloat, offset: CGFloat)? - @objc func panGesture(_ recognizer: UIPanGestureRecognizer) { + guard let (layout, _) = self.validLayout else { + return + } let contentOffset = self.listNode.visibleContentOffset() switch recognizer.state { case .began: let topInset: CGFloat - if self.isExpanded { + if case .regular = layout.metrics.widthClass { + topInset = 0.0 + } else if self.isExpanded { topInset = 0.0 } else if let currentTopInset = self.topInset { topInset = currentTopInset @@ -3231,43 +5518,97 @@ public final class VoiceChatController: ViewController { self.panGestureArguments = (topInset, 0.0) self.controller?.dismissAllTooltips() + + if case .fullscreen = self.displayMode, case .compact = layout.metrics.widthClass { + self.isPanning = true + + self.mainStageBackgroundNode.alpha = 0.0 + self.mainStageBackgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.4) + self.mainStageNode.setControlsHidden(true, animated: true) + + self.fullscreenListNode.alpha = 0.0 + self.fullscreenListNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.1, completion: { [weak self] finished in + self?.attachTileVideos() + + self?.fullscreenListContainer.subnodeTransform = CATransform3DIdentity + }) + + self.listContainer.transform = CATransform3DMakeScale(0.86, 0.86, 1.0) + + self.contentContainer.insertSubnode(self.mainStageContainerNode, aboveSubnode: self.bottomPanelNode) + } case .changed: var translation = recognizer.translation(in: self.contentContainer.view).y - var topInset: CGFloat = 0.0 - if let (currentTopInset, currentPanOffset) = self.panGestureArguments { - topInset = currentTopInset - - if case let .known(value) = contentOffset, value <= 0.5 { - } else { - translation = currentPanOffset - if self.isExpanded { - recognizer.setTranslation(CGPoint(), in: self.contentContainer.view) - } - } - - self.panGestureArguments = (currentTopInset, translation) + if self.isScheduled && translation < 0.0 { + return } - - let currentOffset = topInset + translation - if currentOffset < 20.0 { - self.updateIsFullscreen(true) - } else if currentOffset > 40.0 { - self.updateIsFullscreen(false) - } - - if self.isExpanded { + + let translateBounds: Bool + if case .regular = layout.metrics.widthClass { + translateBounds = true } else { - if currentOffset > 0.0 { - self.listNode.scroller.panGestureRecognizer.setTranslation(CGPoint(), in: self.listNode.scroller) + switch self.displayMode { + case let .modal(isExpanded, previousIsFilled): + var topInset: CGFloat = 0.0 + if let (currentTopInset, currentPanOffset) = self.panGestureArguments { + topInset = currentTopInset + + if case let .known(value) = contentOffset, value <= 0.5 { + } else { + translation = currentPanOffset + if self.isExpanded { + recognizer.setTranslation(CGPoint(), in: self.contentContainer.view) + } + } + + self.panGestureArguments = (currentTopInset, translation) + } + + let currentOffset = topInset + translation + + var isFilled = previousIsFilled + if currentOffset < 20.0 { + isFilled = true + } else if currentOffset > 40.0 { + isFilled = false + } + if isFilled != previousIsFilled { + self.displayMode = .modal(isExpanded: isExpanded, isFilled: isFilled) + self.updateDecorationsColors() + } + + if self.isExpanded { + } else { + if currentOffset > 0.0 { + self.listNode.scroller.panGestureRecognizer.setTranslation(CGPoint(), in: self.listNode.scroller) + } + } + case .fullscreen: + if abs(translation) > 32.0 { + if self.fullscreenListNode.layer.animationKeys()?.contains("opacity") == true { + self.fullscreenListNode.layer.removeAllAnimations() + } + } + var bounds = self.mainStageContainerNode.bounds + bounds.origin.y = -translation + self.mainStageContainerNode.bounds = bounds + + var backgroundFrame = self.mainStageNode.frame + backgroundFrame.origin.y += -translation + self.mainStageBackgroundNode.frame = backgroundFrame + + self.fullscreenListContainer.subnodeTransform = CATransform3DMakeTranslation(0.0, translation, 0.0) } + + translateBounds = !self.isExpanded } if let (layout, navigationHeight) = self.validLayout { self.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: .immediate) - self.updateFloatingHeaderOffset(offset: self.currentContentOffset ?? 0.0, transition: .immediate) + self.updateDecorationsLayout(transition: .immediate) } - if !self.isExpanded { + if translateBounds { var bounds = self.contentContainer.bounds bounds.origin.y = -translation bounds.origin.y = min(0.0, bounds.origin.y) @@ -3301,28 +5642,68 @@ public final class VoiceChatController: ViewController { topInset = self.listNode.frame.height } - if self.isExpanded { + if case .fullscreen = self.displayMode, case .compact = layout.metrics.widthClass { + self.panGestureArguments = nil + self.fullscreenListContainer.subnodeTransform = CATransform3DIdentity + if abs(translation.y) > 100.0 || abs(velocity.y) > 300.0 { + self.mainStageBackgroundNode.layer.removeAllAnimations() + self.currentForcedSpeaker = nil + self.updateDisplayMode(.modal(isExpanded: true, isFilled: true), fromPan: true) + self.effectiveSpeaker = nil + } else { + self.isPanning = false + self.mainStageBackgroundNode.alpha = 1.0 + self.mainStageBackgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15, completion: { [weak self] _ in + self?.attachFullscreenVideos() + }) + self.mainStageNode.setControlsHidden(false, animated: true, delay: 0.15) + + self.fullscreenListNode.alpha = 1.0 + self.fullscreenListNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, delay: 0.15) + + var bounds = self.mainStageContainerNode.bounds + let previousBounds = bounds + bounds.origin.y = 0.0 + self.mainStageContainerNode.bounds = bounds + self.mainStageContainerNode.layer.animateBounds(from: previousBounds, to: self.mainStageContainerNode.bounds, duration: 0.2, timingFunction: kCAMediaTimingFunctionSpring, completion: { [weak self] _ in + if let strongSelf = self { + strongSelf.listContainer.transform = CATransform3DIdentity + strongSelf.contentContainer.insertSubnode(strongSelf.mainStageContainerNode, belowSubnode: strongSelf.transitionContainerNode) + strongSelf.updateMembers() + } + }) + } + } else if case .modal(true, _) = self.displayMode, case .compact = layout.metrics.widthClass { self.panGestureArguments = nil if velocity.y > 300.0 || offset > topInset / 2.0 { - self.isExpanded = false - self.updateIsFullscreen(false) + self.displayMode = .modal(isExpanded: false, isFilled: false) + self.updateDecorationsColors() self.animatingExpansion = true self.listNode.scroller.setContentOffset(CGPoint(), animated: false) - if let (layout, navigationHeight) = self.validLayout { - self.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: .animated(duration: 0.3, curve: .easeInOut)) + let distance: CGFloat + if let topInset = self.topInset { + distance = topInset - offset + } else { + distance = 0.0 } - self.updateFloatingHeaderOffset(offset: self.currentContentOffset ?? 0.0, transition: .animated(duration: 0.3, curve: .easeInOut), completion: { + let initialVelocity: CGFloat = distance.isZero ? 0.0 : abs(velocity.y / distance) + let transition = ContainedViewLayoutTransition.animated(duration: 0.45, curve: .customSpring(damping: 124.0, initialVelocity: initialVelocity)) + if let (layout, navigationHeight) = self.validLayout { + self.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: transition) + } + self.updateDecorationsLayout(transition: transition, completion: { self.animatingExpansion = false }) } else { - self.updateIsFullscreen(true) + self.displayMode = .modal(isExpanded: true, isFilled: true) + self.updateDecorationsColors() self.animatingExpansion = true if let (layout, navigationHeight) = self.validLayout { self.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: .animated(duration: 0.3, curve: .easeInOut)) } - self.updateFloatingHeaderOffset(offset: self.currentContentOffset ?? 0.0, transition: .animated(duration: 0.3, curve: .easeInOut), completion: { + self.updateDecorationsLayout(transition: .animated(duration: 0.3, curve: .easeInOut), completion: { self.animatingExpansion = false }) } @@ -3330,34 +5711,48 @@ public final class VoiceChatController: ViewController { self.panGestureArguments = nil var dismissing = false if bounds.minY < -60 || (bounds.minY < 0.0 && velocity.y > 300.0) { - self.controller?.dismiss(closing: false, manual: true) - dismissing = true - } else if velocity.y < -300.0 || offset < topInset / 2.0 { - if velocity.y > -1500.0 && !self.isFullscreen { + if self.isScheduling { + self.dismissScheduled() + } else if case .regular = layout.metrics.widthClass { + self.controller?.dismiss(closing: false, manual: true) + dismissing = true + } else { + if case .fullscreen = self.displayMode { + } else { + self.controller?.dismiss(closing: false, manual: true) + dismissing = true + } + } + } else if !self.isScheduling && (velocity.y < -300.0 || offset < topInset / 2.0) { + if velocity.y > -2200.0 && !self.isFullscreen { DispatchQueue.main.async { 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.isExpanded = true - self.updateIsFullscreen(true) + + let initialVelocity: CGFloat = offset.isZero ? 0.0 : abs(velocity.y / offset) + let transition = ContainedViewLayoutTransition.animated(duration: 0.45, curve: .customSpring(damping: 124.0, initialVelocity: initialVelocity)) + if case .modal = self.displayMode { + self.displayMode = .modal(isExpanded: true, isFilled: true) + } + self.updateDecorationsColors() self.animatingExpansion = true if let (layout, navigationHeight) = self.validLayout { - self.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: .animated(duration: 0.3, curve: .easeInOut)) + self.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: transition) } - self.updateFloatingHeaderOffset(offset: self.currentContentOffset ?? 0.0, transition: .animated(duration: 0.3, curve: .easeInOut), completion: { + self.updateDecorationsLayout(transition: transition, completion: { self.animatingExpansion = false }) - } else { - self.updateIsFullscreen(false) + } else if !self.isScheduling { + self.updateDecorationsColors() self.animatingExpansion = true self.listNode.scroller.setContentOffset(CGPoint(), animated: false) if let (layout, navigationHeight) = self.validLayout { self.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: .animated(duration: 0.3, curve: .easeInOut)) } - self.updateFloatingHeaderOffset(offset: self.currentContentOffset ?? 0.0, transition: .animated(duration: 0.3, curve: .easeInOut), completion: { + self.updateDecorationsLayout(transition: .animated(duration: 0.3, curve: .easeInOut), completion: { self.animatingExpansion = false }) } @@ -3381,9 +5776,35 @@ public final class VoiceChatController: ViewController { if let (layout, navigationHeight) = self.validLayout { self.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: .animated(duration: 0.3, curve: .easeInOut)) } - self.updateFloatingHeaderOffset(offset: self.currentContentOffset ?? 0.0, transition: .animated(duration: 0.3, curve: .easeInOut), completion: { + self.updateDecorationsLayout(transition: .animated(duration: 0.3, curve: .easeInOut), completion: { self.animatingExpansion = false }) + + if case .fullscreen = self.displayMode, case .regular = layout.metrics.widthClass { + self.fullscreenListContainer.subnodeTransform = CATransform3DIdentity + self.isPanning = false + self.mainStageBackgroundNode.alpha = 1.0 + self.mainStageBackgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15, completion: { [weak self] _ in + self?.attachFullscreenVideos() + }) + self.mainStageNode.setControlsHidden(false, animated: true, delay: 0.15) + + self.fullscreenListNode.alpha = 1.0 + self.fullscreenListNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, delay: 0.15) + + var bounds = self.mainStageContainerNode.bounds + let previousBounds = bounds + bounds.origin.y = 0.0 + self.mainStageContainerNode.bounds = bounds + self.mainStageContainerNode.layer.animateBounds(from: previousBounds, to: self.mainStageContainerNode.bounds, duration: 0.2, timingFunction: kCAMediaTimingFunctionSpring, completion: { [weak self] _ in + if let strongSelf = self { + strongSelf.contentContainer.insertSubnode(strongSelf.mainStageContainerNode, belowSubnode: strongSelf.transitionContainerNode) + strongSelf.updateMembers() + + strongSelf.listContainer.transform = CATransform3DIdentity + } + }) + } default: break } @@ -3391,23 +5812,12 @@ public final class VoiceChatController: ViewController { override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { let result = super.hitTest(point, with: event) - - if let result = result { - for (_, _, videoNode) in self.videoNodes { - if videoNode.view === result || result.isDescendant(of: videoNode.view) { - return result - } - } - } - if result === self.topPanelNode.view { return self.view } - if result === self.bottomPanelNode.view { return self.view } - if !self.bounds.contains(point) { return nil } @@ -3416,6 +5826,768 @@ public final class VoiceChatController: ViewController { } return result } + + fileprivate func scrollToTop() { + if self.isExpanded { + 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 openTitleEditing() { + let _ = (self.context.account.postbox.loadedPeerWithId(self.call.peerId) + |> deliverOnMainQueue).start(next: { [weak self] chatPeer in + guard let strongSelf = self else { + return + } + + let initialTitle = strongSelf.callState?.title ?? "" + let controller = voiceChatTitleEditController(sharedContext: strongSelf.context.sharedContext, account: strongSelf.context.account, forceTheme: strongSelf.darkTheme, title: strongSelf.presentationData.strings.VoiceChat_EditTitleTitle, text: strongSelf.presentationData.strings.VoiceChat_EditTitleText, placeholder: chatPeer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder), value: initialTitle, maxLength: 40, apply: { title in + if let strongSelf = self, let title = title, title != initialTitle { + strongSelf.call.updateTitle(title) + + strongSelf.presentUndoOverlay(content: .voiceChatFlag(text: title.isEmpty ? strongSelf.presentationData.strings.VoiceChat_EditTitleRemoveSuccess : strongSelf.presentationData.strings.VoiceChat_EditTitleSuccess(title).0), action: { _ in return false }) + } + }) + strongSelf.controller?.present(controller, in: .window(.root)) + }) + } + + private func openAvatarForEditing(fromGallery: Bool = false, completion: @escaping () -> Void = {}) { + guard let peerId = self.callState?.myPeerId else { + return + } + + 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: strongSelf.darkTheme) + 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 paintStickersContext = LegacyPaintStickersContext(context: strongSelf.context) +// paintStickersContext.presentStickersController = { completion in +// let controller = DrawingStickersScreen(context: strongSelf.context, selectSticker: { fileReference, node, rect in +// let coder = PostboxEncoder() +// coder.encodeRootObject(fileReference.media) +// completion?(coder.makeData(), fileReference.media.isAnimatedSticker, node.view, rect) +// return true +// }) +// strongSelf.controller?.present(controller, in: .window(.root)) +// return controller +// } + + let mixin = TGMediaAvatarMenuMixin(context: legacyController.context, parentController: emptyController, hasSearchButton: true, hasDeleteButton: hasPhotos && !fromGallery, hasViewButton: false, personalPhoto: peerId.namespace == Namespaces.Peer.CloudUser, isVideo: false, saveEditedPhotos: false, saveCapturedMedia: false, signup: false)! + mixin.forceDark = true + mixin.stickersContext = paintStickersContext + let _ = strongSelf.currentAvatarMixin.swap(mixin) + mixin.requestSearchController = { [weak self] assetsController in + guard let strongSelf = self else { + return + } + let controller = WebSearchController(context: strongSelf.context, peer: peer, chatLocation: nil, configuration: searchBotsConfiguration, mode: .avatar(initialQuery: peer.id.namespace == Namespaces.Peer.CloudUser ? nil : peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), completion: { [weak self] result in + assetsController?.dismiss() + self?.updateProfilePhoto(result) + })) + controller.navigationPresentation = .modal + strongSelf.controller?.push(controller) + + if fromGallery { + completion() + } + } + mixin.didFinishWithImage = { [weak self] image in + if let image = image { + completion() + self?.updateProfilePhoto(image) + } + } + mixin.didFinishWithVideo = { [weak self] image, asset, adjustments in + if let image = image, let asset = asset { + completion() + self?.updateProfileVideo(image, asset: asset, adjustments: adjustments) + } + } + mixin.didFinishWithDelete = { + guard let strongSelf = self else { + return + } + + let proceed = { + let _ = strongSelf.currentAvatarMixin.swap(nil) + let postbox = strongSelf.context.account.postbox + strongSelf.updateAvatarDisposable.set((strongSelf.context.engine.peers.updatePeerPhoto(peerId: peerId, photo: nil, mapResourceToAvatarSizes: { resource, representations in + return mapResourceToAvatarSizes(postbox: postbox, resource: resource, representations: representations) + }) + |> deliverOnMainQueue).start()) + } + + let actionSheet = ActionSheetController(presentationData: presentationData.withUpdated(theme: strongSelf.darkTheme)) + let items: [ActionSheetItem] = [ + ActionSheetButtonItem(title: presentationData.strings.Settings_RemoveConfirmation, color: .destructive, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + proceed() + }) + ] + + actionSheet.setItemGroups([ + ActionSheetItemGroup(items: items), + ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + }) + ]) + ]) + strongSelf.controller?.present(actionSheet, in: .window(.root)) + } + 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 updateProfilePhoto(_ image: UIImage) { + guard let data = image.jpegData(compressionQuality: 0.6), let peerId = self.callState?.myPeerId else { + return + } + + let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) + self.call.account.postbox.mediaBox.storeResourceData(resource.id, data: data) + let representation = TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 640, height: 640), resource: resource, progressiveSizes: [], immediateThumbnailData: nil) + + self.currentUpdatingAvatar = representation + self.updateAvatarPromise.set(.single((representation, 0.0))) + + let postbox = self.call.account.postbox + let signal = peerId.namespace == Namespaces.Peer.CloudUser ? self.call.accountContext.engine.accountData.updateAccountPhoto(resource: resource, videoResource: nil, videoStartTimestamp: nil, mapResourceToAvatarSizes: { resource, representations in + return mapResourceToAvatarSizes(postbox: postbox, resource: resource, representations: representations) + }) : self.call.accountContext.engine.peers.updatePeerPhoto(peerId: peerId, photo: self.call.accountContext.engine.peers.uploadedPeerPhoto(resource: resource), mapResourceToAvatarSizes: { resource, representations in + return mapResourceToAvatarSizes(postbox: postbox, resource: resource, representations: representations) + }) + + self.updateAvatarDisposable.set((signal + |> deliverOnMainQueue).start(next: { [weak self] result in + guard let strongSelf = self else { + return + } + switch result { + case .complete: + strongSelf.updateAvatarPromise.set(.single(nil)) + case let .progress(value): + strongSelf.updateAvatarPromise.set(.single((representation, value))) + } + })) + + self.updateMembers() + } + + private func updateProfileVideo(_ image: UIImage, asset: Any?, adjustments: TGVideoEditAdjustments?) { + guard let data = image.jpegData(compressionQuality: 0.6), let peerId = self.callState?.myPeerId else { + return + } + + let photoResource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) + self.context.account.postbox.mediaBox.storeResourceData(photoResource.id, data: data) + let representation = TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 640, height: 640), resource: photoResource, progressiveSizes: [], immediateThumbnailData: nil) + + self.currentUpdatingAvatar = representation + self.updateAvatarPromise.set(.single((representation, 0.0))) + + var videoStartTimestamp: Double? = nil + if let adjustments = adjustments, adjustments.videoStartValue > 0.0 { + videoStartTimestamp = adjustments.videoStartValue - adjustments.trimStartValue + } + + let context = self.context + let account = self.context.account + let signal = Signal { [weak self] subscriber in + let entityRenderer: LegacyPaintEntityRenderer? = adjustments.flatMap { adjustments in + if let paintingData = adjustments.paintingData, paintingData.hasAnimation { + return LegacyPaintEntityRenderer(account: account, adjustments: adjustments) + } else { + return nil + } + } + let uploadInterface = LegacyLiveUploadInterface(context: context) + let signal: SSignal + if let asset = asset as? AVAsset { + signal = TGMediaVideoConverter.convert(asset, adjustments: adjustments, watcher: uploadInterface, entityRenderer: entityRenderer)! + } else if let url = asset as? URL, let data = try? Data(contentsOf: url, options: [.mappedRead]), let image = UIImage(data: data), let entityRenderer = entityRenderer { + let durationSignal: SSignal = SSignal(generator: { subscriber in + let disposable = (entityRenderer.duration()).start(next: { duration in + subscriber?.putNext(duration) + subscriber?.putCompletion() + }) + + return SBlockDisposable(block: { + disposable.dispose() + }) + }) + signal = durationSignal.map(toSignal: { duration -> SSignal? in + if let duration = duration as? Double { + return TGMediaVideoConverter.renderUIImage(image, duration: duration, adjustments: adjustments, watcher: nil, entityRenderer: entityRenderer)! + } else { + return SSignal.single(nil) + } + }) + + } else { + signal = SSignal.complete() + } + + let signalDisposable = signal.start(next: { next in + if let result = next as? TGMediaVideoConversionResult { + if let image = result.coverImage, let data = image.jpegData(compressionQuality: 0.7) { + account.postbox.mediaBox.storeResourceData(photoResource.id, data: data) + } + + if let timestamp = videoStartTimestamp { + videoStartTimestamp = max(0.0, min(timestamp, result.duration - 0.05)) + } + + var value = stat() + if stat(result.fileURL.path, &value) == 0 { + if let data = try? Data(contentsOf: result.fileURL) { + let resource: TelegramMediaResource + if let liveUploadData = result.liveUploadData as? LegacyLiveUploadInterfaceResult { + resource = LocalFileMediaResource(fileId: liveUploadData.id) + } else { + resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) + } + account.postbox.mediaBox.storeResourceData(resource.id, data: data, synchronous: true) + subscriber.putNext(resource) + } + } + subscriber.putCompletion() + } else if let strongSelf = self, let progress = next as? NSNumber { + Queue.mainQueue().async { + strongSelf.updateAvatarPromise.set(.single((representation, Float(truncating: progress) * 0.25))) + } + } + }, error: { _ in + }, completed: nil) + + let disposable = ActionDisposable { + signalDisposable?.dispose() + } + + return ActionDisposable { + disposable.dispose() + } + } + + self.updateAvatarDisposable.set((signal + |> mapToSignal { videoResource -> Signal in + if peerId.namespace == Namespaces.Peer.CloudUser { + return context.engine.accountData.updateAccountPhoto(resource: photoResource, videoResource: videoResource, videoStartTimestamp: videoStartTimestamp, mapResourceToAvatarSizes: { resource, representations in + return mapResourceToAvatarSizes(postbox: account.postbox, resource: resource, representations: representations) + }) + } else { + return context.engine.peers.updatePeerPhoto(peerId: peerId, photo: context.engine.peers.uploadedPeerPhoto(resource: photoResource), video: context.engine.peers.uploadedPeerVideo(resource: videoResource) |> map(Optional.init), videoStartTimestamp: videoStartTimestamp, mapResourceToAvatarSizes: { resource, representations in + return mapResourceToAvatarSizes(postbox: account.postbox, resource: resource, representations: representations) + }) + } + } + |> deliverOnMainQueue).start(next: { [weak self] result in + guard let strongSelf = self else { + return + } + switch result { + case .complete: + strongSelf.updateAvatarPromise.set(.single(nil)) + case let .progress(value): + strongSelf.updateAvatarPromise.set(.single((representation, 0.25 + value * 0.75))) + } + })) + } + + private func displayUnmuteTooltip() { + guard let (layout, _) = self.validLayout else { + return + } + let location = self.actionButton.view.convert(self.actionButton.bounds, to: self.view).center + var point = CGRect(origin: CGPoint(x: location.x - 5.0, y: location.y - 5.0 - 68.0), size: CGSize(width: 10.0, height: 10.0)) + var position: TooltipScreen.ArrowPosition = .bottom + if case .compact = layout.metrics.widthClass { + if self.isLandscape { + point.origin.x = location.x - 5.0 - 36.0 + point.origin.y = location.y - 5.0 + position = .right + } else if case .fullscreen = self.displayMode { + point.origin.y += 32.0 + } + } + self.controller?.present(TooltipScreen(text: self.presentationData.strings.VoiceChat_UnmuteSuggestion, style: .gradient(UIColor(rgb: 0x1d446c), UIColor(rgb: 0x193e63)), icon: nil, location: .point(point, position), displayDuration: .custom(8.0), shouldDismissOnTouch: { _ in + return .dismiss(consume: false) + }), in: .window(.root)) + } + + private func displayToggleVideoSourceTooltip(screencast: Bool) { +// guard let videoContainerNode = self.mainStageVideoContainerNode else { +// return +// } +// +// let location = videoContainerNode.view.convert(videoContainerNode.otherVideoWrapperNode.frame, to: nil) +// self.controller?.present(TooltipScreen(text: screencast ? self.presentationData.strings.VoiceChat_TapToViewCameraVideo : self.presentationData.strings.VoiceChat_TapToViewScreenVideo, icon: nil, location: .point(location.offsetBy(dx: -9.0, dy: 0.0), .right), displayDuration: .custom(3.0), shouldDismissOnTouch: { _ in +// return .dismiss(consume: false) +// }), in: .window(.root)) + } + + private var isScheduled: Bool { + return self.isScheduling || self.callState?.scheduleTimestamp != nil + } + + private func attachFullscreenVideos() { + guard let (layout, _) = self.validLayout, case .compact = layout.metrics.widthClass else { + return + } + var verticalItemNodes: [String: ASDisplayNode] = [:] + self.listNode.forEachItemNode { itemNode in + if let itemNode = itemNode as? VoiceChatTilesGridItemNode { + for tileNode in itemNode.tileNodes { + if let item = tileNode.item { + verticalItemNodes[String(item.peer.id.toInt64()) + "_" + item.videoEndpointId] = tileNode + } + + if tileNode.item?.peer.id == self.effectiveSpeaker?.0 && tileNode.item?.videoEndpointId == self.effectiveSpeaker?.1 { + tileNode.isHidden = false + } + } + } + } + + self.fullscreenListNode.forEachItemNode { itemNode in + if let itemNode = itemNode as? VoiceChatFullscreenParticipantItemNode, let item = itemNode.item { + let otherItemNode = verticalItemNodes[String(item.peer.id.toInt64()) + "_" + (item.videoEndpointId ?? "")] + itemNode.transitionIn(from: otherItemNode) + } + } + } + + private func attachTileVideos() { + var fullscreenItemNodes: [String: VoiceChatFullscreenParticipantItemNode] = [:] + var tileNodes: [VoiceChatTileItemNode] = [] + if !self.tileGridNode.isHidden { + tileNodes = self.tileGridNode.tileNodes + } else { + self.fullscreenListNode.forEachItemNode { itemNode in + if let itemNode = itemNode as? VoiceChatFullscreenParticipantItemNode, let item = itemNode.item { + fullscreenItemNodes[String(item.peer.id.toInt64()) + "_" + (item.videoEndpointId ?? "")] = itemNode + } + } + self.listNode.forEachItemNode { itemNode in + if let itemNode = itemNode as? VoiceChatTilesGridItemNode { + tileNodes = itemNode.tileNodes + } + } + } + + for tileNode in tileNodes { + if let item = tileNode.item { + let otherItemNode = fullscreenItemNodes[String(item.peer.id.toInt64()) + "_" + item.videoEndpointId] + tileNode.transitionIn(from: otherItemNode) + + if tileNode.item?.peer.id == self.effectiveSpeaker?.0 && tileNode.item?.videoEndpointId == self.effectiveSpeaker?.1 { + tileNode.isHidden = true + } + } + } + } + + private func updateDisplayMode(_ displayMode: DisplayMode, fromPan: Bool = false) { + guard !self.animatingExpansion && !self.animatingMainStage && !self.mainStageNode.animating else { + return + } + self.updateMembers() + + let previousDisplayMode = self.displayMode + var isFullscreen = false + if case .fullscreen = displayMode { + isFullscreen = true + } + + if case .fullscreen = previousDisplayMode, case .fullscreen = displayMode { + self.animatingExpansion = true + } else { + self.animatingMainStage = true + } + + var hasFullscreenList = false + if let (layout, _) = self.validLayout, case .compact = layout.metrics.widthClass { + hasFullscreenList = true + } + + let completion = { + self.displayMode = displayMode + self.updateDecorationsColors() + + self.mainStageContainerNode.isHidden = false + self.mainStageContainerNode.isUserInteractionEnabled = isFullscreen + + let transition: ContainedViewLayoutTransition = .animated(duration: 0.55, curve: .spring) + if case .modal = previousDisplayMode, case .fullscreen = self.displayMode { + self.mainStageNode.alpha = 0.0 + self.updateDecorationsLayout(transition: .immediate) + + var verticalItemNodes: [String: ASDisplayNode] = [:] + + var tileNodes: [VoiceChatTileItemNode] = [] + if !self.tileGridNode.isHidden { + tileNodes = self.tileGridNode.tileNodes + } else { + self.listNode.forEachItemNode { itemNode in + if let itemNode = itemNode as? VoiceChatTilesGridItemNode { + tileNodes = itemNode.tileNodes + } + } + } + for tileNode in tileNodes { + if let item = tileNode.item { + verticalItemNodes[String(item.peer.id.toInt64()) + "_" + item.videoEndpointId] = tileNode + } + } + + let completion = { + let effectiveSpeakerPeerId = self.effectiveSpeaker?.0 + + if hasFullscreenList { + self.fullscreenListContainer.isHidden = false + self.fullscreenListNode.alpha = 0.0 + } + + var gridSnapshotView: UIView? + if !hasFullscreenList, let snapshotView = self.tileGridNode.view.snapshotView(afterScreenUpdates: false) { + gridSnapshotView = snapshotView + self.tileGridNode.view.addSubview(snapshotView) + self.displayPanelVideos = true + self.updateMembers(maybeUpdateVideo: false, force: true) + } + + let completion = { + if hasFullscreenList { + self.attachFullscreenVideos() + + self.fullscreenListNode.alpha = 1.0 + self.fullscreenListNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) + } + } + if let effectiveSpeakerPeerId = effectiveSpeakerPeerId, let otherItemNode = verticalItemNodes[String(effectiveSpeakerPeerId.toInt64()) + "_" + (self.effectiveSpeaker?.1 ?? "")] { + + if hasFullscreenList { + let transitionStartPosition = otherItemNode.view.convert(CGPoint(x: otherItemNode.frame.width / 2.0, y: otherItemNode.frame.height), to: self.fullscreenListContainer.view.superview) + self.fullscreenListContainer.layer.animatePosition(from: transitionStartPosition, to: self.fullscreenListContainer.position, duration: 0.55, timingFunction: kCAMediaTimingFunctionSpring) + } + + self.mainStageNode.animateTransitionIn(from: otherItemNode, transition: transition, completion: { [weak self] in + self?.animatingMainStage = false + }) + self.mainStageNode.alpha = 1.0 + + self.mainStageBackgroundNode.alpha = 1.0 + self.mainStageBackgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: hasFullscreenList ? 0.13 : 0.3, completion: { [weak otherItemNode] _ in + otherItemNode?.alpha = 0.0 + gridSnapshotView?.removeFromSuperview() + completion() + }) + } else { + completion() + } + + if hasFullscreenList { + self.listContainer.layer.animateScale(from: 1.0, to: 0.86, duration: 0.55, timingFunction: kCAMediaTimingFunctionSpring) + } + + if self.isLandscape { + self.transitionMaskTopFillLayer.opacity = 1.0 + } + self.transitionMaskBottomFillLayer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3, removeOnCompletion: false, completion: { [weak self] _ in + Queue.mainQueue().after(0.3) { + self?.transitionMaskTopFillLayer.opacity = 0.0 + self?.transitionMaskBottomFillLayer.removeAllAnimations() + } + }) + + if let (layout, navigationHeight) = self.validLayout { + self.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: transition) + self.updateDecorationsLayout(transition: transition) + } + } + let effectiveSpeakerPeerId = self.effectiveSpeaker?.0 + var index = 0 + for item in self.currentFullscreenEntries { + if case let .peer(entry, _) = item, entry.peer.id == effectiveSpeakerPeerId { + break + } else { + index += 1 + } + } + let position: ListViewScrollPosition + if index > self.currentFullscreenEntries.count - 3 { + index = self.currentFullscreenEntries.count - 1 + position = .bottom(0.0) + } else { + position = .center(.bottom) + } + self.fullscreenListNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: ListViewScrollToItem(index: index, position: position, animated: false, curve: .Default(duration: nil), directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in + completion() + }) + } else if case .fullscreen = previousDisplayMode, case .modal = self.displayMode { + var minimalVisiblePeerid: (PeerId, CGFloat)? + var fullscreenItemNodes: [String: VoiceChatFullscreenParticipantItemNode] = [:] + self.fullscreenListNode.forEachItemNode { itemNode in + if let itemNode = itemNode as? VoiceChatFullscreenParticipantItemNode, let item = itemNode.item { + let convertedFrame = itemNode.view.convert(itemNode.bounds, to: self.transitionContainerNode.view) + if let (_, x) = minimalVisiblePeerid { + if convertedFrame.minX >= 0.0 && convertedFrame.minX < x { + minimalVisiblePeerid = (item.peer.id, convertedFrame.minX) + } + } else if convertedFrame.minX >= 0.0 { + minimalVisiblePeerid = (item.peer.id, convertedFrame.minX) + } + fullscreenItemNodes[String(item.peer.id.toInt64()) + "_" + (item.videoEndpointId ?? "")] = itemNode + } + } + + let completion = { + let effectiveSpeakerPeerId = self.effectiveSpeaker?.0 + var targetTileNode: VoiceChatTileItemNode? + + self.transitionContainerNode.addSubnode(self.mainStageNode) + + self.listContainer.transform = CATransform3DIdentity + + var tileNodes: [VoiceChatTileItemNode] = [] + if !self.tileGridNode.isHidden { + tileNodes = self.tileGridNode.tileNodes + } else { + self.listNode.forEachItemNode { itemNode in + if let itemNode = itemNode as? VoiceChatTilesGridItemNode { + tileNodes = itemNode.tileNodes + } + } + } + for tileNode in tileNodes { + if let item = tileNode.item { + if item.peer.id == effectiveSpeakerPeerId, item.videoEndpointId == self.effectiveSpeaker?.1 { + targetTileNode = tileNode + } + } + } + + var transitionOffset = -self.mainStageContainerNode.bounds.minY + if transitionOffset.isZero, let (layout, _) = self.validLayout { + if case .regular = layout.metrics.widthClass { + transitionOffset += 87.0 + } + if let targetTileNode = targetTileNode { + let transitionTargetPosition = targetTileNode.view.convert(CGPoint(x: targetTileNode.frame.width / 2.0, y: targetTileNode.frame.height), to: self.fullscreenListContainer.view.superview) + self.fullscreenListContainer.layer.animatePosition(from: self.fullscreenListContainer.position, to: transitionTargetPosition, duration: 0.55, timingFunction: kCAMediaTimingFunctionSpring) + } + + if !hasFullscreenList { + self.displayPanelVideos = false + self.listNode.forEachItemNode { itemNode in + if let itemNode = itemNode as? VoiceChatTilesGridItemNode { + itemNode.snapshotForDismissal() + } + } + self.updateMembers(maybeUpdateVideo: false, force: true) + self.attachTileVideos() + + self.mainStageBackgroundNode.alpha = 0.0 + self.mainStageBackgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25) + } else { + self.fullscreenListNode.alpha = 0.0 + self.mainStageBackgroundNode.alpha = 1.0 + self.fullscreenListNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.1, completion: { [weak self] _ in + if let strongSelf = self { + strongSelf.fullscreenListContainer.isHidden = true + strongSelf.fullscreenListNode.alpha = 1.0 + strongSelf.attachTileVideos() + + strongSelf.mainStageBackgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25) + strongSelf.mainStageBackgroundNode.alpha = 0.0 + } + }) + } + } + self.mainStageNode.animateTransitionOut(to: targetTileNode, offset: transitionOffset, transition: transition, completion: { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.effectiveSpeaker = nil + strongSelf.mainStageNode.update(peer: nil, waitForFullSize: false) + strongSelf.mainStageNode.setControlsHidden(false, animated: false) + strongSelf.fullscreenListContainer.isHidden = true + strongSelf.mainStageContainerNode.isHidden = true + strongSelf.mainStageContainerNode.addSubnode(strongSelf.mainStageNode) + + var bounds = strongSelf.mainStageContainerNode.bounds + bounds.origin.y = 0.0 + strongSelf.mainStageContainerNode.bounds = bounds + + strongSelf.contentContainer.insertSubnode(strongSelf.mainStageContainerNode, belowSubnode: strongSelf.transitionContainerNode) + + strongSelf.isPanning = false + strongSelf.animatingMainStage = false + }) + + if hasFullscreenList { + self.listContainer.layer.animateScale(from: 0.86, to: 1.0, duration: 0.55, timingFunction: kCAMediaTimingFunctionSpring) + } + + self.transitionMaskTopFillLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3) + if !transitionOffset.isZero { + self.transitionMaskBottomFillLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3) + } + + if let (layout, navigationHeight) = self.validLayout { + self.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: transition) + self.updateDecorationsLayout(transition: transition) + } + } + if false, let (peerId, _) = minimalVisiblePeerid { + var index = 0 + for item in self.currentEntries { + if case let .peer(entry, _) = item, entry.peer.id == peerId { + break + } else { + index += 1 + } + } + self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: ListViewScrollToItem(index: index, position: .top(0.0), animated: false, curve: .Default(duration: nil), directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in + completion() + }) + } else { + completion() + } + } else if case .fullscreen = self.displayMode { + if let (layout, navigationHeight) = self.validLayout { + let transition: ContainedViewLayoutTransition = .animated(duration: 0.4, curve: .spring) + self.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: transition) + self.updateDecorationsLayout(transition: transition) + } + } + } + + if case .fullscreen(false) = displayMode, case .modal = previousDisplayMode { + self.updateMainVideo(waitForFullSize: true, updateMembers: true, force: true, completion: { + completion() + }) + } else { + completion() + } + } + + fileprivate var actionButtonPosition: CGPoint { + guard let (layout, _) = self.validLayout else { + return CGPoint() + } + let size = layout.size + let hasCameraButton = self.cameraButton.isUserInteractionEnabled + let centralButtonSide = min(size.width, size.height) - 32.0 + let centralButtonSize = CGSize(width: centralButtonSide, height: centralButtonSide) + + if case .regular = layout.metrics.widthClass { + let contentWidth: CGFloat = max(320.0, min(375.0, floor(size.width * 0.3))) + let contentLeftInset: CGFloat + if self.peerIdToEndpointId.isEmpty { + contentLeftInset = floorToScreenPixels((layout.size.width - contentWidth) / 2.0) + } else { + contentLeftInset = self.panelHidden ? layout.size.width : layout.size.width - contentWidth + } + return CGPoint(x: contentLeftInset + floorToScreenPixels(contentWidth / 2.0), y: layout.size.height - self.effectiveBottomAreaHeight - layout.intrinsicInsets.bottom + floor(self.effectiveBottomAreaHeight / 2.0) - 3.0) + } else { + switch self.displayMode { + case .modal: + if self.isLandscape { + let sideInset: CGFloat + let buttonsCount: Int + if hasCameraButton { + sideInset = 26.0 + buttonsCount = 4 + } else { + sideInset = 42.0 + buttonsCount = 3 + } + let spacing = floor((layout.size.height - sideInset * 2.0 - sideButtonSize.height * CGFloat(buttonsCount)) / (CGFloat(buttonsCount - 1))) + let x = layout.size.width - fullscreenBottomAreaHeight - layout.safeInsets.right + floor((fullscreenBottomAreaHeight - sideButtonSize.width) / 2.0) + let actionButtonFrame = CGRect(origin: CGPoint(x: x, y: sideInset + sideButtonSize.height + spacing), size: sideButtonSize) + return actionButtonFrame.center + } else { + let actionButtonFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - centralButtonSize.width) / 2.0), y: layout.size.height - self.effectiveBottomAreaHeight - layout.intrinsicInsets.bottom + floor((self.effectiveBottomAreaHeight - centralButtonSize.height) / 2.0) - 3.0), size: centralButtonSize) + return actionButtonFrame.center + } + case let .fullscreen(controlsHidden): + if self.isLandscape { + let sideInset: CGFloat + let buttonsCount: Int + if hasCameraButton { + sideInset = 26.0 + buttonsCount = 4 + } else { + sideInset = 42.0 + buttonsCount = 3 + } + let spacing = floor((layout.size.height - sideInset * 2.0 - sideButtonSize.height * CGFloat(buttonsCount)) / (CGFloat(buttonsCount - 1))) + let x = layout.size.width - fullscreenBottomAreaHeight - layout.safeInsets.right + (controlsHidden ? fullscreenBottomAreaHeight + layout.safeInsets.right + 30.0 : floor((fullscreenBottomAreaHeight - sideButtonSize.width) / 2.0)) + let actionButtonFrame = CGRect(origin: CGPoint(x: x, y: sideInset + sideButtonSize.height + spacing), size: sideButtonSize) + return actionButtonFrame.center + } else { + let sideInset: CGFloat + let buttonsCount: Int + if hasCameraButton { + sideInset = 26.0 + buttonsCount = 4 + } else { + sideInset = 42.0 + buttonsCount = 3 + } + let spacing = floor((layout.size.width - sideInset * 2.0 - sideButtonSize.width * CGFloat(buttonsCount)) / (CGFloat(buttonsCount - 1))) + let y = layout.size.height - self.effectiveBottomAreaHeight - layout.intrinsicInsets.bottom + (controlsHidden ? self.effectiveBottomAreaHeight + layout.intrinsicInsets.bottom + 30.0: floor((self.effectiveBottomAreaHeight - sideButtonSize.height) / 2.0)) + let secondButtonFrame: CGRect + if hasCameraButton { + let firstButtonFrame = CGRect(origin: CGPoint(x: sideInset, y: y), size: sideButtonSize) + secondButtonFrame = CGRect(origin: CGPoint(x: firstButtonFrame.maxX + spacing, y: y), size: sideButtonSize) + } else { + secondButtonFrame = CGRect(origin: CGPoint(x: sideInset, y: y), size: sideButtonSize) + } + let actionButtonFrame = CGRect(origin: CGPoint(x: secondButtonFrame.maxX + spacing, y: y), size: sideButtonSize) + return actionButtonFrame.center + } + } + } + } } private let sharedContext: SharedAccountContext @@ -3456,7 +6628,10 @@ public final class VoiceChatController: ViewController { super.init(navigationBarPresentationData: nil) - self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait) + self.automaticallyControlPresentationContextLayout = false + self.blocksBackgroundWhenInOverlay = true + + self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .all) self.statusBar.statusBarStyle = .Ignore @@ -3474,6 +6649,10 @@ public final class VoiceChatController: ViewController { return true } |> filter { $0 }) + + self.scrollToTop = { [weak self] in + self?.controllerNode.scrollToTop() + } } required init(coder aDecoder: NSCoder) { @@ -3484,7 +6663,7 @@ public final class VoiceChatController: ViewController { self.idleTimerExtensionDisposable.dispose() if let currentOverlayController = self.currentOverlayController { - currentOverlayController.animateOut(reclaim: false, completion: { _ in }) + currentOverlayController.animateOut(reclaim: false, targetPosition: CGPoint(), completion: { _ in }) } } @@ -3535,6 +6714,8 @@ public final class VoiceChatController: ViewController { let count = navigationController.viewControllers.count if count == 2 || navigationController.viewControllers[count - 2] is ChatController { if case .active(.cantSpeak) = self.controllerNode.actionButton.stateValue { + } else if case .button = self.controllerNode.actionButton.stateValue { + } else if case .scheduled = self.controllerNode.actionButton.stateValue { } else if let chatController = navigationController.viewControllers[count - 2] as? ChatController, chatController.isSendButtonVisible { } else if let tabBarController = navigationController.viewControllers[count - 2] as? TabBarController, let chatListController = tabBarController.controllers[tabBarController.selectedIndex] as? ChatListController, chatListController.isSearchActive { } else { @@ -3570,13 +6751,13 @@ public final class VoiceChatController: ViewController { return true }) } - + private func detachActionButton() { guard self.currentOverlayController == nil && !self.isDisconnected else { return } - let overlayController = VoiceChatOverlayController(actionButton: self.controllerNode.actionButton, audioOutputNode: self.controllerNode.audioOutputNode, leaveNode: self.controllerNode.leaveNode, navigationController: self.navigationController as? NavigationController, initiallyHidden: self.dismissedManually) + let overlayController = VoiceChatOverlayController(actionButton: self.controllerNode.actionButton, audioOutputNode: self.controllerNode.audioButton, cameraNode: self.controllerNode.cameraButton, leaveNode: self.controllerNode.leaveButton, navigationController: self.navigationController as? NavigationController, initiallyHidden: self.dismissedManually) if let navigationController = self.navigationController as? NavigationController { navigationController.presentOverlay(controller: overlayController, inGlobal: true, blockInteraction: false) } @@ -3586,12 +6767,13 @@ public final class VoiceChatController: ViewController { self.reclaimActionButton = { [weak self, weak overlayController] in if let strongSelf = self { - overlayController?.animateOut(reclaim: true, completion: { immediate in + overlayController?.animateOut(reclaim: true, targetPosition: strongSelf.controllerNode.actionButtonPosition, completion: { immediate in if let strongSelf = self, immediate { strongSelf.controllerNode.actionButton.ignoreHierarchyChanges = true + strongSelf.controllerNode.bottomPanelNode.addSubnode(strongSelf.controllerNode.cameraButton) + strongSelf.controllerNode.bottomPanelNode.addSubnode(strongSelf.controllerNode.audioButton) + strongSelf.controllerNode.bottomPanelNode.addSubnode(strongSelf.controllerNode.leaveButton) strongSelf.controllerNode.bottomPanelNode.addSubnode(strongSelf.controllerNode.actionButton) - strongSelf.controllerNode.bottomPanelNode.addSubnode(strongSelf.controllerNode.audioOutputNode) - strongSelf.controllerNode.bottomPanelNode.addSubnode(strongSelf.controllerNode.leaveNode) if immediate, let layout = strongSelf.validLayout { strongSelf.containerLayoutUpdated(layout, transition: .immediate) @@ -3623,31 +6805,43 @@ public final class VoiceChatController: ViewController { override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { super.containerLayoutUpdated(layout, transition: transition) self.validLayout = layout - self.controllerNode.containerLayoutUpdated(layout, navigationHeight: self.navigationHeight, transition: transition) + self.controllerNode.containerLayoutUpdated(layout, navigationHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition) } } private final class VoiceChatContextExtractedContentSource: ContextExtractedContentSource { var keepInPlace: Bool - let ignoreContentTouches: Bool = true + let ignoreContentTouches: Bool = false let blurBackground: Bool + let maskView: UIView? + + private var animateTransitionIn: () -> Void + private var animateTransitionOut: () -> Void - private let controller: ViewController private let sourceNode: ContextExtractedContentContainingNode - init(controller: ViewController, sourceNode: ContextExtractedContentContainingNode, keepInPlace: Bool, blurBackground: Bool) { - self.controller = controller + var centerVertically: Bool + var shouldBeDismissed: Signal + + init(sourceNode: ContextExtractedContentContainingNode, maskView: UIView?, keepInPlace: Bool, blurBackground: Bool, centerVertically: Bool, shouldBeDismissed: Signal, animateTransitionIn: @escaping () -> Void, animateTransitionOut: @escaping () -> Void) { self.sourceNode = sourceNode + self.maskView = maskView self.keepInPlace = keepInPlace self.blurBackground = blurBackground + self.centerVertically = centerVertically + self.shouldBeDismissed = shouldBeDismissed + self.animateTransitionIn = animateTransitionIn + self.animateTransitionOut = animateTransitionOut } func takeView() -> ContextControllerTakeViewInfo? { - return ContextControllerTakeViewInfo(contentContainingNode: self.sourceNode, contentAreaInScreenSpace: UIScreen.main.bounds) + self.animateTransitionIn() + return ContextControllerTakeViewInfo(contentContainingNode: self.sourceNode, contentAreaInScreenSpace: UIScreen.main.bounds, maskView: self.maskView) } func putBack() -> ContextControllerPutBackViewInfo? { - return ContextControllerPutBackViewInfo(contentAreaInScreenSpace: UIScreen.main.bounds) + self.animateTransitionOut() + return ContextControllerPutBackViewInfo(contentAreaInScreenSpace: UIScreen.main.bounds, maskView: self.maskView) } } diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatFullscreenParticipantItem.swift b/submodules/TelegramCallsUI/Sources/VoiceChatFullscreenParticipantItem.swift new file mode 100644 index 0000000000..4ba7b1c7cb --- /dev/null +++ b/submodules/TelegramCallsUI/Sources/VoiceChatFullscreenParticipantItem.swift @@ -0,0 +1,1004 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import SwiftSignalKit +import Postbox +import TelegramCore +import SyncCore +import TelegramPresentationData +import TelegramUIPreferences +import ItemListUI +import PresentationDataUtils +import AvatarNode +import TelegramStringFormatting +import PeerPresenceStatusManager +import ContextUI +import AccountContext +import LegacyComponents +import AudioBlob +import PeerInfoAvatarListNode + +private let avatarFont = avatarPlaceholderFont(size: floor(50.0 * 16.0 / 37.0)) +private let tileSize = CGSize(width: 84.0, height: 84.0) +private let backgroundCornerRadius: CGFloat = 11.0 +private let videoCornerRadius: CGFloat = 23.0 +private let avatarSize: CGFloat = 50.0 +private let videoSize = CGSize(width: 180.0, height: 180.0) + +private let accentColor: UIColor = UIColor(rgb: 0x007aff) +private let constructiveColor: UIColor = UIColor(rgb: 0x34c759) +private let destructiveColor: UIColor = UIColor(rgb: 0xff3b30) + +private let borderLineWidth: CGFloat = 2.0 + +private let fadeColor = UIColor(rgb: 0x000000, alpha: 0.5) +let fadeHeight: CGFloat = 50.0 + +private var fadeImage: UIImage? = { + return generateImage(CGSize(width: fadeHeight, height: fadeHeight), rotatedContext: { size, context in + let bounds = CGRect(origin: CGPoint(), size: size) + context.clear(bounds) + + let colorsArray = [fadeColor.withAlphaComponent(0.0).cgColor, fadeColor.cgColor] as CFArray + var locations: [CGFloat] = [1.0, 0.0] + let gradient = CGGradient(colorsSpace: deviceColorSpace, colors: colorsArray, locations: &locations)! + context.drawLinearGradient(gradient, start: CGPoint(), end: CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions()) + }) +}() + +final class VoiceChatFullscreenParticipantItem: ListViewItem { + enum Icon { + case none + case microphone(Bool, UIColor) + case invite(Bool) + case wantsToSpeak + } + + enum Color { + case generic + case accent + case constructive + case destructive + } + + let presentationData: ItemListPresentationData + let nameDisplayOrder: PresentationPersonNameOrder + let context: AccountContext + let peer: Peer + let videoEndpointId: String? + let isPaused: Bool + let icon: Icon + let text: VoiceChatParticipantItem.ParticipantText + let textColor: Color + let color: Color + let isLandscape: Bool + let active: Bool + let showVideoWhenActive: Bool + let getAudioLevel: (() -> Signal)? + let getVideo: () -> GroupVideoNode? + let action: ((ASDisplayNode?) -> Void)? + let contextAction: ((ASDisplayNode, ContextGesture?) -> Void)? + let getUpdatingAvatar: () -> Signal<(TelegramMediaImageRepresentation, Float)?, NoError> + + public let selectable: Bool = true + + public init(presentationData: ItemListPresentationData, nameDisplayOrder: PresentationPersonNameOrder, context: AccountContext, peer: Peer, videoEndpointId: String?, isPaused: Bool, icon: Icon, text: VoiceChatParticipantItem.ParticipantText, textColor: Color, color: Color, isLandscape: Bool, active: Bool, showVideoWhenActive: Bool, getAudioLevel: (() -> Signal)?, getVideo: @escaping () -> GroupVideoNode?, action: ((ASDisplayNode?) -> Void)?, contextAction: ((ASDisplayNode, ContextGesture?) -> Void)? = nil, getUpdatingAvatar: @escaping () -> Signal<(TelegramMediaImageRepresentation, Float)?, NoError>) { + self.presentationData = presentationData + self.nameDisplayOrder = nameDisplayOrder + self.context = context + self.peer = peer + self.videoEndpointId = videoEndpointId + self.isPaused = isPaused + self.icon = icon + self.text = text + self.textColor = textColor + self.color = color + self.isLandscape = isLandscape + self.active = active + self.showVideoWhenActive = showVideoWhenActive + self.getAudioLevel = getAudioLevel + self.getVideo = getVideo + self.action = action + self.contextAction = contextAction + self.getUpdatingAvatar = getUpdatingAvatar + } + + 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 = VoiceChatFullscreenParticipantItemNode() + let (layout, apply) = node.asyncLayout()(self, params, previousItem == nil, nextItem == nil) + + node.contentSize = layout.contentSize + node.insets = layout.insets + + Queue.mainQueue().async { + completion(node, { + return (node.avatarNode.ready, { _ in apply(synchronousLoads, 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 { + if let nodeValue = node() as? VoiceChatFullscreenParticipantItemNode { + let makeLayout = nodeValue.asyncLayout() + + var animated = true + if case .None = animation { + animated = false + } + + async { + let (layout, apply) = makeLayout(self, params, previousItem == nil, nextItem == nil) + Queue.mainQueue().async { + completion(layout, { _ in + apply(false, animated) + }) + } + } + } + } + } + + public func selected(listView: ListView) { + listView.clearHighlightAnimated(true) + } +} + +class VoiceChatFullscreenParticipantItemNode: ItemListRevealOptionsItemNode { + let contextSourceNode: ContextExtractedContentContainingNode + private let containerNode: ContextControllerSourceNode + let backgroundImageNode: ASImageNode + private let extractedBackgroundImageNode: ASImageNode + let offsetContainerNode: ASDisplayNode + let highlightNode: VoiceChatTileHighlightNode + + private var extractedRect: CGRect? + private var nonExtractedRect: CGRect? + private var extractedVerticalOffset: CGFloat? + + let avatarNode: AvatarNode + let contentWrapperNode: ASDisplayNode + private let titleNode: TextNode + private let statusNode: VoiceChatParticipantStatusNode + private var credibilityIconNode: ASImageNode? + + private let actionContainerNode: ASDisplayNode + private var animationNode: VoiceChatMicrophoneNode? + private var iconNode: ASImageNode? + private var raiseHandNode: VoiceChatRaiseHandNode? + private var actionButtonNode: HighlightableButtonNode + + var audioLevelView: VoiceBlobView? + private let audioLevelDisposable = MetaDisposable() + private var didSetupAudioLevel = false + + private var absoluteLocation: (CGRect, CGSize)? + + private var layoutParams: (VoiceChatFullscreenParticipantItem, ListViewItemLayoutParams, Bool, Bool)? + private var isExtracted = false + private var animatingExtraction = false + private var animatingSelection = false + private var wavesColor: UIColor? + + let videoContainerNode: ASDisplayNode + let videoFadeNode: ASDisplayNode + var videoNode: GroupVideoNode? + + private var profileNode: VoiceChatPeerProfileNode? + + private var raiseHandTimer: SwiftSignalKit.Timer? + private var silenceTimer: SwiftSignalKit.Timer? + + var item: VoiceChatFullscreenParticipantItem? { + return self.layoutParams?.0 + } + + init() { + self.contextSourceNode = ContextExtractedContentContainingNode() + self.containerNode = ContextControllerSourceNode() + + self.backgroundImageNode = ASImageNode() + self.backgroundImageNode.clipsToBounds = true + self.backgroundImageNode.displaysAsynchronously = false + self.backgroundImageNode.alpha = 0.0 + + self.extractedBackgroundImageNode = ASImageNode() + self.extractedBackgroundImageNode.clipsToBounds = true + self.extractedBackgroundImageNode.displaysAsynchronously = false + self.extractedBackgroundImageNode.alpha = 0.0 + + self.highlightNode = VoiceChatTileHighlightNode() + self.highlightNode.isHidden = true + + self.offsetContainerNode = ASDisplayNode() + + self.avatarNode = AvatarNode(font: avatarFont) + self.avatarNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: avatarSize, height: avatarSize)) + + self.contentWrapperNode = ASDisplayNode() + + self.videoContainerNode = ASDisplayNode() + self.videoContainerNode.clipsToBounds = true + + self.videoFadeNode = ASDisplayNode() + self.videoFadeNode.displaysAsynchronously = false + if let image = fadeImage { + self.videoFadeNode.backgroundColor = UIColor(patternImage: image) + } + self.videoContainerNode.addSubnode(self.videoFadeNode) + + self.titleNode = TextNode() + self.titleNode.displaysAsynchronously = false + self.titleNode.isUserInteractionEnabled = false + self.titleNode.contentMode = .left + self.titleNode.contentsScale = UIScreen.main.scale + + self.statusNode = VoiceChatParticipantStatusNode() + + self.actionContainerNode = ASDisplayNode() + self.actionButtonNode = HighlightableButtonNode() + + super.init(layerBacked: false, dynamicBounce: false, rotated: false, seeThrough: false) + + self.isAccessibilityElement = true + + self.containerNode.addSubnode(self.contextSourceNode) + self.containerNode.targetNodeForActivationProgress = self.contextSourceNode.contentNode + self.addSubnode(self.containerNode) + + self.contextSourceNode.contentNode.addSubnode(self.backgroundImageNode) + self.backgroundImageNode.addSubnode(self.extractedBackgroundImageNode) + self.contextSourceNode.contentNode.addSubnode(self.offsetContainerNode) + self.offsetContainerNode.addSubnode(self.videoContainerNode) + self.offsetContainerNode.addSubnode(self.contentWrapperNode) + self.contentWrapperNode.addSubnode(self.titleNode) + self.contentWrapperNode.addSubnode(self.actionContainerNode) + self.actionContainerNode.addSubnode(self.actionButtonNode) + self.offsetContainerNode.addSubnode(self.avatarNode) + self.contextSourceNode.contentNode.addSubnode(self.highlightNode) + self.containerNode.targetNodeForActivationProgress = self.contextSourceNode.contentNode + + self.containerNode.shouldBegin = { [weak self] location in + guard let _ = self else { + return false + } + return true + } + self.containerNode.activated = { [weak self] gesture, _ in + guard let strongSelf = self, let item = strongSelf.layoutParams?.0, let contextAction = item.contextAction else { + gesture.cancel() + return + } + contextAction(strongSelf.contextSourceNode, gesture) + } + self.contextSourceNode.willUpdateIsExtractedToContextPreview = { [weak self] isExtracted, transition in + guard let strongSelf = self, let _ = strongSelf.item else { + return + } + strongSelf.updateIsExtracted(isExtracted, transition: transition) + } + } + + deinit { + self.audioLevelDisposable.dispose() + self.raiseHandTimer?.invalidate() + self.silenceTimer?.invalidate() + } + + override func selected() { + super.selected() + if self.animatingSelection { + return + } + self.layoutParams?.0.action?(self.contextSourceNode) + } + + func transitionIn(from sourceNode: ASDisplayNode?) { + guard let item = self.item else { + return + } + let active = item.active && !item.showVideoWhenActive + + var videoNode: GroupVideoNode? + if let sourceNode = sourceNode as? VoiceChatTileItemNode { + if let sourceVideoNode = sourceNode.videoNode { + sourceNode.videoNode = nil + videoNode = sourceVideoNode + } + } + + if videoNode == nil { + videoNode = item.getVideo() + } + + if videoNode?.isMainstageExclusive == true && active { + videoNode = nil + } + + if let videoNode = videoNode { + if active { + self.avatarNode.alpha = 1.0 + videoNode.alpha = 0.0 + } else { + self.avatarNode.alpha = 0.0 + videoNode.alpha = 1.0 + } + self.videoNode = videoNode + self.videoContainerNode.insertSubnode(videoNode, at: 0) + + videoNode.updateLayout(size: videoSize, layoutMode: .fillOrFitToSquare, transition: .immediate) + videoNode.frame = CGRect(origin: CGPoint(), size: videoSize) + } + } + + var gridVisibility = true { + didSet { + self.updateIsEnabled() + } + } + + func updateIsEnabled() { + guard let (rect, containerSize) = self.absoluteLocation else { + return + } + let isVisibleInContainer = rect.maxY >= 0.0 && rect.minY <= containerSize.height + if let videoNode = self.videoNode, videoNode.supernode === self.videoContainerNode { + videoNode.updateIsEnabled(self.gridVisibility && isVisibleInContainer) + } + } + + private func updateIsExtracted(_ isExtracted: Bool, transition: ContainedViewLayoutTransition) { + guard self.isExtracted != isExtracted, let extractedRect = self.extractedRect, let nonExtractedRect = self.nonExtractedRect, let item = self.item else { + return + } + self.isExtracted = isExtracted + + if item.peer.smallProfileImage != nil { + let springDuration: Double = 0.42 + let springDamping: CGFloat = 124.0 + + if isExtracted { + var hasVideo = false + if let videoNode = self.videoNode, videoNode.supernode == self.videoContainerNode, !videoNode.alpha.isZero { + hasVideo = true + } + let profileNode = VoiceChatPeerProfileNode(context: item.context, size: extractedRect.size, sourceSize: nonExtractedRect.size, peer: item.peer, text: item.text, customNode: hasVideo ? self.videoContainerNode : nil, additionalEntry: .single(nil), requestDismiss: { [weak self] in + self?.contextSourceNode.requestDismiss?() + }) + profileNode.frame = CGRect(origin: CGPoint(), size: extractedRect.size) + self.profileNode = profileNode + self.contextSourceNode.contentNode.addSubnode(profileNode) + + profileNode.animateIn(from: self, targetRect: extractedRect, transition: transition) + var appearenceTransition = transition + if transition.isAnimated { + appearenceTransition = .animated(duration: springDuration, curve: .customSpring(damping: springDamping, initialVelocity: 0.0)) + } + appearenceTransition.updateFrame(node: profileNode, frame: extractedRect) + + self.contextSourceNode.contentNode.customHitTest = { [weak self] point in + if let strongSelf = self, let profileNode = strongSelf.profileNode { + if profileNode.avatarListWrapperNode.frame.contains(point) { + return profileNode.avatarListNode.view + } + } + return nil + } + self.highlightNode.isHidden = true + self.backgroundImageNode.isHidden = true + } else if let profileNode = self.profileNode { + self.profileNode = nil + profileNode.animateOut(to: self, targetRect: nonExtractedRect, transition: transition, completion: { [weak self] in + self?.backgroundImageNode.isHidden = false + }) + + var appearenceTransition = transition + if transition.isAnimated { + appearenceTransition = .animated(duration: 0.2, curve: .easeInOut) + } + appearenceTransition.updateFrame(node: profileNode, frame: nonExtractedRect) + + self.contextSourceNode.contentNode.customHitTest = nil + self.highlightNode.isHidden = !item.active + } + } + } + + func asyncLayout() -> (_ item: VoiceChatFullscreenParticipantItem, _ params: ListViewItemLayoutParams, _ first: Bool, _ last: Bool) -> (ListViewItemNodeLayout, (Bool, Bool) -> Void) { + let makeTitleLayout = TextNode.asyncLayout(self.titleNode) + let makeStatusLayout = self.statusNode.asyncLayout() + + let currentItem = self.layoutParams?.0 + var hasVideo = self.videoNode != nil + + return { item, params, first, last in + let titleFont = Font.semibold(13.0) + var titleAttributedString: NSAttributedString? + + if !hasVideo && item.videoEndpointId != nil { + hasVideo = true + } + let active = item.active && !item.showVideoWhenActive + + var titleColor = item.presentationData.theme.list.itemPrimaryTextColor + if !hasVideo || active { + switch item.textColor { + case .generic: + titleColor = item.presentationData.theme.list.itemPrimaryTextColor + case .accent: + titleColor = item.presentationData.theme.list.itemAccentColor + case .constructive: + titleColor = constructiveColor + case .destructive: + titleColor = destructiveColor + } + } + let currentBoldFont: UIFont = titleFont + + if let user = item.peer as? TelegramUser { + if let firstName = user.firstName, let lastName = user.lastName, !firstName.isEmpty, !lastName.isEmpty { + titleAttributedString = NSAttributedString(string: firstName, font: titleFont, textColor: titleColor) + } else if let firstName = user.firstName, !firstName.isEmpty { + titleAttributedString = NSAttributedString(string: firstName, font: currentBoldFont, textColor: titleColor) + } else if let lastName = user.lastName, !lastName.isEmpty { + titleAttributedString = NSAttributedString(string: lastName, font: currentBoldFont, textColor: titleColor) + } else { + 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) + } else if let channel = item.peer as? TelegramChannel { + titleAttributedString = NSAttributedString(string: channel.title, font: currentBoldFont, textColor: titleColor) + } + + var wavesColor = UIColor(rgb: 0x34c759) + var gradient: VoiceChatTileHighlightNode.Gradient = .active + switch item.color { + case .accent: + wavesColor = accentColor + if case .wantsToSpeak = item.icon { + gradient = .muted + } + case .constructive: + gradient = .speaking + case .destructive: + gradient = .mutedForYou + wavesColor = destructiveColor + default: + break + } + var titleUpdated = false + if let currentColor = currentItem?.textColor, currentColor != item.textColor { + titleUpdated = true + } + + let leftInset: CGFloat = 58.0 + params.leftInset + + var titleIconsWidth: CGFloat = 0.0 + var currentCredibilityIconImage: UIImage? + var credibilityIconOffset: CGFloat = 0.0 + if item.peer.isScam { + currentCredibilityIconImage = PresentationResourcesChatList.scamIcon(item.presentationData.theme, strings: item.presentationData.strings, type: .regular) + credibilityIconOffset = 2.0 + } else if item.peer.isFake { + currentCredibilityIconImage = PresentationResourcesChatList.fakeIcon(item.presentationData.theme, strings: item.presentationData.strings, type: .regular) + credibilityIconOffset = 2.0 + } else if item.peer.isVerified { + currentCredibilityIconImage = PresentationResourcesChatList.verifiedIcon(item.presentationData.theme) + credibilityIconOffset = 3.0 + } + + if let currentCredibilityIconImage = currentCredibilityIconImage { + titleIconsWidth += 4.0 + currentCredibilityIconImage.size.width + } + + let constrainedWidth = params.width - 24.0 - 10.0 + let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: constrainedWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + + let availableWidth = params.availableHeight + let (statusLayout, _) = makeStatusLayout(CGSize(width: availableWidth - 30.0, height: CGFloat.greatestFiniteMagnitude), item.text, true) + + let contentSize = tileSize + let insets = UIEdgeInsets(top: 0.0, left: 0.0, bottom: !last ? 6.0 : 0.0, right: 0.0) + + let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) + + return (layout, { [weak self] synchronousLoad, animated in + if let strongSelf = self { + let hadItem = strongSelf.layoutParams?.0 != nil + strongSelf.layoutParams = (item, params, first, last) + strongSelf.wavesColor = wavesColor + + let videoContainerScale = tileSize.width / videoSize.width + + let appearanceDuration: Double = 0.25 + let apperanceTransition = ContainedViewLayoutTransition.animated(duration: appearanceDuration, curve: .easeInOut) + let videoNode = item.getVideo() + if let currentVideoNode = strongSelf.videoNode, currentVideoNode !== videoNode { + if videoNode == nil { + let snapshotView = currentVideoNode.snapshotView + if strongSelf.avatarNode.alpha.isZero { + strongSelf.animatingSelection = true + strongSelf.videoContainerNode.layer.animateScale(from: videoContainerScale, to: 0.001, duration: appearanceDuration, completion: { _ in + snapshotView?.removeFromSuperview() + }) + strongSelf.avatarNode.layer.animateScale(from: 0.0, to: 1.0, duration: appearanceDuration, completion: { [weak self] _ in + self?.animatingSelection = false + }) + strongSelf.videoContainerNode.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: -9.0), duration: appearanceDuration, additive: true) + strongSelf.audioLevelView?.layer.animateScale(from: 0.0, to: 1.0, duration: appearanceDuration) + } + if currentVideoNode.supernode === strongSelf.videoContainerNode { + apperanceTransition.updateAlpha(node: currentVideoNode, alpha: 0.0) + } else if let snapshotView = snapshotView { + strongSelf.videoContainerNode.view.insertSubview(snapshotView, at: 0) + apperanceTransition.updateAlpha(layer: snapshotView.layer, alpha: 0.0) + } + apperanceTransition.updateAlpha(node: strongSelf.videoFadeNode, alpha: 0.0) + apperanceTransition.updateAlpha(node: strongSelf.avatarNode, alpha: 1.0) + if let audioLevelView = strongSelf.audioLevelView { + apperanceTransition.updateAlpha(layer: audioLevelView.layer, alpha: 1.0) + } + } else { + if currentItem?.peer.id == item.peer.id { + currentVideoNode.layer.animateScale(from: 1.0, to: 0.0, duration: appearanceDuration, removeOnCompletion: false, completion: { [weak self, weak currentVideoNode] _ in + currentVideoNode?.layer.removeAllAnimations() + if currentVideoNode !== self?.videoNode { + currentVideoNode?.removeFromSupernode() + } + }) + } else { + currentVideoNode.removeFromSupernode() + } + } + } + + let videoNodeUpdated = strongSelf.videoNode !== videoNode + strongSelf.videoNode = videoNode + + videoNode?.updateIsBlurred(isBlurred: item.isPaused, light: true) + + let nonExtractedRect: CGRect + let avatarFrame: CGRect + let titleFrame: CGRect + let animationSize: CGSize + let animationFrame: CGRect + let animationScale: CGFloat + + nonExtractedRect = CGRect(origin: CGPoint(), size: layout.contentSize) + strongSelf.containerNode.transform = CATransform3DMakeRotation(item.isLandscape ? 0.0 : CGFloat.pi / 2.0, 0.0, 0.0, 1.0) + avatarFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - avatarSize) / 2.0), y: 7.0), size: CGSize(width: avatarSize, height: avatarSize)) + + animationSize = CGSize(width: 36.0, height: 36.0) + animationScale = 0.66667 + animationFrame = CGRect(x: layout.size.width - 29.0, y: 55.0, width: 24.0, height: 24.0) + titleFrame = CGRect(origin: CGPoint(x: 8.0, y: 63.0), size: titleLayout.size) + + let extractedWidth = availableWidth + var extractedRect = CGRect(x: 0.0, y: 0.0, width: extractedWidth, height: extractedWidth + statusLayout.height + 39.0) + if item.peer.smallProfileImage == nil { + extractedRect = nonExtractedRect + } + strongSelf.extractedRect = extractedRect + strongSelf.nonExtractedRect = nonExtractedRect + + strongSelf.backgroundImageNode.frame = nonExtractedRect + + if strongSelf.backgroundImageNode.image == nil { + strongSelf.backgroundImageNode.image = generateStretchableFilledCircleImage(diameter: backgroundCornerRadius * 2.0, color: UIColor(rgb: 0x1c1c1e)) + strongSelf.backgroundImageNode.alpha = 1.0 + } + strongSelf.extractedBackgroundImageNode.frame = strongSelf.backgroundImageNode.bounds + strongSelf.contextSourceNode.contentRect = extractedRect + + let contentBounds = CGRect(origin: CGPoint(), size: layout.contentSize) + strongSelf.containerNode.frame = contentBounds + strongSelf.contextSourceNode.frame = contentBounds + strongSelf.contentWrapperNode.frame = contentBounds + strongSelf.offsetContainerNode.frame = contentBounds + strongSelf.contextSourceNode.contentNode.frame = contentBounds + strongSelf.actionContainerNode.frame = contentBounds + strongSelf.highlightNode.frame = contentBounds + strongSelf.highlightNode.updateLayout(size: contentBounds.size, transition: .immediate) + + strongSelf.containerNode.isGestureEnabled = item.contextAction != nil + + strongSelf.accessibilityLabel = titleAttributedString?.string + var combinedValueString = "" +// if let statusString = statusAttributedString?.string, !statusString.isEmpty { +// combinedValueString.append(statusString) +// } + + strongSelf.accessibilityValue = combinedValueString + + let transition: ContainedViewLayoutTransition + if animated && hadItem { + transition = ContainedViewLayoutTransition.animated(duration: 0.3, curve: .easeInOut) + } else { + transition = .immediate + } + + if titleUpdated, let snapshotView = strongSelf.titleNode.view.snapshotContentTree() { + strongSelf.titleNode.view.superview?.addSubview(snapshotView) + snapshotView.frame = strongSelf.titleNode.view.frame + snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.18, removeOnCompletion: false, completion: { [weak snapshotView] _ in + snapshotView?.removeFromSuperview() + }) + } + + let _ = titleApply() + transition.updateFrame(node: strongSelf.titleNode, frame: titleFrame) + + if let currentCredibilityIconImage = currentCredibilityIconImage { + let iconNode: ASImageNode + if let current = strongSelf.credibilityIconNode { + iconNode = current + } else { + iconNode = ASImageNode() + iconNode.isLayerBacked = true + iconNode.displaysAsynchronously = false + iconNode.displayWithoutProcessing = true + strongSelf.offsetContainerNode.addSubnode(iconNode) + strongSelf.credibilityIconNode = iconNode + } + iconNode.image = currentCredibilityIconImage + transition.updateFrame(node: iconNode, frame: CGRect(origin: CGPoint(x: leftInset + titleLayout.size.width + 3.0, y: credibilityIconOffset), size: currentCredibilityIconImage.size)) + } else if let credibilityIconNode = strongSelf.credibilityIconNode { + strongSelf.credibilityIconNode = nil + credibilityIconNode.removeFromSupernode() + } + + transition.updateFrameAsPositionAndBounds(node: strongSelf.avatarNode, frame: avatarFrame) + + strongSelf.highlightNode.updateGlowAndGradientAnimations(type: gradient, animated: true) + + let blobFrame = avatarFrame.insetBy(dx: -18.0, dy: -18.0) + if let getAudioLevel = item.getAudioLevel { + if !strongSelf.didSetupAudioLevel || currentItem?.peer.id != item.peer.id { + strongSelf.audioLevelView?.frame = blobFrame + strongSelf.didSetupAudioLevel = true + strongSelf.audioLevelDisposable.set((getAudioLevel() + |> deliverOnMainQueue).start(next: { value in + guard let strongSelf = self else { + return + } + + strongSelf.highlightNode.updateLevel(CGFloat(value)) + + if strongSelf.audioLevelView == nil, value > 0.0 { + let audioLevelView = VoiceBlobView( + frame: blobFrame, + maxLevel: 1.5, + smallBlobRange: (0, 0), + mediumBlobRange: (0.69, 0.87), + bigBlobRange: (0.71, 1.0) + ) + + let maskRect = CGRect(origin: .zero, size: blobFrame.size) + let playbackMaskLayer = CAShapeLayer() + playbackMaskLayer.frame = maskRect + playbackMaskLayer.fillRule = .evenOdd + let maskPath = UIBezierPath() + maskPath.append(UIBezierPath(roundedRect: maskRect.insetBy(dx: 18, dy: 18), cornerRadius: 22)) + maskPath.append(UIBezierPath(rect: maskRect)) + playbackMaskLayer.path = maskPath.cgPath + audioLevelView.layer.mask = playbackMaskLayer + + audioLevelView.setColor(wavesColor) + audioLevelView.alpha = strongSelf.isExtracted ? 0.0 : 1.0 + + strongSelf.audioLevelView = audioLevelView + strongSelf.offsetContainerNode.view.insertSubview(audioLevelView, at: 0) + + if let item = strongSelf.item, strongSelf.videoNode != nil && !active { + audioLevelView.alpha = 0.0 + } + } + + let level = min(1.0, max(0.0, CGFloat(value))) + if let audioLevelView = strongSelf.audioLevelView { + audioLevelView.updateLevel(CGFloat(value)) + + let avatarScale: CGFloat + if value > 0.02 { + audioLevelView.startAnimating() + avatarScale = 1.03 + level * 0.13 + + if let silenceTimer = strongSelf.silenceTimer { + silenceTimer.invalidate() + strongSelf.silenceTimer = nil + } + } else { + avatarScale = 1.0 + if strongSelf.silenceTimer == nil { + let silenceTimer = SwiftSignalKit.Timer(timeout: 1.0, repeat: false, completion: { [weak self] in + self?.audioLevelView?.stopAnimating(duration: 0.75) + self?.silenceTimer = nil + }, queue: Queue.mainQueue()) + strongSelf.silenceTimer = silenceTimer + silenceTimer.start() + } + } + + if let wavesColor = strongSelf.wavesColor { + audioLevelView.setColor(wavesColor, animated: true) + } + + if !strongSelf.animatingSelection { + let transition: ContainedViewLayoutTransition = .animated(duration: 0.15, curve: .easeInOut) + transition.updateTransformScale(node: strongSelf.avatarNode, scale: strongSelf.isExtracted ? 1.0 : avatarScale, beginWithCurrentState: true) + } + } + })) + } + } else if let audioLevelView = strongSelf.audioLevelView { + strongSelf.audioLevelView = nil + audioLevelView.removeFromSuperview() + + strongSelf.audioLevelDisposable.set(nil) + } + + var overrideImage: AvatarNodeImageOverride? + if item.peer.isDeleted { + overrideImage = .deletedIcon + } + strongSelf.avatarNode.setPeer(context: item.context, theme: item.presentationData.theme, peer: item.peer, overrideImage: overrideImage, emptyColor: item.presentationData.theme.list.mediaPlaceholderColor, synchronousLoad: synchronousLoad, storeUnrounded: true) + + var hadMicrophoneNode = false + var hadRaiseHandNode = false + var hadIconNode = false + var nodeToAnimateIn: ASDisplayNode? + + if case let .microphone(muted, color) = item.icon { + let animationNode: VoiceChatMicrophoneNode + if let current = strongSelf.animationNode { + animationNode = current + } else { + animationNode = VoiceChatMicrophoneNode() + strongSelf.animationNode = animationNode + strongSelf.actionButtonNode.addSubnode(animationNode) + + nodeToAnimateIn = animationNode + } + var color = color + if (hasVideo && !active) || color.rgb == 0x979797 { + color = UIColor(rgb: 0xffffff) + } + animationNode.update(state: VoiceChatMicrophoneNode.State(muted: muted, filled: true, color: color), animated: true) + strongSelf.actionButtonNode.isUserInteractionEnabled = false + } else if let animationNode = strongSelf.animationNode { + hadMicrophoneNode = true + strongSelf.animationNode = nil + animationNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) + animationNode.layer.animateScale(from: 1.0, to: 0.001, duration: 0.2, removeOnCompletion: false, completion: { [weak animationNode] _ in + animationNode?.removeFromSupernode() + }) + } + + if case .wantsToSpeak = item.icon { + let raiseHandNode: VoiceChatRaiseHandNode + if let current = strongSelf.raiseHandNode { + raiseHandNode = current + } else { + raiseHandNode = VoiceChatRaiseHandNode(color: item.presentationData.theme.list.itemAccentColor) + raiseHandNode.contentMode = .center + strongSelf.raiseHandNode = raiseHandNode + strongSelf.actionButtonNode.addSubnode(raiseHandNode) + + nodeToAnimateIn = raiseHandNode + raiseHandNode.playRandomAnimation() + + strongSelf.raiseHandTimer = SwiftSignalKit.Timer(timeout: Double.random(in: 8.0 ... 10.5), repeat: true, completion: { + self?.raiseHandNode?.playRandomAnimation() + }, queue: Queue.mainQueue()) + strongSelf.raiseHandTimer?.start() + } + strongSelf.actionButtonNode.isUserInteractionEnabled = false + } else if let raiseHandNode = strongSelf.raiseHandNode { + hadRaiseHandNode = true + strongSelf.raiseHandNode = nil + if let raiseHandTimer = strongSelf.raiseHandTimer { + strongSelf.raiseHandTimer = nil + raiseHandTimer.invalidate() + } + raiseHandNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) + raiseHandNode.layer.animateScale(from: 1.0, to: 0.001, duration: 0.2, removeOnCompletion: false, completion: { [weak raiseHandNode] _ in + raiseHandNode?.removeFromSupernode() + }) + } + + if case let .invite(invited) = item.icon { + let iconNode: ASImageNode + if let current = strongSelf.iconNode { + iconNode = current + } else { + iconNode = ASImageNode() + iconNode.contentMode = .center + strongSelf.iconNode = iconNode + strongSelf.actionButtonNode.addSubnode(iconNode) + + nodeToAnimateIn = iconNode + } + + if invited { + iconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Call/Context Menu/Invited"), color: UIColor(rgb: 0x979797)) + } else { + iconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/AddUser"), color: item.presentationData.theme.list.itemAccentColor) + } + strongSelf.actionButtonNode.isUserInteractionEnabled = false + } else if let iconNode = strongSelf.iconNode { + hadIconNode = true + strongSelf.iconNode = nil + iconNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) + iconNode.layer.animateScale(from: 1.0, to: 0.001, duration: 0.2, removeOnCompletion: false, completion: { [weak iconNode] _ in + iconNode?.removeFromSupernode() + }) + } + + if let node = nodeToAnimateIn, hadMicrophoneNode || hadRaiseHandNode || hadIconNode { + node.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + node.layer.animateScale(from: 0.001, to: 1.0, duration: 0.2) + } + + if !strongSelf.isExtracted && !strongSelf.animatingExtraction && strongSelf.videoContainerNode.supernode == strongSelf.offsetContainerNode { + strongSelf.videoFadeNode.frame = CGRect(x: 0.0, y: videoSize.height - fadeHeight, width: videoSize.width, height: fadeHeight) + strongSelf.videoContainerNode.bounds = CGRect(origin: CGPoint(), size: videoSize) + + if let videoNode = strongSelf.videoNode { + strongSelf.videoFadeNode.alpha = videoNode.alpha + } else { + strongSelf.videoFadeNode.alpha = 0.0 + } + strongSelf.videoContainerNode.position = CGPoint(x: tileSize.width / 2.0, y: tileSize.height / 2.0) + strongSelf.videoContainerNode.cornerRadius = videoCornerRadius + strongSelf.videoContainerNode.transform = CATransform3DMakeScale(videoContainerScale, videoContainerScale, 1.0) + + strongSelf.highlightNode.isHidden = !item.active + } + + let canUpdateAvatarVisibility = !strongSelf.isExtracted && !strongSelf.animatingExtraction + + if let videoNode = videoNode { + if !strongSelf.isExtracted && !strongSelf.animatingExtraction { + if currentItem != nil { + if active { + if strongSelf.avatarNode.alpha.isZero { + strongSelf.animatingSelection = true + strongSelf.videoContainerNode.layer.animateScale(from: videoContainerScale, to: 0.001, duration: appearanceDuration) + strongSelf.avatarNode.layer.animateScale(from: 0.0, to: 1.0, duration: appearanceDuration, completion: { [weak self] _ in + self?.animatingSelection = false + }) + strongSelf.videoContainerNode.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: -9.0), duration: appearanceDuration, additive: true) + strongSelf.audioLevelView?.layer.animateScale(from: 0.0, to: 1.0, duration: appearanceDuration) + } + if videoNodeUpdated { + videoNode.alpha = 0.0 + strongSelf.videoFadeNode.alpha = 0.0 + } else { + apperanceTransition.updateAlpha(node: videoNode, alpha: 0.0) + apperanceTransition.updateAlpha(node: strongSelf.videoFadeNode, alpha: 0.0) + } + apperanceTransition.updateAlpha(node: strongSelf.avatarNode, alpha: 1.0) + if let audioLevelView = strongSelf.audioLevelView { + apperanceTransition.updateAlpha(layer: audioLevelView.layer, alpha: 1.0) + } + } else { + if !strongSelf.avatarNode.alpha.isZero { + strongSelf.videoContainerNode.layer.animateScale(from: 0.001, to: videoContainerScale, duration: appearanceDuration) + strongSelf.avatarNode.layer.animateScale(from: 1.0, to: 0.001, duration: appearanceDuration) + strongSelf.audioLevelView?.layer.animateScale(from: 1.0, to: 0.001, duration: appearanceDuration) + strongSelf.videoContainerNode.layer.animatePosition(from: CGPoint(x: 0.0, y: -9.0), to: CGPoint(), duration: appearanceDuration, additive: true) + } + if videoNode.supernode === strongSelf.videoContainerNode { + apperanceTransition.updateAlpha(node: videoNode, alpha: 1.0) + } + apperanceTransition.updateAlpha(node: strongSelf.videoFadeNode, alpha: 1.0) + apperanceTransition.updateAlpha(node: strongSelf.avatarNode, alpha: 0.0) + if let audioLevelView = strongSelf.audioLevelView { + apperanceTransition.updateAlpha(layer: audioLevelView.layer, alpha: 0.0) + } + } + } else { + if active { + videoNode.alpha = 0.0 + if canUpdateAvatarVisibility { + strongSelf.avatarNode.alpha = 1.0 + } + } else { + videoNode.alpha = 1.0 + strongSelf.avatarNode.alpha = 0.0 + } + } + } + + videoNode.updateLayout(size: videoSize, layoutMode: .fillOrFitToSquare, transition: .immediate) + if !strongSelf.isExtracted && !strongSelf.animatingExtraction { + if videoNode.supernode !== strongSelf.videoContainerNode { + videoNode.clipsToBounds = true + strongSelf.videoContainerNode.insertSubnode(videoNode, at: 0) + } + + videoNode.position = CGPoint(x: videoSize.width / 2.0, y: videoSize.height / 2.0) + videoNode.bounds = CGRect(origin: CGPoint(), size: videoSize) + } + + if let _ = currentItem, videoNodeUpdated { + if active { + if canUpdateAvatarVisibility { + strongSelf.avatarNode.alpha = 1.0 + } + videoNode.alpha = 0.0 + } else { + strongSelf.animatingSelection = true + let previousAvatarNodeAlpha = strongSelf.avatarNode.alpha + strongSelf.avatarNode.alpha = 0.0 + strongSelf.avatarNode.layer.animateAlpha(from: previousAvatarNodeAlpha, to: 0.0, duration: appearanceDuration) + videoNode.layer.animateScale(from: 0.01, to: 1.0, duration: appearanceDuration, completion: { [weak self] _ in + self?.animatingSelection = false + }) + videoNode.alpha = 1.0 + } + } else { + if active { + if canUpdateAvatarVisibility { + strongSelf.avatarNode.alpha = 1.0 + } + videoNode.alpha = 0.0 + } else { + strongSelf.avatarNode.alpha = 0.0 + videoNode.alpha = 1.0 + } + } + } else if canUpdateAvatarVisibility { + strongSelf.avatarNode.alpha = 1.0 + } + + strongSelf.iconNode?.frame = CGRect(origin: CGPoint(), size: animationSize) + strongSelf.animationNode?.frame = CGRect(origin: CGPoint(), size: animationSize) + strongSelf.raiseHandNode?.frame = CGRect(origin: CGPoint(), size: animationSize).insetBy(dx: -6.0, dy: -6.0).offsetBy(dx: -2.0, dy: 0.0) + + strongSelf.actionButtonNode.transform = CATransform3DMakeScale(animationScale, animationScale, 1.0) + transition.updateFrame(node: strongSelf.actionButtonNode, frame: animationFrame) + + strongSelf.updateIsHighlighted(transition: transition) + } + }) + } + } + + var isHighlighted = false + func updateIsHighlighted(transition: ContainedViewLayoutTransition) { + + } + + override func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) { + super.setHighlighted(highlighted, at: point, animated: animated) + + self.isHighlighted = highlighted + + self.updateIsHighlighted(transition: (animated && !highlighted) ? .animated(duration: 0.3, curve: .easeInOut) : .immediate) + } + + 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 headers() -> [ListViewItemHeader]? { + return nil + } + + override func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) { + var rect = rect + rect.origin.y += self.insets.top + self.absoluteLocation = (rect, containerSize) + + self.updateIsEnabled() + } +} diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatOptionsButton.swift b/submodules/TelegramCallsUI/Sources/VoiceChatHeaderButton.swift similarity index 68% rename from submodules/TelegramCallsUI/Sources/VoiceChatOptionsButton.swift rename to submodules/TelegramCallsUI/Sources/VoiceChatHeaderButton.swift index f5cdd8f9c6..92b9af8cb2 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatOptionsButton.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatHeaderButton.swift @@ -6,6 +6,7 @@ import Postbox import AccountContext import TelegramPresentationData import AvatarNode +import AnimationUI func optionsBackgroundImage(dark: Bool) -> UIImage? { return generateImage(CGSize(width: 28.0, height: 28.0), contextGenerator: { size, context in @@ -16,17 +17,34 @@ func optionsBackgroundImage(dark: Bool) -> UIImage? { })?.stretchableImage(withLeftCapWidth: 14, topCapHeight: 14) } -func optionsButtonImage(dark: Bool) -> UIImage? { +func optionsCircleImage(dark: Bool) -> UIImage? { return generateImage(CGSize(width: 28.0, height: 28.0), contextGenerator: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) context.setFillColor(UIColor(rgb: dark ? 0x1c1c1e : 0x2c2c2e).cgColor) context.fillEllipse(in: CGRect(origin: CGPoint(), size: size)) + }) +} + +func panelButtonImage(dark: Bool) -> UIImage? { + return generateImage(CGSize(width: 38.0, height: 28.0), contextGenerator: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + + context.addPath(UIBezierPath(roundedRect: CGRect(origin: CGPoint(), size: size), cornerRadius: 14.0).cgPath) + context.setFillColor(UIColor(rgb: dark ? 0x1c1c1e : 0x2c2c2e).cgColor) + context.fillPath() context.setFillColor(UIColor.white.cgColor) - context.fillEllipse(in: CGRect(x: 6.0, y: 12.0, width: 4.0, height: 4.0)) - context.fillEllipse(in: CGRect(x: 12.0, y: 12.0, width: 4.0, height: 4.0)) - context.fillEllipse(in: CGRect(x: 18.0, y: 12.0, width: 4.0, height: 4.0)) + + if let image = UIImage(bundleImageName: "Call/PanelIcon") { + let imageSize = image.size + let imageRect = CGRect(origin: CGPoint(), size: imageSize) + context.saveGState() + context.translateBy(x: 7.0, y: 2.0) + context.clip(to: imageRect, mask: image.cgImage!) + context.fill(imageRect) + context.restoreGState() + } }) } @@ -51,6 +69,7 @@ func closeButtonImage(dark: Bool) -> UIImage? { final class VoiceChatHeaderButton: HighlightableButtonNode { enum Content { case image(UIImage?) + case more(UIImage?) case avatar(Peer) } @@ -60,13 +79,17 @@ final class VoiceChatHeaderButton: HighlightableButtonNode { let referenceNode: ContextReferenceContentNode let containerNode: ContextControllerSourceNode private let iconNode: ASImageNode + private var animationNode: AnimationNode? private let avatarNode: AvatarNode var contextAction: ((ASDisplayNode, ContextGesture?) -> Void)? - init(context: AccountContext) { + private let wide: Bool + + init(context: AccountContext, wide: Bool = false) { self.context = context self.theme = context.sharedContext.currentPresentationData.with { $0 }.theme + self.wide = wide self.referenceNode = ContextReferenceContentNode() self.containerNode = ContextControllerSourceNode() @@ -99,9 +122,9 @@ final class VoiceChatHeaderButton: HighlightableButtonNode { strongSelf.contextAction?(strongSelf.containerNode, gesture) } - self.iconNode.image = optionsButtonImage(dark: false) + self.iconNode.image = optionsCircleImage(dark: false) - self.containerNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 28.0, height: 28.0)) + self.containerNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: wide ? 38.0 : 28.0, height: 28.0)) self.referenceNode.frame = self.containerNode.bounds self.iconNode.frame = self.containerNode.bounds self.avatarNode.frame = self.containerNode.bounds @@ -109,6 +132,15 @@ final class VoiceChatHeaderButton: HighlightableButtonNode { private var content: Content? func setContent(_ content: Content, animated: Bool = false) { + if case .more = content, self.animationNode == nil { + let iconColor = UIColor(rgb: 0xffffff) + let animationNode = AnimationNode(animation: "anim_profilemore", colors: ["Point 2.Group 1.Fill 1": iconColor, + "Point 3.Group 1.Fill 1": iconColor, + "Point 1.Group 1.Fill 1": iconColor], scale: 1.0) + animationNode.frame = self.containerNode.bounds + self.addSubnode(animationNode) + self.animationNode = animationNode + } if animated { switch content { case let .image(image): @@ -127,7 +159,12 @@ final class VoiceChatHeaderButton: HighlightableButtonNode { self.avatarNode.setPeer(context: self.context, theme: self.theme, peer: peer) self.iconNode.isHidden = true self.avatarNode.isHidden = false - + self.animationNode?.isHidden = true + case let .more(image): + self.iconNode.image = image + self.iconNode.isHidden = false + self.avatarNode.isHidden = true + self.animationNode?.isHidden = false } } else { self.content = content @@ -140,6 +177,12 @@ final class VoiceChatHeaderButton: HighlightableButtonNode { self.avatarNode.setPeer(context: self.context, theme: self.theme, peer: peer) self.iconNode.isHidden = true self.avatarNode.isHidden = false + self.animationNode?.isHidden = true + case let .more(image): + self.iconNode.image = image + self.iconNode.isHidden = false + self.avatarNode.isHidden = true + self.animationNode?.isHidden = false } } } @@ -150,9 +193,13 @@ final class VoiceChatHeaderButton: HighlightableButtonNode { } override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize { - return CGSize(width: 28.0, height: 28.0) + return CGSize(width: wide ? 38.0 : 28.0, height: 28.0) } func onLayout() { } + + func play() { + self.animationNode?.playOnce() + } } diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatInfoContextItem.swift b/submodules/TelegramCallsUI/Sources/VoiceChatInfoContextItem.swift index 577f352ffb..341328fba7 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatInfoContextItem.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatInfoContextItem.swift @@ -17,7 +17,7 @@ public final class VoiceChatInfoContextItem: ContextMenuCustomItem { self.icon = icon } - public func node(presentationData: PresentationData, getController: @escaping () -> ContextController?, actionSelected: @escaping (ContextMenuActionResult) -> Void) -> ContextMenuCustomNode { + public func node(presentationData: PresentationData, getController: @escaping () -> ContextControllerProtocol?, actionSelected: @escaping (ContextMenuActionResult) -> Void) -> ContextMenuCustomNode { return VoiceChatInfoContextItemNode(presentationData: presentationData, item: self, getController: getController, actionSelected: actionSelected) } } @@ -25,14 +25,14 @@ public final class VoiceChatInfoContextItem: ContextMenuCustomItem { private final class VoiceChatInfoContextItemNode: ASDisplayNode, ContextMenuCustomNode { private let item: VoiceChatInfoContextItem private let presentationData: PresentationData - private let getController: () -> ContextController? + private let getController: () -> ContextControllerProtocol? private let actionSelected: (ContextMenuActionResult) -> Void private let backgroundNode: ASDisplayNode private let textNode: ImmediateTextNode private let iconNode: ASImageNode - init(presentationData: PresentationData, item: VoiceChatInfoContextItem, getController: @escaping () -> ContextController?, actionSelected: @escaping (ContextMenuActionResult) -> Void) { + init(presentationData: PresentationData, item: VoiceChatInfoContextItem, getController: @escaping () -> ContextControllerProtocol?, actionSelected: @escaping (ContextMenuActionResult) -> Void) { self.item = item self.presentationData = presentationData self.getController = getController @@ -73,7 +73,7 @@ private final class VoiceChatInfoContextItemNode: ASDisplayNode, ContextMenuCust let standardIconWidth: CGFloat = 32.0 var rightTextInset: CGFloat = sideInset if !iconSize.width.isZero { - rightTextInset = max(iconSize.width, standardIconWidth) + iconSideInset + sideInset - 8.0 + rightTextInset = max(iconSize.width, standardIconWidth) + iconSideInset + sideInset - 12.0 } let textSize = self.textNode.updateLayout(CGSize(width: constrainedWidth - sideInset - rightTextInset, height: .greatestFiniteMagnitude)) diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatJoinScreen.swift b/submodules/TelegramCallsUI/Sources/VoiceChatJoinScreen.swift index e4c8d97308..2c1ecb5060 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatJoinScreen.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatJoinScreen.swift @@ -14,6 +14,7 @@ import PresentationDataUtils import PeerInfoUI import ShareController import AvatarNode +import UndoUI public final class VoiceChatJoinScreen: ViewController { private var controllerNode: Node { @@ -71,7 +72,7 @@ public final class VoiceChatJoinScreen: ViewController { let context = self.context let peerId = self.peerId let invite = self.invite - let signal = updatedCurrentPeerGroupCall(account: context.account, peerId: peerId) + let signal = context.engine.calls.updatedCurrentPeerGroupCall(peerId: peerId) |> castError(GetCurrentGroupCallError.self) |> mapToSignal { call -> Signal<(Peer, GroupCallSummary)?, GetCurrentGroupCallError> in if let call = call { @@ -79,7 +80,7 @@ public final class VoiceChatJoinScreen: ViewController { return transaction.getPeer(peerId) } |> castError(GetCurrentGroupCallError.self) - return combineLatest(peer, getCurrentGroupCall(account: context.account, callId: call.id, accessHash: call.accessHash)) + return combineLatest(peer, context.engine.calls.getCurrentGroupCall(callId: call.id, accessHash: call.accessHash)) |> map { peer, call -> (Peer, GroupCallSummary)? in if let peer = peer, let call = call { return (peer, call) @@ -124,7 +125,7 @@ public final class VoiceChatJoinScreen: ViewController { currentGroupCall = .single(nil) } - self.disposable.set(combineLatest(queue: Queue.mainQueue(), signal, cachedGroupCallDisplayAsAvailablePeers(account: context.account, peerId: peerId) |> castError(GetCurrentGroupCallError.self), cachedData, currentGroupCall).start(next: { [weak self] peerAndCall, availablePeers, cachedData, currentGroupCallIdAndCanUnmute in + self.disposable.set(combineLatest(queue: Queue.mainQueue(), signal, context.engine.calls.cachedGroupCallDisplayAsAvailablePeers(peerId: peerId) |> castError(GetCurrentGroupCallError.self), cachedData, currentGroupCall).start(next: { [weak self] peerAndCall, availablePeers, cachedData, currentGroupCallIdAndCanUnmute in if let strongSelf = self { if let (peer, call) = peerAndCall { if let (currentGroupCall, currentGroupCallId, canUnmute) = currentGroupCallIdAndCanUnmute, call.info.id == currentGroupCallId { @@ -143,8 +144,8 @@ public final class VoiceChatJoinScreen: ViewController { } else if let cachedData = cachedData as? CachedGroupData { defaultJoinAsPeerId = cachedData.callJoinPeerId } - - let activeCall = CachedChannelData.ActiveCall(id: call.info.id, accessHash: call.info.accessHash, title: call.info.title) + + let activeCall = CachedChannelData.ActiveCall(id: call.info.id, accessHash: call.info.accessHash, title: call.info.title, scheduleTimestamp: call.info.scheduleTimestamp, subscribedToScheduled: call.info.subscribedToScheduled) if availablePeers.count > 0 && defaultJoinAsPeerId == nil { strongSelf.dismiss() strongSelf.join(activeCall) @@ -152,7 +153,8 @@ public final class VoiceChatJoinScreen: ViewController { strongSelf.controllerNode.setPeer(call: activeCall, peer: peer, title: call.info.title, memberCount: call.info.participantCount) } } else { - strongSelf.present(textAlertController(context: strongSelf.context, title: nil, text: strongSelf.presentationData.strings.InviteLinks_InviteLinkExpired, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + strongSelf.present(UndoOverlayController(presentationData: presentationData, content: .linkRevoked(text: presentationData.strings.InviteLinks_InviteLinkExpired), elevatedLayout: true, animateInAsReplacement: true, action: { _ in return false }), in: .window(.root)) strongSelf.dismiss() } } @@ -181,7 +183,7 @@ public final class VoiceChatJoinScreen: 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.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition) } class Node: ViewControllerTracingNode, UIScrollViewDelegate { @@ -513,10 +515,16 @@ public final class VoiceChatJoinScreen: ViewController { self.dimNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) let offset = self.bounds.size.height - self.contentBackgroundNode.frame.minY - let dimPosition = self.dimNode.layer.position - self.dimNode.layer.animatePosition(from: CGPoint(x: dimPosition.x, y: dimPosition.y - offset), to: dimPosition, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring) - self.layer.animateBoundsOriginYAdditive(from: -offset, to: 0.0, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring) + + let transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring) + let targetBounds = self.bounds + self.bounds = self.bounds.offsetBy(dx: 0.0, dy: -offset) + self.dimNode.position = CGPoint(x: dimPosition.x, y: dimPosition.y - offset) + transition.animateView({ + self.bounds = targetBounds + self.dimNode.position = dimPosition + }) } } diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatMainStageNode.swift b/submodules/TelegramCallsUI/Sources/VoiceChatMainStageNode.swift new file mode 100644 index 0000000000..c4bde166a4 --- /dev/null +++ b/submodules/TelegramCallsUI/Sources/VoiceChatMainStageNode.swift @@ -0,0 +1,1338 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import SwiftSignalKit +import TelegramPresentationData +import TelegramUIPreferences +import TelegramStringFormatting +import TelegramVoip +import TelegramAudio +import AccountContext +import Postbox +import TelegramCore +import SyncCore +import AppBundle +import PresentationDataUtils +import AvatarNode +import AudioBlob +import TextFormat +import Markdown +import ContextUI + +private let backArrowImage = NavigationBarTheme.generateBackArrowImage(color: .white) +private let backgroundCornerRadius: CGFloat = 11.0 +private let fadeColor = UIColor(rgb: 0x000000, alpha: 0.5) +private let destructiveColor: UIColor = UIColor(rgb: 0xff3b30) + +private class VoiceChatPinButtonNode: HighlightTrackingButtonNode { + private let pinButtonIconNode: VoiceChatPinNode + private let pinButtonClippingnode: ASDisplayNode + private let pinButtonTitleNode: ImmediateTextNode + + init(presentationData: PresentationData) { + self.pinButtonIconNode = VoiceChatPinNode() + self.pinButtonClippingnode = ASDisplayNode() + self.pinButtonClippingnode.clipsToBounds = true + + self.pinButtonTitleNode = ImmediateTextNode() + self.pinButtonTitleNode.attributedText = NSAttributedString(string: presentationData.strings.VoiceChat_Unpin, font: Font.regular(17.0), textColor: .white) + self.pinButtonTitleNode.alpha = 0.0 + + super.init() + + self.addSubnode(self.pinButtonClippingnode) + self.addSubnode(self.pinButtonIconNode) + self.pinButtonClippingnode.addSubnode(self.pinButtonTitleNode) + + self.highligthedChanged = { [weak self] highlighted in + if let strongSelf = self { + if highlighted { + strongSelf.pinButtonClippingnode.layer.removeAnimation(forKey: "opacity") + strongSelf.pinButtonIconNode.layer.removeAnimation(forKey: "opacity") + strongSelf.pinButtonClippingnode.alpha = 0.4 + strongSelf.pinButtonIconNode.alpha = 0.4 + } else { + strongSelf.pinButtonClippingnode.alpha = 1.0 + strongSelf.pinButtonIconNode.alpha = 1.0 + strongSelf.pinButtonClippingnode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) + strongSelf.pinButtonIconNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) + } + } + } + } + + private var isPinned = false + func update(pinned: Bool, animated: Bool) { + let wasPinned = self.isPinned + self.pinButtonIconNode.update(state: .init(pinned: pinned, color: .white), animated: true) + self.isPinned = pinned + + self.pinButtonTitleNode.alpha = self.isPinned ? 1.0 : 0.0 + if animated && pinned != wasPinned { + if wasPinned { + self.pinButtonTitleNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2) + self.pinButtonTitleNode.layer.animatePosition(from: CGPoint(), to: CGPoint(x: self.pinButtonTitleNode.frame.width, y: 0.0), duration: 0.2, additive: true) + } else { + self.pinButtonTitleNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + self.pinButtonTitleNode.layer.animatePosition(from: CGPoint(x: self.pinButtonTitleNode.frame.width, y: 0.0), to: CGPoint(), duration: 0.2, additive: true) + } + } + } + + func update(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize { + let unpinSize = self.pinButtonTitleNode.updateLayout(size) + let pinIconSize = CGSize(width: 48.0, height: 48.0) + let totalSize = CGSize(width: unpinSize.width + pinIconSize.width, height: 44.0) + + transition.updateFrame(node: self.pinButtonIconNode, frame: CGRect(origin: CGPoint(x: totalSize.width - pinIconSize.width, y: 0.0), size: pinIconSize)) + transition.updateFrame(node: self.pinButtonTitleNode, frame: CGRect(origin: CGPoint(x: 4.0, y: 12.0), size: unpinSize)) + transition.updateFrame(node: self.pinButtonClippingnode, frame: CGRect(x: 0.0, y: 0.0, width: totalSize.width - pinIconSize.width * 0.6667, height: 44.0)) + + return totalSize + } +} + +final class VoiceChatMainStageNode: ASDisplayNode { + private let context: AccountContext + private let call: PresentationGroupCall + private var currentPeer: (PeerId, String?, Bool, Bool, Bool)? + private var currentPeerEntry: VoiceChatPeerEntry? + + var callState: PresentationGroupCallState? + + private var currentVideoNode: GroupVideoNode? + + private let backgroundNode: ASDisplayNode + private let topFadeNode: ASDisplayNode + private let bottomFadeNode: ASDisplayNode + private let bottomGradientNode: ASDisplayNode + private let bottomFillNode: ASDisplayNode + private let headerNode: ASDisplayNode + private let backButtonNode: HighlightableButtonNode + private let backButtonArrowNode: ASImageNode + private let pinButtonNode: VoiceChatPinButtonNode + private let audioLevelNode: VoiceChatBlobNode + private let audioLevelDisposable = MetaDisposable() + private let speakingPeerDisposable = MetaDisposable() + private let speakingAudioLevelDisposable = MetaDisposable() + private var backdropAvatarNode: ImageNode + private var avatarNode: ImageNode + private let titleNode: ImmediateTextNode + private let microphoneNode: VoiceChatMicrophoneNode + private let placeholderTextNode: ImmediateTextNode + private let placeholderIconNode: ASImageNode + private let placeholderButton: HighlightTrackingButtonNode + private var placeholderButtonEffectView: UIVisualEffectView? + private let placeholderButtonHighlightNode: ASDisplayNode + private let placeholderButtonTextNode: ImmediateTextNode + + private let speakingContainerNode: ASDisplayNode + private var speakingEffectView: UIVisualEffectView? + private let speakingAvatarNode: AvatarNode + private let speakingTitleNode: ImmediateTextNode + private var speakingAudioLevelView: VoiceBlobView? + + private var validLayout: (CGSize, CGFloat, CGFloat, Bool, Bool)? + + var tapped: (() -> Void)? + var back: (() -> Void)? + var togglePin: (() -> Void)? + var switchTo: ((PeerId) -> Void)? + var stopScreencast: (() -> Void)? + + var controlsHidden: ((Bool) -> Void)? + + var getAudioLevel: ((PeerId) -> Signal)? + var getVideo: ((String, Bool, @escaping (GroupVideoNode?) -> Void) -> Void)? + private let videoReadyDisposable = MetaDisposable() + private var silenceTimer: SwiftSignalKit.Timer? + + init(context: AccountContext, call: PresentationGroupCall) { + self.context = context + self.call = call + + self.backgroundNode = ASDisplayNode() + self.backgroundNode.alpha = 0.0 + self.backgroundNode.backgroundColor = UIColor(rgb: 0x1c1c1e) + + self.topFadeNode = ASDisplayNode() + self.topFadeNode.alpha = 0.0 + self.topFadeNode.displaysAsynchronously = false + if let image = generateImage(CGSize(width: fadeHeight, height: fadeHeight), rotatedContext: { size, context in + let bounds = CGRect(origin: CGPoint(), size: size) + context.clear(bounds) + + let colorsArray = [fadeColor.cgColor, fadeColor.withAlphaComponent(0.0).cgColor] as CFArray + var locations: [CGFloat] = [1.0, 0.0] + let gradient = CGGradient(colorsSpace: deviceColorSpace, colors: colorsArray, locations: &locations)! + context.drawLinearGradient(gradient, start: CGPoint(), end: CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions()) + }) { + self.topFadeNode.backgroundColor = UIColor(patternImage: image) + } + + self.bottomFadeNode = ASDisplayNode() + + self.bottomGradientNode = ASDisplayNode() + self.bottomGradientNode.displaysAsynchronously = false + if let image = generateImage(CGSize(width: fadeHeight, height: fadeHeight), rotatedContext: { size, context in + let bounds = CGRect(origin: CGPoint(), size: size) + context.clear(bounds) + + let colorsArray = [fadeColor.withAlphaComponent(0.0).cgColor, fadeColor.cgColor] as CFArray + var locations: [CGFloat] = [1.0, 0.0] + let gradient = CGGradient(colorsSpace: deviceColorSpace, colors: colorsArray, locations: &locations)! + context.drawLinearGradient(gradient, start: CGPoint(), end: CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions()) + }) { + self.bottomGradientNode.backgroundColor = UIColor(patternImage: image) + } + + self.bottomFillNode = ASDisplayNode() + self.bottomFillNode.backgroundColor = fadeColor + + self.headerNode = ASDisplayNode() + self.headerNode.alpha = 0.0 + + self.backButtonArrowNode = ASImageNode() + self.backButtonArrowNode.displayWithoutProcessing = true + self.backButtonArrowNode.displaysAsynchronously = false + self.backButtonArrowNode.image = NavigationBarTheme.generateBackArrowImage(color: .white) + self.backButtonNode = HighlightableButtonNode() + + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + + self.pinButtonNode = VoiceChatPinButtonNode(presentationData: presentationData) + + self.backdropAvatarNode = ImageNode() + self.backdropAvatarNode.contentMode = .scaleAspectFill + self.backdropAvatarNode.displaysAsynchronously = false + + self.audioLevelNode = VoiceChatBlobNode(size: CGSize(width: 300.0, height: 300.0)) + + self.avatarNode = ImageNode() + self.avatarNode.displaysAsynchronously = false + self.avatarNode.isHidden = true + + self.titleNode = ImmediateTextNode() + self.titleNode.alpha = 0.0 + self.titleNode.displaysAsynchronously = false + self.titleNode.isUserInteractionEnabled = false + + self.microphoneNode = VoiceChatMicrophoneNode() + self.microphoneNode.alpha = 0.0 + + self.speakingContainerNode = ASDisplayNode() + self.speakingContainerNode.alpha = 0.0 + + self.speakingAvatarNode = AvatarNode(font: avatarPlaceholderFont(size: 14.0)) + self.speakingTitleNode = ImmediateTextNode() + self.speakingTitleNode.displaysAsynchronously = false + + self.placeholderTextNode = ImmediateTextNode() + self.placeholderTextNode.alpha = 0.0 + self.placeholderTextNode.maximumNumberOfLines = 2 + self.placeholderTextNode.textAlignment = .center + + self.placeholderIconNode = ASImageNode() + self.placeholderIconNode.alpha = 0.0 + self.placeholderIconNode.contentMode = .scaleAspectFit + self.placeholderIconNode.displaysAsynchronously = false + + self.placeholderButton = HighlightTrackingButtonNode() + self.placeholderButton.alpha = 0.0 + self.placeholderButton.clipsToBounds = true + self.placeholderButton.cornerRadius = backgroundCornerRadius + + self.placeholderButtonHighlightNode = ASDisplayNode() + self.placeholderButtonHighlightNode.alpha = 0.0 + self.placeholderButtonHighlightNode.backgroundColor = UIColor(white: 1.0, alpha: 0.4) + self.placeholderButtonHighlightNode.isUserInteractionEnabled = false + + self.placeholderButtonTextNode = ImmediateTextNode() + self.placeholderButtonTextNode.attributedText = NSAttributedString(string: presentationData.strings.VoiceChat_StopScreenSharingShort, font: Font.semibold(17.0), textColor: .white) + self.placeholderButtonTextNode.isUserInteractionEnabled = false + + super.init() + + self.clipsToBounds = true + self.cornerRadius = backgroundCornerRadius + + self.addSubnode(self.backgroundNode) + self.addSubnode(self.backdropAvatarNode) + self.addSubnode(self.topFadeNode) + self.addSubnode(self.bottomFadeNode) + self.bottomFadeNode.addSubnode(self.bottomGradientNode) + self.bottomFadeNode.addSubnode(self.bottomFillNode) + self.addSubnode(self.audioLevelNode) + self.addSubnode(self.avatarNode) + self.addSubnode(self.titleNode) + self.addSubnode(self.microphoneNode) + self.addSubnode(self.headerNode) + self.headerNode.addSubnode(self.backButtonNode) + self.headerNode.addSubnode(self.backButtonArrowNode) + self.headerNode.addSubnode(self.pinButtonNode) + + self.addSubnode(self.placeholderIconNode) + self.addSubnode(self.placeholderTextNode) + + self.addSubnode(self.placeholderButton) + self.placeholderButton.addSubnode(self.placeholderButtonHighlightNode) + self.placeholderButton.addSubnode(self.placeholderButtonTextNode) + self.placeholderButton.highligthedChanged = { [weak self] highlighted in + if let strongSelf = self { + if highlighted { + strongSelf.placeholderButtonHighlightNode.layer.removeAnimation(forKey: "opacity") + strongSelf.placeholderButtonHighlightNode.alpha = 1.0 + } else { + strongSelf.placeholderButtonHighlightNode.alpha = 0.0 + strongSelf.placeholderButtonHighlightNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2) + } + } + } + self.placeholderButton.addTarget(self, action: #selector(self.stopSharingPressed), forControlEvents: .touchUpInside) + + self.addSubnode(self.speakingContainerNode) + self.speakingContainerNode.addSubnode(self.speakingAvatarNode) + self.speakingContainerNode.addSubnode(self.speakingTitleNode) + + self.backButtonNode.setTitle(presentationData.strings.Common_Back, with: Font.regular(17.0), with: .white, for: []) + self.backButtonNode.hitTestSlop = UIEdgeInsets(top: -8.0, left: -20.0, bottom: -8.0, right: -8.0) + self.backButtonNode.highligthedChanged = { [weak self] highlighted in + if let strongSelf = self { + if highlighted { + strongSelf.backButtonNode.layer.removeAnimation(forKey: "opacity") + strongSelf.backButtonArrowNode.layer.removeAnimation(forKey: "opacity") + strongSelf.backButtonNode.alpha = 0.4 + strongSelf.backButtonArrowNode.alpha = 0.4 + } else { + strongSelf.backButtonNode.alpha = 1.0 + strongSelf.backButtonArrowNode.alpha = 1.0 + strongSelf.backButtonNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) + strongSelf.backButtonArrowNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) + } + } + } + self.backButtonNode.addTarget(self, action: #selector(self.backPressed), forControlEvents: .touchUpInside) + self.pinButtonNode.addTarget(self, action: #selector(self.pinPressed), forControlEvents: .touchUpInside) + } + + deinit { + self.videoReadyDisposable.dispose() + self.audioLevelDisposable.dispose() + self.speakingPeerDisposable.dispose() + self.speakingAudioLevelDisposable.dispose() + self.silenceTimer?.invalidate() + } + + override func didLoad() { + super.didLoad() + + if #available(iOS 13.0, *) { + self.layer.cornerCurve = .continuous + } + + self.topFadeNode.view.layer.rasterizationScale = UIScreen.main.scale + self.topFadeNode.view.layer.shouldRasterize = true + self.bottomFadeNode.view.layer.rasterizationScale = UIScreen.main.scale + self.bottomFadeNode.view.layer.shouldRasterize = true + + let speakingEffectView = UIVisualEffectView(effect: UIBlurEffect(style: .dark)) + speakingEffectView.layer.cornerRadius = 19.0 + speakingEffectView.clipsToBounds = true + if #available(iOS 13.0, *) { + speakingEffectView.layer.cornerCurve = .continuous + } + self.speakingContainerNode.view.insertSubview(speakingEffectView, at: 0) + self.speakingEffectView = speakingEffectView + + let placeholderButtonEffectView = UIVisualEffectView(effect: UIBlurEffect(style: .light)) + placeholderButtonEffectView.isUserInteractionEnabled = false + self.placeholderButton.view.insertSubview(placeholderButtonEffectView, at: 0) + self.placeholderButtonEffectView = placeholderButtonEffectView + + self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tap))) + self.speakingContainerNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.speakingTap))) + } + + @objc private func tap() { + self.tapped?() + } + + @objc private func speakingTap() { + if let peerId = self.effectiveSpeakingPeerId { + self.switchTo?(peerId) + self.update(speakingPeerId: nil) + } + } + + @objc private func backPressed() { + self.back?() + } + + @objc private func pinPressed() { + self.togglePin?() + } + + @objc private func stopSharingPressed() { + self.stopScreencast?() + } + + var visibility = true { + didSet { + if let videoNode = self.currentVideoNode, videoNode.supernode === self { + videoNode.updateIsEnabled(self.visibility) + } + } + } + + var animating: Bool { + return self.animatingIn || self.animatingOut + } + + private var animatingIn = false + private var animatingOut = false + private var appeared = false + + func animateTransitionIn(from sourceNode: ASDisplayNode, transition: ContainedViewLayoutTransition, completion: @escaping () -> Void) { + guard let sourceNode = sourceNode as? VoiceChatTileItemNode, let _ = sourceNode.item, let (_, sideInset, bottomInset, isLandscape, isTablet) = self.validLayout else { + return + } + self.appeared = true + + self.backgroundNode.alpha = 0.0 + self.topFadeNode.alpha = 0.0 + self.titleNode.alpha = 0.0 + self.microphoneNode.alpha = 0.0 + self.headerNode.alpha = 0.0 + + let hasPlaceholder = !self.placeholderIconNode.alpha.isZero + + let alphaTransition = ContainedViewLayoutTransition.animated(duration: 0.3, curve: .easeInOut) + alphaTransition.updateAlpha(node: self.backgroundNode, alpha: 1.0) + alphaTransition.updateAlpha(node: self.topFadeNode, alpha: 1.0) + alphaTransition.updateAlpha(node: self.titleNode, alpha: 1.0) + alphaTransition.updateAlpha(node: self.microphoneNode, alpha: 1.0) + alphaTransition.updateAlpha(node: self.headerNode, alpha: 1.0) + if hasPlaceholder { + self.placeholderIconNode.alpha = 0.0 + self.placeholderTextNode.alpha = 0.0 + alphaTransition.updateAlpha(node: self.placeholderTextNode, alpha: 1.0) + + if !self.placeholderButton.alpha.isZero { + self.placeholderButton.alpha = 0.0 + alphaTransition.updateAlpha(node: self.placeholderButton, alpha: 1.0) + } + } + + let targetFrame = self.frame + + if let snapshotView = sourceNode.infoNode.view.snapshotView(afterScreenUpdates: false) { + self.view.addSubview(snapshotView) + snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak snapshotView] _ in + snapshotView?.removeFromSuperview() + }) + var infoFrame = snapshotView.frame + infoFrame.origin.x = sideInset + infoFrame.origin.y = targetFrame.height - infoFrame.height - (sideInset.isZero ? bottomInset : 14.0) + transition.updateFrame(view: snapshotView, frame: infoFrame) + } + + self.animatingIn = true + let startLocalFrame = sourceNode.view.convert(sourceNode.bounds, to: self.supernode?.view) + self.update(size: startLocalFrame.size, sideInset: sideInset, bottomInset: bottomInset, isLandscape: isLandscape, isTablet: isTablet, force: true, transition: .immediate) + self.frame = startLocalFrame + self.update(size: targetFrame.size, sideInset: sideInset, bottomInset: bottomInset, isLandscape: isLandscape, isTablet: isTablet, force: true, transition: transition) + transition.updateFrame(node: self, frame: targetFrame, completion: { [weak self] _ in + sourceNode.alpha = 1.0 + self?.animatingIn = false + completion() + }) + + if hasPlaceholder, let iconSnapshotView = sourceNode.placeholderIconNode.view.snapshotView(afterScreenUpdates: false), let textSnapshotView = sourceNode.placeholderTextNode.view.snapshotView(afterScreenUpdates: false) { + iconSnapshotView.frame = sourceNode.placeholderIconNode.frame + self.view.addSubview(iconSnapshotView) + textSnapshotView.frame = sourceNode.placeholderTextNode.frame + self.view.addSubview(textSnapshotView) + transition.updatePosition(layer: iconSnapshotView.layer, position: self.placeholderIconNode.position, completion: { [weak self, weak iconSnapshotView] _ in + iconSnapshotView?.removeFromSuperview() + self?.placeholderIconNode.alpha = 1.0 + }) + transition.updateTransformScale(layer: iconSnapshotView.layer, scale: 2.0) + textSnapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak textSnapshotView] _ in + textSnapshotView?.removeFromSuperview() + }) + let textPosition = self.placeholderTextNode.position + self.placeholderTextNode.position = textSnapshotView.center + transition.updatePosition(layer: textSnapshotView.layer, position: textPosition) + transition.updatePosition(node: self.placeholderTextNode, position: textPosition) + } + } + + func animateTransitionOut(to targetNode: ASDisplayNode?, offset: CGFloat, transition: ContainedViewLayoutTransition, completion: @escaping () -> Void) { + guard let (_, sideInset, bottomInset, isLandscape, isTablet) = self.validLayout else { + return + } + + self.appeared = false + + let hasPlaceholder = !self.placeholderIconNode.alpha.isZero + + let alphaTransition = ContainedViewLayoutTransition.animated(duration: 0.3, curve: .easeInOut) + if offset.isZero { + alphaTransition.updateAlpha(node: self.backgroundNode, alpha: 0.0) + } else { + self.backgroundNode.alpha = 0.0 + + self.microphoneNode.alpha = 1.0 + self.titleNode.alpha = 1.0 + self.bottomFadeNode.alpha = 1.0 + } + alphaTransition.updateAlpha(node: self.topFadeNode, alpha: 0.0) + alphaTransition.updateAlpha(node: self.titleNode, alpha: 0.0) + alphaTransition.updateAlpha(node: self.microphoneNode, alpha: 0.0) + alphaTransition.updateAlpha(node: self.headerNode, alpha: 0.0) + alphaTransition.updateAlpha(node: self.bottomFadeNode, alpha: 1.0) + if hasPlaceholder { + alphaTransition.updateAlpha(node: self.placeholderTextNode, alpha: 0.0) + if !self.placeholderButton.alpha.isZero { + self.placeholderButton.alpha = 0.0 + self.placeholderButton.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2) + } + } + + let originalFrame = self.frame + let initialFrame = originalFrame.offsetBy(dx: 0.0, dy: offset) + guard let targetNode = targetNode as? VoiceChatTileItemNode, let _ = targetNode.item else { + guard let supernode = self.supernode else { + completion() + return + } + self.animatingOut = true + self.frame = initialFrame + if offset < 0.0 { + let targetFrame = CGRect(origin: CGPoint(x: 0.0, y: -originalFrame.size.height), size: originalFrame.size) + transition.updateFrame(node: self, frame: targetFrame, completion: { [weak self] _ in + self?.frame = originalFrame + completion() + self?.animatingOut = false + }) + } else { + let targetFrame = CGRect(origin: CGPoint(x: 0.0, y: supernode.frame.height), size: originalFrame.size) + transition.updateFrame(node: self, frame: targetFrame, completion: { [weak self] _ in + self?.frame = originalFrame + completion() + self?.animatingOut = false + }) + } + return + } + + targetNode.isHidden = false + if offset.isZero { + targetNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1) + } + + self.animatingOut = true + let targetFrame = targetNode.view.convert(targetNode.bounds, to: self.supernode?.view) + + let currentVideoNode = self.currentVideoNode + + var infoView: UIView? + if let snapshotView = targetNode.infoNode.view.snapshotView(afterScreenUpdates: false) { + infoView = snapshotView + self.view.addSubview(snapshotView) + snapshotView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3, removeOnCompletion: false) + var infoFrame = snapshotView.frame + infoFrame.origin.y = initialFrame.height - infoFrame.height - (sideInset.isZero ? bottomInset : 14.0) + snapshotView.frame = infoFrame + transition.updateFrame(view: snapshotView, frame: CGRect(origin: CGPoint(), size: targetFrame.size)) + } + + targetNode.alpha = 0.0 + + self.frame = initialFrame + + let textPosition = self.placeholderTextNode.position + var textTargetPosition = textPosition + var textView: UIView? + if hasPlaceholder, let iconSnapshotView = targetNode.placeholderIconNode.view.snapshotView(afterScreenUpdates: false), let textSnapshotView = targetNode.placeholderTextNode.view.snapshotView(afterScreenUpdates: false) { + self.view.addSubview(iconSnapshotView) + self.view.addSubview(textSnapshotView) + iconSnapshotView.transform = CGAffineTransform(scaleX: 2.0, y: 2.0) + iconSnapshotView.center = self.placeholderIconNode.position + textSnapshotView.center = textPosition + textTargetPosition = targetNode.placeholderTextNode.position + + self.placeholderIconNode.alpha = 0.0 + transition.updatePosition(layer: iconSnapshotView.layer, position: targetNode.placeholderIconNode.position, completion: { [weak self, weak iconSnapshotView] _ in + iconSnapshotView?.removeFromSuperview() + self?.placeholderIconNode.alpha = 1.0 + }) + transition.updateTransformScale(layer: iconSnapshotView.layer, scale: 1.0) + + textView = textSnapshotView + textSnapshotView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3, removeOnCompletion: false) + } + + self.update(size: targetFrame.size, sideInset: sideInset, bottomInset: bottomInset, isLandscape: isLandscape, isTablet: isTablet, force: true, transition: transition) + transition.updateFrame(node: self, frame: targetFrame, completion: { [weak self] _ in + if let strongSelf = self { + completion() + + infoView?.removeFromSuperview() + textView?.removeFromSuperview() + currentVideoNode?.isMainstageExclusive = false + targetNode.transitionIn(from: nil) + targetNode.alpha = 1.0 + targetNode.highlightNode.layer.animateAlpha(from: 0.0, to: targetNode.highlightNode.alpha, duration: 0.2) + strongSelf.animatingOut = false + strongSelf.frame = originalFrame + strongSelf.update(size: initialFrame.size, sideInset: sideInset, bottomInset: bottomInset, isLandscape: isLandscape, isTablet: isTablet, transition: .immediate) + } + }) + + if hasPlaceholder { + self.placeholderTextNode.position = textPosition + if let textSnapshotView = textView { + transition.updatePosition(layer: textSnapshotView.layer, position: textTargetPosition) + } + transition.updatePosition(node: self.placeholderTextNode, position: textTargetPosition) + } + + self.update(speakingPeerId: nil) + } + + private var effectiveSpeakingPeerId: PeerId? + private func updateSpeakingPeer() { + guard let (_, _, _, _, isTablet) = self.validLayout else { + return + } + var effectiveSpeakingPeerId = self.speakingPeerId + if let peerId = effectiveSpeakingPeerId, self.currentPeer?.0 == peerId || self.callState?.myPeerId == peerId || isTablet { + effectiveSpeakingPeerId = nil + } + guard self.effectiveSpeakingPeerId != effectiveSpeakingPeerId else { + return + } + self.effectiveSpeakingPeerId = effectiveSpeakingPeerId + if let getAudioLevel = self.getAudioLevel, let peerId = effectiveSpeakingPeerId { + let wavesColor = UIColor(rgb: 0x34c759) + if let speakingAudioLevelView = self.speakingAudioLevelView { + speakingAudioLevelView.removeFromSuperview() + self.speakingAudioLevelView = nil + } + + self.speakingPeerDisposable.set((self.context.account.postbox.loadedPeerWithId(peerId) + |> deliverOnMainQueue).start(next: { [weak self] peer in + guard let strongSelf = self else { + return + } + + let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } + strongSelf.speakingAvatarNode.setPeer(context: strongSelf.context, theme: presentationData.theme, peer: peer) + + let bodyAttributes = MarkdownAttributeSet(font: Font.regular(15.0), textColor: .white, additionalAttributes: [:]) + let boldAttributes = MarkdownAttributeSet(font: Font.semibold(15.0), textColor: .white, additionalAttributes: [:]) + let attributedText = addAttributesToStringWithRanges(presentationData.strings.VoiceChat_ParticipantIsSpeaking(peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)), body: bodyAttributes, argumentAttributes: [0: boldAttributes]) + strongSelf.speakingTitleNode.attributedText = attributedText + + strongSelf.speakingContainerNode.alpha = 0.0 + + if let (size, sideInset, bottomInset, isLandscape, isTablet) = strongSelf.validLayout { + strongSelf.update(size: size, sideInset: sideInset, bottomInset: bottomInset, isLandscape: isLandscape, isTablet: isTablet, transition: .immediate) + } + + strongSelf.speakingContainerNode.alpha = 1.0 + strongSelf.speakingContainerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + strongSelf.speakingContainerNode.layer.animateScale(from: 0.01, to: 1.0, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring) + + let blobFrame = strongSelf.speakingAvatarNode.frame.insetBy(dx: -12.0, dy: -12.0) + strongSelf.speakingAudioLevelDisposable.set((getAudioLevel(peerId) + |> deliverOnMainQueue).start(next: { [weak self] value in + guard let strongSelf = self else { + return + } + + if strongSelf.speakingAudioLevelView == nil, value > 0.0 { + let audioLevelView = VoiceBlobView( + frame: blobFrame, + maxLevel: 1.5, + smallBlobRange: (0, 0), + mediumBlobRange: (0.69, 0.87), + bigBlobRange: (0.71, 1.0) + ) + audioLevelView.setColor(wavesColor) + audioLevelView.alpha = 1.0 + + strongSelf.speakingAudioLevelView = audioLevelView + strongSelf.speakingContainerNode.view.insertSubview(audioLevelView, belowSubview: strongSelf.speakingAvatarNode.view) + } + + let level = min(1.5, max(0.0, CGFloat(value))) + if let audioLevelView = strongSelf.speakingAudioLevelView { + audioLevelView.updateLevel(CGFloat(value)) + + let avatarScale: CGFloat + if value > 0.02 { + audioLevelView.startAnimating(immediately: true) + avatarScale = 1.03 + level * 0.13 + audioLevelView.setColor(wavesColor, animated: true) + } else { + avatarScale = 1.0 + } + + let transition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .easeInOut) + transition.updateTransformScale(node: strongSelf.speakingAvatarNode, scale: avatarScale, beginWithCurrentState: true) + } + })) + })) + } else { + self.speakingPeerDisposable.set(nil) + self.speakingAudioLevelDisposable.set(nil) + + let audioLevelView = self.speakingAudioLevelView + self.speakingAudioLevelView = nil + + if !self.speakingContainerNode.alpha.isZero { + self.speakingContainerNode.alpha = 0.0 + self.speakingContainerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, completion: { _ in + audioLevelView?.removeFromSuperview() + }) + self.speakingContainerNode.layer.animateScale(from: 1.0, to: 0.01, duration: 0.3) + } else { + audioLevelView?.removeFromSuperview() + } + } + } + + private var visiblePeerIds = Set() + func update(visiblePeerIds: Set) { + self.visiblePeerIds = visiblePeerIds + self.updateSpeakingPeer() + } + + private var speakingPeerId: PeerId? + func update(speakingPeerId: PeerId?) { + self.speakingPeerId = speakingPeerId + self.updateSpeakingPeer() + } + + func update(peerEntry: VoiceChatPeerEntry, pinned: Bool) { + let previousPeerEntry = self.currentPeerEntry + self.currentPeerEntry = peerEntry + + let peer = peerEntry.peer + let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } + if !arePeersEqual(previousPeerEntry?.peer, peerEntry.peer) { + self.backdropAvatarNode.setSignal(peerAvatarCompleteImage(account: self.context.account, peer: peer, size: CGSize(width: 240.0, height: 240.0), round: false, font: avatarPlaceholderFont(size: 78.0), drawLetters: false, blurred: true)) + self.avatarNode.setSignal(peerAvatarCompleteImage(account: self.context.account, peer: peer, size: CGSize(width: 180.0, height: 180.0), font: avatarPlaceholderFont(size: 78.0), fullSize: true)) + } + + var gradient: VoiceChatBlobNode.Gradient = .active + var muted = false + var state = peerEntry.state + if let muteState = peerEntry.muteState, case .speaking = state, muteState.mutedByYou || !muteState.canUnmute { + state = .listening + } + var mutedForYou = false + switch state { + case .listening: + if let muteState = peerEntry.muteState { + muted = true + if muteState.mutedByYou { + gradient = .mutedForYou + mutedForYou = true + } else if !muteState.canUnmute { + gradient = .muted + } + } else { + gradient = .active + muted = peerEntry.muteState != nil + } + case .speaking: + if let muteState = peerEntry.muteState, muteState.mutedByYou { + gradient = .mutedForYou + muted = true + mutedForYou = true + } else { + gradient = .speaking + muted = false + } + default: + muted = true + } + + var microphoneColor = UIColor.white + var titleAttributedString = NSAttributedString(string: peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), font: Font.semibold(15.0), textColor: .white) + if mutedForYou { + microphoneColor = destructiveColor + + let updatedString = NSMutableAttributedString(attributedString: titleAttributedString) + updatedString.append(NSAttributedString(string: " \(presentationData.strings.VoiceChat_StatusMutedForYou)", font: Font.regular(15.0), textColor: UIColor.white)) + titleAttributedString = updatedString + } + self.titleNode.attributedText = titleAttributedString + if let (size, sideInset, bottomInset, isLandscape, isTablet) = self.validLayout { + self.update(size: size, sideInset: sideInset, bottomInset: bottomInset, isLandscape: isLandscape, isTablet: isTablet, transition: .immediate) + } + + self.pinButtonNode.update(pinned: pinned, animated: true) + + self.audioLevelNode.startAnimating(immediately: true) + + if let getAudioLevel = self.getAudioLevel, previousPeerEntry?.peer.id != peerEntry.peer.id { + self.avatarNode.layer.removeAllAnimations() + self.avatarNode.transform = CATransform3DIdentity + self.audioLevelNode.updateGlowAndGradientAnimations(type: .active, animated: false) + self.audioLevelNode.updateLevel(0.0, immediately: true) + + self.audioLevelNode.isHidden = self.currentPeer?.1 != nil + self.audioLevelDisposable.set((getAudioLevel(peerEntry.peer.id) + |> deliverOnMainQueue).start(next: { [weak self] value in + guard let strongSelf = self else { + return + } + + let level = min(1.5, max(0.0, CGFloat(value))) + + strongSelf.audioLevelNode.updateLevel(CGFloat(value), immediately: false) + + let avatarScale: CGFloat + if value > 0.02 { + avatarScale = 1.03 + level * 0.13 + } else { + avatarScale = 1.0 + } + + let transition: ContainedViewLayoutTransition = .animated(duration: 0.15, curve: .easeInOut) + transition.updateTransformScale(node: strongSelf.avatarNode, scale: avatarScale, beginWithCurrentState: true) + })) + } + + self.audioLevelNode.updateGlowAndGradientAnimations(type: gradient, animated: true) + + self.microphoneNode.update(state: VoiceChatMicrophoneNode.State(muted: muted, filled: true, color: microphoneColor), animated: true) + } + + private func setAvatarHidden(_ hidden: Bool) { + self.topFadeNode.isHidden = !hidden + self.bottomFadeNode.isHidden = !hidden + self.avatarNode.isHidden = hidden + self.audioLevelNode.isHidden = hidden + } + + func update(peer: (peer: PeerId, endpointId: String?, isMyPeer: Bool, isPresentation: Bool, isPaused: Bool)?, isReady: Bool = true, waitForFullSize: Bool, completion: (() -> Void)? = nil) { + let previousPeer = self.currentPeer + if previousPeer?.0 == peer?.0 && previousPeer?.1 == peer?.1 && previousPeer?.2 == peer?.2 && previousPeer?.3 == peer?.3 && previousPeer?.4 == peer?.4 { + completion?() + return + } + self.currentPeer = peer + + self.updateSpeakingPeer() + + var isTablet = false + if let (_, _, _, _, isTabletValue) = self.validLayout { + isTablet = isTabletValue + } + + if let (_, endpointId, isMyPeer, isPresentation, isPaused) = peer { + let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } + + var showPlaceholder = false + if isMyPeer && isPresentation { + self.placeholderTextNode.attributedText = NSAttributedString(string: presentationData.strings.VoiceChat_YouAreSharingScreen, font: Font.semibold(15.0), textColor: .white) + self.placeholderIconNode.image = generateTintedImage(image: UIImage(bundleImageName: isTablet ? "Call/ScreenShareTablet" : "Call/ScreenSharePhone"), color: .white) + showPlaceholder = true + } else if isPaused { + self.placeholderTextNode.attributedText = NSAttributedString(string: presentationData.strings.VoiceChat_VideoPaused, font: Font.semibold(14.0), textColor: .white) + self.placeholderIconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Call/Pause"), color: .white) + showPlaceholder = true + } + + let updatePlaceholderVisibility = { + let peerChanged = previousPeer?.0 != peer?.0 + let transition: ContainedViewLayoutTransition = self.appeared && !peerChanged ? .animated(duration: 0.2, curve: .easeInOut) : .immediate + transition.updateAlpha(node: self.placeholderTextNode, alpha: showPlaceholder ? 1.0 : 0.0) + transition.updateAlpha(node: self.placeholderIconNode, alpha: showPlaceholder ? 1.0 : 0.0) + transition.updateAlpha(node: self.placeholderButton, alpha: showPlaceholder && !isPaused ? 1.0 : 0.0) + } + + if endpointId != previousPeer?.1 { + updatePlaceholderVisibility() + if let endpointId = endpointId { + var delayTransition = false + if previousPeer?.0 == peer?.0 && previousPeer?.1 == nil && self.appeared { + delayTransition = true + } + if !delayTransition { + self.setAvatarHidden(true) + } + + var waitForFullSize = waitForFullSize + if isMyPeer && !isPresentation && isReady && !self.appeared { + waitForFullSize = false + } + + self.getVideo?(endpointId, isMyPeer && !isPresentation, { [weak self] videoNode in + Queue.mainQueue().async { + guard let strongSelf = self, let videoNode = videoNode else { + return + } + + videoNode.isMainstageExclusive = isMyPeer && !isPresentation + if videoNode.isMainstageExclusive { + videoNode.storeSnapshot() + } + videoNode.tapped = { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.tap() + } + videoNode.sourceContainerNode.activate = { [weak self] sourceNode in + guard let strongSelf = self else { + return + } + strongSelf.setControlsHidden(true, animated: false) + strongSelf.controlsHidden?(true) + let pinchController = PinchController(sourceNode: sourceNode, getContentAreaInScreenSpace: { + return UIScreen.main.bounds + }) + strongSelf.context.sharedContext.mainWindow?.presentInGlobalOverlay(pinchController) + } + videoNode.sourceContainerNode.animatedOut = { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.controlsHidden?(false) + strongSelf.setControlsHidden(false, animated: true) + } + videoNode.updateIsBlurred(isBlurred: isPaused, light: true, animated: false) + videoNode.isUserInteractionEnabled = true + let previousVideoNode = strongSelf.currentVideoNode + var previousVideoNodeSnapshot: UIView? + if let previousVideoNode = previousVideoNode, previousVideoNode.isMainstageExclusive, let snapshotView = previousVideoNode.view.snapshotView(afterScreenUpdates: false) { + previousVideoNodeSnapshot = snapshotView + snapshotView.frame = previousVideoNode.frame + previousVideoNode.view.superview?.insertSubview(snapshotView, aboveSubview: previousVideoNode.view) + } + strongSelf.currentVideoNode = videoNode + strongSelf.insertSubnode(videoNode, aboveSubnode: strongSelf.backdropAvatarNode) + + if delayTransition { + videoNode.alpha = 0.0 + } else if !isReady { + videoNode.alpha = 0.0 + strongSelf.topFadeNode.isHidden = true + strongSelf.bottomFadeNode.isHidden = true + } else if isMyPeer { + videoNode.layer.removeAnimation(forKey: "opacity") + videoNode.alpha = 1.0 + } + if waitForFullSize { + previousVideoNode?.isMainstageExclusive = false + Queue.mainQueue().after(2.0) { + previousVideoNodeSnapshot?.removeFromSuperview() + if let previousVideoNode = previousVideoNode, previousVideoNode.supernode === strongSelf && !previousVideoNode.isMainstageExclusive { + previousVideoNode.removeFromSupernode() + } + } + strongSelf.videoReadyDisposable.set((videoNode.ready + |> filter { $0 } + |> take(1) + |> deliverOnMainQueue).start(next: { [weak self] _ in + Queue.mainQueue().after(0.1) { + guard let strongSelf = self else { + return + } + + if let (size, sideInset, bottomInset, isLandscape, isTablet) = strongSelf.validLayout { + strongSelf.update(size: size, sideInset: sideInset, bottomInset: bottomInset, isLandscape: isLandscape, isTablet: isTablet, transition: .immediate) + } + + Queue.mainQueue().after(0.02) { + completion?() + } + + if videoNode.alpha.isZero { + if delayTransition { + strongSelf.topFadeNode.isHidden = false + strongSelf.bottomFadeNode.isHidden = false + strongSelf.topFadeNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + strongSelf.bottomFadeNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + strongSelf.avatarNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false) + strongSelf.audioLevelNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false) + } + if let videoNode = strongSelf.currentVideoNode { + videoNode.alpha = 1.0 + videoNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3, completion: { [weak self] _ in + if let strongSelf = self { + strongSelf.setAvatarHidden(true) + strongSelf.avatarNode.layer.removeAllAnimations() + strongSelf.audioLevelNode.layer.removeAllAnimations() + previousVideoNodeSnapshot?.removeFromSuperview() + if let previousVideoNode = previousVideoNode, previousVideoNode.supernode === strongSelf { + previousVideoNode.removeFromSupernode() + } + } + }) + } + } else { + previousVideoNodeSnapshot?.removeFromSuperview() + previousVideoNode?.isMainstageExclusive = false + Queue.mainQueue().after(0.07) { + if let previousVideoNode = previousVideoNode, previousVideoNode.supernode === strongSelf { + previousVideoNode.removeFromSupernode() + } + } + } + } + })) + } else { + if let (size, sideInset, bottomInset, isLandscape, isTablet) = strongSelf.validLayout { + strongSelf.update(size: size, sideInset: sideInset, bottomInset: bottomInset, isLandscape: isLandscape, isTablet: isTablet, transition: .immediate) + } + if let previousVideoNode = previousVideoNode { + previousVideoNodeSnapshot?.removeFromSuperview() + previousVideoNode.isMainstageExclusive = false + if previousVideoNode.supernode === strongSelf { + previousVideoNode.removeFromSupernode() + } + } + strongSelf.videoReadyDisposable.set(nil) + completion?() + } + } + }) + } else { + if let currentVideoNode = self.currentVideoNode { + currentVideoNode.isMainstageExclusive = false + if currentVideoNode.supernode === self { + currentVideoNode.removeFromSupernode() + } + self.currentVideoNode = nil + } + self.setAvatarHidden(false) + completion?() + } + } else { + self.setAvatarHidden(endpointId != nil) + if waitForFullSize && !isReady && !isPaused, let videoNode = self.currentVideoNode { + self.videoReadyDisposable.set((videoNode.ready + |> filter { $0 } + |> take(1) + |> deliverOnMainQueue).start(next: { [weak self] _ in + Queue.mainQueue().after(0.1) { + guard let strongSelf = self else { + return + } + + if let (size, sideInset, bottomInset, isLandscape, isTablet) = strongSelf.validLayout { + strongSelf.update(size: size, sideInset: sideInset, bottomInset: bottomInset, isLandscape: isLandscape, isTablet: isTablet, transition: .immediate) + } + + Queue.mainQueue().after(0.02) { + completion?() + } + + updatePlaceholderVisibility() + if videoNode.alpha.isZero { + videoNode.updateIsBlurred(isBlurred: isPaused, light: true, animated: false) + strongSelf.topFadeNode.isHidden = true + strongSelf.bottomFadeNode.isHidden = true + if let videoNode = strongSelf.currentVideoNode { + videoNode.alpha = 1.0 + videoNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3, completion: { [weak self] _ in + if let strongSelf = self { + strongSelf.setAvatarHidden(true) + } + }) + } + } + } + })) + } else { + updatePlaceholderVisibility() + self.currentVideoNode?.updateIsBlurred(isBlurred: isPaused, light: true, animated: true) + completion?() + } + } + } else { + self.videoReadyDisposable.set(nil) + if let currentVideoNode = self.currentVideoNode { + currentVideoNode.isMainstageExclusive = false + if currentVideoNode.supernode === self { + currentVideoNode.removeFromSupernode() + } + self.currentVideoNode = nil + } + completion?() + } + } + + func setControlsHidden(_ hidden: Bool, animated: Bool, delay: Double = 0.0) { + let transition: ContainedViewLayoutTransition = animated ? .animated(duration: 0.2, curve: .easeInOut) : .immediate + transition.updateAlpha(node: self.headerNode, alpha: hidden ? 0.0 : 1.0, delay: delay) + transition.updateAlpha(node: self.topFadeNode, alpha: hidden ? 0.0 : 1.0, delay: delay) + + transition.updateAlpha(node: self.titleNode, alpha: hidden ? 0.0 : 1.0, delay: delay) + transition.updateAlpha(node: self.microphoneNode, alpha: hidden ? 0.0 : 1.0, delay: delay) + transition.updateAlpha(node: self.bottomFadeNode, alpha: hidden ? 0.0 : 1.0, delay: delay) + } + + func update(size: CGSize, sideInset: CGFloat, bottomInset: CGFloat, isLandscape: Bool, isTablet: Bool, force: Bool = false, transition: ContainedViewLayoutTransition) { + self.validLayout = (size, sideInset, bottomInset, isLandscape, isTablet) + + if self.animating && !force { + return + } + + let initialBottomInset = bottomInset + var bottomInset = bottomInset + let layoutMode: GroupVideoNode.LayoutMode + if case .immediate = transition, self.animatingIn { + layoutMode = .fillOrFitToSquare + bottomInset = 0.0 + } else if self.animatingOut { + layoutMode = .fillOrFitToSquare + bottomInset = 0.0 + } else { + if let (_, _, _, isPresentation, _) = self.currentPeer, isPresentation { + layoutMode = .fit + } else { + layoutMode = isLandscape ? .fillHorizontal : .fillVertical + } + } + + if let currentVideoNode = self.currentVideoNode { + transition.updateFrame(node: currentVideoNode, frame: CGRect(origin: CGPoint(), size: size)) + currentVideoNode.updateLayout(size: size, layoutMode: layoutMode, transition: transition) + } + + transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(), size: size)) + transition.updateFrame(node: self.backdropAvatarNode, frame: CGRect(origin: CGPoint(), size: size)) + + let avatarSize = CGSize(width: 180.0, height: 180.0) + let avatarFrame = CGRect(origin: CGPoint(x: (size.width - avatarSize.width) / 2.0, y: (size.height - avatarSize.height) / 2.0), size: avatarSize) + transition.updateFrame(node: self.avatarNode, frame: avatarFrame) + transition.updateFrame(node: self.audioLevelNode, frame: avatarFrame.insetBy(dx: -60.0, dy: -60.0)) + + let animationSize = CGSize(width: 36.0, height: 36.0) + let titleSize = self.titleNode.updateLayout(CGSize(width: size.width - sideInset * 2.0 - 24.0 - animationSize.width, height: size.height)) + transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: sideInset + 12.0 + animationSize.width, y: size.height - bottomInset - titleSize.height - 16.0), size: titleSize)) + + transition.updateFrame(node: self.microphoneNode, frame: CGRect(origin: CGPoint(x: sideInset + 7.0, y: size.height - bottomInset - animationSize.height - 6.0), size: animationSize)) + + var totalFadeHeight: CGFloat = fadeHeight + if size.height != tileHeight && size.width < size.height { + totalFadeHeight += bottomInset + } + transition.updateFrame(node: self.bottomFadeNode, frame: CGRect(x: 0.0, y: size.height - totalFadeHeight, width: size.width, height: totalFadeHeight)) + transition.updateFrame(node: self.bottomGradientNode, frame: CGRect(x: 0.0, y: 0.0, width: size.width, height: fadeHeight)) + transition.updateFrame(node: self.bottomFillNode, frame: CGRect(x: 0.0, y: fadeHeight, width: size.width, height: max(0.0, totalFadeHeight - fadeHeight))) + transition.updateFrame(node: self.topFadeNode, frame: CGRect(x: 0.0, y: 0.0, width: size.width, height: 50.0)) + + let backSize = self.backButtonNode.measure(CGSize(width: 320.0, height: 100.0)) + if let image = self.backButtonArrowNode.image { + transition.updateFrame(node: self.backButtonArrowNode, frame: CGRect(origin: CGPoint(x: sideInset + 8.0, y: 11.0), size: image.size)) + } + transition.updateFrame(node: self.backButtonNode, frame: CGRect(origin: CGPoint(x: sideInset + 27.0, y: 12.0), size: backSize)) + + let offset: CGFloat = sideInset.isZero ? 0.0 : initialBottomInset + 8.0 + let pinButtonSize = self.pinButtonNode.update(size: size, transition: transition) + transition.updateFrame(node: self.pinButtonNode, frame: CGRect(origin: CGPoint(x: size.width - pinButtonSize.width - offset, y: 0.0), size: pinButtonSize)) + + transition.updateFrame(node: self.headerNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: 64.0))) + + let speakingInset: CGFloat = 16.0 + let speakingAvatarSize = CGSize(width: 30.0, height: 30.0) + let speakingTitleSize = self.speakingTitleNode.updateLayout(CGSize(width: size.width - 100.0, height: CGFloat.greatestFiniteMagnitude)) + let speakingContainerSize = CGSize(width: speakingTitleSize.width + speakingInset * 2.0 + speakingAvatarSize.width, height: 38.0) + self.speakingEffectView?.frame = CGRect(origin: CGPoint(), size: speakingContainerSize) + self.speakingAvatarNode.frame = CGRect(origin: CGPoint(x: 4.0, y: 4.0), size: speakingAvatarSize) + self.speakingTitleNode.frame = CGRect(origin: CGPoint(x: 4.0 + speakingAvatarSize.width + 14.0, y: floorToScreenPixels((38.0 - speakingTitleSize.height) / 2.0)), size: speakingTitleSize) + transition.updateFrame(node: self.speakingContainerNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - speakingContainerSize.width) / 2.0), y: 46.0), size: speakingContainerSize)) + + let placeholderTextSize = self.placeholderTextNode.updateLayout(CGSize(width: size.width - 100.0, height: 100.0)) + transition.updateFrame(node: self.placeholderTextNode, frame: CGRect(origin: CGPoint(x: floor((size.width - placeholderTextSize.width) / 2.0), y: floorToScreenPixels(size.height / 2.0) + 10.0), size: placeholderTextSize)) + if let imageSize = self.placeholderIconNode.image?.size { + transition.updateFrame(node: self.placeholderIconNode, frame: CGRect(origin: CGPoint(x: floor((size.width - imageSize.width) / 2.0), y: floorToScreenPixels(size.height / 2.0) - imageSize.height - 8.0), size: imageSize)) + } + + let placeholderButtonTextSize = self.placeholderButtonTextNode.updateLayout(CGSize(width: 240.0, height: 100.0)) + let placeholderButtonSize = CGSize(width: placeholderButtonTextSize.width + 60.0, height: 52.0) + transition.updateFrame(node: self.placeholderButton, frame: CGRect(origin: CGPoint(x: floor((size.width - placeholderButtonSize.width) / 2.0), y: floorToScreenPixels(size.height / 2.0) + 10.0 + placeholderTextSize.height + 30.0), size: placeholderButtonSize)) + self.placeholderButtonEffectView?.frame = CGRect(origin: CGPoint(), size: placeholderButtonSize) + self.placeholderButtonHighlightNode.frame = CGRect(origin: CGPoint(), size: placeholderButtonSize) + self.placeholderButtonTextNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((placeholderButtonSize.width - placeholderButtonTextSize.width) / 2.0), y: floorToScreenPixels((placeholderButtonSize.height - placeholderButtonTextSize.height) / 2.0)), size: placeholderButtonTextSize) + + if let imageSize = self.placeholderIconNode.image?.size { + transition.updateFrame(node: self.placeholderIconNode, frame: CGRect(origin: CGPoint(x: floor((size.width - imageSize.width) / 2.0), y: floorToScreenPixels(size.height / 2.0) - imageSize.height - 8.0), size: imageSize)) + } + } + + func flipVideoIfNeeded() { + guard self.currentPeer?.0 == self.callState?.myPeerId else { + return + } + self.currentVideoNode?.flip(withBackground: false) + } +} + +private let blue = UIColor(rgb: 0x007fff) +private let lightBlue = UIColor(rgb: 0x00affe) +private let green = UIColor(rgb: 0x33c659) +private let activeBlue = UIColor(rgb: 0x00a0b9) +private let purple = UIColor(rgb: 0x3252ef) +private let pink = UIColor(rgb: 0xef436c) + +class VoiceChatBlobNode: ASDisplayNode { + enum Gradient { + case speaking + case active + case connecting + case mutedForYou + case muted + } + private let size: CGSize + + private let blobView: VoiceBlobView + private let foregroundGradientLayer = CAGradientLayer() + + private let hierarchyTrackingNode: HierarchyTrackingNode + private var isCurrentlyInHierarchy = false + + init(size: CGSize) { + self.size = size + self.blobView = VoiceBlobView( + frame: CGRect(origin: CGPoint(), size: size), + maxLevel: 1.5, + smallBlobRange: (0, 0), + mediumBlobRange: (0.69, 0.87), + bigBlobRange: (0.71, 1.0) + ) + self.blobView.setColor(.white) + + self.foregroundGradientLayer.type = .radial + self.foregroundGradientLayer.colors = [lightBlue.cgColor, blue.cgColor, blue.cgColor] + self.foregroundGradientLayer.locations = [0.0, 0.85, 1.0] + self.foregroundGradientLayer.startPoint = CGPoint(x: 1.0, y: 0.0) + self.foregroundGradientLayer.endPoint = CGPoint(x: 0.0, y: 1.0) + + var updateInHierarchy: ((Bool) -> Void)? + self.hierarchyTrackingNode = HierarchyTrackingNode({ value in + updateInHierarchy?(value) + }) + + super.init() + + updateInHierarchy = { [weak self] value in + if let strongSelf = self { + strongSelf.isCurrentlyInHierarchy = value + strongSelf.updateAnimations() + } + } + + self.addSubnode(self.hierarchyTrackingNode) + } + + override func didLoad() { + super.didLoad() + + self.view.mask = self.blobView + self.layer.addSublayer(self.foregroundGradientLayer) + } + + func updateAnimations() { + if !self.isCurrentlyInHierarchy { + self.foregroundGradientLayer.removeAllAnimations() + self.blobView.stopAnimating() + return + } + self.setupGradientAnimations() + self.blobView.startAnimating(immediately: true) + } + + func updateLevel(_ level: CGFloat, immediately: Bool) { + self.blobView.updateLevel(level, immediately: immediately) + } + + func startAnimating(immediately: Bool) { + self.blobView.startAnimating(immediately: immediately) + } + + func stopAnimating() { + self.blobView.stopAnimating(duration: 0.8) + } + + private func setupGradientAnimations() { + if let _ = self.foregroundGradientLayer.animation(forKey: "movement") { + } else { + let previousValue = self.foregroundGradientLayer.startPoint + let newValue: CGPoint + if self.blobView.presentationAudioLevel > 0.22 { + newValue = CGPoint(x: CGFloat.random(in: 0.9 ..< 1.0), y: CGFloat.random(in: 0.15 ..< 0.35)) + } else if self.blobView.presentationAudioLevel > 0.01 { + newValue = CGPoint(x: CGFloat.random(in: 0.57 ..< 0.85), y: CGFloat.random(in: 0.15 ..< 0.45)) + } else { + newValue = CGPoint(x: CGFloat.random(in: 0.6 ..< 0.75), y: CGFloat.random(in: 0.25 ..< 0.45)) + } + self.foregroundGradientLayer.startPoint = newValue + + CATransaction.begin() + + let animation = CABasicAnimation(keyPath: "startPoint") + animation.duration = Double.random(in: 0.8 ..< 1.4) + animation.fromValue = previousValue + animation.toValue = newValue + + CATransaction.setCompletionBlock { [weak self] in + if let isCurrentlyInHierarchy = self?.isCurrentlyInHierarchy, isCurrentlyInHierarchy { + self?.setupGradientAnimations() + } + } + + self.foregroundGradientLayer.add(animation, forKey: "movement") + CATransaction.commit() + } + } + + private var gradient: Gradient? + func updateGlowAndGradientAnimations(type: Gradient, animated: Bool = true) { + guard self.gradient != type else { + return + } + self.gradient = type + let initialColors = self.foregroundGradientLayer.colors + let targetColors: [CGColor] + switch type { + case .speaking: + targetColors = [activeBlue.cgColor, green.cgColor, green.cgColor] + case .active: + targetColors = [lightBlue.cgColor, blue.cgColor, blue.cgColor] + case .connecting: + targetColors = [lightBlue.cgColor, blue.cgColor, blue.cgColor] + case .mutedForYou: + targetColors = [pink.cgColor, destructiveColor.cgColor, destructiveColor.cgColor] + case .muted: + targetColors = [pink.cgColor, purple.cgColor, purple.cgColor] + } + if animated { + self.foregroundGradientLayer.colors = targetColors + self.foregroundGradientLayer.animate(from: initialColors as AnyObject, to: targetColors as AnyObject, keyPath: "colors", timingFunction: CAMediaTimingFunctionName.linear.rawValue, duration: 0.3) + } else { + CATransaction.begin() + CATransaction.setDisableActions(true) + self.foregroundGradientLayer.colors = targetColors + CATransaction.commit() + } + } + + override func layout() { + super.layout() + + self.blobView.frame = self.bounds + self.foregroundGradientLayer.frame = self.bounds.insetBy(dx: -24.0, dy: -24.0) + } +} diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatMicrophoneNode.swift b/submodules/TelegramCallsUI/Sources/VoiceChatMicrophoneNode.swift index d97c234197..0edd0f7c02 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatMicrophoneNode.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatMicrophoneNode.swift @@ -158,12 +158,12 @@ final class VoiceChatMicrophoneNode: ASDisplayNode { context.setFillColor(parameters.color.cgColor) - var clearLineWidth: CGFloat = 4.0 + var clearLineWidth: CGFloat = 2.0 var lineWidth: CGFloat = 1.0 + UIScreenPixel if bounds.size.width > 36.0 { context.scaleBy(x: 2.0, y: 2.0) } else if bounds.size.width < 30.0 { - clearLineWidth = 3.0 + clearLineWidth = 2.0 lineWidth = 1.0 } @@ -207,18 +207,19 @@ final class VoiceChatMicrophoneNode: ASDisplayNode { } if parameters.reverse { - startPoint = CGPoint(x: origin.x + length * (1.0 - parameters.transition), y: origin.y + length * (1.0 - parameters.transition)) - endPoint = CGPoint(x: origin.x + length, y: origin.y + length) + startPoint = CGPoint(x: origin.x + length * (1.0 - parameters.transition), y: origin.y + length * (1.0 - parameters.transition)).offsetBy(dx: UIScreenPixel, dy: -UIScreenPixel) + endPoint = CGPoint(x: origin.x + length, y: origin.y + length).offsetBy(dx: UIScreenPixel, dy: -UIScreenPixel) } else { - startPoint = origin - endPoint = CGPoint(x: origin.x + length * parameters.transition, y: origin.y + length * parameters.transition) + startPoint = origin.offsetBy(dx: UIScreenPixel, dy: -UIScreenPixel) + endPoint = CGPoint(x: origin.x + length * parameters.transition, y: origin.y + length * parameters.transition).offsetBy(dx: UIScreenPixel, dy: -UIScreenPixel) } + context.setBlendMode(.clear) context.setLineWidth(clearLineWidth) - context.move(to: startPoint) - context.addLine(to: endPoint) + context.move(to: startPoint.offsetBy(dx: 0.0, dy: 1.0 + UIScreenPixel)) + context.addLine(to: endPoint.offsetBy(dx: 0.0, dy: 1.0 + UIScreenPixel)) context.strokePath() context.setBlendMode(.normal) diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatOverlayController.swift b/submodules/TelegramCallsUI/Sources/VoiceChatOverlayController.swift index 73a578f66f..ba6f4787df 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatOverlayController.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatOverlayController.swift @@ -97,9 +97,15 @@ public final class VoiceChatOverlayController: ViewController { private var initialRightButtonPosition: CGPoint? func animateIn(from: CGRect) { - guard let actionButton = self.controller?.actionButton, let leftButton = self.controller?.audioOutputNode, let rightButton = self.controller?.leaveNode else { + guard let controller = self.controller, let actionButton = controller.actionButton, let audioOutputNode = controller.audioOutputNode, let cameraNode = controller.cameraNode, let rightButton = controller.leaveNode else { return } + let leftButton: CallControllerButtonItemNode + if audioOutputNode.alpha.isZero { + leftButton = cameraNode + } else { + leftButton = audioOutputNode + } self.initialLeftButtonPosition = leftButton.position self.initialRightButtonPosition = rightButton.position @@ -162,20 +168,25 @@ public final class VoiceChatOverlayController: ViewController { private var animating = false private var dismissed = false - func animateOut(reclaim: Bool, completion: @escaping (Bool) -> Void) { - guard let actionButton = self.controller?.actionButton, let leftButton = self.controller?.audioOutputNode, let rightButton = self.controller?.leaveNode, let layout = self.validLayout else { + func animateOut(reclaim: Bool, targetPosition: CGPoint, completion: @escaping (Bool) -> Void) { + guard let controller = self.controller, let actionButton = controller.actionButton, let audioOutputNode = controller.audioOutputNode, let cameraNode = controller.cameraNode, let rightButton = controller.leaveNode else { return } + let leftButton: CallControllerButtonItemNode + if audioOutputNode.alpha.isZero { + leftButton = cameraNode + } else { + leftButton = audioOutputNode + } if reclaim { self.dismissed = true - let targetPosition = CGPoint(x: layout.size.width / 2.0, y: layout.size.height - layout.intrinsicInsets.bottom - 205.0 / 2.0) if self.isSlidOffscreen { self.isSlidOffscreen = false self.isButtonHidden = true actionButton.layer.sublayerTransform = CATransform3DIdentity actionButton.update(snap: false, animated: false) - actionButton.position = CGPoint(x: targetPosition.x, y: 205.0 / 2.0) + actionButton.position = CGPoint(x: targetPosition.x, y: bottomAreaHeight / 2.0) leftButton.isHidden = false rightButton.isHidden = false @@ -191,7 +202,7 @@ public final class VoiceChatOverlayController: ViewController { actionButton.layer.removeAllAnimations() actionButton.layer.sublayerTransform = CATransform3DIdentity actionButton.update(snap: false, animated: false) - actionButton.position = CGPoint(x: targetPosition.x, y: 205.0 / 2.0) + actionButton.position = CGPoint(x: targetPosition.x, y: bottomAreaHeight / 2.0) leftButton.isHidden = false rightButton.isHidden = false @@ -264,7 +275,14 @@ public final class VoiceChatOverlayController: ViewController { func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { self.validLayout = layout - if let actionButton = self.controller?.actionButton, let leftButton = self.controller?.audioOutputNode, let rightButton = self.controller?.leaveNode, !self.animating && !self.dismissed { + if let controller = self.controller, let actionButton = controller.actionButton, let audioOutputNode = controller.audioOutputNode, let cameraNode = controller.cameraNode, let rightButton = controller.leaveNode, !self.animating && !self.dismissed { + let leftButton: CallControllerButtonItemNode + if audioOutputNode.alpha.isZero { + leftButton = cameraNode + } else { + leftButton = audioOutputNode + } + let convertedRect = actionButton.view.convert(actionButton.bounds, to: self.view) let insets = layout.insets(options: [.input]) @@ -302,6 +320,7 @@ public final class VoiceChatOverlayController: ViewController { } private weak var actionButton: VoiceChatActionButton? + private weak var cameraNode: CallControllerButtonItemNode? private weak var audioOutputNode: CallControllerButtonItemNode? private weak var leaveNode: CallControllerButtonItemNode? @@ -315,9 +334,10 @@ public final class VoiceChatOverlayController: ViewController { private var currentParams: ([UIViewController], [UIViewController], VoiceChatActionButton.State)? fileprivate var initiallyHidden: Bool - init(actionButton: VoiceChatActionButton, audioOutputNode: CallControllerButtonItemNode, leaveNode: CallControllerButtonItemNode, navigationController: NavigationController?, initiallyHidden: Bool) { + init(actionButton: VoiceChatActionButton, audioOutputNode: CallControllerButtonItemNode, cameraNode: CallControllerButtonItemNode, leaveNode: CallControllerButtonItemNode, navigationController: NavigationController?, initiallyHidden: Bool) { self.actionButton = actionButton self.audioOutputNode = audioOutputNode + self.cameraNode = cameraNode self.leaveNode = leaveNode self.parentNavigationController = navigationController self.initiallyHidden = initiallyHidden @@ -371,8 +391,8 @@ public final class VoiceChatOverlayController: ViewController { completion?() } - func animateOut(reclaim: Bool, completion: @escaping (Bool) -> Void) { - self.controllerNode.animateOut(reclaim: reclaim, completion: completion) + func animateOut(reclaim: Bool, targetPosition: CGPoint, completion: @escaping (Bool) -> Void) { + self.controllerNode.animateOut(reclaim: reclaim, targetPosition: targetPosition, completion: completion) } public func updateVisibility() { @@ -396,7 +416,7 @@ public final class VoiceChatOverlayController: ViewController { var slide = true var hidden = true var animated = true - var animateInsets = true + if controllers.count == 1 || controllers.last is ChatController { if let chatController = controllers.last as? ChatController { slide = false @@ -416,9 +436,13 @@ public final class VoiceChatOverlayController: ViewController { hidden = true } - if case .active(.cantSpeak) = state { - hidden = true + switch state { + case .active(.cantSpeak), .button, .scheduled: + hidden = true + default: + break } + if hasVoiceChatController { hidden = false animated = self.initiallyHidden @@ -429,7 +453,6 @@ public final class VoiceChatOverlayController: ViewController { let previousInsets = self.additionalSideInsets self.additionalSideInsets = hidden ? UIEdgeInsets() : UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 75.0) - if previousInsets != self.additionalSideInsets { self.parentNavigationController?.requestLayout(transition: .animated(duration: 0.3, curve: .easeInOut)) } diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatParticipantItem.swift b/submodules/TelegramCallsUI/Sources/VoiceChatParticipantItem.swift index 4759d4027a..e2bcb6bed8 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatParticipantItem.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatParticipantItem.swift @@ -12,23 +12,38 @@ import ItemListUI import PresentationDataUtils import AvatarNode import TelegramStringFormatting -import PeerPresenceStatusManager import ContextUI import AccountContext import LegacyComponents import AudioBlob +import PeerInfoAvatarListNode final class VoiceChatParticipantItem: ListViewItem { - enum ParticipantText { - public enum TextColor { + enum ParticipantText: Equatable { + struct TextIcon: OptionSet { + public var rawValue: Int32 + + public init(rawValue: Int32) { + self.rawValue = rawValue + } + + public init() { + self.rawValue = 0 + } + + public static let volume = TextIcon(rawValue: 1 << 0) + public static let video = TextIcon(rawValue: 1 << 1) + public static let screen = TextIcon(rawValue: 1 << 2) + } + + enum TextColor { case generic case accent case constructive case destructive } - case presence - case text(String, TextColor) + case text(String, TextIcon, TextColor) case none } @@ -39,71 +54,42 @@ final class VoiceChatParticipantItem: ListViewItem { case wantsToSpeak } - struct RevealOption { - enum RevealOptionType { - case neutral - case warning - case destructive - case accent - } - - var type: RevealOptionType - var title: String - var action: () -> Void - - init(type: RevealOptionType, title: String, action: @escaping () -> Void) { - self.type = type - self.title = title - self.action = action - } - } - let presentationData: ItemListPresentationData let dateTimeFormat: PresentationDateTimeFormat let nameDisplayOrder: PresentationPersonNameOrder let context: AccountContext let peer: Peer - let ssrc: UInt32? - let presence: PeerPresence? let text: ParticipantText let expandedText: ParticipantText? let icon: Icon - let enabled: Bool - public let selectable: Bool let getAudioLevel: (() -> Signal)? - let getVideo: () -> GroupVideoNode? - let revealOptions: [RevealOption] - let revealed: Bool? - let setPeerIdWithRevealedOptions: (PeerId?, PeerId?) -> Void - let action: ((ASDisplayNode) -> Void)? + let action: ((ASDisplayNode?) -> Void)? let contextAction: ((ASDisplayNode, ContextGesture?) -> Void)? + let getIsExpanded: () -> Bool + let getUpdatingAvatar: () -> Signal<(TelegramMediaImageRepresentation, Float)?, NoError> - public init(presentationData: ItemListPresentationData, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, context: AccountContext, peer: Peer, ssrc: UInt32?, presence: PeerPresence?, text: ParticipantText, expandedText: ParticipantText?, icon: Icon, enabled: Bool, selectable: Bool, getAudioLevel: (() -> Signal)?, getVideo: @escaping () -> GroupVideoNode?, revealOptions: [RevealOption], revealed: Bool?, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, action: ((ASDisplayNode) -> Void)?, contextAction: ((ASDisplayNode, ContextGesture?) -> Void)? = nil) { + public let selectable: Bool = true + + public init(presentationData: ItemListPresentationData, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, context: AccountContext, peer: Peer, text: ParticipantText, expandedText: ParticipantText?, icon: Icon, getAudioLevel: (() -> Signal)?, action: ((ASDisplayNode?) -> Void)?, contextAction: ((ASDisplayNode, ContextGesture?) -> Void)? = nil, getIsExpanded: @escaping () -> Bool, getUpdatingAvatar: @escaping () -> Signal<(TelegramMediaImageRepresentation, Float)?, NoError>) { self.presentationData = presentationData self.dateTimeFormat = dateTimeFormat self.nameDisplayOrder = nameDisplayOrder self.context = context self.peer = peer - self.ssrc = ssrc - self.presence = presence self.text = text self.expandedText = expandedText self.icon = icon - self.enabled = enabled - self.selectable = selectable self.getAudioLevel = getAudioLevel - self.getVideo = getVideo - self.revealOptions = revealOptions - self.revealed = revealed - self.setPeerIdWithRevealedOptions = setPeerIdWithRevealedOptions self.action = action self.contextAction = contextAction + self.getIsExpanded = getIsExpanded + self.getUpdatingAvatar = getUpdatingAvatar } 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 = VoiceChatParticipantItemNode() - let (layout, apply) = node.asyncLayout()(self, params, previousItem == nil, nextItem == nil) + let (layout, apply) = node.asyncLayout()(self, params, previousItem == nil || previousItem is VoiceChatTilesGridItem, nextItem == nil) node.contentSize = layout.contentSize node.insets = layout.insets @@ -127,7 +113,7 @@ final class VoiceChatParticipantItem: ListViewItem { } async { - let (layout, apply) = makeLayout(self, params, previousItem == nil, nextItem == nil) + let (layout, apply) = makeLayout(self, params, previousItem == nil || previousItem is VoiceChatTilesGridItem, nextItem == nil) Queue.mainQueue().async { completion(layout, { _ in apply(false, animated) @@ -144,51 +130,172 @@ final class VoiceChatParticipantItem: ListViewItem { } private let avatarFont = avatarPlaceholderFont(size: floor(40.0 * 16.0 / 37.0)) +private let tileSize = CGSize(width: 84.0, height: 84.0) +private let backgroundCornerRadius: CGFloat = 14.0 +private let avatarSize: CGFloat = 40.0 + +private let accentColor: UIColor = UIColor(rgb: 0x007aff) +private let constructiveColor: UIColor = UIColor(rgb: 0x34c759) +private let destructiveColor: UIColor = UIColor(rgb: 0xff3b30) + +class VoiceChatParticipantStatusNode: ASDisplayNode { + private var iconNodes: [ASImageNode] + private let textNode: TextNode + + private var currentParams: (CGSize, VoiceChatParticipantItem.ParticipantText)? + + override init() { + self.iconNodes = [] + self.textNode = TextNode() + self.textNode.isUserInteractionEnabled = false + self.textNode.contentMode = .left + self.textNode.contentsScale = UIScreen.main.scale + + super.init() + + self.addSubnode(self.textNode) + } + + func asyncLayout() -> (_ size: CGSize, _ text: VoiceChatParticipantItem.ParticipantText, _ expanded: Bool) -> (CGSize, () -> Void) { + let makeTextLayout = TextNode.asyncLayout(self.textNode) + + return { size, text, expanded in + let statusFont = Font.regular(14.0) + + var attributedString: NSAttributedString? + var color: UIColor = .white + var hasVolume = false + var hasVideo = false + var hasScreen = false + switch text { + case let .text(text, textIcon, textColor): + hasVolume = textIcon.contains(.volume) + hasVideo = textIcon.contains(.video) + hasScreen = textIcon.contains(.screen) + + var textColorValue: UIColor + switch textColor { + case .generic: + textColorValue = UIColor(rgb: 0x98989e) + case .accent: + textColorValue = accentColor + case .constructive: + textColorValue = constructiveColor + case .destructive: + textColorValue = destructiveColor + } + color = textColorValue + attributedString = NSAttributedString(string: text, font: statusFont, textColor: textColorValue) + default: + break + } + + let iconSize = CGSize(width: 16.0, height: 16.0) + let spacing: CGFloat = 3.0 + + var icons: [UIImage] = [] + if hasVolume, let image = generateTintedImage(image: UIImage(bundleImageName: "Call/StatusVolume"), color: color) { + icons.append(image) + } + if hasVideo, let image = generateTintedImage(image: UIImage(bundleImageName: "Call/StatusVideo"), color: color) { + icons.append(image) + } + if hasScreen, let image = generateTintedImage(image: UIImage(bundleImageName: "Call/StatusScreen"), color: color) { + icons.append(image) + } + + let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: attributedString, backgroundColor: nil, maximumNumberOfLines: expanded ? 4 : 1, truncationType: .end, constrainedSize: CGSize(width: size.width - (iconSize.width + spacing) * CGFloat(icons.count), height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + var contentSize = textLayout.size + contentSize.width += (iconSize.width + spacing) * CGFloat(icons.count) + + return (contentSize, { [weak self] in + guard let strongSelf = self else { + return + } + + strongSelf.currentParams = (size, text) + + for i in 0 ..< icons.count { + let iconNode: ASImageNode + if strongSelf.iconNodes.count >= i + 1 { + iconNode = strongSelf.iconNodes[i] + } else { + iconNode = ASImageNode() + strongSelf.addSubnode(iconNode) + strongSelf.iconNodes.append(iconNode) + } + iconNode.frame = CGRect(origin: CGPoint(x: (iconSize.width + spacing) * CGFloat(i), y: 1.0), size: iconSize) + + iconNode.image = icons[i] + } + if strongSelf.iconNodes.count > icons.count { + for i in icons.count ..< strongSelf.iconNodes.count { + strongSelf.iconNodes[i].image = nil + } + } + + + let _ = textApply() + strongSelf.textNode.frame = CGRect(origin: CGPoint(x: (iconSize.width + spacing) * CGFloat(icons.count), y: 0.0), size: textLayout.size) + }) + } + } +} class VoiceChatParticipantItemNode: ItemListRevealOptionsItemNode { private let topStripeNode: ASDisplayNode private let bottomStripeNode: ASDisplayNode + private let highlightContainerNode: ASDisplayNode private let highlightedBackgroundNode: ASDisplayNode - private var disabledOverlayNode: ASDisplayNode? let contextSourceNode: ContextExtractedContentContainingNode private let containerNode: ContextControllerSourceNode + private let backgroundImageNode: ASImageNode private let extractedBackgroundImageNode: ASImageNode private let offsetContainerNode: ASDisplayNode private var extractedRect: CGRect? private var nonExtractedRect: CGRect? + private var extractedVerticalOffset: CGFloat? - fileprivate let avatarNode: AvatarNode + let avatarNode: AvatarNode + private let contentWrapperNode: ASDisplayNode private let titleNode: TextNode - private let statusNode: TextNode - private let expandedStatusNode: TextNode + private let statusNode: VoiceChatParticipantStatusNode + private let expandedStatusNode: VoiceChatParticipantStatusNode private var credibilityIconNode: ASImageNode? + private var avatarTransitionNode: ASImageNode? + private var avatarListContainerNode: ASDisplayNode? + private var avatarListWrapperNode: PinchSourceContainerNode? + private var avatarListNode: PeerInfoAvatarListContainerNode? + private let actionContainerNode: ASDisplayNode private var animationNode: VoiceChatMicrophoneNode? private var iconNode: ASImageNode? private var raiseHandNode: VoiceChatRaiseHandNode? private var actionButtonNode: HighlightableButtonNode - private var audioLevelView: VoiceBlobView? + var audioLevelView: VoiceBlobView? private let audioLevelDisposable = MetaDisposable() private var didSetupAudioLevel = false private var absoluteLocation: (CGRect, CGSize)? - private var peerPresenceManager: PeerPresenceStatusManager? private var layoutParams: (VoiceChatParticipantItem, ListViewItemLayoutParams, Bool, Bool)? + private var isExtracted = false + private var animatingExtraction = false private var wavesColor: UIColor? - - private var videoNode: GroupVideoNode? - + private var raiseHandTimer: SwiftSignalKit.Timer? + private var silenceTimer: SwiftSignalKit.Timer? var item: VoiceChatParticipantItem? { return self.layoutParams?.0 } + private var currentTitle: String? + init() { self.topStripeNode = ASDisplayNode() self.topStripeNode.isLayerBacked = true @@ -199,64 +306,67 @@ class VoiceChatParticipantItemNode: ItemListRevealOptionsItemNode { self.contextSourceNode = ContextExtractedContentContainingNode() self.containerNode = ContextControllerSourceNode() + self.backgroundImageNode = ASImageNode() + self.backgroundImageNode.clipsToBounds = true + self.backgroundImageNode.displaysAsynchronously = false + self.backgroundImageNode.alpha = 0.0 + self.extractedBackgroundImageNode = ASImageNode() + self.extractedBackgroundImageNode.clipsToBounds = true self.extractedBackgroundImageNode.displaysAsynchronously = false self.extractedBackgroundImageNode.alpha = 0.0 - + self.offsetContainerNode = ASDisplayNode() self.avatarNode = AvatarNode(font: avatarFont) self.avatarNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 40.0, height: 40.0)) + self.contentWrapperNode = ASDisplayNode() + self.titleNode = TextNode() self.titleNode.isUserInteractionEnabled = false self.titleNode.contentMode = .left self.titleNode.contentsScale = UIScreen.main.scale - self.statusNode = TextNode() + self.statusNode = VoiceChatParticipantStatusNode() self.statusNode.isUserInteractionEnabled = false - self.statusNode.contentMode = .left - self.statusNode.contentsScale = UIScreen.main.scale - self.expandedStatusNode = TextNode() + self.expandedStatusNode = VoiceChatParticipantStatusNode() self.expandedStatusNode.isUserInteractionEnabled = false - self.expandedStatusNode.contentMode = .left - self.expandedStatusNode.contentsScale = UIScreen.main.scale self.expandedStatusNode.alpha = 0.0 self.actionContainerNode = ASDisplayNode() self.actionButtonNode = HighlightableButtonNode() + self.highlightContainerNode = ASDisplayNode() + self.highlightContainerNode.clipsToBounds = true + self.highlightedBackgroundNode = ASDisplayNode() - self.highlightedBackgroundNode.isLayerBacked = true super.init(layerBacked: false, dynamicBounce: false, rotated: false, seeThrough: false) self.isAccessibilityElement = true + self.highlightContainerNode.addSubnode(self.highlightedBackgroundNode) + self.containerNode.addSubnode(self.contextSourceNode) self.containerNode.targetNodeForActivationProgress = self.contextSourceNode.contentNode self.addSubnode(self.containerNode) - self.contextSourceNode.contentNode.addSubnode(self.extractedBackgroundImageNode) + self.contextSourceNode.contentNode.addSubnode(self.backgroundImageNode) + self.backgroundImageNode.addSubnode(self.extractedBackgroundImageNode) self.contextSourceNode.contentNode.addSubnode(self.offsetContainerNode) - self.offsetContainerNode.addSubnode(self.avatarNode) - self.offsetContainerNode.addSubnode(self.titleNode) - self.offsetContainerNode.addSubnode(self.statusNode) - self.offsetContainerNode.addSubnode(self.expandedStatusNode) - self.offsetContainerNode.addSubnode(self.actionContainerNode) + self.offsetContainerNode.addSubnode(self.contentWrapperNode) + self.contentWrapperNode.addSubnode(self.titleNode) + self.contentWrapperNode.addSubnode(self.statusNode) + self.contentWrapperNode.addSubnode(self.expandedStatusNode) + self.contentWrapperNode.addSubnode(self.actionContainerNode) self.actionContainerNode.addSubnode(self.actionButtonNode) + self.offsetContainerNode.addSubnode(self.avatarNode) self.containerNode.targetNodeForActivationProgress = self.contextSourceNode.contentNode self.actionButtonNode.addTarget(self, action: #selector(self.actionButtonPressed), forControlEvents: .touchUpInside) - - 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) - apply(false, true) - } - }) - + self.containerNode.shouldBegin = { [weak self] location in guard let strongSelf = self else { return false @@ -279,49 +389,387 @@ class VoiceChatParticipantItemNode: ItemListRevealOptionsItemNode { return } + strongSelf.isExtracted = isExtracted + + let inset: CGFloat = 0.0 if isExtracted { - strongSelf.extractedBackgroundImageNode.image = generateStretchableFilledCircleImage(diameter: 28.0, color: item.presentationData.theme.list.itemBlocksBackgroundColor) - } - - if let extractedRect = strongSelf.extractedRect, let nonExtractedRect = strongSelf.nonExtractedRect { - let rect = isExtracted ? extractedRect : nonExtractedRect - transition.updateFrame(node: strongSelf.extractedBackgroundImageNode, frame: rect) - } - - transition.updateAlpha(node: strongSelf.statusNode, alpha: isExtracted ? 0.0 : 1.0) - transition.updateAlpha(node: strongSelf.expandedStatusNode, alpha: isExtracted ? 1.0 : 0.0) - - transition.updateAlpha(node: strongSelf.actionContainerNode, alpha: isExtracted ? 0.0 : 1.0) - - transition.updateSublayerTransformOffset(layer: strongSelf.offsetContainerNode.layer, offset: CGPoint(x: isExtracted ? 12.0 : 0.0, y: 0.0)) - - transition.updateSublayerTransformOffset(layer: strongSelf.actionContainerNode.layer, offset: CGPoint(x: isExtracted ? -24.0 : 0.0, y: 0.0)) - - transition.updateAlpha(node: strongSelf.extractedBackgroundImageNode, alpha: isExtracted ? 1.0 : 0.0, completion: { _ in - if !isExtracted { - self?.extractedBackgroundImageNode.image = nil + strongSelf.contextSourceNode.contentNode.customHitTest = { [weak self] point in + if let strongSelf = self { + if let avatarListWrapperNode = strongSelf.avatarListWrapperNode, avatarListWrapperNode.frame.contains(point) { + return strongSelf.avatarListNode?.view + } + } + return nil } - }) + } else { + strongSelf.contextSourceNode.contentNode.customHitTest = nil + } + + let extractedVerticalOffset = strongSelf.extractedVerticalOffset ?? 0.0 + if let extractedRect = strongSelf.extractedRect, let nonExtractedRect = strongSelf.nonExtractedRect { + let rect: CGRect + if isExtracted { + if extractedVerticalOffset > 0.0 { + rect = CGRect(x: extractedRect.minX, y: extractedRect.minY + extractedVerticalOffset, width: extractedRect.width, height: extractedRect.height - extractedVerticalOffset) + } else { + rect = extractedRect + } + } else { + rect = nonExtractedRect + } + + let springDuration: Double = isExtracted ? 0.42 : 0.3 + let springDamping: CGFloat = isExtracted ? 124.0 : 1000.0 + + let itemBackgroundColor: UIColor = item.getIsExpanded() ? UIColor(rgb: 0x1c1c1e) : UIColor(rgb: 0x2c2c2e) + + if !extractedVerticalOffset.isZero { + let radiusTransition = ContainedViewLayoutTransition.animated(duration: 0.15, curve: .easeInOut) + if isExtracted { + strongSelf.backgroundImageNode.image = generateImage(CGSize(width: backgroundCornerRadius * 2.0, height: backgroundCornerRadius * 2.0), rotatedContext: { (size, context) in + let bounds = CGRect(origin: CGPoint(), size: size) + context.clear(bounds) + + context.setFillColor(itemBackgroundColor.cgColor) + context.fillEllipse(in: bounds) + context.fill(CGRect(x: 0.0, y: 0.0, width: size.width, height: size.height / 2.0)) + })?.stretchableImage(withLeftCapWidth: Int(backgroundCornerRadius), topCapHeight: Int(backgroundCornerRadius)) + strongSelf.extractedBackgroundImageNode.image = generateImage(CGSize(width: backgroundCornerRadius * 2.0, height: backgroundCornerRadius * 2.0), rotatedContext: { (size, context) in + let bounds = CGRect(origin: CGPoint(), size: size) + context.clear(bounds) + + context.setFillColor(item.presentationData.theme.list.itemBlocksBackgroundColor.cgColor) + context.fillEllipse(in: bounds) + context.fill(CGRect(x: 0.0, y: 0.0, width: size.width, height: size.height / 2.0)) + })?.stretchableImage(withLeftCapWidth: Int(backgroundCornerRadius), topCapHeight: Int(backgroundCornerRadius)) + strongSelf.backgroundImageNode.cornerRadius = backgroundCornerRadius + + strongSelf.avatarNode.transform = CATransform3DIdentity + var avatarInitialRect = strongSelf.avatarNode.view.convert(strongSelf.avatarNode.bounds, to: strongSelf.offsetContainerNode.supernode?.view) + if strongSelf.avatarTransitionNode == nil { + transition.updateCornerRadius(node: strongSelf.backgroundImageNode, cornerRadius: 0.0) + + let targetRect = CGRect(x: extractedRect.minX, y: extractedRect.minY, width: extractedRect.width, height: extractedRect.width) + let initialScale = avatarInitialRect.width / targetRect.width + avatarInitialRect.origin.y += backgroundCornerRadius / 2.0 * initialScale + + let avatarListWrapperNode = PinchSourceContainerNode() + avatarListWrapperNode.clipsToBounds = true + avatarListWrapperNode.cornerRadius = backgroundCornerRadius + avatarListWrapperNode.activate = { [weak self] sourceNode in + guard let strongSelf = self else { + return + } + strongSelf.avatarListNode?.controlsContainerNode.alpha = 0.0 + let pinchController = PinchController(sourceNode: sourceNode, getContentAreaInScreenSpace: { + return UIScreen.main.bounds + }) + item.context.sharedContext.mainWindow?.presentInGlobalOverlay(pinchController) + } + avatarListWrapperNode.deactivated = { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.avatarListWrapperNode?.contentNode.layer.animate(from: 0.0 as NSNumber, to: backgroundCornerRadius as NSNumber, keyPath: "cornerRadius", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, duration: 0.3, completion: { _ in + }) + } + avatarListWrapperNode.update(size: targetRect.size, transition: .immediate) + avatarListWrapperNode.frame = CGRect(x: targetRect.minX, y: targetRect.minY, width: targetRect.width, height: targetRect.height + backgroundCornerRadius) + avatarListWrapperNode.animatedOut = { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.avatarListNode?.controlsContainerNode.alpha = 1.0 + strongSelf.avatarListNode?.controlsContainerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) + } + + let transitionNode = ASImageNode() + transitionNode.clipsToBounds = true + transitionNode.displaysAsynchronously = false + transitionNode.displayWithoutProcessing = true + transitionNode.image = strongSelf.avatarNode.unroundedImage + transitionNode.frame = CGRect(origin: CGPoint(), size: targetRect.size) + transitionNode.cornerRadius = targetRect.width / 2.0 + radiusTransition.updateCornerRadius(node: transitionNode, cornerRadius: 0.0) + + strongSelf.avatarNode.isHidden = true + avatarListWrapperNode.contentNode.addSubnode(transitionNode) + + strongSelf.avatarTransitionNode = transitionNode + + let avatarListContainerNode = ASDisplayNode() + avatarListContainerNode.clipsToBounds = true + avatarListContainerNode.frame = CGRect(origin: CGPoint(), size: targetRect.size) + avatarListContainerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + avatarListContainerNode.cornerRadius = targetRect.width / 2.0 + + avatarListWrapperNode.layer.animateSpring(from: initialScale as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: springDuration, initialVelocity: 0.0, damping: springDamping) + avatarListWrapperNode.layer.animateSpring(from: NSValue(cgPoint: avatarInitialRect.center), to: NSValue(cgPoint: avatarListWrapperNode.position), keyPath: "position", duration: springDuration, initialVelocity: 0.0, damping: springDamping) + + radiusTransition.updateCornerRadius(node: avatarListContainerNode, cornerRadius: 0.0) + + let avatarListNode = PeerInfoAvatarListContainerNode(context: item.context) + avatarListWrapperNode.contentNode.clipsToBounds = true + avatarListNode.backgroundColor = .clear + avatarListNode.peer = item.peer + avatarListNode.firstFullSizeOnly = true + avatarListNode.offsetLocation = true + avatarListNode.customCenterTapAction = { [weak self] in + self?.contextSourceNode.requestDismiss?() + } + avatarListNode.frame = CGRect(x: targetRect.width / 2.0, y: targetRect.height / 2.0, width: targetRect.width, height: targetRect.height) + avatarListNode.controlsClippingNode.frame = CGRect(x: -targetRect.width / 2.0, y: -targetRect.height / 2.0, width: targetRect.width, height: targetRect.height) + avatarListNode.controlsClippingOffsetNode.frame = CGRect(origin: CGPoint(x: targetRect.width / 2.0, y: targetRect.height / 2.0), size: CGSize()) + avatarListNode.stripContainerNode.frame = CGRect(x: 0.0, y: 13.0, width: targetRect.width, height: 2.0) + avatarListNode.shadowNode.frame = CGRect(x: 0.0, y: 0.0, width: targetRect.width, height: 44.0) + + avatarListContainerNode.addSubnode(avatarListNode) + avatarListContainerNode.addSubnode(avatarListNode.controlsClippingOffsetNode) + avatarListWrapperNode.contentNode.addSubnode(avatarListContainerNode) + + avatarListNode.update(size: targetRect.size, peer: item.peer, customNode: nil, additionalEntry: item.getUpdatingAvatar(), isExpanded: true, transition: .immediate) + strongSelf.offsetContainerNode.supernode?.addSubnode(avatarListWrapperNode) + + strongSelf.audioLevelView?.alpha = 0.0 + + strongSelf.avatarListWrapperNode = avatarListWrapperNode + strongSelf.avatarListContainerNode = avatarListContainerNode + strongSelf.avatarListNode = avatarListNode + } + } else if let transitionNode = strongSelf.avatarTransitionNode, let avatarListWrapperNode = strongSelf.avatarListWrapperNode, let avatarListContainerNode = strongSelf.avatarListContainerNode { + strongSelf.animatingExtraction = true + + transition.updateCornerRadius(node: strongSelf.backgroundImageNode, cornerRadius: backgroundCornerRadius) + + var avatarInitialRect = CGRect(origin: strongSelf.avatarNode.frame.origin, size: strongSelf.avatarNode.frame.size) + let targetScale = avatarInitialRect.width / avatarListContainerNode.frame.width + avatarInitialRect.origin.y += backgroundCornerRadius / 2.0 * targetScale + + strongSelf.avatarTransitionNode = nil + strongSelf.avatarListWrapperNode = nil + strongSelf.avatarListContainerNode = nil + strongSelf.avatarListNode = nil + + avatarListContainerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak avatarListContainerNode] _ in + avatarListContainerNode?.removeFromSupernode() + }) + + avatarListWrapperNode.layer.animate(from: 1.0 as NSNumber, to: targetScale as NSNumber, keyPath: "transform.scale", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, duration: 0.2, removeOnCompletion: false) + avatarListWrapperNode.layer.animate(from: NSValue(cgPoint: avatarListWrapperNode.position), to: NSValue(cgPoint: avatarInitialRect.center), keyPath: "position", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, duration: 0.2, removeOnCompletion: false, completion: { [weak transitionNode, weak self] _ in + transitionNode?.removeFromSupernode() + self?.avatarNode.isHidden = false + + self?.audioLevelView?.alpha = 1.0 + self?.audioLevelView?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + + if let strongSelf = self { + strongSelf.animatingExtraction = false + } + }) + + radiusTransition.updateCornerRadius(node: avatarListContainerNode, cornerRadius: avatarListContainerNode.frame.width / 2.0) + radiusTransition.updateCornerRadius(node: transitionNode, cornerRadius: avatarListContainerNode.frame.width / 2.0) + } + + let alphaTransition = ContainedViewLayoutTransition.animated(duration: 0.2, curve: .easeInOut) + alphaTransition.updateAlpha(node: strongSelf.statusNode, alpha: isExtracted ? 0.0 : 1.0) + alphaTransition.updateAlpha(node: strongSelf.expandedStatusNode, alpha: isExtracted ? 1.0 : 0.0) + alphaTransition.updateAlpha(node: strongSelf.actionContainerNode, alpha: isExtracted ? 0.0 : 1.0, delay: isExtracted ? 0.0 : 0.1) + + let offsetInitialSublayerTransform = strongSelf.offsetContainerNode.layer.sublayerTransform + strongSelf.offsetContainerNode.layer.sublayerTransform = CATransform3DMakeTranslation(isExtracted ? -43 : 0.0, isExtracted ? extractedVerticalOffset : 0.0, 0.0) + + let actionInitialSublayerTransform = strongSelf.actionContainerNode.layer.sublayerTransform + strongSelf.actionContainerNode.layer.sublayerTransform = CATransform3DMakeTranslation(isExtracted ? 43.0 : 0.0, 0.0, 0.0) + + let initialBackgroundPosition = strongSelf.backgroundImageNode.position + strongSelf.backgroundImageNode.layer.position = rect.center + let initialBackgroundBounds = strongSelf.backgroundImageNode.bounds + strongSelf.backgroundImageNode.layer.bounds = CGRect(origin: CGPoint(), size: rect.size) + + let initialExtractedBackgroundPosition = strongSelf.extractedBackgroundImageNode.position + strongSelf.extractedBackgroundImageNode.layer.position = CGPoint(x: rect.size.width / 2.0, y: rect.size.height / 2.0) + let initialExtractedBackgroundBounds = strongSelf.extractedBackgroundImageNode.bounds + strongSelf.extractedBackgroundImageNode.layer.bounds = strongSelf.backgroundImageNode.layer.bounds + if isExtracted { + strongSelf.offsetContainerNode.layer.animateSpring(from: NSValue(caTransform3D: offsetInitialSublayerTransform), to: NSValue(caTransform3D: strongSelf.offsetContainerNode.layer.sublayerTransform), keyPath: "sublayerTransform", duration: springDuration, delay: 0.0, initialVelocity: 0.0, damping: springDamping) + strongSelf.actionContainerNode.layer.animateSpring(from: NSValue(caTransform3D: actionInitialSublayerTransform), to: NSValue(caTransform3D: strongSelf.actionContainerNode.layer.sublayerTransform), keyPath: "sublayerTransform", duration: springDuration, delay: 0.0, initialVelocity: 0.0, damping: springDamping) + strongSelf.backgroundImageNode.layer.animateSpring(from: NSValue(cgPoint: initialBackgroundPosition), to: NSValue(cgPoint: strongSelf.backgroundImageNode.position), keyPath: "position", duration: springDuration, delay: 0.0, initialVelocity: 0.0, damping: springDamping) + strongSelf.backgroundImageNode.layer.animateSpring(from: NSValue(cgRect: initialBackgroundBounds), to: NSValue(cgRect: strongSelf.backgroundImageNode.bounds), keyPath: "bounds", duration: springDuration, initialVelocity: 0.0, damping: springDamping) + strongSelf.extractedBackgroundImageNode.layer.animateSpring(from: NSValue(cgPoint: initialExtractedBackgroundPosition), to: NSValue(cgPoint: strongSelf.extractedBackgroundImageNode.position), keyPath: "position", duration: springDuration, delay: 0.0, initialVelocity: 0.0, damping: springDamping) + strongSelf.extractedBackgroundImageNode.layer.animateSpring(from: NSValue(cgRect: initialExtractedBackgroundBounds), to: NSValue(cgRect: strongSelf.extractedBackgroundImageNode.bounds), keyPath: "bounds", duration: springDuration, initialVelocity: 0.0, damping: springDamping) + } else { + strongSelf.offsetContainerNode.layer.animate(from: NSValue(caTransform3D: offsetInitialSublayerTransform), to: NSValue(caTransform3D: strongSelf.offsetContainerNode.layer.sublayerTransform), keyPath: "sublayerTransform", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, duration: 0.2) + strongSelf.actionContainerNode.layer.animate(from: NSValue(caTransform3D: actionInitialSublayerTransform), to: NSValue(caTransform3D: strongSelf.actionContainerNode.layer.sublayerTransform), keyPath: "sublayerTransform", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, duration: 0.2) + strongSelf.backgroundImageNode.layer.animate(from: NSValue(cgPoint: initialBackgroundPosition), to: NSValue(cgPoint: strongSelf.backgroundImageNode.position), keyPath: "position", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, duration: 0.2) + strongSelf.backgroundImageNode.layer.animate(from: NSValue(cgRect: initialBackgroundBounds), to: NSValue(cgRect: strongSelf.backgroundImageNode.bounds), keyPath: "bounds", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, duration: 0.2) + strongSelf.extractedBackgroundImageNode.layer.animate(from: NSValue(cgPoint: initialExtractedBackgroundPosition), to: NSValue(cgPoint: strongSelf.extractedBackgroundImageNode.position), keyPath: "position", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, duration: 0.2) + strongSelf.extractedBackgroundImageNode.layer.animate(from: NSValue(cgRect: initialExtractedBackgroundBounds), to: NSValue(cgRect: strongSelf.extractedBackgroundImageNode.bounds), keyPath: "bounds", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, duration: 0.2) + } + + if isExtracted { + strongSelf.backgroundImageNode.alpha = 1.0 + strongSelf.extractedBackgroundImageNode.alpha = 1.0 + strongSelf.extractedBackgroundImageNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1, delay: 0.1, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue) + } else { + strongSelf.extractedBackgroundImageNode.alpha = 0.0 + strongSelf.extractedBackgroundImageNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, delay: 0.0, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, completion: { [weak self] _ in + if let strongSelf = self { + strongSelf.backgroundImageNode.image = nil + strongSelf.extractedBackgroundImageNode.image = nil + strongSelf.extractedBackgroundImageNode.layer.removeAllAnimations() + } + }) + } + } else { + if isExtracted { + strongSelf.backgroundImageNode.alpha = 0.0 + strongSelf.extractedBackgroundImageNode.alpha = 1.0 + strongSelf.backgroundImageNode.image = generateStretchableFilledCircleImage(diameter: backgroundCornerRadius * 2.0, color: itemBackgroundColor) + strongSelf.extractedBackgroundImageNode.image = generateStretchableFilledCircleImage(diameter: backgroundCornerRadius * 2.0, color: item.presentationData.theme.list.itemBlocksBackgroundColor) + } + + transition.updateFrame(node: strongSelf.backgroundImageNode, frame: rect) + transition.updateFrame(node: strongSelf.extractedBackgroundImageNode, frame: CGRect(origin: CGPoint(), size: rect.size)) + + transition.updateAlpha(node: strongSelf.statusNode, alpha: isExtracted ? 0.0 : 1.0) + transition.updateAlpha(node: strongSelf.expandedStatusNode, alpha: isExtracted ? 1.0 : 0.0) + transition.updateAlpha(node: strongSelf.actionContainerNode, alpha: isExtracted ? 0.0 : 1.0) + + transition.updateSublayerTransformOffset(layer: strongSelf.offsetContainerNode.layer, offset: CGPoint(x: isExtracted ? inset : 0.0, y: isExtracted ? extractedVerticalOffset : 0.0)) + transition.updateSublayerTransformOffset(layer: strongSelf.actionContainerNode.layer, offset: CGPoint(x: isExtracted ? -inset * 2.0 : 0.0, y: 0.0)) + + transition.updateAlpha(node: strongSelf.backgroundImageNode, alpha: isExtracted ? 1.0 : 0.0, completion: { _ in + if !isExtracted { + self?.backgroundImageNode.image = nil + self?.extractedBackgroundImageNode.image = nil + } + }) + } + } } } deinit { self.audioLevelDisposable.dispose() self.raiseHandTimer?.invalidate() + self.silenceTimer?.invalidate() + } + + override func didLoad() { + super.didLoad() + + if #available(iOS 13.0, *) { + self.highlightContainerNode.layer.cornerCurve = .continuous + } } override func selected() { super.selected() self.layoutParams?.0.action?(self.contextSourceNode) } + + func animateTransitionIn(from sourceNode: ASDisplayNode, containerNode: ASDisplayNode, transition: ContainedViewLayoutTransition) { + guard let _ = self.item, let sourceNode = sourceNode as? VoiceChatFullscreenParticipantItemNode, let _ = sourceNode.item else { + return + } + var duration: Double = 0.2 + var timingFunction: String = CAMediaTimingFunctionName.easeInEaseOut.rawValue + if case let .animated(transitionDuration, curve) = transition { + duration = transitionDuration + 0.08 + timingFunction = curve.timingFunction + } + + let startContainerAvatarPosition = sourceNode.avatarNode.view.convert(sourceNode.avatarNode.bounds, to: containerNode.view).center + var animate = true + if containerNode.frame.width > containerNode.frame.height { + if startContainerAvatarPosition.y < -tileSize.height * 2.0 || startContainerAvatarPosition.y > containerNode.frame.height + tileSize.height * 2.0 { + animate = false + } + } else { + if startContainerAvatarPosition.x < -tileSize.width * 4.0 || startContainerAvatarPosition.x > containerNode.frame.width + tileSize.width * 4.0 { + animate = false + } + } + if animate { + sourceNode.avatarNode.alpha = 0.0 + sourceNode.audioLevelView?.alpha = 0.0 + + let initialAvatarPosition = self.avatarNode.position + let initialBackgroundPosition = sourceNode.backgroundImageNode.position + let initialContentPosition = sourceNode.contentWrapperNode.position + + let startContainerBackgroundPosition = sourceNode.backgroundImageNode.view.convert(sourceNode.backgroundImageNode.bounds, to: containerNode.view).center + let startContainerContentPosition = sourceNode.contentWrapperNode.view.convert(sourceNode.contentWrapperNode.bounds, to: containerNode.view).center + + let targetContainerAvatarPosition = self.avatarNode.view.convert(self.avatarNode.bounds, to: containerNode.view).center + + sourceNode.backgroundImageNode.position = targetContainerAvatarPosition + sourceNode.contentWrapperNode.position = targetContainerAvatarPosition + containerNode.addSubnode(sourceNode.backgroundImageNode) + containerNode.addSubnode(sourceNode.contentWrapperNode) + + sourceNode.highlightNode.alpha = 0.0 + + sourceNode.backgroundImageNode.layer.animatePosition(from: startContainerBackgroundPosition, to: targetContainerAvatarPosition, duration: duration, timingFunction: timingFunction, completion: { [weak sourceNode] _ in + if let sourceNode = sourceNode { + Queue.mainQueue().after(0.1, { + sourceNode.backgroundImageNode.layer.removeAllAnimations() + sourceNode.contentWrapperNode.layer.removeAllAnimations() + }) + sourceNode.backgroundImageNode.alpha = 1.0 + sourceNode.highlightNode.alpha = 1.0 + sourceNode.backgroundImageNode.position = initialBackgroundPosition + sourceNode.contextSourceNode.contentNode.insertSubnode(sourceNode.backgroundImageNode, at: 0) + } + }) + + sourceNode.contentWrapperNode.layer.animatePosition(from: startContainerContentPosition, to: targetContainerAvatarPosition, duration: duration, timingFunction: timingFunction, completion: { [weak sourceNode] _ in + if let sourceNode = sourceNode { + sourceNode.avatarNode.alpha = 1.0 + sourceNode.audioLevelView?.alpha = 1.0 + sourceNode.contentWrapperNode.position = initialContentPosition + sourceNode.offsetContainerNode.insertSubnode(sourceNode.contentWrapperNode, aboveSubnode: sourceNode.videoContainerNode) + } + }) + + if let audioLevelView = self.audioLevelView { + audioLevelView.center = targetContainerAvatarPosition + containerNode.view.addSubview(audioLevelView) + + audioLevelView.layer.animateScale(from: 1.25, to: 1.0, duration: duration, timingFunction: timingFunction) + audioLevelView.layer.animatePosition(from: startContainerAvatarPosition, to: targetContainerAvatarPosition, duration: duration, timingFunction: timingFunction, removeOnCompletion: false) + } + self.avatarNode.position = targetContainerAvatarPosition + containerNode.addSubnode(self.avatarNode) + + self.avatarNode.layer.animateScale(from: 1.25, to: 1.0, duration: duration, timingFunction: timingFunction) + self.avatarNode.layer.animatePosition(from: startContainerAvatarPosition, to: targetContainerAvatarPosition, duration: duration, timingFunction: timingFunction, completion: { [weak self] _ in + if let strongSelf = self { + strongSelf.avatarNode.position = initialAvatarPosition + strongSelf.offsetContainerNode.addSubnode(strongSelf.avatarNode) + if let audioLevelView = strongSelf.audioLevelView { + audioLevelView.layer.removeAllAnimations() + audioLevelView.center = initialAvatarPosition + strongSelf.offsetContainerNode.view.insertSubview(audioLevelView, at: 0) + } + } + }) + + sourceNode.backgroundImageNode.layer.animateScale(from: 1.0, to: 0.001, duration: duration, timingFunction: timingFunction) + sourceNode.backgroundImageNode.layer.animateAlpha(from: sourceNode.backgroundImageNode.alpha, to: 0.0, duration: duration, timingFunction: timingFunction, removeOnCompletion: false) + sourceNode.contentWrapperNode.layer.animateScale(from: 1.0, to: 0.001, duration: duration, timingFunction: timingFunction) + sourceNode.contentWrapperNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration, timingFunction: timingFunction, removeOnCompletion: false) + } + } func asyncLayout() -> (_ item: VoiceChatParticipantItem, _ params: ListViewItemLayoutParams, _ first: Bool, _ last: Bool) -> (ListViewItemNodeLayout, (Bool, Bool) -> Void) { let makeTitleLayout = TextNode.asyncLayout(self.titleNode) - let makeStatusLayout = TextNode.asyncLayout(self.statusNode) - let makeExpandedStatusLayout = TextNode.asyncLayout(self.expandedStatusNode) - var currentDisabledOverlayNode = self.disabledOverlayNode + let makeStatusLayout = self.statusNode.asyncLayout() + let makeExpandedStatusLayout = self.expandedStatusNode.asyncLayout() let currentItem = self.layoutParams?.0 + let currentTitle = self.currentTitle return { item, params, first, last in var updatedTheme: PresentationTheme? @@ -330,102 +778,57 @@ class VoiceChatParticipantItemNode: ItemListRevealOptionsItemNode { } let titleFont = Font.regular(17.0) - let statusFont = Font.regular(14.0) - var titleAttributedString: NSAttributedString? - var statusAttributedString: NSAttributedString? - var expandedStatusAttributedString: NSAttributedString? + let titleColor = item.presentationData.theme.list.itemPrimaryTextColor let rightInset: CGFloat = params.rightInset - - let titleColor = item.presentationData.theme.list.itemPrimaryTextColor - let currentBoldFont: UIFont = titleFont + var updatedTitle = false 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)) - } - titleAttributedString = string + 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: titleFont, textColor: titleColor)) + case .lastFirst: + string.append(NSAttributedString(string: lastName, font: titleFont, 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 { - titleAttributedString = NSAttributedString(string: firstName, font: currentBoldFont, textColor: titleColor) + titleAttributedString = NSAttributedString(string: firstName, font: titleFont, textColor: titleColor) } else if let lastName = user.lastName, !lastName.isEmpty { - titleAttributedString = NSAttributedString(string: lastName, font: currentBoldFont, textColor: titleColor) + titleAttributedString = NSAttributedString(string: lastName, font: titleFont, textColor: titleColor) } else { - titleAttributedString = NSAttributedString(string: item.presentationData.strings.User_DeletedAccount, font: currentBoldFont, textColor: titleColor) + titleAttributedString = NSAttributedString(string: item.presentationData.strings.User_DeletedAccount, font: titleFont, textColor: titleColor) } } else if let group = item.peer as? TelegramGroup { - titleAttributedString = NSAttributedString(string: group.title, font: currentBoldFont, textColor: titleColor) + titleAttributedString = NSAttributedString(string: group.title, font: titleFont, textColor: titleColor) } else if let channel = item.peer as? TelegramChannel { - titleAttributedString = NSAttributedString(string: channel.title, font: currentBoldFont, textColor: titleColor) + titleAttributedString = NSAttributedString(string: channel.title, font: titleFont, textColor: titleColor) + } + if let currentTitle = currentTitle, currentTitle != titleAttributedString?.string { + updatedTitle = true } var wavesColor = UIColor(rgb: 0x34c759) - 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.presentationData.strings.Bot_GroupStatusReadsHistory - } else { - botStatus = item.presentationData.strings.Bot_GroupStatusDoesNotReadHistory - } - 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, _) = stringAndActivityForUserPresence(strings: item.presentationData.strings, dateTimeFormat: item.dateTimeFormat, presence: presence, relativeTo: Int32(timestamp)) - statusAttributedString = NSAttributedString(string: string, font: statusFont, textColor: 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, textColor): - let textColorValue: UIColor + if case let .text(_, _, textColor) = item.text { switch textColor { - case .generic: - textColorValue = item.presentationData.theme.list.itemSecondaryTextColor - case .accent: - textColorValue = item.presentationData.theme.list.itemAccentColor - wavesColor = textColorValue - case .constructive: - textColorValue = UIColor(rgb: 0x34c759) - case .destructive: - textColorValue = UIColor(rgb: 0xff3b30) - wavesColor = textColorValue + case .accent: + wavesColor = accentColor + case .destructive: + wavesColor = destructiveColor + default: + break } - statusAttributedString = NSAttributedString(string: text, font: statusFont, textColor: textColorValue) - case .none: - break - } - - if let expandedText = item.expandedText, case let .text(text, textColor) = expandedText { - let textColorValue: UIColor - switch textColor { - case .generic: - textColorValue = item.presentationData.theme.list.itemSecondaryTextColor - case .accent: - textColorValue = item.presentationData.theme.list.itemAccentColor - case .constructive: - textColorValue = UIColor(rgb: 0x34c759) - case .destructive: - textColorValue = UIColor(rgb: 0xff3b30) - } - expandedStatusAttributedString = NSAttributedString(string: text, font: statusFont, textColor: textColorValue) - } else { - expandedStatusAttributedString = statusAttributedString } - let leftInset: CGFloat = 65.0 + params.leftInset + let leftInset: CGFloat = 58.0 + params.leftInset let verticalInset: CGFloat = 8.0 let verticalOffset: CGFloat = 0.0 - let avatarSize: CGFloat = 40.0 var titleIconsWidth: CGFloat = 0.0 var currentCredibilityIconImage: UIImage? @@ -444,102 +847,101 @@ class VoiceChatParticipantItemNode: ItemListRevealOptionsItemNode { if let currentCredibilityIconImage = currentCredibilityIconImage { titleIconsWidth += 4.0 + currentCredibilityIconImage.size.width } - - let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - 12.0 - rightInset - 30.0 - titleIconsWidth, 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 - rightInset - 30.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) - let (expandedStatusLayout, expandedStatusApply) = makeExpandedStatusLayout(TextNodeLayoutArguments(attributedString: expandedStatusAttributedString, backgroundColor: nil, maximumNumberOfLines: 4, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - 8.0 - rightInset - 30.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) - let insets = UIEdgeInsets() - - let titleSpacing: CGFloat = statusLayout.size.height == 0.0 ? 0.0 : 1.0 + var expandedRightInset: CGFloat = 30.0 + if item.peer.smallProfileImage != nil { + expandedRightInset = 0.0 + } + + let constrainedWidth = params.width - leftInset - 12.0 - rightInset - 30.0 - titleIconsWidth + let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: constrainedWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + + let expandedWidth = min(params.width - leftInset - rightInset, params.availableHeight - 30.0) + let (statusLayout, statusApply) = makeStatusLayout(CGSize(width: params.width - leftInset - 8.0 - rightInset - 30.0, height: CGFloat.greatestFiniteMagnitude), item.text, false) + let (expandedStatusLayout, expandedStatusApply) = makeExpandedStatusLayout(CGSize(width: expandedWidth - 8.0 - expandedRightInset, height: CGFloat.greatestFiniteMagnitude), item.expandedText ?? item.text, params.availableHeight > params.width) + + let titleSpacing: CGFloat = statusLayout.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 rawHeight: CGFloat = verticalInset * 2.0 + titleLayout.size.height + titleSpacing + statusLayout.height let contentSize = CGSize(width: params.width, height: max(minHeight, rawHeight)) + let insets = UIEdgeInsets() let separatorHeight = UIScreenPixel let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) let layoutSize = layout.size - if !item.enabled { - if currentDisabledOverlayNode == nil { - currentDisabledOverlayNode = ASDisplayNode() - currentDisabledOverlayNode?.backgroundColor = item.presentationData.theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.5) - } - } else { - currentDisabledOverlayNode = nil - } - var animateStatusTransitionFromUp: Bool? if let currentItem = currentItem { - if case .presence = currentItem.text, case let .text(_, newColor) = item.text { + if case let .text(_, _, currentColor) = currentItem.text, case let .text(_, _, newColor) = item.text, currentColor != newColor { animateStatusTransitionFromUp = newColor == .constructive - } else if case let .text(_, currentColor) = currentItem.text, case let .text(_, newColor) = item.text, currentColor != newColor { - animateStatusTransitionFromUp = newColor == .constructive - } else if case .text = currentItem.text, case .presence = item.text { - animateStatusTransitionFromUp = false } } - - let peerRevealOptions: [ItemListRevealOption] - var mappedOptions: [ItemListRevealOption] = [] - var index: Int32 = 0 - for option in item.revealOptions { - let color: UIColor - let textColor: UIColor - switch option.type { - case .neutral: - color = item.presentationData.theme.list.itemDisclosureActions.constructive.fillColor - textColor = item.presentationData.theme.list.itemDisclosureActions.constructive.foregroundColor - case .warning: - color = item.presentationData.theme.list.itemDisclosureActions.warning.fillColor - textColor = item.presentationData.theme.list.itemDisclosureActions.warning.foregroundColor - case .destructive: - color = item.presentationData.theme.list.itemDisclosureActions.destructive.fillColor - textColor = item.presentationData.theme.list.itemDisclosureActions.destructive.foregroundColor - case .accent: - color = item.presentationData.theme.list.itemDisclosureActions.accent.fillColor - textColor = item.presentationData.theme.list.itemDisclosureActions.accent.foregroundColor - } - mappedOptions.append(ItemListRevealOption(key: index, title: option.title, icon: .none, color: color, textColor: textColor)) - index += 1 - } - peerRevealOptions = mappedOptions - + return (layout, { [weak self] synchronousLoad, animated in if let strongSelf = self { + let hadItem = strongSelf.layoutParams?.0 != nil strongSelf.layoutParams = (item, params, first, last) + strongSelf.currentTitle = titleAttributedString?.string strongSelf.wavesColor = wavesColor - - let nonExtractedRect = CGRect(origin: CGPoint(), size: CGSize(width: layout.contentSize.width - 16.0, height: layout.contentSize.height)) - var extractedRect = CGRect(origin: CGPoint(), size: layout.contentSize).insetBy(dx: 16.0 + params.leftInset, dy: 0.0) - let extractedHeight = extractedRect.height + expandedStatusLayout.size.height - statusLayout.size.height + let nonExtractedRect: CGRect + let avatarFrame: CGRect + let titleFrame: CGRect + let animationSize: CGSize + let animationFrame: CGRect + let animationScale: CGFloat + + nonExtractedRect = CGRect(origin: CGPoint(x: 16.0, y: 0.0), size: CGSize(width: layout.contentSize.width - 32.0, height: layout.contentSize.height)) + avatarFrame = CGRect(origin: CGPoint(x: params.leftInset + 8.0, y: floorToScreenPixels((layout.contentSize.height - avatarSize) / 2.0)), size: CGSize(width: avatarSize, height: avatarSize)) + animationSize = CGSize(width: 36.0, height: 36.0) + animationScale = 1.0 + animationFrame = CGRect(x: params.width - animationSize.width - 6.0 - params.rightInset, y: floor((layout.contentSize.height - animationSize.height) / 2.0) + 1.0, width: animationSize.width, height: animationSize.height) + titleFrame = CGRect(origin: CGPoint(x: leftInset, y: verticalInset + verticalOffset), size: titleLayout.size) + + var extractedRect = CGRect(origin: CGPoint(), size: layout.contentSize).insetBy(dx: params.leftInset, dy: 0.0) + var extractedHeight = extractedRect.height + expandedStatusLayout.height - statusLayout.height + var extractedVerticalOffset: CGFloat = 0.0 + if item.peer.smallProfileImage != nil { + extractedRect.size.width = min(extractedRect.width, params.availableHeight - 20.0) + extractedVerticalOffset = extractedRect.width + extractedHeight += extractedVerticalOffset + } + extractedRect.size.height = extractedHeight + strongSelf.extractedVerticalOffset = extractedVerticalOffset strongSelf.extractedRect = extractedRect strongSelf.nonExtractedRect = nonExtractedRect - if strongSelf.contextSourceNode.isExtractedToContextPreview { - strongSelf.extractedBackgroundImageNode.frame = extractedRect + if strongSelf.isExtracted { + var extractedRect = extractedRect + if !extractedVerticalOffset.isZero { + extractedRect = CGRect(x: extractedRect.minX, y: extractedRect.minY + extractedVerticalOffset, width: extractedRect.width, height: extractedRect.height - extractedVerticalOffset) + } + strongSelf.backgroundImageNode.frame = extractedRect } else { - strongSelf.extractedBackgroundImageNode.frame = nonExtractedRect + strongSelf.backgroundImageNode.frame = nonExtractedRect } + strongSelf.extractedBackgroundImageNode.frame = strongSelf.backgroundImageNode.bounds strongSelf.contextSourceNode.contentRect = extractedRect - strongSelf.containerNode.frame = CGRect(origin: CGPoint(), size: layout.contentSize) - strongSelf.contextSourceNode.frame = CGRect(origin: CGPoint(), size: layout.contentSize) - strongSelf.offsetContainerNode.frame = CGRect(origin: CGPoint(), size: layout.contentSize) - strongSelf.contextSourceNode.contentNode.frame = CGRect(origin: CGPoint(), size: layout.contentSize) - strongSelf.containerNode.isGestureEnabled = item.contextAction != nil - strongSelf.actionContainerNode.frame = CGRect(origin: CGPoint(), size: layout.contentSize) + let contentBounds = CGRect(origin: CGPoint(), size: layout.contentSize) + strongSelf.containerNode.frame = contentBounds + strongSelf.contextSourceNode.frame = contentBounds + strongSelf.contentWrapperNode.frame = contentBounds + strongSelf.offsetContainerNode.frame = contentBounds + strongSelf.contextSourceNode.contentNode.frame = contentBounds + strongSelf.actionContainerNode.frame = contentBounds + strongSelf.containerNode.isGestureEnabled = item.contextAction != nil + strongSelf.accessibilityLabel = titleAttributedString?.string var combinedValueString = "" - if let statusString = statusAttributedString?.string, !statusString.isEmpty { - combinedValueString.append(statusString) - } +// if let statusString = statusAttributedString?.string, !statusString.isEmpty { +// combinedValueString.append(statusString) +// } strongSelf.accessibilityValue = combinedValueString @@ -550,27 +952,19 @@ class VoiceChatParticipantItemNode: ItemListRevealOptionsItemNode { } let transition: ContainedViewLayoutTransition - if animated { - transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring) + if animated && hadItem { + transition = ContainedViewLayoutTransition.animated(duration: 0.3, curve: .easeInOut) } else { transition = .immediate } - if let currentDisabledOverlayNode = currentDisabledOverlayNode { - if currentDisabledOverlayNode != strongSelf.disabledOverlayNode { - strongSelf.disabledOverlayNode = currentDisabledOverlayNode - strongSelf.addSubnode(currentDisabledOverlayNode) - currentDisabledOverlayNode.alpha = 0.0 - transition.updateAlpha(node: currentDisabledOverlayNode, alpha: 1.0) - currentDisabledOverlayNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: layout.contentSize.width, height: layout.contentSize.height - separatorHeight)) - } else { - transition.updateFrame(node: currentDisabledOverlayNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: layout.contentSize.width, height: layout.contentSize.height - separatorHeight))) - } - } else if let disabledOverlayNode = strongSelf.disabledOverlayNode { - transition.updateAlpha(node: disabledOverlayNode, alpha: 0.0, completion: { [weak disabledOverlayNode] _ in - disabledOverlayNode?.removeFromSupernode() + if updatedTitle, let snapshotView = strongSelf.titleNode.view.snapshotContentTree() { + strongSelf.titleNode.view.superview?.insertSubview(snapshotView, aboveSubview: strongSelf.titleNode.view) + + snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak snapshotView] _ in + snapshotView?.removeFromSuperview() }) - strongSelf.disabledOverlayNode = nil + strongSelf.titleNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) } if let animateStatusTransitionFromUp = animateStatusTransitionFromUp, !strongSelf.contextSourceNode.isExtractedToContextPreview { @@ -602,13 +996,13 @@ class VoiceChatParticipantItemNode: ItemListRevealOptionsItemNode { strongSelf.topStripeNode.isHidden = first strongSelf.bottomStripeNode.isHidden = last - + transition.updateFrame(node: strongSelf.topStripeNode, frame: CGRect(origin: CGPoint(x: leftInset, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight))) transition.updateFrame(node: strongSelf.bottomStripeNode, frame: CGRect(origin: CGPoint(x: leftInset, y: contentSize.height + -separatorHeight), size: CGSize(width: layoutSize.width - leftInset, height: separatorHeight))) - transition.updateFrame(node: strongSelf.titleNode, frame: CGRect(origin: CGPoint(x: leftInset, y: verticalInset + verticalOffset), size: titleLayout.size)) - transition.updateFrame(node: strongSelf.statusNode, frame: CGRect(origin: CGPoint(x: leftInset, y: strongSelf.titleNode.frame.maxY + titleSpacing), size: statusLayout.size)) - transition.updateFrame(node: strongSelf.expandedStatusNode, frame: CGRect(origin: CGPoint(x: leftInset, y: strongSelf.titleNode.frame.maxY + titleSpacing), size: expandedStatusLayout.size)) + transition.updateFrame(node: strongSelf.titleNode, frame: titleFrame) + transition.updateFrame(node: strongSelf.statusNode, frame: CGRect(origin: CGPoint(x: leftInset, y: strongSelf.titleNode.frame.maxY + titleSpacing), size: statusLayout)) + transition.updateFrame(node: strongSelf.expandedStatusNode, frame: CGRect(origin: CGPoint(x: leftInset, y: strongSelf.titleNode.frame.maxY + titleSpacing), size: expandedStatusLayout)) if let currentCredibilityIconImage = currentCredibilityIconImage { let iconNode: ASImageNode @@ -629,7 +1023,6 @@ class VoiceChatParticipantItemNode: ItemListRevealOptionsItemNode { credibilityIconNode.removeFromSupernode() } - let avatarFrame = CGRect(origin: CGPoint(x: params.leftInset + 15.0, y: floorToScreenPixels((layout.contentSize.height - avatarSize) / 2.0)), size: CGSize(width: avatarSize, height: avatarSize)) transition.updateFrameAsPositionAndBounds(node: strongSelf.avatarNode, frame: avatarFrame) let blobFrame = avatarFrame.insetBy(dx: -14.0, dy: -14.0) @@ -663,6 +1056,8 @@ class VoiceChatParticipantItemNode: ItemListRevealOptionsItemNode { audioLevelView.layer.mask = playbackMaskLayer audioLevelView.setColor(wavesColor) + audioLevelView.alpha = strongSelf.isExtracted ? 0.0 : 1.0 + strongSelf.audioLevelView = audioLevelView strongSelf.offsetContainerNode.view.insertSubview(audioLevelView, at: 0) } @@ -672,19 +1067,31 @@ class VoiceChatParticipantItemNode: ItemListRevealOptionsItemNode { audioLevelView.updateLevel(CGFloat(value)) let avatarScale: CGFloat - if value > 0.0 { + if value > 0.02 { audioLevelView.startAnimating() avatarScale = 1.03 + level * 0.13 if let wavesColor = strongSelf.wavesColor { audioLevelView.setColor(wavesColor, animated: true) } + + if let silenceTimer = strongSelf.silenceTimer { + silenceTimer.invalidate() + strongSelf.silenceTimer = nil + } } else { - audioLevelView.stopAnimating(duration: 0.5) avatarScale = 1.0 + if strongSelf.silenceTimer == nil { + let silenceTimer = SwiftSignalKit.Timer(timeout: 1.0, repeat: false, completion: { [weak self] in + self?.audioLevelView?.stopAnimating(duration: 0.75) + self?.silenceTimer = nil + }, queue: Queue.mainQueue()) + strongSelf.silenceTimer = silenceTimer + silenceTimer.start() + } } let transition: ContainedViewLayoutTransition = .animated(duration: 0.15, curve: .easeInOut) - transition.updateTransformScale(node: strongSelf.avatarNode, scale: avatarScale, beginWithCurrentState: true) + transition.updateTransformScale(node: strongSelf.avatarNode, scale: strongSelf.isExtracted ? 1.0 : avatarScale, beginWithCurrentState: true) } })) } @@ -699,9 +1106,13 @@ class VoiceChatParticipantItemNode: ItemListRevealOptionsItemNode { if item.peer.isDeleted { overrideImage = .deletedIcon } - strongSelf.avatarNode.setPeer(context: item.context, theme: item.presentationData.theme, peer: item.peer, overrideImage: overrideImage, emptyColor: item.presentationData.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, storeUnrounded: true) - strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: layout.contentSize.height + UIScreenPixel + UIScreenPixel)) + strongSelf.highlightContainerNode.frame = CGRect(origin: CGPoint(x: params.leftInset, y: -UIScreenPixel), size: CGSize(width: params.width - params.leftInset - params.rightInset, height: layout.contentSize.height + UIScreenPixel + UIScreenPixel + 11.0)) + + strongSelf.highlightContainerNode.cornerRadius = first ? 11.0 : 0.0 + + strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: params.width, height: layout.contentSize.height + UIScreenPixel + UIScreenPixel)) var hadMicrophoneNode = false var hadRaiseHandNode = false @@ -719,6 +1130,7 @@ class VoiceChatParticipantItemNode: ItemListRevealOptionsItemNode { nodeToAnimateIn = animationNode } + animationNode.alpha = 1.0 animationNode.update(state: VoiceChatMicrophoneNode.State(muted: muted, filled: false, color: color), animated: true) strongSelf.actionButtonNode.isUserInteractionEnabled = false } else if let animationNode = strongSelf.animationNode { @@ -795,40 +1207,16 @@ class VoiceChatParticipantItemNode: ItemListRevealOptionsItemNode { node.layer.animateScale(from: 0.001, to: 1.0, duration: 0.2) } - let videoSize = CGSize(width: avatarSize, height: avatarSize) + strongSelf.avatarNode.isHidden = strongSelf.isExtracted - let videoNode = item.getVideo() - if let current = strongSelf.videoNode, current !== videoNode { - current.removeFromSupernode() - } - let actionOffset: CGFloat = 0.0 - strongSelf.videoNode = videoNode - if let videoNode = videoNode { - videoNode.updateLayout(size: videoSize, transition: .immediate) - if videoNode.supernode !== strongSelf.avatarNode { - videoNode.clipsToBounds = true - videoNode.cornerRadius = avatarSize / 2.0 - strongSelf.avatarNode.addSubnode(videoNode) - } - - videoNode.frame = CGRect(origin: CGPoint(), size: videoSize) - } - - let animationSize = CGSize(width: 36.0, height: 36.0) strongSelf.iconNode?.frame = CGRect(origin: CGPoint(), size: animationSize) strongSelf.animationNode?.frame = CGRect(origin: CGPoint(), size: animationSize) strongSelf.raiseHandNode?.frame = CGRect(origin: CGPoint(), size: animationSize).insetBy(dx: -6.0, dy: -6.0).offsetBy(dx: -2.0, dy: 0.0) - strongSelf.actionButtonNode.frame = CGRect(x: params.width - animationSize.width - 6.0 - params.rightInset + actionOffset, y: floor((layout.contentSize.height - animationSize.height) / 2.0) + 1.0, width: animationSize.width, height: animationSize.height) - - if let presence = item.presence as? TelegramUserPresence { - strongSelf.peerPresenceManager?.reset(presence: presence) - } - + strongSelf.actionButtonNode.transform = CATransform3DMakeScale(animationScale, animationScale, 1.0) + transition.updateFrame(node: strongSelf.actionButtonNode, frame: animationFrame) + strongSelf.updateIsHighlighted(transition: transition) - - strongSelf.setRevealOptions((left: [], right: peerRevealOptions)) - strongSelf.setRevealOptionsOpened(item.revealed ?? false, animated: animated) } }) } @@ -837,8 +1225,8 @@ class VoiceChatParticipantItemNode: ItemListRevealOptionsItemNode { var isHighlighted = false func updateIsHighlighted(transition: ContainedViewLayoutTransition) { if self.isHighlighted { - self.highlightedBackgroundNode.alpha = 1.0 - if self.highlightedBackgroundNode.supernode == nil { + self.highlightContainerNode.alpha = 1.0 + if self.highlightContainerNode.supernode == nil { var anchorNode: ASDisplayNode? if self.bottomStripeNode.supernode != nil { anchorNode = self.bottomStripeNode @@ -846,24 +1234,24 @@ class VoiceChatParticipantItemNode: ItemListRevealOptionsItemNode { anchorNode = self.topStripeNode } if let anchorNode = anchorNode { - self.insertSubnode(self.highlightedBackgroundNode, aboveSubnode: anchorNode) + self.insertSubnode(self.highlightContainerNode, aboveSubnode: anchorNode) } else { - self.addSubnode(self.highlightedBackgroundNode) + self.addSubnode(self.highlightContainerNode) } } } else { - if self.highlightedBackgroundNode.supernode != nil { + if self.highlightContainerNode.supernode != nil { if transition.isAnimated { - self.highlightedBackgroundNode.layer.animateAlpha(from: self.highlightedBackgroundNode.alpha, to: 0.0, duration: 0.4, completion: { [weak self] completed in + self.highlightContainerNode.layer.animateAlpha(from: self.highlightContainerNode.alpha, to: 0.0, duration: 0.4, completion: { [weak self] completed in if let strongSelf = self { if completed { - strongSelf.highlightedBackgroundNode.removeFromSupernode() + strongSelf.highlightContainerNode.removeFromSupernode() } } }) - self.highlightedBackgroundNode.alpha = 0.0 + self.highlightContainerNode.alpha = 0.0 } else { - self.highlightedBackgroundNode.removeFromSupernode() + self.highlightContainerNode.removeFromSupernode() } } } @@ -885,7 +1273,7 @@ class VoiceChatParticipantItemNode: ItemListRevealOptionsItemNode { self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false) } - override func header() -> ListViewItemHeader? { + override func headers() -> [ListViewItemHeader]? { return nil } @@ -900,47 +1288,4 @@ class VoiceChatParticipantItemNode: ItemListRevealOptionsItemNode { contextAction(self.contextSourceNode, nil) } } - - override func updateRevealOffset(offset: CGFloat, transition: ContainedViewLayoutTransition) { - super.updateRevealOffset(offset: offset, transition: transition) - - if let _ = self.layoutParams?.0, let params = self.layoutParams?.1 { - let leftInset: CGFloat = 65.0 + params.leftInset - - var avatarFrame = self.avatarNode.frame - avatarFrame.origin.x = offset + leftInset - 50.0 - transition.updateFrame(node: self.avatarNode, frame: avatarFrame) - - var titleFrame = self.titleNode.frame - titleFrame.origin.x = leftInset + offset - transition.updateFrame(node: self.titleNode, frame: titleFrame) - - var statusFrame = self.statusNode.frame - let previousStatusFrame = statusFrame - statusFrame.origin.x = leftInset + offset - self.statusNode.frame = statusFrame - transition.animatePositionAdditive(node: self.statusNode, offset: CGPoint(x: previousStatusFrame.minX - statusFrame.minX, y: 0)) - } - } - - override func revealOptionsInteractivelyOpened() { - if let item = self.layoutParams?.0 { - item.setPeerIdWithRevealedOptions(item.peer.id, nil) - } - } - - override func revealOptionsInteractivelyClosed() { - if let item = self.layoutParams?.0 { - item.setPeerIdWithRevealedOptions(nil, item.peer.id) - } - } - - override func revealOptionSelected(_ option: ItemListRevealOption, animated: Bool) { - if let item = self.layoutParams?.0 { - item.revealOptions[Int(option.key)].action() - } - - self.setRevealOptionsOpened(false, animated: true) - self.revealOptionsInteractivelyClosed() - } } diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatPeerProfileNode.swift b/submodules/TelegramCallsUI/Sources/VoiceChatPeerProfileNode.swift new file mode 100644 index 0000000000..61472fcb1f --- /dev/null +++ b/submodules/TelegramCallsUI/Sources/VoiceChatPeerProfileNode.swift @@ -0,0 +1,504 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import SwiftSignalKit +import Postbox +import TelegramCore +import SyncCore +import TelegramPresentationData +import TelegramUIPreferences +import PresentationDataUtils +import AvatarNode +import TelegramStringFormatting +import ContextUI +import AccountContext +import LegacyComponents +import PeerInfoAvatarListNode + +private let backgroundCornerRadius: CGFloat = 14.0 + +final class VoiceChatPeerProfileNode: ASDisplayNode { + private let context: AccountContext + private let size: CGSize + private var peer: Peer + private var text: VoiceChatParticipantItem.ParticipantText + private let customNode: ASDisplayNode? + private let additionalEntry: Signal<(TelegramMediaImageRepresentation, Float)?, NoError> + + private let backgroundImageNode: ASImageNode + private let avatarListContainerNode: ASDisplayNode + let avatarListWrapperNode: PinchSourceContainerNode + let avatarListNode: PeerInfoAvatarListContainerNode + private var videoFadeNode: ASImageNode + private let infoNode: ASDisplayNode + private let titleNode: ImmediateTextNode + private let statusNode: VoiceChatParticipantStatusNode + + private var appeared = false + + init(context: AccountContext, size: CGSize, sourceSize: CGSize, peer: Peer, text: VoiceChatParticipantItem.ParticipantText, customNode: ASDisplayNode? = nil, additionalEntry: Signal<(TelegramMediaImageRepresentation, Float)?, NoError>, requestDismiss: (() -> Void)?) { + self.context = context + self.size = size + self.peer = peer + self.text = text + self.customNode = customNode + self.additionalEntry = additionalEntry + + self.backgroundImageNode = ASImageNode() + self.backgroundImageNode.clipsToBounds = true + self.backgroundImageNode.displaysAsynchronously = false + self.backgroundImageNode.displayWithoutProcessing = true + + self.videoFadeNode = ASImageNode() + self.videoFadeNode.displaysAsynchronously = false + self.videoFadeNode.contentMode = .scaleToFill + + self.avatarListContainerNode = ASDisplayNode() + self.avatarListContainerNode.clipsToBounds = true + + self.avatarListWrapperNode = PinchSourceContainerNode() + self.avatarListWrapperNode.clipsToBounds = true + self.avatarListWrapperNode.cornerRadius = backgroundCornerRadius + + self.avatarListNode = PeerInfoAvatarListContainerNode(context: context) + self.avatarListNode.backgroundColor = .clear + self.avatarListNode.peer = peer + self.avatarListNode.firstFullSizeOnly = true + self.avatarListNode.offsetLocation = true + self.avatarListNode.customCenterTapAction = { + requestDismiss?() + } + + self.infoNode = ASDisplayNode() + self.infoNode.clipsToBounds = true + + self.titleNode = ImmediateTextNode() + self.titleNode.isUserInteractionEnabled = false + self.titleNode.contentMode = .left + self.titleNode.contentsScale = UIScreen.main.scale + + self.statusNode = VoiceChatParticipantStatusNode() + self.statusNode.isUserInteractionEnabled = false + + super.init() + + self.clipsToBounds = true + + self.addSubnode(self.backgroundImageNode) + self.addSubnode(self.infoNode) + self.addSubnode(self.videoFadeNode) + self.addSubnode(self.avatarListWrapperNode) + self.infoNode.addSubnode(self.titleNode) + self.infoNode.addSubnode(self.statusNode) + + self.avatarListContainerNode.addSubnode(self.avatarListNode) + self.avatarListContainerNode.addSubnode(self.avatarListNode.controlsClippingOffsetNode) + self.avatarListWrapperNode.contentNode.addSubnode(self.avatarListContainerNode) + + self.avatarListWrapperNode.activate = { [weak self] sourceNode in + guard let strongSelf = self else { + return + } + strongSelf.avatarListNode.controlsContainerNode.alpha = 0.0 + let pinchController = PinchController(sourceNode: sourceNode, getContentAreaInScreenSpace: { + return UIScreen.main.bounds + }) + context.sharedContext.mainWindow?.presentInGlobalOverlay(pinchController) + } + self.avatarListWrapperNode.deactivated = { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.avatarListWrapperNode.contentNode.layer.animate(from: 0.0 as NSNumber, to: backgroundCornerRadius as NSNumber, keyPath: "cornerRadius", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, duration: 0.3, completion: { _ in + }) + } + self.avatarListWrapperNode.animatedOut = { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.avatarListNode.controlsContainerNode.alpha = 1.0 + strongSelf.avatarListNode.controlsContainerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) + } + + self.updateInfo(size: size, sourceSize: sourceSize, animate: false) + } + + func updateInfo(size: CGSize, sourceSize: CGSize, animate: Bool) { + let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } + + let titleFont = Font.regular(17.0) + let titleColor = UIColor.white + var titleAttributedString: NSAttributedString? + if let user = self.peer as? TelegramUser { + if let firstName = user.firstName, let lastName = user.lastName, !firstName.isEmpty, !lastName.isEmpty { + let string = NSMutableAttributedString() + switch presentationData.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: titleFont, textColor: titleColor)) + case .lastFirst: + string.append(NSAttributedString(string: lastName, font: titleFont, 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 { + titleAttributedString = NSAttributedString(string: firstName, font: titleFont, textColor: titleColor) + } else if let lastName = user.lastName, !lastName.isEmpty { + titleAttributedString = NSAttributedString(string: lastName, font: titleFont, textColor: titleColor) + } else { + titleAttributedString = NSAttributedString(string: presentationData.strings.User_DeletedAccount, font: titleFont, textColor: titleColor) + } + } else if let group = peer as? TelegramGroup { + titleAttributedString = NSAttributedString(string: group.title, font: titleFont, textColor: titleColor) + } else if let channel = peer as? TelegramChannel { + titleAttributedString = NSAttributedString(string: channel.title, font: titleFont, textColor: titleColor) + } + self.titleNode.attributedText = titleAttributedString + + let titleSize = self.titleNode.updateLayout(CGSize(width: self.size.width - 24.0, height: size.height)) + + let makeStatusLayout = self.statusNode.asyncLayout() + let (statusLayout, statusApply) = makeStatusLayout(CGSize(width: self.size.width - 24.0, height: CGFloat.greatestFiniteMagnitude), self.text, true) + let _ = statusApply() + + self.titleNode.frame = CGRect(origin: CGPoint(x: 14.0, y: 0.0), size: titleSize) + self.statusNode.frame = CGRect(origin: CGPoint(x: 14.0, y: titleSize.height + 3.0), size: statusLayout) + + let totalHeight = titleSize.height + statusLayout.height + 3.0 + 8.0 + let infoFrame = CGRect(x: 0.0, y: size.height - totalHeight, width: sourceSize.width, height: totalHeight) + + if animate { + let springDuration: Double = !self.appeared ? 0.42 : 0.3 + let springDamping: CGFloat = !self.appeared ? 124.0 : 1000.0 + + let initialInfoPosition = self.infoNode.position + self.infoNode.layer.position = infoFrame.center + let initialInfoBounds = self.infoNode.bounds + self.infoNode.layer.bounds = CGRect(origin: CGPoint(), size: infoFrame.size) + + self.infoNode.layer.animateSpring(from: NSValue(cgPoint: initialInfoPosition), to: NSValue(cgPoint: self.infoNode.position), keyPath: "position", duration: springDuration, delay: 0.0, initialVelocity: 0.0, damping: springDamping) + self.infoNode.layer.animateSpring(from: NSValue(cgRect: initialInfoBounds), to: NSValue(cgRect: self.infoNode.bounds), keyPath: "bounds", duration: springDuration, initialVelocity: 0.0, damping: springDamping) + } else { + self.infoNode.frame = infoFrame + } + } + + func animateIn(from sourceNode: ASDisplayNode, targetRect: CGRect, transition: ContainedViewLayoutTransition) { + let radiusTransition = ContainedViewLayoutTransition.animated(duration: 0.15, curve: .easeInOut) + let springDuration: Double = 0.42 + let springDamping: CGFloat = 124.0 + + if let sourceNode = sourceNode as? VoiceChatTileItemNode { + let sourceRect = sourceNode.bounds + self.backgroundImageNode.frame = sourceNode.bounds + self.updateInfo(size: sourceNode.bounds.size, sourceSize: sourceNode.bounds.size, animate: false) + self.updateInfo(size: targetRect.size, sourceSize: targetRect.size, animate: true) + + self.backgroundImageNode.image = generateImage(CGSize(width: backgroundCornerRadius * 2.0, height: backgroundCornerRadius * 2.0), rotatedContext: { (size, context) in + let bounds = CGRect(origin: CGPoint(), size: size) + context.clear(bounds) + + context.setFillColor(UIColor(rgb: 0x1c1c1e).cgColor) + context.fillEllipse(in: bounds) + context.fill(CGRect(x: 0.0, y: 0.0, width: size.width, height: size.height / 2.0)) + })?.stretchableImage(withLeftCapWidth: Int(backgroundCornerRadius), topCapHeight: Int(backgroundCornerRadius)) + self.backgroundImageNode.cornerRadius = backgroundCornerRadius + + transition.updateCornerRadius(node: self.backgroundImageNode, cornerRadius: 0.0) + + let initialRect = sourceRect + let initialScale: CGFloat = sourceRect.width / targetRect.width + + let targetSize = CGSize(width: targetRect.size.width, height: targetRect.size.width) + self.avatarListWrapperNode.update(size: targetSize, transition: .immediate) + self.avatarListWrapperNode.frame = CGRect(x: targetRect.minX, y: targetRect.minY, width: targetRect.width, height: targetRect.width + backgroundCornerRadius) + + self.avatarListContainerNode.frame = CGRect(origin: CGPoint(), size: targetSize) + self.avatarListContainerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + self.avatarListContainerNode.cornerRadius = targetRect.width / 2.0 + + var appearanceTransition = transition + if transition.isAnimated { + appearanceTransition = .animated(duration: springDuration, curve: .customSpring(damping: springDamping, initialVelocity: 0.0)) + } + + if let videoNode = sourceNode.videoNode { + videoNode.updateLayout(size: targetSize, layoutMode: .fillOrFitToSquare, transition: appearanceTransition) + appearanceTransition.updateFrame(node: videoNode, frame: CGRect(origin: CGPoint(), size: targetSize)) + appearanceTransition.updateFrame(node: sourceNode.videoContainerNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: targetSize.width, height: targetSize.height + backgroundCornerRadius))) + sourceNode.videoContainerNode.cornerRadius = backgroundCornerRadius + } + self.insertSubnode(sourceNode.videoContainerNode, belowSubnode: self.avatarListWrapperNode) + + if let snapshotView = sourceNode.infoNode.view.snapshotView(afterScreenUpdates: false) { + self.videoFadeNode.image = tileFadeImage + self.videoFadeNode.transform = CATransform3DMakeScale(1.0, -1.0, 1.0) + self.videoFadeNode.frame = CGRect(x: 0.0, y: sourceRect.height - sourceNode.fadeNode.frame.height, width: sourceRect.width, height: sourceNode.fadeNode.frame.height) + + self.insertSubnode(self.videoFadeNode, aboveSubnode: sourceNode.videoContainerNode) + self.view.insertSubview(snapshotView, aboveSubview: sourceNode.videoContainerNode.view) + snapshotView.frame = sourceRect + appearanceTransition.updateFrame(view: snapshotView, frame: CGRect(origin: CGPoint(x: 0.0, y: targetSize.height - snapshotView.frame.size.height), size: snapshotView.frame.size)) + snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in + snapshotView.removeFromSuperview() + }) + appearanceTransition.updateFrame(node: self.videoFadeNode, frame: CGRect(origin: CGPoint(x: 0.0, y: targetSize.height - self.videoFadeNode.frame.size.height), size: CGSize(width: targetSize.width, height: self.videoFadeNode.frame.height))) + self.videoFadeNode.alpha = 0.0 + self.videoFadeNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2) + } + + self.avatarListWrapperNode.layer.animateSpring(from: initialScale as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: springDuration, initialVelocity: 0.0, damping: springDamping) + self.avatarListWrapperNode.layer.animateSpring(from: NSValue(cgPoint: initialRect.center), to: NSValue(cgPoint: self.avatarListWrapperNode.position), keyPath: "position", duration: springDuration, initialVelocity: 0.0, damping: springDamping, completion: { [weak self] _ in + if let strongSelf = self { + strongSelf.avatarListNode.updateCustomItemsOnlySynchronously = false + strongSelf.avatarListNode.currentItemNode?.addSubnode(sourceNode.videoContainerNode) + } + }) + + radiusTransition.updateCornerRadius(node: self.avatarListContainerNode, cornerRadius: 0.0) + + self.avatarListWrapperNode.contentNode.clipsToBounds = true + + self.avatarListNode.frame = CGRect(x: targetRect.width / 2.0, y: targetRect.width / 2.0, width: targetRect.width, height: targetRect.width) + self.avatarListNode.controlsClippingNode.frame = CGRect(x: -targetRect.width / 2.0, y: -targetRect.width / 2.0, width: targetRect.width, height: targetRect.width) + self.avatarListNode.controlsClippingOffsetNode.frame = CGRect(origin: CGPoint(x: targetRect.width / 2.0, y: targetRect.width / 2.0), size: CGSize()) + self.avatarListNode.stripContainerNode.frame = CGRect(x: 0.0, y: 13.0, width: targetRect.width, height: 2.0) + self.avatarListNode.shadowNode.frame = CGRect(x: 0.0, y: 0.0, width: targetRect.width, height: 44.0) + + self.avatarListNode.updateCustomItemsOnlySynchronously = true + self.avatarListNode.update(size: targetSize, peer: self.peer, customNode: self.customNode, additionalEntry: self.additionalEntry, isExpanded: true, transition: .immediate) + + let backgroundTargetRect = CGRect(x: 0.0, y: targetSize.height - backgroundCornerRadius * 2.0, width: targetRect.width, height: targetRect.height - targetSize.height + backgroundCornerRadius * 2.0) + let initialBackgroundPosition = self.backgroundImageNode.position + self.backgroundImageNode.layer.position = backgroundTargetRect.center + let initialBackgroundBounds = self.backgroundImageNode.bounds + self.backgroundImageNode.layer.bounds = CGRect(origin: CGPoint(), size: backgroundTargetRect.size) + + self.backgroundImageNode.layer.animateSpring(from: NSValue(cgPoint: initialBackgroundPosition), to: NSValue(cgPoint: self.backgroundImageNode.position), keyPath: "position", duration: springDuration, delay: 0.0, initialVelocity: 0.0, damping: springDamping) + self.backgroundImageNode.layer.animateSpring(from: NSValue(cgRect: initialBackgroundBounds), to: NSValue(cgRect: self.backgroundImageNode.bounds), keyPath: "bounds", duration: springDuration, initialVelocity: 0.0, damping: springDamping) + } else if let sourceNode = sourceNode as? VoiceChatFullscreenParticipantItemNode { + let sourceRect = sourceNode.bounds + self.backgroundImageNode.frame = sourceNode.bounds + self.updateInfo(size: sourceNode.bounds.size, sourceSize: sourceNode.bounds.size, animate: false) + self.updateInfo(size: targetRect.size, sourceSize: targetRect.size, animate: true) + + self.backgroundImageNode.image = generateImage(CGSize(width: backgroundCornerRadius * 2.0, height: backgroundCornerRadius * 2.0), rotatedContext: { (size, context) in + let bounds = CGRect(origin: CGPoint(), size: size) + context.clear(bounds) + + context.setFillColor(UIColor(rgb: 0x1c1c1e).cgColor) + context.fillEllipse(in: bounds) + context.fill(CGRect(x: 0.0, y: 0.0, width: size.width, height: size.height / 2.0)) + })?.stretchableImage(withLeftCapWidth: Int(backgroundCornerRadius), topCapHeight: Int(backgroundCornerRadius)) + self.backgroundImageNode.cornerRadius = backgroundCornerRadius + + transition.updateCornerRadius(node: self.backgroundImageNode, cornerRadius: 0.0) + + let initialRect: CGRect + let hasVideo: Bool + if let videoNode = sourceNode.videoNode, videoNode.supernode == sourceNode.videoContainerNode, !videoNode.alpha.isZero { + initialRect = sourceRect + hasVideo = true + } else { + initialRect = sourceNode.avatarNode.frame + hasVideo = false + } + let initialScale = initialRect.width / targetRect.width + + let targetSize = CGSize(width: targetRect.size.width, height: targetRect.size.width) + self.avatarListWrapperNode.update(size: targetSize, transition: .immediate) + self.avatarListWrapperNode.frame = CGRect(x: targetRect.minX, y: targetRect.minY, width: targetRect.width, height: targetRect.width + backgroundCornerRadius) + + self.avatarListContainerNode.frame = CGRect(origin: CGPoint(), size: targetSize) + self.avatarListContainerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + self.avatarListContainerNode.cornerRadius = targetRect.width / 2.0 + + var appearanceTransition = transition + if transition.isAnimated { + appearanceTransition = .animated(duration: springDuration, curve: .customSpring(damping: springDamping, initialVelocity: 0.0)) + } + + if let videoNode = sourceNode.videoNode, hasVideo { + videoNode.updateLayout(size: targetSize, layoutMode: .fillOrFitToSquare, transition: appearanceTransition) + appearanceTransition.updateFrame(node: videoNode, frame: CGRect(origin: CGPoint(), size: targetSize)) + appearanceTransition.updateFrame(node: sourceNode.videoFadeNode, frame: CGRect(origin: CGPoint(x: 0.0, y: targetSize.height - fadeHeight), size: CGSize(width: targetSize.width, height: fadeHeight))) + appearanceTransition.updateTransformScale(node: sourceNode.videoContainerNode, scale: 1.0) + appearanceTransition.updateFrame(node: sourceNode.videoContainerNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: targetSize.width, height: targetSize.height + backgroundCornerRadius))) + sourceNode.videoContainerNode.cornerRadius = backgroundCornerRadius + appearanceTransition.updateAlpha(node: sourceNode.videoFadeNode, alpha: 0.0) + } else { + let transitionNode = ASImageNode() + transitionNode.clipsToBounds = true + transitionNode.displaysAsynchronously = false + transitionNode.displayWithoutProcessing = true + transitionNode.image = sourceNode.avatarNode.unroundedImage + transitionNode.frame = CGRect(origin: CGPoint(), size: targetSize) + transitionNode.cornerRadius = targetRect.width / 2.0 + radiusTransition.updateCornerRadius(node: transitionNode, cornerRadius: 0.0) + + sourceNode.avatarNode.isHidden = true + self.avatarListWrapperNode.contentNode.insertSubnode(transitionNode, at: 0) + } + self.insertSubnode(sourceNode.videoContainerNode, belowSubnode: self.avatarListWrapperNode) + + self.avatarListWrapperNode.layer.animateSpring(from: initialScale as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: springDuration, initialVelocity: 0.0, damping: springDamping) + self.avatarListWrapperNode.layer.animateSpring(from: NSValue(cgPoint: initialRect.center), to: NSValue(cgPoint: self.avatarListWrapperNode.position), keyPath: "position", duration: springDuration, initialVelocity: 0.0, damping: springDamping, completion: { [weak self] _ in + if let strongSelf = self { + strongSelf.avatarListNode.updateCustomItemsOnlySynchronously = false + strongSelf.avatarListNode.currentItemNode?.addSubnode(sourceNode.videoContainerNode) + } + }) + + radiusTransition.updateCornerRadius(node: self.avatarListContainerNode, cornerRadius: 0.0) + + self.avatarListWrapperNode.contentNode.clipsToBounds = true + + self.avatarListNode.frame = CGRect(x: targetRect.width / 2.0, y: targetRect.width / 2.0, width: targetRect.width, height: targetRect.width) + self.avatarListNode.controlsClippingNode.frame = CGRect(x: -targetRect.width / 2.0, y: -targetRect.width / 2.0, width: targetRect.width, height: targetRect.width) + self.avatarListNode.controlsClippingOffsetNode.frame = CGRect(origin: CGPoint(x: targetRect.width / 2.0, y: targetRect.width / 2.0), size: CGSize()) + self.avatarListNode.stripContainerNode.frame = CGRect(x: 0.0, y: 13.0, width: targetRect.width, height: 2.0) + self.avatarListNode.shadowNode.frame = CGRect(x: 0.0, y: 0.0, width: targetRect.width, height: 44.0) + + self.avatarListNode.updateCustomItemsOnlySynchronously = true + self.avatarListNode.update(size: targetSize, peer: self.peer, customNode: self.customNode, additionalEntry: self.additionalEntry, isExpanded: true, transition: .immediate) + + let backgroundTargetRect = CGRect(x: 0.0, y: targetSize.height - backgroundCornerRadius * 2.0, width: targetRect.width, height: targetRect.height - targetSize.height + backgroundCornerRadius * 2.0) + let initialBackgroundPosition = self.backgroundImageNode.position + self.backgroundImageNode.layer.position = backgroundTargetRect.center + let initialBackgroundBounds = self.backgroundImageNode.bounds + self.backgroundImageNode.layer.bounds = CGRect(origin: CGPoint(), size: backgroundTargetRect.size) + + self.backgroundImageNode.layer.animateSpring(from: NSValue(cgPoint: initialBackgroundPosition), to: NSValue(cgPoint: self.backgroundImageNode.position), keyPath: "position", duration: springDuration, delay: 0.0, initialVelocity: 0.0, damping: springDamping) + self.backgroundImageNode.layer.animateSpring(from: NSValue(cgRect: initialBackgroundBounds), to: NSValue(cgRect: self.backgroundImageNode.bounds), keyPath: "bounds", duration: springDuration, initialVelocity: 0.0, damping: springDamping) + } + self.appeared = true + } + + func animateOut(to targetNode: ASDisplayNode, targetRect: CGRect, transition: ContainedViewLayoutTransition, completion: @escaping () -> Void = {}) { + let radiusTransition = ContainedViewLayoutTransition.animated(duration: 0.2, curve: .easeInOut) + let springDuration: Double = 0.3 + let springDamping: CGFloat = 1000.0 + if let targetNode = targetNode as? VoiceChatTileItemNode { + let initialSize = self.bounds + self.updateInfo(size: targetRect.size, sourceSize: targetRect.size, animate: true) + + transition.updateCornerRadius(node: self.backgroundImageNode, cornerRadius: backgroundCornerRadius) + + let targetScale = targetRect.width / avatarListContainerNode.frame.width + + self.insertSubnode(targetNode.videoContainerNode, belowSubnode: self.avatarListWrapperNode) + self.insertSubnode(self.videoFadeNode, aboveSubnode: targetNode.videoContainerNode) + self.avatarListWrapperNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) + + self.avatarListWrapperNode.layer.animate(from: 1.0 as NSNumber, to: targetScale as NSNumber, keyPath: "transform.scale", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, duration: 0.2, removeOnCompletion: false) + self.avatarListWrapperNode.layer.animate(from: NSValue(cgPoint: self.avatarListWrapperNode.position), to: NSValue(cgPoint: targetRect.center), keyPath: "position", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, duration: 0.2, removeOnCompletion: false, completion: { [weak self, weak targetNode] _ in + if let targetNode = targetNode { + targetNode.contentNode.insertSubnode(targetNode.videoContainerNode, aboveSubnode: targetNode.backgroundNode) + } + completion() + self?.removeFromSupernode() + }) + + radiusTransition.updateCornerRadius(node: self.avatarListContainerNode, cornerRadius: backgroundCornerRadius) + + if let snapshotView = targetNode.infoNode.view.snapshotView(afterScreenUpdates: true) { + self.view.insertSubview(snapshotView, aboveSubview: targetNode.videoContainerNode.view) + let snapshotFrame = snapshotView.frame + snapshotView.frame = CGRect(origin: CGPoint(x: 0.0, y: initialSize.width - snapshotView.frame.size.height), size: snapshotView.frame.size) + transition.updateFrame(view: snapshotView, frame: snapshotFrame) + snapshotView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + transition.updateFrame(node: self.videoFadeNode, frame: CGRect(origin: CGPoint(x: 0.0, y: targetRect.height - self.videoFadeNode.frame.size.height), size: CGSize(width: targetRect.width, height: self.videoFadeNode.frame.height))) + self.videoFadeNode.alpha = 1.0 + self.videoFadeNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } + + if let videoNode = targetNode.videoNode { + videoNode.updateLayout(size: targetRect.size, layoutMode: .fillOrFitToSquare, transition: transition) + transition.updateFrame(node: videoNode, frame: targetRect) + transition.updateFrame(node: targetNode.videoContainerNode, frame: targetRect) + } + + let backgroundTargetRect = targetRect + let initialBackgroundPosition = self.backgroundImageNode.position + self.backgroundImageNode.layer.position = backgroundTargetRect.center + let initialBackgroundBounds = self.backgroundImageNode.bounds + self.backgroundImageNode.layer.bounds = CGRect(origin: CGPoint(), size: backgroundTargetRect.size) + + self.backgroundImageNode.layer.animateSpring(from: NSValue(cgPoint: initialBackgroundPosition), to: NSValue(cgPoint: self.backgroundImageNode.position), keyPath: "position", duration: springDuration, delay: 0.0, initialVelocity: 0.0, damping: springDamping) + self.backgroundImageNode.layer.animateSpring(from: NSValue(cgRect: initialBackgroundBounds), to: NSValue(cgRect: self.backgroundImageNode.bounds), keyPath: "bounds", duration: springDuration, initialVelocity: 0.0, damping: springDamping) + + self.avatarListNode.stripContainerNode.alpha = 0.0 + self.avatarListNode.stripContainerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2) + + self.avatarListNode.shadowNode.alpha = 0.0 + self.avatarListNode.shadowNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2) + + self.infoNode.alpha = 0.0 + self.infoNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2) + } else if let targetNode = targetNode as? VoiceChatFullscreenParticipantItemNode { + let backgroundTargetRect = targetRect + + let initialSize = self.bounds + self.updateInfo(size: targetRect.size, sourceSize: targetRect.size, animate: true) + + targetNode.avatarNode.isHidden = false + + transition.updateCornerRadius(node: self.backgroundImageNode, cornerRadius: backgroundCornerRadius) + + var targetRect = targetRect + let hasVideo: Bool + if let videoNode = targetNode.videoNode, !videoNode.alpha.isZero { + hasVideo = true + } else { + targetRect = targetNode.avatarNode.frame + hasVideo = false + } + let targetScale = targetRect.width / self.avatarListContainerNode.frame.width + + self.insertSubnode(targetNode.videoContainerNode, belowSubnode: self.avatarListWrapperNode) + self.insertSubnode(self.videoFadeNode, aboveSubnode: targetNode.videoContainerNode) + self.avatarListWrapperNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) + + self.avatarListWrapperNode.layer.animate(from: 1.0 as NSNumber, to: targetScale as NSNumber, keyPath: "transform.scale", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, duration: 0.2, removeOnCompletion: false) + self.avatarListWrapperNode.layer.animate(from: NSValue(cgPoint: self.avatarListWrapperNode.position), to: NSValue(cgPoint: targetRect.center), keyPath: "position", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, duration: 0.2, removeOnCompletion: false, completion: { [weak self, weak targetNode] _ in + if let targetNode = targetNode { + targetNode.offsetContainerNode.insertSubnode(targetNode.videoContainerNode, at: 0) + } + completion() + self?.removeFromSupernode() + }) + + radiusTransition.updateCornerRadius(node: self.avatarListContainerNode, cornerRadius: backgroundCornerRadius) + + if hasVideo, let videoNode = targetNode.videoNode { + videoNode.updateLayout(size: CGSize(width: 180.0, height: 180.0), layoutMode: .fillOrFitToSquare, transition: transition) + transition.updateFrame(node: videoNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: 180.0, height: 180.0))) + transition.updateTransformScale(node: targetNode.videoContainerNode, scale: 84.0 / 180.0) + transition.updateFrameAsPositionAndBounds(node: targetNode.videoContainerNode, frame: CGRect(x: 0.0, y: 0.0, width: 180.0, height: 180.0)) + transition.updatePosition(node: targetNode.videoContainerNode, position: CGPoint(x: 42.0, y: 42.0)) + transition.updateFrame(node: targetNode.videoFadeNode, frame: CGRect(x: 0.0, y: 180.0 - fadeHeight, width: 180.0, height: fadeHeight)) + transition.updateAlpha(node: targetNode.videoFadeNode, alpha: 1.0) + } + + let initialBackgroundPosition = self.backgroundImageNode.position + self.backgroundImageNode.layer.position = backgroundTargetRect.center + let initialBackgroundBounds = self.backgroundImageNode.bounds + self.backgroundImageNode.layer.bounds = CGRect(origin: CGPoint(), size: backgroundTargetRect.size) + + self.backgroundImageNode.layer.animateSpring(from: NSValue(cgPoint: initialBackgroundPosition), to: NSValue(cgPoint: self.backgroundImageNode.position), keyPath: "position", duration: springDuration, delay: 0.0, initialVelocity: 0.0, damping: springDamping) + self.backgroundImageNode.layer.animateSpring(from: NSValue(cgRect: initialBackgroundBounds), to: NSValue(cgRect: self.backgroundImageNode.bounds), keyPath: "bounds", duration: springDuration, initialVelocity: 0.0, damping: springDamping) + + self.avatarListNode.stripContainerNode.alpha = 0.0 + self.avatarListNode.stripContainerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2) + + self.avatarListNode.shadowNode.alpha = 0.0 + self.avatarListNode.shadowNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2) + + self.infoNode.alpha = 0.0 + self.infoNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2) + } + } +} diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatPinNode.swift b/submodules/TelegramCallsUI/Sources/VoiceChatPinNode.swift new file mode 100644 index 0000000000..613dd8c150 --- /dev/null +++ b/submodules/TelegramCallsUI/Sources/VoiceChatPinNode.swift @@ -0,0 +1,201 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display + +private let iconImage = generateTintedImage(image: UIImage(bundleImageName: "Call/Pin"), color: .white) + +private final class VoiceChatPinNodeDrawingState: NSObject { + let color: UIColor + let transition: CGFloat + let reverse: Bool + + init(color: UIColor, transition: CGFloat, reverse: Bool) { + self.color = color + self.transition = transition + self.reverse = reverse + + super.init() + } +} + +final class VoiceChatPinNode: ASDisplayNode { + class State: Equatable { + let pinned: Bool + let color: UIColor + + init(pinned: Bool, color: UIColor) { + self.pinned = pinned + self.color = color + } + + static func ==(lhs: State, rhs: State) -> Bool { + if lhs.pinned != rhs.pinned { + return false + } + if lhs.color.argb != rhs.color.argb { + return false + } + return true + } + } + + private class TransitionContext { + let startTime: Double + let duration: Double + let previousState: State + + init(startTime: Double, duration: Double, previousState: State) { + self.startTime = startTime + self.duration = duration + self.previousState = previousState + } + } + + private var animator: ConstantDisplayLinkAnimator? + + private var hasState = false + private var state: State = State(pinned: false, color: .black) + private var transitionContext: TransitionContext? + + override init() { + super.init() + + self.isOpaque = false + } + + func update(state: State, animated: Bool) { + var animated = animated + if !self.hasState { + self.hasState = true + animated = false + } + + if self.state != state { + let previousState = self.state + self.state = state + + if animated { + self.transitionContext = TransitionContext(startTime: CACurrentMediaTime(), duration: 0.18, previousState: previousState) + } + + self.updateAnimations() + self.setNeedsDisplay() + } + } + + private func updateAnimations() { + var animate = false + let timestamp = CACurrentMediaTime() + + if let transitionContext = self.transitionContext { + if transitionContext.startTime + transitionContext.duration < timestamp { + self.transitionContext = nil + } else { + 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() + } + + override public func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol? { + var transitionFraction: CGFloat = self.state.pinned ? 1.0 : 0.0 + var color = self.state.color + + var reverse = false + if let transitionContext = self.transitionContext { + let timestamp = CACurrentMediaTime() + var t = CGFloat((timestamp - transitionContext.startTime) / transitionContext.duration) + t = min(1.0, max(0.0, t)) + + if transitionContext.previousState.pinned != self.state.pinned { + transitionFraction = self.state.pinned ? t : 1.0 - t + + reverse = transitionContext.previousState.pinned + } + + if transitionContext.previousState.color.rgb != color.rgb { + color = transitionContext.previousState.color.interpolateTo(color, fraction: t)! + } + } + + return VoiceChatPinNodeDrawingState(color: color, transition: transitionFraction, reverse: reverse) + } + + @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? VoiceChatPinNodeDrawingState else { + return + } + + context.setFillColor(parameters.color.cgColor) + + let clearLineWidth: CGFloat = 2.0 + let lineWidth: CGFloat = 1.0 + UIScreenPixel + if let iconImage = iconImage?.cgImage { + context.saveGState() + context.translateBy(x: bounds.midX, y: bounds.midY) + context.scaleBy(x: 1.0, y: -1.0) + context.translateBy(x: -bounds.midX, y: -bounds.midY) + context.draw(iconImage, in: CGRect(origin: CGPoint(), size: CGSize(width: 48.0, height: 48.0))) + context.restoreGState() + } + + if parameters.transition > 0.0 { + let startPoint: CGPoint + let endPoint: CGPoint + + let origin = CGPoint(x: 14.0, y: 16.0 - UIScreenPixel) + let length: CGFloat = 17.0 + + if parameters.reverse { + startPoint = CGPoint(x: origin.x + length * (1.0 - parameters.transition), y: origin.y + length * (1.0 - parameters.transition)).offsetBy(dx: UIScreenPixel, dy: -UIScreenPixel) + endPoint = CGPoint(x: origin.x + length, y: origin.y + length).offsetBy(dx: UIScreenPixel, dy: -UIScreenPixel) + } else { + startPoint = origin.offsetBy(dx: UIScreenPixel, dy: -UIScreenPixel) + endPoint = CGPoint(x: origin.x + length * parameters.transition, y: origin.y + length * parameters.transition).offsetBy(dx: UIScreenPixel, dy: -UIScreenPixel) + } + + + context.setBlendMode(.clear) + context.setLineWidth(clearLineWidth) + + context.move(to: startPoint.offsetBy(dx: 0.0, dy: 1.0 + UIScreenPixel)) + context.addLine(to: endPoint.offsetBy(dx: 0.0, dy: 1.0 + UIScreenPixel)) + context.strokePath() + + context.setBlendMode(.normal) + context.setStrokeColor(parameters.color.cgColor) + context.setLineWidth(lineWidth) + context.setLineCap(.round) + context.setLineJoin(.round) + + context.move(to: startPoint) + context.addLine(to: endPoint) + context.strokePath() + } + } +} diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatRecordingContextItem.swift b/submodules/TelegramCallsUI/Sources/VoiceChatRecordingContextItem.swift index ae86077f45..235c917d83 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatRecordingContextItem.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatRecordingContextItem.swift @@ -22,14 +22,14 @@ func generateStartRecordingIcon(color: UIColor) -> UIImage? { final class VoiceChatRecordingContextItem: ContextMenuCustomItem { fileprivate let timestamp: Int32 - fileprivate let action: (ContextController, @escaping (ContextMenuActionResult) -> Void) -> Void + fileprivate let action: (ContextControllerProtocol, @escaping (ContextMenuActionResult) -> Void) -> Void - init(timestamp: Int32, action: @escaping (ContextController, @escaping (ContextMenuActionResult) -> Void) -> Void) { + init(timestamp: Int32, action: @escaping (ContextControllerProtocol, @escaping (ContextMenuActionResult) -> Void) -> Void) { self.timestamp = timestamp self.action = action } - func node(presentationData: PresentationData, getController: @escaping () -> ContextController?, actionSelected: @escaping (ContextMenuActionResult) -> Void) -> ContextMenuCustomNode { + func node(presentationData: PresentationData, getController: @escaping () -> ContextControllerProtocol?, actionSelected: @escaping (ContextMenuActionResult) -> Void) -> ContextMenuCustomNode { return VoiceChatRecordingContextItemNode(presentationData: presentationData, item: self, getController: getController, actionSelected: actionSelected) } } @@ -94,7 +94,7 @@ class VoiceChatRecordingIconNode: ASDisplayNode { private final class VoiceChatRecordingContextItemNode: ASDisplayNode, ContextMenuCustomNode { private let item: VoiceChatRecordingContextItem private let presentationData: PresentationData - private let getController: () -> ContextController? + private let getController: () -> ContextControllerProtocol? private let actionSelected: (ContextMenuActionResult) -> Void private let backgroundNode: ASDisplayNode @@ -108,7 +108,7 @@ private final class VoiceChatRecordingContextItemNode: ASDisplayNode, ContextMen private var pointerInteraction: PointerInteraction? - init(presentationData: PresentationData, item: VoiceChatRecordingContextItem, getController: @escaping () -> ContextController?, actionSelected: @escaping (ContextMenuActionResult) -> Void) { + init(presentationData: PresentationData, item: VoiceChatRecordingContextItem, getController: @escaping () -> ContextControllerProtocol?, actionSelected: @escaping (ContextMenuActionResult) -> Void) { self.item = item self.presentationData = presentationData self.getController = getController diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatShareScreenContextItem.swift b/submodules/TelegramCallsUI/Sources/VoiceChatShareScreenContextItem.swift new file mode 100644 index 0000000000..a91ab9b17b --- /dev/null +++ b/submodules/TelegramCallsUI/Sources/VoiceChatShareScreenContextItem.swift @@ -0,0 +1,188 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import SwiftSignalKit +import TelegramPresentationData +import AppBundle +import ContextUI +import TelegramStringFormatting +import ReplayKit +import AccountContext + +final class VoiceChatShareScreenContextItem: ContextMenuCustomItem { + fileprivate let context: AccountContext + fileprivate let text: String + fileprivate let icon: (PresentationTheme) -> UIImage? + fileprivate let action: (ContextControllerProtocol, @escaping (ContextMenuActionResult) -> Void) -> Void + + init(context: AccountContext, text: String, icon: @escaping (PresentationTheme) -> UIImage?, action: @escaping (ContextControllerProtocol, @escaping (ContextMenuActionResult) -> Void) -> Void) { + self.context = context + self.text = text + self.icon = icon + self.action = action + } + + func node(presentationData: PresentationData, getController: @escaping () -> ContextControllerProtocol?, actionSelected: @escaping (ContextMenuActionResult) -> Void) -> ContextMenuCustomNode { + return VoiceChatShareScreenContextItemNode(presentationData: presentationData, item: self, getController: getController, actionSelected: actionSelected) + } +} + +private let textFont = Font.regular(17.0) + +private final class VoiceChatShareScreenContextItemNode: ASDisplayNode, ContextMenuCustomNode { + private let item: VoiceChatShareScreenContextItem + private let presentationData: PresentationData + private let getController: () -> ContextControllerProtocol? + private let actionSelected: (ContextMenuActionResult) -> Void + + private let backgroundNode: ASDisplayNode + private let highlightedBackgroundNode: ASDisplayNode + private let textNode: ImmediateTextNode + private let iconNode: ASImageNode + + private var timer: SwiftSignalKit.Timer? + + private var pointerInteraction: PointerInteraction? + + private var broadcastPickerView: UIView? + private var applicationStateDisposable: Disposable? + + init(presentationData: PresentationData, item: VoiceChatShareScreenContextItem, getController: @escaping () -> ContextControllerProtocol?, actionSelected: @escaping (ContextMenuActionResult) -> Void) { + self.item = item + self.presentationData = presentationData + self.getController = getController + self.actionSelected = actionSelected + + let textFont = Font.regular(presentationData.listsFontSize.baseDisplaySize) + + self.backgroundNode = ASDisplayNode() + self.backgroundNode.isAccessibilityElement = false + self.backgroundNode.backgroundColor = presentationData.theme.contextMenu.itemBackgroundColor + self.highlightedBackgroundNode = ASDisplayNode() + self.highlightedBackgroundNode.isAccessibilityElement = false + self.highlightedBackgroundNode.backgroundColor = presentationData.theme.contextMenu.itemHighlightedBackgroundColor + self.highlightedBackgroundNode.alpha = 0.0 + + self.textNode = ImmediateTextNode() + self.textNode.isAccessibilityElement = false + self.textNode.isUserInteractionEnabled = false + self.textNode.displaysAsynchronously = false + self.textNode.attributedText = NSAttributedString(string: item.text, font: textFont, textColor: presentationData.theme.contextMenu.primaryColor) + self.textNode.maximumNumberOfLines = 1 + + if #available(iOS 12.0, *) { + let broadcastPickerView = RPSystemBroadcastPickerView(frame: CGRect(x: 0, y: 0, width: 50, height: 52.0)) + broadcastPickerView.alpha = 0.02 + broadcastPickerView.preferredExtension = "\(item.context.sharedContext.applicationBindings.appBundleId).BroadcastUpload" + broadcastPickerView.showsMicrophoneButton = false + self.broadcastPickerView = broadcastPickerView + } + + self.iconNode = ASImageNode() + self.iconNode.isAccessibilityElement = false + self.iconNode.displaysAsynchronously = false + self.iconNode.displayWithoutProcessing = true + self.iconNode.isUserInteractionEnabled = false + self.iconNode.image = item.icon(presentationData.theme) + + super.init() + + self.addSubnode(self.backgroundNode) + self.addSubnode(self.highlightedBackgroundNode) + if let broadcastPickerView = self.broadcastPickerView { + self.view.addSubview(broadcastPickerView) + } + self.addSubnode(self.textNode) + self.addSubnode(self.iconNode) + } + + deinit { + self.timer?.invalidate() + self.applicationStateDisposable?.dispose() + } + + override func didLoad() { + super.didLoad() + + self.applicationStateDisposable = (self.item.context.sharedContext.applicationBindings.applicationIsActive + |> filter { !$0 } + |> take(1) + |> deliverOnMainQueue).start(next: { [weak self] _ in + guard let strongSelf = self else { + return + } + strongSelf.getController()?.dismiss(completion: nil) + }) + } + + private var validLayout: CGSize? + func updateLayout(constrainedWidth: CGFloat) -> (CGSize, (CGSize, ContainedViewLayoutTransition) -> Void) { + let sideInset: CGFloat = 16.0 + let iconSideInset: CGFloat = 12.0 + let verticalInset: CGFloat = 12.0 + + let iconSize = self.iconNode.image.flatMap({ $0.size }) ?? CGSize() + + let standardIconWidth: CGFloat = 32.0 + var rightTextInset: CGFloat = sideInset + if !iconSize.width.isZero { + rightTextInset = max(iconSize.width, standardIconWidth) + iconSideInset + sideInset + } + + let textSize = self.textNode.updateLayout(CGSize(width: constrainedWidth - sideInset - rightTextInset, height: .greatestFiniteMagnitude)) + + + let verticalSpacing: CGFloat = 2.0 + let combinedTextHeight = textSize.height + verticalSpacing + return (CGSize(width: textSize.width + sideInset + rightTextInset, height: verticalInset * 2.0 + combinedTextHeight), { size, transition in + self.validLayout = size + + let verticalOrigin = floor((size.height - combinedTextHeight) / 2.0) + let textFrame = CGRect(origin: CGPoint(x: sideInset, y: verticalOrigin), size: textSize) + transition.updateFrameAdditive(node: self.textNode, frame: textFrame) + + if !iconSize.width.isZero { + transition.updateFrameAdditive(node: self.iconNode, frame: 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.backgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height))) + transition.updateFrame(node: self.highlightedBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height))) + + if let broadcastPickerView = self.broadcastPickerView { + broadcastPickerView.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height)) + } + }) + } + + func updateTheme(presentationData: PresentationData) { + self.backgroundNode.backgroundColor = presentationData.theme.contextMenu.itemBackgroundColor + self.highlightedBackgroundNode.backgroundColor = presentationData.theme.contextMenu.itemHighlightedBackgroundColor + + let textFont = Font.regular(presentationData.listsFontSize.baseDisplaySize) + let subtextFont = Font.regular(presentationData.listsFontSize.baseDisplaySize * 13.0 / 17.0) + + self.textNode.attributedText = NSAttributedString(string: self.textNode.attributedText?.string ?? "", font: textFont, textColor: presentationData.theme.contextMenu.primaryColor) + } + + @objc private func buttonPressed() { + self.performAction() + } + + func performAction() { + guard let controller = self.getController() else { + return + } + self.item.action(controller, { [weak self] result in + self?.actionSelected(result) + }) + } + + func setIsHighlighted(_ value: Bool) { + if value { + self.highlightedBackgroundNode.alpha = 1.0 + } else { + self.highlightedBackgroundNode.alpha = 0.0 + } + } +} diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatTileGridNode.swift b/submodules/TelegramCallsUI/Sources/VoiceChatTileGridNode.swift new file mode 100644 index 0000000000..dca6322e0d --- /dev/null +++ b/submodules/TelegramCallsUI/Sources/VoiceChatTileGridNode.swift @@ -0,0 +1,335 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display +import SwiftSignalKit +import AccountContext + +private let tileSpacing: CGFloat = 4.0 +let tileHeight: CGFloat = 180.0 + +enum VoiceChatTileLayoutMode { + case pairs + case rows + case grid +} + +final class VoiceChatTileGridNode: ASDisplayNode { + private let context: AccountContext + + private var items: [VoiceChatTileItem] = [] + fileprivate var itemNodes: [String: VoiceChatTileItemNode] = [:] + private var isFirstTime = true + + private var absoluteLocation: (CGRect, CGSize)? + + var tileNodes: [VoiceChatTileItemNode] { + return Array(self.itemNodes.values) + } + + init(context: AccountContext) { + self.context = context + + super.init() + + self.clipsToBounds = true + } + + var visibility = true { + didSet { + for (_, tileNode) in self.itemNodes { + tileNode.visibility = self.visibility + } + } + } + + func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) { + self.absoluteLocation = (rect, containerSize) + for itemNode in self.itemNodes.values { + var localRect = rect + localRect.origin = localRect.origin.offsetBy(dx: itemNode.frame.minX, dy: itemNode.frame.minY) + localRect.size = itemNode.frame.size + itemNode.updateAbsoluteRect(localRect, within: containerSize) + } + } + + func update(size: CGSize, layoutMode: VoiceChatTileLayoutMode, items: [VoiceChatTileItem], transition: ContainedViewLayoutTransition, completion: @escaping () -> Void = {}) -> CGSize { + let wasEmpty = self.items.isEmpty + self.items = items + + var validIds: [String] = [] + + let colsCount: CGFloat + if case .grid = layoutMode { + if items.count < 3 { + colsCount = 1 + } else if items.count < 5 { + colsCount = 2 + } else { + colsCount = 3 + } + } else { + colsCount = 2 + } + let rowsCount = ceil(CGFloat(items.count) / colsCount) + + let genericItemWidth = floorToScreenPixels((size.width - tileSpacing * (colsCount - 1)) / colsCount) + let lastRowItemsAreWide: Bool + let lastRowItemWidth: CGFloat + if case .grid = layoutMode { + lastRowItemsAreWide = [1, 2].contains(items.count) || items.count % Int(colsCount) != 0 + var lastRowItemsCount = CGFloat(items.count % Int(colsCount)) + if lastRowItemsCount.isZero { + lastRowItemsCount = colsCount + } + lastRowItemWidth = floorToScreenPixels((size.width - tileSpacing * (lastRowItemsCount - 1)) / lastRowItemsCount) + } else { + lastRowItemsAreWide = items.count == 1 || items.count % Int(colsCount) != 0 + lastRowItemWidth = size.width + } + + let isFirstTime = self.isFirstTime + if isFirstTime { + self.isFirstTime = false + } + + var availableWidth = min(size.width, size.height) + var itemHeight = tileHeight + if case .grid = layoutMode { + itemHeight = size.height / rowsCount - (tileSpacing * (rowsCount - 1)) + } + + for i in 0 ..< self.items.count { + let item = self.items[i] + let col = CGFloat(i % Int(colsCount)) + let row = floor(CGFloat(i) / colsCount) + let isLastRow = row == (rowsCount - 1) + + let rowItemWidth = isLastRow && lastRowItemsAreWide ? lastRowItemWidth : genericItemWidth + let itemSize = CGSize( + width: rowItemWidth, + height: itemHeight + ) + + if case .grid = layoutMode { + availableWidth = rowItemWidth + } + + let itemFrame = CGRect(origin: CGPoint(x: col * (rowItemWidth + tileSpacing), y: row * (itemHeight + tileSpacing)), size: itemSize) + + validIds.append(item.id) + var itemNode: VoiceChatTileItemNode? + var wasAdded = false + if let current = self.itemNodes[item.id] { + itemNode = current + current.update(size: itemSize, availableWidth: availableWidth, item: item, transition: transition) + } else { + wasAdded = true + let addedItemNode = VoiceChatTileItemNode(context: self.context) + itemNode = addedItemNode + addedItemNode.update(size: itemSize, availableWidth: availableWidth, item: item, transition: .immediate) + self.itemNodes[self.items[i].id] = addedItemNode + self.addSubnode(addedItemNode) + } + if let itemNode = itemNode { + itemNode.visibility = self.visibility + let itemTransition: ContainedViewLayoutTransition = wasAdded ? .immediate : transition + itemTransition.updateFrameAsPositionAndBounds(node: itemNode, frame: itemFrame) + if wasAdded && !isFirstTime { + itemNode.layer.animateScale(from: 0.0, to: 1.0, duration: wasEmpty ? 0.4 : 0.3) + itemNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } + + if let (rect, containerSize) = self.absoluteLocation { + var localRect = rect + localRect.origin = localRect.origin.offsetBy(dx: itemFrame.minX, dy: itemFrame.minY) + localRect.size = itemFrame.size + itemNode.updateAbsoluteRect(localRect, within: containerSize) + } + } + } + + var removeIds: [String] = [] + 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.layer.animateScale(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, additive: true) + itemNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak itemNode] _ in + itemNode?.removeFromSupernode() + }) + } + } + + if case let .animated(duration, _) = transition { + Queue.mainQueue().after(duration) { + completion() + } + } else { + completion() + } + + let rowCount = ceil(CGFloat(self.items.count) / 2.0) + return CGSize(width: size.width, height: rowCount * (itemHeight + tileSpacing)) + } +} + +final class VoiceChatTilesGridItem: ListViewItem { + let context: AccountContext + let tiles: [VoiceChatTileItem] + let layoutMode: VoiceChatTileLayoutMode + let getIsExpanded: () -> Bool + + init(context: AccountContext, tiles: [VoiceChatTileItem], layoutMode: VoiceChatTileLayoutMode, getIsExpanded: @escaping () -> Bool) { + self.context = context + self.tiles = tiles + self.layoutMode = layoutMode + self.getIsExpanded = getIsExpanded + } + + 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 = VoiceChatTilesGridItemNode() + let (layout, apply) = node.asyncLayout()(self, params) + + 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? VoiceChatTilesGridItemNode { + let makeLayout = nodeValue.asyncLayout() + + async { + let (layout, apply) = makeLayout(self, params) + Queue.mainQueue().async { + completion(layout, { _ in + apply() + }) + } + } + } + } + } +} + +final class VoiceChatTilesGridItemNode: ListViewItemNode { + private var item: VoiceChatTilesGridItem? + + private var tileGridNode: VoiceChatTileGridNode? + let backgroundNode: ASDisplayNode + let cornersNode: ASImageNode + + private var absoluteLocation: (CGRect, CGSize)? + + var tileNodes: [VoiceChatTileItemNode] { + if let values = self.tileGridNode?.itemNodes.values { + return Array(values) + } else { + return [] + } + } + + init() { + self.backgroundNode = ASDisplayNode() + + self.cornersNode = ASImageNode() + self.cornersNode.displaysAsynchronously = false + self.cornersNode.image = decorationCornersImage(top: true, bottom: false, dark: false) + + super.init(layerBacked: false, dynamicBounce: false) + + self.addSubnode(self.backgroundNode) + self.addSubnode(self.cornersNode) + } + + override func animateFrameTransition(_ progress: CGFloat, _ currentValue: CGFloat) { + super.animateFrameTransition(progress, currentValue) + + if let tileGridNode = self.tileGridNode { + var gridFrame = tileGridNode.frame + gridFrame.size.height = currentValue + tileGridNode.frame = gridFrame + } + + var backgroundFrame = self.backgroundNode.frame + backgroundFrame.size.height = currentValue + self.backgroundNode.frame = backgroundFrame + + var cornersFrame = self.cornersNode.frame + cornersFrame.origin.y = currentValue + self.cornersNode.frame = cornersFrame + } + + func asyncLayout() -> (_ item: VoiceChatTilesGridItem, _ params: ListViewItemLayoutParams) -> (ListViewItemNodeLayout, () -> Void) { + let currentItem = self.item + return { item, params in + let rowCount = ceil(CGFloat(item.tiles.count) / 2.0) + let contentSize = CGSize(width: params.width, height: rowCount * (tileHeight + tileSpacing)) + let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: UIEdgeInsets()) + return (layout, { [weak self] in + if let strongSelf = self { + strongSelf.item = item + + let tileGridNode: VoiceChatTileGridNode + if let current = strongSelf.tileGridNode { + tileGridNode = current + } else { + strongSelf.backgroundNode.backgroundColor = item.getIsExpanded() ? fullscreenBackgroundColor : panelBackgroundColor + strongSelf.cornersNode.image = decorationCornersImage(top: true, bottom: false, dark: item.getIsExpanded()) + + tileGridNode = VoiceChatTileGridNode(context: item.context) + tileGridNode.visibility = strongSelf.gridVisibility + strongSelf.addSubnode(tileGridNode) + strongSelf.tileGridNode = tileGridNode + } + + + if let (rect, size) = strongSelf.absoluteLocation { + tileGridNode.updateAbsoluteRect(rect, within: size) + } + + let transition: ContainedViewLayoutTransition = currentItem == nil ? .immediate : .animated(duration: 0.3, curve: .easeInOut) + let tileGridSize = tileGridNode.update(size: CGSize(width: params.width - params.leftInset - params.rightInset, height: params.availableHeight), layoutMode: item.layoutMode, items: item.tiles, transition: transition) + if currentItem == nil { + tileGridNode.frame = CGRect(x: params.leftInset, y: 0.0, width: tileGridSize.width, height: tileGridSize.height) + strongSelf.backgroundNode.frame = tileGridNode.frame + strongSelf.cornersNode.frame = CGRect(x: params.leftInset, y: layout.size.height, width: tileGridSize.width, height: 50.0) + } else { + transition.updateFrame(node: tileGridNode, frame: CGRect(origin: CGPoint(x: params.leftInset, y: 0.0), size: tileGridSize)) + transition.updateFrame(node: strongSelf.backgroundNode, frame: CGRect(origin: CGPoint(x: params.leftInset, y: 0.0), size: tileGridSize)) + strongSelf.cornersNode.frame = CGRect(x: params.leftInset, y: layout.size.height, width: tileGridSize.width, height: 50.0) + } + } + }) + } + } + + override public func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) { + self.absoluteLocation = (rect, containerSize) + self.tileGridNode?.updateAbsoluteRect(rect, within: containerSize) + } + + var gridVisibility: Bool = true { + didSet { + self.tileGridNode?.visibility = self.gridVisibility + } + } + + func snapshotForDismissal() { + if let snapshotView = self.tileGridNode?.view.snapshotView(afterScreenUpdates: false) { + self.tileGridNode?.view.addSubview(snapshotView) + } + } +} diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatTileItemNode.swift b/submodules/TelegramCallsUI/Sources/VoiceChatTileItemNode.swift new file mode 100644 index 0000000000..f51e3c71ce --- /dev/null +++ b/submodules/TelegramCallsUI/Sources/VoiceChatTileItemNode.swift @@ -0,0 +1,986 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display +import SwiftSignalKit +import Postbox +import SyncCore +import TelegramCore +import AccountContext +import TelegramUIPreferences +import TelegramPresentationData +import AvatarNode + +private let backgroundCornerRadius: CGFloat = 11.0 +private let borderLineWidth: CGFloat = 2.0 + +private let destructiveColor: UIColor = UIColor(rgb: 0xff3b30) + +final class VoiceChatTileItem: Equatable { + enum Icon: Equatable { + case none + case microphone(Bool) + case presentation + } + + let account: Account + let peer: Peer + let videoEndpointId: String + let videoReady: Bool + let videoTimeouted: Bool + let isVideoLimit: Bool + let videoLimit: Int32 + let isPaused: Bool + let isOwnScreencast: Bool + let strings: PresentationStrings + let nameDisplayOrder: PresentationPersonNameOrder + let icon: Icon + let text: VoiceChatParticipantItem.ParticipantText + let additionalText: VoiceChatParticipantItem.ParticipantText? + let speaking: Bool + let secondary: Bool + let isTablet: Bool + let action: () -> Void + let contextAction: ((ASDisplayNode, ContextGesture?) -> Void)? + let getVideo: (GroupVideoNode.Position) -> GroupVideoNode? + let getAudioLevel: (() -> Signal)? + + var id: String { + return self.videoEndpointId + } + + init(account: Account, peer: Peer, videoEndpointId: String, videoReady: Bool, videoTimeouted: Bool, isVideoLimit: Bool, videoLimit: Int32, isPaused: Bool, isOwnScreencast: Bool, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, speaking: Bool, secondary: Bool, isTablet: Bool, icon: Icon, text: VoiceChatParticipantItem.ParticipantText, additionalText: VoiceChatParticipantItem.ParticipantText?, action: @escaping () -> Void, contextAction: ((ASDisplayNode, ContextGesture?) -> Void)?, getVideo: @escaping (GroupVideoNode.Position) -> GroupVideoNode?, getAudioLevel: (() -> Signal)?) { + self.account = account + self.peer = peer + self.videoEndpointId = videoEndpointId + self.videoReady = videoReady + self.videoTimeouted = videoTimeouted + self.isVideoLimit = isVideoLimit + self.videoLimit = videoLimit + self.isPaused = isPaused + self.isOwnScreencast = isOwnScreencast + self.strings = strings + self.nameDisplayOrder = nameDisplayOrder + self.icon = icon + self.text = text + self.additionalText = additionalText + self.speaking = speaking + self.secondary = secondary + self.isTablet = isTablet + self.action = action + self.contextAction = contextAction + self.getVideo = getVideo + self.getAudioLevel = getAudioLevel + } + + static func == (lhs: VoiceChatTileItem, rhs: VoiceChatTileItem) -> Bool { + if !arePeersEqual(lhs.peer, rhs.peer) { + return false + } + if lhs.videoEndpointId != rhs.videoEndpointId { + return false + } + if lhs.videoReady != rhs.videoReady { + return false + } + if lhs.videoTimeouted != rhs.videoTimeouted { + return false + } + if lhs.isPaused != rhs.isPaused { + return false + } + if lhs.isOwnScreencast != rhs.isOwnScreencast { + return false + } + if lhs.icon != rhs.icon { + return false + } + if lhs.text != rhs.text { + return false + } + if lhs.additionalText != rhs.additionalText { + return false + } + if lhs.speaking != rhs.speaking { + return false + } + if lhs.secondary != rhs.secondary { + return false + } + if lhs.icon != rhs.icon { + return false + } + return true + } +} + +private let fadeColor = UIColor(rgb: 0x000000, alpha: 0.5) + +var tileFadeImage: UIImage? = { + return generateImage(CGSize(width: fadeHeight, height: fadeHeight), rotatedContext: { size, context in + let bounds = CGRect(origin: CGPoint(), size: size) + context.clear(bounds) + + let colorsArray = [fadeColor.withAlphaComponent(0.0).cgColor, fadeColor.cgColor] as CFArray + var locations: [CGFloat] = [1.0, 0.0] + let gradient = CGGradient(colorsSpace: deviceColorSpace, colors: colorsArray, locations: &locations)! + context.drawLinearGradient(gradient, start: CGPoint(), end: CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions()) + }) +}() + +final class VoiceChatTileItemNode: ASDisplayNode { + private let context: AccountContext + + let contextSourceNode: ContextExtractedContentContainingNode + private let containerNode: ContextControllerSourceNode + let contentNode: ASDisplayNode + let backgroundNode: ASDisplayNode + var videoContainerNode: ASDisplayNode + var videoNode: GroupVideoNode? + let infoNode: ASDisplayNode + let fadeNode: ASDisplayNode + private var shimmerNode: VoiceChatTileShimmeringNode? + private let titleNode: ImmediateTextNode + private var iconNode: ASImageNode? + private var animationNode: VoiceChatMicrophoneNode? + var highlightNode: VoiceChatTileHighlightNode + private let statusNode: VoiceChatParticipantStatusNode + + let placeholderTextNode: ImmediateTextNode + let placeholderIconNode: ASImageNode + + private var profileNode: VoiceChatPeerProfileNode? + private var extractedRect: CGRect? + private var nonExtractedRect: CGRect? + + private var validLayout: (CGSize, CGFloat)? + var item: VoiceChatTileItem? + private var isExtracted = false + + private let audioLevelDisposable = MetaDisposable() + + init(context: AccountContext) { + self.context = context + + self.contextSourceNode = ContextExtractedContentContainingNode() + self.containerNode = ContextControllerSourceNode() + + self.contentNode = ASDisplayNode() + self.contentNode.clipsToBounds = true + self.contentNode.cornerRadius = backgroundCornerRadius + + self.backgroundNode = ASDisplayNode() + self.backgroundNode.backgroundColor = panelBackgroundColor + + self.videoContainerNode = ASDisplayNode() + self.videoContainerNode.clipsToBounds = true + + self.infoNode = ASDisplayNode() + + self.fadeNode = ASDisplayNode() + self.fadeNode.displaysAsynchronously = false + if let image = tileFadeImage { + self.fadeNode.backgroundColor = UIColor(patternImage: image) + } + + self.titleNode = ImmediateTextNode() + self.titleNode.displaysAsynchronously = false + + self.statusNode = VoiceChatParticipantStatusNode() + + self.highlightNode = VoiceChatTileHighlightNode() + self.highlightNode.alpha = 0.0 + self.highlightNode.updateGlowAndGradientAnimations(type: .speaking) + + self.placeholderTextNode = ImmediateTextNode() + self.placeholderTextNode.alpha = 0.0 + self.placeholderTextNode.maximumNumberOfLines = 2 + self.placeholderTextNode.textAlignment = .center + + self.placeholderIconNode = ASImageNode() + self.placeholderIconNode.alpha = 0.0 + self.placeholderIconNode.contentMode = .scaleAspectFit + self.placeholderIconNode.displaysAsynchronously = false + + super.init() + + self.containerNode.addSubnode(self.contextSourceNode) + self.containerNode.targetNodeForActivationProgress = self.contextSourceNode.contentNode + self.addSubnode(self.containerNode) + + self.contextSourceNode.contentNode.addSubnode(self.contentNode) + self.contentNode.addSubnode(self.backgroundNode) + self.contentNode.addSubnode(self.videoContainerNode) + self.contentNode.addSubnode(self.fadeNode) + self.contentNode.addSubnode(self.infoNode) + self.infoNode.addSubnode(self.titleNode) + self.contentNode.addSubnode(self.placeholderTextNode) + self.contentNode.addSubnode(self.placeholderIconNode) + self.contentNode.addSubnode(self.highlightNode) + + self.containerNode.shouldBegin = { [weak self] location in + guard let strongSelf = self, let item = strongSelf.item, item.videoReady && !item.isVideoLimit else { + return false + } + return true + } + self.containerNode.activated = { [weak self] gesture, _ in + guard let strongSelf = self, let item = strongSelf.item, let contextAction = item.contextAction, !item.isVideoLimit else { + gesture.cancel() + return + } + contextAction(strongSelf.contextSourceNode, gesture) + } + self.contextSourceNode.willUpdateIsExtractedToContextPreview = { [weak self] isExtracted, transition in + guard let strongSelf = self, let _ = strongSelf.item else { + return + } + strongSelf.updateIsExtracted(isExtracted, transition: transition) + } + } + + deinit { + self.audioLevelDisposable.dispose() + } + + override func didLoad() { + super.didLoad() + + if #available(iOS 13.0, *) { + self.contentNode.layer.cornerCurve = .continuous + } + + self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tap))) + } + + @objc private func tap() { + if let item = self.item { + item.action() + } + } + + private func updateIsExtracted(_ isExtracted: Bool, transition: ContainedViewLayoutTransition) { + guard self.isExtracted != isExtracted, let extractedRect = self.extractedRect, let nonExtractedRect = self.nonExtractedRect, let item = self.item else { + return + } + self.isExtracted = isExtracted + + let springDuration: Double = 0.42 + let springDamping: CGFloat = 124.0 + if isExtracted { + let profileNode = VoiceChatPeerProfileNode(context: self.context, size: extractedRect.size, sourceSize: nonExtractedRect.size, peer: item.peer, text: item.text, customNode: self.videoContainerNode, additionalEntry: .single(nil), requestDismiss: { [weak self] in + self?.contextSourceNode.requestDismiss?() + }) + profileNode.frame = CGRect(origin: CGPoint(), size: self.bounds.size) + self.profileNode = profileNode + self.contextSourceNode.contentNode.addSubnode(profileNode) + + profileNode.animateIn(from: self, targetRect: extractedRect, transition: transition) + var appearenceTransition = transition + if transition.isAnimated { + appearenceTransition = .animated(duration: springDuration, curve: .customSpring(damping: springDamping, initialVelocity: 0.0)) + } + appearenceTransition.updateFrame(node: profileNode, frame: extractedRect) + + self.contextSourceNode.contentNode.customHitTest = { [weak self] point in + if let strongSelf = self, let profileNode = strongSelf.profileNode { + if profileNode.avatarListWrapperNode.frame.contains(point) { + return profileNode.avatarListNode.view + } + } + return nil + } + + self.backgroundNode.isHidden = true + self.fadeNode.isHidden = true + self.infoNode.isHidden = true + self.highlightNode.isHidden = true + } else if let profileNode = self.profileNode { + self.profileNode = nil + + self.infoNode.isHidden = false + profileNode.animateOut(to: self, targetRect: nonExtractedRect, transition: transition, completion: { [weak self] in + if let strongSelf = self { + strongSelf.backgroundNode.isHidden = false + strongSelf.fadeNode.isHidden = false + strongSelf.highlightNode.isHidden = false + } + }) + + var appearenceTransition = transition + if transition.isAnimated { + appearenceTransition = .animated(duration: 0.2, curve: .easeInOut) + } + appearenceTransition.updateFrame(node: profileNode, frame: nonExtractedRect) + + self.contextSourceNode.contentNode.customHitTest = nil + } + } + + private var absoluteLocation: (CGRect, CGSize)? + func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) { + self.absoluteLocation = (rect, containerSize) + if let shimmerNode = self.shimmerNode { + shimmerNode.updateAbsoluteRect(rect, within: containerSize) + } + self.updateIsEnabled() + } + + var visibility = true { + didSet { + self.updateIsEnabled() + } + } + + func updateIsEnabled() { + guard let (rect, containerSize) = self.absoluteLocation else { + return + } + let isVisibleInContainer = rect.maxY >= 0.0 && rect.minY <= containerSize.height + if let videoNode = self.videoNode, videoNode.supernode === self.videoContainerNode { + videoNode.updateIsEnabled(self.visibility && isVisibleInContainer) + } + } + + func update(size: CGSize, availableWidth: CGFloat, item: VoiceChatTileItem, transition: ContainedViewLayoutTransition) { + guard self.validLayout?.0 != size || self.validLayout?.1 != availableWidth || self.item != item else { + return + } + + self.validLayout = (size, availableWidth) + + if !item.videoReady || item.isOwnScreencast { + let shimmerNode: VoiceChatTileShimmeringNode + let shimmerTransition: ContainedViewLayoutTransition + if let current = self.shimmerNode { + shimmerNode = current + shimmerTransition = transition + } else { + shimmerNode = VoiceChatTileShimmeringNode(account: item.account, peer: item.peer) + self.contentNode.insertSubnode(shimmerNode, aboveSubnode: self.fadeNode) + self.shimmerNode = shimmerNode + + if let (rect, containerSize) = self.absoluteLocation { + shimmerNode.updateAbsoluteRect(rect, within: containerSize) + } + shimmerTransition = .immediate + } + shimmerTransition.updateFrame(node: shimmerNode, frame: CGRect(origin: CGPoint(), size: size)) + shimmerNode.update(shimmeringColor: UIColor.white, shimmering: !item.isOwnScreencast && !item.videoTimeouted && !item.isPaused, size: size, transition: shimmerTransition) + } else if let shimmerNode = self.shimmerNode { + self.shimmerNode = nil + shimmerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak shimmerNode] _ in + shimmerNode?.removeFromSupernode() + }) + } + + var nodeToAnimateIn: ASDisplayNode? + var placeholderAppeared = false + + var itemTransition = transition + if self.item != item { + let previousItem = self.item + self.item = item + + if let getAudioLevel = item.getAudioLevel { + self.audioLevelDisposable.set((getAudioLevel() + |> deliverOnMainQueue).start(next: { [weak self] value in + guard let strongSelf = self else { + return + } + strongSelf.highlightNode.updateLevel(CGFloat(value)) + })) + } + + let transition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .easeInOut) + transition.updateAlpha(node: self.highlightNode, alpha: item.speaking ? 1.0 : 0.0) + + if previousItem?.videoEndpointId != item.videoEndpointId || self.videoNode == nil { + if let current = self.videoNode { + self.videoNode = nil + current.removeFromSupernode() + } + + if let videoNode = item.getVideo(item.secondary ? .list : .tile) { + itemTransition = .immediate + self.videoNode = videoNode + self.videoContainerNode.addSubnode(videoNode) + self.updateIsEnabled() + } + } + + self.videoNode?.updateIsBlurred(isBlurred: item.isPaused, light: true) + + var showPlaceholder = false + if item.isVideoLimit { + self.placeholderTextNode.attributedText = NSAttributedString(string: item.strings.VoiceChat_VideoParticipantsLimitExceeded(String(item.videoLimit)).0, font: Font.semibold(13.0), textColor: .white) + self.placeholderIconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Call/VideoUnavailable"), color: .white) + showPlaceholder = true + } else if item.isOwnScreencast { + self.placeholderTextNode.attributedText = NSAttributedString(string: item.strings.VoiceChat_YouAreSharingScreen, font: Font.semibold(13.0), textColor: .white) + self.placeholderIconNode.image = generateTintedImage(image: UIImage(bundleImageName: item.isTablet ? "Call/ScreenShareTablet" : "Call/ScreenSharePhone"), color: .white) + showPlaceholder = true + } else if item.isPaused { + self.placeholderTextNode.attributedText = NSAttributedString(string: item.strings.VoiceChat_VideoPaused, font: Font.semibold(13.0), textColor: .white) + self.placeholderIconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Call/Pause"), color: .white) + showPlaceholder = true + } + + placeholderAppeared = self.placeholderTextNode.alpha.isZero && showPlaceholder + transition.updateAlpha(node: self.placeholderTextNode, alpha: showPlaceholder ? 1.0 : 0.0) + transition.updateAlpha(node: self.placeholderIconNode, alpha: showPlaceholder ? 1.0 : 0.0) + + let titleFont = Font.semibold(13.0) + let titleColor = UIColor.white + var titleAttributedString: NSAttributedString? + if item.isVideoLimit { + titleAttributedString = nil + } 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: titleFont, textColor: titleColor)) + case .lastFirst: + string.append(NSAttributedString(string: lastName, font: titleFont, 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 { + titleAttributedString = NSAttributedString(string: firstName, font: titleFont, textColor: titleColor) + } 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) + } + } else if let group = item.peer as? TelegramGroup { + titleAttributedString = NSAttributedString(string: group.title, font: titleFont, textColor: titleColor) + } else if let channel = item.peer as? TelegramChannel { + titleAttributedString = NSAttributedString(string: channel.title, font: titleFont, textColor: titleColor) + } + + var microphoneColor = UIColor.white + if let additionalText = item.additionalText, case let .text(_, _, color) = additionalText { + if case .destructive = color { + microphoneColor = destructiveColor + } + } + self.titleNode.attributedText = titleAttributedString + + var hadMicrophoneNode = false + var hadIconNode = false + + if case let .microphone(muted) = item.icon { + let animationNode: VoiceChatMicrophoneNode + if let current = self.animationNode { + animationNode = current + } else { + animationNode = VoiceChatMicrophoneNode() + self.animationNode = animationNode + self.infoNode.addSubnode(animationNode) + + nodeToAnimateIn = animationNode + } + animationNode.alpha = 1.0 + animationNode.update(state: VoiceChatMicrophoneNode.State(muted: muted, filled: true, color: microphoneColor), animated: true) + } else if let animationNode = self.animationNode { + hadMicrophoneNode = true + self.animationNode = nil + animationNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) + animationNode.layer.animateScale(from: 1.0, to: 0.001, duration: 0.2, removeOnCompletion: false, completion: { [weak animationNode] _ in + animationNode?.removeFromSupernode() + }) + } + + if case .presentation = item.icon { + let iconNode: ASImageNode + if let current = self.iconNode { + iconNode = current + } else { + iconNode = ASImageNode() + iconNode.displaysAsynchronously = false + iconNode.contentMode = .center + self.iconNode = iconNode + self.infoNode.addSubnode(iconNode) + + nodeToAnimateIn = iconNode + } + + iconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Call/StatusScreen"), color: .white) + } else if let iconNode = self.iconNode { + hadIconNode = true + self.iconNode = nil + iconNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) + iconNode.layer.animateScale(from: 1.0, to: 0.001, duration: 0.2, removeOnCompletion: false, completion: { [weak iconNode] _ in + iconNode?.removeFromSupernode() + }) + } + + if let node = nodeToAnimateIn, hadMicrophoneNode || hadIconNode { + node.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + node.layer.animateScale(from: 0.001, to: 1.0, duration: 0.2) + } + } + + let bounds = CGRect(origin: CGPoint(), size: size) + self.containerNode.frame = bounds + self.contextSourceNode.frame = bounds + self.contextSourceNode.contentNode.frame = bounds + + transition.updateFrame(node: self.contentNode, frame: bounds) + + let extractedWidth = availableWidth + let makeStatusLayout = self.statusNode.asyncLayout() + let (statusLayout, _) = makeStatusLayout(CGSize(width: availableWidth - 30.0, height: CGFloat.greatestFiniteMagnitude), item.text, true) + + let extractedRect = CGRect(x: 0.0, y: 0.0, width: extractedWidth, height: extractedWidth + statusLayout.height + 39.0) + let nonExtractedRect = bounds + self.extractedRect = extractedRect + self.nonExtractedRect = nonExtractedRect + + self.contextSourceNode.contentRect = extractedRect + + if self.videoContainerNode.supernode === self.contentNode { + if let videoNode = self.videoNode { + itemTransition.updateFrame(node: videoNode, frame: bounds) + if videoNode.supernode === self.videoContainerNode { + videoNode.updateLayout(size: size, layoutMode: .fillOrFitToSquare, transition: itemTransition) + } + } + transition.updateFrame(node: self.videoContainerNode, frame: bounds) + } + + transition.updateFrame(node: self.backgroundNode, frame: bounds) + transition.updateFrame(node: self.highlightNode, frame: bounds) + self.highlightNode.updateLayout(size: bounds.size, transition: transition) + transition.updateFrame(node: self.infoNode, frame: bounds) + transition.updateFrame(node: self.fadeNode, frame: CGRect(x: 0.0, y: size.height - fadeHeight, width: size.width, height: fadeHeight)) + + let titleSize = self.titleNode.updateLayout(CGSize(width: size.width - 50.0, height: size.height)) + self.titleNode.frame = CGRect(origin: CGPoint(x: 30.0, y: size.height - titleSize.height - 8.0), size: titleSize) + + var transition = transition + if nodeToAnimateIn != nil || placeholderAppeared { + transition = .immediate + } + + if let iconNode = self.iconNode, let image = iconNode.image { + transition.updateFrame(node: iconNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels(16.0 - image.size.width / 2.0), y: floorToScreenPixels(size.height - 15.0 - image.size.height / 2.0)), size: image.size)) + } + + if let animationNode = self.animationNode { + let animationSize = CGSize(width: 36.0, height: 36.0) + animationNode.bounds = CGRect(origin: CGPoint(), size: animationSize) + animationNode.transform = CATransform3DMakeScale(0.66667, 0.66667, 1.0) + transition.updatePosition(node: animationNode, position: CGPoint(x: 16.0, y: size.height - 15.0)) + } + + let placeholderTextSize = self.placeholderTextNode.updateLayout(CGSize(width: size.width - 30.0, height: 100.0)) + transition.updateFrame(node: self.placeholderTextNode, frame: CGRect(origin: CGPoint(x: floor((size.width - placeholderTextSize.width) / 2.0), y: floorToScreenPixels(size.height / 2.0) + 10.0), size: placeholderTextSize)) + if let image = self.placeholderIconNode.image { + let imageScale: CGFloat = item.isVideoLimit ? 1.0 : 0.5 + let imageSize = CGSize(width: image.size.width * imageScale, height: image.size.height * imageScale) + transition.updateFrame(node: self.placeholderIconNode, frame: CGRect(origin: CGPoint(x: floor((size.width - imageSize.width) / 2.0), y: floorToScreenPixels(size.height / 2.0) - imageSize.height - 4.0), size: imageSize)) + } + } + + func transitionIn(from sourceNode: ASDisplayNode?) { + guard let item = self.item else { + return + } + var videoNode: GroupVideoNode? + if let sourceNode = sourceNode as? VoiceChatFullscreenParticipantItemNode, let _ = sourceNode.item { + if let sourceVideoNode = sourceNode.videoNode { + sourceNode.videoNode = nil + videoNode = sourceVideoNode + } + } + + if videoNode == nil { + videoNode = item.getVideo(item.secondary ? .list : .tile) + } + + if let videoNode = videoNode { + videoNode.alpha = 1.0 + self.videoNode = videoNode + self.videoContainerNode.addSubnode(videoNode) + + videoNode.updateLayout(size: self.bounds.size, layoutMode: .fillOrFitToSquare, transition: .immediate) + videoNode.frame = self.bounds + + self.updateIsEnabled() + } + } +} + +private let blue = UIColor(rgb: 0x007fff) +private let lightBlue = UIColor(rgb: 0x00affe) +private let green = UIColor(rgb: 0x33c659) +private let activeBlue = UIColor(rgb: 0x00a0b9) +private let purple = UIColor(rgb: 0x3252ef) +private let pink = UIColor(rgb: 0xef436c) + +class VoiceChatTileHighlightNode: ASDisplayNode { + enum Gradient { + case speaking + case active + case mutedForYou + case muted + } + + private var maskView: UIView? + private let maskLayer = CALayer() + + private let foregroundGradientLayer = CAGradientLayer() + + private let hierarchyTrackingNode: HierarchyTrackingNode + private var isCurrentlyInHierarchy = false + + private var audioLevel: CGFloat = 0.0 + private var presentationAudioLevel: CGFloat = 0.0 + + private var displayLinkAnimator: ConstantDisplayLinkAnimator? + + override init() { + self.foregroundGradientLayer.type = .radial + self.foregroundGradientLayer.colors = [lightBlue.cgColor, blue.cgColor, blue.cgColor] + self.foregroundGradientLayer.locations = [0.0, 0.85, 1.0] + self.foregroundGradientLayer.startPoint = CGPoint(x: 1.0, y: 0.0) + self.foregroundGradientLayer.endPoint = CGPoint(x: 0.0, y: 1.0) + + var updateInHierarchy: ((Bool) -> Void)? + self.hierarchyTrackingNode = HierarchyTrackingNode({ value in + updateInHierarchy?(value) + }) + + super.init() + + updateInHierarchy = { [weak self] value in + if let strongSelf = self { + strongSelf.isCurrentlyInHierarchy = value + strongSelf.updateAnimations() + } + } + + self.displayLinkAnimator = ConstantDisplayLinkAnimator() { [weak self] in + guard let strongSelf = self else { return } + + strongSelf.presentationAudioLevel = strongSelf.presentationAudioLevel * 0.9 + strongSelf.audioLevel * 0.1 + } + + self.addSubnode(self.hierarchyTrackingNode) + } + + override func didLoad() { + super.didLoad() + + self.layer.addSublayer(self.foregroundGradientLayer) + + let maskView = UIView() + maskView.layer.addSublayer(self.maskLayer) + self.maskView = maskView + + self.maskLayer.masksToBounds = true + self.maskLayer.cornerRadius = backgroundCornerRadius - UIScreenPixel + self.maskLayer.borderColor = UIColor.white.cgColor + self.maskLayer.borderWidth = borderLineWidth + + self.view.mask = self.maskView + } + + func updateAnimations() { + if !self.isCurrentlyInHierarchy { + self.foregroundGradientLayer.removeAllAnimations() + return + } + self.setupGradientAnimations() + } + + func updateLevel(_ level: CGFloat) { + self.audioLevel = level + } + + func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) { + let bounds = CGRect(origin: CGPoint(), size: size) + if let maskView = self.maskView { + transition.updateFrame(view: maskView, frame: bounds) + } + transition.updateFrame(layer: self.maskLayer, frame: bounds) + transition.updateFrame(layer: self.foregroundGradientLayer, frame: bounds) + } + + private func setupGradientAnimations() { + if let _ = self.foregroundGradientLayer.animation(forKey: "movement") { + } else { + let previousValue = self.foregroundGradientLayer.startPoint + let newValue: CGPoint + if self.presentationAudioLevel > 0.22 { + newValue = CGPoint(x: CGFloat.random(in: 0.9 ..< 1.0), y: CGFloat.random(in: 0.15 ..< 0.35)) + } else if self.presentationAudioLevel > 0.01 { + newValue = CGPoint(x: CGFloat.random(in: 0.57 ..< 0.85), y: CGFloat.random(in: 0.15 ..< 0.45)) + } else { + newValue = CGPoint(x: CGFloat.random(in: 0.6 ..< 0.75), y: CGFloat.random(in: 0.25 ..< 0.45)) + } + self.foregroundGradientLayer.startPoint = newValue + + CATransaction.begin() + + let animation = CABasicAnimation(keyPath: "startPoint") + animation.duration = Double.random(in: 0.8 ..< 1.4) + animation.fromValue = previousValue + animation.toValue = newValue + + CATransaction.setCompletionBlock { [weak self] in + if let isCurrentlyInHierarchy = self?.isCurrentlyInHierarchy, isCurrentlyInHierarchy { + self?.setupGradientAnimations() + } + } + + self.foregroundGradientLayer.add(animation, forKey: "movement") + CATransaction.commit() + } + } + + private var gradient: Gradient? + func updateGlowAndGradientAnimations(type: Gradient, animated: Bool = true) { + guard self.gradient != type else { + return + } + self.gradient = type + let initialColors = self.foregroundGradientLayer.colors + let targetColors: [CGColor] + switch type { + case .speaking: + targetColors = [activeBlue.cgColor, green.cgColor, green.cgColor] + case .active: + targetColors = [lightBlue.cgColor, blue.cgColor, blue.cgColor] + case .mutedForYou: + targetColors = [pink.cgColor, destructiveColor.cgColor, destructiveColor.cgColor] + case .muted: + targetColors = [pink.cgColor, purple.cgColor, purple.cgColor] + } + self.foregroundGradientLayer.colors = targetColors + if animated { + self.foregroundGradientLayer.animate(from: initialColors as AnyObject, to: targetColors as AnyObject, keyPath: "colors", timingFunction: CAMediaTimingFunctionName.linear.rawValue, duration: 0.3) + } + self.updateAnimations() + } +} + +final class ShimmerEffectForegroundNode: ASDisplayNode { + private var currentForegroundColor: UIColor? + private let imageNodeContainer: ASDisplayNode + private let imageNode: ASDisplayNode + + private var absoluteLocation: (CGRect, CGSize)? + private var isCurrentlyInHierarchy = false + private var shouldBeAnimating = false + + private let size: CGFloat + + init(size: CGFloat) { + self.size = size + + self.imageNodeContainer = ASDisplayNode() + self.imageNodeContainer.isLayerBacked = true + + self.imageNode = ASDisplayNode() + self.imageNode.isLayerBacked = true + self.imageNode.displaysAsynchronously = false + + 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(foregroundColor: UIColor) { + if let currentForegroundColor = self.currentForegroundColor, currentForegroundColor.isEqual(foregroundColor) { + return + } + self.currentForegroundColor = foregroundColor + + let image = generateImage(CGSize(width: self.size, height: 16.0), opaque: false, scale: 1.0, rotatedContext: { size, context in + context.clear(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: size.width, y: 0.0), options: CGGradientDrawingOptions()) + }) + if let image = image { + self.imageNode.backgroundColor = UIColor(patternImage: image) + } + } + + 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() + } else { + self.updateAnimation() + } + } + + 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 = self.size + self.imageNode.frame = CGRect(origin: CGPoint(x: -gradientHeight, y: 0.0), size: CGSize(width: gradientHeight, height: containerSize.height)) + let animation = self.imageNode.layer.makeAnimation(from: 0.0 as NSNumber, to: (containerSize.width + gradientHeight) as NSNumber, keyPath: "position.x", 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 class VoiceChatTileShimmeringNode: ASDisplayNode { + private let backgroundNode: ImageNode + private let effectNode: ShimmerEffectForegroundNode + + private let borderNode: ASDisplayNode + private var borderMaskView: UIView? + private let borderEffectNode: ShimmerEffectForegroundNode + + private var currentShimmeringColor: UIColor? + private var currentShimmering: Bool? + private var currentSize: CGSize? + + public init(account: Account, peer: Peer) { + self.backgroundNode = ImageNode(enableHasImage: false, enableEmpty: false, enableAnimatedTransition: true) + self.backgroundNode.displaysAsynchronously = false + self.backgroundNode.contentMode = .scaleAspectFill + + self.effectNode = ShimmerEffectForegroundNode(size: 240.0) + + self.borderNode = ASDisplayNode() + self.borderEffectNode = ShimmerEffectForegroundNode(size: 320.0) + + super.init() + + self.clipsToBounds = true + self.cornerRadius = backgroundCornerRadius + + self.addSubnode(self.backgroundNode) + self.addSubnode(self.effectNode) + self.addSubnode(self.borderNode) + self.borderNode.addSubnode(self.borderEffectNode) + + self.backgroundNode.setSignal(peerAvatarCompleteImage(account: account, peer: peer, size: CGSize(width: 250.0, height: 250.0), round: false, font: Font.regular(16.0), drawLetters: false, fullSize: false, blurred: true)) + } + + public override func didLoad() { + super.didLoad() + + if self.effectNode.supernode != nil { + self.effectNode.layer.compositingFilter = "screenBlendMode" + self.borderEffectNode.layer.compositingFilter = "screenBlendMode" + + let borderMaskView = UIView() + borderMaskView.layer.borderWidth = 1.0 + borderMaskView.layer.borderColor = UIColor.white.cgColor + borderMaskView.layer.cornerRadius = backgroundCornerRadius + self.borderMaskView = borderMaskView + + if let size = self.currentSize { + borderMaskView.frame = CGRect(origin: CGPoint(), size: size) + } + self.borderNode.view.mask = borderMaskView + + if #available(iOS 13.0, *) { + borderMaskView.layer.cornerCurve = .continuous + } + } + if #available(iOS 13.0, *) { + self.layer.cornerCurve = .continuous + } + } + + public func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) { + self.effectNode.updateAbsoluteRect(rect, within: containerSize) + self.borderEffectNode.updateAbsoluteRect(rect, within: containerSize) + } + + public func update(shimmeringColor: UIColor, shimmering: Bool, size: CGSize, transition: ContainedViewLayoutTransition) { + if let currentShimmeringColor = self.currentShimmeringColor, currentShimmeringColor.isEqual(shimmeringColor) && self.currentSize == size && self.currentShimmering == shimmering { + return + } + + let firstTime = self.currentShimmering == nil + self.currentShimmeringColor = shimmeringColor + self.currentShimmering = shimmering + self.currentSize = size + + let transition: ContainedViewLayoutTransition = firstTime ? .immediate : (transition.isAnimated ? transition : .animated(duration: 0.45, curve: .easeInOut)) + transition.updateAlpha(node: self.effectNode, alpha: shimmering ? 1.0 : 0.0) + transition.updateAlpha(node: self.borderNode, alpha: shimmering ? 1.0 : 0.0) + + let bounds = CGRect(origin: CGPoint(), size: size) + + self.effectNode.update(foregroundColor: shimmeringColor.withAlphaComponent(0.3)) + transition.updateFrame(node: self.effectNode, frame: bounds) + + self.borderEffectNode.update(foregroundColor: shimmeringColor.withAlphaComponent(0.45)) + transition.updateFrame(node: self.borderEffectNode, frame: bounds) + + transition.updateFrame(node: self.backgroundNode, frame: bounds) + transition.updateFrame(node: self.borderNode, frame: bounds) + if let borderMaskView = self.borderMaskView { + transition.updateFrame(view: borderMaskView, frame: bounds) + } + } +} diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatTimerNode.swift b/submodules/TelegramCallsUI/Sources/VoiceChatTimerNode.swift new file mode 100644 index 0000000000..9ae4a52d95 --- /dev/null +++ b/submodules/TelegramCallsUI/Sources/VoiceChatTimerNode.swift @@ -0,0 +1,182 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display +import SwiftSignalKit +import TelegramPresentationData +import TelegramStringFormatting + +private let purple = UIColor(rgb: 0x3252ef) +private let pink = UIColor(rgb: 0xef436c) + +private let latePurple = UIColor(rgb: 0x974aa9) +private let latePink = UIColor(rgb: 0xf0436c) + +final class VoiceChatTimerNode: ASDisplayNode { + private let strings: PresentationStrings + private let dateTimeFormat: PresentationDateTimeFormat + + private let titleNode: ImmediateTextNode + private let subtitleNode: ImmediateTextNode + + private let timerNode: ImmediateTextNode + + private let foregroundView = UIView() + private let foregroundGradientLayer = CAGradientLayer() + private let maskView = UIView() + + private var validLayout: CGSize? + + private var updateTimer: SwiftSignalKit.Timer? + + private let hierarchyTrackingNode: HierarchyTrackingNode + private var isCurrentlyInHierarchy = false + + private var isLate = false + + init(strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat) { + self.strings = strings + self.dateTimeFormat = dateTimeFormat + + var updateInHierarchy: ((Bool) -> Void)? + self.hierarchyTrackingNode = HierarchyTrackingNode({ value in + updateInHierarchy?(value) + }) + + self.titleNode = ImmediateTextNode() + self.subtitleNode = ImmediateTextNode() + + self.timerNode = ImmediateTextNode() + + super.init() + + self.addSubnode(self.hierarchyTrackingNode) + + self.allowsGroupOpacity = true + self.isUserInteractionEnabled = false + + self.foregroundGradientLayer.type = .radial + self.foregroundGradientLayer.colors = [pink.cgColor, purple.cgColor, purple.cgColor] + self.foregroundGradientLayer.locations = [0.0, 0.85, 1.0] + self.foregroundGradientLayer.startPoint = CGPoint(x: 1.0, y: 0.0) + self.foregroundGradientLayer.endPoint = CGPoint(x: 0.0, y: 1.0) + + self.foregroundView.mask = self.maskView + self.foregroundView.layer.addSublayer(self.foregroundGradientLayer) + + self.view.addSubview(self.foregroundView) + self.addSubnode(self.titleNode) + self.addSubnode(self.subtitleNode) + + self.maskView.addSubnode(self.timerNode) + + updateInHierarchy = { [weak self] value in + if let strongSelf = self { + strongSelf.isCurrentlyInHierarchy = value + strongSelf.updateAnimations() + } + } + } + + deinit { + self.updateTimer?.invalidate() + } + + func animateIn() { + self.foregroundView.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.6, damping: 100.0) + } + + private func updateAnimations() { + if self.isInHierarchy { + self.setupGradientAnimations() + } else { + self.foregroundGradientLayer.removeAllAnimations() + } + } + + private func setupGradientAnimations() { + if let _ = self.foregroundGradientLayer.animation(forKey: "movement") { + } else { + let previousValue = self.foregroundGradientLayer.startPoint + let newValue = CGPoint(x: CGFloat.random(in: 0.65 ..< 0.85), y: CGFloat.random(in: 0.1 ..< 0.45)) + self.foregroundGradientLayer.startPoint = newValue + + CATransaction.begin() + + let animation = CABasicAnimation(keyPath: "startPoint") + animation.duration = Double.random(in: 0.8 ..< 1.4) + animation.fromValue = previousValue + animation.toValue = newValue + + CATransaction.setCompletionBlock { [weak self] in + if let isCurrentlyInHierarchy = self?.isCurrentlyInHierarchy, isCurrentlyInHierarchy { + self?.setupGradientAnimations() + } + } + + self.foregroundGradientLayer.add(animation, forKey: "movement") + CATransaction.commit() + } + } + + func update(size: CGSize, scheduleTime: Int32?, transition: ContainedViewLayoutTransition) { + if self.validLayout == nil { + self.updateAnimations() + } + self.validLayout = size + + guard let scheduleTime = scheduleTime else { + return + } + + self.foregroundView.frame = CGRect(origin: CGPoint(), size: size) + self.foregroundGradientLayer.frame = self.foregroundView.bounds + self.maskView.frame = self.foregroundView.bounds + + let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) + let elapsedTime = scheduleTime - currentTime + let timerText: String + if elapsedTime >= 86400 { + timerText = scheduledTimeIntervalString(strings: self.strings, value: elapsedTime) + } else { + timerText = textForTimeout(value: abs(elapsedTime)) + if elapsedTime < 0 && !self.isLate { + self.isLate = true + self.foregroundGradientLayer.colors = [latePink.cgColor, latePurple.cgColor, latePurple.cgColor] + } + } + + if self.updateTimer == nil { + let timer = SwiftSignalKit.Timer(timeout: 0.5, repeat: true, completion: { [weak self] in + if let strongSelf = self, let size = strongSelf.validLayout { + strongSelf.update(size: size, scheduleTime: scheduleTime, transition: .immediate) + } + }, queue: Queue.mainQueue()) + self.updateTimer = timer + timer.start() + } + + let subtitle = humanReadableStringForTimestamp(strings: self.strings, dateTimeFormat: self.dateTimeFormat, timestamp: scheduleTime, alwaysShowTime: true).0 + + self.titleNode.attributedText = NSAttributedString(string: elapsedTime < 0 ? self.strings.VoiceChat_LateBy : self.strings.VoiceChat_StartsIn, font: Font.with(size: 23.0, design: .round, weight: .semibold, traits: []), textColor: .white) + let titleSize = self.titleNode.updateLayout(size) + self.titleNode.frame = CGRect(x: floor((size.width - titleSize.width) / 2.0), y: 48.0, width: titleSize.width, height: titleSize.height) + + + self.timerNode.attributedText = NSAttributedString(string: timerText, font: Font.with(size: 68.0, design: .round, weight: .semibold, traits: [.monospacedNumbers]), textColor: .white) + + var timerSize = self.timerNode.updateLayout(CGSize(width: size.width + 100.0, height: size.height)) + if timerSize.width > size.width - 32.0 { + self.timerNode.attributedText = NSAttributedString(string: timerText, font: Font.with(size: 60.0, design: .round, weight: .semibold, traits: [.monospacedNumbers]), textColor: .white) + timerSize = self.timerNode.updateLayout(CGSize(width: size.width + 100.0, height: size.height)) + } + + self.timerNode.frame = CGRect(x: floor((size.width - timerSize.width) / 2.0), y: 78.0, width: timerSize.width, height: timerSize.height) + + self.subtitleNode.attributedText = NSAttributedString(string: subtitle, font: Font.with(size: 21.0, design: .round, weight: .semibold, traits: []), textColor: .white) + let subtitleSize = self.subtitleNode.updateLayout(size) + self.subtitleNode.frame = CGRect(x: floor((size.width - subtitleSize.width) / 2.0), y: 164.0, width: subtitleSize.width, height: subtitleSize.height) + + self.foregroundView.frame = CGRect(origin: CGPoint(), size: size) + } +} diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatTitleEditController.swift b/submodules/TelegramCallsUI/Sources/VoiceChatTitleEditController.swift index ba30aeec5e..0f4f27e534 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatTitleEditController.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatTitleEditController.swift @@ -31,7 +31,11 @@ private final class VoiceChatTitleEditInputFieldNode: ASDisplayNode, ASEditableT set { self.textInputNode.attributedText = NSAttributedString(string: newValue, font: Font.regular(17.0), textColor: self.theme.actionSheet.inputTextColor) self.placeholderNode.isHidden = !newValue.isEmpty - self.clearButton.isHidden = newValue.isEmpty + if self.textInputNode.isFirstResponder() { + self.clearButton.isHidden = newValue.isEmpty + } else { + self.clearButton.isHidden = true + } } } @@ -41,8 +45,11 @@ private final class VoiceChatTitleEditInputFieldNode: ASDisplayNode, ASEditableT } } - init(theme: PresentationTheme, placeholder: String) { + private let maxLength: Int + + init(theme: PresentationTheme, placeholder: String, maxLength: Int, returnKeyType: UIReturnKeyType = .done) { self.theme = theme + self.maxLength = maxLength self.backgroundNode = ASImageNode() self.backgroundNode.isLayerBacked = true @@ -58,7 +65,7 @@ private final class VoiceChatTitleEditInputFieldNode: ASDisplayNode, ASEditableT self.textInputNode.keyboardAppearance = theme.rootController.keyboardColor.keyboardAppearance self.textInputNode.keyboardType = .default self.textInputNode.autocapitalizationType = .sentences - self.textInputNode.returnKeyType = .done + self.textInputNode.returnKeyType = returnKeyType self.textInputNode.autocorrectionType = .default self.textInputNode.tintColor = theme.actionSheet.controlAccentColor @@ -133,9 +140,17 @@ private final class VoiceChatTitleEditInputFieldNode: ASDisplayNode, ASEditableT self.clearButton.isHidden = !self.placeholderNode.isHidden } + func editableTextNodeDidBeginEditing(_ editableTextNode: ASEditableTextNode) { + self.clearButton.isHidden = (editableTextNode.textView.text ?? "").isEmpty + } + + func editableTextNodeDidFinishEditing(_ editableTextNode: ASEditableTextNode) { + self.clearButton.isHidden = true + } + func editableTextNode(_ editableTextNode: ASEditableTextNode, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool { let updatedText = (editableTextNode.textView.text as NSString).replacingCharacters(in: range, with: text) - if updatedText.count > 40 { + if updatedText.count > maxLength { self.textInputNode.layer.addShakeAnimation() return false } @@ -171,7 +186,6 @@ private final class VoiceChatTitleEditInputFieldNode: ASDisplayNode, ASEditableT self.clearButton.isHidden = true self.textInputNode.attributedText = nil - self.deactivateInput() self.updateHeight?() } } @@ -205,7 +219,7 @@ private final class VoiceChatTitleEditAlertContentNode: AlertContentNode { return self.isUserInteractionEnabled } - init(theme: AlertControllerTheme, ptheme: PresentationTheme, strings: PresentationStrings, actions: [TextAlertAction], title: String, text: String, placeholder: String, value: String?) { + init(theme: AlertControllerTheme, ptheme: PresentationTheme, strings: PresentationStrings, actions: [TextAlertAction], title: String, text: String, placeholder: String, value: String?, maxLength: Int) { self.strings = strings self.title = title self.text = text @@ -215,7 +229,7 @@ private final class VoiceChatTitleEditAlertContentNode: AlertContentNode { self.textNode = ASTextNode() self.textNode.maximumNumberOfLines = 8 - self.inputFieldNode = VoiceChatTitleEditInputFieldNode(theme: ptheme, placeholder: placeholder) + self.inputFieldNode = VoiceChatTitleEditInputFieldNode(theme: ptheme, placeholder: placeholder, maxLength: maxLength) self.inputFieldNode.text = value ?? "" self.actionNodesSeparator = ASDisplayNode() @@ -408,7 +422,7 @@ private final class VoiceChatTitleEditAlertContentNode: AlertContentNode { } } -func voiceChatTitleEditController(sharedContext: SharedAccountContext, account: Account, forceTheme: PresentationTheme?, title: String, text: String, placeholder: String, value: String?, apply: @escaping (String?) -> Void) -> AlertController { +func voiceChatTitleEditController(sharedContext: SharedAccountContext, account: Account, forceTheme: PresentationTheme?, title: String, text: String, placeholder: String, doneButtonTitle: String? = nil, value: String?, maxLength: Int, apply: @escaping (String?) -> Void) -> AlertController { var presentationData = sharedContext.currentPresentationData.with { $0 } if let forceTheme = forceTheme { presentationData = presentationData.withUpdated(theme: forceTheme) @@ -419,11 +433,11 @@ func voiceChatTitleEditController(sharedContext: SharedAccountContext, account: let actions: [TextAlertAction] = [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: { dismissImpl?(true) - }), TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Done, action: { + }), TextAlertAction(type: .defaultAction, title: doneButtonTitle ?? presentationData.strings.Common_Done, action: { applyImpl?() })] - let contentNode = VoiceChatTitleEditAlertContentNode(theme: AlertControllerTheme(presentationData: presentationData), ptheme: presentationData.theme, strings: presentationData.strings, actions: actions, title: title, text: text, placeholder: placeholder, value: value) + let contentNode = VoiceChatTitleEditAlertContentNode(theme: AlertControllerTheme(presentationData: presentationData), ptheme: presentationData.theme, strings: presentationData.strings, actions: actions, title: title, text: text, placeholder: placeholder, value: value, maxLength: maxLength) contentNode.complete = { applyImpl?() } @@ -450,8 +464,307 @@ func voiceChatTitleEditController(sharedContext: SharedAccountContext, account: controller.dismissed = { presentationDataDisposable.dispose() } - dismissImpl = { [weak controller] animated in - contentNode.inputFieldNode.deactivateInput() + dismissImpl = { [weak controller, weak contentNode] animated in + contentNode?.inputFieldNode.deactivateInput() + if animated { + controller?.dismissAnimated() + } else { + controller?.dismiss() + } + } + return controller +} + +private final class VoiceChatUserNameEditAlertContentNode: AlertContentNode { + private let strings: PresentationStrings + private let title: String + + private let titleNode: ASTextNode + let firstNameInputFieldNode: VoiceChatTitleEditInputFieldNode + let lastNameInputFieldNode: VoiceChatTitleEditInputFieldNode + + private let actionNodesSeparator: ASDisplayNode + private let actionNodes: [TextAlertContentActionNode] + private let actionVerticalSeparators: [ASDisplayNode] + + private let disposable = MetaDisposable() + + private var validLayout: CGSize? + + private let hapticFeedback = HapticFeedback() + + var complete: (() -> Void)? { + didSet { + self.lastNameInputFieldNode.complete = self.complete + } + } + + override var dismissOnOutsideTap: Bool { + return self.isUserInteractionEnabled + } + + init(theme: AlertControllerTheme, ptheme: PresentationTheme, strings: PresentationStrings, actions: [TextAlertAction], title: String, firstNamePlaceholder: String, lastNamePlaceholder: String, firstNameValue: String?, lastNameValue: String?, maxLength: Int) { + self.strings = strings + self.title = title + + self.titleNode = ASTextNode() + self.titleNode.maximumNumberOfLines = 2 + + self.firstNameInputFieldNode = VoiceChatTitleEditInputFieldNode(theme: ptheme, placeholder: firstNamePlaceholder, maxLength: maxLength, returnKeyType: .next) + self.firstNameInputFieldNode.text = firstNameValue ?? "" + + self.lastNameInputFieldNode = VoiceChatTitleEditInputFieldNode(theme: ptheme, placeholder: lastNamePlaceholder, maxLength: maxLength) + self.lastNameInputFieldNode.text = lastNameValue ?? "" + + 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.titleNode) + + self.addSubnode(self.firstNameInputFieldNode) + self.addSubnode(self.lastNameInputFieldNode) + + self.addSubnode(self.actionNodesSeparator) + + for actionNode in self.actionNodes { + self.addSubnode(actionNode) + } + + for separatorNode in self.actionVerticalSeparators { + self.addSubnode(separatorNode) + } + + self.updateTheme(theme) + + self.firstNameInputFieldNode.complete = { [weak self] in + self?.lastNameInputFieldNode.activateInput() + } + } + + deinit { + self.disposable.dispose() + } + + var firstName: String { + return self.firstNameInputFieldNode.text + } + + var lastName: String { + return self.lastNameInputFieldNode.text + } + + override func updateTheme(_ theme: AlertControllerTheme) { + self.titleNode.attributedText = NSAttributedString(string: self.title, font: Font.bold(17.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 spacing: CGFloat = 0.0 + + let titleSize = self.titleNode.measure(measureSize) + transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - titleSize.width) / 2.0), y: origin.y), size: titleSize)) + origin.y += titleSize.height + 4.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.updateLayout(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: 9.0, right: 18.0) + + var contentWidth = max(titleSize.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 inputFieldWidth = resultWidth + let firstInputFieldHeight = self.firstNameInputFieldNode.updateLayout(width: inputFieldWidth, transition: transition) + transition.updateFrame(node: self.firstNameInputFieldNode, frame: CGRect(x: 0.0, y: origin.y, width: resultWidth, height: firstInputFieldHeight)) + + origin.y += firstInputFieldHeight + spacing + + let lastInputFieldHeight = self.lastNameInputFieldNode.updateLayout(width: inputFieldWidth, transition: transition) + transition.updateFrame(node: self.lastNameInputFieldNode, frame: CGRect(x: 0.0, y: origin.y, width: resultWidth, height: lastInputFieldHeight)) + + let resultSize = CGSize(width: resultWidth, height: titleSize.height + firstInputFieldHeight + spacing + lastInputFieldHeight + actionsHeight + 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 { + self.firstNameInputFieldNode.activateInput() + } + + return resultSize + } + + func animateError() { + if self.firstNameInputFieldNode.text.isEmpty { + self.firstNameInputFieldNode.layer.addShakeAnimation() + } + self.hapticFeedback.error() + } +} + +func voiceChatUserNameController(sharedContext: SharedAccountContext, account: Account, forceTheme: PresentationTheme?, title: String, firstNamePlaceholder: String, lastNamePlaceholder: String, doneButtonTitle: String? = nil, firstName: String?, lastName: String?, maxLength: Int, apply: @escaping ((String, String)?) -> Void) -> AlertController { + var presentationData = sharedContext.currentPresentationData.with { $0 } + if let forceTheme = forceTheme { + presentationData = presentationData.withUpdated(theme: forceTheme) + } + + var dismissImpl: ((Bool) -> Void)? + var applyImpl: (() -> Void)? + + let actions: [TextAlertAction] = [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: { + dismissImpl?(true) + }), TextAlertAction(type: .defaultAction, title: doneButtonTitle ?? presentationData.strings.Common_Done, action: { + applyImpl?() + })] + + let contentNode = VoiceChatUserNameEditAlertContentNode(theme: AlertControllerTheme(presentationData: presentationData), ptheme: presentationData.theme, strings: presentationData.strings, actions: actions, title: title, firstNamePlaceholder: firstNamePlaceholder, lastNamePlaceholder: lastNamePlaceholder, firstNameValue: firstName, lastNameValue: lastName, maxLength: maxLength) + contentNode.complete = { + applyImpl?() + } + applyImpl = { [weak contentNode] in + guard let contentNode = contentNode else { + return + } + + let previousFirstName = firstName ?? "" + let previousLastName = lastName ?? "" + let newFirstName = contentNode.firstName.trimmingCharacters(in: .whitespacesAndNewlines) + let newLastName = contentNode.lastName.trimmingCharacters(in: .whitespacesAndNewlines) + + if newFirstName.isEmpty { + contentNode.animateError() + return + } + + dismissImpl?(true) + + if previousFirstName != newFirstName || previousLastName != newLastName { + apply((newFirstName, newLastName)) + } else { + apply(nil) + } + } + + let controller = AlertController(theme: AlertControllerTheme(presentationData: presentationData), contentNode: contentNode) + let presentationDataDisposable = sharedContext.presentationData.start(next: { [weak controller, weak contentNode] presentationData in + var presentationData = presentationData + if let forceTheme = forceTheme { + presentationData = presentationData.withUpdated(theme: forceTheme) + } + controller?.theme = AlertControllerTheme(presentationData: presentationData) + contentNode?.firstNameInputFieldNode.updateTheme(presentationData.theme) + contentNode?.lastNameInputFieldNode.updateTheme(presentationData.theme) + }) + controller.dismissed = { + presentationDataDisposable.dispose() + } + dismissImpl = { [weak controller, weak contentNode] animated in + contentNode?.firstNameInputFieldNode.deactivateInput() + contentNode?.lastNameInputFieldNode.deactivateInput() if animated { controller?.dismissAnimated() } else { diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatTitleNode.swift b/submodules/TelegramCallsUI/Sources/VoiceChatTitleNode.swift new file mode 100644 index 0000000000..7bf78c1574 --- /dev/null +++ b/submodules/TelegramCallsUI/Sources/VoiceChatTitleNode.swift @@ -0,0 +1,120 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display +import TelegramPresentationData +import ChatTitleActivityNode + +private let constructiveColor: UIColor = UIColor(rgb: 0x34c759) + +final class VoiceChatTitleNode: ASDisplayNode { + private var theme: PresentationTheme + + private let titleNode: ASTextNode + private let infoNode: ChatTitleActivityNode + let recordingIconNode: VoiceChatRecordingIconNode + + public var isRecording: Bool = false { + didSet { + self.recordingIconNode.isHidden = !self.isRecording + } + } + + var tapped: (() -> Void)? + + init(theme: PresentationTheme) { + self.theme = theme + + self.titleNode = ASTextNode() + self.titleNode.displaysAsynchronously = false + self.titleNode.maximumNumberOfLines = 1 + self.titleNode.truncationMode = .byTruncatingTail + self.titleNode.isOpaque = false + + self.infoNode = ChatTitleActivityNode() + + self.recordingIconNode = VoiceChatRecordingIconNode(hasBackground: false) + + super.init() + + self.addSubnode(self.titleNode) + self.addSubnode(self.infoNode) + self.addSubnode(self.recordingIconNode) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func didLoad() { + super.didLoad() + + self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tap))) + } + + override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { + if point.y > 0.0 && point.y < self.frame.size.height && point.x > min(self.titleNode.frame.minX, self.infoNode.frame.minX) && point.x < max(self.recordingIconNode.frame.maxX, self.infoNode.frame.maxX) { + return true + } else { + return false + } + } + + @objc private func tap() { + self.tapped?() + } + + func update(size: CGSize, title: String, subtitle: String, speaking: Bool, slide: Bool, transition: ContainedViewLayoutTransition) { + guard !size.width.isZero else { + return + } + var titleUpdated = false + if let previousTitle = self.titleNode.attributedText?.string { + titleUpdated = previousTitle != title + } + + if titleUpdated, let snapshotView = self.titleNode.view.snapshotContentTree() { + snapshotView.frame = self.titleNode.frame + self.view.addSubview(snapshotView) + + snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak snapshotView] _ in + snapshotView?.removeFromSuperview() + }) + + self.titleNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + + if slide { + self.infoNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + let offset: CGFloat = 16.0 + snapshotView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: -offset), duration: 0.2, removeOnCompletion: false, additive: true) + self.titleNode.layer.animatePosition(from: CGPoint(x: 0.0, y: offset), to: CGPoint(), duration: 0.2, additive: true) + self.infoNode.layer.animatePosition(from: CGPoint(x: 0.0, y: offset), to: CGPoint(), duration: 0.2, additive: true) + } + } + + self.titleNode.attributedText = NSAttributedString(string: title, font: Font.medium(17.0), textColor: UIColor(rgb: 0xffffff)) + + var state = ChatTitleActivityNodeState.none + if speaking { + state = .recordingVoice(NSAttributedString(string: subtitle, font: Font.regular(13.0), textColor: constructiveColor), constructiveColor) + } else { + state = .info(NSAttributedString(string: subtitle, font: Font.regular(13.0), textColor: UIColor(rgb: 0xffffff, alpha: 0.5)), .generic) + } + let _ = self.infoNode.transitionToState(state, animation: .slide) + + let constrainedSize = CGSize(width: size.width - 140.0, height: size.height) + let titleSize = self.titleNode.measure(constrainedSize) + let infoSize = self.infoNode.updateLayout(constrainedSize, offset: 1.0, alignment: .center) + let titleInfoSpacing: CGFloat = 0.0 + + let combinedHeight = titleSize.height + infoSize.height + titleInfoSpacing + + let titleFrame = CGRect(origin: CGPoint(x: floor((size.width - titleSize.width) / 2.0), y: floor((size.height - combinedHeight) / 2.0)), size: titleSize) + self.titleNode.frame = titleFrame + self.infoNode.frame = CGRect(origin: CGPoint(x: floor((size.width - infoSize.width) / 2.0), y: floor((size.height - combinedHeight) / 2.0) + titleSize.height + titleInfoSpacing), size: infoSize) + + let iconSide = 16.0 + (1.0 + UIScreenPixel) * 2.0 + let iconSize: CGSize = CGSize(width: iconSide, height: iconSide) + self.recordingIconNode.frame = CGRect(origin: CGPoint(x: titleFrame.maxX + 1.0, y: titleFrame.minY + 1.0), size: iconSize) + } +} diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatVolumeContextItem.swift b/submodules/TelegramCallsUI/Sources/VoiceChatVolumeContextItem.swift index 08fc213063..43e89a59ab 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatVolumeContextItem.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatVolumeContextItem.swift @@ -17,7 +17,7 @@ final class VoiceChatVolumeContextItem: ContextMenuCustomItem { self.valueChanged = valueChanged } - func node(presentationData: PresentationData, getController: @escaping () -> ContextController?, actionSelected: @escaping (ContextMenuActionResult) -> Void) -> ContextMenuCustomNode { + func node(presentationData: PresentationData, getController: @escaping () -> ContextControllerProtocol?, actionSelected: @escaping (ContextMenuActionResult) -> Void) -> ContextMenuCustomNode { return VoiceChatVolumeContextItemNode(presentationData: presentationData, getController: getController, minValue: self.minValue, value: self.value, valueChanged: self.valueChanged) } } @@ -45,7 +45,7 @@ private final class VoiceChatVolumeContextItemNode: ASDisplayNode, ContextMenuCu private let hapticFeedback = HapticFeedback() - init(presentationData: PresentationData, getController: @escaping () -> ContextController?, minValue: CGFloat, value: CGFloat, valueChanged: @escaping (CGFloat, Bool) -> Void) { + init(presentationData: PresentationData, getController: @escaping () -> ContextControllerProtocol?, minValue: CGFloat, value: CGFloat, valueChanged: @escaping (CGFloat, Bool) -> Void) { self.presentationData = presentationData self.minValue = minValue self.value = value diff --git a/submodules/TelegramCore/Sources/Account.swift b/submodules/TelegramCore/Sources/Account/Account.swift similarity index 97% rename from submodules/TelegramCore/Sources/Account.swift rename to submodules/TelegramCore/Sources/Account/Account.swift index 73786ebc8e..3c269a5ee9 100644 --- a/submodules/TelegramCore/Sources/Account.swift +++ b/submodules/TelegramCore/Sources/Account/Account.swift @@ -67,6 +67,11 @@ public class UnauthorizedAccount { public var updateLoginTokenEvents: Signal { return self.updateLoginTokenPipe.signal() } + + private let serviceNotificationPipe = ValuePipe() + public var serviceNotificationEvents: Signal { + return self.serviceNotificationPipe.signal() + } public var masterDatacenterId: Int32 { return Int32(self.network.mtProto.datacenterId) @@ -83,8 +88,11 @@ public class UnauthorizedAccount { self.postbox = postbox self.network = network let updateLoginTokenPipe = self.updateLoginTokenPipe + let serviceNotificationPipe = self.serviceNotificationPipe self.stateManager = UnauthorizedAccountStateManager(network: network, updateLoginToken: { updateLoginTokenPipe.putNext(Void()) + }, displayServiceNotification: { text in + serviceNotificationPipe.putNext(text) }) network.shouldKeepConnection.set(self.shouldBeServiceTaskMaster.get() @@ -362,13 +370,14 @@ public struct TwoStepAuthData { public let unconfirmedEmailPattern: String? public let secretRandom: Data public let nextSecurePasswordDerivation: TwoStepSecurePasswordDerivation + public let pendingResetTimestamp: Int32? } -public func twoStepAuthData(_ network: Network) -> Signal { +func _internal_twoStepAuthData(_ network: Network) -> Signal { return network.request(Api.functions.account.getPassword()) |> map { config -> TwoStepAuthData in switch config { - case let .password(flags, currentAlgo, srpB, srpId, hint, emailUnconfirmedPattern, newAlgo, newSecureAlgo, secureRandom): + case let .password(flags, currentAlgo, srpB, srpId, hint, emailUnconfirmedPattern, newAlgo, newSecureAlgo, secureRandom, pendingResetDate): let hasRecovery = (flags & (1 << 0)) != 0 let hasSecureValues = (flags & (1 << 1)) != 0 @@ -390,7 +399,7 @@ public func twoStepAuthData(_ network: Network) -> Signal Signal { - return twoStepAuthData(account.network) + return _internal_twoStepAuthData(account.network) |> mapToSignal { authData -> Signal in guard let currentPasswordDerivation = authData.currentPasswordDerivation, let srpSessionData = authData.srpSessionData else { return .fail(MTRpcError(errorCode: 400, errorDescription: "INTERNAL_NO_PASSWORD")) @@ -843,7 +852,7 @@ public class Account { 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 messageMediaPreuploadManager: MessageMediaPreuploadManager! private(set) var mediaReferenceRevalidationContext: MediaReferenceRevalidationContext! private var peerInputActivityManager: PeerInputActivityManager! private var localInputActivityManager: PeerInputActivityManager! @@ -1046,7 +1055,7 @@ public class Account { self.managedOperationsDisposable.add(managedSynchronizeRecentlyUsedMediaOperations(postbox: self.postbox, network: self.network, category: .stickers, revalidationContext: self.mediaReferenceRevalidationContext).start()) self.managedOperationsDisposable.add(managedSynchronizeSavedGifsOperations(postbox: self.postbox, network: self.network, revalidationContext: self.mediaReferenceRevalidationContext).start()) self.managedOperationsDisposable.add(managedSynchronizeSavedStickersOperations(postbox: self.postbox, network: self.network, revalidationContext: self.mediaReferenceRevalidationContext).start()) - self.managedOperationsDisposable.add(managedRecentlyUsedInlineBots(postbox: self.postbox, network: self.network, accountPeerId: peerId).start()) + self.managedOperationsDisposable.add(_internal_managedRecentlyUsedInlineBots(postbox: self.postbox, network: self.network, accountPeerId: peerId).start()) self.managedOperationsDisposable.add(managedLocalTypingActivities(activities: self.localInputActivityManager.allActivities(), postbox: self.postbox, network: self.network, accountPeerId: self.peerId).start()) self.managedOperationsDisposable.add(managedSynchronizeConsumeMessageContentOperations(postbox: self.postbox, network: self.network, stateManager: self.stateManager).start()) self.managedOperationsDisposable.add(managedConsumePersonalMessagesActions(postbox: self.postbox, network: self.network, stateManager: self.stateManager).start()) @@ -1123,15 +1132,17 @@ public class Account { self.managedOperationsDisposable.add(managedAnimatedEmojiUpdates(postbox: self.postbox, network: self.network).start()) } self.managedOperationsDisposable.add(managedGreetingStickers(postbox: self.postbox, network: self.network).start()) - - let mediaBox = postbox.mediaBox - self.storageSettingsDisposable = accountManager.sharedData(keys: [SharedDataKeys.cacheStorageSettings]).start(next: { [weak mediaBox] sharedData in - guard let mediaBox = mediaBox else { - return - } - let settings: CacheStorageSettings = sharedData.entries[SharedDataKeys.cacheStorageSettings] as? CacheStorageSettings ?? CacheStorageSettings.defaultSettings - mediaBox.setMaxStoreTimes(general: settings.defaultCacheStorageTimeout, shortLived: 60 * 60, gigabytesLimit: settings.defaultCacheStorageLimitGigabytes) - }) + + if !supplementary { + let mediaBox = postbox.mediaBox + self.storageSettingsDisposable = accountManager.sharedData(keys: [SharedDataKeys.cacheStorageSettings]).start(next: { [weak mediaBox] sharedData in + guard let mediaBox = mediaBox else { + return + } + let settings: CacheStorageSettings = sharedData.entries[SharedDataKeys.cacheStorageSettings] as? CacheStorageSettings ?? CacheStorageSettings.defaultSettings + mediaBox.setMaxStoreTimes(general: settings.defaultCacheStorageTimeout, shortLived: 60 * 60, gigabytesLimit: settings.defaultCacheStorageLimitGigabytes) + }) + } let _ = masterNotificationsKey(masterNotificationKeyValue: self.masterNotificationKey, postbox: self.postbox, ignoreDisabled: false).start(next: { key in let encoder = JSONEncoder() @@ -1249,9 +1260,9 @@ public func setupAccount(_ account: Account, fetchCachedResourceRepresentation: account.postbox.mediaBox.preFetchedResourcePath = preFetchedResourcePath account.postbox.mediaBox.fetchResource = { [weak account] resource, intervals, parameters -> Signal in if let strongAccount = account { - if let result = fetchResource(account: strongAccount, resource: resource, intervals: intervals, parameters: parameters) { + if let result = strongAccount.auxiliaryMethods.fetchResource(strongAccount, resource, intervals, parameters) { return result - } else if let result = strongAccount.auxiliaryMethods.fetchResource(strongAccount, resource, intervals, parameters) { + } else if let result = fetchResource(account: strongAccount, resource: resource, intervals: intervals, parameters: parameters) { return result } else { return .never() diff --git a/submodules/TelegramCore/Sources/AccountIntermediateState.swift b/submodules/TelegramCore/Sources/Account/AccountIntermediateState.swift similarity index 99% rename from submodules/TelegramCore/Sources/AccountIntermediateState.swift rename to submodules/TelegramCore/Sources/Account/AccountIntermediateState.swift index c468b1f85c..dcce140d9e 100644 --- a/submodules/TelegramCore/Sources/AccountIntermediateState.swift +++ b/submodules/TelegramCore/Sources/Account/AccountIntermediateState.swift @@ -417,7 +417,7 @@ struct AccountMutableState { switch user { case let .user(_, id, _, _, _, _, _, _, status, _, _, _, _): if let status = status { - presences[PeerId(namespace: Namespaces.Peer.CloudUser, id: id)] = status + presences[PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt32Value(id))] = status } break case .userEmpty: diff --git a/submodules/TelegramCore/Sources/AccountManager.swift b/submodules/TelegramCore/Sources/Account/AccountManager.swift similarity index 96% rename from submodules/TelegramCore/Sources/AccountManager.swift rename to submodules/TelegramCore/Sources/Account/AccountManager.swift index 69ed985493..b1e3d106d0 100644 --- a/submodules/TelegramCore/Sources/AccountManager.swift +++ b/submodules/TelegramCore/Sources/Account/AccountManager.swift @@ -174,6 +174,8 @@ private var declaredEncodables: Void = { declareEncodable(CachedPeerExportedInvitations.self, f: { CachedPeerExportedInvitations(decoder: $0) }) declareEncodable(ExportedInvitation.self, f: { ExportedInvitation(decoder: $0) }) declareEncodable(CachedDisplayAsPeers.self, f: { CachedDisplayAsPeers(decoder: $0) }) + declareEncodable(WallpapersState.self, f: { WallpapersState(decoder: $0) }) + declareEncodable(WallpaperDataResource.self, f: { WallpaperDataResource(decoder: $0) }) return }() @@ -187,7 +189,27 @@ public func rootPathForBasePath(_ appGroupPath: String) -> String { } public func performAppGroupUpgrades(appGroupPath: String, rootPath: String) { - let _ = try? FileManager.default.createDirectory(at: URL(fileURLWithPath: rootPath), withIntermediateDirectories: true, attributes: nil) + DispatchQueue.global(qos: .default).async { + let _ = try? FileManager.default.createDirectory(at: URL(fileURLWithPath: rootPath), withIntermediateDirectories: true, attributes: nil) + + if let items = FileManager.default.enumerator(at: URL(fileURLWithPath: appGroupPath), includingPropertiesForKeys: [.isDirectoryKey], options: [.skipsHiddenFiles, .skipsSubdirectoryDescendants], errorHandler: nil) { + let allowedDirectories: [String] = [ + "telegram-data", + "Library" + ] + + for url in items { + guard let url = url as? URL else { + continue + } + if let isDirectory = try? url.resourceValues(forKeys: [.isDirectoryKey]).isDirectory, isDirectory { + if !allowedDirectories.contains(url.lastPathComponent) { + let _ = try? FileManager.default.removeItem(at: url) + } + } + } + } + } do { var resourceValues = URLResourceValues() diff --git a/submodules/TelegramCore/Sources/AccountStateReset.swift b/submodules/TelegramCore/Sources/AccountStateReset.swift deleted file mode 100644 index 02d0d22972..0000000000 --- a/submodules/TelegramCore/Sources/AccountStateReset.swift +++ /dev/null @@ -1,292 +0,0 @@ -import Foundation -import Postbox -import SwiftSignalKit -import TelegramApi -import MtProtoKit - -import SyncCore - -private struct LocalChatListEntryRange { - var entries: [ChatListNamespaceEntry] - var upperBound: ChatListIndex? - var lowerBound: ChatListIndex - var count: Int32 - var hash: UInt32 - - var apiHash: Int32 { - return Int32(bitPattern: self.hash & UInt32(0x7FFFFFFF)) - } -} - -private func combineHash(_ value: Int32, into hash: inout UInt32) { - let low = UInt32(bitPattern: value) - hash = (hash &* 20261) &+ low -} - -private func combineChatListNamespaceEntryHash(index: ChatListIndex, readState: PeerReadState?, topMessageAttributes: [MessageAttribute], tagSummary: MessageHistoryTagNamespaceSummary?, interfaceState: PeerChatInterfaceState?, into hash: inout UInt32) { - /* - dialog.pinned ? 1 : 0, - dialog.unread_mark ? 1 : 0, - dialog.peer.channel_id || dialog.peer.chat_id || dialog.peer.user_id, - dialog.top_message.id, - top_message.edit_date || top_message.date, - dialog.read_inbox_max_id, - dialog.read_outbox_max_id, - dialog.unread_count, - dialog.unread_mentions_count, - draft.draft.date || 0 - - */ - - combineHash(index.pinningIndex != nil ? 1 : 0, into: &hash) - if let readState = readState, readState.markedUnread { - combineHash(1, into: &hash) - } else { - combineHash(0, into: &hash) - } - combineHash(index.messageIndex.id.peerId.id, into: &hash) - combineHash(index.messageIndex.id.id, into: &hash) - var timestamp = index.messageIndex.timestamp - for attribute in topMessageAttributes { - if let attribute = attribute as? EditedMessageAttribute { - timestamp = max(timestamp, attribute.date) - } - } - combineHash(timestamp, into: &hash) - if let readState = readState, case let .idBased(maxIncomingReadId, maxOutgoingReadId, _, count, _) = readState { - combineHash(maxIncomingReadId, into: &hash) - combineHash(maxOutgoingReadId, into: &hash) - combineHash(count, into: &hash) - } else { - combineHash(0, into: &hash) - combineHash(0, into: &hash) - combineHash(0, into: &hash) - } - - if let tagSummary = tagSummary { - combineHash(tagSummary.count, into: &hash) - } else { - combineHash(0, into: &hash) - } - - if let embeddedState = interfaceState?.chatListEmbeddedState { - combineHash(embeddedState.timestamp, into: &hash) - } else { - combineHash(0, into: &hash) - } -} - -private func localChatListEntryRanges(_ entries: [ChatListNamespaceEntry], limit: Int) -> [LocalChatListEntryRange] { - var result: [LocalChatListEntryRange] = [] - var currentRange: LocalChatListEntryRange? - for i in 0 ..< entries.count { - switch entries[i] { - case let .peer(index, readState, topMessageAttributes, tagSummary, interfaceState): - var updatedRange: LocalChatListEntryRange - if let current = currentRange { - updatedRange = current - } else { - updatedRange = LocalChatListEntryRange(entries: [], upperBound: result.last?.lowerBound, lowerBound: index, count: 0, hash: 0) - } - updatedRange.entries.append(entries[i]) - updatedRange.lowerBound = index - updatedRange.count += 1 - - combineChatListNamespaceEntryHash(index: index, readState: readState, topMessageAttributes: topMessageAttributes, tagSummary: tagSummary, interfaceState: interfaceState, into: &updatedRange.hash) - - if Int(updatedRange.count) >= limit { - result.append(updatedRange) - currentRange = nil - } else { - currentRange = updatedRange - } - case .hole: - if let currentRangeValue = currentRange { - result.append(currentRangeValue) - currentRange = nil - } - } - } - if let currentRangeValue = currentRange { - result.append(currentRangeValue) - currentRange = nil - } - return result -} - -private struct ResolvedChatListResetRange { - let head: Bool - let local: LocalChatListEntryRange - let remote: FetchedChatList -} - -/*func accountStateReset(postbox: Postbox, network: Network, accountPeerId: PeerId) -> Signal { - let pinnedChats: Signal = network.request(Api.functions.messages.getPinnedDialogs(folderId: 0)) - |> retryRequest - let state: Signal = network.request(Api.functions.updates.getState()) - |> retryRequest - - return postbox.transaction { transaction -> [ChatListNamespaceEntry] in - return transaction.getChatListNamespaceEntries(groupId: .root, namespace: Namespaces.Message.Cloud, summaryTag: MessageTags.unseenPersonalMessage) - } - |> mapToSignal { localChatListEntries -> Signal in - let localRanges = localChatListEntryRanges(localChatListEntries, limit: 100) - var signal: Signal = .complete() - for i in 0 ..< localRanges.count { - let upperBound: MessageIndex - let head = i == 0 - let localRange = localRanges[i] - if let rangeUpperBound = localRange.upperBound { - upperBound = rangeUpperBound.messageIndex.predecessor() - } else { - upperBound = MessageIndex(id: MessageId(peerId: PeerId(namespace: Namespaces.Peer.Empty, id: 0), namespace: Namespaces.Message.Cloud, id: 0), timestamp: 0) - } - - let rangeSignal: Signal = fetchChatList(postbox: postbox, network: network, location: .general, upperBound: upperBound, hash: localRange.apiHash, limit: localRange.count) - |> map { remote -> ResolvedChatListResetRange? in - if let remote = remote { - return ResolvedChatListResetRange(head: head, local: localRange, remote: remote) - } else { - return nil - } - } - - signal = signal - |> then(rangeSignal) - } - let collectedResolvedRanges: Signal<[ResolvedChatListResetRange], NoError> = signal - |> map { next -> [ResolvedChatListResetRange] in - if let next = next { - return [next] - } else { - return [] - } - } - |> reduceLeft(value: [], f: { list, next in - var list = list - list.append(contentsOf: next) - return list - }) - - return combineLatest(collectedResolvedRanges, state) - |> mapToSignal { collectedRanges, state -> Signal in - return postbox.transaction { transaction -> Void in - for range in collectedRanges { - let previousPeerIds = transaction.resetChatList(keepPeerNamespaces: [Namespaces.Peer.SecretChat], upperBound: range.local.upperBound ?? ChatListIndex.absoluteUpperBound, lowerBound: range.local.lowerBound) - #if DEBUG - for peerId in previousPeerIds { - print("pre \(peerId) [\(transaction.getPeer(peerId)?.debugDisplayTitle ?? "nil")]") - } - print("pre hash \(range.local.hash)") - print("") - - var preRecalculatedHash: UInt32 = 0 - for entry in range.local.entries { - switch entry { - case let .peer(index, readState, topMessageAttributes, tagSummary, interfaceState): - print("val \(index.messageIndex.id.peerId) [\(transaction.getPeer(index.messageIndex.id.peerId)?.debugDisplayTitle ?? "nil")]") - combineChatListNamespaceEntryHash(index: index, readState: readState, topMessageAttributes: topMessageAttributes, tagSummary: nil, interfaceState: nil, into: &preRecalculatedHash) - default: - break - } - } - print("pre recalculated hash \(preRecalculatedHash)") - print("") - - var hash: UInt32 = 0 - range.remote.storeMessages.compactMap({ message -> MessageIndex? in - if case let .Id(id) = message.id { - if range.remote.topMessageIds[id.peerId] == id { - return message.index - } - } - return nil - }).sorted(by: { lhs, rhs in - return lhs > rhs - }).forEach({ index in - var topMessageAttributes: [MessageAttribute] = [] - for message in range.remote.storeMessages { - if case let .Id(id) = message.id, id == index.id { - topMessageAttributes = message.attributes - } - } - combineChatListNamespaceEntryHash(index: ChatListIndex(pinningIndex: nil, messageIndex: index), readState: range.remote.readStates[index.id.peerId]?[Namespaces.Message.Cloud], topMessageAttributes: topMessageAttributes, tagSummary: nil, interfaceState: nil, into: &hash) - print("upd \(index.id.peerId) [\(transaction.getPeer(index.id.peerId)?.debugDisplayTitle ?? "nil")]") - }) - print("upd hash \(hash)") - #endif - - updatePeers(transaction: transaction, peers: range.remote.peers, update: { _, updated -> Peer in - return updated - }) - updatePeerPresences(transaction: transaction, accountPeerId: accountPeerId, peerPresences: range.remote.peerPresences) - - transaction.updateCurrentPeerNotificationSettings(range.remote.notificationSettings) - - var allPeersWithMessages = Set() - for message in range.remote.storeMessages { - allPeersWithMessages.insert(message.id.peerId) - } - - for (_, messageId) in range.remote.topMessageIds { - if messageId.id > 1 { - var skipHole = false - if let localTopId = transaction.getTopPeerMessageIndex(peerId: messageId.peerId, namespace: messageId.namespace)?.id { - if localTopId >= messageId { - skipHole = true - } - } - if !skipHole { - //transaction.addHole(MessageId(peerId: messageId.peerId, namespace: messageId.namespace, id: messageId.id - 1)) - } - } - } - - let _ = transaction.addMessages(range.remote.storeMessages, location: .UpperHistoryBlock) - - transaction.resetIncomingReadStates(range.remote.readStates) - - for (peerId, chatState) in range.remote.chatStates { - if let chatState = chatState as? ChannelState { - if let current = transaction.getPeerChatState(peerId) as? ChannelState { - transaction.setPeerChatState(peerId, state: current.withUpdatedPts(chatState.pts)) - } else { - transaction.setPeerChatState(peerId, state: chatState) - } - } else { - transaction.setPeerChatState(peerId, state: chatState) - } - } - - for (peerId, summary) in range.remote.mentionTagSummaries { - transaction.replaceMessageTagSummary(peerId: peerId, tagMask: .unseenPersonalMessage, namespace: Namespaces.Message.Cloud, count: summary.count, maxId: summary.range.maxId) - } - - let namespacesWithHoles: [PeerId.Namespace: [MessageId.Namespace]] = [ - Namespaces.Peer.CloudUser: [Namespaces.Message.Cloud], - Namespaces.Peer.CloudGroup: [Namespaces.Message.Cloud], - Namespaces.Peer.CloudChannel: [Namespaces.Message.Cloud] - ] - for peerId in previousPeerIds { - if !allPeersWithMessages.contains(peerId), let namespaces = namespacesWithHoles[peerId.namespace] { - for namespace in namespaces { - //transaction.addHole(MessageId(peerId: peerId, namespace: namespace, id: Int32.max - 1)) - } - } - } - - if range.head { - transaction.setPinnedItemIds(groupId: nil, itemIds: range.remote.pinnedItemIds ?? []) - } - } - - if let currentState = transaction.getState() as? AuthorizedAccountState, let embeddedState = currentState.state { - switch state { - case let .state(pts, _, _, seq, _): - transaction.setState(currentState.changedState(AuthorizedAccountState.State(pts: pts, qts: embeddedState.qts, date: embeddedState.date, seq: seq))) - } - } - } - } - } -}*/ diff --git a/submodules/TelegramCore/Sources/ApiGroupOrChannel.swift b/submodules/TelegramCore/Sources/ApiUtils/ApiGroupOrChannel.swift similarity index 79% rename from submodules/TelegramCore/Sources/ApiGroupOrChannel.swift rename to submodules/TelegramCore/Sources/ApiUtils/ApiGroupOrChannel.swift index 46c548fae7..bb4734b39a 100644 --- a/submodules/TelegramCore/Sources/ApiGroupOrChannel.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/ApiGroupOrChannel.swift @@ -7,19 +7,15 @@ import SyncCore func imageRepresentationsForApiChatPhoto(_ photo: Api.ChatPhoto) -> [TelegramMediaImageRepresentation] { var representations: [TelegramMediaImageRepresentation] = [] switch photo { - case let .chatPhoto(flags, photoSmall, photoBig, dcId): + case let .chatPhoto(_, photoId, strippedThumb, dcId): let smallResource: TelegramMediaResource let fullSizeResource: TelegramMediaResource - switch photoSmall { - case let .fileLocationToBeDeprecated(volumeId, localId): - smallResource = CloudPeerPhotoSizeMediaResource(datacenterId: dcId, sizeSpec: .small, volumeId: volumeId, localId: localId) - } - switch photoBig { - case let .fileLocationToBeDeprecated(volumeId, localId): - fullSizeResource = CloudPeerPhotoSizeMediaResource(datacenterId: dcId, sizeSpec: .fullSize, volumeId: volumeId, localId: localId) - } - representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 80, height: 80), resource: smallResource, progressiveSizes: [])) - representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 640, height: 640), resource: fullSizeResource, progressiveSizes: [])) + + smallResource = CloudPeerPhotoSizeMediaResource(datacenterId: dcId, photoId: photoId, sizeSpec: .small, volumeId: nil, localId: nil) + fullSizeResource = CloudPeerPhotoSizeMediaResource(datacenterId: dcId, photoId: photoId, sizeSpec: .fullSize, volumeId: nil, localId: nil) + + representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 80, height: 80), resource: smallResource, progressiveSizes: [], immediateThumbnailData: strippedThumb?.makeData())) + representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 640, height: 640), resource: fullSizeResource, progressiveSizes: [], immediateThumbnailData: strippedThumb?.makeData())) case .chatPhotoEmpty: break } @@ -34,7 +30,7 @@ func parseTelegramGroupOrChannel(chat: Api.Chat) -> Peer? { if let migratedTo = migratedTo { switch migratedTo { case let .inputChannel(channelId, accessHash): - migrationReference = TelegramGroupToChannelMigrationReference(peerId: PeerId(namespace: Namespaces.Peer.CloudChannel, id: channelId), accessHash: accessHash) + migrationReference = TelegramGroupToChannelMigrationReference(peerId: PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt32Value(channelId)), accessHash: accessHash) case .inputChannelEmpty: break case .inputChannelFromMessage: @@ -57,11 +53,11 @@ func parseTelegramGroupOrChannel(chat: Api.Chat) -> Peer? { if (flags & Int32(1 << 24)) != 0 { groupFlags.insert(.hasActiveVoiceChat) } - return TelegramGroup(id: PeerId(namespace: Namespaces.Peer.CloudGroup, id: id), title: title, photo: imageRepresentationsForApiChatPhoto(photo), participantCount: Int(participantsCount), role: role, membership: left ? .Left : .Member, flags: groupFlags, defaultBannedRights: defaultBannedRights.flatMap(TelegramChatBannedRights.init(apiBannedRights:)), migrationReference: migrationReference, creationDate: date, version: Int(version)) + return TelegramGroup(id: PeerId(namespace: Namespaces.Peer.CloudGroup, id: PeerId.Id._internalFromInt32Value(id)), title: title, photo: imageRepresentationsForApiChatPhoto(photo), participantCount: Int(participantsCount), role: role, membership: left ? .Left : .Member, flags: groupFlags, defaultBannedRights: defaultBannedRights.flatMap(TelegramChatBannedRights.init(apiBannedRights:)), migrationReference: migrationReference, creationDate: date, version: Int(version)) case let .chatEmpty(id): - return TelegramGroup(id: PeerId(namespace: Namespaces.Peer.CloudGroup, id: id), title: "", photo: [], participantCount: 0, role: .member, membership: .Removed, flags: [], defaultBannedRights: nil, migrationReference: nil, creationDate: 0, version: 0) + return TelegramGroup(id: PeerId(namespace: Namespaces.Peer.CloudGroup, id: PeerId.Id._internalFromInt32Value(id)), title: "", photo: [], participantCount: 0, role: .member, membership: .Removed, flags: [], defaultBannedRights: nil, migrationReference: nil, creationDate: 0, version: 0) case let .chatForbidden(id, title): - return TelegramGroup(id: PeerId(namespace: Namespaces.Peer.CloudGroup, id: id), title: title, photo: [], participantCount: 0, role: .member, membership: .Removed, flags: [], defaultBannedRights: nil, migrationReference: nil, creationDate: 0, version: 0) + return TelegramGroup(id: PeerId(namespace: Namespaces.Peer.CloudGroup, id: PeerId.Id._internalFromInt32Value(id)), title: title, photo: [], participantCount: 0, role: .member, membership: .Removed, flags: [], defaultBannedRights: nil, migrationReference: nil, creationDate: 0, version: 0) case let .channel(flags, id, accessHash, title, username, photo, date, version, restrictionReason, adminRights, bannedRights, defaultBannedRights, _): let isMin = (flags & (1 << 12)) != 0 @@ -133,7 +129,7 @@ func parseTelegramGroupOrChannel(chat: Api.Chat) -> Peer? { } } - return TelegramChannel(id: PeerId(namespace: Namespaces.Peer.CloudChannel, id: id), accessHash: accessHashValue, title: title, username: username, photo: imageRepresentationsForApiChatPhoto(photo), creationDate: date, version: version, participationStatus: participationStatus, info: info, flags: channelFlags, restrictionInfo: restrictionInfo, adminRights: adminRights.flatMap(TelegramChatAdminRights.init), bannedRights: bannedRights.flatMap(TelegramChatBannedRights.init), defaultBannedRights: defaultBannedRights.flatMap(TelegramChatBannedRights.init)) + return TelegramChannel(id: PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt32Value(id)), accessHash: accessHashValue, title: title, username: username, photo: imageRepresentationsForApiChatPhoto(photo), creationDate: date, version: version, participationStatus: participationStatus, info: info, flags: channelFlags, restrictionInfo: restrictionInfo, adminRights: adminRights.flatMap(TelegramChatAdminRights.init), bannedRights: bannedRights.flatMap(TelegramChatBannedRights.init), defaultBannedRights: defaultBannedRights.flatMap(TelegramChatBannedRights.init)) case let .channelForbidden(flags, id, accessHash, title, untilDate): let info: TelegramChannelInfo if (flags & Int32(1 << 8)) != 0 { @@ -142,7 +138,7 @@ func parseTelegramGroupOrChannel(chat: Api.Chat) -> Peer? { info = .broadcast(TelegramChannelBroadcastInfo(flags: [])) } - return TelegramChannel(id: PeerId(namespace: Namespaces.Peer.CloudChannel, id: id), accessHash: .personal(accessHash), title: title, username: nil, photo: [], creationDate: 0, version: 0, participationStatus: .kicked, info: info, flags: TelegramChannelFlags(), restrictionInfo: nil, adminRights: nil, bannedRights: TelegramChatBannedRights(flags: [.banReadMessages], untilDate: untilDate ?? Int32.max), defaultBannedRights: nil) + return TelegramChannel(id: PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt32Value(id)), accessHash: .personal(accessHash), title: title, username: nil, photo: [], creationDate: 0, version: 0, participationStatus: .kicked, info: info, flags: TelegramChannelFlags(), restrictionInfo: nil, adminRights: nil, bannedRights: TelegramChatBannedRights(flags: [.banReadMessages], untilDate: untilDate ?? Int32.max), defaultBannedRights: nil) } } diff --git a/submodules/TelegramCore/Sources/ApiUtils.swift b/submodules/TelegramCore/Sources/ApiUtils/ApiUtils.swift similarity index 74% rename from submodules/TelegramCore/Sources/ApiUtils.swift rename to submodules/TelegramCore/Sources/ApiUtils/ApiUtils.swift index f7beff73f4..9f9ffc38f9 100644 --- a/submodules/TelegramCore/Sources/ApiUtils.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/ApiUtils.swift @@ -8,11 +8,11 @@ public extension PeerReference { var id: PeerId { switch self { case let .user(id, _): - return PeerId(namespace: Namespaces.Peer.CloudUser, id: id) + return PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt32Value(id)) case let .group(id): - return PeerId(namespace: Namespaces.Peer.CloudGroup, id: id) + return PeerId(namespace: Namespaces.Peer.CloudGroup, id: PeerId.Id._internalFromInt32Value(id)) case let .channel(id, _): - return PeerId(namespace: Namespaces.Peer.CloudChannel, id: id) + return PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt32Value(id)) } } } @@ -49,12 +49,12 @@ extension PeerReference { func forceApiInputPeer(_ peer: Peer) -> Api.InputPeer? { switch peer { case let user as TelegramUser: - return Api.InputPeer.inputPeerUser(userId: user.id.id, accessHash: user.accessHash?.value ?? 0) + return Api.InputPeer.inputPeerUser(userId: user.id.id._internalGetInt32Value(), accessHash: user.accessHash?.value ?? 0) case let group as TelegramGroup: - return Api.InputPeer.inputPeerChat(chatId: group.id.id) + return Api.InputPeer.inputPeerChat(chatId: group.id.id._internalGetInt32Value()) case let channel as TelegramChannel: if let accessHash = channel.accessHash { - return Api.InputPeer.inputPeerChannel(channelId: channel.id.id, accessHash: accessHash.value) + return Api.InputPeer.inputPeerChannel(channelId: channel.id.id._internalGetInt32Value(), accessHash: accessHash.value) } else { return nil } @@ -66,12 +66,12 @@ func forceApiInputPeer(_ peer: Peer) -> Api.InputPeer? { func apiInputPeer(_ peer: Peer) -> Api.InputPeer? { switch peer { case let user as TelegramUser where user.accessHash != nil: - return Api.InputPeer.inputPeerUser(userId: user.id.id, accessHash: user.accessHash!.value) + return Api.InputPeer.inputPeerUser(userId: user.id.id._internalGetInt32Value(), accessHash: user.accessHash!.value) case let group as TelegramGroup: - return Api.InputPeer.inputPeerChat(chatId: group.id.id) + return Api.InputPeer.inputPeerChat(chatId: group.id.id._internalGetInt32Value()) case let channel as TelegramChannel: if let accessHash = channel.accessHash { - return Api.InputPeer.inputPeerChannel(channelId: channel.id.id, accessHash: accessHash.value) + return Api.InputPeer.inputPeerChannel(channelId: channel.id.id._internalGetInt32Value(), accessHash: accessHash.value) } else { return nil } @@ -82,7 +82,7 @@ func apiInputPeer(_ peer: Peer) -> Api.InputPeer? { func apiInputChannel(_ peer: Peer) -> Api.InputChannel? { if let channel = peer as? TelegramChannel, let accessHash = channel.accessHash { - return Api.InputChannel.inputChannel(channelId: channel.id.id, accessHash: accessHash.value) + return Api.InputChannel.inputChannel(channelId: channel.id.id._internalGetInt32Value(), accessHash: accessHash.value) } else { return nil } @@ -90,7 +90,7 @@ func apiInputChannel(_ peer: Peer) -> Api.InputChannel? { func apiInputUser(_ peer: Peer) -> Api.InputUser? { if let user = peer as? TelegramUser, let accessHash = user.accessHash { - return Api.InputUser.inputUser(userId: user.id.id, accessHash: accessHash.value) + return Api.InputUser.inputUser(userId: user.id.id._internalGetInt32Value(), accessHash: accessHash.value) } else { return nil } @@ -98,7 +98,7 @@ func apiInputUser(_ peer: Peer) -> Api.InputUser? { func apiInputSecretChat(_ peer: Peer) -> Api.InputEncryptedChat? { if let chat = peer as? TelegramSecretChat { - return Api.InputEncryptedChat.inputEncryptedChat(chatId: peer.id.id, accessHash: chat.accessHash) + return Api.InputEncryptedChat.inputEncryptedChat(chatId: peer.id.id._internalGetInt32Value(), accessHash: chat.accessHash) } else { return nil } diff --git a/submodules/TelegramCore/Sources/BotInfo.swift b/submodules/TelegramCore/Sources/ApiUtils/BotInfo.swift similarity index 100% rename from submodules/TelegramCore/Sources/BotInfo.swift rename to submodules/TelegramCore/Sources/ApiUtils/BotInfo.swift diff --git a/submodules/TelegramCore/Sources/CachedChannelParticipants.swift b/submodules/TelegramCore/Sources/ApiUtils/CachedChannelParticipants.swift similarity index 87% rename from submodules/TelegramCore/Sources/CachedChannelParticipants.swift rename to submodules/TelegramCore/Sources/ApiUtils/CachedChannelParticipants.swift index c59f280f4d..8964880bd0 100644 --- a/submodules/TelegramCore/Sources/CachedChannelParticipants.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/CachedChannelParticipants.swift @@ -199,19 +199,19 @@ extension ChannelParticipant { init(apiParticipant: Api.ChannelParticipant) { switch apiParticipant { case let .channelParticipant(userId, date): - self = .member(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: userId), invitedAt: date, adminInfo: nil, banInfo: nil, rank: nil) + self = .member(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt32Value(userId)), invitedAt: date, adminInfo: nil, banInfo: nil, rank: nil) case let .channelParticipantCreator(_, userId, adminRights, rank): - self = .creator(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: userId), adminInfo: ChannelParticipantAdminInfo(rights: TelegramChatAdminRights(apiAdminRights: adminRights) ?? TelegramChatAdminRights(rights: []), promotedBy: PeerId(namespace: Namespaces.Peer.CloudUser, id: userId), canBeEditedByAccountPeer: true), rank: rank) + self = .creator(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt32Value(userId)), adminInfo: ChannelParticipantAdminInfo(rights: TelegramChatAdminRights(apiAdminRights: adminRights) ?? TelegramChatAdminRights(rights: []), promotedBy: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt32Value(userId)), canBeEditedByAccountPeer: true), rank: rank) case let .channelParticipantBanned(flags, userId, restrictedBy, date, bannedRights): let hasLeft = (flags & (1 << 0)) != 0 - let banInfo = ChannelParticipantBannedInfo(rights: TelegramChatBannedRights(apiBannedRights: bannedRights), restrictedBy: PeerId(namespace: Namespaces.Peer.CloudUser, id: restrictedBy), timestamp: date, isMember: !hasLeft) - self = .member(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: userId), invitedAt: date, adminInfo: nil, banInfo: banInfo, rank: nil) + let banInfo = ChannelParticipantBannedInfo(rights: TelegramChatBannedRights(apiBannedRights: bannedRights), restrictedBy: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt32Value(restrictedBy)), timestamp: date, isMember: !hasLeft) + self = .member(id: userId.peerId, invitedAt: date, adminInfo: nil, banInfo: banInfo, rank: nil) case let .channelParticipantAdmin(flags, userId, _, promotedBy, date, adminRights, rank: rank): - self = .member(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: userId), invitedAt: date, adminInfo: ChannelParticipantAdminInfo(rights: TelegramChatAdminRights(apiAdminRights: adminRights) ?? TelegramChatAdminRights(rights: []), promotedBy: PeerId(namespace: Namespaces.Peer.CloudUser, id: promotedBy), canBeEditedByAccountPeer: (flags & (1 << 0)) != 0), banInfo: nil, rank: rank) + self = .member(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt32Value(userId)), invitedAt: date, adminInfo: ChannelParticipantAdminInfo(rights: TelegramChatAdminRights(apiAdminRights: adminRights) ?? TelegramChatAdminRights(rights: []), promotedBy: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt32Value(promotedBy)), canBeEditedByAccountPeer: (flags & (1 << 0)) != 0), banInfo: nil, rank: rank) case let .channelParticipantSelf(userId, _, date): - self = .member(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: userId), invitedAt: date, adminInfo: nil, banInfo: nil, rank: nil) + self = .member(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt32Value(userId)), invitedAt: date, adminInfo: nil, banInfo: nil, rank: nil) case let .channelParticipantLeft(userId): - self = .member(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: userId), invitedAt: 0, adminInfo: nil, banInfo: nil, rank: nil) + self = .member(id: userId.peerId, invitedAt: 0, adminInfo: nil, banInfo: nil, rank: nil) } } } diff --git a/submodules/TelegramCore/Sources/CachedGroupParticipants.swift b/submodules/TelegramCore/Sources/ApiUtils/CachedGroupParticipants.swift similarity index 69% rename from submodules/TelegramCore/Sources/CachedGroupParticipants.swift rename to submodules/TelegramCore/Sources/ApiUtils/CachedGroupParticipants.swift index ceb0b1839c..c536963ca3 100644 --- a/submodules/TelegramCore/Sources/CachedGroupParticipants.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/CachedGroupParticipants.swift @@ -8,11 +8,11 @@ extension GroupParticipant { init(apiParticipant: Api.ChatParticipant) { switch apiParticipant { case let .chatParticipantCreator(userId): - self = .creator(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: userId)) + self = .creator(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt32Value(userId))) case let .chatParticipantAdmin(userId, inviterId, date): - self = .admin(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: userId), invitedBy: PeerId(namespace: Namespaces.Peer.CloudUser, id: inviterId), invitedAt: date) + self = .admin(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt32Value(userId)), invitedBy: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt32Value(inviterId)), invitedAt: date) case let .chatParticipant(userId, inviterId, date): - self = .member(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: userId), invitedBy: PeerId(namespace: Namespaces.Peer.CloudUser, id: inviterId), invitedAt: date) + self = .member(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt32Value(userId)), invitedBy: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt32Value(inviterId)), invitedAt: date) } } } diff --git a/submodules/TelegramCore/Sources/ChatContextResult.swift b/submodules/TelegramCore/Sources/ApiUtils/ChatContextResult.swift similarity index 91% rename from submodules/TelegramCore/Sources/ChatContextResult.swift rename to submodules/TelegramCore/Sources/ApiUtils/ChatContextResult.swift index 1b3069c2db..1a1668cb64 100644 --- a/submodules/TelegramCore/Sources/ChatContextResult.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/ChatContextResult.swift @@ -19,6 +19,7 @@ public enum ChatContextResultMessage: PostboxCoding, Equatable, Codable { case text(text: String, entities: TextEntitiesMessageAttribute?, disableUrlPreview: Bool, replyMarkup: ReplyMarkupMessageAttribute?) case mapLocation(media: TelegramMediaMap, replyMarkup: ReplyMarkupMessageAttribute?) case contact(media: TelegramMediaContact, replyMarkup: ReplyMarkupMessageAttribute?) + case invoice(media: TelegramMediaInvoice, replyMarkup: ReplyMarkupMessageAttribute?) public init(decoder: PostboxDecoder) { switch decoder.decodeInt32ForKey("_v", orElse: 0) { @@ -30,6 +31,8 @@ public enum ChatContextResultMessage: PostboxCoding, Equatable, Codable { self = .mapLocation(media: decoder.decodeObjectForKey("l") as! TelegramMediaMap, replyMarkup: decoder.decodeObjectForKey("m") as? ReplyMarkupMessageAttribute) case 3: self = .contact(media: decoder.decodeObjectForKey("c") as! TelegramMediaContact, replyMarkup: decoder.decodeObjectForKey("m") as? ReplyMarkupMessageAttribute) + case 4: + self = .invoice(media: decoder.decodeObjectForKey("i") as! TelegramMediaInvoice, replyMarkup: decoder.decodeObjectForKey("m") as? ReplyMarkupMessageAttribute) default: self = .auto(caption: "", entities: nil, replyMarkup: nil) } @@ -80,6 +83,14 @@ public enum ChatContextResultMessage: PostboxCoding, Equatable, Codable { } else { encoder.encodeNil(forKey: "m") } + case let .invoice(media: media, replyMarkup): + encoder.encodeInt32(4, forKey: "_v") + encoder.encodeObject(media, forKey: "i") + if let replyMarkup = replyMarkup { + encoder.encodeObject(replyMarkup, forKey: "m") + } else { + encoder.encodeNil(forKey: "m") + } } } @@ -157,6 +168,18 @@ public enum ChatContextResultMessage: PostboxCoding, Equatable, Codable { } else { return false } + case let .invoice(lhsMedia, lhsReplyMarkup): + if case let .invoice(rhsMedia, rhsReplyMarkup) = rhs { + if !lhsMedia.isEqual(to: rhsMedia) { + return false + } + if lhsReplyMarkup != rhsReplyMarkup { + return false + } + return true + } else { + return false + } } } } @@ -444,6 +467,19 @@ extension ChatContextResultMessage { parsedReplyMarkup = ReplyMarkupMessageAttribute(apiMarkup: replyMarkup) } self = .contact(media: media, replyMarkup: parsedReplyMarkup) + case let .botInlineMessageMediaInvoice(flags, title, description, photo, currency, totalAmount, replyMarkup): + var parsedFlags = TelegramMediaInvoiceFlags() + if (flags & (1 << 3)) != 0 { + parsedFlags.insert(.isTest) + } + if (flags & (1 << 1)) != 0 { + parsedFlags.insert(.shippingAddressRequested) + } + var parsedReplyMarkup: ReplyMarkupMessageAttribute? + if let replyMarkup = replyMarkup { + parsedReplyMarkup = ReplyMarkupMessageAttribute(apiMarkup: replyMarkup) + } + self = .invoice(media: TelegramMediaInvoice(title: title, description: description, photo: photo.flatMap(TelegramMediaWebFile.init), receiptMessageId: nil, currency: currency, totalAmount: totalAmount, startParam: "", flags: parsedFlags), replyMarkup: parsedReplyMarkup) } } } diff --git a/submodules/TelegramCore/Sources/CloudFileMediaResource.swift b/submodules/TelegramCore/Sources/ApiUtils/CloudFileMediaResource.swift similarity index 82% rename from submodules/TelegramCore/Sources/CloudFileMediaResource.swift rename to submodules/TelegramCore/Sources/ApiUtils/CloudFileMediaResource.swift index 71a2ac6b6a..57dc51dc09 100644 --- a/submodules/TelegramCore/Sources/CloudFileMediaResource.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/CloudFileMediaResource.swift @@ -34,7 +34,7 @@ extension CloudDocumentSizeMediaResource: TelegramCloudMediaResource, TelegramMu } } -extension CloudPeerPhotoSizeMediaResource: TelegramMultipartFetchableResource { +extension CloudPeerPhotoSizeMediaResource: TelegramMultipartFetchableResource { func apiInputLocation(peerReference: PeerReference) -> Api.InputFileLocation? { let flags: Int32 switch self.sizeSpec { @@ -43,13 +43,21 @@ extension CloudPeerPhotoSizeMediaResource: TelegramMultipartFetchableResource { case .fullSize: flags = 1 << 0 } - return Api.InputFileLocation.inputPeerPhotoFileLocation(flags: flags, peer: peerReference.inputPeer, volumeId: self.volumeId, localId: self.localId) + if let photoId = self.photoId { + return Api.InputFileLocation.inputPeerPhotoFileLocation(flags: flags, peer: peerReference.inputPeer, photoId: photoId) + } else { + return nil + } } } -extension CloudStickerPackThumbnailMediaResource: TelegramMultipartFetchableResource { +extension CloudStickerPackThumbnailMediaResource: TelegramMultipartFetchableResource { func apiInputLocation(packReference: StickerPackReference) -> Api.InputFileLocation? { - return Api.InputFileLocation.inputStickerSetThumb(stickerset: packReference.apiInputStickerSet, volumeId: self.volumeId, localId: self.localId) + if let thumbVersion = self.thumbVersion { + return Api.InputFileLocation.inputStickerSetThumb(stickerset: packReference.apiInputStickerSet, thumbVersion: thumbVersion) + } else { + return nil + } } } diff --git a/submodules/TelegramCore/Sources/CloudMediaResourceParameters.swift b/submodules/TelegramCore/Sources/ApiUtils/CloudMediaResourceParameters.swift similarity index 100% rename from submodules/TelegramCore/Sources/CloudMediaResourceParameters.swift rename to submodules/TelegramCore/Sources/ApiUtils/CloudMediaResourceParameters.swift diff --git a/submodules/TelegramCore/Sources/EncryptedMediaResource.swift b/submodules/TelegramCore/Sources/ApiUtils/EncryptedMediaResource.swift similarity index 100% rename from submodules/TelegramCore/Sources/EncryptedMediaResource.swift rename to submodules/TelegramCore/Sources/ApiUtils/EncryptedMediaResource.swift diff --git a/submodules/TelegramCore/Sources/ExportedInvitation.swift b/submodules/TelegramCore/Sources/ApiUtils/ExportedInvitation.swift similarity index 69% rename from submodules/TelegramCore/Sources/ExportedInvitation.swift rename to submodules/TelegramCore/Sources/ApiUtils/ExportedInvitation.swift index cd4fb5b3fb..48b008b36c 100644 --- a/submodules/TelegramCore/Sources/ExportedInvitation.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/ExportedInvitation.swift @@ -8,7 +8,7 @@ extension ExportedInvitation { init(apiExportedInvite: Api.ExportedChatInvite) { switch apiExportedInvite { case let .chatInviteExported(flags, link, adminId, date, startDate, expireDate, usageLimit, usage): - self = ExportedInvitation(link: link, isPermanent: (flags & (1 << 5)) != 0, isRevoked: (flags & (1 << 0)) != 0, adminId: PeerId(namespace: Namespaces.Peer.CloudUser, id: adminId), date: date, startDate: startDate, expireDate: expireDate, usageLimit: usageLimit, count: usage) + self = ExportedInvitation(link: link, isPermanent: (flags & (1 << 5)) != 0, isRevoked: (flags & (1 << 0)) != 0, adminId: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt32Value(adminId)), date: date, startDate: startDate, expireDate: expireDate, usageLimit: usageLimit, count: usage) } } } diff --git a/submodules/TelegramCore/Sources/ImageRepresentationWithReference.swift b/submodules/TelegramCore/Sources/ApiUtils/ImageRepresentationWithReference.swift similarity index 100% rename from submodules/TelegramCore/Sources/ImageRepresentationWithReference.swift rename to submodules/TelegramCore/Sources/ApiUtils/ImageRepresentationWithReference.swift diff --git a/submodules/TelegramCore/Sources/InstantPage.swift b/submodules/TelegramCore/Sources/ApiUtils/InstantPage.swift similarity index 100% rename from submodules/TelegramCore/Sources/InstantPage.swift rename to submodules/TelegramCore/Sources/ApiUtils/InstantPage.swift diff --git a/submodules/TelegramCore/Sources/MediaResourceApiUtils.swift b/submodules/TelegramCore/Sources/ApiUtils/MediaResourceApiUtils.swift similarity index 100% rename from submodules/TelegramCore/Sources/MediaResourceApiUtils.swift rename to submodules/TelegramCore/Sources/ApiUtils/MediaResourceApiUtils.swift diff --git a/submodules/TelegramCore/Sources/PeerAccessRestrictionInfo.swift b/submodules/TelegramCore/Sources/ApiUtils/PeerAccessRestrictionInfo.swift similarity index 100% rename from submodules/TelegramCore/Sources/PeerAccessRestrictionInfo.swift rename to submodules/TelegramCore/Sources/ApiUtils/PeerAccessRestrictionInfo.swift diff --git a/submodules/TelegramCore/Sources/CachedChannelData.swift b/submodules/TelegramCore/Sources/ApiUtils/PeerGeoLocation.swift similarity index 100% rename from submodules/TelegramCore/Sources/CachedChannelData.swift rename to submodules/TelegramCore/Sources/ApiUtils/PeerGeoLocation.swift diff --git a/submodules/TelegramCore/Sources/ReactionsMessageAttribute.swift b/submodules/TelegramCore/Sources/ApiUtils/ReactionsMessageAttribute.swift similarity index 100% rename from submodules/TelegramCore/Sources/ReactionsMessageAttribute.swift rename to submodules/TelegramCore/Sources/ApiUtils/ReactionsMessageAttribute.swift diff --git a/submodules/TelegramCore/Sources/RemoteStorageConfiguration.swift b/submodules/TelegramCore/Sources/ApiUtils/RemoteStorageConfiguration.swift similarity index 88% rename from submodules/TelegramCore/Sources/RemoteStorageConfiguration.swift rename to submodules/TelegramCore/Sources/ApiUtils/RemoteStorageConfiguration.swift index 174478236d..90a45f895d 100644 --- a/submodules/TelegramCore/Sources/RemoteStorageConfiguration.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/RemoteStorageConfiguration.swift @@ -4,7 +4,7 @@ import SwiftSignalKit import SyncCore -public func currentWebDocumentsHostDatacenterId(postbox: Postbox, isTestingEnvironment: Bool) -> Signal { +func currentWebDocumentsHostDatacenterId(postbox: Postbox, isTestingEnvironment: Bool) -> Signal { return postbox.transaction { transaction -> Int32 in if let entry = transaction.getPreferencesEntry(key: PreferencesKeys.remoteStorageConfiguration) as? RemoteStorageConfiguration { return entry.webDocumentsHostDatacenterId diff --git a/submodules/TelegramCore/Sources/ReplyMarkupMessageAttribute.swift b/submodules/TelegramCore/Sources/ApiUtils/ReplyMarkupMessageAttribute.swift similarity index 91% rename from submodules/TelegramCore/Sources/ReplyMarkupMessageAttribute.swift rename to submodules/TelegramCore/Sources/ApiUtils/ReplyMarkupMessageAttribute.swift index 3f42956000..02917a8b14 100644 --- a/submodules/TelegramCore/Sources/ReplyMarkupMessageAttribute.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/ReplyMarkupMessageAttribute.swift @@ -56,8 +56,9 @@ extension ReplyMarkupMessageAttribute { convenience init(apiMarkup: Api.ReplyMarkup) { var rows: [ReplyMarkupRow] = [] var flags = ReplyMarkupMessageFlags() + var placeholder: String? switch apiMarkup { - case let .replyKeyboardMarkup(markupFlags, apiRows): + case let .replyKeyboardMarkup(markupFlags, apiRows, apiPlaceholder): rows = apiRows.map { ReplyMarkupRow(apiRow: $0) } if (markupFlags & (1 << 0)) != 0 { flags.insert(.fit) @@ -68,10 +69,11 @@ extension ReplyMarkupMessageAttribute { if (markupFlags & (1 << 2)) != 0 { flags.insert(.personal) } + placeholder = apiPlaceholder case let .replyInlineMarkup(apiRows): rows = apiRows.map { ReplyMarkupRow(apiRow: $0) } flags.insert(.inline) - case let .replyKeyboardForceReply(forceReplyFlags): + case let .replyKeyboardForceReply(forceReplyFlags, apiPlaceholder): if (forceReplyFlags & (1 << 1)) != 0 { flags.insert(.once) } @@ -79,11 +81,12 @@ extension ReplyMarkupMessageAttribute { flags.insert(.personal) } flags.insert(.setupReply) + placeholder = apiPlaceholder case let .replyKeyboardHide(hideFlags): if (hideFlags & (1 << 2)) != 0 { flags.insert(.personal) } } - self.init(rows: rows, flags: flags) + self.init(rows: rows, flags: flags, placeholder: placeholder) } } diff --git a/submodules/TelegramCore/Sources/RichText.swift b/submodules/TelegramCore/Sources/ApiUtils/RichText.swift similarity index 100% rename from submodules/TelegramCore/Sources/RichText.swift rename to submodules/TelegramCore/Sources/ApiUtils/RichText.swift diff --git a/submodules/TelegramCore/Sources/StoreMessage_Telegram.swift b/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift similarity index 96% rename from submodules/TelegramCore/Sources/StoreMessage_Telegram.swift rename to submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift index bb7bea2dc2..19efa3f4c5 100644 --- a/submodules/TelegramCore/Sources/StoreMessage_Telegram.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift @@ -157,14 +157,14 @@ func apiMessagePeerIds(_ message: Api.Message) -> [PeerId] { } if let viaBotId = viaBotId { - result.append(PeerId(namespace: Namespaces.Peer.CloudUser, id: viaBotId)) + result.append(PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt32Value(viaBotId))) } if let media = media { switch media { case let .messageMediaContact(_, _, _, _, userId): if userId != 0 { - result.append(PeerId(namespace: Namespaces.Peer.CloudUser, id: userId)) + result.append(PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt32Value(userId))) } default: break @@ -175,7 +175,7 @@ func apiMessagePeerIds(_ message: Api.Message) -> [PeerId] { for entity in entities { switch entity { case let .messageEntityMentionName(_, _, userId): - result.append(PeerId(namespace: Namespaces.Peer.CloudUser, id: userId)) + result.append(PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt32Value(userId))) default: break } @@ -196,30 +196,30 @@ func apiMessagePeerIds(_ message: Api.Message) -> [PeerId] { } switch action { - case .messageActionChannelCreate, .messageActionChatDeletePhoto, .messageActionChatEditPhoto, .messageActionChatEditTitle, .messageActionEmpty, .messageActionPinMessage, .messageActionHistoryClear, .messageActionGameScore, .messageActionPaymentSent, .messageActionPaymentSentMe, .messageActionPhoneCall, .messageActionScreenshotTaken, .messageActionCustomAction, .messageActionBotAllowed, .messageActionSecureValuesSent, .messageActionSecureValuesSentMe, .messageActionContactSignUp, .messageActionGroupCall, .messageActionSetMessagesTTL: + case .messageActionChannelCreate, .messageActionChatDeletePhoto, .messageActionChatEditPhoto, .messageActionChatEditTitle, .messageActionEmpty, .messageActionPinMessage, .messageActionHistoryClear, .messageActionGameScore, .messageActionPaymentSent, .messageActionPaymentSentMe, .messageActionPhoneCall, .messageActionScreenshotTaken, .messageActionCustomAction, .messageActionBotAllowed, .messageActionSecureValuesSent, .messageActionSecureValuesSentMe, .messageActionContactSignUp, .messageActionGroupCall, .messageActionSetMessagesTTL, .messageActionGroupCallScheduled: break case let .messageActionChannelMigrateFrom(_, chatId): - result.append(PeerId(namespace: Namespaces.Peer.CloudGroup, id: chatId)) + result.append(PeerId(namespace: Namespaces.Peer.CloudGroup, id: PeerId.Id._internalFromInt32Value(chatId))) case let .messageActionChatAddUser(users): for id in users { - result.append(PeerId(namespace: Namespaces.Peer.CloudUser, id: id)) + result.append(PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt32Value(id))) } case let .messageActionChatCreate(_, users): for id in users { - result.append(PeerId(namespace: Namespaces.Peer.CloudUser, id: id)) + result.append(PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt32Value(id))) } case let .messageActionChatDeleteUser(userId): - result.append(PeerId(namespace: Namespaces.Peer.CloudUser, id: userId)) + result.append(PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt32Value(userId))) case let .messageActionChatJoinedByLink(inviterId): - result.append(PeerId(namespace: Namespaces.Peer.CloudUser, id: inviterId)) + result.append(PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt32Value(inviterId))) case let .messageActionChatMigrateTo(channelId): - result.append(PeerId(namespace: Namespaces.Peer.CloudChannel, id: channelId)) + result.append(PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt32Value(channelId))) case let .messageActionGeoProximityReached(fromId, toId, _): result.append(fromId.peerId) result.append(toId.peerId) case let .messageActionInviteToGroupCall(_, userIds): for id in userIds { - result.append(PeerId(namespace: Namespaces.Peer.CloudUser, id: id)) + result.append(PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt32Value(id))) } } @@ -263,7 +263,7 @@ func textMediaAndExpirationTimerFromApiMedia(_ media: Api.MessageMedia?, _ peerI return (TelegramMediaExpiredContent(data: .image), nil) } case let .messageMediaContact(phoneNumber, firstName, lastName, vcard, userId): - let contactPeerId: PeerId? = userId == 0 ? nil : PeerId(namespace: Namespaces.Peer.CloudUser, id: userId) + let contactPeerId: PeerId? = userId == 0 ? nil : PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt32Value(userId)) let mediaContact = TelegramMediaContact(firstName: firstName, lastName: lastName, phoneNumber: phoneNumber, peerId: contactPeerId, vCardData: vcard.isEmpty ? nil : vcard) return (mediaContact, nil) case let .messageMediaGeo(geo): @@ -354,7 +354,7 @@ func messageTextEntitiesFromApiEntities(_ entities: [Api.MessageEntity]) -> [Mes case let .messageEntityTextUrl(offset, length, url): result.append(MessageTextEntity(range: Int(offset) ..< Int(offset + length), type: .TextUrl(url: url))) case let .messageEntityMentionName(offset, length, userId): - result.append(MessageTextEntity(range: Int(offset) ..< Int(offset + length), type: .TextMention(peerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: userId)))) + result.append(MessageTextEntity(range: Int(offset) ..< Int(offset + length), type: .TextMention(peerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt32Value(userId))))) case let .messageEntityPhone(offset, length): result.append(MessageTextEntity(range: Int(offset) ..< Int(offset + length), type: .PhoneNumber)) case let .messageEntityCashtag(offset, length): @@ -385,10 +385,10 @@ extension StoreMessage { peerId = chatPeerId.peerId authorId = resolvedFromId case let .peerChat(chatId): - peerId = PeerId(namespace: Namespaces.Peer.CloudGroup, id: chatId) + peerId = PeerId(namespace: Namespaces.Peer.CloudGroup, id: PeerId.Id._internalFromInt32Value(chatId)) authorId = resolvedFromId case let .peerChannel(channelId): - peerId = PeerId(namespace: Namespaces.Peer.CloudChannel, id: channelId) + peerId = PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt32Value(channelId)) authorId = resolvedFromId } @@ -444,15 +444,7 @@ extension StoreMessage { } if let savedFromPeer = savedFromPeer, let savedFromMsgId = savedFromMsgId { - let peerId: PeerId - switch savedFromPeer { - case let .peerChannel(channelId): - peerId = PeerId(namespace: Namespaces.Peer.CloudChannel, id: channelId) - case let .peerChat(chatId): - peerId = PeerId(namespace: Namespaces.Peer.CloudGroup, id: chatId) - case let .peerUser(userId): - peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: userId) - } + let peerId: PeerId = savedFromPeer.peerId let messageId: MessageId = MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: savedFromMsgId) attributes.append(SourceReferenceMessageAttribute(messageId: messageId)) } @@ -510,7 +502,7 @@ extension StoreMessage { } if let viaBotId = viaBotId { - attributes.append(InlineBotMessageAttribute(peerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: viaBotId), title: nil)) + attributes.append(InlineBotMessageAttribute(peerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt32Value(viaBotId)), title: nil)) } if namespace != Namespaces.Message.ScheduledCloud { @@ -569,7 +561,7 @@ extension StoreMessage { recentRepliersPeerIds = nil } - let commentsPeerId = channelId.flatMap { PeerId(namespace: Namespaces.Peer.CloudChannel, id: $0) } + let commentsPeerId = channelId.flatMap { PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt32Value($0)) } attributes.append(ReplyThreadMessageAttribute(count: repliesCount, latestUsers: recentRepliersPeerIds ?? [], commentsPeerId: commentsPeerId, maxMessageId: maxId, maxReadMessageId: readMaxId)) } diff --git a/submodules/TelegramCore/Sources/TelegramChannel.swift b/submodules/TelegramCore/Sources/ApiUtils/TelegramChannel.swift similarity index 100% rename from submodules/TelegramCore/Sources/TelegramChannel.swift rename to submodules/TelegramCore/Sources/ApiUtils/TelegramChannel.swift diff --git a/submodules/TelegramCore/Sources/TelegramChannelAdminRights.swift b/submodules/TelegramCore/Sources/ApiUtils/TelegramChannelAdminRights.swift similarity index 100% rename from submodules/TelegramCore/Sources/TelegramChannelAdminRights.swift rename to submodules/TelegramCore/Sources/ApiUtils/TelegramChannelAdminRights.swift diff --git a/submodules/TelegramCore/Sources/TelegramChannelBannedRights.swift b/submodules/TelegramCore/Sources/ApiUtils/TelegramChannelBannedRights.swift similarity index 100% rename from submodules/TelegramCore/Sources/TelegramChannelBannedRights.swift rename to submodules/TelegramCore/Sources/ApiUtils/TelegramChannelBannedRights.swift diff --git a/submodules/TelegramCore/Sources/TelegramGroup.swift b/submodules/TelegramCore/Sources/ApiUtils/TelegramGroup.swift similarity index 100% rename from submodules/TelegramCore/Sources/TelegramGroup.swift rename to submodules/TelegramCore/Sources/ApiUtils/TelegramGroup.swift diff --git a/submodules/TelegramCore/Sources/TelegramMediaAction.swift b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift similarity index 88% rename from submodules/TelegramCore/Sources/TelegramMediaAction.swift rename to submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift index 8e93f430ac..d21d94f107 100644 --- a/submodules/TelegramCore/Sources/TelegramMediaAction.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift @@ -9,23 +9,23 @@ func telegramMediaActionFromApiAction(_ action: Api.MessageAction) -> TelegramMe case let .messageActionChannelCreate(title): return TelegramMediaAction(action: .groupCreated(title: title)) case let .messageActionChannelMigrateFrom(title, chatId): - return TelegramMediaAction(action: .channelMigratedFromGroup(title: title, groupId: PeerId(namespace: Namespaces.Peer.CloudGroup, id: chatId))) + return TelegramMediaAction(action: .channelMigratedFromGroup(title: title, groupId: PeerId(namespace: Namespaces.Peer.CloudGroup, id: PeerId.Id._internalFromInt32Value(chatId)))) case let .messageActionChatAddUser(users): - return TelegramMediaAction(action: .addedMembers(peerIds: users.map({ PeerId(namespace: Namespaces.Peer.CloudUser, id: $0) }))) + return TelegramMediaAction(action: .addedMembers(peerIds: users.map({ PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt32Value($0)) }))) case let .messageActionChatCreate(title, _): return TelegramMediaAction(action: .groupCreated(title: title)) case .messageActionChatDeletePhoto: return TelegramMediaAction(action: .photoUpdated(image: nil)) case let .messageActionChatDeleteUser(userId): - return TelegramMediaAction(action: .removedMembers(peerIds: [PeerId(namespace: Namespaces.Peer.CloudUser, id: userId)])) + return TelegramMediaAction(action: .removedMembers(peerIds: [PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt32Value(userId))])) case let .messageActionChatEditPhoto(photo): return TelegramMediaAction(action: .photoUpdated(image: telegramMediaImageFromApiPhoto(photo))) case let .messageActionChatEditTitle(title): return TelegramMediaAction(action: .titleUpdated(title: title)) case let .messageActionChatJoinedByLink(inviterId): - return TelegramMediaAction(action: .joinedByLink(inviter: PeerId(namespace: Namespaces.Peer.CloudUser, id: inviterId))) + return TelegramMediaAction(action: .joinedByLink(inviter: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt32Value(inviterId)))) case let .messageActionChatMigrateTo(channelId): - return TelegramMediaAction(action: .groupMigratedToChannel(channelId: PeerId(namespace: Namespaces.Peer.CloudChannel, id: channelId))) + return TelegramMediaAction(action: .groupMigratedToChannel(channelId: PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt32Value(channelId)))) case .messageActionHistoryClear: return TelegramMediaAction(action: .historyCleared) case .messageActionPinMessage: @@ -62,17 +62,22 @@ func telegramMediaActionFromApiAction(_ action: Api.MessageAction) -> TelegramMe case let .messageActionGroupCall(_, call, duration): switch call { case let .inputGroupCall(id, accessHash): - return TelegramMediaAction(action: .groupPhoneCall(callId: id, accessHash: accessHash, duration: duration)) + return TelegramMediaAction(action: .groupPhoneCall(callId: id, accessHash: accessHash, scheduleDate: nil, duration: duration)) } case let .messageActionInviteToGroupCall(call, userIds): switch call { case let .inputGroupCall(id, accessHash): return TelegramMediaAction(action: .inviteToGroupPhoneCall(callId: id, accessHash: accessHash, peerIds: userIds.map { userId in - PeerId(namespace: Namespaces.Peer.CloudUser, id: userId) + PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt32Value(userId)) })) } case let .messageActionSetMessagesTTL(period): return TelegramMediaAction(action: .messageAutoremoveTimeoutUpdated(period)) + case let .messageActionGroupCallScheduled(call, scheduleDate): + switch call { + case let .inputGroupCall(id, accessHash): + return TelegramMediaAction(action: .groupPhoneCall(callId: id, accessHash: accessHash, scheduleDate: scheduleDate, duration: nil)) + } } } diff --git a/submodules/TelegramCore/Sources/TelegramMediaFile.swift b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaFile.swift similarity index 75% rename from submodules/TelegramCore/Sources/TelegramMediaFile.swift rename to submodules/TelegramCore/Sources/ApiUtils/TelegramMediaFile.swift index d75d737956..02a0afa129 100644 --- a/submodules/TelegramCore/Sources/TelegramMediaFile.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaFile.swift @@ -125,24 +125,15 @@ func telegramMediaFileThumbnailRepresentationsFromApiSizes(datacenterId: Int32, var representations: [TelegramMediaImageRepresentation] = [] for size in sizes { switch size { - case let .photoCachedSize(type, location, w, h, _): - switch location { - case let .fileLocationToBeDeprecated(volumeId, localId): - let resource = CloudDocumentSizeMediaResource(datacenterId: datacenterId, documentId: documentId, accessHash: accessHash, sizeSpec: type, volumeId: volumeId, localId: localId, fileReference: fileReference) - representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: resource, progressiveSizes: [])) - } - case let .photoSize(type, location, w, h, _): - switch location { - case let .fileLocationToBeDeprecated(volumeId, localId): - let resource = CloudDocumentSizeMediaResource(datacenterId: datacenterId, documentId: documentId, accessHash: accessHash, sizeSpec: type, volumeId: volumeId, localId: localId, fileReference: fileReference) - representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: resource, progressiveSizes: [])) - } - case let .photoSizeProgressive(type, location, w, h, sizes): - switch location { - case let .fileLocationToBeDeprecated(volumeId, localId): - let resource = CloudDocumentSizeMediaResource(datacenterId: datacenterId, documentId: documentId, accessHash: accessHash, sizeSpec: type, volumeId: volumeId, localId: localId, fileReference: fileReference) - representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: resource, progressiveSizes: sizes)) - } + case let .photoCachedSize(type, w, h, _): + let resource = CloudDocumentSizeMediaResource(datacenterId: datacenterId, documentId: documentId, accessHash: accessHash, sizeSpec: type, fileReference: fileReference) + representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: resource, progressiveSizes: [], immediateThumbnailData: nil)) + case let .photoSize(type, w, h, _): + let resource = CloudDocumentSizeMediaResource(datacenterId: datacenterId, documentId: documentId, accessHash: accessHash, sizeSpec: type, fileReference: fileReference) + representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: resource, progressiveSizes: [], immediateThumbnailData: nil)) + case let .photoSizeProgressive(type, w, h, sizes): + let resource = CloudDocumentSizeMediaResource(datacenterId: datacenterId, documentId: documentId, accessHash: accessHash, sizeSpec: type, fileReference: fileReference) + representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: resource, progressiveSizes: sizes, immediateThumbnailData: nil)) case let .photoPathSize(_, data): immediateThumbnailData = data.makeData() case let .photoStrippedSize(_, data): @@ -166,12 +157,9 @@ func telegramMediaFileFromApiDocument(_ document: Api.Document) -> TelegramMedia if let videoThumbs = videoThumbs { for thumb in videoThumbs { switch thumb { - case let .videoSize(_, type, location, w, h, _, _): + case let .videoSize(_, type, w, h, _, _): let resource: TelegramMediaResource - switch location { - case let .fileLocationToBeDeprecated(volumeId, localId): - resource = CloudDocumentSizeMediaResource(datacenterId: dcId, documentId: id, accessHash: accessHash, sizeSpec: type, volumeId: volumeId, localId: localId, fileReference: fileReference.makeData()) - } + resource = CloudDocumentSizeMediaResource(datacenterId: dcId, documentId: id, accessHash: accessHash, sizeSpec: type, fileReference: fileReference.makeData()) videoThumbnails.append(TelegramMediaFile.VideoThumbnail( dimensions: PixelDimensions(width: w, height: h), diff --git a/submodules/TelegramCore/Sources/TelegramMediaGame.swift b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaGame.swift similarity index 100% rename from submodules/TelegramCore/Sources/TelegramMediaGame.swift rename to submodules/TelegramCore/Sources/ApiUtils/TelegramMediaGame.swift diff --git a/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaImage.swift b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaImage.swift new file mode 100644 index 0000000000..f0bbd3e317 --- /dev/null +++ b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaImage.swift @@ -0,0 +1,61 @@ +import Foundation +import Postbox +import TelegramApi + +import SyncCore + +func telegramMediaImageRepresentationsFromApiSizes(datacenterId: Int32, photoId: Int64, accessHash: Int64, fileReference: Data?, sizes: [Api.PhotoSize]) -> (immediateThumbnail: Data?, representations: [TelegramMediaImageRepresentation]) { + var immediateThumbnailData: Data? + var representations: [TelegramMediaImageRepresentation] = [] + for size in sizes { + switch size { + case let .photoCachedSize(type, w, h, _): + let resource = CloudPhotoSizeMediaResource(datacenterId: datacenterId, photoId: photoId, accessHash: accessHash, sizeSpec: type, size: nil, fileReference: fileReference) + representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: resource, progressiveSizes: [], immediateThumbnailData: nil)) + case let .photoSize(type, w, h, size): + let resource = CloudPhotoSizeMediaResource(datacenterId: datacenterId, photoId: photoId, accessHash: accessHash, sizeSpec: type, size: Int(size), fileReference: fileReference) + representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: resource, progressiveSizes: [], immediateThumbnailData: nil)) + case let .photoSizeProgressive(type, w, h, sizes): + if !sizes.isEmpty { + let resource = CloudPhotoSizeMediaResource(datacenterId: datacenterId, photoId: photoId, accessHash: accessHash, sizeSpec: type, size: Int(sizes[sizes.count - 1]), fileReference: fileReference) + representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: resource, progressiveSizes: sizes, immediateThumbnailData: nil)) + } + case let .photoStrippedSize(_, data): + immediateThumbnailData = data.makeData() + case .photoPathSize: + break + case .photoSizeEmpty: + break + } + } + return (immediateThumbnailData, representations) +} + +func telegramMediaImageFromApiPhoto(_ photo: Api.Photo) -> TelegramMediaImage? { + switch photo { + case let .photo(flags, id, accessHash, fileReference, _, sizes, videoSizes, dcId): + let (immediateThumbnailData, representations) = telegramMediaImageRepresentationsFromApiSizes(datacenterId: dcId, photoId: id, accessHash: accessHash, fileReference: fileReference.makeData(), sizes: sizes) + var imageFlags: TelegramMediaImageFlags = [] + let hasStickers = (flags & (1 << 0)) != 0 + if hasStickers { + imageFlags.insert(.hasStickers) + } + + var videoRepresentations: [TelegramMediaImage.VideoRepresentation] = [] + if let videoSizes = videoSizes { + for size in videoSizes { + switch size { + case let .videoSize(_, type, w, h, size, videoStartTs): + let resource: TelegramMediaResource + resource = CloudPhotoSizeMediaResource(datacenterId: dcId, photoId: id, accessHash: accessHash, sizeSpec: type, size: Int(size), fileReference: fileReference.makeData()) + + videoRepresentations.append(TelegramMediaImage.VideoRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: resource, startTimestamp: videoStartTs)) + } + } + } + + return TelegramMediaImage(imageId: MediaId(namespace: Namespaces.Media.CloudImage, id: id), representations: representations, videoRepresentations: videoRepresentations, 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/TelegramMediaMap.swift b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaMap.swift similarity index 100% rename from submodules/TelegramCore/Sources/TelegramMediaMap.swift rename to submodules/TelegramCore/Sources/ApiUtils/TelegramMediaMap.swift diff --git a/submodules/TelegramCore/Sources/TelegramMediaPoll.swift b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaPoll.swift similarity index 95% rename from submodules/TelegramCore/Sources/TelegramMediaPoll.swift rename to submodules/TelegramCore/Sources/ApiUtils/TelegramMediaPoll.swift index 34c84ec378..2d45433129 100644 --- a/submodules/TelegramCore/Sources/TelegramMediaPoll.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaPoll.swift @@ -36,7 +36,7 @@ extension TelegramMediaPollResults { } 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) } + return recentVoters.map { PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt32Value($0)) } } ?? [], solution: parsedSolution) } } diff --git a/submodules/TelegramCore/Sources/TelegramMediaWebDocument.swift b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaWebDocument.swift similarity index 100% rename from submodules/TelegramCore/Sources/TelegramMediaWebDocument.swift rename to submodules/TelegramCore/Sources/ApiUtils/TelegramMediaWebDocument.swift diff --git a/submodules/TelegramCore/Sources/TelegramMediaWebFile.swift b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaWebFile.swift similarity index 100% rename from submodules/TelegramCore/Sources/TelegramMediaWebFile.swift rename to submodules/TelegramCore/Sources/ApiUtils/TelegramMediaWebFile.swift diff --git a/submodules/TelegramCore/Sources/TelegramMediaWebpage.swift b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaWebpage.swift similarity index 100% rename from submodules/TelegramCore/Sources/TelegramMediaWebpage.swift rename to submodules/TelegramCore/Sources/ApiUtils/TelegramMediaWebpage.swift diff --git a/submodules/TelegramCore/Sources/TelegramPeerNotificationSettings.swift b/submodules/TelegramCore/Sources/ApiUtils/TelegramPeerNotificationSettings.swift similarity index 100% rename from submodules/TelegramCore/Sources/TelegramPeerNotificationSettings.swift rename to submodules/TelegramCore/Sources/ApiUtils/TelegramPeerNotificationSettings.swift diff --git a/submodules/TelegramCore/Sources/TelegramUser.swift b/submodules/TelegramCore/Sources/ApiUtils/TelegramUser.swift similarity index 88% rename from submodules/TelegramCore/Sources/TelegramUser.swift rename to submodules/TelegramCore/Sources/ApiUtils/TelegramUser.swift index c4225bb235..83cbbca4f8 100644 --- a/submodules/TelegramCore/Sources/TelegramUser.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/TelegramUser.swift @@ -7,21 +7,17 @@ import SyncCore func parsedTelegramProfilePhoto(_ photo: Api.UserProfilePhoto) -> [TelegramMediaImageRepresentation] { var representations: [TelegramMediaImageRepresentation] = [] switch photo { - case let .userProfilePhoto(flags, _, photoSmall, photoBig, dcId): + case let .userProfilePhoto(flags, id, strippedThumb, dcId): let _ = (flags & (1 << 0)) != 0 let smallResource: TelegramMediaResource let fullSizeResource: TelegramMediaResource - switch photoSmall { - case let .fileLocationToBeDeprecated(volumeId, localId): - smallResource = CloudPeerPhotoSizeMediaResource(datacenterId: dcId, sizeSpec: .small, volumeId: volumeId, localId: localId) - } - switch photoBig { - case let .fileLocationToBeDeprecated(volumeId, localId): - fullSizeResource = CloudPeerPhotoSizeMediaResource(datacenterId: dcId, sizeSpec: .fullSize, volumeId: volumeId, localId: localId) - } - representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 80, height: 80), resource: smallResource, progressiveSizes: [])) - representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 640, height: 640), resource: fullSizeResource, progressiveSizes: [])) + + smallResource = CloudPeerPhotoSizeMediaResource(datacenterId: dcId, photoId: id, sizeSpec: .small, volumeId: nil, localId: nil) + fullSizeResource = CloudPeerPhotoSizeMediaResource(datacenterId: dcId, photoId: id, sizeSpec: .fullSize, volumeId: nil, localId: nil) + + representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 80, height: 80), resource: smallResource, progressiveSizes: [], immediateThumbnailData: strippedThumb?.makeData())) + representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 640, height: 640), resource: fullSizeResource, progressiveSizes: [], immediateThumbnailData: strippedThumb?.makeData())) case .userProfilePhotoEmpty: break } @@ -74,9 +70,9 @@ extension TelegramUser { let restrictionInfo: PeerAccessRestrictionInfo? = restrictionReason.flatMap(PeerAccessRestrictionInfo.init(apiReasons:)) - self.init(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: id), accessHash: accessHashValue, firstName: firstName, lastName: lastName, username: username, phone: phone, photo: representations, botInfo: botInfo, restrictionInfo: restrictionInfo, flags: userFlags) + self.init(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt32Value(id)), accessHash: accessHashValue, firstName: firstName, lastName: lastName, username: username, phone: phone, photo: representations, botInfo: botInfo, restrictionInfo: restrictionInfo, flags: userFlags) case let .userEmpty(id): - self.init(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: id), accessHash: nil, firstName: nil, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: []) + self.init(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt32Value(id)), accessHash: nil, firstName: nil, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: []) } } diff --git a/submodules/TelegramCore/Sources/TelegramUserPresence.swift b/submodules/TelegramCore/Sources/ApiUtils/TelegramUserPresence.swift similarity index 100% rename from submodules/TelegramCore/Sources/TelegramUserPresence.swift rename to submodules/TelegramCore/Sources/ApiUtils/TelegramUserPresence.swift diff --git a/submodules/TelegramCore/Sources/TextEntitiesMessageAttribute.swift b/submodules/TelegramCore/Sources/ApiUtils/TextEntitiesMessageAttribute.swift similarity index 100% rename from submodules/TelegramCore/Sources/TextEntitiesMessageAttribute.swift rename to submodules/TelegramCore/Sources/ApiUtils/TextEntitiesMessageAttribute.swift diff --git a/submodules/TelegramCore/Sources/Theme.swift b/submodules/TelegramCore/Sources/ApiUtils/Theme.swift similarity index 100% rename from submodules/TelegramCore/Sources/Theme.swift rename to submodules/TelegramCore/Sources/ApiUtils/Theme.swift diff --git a/submodules/TelegramCore/Sources/ApiUtils/Wallpaper.swift b/submodules/TelegramCore/Sources/ApiUtils/Wallpaper.swift new file mode 100644 index 0000000000..d4fab6e8c0 --- /dev/null +++ b/submodules/TelegramCore/Sources/ApiUtils/Wallpaper.swift @@ -0,0 +1,113 @@ +import Foundation +import Postbox +import SwiftSignalKit +import TelegramApi + +import SyncCore + +extension WallpaperSettings { + init(apiWallpaperSettings: Api.WallPaperSettings) { + switch apiWallpaperSettings { + case let .wallPaperSettings(flags, backgroundColor, secondBackgroundColor, thirdBackgroundColor, fourthBackgroundColor, intensity, rotation): + var colors: [UInt32] = [] + if let backgroundColor = backgroundColor { + colors.append(UInt32(bitPattern: backgroundColor)) + } + if let secondBackgroundColor = secondBackgroundColor { + colors.append(UInt32(bitPattern: secondBackgroundColor)) + } + if let thirdBackgroundColor = thirdBackgroundColor { + colors.append(UInt32(bitPattern: thirdBackgroundColor)) + } + if let fourthBackgroundColor = fourthBackgroundColor { + colors.append(UInt32(bitPattern: fourthBackgroundColor)) + } + self = WallpaperSettings(blur: (flags & 1 << 1) != 0, motion: (flags & 1 << 2) != 0, colors: colors, intensity: intensity, rotation: rotation) + } + } +} + +func apiWallpaperSettings(_ wallpaperSettings: WallpaperSettings) -> Api.WallPaperSettings { + var flags: Int32 = 0 + var backgroundColor: Int32? + if wallpaperSettings.colors.count >= 1 { + flags |= (1 << 0) + backgroundColor = Int32(bitPattern: wallpaperSettings.colors[0]) + } + if wallpaperSettings.blur { + flags |= (1 << 1) + } + if wallpaperSettings.motion { + flags |= (1 << 2) + } + if let _ = wallpaperSettings.intensity { + flags |= (1 << 3) + } + var secondBackgroundColor: Int32? + if wallpaperSettings.colors.count >= 2 { + flags |= (1 << 4) + secondBackgroundColor = Int32(bitPattern: wallpaperSettings.colors[1]) + } + var thirdBackgroundColor: Int32? + if wallpaperSettings.colors.count >= 3 { + flags |= (1 << 5) + thirdBackgroundColor = Int32(bitPattern: wallpaperSettings.colors[2]) + } + var fourthBackgroundColor: Int32? + if wallpaperSettings.colors.count >= 4 { + flags |= (1 << 6) + fourthBackgroundColor = Int32(bitPattern: wallpaperSettings.colors[3]) + } + return .wallPaperSettings(flags: flags, backgroundColor: backgroundColor, secondBackgroundColor: secondBackgroundColor, thirdBackgroundColor: thirdBackgroundColor, fourthBackgroundColor: fourthBackgroundColor, intensity: wallpaperSettings.intensity, rotation: wallpaperSettings.rotation ?? 0) +} + +extension TelegramWallpaper { + init(apiWallpaper: Api.WallPaper) { + switch apiWallpaper { + case let .wallPaper(id, flags, accessHash, slug, document, settings): + if let file = telegramMediaFileFromApiDocument(document) { + let wallpaperSettings: WallpaperSettings + if let settings = settings { + wallpaperSettings = WallpaperSettings(apiWallpaperSettings: settings) + } else { + wallpaperSettings = WallpaperSettings() + } + 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() + self = .color(0xffffff) + } + case let .wallPaperNoFile(id, _, settings): + if let settings = settings, case let .wallPaperSettings(_, backgroundColor, secondBackgroundColor, thirdBackgroundColor, fourthBackgroundColor, _, rotation) = settings { + let colors: [UInt32] = ([backgroundColor, secondBackgroundColor, thirdBackgroundColor, fourthBackgroundColor] as [Int32?]).compactMap({ color -> UInt32? in + return color.flatMap(UInt32.init(bitPattern:)) + }) + if colors.count > 1 { + self = .gradient(id, colors, WallpaperSettings(rotation: rotation)) + } else if colors.count == 1 { + self = .color(UInt32(bitPattern: colors[0])) + } else { + self = .color(0xffffff) + } + } else { + self = .color(0xffffff) + } + + } + } + + var apiInputWallpaperAndSettings: (Api.InputWallPaper?, Api.WallPaperSettings)? { + switch self { + case .builtin: + return nil + case let .file(_, _, _, _, _, _, slug, _, settings): + return (.inputWallPaperSlug(slug: slug), apiWallpaperSettings(settings)) + case let .color(color): + return (.inputWallPaperNoFile(id: 0), apiWallpaperSettings(WallpaperSettings(colors: [color]))) + case let .gradient(id, colors, settings): + return (.inputWallPaperNoFile(id: id ?? 0), apiWallpaperSettings(WallpaperSettings(colors: colors, rotation: settings.rotation))) + default: + return nil + } + } +} diff --git a/submodules/TelegramCore/Sources/Authorization.swift b/submodules/TelegramCore/Sources/Authorization.swift index ed6ba8e60a..ce58de6028 100644 --- a/submodules/TelegramCore/Sources/Authorization.swift +++ b/submodules/TelegramCore/Sources/Authorization.swift @@ -31,6 +31,44 @@ func switchToAuthorizedAccount(transaction: AccountManagerModifier, account: Una transaction.removeAuth() } +private struct Regex { + let pattern: String + let options: NSRegularExpression.Options! + + private var matcher: NSRegularExpression { + return try! NSRegularExpression(pattern: self.pattern, options: self.options) + } + + init(_ pattern: String) { + self.pattern = pattern + self.options = [] + } + + func match(_ string: String, options: NSRegularExpression.MatchingOptions = []) -> Bool { + return self.matcher.numberOfMatches(in: string, options: options, range: NSMakeRange(0, string.utf16.count)) != 0 + } +} + +private protocol RegularExpressionMatchable { + func match(_ regex: Regex) -> Bool +} + +private struct MatchString: RegularExpressionMatchable { + private let string: String + + init(_ string: String) { + self.string = string + } + + func match(_ regex: Regex) -> Bool { + return regex.match(self.string) + } +} + +private func ~=(pattern: Regex, matchable: T) -> Bool { + return matchable.match(pattern) +} + public func sendAuthorizationCode(accountManager: AccountManager, account: UnauthorizedAccount, phoneNumber: String, apiId: Int32, apiHash: String, syncContacts: Bool) -> Signal { let sendCode = Api.functions.auth.sendCode(phoneNumber: phoneNumber, apiId: apiId, apiHash: apiHash, settings: .codeSettings(flags: 0)) @@ -39,7 +77,7 @@ public func sendAuthorizationCode(accountManager: AccountManager, account: Unaut return (result, account) } |> `catch` { error -> Signal<(Api.auth.SentCode, UnauthorizedAccount), MTRpcError> in - switch (error.errorDescription ?? "") { + switch MatchString(error.errorDescription ?? "") { case Regex("(PHONE_|USER_|NETWORK_)MIGRATE_(\\d+)"): let range = error.errorDescription.range(of: "MIGRATE_")! let updatedMasterDatacenterId = Int32(error.errorDescription[range.upperBound ..< error.errorDescription.endIndex])! @@ -85,7 +123,6 @@ public func sendAuthorizationCode(accountManager: AccountManager, account: Unaut return account } |> mapError { _ -> AuthorizationCodeRequestError in - return .generic(info: nil) } } } @@ -123,7 +160,7 @@ public func resendAuthorizationCode(account: UnauthorizedAccount) -> Signal mapError { _ -> AuthorizationCodeRequestError in return .generic(info: nil) } + } |> mapError { _ -> AuthorizationCodeRequestError in } } } else { return .fail(.generic(info: nil)) @@ -136,7 +173,6 @@ public func resendAuthorizationCode(account: UnauthorizedAccount) -> Signal mapError { _ -> AuthorizationCodeRequestError in - return .generic(info: nil) } |> switchToLatest } @@ -233,7 +269,6 @@ public func authorizeWithCode(accountManager: AccountManager, account: Unauthori } |> switchToLatest |> mapError { _ -> AuthorizationCodeVerificationError in - return .generic } } default: @@ -244,7 +279,6 @@ public func authorizeWithCode(accountManager: AccountManager, account: Unauthori } } |> mapError { _ -> AuthorizationCodeVerificationError in - return .generic } |> switchToLatest } @@ -294,7 +328,6 @@ public func authorizeWithPassword(accountManager: AccountManager, account: Unaut } |> switchToLatest |> mapError { _ -> AuthorizationPasswordVerificationError in - return .generic } } } @@ -309,38 +342,15 @@ public enum PasswordRecoveryOption { case email(pattern: String) } -public func requestPasswordRecovery(account: UnauthorizedAccount) -> Signal { - return account.network.request(Api.functions.auth.requestPasswordRecovery()) - |> map(Optional.init) - |> `catch` { error -> Signal in - if error.errorDescription.hasPrefix("FLOOD_WAIT") { - return .fail(.limitExceeded) - } else if error.errorDescription.hasPrefix("PASSWORD_RECOVERY_NA") { - return .single(nil) - } else { - return .fail(.generic) - } - } - |> map { result -> PasswordRecoveryOption in - if let result = result { - switch result { - case let .passwordRecovery(emailPattern): - return .email(pattern: emailPattern) - } - } else { - return .none - } - } -} - public enum PasswordRecoveryError { case invalidCode case limitExceeded case expired + case generic } -public func performPasswordRecovery(accountManager: AccountManager, account: UnauthorizedAccount, code: String, syncContacts: Bool) -> Signal { - return account.network.request(Api.functions.auth.recoverPassword(code: code)) +func _internal_checkPasswordRecoveryCode(network: Network, code: String) -> Signal { + return network.request(Api.functions.auth.checkRecoveryPassword(code: code), automaticFloodWait: false) |> mapError { error -> PasswordRecoveryError in if error.errorDescription.hasPrefix("FLOOD_WAIT") { return .limitExceeded @@ -350,26 +360,79 @@ public func performPasswordRecovery(accountManager: AccountManager, account: Una return .invalidCode } } - |> mapToSignal { result -> Signal in - return account.postbox.transaction { transaction -> Signal in - switch result { - case let .authorization(_, _, user): - let user = TelegramUser(user: user) - let state = AuthorizedAccountState(isTestingEnvironment: account.testingEnvironment, masterDatacenterId: account.masterDatacenterId, peerId: user.id, state: nil) - /*transaction.updatePeersInternal([user], update: { current, peer -> Peer? in - return peer - })*/ - initializedAppSettingsAfterLogin(transaction: transaction, appVersion: account.networkArguments.appVersion, syncContacts: syncContacts) - transaction.setState(state) - return accountManager.transaction { transaction -> Void in - switchToAuthorizedAccount(transaction: transaction, account: account) - } - case .authorizationSignUpRequired: - return .complete() + |> mapToSignal { result -> Signal in + return .complete() + } +} + +public final class RecoveredAccountData { + let authorization: Api.auth.Authorization + + init(authorization: Api.auth.Authorization) { + self.authorization = authorization + } +} + +public func loginWithRecoveredAccountData(accountManager: AccountManager, account: UnauthorizedAccount, recoveredAccountData: RecoveredAccountData, syncContacts: Bool) -> Signal { + return account.postbox.transaction { transaction -> Signal in + switch recoveredAccountData.authorization { + case let .authorization(_, _, user): + 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 -> Void in + switchToAuthorizedAccount(transaction: transaction, account: account) + } + case .authorizationSignUpRequired: + return .complete() + } + } + |> switchToLatest + |> ignoreValues +} + +func _internal_performPasswordRecovery(network: Network, code: String, updatedPassword: UpdatedTwoStepVerificationPassword) -> Signal { + return _internal_twoStepAuthData(network) + |> mapError { _ -> PasswordRecoveryError in + return .generic + } + |> mapToSignal { authData -> Signal in + let newSettings: Api.account.PasswordInputSettings? + switch updatedPassword { + case .none: + newSettings = nil + case let .password(password, hint, email): + var flags: Int32 = 1 << 0 + if email != nil { + flags |= (1 << 1) + } + + guard let (updatedPasswordHash, updatedPasswordDerivation) = passwordUpdateKDF(encryptionProvider: network.encryptionProvider, password: password, derivation: authData.nextPasswordDerivation) else { + return .fail(.invalidCode) + } + + newSettings = Api.account.PasswordInputSettings.passwordInputSettings(flags: flags, newAlgo: updatedPasswordDerivation.apiAlgo, newPasswordHash: Buffer(data: updatedPasswordHash), hint: hint, email: email, newSecureSettings: nil) + } + + var flags: Int32 = 0 + if newSettings != nil { + flags |= 1 << 0 + } + return network.request(Api.functions.auth.recoverPassword(flags: flags, code: code, newSettings: newSettings), automaticFloodWait: false) + |> mapError { error -> PasswordRecoveryError in + if error.errorDescription.hasPrefix("FLOOD_WAIT") { + return .limitExceeded + } else if error.errorDescription.hasPrefix("PASSWORD_RECOVERY_EXPIRED") { + return .expired + } else { + return .invalidCode } } - |> switchToLatest - |> mapError { _ in return PasswordRecoveryError.expired } + |> mapToSignal { result -> Signal in + return .single(RecoveredAccountData(authorization: result)) + } } } @@ -418,7 +481,7 @@ public func performAccountReset(account: UnauthorizedAccount) -> Signal mapError { _ in return AccountResetError.generic } + |> mapError { _ -> AccountResetError in } } } @@ -467,10 +530,10 @@ public func signUpWithName(accountManager: AccountManager, account: Unauthorized |> castError(SignUpError.self) if let avatarData = avatarData { - let resource = LocalFileMediaResource(fileId: arc4random64()) + let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) account.postbox.mediaBox.storeResourceData(resource.id, data: avatarData) - return updatePeerPhotoInternal(postbox: account.postbox, network: account.network, stateManager: nil, accountPeerId: user.id, peer: .single(user), photo: uploadedPeerPhoto(postbox: account.postbox, network: account.network, resource: resource), video: avatarVideo, videoStartTimestamp: videoStartTimestamp, mapResourceToAvatarSizes: { _, _ in .single([:]) }) + return _internal_updatePeerPhotoInternal(postbox: account.postbox, network: account.network, stateManager: nil, accountPeerId: user.id, peer: .single(user), photo: _internal_uploadedPeerPhoto(postbox: account.postbox, network: account.network, resource: resource), video: avatarVideo, videoStartTimestamp: videoStartTimestamp, mapResourceToAvatarSizes: { _, _ in .single([:]) }) |> `catch` { _ -> Signal in return .complete() } @@ -497,7 +560,6 @@ public func signUpWithName(accountManager: AccountManager, account: Unauthorized } } |> mapError { _ -> SignUpError in - return .generic } |> switchToLatest } diff --git a/submodules/TelegramCore/Sources/BotPaymentForm.swift b/submodules/TelegramCore/Sources/BotPaymentForm.swift deleted file mode 100644 index cdb8a9ce8a..0000000000 --- a/submodules/TelegramCore/Sources/BotPaymentForm.swift +++ /dev/null @@ -1,393 +0,0 @@ -import Foundation -import Postbox -import MtProtoKit -import SwiftSignalKit -import TelegramApi - -import SyncCore - -public struct BotPaymentInvoiceFields: OptionSet { - public var rawValue: Int32 - - public init(rawValue: Int32) { - self.rawValue = rawValue - } - - public init() { - self.rawValue = 0 - } - - public static let name = BotPaymentInvoiceFields(rawValue: 1 << 0) - public static let phone = BotPaymentInvoiceFields(rawValue: 1 << 1) - public static let email = BotPaymentInvoiceFields(rawValue: 1 << 2) - public static let shippingAddress = BotPaymentInvoiceFields(rawValue: 1 << 3) - public static let flexibleShipping = BotPaymentInvoiceFields(rawValue: 1 << 4) -} - -public struct BotPaymentPrice : Equatable { - public let label: String - public let amount: Int64 - - public init(label: String, amount: Int64) { - self.label = label - self.amount = amount - } -} - -public struct BotPaymentInvoice : Equatable { - public let isTest: Bool - public let requestedFields: BotPaymentInvoiceFields - public let currency: String - public let prices: [BotPaymentPrice] -} - -public struct BotPaymentNativeProvider : Equatable { - public let name: String - public let params: String -} - -public struct BotPaymentShippingAddress: Equatable { - public let streetLine1: String - public let streetLine2: String - public let city: String - public let state: String - public let countryIso2: String - public let postCode: String - - public init(streetLine1: String, streetLine2: String, city: String, state: String, countryIso2: String, postCode: String) { - self.streetLine1 = streetLine1 - self.streetLine2 = streetLine2 - self.city = city - self.state = state - self.countryIso2 = countryIso2 - self.postCode = postCode - } -} - -public struct BotPaymentRequestedInfo: Equatable { - public let name: String? - public let phone: String? - public let email: String? - public let shippingAddress: BotPaymentShippingAddress? - - public init(name: String?, phone: String?, email: String?, shippingAddress: BotPaymentShippingAddress?) { - self.name = name - self.phone = phone - self.email = email - self.shippingAddress = shippingAddress - } -} - -public enum BotPaymentSavedCredentials: Equatable { - case card(id: String, title: String) - - public static func ==(lhs: BotPaymentSavedCredentials, rhs: BotPaymentSavedCredentials) -> Bool { - switch lhs { - case let .card(id, title): - if case .card(id, title) = rhs { - return true - } else { - return false - } - } - } -} - -public struct BotPaymentForm : Equatable { - public let canSaveCredentials: Bool - public let passwordMissing: Bool - public let invoice: BotPaymentInvoice - public let providerId: PeerId - public let url: String - public let nativeProvider: BotPaymentNativeProvider? - public let savedInfo: BotPaymentRequestedInfo? - public let savedCredentials: BotPaymentSavedCredentials? -} - -public enum BotPaymentFormRequestError { - case generic -} - -extension BotPaymentInvoice { - init(apiInvoice: Api.Invoice) { - switch apiInvoice { - case let .invoice(flags, currency, prices): - var fields = BotPaymentInvoiceFields() - if (flags & (1 << 1)) != 0 { - fields.insert(.name) - } - if (flags & (1 << 2)) != 0 { - fields.insert(.phone) - } - if (flags & (1 << 3)) != 0 { - fields.insert(.email) - } - if (flags & (1 << 4)) != 0 { - fields.insert(.shippingAddress) - } - if (flags & (1 << 5)) != 0 { - fields.insert(.flexibleShipping) - } - self.init(isTest: (flags & (1 << 0)) != 0, requestedFields: fields, currency: currency, prices: prices.map { - switch $0 { - case let .labeledPrice(label, amount): - return BotPaymentPrice(label: label, amount: amount) - } - }) - } - } -} - -extension BotPaymentRequestedInfo { - init(apiInfo: Api.PaymentRequestedInfo) { - switch apiInfo { - case let .paymentRequestedInfo(_, name, phone, email, shippingAddress): - var parsedShippingAddress: BotPaymentShippingAddress? - if let shippingAddress = shippingAddress { - switch shippingAddress { - case let .postAddress(streetLine1, streetLine2, city, state, countryIso2, postCode): - parsedShippingAddress = BotPaymentShippingAddress(streetLine1: streetLine1, streetLine2: streetLine2, city: city, state: state, countryIso2: countryIso2, postCode: postCode) - } - } - self.init(name: name, phone: phone, email: email, shippingAddress: parsedShippingAddress) - } - } -} - -public func fetchBotPaymentForm(postbox: Postbox, network: Network, messageId: MessageId) -> Signal { - return network.request(Api.functions.payments.getPaymentForm(msgId: messageId.id)) - |> `catch` { _ -> Signal in - return .fail(.generic) - } - |> mapToSignal { result -> Signal in - return postbox.transaction { transaction -> BotPaymentForm in - switch result { - case let .paymentForm(flags, _, invoice, providerId, url, nativeProvider, nativeParams, savedInfo, savedCredentials, apiUsers): - var peers: [Peer] = [] - for user in apiUsers { - let parsed = TelegramUser(user: user) - peers.append(parsed) - } - updatePeers(transaction: transaction, peers: peers, update: { _, updated in - return updated - }) - - let parsedInvoice = BotPaymentInvoice(apiInvoice: invoice) - var parsedNativeProvider: BotPaymentNativeProvider? - if let nativeProvider = nativeProvider, let nativeParams = nativeParams { - switch nativeParams { - case let .dataJSON(data): - parsedNativeProvider = BotPaymentNativeProvider(name: nativeProvider, params: data) - } - } - let parsedSavedInfo = savedInfo.flatMap(BotPaymentRequestedInfo.init) - var parsedSavedCredentials: BotPaymentSavedCredentials? - if let savedCredentials = savedCredentials { - switch savedCredentials { - case let .paymentSavedCredentialsCard(id, title): - parsedSavedCredentials = .card(id: id, title: title) - } - } - return BotPaymentForm(canSaveCredentials: (flags & (1 << 2)) != 0, passwordMissing: (flags & (1 << 3)) != 0, invoice: parsedInvoice, providerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: providerId), url: url, nativeProvider: parsedNativeProvider, savedInfo: parsedSavedInfo, savedCredentials: parsedSavedCredentials) - } - } |> mapError { _ -> BotPaymentFormRequestError in return .generic } - } -} - -public enum ValidateBotPaymentFormError { - case generic - case shippingNotAvailable - case addressStateInvalid - case addressPostcodeInvalid - case addressCityInvalid - case nameInvalid - case emailInvalid - case phoneInvalid -} - -public struct BotPaymentShippingOption : Equatable { - public let id: String - public let title: String - public let prices: [BotPaymentPrice] -} - -public struct BotPaymentValidatedFormInfo : Equatable { - public let id: String? - public let shippingOptions: [BotPaymentShippingOption]? -} - -extension BotPaymentShippingOption { - init(apiOption: Api.ShippingOption) { - switch apiOption { - case let .shippingOption(id, title, prices): - self.init(id: id, title: title, prices: prices.map { - switch $0 { - case let .labeledPrice(label, amount): - return BotPaymentPrice(label: label, amount: amount) - } - }) - } - } -} - -public func validateBotPaymentForm(network: Network, saveInfo: Bool, messageId: MessageId, formInfo: BotPaymentRequestedInfo) -> Signal { - var flags: Int32 = 0 - if saveInfo { - flags |= (1 << 0) - } - var infoFlags: Int32 = 0 - if let _ = formInfo.name { - infoFlags |= (1 << 0) - } - if let _ = formInfo.phone { - infoFlags |= (1 << 1) - } - if let _ = formInfo.email { - infoFlags |= (1 << 2) - } - var apiShippingAddress: Api.PostAddress? - if let address = formInfo.shippingAddress { - infoFlags |= (1 << 3) - apiShippingAddress = .postAddress(streetLine1: address.streetLine1, streetLine2: address.streetLine2, city: address.city, state: address.state, countryIso2: address.countryIso2, postCode: address.postCode) - } - return network.request(Api.functions.payments.validateRequestedInfo(flags: flags, msgId: messageId.id, info: .paymentRequestedInfo(flags: infoFlags, name: formInfo.name, phone: formInfo.phone, email: formInfo.email, shippingAddress: apiShippingAddress))) - |> mapError { error -> ValidateBotPaymentFormError in - if error.errorDescription == "SHIPPING_NOT_AVAILABLE" { - return .shippingNotAvailable - } else if error.errorDescription == "ADDRESS_STATE_INVALID" { - return .addressStateInvalid - } else if error.errorDescription == "ADDRESS_POSTCODE_INVALID" { - return .addressPostcodeInvalid - } else if error.errorDescription == "ADDRESS_CITY_INVALID" { - return .addressCityInvalid - } else if error.errorDescription == "REQ_INFO_NAME_INVALID" { - return .nameInvalid - } else if error.errorDescription == "REQ_INFO_EMAIL_INVALID" { - return .emailInvalid - } else if error.errorDescription == "REQ_INFO_PHONE_INVALID" { - return .phoneInvalid - } else { - return .generic - } - } - |> map { result -> BotPaymentValidatedFormInfo in - switch result { - case let .validatedRequestedInfo(_, id, shippingOptions): - return BotPaymentValidatedFormInfo(id: id, shippingOptions: shippingOptions.flatMap { - return $0.map(BotPaymentShippingOption.init) - }) - } - } -} - -public enum BotPaymentCredentials { - case generic(data: String, saveOnServer: Bool) - case saved(id: String, tempPassword: Data) - case applePay(data: String) -} - -public enum SendBotPaymentFormError { - case generic - case precheckoutFailed - case paymentFailed - case alreadyPaid -} - -public enum SendBotPaymentResult { - case done - case externalVerificationRequired(url: String) -} - -public func sendBotPaymentForm(account: Account, messageId: MessageId, validatedInfoId: String?, shippingOptionId: String?, credentials: BotPaymentCredentials) -> Signal { - let apiCredentials: Api.InputPaymentCredentials - switch credentials { - case let .generic(data, saveOnServer): - var credentialsFlags: Int32 = 0 - if saveOnServer { - credentialsFlags |= (1 << 0) - } - apiCredentials = .inputPaymentCredentials(flags: credentialsFlags, data: .dataJSON(data: data)) - case let .saved(id, tempPassword): - apiCredentials = .inputPaymentCredentialsSaved(id: id, tmpPassword: Buffer(data: tempPassword)) - case let .applePay(data): - apiCredentials = .inputPaymentCredentialsApplePay(paymentData: .dataJSON(data: data)) - } - var flags: Int32 = 0 - if validatedInfoId != nil { - flags |= (1 << 0) - } - if shippingOptionId != nil { - flags |= (1 << 1) - } - return account.network.request(Api.functions.payments.sendPaymentForm(flags: flags, msgId: messageId.id, requestedInfoId: validatedInfoId, shippingOptionId: shippingOptionId, credentials: apiCredentials)) - |> map { result -> SendBotPaymentResult in - switch result { - case let .paymentResult(updates): - account.stateManager.addUpdates(updates) - return .done - case let .paymentVerificationNeeded(url): - return .externalVerificationRequired(url: url) - } - } - |> `catch` { error -> Signal in - if error.errorDescription == "BOT_PRECHECKOUT_FAILED" { - return .fail(.precheckoutFailed) - } else if error.errorDescription == "PAYMENT_FAILED" { - return .fail(.paymentFailed) - } else if error.errorDescription == "INVOICE_ALREADY_PAID" { - return .fail(.alreadyPaid) - } - return .fail(.generic) - } -} - -public struct BotPaymentReceipt : Equatable { - public let invoice: BotPaymentInvoice - public let info: BotPaymentRequestedInfo? - public let shippingOption: BotPaymentShippingOption? - public let credentialsTitle: String -} - -public func requestBotPaymentReceipt(network: Network, messageId: MessageId) -> Signal { - return network.request(Api.functions.payments.getPaymentReceipt(msgId: messageId.id)) - |> retryRequest - |> map { result -> BotPaymentReceipt in - switch result { - case let .paymentReceipt(_, _, _, invoice, _, info, shipping, _, _, credentialsTitle, _): - let parsedInvoice = BotPaymentInvoice(apiInvoice: invoice) - let parsedInfo = info.flatMap(BotPaymentRequestedInfo.init) - let shippingOption = shipping.flatMap(BotPaymentShippingOption.init) - return BotPaymentReceipt(invoice: parsedInvoice, info: parsedInfo, shippingOption: shippingOption, credentialsTitle: credentialsTitle) - } - } -} - -public struct BotPaymentInfo: OptionSet { - public var rawValue: Int32 - - public init(rawValue: Int32) { - self.rawValue = rawValue - } - - public init() { - self.rawValue = 0 - } - - public static let paymentInfo = BotPaymentInfo(rawValue: 1 << 0) - public static let shippingInfo = BotPaymentInfo(rawValue: 1 << 1) -} - -public func clearBotPaymentInfo(network: Network, info: BotPaymentInfo) -> Signal { - var flags: Int32 = 0 - if info.contains(.paymentInfo) { - flags |= (1 << 0) - } - if info.contains(.shippingInfo) { - flags |= (1 << 1) - } - return network.request(Api.functions.payments.clearSavedInfo(flags: flags)) - |> retryRequest - |> mapToSignal { _ -> Signal in - return .complete() - } -} diff --git a/submodules/TelegramCore/Sources/ChannelAdmins.swift b/submodules/TelegramCore/Sources/ChannelAdmins.swift deleted file mode 100644 index 8c6f54c8de..0000000000 --- a/submodules/TelegramCore/Sources/ChannelAdmins.swift +++ /dev/null @@ -1,82 +0,0 @@ -import Foundation -import Postbox -import SwiftSignalKit -import TelegramApi -import MtProtoKit - -import SyncCore - -public func channelAdmins(account: Account, peerId: PeerId) -> Signal<[RenderedChannelParticipant], NoError> { - return account.postbox.transaction { transaction -> Signal<[RenderedChannelParticipant], NoError> in - if let peer = transaction.getPeer(peerId), let inputChannel = apiInputChannel(peer) { - return account.network.request(Api.functions.channels.getParticipants(channel: inputChannel, filter: .channelParticipantsAdmins, offset: 0, limit: 100, hash: 0)) - |> retryRequest - |> mapToSignal { result -> Signal<[RenderedChannelParticipant], NoError> in - switch result { - case let .channelParticipants(count, participants, users): - var items: [RenderedChannelParticipant] = [] - - var peers: [PeerId: Peer] = [:] - var presences:[PeerId: PeerPresence] = [:] - for user in users { - let peer = TelegramUser(user: user) - peers[peer.id] = peer - if let presence = TelegramUserPresence(apiUser: user) { - presences[peer.id] = presence - } - } - - for participant in CachedChannelParticipants(apiParticipants: participants).participants { - if let peer = peers[participant.peerId] { - items.append(RenderedChannelParticipant(participant: participant, peer: peer, peers: peers, presences: presences)) - } - } - - return account.postbox.transaction { transaction -> [RenderedChannelParticipant] in - transaction.updatePeerCachedData(peerIds: Set([peerId]), update: { _, cachedData -> CachedPeerData? in - if let cachedData = cachedData as? CachedChannelData { - return cachedData.withUpdatedParticipantsSummary(cachedData.participantsSummary.withUpdatedAdminCount(count)) - } else { - return cachedData - } - }) - return items - } - case .channelParticipantsNotModified: - return .single([]) - } - } - } else { - return .single([]) - } - } |> switchToLatest -} - -public func channelAdminIds(postbox: Postbox, network: Network, peerId: PeerId, hash: Int32) -> Signal<[PeerId], NoError> { - return postbox.transaction { transaction in - if let peer = transaction.getPeer(peerId) as? TelegramChannel, case .group = peer.info, let apiChannel = apiInputChannel(peer) { - let api = Api.functions.channels.getParticipants(channel: apiChannel, filter: .channelParticipantsAdmins, offset: 0, limit: 100, hash: hash) - return network.request(api) |> retryRequest |> mapToSignal { result in - switch result { - case let .channelParticipants(_, participants, users): - let users = users.filter({ user in - return participants.contains(where: { participant in - switch participant { - case let .channelParticipantAdmin(_, userId, _, _, _, _, _): - return user.peerId.id == userId - case let .channelParticipantCreator(_, userId, _, _): - return user.peerId.id == userId - default: - return false - } - }) - }) - return .single(users.map({TelegramUser(user: $0).id})) - default: - return .complete() - } - } - } - return .complete() - } |> switchToLatest -} diff --git a/submodules/TelegramCore/Sources/ChannelParticipants.swift b/submodules/TelegramCore/Sources/ChannelParticipants.swift deleted file mode 100644 index e3fb732676..0000000000 --- a/submodules/TelegramCore/Sources/ChannelParticipants.swift +++ /dev/null @@ -1,84 +0,0 @@ -import Foundation -import Postbox -import SwiftSignalKit -import TelegramApi -import MtProtoKit - -import SyncCore - -public struct RenderedChannelParticipant: Equatable { - public let participant: ChannelParticipant - public let peer: Peer - public let peers: [PeerId: Peer] - public let presences: [PeerId: PeerPresence] - - public init(participant: ChannelParticipant, peer: Peer, peers: [PeerId: Peer] = [:], presences: [PeerId: PeerPresence] = [:]) { - self.participant = participant - self.peer = peer - self.peers = peers - self.presences = presences - } - - public static func ==(lhs: RenderedChannelParticipant, rhs: RenderedChannelParticipant) -> Bool { - return lhs.participant == rhs.participant && lhs.peer.isEqual(rhs.peer) - } -} - -func updateChannelParticipantsSummary(account: Account, peerId: PeerId) -> Signal { - return account.postbox.transaction { transaction -> Signal in - if let peer = transaction.getPeer(peerId), let inputChannel = apiInputChannel(peer) { - let admins = account.network.request(Api.functions.channels.getParticipants(channel: inputChannel, filter: .channelParticipantsAdmins, offset: 0, limit: 0, hash: 0)) - let members = account.network.request(Api.functions.channels.getParticipants(channel: inputChannel, filter: .channelParticipantsRecent, offset: 0, limit: 0, hash: 0)) - let banned = account.network.request(Api.functions.channels.getParticipants(channel: inputChannel, filter: .channelParticipantsBanned(q: ""), offset: 0, limit: 0, hash: 0)) - let kicked = account.network.request(Api.functions.channels.getParticipants(channel: inputChannel, filter: .channelParticipantsKicked(q: ""), offset: 0, limit: 0, hash: 0)) - return combineLatest(admins, members, banned, kicked) - |> mapToSignal { admins, members, banned, kicked -> Signal in - return account.postbox.transaction { transaction -> Void in - transaction.updatePeerCachedData(peerIds: Set([peerId]), update: { _, current in - if let current = current as? CachedChannelData { - let adminCount: Int32 - switch admins { - case let .channelParticipants(count, _, _): - adminCount = count - case .channelParticipantsNotModified: - assertionFailure() - adminCount = 0 - } - let memberCount: Int32 - switch members { - case let .channelParticipants(count, _, _): - memberCount = count - case .channelParticipantsNotModified: - assertionFailure() - memberCount = 0 - } - let bannedCount: Int32 - switch banned { - case let .channelParticipants(count, _, _): - bannedCount = count - case .channelParticipantsNotModified: - assertionFailure() - bannedCount = 0 - } - let kickedCount: Int32 - switch kicked { - case let .channelParticipants(count, _, _): - kickedCount = count - case .channelParticipantsNotModified: - assertionFailure() - kickedCount = 0 - } - return current.withUpdatedParticipantsSummary(CachedChannelParticipantsSummary(memberCount: memberCount, adminCount: adminCount, bannedCount: bannedCount, kickedCount: kickedCount)) - } - return current - }) - } |> mapError { _ -> MTRpcError in return MTRpcError(errorCode: 0, errorDescription: "") } - } - |> `catch` { _ -> Signal in - return .complete() - } - } else { - return .complete() - } - } |> switchToLatest -} diff --git a/submodules/TelegramCore/Sources/ChatHistoryImport.swift b/submodules/TelegramCore/Sources/ChatHistoryImport.swift deleted file mode 100644 index d1e5811512..0000000000 --- a/submodules/TelegramCore/Sources/ChatHistoryImport.swift +++ /dev/null @@ -1,244 +0,0 @@ -import Foundation -import SwiftSignalKit -import Postbox -import SyncCore -import TelegramApi - -public enum ChatHistoryImport { - public struct Session { - fileprivate var peerId: PeerId - fileprivate var inputPeer: Api.InputPeer - fileprivate var id: Int64 - } - - public enum InitImportError { - case generic - case chatAdminRequired - case invalidChatType - case userBlocked - case limitExceeded - } - - public enum ParsedInfo { - case privateChat(title: String?) - case group(title: String?) - case unknown(title: String?) - } - - public enum GetInfoError { - case generic - case parseError - } - - public static func getInfo(account: Account, header: String) -> Signal { - return account.network.request(Api.functions.messages.checkHistoryImport(importHead: header)) - |> mapError { _ -> GetInfoError in - return .generic - } - |> mapToSignal { result -> Signal in - switch result { - case let .historyImportParsed(flags, title): - if (flags & (1 << 0)) != 0 { - return .single(.privateChat(title: title)) - } else if (flags & (1 << 1)) != 0 { - return .single(.group(title: title)) - } else { - return .single(.unknown(title: title)) - } - } - } - } - - public static func initSession(account: Account, peerId: PeerId, file: TempBoxFile, mediaCount: Int32) -> Signal { - return multipartUpload(network: account.network, postbox: account.postbox, source: .tempFile(file), encrypt: false, tag: nil, hintFileSize: nil, hintFileIsLarge: false, forceNoBigParts: true, useLargerParts: true, increaseParallelParts: true, useMultiplexedRequests: false, useCompression: true) - |> mapError { _ -> InitImportError in - return .generic - } - |> mapToSignal { result -> Signal in - switch result { - case let .inputFile(inputFile): - return account.postbox.transaction { transaction -> Api.InputPeer? in - return transaction.getPeer(peerId).flatMap(apiInputPeer) - } - |> castError(InitImportError.self) - |> mapToSignal { inputPeer -> Signal in - guard let inputPeer = inputPeer else { - return .fail(.generic) - } - return account.network.request(Api.functions.messages.initHistoryImport(peer: inputPeer, file: inputFile, mediaCount: mediaCount), automaticFloodWait: false) - |> mapError { error -> InitImportError in - if error.errorDescription == "CHAT_ADMIN_REQUIRED" { - return .chatAdminRequired - } else if error.errorDescription == "IMPORT_PEER_TYPE_INVALID" { - return .invalidChatType - } else if error.errorDescription == "USER_IS_BLOCKED" { - return .userBlocked - } else if error.errorDescription == "FLOOD_WAIT" { - return .limitExceeded - } else { - return .generic - } - } - |> map { result -> Session in - switch result { - case let .historyImport(id): - return Session(peerId: peerId, inputPeer: inputPeer, id: id) - } - } - } - case .progress: - return .complete() - case .inputSecretFile: - return .fail(.generic) - } - } - } - - public enum MediaType { - case photo - case file - case video - case sticker - case voice - } - - public enum UploadMediaError { - case generic - case chatAdminRequired - } - - public static func uploadMedia(account: Account, session: Session, file: TempBoxFile, disposeFileAfterDone: Bool, fileName: String, mimeType: String, type: MediaType) -> Signal { - var forceNoBigParts = true - guard let size = fileSize(file.path), size != 0 else { - return .single(1.0) - } - if size >= 30 * 1024 * 1024 { - forceNoBigParts = false - } - - return multipartUpload(network: account.network, postbox: account.postbox, source: .tempFile(file), encrypt: false, tag: nil, hintFileSize: nil, hintFileIsLarge: false, forceNoBigParts: forceNoBigParts, useLargerParts: true, useMultiplexedRequests: true) - |> mapError { _ -> UploadMediaError in - return .generic - } - |> mapToSignal { result -> Signal in - let inputMedia: Api.InputMedia - switch result { - case let .inputFile(inputFile): - switch type { - case .photo: - inputMedia = .inputMediaUploadedPhoto(flags: 0, file: inputFile, stickers: nil, ttlSeconds: nil) - case .file, .video, .sticker, .voice: - var attributes: [Api.DocumentAttribute] = [] - attributes.append(.documentAttributeFilename(fileName: fileName)) - var resolvedMimeType = mimeType - switch type { - case .video: - resolvedMimeType = "video/mp4" - case .sticker: - resolvedMimeType = "image/webp" - case .voice: - resolvedMimeType = "audio/ogg" - default: - break - } - inputMedia = .inputMediaUploadedDocument(flags: 0, file: inputFile, thumb: nil, mimeType: resolvedMimeType, attributes: attributes, stickers: nil, ttlSeconds: nil) - } - case let .progress(value): - return .single(value) - case .inputSecretFile: - return .fail(.generic) - } - return account.network.request(Api.functions.messages.uploadImportedMedia(peer: session.inputPeer, importId: session.id, fileName: fileName, media: inputMedia)) - |> mapError { error -> UploadMediaError in - switch error.errorDescription { - case "CHAT_ADMIN_REQUIRED": - return .chatAdminRequired - default: - return .generic - } - } - |> mapToSignal { result -> Signal in - return .single(1.0) - } - |> afterDisposed { - if disposeFileAfterDone { - TempBox.shared.dispose(file) - } - } - } - } - - public enum StartImportError { - case generic - } - - public static func startImport(account: Account, session: Session) -> Signal { - return account.network.request(Api.functions.messages.startHistoryImport(peer: session.inputPeer, importId: session.id)) - |> mapError { _ -> StartImportError in - return .generic - } - |> mapToSignal { result -> Signal in - if case .boolTrue = result { - return .complete() - } else { - return .fail(.generic) - } - } - } - - public enum CheckPeerImportResult { - case allowed - case alert(String) - } - - public enum CheckPeerImportError { - case generic - case chatAdminRequired - case invalidChatType - case userBlocked - case limitExceeded - case notMutualContact - } - - public static func checkPeerImport(account: Account, peerId: PeerId) -> Signal { - return account.postbox.transaction { transaction -> Peer? in - return transaction.getPeer(peerId) - } - |> castError(CheckPeerImportError.self) - |> mapToSignal { peer -> Signal in - guard let peer = peer else { - return .fail(.generic) - } - guard let inputPeer = apiInputPeer(peer) else { - return .fail(.generic) - } - - return account.network.request(Api.functions.messages.checkHistoryImportPeer(peer: inputPeer)) - |> mapError { error -> CheckPeerImportError in - if error.errorDescription == "CHAT_ADMIN_REQUIRED" { - return .chatAdminRequired - } else if error.errorDescription == "IMPORT_PEER_TYPE_INVALID" { - return .invalidChatType - } else if error.errorDescription == "USER_IS_BLOCKED" { - return .userBlocked - } else if error.errorDescription == "USER_NOT_MUTUAL_CONTACT" { - return .notMutualContact - } else if error.errorDescription == "FLOOD_WAIT" { - return .limitExceeded - } else { - return .generic - } - } - |> map { result -> CheckPeerImportResult in - switch result { - case let .checkedHistoryImportPeer(confirmText): - if confirmText.isEmpty { - return .allowed - } else { - return .alert(confirmText) - } - } - } - } - } -} diff --git a/submodules/TelegramCore/Sources/Config/TelegramCore.xcconfig b/submodules/TelegramCore/Sources/Config/TelegramCore.xcconfig deleted file mode 100644 index c3b3ada1b4..0000000000 --- a/submodules/TelegramCore/Sources/Config/TelegramCore.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -SWIFT_INCLUDE_PATHS = $(SRCROOT)/TelegramCore -MODULEMAP_PRIVATE_FILE = $(SRCROOT)/TelegramCore/module.private.modulemap diff --git a/submodules/TelegramCore/Sources/CoreSettings.swift b/submodules/TelegramCore/Sources/CoreSettings.swift deleted file mode 100644 index 6fed549367..0000000000 --- a/submodules/TelegramCore/Sources/CoreSettings.swift +++ /dev/null @@ -1,59 +0,0 @@ -import Foundation -import Postbox -import SwiftSignalKit -import MtProtoKit - -import SyncCore - -public final class CoreSettings: PreferencesEntry, Equatable { - public let fastForward: Bool - - public static var defaultSettings = CoreSettings(fastForward: true) - - public init(fastForward: Bool) { - self.fastForward = fastForward - } - - public init(decoder: PostboxDecoder) { - self.fastForward = decoder.decodeInt32ForKey("fastForward", orElse: 0) != 0 - } - - public func encode(_ encoder: PostboxEncoder) { - encoder.encodeInt32(self.fastForward ? 1 : 0, forKey: "fastForward") - } - - public func withUpdatedFastForward(_ fastForward: Bool) -> CoreSettings { - return CoreSettings(fastForward: fastForward) - } - - public func isEqual(to: PreferencesEntry) -> Bool { - guard let to = to as? CoreSettings else { - return false - } - - return self == to - } - - public static func ==(lhs: CoreSettings, rhs: CoreSettings) -> Bool { - if lhs.fastForward != rhs.fastForward { - return false - } - return true - } -} - -public func updateCoreSettings(postbox: Postbox, _ f: @escaping (CoreSettings) -> CoreSettings) -> Signal { - return postbox.transaction { transaction -> Void in - var updated: CoreSettings? - transaction.updatePreferencesEntry(key: PreferencesKeys.coreSettings, { current in - if let current = current as? CoreSettings { - updated = f(current) - return updated - } else { - updated = f(CoreSettings.defaultSettings) - return updated - } - }) - } -} - diff --git a/submodules/TelegramCore/Sources/DeleteAccount.swift b/submodules/TelegramCore/Sources/DeleteAccount.swift deleted file mode 100644 index 6ef229ca72..0000000000 --- a/submodules/TelegramCore/Sources/DeleteAccount.swift +++ /dev/null @@ -1,16 +0,0 @@ -import Foundation -import SwiftSignalKit -import Postbox -import TelegramApi - -public enum DeleteAccountError { - case generic -} - -public func deleteAccount(account: Account) -> Signal { - return account.network.request(Api.functions.account.deleteAccount(reason: "GDPR")) - |> mapError { _ -> DeleteAccountError in - return .generic - } - |> ignoreValues -} diff --git a/submodules/TelegramCore/Sources/Either.swift b/submodules/TelegramCore/Sources/Either.swift deleted file mode 100644 index 1478adbc58..0000000000 --- a/submodules/TelegramCore/Sources/Either.swift +++ /dev/null @@ -1,6 +0,0 @@ -import Foundation - -public enum Either { - case left(value: Left) - case right(value: Right) -} diff --git a/submodules/TelegramCore/Sources/GlobalTelegramCoreConfiguration.swift b/submodules/TelegramCore/Sources/GlobalTelegramCoreConfiguration.swift deleted file mode 100644 index 7cf05d8aa9..0000000000 --- a/submodules/TelegramCore/Sources/GlobalTelegramCoreConfiguration.swift +++ /dev/null @@ -1,5 +0,0 @@ -import Foundation - -public final class GlobalTelegramCoreConfiguration { - public static var readMessages: Bool = true -} diff --git a/submodules/TelegramCore/Sources/Info.plist b/submodules/TelegramCore/Sources/Info.plist deleted file mode 100644 index fbe1e6b314..0000000000 --- a/submodules/TelegramCore/Sources/Info.plist +++ /dev/null @@ -1,24 +0,0 @@ - - - - - CFBundleDevelopmentRegion - en - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - FMWK - CFBundleShortVersionString - 1.0 - CFBundleVersion - $(CURRENT_PROJECT_VERSION) - NSPrincipalClass - - - diff --git a/submodules/TelegramCore/Sources/GroupReturnAndLeft.swift b/submodules/TelegramCore/Sources/MacOS/GroupReturnAndLeft.swift similarity index 87% rename from submodules/TelegramCore/Sources/GroupReturnAndLeft.swift rename to submodules/TelegramCore/Sources/MacOS/GroupReturnAndLeft.swift index 16360909ab..5073e42342 100644 --- a/submodules/TelegramCore/Sources/GroupReturnAndLeft.swift +++ b/submodules/TelegramCore/Sources/MacOS/GroupReturnAndLeft.swift @@ -9,7 +9,7 @@ public func returnGroup(account: Account, peerId: PeerId) -> Signal take(1) |> mapToSignal { peer -> Signal in if let inputUser = apiInputUser(peer) { - return account.network.request(Api.functions.messages.addChatUser(chatId: peerId.id, userId: inputUser, fwdLimit: 50)) + return account.network.request(Api.functions.messages.addChatUser(chatId: peerId.id._internalGetInt32Value(), userId: inputUser, fwdLimit: 50)) |> retryRequest |> mapToSignal { updates -> Signal in account.stateManager.addUpdates(updates) @@ -26,7 +26,7 @@ public func leftGroup(account: Account, peerId: PeerId) -> Signal |> take(1) |> mapToSignal { peer -> Signal in if let inputUser = apiInputUser(peer) { - return account.network.request(Api.functions.messages.deleteChatUser(flags: 0, chatId: peerId.id, userId: inputUser)) + return account.network.request(Api.functions.messages.deleteChatUser(flags: 0, chatId: peerId.id._internalGetInt32Value(), userId: inputUser)) |> retryRequest |> mapToSignal { updates -> Signal in account.stateManager.addUpdates(updates) diff --git a/submodules/TelegramCore/Sources/MacInternalUpdater.swift b/submodules/TelegramCore/Sources/MacOS/MacInternalUpdater.swift similarity index 98% rename from submodules/TelegramCore/Sources/MacInternalUpdater.swift rename to submodules/TelegramCore/Sources/MacOS/MacInternalUpdater.swift index f185d80ce8..a54dd51ab4 100644 --- a/submodules/TelegramCore/Sources/MacInternalUpdater.swift +++ b/submodules/TelegramCore/Sources/MacOS/MacInternalUpdater.swift @@ -12,7 +12,7 @@ public enum InternalUpdaterError { } public func requestUpdatesXml(account: Account, source: String) -> Signal { - return resolvePeerByName(account: account, name: source) + return TelegramEngine(account: account).peers.resolvePeerByName(name: source) |> castError(InternalUpdaterError.self) |> mapToSignal { peerId -> Signal in return account.postbox.transaction { transaction in @@ -79,7 +79,7 @@ public enum AppUpdateDownloadResult { } public func downloadAppUpdate(account: Account, source: String, messageId: Int32) -> Signal { - return resolvePeerByName(account: account, name: source) + return TelegramEngine(account: account).peers.resolvePeerByName(name: source) |> castError(InternalUpdaterError.self) |> mapToSignal { peerId -> Signal in return account.postbox.transaction { transaction in diff --git a/submodules/TelegramCore/Sources/NotificationAutolockReportManager.swift b/submodules/TelegramCore/Sources/MacOS/NotificationAutolockReportManager.swift similarity index 100% rename from submodules/TelegramCore/Sources/NotificationAutolockReportManager.swift rename to submodules/TelegramCore/Sources/MacOS/NotificationAutolockReportManager.swift diff --git a/submodules/TelegramCore/Sources/MessageReactionList.swift b/submodules/TelegramCore/Sources/MessageReactionList.swift deleted file mode 100644 index e011fb66e4..0000000000 --- a/submodules/TelegramCore/Sources/MessageReactionList.swift +++ /dev/null @@ -1,210 +0,0 @@ -import Foundation -import Postbox -import SwiftSignalKit -import TelegramApi -import MtProtoKit - -import SyncCore - -public enum MessageReactionListCategory: Hashable { - case all - case reaction(String) -} - -public final class MessageReactionListCategoryItem: Equatable { - public let peer: Peer - public let reaction: String - - init(peer: Peer, reaction: String) { - self.peer = peer - self.reaction = reaction - } - - public static func ==(lhs: MessageReactionListCategoryItem, rhs: MessageReactionListCategoryItem) -> Bool { - if lhs.peer.id != rhs.peer.id { - return false - } - if lhs.reaction != rhs.reaction { - return false - } - return true - } -} - -public struct MessageReactionListCategoryState: Equatable { - public var count: Int - public var completed: Bool - public var items: [MessageReactionListCategoryItem] - public var loadingMore: Bool - fileprivate var nextOffset: String? -} - -private enum LoadReactionsError { - case generic -} - -private final class MessageReactionCategoryContext { - private let postbox: Postbox - private let network: Network - private let messageId: MessageId - private let category: MessageReactionListCategory - private var state: MessageReactionListCategoryState - var statePromise: ValuePromise - - private let loadingDisposable = MetaDisposable() - - init(postbox: Postbox, network: Network, messageId: MessageId, category: MessageReactionListCategory, initialState: MessageReactionListCategoryState) { - self.postbox = postbox - self.network = network - self.messageId = messageId - self.category = category - self.state = initialState - self.statePromise = ValuePromise(initialState) - } - - deinit { - self.loadingDisposable.dispose() - } - - func loadMore() { - if self.state.completed || self.state.loadingMore { - return - } - /*self.state.loadingMore = true - self.statePromise.set(self.state) - - var flags: Int32 = 0 - var reaction: String? - switch self.category { - case .all: - break - case let .reaction(value): - flags |= 1 << 0 - reaction = value - } - let messageId = self.messageId - let offset = self.state.nextOffset - var request = self.postbox.transaction { transaction -> Api.InputPeer? in - let inputPeer = transaction.getPeer(messageId.peerId).flatMap(apiInputPeer) - return inputPeer - } - |> castError(LoadReactionsError.self) - |> mapToSignal { inputPeer -> Signal in - guard let inputPeer = inputPeer else { - return .fail(.generic) - } - return self.network.request(Api.functions.messages.getMessageReactionsList(flags: flags, peer: inputPeer, id: messageId.id, reaction: reaction, offset: offset, limit: 64)) - |> mapError { _ -> LoadReactionsError in - return .generic - } - } - //#if DEBUG - //request = request |> delay(1.0, queue: .mainQueue()) - //#endif - self.loadingDisposable.set((request - |> deliverOnMainQueue).start(next: { [weak self] result in - guard let strongSelf = self else { - return - } - let currentState = strongSelf.state - let _ = (strongSelf.postbox.transaction { transaction -> MessageReactionListCategoryState in - var mergedItems = currentState.items - var currentIds = Set(mergedItems.lazy.map { $0.peer.id }) - switch result { - case let .messageReactionsList(_, count, reactions, users, nextOffset): - var peers: [Peer] = [] - for user in users { - let parsedUser = TelegramUser(user: user) - peers.append(parsedUser) - } - updatePeers(transaction: transaction, peers: peers, update: { _, updated in updated }) - for reaction in reactions { - switch reaction { - case let .messageUserReaction(userId, reaction): - if let peer = transaction.getPeer(PeerId(namespace: Namespaces.Peer.CloudUser, id: userId)) { - if !currentIds.contains(peer.id) { - currentIds.insert(peer.id) - mergedItems.append(MessageReactionListCategoryItem(peer: peer, reaction: reaction)) - } - } - } - } - return MessageReactionListCategoryState(count: max(mergedItems.count, Int(count)), completed: nextOffset == nil, items: mergedItems, loadingMore: false, nextOffset: nextOffset) - } - } - |> deliverOnMainQueue).start(next: { state in - guard let strongSelf = self else { - return - } - strongSelf.state = state - strongSelf.statePromise.set(state) - }) - }, error: { _ in - - }))*/ - } -} - -public struct MessageReactionListState: Equatable { - public var states: [(MessageReactionListCategory, MessageReactionListCategoryState)] - - public static func ==(lhs: MessageReactionListState, rhs: MessageReactionListState) -> Bool { - if lhs.states.count != rhs.states.count { - return false - } - for i in 0 ..< lhs.states.count { - if lhs.states[i].0 != rhs.states[i].0 { - return false - } - if lhs.states[i].1 != rhs.states[i].1 { - return false - } - } - return true - } -} - -public final class MessageReactionListContext { - private let postbox: Postbox - private let network: Network - - private var categoryContexts: [MessageReactionListCategory: MessageReactionCategoryContext] = [:] - - private let _state = Promise() - public var state: Signal { - return self._state.get() - } - - public init(postbox: Postbox, network: Network, messageId: MessageId, initialReactions: [MessageReaction]) { - self.postbox = postbox - self.network = network - - var allState = MessageReactionListCategoryState(count: 0, completed: false, items: [], loadingMore: false, nextOffset: nil) - var signals: [Signal<(MessageReactionListCategory, MessageReactionListCategoryState), NoError>] = [] - for reaction in initialReactions { - allState.count += Int(reaction.count) - let context = MessageReactionCategoryContext(postbox: postbox, network: network, messageId: messageId, category: .reaction(reaction.value), initialState: MessageReactionListCategoryState(count: Int(reaction.count), completed: false, items: [], loadingMore: false, nextOffset: nil)) - signals.append(context.statePromise.get() |> map { value -> (MessageReactionListCategory, MessageReactionListCategoryState) in - return (.reaction(reaction.value), value) - }) - self.categoryContexts[.reaction(reaction.value)] = context - context.loadMore() - } - let allContext = MessageReactionCategoryContext(postbox: postbox, network: network, messageId: messageId, category: .all, initialState: allState) - signals.insert(allContext.statePromise.get() |> map { value -> (MessageReactionListCategory, MessageReactionListCategoryState) in - return (.all, value) - }, at: 0) - self.categoryContexts[.all] = allContext - - self._state.set(combineLatest(queue: .mainQueue(), signals) - |> map { states in - return MessageReactionListState(states: states) - }) - - allContext.loadMore() - } - - public func loadMore(category: MessageReactionListCategory) { - self.categoryContexts[category]?.loadMore() - } -} diff --git a/submodules/TelegramCore/Sources/MultipeerManager.swift b/submodules/TelegramCore/Sources/MultipeerManager.swift deleted file mode 100644 index 90004e3427..0000000000 --- a/submodules/TelegramCore/Sources/MultipeerManager.swift +++ /dev/null @@ -1,119 +0,0 @@ -import Foundation -import MultipeerConnectivity - -protocol ColorServiceManagerDelegate { - - func connectedDevicesChanged(manager : ColorServiceManager, connectedDevices: [String]) - func colorChanged(manager : ColorServiceManager, colorString: String) - -} - -class ColorServiceManager : NSObject { - private let ColorServiceType = "tg-p2p" - - private let myPeerId = MCPeerID(displayName: UIDevice.current.name) - - private let serviceAdvertiser : MCNearbyServiceAdvertiser - private let serviceBrowser : MCNearbyServiceBrowser - - var delegate : ColorServiceManagerDelegate? - - lazy var session : MCSession = { - let session = MCSession(peer: self.myPeerId, securityIdentity: nil, encryptionPreference: .required) - session.delegate = self - return session - }() - - override init() { - self.serviceAdvertiser = MCNearbyServiceAdvertiser(peer: myPeerId, discoveryInfo: nil, serviceType: ColorServiceType) - self.serviceBrowser = MCNearbyServiceBrowser(peer: myPeerId, serviceType: ColorServiceType) - - super.init() - - self.serviceAdvertiser.delegate = self - self.serviceAdvertiser.startAdvertisingPeer() - - self.serviceBrowser.delegate = self - self.serviceBrowser.startBrowsingForPeers() - } - - func send(colorName : String) { - NSLog("%@", "sendColor: \(colorName) to \(session.connectedPeers.count) peers") - - if session.connectedPeers.count > 0 { - do { - try self.session.send(colorName.data(using: .utf8)!, toPeers: session.connectedPeers, with: .reliable) - } - catch let error { - NSLog("%@", "Error for sending: \(error)") - } - } - - } - - deinit { - self.serviceAdvertiser.stopAdvertisingPeer() - self.serviceBrowser.stopBrowsingForPeers() - } - -} - -extension ColorServiceManager : MCNearbyServiceAdvertiserDelegate { - - func advertiser(_ advertiser: MCNearbyServiceAdvertiser, didNotStartAdvertisingPeer error: Error) { - NSLog("%@", "didNotStartAdvertisingPeer: \(error)") - } - - func advertiser(_ advertiser: MCNearbyServiceAdvertiser, didReceiveInvitationFromPeer peerID: MCPeerID, withContext context: Data?, invitationHandler: @escaping (Bool, MCSession?) -> Void) { - NSLog("%@", "didReceiveInvitationFromPeer \(peerID)") - invitationHandler(true, self.session) - } - -} - -extension ColorServiceManager : MCNearbyServiceBrowserDelegate { - - func browser(_ browser: MCNearbyServiceBrowser, didNotStartBrowsingForPeers error: Error) { - NSLog("%@", "didNotStartBrowsingForPeers: \(error)") - } - - func browser(_ browser: MCNearbyServiceBrowser, foundPeer peerID: MCPeerID, withDiscoveryInfo info: [String : String]?) { - NSLog("%@", "foundPeer: \(peerID)") - NSLog("%@", "invitePeer: \(peerID)") - browser.invitePeer(peerID, to: self.session, withContext: nil, timeout: 10) - } - - func browser(_ browser: MCNearbyServiceBrowser, lostPeer peerID: MCPeerID) { - NSLog("%@", "lostPeer: \(peerID)") - } - -} - -extension ColorServiceManager : MCSessionDelegate { - - func session(_ session: MCSession, peer peerID: MCPeerID, didChange state: MCSessionState) { - NSLog("%@", "peer \(peerID) didChangeState: \(state)") - self.delegate?.connectedDevicesChanged(manager: self, connectedDevices: - session.connectedPeers.map{$0.displayName}) - } - - func session(_ session: MCSession, didReceive data: Data, fromPeer peerID: MCPeerID) { - NSLog("%@", "didReceiveData: \(data)") - let str = String(data: data, encoding: .utf8)! - self.delegate?.colorChanged(manager: self, colorString: str) - } - - func session(_ session: MCSession, didReceive stream: InputStream, withName streamName: String, fromPeer peerID: MCPeerID) { - NSLog("%@", "didReceiveStream") - } - - func session(_ session: MCSession, didStartReceivingResourceWithName resourceName: String, fromPeer peerID: MCPeerID, with progress: Progress) { - NSLog("%@", "didStartReceivingResourceWithName") - } - - func session(_ session: MCSession, didFinishReceivingResourceWithName resourceName: String, fromPeer peerID: MCPeerID, at localURL: URL?, withError error: Error?) { - NSLog("%@", "didFinishReceivingResourceWithName") - } - -} - diff --git a/submodules/TelegramCore/Sources/Download.swift b/submodules/TelegramCore/Sources/Network/Download.swift similarity index 100% rename from submodules/TelegramCore/Sources/Download.swift rename to submodules/TelegramCore/Sources/Network/Download.swift diff --git a/submodules/TelegramCore/Sources/FetchHttpResource.swift b/submodules/TelegramCore/Sources/Network/FetchHttpResource.swift similarity index 100% rename from submodules/TelegramCore/Sources/FetchHttpResource.swift rename to submodules/TelegramCore/Sources/Network/FetchHttpResource.swift diff --git a/submodules/TelegramCore/Sources/FetchedMediaResource.swift b/submodules/TelegramCore/Sources/Network/FetchedMediaResource.swift similarity index 93% rename from submodules/TelegramCore/Sources/FetchedMediaResource.swift rename to submodules/TelegramCore/Sources/Network/FetchedMediaResource.swift index 33f492620e..e45786cfe8 100644 --- a/submodules/TelegramCore/Sources/FetchedMediaResource.swift +++ b/submodules/TelegramCore/Sources/Network/FetchedMediaResource.swift @@ -69,9 +69,27 @@ public func stickerPackFileReference(_ file: TelegramMediaFile) -> FileMediaRefe return .standalone(media: file) } +private func areResourcesEqual(_ lhs: MediaResource, _ rhs: MediaResource) -> Bool { + if let lhsResource = lhs as? CloudDocumentMediaResource, let rhsResource = rhs as? CloudDocumentMediaResource { + if lhsResource.fileId == rhsResource.fileId { + return true + } + } else if let lhsResource = lhs as? CloudDocumentSizeMediaResource, let rhsResource = rhs as? CloudDocumentSizeMediaResource { + if lhsResource.documentId == rhsResource.documentId && lhsResource.sizeSpec == rhsResource.sizeSpec { + return true + } + } + return lhs.id.isEqual(to: rhs.id) +} + private func findMediaResource(media: Media, previousMedia: Media?, resource: MediaResource) -> TelegramMediaResource? { if let image = media as? TelegramMediaImage { for representation in image.representations { + if let updatedResource = representation.resource as? CloudPhotoSizeMediaResource, let previousResource = resource as? CloudPhotoSizeMediaResource { + if updatedResource.photoId == previousResource.photoId && updatedResource.sizeSpec == previousResource.sizeSpec { + return representation.resource + } + } if representation.resource.id.isEqual(to: resource.id) { return representation.resource } @@ -81,33 +99,15 @@ private func findMediaResource(media: Media, previousMedia: Media?, resource: Me return representation.resource } } - if let legacyResource = resource as? CloudFileMediaResource { - for representation in image.representations { - if let updatedResource = representation.resource as? CloudPhotoSizeMediaResource { - if updatedResource.localId == legacyResource.localId && updatedResource.volumeId == legacyResource.volumeId { - return representation.resource - } - } - } - } } else if let file = media as? TelegramMediaFile { - if file.resource.id.isEqual(to: resource.id) { + if areResourcesEqual(file.resource, resource) { return file.resource } else { for representation in file.previewRepresentations { - if representation.resource.id.isEqual(to: resource.id) { + if areResourcesEqual(representation.resource, resource) { return representation.resource } } - if let legacyResource = resource as? CloudFileMediaResource { - for representation in file.previewRepresentations { - if let updatedResource = representation.resource as? CloudDocumentSizeMediaResource { - if updatedResource.localId == legacyResource.localId && updatedResource.volumeId == legacyResource.volumeId { - return representation.resource - } - } - } - } } } else if let webPage = media as? TelegramMediaWebpage, case let .Loaded(content) = webPage.content { if let image = content.image, let result = findMediaResource(media: image, previousMedia: previousMedia, resource: resource) { @@ -382,7 +382,7 @@ final class MediaReferenceRevalidationContext { func peer(postbox: Postbox, network: Network, background: Bool, peer: PeerReference) -> Signal { return self.genericItem(key: .peer(peer: peer), background: background, request: { next, error in - return (updatedRemotePeer(postbox: postbox, network: network, peer: peer) + return (_internal_updatedRemotePeer(postbox: postbox, network: network, peer: peer) |> mapError { _ -> RevalidateMediaReferenceError in return .generic }).start(next: { value in @@ -400,7 +400,13 @@ final class MediaReferenceRevalidationContext { } func wallpapers(postbox: Postbox, network: Network, background: Bool, wallpaper: WallpaperReference?) -> Signal<[TelegramWallpaper], RevalidateMediaReferenceError> { - return self.genericItem(key: .wallpapers, background: background, request: { next, error in + let key: MediaReferenceRevalidationKey + if let wallpaper = wallpaper { + key = .wallpaper(wallpaper: wallpaper) + } else { + key = .wallpapers + } + return self.genericItem(key: key, background: background, request: { next, error in let signal: Signal<[TelegramWallpaper]?, RevalidateMediaReferenceError> if let wallpaper = wallpaper, case let .slug(slug) = wallpaper { signal = getWallpaper(network: network, slug: slug) @@ -412,7 +418,6 @@ final class MediaReferenceRevalidationContext { signal = telegramWallpapers(postbox: postbox, network: network, forceUpdate: true) |> last |> mapError { _ -> RevalidateMediaReferenceError in - return .generic } } return (signal @@ -456,7 +461,7 @@ final class MediaReferenceRevalidationContext { func peerAvatars(postbox: Postbox, network: Network, background: Bool, peer: PeerReference) -> Signal<[TelegramPeerPhoto], RevalidateMediaReferenceError> { return self.genericItem(key: .peerAvatars(peer: peer), background: background, request: { next, error in - return (requestPeerPhotos(postbox: postbox, network: network, peerId: peer.id) + return (_internal_requestPeerPhotos(postbox: postbox, network: network, peerId: peer.id) |> mapError { _ -> RevalidateMediaReferenceError in return .generic }).start(next: { value in @@ -618,19 +623,15 @@ func revalidateMediaResourceReference(postbox: Postbox, network: Network, revali return revalidationContext.peer(postbox: postbox, network: network, background: info.preferBackgroundReferenceRevalidation, peer: peer) |> mapToSignal { updatedPeer -> Signal in for representation in updatedPeer.profileImageRepresentations { + if let updatedResource = representation.resource as? CloudPeerPhotoSizeMediaResource, let previousResource = resource as? CloudPeerPhotoSizeMediaResource { + if updatedResource.sizeSpec == previousResource.sizeSpec { + return .single(RevalidatedMediaResource(updatedResource: representation.resource, updatedReference: nil)) + } + } if representation.resource.id.isEqual(to: resource.id) { return .single(RevalidatedMediaResource(updatedResource: representation.resource, updatedReference: nil)) } } - if let legacyResource = resource as? CloudFileMediaResource { - for representation in updatedPeer.profileImageRepresentations { - if let updatedResource = representation.resource as? CloudPeerPhotoSizeMediaResource { - if updatedResource.localId == legacyResource.localId && updatedResource.volumeId == legacyResource.volumeId { - return .single(RevalidatedMediaResource(updatedResource: updatedResource, updatedReference: nil)) - } - } - } - } return .fail(.generic) } case let .avatarList(peer, _): @@ -693,12 +694,8 @@ func revalidateMediaResourceReference(postbox: Postbox, network: Network, revali if thumbnail.resource.id.isEqual(to: resource.id) { return .single(RevalidatedMediaResource(updatedResource: thumbnail.resource, updatedReference: nil)) } - if let legacyResource = resource as? CloudFileMediaResource { - if let updatedResource = thumbnail.resource as? CloudStickerPackThumbnailMediaResource { - if updatedResource.localId == legacyResource.localId && updatedResource.volumeId == legacyResource.volumeId { - return .single(RevalidatedMediaResource(updatedResource: updatedResource, updatedReference: nil)) - } - } + if let _ = thumbnail.resource as? CloudStickerPackThumbnailMediaResource, let _ = resource as? CloudStickerPackThumbnailMediaResource { + return .single(RevalidatedMediaResource(updatedResource: thumbnail.resource, updatedReference: nil)) } } return .fail(.generic) diff --git a/submodules/TelegramCore/Sources/MultipartFetch.swift b/submodules/TelegramCore/Sources/Network/MultipartFetch.swift similarity index 99% rename from submodules/TelegramCore/Sources/MultipartFetch.swift rename to submodules/TelegramCore/Sources/Network/MultipartFetch.swift index 3b777d69e1..c5f525144e 100644 --- a/submodules/TelegramCore/Sources/MultipartFetch.swift +++ b/submodules/TelegramCore/Sources/Network/MultipartFetch.swift @@ -445,7 +445,7 @@ private final class MultipartFetchManager { init(resource: TelegramMediaResource, parameters: MediaResourceFetchParameters?, size: Int?, intervals: Signal<[(Range, MediaBoxFetchPriority)], NoError>, encryptionKey: SecretFileEncryptionKey?, decryptedSize: Int32?, location: MultipartFetchMasterLocation, postbox: Postbox, network: Network, revalidationContext: MediaReferenceRevalidationContext, partReady: @escaping (Int, Data) -> Void, reportCompleteSize: @escaping (Int) -> Void) { self.resource = resource self.parameters = parameters - self.consumerId = arc4random64() + self.consumerId = Int64.random(in: Int64.min ... Int64.max) self.completeSize = size if let size = size { @@ -454,7 +454,7 @@ private final class MultipartFetchManager { self.parallelParts = 4 * 4 } else { self.defaultPartSize = 128 * 1024 - self.parallelParts = 4 + self.parallelParts = 8 } } else { self.parallelParts = 1 @@ -752,10 +752,9 @@ func multipartFetch(postbox: Postbox, network: Network, mediaReferenceRevalidati if let location = resource.apiInputLocation(peerReference: peer) { return .location(location) } else { - return .none + return .revalidate } case .messageAuthorAvatar: - return .revalidate default: return .none @@ -769,7 +768,7 @@ func multipartFetch(postbox: Postbox, network: Network, mediaReferenceRevalidati if let location = resource.apiInputLocation(packReference: stickerPack) { return .location(location) } else { - return .none + return .revalidate } default: return .none diff --git a/submodules/TelegramCore/Sources/MultipartUpload.swift b/submodules/TelegramCore/Sources/Network/MultipartUpload.swift similarity index 99% rename from submodules/TelegramCore/Sources/MultipartUpload.swift rename to submodules/TelegramCore/Sources/Network/MultipartUpload.swift index 359f815c11..7ac8cb2005 100644 --- a/submodules/TelegramCore/Sources/MultipartUpload.swift +++ b/submodules/TelegramCore/Sources/Network/MultipartUpload.swift @@ -395,7 +395,7 @@ func multipartUpload(network: Network, postbox: Postbox, source: MultipartUpload let uploadInterface: Signal if useMultiplexedRequests { - uploadInterface = .single(.multiplexed(manager: network.multiplexedRequestManager, datacenterId: network.datacenterId, consumerId: arc4random64())) + uploadInterface = .single(.multiplexed(manager: network.multiplexedRequestManager, datacenterId: network.datacenterId, consumerId: Int64.random(in: Int64.min ... Int64.max))) } else { uploadInterface = network.upload(tag: tag) |> map { download -> UploadInterface in diff --git a/submodules/TelegramCore/Sources/MultiplexedRequestManager.swift b/submodules/TelegramCore/Sources/Network/MultiplexedRequestManager.swift similarity index 100% rename from submodules/TelegramCore/Sources/MultiplexedRequestManager.swift rename to submodules/TelegramCore/Sources/Network/MultiplexedRequestManager.swift diff --git a/submodules/TelegramCore/Sources/Network.swift b/submodules/TelegramCore/Sources/Network/Network.swift similarity index 100% rename from submodules/TelegramCore/Sources/Network.swift rename to submodules/TelegramCore/Sources/Network/Network.swift diff --git a/submodules/TelegramCore/Sources/NetworkType.swift b/submodules/TelegramCore/Sources/Network/NetworkType.swift similarity index 100% rename from submodules/TelegramCore/Sources/NetworkType.swift rename to submodules/TelegramCore/Sources/Network/NetworkType.swift diff --git a/submodules/TelegramCore/Sources/ProxyServersStatuses.swift b/submodules/TelegramCore/Sources/Network/ProxyServersStatuses.swift similarity index 100% rename from submodules/TelegramCore/Sources/ProxyServersStatuses.swift rename to submodules/TelegramCore/Sources/Network/ProxyServersStatuses.swift diff --git a/submodules/TelegramCore/Sources/PeerParticipants.swift b/submodules/TelegramCore/Sources/PeerParticipants.swift deleted file mode 100644 index 6f12d25956..0000000000 --- a/submodules/TelegramCore/Sources/PeerParticipants.swift +++ /dev/null @@ -1,40 +0,0 @@ -import Foundation -import Postbox -import SwiftSignalKit - -import SyncCore - -private struct PeerParticipants: Equatable { - let peers: [Peer] - - static func ==(lhs: PeerParticipants, rhs: PeerParticipants) -> Bool { - if lhs.peers.count != rhs.peers.count { - return false - } - for i in 0 ..< lhs.peers.count { - if !lhs.peers[i].isEqual(rhs.peers[i]) { - return false - } - } - return true - } -} - -public func peerParticipants(postbox: Postbox, id: PeerId) -> Signal<[Peer], NoError> { - return postbox.peerView(id: id) |> map { view -> PeerParticipants in - if let cachedGroupData = view.cachedData as? CachedGroupData, let participants = cachedGroupData.participants { - var peers: [Peer] = [] - for participant in participants.participants { - if let peer = view.peers[participant.peerId] { - peers.append(peer) - } - } - return PeerParticipants(peers: peers) - } else { - return PeerParticipants(peers: []) - } - } - |> distinctUntilChanged |> map { participants in - return participants.peers - } -} diff --git a/submodules/TelegramCore/Sources/PeerStatistics.swift b/submodules/TelegramCore/Sources/PeerStatistics.swift index 83f9383e9a..21726144ca 100644 --- a/submodules/TelegramCore/Sources/PeerStatistics.swift +++ b/submodules/TelegramCore/Sources/PeerStatistics.swift @@ -1065,7 +1065,7 @@ extension GroupStatsTopPoster { init(apiStatsGroupTopPoster: Api.StatsGroupTopPoster) { switch apiStatsGroupTopPoster { case let .statsGroupTopPoster(userId, messages, avgChars): - self = GroupStatsTopPoster(peerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: userId), messageCount: messages, averageChars: avgChars) + self = GroupStatsTopPoster(peerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt32Value(userId)), messageCount: messages, averageChars: avgChars) } } } @@ -1074,7 +1074,7 @@ extension GroupStatsTopAdmin { init(apiStatsGroupTopAdmin: Api.StatsGroupTopAdmin) { switch apiStatsGroupTopAdmin { case let .statsGroupTopAdmin(userId, deleted, kicked, banned): - self = GroupStatsTopAdmin(peerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: userId), deletedCount: deleted, kickedCount: kicked, bannedCount: banned) + self = GroupStatsTopAdmin(peerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt32Value(userId)), deletedCount: deleted, kickedCount: kicked, bannedCount: banned) } } } @@ -1083,7 +1083,7 @@ extension GroupStatsTopInviter { init(apiStatsGroupTopInviter: Api.StatsGroupTopInviter) { switch apiStatsGroupTopInviter { case let .statsGroupTopInviter(userId, invitations): - self = GroupStatsTopInviter(peerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: userId), inviteCount: invitations) + self = GroupStatsTopInviter(peerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt32Value(userId)), inviteCount: invitations) } } } diff --git a/submodules/TelegramCore/Sources/ChatUpdatingMessageMedia.swift b/submodules/TelegramCore/Sources/PendingMessages/ChatUpdatingMessageMedia.swift similarity index 100% rename from submodules/TelegramCore/Sources/ChatUpdatingMessageMedia.swift rename to submodules/TelegramCore/Sources/PendingMessages/ChatUpdatingMessageMedia.swift diff --git a/submodules/TelegramCore/Sources/EnqueueMessage.swift b/submodules/TelegramCore/Sources/PendingMessages/EnqueueMessage.swift similarity index 92% rename from submodules/TelegramCore/Sources/EnqueueMessage.swift rename to submodules/TelegramCore/Sources/PendingMessages/EnqueueMessage.swift index 6c5fddddc3..c664d3e96a 100644 --- a/submodules/TelegramCore/Sources/EnqueueMessage.swift +++ b/submodules/TelegramCore/Sources/PendingMessages/EnqueueMessage.swift @@ -11,13 +11,13 @@ public enum EnqueueMessageGrouping { } public enum EnqueueMessage { - case message(text: String, attributes: [MessageAttribute], mediaReference: AnyMediaReference?, replyToMessageId: MessageId?, localGroupingKey: Int64?) - case forward(source: MessageId, grouping: EnqueueMessageGrouping, attributes: [MessageAttribute]) + case message(text: String, attributes: [MessageAttribute], mediaReference: AnyMediaReference?, replyToMessageId: MessageId?, localGroupingKey: Int64?, correlationId: Int64?) + case forward(source: MessageId, grouping: EnqueueMessageGrouping, attributes: [MessageAttribute], correlationId: Int64?) public func withUpdatedReplyToMessageId(_ replyToMessageId: MessageId?) -> EnqueueMessage { switch self { - case let .message(text, attributes, mediaReference, _, localGroupingKey): - return .message(text: text, attributes: attributes, mediaReference: mediaReference, replyToMessageId: replyToMessageId, localGroupingKey: localGroupingKey) + case let .message(text, attributes, mediaReference, _, localGroupingKey, correlationId): + return .message(text: text, attributes: attributes, mediaReference: mediaReference, replyToMessageId: replyToMessageId, localGroupingKey: localGroupingKey, correlationId: correlationId) case .forward: return self } @@ -25,21 +25,41 @@ public enum EnqueueMessage { public func withUpdatedAttributes(_ f: ([MessageAttribute]) -> [MessageAttribute]) -> EnqueueMessage { switch self { - case let .message(text, attributes, mediaReference, replyToMessageId, localGroupingKey): - return .message(text: text, attributes: f(attributes), mediaReference: mediaReference, replyToMessageId: replyToMessageId, localGroupingKey: localGroupingKey) - case let .forward(source, grouping, attributes): - return .forward(source: source, grouping: grouping, attributes: f(attributes)) + case let .message(text, attributes, mediaReference, replyToMessageId, localGroupingKey, correlationId): + return .message(text: text, attributes: f(attributes), mediaReference: mediaReference, replyToMessageId: replyToMessageId, localGroupingKey: localGroupingKey, correlationId: correlationId) + case let .forward(source, grouping, attributes, correlationId): + return .forward(source: source, grouping: grouping, attributes: f(attributes), correlationId: correlationId) } } public func withUpdatedGroupingKey(_ f: (Int64?) -> Int64?) -> EnqueueMessage { switch self { - case let .message(text, attributes, mediaReference, replyToMessageId, localGroupingKey): - return .message(text: text, attributes: attributes, mediaReference: mediaReference, replyToMessageId: replyToMessageId, localGroupingKey: f(localGroupingKey)) + case let .message(text, attributes, mediaReference, replyToMessageId, localGroupingKey, correlationId): + return .message(text: text, attributes: attributes, mediaReference: mediaReference, replyToMessageId: replyToMessageId, localGroupingKey: f(localGroupingKey), correlationId: correlationId) case .forward: return self } } + + public func withUpdatedCorrelationId(_ value: Int64?) -> EnqueueMessage { + switch self { + case let .message(text, attributes, mediaReference, replyToMessageId, localGroupingKey, _): + return .message(text: text, attributes: attributes, mediaReference: mediaReference, replyToMessageId: replyToMessageId, localGroupingKey: localGroupingKey, correlationId: value) + case let .forward(source, grouping, attributes, _): + return .forward(source: source, grouping: grouping, attributes: attributes, correlationId: value) + } + } +} + +private extension EnqueueMessage { + var correlationId: Int64? { + switch self { + case let .message(_, _, _, _, _, correlationId): + return correlationId + case let .forward(_, _, _, correlationId): + return correlationId + } + } } func augmentMediaWithReference(_ mediaReference: AnyMediaReference) -> Media { @@ -62,9 +82,9 @@ func augmentMediaWithReference(_ mediaReference: AnyMediaReference) -> Media { 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, videoThumbnails: file.videoThumbnails, immediateThumbnailData: file.immediateThumbnailData, mimeType: file.mimeType, size: file.size, attributes: file.attributes) + return TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: Int64.random(in: Int64.min ... Int64.max)), partialReference: file.partialReference, resource: file.resource, previewRepresentations: file.previewRepresentations, videoThumbnails: file.videoThumbnails, 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, flags: []) + return TelegramMediaImage(imageId: MediaId(namespace: Namespaces.Media.LocalImage, id: Int64.random(in: Int64.min ... Int64.max)), representations: image.representations, immediateThumbnailData: image.immediateThumbnailData, reference: image.reference, partialReference: image.partialReference, flags: []) } else { return media } @@ -139,7 +159,7 @@ private func opportunisticallyTransformOutgoingMedia(network: Network, postbox: var hasMedia = false loop: for message in messages { switch message { - case let .message(_, _, mediaReference, _, _): + case let .message(_, _, mediaReference, _, _, _): if mediaReference != nil { hasMedia = true break loop @@ -156,14 +176,14 @@ private func opportunisticallyTransformOutgoingMedia(network: Network, postbox: var signals: [Signal<(Bool, EnqueueMessage), NoError>] = [] for message in messages { switch message { - case let .message(text, attributes, mediaReference, replyToMessageId, localGroupingKey): + case let .message(text, attributes, mediaReference, replyToMessageId, localGroupingKey, correlationId): if let mediaReference = mediaReference { signals.append(opportunisticallyTransformMessageWithMedia(network: network, postbox: postbox, transformOutgoingMessageMedia: transformOutgoingMessageMedia, mediaReference: mediaReference, userInteractive: userInteractive) |> map { result -> (Bool, EnqueueMessage) in if let result = result { - return (true, .message(text: text, attributes: attributes, mediaReference: .standalone(media: result.media), replyToMessageId: replyToMessageId, localGroupingKey: localGroupingKey)) + return (true, .message(text: text, attributes: attributes, mediaReference: .standalone(media: result.media), replyToMessageId: replyToMessageId, localGroupingKey: localGroupingKey, correlationId: correlationId)) } else { - return (false, .message(text: text, attributes: attributes, mediaReference: mediaReference, replyToMessageId: replyToMessageId, localGroupingKey: localGroupingKey)) + return (false, .message(text: text, attributes: attributes, mediaReference: mediaReference, replyToMessageId: replyToMessageId, localGroupingKey: localGroupingKey, correlationId: correlationId)) } }) } else { @@ -235,12 +255,12 @@ public func resendMessages(account: Account, messageIds: [MessageId]) -> Signal< } } - messages.append(.message(text: message.text, attributes: filteredAttributes, mediaReference: message.media.first.flatMap(AnyMediaReference.standalone), replyToMessageId: replyToMessageId, localGroupingKey: message.groupingKey)) + messages.append(.message(text: message.text, attributes: filteredAttributes, mediaReference: message.media.first.flatMap(AnyMediaReference.standalone), replyToMessageId: replyToMessageId, localGroupingKey: message.groupingKey, correlationId: nil)) } } let _ = enqueueMessages(transaction: transaction, account: account, peerId: peerId, messages: messages.map { (false, $0) }) } - deleteMessages(transaction: transaction, mediaBox: account.postbox.mediaBox, ids: removeMessageIds, deleteMedia: false) + _internal_deleteMessages(transaction: transaction, mediaBox: account.postbox.mediaBox, ids: removeMessageIds, deleteMedia: false) } } @@ -258,7 +278,7 @@ func enqueueMessages(transaction: Transaction, account: Account, peerId: PeerId, } } switch message { - case let .message(_, _, _, replyToMessageId, _): + case let .message(_, _, _, replyToMessageId, _, _): if let replyToMessageId = replyToMessageId, replyToMessageId.peerId != peerId, let replyMessage = transaction.getMessage(replyToMessageId) { var canBeForwarded = true if replyMessage.id.namespace != Namespaces.Message.Cloud { @@ -271,10 +291,10 @@ func enqueueMessages(transaction: Transaction, account: Account, peerId: PeerId, } } if canBeForwarded { - updatedMessages.append((true, .forward(source: replyToMessageId, grouping: .none, attributes: []))) + updatedMessages.append((true, .forward(source: replyToMessageId, grouping: .none, attributes: [], correlationId: nil))) } } - case let .forward(sourceId, _, _): + case let .forward(sourceId, _, _, _): if let sourceMessage = forwardedMessageToBeReuploaded(transaction: transaction, id: sourceId) { var mediaReference: AnyMediaReference? if sourceMessage.id.peerId.namespace == Namespaces.Peer.SecretChat { @@ -282,7 +302,7 @@ func enqueueMessages(transaction: Transaction, account: Account, peerId: PeerId, mediaReference = .standalone(media: media) } } - updatedMessages.append((transformedMedia, .message(text: sourceMessage.text, attributes: sourceMessage.attributes, mediaReference: mediaReference, replyToMessageId: nil, localGroupingKey: nil))) + updatedMessages.append((transformedMedia, .message(text: sourceMessage.text, attributes: sourceMessage.attributes, mediaReference: mediaReference, replyToMessageId: nil, localGroupingKey: nil, correlationId: nil))) continue outer } } @@ -319,11 +339,11 @@ func enqueueMessages(transaction: Transaction, account: Account, peerId: PeerId, if transformedMedia { infoFlags.insert(.transformedMedia) } - attributes.append(OutgoingMessageInfoAttribute(uniqueId: randomId, flags: infoFlags, acknowledged: false)) + attributes.append(OutgoingMessageInfoAttribute(uniqueId: randomId, flags: infoFlags, acknowledged: false, correlationId: message.correlationId)) globallyUniqueIds.append(randomId) switch message { - case let .message(text, requestedAttributes, mediaReference, replyToMessageId, localGroupingKey): + case let .message(text, requestedAttributes, mediaReference, replyToMessageId, localGroupingKey, correlationId): var peerAutoremoveTimeout: Int32? if let peer = peer as? TelegramSecretChat { var isAction = false @@ -495,7 +515,7 @@ func enqueueMessages(transaction: Transaction, account: Account, peerId: PeerId, } storeMessages.append(StoreMessage(peerId: peerId, namespace: messageNamespace, globallyUniqueId: randomId, groupingKey: localGroupingKey, threadId: threadId, timestamp: effectiveTimestamp, flags: flags, tags: tags, globalTags: globalTags, localTags: localTags, forwardInfo: nil, authorId: authorId, text: text, attributes: attributes, media: mediaList)) - case let .forward(source, grouping, requestedAttributes): + case let .forward(source, grouping, requestedAttributes, correlationId): let sourceMessage = transaction.getMessage(source) if let sourceMessage = sourceMessage, let author = sourceMessage.author ?? sourceMessage.peers[sourceMessage.id.peerId] { if let peer = peer as? TelegramSecretChat { @@ -583,7 +603,7 @@ func enqueueMessages(transaction: Transaction, account: Account, peerId: PeerId, } if !rows.isEmpty { - attributes.append(ReplyMarkupMessageAttribute(rows: rows, flags: sourceReplyMarkup.flags)) + attributes.append(ReplyMarkupMessageAttribute(rows: rows, flags: sourceReplyMarkup.flags, placeholder: sourceReplyMarkup.placeholder)) } } @@ -676,7 +696,7 @@ func enqueueMessages(transaction: Transaction, account: Account, peerId: PeerId, if let generatedKey = localGroupingKeyBySourceKey[groupingKey] { localGroupingKey = generatedKey } else { - let generatedKey = arc4random64() + let generatedKey = Int64.random(in: Int64.min ... Int64.max) localGroupingKeyBySourceKey[groupingKey] = generatedKey localGroupingKey = generatedKey } diff --git a/submodules/TelegramCore/Sources/PendingMessageUploadedContent.swift b/submodules/TelegramCore/Sources/PendingMessages/PendingMessageUploadedContent.swift similarity index 99% rename from submodules/TelegramCore/Sources/PendingMessageUploadedContent.swift rename to submodules/TelegramCore/Sources/PendingMessages/PendingMessageUploadedContent.swift index 0de3aa8a3a..08dc51b7df 100644 --- a/submodules/TelegramCore/Sources/PendingMessageUploadedContent.swift +++ b/submodules/TelegramCore/Sources/PendingMessages/PendingMessageUploadedContent.swift @@ -374,7 +374,7 @@ private func uploadedMediaImageContent(network: Network, postbox: Postbox, trans let attribute = updatedAttributes[index] as! OutgoingMessageInfoAttribute updatedAttributes[index] = attribute.withUpdatedFlags(attribute.flags.union([.transformedMedia])) } else { - updatedAttributes.append(OutgoingMessageInfoAttribute(uniqueId: arc4random64(), flags: [.transformedMedia], acknowledged: false)) + updatedAttributes.append(OutgoingMessageInfoAttribute(uniqueId: Int64.random(in: Int64.min ... Int64.max), flags: [.transformedMedia], acknowledged: false, correlationId: nil)) } return .update(StoreMessage(id: currentMessage.id, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, threadId: currentMessage.threadId, timestamp: currentMessage.timestamp, flags: StoreMessageFlags(currentMessage.flags), tags: currentMessage.tags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: storeForwardInfo, authorId: currentMessage.author?.id, text: currentMessage.text, attributes: updatedAttributes, media: currentMessage.media)) }) @@ -664,7 +664,7 @@ private func uploadedMediaFileContent(network: Network, postbox: Postbox, auxili let attribute = updatedAttributes[index] as! OutgoingMessageInfoAttribute updatedAttributes[index] = attribute.withUpdatedFlags(attribute.flags.union([.transformedMedia])) } else { - updatedAttributes.append(OutgoingMessageInfoAttribute(uniqueId: arc4random64(), flags: [.transformedMedia], acknowledged: false)) + updatedAttributes.append(OutgoingMessageInfoAttribute(uniqueId: Int64.random(in: Int64.min ... Int64.max), flags: [.transformedMedia], acknowledged: false, correlationId: nil)) } return .update(StoreMessage(id: currentMessage.id, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, threadId: currentMessage.threadId, timestamp: currentMessage.timestamp, flags: StoreMessageFlags(currentMessage.flags), tags: currentMessage.tags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: storeForwardInfo, authorId: currentMessage.author?.id, text: currentMessage.text, attributes: updatedAttributes, media: currentMessage.media)) }) diff --git a/submodules/TelegramCore/Sources/PendingUpdateMessageManager.swift b/submodules/TelegramCore/Sources/PendingMessages/PendingUpdateMessageManager.swift similarity index 100% rename from submodules/TelegramCore/Sources/PendingUpdateMessageManager.swift rename to submodules/TelegramCore/Sources/PendingMessages/PendingUpdateMessageManager.swift diff --git a/submodules/TelegramCore/Sources/RequestEditMessage.swift b/submodules/TelegramCore/Sources/PendingMessages/RequestEditMessage.swift similarity index 96% rename from submodules/TelegramCore/Sources/RequestEditMessage.swift rename to submodules/TelegramCore/Sources/PendingMessages/RequestEditMessage.swift index 69ffc23c06..a97ce331bb 100644 --- a/submodules/TelegramCore/Sources/RequestEditMessage.swift +++ b/submodules/TelegramCore/Sources/PendingMessages/RequestEditMessage.swift @@ -28,7 +28,7 @@ public enum RequestEditMessageError { case invalidGrouping } -public func requestEditMessage(account: Account, messageId: MessageId, text: String, media: RequestEditMessageMedia, entities: TextEntitiesMessageAttribute? = nil, disableUrlPreview: Bool = false, scheduleTime: Int32? = nil) -> Signal { +func _internal_requestEditMessage(account: Account, messageId: MessageId, text: String, media: RequestEditMessageMedia, entities: TextEntitiesMessageAttribute? = nil, disableUrlPreview: Bool = false, scheduleTime: Int32? = nil) -> Signal { 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) } @@ -255,7 +255,7 @@ private func requestEditMessageInternal(postbox: Postbox, network: Network, stat } } -public func requestEditLiveLocation(postbox: Postbox, network: Network, stateManager: AccountStateManager, messageId: MessageId, stop: Bool, coordinate: (latitude: Double, longitude: Double, accuracyRadius: Int32?)?, heading: Int32?, proximityNotificationRadius: Int32?) -> Signal { +func _internal_requestEditLiveLocation(postbox: Postbox, network: Network, stateManager: AccountStateManager, messageId: MessageId, stop: Bool, coordinate: (latitude: Double, longitude: Double, accuracyRadius: Int32?)?, heading: Int32?, proximityNotificationRadius: Int32?) -> Signal { return postbox.transaction { transaction -> (Api.InputPeer, TelegramMediaMap)? in guard let inputPeer = transaction.getPeer(messageId.peerId).flatMap(apiInputPeer) else { return nil diff --git a/submodules/TelegramCore/Sources/StandaloneSendMessage.swift b/submodules/TelegramCore/Sources/PendingMessages/StandaloneSendMessage.swift similarity index 99% rename from submodules/TelegramCore/Sources/StandaloneSendMessage.swift rename to submodules/TelegramCore/Sources/PendingMessages/StandaloneSendMessage.swift index f43e7c9b91..bbba34b3a7 100644 --- a/submodules/TelegramCore/Sources/StandaloneSendMessage.swift +++ b/submodules/TelegramCore/Sources/PendingMessages/StandaloneSendMessage.swift @@ -74,7 +74,7 @@ private func sendMessageContent(account: Account, peerId: PeerId, attributes: [M if peerId.namespace == Namespaces.Peer.SecretChat { return .complete() } else if let peer = transaction.getPeer(peerId), let inputPeer = apiInputPeer(peer) { - var uniqueId: Int64 = arc4random64() + var uniqueId: Int64 = Int64.random(in: Int64.min ... Int64.max) //var forwardSourceInfoAttribute: ForwardSourceInfoAttribute? var messageEntities: [Api.MessageEntity]? var replyMessageId: Int32? diff --git a/submodules/TelegramCore/Sources/StandaloneUploadedMedia.swift b/submodules/TelegramCore/Sources/PendingMessages/StandaloneUploadedMedia.swift similarity index 92% rename from submodules/TelegramCore/Sources/StandaloneUploadedMedia.swift rename to submodules/TelegramCore/Sources/PendingMessages/StandaloneUploadedMedia.swift index a704f06335..16831f3f70 100644 --- a/submodules/TelegramCore/Sources/StandaloneUploadedMedia.swift +++ b/submodules/TelegramCore/Sources/PendingMessages/StandaloneUploadedMedia.swift @@ -87,7 +87,7 @@ public func standaloneUploadedImage(account: Account, peerId: PeerId, text: Stri case let .inputSecretFile(file, _, key): return account.postbox.transaction { transaction -> Api.InputEncryptedChat? in if let peer = transaction.getPeer(peerId) as? TelegramSecretChat { - return Api.InputEncryptedChat.inputEncryptedChat(chatId: peer.id.id, accessHash: peer.accessHash) + return Api.InputEncryptedChat.inputEncryptedChat(chatId: peer.id.id._internalGetInt32Value(), accessHash: peer.accessHash) } return nil } @@ -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), progressiveSizes: [])], immediateThumbnailData: nil, reference: nil, partialReference: nil, flags: []))))) + return .single(.result(.media(.standalone(media: TelegramMediaImage(imageId: MediaId(namespace: Namespaces.Media.LocalImage, id: Int64.random(in: Int64.min ... Int64.max)), representations: [TelegramMediaImageRepresentation(dimensions: dimensions, resource: SecretFileMediaResource(fileId: id, accessHash: accessHash, containerSize: size, decryptedSize: Int32(data.count), datacenterId: Int(dcId), key: key), progressiveSizes: [], immediateThumbnailData: nil)], immediateThumbnailData: nil, reference: nil, partialReference: nil, flags: []))))) case .encryptedFileEmpty: return .fail(.generic) } @@ -181,7 +181,7 @@ public func standaloneUploadedFile(account: Account, peerId: PeerId, text: Strin case let .inputSecretFile(file, _, key): return account.postbox.transaction { transaction -> Api.InputEncryptedChat? in if let peer = transaction.getPeer(peerId) as? TelegramSecretChat { - return Api.InputEncryptedChat.inputEncryptedChat(chatId: peer.id.id, accessHash: peer.accessHash) + return Api.InputEncryptedChat.inputEncryptedChat(chatId: peer.id.id._internalGetInt32Value(), accessHash: peer.accessHash) } return nil } @@ -195,7 +195,7 @@ public func standaloneUploadedFile(account: Account, peerId: PeerId, text: Strin |> mapToSignal { result -> Signal in switch result { case let .encryptedFile(id, accessHash, size, dcId, _): - let media = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: arc4random64()), partialReference: nil, resource: SecretFileMediaResource(fileId: id, accessHash: accessHash, containerSize: size, decryptedSize: size, datacenterId: Int(dcId), key: key), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: mimeType, size: Int(size), attributes: attributes) + let media = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: Int64.random(in: Int64.min ... Int64.max)), partialReference: nil, resource: SecretFileMediaResource(fileId: id, accessHash: accessHash, containerSize: size, decryptedSize: size, datacenterId: Int(dcId), key: key), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: mimeType, size: Int(size), attributes: attributes) return .single(.result(.media(.standalone(media: media)))) case .encryptedFileEmpty: diff --git a/submodules/TelegramCore/Sources/Random.swift b/submodules/TelegramCore/Sources/Random.swift deleted file mode 100644 index 20bf912990..0000000000 --- a/submodules/TelegramCore/Sources/Random.swift +++ /dev/null @@ -1,7 +0,0 @@ -import Foundation - -public func arc4random64() -> Int64 { - var value: Int64 = 0 - arc4random_buf(&value, 8) - return value -} diff --git a/submodules/TelegramCore/Sources/Regex.swift b/submodules/TelegramCore/Sources/Regex.swift deleted file mode 100644 index 474e383e1f..0000000000 --- a/submodules/TelegramCore/Sources/Regex.swift +++ /dev/null @@ -1,33 +0,0 @@ -import Foundation - -public struct Regex { - let pattern: String - let options: NSRegularExpression.Options! - - private var matcher: NSRegularExpression { - return try! NSRegularExpression(pattern: self.pattern, options: self.options) - } - - public init(_ pattern: String) { - self.pattern = pattern - self.options = [] - } - - public func match(_ string: String, options: NSRegularExpression.MatchingOptions = []) -> Bool { - return self.matcher.numberOfMatches(in: string, options: options, range: NSMakeRange(0, string.utf16.count)) != 0 - } -} - -public protocol RegularExpressionMatchable { - func match(_ regex: Regex) -> Bool -} - -extension String: RegularExpressionMatchable { - public func match(_ regex: Regex) -> Bool { - return regex.match(self) - } -} - -public func ~=(pattern: Regex, matchable: T) -> Bool { - return matchable.match(pattern) -} diff --git a/submodules/TelegramCore/Sources/RequestPhoneNumber.swift b/submodules/TelegramCore/Sources/RequestPhoneNumber.swift deleted file mode 100644 index 2842feaa2e..0000000000 --- a/submodules/TelegramCore/Sources/RequestPhoneNumber.swift +++ /dev/null @@ -1,28 +0,0 @@ -import Foundation -import Postbox -import SwiftSignalKit -import TelegramApi - -public func requestPhoneNumber(account: Account, peerId: PeerId) -> Signal { - return .never() - /*return account.postbox.transaction { transaction -> Api.InputPeer? in - return transaction.getPeer(peerId).flatMap(apiInputPeer) - } - |> mapToSignal { inputPeer -> Signal in - guard let inputPeer = inputPeer else { - return .complete() - } - return account.network.request(Api.functions.messages.sendPhoneNumberRequest(peer: inputPeer, randomId: arc4random64())) - |> map(Optional.init) - |> `catch` { _ -> Signal in - return .single(nil) - } - |> mapToSignal { updates -> Signal in - if let updates = updates { - account.stateManager.addUpdates(updates) - } - return .complete() - } - }*/ -} - diff --git a/submodules/TelegramCore/Sources/SecretChatEncryption.swift b/submodules/TelegramCore/Sources/SecretChats/SecretChatEncryption.swift similarity index 100% rename from submodules/TelegramCore/Sources/SecretChatEncryption.swift rename to submodules/TelegramCore/Sources/SecretChats/SecretChatEncryption.swift diff --git a/submodules/TelegramCore/Sources/SecretChatEncryptionConfig.swift b/submodules/TelegramCore/Sources/SecretChats/SecretChatEncryptionConfig.swift similarity index 100% rename from submodules/TelegramCore/Sources/SecretChatEncryptionConfig.swift rename to submodules/TelegramCore/Sources/SecretChats/SecretChatEncryptionConfig.swift diff --git a/submodules/TelegramCore/Sources/SecretChatFileReference.swift b/submodules/TelegramCore/Sources/SecretChats/SecretChatFileReference.swift similarity index 100% rename from submodules/TelegramCore/Sources/SecretChatFileReference.swift rename to submodules/TelegramCore/Sources/SecretChats/SecretChatFileReference.swift diff --git a/submodules/TelegramCore/Sources/SecretChatIncomingEncryptedOperation.swift b/submodules/TelegramCore/Sources/SecretChats/SecretChatIncomingEncryptedOperation.swift similarity index 60% rename from submodules/TelegramCore/Sources/SecretChatIncomingEncryptedOperation.swift rename to submodules/TelegramCore/Sources/SecretChats/SecretChatIncomingEncryptedOperation.swift index ac842093b8..6977f48dad 100644 --- a/submodules/TelegramCore/Sources/SecretChatIncomingEncryptedOperation.swift +++ b/submodules/TelegramCore/Sources/SecretChats/SecretChatIncomingEncryptedOperation.swift @@ -17,9 +17,9 @@ extension SecretChatIncomingEncryptedOperation { convenience init(message: Api.EncryptedMessage) { switch message { case let .encryptedMessage(randomId, chatId, date, bytes, file): - self.init(peerId: PeerId(namespace: Namespaces.Peer.SecretChat, id: chatId), globallyUniqueId: randomId, timestamp: date, type: .message, keyFingerprint: keyFingerprintFromBytes(bytes), contents: MemoryBuffer(bytes), mediaFileReference: SecretChatFileReference(file)) + self.init(peerId: PeerId(namespace: Namespaces.Peer.SecretChat, id: PeerId.Id._internalFromInt32Value(chatId)), globallyUniqueId: randomId, timestamp: date, type: .message, keyFingerprint: keyFingerprintFromBytes(bytes), contents: MemoryBuffer(bytes), mediaFileReference: SecretChatFileReference(file)) case let .encryptedMessageService(randomId, chatId, date, bytes): - self.init(peerId: PeerId(namespace: Namespaces.Peer.SecretChat, id: chatId), globallyUniqueId: randomId, timestamp: date, type: .service, keyFingerprint: keyFingerprintFromBytes(bytes), contents: MemoryBuffer(bytes), mediaFileReference: nil) + self.init(peerId: PeerId(namespace: Namespaces.Peer.SecretChat, id: PeerId.Id._internalFromInt32Value(chatId)), globallyUniqueId: randomId, timestamp: date, type: .service, keyFingerprint: keyFingerprintFromBytes(bytes), contents: MemoryBuffer(bytes), mediaFileReference: nil) } } } diff --git a/submodules/TelegramCore/Sources/SecretChatLayerNegotiation.swift b/submodules/TelegramCore/Sources/SecretChats/SecretChatLayerNegotiation.swift similarity index 90% rename from submodules/TelegramCore/Sources/SecretChatLayerNegotiation.swift rename to submodules/TelegramCore/Sources/SecretChats/SecretChatLayerNegotiation.swift index ed5c0fbad3..474b62523c 100644 --- a/submodules/TelegramCore/Sources/SecretChatLayerNegotiation.swift +++ b/submodules/TelegramCore/Sources/SecretChats/SecretChatLayerNegotiation.swift @@ -23,11 +23,11 @@ func secretChatAddReportCurrentLayerSupportOperationAndUpdateRequestedLayer(tran switch state.embeddedState { case .basicLayer: var updatedState = state - updatedState = addSecretChatOutgoingOperation(transaction: transaction, peerId: peerId, operation: .reportLayerSupport(layer: .layer8, actionGloballyUniqueId: arc4random64(), layerSupport: topSupportedLayer.rawValue), state: updatedState) + updatedState = addSecretChatOutgoingOperation(transaction: transaction, peerId: peerId, operation: .reportLayerSupport(layer: .layer8, actionGloballyUniqueId: Int64.random(in: Int64.min ... Int64.max), layerSupport: topSupportedLayer.rawValue), state: updatedState) return updatedState case let .sequenceBasedLayer(sequenceState): var updatedState = state - updatedState = addSecretChatOutgoingOperation(transaction: transaction, peerId: peerId, operation: .reportLayerSupport(layer: sequenceState.layerNegotiationState.activeLayer.secretChatLayer, actionGloballyUniqueId: arc4random64(), layerSupport: topSupportedLayer.rawValue), state: updatedState) + updatedState = addSecretChatOutgoingOperation(transaction: transaction, peerId: peerId, operation: .reportLayerSupport(layer: sequenceState.layerNegotiationState.activeLayer.secretChatLayer, actionGloballyUniqueId: Int64.random(in: Int64.min ... Int64.max), layerSupport: topSupportedLayer.rawValue), state: updatedState) updatedState = updatedState.withUpdatedEmbeddedState(.sequenceBasedLayer(sequenceState.withUpdatedLayerNegotiationState(sequenceState.layerNegotiationState.withUpdatedLocallyRequestedLayer(topSupportedLayer.rawValue)))) return updatedState default: diff --git a/submodules/TelegramCore/Sources/SecretChatOutgoingOperation.swift b/submodules/TelegramCore/Sources/SecretChats/SecretChatOutgoingOperation.swift similarity index 100% rename from submodules/TelegramCore/Sources/SecretChatOutgoingOperation.swift rename to submodules/TelegramCore/Sources/SecretChats/SecretChatOutgoingOperation.swift diff --git a/submodules/TelegramCore/Sources/SecretChatRekeySession.swift b/submodules/TelegramCore/Sources/SecretChats/SecretChatRekeySession.swift similarity index 90% rename from submodules/TelegramCore/Sources/SecretChatRekeySession.swift rename to submodules/TelegramCore/Sources/SecretChats/SecretChatRekeySession.swift index d48e08b32f..d6feff50bf 100644 --- a/submodules/TelegramCore/Sources/SecretChatRekeySession.swift +++ b/submodules/TelegramCore/Sources/SecretChats/SecretChatRekeySession.swift @@ -16,12 +16,12 @@ func secretChatInitiateRekeySessionIfNeeded(transaction: Transaction, peerId: Pe let tagLocalIndex = transaction.operationLogGetNextEntryLocalIndex(peerId: peerId, tag: OperationLogTags.SecretOutgoing) let canonicalIndex = sequenceState.canonicalOutgoingOperationIndex(tagLocalIndex) if let key = state.keychain.latestKey(validForSequenceBasedCanonicalIndex: canonicalIndex), key.useCount >= keyUseCountThreshold { - let sessionId = arc4random64() + let sessionId = Int64.random(in: Int64.min ... Int64.max) let aBytes = malloc(256)! let _ = SecRandomCopyBytes(nil, 256, aBytes.assumingMemoryBound(to: UInt8.self)) let a = MemoryBuffer(memory: aBytes, capacity: 256, length: 256, freeWhenDone: true) - transaction.operationLogAddEntry(peerId: peerId, tag: OperationLogTags.SecretOutgoing, tagLocalIndex: .automatic, tagMergedIndex: .automatic, contents: SecretChatOutgoingOperation(contents: .pfsRequestKey(layer: sequenceState.layerNegotiationState.activeLayer, actionGloballyUniqueId: arc4random64(), rekeySessionId: sessionId, a: a), mutable: true, delivered: false)) + transaction.operationLogAddEntry(peerId: peerId, tag: OperationLogTags.SecretOutgoing, tagLocalIndex: .automatic, tagMergedIndex: .automatic, contents: SecretChatOutgoingOperation(contents: .pfsRequestKey(layer: sequenceState.layerNegotiationState.activeLayer, actionGloballyUniqueId: Int64.random(in: Int64.min ... Int64.max), rekeySessionId: sessionId, a: a), mutable: true, delivered: false)) return state.withUpdatedEmbeddedState(.sequenceBasedLayer(sequenceState.withUpdatedRekeyState(SecretChatRekeySessionState(id: sessionId, data: .requesting)))) } default: @@ -69,7 +69,7 @@ func secretChatAdvanceRekeySessionIfNeeded(encryptionProvider: EncryptionProvide assert(remoteKeyFingerprint == keyFingerprint) - transaction.operationLogAddEntry(peerId: peerId, tag: OperationLogTags.SecretOutgoing, tagLocalIndex: .automatic, tagMergedIndex: .automatic, contents: SecretChatOutgoingOperation(contents: .pfsCommitKey(layer: sequenceState.layerNegotiationState.activeLayer, actionGloballyUniqueId: arc4random64(), rekeySessionId: rekeySession.id, keyFingerprint: keyFingerprint), mutable: true, delivered: false)) + transaction.operationLogAddEntry(peerId: peerId, tag: OperationLogTags.SecretOutgoing, tagLocalIndex: .automatic, tagMergedIndex: .automatic, contents: SecretChatOutgoingOperation(contents: .pfsCommitKey(layer: sequenceState.layerNegotiationState.activeLayer, actionGloballyUniqueId: Int64.random(in: Int64.min ... Int64.max), rekeySessionId: rekeySession.id, keyFingerprint: keyFingerprint), mutable: true, delivered: false)) let keyValidityOperationIndex = transaction.operationLogGetNextEntryLocalIndex(peerId: peerId, tag: OperationLogTags.SecretOutgoing) let keyValidityOperationCanonicalIndex = sequenceState.canonicalOutgoingOperationIndex(keyValidityOperationIndex) @@ -92,7 +92,7 @@ func secretChatAdvanceRekeySessionIfNeeded(encryptionProvider: EncryptionProvide return SecretChatKey(fingerprint: keyFingerprint, key: key, validity: .sequenceBasedIndexRange(fromCanonicalIndex: keyValidityOperationCanonicalIndex), useCount: 0) })) - transaction.operationLogAddEntry(peerId: peerId, tag: OperationLogTags.SecretOutgoing, tagLocalIndex: .automatic, tagMergedIndex: .automatic, contents: SecretChatOutgoingOperation(contents: .noop(layer: sequenceState.layerNegotiationState.activeLayer, actionGloballyUniqueId: arc4random64()), mutable: true, delivered: false)) + transaction.operationLogAddEntry(peerId: peerId, tag: OperationLogTags.SecretOutgoing, tagLocalIndex: .automatic, tagMergedIndex: .automatic, contents: SecretChatOutgoingOperation(contents: .noop(layer: sequenceState.layerNegotiationState.activeLayer, actionGloballyUniqueId: Int64.random(in: Int64.min ... Int64.max)), mutable: true, delivered: false)) return updatedState } else { @@ -107,7 +107,7 @@ func secretChatAdvanceRekeySessionIfNeeded(encryptionProvider: EncryptionProvide switch rekeySession.data { case .requesting, .requested: if rekeySessionId < rekeySession.id { - transaction.operationLogAddEntry(peerId: peerId, tag: OperationLogTags.SecretOutgoing, tagLocalIndex: .automatic, tagMergedIndex: .automatic, contents: SecretChatOutgoingOperation(contents: .pfsAbortSession(layer: sequenceState.layerNegotiationState.activeLayer, actionGloballyUniqueId: arc4random64(), rekeySessionId: rekeySession.id), mutable: true, delivered: false)) + transaction.operationLogAddEntry(peerId: peerId, tag: OperationLogTags.SecretOutgoing, tagLocalIndex: .automatic, tagMergedIndex: .automatic, contents: SecretChatOutgoingOperation(contents: .pfsAbortSession(layer: sequenceState.layerNegotiationState.activeLayer, actionGloballyUniqueId: Int64.random(in: Int64.min ... Int64.max), rekeySessionId: rekeySession.id), mutable: true, delivered: false)) } else { acceptSession = false } @@ -123,7 +123,7 @@ func secretChatAdvanceRekeySessionIfNeeded(encryptionProvider: EncryptionProvide let rekeySession = SecretChatRekeySessionState(id: rekeySessionId, data: .accepting) - transaction.operationLogAddEntry(peerId: peerId, tag: OperationLogTags.SecretOutgoing, tagLocalIndex: .automatic, tagMergedIndex: .automatic, contents: SecretChatOutgoingOperation(contents: .pfsAcceptKey(layer: sequenceState.layerNegotiationState.activeLayer, actionGloballyUniqueId: arc4random64(), rekeySessionId: rekeySession.id, gA: gA, b: b), mutable: true, delivered: false)) + transaction.operationLogAddEntry(peerId: peerId, tag: OperationLogTags.SecretOutgoing, tagLocalIndex: .automatic, tagMergedIndex: .automatic, contents: SecretChatOutgoingOperation(contents: .pfsAcceptKey(layer: sequenceState.layerNegotiationState.activeLayer, actionGloballyUniqueId: Int64.random(in: Int64.min ... Int64.max), rekeySessionId: rekeySession.id, gA: gA, b: b), mutable: true, delivered: false)) return state.withUpdatedEmbeddedState(.sequenceBasedLayer(sequenceState.withUpdatedRekeyState(rekeySession))) } } diff --git a/submodules/TelegramCore/Sources/SetSecretChatMessageAutoremoveTimeoutInteractively.swift b/submodules/TelegramCore/Sources/SecretChats/SetSecretChatMessageAutoremoveTimeoutInteractively.swift similarity index 82% rename from submodules/TelegramCore/Sources/SetSecretChatMessageAutoremoveTimeoutInteractively.swift rename to submodules/TelegramCore/Sources/SecretChats/SetSecretChatMessageAutoremoveTimeoutInteractively.swift index eb1473f89c..8dba9e3b3c 100644 --- a/submodules/TelegramCore/Sources/SetSecretChatMessageAutoremoveTimeoutInteractively.swift +++ b/submodules/TelegramCore/Sources/SecretChats/SetSecretChatMessageAutoremoveTimeoutInteractively.swift @@ -4,7 +4,7 @@ import SwiftSignalKit import SyncCore -public func setSecretChatMessageAutoremoveTimeoutInteractively(account: Account, peerId: PeerId, timeout: Int32?) -> Signal { +func _internal_setSecretChatMessageAutoremoveTimeoutInteractively(account: Account, peerId: PeerId, timeout: Int32?) -> Signal { return account.postbox.transaction { transaction -> Void in if let peer = transaction.getPeer(peerId) as? TelegramSecretChat, let state = transaction.getPeerChatState(peerId) as? SecretChatState { if state.messageAutoremoveTimeout != timeout { @@ -17,13 +17,13 @@ public func setSecretChatMessageAutoremoveTimeoutInteractively(account: Account, transaction.setPeerChatState(peerId, state: updatedState) } - let _ = enqueueMessages(transaction: transaction, account: account, peerId: peerId, messages: [(true, .message(text: "", attributes: [], mediaReference: .standalone(media: TelegramMediaAction(action: TelegramMediaActionType.messageAutoremoveTimeoutUpdated(timeout == nil ? 0 : timeout!))), replyToMessageId: nil, localGroupingKey: nil))]) + let _ = enqueueMessages(transaction: transaction, account: account, peerId: peerId, messages: [(true, .message(text: "", attributes: [], mediaReference: .standalone(media: TelegramMediaAction(action: TelegramMediaActionType.messageAutoremoveTimeoutUpdated(timeout == nil ? 0 : timeout!))), replyToMessageId: nil, localGroupingKey: nil, correlationId: nil))]) } } } } -public func addSecretChatMessageScreenshot(account: Account, peerId: PeerId) -> Signal { +func _internal_addSecretChatMessageScreenshot(account: Account, peerId: PeerId) -> Signal { return account.postbox.transaction { transaction -> Void in if let _ = transaction.getPeer(peerId) as? TelegramSecretChat, let state = transaction.getPeerChatState(peerId) as? SecretChatState { switch state.embeddedState { @@ -32,7 +32,7 @@ public func addSecretChatMessageScreenshot(account: Account, peerId: PeerId) -> default: break } - let _ = enqueueMessages(transaction: transaction, account: account, peerId: peerId, messages: [(true, .message(text: "", attributes: [], mediaReference: .standalone(media: TelegramMediaAction(action: TelegramMediaActionType.historyScreenshot)), replyToMessageId: nil, localGroupingKey: nil))]) + let _ = enqueueMessages(transaction: transaction, account: account, peerId: peerId, messages: [(true, .message(text: "", attributes: [], mediaReference: .standalone(media: TelegramMediaAction(action: TelegramMediaActionType.historyScreenshot)), replyToMessageId: nil, localGroupingKey: nil, correlationId: nil))]) } } } diff --git a/submodules/TelegramCore/Sources/UpdateSecretChat.swift b/submodules/TelegramCore/Sources/SecretChats/UpdateSecretChat.swift similarity index 92% rename from submodules/TelegramCore/Sources/UpdateSecretChat.swift rename to submodules/TelegramCore/Sources/SecretChats/UpdateSecretChat.swift index 1ddf64698a..b3bcc86e9f 100644 --- a/submodules/TelegramCore/Sources/UpdateSecretChat.swift +++ b/submodules/TelegramCore/Sources/SecretChats/UpdateSecretChat.swift @@ -19,7 +19,7 @@ func updateSecretChat(encryptionProvider: EncryptionProvider, accountPeerId: Pee assert((currentPeer == nil) == (currentState == nil)) switch chat { case let .encryptedChat(_, _, _, adminId, _, gAOrB, remoteKeyFingerprint): - if let currentPeer = currentPeer, let currentState = currentState, adminId == accountPeerId.id { + if let currentPeer = currentPeer, let currentState = currentState, adminId == accountPeerId.id._internalGetInt32Value() { if case let .handshake(handshakeState) = currentState.embeddedState, case let .requested(_, p, a) = handshakeState { let pData = p.makeData() let aData = a.makeData() @@ -50,7 +50,7 @@ func updateSecretChat(encryptionProvider: EncryptionProvider, accountPeerId: Pee var updatedState = currentState updatedState = updatedState.withUpdatedKeychain(SecretChatKeychain(keys: [SecretChatKey(fingerprint: keyFingerprint, key: MemoryBuffer(data: key), validity: .indefinite, useCount: 0)])) - updatedState = updatedState.withUpdatedEmbeddedState(.sequenceBasedLayer(SecretChatSequenceBasedLayerState(layerNegotiationState: SecretChatLayerNegotiationState(activeLayer: .layer46, locallyRequestedLayer: nil, remotelyRequestedLayer: nil), rekeyState: nil, baseIncomingOperationIndex: transaction.operationLogGetNextEntryLocalIndex(peerId: currentPeer.id, tag: OperationLogTags.SecretIncomingDecrypted), baseOutgoingOperationIndex: transaction.operationLogGetNextEntryLocalIndex(peerId: currentPeer.id, tag: OperationLogTags.SecretOutgoing), topProcessedCanonicalIncomingOperationIndex: nil))) + updatedState = updatedState.withUpdatedEmbeddedState(.sequenceBasedLayer(SecretChatSequenceBasedLayerState(layerNegotiationState: SecretChatLayerNegotiationState(activeLayer: .layer73, locallyRequestedLayer: nil, remotelyRequestedLayer: nil), rekeyState: nil, baseIncomingOperationIndex: transaction.operationLogGetNextEntryLocalIndex(peerId: currentPeer.id, tag: OperationLogTags.SecretIncomingDecrypted), baseOutgoingOperationIndex: transaction.operationLogGetNextEntryLocalIndex(peerId: currentPeer.id, tag: OperationLogTags.SecretOutgoing), topProcessedCanonicalIncomingOperationIndex: nil))) updatedState = updatedState.withUpdatedKeyFingerprint(SecretChatKeyFingerprint(sha1: SecretChatKeySha1Fingerprint(digest: sha1Digest(key)), sha256: SecretChatKeySha256Fingerprint(digest: sha256Digest(key)))) @@ -78,7 +78,7 @@ func updateSecretChat(encryptionProvider: EncryptionProvider, accountPeerId: Pee if isRemoved { let peerId = currentPeer.id - clearHistory(transaction: transaction, mediaBox: mediaBox, peerId: peerId, namespaces: .all) + _internal_clearHistory(transaction: transaction, mediaBox: mediaBox, peerId: peerId, namespaces: .all) transaction.updatePeerChatListInclusion(peerId, inclusion: .notIncluded) transaction.removeOrderedItemListItem(collectionId: Namespaces.OrderedItemList.RecentlySearchedPeerIds, itemId: RecentPeerItemId(peerId).rawValue) } @@ -88,7 +88,7 @@ func updateSecretChat(encryptionProvider: EncryptionProvider, accountPeerId: Pee case .encryptedChatEmpty(_): break case let .encryptedChatRequested(_, folderId, _, accessHash, date, adminId, participantId, gA): - if currentPeer == nil && participantId == accountPeerId.id { + if currentPeer == nil && participantId == accountPeerId.id._internalGetInt32Value() { if settings.acceptOnThisDevice { let state = SecretChatState(role: .participant, embeddedState: .handshake(.accepting), keychain: SecretChatKeychain(keys: []), keyFingerprint: nil, messageAutoremoveTimeout: nil) @@ -99,7 +99,7 @@ func updateSecretChat(encryptionProvider: EncryptionProvider, accountPeerId: Pee let updatedState = addSecretChatOutgoingOperation(transaction: transaction, peerId: chat.peerId, operation: .initialHandshakeAccept(gA: MemoryBuffer(gA), accessHash: accessHash, b: b), state: state) transaction.setPeerChatState(chat.peerId, state: updatedState) - let peer = TelegramSecretChat(id: chat.peerId, creationDate: date, regularPeerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: adminId), accessHash: accessHash, role: updatedState.role, embeddedState: updatedState.embeddedState.peerState, messageAutoremoveTimeout: nil) + let peer = TelegramSecretChat(id: chat.peerId, creationDate: date, regularPeerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt32Value(adminId)), accessHash: accessHash, role: updatedState.role, embeddedState: updatedState.embeddedState.peerState, messageAutoremoveTimeout: nil) updatePeers(transaction: transaction, peers: [peer], update: { _, updated in return updated }) if folderId != nil { transaction.updatePeerChatListInclusion(peer.id, inclusion: .ifHasMessagesOrOneOf(groupId: Namespaces.PeerGroup.archive, pinningIndex: nil, minTimestamp: date)) @@ -122,9 +122,9 @@ func updateSecretChat(encryptionProvider: EncryptionProvider, accountPeerId: Pee Logger.shared.log("State", "got encryptedChatRequested, but peer already exists or this account is creator") } case let .encryptedChatWaiting(_, accessHash, date, adminId, participantId): - if let requestData = requestData, currentPeer == nil && adminId == accountPeerId.id { + if let requestData = requestData, currentPeer == nil && adminId == accountPeerId.id._internalGetInt32Value() { let state = SecretChatState(role: .creator, embeddedState: .handshake(.requested(g: requestData.g, p: requestData.p, a: requestData.a)), keychain: SecretChatKeychain(keys: []), keyFingerprint: nil, messageAutoremoveTimeout: nil) - let peer = TelegramSecretChat(id: chat.peerId, creationDate: date, regularPeerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: participantId), accessHash: accessHash, role: state.role, embeddedState: state.embeddedState.peerState, messageAutoremoveTimeout: nil) + let peer = TelegramSecretChat(id: chat.peerId, creationDate: date, regularPeerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt32Value(participantId)), accessHash: accessHash, role: state.role, embeddedState: state.embeddedState.peerState, messageAutoremoveTimeout: nil) updatePeers(transaction: transaction, peers: [peer], update: { _, updated in return updated }) transaction.setPeerChatState(peer.id, state: state) transaction.resetIncomingReadStates([peer.id: [ diff --git a/submodules/TelegramCore/Sources/AutodownloadSettings.swift b/submodules/TelegramCore/Sources/Settings/AutodownloadSettings.swift similarity index 100% rename from submodules/TelegramCore/Sources/AutodownloadSettings.swift rename to submodules/TelegramCore/Sources/Settings/AutodownloadSettings.swift diff --git a/submodules/TelegramCore/Sources/CacheStorageSettings.swift b/submodules/TelegramCore/Sources/Settings/CacheStorageSettings.swift similarity index 100% rename from submodules/TelegramCore/Sources/CacheStorageSettings.swift rename to submodules/TelegramCore/Sources/Settings/CacheStorageSettings.swift diff --git a/submodules/TelegramCore/Sources/ContentPrivacySettings.swift b/submodules/TelegramCore/Sources/Settings/ContentPrivacySettings.swift similarity index 100% rename from submodules/TelegramCore/Sources/ContentPrivacySettings.swift rename to submodules/TelegramCore/Sources/Settings/ContentPrivacySettings.swift diff --git a/submodules/TelegramCore/Sources/ContentSettings.swift b/submodules/TelegramCore/Sources/Settings/ContentSettings.swift similarity index 100% rename from submodules/TelegramCore/Sources/ContentSettings.swift rename to submodules/TelegramCore/Sources/Settings/ContentSettings.swift diff --git a/submodules/TelegramCore/Sources/GlobalNotificationSettings.swift b/submodules/TelegramCore/Sources/Settings/GlobalNotificationSettings.swift similarity index 100% rename from submodules/TelegramCore/Sources/GlobalNotificationSettings.swift rename to submodules/TelegramCore/Sources/Settings/GlobalNotificationSettings.swift diff --git a/submodules/TelegramCore/Sources/LimitsConfiguration.swift b/submodules/TelegramCore/Sources/Settings/LimitsConfiguration.swift similarity index 100% rename from submodules/TelegramCore/Sources/LimitsConfiguration.swift rename to submodules/TelegramCore/Sources/Settings/LimitsConfiguration.swift diff --git a/submodules/TelegramCore/Sources/LoggingSettings.swift b/submodules/TelegramCore/Sources/Settings/LoggingSettings.swift similarity index 100% rename from submodules/TelegramCore/Sources/LoggingSettings.swift rename to submodules/TelegramCore/Sources/Settings/LoggingSettings.swift diff --git a/submodules/TelegramCore/Sources/NetworkSettings.swift b/submodules/TelegramCore/Sources/Settings/NetworkSettings.swift similarity index 100% rename from submodules/TelegramCore/Sources/NetworkSettings.swift rename to submodules/TelegramCore/Sources/Settings/NetworkSettings.swift diff --git a/submodules/TelegramCore/Sources/PeerContactSettings.swift b/submodules/TelegramCore/Sources/Settings/PeerContactSettings.swift similarity index 96% rename from submodules/TelegramCore/Sources/PeerContactSettings.swift rename to submodules/TelegramCore/Sources/Settings/PeerContactSettings.swift index 6bc742bd87..d2ac0d1a85 100644 --- a/submodules/TelegramCore/Sources/PeerContactSettings.swift +++ b/submodules/TelegramCore/Sources/Settings/PeerContactSettings.swift @@ -65,5 +65,5 @@ public func unarchiveAutomaticallyArchivedPeer(account: Account, peerId: PeerId) } |> deliverOnMainQueue).start() - let _ = updatePeerMuteSetting(account: account, peerId: peerId, muteInterval: nil).start() + let _ = _internal_updatePeerMuteSetting(account: account, peerId: peerId, muteInterval: nil).start() } diff --git a/submodules/TelegramCore/Sources/PrivacySettings.swift b/submodules/TelegramCore/Sources/Settings/PrivacySettings.swift similarity index 94% rename from submodules/TelegramCore/Sources/PrivacySettings.swift rename to submodules/TelegramCore/Sources/Settings/PrivacySettings.swift index 0ad2eade68..3f44df44b3 100644 --- a/submodules/TelegramCore/Sources/PrivacySettings.swift +++ b/submodules/TelegramCore/Sources/Settings/PrivacySettings.swift @@ -162,7 +162,7 @@ extension SelectivePrivacySettings { current = .enableContacts(enableFor: [:], disableFor: [:]) case let .privacyValueAllowUsers(users): for id in users { - if let peer = peers[PeerId(namespace: Namespaces.Peer.CloudUser, id: id)] { + if let peer = peers[PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt32Value(id))] { enableFor[peer.peer.id] = peer } } @@ -172,13 +172,13 @@ extension SelectivePrivacySettings { break case let .privacyValueDisallowUsers(users): for id in users { - if let peer = peers[PeerId(namespace: Namespaces.Peer.CloudUser, id: id)] { + if let peer = peers[PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt32Value(id))] { disableFor[peer.peer.id] = peer } } case let .privacyValueAllowChatParticipants(chats): for id in chats { - for possibleId in [PeerId(namespace: Namespaces.Peer.CloudGroup, id: id), PeerId(namespace: Namespaces.Peer.CloudChannel, id: id)] { + for possibleId in [PeerId(namespace: Namespaces.Peer.CloudGroup, id: PeerId.Id._internalFromInt32Value(id)), PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt32Value(id))] { if let peer = peers[possibleId] { enableFor[peer.peer.id] = peer } @@ -186,7 +186,7 @@ extension SelectivePrivacySettings { } case let .privacyValueDisallowChatParticipants(chats): for id in chats { - for possibleId in [PeerId(namespace: Namespaces.Peer.CloudGroup, id: id), PeerId(namespace: Namespaces.Peer.CloudChannel, id: id)] { + for possibleId in [PeerId(namespace: Namespaces.Peer.CloudGroup, id: PeerId.Id._internalFromInt32Value(id)), PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt32Value(id))] { if let peer = peers[possibleId] { disableFor[peer.peer.id] = peer } diff --git a/submodules/TelegramCore/Sources/ProxySettings.swift b/submodules/TelegramCore/Sources/Settings/ProxySettings.swift similarity index 100% rename from submodules/TelegramCore/Sources/ProxySettings.swift rename to submodules/TelegramCore/Sources/Settings/ProxySettings.swift diff --git a/submodules/TelegramCore/Sources/VoipConfiguration.swift b/submodules/TelegramCore/Sources/Settings/VoipConfiguration.swift similarity index 100% rename from submodules/TelegramCore/Sources/VoipConfiguration.swift rename to submodules/TelegramCore/Sources/Settings/VoipConfiguration.swift diff --git a/submodules/TelegramCore/Sources/SingleMessageView.swift b/submodules/TelegramCore/Sources/SingleMessageView.swift deleted file mode 100644 index 203daf6bce..0000000000 --- a/submodules/TelegramCore/Sources/SingleMessageView.swift +++ /dev/null @@ -1,105 +0,0 @@ -import Foundation -import Postbox -import SwiftSignalKit -import TelegramApi -import MtProtoKit - -import SyncCore - -public func singleMessageView(account: Account, messageId: MessageId, loadIfNotExists: Bool) -> Signal { - return Signal { subscriber in - let loadedMessage = account.postbox.transaction { transaction -> Signal in - if transaction.getMessage(messageId) == nil, loadIfNotExists { - return fetchMessage(transaction: transaction, account: account, messageId: messageId) - } else { - return .complete() - } - } |> switchToLatest - - let disposable = loadedMessage.start() - let viewDisposable = account.postbox.messageView(messageId).start(next: { view in - subscriber.putNext(view) - }) - - return ActionDisposable { - disposable.dispose() - viewDisposable.dispose() - } - } -} - -private func fetchMessage(transaction: Transaction, account: Account, messageId: MessageId) -> Signal { - if let peer = transaction.getPeer(messageId.peerId) { - var signal: Signal? - if messageId.namespace == Namespaces.Message.ScheduledCloud { - if let inputPeer = apiInputPeer(peer) { - signal = account.network.request(Api.functions.messages.getScheduledMessages(peer: inputPeer, id: [messageId.id])) - } - } else if messageId.peerId.namespace == Namespaces.Peer.CloudUser || messageId.peerId.namespace == Namespaces.Peer.CloudGroup { - signal = account.network.request(Api.functions.messages.getMessages(id: [Api.InputMessage.inputMessageID(id: messageId.id)])) - } else if messageId.peerId.namespace == Namespaces.Peer.CloudChannel { - if let inputChannel = apiInputChannel(peer) { - signal = account.network.request(Api.functions.channels.getMessages(channel: inputChannel, id: [Api.InputMessage.inputMessageID(id: messageId.id)])) - } - } - if let signal = signal { - return signal - |> `catch` { _ -> Signal in - return .single(.messages(messages: [], chats: [], users: [])) - } - |> mapToSignal { result -> Signal in - return account.postbox.transaction { transaction -> Void in - let apiMessages: [Api.Message] - let apiChats: [Api.Chat] - let apiUsers: [Api.User] - switch result { - case let .messages(messages, chats, users): - apiMessages = messages - apiChats = chats - apiUsers = users - case let .messagesSlice(_, _, _, _, messages, chats, users): - apiMessages = messages - apiChats = chats - apiUsers = users - case let .channelMessages(_, _, _, _, messages, chats, users): - apiMessages = messages - apiChats = chats - apiUsers = users - case .messagesNotModified: - apiMessages = [] - apiChats = [] - apiUsers = [] - } - - var peers: [PeerId: Peer] = [:] - - for user in apiUsers { - if let user = TelegramUser.merge(transaction.getPeer(user.peerId) as? TelegramUser, rhs: user) { - peers[user.id] = user - } - } - - for chat in apiChats { - if let groupOrChannel = parseTelegramGroupOrChannel(chat: chat) { - peers[groupOrChannel.id] = groupOrChannel - } - } - - updatePeers(transaction: transaction, peers: Array(peers.values), update: { _, updated in - return updated - }) - - for message in apiMessages { - if let message = StoreMessage(apiMessage: message, namespace: messageId.namespace) { - let _ = transaction.addMessages([message], location: .Random) - } - } - } - } - } else { - return .complete() - } - } else { - return .complete() - } -} diff --git a/submodules/TelegramCore/Sources/AccountState.swift b/submodules/TelegramCore/Sources/State/AccountState.swift similarity index 100% rename from submodules/TelegramCore/Sources/AccountState.swift rename to submodules/TelegramCore/Sources/State/AccountState.swift diff --git a/submodules/TelegramCore/Sources/AccountStateManagementUtils.swift b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift similarity index 94% rename from submodules/TelegramCore/Sources/AccountStateManagementUtils.swift rename to submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift index bf4ddea375..5004e5bb9d 100644 --- a/submodules/TelegramCore/Sources/AccountStateManagementUtils.swift +++ b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift @@ -23,7 +23,7 @@ private func peerIdsFromUpdateGroups(_ groups: [UpdateGroup]) -> Set { } switch group { case let .updateChannelPts(channelId, _, _): - peerIds.insert(PeerId(namespace: Namespaces.Peer.CloudChannel, id: channelId)) + peerIds.insert(PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt32Value(channelId))) default: break } @@ -77,7 +77,7 @@ private func peerIdsRequiringLocalChatStateFromUpdates(_ updates: [Api.Update]) } switch update { case let .updateChannelTooLong(_, channelId, _): - let peerId = PeerId(namespace: Namespaces.Peer.CloudChannel, id: channelId) + let peerId = PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt32Value(channelId)) peerIds.insert(peerId) case let .updateFolderPeers(folderPeers, _, _): for peer in folderPeers { @@ -87,7 +87,7 @@ private func peerIdsRequiringLocalChatStateFromUpdates(_ updates: [Api.Update]) } } case let .updateReadChannelInbox(_, _, channelId, _, _, _): - peerIds.insert(PeerId(namespace: Namespaces.Peer.CloudChannel, id: channelId)) + peerIds.insert(PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt32Value(channelId))) case let .updateReadHistoryInbox(_, _, peer, _, _, _, _): peerIds.insert(peer.peerId) case let .updateDraftMessage(peer, draft): @@ -109,16 +109,25 @@ private func peerIdsRequiringLocalChatStateFromUpdateGroups(_ groups: [UpdateGro for group in groups { peerIds.formUnion(peerIdsRequiringLocalChatStateFromUpdates(group.updates)) - - /*for chat in group.chats { - if let channel = parseTelegramGroupOrChannel(chat: chat) as? TelegramChannel { + + var channelUpdates = Set() + for update in group.updates { + switch update { + case let .updateChannel(channelId): + channelUpdates.insert(PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt32Value(channelId))) + default: + break + } + } + for chat in group.chats { + if let channel = parseTelegramGroupOrChannel(chat: chat) as? TelegramChannel, channelUpdates.contains(channel.id) { if let accessHash = channel.accessHash, case .personal = accessHash { if case .member = channel.participationStatus { peerIds.insert(channel.id) } } } - }*/ + } switch group { case let .ensurePeerHasLocalState(peerId): @@ -138,7 +147,7 @@ private func locallyGeneratedMessageTimestampsFromUpdateGroups(_ groups: [Update switch update { case let .updateServiceNotification(_, date, _, _, _, _): if let date = date { - let peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: 777000) + let peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt32Value(777000)) if messageTimestamps[peerId] == nil { messageTimestamps[peerId] = [(Namespaces.Message.Local, date)] } else { @@ -294,7 +303,7 @@ private func peerIdsRequiringLocalChatStateFromDifference(_ difference: Api.upda } switch update { case let .updateChannelTooLong(_, channelId, _): - let peerId = PeerId(namespace: Namespaces.Peer.CloudChannel, id: channelId) + let peerId = PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt32Value(channelId)) peerIds.insert(peerId) default: break @@ -316,7 +325,7 @@ private func peerIdsRequiringLocalChatStateFromDifference(_ difference: Api.upda } switch update { case let .updateChannelTooLong(_, channelId, _): - let peerId = PeerId(namespace: Namespaces.Peer.CloudChannel, id: channelId) + let peerId = PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt32Value(channelId)) peerIds.insert(peerId) default: break @@ -349,7 +358,7 @@ private func locallyGeneratedMessageTimestampsFromDifference(_ difference: Api.u switch update { case let .updateServiceNotification(_, date, _, _, _, _): if let date = date { - let peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: 777000) + let peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt32Value(777000)) if messageTimestamps[peerId] == nil { messageTimestamps[peerId] = [(Namespaces.Message.Local, date)] } else { @@ -675,21 +684,21 @@ private func sortedUpdates(_ updates: [Api.Update]) -> [Api.Update] { for update in updates { switch update { case let .updateChannelTooLong(_, channelId, _): - let peerId = PeerId(namespace: Namespaces.Peer.CloudChannel, id: channelId) + let peerId = PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt32Value(channelId)) if updatesByChannel[peerId] == nil { updatesByChannel[peerId] = [update] } else { updatesByChannel[peerId]!.append(update) } case let .updateDeleteChannelMessages(channelId, _, _, _): - let peerId = PeerId(namespace: Namespaces.Peer.CloudChannel, id: channelId) + let peerId = PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt32Value(channelId)) if updatesByChannel[peerId] == nil { updatesByChannel[peerId] = [update] } else { updatesByChannel[peerId]!.append(update) } case let .updatePinnedChannelMessages(_, channelId, _, _, _): - let peerId = PeerId(namespace: Namespaces.Peer.CloudChannel, id: channelId) + let peerId = PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt32Value(channelId)) if updatesByChannel[peerId] == nil { updatesByChannel[peerId] = [update] } else { @@ -716,14 +725,14 @@ private func sortedUpdates(_ updates: [Api.Update]) -> [Api.Update] { otherUpdates.append(update) } case let .updateChannelWebPage(channelId, _, _, _): - let peerId = PeerId(namespace: Namespaces.Peer.CloudChannel, id: channelId) + let peerId = PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt32Value(channelId)) if updatesByChannel[peerId] == nil { updatesByChannel[peerId] = [update] } else { updatesByChannel[peerId]!.append(update) } case let .updateChannelAvailableMessages(channelId, _): - let peerId = PeerId(namespace: Namespaces.Peer.CloudChannel, id: channelId) + let peerId = PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt32Value(channelId)) if updatesByChannel[peerId] == nil { updatesByChannel[peerId] = [update] } else { @@ -808,7 +817,7 @@ private func finalStateWithUpdatesAndServerTime(postbox: Postbox, network: Netwo for update in sortedUpdates(updates) { switch update { case let .updateChannelTooLong(_, channelId, channelPts): - let peerId = PeerId(namespace: Namespaces.Peer.CloudChannel, id: channelId) + let peerId = PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt32Value(channelId)) if !channelsToPoll.contains(peerId) { if let channelPts = channelPts, let channelState = state.channelStates[peerId], channelState.pts >= channelPts { Logger.shared.log("State", "channel \(peerId) (\((updatedState.peers[peerId] as? TelegramChannel)?.title ?? "nil")) skip updateChannelTooLong by pts") @@ -817,7 +826,7 @@ private func finalStateWithUpdatesAndServerTime(postbox: Postbox, network: Netwo } } case let .updateDeleteChannelMessages(channelId, messages, pts: pts, ptsCount): - let peerId = PeerId(namespace: Namespaces.Peer.CloudChannel, id: channelId) + let peerId = PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt32Value(channelId)) if let previousState = updatedState.channelStates[peerId] { if previousState.pts >= pts { Logger.shared.log("State", "channel \(peerId) (\((updatedState.peers[peerId] as? TelegramChannel)?.title ?? "nil")) skip old delete update") @@ -868,7 +877,7 @@ private func finalStateWithUpdatesAndServerTime(postbox: Postbox, network: Netwo Logger.shared.log("State", "Invalid updateEditChannelMessage") } case let .updateChannelWebPage(channelId, apiWebpage, pts, ptsCount): - let peerId = PeerId(namespace: Namespaces.Peer.CloudChannel, id: channelId) + let peerId = PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt32Value(channelId)) if let previousState = updatedState.channelStates[peerId] { if previousState.pts >= pts { } else if previousState.pts + ptsCount == pts { @@ -894,7 +903,7 @@ private func finalStateWithUpdatesAndServerTime(postbox: Postbox, network: Netwo } } case let .updateChannelAvailableMessages(channelId, minId): - let peerId = PeerId(namespace: Namespaces.Peer.CloudChannel, id: channelId) + let peerId = PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt32Value(channelId)) updatedState.updateMinAvailableMessage(MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: minId)) case let .updateDeleteMessages(messages, _, _): updatedState.deleteMessagesWithGlobalIds(messages) @@ -971,7 +980,7 @@ private func finalStateWithUpdatesAndServerTime(postbox: Postbox, network: Netwo if popup { updatedState.addDisplayAlert(text, isDropAuth: type.hasPrefix("AUTH_KEY_DROP_")) } else if let date = date { - let peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: 777000) + let peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt32Value(777000)) if updatedState.peers[peerId] == nil { updatedState.updatePeer(peerId, { peer in @@ -1020,13 +1029,13 @@ private func finalStateWithUpdatesAndServerTime(postbox: Postbox, network: Netwo } } case let .updateReadChannelInbox(_, folderId, channelId, maxId, stillUnreadCount, pts): - updatedState.resetIncomingReadState(groupId: PeerGroupId(rawValue: folderId ?? 0), peerId: PeerId(namespace: Namespaces.Peer.CloudChannel, id: channelId), namespace: Namespaces.Message.Cloud, maxIncomingReadId: maxId, count: stillUnreadCount, pts: pts) + updatedState.resetIncomingReadState(groupId: PeerGroupId(rawValue: folderId ?? 0), peerId: PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt32Value(channelId)), namespace: Namespaces.Message.Cloud, maxIncomingReadId: maxId, count: stillUnreadCount, pts: pts) case let .updateReadChannelOutbox(channelId, maxId): - updatedState.readOutbox(MessageId(peerId: PeerId(namespace: Namespaces.Peer.CloudChannel, id: channelId), namespace: Namespaces.Message.Cloud, id: maxId), timestamp: nil) + updatedState.readOutbox(MessageId(peerId: PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt32Value(channelId)), namespace: Namespaces.Message.Cloud, id: maxId), timestamp: nil) case let .updateChannel(channelId): - updatedState.addExternallyUpdatedPeerId(PeerId(namespace: Namespaces.Peer.CloudChannel, id: channelId)) + updatedState.addExternallyUpdatedPeerId(PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt32Value(channelId))) case let .updateChat(chatId): - updatedState.addExternallyUpdatedPeerId(PeerId(namespace: Namespaces.Peer.CloudGroup, id: chatId)) + updatedState.addExternallyUpdatedPeerId(PeerId(namespace: Namespaces.Peer.CloudGroup, id: PeerId.Id._internalFromInt32Value(chatId))) case let .updateReadHistoryInbox(_, folderId, peer, maxId, stillUnreadCount, pts, _): updatedState.resetIncomingReadState(groupId: PeerGroupId(rawValue: folderId ?? 0), peerId: peer.peerId, namespace: Namespaces.Message.Cloud, maxIncomingReadId: maxId, count: stillUnreadCount, pts: pts) case let .updateReadHistoryOutbox(peer, maxId, _, _): @@ -1034,11 +1043,11 @@ private func finalStateWithUpdatesAndServerTime(postbox: Postbox, network: Netwo case let .updateReadChannelDiscussionInbox(_, channelId, topMsgId, readMaxId, mainChannelId, mainChannelPost): var mainChannelMessage: MessageId? if let mainChannelId = mainChannelId, let mainChannelPost = mainChannelPost { - mainChannelMessage = MessageId(peerId: PeerId(namespace: Namespaces.Peer.CloudChannel, id: mainChannelId), namespace: Namespaces.Message.Cloud, id: mainChannelPost) + mainChannelMessage = MessageId(peerId: PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt32Value(mainChannelId)), namespace: Namespaces.Message.Cloud, id: mainChannelPost) } - updatedState.readThread(threadMessageId: MessageId(peerId: PeerId(namespace: Namespaces.Peer.CloudChannel, id: channelId), namespace: Namespaces.Message.Cloud, id: topMsgId), readMaxId: readMaxId, isIncoming: true, mainChannelMessage: mainChannelMessage) + updatedState.readThread(threadMessageId: MessageId(peerId: PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt32Value(channelId)), namespace: Namespaces.Message.Cloud, id: topMsgId), readMaxId: readMaxId, isIncoming: true, mainChannelMessage: mainChannelMessage) case let .updateReadChannelDiscussionOutbox(channelId, topMsgId, readMaxId): - updatedState.readThread(threadMessageId: MessageId(peerId: PeerId(namespace: Namespaces.Peer.CloudChannel, id: channelId), namespace: Namespaces.Message.Cloud, id: topMsgId), readMaxId: readMaxId, isIncoming: false, mainChannelMessage: nil) + updatedState.readThread(threadMessageId: MessageId(peerId: PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt32Value(channelId)), namespace: Namespaces.Message.Cloud, id: topMsgId), readMaxId: readMaxId, isIncoming: false, mainChannelMessage: nil) case let .updateDialogUnreadMark(flags, peer): switch peer { case let .dialogPeer(peer): @@ -1072,9 +1081,9 @@ private func finalStateWithUpdatesAndServerTime(postbox: Postbox, network: Netwo let groupPeerId: PeerId switch participants { case let .chatParticipants(chatId, _, _): - groupPeerId = PeerId(namespace: Namespaces.Peer.CloudGroup, id: chatId) + groupPeerId = PeerId(namespace: Namespaces.Peer.CloudGroup, id: PeerId.Id._internalFromInt32Value(chatId)) case let .chatParticipantsForbidden(_, chatId, _): - groupPeerId = PeerId(namespace: Namespaces.Peer.CloudGroup, id: chatId) + groupPeerId = PeerId(namespace: Namespaces.Peer.CloudGroup, id: PeerId.Id._internalFromInt32Value(chatId)) } updatedState.updateCachedPeerData(groupPeerId, { current in let previous: CachedGroupData @@ -1086,9 +1095,9 @@ private func finalStateWithUpdatesAndServerTime(postbox: Postbox, network: Netwo return previous.withUpdatedParticipants(CachedGroupParticipants(apiParticipants: participants)) }) case let .updateChatParticipantAdd(chatId, userId, inviterId, date, _): - let groupPeerId = PeerId(namespace: Namespaces.Peer.CloudGroup, id: chatId) - let userPeerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: userId) - let inviterPeerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: inviterId) + let groupPeerId = PeerId(namespace: Namespaces.Peer.CloudGroup, id: PeerId.Id._internalFromInt32Value(chatId)) + let userPeerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt32Value(userId)) + let inviterPeerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt32Value(inviterId)) updatedState.updateCachedPeerData(groupPeerId, { current in if let current = current as? CachedGroupData, let participants = current.participants { var updatedParticipants = participants.participants @@ -1101,8 +1110,8 @@ private func finalStateWithUpdatesAndServerTime(postbox: Postbox, network: Netwo } }) case let .updateChatParticipantDelete(chatId, userId, _): - let groupPeerId = PeerId(namespace: Namespaces.Peer.CloudGroup, id: chatId) - let userPeerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: userId) + let groupPeerId = PeerId(namespace: Namespaces.Peer.CloudGroup, id: PeerId.Id._internalFromInt32Value(chatId)) + let userPeerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt32Value(userId)) updatedState.updateCachedPeerData(groupPeerId, { current in if let current = current as? CachedGroupData, let participants = current.participants { var updatedParticipants = participants.participants @@ -1115,8 +1124,8 @@ private func finalStateWithUpdatesAndServerTime(postbox: Postbox, network: Netwo } }) case let .updateChatParticipantAdmin(chatId, userId, isAdmin, _): - let groupPeerId = PeerId(namespace: Namespaces.Peer.CloudGroup, id: chatId) - let userPeerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: userId) + let groupPeerId = PeerId(namespace: Namespaces.Peer.CloudGroup, id: PeerId.Id._internalFromInt32Value(chatId)) + let userPeerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt32Value(userId)) updatedState.updateCachedPeerData(groupPeerId, { current in if let current = current as? CachedGroupData, let participants = current.participants { var updatedParticipants = participants.participants @@ -1147,12 +1156,12 @@ private func finalStateWithUpdatesAndServerTime(postbox: Postbox, network: Netwo } }) case let .updatePinnedChannelMessages(flags, channelId, messages, pts, ptsCount): - let peerId = PeerId(namespace: Namespaces.Peer.CloudChannel, id: channelId) + let peerId = PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt32Value(channelId)) if let previousState = updatedState.channelStates[peerId] { if previousState.pts >= pts { Logger.shared.log("State", "channel \(peerId) (\((updatedState.peers[peerId] as? TelegramChannel)?.title ?? "nil")) skip old pinned messages update") } else if previousState.pts + ptsCount == pts { - let channelPeerId = PeerId(namespace: Namespaces.Peer.CloudChannel, id: channelId) + let channelPeerId = PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt32Value(channelId)) updatedState.updateMessagesPinned(ids: messages.map { id in MessageId(peerId: channelPeerId, namespace: Namespaces.Message.Cloud, id: id) }, pinned: (flags & (1 << 0)) != 0) @@ -1181,10 +1190,10 @@ private func finalStateWithUpdatesAndServerTime(postbox: Postbox, network: Netwo return previous.withUpdatedIsBlocked(blocked == .boolTrue) }) case let .updateUserStatus(userId, status): - updatedState.mergePeerPresences([PeerId(namespace: Namespaces.Peer.CloudUser, id: userId): status], explicit: true) + updatedState.mergePeerPresences([PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt32Value(userId)): status], explicit: true) case let .updateUserName(userId, firstName, lastName, username): //TODO add contact checking for apply first and last name - updatedState.updatePeer(PeerId(namespace: Namespaces.Peer.CloudUser, id: userId), { peer in + updatedState.updatePeer(PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt32Value(userId)), { peer in if let user = peer as? TelegramUser { return user.withUpdatedUsername(username) } else { @@ -1192,7 +1201,7 @@ private func finalStateWithUpdatesAndServerTime(postbox: Postbox, network: Netwo } }) case let .updateUserPhoto(userId, _, photo, _): - updatedState.updatePeer(PeerId(namespace: Namespaces.Peer.CloudUser, id: userId), { peer in + updatedState.updatePeer(PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt32Value(userId)), { peer in if let user = peer as? TelegramUser { return user.withUpdatedPhoto(parsedTelegramProfilePhoto(photo)) } else { @@ -1200,7 +1209,7 @@ private func finalStateWithUpdatesAndServerTime(postbox: Postbox, network: Netwo } }) case let .updateUserPhone(userId, phone): - updatedState.updatePeer(PeerId(namespace: Namespaces.Peer.CloudUser, id: userId), { peer in + updatedState.updatePeer(PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt32Value(userId)), { peer in if let user = peer as? TelegramUser { return user.withUpdatedPhone(phone.isEmpty ? nil : phone) } else { @@ -1243,7 +1252,7 @@ private func finalStateWithUpdatesAndServerTime(postbox: Postbox, network: Netwo case let .updateNewEncryptedMessage(message, _): updatedState.addSecretMessages([message]) case let .updateEncryptedMessagesRead(chatId, maxDate, date): - updatedState.readSecretOutbox(peerId: PeerId(namespace: Namespaces.Peer.SecretChat, id: chatId), timestamp: maxDate, actionTimestamp: date) + updatedState.readSecretOutbox(peerId: PeerId(namespace: Namespaces.Peer.SecretChat, id: PeerId.Id._internalFromInt32Value(chatId)), timestamp: maxDate, actionTimestamp: date) case let .updateUserTyping(userId, type): if let date = updatesDate, date + 60 > serverTime { let activity = PeerInputActivity(apiType: type, timestamp: date) @@ -1252,7 +1261,7 @@ private func finalStateWithUpdatesAndServerTime(postbox: Postbox, network: Netwo category = .voiceChat } - updatedState.addPeerInputActivity(chatPeerId: PeerActivitySpace(peerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: userId), category: category), peerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: userId), activity: activity) + updatedState.addPeerInputActivity(chatPeerId: PeerActivitySpace(peerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt32Value(userId)), category: category), peerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt32Value(userId)), activity: activity) } case let .updateChatUserTyping(chatId, userId, type): if let date = updatesDate, date + 60 > serverTime { @@ -1262,11 +1271,11 @@ private func finalStateWithUpdatesAndServerTime(postbox: Postbox, network: Netwo category = .voiceChat } - updatedState.addPeerInputActivity(chatPeerId: PeerActivitySpace(peerId: PeerId(namespace: Namespaces.Peer.CloudGroup, id: chatId), category: category), peerId: userId.peerId, activity: activity) + updatedState.addPeerInputActivity(chatPeerId: PeerActivitySpace(peerId: PeerId(namespace: Namespaces.Peer.CloudGroup, id: PeerId.Id._internalFromInt32Value(chatId)), category: category), peerId: userId.peerId, activity: activity) } case let .updateChannelUserTyping(_, channelId, topMsgId, userId, type): if let date = updatesDate, date + 60 > serverTime { - let channelPeerId = PeerId(namespace: Namespaces.Peer.CloudChannel, id: channelId) + let channelPeerId = PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt32Value(channelId)) let threadId = topMsgId.flatMap { makeMessageThreadId(MessageId(peerId: channelPeerId, namespace: Namespaces.Message.Cloud, id: $0)) } let activity = PeerInputActivity(apiType: type, timestamp: date) @@ -1281,7 +1290,7 @@ private func finalStateWithUpdatesAndServerTime(postbox: Postbox, network: Netwo } case let .updateEncryptedChatTyping(chatId): if let date = updatesDate, date + 60 > serverTime { - updatedState.addPeerInputActivity(chatPeerId: PeerActivitySpace(peerId: PeerId(namespace: Namespaces.Peer.SecretChat, id: chatId), category: .global), peerId: nil, activity: .typingText) + updatedState.addPeerInputActivity(chatPeerId: PeerActivitySpace(peerId: PeerId(namespace: Namespaces.Peer.SecretChat, id: PeerId.Id._internalFromInt32Value(chatId)), category: .global), peerId: nil, activity: .typingText) } case let .updateDialogPinned(flags, folderId, peer): let groupId: PeerGroupId = folderId.flatMap(PeerGroupId.init(rawValue:)) ?? .root @@ -1316,9 +1325,9 @@ private func finalStateWithUpdatesAndServerTime(postbox: Postbox, network: Netwo case let .updateReadMessagesContents(messages, _, _): updatedState.addReadMessagesContents((nil, messages)) case let .updateChannelReadMessagesContents(channelId, messages): - updatedState.addReadMessagesContents((PeerId(namespace: Namespaces.Peer.CloudChannel, id: channelId), messages)) + updatedState.addReadMessagesContents((PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt32Value(channelId)), messages)) case let .updateChannelMessageViews(channelId, id, views): - updatedState.addUpdateMessageImpressionCount(id: MessageId(peerId: PeerId(namespace: Namespaces.Peer.CloudChannel, id: channelId), namespace: Namespaces.Message.Cloud, id: id), count: views) + updatedState.addUpdateMessageImpressionCount(id: MessageId(peerId: PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt32Value(channelId)), namespace: Namespaces.Message.Cloud, id: id), count: views) /*case let .updateChannelMessageForwards(channelId, id, forwards): updatedState.addUpdateMessageForwardsCount(id: MessageId(peerId: PeerId(namespace: Namespaces.Peer.CloudChannel, id: channelId), namespace: Namespaces.Message.Cloud, id: id), count: forwards)*/ case let .updateNewStickerSet(stickerset): @@ -1358,8 +1367,8 @@ private func finalStateWithUpdatesAndServerTime(postbox: Postbox, network: Netwo updatedState.updateGroupCallParticipants(id: id, accessHash: accessHash, participants: participants, version: version) } case let .updateGroupCall(channelId, call): - updatedState.updateGroupCall(peerId: PeerId(namespace: Namespaces.Peer.CloudChannel, id: channelId), call: call) - updatedState.updateGroupCall(peerId: PeerId(namespace: Namespaces.Peer.CloudGroup, id: channelId), call: call) + updatedState.updateGroupCall(peerId: PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt32Value(channelId)), call: call) + updatedState.updateGroupCall(peerId: PeerId(namespace: Namespaces.Peer.CloudGroup, id: PeerId.Id._internalFromInt32Value(channelId)), call: call) case let .updatePeerHistoryTTL(_, peer, ttl): updatedState.updateAutoremoveTimeout(peer: peer, value: CachedPeerAutoremoveTimeout.Value(ttl)) case let .updateLangPackTooLong(langCode): @@ -1415,6 +1424,38 @@ private func finalStateWithUpdatesAndServerTime(postbox: Postbox, network: Netwo updatedState.addUpdateChatListFilterOrder(order: order) case let .updateDialogFilter(_, id, filter): updatedState.addUpdateChatListFilter(id: id, filter: filter) + case let .updateBotCommands(peer, botId, apiCommands): + let botPeerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt32Value(botId)) + let commands: [BotCommand] = apiCommands.map { command in + switch command { + case let .botCommand(command, description): + return BotCommand(text: command, description: description) + } + } + updatedState.updateCachedPeerData(peer.peerId, { current in + if peer.peerId.namespace == Namespaces.Peer.CloudUser, let previous = current as? CachedUserData { + if let botInfo = previous.botInfo { + return previous.withUpdatedBotInfo(BotInfo(description: botInfo.description, commands: commands)) + } + } else if peer.peerId.namespace == Namespaces.Peer.CloudGroup, let previous = current as? CachedGroupData { + if let index = previous.botInfos.firstIndex(where: { $0.peerId == botPeerId }) { + var updatedBotInfos = previous.botInfos + let previousBotInfo = updatedBotInfos[index] + updatedBotInfos.remove(at: index) + updatedBotInfos.insert(CachedPeerBotInfo(peerId: botPeerId, botInfo: BotInfo(description: previousBotInfo.botInfo.description, commands: commands)), at: index) + return previous.withUpdatedBotInfos(updatedBotInfos) + } + } else if peer.peerId.namespace == Namespaces.Peer.CloudChannel, let previous = current as? CachedChannelData { + if let index = previous.botInfos.firstIndex(where: { $0.peerId == botPeerId }) { + var updatedBotInfos = previous.botInfos + let previousBotInfo = updatedBotInfos[index] + updatedBotInfos.remove(at: index) + updatedBotInfos.insert(CachedPeerBotInfo(peerId: botPeerId, botInfo: BotInfo(description: previousBotInfo.botInfo.description, commands: commands)), at: index) + return previous.withUpdatedBotInfos(updatedBotInfos) + } + } + return current + }) default: break } @@ -1667,49 +1708,49 @@ private func resolveMissingPeerChatInfos(network: Network, state: AccountMutable } } -func keepPollingChannel(postbox: Postbox, network: Network, peerId: PeerId, stateManager: AccountStateManager) -> Signal { - let signal: Signal = postbox.transaction { transaction -> Signal in - if let accountState = (transaction.getState() as? AuthorizedAccountState)?.state, let peer = transaction.getPeer(peerId) { - var channelStates: [PeerId: AccountStateChannelState] = [:] - if let channelState = transaction.getPeerChatState(peerId) as? ChannelState { - channelStates[peerId] = AccountStateChannelState(pts: channelState.pts) - } - let initialPeers: [PeerId: Peer] = [peerId: peer] - var peerChatInfos: [PeerId: PeerChatInfo] = [:] - let inclusion = transaction.getPeerChatListInclusion(peerId) - var hasValidInclusion = false - switch inclusion { - case .ifHasMessagesOrOneOf: - hasValidInclusion = true - case .notIncluded: - hasValidInclusion = false - } - if hasValidInclusion { - if let notificationSettings = transaction.getPeerNotificationSettings(peerId) as? TelegramPeerNotificationSettings { - peerChatInfos[peerId] = PeerChatInfo(notificationSettings: notificationSettings) - } - } - let initialState = AccountMutableState(initialState: AccountInitialState(state: accountState, peerIds: Set(), peerIdsRequiringLocalChatState: Set(), channelStates: channelStates, peerChatInfos: peerChatInfos, locallyGeneratedMessageTimestamps: [:], cloudReadStates: [:], channelsToPollExplicitely: Set()), initialPeers: initialPeers, initialReferencedMessageIds: Set(), initialStoredMessages: Set(), initialReadInboxMaxIds: [:], storedMessagesByPeerIdAndTimestamp: [:]) - return pollChannel(network: network, peer: peer, state: initialState) - |> mapToSignal { (finalState, _, timeout) -> Signal in - return resolveAssociatedMessages(network: network, state: finalState) - |> mapToSignal { resultingState -> Signal in - return resolveMissingPeerChatInfos(network: network, state: resultingState) - |> map { resultingState, _ -> AccountFinalState in - return AccountFinalState(state: resultingState, shouldPoll: false, incomplete: false, missingUpdatesFromChannels: Set(), discard: false) - } - } - |> mapToSignal { finalState -> Signal in - return stateManager.addReplayAsynchronouslyBuiltFinalState(finalState) - |> mapToSignal { _ -> Signal in - return .complete() |> delay(Double(timeout ?? 30), queue: Queue.concurrentDefaultQueue()) - } - } - } - } else { +func keepPollingChannel(postbox: Postbox, network: Network, peerId: PeerId, stateManager: AccountStateManager) -> Signal { + let signal: Signal = postbox.transaction { transaction -> Signal in + guard let accountState = (transaction.getState() as? AuthorizedAccountState)?.state, let peer = transaction.getPeer(peerId) else { return .complete() |> delay(30.0, queue: Queue.concurrentDefaultQueue()) } + + var channelStates: [PeerId: AccountStateChannelState] = [:] + if let channelState = transaction.getPeerChatState(peerId) as? ChannelState { + channelStates[peerId] = AccountStateChannelState(pts: channelState.pts) + } + let initialPeers: [PeerId: Peer] = [peerId: peer] + var peerChatInfos: [PeerId: PeerChatInfo] = [:] + let inclusion = transaction.getPeerChatListInclusion(peerId) + var hasValidInclusion = false + switch inclusion { + case .ifHasMessagesOrOneOf: + hasValidInclusion = true + case .notIncluded: + hasValidInclusion = false + } + if hasValidInclusion { + if let notificationSettings = transaction.getPeerNotificationSettings(peerId) as? TelegramPeerNotificationSettings { + peerChatInfos[peerId] = PeerChatInfo(notificationSettings: notificationSettings) + } + } + let initialState = AccountMutableState(initialState: AccountInitialState(state: accountState, peerIds: Set(), peerIdsRequiringLocalChatState: Set(), channelStates: channelStates, peerChatInfos: peerChatInfos, locallyGeneratedMessageTimestamps: [:], cloudReadStates: [:], channelsToPollExplicitely: Set()), initialPeers: initialPeers, initialReferencedMessageIds: Set(), initialStoredMessages: Set(), initialReadInboxMaxIds: [:], storedMessagesByPeerIdAndTimestamp: [:]) + return pollChannel(network: network, peer: peer, state: initialState) + |> mapToSignal { (finalState, _, timeout) -> Signal in + return resolveAssociatedMessages(network: network, state: finalState) + |> mapToSignal { resultingState -> Signal in + return resolveMissingPeerChatInfos(network: network, state: resultingState) + |> map { resultingState, _ -> AccountFinalState in + return AccountFinalState(state: resultingState, shouldPoll: false, incomplete: false, missingUpdatesFromChannels: Set(), discard: false) + } + } + |> mapToSignal { finalState -> Signal in + return stateManager.addReplayAsynchronouslyBuiltFinalState(finalState) + |> mapToSignal { _ -> Signal in + return .single(timeout ?? 30) |> then(.complete() |> delay(Double(timeout ?? 30), queue: Queue.concurrentDefaultQueue())) + } + } + } } |> switchToLatest |> restart @@ -1782,15 +1823,7 @@ private func resetChannels(network: Network, peers: [Peer], state: AccountMutabl continue loop } - let peerId: PeerId - switch apiPeer { - case let .peerUser(userId): - peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: userId) - case let .peerChat(chatId): - peerId = PeerId(namespace: Namespaces.Peer.CloudGroup, id: chatId) - case let .peerChannel(channelId): - peerId = PeerId(namespace: Namespaces.Peer.CloudChannel, id: channelId) - } + let peerId: PeerId = apiPeer.peerId if readStates[peerId] == nil { readStates[peerId] = [:] @@ -1958,7 +1991,7 @@ private func pollChannel(network: Network, peer: Peer, state: AccountMutableStat Logger.shared.log("State", "Invalid updateEditChannelMessage") } case let .updatePinnedChannelMessages(flags, channelId, messages, _, _): - let channelPeerId = PeerId(namespace: Namespaces.Peer.CloudChannel, id: channelId) + let channelPeerId = PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt32Value(channelId)) updatedState.updateMessagesPinned(ids: messages.map { id in MessageId(peerId: channelPeerId, namespace: Namespaces.Message.Cloud, id: id) }, pinned: (flags & (1 << 0)) != 0) @@ -2485,11 +2518,11 @@ func replayFinalState(accountManager: AccountManager, postbox: Postbox, accountP addMessageMediaResourceIdsToRemove(media: media, resourceIds: &resourceIds) }) if !resourceIds.isEmpty { - let _ = mediaBox.removeCachedResources(Set(resourceIds)).start() + let _ = mediaBox.removeCachedResources(Set(resourceIds), force: true).start() } deletedMessageIds.append(contentsOf: ids.map { .global($0) }) case let .DeleteMessages(ids): - deleteMessages(transaction: transaction, mediaBox: mediaBox, ids: ids, manualAddMessageThreadStatsDifference: { id, add, remove in + _internal_deleteMessages(transaction: transaction, mediaBox: mediaBox, ids: ids, manualAddMessageThreadStatsDifference: { id, add, remove in addMessageThreadStatsDifference(threadMessageId: id, remove: remove, addedMessagePeer: nil, addedMessageId: nil, isOutgoing: false) }) deletedMessageIds.append(contentsOf: ids.map { .messageId($0) }) @@ -2502,7 +2535,7 @@ func replayFinalState(accountManager: AccountManager, postbox: Postbox, accountP addMessageMediaResourceIdsToRemove(media: media, resourceIds: &resourceIds) }) if !resourceIds.isEmpty { - let _ = mediaBox.removeCachedResources(Set(resourceIds)).start() + let _ = mediaBox.removeCachedResources(Set(resourceIds), force: true).start() } case let .UpdatePeerChatInclusion(peerId, groupId, changedGroup): let currentInclusion = transaction.getPeerChatListInclusion(peerId) @@ -2722,6 +2755,7 @@ func replayFinalState(accountManager: AccountManager, postbox: Postbox, accountP if count == 0 { transaction.removeHole(peerId: peerId, namespace: namespace, space: .tag(.unseenPersonalMessage), range: 1 ... (Int32.max - 1)) let ids = transaction.getMessageIndicesWithTag(peerId: peerId, namespace: namespace, tag: .unseenPersonalMessage).map({ $0.id }) + Logger.shared.log("State", "will call markUnseenPersonalMessage for \(ids.count) messages") for id in ids { markUnseenPersonalMessage(transaction: transaction, id: id, addSynchronizeAction: false) } @@ -2980,22 +3014,23 @@ func replayFinalState(accountManager: AccountManager, postbox: Postbox, accountP if let info = GroupCallInfo(call) { transaction.updatePeerCachedData(peerIds: Set([peerId]), update: { _, current in if let current = current as? CachedChannelData { - return current.withUpdatedActiveCall(CachedChannelData.ActiveCall(id: info.id, accessHash: info.accessHash, title: info.title)) + return current.withUpdatedActiveCall(CachedChannelData.ActiveCall(id: info.id, accessHash: info.accessHash, title: info.title, scheduleTimestamp: info.scheduleTimestamp, subscribedToScheduled: info.subscribedToScheduled)) } else if let current = current as? CachedGroupData { - return current.withUpdatedActiveCall(CachedChannelData.ActiveCall(id: info.id, accessHash: info.accessHash, title: info.title)) + return current.withUpdatedActiveCall(CachedChannelData.ActiveCall(id: info.id, accessHash: info.accessHash, title: info.title, scheduleTimestamp: info.scheduleTimestamp, subscribedToScheduled: info.subscribedToScheduled)) } else { return current } }) switch call { - case let .groupCall(flags, _, _, _, _, title, streamDcId, recordStartDate, _): + case let .groupCall(flags, _, _, _, title, _, recordStartDate, scheduleDate, _, _, _): let isMuted = (flags & (1 << 1)) != 0 let canChange = (flags & (1 << 2)) != 0 + let isVideoEnabled = (flags & (1 << 9)) != 0 let defaultParticipantsAreMuted = GroupCallParticipantsContext.State.DefaultParticipantsAreMuted(isMuted: isMuted, canChange: canChange) updatedGroupCallParticipants.append(( info.id, - .call(isTerminated: false, defaultParticipantsAreMuted: defaultParticipantsAreMuted, title: title, recordingStartTimestamp: recordStartDate) + .call(isTerminated: false, defaultParticipantsAreMuted: defaultParticipantsAreMuted, title: title, recordingStartTimestamp: recordStartDate, scheduleTimestamp: scheduleDate, isVideoEnabled: isVideoEnabled) )) default: break @@ -3004,7 +3039,7 @@ func replayFinalState(accountManager: AccountManager, postbox: Postbox, accountP case let .groupCallDiscarded(callId, _, _): updatedGroupCallParticipants.append(( callId, - .call(isTerminated: true, defaultParticipantsAreMuted: GroupCallParticipantsContext.State.DefaultParticipantsAreMuted(isMuted: false, canChange: false), title: nil, recordingStartTimestamp: nil) + .call(isTerminated: true, defaultParticipantsAreMuted: GroupCallParticipantsContext.State.DefaultParticipantsAreMuted(isMuted: false, canChange: false), title: nil, recordingStartTimestamp: nil, scheduleTimestamp: nil, isVideoEnabled: false) )) transaction.updatePeerCachedData(peerIds: Set([peerId]), update: { _, current in @@ -3204,7 +3239,7 @@ func replayFinalState(accountManager: AccountManager, postbox: Postbox, accountP } switch set { - case let .stickerSet(flags, _, _, _, _, _, _, _, _, _): + case let .stickerSet(flags, _, _, _, _, _, _, _, _, _, _): if (flags & (1 << 3)) != 0 { namespace = Namespaces.ItemCollection.CloudMaskPacks } else { diff --git a/submodules/TelegramCore/Sources/AccountStateManager.swift b/submodules/TelegramCore/Sources/State/AccountStateManager.swift similarity index 100% rename from submodules/TelegramCore/Sources/AccountStateManager.swift rename to submodules/TelegramCore/Sources/State/AccountStateManager.swift diff --git a/submodules/TelegramCore/Sources/AccountViewTracker.swift b/submodules/TelegramCore/Sources/State/AccountViewTracker.swift similarity index 95% rename from submodules/TelegramCore/Sources/AccountViewTracker.swift rename to submodules/TelegramCore/Sources/State/AccountViewTracker.swift index 859a4b532c..8dd610993c 100644 --- a/submodules/TelegramCore/Sources/AccountViewTracker.swift +++ b/submodules/TelegramCore/Sources/State/AccountViewTracker.swift @@ -224,9 +224,21 @@ private final class CachedChannelParticipantsContext { private final class ChannelPollingContext { var subscribers = Bag() let disposable = MetaDisposable() + let isUpdated = Promise(false) + + private(set) var isUpdatedValue: Bool = false + private var isUpdatedDisposable: Disposable? + + init(queue: Queue) { + self.isUpdatedDisposable = (self.isUpdated.get() + |> deliverOn(queue)).start(next: { [weak self] value in + self?.isUpdatedValue = value + }) + } deinit { self.disposable.dispose() + self.isUpdatedDisposable?.dispose() } } @@ -717,7 +729,7 @@ public final class AccountViewTracker { switch replies { case let .messageReplies(_, repliesCountValue, _, recentRepliers, channelId, maxId, readMaxId): if let channelId = channelId { - commentsChannelId = PeerId(namespace: Namespaces.Peer.CloudChannel, id: channelId) + commentsChannelId = PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt32Value(channelId)) } repliesCount = repliesCountValue if let recentRepliers = recentRepliers { @@ -1224,7 +1236,7 @@ public final class AccountViewTracker { 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 + context.disposable.set(combineLatest(fetchAndUpdateSupplementalCachedPeerData(peerId: peerId, network: account.network, postbox: account.postbox), _internal_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 @@ -1266,7 +1278,7 @@ public final class AccountViewTracker { 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 + context.disposable.set(combineLatest(fetchAndUpdateSupplementalCachedPeerData(peerId: peerId, network: account.network, postbox: account.postbox), _internal_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 @@ -1302,13 +1314,27 @@ public final class AccountViewTracker { if let current = self.channelPollingContexts[peerId] { context = current } else { - context = ChannelPollingContext() + context = ChannelPollingContext(queue: self.queue) self.channelPollingContexts[peerId] = context } if context.subscribers.isEmpty { if let account = self.account { - context.disposable.set(keepPollingChannel(postbox: account.postbox, network: account.network, peerId: peerId, stateManager: account.stateManager).start()) + let queue = self.queue + context.disposable.set(keepPollingChannel(postbox: account.postbox, network: account.network, peerId: peerId, stateManager: account.stateManager).start(next: { [weak context] isValidForTimeout in + queue.async { + guard let context = context else { + return + } + context.isUpdated.set( + .single(true) + |> then( + .single(false) + |> delay(Double(isValidForTimeout), queue: queue) + ) + ) + } + })) } } @@ -1378,34 +1404,68 @@ public final class AccountViewTracker { peerId = peerIdValue } if peerId.namespace == Namespaces.Peer.CloudChannel { - return Signal { subscriber in + return Signal<(MessageHistoryView, ViewUpdateType, InitialMessageHistoryData?), NoError> { subscriber in let combinedDisposable = MetaDisposable() self.queue.async { + let polled = self.polledChannel(peerId: peerId).start() + var addHole = false + let pollingCompleted: Signal if let context = self.channelPollingContexts[peerId] { - if context.subscribers.isEmpty { + if !context.isUpdatedValue { addHole = true } + pollingCompleted = context.isUpdated.get() } else { addHole = true + pollingCompleted = .single(true) } - if addHole { - let _ = self.account?.postbox.transaction({ transaction -> Void in - if transaction.getPeerChatListIndex(peerId) == nil { - if let message = transaction.getTopPeerMessageId(peerId: peerId, namespace: Namespaces.Message.Cloud) { - //transaction.addHole(peerId: peerId, namespace: Namespaces.Message.Cloud, space: .everywhere, range: message.id + 1 ... (Int32.max - 1)) - } + let isAutomaticallyTracked = self.account!.postbox.transaction { transaction -> Bool in + if transaction.getPeerChatListIndex(peerId) == nil { + if addHole { + transaction.addHole(peerId: peerId, namespace: Namespaces.Message.Cloud, space: .everywhere, range: 1 ... (Int32.max - 1)) } - }).start() + return false + } else { + return true + } } - let disposable = history.start(next: { next in + + let historyIsValid = combineLatest(queue: self.queue, + pollingCompleted, + isAutomaticallyTracked + ) + |> map { lhs, rhs -> Bool in + return lhs || rhs + } + + var loaded = false + let validHistory = historyIsValid + |> distinctUntilChanged + |> take(until: { next in + if next { + return SignalTakeAction(passthrough: true, complete: true) + } else { + return SignalTakeAction(passthrough: true, complete: false) + } + }) + |> mapToSignal { isValid -> Signal<(MessageHistoryView, ViewUpdateType, InitialMessageHistoryData?), NoError> in + if isValid { + assert(!loaded) + loaded = true + return history + } else { + let view = MessageHistoryView(tagMask: nil, namespaces: .all, entries: [], holeEarlier: true, holeLater: true, isLoading: true) + return .single((view, .Initial, nil)) + } + } + + let disposable = validHistory.start(next: { next in subscriber.putNext(next) - }, error: { error in - subscriber.putError(error) }, completed: { subscriber.putCompletion() }) - let polled = self.polledChannel(peerId: peerId).start() + combinedDisposable.set(ActionDisposable { disposable.dispose() polled.dispose() diff --git a/submodules/TelegramCore/Sources/AppChangelog.swift b/submodules/TelegramCore/Sources/State/AppChangelog.swift similarity index 100% rename from submodules/TelegramCore/Sources/AppChangelog.swift rename to submodules/TelegramCore/Sources/State/AppChangelog.swift diff --git a/submodules/TelegramCore/Sources/AppChangelogState.swift b/submodules/TelegramCore/Sources/State/AppChangelogState.swift similarity index 100% rename from submodules/TelegramCore/Sources/AppChangelogState.swift rename to submodules/TelegramCore/Sources/State/AppChangelogState.swift diff --git a/submodules/TelegramCore/Sources/AppConfiguration.swift b/submodules/TelegramCore/Sources/State/AppConfiguration.swift similarity index 86% rename from submodules/TelegramCore/Sources/AppConfiguration.swift rename to submodules/TelegramCore/Sources/State/AppConfiguration.swift index 827589c8ad..1599a52130 100644 --- a/submodules/TelegramCore/Sources/AppConfiguration.swift +++ b/submodules/TelegramCore/Sources/State/AppConfiguration.swift @@ -1,7 +1,7 @@ import Postbox import SyncCore -public func currentAppConfiguration(transaction: Transaction) -> AppConfiguration { +private func currentAppConfiguration(transaction: Transaction) -> AppConfiguration { if let entry = transaction.getPreferencesEntry(key: PreferencesKeys.appConfiguration) as? AppConfiguration { return entry } else { diff --git a/submodules/TelegramCore/Sources/AppUpdate.swift b/submodules/TelegramCore/Sources/State/AppUpdate.swift similarity index 99% rename from submodules/TelegramCore/Sources/AppUpdate.swift rename to submodules/TelegramCore/Sources/State/AppUpdate.swift index 5ab4dc5c7d..546b6b8778 100644 --- a/submodules/TelegramCore/Sources/AppUpdate.swift +++ b/submodules/TelegramCore/Sources/State/AppUpdate.swift @@ -16,7 +16,7 @@ public struct AppUpdateInfo: Equatable { extension AppUpdateInfo { init?(apiAppUpdate: Api.help.AppUpdate) { switch apiAppUpdate { - case let .appUpdate(flags, _, version, text, entities, _, _): + case let .appUpdate(flags, _, version, text, entities, _, _, _): self.blocking = (flags & (1 << 0)) != 0 self.version = version self.text = text diff --git a/submodules/TelegramCore/Sources/ApplyUpdateMessage.swift b/submodules/TelegramCore/Sources/State/ApplyUpdateMessage.swift similarity index 100% rename from submodules/TelegramCore/Sources/ApplyUpdateMessage.swift rename to submodules/TelegramCore/Sources/State/ApplyUpdateMessage.swift diff --git a/submodules/TelegramCore/Sources/CachedSentMediaReferences.swift b/submodules/TelegramCore/Sources/State/CachedSentMediaReferences.swift similarity index 100% rename from submodules/TelegramCore/Sources/CachedSentMediaReferences.swift rename to submodules/TelegramCore/Sources/State/CachedSentMediaReferences.swift diff --git a/submodules/TelegramCore/Sources/CallSessionManager.swift b/submodules/TelegramCore/Sources/State/CallSessionManager.swift similarity index 95% rename from submodules/TelegramCore/Sources/CallSessionManager.swift rename to submodules/TelegramCore/Sources/State/CallSessionManager.swift index 69401d05cb..7d6cdfde5a 100644 --- a/submodules/TelegramCore/Sources/CallSessionManager.swift +++ b/submodules/TelegramCore/Sources/State/CallSessionManager.swift @@ -246,7 +246,7 @@ private final class CallSessionContext { var isVideoPossible: Bool var state: CallSessionInternalState let subscribers = Bag<(CallSession) -> Void>() - let signalingSubscribers = Bag<(Data) -> Void>() + var signalingReceiver: (([Data]) -> Void)? let signalingDisposables = DisposableSet() @@ -254,7 +254,7 @@ private final class CallSessionContext { var isEmpty: Bool { if case .terminated = self.state { - return self.subscribers.isEmpty && self.signalingSubscribers.isEmpty + return self.subscribers.isEmpty } else { return false } @@ -303,6 +303,8 @@ private final class CallSessionManagerContext { private let ringingSubscribers = Bag<([CallSessionRingingState]) -> Void>() private var contexts: [CallSessionInternalId: CallSessionContext] = [:] private var contextIdByStableId: [CallSessionStableId: CallSessionInternalId] = [:] + + private var enqueuedSignalingData: [Int64: [Data]] = [:] private let disposables = DisposableSet() @@ -395,29 +397,31 @@ private final class CallSessionManagerContext { } } - func callSignalingData(internalId: CallSessionInternalId) -> Signal { + func beginReceivingCallSignalingData(internalId: CallSessionInternalId, _ receiver: @escaping ([Data]) -> Void) -> Disposable { let queue = self.queue - return Signal { [weak self] subscriber in - let disposable = MetaDisposable() - queue.async { - if let strongSelf = self, let context = strongSelf.contexts[internalId] { - let index = context.signalingSubscribers.add { next in - subscriber.putNext(next) + + let disposable = MetaDisposable() + queue.async { [weak self] in + if let strongSelf = self, let context = strongSelf.contexts[internalId] { + context.signalingReceiver = receiver + + for (listStableId, listInternalId) in strongSelf.contextIdByStableId { + if listInternalId == internalId { + strongSelf.deliverCallSignalingData(id: listStableId) + break } - disposable.set(ActionDisposable { - queue.async { - if let strongSelf = self, let context = strongSelf.contexts[internalId] { - context.signalingSubscribers.remove(index) - if context.isEmpty { - strongSelf.contexts.removeValue(forKey: internalId) - } - } - } - }) } + + disposable.set(ActionDisposable { + queue.async { + if let strongSelf = self, let context = strongSelf.contexts[internalId] { + context.signalingReceiver = nil + } + } + }) } - return disposable } + return disposable } private func ringingStatesValue() -> [CallSessionRingingState] { @@ -475,6 +479,7 @@ private final class CallSessionManagerContext { })) self.contextIdByStableId[stableId] = internalId self.contextUpdated(internalId: internalId) + self.deliverCallSignalingData(id: stableId) self.ringingStatesUpdated() return internalId } else { @@ -558,7 +563,7 @@ private final class CallSessionManagerContext { |> 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() + _internal_saveCallDebugLog(network: network, callId: CallId(id: id, accessHash: accessHash), log: debugLog).start() } }) } @@ -596,7 +601,7 @@ private final class CallSessionManagerContext { func accept(internalId: CallSessionInternalId) { if let context = self.contexts[internalId] { switch context.state { - case let .ringing(id, accessHash, gAHash, b, remoteVersions): + case let .ringing(id, accessHash, gAHash, b, _): let acceptVersions = self.versions.map({ $0.version }) context.state = .accepting(id: id, accessHash: accessHash, gAHash: gAHash, b: b, disposable: (acceptCallSession(postbox: self.postbox, network: self.network, stableId: id, accessHash: accessHash, b: b, maxLayer: self.maxLayer, versions: acceptVersions) |> deliverOn(self.queue)).start(next: { [weak self] result in if let strongSelf = self, let context = strongSelf.contexts[internalId] { @@ -652,7 +657,7 @@ private final class CallSessionManagerContext { switch call { case .phoneCallEmpty: break - case let .phoneCallAccepted(flags, id, _, _, _, _, gB, remoteProtocol): + case let .phoneCallAccepted(_, id, _, _, _, _, gB, remoteProtocol): let remoteVersions: [String] switch remoteProtocol { case let .phoneCallProtocol(_, _, _, versions): @@ -815,7 +820,7 @@ private final class CallSessionManagerContext { versions = libraryVersions } if self.contextIdByStableId[id] == nil { - let internalId = self.addIncoming(peerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: adminId), stableId: id, accessHash: accessHash, timestamp: date, gAHash: gAHash.makeData(), versions: versions, isVideo: isVideo) + let internalId = self.addIncoming(peerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt32Value(adminId)), stableId: id, accessHash: accessHash, timestamp: date, gAHash: gAHash.makeData(), versions: versions, isVideo: isVideo) if let internalId = internalId { var resultRingingStateValue: CallSessionRingingState? for ringingState in self.ringingStatesValue() { @@ -854,11 +859,22 @@ private final class CallSessionManagerContext { } func addCallSignalingData(id: Int64, data: Data) { + if self.enqueuedSignalingData[id] == nil { + self.enqueuedSignalingData[id] = [] + } + self.enqueuedSignalingData[id]?.append(data) + + self.deliverCallSignalingData(id: id) + } + + private func deliverCallSignalingData(id: Int64) { guard let internalId = self.contextIdByStableId[id], let context = self.contexts[internalId] else { return } - for f in context.signalingSubscribers.copyItems() { - f(data) + if let signalingReceiver = context.signalingReceiver { + if let data = self.enqueuedSignalingData.removeValue(forKey: id) { + signalingReceiver(data) + } } } @@ -894,7 +910,7 @@ private final class CallSessionManagerContext { let randomStatus = SecRandomCopyBytes(nil, 256, aBytes.assumingMemoryBound(to: UInt8.self)) let a = Data(bytesNoCopy: aBytes, count: 256, deallocator: .free) if randomStatus == 0 { - self.contexts[internalId] = CallSessionContext(peerId: peerId, isOutgoing: true, type: isVideo ? .video : .audio, isVideoPossible: enableVideo || isVideo, state: .requesting(a: a, disposable: (requestCallSession(postbox: self.postbox, network: self.network, peerId: peerId, a: a, maxLayer: self.maxLayer, versions: self.filteredVersions(enableVideo: enableVideo), isVideo: isVideo) |> deliverOn(queue)).start(next: { [weak self] result in + self.contexts[internalId] = CallSessionContext(peerId: peerId, isOutgoing: true, type: isVideo ? .video : .audio, isVideoPossible: enableVideo || isVideo, state: .requesting(a: a, disposable: (requestCallSession(postbox: self.postbox, network: self.network, peerId: peerId, a: a, maxLayer: self.maxLayer, versions: self.filteredVersions(enableVideo: true), isVideo: isVideo) |> deliverOn(queue)).start(next: { [weak self] result in if let strongSelf = self, let context = strongSelf.contexts[internalId] { if case .requesting = context.state { switch result { @@ -902,6 +918,7 @@ private final class CallSessionManagerContext { context.state = .requested(id: id, accessHash: accessHash, a: a, gA: gA, config: config, remoteConfirmationTimestamp: remoteConfirmationTimestamp) strongSelf.contextIdByStableId[id] = internalId strongSelf.contextUpdated(internalId: internalId) + strongSelf.deliverCallSignalingData(id: id) case let .failed(error): context.state = .terminated(id: nil, accessHash: nil, reason: .error(error), reportRating: false, sendDebugLogs: false) strongSelf.contextUpdated(internalId: internalId) @@ -1044,16 +1061,14 @@ public final class CallSessionManager { } } - public func callSignalingData(internalId: CallSessionInternalId) -> Signal { - return Signal { [weak self] subscriber in - let disposable = MetaDisposable() - self?.withContext { context in - disposable.set(context.callSignalingData(internalId: internalId).start(next: { next in - subscriber.putNext(next) - })) - } - return disposable + public func beginReceivingCallSignalingData(internalId: CallSessionInternalId, _ receiver: @escaping ([Data]) -> Void) -> Disposable { + let disposable = MetaDisposable() + + self.withContext { context in + disposable.set(context.beginReceivingCallSignalingData(internalId: internalId, receiver)) } + + return disposable } } diff --git a/submodules/TelegramCore/Sources/ChannelState.swift b/submodules/TelegramCore/Sources/State/ChannelState.swift similarity index 90% rename from submodules/TelegramCore/Sources/ChannelState.swift rename to submodules/TelegramCore/Sources/State/ChannelState.swift index 504cd68f65..effa4e6c62 100644 --- a/submodules/TelegramCore/Sources/ChannelState.swift +++ b/submodules/TelegramCore/Sources/State/ChannelState.swift @@ -18,11 +18,11 @@ func channelUpdatesByPeerId(updates: [ChannelUpdate]) -> [PeerId: [ChannelUpdate case let .updateNewChannelMessage(message, _, _): peerId = apiMessagePeerId(message) case let .updateDeleteChannelMessages(channelId, _, _, _): - peerId = PeerId(namespace: Namespaces.Peer.CloudChannel, id: channelId) + peerId = PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt32Value(channelId)) case let .updateEditChannelMessage(message, _, _): peerId = apiMessagePeerId(message) case let .updateChannelWebPage(channelId, _, _, _): - peerId = PeerId(namespace: Namespaces.Peer.CloudChannel, id: channelId) + peerId = PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt32Value(channelId)) default: break } diff --git a/submodules/TelegramCore/Sources/ChatHistoryPreloadManager.swift b/submodules/TelegramCore/Sources/State/ChatHistoryPreloadManager.swift similarity index 100% rename from submodules/TelegramCore/Sources/ChatHistoryPreloadManager.swift rename to submodules/TelegramCore/Sources/State/ChatHistoryPreloadManager.swift diff --git a/submodules/TelegramCore/Sources/CloudChatRemoveMessagesOperation.swift b/submodules/TelegramCore/Sources/State/CloudChatRemoveMessagesOperation.swift similarity index 100% rename from submodules/TelegramCore/Sources/CloudChatRemoveMessagesOperation.swift rename to submodules/TelegramCore/Sources/State/CloudChatRemoveMessagesOperation.swift diff --git a/submodules/TelegramCore/Sources/ContactSyncManager.swift b/submodules/TelegramCore/Sources/State/ContactSyncManager.swift similarity index 98% rename from submodules/TelegramCore/Sources/ContactSyncManager.swift rename to submodules/TelegramCore/Sources/State/ContactSyncManager.swift index 83b1d3db2a..b5b4aca039 100644 --- a/submodules/TelegramCore/Sources/ContactSyncManager.swift +++ b/submodules/TelegramCore/Sources/State/ContactSyncManager.swift @@ -345,7 +345,7 @@ private func pushDeviceContactData(postbox: Postbox, network: Network, contacts: for item in imported { switch item { case let .importedContact(userId, _): - addedContactPeerIds.insert(PeerId(namespace: Namespaces.Peer.CloudUser, id: userId)) + addedContactPeerIds.insert(PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt32Value(userId))) } } for item in retryContacts { @@ -399,7 +399,7 @@ private func updateContactPresences(postbox: Postbox, network: Network, accountP for status in statuses { switch status { case let .contactStatus(userId, status): - peerPresences[PeerId(namespace: Namespaces.Peer.CloudUser, id: userId)] = TelegramUserPresence(apiStatus: status) + peerPresences[PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt32Value(userId))] = TelegramUserPresence(apiStatus: status) } } updatePeerPresences(transaction: transaction, accountPeerId: accountPeerId, peerPresences: peerPresences) diff --git a/submodules/TelegramCore/Sources/Fetch.swift b/submodules/TelegramCore/Sources/State/Fetch.swift similarity index 76% rename from submodules/TelegramCore/Sources/Fetch.swift rename to submodules/TelegramCore/Sources/State/Fetch.swift index 34b3a5c0c8..a8a796d242 100644 --- a/submodules/TelegramCore/Sources/Fetch.swift +++ b/submodules/TelegramCore/Sources/State/Fetch.swift @@ -63,6 +63,21 @@ func fetchResource(account: Account, resource: MediaResource, intervals: Signal< } else if let httpReference = resource as? HttpReferenceMediaResource { return .single(.dataPart(resourceOffset: 0, data: Data(), range: 0 ..< 0, complete: false)) |> then(fetchHttpResource(url: httpReference.url)) + } else if let wallpaperResource = resource as? WallpaperDataResource { + return getWallpaper(network: account.network, slug: wallpaperResource.slug) + |> mapError { _ -> MediaResourceDataFetchError in + return .generic + } + |> mapToSignal { wallpaper -> Signal in + guard case let .file(file) = wallpaper else { + return .fail(.generic) + } + guard let cloudResource = file.file.resource as? TelegramMultipartFetchableResource else { + return .fail(.generic) + } + return .single(.dataPart(resourceOffset: 0, data: Data(), range: 0 ..< 0, complete: false)) + |> then(fetchCloudMediaLocation(account: account, resource: cloudResource, datacenterId: cloudResource.datacenterId, size: resource.size == 0 ? nil : resource.size, intervals: intervals, parameters: MediaResourceFetchParameters(tag: nil, info: TelegramCloudMediaResourceFetchInfo(reference: .standalone(resource: file.file.resource), preferBackgroundReferenceRevalidation: false, continueInBackground: false), isRandomAccessAllowed: true))) + } } return nil } diff --git a/submodules/TelegramCore/Sources/FetchChatList.swift b/submodules/TelegramCore/Sources/State/FetchChatList.swift similarity index 99% rename from submodules/TelegramCore/Sources/FetchChatList.swift rename to submodules/TelegramCore/Sources/State/FetchChatList.swift index 9da9b60c2c..1fdf736c59 100644 --- a/submodules/TelegramCore/Sources/FetchChatList.swift +++ b/submodules/TelegramCore/Sources/State/FetchChatList.swift @@ -113,11 +113,11 @@ private func parseDialogs(apiDialogs: [Api.Dialog], apiMessages: [Api.Message], let peerId: PeerId switch apiPeer { case let .peerUser(userId): - peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: userId) + peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt32Value(userId)) case let .peerChat(chatId): - peerId = PeerId(namespace: Namespaces.Peer.CloudGroup, id: chatId) + peerId = PeerId(namespace: Namespaces.Peer.CloudGroup, id: PeerId.Id._internalFromInt32Value(chatId)) case let .peerChannel(channelId): - peerId = PeerId(namespace: Namespaces.Peer.CloudChannel, id: channelId) + peerId = PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt32Value(channelId)) } if readStates[peerId] == nil { diff --git a/submodules/TelegramCore/Sources/FetchSecretFileResource.swift b/submodules/TelegramCore/Sources/State/FetchSecretFileResource.swift similarity index 100% rename from submodules/TelegramCore/Sources/FetchSecretFileResource.swift rename to submodules/TelegramCore/Sources/State/FetchSecretFileResource.swift diff --git a/submodules/TelegramCore/Sources/HistoryViewStateValidation.swift b/submodules/TelegramCore/Sources/State/HistoryViewStateValidation.swift similarity index 99% rename from submodules/TelegramCore/Sources/HistoryViewStateValidation.swift rename to submodules/TelegramCore/Sources/State/HistoryViewStateValidation.swift index f66c02d007..9dfa747529 100644 --- a/submodules/TelegramCore/Sources/HistoryViewStateValidation.swift +++ b/submodules/TelegramCore/Sources/State/HistoryViewStateValidation.swift @@ -410,7 +410,7 @@ private func hashForMessages(_ messages: [Message], withChannelIds: Bool) -> Int for message in sorted { if withChannelIds { - acc = (acc &* 20261) &+ UInt32(message.id.peerId.id) + acc = (acc &* 20261) &+ UInt32(message.id.peerId.id._internalGetInt32Value()) } acc = (acc &* 20261) &+ UInt32(message.id.id) @@ -435,7 +435,7 @@ private func hashForMessages(_ messages: [StoreMessage], withChannelIds: Bool) - for message in messages { if case let .Id(id) = message.id { if withChannelIds { - acc = (acc &* 20261) &+ UInt32(id.peerId.id) + acc = (acc &* 20261) &+ UInt32(id.peerId.id._internalGetInt32Value()) } acc = (acc &* 20261) &+ UInt32(id.id) var timestamp = message.timestamp @@ -832,7 +832,7 @@ private func validateBatch(postbox: Postbox, network: Network, transaction: Tran return .update(StoreMessage(id: currentMessage.id, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, threadId: currentMessage.threadId, timestamp: currentMessage.timestamp, flags: StoreMessageFlags(currentMessage.flags), tags: updatedTags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: storeForwardInfo, authorId: currentMessage.author?.id, text: currentMessage.text, attributes: attributes, media: currentMessage.media)) }) } else { - deleteMessages(transaction: transaction, mediaBox: postbox.mediaBox, ids: [id]) + _internal_deleteMessages(transaction: transaction, mediaBox: postbox.mediaBox, ids: [id]) Logger.shared.log("HistoryValidation", "deleting message \(id) in \(id.peerId)") } } @@ -1011,7 +1011,7 @@ private func validateReplyThreadBatch(postbox: Postbox, network: Network, transa for id in removedMessageIds { if !validMessageIds.contains(id) { - deleteMessages(transaction: transaction, mediaBox: postbox.mediaBox, ids: [id]) + _internal_deleteMessages(transaction: transaction, mediaBox: postbox.mediaBox, ids: [id]) Logger.shared.log("HistoryValidation", "deleting thread message \(id) in \(id.peerId)") } } diff --git a/submodules/TelegramCore/Sources/Holes.swift b/submodules/TelegramCore/Sources/State/Holes.swift similarity index 98% rename from submodules/TelegramCore/Sources/Holes.swift rename to submodules/TelegramCore/Sources/State/Holes.swift index fc66edb831..946a9faea0 100644 --- a/submodules/TelegramCore/Sources/Holes.swift +++ b/submodules/TelegramCore/Sources/State/Holes.swift @@ -573,13 +573,13 @@ func fetchMessageHistoryHole(accountPeerId: PeerId, source: FetchMessageHistoryH func groupBoundaryPeer(_ peerId: PeerId, accountPeerId: PeerId) -> Api.Peer { switch peerId.namespace { case Namespaces.Peer.CloudUser: - return Api.Peer.peerUser(userId: peerId.id) + return Api.Peer.peerUser(userId: peerId.id._internalGetInt32Value()) case Namespaces.Peer.CloudGroup: - return Api.Peer.peerChat(chatId: peerId.id) + return Api.Peer.peerChat(chatId: peerId.id._internalGetInt32Value()) case Namespaces.Peer.CloudChannel: - return Api.Peer.peerChannel(channelId: peerId.id) + return Api.Peer.peerChannel(channelId: peerId.id._internalGetInt32Value()) default: - return Api.Peer.peerUser(userId: accountPeerId.id) + return Api.Peer.peerUser(userId: accountPeerId.id._internalGetInt32Value()) } } @@ -698,7 +698,7 @@ func fetchCallListHole(network: Network, postbox: Postbox, accountPeerId: PeerId var updatedIndex: MessageIndex? if let topIndex = topIndex { - updatedIndex = topIndex.predecessor() + updatedIndex = topIndex.globalPredecessor() } transaction.replaceGlobalMessageTagsHole(globalTags: [.Calls, .MissedCalls], index: holeIndex, with: updatedIndex, messages: storeMessages) diff --git a/submodules/TelegramCore/Sources/InitializeAccountAfterLogin.swift b/submodules/TelegramCore/Sources/State/InitializeAccountAfterLogin.swift similarity index 100% rename from submodules/TelegramCore/Sources/InitializeAccountAfterLogin.swift rename to submodules/TelegramCore/Sources/State/InitializeAccountAfterLogin.swift diff --git a/submodules/TelegramCore/Sources/ManagedAccountPresence.swift b/submodules/TelegramCore/Sources/State/ManagedAccountPresence.swift similarity index 100% rename from submodules/TelegramCore/Sources/ManagedAccountPresence.swift rename to submodules/TelegramCore/Sources/State/ManagedAccountPresence.swift diff --git a/submodules/TelegramCore/Sources/ManagedAnimatedEmojiUpdates.swift b/submodules/TelegramCore/Sources/State/ManagedAnimatedEmojiUpdates.swift similarity index 75% rename from submodules/TelegramCore/Sources/ManagedAnimatedEmojiUpdates.swift rename to submodules/TelegramCore/Sources/State/ManagedAnimatedEmojiUpdates.swift index bb16cf50fa..1577d99a51 100644 --- a/submodules/TelegramCore/Sources/ManagedAnimatedEmojiUpdates.swift +++ b/submodules/TelegramCore/Sources/State/ManagedAnimatedEmojiUpdates.swift @@ -5,7 +5,7 @@ import TelegramApi import MtProtoKit func managedAnimatedEmojiUpdates(postbox: Postbox, network: Network) -> Signal { - let poll = loadedStickerPack(postbox: postbox, network: network, reference: .animatedEmoji, forceActualized: true) + let poll = _internal_loadedStickerPack(postbox: postbox, network: network, reference: .animatedEmoji, forceActualized: true) |> mapToSignal { _ -> Signal in return .complete() } diff --git a/submodules/TelegramCore/Sources/ManagedAppConfigurationUpdates.swift b/submodules/TelegramCore/Sources/State/ManagedAppConfigurationUpdates.swift similarity index 93% rename from submodules/TelegramCore/Sources/ManagedAppConfigurationUpdates.swift rename to submodules/TelegramCore/Sources/State/ManagedAppConfigurationUpdates.swift index b7a757eee1..eb4fab79b5 100644 --- a/submodules/TelegramCore/Sources/ManagedAppConfigurationUpdates.swift +++ b/submodules/TelegramCore/Sources/State/ManagedAppConfigurationUpdates.swift @@ -30,7 +30,9 @@ func updateAppConfigurationOnce(postbox: Postbox, network: Network) -> Signal Signal { let poll = Signal { subscriber in - return updateAppConfigurationOnce(postbox: postbox, network: network).start() + return updateAppConfigurationOnce(postbox: postbox, network: network).start(completed: { + subscriber.putCompletion() + }) } return (poll |> then(.complete() |> suspendAwareDelay(1.0 * 60.0 * 60.0, queue: Queue.concurrentDefaultQueue()))) |> restart } diff --git a/submodules/TelegramCore/Sources/ManagedAutodownloadSettingsUpdates.swift b/submodules/TelegramCore/Sources/State/ManagedAutodownloadSettingsUpdates.swift similarity index 95% rename from submodules/TelegramCore/Sources/ManagedAutodownloadSettingsUpdates.swift rename to submodules/TelegramCore/Sources/State/ManagedAutodownloadSettingsUpdates.swift index 620fe11e9c..e68464b5db 100644 --- a/submodules/TelegramCore/Sources/ManagedAutodownloadSettingsUpdates.swift +++ b/submodules/TelegramCore/Sources/State/ManagedAutodownloadSettingsUpdates.swift @@ -14,7 +14,9 @@ func managedAutodownloadSettingsUpdates(accountManager: AccountManager, network: return updateAutodownloadSettingsInteractively(accountManager: accountManager, { _ -> AutodownloadSettings in return AutodownloadSettings(apiAutodownloadSettings: result) }) - }).start() + }).start(completed: { + subscriber.putCompletion() + }) } return (poll |> then(.complete() |> suspendAwareDelay(1.0 * 60.0 * 60.0, queue: Queue.concurrentDefaultQueue()))) |> restart } diff --git a/submodules/TelegramCore/Sources/ManagedAutoremoveMessageOperations.swift b/submodules/TelegramCore/Sources/State/ManagedAutoremoveMessageOperations.swift similarity index 89% rename from submodules/TelegramCore/Sources/ManagedAutoremoveMessageOperations.swift rename to submodules/TelegramCore/Sources/State/ManagedAutoremoveMessageOperations.swift index 6fea6b7b9d..cb3cb92aad 100644 --- a/submodules/TelegramCore/Sources/ManagedAutoremoveMessageOperations.swift +++ b/submodules/TelegramCore/Sources/State/ManagedAutoremoveMessageOperations.swift @@ -62,8 +62,12 @@ func managedAutoremoveMessageOperations(network: Network, postbox: Postbox, isRe |> distinctUntilChanged*/ let timeOffset: Signal = .single(0.0) - - let disposable = combineLatest(timeOffset, postbox.timestampBasedMessageAttributesView(tag: isRemove ? 0 : 1)).start(next: { timeOffset, view in + + Logger.shared.log("Autoremove", "starting isRemove: \(isRemove)") + + let tag: UInt16 = isRemove ? 0 : 1 + + let disposable = combineLatest(timeOffset, postbox.timestampBasedMessageAttributesView(tag: tag)).start(next: { timeOffset, view in let (disposeOperations, beginOperations) = helper.with { helper -> (disposeOperations: [Disposable], beginOperations: [(TimestampBasedMessageAttributesEntry, MetaDisposable)]) in return helper.update(view.head) } @@ -79,9 +83,11 @@ func managedAutoremoveMessageOperations(network: Network, postbox: Postbox, isRe let signal = Signal.complete() |> suspendAwareDelay(delay, queue: Queue.concurrentDefaultQueue()) |> then(postbox.transaction { transaction -> Void in + Logger.shared.log("Autoremove", "Performing autoremove for \(entry.messageId), isRemove: \(isRemove)") + if let message = transaction.getMessage(entry.messageId) { if message.id.peerId.namespace == Namespaces.Peer.SecretChat || isRemove { - deleteMessages(transaction: transaction, mediaBox: postbox.mediaBox, ids: [entry.messageId]) + _internal_deleteMessages(transaction: transaction, mediaBox: postbox.mediaBox, ids: [entry.messageId]) } else { transaction.updateMessage(message.id, update: { currentMessage in var storeForwardInfo: StoreMessageForwardInfo? @@ -106,6 +112,9 @@ func managedAutoremoveMessageOperations(network: Network, postbox: Postbox, isRe return .update(StoreMessage(id: currentMessage.id, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, threadId: currentMessage.threadId, timestamp: currentMessage.timestamp, flags: StoreMessageFlags(currentMessage.flags), tags: currentMessage.tags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: storeForwardInfo, authorId: currentMessage.author?.id, text: currentMessage.text, attributes: updatedAttributes, media: updatedMedia)) }) } + } else { + transaction.clearTimestampBasedAttribute(id: entry.messageId, tag: tag) + Logger.shared.log("Autoremove", "No message to autoremove for \(entry.messageId)") } }) disposable.set(signal.start()) diff --git a/submodules/TelegramCore/Sources/ManagedChatListHoles.swift b/submodules/TelegramCore/Sources/State/ManagedChatListHoles.swift similarity index 100% rename from submodules/TelegramCore/Sources/ManagedChatListHoles.swift rename to submodules/TelegramCore/Sources/State/ManagedChatListHoles.swift diff --git a/submodules/TelegramCore/Sources/ManagedCloudChatRemoveMessagesOperations.swift b/submodules/TelegramCore/Sources/State/ManagedCloudChatRemoveMessagesOperations.swift similarity index 94% rename from submodules/TelegramCore/Sources/ManagedCloudChatRemoveMessagesOperations.swift rename to submodules/TelegramCore/Sources/State/ManagedCloudChatRemoveMessagesOperations.swift index 6e769af8a3..eb40e138ec 100644 --- a/submodules/TelegramCore/Sources/ManagedCloudChatRemoveMessagesOperations.swift +++ b/submodules/TelegramCore/Sources/State/ManagedCloudChatRemoveMessagesOperations.swift @@ -96,7 +96,7 @@ func managedCloudChatRemoveMessagesOperations(postbox: Postbox, network: Network } } else if let operation = entry.contents as? CloudChatClearHistoryOperation { if let peer = transaction.getPeer(entry.peerId) { - return clearHistory(transaction: transaction, postbox: postbox, network: network, stateManager: stateManager, peer: peer, operation: operation) + return _internal_clearHistory(transaction: transaction, postbox: postbox, network: network, stateManager: stateManager, peer: peer, operation: operation) } else { return .complete() } @@ -177,7 +177,7 @@ private func removeMessages(postbox: Postbox, network: Network, stateManager: Ac if let result = result { switch result { case let .affectedMessages(pts, ptsCount): - stateManager.addUpdateGroups([.updateChannelPts(channelId: peer.id.id, pts: pts, ptsCount: ptsCount)]) + stateManager.addUpdateGroups([.updateChannelPts(channelId: peer.id.id._internalGetInt32Value(), pts: pts, ptsCount: ptsCount)]) } } return .complete() @@ -267,7 +267,7 @@ private func removeChat(transaction: Transaction, postbox: Postbox, network: Net } else if peer.id.namespace == Namespaces.Peer.CloudGroup { let deleteUser: Signal if operation.deleteGloballyIfPossible { - deleteUser = network.request(Api.functions.messages.deleteChat(chatId: peer.id.id)) + deleteUser = network.request(Api.functions.messages.deleteChat(chatId: peer.id.id._internalGetInt32Value())) |> `catch` { _ in return .single(.boolFalse) } @@ -275,7 +275,7 @@ private func removeChat(transaction: Transaction, postbox: Postbox, network: Net return .complete() } } else { - deleteUser = network.request(Api.functions.messages.deleteChatUser(flags: 0, chatId: peer.id.id, userId: Api.InputUser.inputUserSelf)) + deleteUser = network.request(Api.functions.messages.deleteChatUser(flags: 0, chatId: peer.id.id._internalGetInt32Value(), userId: Api.InputUser.inputUserSelf)) |> map { result -> Api.Updates? in return result } @@ -311,7 +311,7 @@ private func removeChat(transaction: Transaction, postbox: Postbox, network: Net |> then(deleteUser) |> then(reportSignal) |> then(postbox.transaction { transaction -> Void in - clearHistory(transaction: transaction, mediaBox: postbox.mediaBox, peerId: peer.id, namespaces: .all) + _internal_clearHistory(transaction: transaction, mediaBox: postbox.mediaBox, peerId: peer.id, namespaces: .all) }) } else if peer.id.namespace == Namespaces.Peer.CloudUser { if let inputPeer = apiInputPeer(peer) { @@ -330,7 +330,7 @@ private func removeChat(transaction: Transaction, postbox: Postbox, network: Net return requestClearHistory(postbox: postbox, network: network, stateManager: stateManager, inputPeer: inputPeer, maxId: operation.topMessageId?.id ?? Int32.max - 1, justClear: false, type: operation.deleteGloballyIfPossible ? .forEveryone : .forLocalPeer) |> then(reportSignal) |> then(postbox.transaction { transaction -> Void in - clearHistory(transaction: transaction, mediaBox: postbox.mediaBox, peerId: peer.id, namespaces: .not(Namespaces.Message.allScheduled)) + _internal_clearHistory(transaction: transaction, mediaBox: postbox.mediaBox, peerId: peer.id, namespaces: .not(Namespaces.Message.allScheduled)) }) } else { return .complete() @@ -376,7 +376,7 @@ private func requestClearHistory(postbox: Postbox, network: Network, stateManage } } -private func clearHistory(transaction: Transaction, postbox: Postbox, network: Network, stateManager: AccountStateManager, peer: Peer, operation: CloudChatClearHistoryOperation) -> Signal { +private func _internal_clearHistory(transaction: Transaction, postbox: Postbox, network: Network, stateManager: AccountStateManager, peer: Peer, operation: CloudChatClearHistoryOperation) -> Signal { if peer.id.namespace == Namespaces.Peer.CloudGroup || peer.id.namespace == Namespaces.Peer.CloudUser { if let inputPeer = apiInputPeer(peer) { return requestClearHistory(postbox: postbox, network: network, stateManager: stateManager, inputPeer: inputPeer, maxId: operation.topMessageId.id, justClear: true, type: operation.type) diff --git a/submodules/TelegramCore/Sources/ManagedConfigurationUpdates.swift b/submodules/TelegramCore/Sources/State/ManagedConfigurationUpdates.swift similarity index 98% rename from submodules/TelegramCore/Sources/ManagedConfigurationUpdates.swift rename to submodules/TelegramCore/Sources/State/ManagedConfigurationUpdates.swift index 39de484df8..4818ad8f99 100644 --- a/submodules/TelegramCore/Sources/ManagedConfigurationUpdates.swift +++ b/submodules/TelegramCore/Sources/State/ManagedConfigurationUpdates.swift @@ -90,7 +90,9 @@ func managedConfigurationUpdates(accountManager: AccountManager, postbox: Postbo } } |> switchToLatest - }).start() + }).start(completed: { + subscriber.putCompletion() + }) } return (poll |> then(.complete() |> suspendAwareDelay(1.0 * 60.0 * 60.0, queue: Queue.concurrentDefaultQueue()))) |> restart diff --git a/submodules/TelegramCore/Sources/ManagedConsumePersonalMessagesActions.swift b/submodules/TelegramCore/Sources/State/ManagedConsumePersonalMessagesActions.swift similarity index 100% rename from submodules/TelegramCore/Sources/ManagedConsumePersonalMessagesActions.swift rename to submodules/TelegramCore/Sources/State/ManagedConsumePersonalMessagesActions.swift diff --git a/submodules/TelegramCore/Sources/ManagedGlobalNotificationSettings.swift b/submodules/TelegramCore/Sources/State/ManagedGlobalNotificationSettings.swift similarity index 100% rename from submodules/TelegramCore/Sources/ManagedGlobalNotificationSettings.swift rename to submodules/TelegramCore/Sources/State/ManagedGlobalNotificationSettings.swift diff --git a/submodules/TelegramCore/Sources/ManagedLocalInputActivities.swift b/submodules/TelegramCore/Sources/State/ManagedLocalInputActivities.swift similarity index 94% rename from submodules/TelegramCore/Sources/ManagedLocalInputActivities.swift rename to submodules/TelegramCore/Sources/State/ManagedLocalInputActivities.swift index 95b5eea8cf..c422460cf4 100644 --- a/submodules/TelegramCore/Sources/ManagedLocalInputActivities.swift +++ b/submodules/TelegramCore/Sources/State/ManagedLocalInputActivities.swift @@ -143,7 +143,14 @@ private func requestActivity(postbox: Postbox, network: Network, accountPeerId: return .complete() } if let channel = peer as? TelegramChannel, case .broadcast = channel.info { - return .complete() + if let activity = activity { + switch activity { + case .speakingInGroupCall: + break + default: + return .complete() + } + } } if let _ = peer as? TelegramUser { if let presence = transaction.getPeerPresence(peerId: peerId) as? TelegramUserPresence { @@ -177,7 +184,8 @@ private func requestActivity(postbox: Postbox, network: Network, accountPeerId: return .complete() } } else if let peer = peer as? TelegramSecretChat, activity == .typingText { - return network.request(Api.functions.messages.setEncryptedTyping(peer: .inputEncryptedChat(chatId: peer.id.id, accessHash: peer.accessHash), typing: .boolTrue)) + let _ = PeerId(peer.id.toInt64()) + return network.request(Api.functions.messages.setEncryptedTyping(peer: .inputEncryptedChat(chatId: peer.id.id._internalGetInt32Value(), accessHash: peer.accessHash), typing: .boolTrue)) |> `catch` { _ -> Signal in return .single(.boolFalse) } diff --git a/submodules/TelegramCore/Sources/ManagedLocalizationUpdatesOperations.swift b/submodules/TelegramCore/Sources/State/ManagedLocalizationUpdatesOperations.swift similarity index 99% rename from submodules/TelegramCore/Sources/ManagedLocalizationUpdatesOperations.swift rename to submodules/TelegramCore/Sources/State/ManagedLocalizationUpdatesOperations.swift index 4a95be5cf0..68157e0cfd 100644 --- a/submodules/TelegramCore/Sources/ManagedLocalizationUpdatesOperations.swift +++ b/submodules/TelegramCore/Sources/State/ManagedLocalizationUpdatesOperations.swift @@ -228,7 +228,7 @@ private func synchronizeLocalizationUpdates(accountManager: AccountManager, post case .reset: return accountManager.transaction { transaction -> Signal in let (primary, _) = getLocalization(transaction) - return downloadAndApplyLocalization(accountManager: accountManager, postbox: postbox, network: network, languageCode: primary.code) + return _internal_downloadAndApplyLocalization(accountManager: accountManager, postbox: postbox, network: network, languageCode: primary.code) |> mapError { _ -> Void in return Void() } diff --git a/submodules/TelegramCore/Sources/ManagedMessageHistoryHoles.swift b/submodules/TelegramCore/Sources/State/ManagedMessageHistoryHoles.swift similarity index 100% rename from submodules/TelegramCore/Sources/ManagedMessageHistoryHoles.swift rename to submodules/TelegramCore/Sources/State/ManagedMessageHistoryHoles.swift diff --git a/submodules/TelegramCore/Sources/ManagedNotificationSettingsBehaviors.swift b/submodules/TelegramCore/Sources/State/ManagedNotificationSettingsBehaviors.swift similarity index 100% rename from submodules/TelegramCore/Sources/ManagedNotificationSettingsBehaviors.swift rename to submodules/TelegramCore/Sources/State/ManagedNotificationSettingsBehaviors.swift diff --git a/submodules/TelegramCore/Sources/ManagedPendingPeerNotificationSettings.swift b/submodules/TelegramCore/Sources/State/ManagedPendingPeerNotificationSettings.swift similarity index 100% rename from submodules/TelegramCore/Sources/ManagedPendingPeerNotificationSettings.swift rename to submodules/TelegramCore/Sources/State/ManagedPendingPeerNotificationSettings.swift diff --git a/submodules/TelegramCore/Sources/ManagedProxyInfoUpdates.swift b/submodules/TelegramCore/Sources/State/ManagedProxyInfoUpdates.swift similarity index 100% rename from submodules/TelegramCore/Sources/ManagedProxyInfoUpdates.swift rename to submodules/TelegramCore/Sources/State/ManagedProxyInfoUpdates.swift diff --git a/submodules/TelegramCore/Sources/ManagedRecentStickers.swift b/submodules/TelegramCore/Sources/State/ManagedRecentStickers.swift similarity index 93% rename from submodules/TelegramCore/Sources/ManagedRecentStickers.swift rename to submodules/TelegramCore/Sources/State/ManagedRecentStickers.swift index 8323da2f5f..d1f3c2d489 100644 --- a/submodules/TelegramCore/Sources/ManagedRecentStickers.swift +++ b/submodules/TelegramCore/Sources/State/ManagedRecentStickers.swift @@ -27,8 +27,18 @@ private func managedRecentMedia(postbox: Postbox, network: Network, collectionId itemIds.reverse() } return fetch(forceFetch ? 0 : hashForIds(itemIds)) - |> mapToSignal { items in - if let items = items { + |> mapToSignal { sourceItems in + var items: [OrderedItemListEntry] = [] + if let sourceItems = sourceItems { + var existingIds = Set() + for item in sourceItems { + let id = item.id.makeData() + if !existingIds.contains(id) { + existingIds.insert(id) + items.append(item) + } + } + return postbox.transaction { transaction -> Void in transaction.replaceOrderedItemListItems(collectionId: collectionId, items: items) } diff --git a/submodules/TelegramCore/Sources/ManagedSecretChatOutgoingOperations.swift b/submodules/TelegramCore/Sources/State/ManagedSecretChatOutgoingOperations.swift similarity index 99% rename from submodules/TelegramCore/Sources/ManagedSecretChatOutgoingOperations.swift rename to submodules/TelegramCore/Sources/State/ManagedSecretChatOutgoingOperations.swift index e8cedadb60..56262edfa6 100644 --- a/submodules/TelegramCore/Sources/ManagedSecretChatOutgoingOperations.swift +++ b/submodules/TelegramCore/Sources/State/ManagedSecretChatOutgoingOperations.swift @@ -227,7 +227,7 @@ private func initialHandshakeAccept(postbox: Postbox, network: Network, peerId: memcpy(&keyFingerprint, bytes.advanced(by: keyHash.count - 8), 8) } - let result = network.request(Api.functions.messages.acceptEncryption(peer: .inputEncryptedChat(chatId: peerId.id, accessHash: accessHash), gB: Buffer(data: gb), keyFingerprint: keyFingerprint)) + let result = network.request(Api.functions.messages.acceptEncryption(peer: .inputEncryptedChat(chatId: peerId.id._internalGetInt32Value(), accessHash: accessHash), gB: Buffer(data: gb), keyFingerprint: keyFingerprint)) let response = result |> map { result -> Api.EncryptedChat? in @@ -245,7 +245,7 @@ private func initialHandshakeAccept(postbox: Postbox, network: Network, peerId: if let state = transaction.getPeerChatState(peerId) as? SecretChatState { var updatedState = state updatedState = updatedState.withUpdatedKeychain(SecretChatKeychain(keys: [SecretChatKey(fingerprint: keyFingerprint, key: MemoryBuffer(data: key), validity: .indefinite, useCount: 0)])) - updatedState = updatedState.withUpdatedEmbeddedState(.sequenceBasedLayer(SecretChatSequenceBasedLayerState(layerNegotiationState: SecretChatLayerNegotiationState(activeLayer: .layer46, locallyRequestedLayer: nil, remotelyRequestedLayer: nil), rekeyState: nil, baseIncomingOperationIndex: transaction.operationLogGetNextEntryLocalIndex(peerId: peerId, tag: OperationLogTags.SecretIncomingDecrypted), baseOutgoingOperationIndex: transaction.operationLogGetNextEntryLocalIndex(peerId: peerId, tag: OperationLogTags.SecretOutgoing), topProcessedCanonicalIncomingOperationIndex: nil))) + updatedState = updatedState.withUpdatedEmbeddedState(.sequenceBasedLayer(SecretChatSequenceBasedLayerState(layerNegotiationState: SecretChatLayerNegotiationState(activeLayer: .layer73, locallyRequestedLayer: nil, remotelyRequestedLayer: nil), rekeyState: nil, baseIncomingOperationIndex: transaction.operationLogGetNextEntryLocalIndex(peerId: peerId, tag: OperationLogTags.SecretIncomingDecrypted), baseOutgoingOperationIndex: transaction.operationLogGetNextEntryLocalIndex(peerId: peerId, tag: OperationLogTags.SecretOutgoing), topProcessedCanonicalIncomingOperationIndex: nil))) updatedState = updatedState.withUpdatedKeyFingerprint(SecretChatKeyFingerprint(sha1: SecretChatKeySha1Fingerprint(digest: sha1Digest(key)), sha256: SecretChatKeySha256Fingerprint(digest: sha256Digest(key)))) var layer: SecretChatLayer? @@ -258,7 +258,7 @@ private func initialHandshakeAccept(postbox: Postbox, network: Network, peerId: layer = sequenceState.layerNegotiationState.activeLayer.secretChatLayer } if let layer = layer { - updatedState = addSecretChatOutgoingOperation(transaction: transaction, peerId: peerId, operation: .reportLayerSupport(layer: layer, actionGloballyUniqueId: arc4random64(), layerSupport: 46), state: updatedState) + updatedState = addSecretChatOutgoingOperation(transaction: transaction, peerId: peerId, operation: .reportLayerSupport(layer: layer, actionGloballyUniqueId: Int64.random(in: Int64.min ... Int64.max), layerSupport: 46), state: updatedState) } transaction.setPeerChatState(peerId, state: updatedState) if let peer = transaction.getPeer(peerId) as? TelegramSecretChat { @@ -1352,7 +1352,7 @@ private func markOutgoingOperationAsCompleted(transaction: Transaction, peerId: if let operation = entry?.contents as? SecretChatOutgoingOperation { return PeerOperationLogEntryUpdate(mergedIndex: .remove, contents: .update(operation.withUpdatedDelivered(true))) } else { - assertionFailure() + //assertionFailure() return PeerOperationLogEntryUpdate(mergedIndex: .remove, contents: .none) } }) @@ -1377,7 +1377,7 @@ private func replaceOutgoingOperationWithEmptyMessage(transaction: Transaction, if let layer = layer { transaction.operationLogUpdateEntry(peerId: peerId, tag: OperationLogTags.SecretOutgoing, tagLocalIndex: tagLocalIndex, { entry in if let _ = entry?.contents as? SecretChatOutgoingOperation { - return PeerOperationLogEntryUpdate(mergedIndex: .none, contents: .update(SecretChatOutgoingOperation(contents: SecretChatOutgoingOperationContents.deleteMessages(layer: layer, actionGloballyUniqueId: arc4random64(), globallyUniqueIds: [globallyUniqueId]), mutable: true, delivered: false))) + return PeerOperationLogEntryUpdate(mergedIndex: .none, contents: .update(SecretChatOutgoingOperation(contents: SecretChatOutgoingOperationContents.deleteMessages(layer: layer, actionGloballyUniqueId: Int64.random(in: Int64.min ... Int64.max), globallyUniqueIds: [globallyUniqueId]), mutable: true, delivered: false))) } else { assertionFailure() return PeerOperationLogEntryUpdate(mergedIndex: .remove, contents: .none) @@ -1519,8 +1519,8 @@ private func sendMessage(auxiliaryMethods: AccountAuxiliaryMethods, postbox: Pos } } } else { - replaceOutgoingOperationWithEmptyMessage(transaction: transaction, peerId: messageId.peerId, tagLocalIndex: tagLocalIndex, globallyUniqueId: arc4random64()) - deleteMessages(transaction: transaction, mediaBox: postbox.mediaBox, ids: [messageId]) + replaceOutgoingOperationWithEmptyMessage(transaction: transaction, peerId: messageId.peerId, tagLocalIndex: tagLocalIndex, globallyUniqueId: Int64.random(in: Int64.min ... Int64.max)) + _internal_deleteMessages(transaction: transaction, mediaBox: postbox.mediaBox, ids: [messageId]) return .complete() } } else { @@ -1636,7 +1636,7 @@ private func sendBoxedDecryptedMessage(postbox: Postbox, network: Network, peer: decryptedMessage.serialize(payload, role: state.role, sequenceInfo: sequenceInfo) let encryptedPayload = encryptedMessageContents(parameters: parameters, data: MemoryBuffer(payload)) let sendMessage: Signal - let inputPeer = Api.InputEncryptedChat.inputEncryptedChat(chatId: peer.id.id, accessHash: peer.accessHash) + let inputPeer = Api.InputEncryptedChat.inputEncryptedChat(chatId: peer.id.id._internalGetInt32Value(), accessHash: peer.accessHash) var flags: Int32 = 0 if silent { @@ -1646,7 +1646,7 @@ private func sendBoxedDecryptedMessage(postbox: Postbox, network: Network, peer: if asService { let actionRandomId: Int64 if wasDelivered { - actionRandomId = arc4random64() + actionRandomId = Int64.random(in: Int64.min ... Int64.max) } else { actionRandomId = globallyUniqueId } @@ -1676,7 +1676,7 @@ private func requestTerminateSecretChat(postbox: Postbox, network: Network, peer if requestRemoteHistoryRemoval { flags |= 1 << 0 } - return network.request(Api.functions.messages.discardEncryption(flags: flags, chatId: peerId.id)) + return network.request(Api.functions.messages.discardEncryption(flags: flags, chatId: peerId.id._internalGetInt32Value())) |> map(Optional.init) |> `catch` { _ in return .single(nil) @@ -1692,7 +1692,7 @@ private func requestTerminateSecretChat(postbox: Postbox, network: Network, peer } |> mapToSignal { peer -> Signal in if let peer = peer { - return network.request(Api.functions.messages.reportEncryptedSpam(peer: Api.InputEncryptedChat.inputEncryptedChat(chatId: peer.id.id, accessHash: peer.accessHash))) + return network.request(Api.functions.messages.reportEncryptedSpam(peer: Api.InputEncryptedChat.inputEncryptedChat(chatId: peer.id.id._internalGetInt32Value(), accessHash: peer.accessHash))) |> map(Optional.init) |> `catch` { _ -> Signal in return .single(nil) diff --git a/submodules/TelegramCore/Sources/ManagedServiceViews.swift b/submodules/TelegramCore/Sources/State/ManagedServiceViews.swift similarity index 100% rename from submodules/TelegramCore/Sources/ManagedServiceViews.swift rename to submodules/TelegramCore/Sources/State/ManagedServiceViews.swift diff --git a/submodules/TelegramCore/Sources/ManagedSynchronizeAppLogEventsOperations.swift b/submodules/TelegramCore/Sources/State/ManagedSynchronizeAppLogEventsOperations.swift similarity index 100% rename from submodules/TelegramCore/Sources/ManagedSynchronizeAppLogEventsOperations.swift rename to submodules/TelegramCore/Sources/State/ManagedSynchronizeAppLogEventsOperations.swift diff --git a/submodules/TelegramCore/Sources/ManagedSynchronizeChatInputStateOperations.swift b/submodules/TelegramCore/Sources/State/ManagedSynchronizeChatInputStateOperations.swift similarity index 100% rename from submodules/TelegramCore/Sources/ManagedSynchronizeChatInputStateOperations.swift rename to submodules/TelegramCore/Sources/State/ManagedSynchronizeChatInputStateOperations.swift diff --git a/submodules/TelegramCore/Sources/ManagedSynchronizeConsumeMessageContentsOperations.swift b/submodules/TelegramCore/Sources/State/ManagedSynchronizeConsumeMessageContentsOperations.swift similarity index 100% rename from submodules/TelegramCore/Sources/ManagedSynchronizeConsumeMessageContentsOperations.swift rename to submodules/TelegramCore/Sources/State/ManagedSynchronizeConsumeMessageContentsOperations.swift diff --git a/submodules/TelegramCore/Sources/ManagedSynchronizeEmojiKeywordsOperations.swift b/submodules/TelegramCore/Sources/State/ManagedSynchronizeEmojiKeywordsOperations.swift similarity index 100% rename from submodules/TelegramCore/Sources/ManagedSynchronizeEmojiKeywordsOperations.swift rename to submodules/TelegramCore/Sources/State/ManagedSynchronizeEmojiKeywordsOperations.swift diff --git a/submodules/TelegramCore/Sources/ManagedSynchronizeGroupMessageStats.swift b/submodules/TelegramCore/Sources/State/ManagedSynchronizeGroupMessageStats.swift similarity index 100% rename from submodules/TelegramCore/Sources/ManagedSynchronizeGroupMessageStats.swift rename to submodules/TelegramCore/Sources/State/ManagedSynchronizeGroupMessageStats.swift diff --git a/submodules/TelegramCore/Sources/ManagedSynchronizeGroupedPeersOperations.swift b/submodules/TelegramCore/Sources/State/ManagedSynchronizeGroupedPeersOperations.swift similarity index 100% rename from submodules/TelegramCore/Sources/ManagedSynchronizeGroupedPeersOperations.swift rename to submodules/TelegramCore/Sources/State/ManagedSynchronizeGroupedPeersOperations.swift diff --git a/submodules/TelegramCore/Sources/ManagedSynchronizeInstalledStickerPacksOperations.swift b/submodules/TelegramCore/Sources/State/ManagedSynchronizeInstalledStickerPacksOperations.swift similarity index 100% rename from submodules/TelegramCore/Sources/ManagedSynchronizeInstalledStickerPacksOperations.swift rename to submodules/TelegramCore/Sources/State/ManagedSynchronizeInstalledStickerPacksOperations.swift diff --git a/submodules/TelegramCore/Sources/ManagedSynchronizeMarkAllUnseenPersonalMessagesOperations.swift b/submodules/TelegramCore/Sources/State/ManagedSynchronizeMarkAllUnseenPersonalMessagesOperations.swift similarity index 100% rename from submodules/TelegramCore/Sources/ManagedSynchronizeMarkAllUnseenPersonalMessagesOperations.swift rename to submodules/TelegramCore/Sources/State/ManagedSynchronizeMarkAllUnseenPersonalMessagesOperations.swift diff --git a/submodules/TelegramCore/Sources/ManagedSynchronizeMarkFeaturedStickerPacksAsSeenOperations.swift b/submodules/TelegramCore/Sources/State/ManagedSynchronizeMarkFeaturedStickerPacksAsSeenOperations.swift similarity index 100% rename from submodules/TelegramCore/Sources/ManagedSynchronizeMarkFeaturedStickerPacksAsSeenOperations.swift rename to submodules/TelegramCore/Sources/State/ManagedSynchronizeMarkFeaturedStickerPacksAsSeenOperations.swift diff --git a/submodules/TelegramCore/Sources/ManagedSynchronizePeerReadStates.swift b/submodules/TelegramCore/Sources/State/ManagedSynchronizePeerReadStates.swift similarity index 100% rename from submodules/TelegramCore/Sources/ManagedSynchronizePeerReadStates.swift rename to submodules/TelegramCore/Sources/State/ManagedSynchronizePeerReadStates.swift diff --git a/submodules/TelegramCore/Sources/ManagedSynchronizePinnedChatsOperations.swift b/submodules/TelegramCore/Sources/State/ManagedSynchronizePinnedChatsOperations.swift similarity index 95% rename from submodules/TelegramCore/Sources/ManagedSynchronizePinnedChatsOperations.swift rename to submodules/TelegramCore/Sources/State/ManagedSynchronizePinnedChatsOperations.swift index b5ff3b474e..90280571f0 100644 --- a/submodules/TelegramCore/Sources/ManagedSynchronizePinnedChatsOperations.swift +++ b/submodules/TelegramCore/Sources/State/ManagedSynchronizePinnedChatsOperations.swift @@ -83,7 +83,7 @@ func managedSynchronizePinnedChatsOperations(postbox: Postbox, network: Network, let signal = withTakenOperation(postbox: postbox, peerId: entry.peerId, tagLocalIndex: entry.tagLocalIndex, { transaction, entry -> Signal in if let entry = entry { if let operation = entry.contents as? SynchronizePinnedChatsOperation { - return synchronizePinnedChats(transaction: transaction, postbox: postbox, network: network, accountPeerId: accountPeerId, stateManager: stateManager, groupId: PeerGroupId(rawValue: entry.peerId.id), operation: operation) + return synchronizePinnedChats(transaction: transaction, postbox: postbox, network: network, accountPeerId: accountPeerId, stateManager: stateManager, groupId: PeerGroupId(rawValue: entry.peerId.id._internalGetInt32Value()), operation: operation) } else { assertionFailure() } @@ -182,15 +182,7 @@ private func synchronizePinnedChats(transaction: Transaction, postbox: Postbox, continue loop } - let peerId: PeerId - switch apiPeer { - case let .peerUser(userId): - peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: userId) - case let .peerChat(chatId): - peerId = PeerId(namespace: Namespaces.Peer.CloudGroup, id: chatId) - case let .peerChannel(channelId): - peerId = PeerId(namespace: Namespaces.Peer.CloudChannel, id: channelId) - } + let peerId: PeerId = apiPeer.peerId remoteItemIds.append(.peer(peerId)) diff --git a/submodules/TelegramCore/Sources/ManagedSynchronizeRecentlyUsedMediaOperations.swift b/submodules/TelegramCore/Sources/State/ManagedSynchronizeRecentlyUsedMediaOperations.swift similarity index 100% rename from submodules/TelegramCore/Sources/ManagedSynchronizeRecentlyUsedMediaOperations.swift rename to submodules/TelegramCore/Sources/State/ManagedSynchronizeRecentlyUsedMediaOperations.swift diff --git a/submodules/TelegramCore/Sources/ManagedSynchronizeSavedGifsOperations.swift b/submodules/TelegramCore/Sources/State/ManagedSynchronizeSavedGifsOperations.swift similarity index 100% rename from submodules/TelegramCore/Sources/ManagedSynchronizeSavedGifsOperations.swift rename to submodules/TelegramCore/Sources/State/ManagedSynchronizeSavedGifsOperations.swift diff --git a/submodules/TelegramCore/Sources/ManagedSynchronizeSavedStickersOperations.swift b/submodules/TelegramCore/Sources/State/ManagedSynchronizeSavedStickersOperations.swift similarity index 100% rename from submodules/TelegramCore/Sources/ManagedSynchronizeSavedStickersOperations.swift rename to submodules/TelegramCore/Sources/State/ManagedSynchronizeSavedStickersOperations.swift diff --git a/submodules/TelegramCore/Sources/ManagedVoipConfigurationUpdates.swift b/submodules/TelegramCore/Sources/State/ManagedVoipConfigurationUpdates.swift similarity index 92% rename from submodules/TelegramCore/Sources/ManagedVoipConfigurationUpdates.swift rename to submodules/TelegramCore/Sources/State/ManagedVoipConfigurationUpdates.swift index b3da17c751..2465ca6d99 100644 --- a/submodules/TelegramCore/Sources/ManagedVoipConfigurationUpdates.swift +++ b/submodules/TelegramCore/Sources/State/ManagedVoipConfigurationUpdates.swift @@ -19,7 +19,9 @@ func managedVoipConfigurationUpdates(postbox: Postbox, network: Network) -> Sign }) } } - }).start() + }).start(completed: { + subscriber.putCompletion() + }) } return (poll |> then(.complete() |> suspendAwareDelay(12.0 * 60.0 * 60.0, queue: Queue.concurrentDefaultQueue()))) |> restart } diff --git a/submodules/TelegramCore/Sources/MessageMediaPreuploadManager.swift b/submodules/TelegramCore/Sources/State/MessageMediaPreuploadManager.swift similarity index 95% rename from submodules/TelegramCore/Sources/MessageMediaPreuploadManager.swift rename to submodules/TelegramCore/Sources/State/MessageMediaPreuploadManager.swift index e53e9b0414..dedd571f41 100644 --- a/submodules/TelegramCore/Sources/MessageMediaPreuploadManager.swift +++ b/submodules/TelegramCore/Sources/State/MessageMediaPreuploadManager.swift @@ -33,7 +33,7 @@ private final class MessageMediaPreuploadManagerContext { assert(self.queue.isCurrent()) } - func add(network: Network, postbox: Postbox, id: Int64, encrypt: Bool, tag: MediaResourceFetchTag?, source: Signal, onComplete:(()->Void)? = nil) { + func add(network: Network, postbox: Postbox, id: Int64, encrypt: Bool, tag: MediaResourceFetchTag?, source: Signal, onComplete: (()->Void)? = nil) { let context = MessageMediaPreuploadManagerUploadContext() self.uploadContexts[id] = context let queue = self.queue @@ -103,7 +103,7 @@ private final class MessageMediaPreuploadManagerContext { } } -public final class MessageMediaPreuploadManager { +final class MessageMediaPreuploadManager { private let impl: QueueLocalObject init() { @@ -113,7 +113,7 @@ public final class MessageMediaPreuploadManager { }) } - public func add(network: Network, postbox: Postbox, id: Int64, encrypt: Bool, tag: MediaResourceFetchTag?, source: Signal, onComplete:(()->Void)? = nil) { + func add(network: Network, postbox: Postbox, id: Int64, encrypt: Bool, tag: MediaResourceFetchTag?, source: Signal, onComplete:(()->Void)? = nil) { self.impl.with { context in context.add(network: network, postbox: postbox, id: id, encrypt: encrypt, tag: tag, source: source, onComplete: onComplete) } diff --git a/submodules/TelegramCore/Sources/MessageReactions.swift b/submodules/TelegramCore/Sources/State/MessageReactions.swift similarity index 100% rename from submodules/TelegramCore/Sources/MessageReactions.swift rename to submodules/TelegramCore/Sources/State/MessageReactions.swift diff --git a/submodules/TelegramCore/Sources/PeerInputActivity.swift b/submodules/TelegramCore/Sources/State/PeerInputActivity.swift similarity index 97% rename from submodules/TelegramCore/Sources/PeerInputActivity.swift rename to submodules/TelegramCore/Sources/State/PeerInputActivity.swift index 36ff067c50..611ee31e27 100644 --- a/submodules/TelegramCore/Sources/PeerInputActivity.swift +++ b/submodules/TelegramCore/Sources/State/PeerInputActivity.swift @@ -63,7 +63,7 @@ extension PeerInputActivity { self = .uploadingInstantVideo(progress: progress) case .speakingInGroupCallAction: self = .speakingInGroupCall(timestamp: timestamp) - case let .sendMessageHistoryImportAction(progress): + case let .sendMessageHistoryImportAction: return nil } } diff --git a/submodules/TelegramCore/Sources/PeerInputActivityManager.swift b/submodules/TelegramCore/Sources/State/PeerInputActivityManager.swift similarity index 100% rename from submodules/TelegramCore/Sources/PeerInputActivityManager.swift rename to submodules/TelegramCore/Sources/State/PeerInputActivityManager.swift diff --git a/submodules/TelegramCore/Sources/PendingMessageManager.swift b/submodules/TelegramCore/Sources/State/PendingMessageManager.swift similarity index 100% rename from submodules/TelegramCore/Sources/PendingMessageManager.swift rename to submodules/TelegramCore/Sources/State/PendingMessageManager.swift diff --git a/submodules/TelegramCore/Sources/ProcessSecretChatIncomingDecryptedOperations.swift b/submodules/TelegramCore/Sources/State/ProcessSecretChatIncomingDecryptedOperations.swift similarity index 96% rename from submodules/TelegramCore/Sources/ProcessSecretChatIncomingDecryptedOperations.swift rename to submodules/TelegramCore/Sources/State/ProcessSecretChatIncomingDecryptedOperations.swift index ee7c56aaf5..d9f28c1f79 100644 --- a/submodules/TelegramCore/Sources/ProcessSecretChatIncomingDecryptedOperations.swift +++ b/submodules/TelegramCore/Sources/State/ProcessSecretChatIncomingDecryptedOperations.swift @@ -190,7 +190,7 @@ func processSecretChatIncomingDecryptedOperations(encryptionProvider: Encryption let role = updatedState.role let fromSeqNo: Int32 = (topProcessedCanonicalIncomingOperationIndex + 1) * 2 + (role == .creator ? 0 : 1) let toSeqNo: Int32 = (canonicalIncomingIndex - 1) * 2 + (role == .creator ? 0 : 1) - updatedState = addSecretChatOutgoingOperation(transaction: transaction, peerId: peerId, operation: SecretChatOutgoingOperationContents.resendOperations(layer: layer, actionGloballyUniqueId: arc4random64(), fromSeqNo: fromSeqNo, toSeqNo: toSeqNo), state: updatedState) + updatedState = addSecretChatOutgoingOperation(transaction: transaction, peerId: peerId, operation: SecretChatOutgoingOperationContents.resendOperations(layer: layer, actionGloballyUniqueId: Int64.random(in: Int64.min ... Int64.max), fromSeqNo: fromSeqNo, toSeqNo: toSeqNo), state: updatedState) } else { assertionFailure() } @@ -204,7 +204,7 @@ func processSecretChatIncomingDecryptedOperations(encryptionProvider: Encryption let role = updatedState.role let fromSeqNo: Int32 = Int32(0 * 2) + (role == .creator ? Int32(0) : Int32(1)) let toSeqNo: Int32 = (canonicalIncomingIndex - 1) * 2 + (role == .creator ? 0 : 1) - updatedState = addSecretChatOutgoingOperation(transaction: transaction, peerId: peerId, operation: SecretChatOutgoingOperationContents.resendOperations(layer: layer, actionGloballyUniqueId: arc4random64(), fromSeqNo: fromSeqNo, toSeqNo: toSeqNo), state: updatedState) + updatedState = addSecretChatOutgoingOperation(transaction: transaction, peerId: peerId, operation: SecretChatOutgoingOperationContents.resendOperations(layer: layer, actionGloballyUniqueId: Int64.random(in: Int64.min ... Int64.max), fromSeqNo: fromSeqNo, toSeqNo: toSeqNo), state: updatedState) } else { assertionFailure() } @@ -230,15 +230,15 @@ func processSecretChatIncomingDecryptedOperations(encryptionProvider: Encryption if layerSupport >= 101 { let sequenceBasedLayerState = SecretChatSequenceBasedLayerState(layerNegotiationState: SecretChatLayerNegotiationState(activeLayer: .layer101, locallyRequestedLayer: 101, remotelyRequestedLayer: layerSupport), rekeyState: nil, baseIncomingOperationIndex: entry.tagLocalIndex, baseOutgoingOperationIndex: transaction.operationLogGetNextEntryLocalIndex(peerId: peerId, tag: OperationLogTags.SecretOutgoing), topProcessedCanonicalIncomingOperationIndex: nil) updatedState = updatedState.withUpdatedEmbeddedState(.sequenceBasedLayer(sequenceBasedLayerState)) - updatedState = addSecretChatOutgoingOperation(transaction: transaction, peerId: peerId, operation: .reportLayerSupport(layer: .layer101, actionGloballyUniqueId: arc4random64(), layerSupport: 101), state: updatedState) + updatedState = addSecretChatOutgoingOperation(transaction: transaction, peerId: peerId, operation: .reportLayerSupport(layer: .layer101, actionGloballyUniqueId: Int64.random(in: Int64.min ... Int64.max), layerSupport: 101), state: updatedState) } else if layerSupport >= 73 { let sequenceBasedLayerState = SecretChatSequenceBasedLayerState(layerNegotiationState: SecretChatLayerNegotiationState(activeLayer: .layer73, locallyRequestedLayer: 73, remotelyRequestedLayer: layerSupport), rekeyState: nil, baseIncomingOperationIndex: entry.tagLocalIndex, baseOutgoingOperationIndex: transaction.operationLogGetNextEntryLocalIndex(peerId: peerId, tag: OperationLogTags.SecretOutgoing), topProcessedCanonicalIncomingOperationIndex: nil) updatedState = updatedState.withUpdatedEmbeddedState(.sequenceBasedLayer(sequenceBasedLayerState)) - updatedState = addSecretChatOutgoingOperation(transaction: transaction, peerId: peerId, operation: .reportLayerSupport(layer: .layer73, actionGloballyUniqueId: arc4random64(), layerSupport: 101), state: updatedState) + updatedState = addSecretChatOutgoingOperation(transaction: transaction, peerId: peerId, operation: .reportLayerSupport(layer: .layer73, actionGloballyUniqueId: Int64.random(in: Int64.min ... Int64.max), layerSupport: 101), state: updatedState) } else if layerSupport >= 46 { - let sequenceBasedLayerState = SecretChatSequenceBasedLayerState(layerNegotiationState: SecretChatLayerNegotiationState(activeLayer: .layer46, locallyRequestedLayer: 46, remotelyRequestedLayer: layerSupport), rekeyState: nil, baseIncomingOperationIndex: entry.tagLocalIndex, baseOutgoingOperationIndex: transaction.operationLogGetNextEntryLocalIndex(peerId: peerId, tag: OperationLogTags.SecretOutgoing), topProcessedCanonicalIncomingOperationIndex: nil) + let sequenceBasedLayerState = SecretChatSequenceBasedLayerState(layerNegotiationState: SecretChatLayerNegotiationState(activeLayer: .layer73, locallyRequestedLayer: 46, remotelyRequestedLayer: layerSupport), rekeyState: nil, baseIncomingOperationIndex: entry.tagLocalIndex, baseOutgoingOperationIndex: transaction.operationLogGetNextEntryLocalIndex(peerId: peerId, tag: OperationLogTags.SecretOutgoing), topProcessedCanonicalIncomingOperationIndex: nil) updatedState = updatedState.withUpdatedEmbeddedState(.sequenceBasedLayer(sequenceBasedLayerState)) - updatedState = addSecretChatOutgoingOperation(transaction: transaction, peerId: peerId, operation: .reportLayerSupport(layer: .layer46, actionGloballyUniqueId: arc4random64(), layerSupport: 101), state: updatedState) + updatedState = addSecretChatOutgoingOperation(transaction: transaction, peerId: peerId, operation: .reportLayerSupport(layer: .layer73, actionGloballyUniqueId: Int64.random(in: Int64.min ... Int64.max), layerSupport: 101), state: updatedState) } else { throw MessageParsingError.contentParsingError } @@ -276,10 +276,10 @@ func processSecretChatIncomingDecryptedOperations(encryptionProvider: Encryption } } } - deleteMessages(transaction: transaction, mediaBox: mediaBox, ids: filteredMessageIds) + _internal_deleteMessages(transaction: transaction, mediaBox: mediaBox, ids: filteredMessageIds) } case .clearHistory: - clearHistory(transaction: transaction, mediaBox: mediaBox, peerId: peerId, namespaces: .all) + _internal_clearHistory(transaction: transaction, mediaBox: mediaBox, peerId: peerId, namespaces: .all) case let .markMessagesContentAsConsumed(globallyUniqueIds): var messageIds: [MessageId] = [] for id in globallyUniqueIds { @@ -711,11 +711,11 @@ private func parseMessage(peerId: PeerId, authorId: PeerId, tagLocalIndex: Int32 if let file = file { var representations: [TelegramMediaImageRepresentation] = [] if thumb.size != 0 { - let resource = LocalFileMediaResource(fileId: arc4random64()) - representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: thumbW, height: thumbH), resource: resource, progressiveSizes: [])) + let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) + representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: thumbW, height: thumbH), resource: resource, progressiveSizes: [], immediateThumbnailData: nil)) 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), progressiveSizes: [])) + representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: file.resource(key: SecretFileEncryptionKey(aesKey: key.makeData(), aesIv: iv.makeData()), decryptedSize: size), progressiveSizes: [], immediateThumbnailData: 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) } @@ -737,8 +737,8 @@ private func parseMessage(peerId: PeerId, authorId: PeerId, tagLocalIndex: Int32 } var previewRepresentations: [TelegramMediaImageRepresentation] = [] if thumb.size != 0 { - let resource = LocalFileMediaResource(fileId: arc4random64()) - previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: thumbW, height: thumbH), resource: resource, progressiveSizes: [])) + let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) + previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: thumbW, height: thumbH), resource: resource, progressiveSizes: [], immediateThumbnailData: nil)) resources.append((resource, thumb.makeData())) } let fileMedia = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.CloudSecretFile, id: file.id), partialReference: nil, resource: file.resource(key: SecretFileEncryptionKey(aesKey: key.makeData(), aesIv: iv.makeData()), decryptedSize: size), previewRepresentations: previewRepresentations, videoThumbnails: [], immediateThumbnailData: nil, mimeType: mimeType, size: Int(size), attributes: parsedAttributes) @@ -752,8 +752,8 @@ private func parseMessage(peerId: PeerId, authorId: PeerId, tagLocalIndex: Int32 let parsedAttributes: [TelegramMediaFileAttribute] = [.Video(duration: Int(duration), size: PixelDimensions(width: w, height: h), flags: []), .FileName(fileName: "video.mov")] var previewRepresentations: [TelegramMediaImageRepresentation] = [] if thumb.size != 0 { - let resource = LocalFileMediaResource(fileId: arc4random64()) - previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: thumbW, height: thumbH), resource: resource, progressiveSizes: [])) + let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) + previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: thumbW, height: thumbH), resource: resource, progressiveSizes: [], immediateThumbnailData: nil)) resources.append((resource, thumb.makeData())) } let fileMedia = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.CloudSecretFile, id: file.id), partialReference: nil, resource: file.resource(key: SecretFileEncryptionKey(aesKey: key.makeData(), aesIv: iv.makeData()), decryptedSize: size), previewRepresentations: previewRepresentations, videoThumbnails: [], immediateThumbnailData: nil, mimeType: mimeType, size: Int(size), attributes: parsedAttributes) @@ -771,7 +771,7 @@ private func parseMessage(peerId: PeerId, authorId: PeerId, tagLocalIndex: Int32 case let .photoSize(_, location, w, h, size): switch location { case let .fileLocation(dcId, volumeId, localId, secret): - previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: CloudFileMediaResource(datacenterId: Int(dcId), volumeId: volumeId, localId: localId, secret: secret, size: size == 0 ? nil : Int(size), fileReference: nil), progressiveSizes: [])) + previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: CloudFileMediaResource(datacenterId: Int(dcId), volumeId: volumeId, localId: localId, secret: secret, size: size == 0 ? nil : Int(size), fileReference: nil), progressiveSizes: [], immediateThumbnailData: nil)) case .fileLocationUnavailable: break } @@ -781,7 +781,7 @@ private func parseMessage(peerId: PeerId, authorId: PeerId, tagLocalIndex: Int32 case let .fileLocation(dcId, volumeId, localId, secret): let resource = CloudFileMediaResource(datacenterId: Int(dcId), volumeId: volumeId, localId: localId, secret: secret, size: bytes.size, fileReference: nil) resources.append((resource, bytes.makeData())) - previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: resource, progressiveSizes: [])) + previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: resource, progressiveSizes: [], immediateThumbnailData: nil)) case .fileLocationUnavailable: break } @@ -792,11 +792,11 @@ private func parseMessage(peerId: PeerId, authorId: PeerId, tagLocalIndex: Int32 let fileMedia = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.CloudFile, id: id), partialReference: nil, resource: CloudDocumentMediaResource(datacenterId: Int(dcId), fileId: id, accessHash: accessHash, size: Int(size), fileReference: nil, fileName: nil), previewRepresentations: previewRepresentations, videoThumbnails: [], immediateThumbnailData: nil, mimeType: mimeType, size: Int(size), attributes: parsedAttributes) parsedMedia.append(fileMedia) case let .decryptedMessageMediaWebPage(url): - parsedMedia.append(TelegramMediaWebpage(webpageId: MediaId(namespace: Namespaces.Media.LocalWebpage, id: arc4random64()), content: .Pending(0, url))) + parsedMedia.append(TelegramMediaWebpage(webpageId: MediaId(namespace: Namespaces.Media.LocalWebpage, id: Int64.random(in: Int64.min ... Int64.max)), content: .Pending(0, url))) case let .decryptedMessageMediaGeoPoint(lat, long): parsedMedia.append(TelegramMediaMap(latitude: lat, longitude: long, heading: nil, accuracyRadius: nil, geoPlace: nil, venue: nil, liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil)) case let .decryptedMessageMediaContact(phoneNumber, firstName, lastName, userId): - parsedMedia.append(TelegramMediaContact(firstName: firstName, lastName: lastName, phoneNumber: phoneNumber, peerId: userId == 0 ? nil : PeerId(namespace: Namespaces.Peer.CloudUser, id: userId), vCardData: nil)) + parsedMedia.append(TelegramMediaContact(firstName: firstName, lastName: lastName, phoneNumber: phoneNumber, peerId: userId == 0 ? nil : PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt32Value(userId)), vCardData: nil)) case let .decryptedMessageMediaVenue(lat, long, title, address, provider, venueId): parsedMedia.append(TelegramMediaMap(latitude: lat, longitude: long, heading: nil, accuracyRadius: nil, geoPlace: nil, venue: MapVenue(title: title, address: address, provider: provider, id: venueId, type: nil), liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil)) case .decryptedMessageMediaEmpty: @@ -913,11 +913,11 @@ private func parseMessage(peerId: PeerId, authorId: PeerId, tagLocalIndex: Int32 if let file = file { var representations: [TelegramMediaImageRepresentation] = [] if thumb.size != 0 { - let resource = LocalFileMediaResource(fileId: arc4random64()) - representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: thumbW, height: thumbH), resource: resource, progressiveSizes: [])) + let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) + representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: thumbW, height: thumbH), resource: resource, progressiveSizes: [], immediateThumbnailData: nil)) 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), progressiveSizes: [])) + representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: file.resource(key: SecretFileEncryptionKey(aesKey: key.makeData(), aesIv: iv.makeData()), decryptedSize: size), progressiveSizes: [], immediateThumbnailData: 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) } @@ -940,8 +940,8 @@ private func parseMessage(peerId: PeerId, authorId: PeerId, tagLocalIndex: Int32 } var previewRepresentations: [TelegramMediaImageRepresentation] = [] if thumb.size != 0 { - let resource = LocalFileMediaResource(fileId: arc4random64()) - previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: thumbW, height: thumbH), resource: resource, progressiveSizes: [])) + let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) + previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: thumbW, height: thumbH), resource: resource, progressiveSizes: [], immediateThumbnailData: nil)) resources.append((resource, thumb.makeData())) } let fileMedia = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.CloudSecretFile, id: file.id), partialReference: nil, resource: file.resource(key: SecretFileEncryptionKey(aesKey: key.makeData(), aesIv: iv.makeData()), decryptedSize: size), previewRepresentations: previewRepresentations, videoThumbnails: [], immediateThumbnailData: nil, mimeType: mimeType, size: Int(size), attributes: parsedAttributes) @@ -971,8 +971,8 @@ private func parseMessage(peerId: PeerId, authorId: PeerId, tagLocalIndex: Int32 let parsedAttributes: [TelegramMediaFileAttribute] = [.Video(duration: Int(duration), size: PixelDimensions(width: w, height: h), flags: []), .FileName(fileName: "video.mov")] var previewRepresentations: [TelegramMediaImageRepresentation] = [] if thumb.size != 0 { - let resource = LocalFileMediaResource(fileId: arc4random64()) - previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: thumbW, height: thumbH), resource: resource, progressiveSizes: [])) + let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) + previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: thumbW, height: thumbH), resource: resource, progressiveSizes: [], immediateThumbnailData: nil)) resources.append((resource, thumb.makeData())) } let fileMedia = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.CloudSecretFile, id: file.id), partialReference: nil, resource: file.resource(key: SecretFileEncryptionKey(aesKey: key.makeData(), aesIv: iv.makeData()), decryptedSize: size), previewRepresentations: previewRepresentations, videoThumbnails: [], immediateThumbnailData: nil, mimeType: mimeType, size: Int(size), attributes: parsedAttributes) @@ -990,7 +990,7 @@ private func parseMessage(peerId: PeerId, authorId: PeerId, tagLocalIndex: Int32 case let .photoSize(_, location, w, h, size): switch location { case let .fileLocation(dcId, volumeId, localId, secret): - previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: CloudFileMediaResource(datacenterId: Int(dcId), volumeId: volumeId, localId: localId, secret: secret, size: size == 0 ? nil : Int(size), fileReference: nil), progressiveSizes: [])) + previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: CloudFileMediaResource(datacenterId: Int(dcId), volumeId: volumeId, localId: localId, secret: secret, size: size == 0 ? nil : Int(size), fileReference: nil), progressiveSizes: [], immediateThumbnailData: nil)) case .fileLocationUnavailable: break } @@ -1000,7 +1000,7 @@ private func parseMessage(peerId: PeerId, authorId: PeerId, tagLocalIndex: Int32 case let .fileLocation(dcId, volumeId, localId, secret): let resource = CloudFileMediaResource(datacenterId: Int(dcId), volumeId: volumeId, localId: localId, secret: secret, size: bytes.size, fileReference: nil) resources.append((resource, bytes.makeData())) - previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: resource, progressiveSizes: [])) + previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: resource, progressiveSizes: [], immediateThumbnailData: nil)) case .fileLocationUnavailable: break } @@ -1011,11 +1011,11 @@ private func parseMessage(peerId: PeerId, authorId: PeerId, tagLocalIndex: Int32 let fileMedia = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.CloudFile, id: id), partialReference: nil, resource: CloudDocumentMediaResource(datacenterId: Int(dcId), fileId: id, accessHash: accessHash, size: Int(size), fileReference: nil, fileName: nil), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: mimeType, size: Int(size), attributes: parsedAttributes) parsedMedia.append(fileMedia) case let .decryptedMessageMediaWebPage(url): - parsedMedia.append(TelegramMediaWebpage(webpageId: MediaId(namespace: Namespaces.Media.LocalWebpage, id: arc4random64()), content: .Pending(0, url))) + parsedMedia.append(TelegramMediaWebpage(webpageId: MediaId(namespace: Namespaces.Media.LocalWebpage, id: Int64.random(in: Int64.min ... Int64.max)), content: .Pending(0, url))) case let .decryptedMessageMediaGeoPoint(lat, long): parsedMedia.append(TelegramMediaMap(latitude: lat, longitude: long, heading: nil, accuracyRadius: nil, geoPlace: nil, venue: nil, liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil)) case let .decryptedMessageMediaContact(phoneNumber, firstName, lastName, userId): - parsedMedia.append(TelegramMediaContact(firstName: firstName, lastName: lastName, phoneNumber: phoneNumber, peerId: userId == 0 ? nil : PeerId(namespace: Namespaces.Peer.CloudUser, id: userId), vCardData: nil)) + parsedMedia.append(TelegramMediaContact(firstName: firstName, lastName: lastName, phoneNumber: phoneNumber, peerId: userId == 0 ? nil : PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt32Value(userId)), vCardData: nil)) case let .decryptedMessageMediaVenue(lat, long, title, address, provider, venueId): parsedMedia.append(TelegramMediaMap(latitude: lat, longitude: long, heading: nil, accuracyRadius: nil, geoPlace: nil, venue: MapVenue(title: title, address: address, provider: provider, id: venueId, type: nil), liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil)) case .decryptedMessageMediaEmpty: @@ -1151,11 +1151,11 @@ private func parseMessage(peerId: PeerId, authorId: PeerId, tagLocalIndex: Int32 if let file = file { var representations: [TelegramMediaImageRepresentation] = [] if thumb.size != 0 { - let resource = LocalFileMediaResource(fileId: arc4random64()) - representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: thumbW, height: thumbH), resource: resource, progressiveSizes: [])) + let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) + representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: thumbW, height: thumbH), resource: resource, progressiveSizes: [], immediateThumbnailData: nil)) 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), progressiveSizes: [])) + representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: file.resource(key: SecretFileEncryptionKey(aesKey: key.makeData(), aesIv: iv.makeData()), decryptedSize: size), progressiveSizes: [], immediateThumbnailData: 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) } @@ -1178,8 +1178,8 @@ private func parseMessage(peerId: PeerId, authorId: PeerId, tagLocalIndex: Int32 } var previewRepresentations: [TelegramMediaImageRepresentation] = [] if thumb.size != 0 { - let resource = LocalFileMediaResource(fileId: arc4random64()) - previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: thumbW, height: thumbH), resource: resource, progressiveSizes: [])) + let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) + previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: thumbW, height: thumbH), resource: resource, progressiveSizes: [], immediateThumbnailData: nil)) resources.append((resource, thumb.makeData())) } let fileMedia = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.CloudSecretFile, id: file.id), partialReference: nil, resource: file.resource(key: SecretFileEncryptionKey(aesKey: key.makeData(), aesIv: iv.makeData()), decryptedSize: size), previewRepresentations: previewRepresentations, videoThumbnails: [], immediateThumbnailData: nil, mimeType: mimeType, size: Int(size), attributes: parsedAttributes) @@ -1209,8 +1209,8 @@ private func parseMessage(peerId: PeerId, authorId: PeerId, tagLocalIndex: Int32 let parsedAttributes: [TelegramMediaFileAttribute] = [.Video(duration: Int(duration), size: PixelDimensions(width: w, height: h), flags: []), .FileName(fileName: "video.mov")] var previewRepresentations: [TelegramMediaImageRepresentation] = [] if thumb.size != 0 { - let resource = LocalFileMediaResource(fileId: arc4random64()) - previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: thumbW, height: thumbH), resource: resource, progressiveSizes: [])) + let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) + previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: thumbW, height: thumbH), resource: resource, progressiveSizes: [], immediateThumbnailData: nil)) resources.append((resource, thumb.makeData())) } let fileMedia = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.CloudSecretFile, id: file.id), partialReference: nil, resource: file.resource(key: SecretFileEncryptionKey(aesKey: key.makeData(), aesIv: iv.makeData()), decryptedSize: size), previewRepresentations: previewRepresentations, videoThumbnails: [], immediateThumbnailData: nil, mimeType: mimeType, size: Int(size), attributes: parsedAttributes) @@ -1228,7 +1228,7 @@ private func parseMessage(peerId: PeerId, authorId: PeerId, tagLocalIndex: Int32 case let .photoSize(_, location, w, h, size): switch location { case let .fileLocation(dcId, volumeId, localId, secret): - previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: CloudFileMediaResource(datacenterId: Int(dcId), volumeId: volumeId, localId: localId, secret: secret, size: size == 0 ? nil : Int(size), fileReference: nil), progressiveSizes: [])) + previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: CloudFileMediaResource(datacenterId: Int(dcId), volumeId: volumeId, localId: localId, secret: secret, size: size == 0 ? nil : Int(size), fileReference: nil), progressiveSizes: [], immediateThumbnailData: nil)) case .fileLocationUnavailable: break } @@ -1238,7 +1238,7 @@ private func parseMessage(peerId: PeerId, authorId: PeerId, tagLocalIndex: Int32 case let .fileLocation(dcId, volumeId, localId, secret): let resource = CloudFileMediaResource(datacenterId: Int(dcId), volumeId: volumeId, localId: localId, secret: secret, size: bytes.size, fileReference: nil) resources.append((resource, bytes.makeData())) - previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: resource, progressiveSizes: [])) + previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: resource, progressiveSizes: [], immediateThumbnailData: nil)) case .fileLocationUnavailable: break } @@ -1249,11 +1249,11 @@ private func parseMessage(peerId: PeerId, authorId: PeerId, tagLocalIndex: Int32 let fileMedia = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.CloudFile, id: id), partialReference: nil, resource: CloudDocumentMediaResource(datacenterId: Int(dcId), fileId: id, accessHash: accessHash, size: Int(size), fileReference: nil, fileName: nil), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: mimeType, size: Int(size), attributes: parsedAttributes) parsedMedia.append(fileMedia) case let .decryptedMessageMediaWebPage(url): - parsedMedia.append(TelegramMediaWebpage(webpageId: MediaId(namespace: Namespaces.Media.LocalWebpage, id: arc4random64()), content: .Pending(0, url))) + parsedMedia.append(TelegramMediaWebpage(webpageId: MediaId(namespace: Namespaces.Media.LocalWebpage, id: Int64.random(in: Int64.min ... Int64.max)), content: .Pending(0, url))) case let .decryptedMessageMediaGeoPoint(lat, long): parsedMedia.append(TelegramMediaMap(latitude: lat, longitude: long, heading: nil, accuracyRadius: nil, geoPlace: nil, venue: nil, liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil)) case let .decryptedMessageMediaContact(phoneNumber, firstName, lastName, userId): - parsedMedia.append(TelegramMediaContact(firstName: firstName, lastName: lastName, phoneNumber: phoneNumber, peerId: userId == 0 ? nil : PeerId(namespace: Namespaces.Peer.CloudUser, id: userId), vCardData: nil)) + parsedMedia.append(TelegramMediaContact(firstName: firstName, lastName: lastName, phoneNumber: phoneNumber, peerId: userId == 0 ? nil : PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt32Value(userId)), vCardData: nil)) case let .decryptedMessageMediaVenue(lat, long, title, address, provider, venueId): parsedMedia.append(TelegramMediaMap(latitude: lat, longitude: long, heading: nil, accuracyRadius: nil, geoPlace: nil, venue: MapVenue(title: title, address: address, provider: provider, id: venueId, type: nil), liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil)) case .decryptedMessageMediaEmpty: diff --git a/submodules/TelegramCore/Sources/ProcessSecretChatIncomingEncryptedOperations.swift b/submodules/TelegramCore/Sources/State/ProcessSecretChatIncomingEncryptedOperations.swift similarity index 100% rename from submodules/TelegramCore/Sources/ProcessSecretChatIncomingEncryptedOperations.swift rename to submodules/TelegramCore/Sources/State/ProcessSecretChatIncomingEncryptedOperations.swift diff --git a/submodules/TelegramCore/Sources/Serialization.swift b/submodules/TelegramCore/Sources/State/Serialization.swift similarity index 99% rename from submodules/TelegramCore/Sources/Serialization.swift rename to submodules/TelegramCore/Sources/State/Serialization.swift index f0775e12ba..96684c4e81 100644 --- a/submodules/TelegramCore/Sources/Serialization.swift +++ b/submodules/TelegramCore/Sources/State/Serialization.swift @@ -210,7 +210,7 @@ public class BoxedMessage: NSObject { public class Serialization: NSObject, MTSerialization { public func currentLayer() -> UInt { - return 125 + return 131 } public func parseMessage(_ data: Data!) -> Any! { diff --git a/submodules/TelegramCore/Sources/StickerManagement.swift b/submodules/TelegramCore/Sources/State/StickerManagement.swift similarity index 97% rename from submodules/TelegramCore/Sources/StickerManagement.swift rename to submodules/TelegramCore/Sources/State/StickerManagement.swift index ab3da0cac2..4aefb157f1 100644 --- a/submodules/TelegramCore/Sources/StickerManagement.swift +++ b/submodules/TelegramCore/Sources/State/StickerManagement.swift @@ -90,7 +90,7 @@ public func preloadedFeaturedStickerSet(network: Network, postbox: Postbox, id: return postbox.transaction { transaction -> Signal in if let pack = transaction.getOrderedItemListItem(collectionId: Namespaces.OrderedItemList.CloudFeaturedStickerPacks, itemId: FeaturedStickerPackItemId(id.id).rawValue)?.contents as? FeaturedStickerPackItem { if pack.topItems.count < 5 && pack.topItems.count < pack.info.count { - return requestStickerSet(postbox: postbox, network: network, reference: .id(id: pack.info.id.id, accessHash: pack.info.accessHash)) + return _internal_requestStickerSet(postbox: postbox, network: network, reference: .id(id: pack.info.id.id, accessHash: pack.info.accessHash)) |> map(Optional.init) |> `catch` { _ -> Signal in return .single(nil) diff --git a/submodules/TelegramCore/Sources/SynchronizeAppLogEventsOperation.swift b/submodules/TelegramCore/Sources/State/SynchronizeAppLogEventsOperation.swift similarity index 94% rename from submodules/TelegramCore/Sources/SynchronizeAppLogEventsOperation.swift rename to submodules/TelegramCore/Sources/State/SynchronizeAppLogEventsOperation.swift index 541fb91a73..7ab8ac4b8f 100644 --- a/submodules/TelegramCore/Sources/SynchronizeAppLogEventsOperation.swift +++ b/submodules/TelegramCore/Sources/State/SynchronizeAppLogEventsOperation.swift @@ -7,7 +7,7 @@ import SyncCore public func addAppLogEvent(postbox: Postbox, time: Double = Date().timeIntervalSince1970, type: String, peerId: PeerId? = nil, data: JSON = .dictionary([:])) { let tag: PeerOperationLogTag = OperationLogTags.SynchronizeAppLogEvents - let peerId = PeerId(namespace: 0, id: 0) + let peerId = PeerId(0) let _ = (postbox.transaction { transaction in transaction.operationLogAddEntry(peerId: peerId, tag: tag, tagLocalIndex: .automatic, tagMergedIndex: .automatic, contents: SynchronizeAppLogEventsOperation(content: .add(time: time, type: type, peerId: peerId, data: data))) }).start() @@ -15,7 +15,7 @@ public func addAppLogEvent(postbox: Postbox, time: Double = Date().timeIntervalS public func invokeAppLogEventsSynchronization(postbox: Postbox) { let tag: PeerOperationLogTag = OperationLogTags.SynchronizeAppLogEvents - let peerId = PeerId(namespace: 0, id: 0) + let peerId = PeerId(0) let _ = (postbox.transaction { transaction in var topOperation: (SynchronizeSavedStickersOperation, Int32)? diff --git a/submodules/TelegramCore/Sources/SynchronizeChatInputStateOperation.swift b/submodules/TelegramCore/Sources/State/SynchronizeChatInputStateOperation.swift similarity index 100% rename from submodules/TelegramCore/Sources/SynchronizeChatInputStateOperation.swift rename to submodules/TelegramCore/Sources/State/SynchronizeChatInputStateOperation.swift diff --git a/submodules/TelegramCore/Sources/SynchronizeConsumeMessageContentsOperation.swift b/submodules/TelegramCore/Sources/State/SynchronizeConsumeMessageContentsOperation.swift similarity index 100% rename from submodules/TelegramCore/Sources/SynchronizeConsumeMessageContentsOperation.swift rename to submodules/TelegramCore/Sources/State/SynchronizeConsumeMessageContentsOperation.swift diff --git a/submodules/TelegramCore/Sources/SynchronizeEmojiKeywordsOperation.swift b/submodules/TelegramCore/Sources/State/SynchronizeEmojiKeywordsOperation.swift similarity index 82% rename from submodules/TelegramCore/Sources/SynchronizeEmojiKeywordsOperation.swift rename to submodules/TelegramCore/Sources/State/SynchronizeEmojiKeywordsOperation.swift index 52fb84441a..7baa130399 100644 --- a/submodules/TelegramCore/Sources/SynchronizeEmojiKeywordsOperation.swift +++ b/submodules/TelegramCore/Sources/State/SynchronizeEmojiKeywordsOperation.swift @@ -1,11 +1,12 @@ import Foundation import Postbox +import MurMurHash32 import SyncCore func addSynchronizeEmojiKeywordsOperation(transaction: Transaction, inputLanguageCode: String, languageCode: String?, fromVersion: Int32?) { let tag = OperationLogTags.SynchronizeEmojiKeywords - let peerId = PeerId(emojiKeywordColletionIdForCode(inputLanguageCode).id) + let peerId = PeerId(namespace: PeerId.Namespace._internalFromInt32Value(0), id: PeerId.Id._internalFromInt32Value(murMurHashString32(inputLanguageCode))) var hasExistingOperation = false transaction.operationLogEnumerateEntries(peerId: peerId, tag: tag) { entry -> Bool in diff --git a/submodules/TelegramCore/Sources/SynchronizeGroupedPeersOperation.swift b/submodules/TelegramCore/Sources/State/SynchronizeGroupedPeersOperation.swift similarity index 97% rename from submodules/TelegramCore/Sources/SynchronizeGroupedPeersOperation.swift rename to submodules/TelegramCore/Sources/State/SynchronizeGroupedPeersOperation.swift index ccc308a736..ee66d432f9 100644 --- a/submodules/TelegramCore/Sources/SynchronizeGroupedPeersOperation.swift +++ b/submodules/TelegramCore/Sources/State/SynchronizeGroupedPeersOperation.swift @@ -36,7 +36,7 @@ public func updatePeerGroupIdInteractively(transaction: Transaction, peerId: Pee private func addSynchronizeGroupedPeersOperation(transaction: Transaction, peerId: PeerId, groupId: PeerGroupId) { let tag: PeerOperationLogTag = OperationLogTags.SynchronizeGroupedPeers - let logPeerId = PeerId(namespace: 0, id: 0) + let logPeerId = PeerId(0) transaction.operationLogAddEntry(peerId: logPeerId, tag: tag, tagLocalIndex: .automatic, tagMergedIndex: .automatic, contents: SynchronizeGroupedPeersOperation(peerId: peerId, groupId: groupId)) } diff --git a/submodules/TelegramCore/Sources/SynchronizeInstalledStickerPacksOperation.swift b/submodules/TelegramCore/Sources/State/SynchronizeInstalledStickerPacksOperation.swift similarity index 84% rename from submodules/TelegramCore/Sources/SynchronizeInstalledStickerPacksOperation.swift rename to submodules/TelegramCore/Sources/State/SynchronizeInstalledStickerPacksOperation.swift index f550172e03..0cb242c17f 100644 --- a/submodules/TelegramCore/Sources/SynchronizeInstalledStickerPacksOperation.swift +++ b/submodules/TelegramCore/Sources/State/SynchronizeInstalledStickerPacksOperation.swift @@ -35,7 +35,7 @@ func addSynchronizeInstalledStickerPacksOperation(transaction: Transaction, name } var previousStickerPackIds: [ItemCollectionId]? var archivedPacks: [ItemCollectionId] = [] - transaction.operationLogEnumerateEntries(peerId: PeerId(namespace: 0, id: 0), tag: tag, { entry in + transaction.operationLogEnumerateEntries(peerId: PeerId(0), tag: tag, { entry in updateLocalIndex = entry.tagLocalIndex if let operation = entry.contents as? SynchronizeInstalledStickerPacksOperation { previousStickerPackIds = operation.previousPacks @@ -64,16 +64,16 @@ func addSynchronizeInstalledStickerPacksOperation(transaction: Transaction, name } let operationContents = SynchronizeInstalledStickerPacksOperation(previousPacks: previousPacks, archivedPacks: archivedPacks, noDelay: noDelay) if let updateLocalIndex = updateLocalIndex { - let _ = transaction.operationLogRemoveEntry(peerId: PeerId(namespace: 0, id: 0), tag: tag, tagLocalIndex: updateLocalIndex) + let _ = transaction.operationLogRemoveEntry(peerId: PeerId(0), tag: tag, tagLocalIndex: updateLocalIndex) } - transaction.operationLogAddEntry(peerId: PeerId(namespace: 0, id: 0), tag: tag, tagLocalIndex: .automatic, tagMergedIndex: .automatic, contents: operationContents) + transaction.operationLogAddEntry(peerId: PeerId(0), tag: tag, tagLocalIndex: .automatic, tagMergedIndex: .automatic, contents: operationContents) } func addSynchronizeMarkFeaturedStickerPacksAsSeenOperation(transaction: Transaction, ids: [ItemCollectionId]) { var updateLocalIndex: Int32? let tag: PeerOperationLogTag = OperationLogTags.SynchronizeMarkFeaturedStickerPacksAsSeen var previousIds = Set() - transaction.operationLogEnumerateEntries(peerId: PeerId(namespace: 0, id: 0), tag: tag, { entry in + transaction.operationLogEnumerateEntries(peerId: PeerId(0), tag: tag, { entry in updateLocalIndex = entry.tagLocalIndex if let operation = entry.contents as? SynchronizeMarkFeaturedStickerPacksAsSeenOperation { previousIds = Set(operation.ids) @@ -84,7 +84,7 @@ func addSynchronizeMarkFeaturedStickerPacksAsSeenOperation(transaction: Transact }) let operationContents = SynchronizeMarkFeaturedStickerPacksAsSeenOperation(ids: Array(previousIds.union(Set(ids)))) if let updateLocalIndex = updateLocalIndex { - let _ = transaction.operationLogRemoveEntry(peerId: PeerId(namespace: 0, id: 0), tag: tag, tagLocalIndex: updateLocalIndex) + let _ = transaction.operationLogRemoveEntry(peerId: PeerId(0), tag: tag, tagLocalIndex: updateLocalIndex) } - transaction.operationLogAddEntry(peerId: PeerId(namespace: 0, id: 0), tag: tag, tagLocalIndex: .automatic, tagMergedIndex: .automatic, contents: operationContents) + transaction.operationLogAddEntry(peerId: PeerId(0), tag: tag, tagLocalIndex: .automatic, tagMergedIndex: .automatic, contents: operationContents) } diff --git a/submodules/TelegramCore/Sources/SynchronizeLocalizationUpdatesOperation.swift b/submodules/TelegramCore/Sources/State/SynchronizeLocalizationUpdatesOperation.swift similarity index 94% rename from submodules/TelegramCore/Sources/SynchronizeLocalizationUpdatesOperation.swift rename to submodules/TelegramCore/Sources/State/SynchronizeLocalizationUpdatesOperation.swift index f471030761..b2e72b4a2a 100644 --- a/submodules/TelegramCore/Sources/SynchronizeLocalizationUpdatesOperation.swift +++ b/submodules/TelegramCore/Sources/State/SynchronizeLocalizationUpdatesOperation.swift @@ -6,7 +6,7 @@ import SyncCore func addSynchronizeLocalizationUpdatesOperation(transaction: Transaction) { let tag: PeerOperationLogTag = OperationLogTags.SynchronizeLocalizationUpdates - let peerId = PeerId(namespace: 0, id: 0) + let peerId = PeerId(0) var topLocalIndex: Int32? transaction.operationLogEnumerateEntries(peerId: peerId, tag: tag, { entry in diff --git a/submodules/TelegramCore/Sources/SynchronizeMarkAllUnseenPersonalMessagesOperation.swift b/submodules/TelegramCore/Sources/State/SynchronizeMarkAllUnseenPersonalMessagesOperation.swift similarity index 100% rename from submodules/TelegramCore/Sources/SynchronizeMarkAllUnseenPersonalMessagesOperation.swift rename to submodules/TelegramCore/Sources/State/SynchronizeMarkAllUnseenPersonalMessagesOperation.swift diff --git a/submodules/TelegramCore/Sources/SynchronizePeerReadState.swift b/submodules/TelegramCore/Sources/State/SynchronizePeerReadState.swift similarity index 99% rename from submodules/TelegramCore/Sources/SynchronizePeerReadState.swift rename to submodules/TelegramCore/Sources/State/SynchronizePeerReadState.swift index 6d745cb599..46d81f30be 100644 --- a/submodules/TelegramCore/Sources/SynchronizePeerReadState.swift +++ b/submodules/TelegramCore/Sources/State/SynchronizePeerReadState.swift @@ -206,10 +206,6 @@ private func validatePeerReadState(network: Network, postbox: Postbox, stateMana } private func pushPeerReadState(network: Network, postbox: Postbox, stateManager: AccountStateManager, peerId: PeerId, readState: PeerReadState) -> Signal { - if !GlobalTelegramCoreConfiguration.readMessages { - return .single(readState) - } - if peerId.namespace == Namespaces.Peer.SecretChat { return inputSecretChat(postbox: postbox, peerId: peerId) |> mapToSignal { inputPeer -> Signal in diff --git a/submodules/TelegramCore/Sources/SynchronizeRecentlyUsedMediaOperations.swift b/submodules/TelegramCore/Sources/State/SynchronizeRecentlyUsedMediaOperations.swift similarity index 98% rename from submodules/TelegramCore/Sources/SynchronizeRecentlyUsedMediaOperations.swift rename to submodules/TelegramCore/Sources/State/SynchronizeRecentlyUsedMediaOperations.swift index c8104c39fd..ab5ca09fab 100644 --- a/submodules/TelegramCore/Sources/SynchronizeRecentlyUsedMediaOperations.swift +++ b/submodules/TelegramCore/Sources/State/SynchronizeRecentlyUsedMediaOperations.swift @@ -14,7 +14,7 @@ func addSynchronizeRecentlyUsedMediaOperation(transaction: Transaction, category case .stickers: tag = OperationLogTags.SynchronizeRecentlyUsedStickers } - let peerId = PeerId(namespace: 0, id: 0) + let peerId = PeerId(0) var topOperation: (SynchronizeRecentlyUsedMediaOperation, Int32)? transaction.operationLogEnumerateEntries(peerId: peerId, tag: tag, { entry in diff --git a/submodules/TelegramCore/Sources/SynchronizeSavedGifsOperation.swift b/submodules/TelegramCore/Sources/State/SynchronizeSavedGifsOperation.swift similarity index 98% rename from submodules/TelegramCore/Sources/SynchronizeSavedGifsOperation.swift rename to submodules/TelegramCore/Sources/State/SynchronizeSavedGifsOperation.swift index e130f8c64e..2b4c00961a 100644 --- a/submodules/TelegramCore/Sources/SynchronizeSavedGifsOperation.swift +++ b/submodules/TelegramCore/Sources/State/SynchronizeSavedGifsOperation.swift @@ -6,7 +6,7 @@ import SyncCore func addSynchronizeSavedGifsOperation(transaction: Transaction, operation: SynchronizeSavedGifsOperationContent) { let tag: PeerOperationLogTag = OperationLogTags.SynchronizeSavedGifs - let peerId = PeerId(namespace: 0, id: 0) + let peerId = PeerId(0) var topOperation: (SynchronizeSavedGifsOperation, Int32)? transaction.operationLogEnumerateEntries(peerId: peerId, tag: tag, { entry in diff --git a/submodules/TelegramCore/Sources/SynchronizeSavedStickersOperation.swift b/submodules/TelegramCore/Sources/State/SynchronizeSavedStickersOperation.swift similarity index 99% rename from submodules/TelegramCore/Sources/SynchronizeSavedStickersOperation.swift rename to submodules/TelegramCore/Sources/State/SynchronizeSavedStickersOperation.swift index bd53548f3e..5eb5ea9075 100644 --- a/submodules/TelegramCore/Sources/SynchronizeSavedStickersOperation.swift +++ b/submodules/TelegramCore/Sources/State/SynchronizeSavedStickersOperation.swift @@ -7,7 +7,7 @@ import SyncCore func addSynchronizeSavedStickersOperation(transaction: Transaction, operation: SynchronizeSavedStickersOperationContent) { let tag: PeerOperationLogTag = OperationLogTags.SynchronizeSavedStickers - let peerId = PeerId(namespace: 0, id: 0) + let peerId = PeerId(0) var topOperation: (SynchronizeSavedStickersOperation, Int32)? transaction.operationLogEnumerateEntries(peerId: peerId, tag: tag, { entry in diff --git a/submodules/TelegramCore/Sources/UnauthorizedAccountStateManager.swift b/submodules/TelegramCore/Sources/State/UnauthorizedAccountStateManager.swift similarity index 82% rename from submodules/TelegramCore/Sources/UnauthorizedAccountStateManager.swift rename to submodules/TelegramCore/Sources/State/UnauthorizedAccountStateManager.swift index b9993cf1f9..57895597f9 100644 --- a/submodules/TelegramCore/Sources/UnauthorizedAccountStateManager.swift +++ b/submodules/TelegramCore/Sources/State/UnauthorizedAccountStateManager.swift @@ -54,10 +54,12 @@ final class UnauthorizedAccountStateManager { private var updateService: UnauthorizedUpdateMessageService? private let updateServiceDisposable = MetaDisposable() private let updateLoginToken: () -> Void + private let displayServiceNotification: (String) -> Void - init(network: Network, updateLoginToken: @escaping () -> Void) { + init(network: Network, updateLoginToken: @escaping () -> Void, displayServiceNotification: @escaping (String) -> Void) { self.network = network self.updateLoginToken = updateLoginToken + self.displayServiceNotification = displayServiceNotification } deinit { @@ -69,11 +71,17 @@ final class UnauthorizedAccountStateManager { if self.updateService == nil { self.updateService = UnauthorizedUpdateMessageService() let updateLoginToken = self.updateLoginToken + let displayServiceNotification = self.displayServiceNotification self.updateServiceDisposable.set(self.updateService!.pipe.signal().start(next: { updates in for update in updates { switch update { case .updateLoginToken: updateLoginToken() + case let .updateServiceNotification(flags, _, _, message, _, _): + let popup = (flags & (1 << 0)) != 0 + if popup { + displayServiceNotification(message) + } default: break } diff --git a/submodules/TelegramCore/Sources/UpdateGroup.swift b/submodules/TelegramCore/Sources/State/UpdateGroup.swift similarity index 100% rename from submodules/TelegramCore/Sources/UpdateGroup.swift rename to submodules/TelegramCore/Sources/State/UpdateGroup.swift diff --git a/submodules/TelegramCore/Sources/UpdateMessageService.swift b/submodules/TelegramCore/Sources/State/UpdateMessageService.swift similarity index 99% rename from submodules/TelegramCore/Sources/UpdateMessageService.swift rename to submodules/TelegramCore/Sources/State/UpdateMessageService.swift index aa4b178dd1..fc375938cc 100644 --- a/submodules/TelegramCore/Sources/UpdateMessageService.swift +++ b/submodules/TelegramCore/Sources/State/UpdateMessageService.swift @@ -67,7 +67,7 @@ class UpdateMessageService: NSObject, MTMessageService { case let .updateShortMessage(flags, id, userId, message, pts, ptsCount, date, fwdFrom, viaBotId, replyHeader, entities, ttlPeriod): let generatedFromId: Api.Peer if (Int(flags) & 1 << 1) != 0 { - generatedFromId = Api.Peer.peerUser(userId: self.peerId.id) + generatedFromId = Api.Peer.peerUser(userId: self.peerId.id._internalGetInt32Value()) } else { generatedFromId = Api.Peer.peerUser(userId: userId) } diff --git a/submodules/TelegramCore/Sources/UpdatesApiUtils.swift b/submodules/TelegramCore/Sources/State/UpdatesApiUtils.swift similarity index 86% rename from submodules/TelegramCore/Sources/UpdatesApiUtils.swift rename to submodules/TelegramCore/Sources/State/UpdatesApiUtils.swift index b9f6ae6935..e32acae449 100644 --- a/submodules/TelegramCore/Sources/UpdatesApiUtils.swift +++ b/submodules/TelegramCore/Sources/State/UpdatesApiUtils.swift @@ -9,13 +9,10 @@ private func collectPreCachedResources(for photo: Api.Photo) -> [(MediaResource, case let .photo(_, id, accessHash, fileReference, _, sizes, _, dcId): for size in sizes { switch size { - case let .photoCachedSize(type, location, _, _, bytes): - switch location { - case let .fileLocationToBeDeprecated(volumeId, localId): - let resource = CloudPhotoSizeMediaResource(datacenterId: dcId, photoId: id, accessHash: accessHash, sizeSpec: type, volumeId: volumeId, localId: localId, size: nil, fileReference: fileReference.makeData()) - let data = bytes.makeData() - return [(resource, data)] - } + case let .photoCachedSize(type, _, _, bytes): + let resource = CloudPhotoSizeMediaResource(datacenterId: dcId, photoId: id, accessHash: accessHash, sizeSpec: type, size: nil, fileReference: fileReference.makeData()) + let data = bytes.makeData() + return [(resource, data)] default: break } @@ -32,13 +29,10 @@ private func collectPreCachedResources(for document: Api.Document) -> [(MediaRes if let thumbs = thumbs { for thumb in thumbs { switch thumb { - case let .photoCachedSize(type, location, _, _, bytes): - switch location { - case let .fileLocationToBeDeprecated(volumeId, localId): - let resource = CloudDocumentSizeMediaResource(datacenterId: dcId, documentId: id, accessHash: accessHash, sizeSpec: type, volumeId: volumeId, localId: localId, fileReference: fileReference.makeData()) - let data = bytes.makeData() - return [(resource, data)] - } + case let .photoCachedSize(type, _, _, bytes): + let resource = CloudDocumentSizeMediaResource(datacenterId: dcId, documentId: id, accessHash: accessHash, sizeSpec: type, fileReference: fileReference.makeData()) + let data = bytes.makeData() + return [(resource, data)] default: break } @@ -152,15 +146,15 @@ extension Api.Chat { var peerId: PeerId { switch self { case let .chat(chat): - return PeerId(namespace: Namespaces.Peer.CloudGroup, id: chat.id) + return PeerId(namespace: Namespaces.Peer.CloudGroup, id: PeerId.Id._internalFromInt32Value(chat.id)) case let .chatEmpty(id): - return PeerId(namespace: Namespaces.Peer.CloudGroup, id: id) + return PeerId(namespace: Namespaces.Peer.CloudGroup, id: PeerId.Id._internalFromInt32Value(id)) case let .chatForbidden(id, _): - return PeerId(namespace: Namespaces.Peer.CloudGroup, id: id) + return PeerId(namespace: Namespaces.Peer.CloudGroup, id: PeerId.Id._internalFromInt32Value(id)) case let .channel(channel): - return PeerId(namespace: Namespaces.Peer.CloudChannel, id: channel.id) + return PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt32Value(channel.id)) case let .channelForbidden(_, id, _, _, _): - return PeerId(namespace: Namespaces.Peer.CloudChannel, id: id) + return PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt32Value(id)) } } } @@ -169,9 +163,9 @@ extension Api.User { var peerId: PeerId { switch self { case .user(_, let id, _, _, _, _, _, _, _, _, _, _, _): - return PeerId(namespace: Namespaces.Peer.CloudUser, id: id) + return PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt32Value(id)) case let .userEmpty(id): - return PeerId(namespace: Namespaces.Peer.CloudUser, id: id) + return PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt32Value(id)) } } } @@ -180,11 +174,11 @@ extension Api.Peer { var peerId: PeerId { switch self { case let .peerChannel(channelId): - return PeerId(namespace: Namespaces.Peer.CloudChannel, id: channelId) + return PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt32Value(channelId)) case let .peerChat(chatId): - return PeerId(namespace: Namespaces.Peer.CloudGroup, id: chatId) + return PeerId(namespace: Namespaces.Peer.CloudGroup, id: PeerId.Id._internalFromInt32Value(chatId)) case let .peerUser(userId): - return PeerId(namespace: Namespaces.Peer.CloudUser, id: userId) + return PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt32Value(userId)) } } } @@ -254,40 +248,40 @@ extension Api.Update { var peerIds: [PeerId] { switch self { case let .updateChannel(channelId): - return [PeerId(namespace: Namespaces.Peer.CloudChannel, id: channelId)] + return [PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt32Value(channelId))] case let .updateChat(chatId): - return [PeerId(namespace: Namespaces.Peer.CloudGroup, id: chatId)] + return [PeerId(namespace: Namespaces.Peer.CloudGroup, id: PeerId.Id._internalFromInt32Value(chatId))] case let .updateChannelTooLong(_, channelId, _): - return [PeerId(namespace: Namespaces.Peer.CloudChannel, id: channelId)] + return [PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt32Value(channelId))] case let .updateChatParticipantAdd(chatId, userId, inviterId, _, _): - return [PeerId(namespace: Namespaces.Peer.CloudGroup, id: chatId), PeerId(namespace: Namespaces.Peer.CloudUser, id: userId), PeerId(namespace: Namespaces.Peer.CloudUser, id: inviterId)] + return [PeerId(namespace: Namespaces.Peer.CloudGroup, id: PeerId.Id._internalFromInt32Value(chatId)), PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt32Value(userId)), PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt32Value(inviterId))] case let .updateChatParticipantAdmin(chatId, userId, _, _): - return [PeerId(namespace: Namespaces.Peer.CloudGroup, id: chatId), PeerId(namespace: Namespaces.Peer.CloudUser, id: userId)] + return [PeerId(namespace: Namespaces.Peer.CloudGroup, id: PeerId.Id._internalFromInt32Value(chatId)), PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt32Value(userId))] case let .updateChatParticipantDelete(chatId, userId, _): - return [PeerId(namespace: Namespaces.Peer.CloudGroup, id: chatId), PeerId(namespace: Namespaces.Peer.CloudUser, id: userId)] + return [PeerId(namespace: Namespaces.Peer.CloudGroup, id: PeerId.Id._internalFromInt32Value(chatId)), PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt32Value(userId))] case let .updateChatParticipants(participants): switch participants { case let .chatParticipants(chatId, _, _): - return [PeerId(namespace: Namespaces.Peer.CloudGroup, id: chatId)] + return [PeerId(namespace: Namespaces.Peer.CloudGroup, id: PeerId.Id._internalFromInt32Value(chatId))] case let .chatParticipantsForbidden(_, chatId, _): - return [PeerId(namespace: Namespaces.Peer.CloudGroup, id: chatId)] + return [PeerId(namespace: Namespaces.Peer.CloudGroup, id: PeerId.Id._internalFromInt32Value(chatId))] } case let .updateDeleteChannelMessages(channelId, _, _, _): - return [PeerId(namespace: Namespaces.Peer.CloudChannel, id: channelId)] + return [PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt32Value(channelId))] case let .updatePinnedChannelMessages(_, channelId, _, _, _): - return [PeerId(namespace: Namespaces.Peer.CloudChannel, id: channelId)] + return [PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt32Value(channelId))] case let .updateNewChannelMessage(message, _, _): return apiMessagePeerIds(message) case let .updateEditChannelMessage(message, _, _): return apiMessagePeerIds(message) case let .updateChannelWebPage(channelId, _, _, _): - return [PeerId(namespace: Namespaces.Peer.CloudChannel, id: channelId)] + return [PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt32Value(channelId))] case let .updateNewMessage(message, _, _): return apiMessagePeerIds(message) case let .updateEditMessage(message, _, _): return apiMessagePeerIds(message) case let .updateReadChannelInbox(_, _, channelId, _, _, _): - return [PeerId(namespace: Namespaces.Peer.CloudChannel, id: channelId)] + return [PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt32Value(channelId))] case let .updateNotifySettings(peer, _): switch peer { case let .notifyPeer(peer): @@ -296,14 +290,14 @@ extension Api.Update { return [] } case let .updateUserName(userId, _, _, _): - return [PeerId(namespace: Namespaces.Peer.CloudUser, id: userId)] + return [PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt32Value(userId))] case let .updateUserPhone(userId, _): - return [PeerId(namespace: Namespaces.Peer.CloudUser, id: userId)] + return [PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt32Value(userId))] case let .updateUserPhoto(userId, _, _, _): - return [PeerId(namespace: Namespaces.Peer.CloudUser, id: userId)] + return [PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt32Value(userId))] case let .updateServiceNotification(_, inboxDate, _, _, _, _): if let _ = inboxDate { - return [PeerId(namespace: Namespaces.Peer.CloudUser, id: 777000)] + return [PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt32Value(777000))] } else { return [] } @@ -424,9 +418,9 @@ extension Api.Updates { case .updatesTooLong: return [] case let .updateShortMessage(_, id, userId, _, _, _, _, _, _, _, _, _): - return [MessageId(peerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: userId), namespace: Namespaces.Message.Cloud, id: id)] + return [MessageId(peerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt32Value(userId)), namespace: Namespaces.Message.Cloud, id: id)] case let .updateShortChatMessage(_, id, _, chatId, _, _, _, _, _, _, _, _, _): - return [MessageId(peerId: PeerId(namespace: Namespaces.Peer.CloudGroup, id: chatId), namespace: Namespaces.Message.Cloud, id: id)] + return [MessageId(peerId: PeerId(namespace: Namespaces.Peer.CloudGroup, id: PeerId.Id._internalFromInt32Value(chatId)), namespace: Namespaces.Message.Cloud, id: id)] } } @@ -555,15 +549,15 @@ extension Api.EncryptedChat { var peerId: PeerId { switch self { case let .encryptedChat(id, _, _, _, _, _, _): - return PeerId(namespace: Namespaces.Peer.SecretChat, id: id) + return PeerId(namespace: Namespaces.Peer.SecretChat, id: PeerId.Id._internalFromInt32Value(id)) case let .encryptedChatDiscarded(_, id): - return PeerId(namespace: Namespaces.Peer.SecretChat, id: id) + return PeerId(namespace: Namespaces.Peer.SecretChat, id: PeerId.Id._internalFromInt32Value(id)) case let .encryptedChatEmpty(id): - return PeerId(namespace: Namespaces.Peer.SecretChat, id: id) + return PeerId(namespace: Namespaces.Peer.SecretChat, id: PeerId.Id._internalFromInt32Value(id)) case let .encryptedChatRequested(_, _, id, _, _, _, _, _): - return PeerId(namespace: Namespaces.Peer.SecretChat, id: id) + return PeerId(namespace: Namespaces.Peer.SecretChat, id: PeerId.Id._internalFromInt32Value(id)) case let .encryptedChatWaiting(id, _, _, _, _): - return PeerId(namespace: Namespaces.Peer.SecretChat, id: id) + return PeerId(namespace: Namespaces.Peer.SecretChat, id: PeerId.Id._internalFromInt32Value(id)) } } } @@ -572,9 +566,9 @@ extension Api.EncryptedMessage { var peerId: PeerId { switch self { case let .encryptedMessage(_, chatId, _, _, _): - return PeerId(namespace: Namespaces.Peer.SecretChat, id: chatId) + return PeerId(namespace: Namespaces.Peer.SecretChat, id: PeerId.Id._internalFromInt32Value(chatId)) case let .encryptedMessageService(_, chatId, _, _): - return PeerId(namespace: Namespaces.Peer.SecretChat, id: chatId) + return PeerId(namespace: Namespaces.Peer.SecretChat, id: PeerId.Id._internalFromInt32Value(chatId)) } } } diff --git a/submodules/TelegramCore/Sources/StringFormat.swift b/submodules/TelegramCore/Sources/StringFormat.swift deleted file mode 100644 index e71bca62ab..0000000000 --- a/submodules/TelegramCore/Sources/StringFormat.swift +++ /dev/null @@ -1,36 +0,0 @@ -import Foundation - -// Incuding at least one Objective-C class in a swift file ensures that it doesn't get stripped by the linker -private final class LinkHelperClass: NSObject { -} - -public func dataSizeString(_ size: Int, forceDecimal: Bool = false, decimalSeparator: String = ".") -> String { - return dataSizeString(Int64(size), forceDecimal: forceDecimal, decimalSeparator: decimalSeparator) -} - -public func dataSizeString(_ size: Int64, forceDecimal: Bool = false, decimalSeparator: String = ".") -> String { - if size >= 1024 * 1024 * 1024 { - let remainder = Int64((Double(size % (1024 * 1024 * 1024)) / (1024 * 1024 * 102.4)).rounded(.down)) - if remainder != 0 || forceDecimal { - return "\(size / (1024 * 1024 * 1024))\(decimalSeparator)\(remainder) GB" - } else { - return "\(size / (1024 * 1024 * 1024)) GB" - } - } else if size >= 1024 * 1024 { - let remainder = Int64((Double(size % (1024 * 1024)) / (1024.0 * 102.4)).rounded(.down)) - if remainder != 0 || forceDecimal { - return "\(size / (1024 * 1024))\(decimalSeparator)\(remainder) MB" - } else { - return "\(size / (1024 * 1024)) MB" - } - } else if size >= 1024 { - let remainder = (size % (1024)) / (102) - if remainder != 0 || forceDecimal { - return "\(size / 1024)\(decimalSeparator)\(remainder) KB" - } else { - return "\(size / 1024) KB" - } - } else { - return "\(size) B" - } -} diff --git a/submodules/TelegramCore/Sources/Suggestions.swift b/submodules/TelegramCore/Sources/Suggestions.swift index aaa642eba5..1703b0316d 100644 --- a/submodules/TelegramCore/Sources/Suggestions.swift +++ b/submodules/TelegramCore/Sources/Suggestions.swift @@ -7,12 +7,22 @@ import SyncCore public enum ServerProvidedSuggestion: String { case autoarchivePopular = "AUTOARCHIVE_POPULAR" case newcomerTicks = "NEWCOMER_TICKS" + case validatePhoneNumber = "VALIDATE_PHONE_NUMBER" + case validatePassword = "VALIDATE_PASSWORD" } -public func getServerProvidedSuggestions(postbox: Postbox) -> Signal<[ServerProvidedSuggestion], NoError> { +private var dismissedSuggestionsPromise = ValuePromise<[AccountRecordId: Set]>([:]) +private var dismissedSuggestions: [AccountRecordId: Set] = [:] { + didSet { + dismissedSuggestionsPromise.set(dismissedSuggestions) + } +} + +public func getServerProvidedSuggestions(account: Account) -> Signal<[ServerProvidedSuggestion], NoError> { let key: PostboxViewKey = .preferences(keys: Set([PreferencesKeys.appConfiguration])) - return postbox.combinedView(keys: [key]) - |> map { views -> [ServerProvidedSuggestion] in + return combineLatest(account.postbox.combinedView(keys: [key]), dismissedSuggestionsPromise.get()) + |> map { views, dismissedSuggestionsValue -> [ServerProvidedSuggestion] in + let dismissedSuggestions = dismissedSuggestionsValue[account.id] ?? Set() guard let view = views.views[key] as? PreferencesView else { return [] } @@ -24,12 +34,17 @@ public func getServerProvidedSuggestions(postbox: Postbox) -> Signal<[ServerProv } return list.compactMap { item -> ServerProvidedSuggestion? in return ServerProvidedSuggestion(rawValue: item) - } + }.filter { !dismissedSuggestions.contains($0) } } |> distinctUntilChanged } public func dismissServerProvidedSuggestion(account: Account, suggestion: ServerProvidedSuggestion) -> Signal { + if let _ = dismissedSuggestions[account.id] { + dismissedSuggestions[account.id]?.insert(suggestion) + } else { + dismissedSuggestions[account.id] = Set([suggestion]) + } return account.network.request(Api.functions.help.dismissSuggestion(peer: .inputPeerEmpty, suggestion: suggestion.rawValue)) |> `catch` { _ -> Signal in return .single(.boolFalse) diff --git a/submodules/TelegramCore/Sources/TelegramCore.h b/submodules/TelegramCore/Sources/TelegramCore.h deleted file mode 100644 index 286a45d335..0000000000 --- a/submodules/TelegramCore/Sources/TelegramCore.h +++ /dev/null @@ -1,20 +0,0 @@ -// -// TelegramCore.h -// TelegramCore -// -// Created by Peter on 8/1/16. -// Copyright © 2016 Peter. All rights reserved. -// - -#import - -//! Project version number for TelegramCore. -FOUNDATION_EXPORT double TelegramCoreVersionNumber; - -//! Project version string for TelegramCore. -FOUNDATION_EXPORT const unsigned char TelegramCoreVersionString[]; - -#import -#import -#import -#import diff --git a/submodules/TelegramCore/Sources/ChangeAccountPhoneNumber.swift b/submodules/TelegramCore/Sources/TelegramEngine/AccountData/ChangeAccountPhoneNumber.swift similarity index 86% rename from submodules/TelegramCore/Sources/ChangeAccountPhoneNumber.swift rename to submodules/TelegramCore/Sources/TelegramEngine/AccountData/ChangeAccountPhoneNumber.swift index b4fcc97cd6..8ceec98541 100644 --- a/submodules/TelegramCore/Sources/ChangeAccountPhoneNumber.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/AccountData/ChangeAccountPhoneNumber.swift @@ -33,10 +33,11 @@ public enum RequestChangeAccountPhoneNumberVerificationError { case invalidPhoneNumber case limitExceeded case phoneNumberOccupied + case phoneBanned case generic } -public func requestChangeAccountPhoneNumberVerification(account: Account, phoneNumber: String) -> Signal { +func _internal_requestChangeAccountPhoneNumberVerification(account: Account, phoneNumber: String) -> Signal { return account.network.request(Api.functions.account.sendChangePhoneCode(phoneNumber: phoneNumber, settings: .codeSettings(flags: 0)), automaticFloodWait: false) |> mapError { error -> RequestChangeAccountPhoneNumberVerificationError in if error.errorDescription.hasPrefix("FLOOD_WAIT") { @@ -45,6 +46,8 @@ public func requestChangeAccountPhoneNumberVerification(account: Account, phoneN return .invalidPhoneNumber } else if error.errorDescription == "PHONE_NUMBER_OCCUPIED" { return .phoneNumberOccupied + } else if error.errorDescription == "PHONE_NUMBER_BANNED" { + return .phoneBanned } else { return .generic } @@ -61,7 +64,7 @@ public func requestChangeAccountPhoneNumberVerification(account: Account, phoneN } } -public func requestNextChangeAccountPhoneNumberVerification(account: Account, phoneNumber: String, phoneCodeHash: String) -> Signal { +func _internal_requestNextChangeAccountPhoneNumberVerification(account: Account, phoneNumber: String, phoneCodeHash: String) -> Signal { return account.network.request(Api.functions.auth.resendCode(phoneNumber: phoneNumber, phoneCodeHash: phoneCodeHash), automaticFloodWait: false) |> mapError { error -> RequestChangeAccountPhoneNumberVerificationError in if error.errorDescription.hasPrefix("FLOOD_WAIT") { @@ -93,7 +96,7 @@ public enum ChangeAccountPhoneNumberError { case limitExceeded } -public func requestChangeAccountPhoneNumber(account: Account, phoneNumber: String, phoneCodeHash: String, phoneCode: String) -> Signal { +func _internal_requestChangeAccountPhoneNumber(account: Account, phoneNumber: String, phoneCodeHash: String, phoneCode: String) -> Signal { return account.network.request(Api.functions.account.changePhone(phoneNumber: phoneNumber, phoneCodeHash: phoneCodeHash, phoneCode: phoneCode), automaticFloodWait: false) |> mapError { error -> ChangeAccountPhoneNumberError in if error.errorDescription.hasPrefix("FLOOD_WAIT") { diff --git a/submodules/TelegramCore/Sources/RegisterNotificationToken.swift b/submodules/TelegramCore/Sources/TelegramEngine/AccountData/RegisterNotificationToken.swift similarity index 71% rename from submodules/TelegramCore/Sources/RegisterNotificationToken.swift rename to submodules/TelegramCore/Sources/TelegramEngine/AccountData/RegisterNotificationToken.swift index d57cb98c9b..9349247abb 100644 --- a/submodules/TelegramCore/Sources/RegisterNotificationToken.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/AccountData/RegisterNotificationToken.swift @@ -10,7 +10,7 @@ public enum NotificationTokenType { case voip } -public func unregisterNotificationToken(account: Account, token: Data, type: NotificationTokenType, otherAccountUserIds: [Int32]) -> Signal { +func _internal_unregisterNotificationToken(account: Account, token: Data, type: NotificationTokenType, otherAccountUserIds: [PeerId.Id]) -> Signal { let mappedType: Int32 switch type { case .aps: @@ -18,12 +18,12 @@ public func unregisterNotificationToken(account: Account, token: Data, type: Not case .voip: mappedType = 9 } - return account.network.request(Api.functions.account.unregisterDevice(tokenType: mappedType, token: hexString(token), otherUids: otherAccountUserIds)) + return account.network.request(Api.functions.account.unregisterDevice(tokenType: mappedType, token: hexString(token), otherUids: otherAccountUserIds.map({ $0._internalGetInt32Value() }))) |> retryRequest |> ignoreValues } -public func registerNotificationToken(account: Account, token: Data, type: NotificationTokenType, sandbox: Bool, otherAccountUserIds: [Int32], excludeMutedChats: Bool) -> Signal { +func _internal_registerNotificationToken(account: Account, token: Data, type: NotificationTokenType, sandbox: Bool, otherAccountUserIds: [PeerId.Id], excludeMutedChats: Bool) -> Signal { return masterNotificationsKey(account: account, ignoreDisabled: false) |> mapToSignal { masterKey -> Signal in let mappedType: Int32 @@ -42,7 +42,7 @@ public func registerNotificationToken(account: Account, token: Data, type: Notif if excludeMutedChats { flags |= 1 << 0 } - return account.network.request(Api.functions.account.registerDevice(flags: flags, tokenType: mappedType, token: hexString(token), appSandbox: sandbox ? .boolTrue : .boolFalse, secret: Buffer(data: keyData), otherUids: otherAccountUserIds)) + return account.network.request(Api.functions.account.registerDevice(flags: flags, tokenType: mappedType, token: hexString(token), appSandbox: sandbox ? .boolTrue : .boolFalse, secret: Buffer(data: keyData), otherUids: otherAccountUserIds.map({ $0._internalGetInt32Value() }))) |> retryRequest |> ignoreValues } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/AccountData/TelegramEngineAccountData.swift b/submodules/TelegramCore/Sources/TelegramEngine/AccountData/TelegramEngineAccountData.swift new file mode 100644 index 0000000000..5fbab699aa --- /dev/null +++ b/submodules/TelegramCore/Sources/TelegramEngine/AccountData/TelegramEngineAccountData.swift @@ -0,0 +1,62 @@ +import Foundation +import SwiftSignalKit +import Postbox +import SyncCore + +public extension TelegramEngine { + final class AccountData { + private let account: Account + + init(account: Account) { + self.account = account + } + + public func acceptTermsOfService(id: String) -> Signal { + return _internal_acceptTermsOfService(account: self.account, id: id) + } + + public func resetAccountDueTermsOfService() -> Signal { + return _internal_resetAccountDueTermsOfService(network: self.account.network) + } + + public func requestChangeAccountPhoneNumberVerification(phoneNumber: String) -> Signal { + return _internal_requestChangeAccountPhoneNumberVerification(account: self.account, phoneNumber: phoneNumber) + } + + public func requestNextChangeAccountPhoneNumberVerification(phoneNumber: String, phoneCodeHash: String) -> Signal { + return _internal_requestNextChangeAccountPhoneNumberVerification(account: self.account, phoneNumber: phoneNumber, phoneCodeHash: phoneCodeHash) + } + + public func requestChangeAccountPhoneNumber(phoneNumber: String, phoneCodeHash: String, phoneCode: String) -> Signal { + return _internal_requestChangeAccountPhoneNumber(account: self.account, phoneNumber: phoneNumber, phoneCodeHash: phoneCodeHash, phoneCode: phoneCode) + } + + public func updateAccountPeerName(firstName: String, lastName: String) -> Signal { + return _internal_updateAccountPeerName(account: self.account, firstName: firstName, lastName: lastName) + } + + public func updateAbout(about: String?) -> Signal { + return _internal_updateAbout(account: self.account, about: about) + } + + public func unregisterNotificationToken(token: Data, type: NotificationTokenType, otherAccountUserIds: [PeerId.Id]) -> Signal { + return _internal_unregisterNotificationToken(account: self.account, token: token, type: type, otherAccountUserIds: otherAccountUserIds) + } + + public func registerNotificationToken(token: Data, type: NotificationTokenType, sandbox: Bool, otherAccountUserIds: [PeerId.Id], excludeMutedChats: Bool) -> Signal { + return _internal_registerNotificationToken(account: self.account, token: token, type: type, sandbox: sandbox, otherAccountUserIds: otherAccountUserIds, excludeMutedChats: excludeMutedChats) + } + + public func updateAccountPhoto(resource: MediaResource?, videoResource: MediaResource?, videoStartTimestamp: Double?, mapResourceToAvatarSizes: @escaping (MediaResource, [TelegramMediaImageRepresentation]) -> Signal<[Int: Data], NoError>) -> Signal { + return _internal_updateAccountPhoto(account: self.account, resource: resource, videoResource: videoResource, videoStartTimestamp: videoStartTimestamp, mapResourceToAvatarSizes: mapResourceToAvatarSizes) + } + + public func updatePeerPhotoExisting(reference: TelegramMediaImageReference) -> Signal { + return _internal_updatePeerPhotoExisting(network: self.account.network, reference: reference) + } + + public func removeAccountPhoto(reference: TelegramMediaImageReference?) -> Signal { + return _internal_removeAccountPhoto(network: self.account.network, reference: reference) + } + } +} diff --git a/submodules/TelegramCore/Sources/TermsOfService.swift b/submodules/TelegramCore/Sources/TelegramEngine/AccountData/TermsOfService.swift similarity index 93% rename from submodules/TelegramCore/Sources/TermsOfService.swift rename to submodules/TelegramCore/Sources/TelegramEngine/AccountData/TermsOfService.swift index e3514b3fe9..b0c66a3537 100644 --- a/submodules/TelegramCore/Sources/TermsOfService.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/AccountData/TermsOfService.swift @@ -34,7 +34,7 @@ extension TermsOfServiceUpdate { } } -public func acceptTermsOfService(account: Account, id: String) -> Signal { +func _internal_acceptTermsOfService(account: Account, id: String) -> Signal { return account.network.request(Api.functions.help.acceptTermsOfService(id: .dataJSON(data: id))) |> `catch` { _ -> Signal in return .complete() @@ -45,7 +45,7 @@ public func acceptTermsOfService(account: Account, id: String) -> Signal Signal { +func _internal_resetAccountDueTermsOfService(network: Network) -> Signal { return network.request(Api.functions.account.deleteAccount(reason: "Decline ToS update")) |> retryRequest |> map { _ in return } diff --git a/submodules/TelegramCore/Sources/UpdateAccountPeerName.swift b/submodules/TelegramCore/Sources/TelegramEngine/AccountData/UpdateAccountPeerName.swift similarity index 88% rename from submodules/TelegramCore/Sources/UpdateAccountPeerName.swift rename to submodules/TelegramCore/Sources/TelegramEngine/AccountData/UpdateAccountPeerName.swift index f46bc43b89..533a240653 100644 --- a/submodules/TelegramCore/Sources/UpdateAccountPeerName.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/AccountData/UpdateAccountPeerName.swift @@ -6,7 +6,7 @@ import MtProtoKit import SyncCore -public func updateAccountPeerName(account: Account, firstName: String, lastName: String) -> Signal { +func _internal_updateAccountPeerName(account: Account, firstName: String, lastName: String) -> Signal { return account.network.request(Api.functions.account.updateProfile(flags: (1 << 0) | (1 << 1), firstName: firstName, lastName: lastName, about: nil)) |> map { result -> Api.User? in return result @@ -30,7 +30,7 @@ public enum UpdateAboutError { } -public func updateAbout(account: Account, about: String?) -> Signal { +func _internal_updateAbout(account: Account, about: String?) -> Signal { return account.network.request(Api.functions.account.updateProfile(flags: about == nil ? 0 : (1 << 2), firstName: nil, lastName: nil, about: about)) |> mapError { _ -> UpdateAboutError in return .generic diff --git a/submodules/TelegramCore/Sources/AuthTransfer.swift b/submodules/TelegramCore/Sources/TelegramEngine/Auth/AuthTransfer.swift similarity index 96% rename from submodules/TelegramCore/Sources/AuthTransfer.swift rename to submodules/TelegramCore/Sources/TelegramEngine/Auth/AuthTransfer.swift index e661aa715d..0e5e086488 100644 --- a/submodules/TelegramCore/Sources/AuthTransfer.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Auth/AuthTransfer.swift @@ -21,8 +21,8 @@ public enum ExportAuthTransferTokenResult { 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)) +func _internal_exportAuthTransferToken(accountManager: AccountManager, account: UnauthorizedAccount, otherAccountUserIds: [PeerId.Id], syncContacts: Bool) -> Signal { + return account.network.request(Api.functions.auth.exportLoginToken(apiId: account.networkArguments.apiId, apiHash: account.networkArguments.apiHash, exceptIds: otherAccountUserIds.map({ $0._internalGetInt32Value() }))) |> map(Optional.init) |> `catch` { error -> Signal in if error.errorDescription == "SESSION_PASSWORD_NEEDED" { diff --git a/submodules/TelegramCore/Sources/CancelAccountReset.swift b/submodules/TelegramCore/Sources/TelegramEngine/Auth/CancelAccountReset.swift similarity index 85% rename from submodules/TelegramCore/Sources/CancelAccountReset.swift rename to submodules/TelegramCore/Sources/TelegramEngine/Auth/CancelAccountReset.swift index ef3215c26c..60e1b8e0ca 100644 --- a/submodules/TelegramCore/Sources/CancelAccountReset.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Auth/CancelAccountReset.swift @@ -18,7 +18,7 @@ public enum RequestCancelAccountResetDataError { case generic } -public func requestCancelAccountResetData(network: Network, hash: String) -> Signal { +func _internal_requestCancelAccountResetData(network: Network, hash: String) -> Signal { return network.request(Api.functions.account.sendConfirmPhoneCode(hash: hash, settings: .codeSettings(flags: 0)), automaticFloodWait: false) |> mapError { error -> RequestCancelAccountResetDataError in if error.errorDescription.hasPrefix("FLOOD_WAIT") { @@ -39,7 +39,7 @@ public func requestCancelAccountResetData(network: Network, hash: String) -> Sig } } -public func requestNextCancelAccountResetOption(network: Network, phoneNumber: String, phoneCodeHash: String) -> Signal { +func _internal_requestNextCancelAccountResetOption(network: Network, phoneNumber: String, phoneCodeHash: String) -> Signal { return network.request(Api.functions.auth.resendCode(phoneNumber: phoneNumber, phoneCodeHash: phoneCodeHash), automaticFloodWait: false) |> mapError { error -> RequestCancelAccountResetDataError in if error.errorDescription.hasPrefix("FLOOD_WAIT") { @@ -67,7 +67,7 @@ public enum CancelAccountResetError { case limitExceeded } -public func requestCancelAccountReset(network: Network, phoneCodeHash: String, phoneCode: String) -> Signal { +func _internal_requestCancelAccountReset(network: Network, phoneCodeHash: String, phoneCode: String) -> Signal { return network.request(Api.functions.account.confirmPhone(phoneCodeHash: phoneCodeHash, phoneCode: phoneCode)) |> mapError { error -> CancelAccountResetError in if error.errorDescription.hasPrefix("FLOOD_WAIT") { diff --git a/submodules/TelegramCore/Sources/ConfirmTwoStepRecoveryEmail.swift b/submodules/TelegramCore/Sources/TelegramEngine/Auth/ConfirmTwoStepRecoveryEmail.swift similarity index 80% rename from submodules/TelegramCore/Sources/ConfirmTwoStepRecoveryEmail.swift rename to submodules/TelegramCore/Sources/TelegramEngine/Auth/ConfirmTwoStepRecoveryEmail.swift index dcc5c4433f..240ae7f8a0 100644 --- a/submodules/TelegramCore/Sources/ConfirmTwoStepRecoveryEmail.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Auth/ConfirmTwoStepRecoveryEmail.swift @@ -12,7 +12,7 @@ public enum ConfirmTwoStepRecoveryEmailError { case generic } -public func confirmTwoStepRecoveryEmail(network: Network, code: String) -> Signal { +func _internal_confirmTwoStepRecoveryEmail(network: Network, code: String) -> Signal { return network.request(Api.functions.account.confirmPasswordEmail(code: code), automaticFloodWait: false) |> mapError { error -> ConfirmTwoStepRecoveryEmailError in if error.errorDescription == "EMAIL_INVALID" { @@ -34,7 +34,7 @@ public enum ResendTwoStepRecoveryEmailError { case generic } -public func resendTwoStepRecoveryEmail(network: Network) -> Signal { +func _internal_resendTwoStepRecoveryEmail(network: Network) -> Signal { return network.request(Api.functions.account.resendPasswordEmail(), automaticFloodWait: false) |> mapError { error -> ResendTwoStepRecoveryEmailError in if error.errorDescription.hasPrefix("FLOOD_WAIT") { @@ -49,7 +49,7 @@ public enum CancelTwoStepRecoveryEmailError { case generic } -public func cancelTwoStepRecoveryEmail(network: Network) -> Signal { +func _internal_cancelTwoStepRecoveryEmail(network: Network) -> Signal { return network.request(Api.functions.account.cancelPasswordEmail(), automaticFloodWait: false) |> mapError { _ -> CancelTwoStepRecoveryEmailError in return .generic diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Auth/TelegramEngineAuth.swift b/submodules/TelegramCore/Sources/TelegramEngine/Auth/TelegramEngineAuth.swift new file mode 100644 index 0000000000..c0f69f9d95 --- /dev/null +++ b/submodules/TelegramCore/Sources/TelegramEngine/Auth/TelegramEngineAuth.swift @@ -0,0 +1,195 @@ +import SwiftSignalKit +import Postbox +import TelegramApi +import MtProtoKit +import SyncCore + +public extension TelegramEngineUnauthorized { + final class Auth { + private let account: UnauthorizedAccount + + init(account: UnauthorizedAccount) { + self.account = account + } + + public func exportAuthTransferToken(accountManager: AccountManager, otherAccountUserIds: [PeerId.Id], syncContacts: Bool) -> Signal { + return _internal_exportAuthTransferToken(accountManager: accountManager, account: self.account, otherAccountUserIds: otherAccountUserIds, syncContacts: syncContacts) + } + + public func twoStepAuthData() -> Signal { + return _internal_twoStepAuthData(self.account.network) + } + + public func updateTwoStepVerificationPassword(currentPassword: String?, updatedPassword: UpdatedTwoStepVerificationPassword) -> Signal { + return _internal_updateTwoStepVerificationPassword(network: self.account.network, currentPassword: currentPassword, updatedPassword: updatedPassword) + } + + public func requestTwoStepVerificationPasswordRecoveryCode() -> Signal { + return _internal_requestTwoStepVerificationPasswordRecoveryCode(network: self.account.network) + } + + public func checkPasswordRecoveryCode(code: String) -> Signal { + return _internal_checkPasswordRecoveryCode(network: self.account.network, code: code) + } + + public func performPasswordRecovery(code: String, updatedPassword: UpdatedTwoStepVerificationPassword) -> Signal { + return _internal_performPasswordRecovery(network: self.account.network, code: code, updatedPassword: updatedPassword) + } + + public func resendTwoStepRecoveryEmail() -> Signal { + return _internal_resendTwoStepRecoveryEmail(network: self.account.network) + } + + public func uploadedPeerVideo(resource: MediaResource) -> Signal { + return _internal_uploadedPeerVideo(postbox: self.account.postbox, network: self.account.network, messageMediaPreuploadManager: nil, resource: resource) + } + } +} + +public enum DeleteAccountError { + case generic +} + +public extension TelegramEngine { + final class Auth { + private let account: Account + + init(account: Account) { + self.account = account + } + + public func twoStepAuthData() -> Signal { + return _internal_twoStepAuthData(self.account.network) + } + + public func updateTwoStepVerificationPassword(currentPassword: String?, updatedPassword: UpdatedTwoStepVerificationPassword) -> Signal { + return _internal_updateTwoStepVerificationPassword(network: self.account.network, currentPassword: currentPassword, updatedPassword: updatedPassword) + } + + public func deleteAccount() -> Signal { + return self.account.network.request(Api.functions.account.deleteAccount(reason: "GDPR")) + |> mapError { _ -> DeleteAccountError in + return .generic + } + |> ignoreValues + } + + public func updateTwoStepVerificationEmail(currentPassword: String, updatedEmail: String) -> Signal { + return _internal_updateTwoStepVerificationEmail(network: self.account.network, currentPassword: currentPassword, updatedEmail: updatedEmail) + } + + public func confirmTwoStepRecoveryEmail(code: String) -> Signal { + return _internal_confirmTwoStepRecoveryEmail(network: self.account.network, code: code) + } + + public func resendTwoStepRecoveryEmail() -> Signal { + return _internal_resendTwoStepRecoveryEmail(network: self.account.network) + } + + public func cancelTwoStepRecoveryEmail() -> Signal { + return _internal_cancelTwoStepRecoveryEmail(network: self.account.network) + } + + public func twoStepVerificationConfiguration() -> Signal { + return _internal_twoStepVerificationConfiguration(account: self.account) + } + + public func requestTwoStepVerifiationSettings(password: String) -> Signal { + return _internal_requestTwoStepVerifiationSettings(network: self.account.network, password: password) + } + + public func requestTwoStepVerificationPasswordRecoveryCode() -> Signal { + return _internal_requestTwoStepVerificationPasswordRecoveryCode(network: self.account.network) + } + + public func performPasswordRecovery(code: String, updatedPassword: UpdatedTwoStepVerificationPassword) -> Signal { + return _internal_performPasswordRecovery(network: self.account.network, code: code, updatedPassword: updatedPassword) + } + + public func cachedTwoStepPasswordToken() -> Signal { + return _internal_cachedTwoStepPasswordToken(postbox: self.account.postbox) + } + + public func cacheTwoStepPasswordToken(token: TemporaryTwoStepPasswordToken?) -> Signal { + return _internal_cacheTwoStepPasswordToken(postbox: self.account.postbox, token: token) + } + + public func requestTemporaryTwoStepPasswordToken(password: String, period: Int32, requiresBiometrics: Bool) -> Signal { + return _internal_requestTemporaryTwoStepPasswordToken(account: self.account, password: password, period: period, requiresBiometrics: requiresBiometrics) + } + + public func checkPasswordRecoveryCode(code: String) -> Signal { + return _internal_checkPasswordRecoveryCode(network: self.account.network, code: code) + } + + public func requestTwoStepPasswordReset() -> Signal { + return _internal_requestTwoStepPasswordReset(network: self.account.network) + } + + public func declineTwoStepPasswordReset() -> Signal { + return _internal_declineTwoStepPasswordReset(network: self.account.network) + } + + public func requestCancelAccountResetData(hash: String) -> Signal { + return _internal_requestCancelAccountResetData(network: self.account.network, hash: hash) + } + + public func requestNextCancelAccountResetOption(phoneNumber: String, phoneCodeHash: String) -> Signal { + return _internal_requestNextCancelAccountResetOption(network: self.account.network, phoneNumber: phoneNumber, phoneCodeHash: phoneCodeHash) + } + + public func requestCancelAccountReset(phoneCodeHash: String, phoneCode: String) -> Signal { + return _internal_requestCancelAccountReset(network: self.account.network, phoneCodeHash: phoneCodeHash, phoneCode: phoneCode) + } + } +} + +public extension SomeTelegramEngine { + final class Auth { + private let engine: SomeTelegramEngine + + init(engine: SomeTelegramEngine) { + self.engine = engine + } + + public func twoStepAuthData() -> Signal { + switch self.engine { + case let .authorized(engine): + return engine.auth.twoStepAuthData() + case let .unauthorized(engine): + return engine.auth.twoStepAuthData() + } + } + + public func updateTwoStepVerificationPassword(currentPassword: String?, updatedPassword: UpdatedTwoStepVerificationPassword) -> Signal { + switch self.engine { + case let .authorized(engine): + return engine.auth.updateTwoStepVerificationPassword(currentPassword: currentPassword, updatedPassword: updatedPassword) + case let .unauthorized(engine): + return engine.auth.updateTwoStepVerificationPassword(currentPassword: currentPassword, updatedPassword: updatedPassword) + } + } + + public func requestTwoStepVerificationPasswordRecoveryCode() -> Signal { + switch self.engine { + case let .authorized(engine): + return engine.auth.requestTwoStepVerificationPasswordRecoveryCode() + case let .unauthorized(engine): + return engine.auth.requestTwoStepVerificationPasswordRecoveryCode() + } + } + + public func checkPasswordRecoveryCode(code: String) -> Signal { + switch self.engine { + case let .authorized(engine): + return engine.auth.checkPasswordRecoveryCode(code: code) + case let .unauthorized(engine): + return engine.auth.checkPasswordRecoveryCode(code: code) + } + } + } + + var auth: Auth { + return Auth(engine: self) + } +} diff --git a/submodules/TelegramCore/Sources/TwoStepVerification.swift b/submodules/TelegramCore/Sources/TelegramEngine/Auth/TwoStepVerification.swift similarity index 78% rename from submodules/TelegramCore/Sources/TwoStepVerification.swift rename to submodules/TelegramCore/Sources/TelegramEngine/Auth/TwoStepVerification.swift index 286db5bae4..2ac72ff952 100644 --- a/submodules/TelegramCore/Sources/TwoStepVerification.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Auth/TwoStepVerification.swift @@ -8,17 +8,17 @@ import SyncCore public enum TwoStepVerificationConfiguration { case notSet(pendingEmail: TwoStepVerificationPendingEmail?) - case set(hint: String, hasRecoveryEmail: Bool, pendingEmail: TwoStepVerificationPendingEmail?, hasSecureValues: Bool) + case set(hint: String, hasRecoveryEmail: Bool, pendingEmail: TwoStepVerificationPendingEmail?, hasSecureValues: Bool, pendingResetTimestamp: Int32?) } -public func twoStepVerificationConfiguration(account: Account) -> Signal { +func _internal_twoStepVerificationConfiguration(account: Account) -> Signal { return account.network.request(Api.functions.account.getPassword()) |> retryRequest |> map { result -> TwoStepVerificationConfiguration in switch result { case let .password(password): if password.currentAlgo != nil { - return .set(hint: password.hint ?? "", hasRecoveryEmail: (password.flags & (1 << 0)) != 0, pendingEmail: password.emailUnconfirmedPattern.flatMap({ TwoStepVerificationPendingEmail(pattern: $0, codeLength: nil) }), hasSecureValues: (password.flags & (1 << 1)) != 0) + return .set(hint: password.hint ?? "", hasRecoveryEmail: (password.flags & (1 << 0)) != 0, pendingEmail: password.emailUnconfirmedPattern.flatMap({ TwoStepVerificationPendingEmail(pattern: $0, codeLength: nil) }), hasSecureValues: (password.flags & (1 << 1)) != 0, pendingResetTimestamp: password.pendingResetDate) } else { return .notSet(pendingEmail: password.emailUnconfirmedPattern.flatMap({ TwoStepVerificationPendingEmail(pattern: $0, codeLength: nil) })) } @@ -37,8 +37,8 @@ public struct TwoStepVerificationSettings { public let secureSecret: TwoStepVerificationSecureSecret? } -public func requestTwoStepVerifiationSettings(network: Network, password: String) -> Signal { - return twoStepAuthData(network) +func _internal_requestTwoStepVerifiationSettings(network: Network, password: String) -> Signal { + return _internal_twoStepAuthData(network) |> mapError { error -> AuthorizationPasswordVerificationError in if error.errorDescription.hasPrefix("FLOOD_WAIT") { return .limitExceeded @@ -111,14 +111,14 @@ public enum UpdatedTwoStepVerificationPassword { case password(password: String, hint: String, email: String?) } -public func updateTwoStepVerificationPassword(network: Network, currentPassword: String?, updatedPassword: UpdatedTwoStepVerificationPassword) -> Signal { - return twoStepAuthData(network) +func _internal_updateTwoStepVerificationPassword(network: Network, currentPassword: String?, updatedPassword: UpdatedTwoStepVerificationPassword) -> Signal { + return _internal_twoStepAuthData(network) |> mapError { _ -> UpdateTwoStepVerificationPasswordError in return .generic } |> mapToSignal { authData -> Signal in if let _ = authData.currentPasswordDerivation { - return requestTwoStepVerifiationSettings(network: network, password: currentPassword ?? "") + return _internal_requestTwoStepVerifiationSettings(network: network, password: currentPassword ?? "") |> mapError { _ -> UpdateTwoStepVerificationPasswordError in return .generic } @@ -130,7 +130,7 @@ public func updateTwoStepVerificationPassword(network: Network, currentPassword: } } |> mapToSignal { secureSecret -> Signal<(TwoStepAuthData, TwoStepVerificationSecureSecret?), UpdateTwoStepVerificationPasswordError> in - return twoStepAuthData(network) + return _internal_twoStepAuthData(network) |> mapError { _ -> UpdateTwoStepVerificationPasswordError in return .generic } @@ -205,7 +205,7 @@ public func updateTwoStepVerificationPassword(network: Network, currentPassword: codeLength = value } } - return twoStepAuthData(network) + return _internal_twoStepAuthData(network) |> map { result -> UpdateTwoStepVerificationPasswordResult in return .password(password: password, pendingEmail: result.unconfirmedEmailPattern.flatMap({ TwoStepVerificationPendingEmail(pattern: $0, codeLength: codeLength) })) } @@ -233,7 +233,7 @@ enum UpdateTwoStepVerificationSecureSecretError { } func updateTwoStepVerificationSecureSecret(network: Network, password: String, secret: Data) -> Signal { - return twoStepAuthData(network) + return _internal_twoStepAuthData(network) |> mapError { _ -> UpdateTwoStepVerificationSecureSecretError in return .generic } @@ -263,8 +263,8 @@ func updateTwoStepVerificationSecureSecret(network: Network, password: String, s } } -public func updateTwoStepVerificationEmail(network: Network, currentPassword: String, updatedEmail: String) -> Signal { - return twoStepAuthData(network) +func _internal_updateTwoStepVerificationEmail(network: Network, currentPassword: String, updatedEmail: String) -> Signal { + return _internal_twoStepAuthData(network) |> mapError { _ -> UpdateTwoStepVerificationPasswordError in return .generic } @@ -286,7 +286,7 @@ public func updateTwoStepVerificationEmail(network: Network, currentPassword: St } |> `catch` { error -> Signal in if error.errorDescription.hasPrefix("EMAIL_UNCONFIRMED") { - return twoStepAuthData(network) + return _internal_twoStepAuthData(network) |> map { result -> UpdateTwoStepVerificationPasswordResult in var codeLength: Int32? if error.errorDescription.hasPrefix("EMAIL_UNCONFIRMED_") { @@ -312,19 +312,26 @@ public func updateTwoStepVerificationEmail(network: Network, currentPassword: St public enum RequestTwoStepVerificationPasswordRecoveryCodeError { case generic + case limitExceeded } -public func requestTwoStepVerificationPasswordRecoveryCode(network: Network) -> Signal { +func _internal_requestTwoStepVerificationPasswordRecoveryCode(network: Network) -> Signal { return network.request(Api.functions.auth.requestPasswordRecovery(), automaticFloodWait: false) - |> mapError { _ -> RequestTwoStepVerificationPasswordRecoveryCodeError in + |> mapError { error -> RequestTwoStepVerificationPasswordRecoveryCodeError in + if error.errorDescription.hasPrefix("FLOOD_WAIT") { + return .limitExceeded + } else if error.errorDescription.hasPrefix("PASSWORD_RECOVERY_NA") { + return .generic + } else { return .generic } - |> map { result -> String in - switch result { - case let .passwordRecovery(emailPattern): - return emailPattern - } + } + |> map { result -> String in + switch result { + case let .passwordRecovery(emailPattern): + return emailPattern } + } } public enum RecoverTwoStepVerificationPasswordError { @@ -334,25 +341,7 @@ public enum RecoverTwoStepVerificationPasswordError { case invalidCode } -public func recoverTwoStepVerificationPassword(network: Network, code: String) -> Signal { - return network.request(Api.functions.auth.recoverPassword(code: code), automaticFloodWait: false) - |> mapError { error -> RecoverTwoStepVerificationPasswordError in - if error.errorDescription.hasPrefix("FLOOD_WAIT_") { - return .limitExceeded - } else if error.errorDescription == "PASSWORD_RECOVERY_EXPIRED" { - return .codeExpired - } else if error.errorDescription == "CODE_INVALID" { - return .invalidCode - } else { - return .generic - } - } - |> mapToSignal { _ -> Signal in - return .complete() - } -} - -public func cachedTwoStepPasswordToken(postbox: Postbox) -> Signal { +func _internal_cachedTwoStepPasswordToken(postbox: Postbox) -> Signal { return postbox.transaction { transaction -> TemporaryTwoStepPasswordToken? in let key = ValueBoxKey(length: 1) key.setUInt8(0, value: 0) @@ -360,7 +349,7 @@ public func cachedTwoStepPasswordToken(postbox: Postbox) -> Signal Signal { +func _internal_cacheTwoStepPasswordToken(postbox: Postbox, token: TemporaryTwoStepPasswordToken?) -> Signal { return postbox.transaction { transaction -> Void in let key = ValueBoxKey(length: 1) key.setUInt8(0, value: 0) @@ -372,8 +361,8 @@ public func cacheTwoStepPasswordToken(postbox: Postbox, token: TemporaryTwoStepP } } -public func requestTemporaryTwoStepPasswordToken(account: Account, password: String, period: Int32, requiresBiometrics: Bool) -> Signal { - return twoStepAuthData(account.network) +func _internal_requestTemporaryTwoStepPasswordToken(account: Account, password: String, period: Int32, requiresBiometrics: Bool) -> Signal { + return _internal_twoStepAuthData(account.network) |> mapToSignal { authData -> Signal in guard let currentPasswordDerivation = authData.currentPasswordDerivation, let srpSessionData = authData.srpSessionData else { return .fail(MTRpcError(errorCode: 400, errorDescription: "NO_PASSWORD")) @@ -402,3 +391,60 @@ public func requestTemporaryTwoStepPasswordToken(account: Account, password: Str } } } + +public enum RequestTwoStepPasswordResetResult { + public enum ErrorReason { + case generic + case limitExceeded(retryAtTimestamp: Int32?) + } + + case done + case waitingForReset(resetAtTimestamp: Int32) + case declined + case error(reason: ErrorReason) +} + +func _internal_requestTwoStepPasswordReset(network: Network) -> Signal { + return network.request(Api.functions.account.resetPassword(), automaticFloodWait: false) + |> map { result -> RequestTwoStepPasswordResetResult in + switch result { + case let .resetPasswordFailedWait(retryDate): + return .error(reason: .limitExceeded(retryAtTimestamp: retryDate)) + case .resetPasswordOk: + return .done + case let .resetPasswordRequestedWait(untilDate): + return .waitingForReset(resetAtTimestamp: untilDate) + } + } + |> `catch` { error -> Signal in + if error.errorDescription.hasPrefix("FLOOD_WAIT") { + return .single(.error(reason: .limitExceeded(retryAtTimestamp: nil))) + } else if error.errorDescription.hasPrefix("RESET_WAIT_") { + if let remainingSeconds = Int32(error.errorDescription[error.errorDescription.index(error.errorDescription.startIndex, offsetBy: "RESET_WAIT_".count)...]) { + let timestamp = Int32(network.globalTime) + return .single(.waitingForReset(resetAtTimestamp: timestamp + remainingSeconds)) + } else { + return .single(.error(reason: .generic)) + } + } else if error.errorDescription.hasPrefix("RESET_PREVIOUS_WAIT_") { + if let remainingSeconds = Int32(error.errorDescription[error.errorDescription.index(error.errorDescription.startIndex, offsetBy: "RESET_PREVIOUS_WAIT_".count)...]) { + let timestamp = Int32(network.globalTime) + return .single(.waitingForReset(resetAtTimestamp: timestamp + remainingSeconds)) + } else { + return .single(.error(reason: .generic)) + } + } else if error.errorDescription == "RESET_PREVIOUS_DECLINE" { + return .single(.declined) + } else { + return .single(.error(reason: .generic)) + } + } +} + +func _internal_declineTwoStepPasswordReset(network: Network) -> Signal { + return network.request(Api.functions.account.declinePasswordReset()) + |> `catch` { _ -> Signal in + return .single(.boolFalse) + } + |> ignoreValues +} diff --git a/submodules/TelegramCore/Sources/GroupCalls.swift b/submodules/TelegramCore/Sources/TelegramEngine/Calls/GroupCalls.swift similarity index 64% rename from submodules/TelegramCore/Sources/GroupCalls.swift rename to submodules/TelegramCore/Sources/TelegramEngine/Calls/GroupCalls.swift index 9b7eec5c9e..5bf3a438e2 100644 --- a/submodules/TelegramCore/Sources/GroupCalls.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Calls/GroupCalls.swift @@ -8,27 +8,42 @@ public struct GroupCallInfo: Equatable { public var id: Int64 public var accessHash: Int64 public var participantCount: Int - public var clientParams: String? public var streamDcId: Int32? public var title: String? + public var scheduleTimestamp: Int32? + public var subscribedToScheduled: Bool public var recordingStartTimestamp: Int32? + public var sortAscending: Bool + public var defaultParticipantsAreMuted: GroupCallParticipantsContext.State.DefaultParticipantsAreMuted? + public var isVideoEnabled: Bool + public var unmutedVideoLimit: Int public init( id: Int64, accessHash: Int64, participantCount: Int, - clientParams: String?, streamDcId: Int32?, title: String?, - recordingStartTimestamp: Int32? + scheduleTimestamp: Int32?, + subscribedToScheduled: Bool, + recordingStartTimestamp: Int32?, + sortAscending: Bool, + defaultParticipantsAreMuted: GroupCallParticipantsContext.State.DefaultParticipantsAreMuted?, + isVideoEnabled: Bool, + unmutedVideoLimit: Int ) { self.id = id self.accessHash = accessHash self.participantCount = participantCount - self.clientParams = clientParams self.streamDcId = streamDcId self.title = title + self.scheduleTimestamp = scheduleTimestamp + self.subscribedToScheduled = subscribedToScheduled self.recordingStartTimestamp = recordingStartTimestamp + self.sortAscending = sortAscending + self.defaultParticipantsAreMuted = defaultParticipantsAreMuted + self.isVideoEnabled = isVideoEnabled + self.unmutedVideoLimit = unmutedVideoLimit } } @@ -40,22 +55,20 @@ public struct GroupCallSummary: Equatable { extension GroupCallInfo { init?(_ call: Api.GroupCall) { switch call { - case let .groupCall(_, id, accessHash, participantCount, params, title, streamDcId, recordStartDate, _): - var clientParams: String? - if let params = params { - switch params { - case let .dataJSON(data): - clientParams = data - } - } + case let .groupCall(flags, id, accessHash, participantsCount, title, streamDcId, recordStartDate, scheduleDate, _, unmutedVideoLimit, _): self.init( id: id, accessHash: accessHash, - participantCount: Int(participantCount), - clientParams: clientParams, + participantCount: Int(participantsCount), streamDcId: streamDcId, title: title, - recordingStartTimestamp: recordStartDate + scheduleTimestamp: scheduleDate, + subscribedToScheduled: (flags & (1 << 8)) != 0, + recordingStartTimestamp: recordStartDate, + sortAscending: (flags & (1 << 6)) != 0, + defaultParticipantsAreMuted: GroupCallParticipantsContext.State.DefaultParticipantsAreMuted(isMuted: (flags & (1 << 1)) != 0, canChange: (flags & (1 << 2)) != 0), + isVideoEnabled: (flags & (1 << 9)) != 0, + unmutedVideoLimit: Int(unmutedVideoLimit) ) case .groupCallDiscarded: return nil @@ -67,7 +80,7 @@ public enum GetCurrentGroupCallError { case generic } -public func getCurrentGroupCall(account: Account, callId: Int64, accessHash: Int64) -> Signal { +func _internal_getCurrentGroupCall(account: Account, callId: Int64, accessHash: Int64, peerId: PeerId? = nil) -> Signal { return account.network.request(Api.functions.phone.getGroupCall(call: .inputGroupCall(id: callId, accessHash: accessHash))) |> mapError { _ -> GetCurrentGroupCallError in return .generic @@ -96,63 +109,24 @@ public func getCurrentGroupCall(account: Account, callId: Int64, accessHash: Int peers.append(peer) } } + if let peerId = peerId { + transaction.updatePeerCachedData(peerIds: [peerId], update: { _, current in + if let cachedData = current as? CachedChannelData { + return cachedData.withUpdatedActiveCall(CachedChannelData.ActiveCall.init(id: info.id, accessHash: info.accessHash, title: info.title, scheduleTimestamp: info.scheduleTimestamp, subscribedToScheduled: cachedData.activeCall?.subscribedToScheduled ?? false)) + } else if let cachedData = current as? CachedGroupData { + return cachedData.withUpdatedActiveCall(CachedChannelData.ActiveCall(id: info.id, accessHash: info.accessHash, title: info.title, scheduleTimestamp: info.scheduleTimestamp, subscribedToScheduled: cachedData.activeCall?.subscribedToScheduled ?? false)) + } else { + return current + } + }) + } updatePeers(transaction: transaction, peers: peers, update: { _, updated -> Peer in return updated }) updatePeerPresences(transaction: transaction, accountPeerId: account.peerId, peerPresences: peerPresences) - var parsedParticipants: [GroupCallParticipantsContext.Participant] = [] - - loop: for participant in participants { - switch participant { - case let .groupCallParticipant(flags, apiPeerId, date, activeDate, source, volume, about, raiseHandRating): - let peerId: PeerId - switch apiPeerId { - case let .peerUser(userId): - peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: userId) - case let .peerChat(chatId): - peerId = PeerId(namespace: Namespaces.Peer.CloudGroup, id: chatId) - case let .peerChannel(channelId): - peerId = PeerId(namespace: Namespaces.Peer.CloudChannel, id: channelId) - } - - let ssrc = UInt32(bitPattern: source) - guard let peer = transaction.getPeer(peerId) else { - continue loop - } - let muted = (flags & (1 << 0)) != 0 - let mutedByYou = (flags & (1 << 9)) != 0 - var muteState: GroupCallParticipantsContext.Participant.MuteState? - if muted { - let canUnmute = (flags & (1 << 2)) != 0 - muteState = GroupCallParticipantsContext.Participant.MuteState(canUnmute: canUnmute, mutedByYou: mutedByYou) - } else if mutedByYou { - muteState = GroupCallParticipantsContext.Participant.MuteState(canUnmute: false, mutedByYou: mutedByYou) - } - let jsonParams: String? = nil - /*if let params = params { - switch params { - case let .dataJSON(data): - jsonParams = data - } - }*/ - parsedParticipants.append(GroupCallParticipantsContext.Participant( - peer: peer, - ssrc: ssrc, - jsonParams: jsonParams, - joinTimestamp: date, - raiseHandRating: raiseHandRating, - hasRaiseHand: raiseHandRating != nil, - activityTimestamp: activeDate.flatMap(Double.init), - activityRank: nil, - muteState: muteState, - volume: volume, - about: about - )) - } - } - + let parsedParticipants = participants.compactMap { GroupCallParticipantsContext.Participant($0, transaction: transaction) } return GroupCallSummary( info: info, topParticipants: parsedParticipants @@ -167,9 +141,10 @@ public func getCurrentGroupCall(account: Account, callId: Int64, accessHash: Int public enum CreateGroupCallError { case generic case anonymousNotAllowed + case scheduledTooLate } -public func createGroupCall(account: Account, peerId: PeerId) -> Signal { +func _internal_createGroupCall(account: Account, peerId: PeerId, title: String?, scheduleDate: Int32?) -> Signal { return account.postbox.transaction { transaction -> Api.InputPeer? in let callPeer = transaction.getPeer(peerId).flatMap(apiInputPeer) return callPeer @@ -179,11 +154,19 @@ public func createGroupCall(account: Account, peerId: PeerId) -> Signal mapError { error -> CreateGroupCallError in if error.errorDescription == "ANONYMOUS_CALLS_DISABLED" { return .anonymousNotAllowed + } else if error.errorDescription == "SCHEDULE_DATE_TOO_LATE" { + return .scheduledTooLate } return .generic } @@ -206,9 +189,9 @@ public func createGroupCall(account: Account, peerId: PeerId) -> Signal GroupCallInfo in transaction.updatePeerCachedData(peerIds: Set([peerId]), update: { _, cachedData -> CachedPeerData? in if let cachedData = cachedData as? CachedChannelData { - return cachedData.withUpdatedActiveCall(CachedChannelData.ActiveCall(id: callInfo.id, accessHash: callInfo.accessHash, title: callInfo.title)) + return cachedData.withUpdatedActiveCall(CachedChannelData.ActiveCall(id: callInfo.id, accessHash: callInfo.accessHash, title: callInfo.title, scheduleTimestamp: callInfo.scheduleTimestamp, subscribedToScheduled: callInfo.subscribedToScheduled)) } else if let cachedData = cachedData as? CachedGroupData { - return cachedData.withUpdatedActiveCall(CachedChannelData.ActiveCall(id: callInfo.id, accessHash: callInfo.accessHash, title: callInfo.title)) + return cachedData.withUpdatedActiveCall(CachedChannelData.ActiveCall(id: callInfo.id, accessHash: callInfo.accessHash, title: callInfo.title, scheduleTimestamp: callInfo.scheduleTimestamp, subscribedToScheduled: callInfo.subscribedToScheduled)) } else { return cachedData } @@ -223,22 +206,165 @@ public func createGroupCall(account: Account, peerId: PeerId) -> Signal Signal { + return account.network.request(Api.functions.phone.startScheduledGroupCall(call: .inputGroupCall(id: callId, accessHash: accessHash))) + |> mapError { error -> StartScheduledGroupCallError in + return .generic + } + |> mapToSignal { result -> Signal in + var parsedCall: GroupCallInfo? + loop: for update in result.allUpdates { + switch update { + case let .updateGroupCall(_, call): + parsedCall = GroupCallInfo(call) + break loop + default: + break + } + } + + guard let callInfo = parsedCall else { + return .fail(.generic) + } + + return account.postbox.transaction { transaction -> GroupCallInfo in + transaction.updatePeerCachedData(peerIds: Set([peerId]), update: { _, cachedData -> CachedPeerData? in + if let cachedData = cachedData as? CachedChannelData { + return cachedData.withUpdatedActiveCall(CachedChannelData.ActiveCall(id: callInfo.id, accessHash: callInfo.accessHash, title: callInfo.title, scheduleTimestamp: nil, subscribedToScheduled: false)) + } else if let cachedData = cachedData as? CachedGroupData { + return cachedData.withUpdatedActiveCall(CachedChannelData.ActiveCall(id: callInfo.id, accessHash: callInfo.accessHash, title: callInfo.title, scheduleTimestamp: nil, subscribedToScheduled: false)) + } else { + return cachedData + } + }) + + account.stateManager.addUpdates(result) + + return callInfo + } + |> castError(StartScheduledGroupCallError.self) + } +} + +public enum ToggleScheduledGroupCallSubscriptionError { + case generic +} + +func _internal_toggleScheduledGroupCallSubscription(account: Account, peerId: PeerId, callId: Int64, accessHash: Int64, subscribe: Bool) -> Signal { + return account.network.request(Api.functions.phone.toggleGroupCallStartSubscription(call: .inputGroupCall(id: callId, accessHash: accessHash), subscribed: subscribe ? .boolTrue : .boolFalse)) + |> mapError { error -> ToggleScheduledGroupCallSubscriptionError in + return .generic + } + |> mapToSignal { result -> Signal in + var parsedCall: GroupCallInfo? + loop: for update in result.allUpdates { + switch update { + case let .updateGroupCall(_, call): + parsedCall = GroupCallInfo(call) + break loop + default: + break + } + } + + guard let callInfo = parsedCall else { + return .fail(.generic) + } + + return account.postbox.transaction { transaction in + transaction.updatePeerCachedData(peerIds: Set([peerId]), update: { _, cachedData -> CachedPeerData? in + if let cachedData = cachedData as? CachedChannelData { + return cachedData.withUpdatedActiveCall(CachedChannelData.ActiveCall(id: callInfo.id, accessHash: callInfo.accessHash, title: callInfo.title, scheduleTimestamp: callInfo.scheduleTimestamp, subscribedToScheduled: callInfo.subscribedToScheduled)) + } else if let cachedData = cachedData as? CachedGroupData { + return cachedData.withUpdatedActiveCall(CachedChannelData.ActiveCall(id: callInfo.id, accessHash: callInfo.accessHash, title: callInfo.title, scheduleTimestamp: callInfo.scheduleTimestamp, subscribedToScheduled: callInfo.subscribedToScheduled)) + } else { + return cachedData + } + }) + + account.stateManager.addUpdates(result) + } + |> castError(ToggleScheduledGroupCallSubscriptionError.self) + } +} + +public enum UpdateGroupCallJoinAsPeerError { + case generic +} + +func _internal_updateGroupCallJoinAsPeer(account: Account, peerId: PeerId, joinAs: PeerId) -> Signal { + return account.postbox.transaction { transaction -> (Api.InputPeer, Api.InputPeer)? in + if let peer = transaction.getPeer(peerId), let joinAsPeer = transaction.getPeer(joinAs), let inputPeer = apiInputPeer(peer), let joinInputPeer = apiInputPeer(joinAsPeer) { + return (inputPeer, joinInputPeer) + } else { + return nil + } + } + |> castError(UpdateGroupCallJoinAsPeerError.self) + |> mapToSignal { result in + guard let (inputPeer, joinInputPeer) = result else { + return .fail(.generic) + } + return account.network.request(Api.functions.phone.saveDefaultGroupCallJoinAs(peer: inputPeer, joinAs: joinInputPeer)) + |> mapError { _ -> UpdateGroupCallJoinAsPeerError in + return .generic + } + |> mapToSignal { result -> Signal in + return account.postbox.transaction { transaction in + transaction.updatePeerCachedData(peerIds: Set([peerId]), update: { _, cachedData -> CachedPeerData? in + if let cachedData = cachedData as? CachedChannelData { + return cachedData.withUpdatedCallJoinPeerId(joinAs) + } else if let cachedData = cachedData as? CachedGroupData { + return cachedData.withUpdatedCallJoinPeerId(joinAs) + } else { + return cachedData + } + }) + } + |> castError(UpdateGroupCallJoinAsPeerError.self) + |> ignoreValues + } + } +} + public enum GetGroupCallParticipantsError { case generic } -public func getGroupCallParticipants(account: Account, callId: Int64, accessHash: Int64, offset: String, ssrcs: [UInt32], limit: Int32) -> Signal { - return account.network.request(Api.functions.phone.getGroupParticipants(call: .inputGroupCall(id: callId, accessHash: accessHash), ids: [], sources: ssrcs.map { Int32(bitPattern: $0) }, offset: offset, limit: limit)) +func _internal_getGroupCallParticipants(account: Account, callId: Int64, accessHash: Int64, offset: String, ssrcs: [UInt32], limit: Int32, sortAscending: Bool?) -> Signal { + let sortAscendingValue: Signal<(Bool, Int32?, Bool, GroupCallParticipantsContext.State.DefaultParticipantsAreMuted?, Bool, Int), GetGroupCallParticipantsError> + + sortAscendingValue = _internal_getCurrentGroupCall(account: account, callId: callId, accessHash: accessHash) |> mapError { _ -> GetGroupCallParticipantsError in return .generic } - |> mapToSignal { result -> Signal in + |> mapToSignal { result -> Signal<(Bool, Int32?, Bool, GroupCallParticipantsContext.State.DefaultParticipantsAreMuted?, Bool, Int), GetGroupCallParticipantsError> in + guard let result = result else { + return .fail(.generic) + } + return .single((sortAscending ?? result.info.sortAscending, result.info.scheduleTimestamp, result.info.subscribedToScheduled, result.info.defaultParticipantsAreMuted, result.info.isVideoEnabled, result.info.unmutedVideoLimit)) + } + + return combineLatest( + account.network.request(Api.functions.phone.getGroupParticipants(call: .inputGroupCall(id: callId, accessHash: accessHash), ids: [], sources: ssrcs.map { Int32(bitPattern: $0) }, offset: offset, limit: limit)) + |> mapError { _ -> GetGroupCallParticipantsError in + return .generic + }, + sortAscendingValue + ) + |> mapToSignal { result, sortAscendingAndScheduleTimestamp -> Signal in return account.postbox.transaction { transaction -> GroupCallParticipantsContext.State in var parsedParticipants: [GroupCallParticipantsContext.Participant] = [] let totalCount: Int let version: Int32 let nextParticipantsFetchOffset: String? + let (sortAscendingValue, scheduleTimestamp, subscribedToScheduled, defaultParticipantsAreMuted, isVideoEnabled, unmutedVideoLimit) = sortAscendingAndScheduleTimestamp + switch result { case let .groupParticipants(count, participants, nextOffset, chats, users, apiVersion): totalCount = Int(count) @@ -272,66 +398,25 @@ public func getGroupCallParticipants(account: Account, callId: Int64, accessHash }) updatePeerPresences(transaction: transaction, accountPeerId: account.peerId, peerPresences: peerPresences) - loop: for participant in participants { - switch participant { - case let .groupCallParticipant(flags, apiPeerId, date, activeDate, source, volume, about, raiseHandRating): - let peerId: PeerId - switch apiPeerId { - case let .peerUser(userId): - peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: userId) - case let .peerChat(chatId): - peerId = PeerId(namespace: Namespaces.Peer.CloudGroup, id: chatId) - case let .peerChannel(channelId): - peerId = PeerId(namespace: Namespaces.Peer.CloudChannel, id: channelId) - } - let ssrc = UInt32(bitPattern: source) - guard let peer = transaction.getPeer(peerId) else { - continue loop - } - let muted = (flags & (1 << 0)) != 0 - let mutedByYou = (flags & (1 << 9)) != 0 - var muteState: GroupCallParticipantsContext.Participant.MuteState? - if muted { - let canUnmute = (flags & (1 << 2)) != 0 - muteState = GroupCallParticipantsContext.Participant.MuteState(canUnmute: canUnmute, mutedByYou: mutedByYou) - } else if mutedByYou { - muteState = GroupCallParticipantsContext.Participant.MuteState(canUnmute: false, mutedByYou: mutedByYou) - } - let jsonParams: String? = nil - /*if let params = params { - switch params { - case let .dataJSON(data): - jsonParams = data - } - }*/ - parsedParticipants.append(GroupCallParticipantsContext.Participant( - peer: peer, - ssrc: ssrc, - jsonParams: jsonParams, - joinTimestamp: date, - raiseHandRating: raiseHandRating, - hasRaiseHand: raiseHandRating != nil, - activityTimestamp: activeDate.flatMap(Double.init), - activityRank: nil, - muteState: muteState, - volume: volume, - about: about - )) - } - } + parsedParticipants = participants.compactMap { GroupCallParticipantsContext.Participant($0, transaction: transaction) } } - - parsedParticipants.sort() + + parsedParticipants.sort(by: { GroupCallParticipantsContext.Participant.compare(lhs: $0, rhs: $1, sortAscending: sortAscendingValue) }) return GroupCallParticipantsContext.State( participants: parsedParticipants, nextParticipantsFetchOffset: nextParticipantsFetchOffset, adminIds: Set(), isCreator: false, - defaultParticipantsAreMuted: GroupCallParticipantsContext.State.DefaultParticipantsAreMuted(isMuted: false, canChange: false), + defaultParticipantsAreMuted: defaultParticipantsAreMuted ?? GroupCallParticipantsContext.State.DefaultParticipantsAreMuted(isMuted: false, canChange: false), + sortAscending: sortAscendingValue, recordingStartTimestamp: nil, title: nil, + scheduleTimestamp: scheduleTimestamp, + subscribedToScheduled: subscribedToScheduled, totalCount: totalCount, + isVideoEnabled: isVideoEnabled, + unmutedVideoLimit: unmutedVideoLimit, version: version ) } @@ -343,6 +428,7 @@ public enum JoinGroupCallError { case generic case anonymousNotAllowed case tooManyParticipants + case invalidJoinAsPeer } public struct JoinGroupCallResult { @@ -354,9 +440,10 @@ public struct JoinGroupCallResult { public var callInfo: GroupCallInfo public var state: GroupCallParticipantsContext.State public var connectionMode: ConnectionMode + public var jsonParams: String } -public func joinGroupCall(account: Account, peerId: PeerId, joinAs: PeerId?, callId: Int64, accessHash: Int64, preferMuted: Bool, joinPayload: String, peerAdminIds: Signal<[PeerId], NoError>, inviteHash: String? = nil) -> Signal { +func _internal_joinGroupCall(account: Account, peerId: PeerId, joinAs: PeerId?, callId: Int64, accessHash: Int64, preferMuted: Bool, joinPayload: String, peerAdminIds: Signal<[PeerId], NoError>, inviteHash: String? = nil) -> Signal { return account.postbox.transaction { transaction -> Api.InputPeer? in if let joinAs = joinAs { return transaction.getPeer(joinAs).flatMap(apiInputPeer) @@ -374,21 +461,44 @@ public func joinGroupCall(account: Account, peerId: PeerId, joinAs: PeerId?, cal if preferMuted { flags |= (1 << 0) } + flags |= (1 << 2) if let _ = inviteHash { flags |= (1 << 1) } let joinRequest = account.network.request(Api.functions.phone.joinGroupCall(flags: flags, call: .inputGroupCall(id: callId, accessHash: accessHash), joinAs: inputJoinAs, inviteHash: inviteHash, params: .dataJSON(data: joinPayload))) - |> mapError { error -> JoinGroupCallError in + |> `catch` { error -> Signal in if error.errorDescription == "GROUPCALL_ANONYMOUS_FORBIDDEN" { - return .anonymousNotAllowed + return .fail(.anonymousNotAllowed) } else if error.errorDescription == "GROUPCALL_PARTICIPANTS_TOO_MUCH" { - return .tooManyParticipants + return .fail(.tooManyParticipants) + } else if error.errorDescription == "JOIN_AS_PEER_INVALID" { + return .fail(.invalidJoinAsPeer) + } else if error.errorDescription == "GROUPCALL_INVALID" { + return account.postbox.transaction { transaction -> Signal in + transaction.updatePeerCachedData(peerIds: Set([peerId]), update: { _, current in + if let current = current as? CachedGroupData { + if current.activeCall?.id == callId { + return current.withUpdatedActiveCall(nil) + } + } else if let current = current as? CachedChannelData { + if current.activeCall?.id == callId { + return current.withUpdatedActiveCall(nil) + } + } + return current + }) + + return .fail(.generic) + } + |> castError(JoinGroupCallError.self) + |> switchToLatest + } else { + return .fail(.generic) } - return .generic } - let getParticipantsRequest = getGroupCallParticipants(account: account, callId: callId, accessHash: accessHash, offset: "", ssrcs: [], limit: 100) + let getParticipantsRequest = _internal_getGroupCallParticipants(account: account, callId: callId, accessHash: accessHash, offset: "", ssrcs: [], limit: 100, sortAscending: true) |> mapError { _ -> JoinGroupCallError in return .generic } @@ -397,7 +507,7 @@ public func joinGroupCall(account: Account, peerId: PeerId, joinAs: PeerId?, cal joinRequest, getParticipantsRequest ) - |> mapToSignal { updates, participantsState -> Signal in + |> mapToSignal { updates, participantsState -> Signal in let peer = account.postbox.transaction { transaction -> Peer? in return transaction.getPeer(peerId) } @@ -426,31 +536,41 @@ public func joinGroupCall(account: Account, peerId: PeerId, joinAs: PeerId?, cal account.stateManager.addUpdates(updates) var maybeParsedCall: GroupCallInfo? + var maybeParsedClientParams: String? loop: for update in updates.allUpdates { switch update { case let .updateGroupCall(_, call): maybeParsedCall = GroupCallInfo(call) switch call { - case let .groupCall(flags, _, _, _, _, title, _, recordStartDate, _): + case let .groupCall(flags, _, _, _, title, _, recordStartDate, scheduleDate, _, unmutedVideoLimit, _): let isMuted = (flags & (1 << 1)) != 0 let canChange = (flags & (1 << 2)) != 0 + let isVideoEnabled = (flags & (1 << 9)) != 0 state.defaultParticipantsAreMuted = GroupCallParticipantsContext.State.DefaultParticipantsAreMuted(isMuted: isMuted, canChange: canChange) state.title = title state.recordingStartTimestamp = recordStartDate + state.scheduleTimestamp = scheduleDate + state.isVideoEnabled = isVideoEnabled + state.unmutedVideoLimit = Int(unmutedVideoLimit) default: break } - - break loop + case let .updateGroupCallConnection(_, params): + switch params { + case let .dataJSON(data): + maybeParsedClientParams = data + } default: break } } - guard let parsedCall = maybeParsedCall else { + guard let parsedCall = maybeParsedCall, let parsedClientParams = maybeParsedClientParams else { return .fail(.generic) } + + state.sortAscending = parsedCall.sortAscending let apiUsers: [Api.User] = [] @@ -468,7 +588,7 @@ public func joinGroupCall(account: Account, peerId: PeerId, joinAs: PeerId?, cal } let connectionMode: JoinGroupCallResult.ConnectionMode - if let clientParams = parsedCall.clientParams, let clientParamsData = clientParams.data(using: .utf8), let dict = (try? JSONSerialization.jsonObject(with: clientParamsData, options: [])) as? [String: Any] { + if let clientParamsData = parsedClientParams.data(using: .utf8), let dict = (try? JSONSerialization.jsonObject(with: clientParamsData, options: [])) as? [String: Any] { if let stream = dict["stream"] as? Bool, stream { connectionMode = .broadcast } else { @@ -481,9 +601,9 @@ public func joinGroupCall(account: Account, peerId: PeerId, joinAs: PeerId?, cal return account.postbox.transaction { transaction -> JoinGroupCallResult in transaction.updatePeerCachedData(peerIds: Set([peerId]), update: { _, cachedData -> CachedPeerData? in if let cachedData = cachedData as? CachedChannelData { - return cachedData.withUpdatedCallJoinPeerId(joinAs) + return cachedData.withUpdatedCallJoinPeerId(joinAs).withUpdatedActiveCall(CachedChannelData.ActiveCall(id: parsedCall.id, accessHash: parsedCall.accessHash, title: parsedCall.title, scheduleTimestamp: nil, subscribedToScheduled: false)) } else if let cachedData = cachedData as? CachedGroupData { - return cachedData.withUpdatedCallJoinPeerId(joinAs) + return cachedData.withUpdatedCallJoinPeerId(joinAs).withUpdatedActiveCall(CachedChannelData.ActiveCall(id: parsedCall.id, accessHash: parsedCall.accessHash, title: parsedCall.title, scheduleTimestamp: nil, subscribedToScheduled: false)) } else { return cachedData } @@ -496,16 +616,8 @@ public func joinGroupCall(account: Account, peerId: PeerId, joinAs: PeerId?, cal case let .updateGroupCallParticipants(_, participants, _): loop: for participant in participants { switch participant { - case let .groupCallParticipant(flags, apiPeerId, date, activeDate, source, volume, about, raiseHandRating): - let peerId: PeerId - switch apiPeerId { - case let .peerUser(userId): - peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: userId) - case let .peerChat(chatId): - peerId = PeerId(namespace: Namespaces.Peer.CloudGroup, id: chatId) - case let .peerChannel(channelId): - peerId = PeerId(namespace: Namespaces.Peer.CloudChannel, id: channelId) - } + case let .groupCallParticipant(flags, apiPeerId, date, activeDate, source, volume, about, raiseHandRating, video, presentation): + let peerId: PeerId = apiPeerId.peerId let ssrc = UInt32(bitPattern: source) guard let peer = transaction.getPeer(peerId) else { continue loop @@ -519,18 +631,19 @@ public func joinGroupCall(account: Account, peerId: PeerId, joinAs: PeerId?, cal } else if mutedByYou { muteState = GroupCallParticipantsContext.Participant.MuteState(canUnmute: false, mutedByYou: mutedByYou) } - let jsonParams: String? = nil - /*if let params = params { - switch params { - case let .dataJSON(data): - jsonParams = data - } - }*/ + var videoDescription = video.flatMap(GroupCallParticipantsContext.Participant.VideoDescription.init) + var presentationDescription = presentation.flatMap(GroupCallParticipantsContext.Participant.VideoDescription.init) + if muteState?.canUnmute == false { + videoDescription = nil + presentationDescription = nil + } + let joinedVideo = (flags & (1 << 15)) != 0 if !state.participants.contains(where: { $0.peer.id == peer.id }) { state.participants.append(GroupCallParticipantsContext.Participant( peer: peer, ssrc: ssrc, - jsonParams: jsonParams, + videoDescription: videoDescription, + presentationDescription: presentationDescription, joinTimestamp: date, raiseHandRating: raiseHandRating, hasRaiseHand: raiseHandRating != nil, @@ -538,7 +651,8 @@ public func joinGroupCall(account: Account, peerId: PeerId, joinAs: PeerId?, cal activityRank: nil, muteState: muteState, volume: volume, - about: about + about: about, + joinedVideo: joinedVideo )) } } @@ -548,7 +662,7 @@ public func joinGroupCall(account: Account, peerId: PeerId, joinAs: PeerId?, cal } } - state.participants.sort() + state.participants.sort(by: { GroupCallParticipantsContext.Participant.compare(lhs: $0, rhs: $1, sortAscending: state.sortAscending) }) updatePeers(transaction: transaction, peers: peers, update: { _, updated -> Peer in return updated @@ -558,21 +672,86 @@ public func joinGroupCall(account: Account, peerId: PeerId, joinAs: PeerId?, cal return JoinGroupCallResult( callInfo: parsedCall, state: state, - connectionMode: connectionMode + connectionMode: connectionMode, + jsonParams: parsedClientParams ) } |> castError(JoinGroupCallError.self) } } } - +} + +public struct JoinGroupCallAsScreencastResult { + public var jsonParams: String + public var endpointId: String +} + +func _internal_joinGroupCallAsScreencast(account: Account, peerId: PeerId, callId: Int64, accessHash: Int64, joinPayload: String) -> Signal { + return account.network.request(Api.functions.phone.joinGroupCallPresentation(call: .inputGroupCall(id: callId, accessHash: accessHash), params: .dataJSON(data: joinPayload))) + |> mapError { _ -> JoinGroupCallError in + return .generic + } + |> mapToSignal { updates -> Signal in + account.stateManager.addUpdates(updates) + + var maybeParsedClientParams: String? + loop: for update in updates.allUpdates { + switch update { + case let .updateGroupCallConnection(_, params): + switch params { + case let .dataJSON(data): + maybeParsedClientParams = data + } + default: + break + } + } + + guard let parsedClientParams = maybeParsedClientParams else { + return .fail(.generic) + } + + var maybeEndpointId: String? + + if let jsonData = parsedClientParams.data(using: .utf8), let json = try? JSONSerialization.jsonObject(with: jsonData, options: []) as? [String: Any] { + if let videoSection = json["video"] as? [String: Any] { + maybeEndpointId = videoSection["endpoint"] as? String + } + } + + guard let endpointId = maybeEndpointId else { + return .fail(.generic) + } + + return .single(JoinGroupCallAsScreencastResult( + jsonParams: parsedClientParams, + endpointId: endpointId + )) + } +} + +public enum LeaveGroupCallAsScreencastError { + case generic +} + +func _internal_leaveGroupCallAsScreencast(account: Account, callId: Int64, accessHash: Int64) -> Signal { + return account.network.request(Api.functions.phone.leaveGroupCallPresentation(call: .inputGroupCall(id: callId, accessHash: accessHash))) + |> mapError { _ -> LeaveGroupCallAsScreencastError in + return .generic + } + |> mapToSignal { updates -> Signal in + account.stateManager.addUpdates(updates) + + return .complete() + } } public enum LeaveGroupCallError { case generic } -public func leaveGroupCall(account: Account, callId: Int64, accessHash: Int64, source: UInt32) -> Signal { +func _internal_leaveGroupCall(account: Account, callId: Int64, accessHash: Int64, source: UInt32) -> Signal { return account.network.request(Api.functions.phone.leaveGroupCall(call: .inputGroupCall(id: callId, accessHash: accessHash), source: Int32(bitPattern: source))) |> mapError { _ -> LeaveGroupCallError in return .generic @@ -588,7 +767,7 @@ public enum StopGroupCallError { case generic } -public func stopGroupCall(account: Account, peerId: PeerId, callId: Int64, accessHash: Int64) -> Signal { +func _internal_stopGroupCall(account: Account, peerId: PeerId, callId: Int64, accessHash: Int64) -> Signal { return account.network.request(Api.functions.phone.discardGroupCall(call: .inputGroupCall(id: callId, accessHash: accessHash))) |> mapError { _ -> StopGroupCallError in return .generic @@ -630,26 +809,13 @@ public func stopGroupCall(account: Account, peerId: PeerId, callId: Int64, acces } } -public enum CheckGroupCallResult { - case success - case restart -} - -public func checkGroupCall(account: Account, callId: Int64, accessHash: Int64, ssrc: Int32) -> Signal { - return account.network.request(Api.functions.phone.checkGroupCall(call: .inputGroupCall(id: callId, accessHash: accessHash), source: ssrc)) - |> `catch` { _ -> Signal in - return .single(.boolFalse) +func _internal_checkGroupCall(account: Account, callId: Int64, accessHash: Int64, ssrcs: [UInt32]) -> Signal<[UInt32], NoError> { + return account.network.request(Api.functions.phone.checkGroupCall(call: .inputGroupCall(id: callId, accessHash: accessHash), sources: ssrcs.map(Int32.init(bitPattern:)))) + |> `catch` { _ -> Signal<[Int32], NoError> in + return .single([]) } - |> map { result -> CheckGroupCallResult in - #if DEBUG - //return .restart - #endif - switch result { - case .boolTrue: - return .success - case .boolFalse: - return .restart - } + |> map { result -> [UInt32] in + return result.map(UInt32.init(bitPattern:)) } } @@ -670,7 +836,7 @@ private func binaryInsertionIndex(_ inputArr: [GroupCallParticipantsContext.Part } public final class GroupCallParticipantsContext { - public struct Participant: Equatable, Comparable { + public struct Participant: Equatable, CustomStringConvertible { public struct MuteState: Equatable { public var canUnmute: Bool public var mutedByYou: Bool @@ -680,10 +846,23 @@ public final class GroupCallParticipantsContext { self.mutedByYou = mutedByYou } } + + public struct VideoDescription: Equatable { + public struct SsrcGroup: Equatable { + public var semantics: String + public var ssrcs: [UInt32] + } + + public var endpointId: String + public var ssrcGroups: [SsrcGroup] + public var audioSsrc: UInt32? + public var isPaused: Bool + } public var peer: Peer public var ssrc: UInt32? - public var jsonParams: String? + public var videoDescription: VideoDescription? + public var presentationDescription: VideoDescription? public var joinTimestamp: Int32 public var raiseHandRating: Int64? public var hasRaiseHand: Bool @@ -692,11 +871,13 @@ public final class GroupCallParticipantsContext { public var muteState: MuteState? public var volume: Int32? public var about: String? + public var joinedVideo: Bool public init( peer: Peer, ssrc: UInt32?, - jsonParams: String?, + videoDescription: VideoDescription?, + presentationDescription: VideoDescription?, joinTimestamp: Int32, raiseHandRating: Int64?, hasRaiseHand: Bool, @@ -704,11 +885,13 @@ public final class GroupCallParticipantsContext { activityRank: Int?, muteState: MuteState?, volume: Int32?, - about: String? + about: String?, + joinedVideo: Bool ) { self.peer = peer self.ssrc = ssrc - self.jsonParams = jsonParams + self.videoDescription = videoDescription + self.presentationDescription = presentationDescription self.joinTimestamp = joinTimestamp self.raiseHandRating = raiseHandRating self.hasRaiseHand = hasRaiseHand @@ -717,11 +900,18 @@ public final class GroupCallParticipantsContext { self.muteState = muteState self.volume = volume self.about = about + self.joinedVideo = joinedVideo + } + + public var description: String { + return "Participant(peer: \(peer.id): \(peer.debugDisplayTitle), ssrc: \(String(describing: self.ssrc))" } - public mutating func mergeActivity(from other: Participant) { + public mutating func mergeActivity(from other: Participant, mergeActivityTimestamp: Bool) { self.activityRank = other.activityRank - self.activityTimestamp = other.activityTimestamp + if mergeActivityTimestamp { + self.activityTimestamp = other.activityTimestamp + } } public static func ==(lhs: Participant, rhs: Participant) -> Bool { @@ -731,6 +921,12 @@ public final class GroupCallParticipantsContext { if lhs.ssrc != rhs.ssrc { return false } + if lhs.videoDescription != rhs.videoDescription { + return false + } + if lhs.presentationDescription != rhs.presentationDescription { + return false + } if lhs.joinTimestamp != rhs.joinTimestamp { return false } @@ -761,7 +957,13 @@ public final class GroupCallParticipantsContext { return true } - public static func <(lhs: Participant, rhs: Participant) -> Bool { + public static func compare(lhs: Participant, rhs: Participant, sortAscending: Bool) -> Bool { + let lhsCanUnmute = lhs.muteState?.canUnmute ?? true + let rhsCanUnmute = rhs.muteState?.canUnmute ?? true + if lhsCanUnmute != rhsCanUnmute { + return lhsCanUnmute + } + if let lhsActivityRank = lhs.activityRank, let rhsActivityRank = rhs.activityRank { if lhsActivityRank != rhsActivityRank { return lhsActivityRank < rhsActivityRank @@ -793,7 +995,11 @@ public final class GroupCallParticipantsContext { } if lhs.joinTimestamp != rhs.joinTimestamp { - return lhs.joinTimestamp > rhs.joinTimestamp + if sortAscending { + return lhs.joinTimestamp < rhs.joinTimestamp + } else { + return lhs.joinTimestamp > rhs.joinTimestamp + } } return lhs.peer.id < rhs.peer.id @@ -804,6 +1010,11 @@ public final class GroupCallParticipantsContext { public struct DefaultParticipantsAreMuted: Equatable { public var isMuted: Bool public var canChange: Bool + + public init(isMuted: Bool, canChange: Bool) { + self.isMuted = isMuted + self.canChange = canChange + } } public var participants: [Participant] @@ -811,12 +1022,17 @@ public final class GroupCallParticipantsContext { public var adminIds: Set public var isCreator: Bool public var defaultParticipantsAreMuted: DefaultParticipantsAreMuted + public var sortAscending: Bool public var recordingStartTimestamp: Int32? public var title: String? + public var scheduleTimestamp: Int32? + public var subscribedToScheduled: Bool public var totalCount: Int + public var isVideoEnabled: Bool + public var unmutedVideoLimit: Int public var version: Int32 - public mutating func mergeActivity(from other: State, myPeerId: PeerId, previousMyPeerId: PeerId?) { + public mutating func mergeActivity(from other: State, myPeerId: PeerId?, previousMyPeerId: PeerId?, mergeActivityTimestamps: Bool) { var indexMap: [PeerId: Int] = [:] for i in 0 ..< other.participants.count { indexMap[other.participants[i].peer.id] = i @@ -824,14 +1040,46 @@ public final class GroupCallParticipantsContext { for i in 0 ..< self.participants.count { if let index = indexMap[self.participants[i].peer.id] { - self.participants[i].mergeActivity(from: other.participants[index]) + self.participants[i].mergeActivity(from: other.participants[index], mergeActivityTimestamp: mergeActivityTimestamps) if self.participants[i].peer.id == myPeerId || self.participants[i].peer.id == previousMyPeerId { self.participants[i].joinTimestamp = other.participants[index].joinTimestamp } } } - self.participants.sort() + self.participants.sort(by: { GroupCallParticipantsContext.Participant.compare(lhs: $0, rhs: $1, sortAscending: self.sortAscending) }) + } + + public init( + participants: [Participant], + nextParticipantsFetchOffset: String?, + adminIds: Set, + isCreator: Bool, + defaultParticipantsAreMuted: DefaultParticipantsAreMuted, + sortAscending: Bool, + recordingStartTimestamp: Int32?, + title: String?, + scheduleTimestamp: Int32?, + subscribedToScheduled: Bool, + totalCount: Int, + isVideoEnabled: Bool, + unmutedVideoLimit: Int, + version: Int32 + ) { + self.participants = participants + self.nextParticipantsFetchOffset = nextParticipantsFetchOffset + self.adminIds = adminIds + self.isCreator = isCreator + self.defaultParticipantsAreMuted = defaultParticipantsAreMuted + self.sortAscending = sortAscending + self.recordingStartTimestamp = recordingStartTimestamp + self.title = title + self.scheduleTimestamp = scheduleTimestamp + self.subscribedToScheduled = subscribedToScheduled + self.totalCount = totalCount + self.isVideoEnabled = isVideoEnabled + self.unmutedVideoLimit = unmutedVideoLimit + self.version = version } } @@ -881,7 +1129,8 @@ public final class GroupCallParticipantsContext { public var peerId: PeerId public var ssrc: UInt32? - public var jsonParams: String? + public var videoDescription: GroupCallParticipantsContext.Participant.VideoDescription? + public var presentationDescription: GroupCallParticipantsContext.Participant.VideoDescription? public var joinTimestamp: Int32 public var activityTimestamp: Double? public var raiseHandRating: Int64? @@ -889,12 +1138,14 @@ public final class GroupCallParticipantsContext { public var participationStatusChange: ParticipationStatusChange public var volume: Int32? public var about: String? + public var joinedVideo: Bool public var isMin: Bool init( peerId: PeerId, ssrc: UInt32?, - jsonParams: String?, + videoDescription: GroupCallParticipantsContext.Participant.VideoDescription?, + presentationDescription: GroupCallParticipantsContext.Participant.VideoDescription?, joinTimestamp: Int32, activityTimestamp: Double?, raiseHandRating: Int64?, @@ -902,11 +1153,13 @@ public final class GroupCallParticipantsContext { participationStatusChange: ParticipationStatusChange, volume: Int32?, about: String?, + joinedVideo: Bool, isMin: Bool ) { self.peerId = peerId self.ssrc = ssrc - self.jsonParams = jsonParams + self.videoDescription = videoDescription + self.presentationDescription = presentationDescription self.joinTimestamp = joinTimestamp self.activityTimestamp = activityTimestamp self.raiseHandRating = raiseHandRating @@ -914,6 +1167,7 @@ public final class GroupCallParticipantsContext { self.participationStatusChange = participationStatusChange self.volume = volume self.about = about + self.joinedVideo = joinedVideo self.isMin = isMin } } @@ -925,15 +1179,17 @@ public final class GroupCallParticipantsContext { } case state(update: StateUpdate) - case call(isTerminated: Bool, defaultParticipantsAreMuted: State.DefaultParticipantsAreMuted, title: String?, recordingStartTimestamp: Int32?) + case call(isTerminated: Bool, defaultParticipantsAreMuted: State.DefaultParticipantsAreMuted, title: String?, recordingStartTimestamp: Int32?, scheduleTimestamp: Int32?, isVideoEnabled: Bool) } public final class MemberEvent { public let peerId: PeerId + public let canUnmute: Bool public let joined: Bool - public init(peerId: PeerId, joined: Bool) { + public init(peerId: PeerId, canUnmute: Bool, joined: Bool) { self.peerId = peerId + self.canUnmute = canUnmute self.joined = joined } } @@ -958,11 +1214,24 @@ public final class GroupCallParticipantsContext { public var state: Signal { let accountPeerId = self.account.peerId + let myPeerId = self.myPeerId return self.statePromise.get() |> map { state -> State in var publicState = state.state var sortAgain = false - let canSeeHands = state.state.isCreator || state.state.adminIds.contains(accountPeerId) + var canSeeHands = state.state.isCreator || state.state.adminIds.contains(accountPeerId) + for participant in publicState.participants { + if participant.peer.id == myPeerId { + if let muteState = participant.muteState { + if muteState.canUnmute { + canSeeHands = true + } + } else { + canSeeHands = true + } + break + } + } for i in 0 ..< publicState.participants.count { if let pendingMuteState = state.overlayState.pendingMuteStateChanges[publicState.participants[i].peer.id] { publicState.participants[i].muteState = pendingMuteState.state @@ -974,7 +1243,7 @@ public final class GroupCallParticipantsContext { } } if sortAgain { - publicState.participants.sort() + publicState.participants.sort(by: { GroupCallParticipantsContext.Participant.compare(lhs: $0, rhs: $1, sortAscending: publicState.sortAscending) }) } return publicState } @@ -1016,15 +1285,19 @@ public final class GroupCallParticipantsContext { private var activityRankResetTimer: SwiftSignalKit.Timer? private let updateDefaultMuteDisposable = MetaDisposable() + private let resetInviteLinksDisposable = MetaDisposable() private let updateShouldBeRecordingDisposable = MetaDisposable() + private var localVideoIsMuted: Bool? = nil + private var localIsVideoPaused: Bool? = nil + private var localIsPresentationPaused: Bool? = nil public struct ServiceState { fileprivate var nextActivityRank: Int = 0 } public private(set) var serviceState: ServiceState - public init(account: Account, peerId: PeerId, myPeerId: PeerId, id: Int64, accessHash: Int64, state: State, previousServiceState: ServiceState?) { + init(account: Account, peerId: PeerId, myPeerId: PeerId, id: Int64, accessHash: Int64, state: State, previousServiceState: ServiceState?) { self.account = account self.myPeerId = myPeerId self.id = id @@ -1088,7 +1361,7 @@ public final class GroupCallParticipantsContext { } if updated { - updatedParticipants.sort() + updatedParticipants.sort(by: { GroupCallParticipantsContext.Participant.compare(lhs: $0, rhs: $1, sortAscending: strongSelf.stateValue.state.sortAscending) }) strongSelf.stateValue = InternalState( state: State( @@ -1097,9 +1370,14 @@ public final class GroupCallParticipantsContext { adminIds: strongSelf.stateValue.state.adminIds, isCreator: strongSelf.stateValue.state.isCreator, defaultParticipantsAreMuted: strongSelf.stateValue.state.defaultParticipantsAreMuted, + sortAscending: strongSelf.stateValue.state.sortAscending, recordingStartTimestamp: strongSelf.stateValue.state.recordingStartTimestamp, title: strongSelf.stateValue.state.title, + scheduleTimestamp: strongSelf.stateValue.state.scheduleTimestamp, + subscribedToScheduled: strongSelf.stateValue.state.subscribedToScheduled, totalCount: strongSelf.stateValue.state.totalCount, + isVideoEnabled: strongSelf.stateValue.state.isVideoEnabled, + unmutedVideoLimit: strongSelf.stateValue.state.unmutedVideoLimit, version: strongSelf.stateValue.state.version ), overlayState: strongSelf.stateValue.overlayState @@ -1133,7 +1411,7 @@ public final class GroupCallParticipantsContext { } } if updated { - strongSelf.stateValue.state.participants.sort() + strongSelf.stateValue.state.participants.sort(by: { GroupCallParticipantsContext.Participant.compare(lhs: $0, rhs: $1, sortAscending: strongSelf.stateValue.state.sortAscending) }) } }, queue: .mainQueue()) self.activityRankResetTimer?.start() @@ -1146,6 +1424,7 @@ public final class GroupCallParticipantsContext { self.updateDefaultMuteDisposable.dispose() self.updateShouldBeRecordingDisposable.dispose() self.activityRankResetTimer?.invalidate() + resetInviteLinksDisposable.dispose() } public func addUpdates(updates: [Update]) { @@ -1153,11 +1432,13 @@ public final class GroupCallParticipantsContext { for update in updates { if case let .state(update) = update { stateUpdates.append(update) - } else if case let .call(_, defaultParticipantsAreMuted, title, recordingStartTimestamp) = update { + } else if case let .call(_, defaultParticipantsAreMuted, title, recordingStartTimestamp, scheduleTimestamp, isVideoEnabled) = update { var state = self.stateValue.state state.defaultParticipantsAreMuted = defaultParticipantsAreMuted state.recordingStartTimestamp = recordingStartTimestamp state.title = title + state.scheduleTimestamp = scheduleTimestamp + state.isVideoEnabled = isVideoEnabled self.stateValue.state = state } @@ -1218,7 +1499,7 @@ public final class GroupCallParticipantsContext { } if updated { - updatedParticipants.sort() + updatedParticipants.sort(by: { GroupCallParticipantsContext.Participant.compare(lhs: $0, rhs: $1, sortAscending: strongSelf.stateValue.state.sortAscending) }) strongSelf.stateValue = InternalState( state: State( @@ -1227,9 +1508,14 @@ public final class GroupCallParticipantsContext { adminIds: strongSelf.stateValue.state.adminIds, isCreator: strongSelf.stateValue.state.isCreator, defaultParticipantsAreMuted: strongSelf.stateValue.state.defaultParticipantsAreMuted, + sortAscending: strongSelf.stateValue.state.sortAscending, recordingStartTimestamp: strongSelf.stateValue.state.recordingStartTimestamp, title: strongSelf.stateValue.state.title, + scheduleTimestamp: strongSelf.stateValue.state.scheduleTimestamp, + subscribedToScheduled: strongSelf.stateValue.state.subscribedToScheduled, totalCount: strongSelf.stateValue.state.totalCount, + isVideoEnabled: strongSelf.stateValue.state.isVideoEnabled, + unmutedVideoLimit: strongSelf.stateValue.state.unmutedVideoLimit, version: strongSelf.stateValue.state.version ), overlayState: strongSelf.stateValue.overlayState @@ -1247,6 +1533,9 @@ public final class GroupCallParticipantsContext { if let ssrc = participant.ssrc { existingSsrcs.insert(ssrc) } + if let presentationDescription = participant.presentationDescription, let presentationAudioSsrc = presentationDescription.audioSsrc { + existingSsrcs.insert(presentationAudioSsrc) + } } for ssrc in ssrcs { @@ -1271,8 +1560,10 @@ public final class GroupCallParticipantsContext { self.isLoadingMore = true let ssrcs = self.missingSsrcs + + Logger.shared.log("GroupCallParticipantsContext", "will request ssrcs=\(ssrcs)") - self.disposable.set((getGroupCallParticipants(account: self.account, callId: self.id, accessHash: self.accessHash, offset: "", ssrcs: Array(ssrcs), limit: 100) + self.disposable.set((_internal_getGroupCallParticipants(account: self.account, callId: self.id, accessHash: self.accessHash, offset: "", ssrcs: Array(ssrcs), limit: 100, sortAscending: true) |> deliverOnMainQueue).start(next: { [weak self] state in guard let strongSelf = self else { return @@ -1280,10 +1571,12 @@ public final class GroupCallParticipantsContext { strongSelf.isLoadingMore = false strongSelf.missingSsrcs.subtract(ssrcs) + + Logger.shared.log("GroupCallParticipantsContext", "did receive response for ssrcs=\(ssrcs), \(state.participants)") var updatedState = strongSelf.stateValue.state - updatedState.participants = mergeAndSortParticipants(current: updatedState.participants, with: state.participants) + updatedState.participants = mergeAndSortParticipants(current: updatedState.participants, with: state.participants, sortAscending: updatedState.sortAscending) updatedState.totalCount = max(updatedState.totalCount, state.totalCount) updatedState.version = max(updatedState.version, updatedState.version) @@ -1359,7 +1652,7 @@ public final class GroupCallParticipantsContext { if let index = updatedParticipants.firstIndex(where: { $0.peer.id == participantUpdate.peerId }) { updatedParticipants.remove(at: index) updatedTotalCount = max(0, updatedTotalCount - 1) - strongSelf.memberEventsPipe.putNext(MemberEvent(peerId: participantUpdate.peerId, joined: false)) + strongSelf.memberEventsPipe.putNext(MemberEvent(peerId: participantUpdate.peerId, canUnmute: false, joined: false)) } else if isVersionUpdate { updatedTotalCount = max(0, updatedTotalCount - 1) } @@ -1382,7 +1675,7 @@ public final class GroupCallParticipantsContext { updatedParticipants.remove(at: index) } else if case .joined = participantUpdate.participationStatusChange { updatedTotalCount += 1 - strongSelf.memberEventsPipe.putNext(MemberEvent(peerId: participantUpdate.peerId, joined: true)) + strongSelf.memberEventsPipe.putNext(MemberEvent(peerId: participantUpdate.peerId, canUnmute: participantUpdate.muteState?.canUnmute ?? true, joined: true)) } var activityTimestamp: Double? @@ -1392,6 +1685,11 @@ public final class GroupCallParticipantsContext { activityTimestamp = participantUpdate.activityTimestamp ?? previousActivityTimestamp } + if let muteState = participantUpdate.muteState, !muteState.canUnmute { + previousActivityRank = nil + activityTimestamp = nil + } + var volume = participantUpdate.volume var muteState = participantUpdate.muteState if participantUpdate.isMin { @@ -1404,11 +1702,11 @@ public final class GroupCallParticipantsContext { volume = previousVolume } } - let participant = Participant( peer: peer, ssrc: participantUpdate.ssrc, - jsonParams: participantUpdate.jsonParams, + videoDescription: participantUpdate.videoDescription, + presentationDescription: participantUpdate.presentationDescription, joinTimestamp: previousJoinTimestamp ?? participantUpdate.joinTimestamp, raiseHandRating: participantUpdate.raiseHandRating, hasRaiseHand: participantUpdate.raiseHandRating != nil, @@ -1416,7 +1714,8 @@ public final class GroupCallParticipantsContext { activityRank: previousActivityRank, muteState: muteState, volume: volume, - about: participantUpdate.about + about: participantUpdate.about, + joinedVideo: participantUpdate.joinedVideo ) updatedParticipants.append(participant) } @@ -1435,8 +1734,12 @@ public final class GroupCallParticipantsContext { let defaultParticipantsAreMuted = strongSelf.stateValue.state.defaultParticipantsAreMuted let recordingStartTimestamp = strongSelf.stateValue.state.recordingStartTimestamp let title = strongSelf.stateValue.state.title + let scheduleTimestamp = strongSelf.stateValue.state.scheduleTimestamp + let subscribedToScheduled = strongSelf.stateValue.state.subscribedToScheduled + let isVideoEnabled = strongSelf.stateValue.state.isVideoEnabled + let unmutedVideoLimit = strongSelf.stateValue.state.unmutedVideoLimit - updatedParticipants.sort() + updatedParticipants.sort(by: { GroupCallParticipantsContext.Participant.compare(lhs: $0, rhs: $1, sortAscending: strongSelf.stateValue.state.sortAscending) }) strongSelf.stateValue = InternalState( state: State( @@ -1445,9 +1748,14 @@ public final class GroupCallParticipantsContext { adminIds: adminIds, isCreator: isCreator, defaultParticipantsAreMuted: defaultParticipantsAreMuted, + sortAscending: strongSelf.stateValue.state.sortAscending, recordingStartTimestamp: recordingStartTimestamp, title: title, + scheduleTimestamp: scheduleTimestamp, + subscribedToScheduled: subscribedToScheduled, totalCount: updatedTotalCount, + isVideoEnabled: isVideoEnabled, + unmutedVideoLimit: unmutedVideoLimit, version: update.version ), overlayState: updatedOverlayState @@ -1467,13 +1775,21 @@ public final class GroupCallParticipantsContext { self.updateQueue.removeAll() - self.disposable.set((getGroupCallParticipants(account: self.account, callId: self.id, accessHash: self.accessHash, offset: "", ssrcs: [], limit: 100) + self.disposable.set((_internal_getGroupCallParticipants(account: self.account, callId: self.id, accessHash: self.accessHash, offset: "", ssrcs: [], limit: 100, sortAscending: self.stateValue.state.sortAscending) |> deliverOnMainQueue).start(next: { [weak self] state in guard let strongSelf = self else { return } strongSelf.isLoadingMore = false strongSelf.shouldResetStateFromServer = false + var state = state + state.adminIds = strongSelf.stateValue.state.adminIds + state.isCreator = strongSelf.stateValue.state.isCreator + state.defaultParticipantsAreMuted = strongSelf.stateValue.state.defaultParticipantsAreMuted + state.title = strongSelf.stateValue.state.title + state.recordingStartTimestamp = strongSelf.stateValue.state.recordingStartTimestamp + state.scheduleTimestamp = strongSelf.stateValue.state.scheduleTimestamp + state.mergeActivity(from: strongSelf.stateValue.state, myPeerId: nil, previousMyPeerId: nil, mergeActivityTimestamps: false) strongSelf.stateValue.state = state strongSelf.endedProcessingUpdate() })) @@ -1526,8 +1842,10 @@ public final class GroupCallParticipantsContext { if let volume = volume, volume > 0 { flags |= 1 << 1 } + var muted: Api.Bool? if let muteState = muteState, (!muteState.canUnmute || peerId == myPeerId || muteState.mutedByYou) { flags |= 1 << 0 + muted = .boolTrue } let raiseHandApi: Api.Bool? if let raiseHand = raiseHand { @@ -1537,7 +1855,7 @@ public final class GroupCallParticipantsContext { raiseHandApi = nil } - return account.network.request(Api.functions.phone.editGroupCallParticipant(flags: flags, call: .inputGroupCall(id: id, accessHash: accessHash), participant: inputPeer, volume: volume, raiseHand: raiseHandApi)) + return account.network.request(Api.functions.phone.editGroupCallParticipant(flags: flags, call: .inputGroupCall(id: id, accessHash: accessHash), participant: inputPeer, muted: muted, volume: volume, raiseHand: raiseHandApi, videoStopped: nil, videoPaused: nil, presentationPaused: nil)) |> map(Optional.init) |> `catch` { _ -> Signal in return .single(nil) @@ -1576,6 +1894,85 @@ public final class GroupCallParticipantsContext { } })) } + + public func updateVideoState(peerId: PeerId, isVideoMuted: Bool?, isVideoPaused: Bool?, isPresentationPaused: Bool?) { + if self.localVideoIsMuted == isVideoMuted && self.localIsVideoPaused == isVideoPaused && self.localIsPresentationPaused == isPresentationPaused { + return + } + self.localVideoIsMuted = isVideoMuted + self.localIsVideoPaused = isVideoPaused + self.localIsPresentationPaused = isPresentationPaused + + let disposable = MetaDisposable() + + let account = self.account + let id = self.id + let accessHash = self.accessHash + + let signal: Signal = self.account.postbox.transaction { transaction -> Api.InputPeer? in + return transaction.getPeer(peerId).flatMap(apiInputPeer) + } + |> mapToSignal { inputPeer -> Signal in + guard let inputPeer = inputPeer else { + return .single(nil) + } + var flags: Int32 = 0 + var videoMuted: Api.Bool? + + if let isVideoMuted = isVideoMuted { + videoMuted = isVideoMuted ? .boolTrue : .boolFalse + flags |= 1 << 3 + } + + var videoPaused: Api.Bool? + if isVideoMuted != nil, let isVideoPaused = isVideoPaused { + videoPaused = isVideoPaused ? .boolTrue : .boolFalse + flags |= 1 << 4 + } + var presentationPaused: Api.Bool? + + if let isPresentationPaused = isPresentationPaused { + presentationPaused = isPresentationPaused ? .boolTrue : .boolFalse + flags |= 1 << 5 + } + + return account.network.request(Api.functions.phone.editGroupCallParticipant(flags: flags, call: .inputGroupCall(id: id, accessHash: accessHash), participant: inputPeer, muted: nil, volume: nil, raiseHand: nil, videoStopped: videoMuted, videoPaused: videoPaused, presentationPaused: presentationPaused)) + |> map(Optional.init) + |> `catch` { _ -> Signal in + return .single(nil) + } + } + + disposable.set((signal + |> deliverOnMainQueue).start(next: { [weak self] updates in + guard let strongSelf = self else { + return + } + + if let updates = updates { + var stateUpdates: [GroupCallParticipantsContext.Update] = [] + + loop: for update in updates.allUpdates { + switch update { + case let .updateGroupCallParticipants(call, participants, version): + switch call { + case let .inputGroupCall(updateCallId, _): + if updateCallId != id { + continue loop + } + } + stateUpdates.append(.state(update: GroupCallParticipantsContext.Update.StateUpdate(participants: participants, version: version, removePendingMuteStates: [peerId]))) + default: + break + } + } + + strongSelf.addUpdates(updates: stateUpdates) + + strongSelf.account.stateManager.addUpdates(updates) + } + })) + } public func raiseHand() { self.updateMuteState(peerId: self.myPeerId, muteState: nil, volume: nil, raiseHand: true) @@ -1617,6 +2014,16 @@ public final class GroupCallParticipantsContext { })) } + public func resetInviteLinks() { + self.resetInviteLinksDisposable.set((self.account.network.request(Api.functions.phone.toggleGroupCallSettings(flags: 1 << 1, call: .inputGroupCall(id: self.id, accessHash: self.accessHash), joinMuted: nil)) + |> deliverOnMainQueue).start(next: { [weak self] updates in + guard let strongSelf = self else { + return + } + strongSelf.account.stateManager.addUpdates(updates) + })) + } + public func loadMore(token: String) { if token != self.stateValue.state.nextParticipantsFetchOffset { Logger.shared.log("GroupCallParticipantsContext", "loadMore called with an invalid token \(token) (the valid one is \(String(describing: self.stateValue.state.nextParticipantsFetchOffset)))") @@ -1627,7 +2034,7 @@ public final class GroupCallParticipantsContext { } self.isLoadingMore = true - self.disposable.set((getGroupCallParticipants(account: self.account, callId: self.id, accessHash: self.accessHash, offset: token, ssrcs: [], limit: 100) + self.disposable.set((_internal_getGroupCallParticipants(account: self.account, callId: self.id, accessHash: self.accessHash, offset: token, ssrcs: [], limit: 100, sortAscending: self.stateValue.state.sortAscending) |> deliverOnMainQueue).start(next: { [weak self] state in guard let strongSelf = self else { return @@ -1636,7 +2043,7 @@ public final class GroupCallParticipantsContext { var updatedState = strongSelf.stateValue.state - updatedState.participants = mergeAndSortParticipants(current: updatedState.participants, with: state.participants) + updatedState.participants = mergeAndSortParticipants(current: updatedState.participants, with: state.participants, sortAscending: updatedState.sortAscending) updatedState.nextParticipantsFetchOffset = state.nextParticipantsFetchOffset updatedState.totalCount = max(updatedState.totalCount, state.totalCount) @@ -1654,16 +2061,8 @@ public final class GroupCallParticipantsContext { extension GroupCallParticipantsContext.Update.StateUpdate.ParticipantUpdate { init(_ apiParticipant: Api.GroupCallParticipant) { switch apiParticipant { - case let .groupCallParticipant(flags, apiPeerId, date, activeDate, source, volume, about, raiseHandRating): - let peerId: PeerId - switch apiPeerId { - case let .peerUser(userId): - peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: userId) - case let .peerChat(chatId): - peerId = PeerId(namespace: Namespaces.Peer.CloudGroup, id: chatId) - case let .peerChannel(channelId): - peerId = PeerId(namespace: Namespaces.Peer.CloudChannel, id: channelId) - } + case let .groupCallParticipant(flags, apiPeerId, date, activeDate, source, volume, about, raiseHandRating, video, presentation): + let peerId: PeerId = apiPeerId.peerId let ssrc = UInt32(bitPattern: source) let muted = (flags & (1 << 0)) != 0 let mutedByYou = (flags & (1 << 9)) != 0 @@ -1676,6 +2075,7 @@ extension GroupCallParticipantsContext.Update.StateUpdate.ParticipantUpdate { } let isRemoved = (flags & (1 << 1)) != 0 let justJoined = (flags & (1 << 4)) != 0 + let joinedVideo = (flags & (1 << 15)) != 0 let isMin = (flags & (1 << 8)) != 0 let participationStatusChange: GroupCallParticipantsContext.Update.StateUpdate.ParticipantUpdate.ParticipationStatusChange @@ -1687,18 +2087,17 @@ extension GroupCallParticipantsContext.Update.StateUpdate.ParticipantUpdate { participationStatusChange = .none } - let jsonParams: String? = nil - /*if let params = params { - switch params { - case let .dataJSON(data): - jsonParams = data - } - }*/ - + var videoDescription = video.flatMap(GroupCallParticipantsContext.Participant.VideoDescription.init) + var presentationDescription = presentation.flatMap(GroupCallParticipantsContext.Participant.VideoDescription.init) + if muteState?.canUnmute == false { + videoDescription = nil + presentationDescription = nil + } self.init( peerId: peerId, ssrc: ssrc, - jsonParams: jsonParams, + videoDescription: videoDescription, + presentationDescription: presentationDescription, joinTimestamp: date, activityTimestamp: activeDate.flatMap(Double.init), raiseHandRating: raiseHandRating, @@ -1706,6 +2105,7 @@ extension GroupCallParticipantsContext.Update.StateUpdate.ParticipantUpdate { participationStatusChange: participationStatusChange, volume: volume, about: about, + joinedVideo: joinedVideo, isMin: isMin ) } @@ -1714,68 +2114,8 @@ extension GroupCallParticipantsContext.Update.StateUpdate.ParticipantUpdate { extension GroupCallParticipantsContext.Update.StateUpdate { init(participants: [Api.GroupCallParticipant], version: Int32, removePendingMuteStates: Set = Set()) { - var participantUpdates: [GroupCallParticipantsContext.Update.StateUpdate.ParticipantUpdate] = [] - for participant in participants { - switch participant { - case let .groupCallParticipant(flags, apiPeerId, date, activeDate, source, volume, about, raiseHandRating): - let peerId: PeerId - switch apiPeerId { - case let .peerUser(userId): - peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: userId) - case let .peerChat(chatId): - peerId = PeerId(namespace: Namespaces.Peer.CloudGroup, id: chatId) - case let .peerChannel(channelId): - peerId = PeerId(namespace: Namespaces.Peer.CloudChannel, id: channelId) - } - let ssrc = UInt32(bitPattern: source) - let muted = (flags & (1 << 0)) != 0 - let mutedByYou = (flags & (1 << 9)) != 0 - var muteState: GroupCallParticipantsContext.Participant.MuteState? - if muted { - let canUnmute = (flags & (1 << 2)) != 0 - muteState = GroupCallParticipantsContext.Participant.MuteState(canUnmute: canUnmute, mutedByYou: mutedByYou) - } else if mutedByYou { - muteState = GroupCallParticipantsContext.Participant.MuteState(canUnmute: false, mutedByYou: mutedByYou) - } - let isRemoved = (flags & (1 << 1)) != 0 - let justJoined = (flags & (1 << 4)) != 0 - let isMin = (flags & (1 << 8)) != 0 - - let participationStatusChange: GroupCallParticipantsContext.Update.StateUpdate.ParticipantUpdate.ParticipationStatusChange - if isRemoved { - participationStatusChange = .left - } else if justJoined { - participationStatusChange = .joined - } else { - participationStatusChange = .none - } - - let jsonParams: String? = nil - /*if let params = params { - switch params { - case let .dataJSON(data): - jsonParams = data - } - }*/ - - participantUpdates.append(GroupCallParticipantsContext.Update.StateUpdate.ParticipantUpdate( - peerId: peerId, - ssrc: ssrc, - jsonParams: jsonParams, - joinTimestamp: date, - activityTimestamp: activeDate.flatMap(Double.init), - raiseHandRating: raiseHandRating, - muteState: muteState, - participationStatusChange: participationStatusChange, - volume: volume, - about: about, - isMin: isMin - )) - } - } - self.init( - participantUpdates: participantUpdates, + participantUpdates: participants.map { GroupCallParticipantsContext.Update.StateUpdate.ParticipantUpdate($0) }, version: version, removePendingMuteStates: removePendingMuteStates ) @@ -1786,7 +2126,7 @@ public enum InviteToGroupCallError { case generic } -public func inviteToGroupCall(account: Account, callId: Int64, accessHash: Int64, peerId: PeerId) -> Signal { +func _internal_inviteToGroupCall(account: Account, callId: Int64, accessHash: Int64, peerId: PeerId) -> Signal { return account.postbox.transaction { transaction -> Peer? in return transaction.getPeer(peerId) } @@ -1821,7 +2161,7 @@ public struct GroupCallInviteLinks { } } -public func groupCallInviteLinks(account: Account, callId: Int64, accessHash: Int64) -> Signal { +func _internal_groupCallInviteLinks(account: Account, callId: Int64, accessHash: Int64) -> Signal { let call = Api.InputGroupCall.inputGroupCall(id: callId, accessHash: accessHash) let listenerInvite: Signal = account.network.request(Api.functions.phone.exportGroupCallInvite(flags: 0, call: call)) |> map(Optional.init) @@ -1861,7 +2201,7 @@ public enum EditGroupCallTitleError { case generic } -public func editGroupCallTitle(account: Account, callId: Int64, accessHash: Int64, title: String) -> Signal { +func _internal_editGroupCallTitle(account: Account, callId: Int64, accessHash: Int64, title: String) -> Signal { return account.network.request(Api.functions.phone.editGroupCallTitle(call: .inputGroupCall(id: callId, accessHash: accessHash), title: title)) |> mapError { _ -> EditGroupCallTitleError in return .generic } @@ -1871,7 +2211,7 @@ public func editGroupCallTitle(account: Account, callId: Int64, accessHash: Int6 } } -public func groupCallDisplayAsAvailablePeers(network: Network, postbox: Postbox, peerId: PeerId) -> Signal<[FoundPeer], NoError> { +func _internal_groupCallDisplayAsAvailablePeers(network: Network, postbox: Postbox, peerId: PeerId) -> Signal<[FoundPeer], NoError> { return postbox.transaction { transaction -> Api.InputPeer? in return transaction.getPeer(peerId).flatMap(apiInputPeer) } |> mapToSignal { inputPeer in @@ -1939,8 +2279,16 @@ public final class CachedDisplayAsPeers: PostboxCoding { } } +func _internal_clearCachedGroupCallDisplayAsAvailablePeers(account: Account, peerId: PeerId) -> Signal { + return account.postbox.transaction { transaction -> Void in + let key = ValueBoxKey(length: 8) + key.setInt64(0, value: peerId.toInt64()) + transaction.removeItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedGroupCallDisplayAsPeers, key: key)) + } + |> ignoreValues +} -public func cachedGroupCallDisplayAsAvailablePeers(account: Account, peerId: PeerId) -> Signal<[FoundPeer], NoError> { +func _internal_cachedGroupCallDisplayAsAvailablePeers(account: Account, peerId: PeerId) -> Signal<[FoundPeer], NoError> { let key = ValueBoxKey(length: 8) key.setInt64(0, value: peerId.toInt64()) return account.postbox.transaction { transaction -> ([FoundPeer], Int32)? in @@ -1966,7 +2314,7 @@ public func cachedGroupCallDisplayAsAvailablePeers(account: Account, peerId: Pee if let (cachedPeers, timestamp) = cachedPeersAndTimestamp, currentTimestamp - timestamp < 60 * 3 && !cachedPeers.isEmpty { return .single(cachedPeers) } else { - return groupCallDisplayAsAvailablePeers(network: account.network, postbox: account.postbox, peerId: peerId) + return _internal_groupCallDisplayAsAvailablePeers(network: account.network, postbox: account.postbox, peerId: peerId) |> mapToSignal { peers -> Signal<[FoundPeer], NoError> in return account.postbox.transaction { transaction -> [FoundPeer] in let currentTimestamp = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) @@ -1978,8 +2326,8 @@ public func cachedGroupCallDisplayAsAvailablePeers(account: Account, peerId: Pee } } -public func updatedCurrentPeerGroupCall(account: Account, peerId: PeerId) -> Signal { - return fetchAndUpdateCachedPeerData(accountPeerId: account.peerId, peerId: peerId, network: account.network, postbox: account.postbox) +func _internal_updatedCurrentPeerGroupCall(account: Account, peerId: PeerId) -> Signal { + return _internal_fetchAndUpdateCachedPeerData(accountPeerId: account.peerId, peerId: peerId, network: account.network, postbox: account.postbox) |> mapToSignal { _ -> Signal in return account.postbox.transaction { transaction -> CachedChannelData.ActiveCall? in return (transaction.getPeerCachedData(peerId: peerId) as? CachedChannelData)?.activeCall @@ -1987,7 +2335,7 @@ public func updatedCurrentPeerGroupCall(account: Account, peerId: PeerId) -> Sig } } -private func mergeAndSortParticipants(current currentParticipants: [GroupCallParticipantsContext.Participant], with updatedParticipants: [GroupCallParticipantsContext.Participant]) -> [GroupCallParticipantsContext.Participant] { +private func mergeAndSortParticipants(current currentParticipants: [GroupCallParticipantsContext.Participant], with updatedParticipants: [GroupCallParticipantsContext.Participant], sortAscending: Bool) -> [GroupCallParticipantsContext.Participant] { var mergedParticipants = currentParticipants var existingParticipantIndices: [PeerId: Int] = [:] @@ -2001,8 +2349,8 @@ private func mergeAndSortParticipants(current currentParticipants: [GroupCallPar mergedParticipants.append(participant) } } - - mergedParticipants.sort() + + mergedParticipants.sort(by: { GroupCallParticipantsContext.Participant.compare(lhs: $0, rhs: $1, sortAscending: sortAscending) }) return mergedParticipants } @@ -2015,7 +2363,7 @@ public final class AudioBroadcastDataSource { } } -public func getAudioBroadcastDataSource(account: Account, callId: Int64, accessHash: Int64) -> Signal { +func _internal_getAudioBroadcastDataSource(account: Account, callId: Int64, accessHash: Int64) -> Signal { return account.network.request(Api.functions.phone.getGroupCall(call: .inputGroupCall(id: callId, accessHash: accessHash))) |> map(Optional.init) |> `catch` { _ -> Signal in @@ -2051,7 +2399,7 @@ public struct GetAudioBroadcastPartResult { public var responseTimestamp: Double } -public func getAudioBroadcastPart(dataSource: AudioBroadcastDataSource, callId: Int64, accessHash: Int64, timestampIdMilliseconds: Int64, durationMilliseconds: Int64) -> Signal { +func _internal_getAudioBroadcastPart(dataSource: AudioBroadcastDataSource, callId: Int64, accessHash: Int64, timestampIdMilliseconds: Int64, durationMilliseconds: Int64) -> Signal { let scale: Int32 switch durationMilliseconds { case 1000: @@ -2083,12 +2431,12 @@ public func getAudioBroadcastPart(dataSource: AudioBroadcastDataSource, callId: status: .rejoinNeeded, responseTimestamp: responseTimestamp )) - } else if error.errorDescription.hasPrefix("FLOOD_WAIT") { + } else if error.errorDescription.hasPrefix("FLOOD_WAIT") || error.errorDescription == "TIME_TOO_BIG" { return .single(GetAudioBroadcastPartResult( status: .notReady, responseTimestamp: responseTimestamp )) - } else if error.errorDescription == "TIME_INVALID" || error.errorDescription == "TIME_TOO_SMALL" || error.errorDescription == "TIME_TOO_BIG" { + } else if error.errorDescription == "TIME_INVALID" || error.errorDescription == "TIME_TOO_SMALL" { return .single(GetAudioBroadcastPartResult( status: .resyncNeeded, responseTimestamp: responseTimestamp @@ -2101,3 +2449,66 @@ public func getAudioBroadcastPart(dataSource: AudioBroadcastDataSource, callId: } } } + +extension GroupCallParticipantsContext.Participant { + init?(_ apiParticipant: Api.GroupCallParticipant, transaction: Transaction) { + switch apiParticipant { + case let .groupCallParticipant(flags, apiPeerId, date, activeDate, source, volume, about, raiseHandRating, video, presentation): + let peerId: PeerId = apiPeerId.peerId + let ssrc = UInt32(bitPattern: source) + guard let peer = transaction.getPeer(peerId) else { + return nil + } + let muted = (flags & (1 << 0)) != 0 + let mutedByYou = (flags & (1 << 9)) != 0 + var muteState: GroupCallParticipantsContext.Participant.MuteState? + if muted { + let canUnmute = (flags & (1 << 2)) != 0 + muteState = GroupCallParticipantsContext.Participant.MuteState(canUnmute: canUnmute, mutedByYou: mutedByYou) + } else if mutedByYou { + muteState = GroupCallParticipantsContext.Participant.MuteState(canUnmute: false, mutedByYou: mutedByYou) + } + + var videoDescription = video.flatMap(GroupCallParticipantsContext.Participant.VideoDescription.init) + var presentationDescription = presentation.flatMap(GroupCallParticipantsContext.Participant.VideoDescription.init) + if muteState?.canUnmute == false { + videoDescription = nil + presentationDescription = nil + } + let joinedVideo = (flags & (1 << 15)) != 0 + + self.init( + peer: peer, + ssrc: ssrc, + videoDescription: videoDescription, + presentationDescription: presentationDescription, + joinTimestamp: date, + raiseHandRating: raiseHandRating, + hasRaiseHand: raiseHandRating != nil, + activityTimestamp: activeDate.flatMap(Double.init), + activityRank: nil, + muteState: muteState, + volume: volume, + about: about, + joinedVideo: joinedVideo + ) + } + } +} + +private extension GroupCallParticipantsContext.Participant.VideoDescription { + init(_ apiVideo: Api.GroupCallParticipantVideo) { + switch apiVideo { + case let .groupCallParticipantVideo(flags, endpoint, sourceGroups, audioSource): + var parsedSsrcGroups: [SsrcGroup] = [] + for group in sourceGroups { + switch group { + case let .groupCallParticipantVideoSourceGroup(semantics, sources): + parsedSsrcGroups.append(SsrcGroup(semantics: semantics, ssrcs: sources.map(UInt32.init(bitPattern:)))) + } + } + let isPaused = (flags & (1 << 0)) != 0 + self.init(endpointId: endpoint, ssrcGroups: parsedSsrcGroups, audioSsrc: audioSource.flatMap(UInt32.init(bitPattern:)), isPaused: isPaused) + } + } +} diff --git a/submodules/TelegramCore/Sources/RateCall.swift b/submodules/TelegramCore/Sources/TelegramEngine/Calls/RateCall.swift similarity index 75% rename from submodules/TelegramCore/Sources/RateCall.swift rename to submodules/TelegramCore/Sources/TelegramEngine/Calls/RateCall.swift index bac6cd7258..7a8ed6ebee 100644 --- a/submodules/TelegramCore/Sources/RateCall.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Calls/RateCall.swift @@ -4,7 +4,7 @@ import MtProtoKit import SwiftSignalKit import TelegramApi -public func rateCall(account: Account, callId: CallId, starsCount: Int32, comment: String = "", userInitiated: Bool) -> Signal { +func _internal_rateCall(account: Account, callId: CallId, starsCount: Int32, comment: String = "", userInitiated: Bool) -> Signal { var flags: Int32 = 0 if userInitiated { flags |= (1 << 0) @@ -14,7 +14,7 @@ public func rateCall(account: Account, callId: CallId, starsCount: Int32, commen |> map { _ in } } -public func saveCallDebugLog(network: Network, callId: CallId, log: String) -> Signal { +func _internal_saveCallDebugLog(network: Network, callId: CallId, log: String) -> Signal { if log.count > 1024 * 16 { return .complete() } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Calls/TelegramEngineCalls.swift b/submodules/TelegramCore/Sources/TelegramEngine/Calls/TelegramEngineCalls.swift new file mode 100644 index 0000000000..3e0f02a662 --- /dev/null +++ b/submodules/TelegramCore/Sources/TelegramEngine/Calls/TelegramEngineCalls.swift @@ -0,0 +1,109 @@ +import SwiftSignalKit +import Postbox +import SyncCore + +public extension TelegramEngine { + final class Calls { + private let account: Account + + init(account: Account) { + self.account = account + } + + public func rateCall(callId: CallId, starsCount: Int32, comment: String = "", userInitiated: Bool) -> Signal { + return _internal_rateCall(account: self.account, callId: callId, starsCount: starsCount, comment: comment, userInitiated: userInitiated) + } + + public func saveCallDebugLog(callId: CallId, log: String) -> Signal { + return _internal_saveCallDebugLog(network: self.account.network, callId: callId, log: log) + } + + public func getCurrentGroupCall(callId: Int64, accessHash: Int64, peerId: PeerId? = nil) -> Signal { + return _internal_getCurrentGroupCall(account: self.account, callId: callId, accessHash: accessHash, peerId: peerId) + } + + public func createGroupCall(peerId: PeerId, title: String?, scheduleDate: Int32?) -> Signal { + return _internal_createGroupCall(account: self.account, peerId: peerId, title: title, scheduleDate: scheduleDate) + } + + public func startScheduledGroupCall(peerId: PeerId, callId: Int64, accessHash: Int64) -> Signal { + return _internal_startScheduledGroupCall(account: self.account, peerId: peerId, callId: callId, accessHash: accessHash) + } + + public func toggleScheduledGroupCallSubscription(peerId: PeerId, callId: Int64, accessHash: Int64, subscribe: Bool) -> Signal { + return _internal_toggleScheduledGroupCallSubscription(account: self.account, peerId: peerId, callId: callId, accessHash: accessHash, subscribe: subscribe) + } + + public func updateGroupCallJoinAsPeer(peerId: PeerId, joinAs: PeerId) -> Signal { + return _internal_updateGroupCallJoinAsPeer(account: self.account, peerId: peerId, joinAs: joinAs) + } + + public func getGroupCallParticipants(callId: Int64, accessHash: Int64, offset: String, ssrcs: [UInt32], limit: Int32, sortAscending: Bool?) -> Signal { + return _internal_getGroupCallParticipants(account: self.account, callId: callId, accessHash: accessHash, offset: offset, ssrcs: ssrcs, limit: limit, sortAscending: sortAscending) + } + + public func joinGroupCall(peerId: PeerId, joinAs: PeerId?, callId: Int64, accessHash: Int64, preferMuted: Bool, joinPayload: String, peerAdminIds: Signal<[PeerId], NoError>, inviteHash: String? = nil) -> Signal { + return _internal_joinGroupCall(account: self.account, peerId: peerId, joinAs: joinAs, callId: callId, accessHash: accessHash, preferMuted: preferMuted, joinPayload: joinPayload, peerAdminIds: peerAdminIds, inviteHash: inviteHash) + } + + public func joinGroupCallAsScreencast(peerId: PeerId, callId: Int64, accessHash: Int64, joinPayload: String) -> Signal { + return _internal_joinGroupCallAsScreencast(account: self.account, peerId: peerId, callId: callId, accessHash: accessHash, joinPayload: joinPayload) + } + + public func leaveGroupCallAsScreencast(callId: Int64, accessHash: Int64) -> Signal { + return _internal_leaveGroupCallAsScreencast(account: self.account, callId: callId, accessHash: accessHash) + } + + public func leaveGroupCall(callId: Int64, accessHash: Int64, source: UInt32) -> Signal { + return _internal_leaveGroupCall(account: self.account, callId: callId, accessHash: accessHash, source: source) + } + + public func stopGroupCall(peerId: PeerId, callId: Int64, accessHash: Int64) -> Signal { + return _internal_stopGroupCall(account: self.account, peerId: peerId, callId: callId, accessHash: accessHash) + } + + public func checkGroupCall(callId: Int64, accessHash: Int64, ssrcs: [UInt32]) -> Signal<[UInt32], NoError> { + return _internal_checkGroupCall(account: account, callId: callId, accessHash: accessHash, ssrcs: ssrcs) + } + + public func inviteToGroupCall(callId: Int64, accessHash: Int64, peerId: PeerId) -> Signal { + return _internal_inviteToGroupCall(account: self.account, callId: callId, accessHash: accessHash, peerId: peerId) + } + + public func groupCallInviteLinks(callId: Int64, accessHash: Int64) -> Signal { + return _internal_groupCallInviteLinks(account: self.account, callId: callId, accessHash: accessHash) + } + + public func editGroupCallTitle(callId: Int64, accessHash: Int64, title: String) -> Signal { + return _internal_editGroupCallTitle(account: self.account, callId: callId, accessHash: accessHash, title: title) + } + + /*public func groupCallDisplayAsAvailablePeers(peerId: PeerId) -> Signal<[FoundPeer], NoError> { + return _internal_groupCallDisplayAsAvailablePeers(network: self.account.network, postbox: self.account.postbox, peerId: peerId) + }*/ + + public func clearCachedGroupCallDisplayAsAvailablePeers(peerId: PeerId) -> Signal { + return _internal_clearCachedGroupCallDisplayAsAvailablePeers(account: self.account, peerId: peerId) + } + + public func cachedGroupCallDisplayAsAvailablePeers(peerId: PeerId) -> Signal<[FoundPeer], NoError> { + return _internal_cachedGroupCallDisplayAsAvailablePeers(account: self.account, peerId: peerId) + } + + public func updatedCurrentPeerGroupCall(peerId: PeerId) -> Signal { + return _internal_updatedCurrentPeerGroupCall(account: self.account, peerId: peerId) + } + + public func getAudioBroadcastDataSource(callId: Int64, accessHash: Int64) -> Signal { + return _internal_getAudioBroadcastDataSource(account: self.account, callId: callId, accessHash: accessHash) + } + + public func getAudioBroadcastPart(dataSource: AudioBroadcastDataSource, callId: Int64, accessHash: Int64, timestampIdMilliseconds: Int64, durationMilliseconds: Int64) -> Signal { + return _internal_getAudioBroadcastPart(dataSource: dataSource, callId: callId, accessHash: accessHash, timestampIdMilliseconds: timestampIdMilliseconds, durationMilliseconds: durationMilliseconds) + } + + public func groupCall(peerId: PeerId, myPeerId: PeerId, id: Int64, accessHash: Int64, state: GroupCallParticipantsContext.State, previousServiceState: GroupCallParticipantsContext.ServiceState?) -> GroupCallParticipantsContext { + return GroupCallParticipantsContext(account: self.account, peerId: peerId, myPeerId: myPeerId, id: id, accessHash: accessHash, state: state, previousServiceState: previousServiceState) + } + } +} diff --git a/submodules/TelegramCore/Sources/ContactManagement.swift b/submodules/TelegramCore/Sources/TelegramEngine/Contacts/ContactManagement.swift similarity index 95% rename from submodules/TelegramCore/Sources/ContactManagement.swift rename to submodules/TelegramCore/Sources/TelegramEngine/Contacts/ContactManagement.swift index f2bb32ffc9..43f9be29db 100644 --- a/submodules/TelegramCore/Sources/ContactManagement.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Contacts/ContactManagement.swift @@ -56,7 +56,7 @@ func syncContactsOnce(network: Network, postbox: Postbox, accountPeerId: PeerId) let contactPeerIds = transaction.getContactPeerIds() let totalCount = transaction.getRemoteContactCount() let peerIds = Set(contactPeerIds.filter({ $0.namespace == Namespaces.Peer.CloudUser })) - return hashForCountAndIds(count: totalCount, ids: peerIds.map({ $0.id }).sorted()) + return hashForCountAndIds(count: totalCount, ids: peerIds.map({ $0.id._internalGetInt32Value() }).sorted()) } let updatedPeers = initialContactPeerIdsHash @@ -105,7 +105,7 @@ func syncContactsOnce(network: Network, postbox: Postbox, accountPeerId: PeerId) return appliedUpdatedPeers } -public func deleteContactPeerInteractively(account: Account, peerId: PeerId) -> Signal { +func _internal_deleteContactPeerInteractively(account: Account, peerId: PeerId) -> Signal { return account.postbox.transaction { transaction -> Signal in if let peer = transaction.getPeer(peerId), let inputUser = apiInputUser(peer) { return account.network.request(Api.functions.contacts.deleteContacts(id: [inputUser])) @@ -133,7 +133,7 @@ public func deleteContactPeerInteractively(account: Account, peerId: PeerId) -> |> switchToLatest } -public func deleteAllContacts(account: Account) -> Signal { +func _internal_deleteAllContacts(account: Account) -> Signal { return account.postbox.transaction { transaction -> [Api.InputUser] in return transaction.getContactPeerIds().compactMap(transaction.getPeer).compactMap({ apiInputUser($0) }).compactMap({ $0 }) } @@ -166,7 +166,7 @@ public func deleteAllContacts(account: Account) -> Signal { } } -public func resetSavedContacts(network: Network) -> Signal { +func _internal_resetSavedContacts(network: Network) -> Signal { return network.request(Api.functions.contacts.resetSaved()) |> `catch` { _ -> Signal in return .single(.boolFalse) diff --git a/submodules/TelegramCore/Sources/DeviceContact.swift b/submodules/TelegramCore/Sources/TelegramEngine/Contacts/DeviceContact.swift similarity index 100% rename from submodules/TelegramCore/Sources/DeviceContact.swift rename to submodules/TelegramCore/Sources/TelegramEngine/Contacts/DeviceContact.swift diff --git a/submodules/TelegramCore/Sources/ImportContact.swift b/submodules/TelegramCore/Sources/TelegramEngine/Contacts/ImportContact.swift similarity index 90% rename from submodules/TelegramCore/Sources/ImportContact.swift rename to submodules/TelegramCore/Sources/TelegramEngine/Contacts/ImportContact.swift index 0783431a6d..5f3d5cf6bb 100644 --- a/submodules/TelegramCore/Sources/ImportContact.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Contacts/ImportContact.swift @@ -4,8 +4,7 @@ import SwiftSignalKit import SyncCore -public func importContact(account: Account, firstName: String, lastName: String, phoneNumber: String) -> Signal { - +func _internal_importContact(account: Account, firstName: String, lastName: String, phoneNumber: String) -> Signal { let input = Api.InputContact.inputPhoneContact(clientId: 1, phone: phoneNumber, firstName: firstName, lastName: lastName) return account.network.request(Api.functions.contacts.importContacts(contacts: [input])) @@ -42,7 +41,7 @@ public enum AddContactError { case generic } -public func addContactInteractively(account: Account, peerId: PeerId, firstName: String, lastName: String, phoneNumber: String, addToPrivacyExceptions: Bool) -> Signal { +func _internal_addContactInteractively(account: Account, peerId: PeerId, firstName: String, lastName: String, phoneNumber: String, addToPrivacyExceptions: Bool) -> Signal { return account.postbox.transaction { transaction -> (Api.InputUser, String)? in if let user = transaction.getPeer(peerId) as? TelegramUser, let inputUser = apiInputUser(user) { return (inputUser, user.phone == nil ? phoneNumber : "") @@ -99,7 +98,7 @@ public enum AcceptAndShareContactError { case generic } -public func acceptAndShareContact(account: Account, peerId: PeerId) -> Signal { +func _internal_acceptAndShareContact(account: Account, peerId: PeerId) -> Signal { return account.postbox.transaction { transaction -> Api.InputUser? in return transaction.getPeer(peerId).flatMap(apiInputUser) } diff --git a/submodules/TelegramCore/Sources/PhoneNumber.swift b/submodules/TelegramCore/Sources/TelegramEngine/Contacts/PhoneNumber.swift similarity index 100% rename from submodules/TelegramCore/Sources/PhoneNumber.swift rename to submodules/TelegramCore/Sources/TelegramEngine/Contacts/PhoneNumber.swift diff --git a/submodules/TelegramCore/Sources/TelegramDeviceContactImportInfo.swift b/submodules/TelegramCore/Sources/TelegramEngine/Contacts/TelegramDeviceContactImportInfo.swift similarity index 92% rename from submodules/TelegramCore/Sources/TelegramDeviceContactImportInfo.swift rename to submodules/TelegramCore/Sources/TelegramEngine/Contacts/TelegramDeviceContactImportInfo.swift index 7d02ab0fa2..0a82dd2dc1 100644 --- a/submodules/TelegramCore/Sources/TelegramDeviceContactImportInfo.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Contacts/TelegramDeviceContactImportInfo.swift @@ -48,7 +48,7 @@ enum TelegramDeviceContactImportIdentifier: Hashable, Comparable, Equatable { } } -public func deviceContactsImportedByCount(postbox: Postbox, contacts: [(String, [DeviceContactNormalizedPhoneNumber])]) -> Signal<[String: Int32], NoError> { +func _internal_deviceContactsImportedByCount(postbox: Postbox, contacts: [(String, [DeviceContactNormalizedPhoneNumber])]) -> Signal<[String: Int32], NoError> { return postbox.transaction { transaction -> [String: Int32] in var result: [String: Int32] = [:] for (id, numbers) in contacts { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Contacts/TelegramEngineContacts.swift b/submodules/TelegramCore/Sources/TelegramEngine/Contacts/TelegramEngineContacts.swift new file mode 100644 index 0000000000..172f879418 --- /dev/null +++ b/submodules/TelegramCore/Sources/TelegramEngine/Contacts/TelegramEngineContacts.swift @@ -0,0 +1,44 @@ +import SwiftSignalKit +import Postbox + +public extension TelegramEngine { + final class Contacts { + private let account: Account + + init(account: Account) { + self.account = account + } + + public func deleteContactPeerInteractively(peerId: PeerId) -> Signal { + return _internal_deleteContactPeerInteractively(account: self.account, peerId: peerId) + } + + public func deleteAllContacts() -> Signal { + return _internal_deleteAllContacts(account: self.account) + } + + public func resetSavedContacts() -> Signal { + return _internal_resetSavedContacts(network: self.account.network) + } + + public func updateContactName(peerId: PeerId, firstName: String, lastName: String) -> Signal { + return _internal_updateContactName(account: self.account, peerId: peerId, firstName: firstName, lastName: lastName) + } + + public func deviceContactsImportedByCount(contacts: [(String, [DeviceContactNormalizedPhoneNumber])]) -> Signal<[String: Int32], NoError> { + return _internal_deviceContactsImportedByCount(postbox: self.account.postbox, contacts: contacts) + } + + public func importContact(firstName: String, lastName: String, phoneNumber: String) -> Signal { + return _internal_importContact(account: self.account, firstName: firstName, lastName: lastName, phoneNumber: phoneNumber) + } + + public func addContactInteractively(peerId: PeerId, firstName: String, lastName: String, phoneNumber: String, addToPrivacyExceptions: Bool) -> Signal { + return _internal_addContactInteractively(account: self.account, peerId: peerId, firstName: firstName, lastName: lastName, phoneNumber: phoneNumber, addToPrivacyExceptions: addToPrivacyExceptions) + } + + public func acceptAndShareContact(peerId: PeerId) -> Signal { + return _internal_acceptAndShareContact(account: self.account, peerId: peerId) + } + } +} diff --git a/submodules/TelegramCore/Sources/UpdateContactName.swift b/submodules/TelegramCore/Sources/TelegramEngine/Contacts/UpdateContactName.swift similarity index 86% rename from submodules/TelegramCore/Sources/UpdateContactName.swift rename to submodules/TelegramCore/Sources/TelegramEngine/Contacts/UpdateContactName.swift index cb5ba9f76e..dd6939d74d 100644 --- a/submodules/TelegramCore/Sources/UpdateContactName.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Contacts/UpdateContactName.swift @@ -10,7 +10,7 @@ public enum UpdateContactNameError { case generic } -public func updateContactName(account: Account, peerId: PeerId, firstName: String, lastName: String) -> Signal { +func _internal_updateContactName(account: Account, peerId: PeerId, firstName: String, lastName: String) -> Signal { return account.postbox.transaction { transaction -> Signal in if let peer = transaction.getPeer(peerId) as? TelegramUser, let inputUser = apiInputUser(peer) { return account.network.request(Api.functions.contacts.addContact(flags: 0, id: inputUser, firstName: firstName, lastName: lastName, phone: "")) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/HistoryImport/TelegramEngineHistoryImport.swift b/submodules/TelegramCore/Sources/TelegramEngine/HistoryImport/TelegramEngineHistoryImport.swift new file mode 100644 index 0000000000..309c0673a0 --- /dev/null +++ b/submodules/TelegramCore/Sources/TelegramEngine/HistoryImport/TelegramEngineHistoryImport.swift @@ -0,0 +1,255 @@ +import Foundation +import SwiftSignalKit +import Postbox +import SyncCore +import TelegramApi + +public extension TelegramEngine { + final class HistoryImport { + private let account: Account + + init(account: Account) { + self.account = account + } + + public struct Session { + fileprivate var peerId: PeerId + fileprivate var inputPeer: Api.InputPeer + fileprivate var id: Int64 + } + + public enum InitImportError { + case generic + case chatAdminRequired + case invalidChatType + case userBlocked + case limitExceeded + } + + public enum ParsedInfo { + case privateChat(title: String?) + case group(title: String?) + case unknown(title: String?) + } + + public enum GetInfoError { + case generic + case parseError + } + + public func getInfo(header: String) -> Signal { + return self.account.network.request(Api.functions.messages.checkHistoryImport(importHead: header)) + |> mapError { _ -> GetInfoError in + return .generic + } + |> mapToSignal { result -> Signal in + switch result { + case let .historyImportParsed(flags, title): + if (flags & (1 << 0)) != 0 { + return .single(.privateChat(title: title)) + } else if (flags & (1 << 1)) != 0 { + return .single(.group(title: title)) + } else { + return .single(.unknown(title: title)) + } + } + } + } + + public func initSession(peerId: PeerId, file: TempBoxFile, mediaCount: Int32) -> Signal { + let account = self.account + return multipartUpload(network: self.account.network, postbox: self.account.postbox, source: .tempFile(file), encrypt: false, tag: nil, hintFileSize: nil, hintFileIsLarge: false, forceNoBigParts: true, useLargerParts: true, increaseParallelParts: true, useMultiplexedRequests: false, useCompression: true) + |> mapError { _ -> InitImportError in + return .generic + } + |> mapToSignal { result -> Signal in + switch result { + case let .inputFile(inputFile): + return account.postbox.transaction { transaction -> Api.InputPeer? in + return transaction.getPeer(peerId).flatMap(apiInputPeer) + } + |> castError(InitImportError.self) + |> mapToSignal { inputPeer -> Signal in + guard let inputPeer = inputPeer else { + return .fail(.generic) + } + return account.network.request(Api.functions.messages.initHistoryImport(peer: inputPeer, file: inputFile, mediaCount: mediaCount), automaticFloodWait: false) + |> mapError { error -> InitImportError in + if error.errorDescription == "CHAT_ADMIN_REQUIRED" { + return .chatAdminRequired + } else if error.errorDescription == "IMPORT_PEER_TYPE_INVALID" { + return .invalidChatType + } else if error.errorDescription == "USER_IS_BLOCKED" { + return .userBlocked + } else if error.errorDescription == "FLOOD_WAIT" { + return .limitExceeded + } else { + return .generic + } + } + |> map { result -> Session in + switch result { + case let .historyImport(id): + return Session(peerId: peerId, inputPeer: inputPeer, id: id) + } + } + } + case .progress: + return .complete() + case .inputSecretFile: + return .fail(.generic) + } + } + } + + public enum MediaType { + case photo + case file + case video + case sticker + case voice + } + + public enum UploadMediaError { + case generic + case chatAdminRequired + } + + public func uploadMedia(session: Session, file: TempBoxFile, disposeFileAfterDone: Bool, fileName: String, mimeType: String, type: MediaType) -> Signal { + var forceNoBigParts = true + guard let size = fileSize(file.path), size != 0 else { + return .single(1.0) + } + if size >= 30 * 1024 * 1024 { + forceNoBigParts = false + } + + let account = self.account + return multipartUpload(network: self.account.network, postbox: self.account.postbox, source: .tempFile(file), encrypt: false, tag: nil, hintFileSize: nil, hintFileIsLarge: false, forceNoBigParts: forceNoBigParts, useLargerParts: true, useMultiplexedRequests: true) + |> mapError { _ -> UploadMediaError in + return .generic + } + |> mapToSignal { result -> Signal in + let inputMedia: Api.InputMedia + switch result { + case let .inputFile(inputFile): + switch type { + case .photo: + inputMedia = .inputMediaUploadedPhoto(flags: 0, file: inputFile, stickers: nil, ttlSeconds: nil) + case .file, .video, .sticker, .voice: + var attributes: [Api.DocumentAttribute] = [] + attributes.append(.documentAttributeFilename(fileName: fileName)) + var resolvedMimeType = mimeType + switch type { + case .video: + resolvedMimeType = "video/mp4" + case .sticker: + resolvedMimeType = "image/webp" + case .voice: + resolvedMimeType = "audio/ogg" + default: + break + } + inputMedia = .inputMediaUploadedDocument(flags: 0, file: inputFile, thumb: nil, mimeType: resolvedMimeType, attributes: attributes, stickers: nil, ttlSeconds: nil) + } + case let .progress(value): + return .single(value) + case .inputSecretFile: + return .fail(.generic) + } + return account.network.request(Api.functions.messages.uploadImportedMedia(peer: session.inputPeer, importId: session.id, fileName: fileName, media: inputMedia)) + |> mapError { error -> UploadMediaError in + switch error.errorDescription { + case "CHAT_ADMIN_REQUIRED": + return .chatAdminRequired + default: + return .generic + } + } + |> mapToSignal { result -> Signal in + return .single(1.0) + } + |> afterDisposed { + if disposeFileAfterDone { + TempBox.shared.dispose(file) + } + } + } + } + + public enum StartImportError { + case generic + } + + public func startImport(session: Session) -> Signal { + return self.account.network.request(Api.functions.messages.startHistoryImport(peer: session.inputPeer, importId: session.id)) + |> mapError { _ -> StartImportError in + return .generic + } + |> mapToSignal { result -> Signal in + if case .boolTrue = result { + return .complete() + } else { + return .fail(.generic) + } + } + } + + public enum CheckPeerImportResult { + case allowed + case alert(String) + } + + public enum CheckPeerImportError { + case generic + case chatAdminRequired + case invalidChatType + case userBlocked + case limitExceeded + case notMutualContact + } + + public func checkPeerImport(peerId: PeerId) -> Signal { + let account = self.account + return self.account.postbox.transaction { transaction -> Peer? in + return transaction.getPeer(peerId) + } + |> castError(CheckPeerImportError.self) + |> mapToSignal { peer -> Signal in + guard let peer = peer else { + return .fail(.generic) + } + guard let inputPeer = apiInputPeer(peer) else { + return .fail(.generic) + } + + return account.network.request(Api.functions.messages.checkHistoryImportPeer(peer: inputPeer)) + |> mapError { error -> CheckPeerImportError in + if error.errorDescription == "CHAT_ADMIN_REQUIRED" { + return .chatAdminRequired + } else if error.errorDescription == "IMPORT_PEER_TYPE_INVALID" { + return .invalidChatType + } else if error.errorDescription == "USER_IS_BLOCKED" { + return .userBlocked + } else if error.errorDescription == "USER_NOT_MUTUAL_CONTACT" { + return .notMutualContact + } else if error.errorDescription == "FLOOD_WAIT" { + return .limitExceeded + } else { + return .generic + } + } + |> map { result -> CheckPeerImportResult in + switch result { + case let .checkedHistoryImportPeer(confirmText): + if confirmText.isEmpty { + return .allowed + } else { + return .alert(confirmText) + } + } + } + } + } + } +} diff --git a/submodules/TelegramCore/Sources/Countries.swift b/submodules/TelegramCore/Sources/TelegramEngine/Localization/Countries.swift similarity index 97% rename from submodules/TelegramCore/Sources/Countries.swift rename to submodules/TelegramCore/Sources/TelegramEngine/Localization/Countries.swift index 6ba3de2611..42fd67735d 100644 --- a/submodules/TelegramCore/Sources/Countries.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Localization/Countries.swift @@ -102,7 +102,7 @@ public final class CountriesList: PreferencesEntry, Equatable { } -public func getCountriesList(accountManager: AccountManager, network: Network, langCode: String?, forceUpdate: Bool = false) -> Signal<[Country], NoError> { +func _internal_getCountriesList(accountManager: AccountManager, network: Network, langCode: String?, forceUpdate: Bool = false) -> Signal<[Country], NoError> { let fetch: ([Country]?, Int32?) -> Signal<[Country], NoError> = { current, hash in return network.request(Api.functions.help.getCountriesList(langCode: langCode ?? "", hash: hash ?? 0)) |> retryRequest diff --git a/submodules/TelegramCore/Sources/LocalizationInfo.swift b/submodules/TelegramCore/Sources/TelegramEngine/Localization/LocalizationInfo.swift similarity index 100% rename from submodules/TelegramCore/Sources/LocalizationInfo.swift rename to submodules/TelegramCore/Sources/TelegramEngine/Localization/LocalizationInfo.swift diff --git a/submodules/TelegramCore/Sources/LocalizationListState.swift b/submodules/TelegramCore/Sources/TelegramEngine/Localization/LocalizationListState.swift similarity index 96% rename from submodules/TelegramCore/Sources/LocalizationListState.swift rename to submodules/TelegramCore/Sources/TelegramEngine/Localization/LocalizationListState.swift index 662173ff39..090e40a0cb 100644 --- a/submodules/TelegramCore/Sources/LocalizationListState.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Localization/LocalizationListState.swift @@ -51,7 +51,7 @@ func updateLocalizationListStateInteractively(transaction: Transaction, _ f: @es }) } -public func synchronizedLocalizationListState(postbox: Postbox, network: Network) -> Signal { +func _internal_synchronizedLocalizationListState(postbox: Postbox, network: Network) -> Signal { return network.request(Api.functions.langpack.getLanguages(langPack: "")) |> retryRequest |> mapToSignal { languages -> Signal in diff --git a/submodules/TelegramCore/Sources/LocalizationPreview.swift b/submodules/TelegramCore/Sources/TelegramEngine/Localization/LocalizationPreview.swift similarity index 76% rename from submodules/TelegramCore/Sources/LocalizationPreview.swift rename to submodules/TelegramCore/Sources/TelegramEngine/Localization/LocalizationPreview.swift index 919c74c013..b6aa82f737 100644 --- a/submodules/TelegramCore/Sources/LocalizationPreview.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Localization/LocalizationPreview.swift @@ -9,7 +9,7 @@ public enum RequestLocalizationPreviewError { case generic } -public func requestLocalizationPreview(network: Network, identifier: String) -> Signal { +func _internal_requestLocalizationPreview(network: Network, identifier: String) -> Signal { return network.request(Api.functions.langpack.getLanguage(langPack: "", langCode: identifier)) |> mapError { _ -> RequestLocalizationPreviewError in return .generic diff --git a/submodules/TelegramCore/Sources/Localizations.swift b/submodules/TelegramCore/Sources/TelegramEngine/Localization/Localizations.swift similarity index 85% rename from submodules/TelegramCore/Sources/Localizations.swift rename to submodules/TelegramCore/Sources/TelegramEngine/Localization/Localizations.swift index 4f6e4fdef6..fa98c35845 100644 --- a/submodules/TelegramCore/Sources/Localizations.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Localization/Localizations.swift @@ -5,14 +5,14 @@ import SwiftSignalKit import SyncCore -public func currentlySuggestedLocalization(network: Network, extractKeys: [String]) -> Signal { +func _internal_currentlySuggestedLocalization(network: Network, extractKeys: [String]) -> Signal { return network.request(Api.functions.help.getConfig()) |> retryRequest |> mapToSignal { result -> Signal in switch result { case let .config(config): if let suggestedLangCode = config.suggestedLangCode { - return suggestedLocalizationInfo(network: network, languageCode: suggestedLangCode, extractKeys: extractKeys) |> map(Optional.init) + return _internal_suggestedLocalizationInfo(network: network, languageCode: suggestedLangCode, extractKeys: extractKeys) |> map(Optional.init) } else { return .single(nil) } @@ -20,7 +20,7 @@ public func currentlySuggestedLocalization(network: Network, extractKeys: [Strin } } -public func suggestedLocalizationInfo(network: Network, languageCode: String, extractKeys: [String]) -> Signal { +func _internal_suggestedLocalizationInfo(network: Network, languageCode: String, extractKeys: [String]) -> Signal { return combineLatest(network.request(Api.functions.langpack.getLanguages(langPack: "")), network.request(Api.functions.langpack.getStrings(langPack: "", langCode: languageCode, keys: extractKeys))) |> retryRequest |> map { languages, strings -> SuggestedLocalizationInfo in @@ -40,7 +40,7 @@ public func suggestedLocalizationInfo(network: Network, languageCode: String, ex } } -public func availableLocalizations(postbox: Postbox, network: Network, allowCached: Bool) -> Signal<[LocalizationInfo], NoError> { +func _internal_availableLocalizations(postbox: Postbox, network: Network, allowCached: Bool) -> Signal<[LocalizationInfo], NoError> { let cached: Signal<[LocalizationInfo], NoError> if allowCached { cached = postbox.transaction { transaction -> Signal<[LocalizationInfo], NoError> in @@ -69,7 +69,7 @@ public enum DownloadLocalizationError { case generic } -public func downloadLocalization(network: Network, languageCode: String) -> Signal { +func _internal_downloadLocalization(network: Network, languageCode: String) -> Signal { return network.request(Api.functions.langpack.getLangPack(langPack: "", langCode: languageCode)) |> mapError { _ -> DownloadLocalizationError in return .generic @@ -100,16 +100,16 @@ public enum DownloadAndApplyLocalizationError { case generic } -public func downloadAndApplyLocalization(accountManager: AccountManager, postbox: Postbox, network: Network, languageCode: String) -> Signal { - return requestLocalizationPreview(network: network, identifier: languageCode) +func _internal_downloadAndApplyLocalization(accountManager: AccountManager, postbox: Postbox, network: Network, languageCode: String) -> Signal { + return _internal_requestLocalizationPreview(network: network, identifier: languageCode) |> mapError { _ -> DownloadAndApplyLocalizationError in return .generic } |> mapToSignal { preview -> Signal in var primaryAndSecondaryLocalizations: [Signal] = [] - primaryAndSecondaryLocalizations.append(downloadLocalization(network: network, languageCode: preview.languageCode)) + primaryAndSecondaryLocalizations.append(_internal_downloadLocalization(network: network, languageCode: preview.languageCode)) if let secondaryCode = preview.baseLanguageCode { - primaryAndSecondaryLocalizations.append(downloadLocalization(network: network, languageCode: secondaryCode)) + primaryAndSecondaryLocalizations.append(_internal_downloadLocalization(network: network, languageCode: secondaryCode)) } return combineLatest(primaryAndSecondaryLocalizations) |> mapError { _ -> DownloadAndApplyLocalizationError in diff --git a/submodules/TelegramCore/Sources/SuggestedLocalizationEntry.swift b/submodules/TelegramCore/Sources/TelegramEngine/Localization/SuggestedLocalizationEntry.swift similarity index 83% rename from submodules/TelegramCore/Sources/SuggestedLocalizationEntry.swift rename to submodules/TelegramCore/Sources/TelegramEngine/Localization/SuggestedLocalizationEntry.swift index 6ba5d1d739..f5e225ab4c 100644 --- a/submodules/TelegramCore/Sources/SuggestedLocalizationEntry.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Localization/SuggestedLocalizationEntry.swift @@ -4,7 +4,7 @@ import SwiftSignalKit import SyncCore -public func markSuggestedLocalizationAsSeenInteractively(postbox: Postbox, languageCode: String) -> Signal { +func _internal_markSuggestedLocalizationAsSeenInteractively(postbox: Postbox, languageCode: String) -> Signal { return postbox.transaction { transaction -> Void in transaction.updatePreferencesEntry(key: PreferencesKeys.suggestedLocalization, { current in if let current = current as? SuggestedLocalizationEntry { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Localization/TelegramEngineLocalization.swift b/submodules/TelegramCore/Sources/TelegramEngine/Localization/TelegramEngineLocalization.swift new file mode 100644 index 0000000000..699ffb7b3f --- /dev/null +++ b/submodules/TelegramCore/Sources/TelegramEngine/Localization/TelegramEngineLocalization.swift @@ -0,0 +1,63 @@ +import SwiftSignalKit +import Postbox +import SyncCore + +public extension TelegramEngine { + final class Localization { + private let account: Account + + init(account: Account) { + self.account = account + } + + public func getCountriesList(accountManager: AccountManager, langCode: String?, forceUpdate: Bool = false) -> Signal<[Country], NoError> { + return _internal_getCountriesList(accountManager: accountManager, network: self.account.network, langCode: langCode, forceUpdate: forceUpdate) + } + + public func markSuggestedLocalizationAsSeenInteractively(languageCode: String) -> Signal { + return _internal_markSuggestedLocalizationAsSeenInteractively(postbox: self.account.postbox, languageCode: languageCode) + } + + public func synchronizedLocalizationListState() -> Signal { + return _internal_synchronizedLocalizationListState(postbox: self.account.postbox, network: self.account.network) + } + + public func suggestedLocalizationInfo(languageCode: String, extractKeys: [String]) -> Signal { + return _internal_suggestedLocalizationInfo(network: self.account.network, languageCode: languageCode, extractKeys: extractKeys) + } + + public func requestLocalizationPreview(identifier: String) -> Signal { + return _internal_requestLocalizationPreview(network: self.account.network, identifier: identifier) + } + + public func downloadAndApplyLocalization(accountManager: AccountManager, languageCode: String) -> Signal { + return _internal_downloadAndApplyLocalization(accountManager: accountManager, postbox: self.account.postbox, network: self.account.network, languageCode: languageCode) + } + } +} + +public extension TelegramEngineUnauthorized { + final class Localization { + private let account: UnauthorizedAccount + + init(account: UnauthorizedAccount) { + self.account = account + } + + public func getCountriesList(accountManager: AccountManager, langCode: String?, forceUpdate: Bool = false) -> Signal<[Country], NoError> { + return _internal_getCountriesList(accountManager: accountManager, network: self.account.network, langCode: langCode, forceUpdate: forceUpdate) + } + + public func markSuggestedLocalizationAsSeenInteractively(languageCode: String) -> Signal { + return _internal_markSuggestedLocalizationAsSeenInteractively(postbox: self.account.postbox, languageCode: languageCode) + } + + public func currentlySuggestedLocalization(extractKeys: [String]) -> Signal { + return _internal_currentlySuggestedLocalization(network: self.account.network, extractKeys: extractKeys) + } + + public func downloadAndApplyLocalization(accountManager: AccountManager, languageCode: String) -> Signal { + return _internal_downloadAndApplyLocalization(accountManager: accountManager, postbox: self.account.postbox, network: self.account.network, languageCode: languageCode) + } + } +} diff --git a/submodules/TelegramCore/Sources/ApplyMaxReadIndexInteractively.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/ApplyMaxReadIndexInteractively.swift similarity index 96% rename from submodules/TelegramCore/Sources/ApplyMaxReadIndexInteractively.swift rename to submodules/TelegramCore/Sources/TelegramEngine/Messages/ApplyMaxReadIndexInteractively.swift index c00e1ca67f..ea804adba5 100644 --- a/submodules/TelegramCore/Sources/ApplyMaxReadIndexInteractively.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/ApplyMaxReadIndexInteractively.swift @@ -5,13 +5,13 @@ import SwiftSignalKit import SyncCore -public func applyMaxReadIndexInteractively(postbox: Postbox, stateManager: AccountStateManager, index: MessageIndex) -> Signal { +func _internal_applyMaxReadIndexInteractively(postbox: Postbox, stateManager: AccountStateManager, index: MessageIndex) -> Signal { return postbox.transaction { transaction -> Void in - applyMaxReadIndexInteractively(transaction: transaction, stateManager: stateManager, index: index) + _internal_applyMaxReadIndexInteractively(transaction: transaction, stateManager: stateManager, index: index) } } -func applyMaxReadIndexInteractively(transaction: Transaction, stateManager: AccountStateManager, index: MessageIndex) { +func _internal_applyMaxReadIndexInteractively(transaction: Transaction, stateManager: AccountStateManager, index: MessageIndex) { let messageIds = transaction.applyInteractiveReadMaxIndex(index) if index.id.peerId.namespace == Namespaces.Peer.SecretChat { let timestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) diff --git a/submodules/TelegramCore/Sources/ClearCloudDrafts.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/ClearCloudDrafts.swift similarity index 96% rename from submodules/TelegramCore/Sources/ClearCloudDrafts.swift rename to submodules/TelegramCore/Sources/TelegramEngine/Messages/ClearCloudDrafts.swift index 7a5038ce2e..4f1942760b 100644 --- a/submodules/TelegramCore/Sources/ClearCloudDrafts.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/ClearCloudDrafts.swift @@ -5,7 +5,7 @@ import TelegramApi import SyncCore -public func clearCloudDraftsInteractively(postbox: Postbox, network: Network, accountPeerId: PeerId) -> Signal { +func _internal_clearCloudDraftsInteractively(postbox: Postbox, network: Network, accountPeerId: PeerId) -> Signal { return network.request(Api.functions.messages.getAllDrafts()) |> retryRequest |> mapToSignal { updates -> Signal in diff --git a/submodules/TelegramCore/Sources/DeleteMessages.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/DeleteMessages.swift similarity index 87% rename from submodules/TelegramCore/Sources/DeleteMessages.swift rename to submodules/TelegramCore/Sources/TelegramEngine/Messages/DeleteMessages.swift index d432d2f604..fc69b68f80 100644 --- a/submodules/TelegramCore/Sources/DeleteMessages.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/DeleteMessages.swift @@ -23,7 +23,7 @@ func addMessageMediaResourceIdsToRemove(message: Message, resourceIds: inout [Wr } } -public func deleteMessages(transaction: Transaction, mediaBox: MediaBox, ids: [MessageId], deleteMedia: Bool = true, manualAddMessageThreadStatsDifference: ((MessageId, Int, Int) -> Void)? = nil) { +func _internal_deleteMessages(transaction: Transaction, mediaBox: MediaBox, ids: [MessageId], deleteMedia: Bool = true, manualAddMessageThreadStatsDifference: ((MessageId, Int, Int) -> Void)? = nil) { var resourceIds: [WrappedMediaResourceId] = [] if deleteMedia { for id in ids { @@ -35,7 +35,7 @@ public func deleteMessages(transaction: Transaction, mediaBox: MediaBox, ids: [M } } if !resourceIds.isEmpty { - let _ = mediaBox.removeCachedResources(Set(resourceIds)).start() + let _ = mediaBox.removeCachedResources(Set(resourceIds), force: true).start() } for id in ids { if id.peerId.namespace == Namespaces.Peer.CloudChannel && id.namespace == Namespaces.Message.Cloud { @@ -57,7 +57,7 @@ public func deleteMessages(transaction: Transaction, mediaBox: MediaBox, ids: [M }) } -public func deleteAllMessagesWithAuthor(transaction: Transaction, mediaBox: MediaBox, peerId: PeerId, authorId: PeerId, namespace: MessageId.Namespace) { +func _internal_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 addMessageMediaResourceIdsToRemove(media: media, resourceIds: &resourceIds) @@ -67,17 +67,17 @@ public func deleteAllMessagesWithAuthor(transaction: Transaction, mediaBox: Medi } } -public func deleteAllMessagesWithForwardAuthor(transaction: Transaction, mediaBox: MediaBox, peerId: PeerId, forwardAuthorId: PeerId, namespace: MessageId.Namespace) { +func _internal_deleteAllMessagesWithForwardAuthor(transaction: Transaction, mediaBox: MediaBox, peerId: PeerId, forwardAuthorId: PeerId, namespace: MessageId.Namespace) { var resourceIds: [WrappedMediaResourceId] = [] transaction.removeAllMessagesWithForwardAuthor(peerId, forwardAuthorId: forwardAuthorId, namespace: namespace, forEachMedia: { media in addMessageMediaResourceIdsToRemove(media: media, resourceIds: &resourceIds) }) if !resourceIds.isEmpty { - let _ = mediaBox.removeCachedResources(Set(resourceIds)).start() + let _ = mediaBox.removeCachedResources(Set(resourceIds), force: true).start() } } -public func clearHistory(transaction: Transaction, mediaBox: MediaBox, peerId: PeerId, namespaces: MessageIdNamespaces) { +func _internal_clearHistory(transaction: Transaction, mediaBox: MediaBox, peerId: PeerId, namespaces: MessageIdNamespaces) { if peerId.namespace == Namespaces.Peer.SecretChat { var resourceIds: [WrappedMediaResourceId] = [] transaction.withAllMessages(peerId: peerId, { message in @@ -85,7 +85,7 @@ public func clearHistory(transaction: Transaction, mediaBox: MediaBox, peerId: P return true }) if !resourceIds.isEmpty { - let _ = mediaBox.removeCachedResources(Set(resourceIds)).start() + let _ = mediaBox.removeCachedResources(Set(resourceIds), force: true).start() } } transaction.clearHistory(peerId, namespaces: namespaces, forEachMedia: { _ in @@ -96,7 +96,7 @@ public enum ClearCallHistoryError { case generic } -public func clearCallHistory(account: Account, forEveryone: Bool) -> Signal { +func _internal_clearCallHistory(account: Account, forEveryone: Bool) -> Signal { return account.postbox.transaction { transaction -> Signal in var flags: Int32 = 0 if forEveryone { @@ -146,7 +146,7 @@ public enum SetChatMessageAutoremoveTimeoutError { case generic } -public func setChatMessageAutoremoveTimeoutInteractively(account: Account, peerId: PeerId, timeout: Int32?) -> Signal { +func _internal_setChatMessageAutoremoveTimeoutInteractively(account: Account, peerId: PeerId, timeout: Int32?) -> Signal { return account.postbox.transaction { transaction -> Api.InputPeer? in return transaction.getPeer(peerId).flatMap(apiInputPeer) } diff --git a/submodules/TelegramCore/Sources/DeleteMessagesInteractively.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/DeleteMessagesInteractively.swift similarity index 85% rename from submodules/TelegramCore/Sources/DeleteMessagesInteractively.swift rename to submodules/TelegramCore/Sources/TelegramEngine/Messages/DeleteMessagesInteractively.swift index e5f8730294..4dd24abec2 100644 --- a/submodules/TelegramCore/Sources/DeleteMessagesInteractively.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/DeleteMessagesInteractively.swift @@ -6,7 +6,7 @@ import MtProtoKit import SyncCore -public func deleteMessagesInteractively(account: Account, messageIds: [MessageId], type: InteractiveMessagesDeletionType, deleteAllInGroup: Bool = false) -> Signal { +func _internal_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) } @@ -80,7 +80,7 @@ func deleteMessagesInteractively(transaction: Transaction, stateManager: Account globallyUniqueIds.append(globallyUniqueId) } } - let updatedState = addSecretChatOutgoingOperation(transaction: transaction, peerId: peerId, operation: SecretChatOutgoingOperationContents.deleteMessages(layer: layer, actionGloballyUniqueId: arc4random64(), globallyUniqueIds: globallyUniqueIds), state: state) + let updatedState = addSecretChatOutgoingOperation(transaction: transaction, peerId: peerId, operation: SecretChatOutgoingOperationContents.deleteMessages(layer: layer, actionGloballyUniqueId: Int64.random(in: Int64.min ... Int64.max), globallyUniqueIds: globallyUniqueIds), state: state) if updatedState != state { transaction.setPeerChatState(peerId, state: updatedState) } @@ -88,7 +88,7 @@ func deleteMessagesInteractively(transaction: Transaction, stateManager: Account } } } - deleteMessages(transaction: transaction, mediaBox: postbox.mediaBox, ids: messageIds) + _internal_deleteMessages(transaction: transaction, mediaBox: postbox.mediaBox, ids: messageIds) stateManager?.notifyDeletedMessages(messageIds: messageIds) @@ -97,22 +97,22 @@ func deleteMessagesInteractively(transaction: Transaction, stateManager: Account } } -public func clearHistoryInteractively(postbox: Postbox, peerId: PeerId, type: InteractiveHistoryClearingType) -> Signal { +func _internal_clearHistoryInteractively(postbox: Postbox, peerId: PeerId, type: InteractiveHistoryClearingType) -> Signal { return postbox.transaction { transaction -> Void in if peerId.namespace == Namespaces.Peer.CloudUser || peerId.namespace == Namespaces.Peer.CloudGroup || peerId.namespace == Namespaces.Peer.CloudChannel { cloudChatAddClearHistoryOperation(transaction: transaction, peerId: peerId, explicitTopMessageId: nil, type: CloudChatClearHistoryType(type)) if type == .scheduledMessages { - clearHistory(transaction: transaction, mediaBox: postbox.mediaBox, peerId: peerId, namespaces: .just(Namespaces.Message.allScheduled)) + _internal_clearHistory(transaction: transaction, mediaBox: postbox.mediaBox, peerId: peerId, namespaces: .just(Namespaces.Message.allScheduled)) } else { var topIndex: MessageIndex? if let topMessageId = transaction.getTopPeerMessageId(peerId: peerId, namespace: Namespaces.Message.Cloud), let topMessage = transaction.getMessage(topMessageId) { topIndex = topMessage.index } - clearHistory(transaction: transaction, mediaBox: postbox.mediaBox, peerId: peerId, namespaces: .not(Namespaces.Message.allScheduled)) + _internal_clearHistory(transaction: transaction, mediaBox: postbox.mediaBox, peerId: peerId, namespaces: .not(Namespaces.Message.allScheduled)) if let cachedData = transaction.getPeerCachedData(peerId: peerId) as? CachedChannelData, let migrationReference = cachedData.migrationReference { cloudChatAddClearHistoryOperation(transaction: transaction, peerId: migrationReference.maxMessageId.peerId, explicitTopMessageId: MessageId(peerId: migrationReference.maxMessageId.peerId, namespace: migrationReference.maxMessageId.namespace, id: migrationReference.maxMessageId.id + 1), type: CloudChatClearHistoryType(type)) - clearHistory(transaction: transaction, mediaBox: postbox.mediaBox, peerId: migrationReference.maxMessageId.peerId, namespaces: .all) + _internal_clearHistory(transaction: transaction, mediaBox: postbox.mediaBox, peerId: migrationReference.maxMessageId.peerId, namespaces: .all) } if let topIndex = topIndex { if peerId.namespace == Namespaces.Peer.CloudUser { @@ -123,7 +123,7 @@ public func clearHistoryInteractively(postbox: Postbox, peerId: PeerId, type: In } } } else if peerId.namespace == Namespaces.Peer.SecretChat { - clearHistory(transaction: transaction, mediaBox: postbox.mediaBox, peerId: peerId, namespaces: .all) + _internal_clearHistory(transaction: transaction, mediaBox: postbox.mediaBox, peerId: peerId, namespaces: .all) if let state = transaction.getPeerChatState(peerId) as? SecretChatState { var layer: SecretChatLayer? @@ -137,7 +137,7 @@ public func clearHistoryInteractively(postbox: Postbox, peerId: PeerId, type: In } if let layer = layer { - let updatedState = addSecretChatOutgoingOperation(transaction: transaction, peerId: peerId, operation: SecretChatOutgoingOperationContents.clearHistory(layer: layer, actionGloballyUniqueId: arc4random64()), state: state) + let updatedState = addSecretChatOutgoingOperation(transaction: transaction, peerId: peerId, operation: SecretChatOutgoingOperationContents.clearHistory(layer: layer, actionGloballyUniqueId: Int64.random(in: Int64.min ... Int64.max)), state: state) if updatedState != state { transaction.setPeerChatState(peerId, state: updatedState) } @@ -147,7 +147,7 @@ public func clearHistoryInteractively(postbox: Postbox, peerId: PeerId, type: In } } -public func clearAuthorHistory(account: Account, peerId: PeerId, memberId: PeerId) -> Signal { +func _internal_clearAuthorHistory(account: Account, peerId: PeerId, memberId: PeerId) -> Signal { return account.postbox.transaction { transaction -> Signal in if let peer = transaction.getPeer(peerId), let memberPeer = transaction.getPeer(memberId), let inputChannel = apiInputChannel(peer), let inputUser = apiInputUser(memberPeer) { @@ -178,7 +178,7 @@ public func clearAuthorHistory(account: Account, peerId: PeerId, memberId: PeerI |> `catch` { success -> Signal in if success { return account.postbox.transaction { transaction -> Void in - deleteAllMessagesWithAuthor(transaction: transaction, mediaBox: account.postbox.mediaBox, peerId: peerId, authorId: memberId, namespace: Namespaces.Message.Cloud) + _internal_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/TelegramEngine/Messages/EarliestUnseenPersonalMentionMessage.swift similarity index 95% rename from submodules/TelegramCore/Sources/EarliestUnseenPersonalMentionMessage.swift rename to submodules/TelegramCore/Sources/TelegramEngine/Messages/EarliestUnseenPersonalMentionMessage.swift index 54c5a0b320..d3aa62b2bf 100644 --- a/submodules/TelegramCore/Sources/EarliestUnseenPersonalMentionMessage.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/EarliestUnseenPersonalMentionMessage.swift @@ -10,7 +10,7 @@ public enum EarliestUnseenPersonalMentionMessageResult: Equatable { case result(MessageId?) } -public func earliestUnseenPersonalMentionMessage(account: Account, peerId: PeerId) -> Signal { +func _internal_earliestUnseenPersonalMentionMessage(account: Account, peerId: PeerId) -> Signal { return account.viewTracker.aroundMessageHistoryViewForLocation(.peer(peerId), index: .lowerBound, anchorIndex: .lowerBound, count: 4, fixedCombinedReadStates: nil, tagMask: .unseenPersonalMessage, additionalData: [.peerChatState(peerId)]) |> mapToSignal { view -> Signal in if view.0.isLoading { diff --git a/submodules/TelegramCore/Sources/ExportMessageLink.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/ExportMessageLink.swift similarity index 89% rename from submodules/TelegramCore/Sources/ExportMessageLink.swift rename to submodules/TelegramCore/Sources/TelegramEngine/Messages/ExportMessageLink.swift index 2d32877418..66c64fce1e 100644 --- a/submodules/TelegramCore/Sources/ExportMessageLink.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/ExportMessageLink.swift @@ -3,7 +3,7 @@ import Postbox import TelegramApi import SwiftSignalKit -public func exportMessageLink(account: Account, peerId: PeerId, messageId: MessageId, isThread: Bool = false) -> Signal { +func _internal_exportMessageLink(account: Account, peerId: PeerId, messageId: MessageId, isThread: Bool = false) -> Signal { return account.postbox.transaction { transaction -> (Peer, MessageId)? in var peer: Peer? = transaction.getPeer(messageId.peerId) if let peer = peer { diff --git a/submodules/TelegramCore/Sources/ForwardGame.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/ForwardGame.swift similarity index 80% rename from submodules/TelegramCore/Sources/ForwardGame.swift rename to submodules/TelegramCore/Sources/TelegramEngine/Messages/ForwardGame.swift index 6627125458..7a592178fa 100644 --- a/submodules/TelegramCore/Sources/ForwardGame.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/ForwardGame.swift @@ -3,10 +3,10 @@ import Postbox import TelegramApi import SwiftSignalKit -public func forwardGameWithScore(account: Account, messageId: MessageId, to peerId: PeerId) -> Signal { +func _internal_forwardGameWithScore(account: Account, messageId: MessageId, to peerId: PeerId) -> Signal { return account.postbox.transaction { transaction -> Signal in if let _ = transaction.getMessage(messageId), let fromPeer = transaction.getPeer(messageId.peerId), let fromInputPeer = apiInputPeer(fromPeer), let toPeer = transaction.getPeer(peerId), let toInputPeer = apiInputPeer(toPeer) { - return account.network.request(Api.functions.messages.forwardMessages(flags: 1 << 8, fromPeer: fromInputPeer, id: [messageId.id], randomId: [arc4random64()], toPeer: toInputPeer, scheduleDate: nil)) + return account.network.request(Api.functions.messages.forwardMessages(flags: 1 << 8, fromPeer: fromInputPeer, id: [messageId.id], randomId: [Int64.random(in: Int64.min ... Int64.max)], toPeer: toInputPeer, scheduleDate: nil)) |> map(Optional.init) |> `catch` { _ -> Signal in return .single(nil) diff --git a/submodules/TelegramCore/Sources/InstallInteractiveReadMessagesAction.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/InstallInteractiveReadMessagesAction.swift similarity index 92% rename from submodules/TelegramCore/Sources/InstallInteractiveReadMessagesAction.swift rename to submodules/TelegramCore/Sources/TelegramEngine/Messages/InstallInteractiveReadMessagesAction.swift index ec862edad0..4b853a0b0f 100644 --- a/submodules/TelegramCore/Sources/InstallInteractiveReadMessagesAction.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/InstallInteractiveReadMessagesAction.swift @@ -5,7 +5,7 @@ import SwiftSignalKit import SyncCore -public func installInteractiveReadMessagesAction(postbox: Postbox, stateManager: AccountStateManager, peerId: PeerId) -> Disposable { +func _internal_installInteractiveReadMessagesAction(postbox: Postbox, stateManager: AccountStateManager, peerId: PeerId) -> Disposable { return postbox.installStoreMessageAction(peerId: peerId, { messages, transaction in var consumeMessageIds: [MessageId] = [] @@ -56,7 +56,7 @@ public func installInteractiveReadMessagesAction(postbox: Postbox, stateManager: } for (_, index) in readMessageIndexByNamespace { - applyMaxReadIndexInteractively(transaction: transaction, stateManager: stateManager, index: index) + _internal_applyMaxReadIndexInteractively(transaction: transaction, stateManager: stateManager, index: index) } }) } diff --git a/submodules/TelegramCore/Sources/LoadMessagesIfNecessary.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/LoadMessagesIfNecessary.swift similarity index 96% rename from submodules/TelegramCore/Sources/LoadMessagesIfNecessary.swift rename to submodules/TelegramCore/Sources/TelegramEngine/Messages/LoadMessagesIfNecessary.swift index 66ffa33865..f2e4bd052f 100644 --- a/submodules/TelegramCore/Sources/LoadMessagesIfNecessary.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/LoadMessagesIfNecessary.swift @@ -11,7 +11,7 @@ public enum GetMessagesStrategy { case cloud } -public func getMessagesLoadIfNecessary(_ messageIds: [MessageId], postbox: Postbox, network: Network, accountPeerId: PeerId, strategy: GetMessagesStrategy = .cloud) -> Signal <[Message], NoError> { +func _internal_getMessagesLoadIfNecessary(_ messageIds: [MessageId], postbox: Postbox, network: Network, accountPeerId: PeerId, strategy: GetMessagesStrategy = .cloud) -> Signal <[Message], NoError> { let postboxSignal = postbox.transaction { transaction -> ([Message], Set, SimpleDictionary) in var ids = messageIds diff --git a/submodules/TelegramCore/Sources/MarkAllChatsAsRead.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/MarkAllChatsAsRead.swift similarity index 96% rename from submodules/TelegramCore/Sources/MarkAllChatsAsRead.swift rename to submodules/TelegramCore/Sources/TelegramEngine/Messages/MarkAllChatsAsRead.swift index 8a3481c9e5..69dbd159a7 100644 --- a/submodules/TelegramCore/Sources/MarkAllChatsAsRead.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/MarkAllChatsAsRead.swift @@ -6,7 +6,7 @@ import MtProtoKit import SyncCore -public func markAllChatsAsRead(postbox: Postbox, network: Network, stateManager: AccountStateManager) -> Signal { +func _internal_markAllChatsAsRead(postbox: Postbox, network: Network, stateManager: AccountStateManager) -> Signal { return network.request(Api.functions.messages.getDialogUnreadMarks()) |> map(Optional.init) |> `catch` { _ -> Signal<[Api.DialogPeer]?, NoError> in diff --git a/submodules/TelegramCore/Sources/MarkMessageContentAsConsumedInteractively.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/MarkMessageContentAsConsumedInteractively.swift similarity index 96% rename from submodules/TelegramCore/Sources/MarkMessageContentAsConsumedInteractively.swift rename to submodules/TelegramCore/Sources/TelegramEngine/Messages/MarkMessageContentAsConsumedInteractively.swift index c302433985..7a39d5bb17 100644 --- a/submodules/TelegramCore/Sources/MarkMessageContentAsConsumedInteractively.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/MarkMessageContentAsConsumedInteractively.swift @@ -5,7 +5,7 @@ import SwiftSignalKit import SyncCore -public func markMessageContentAsConsumedInteractively(postbox: Postbox, messageId: MessageId) -> Signal { +func _internal_markMessageContentAsConsumedInteractively(postbox: Postbox, messageId: MessageId) -> Signal { return postbox.transaction { transaction -> Void in if let message = transaction.getMessage(messageId), message.flags.contains(.Incoming) { var updateMessage = false @@ -32,7 +32,7 @@ public func markMessageContentAsConsumedInteractively(postbox: Postbox, messageI var globallyUniqueIds: [Int64] = [] if let globallyUniqueId = message.globallyUniqueId { globallyUniqueIds.append(globallyUniqueId) - let updatedState = addSecretChatOutgoingOperation(transaction: transaction, peerId: message.id.peerId, operation: SecretChatOutgoingOperationContents.readMessagesContent(layer: layer, actionGloballyUniqueId: arc4random64(), globallyUniqueIds: globallyUniqueIds), state: state) + let updatedState = addSecretChatOutgoingOperation(transaction: transaction, peerId: message.id.peerId, operation: SecretChatOutgoingOperationContents.readMessagesContent(layer: layer, actionGloballyUniqueId: Int64.random(in: Int64.min ... Int64.max), globallyUniqueIds: globallyUniqueIds), state: state) if updatedState != state { transaction.setPeerChatState(message.id.peerId, state: updatedState) } @@ -75,7 +75,7 @@ public func markMessageContentAsConsumedInteractively(postbox: Postbox, messageI } if let state = state, let layer = layer, let globallyUniqueId = message.globallyUniqueId { - let updatedState = addSecretChatOutgoingOperation(transaction: transaction, peerId: messageId.peerId, operation: .readMessagesContent(layer: layer, actionGloballyUniqueId: arc4random64(), globallyUniqueIds: [globallyUniqueId]), state: state) + let updatedState = addSecretChatOutgoingOperation(transaction: transaction, peerId: messageId.peerId, operation: .readMessagesContent(layer: layer, actionGloballyUniqueId: Int64.random(in: Int64.min ... Int64.max), globallyUniqueIds: [globallyUniqueId]), state: state) if updatedState != state { transaction.setPeerChatState(messageId.peerId, state: updatedState) } @@ -106,7 +106,7 @@ public func markMessageContentAsConsumedInteractively(postbox: Postbox, messageI } if let state = state, let layer = layer, let globallyUniqueId = message.globallyUniqueId { - let updatedState = addSecretChatOutgoingOperation(transaction: transaction, peerId: messageId.peerId, operation: .readMessagesContent(layer: layer, actionGloballyUniqueId: arc4random64(), globallyUniqueIds: [globallyUniqueId]), state: state) + let updatedState = addSecretChatOutgoingOperation(transaction: transaction, peerId: messageId.peerId, operation: .readMessagesContent(layer: layer, actionGloballyUniqueId: Int64.random(in: Int64.min ... Int64.max), globallyUniqueIds: [globallyUniqueId]), state: state) if updatedState != state { transaction.setPeerChatState(messageId.peerId, state: updatedState) } diff --git a/submodules/TelegramCore/Sources/OutgoingMessageWithChatContextResult.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/OutgoingMessageWithChatContextResult.swift similarity index 75% rename from submodules/TelegramCore/Sources/OutgoingMessageWithChatContextResult.swift rename to submodules/TelegramCore/Sources/TelegramEngine/Messages/OutgoingMessageWithChatContextResult.swift index c800fb9793..51b5b930f5 100644 --- a/submodules/TelegramCore/Sources/OutgoingMessageWithChatContextResult.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/OutgoingMessageWithChatContextResult.swift @@ -3,8 +3,15 @@ import Postbox import SwiftSignalKit import SyncCore +func _internal_enqueueOutgoingMessageWithChatContextResult(account: Account, to peerId: PeerId, results: ChatContextResultCollection, result: ChatContextResult, replyToMessageId: MessageId?, hideVia: Bool, silentPosting: Bool, scheduleTime: Int32?, correlationId: Int64?) -> Bool { + guard let message = outgoingMessageWithChatContextResult(to: peerId, results: results, result: result, replyToMessageId: replyToMessageId, hideVia: hideVia, silentPosting: silentPosting, scheduleTime: scheduleTime, correlationId: correlationId) else { + return false + } + let _ = enqueueMessages(account: account, peerId: peerId, messages: [message]).start() + return true +} -public func outgoingMessageWithChatContextResult(to peerId: PeerId, results: ChatContextResultCollection, result: ChatContextResult, hideVia: Bool = false, scheduleTime: Int32? = nil) -> EnqueueMessage? { +private func outgoingMessageWithChatContextResult(to peerId: PeerId, results: ChatContextResultCollection, result: ChatContextResult, replyToMessageId: MessageId?, hideVia: Bool, silentPosting: Bool, scheduleTime: Int32?, correlationId: Int64?) -> EnqueueMessage? { var attributes: [MessageAttribute] = [] attributes.append(OutgoingChatContextResultMessageAttribute(queryId: result.queryId, id: result.id, hideVia: hideVia)) if !hideVia { @@ -13,6 +20,9 @@ public func outgoingMessageWithChatContextResult(to peerId: PeerId, results: Cha if let scheduleTime = scheduleTime { attributes.append(OutgoingScheduleInfoMessageAttribute(scheduleTime: scheduleTime)) } + if silentPosting { + attributes.append(NotificationInfoMessageAttribute(flags: .muted)) + } switch result.message { case let .auto(caption, entities, replyMarkup): if let entities = entities { @@ -32,19 +42,19 @@ public func outgoingMessageWithChatContextResult(to peerId: PeerId, results: Cha return true } if let media: Media = internalReference.file ?? internalReference.image { - return .message(text: caption, attributes: filteredAttributes, mediaReference: .standalone(media: media), replyToMessageId: nil, localGroupingKey: nil) + return .message(text: caption, attributes: filteredAttributes, mediaReference: .standalone(media: media), replyToMessageId: replyToMessageId, localGroupingKey: nil, correlationId: correlationId) } else { - return .message(text: caption, attributes: filteredAttributes, mediaReference: nil, replyToMessageId: nil, localGroupingKey: nil) + return .message(text: caption, attributes: filteredAttributes, mediaReference: nil, replyToMessageId: replyToMessageId, localGroupingKey: nil, correlationId: correlationId) } } else { - return .message(text: "", attributes: attributes, mediaReference: .standalone(media: TelegramMediaGame(gameId: 0, accessHash: 0, name: "", title: internalReference.title ?? "", description: internalReference.description ?? "", image: internalReference.image, file: internalReference.file)), replyToMessageId: nil, localGroupingKey: nil) + return .message(text: "", attributes: attributes, mediaReference: .standalone(media: TelegramMediaGame(gameId: 0, accessHash: 0, name: "", title: internalReference.title ?? "", description: internalReference.description ?? "", image: internalReference.image, file: internalReference.file)), replyToMessageId: replyToMessageId, localGroupingKey: nil, correlationId: correlationId) } } else if let file = internalReference.file, internalReference.type == "gif" { - return .message(text: caption, attributes: attributes, mediaReference: .standalone(media: file), replyToMessageId: nil, localGroupingKey: nil) + return .message(text: caption, attributes: attributes, mediaReference: .standalone(media: file), replyToMessageId: replyToMessageId, localGroupingKey: nil, correlationId: correlationId) } else if let image = internalReference.image { - return .message(text: caption, attributes: attributes, mediaReference: .standalone(media: image), replyToMessageId: nil, localGroupingKey: nil) + return .message(text: caption, attributes: attributes, mediaReference: .standalone(media: image), replyToMessageId: replyToMessageId, localGroupingKey: nil, correlationId: correlationId) } else if let file = internalReference.file { - return .message(text: caption, attributes: attributes, mediaReference: .standalone(media: file), replyToMessageId: nil, localGroupingKey: nil) + return .message(text: caption, attributes: attributes, mediaReference: .standalone(media: file), replyToMessageId: replyToMessageId, localGroupingKey: nil, correlationId: correlationId) } else { return nil } @@ -55,10 +65,10 @@ 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, progressiveSizes: [])], immediateThumbnailData: nil, reference: nil, partialReference: nil, flags: []) - return .message(text: caption, attributes: attributes, mediaReference: .standalone(media: tmpImage), replyToMessageId: nil, localGroupingKey: nil) + let tmpImage = TelegramMediaImage(imageId: MediaId(namespace: Namespaces.Media.LocalImage, id: randomId), representations: [TelegramMediaImageRepresentation(dimensions: imageDimensions, resource: thumbnailResource, progressiveSizes: [], immediateThumbnailData: nil)], immediateThumbnailData: nil, reference: nil, partialReference: nil, flags: []) + return .message(text: caption, attributes: attributes, mediaReference: .standalone(media: tmpImage), replyToMessageId: replyToMessageId, localGroupingKey: nil, correlationId: correlationId) } else { - return .message(text: caption, attributes: attributes, mediaReference: nil, replyToMessageId: nil, localGroupingKey: nil) + return .message(text: caption, attributes: attributes, mediaReference: nil, replyToMessageId: replyToMessageId, localGroupingKey: nil, correlationId: correlationId) } } else if externalReference.type == "document" || externalReference.type == "gif" || externalReference.type == "audio" || externalReference.type == "voice" { var videoThumbnails: [TelegramMediaFile.VideoThumbnail] = [] @@ -71,7 +81,7 @@ public func outgoingMessageWithChatContextResult(to peerId: PeerId, results: Cha if thumbnail.mimeType.hasPrefix("video/") { videoThumbnails.append(TelegramMediaFile.VideoThumbnail(dimensions: thumbnail.dimensions ?? PixelDimensions(width: 128, height: 128), resource: thumbnailResource)) } else { - previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: thumbnail.dimensions ?? PixelDimensions(width: 128, height: 128), resource: thumbnailResource, progressiveSizes: [])) + previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: thumbnail.dimensions ?? PixelDimensions(width: 128, height: 128), resource: thumbnailResource, progressiveSizes: [], immediateThumbnailData: nil)) } } var fileName = "file" @@ -118,9 +128,9 @@ public func outgoingMessageWithChatContextResult(to peerId: PeerId, results: Cha } let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: randomId), partialReference: nil, resource: resource, previewRepresentations: previewRepresentations, videoThumbnails: videoThumbnails, immediateThumbnailData: nil, mimeType: externalReference.content?.mimeType ?? "application/binary", size: nil, attributes: fileAttributes) - return .message(text: caption, attributes: attributes, mediaReference: .standalone(media: file), replyToMessageId: nil, localGroupingKey: nil) + return .message(text: caption, attributes: attributes, mediaReference: .standalone(media: file), replyToMessageId: replyToMessageId, localGroupingKey: nil, correlationId: correlationId) } else { - return .message(text: caption, attributes: attributes, mediaReference: nil, replyToMessageId: nil, localGroupingKey: nil) + return .message(text: caption, attributes: attributes, mediaReference: nil, replyToMessageId: replyToMessageId, localGroupingKey: nil, correlationId: correlationId) } } case let .text(text, entities, disableUrlPreview, replyMarkup): @@ -130,16 +140,21 @@ public func outgoingMessageWithChatContextResult(to peerId: PeerId, results: Cha if let replyMarkup = replyMarkup { attributes.append(replyMarkup) } - return .message(text: text, attributes: attributes, mediaReference: nil, replyToMessageId: nil, localGroupingKey: nil) + return .message(text: text, attributes: attributes, mediaReference: nil, replyToMessageId: replyToMessageId, localGroupingKey: nil, correlationId: correlationId) case let .mapLocation(media, replyMarkup): if let replyMarkup = replyMarkup { attributes.append(replyMarkup) } - return .message(text: "", attributes: attributes, mediaReference: .standalone(media: media), replyToMessageId: nil, localGroupingKey: nil) + return .message(text: "", attributes: attributes, mediaReference: .standalone(media: media), replyToMessageId: replyToMessageId, localGroupingKey: nil, correlationId: correlationId) case let .contact(media, replyMarkup): if let replyMarkup = replyMarkup { attributes.append(replyMarkup) } - return .message(text: "", attributes: attributes, mediaReference: .standalone(media: media), replyToMessageId: nil, localGroupingKey: nil) + return .message(text: "", attributes: attributes, mediaReference: .standalone(media: media), replyToMessageId: replyToMessageId, localGroupingKey: nil, correlationId: correlationId) + case let .invoice(media, replyMarkup): + if let replyMarkup = replyMarkup { + attributes.append(replyMarkup) + } + return .message(text: "", attributes: attributes, mediaReference: .standalone(media: media), replyToMessageId: replyToMessageId, localGroupingKey: nil, correlationId: correlationId) } } diff --git a/submodules/TelegramCore/Sources/PeerLiveLocationsContext.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/PeerLiveLocationsContext.swift similarity index 88% rename from submodules/TelegramCore/Sources/PeerLiveLocationsContext.swift rename to submodules/TelegramCore/Sources/TelegramEngine/Messages/PeerLiveLocationsContext.swift index 3609e85ced..04be82ce54 100644 --- a/submodules/TelegramCore/Sources/PeerLiveLocationsContext.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/PeerLiveLocationsContext.swift @@ -2,10 +2,9 @@ import Foundation import Postbox import SwiftSignalKit import TelegramApi - import SyncCore -public func topPeerActiveLiveLocationMessages(viewTracker: AccountViewTracker, accountPeerId: PeerId, peerId: PeerId) -> Signal<(Peer?, [Message]), NoError> { +func _internal_topPeerActiveLiveLocationMessages(viewTracker: AccountViewTracker, accountPeerId: PeerId, peerId: PeerId) -> Signal<(Peer?, [Message]), NoError> { return viewTracker.aroundMessageHistoryViewForLocation(.peer(peerId), index: .upperBound, anchorIndex: .upperBound, count: 50, fixedCombinedReadStates: nil, tagMask: .liveLocation, orderStatistics: [], additionalData: [.peer(accountPeerId)]) |> map { (view, _, _) -> (Peer?, [Message]) in var accountPeer: Peer? diff --git a/submodules/TelegramCore/Sources/Polls.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Polls.swift similarity index 96% rename from submodules/TelegramCore/Sources/Polls.swift rename to submodules/TelegramCore/Sources/TelegramEngine/Messages/Polls.swift index 9c20e6b9c7..6aefb2b7eb 100644 --- a/submodules/TelegramCore/Sources/Polls.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Polls.swift @@ -10,7 +10,7 @@ public enum RequestMessageSelectPollOptionError { case generic } -public func requestMessageSelectPollOption(account: Account, messageId: MessageId, opaqueIdentifiers: [Data]) -> Signal { +func _internal_requestMessageSelectPollOption(account: Account, messageId: MessageId, opaqueIdentifiers: [Data]) -> Signal { return account.postbox.loadedPeerWithId(messageId.peerId) |> take(1) |> castError(RequestMessageSelectPollOptionError.self) @@ -80,7 +80,7 @@ public func requestMessageSelectPollOption(account: Account, messageId: MessageI } } -public func requestClosePoll(postbox: Postbox, network: Network, stateManager: AccountStateManager, messageId: MessageId) -> Signal { +func _internal_requestClosePoll(postbox: Postbox, network: Network, stateManager: AccountStateManager, messageId: MessageId) -> Signal { return postbox.transaction { transaction -> (TelegramMediaPoll, Api.InputPeer)? in guard let inputPeer = transaction.getPeer(messageId.peerId).flatMap(apiInputPeer) else { return nil @@ -286,11 +286,11 @@ private final class PollResultsOptionContext { let peerId: PeerId switch vote { case let .messageUserVote(userId, _, _): - peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: userId) + peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt32Value(userId)) case let .messageUserVoteInputOption(userId, _): - peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: userId) + peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt32Value(userId)) case let .messageUserVoteMultiple(userId, _, _): - peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: userId) + peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt32Value(userId)) } if let peer = transaction.getPeer(peerId) { resultPeers.append(RenderedPeer(peer: peer)) @@ -419,7 +419,7 @@ public final class PollResultsContext { } } - public init(account: Account, messageId: MessageId, poll: TelegramMediaPoll) { + 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) diff --git a/submodules/TelegramCore/Sources/RecentlyUsedHashtags.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/RecentlyUsedHashtags.swift similarity index 90% rename from submodules/TelegramCore/Sources/RecentlyUsedHashtags.swift rename to submodules/TelegramCore/Sources/TelegramEngine/Messages/RecentlyUsedHashtags.swift index 0bd04d68a4..9c313facbe 100644 --- a/submodules/TelegramCore/Sources/RecentlyUsedHashtags.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/RecentlyUsedHashtags.swift @@ -30,7 +30,7 @@ func addRecentlyUsedHashtag(transaction: Transaction, string: String) { } } -public func removeRecentlyUsedHashtag(postbox: Postbox, string: String) -> Signal { +func _internal_removeRecentlyUsedHashtag(postbox: Postbox, string: String) -> Signal { return postbox.transaction { transaction -> Void in if let itemId = RecentHashtagItemId(string) { transaction.removeOrderedItemListItem(collectionId: Namespaces.OrderedItemList.RecentlyUsedHashtags, itemId: itemId.rawValue) @@ -38,7 +38,7 @@ public func removeRecentlyUsedHashtag(postbox: Postbox, string: String) -> Signa } } -public func recentlyUsedHashtags(postbox: Postbox) -> Signal<[String], NoError> { +func _internal_recentlyUsedHashtags(postbox: Postbox) -> Signal<[String], NoError> { return postbox.combinedView(keys: [.orderedItemList(id: Namespaces.OrderedItemList.RecentlyUsedHashtags)]) |> mapToSignal { view -> Signal<[String], NoError> in return postbox.transaction { transaction -> [String] in diff --git a/submodules/TelegramCore/Sources/ReplyThreadHistory.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/ReplyThreadHistory.swift similarity index 99% rename from submodules/TelegramCore/Sources/ReplyThreadHistory.swift rename to submodules/TelegramCore/Sources/TelegramEngine/Messages/ReplyThreadHistory.swift index c9626ed73d..38df1cb764 100644 --- a/submodules/TelegramCore/Sources/ReplyThreadHistory.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/ReplyThreadHistory.swift @@ -453,7 +453,7 @@ public enum FetchChannelReplyThreadMessageError { case generic } -public func fetchChannelReplyThreadMessage(account: Account, messageId: MessageId, atMessageId: MessageId?) -> Signal { +func _internal_fetchChannelReplyThreadMessage(account: Account, messageId: MessageId, atMessageId: MessageId?) -> Signal { return account.postbox.transaction { transaction -> Api.InputPeer? in return transaction.getPeer(messageId.peerId).flatMap(apiInputPeer) } diff --git a/submodules/TelegramCore/Sources/RequestChatContextResults.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/RequestChatContextResults.swift similarity index 95% rename from submodules/TelegramCore/Sources/RequestChatContextResults.swift rename to submodules/TelegramCore/Sources/TelegramEngine/Messages/RequestChatContextResults.swift index 20ef3a4b63..39f0488710 100644 --- a/submodules/TelegramCore/Sources/RequestChatContextResults.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/RequestChatContextResults.swift @@ -51,7 +51,7 @@ public struct RequestChatContextResultsResult { } } -public func requestChatContextResults(account: Account, botId: PeerId, peerId: PeerId, query: String, location: Signal<(Double, Double)?, NoError> = .single(nil), offset: String, incompleteResults: Bool = false, staleCachedResults: Bool = false) -> Signal { +func _internal_requestChatContextResults(account: Account, botId: PeerId, peerId: PeerId, query: String, location: Signal<(Double, Double)?, NoError> = .single(nil), offset: String, incompleteResults: Bool = false, staleCachedResults: Bool = false) -> Signal { return account.postbox.transaction { transaction -> (bot: Peer, peer: Peer)? in if let bot = transaction.getPeer(botId), let peer = transaction.getPeer(peerId) { return (bot, peer) diff --git a/submodules/TelegramCore/Sources/RequestMessageActionCallback.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/RequestMessageActionCallback.swift similarity index 93% rename from submodules/TelegramCore/Sources/RequestMessageActionCallback.swift rename to submodules/TelegramCore/Sources/TelegramEngine/Messages/RequestMessageActionCallback.swift index edfeaa6777..6bfa85422f 100644 --- a/submodules/TelegramCore/Sources/RequestMessageActionCallback.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/RequestMessageActionCallback.swift @@ -24,7 +24,7 @@ public enum MessageActionCallbackError { case userBlocked } -public func requestMessageActionCallbackPasswordCheck(account: Account, messageId: MessageId, isGame: Bool, data: MemoryBuffer?) -> Signal { +func _internal_requestMessageActionCallbackPasswordCheck(account: Account, messageId: MessageId, isGame: Bool, data: MemoryBuffer?) -> Signal { return account.postbox.loadedPeerWithId(messageId.peerId) |> castError(MessageActionCallbackError.self) |> take(1) @@ -72,7 +72,7 @@ public func requestMessageActionCallbackPasswordCheck(account: Account, messageI } } -public func requestMessageActionCallback(account: Account, messageId: MessageId, isGame :Bool, password: String?, data: MemoryBuffer?) -> Signal { +func _internal_requestMessageActionCallback(account: Account, messageId: MessageId, isGame :Bool, password: String?, data: MemoryBuffer?) -> Signal { return account.postbox.loadedPeerWithId(messageId.peerId) |> castError(MessageActionCallbackError.self) |> take(1) @@ -92,7 +92,7 @@ public func requestMessageActionCallback(account: Account, messageId: MessageId, if let password = password, !password.isEmpty { flags |= Int32(1 << 2) - checkPassword = twoStepAuthData(account.network) + checkPassword = _internal_twoStepAuthData(account.network) |> mapError { error -> MessageActionCallbackError in if error.errorDescription.hasPrefix("FLOOD_WAIT") { return .limitExceeded @@ -179,7 +179,7 @@ public enum MessageActionUrlSubject { case url(String) } -public func requestMessageActionUrlAuth(account: Account, subject: MessageActionUrlSubject) -> Signal { +func _internal_requestMessageActionUrlAuth(account: Account, subject: MessageActionUrlSubject) -> Signal { let request: Signal var flags: Int32 = 0 switch subject { @@ -221,7 +221,7 @@ public func requestMessageActionUrlAuth(account: Account, subject: MessageAction } } -public func acceptMessageActionUrlAuth(account: Account, subject: MessageActionUrlSubject, allowWriteAccess: Bool) -> Signal { +func _internal_acceptMessageActionUrlAuth(account: Account, subject: MessageActionUrlSubject, allowWriteAccess: Bool) -> Signal { var flags: Int32 = 0 if allowWriteAccess { flags |= Int32(1 << 0) diff --git a/submodules/TelegramCore/Sources/RequestStartBot.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/RequestStartBot.swift similarity index 82% rename from submodules/TelegramCore/Sources/RequestStartBot.swift rename to submodules/TelegramCore/Sources/TelegramEngine/Messages/RequestStartBot.swift index 44e6b1118b..9b46a05b71 100644 --- a/submodules/TelegramCore/Sources/RequestStartBot.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/RequestStartBot.swift @@ -6,12 +6,12 @@ import MtProtoKit import SyncCore -public func requestStartBot(account: Account, botPeerId: PeerId, payload: String?) -> Signal { +func _internal_requestStartBot(account: Account, botPeerId: PeerId, payload: String?) -> Signal { if let payload = payload, !payload.isEmpty { return account.postbox.loadedPeerWithId(botPeerId) |> mapToSignal { botPeer -> Signal in if let inputUser = apiInputUser(botPeer) { - let r = account.network.request(Api.functions.messages.startBot(bot: inputUser, peer: .inputPeerEmpty, randomId: arc4random64(), startParam: payload)) + let r = account.network.request(Api.functions.messages.startBot(bot: inputUser, peer: .inputPeerEmpty, randomId: Int64.random(in: Int64.min ... Int64.max), startParam: payload)) |> mapToSignal { result -> Signal in account.stateManager.addUpdates(result) return .complete() @@ -26,7 +26,7 @@ public func requestStartBot(account: Account, botPeerId: PeerId, payload: String } } } else { - return enqueueMessages(account: account, peerId: botPeerId, messages: [.message(text: "/start", attributes: [], mediaReference: nil, replyToMessageId: nil, localGroupingKey: nil)]) |> mapToSignal { _ -> Signal in + return enqueueMessages(account: account, peerId: botPeerId, messages: [.message(text: "/start", attributes: [], mediaReference: nil, replyToMessageId: nil, localGroupingKey: nil, correlationId: nil)]) |> mapToSignal { _ -> Signal in return .complete() } } @@ -41,21 +41,21 @@ public enum StartBotInGroupResult { case channelParticipant(RenderedChannelParticipant) } -public func requestStartBotInGroup(account: Account, botPeerId: PeerId, groupPeerId: PeerId, payload: String?) -> Signal { +func _internal_requestStartBotInGroup(account: Account, botPeerId: PeerId, groupPeerId: PeerId, payload: String?) -> Signal { return account.postbox.transaction { transaction -> (Peer?, Peer?) in return (transaction.getPeer(botPeerId), transaction.getPeer(groupPeerId)) } |> mapError { _ -> RequestStartBotInGroupError in return .generic } |> mapToSignal { botPeer, groupPeer -> Signal in if let botPeer = botPeer, let inputUser = apiInputUser(botPeer), let groupPeer = groupPeer, let inputGroup = apiInputPeer(groupPeer) { - let request = account.network.request(Api.functions.messages.startBot(bot: inputUser, peer: inputGroup, randomId: arc4random64(), startParam: payload ?? "")) + let request = account.network.request(Api.functions.messages.startBot(bot: inputUser, peer: inputGroup, randomId: Int64.random(in: Int64.min ... Int64.max), startParam: payload ?? "")) |> mapError { _ -> RequestStartBotInGroupError in return .generic } |> mapToSignal { result -> Signal in account.stateManager.addUpdates(result) if groupPeerId.namespace == Namespaces.Peer.CloudChannel { - return fetchChannelParticipant(account: account, peerId: groupPeerId, participantId: botPeerId) + return _internal_fetchChannelParticipant(account: account, peerId: groupPeerId, participantId: botPeerId) |> mapError { _ -> RequestStartBotInGroupError in return .generic } |> mapToSignal { participant -> Signal in diff --git a/submodules/TelegramCore/Sources/ScheduledMessages.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/ScheduledMessages.swift similarity index 98% rename from submodules/TelegramCore/Sources/ScheduledMessages.swift rename to submodules/TelegramCore/Sources/TelegramEngine/Messages/ScheduledMessages.swift index f8500bb722..65d6906e20 100644 --- a/submodules/TelegramCore/Sources/ScheduledMessages.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/ScheduledMessages.swift @@ -5,7 +5,7 @@ import TelegramApi import SyncCore -public func sendScheduledMessageNowInteractively(postbox: Postbox, messageId: MessageId) -> Signal { +func _internal_sendScheduledMessageNowInteractively(postbox: Postbox, messageId: MessageId) -> Signal { return postbox.transaction { transaction -> Void in transaction.setPendingMessageAction(type: .sendScheduledMessageImmediately, id: messageId, action: SendScheduledMessageImmediatelyAction()) } diff --git a/submodules/TelegramCore/Sources/SearchMessages.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/SearchMessages.swift similarity index 98% rename from submodules/TelegramCore/Sources/SearchMessages.swift rename to submodules/TelegramCore/Sources/TelegramEngine/Messages/SearchMessages.swift index 6e89ef2414..0d70dc6986 100644 --- a/submodules/TelegramCore/Sources/SearchMessages.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/SearchMessages.swift @@ -184,7 +184,7 @@ private func mergedResult(_ state: SearchMessagesState) -> SearchMessagesResult return SearchMessagesResult(messages: messages, readStates: readStates, totalCount: state.main.totalCount + (state.additional?.totalCount ?? 0), completed: state.main.completed && (state.additional?.completed ?? true)) } -public func searchMessages(account: Account, location: SearchMessagesLocation, query: String, state: SearchMessagesState?, limit: Int32 = 100) -> Signal<(SearchMessagesResult, SearchMessagesState), NoError> { +func _internal_searchMessages(account: Account, location: SearchMessagesLocation, query: String, state: SearchMessagesState?, limit: Int32 = 100) -> Signal<(SearchMessagesResult, SearchMessagesState), NoError> { let remoteSearchResult: Signal<(Api.messages.Messages?, Api.messages.Messages?), NoError> switch location { case let .peer(peerId, fromId, tags, topMsgId, minDate, maxDate): @@ -379,7 +379,7 @@ public func searchMessages(account: Account, location: SearchMessagesLocation, q } } -public func downloadMessage(postbox: Postbox, network: Network, messageId: MessageId) -> Signal { +func _internal_downloadMessage(postbox: Postbox, network: Network, messageId: MessageId) -> Signal { return postbox.transaction { transaction -> Message? in return transaction.getMessage(messageId) } |> mapToSignal { message in @@ -562,7 +562,7 @@ func fetchRemoteMessage(postbox: Postbox, source: FetchMessageHistoryHoleSource, } } -public func searchMessageIdByTimestamp(account: Account, peerId: PeerId, threadId: Int64?, timestamp: Int32) -> Signal { +func _internal_searchMessageIdByTimestamp(account: Account, peerId: PeerId, threadId: Int64?, timestamp: Int32) -> Signal { return account.postbox.transaction { transaction -> Signal in if peerId.namespace == Namespaces.Peer.SecretChat { return .single(transaction.findClosestMessageIdByTimestamp(peerId: peerId, timestamp: timestamp)) @@ -672,7 +672,7 @@ public enum UpdatedRemotePeerError { case generic } -public func updatedRemotePeer(postbox: Postbox, network: Network, peer: PeerReference) -> Signal { +func _internal_updatedRemotePeer(postbox: Postbox, network: Network, peer: PeerReference) -> Signal { if let inputUser = peer.inputUser { return network.request(Api.functions.users.getUsers(id: [inputUser])) |> mapError { _ -> UpdatedRemotePeerError in @@ -721,7 +721,6 @@ public func updatedRemotePeer(postbox: Postbox, network: Network, peer: PeerRefe return updatedPeer } |> mapError { _ -> UpdatedRemotePeerError in - return .generic } } else { return .fail(.generic) @@ -748,7 +747,6 @@ public func updatedRemotePeer(postbox: Postbox, network: Network, peer: PeerRefe return updatedPeer } |> mapError { _ -> UpdatedRemotePeerError in - return .generic } } else { return .fail(.generic) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift new file mode 100644 index 0000000000..6dc415b280 --- /dev/null +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift @@ -0,0 +1,175 @@ +import Foundation +import SwiftSignalKit +import Postbox +import SyncCore + +public extension TelegramEngine { + final class Messages { + private let account: Account + + init(account: Account) { + self.account = account + } + + public func clearCloudDraftsInteractively() -> Signal { + return _internal_clearCloudDraftsInteractively(postbox: self.account.postbox, network: self.account.network, accountPeerId: self.account.peerId) + } + + public func applyMaxReadIndexInteractively(index: MessageIndex) -> Signal { + return _internal_applyMaxReadIndexInteractively(postbox: self.account.postbox, stateManager: self.account.stateManager, index: index) + } + + public func sendScheduledMessageNowInteractively(messageId: MessageId) -> Signal { + return _internal_sendScheduledMessageNowInteractively(postbox: self.account.postbox, messageId: messageId) + } + + public func requestMessageActionCallbackPasswordCheck(messageId: MessageId, isGame: Bool, data: MemoryBuffer?) -> Signal { + return _internal_requestMessageActionCallbackPasswordCheck(account: self.account, messageId: messageId, isGame: isGame, data: data) + } + + public func requestMessageActionCallback(messageId: MessageId, isGame: Bool, password: String?, data: MemoryBuffer?) -> Signal { + return _internal_requestMessageActionCallback(account: self.account, messageId: messageId, isGame: isGame, password: password, data: data) + } + + public func requestMessageActionUrlAuth(subject: MessageActionUrlSubject) -> Signal { + _internal_requestMessageActionUrlAuth(account: self.account, subject: subject) + } + + public func acceptMessageActionUrlAuth(subject: MessageActionUrlSubject, allowWriteAccess: Bool) -> Signal { + return _internal_acceptMessageActionUrlAuth(account: self.account, subject: subject, allowWriteAccess: allowWriteAccess) + } + + public func searchMessages(location: SearchMessagesLocation, query: String, state: SearchMessagesState?, limit: Int32 = 100) -> Signal<(SearchMessagesResult, SearchMessagesState), NoError> { + return _internal_searchMessages(account: self.account, location: location, query: query, state: state, limit: limit) + } + + public func downloadMessage(messageId: MessageId) -> Signal { + return _internal_downloadMessage(postbox: self.account.postbox, network: self.account.network, messageId: messageId) + } + + public func searchMessageIdByTimestamp(peerId: PeerId, threadId: Int64?, timestamp: Int32) -> Signal { + return _internal_searchMessageIdByTimestamp(account: self.account, peerId: peerId, threadId: threadId, timestamp: timestamp) + } + + public func deleteMessages(transaction: Transaction, ids: [MessageId], deleteMedia: Bool = true, manualAddMessageThreadStatsDifference: ((MessageId, Int, Int) -> Void)? = nil) { + return _internal_deleteMessages(transaction: transaction, mediaBox: self.account.postbox.mediaBox, ids: ids, deleteMedia: deleteMedia, manualAddMessageThreadStatsDifference: manualAddMessageThreadStatsDifference) + } + + public func deleteAllMessagesWithAuthor(transaction: Transaction, peerId: PeerId, authorId: PeerId, namespace: MessageId.Namespace) { + return _internal_deleteAllMessagesWithAuthor(transaction: transaction, mediaBox: self.account.postbox.mediaBox, peerId: peerId, authorId: authorId, namespace: namespace) + } + + public func deleteAllMessagesWithForwardAuthor(transaction: Transaction, peerId: PeerId, forwardAuthorId: PeerId, namespace: MessageId.Namespace) { + return _internal_deleteAllMessagesWithForwardAuthor(transaction: transaction, mediaBox: self.account.postbox.mediaBox, peerId: peerId, forwardAuthorId: forwardAuthorId, namespace: namespace) + } + + public func clearCallHistory(forEveryone: Bool) -> Signal { + return _internal_clearCallHistory(account: self.account, forEveryone: forEveryone) + } + + public func deleteMessagesInteractively(messageIds: [MessageId], type: InteractiveMessagesDeletionType, deleteAllInGroup: Bool = false) -> Signal { + return _internal_deleteMessagesInteractively(account: self.account, messageIds: messageIds, type: type, deleteAllInGroup: deleteAllInGroup) + } + + public func clearHistoryInteractively(peerId: PeerId, type: InteractiveHistoryClearingType) -> Signal { + return _internal_clearHistoryInteractively(postbox: self.account.postbox, peerId: peerId, type: type) + } + + public func clearAuthorHistory(peerId: PeerId, memberId: PeerId) -> Signal { + return _internal_clearAuthorHistory(account: self.account, peerId: peerId, memberId: memberId) + } + + public func requestEditMessage(messageId: MessageId, text: String, media: RequestEditMessageMedia, entities: TextEntitiesMessageAttribute? = nil, disableUrlPreview: Bool = false, scheduleTime: Int32? = nil) -> Signal { + return _internal_requestEditMessage(account: self.account, messageId: messageId, text: text, media: media, entities: entities, disableUrlPreview: disableUrlPreview, scheduleTime: scheduleTime) + } + + public func requestEditLiveLocation(messageId: MessageId, stop: Bool, coordinate: (latitude: Double, longitude: Double, accuracyRadius: Int32?)?, heading: Int32?, proximityNotificationRadius: Int32?) -> Signal { + return _internal_requestEditLiveLocation(postbox: self.account.postbox, network: self.account.network, stateManager: self.account.stateManager, messageId: messageId, stop: stop, coordinate: coordinate, heading: heading, proximityNotificationRadius: proximityNotificationRadius) + } + + public func addSecretChatMessageScreenshot(peerId: PeerId) -> Signal { + return _internal_addSecretChatMessageScreenshot(account: self.account, peerId: peerId) + |> ignoreValues + } + + public func forwardGameWithScore(messageId: MessageId, to peerId: PeerId) -> Signal { + return _internal_forwardGameWithScore(account: self.account, messageId: messageId, to: peerId) + } + + public func requestUpdatePinnedMessage(peerId: PeerId, update: PinnedMessageUpdate) -> Signal { + return _internal_requestUpdatePinnedMessage(account: self.account, peerId: peerId, update: update) + } + + public func requestUnpinAllMessages(peerId: PeerId) -> Signal { + return _internal_requestUnpinAllMessages(account: self.account, peerId: peerId) + } + + public func fetchChannelReplyThreadMessage(messageId: MessageId, atMessageId: MessageId?) -> Signal { + return _internal_fetchChannelReplyThreadMessage(account: self.account, messageId: messageId, atMessageId: atMessageId) + } + + public func requestStartBot(botPeerId: PeerId, payload: String?) -> Signal { + return _internal_requestStartBot(account: self.account, botPeerId: botPeerId, payload: payload) + } + + public func requestStartBotInGroup(botPeerId: PeerId, groupPeerId: PeerId, payload: String?) -> Signal { + return _internal_requestStartBotInGroup(account: self.account, botPeerId: botPeerId, groupPeerId: groupPeerId, payload: payload) + } + + public func markAllChatsAsRead() -> Signal { + return _internal_markAllChatsAsRead(postbox: self.account.postbox, network: self.account.network, stateManager: self.account.stateManager) + } + + public func getMessagesLoadIfNecessary(_ messageIds: [MessageId], strategy: GetMessagesStrategy = .cloud) -> Signal <[Message], NoError> { + return _internal_getMessagesLoadIfNecessary(messageIds, postbox: self.account.postbox, network: self.account.network, accountPeerId: self.account.peerId, strategy: strategy) + } + + public func markMessageContentAsConsumedInteractively(messageId: MessageId) -> Signal { + return _internal_markMessageContentAsConsumedInteractively(postbox: self.account.postbox, messageId: messageId) + } + + public func installInteractiveReadMessagesAction(peerId: PeerId) -> Disposable { + return _internal_installInteractiveReadMessagesAction(postbox: self.account.postbox, stateManager: self.account.stateManager, peerId: peerId) + } + + public func requestMessageSelectPollOption(messageId: MessageId, opaqueIdentifiers: [Data]) -> Signal { + return _internal_requestMessageSelectPollOption(account: self.account, messageId: messageId, opaqueIdentifiers: opaqueIdentifiers) + } + + public func requestClosePoll(messageId: MessageId) -> Signal { + return _internal_requestClosePoll(postbox: self.account.postbox, network: self.account.network, stateManager: self.account.stateManager, messageId: messageId) + } + + public func pollResults(messageId: MessageId, poll: TelegramMediaPoll) -> PollResultsContext { + return PollResultsContext(account: self.account, messageId: messageId, poll: poll) + } + + public func earliestUnseenPersonalMentionMessage(peerId: PeerId) -> Signal { + return _internal_earliestUnseenPersonalMentionMessage(account: self.account, peerId: peerId) + } + + public func exportMessageLink(peerId: PeerId, messageId: MessageId, isThread: Bool = false) -> Signal { + return _internal_exportMessageLink(account: self.account, peerId: peerId, messageId: messageId, isThread: isThread) + } + + public func enqueueOutgoingMessageWithChatContextResult(to peerId: PeerId, results: ChatContextResultCollection, result: ChatContextResult, replyToMessageId: MessageId? = nil, hideVia: Bool = false, silentPosting: Bool = false, scheduleTime: Int32? = nil, correlationId: Int64? = nil) -> Bool { + return _internal_enqueueOutgoingMessageWithChatContextResult(account: self.account, to: peerId, results: results, result: result, replyToMessageId: replyToMessageId, hideVia: hideVia, silentPosting: silentPosting, scheduleTime: scheduleTime, correlationId: correlationId) + } + + public func requestChatContextResults(botId: PeerId, peerId: PeerId, query: String, location: Signal<(Double, Double)?, NoError> = .single(nil), offset: String, incompleteResults: Bool = false, staleCachedResults: Bool = false) -> Signal { + return _internal_requestChatContextResults(account: self.account, botId: botId, peerId: peerId, query: query, location: location, offset: offset, incompleteResults: incompleteResults, staleCachedResults: staleCachedResults) + } + + public func removeRecentlyUsedHashtag(string: String) -> Signal { + return _internal_removeRecentlyUsedHashtag(postbox: self.account.postbox, string: string) + } + + public func recentlyUsedHashtags() -> Signal<[String], NoError> { + return _internal_recentlyUsedHashtags(postbox: self.account.postbox) + } + + public func topPeerActiveLiveLocationMessages(peerId: PeerId) -> Signal<(Peer?, [Message]), NoError> { + return _internal_topPeerActiveLiveLocationMessages(viewTracker: self.account.viewTracker, accountPeerId: self.account.peerId, peerId: peerId) + } + } +} diff --git a/submodules/TelegramCore/Sources/UpdatePinnedMessage.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/UpdatePinnedMessage.swift similarity index 97% rename from submodules/TelegramCore/Sources/UpdatePinnedMessage.swift rename to submodules/TelegramCore/Sources/TelegramEngine/Messages/UpdatePinnedMessage.swift index 869f3ba0c2..f7a140da28 100644 --- a/submodules/TelegramCore/Sources/UpdatePinnedMessage.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/UpdatePinnedMessage.swift @@ -15,7 +15,7 @@ public enum PinnedMessageUpdate { case clear(id: MessageId) } -public func requestUpdatePinnedMessage(account: Account, peerId: PeerId, update: PinnedMessageUpdate) -> Signal { +func _internal_requestUpdatePinnedMessage(account: Account, peerId: PeerId, update: PinnedMessageUpdate) -> Signal { return account.postbox.transaction { transaction -> (Peer?, CachedPeerData?) in return (transaction.getPeer(peerId), transaction.getPeerCachedData(peerId: peerId)) } @@ -112,7 +112,7 @@ public func requestUpdatePinnedMessage(account: Account, peerId: PeerId, update: } } -public func requestUnpinAllMessages(account: Account, peerId: PeerId) -> Signal { +func _internal_requestUnpinAllMessages(account: Account, peerId: PeerId) -> Signal { return account.postbox.transaction { transaction -> (Peer?, CachedPeerData?) in return (transaction.getPeer(peerId), transaction.getPeerCachedData(peerId: peerId)) } diff --git a/submodules/TelegramCore/Sources/BankCards.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/BankCards.swift similarity index 94% rename from submodules/TelegramCore/Sources/BankCards.swift rename to submodules/TelegramCore/Sources/TelegramEngine/Payments/BankCards.swift index b600491d83..f756b95b71 100644 --- a/submodules/TelegramCore/Sources/BankCards.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/BankCards.swift @@ -5,17 +5,7 @@ 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 { +func _internal_getBankCardInfo(account: Account, cardNumber: String) -> Signal { return currentWebDocumentsHostDatacenterId(postbox: account.postbox, isTestingEnvironment: false) |> mapToSignal { datacenterId in let signal: Signal @@ -38,6 +28,16 @@ public func getBankCardInfo(account: Account, cardNumber: String) -> Signal Bool { + switch lhs { + case let .card(id, title): + if case .card(id, title) = rhs { + return true + } else { + return false + } + } + } +} + +public struct BotPaymentForm : Equatable { + public let id: Int64 + public let canSaveCredentials: Bool + public let passwordMissing: Bool + public let invoice: BotPaymentInvoice + public let paymentBotId: PeerId + public let providerId: PeerId + public let url: String + public let nativeProvider: BotPaymentNativeProvider? + public let savedInfo: BotPaymentRequestedInfo? + public let savedCredentials: BotPaymentSavedCredentials? +} + +public enum BotPaymentFormRequestError { + case generic +} + +extension BotPaymentInvoice { + init(apiInvoice: Api.Invoice) { + switch apiInvoice { + case let .invoice(flags, currency, prices, maxTipAmount, suggestedTipAmounts): + var fields = BotPaymentInvoiceFields() + if (flags & (1 << 1)) != 0 { + fields.insert(.name) + } + if (flags & (1 << 2)) != 0 { + fields.insert(.phone) + } + if (flags & (1 << 3)) != 0 { + fields.insert(.email) + } + if (flags & (1 << 4)) != 0 { + fields.insert(.shippingAddress) + } + if (flags & (1 << 5)) != 0 { + fields.insert(.flexibleShipping) + } + if (flags & (1 << 6)) != 0 { + fields.insert(.phoneAvailableToProvider) + } + if (flags & (1 << 7)) != 0 { + fields.insert(.emailAvailableToProvider) + } + var parsedTip: BotPaymentInvoice.Tip? + if let maxTipAmount = maxTipAmount, let suggestedTipAmounts = suggestedTipAmounts { + parsedTip = BotPaymentInvoice.Tip(max: maxTipAmount, suggested: suggestedTipAmounts) + } + self.init(isTest: (flags & (1 << 0)) != 0, requestedFields: fields, currency: currency, prices: prices.map { + switch $0 { + case let .labeledPrice(label, amount): + return BotPaymentPrice(label: label, amount: amount) + } + }, tip: parsedTip) + } + } +} + +extension BotPaymentRequestedInfo { + init(apiInfo: Api.PaymentRequestedInfo) { + switch apiInfo { + case let .paymentRequestedInfo(_, name, phone, email, shippingAddress): + var parsedShippingAddress: BotPaymentShippingAddress? + if let shippingAddress = shippingAddress { + switch shippingAddress { + case let .postAddress(streetLine1, streetLine2, city, state, countryIso2, postCode): + parsedShippingAddress = BotPaymentShippingAddress(streetLine1: streetLine1, streetLine2: streetLine2, city: city, state: state, countryIso2: countryIso2, postCode: postCode) + } + } + self.init(name: name, phone: phone, email: email, shippingAddress: parsedShippingAddress) + } + } +} + +func _internal_fetchBotPaymentForm(postbox: Postbox, network: Network, messageId: MessageId, themeParams: [String: Any]?) -> Signal { + return postbox.transaction { transaction -> Api.InputPeer? in + return transaction.getPeer(messageId.peerId).flatMap(apiInputPeer) + } + |> castError(BotPaymentFormRequestError.self) + |> mapToSignal { inputPeer -> Signal in + guard let inputPeer = inputPeer else { + return .fail(.generic) + } + var flags: Int32 = 0 + var serializedThemeParams: Api.DataJSON? + if let themeParams = themeParams, let data = try? JSONSerialization.data(withJSONObject: themeParams, options: []), let dataString = String(data: data, encoding: .utf8) { + serializedThemeParams = Api.DataJSON.dataJSON(data: dataString) + } + if serializedThemeParams != nil { + flags |= 1 << 0 + } + + return network.request(Api.functions.payments.getPaymentForm(flags: flags, peer: inputPeer, msgId: messageId.id, themeParams: serializedThemeParams)) + |> `catch` { _ -> Signal in + return .fail(.generic) + } + |> mapToSignal { result -> Signal in + return postbox.transaction { transaction -> BotPaymentForm in + switch result { + case let .paymentForm(flags, id, botId, invoice, providerId, url, nativeProvider, nativeParams, savedInfo, savedCredentials, apiUsers): + var peers: [Peer] = [] + for user in apiUsers { + let parsed = TelegramUser(user: user) + peers.append(parsed) + } + updatePeers(transaction: transaction, peers: peers, update: { _, updated in + return updated + }) + + let parsedInvoice = BotPaymentInvoice(apiInvoice: invoice) + var parsedNativeProvider: BotPaymentNativeProvider? + if let nativeProvider = nativeProvider, let nativeParams = nativeParams { + switch nativeParams { + case let .dataJSON(data): + parsedNativeProvider = BotPaymentNativeProvider(name: nativeProvider, params: data) + } + } + let parsedSavedInfo = savedInfo.flatMap(BotPaymentRequestedInfo.init) + var parsedSavedCredentials: BotPaymentSavedCredentials? + if let savedCredentials = savedCredentials { + switch savedCredentials { + case let .paymentSavedCredentialsCard(id, title): + parsedSavedCredentials = .card(id: id, title: title) + } + } + return BotPaymentForm(id: id, canSaveCredentials: (flags & (1 << 2)) != 0, passwordMissing: (flags & (1 << 3)) != 0, invoice: parsedInvoice, paymentBotId: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt32Value(botId)), providerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt32Value(providerId)), url: url, nativeProvider: parsedNativeProvider, savedInfo: parsedSavedInfo, savedCredentials: parsedSavedCredentials) + } + } + |> mapError { _ -> BotPaymentFormRequestError in } + } + } +} + +public enum ValidateBotPaymentFormError { + case generic + case shippingNotAvailable + case addressStateInvalid + case addressPostcodeInvalid + case addressCityInvalid + case nameInvalid + case emailInvalid + case phoneInvalid +} + +public struct BotPaymentShippingOption : Equatable { + public let id: String + public let title: String + public let prices: [BotPaymentPrice] +} + +public struct BotPaymentValidatedFormInfo : Equatable { + public let id: String? + public let shippingOptions: [BotPaymentShippingOption]? +} + +extension BotPaymentShippingOption { + init(apiOption: Api.ShippingOption) { + switch apiOption { + case let .shippingOption(id, title, prices): + self.init(id: id, title: title, prices: prices.map { + switch $0 { + case let .labeledPrice(label, amount): + return BotPaymentPrice(label: label, amount: amount) + } + }) + } + } +} + +func _internal_validateBotPaymentForm(account: Account, saveInfo: Bool, messageId: MessageId, formInfo: BotPaymentRequestedInfo) -> Signal { + return account.postbox.transaction { transaction -> Api.InputPeer? in + return transaction.getPeer(messageId.peerId).flatMap(apiInputPeer) + } + |> castError(ValidateBotPaymentFormError.self) + |> mapToSignal { inputPeer -> Signal in + guard let inputPeer = inputPeer else { + return .fail(.generic) + } + + var flags: Int32 = 0 + if saveInfo { + flags |= (1 << 0) + } + var infoFlags: Int32 = 0 + if let _ = formInfo.name { + infoFlags |= (1 << 0) + } + if let _ = formInfo.phone { + infoFlags |= (1 << 1) + } + if let _ = formInfo.email { + infoFlags |= (1 << 2) + } + var apiShippingAddress: Api.PostAddress? + if let address = formInfo.shippingAddress { + infoFlags |= (1 << 3) + apiShippingAddress = .postAddress(streetLine1: address.streetLine1, streetLine2: address.streetLine2, city: address.city, state: address.state, countryIso2: address.countryIso2, postCode: address.postCode) + } + return account.network.request(Api.functions.payments.validateRequestedInfo(flags: flags, peer: inputPeer, msgId: messageId.id, info: .paymentRequestedInfo(flags: infoFlags, name: formInfo.name, phone: formInfo.phone, email: formInfo.email, shippingAddress: apiShippingAddress))) + |> mapError { error -> ValidateBotPaymentFormError in + if error.errorDescription == "SHIPPING_NOT_AVAILABLE" { + return .shippingNotAvailable + } else if error.errorDescription == "ADDRESS_STATE_INVALID" { + return .addressStateInvalid + } else if error.errorDescription == "ADDRESS_POSTCODE_INVALID" { + return .addressPostcodeInvalid + } else if error.errorDescription == "ADDRESS_CITY_INVALID" { + return .addressCityInvalid + } else if error.errorDescription == "REQ_INFO_NAME_INVALID" { + return .nameInvalid + } else if error.errorDescription == "REQ_INFO_EMAIL_INVALID" { + return .emailInvalid + } else if error.errorDescription == "REQ_INFO_PHONE_INVALID" { + return .phoneInvalid + } else { + return .generic + } + } + |> map { result -> BotPaymentValidatedFormInfo in + switch result { + case let .validatedRequestedInfo(_, id, shippingOptions): + return BotPaymentValidatedFormInfo(id: id, shippingOptions: shippingOptions.flatMap { + return $0.map(BotPaymentShippingOption.init) + }) + } + } + } +} + +public enum BotPaymentCredentials { + case generic(data: String, saveOnServer: Bool) + case saved(id: String, tempPassword: Data) + case applePay(data: String) +} + +public enum SendBotPaymentFormError { + case generic + case precheckoutFailed + case paymentFailed + case alreadyPaid +} + +public enum SendBotPaymentResult { + case done(receiptMessageId: MessageId?) + case externalVerificationRequired(url: String) +} + +func _internal_sendBotPaymentForm(account: Account, messageId: MessageId, formId: Int64, validatedInfoId: String?, shippingOptionId: String?, tipAmount: Int64?, credentials: BotPaymentCredentials) -> Signal { + return account.postbox.transaction { transaction -> Api.InputPeer? in + return transaction.getPeer(messageId.peerId).flatMap(apiInputPeer) + } + |> castError(SendBotPaymentFormError.self) + |> mapToSignal { inputPeer -> Signal in + guard let inputPeer = inputPeer else { + return .fail(.generic) + } + + let apiCredentials: Api.InputPaymentCredentials + switch credentials { + case let .generic(data, saveOnServer): + var credentialsFlags: Int32 = 0 + if saveOnServer { + credentialsFlags |= (1 << 0) + } + apiCredentials = .inputPaymentCredentials(flags: credentialsFlags, data: .dataJSON(data: data)) + case let .saved(id, tempPassword): + apiCredentials = .inputPaymentCredentialsSaved(id: id, tmpPassword: Buffer(data: tempPassword)) + case let .applePay(data): + apiCredentials = .inputPaymentCredentialsApplePay(paymentData: .dataJSON(data: data)) + } + var flags: Int32 = 0 + if validatedInfoId != nil { + flags |= (1 << 0) + } + if shippingOptionId != nil { + flags |= (1 << 1) + } + if tipAmount != nil { + flags |= (1 << 2) + } + return account.network.request(Api.functions.payments.sendPaymentForm(flags: flags, formId: formId, peer: inputPeer, msgId: messageId.id, requestedInfoId: validatedInfoId, shippingOptionId: shippingOptionId, credentials: apiCredentials, tipAmount: tipAmount)) + |> map { result -> SendBotPaymentResult in + switch result { + case let .paymentResult(updates): + account.stateManager.addUpdates(updates) + var receiptMessageId: MessageId? + for apiMessage in updates.messages { + if let message = StoreMessage(apiMessage: apiMessage) { + for media in message.media { + if let action = media as? TelegramMediaAction { + if case .paymentSent = action.action { + for attribute in message.attributes { + if let reply = attribute as? ReplyMessageAttribute { + if reply.messageId == messageId { + if case let .Id(id) = message.id { + receiptMessageId = id + } + } + } + } + } + } + } + } + } + return .done(receiptMessageId: receiptMessageId) + case let .paymentVerificationNeeded(url): + return .externalVerificationRequired(url: url) + } + } + |> `catch` { error -> Signal in + if error.errorDescription == "BOT_PRECHECKOUT_FAILED" { + return .fail(.precheckoutFailed) + } else if error.errorDescription == "PAYMENT_FAILED" { + return .fail(.paymentFailed) + } else if error.errorDescription == "INVOICE_ALREADY_PAID" { + return .fail(.alreadyPaid) + } + return .fail(.generic) + } + } +} + +public struct BotPaymentReceipt : Equatable { + public let invoice: BotPaymentInvoice + public let info: BotPaymentRequestedInfo? + public let shippingOption: BotPaymentShippingOption? + public let credentialsTitle: String + public let invoiceMedia: TelegramMediaInvoice + public let tipAmount: Int64? + public let botPaymentId: PeerId + public static func ==(lhs: BotPaymentReceipt, rhs: BotPaymentReceipt) -> Bool { + if lhs.invoice != rhs.invoice { + return false + } + if lhs.info != rhs.info { + return false + } + if lhs.shippingOption != rhs.shippingOption { + return false + } + if lhs.credentialsTitle != rhs.credentialsTitle { + return false + } + if !lhs.invoiceMedia.isEqual(to: rhs.invoiceMedia) { + return false + } + if lhs.tipAmount != rhs.tipAmount { + return false + } + if lhs.botPaymentId != rhs.botPaymentId { + return false + } + return true + } +} + +public enum RequestBotPaymentReceiptError { + case generic +} + +func _internal_requestBotPaymentReceipt(account: Account, messageId: MessageId) -> Signal { + return account.postbox.transaction { transaction -> Api.InputPeer? in + return transaction.getPeer(messageId.peerId).flatMap(apiInputPeer) + } + |> castError(RequestBotPaymentReceiptError.self) + |> mapToSignal { inputPeer -> Signal in + guard let inputPeer = inputPeer else { + return .fail(.generic) + } + + return account.network.request(Api.functions.payments.getPaymentReceipt(peer: inputPeer, msgId: messageId.id)) + |> mapError { _ -> RequestBotPaymentReceiptError in + return .generic + } + |> mapToSignal { result -> Signal in + return account.postbox.transaction { transaction -> BotPaymentReceipt in + switch result { + case let .paymentReceipt(flags, date, botId, providerId, title, description, photo, invoice, info, shipping, tipAmount, currency, totalAmount, credentialsTitle, users): + var peers: [Peer] = [] + for user in users { + peers.append(TelegramUser(user: user)) + } + updatePeers(transaction: transaction, peers: peers, update: { _, updated in return updated }) + + let parsedInvoice = BotPaymentInvoice(apiInvoice: invoice) + let parsedInfo = info.flatMap(BotPaymentRequestedInfo.init) + let shippingOption = shipping.flatMap(BotPaymentShippingOption.init) + + /*let fields = BotPaymentInvoiceFields() + + let form = BotPaymentForm( + id: 0, + canSaveCredentials: false, + passwordMissing: false, + invoice: BotPaymentInvoice( + isTest: false, + requestedFields: fields, + currency: currency, + prices: [], + tip: nil + ), + providerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt32Value(providerId)), + url: "", + nativeProvider: nil, + savedInfo: nil, + savedCredentials: nil + )*/ + + let invoiceMedia = TelegramMediaInvoice( + title: title, + description: description, + photo: photo.flatMap(TelegramMediaWebFile.init), + receiptMessageId: nil, + currency: currency, + totalAmount: totalAmount, + startParam: "", + flags: [] + ) + + let botPaymentId = PeerId.init(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt32Value(botId)) + + return BotPaymentReceipt(invoice: parsedInvoice, info: parsedInfo, shippingOption: shippingOption, credentialsTitle: credentialsTitle, invoiceMedia: invoiceMedia, tipAmount: tipAmount, botPaymentId: botPaymentId) + } + } + |> castError(RequestBotPaymentReceiptError.self) + } + } +} + +public struct BotPaymentInfo: OptionSet { + public var rawValue: Int32 + + public init(rawValue: Int32) { + self.rawValue = rawValue + } + + public init() { + self.rawValue = 0 + } + + public static let paymentInfo = BotPaymentInfo(rawValue: 1 << 0) + public static let shippingInfo = BotPaymentInfo(rawValue: 1 << 1) +} + +func _internal_clearBotPaymentInfo(network: Network, info: BotPaymentInfo) -> Signal { + var flags: Int32 = 0 + if info.contains(.paymentInfo) { + flags |= (1 << 0) + } + if info.contains(.shippingInfo) { + flags |= (1 << 1) + } + return network.request(Api.functions.payments.clearSavedInfo(flags: flags)) + |> retryRequest + |> mapToSignal { _ -> Signal in + return .complete() + } +} diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/TelegramEnginePayments.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/TelegramEnginePayments.swift new file mode 100644 index 0000000000..7bde71b986 --- /dev/null +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/TelegramEnginePayments.swift @@ -0,0 +1,36 @@ +import SwiftSignalKit +import Postbox + +public extension TelegramEngine { + final class Payments { + private let account: Account + + init(account: Account) { + self.account = account + } + + public func getBankCardInfo(cardNumber: String) -> Signal { + return _internal_getBankCardInfo(account: self.account, cardNumber: cardNumber) + } + + public func fetchBotPaymentForm(messageId: MessageId, themeParams: [String: Any]?) -> Signal { + return _internal_fetchBotPaymentForm(postbox: self.account.postbox, network: self.account.network, messageId: messageId, themeParams: themeParams) + } + + public func validateBotPaymentForm(saveInfo: Bool, messageId: MessageId, formInfo: BotPaymentRequestedInfo) -> Signal { + return _internal_validateBotPaymentForm(account: self.account, saveInfo: saveInfo, messageId: messageId, formInfo: formInfo) + } + + public func sendBotPaymentForm(messageId: MessageId, formId: Int64, validatedInfoId: String?, shippingOptionId: String?, tipAmount: Int64?, credentials: BotPaymentCredentials) -> Signal { + return _internal_sendBotPaymentForm(account: self.account, messageId: messageId, formId: formId, validatedInfoId: validatedInfoId, shippingOptionId: shippingOptionId, tipAmount: tipAmount, credentials: credentials) + } + + public func requestBotPaymentReceipt(messageId: MessageId) -> Signal { + return _internal_requestBotPaymentReceipt(account: self.account, messageId: messageId) + } + + public func clearBotPaymentInfo(info: BotPaymentInfo) -> Signal { + return _internal_clearBotPaymentInfo(network: self.account.network, info: info) + } + } +} diff --git a/submodules/TelegramCore/Sources/AddPeerMember.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/AddPeerMember.swift similarity index 92% rename from submodules/TelegramCore/Sources/AddPeerMember.swift rename to submodules/TelegramCore/Sources/TelegramEngine/Peers/AddPeerMember.swift index 5edaa1c553..fffb6cc578 100644 --- a/submodules/TelegramCore/Sources/AddPeerMember.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/AddPeerMember.swift @@ -14,11 +14,11 @@ public enum AddGroupMemberError { case tooManyChannels } -public func addGroupMember(account: Account, peerId: PeerId, memberId: PeerId) -> Signal { +func _internal_addGroupMember(account: Account, peerId: PeerId, memberId: PeerId) -> Signal { return account.postbox.transaction { transaction -> Signal in if let peer = transaction.getPeer(peerId), let memberPeer = transaction.getPeer(memberId), let inputUser = apiInputUser(memberPeer) { if let group = peer as? TelegramGroup { - return account.network.request(Api.functions.messages.addChatUser(chatId: group.id.id, userId: inputUser, fwdLimit: 100)) + return account.network.request(Api.functions.messages.addChatUser(chatId: group.id.id._internalGetInt32Value(), userId: inputUser, fwdLimit: 100)) |> mapError { error -> AddGroupMemberError in switch error.errorDescription { case "USERS_TOO_MUCH": @@ -57,7 +57,7 @@ public func addGroupMember(account: Account, peerId: PeerId, memberId: PeerId) - }) } } - |> mapError { _ -> AddGroupMemberError in return .generic } + |> mapError { _ -> AddGroupMemberError in } } } else { return .fail(.generic) @@ -65,7 +65,7 @@ public func addGroupMember(account: Account, peerId: PeerId, memberId: PeerId) - } else { return .fail(.generic) } - } |> mapError { _ -> AddGroupMemberError in return .generic } |> switchToLatest + } |> mapError { _ -> AddGroupMemberError in } |> switchToLatest } public enum AddChannelMemberError { @@ -79,10 +79,9 @@ public enum AddChannelMemberError { case tooMuchBots } -public func addChannelMember(account: Account, peerId: PeerId, memberId: PeerId) -> Signal<(ChannelParticipant?, RenderedChannelParticipant), AddChannelMemberError> { - return fetchChannelParticipant(account: account, peerId: peerId, participantId: memberId) +func _internal_addChannelMember(account: Account, peerId: PeerId, memberId: PeerId) -> Signal<(ChannelParticipant?, RenderedChannelParticipant), AddChannelMemberError> { + return _internal_fetchChannelParticipant(account: account, peerId: peerId, participantId: memberId) |> mapError { error -> AddChannelMemberError in - return .generic } |> mapToSignal { currentParticipant -> Signal<(ChannelParticipant?, RenderedChannelParticipant), AddChannelMemberError> in return account.postbox.transaction { transaction -> Signal<(ChannelParticipant?, RenderedChannelParticipant), AddChannelMemberError> in @@ -158,7 +157,7 @@ public func addChannelMember(account: Account, peerId: PeerId, memberId: PeerId) if let presence = transaction.getPeerPresence(peerId: memberPeer.id) { presences[memberPeer.id] = presence } - if case let .member(_, _, maybeAdminInfo, maybeBannedInfo, _) = updatedParticipant { + if case let .member(_, _, maybeAdminInfo, _, _) = updatedParticipant { if let adminInfo = maybeAdminInfo { if let peer = transaction.getPeer(adminInfo.promotedBy) { peers[peer.id] = peer @@ -167,7 +166,7 @@ public func addChannelMember(account: Account, peerId: PeerId, memberId: PeerId) } return (currentParticipant, RenderedChannelParticipant(participant: updatedParticipant, peer: memberPeer, peers: peers, presences: presences)) } - |> mapError { _ -> AddChannelMemberError in return .generic } + |> mapError { _ -> AddChannelMemberError in } } } else { return .fail(.generic) @@ -176,12 +175,12 @@ public func addChannelMember(account: Account, peerId: PeerId, memberId: PeerId) return .fail(.generic) } } - |> mapError { _ -> AddChannelMemberError in return .generic } + |> mapError { _ -> AddChannelMemberError in } |> switchToLatest } } -public func addChannelMembers(account: Account, peerId: PeerId, memberIds: [PeerId]) -> Signal { +func _internal_addChannelMembers(account: Account, peerId: PeerId, memberIds: [PeerId]) -> Signal { let signal = account.postbox.transaction { transaction -> Signal in var memberPeerIds: [PeerId:Peer] = [:] var inputUsers: [Api.InputUser] = [] diff --git a/submodules/TelegramCore/Sources/AddressNames.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/AddressNames.swift similarity index 91% rename from submodules/TelegramCore/Sources/AddressNames.swift rename to submodules/TelegramCore/Sources/TelegramEngine/Peers/AddressNames.swift index 4f14c18d00..017cd4ba21 100644 --- a/submodules/TelegramCore/Sources/AddressNames.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/AddressNames.swift @@ -26,7 +26,7 @@ public enum AddressNameDomain { case theme(TelegramTheme) } -public func checkAddressNameFormat(_ value: String, canEmpty: Bool = false) -> AddressNameFormatError? { +func _internal_checkAddressNameFormat(_ value: String, canEmpty: Bool = false) -> AddressNameFormatError? { var index = 0 let length = value.count for char in value { @@ -52,7 +52,7 @@ public func checkAddressNameFormat(_ value: String, canEmpty: Bool = false) -> A return nil } -public func addressNameAvailability(account: Account, domain: AddressNameDomain, name: String) -> Signal { +func _internal_addressNameAvailability(account: Account, domain: AddressNameDomain, name: String) -> Signal { return account.postbox.transaction { transaction -> Signal in switch domain { case .account: @@ -120,7 +120,7 @@ public enum UpdateAddressNameError { case generic } -public func updateAddressName(account: Account, domain: AddressNameDomain, name: String?) -> Signal { +func _internal_updateAddressName(account: Account, domain: AddressNameDomain, name: String?) -> Signal { return account.postbox.transaction { transaction -> Signal in switch domain { case .account: @@ -134,7 +134,7 @@ public func updateAddressName(account: Account, domain: AddressNameDomain, name: updatePeers(transaction: transaction, peers: [user], update: { _, updated in return updated }) - } |> mapError { _ -> UpdateAddressNameError in return .generic } + } |> mapError { _ -> UpdateAddressNameError in } } case let .peer(peerId): if let peer = transaction.getPeer(peerId), let inputChannel = apiInputChannel(peer) { @@ -155,7 +155,7 @@ public func updateAddressName(account: Account, domain: AddressNameDomain, name: }) } } - } |> mapError { _ -> UpdateAddressNameError in return .generic } + } |> mapError { _ -> UpdateAddressNameError in } } } else { return .fail(.generic) @@ -170,10 +170,10 @@ public func updateAddressName(account: Account, domain: AddressNameDomain, name: return Void() } } - } |> mapError { _ -> UpdateAddressNameError in return .generic } |> switchToLatest + } |> mapError { _ -> UpdateAddressNameError in } |> switchToLatest } -public func checkPublicChannelCreationAvailability(account: Account, location: Bool = false) -> Signal { +func _internal_checkPublicChannelCreationAvailability(account: Account, location: Bool = false) -> Signal { var flags: Int32 = (1 << 1) if location { flags |= (1 << 0) @@ -194,7 +194,7 @@ public enum AdminedPublicChannelsScope { case forVoiceChat } -public func adminedPublicChannels(account: Account, scope: AdminedPublicChannelsScope = .all) -> Signal<[Peer], NoError> { +func _internal_adminedPublicChannels(account: Account, scope: AdminedPublicChannelsScope = .all) -> Signal<[Peer], NoError> { var flags: Int32 = 0 switch scope { case .all: @@ -238,7 +238,7 @@ public enum ChannelAddressNameAssignmentAvailability { case addressNameLimitReached } -public func channelAddressNameAssignmentAvailability(account: Account, peerId: PeerId?) -> Signal { +func _internal_channelAddressNameAssignmentAvailability(account: Account, peerId: PeerId?) -> Signal { return account.postbox.transaction { transaction -> Signal in var inputChannel: Api.InputChannel? if let peerId = peerId { diff --git a/submodules/TelegramCore/Sources/ChangePeerNotificationSettings.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChangePeerNotificationSettings.swift similarity index 86% rename from submodules/TelegramCore/Sources/ChangePeerNotificationSettings.swift rename to submodules/TelegramCore/Sources/TelegramEngine/Peers/ChangePeerNotificationSettings.swift index 33df953355..1836f8e221 100644 --- a/submodules/TelegramCore/Sources/ChangePeerNotificationSettings.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChangePeerNotificationSettings.swift @@ -4,7 +4,7 @@ import SwiftSignalKit import SyncCore -public func togglePeerMuted(account: Account, peerId: PeerId) -> Signal { +func _internal_togglePeerMuted(account: Account, peerId: PeerId) -> Signal { return account.postbox.transaction { transaction -> Void in if let peer = transaction.getPeer(peerId) { var notificationPeerId = peerId @@ -39,13 +39,13 @@ public func togglePeerMuted(account: Account, peerId: PeerId) -> Signal Signal { +func _internal_updatePeerMuteSetting(account: Account, peerId: PeerId, muteInterval: Int32?) -> Signal { return account.postbox.transaction { transaction -> Void in updatePeerMuteSetting(transaction: transaction, peerId: peerId, muteInterval: muteInterval) } } -public func updatePeerMuteSetting(transaction: Transaction, peerId: PeerId, muteInterval: Int32?) { +func updatePeerMuteSetting(transaction: Transaction, peerId: PeerId, muteInterval: Int32?) { if let peer = transaction.getPeer(peerId) { var notificationPeerId = peerId if let associatedPeerId = peer.associatedPeerId { @@ -82,13 +82,13 @@ public func updatePeerMuteSetting(transaction: Transaction, peerId: PeerId, mute } } -public func updatePeerDisplayPreviewsSetting(account: Account, peerId: PeerId, displayPreviews: PeerNotificationDisplayPreviews) -> Signal { +func _internal_updatePeerDisplayPreviewsSetting(account: Account, peerId: PeerId, displayPreviews: PeerNotificationDisplayPreviews) -> Signal { return account.postbox.transaction { transaction -> Void in updatePeerDisplayPreviewsSetting(transaction: transaction, peerId: peerId, displayPreviews: displayPreviews) } } -public func updatePeerDisplayPreviewsSetting(transaction: Transaction, peerId: PeerId, displayPreviews: PeerNotificationDisplayPreviews) { +func updatePeerDisplayPreviewsSetting(transaction: Transaction, peerId: PeerId, displayPreviews: PeerNotificationDisplayPreviews) { if let peer = transaction.getPeer(peerId) { var notificationPeerId = peerId if let associatedPeerId = peer.associatedPeerId { @@ -108,13 +108,13 @@ public func updatePeerDisplayPreviewsSetting(transaction: Transaction, peerId: P } } -public func updatePeerNotificationSoundInteractive(account: Account, peerId: PeerId, sound: PeerMessageSound) -> Signal { +func _internal_updatePeerNotificationSoundInteractive(account: Account, peerId: PeerId, sound: PeerMessageSound) -> Signal { return account.postbox.transaction { transaction -> Void in updatePeerNotificationSoundInteractive(transaction: transaction, peerId: peerId, sound: sound) } } -public func updatePeerNotificationSoundInteractive(transaction: Transaction, peerId: PeerId, sound: PeerMessageSound) { +func updatePeerNotificationSoundInteractive(transaction: Transaction, peerId: PeerId, sound: PeerMessageSound) { if let peer = transaction.getPeer(peerId) { var notificationPeerId = peerId if let associatedPeerId = peer.associatedPeerId { diff --git a/submodules/TelegramCore/Sources/ChannelAdminEventLogContext.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelAdminEventLogContext.swift similarity index 99% rename from submodules/TelegramCore/Sources/ChannelAdminEventLogContext.swift rename to submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelAdminEventLogContext.swift index 5a158a671e..5568220adc 100644 --- a/submodules/TelegramCore/Sources/ChannelAdminEventLogContext.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelAdminEventLogContext.swift @@ -96,7 +96,7 @@ public final class ChannelAdminEventLogContext { private let loadMoreDisposable = MetaDisposable() - public init(postbox: Postbox, network: Network, peerId: PeerId) { + init(postbox: Postbox, network: Network, peerId: PeerId) { self.postbox = postbox self.network = network self.peerId = peerId diff --git a/submodules/TelegramCore/Sources/ChannelAdminEventLogs.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelAdminEventLogs.swift similarity index 96% rename from submodules/TelegramCore/Sources/ChannelAdminEventLogs.swift rename to submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelAdminEventLogs.swift index 3a38204a5d..180244ebb2 100644 --- a/submodules/TelegramCore/Sources/ChannelAdminEventLogs.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelAdminEventLogs.swift @@ -114,7 +114,7 @@ private func boolFromApiValue(_ value: Api.Bool) -> Bool { } } -public func channelAdminLogEvents(postbox: Postbox, network: Network, peerId: PeerId, maxId: AdminLogEventId, minId: AdminLogEventId, limit: Int32 = 100, query: String? = nil, filter: AdminLogEventsFlags? = nil, admins: [PeerId]? = nil) -> Signal { +func channelAdminLogEvents(postbox: Postbox, network: Network, peerId: PeerId, maxId: AdminLogEventId, minId: AdminLogEventId, limit: Int32 = 100, query: String? = nil, filter: AdminLogEventsFlags? = nil, admins: [PeerId]? = nil) -> Signal { return postbox.transaction { transaction -> (Peer?, [Peer]?) in return (transaction.getPeer(peerId), admins?.compactMap { transaction.getPeer($0) }) } @@ -220,7 +220,7 @@ public func channelAdminLogEvents(postbox: Postbox, network: Network, peerId: Pe action = .pollStopped(rendered) } case let .channelAdminLogEventActionChangeLinkedChat(prevValue, newValue): - action = .linkedPeerUpdated(previous: prevValue == 0 ? nil : peers[PeerId(namespace: Namespaces.Peer.CloudChannel, id: prevValue)], updated: newValue == 0 ? nil : peers[PeerId(namespace: Namespaces.Peer.CloudChannel, id: newValue)]) + action = .linkedPeerUpdated(previous: prevValue == 0 ? nil : peers[PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt32Value(prevValue))], updated: newValue == 0 ? nil : peers[PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt32Value(newValue))]) case let .channelAdminLogEventActionChangeLocation(prevValue, newValue): action = .changeGeoLocation(previous: PeerGeoLocation(apiLocation: prevValue), updated: PeerGeoLocation(apiLocation: newValue)) case let .channelAdminLogEventActionToggleSlowMode(prevValue, newValue): @@ -251,7 +251,7 @@ public func channelAdminLogEvents(postbox: Postbox, network: Network, peerId: Pe case let .channelAdminLogEventActionChangeHistoryTTL(prevValue, newValue): action = .changeHistoryTTL(previousValue: prevValue, updatedValue: newValue) } - let peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: userId) + let peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt32Value(userId)) if let action = action { events.append(AdminLogEvent(id: id, peerId: peerId, date: date, action: action)) } diff --git a/submodules/TelegramCore/Sources/ChannelBlacklist.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelBlacklist.swift similarity index 60% rename from submodules/TelegramCore/Sources/ChannelBlacklist.swift rename to submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelBlacklist.swift index cb735a667d..91bd5c9ef3 100644 --- a/submodules/TelegramCore/Sources/ChannelBlacklist.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelBlacklist.swift @@ -3,137 +3,13 @@ import Postbox import SwiftSignalKit import TelegramApi import MtProtoKit - import SyncCore -private enum ChannelBlacklistFilter { - case restricted - case banned -} - -private func fetchChannelBlacklist(account: Account, peerId: PeerId, filter: ChannelBlacklistFilter) -> Signal<[RenderedChannelParticipant], NoError> { - return account.postbox.transaction { transaction -> Signal<[RenderedChannelParticipant], NoError> in - if let peer = transaction.getPeer(peerId), let inputChannel = apiInputChannel(peer) { - let apiFilter: Api.ChannelParticipantsFilter - switch filter { - case .restricted: - apiFilter = .channelParticipantsBanned(q: "") - case .banned: - apiFilter = .channelParticipantsKicked(q: "") - } - return account.network.request(Api.functions.channels.getParticipants(channel: inputChannel, filter: apiFilter, offset: 0, limit: 100, hash: 0)) - |> retryRequest - |> map { result -> [RenderedChannelParticipant] in - var items: [RenderedChannelParticipant] = [] - switch result { - case let .channelParticipants(_, participants, users): - var peers: [PeerId: Peer] = [:] - var presences:[PeerId: PeerPresence] = [:] - for user in users { - let peer = TelegramUser(user: user) - peers[peer.id] = peer - if let presence = TelegramUserPresence(apiUser: user) { - presences[peer.id] = presence - } - } - - for participant in CachedChannelParticipants(apiParticipants: participants).participants { - if let peer = peers[participant.peerId] { - items.append(RenderedChannelParticipant(participant: participant, peer: peer, peers: peers, presences: presences)) - } - - } - case .channelParticipantsNotModified: - assertionFailure() - break - } - return items - } - } else { - return .single([]) - } - } |> switchToLatest -} - -public struct ChannelBlacklist { - public let banned: [RenderedChannelParticipant] - public let restricted: [RenderedChannelParticipant] - - public init(banned: [RenderedChannelParticipant], restricted: [RenderedChannelParticipant]) { - self.banned = banned - self.restricted = restricted - } - - public var isEmpty: Bool { - return banned.isEmpty && restricted.isEmpty - } - - public func withRemovedPeerId(_ memberId:PeerId) -> ChannelBlacklist { - var updatedRestricted = restricted - var updatedBanned = banned - - for i in 0 ..< updatedBanned.count { - if updatedBanned[i].peer.id == memberId { - updatedBanned.remove(at: i) - break - } - } - for i in 0 ..< updatedRestricted.count { - if updatedRestricted[i].peer.id == memberId { - updatedRestricted.remove(at: i) - break - } - } - return ChannelBlacklist(banned: updatedBanned, restricted: updatedRestricted) - } - - public func withRemovedParticipant(_ participant:RenderedChannelParticipant) -> ChannelBlacklist { - let updated = self.withRemovedPeerId(participant.participant.peerId) - var updatedRestricted = updated.restricted - var updatedBanned = updated.banned - - if case let .member(_, _, _, maybeBanInfo, _) = participant.participant, let banInfo = maybeBanInfo { - if banInfo.rights.flags.contains(.banReadMessages) { - updatedBanned.insert(participant, at: 0) - } else { - if !banInfo.rights.flags.isEmpty { - updatedRestricted.insert(participant, at: 0) - } - } - } - - - return ChannelBlacklist(banned: updatedBanned, restricted: updatedRestricted) - } -} - -public func channelBlacklistParticipants(account: Account, peerId: PeerId) -> Signal { - return combineLatest(fetchChannelBlacklist(account: account, peerId: peerId, filter: .restricted), fetchChannelBlacklist(account: account, peerId: peerId, filter: .banned)) - |> map { restricted, banned in - var r: [RenderedChannelParticipant] = [] - var b: [RenderedChannelParticipant] = [] - var peerIds = Set() - for participant in restricted { - if !peerIds.contains(participant.peer.id) { - peerIds.insert(participant.peer.id) - r.append(participant) - } - } - for participant in banned { - if !peerIds.contains(participant.peer.id) { - peerIds.insert(participant.peer.id) - b.append(participant) - } - } - return ChannelBlacklist(banned: b, restricted: r) - } -} - -public func updateChannelMemberBannedRights(account: Account, peerId: PeerId, memberId: PeerId, rights: TelegramChatBannedRights?) -> Signal<(ChannelParticipant?, RenderedChannelParticipant?, Bool), NoError> { - return fetchChannelParticipant(account: account, peerId: peerId, participantId: memberId) +func _internal_updateChannelMemberBannedRights(account: Account, peerId: PeerId, memberId: PeerId, rights: TelegramChatBannedRights?) -> Signal<(ChannelParticipant?, RenderedChannelParticipant?, Bool), NoError> { + return _internal_fetchChannelParticipant(account: account, peerId: peerId, participantId: memberId) |> mapToSignal { currentParticipant -> Signal<(ChannelParticipant?, RenderedChannelParticipant?, Bool), NoError> in return account.postbox.transaction { transaction -> Signal<(ChannelParticipant?, RenderedChannelParticipant?, Bool), NoError> in - if let peer = transaction.getPeer(peerId), let inputChannel = apiInputChannel(peer), let _ = transaction.getPeer(account.peerId), let memberPeer = transaction.getPeer(memberId), let inputUser = apiInputUser(memberPeer) { + if let peer = transaction.getPeer(peerId), let inputChannel = apiInputChannel(peer), let _ = transaction.getPeer(account.peerId), let memberPeer = transaction.getPeer(memberId), let inputPeer = apiInputPeer(memberPeer) { let updatedParticipant: ChannelParticipant if let currentParticipant = currentParticipant, case let .member(_, invitedAt, _, currentBanInfo, _) = currentParticipant { let banInfo: ChannelParticipantBannedInfo? @@ -152,8 +28,15 @@ public func updateChannelMemberBannedRights(account: Account, peerId: PeerId, me } updatedParticipant = ChannelParticipant.member(id: memberId, invitedAt: Int32(Date().timeIntervalSince1970), adminInfo: nil, banInfo: banInfo, rank: nil) } + + let apiRights: Api.ChatBannedRights + if let rights = rights, !rights.flags.isEmpty { + apiRights = rights.apiBannedRights + } else { + apiRights = .chatBannedRights(flags: 0, untilDate: 0) + } - return account.network.request(Api.functions.channels.editBanned(channel: inputChannel, userId: inputUser, bannedRights: rights?.apiBannedRights ?? Api.ChatBannedRights.chatBannedRights(flags: 0, untilDate: 0))) + return account.network.request(Api.functions.channels.editBanned(channel: inputChannel, participant: inputPeer, bannedRights: apiRights)) |> retryRequest |> mapToSignal { result -> Signal<(ChannelParticipant?, RenderedChannelParticipant?, Bool), NoError> in account.stateManager.addUpdates(result) @@ -251,7 +134,7 @@ public func updateChannelMemberBannedRights(account: Account, peerId: PeerId, me } } -public func updateDefaultChannelMemberBannedRights(account: Account, peerId: PeerId, rights: TelegramChatBannedRights) -> Signal { +func _internal_updateDefaultChannelMemberBannedRights(account: Account, peerId: PeerId, rights: TelegramChatBannedRights) -> Signal { return account.postbox.transaction { transaction -> Signal in guard let peer = transaction.getPeer(peerId), let inputPeer = apiInputPeer(peer), let _ = transaction.getPeer(account.peerId) else { return .complete() diff --git a/submodules/TelegramCore/Sources/ChannelCreation.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelCreation.swift similarity index 89% rename from submodules/TelegramCore/Sources/ChannelCreation.swift rename to submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelCreation.swift index 9066882f6d..afb9dfcbe6 100644 --- a/submodules/TelegramCore/Sources/ChannelCreation.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelCreation.swift @@ -71,11 +71,11 @@ private func createChannel(account: Account, title: String, description: String? |> switchToLatest } -public func createChannel(account: Account, title: String, description: String?) -> Signal { +func _internal_createChannel(account: Account, title: String, description: String?) -> Signal { return createChannel(account: account, title: title, description: description, isSupergroup: false) } -public func createSupergroup(account: Account, title: String, description: String?, location: (latitude: Double, longitude: Double, address: String)? = nil, isForHistoryImport: Bool = false) -> Signal { +func _internal_createSupergroup(account: Account, title: String, description: String?, location: (latitude: Double, longitude: Double, address: String)? = nil, isForHistoryImport: Bool = false) -> Signal { return createChannel(account: account, title: title, description: description, isSupergroup: true, location: location, isForHistoryImport: isForHistoryImport) } @@ -83,7 +83,7 @@ public enum DeleteChannelError { case generic } -public func deleteChannel(account: Account, peerId: PeerId) -> Signal { +func _internal_deleteChannel(account: Account, peerId: PeerId) -> Signal { return account.postbox.transaction { transaction -> Api.InputChannel? in return transaction.getPeer(peerId).flatMap(apiInputChannel) } diff --git a/submodules/TelegramCore/Sources/ChannelHistoryAvailabilitySettings.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelHistoryAvailabilitySettings.swift similarity index 88% rename from submodules/TelegramCore/Sources/ChannelHistoryAvailabilitySettings.swift rename to submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelHistoryAvailabilitySettings.swift index 3d05721897..dd66a2d550 100644 --- a/submodules/TelegramCore/Sources/ChannelHistoryAvailabilitySettings.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelHistoryAvailabilitySettings.swift @@ -9,7 +9,7 @@ public enum ChannelHistoryAvailabilityError { case hasNotPermissions } -public func updateChannelHistoryAvailabilitySettingsInteractively(postbox: Postbox, network: Network, accountStateManager: AccountStateManager, peerId: PeerId, historyAvailableForNewMembers: Bool) -> Signal { +func _internal_updateChannelHistoryAvailabilitySettingsInteractively(postbox: Postbox, network: Network, accountStateManager: AccountStateManager, peerId: PeerId, historyAvailableForNewMembers: Bool) -> Signal { return postbox.transaction { transaction -> Peer? in return transaction.getPeer(peerId) } diff --git a/submodules/TelegramCore/Sources/ChannelMembers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelMembers.swift similarity index 90% rename from submodules/TelegramCore/Sources/ChannelMembers.swift rename to submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelMembers.swift index 2fe9cbed67..bcfee6c545 100644 --- a/submodules/TelegramCore/Sources/ChannelMembers.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelMembers.swift @@ -21,7 +21,7 @@ public enum ChannelMembersCategory { case mentions(threadId: MessageId?, filter: ChannelMembersCategoryFilter) } -public func channelMembers(postbox: Postbox, network: Network, accountPeerId: PeerId, peerId: PeerId, category: ChannelMembersCategory = .recent(.all), offset: Int32 = 0, limit: Int32 = 64, hash: Int32 = 0) -> Signal<[RenderedChannelParticipant]?, NoError> { +func _internal_channelMembers(postbox: Postbox, network: Network, accountPeerId: PeerId, peerId: PeerId, category: ChannelMembersCategory = .recent(.all), offset: Int32 = 0, limit: Int32 = 64, hash: Int32 = 0) -> Signal<[RenderedChannelParticipant]?, NoError> { return postbox.transaction { transaction -> Signal<[RenderedChannelParticipant]?, NoError> in if let peer = transaction.getPeer(peerId), let inputChannel = apiInputChannel(peer) { let apiFilter: Api.ChannelParticipantsFilter @@ -83,7 +83,7 @@ public func channelMembers(postbox: Postbox, network: Network, accountPeerId: Pe return postbox.transaction { transaction -> [RenderedChannelParticipant]? in var items: [RenderedChannelParticipant] = [] switch result { - case let .channelParticipants(_, participants, users): + case let .channelParticipants(_, participants, chats, users): var peers: [PeerId: Peer] = [:] var presences: [PeerId: PeerPresence] = [:] for user in users { @@ -93,6 +93,11 @@ public func channelMembers(postbox: Postbox, network: Network, accountPeerId: Pe presences[peer.id] = presence } } + for chat in chats { + if let groupOrChannel = parseTelegramGroupOrChannel(chat: chat) { + peers[groupOrChannel.id] = groupOrChannel + } + } updatePeers(transaction: transaction, peers: Array(peers.values), update: { _, updated in return updated }) diff --git a/submodules/TelegramCore/Sources/ChannelOwnershipTransfer.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelOwnershipTransfer.swift similarity index 93% rename from submodules/TelegramCore/Sources/ChannelOwnershipTransfer.swift rename to submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelOwnershipTransfer.swift index 976766c460..8d57c103a8 100644 --- a/submodules/TelegramCore/Sources/ChannelOwnershipTransfer.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelOwnershipTransfer.swift @@ -21,7 +21,7 @@ public enum ChannelOwnershipTransferError { case userBlocked } -public func checkOwnershipTranfserAvailability(postbox: Postbox, network: Network, accountStateManager: AccountStateManager, memberId: PeerId) -> Signal { +func _internal_checkOwnershipTranfserAvailability(postbox: Postbox, network: Network, accountStateManager: AccountStateManager, memberId: PeerId) -> Signal { return postbox.transaction { transaction -> Peer? in return transaction.getPeer(memberId) } @@ -72,12 +72,12 @@ public func checkOwnershipTranfserAvailability(postbox: Postbox, network: Networ } } -public func updateChannelOwnership(account: Account, accountStateManager: AccountStateManager, channelId: PeerId, memberId: PeerId, password: String) -> Signal<[(ChannelParticipant?, RenderedChannelParticipant)], ChannelOwnershipTransferError> { +func _internal_updateChannelOwnership(account: Account, accountStateManager: AccountStateManager, channelId: PeerId, memberId: PeerId, password: String) -> Signal<[(ChannelParticipant?, RenderedChannelParticipant)], ChannelOwnershipTransferError> { guard !password.isEmpty else { return .fail(.invalidPassword) } - return combineLatest(fetchChannelParticipant(account: account, peerId: channelId, participantId: account.peerId), fetchChannelParticipant(account: account, peerId: channelId, participantId: memberId)) + return combineLatest(_internal_fetchChannelParticipant(account: account, peerId: channelId, participantId: account.peerId), _internal_fetchChannelParticipant(account: account, peerId: channelId, participantId: memberId)) |> mapError { error -> ChannelOwnershipTransferError in return .generic } @@ -95,7 +95,7 @@ public func updateChannelOwnership(account: Account, accountStateManager: Accoun let updatedParticipant = ChannelParticipant.creator(id: user.id, adminInfo: nil, rank: currentParticipant?.rank) let updatedPreviousCreator = ChannelParticipant.member(id: accountUser.id, invitedAt: Int32(Date().timeIntervalSince1970), adminInfo: ChannelParticipantAdminInfo(rights: TelegramChatAdminRights(rights: flags), promotedBy: accountUser.id, canBeEditedByAccountPeer: false), banInfo: nil, rank: currentCreator?.rank) - let checkPassword = twoStepAuthData(account.network) + let checkPassword = _internal_twoStepAuthData(account.network) |> mapError { error -> ChannelOwnershipTransferError in if error.errorDescription.hasPrefix("FLOOD_WAIT") { return .limitExceeded diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelParticipants.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelParticipants.swift new file mode 100644 index 0000000000..6f975acb71 --- /dev/null +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelParticipants.swift @@ -0,0 +1,25 @@ +import Foundation +import Postbox +import SwiftSignalKit +import TelegramApi +import MtProtoKit + +import SyncCore + +public struct RenderedChannelParticipant: Equatable { + public let participant: ChannelParticipant + public let peer: Peer + public let peers: [PeerId: Peer] + public let presences: [PeerId: PeerPresence] + + public init(participant: ChannelParticipant, peer: Peer, peers: [PeerId: Peer] = [:], presences: [PeerId: PeerPresence] = [:]) { + self.participant = participant + self.peer = peer + self.peers = peers + self.presences = presences + } + + public static func ==(lhs: RenderedChannelParticipant, rhs: RenderedChannelParticipant) -> Bool { + return lhs.participant == rhs.participant && lhs.peer.isEqual(rhs.peer) + } +} diff --git a/submodules/TelegramCore/Sources/ChatListFiltering.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChatListFiltering.swift similarity index 95% rename from submodules/TelegramCore/Sources/ChatListFiltering.swift rename to submodules/TelegramCore/Sources/TelegramEngine/Peers/ChatListFiltering.swift index 363bf5e38b..d94bb2b995 100644 --- a/submodules/TelegramCore/Sources/ChatListFiltering.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChatListFiltering.swift @@ -298,22 +298,22 @@ extension ChatListFilter { includePeers: ChatListFilterIncludePeers(rawPeers: includePeers.compactMap { peer -> PeerId? in switch peer { case let .inputPeerUser(userId, _): - return PeerId(namespace: Namespaces.Peer.CloudUser, id: userId) + return PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt32Value(userId)) case let .inputPeerChat(chatId): - return PeerId(namespace: Namespaces.Peer.CloudGroup, id: chatId) + return PeerId(namespace: Namespaces.Peer.CloudGroup, id: PeerId.Id._internalFromInt32Value(chatId)) case let .inputPeerChannel(channelId, _): - return PeerId(namespace: Namespaces.Peer.CloudChannel, id: channelId) + return PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt32Value(channelId)) default: return nil } }, rawPinnedPeers: pinnedPeers.compactMap { peer -> PeerId? in switch peer { case let .inputPeerUser(userId, _): - return PeerId(namespace: Namespaces.Peer.CloudUser, id: userId) + return PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt32Value(userId)) case let .inputPeerChat(chatId): - return PeerId(namespace: Namespaces.Peer.CloudGroup, id: chatId) + return PeerId(namespace: Namespaces.Peer.CloudGroup, id: PeerId.Id._internalFromInt32Value(chatId)) case let .inputPeerChannel(channelId, _): - return PeerId(namespace: Namespaces.Peer.CloudChannel, id: channelId) + return PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt32Value(channelId)) default: return nil } @@ -321,11 +321,11 @@ extension ChatListFilter { excludePeers: excludePeers.compactMap { peer -> PeerId? in switch peer { case let .inputPeerUser(userId, _): - return PeerId(namespace: Namespaces.Peer.CloudUser, id: userId) + return PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt32Value(userId)) case let .inputPeerChat(chatId): - return PeerId(namespace: Namespaces.Peer.CloudGroup, id: chatId) + return PeerId(namespace: Namespaces.Peer.CloudGroup, id: PeerId.Id._internalFromInt32Value(chatId)) case let .inputPeerChannel(channelId, _): - return PeerId(namespace: Namespaces.Peer.CloudChannel, id: channelId) + return PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt32Value(channelId)) default: return nil } @@ -367,7 +367,7 @@ public enum RequestUpdateChatListFilterError { case generic } -public func requestUpdateChatListFilter(postbox: Postbox, network: Network, id: Int32, filter: ChatListFilter?) -> Signal { +func _internal_requestUpdateChatListFilter(postbox: Postbox, network: Network, id: Int32, filter: ChatListFilter?) -> Signal { return postbox.transaction { transaction -> Api.DialogFilter? in return filter?.apiFilter(transaction: transaction) } @@ -391,7 +391,7 @@ public enum RequestUpdateChatListFilterOrderError { case generic } -public func requestUpdateChatListFilterOrder(account: Account, ids: [Int32]) -> Signal { +func _internal_requestUpdateChatListFilterOrder(account: Account, ids: [Int32]) -> Signal { return account.network.request(Api.functions.messages.updateDialogFiltersOrder(order: ids)) |> mapError { _ -> RequestUpdateChatListFilterOrderError in return .generic @@ -426,11 +426,11 @@ private func requestChatListFilters(accountPeerId: PeerId, postbox: Postbox, net var peerId: PeerId? switch peer { case let .inputPeerUser(userId, _): - peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: userId) + peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt32Value(userId)) case let .inputPeerChat(chatId): - peerId = PeerId(namespace: Namespaces.Peer.CloudGroup, id: chatId) + peerId = PeerId(namespace: Namespaces.Peer.CloudGroup, id: PeerId.Id._internalFromInt32Value(chatId)) case let .inputPeerChannel(channelId, _): - peerId = PeerId(namespace: Namespaces.Peer.CloudChannel, id: channelId) + peerId = PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt32Value(channelId)) default: break } @@ -446,11 +446,11 @@ private func requestChatListFilters(accountPeerId: PeerId, postbox: Postbox, net var peerId: PeerId? switch peer { case let .inputPeerUser(userId, _): - peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: userId) + peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt32Value(userId)) case let .inputPeerChat(chatId): - peerId = PeerId(namespace: Namespaces.Peer.CloudGroup, id: chatId) + peerId = PeerId(namespace: Namespaces.Peer.CloudGroup, id: PeerId.Id._internalFromInt32Value(chatId)) case let .inputPeerChannel(channelId, _): - peerId = PeerId(namespace: Namespaces.Peer.CloudChannel, id: channelId) + peerId = PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt32Value(channelId)) default: break } @@ -781,7 +781,7 @@ struct ChatListFiltersState: PreferencesEntry, Equatable { } } -public func generateNewChatListFilterId(filters: [ChatListFilter]) -> Int32 { +func _internal_generateNewChatListFilterId(filters: [ChatListFilter]) -> Int32 { while true { let id = Int32(2 + arc4random_uniform(255 - 2)) if !filters.contains(where: { $0.id == id }) { @@ -790,7 +790,7 @@ public func generateNewChatListFilterId(filters: [ChatListFilter]) -> Int32 { } } -public func updateChatListFiltersInteractively(postbox: Postbox, _ f: @escaping ([ChatListFilter]) -> [ChatListFilter]) -> Signal<[ChatListFilter], NoError> { +func _internal_updateChatListFiltersInteractively(postbox: Postbox, _ f: @escaping ([ChatListFilter]) -> [ChatListFilter]) -> Signal<[ChatListFilter], NoError> { return postbox.transaction { transaction -> [ChatListFilter] in var updated: [ChatListFilter] = [] var hasUpdates = false @@ -811,7 +811,7 @@ public func updateChatListFiltersInteractively(postbox: Postbox, _ f: @escaping } } -public func updateChatListFiltersInteractively(transaction: Transaction, _ f: ([ChatListFilter]) -> [ChatListFilter]) { +func _internal_updateChatListFiltersInteractively(transaction: Transaction, _ f: ([ChatListFilter]) -> [ChatListFilter]) { var hasUpdates = false transaction.updatePreferencesEntry(key: PreferencesKeys.chatListFilters, { entry in var state = entry as? ChatListFiltersState ?? ChatListFiltersState.default @@ -828,7 +828,7 @@ public func updateChatListFiltersInteractively(transaction: Transaction, _ f: ([ } -public func updatedChatListFilters(postbox: Postbox) -> Signal<[ChatListFilter], NoError> { +func _internal_updatedChatListFilters(postbox: Postbox) -> Signal<[ChatListFilter], NoError> { return postbox.preferencesView(keys: [PreferencesKeys.chatListFilters]) |> map { preferences -> [ChatListFilter] in let filtersState = preferences.values[PreferencesKeys.chatListFilters] as? ChatListFiltersState ?? ChatListFiltersState.default @@ -837,7 +837,7 @@ public func updatedChatListFilters(postbox: Postbox) -> Signal<[ChatListFilter], |> distinctUntilChanged } -public func updatedChatListFiltersInfo(postbox: Postbox) -> Signal<(filters: [ChatListFilter], synchronized: Bool), NoError> { +func _internal_updatedChatListFiltersInfo(postbox: Postbox) -> Signal<(filters: [ChatListFilter], synchronized: Bool), NoError> { return postbox.preferencesView(keys: [PreferencesKeys.chatListFilters]) |> map { preferences -> (filters: [ChatListFilter], synchronized: Bool) in let filtersState = preferences.values[PreferencesKeys.chatListFilters] as? ChatListFiltersState ?? ChatListFiltersState.default @@ -854,7 +854,7 @@ public func updatedChatListFiltersInfo(postbox: Postbox) -> Signal<(filters: [Ch }) } -public func currentChatListFilters(postbox: Postbox) -> Signal<[ChatListFilter], NoError> { +func _internal_currentChatListFilters(postbox: Postbox) -> Signal<[ChatListFilter], NoError> { return postbox.transaction { transaction -> [ChatListFilter] in let settings = transaction.getPreferencesEntry(key: PreferencesKeys.chatListFilters) as? ChatListFiltersState ?? ChatListFiltersState.default return settings.filters @@ -941,7 +941,7 @@ public struct ChatListFiltersFeaturedState: PreferencesEntry, Equatable { } } -public func markChatListFeaturedFiltersAsSeen(postbox: Postbox) -> Signal { +func _internal_markChatListFeaturedFiltersAsSeen(postbox: Postbox) -> Signal { return postbox.transaction { transaction -> Void in transaction.updatePreferencesEntry(key: PreferencesKeys.chatListFiltersFeaturedState, { entry in guard var state = entry as? ChatListFiltersFeaturedState else { @@ -954,7 +954,7 @@ public func markChatListFeaturedFiltersAsSeen(postbox: Postbox) -> Signal ignoreValues } -public func unmarkChatListFeaturedFiltersAsSeen(transaction: Transaction) { +func _internal_unmarkChatListFeaturedFiltersAsSeen(transaction: Transaction) { transaction.updatePreferencesEntry(key: PreferencesKeys.chatListFiltersFeaturedState, { entry in guard var state = entry as? ChatListFiltersFeaturedState else { return entry @@ -964,7 +964,7 @@ public func unmarkChatListFeaturedFiltersAsSeen(transaction: Transaction) { }) } -public func updateChatListFeaturedFilters(postbox: Postbox, network: Network) -> Signal { +func _internal_updateChatListFeaturedFilters(postbox: Postbox, network: Network) -> Signal { return network.request(Api.functions.messages.getSuggestedDialogFilters()) |> `catch` { _ -> Signal<[Api.DialogFilterSuggested], NoError> in return .single([]) @@ -1092,7 +1092,7 @@ private func withTakenOperation(postbox: Postbox, peerId: PeerId, tag: PeerOpera func requestChatListFiltersSync(transaction: Transaction) { let tag: PeerOperationLogTag = OperationLogTags.SynchronizeChatListFilters - let peerId = PeerId(namespace: 0, id: 0) + let peerId = PeerId(0) var topOperation: (SynchronizeChatListFiltersOperation, Int32)? transaction.operationLogEnumerateEntries(peerId: peerId, tag: tag, { entry in @@ -1111,7 +1111,7 @@ func requestChatListFiltersSync(transaction: Transaction) { func managedChatListFilters(postbox: Postbox, network: Network, accountPeerId: PeerId) -> Signal { return Signal { _ in - let updateFeaturedDisposable = updateChatListFeaturedFilters(postbox: postbox, network: network).start() + let updateFeaturedDisposable = _internal_updateChatListFeaturedFilters(postbox: postbox, network: network).start() let _ = postbox.transaction({ transaction in requestChatListFiltersSync(transaction: transaction) }).start() @@ -1218,7 +1218,7 @@ private func synchronizeChatListFilters(transaction: Transaction, accountPeerId: if !mergedFilterIds.contains(where: { $0 == filter.id }) { deleteSignals = deleteSignals |> then( - requestUpdateChatListFilter(postbox: postbox, network: network, id: filter.id, filter: nil) + _internal_requestUpdateChatListFilter(postbox: postbox, network: network, id: filter.id, filter: nil) |> `catch` { _ -> Signal in return .complete() } @@ -1238,7 +1238,7 @@ private func synchronizeChatListFilters(transaction: Transaction, accountPeerId: if updated { addSignals = addSignals |> then( - requestUpdateChatListFilter(postbox: postbox, network: network, id: filter.id, filter: filter) + _internal_requestUpdateChatListFilter(postbox: postbox, network: network, id: filter.id, filter: filter) |> `catch` { _ -> Signal in return .complete() } diff --git a/submodules/TelegramCore/Sources/ChatOnlineMembers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChatOnlineMembers.swift similarity index 86% rename from submodules/TelegramCore/Sources/ChatOnlineMembers.swift rename to submodules/TelegramCore/Sources/TelegramEngine/Peers/ChatOnlineMembers.swift index 6be97c6046..aeb9e8593e 100644 --- a/submodules/TelegramCore/Sources/ChatOnlineMembers.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChatOnlineMembers.swift @@ -3,7 +3,7 @@ import SwiftSignalKit import Postbox import TelegramApi -public func chatOnlineMembers(postbox: Postbox, network: Network, peerId: PeerId) -> Signal { +func _internal_chatOnlineMembers(postbox: Postbox, network: Network, peerId: PeerId) -> Signal { return postbox.transaction { transaction -> Api.InputPeer? in return transaction.getPeer(peerId).flatMap(apiInputPeer) } diff --git a/submodules/TelegramCore/Sources/CheckPeerChatServiceActions.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/CheckPeerChatServiceActions.swift similarity index 87% rename from submodules/TelegramCore/Sources/CheckPeerChatServiceActions.swift rename to submodules/TelegramCore/Sources/TelegramEngine/Peers/CheckPeerChatServiceActions.swift index d72f1b809a..01b5acd4ce 100644 --- a/submodules/TelegramCore/Sources/CheckPeerChatServiceActions.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/CheckPeerChatServiceActions.swift @@ -4,7 +4,7 @@ import SwiftSignalKit import SyncCore -public func checkPeerChatServiceActions(postbox: Postbox, peerId: PeerId) -> Signal { +func _internal_checkPeerChatServiceActions(postbox: Postbox, peerId: PeerId) -> Signal { return postbox.transaction { transaction -> Void in transaction.applyMarkUnread(peerId: peerId, namespace: Namespaces.Message.SecretIncoming, value: false, interactive: true) diff --git a/submodules/TelegramCore/Sources/ConvertGroupToSupergroup.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ConvertGroupToSupergroup.swift similarity index 93% rename from submodules/TelegramCore/Sources/ConvertGroupToSupergroup.swift rename to submodules/TelegramCore/Sources/TelegramEngine/Peers/ConvertGroupToSupergroup.swift index a91862c0d2..2edaf4434c 100644 --- a/submodules/TelegramCore/Sources/ConvertGroupToSupergroup.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ConvertGroupToSupergroup.swift @@ -9,8 +9,8 @@ public enum ConvertGroupToSupergroupError { case tooManyChannels } -public func convertGroupToSupergroup(account: Account, peerId: PeerId) -> Signal { - return account.network.request(Api.functions.messages.migrateChat(chatId: peerId.id)) +func _internal_convertGroupToSupergroup(account: Account, peerId: PeerId) -> Signal { + return account.network.request(Api.functions.messages.migrateChat(chatId: peerId.id._internalGetInt32Value())) |> mapError { error -> ConvertGroupToSupergroupError in if error.errorDescription == "CHANNELS_TOO_MUCH" { return .tooManyChannels diff --git a/submodules/TelegramCore/Sources/CreateGroup.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/CreateGroup.swift similarity index 93% rename from submodules/TelegramCore/Sources/CreateGroup.swift rename to submodules/TelegramCore/Sources/TelegramEngine/Peers/CreateGroup.swift index ec68df3240..4245f6cb52 100644 --- a/submodules/TelegramCore/Sources/CreateGroup.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/CreateGroup.swift @@ -13,7 +13,7 @@ public enum CreateGroupError { case serverProvided(String) } -public func createGroup(account: Account, title: String, peerIds: [PeerId]) -> Signal { +func _internal_createGroup(account: Account, title: String, peerIds: [PeerId]) -> Signal { return account.postbox.transaction { transaction -> Signal in var inputUsers: [Api.InputUser] = [] for peerId in peerIds { diff --git a/submodules/TelegramCore/Sources/CreateSecretChat.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/CreateSecretChat.swift similarity index 96% rename from submodules/TelegramCore/Sources/CreateSecretChat.swift rename to submodules/TelegramCore/Sources/TelegramEngine/Peers/CreateSecretChat.swift index 9ce1c2b48b..7d57209fb7 100644 --- a/submodules/TelegramCore/Sources/CreateSecretChat.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/CreateSecretChat.swift @@ -9,7 +9,7 @@ public enum CreateSecretChatError { case limitExceeded } -public func createSecretChat(account: Account, peerId: PeerId) -> Signal { +func _internal_createSecretChat(account: Account, peerId: PeerId) -> Signal { return account.postbox.transaction { transaction -> Signal in if let peer = transaction.getPeer(peerId), let inputUser = apiInputUser(peer) { return validatedEncryptionConfig(postbox: account.postbox, network: account.network) diff --git a/submodules/TelegramCore/Sources/FindChannelById.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/FindChannelById.swift similarity index 90% rename from submodules/TelegramCore/Sources/FindChannelById.swift rename to submodules/TelegramCore/Sources/TelegramEngine/Peers/FindChannelById.swift index ec269da970..9d288f6278 100644 --- a/submodules/TelegramCore/Sources/FindChannelById.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/FindChannelById.swift @@ -3,7 +3,7 @@ import SwiftSignalKit import Postbox import TelegramApi -public func findChannelById(postbox: Postbox, network: Network, channelId: Int32) -> Signal { +func _internal_findChannelById(postbox: Postbox, network: Network, channelId: Int32) -> Signal { return network.request(Api.functions.channels.getChannels(id: [.inputChannel(channelId: channelId, accessHash: 0)])) |> map(Optional.init) |> `catch` { _ -> Signal in diff --git a/submodules/TelegramCore/Sources/GroupsInCommon.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/GroupsInCommon.swift similarity index 78% rename from submodules/TelegramCore/Sources/GroupsInCommon.swift rename to submodules/TelegramCore/Sources/TelegramEngine/Peers/GroupsInCommon.swift index 85e72c3aac..9fad743857 100644 --- a/submodules/TelegramCore/Sources/GroupsInCommon.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/GroupsInCommon.swift @@ -58,7 +58,7 @@ private final class GroupsInCommonContextImpl { guard let inputUser = inputUser else { return .single(([], 0)) } - return network.request(Api.functions.messages.getCommonChats(userId: inputUser, maxId: maxId ?? 0, limit: limit)) + return network.request(Api.functions.messages.getCommonChats(userId: inputUser, maxId: maxId?._internalGetInt32Value() ?? 0, limit: limit)) |> map(Optional.init) |> `catch` { _ -> Signal in return .single(nil) @@ -154,36 +154,3 @@ public final class GroupsInCommonContext { } } } - -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<[Peer], NoError> in - let chats: [Api.Chat] - switch result { - case let .chats(chats: apiChats): - chats = apiChats - case let .chatsSlice(count: _, chats: apiChats): - chats = apiChats - } - - return account.postbox.transaction { transaction -> [Peer] 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 - } - } - } else { - return .single([]) - } - } |> switchToLatest -} diff --git a/submodules/TelegramCore/Sources/InactiveChannels.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/InactiveChannels.swift similarity index 94% rename from submodules/TelegramCore/Sources/InactiveChannels.swift rename to submodules/TelegramCore/Sources/TelegramEngine/Peers/InactiveChannels.swift index 9d36592941..718fd44925 100644 --- a/submodules/TelegramCore/Sources/InactiveChannels.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/InactiveChannels.swift @@ -19,7 +19,7 @@ public struct InactiveChannel : Equatable { } } -public func inactiveChannelList(network: Network) -> Signal<[InactiveChannel], NoError> { +func _internal_inactiveChannelList(network: Network) -> Signal<[InactiveChannel], NoError> { return network.request(Api.functions.channels.getInactiveChannels()) |> retryRequest |> map { result in diff --git a/submodules/TelegramCore/Sources/InvitationLinks.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/InvitationLinks.swift similarity index 94% rename from submodules/TelegramCore/Sources/InvitationLinks.swift rename to submodules/TelegramCore/Sources/TelegramEngine/Peers/InvitationLinks.swift index d5d048ddaf..840afb6ce2 100644 --- a/submodules/TelegramCore/Sources/InvitationLinks.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/InvitationLinks.swift @@ -3,11 +3,9 @@ import Postbox import SwiftSignalKit import TelegramApi import MtProtoKit - import SyncCore - -public func revokePersistentPeerExportedInvitation(account: Account, peerId: PeerId) -> Signal { +func _internal_revokePersistentPeerExportedInvitation(account: Account, peerId: PeerId) -> Signal { return account.postbox.transaction { transaction -> Signal in if let peer = transaction.getPeer(peerId), let inputPeer = apiInputPeer(peer) { let flags: Int32 = (1 << 2) @@ -58,7 +56,7 @@ public enum CreatePeerExportedInvitationError { case generic } -public func createPeerExportedInvitation(account: Account, peerId: PeerId, expireDate: Int32?, usageLimit: Int32?) -> Signal { +func _internal_createPeerExportedInvitation(account: Account, peerId: PeerId, expireDate: Int32?, usageLimit: Int32?) -> Signal { return account.postbox.transaction { transaction -> Signal in if let peer = transaction.getPeer(peerId), let inputPeer = apiInputPeer(peer) { var flags: Int32 = 0 @@ -85,7 +83,7 @@ public enum EditPeerExportedInvitationError { case generic } -public func editPeerExportedInvitation(account: Account, peerId: PeerId, link: String, expireDate: Int32?, usageLimit: Int32?) -> Signal { +func _internal_editPeerExportedInvitation(account: Account, peerId: PeerId, link: String, expireDate: Int32?, usageLimit: Int32?) -> Signal { return account.postbox.transaction { transaction -> Signal in if let peer = transaction.getPeer(peerId), let inputPeer = apiInputPeer(peer) { var flags: Int32 = 0 @@ -131,7 +129,7 @@ public enum RevokeExportedInvitationResult { case replace(ExportedInvitation, ExportedInvitation) } -public func revokePeerExportedInvitation(account: Account, peerId: PeerId, link: String) -> Signal { +func _internal_revokePeerExportedInvitation(account: Account, peerId: PeerId, link: String) -> Signal { return account.postbox.transaction { transaction -> Signal in if let peer = transaction.getPeer(peerId), let inputPeer = apiInputPeer(peer) { let flags: Int32 = (1 << 2) @@ -197,7 +195,7 @@ public struct ExportedInvitations : Equatable { public let totalCount: Int32 } -public func peerExportedInvitations(account: Account, peerId: PeerId, revoked: Bool, adminId: PeerId? = nil, offsetLink: ExportedInvitation? = nil) -> Signal { +func _internal_peerExportedInvitations(account: Account, peerId: PeerId, revoked: Bool, adminId: PeerId? = nil, offsetLink: ExportedInvitation? = nil) -> Signal { return account.postbox.transaction { transaction -> Signal in if let peer = transaction.getPeer(peerId), let inputPeer = apiInputPeer(peer), let adminPeer = transaction.getPeer(adminId ?? account.peerId), let adminId = apiInputUser(adminPeer) { var flags: Int32 = 0 @@ -244,7 +242,7 @@ public enum DeletePeerExportedInvitationError { case generic } -public func deletePeerExportedInvitation(account: Account, peerId: PeerId, link: String) -> Signal { +func _internal_deletePeerExportedInvitation(account: Account, peerId: PeerId, link: String) -> Signal { return account.postbox.transaction { transaction -> Signal in if let peer = transaction.getPeer(peerId), let inputPeer = apiInputPeer(peer) { return account.network.request(Api.functions.messages.deleteExportedChatInvite(peer: inputPeer, link: link)) @@ -258,7 +256,7 @@ public func deletePeerExportedInvitation(account: Account, peerId: PeerId, link: |> switchToLatest } -public func deleteAllRevokedPeerExportedInvitations(account: Account, peerId: PeerId, adminId: PeerId) -> Signal { +func _internal_deleteAllRevokedPeerExportedInvitations(account: Account, peerId: PeerId, adminId: PeerId) -> Signal { return account.postbox.transaction { transaction -> Signal in if let peer = transaction.getPeer(peerId), let inputPeer = apiInputPeer(peer), let adminPeer = transaction.getPeer(adminId), let inputAdminId = apiInputUser(adminPeer) { return account.network.request(Api.functions.messages.deleteRevokedExportedChatInvites(peer: inputPeer, adminId: inputAdminId)) @@ -304,7 +302,7 @@ final class CachedPeerExportedInvitations: PostboxCoding { let canLoadMore: Bool let count: Int32 - public static func key(peerId: PeerId, revoked: Bool) -> ValueBoxKey { + static func key(peerId: PeerId, revoked: Bool) -> ValueBoxKey { let key = ValueBoxKey(length: 8 + 4) key.setInt64(0, value: peerId.toInt64()) key.setInt32(8, value: revoked ? 1 : 0) @@ -317,13 +315,13 @@ final class CachedPeerExportedInvitations: PostboxCoding { self.count = count } - public init(decoder: PostboxDecoder) { + init(decoder: PostboxDecoder) { self.invitations = decoder.decodeObjectArrayForKey("invitations") self.canLoadMore = decoder.decodeBoolForKey("canLoadMore", orElse: false) self.count = decoder.decodeInt32ForKey("count", orElse: 0) } - public func encode(_ encoder: PostboxEncoder) { + func encode(_ encoder: PostboxEncoder) { encoder.encodeObjectArray(self.invitations, forKey: "invitations") encoder.encodeBool(self.canLoadMore, forKey: "canLoadMore") encoder.encodeInt32(self.count, forKey: "count") @@ -494,7 +492,7 @@ private final class PeerExportedInvitationsContextImpl { self.updateState() } - public func add(_ invite: ExportedInvitation) { + func add(_ invite: ExportedInvitation) { var results = self.results results.removeAll(where: { $0.link == invite.link}) results.insert(invite, at: 0) @@ -503,7 +501,7 @@ private final class PeerExportedInvitationsContextImpl { self.updateCache() } - public func update(_ invite: ExportedInvitation) { + func update(_ invite: ExportedInvitation) { var results = self.results if let index = self.results.firstIndex(where: { $0.link == invite.link }) { results[index] = invite @@ -513,7 +511,7 @@ private final class PeerExportedInvitationsContextImpl { self.updateCache() } - public func remove(_ invite: ExportedInvitation) { + func remove(_ invite: ExportedInvitation) { var results = self.results results.removeAll(where: { $0.link == invite.link}) self.results = results @@ -521,7 +519,7 @@ private final class PeerExportedInvitationsContextImpl { self.updateCache() } - public func clear() { + func clear() { self.results = [] self.count = 0 self.updateState() @@ -564,7 +562,7 @@ public final class PeerExportedInvitationsContext { } } - public init(account: Account, peerId: PeerId, adminId: PeerId?, revoked: Bool, forceUpdate: Bool) { + init(account: Account, peerId: PeerId, adminId: PeerId?, revoked: Bool, forceUpdate: Bool) { let queue = self.queue self.impl = QueueLocalObject(queue: queue, generate: { return PeerExportedInvitationsContextImpl(queue: queue, account: account, peerId: peerId, adminId: adminId, revoked: revoked, forceUpdate: forceUpdate) @@ -629,7 +627,7 @@ final class CachedPeerInvitationImporters: PostboxCoding { let dates: [PeerId: Int32] let count: Int32 - public static func key(peerId: PeerId, link: String) -> ValueBoxKey { + static func key(peerId: PeerId, link: String) -> ValueBoxKey { let key = ValueBoxKey(length: 8 + 4) key.setInt64(0, value: peerId.toInt64()) key.setInt32(8, value: Int32(HashFunctions.murMurHash32(link))) @@ -644,13 +642,13 @@ final class CachedPeerInvitationImporters: PostboxCoding { self.count = count } - public init(peerIds: [PeerId], dates: [PeerId: Int32], count: Int32) { + init(peerIds: [PeerId], dates: [PeerId: Int32], count: Int32) { self.peerIds = peerIds self.dates = dates self.count = count } - public init(decoder: PostboxDecoder) { + init(decoder: PostboxDecoder) { self.peerIds = decoder.decodeInt64ArrayForKey("peerIds").map(PeerId.init) var dates: [PeerId: Int32] = [:] @@ -658,7 +656,7 @@ final class CachedPeerInvitationImporters: PostboxCoding { for index in stride(from: 0, to: datesArray.endIndex, by: 2) { let userId = datesArray[index] let date = datesArray[index + 1] - let peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: userId) + let peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt32Value(userId)) dates[peerId] = date } self.dates = dates @@ -666,12 +664,12 @@ final class CachedPeerInvitationImporters: PostboxCoding { self.count = decoder.decodeInt32ForKey("count", orElse: 0) } - public func encode(_ encoder: PostboxEncoder) { + func encode(_ encoder: PostboxEncoder) { encoder.encodeInt64Array(self.peerIds.map { $0.toInt64() }, forKey: "peerIds") var dates: [Int32] = [] for (peerId, date) in self.dates { - dates.append(peerId.id) + dates.append(peerId.id._internalGetInt32Value()) dates.append(date) } encoder.encodeInt32Array(dates, forKey: "dates") @@ -791,7 +789,7 @@ private final class PeerInvitationImportersContextImpl { let date: Int32 switch importer { case let .chatInviteImporter(userId, dateValue): - peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: userId) + peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt32Value(userId)) date = dateValue } if let peer = transaction.getPeer(peerId) { @@ -859,7 +857,7 @@ public final class PeerInvitationImportersContext { } } - public init(account: Account, peerId: PeerId, invite: ExportedInvitation) { + init(account: Account, peerId: PeerId, invite: ExportedInvitation) { let queue = self.queue self.impl = QueueLocalObject(queue: queue, generate: { return PeerInvitationImportersContextImpl(queue: queue, account: account, peerId: peerId, invite: invite) @@ -879,7 +877,7 @@ public struct ExportedInvitationCreator : Equatable { public let revokedCount: Int32 } -public func peerExportedInvitationsCreators(account: Account, peerId: PeerId) -> Signal<[ExportedInvitationCreator], NoError> { +func _internal_peerExportedInvitationsCreators(account: Account, peerId: PeerId) -> Signal<[ExportedInvitationCreator], NoError> { return account.postbox.transaction { transaction -> Signal<[ExportedInvitationCreator], NoError> in if let peer = transaction.getPeer(peerId), let inputPeer = apiInputPeer(peer) { var isCreator = false @@ -911,7 +909,7 @@ public func peerExportedInvitationsCreators(account: Account, peerId: PeerId) -> for admin in admins { switch admin { case let .chatAdminWithInvites(adminId, invitesCount, revokedInvitesCount): - let peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: adminId) + let peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt32Value(adminId)) if let peer = peersMap[peerId], peerId != account.peerId { creators.append(ExportedInvitationCreator(peer: RenderedPeer(peer: peer), count: invitesCount, revokedCount: revokedInvitesCount)) } diff --git a/submodules/TelegramCore/Sources/JoinChannel.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/JoinChannel.swift similarity index 94% rename from submodules/TelegramCore/Sources/JoinChannel.swift rename to submodules/TelegramCore/Sources/TelegramEngine/Peers/JoinChannel.swift index 579ec090bf..88d2950e9f 100644 --- a/submodules/TelegramCore/Sources/JoinChannel.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/JoinChannel.swift @@ -12,7 +12,7 @@ public enum JoinChannelError { case tooMuchUsers } -public func joinChannel(account: Account, peerId: PeerId, hash: String?) -> Signal { +func _internal_joinChannel(account: Account, peerId: PeerId, hash: String?) -> Signal { return account.postbox.loadedPeerWithId(peerId) |> take(1) |> castError(JoinChannelError.self) @@ -38,7 +38,7 @@ public func joinChannel(account: Account, peerId: PeerId, hash: String?) -> Sign |> mapToSignal { updates -> Signal in account.stateManager.addUpdates(updates) - return account.network.request(Api.functions.channels.getParticipant(channel: inputChannel, userId: .inputUserSelf)) + return account.network.request(Api.functions.channels.getParticipant(channel: inputChannel, participant: .inputPeerSelf)) |> map(Optional.init) |> `catch` { _ -> Signal in return .single(nil) @@ -59,7 +59,7 @@ public func joinChannel(account: Account, peerId: PeerId, hash: String?) -> Sign } let updatedParticipant: ChannelParticipant switch result { - case let .channelParticipant(participant, _): + case let .channelParticipant(participant, _, _): updatedParticipant = ChannelParticipant(apiParticipant: participant) } if case let .member(_, _, maybeAdminInfo, _, _) = updatedParticipant { diff --git a/submodules/TelegramCore/Sources/JoinLink.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/JoinLink.swift similarity index 94% rename from submodules/TelegramCore/Sources/JoinLink.swift rename to submodules/TelegramCore/Sources/TelegramEngine/Peers/JoinLink.swift index cd8fbc8486..fccad10f9c 100644 --- a/submodules/TelegramCore/Sources/JoinLink.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/JoinLink.swift @@ -29,7 +29,7 @@ public enum ExternalJoiningChatState { case peek(PeerId, Int32) } -public func joinChatInteractively(with hash: String, account: Account) -> Signal { +func _internal_joinChatInteractively(with hash: String, account: Account) -> Signal { return account.network.request(Api.functions.messages.importChatInvite(hash: hash)) |> mapError { error -> JoinLinkError in switch error.errorDescription { @@ -59,7 +59,7 @@ public func joinChatInteractively(with hash: String, account: Account) -> Signal } } -public func joinLinkInformation(_ hash: String, account: Account) -> Signal { +func _internal_joinLinkInformation(_ hash: String, account: Account) -> Signal { return account.network.request(Api.functions.messages.checkChatInvite(hash: hash)) |> map(Optional.init) |> `catch` { _ -> Signal in diff --git a/submodules/TelegramCore/Sources/ManageChannelDiscussionGroup.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ManageChannelDiscussionGroup.swift similarity index 93% rename from submodules/TelegramCore/Sources/ManageChannelDiscussionGroup.swift rename to submodules/TelegramCore/Sources/TelegramEngine/Peers/ManageChannelDiscussionGroup.swift index e8fc694411..db0cd8f7d0 100644 --- a/submodules/TelegramCore/Sources/ManageChannelDiscussionGroup.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ManageChannelDiscussionGroup.swift @@ -9,7 +9,7 @@ public enum AvailableChannelDiscussionGroupError { case generic } -public func availableGroupsForChannelDiscussion(postbox: Postbox, network: Network) -> Signal<[Peer], AvailableChannelDiscussionGroupError> { +func _internal_availableGroupsForChannelDiscussion(postbox: Postbox, network: Network) -> Signal<[Peer], AvailableChannelDiscussionGroupError> { return network.request(Api.functions.channels.getGroupsForDiscussion()) |> mapError { error in return .generic @@ -39,7 +39,7 @@ public enum ChannelDiscussionGroupError { case tooManyChannels } -public func updateGroupDiscussionForChannel(network: Network, postbox: Postbox, channelId: PeerId?, groupId: PeerId?) -> Signal { +func _internal_updateGroupDiscussionForChannel(network: Network, postbox: Postbox, channelId: PeerId?, groupId: PeerId?) -> Signal { return postbox.transaction { transaction -> (channel: Peer?, group: Peer?) in return (channel: channelId.flatMap(transaction.getPeer), group: groupId.flatMap(transaction.getPeer)) } diff --git a/submodules/TelegramCore/Sources/NotificationExceptionsList.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/NotificationExceptionsList.swift similarity index 89% rename from submodules/TelegramCore/Sources/NotificationExceptionsList.swift rename to submodules/TelegramCore/Sources/TelegramEngine/Peers/NotificationExceptionsList.swift index e5fb3f8934..1c43e7a60f 100644 --- a/submodules/TelegramCore/Sources/NotificationExceptionsList.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/NotificationExceptionsList.swift @@ -19,7 +19,7 @@ public final class NotificationExceptionsList: Equatable { } } -public func notificationExceptionsList(postbox: Postbox, network: Network) -> Signal { +func _internal_notificationExceptionsList(postbox: Postbox, network: Network) -> Signal { return network.request(Api.functions.account.getNotifyExceptions(flags: 1 << 1, peer: nil)) |> retryRequest |> mapToSignal { result -> Signal in @@ -49,11 +49,11 @@ public func notificationExceptionsList(postbox: Postbox, network: Network) -> Si let peerId: PeerId switch notifyPeer { case let .peerUser(userId): - peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: userId) + peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt32Value(userId)) case let .peerChat(chatId): - peerId = PeerId(namespace: Namespaces.Peer.CloudGroup, id: chatId) + peerId = PeerId(namespace: Namespaces.Peer.CloudGroup, id: PeerId.Id._internalFromInt32Value(chatId)) case let .peerChannel(channelId): - peerId = PeerId(namespace: Namespaces.Peer.CloudChannel, id: channelId) + peerId = PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt32Value(channelId)) } settings[peerId] = TelegramPeerNotificationSettings(apiSettings: notifySettings) default: diff --git a/submodules/TelegramCore/Sources/PeerAdmins.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/PeerAdmins.swift similarity index 91% rename from submodules/TelegramCore/Sources/PeerAdmins.swift rename to submodules/TelegramCore/Sources/TelegramEngine/Peers/PeerAdmins.swift index f259e0b50f..9a8eea7fdf 100644 --- a/submodules/TelegramCore/Sources/PeerAdmins.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/PeerAdmins.swift @@ -10,11 +10,11 @@ public enum RemoveGroupAdminError { case generic } -public func removeGroupAdmin(account: Account, peerId: PeerId, adminId: PeerId) -> Signal { +func _internal_removeGroupAdmin(account: Account, peerId: PeerId, adminId: PeerId) -> Signal { return account.postbox.transaction { transaction -> Signal in if let peer = transaction.getPeer(peerId), let adminPeer = transaction.getPeer(adminId), let inputUser = apiInputUser(adminPeer) { if let group = peer as? TelegramGroup { - return account.network.request(Api.functions.messages.editChatAdmin(chatId: group.id.id, userId: inputUser, isAdmin: .boolFalse)) + return account.network.request(Api.functions.messages.editChatAdmin(chatId: group.id.id._internalGetInt32Value(), userId: inputUser, isAdmin: .boolFalse)) |> mapError { _ -> RemoveGroupAdminError in return .generic } |> mapToSignal { result -> Signal in return account.postbox.transaction { transaction -> Void in @@ -58,14 +58,14 @@ public enum AddGroupAdminError { case adminsTooMuch } -public func addGroupAdmin(account: Account, peerId: PeerId, adminId: PeerId) -> Signal { +func _internal_addGroupAdmin(account: Account, peerId: PeerId, adminId: PeerId) -> Signal { return account.postbox.transaction { transaction -> Signal in if let peer = transaction.getPeer(peerId), let adminPeer = transaction.getPeer(adminId), let inputUser = apiInputUser(adminPeer) { if let group = peer as? TelegramGroup { - return account.network.request(Api.functions.messages.editChatAdmin(chatId: group.id.id, userId: inputUser, isAdmin: .boolTrue)) + return account.network.request(Api.functions.messages.editChatAdmin(chatId: group.id.id._internalGetInt32Value(), userId: inputUser, isAdmin: .boolTrue)) |> `catch` { error -> Signal in if error.errorDescription == "USER_NOT_PARTICIPANT" { - return addGroupMember(account: account, peerId: peerId, memberId: adminId) + return _internal_addGroupMember(account: account, peerId: peerId, memberId: adminId) |> mapError { error -> AddGroupAdminError in return .addMemberError(error) } @@ -73,7 +73,7 @@ public func addGroupAdmin(account: Account, peerId: PeerId, adminId: PeerId) -> return .complete() } |> then( - account.network.request(Api.functions.messages.editChatAdmin(chatId: group.id.id, userId: inputUser, isAdmin: .boolTrue)) + account.network.request(Api.functions.messages.editChatAdmin(chatId: group.id.id._internalGetInt32Value(), userId: inputUser, isAdmin: .boolTrue)) |> mapError { error -> AddGroupAdminError in return .generic } @@ -127,14 +127,14 @@ public enum UpdateChannelAdminRightsError { case adminsTooMuch } -public func fetchChannelParticipant(account: Account, peerId: PeerId, participantId: PeerId) -> Signal { +func _internal_fetchChannelParticipant(account: Account, peerId: PeerId, participantId: PeerId) -> Signal { return account.postbox.transaction { transaction -> Signal in - if let peer = transaction.getPeer(peerId), let adminPeer = transaction.getPeer(participantId), let inputUser = apiInputUser(adminPeer) { + if let peer = transaction.getPeer(peerId), let adminPeer = transaction.getPeer(participantId), let inputPeer = apiInputPeer(adminPeer) { if let channel = peer as? TelegramChannel, let inputChannel = apiInputChannel(channel) { - return account.network.request(Api.functions.channels.getParticipant(channel: inputChannel, userId: inputUser)) + return account.network.request(Api.functions.channels.getParticipant(channel: inputChannel, participant: inputPeer)) |> map { result -> ChannelParticipant? in switch result { - case let .channelParticipant(participant, _): + case let .channelParticipant(participant, _, _): return ChannelParticipant(apiParticipant: participant) } } @@ -150,8 +150,8 @@ public func fetchChannelParticipant(account: Account, peerId: PeerId, participan } |> switchToLatest } -public func updateChannelAdminRights(account: Account, peerId: PeerId, adminId: PeerId, rights: TelegramChatAdminRights?, rank: String?) -> Signal<(ChannelParticipant?, RenderedChannelParticipant), UpdateChannelAdminRightsError> { - return fetchChannelParticipant(account: account, peerId: peerId, participantId: adminId) +func _internal_updateChannelAdminRights(account: Account, peerId: PeerId, adminId: PeerId, rights: TelegramChatAdminRights?, rank: String?) -> Signal<(ChannelParticipant?, RenderedChannelParticipant), UpdateChannelAdminRightsError> { + return _internal_fetchChannelParticipant(account: account, peerId: peerId, participantId: adminId) |> mapError { error -> UpdateChannelAdminRightsError in } |> mapToSignal { currentParticipant -> Signal<(ChannelParticipant?, RenderedChannelParticipant), UpdateChannelAdminRightsError> in @@ -188,7 +188,7 @@ public func updateChannelAdminRights(account: Account, peerId: PeerId, adminId: |> map { [$0] } |> `catch` { error -> Signal<[Api.Updates], UpdateChannelAdminRightsError> in if error.errorDescription == "USER_NOT_PARTICIPANT" { - return addChannelMember(account: account, peerId: peerId, memberId: adminId) + return _internal_addChannelMember(account: account, peerId: peerId, memberId: adminId) |> map { _ -> [Api.Updates] in return [] } diff --git a/submodules/TelegramCore/Sources/PeerCommands.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/PeerCommands.swift similarity index 92% rename from submodules/TelegramCore/Sources/PeerCommands.swift rename to submodules/TelegramCore/Sources/TelegramEngine/Peers/PeerCommands.swift index 3f6686ae98..f4be8281bd 100644 --- a/submodules/TelegramCore/Sources/PeerCommands.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/PeerCommands.swift @@ -26,7 +26,7 @@ public struct PeerCommands: Equatable { } } -public func peerCommands(account: Account, id: PeerId) -> Signal { +func _internal_peerCommands(account: Account, id: PeerId) -> Signal { return account.postbox.peerView(id: id) |> map { view -> PeerCommands in if let cachedUserData = view.cachedData as? CachedUserData { if let botInfo = cachedUserData.botInfo { @@ -39,8 +39,7 @@ public func peerCommands(account: Account, id: PeerId) -> Signal Signal<[Int: Data], NoError>) -> Signal { - return updatePeerPhoto(postbox: account.postbox, network: account.network, stateManager: account.stateManager, accountPeerId: account.peerId, peerId: account.peerId, photo: resource.flatMap({ uploadedPeerPhoto(postbox: account.postbox, network: account.network, resource: $0) }), video: videoResource.flatMap({ uploadedPeerVideo(postbox: account.postbox, network: account.network, messageMediaPreuploadManager: account.messageMediaPreuploadManager, resource: $0) |> map(Optional.init) }), videoStartTimestamp: videoStartTimestamp, mapResourceToAvatarSizes: mapResourceToAvatarSizes) +func _internal_updateAccountPhoto(account: Account, resource: MediaResource?, videoResource: MediaResource?, videoStartTimestamp: Double?, mapResourceToAvatarSizes: @escaping (MediaResource, [TelegramMediaImageRepresentation]) -> Signal<[Int: Data], NoError>) -> Signal { + return _internal_updatePeerPhoto(postbox: account.postbox, network: account.network, stateManager: account.stateManager, accountPeerId: account.peerId, peerId: account.peerId, photo: resource.flatMap({ _internal_uploadedPeerPhoto(postbox: account.postbox, network: account.network, resource: $0) }), video: videoResource.flatMap({ _internal_uploadedPeerVideo(postbox: account.postbox, network: account.network, messageMediaPreuploadManager: account.messageMediaPreuploadManager, resource: $0) |> map(Optional.init) }), videoStartTimestamp: videoStartTimestamp, mapResourceToAvatarSizes: mapResourceToAvatarSizes) } public struct UploadedPeerPhotoData { @@ -37,7 +37,7 @@ enum UploadedPeerPhotoDataContent { case error } -public func uploadedPeerPhoto(postbox: Postbox, network: Network, resource: MediaResource) -> Signal { +func _internal_uploadedPeerPhoto(postbox: Postbox, network: Network, resource: MediaResource) -> Signal { return multipartUpload(network: network, postbox: postbox, source: .resource(.standalone(resource: resource)), encrypt: false, tag: TelegramMediaResourceFetchTag(statsCategory: .image), hintFileSize: nil, hintFileIsLarge: false, forceNoBigParts: false) |> map { result -> UploadedPeerPhotoData in return UploadedPeerPhotoData(resource: resource, content: .result(result)) @@ -47,7 +47,7 @@ public func uploadedPeerPhoto(postbox: Postbox, network: Network, resource: Medi } } -public func uploadedPeerVideo(postbox: Postbox, network: Network, messageMediaPreuploadManager: MessageMediaPreuploadManager?, resource: MediaResource) -> Signal { +func _internal_uploadedPeerVideo(postbox: Postbox, network: Network, messageMediaPreuploadManager: MessageMediaPreuploadManager?, resource: MediaResource) -> Signal { if let messageMediaPreuploadManager = messageMediaPreuploadManager { return messageMediaPreuploadManager.upload(network: network, postbox: postbox, source: .resource(.standalone(resource: resource)), encrypt: false, tag: TelegramMediaResourceFetchTag(statsCategory: .video), hintFileSize: nil, hintFileIsLarge: false) |> map { result -> UploadedPeerPhotoData in @@ -67,11 +67,11 @@ public func uploadedPeerVideo(postbox: Postbox, network: Network, messageMediaPr } } -public func updatePeerPhoto(postbox: Postbox, network: Network, stateManager: AccountStateManager?, accountPeerId: PeerId, peerId: PeerId, photo: Signal?, video: Signal? = nil, videoStartTimestamp: Double? = nil, mapResourceToAvatarSizes: @escaping (MediaResource, [TelegramMediaImageRepresentation]) -> Signal<[Int: Data], NoError>) -> Signal { - return updatePeerPhotoInternal(postbox: postbox, network: network, stateManager: stateManager, accountPeerId: accountPeerId, peer: postbox.loadedPeerWithId(peerId), photo: photo, video: video, videoStartTimestamp: videoStartTimestamp, mapResourceToAvatarSizes: mapResourceToAvatarSizes) +func _internal_updatePeerPhoto(postbox: Postbox, network: Network, stateManager: AccountStateManager?, accountPeerId: PeerId, peerId: PeerId, photo: Signal?, video: Signal? = nil, videoStartTimestamp: Double? = nil, mapResourceToAvatarSizes: @escaping (MediaResource, [TelegramMediaImageRepresentation]) -> Signal<[Int: Data], NoError>) -> Signal { + return _internal_updatePeerPhotoInternal(postbox: postbox, network: network, stateManager: stateManager, accountPeerId: accountPeerId, peer: postbox.loadedPeerWithId(peerId), photo: photo, video: video, videoStartTimestamp: videoStartTimestamp, mapResourceToAvatarSizes: mapResourceToAvatarSizes) } -public func updatePeerPhotoInternal(postbox: Postbox, network: Network, stateManager: AccountStateManager?, accountPeerId: PeerId, peer: Signal, photo: Signal?, video: Signal?, videoStartTimestamp: Double?, mapResourceToAvatarSizes: @escaping (MediaResource, [TelegramMediaImageRepresentation]) -> Signal<[Int: Data], NoError>) -> Signal { +func _internal_updatePeerPhotoInternal(postbox: Postbox, network: Network, stateManager: AccountStateManager?, accountPeerId: PeerId, peer: Signal, photo: Signal?, video: Signal?, videoStartTimestamp: Double?, mapResourceToAvatarSizes: @escaping (MediaResource, [TelegramMediaImageRepresentation]) -> Signal<[Int: Data], NoError>) -> Signal { return peer |> mapError { _ in return .generic } |> mapToSignal { peer -> Signal in @@ -158,16 +158,10 @@ public func updatePeerPhotoInternal(postbox: Postbox, network: Network, stateMan } for size in sizes { switch size { - case let .photoSize(_, location, w, h, _): - switch location { - case let .fileLocationToBeDeprecated(volumeId, localId): - representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: CloudPeerPhotoSizeMediaResource(datacenterId: dcId, sizeSpec: w <= 200 ? .small : .fullSize, volumeId: volumeId, localId: localId), progressiveSizes: [])) - } - case let .photoSizeProgressive(_, location, w, h, sizes): - switch location { - case let .fileLocationToBeDeprecated(volumeId, localId): - representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: CloudPeerPhotoSizeMediaResource(datacenterId: dcId, sizeSpec: w <= 200 ? .small : .fullSize, volumeId: volumeId, localId: localId), progressiveSizes: sizes)) - } + case let .photoSize(_, w, h, _): + representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: CloudPeerPhotoSizeMediaResource(datacenterId: dcId, photoId: id, sizeSpec: w <= 200 ? .small : .fullSize, volumeId: nil, localId: nil), progressiveSizes: [], immediateThumbnailData: nil)) + case let .photoSizeProgressive(_, w, h, sizes): + representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: CloudPeerPhotoSizeMediaResource(datacenterId: dcId, photoId: id, sizeSpec: w <= 200 ? .small : .fullSize, volumeId: nil, localId: nil), progressiveSizes: sizes, immediateThumbnailData: nil)) default: break } @@ -176,12 +170,9 @@ public func updatePeerPhotoInternal(postbox: Postbox, network: Network, stateMan if let videoSizes = videoSizes { for size in videoSizes { switch size { - case let .videoSize(_, type, location, w, h, size, videoStartTs): + case let .videoSize(_, type, w, h, size, videoStartTs): let resource: TelegramMediaResource - switch location { - case let .fileLocationToBeDeprecated(volumeId, localId): - resource = CloudPhotoSizeMediaResource(datacenterId: dcId, photoId: id, accessHash: accessHash, sizeSpec: type, volumeId: volumeId, localId: localId, size: Int(size), fileReference: fileReference.makeData()) - } + resource = CloudPhotoSizeMediaResource(datacenterId: dcId, photoId: id, accessHash: accessHash, sizeSpec: type, size: Int(size), fileReference: fileReference.makeData()) videoRepresentations.append(TelegramMediaImage.VideoRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: resource, startTimestamp: videoStartTs)) } @@ -223,7 +214,7 @@ public func updatePeerPhotoInternal(postbox: Postbox, network: Network, stateMan let request: Signal if let peer = peer as? TelegramGroup { - request = network.request(Api.functions.messages.editChatPhoto(chatId: peer.id.id, photo: .inputChatUploadedPhoto(flags: flags, file: file, video: videoFile, videoStartTs: videoStartTimestamp))) + request = network.request(Api.functions.messages.editChatPhoto(chatId: peer.id.id._internalGetInt32Value(), photo: .inputChatUploadedPhoto(flags: flags, file: file, video: videoFile, videoStartTs: videoStartTimestamp))) } else if let peer = peer as? TelegramChannel, let inputChannel = apiInputChannel(peer) { request = network.request(Api.functions.channels.editPhoto(channel: inputChannel, photo: .inputChatUploadedPhoto(flags: flags, file: file, video: videoFile, videoStartTs: videoStartTimestamp))) } else { @@ -269,7 +260,7 @@ public func updatePeerPhotoInternal(postbox: Postbox, network: Network, stateMan } |> mapToSignal { result, resource, videoResource -> Signal in if case .complete = result { - return fetchAndUpdateCachedPeerData(accountPeerId: accountPeerId, peerId: peer.id, network: network, postbox: postbox) + return _internal_fetchAndUpdateCachedPeerData(accountPeerId: accountPeerId, peerId: peer.id, network: network, postbox: postbox) |> castError(UploadPeerPhotoError.self) |> mapToSignal { status -> Signal in return postbox.transaction { transaction in @@ -311,7 +302,7 @@ public func updatePeerPhotoInternal(postbox: Postbox, network: Network, stateMan } else { let request: Signal if let peer = peer as? TelegramGroup { - request = network.request(Api.functions.messages.editChatPhoto(chatId: peer.id.id, photo: .inputChatPhotoEmpty)) + request = network.request(Api.functions.messages.editChatPhoto(chatId: peer.id.id._internalGetInt32Value(), photo: .inputChatPhotoEmpty)) } else if let peer = peer as? TelegramChannel, let inputChannel = apiInputChannel(peer) { request = network.request(Api.functions.channels.editPhoto(channel: inputChannel, photo: .inputChatPhotoEmpty)) } else { @@ -344,7 +335,7 @@ public func updatePeerPhotoInternal(postbox: Postbox, network: Network, stateMan } } -public func updatePeerPhotoExisting(network: Network, reference: TelegramMediaImageReference) -> Signal { +func _internal_updatePeerPhotoExisting(network: Network, reference: TelegramMediaImageReference) -> Signal { switch reference { case let .cloud(imageId, accessHash, fileReference): return network.request(Api.functions.photos.updateProfilePhoto(id: .inputPhoto(id: imageId, accessHash: accessHash, fileReference: Buffer(data: fileReference)))) @@ -361,7 +352,7 @@ public func updatePeerPhotoExisting(network: Network, reference: TelegramMediaIm } } -public func removeAccountPhoto(network: Network, reference: TelegramMediaImageReference?) -> Signal { +func _internal_removeAccountPhoto(network: Network, reference: TelegramMediaImageReference?) -> Signal { if let reference = reference { switch reference { case let .cloud(imageId, accessHash, fileReference): diff --git a/submodules/TelegramCore/Sources/PeerSpecificStickerPack.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/PeerSpecificStickerPack.swift similarity index 86% rename from submodules/TelegramCore/Sources/PeerSpecificStickerPack.swift rename to submodules/TelegramCore/Sources/TelegramEngine/Peers/PeerSpecificStickerPack.swift index 0a9cea6eb7..c50de3ef03 100644 --- a/submodules/TelegramCore/Sources/PeerSpecificStickerPack.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/PeerSpecificStickerPack.swift @@ -17,7 +17,7 @@ public struct PeerSpecificStickerPackData { public let canSetup: Bool } -public func peerSpecificStickerPack(postbox: Postbox, network: Network, peerId: PeerId) -> Signal { +func _internal_peerSpecificStickerPack(postbox: Postbox, network: Network, peerId: PeerId) -> Signal { if peerId.namespace == Namespaces.Peer.CloudChannel { let signal: Signal<(WrappedStickerPackCollectionInfo, Bool), NoError> = postbox.combinedView(keys: [.cachedPeerData(peerId: peerId)]) |> map { view -> (WrappedStickerPackCollectionInfo, Bool) in @@ -31,7 +31,7 @@ public func peerSpecificStickerPack(postbox: Postbox, network: Network, peerId: return signal |> mapToSignal { info, canInstall -> Signal in if let info = info.info { - return cachedStickerPack(postbox: postbox, network: network, reference: .id(id: info.id.id, accessHash: info.accessHash), forceRemote: false) + return _internal_cachedStickerPack(postbox: postbox, network: network, reference: .id(id: info.id.id, accessHash: info.accessHash), forceRemote: false) |> map { result -> PeerSpecificStickerPackData in if case let .result(info, items, _) = result { return PeerSpecificStickerPackData(packInfo: (info, items), canSetup: canInstall) diff --git a/submodules/TelegramCore/Sources/RecentPeers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/RecentPeers.swift similarity index 92% rename from submodules/TelegramCore/Sources/RecentPeers.swift rename to submodules/TelegramCore/Sources/TelegramEngine/Peers/RecentPeers.swift index 88108ac206..11926c1364 100644 --- a/submodules/TelegramCore/Sources/RecentPeers.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/RecentPeers.swift @@ -16,7 +16,7 @@ private func cachedRecentPeersEntryId() -> ItemCacheEntryId { return ItemCacheEntryId(collectionId: 101, key: CachedRecentPeers.cacheKey()) } -public func recentPeers(account: Account) -> Signal { +func _internal_recentPeers(account: Account) -> Signal { let key = PostboxViewKey.cachedItem(cachedRecentPeersEntryId()) return account.postbox.combinedView(keys: [key]) |> mapToSignal { views -> Signal in @@ -41,14 +41,14 @@ public func recentPeers(account: Account) -> Signal { } } -public func getRecentPeers(transaction: Transaction) -> [PeerId] { +public func _internal_getRecentPeers(transaction: Transaction) -> [PeerId] { guard let entry = transaction.retrieveItemCacheEntry(id: cachedRecentPeersEntryId()) as? CachedRecentPeers else { return [] } return entry.ids } -public func managedUpdatedRecentPeers(accountPeerId: PeerId, postbox: Postbox, network: Network) -> Signal { +func _internal_managedUpdatedRecentPeers(accountPeerId: PeerId, postbox: Postbox, network: Network) -> Signal { let key = PostboxViewKey.cachedItem(cachedRecentPeersEntryId()) let peersEnabled = postbox.combinedView(keys: [key]) |> map { views -> Bool in @@ -96,7 +96,7 @@ public func managedUpdatedRecentPeers(accountPeerId: PeerId, postbox: Postbox, n } } -public func removeRecentPeer(account: Account, peerId: PeerId) -> Signal { +func _internal_removeRecentPeer(account: Account, peerId: PeerId) -> Signal { return account.postbox.transaction { transaction -> Signal in guard let entry = transaction.retrieveItemCacheEntry(id: cachedRecentPeersEntryId()) as? CachedRecentPeers else { return .complete() @@ -121,7 +121,7 @@ public func removeRecentPeer(account: Account, peerId: PeerId) -> Signal switchToLatest } -public func updateRecentPeersEnabled(postbox: Postbox, network: Network, enabled: Bool) -> Signal { +func _internal_updateRecentPeersEnabled(postbox: Postbox, network: Network, enabled: Bool) -> Signal { return postbox.transaction { transaction -> Signal in var currentValue = true if let entry = transaction.retrieveItemCacheEntry(id: cachedRecentPeersEntryId()) as? CachedRecentPeers { @@ -149,7 +149,7 @@ public func updateRecentPeersEnabled(postbox: Postbox, network: Network, enabled } |> switchToLatest } -public func managedRecentlyUsedInlineBots(postbox: Postbox, network: Network, accountPeerId: PeerId) -> Signal { +func _internal_managedRecentlyUsedInlineBots(postbox: Postbox, network: Network, accountPeerId: PeerId) -> Signal { let remotePeers = network.request(Api.functions.contacts.getTopPeers(flags: 1 << 2, offset: 0, limit: 16, hash: 0)) |> retryRequest |> map { result -> ([Peer], [PeerId: PeerPresence], [(PeerId, Double)])? in @@ -207,7 +207,7 @@ public func managedRecentlyUsedInlineBots(postbox: Postbox, network: Network, ac return updatedRemotePeers } -public func addRecentlyUsedInlineBot(postbox: Postbox, peerId: PeerId) -> Signal { +func _internal_addRecentlyUsedInlineBot(postbox: Postbox, peerId: PeerId) -> Signal { return postbox.transaction { transaction -> Void in var maxRating = 1.0 for entry in transaction.getOrderedListItems(collectionId: Namespaces.OrderedItemList.CloudRecentInlineBots) { @@ -219,7 +219,7 @@ public func addRecentlyUsedInlineBot(postbox: Postbox, peerId: PeerId) -> Signal } } -public func recentlyUsedInlineBots(postbox: Postbox) -> Signal<[(Peer, Double)], NoError> { +func _internal_recentlyUsedInlineBots(postbox: Postbox) -> Signal<[(Peer, Double)], NoError> { return postbox.combinedView(keys: [.orderedItemList(id: Namespaces.OrderedItemList.CloudRecentInlineBots)]) |> take(1) |> mapToSignal { view -> Signal<[(Peer, Double)], NoError> in @@ -238,7 +238,7 @@ public func recentlyUsedInlineBots(postbox: Postbox) -> Signal<[(Peer, Double)], } } -public func removeRecentlyUsedInlineBot(account: Account, peerId: PeerId) -> Signal { +func _internal_removeRecentlyUsedInlineBot(account: Account, peerId: PeerId) -> Signal { return account.postbox.transaction { transaction -> Signal in transaction.removeOrderedItemListItem(collectionId: Namespaces.OrderedItemList.CloudRecentInlineBots, itemId: RecentPeerItemId(peerId).rawValue) diff --git a/submodules/TelegramCore/Sources/RecentlySearchedPeerIds.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/RecentlySearchedPeerIds.swift similarity index 92% rename from submodules/TelegramCore/Sources/RecentlySearchedPeerIds.swift rename to submodules/TelegramCore/Sources/TelegramEngine/Peers/RecentlySearchedPeerIds.swift index ad9544f4dc..f54fc35884 100644 --- a/submodules/TelegramCore/Sources/RecentlySearchedPeerIds.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/RecentlySearchedPeerIds.swift @@ -4,19 +4,19 @@ import SwiftSignalKit import SyncCore -public func addRecentlySearchedPeer(postbox: Postbox, peerId: PeerId) -> Signal { +func _internal_addRecentlySearchedPeer(postbox: Postbox, peerId: PeerId) -> Signal { return postbox.transaction { transaction -> Void in transaction.addOrMoveToFirstPositionOrderedItemListItem(collectionId: Namespaces.OrderedItemList.RecentlySearchedPeerIds, item: OrderedItemListEntry(id: RecentPeerItemId(peerId).rawValue, contents: RecentPeerItem(rating: 0.0)), removeTailIfCountExceeds: 20) } } -public func removeRecentlySearchedPeer(postbox: Postbox, peerId: PeerId) -> Signal { +func _internal_removeRecentlySearchedPeer(postbox: Postbox, peerId: PeerId) -> Signal { return postbox.transaction { transaction -> Void in transaction.removeOrderedItemListItem(collectionId: Namespaces.OrderedItemList.RecentlySearchedPeerIds, itemId: RecentPeerItemId(peerId).rawValue) } } -public func clearRecentlySearchedPeers(postbox: Postbox) -> Signal { +func _internal_clearRecentlySearchedPeers(postbox: Postbox) -> Signal { return postbox.transaction { transaction -> Void in transaction.replaceOrderedItemListItems(collectionId: Namespaces.OrderedItemList.RecentlySearchedPeerIds, items: []) } @@ -34,7 +34,7 @@ public struct RecentlySearchedPeer: Equatable { public let subpeerSummary: RecentlySearchedPeerSubpeerSummary? } -public func recentlySearchedPeers(postbox: Postbox) -> Signal<[RecentlySearchedPeer], NoError> { +func _internal_recentlySearchedPeers(postbox: Postbox) -> Signal<[RecentlySearchedPeer], NoError> { return postbox.combinedView(keys: [.orderedItemList(id: Namespaces.OrderedItemList.RecentlySearchedPeerIds)]) |> mapToSignal { view -> Signal<[RecentlySearchedPeer], NoError> in var peerIds: [PeerId] = [] diff --git a/submodules/TelegramCore/Sources/RemovePeerChat.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/RemovePeerChat.swift similarity index 78% rename from submodules/TelegramCore/Sources/RemovePeerChat.swift rename to submodules/TelegramCore/Sources/TelegramEngine/Peers/RemovePeerChat.swift index 0cae498d09..3858dcf79e 100644 --- a/submodules/TelegramCore/Sources/RemovePeerChat.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/RemovePeerChat.swift @@ -4,13 +4,13 @@ import SwiftSignalKit import SyncCore -public func removePeerChat(account: Account, peerId: PeerId, reportChatSpam: Bool, deleteGloballyIfPossible: Bool = false) -> Signal { +func _internal_removePeerChat(account: Account, peerId: PeerId, reportChatSpam: Bool, deleteGloballyIfPossible: Bool = false) -> Signal { return account.postbox.transaction { transaction -> Void in - removePeerChat(account: account, transaction: transaction, mediaBox: account.postbox.mediaBox, peerId: peerId, reportChatSpam: reportChatSpam, deleteGloballyIfPossible: deleteGloballyIfPossible) + _internal_removePeerChat(account: account, transaction: transaction, mediaBox: account.postbox.mediaBox, peerId: peerId, reportChatSpam: reportChatSpam, deleteGloballyIfPossible: deleteGloballyIfPossible) } } -public func terminateSecretChat(transaction: Transaction, peerId: PeerId, requestRemoteHistoryRemoval: Bool) { +func _internal_terminateSecretChat(transaction: Transaction, peerId: PeerId, requestRemoteHistoryRemoval: Bool) { if let state = transaction.getPeerChatState(peerId) as? SecretChatState, state.embeddedState != .terminated { let updatedState = addSecretChatOutgoingOperation(transaction: transaction, peerId: peerId, operation: SecretChatOutgoingOperationContents.terminate(reportSpam: false, requestRemoteHistoryRemoval: requestRemoteHistoryRemoval), state: state).withUpdatedEmbeddedState(.terminated) if updatedState != state { @@ -24,7 +24,7 @@ public func terminateSecretChat(transaction: Transaction, peerId: PeerId, reques } } -public func removePeerChat(account: Account, transaction: Transaction, mediaBox: MediaBox, peerId: PeerId, reportChatSpam: Bool, deleteGloballyIfPossible: Bool) { +func _internal_removePeerChat(account: Account, transaction: Transaction, mediaBox: MediaBox, peerId: PeerId, reportChatSpam: Bool, deleteGloballyIfPossible: Bool) { if let _ = transaction.getPeerChatInterfaceState(peerId) { transaction.updatePeerChatInterfaceState(peerId, update: { current in if let current = current { @@ -34,7 +34,7 @@ public func removePeerChat(account: Account, transaction: Transaction, mediaBox: } }) } - updateChatListFiltersInteractively(transaction: transaction, { filters in + _internal_updateChatListFiltersInteractively(transaction: transaction, { filters in var filters = filters for i in 0 ..< filters.count { if filters[i].data.includePeers.peers.contains(peerId) { @@ -58,17 +58,17 @@ public func removePeerChat(account: Account, transaction: Transaction, mediaBox: } } } - clearHistory(transaction: transaction, mediaBox: mediaBox, peerId: peerId, namespaces: .all) + _internal_clearHistory(transaction: transaction, mediaBox: mediaBox, peerId: peerId, namespaces: .all) transaction.updatePeerChatListInclusion(peerId, inclusion: .notIncluded) transaction.removeOrderedItemListItem(collectionId: Namespaces.OrderedItemList.RecentlySearchedPeerIds, itemId: RecentPeerItemId(peerId).rawValue) } else { cloudChatAddRemoveChatOperation(transaction: transaction, peerId: peerId, reportChatSpam: reportChatSpam, deleteGloballyIfPossible: deleteGloballyIfPossible) if peerId.namespace == Namespaces.Peer.CloudUser { transaction.updatePeerChatListInclusion(peerId, inclusion: .notIncluded) - clearHistory(transaction: transaction, mediaBox: mediaBox, peerId: peerId, namespaces: .all) + _internal_clearHistory(transaction: transaction, mediaBox: mediaBox, peerId: peerId, namespaces: .all) } else if peerId.namespace == Namespaces.Peer.CloudGroup { transaction.updatePeerChatListInclusion(peerId, inclusion: .notIncluded) - clearHistory(transaction: transaction, mediaBox: mediaBox, peerId: peerId, namespaces: .all) + _internal_clearHistory(transaction: transaction, mediaBox: mediaBox, peerId: peerId, namespaces: .all) } else { transaction.updatePeerChatListInclusion(peerId, inclusion: .notIncluded) } diff --git a/submodules/TelegramCore/Sources/RemovePeerMember.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/RemovePeerMember.swift similarity index 85% rename from submodules/TelegramCore/Sources/RemovePeerMember.swift rename to submodules/TelegramCore/Sources/TelegramEngine/Peers/RemovePeerMember.swift index f07ba2ccaf..c8cf17ba4b 100644 --- a/submodules/TelegramCore/Sources/RemovePeerMember.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/RemovePeerMember.swift @@ -6,9 +6,9 @@ import MtProtoKit import SyncCore -public func removePeerMember(account: Account, peerId: PeerId, memberId: PeerId) -> Signal { +func _internal_removePeerMember(account: Account, peerId: PeerId, memberId: PeerId) -> Signal { if peerId.namespace == Namespaces.Peer.CloudChannel { - return updateChannelMemberBannedRights(account: account, peerId: peerId, memberId: memberId, rights: TelegramChatBannedRights(flags: [.banReadMessages], untilDate: 0)) + return _internal_updateChannelMemberBannedRights(account: account, peerId: peerId, memberId: memberId, rights: TelegramChatBannedRights(flags: [.banReadMessages], untilDate: 0)) |> mapToSignal { _ -> Signal in return .complete() } @@ -17,7 +17,7 @@ public func removePeerMember(account: Account, peerId: PeerId, memberId: PeerId) return account.postbox.transaction { transaction -> Signal in if let peer = transaction.getPeer(peerId), let memberPeer = transaction.getPeer(memberId), let inputUser = apiInputUser(memberPeer) { if let group = peer as? TelegramGroup { - return account.network.request(Api.functions.messages.deleteChatUser(flags: 0, chatId: group.id.id, userId: inputUser)) + return account.network.request(Api.functions.messages.deleteChatUser(flags: 0, chatId: group.id.id._internalGetInt32Value(), userId: inputUser)) |> mapError { error -> Void in return Void() } diff --git a/submodules/TelegramCore/Sources/ReportPeer.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ReportPeer.swift similarity index 92% rename from submodules/TelegramCore/Sources/ReportPeer.swift rename to submodules/TelegramCore/Sources/TelegramEngine/Peers/ReportPeer.swift index 2438920e0d..f37a6edf10 100644 --- a/submodules/TelegramCore/Sources/ReportPeer.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ReportPeer.swift @@ -6,11 +6,11 @@ import MtProtoKit import SyncCore -public func reportPeer(account: Account, peerId: PeerId) -> Signal { +func _internal_reportPeer(account: Account, peerId: PeerId) -> Signal { return account.postbox.transaction { transaction -> Signal in if let peer = transaction.getPeer(peerId) { if let peer = peer as? TelegramSecretChat { - return account.network.request(Api.functions.messages.reportEncryptedSpam(peer: Api.InputEncryptedChat.inputEncryptedChat(chatId: peer.id.id, accessHash: peer.accessHash))) + return account.network.request(Api.functions.messages.reportEncryptedSpam(peer: Api.InputEncryptedChat.inputEncryptedChat(chatId: peer.id.id._internalGetInt32Value(), accessHash: peer.accessHash))) |> map(Optional.init) |> `catch` { _ -> Signal in return .single(nil) @@ -110,7 +110,7 @@ private extension ReportReason { } } -public func reportPeer(account: Account, peerId: PeerId, reason: ReportReason, message: String) -> Signal { +func _internal_reportPeer(account: Account, peerId: PeerId, reason: ReportReason, message: String) -> Signal { return account.postbox.transaction { transaction -> Signal in if let peer = transaction.getPeer(peerId), let inputPeer = apiInputPeer(peer) { return account.network.request(Api.functions.account.reportPeer(peer: inputPeer, reason: reason.apiReason, message: message)) @@ -126,7 +126,7 @@ public func reportPeer(account: Account, peerId: PeerId, reason: ReportReason, m } |> switchToLatest } -public func reportPeerPhoto(account: Account, peerId: PeerId, reason: ReportReason, message: String) -> Signal { +func _internal_reportPeerPhoto(account: Account, peerId: PeerId, reason: ReportReason, message: String) -> Signal { return account.postbox.transaction { transaction -> Signal in if let peer = transaction.getPeer(peerId), let inputPeer = apiInputPeer(peer) { return account.network.request(Api.functions.account.reportProfilePhoto(peer: inputPeer, photoId: .inputPhotoEmpty, reason: reason.apiReason, message: message)) @@ -142,7 +142,7 @@ public func reportPeerPhoto(account: Account, peerId: PeerId, reason: ReportReas } |> switchToLatest } -public func reportPeerMessages(account: Account, messageIds: [MessageId], reason: ReportReason, message: String) -> Signal { +func _internal_reportPeerMessages(account: Account, messageIds: [MessageId], reason: ReportReason, message: String) -> Signal { return account.postbox.transaction { transaction -> Signal in let groupedIds = messagesIdsGroupedByPeerId(messageIds) let signals = groupedIds.values.compactMap { ids -> Signal? in @@ -165,7 +165,7 @@ public func reportPeerMessages(account: Account, messageIds: [MessageId], reason } |> switchToLatest } -public func dismissPeerStatusOptions(account: Account, peerId: PeerId) -> Signal { +func _internal_dismissPeerStatusOptions(account: Account, peerId: PeerId) -> Signal { return account.postbox.transaction { transaction -> Signal in transaction.updatePeerCachedData(peerIds: Set([peerId]), update: { _, current in if let current = current as? CachedUserData { @@ -203,7 +203,7 @@ public func dismissPeerStatusOptions(account: Account, peerId: PeerId) -> Signal } |> switchToLatest } -public func reportRepliesMessage(account: Account, messageId: MessageId, deleteMessage: Bool, deleteHistory: Bool, reportSpam: Bool) -> Signal { +func _internal_reportRepliesMessage(account: Account, messageId: MessageId, deleteMessage: Bool, deleteHistory: Bool, reportSpam: Bool) -> Signal { if messageId.namespace != Namespaces.Message.Cloud { return .complete() } diff --git a/submodules/TelegramCore/Sources/RequestUserPhotos.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/RequestUserPhotos.swift similarity index 98% rename from submodules/TelegramCore/Sources/RequestUserPhotos.swift rename to submodules/TelegramCore/Sources/TelegramEngine/Peers/RequestUserPhotos.swift index 9124bde8b2..a108b0b35c 100644 --- a/submodules/TelegramCore/Sources/RequestUserPhotos.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/RequestUserPhotos.swift @@ -24,7 +24,7 @@ public struct TelegramPeerPhoto { } } -public func requestPeerPhotos(postbox: Postbox, network: Network, peerId: PeerId) -> Signal<[TelegramPeerPhoto], NoError> { +func _internal_requestPeerPhotos(postbox: Postbox, network: Network, peerId: PeerId) -> Signal<[TelegramPeerPhoto], NoError> { return postbox.transaction{ transaction -> Peer? in return transaction.getPeer(peerId) } diff --git a/submodules/TelegramCore/Sources/ResolvePeerByName.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ResolvePeerByName.swift similarity index 96% rename from submodules/TelegramCore/Sources/ResolvePeerByName.swift rename to submodules/TelegramCore/Sources/TelegramEngine/Peers/ResolvePeerByName.swift index 2e4f7ec625..1b0c01f0b4 100644 --- a/submodules/TelegramCore/Sources/ResolvePeerByName.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ResolvePeerByName.swift @@ -18,7 +18,7 @@ public enum ResolvePeerByNameOptionRemote { case update } -public func resolvePeerByName(account: Account, name: String, ageLimit: Int32 = 2 * 60 * 60 * 24) -> Signal { +func _internal_resolvePeerByName(account: Account, name: String, ageLimit: Int32 = 2 * 60 * 60 * 24) -> Signal { var normalizedName = name if normalizedName.hasPrefix("@") { normalizedName = String(normalizedName[name.index(after: name.startIndex)...]) diff --git a/submodules/TelegramCore/Sources/SearchGroupMembers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/SearchGroupMembers.swift similarity index 61% rename from submodules/TelegramCore/Sources/SearchGroupMembers.swift rename to submodules/TelegramCore/Sources/TelegramEngine/Peers/SearchGroupMembers.swift index 378e41837c..1a2f83e841 100644 --- a/submodules/TelegramCore/Sources/SearchGroupMembers.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/SearchGroupMembers.swift @@ -4,6 +4,41 @@ import SwiftSignalKit import SyncCore +private struct PeerParticipants: Equatable { + let peers: [Peer] + + static func ==(lhs: PeerParticipants, rhs: PeerParticipants) -> Bool { + if lhs.peers.count != rhs.peers.count { + return false + } + for i in 0 ..< lhs.peers.count { + if !lhs.peers[i].isEqual(rhs.peers[i]) { + return false + } + } + return true + } +} + +private func peerParticipants(postbox: Postbox, id: PeerId) -> Signal<[Peer], NoError> { + return postbox.peerView(id: id) |> map { view -> PeerParticipants in + if let cachedGroupData = view.cachedData as? CachedGroupData, let participants = cachedGroupData.participants { + var peers: [Peer] = [] + for participant in participants.participants { + if let peer = view.peers[participant.peerId] { + peers.append(peer) + } + } + return PeerParticipants(peers: peers) + } else { + return PeerParticipants(peers: []) + } + } + |> distinctUntilChanged |> map { participants in + return participants.peers + } +} + private func searchLocalGroupMembers(postbox: Postbox, peerId: PeerId, query: String) -> Signal<[Peer], NoError> { return peerParticipants(postbox: postbox, id: peerId) |> map { peers -> [Peer] in @@ -28,7 +63,7 @@ private func searchLocalGroupMembers(postbox: Postbox, peerId: PeerId, query: St } } -public func searchGroupMembers(postbox: Postbox, network: Network, accountPeerId: PeerId, peerId: PeerId, query: String) -> Signal<[Peer], NoError> { +func _internal_searchGroupMembers(postbox: Postbox, network: Network, accountPeerId: PeerId, peerId: PeerId, query: String) -> Signal<[Peer], NoError> { if peerId.namespace == Namespaces.Peer.CloudChannel && !query.isEmpty { return searchLocalGroupMembers(postbox: postbox, peerId: peerId, query: query) |> mapToSignal { local -> Signal<[Peer], NoError> in @@ -40,7 +75,7 @@ public func searchGroupMembers(postbox: Postbox, network: Network, accountPeerId } return localResult |> then( - channelMembers(postbox: postbox, network: network, accountPeerId: accountPeerId, peerId: peerId, category: .recent(.search(query))) + _internal_channelMembers(postbox: postbox, network: network, accountPeerId: accountPeerId, peerId: peerId, category: .recent(.search(query))) |> map { participants -> [Peer] in var result: [Peer] = local let existingIds = Set(local.map { $0.id }) diff --git a/submodules/TelegramCore/Sources/SearchPeers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/SearchPeers.swift similarity index 73% rename from submodules/TelegramCore/Sources/SearchPeers.swift rename to submodules/TelegramCore/Sources/TelegramEngine/Peers/SearchPeers.swift index ab6b93a3e7..0cd869c7d2 100644 --- a/submodules/TelegramCore/Sources/SearchPeers.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/SearchPeers.swift @@ -20,7 +20,7 @@ public struct FoundPeer: Equatable { } } -public func searchPeers(account: Account, query: String) -> Signal<([FoundPeer], [FoundPeer]), NoError> { +func _internal_searchPeers(account: Account, query: String) -> Signal<([FoundPeer], [FoundPeer]), NoError> { let searchResult = account.network.request(Api.functions.contacts.search(q: query, limit: 20), automaticFloodWait: false) |> map(Optional.init) |> `catch` { _ in @@ -61,15 +61,7 @@ public func searchPeers(account: Account, query: String) -> Signal<([FoundPeer], var renderedMyPeers: [FoundPeer] = [] for result in myResults { - let peerId: PeerId - switch result { - case let .peerUser(userId): - peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: userId) - case let .peerChat(chatId): - peerId = PeerId(namespace: Namespaces.Peer.CloudGroup, id: chatId) - case let .peerChannel(channelId): - peerId = PeerId(namespace: Namespaces.Peer.CloudChannel, id: channelId) - } + let peerId: PeerId = result.peerId if let peer = peers[peerId] { if let group = peer as? TelegramGroup, group.migrationReference != nil { continue @@ -80,15 +72,7 @@ public func searchPeers(account: Account, query: String) -> Signal<([FoundPeer], var renderedPeers: [FoundPeer] = [] for result in results { - let peerId: PeerId - switch result { - case let .peerUser(userId): - peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: userId) - case let .peerChat(chatId): - peerId = PeerId(namespace: Namespaces.Peer.CloudGroup, id: chatId) - case let .peerChannel(channelId): - peerId = PeerId(namespace: Namespaces.Peer.CloudChannel, id: channelId) - } + let peerId: PeerId = result.peerId if let peer = peers[peerId] { if let group = peer as? TelegramGroup, group.migrationReference != nil { continue diff --git a/submodules/TelegramCore/Sources/SlowMode.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/SlowMode.swift similarity index 86% rename from submodules/TelegramCore/Sources/SlowMode.swift rename to submodules/TelegramCore/Sources/TelegramEngine/Peers/SlowMode.swift index 7134b47d08..a2f3ee94f0 100644 --- a/submodules/TelegramCore/Sources/SlowMode.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/SlowMode.swift @@ -9,7 +9,7 @@ public enum UpdateChannelSlowModeError { case tooManyChannels } -public func updateChannelSlowModeInteractively(postbox: Postbox, network: Network, accountStateManager: AccountStateManager, peerId: PeerId, timeout: Int32?) -> Signal { +func _internal_updateChannelSlowModeInteractively(postbox: Postbox, network: Network, accountStateManager: AccountStateManager, peerId: PeerId, timeout: Int32?) -> Signal { return postbox.transaction { transaction -> Peer? in return transaction.getPeer(peerId) } diff --git a/submodules/TelegramCore/Sources/SupportPeerId.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/SupportPeerId.swift similarity index 92% rename from submodules/TelegramCore/Sources/SupportPeerId.swift rename to submodules/TelegramCore/Sources/TelegramEngine/Peers/SupportPeerId.swift index 783c7c8552..afa575bc0e 100644 --- a/submodules/TelegramCore/Sources/SupportPeerId.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/SupportPeerId.swift @@ -5,7 +5,7 @@ import MtProtoKit import SyncCore -public func supportPeerId(account:Account) -> Signal { +func _internal_supportPeerId(account: Account) -> Signal { return account.network.request(Api.functions.help.getSupport()) |> map(Optional.init) |> `catch` { _ in diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift new file mode 100644 index 0000000000..00f3ddddb6 --- /dev/null +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift @@ -0,0 +1,463 @@ +import Foundation +import SwiftSignalKit +import Postbox +import SyncCore + +public enum AddressNameValidationStatus: Equatable { + case checking + case invalidFormat(AddressNameFormatError) + case availability(AddressNameAvailability) +} + +public extension TelegramEngine { + final class Peers { + private let account: Account + + init(account: Account) { + self.account = account + } + + public func addressNameAvailability(domain: AddressNameDomain, name: String) -> Signal { + return _internal_addressNameAvailability(account: self.account, domain: domain, name: name) + } + + public func updateAddressName(domain: AddressNameDomain, name: String?) -> Signal { + return _internal_updateAddressName(account: self.account, domain: domain, name: name) + } + + public func checkPublicChannelCreationAvailability(location: Bool = false) -> Signal { + return _internal_checkPublicChannelCreationAvailability(account: self.account, location: location) + } + + public func adminedPublicChannels(scope: AdminedPublicChannelsScope = .all) -> Signal<[Peer], NoError> { + return _internal_adminedPublicChannels(account: self.account, scope: scope) + } + + public func channelAddressNameAssignmentAvailability(peerId: PeerId?) -> Signal { + return _internal_channelAddressNameAssignmentAvailability(account: self.account, peerId: peerId) + } + + public func validateAddressNameInteractive(domain: AddressNameDomain, name: String) -> Signal { + if let error = _internal_checkAddressNameFormat(name) { + return .single(.invalidFormat(error)) + } else { + return .single(.checking) + |> then( + self.addressNameAvailability(domain: domain, name: name) + |> delay(0.3, queue: Queue.concurrentDefaultQueue()) + |> map { result -> AddressNameValidationStatus in + .availability(result) + } + ) + } + } + + public func findChannelById(channelId: Int32) -> Signal { + return _internal_findChannelById(postbox: self.account.postbox, network: self.account.network, channelId: channelId) + } + + public func supportPeerId() -> Signal { + return _internal_supportPeerId(account: self.account) + } + + public func inactiveChannelList() -> Signal<[InactiveChannel], NoError> { + return _internal_inactiveChannelList(network: self.account.network) + } + + public func resolvePeerByName(name: String, ageLimit: Int32 = 2 * 60 * 60 * 24) -> Signal { + return _internal_resolvePeerByName(account: self.account, name: name, ageLimit: ageLimit) + } + + public func searchPeers(query: String) -> Signal<([FoundPeer], [FoundPeer]), NoError> { + return _internal_searchPeers(account: self.account, query: query) + } + + public func updatedRemotePeer(peer: PeerReference) -> Signal { + return _internal_updatedRemotePeer(postbox: self.account.postbox, network: self.account.network, peer: peer) + } + + public func chatOnlineMembers(peerId: PeerId) -> Signal { + return _internal_chatOnlineMembers(postbox: self.account.postbox, network: self.account.network, peerId: peerId) + } + + public func convertGroupToSupergroup(peerId: PeerId) -> Signal { + return _internal_convertGroupToSupergroup(account: self.account, peerId: peerId) + } + + public func createGroup(title: String, peerIds: [PeerId]) -> Signal { + return _internal_createGroup(account: self.account, title: title, peerIds: peerIds) + } + + public func createSecretChat(peerId: PeerId) -> Signal { + return _internal_createSecretChat(account: self.account, peerId: peerId) + } + + public func setChatMessageAutoremoveTimeoutInteractively(peerId: PeerId, timeout: Int32?) -> Signal { + if peerId.namespace == Namespaces.Peer.SecretChat { + return _internal_setSecretChatMessageAutoremoveTimeoutInteractively(account: self.account, peerId: peerId, timeout: timeout) + |> ignoreValues + |> castError(SetChatMessageAutoremoveTimeoutError.self) + } else { + return _internal_setChatMessageAutoremoveTimeoutInteractively(account: self.account, peerId: peerId, timeout: timeout) + } + } + + public func updateChannelSlowModeInteractively(peerId: PeerId, timeout: Int32?) -> Signal { + return _internal_updateChannelSlowModeInteractively(postbox: self.account.postbox, network: self.account.network, accountStateManager: self.account.stateManager, peerId: peerId, timeout: timeout) + } + + public func reportPeer(peerId: PeerId) -> Signal { + return _internal_reportPeer(account: self.account, peerId: peerId) + } + + public func reportPeer(peerId: PeerId, reason: ReportReason, message: String) -> Signal { + return _internal_reportPeer(account: self.account, peerId: peerId, reason: reason, message: message) + } + + public func reportPeerPhoto(peerId: PeerId, reason: ReportReason, message: String) -> Signal { + return _internal_reportPeerPhoto(account: self.account, peerId: peerId, reason: reason, message: message) + } + + public func reportPeerMessages(messageIds: [MessageId], reason: ReportReason, message: String) -> Signal { + return _internal_reportPeerMessages(account: account, messageIds: messageIds, reason: reason, message: message) + } + + public func dismissPeerStatusOptions(peerId: PeerId) -> Signal { + return _internal_dismissPeerStatusOptions(account: self.account, peerId: peerId) + } + + public func reportRepliesMessage(messageId: MessageId, deleteMessage: Bool, deleteHistory: Bool, reportSpam: Bool) -> Signal { + return _internal_reportRepliesMessage(account: self.account, messageId: messageId, deleteMessage: deleteMessage, deleteHistory: deleteHistory, reportSpam: reportSpam) + } + + public func togglePeerMuted(peerId: PeerId) -> Signal { + return _internal_togglePeerMuted(account: self.account, peerId: peerId) + } + + public func updatePeerMuteSetting(peerId: PeerId, muteInterval: Int32?) -> Signal { + return _internal_updatePeerMuteSetting(account: self.account, peerId: peerId, muteInterval: muteInterval) + } + + public func updatePeerDisplayPreviewsSetting(peerId: PeerId, displayPreviews: PeerNotificationDisplayPreviews) -> Signal { + return _internal_updatePeerDisplayPreviewsSetting(account: self.account, peerId: peerId, displayPreviews: displayPreviews) + } + + public func updatePeerNotificationSoundInteractive(peerId: PeerId, sound: PeerMessageSound) -> Signal { + return _internal_updatePeerNotificationSoundInteractive(account: self.account, peerId: peerId, sound: sound) + } + + public func removeCustomNotificationSettings(peerIds: [PeerId]) -> Signal { + return self.account.postbox.transaction { transaction -> Void in + for peerId in peerIds { + TelegramCore.updatePeerNotificationSoundInteractive(transaction: transaction, peerId: peerId, sound: .default) + TelegramCore.updatePeerMuteSetting(transaction: transaction, peerId: peerId, muteInterval: nil) + TelegramCore.updatePeerDisplayPreviewsSetting(transaction: transaction, peerId: peerId, displayPreviews: .default) + } + } + |> ignoreValues + } + + public func channelAdminEventLog(peerId: PeerId) -> ChannelAdminEventLogContext { + return ChannelAdminEventLogContext(postbox: self.account.postbox, network: self.account.network, peerId: peerId) + } + + public func updateChannelMemberBannedRights(peerId: PeerId, memberId: PeerId, rights: TelegramChatBannedRights?) -> Signal<(ChannelParticipant?, RenderedChannelParticipant?, Bool), NoError> { + return _internal_updateChannelMemberBannedRights(account: self.account, peerId: peerId, memberId: memberId, rights: rights) + } + + public func updateDefaultChannelMemberBannedRights(peerId: PeerId, rights: TelegramChatBannedRights) -> Signal { + return _internal_updateDefaultChannelMemberBannedRights(account: self.account, peerId: peerId, rights: rights) + } + + public func createChannel(title: String, description: String?) -> Signal { + return _internal_createChannel(account: self.account, title: title, description: description) + } + + public func createSupergroup(title: String, description: String?, location: (latitude: Double, longitude: Double, address: String)? = nil, isForHistoryImport: Bool = false) -> Signal { + return _internal_createSupergroup(account: self.account, title: title, description: description, location: location, isForHistoryImport: isForHistoryImport) + } + + public func deleteChannel(peerId: PeerId) -> Signal { + return _internal_deleteChannel(account: self.account, peerId: peerId) + } + + public func updateChannelHistoryAvailabilitySettingsInteractively(peerId: PeerId, historyAvailableForNewMembers: Bool) -> Signal { + return _internal_updateChannelHistoryAvailabilitySettingsInteractively(postbox: self.account.postbox, network: self.account.network, accountStateManager: self.account.stateManager, peerId: peerId, historyAvailableForNewMembers: historyAvailableForNewMembers) + } + + public func channelMembers(peerId: PeerId, category: ChannelMembersCategory = .recent(.all), offset: Int32 = 0, limit: Int32 = 64, hash: Int32 = 0) -> Signal<[RenderedChannelParticipant]?, NoError> { + return _internal_channelMembers(postbox: self.account.postbox, network: self.account.network, accountPeerId: self.account.peerId, peerId: peerId, category: category, offset: offset, limit: limit, hash: hash) + } + + public func checkOwnershipTranfserAvailability(memberId: PeerId) -> Signal { + return _internal_checkOwnershipTranfserAvailability(postbox: self.account.postbox, network: self.account.network, accountStateManager: self.account.stateManager, memberId: memberId) + } + + public func updateChannelOwnership(channelId: PeerId, memberId: PeerId, password: String) -> Signal<[(ChannelParticipant?, RenderedChannelParticipant)], ChannelOwnershipTransferError> { + return _internal_updateChannelOwnership(account: self.account, accountStateManager: self.account.stateManager, channelId: channelId, memberId: memberId, password: password) + } + + public func searchGroupMembers(peerId: PeerId, query: String) -> Signal<[Peer], NoError> { + return _internal_searchGroupMembers(postbox: self.account.postbox, network: self.account.network, accountPeerId: self.account.peerId, peerId: peerId, query: query) + } + + public func toggleShouldChannelMessagesSignatures(peerId: PeerId, enabled: Bool) -> Signal { + return _internal_toggleShouldChannelMessagesSignatures(account: self.account, peerId: peerId, enabled: enabled) + } + + public func requestPeerPhotos(peerId: PeerId) -> Signal<[TelegramPeerPhoto], NoError> { + return _internal_requestPeerPhotos(postbox: self.account.postbox, network: self.account.network, peerId: peerId) + } + + public func updateGroupSpecificStickerset(peerId: PeerId, info: StickerPackCollectionInfo?) -> Signal { + return _internal_updateGroupSpecificStickerset(postbox: self.account.postbox, network: self.account.network, peerId: peerId, info: info) + } + + public func joinChannel(peerId: PeerId, hash: String?) -> Signal { + return _internal_joinChannel(account: self.account, peerId: peerId, hash: hash) + } + + public func removePeerMember(peerId: PeerId, memberId: PeerId) -> Signal { + return _internal_removePeerMember(account: self.account, peerId: peerId, memberId: memberId) + } + + public func availableGroupsForChannelDiscussion() -> Signal<[Peer], AvailableChannelDiscussionGroupError> { + return _internal_availableGroupsForChannelDiscussion(postbox: self.account.postbox, network: self.account.network) + } + + public func updateGroupDiscussionForChannel(channelId: PeerId?, groupId: PeerId?) -> Signal { + return _internal_updateGroupDiscussionForChannel(network: self.account.network, postbox: self.account.postbox, channelId: channelId, groupId: groupId) + } + + public func peerCommands(id: PeerId) -> Signal { + return _internal_peerCommands(account: self.account, id: id) + } + + public func addGroupAdmin(peerId: PeerId, adminId: PeerId) -> Signal { + return _internal_addGroupAdmin(account: self.account, peerId: peerId, adminId: adminId) + } + + public func removeGroupAdmin(peerId: PeerId, adminId: PeerId) -> Signal { + return _internal_removeGroupAdmin(account: self.account, peerId: peerId, adminId: adminId) + } + + public func fetchChannelParticipant(peerId: PeerId, participantId: PeerId) -> Signal { + return _internal_fetchChannelParticipant(account: self.account, peerId: peerId, participantId: participantId) + } + + public func updateChannelAdminRights(peerId: PeerId, adminId: PeerId, rights: TelegramChatAdminRights?, rank: String?) -> Signal<(ChannelParticipant?, RenderedChannelParticipant), UpdateChannelAdminRightsError> { + return _internal_updateChannelAdminRights(account: self.account, peerId: peerId, adminId: adminId, rights: rights, rank: rank) + } + + public func peerSpecificStickerPack(peerId: PeerId) -> Signal { + return _internal_peerSpecificStickerPack(postbox: self.account.postbox, network: self.account.network, peerId: peerId) + } + + public func addRecentlySearchedPeer(peerId: PeerId) -> Signal { + return _internal_addRecentlySearchedPeer(postbox: self.account.postbox, peerId: peerId) + } + + public func removeRecentlySearchedPeer(peerId: PeerId) -> Signal { + return _internal_removeRecentlySearchedPeer(postbox: self.account.postbox, peerId: peerId) + } + + public func clearRecentlySearchedPeers() -> Signal { + return _internal_clearRecentlySearchedPeers(postbox: self.account.postbox) + } + + public func recentlySearchedPeers() -> Signal<[RecentlySearchedPeer], NoError> { + return _internal_recentlySearchedPeers(postbox: self.account.postbox) + } + + public func removePeerChat(peerId: PeerId, reportChatSpam: Bool, deleteGloballyIfPossible: Bool = false) -> Signal { + return _internal_removePeerChat(account: self.account, peerId: peerId, reportChatSpam: reportChatSpam, deleteGloballyIfPossible: deleteGloballyIfPossible) + } + + public func removePeerChats(peerIds: [PeerId]) -> Signal { + return self.account.postbox.transaction { transaction -> Void in + for peerId in peerIds { + _internal_removePeerChat(account: self.account, transaction: transaction, mediaBox: self.account.postbox.mediaBox, peerId: peerId, reportChatSpam: false, deleteGloballyIfPossible: peerId.namespace == Namespaces.Peer.SecretChat) + } + } + |> ignoreValues + } + + public func terminateSecretChat(peerId: PeerId, requestRemoteHistoryRemoval: Bool) -> Signal { + return self.account.postbox.transaction { transaction -> Void in + _internal_terminateSecretChat(transaction: transaction, peerId: peerId, requestRemoteHistoryRemoval: requestRemoteHistoryRemoval) + } + |> ignoreValues + } + + public func addGroupMember(peerId: PeerId, memberId: PeerId) -> Signal { + return _internal_addGroupMember(account: self.account, peerId: peerId, memberId: memberId) + } + + public func addChannelMember(peerId: PeerId, memberId: PeerId) -> Signal<(ChannelParticipant?, RenderedChannelParticipant), AddChannelMemberError> { + return _internal_addChannelMember(account: self.account, peerId: peerId, memberId: memberId) + } + + public func addChannelMembers(peerId: PeerId, memberIds: [PeerId]) -> Signal { + return _internal_addChannelMembers(account: self.account, peerId: peerId, memberIds: memberIds) + } + + public func recentPeers() -> Signal { + return _internal_recentPeers(account: self.account) + } + + public func managedUpdatedRecentPeers() -> Signal { + return _internal_managedUpdatedRecentPeers(accountPeerId: self.account.peerId, postbox: self.account.postbox, network: self.account.network) + } + + public func removeRecentPeer(peerId: PeerId) -> Signal { + return _internal_removeRecentPeer(account: self.account, peerId: peerId) + } + + public func updateRecentPeersEnabled(enabled: Bool) -> Signal { + return _internal_updateRecentPeersEnabled(postbox: self.account.postbox, network: self.account.network, enabled: enabled) + } + + public func addRecentlyUsedInlineBot(peerId: PeerId) -> Signal { + return _internal_addRecentlyUsedInlineBot(postbox: self.account.postbox, peerId: peerId) + } + + public func recentlyUsedInlineBots() -> Signal<[(Peer, Double)], NoError> { + return _internal_recentlyUsedInlineBots(postbox: self.account.postbox) + } + + public func removeRecentlyUsedInlineBot(peerId: PeerId) -> Signal { + return _internal_removeRecentlyUsedInlineBot(account: self.account, peerId: peerId) + } + + public func uploadedPeerPhoto(resource: MediaResource) -> Signal { + return _internal_uploadedPeerPhoto(postbox: self.account.postbox, network: self.account.network, resource: resource) + } + + public func uploadedPeerVideo(resource: MediaResource) -> Signal { + return _internal_uploadedPeerVideo(postbox: self.account.postbox, network: self.account.network, messageMediaPreuploadManager: self.account.messageMediaPreuploadManager, resource: resource) + } + + public func updatePeerPhoto(peerId: PeerId, photo: Signal?, video: Signal? = nil, videoStartTimestamp: Double? = nil, mapResourceToAvatarSizes: @escaping (MediaResource, [TelegramMediaImageRepresentation]) -> Signal<[Int: Data], NoError>) -> Signal { + return _internal_updatePeerPhoto(postbox: self.account.postbox, network: self.account.network, stateManager: self.account.stateManager, accountPeerId: self.account.peerId, peerId: peerId, photo: photo, video: video, videoStartTimestamp: videoStartTimestamp, mapResourceToAvatarSizes: mapResourceToAvatarSizes) + } + + public func requestUpdateChatListFilter(id: Int32, filter: ChatListFilter?) -> Signal { + return _internal_requestUpdateChatListFilter(postbox: self.account.postbox, network: self.account.network, id: id, filter: filter) + } + + public func requestUpdateChatListFilterOrder(ids: [Int32]) -> Signal { + return _internal_requestUpdateChatListFilterOrder(account: self.account, ids: ids) + } + + public func generateNewChatListFilterId(filters: [ChatListFilter]) -> Int32 { + return _internal_generateNewChatListFilterId(filters: filters) + } + + public func updateChatListFiltersInteractively(_ f: @escaping ([ChatListFilter]) -> [ChatListFilter]) -> Signal<[ChatListFilter], NoError> { + return _internal_updateChatListFiltersInteractively(postbox: self.account.postbox, f) + } + + public func updatedChatListFilters() -> Signal<[ChatListFilter], NoError> { + return _internal_updatedChatListFilters(postbox: self.account.postbox) + } + + public func updatedChatListFiltersInfo() -> Signal<(filters: [ChatListFilter], synchronized: Bool), NoError> { + return _internal_updatedChatListFiltersInfo(postbox: self.account.postbox) + } + + public func currentChatListFilters() -> Signal<[ChatListFilter], NoError> { + return _internal_currentChatListFilters(postbox: self.account.postbox) + } + + public func markChatListFeaturedFiltersAsSeen() -> Signal { + return _internal_markChatListFeaturedFiltersAsSeen(postbox: self.account.postbox) + } + + public func updateChatListFeaturedFilters() -> Signal { + return _internal_updateChatListFeaturedFilters(postbox: self.account.postbox, network: self.account.network) + } + + public func unmarkChatListFeaturedFiltersAsSeen() -> Signal { + return self.account.postbox.transaction { transaction in + _internal_unmarkChatListFeaturedFiltersAsSeen(transaction: transaction) + } + |> ignoreValues + } + + public func checkPeerChatServiceActions(peerId: PeerId) -> Signal { + return _internal_checkPeerChatServiceActions(postbox: self.account.postbox, peerId: peerId) + } + + public func createPeerExportedInvitation(peerId: PeerId, expireDate: Int32?, usageLimit: Int32?) -> Signal { + return _internal_createPeerExportedInvitation(account: self.account, peerId: peerId, expireDate: expireDate, usageLimit: usageLimit) + } + + public func editPeerExportedInvitation(peerId: PeerId, link: String, expireDate: Int32?, usageLimit: Int32?) -> Signal { + return _internal_editPeerExportedInvitation(account: self.account, peerId: peerId, link: link, expireDate: expireDate, usageLimit: usageLimit) + } + + public func revokePeerExportedInvitation(peerId: PeerId, link: String) -> Signal { + return _internal_revokePeerExportedInvitation(account: self.account, peerId: peerId, link: link) + } + + public func deletePeerExportedInvitation(peerId: PeerId, link: String) -> Signal { + return _internal_deletePeerExportedInvitation(account: self.account, peerId: peerId, link: link) + } + + public func deleteAllRevokedPeerExportedInvitations(peerId: PeerId, adminId: PeerId) -> Signal { + return _internal_deleteAllRevokedPeerExportedInvitations(account: self.account, peerId: peerId, adminId: adminId) + } + + public func peerExportedInvitationsCreators(peerId: PeerId) -> Signal<[ExportedInvitationCreator], NoError> { + return _internal_peerExportedInvitationsCreators(account: self.account, peerId: peerId) + } + + public func peerExportedInvitations(peerId: PeerId, adminId: PeerId?, revoked: Bool, forceUpdate: Bool) -> PeerExportedInvitationsContext { + return PeerExportedInvitationsContext(account: self.account, peerId: peerId, adminId: adminId, revoked: revoked, forceUpdate: forceUpdate) + } + + public func peerInvitationImporters(peerId: PeerId, invite: ExportedInvitation) -> PeerInvitationImportersContext { + return PeerInvitationImportersContext(account: self.account, peerId: peerId, invite: invite) + } + + public func notificationExceptionsList() -> Signal { + return _internal_notificationExceptionsList(postbox: self.account.postbox, network: self.account.network) + } + + public func fetchAndUpdateCachedPeerData(peerId: PeerId) -> Signal { + return _internal_fetchAndUpdateCachedPeerData(accountPeerId: self.account.peerId, peerId: peerId, network: self.account.network, postbox: self.account.postbox) + } + + public func toggleItemPinned(location: TogglePeerChatPinnedLocation, itemId: PinnedItemId) -> Signal { + return _internal_toggleItemPinned(postbox: self.account.postbox, location: location, itemId: itemId) + } + + public func getPinnedItemIds(location: TogglePeerChatPinnedLocation) -> Signal<[PinnedItemId], NoError> { + return self.account.postbox.transaction { transaction -> [PinnedItemId] in + return _internal_getPinnedItemIds(transaction: transaction, location: location) + } + } + + public func reorderPinnedItemIds(location: TogglePeerChatPinnedLocation, itemIds: [PinnedItemId]) -> Signal { + return self.account.postbox.transaction { transaction -> Bool in + return _internal_reorderPinnedItemIds(transaction: transaction, location: location, itemIds: itemIds) + } + } + + public func joinChatInteractively(with hash: String) -> Signal { + return _internal_joinChatInteractively(with: hash, account: self.account) + } + + public func joinLinkInformation(_ hash: String) -> Signal { + return _internal_joinLinkInformation(hash, account: self.account) + } + + public func updatePeerTitle(peerId: PeerId, title: String) -> Signal { + return _internal_updatePeerTitle(account: self.account, peerId: peerId, title: title) + } + + public func updatePeerDescription(peerId: PeerId, description: String?) -> Signal { + return _internal_updatePeerDescription(account: self.account, peerId: peerId, description: description) + } + } +} diff --git a/submodules/TelegramCore/Sources/ToggleChannelSignatures.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ToggleChannelSignatures.swift similarity index 83% rename from submodules/TelegramCore/Sources/ToggleChannelSignatures.swift rename to submodules/TelegramCore/Sources/TelegramEngine/Peers/ToggleChannelSignatures.swift index 9505ce02db..27dc326ffc 100644 --- a/submodules/TelegramCore/Sources/ToggleChannelSignatures.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ToggleChannelSignatures.swift @@ -6,7 +6,7 @@ import MtProtoKit import SyncCore -public func toggleShouldChannelMessagesSignatures(account:Account, peerId:PeerId, enabled: Bool) -> Signal { +func _internal_toggleShouldChannelMessagesSignatures(account:Account, peerId:PeerId, enabled: Bool) -> Signal { return account.postbox.transaction { transaction -> Signal in if let peer = transaction.getPeer(peerId) as? TelegramChannel, let inputChannel = apiInputChannel(peer) { return account.network.request(Api.functions.channels.toggleSignatures(channel: inputChannel, enabled: enabled ? .boolTrue : .boolFalse)) |> retryRequest |> map { updates -> Void in diff --git a/submodules/TelegramCore/Sources/TogglePeerChatPinned.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/TogglePeerChatPinned.swift similarity index 86% rename from submodules/TelegramCore/Sources/TogglePeerChatPinned.swift rename to submodules/TelegramCore/Sources/TelegramEngine/Peers/TogglePeerChatPinned.swift index ab3a0de39e..a49b9b445d 100644 --- a/submodules/TelegramCore/Sources/TogglePeerChatPinned.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/TogglePeerChatPinned.swift @@ -14,7 +14,7 @@ public enum TogglePeerChatPinnedResult { case limitExceeded(Int) } -public func toggleItemPinned(postbox: Postbox, location: TogglePeerChatPinnedLocation, itemId: PinnedItemId) -> Signal { +func _internal_toggleItemPinned(postbox: Postbox, location: TogglePeerChatPinnedLocation, itemId: PinnedItemId) -> Signal { return postbox.transaction { transaction -> TogglePeerChatPinnedResult in switch location { case let .group(groupId): @@ -60,7 +60,7 @@ public func toggleItemPinned(postbox: Postbox, location: TogglePeerChatPinnedLoc } case let .filter(filterId): var result: TogglePeerChatPinnedResult = .done - updateChatListFiltersInteractively(transaction: transaction, { filters in + _internal_updateChatListFiltersInteractively(transaction: transaction, { filters in var filters = filters if let index = filters.firstIndex(where: { $0.id == filterId }) { switch itemId { @@ -81,13 +81,13 @@ public func toggleItemPinned(postbox: Postbox, location: TogglePeerChatPinnedLoc } } -public func getPinnedItemIds(transaction: Transaction, location: TogglePeerChatPinnedLocation) -> [PinnedItemId] { +func _internal_getPinnedItemIds(transaction: Transaction, location: TogglePeerChatPinnedLocation) -> [PinnedItemId] { switch location { case let .group(groupId): return transaction.getPinnedItemIds(groupId: groupId) case let .filter(filterId): var itemIds: [PinnedItemId] = [] - let _ = updateChatListFiltersInteractively(transaction: transaction, { filters in + let _ = _internal_updateChatListFiltersInteractively(transaction: transaction, { filters in if let index = filters.firstIndex(where: { $0.id == filterId }) { itemIds = filters[index].data.includePeers.pinnedPeers.map { peerId in return .peer(peerId) @@ -99,7 +99,7 @@ public func getPinnedItemIds(transaction: Transaction, location: TogglePeerChatP } } -public func reorderPinnedItemIds(transaction: Transaction, location: TogglePeerChatPinnedLocation, itemIds: [PinnedItemId]) -> Bool { +func _internal_reorderPinnedItemIds(transaction: Transaction, location: TogglePeerChatPinnedLocation, itemIds: [PinnedItemId]) -> Bool { switch location { case let .group(groupId): if transaction.getPinnedItemIds(groupId: groupId) != itemIds { @@ -111,7 +111,7 @@ public func reorderPinnedItemIds(transaction: Transaction, location: TogglePeerC } case let .filter(filterId): var result: Bool = false - updateChatListFiltersInteractively(transaction: transaction, { filters in + _internal_updateChatListFiltersInteractively(transaction: transaction, { filters in var filters = filters if let index = filters.firstIndex(where: { $0.id == filterId }) { let peerIds: [PeerId] = itemIds.map { itemId -> PeerId in diff --git a/submodules/TelegramCore/Sources/UpdateCachedPeerData.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateCachedPeerData.swift similarity index 95% rename from submodules/TelegramCore/Sources/UpdateCachedPeerData.swift rename to submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateCachedPeerData.swift index 61a7faea29..a4e378fbb8 100644 --- a/submodules/TelegramCore/Sources/UpdateCachedPeerData.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateCachedPeerData.swift @@ -124,7 +124,7 @@ func fetchAndUpdateSupplementalCachedPeerData(peerId rawPeerId: PeerId, network: } } -func fetchAndUpdateCachedPeerData(accountPeerId: PeerId, peerId rawPeerId: PeerId, network: Network, postbox: Postbox) -> Signal { +func _internal_fetchAndUpdateCachedPeerData(accountPeerId: PeerId, peerId rawPeerId: PeerId, network: Network, postbox: Postbox) -> Signal { return postbox.combinedView(keys: [.basicPeer(rawPeerId)]) |> mapToSignal { views -> Signal in if accountPeerId == rawPeerId { @@ -133,7 +133,7 @@ func fetchAndUpdateCachedPeerData(accountPeerId: PeerId, peerId rawPeerId: PeerI guard let view = views.views[.basicPeer(rawPeerId)] as? BasicPeerView else { return .complete() } - guard let peer = view.peer else { + guard let _ = view.peer else { return .complete() } return .single(true) @@ -217,7 +217,7 @@ func fetchAndUpdateCachedPeerData(accountPeerId: PeerId, peerId rawPeerId: PeerI } } } else if peerId.namespace == Namespaces.Peer.CloudGroup { - return network.request(Api.functions.messages.getFullChat(chatId: peerId.id)) + return network.request(Api.functions.messages.getFullChat(chatId: peerId.id._internalGetInt32Value())) |> retryRequest |> mapToSignal { result -> Signal in return postbox.transaction { transaction -> Bool in @@ -236,7 +236,7 @@ func fetchAndUpdateCachedPeerData(accountPeerId: PeerId, peerId rawPeerId: PeerI for botInfo in chatFull.botInfo ?? [] { switch botInfo { case let .botInfo(userId, _, _): - let peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: userId) + let peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt32Value(userId)) let parsedBotInfo = BotInfo(apiBotInfo: botInfo) botInfos.append(CachedPeerBotInfo(peerId: peerId, botInfo: parsedBotInfo)) } @@ -306,7 +306,7 @@ func fetchAndUpdateCachedPeerData(accountPeerId: PeerId, peerId rawPeerId: PeerI if let inputCall = chatFull.call { switch inputCall { case let .inputGroupCall(id, accessHash): - updatedActiveCall = CachedChannelData.ActiveCall(id: id, accessHash: accessHash, title: previous.activeCall?.title) + updatedActiveCall = CachedChannelData.ActiveCall(id: id, accessHash: accessHash, title: previous.activeCall?.title, scheduleTimestamp: previous.activeCall?.scheduleTimestamp, subscribedToScheduled: previous.activeCall?.subscribedToScheduled ?? false) } } @@ -338,7 +338,7 @@ func fetchAndUpdateCachedPeerData(accountPeerId: PeerId, peerId rawPeerId: PeerI } return .single(nil) } - let participantSignal = network.request(Api.functions.channels.getParticipant(channel: inputChannel, userId: .inputUserSelf)) + let participantSignal = network.request(Api.functions.channels.getParticipant(channel: inputChannel, participant: .inputPeerSelf)) |> map(Optional.init) |> `catch` { error -> Signal in return .single(nil) @@ -381,7 +381,7 @@ func fetchAndUpdateCachedPeerData(accountPeerId: PeerId, peerId rawPeerId: PeerI let linkedDiscussionPeerId: PeerId? if let linkedChatId = linkedChatId, linkedChatId != 0 { - linkedDiscussionPeerId = PeerId(namespace: Namespaces.Peer.CloudChannel, id: linkedChatId) + linkedDiscussionPeerId = PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt32Value(linkedChatId)) } else { linkedDiscussionPeerId = nil } @@ -399,7 +399,7 @@ func fetchAndUpdateCachedPeerData(accountPeerId: PeerId, peerId rawPeerId: PeerI for botInfo in apiBotInfos { switch botInfo { case let .botInfo(userId, _, _): - let peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: userId) + let peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt32Value(userId)) let parsedBotInfo = BotInfo(apiBotInfo: botInfo) botInfos.append(CachedPeerBotInfo(peerId: peerId, botInfo: parsedBotInfo)) } @@ -421,7 +421,7 @@ func fetchAndUpdateCachedPeerData(accountPeerId: PeerId, peerId rawPeerId: PeerI 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)) + migrationReference = ChannelMigrationReference(maxMessageId: MessageId(peerId: PeerId(namespace: Namespaces.Peer.CloudGroup, id: PeerId.Id._internalFromInt32Value(migratedFromChatId)), namespace: Namespaces.Message.Cloud, id: migratedFromMaxId)) } var peers: [Peer] = [] @@ -442,7 +442,7 @@ func fetchAndUpdateCachedPeerData(accountPeerId: PeerId, peerId rawPeerId: PeerI if let participantResult = participantResult { switch participantResult { - case let .channelParticipant(_, users): + case let .channelParticipant(_, chats, users): for user in users { if let telegramUser = TelegramUser.merge(transaction.getPeer(user.peerId) as? TelegramUser, rhs: user) { peers.append(telegramUser) @@ -451,6 +451,11 @@ func fetchAndUpdateCachedPeerData(accountPeerId: PeerId, peerId rawPeerId: PeerI } } } + for chat in chats { + if let groupOrChannel = parseTelegramGroupOrChannel(chat: chat) { + peers.append(groupOrChannel) + } + } } } @@ -463,7 +468,7 @@ func fetchAndUpdateCachedPeerData(accountPeerId: PeerId, peerId rawPeerId: PeerI let stickerPack: StickerPackCollectionInfo? = stickerSet.flatMap { apiSet -> StickerPackCollectionInfo in let namespace: ItemCollectionId.Namespace switch apiSet { - case let .stickerSet(flags, _, _, _, _, _, _, _, _, _): + case let .stickerSet(flags, _, _, _, _, _, _, _, _, _, _): if (flags & (1 << 3)) != 0 { namespace = Namespaces.ItemCollection.CloudMaskPacks } else { @@ -482,10 +487,10 @@ func fetchAndUpdateCachedPeerData(accountPeerId: PeerId, peerId rawPeerId: PeerI var invitedBy: PeerId? if let participantResult = participantResult { switch participantResult { - case let.channelParticipant(participant, _): + case let.channelParticipant(participant, _, _): switch participant { case let .channelParticipantSelf(_, inviterId, _): - invitedBy = PeerId(namespace: Namespaces.Peer.CloudUser, id: inviterId) + invitedBy = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt32Value(inviterId)) default: break } @@ -511,7 +516,7 @@ func fetchAndUpdateCachedPeerData(accountPeerId: PeerId, peerId rawPeerId: PeerI if let inputCall = inputCall { switch inputCall { case let .inputGroupCall(id, accessHash): - updatedActiveCall = CachedChannelData.ActiveCall(id: id, accessHash: accessHash, title: previous.activeCall?.title) + updatedActiveCall = CachedChannelData.ActiveCall(id: id, accessHash: accessHash, title: previous.activeCall?.title, scheduleTimestamp: previous.activeCall?.scheduleTimestamp, subscribedToScheduled: previous.activeCall?.subscribedToScheduled ?? false) } } diff --git a/submodules/TelegramCore/Sources/UpdateGroupSpecificStickerset.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateGroupSpecificStickerset.swift similarity index 89% rename from submodules/TelegramCore/Sources/UpdateGroupSpecificStickerset.swift rename to submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateGroupSpecificStickerset.swift index 860cdea9cc..22f063f1d4 100644 --- a/submodules/TelegramCore/Sources/UpdateGroupSpecificStickerset.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateGroupSpecificStickerset.swift @@ -9,7 +9,7 @@ public enum UpdateGroupSpecificStickersetError { case generic } -public func updateGroupSpecificStickerset(postbox: Postbox, network: Network, peerId: PeerId, info: StickerPackCollectionInfo?) -> Signal { +func _internal_updateGroupSpecificStickerset(postbox: Postbox, network: Network, peerId: PeerId, info: StickerPackCollectionInfo?) -> Signal { return postbox.loadedPeerWithId(peerId) |> castError(UpdateGroupSpecificStickersetError.self) |> mapToSignal { peer -> Signal in diff --git a/submodules/TelegramCore/Sources/UpdatePeerInfo.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdatePeerInfo.swift similarity index 92% rename from submodules/TelegramCore/Sources/UpdatePeerInfo.swift rename to submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdatePeerInfo.swift index d70ded671a..6eaba7a6ae 100644 --- a/submodules/TelegramCore/Sources/UpdatePeerInfo.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdatePeerInfo.swift @@ -10,7 +10,7 @@ public enum UpdatePeerTitleError { case generic } -public func updatePeerTitle(account: Account, peerId: PeerId, title: String) -> Signal { +func _internal_updatePeerTitle(account: Account, peerId: PeerId, title: String) -> Signal { return account.postbox.transaction { transaction -> Signal in if let peer = transaction.getPeer(peerId) { if let peer = peer as? TelegramChannel, let inputChannel = apiInputChannel(peer) { @@ -27,10 +27,10 @@ public func updatePeerTitle(account: Account, peerId: PeerId, title: String) -> return updated }) } - } |> mapError { _ -> UpdatePeerTitleError in return .generic } + } |> mapError { _ -> UpdatePeerTitleError in } } } else if let peer = peer as? TelegramGroup { - return account.network.request(Api.functions.messages.editChatTitle(chatId: peer.id.id, title: title)) + return account.network.request(Api.functions.messages.editChatTitle(chatId: peer.id.id._internalGetInt32Value(), title: title)) |> mapError { _ -> UpdatePeerTitleError in return .generic } @@ -58,7 +58,7 @@ public enum UpdatePeerDescriptionError { case generic } -public func updatePeerDescription(account: Account, peerId: PeerId, description: String?) -> Signal { +func _internal_updatePeerDescription(account: Account, peerId: PeerId, description: String?) -> Signal { return account.postbox.transaction { transaction -> Signal in if let peer = transaction.getPeer(peerId) { if (peer is TelegramChannel || peer is TelegramGroup), let inputPeer = apiInputPeer(peer) { diff --git a/submodules/TelegramCore/Sources/PeersNearby.swift b/submodules/TelegramCore/Sources/TelegramEngine/PeersNearby/PeersNearby.swift similarity index 98% rename from submodules/TelegramCore/Sources/PeersNearby.swift rename to submodules/TelegramCore/Sources/TelegramEngine/PeersNearby/PeersNearby.swift index 6823782443..e7c766a80b 100644 --- a/submodules/TelegramCore/Sources/PeersNearby.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/PeersNearby/PeersNearby.swift @@ -25,7 +25,7 @@ public enum PeerNearbyVisibilityUpdate { case invisible } -public func updatePeersNearbyVisibility(account: Account, update: PeerNearbyVisibilityUpdate, background: Bool) -> Signal { +func _internal_updatePeersNearbyVisibility(account: Account, update: PeerNearbyVisibilityUpdate, background: Bool) -> Signal { var flags: Int32 = 0 var geoPoint: Api.InputGeoPoint var selfExpires: Int32? @@ -138,7 +138,6 @@ public final class PeersNearbyContext { } |> restartIfError |> `catch` { _ -> Signal<[PeerNearby], NoError> in - return .single([]) } self.disposable.set((combined diff --git a/submodules/TelegramCore/Sources/TelegramEngine/PeersNearby/TelegramEnginePeersNearby.swift b/submodules/TelegramCore/Sources/TelegramEngine/PeersNearby/TelegramEnginePeersNearby.swift new file mode 100644 index 0000000000..3117401c2c --- /dev/null +++ b/submodules/TelegramCore/Sources/TelegramEngine/PeersNearby/TelegramEnginePeersNearby.swift @@ -0,0 +1,15 @@ +import SwiftSignalKit + +public extension TelegramEngine { + final class PeersNearby { + private let account: Account + + init(account: Account) { + self.account = account + } + + public func updatePeersNearbyVisibility(update: PeerNearbyVisibilityUpdate, background: Bool) -> Signal { + return _internal_updatePeersNearbyVisibility(account: self.account, update: update, background: background) + } + } +} diff --git a/submodules/TelegramCore/Sources/ActiveSessionsContext.swift b/submodules/TelegramCore/Sources/TelegramEngine/Privacy/ActiveSessionsContext.swift similarity index 99% rename from submodules/TelegramCore/Sources/ActiveSessionsContext.swift rename to submodules/TelegramCore/Sources/TelegramEngine/Privacy/ActiveSessionsContext.swift index c060382a5a..637e075d18 100644 --- a/submodules/TelegramCore/Sources/ActiveSessionsContext.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Privacy/ActiveSessionsContext.swift @@ -136,7 +136,7 @@ public final class ActiveSessionsContext { } } - public init(account: Account) { + init(account: Account) { self.impl = QueueLocalObject(queue: Queue.mainQueue(), generate: { return ActiveSessionsContextImpl(account: account) }) @@ -218,7 +218,7 @@ public final class WebSessionsContext { private let disposable = MetaDisposable() - public init(account: Account) { + init(account: Account) { assert(Queue.mainQueue().isCurrent()) self.account = account diff --git a/submodules/TelegramCore/Sources/BlockedPeers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Privacy/BlockedPeers.swift similarity index 96% rename from submodules/TelegramCore/Sources/BlockedPeers.swift rename to submodules/TelegramCore/Sources/TelegramEngine/Privacy/BlockedPeers.swift index c671ed35a4..bd5794c41c 100644 --- a/submodules/TelegramCore/Sources/BlockedPeers.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Privacy/BlockedPeers.swift @@ -41,7 +41,7 @@ public func requestBlockedPeers(account: Account) -> Signal<[Peer], NoError> { } } -public func requestUpdatePeerIsBlocked(account: Account, peerId: PeerId, isBlocked: Bool) -> Signal { +func _internal_requestUpdatePeerIsBlocked(account: Account, peerId: PeerId, isBlocked: Bool) -> Signal { return account.postbox.transaction { transaction -> Signal in if let peer = transaction.getPeer(peerId), let inputPeer = apiInputPeer(peer) { let signal: Signal diff --git a/submodules/TelegramCore/Sources/BlockedPeersContext.swift b/submodules/TelegramCore/Sources/TelegramEngine/Privacy/BlockedPeersContext.swift similarity index 99% rename from submodules/TelegramCore/Sources/BlockedPeersContext.swift rename to submodules/TelegramCore/Sources/TelegramEngine/Privacy/BlockedPeersContext.swift index 06985ae459..fae1f08062 100644 --- a/submodules/TelegramCore/Sources/BlockedPeersContext.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Privacy/BlockedPeersContext.swift @@ -3,7 +3,6 @@ import TelegramApi import Postbox import SwiftSignalKit import MtProtoKit - import SyncCore public struct BlockedPeersContextState: Equatable { diff --git a/submodules/TelegramCore/Sources/RecentAccountSession.swift b/submodules/TelegramCore/Sources/TelegramEngine/Privacy/RecentAccountSession.swift similarity index 100% rename from submodules/TelegramCore/Sources/RecentAccountSession.swift rename to submodules/TelegramCore/Sources/TelegramEngine/Privacy/RecentAccountSession.swift diff --git a/submodules/TelegramCore/Sources/RecentAccountSessions.swift b/submodules/TelegramCore/Sources/TelegramEngine/Privacy/RecentAccountSessions.swift similarity index 83% rename from submodules/TelegramCore/Sources/RecentAccountSessions.swift rename to submodules/TelegramCore/Sources/TelegramEngine/Privacy/RecentAccountSessions.swift index 68ee35add2..3ceba550e1 100644 --- a/submodules/TelegramCore/Sources/RecentAccountSessions.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Privacy/RecentAccountSessions.swift @@ -3,7 +3,7 @@ import Postbox import TelegramApi import SwiftSignalKit -public func requestRecentAccountSessions(account: Account) -> Signal<[RecentAccountSession], NoError> { +func requestRecentAccountSessions(account: Account) -> Signal<[RecentAccountSession], NoError> { return account.network.request(Api.functions.account.getAuthorizations()) |> retryRequest |> map { result -> [RecentAccountSession] in @@ -23,7 +23,7 @@ public enum TerminateSessionError { case freshReset } -public func terminateAccountSession(account: Account, hash: Int64) -> Signal { +func terminateAccountSession(account: Account, hash: Int64) -> Signal { return account.network.request(Api.functions.account.resetAuthorization(hash: hash)) |> mapError { error -> TerminateSessionError in if error.errorCode == 406 { @@ -36,7 +36,7 @@ public func terminateAccountSession(account: Account, hash: Int64) -> Signal Signal { +func terminateOtherAccountSessions(account: Account) -> Signal { return account.network.request(Api.functions.auth.resetAuthorizations()) |> mapError { error -> TerminateSessionError in if error.errorCode == 406 { diff --git a/submodules/TelegramCore/Sources/RecentWebSessions.swift b/submodules/TelegramCore/Sources/TelegramEngine/Privacy/RecentWebSessions.swift similarity index 81% rename from submodules/TelegramCore/Sources/RecentWebSessions.swift rename to submodules/TelegramCore/Sources/TelegramEngine/Privacy/RecentWebSessions.swift index 5c8d25baa9..d36c953a57 100644 --- a/submodules/TelegramCore/Sources/RecentWebSessions.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Privacy/RecentWebSessions.swift @@ -21,7 +21,7 @@ public struct WebAuthorization : Equatable { } } -public func webSessions(network: Network) -> Signal<([WebAuthorization], [PeerId: Peer]), NoError> { +func webSessions(network: Network) -> Signal<([WebAuthorization], [PeerId: Peer]), NoError> { return network.request(Api.functions.account.getWebAuthorizations()) |> retryRequest |> map { result -> ([WebAuthorization], [PeerId : Peer]) in @@ -32,7 +32,7 @@ public func webSessions(network: Network) -> Signal<([WebAuthorization], [PeerId for authorization in authorizations { switch authorization { case let .webAuthorization(hash, botId, domain, browser, platform, dateCreated, dateActive, ip, region): - sessions.append(WebAuthorization(hash: hash, botId: PeerId(namespace: Namespaces.Peer.CloudUser, id: botId), domain: domain, browser: browser, platform: platform, dateCreated: dateCreated, dateActive: dateActive, ip: ip, region: region)) + sessions.append(WebAuthorization(hash: hash, botId: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt32Value(botId)), domain: domain, browser: browser, platform: platform, dateCreated: dateCreated, dateActive: dateActive, ip: ip, region: region)) } } @@ -46,7 +46,7 @@ public func webSessions(network: Network) -> Signal<([WebAuthorization], [PeerId } -public func terminateWebSession(network: Network, hash: Int64) -> Signal { +func terminateWebSession(network: Network, hash: Int64) -> Signal { return network.request(Api.functions.account.resetWebAuthorization(hash: hash)) |> retryRequest |> map { result in @@ -61,7 +61,7 @@ public func terminateWebSession(network: Network, hash: Int64) -> Signal Signal { +func terminateAllWebSessions(network: Network) -> Signal { return network.request(Api.functions.account.resetWebAuthorizations()) |> retryRequest |> map { _ in } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Privacy/TelegramEnginePrivacy.swift b/submodules/TelegramCore/Sources/TelegramEngine/Privacy/TelegramEnginePrivacy.swift new file mode 100644 index 0000000000..19533db705 --- /dev/null +++ b/submodules/TelegramCore/Sources/TelegramEngine/Privacy/TelegramEnginePrivacy.swift @@ -0,0 +1,44 @@ +import SwiftSignalKit +import Postbox + +public extension TelegramEngine { + final class Privacy { + private let account: Account + + init(account: Account) { + self.account = account + } + + public func requestUpdatePeerIsBlocked(peerId: PeerId, isBlocked: Bool) -> Signal { + return _internal_requestUpdatePeerIsBlocked(account: self.account, peerId: peerId, isBlocked: isBlocked) + } + + public func activeSessions() -> ActiveSessionsContext { + return ActiveSessionsContext(account: self.account) + } + + public func webSessions() -> WebSessionsContext { + return WebSessionsContext(account: self.account) + } + + public func requestAccountPrivacySettings() -> Signal { + return _internal_requestAccountPrivacySettings(account: self.account) + } + + public func updateAccountAutoArchiveChats(value: Bool) -> Signal { + return _internal_updateAccountAutoArchiveChats(account: self.account, value: value) + } + + public func updateAccountRemovalTimeout(timeout: Int32) -> Signal { + return _internal_updateAccountRemovalTimeout(account: self.account, timeout: timeout) + } + + public func updatePhoneNumberDiscovery(value: Bool) -> Signal { + return _internal_updatePhoneNumberDiscovery(account: self.account, value: value) + } + + public func updateSelectiveAccountPrivacySettings(type: UpdateSelectiveAccountPrivacySettingsType, settings: SelectivePrivacySettings) -> Signal { + return _internal_updateSelectiveAccountPrivacySettings(account: self.account, type: type, settings: settings) + } + } +} diff --git a/submodules/TelegramCore/Sources/UpdatedAccountPrivacySettings.swift b/submodules/TelegramCore/Sources/TelegramEngine/Privacy/UpdatedAccountPrivacySettings.swift similarity index 94% rename from submodules/TelegramCore/Sources/UpdatedAccountPrivacySettings.swift rename to submodules/TelegramCore/Sources/TelegramEngine/Privacy/UpdatedAccountPrivacySettings.swift index 0f8c5b47a6..b0e7daf71a 100644 --- a/submodules/TelegramCore/Sources/UpdatedAccountPrivacySettings.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Privacy/UpdatedAccountPrivacySettings.swift @@ -5,7 +5,7 @@ import SwiftSignalKit import SyncCore -public func requestAccountPrivacySettings(account: Account) -> Signal { +func _internal_requestAccountPrivacySettings(account: Account) -> Signal { let lastSeenPrivacy = account.network.request(Api.functions.account.getPrivacy(key: .inputPrivacyKeyStatusTimestamp)) let groupPrivacy = account.network.request(Api.functions.account.getPrivacy(key: .inputPrivacyKeyChatInvite)) let voiceCallPrivacy = account.network.request(Api.functions.account.getPrivacy(key: .inputPrivacyKeyPhoneCall)) @@ -140,8 +140,7 @@ public func requestAccountPrivacySettings(account: Account) -> Signal Signal { - +func _internal_updateAccountAutoArchiveChats(account: Account, value: Bool) -> Signal { return account.network.request(Api.functions.account.setGlobalPrivacySettings( settings: .globalPrivacySettings(flags: 1 << 0, archiveAndMuteNewNoncontactPeers: value ? .boolTrue : .boolFalse) )) @@ -149,7 +148,7 @@ public func updateAccountAutoArchiveChats(account: Account, value: Bool) -> Sign |> ignoreValues } -public func updateAccountRemovalTimeout(account: Account, timeout: Int32) -> Signal { +func _internal_updateAccountRemovalTimeout(account: Account, timeout: Int32) -> Signal { return account.network.request(Api.functions.account.setAccountTTL(ttl: .accountDaysTTL(days: timeout / (24 * 60 * 60)))) |> retryRequest |> mapToSignal { _ -> Signal in @@ -157,7 +156,7 @@ public func updateAccountRemovalTimeout(account: Account, timeout: Int32) -> Sig } } -public func updatePhoneNumberDiscovery(account: Account, value: Bool) -> Signal { +func _internal_updatePhoneNumberDiscovery(account: Account, value: Bool) -> Signal { var rules: [Api.InputPrivacyRule] = [] if value { rules.append(.inputPrivacyValueAllowAll) @@ -223,7 +222,7 @@ private func apiUserAndGroupIds(peerIds: [PeerId: SelectivePrivacyPeer]) -> (use return (users, groups) } -public func updateSelectiveAccountPrivacySettings(account: Account, type: UpdateSelectiveAccountPrivacySettingsType, settings: SelectivePrivacySettings) -> Signal { +func _internal_updateSelectiveAccountPrivacySettings(account: Account, type: UpdateSelectiveAccountPrivacySettingsType, settings: SelectivePrivacySettings) -> Signal { return account.postbox.transaction { transaction -> Signal in var rules: [Api.InputPrivacyRule] = [] switch settings { @@ -234,7 +233,7 @@ public func updateSelectiveAccountPrivacySettings(account: Account, type: Update rules.append(Api.InputPrivacyRule.inputPrivacyValueAllowUsers(users: apiInputUsers(transaction: transaction, peerIds: enablePeers.users))) } if !enablePeers.groups.isEmpty { - rules.append(Api.InputPrivacyRule.inputPrivacyValueAllowChatParticipants(chats: enablePeers.groups.map({ $0.id }))) + rules.append(Api.InputPrivacyRule.inputPrivacyValueAllowChatParticipants(chats: enablePeers.groups.map({ $0.id._internalGetInt32Value() }))) } rules.append(Api.InputPrivacyRule.inputPrivacyValueDisallowAll) @@ -246,14 +245,14 @@ public func updateSelectiveAccountPrivacySettings(account: Account, type: Update rules.append(Api.InputPrivacyRule.inputPrivacyValueAllowUsers(users: apiInputUsers(transaction: transaction, peerIds: enablePeers.users))) } if !enablePeers.groups.isEmpty { - rules.append(Api.InputPrivacyRule.inputPrivacyValueAllowChatParticipants(chats: enablePeers.groups.map({ $0.id }))) + rules.append(Api.InputPrivacyRule.inputPrivacyValueAllowChatParticipants(chats: enablePeers.groups.map({ $0.id._internalGetInt32Value() }))) } if !disablePeers.users.isEmpty { rules.append(Api.InputPrivacyRule.inputPrivacyValueDisallowUsers(users: apiInputUsers(transaction: transaction, peerIds: disablePeers.users))) } if !disablePeers.groups.isEmpty { - rules.append(Api.InputPrivacyRule.inputPrivacyValueDisallowChatParticipants(chats: disablePeers.groups.map({ $0.id }))) + rules.append(Api.InputPrivacyRule.inputPrivacyValueDisallowChatParticipants(chats: disablePeers.groups.map({ $0.id._internalGetInt32Value() }))) } rules.append(Api.InputPrivacyRule.inputPrivacyValueAllowContacts) @@ -264,7 +263,7 @@ public func updateSelectiveAccountPrivacySettings(account: Account, type: Update rules.append(Api.InputPrivacyRule.inputPrivacyValueDisallowUsers(users: apiInputUsers(transaction: transaction, peerIds: disablePeers.users))) } if !disablePeers.groups.isEmpty { - rules.append(Api.InputPrivacyRule.inputPrivacyValueDisallowChatParticipants(chats: disablePeers.groups.map({ $0.id }))) + rules.append(Api.InputPrivacyRule.inputPrivacyValueDisallowChatParticipants(chats: disablePeers.groups.map({ $0.id._internalGetInt32Value() }))) } rules.append(Api.InputPrivacyRule.inputPrivacyValueAllowAll) diff --git a/submodules/TelegramCore/Sources/DeepLinkInfo.swift b/submodules/TelegramCore/Sources/TelegramEngine/Resolve/DeepLinkInfo.swift similarity index 87% rename from submodules/TelegramCore/Sources/DeepLinkInfo.swift rename to submodules/TelegramCore/Sources/TelegramEngine/Resolve/DeepLinkInfo.swift index c56272ad08..c517df7d6c 100644 --- a/submodules/TelegramCore/Sources/DeepLinkInfo.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Resolve/DeepLinkInfo.swift @@ -10,7 +10,7 @@ public struct DeepLinkInfo { public let updateApp: Bool } -public func getDeepLinkInfo(network: Network, path: String) -> Signal { +func _internal_getDeepLinkInfo(network: Network, path: String) -> Signal { return network.request(Api.functions.help.getDeepLinkInfo(path: path)) |> retryRequest |> map { value -> DeepLinkInfo? in switch value { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Resolve/TelegramEngineResolve.swift b/submodules/TelegramCore/Sources/TelegramEngine/Resolve/TelegramEngineResolve.swift new file mode 100644 index 0000000000..909b39dda8 --- /dev/null +++ b/submodules/TelegramCore/Sources/TelegramEngine/Resolve/TelegramEngineResolve.swift @@ -0,0 +1,16 @@ +import SwiftSignalKit +import Postbox + +public extension TelegramEngine { + final class Resolve { + private let account: Account + + init(account: Account) { + self.account = account + } + + public func getDeepLinkInfo(path: String) -> Signal { + return _internal_getDeepLinkInfo(network: self.account.network, path: path) + } + } +} diff --git a/submodules/TelegramCore/Sources/CollectCacheUsageStats.swift b/submodules/TelegramCore/Sources/TelegramEngine/Resources/CollectCacheUsageStats.swift similarity index 98% rename from submodules/TelegramCore/Sources/CollectCacheUsageStats.swift rename to submodules/TelegramCore/Sources/TelegramEngine/Resources/CollectCacheUsageStats.swift index 1c3bc503de..f668e7e65f 100644 --- a/submodules/TelegramCore/Sources/CollectCacheUsageStats.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Resources/CollectCacheUsageStats.swift @@ -53,7 +53,7 @@ private final class CacheUsageStatsState { var upperBound: MessageIndex? } -public func collectCacheUsageStats(account: Account, peerId: PeerId? = nil, additionalCachePaths: [String] = [], logFilesPath: String? = nil) -> Signal { +func _internal_collectCacheUsageStats(account: Account, peerId: PeerId? = nil, additionalCachePaths: [String] = [], logFilesPath: String? = nil) -> Signal { var initialState = CacheUsageStatsState() if let peerId = peerId { initialState.lowerBound = MessageIndex.lowerBound(peerId: peerId) @@ -291,6 +291,6 @@ public func collectCacheUsageStats(account: Account, peerId: PeerId? = nil, addi } } -public func clearCachedMediaResources(account: Account, mediaResourceIds: Set) -> Signal { +func _internal_clearCachedMediaResources(account: Account, mediaResourceIds: Set) -> Signal { return account.postbox.mediaBox.removeCachedResources(mediaResourceIds) } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Resources/TelegramEngineResources.swift b/submodules/TelegramCore/Sources/TelegramEngine/Resources/TelegramEngineResources.swift new file mode 100644 index 0000000000..726005cae6 --- /dev/null +++ b/submodules/TelegramCore/Sources/TelegramEngine/Resources/TelegramEngineResources.swift @@ -0,0 +1,24 @@ +import SwiftSignalKit +import Postbox + +public extension TelegramEngine { + final class Resources { + private let account: Account + + init(account: Account) { + self.account = account + } + + public func preUpload(id: Int64, encrypt: Bool, tag: MediaResourceFetchTag?, source: Signal, onComplete: (()->Void)? = nil) { + return self.account.messageMediaPreuploadManager.add(network: self.account.network, postbox: self.account.postbox, id: id, encrypt: encrypt, tag: tag, source: source, onComplete: onComplete) + } + + public func collectCacheUsageStats(peerId: PeerId? = nil, additionalCachePaths: [String] = [], logFilesPath: String? = nil) -> Signal { + return _internal_collectCacheUsageStats(account: self.account, peerId: peerId, additionalCachePaths: additionalCachePaths, logFilesPath: logFilesPath) + } + + public func clearCachedMediaResources(mediaResourceIds: Set) -> Signal { + return _internal_clearCachedMediaResources(account: self.account, mediaResourceIds: mediaResourceIds) + } + } +} diff --git a/submodules/TelegramCore/Sources/AccessSecureId.swift b/submodules/TelegramCore/Sources/TelegramEngine/SecureId/AccessSecureId.swift similarity index 95% rename from submodules/TelegramCore/Sources/AccessSecureId.swift rename to submodules/TelegramCore/Sources/TelegramEngine/SecureId/AccessSecureId.swift index 11bdaadc44..96e678f46e 100644 --- a/submodules/TelegramCore/Sources/AccessSecureId.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/SecureId/AccessSecureId.swift @@ -153,8 +153,8 @@ public enum SecureIdAccessError { case secretPasswordMismatch } -public func accessSecureId(network: Network, password: String) -> Signal<(context: SecureIdAccessContext, settings: TwoStepVerificationSettings), SecureIdAccessError> { - return requestTwoStepVerifiationSettings(network: network, password: password) +func _internal_accessSecureId(network: Network, password: String) -> Signal<(context: SecureIdAccessContext, settings: TwoStepVerificationSettings), SecureIdAccessError> { + return _internal_requestTwoStepVerifiationSettings(network: network, password: password) |> mapError { error -> SecureIdAccessError in return .passwordError(error) } diff --git a/submodules/TelegramCore/Sources/GrantSecureIdAccess.swift b/submodules/TelegramCore/Sources/TelegramEngine/SecureId/GrantSecureIdAccess.swift similarity index 97% rename from submodules/TelegramCore/Sources/GrantSecureIdAccess.swift rename to submodules/TelegramCore/Sources/TelegramEngine/SecureId/GrantSecureIdAccess.swift index ef93a7ac0f..c191a5b09b 100644 --- a/submodules/TelegramCore/Sources/GrantSecureIdAccess.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/SecureId/GrantSecureIdAccess.swift @@ -333,7 +333,7 @@ public func grantSecureIdAccess(network: Network, peerId: PeerId, publicKey: Str valueHashes.append(.secureValueHash(type: apiSecureValueType(value: value.value), hash: Buffer(data: value.opaqueHash))) } - return network.request(Api.functions.account.acceptAuthorization(botId: peerId.id, scope: scope, publicKey: publicKey, valueHashes: valueHashes, credentials: .secureCredentialsEncrypted(data: Buffer(data: encryptedCredentialsData), hash: Buffer(data: decryptedCredentialsHash), secret: Buffer(data: encryptedSecretData)))) + return network.request(Api.functions.account.acceptAuthorization(botId: peerId.id._internalGetInt32Value(), scope: scope, publicKey: publicKey, valueHashes: valueHashes, credentials: .secureCredentialsEncrypted(data: Buffer(data: encryptedCredentialsData), hash: Buffer(data: decryptedCredentialsHash), secret: Buffer(data: encryptedSecretData)))) |> mapError { error -> GrantSecureIdAccessError in return .generic } diff --git a/submodules/TelegramCore/Sources/RequestSecureIdForm.swift b/submodules/TelegramCore/Sources/TelegramEngine/SecureId/RequestSecureIdForm.swift similarity index 99% rename from submodules/TelegramCore/Sources/RequestSecureIdForm.swift rename to submodules/TelegramCore/Sources/TelegramEngine/SecureId/RequestSecureIdForm.swift index 6a79f35ff1..e7d82f7a20 100644 --- a/submodules/TelegramCore/Sources/RequestSecureIdForm.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/SecureId/RequestSecureIdForm.swift @@ -251,7 +251,7 @@ public func requestSecureIdForm(postbox: Postbox, network: Network, peerId: Peer if publicKey.isEmpty { return .fail(.serverError("PUBLIC_KEY_REQUIRED")) } - return network.request(Api.functions.account.getAuthorizationForm(botId: peerId.id, scope: scope, publicKey: publicKey)) + return network.request(Api.functions.account.getAuthorizationForm(botId: peerId.id._internalGetInt32Value(), scope: scope, publicKey: publicKey)) |> mapError { error -> RequestSecureIdFormError in switch error.errorDescription { case "APP_VERSION_OUTDATED": diff --git a/submodules/TelegramCore/Sources/SaveSecureIdValue.swift b/submodules/TelegramCore/Sources/TelegramEngine/SecureId/SaveSecureIdValue.swift similarity index 99% rename from submodules/TelegramCore/Sources/SaveSecureIdValue.swift rename to submodules/TelegramCore/Sources/TelegramEngine/SecureId/SaveSecureIdValue.swift index 06fb9d5cdb..1523513f4c 100644 --- a/submodules/TelegramCore/Sources/SaveSecureIdValue.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/SecureId/SaveSecureIdValue.swift @@ -276,7 +276,7 @@ public func deleteSecureIdValues(network: Network, keys: Set) } public func dropSecureId(network: Network, currentPassword: String) -> Signal { - return twoStepAuthData(network) + return _internal_twoStepAuthData(network) |> mapError { _ -> AuthorizationPasswordVerificationError in return .generic } diff --git a/submodules/TelegramCore/Sources/SecureFileMediaResource.swift b/submodules/TelegramCore/Sources/TelegramEngine/SecureId/SecureFileMediaResource.swift similarity index 100% rename from submodules/TelegramCore/Sources/SecureFileMediaResource.swift rename to submodules/TelegramCore/Sources/TelegramEngine/SecureId/SecureFileMediaResource.swift diff --git a/submodules/TelegramCore/Sources/SecureIdAddressValue.swift b/submodules/TelegramCore/Sources/TelegramEngine/SecureId/SecureIdAddressValue.swift similarity index 100% rename from submodules/TelegramCore/Sources/SecureIdAddressValue.swift rename to submodules/TelegramCore/Sources/TelegramEngine/SecureId/SecureIdAddressValue.swift diff --git a/submodules/TelegramCore/Sources/SecureIdBankStatementValue.swift b/submodules/TelegramCore/Sources/TelegramEngine/SecureId/SecureIdBankStatementValue.swift similarity index 100% rename from submodules/TelegramCore/Sources/SecureIdBankStatementValue.swift rename to submodules/TelegramCore/Sources/TelegramEngine/SecureId/SecureIdBankStatementValue.swift diff --git a/submodules/TelegramCore/Sources/SecureIdConfiguration.swift b/submodules/TelegramCore/Sources/TelegramEngine/SecureId/SecureIdConfiguration.swift similarity index 100% rename from submodules/TelegramCore/Sources/SecureIdConfiguration.swift rename to submodules/TelegramCore/Sources/TelegramEngine/SecureId/SecureIdConfiguration.swift diff --git a/submodules/TelegramCore/Sources/SecureIdDataTypes.swift b/submodules/TelegramCore/Sources/TelegramEngine/SecureId/SecureIdDataTypes.swift similarity index 100% rename from submodules/TelegramCore/Sources/SecureIdDataTypes.swift rename to submodules/TelegramCore/Sources/TelegramEngine/SecureId/SecureIdDataTypes.swift diff --git a/submodules/TelegramCore/Sources/SecureIdDriversLicenseValue.swift b/submodules/TelegramCore/Sources/TelegramEngine/SecureId/SecureIdDriversLicenseValue.swift similarity index 100% rename from submodules/TelegramCore/Sources/SecureIdDriversLicenseValue.swift rename to submodules/TelegramCore/Sources/TelegramEngine/SecureId/SecureIdDriversLicenseValue.swift diff --git a/submodules/TelegramCore/Sources/SecureIdEmailValue.swift b/submodules/TelegramCore/Sources/TelegramEngine/SecureId/SecureIdEmailValue.swift similarity index 100% rename from submodules/TelegramCore/Sources/SecureIdEmailValue.swift rename to submodules/TelegramCore/Sources/TelegramEngine/SecureId/SecureIdEmailValue.swift diff --git a/submodules/TelegramCore/Sources/SecureIdForm.swift b/submodules/TelegramCore/Sources/TelegramEngine/SecureId/SecureIdForm.swift similarity index 100% rename from submodules/TelegramCore/Sources/SecureIdForm.swift rename to submodules/TelegramCore/Sources/TelegramEngine/SecureId/SecureIdForm.swift diff --git a/submodules/TelegramCore/Sources/SecureIdIDCardValue.swift b/submodules/TelegramCore/Sources/TelegramEngine/SecureId/SecureIdIDCardValue.swift similarity index 100% rename from submodules/TelegramCore/Sources/SecureIdIDCardValue.swift rename to submodules/TelegramCore/Sources/TelegramEngine/SecureId/SecureIdIDCardValue.swift diff --git a/submodules/TelegramCore/Sources/SecureIdInternalPassportValue.swift b/submodules/TelegramCore/Sources/TelegramEngine/SecureId/SecureIdInternalPassportValue.swift similarity index 100% rename from submodules/TelegramCore/Sources/SecureIdInternalPassportValue.swift rename to submodules/TelegramCore/Sources/TelegramEngine/SecureId/SecureIdInternalPassportValue.swift diff --git a/submodules/TelegramCore/Sources/SecureIdPadding.swift b/submodules/TelegramCore/Sources/TelegramEngine/SecureId/SecureIdPadding.swift similarity index 100% rename from submodules/TelegramCore/Sources/SecureIdPadding.swift rename to submodules/TelegramCore/Sources/TelegramEngine/SecureId/SecureIdPadding.swift diff --git a/submodules/TelegramCore/Sources/SecureIdPassportRegistrationValue.swift b/submodules/TelegramCore/Sources/TelegramEngine/SecureId/SecureIdPassportRegistrationValue.swift similarity index 100% rename from submodules/TelegramCore/Sources/SecureIdPassportRegistrationValue.swift rename to submodules/TelegramCore/Sources/TelegramEngine/SecureId/SecureIdPassportRegistrationValue.swift diff --git a/submodules/TelegramCore/Sources/SecureIdPassportValue.swift b/submodules/TelegramCore/Sources/TelegramEngine/SecureId/SecureIdPassportValue.swift similarity index 100% rename from submodules/TelegramCore/Sources/SecureIdPassportValue.swift rename to submodules/TelegramCore/Sources/TelegramEngine/SecureId/SecureIdPassportValue.swift diff --git a/submodules/TelegramCore/Sources/SecureIdPersonalDetailsValue.swift b/submodules/TelegramCore/Sources/TelegramEngine/SecureId/SecureIdPersonalDetailsValue.swift similarity index 100% rename from submodules/TelegramCore/Sources/SecureIdPersonalDetailsValue.swift rename to submodules/TelegramCore/Sources/TelegramEngine/SecureId/SecureIdPersonalDetailsValue.swift diff --git a/submodules/TelegramCore/Sources/SecureIdPhoneValue.swift b/submodules/TelegramCore/Sources/TelegramEngine/SecureId/SecureIdPhoneValue.swift similarity index 100% rename from submodules/TelegramCore/Sources/SecureIdPhoneValue.swift rename to submodules/TelegramCore/Sources/TelegramEngine/SecureId/SecureIdPhoneValue.swift diff --git a/submodules/TelegramCore/Sources/SecureIdRentalAgreementValue.swift b/submodules/TelegramCore/Sources/TelegramEngine/SecureId/SecureIdRentalAgreementValue.swift similarity index 100% rename from submodules/TelegramCore/Sources/SecureIdRentalAgreementValue.swift rename to submodules/TelegramCore/Sources/TelegramEngine/SecureId/SecureIdRentalAgreementValue.swift diff --git a/submodules/TelegramCore/Sources/SecureIdTemporaryRegistrationValue.swift b/submodules/TelegramCore/Sources/TelegramEngine/SecureId/SecureIdTemporaryRegistrationValue.swift similarity index 100% rename from submodules/TelegramCore/Sources/SecureIdTemporaryRegistrationValue.swift rename to submodules/TelegramCore/Sources/TelegramEngine/SecureId/SecureIdTemporaryRegistrationValue.swift diff --git a/submodules/TelegramCore/Sources/SecureIdUtilityBillValue.swift b/submodules/TelegramCore/Sources/TelegramEngine/SecureId/SecureIdUtilityBillValue.swift similarity index 100% rename from submodules/TelegramCore/Sources/SecureIdUtilityBillValue.swift rename to submodules/TelegramCore/Sources/TelegramEngine/SecureId/SecureIdUtilityBillValue.swift diff --git a/submodules/TelegramCore/Sources/SecureIdValue.swift b/submodules/TelegramCore/Sources/TelegramEngine/SecureId/SecureIdValue.swift similarity index 100% rename from submodules/TelegramCore/Sources/SecureIdValue.swift rename to submodules/TelegramCore/Sources/TelegramEngine/SecureId/SecureIdValue.swift diff --git a/submodules/TelegramCore/Sources/SecureIdValueAccessContext.swift b/submodules/TelegramCore/Sources/TelegramEngine/SecureId/SecureIdValueAccessContext.swift similarity index 100% rename from submodules/TelegramCore/Sources/SecureIdValueAccessContext.swift rename to submodules/TelegramCore/Sources/TelegramEngine/SecureId/SecureIdValueAccessContext.swift diff --git a/submodules/TelegramCore/Sources/SecureIdValueContentError.swift b/submodules/TelegramCore/Sources/TelegramEngine/SecureId/SecureIdValueContentError.swift similarity index 100% rename from submodules/TelegramCore/Sources/SecureIdValueContentError.swift rename to submodules/TelegramCore/Sources/TelegramEngine/SecureId/SecureIdValueContentError.swift diff --git a/submodules/TelegramCore/Sources/SecureIdVerificationDocumentReference.swift b/submodules/TelegramCore/Sources/TelegramEngine/SecureId/SecureIdVerificationDocumentReference.swift similarity index 100% rename from submodules/TelegramCore/Sources/SecureIdVerificationDocumentReference.swift rename to submodules/TelegramCore/Sources/TelegramEngine/SecureId/SecureIdVerificationDocumentReference.swift diff --git a/submodules/TelegramCore/Sources/TelegramEngine/SecureId/TelegramEngineSecureId.swift b/submodules/TelegramCore/Sources/TelegramEngine/SecureId/TelegramEngineSecureId.swift new file mode 100644 index 0000000000..935ac07d37 --- /dev/null +++ b/submodules/TelegramCore/Sources/TelegramEngine/SecureId/TelegramEngineSecureId.swift @@ -0,0 +1,15 @@ +import SwiftSignalKit + +public extension TelegramEngine { + final class SecureId { + private let account: Account + + init(account: Account) { + self.account = account + } + + public func accessSecureId(password: String) -> Signal<(context: SecureIdAccessContext, settings: TwoStepVerificationSettings), SecureIdAccessError> { + return _internal_accessSecureId(network: self.account.network, password: password) + } + } +} diff --git a/submodules/TelegramCore/Sources/UploadSecureIdFile.swift b/submodules/TelegramCore/Sources/TelegramEngine/SecureId/UploadSecureIdFile.swift similarity index 100% rename from submodules/TelegramCore/Sources/UploadSecureIdFile.swift rename to submodules/TelegramCore/Sources/TelegramEngine/SecureId/UploadSecureIdFile.swift diff --git a/submodules/TelegramCore/Sources/VerifySecureIdValue.swift b/submodules/TelegramCore/Sources/TelegramEngine/SecureId/VerifySecureIdValue.swift similarity index 100% rename from submodules/TelegramCore/Sources/VerifySecureIdValue.swift rename to submodules/TelegramCore/Sources/TelegramEngine/SecureId/VerifySecureIdValue.swift diff --git a/submodules/TelegramCore/Sources/ArchivedStickerPacks.swift b/submodules/TelegramCore/Sources/TelegramEngine/Stickers/ArchivedStickerPacks.swift similarity index 87% rename from submodules/TelegramCore/Sources/ArchivedStickerPacks.swift rename to submodules/TelegramCore/Sources/TelegramEngine/Stickers/ArchivedStickerPacks.swift index 5ffd9cb9b8..dac1149970 100644 --- a/submodules/TelegramCore/Sources/ArchivedStickerPacks.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Stickers/ArchivedStickerPacks.swift @@ -29,7 +29,7 @@ public final class ArchivedStickerPackItem { } } -public func archivedStickerPacks(account: Account, namespace: ArchivedStickerPacksNamespace = .stickers) -> Signal<[ArchivedStickerPackItem], NoError> { +func _internal_archivedStickerPacks(account: Account, namespace: ArchivedStickerPacksNamespace = .stickers) -> Signal<[ArchivedStickerPackItem], NoError> { var flags: Int32 = 0 if case .masks = namespace { flags |= 1 << 0 @@ -50,7 +50,7 @@ public func archivedStickerPacks(account: Account, namespace: ArchivedStickerPac } } -public func removeArchivedStickerPack(account: Account, info: StickerPackCollectionInfo) -> Signal { +func _internal_removeArchivedStickerPack(account: Account, info: StickerPackCollectionInfo) -> Signal { return account.network.request(Api.functions.messages.uninstallStickerSet(stickerset: Api.InputStickerSet.inputStickerSetID(id: info.id.id, accessHash: info.accessHash))) |> `catch` { _ -> Signal in return .single(.boolFalse) diff --git a/submodules/TelegramCore/Sources/CachedStickerPack.swift b/submodules/TelegramCore/Sources/TelegramEngine/Stickers/CachedStickerPack.swift similarity index 98% rename from submodules/TelegramCore/Sources/CachedStickerPack.swift rename to submodules/TelegramCore/Sources/TelegramEngine/Stickers/CachedStickerPack.swift index 3362bb657f..f9b31ebe8a 100644 --- a/submodules/TelegramCore/Sources/CachedStickerPack.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Stickers/CachedStickerPack.swift @@ -36,7 +36,7 @@ func cacheStickerPack(transaction: Transaction, info: StickerPackCollectionInfo, } } -public func cachedStickerPack(postbox: Postbox, network: Network, reference: StickerPackReference, forceRemote: Bool) -> Signal { +func _internal_cachedStickerPack(postbox: Postbox, network: Network, reference: StickerPackReference, forceRemote: Bool) -> Signal { return postbox.transaction { transaction -> CachedStickerPackResult? in if let (info, items, local) = cachedStickerPack(transaction: transaction, reference: reference) { if local { diff --git a/submodules/TelegramCore/Sources/EmojiKeywords.swift b/submodules/TelegramCore/Sources/TelegramEngine/Stickers/EmojiKeywords.swift similarity index 96% rename from submodules/TelegramCore/Sources/EmojiKeywords.swift rename to submodules/TelegramCore/Sources/TelegramEngine/Stickers/EmojiKeywords.swift index 03f7d35eb8..7dd137a10f 100644 --- a/submodules/TelegramCore/Sources/EmojiKeywords.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Stickers/EmojiKeywords.swift @@ -11,7 +11,7 @@ private enum SearchEmojiKeywordsIntermediateResult { case completed([EmojiKeywordItem]) } -public func searchEmojiKeywords(postbox: Postbox, inputLanguageCode: String, query: String, completeMatch: Bool) -> Signal<[EmojiKeywordItem], NoError> { +func _internal_searchEmojiKeywords(postbox: Postbox, inputLanguageCode: String, query: String, completeMatch: Bool) -> Signal<[EmojiKeywordItem], NoError> { guard !query.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { return .single([]) } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Stickers/ImportStickers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Stickers/ImportStickers.swift new file mode 100644 index 0000000000..78423d64e7 --- /dev/null +++ b/submodules/TelegramCore/Sources/TelegramEngine/Stickers/ImportStickers.swift @@ -0,0 +1,253 @@ +import Foundation +import Postbox +import SwiftSignalKit +import TelegramApi + +import SyncCore + +public enum UploadStickerStatus { + case progress(Float) + case complete(CloudDocumentMediaResource, String) +} + +public enum UploadStickerError { + case generic +} + +private struct UploadedStickerData { + fileprivate let resource: MediaResource + fileprivate let content: UploadedStickerDataContent +} + +private enum UploadedStickerDataContent { + case result(MultipartUploadResult) + case error +} + +private func uploadedSticker(postbox: Postbox, network: Network, resource: MediaResource) -> Signal { + return multipartUpload(network: network, postbox: postbox, source: .resource(.standalone(resource: resource)), encrypt: false, tag: TelegramMediaResourceFetchTag(statsCategory: .file), hintFileSize: nil, hintFileIsLarge: false, forceNoBigParts: false) + |> map { result -> UploadedStickerData in + return UploadedStickerData(resource: resource, content: .result(result)) + } + |> `catch` { _ -> Signal in + return .single(UploadedStickerData(resource: resource, content: .error)) + } +} + +func _internal_uploadSticker(account: Account, peer: Peer, resource: MediaResource, alt: String, dimensions: PixelDimensions, isAnimated: Bool) -> Signal { + guard let inputPeer = apiInputPeer(peer) else { + return .fail(.generic) + } + return uploadedSticker(postbox: account.postbox, network: account.network, resource: resource) + |> mapError { _ -> UploadStickerError in return .generic } + |> mapToSignal { result -> Signal in + switch result.content { + case .error: + return .fail(.generic) + case let .result(resultData): + switch resultData { + case let .progress(progress): + return .single(.progress(progress)) + case let .inputFile(file): + var flags: Int32 = 0 + flags |= (1 << 4) + var attributes: [Api.DocumentAttribute] = [] + attributes.append(.documentAttributeSticker(flags: 0, alt: alt, stickerset: .inputStickerSetEmpty, maskCoords: nil)) + attributes.append(.documentAttributeImageSize(w: dimensions.width, h: dimensions.height)) + return account.network.request(Api.functions.messages.uploadMedia(peer: inputPeer, media: Api.InputMedia.inputMediaUploadedDocument(flags: flags, file: file, thumb: nil, mimeType: isAnimated ? "application/x-tgsticker": "image/png", attributes: attributes, stickers: nil, ttlSeconds: nil))) + |> mapError { _ -> UploadStickerError in return .generic } + |> mapToSignal { media -> Signal in + switch media { + case let .messageMediaDocument(_, document, _): + if let document = document, let file = telegramMediaFileFromApiDocument(document), let resource = file.resource as? CloudDocumentMediaResource { + return .single(.complete(resource, file.mimeType)) + } + default: + break + } + return .fail(.generic) + } + default: + return .fail(.generic) + } + } + } +} + +public enum CreateStickerSetError { + case generic +} + +public struct ImportSticker { + public let resource: MediaResource + let emojis: [String] + public let dimensions: PixelDimensions + + public init(resource: MediaResource, emojis: [String], dimensions: PixelDimensions) { + self.resource = resource + self.emojis = emojis + self.dimensions = dimensions + } +} + +public enum CreateStickerSetStatus { + case progress(Float, Int32, Int32) + case complete(StickerPackCollectionInfo, [ItemCollectionItem]) +} + +func _internal_createStickerSet(account: Account, title: String, shortName: String, stickers: [ImportSticker], thumbnail: ImportSticker?, isAnimated: Bool, software: String?) -> Signal { + return account.postbox.loadedPeerWithId(account.peerId) + |> castError(CreateStickerSetError.self) + |> mapToSignal { peer -> Signal in + guard let inputUser = apiInputUser(peer) else { + return .fail(.generic) + } + var uploadStickers: [Signal] = [] + var stickers = stickers + if let thumbnail = thumbnail { + stickers.append(thumbnail) + } + for sticker in stickers { + if let resource = sticker.resource as? CloudDocumentMediaResource { + uploadStickers.append(.single(.complete(resource, isAnimated ? "application/x-tgsticker": "image/png"))) + } else { + uploadStickers.append(_internal_uploadSticker(account: account, peer: peer, resource: sticker.resource, alt: sticker.emojis.first ?? "", dimensions: sticker.dimensions, isAnimated: isAnimated) + |> mapError { _ -> CreateStickerSetError in + return .generic + }) + } + } + return combineLatest(uploadStickers) + |> mapToSignal { uploadedStickers -> Signal in + var resources: [CloudDocumentMediaResource] = [] + for sticker in uploadedStickers { + if case let .complete(resource, _) = sticker { + resources.append(resource) + } + } + if resources.count == stickers.count { + var flags: Int32 = 0 + if isAnimated { + flags |= (1 << 1) + } + var inputStickers: [Api.InputStickerSetItem] = [] + let stickerDocuments = thumbnail != nil ? resources.dropLast() : resources + for i in 0 ..< stickerDocuments.count { + let sticker = stickers[i] + let resource = resources[i] + inputStickers.append(.inputStickerSetItem(flags: 0, document: .inputDocument(id: resource.fileId, accessHash: resource.accessHash, fileReference: Buffer(data: resource.fileReference ?? Data())), emoji: sticker.emojis.first ?? "", maskCoords: nil)) + } + var thumbnailDocument: Api.InputDocument? + if thumbnail != nil, let resource = resources.last { + flags |= (1 << 2) + thumbnailDocument = .inputDocument(id: resource.fileId, accessHash: resource.accessHash, fileReference: Buffer(data: resource.fileReference ?? Data())) + } + if let software = software, !software.isEmpty { + flags |= (1 << 3) + } + return account.network.request(Api.functions.stickers.createStickerSet(flags: flags, userId: inputUser, title: title, shortName: shortName, thumb: thumbnailDocument, stickers: inputStickers, software: software)) + |> mapError { error -> CreateStickerSetError in + return .generic + } + |> mapToSignal { result -> Signal in + let info: StickerPackCollectionInfo + var items: [ItemCollectionItem] = [] + + switch result { + case let .stickerSet(set, packs, documents): + let namespace: ItemCollectionId.Namespace + switch set { + case let .stickerSet(flags, _, _, _, _, _, _, _, _, _, _): + if (flags & (1 << 3)) != 0 { + namespace = Namespaces.ItemCollection.CloudMaskPacks + } else { + namespace = Namespaces.ItemCollection.CloudStickerPacks + } + } + info = StickerPackCollectionInfo(apiSet: set, namespace: namespace) + var indexKeysByFile: [MediaId: [MemoryBuffer]] = [:] + for pack in packs { + switch pack { + case let .stickerPack(text, fileIds): + let key = ValueBoxKey(text).toMemoryBuffer() + for fileId in fileIds { + let mediaId = MediaId(namespace: Namespaces.Media.CloudFile, id: fileId) + if indexKeysByFile[mediaId] == nil { + indexKeysByFile[mediaId] = [key] + } else { + indexKeysByFile[mediaId]!.append(key) + } + } + } + } + + for apiDocument in documents { + if let file = telegramMediaFileFromApiDocument(apiDocument), let id = file.id { + let fileIndexKeys: [MemoryBuffer] + if let indexKeys = indexKeysByFile[id] { + fileIndexKeys = indexKeys + } else { + fileIndexKeys = [] + } + items.append(StickerPackItem(index: ItemCollectionItemIndex(index: Int32(items.count), id: id.id), file: file, indexKeys: fileIndexKeys)) + } + } + } + return .single(.complete(info, items)) + } + } else { + var totalProgress: Float = 0.0 + var completeCount: Int32 = 0 + for sticker in uploadedStickers { + switch sticker { + case .complete: + totalProgress += 1.0 + completeCount += 1 + case let .progress(progress): + totalProgress += progress + if progress == 1.0 { + completeCount += 1 + } + } + } + let normalizedProgress = min(1.0, max(0.0, totalProgress / Float(stickers.count))) + return .single(.progress(normalizedProgress, completeCount, Int32(uploadedStickers.count))) + } + } + } +} + +func _internal_getStickerSetShortNameSuggestion(account: Account, title: String) -> Signal { + return account.network.request(Api.functions.stickers.suggestShortName(title: title)) + |> map (Optional.init) + |> `catch` { _ in + return .single(nil) + } + |> map { result in + guard let result = result else { + return nil + } + switch result { + case let .suggestedShortName(shortName): + return shortName + } + } +} + +func _internal_stickerSetShortNameAvailability(account: Account, shortName: String) -> Signal { + return account.network.request(Api.functions.stickers.checkShortName(shortName: shortName)) + |> map { result -> AddressNameAvailability in + switch result { + case .boolTrue: + return .available + case .boolFalse: + return .taken + } + } + |> `catch` { error -> Signal in + if error.errorDescription == "SHORT_NAME_OCCUPIED" { + return .single(.taken) + } + return .single(.invalid) + } +} diff --git a/submodules/TelegramCore/Sources/LoadedStickerPack.swift b/submodules/TelegramCore/Sources/TelegramEngine/Stickers/LoadedStickerPack.swift similarity index 93% rename from submodules/TelegramCore/Sources/LoadedStickerPack.swift rename to submodules/TelegramCore/Sources/TelegramEngine/Stickers/LoadedStickerPack.swift index c793a70259..f3349c812e 100644 --- a/submodules/TelegramCore/Sources/LoadedStickerPack.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Stickers/LoadedStickerPack.swift @@ -47,7 +47,7 @@ func updatedRemoteStickerPack(postbox: Postbox, network: Network, reference: Sti case let .stickerSet(set, packs, documents): let namespace: ItemCollectionId.Namespace switch set { - case let .stickerSet(flags, _, _, _, _, _, _, _, _, _): + case let .stickerSet(flags, _, _, _, _, _, _, _, _, _, _): if (flags & (1 << 3)) != 0 { namespace = Namespaces.ItemCollection.CloudMaskPacks } else { @@ -95,8 +95,8 @@ func updatedRemoteStickerPack(postbox: Postbox, network: Network, reference: Sti } } -public func loadedStickerPack(postbox: Postbox, network: Network, reference: StickerPackReference, forceActualized: Bool) -> Signal { - return cachedStickerPack(postbox: postbox, network: network, reference: reference, forceRemote: forceActualized) +func _internal_loadedStickerPack(postbox: Postbox, network: Network, reference: StickerPackReference, forceActualized: Bool) -> Signal { + return _internal_cachedStickerPack(postbox: postbox, network: network, reference: reference, forceRemote: forceActualized) |> map { result -> LoadedStickerPack in switch result { case .none: diff --git a/submodules/TelegramCore/Sources/SearchStickers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Stickers/SearchStickers.swift similarity index 90% rename from submodules/TelegramCore/Sources/SearchStickers.swift rename to submodules/TelegramCore/Sources/TelegramEngine/Stickers/SearchStickers.swift index 4f2e712a6e..0f1464fbf8 100644 --- a/submodules/TelegramCore/Sources/SearchStickers.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Stickers/SearchStickers.swift @@ -58,19 +58,29 @@ public struct SearchStickersScope: OptionSet { public static let remote = SearchStickersScope(rawValue: 1 << 1) } -public func randomGreetingSticker(account: Account) -> Signal { - return account.postbox.transaction { transaction -> FoundStickerItem? in - var stickerItems: [FoundStickerItem] = [] - for entry in transaction.getOrderedListItems(collectionId: Namespaces.OrderedItemList.CloudGreetingStickers) { - if let item = entry.contents as? RecentMediaItem, let file = item.media as? TelegramMediaFile { - stickerItems.append(FoundStickerItem(file: file, stringRepresentations: [])) - } +func _internal_randomGreetingSticker(account: Account) -> Signal { + let key: PostboxViewKey = .orderedItemList(id: Namespaces.OrderedItemList.CloudGreetingStickers) + return account.postbox.combinedView(keys: [key]) + |> map { views -> [OrderedItemListEntry]? in + if let view = views.views[key] as? OrderedItemListView, !view.items.isEmpty { + return view.items + } else { + return nil } - return stickerItems.randomElement() + } + |> filter { items in + return items != nil + } + |> take(1) + |> map { items -> FoundStickerItem? in + if let randomItem = items?.randomElement(), let item = randomItem.contents as? RecentMediaItem, let file = item.media as? TelegramMediaFile { + return FoundStickerItem(file: file, stringRepresentations: []) + } + return nil } } -public func searchStickers(account: Account, query: String, scope: SearchStickersScope = [.installed, .remote]) -> Signal<[FoundStickerItem], NoError> { +func _internal_searchStickers(account: Account, query: String, scope: SearchStickersScope = [.installed, .remote]) -> Signal<[FoundStickerItem], NoError> { if scope.isEmpty { return .single([]) } @@ -265,7 +275,7 @@ public struct FoundStickerSets { } } -public func searchStickerSetsRemotely(network: Network, query: String) -> Signal { +func _internal_searchStickerSetsRemotely(network: Network, query: String) -> Signal { return network.request(Api.functions.messages.searchStickerSets(flags: 0, q: query, hash: 0)) |> mapError {_ in} |> mapToSignal { value in @@ -291,7 +301,7 @@ public func searchStickerSetsRemotely(network: Network, query: String) -> Signal } } -public func searchStickerSets(postbox: Postbox, query: String) -> Signal { +func _internal_searchStickerSets(postbox: Postbox, query: String) -> Signal { return postbox.transaction { transaction -> Signal in let infos = transaction.getItemCollectionsInfos(namespace: Namespaces.ItemCollection.CloudStickerPacks) @@ -324,19 +334,19 @@ public func searchStickerSets(postbox: Postbox, query: String) -> Signal switchToLatest } -public func searchGifs(account: Account, query: String, nextOffset: String = "") -> Signal { +func _internal_searchGifs(account: Account, query: String, nextOffset: String = "") -> Signal { return account.postbox.transaction { transaction -> String in let configuration = currentSearchBotsConfiguration(transaction: transaction) return configuration.gifBotUsername ?? "gif" } |> mapToSignal { - return resolvePeerByName(account: account, name: $0) + return _internal_resolvePeerByName(account: account, name: $0) } |> filter { $0 != nil } |> map { $0! } |> mapToSignal { peerId -> Signal in return account.postbox.loadedPeerWithId(peerId) } |> mapToSignal { peer -> Signal in - return requestChatContextResults(account: account, botId: peer.id, peerId: account.peerId, query: query, offset: nextOffset) + return _internal_requestChatContextResults(account: account, botId: peer.id, peerId: account.peerId, query: query, offset: nextOffset) |> map { results -> ChatContextResultCollection? in return results?.results } diff --git a/submodules/TelegramCore/Sources/StickerPack.swift b/submodules/TelegramCore/Sources/TelegramEngine/Stickers/StickerPack.swift similarity index 73% rename from submodules/TelegramCore/Sources/StickerPack.swift rename to submodules/TelegramCore/Sources/TelegramEngine/Stickers/StickerPack.swift index c885316fdf..61e247260f 100644 --- a/submodules/TelegramCore/Sources/StickerPack.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Stickers/StickerPack.swift @@ -5,29 +5,20 @@ import SwiftSignalKit import SyncCore import MtProtoKit -func telegramStickerPackThumbnailRepresentationFromApiSizes(datacenterId: Int32, sizes: [Api.PhotoSize]) -> (immediateThumbnail: Data?, representations: [TelegramMediaImageRepresentation]) { +func telegramStickerPackThumbnailRepresentationFromApiSizes(datacenterId: Int32, thumbVersion: Int32?, sizes: [Api.PhotoSize]) -> (immediateThumbnail: Data?, representations: [TelegramMediaImageRepresentation]) { var immediateThumbnailData: Data? var representations: [TelegramMediaImageRepresentation] = [] for size in sizes { switch size { - case let .photoCachedSize(_, location, w, h, _): - switch location { - case let .fileLocationToBeDeprecated(volumeId, localId): - let resource = CloudStickerPackThumbnailMediaResource(datacenterId: datacenterId, volumeId: volumeId, localId: localId) - representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: resource, progressiveSizes: [])) - } - case let .photoSize(_, location, w, h, _): - switch location { - case let .fileLocationToBeDeprecated(volumeId, localId): - let resource = CloudStickerPackThumbnailMediaResource(datacenterId: datacenterId, volumeId: volumeId, localId: localId) - representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: resource, progressiveSizes: [])) - } - case let .photoSizeProgressive(_, location, w, h, sizes): - switch location { - case let .fileLocationToBeDeprecated(volumeId, localId): - let resource = CloudStickerPackThumbnailMediaResource(datacenterId: datacenterId, volumeId: volumeId, localId: localId) - representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: resource, progressiveSizes: sizes)) - } + case let .photoCachedSize(_, w, h, _): + let resource = CloudStickerPackThumbnailMediaResource(datacenterId: datacenterId, thumbVersion: thumbVersion, volumeId: nil, localId: nil) + representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: resource, progressiveSizes: [], immediateThumbnailData: nil)) + case let .photoSize(_, w, h, _): + let resource = CloudStickerPackThumbnailMediaResource(datacenterId: datacenterId, thumbVersion: thumbVersion, volumeId: nil, localId: nil) + representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: resource, progressiveSizes: [], immediateThumbnailData: nil)) + case let .photoSizeProgressive(_, w, h, sizes): + let resource = CloudStickerPackThumbnailMediaResource(datacenterId: datacenterId, thumbVersion: thumbVersion, volumeId: nil, localId: nil) + representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: resource, progressiveSizes: sizes, immediateThumbnailData: nil)) case let .photoPathSize(_, data): immediateThumbnailData = data.makeData() case .photoStrippedSize: @@ -42,7 +33,7 @@ func telegramStickerPackThumbnailRepresentationFromApiSizes(datacenterId: Int32, extension StickerPackCollectionInfo { convenience init(apiSet: Api.StickerSet, namespace: ItemCollectionId.Namespace) { switch apiSet { - case let .stickerSet(flags, _, id, accessHash, title, shortName, thumbs, thumbDcId, count, nHash): + case let .stickerSet(flags, _, id, accessHash, title, shortName, thumbs, thumbDcId, thumbVersion, count, nHash): var setFlags: StickerPackCollectionInfoFlags = StickerPackCollectionInfoFlags() if (flags & (1 << 2)) != 0 { setFlags.insert(.isOfficial) @@ -57,7 +48,7 @@ extension StickerPackCollectionInfo { var thumbnailRepresentation: TelegramMediaImageRepresentation? var immediateThumbnailData: Data? if let thumbs = thumbs, let thumbDcId = thumbDcId { - let (data, representations) = telegramStickerPackThumbnailRepresentationFromApiSizes(datacenterId: thumbDcId, sizes: thumbs) + let (data, representations) = telegramStickerPackThumbnailRepresentationFromApiSizes(datacenterId: thumbDcId, thumbVersion: thumbVersion, sizes: thumbs) thumbnailRepresentation = representations.first immediateThumbnailData = data } @@ -67,7 +58,7 @@ extension StickerPackCollectionInfo { } } -public func stickerPacksAttachedToMedia(account: Account, media: AnyMediaReference) -> Signal<[StickerPackReference], NoError> { +func _internal_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) { @@ -88,7 +79,7 @@ public func stickerPacksAttachedToMedia(account: Account, media: AnyMediaReferen |> 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, _) = reference, let representation = largestImageRepresentation(imageReference.media.representations) { + if let imageReference = media.concrete(TelegramMediaImage.self), let reference = imageReference.media.reference, case let .cloud(imageId, accessHash, _) = reference, let _ = largestImageRepresentation(imageReference.media.representations) { inputMedia = .inputStickeredMediaPhoto(id: Api.InputPhoto.inputPhoto(id: imageId, accessHash: accessHash, fileReference: Buffer(data: updatedReference))) } 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))) diff --git a/submodules/TelegramCore/Sources/StickerPackInteractiveOperations.swift b/submodules/TelegramCore/Sources/TelegramEngine/Stickers/StickerPackInteractiveOperations.swift similarity index 84% rename from submodules/TelegramCore/Sources/StickerPackInteractiveOperations.swift rename to submodules/TelegramCore/Sources/TelegramEngine/Stickers/StickerPackInteractiveOperations.swift index d923975fe1..6ac55d9bed 100644 --- a/submodules/TelegramCore/Sources/StickerPackInteractiveOperations.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Stickers/StickerPackInteractiveOperations.swift @@ -4,7 +4,7 @@ import SwiftSignalKit import SyncCore -public func addStickerPackInteractively(postbox: Postbox, info: StickerPackCollectionInfo, items: [ItemCollectionItem], positionInList: Int? = nil) -> Signal { +func _internal_addStickerPackInteractively(postbox: Postbox, info: StickerPackCollectionInfo, items: [ItemCollectionItem], positionInList: Int? = nil) -> Signal { return postbox.transaction { transaction -> Void in let namespace: SynchronizeInstalledStickerPacksOperationNamespace? switch info.id.namespace { @@ -44,11 +44,11 @@ public enum RemoveStickerPackOption { case archive } -public func removeStickerPackInteractively(postbox: Postbox, id: ItemCollectionId, option: RemoveStickerPackOption) -> Signal<(Int, [ItemCollectionItem])?, NoError> { - return removeStickerPacksInteractively(postbox: postbox, ids: [id], option: option) +func _internal_removeStickerPackInteractively(postbox: Postbox, id: ItemCollectionId, option: RemoveStickerPackOption) -> Signal<(Int, [ItemCollectionItem])?, NoError> { + return _internal_removeStickerPacksInteractively(postbox: postbox, ids: [id], option: option) } -public func removeStickerPacksInteractively(postbox: Postbox, ids: [ItemCollectionId], option: RemoveStickerPackOption) -> Signal<(Int, [ItemCollectionItem])?, NoError> { +func _internal_removeStickerPacksInteractively(postbox: Postbox, ids: [ItemCollectionId], option: RemoveStickerPackOption) -> Signal<(Int, [ItemCollectionItem])?, NoError> { return postbox.transaction { transaction -> (Int, [ItemCollectionItem])? in var commonNamespace: SynchronizeInstalledStickerPacksOperationNamespace? for id in ids { @@ -80,7 +80,9 @@ public func removeStickerPacksInteractively(postbox: Postbox, ids: [ItemCollecti let items = transaction.getItemCollectionItems(collectionId: id) addSynchronizeInstalledStickerPacksOperation(transaction: transaction, namespace: namespace, content: content, noDelay: false) - transaction.removeItemCollection(collectionId: id) + for id in ids { + transaction.removeItemCollection(collectionId: id) + } return index.flatMap { ($0, items) } } else { return nil @@ -91,7 +93,7 @@ public func removeStickerPacksInteractively(postbox: Postbox, ids: [ItemCollecti } } -public func markFeaturedStickerPacksAsSeenInteractively(postbox: Postbox, ids: [ItemCollectionId]) -> Signal { +func _internal_markFeaturedStickerPacksAsSeenInteractively(postbox: Postbox, ids: [ItemCollectionId]) -> Signal { return postbox.transaction { transaction -> Void in let idsSet = Set(ids) var items = transaction.getOrderedListItems(collectionId: Namespaces.OrderedItemList.CloudFeaturedStickerPacks) diff --git a/submodules/TelegramCore/Sources/StickerSetInstallation.swift b/submodules/TelegramCore/Sources/TelegramEngine/Stickers/StickerSetInstallation.swift similarity index 95% rename from submodules/TelegramCore/Sources/StickerSetInstallation.swift rename to submodules/TelegramCore/Sources/TelegramEngine/Stickers/StickerSetInstallation.swift index 9c65ddb251..18318676ee 100644 --- a/submodules/TelegramCore/Sources/StickerSetInstallation.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Stickers/StickerSetInstallation.swift @@ -24,7 +24,7 @@ public enum RequestStickerSetResult { } } -public func requestStickerSet(postbox: Postbox, network: Network, reference: StickerPackReference) -> Signal { +func _internal_requestStickerSet(postbox: Postbox, network: Network, reference: StickerPackReference) -> Signal { let collectionId: ItemCollectionId? let input: Api.InputStickerSet @@ -133,8 +133,7 @@ public final class CoveredStickerSet : Equatable { } } -public func installStickerSetInteractively(account: Account, info: StickerPackCollectionInfo, items: [ItemCollectionItem]) -> Signal { - +func _internal_installStickerSetInteractively(account: Account, info: StickerPackCollectionInfo, items: [ItemCollectionItem]) -> Signal { return account.network.request(Api.functions.messages.installStickerSet(stickerset: .inputStickerSetID(id: info.id.id, accessHash: info.accessHash), archived: .boolFalse)) |> mapError { _ -> InstallStickerSetError in return .generic } |> mapToSignal { result -> Signal in @@ -199,7 +198,7 @@ public func installStickerSetInteractively(account: Account, info: StickerPackCo } -public func uninstallStickerSetInteractively(account: Account, info: StickerPackCollectionInfo) -> Signal { +func _internal_uninstallStickerSetInteractively(account: Account, info: StickerPackCollectionInfo) -> Signal { return account.network.request(Api.functions.messages.uninstallStickerSet(stickerset: .inputStickerSetID(id: info.id.id, accessHash: info.accessHash))) |> `catch` { _ -> Signal in return .single(.boolFalse) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Stickers/TelegramEngineStickers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Stickers/TelegramEngineStickers.swift new file mode 100644 index 0000000000..56cd62570d --- /dev/null +++ b/submodules/TelegramCore/Sources/TelegramEngine/Stickers/TelegramEngineStickers.swift @@ -0,0 +1,100 @@ +import SwiftSignalKit +import SyncCore +import Postbox + +public extension TelegramEngine { + final class Stickers { + private let account: Account + + init(account: Account) { + self.account = account + } + + public func archivedStickerPacks(namespace: ArchivedStickerPacksNamespace = .stickers) -> Signal<[ArchivedStickerPackItem], NoError> { + return _internal_archivedStickerPacks(account: account, namespace: namespace) + } + + public func removeArchivedStickerPack(info: StickerPackCollectionInfo) -> Signal { + return _internal_removeArchivedStickerPack(account: self.account, info: info) + } + + public func cachedStickerPack(reference: StickerPackReference, forceRemote: Bool) -> Signal { + return _internal_cachedStickerPack(postbox: self.account.postbox, network: self.account.network, reference: reference, forceRemote: forceRemote) + } + + public func loadedStickerPack(reference: StickerPackReference, forceActualized: Bool) -> Signal { + return _internal_loadedStickerPack(postbox: self.account.postbox, network: self.account.network, reference: reference, forceActualized: forceActualized) + } + + public func randomGreetingSticker() -> Signal { + return _internal_randomGreetingSticker(account: self.account) + } + + public func searchStickers(query: String, scope: SearchStickersScope = [.installed, .remote]) -> Signal<[FoundStickerItem], NoError> { + return _internal_searchStickers(account: self.account, query: query, scope: scope) + } + + public func searchStickerSetsRemotely(query: String) -> Signal { + return _internal_searchStickerSetsRemotely(network: self.account.network, query: query) + } + + public func searchStickerSets(query: String) -> Signal { + return _internal_searchStickerSets(postbox: self.account.postbox, query: query) + } + + public func searchGifs(query: String, nextOffset: String = "") -> Signal { + return _internal_searchGifs(account: self.account, query: query, nextOffset: nextOffset) + } + + public func addStickerPackInteractively(info: StickerPackCollectionInfo, items: [ItemCollectionItem], positionInList: Int? = nil) -> Signal { + return _internal_addStickerPackInteractively(postbox: self.account.postbox, info: info, items: items, positionInList: positionInList) + } + + public func removeStickerPackInteractively(id: ItemCollectionId, option: RemoveStickerPackOption) -> Signal<(Int, [ItemCollectionItem])?, NoError> { + return _internal_removeStickerPackInteractively(postbox: self.account.postbox, id: id, option: option) + } + + public func removeStickerPacksInteractively(ids: [ItemCollectionId], option: RemoveStickerPackOption) -> Signal<(Int, [ItemCollectionItem])?, NoError> { + return _internal_removeStickerPacksInteractively(postbox: self.account.postbox, ids: ids, option: option) + } + + public func markFeaturedStickerPacksAsSeenInteractively(ids: [ItemCollectionId]) -> Signal { + return _internal_markFeaturedStickerPacksAsSeenInteractively(postbox: self.account.postbox, ids: ids) + } + + public func searchEmojiKeywords(inputLanguageCode: String, query: String, completeMatch: Bool) -> Signal<[EmojiKeywordItem], NoError> { + return _internal_searchEmojiKeywords(postbox: self.account.postbox, inputLanguageCode: inputLanguageCode, query: query, completeMatch: completeMatch) + } + + public func stickerPacksAttachedToMedia(media: AnyMediaReference) -> Signal<[StickerPackReference], NoError> { + return _internal_stickerPacksAttachedToMedia(account: self.account, media: media) + } + + public func uploadSticker(peer: Peer, resource: MediaResource, alt: String, dimensions: PixelDimensions, isAnimated: Bool) -> Signal { + return _internal_uploadSticker(account: self.account, peer: peer, resource: resource, alt: alt, dimensions: dimensions, isAnimated: isAnimated) + } + + public func createStickerSet(title: String, shortName: String, stickers: [ImportSticker], thumbnail: ImportSticker?, isAnimated: Bool, software: String?) -> Signal { + return _internal_createStickerSet(account: self.account, title: title, shortName: shortName, stickers: stickers, thumbnail: thumbnail, isAnimated: isAnimated, software: software) + } + + public func getStickerSetShortNameSuggestion(title: String) -> Signal { + return _internal_getStickerSetShortNameSuggestion(account: self.account, title: title) + } + + public func validateStickerSetShortNameInteractive(shortName: String) -> Signal { + if let error = _internal_checkAddressNameFormat(shortName) { + return .single(.invalidFormat(error)) + } else { + return .single(.checking) + |> then( + _internal_stickerSetShortNameAvailability(account: self.account, shortName: shortName) + |> delay(0.3, queue: Queue.concurrentDefaultQueue()) + |> map { result -> AddressNameValidationStatus in + .availability(result) + } + ) + } + } + } +} diff --git a/submodules/TelegramCore/Sources/TelegramEngine/TelegramEngine.swift b/submodules/TelegramCore/Sources/TelegramEngine/TelegramEngine.swift new file mode 100644 index 0000000000..d4902923e1 --- /dev/null +++ b/submodules/TelegramCore/Sources/TelegramEngine/TelegramEngine.swift @@ -0,0 +1,91 @@ +import SwiftSignalKit +import Postbox + +public final class TelegramEngine { + public let account: Account + + public init(account: Account) { + self.account = account + } + + public lazy var secureId: SecureId = { + return SecureId(account: self.account) + }() + + public lazy var peersNearby: PeersNearby = { + return PeersNearby(account: self.account) + }() + + public lazy var payments: Payments = { + return Payments(account: self.account) + }() + + public lazy var peers: Peers = { + return Peers(account: self.account) + }() + + public lazy var auth: Auth = { + return Auth(account: self.account) + }() + + public lazy var accountData: AccountData = { + return AccountData(account: self.account) + }() + + public lazy var stickers: Stickers = { + return Stickers(account: self.account) + }() + + public lazy var localization: Localization = { + return Localization(account: self.account) + }() + + public lazy var messages: Messages = { + return Messages(account: self.account) + }() + + public lazy var privacy: Privacy = { + return Privacy(account: self.account) + }() + + public lazy var calls: Calls = { + return Calls(account: self.account) + }() + + public lazy var historyImport: HistoryImport = { + return HistoryImport(account: self.account) + }() + + public lazy var contacts: Contacts = { + return Contacts(account: self.account) + }() + + public lazy var resources: Resources = { + return Resources(account: self.account) + }() + + public lazy var resolve: Resolve = { + return Resolve(account: self.account) + }() +} + +public final class TelegramEngineUnauthorized { + public let account: UnauthorizedAccount + + public init(account: UnauthorizedAccount) { + self.account = account + } + + public lazy var auth: Auth = { + return Auth(account: self.account) + }() + + public lazy var localization: Localization = { + return Localization(account: self.account) + }() +} + +public enum SomeTelegramEngine { + case unauthorized(TelegramEngineUnauthorized) + case authorized(TelegramEngine) +} diff --git a/submodules/TelegramCore/Sources/TelegramMediaImage.swift b/submodules/TelegramCore/Sources/TelegramMediaImage.swift deleted file mode 100644 index 5dca93ac69..0000000000 --- a/submodules/TelegramCore/Sources/TelegramMediaImage.swift +++ /dev/null @@ -1,73 +0,0 @@ -import Foundation -import Postbox -import TelegramApi - -import SyncCore - -func telegramMediaImageRepresentationsFromApiSizes(datacenterId: Int32, photoId: Int64, accessHash: Int64, fileReference: Data?, sizes: [Api.PhotoSize]) -> (immediateThumbnail: Data?, representations: [TelegramMediaImageRepresentation]) { - var immediateThumbnailData: Data? - var representations: [TelegramMediaImageRepresentation] = [] - for size in sizes { - switch size { - case let .photoCachedSize(type, location, w, h, _): - switch location { - case let .fileLocationToBeDeprecated(volumeId, localId): - let resource = CloudPhotoSizeMediaResource(datacenterId: datacenterId, photoId: photoId, accessHash: accessHash, sizeSpec: type, volumeId: volumeId, localId: localId, size: nil, fileReference: fileReference) - representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: resource, progressiveSizes: [])) - } - case let .photoSize(type, location, w, h, size): - switch location { - case let .fileLocationToBeDeprecated(volumeId, localId): - let resource = CloudPhotoSizeMediaResource(datacenterId: datacenterId, photoId: photoId, accessHash: accessHash, sizeSpec: type, volumeId: volumeId, localId: localId, size: Int(size), fileReference: fileReference) - representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: resource, progressiveSizes: [])) - } - case let .photoSizeProgressive(type, location, w, h, sizes): - switch location { - case let .fileLocationToBeDeprecated(volumeId, localId): - if !sizes.isEmpty { - let resource = CloudPhotoSizeMediaResource(datacenterId: datacenterId, photoId: photoId, accessHash: accessHash, sizeSpec: type, volumeId: volumeId, localId: localId, size: Int(sizes[sizes.count - 1]), fileReference: fileReference) - representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: resource, progressiveSizes: sizes)) - } - } - case let .photoStrippedSize(_, data): - immediateThumbnailData = data.makeData() - case .photoPathSize: - break - case .photoSizeEmpty: - break - } - } - return (immediateThumbnailData, representations) -} - -func telegramMediaImageFromApiPhoto(_ photo: Api.Photo) -> TelegramMediaImage? { - switch photo { - case let .photo(flags, id, accessHash, fileReference, _, sizes, videoSizes, dcId): - let (immediateThumbnailData, representations) = telegramMediaImageRepresentationsFromApiSizes(datacenterId: dcId, photoId: id, accessHash: accessHash, fileReference: fileReference.makeData(), sizes: sizes) - var imageFlags: TelegramMediaImageFlags = [] - let hasStickers = (flags & (1 << 0)) != 0 - if hasStickers { - imageFlags.insert(.hasStickers) - } - - var videoRepresentations: [TelegramMediaImage.VideoRepresentation] = [] - if let videoSizes = videoSizes { - for size in videoSizes { - switch size { - case let .videoSize(_, type, location, w, h, size, videoStartTs): - let resource: TelegramMediaResource - switch location { - case let .fileLocationToBeDeprecated(volumeId, localId): - resource = CloudPhotoSizeMediaResource(datacenterId: dcId, photoId: id, accessHash: accessHash, sizeSpec: type, volumeId: volumeId, localId: localId, size: Int(size), fileReference: fileReference.makeData()) - } - - videoRepresentations.append(TelegramMediaImage.VideoRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: resource, startTimestamp: videoStartTs)) - } - } - } - - return TelegramMediaImage(imageId: MediaId(namespace: Namespaces.Media.CloudImage, id: id), representations: representations, videoRepresentations: videoRepresentations, 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/Unixtime.swift b/submodules/TelegramCore/Sources/Unixtime.swift deleted file mode 100644 index 090a84723b..0000000000 --- a/submodules/TelegramCore/Sources/Unixtime.swift +++ /dev/null @@ -1,126 +0,0 @@ -import Foundation - -public struct DateTime { - public let seconds: Int32 // 0 ... 59 - public let minutes: Int32 // 0 ... 59 - public let hours: Int32 // 0 ... 23 - public let dayOfMonth: Int32 // 1 ... 31 - public let month: Int32 // 0 ... 11 - public let year: Int32 // since 1900 - public let dayOfWeek: Int32 // 0 ... 6 - public let dayOfYear: Int32 // 0 ... 365 -} - -private let daysSinceJan1st: [[UInt32]] = -[ - [0,31,59,90,120,151,181,212,243,273,304,334,365], // 365 days, non-leap - [0,31,60,91,121,152,182,213,244,274,305,335,366] // 366 days, leap -] - -public func secondsSinceEpochToDateTime(_ secondsSinceEpoch: Int64) -> DateTime { - var sec: UInt64 - let quadricentennials: UInt32 - var centennials: UInt32 - var quadrennials: UInt32 - var annuals: UInt32 - let year: UInt32 - let leap: UInt32 - let yday: UInt32 - let hour: UInt32 - let min: UInt32 - var month: UInt32 - var mday: UInt32 - let wday: UInt32 - - /* - 400 years: - - 1st hundred, starting immediately after a leap year that's a multiple of 400: - n n n l \ - n n n l } 24 times - ... / - n n n l / - n n n n - - 2nd hundred: - n n n l \ - n n n l } 24 times - ... / - n n n l / - n n n n - - 3rd hundred: - n n n l \ - n n n l } 24 times - ... / - n n n l / - n n n n - - 4th hundred: - n n n l \ - n n n l } 24 times - ... / - n n n l / - n n n L <- 97'th leap year every 400 years - */ - - // Re-bias from 1970 to 1601: - // 1970 - 1601 = 369 = 3*100 + 17*4 + 1 years (incl. 89 leap days) = - // (3*100*(365+24/100) + 17*4*(365+1/4) + 1*365)*24*3600 seconds - sec = UInt64(secondsSinceEpoch) + (11644473600 as UInt64) - - wday = (uint)((sec / 86400 + 1) % 7); // day of week - - // Remove multiples of 400 years (incl. 97 leap days) - quadricentennials = UInt32((UInt64(sec) / (12622780800 as UInt64))) // 400*365.2425*24*3600 - sec %= 12622780800 as UInt64 - - // Remove multiples of 100 years (incl. 24 leap days), can't be more than 3 - // (because multiples of 4*100=400 years (incl. leap days) have been removed) - centennials = UInt32(UInt64(sec) / (3155673600 as UInt64)) // 100*(365+24/100)*24*3600 - if centennials > 3 { - centennials = 3 - } - sec -= UInt64(centennials) * (3155673600 as UInt64) - - // Remove multiples of 4 years (incl. 1 leap day), can't be more than 24 - // (because multiples of 25*4=100 years (incl. leap days) have been removed) - quadrennials = UInt32((UInt64(sec) / (126230400 as UInt64))) // 4*(365+1/4)*24*3600 - if quadrennials > 24 { - quadrennials = 24 - } - sec -= UInt64(quadrennials) * (126230400 as UInt64) - - // Remove multiples of years (incl. 0 leap days), can't be more than 3 - // (because multiples of 4 years (incl. leap days) have been removed) - annuals = UInt32(sec / (31536000 as UInt64)) // 365*24*3600 - if annuals > 3 { - annuals = 3 - } - sec -= UInt64(annuals) * (31536000 as UInt64) - - // Calculate the year and find out if it's leap - year = 1601 + quadricentennials * 400 + centennials * 100 + quadrennials * 4 + annuals; - leap = (!(year % UInt32(4) != 0) && ((year % UInt32(100) != 0) || !(year % UInt32(400) != 0))) ? 1 : 0 - - // Calculate the day of the year and the time - yday = UInt32(sec / (86400 as UInt64)) - sec %= 86400; - hour = UInt32(sec / 3600); - sec %= 3600; - min = UInt32(sec / 60); - sec %= 60; - - mday = 1 - month = 1 - while month < 13 { - if (yday < daysSinceJan1st[Int(leap)][Int(month)]) { - mday += yday - daysSinceJan1st[Int(leap)][Int(month - 1)] - break - } - - month += 1 - } - - return DateTime(seconds: Int32(sec), minutes: Int32(min), hours: Int32(hour), dayOfMonth: Int32(mday), month: Int32(month - 1), year: Int32(year - 1900), dayOfWeek: Int32(wday), dayOfYear: Int32(yday)) -} diff --git a/submodules/TelegramCore/Sources/CanSendMessagesToPeer.swift b/submodules/TelegramCore/Sources/Utils/CanSendMessagesToPeer.swift similarity index 100% rename from submodules/TelegramCore/Sources/CanSendMessagesToPeer.swift rename to submodules/TelegramCore/Sources/Utils/CanSendMessagesToPeer.swift diff --git a/submodules/TelegramCore/Sources/DecryptedResourceData.swift b/submodules/TelegramCore/Sources/Utils/DecryptedResourceData.swift similarity index 100% rename from submodules/TelegramCore/Sources/DecryptedResourceData.swift rename to submodules/TelegramCore/Sources/Utils/DecryptedResourceData.swift diff --git a/submodules/TelegramCore/Sources/ImageRepresentationsUtils.swift b/submodules/TelegramCore/Sources/Utils/ImageRepresentationsUtils.swift similarity index 100% rename from submodules/TelegramCore/Sources/ImageRepresentationsUtils.swift rename to submodules/TelegramCore/Sources/Utils/ImageRepresentationsUtils.swift diff --git a/submodules/TelegramCore/Sources/JSON.swift b/submodules/TelegramCore/Sources/Utils/JSON.swift similarity index 100% rename from submodules/TelegramCore/Sources/JSON.swift rename to submodules/TelegramCore/Sources/Utils/JSON.swift diff --git a/submodules/TelegramCore/Sources/Log.swift b/submodules/TelegramCore/Sources/Utils/Log.swift similarity index 100% rename from submodules/TelegramCore/Sources/Log.swift rename to submodules/TelegramCore/Sources/Utils/Log.swift diff --git a/submodules/TelegramCore/Sources/MD5.swift b/submodules/TelegramCore/Sources/Utils/MD5.swift similarity index 100% rename from submodules/TelegramCore/Sources/MD5.swift rename to submodules/TelegramCore/Sources/Utils/MD5.swift diff --git a/submodules/TelegramCore/Sources/MediaResourceNetworkStatsTag.swift b/submodules/TelegramCore/Sources/Utils/MediaResourceNetworkStatsTag.swift similarity index 81% rename from submodules/TelegramCore/Sources/MediaResourceNetworkStatsTag.swift rename to submodules/TelegramCore/Sources/Utils/MediaResourceNetworkStatsTag.swift index 8518603109..8d008623d3 100644 --- a/submodules/TelegramCore/Sources/MediaResourceNetworkStatsTag.swift +++ b/submodules/TelegramCore/Sources/Utils/MediaResourceNetworkStatsTag.swift @@ -9,7 +9,7 @@ public enum MediaResourceStatsCategory { case call } -public final class TelegramMediaResourceFetchTag: MediaResourceFetchTag { +final class TelegramMediaResourceFetchTag: MediaResourceFetchTag { public let statsCategory: MediaResourceStatsCategory public init(statsCategory: MediaResourceStatsCategory) { diff --git a/submodules/TelegramCore/Sources/MemoryBufferExtensions.swift b/submodules/TelegramCore/Sources/Utils/MemoryBufferExtensions.swift similarity index 100% rename from submodules/TelegramCore/Sources/MemoryBufferExtensions.swift rename to submodules/TelegramCore/Sources/Utils/MemoryBufferExtensions.swift diff --git a/submodules/TelegramCore/Sources/MessageUtils.swift b/submodules/TelegramCore/Sources/Utils/MessageUtils.swift similarity index 91% rename from submodules/TelegramCore/Sources/MessageUtils.swift rename to submodules/TelegramCore/Sources/Utils/MessageUtils.swift index b771a11815..4ac9fb61f0 100644 --- a/submodules/TelegramCore/Sources/MessageUtils.swift +++ b/submodules/TelegramCore/Sources/Utils/MessageUtils.swift @@ -25,6 +25,22 @@ public extension Message { } return nil } + + var visibleReplyMarkupPlaceholder: String? { + for attribute in self.attributes { + if let attribute = attribute as? ReplyMarkupMessageAttribute { + if !attribute.flags.contains(.inline) { + if attribute.flags.contains(.personal) { + if !personal { + return nil + } + } + return attribute.placeholder + } + } + } + return nil + } var muted: Bool { for attribute in self.attributes { @@ -184,10 +200,15 @@ func locallyRenderedMessage(message: StoreMessage, peers: [PeerId: Peer]) -> Mes messagePeers[source.id] = source } } + + var hasher = Hasher() + hasher.combine(id.id) + hasher.combine(id.peerId) - var hash: Int32 = id.id - hash = hash &* 31 &+ id.peerId.id - let stableId = UInt32(clamping: hash) + let hashValue = Int64(hasher.finalize()) + let first = UInt32((hashValue >> 32) & 0xffffffff) + let second = UInt32(hashValue & 0xffffffff) + let stableId = first &+ second return Message(stableId: stableId, stableVersion: 0, id: id, globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: message.threadId, timestamp: message.timestamp, flags: MessageFlags(message.flags), tags: message.tags, globalTags: message.globalTags, localTags: message.localTags, forwardInfo: forwardInfo, author: author, text: message.text, attributes: message.attributes, media: message.media, peers: messagePeers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) } diff --git a/submodules/TelegramCore/Sources/PeerUtils.swift b/submodules/TelegramCore/Sources/Utils/PeerUtils.swift similarity index 96% rename from submodules/TelegramCore/Sources/PeerUtils.swift rename to submodules/TelegramCore/Sources/Utils/PeerUtils.swift index 9bee3aa061..c39e549776 100644 --- a/submodules/TelegramCore/Sources/PeerUtils.swift +++ b/submodules/TelegramCore/Sources/Utils/PeerUtils.swift @@ -241,7 +241,7 @@ public func isServicePeer(_ peer: Peer) -> Bool { if peer.id.isReplies { return true } - return (peer.id.namespace == Namespaces.Peer.CloudUser && (peer.id.id == 777000 || peer.id.id == 333000)) + return (peer.id.namespace == Namespaces.Peer.CloudUser && (peer.id.id._internalGetInt32Value() == 777000 || peer.id.id._internalGetInt32Value() == 333000)) } return false } @@ -249,7 +249,7 @@ public func isServicePeer(_ peer: Peer) -> Bool { public extension PeerId { var isReplies: Bool { if self.namespace == Namespaces.Peer.CloudUser { - if self.id == 708513 || self.id == 1271266957 { + if self.id._internalGetInt32Value() == 708513 || self.id._internalGetInt32Value() == 1271266957 { return true } } @@ -268,7 +268,7 @@ public extension PeerId { var isImport: Bool { if self.namespace == Namespaces.Peer.CloudUser { - if self.id == 225079 { + if self.id._internalGetInt32Value() == 225079 { return true } } diff --git a/submodules/TelegramCore/Sources/Utils/StringFormat.swift b/submodules/TelegramCore/Sources/Utils/StringFormat.swift new file mode 100644 index 0000000000..9d92b02743 --- /dev/null +++ b/submodules/TelegramCore/Sources/Utils/StringFormat.swift @@ -0,0 +1,52 @@ +import Foundation + +// Incuding at least one Objective-C class in a swift file ensures that it doesn't get stripped by the linker +private final class LinkHelperClass: NSObject { +} + +public func dataSizeString(_ size: Int, forceDecimal: Bool = false, formatting: DataSizeStringFormatting) -> String { + return dataSizeString(Int64(size), forceDecimal: forceDecimal, formatting: formatting) +} + +public struct DataSizeStringFormatting { + let decimalSeparator: String + let byte: (String) -> (String, [(Int, NSRange)]) + let kilobyte: (String) -> (String, [(Int, NSRange)]) + let megabyte: (String) -> (String, [(Int, NSRange)]) + let gigabyte: (String) -> (String, [(Int, NSRange)]) + + public init(decimalSeparator: String, byte: @escaping (String) -> (String, [(Int, NSRange)]), kilobyte: @escaping (String) -> (String, [(Int, NSRange)]), megabyte: @escaping (String) -> (String, [(Int, NSRange)]), gigabyte: @escaping (String) -> (String, [(Int, NSRange)])) { + self.decimalSeparator = decimalSeparator + self.byte = byte + self.kilobyte = kilobyte + self.megabyte = megabyte + self.gigabyte = gigabyte + } +} + +public func dataSizeString(_ size: Int64, forceDecimal: Bool = false, formatting: DataSizeStringFormatting) -> String { + if size >= 1024 * 1024 * 1024 { + let remainder = Int64((Double(size % (1024 * 1024 * 1024)) / (1024 * 1024 * 102.4)).rounded(.down)) + if remainder != 0 || forceDecimal { + return formatting.gigabyte("\(size / (1024 * 1024 * 1024))\(formatting.decimalSeparator)\(remainder)").0 + } else { + return formatting.gigabyte("\(size / (1024 * 1024 * 1024))").0 + } + } else if size >= 1024 * 1024 { + let remainder = Int64((Double(size % (1024 * 1024)) / (1024.0 * 102.4)).rounded(.down)) + if remainder != 0 || forceDecimal { + return formatting.megabyte( "\(size / (1024 * 1024))\(formatting.decimalSeparator)\(remainder)").0 + } else { + return formatting.megabyte("\(size / (1024 * 1024))").0 + } + } else if size >= 1024 { + let remainder = (size % (1024)) / (102) + if remainder != 0 || forceDecimal { + return formatting.kilobyte("\(size / 1024)\(formatting.decimalSeparator)\(remainder)").0 + } else { + return formatting.kilobyte("\(size / 1024)").0 + } + } else { + return formatting.byte("\(size)").0 + } +} diff --git a/submodules/TelegramCore/Sources/UpdateMessageMedia.swift b/submodules/TelegramCore/Sources/Utils/UpdateMessageMedia.swift similarity index 100% rename from submodules/TelegramCore/Sources/UpdateMessageMedia.swift rename to submodules/TelegramCore/Sources/Utils/UpdateMessageMedia.swift diff --git a/submodules/TelegramCore/Sources/ValidateAddressNameInteractive.swift b/submodules/TelegramCore/Sources/ValidateAddressNameInteractive.swift deleted file mode 100644 index 2b45f63e2e..0000000000 --- a/submodules/TelegramCore/Sources/ValidateAddressNameInteractive.swift +++ /dev/null @@ -1,24 +0,0 @@ -import Foundation -import Postbox -import SwiftSignalKit - -public enum AddressNameValidationStatus: Equatable { - case checking - case invalidFormat(AddressNameFormatError) - case availability(AddressNameAvailability) -} - -public func validateAddressNameInteractive(account: Account, domain: AddressNameDomain, name: String) -> Signal { - if let error = checkAddressNameFormat(name) { - return .single(.invalidFormat(error)) - } else { - return .single(.checking) - |> then( - addressNameAvailability(account: account, domain: domain, name: name) - |> delay(0.3, queue: Queue.concurrentDefaultQueue()) - |> map { result -> AddressNameValidationStatus in - .availability(result) - } - ) - } -} diff --git a/submodules/TelegramCore/Sources/Wallpaper.swift b/submodules/TelegramCore/Sources/Wallpaper.swift deleted file mode 100644 index 4d41fa0368..0000000000 --- a/submodules/TelegramCore/Sources/Wallpaper.swift +++ /dev/null @@ -1,83 +0,0 @@ -import Foundation -import Postbox -import SwiftSignalKit -import TelegramApi - -import SyncCore - -extension WallpaperSettings { - init(apiWallpaperSettings: Api.WallPaperSettings) { - switch apiWallpaperSettings { - 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) - } - 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 { - init(apiWallpaper: Api.WallPaper) { - switch apiWallpaper { - case let .wallPaper(id, flags, accessHash, slug, document, settings): - if let file = telegramMediaFileFromApiDocument(document) { - let wallpaperSettings: WallpaperSettings - if let settings = settings { - wallpaperSettings = WallpaperSettings(apiWallpaperSettings: settings) - } else { - wallpaperSettings = WallpaperSettings() - } - 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() - 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 4557a74894..59b32a0346 100644 --- a/submodules/TelegramCore/Sources/Wallpapers.swift +++ b/submodules/TelegramCore/Sources/Wallpapers.swift @@ -7,7 +7,7 @@ import SyncCore public func telegramWallpapers(postbox: Postbox, network: Network, forceUpdate: Bool = false) -> Signal<[TelegramWallpaper], NoError> { let fetch: ([TelegramWallpaper]?, Int32?) -> Signal<[TelegramWallpaper], NoError> = { current, hash in - network.request(Api.functions.account.getWallPapers(hash: hash ?? 0)) + network.request(Api.functions.account.getWallPapers(hash: 0)) |> retryRequest |> mapToSignal { result -> Signal<([TelegramWallpaper], Int32), NoError> in switch result { @@ -162,10 +162,16 @@ private func saveUnsaveWallpaper(account: Account, wallpaper: TelegramWallpaper, } public func installWallpaper(account: Account, wallpaper: TelegramWallpaper) -> Signal { - guard case let .file(_, _, _, _, _, _, slug, _, settings) = wallpaper else { + guard case let .file(id, accessHash, _, _, _, _, slug, _, settings) = wallpaper else { return .complete() } - return account.network.request(Api.functions.account.installWallPaper(wallpaper: Api.InputWallPaper.inputWallPaperSlug(slug: slug), settings: apiWallpaperSettings(settings))) + let inputWallpaper: Api.InputWallPaper + if id != 0 && accessHash != 0 { + inputWallpaper = .inputWallPaper(id: id, accessHash: accessHash) + } else { + inputWallpaper = .inputWallPaperSlug(slug: slug) + } + return account.network.request(Api.functions.account.installWallPaper(wallpaper: inputWallpaper, settings: apiWallpaperSettings(settings))) |> `catch` { _ -> Signal in return .complete() } diff --git a/submodules/TelegramCore/Sources/module.private.modulemap b/submodules/TelegramCore/Sources/module.private.modulemap deleted file mode 100644 index 06df6c843a..0000000000 --- a/submodules/TelegramCore/Sources/module.private.modulemap +++ /dev/null @@ -1,3 +0,0 @@ -module TelegramCore.TelegramCorePrivate { - export * -} diff --git a/submodules/TelegramPermissionsUI/Sources/PermissionContentNode.swift b/submodules/TelegramPermissionsUI/Sources/PermissionContentNode.swift index 6c53c5df9b..7d3a07c73f 100644 --- a/submodules/TelegramPermissionsUI/Sources/PermissionContentNode.swift +++ b/submodules/TelegramPermissionsUI/Sources/PermissionContentNode.swift @@ -233,7 +233,7 @@ public final class PermissionContentNode: ASDisplayNode { } if let _ = self.animationNode, size.width < size.height { imageSpacing = floor(availableHeight * 0.12) - imageSize = CGSize(width: 200.0, height: 200.0) + imageSize = CGSize(width: 240.0, height: 240.0) contentHeight += imageSize.height + imageSpacing } diff --git a/submodules/TelegramPermissionsUI/Sources/PermissionController.swift b/submodules/TelegramPermissionsUI/Sources/PermissionController.swift index 9639657c4c..4bd56eb471 100644 --- a/submodules/TelegramPermissionsUI/Sources/PermissionController.swift +++ b/submodules/TelegramPermissionsUI/Sources/PermissionController.swift @@ -39,7 +39,7 @@ public final class PermissionController: ViewController { let navigationBarPresentationData: NavigationBarPresentationData if splashScreen { - navigationBarPresentationData = NavigationBarPresentationData(theme: NavigationBarTheme(buttonColor: self.presentationData.theme.rootController.navigationBar.accentTextColor, disabledButtonColor: self.presentationData.theme.rootController.navigationBar.disabledButtonColor, primaryTextColor: self.presentationData.theme.rootController.navigationBar.primaryTextColor, backgroundColor: .clear, separatorColor: .clear, badgeBackgroundColor: .clear, badgeStrokeColor: .clear, badgeTextColor: .clear), strings: NavigationBarStrings(presentationStrings: self.presentationData.strings)) + navigationBarPresentationData = NavigationBarPresentationData(theme: NavigationBarTheme(buttonColor: self.presentationData.theme.rootController.navigationBar.accentTextColor, disabledButtonColor: self.presentationData.theme.rootController.navigationBar.disabledButtonColor, primaryTextColor: self.presentationData.theme.rootController.navigationBar.primaryTextColor, backgroundColor: .clear, enableBackgroundBlur: false, separatorColor: .clear, badgeBackgroundColor: .clear, badgeStrokeColor: .clear, badgeTextColor: .clear), strings: NavigationBarStrings(presentationStrings: self.presentationData.strings)) } else { navigationBarPresentationData = NavigationBarPresentationData(presentationData: self.presentationData) } @@ -89,7 +89,7 @@ public final class PermissionController: ViewController { let navigationBarPresentationData: NavigationBarPresentationData if self.splashScreen { - navigationBarPresentationData = NavigationBarPresentationData(theme: NavigationBarTheme(buttonColor: self.presentationData.theme.rootController.navigationBar.accentTextColor, disabledButtonColor: self.presentationData.theme.rootController.navigationBar.disabledButtonColor, primaryTextColor: self.presentationData.theme.rootController.navigationBar.primaryTextColor, backgroundColor: .clear, separatorColor: .clear, badgeBackgroundColor: .clear, badgeStrokeColor: .clear, badgeTextColor: .clear), strings: NavigationBarStrings(presentationStrings: self.presentationData.strings)) + navigationBarPresentationData = NavigationBarPresentationData(theme: NavigationBarTheme(buttonColor: self.presentationData.theme.rootController.navigationBar.accentTextColor, disabledButtonColor: self.presentationData.theme.rootController.navigationBar.disabledButtonColor, primaryTextColor: self.presentationData.theme.rootController.navigationBar.primaryTextColor, backgroundColor: .clear, enableBackgroundBlur: false, separatorColor: .clear, badgeBackgroundColor: .clear, badgeStrokeColor: .clear, badgeTextColor: .clear), strings: NavigationBarStrings(presentationStrings: self.presentationData.strings)) } else { navigationBarPresentationData = NavigationBarPresentationData(presentationData: self.presentationData) } @@ -253,7 +253,7 @@ public final class PermissionController: ViewController { self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(self.cancelPressed)) } - self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.splashScreen ? 0.0 : self.navigationHeight, transition: transition) + self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.splashScreen ? 0.0 : self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition) } @objc private func nextPressed() { diff --git a/submodules/TelegramPresentationData/BUILD b/submodules/TelegramPresentationData/BUILD index 741b28c6b7..424efdb88a 100644 --- a/submodules/TelegramPresentationData/BUILD +++ b/submodules/TelegramPresentationData/BUILD @@ -17,6 +17,7 @@ swift_library( "//submodules/AppBundle:AppBundle", "//submodules/StringPluralization:StringPluralization", "//submodules/Sunrise:Sunrise", + "//submodules/TinyThumbnail:TinyThumbnail", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramPresentationData/Sources/ChatControllerBackgroundNode.swift b/submodules/TelegramPresentationData/Sources/ChatControllerBackgroundNode.swift index f72ccd56f1..04a846ed1e 100644 --- a/submodules/TelegramPresentationData/Sources/ChatControllerBackgroundNode.swift +++ b/submodules/TelegramPresentationData/Sources/ChatControllerBackgroundNode.swift @@ -8,6 +8,7 @@ import SwiftSignalKit import Postbox import MediaResources import AppBundle +import TinyThumbnail private var backgroundImageForWallpaper: (TelegramWallpaper, Bool, UIImage)? @@ -42,9 +43,9 @@ public func chatControllerBackgroundImage(theme: PresentationTheme?, wallpaper i context.setFillColor(UIColor(argb: color).withAlphaComponent(1.0).cgColor) context.fill(CGRect(origin: CGPoint(), size: size)) }) - case let .gradient(topColor, bottomColor, settings): + case let .gradient(_, colors, 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 + let gradientColors = [UIColor(argb: colors.count >= 1 ? colors[0] : 0).cgColor, UIColor(argb: colors.count >= 2 ? colors[1] : 0).cgColor] as CFArray var locations: [CGFloat] = [0.0, 1.0] let colorSpace = CGColorSpaceCreateDeviceRGB() @@ -73,14 +74,8 @@ public func chatControllerBackgroundImage(theme: PresentationTheme?, wallpaper i } } case let .file(file): - 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, 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() - } - }) - backgroundImage = image + if wallpaper.isPattern { + backgroundImage = nil } else { if file.settings.blur && composed { var image: UIImage? @@ -107,7 +102,6 @@ public func chatControllerBackgroundImage(theme: PresentationTheme?, wallpaper i 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 { @@ -135,17 +129,17 @@ public func chatControllerBackgroundImageSignal(wallpaper: TelegramWallpaper, me |> 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 + case let .gradient(_, colors, settings): + return .single((generateImage(CGSize(width: 640.0, height: 1280.0).fitted(CGSize(width: 100.0, height: 100.0)), rotatedContext: { size, context in + let gradientColors = [UIColor(rgb: colors.count >= 1 ? colors[0] : 0).cgColor, UIColor(rgb: colors.count >= 2 ? colors[1] : 0).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.translateBy(x: size.width / 2.0, y: size.height / 2.0) context.rotate(by: CGFloat(settings.rotation ?? 0) * CGFloat.pi / 180.0) - context.translateBy(x: -320.0, y: -640.0) + context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0) context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: size.height), options: [.drawsBeforeStartLocation, .drawsAfterEndLocation]) }), true)) @@ -174,76 +168,51 @@ public func chatControllerBackgroundImageSignal(wallpaper: TelegramWallpaper, me } } 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) - } + if wallpaper.isPattern { + return .single((nil, true)) } 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 + let effectiveMediaBox = mediaBox + + 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 { + return Signal { subscriber in + let fetch = fetchedMediaResource(mediaBox: accountMediaBox, reference: MediaResourceReference.wallpaper(wallpaper: WallpaperReference.slug(file.slug), resource: file.file.resource)).start() + var didOutputBlurred = false + let data = accountMediaBox.cachedResourceRepresentation(file.file.resource, representation: representation, complete: true, fetch: true, attemptSynchronously: true).start(next: { data in + if data.complete { + if let image = UIImage(contentsOfFile: data.path)?.precomposed() { + mediaBox.copyResourceData(file.file.resource.id, fromTempPath: data.path) + subscriber.putNext((image, true)) + } + } else if !didOutputBlurred { + didOutputBlurred = true + if let immediateThumbnailData = file.file.immediateThumbnailData, let decodedData = decodeTinyThumbnail(data: immediateThumbnailData) { + if let image = UIImage(data: decodedData)?.precomposed() { + subscriber.putNext((image, false)) + } + } + } + }) + + return ActionDisposable { + fetch.dispose() + data.dispose() + } } - } - |> afterNext { image in - cacheWallpaper(image?.0) } } else { var path: String? @@ -257,6 +226,31 @@ public func chatControllerBackgroundImageSignal(wallpaper: TelegramWallpaper, me |> afterNext { image in cacheWallpaper(image?.0) } + } else { + return Signal { subscriber in + let fetch = fetchedMediaResource(mediaBox: accountMediaBox, reference: MediaResourceReference.wallpaper(wallpaper: WallpaperReference.slug(file.slug), resource: file.file.resource)).start() + var didOutputBlurred = false + let data = accountMediaBox.resourceData(file.file.resource).start(next: { data in + if data.complete { + if let image = UIImage(contentsOfFile: data.path)?.precomposed() { + mediaBox.copyResourceData(file.file.resource.id, fromTempPath: data.path) + subscriber.putNext((image, true)) + } + } else if !didOutputBlurred { + didOutputBlurred = true + if let immediateThumbnailData = file.file.immediateThumbnailData, let decodedData = decodeTinyThumbnail(data: immediateThumbnailData) { + if let image = UIImage(data: decodedData)?.precomposed() { + subscriber.putNext((image, false)) + } + } + } + }) + + return ActionDisposable { + fetch.dispose() + data.dispose() + } + } } } } diff --git a/submodules/TelegramPresentationData/Sources/ChatMessageBubbleImages.swift b/submodules/TelegramPresentationData/Sources/ChatMessageBubbleImages.swift index 78ffaa5bad..4b3ad3a60e 100644 --- a/submodules/TelegramPresentationData/Sources/ChatMessageBubbleImages.swift +++ b/submodules/TelegramPresentationData/Sources/ChatMessageBubbleImages.swift @@ -197,8 +197,8 @@ public func messageBubbleImage(maxCornerRadius: CGFloat, minCornerRadius: CGFloa 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 + borderWidth = UIScreenPixel + innerExtension + borderOffset = -innerExtension / 2.0// + UIScreenPixel * 2.0 / 2.0 } context.setLineWidth(borderWidth) diff --git a/submodules/TelegramPresentationData/Sources/ChatPresentationData.swift b/submodules/TelegramPresentationData/Sources/ChatPresentationData.swift index 614ea5416e..4ed273afbf 100644 --- a/submodules/TelegramPresentationData/Sources/ChatPresentationData.swift +++ b/submodules/TelegramPresentationData/Sources/ChatPresentationData.swift @@ -66,6 +66,6 @@ public final class ChatPresentationData { extension ChatPresentationData { public convenience init(presentationData: PresentationData) { - self.init(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.init(theme: ChatPresentationThemeData(theme: presentationData.theme, wallpaper: presentationData.chatWallpaper), fontSize: presentationData.chatFontSize, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, disableAnimations: true, largeEmoji: presentationData.largeEmoji, chatBubbleCorners: presentationData.chatBubbleCorners) } } diff --git a/submodules/TelegramPresentationData/Sources/ComponentsThemes.swift b/submodules/TelegramPresentationData/Sources/ComponentsThemes.swift index 4467fbb77f..0168febaff 100644 --- a/submodules/TelegramPresentationData/Sources/ComponentsThemes.swift +++ b/submodules/TelegramPresentationData/Sources/ComponentsThemes.swift @@ -45,9 +45,9 @@ public extension TabBarControllerTheme { } public extension NavigationBarTheme { - convenience init(rootControllerTheme: PresentationTheme, hideBackground: Bool = false, hideBadge: Bool = false) { + convenience init(rootControllerTheme: PresentationTheme, enableBackgroundBlur: Bool = true, hideBackground: Bool = false, hideBadge: Bool = false) { let theme = rootControllerTheme.rootController.navigationBar - self.init(buttonColor: theme.buttonColor, disabledButtonColor: theme.disabledButtonColor, primaryTextColor: theme.primaryTextColor, backgroundColor: hideBackground ? .clear : theme.backgroundColor, separatorColor: hideBackground ? .clear : theme.separatorColor, badgeBackgroundColor: hideBadge ? .clear : theme.badgeBackgroundColor, badgeStrokeColor: hideBadge ? .clear : theme.badgeStrokeColor, badgeTextColor: hideBadge ? .clear : theme.badgeTextColor) + self.init(buttonColor: theme.buttonColor, disabledButtonColor: theme.disabledButtonColor, primaryTextColor: theme.primaryTextColor, backgroundColor: hideBackground ? .clear : theme.blurredBackgroundColor, enableBackgroundBlur: enableBackgroundBlur, separatorColor: hideBackground ? .clear : theme.separatorColor, badgeBackgroundColor: hideBadge ? .clear : theme.badgeBackgroundColor, badgeStrokeColor: hideBadge ? .clear : theme.badgeStrokeColor, badgeTextColor: hideBadge ? .clear : theme.badgeTextColor) } } @@ -104,13 +104,6 @@ public extension AlertControllerTheme { } } -extension PeekControllerTheme { - convenience public init(presentationTheme: PresentationTheme) { - let actionSheet = presentationTheme.actionSheet - self.init(isDark: actionSheet.backgroundType == .dark, menuBackgroundColor: actionSheet.opaqueItemBackgroundColor, menuItemHighligtedColor: actionSheet.opaqueItemHighlightedBackgroundColor, menuItemSeparatorColor: actionSheet.opaqueItemSeparatorColor, accentColor: actionSheet.controlAccentColor, destructiveColor: actionSheet.destructiveActionTextColor) - } -} - public extension NavigationControllerTheme { convenience init(presentationTheme: PresentationTheme) { let navigationStatusBar: NavigationStatusBarStyle diff --git a/submodules/TelegramPresentationData/Sources/DefaultDarkPresentationTheme.swift b/submodules/TelegramPresentationData/Sources/DefaultDarkPresentationTheme.swift index f81fe6f02d..7580569fdc 100644 --- a/submodules/TelegramPresentationData/Sources/DefaultDarkPresentationTheme.swift +++ b/submodules/TelegramPresentationData/Sources/DefaultDarkPresentationTheme.swift @@ -5,9 +5,56 @@ import SyncCore import TelegramUIPreferences public let defaultDarkPresentationTheme = makeDefaultDarkPresentationTheme(preview: false) -public let defaultDarkColorPresentationTheme = customizeDefaultDarkPresentationTheme(theme: defaultDarkPresentationTheme, editing: false, title: nil, accentColor: UIColor(rgb: 0x007aff), backgroundColors: nil, bubbleColors: nil, wallpaper: nil) +public let defaultDarkColorPresentationTheme = customizeDefaultDarkPresentationTheme(theme: defaultDarkPresentationTheme, editing: false, title: nil, accentColor: UIColor(rgb: 0x007aff), backgroundColors: [], bubbleColors: nil, wallpaper: nil, baseColor: nil) -public func customizeDefaultDarkPresentationTheme(theme: PresentationTheme, editing: Bool, title: String?, accentColor: UIColor?, backgroundColors: (UIColor, UIColor?)?, bubbleColors: (UIColor, UIColor?)?, wallpaper forcedWallpaper: TelegramWallpaper? = nil) -> PresentationTheme { +private extension PresentationThemeBaseColor { +/* + + Оранжевая с красным + https://t.me/bg/9LW_RcoOSVACAAAAFTk3DTyXN-M?bg_color=fec496~dd6cb9~962fbf~4f5bd5&intensity=-40 + + Голубая с розовым + https://t.me/bg/9iklpvIPQVABAAAAORQXKur_Eyc?bg_color=8adbf2~888dec~e39fea~679ced&intensity=-30 + + Мятная + https://t.me/bg/CJNyxPMgSVAEAAAAvW9sMwc51cw?bg_color=7fa381~fff5c5~336f55~fbe37d&intensity=-20 + + Синяя с розовым + https://t.me/bg/9LW_RcoOSVACAAAAFTk3DTyXN-M?bg_color=fec496~dd6cb9~962fbf~4f5bd5&intensity=-40 + +*/ + + var colorWallpaper: (BuiltinWallpaperData, Int32, [UInt32])? { + switch self { + case .blue: + return nil + case .cyan: + return (.variant5, -30, [0xa4dbff, 0x009fdd, 0x527bdd]) + case .green: + return (.variant3, -20, [0x7fa381, 0xfff5c5, 0x336f55, 0xfbe37d]) + case .pink: + return (.variant9, -35, [0xe4b2ea, 0x8376c2, 0xeab9d9, 0xb493e6]) + case .orange: + return (.variant2, -40, [0xfec496, 0xdd6cb9, 0x962fbf, 0x4f5bd5]) + case .purple: + return (.variant6, -30, [0x8adbf2, 0x888dec, 0xe39fea, 0x679ced]) + case .red: + return (.variant4, -35, [0xe4b2ea, 0x8376c2, 0xeab9d9, 0xb493e6]) + case .yellow: + return (.variant1, -30, [0xeaa36e, 0xf0e486, 0xf29ebf, 0xe8c06e]) + case .gray: + return nil + case .black: + return nil + case .white: + return nil + case .custom, .preset, .theme: + return nil + } + } +} + +public func customizeDefaultDarkPresentationTheme(theme: PresentationTheme, editing: Bool, title: String?, accentColor: UIColor?, backgroundColors: [UInt32], bubbleColors: (UIColor, UIColor?)?, wallpaper forcedWallpaper: TelegramWallpaper? = nil, baseColor: PresentationThemeBaseColor? = nil) -> PresentationTheme { if (theme.referenceTheme != .night) { return theme } @@ -82,11 +129,13 @@ public func customizeDefaultDarkPresentationTheme(theme: PresentationTheme, edit 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 if let baseColor = baseColor, let (variant, intensity, colors) = baseColor.colorWallpaper, !colors.isEmpty { + defaultWallpaper = defaultBuiltinWallpaper(data: variant, colors: colors, intensity: intensity) + } else if !backgroundColors.isEmpty { + if backgroundColors.count >= 2 { + defaultWallpaper = .gradient(nil, backgroundColors, WallpaperSettings()) } else { - defaultWallpaper = .color(backgroundColors.0.argb) + defaultWallpaper = .color(backgroundColors[0]) } } @@ -231,18 +280,6 @@ public func customizeDefaultDarkPresentationTheme(theme: PresentationTheme, edit } 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: UIColor(rgb: 0xffffff), - textColor: UIColor(rgb: 0x828282), - selectedTextColor: UIColor(rgb: 0xffffff), - badgeBackgroundColor: UIColor(rgb: 0xffffff), - badgeStrokeColor: UIColor(rgb: 0x1c1c1d), - badgeTextColor: UIColor(rgb: 0x000000) - ) - let rootNavigationBar = PresentationThemeRootNavigationBar( buttonColor: UIColor(rgb: 0xffffff), disabledButtonColor: UIColor(rgb: 0x525252), @@ -250,7 +287,8 @@ public func makeDefaultDarkPresentationTheme(extendingThemeReference: Presentati secondaryTextColor: UIColor(rgb: 0xffffff, alpha: 0.5), controlColor: UIColor(rgb: 0x767676), accentTextColor: UIColor(rgb: 0xffffff), - backgroundColor: UIColor(rgb: 0x1c1c1d), + blurredBackgroundColor: UIColor(rgb: 0x1d1d1d, alpha: 0.9), + opaqueBackgroundColor: UIColor(rgb: 0x1d1d1d).mixedWith(UIColor(rgb: 0x000000), alpha: 0.1), separatorColor: UIColor(rgb: 0x3d3d40), badgeBackgroundColor: UIColor(rgb: 0xffffff), badgeStrokeColor: UIColor(rgb: 0x1c1c1d), @@ -263,6 +301,18 @@ public func makeDefaultDarkPresentationTheme(extendingThemeReference: Presentati clearButtonForegroundColor: UIColor(rgb: 0xffffff) ) + let rootTabBar = PresentationThemeRootTabBar( + backgroundColor: rootNavigationBar.blurredBackgroundColor, + separatorColor: UIColor(rgb: 0x3d3d40), + iconColor: UIColor(rgb: 0x828282), + selectedIconColor: UIColor(rgb: 0xffffff), + textColor: UIColor(rgb: 0x828282), + selectedTextColor: UIColor(rgb: 0xffffff), + badgeBackgroundColor: UIColor(rgb: 0xffffff), + badgeStrokeColor: UIColor(rgb: 0x1c1c1d), + badgeTextColor: UIColor(rgb: 0x000000) + ) + let navigationSearchBar = PresentationThemeNavigationSearchBar( backgroundColor: UIColor(rgb: 0x1c1c1d), accentColor: UIColor(rgb: 0xffffff), @@ -341,25 +391,31 @@ public func makeDefaultDarkPresentationTheme(extendingThemeReference: Presentati ), 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), + backgroundColor: UIColor(rgb: 0x272728), + strokeColor: UIColor(rgb: 0x272728), + placeholderColor: UIColor(rgb: 0x98989e), primaryColor: UIColor(rgb: 0xffffff), - controlColor: UIColor(rgb: 0x4d4d4d) + controlColor: UIColor(rgb: 0x98989e) ), freePlainInputField: PresentationInputFieldTheme( - backgroundColor: UIColor(rgb: 0xffffff, alpha: 0.5), - strokeColor: UIColor(rgb: 0xffffff, alpha: 0.5), - placeholderColor: UIColor(rgb: 0x4d4d4d), + backgroundColor: UIColor(rgb: 0x272728), + strokeColor: UIColor(rgb: 0x272728), + placeholderColor: UIColor(rgb: 0x98989e), primaryColor: UIColor(rgb: 0xffffff), - controlColor: UIColor(rgb: 0x4d4d4d) + controlColor: UIColor(rgb: 0x98989e) ), 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: UIColor(rgb: 0xffffff), color2: UIColor(rgb: 0x929196), color3: UIColor(rgb: 0x333333)), - itemInputField: PresentationInputFieldTheme(backgroundColor: UIColor(rgb: 0x0f0f0f), strokeColor: UIColor(rgb: 0x0f0f0f), placeholderColor: UIColor(rgb: 0x8f8f8f), primaryColor: UIColor(rgb: 0xffffff), controlColor: UIColor(rgb: 0x8f8f8f)) + itemInputField: PresentationInputFieldTheme(backgroundColor: UIColor(rgb: 0x0f0f0f), strokeColor: UIColor(rgb: 0x0f0f0f), placeholderColor: UIColor(rgb: 0x8f8f8f), primaryColor: UIColor(rgb: 0xffffff), controlColor: UIColor(rgb: 0x8f8f8f)), + paymentOption: PresentationThemeList.PaymentOption( + inactiveFillColor: UIColor(rgb: 0x00A650).withMultipliedAlpha(0.3), + inactiveForegroundColor: UIColor(rgb: 0x00A650), + activeFillColor: UIColor(rgb: 0x00A650), + activeForegroundColor: UIColor(rgb: 0xffffff) + ) ) let chatList = PresentationThemeChatList( @@ -397,10 +453,12 @@ public func makeDefaultDarkPresentationTheme(extendingThemeReference: Presentati unpinnedArchiveAvatarColor: PresentationThemeArchiveAvatarColors(backgroundColors: PresentationThemeGradientColors(topColor: UIColor(rgb: 0x666666), bottomColor: UIColor(rgb: 0x666666)), foregroundColor: UIColor(rgb: 0x000000)), onlineDotColor: UIColor(rgb: 0x4cc91f) ) + + let incomingBubbleAlpha: CGFloat = 0.9 let message = PresentationThemeChatMessage( - 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)), + incoming: PresentationThemePartedColors(bubble: PresentationThemeBubbleColor(withWallpaper: PresentationThemeBubbleColorComponents(fill: UIColor(rgb: 0x1D1D1D, alpha: incomingBubbleAlpha), highlightedFill: UIColor(rgb: 0x353539), stroke: UIColor(rgb: 0x262628), shadow: nil), withoutWallpaper: PresentationThemeBubbleColorComponents(fill: UIColor(rgb: 0x1D1D1D, alpha: incomingBubbleAlpha), 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: 0x61BCF9), gradientFill: UIColor(rgb: 0x007AFF), highlightedFill: UIColor(rgb: 0x61BCF9), stroke: .clear, shadow: nil), withoutWallpaper: PresentationThemeBubbleColorComponents(fill: UIColor(rgb: 0x61BCF9), gradientFill: UIColor(rgb: 0x007AFF), highlightedFill: UIColor(rgb: 0x61BCF9), stroke: .clear, 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), @@ -433,15 +491,15 @@ public func makeDefaultDarkPresentationTheme(extendingThemeReference: Presentati ) let inputPanel = PresentationThemeChatInputPanel( - panelBackgroundColor: UIColor(rgb: 0x1c1c1d), - panelBackgroundColorNoWallpaper: UIColor(rgb: 0x000000), + panelBackgroundColor: rootNavigationBar.blurredBackgroundColor, + panelBackgroundColorNoWallpaper: UIColor(rgb: 0x000000, alpha: 0.94), panelSeparatorColor: UIColor(rgb: 0x3d3d40), 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), + inputStrokeColor: UIColor(rgb: 0xffffff, alpha: 0.1), inputPlaceholderColor: UIColor(rgb: 0x7b7b7b), inputTextColor: UIColor(rgb: 0xffffff), inputControlColor: UIColor(rgb: 0x7b7b7b), diff --git a/submodules/TelegramPresentationData/Sources/DefaultDarkTintedPresentationTheme.swift b/submodules/TelegramPresentationData/Sources/DefaultDarkTintedPresentationTheme.swift index 9a9326fe19..1d1ebd05a6 100644 --- a/submodules/TelegramPresentationData/Sources/DefaultDarkTintedPresentationTheme.swift +++ b/submodules/TelegramPresentationData/Sources/DefaultDarkTintedPresentationTheme.swift @@ -3,11 +3,43 @@ import UIKit import TelegramCore import SyncCore import TelegramUIPreferences +import Postbox 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 { +private extension PresentationThemeBaseColor { + var colorWallpaper: (BuiltinWallpaperData, Int32, [UInt32])? { + switch self { + case .blue: + return (.variant7, 40, [0x1e3557, 0x182036, 0x1c4352, 0x16263a]) + case .cyan: + return (.variant3, 40, [0x1e3557, 0x151a36, 0x1c4352, 0x2a4541]) + case .green: + return (.variant3, 40, [0x2d4836, 0x172b19, 0x364331, 0x103231]) + case .pink: + return (.variant9, 40, [0x2c0b22, 0x290020, 0x160a22, 0x3b1834]) + case .orange: + return (.variant10, 40, [0x2c211b, 0x442917, 0x22191f, 0x3b2714]) + case .purple: + return (.variant11, 40, [0x3a1c3a, 0x24193c, 0x392e3e, 0x1a1632]) + case .red: + return (.variant4, 40, [0x2c211b, 0x44332a, 0x22191f, 0x3b2d36]) + case .yellow: + return (.variant2, 40, [0x2c2512, 0x45360b, 0x221d08, 0x3b2f13]) + case .gray: + return (.variant6, 40, [0x1c2731, 0x1a1c25, 0x27303b, 0x1b1b21]) + case .black: + return nil + case .white: + return nil + case .custom, .preset, .theme: + return nil + } + } +} + +public func customizeDefaultDarkTintedPresentationTheme(theme: PresentationTheme, editing: Bool, title: String?, accentColor: UIColor?, backgroundColors: [UInt32], bubbleColors: (UIColor, UIColor?)?, wallpaper forcedWallpaper: TelegramWallpaper? = nil, baseColor: PresentationThemeBaseColor? = nil) -> PresentationTheme { if (theme.referenceTheme != .nightAccent) { return theme } @@ -46,8 +78,12 @@ public func customizeDefaultDarkTintedPresentationTheme(theme: PresentationTheme 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) + if let baseColor = baseColor, let (variant, intensity, colors) = baseColor.colorWallpaper, !colors.isEmpty { + suggestedWallpaper = defaultBuiltinWallpaper(data: variant, colors: colors, intensity: intensity) + } else { + let color = accentColor.withMultiplied(hue: 1.024, saturation: 0.573, brightness: 0.18) + suggestedWallpaper = .color(color.argb) + } } let accentColor = accentColor ?? defaultDarkTintedAccentColor @@ -87,7 +123,7 @@ public func customizeDefaultDarkTintedPresentationTheme(theme: PresentationTheme 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, + backgroundColor: mainBackgroundColor?.withAlphaComponent(0.9), separatorColor: mainSeparatorColor, iconColor: mainForegroundColor, selectedIconColor: accentColor, @@ -100,7 +136,8 @@ public func customizeDefaultDarkTintedPresentationTheme(theme: PresentationTheme secondaryTextColor: mainSecondaryColor, controlColor: mainSecondaryColor, accentTextColor: accentColor, - backgroundColor: mainBackgroundColor, + blurredBackgroundColor: mainBackgroundColor?.withAlphaComponent(0.9), + opaqueBackgroundColor: mainBackgroundColor, separatorColor: mainSeparatorColor, segmentedBackgroundColor: mainInputColor, segmentedForegroundColor: mainBackgroundColor, @@ -147,12 +184,12 @@ public func customizeDefaultDarkTintedPresentationTheme(theme: PresentationTheme ), controlSecondaryColor: mainSecondaryTextColor?.withAlphaComponent(0.5), freeInputField: list.freeInputField.withUpdated( - backgroundColor: mainSecondaryTextColor?.withAlphaComponent(0.5), - strokeColor: mainSecondaryTextColor?.withAlphaComponent(0.5) + backgroundColor: accentColor?.withMultiplied(hue: 1.029, saturation: 0.609, brightness: 0.12), + strokeColor: accentColor?.withMultiplied(hue: 1.029, saturation: 0.609, brightness: 0.12) ), freePlainInputField: list.freePlainInputField.withUpdated( - backgroundColor: mainSecondaryTextColor?.withAlphaComponent(0.5), - strokeColor: mainSecondaryTextColor?.withAlphaComponent(0.5) + backgroundColor: accentColor?.withMultiplied(hue: 1.029, saturation: 0.609, brightness: 0.12), + strokeColor: accentColor?.withMultiplied(hue: 1.029, saturation: 0.609, brightness: 0.12) ), mediaPlaceholderColor: UIColor(rgb: 0xffffff).mixedWith(mainBackgroundColor ?? list.itemBlocksBackgroundColor, alpha: 0.9), pageIndicatorInactiveColor: mainSecondaryTextColor?.withAlphaComponent(0.4), @@ -222,11 +259,11 @@ public func customizeDefaultDarkTintedPresentationTheme(theme: PresentationTheme 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 if !backgroundColors.isEmpty { + if backgroundColors.count >= 2 { + defaultWallpaper = .gradient(nil, backgroundColors, WallpaperSettings()) } else { - defaultWallpaper = .color(backgroundColors.0.argb) + defaultWallpaper = .color(backgroundColors[0]) } } else if let forcedWallpaper = suggestedWallpaper { defaultWallpaper = forcedWallpaper @@ -264,6 +301,8 @@ public func customizeDefaultDarkTintedPresentationTheme(theme: PresentationTheme highlightedIncomingBubbleColor = accentColor?.withMultiplied(hue: 1.03, saturation: 0.463, brightness: 0.29) highlightedOutgoingBubbleColor = outgoingBubbleFillColor?.withMultiplied(hue: 1.019, saturation: 0.609, brightness: 0.63) } + + let incomingFillColor = mainBackgroundColor?.withMultipliedAlpha(0.9) chat = chat.withUpdated( defaultWallpaper: defaultWallpaper, @@ -271,14 +310,14 @@ public func customizeDefaultDarkTintedPresentationTheme(theme: PresentationTheme incoming: chat.message.incoming.withUpdated( bubble: chat.message.incoming.bubble.withUpdated( withWallpaper: chat.message.outgoing.bubble.withWallpaper.withUpdated( - fill: mainBackgroundColor, - gradientFill: mainBackgroundColor, + fill: incomingFillColor, + gradientFill: incomingFillColor, highlightedFill: highlightedIncomingBubbleColor, stroke: mainBackgroundColor ), withoutWallpaper: chat.message.outgoing.bubble.withoutWallpaper.withUpdated( - fill: mainBackgroundColor, - gradientFill: mainBackgroundColor, + fill: incomingFillColor, + gradientFill: incomingFillColor, highlightedFill: highlightedIncomingBubbleColor, stroke: mainBackgroundColor ) @@ -379,7 +418,7 @@ public func customizeDefaultDarkTintedPresentationTheme(theme: PresentationTheme unreadBarStrokeColor: mainBackgroundColor ), inputPanel: chat.inputPanel.withUpdated( - panelBackgroundColor: mainBackgroundColor, + panelBackgroundColor: mainBackgroundColor?.withAlphaComponent(0.9), panelSeparatorColor: mainSeparatorColor, panelControlAccentColor: accentColor, panelControlColor: mainSecondaryTextColor?.withAlphaComponent(0.5), @@ -414,7 +453,7 @@ public func customizeDefaultDarkTintedPresentationTheme(theme: PresentationTheme buttonHighlightedStrokeColor: accentColor?.withMultiplied(hue: 1.019, saturation: 0.39, brightness: 0.07) ), historyNavigation: chat.historyNavigation.withUpdated( - fillColor: mainBackgroundColor, + fillColor: mainBackgroundColor?.withAlphaComponent(0.9), strokeColor: mainSeparatorColor, foregroundColor: mainSecondaryTextColor?.withAlphaComponent(0.5), badgeBackgroundColor: accentColor, @@ -503,7 +542,8 @@ public func makeDefaultDarkTintedPresentationTheme(extendingThemeReference: Pres secondaryTextColor: mainSecondaryColor, controlColor: mainSecondaryColor, accentTextColor: accentColor, - backgroundColor: mainBackgroundColor, + blurredBackgroundColor: mainBackgroundColor.withAlphaComponent(0.9), + opaqueBackgroundColor: mainBackgroundColor, separatorColor: mainSeparatorColor, badgeBackgroundColor: UIColor(rgb: 0xef5b5b), badgeStrokeColor: UIColor(rgb: 0xef5b5b), @@ -594,25 +634,31 @@ public func makeDefaultDarkTintedPresentationTheme(extendingThemeReference: Pres ), controlSecondaryColor: mainSecondaryTextColor.withAlphaComponent(0.5), freeInputField: PresentationInputFieldTheme( - backgroundColor: mainSecondaryTextColor.withAlphaComponent(0.5), - strokeColor: mainSecondaryTextColor.withAlphaComponent(0.5), - placeholderColor: UIColor(rgb: 0x4d4d4d), + backgroundColor: accentColor.withMultiplied(hue: 1.029, saturation: 0.609, brightness: 0.12), + strokeColor: accentColor.withMultiplied(hue: 1.029, saturation: 0.609, brightness: 0.12), + placeholderColor: mainSecondaryTextColor.withAlphaComponent(0.5), primaryColor: .white, - controlColor: UIColor(rgb: 0x4d4d4d) + controlColor: mainSecondaryTextColor.withAlphaComponent(0.5) ), freePlainInputField: PresentationInputFieldTheme( - backgroundColor: mainSecondaryTextColor.withAlphaComponent(0.5), - strokeColor: mainSecondaryTextColor.withAlphaComponent(0.5), - placeholderColor: UIColor(rgb: 0x4d4d4d), + backgroundColor: accentColor.withMultiplied(hue: 1.029, saturation: 0.609, brightness: 0.12), + strokeColor: accentColor.withMultiplied(hue: 1.029, saturation: 0.609, brightness: 0.12), + placeholderColor: mainSecondaryTextColor.withAlphaComponent(0.5), primaryColor: .white, - controlColor: UIColor(rgb: 0x4d4d4d) + controlColor: mainSecondaryTextColor.withAlphaComponent(0.5) ), mediaPlaceholderColor: UIColor(rgb: 0xffffff).mixedWith(mainBackgroundColor, alpha: 0.9), scrollIndicatorColor: UIColor(white: 1.0, alpha: 0.3), pageIndicatorInactiveColor: mainSecondaryTextColor.withAlphaComponent(0.4), inputClearButtonColor: mainSecondaryColor, itemBarChart: PresentationThemeItemBarChart(color1: accentColor, color2: mainSecondaryTextColor.withAlphaComponent(0.5), color3: accentColor.withMultiplied(hue: 1.038, saturation: 0.329, brightness: 0.33)), - itemInputField: PresentationInputFieldTheme(backgroundColor: mainInputColor, strokeColor: mainInputColor, placeholderColor: mainSecondaryColor, primaryColor: UIColor(rgb: 0xffffff), controlColor: mainSecondaryColor) + itemInputField: PresentationInputFieldTheme(backgroundColor: mainInputColor, strokeColor: mainInputColor, placeholderColor: mainSecondaryColor, primaryColor: UIColor(rgb: 0xffffff), controlColor: mainSecondaryColor), + paymentOption: PresentationThemeList.PaymentOption( + inactiveFillColor: UIColor(rgb: 0x00A650).withMultipliedAlpha(0.3), + inactiveForegroundColor: UIColor(rgb: 0x00A650), + activeFillColor: UIColor(rgb: 0x00A650), + activeForegroundColor: UIColor(rgb: 0xffffff) + ) ) let chatList = PresentationThemeChatList( @@ -639,7 +685,7 @@ public func makeDefaultDarkTintedPresentationTheme(extendingThemeReference: Pres unreadBadgeInactiveBackgroundColor: mainSecondaryTextColor.withAlphaComponent(0.4), unreadBadgeInactiveTextColor: additionalBackgroundColor, pinnedBadgeColor: mainSecondaryTextColor.withAlphaComponent(0.5), - pinnedSearchBarColor: mainInputColor, + pinnedSearchBarColor: accentColor.withMultiplied(hue: 1.029, saturation: 0.609, brightness: 0.12), regularSearchBarColor: accentColor.withMultiplied(hue: 1.029, saturation: 0.609, brightness: 0.12), sectionHeaderFillColor: mainBackgroundColor, sectionHeaderTextColor: mainSecondaryTextColor.withAlphaComponent(0.5), @@ -652,9 +698,10 @@ public func makeDefaultDarkTintedPresentationTheme(extendingThemeReference: Pres ) let buttonStrokeColor = accentColor.withMultiplied(hue: 1.014, saturation: 0.56, brightness: 0.64).withAlphaComponent(0.15) + let incomingFillColor = mainBackgroundColor.withMultipliedAlpha(0.9) let message = PresentationThemeChatMessage( - 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), + incoming: PresentationThemePartedColors(bubble: PresentationThemeBubbleColor(withWallpaper: PresentationThemeBubbleColorComponents(fill: incomingFillColor, highlightedFill: highlightedIncomingBubbleColor, stroke: mainBackgroundColor, shadow: nil), withoutWallpaper: PresentationThemeBubbleColorComponents(fill: incomingFillColor, 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), @@ -741,7 +788,7 @@ public func makeDefaultDarkTintedPresentationTheme(extendingThemeReference: Pres ) let chat = PresentationThemeChat( - defaultWallpaper: .color(accentColor.withMultiplied(hue: 1.024, saturation: 0.573, brightness: 0.18).argb), + defaultWallpaper: defaultBuiltinWallpaper(data: .default, colors: [0x1b2836, 0x121a22, 0x1b2836, 0x121a22]), message: message, serviceMessage: serviceMessage, inputPanel: inputPanel, @@ -775,7 +822,7 @@ public func makeDefaultDarkTintedPresentationTheme(extendingThemeReference: Pres let contextMenu = PresentationThemeContextMenu( dimColor: UIColor(rgb: 0x000000, alpha: 0.6), - backgroundColor: rootNavigationBar.backgroundColor.withAlphaComponent(0.78), + backgroundColor: rootNavigationBar.opaqueBackgroundColor.withAlphaComponent(0.78), itemSeparatorColor: UIColor(rgb: 0xffffff, alpha: 0.15), sectionSeparatorColor: UIColor(rgb: 0x000000, alpha: 0.2), itemBackgroundColor: UIColor(rgb: 0x000000, alpha: 0.0), diff --git a/submodules/TelegramPresentationData/Sources/DefaultDayPresentationTheme.swift b/submodules/TelegramPresentationData/Sources/DefaultDayPresentationTheme.swift index a980bbbb11..c46547f639 100644 --- a/submodules/TelegramPresentationData/Sources/DefaultDayPresentationTheme.swift +++ b/submodules/TelegramPresentationData/Sources/DefaultDayPresentationTheme.swift @@ -3,12 +3,40 @@ import UIKit import TelegramCore import SyncCore import TelegramUIPreferences +import Postbox +import SwiftSignalKit -public let defaultServiceBackgroundColor = UIColor(rgb: 0x000000, alpha: 0.3) +public func selectDateFillStaticColor(theme: PresentationTheme, wallpaper: TelegramWallpaper) -> UIColor { + if case .color(0xffffff) = wallpaper { + return theme.chat.serviceMessage.components.withDefaultWallpaper.dateFillStatic + } else if case .builtin = wallpaper { + return UIColor(rgb: 0x748391, alpha: 0.45) + } else { + return theme.chat.serviceMessage.components.withCustomWallpaper.dateFillStatic + } +} + +public func dateFillNeedsBlur(theme: PresentationTheme, wallpaper: TelegramWallpaper) -> Bool { + if case .builtin = wallpaper { + return false + } else if case .color = wallpaper { + return false + } else if case let .file(_, _, _, _, isPattern, _, _, _, settings) = wallpaper { + if isPattern, let intensity = settings.intensity, intensity < 0 { + return false + } else { + return true + } + } else { + return true + } +} + +public let defaultServiceBackgroundColor = UIColor(rgb: 0x000000, alpha: 0.2) 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 { +public func customizeDefaultDayTheme(theme: PresentationTheme, editing: Bool, title: String?, accentColor: UIColor?, backgroundColors: [UInt32], bubbleColors: (UIColor, UIColor?)?, wallpaper forcedWallpaper: TelegramWallpaper? = nil, serviceBackgroundColor: UIColor?) -> PresentationTheme { if (theme.referenceTheme != .day && theme.referenceTheme != .dayClassic) { return theme } @@ -30,7 +58,7 @@ public func customizeDefaultDayTheme(theme: PresentationTheme, editing: Bool, ti let accentColor = accentColor ?? defaultDayAccentColor bubbleColors = (accentColor.withMultiplied(hue: 0.966, saturation: 0.61, brightness: 0.98), accentColor) } else { - if let accentColor = accentColor { + if let accentColor = accentColor, !accentColor.alpha.isZero { 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 { @@ -38,13 +66,11 @@ public func customizeDefaultDayTheme(theme: PresentationTheme, editing: Bool, ti } 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()) + + suggestedWallpaper = .gradient(nil, defaultBuiltinWallpaperGradientColors.map(\.rgb), WallpaperSettings()) } else { bubbleColors = (UIColor(rgb: 0xe1ffc7), nil) - suggestedWallpaper = .builtin(WallpaperSettings()) + suggestedWallpaper = .gradient(nil, defaultBuiltinWallpaperGradientColors.map(\.rgb), WallpaperSettings()) } } } @@ -201,11 +227,11 @@ public func customizeDefaultDayTheme(theme: PresentationTheme, editing: Bool, ti 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 if !backgroundColors.isEmpty { + if backgroundColors.count >= 2 { + defaultWallpaper = .gradient(nil, backgroundColors, WallpaperSettings()) } else { - defaultWallpaper = .color(backgroundColors.0.argb) + defaultWallpaper = .color(backgroundColors[0]) } } else if let forcedWallpaper = suggestedWallpaper { defaultWallpaper = forcedWallpaper @@ -319,6 +345,10 @@ public func customizeDefaultDayTheme(theme: PresentationTheme, editing: Bool, ti public func makeDefaultDayPresentationTheme(extendingThemeReference: PresentationThemeReference? = nil, serviceBackgroundColor: UIColor?, day: Bool, preview: Bool) -> PresentationTheme { var serviceBackgroundColor = serviceBackgroundColor ?? defaultServiceBackgroundColor + + if !day { + serviceBackgroundColor = UIColor(white: 0.0, alpha: 0.2) + } let intro = PresentationThemeIntro( statusBarStyle: .black, @@ -333,9 +363,30 @@ public func makeDefaultDayPresentationTheme(extendingThemeReference: Presentatio backgroundColors: PresentationThemeGradientColors(topColor: UIColor(rgb: 0x46739e), bottomColor: UIColor(rgb: 0x2a5982)), buttonColor: .clear ) - + + 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), + blurredBackgroundColor: UIColor(rgb: 0xf2f2f2, alpha: 0.9), + opaqueBackgroundColor: UIColor(rgb: 0xf7f7f7).mixedWith(.white, alpha: 0.14), + separatorColor: UIColor(rgb: 0xc8c7cc), + badgeBackgroundColor: UIColor(rgb: 0xff3b30), + badgeStrokeColor: UIColor(rgb: 0xff3b30), + badgeTextColor: UIColor(rgb: 0xffffff), + segmentedBackgroundColor: UIColor(rgb: 0x000000, alpha: 0.06), + segmentedForegroundColor: UIColor(rgb: 0xf7f7f7), + segmentedTextColor: UIColor(rgb: 0x000000), + segmentedDividerColor: UIColor(rgb: 0xd6d6dc), + clearButtonBackgroundColor: UIColor(rgb: 0xE3E3E3, alpha: 0.78), + clearButtonForegroundColor: UIColor(rgb: 0x7f7f7f) + ) + let rootTabBar = PresentationThemeRootTabBar( - backgroundColor: UIColor(rgb: 0xf7f7f7), + backgroundColor: rootNavigationBar.blurredBackgroundColor, separatorColor: UIColor(rgb: 0xa3a3a3), iconColor: UIColor(rgb: 0x959595), selectedIconColor: UIColor(rgb: 0x007ee5), @@ -346,30 +397,10 @@ public func makeDefaultDayPresentationTheme(extendingThemeReference: Presentatio 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: 0xc8c7cc), - 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), - clearButtonBackgroundColor: UIColor(rgb: 0xE3E3E3, alpha: 0.78), - clearButtonForegroundColor: UIColor(rgb: 0x7f7f7f) - ) - let navigationSearchBar = PresentationThemeNavigationSearchBar( backgroundColor: UIColor(rgb: 0xffffff), accentColor: UIColor(rgb: 0x007ee5), - inputFillColor: UIColor(rgb: 0xe9e9e9), + inputFillColor: UIColor(rgb: 0x000000, alpha: 0.06), inputTextColor: UIColor(rgb: 0x000000), inputPlaceholderTextColor: UIColor(rgb: 0x8e8e93), inputIconColor: UIColor(rgb: 0x8e8e93), @@ -448,7 +479,13 @@ public func makeDefaultDayPresentationTheme(extendingThemeReference: Presentatio pageIndicatorInactiveColor: UIColor(rgb: 0xe3e3e7), inputClearButtonColor: UIColor(rgb: 0xcccccc), itemBarChart: PresentationThemeItemBarChart(color1: UIColor(rgb: 0x007ee5), color2: UIColor(rgb: 0xc8c7cc), color3: UIColor(rgb: 0xf2f1f7)), - itemInputField: PresentationInputFieldTheme(backgroundColor: UIColor(rgb: 0xf2f2f7), strokeColor: UIColor(rgb: 0xf2f2f7), placeholderColor: UIColor(rgb: 0xb6b6bb), primaryColor: UIColor(rgb: 0x000000), controlColor: UIColor(rgb: 0xb6b6bb)) + itemInputField: PresentationInputFieldTheme(backgroundColor: UIColor(rgb: 0xf2f2f7), strokeColor: UIColor(rgb: 0xf2f2f7), placeholderColor: UIColor(rgb: 0xb6b6bb), primaryColor: UIColor(rgb: 0x000000), controlColor: UIColor(rgb: 0xb6b6bb)), + paymentOption: PresentationThemeList.PaymentOption( + inactiveFillColor: UIColor(rgb: 0x00A650).withMultipliedAlpha(0.1), + inactiveForegroundColor: UIColor(rgb: 0x00A650), + activeFillColor: UIColor(rgb: 0x00A650), + activeForegroundColor: UIColor(rgb: 0xffffff) + ) ) let chatList = PresentationThemeChatList( @@ -487,7 +524,12 @@ public func makeDefaultDayPresentationTheme(extendingThemeReference: Presentatio onlineDotColor: UIColor(rgb: 0x4cc91f) ) - let bubbleStrokeColor = serviceBackgroundColor.withMultiplied(hue: 0.999, saturation: 1.667, brightness: 1.1).withAlphaComponent(0.2) + let bubbleStrokeColor: UIColor + if day { + bubbleStrokeColor = serviceBackgroundColor.withMultiplied(hue: 0.999, saturation: 1.667, brightness: 1.1).withAlphaComponent(0.2) + } else { + bubbleStrokeColor = UIColor(white: 0.0, alpha: 0.2) + } let message = PresentationThemeChatMessage( incoming: PresentationThemePartedColors( @@ -550,7 +592,7 @@ public func makeDefaultDayPresentationTheme(extendingThemeReference: Presentatio 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), - stickerPlaceholderColor: PresentationThemeVariableColor(withWallpaper: serviceBackgroundColor.withAlphaComponent(0.3), withoutWallpaper: UIColor(rgb: 0x748391, alpha: 0.25)), + stickerPlaceholderColor: PresentationThemeVariableColor(withWallpaper: serviceBackgroundColor, withoutWallpaper: UIColor(rgb: 0x748391, alpha: 0.25)), stickerPlaceholderShimmerColor: PresentationThemeVariableColor(withWallpaper: UIColor(rgb: 0xffffff, alpha: 0.2), withoutWallpaper: UIColor(rgb: 0x000000, alpha: 0.1)) ) @@ -623,7 +665,7 @@ public func makeDefaultDayPresentationTheme(extendingThemeReference: Presentatio ) let serviceMessage = PresentationThemeServiceMessage( - components: PresentationThemeServiceMessageColor(withDefaultWallpaper: PresentationThemeServiceMessageColorComponents(fill: UIColor(rgb: 0x939fab, alpha: 0.5), 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))), + components: PresentationThemeServiceMessageColor(withDefaultWallpaper: PresentationThemeServiceMessageColorComponents(fill: UIColor(rgb: 0x939fab, alpha: 0.5), primaryText: UIColor(rgb: 0xffffff), linkHighlight: UIColor(rgb: 0x748391, alpha: 0.25), scam: UIColor(rgb: 0xffffff), dateFillStatic: UIColor(rgb: 0x000000, alpha: 0.2), 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: UIColor(rgb: 0x000000, alpha: 0.2), 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), @@ -631,7 +673,7 @@ public func makeDefaultDayPresentationTheme(extendingThemeReference: Presentatio ) 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: 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))), + 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: UIColor(rgb: 0x000000, alpha: 0.2), dateFillFloating: serviceBackgroundColor.withAlphaComponent(serviceBackgroundColor.alpha * 0.6667))), unreadBarFillColor: UIColor(rgb: 0xffffff), unreadBarStrokeColor: UIColor(rgb: 0xffffff), unreadBarTextColor: UIColor(rgb: 0x8d8e93), @@ -645,15 +687,15 @@ public func makeDefaultDayPresentationTheme(extendingThemeReference: Presentatio ) let inputPanel = PresentationThemeChatInputPanel( - panelBackgroundColor: UIColor(rgb: 0xf7f7f7), - panelBackgroundColorNoWallpaper: UIColor(rgb: 0xffffff), + panelBackgroundColor: rootNavigationBar.blurredBackgroundColor, + panelBackgroundColorNoWallpaper: rootNavigationBar.blurredBackgroundColor, panelSeparatorColor: UIColor(rgb: 0xb2b2b2), 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), + inputStrokeColor: UIColor(rgb: 0x000000, alpha: 0.1), inputPlaceholderColor: UIColor(rgb: 0xbebec0), inputTextColor: UIColor(rgb: 0x000000), inputControlColor: UIColor(rgb: 0xa0a7b0), @@ -696,9 +738,11 @@ public func makeDefaultDayPresentationTheme(extendingThemeReference: Presentatio badgeStrokeColor: UIColor(rgb: 0x007ee5), badgeTextColor: UIColor(rgb: 0xffffff) ) - + + let defaultPatternWallpaper: TelegramWallpaper = defaultBuiltinWallpaper(data: .default, colors: defaultBuiltinWallpaperGradientColors.map(\.rgb)) + let chat = PresentationThemeChat( - defaultWallpaper: day ? .color(0xffffff) : .builtin(WallpaperSettings()), + defaultWallpaper: day ? .color(0xffffff) : defaultPatternWallpaper, message: day ? messageDay : message, serviceMessage: day ? serviceMessageDay : serviceMessage, inputPanel: inputPanel, @@ -791,3 +835,269 @@ public func makeDefaultDayPresentationTheme(extendingThemeReference: Presentatio preview: preview ) } + +public let defaultBuiltinWallpaperGradientColors: [UIColor] = [ + UIColor(rgb: 0xdbddbb), + UIColor(rgb: 0x6ba587), + UIColor(rgb: 0xd5d88d), + UIColor(rgb: 0x88b884) +] + +public struct BuiltinWallpaperData { + var wallpaperId: Int64 + var wallpaperAccessHash: Int64 + var slug: String + var fileId: Int64 + var fileAccessHash: Int64 + var datacenterId: Int + var fileSize: Int +} + +public extension BuiltinWallpaperData { + static let `default` = BuiltinWallpaperData( + wallpaperId: 5951821522514477057, + wallpaperAccessHash: 542913527008942388, + slug: "fqv01SQemVIBAAAApND8LDRUhRU", + fileId: 5789658100176783156, + fileAccessHash: 2106033778341319685, + datacenterId: 4, + fileSize: 183832 + ) + static let variant1 = BuiltinWallpaperData( + wallpaperId: 5784984711902265347, + wallpaperAccessHash: -7073897034484875424, + slug: "RlZs2PJkSFADAAAAElGaGwgJBgU", + fileId: 5924571028763183790, + fileAccessHash: 8131740629580593134, + datacenterId: 4, + fileSize: 175995 + ) + static let variant2 = BuiltinWallpaperData( + wallpaperId: 5785171457080295426, + wallpaperAccessHash: 7299737721761177260, + slug: "9LW_RcoOSVACAAAAFTk3DTyXN-M", + fileId: 5927041584146156278, + fileAccessHash: -5921024951834087382, + datacenterId: 4, + fileSize: 134539 + ) + static let variant3 = BuiltinWallpaperData( + wallpaperId: 5785191424383254532, + wallpaperAccessHash: 6428855567842967483, + slug: "CJNyxPMgSVAEAAAAvW9sMwc51cw", + fileId: 5785343895722264360, + fileAccessHash: 3407562549390786397, + datacenterId: 4, + fileSize: 312605 + ) + static let variant4 = BuiltinWallpaperData( + wallpaperId: 5785123761468473345, + wallpaperAccessHash: -6430405714673464374, + slug: "BQqgrGnjSFABAAAA8mQDBXQcARE", + fileId: 5924847998319201207, + fileAccessHash: 6746675725325490532, + datacenterId: 4, + fileSize: 55699 + ) + static let variant5 = BuiltinWallpaperData( + wallpaperId: 5785021373743104005, + wallpaperAccessHash: -1374597781576365315, + slug: "MIo6r0qGSFAFAAAAtL8TsDzNX60", + fileId: 5782630687571969871, + fileAccessHash: 8944679612701303524, + datacenterId: 4, + fileSize: 100992 + ) + static let variant6 = BuiltinWallpaperData( + wallpaperId: 5782920928576929793, + wallpaperAccessHash: -2397741670740938317, + slug: "9iklpvIPQVABAAAAORQXKur_Eyc", + fileId: 5924714386181589959, + fileAccessHash: -316419094644368953, + datacenterId: 4, + fileSize: 106249 + ) + static let variant7 = BuiltinWallpaperData( + wallpaperId: 5931406765567508492, + wallpaperAccessHash: 7991333610111953175, + slug: "H6rz6geXUFIMAAAAuUs7m6cXbcc", + fileId: 5931433527508732666, + fileAccessHash: -8637914243010610774, + datacenterId: 4, + fileSize: 76332 + ) + static let variant8 = BuiltinWallpaperData( + wallpaperId: 5785007509588672513, + wallpaperAccessHash: 8437532349638900210, + slug: "kO4jyq55SFABAAAA0WEpcLfahXk", + fileId: 5925009274341165314, + fileAccessHash: 5091210796362176800, + datacenterId: 4, + fileSize: 78338 + ) + static let variant9 = BuiltinWallpaperData( + wallpaperId: 5785068300555780101, + wallpaperAccessHash: -4335874468273472323, + slug: "mP3FG_iwSFAFAAAA2AklJO978pA", + fileId: 5924664689115007842, + fileAccessHash: -4490072684673383370, + datacenterId: 4, + fileSize: 51705 + ) + static let variant10 = BuiltinWallpaperData( + wallpaperId: 5785165465600917506, + wallpaperAccessHash: 4563443115749434444, + slug: "Ujx2TFcJSVACAAAARJ4vLa50MkM", + fileId: 5924792752154872619, + fileAccessHash: -2210879717040856036, + datacenterId: 4, + fileSize: 114694 + ) + static let variant11 = BuiltinWallpaperData( + wallpaperId: 5785225431934304257, + wallpaperAccessHash: 3814946612408881045, + slug: "RepJ5uE_SVABAAAAr4d0YhgB850", + fileId: 5927262354055105101, + fileAccessHash: -435932841948252811, + datacenterId: 4, + fileSize: 66465 + ) + static let variant12 = BuiltinWallpaperData( + wallpaperId: 5785328386595356675, + wallpaperAccessHash: -5900784223259948847, + slug: "9GcNVISdSVADAAAAUcw5BYjELW4", + fileId: 5926924928539429325, + fileAccessHash: -5306472339097647861, + datacenterId: 4, + fileSize: 57262 + ) + static let variant13 = BuiltinWallpaperData( + wallpaperId: 6041986402319597570, + wallpaperAccessHash: -8909137552203056986, + slug: "-Xc-np9y2VMCAAAARKr0yNNPYW0", + fileId: 5789856918507882132, + fileAccessHash: 2327344847690632249, + datacenterId: 4, + fileSize: 104932 + ) + static let variant14 = BuiltinWallpaperData( + wallpaperId: 5784981280223395841, + wallpaperAccessHash: 8334701614156015552, + slug: "JrNEYdNhSFABAAAA9WtRdJkPRbY", + fileId: 5924784243824658746, + fileAccessHash: -2563505106174626287, + datacenterId: 4, + fileSize: 122246 + ) + + static func generate(account: Account) { + let slugToName: [(String, String)] = [ + ("fqv01SQemVIBAAAApND8LDRUhRU", "`default`"), + ("RlZs2PJkSFADAAAAElGaGwgJBgU", "variant1"), + ("9LW_RcoOSVACAAAAFTk3DTyXN-M", "variant2"), + ("CJNyxPMgSVAEAAAAvW9sMwc51cw", "variant3"), + ("BQqgrGnjSFABAAAA8mQDBXQcARE", "variant4"), + ("MIo6r0qGSFAFAAAAtL8TsDzNX60", "variant5"), + ("9iklpvIPQVABAAAAORQXKur_Eyc", "variant6"), + ("H6rz6geXUFIMAAAAuUs7m6cXbcc", "variant7"), + ("kO4jyq55SFABAAAA0WEpcLfahXk", "variant8"), + ("mP3FG_iwSFAFAAAA2AklJO978pA", "variant9"), + ("Ujx2TFcJSVACAAAARJ4vLa50MkM", "variant10"), + ("RepJ5uE_SVABAAAAr4d0YhgB850", "variant11"), + ("9GcNVISdSVADAAAAUcw5BYjELW4", "variant12"), + ("-Xc-np9y2VMCAAAARKr0yNNPYW0", "variant13"), + ("JrNEYdNhSFABAAAA9WtRdJkPRbY", "variant14"), + ] + + var signals: [Signal] = [] + for (slug, name) in slugToName { + signals.append(getWallpaper(network: account.network, slug: slug) + |> map { wallpaper -> String? in + switch wallpaper { + case let .file(id, accessHash, _, _, _, _, _, file, _): + guard let resource = file.resource as? CloudDocumentMediaResource else { + return nil + } + guard let size = file.size else { + return nil + } + return """ +static let \(name) = BuiltinWallpaperData( + wallpaperId: \(id), + wallpaperAccessHash: \(accessHash), + slug: "\(slug)", + fileId: \(file.fileId.id), + fileAccessHash: \(resource.accessHash), + datacenterId: \(resource.datacenterId), + fileSize: \(size) +) +""" + default: + return nil + } + }) + } + + let _ = (combineLatest(signals) + |> map { strings -> String in + var result = "" + for case let string? in strings { + if !result.isEmpty { + result.append("\n") + } + result.append(string) + } + return result + } + |> deliverOnMainQueue).start(next: { result in + print("\(result)") + }) + } +} + +public func defaultBuiltinWallpaper(data: BuiltinWallpaperData, colors: [UInt32], intensity: Int32 = 50, rotation: Int32? = nil) -> TelegramWallpaper { + return .file( + id: data.wallpaperId, + accessHash: data.wallpaperAccessHash, + isCreator: false, + isDefault: false, + isPattern: true, + isDark: false, + slug: data.slug, + file: TelegramMediaFile( + fileId: MediaId(namespace: Namespaces.Media.CloudFile, id: data.fileId), + partialReference: nil, + resource: CloudDocumentMediaResource( + datacenterId: data.datacenterId, + fileId: data.fileId, + accessHash: data.fileAccessHash, + size: data.fileSize, + fileReference: Data(), + fileName: "pattern.tgv" + ), + previewRepresentations: [ + TelegramMediaImageRepresentation( + dimensions: PixelDimensions(width: 155, height: 320), + resource: CloudDocumentSizeMediaResource( + datacenterId: 1, + documentId: data.fileId, + accessHash: data.fileAccessHash, + sizeSpec: "m", + fileReference: Data() + ), + progressiveSizes: [], + immediateThumbnailData: nil + ) + ], + videoThumbnails: [], + immediateThumbnailData: nil, + mimeType: "application/x-tgwallpattern", + size: data.fileSize, + attributes: [ + .ImageSize(size: PixelDimensions(width: 1440, height: 2960)), + .FileName(fileName: "pattern.tgv") + ] + ), + settings: WallpaperSettings(colors: colors, intensity: intensity, rotation: rotation) + ) +} diff --git a/submodules/TelegramPresentationData/Sources/MakePresentationTheme.swift b/submodules/TelegramPresentationData/Sources/MakePresentationTheme.swift index 6bd5dbfcec..7d505441a3 100644 --- a/submodules/TelegramPresentationData/Sources/MakePresentationTheme.swift +++ b/submodules/TelegramPresentationData/Sources/MakePresentationTheme.swift @@ -19,33 +19,31 @@ public func makeDefaultPresentationTheme(reference: PresentationBuiltinThemeRefe return theme } -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 { +public func customizePresentationTheme(_ theme: PresentationTheme, editing: Bool, title: String? = nil, accentColor: UIColor?, backgroundColors: [UInt32], bubbleColors: (UIColor, UIColor?)?, wallpaper: TelegramWallpaper? = nil, baseColor: PresentationThemeBaseColor? = nil) -> PresentationTheme { + if accentColor == nil && bubbleColors == nil && backgroundColors.isEmpty && 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) + return customizeDefaultDarkPresentationTheme(theme: theme, editing: editing, title: title, accentColor: accentColor, backgroundColors: backgroundColors, bubbleColors: bubbleColors, wallpaper: wallpaper, baseColor: baseColor) case .nightAccent: - return customizeDefaultDarkTintedPresentationTheme(theme: theme, editing: editing, title: title, accentColor: accentColor, backgroundColors: backgroundColors, bubbleColors: bubbleColors, wallpaper: wallpaper) + return customizeDefaultDarkTintedPresentationTheme(theme: theme, editing: editing, title: title, accentColor: accentColor, backgroundColors: backgroundColors, bubbleColors: bubbleColors, wallpaper: wallpaper, baseColor: baseColor) } - - 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) + return customizePresentationTheme(defaultTheme, editing: true, title: title, accentColor: UIColor(argb: settings.accentColor), backgroundColors: [], 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? { +public func makePresentationTheme(mediaBox: MediaBox, themeReference: PresentationThemeReference, extendingThemeReference: PresentationThemeReference? = nil, accentColor: UIColor? = nil, backgroundColors: [UInt32] = [], bubbleColors: (UIColor, UIColor?)? = nil, wallpaper: TelegramWallpaper? = nil, baseColor: PresentationThemeBaseColor? = nil, serviceBackgroundColor: UIColor? = nil, preview: Bool = false) -> PresentationTheme? { let theme: PresentationTheme switch themeReference { case let .builtin(reference): let defaultTheme = makeDefaultPresentationTheme(reference: reference, extendingThemeReference: extendingThemeReference, serviceBackgroundColor: serviceBackgroundColor, preview: preview) - theme = customizePresentationTheme(defaultTheme, editing: true, accentColor: accentColor, backgroundColors: backgroundColors, bubbleColors: bubbleColors, wallpaper: wallpaper) + theme = customizePresentationTheme(defaultTheme, editing: true, accentColor: accentColor, backgroundColors: backgroundColors, bubbleColors: bubbleColors, wallpaper: wallpaper, baseColor: baseColor) 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, themeReference: themeReference, resolvedWallpaper: info.resolvedWallpaper) { theme = customizePresentationTheme(loadedTheme, editing: false, accentColor: accentColor, backgroundColors: backgroundColors, bubbleColors: bubbleColors, wallpaper: wallpaper) @@ -54,7 +52,7 @@ public func makePresentationTheme(mediaBox: MediaBox, themeReference: Presentati } case let .cloud(info): 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) { + if let loadedTheme = makePresentationTheme(mediaBox: mediaBox, themeReference: .builtin(PresentationBuiltinThemeReference(baseTheme: settings.baseTheme)), extendingThemeReference: themeReference, accentColor: accentColor ?? UIColor(argb: settings.accentColor), backgroundColors: [], 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 diff --git a/submodules/TelegramPresentationData/Sources/NumericFormat.swift b/submodules/TelegramPresentationData/Sources/NumericFormat.swift index 0dc29280e3..0b67ddc0db 100644 --- a/submodules/TelegramPresentationData/Sources/NumericFormat.swift +++ b/submodules/TelegramPresentationData/Sources/NumericFormat.swift @@ -37,7 +37,7 @@ public func presentationStringsFormattedNumber(_ count: Int32, _ groupingSeparat } } -public func timeIntervalString(strings: PresentationStrings, value: Int32, preferLowerValue: Bool = false, roundToNearest: Bool = false) -> String { +public func timeIntervalString(strings: PresentationStrings, value: Int32, preferLowerValue: Bool = false) -> String { if preferLowerValue { if value <= 60 { return strings.MessageTimer_Seconds(max(1, value)) @@ -69,6 +69,38 @@ public func timeIntervalString(strings: PresentationStrings, value: Int32, prefe } } +public func scheduledTimeIntervalString(strings: PresentationStrings, value: Int32, preferLowerValue: Bool = false) -> String { + if preferLowerValue { + if value <= 60 { + return strings.ScheduledIn_Seconds(max(1, value)) + } else if value <= 60 * 60 { + return strings.ScheduledIn_Minutes(max(1, value / 60)) + } else if value <= 60 * 60 * 24 { + return strings.ScheduledIn_Hours(max(1, value / (60 * 60))) + } else if value <= 60 * 60 * 24 * 7 { + return strings.ScheduledIn_Days(max(1, value / (60 * 60 * 24))) + } else if value <= 60 * 60 * 24 * 30 { + return strings.ScheduledIn_Weeks(max(1, value / (60 * 60 * 24 * 7))) + } else { + return strings.ScheduledIn_Months(max(1, value / (60 * 60 * 24 * 30))) + } + } else { + if value < 60 { + return strings.ScheduledIn_Seconds(max(1, value)) + } else if value < 60 * 60 { + return strings.ScheduledIn_Minutes(max(1, value / 60)) + } else if value < 60 * 60 * 24 { + return strings.ScheduledIn_Hours(max(1, value / (60 * 60))) + } else if value < 60 * 60 * 24 * 7 { + return strings.ScheduledIn_Days(max(1, value / (60 * 60 * 24))) + } else if value < 60 * 60 * 24 * 30 { + return strings.ScheduledIn_Weeks(max(1, value / (60 * 60 * 24 * 7))) + } else { + return strings.ScheduledIn_Months(max(1, value / (60 * 60 * 24 * 30))) + } + } +} + public func shortTimeIntervalString(strings: PresentationStrings, value: Int32) -> String { if value < 60 { return strings.MessageTimer_ShortSeconds(max(1, value)) diff --git a/submodules/TelegramPresentationData/Sources/PresentationData.swift b/submodules/TelegramPresentationData/Sources/PresentationData.swift index f3ec3dfcb2..9a007fe03d 100644 --- a/submodules/TelegramPresentationData/Sources/PresentationData.swift +++ b/submodules/TelegramPresentationData/Sources/PresentationData.swift @@ -16,6 +16,7 @@ public struct PresentationDateTimeFormat: Equatable { public let dateFormat: PresentationDateFormat public let dateSeparator: String public let dateSuffix: String + public let requiresFullYear: Bool public let decimalSeparator: String public let groupingSeparator: String @@ -24,15 +25,17 @@ public struct PresentationDateTimeFormat: Equatable { self.dateFormat = .monthFirst self.dateSeparator = "." self.dateSuffix = "" + self.requiresFullYear = false self.decimalSeparator = "." self.groupingSeparator = "." } - public init(timeFormat: PresentationTimeFormat, dateFormat: PresentationDateFormat, dateSeparator: String, dateSuffix: String, decimalSeparator: String, groupingSeparator: String) { + public init(timeFormat: PresentationTimeFormat, dateFormat: PresentationDateFormat, dateSeparator: String, dateSuffix: String, requiresFullYear: Bool, decimalSeparator: String, groupingSeparator: String) { self.timeFormat = timeFormat self.dateFormat = dateFormat self.dateSeparator = dateSeparator self.dateSuffix = dateSuffix + self.requiresFullYear = requiresFullYear self.decimalSeparator = decimalSeparator self.groupingSeparator = groupingSeparator } @@ -83,10 +86,10 @@ public final class PresentationData: Equatable { public let dateTimeFormat: PresentationDateTimeFormat public let nameDisplayOrder: PresentationPersonNameOrder public let nameSortOrder: PresentationPersonNameOrder - public let disableAnimations: Bool + public let reduceMotion: Bool public let 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) { + public init(strings: PresentationStrings, theme: PresentationTheme, autoNightModeTriggered: Bool, chatWallpaper: TelegramWallpaper, chatFontSize: PresentationFontSize, chatBubbleCorners: PresentationChatBubbleCorners, listsFontSize: PresentationFontSize, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, nameSortOrder: PresentationPersonNameOrder, reduceMotion: Bool, largeEmoji: Bool) { self.strings = strings self.theme = theme self.autoNightModeTriggered = autoNightModeTriggered @@ -97,16 +100,16 @@ public final class PresentationData: Equatable { self.dateTimeFormat = dateTimeFormat self.nameDisplayOrder = nameDisplayOrder self.nameSortOrder = nameSortOrder - self.disableAnimations = disableAnimations + self.reduceMotion = reduceMotion self.largeEmoji = largeEmoji } public func withUpdated(theme: PresentationTheme) -> PresentationData { - return PresentationData(strings: self.strings, theme: theme, autoNightModeTriggered: self.autoNightModeTriggered, chatWallpaper: self.chatWallpaper, chatFontSize: self.chatFontSize, chatBubbleCorners: self.chatBubbleCorners, listsFontSize: self.listsFontSize, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, nameSortOrder: self.nameSortOrder, disableAnimations: self.disableAnimations, largeEmoji: self.largeEmoji) + return PresentationData(strings: self.strings, theme: theme, autoNightModeTriggered: self.autoNightModeTriggered, chatWallpaper: self.chatWallpaper, chatFontSize: self.chatFontSize, chatBubbleCorners: self.chatBubbleCorners, listsFontSize: self.listsFontSize, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, nameSortOrder: self.nameSortOrder, reduceMotion: self.reduceMotion, largeEmoji: self.largeEmoji) } 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.chatFontSize == rhs.chatFontSize && lhs.chatBubbleCorners == rhs.chatBubbleCorners && lhs.listsFontSize == rhs.listsFontSize && 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.reduceMotion == rhs.reduceMotion && lhs.largeEmoji == rhs.largeEmoji } } @@ -157,13 +160,17 @@ private func currentDateTimeFormat() -> PresentationDateTimeFormat { let dateFormat: PresentationDateFormat var dateSeparator = "/" var dateSuffix = "" + var requiresFullYear = false if let dateString = DateFormatter.dateFormat(fromTemplate: "MdY", options: 0, locale: locale) { for separator in [". ", ".", "/", "-", "/"] { if dateString.contains(separator) { if separator == ". " { dateSuffix = "." + dateSeparator = "." + requiresFullYear = true + } else { + dateSeparator = separator } - dateSeparator = separator break } } @@ -178,7 +185,7 @@ private func currentDateTimeFormat() -> PresentationDateTimeFormat { let decimalSeparator = locale.decimalSeparator ?? "." let groupingSeparator = locale.groupingSeparator ?? "" - return PresentationDateTimeFormat(timeFormat: timeFormat, dateFormat: dateFormat, dateSeparator: dateSeparator, dateSuffix: dateSuffix, decimalSeparator: decimalSeparator, groupingSeparator: groupingSeparator) + return PresentationDateTimeFormat(timeFormat: timeFormat, dateFormat: dateFormat, dateSeparator: dateSeparator, dateSuffix: dateSuffix, requiresFullYear: requiresFullYear, decimalSeparator: decimalSeparator, groupingSeparator: groupingSeparator) } private func currentPersonNameSortOrder() -> PresentationPersonNameOrder { @@ -285,7 +292,7 @@ public func currentPresentationDataAndSettings(accountManager: AccountManager, s } let effectiveColors = themeSettings.themeSpecificAccentColors[effectiveTheme.index] - let theme = makePresentationTheme(mediaBox: accountManager.mediaBox, themeReference: effectiveTheme, accentColor: effectiveColors?.color, bubbleColors: effectiveColors?.customBubbleColors) ?? defaultPresentationTheme + let theme = makePresentationTheme(mediaBox: accountManager.mediaBox, themeReference: effectiveTheme, accentColor: effectiveColors?.color, bubbleColors: effectiveColors?.customBubbleColors, baseColor: effectiveColors?.baseColor) ?? defaultPresentationTheme let effectiveChatWallpaper: TelegramWallpaper = (themeSettings.themeSpecificChatWallpapers[coloredThemeIndex(reference: effectiveTheme, accentColor: effectiveColors)] ?? themeSettings.themeSpecificChatWallpapers[effectiveTheme.index]) ?? theme.chat.defaultWallpaper @@ -304,7 +311,7 @@ public func currentPresentationDataAndSettings(accountManager: AccountManager, s 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) + return InitialPresentationDataAndSettings(presentationData: PresentationData(strings: stringsValue, theme: theme, autoNightModeTriggered: autoNightModeTriggered, chatWallpaper: effectiveChatWallpaper, chatFontSize: chatFontSize, chatBubbleCorners: chatBubbleCorners, listsFontSize: listsFontSize, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, nameSortOrder: nameSortOrder, reduceMotion: themeSettings.reduceMotion, largeEmoji: themeSettings.largeEmoji), automaticMediaDownloadSettings: automaticMediaDownloadSettings, autodownloadSettings: autodownloadSettings, callListSettings: callListSettings, inAppNotificationSettings: inAppNotificationSettings, mediaInputSettings: mediaInputSettings, experimentalUISettings: experimentalUISettings) } } @@ -437,9 +444,13 @@ public func serviceColor(for wallpaper: (TelegramWallpaper, UIImage?)) -> UIColo 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 let .gradient(_, colors, _): + if colors.count == 2 { + let mixedColor = UIColor(argb: colors[0]).mixedWith(UIColor(argb: colors[1]), alpha: 0.5) + return serviceColor(with: mixedColor) + } else { + return UIColor(rgb: 0x000000, alpha: 0.3) + } case .image: if let image = wallpaper.1 { return serviceColor(with: averageColor(from: image)) @@ -448,10 +459,10 @@ public func serviceColor(for wallpaper: (TelegramWallpaper, UIImage?)) -> UIColo } 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) + if file.settings.colors.count >= 1 && file.settings.colors.count <= 2 { + var mixedColor = UIColor(argb: file.settings.colors[0]) + if file.settings.colors.count >= 2 { + mixedColor = mixedColor.mixedWith(UIColor(argb: file.settings.colors[1]), alpha: 0.5) } return serviceColor(with: mixedColor) } else { @@ -489,12 +500,17 @@ public func chatServiceBackgroundColor(wallpaper: TelegramWallpaper, mediaBox: M } else { switch wallpaper { case .builtin: - return .single(UIColor(rgb: 0x748391, alpha: 0.45)) + return .single(UIColor(rgb: 0x000000, alpha: 0.2)) case let .color(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 .gradient(_, colors, _): + if colors.count == 2 { + let mixedColor = UIColor(argb: colors[0]).mixedWith(UIColor(argb: colors[1]), alpha: 0.5) + return .single( + serviceColor(with: mixedColor)) + } else { + return .single(UIColor(rgb: 0x000000, alpha: 0.3)) + } case let .image(representations, _): if let largest = largestImageRepresentation(representations) { return Signal { subscriber in @@ -517,10 +533,10 @@ public func chatServiceBackgroundColor(wallpaper: TelegramWallpaper, mediaBox: M } case let .file(file): if wallpaper.isPattern { - if let color = file.settings.color { - var mixedColor = UIColor(argb: color) - if let bottomColor = file.settings.bottomColor { - mixedColor = mixedColor.mixedWith(UIColor(rgb: bottomColor), alpha: 0.5) + if file.settings.colors.count >= 1 && file.settings.colors.count <= 2 { + var mixedColor = UIColor(argb: file.settings.colors[0]) + if file.settings.colors.count >= 2 { + mixedColor = mixedColor.mixedWith(UIColor(argb: file.settings.colors[1]), alpha: 0.5) } return .single(serviceColor(with: mixedColor)) } else { @@ -567,7 +583,7 @@ public func updatedPresentationData(accountManager: AccountManager, applicationI if let themeSpecificWallpaper = themeSpecificWallpaper { currentWallpaper = themeSpecificWallpaper } else { - let theme = makePresentationTheme(mediaBox: accountManager.mediaBox, themeReference: themeSettings.theme, accentColor: currentColors?.color, bubbleColors: currentColors?.customBubbleColors, wallpaper: currentColors?.wallpaper) ?? defaultPresentationTheme + let theme = makePresentationTheme(mediaBox: accountManager.mediaBox, themeReference: themeSettings.theme, accentColor: currentColors?.color, bubbleColors: currentColors?.customBubbleColors, wallpaper: currentColors?.wallpaper, baseColor: currentColors?.baseColor) ?? defaultPresentationTheme currentWallpaper = theme.chat.defaultWallpaper } @@ -603,7 +619,7 @@ public func updatedPresentationData(accountManager: AccountManager, applicationI effectiveColors = nil } - let themeValue = makePresentationTheme(mediaBox: accountManager.mediaBox, themeReference: effectiveTheme, accentColor: effectiveColors?.color, bubbleColors: effectiveColors?.customBubbleColors, wallpaper: effectiveColors?.wallpaper, serviceBackgroundColor: serviceBackgroundColor) ?? defaultPresentationTheme + let themeValue = makePresentationTheme(mediaBox: accountManager.mediaBox, themeReference: effectiveTheme, accentColor: effectiveColors?.color, bubbleColors: effectiveColors?.customBubbleColors, wallpaper: effectiveColors?.wallpaper, baseColor: effectiveColors?.baseColor, serviceBackgroundColor: serviceBackgroundColor) ?? defaultPresentationTheme if autoNightModeTriggered && !switchedToNightModeWallpaper { switch effectiveChatWallpaper { @@ -639,7 +655,7 @@ public func updatedPresentationData(accountManager: AccountManager, applicationI 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) + return PresentationData(strings: stringsValue, theme: themeValue, autoNightModeTriggered: autoNightModeTriggered, chatWallpaper: effectiveChatWallpaper, chatFontSize: chatFontSize, chatBubbleCorners: chatBubbleCorners, listsFontSize: listsFontSize, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, nameSortOrder: nameSortOrder, reduceMotion: themeSettings.reduceMotion, largeEmoji: themeSettings.largeEmoji) } } else { return .complete() @@ -674,19 +690,19 @@ public func defaultPresentationData() -> PresentationData { 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) + return PresentationData(strings: defaultPresentationStrings, theme: defaultPresentationTheme, autoNightModeTriggered: false, chatWallpaper: defaultPresentationTheme.chat.defaultWallpaper, chatFontSize: chatFontSize, chatBubbleCorners: chatBubbleCorners, listsFontSize: listsFontSize, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, nameSortOrder: nameSortOrder, reduceMotion: themeSettings.reduceMotion, 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) + 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, reduceMotion: self.reduceMotion, 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) + 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, reduceMotion: self.reduceMotion, 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) + 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, reduceMotion: self.reduceMotion, largeEmoji: self.largeEmoji) } } diff --git a/submodules/TelegramPresentationData/Sources/PresentationStrings.swift b/submodules/TelegramPresentationData/Sources/PresentationStrings.swift index 6993d86a4e..e701e90889 100644 --- a/submodules/TelegramPresentationData/Sources/PresentationStrings.swift +++ b/submodules/TelegramPresentationData/Sources/PresentationStrings.swift @@ -208,5441 +208,5680 @@ public final class PresentationStrings: Equatable { public var ChatListFolder_CategoryNonContacts: String { return self._s[18]! } public var Gif_NoGifsPlaceholder: String { return self._s[19]! } public var Conversation_ShareInlineBotLocationConfirmation: String { return self._s[20]! } - public var AutoNightTheme_ScheduleSection: String { return self._s[21]! } - public var Map_LiveLocationTitle: String { return self._s[22]! } - public var Passport_PasswordCreate: String { return self._s[23]! } - public var Settings_ProxyConnected: String { return self._s[24]! } + public func VoiceChat_StatusLateBy(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[21]!, self._r[21]!, [_0]) + } + public var AutoNightTheme_ScheduleSection: String { return self._s[22]! } + public var Map_LiveLocationTitle: String { return self._s[23]! } + public var Passport_PasswordCreate: String { return self._s[24]! } + public var Settings_ProxyConnected: String { return self._s[25]! } public func PUSH_PINNED_TEXT(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[25]!, self._r[25]!, [_1, _2]) + return formatWithArgumentRanges(self._s[26]!, self._r[26]!, [_1, _2]) } - public var Channel_Management_LabelOwner: String { return self._s[26]! } - public var ApplyLanguage_ApplySuccess: String { return self._s[27]! } - public var Group_Setup_HistoryHidden: String { return self._s[28]! } - public var Month_ShortNovember: String { return self._s[29]! } - public var Call_ReportIncludeLog: String { return self._s[30]! } - public var ChatList_RemoveFolder: String { return self._s[31]! } - public var PrivacyPhoneNumberSettings_CustomHelp: String { return self._s[32]! } - public var Appearance_ThemePreview_ChatList_5_Text: String { return self._s[33]! } - public var Checkout_Receipt_Title: String { return self._s[34]! } + public var Channel_Management_LabelOwner: String { return self._s[27]! } + public var ApplyLanguage_ApplySuccess: String { return self._s[28]! } + public var Group_Setup_HistoryHidden: String { return self._s[29]! } + public var ImportStickerPack_ChooseLinkDescription: String { return self._s[30]! } + public var Month_ShortNovember: String { return self._s[31]! } + public var ImportStickerPack_CheckingLink: String { return self._s[32]! } + public var Call_ReportIncludeLog: String { return self._s[33]! } + public var ChatList_RemoveFolder: String { return self._s[34]! } + public var PrivacyPhoneNumberSettings_CustomHelp: String { return self._s[35]! } + public var Appearance_ThemePreview_ChatList_5_Text: String { return self._s[36]! } + public var Checkout_Receipt_Title: String { return self._s[37]! } public func Conversation_ClearChatConfirmation(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[35]!, self._r[35]!, [_0]) + return formatWithArgumentRanges(self._s[38]!, self._r[38]!, [_0]) } - public var AuthSessions_LogOutApplicationsHelp: String { return self._s[36]! } - public var SearchImages_Title: String { return self._s[37]! } - public var Notification_PaymentSent: String { return self._s[38]! } - public var Appearance_TintAllColors: String { return self._s[39]! } - public var Group_Setup_TypePublicHelp: String { return self._s[40]! } - public var ChatSettings_Cache: String { return self._s[41]! } - public var InviteLink_RevokedLinks: String { return self._s[42]! } - public var Login_InvalidLastNameError: String { return self._s[43]! } - public var PeerInfo_PaneMedia: String { return self._s[44]! } - public var InviteLink_Revoked: String { return self._s[45]! } - public var StickerPacks_ActionShare: String { return self._s[46]! } - public var GroupPermission_PermissionGloballyDisabled: String { return self._s[47]! } - public var LiveLocationUpdated_JustNow: String { return self._s[48]! } + public var AuthSessions_LogOutApplicationsHelp: String { return self._s[39]! } + public var SearchImages_Title: String { return self._s[40]! } + public var Notification_PaymentSent: String { return self._s[41]! } + public var Appearance_TintAllColors: String { return self._s[42]! } + public var Group_Setup_TypePublicHelp: String { return self._s[43]! } + public var ChatSettings_Cache: String { return self._s[44]! } + public var InviteLink_RevokedLinks: String { return self._s[45]! } + public var Login_InvalidLastNameError: String { return self._s[46]! } + public var PeerInfo_PaneMedia: String { return self._s[47]! } + public var InviteLink_Revoked: String { return self._s[48]! } + public var StickerPacks_ActionShare: String { return self._s[49]! } + public var GroupPermission_PermissionGloballyDisabled: String { return self._s[50]! } + public var LiveLocationUpdated_JustNow: String { return self._s[51]! } public func Map_LiveLocationPrivateDescription(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[49]!, self._r[49]!, [_0]) + return formatWithArgumentRanges(self._s[52]!, self._r[52]!, [_0]) } - public var Channel_Info_Members: String { return self._s[50]! } + public var Channel_Info_Members: String { return self._s[53]! } public func Channel_CommentsGroup_HeaderSet(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[51]!, self._r[51]!, [_0]) + return formatWithArgumentRanges(self._s[54]!, self._r[54]!, [_0]) } - public var Common_edit: String { return self._s[52]! } - public var ChatList_DeleteSavedMessagesConfirmationText: String { return self._s[54]! } - public var OldChannels_GroupEmptyFormat: String { return self._s[55]! } + public var Common_edit: String { return self._s[55]! } + public var ChatList_DeleteSavedMessagesConfirmationText: String { return self._s[57]! } + public var OldChannels_GroupEmptyFormat: String { return self._s[58]! } public func PUSH_PINNED_AUDIO(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[56]!, self._r[56]!, [_1]) + return formatWithArgumentRanges(self._s[59]!, self._r[59]!, [_1]) } - public var Passport_DiscardMessageAction: String { return self._s[57]! } - public var VoiceChat_StopRecordingTitle: String { return self._s[58]! } - public var Passport_FieldOneOf_FinalDelimeter: String { return self._s[59]! } - public var Stickers_SuggestNone: String { return self._s[60]! } + public var Passport_DiscardMessageAction: String { return self._s[60]! } + public var VoiceChat_StopRecordingTitle: String { return self._s[61]! } + public var Passport_FieldOneOf_FinalDelimeter: String { return self._s[62]! } + public var Stickers_SuggestNone: String { return self._s[63]! } public func Channel_AdminLog_JoinedViaInviteLink(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[61]!, self._r[61]!, [_1, _2]) + return formatWithArgumentRanges(self._s[64]!, self._r[64]!, [_1, _2]) } - public var Channel_AdminLog_CanPinMessages: String { return self._s[62]! } - public var Stickers_Search: String { return self._s[64]! } - public var Passport_Identity_EditPersonalDetails: String { return self._s[65]! } - public var NotificationSettings_ShowNotificationsAllAccounts: String { return self._s[66]! } - public var Login_ContinueWithLocalization: String { return self._s[67]! } - public var Privacy_ProfilePhoto_NeverShareWith_Title: String { return self._s[68]! } - public var TextFormat_Italic: String { return self._s[70]! } - public var ChatList_Search_NoResultsFitlerLinks: String { return self._s[72]! } - public var Stickers_GroupChooseStickerPack: String { return self._s[73]! } - public var Notification_MessageLifetime1w: String { return self._s[74]! } - public var Channel_Management_AddModerator: String { return self._s[75]! } - public var Conversation_UnsupportedMediaPlaceholder: String { return self._s[76]! } - public var Gif_Search: String { return self._s[77]! } - public var Checkout_ErrorGeneric: String { return self._s[78]! } - public var Conversation_ContextMenuSendMessage: String { return self._s[79]! } - public var Map_SetThisLocation: String { return self._s[80]! } - public var Notifications_ExceptionsDefaultSound: String { return self._s[81]! } - public var PrivacySettings_AutoArchiveInfo: String { return self._s[82]! } - public var Stats_NotificationsTitle: String { return self._s[83]! } - public var Conversation_ClearSecretHistory: String { return self._s[85]! } + public var Channel_AdminLog_CanPinMessages: String { return self._s[65]! } + public var Stickers_Search: String { return self._s[67]! } + public var Passport_Identity_EditPersonalDetails: String { return self._s[68]! } + public var NotificationSettings_ShowNotificationsAllAccounts: String { return self._s[69]! } + public var Login_ContinueWithLocalization: String { return self._s[70]! } + public var Privacy_ProfilePhoto_NeverShareWith_Title: String { return self._s[71]! } + public var TextFormat_Italic: String { return self._s[73]! } + public var ChatList_Search_NoResultsFitlerLinks: String { return self._s[75]! } + public var Stickers_GroupChooseStickerPack: String { return self._s[76]! } + public var Notification_MessageLifetime1w: String { return self._s[77]! } + public var Channel_Management_AddModerator: String { return self._s[78]! } + public var Conversation_UnsupportedMediaPlaceholder: String { return self._s[79]! } + public var Gif_Search: String { return self._s[80]! } + public var Checkout_ErrorGeneric: String { return self._s[81]! } + public var Conversation_ContextMenuSendMessage: String { return self._s[82]! } + public var Map_SetThisLocation: String { return self._s[83]! } + public var Notifications_ExceptionsDefaultSound: String { return self._s[84]! } + public var PrivacySettings_AutoArchiveInfo: String { return self._s[85]! } + public var Stats_NotificationsTitle: String { return self._s[86]! } + public var Conversation_ClearSecretHistory: String { return self._s[88]! } public func Conversation_DeleteAllMessagesInChat(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[86]!, self._r[86]!, [_0]) + return formatWithArgumentRanges(self._s[89]!, self._r[89]!, [_0]) } public func Notification_CallFormat(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[87]!, self._r[87]!, [_1, _2]) + return formatWithArgumentRanges(self._s[90]!, self._r[90]!, [_1, _2]) } - public var ChatListFolder_DiscardDiscard: String { return self._s[88]! } - public var PrivacyLastSeenSettings_AlwaysShareWith: String { return self._s[89]! } - public var Contacts_InviteFriends: String { return self._s[90]! } - public var Group_LinkedChannel: String { return self._s[91]! } - public var ChatList_DeleteForAllMembers: String { return self._s[92]! } - public var Notification_PassportValuePhone: String { return self._s[94]! } + public var ChatListFolder_DiscardDiscard: String { return self._s[91]! } + public var PrivacyLastSeenSettings_AlwaysShareWith: String { return self._s[92]! } + public var Contacts_InviteFriends: String { return self._s[93]! } + public var Group_LinkedChannel: String { return self._s[94]! } + public var ChatList_DeleteForAllMembers: String { return self._s[95]! } + public var Notification_PassportValuePhone: String { return self._s[97]! } public func InviteText_SingleContact(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[95]!, self._r[95]!, [_0]) + return formatWithArgumentRanges(self._s[98]!, self._r[98]!, [_0]) } - public var UserInfo_BotHelp: String { return self._s[97]! } - public var Passport_Identity_MainPage: String { return self._s[99]! } - public var LogoutOptions_ContactSupportText: String { return self._s[100]! } + public var UserInfo_BotHelp: String { return self._s[100]! } + public var Passport_Identity_MainPage: String { return self._s[102]! } + public var LogoutOptions_ContactSupportText: String { return self._s[103]! } public func VoiceOver_Chat_Title(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[101]!, self._r[101]!, [_0]) + return formatWithArgumentRanges(self._s[104]!, self._r[104]!, [_0]) } - public var StickerPack_ShowStickers: String { return self._s[103]! } - public var AttachmentMenu_PhotoOrVideo: String { return self._s[104]! } - public var Map_Satellite: String { return self._s[105]! } - public var Passport_Identity_MainPageHelp: String { return self._s[106]! } - public var Profile_About: String { return self._s[108]! } - public var Group_Setup_TypePrivate: String { return self._s[109]! } - public var Notifications_ChannelNotifications: String { return self._s[110]! } - public var Call_VoiceOver_VoiceCallIncoming: String { return self._s[111]! } + public var StickerPack_ShowStickers: String { return self._s[106]! } + public var AttachmentMenu_PhotoOrVideo: String { return self._s[107]! } + public var Map_Satellite: String { return self._s[108]! } + public var Passport_Identity_MainPageHelp: String { return self._s[109]! } + public var Profile_About: String { return self._s[111]! } + public var Group_Setup_TypePrivate: String { return self._s[112]! } + public func ScheduleVoiceChat_ChannelText(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[113]!, self._r[113]!, [_0]) + } + public var Notifications_ChannelNotifications: String { return self._s[114]! } + public var Call_VoiceOver_VoiceCallIncoming: String { return self._s[115]! } public func Login_WillCallYou(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[112]!, self._r[112]!, [_0]) + return formatWithArgumentRanges(self._s[116]!, self._r[116]!, [_0]) } - public var WallpaperPreview_Motion: String { return self._s[113]! } - public var Message_VideoMessage: String { return self._s[114]! } - public var SharedMedia_CategoryOther: String { return self._s[115]! } - public var Passport_FieldIdentityUploadHelp: String { return self._s[116]! } - public var PUSH_REMINDER_TITLE: String { return self._s[117]! } - public var Appearance_ThemePreview_Chat_3_Text: String { return self._s[119]! } - public var Login_ResetAccountProtected_Reset: String { return self._s[121]! } - public var Passport_Identity_TypeInternalPassportUploadScan: String { return self._s[122]! } + public var WallpaperPreview_Motion: String { return self._s[117]! } + public var Message_VideoMessage: String { return self._s[118]! } + public var SharedMedia_CategoryOther: String { return self._s[120]! } + public var Passport_FieldIdentityUploadHelp: String { return self._s[121]! } + public var PUSH_REMINDER_TITLE: String { return self._s[122]! } + public var Appearance_ThemePreview_Chat_3_Text: String { return self._s[124]! } + public var Login_ResetAccountProtected_Reset: String { return self._s[126]! } + public var Passport_Identity_TypeInternalPassportUploadScan: String { return self._s[127]! } public func Location_ProximityNotification_Notify(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[123]!, self._r[123]!, [_0]) + return formatWithArgumentRanges(self._s[128]!, self._r[128]!, [_0]) } - public var ChatList_PeerTypeContact: String { return self._s[124]! } - public var Stickers_SuggestAll: String { return self._s[126]! } - public var EmptyGroupInfo_Line3: String { return self._s[127]! } - public var Login_InvalidPhoneError: String { return self._s[128]! } - public var MediaPicker_GroupDescription: String { return self._s[129]! } - public var NetworkUsageSettings_MediaDocumentDataSection: String { return self._s[130]! } - public var Conversation_PrivateChannelTimeLimitedAlertText: String { return self._s[131]! } - public var PrivateDataSettings_Title: String { return self._s[132]! } - public var SecretChat_Title: String { return self._s[133]! } - public var Privacy_ChatsTitle: String { return self._s[134]! } - public var EditProfile_NameAndPhotoHelp: String { return self._s[135]! } - public var Watch_MessageView_Forward: String { return self._s[137]! } - public var ChannelMembers_WhoCanAddMembers_AllMembers: String { return self._s[138]! } + public var ChatList_PeerTypeContact: String { return self._s[129]! } + public var Stickers_SuggestAll: String { return self._s[131]! } + public var EmptyGroupInfo_Line3: String { return self._s[132]! } + public var Login_InvalidPhoneError: String { return self._s[133]! } + public var MediaPicker_GroupDescription: String { return self._s[134]! } + public func UserInfo_LinkForwardTooltip_Chat_One(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[135]!, self._r[135]!, [_0]) + } + public var NetworkUsageSettings_MediaDocumentDataSection: String { return self._s[136]! } + public var Conversation_PrivateChannelTimeLimitedAlertText: String { return self._s[137]! } + public var PrivateDataSettings_Title: String { return self._s[138]! } + public var SecretChat_Title: String { return self._s[139]! } + public var Privacy_ChatsTitle: String { return self._s[140]! } + public var EditProfile_NameAndPhotoHelp: String { return self._s[141]! } + public var Watch_MessageView_Forward: String { return self._s[143]! } + public var ChannelMembers_WhoCanAddMembers_AllMembers: String { return self._s[144]! } public func PUSH_PINNED_QUIZ(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[139]!, self._r[139]!, [_1, _2]) + return formatWithArgumentRanges(self._s[145]!, self._r[145]!, [_1, _2]) } public func Channel_AdminLog_EndedVoiceChat(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[140]!, self._r[140]!, [_1]) + return formatWithArgumentRanges(self._s[146]!, self._r[146]!, [_1]) } - public var InviteLink_ExpiredLink: String { return self._s[141]! } - public var PhotoEditor_DiscardChanges: String { return self._s[142]! } - public var SocksProxySetup_AdNoticeHelp: String { return self._s[143]! } - public var Date_DialogDateFormat: String { return self._s[144]! } - public var SettingsSearch_Synonyms_Proxy_Title: String { return self._s[145]! } - public var Notifications_AlertTones: String { return self._s[146]! } - public var Permissions_SiriAllow_v0: String { return self._s[147]! } - public var Tour_StartButton: String { return self._s[148]! } - public var Stats_InstantViewInteractionsTitle: String { return self._s[149]! } - public var UserInfo_ScamUserWarning: String { return self._s[152]! } - public var NotificationsSound_Chime: String { return self._s[153]! } - public var Update_Skip: String { return self._s[154]! } + public var InviteLink_ExpiredLink: String { return self._s[147]! } + public var PhotoEditor_DiscardChanges: String { return self._s[148]! } + public var SocksProxySetup_AdNoticeHelp: String { return self._s[149]! } + public var Date_DialogDateFormat: String { return self._s[150]! } + public var SettingsSearch_Synonyms_Proxy_Title: String { return self._s[151]! } + public var Notifications_AlertTones: String { return self._s[152]! } + public var Permissions_SiriAllow_v0: String { return self._s[153]! } + public var Tour_StartButton: String { return self._s[154]! } + public var Stats_InstantViewInteractionsTitle: String { return self._s[155]! } + public var UserInfo_ScamUserWarning: String { return self._s[158]! } + public var NotificationsSound_Chime: String { return self._s[159]! } + public var Update_Skip: String { return self._s[160]! } public func ChannelInfo_ChannelForbidden(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[155]!, self._r[155]!, [_0]) + return formatWithArgumentRanges(self._s[161]!, self._r[161]!, [_0]) } - public var SettingsSearch_Synonyms_EditProfile_PhoneNumber: String { return self._s[156]! } - public var Notifications_PermissionsTitle: String { return self._s[157]! } - public var Channel_AdminLog_BanSendMedia: String { return self._s[158]! } - public var Notifications_Badge_CountUnreadMessages: String { return self._s[159]! } - public var Appearance_AppIcon: String { return self._s[160]! } - public var Passport_Identity_FilesUploadNew: String { return self._s[161]! } + public var SettingsSearch_Synonyms_EditProfile_PhoneNumber: String { return self._s[162]! } + public var Notifications_PermissionsTitle: String { return self._s[163]! } + public var Channel_AdminLog_BanSendMedia: String { return self._s[164]! } + public var Notifications_Badge_CountUnreadMessages: String { return self._s[165]! } + public var Appearance_AppIcon: String { return self._s[166]! } + public var Passport_Identity_FilesUploadNew: String { return self._s[167]! } public func Passport_Email_UseTelegramEmail(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[162]!, self._r[162]!, [_0]) + return formatWithArgumentRanges(self._s[168]!, self._r[168]!, [_0]) } - public var CreatePoll_QuizTitle: String { return self._s[163]! } - public var DialogList_DeleteConversationConfirmation: String { return self._s[164]! } - public var NotificationsSound_Calypso: String { return self._s[165]! } - public var ChannelMembers_GroupAdminsTitle: String { return self._s[166]! } - public var Checkout_NewCard_PaymentCard: String { return self._s[167]! } - public var Wallpaper_SetCustomBackground: String { return self._s[169]! } - public var Conversation_ContextMenuOpenProfile: String { return self._s[170]! } + public var CreatePoll_QuizTitle: String { return self._s[169]! } + public var DialogList_DeleteConversationConfirmation: String { return self._s[170]! } + public var NotificationsSound_Calypso: String { return self._s[171]! } + public var ChannelMembers_GroupAdminsTitle: String { return self._s[173]! } + public var Checkout_NewCard_PaymentCard: String { return self._s[174]! } + public var Wallpaper_SetCustomBackground: String { return self._s[176]! } + public var Conversation_ContextMenuOpenProfile: String { return self._s[177]! } public func PUSH_MESSAGE_VIDEO_SECRET(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[172]!, self._r[172]!, [_1]) + return formatWithArgumentRanges(self._s[179]!, self._r[179]!, [_1]) } - public var AuthSessions_Terminate: String { return self._s[173]! } - public var ShareFileTip_CloseTip: String { return self._s[174]! } - public var ChatSettings_DownloadInBackgroundInfo: String { return self._s[175]! } - public var Channel_Moderator_AccessLevelRevoke: String { return self._s[176]! } - public var Channel_AdminLogFilter_EventsDeletedMessages: String { return self._s[177]! } - public var Passport_Language_fr: String { return self._s[178]! } + public var AuthSessions_Terminate: String { return self._s[180]! } + public var ShareFileTip_CloseTip: String { return self._s[181]! } + public var ChatSettings_DownloadInBackgroundInfo: String { return self._s[182]! } + public var Channel_Moderator_AccessLevelRevoke: String { return self._s[183]! } + public var Channel_AdminLogFilter_EventsDeletedMessages: String { return self._s[184]! } + public var Passport_Language_fr: String { return self._s[185]! } public func Watch_Time_ShortTodayAt(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[180]!, self._r[180]!, [_0]) + return formatWithArgumentRanges(self._s[187]!, self._r[187]!, [_0]) } - public var Passport_Identity_TypeIdentityCard: String { return self._s[181]! } - public var VoiceChat_MuteForMe: String { return self._s[182]! } + public var Passport_Identity_TypeIdentityCard: String { return self._s[188]! } + public var VoiceChat_MuteForMe: String { return self._s[189]! } public func Conversation_OpenBotLinkAllowMessages(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[183]!, self._r[183]!, [_0]) + return formatWithArgumentRanges(self._s[190]!, self._r[190]!, [_0]) } - public var ReportPeer_ReasonCopyright: String { return self._s[184]! } - public var Permissions_PeopleNearbyText_v0: String { return self._s[186]! } - public var Channel_Stickers_NotFoundHelp: String { return self._s[187]! } - public var Passport_Identity_AddDriversLicense: String { return self._s[188]! } - public var AutoDownloadSettings_AutodownloadFiles: String { return self._s[189]! } - public var Permissions_SiriAllowInSettings_v0: String { return self._s[190]! } + public var ReportPeer_ReasonCopyright: String { return self._s[191]! } + public var Permissions_PeopleNearbyText_v0: String { return self._s[193]! } + public var Channel_Stickers_NotFoundHelp: String { return self._s[194]! } + public var Passport_Identity_AddDriversLicense: String { return self._s[195]! } + public var AutoDownloadSettings_AutodownloadFiles: String { return self._s[196]! } + public var Permissions_SiriAllowInSettings_v0: String { return self._s[197]! } public func Conversation_ForwardTooltip_ManyChats_Many(_ _0: String, _ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[191]!, self._r[191]!, [_0, _1]) + return formatWithArgumentRanges(self._s[198]!, self._r[198]!, [_0, _1]) } - public var ApplyLanguage_ChangeLanguageTitle: String { return self._s[192]! } - public var Map_LocatingError: String { return self._s[194]! } - public var ChatSettings_AutoDownloadSettings_TypePhoto: String { return self._s[195]! } + public var ApplyLanguage_ChangeLanguageTitle: String { return self._s[199]! } + public var Map_LocatingError: String { return self._s[201]! } + public var ChatSettings_AutoDownloadSettings_TypePhoto: String { return self._s[202]! } public func VoiceOver_Chat_MusicFrom(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[197]!, self._r[197]!, [_0]) + return formatWithArgumentRanges(self._s[204]!, self._r[204]!, [_0]) } public func Contacts_AccessDeniedHelpLandscape(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[198]!, self._r[198]!, [_0]) - } - public var Channel_AdminLog_EmptyFilterText: String { return self._s[199]! } - public var Login_SmsRequestState2: String { return self._s[200]! } - public var Conversation_Unmute: String { return self._s[202]! } - public var TwoFactorSetup_Intro_Text: String { return self._s[203]! } - public var Channel_AdminLog_BanSendMessages: String { return self._s[204]! } - public func Channel_Management_RemovedBy(_ _0: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(self._s[205]!, self._r[205]!, [_0]) } - public var AccessDenied_LocationDenied: String { return self._s[206]! } - public var Share_AuthTitle: String { return self._s[207]! } - public var Month_ShortAugust: String { return self._s[208]! } + public var Channel_AdminLog_EmptyFilterText: String { return self._s[206]! } + public var Login_SmsRequestState2: String { return self._s[207]! } + public var Conversation_Unmute: String { return self._s[209]! } + public var TwoFactorSetup_Intro_Text: String { return self._s[210]! } + public var Channel_AdminLog_BanSendMessages: String { return self._s[211]! } + public func Channel_Management_RemovedBy(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[212]!, self._r[212]!, [_0]) + } + public var AccessDenied_LocationDenied: String { return self._s[213]! } + public var Share_AuthTitle: String { return self._s[214]! } + public var Month_ShortAugust: String { return self._s[215]! } public func Notification_PinnedDeletedMessage(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[209]!, self._r[209]!, [_0]) + return formatWithArgumentRanges(self._s[216]!, self._r[216]!, [_0]) } - public var Channel_BanUser_PermissionSendMedia: String { return self._s[210]! } - public var SettingsSearch_Synonyms_Data_DownloadInBackground: String { return self._s[211]! } + public var Channel_BanUser_PermissionSendMedia: String { return self._s[217]! } + public var SettingsSearch_Synonyms_Data_DownloadInBackground: String { return self._s[218]! } public func PUSH_CONTACT_JOINED(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[212]!, self._r[212]!, [_1]) + return formatWithArgumentRanges(self._s[219]!, self._r[219]!, [_1]) } - public var WallpaperSearch_ColorTitle: String { return self._s[214]! } - public var Wallpaper_Search: String { return self._s[215]! } - public var ClearCache_StorageUsage: String { return self._s[216]! } - public var CreatePoll_TextPlaceholder: String { return self._s[217]! } - public var Conversation_EditingMessagePanelTitle: String { return self._s[218]! } - public var Channel_EditAdmin_PermissionBanUsers: String { return self._s[219]! } - public var OldChannels_NoticeCreateText: String { return self._s[220]! } - public var ProfilePhoto_MainVideo: String { return self._s[221]! } - public var VoiceChat_StatusListening: String { return self._s[222]! } - public var InviteLink_DeleteLinkAlert_Text: String { return self._s[223]! } - public var UserInfo_NotificationsDisabled: String { return self._s[224]! } - public var Map_Unknown: String { return self._s[225]! } - public var Notifications_MessageNotificationsAlert: String { return self._s[226]! } - public var Conversation_StopQuiz: String { return self._s[227]! } - public var Checkout_LiabilityAlertTitle: String { return self._s[228]! } + public var WallpaperSearch_ColorTitle: String { return self._s[221]! } + public var Wallpaper_Search: String { return self._s[222]! } + public var ClearCache_StorageUsage: String { return self._s[223]! } + public var CreatePoll_TextPlaceholder: String { return self._s[224]! } + public var Conversation_EditingMessagePanelTitle: String { return self._s[225]! } + public var Channel_EditAdmin_PermissionBanUsers: String { return self._s[226]! } + public var OldChannels_NoticeCreateText: String { return self._s[227]! } + public var ProfilePhoto_MainVideo: String { return self._s[228]! } + public var VoiceChat_StatusListening: String { return self._s[229]! } + public var InviteLink_DeleteLinkAlert_Text: String { return self._s[230]! } + public var UserInfo_NotificationsDisabled: String { return self._s[231]! } + public var Map_Unknown: String { return self._s[232]! } + public var Notifications_MessageNotificationsAlert: String { return self._s[233]! } + public var Conversation_StopQuiz: String { return self._s[234]! } + public var Checkout_LiabilityAlertTitle: String { return self._s[235]! } public func Username_UsernameIsAvailable(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[229]!, self._r[229]!, [_0]) + return formatWithArgumentRanges(self._s[236]!, self._r[236]!, [_0]) } - public var CreatePoll_OptionPlaceholder: String { return self._s[230]! } - public var Conversation_RestrictedStickers: String { return self._s[231]! } - public var MemberSearch_BotSection: String { return self._s[233]! } - public var Channel_Management_AddModeratorHelp: String { return self._s[235]! } - public var Widget_ShortcutsGalleryDescription: String { return self._s[236]! } - public var MaskStickerSettings_Title: String { return self._s[237]! } - public var ShareMenu_Comment: String { return self._s[238]! } - public var GroupInfo_Notifications: String { return self._s[239]! } - public var CheckoutInfo_ReceiverInfoTitle: String { return self._s[240]! } + public var CreatePoll_OptionPlaceholder: String { return self._s[237]! } + public var Conversation_RestrictedStickers: String { return self._s[238]! } + public var MemberSearch_BotSection: String { return self._s[240]! } + public var Channel_Management_AddModeratorHelp: String { return self._s[242]! } + public var Widget_ShortcutsGalleryDescription: String { return self._s[243]! } + public var MaskStickerSettings_Title: String { return self._s[244]! } + public var ShareMenu_Comment: String { return self._s[245]! } + public var GroupInfo_Notifications: String { return self._s[246]! } + public var CheckoutInfo_ReceiverInfoTitle: String { return self._s[247]! } public func DialogList_EncryptedChatStartedOutgoing(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[241]!, self._r[241]!, [_0]) - } - public var Conversation_ContextMenuCopyLink: String { return self._s[242]! } - public var VoiceChat_MutedHelp: String { return self._s[245]! } - public var ChatListFolder_CategoryMuted: String { return self._s[246]! } - public var TwoStepAuth_AddHintDescription: String { return self._s[247]! } - public func VoiceOver_Chat_Duration(_ _0: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(self._s[248]!, self._r[248]!, [_0]) } - public var Conversation_ClousStorageInfo_Description3: String { return self._s[249]! } - public var BroadcastGroups_LimitAlert_SettingsTip: String { return self._s[250]! } - public var Contacts_SortByPresence: String { return self._s[251]! } - public var Watch_Location_Access: String { return self._s[252]! } - public var WallpaperPreview_CustomColorTopText: String { return self._s[253]! } - public var Passport_Address_TypeBankStatement: String { return self._s[254]! } - public var Group_Username_RevokeExistingUsernamesInfo: String { return self._s[255]! } - public var Conversation_ClearPrivateHistory: String { return self._s[256]! } - public var ChatList_Mute: String { return self._s[259]! } - public var Channel_AdminLog_CanDeleteMessagesOfOthers: String { return self._s[260]! } - public var Stats_PostsTitle: String { return self._s[261]! } + public var Conversation_ContextMenuCopyLink: String { return self._s[249]! } + public var VoiceChat_MutedHelp: String { return self._s[252]! } + public var ChatListFolder_CategoryMuted: String { return self._s[253]! } + public var TwoStepAuth_AddHintDescription: String { return self._s[254]! } + public func VoiceOver_Chat_Duration(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[255]!, self._r[255]!, [_0]) + } + public var Conversation_ClousStorageInfo_Description3: String { return self._s[256]! } + public var BroadcastGroups_LimitAlert_SettingsTip: String { return self._s[257]! } + public var Contacts_SortByPresence: String { return self._s[258]! } + public var Watch_Location_Access: String { return self._s[259]! } + public var WallpaperPreview_CustomColorTopText: String { return self._s[260]! } + public var Passport_Address_TypeBankStatement: String { return self._s[261]! } + public var Group_Username_RevokeExistingUsernamesInfo: String { return self._s[262]! } + public var Conversation_ClearPrivateHistory: String { return self._s[263]! } + public var ChatList_Mute: String { return self._s[266]! } + public var Channel_AdminLog_CanDeleteMessagesOfOthers: String { return self._s[267]! } + public var Stats_PostsTitle: String { return self._s[268]! } public func Conversation_AutoremoveTimerSetGroup(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[262]!, self._r[262]!, [_1]) + return formatWithArgumentRanges(self._s[269]!, self._r[269]!, [_1]) } - public var Paint_Masks: String { return self._s[264]! } - public var PasscodeSettings_TryAgainIn1Minute: String { return self._s[266]! } - public var Chat_AttachmentLimitReached: String { return self._s[267]! } - public var StickerPackActionInfo_ArchivedTitle: String { return self._s[268]! } - public var Watch_Stickers_StickerPacks: String { return self._s[270]! } - public var Channel_Setup_Title: String { return self._s[271]! } - public var GroupInfo_Administrators: String { return self._s[272]! } - public var InviteLink_PublicLink: String { return self._s[273]! } - public var InviteLink_DeleteLinkAlert_Action: String { return self._s[275]! } - public var NotificationSettings_ShowNotificationsAllAccountsInfoOff: String { return self._s[276]! } - public var Conversation_ContextMenuDiscuss: String { return self._s[277]! } - public var StickerPack_BuiltinPackName: String { return self._s[278]! } - public var Conversation_GreetingText: String { return self._s[280]! } - public var TwoStepAuth_RecoveryEmailChangeDescription: String { return self._s[282]! } - public var Checkout_ShippingMethod: String { return self._s[284]! } - public var ClearCache_FreeSpace: String { return self._s[285]! } - public var EditTheme_Expand_Preview_IncomingReplyText: String { return self._s[286]! } - public var SettingsSearch_Synonyms_Notifications_ChannelNotificationsSound: String { return self._s[289]! } + public var Paint_Masks: String { return self._s[271]! } + public var PasscodeSettings_TryAgainIn1Minute: String { return self._s[274]! } + public var Chat_AttachmentLimitReached: String { return self._s[275]! } + public var StickerPackActionInfo_ArchivedTitle: String { return self._s[276]! } + public var Watch_Stickers_StickerPacks: String { return self._s[278]! } + public var Channel_Setup_Title: String { return self._s[279]! } + public var GroupInfo_Administrators: String { return self._s[280]! } + public var InviteLink_PublicLink: String { return self._s[281]! } + public var InviteLink_DeleteLinkAlert_Action: String { return self._s[283]! } + public var NotificationSettings_ShowNotificationsAllAccountsInfoOff: String { return self._s[284]! } + public var Conversation_ContextMenuDiscuss: String { return self._s[285]! } + public var StickerPack_BuiltinPackName: String { return self._s[286]! } + public var Conversation_GreetingText: String { return self._s[288]! } + public var TwoStepAuth_RecoveryEmailChangeDescription: String { return self._s[290]! } + public var Checkout_ShippingMethod: String { return self._s[292]! } + public var ClearCache_FreeSpace: String { return self._s[293]! } + public var EditTheme_Expand_Preview_IncomingReplyText: String { return self._s[294]! } + public var SettingsSearch_Synonyms_Notifications_ChannelNotificationsSound: String { return self._s[297]! } public func TwoStepAuth_ConfirmEmailDescription(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[290]!, self._r[290]!, [_1]) + return formatWithArgumentRanges(self._s[298]!, self._r[298]!, [_1]) } - public var Conversation_typing: String { return self._s[291]! } + public var Conversation_typing: String { return self._s[299]! } public func PrivacySettings_LastSeenContactsMinus(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[293]!, self._r[293]!, [_0]) + return formatWithArgumentRanges(self._s[301]!, self._r[301]!, [_0]) } - public var WebSearch_RecentSectionTitle: String { return self._s[294]! } - public var VoiceChat_EndConfirmationTitle: String { return self._s[295]! } - public var ChatList_UnhideAction: String { return self._s[297]! } - public var PasscodeSettings_6DigitCode: String { return self._s[298]! } - public var CallFeedback_AddComment: String { return self._s[299]! } - public var LoginPassword_PasswordHelp: String { return self._s[300]! } - public var Call_Flip: String { return self._s[301]! } - public var Weekday_ShortWednesday: String { return self._s[303]! } - public var VoiceOver_Chat_PollFinalResults: String { return self._s[304]! } - public var PeerInfo_ButtonAddMember: String { return self._s[305]! } - public var Call_Decline: String { return self._s[307]! } - public var VoiceChat_InviteMemberToGroupFirstAdd: String { return self._s[308]! } - public var Join_ChannelsTooMuch: String { return self._s[310]! } + public var WebSearch_RecentSectionTitle: String { return self._s[302]! } + public var VoiceChat_EndConfirmationTitle: String { return self._s[303]! } + public var VoiceChat_TapToAddPhoto: String { return self._s[304]! } + public var ChatList_UnhideAction: String { return self._s[306]! } + public var PasscodeSettings_6DigitCode: String { return self._s[307]! } + public var CallFeedback_AddComment: String { return self._s[308]! } + public var LoginPassword_PasswordHelp: String { return self._s[309]! } + public var Call_Flip: String { return self._s[310]! } + public var Weekday_ShortWednesday: String { return self._s[312]! } + public var VoiceOver_Chat_PollFinalResults: String { return self._s[313]! } + public var ScheduleVoiceChat_Title: String { return self._s[314]! } + public var PeerInfo_ButtonAddMember: String { return self._s[315]! } + public var ImportStickerPack_ChooseNameDescription: String { return self._s[316]! } + public var Call_Decline: String { return self._s[318]! } + public var VoiceChat_InviteMemberToGroupFirstAdd: String { return self._s[319]! } + public var Join_ChannelsTooMuch: String { return self._s[321]! } public func PUSH_CHANNEL_MESSAGE_NOTEXT(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[311]!, self._r[311]!, [_1]) + return formatWithArgumentRanges(self._s[322]!, self._r[322]!, [_1]) } - public var Passport_Identity_Selfie: String { return self._s[312]! } - public var Privacy_ContactsTitle: String { return self._s[313]! } - public var GroupInfo_InviteLink_Title: String { return self._s[315]! } - public var TwoFactorSetup_Password_PlaceholderPassword: String { return self._s[316]! } + public var Passport_Identity_Selfie: String { return self._s[323]! } + public var Privacy_ContactsTitle: String { return self._s[324]! } + public var GroupInfo_InviteLink_Title: String { return self._s[326]! } + public var TwoFactorSetup_Password_PlaceholderPassword: String { return self._s[327]! } public func Channel_AdminLog_UpdatedParticipantVolume(_ _1: String, _ _2: String, _ _3: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[317]!, self._r[317]!, [_1, _2, _3]) + return formatWithArgumentRanges(self._s[328]!, self._r[328]!, [_1, _2, _3]) } - public var Conversation_OpenFile: String { return self._s[318]! } - public var Map_SetThisPlace: String { return self._s[319]! } - public var Channel_Info_Management: String { return self._s[320]! } - public var Passport_Language_hr: String { return self._s[321]! } - public var VoiceChat_Title: String { return self._s[322]! } - public var EditTheme_Edit_Preview_IncomingText: String { return self._s[325]! } - public var OpenFile_Proceed: String { return self._s[326]! } - public var Conversation_SecretChatContextBotAlert: String { return self._s[328]! } - public var GroupInfo_Permissions_SlowmodeValue_Off: String { return self._s[329]! } - public var Privacy_Calls_P2PContacts: String { return self._s[330]! } - public var Appearance_PickAccentColor: String { return self._s[331]! } - public var MediaPicker_TapToUngroupDescription: String { return self._s[332]! } - public var Localization_EnglishLanguageName: String { return self._s[333]! } - public var Stickers_SuggestStickers: String { return self._s[334]! } - public var Passport_Language_ko: String { return self._s[335]! } - public var Settings_ProxyDisabled: String { return self._s[336]! } - public var PrivacySettings_PasscodeOff: String { return self._s[337]! } - public var Undo_LeftChannel: String { return self._s[338]! } - public var Appearance_AutoNightThemeDisabled: String { return self._s[339]! } - public var TextFormat_Bold: String { return self._s[340]! } - public var Login_InfoTitle: String { return self._s[341]! } - public var Channel_BanUser_PermissionSendPolls: String { return self._s[342]! } - public var Settings_AddAnotherAccount: String { return self._s[343]! } - public var GroupPermission_NewTitle: String { return self._s[344]! } - public var Login_SelectCountry_Title: String { return self._s[345]! } - public var Cache_ServiceFiles: String { return self._s[346]! } + public var TwoFactorSetup_PasswordRecovery_SkipAlertAction: String { return self._s[330]! } + public var Conversation_OpenFile: String { return self._s[331]! } + public var Map_SetThisPlace: String { return self._s[332]! } + public var Channel_Info_Management: String { return self._s[333]! } + public var Passport_Language_hr: String { return self._s[334]! } + public var VoiceChat_Title: String { return self._s[335]! } + public var EditTheme_Edit_Preview_IncomingText: String { return self._s[339]! } + public var VoiceChat_EditBioSave: String { return self._s[340]! } + public var OpenFile_Proceed: String { return self._s[341]! } + public var Conversation_SecretChatContextBotAlert: String { return self._s[343]! } + public var GroupInfo_Permissions_SlowmodeValue_Off: String { return self._s[344]! } + public var Privacy_Calls_P2PContacts: String { return self._s[345]! } + public var Appearance_PickAccentColor: String { return self._s[346]! } + public var TwoFactorSetup_PasswordRecovery_Title: String { return self._s[347]! } + public var MediaPicker_TapToUngroupDescription: String { return self._s[348]! } + public var Localization_EnglishLanguageName: String { return self._s[349]! } + public var Stickers_SuggestStickers: String { return self._s[350]! } + public var Passport_Language_ko: String { return self._s[351]! } + public var Settings_ProxyDisabled: String { return self._s[352]! } + public var PrivacySettings_PasscodeOff: String { return self._s[353]! } + public var Undo_LeftChannel: String { return self._s[354]! } + public var Appearance_AutoNightThemeDisabled: String { return self._s[355]! } + public var TextFormat_Bold: String { return self._s[356]! } + public var Login_InfoTitle: String { return self._s[357]! } + public var Channel_BanUser_PermissionSendPolls: String { return self._s[358]! } + public var Settings_AddAnotherAccount: String { return self._s[359]! } + public var GroupPermission_NewTitle: String { return self._s[360]! } + public var Login_SelectCountry_Title: String { return self._s[361]! } + public var Cache_ServiceFiles: String { return self._s[362]! } public func AutoremoveSetup_TimerValueAfter(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[347]!, self._r[347]!, [_0]) + return formatWithArgumentRanges(self._s[363]!, self._r[363]!, [_0]) } - public var Passport_Language_nl: String { return self._s[348]! } - public var Contacts_TopSection: String { return self._s[349]! } - public var Passport_Identity_DateOfBirthPlaceholder: String { return self._s[350]! } - public var VoiceChat_StatusInvited: String { return self._s[352]! } - public var Conversation_ContextMenuReport: String { return self._s[353]! } + public var Passport_Language_nl: String { return self._s[364]! } + public var Contacts_TopSection: String { return self._s[365]! } + public var Passport_Identity_DateOfBirthPlaceholder: String { return self._s[366]! } + public var VoiceChat_StatusInvited: String { return self._s[368]! } + public var Conversation_ContextMenuReport: String { return self._s[369]! } public func Login_BannedPhoneBody(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[354]!, self._r[354]!, [_0]) - } - public var Conversation_Search: String { return self._s[355]! } - public var Group_Setup_HistoryVisibleHelp: String { return self._s[357]! } - public var ReportPeer_AlertSuccess: String { return self._s[359]! } - public var AutoNightTheme_Title: String { return self._s[361]! } - public func Notification_PinnedTextMessage(_ _0: String, _ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[363]!, self._r[363]!, [_0, _1]) - } - public func Conversation_OpenBotLinkText(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[364]!, self._r[364]!, [_0]) - } - public var Conversation_ShareBotContactConfirmation: String { return self._s[365]! } - public var TwoStepAuth_RecoveryCode: String { return self._s[366]! } - public var GroupInfo_Permissions_BroadcastTitle: String { return self._s[367]! } - public var SocksProxySetup_ConnectAndSave: String { return self._s[368]! } - public func MESSAGE_INVOICE(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[369]!, self._r[369]!, [_1, _2]) - } - public func Channel_AdminLog_MessageChangedGroupUsername(_ _0: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(self._s[370]!, self._r[370]!, [_0]) } + public var Conversation_Search: String { return self._s[371]! } + public var Group_Setup_HistoryVisibleHelp: String { return self._s[373]! } + public var ReportPeer_AlertSuccess: String { return self._s[375]! } + public var AutoNightTheme_Title: String { return self._s[377]! } + public func Notification_PinnedTextMessage(_ _0: String, _ _1: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[379]!, self._r[379]!, [_0, _1]) + } + public func Conversation_OpenBotLinkText(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[380]!, self._r[380]!, [_0]) + } + public var Conversation_ShareBotContactConfirmation: String { return self._s[381]! } + public var TwoStepAuth_RecoveryCode: String { return self._s[382]! } + public var GroupInfo_Permissions_BroadcastTitle: String { return self._s[383]! } + public var SocksProxySetup_ConnectAndSave: String { return self._s[384]! } + public func MESSAGE_INVOICE(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[385]!, self._r[385]!, [_1, _2]) + } + public func Channel_AdminLog_MessageChangedGroupUsername(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[386]!, self._r[386]!, [_0]) + } public func BroadcastGroups_LimitAlert_Text(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[371]!, self._r[371]!, [_0]) + return formatWithArgumentRanges(self._s[387]!, self._r[387]!, [_0]) } - public var Replies_BlockAndDeleteRepliesActionTitle: String { return self._s[373]! } + public var Replies_BlockAndDeleteRepliesActionTitle: String { return self._s[389]! } public func Notification_GroupInviter(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[374]!, self._r[374]!, [_0]) + return formatWithArgumentRanges(self._s[390]!, self._r[390]!, [_0]) } - public var VoiceChat_CopyInviteLink: String { return self._s[375]! } - public var Conversation_InfoGroup: String { return self._s[376]! } + public var VoiceChat_CopyInviteLink: String { return self._s[391]! } + public var Conversation_InfoGroup: String { return self._s[392]! } public func Map_AccurateTo(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[378]!, self._r[378]!, [_0]) + return formatWithArgumentRanges(self._s[394]!, self._r[394]!, [_0]) } - public var Conversation_ChatBackground: String { return self._s[379]! } - public var PhotoEditor_Set: String { return self._s[380]! } + public var Conversation_ChatBackground: String { return self._s[395]! } + public var PhotoEditor_Set: String { return self._s[396]! } public func Channel_Management_PromotedBy(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[382]!, self._r[382]!, [_0]) + return formatWithArgumentRanges(self._s[398]!, self._r[398]!, [_0]) } - public var IntentsSettings_SuggestedChatsContacts: String { return self._s[383]! } - public var Passport_Phone_Title: String { return self._s[385]! } - public var Conversation_EditingMessageMediaChange: String { return self._s[386]! } - public var Channel_LinkItem: String { return self._s[387]! } - public var VoiceChat_EndConfirmationText: String { return self._s[388]! } + public var TwoStepAuth_CancelResetTitle: String { return self._s[399]! } + public var IntentsSettings_SuggestedChatsContacts: String { return self._s[400]! } + public var Passport_Phone_Title: String { return self._s[402]! } + public var Conversation_EditingMessageMediaChange: String { return self._s[403]! } + public var Channel_LinkItem: String { return self._s[404]! } + public var VoiceChat_EndConfirmationText: String { return self._s[405]! } public func PUSH_CHAT_DELETE_MEMBER(_ _1: String, _ _2: String, _ _3: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[389]!, self._r[389]!, [_1, _2, _3]) + return formatWithArgumentRanges(self._s[406]!, self._r[406]!, [_1, _2, _3]) } - public var Conversation_DeleteManyMessages: String { return self._s[391]! } - public var Notifications_Badge_IncludeMutedChats: String { return self._s[392]! } - public var Channel_AddUserLeftError: String { return self._s[394]! } - public var AuthSessions_AddedDeviceTitle: String { return self._s[396]! } - public var Privacy_Calls_NeverAllow_Placeholder: String { return self._s[397]! } - public var Settings_ProxyConnecting: String { return self._s[398]! } - public var Theme_Colors_Accent: String { return self._s[399]! } - public var Theme_Colors_ColorWallpaperWarning: String { return self._s[400]! } + public var Conversation_DeleteManyMessages: String { return self._s[408]! } + public var Notifications_Badge_IncludeMutedChats: String { return self._s[409]! } + public var Channel_AddUserLeftError: String { return self._s[411]! } + public var AuthSessions_AddedDeviceTitle: String { return self._s[413]! } + public var Privacy_Calls_NeverAllow_Placeholder: String { return self._s[414]! } + public var Settings_ProxyConnecting: String { return self._s[415]! } + public var Theme_Colors_Accent: String { return self._s[417]! } + public var Theme_Colors_ColorWallpaperWarning: String { return self._s[418]! } public func PUSH_PHONE_CALL_MISSED(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[402]!, self._r[402]!, [_1]) + return formatWithArgumentRanges(self._s[420]!, self._r[420]!, [_1]) } - public var Passport_Language_lo: String { return self._s[403]! } + public var Passport_Language_lo: String { return self._s[421]! } public func Watch_Time_ShortWeekdayAt(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[405]!, self._r[405]!, [_1, _2]) + return formatWithArgumentRanges(self._s[423]!, self._r[423]!, [_1, _2]) } - public var Permissions_NotificationsText_v0: String { return self._s[406]! } - public var BroadcastGroups_LimitAlert_Title: String { return self._s[407]! } - public var ChatList_Context_RemoveFromRecents: String { return self._s[408]! } - public var Watch_GroupInfo_Title: String { return self._s[409]! } - public var Settings_AddDevice: String { return self._s[411]! } - public var WallpaperPreview_SwipeColorsTopText: String { return self._s[412]! } + public var Permissions_NotificationsText_v0: String { return self._s[424]! } + public var BroadcastGroups_LimitAlert_Title: String { return self._s[425]! } + public var Settings_CheckPasswordText: String { return self._s[426]! } + public var ChatList_Context_RemoveFromRecents: String { return self._s[427]! } + public var Watch_GroupInfo_Title: String { return self._s[428]! } + public var Settings_AddDevice: String { return self._s[430]! } + public var WallpaperPreview_SwipeColorsTopText: String { return self._s[431]! } public func PUSH_CHANNEL_ALBUM(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[413]!, self._r[413]!, [_1]) + return formatWithArgumentRanges(self._s[432]!, self._r[432]!, [_1]) } - public var Conversation_AutoremoveActionEdit: String { return self._s[414]! } - public var TwoStepAuth_Disable: String { return self._s[416]! } + public var ImportStickerPack_Create: String { return self._s[433]! } + public var Conversation_AutoremoveActionEdit: String { return self._s[434]! } + public var TwoStepAuth_Disable: String { return self._s[436]! } public func Conversation_AddNameToContacts(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[417]!, self._r[417]!, [_0]) + return formatWithArgumentRanges(self._s[437]!, self._r[437]!, [_0]) } public func Time_PreciseDate_m10(_ _1: String, _ _2: String, _ _3: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[418]!, self._r[418]!, [_1, _2, _3]) + return formatWithArgumentRanges(self._s[438]!, self._r[438]!, [_1, _2, _3]) } public func Login_WillSendSms(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[419]!, self._r[419]!, [_0]) + return formatWithArgumentRanges(self._s[439]!, self._r[439]!, [_0]) } - public var Channel_AdminLog_BanReadMessages: String { return self._s[420]! } - public var Undo_ChatDeleted: String { return self._s[421]! } - public var ContactInfo_URLLabelHomepage: String { return self._s[422]! } + public var Channel_AdminLog_BanReadMessages: String { return self._s[440]! } + public var Undo_ChatDeleted: String { return self._s[441]! } + public var ContactInfo_URLLabelHomepage: String { return self._s[442]! } public func PUSH_CHAT_MESSAGE_STICKER(_ _1: String, _ _2: String, _ _3: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[423]!, self._r[423]!, [_1, _2, _3]) + return formatWithArgumentRanges(self._s[443]!, self._r[443]!, [_1, _2, _3]) } - public var FastTwoStepSetup_EmailHelp: String { return self._s[424]! } - public var Contacts_SelectAll: String { return self._s[425]! } - public var Privacy_ContactsReset: String { return self._s[426]! } - public var AttachmentMenu_File: String { return self._s[428]! } - public var PasscodeSettings_EncryptData: String { return self._s[429]! } - public var EditTheme_ThemeTemplateAlertText: String { return self._s[430]! } + public var FastTwoStepSetup_EmailHelp: String { return self._s[444]! } + public var Contacts_SelectAll: String { return self._s[445]! } + public var Privacy_ContactsReset: String { return self._s[446]! } + public var AttachmentMenu_File: String { return self._s[448]! } + public var PasscodeSettings_EncryptData: String { return self._s[449]! } + public var EditTheme_ThemeTemplateAlertText: String { return self._s[450]! } public func Privacy_GroupsAndChannels_InviteToChannelError(_ _0: String, _ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[432]!, self._r[432]!, [_0, _1]) + return formatWithArgumentRanges(self._s[452]!, self._r[452]!, [_0, _1]) } public func Profile_CreateEncryptedChatOutdatedError(_ _0: String, _ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[433]!, self._r[433]!, [_0, _1]) + return formatWithArgumentRanges(self._s[453]!, self._r[453]!, [_0, _1]) } - public var PhotoEditor_ShadowsTint: String { return self._s[435]! } - public var GroupInfo_ChatAdmins: String { return self._s[436]! } - public var ArchivedChats_IntroTitle2: String { return self._s[437]! } - public var Cache_LowDiskSpaceText: String { return self._s[438]! } - public var CreatePoll_Anonymous: String { return self._s[439]! } - public var Report_AdditionalDetailsText: String { return self._s[440]! } - public var Checkout_PaymentMethod_New: String { return self._s[441]! } - public var Invitation_JoinGroup: String { return self._s[442]! } + public var PhotoEditor_ShadowsTint: String { return self._s[455]! } + public var GroupInfo_ChatAdmins: String { return self._s[456]! } + public var ArchivedChats_IntroTitle2: String { return self._s[457]! } + public var Cache_LowDiskSpaceText: String { return self._s[458]! } + public var CreatePoll_Anonymous: String { return self._s[459]! } + public var Report_AdditionalDetailsText: String { return self._s[460]! } + public var Checkout_PaymentMethod_New: String { return self._s[461]! } + public var Invitation_JoinGroup: String { return self._s[462]! } public func Time_MonthOfYear_m4(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[445]!, self._r[445]!, [_0]) + return formatWithArgumentRanges(self._s[465]!, self._r[465]!, [_0]) } - public var CheckoutInfo_SaveInfoHelp: String { return self._s[446]! } - public var Notification_Reply: String { return self._s[448]! } + public var CheckoutInfo_SaveInfoHelp: String { return self._s[466]! } + public var Notification_Reply: String { return self._s[468]! } public func Login_PhoneBannedEmailSubject(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[449]!, self._r[449]!, [_0]) + return formatWithArgumentRanges(self._s[469]!, self._r[469]!, [_0]) } - public var Login_PhoneTitle: String { return self._s[450]! } - public var VoiceChat_UnmuteHelp: String { return self._s[451]! } - public var VoiceOver_Media_PlaybackRateNormal: String { return self._s[452]! } + public var Login_PhoneTitle: String { return self._s[470]! } + public var VoiceChat_UnmuteHelp: String { return self._s[471]! } + public var VoiceOver_Media_PlaybackRateNormal: String { return self._s[472]! } public func PUSH_CHAT_MESSAGE_INVOICE(_ _1: String, _ _2: String, _ _3: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[453]!, self._r[453]!, [_1, _2, _3]) + return formatWithArgumentRanges(self._s[473]!, self._r[473]!, [_1, _2, _3]) } - public var Appearance_TextSize_Title: String { return self._s[454]! } - public var NetworkUsageSettings_MediaImageDataSection: String { return self._s[456]! } - public var VoiceOver_Navigation_Compose: String { return self._s[457]! } + public var Appearance_TextSize_Title: String { return self._s[474]! } + public var NetworkUsageSettings_MediaImageDataSection: String { return self._s[476]! } + public var VoiceOver_Navigation_Compose: String { return self._s[477]! } public func Channel_AdminLog_MessageChangedAutoremoveTimeoutRemove(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[458]!, self._r[458]!, [_1]) + return formatWithArgumentRanges(self._s[478]!, self._r[478]!, [_1]) } - public var Passport_InfoText: String { return self._s[459]! } - public var ApplyLanguage_ApplyLanguageAction: String { return self._s[460]! } - public var MessagePoll_LabelClosed: String { return self._s[462]! } - public var AttachmentMenu_SendAsFiles: String { return self._s[463]! } - public var KeyCommand_FocusOnInputField: String { return self._s[464]! } - public var Conversation_ContextViewThread: String { return self._s[465]! } - public var ChatImport_SelectionErrorGroupGeneric: String { return self._s[466]! } - public var Privacy_SecretChatsLinkPreviews: String { return self._s[468]! } - public var Permissions_PeopleNearbyAllow_v0: String { return self._s[469]! } - public var Conversation_ContextMenuMention: String { return self._s[471]! } - public var CreatePoll_QuizInfo: String { return self._s[472]! } - public var Appearance_ThemePreview_ChatList_2_Name: String { return self._s[473]! } - public var Username_LinkCopied: String { return self._s[474]! } - public var IntentsSettings_SuggestedAndSpotlightChatsInfo: String { return self._s[475]! } - public var TwoStepAuth_ChangePassword: String { return self._s[476]! } - public var Watch_Suggestion_Thanks: String { return self._s[477]! } - public var Channel_TitleInfo: String { return self._s[478]! } - public var ChatList_ChatTypesSection: String { return self._s[479]! } + public var Passport_InfoText: String { return self._s[479]! } + public var ApplyLanguage_ApplyLanguageAction: String { return self._s[480]! } + public var MessagePoll_LabelClosed: String { return self._s[482]! } + public var AttachmentMenu_SendAsFiles: String { return self._s[483]! } + public var KeyCommand_FocusOnInputField: String { return self._s[484]! } + public var Conversation_ContextViewThread: String { return self._s[485]! } + public var ChatImport_SelectionErrorGroupGeneric: String { return self._s[486]! } + public var Privacy_SecretChatsLinkPreviews: String { return self._s[488]! } + public var Permissions_PeopleNearbyAllow_v0: String { return self._s[489]! } + public var Conversation_ContextMenuMention: String { return self._s[491]! } + public var CreatePoll_QuizInfo: String { return self._s[492]! } + public var Appearance_ThemePreview_ChatList_2_Name: String { return self._s[493]! } + public var Username_LinkCopied: String { return self._s[494]! } + public var IntentsSettings_SuggestedAndSpotlightChatsInfo: String { return self._s[495]! } + public var TwoStepAuth_ChangePassword: String { return self._s[496]! } + public var Watch_Suggestion_Thanks: String { return self._s[497]! } + public var Channel_TitleInfo: String { return self._s[498]! } + public var ChatList_ChatTypesSection: String { return self._s[499]! } public func Watch_LastSeen_AtDate(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[480]!, self._r[480]!, [_0]) + return formatWithArgumentRanges(self._s[500]!, self._r[500]!, [_0]) } public func Channel_AdminLog_PollStopped(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[481]!, self._r[481]!, [_0]) + return formatWithArgumentRanges(self._s[501]!, self._r[501]!, [_0]) } public func Channel_AdminLog_MessageRemovedAdminNameUsername(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[482]!, self._r[482]!, [_1, _2]) + return formatWithArgumentRanges(self._s[502]!, self._r[502]!, [_1, _2]) } - public var AuthSessions_AddDevice_InvalidQRCode: String { return self._s[483]! } + public var AuthSessions_AddDevice_InvalidQRCode: String { return self._s[503]! } public func Call_MicrophoneOff(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[484]!, self._r[484]!, [_0]) + return formatWithArgumentRanges(self._s[504]!, self._r[504]!, [_0]) } - public var Channel_AdminLogFilter_ChannelEventsInfo: String { return self._s[485]! } - public var Profile_MessageLifetimeForever: String { return self._s[486]! } - public var ArchivedChats_IntroText1: String { return self._s[487]! } - public var Notifications_ChannelNotificationsPreview: String { return self._s[488]! } - public var Map_PullUpForPlaces: String { return self._s[490]! } - public var UserInfo_TelegramCall: String { return self._s[491]! } - public var Conversation_ShareMyContactInfo: String { return self._s[492]! } - public var ChatList_Tabs_All: String { return self._s[493]! } - public var Notification_PassportValueEmail: String { return self._s[494]! } - public var Notification_VideoCallIncoming: String { return self._s[495]! } - public var SettingsSearch_Synonyms_Appearance_AutoNightTheme: String { return self._s[496]! } - public var Channel_Username_InvalidTaken: String { return self._s[497]! } - public var GroupPermission_EditingDisabled: String { return self._s[498]! } - public var InviteLink_PeopleJoinedShortNone: String { return self._s[499]! } - public var ChatContextMenu_TextSelectionTip: String { return self._s[500]! } - public var Passport_Language_pl: String { return self._s[502]! } - public var Call_Accept: String { return self._s[503]! } - public var ChatListFolder_NameSectionHeader: String { return self._s[504]! } - public var InviteLink_ExpiredLinkStatus: String { return self._s[505]! } + public var Channel_AdminLogFilter_ChannelEventsInfo: String { return self._s[505]! } + public var Profile_MessageLifetimeForever: String { return self._s[506]! } + public var ArchivedChats_IntroText1: String { return self._s[507]! } + public var Notifications_ChannelNotificationsPreview: String { return self._s[508]! } + public var Map_PullUpForPlaces: String { return self._s[510]! } + public var UserInfo_TelegramCall: String { return self._s[511]! } + public var Conversation_ShareMyContactInfo: String { return self._s[512]! } + public var ChatList_Tabs_All: String { return self._s[513]! } + public var Notification_PassportValueEmail: String { return self._s[514]! } + public var Notification_VideoCallIncoming: String { return self._s[515]! } + public var SettingsSearch_Synonyms_Appearance_AutoNightTheme: String { return self._s[516]! } + public var Channel_Username_InvalidTaken: String { return self._s[517]! } + public var GroupPermission_EditingDisabled: String { return self._s[518]! } + public var InviteLink_PeopleJoinedShortNone: String { return self._s[519]! } + public var ChatContextMenu_TextSelectionTip: String { return self._s[520]! } + public var Passport_Language_pl: String { return self._s[522]! } + public var Call_Accept: String { return self._s[523]! } + public var ChatListFolder_NameSectionHeader: String { return self._s[524]! } + public var InviteLink_ExpiredLinkStatus: String { return self._s[525]! } public func Passport_Identity_NativeNameTitle(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[506]!, self._r[506]!, [_0]) + return formatWithArgumentRanges(self._s[526]!, self._r[526]!, [_0]) } - public var ClearCache_Forever: String { return self._s[507]! } + public var ClearCache_Forever: String { return self._s[527]! } + public var VoiceChat_TapToEditTitle: String { return self._s[529]! } public func ChannelInfo_AddParticipantConfirmation(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[509]!, self._r[509]!, [_0]) + return formatWithArgumentRanges(self._s[530]!, self._r[530]!, [_0]) } - public var Group_EditAdmin_RankAdminPlaceholder: String { return self._s[510]! } - public var Calls_SubmitRating: String { return self._s[511]! } - public var Location_LiveLocationRequired_ShareLocation: String { return self._s[512]! } + public var Group_EditAdmin_RankAdminPlaceholder: String { return self._s[531]! } + public var Calls_SubmitRating: String { return self._s[532]! } + public var Location_LiveLocationRequired_ShareLocation: String { return self._s[533]! } public func ChatList_AddedToFolderTooltip(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[513]!, self._r[513]!, [_1, _2]) + return formatWithArgumentRanges(self._s[534]!, self._r[534]!, [_1, _2]) } - public var IntentsSettings_MainAccountInfo: String { return self._s[514]! } - public var Map_Hybrid: String { return self._s[516]! } - public var ChatList_Context_Archive: String { return self._s[517]! } - public var Message_PinnedDocumentMessage: String { return self._s[518]! } - public var State_ConnectingToProxyInfo: String { return self._s[519]! } - public var Passport_Identity_NativeNameGenericTitle: String { return self._s[521]! } - public var Settings_AppLanguage: String { return self._s[522]! } + public var IntentsSettings_MainAccountInfo: String { return self._s[535]! } + public var Map_Hybrid: String { return self._s[537]! } + public var ChatList_Context_Archive: String { return self._s[538]! } + public var Message_PinnedDocumentMessage: String { return self._s[539]! } + public var State_ConnectingToProxyInfo: String { return self._s[540]! } + public var Passport_Identity_NativeNameGenericTitle: String { return self._s[542]! } + public var Settings_AppLanguage: String { return self._s[543]! } public func Checkout_SavePasswordTimeoutAndFaceId(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[523]!, self._r[523]!, [_0]) + return formatWithArgumentRanges(self._s[544]!, self._r[544]!, [_0]) } - public var Notifications_PermissionsEnable: String { return self._s[525]! } - public var CheckoutInfo_ShippingInfoAddress1Placeholder: String { return self._s[526]! } + public var Notifications_PermissionsEnable: String { return self._s[546]! } + public var CheckoutInfo_ShippingInfoAddress1Placeholder: String { return self._s[547]! } public func UserInfo_BlockActionTitle(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[527]!, self._r[527]!, [_0]) + return formatWithArgumentRanges(self._s[548]!, self._r[548]!, [_0]) } public func AuthSessions_Message(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[528]!, self._r[528]!, [_0]) + return formatWithArgumentRanges(self._s[549]!, self._r[549]!, [_0]) } - public var NotificationsSound_Aurora: String { return self._s[531]! } - public var ScheduledMessages_ClearAll: String { return self._s[534]! } + public var NotificationsSound_Aurora: String { return self._s[552]! } + public var ScheduledMessages_ClearAll: String { return self._s[555]! } public func CancelResetAccount_TextSMS(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[535]!, self._r[535]!, [_0]) + return formatWithArgumentRanges(self._s[556]!, self._r[556]!, [_0]) } - public var Settings_BlockedUsers: String { return self._s[537]! } - public var VoiceOver_Keyboard: String { return self._s[539]! } + public var Settings_BlockedUsers: String { return self._s[558]! } + public var Checkout_TipItem: String { return self._s[559]! } + public var VoiceOver_Keyboard: String { return self._s[561]! } public func UserInfo_StartSecretChatConfirmation(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[540]!, self._r[540]!, [_0]) + return formatWithArgumentRanges(self._s[562]!, self._r[562]!, [_0]) } - public var Passport_Language_hu: String { return self._s[541]! } + public var Passport_Language_hu: String { return self._s[563]! } public func Conversation_ScheduleMessage_SendTomorrow(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[542]!, self._r[542]!, [_0]) + return formatWithArgumentRanges(self._s[564]!, self._r[564]!, [_0]) } - public var StickerPack_Share: String { return self._s[543]! } - public var Checkout_NewCard_SaveInfoEnableHelp: String { return self._s[544]! } + public var StickerPack_Share: String { return self._s[565]! } + public var Checkout_NewCard_SaveInfoEnableHelp: String { return self._s[566]! } public func ForwardedAuthors2(_ _0: String, _ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[545]!, self._r[545]!, [_0, _1]) + return formatWithArgumentRanges(self._s[567]!, self._r[567]!, [_0, _1]) } - public var Privacy_ContactsResetConfirmation: String { return self._s[546]! } - public var VoiceChat_EditTitle: String { return self._s[547]! } - public var AppleWatch_ReplyPresets: String { return self._s[548]! } - public var Bot_GenericBotStatus: String { return self._s[549]! } - public var Appearance_ShareThemeColor: String { return self._s[550]! } - public var AuthSessions_AddDevice_UrlLoginHint: String { return self._s[551]! } - public var ReportGroupLocation_Title: String { return self._s[552]! } + public var Privacy_ContactsResetConfirmation: String { return self._s[568]! } + public var VoiceChat_EditTitle: String { return self._s[569]! } + public var AppleWatch_ReplyPresets: String { return self._s[570]! } + public var Bot_GenericBotStatus: String { return self._s[571]! } + public var Appearance_ShareThemeColor: String { return self._s[572]! } + public var AuthSessions_AddDevice_UrlLoginHint: String { return self._s[575]! } + public var ReportGroupLocation_Title: String { return self._s[576]! } public func Conversation_AutoremoveTimerSetUserYou(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[553]!, self._r[553]!, [_1]) + return formatWithArgumentRanges(self._s[577]!, self._r[577]!, [_1]) } public func Activity_RemindAboutUser(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[554]!, self._r[554]!, [_0]) + return formatWithArgumentRanges(self._s[578]!, self._r[578]!, [_0]) } - public var Profile_CreateEncryptedChatError: String { return self._s[555]! } - public var Channel_EditAdmin_TransferOwnership: String { return self._s[556]! } - public var Wallpaper_ErrorNotFound: String { return self._s[557]! } - public var Bot_GenericSupportStatus: String { return self._s[558]! } - public var Activity_UploadingPhoto: String { return self._s[560]! } - public var Intents_ErrorLockedTitle: String { return self._s[561]! } - public var Watch_UserInfo_Title: String { return self._s[563]! } - public var SocksProxySetup_ProxyTelegram: String { return self._s[564]! } - public var Appearance_ThemeDay: String { return self._s[565]! } + public var Profile_CreateEncryptedChatError: String { return self._s[579]! } + public var Channel_EditAdmin_TransferOwnership: String { return self._s[580]! } + public var Wallpaper_ErrorNotFound: String { return self._s[581]! } + public var Bot_GenericSupportStatus: String { return self._s[582]! } + public var Activity_UploadingPhoto: String { return self._s[584]! } + public var Intents_ErrorLockedTitle: String { return self._s[585]! } + public var Watch_UserInfo_Title: String { return self._s[587]! } + public var SocksProxySetup_ProxyTelegram: String { return self._s[588]! } + public var Appearance_ThemeDay: String { return self._s[589]! } public func ApplyLanguage_ChangeLanguageOfficialText(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[566]!, self._r[566]!, [_1]) + return formatWithArgumentRanges(self._s[590]!, self._r[590]!, [_1]) } public func FileSize_B(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[567]!, self._r[567]!, [_0]) + return formatWithArgumentRanges(self._s[591]!, self._r[591]!, [_0]) } - public var InviteLink_AdditionalLinks: String { return self._s[568]! } - public var Passport_Title: String { return self._s[571]! } + public var InviteLink_AdditionalLinks: String { return self._s[592]! } + public var Passport_Title: String { return self._s[596]! } public func Time_PreciseDate_m3(_ _1: String, _ _2: String, _ _3: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[573]!, self._r[573]!, [_1, _2, _3]) + return formatWithArgumentRanges(self._s[598]!, self._r[598]!, [_1, _2, _3]) } - public var CheckoutInfo_ShippingInfoCountryPlaceholder: String { return self._s[574]! } - public var SocksProxySetup_ShareLink: String { return self._s[577]! } - public var AuthSessions_OtherDevices: String { return self._s[578]! } - public var IntentsSettings_SuggestedChatsGroups: String { return self._s[579]! } - public var Watch_MessageView_Reply: String { return self._s[580]! } - public var Camera_FlashOn: String { return self._s[582]! } + public var CheckoutInfo_ShippingInfoCountryPlaceholder: String { return self._s[599]! } + public var VoiceChat_OpenGroup: String { return self._s[601]! } + public var SocksProxySetup_ShareLink: String { return self._s[603]! } + public var AuthSessions_OtherDevices: String { return self._s[604]! } + public var IntentsSettings_SuggestedChatsGroups: String { return self._s[605]! } + public var Watch_MessageView_Reply: String { return self._s[606]! } + public var Camera_FlashOn: String { return self._s[608]! } public func PUSH_MESSAGE_STICKER(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[583]!, self._r[583]!, [_1, _2]) + return formatWithArgumentRanges(self._s[609]!, self._r[609]!, [_1, _2]) } - public var Conversation_ContextMenuBlock: String { return self._s[584]! } - public var Channel_EditAdmin_PermissionEditMessages: String { return self._s[586]! } - public var Privacy_Calls_NeverAllow: String { return self._s[587]! } - public var BroadcastGroups_Cancel: String { return self._s[588]! } - public var SharedMedia_CategoryLinks: String { return self._s[589]! } - public var Conversation_PinMessageAlertGroup: String { return self._s[592]! } - public var Passport_Identity_ScansHelp: String { return self._s[594]! } - public var ShareMenu_CopyShareLink: String { return self._s[595]! } - public var StickerSettings_MaskContextInfo: String { return self._s[596]! } - public var InviteLink_Create_EditTitle: String { return self._s[597]! } - public var SocksProxySetup_ProxyStatusChecking: String { return self._s[598]! } - public var AutoDownloadSettings_AutodownloadPhotos: String { return self._s[601]! } - public var ChatImportActivity_Success: String { return self._s[603]! } - public var Checkout_ErrorPrecheckoutFailed: String { return self._s[604]! } - public var NotificationsSound_Popcorn: String { return self._s[605]! } - public var FeatureDisabled_Oops: String { return self._s[606]! } + public var Conversation_ContextMenuBlock: String { return self._s[610]! } + public var Channel_EditAdmin_PermissionEditMessages: String { return self._s[612]! } + public var Privacy_Calls_NeverAllow: String { return self._s[613]! } + public var BroadcastGroups_Cancel: String { return self._s[614]! } + public var SharedMedia_CategoryLinks: String { return self._s[615]! } + public var Conversation_PinMessageAlertGroup: String { return self._s[618]! } + public var Passport_Identity_ScansHelp: String { return self._s[620]! } + public var ShareMenu_CopyShareLink: String { return self._s[621]! } + public var StickerSettings_MaskContextInfo: String { return self._s[622]! } + public var InviteLink_Create_EditTitle: String { return self._s[623]! } + public var SocksProxySetup_ProxyStatusChecking: String { return self._s[624]! } + public var TwoStepAuth_RecoveryEmailResetNoAccess: String { return self._s[625]! } + public var AutoDownloadSettings_AutodownloadPhotos: String { return self._s[628]! } + public var ChatImportActivity_Success: String { return self._s[630]! } + public var Checkout_ErrorPrecheckoutFailed: String { return self._s[631]! } + public var NotificationsSound_Popcorn: String { return self._s[632]! } + public var FeatureDisabled_Oops: String { return self._s[633]! } public func Channel_AdminLog_MessageChangedChannelAbout(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[607]!, self._r[607]!, [_0]) + return formatWithArgumentRanges(self._s[634]!, self._r[634]!, [_0]) } - public var Notification_PinnedMessage: String { return self._s[608]! } - public var Tour_Title4: String { return self._s[609]! } + public var Notification_PinnedMessage: String { return self._s[635]! } + public var Tour_Title4: String { return self._s[636]! } public func Notification_VoiceChatInvitationForYou(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[610]!, self._r[610]!, [_1]) + return formatWithArgumentRanges(self._s[637]!, self._r[637]!, [_1]) } - public var Watch_Suggestion_OK: String { return self._s[611]! } - public var Compose_TokenListPlaceholder: String { return self._s[612]! } - public var InviteLink_PermanentLink: String { return self._s[613]! } - public var EditTheme_Edit_TopInfo: String { return self._s[614]! } - public var Gif_NoGifsFound: String { return self._s[615]! } - public var Login_InvalidCountryCode: String { return self._s[616]! } - public var SettingsSearch_Synonyms_Notifications_ChannelNotificationsExceptions: String { return self._s[617]! } - public var Call_VoiceOver_VideoCallMissed: String { return self._s[618]! } + public var Watch_Suggestion_OK: String { return self._s[638]! } + public var Compose_TokenListPlaceholder: String { return self._s[639]! } + public var InviteLink_PermanentLink: String { return self._s[640]! } + public var EditTheme_Edit_TopInfo: String { return self._s[641]! } + public var Gif_NoGifsFound: String { return self._s[642]! } + public var Login_InvalidCountryCode: String { return self._s[643]! } + public var SettingsSearch_Synonyms_Notifications_ChannelNotificationsExceptions: String { return self._s[644]! } + public var Call_VoiceOver_VideoCallMissed: String { return self._s[645]! } + public var VoiceChat_ChangeNameTitle: String { return self._s[647]! } public func PUSH_LOCKED_MESSAGE(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[620]!, self._r[620]!, [_1]) + return formatWithArgumentRanges(self._s[648]!, self._r[648]!, [_1]) } - public var Profile_CreateNewContact: String { return self._s[621]! } - public var AutoDownloadSettings_DataUsageLow: String { return self._s[622]! } - public var SettingsSearch_Synonyms_Notifications_InAppNotificationsPreview: String { return self._s[623]! } - public var Group_Setup_TypePublic: String { return self._s[624]! } - public var Weekday_ShortSaturday: String { return self._s[625]! } + public var Profile_CreateNewContact: String { return self._s[649]! } + public var AutoDownloadSettings_DataUsageLow: String { return self._s[650]! } + public var SettingsSearch_Synonyms_Notifications_InAppNotificationsPreview: String { return self._s[651]! } + public var Group_Setup_TypePublic: String { return self._s[652]! } + public var Weekday_ShortSaturday: String { return self._s[653]! } public func Time_MonthOfYear_m12(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[626]!, self._r[626]!, [_0]) + return formatWithArgumentRanges(self._s[654]!, self._r[654]!, [_0]) } - public var LiveLocation_MenuStopAll: String { return self._s[627]! } + public var LiveLocation_MenuStopAll: String { return self._s[655]! } public func DialogList_EncryptedChatStartedIncoming(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[628]!, self._r[628]!, [_0]) + return formatWithArgumentRanges(self._s[656]!, self._r[656]!, [_0]) } - public var ChatListFolder_NamePlaceholder: String { return self._s[629]! } - public var Channel_OwnershipTransfer_ErrorPublicChannelsTooMuch: String { return self._s[630]! } + public var ChatListFolder_NamePlaceholder: String { return self._s[657]! } + public var Channel_OwnershipTransfer_ErrorPublicChannelsTooMuch: String { return self._s[658]! } public func PUSH_CHAT_MESSAGE_GAME(_ _1: String, _ _2: String, _ _3: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[631]!, self._r[631]!, [_1, _2, _3]) + return formatWithArgumentRanges(self._s[659]!, self._r[659]!, [_1, _2, _3]) } - public var VoiceChat_ChatFullAlertText: String { return self._s[632]! } - public var Chat_GenericPsaTooltip: String { return self._s[634]! } - public var ChannelInfo_CreateVoiceChat: String { return self._s[635]! } + public var VoiceChat_ChatFullAlertText: String { return self._s[660]! } + public var Chat_GenericPsaTooltip: String { return self._s[662]! } + public var ChannelInfo_CreateVoiceChat: String { return self._s[663]! } public func Message_ForwardedMessageShort(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[636]!, self._r[636]!, [_0]) + return formatWithArgumentRanges(self._s[664]!, self._r[664]!, [_0]) } - public var PrivacyLastSeenSettings_AlwaysShareWith_Placeholder: String { return self._s[637]! } - public var Login_PhoneAndCountryHelp: String { return self._s[638]! } - public var SaveIncomingPhotosSettings_From: String { return self._s[640]! } - public var Conversation_JumpToDate: String { return self._s[641]! } - public var AuthSessions_AddDevice: String { return self._s[642]! } - public var Settings_FAQ: String { return self._s[644]! } + public var PrivacyLastSeenSettings_AlwaysShareWith_Placeholder: String { return self._s[665]! } + public var Login_PhoneAndCountryHelp: String { return self._s[666]! } + public var SaveIncomingPhotosSettings_From: String { return self._s[668]! } + public var Conversation_JumpToDate: String { return self._s[669]! } + public var AuthSessions_AddDevice: String { return self._s[670]! } + public var Settings_FAQ: String { return self._s[672]! } public func ChatImport_CreateGroupAlertText(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[645]!, self._r[645]!, [_0]) + return formatWithArgumentRanges(self._s[673]!, self._r[673]!, [_0]) } - public var Username_Title: String { return self._s[646]! } - public var DialogList_Read: String { return self._s[647]! } - public var Conversation_InstantPagePreview: String { return self._s[648]! } - public var Report_Succeed: String { return self._s[650]! } - public var Login_ResetAccountProtected_Title: String { return self._s[651]! } - public var CallFeedback_ReasonDistortedSpeech: String { return self._s[652]! } - public var Channel_EditAdmin_PermissionChangeInfo: String { return self._s[653]! } + public var Username_Title: String { return self._s[674]! } + public var DialogList_Read: String { return self._s[675]! } + public var Conversation_InstantPagePreview: String { return self._s[676]! } + public var Report_Succeed: String { return self._s[678]! } + public var Login_ResetAccountProtected_Title: String { return self._s[679]! } + public var CallFeedback_ReasonDistortedSpeech: String { return self._s[680]! } + public var Channel_EditAdmin_PermissionChangeInfo: String { return self._s[681]! } public func Channel_AdminLog_MessageRankUsername(_ _1: String, _ _2: String, _ _3: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[654]!, self._r[654]!, [_1, _2, _3]) + return formatWithArgumentRanges(self._s[682]!, self._r[682]!, [_1, _2, _3]) } - public var WallpaperPreview_PreviewBottomText: String { return self._s[656]! } - public var Privacy_SecretChatsTitle: String { return self._s[659]! } + public var WallpaperPreview_PreviewBottomText: String { return self._s[684]! } + public var Privacy_SecretChatsTitle: String { return self._s[687]! } public func Notification_PassportValuesSentMessage(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[660]!, self._r[660]!, [_1, _2]) + return formatWithArgumentRanges(self._s[688]!, self._r[688]!, [_1, _2]) } - public var Checkout_NewCard_SaveInfoHelp: String { return self._s[661]! } - public var Conversation_ClousStorageInfo_Description4: String { return self._s[662]! } - public var PasscodeSettings_TurnPasscodeOn: String { return self._s[663]! } - public var Message_ReplyActionButtonShowReceipt: String { return self._s[664]! } + public var Checkout_NewCard_SaveInfoHelp: String { return self._s[689]! } + public var Conversation_ClousStorageInfo_Description4: String { return self._s[690]! } + public var PasscodeSettings_TurnPasscodeOn: String { return self._s[691]! } + public var Message_ReplyActionButtonShowReceipt: String { return self._s[692]! } public func PrivacyPolicy_AgeVerificationMessage(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[665]!, self._r[665]!, [_0]) + return formatWithArgumentRanges(self._s[693]!, self._r[693]!, [_0]) } - public var GroupInfo_DeleteAndExitConfirmation: String { return self._s[667]! } - public var TwoStepAuth_ConfirmationAbort: String { return self._s[668]! } - public var PrivacySettings_LastSeenEverybody: String { return self._s[669]! } - public var CallFeedback_ReasonDropped: String { return self._s[670]! } + public var GroupInfo_DeleteAndExitConfirmation: String { return self._s[695]! } + public var TwoStepAuth_ConfirmationAbort: String { return self._s[696]! } + public var PrivacySettings_LastSeenEverybody: String { return self._s[697]! } + public var CallFeedback_ReasonDropped: String { return self._s[698]! } public func ScheduledMessages_ScheduledDate(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[671]!, self._r[671]!, [_0]) + return formatWithArgumentRanges(self._s[699]!, self._r[699]!, [_0]) } - public var WebSearch_Images: String { return self._s[672]! } - public var Passport_Identity_Surname: String { return self._s[673]! } - public var Channel_Stickers_CreateYourOwn: String { return self._s[674]! } - public var TwoFactorSetup_Email_Title: String { return self._s[675]! } - public var Cache_ClearEmpty: String { return self._s[676]! } - public var AuthSessions_AddDeviceIntro_Action: String { return self._s[677]! } - public var Theme_Context_Apply: String { return self._s[678]! } - public var GroupInfo_Permissions_SearchPlaceholder: String { return self._s[679]! } - public var CallList_DeleteAllForEveryone: String { return self._s[680]! } + public var WebSearch_Images: String { return self._s[700]! } + public var Passport_Identity_Surname: String { return self._s[701]! } + public var Channel_Stickers_CreateYourOwn: String { return self._s[702]! } + public var TwoFactorSetup_Email_Title: String { return self._s[703]! } + public var Cache_ClearEmpty: String { return self._s[704]! } + public var AuthSessions_AddDeviceIntro_Action: String { return self._s[705]! } + public var Theme_Context_Apply: String { return self._s[706]! } + public var GroupInfo_Permissions_SearchPlaceholder: String { return self._s[707]! } + public var CallList_DeleteAllForEveryone: String { return self._s[708]! } public func BroadcastGroups_Success(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[681]!, self._r[681]!, [_0]) + return formatWithArgumentRanges(self._s[709]!, self._r[709]!, [_0]) } - public var AutoDownloadSettings_DocumentsTitle: String { return self._s[682]! } + public var AutoDownloadSettings_DocumentsTitle: String { return self._s[710]! } public func NetworkUsageSettings_CellularUsageSince(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[683]!, self._r[683]!, [_0]) + return formatWithArgumentRanges(self._s[711]!, self._r[711]!, [_0]) } - public var Call_StatusRinging: String { return self._s[684]! } + public var Call_StatusRinging: String { return self._s[712]! } public func Map_DistanceAway(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[685]!, self._r[685]!, [_0]) + return formatWithArgumentRanges(self._s[713]!, self._r[713]!, [_0]) } public func DialogList_SingleTypingSuffix(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[686]!, self._r[686]!, [_0]) + return formatWithArgumentRanges(self._s[714]!, self._r[714]!, [_0]) } - public var Cache_ClearNone: String { return self._s[687]! } - public var PrivacyPolicy_Accept: String { return self._s[688]! } - public var Contacts_PhoneNumber: String { return self._s[689]! } - public var Passport_Identity_OneOfTypePassport: String { return self._s[690]! } - public var PhotoEditor_HighlightsTint: String { return self._s[692]! } - public var AutoDownloadSettings_AutodownloadVideos: String { return self._s[693]! } - public var Checkout_PaymentMethod_Title: String { return self._s[696]! } - public var Month_GenAugust: String { return self._s[698]! } - public var DialogList_Draft: String { return self._s[699]! } - public var ChatList_EmptyChatListFilterText: String { return self._s[700]! } - public var PeopleNearby_Description: String { return self._s[701]! } - public var WallpaperPreview_SwipeColorsBottomText: String { return self._s[702]! } - public var VoiceChat_InviteLink_CopyListenerLink: String { return self._s[703]! } - public var VoiceChat_EditTitleRemoveSuccess: String { return self._s[704]! } - public var SettingsSearch_Synonyms_Privacy_Data_TopPeers: String { return self._s[706]! } - public var Watch_Message_ForwardedFrom: String { return self._s[707]! } - public var Notification_Mute1h: String { return self._s[708]! } - public var Appearance_ThemePreview_Chat_3_TextWithLink: String { return self._s[709]! } - public var SettingsSearch_Synonyms_Privacy_AuthSessions: String { return self._s[711]! } - public var Channel_Edit_LinkItem: String { return self._s[712]! } - public var Presence_online: String { return self._s[713]! } - public var AutoDownloadSettings_Title: String { return self._s[714]! } - public var Conversation_MessageDialogRetry: String { return self._s[715]! } - public var SettingsSearch_Synonyms_ChatSettings_OpenLinksIn: String { return self._s[717]! } - public var Channel_About_Placeholder: String { return self._s[719]! } - public var Passport_Language_sl: String { return self._s[720]! } - public var AppleWatch_Title: String { return self._s[722]! } - public var RepliesChat_DescriptionText: String { return self._s[724]! } - public var Stats_Message_PrivateShares: String { return self._s[725]! } - public var Settings_ViewPhoto: String { return self._s[726]! } - public var Conversation_ForwardTooltip_SavedMessages_One: String { return self._s[727]! } - public var ChatList_DeleteSavedMessagesConfirmation: String { return self._s[728]! } - public var Cache_ClearProgress: String { return self._s[729]! } - public var Cache_Music: String { return self._s[730]! } - public var Conversation_ContextMenuShare: String { return self._s[732]! } - public var AutoDownloadSettings_Unlimited: String { return self._s[733]! } - public var Channel_OwnershipTransfer_ErrorPrivacyRestricted: String { return self._s[734]! } - public var Contacts_PermissionsAllow: String { return self._s[735]! } - public var Passport_Language_vi: String { return self._s[737]! } + public var Cache_ClearNone: String { return self._s[715]! } + public var PrivacyPolicy_Accept: String { return self._s[716]! } + public var Contacts_PhoneNumber: String { return self._s[717]! } + public var Passport_Identity_OneOfTypePassport: String { return self._s[718]! } + public var PhotoEditor_HighlightsTint: String { return self._s[720]! } + public var AutoDownloadSettings_AutodownloadVideos: String { return self._s[721]! } + public var Checkout_PaymentMethod_Title: String { return self._s[724]! } + public var Month_GenAugust: String { return self._s[726]! } + public var DialogList_Draft: String { return self._s[727]! } + public var ChatList_EmptyChatListFilterText: String { return self._s[728]! } + public var PeopleNearby_Description: String { return self._s[729]! } + public var WallpaperPreview_SwipeColorsBottomText: String { return self._s[730]! } + public var VoiceChat_InviteLink_CopyListenerLink: String { return self._s[731]! } + public var VoiceChat_EditTitleRemoveSuccess: String { return self._s[732]! } + public var SettingsSearch_Synonyms_Privacy_Data_TopPeers: String { return self._s[734]! } + public var Watch_Message_ForwardedFrom: String { return self._s[735]! } + public var Notification_Mute1h: String { return self._s[736]! } + public var Appearance_ThemePreview_Chat_3_TextWithLink: String { return self._s[737]! } + public var SettingsSearch_Synonyms_Privacy_AuthSessions: String { return self._s[739]! } + public var Channel_Edit_LinkItem: String { return self._s[740]! } + public var Presence_online: String { return self._s[741]! } + public var AutoDownloadSettings_Title: String { return self._s[742]! } + public var Conversation_MessageDialogRetry: String { return self._s[743]! } + public var SettingsSearch_Synonyms_ChatSettings_OpenLinksIn: String { return self._s[745]! } + public var Channel_About_Placeholder: String { return self._s[747]! } + public var Passport_Language_sl: String { return self._s[748]! } + public var AppleWatch_Title: String { return self._s[750]! } + public var RepliesChat_DescriptionText: String { return self._s[752]! } + public var Stats_Message_PrivateShares: String { return self._s[753]! } + public var Settings_ViewPhoto: String { return self._s[754]! } + public var Conversation_ForwardTooltip_SavedMessages_One: String { return self._s[755]! } + public var ChatList_DeleteSavedMessagesConfirmation: String { return self._s[756]! } + public var Cache_ClearProgress: String { return self._s[757]! } + public var Cache_Music: String { return self._s[758]! } + public var Conversation_ContextMenuShare: String { return self._s[760]! } + public var AutoDownloadSettings_Unlimited: String { return self._s[761]! } + public var Channel_OwnershipTransfer_ErrorPrivacyRestricted: String { return self._s[762]! } + public var Contacts_PermissionsAllow: String { return self._s[763]! } + public var Passport_Language_vi: String { return self._s[765]! } + public var TwoFactorSetup_PasswordRecovery_PlaceholderPassword: String { return self._s[766]! } public func PUSH_MESSAGE_TEXT(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[740]!, self._r[740]!, [_1, _2]) + return formatWithArgumentRanges(self._s[769]!, self._r[769]!, [_1, _2]) } - public var Passport_Language_de: String { return self._s[741]! } - public var Notifications_PermissionsText: String { return self._s[743]! } - public var GroupRemoved_AddToGroup: String { return self._s[744]! } - public var Appearance_ThemePreview_ChatList_4_Text: String { return self._s[745]! } - public var ChangePhoneNumberCode_RequestingACall: String { return self._s[746]! } - public var Login_TermsOfServiceAgree: String { return self._s[747]! } - public var VoiceOver_Navigation_ProxySettings: String { return self._s[748]! } + public var Passport_Language_de: String { return self._s[770]! } + public var Notifications_PermissionsText: String { return self._s[772]! } + public var GroupRemoved_AddToGroup: String { return self._s[773]! } + public var Appearance_ThemePreview_ChatList_4_Text: String { return self._s[774]! } + public var ChangePhoneNumberCode_RequestingACall: String { return self._s[775]! } + public var Login_TermsOfServiceAgree: String { return self._s[776]! } + public var VoiceOver_Navigation_ProxySettings: String { return self._s[777]! } public func PUSH_CHAT_JOINED(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[749]!, self._r[749]!, [_1, _2]) + return formatWithArgumentRanges(self._s[778]!, self._r[778]!, [_1, _2]) } - public var SettingsSearch_Synonyms_Data_CallsUseLessData: String { return self._s[751]! } + public var SettingsSearch_Synonyms_Data_CallsUseLessData: String { return self._s[780]! } + public var VoiceChat_VideoPreviewStopScreenSharing: String { return self._s[781]! } public func PUSH_CHAT_VOICECHAT_START(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[752]!, self._r[752]!, [_1, _2]) + return formatWithArgumentRanges(self._s[782]!, self._r[782]!, [_1, _2]) } - public var ChatListFolder_NameGroups: String { return self._s[753]! } - public var SocksProxySetup_ProxyDetailsTitle: String { return self._s[754]! } + public var ChatListFolder_NameGroups: String { return self._s[783]! } + public var SocksProxySetup_ProxyDetailsTitle: String { return self._s[784]! } + public var VoiceChat_EditDescriptionSave: String { return self._s[785]! } public func Channel_AdminLog_MessageChangedLinkedGroup(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[755]!, self._r[755]!, [_1, _2]) + return formatWithArgumentRanges(self._s[786]!, self._r[786]!, [_1, _2]) } - public var Watch_Suggestion_TalkLater: String { return self._s[756]! } - public var Checkout_ShippingOption_Title: String { return self._s[757]! } - public var Conversation_TitleRepliesEmpty: String { return self._s[758]! } - public var CreatePoll_TextHeader: String { return self._s[759]! } - public var VoiceOver_Chat_Message: String { return self._s[761]! } - public var InfoPlist_NSLocationWhenInUseUsageDescription: String { return self._s[762]! } - public var ContactInfo_Note: String { return self._s[764]! } - public var Channel_AdminLog_InfoPanelAlertText: String { return self._s[765]! } - public var Checkout_NewCard_CardholderNameTitle: String { return self._s[766]! } - public var AutoDownloadSettings_Photos: String { return self._s[767]! } - public var UserInfo_NotificationsDefaultDisabled: String { return self._s[768]! } + public var Watch_Suggestion_TalkLater: String { return self._s[787]! } + public var Checkout_ShippingOption_Title: String { return self._s[788]! } + public var Conversation_TitleRepliesEmpty: String { return self._s[789]! } + public var CreatePoll_TextHeader: String { return self._s[790]! } + public var VoiceOver_Chat_Message: String { return self._s[792]! } + public var InfoPlist_NSLocationWhenInUseUsageDescription: String { return self._s[793]! } + public var ContactInfo_Note: String { return self._s[795]! } + public var Channel_AdminLog_InfoPanelAlertText: String { return self._s[796]! } + public var Checkout_NewCard_CardholderNameTitle: String { return self._s[797]! } + public var AutoDownloadSettings_Photos: String { return self._s[798]! } + public var UserInfo_NotificationsDefaultDisabled: String { return self._s[799]! } public func Conversation_ForwardTooltip_Chat_One(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[769]!, self._r[769]!, [_0]) + return formatWithArgumentRanges(self._s[800]!, self._r[800]!, [_0]) } - public var Channel_Info_Subscribers: String { return self._s[770]! } - public var ChatList_DeleteForCurrentUser: String { return self._s[771]! } - public var ChatListFolderSettings_FoldersSection: String { return self._s[772]! } - public var VoiceOver_ChatList_OutgoingMessage: String { return self._s[773]! } + public var Channel_Info_Subscribers: String { return self._s[801]! } + public var ChatList_DeleteForCurrentUser: String { return self._s[802]! } + public var ChatListFolderSettings_FoldersSection: String { return self._s[803]! } + public var ChannelInfo_ScheduleVoiceChat: String { return self._s[804]! } + public var VoiceOver_ChatList_OutgoingMessage: String { return self._s[805]! } public func Time_PreciseDate_m9(_ _1: String, _ _2: String, _ _3: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[777]!, self._r[777]!, [_1, _2, _3]) + return formatWithArgumentRanges(self._s[809]!, self._r[809]!, [_1, _2, _3]) } - public var AutoNightTheme_System: String { return self._s[778]! } - public var Call_StatusWaiting: String { return self._s[779]! } - public var GroupInfo_GroupHistoryHidden: String { return self._s[780]! } + public var AutoNightTheme_System: String { return self._s[810]! } + public var Call_StatusWaiting: String { return self._s[811]! } + public var GroupInfo_GroupHistoryHidden: String { return self._s[812]! } public func CHAT_MESSAGE_INVOICE(_ _1: String, _ _2: String, _ _3: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[781]!, self._r[781]!, [_1, _2, _3]) + return formatWithArgumentRanges(self._s[813]!, self._r[813]!, [_1, _2, _3]) } - public var Conversation_ContextMenuCopy: String { return self._s[783]! } - public var Notifications_MessageNotificationsPreview: String { return self._s[784]! } - public var Notifications_InAppNotificationsVibrate: String { return self._s[785]! } + public var Conversation_ContextMenuCopy: String { return self._s[815]! } + public var Notifications_MessageNotificationsPreview: String { return self._s[816]! } + public var Notifications_InAppNotificationsVibrate: String { return self._s[817]! } public func Conversation_RestrictedTextTimed(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[786]!, self._r[786]!, [_0]) + return formatWithArgumentRanges(self._s[818]!, self._r[818]!, [_0]) } - public var Group_Status: String { return self._s[788]! } - public var Group_Setup_HistoryVisible: String { return self._s[789]! } - public var Conversation_UploadFileTooLarge: String { return self._s[790]! } - public var Conversation_DiscardVoiceMessageAction: String { return self._s[791]! } - public var Paint_Edit: String { return self._s[792]! } - public var PeerInfo_AutoremoveMessages: String { return self._s[793]! } + public var Group_Status: String { return self._s[820]! } + public var Group_Setup_HistoryVisible: String { return self._s[821]! } + public var Conversation_UploadFileTooLarge: String { return self._s[822]! } + public var Conversation_DiscardVoiceMessageAction: String { return self._s[823]! } + public var Paint_Edit: String { return self._s[824]! } + public var PeerInfo_AutoremoveMessages: String { return self._s[825]! } public func ChatImport_SelectionConfirmationGroupWithoutTitle(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[795]!, self._r[795]!, [_0]) + return formatWithArgumentRanges(self._s[827]!, self._r[827]!, [_0]) } - public var Channel_EditAdmin_CannotEdit: String { return self._s[796]! } - public var Username_InvalidTooShort: String { return self._s[797]! } - public var ClearCache_StorageOtherApps: String { return self._s[798]! } - public var Conversation_ViewMessage: String { return self._s[799]! } - public var GroupInfo_PublicLinkAdd: String { return self._s[801]! } + public var Channel_EditAdmin_CannotEdit: String { return self._s[828]! } + public var Username_InvalidTooShort: String { return self._s[829]! } + public var ClearCache_StorageOtherApps: String { return self._s[831]! } + public var Conversation_ViewMessage: String { return self._s[832]! } + public var GroupInfo_PublicLinkAdd: String { return self._s[834]! } public func Notification_RemovedGroupPhoto(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[802]!, self._r[802]!, [_0]) + return formatWithArgumentRanges(self._s[835]!, self._r[835]!, [_0]) } - public var CallSettings_Title: String { return self._s[803]! } + public var CallSettings_Title: String { return self._s[836]! } public func Conversation_BotInteractiveUrlAlert(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[804]!, self._r[804]!, [_0]) - } - public func VoiceOver_Chat_ContactFrom(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[807]!, self._r[807]!, [_0]) - } - public var PUSH_SENDER_YOU: String { return self._s[810]! } - public func Conversation_DeletedFromContacts(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[811]!, self._r[811]!, [_0]) - } - public var Profile_ShareContactButton: String { return self._s[812]! } - public var GroupInfo_Permissions_SectionTitle: String { return self._s[813]! } - public func VoiceOver_Chat_StickerFrom(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[814]!, self._r[814]!, [_0]) - } - public var Map_ShareLiveLocation: String { return self._s[815]! } - public var ChatListFolder_TitleEdit: String { return self._s[816]! } - public func VoiceOver_Chat_AnimatedStickerFrom(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[817]!, self._r[817]!, [_0]) - } - public var Passport_Address_Address: String { return self._s[819]! } - public var LastSeen_JustNow: String { return self._s[821]! } - public func SecretImage_NotViewedYet(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[822]!, self._r[822]!, [_0]) - } - public var ContactInfo_PhoneLabelOther: String { return self._s[823]! } - public var PasscodeSettings_DoNotMatch: String { return self._s[824]! } - public var Weekday_Today: String { return self._s[827]! } - public var DialogList_Title: String { return self._s[828]! } - public var SettingsSearch_Synonyms_Notifications_MessageNotificationsPreview: String { return self._s[829]! } - public var Cache_ClearCache: String { return self._s[830]! } - public var CreatePoll_ExplanationInfo: String { return self._s[831]! } - public var Notifications_ResetAllNotificationsHelp: String { return self._s[833]! } - public var Stats_MessageTitle: String { return self._s[834]! } - public var Passport_Address_Street: String { return self._s[836]! } - public func Channel_AdminLog_MessageRemovedGroupUsername(_ _0: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(self._s[837]!, self._r[837]!, [_0]) } - public var Channel_AdminLog_ChannelEmptyText: String { return self._s[838]! } - public func Login_PhoneGenericEmailSubject(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[839]!, self._r[839]!, [_0]) + public func VoiceOver_Chat_ContactFrom(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[840]!, self._r[840]!, [_0]) } - public var TwoStepAuth_Email: String { return self._s[841]! } - public var Conversation_SecretLinkPreviewAlert: String { return self._s[842]! } - public var PrivacySettings_PasscodeOn: String { return self._s[843]! } - public var Camera_SquareMode: String { return self._s[845]! } - public var SocksProxySetup_Port: String { return self._s[846]! } - public var Watch_LastSeen_JustNow: String { return self._s[848]! } + public var PUSH_SENDER_YOU: String { return self._s[843]! } + public func Conversation_DeletedFromContacts(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[844]!, self._r[844]!, [_0]) + } + public var Profile_ShareContactButton: String { return self._s[845]! } + public var GroupInfo_Permissions_SectionTitle: String { return self._s[846]! } + public func VoiceOver_Chat_StickerFrom(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[847]!, self._r[847]!, [_0]) + } + public var Map_ShareLiveLocation: String { return self._s[848]! } + public var ChatListFolder_TitleEdit: String { return self._s[849]! } + public func VoiceOver_Chat_AnimatedStickerFrom(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[850]!, self._r[850]!, [_0]) + } + public var Passport_Address_Address: String { return self._s[852]! } + public var LastSeen_JustNow: String { return self._s[854]! } + public func SecretImage_NotViewedYet(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[855]!, self._r[855]!, [_0]) + } + public var ContactInfo_PhoneLabelOther: String { return self._s[856]! } + public var PasscodeSettings_DoNotMatch: String { return self._s[857]! } + public var Weekday_Today: String { return self._s[860]! } + public var DialogList_Title: String { return self._s[861]! } + public var SettingsSearch_Synonyms_Notifications_MessageNotificationsPreview: String { return self._s[862]! } + public var Cache_ClearCache: String { return self._s[863]! } + public var CreatePoll_ExplanationInfo: String { return self._s[864]! } + public var Notifications_ResetAllNotificationsHelp: String { return self._s[866]! } + public var Stats_MessageTitle: String { return self._s[867]! } + public var Passport_Address_Street: String { return self._s[869]! } + public func Channel_AdminLog_MessageRemovedGroupUsername(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[870]!, self._r[870]!, [_0]) + } + public var Channel_AdminLog_ChannelEmptyText: String { return self._s[871]! } + public func Login_PhoneGenericEmailSubject(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[872]!, self._r[872]!, [_0]) + } + public var TwoStepAuth_Email: String { return self._s[874]! } + public var Conversation_SecretLinkPreviewAlert: String { return self._s[875]! } + public var PrivacySettings_PasscodeOn: String { return self._s[876]! } + public var Camera_SquareMode: String { return self._s[878]! } + public var SocksProxySetup_Port: String { return self._s[879]! } + public var Watch_LastSeen_JustNow: String { return self._s[881]! } public func Location_ProximityAlertSetText(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[849]!, self._r[849]!, [_1, _2]) + return formatWithArgumentRanges(self._s[882]!, self._r[882]!, [_1, _2]) } public func PUSH_MESSAGE_GAME(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[850]!, self._r[850]!, [_1, _2]) + return formatWithArgumentRanges(self._s[883]!, self._r[883]!, [_1, _2]) } public func Watch_LastSeen_YesterdayAt(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[851]!, self._r[851]!, [_0]) + return formatWithArgumentRanges(self._s[884]!, self._r[884]!, [_0]) } - public var EditTheme_Expand_Preview_OutgoingText: String { return self._s[852]! } - public var Channel_AdminLogFilter_EventsTitle: String { return self._s[853]! } - public var Watch_Suggestion_HoldOn: String { return self._s[856]! } + public var VoiceChat_CancelVoiceChat: String { return self._s[885]! } + public var EditTheme_Expand_Preview_OutgoingText: String { return self._s[886]! } + public var Channel_AdminLogFilter_EventsTitle: String { return self._s[887]! } + public var Watch_Suggestion_HoldOn: String { return self._s[890]! } public func PUSH_CHANNEL_MESSAGE_GEOLIVE(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[857]!, self._r[857]!, [_1]) + return formatWithArgumentRanges(self._s[891]!, self._r[891]!, [_1]) } - public var CallSettings_TabIcon: String { return self._s[858]! } - public var ScheduledMessages_SendNow: String { return self._s[859]! } - public var Stats_GroupTopWeekdaysTitle: String { return self._s[860]! } - public var UserInfo_PhoneCall: String { return self._s[861]! } - public var Month_GenMarch: String { return self._s[862]! } - public var Camera_Discard: String { return self._s[863]! } - public var InfoPlist_NSFaceIDUsageDescription: String { return self._s[864]! } - public var Passport_RequestedInformation: String { return self._s[865]! } - public var VoiceChat_RecordingTitlePlaceholder: String { return self._s[867]! } + public var CallSettings_TabIcon: String { return self._s[892]! } + public var ScheduledMessages_SendNow: String { return self._s[893]! } + public var Stats_GroupTopWeekdaysTitle: String { return self._s[894]! } + public var ImportStickerPack_NamePlaceholder: String { return self._s[895]! } + public var UserInfo_PhoneCall: String { return self._s[896]! } + public var Month_GenMarch: String { return self._s[897]! } + public var Camera_Discard: String { return self._s[898]! } + public var InfoPlist_NSFaceIDUsageDescription: String { return self._s[899]! } + public var Passport_RequestedInformation: String { return self._s[900]! } + public var VoiceChat_RecordingTitlePlaceholder: String { return self._s[902]! } public func Notification_ProximityYouReached(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[868]!, self._r[868]!, [_1, _2]) + return formatWithArgumentRanges(self._s[903]!, self._r[903]!, [_1, _2]) } - public var Passport_Language_ro: String { return self._s[869]! } + public var Passport_Language_ro: String { return self._s[904]! } public func PUSH_CHAT_MESSAGE_DOC(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[870]!, self._r[870]!, [_1, _2]) - } - public var AutoDownloadSettings_ResetHelp: String { return self._s[871]! } - public var Passport_Identity_DocumentDetails: String { return self._s[873]! } - public var Passport_Address_ScansHelp: String { return self._s[874]! } - public var Location_LiveLocationRequired_Title: String { return self._s[875]! } - public var ClearCache_StorageCache: String { return self._s[876]! } - public var Theme_Colors_ColorWallpaperWarningProceed: String { return self._s[877]! } - public var Conversation_RestrictedText: String { return self._s[878]! } - public var Notifications_MessageNotifications: String { return self._s[880]! } - public var Passport_Scans: String { return self._s[881]! } - public var TwoStepAuth_SetupHintTitle: String { return self._s[883]! } - public var LogoutOptions_ContactSupportTitle: String { return self._s[884]! } - public var Passport_Identity_SelfieHelp: String { return self._s[885]! } - public var Permissions_NotificationsUnreachableText_v0: String { return self._s[886]! } - public var Privacy_PaymentsClear_PaymentInfo: String { return self._s[887]! } - public var ShareMenu_CopyShareLinkGame: String { return self._s[888]! } - public var PeerInfo_ButtonSearch: String { return self._s[889]! } - public func Notification_ProximityReachedYou(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[892]!, self._r[892]!, [_1, _2]) - } - public var SettingsSearch_Synonyms_Privacy_Data_ClearPaymentsInfo: String { return self._s[893]! } - public var Passport_FieldIdentityTranslationHelp: String { return self._s[895]! } - public var Conversation_InputTextSilentBroadcastPlaceholder: String { return self._s[896]! } - public var Month_GenSeptember: String { return self._s[897]! } - public func Call_GroupFormat(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[899]!, self._r[899]!, [_1, _2]) - } - public var StickerPacksSettings_ArchivedPacks: String { return self._s[900]! } - public func Notification_VoiceChatInvitation(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[902]!, self._r[902]!, [_1, _2]) - } - public func Channel_Username_LinkHint(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[903]!, self._r[903]!, [_0]) - } - public func PUSH_PINNED_CONTACT(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(self._s[905]!, self._r[905]!, [_1, _2]) } + public var AutoDownloadSettings_ResetHelp: String { return self._s[906]! } + public var Passport_Identity_DocumentDetails: String { return self._s[908]! } + public var Passport_Address_ScansHelp: String { return self._s[909]! } + public var Location_LiveLocationRequired_Title: String { return self._s[910]! } + public var WallpaperPreview_PreviewBottomTextAnimatable: String { return self._s[911]! } + public var ClearCache_StorageCache: String { return self._s[912]! } + public var Theme_Colors_ColorWallpaperWarningProceed: String { return self._s[913]! } + public var Conversation_RestrictedText: String { return self._s[914]! } + public var Notifications_MessageNotifications: String { return self._s[916]! } + public var Passport_Scans: String { return self._s[917]! } + public func VoiceChat_StatusStartsIn(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[919]!, self._r[919]!, [_0]) + } + public var TwoStepAuth_SetupHintTitle: String { return self._s[920]! } + public var LogoutOptions_ContactSupportTitle: String { return self._s[921]! } + public var Passport_Identity_SelfieHelp: String { return self._s[922]! } + public var Permissions_NotificationsUnreachableText_v0: String { return self._s[923]! } + public var Privacy_PaymentsClear_PaymentInfo: String { return self._s[924]! } + public var ShareMenu_CopyShareLinkGame: String { return self._s[925]! } + public var PeerInfo_ButtonSearch: String { return self._s[926]! } + public func Notification_ProximityReachedYou(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[929]!, self._r[929]!, [_1, _2]) + } + public var SettingsSearch_Synonyms_Privacy_Data_ClearPaymentsInfo: String { return self._s[930]! } + public var Passport_FieldIdentityTranslationHelp: String { return self._s[932]! } + public var Conversation_InputTextSilentBroadcastPlaceholder: String { return self._s[933]! } + public var Month_GenSeptember: String { return self._s[934]! } + public func Call_GroupFormat(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[936]!, self._r[936]!, [_1, _2]) + } + public var StickerPacksSettings_ArchivedPacks: String { return self._s[937]! } + public func Notification_VoiceChatInvitation(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[939]!, self._r[939]!, [_1, _2]) + } + public func Channel_Username_LinkHint(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[940]!, self._r[940]!, [_0]) + } + public func PUSH_PINNED_CONTACT(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[942]!, self._r[942]!, [_1, _2]) + } public func PUSH_MESSAGE_VIDEOS(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[906]!, self._r[906]!, [_1, _2]) + return formatWithArgumentRanges(self._s[943]!, self._r[943]!, [_1, _2]) } - public var Calls_NotNow: String { return self._s[908]! } - public var Settings_ChatFolders: String { return self._s[912]! } - public var Login_PadPhoneHelpTitle: String { return self._s[913]! } - public var TwoStepAuth_EnterPasswordInvalid: String { return self._s[914]! } - public var Widget_MessageAutoremoveTimerRemoved: String { return self._s[915]! } - public var VoiceChat_RecordingSaved: String { return self._s[916]! } - public var Settings_ChatBackground: String { return self._s[917]! } + public var Calls_NotNow: String { return self._s[945]! } + public var Settings_ChatFolders: String { return self._s[950]! } + public var Login_PadPhoneHelpTitle: String { return self._s[951]! } + public var TwoStepAuth_EnterPasswordInvalid: String { return self._s[952]! } + public var Widget_MessageAutoremoveTimerRemoved: String { return self._s[953]! } + public var VoiceChat_RecordingSaved: String { return self._s[954]! } + public var Settings_ChatBackground: String { return self._s[955]! } public func PUSH_CHAT_MESSAGE_CONTACT(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[919]!, self._r[919]!, [_1, _2]) + return formatWithArgumentRanges(self._s[957]!, self._r[957]!, [_1, _2]) } - public var ProxyServer_VoiceOver_Active: String { return self._s[920]! } - public var Call_StatusBusy: String { return self._s[921]! } - public var Conversation_MessageDeliveryFailed: String { return self._s[922]! } - public var Login_NetworkError: String { return self._s[924]! } - public var TwoStepAuth_SetupPasswordDescription: String { return self._s[925]! } - public var Privacy_Calls_Integration: String { return self._s[926]! } - public var DialogList_SearchSectionMessages: String { return self._s[927]! } - public var AutoDownloadSettings_VideosTitle: String { return self._s[928]! } - public var Preview_DeletePhoto: String { return self._s[929]! } - public var PrivacySettings_PhoneNumber: String { return self._s[931]! } - public var Forward_ErrorDisabledForChat: String { return self._s[932]! } - public var Watch_Compose_CurrentLocation: String { return self._s[933]! } - public var Settings_CallSettings: String { return self._s[934]! } - public var AutoDownloadSettings_TypePrivateChats: String { return self._s[935]! } - public var Conversation_StickerRemovedFromFavorites: String { return self._s[936]! } - public var ChatList_Context_MarkAllAsRead: String { return self._s[937]! } - public var ChatSettings_AutoPlayAnimations: String { return self._s[938]! } - public var SaveIncomingPhotosSettings_Title: String { return self._s[939]! } - public var OwnershipTransfer_SecurityRequirements: String { return self._s[940]! } - public var Map_LiveLocationFor1Hour: String { return self._s[941]! } + public var ProxyServer_VoiceOver_Active: String { return self._s[958]! } + public var Call_StatusBusy: String { return self._s[959]! } + public var Conversation_MessageDeliveryFailed: String { return self._s[960]! } + public var Login_NetworkError: String { return self._s[962]! } + public var TwoStepAuth_SetupPasswordDescription: String { return self._s[963]! } + public var Privacy_Calls_Integration: String { return self._s[964]! } + public var DialogList_SearchSectionMessages: String { return self._s[965]! } + public var AutoDownloadSettings_VideosTitle: String { return self._s[966]! } + public var Preview_DeletePhoto: String { return self._s[967]! } + public var VoiceChat_Video: String { return self._s[968]! } + public var PrivacySettings_PhoneNumber: String { return self._s[970]! } + public var Forward_ErrorDisabledForChat: String { return self._s[971]! } + public var Watch_Compose_CurrentLocation: String { return self._s[972]! } + public var Settings_CallSettings: String { return self._s[973]! } + public var TwoFactorRemember_Done_Action: String { return self._s[974]! } + public var AutoDownloadSettings_TypePrivateChats: String { return self._s[975]! } + public var Conversation_StickerRemovedFromFavorites: String { return self._s[976]! } + public var ChatList_Context_MarkAllAsRead: String { return self._s[977]! } + public var ChatSettings_AutoPlayAnimations: String { return self._s[978]! } + public var SaveIncomingPhotosSettings_Title: String { return self._s[979]! } + public var OwnershipTransfer_SecurityRequirements: String { return self._s[980]! } + public var Map_LiveLocationFor1Hour: String { return self._s[981]! } public func Privacy_GroupsAndChannels_InviteToGroupError(_ _0: String, _ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[942]!, self._r[942]!, [_0, _1]) + return formatWithArgumentRanges(self._s[982]!, self._r[982]!, [_0, _1]) } - public var VoiceChat_MutedByAdmin: String { return self._s[943]! } + public var VoiceChat_MutedByAdmin: String { return self._s[983]! } public func Notification_PinnedLiveLocationMessage(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[944]!, self._r[944]!, [_0]) + return formatWithArgumentRanges(self._s[984]!, self._r[984]!, [_0]) } - public var Conversation_UnvotePoll: String { return self._s[945]! } - public var TwoStepAuth_EnterEmailCode: String { return self._s[946]! } + public var Conversation_UnvotePoll: String { return self._s[985]! } + public var TwoStepAuth_EnterEmailCode: String { return self._s[986]! } public func LOCAL_MESSAGE_FWDS(_ _1: String, _ _2: Int) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[947]!, self._r[947]!, [_1, "\(_2)"]) + return formatWithArgumentRanges(self._s[987]!, self._r[987]!, [_1, "\(_2)"]) } - public var Passport_InfoTitle: String { return self._s[948]! } + public var Passport_InfoTitle: String { return self._s[988]! } public func Conversation_Bytes(_ _0: Int) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[949]!, self._r[949]!, ["\(_0)"]) + return formatWithArgumentRanges(self._s[989]!, self._r[989]!, ["\(_0)"]) } - public var AccentColor_Title: String { return self._s[950]! } + public var AccentColor_Title: String { return self._s[990]! } public func PUSH_MESSAGE_INVOICE(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[951]!, self._r[951]!, [_1, _2]) + return formatWithArgumentRanges(self._s[991]!, self._r[991]!, [_1, _2]) } public func Notification_JoinedChannel(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[954]!, self._r[954]!, [_0]) + return formatWithArgumentRanges(self._s[994]!, self._r[994]!, [_0]) } - public var AutoDownloadSettings_DataUsageCustom: String { return self._s[955]! } - public var Conversation_ShareBotLocationConfirmation: String { return self._s[956]! } - public var PrivacyPhoneNumberSettings_WhoCanSeeMyPhoneNumber: String { return self._s[957]! } - public var VoiceOver_Editing_ClearText: String { return self._s[958]! } - public var Conversation_Unarchive: String { return self._s[959]! } - public var Notification_CallOutgoing: String { return self._s[960]! } - public var Channel_Setup_PublicNoLink: String { return self._s[961]! } - public var Passport_Identity_GenderPlaceholder: String { return self._s[962]! } - public var Message_Animation: String { return self._s[963]! } - public var SettingsSearch_Synonyms_Appearance_Animations: String { return self._s[964]! } - public var ChatSettings_ConnectionType_Title: String { return self._s[965]! } + public var AutoDownloadSettings_DataUsageCustom: String { return self._s[995]! } + public var Conversation_ShareBotLocationConfirmation: String { return self._s[996]! } + public var PrivacyPhoneNumberSettings_WhoCanSeeMyPhoneNumber: String { return self._s[997]! } + public var VoiceOver_Editing_ClearText: String { return self._s[998]! } + public var Conversation_Unarchive: String { return self._s[999]! } + public var Notification_CallOutgoing: String { return self._s[1000]! } + public var Channel_Setup_PublicNoLink: String { return self._s[1001]! } + public var Passport_Identity_GenderPlaceholder: String { return self._s[1002]! } + public var Message_Animation: String { return self._s[1003]! } + public var SettingsSearch_Synonyms_Appearance_Animations: String { return self._s[1004]! } + public var ChatSettings_ConnectionType_Title: String { return self._s[1005]! } public func Watch_Time_ShortFullAt(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[966]!, self._r[966]!, [_1, _2]) + return formatWithArgumentRanges(self._s[1006]!, self._r[1006]!, [_1, _2]) } public func VoiceChat_StatusSpeakingVolume(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[967]!, self._r[967]!, [_0]) + return formatWithArgumentRanges(self._s[1007]!, self._r[1007]!, [_0]) } - public var Notification_CallBack: String { return self._s[968]! } - public var Appearance_Title: String { return self._s[971]! } - public var NotificationsSound_Glass: String { return self._s[973]! } - public var AutoDownloadSettings_CellularTitle: String { return self._s[975]! } - public var Notifications_PermissionsSuppressWarningTitle: String { return self._s[977]! } - public var ChatSearch_SearchPlaceholder: String { return self._s[978]! } - public var Passport_Identity_AddPassport: String { return self._s[979]! } - public var GroupPermission_NoAddMembers: String { return self._s[981]! } - public var ContactList_Context_SendMessage: String { return self._s[982]! } - public var PhotoEditor_GrainTool: String { return self._s[983]! } - public var Settings_CopyPhoneNumber: String { return self._s[984]! } - public var Passport_Address_City: String { return self._s[985]! } - public var ChannelRemoved_RemoveInfo: String { return self._s[986]! } - public var SocksProxySetup_Password: String { return self._s[988]! } - public var Settings_Passport: String { return self._s[989]! } - public var Channel_MessagePhotoUpdated: String { return self._s[991]! } - public var Stats_LanguagesTitle: String { return self._s[992]! } - public var ChatList_PeerTypeGroup: String { return self._s[993]! } - public var Privacy_Calls_P2PHelp: String { return self._s[994]! } - public var VoiceOver_Chat_PollNoVotes: String { return self._s[995]! } - public var Embed_PlayingInPIP: String { return self._s[996]! } - public var BlockedUsers_BlockUser: String { return self._s[999]! } - public var Login_CancelPhoneVerificationContinue: String { return self._s[1000]! } + public var Notification_CallBack: String { return self._s[1008]! } + public var Appearance_Title: String { return self._s[1011]! } + public var NotificationsSound_Glass: String { return self._s[1013]! } + public var AutoDownloadSettings_CellularTitle: String { return self._s[1015]! } + public var Notifications_PermissionsSuppressWarningTitle: String { return self._s[1017]! } + public var ChatSearch_SearchPlaceholder: String { return self._s[1018]! } + public var Passport_Identity_AddPassport: String { return self._s[1019]! } + public var GroupPermission_NoAddMembers: String { return self._s[1021]! } + public var ContactList_Context_SendMessage: String { return self._s[1022]! } + public var PhotoEditor_GrainTool: String { return self._s[1023]! } + public var Settings_CopyPhoneNumber: String { return self._s[1024]! } + public var Passport_Address_City: String { return self._s[1025]! } + public var VoiceChat_LeaveAndCancelVoiceChat: String { return self._s[1026]! } + public var ChannelRemoved_RemoveInfo: String { return self._s[1027]! } + public var SocksProxySetup_Password: String { return self._s[1029]! } + public var Settings_Passport: String { return self._s[1030]! } + public var Channel_MessagePhotoUpdated: String { return self._s[1032]! } + public var Stats_LanguagesTitle: String { return self._s[1033]! } + public var ChatList_PeerTypeGroup: String { return self._s[1034]! } + public var Privacy_Calls_P2PHelp: String { return self._s[1035]! } + public var VoiceOver_Chat_PollNoVotes: String { return self._s[1036]! } + public var Embed_PlayingInPIP: String { return self._s[1037]! } + public var ImportStickerPack_GeneratingLink: String { return self._s[1039]! } + public var BlockedUsers_BlockUser: String { return self._s[1041]! } + public var Login_CancelPhoneVerificationContinue: String { return self._s[1042]! } public func PUSH_CHANNEL_MESSAGE(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1001]!, self._r[1001]!, [_1]) + return formatWithArgumentRanges(self._s[1043]!, self._r[1043]!, [_1]) } - public var AuthSessions_LoggedIn: String { return self._s[1002]! } - public var Channel_AdminLog_MessagePreviousCaption: String { return self._s[1003]! } - public var Activity_UploadingDocument: String { return self._s[1004]! } - public var PeopleNearby_NoMembers: String { return self._s[1005]! } - public var SettingsSearch_Synonyms_Stickers_Masks: String { return self._s[1008]! } - public var ChatSettings_AutoPlayVideos: String { return self._s[1009]! } - public var VoiceOver_Chat_OpenLinkHint: String { return self._s[1010]! } - public var InstantPage_VoiceOver_IncreaseFontSize: String { return self._s[1011]! } - public var Settings_ViewVideo: String { return self._s[1012]! } - public var Map_ShowPlaces: String { return self._s[1014]! } - public var Passport_Phone_UseTelegramNumberHelp: String { return self._s[1015]! } - public var InviteLink_Create_Title: String { return self._s[1016]! } - public var Notification_CreatedGroup: String { return self._s[1017]! } - public var SettingsSearch_Synonyms_Appearance_ChatBackground_Custom: String { return self._s[1018]! } + public var AuthSessions_LoggedIn: String { return self._s[1044]! } + public var Channel_AdminLog_MessagePreviousCaption: String { return self._s[1045]! } + public var Activity_UploadingDocument: String { return self._s[1046]! } + public var PeopleNearby_NoMembers: String { return self._s[1047]! } + public var TwoFactorRemember_Text: String { return self._s[1049]! } + public var SettingsSearch_Synonyms_Stickers_Masks: String { return self._s[1051]! } + public var ChatSettings_AutoPlayVideos: String { return self._s[1052]! } + public var VoiceOver_Chat_OpenLinkHint: String { return self._s[1053]! } + public var InstantPage_VoiceOver_IncreaseFontSize: String { return self._s[1054]! } + public var Settings_ViewVideo: String { return self._s[1055]! } + public var Map_ShowPlaces: String { return self._s[1057]! } + public var Passport_Phone_UseTelegramNumberHelp: String { return self._s[1058]! } + public var InviteLink_Create_Title: String { return self._s[1059]! } + public var Notification_CreatedGroup: String { return self._s[1060]! } + public var SettingsSearch_Synonyms_Appearance_ChatBackground_Custom: String { return self._s[1061]! } public func PrivacySettings_LastSeenContactsPlus(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1019]!, self._r[1019]!, [_0]) + return formatWithArgumentRanges(self._s[1062]!, self._r[1062]!, [_0]) } - public var Conversation_StatusLeftGroup: String { return self._s[1020]! } - public var Theme_Colors_Messages: String { return self._s[1021]! } - public var AuthSessions_EmptyText: String { return self._s[1022]! } + public var Conversation_StatusLeftGroup: String { return self._s[1063]! } + public var Theme_Colors_Messages: String { return self._s[1064]! } + public var AuthSessions_EmptyText: String { return self._s[1065]! } public func PUSH_MESSAGE_CONTACT(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1023]!, self._r[1023]!, [_1]) + return formatWithArgumentRanges(self._s[1066]!, self._r[1066]!, [_1]) } - public var UserInfo_StartSecretChat: String { return self._s[1024]! } - public var ChatListFolderSettings_EditFoldersInfo: String { return self._s[1025]! } - public var Channel_Edit_PrivatePublicLinkAlert: String { return self._s[1026]! } - public var Conversation_ReportSpamGroupConfirmation: String { return self._s[1027]! } - public var Conversation_PrivateMessageLinkCopied: String { return self._s[1029]! } - public var PeerInfo_PaneFiles: String { return self._s[1030]! } - public var VoiceChat_DisplayAs: String { return self._s[1031]! } - public var PrivacySettings_AutoArchive: String { return self._s[1032]! } - public var Camera_VideoMode: String { return self._s[1033]! } - public var NotificationsSound_Alert: String { return self._s[1034]! } - public var Privacy_Forwards_NeverAllow_Title: String { return self._s[1035]! } - public var Appearance_AutoNightTheme: String { return self._s[1036]! } - public var Passport_Language_he: String { return self._s[1037]! } - public var Passport_InvalidPasswordError: String { return self._s[1038]! } - public var Conversation_PinMessageAlert_OnlyPin: String { return self._s[1039]! } - public var UserInfo_InviteBotToGroup: String { return self._s[1040]! } - public var Conversation_SilentBroadcastTooltipOff: String { return self._s[1041]! } - public var Common_TakePhoto: String { return self._s[1042]! } + public var UserInfo_StartSecretChat: String { return self._s[1067]! } + public var ChatListFolderSettings_EditFoldersInfo: String { return self._s[1068]! } + public var Channel_Edit_PrivatePublicLinkAlert: String { return self._s[1069]! } + public var TwoFactorSetup_ResetDone_Title: String { return self._s[1070]! } + public var Conversation_ReportSpamGroupConfirmation: String { return self._s[1071]! } + public var Conversation_PrivateMessageLinkCopied: String { return self._s[1073]! } + public var PeerInfo_PaneFiles: String { return self._s[1074]! } + public var VoiceChat_DisplayAs: String { return self._s[1075]! } + public var PrivacySettings_AutoArchive: String { return self._s[1076]! } + public var Camera_VideoMode: String { return self._s[1077]! } + public var NotificationsSound_Alert: String { return self._s[1078]! } + public var Privacy_Forwards_NeverAllow_Title: String { return self._s[1079]! } + public var Appearance_AutoNightTheme: String { return self._s[1080]! } + public var Passport_Language_he: String { return self._s[1081]! } + public var Passport_InvalidPasswordError: String { return self._s[1082]! } + public var Conversation_PinMessageAlert_OnlyPin: String { return self._s[1083]! } + public var UserInfo_InviteBotToGroup: String { return self._s[1084]! } + public var Conversation_SilentBroadcastTooltipOff: String { return self._s[1085]! } + public var Common_TakePhoto: String { return self._s[1086]! } public func Channel_AdminLog_RevokedInviteLink(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1043]!, self._r[1043]!, [_1, _2]) + return formatWithArgumentRanges(self._s[1087]!, self._r[1087]!, [_1, _2]) } - public var Passport_Email_UseTelegramEmailHelp: String { return self._s[1044]! } - public var ChatList_Context_JoinChannel: String { return self._s[1045]! } - public var MediaPlayer_UnknownArtist: String { return self._s[1046]! } - public var KeyCommand_JumpToPreviousUnreadChat: String { return self._s[1049]! } - public var Channel_OwnershipTransfer_Title: String { return self._s[1050]! } - public var EditTheme_UploadEditedTheme: String { return self._s[1051]! } - public var Settings_SetProfilePhotoOrVideo: String { return self._s[1053]! } - public var Passport_FieldOneOf_Delimeter: String { return self._s[1054]! } - public var MessagePoll_ViewResults: String { return self._s[1055]! } - public var Group_Setup_TypePrivateHelp: String { return self._s[1056]! } - public var Passport_Address_OneOfTypeUtilityBill: String { return self._s[1057]! } - public var ChatList_Search_ShowLess: String { return self._s[1058]! } - public var InviteLink_Create_UsersLimitNoLimit: String { return self._s[1059]! } - public var UserInfo_ShareBot: String { return self._s[1060]! } - public var Privacy_Calls_P2P: String { return self._s[1062]! } - public var WebBrowser_InAppSafari: String { return self._s[1063]! } - public var SharedMedia_EmptyFilesText: String { return self._s[1066]! } - public var Channel_AdminLog_MessagePreviousMessage: String { return self._s[1067]! } - public var GroupInfo_SetSound: String { return self._s[1068]! } - public var Permissions_PeopleNearbyAllowInSettings_v0: String { return self._s[1069]! } + public var Passport_Email_UseTelegramEmailHelp: String { return self._s[1088]! } + public var ChatList_Context_JoinChannel: String { return self._s[1089]! } + public var MediaPlayer_UnknownArtist: String { return self._s[1090]! } + public var VoiceChat_EditDescriptionText: String { return self._s[1091]! } + public var KeyCommand_JumpToPreviousUnreadChat: String { return self._s[1094]! } + public var Channel_OwnershipTransfer_Title: String { return self._s[1095]! } + public var EditTheme_UploadEditedTheme: String { return self._s[1096]! } + public var Settings_SetProfilePhotoOrVideo: String { return self._s[1098]! } + public var Passport_FieldOneOf_Delimeter: String { return self._s[1099]! } + public var MessagePoll_ViewResults: String { return self._s[1100]! } + public var Group_Setup_TypePrivateHelp: String { return self._s[1101]! } + public func UserInfo_ContactForwardTooltip_Chat_One(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[1102]!, self._r[1102]!, [_0]) + } + public var Passport_Address_OneOfTypeUtilityBill: String { return self._s[1103]! } + public var Privacy_PaymentsClear_ShippingInfoCleared: String { return self._s[1104]! } + public var ChatList_Search_ShowLess: String { return self._s[1105]! } + public var InviteLink_Create_UsersLimitNoLimit: String { return self._s[1106]! } + public var UserInfo_ShareBot: String { return self._s[1107]! } + public var Privacy_Calls_P2P: String { return self._s[1109]! } + public var WebBrowser_InAppSafari: String { return self._s[1110]! } + public var SharedMedia_EmptyFilesText: String { return self._s[1113]! } + public var Channel_AdminLog_MessagePreviousMessage: String { return self._s[1114]! } + public var GroupInfo_SetSound: String { return self._s[1115]! } + public var Permissions_PeopleNearbyAllowInSettings_v0: String { return self._s[1116]! } public func Conversation_AutoremoveRemainingTime(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1070]!, self._r[1070]!, [_0]) + return formatWithArgumentRanges(self._s[1117]!, self._r[1117]!, [_0]) } - public var Channel_AdminLog_MessagePreviousDescription: String { return self._s[1071]! } - public var Channel_AdminLogFilter_EventsAll: String { return self._s[1072]! } - public var CallSettings_UseLessData: String { return self._s[1073]! } - public var InfoPlist_NSCameraUsageDescription: String { return self._s[1074]! } - public var NotificationsSound_Chord: String { return self._s[1075]! } - public var PhotoEditor_CurvesTool: String { return self._s[1076]! } - public var Appearance_ThemePreview_Chat_2_Text: String { return self._s[1077]! } - public var Resolve_ErrorNotFound: String { return self._s[1078]! } - public var Activity_PlayingGame: String { return self._s[1079]! } + public var Channel_AdminLog_MessagePreviousDescription: String { return self._s[1118]! } + public var Channel_AdminLogFilter_EventsAll: String { return self._s[1119]! } + public var CallSettings_UseLessData: String { return self._s[1120]! } + public var InfoPlist_NSCameraUsageDescription: String { return self._s[1121]! } + public var NotificationsSound_Chord: String { return self._s[1122]! } + public var PhotoEditor_CurvesTool: String { return self._s[1123]! } + public var Appearance_ThemePreview_Chat_2_Text: String { return self._s[1124]! } + public var Resolve_ErrorNotFound: String { return self._s[1125]! } + public var Activity_PlayingGame: String { return self._s[1126]! } public func VoiceChat_InvitedPeerText(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1082]!, self._r[1082]!, [_0]) - } - public var StickerPacksSettings_AnimatedStickersInfo: String { return self._s[1083]! } - public func PUSH_CHANNEL_MESSAGE_GEO(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1084]!, self._r[1084]!, [_1]) - } - public var Conversation_ShareBotContactConfirmationTitle: String { return self._s[1085]! } - public var Notification_CallIncoming: String { return self._s[1086]! } - public var Stats_EnabledNotifications: String { return self._s[1087]! } - public var Notification_VoiceChatStartedChannel: String { return self._s[1088]! } - public var Notifications_PermissionsOpenSettings: String { return self._s[1089]! } - public var Checkout_ErrorProviderAccountTimeout: String { return self._s[1090]! } - public func Activity_RemindAboutChannel(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1091]!, self._r[1091]!, [_0]) - } - public var VoiceChat_StatusMutedYou: String { return self._s[1092]! } - public var VoiceOver_Chat_ReplyToYourMessage: String { return self._s[1093]! } - public var Channel_DiscussionGroup_MakeHistoryPublic: String { return self._s[1094]! } - public var StickerPacksSettings_Title: String { return self._s[1095]! } - public func Channel_AdminLog_MessageGroupPreHistoryVisible(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1096]!, self._r[1096]!, [_0]) - } - public var Watch_NoConnection: String { return self._s[1097]! } - public var EncryptionKey_Title: String { return self._s[1098]! } - public var Widget_AuthRequired: String { return self._s[1099]! } - public func PUSH_MESSAGE_ROUND(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1100]!, self._r[1100]!, [_1]) - } - public var Notifications_ExceptionsTitle: String { return self._s[1101]! } - public var EditTheme_Expand_TopInfo: String { return self._s[1102]! } - public func Contacts_AddPhoneNumber(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1103]!, self._r[1103]!, [_0]) - } - public var Channel_AdminLogFilter_EventsRestrictions: String { return self._s[1105]! } - public var Notifications_GroupNotificationsSound: String { return self._s[1106]! } - public var VoiceChat_SpeakPermissionAdmin: String { return self._s[1107]! } - public var Passport_Email_EnterOtherEmail: String { return self._s[1108]! } - public func VoiceChat_RemovePeerConfirmation(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1111]!, self._r[1111]!, [_0]) - } - public var Conversation_AddToContacts: String { return self._s[1112]! } - public var AutoDownloadSettings_DataUsageMedium: String { return self._s[1113]! } - public var AuthSessions_LogOutApplications: String { return self._s[1115]! } - public var VoiceChat_LeaveVoiceChat: String { return self._s[1116]! } - public var ChatList_Context_Unpin: String { return self._s[1117]! } - public var PeopleNearby_DiscoverDescription: String { return self._s[1118]! } - public var UserInfo_FakeBotWarning: String { return self._s[1119]! } - public var Notification_MessageLifetime1d: String { return self._s[1120]! } - public var PrivacyLastSeenSettings_NeverShareWith_Title: String { return self._s[1121]! } - public var ChatListFolder_CategoryChannels: String { return self._s[1122]! } - public var VoiceOver_Chat_SeenByRecipient: String { return self._s[1123]! } - public var Notifications_PermissionsAllow: String { return self._s[1124]! } - public var Undo_ScheduledMessagesCleared: String { return self._s[1125]! } - public var AutoDownloadSettings_PrivateChats: String { return self._s[1127]! } - public var ApplyLanguage_ChangeLanguageAction: String { return self._s[1128]! } - public var ChatImportActivity_ErrorInvalidChatType: String { return self._s[1129]! } - public func PrivacySettings_LastSeenNobodyPlus(_ _0: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(self._s[1130]!, self._r[1130]!, [_0]) } - public var Conversation_AutoremoveTimerRemovedChannel: String { return self._s[1132]! } - public var Notifications_MessageNotificationsHelp: String { return self._s[1134]! } - public var WallpaperSearch_ColorPink: String { return self._s[1135]! } - public var ContactInfo_PhoneNumberHidden: String { return self._s[1136]! } - public var Passport_Identity_IssueDate: String { return self._s[1138]! } + public var StickerPacksSettings_AnimatedStickersInfo: String { return self._s[1131]! } + public func PUSH_CHANNEL_MESSAGE_GEO(_ _1: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[1132]!, self._r[1132]!, [_1]) + } + public var Conversation_ShareBotContactConfirmationTitle: String { return self._s[1133]! } + public var Notification_CallIncoming: String { return self._s[1134]! } + public var Stats_EnabledNotifications: String { return self._s[1135]! } + public var Notification_VoiceChatStartedChannel: String { return self._s[1136]! } + public var Notifications_PermissionsOpenSettings: String { return self._s[1137]! } + public var Checkout_ErrorProviderAccountTimeout: String { return self._s[1138]! } + public func Activity_RemindAboutChannel(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[1139]!, self._r[1139]!, [_0]) + } + public var VoiceChat_StatusMutedYou: String { return self._s[1140]! } + public var VoiceOver_Chat_ReplyToYourMessage: String { return self._s[1141]! } + public var Channel_DiscussionGroup_MakeHistoryPublic: String { return self._s[1142]! } + public var StickerPacksSettings_Title: String { return self._s[1143]! } + public func Channel_AdminLog_MessageGroupPreHistoryVisible(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[1144]!, self._r[1144]!, [_0]) + } + public var Watch_NoConnection: String { return self._s[1145]! } + public var EncryptionKey_Title: String { return self._s[1146]! } + public var Widget_AuthRequired: String { return self._s[1147]! } + public func PUSH_MESSAGE_ROUND(_ _1: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[1148]!, self._r[1148]!, [_1]) + } + public var Notifications_ExceptionsTitle: String { return self._s[1149]! } + public var EditTheme_Expand_TopInfo: String { return self._s[1150]! } + public func Contacts_AddPhoneNumber(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[1151]!, self._r[1151]!, [_0]) + } + public var Channel_AdminLogFilter_EventsRestrictions: String { return self._s[1153]! } + public var Notifications_GroupNotificationsSound: String { return self._s[1154]! } + public var VoiceChat_SpeakPermissionAdmin: String { return self._s[1155]! } + public var Passport_Email_EnterOtherEmail: String { return self._s[1156]! } + public func VoiceChat_RemovePeerConfirmation(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[1159]!, self._r[1159]!, [_0]) + } + public var Conversation_AddToContacts: String { return self._s[1160]! } + public var AutoDownloadSettings_DataUsageMedium: String { return self._s[1161]! } + public var AuthSessions_LogOutApplications: String { return self._s[1163]! } + public var VoiceChat_LeaveVoiceChat: String { return self._s[1164]! } + public var ChatList_Context_Unpin: String { return self._s[1165]! } + public var PeopleNearby_DiscoverDescription: String { return self._s[1166]! } + public var UserInfo_FakeBotWarning: String { return self._s[1167]! } + public var Notification_MessageLifetime1d: String { return self._s[1168]! } + public var PrivacyLastSeenSettings_NeverShareWith_Title: String { return self._s[1169]! } + public var ChatListFolder_CategoryChannels: String { return self._s[1170]! } + public var VoiceOver_Chat_SeenByRecipient: String { return self._s[1171]! } + public var Notifications_PermissionsAllow: String { return self._s[1172]! } + public var Undo_ScheduledMessagesCleared: String { return self._s[1173]! } + public var AutoDownloadSettings_PrivateChats: String { return self._s[1175]! } + public var VoiceChat_ImproveYourProfileText: String { return self._s[1176]! } + public var ApplyLanguage_ChangeLanguageAction: String { return self._s[1177]! } + public var ChatImportActivity_ErrorInvalidChatType: String { return self._s[1178]! } + public func Conversation_ScheduledVoiceChatStartsToday(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[1179]!, self._r[1179]!, [_0]) + } + public func PrivacySettings_LastSeenNobodyPlus(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[1180]!, self._r[1180]!, [_0]) + } + public var Conversation_AutoremoveTimerRemovedChannel: String { return self._s[1182]! } + public var Notifications_MessageNotificationsHelp: String { return self._s[1184]! } + public var WallpaperSearch_ColorPink: String { return self._s[1185]! } + public var ContactInfo_PhoneNumberHidden: String { return self._s[1186]! } + public var Passport_Identity_IssueDate: String { return self._s[1188]! } public func PUSH_CHAT_MESSAGE_GIF(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1139]!, self._r[1139]!, [_1, _2]) + return formatWithArgumentRanges(self._s[1189]!, self._r[1189]!, [_1, _2]) } - public var ChatList_DeleteForAllSubscribersConfirmationText: String { return self._s[1140]! } - public var Channel_Info_Description: String { return self._s[1141]! } - public var PrivacySettings_DeleteAccountIfAwayFor: String { return self._s[1142]! } - public var Weekday_ShortTuesday: String { return self._s[1143]! } - public var Common_Back: String { return self._s[1144]! } - public var Chat_PinnedMessagesHiddenTitle: String { return self._s[1146]! } - public var ChatListFolder_AddChats: String { return self._s[1147]! } - public var Common_Close: String { return self._s[1149]! } - public var Map_OpenIn: String { return self._s[1150]! } - public var Group_Setup_HistoryTitle: String { return self._s[1151]! } - public var SettingsSearch_Synonyms_Data_AutoDownloadUsingWifi: String { return self._s[1152]! } - public var Notification_MessageLifetime1h: String { return self._s[1153]! } + public var ChatList_DeleteForAllSubscribersConfirmationText: String { return self._s[1190]! } + public var Channel_Info_Description: String { return self._s[1191]! } + public var PrivacySettings_DeleteAccountIfAwayFor: String { return self._s[1192]! } + public var Weekday_ShortTuesday: String { return self._s[1193]! } + public var Common_Back: String { return self._s[1194]! } + public var Chat_PinnedMessagesHiddenTitle: String { return self._s[1196]! } + public var ChatListFolder_AddChats: String { return self._s[1197]! } + public var Common_Close: String { return self._s[1199]! } + public var Map_OpenIn: String { return self._s[1200]! } + public var Group_Setup_HistoryTitle: String { return self._s[1201]! } + public var SettingsSearch_Synonyms_Data_AutoDownloadUsingWifi: String { return self._s[1202]! } + public var Notification_MessageLifetime1h: String { return self._s[1203]! } public func CancelResetAccount_Success(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1154]!, self._r[1154]!, [_0]) + return formatWithArgumentRanges(self._s[1204]!, self._r[1204]!, [_0]) } - public var Watch_Contacts_NoResults: String { return self._s[1156]! } - public var TwoStepAuth_SetupResendEmailCode: String { return self._s[1157]! } - public var Checkout_Phone: String { return self._s[1158]! } - public var OwnershipTransfer_ComeBackLater: String { return self._s[1159]! } + public var Watch_Contacts_NoResults: String { return self._s[1206]! } + public var TwoStepAuth_SetupResendEmailCode: String { return self._s[1207]! } + public var Checkout_Phone: String { return self._s[1208]! } + public var OwnershipTransfer_ComeBackLater: String { return self._s[1209]! } public func Channel_CommentsGroup_HeaderGroupSet(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1160]!, self._r[1160]!, [_0]) + return formatWithArgumentRanges(self._s[1210]!, self._r[1210]!, [_0]) } public func DialogList_MultipleTypingSuffix(_ _0: Int) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1161]!, self._r[1161]!, ["\(_0)"]) + return formatWithArgumentRanges(self._s[1211]!, self._r[1211]!, ["\(_0)"]) } - public var Conversation_AudioRateTooltipSpeedUp: String { return self._s[1162]! } - public var ChatAdmins_Title: String { return self._s[1163]! } - public var Appearance_ThemePreview_Chat_7_Text: String { return self._s[1164]! } + public var Conversation_AudioRateTooltipSpeedUp: String { return self._s[1212]! } + public var ChatAdmins_Title: String { return self._s[1213]! } + public var Appearance_ThemePreview_Chat_7_Text: String { return self._s[1214]! } public func PUSH_CHANNEL_MESSAGE_POLL(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1165]!, self._r[1165]!, [_1]) + return formatWithArgumentRanges(self._s[1215]!, self._r[1215]!, [_1]) } - public var Common_Done: String { return self._s[1166]! } - public var ChatList_HeaderImportIntoAnExistingGroup: String { return self._s[1167]! } - public var Appearance_ThemeCarouselNight: String { return self._s[1170]! } + public var Common_Done: String { return self._s[1216]! } + public var ChatList_HeaderImportIntoAnExistingGroup: String { return self._s[1217]! } + public var Appearance_AppIconNew2: String { return self._s[1218]! } public func PUSH_PINNED_VIDEO(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1172]!, self._r[1172]!, [_1]) + return formatWithArgumentRanges(self._s[1222]!, self._r[1222]!, [_1]) } - public var InviteLink_Expired: String { return self._s[1174]! } - public var Preview_OpenInInstagram: String { return self._s[1175]! } - public var Wallpaper_SetColor: String { return self._s[1179]! } - public var VoiceOver_Media_PlaybackRate: String { return self._s[1180]! } - public var ChatSettings_Groups: String { return self._s[1181]! } + public var Appearance_ThemeCarouselNight: String { return self._s[1223]! } + public var InviteLink_Expired: String { return self._s[1225]! } + public var Preview_OpenInInstagram: String { return self._s[1226]! } + public var Wallpaper_SetColor: String { return self._s[1231]! } + public var VoiceOver_Media_PlaybackRate: String { return self._s[1232]! } + public var ChatSettings_Groups: String { return self._s[1233]! } public func VoiceOver_Chat_VoiceMessageFrom(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1182]!, self._r[1182]!, [_0]) + return formatWithArgumentRanges(self._s[1234]!, self._r[1234]!, [_0]) } - public var Contacts_SortedByName: String { return self._s[1183]! } - public var SettingsSearch_Synonyms_Notifications_ContactJoined: String { return self._s[1184]! } - public var Channel_Management_LabelCreator: String { return self._s[1185]! } - public var Contacts_PermissionsSuppressWarningTitle: String { return self._s[1186]! } + public var Contacts_SortedByName: String { return self._s[1235]! } + public var SettingsSearch_Synonyms_Notifications_ContactJoined: String { return self._s[1236]! } + public var Channel_Management_LabelCreator: String { return self._s[1237]! } + public var Contacts_PermissionsSuppressWarningTitle: String { return self._s[1238]! } public func PrivacySettings_LastSeenContactsMinusPlus(_ _0: String, _ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1187]!, self._r[1187]!, [_0, _1]) + return formatWithArgumentRanges(self._s[1239]!, self._r[1239]!, [_0, _1]) } - public var Group_GroupMembersHeader: String { return self._s[1188]! } - public var Group_PublicLink_Title: String { return self._s[1189]! } - public var Channel_OwnershipTransfer_ErrorAdminsTooMuch: String { return self._s[1190]! } - public var VoiceOver_Chat_Photo: String { return self._s[1191]! } - public var TwoFactorSetup_EmailVerification_Placeholder: String { return self._s[1192]! } - public var IntentsSettings_SuggestBy: String { return self._s[1193]! } - public var Privacy_Calls_AlwaysAllow_Placeholder: String { return self._s[1194]! } - public var Appearance_ThemePreview_ChatList_1_Name: String { return self._s[1195]! } - public var PhoneNumberHelp_ChangeNumber: String { return self._s[1196]! } - public var LogoutOptions_SetPasscodeText: String { return self._s[1197]! } - public var Map_OpenInMaps: String { return self._s[1198]! } - public var ContactInfo_PhoneLabelWorkFax: String { return self._s[1199]! } - public var BlockedUsers_Unblock: String { return self._s[1200]! } + public var Group_GroupMembersHeader: String { return self._s[1240]! } + public var Group_PublicLink_Title: String { return self._s[1241]! } + public var Channel_OwnershipTransfer_ErrorAdminsTooMuch: String { return self._s[1242]! } + public var VoiceOver_Chat_Photo: String { return self._s[1243]! } + public var TwoFactorSetup_EmailVerification_Placeholder: String { return self._s[1244]! } + public var IntentsSettings_SuggestBy: String { return self._s[1245]! } + public var Privacy_Calls_AlwaysAllow_Placeholder: String { return self._s[1246]! } + public var Appearance_ThemePreview_ChatList_1_Name: String { return self._s[1247]! } + public var PhoneNumberHelp_ChangeNumber: String { return self._s[1248]! } + public var LogoutOptions_SetPasscodeText: String { return self._s[1249]! } + public var Map_OpenInMaps: String { return self._s[1250]! } + public var ContactInfo_PhoneLabelWorkFax: String { return self._s[1251]! } + public var BlockedUsers_Unblock: String { return self._s[1252]! } public func Settings_ApplyProxyAlert(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1201]!, self._r[1201]!, [_1, _2]) + return formatWithArgumentRanges(self._s[1253]!, self._r[1253]!, [_1, _2]) } public func Channel_AdminLog_MessageRestrictedNameUsername(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1202]!, self._r[1202]!, [_1, _2]) + return formatWithArgumentRanges(self._s[1254]!, self._r[1254]!, [_1, _2]) } - public var ChatImport_CreateGroupAlertTitle: String { return self._s[1204]! } - public var Conversation_Block: String { return self._s[1205]! } - public var VoiceChat_PersonalAccount: String { return self._s[1206]! } - public var Passport_Scans_UploadNew: String { return self._s[1207]! } - public var Share_Title: String { return self._s[1208]! } - public var Conversation_ApplyLocalization: String { return self._s[1209]! } - public var SharedMedia_EmptyLinksText: String { return self._s[1210]! } - public var Settings_NotificationsAndSounds: String { return self._s[1211]! } - public var Stats_ViewsByHoursTitle: String { return self._s[1212]! } - public var PhotoEditor_QualityMedium: String { return self._s[1213]! } - public var Conversation_ContextMenuCancelSending: String { return self._s[1214]! } + public var ChatImport_CreateGroupAlertTitle: String { return self._s[1256]! } + public var Conversation_Block: String { return self._s[1257]! } + public var VoiceChat_PersonalAccount: String { return self._s[1258]! } + public var Passport_Scans_UploadNew: String { return self._s[1259]! } + public var Share_Title: String { return self._s[1260]! } + public var Conversation_ApplyLocalization: String { return self._s[1261]! } + public var SharedMedia_EmptyLinksText: String { return self._s[1262]! } + public var Settings_NotificationsAndSounds: String { return self._s[1263]! } + public var Stats_ViewsByHoursTitle: String { return self._s[1264]! } + public var PhotoEditor_QualityMedium: String { return self._s[1265]! } + public var Conversation_ContextMenuCancelSending: String { return self._s[1266]! } public func PUSH_CHANNEL_MESSAGE_GAME(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1215]!, self._r[1215]!, [_1, _2]) + return formatWithArgumentRanges(self._s[1267]!, self._r[1267]!, [_1, _2]) } - public var Conversation_RestrictedInline: String { return self._s[1216]! } - public var Passport_Language_tr: String { return self._s[1217]! } - public var Call_Mute: String { return self._s[1218]! } + public var Conversation_RestrictedInline: String { return self._s[1268]! } + public var Passport_Language_tr: String { return self._s[1269]! } + public var Call_Mute: String { return self._s[1270]! } public func Conversation_NoticeInvitedByInGroup(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1219]!, self._r[1219]!, [_0]) + return formatWithArgumentRanges(self._s[1271]!, self._r[1271]!, [_0]) } - public var Passport_Language_bn: String { return self._s[1220]! } - public var Common_Save: String { return self._s[1222]! } - public var AccessDenied_LocationTracking: String { return self._s[1224]! } - public var Month_ShortOctober: String { return self._s[1225]! } - public var AutoDownloadSettings_WiFi: String { return self._s[1226]! } - public var ProfilePhoto_SetMainPhoto: String { return self._s[1228]! } - public var ChangePhoneNumberNumber_NewNumber: String { return self._s[1229]! } + public var Passport_Language_bn: String { return self._s[1272]! } + public var Common_Save: String { return self._s[1274]! } + public var AccessDenied_LocationTracking: String { return self._s[1276]! } + public var Month_ShortOctober: String { return self._s[1277]! } + public var AutoDownloadSettings_WiFi: String { return self._s[1278]! } + public var ProfilePhoto_SetMainPhoto: String { return self._s[1280]! } + public var ChangePhoneNumberNumber_NewNumber: String { return self._s[1281]! } public func Time_MonthOfYear_m3(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1230]!, self._r[1230]!, [_0]) + return formatWithArgumentRanges(self._s[1282]!, self._r[1282]!, [_0]) } - public var Watch_ChannelInfo_Title: String { return self._s[1231]! } - public var State_Updating: String { return self._s[1232]! } - public var Conversation_UnblockUser: String { return self._s[1233]! } - public var Notifications_ChannelNotificationsSound: String { return self._s[1234]! } - public var Map_GetDirections: String { return self._s[1235]! } - public var Watch_Compose_AddContact: String { return self._s[1237]! } - public var Conversation_Dice_u26BD: String { return self._s[1238]! } - public var AccessDenied_PhotosRestricted: String { return self._s[1239]! } + public var Watch_ChannelInfo_Title: String { return self._s[1283]! } + public var State_Updating: String { return self._s[1284]! } + public var Conversation_UnblockUser: String { return self._s[1285]! } + public var Notifications_ChannelNotificationsSound: String { return self._s[1286]! } + public var Map_GetDirections: String { return self._s[1287]! } + public var Watch_Compose_AddContact: String { return self._s[1289]! } + public var Conversation_Dice_u26BD: String { return self._s[1290]! } + public var AccessDenied_PhotosRestricted: String { return self._s[1291]! } public func Channel_AdminLog_MessageRank(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1240]!, self._r[1240]!, [_1]) + return formatWithArgumentRanges(self._s[1292]!, self._r[1292]!, [_1]) } - public var Map_LoadError: String { return self._s[1242]! } - public var SettingsSearch_Synonyms_Privacy_Calls: String { return self._s[1243]! } - public var PhotoEditor_CropAuto: String { return self._s[1244]! } + public var Map_LoadError: String { return self._s[1294]! } + public var SettingsSearch_Synonyms_Privacy_Calls: String { return self._s[1295]! } + public var PhotoEditor_CropAuto: String { return self._s[1296]! } public func Target_ShareGameConfirmationPrivate(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1247]!, self._r[1247]!, [_0]) + return formatWithArgumentRanges(self._s[1299]!, self._r[1299]!, [_0]) } - public var Username_TooManyPublicUsernamesError: String { return self._s[1249]! } + public var Username_TooManyPublicUsernamesError: String { return self._s[1301]! } public func PUSH_PINNED_GAME(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1250]!, self._r[1250]!, [_1]) + return formatWithArgumentRanges(self._s[1302]!, self._r[1302]!, [_1]) } - public var Settings_PhoneNumber: String { return self._s[1251]! } + public var Settings_PhoneNumber: String { return self._s[1303]! } public func Channel_AdminLog_MessageTransferedName(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1252]!, self._r[1252]!, [_1]) + return formatWithArgumentRanges(self._s[1304]!, self._r[1304]!, [_1]) } - public var Month_GenJune: String { return self._s[1254]! } - public var Notifications_ExceptionsGroupPlaceholder: String { return self._s[1255]! } - public var ChatListFolder_CategoryRead: String { return self._s[1256]! } - public var LoginPassword_ResetAccount: String { return self._s[1257]! } + public var Month_GenJune: String { return self._s[1306]! } + public var Notifications_ExceptionsGroupPlaceholder: String { return self._s[1307]! } + public var ChatListFolder_CategoryRead: String { return self._s[1308]! } + public var LoginPassword_ResetAccount: String { return self._s[1309]! } public func DialogList_SingleUploadingFileSuffix(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1258]!, self._r[1258]!, [_0]) + return formatWithArgumentRanges(self._s[1310]!, self._r[1310]!, [_0]) } - public var Call_CameraConfirmationConfirm: String { return self._s[1259]! } - public var Notification_RenamedChannel: String { return self._s[1260]! } + public var Call_CameraConfirmationConfirm: String { return self._s[1311]! } + public var Notification_RenamedChannel: String { return self._s[1312]! } public func Channel_AdminLog_MessageUnpinned(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1261]!, self._r[1261]!, [_0]) + return formatWithArgumentRanges(self._s[1313]!, self._r[1313]!, [_0]) } - public var Channel_AdminLogFilter_EventsAdmins: String { return self._s[1262]! } - public var IntentsSettings_Title: String { return self._s[1264]! } - public var CallList_DeleteAllForMe: String { return self._s[1265]! } - public var Settings_AppleWatch: String { return self._s[1266]! } - public var Conversation_LinkCopied: String { return self._s[1267]! } - public var DialogList_NoMessagesText: String { return self._s[1268]! } - public var GroupPermission_NoChangeInfo: String { return self._s[1269]! } - public var Channel_ErrorAccessDenied: String { return self._s[1271]! } - public var ScheduledMessages_EmptyPlaceholder: String { return self._s[1272]! } + public var Channel_AdminLogFilter_EventsAdmins: String { return self._s[1314]! } + public var IntentsSettings_Title: String { return self._s[1316]! } + public var CallList_DeleteAllForMe: String { return self._s[1317]! } + public var Settings_AppleWatch: String { return self._s[1318]! } + public var Conversation_LinkCopied: String { return self._s[1319]! } + public var DialogList_NoMessagesText: String { return self._s[1320]! } + public func VoiceChat_SendPublicLinkText(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[1321]!, self._r[1321]!, [_1, _2]) + } + public var GroupPermission_NoChangeInfo: String { return self._s[1322]! } + public var Channel_ErrorAccessDenied: String { return self._s[1324]! } + public var ScheduledMessages_EmptyPlaceholder: String { return self._s[1325]! } public func Message_StickerText(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1273]!, self._r[1273]!, [_0]) + return formatWithArgumentRanges(self._s[1326]!, self._r[1326]!, [_0]) } - public var AuthSessions_TerminateOtherSessionsHelp: String { return self._s[1274]! } - public var StickerPacksSettings_AnimatedStickers: String { return self._s[1275]! } - public var Month_ShortJanuary: String { return self._s[1276]! } - public var Conversation_UnreadMessages: String { return self._s[1277]! } - public var Conversation_PrivateChannelTooltip: String { return self._s[1279]! } - public var Call_VoiceOver_VideoCallCanceled: String { return self._s[1280]! } - public var PrivacySettings_DeleteAccountTitle: String { return self._s[1282]! } - public var Channel_Members_AddBannedErrorAdmin: String { return self._s[1283]! } + public var AuthSessions_TerminateOtherSessionsHelp: String { return self._s[1327]! } + public var StickerPacksSettings_AnimatedStickers: String { return self._s[1328]! } + public var Month_ShortJanuary: String { return self._s[1329]! } + public var Conversation_UnreadMessages: String { return self._s[1330]! } + public var Conversation_PrivateChannelTooltip: String { return self._s[1332]! } + public var Call_VoiceOver_VideoCallCanceled: String { return self._s[1333]! } + public var PrivacySettings_DeleteAccountTitle: String { return self._s[1335]! } + public var Channel_Members_AddBannedErrorAdmin: String { return self._s[1336]! } public func Conversation_ShareMyPhoneNumberConfirmation(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1287]!, self._r[1287]!, [_1, _2]) - } - public var Widget_ApplicationLocked: String { return self._s[1288]! } - public func TextFormat_AddLinkText(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1289]!, self._r[1289]!, [_0]) - } - public var Common_TakePhotoOrVideo: String { return self._s[1290]! } - public var Passport_Language_ru: String { return self._s[1291]! } - public var MediaPicker_VideoMuteDescription: String { return self._s[1292]! } - public var EditTheme_ErrorLinkTaken: String { return self._s[1293]! } - public func Group_EditAdmin_RankInfo(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1295]!, self._r[1295]!, [_0]) - } - public var Channel_Members_AddAdminErrorBlacklisted: String { return self._s[1296]! } - public var Conversation_Owner: String { return self._s[1298]! } - public var Settings_FAQ_Intro: String { return self._s[1299]! } - public var PhotoEditor_QualityLow: String { return self._s[1301]! } - public var Widget_GalleryTitle: String { return self._s[1302]! } - public var Call_End: String { return self._s[1303]! } - public var StickerPacksSettings_FeaturedPacks: String { return self._s[1305]! } - public var Privacy_ContactsSyncHelp: String { return self._s[1306]! } - public var OldChannels_NoticeUpgradeText: String { return self._s[1310]! } - public var Conversation_Call: String { return self._s[1312]! } - public var Watch_MessageView_Title: String { return self._s[1313]! } - public func Notification_RenamedChat(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1314]!, self._r[1314]!, [_0]) - } - public var Passport_PasswordCompleteSetup: String { return self._s[1315]! } - public func Notification_ChangedGroupVideo(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1316]!, self._r[1316]!, [_0]) - } - public func TwoFactorSetup_EmailVerification_Text(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1318]!, self._r[1318]!, [_0]) - } - public var Map_Location: String { return self._s[1319]! } - public var Watch_MessageView_ViewOnPhone: String { return self._s[1320]! } - public var Login_CountryCode: String { return self._s[1321]! } - public var Channel_DiscussionGroup_PrivateGroup: String { return self._s[1323]! } - public var ChatState_ConnectingToProxy: String { return self._s[1324]! } - public var Login_CallRequestState3: String { return self._s[1325]! } - public var NetworkUsageSettings_MediaAudioDataSection: String { return self._s[1328]! } - public var SocksProxySetup_ProxyStatusConnecting: String { return self._s[1329]! } - public var Widget_ChatsGalleryDescription: String { return self._s[1331]! } - public var PrivacyLastSeenSettings_NeverShareWith_Placeholder: String { return self._s[1333]! } - public var InstantPage_FontSanFrancisco: String { return self._s[1334]! } - public var Call_StatusEnded: String { return self._s[1335]! } - public var MusicPlayer_VoiceNote: String { return self._s[1338]! } - public var ChatImportActivity_ErrorUserBlocked: String { return self._s[1339]! } - public func PUSH_CHANNEL_MESSAGE_TEXT(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(self._s[1340]!, self._r[1340]!, [_1, _2]) } - public var VoiceOver_MessageContextShare: String { return self._s[1341]! } - public var ProfilePhoto_SearchWeb: String { return self._s[1342]! } - public var EditProfile_Title: String { return self._s[1343]! } + public var Widget_ApplicationLocked: String { return self._s[1341]! } + public func TextFormat_AddLinkText(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[1342]!, self._r[1342]!, [_0]) + } + public var Common_TakePhotoOrVideo: String { return self._s[1343]! } + public var Passport_Language_ru: String { return self._s[1345]! } + public var MediaPicker_VideoMuteDescription: String { return self._s[1346]! } + public var EditTheme_ErrorLinkTaken: String { return self._s[1347]! } + public func Group_EditAdmin_RankInfo(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[1349]!, self._r[1349]!, [_0]) + } + public var VoiceChat_ShareShort: String { return self._s[1350]! } + public var Channel_Members_AddAdminErrorBlacklisted: String { return self._s[1351]! } + public var Conversation_Owner: String { return self._s[1353]! } + public var Settings_FAQ_Intro: String { return self._s[1354]! } + public var PhotoEditor_QualityLow: String { return self._s[1356]! } + public var Widget_GalleryTitle: String { return self._s[1357]! } + public var Call_End: String { return self._s[1358]! } + public var StickerPacksSettings_FeaturedPacks: String { return self._s[1360]! } + public var Privacy_ContactsSyncHelp: String { return self._s[1361]! } + public var OldChannels_NoticeUpgradeText: String { return self._s[1365]! } + public var Conversation_Call: String { return self._s[1367]! } + public var Watch_MessageView_Title: String { return self._s[1368]! } + public func Notification_RenamedChat(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[1369]!, self._r[1369]!, [_0]) + } + public var Passport_PasswordCompleteSetup: String { return self._s[1370]! } + public func Notification_ChangedGroupVideo(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[1371]!, self._r[1371]!, [_0]) + } + public func TwoFactorSetup_EmailVerification_Text(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[1373]!, self._r[1373]!, [_0]) + } + public var Map_Location: String { return self._s[1374]! } + public var Watch_MessageView_ViewOnPhone: String { return self._s[1375]! } + public var Login_CountryCode: String { return self._s[1376]! } + public var Channel_DiscussionGroup_PrivateGroup: String { return self._s[1378]! } + public var ChatState_ConnectingToProxy: String { return self._s[1379]! } + public var Login_CallRequestState3: String { return self._s[1380]! } + public var NetworkUsageSettings_MediaAudioDataSection: String { return self._s[1383]! } + public var SocksProxySetup_ProxyStatusConnecting: String { return self._s[1384]! } + public var Widget_ChatsGalleryDescription: String { return self._s[1386]! } + public var PrivacyLastSeenSettings_NeverShareWith_Placeholder: String { return self._s[1388]! } + public var InstantPage_FontSanFrancisco: String { return self._s[1389]! } + public var Call_StatusEnded: String { return self._s[1390]! } + public func Checkout_SuccessfulTooltip(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[1393]!, self._r[1393]!, [_1, _2]) + } + public var MusicPlayer_VoiceNote: String { return self._s[1394]! } + public var ChatImportActivity_ErrorUserBlocked: String { return self._s[1395]! } + public func PUSH_CHANNEL_MESSAGE_TEXT(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[1396]!, self._r[1396]!, [_1, _2]) + } + public var VoiceOver_MessageContextShare: String { return self._s[1397]! } + public var ProfilePhoto_SearchWeb: String { return self._s[1398]! } + public var EditProfile_Title: String { return self._s[1399]! } public func Notification_PinnedQuizMessage(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1344]!, self._r[1344]!, [_0]) + return formatWithArgumentRanges(self._s[1400]!, self._r[1400]!, [_0]) } - public var VoiceChat_Unmute: String { return self._s[1345]! } - public var ChangePhoneNumberCode_CodePlaceholder: String { return self._s[1346]! } - public var NetworkUsageSettings_ResetStats: String { return self._s[1348]! } - public var NetworkUsageSettings_GeneralDataSection: String { return self._s[1349]! } - public var StickerPackActionInfo_AddedTitle: String { return self._s[1350]! } - public var Channel_BanUser_PermissionSendStickersAndGifs: String { return self._s[1351]! } + public var VoiceChat_Unmute: String { return self._s[1401]! } + public var ChangePhoneNumberCode_CodePlaceholder: String { return self._s[1402]! } + public var NetworkUsageSettings_ResetStats: String { return self._s[1404]! } + public var NetworkUsageSettings_GeneralDataSection: String { return self._s[1405]! } + public var StickerPackActionInfo_AddedTitle: String { return self._s[1406]! } + public var Channel_BanUser_PermissionSendStickersAndGifs: String { return self._s[1407]! } public func Call_ParticipantVideoVersionOutdatedError(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1352]!, self._r[1352]!, [_0]) + return formatWithArgumentRanges(self._s[1408]!, self._r[1408]!, [_0]) } - public var Location_ProximityNotification_Title: String { return self._s[1353]! } - public var AuthSessions_AddDeviceIntro_Text1: String { return self._s[1354]! } - public var Passport_Identity_LatinNameHelp: String { return self._s[1357]! } - public var AuthSessions_AddDeviceIntro_Text2: String { return self._s[1358]! } - public var Stats_GroupMembersTitle: String { return self._s[1359]! } - public var AuthSessions_AddDeviceIntro_Text3: String { return self._s[1360]! } - public var InviteLink_InviteLinkRevoked: String { return self._s[1361]! } - public var Contacts_PermissionsSuppressWarningText: String { return self._s[1362]! } - public var OpenFile_PotentiallyDangerousContentAlert: String { return self._s[1363]! } - public var Settings_SetUsername: String { return self._s[1364]! } - public var GroupInfo_ActionRestrict: String { return self._s[1365]! } - public var SettingsSearch_Synonyms_SavedMessages: String { return self._s[1366]! } + public var Location_ProximityNotification_Title: String { return self._s[1409]! } + public var AuthSessions_AddDeviceIntro_Text1: String { return self._s[1410]! } + public var Passport_Identity_LatinNameHelp: String { return self._s[1413]! } + public var AuthSessions_AddDeviceIntro_Text2: String { return self._s[1414]! } + public var Stats_GroupMembersTitle: String { return self._s[1415]! } + public var AuthSessions_AddDeviceIntro_Text3: String { return self._s[1416]! } + public var InviteLink_InviteLinkRevoked: String { return self._s[1417]! } + public var Contacts_PermissionsSuppressWarningText: String { return self._s[1418]! } + public var OpenFile_PotentiallyDangerousContentAlert: String { return self._s[1419]! } + public var Settings_SetUsername: String { return self._s[1420]! } + public var GroupInfo_ActionRestrict: String { return self._s[1421]! } + public var SettingsSearch_Synonyms_SavedMessages: String { return self._s[1422]! } public func Time_PreciseDate_m2(_ _1: String, _ _2: String, _ _3: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1367]!, self._r[1367]!, [_1, _2, _3]) + return formatWithArgumentRanges(self._s[1423]!, self._r[1423]!, [_1, _2, _3]) } - public var Notifications_DisplayNamesOnLockScreenInfoWithLink: String { return self._s[1369]! } - public var Notification_Exceptions_AlwaysOff: String { return self._s[1370]! } - public var Conversation_ContextMenuDelete: String { return self._s[1371]! } - public var Privacy_Calls_WhoCanCallMe: String { return self._s[1372]! } - public var ChatList_PsaAlert_covid: String { return self._s[1375]! } - public var VoiceOver_SilentPostOn: String { return self._s[1376]! } - public var DialogList_Pin: String { return self._s[1377]! } - public var Channel_AdminLog_CanInviteUsersViaLink: String { return self._s[1378]! } - public var PrivacySettings_SecurityTitle: String { return self._s[1379]! } - public var GroupPermission_NotAvailableInPublicGroups: String { return self._s[1380]! } - public var PeopleNearby_Groups: String { return self._s[1381]! } - public var Message_File: String { return self._s[1382]! } - public var Calls_NoCallsPlaceholder: String { return self._s[1383]! } - public var ChatList_GenericPsaLabel: String { return self._s[1385]! } - public var UserInfo_LastNamePlaceholder: String { return self._s[1386]! } - public var IntentsSettings_Reset: String { return self._s[1388]! } - public var Call_ConnectionErrorTitle: String { return self._s[1389]! } - public var PhotoEditor_SaturationTool: String { return self._s[1390]! } - public var ChatSettings_AutomaticVideoMessageDownload: String { return self._s[1391]! } - public var SettingsSearch_Synonyms_Stickers_ArchivedPacks: String { return self._s[1392]! } - public var Conversation_SearchNoResults: String { return self._s[1393]! } - public var Channel_DiscussionGroup_PrivateChannel: String { return self._s[1394]! } - public var Map_OpenInWaze: String { return self._s[1395]! } - public var InviteLink_PeopleJoinedNone: String { return self._s[1396]! } - public var WallpaperPreview_Title: String { return self._s[1397]! } + public var Notifications_DisplayNamesOnLockScreenInfoWithLink: String { return self._s[1425]! } + public var Notification_Exceptions_AlwaysOff: String { return self._s[1426]! } + public var Conversation_ContextMenuDelete: String { return self._s[1427]! } + public var Privacy_Calls_WhoCanCallMe: String { return self._s[1428]! } + public var ChatList_PsaAlert_covid: String { return self._s[1431]! } + public var VoiceOver_SilentPostOn: String { return self._s[1432]! } + public var DialogList_Pin: String { return self._s[1433]! } + public var Channel_AdminLog_CanInviteUsersViaLink: String { return self._s[1434]! } + public var PrivacySettings_SecurityTitle: String { return self._s[1435]! } + public var GroupPermission_NotAvailableInPublicGroups: String { return self._s[1436]! } + public var PeopleNearby_Groups: String { return self._s[1437]! } + public var Message_File: String { return self._s[1438]! } + public var Calls_NoCallsPlaceholder: String { return self._s[1439]! } + public var ChatList_GenericPsaLabel: String { return self._s[1442]! } + public var UserInfo_LastNamePlaceholder: String { return self._s[1443]! } + public var IntentsSettings_Reset: String { return self._s[1445]! } + public var Call_ConnectionErrorTitle: String { return self._s[1446]! } + public var PhotoEditor_SaturationTool: String { return self._s[1447]! } + public var ChatSettings_AutomaticVideoMessageDownload: String { return self._s[1448]! } + public var SettingsSearch_Synonyms_Stickers_ArchivedPacks: String { return self._s[1449]! } + public var Conversation_SearchNoResults: String { return self._s[1450]! } + public var Channel_DiscussionGroup_PrivateChannel: String { return self._s[1451]! } + public var Map_OpenInWaze: String { return self._s[1452]! } + public var InviteLink_PeopleJoinedNone: String { return self._s[1453]! } + public var WallpaperPreview_Title: String { return self._s[1454]! } public func Passport_AcceptHelp(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1399]!, self._r[1399]!, [_1, _2]) - } - public var AuthSessions_AddDeviceIntro_Title: String { return self._s[1400]! } - public var VoiceOver_Chat_RecordModeVideoMessageInfo: String { return self._s[1401]! } - public var VoiceOver_Chat_ChannelInfo: String { return self._s[1402]! } - public var Conversation_ImageCopied: String { return self._s[1403]! } - public var Passport_Identity_OneOfTypeInternalPassport: String { return self._s[1404]! } - public var Notifications_PermissionsUnreachableTitle: String { return self._s[1406]! } - public var Stats_Total: String { return self._s[1409]! } - public var Stats_GroupMessages: String { return self._s[1410]! } - public var TwoFactorSetup_Email_SkipAction: String { return self._s[1411]! } - public var CheckoutInfo_ErrorPhoneInvalid: String { return self._s[1412]! } - public var VoiceChat_DisplayAsInfoGroup: String { return self._s[1413]! } - public var Passport_Identity_Translation: String { return self._s[1414]! } - public var Notifications_TextTone: String { return self._s[1417]! } - public var Settings_RemoveConfirmation: String { return self._s[1419]! } - public var ScheduledMessages_Delete: String { return self._s[1420]! } - public var Channel_AdminLog_BanEmbedLinks: String { return self._s[1421]! } - public var Passport_PasswordNext: String { return self._s[1422]! } - public func PUSH_ENCRYPTED_MESSAGE(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1423]!, self._r[1423]!, [_1]) - } - public var Passport_Address_EditBankStatement: String { return self._s[1424]! } - public var PhotoEditor_ShadowsTool: String { return self._s[1425]! } - public var Notification_VideoCallMissed: String { return self._s[1426]! } - public var AccessDenied_CameraDisabled: String { return self._s[1427]! } - public var AuthSessions_AddDevice_ScanInfo: String { return self._s[1428]! } - public var Notifications_ExceptionsMuted: String { return self._s[1429]! } - public var Conversation_ScheduleMessage_SendWhenOnline: String { return self._s[1430]! } - public var Channel_BlackList_Title: String { return self._s[1431]! } - public var PasscodeSettings_4DigitCode: String { return self._s[1432]! } - public var NotificationsSound_Bamboo: String { return self._s[1433]! } - public var PrivacySettings_LastSeenContacts: String { return self._s[1434]! } - public var Passport_Address_TypeUtilityBill: String { return self._s[1435]! } - public var Passport_Address_CountryPlaceholder: String { return self._s[1436]! } - public var GroupPermission_SectionTitle: String { return self._s[1437]! } - public var InviteLink_ContextRevoke: String { return self._s[1438]! } - public func Notification_InvitedMultiple(_ _0: String, _ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1439]!, self._r[1439]!, [_0, _1]) - } - public var CheckoutInfo_ShippingInfoStatePlaceholder: String { return self._s[1440]! } - public var Channel_LeaveChannel: String { return self._s[1441]! } - public var Watch_Notification_Joined: String { return self._s[1442]! } - public var PeerInfo_ButtonMore: String { return self._s[1443]! } - public var Passport_FieldEmailHelp: String { return self._s[1444]! } - public var ChatList_Context_Pin: String { return self._s[1445]! } - public func Time_MonthOfYear_m9(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1446]!, self._r[1446]!, [_0]) - } - public var Group_Location_CreateInThisPlace: String { return self._s[1447]! } - public var PhotoEditor_QualityVeryHigh: String { return self._s[1448]! } - public var Tour_Title5: String { return self._s[1449]! } - public func PUSH_CHAT_MESSAGE_FWD(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1450]!, self._r[1450]!, [_1, _2]) - } - public var Passport_Language_en: String { return self._s[1451]! } - public var Checkout_Name: String { return self._s[1452]! } - public var ChatImport_Title: String { return self._s[1453]! } - public func NetworkUsageSettings_WifiUsageSince(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1454]!, self._r[1454]!, [_0]) - } - public var PhotoEditor_EnhanceTool: String { return self._s[1455]! } - public func PUSH_CHAT_DELETE_YOU(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(self._s[1456]!, self._r[1456]!, [_1, _2]) } - public var PeerInfo_CustomizeNotifications: String { return self._s[1457]! } - public func Login_TermsOfService_ProceedBot(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1458]!, self._r[1458]!, [_0]) + public var AuthSessions_AddDeviceIntro_Title: String { return self._s[1457]! } + public var VoiceOver_Chat_RecordModeVideoMessageInfo: String { return self._s[1458]! } + public var TwoFactorSetup_ResetDone_TextNoPassword: String { return self._s[1459]! } + public var VoiceOver_Chat_ChannelInfo: String { return self._s[1460]! } + public var Conversation_ImageCopied: String { return self._s[1461]! } + public var Passport_Identity_OneOfTypeInternalPassport: String { return self._s[1462]! } + public var Notifications_PermissionsUnreachableTitle: String { return self._s[1464]! } + public var Stats_Total: String { return self._s[1467]! } + public var Stats_GroupMessages: String { return self._s[1468]! } + public var TwoFactorSetup_Email_SkipAction: String { return self._s[1469]! } + public var CheckoutInfo_ErrorPhoneInvalid: String { return self._s[1470]! } + public var VoiceChat_You: String { return self._s[1471]! } + public var VoiceChat_DisplayAsInfoGroup: String { return self._s[1472]! } + public var Passport_Identity_Translation: String { return self._s[1473]! } + public var Notifications_TextTone: String { return self._s[1476]! } + public var Settings_RemoveConfirmation: String { return self._s[1478]! } + public var ScheduledMessages_Delete: String { return self._s[1479]! } + public var Channel_AdminLog_BanEmbedLinks: String { return self._s[1480]! } + public var Passport_PasswordNext: String { return self._s[1481]! } + public func PUSH_ENCRYPTED_MESSAGE(_ _1: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[1482]!, self._r[1482]!, [_1]) } - public var Group_ErrorSendRestrictedMedia: String { return self._s[1459]! } - public func UserInfo_NotificationsDefaultSound(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1460]!, self._r[1460]!, [_0]) + public var Passport_Address_EditBankStatement: String { return self._s[1483]! } + public var PhotoEditor_ShadowsTool: String { return self._s[1484]! } + public var Notification_VideoCallMissed: String { return self._s[1485]! } + public var AccessDenied_CameraDisabled: String { return self._s[1487]! } + public var AuthSessions_AddDevice_ScanInfo: String { return self._s[1488]! } + public var Notifications_ExceptionsMuted: String { return self._s[1489]! } + public var VoiceChat_TapToViewScreenVideo: String { return self._s[1490]! } + public var Conversation_ScheduleMessage_SendWhenOnline: String { return self._s[1491]! } + public var Channel_BlackList_Title: String { return self._s[1492]! } + public var PasscodeSettings_4DigitCode: String { return self._s[1493]! } + public var NotificationsSound_Bamboo: String { return self._s[1494]! } + public var Conversation_InputMenu: String { return self._s[1495]! } + public var PrivacySettings_LastSeenContacts: String { return self._s[1496]! } + public var Passport_Address_TypeUtilityBill: String { return self._s[1497]! } + public var Passport_Address_CountryPlaceholder: String { return self._s[1498]! } + public var GroupPermission_SectionTitle: String { return self._s[1499]! } + public var InviteLink_ContextRevoke: String { return self._s[1500]! } + public func Notification_InvitedMultiple(_ _0: String, _ _1: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[1501]!, self._r[1501]!, [_0, _1]) } - public var Login_UnknownError: String { return self._s[1461]! } - public var Conversation_ImportedMessageHint: String { return self._s[1463]! } - public func VoiceChat_ForwardTooltip_Chat(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1464]!, self._r[1464]!, [_0]) + public var CheckoutInfo_ShippingInfoStatePlaceholder: String { return self._s[1502]! } + public var Channel_LeaveChannel: String { return self._s[1503]! } + public var Watch_Notification_Joined: String { return self._s[1504]! } + public var PeerInfo_ButtonMore: String { return self._s[1505]! } + public var Passport_FieldEmailHelp: String { return self._s[1506]! } + public var ChatList_Context_Pin: String { return self._s[1507]! } + public func Time_MonthOfYear_m9(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[1508]!, self._r[1508]!, [_0]) } - public var Passport_Identity_TypeDriversLicense: String { return self._s[1466]! } - public var ChatList_AutoarchiveSuggestion_Title: String { return self._s[1467]! } - public var Watch_PhotoView_Title: String { return self._s[1468]! } - public var Appearance_ThemePreview_ChatList_3_Text: String { return self._s[1469]! } - public var Checkout_TotalAmount: String { return self._s[1470]! } - public var ChatList_RemoveFolderAction: String { return self._s[1471]! } - public func GroupInfo_Permissions_BroadcastConvertInfo(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1472]!, self._r[1472]!, [_0]) + public var Group_Location_CreateInThisPlace: String { return self._s[1509]! } + public var PhotoEditor_QualityVeryHigh: String { return self._s[1510]! } + public var Tour_Title5: String { return self._s[1511]! } + public func PUSH_CHAT_MESSAGE_FWD(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[1512]!, self._r[1512]!, [_1, _2]) } - public var GroupInfo_SetGroupPhoto: String { return self._s[1473]! } - public var Watch_AppName: String { return self._s[1474]! } - public func PUSH_PINNED_GAME_SCORE(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1475]!, self._r[1475]!, [_1]) - } - public var Channel_Username_CheckingUsername: String { return self._s[1476]! } - public var ContactList_Context_Call: String { return self._s[1477]! } - public var ChatList_ReorderTabs: String { return self._s[1478]! } - public var Watch_ChatList_Compose: String { return self._s[1479]! } - public func Conversation_LiveLocationYouAnd(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1480]!, self._r[1480]!, [_0]) - } - public var Channel_AdminLog_EmptyFilterTitle: String { return self._s[1481]! } - public var ArchivedChats_IntroTitle1: String { return self._s[1482]! } - public func PUSH_ENCRYPTION_ACCEPT(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1483]!, self._r[1483]!, [_1]) - } - public var Call_StatusRequesting: String { return self._s[1485]! } - public var Checkout_TotalPaidAmount: String { return self._s[1486]! } - public var Weekday_Friday: String { return self._s[1488]! } - public var CreateGroup_ChannelsTooMuch: String { return self._s[1489]! } - public func ChatImport_SelectionConfirmationUserWithoutTitle(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1490]!, self._r[1490]!, [_0]) - } - public var Watch_ChatList_NoConversationsText: String { return self._s[1491]! } - public var Group_Members_AddMembersHelp: String { return self._s[1492]! } - public func Channel_AdminLog_MessageChangedGroupStickerPack(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1493]!, self._r[1493]!, [_0]) - } - public var SecretVideo_Title: String { return self._s[1494]! } - public func Notification_PinnedStickerMessage(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1497]!, self._r[1497]!, [_0]) - } - public var Undo_Undo: String { return self._s[1498]! } - public var Watch_Microphone_Access: String { return self._s[1499]! } - public func ChatImport_SelectionConfirmationGroupWithTitle(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1500]!, self._r[1500]!, [_1, _2]) - } - public func PUSH_CHAT_MESSAGE_PHOTO(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1501]!, self._r[1501]!, [_1, _2]) - } - public func ChatList_Search_NoResultsQueryDescription(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1502]!, self._r[1502]!, [_0]) - } - public var Checkout_NewCard_PostcodeTitle: String { return self._s[1504]! } - public var TwoFactorSetup_Intro_Action: String { return self._s[1505]! } - public var Passport_Language_ne: String { return self._s[1506]! } - public var TwoStepAuth_EmailHelp: String { return self._s[1508]! } - public var Profile_MessageLifetime2s: String { return self._s[1509]! } - public func Conversation_MessageDialogRetryAll(_ _1: Int) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1510]!, self._r[1510]!, ["\(_1)"]) - } - public func Items_NOfM(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1511]!, self._r[1511]!, [_1, _2]) - } - public var Media_LimitedAccessText: String { return self._s[1512]! } - public func PUSH_CHAT_TITLE_EDITED(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1513]!, self._r[1513]!, [_1, _2]) - } - public var GroupPermission_NoPinMessages: String { return self._s[1514]! } - public func Notification_VoiceChatStarted(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1515]!, self._r[1515]!, [_1]) - } - public func Notification_CreatedChat(_ _0: String) -> (String, [(Int, NSRange)]) { + public var Passport_Language_en: String { return self._s[1513]! } + public var Checkout_Name: String { return self._s[1514]! } + public var ChatImport_Title: String { return self._s[1515]! } + public func NetworkUsageSettings_WifiUsageSince(_ _0: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(self._s[1516]!, self._r[1516]!, [_0]) } - public var FastTwoStepSetup_HintHelp: String { return self._s[1517]! } - public var VoiceOver_SilentPostOff: String { return self._s[1518]! } - public var WallpaperSearch_ColorRed: String { return self._s[1519]! } - public var Watch_ConnectionDescription: String { return self._s[1520]! } - public var Notification_Exceptions_AddException: String { return self._s[1521]! } - public var LocalGroup_IrrelevantWarning: String { return self._s[1522]! } - public var VoiceOver_MessageContextDelete: String { return self._s[1523]! } - public var LogoutOptions_AlternativeOptionsSection: String { return self._s[1524]! } - public var Passport_PasswordPlaceholder: String { return self._s[1525]! } - public var TwoStepAuth_RecoveryEmailAddDescription: String { return self._s[1526]! } - public var Stats_MessageInteractionsTitle: String { return self._s[1527]! } - public var Appearance_ThemeCarouselClassic: String { return self._s[1528]! } - public var TwoFactorSetup_Email_SkipConfirmationText: String { return self._s[1530]! } - public var Channel_AdminLog_PinMessages: String { return self._s[1531]! } - public var Passport_Address_AddRentalAgreement: String { return self._s[1533]! } - public var Watch_Message_Game: String { return self._s[1534]! } - public var PrivacyLastSeenSettings_NeverShareWith: String { return self._s[1535]! } - public var PrivacyPolicy_DeclineLastWarning: String { return self._s[1536]! } - public var EditTheme_FileReadError: String { return self._s[1537]! } - public var Group_ErrorAddBlocked: String { return self._s[1538]! } - public var CallSettings_UseLessDataLongDescription: String { return self._s[1539]! } + public var PhotoEditor_EnhanceTool: String { return self._s[1517]! } + public func PUSH_CHAT_DELETE_YOU(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[1518]!, self._r[1518]!, [_1, _2]) + } + public func VoiceChat_UserCanNowSpeak(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[1519]!, self._r[1519]!, [_0]) + } + public var PeerInfo_CustomizeNotifications: String { return self._s[1520]! } + public func Login_TermsOfService_ProceedBot(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[1521]!, self._r[1521]!, [_0]) + } + public var Group_ErrorSendRestrictedMedia: String { return self._s[1522]! } + public func UserInfo_NotificationsDefaultSound(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[1523]!, self._r[1523]!, [_0]) + } + public var Login_UnknownError: String { return self._s[1524]! } + public var Conversation_ImportedMessageHint: String { return self._s[1526]! } + public func VoiceChat_ForwardTooltip_Chat(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[1527]!, self._r[1527]!, [_0]) + } + public var Passport_Identity_TypeDriversLicense: String { return self._s[1529]! } + public var ChatList_AutoarchiveSuggestion_Title: String { return self._s[1530]! } + public var Watch_PhotoView_Title: String { return self._s[1531]! } + public var Appearance_ThemePreview_ChatList_3_Text: String { return self._s[1532]! } + public var Checkout_TotalAmount: String { return self._s[1533]! } + public var ChatList_RemoveFolderAction: String { return self._s[1534]! } + public func GroupInfo_Permissions_BroadcastConvertInfo(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[1535]!, self._r[1535]!, [_0]) + } + public var GroupInfo_SetGroupPhoto: String { return self._s[1536]! } + public var Watch_AppName: String { return self._s[1537]! } + public func PUSH_PINNED_GAME_SCORE(_ _1: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[1538]!, self._r[1538]!, [_1]) + } + public var Channel_Username_CheckingUsername: String { return self._s[1539]! } + public var ContactList_Context_Call: String { return self._s[1540]! } + public var ChatList_ReorderTabs: String { return self._s[1541]! } + public var Watch_ChatList_Compose: String { return self._s[1542]! } + public func Conversation_LiveLocationYouAnd(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[1543]!, self._r[1543]!, [_0]) + } + public var Channel_AdminLog_EmptyFilterTitle: String { return self._s[1544]! } + public var ArchivedChats_IntroTitle1: String { return self._s[1545]! } + public func PUSH_ENCRYPTION_ACCEPT(_ _1: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[1546]!, self._r[1546]!, [_1]) + } + public var Call_StatusRequesting: String { return self._s[1548]! } + public var Checkout_TotalPaidAmount: String { return self._s[1549]! } + public var Weekday_Friday: String { return self._s[1551]! } + public var CreateGroup_ChannelsTooMuch: String { return self._s[1552]! } + public func ChatImport_SelectionConfirmationUserWithoutTitle(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[1553]!, self._r[1553]!, [_0]) + } + public var Watch_ChatList_NoConversationsText: String { return self._s[1554]! } + public var Group_Members_AddMembersHelp: String { return self._s[1555]! } + public func Channel_AdminLog_MessageChangedGroupStickerPack(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[1556]!, self._r[1556]!, [_0]) + } + public var SecretVideo_Title: String { return self._s[1557]! } + public func Notification_PinnedStickerMessage(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[1560]!, self._r[1560]!, [_0]) + } + public var Undo_Undo: String { return self._s[1561]! } + public var Watch_Microphone_Access: String { return self._s[1562]! } + public func ChatImport_SelectionConfirmationGroupWithTitle(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[1563]!, self._r[1563]!, [_1, _2]) + } + public func PUSH_CHAT_MESSAGE_PHOTO(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[1564]!, self._r[1564]!, [_1, _2]) + } + public func ChatList_Search_NoResultsQueryDescription(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[1565]!, self._r[1565]!, [_0]) + } + public var Checkout_NewCard_PostcodeTitle: String { return self._s[1567]! } + public var TwoFactorSetup_Intro_Action: String { return self._s[1568]! } + public var Passport_Language_ne: String { return self._s[1569]! } + public var TwoStepAuth_EmailHelp: String { return self._s[1571]! } + public var Profile_MessageLifetime2s: String { return self._s[1572]! } + public func Conversation_MessageDialogRetryAll(_ _1: Int) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[1574]!, self._r[1574]!, ["\(_1)"]) + } + public func Items_NOfM(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[1575]!, self._r[1575]!, [_1, _2]) + } + public var VoiceChat_SendPublicLinkSend: String { return self._s[1576]! } + public var Media_LimitedAccessText: String { return self._s[1577]! } + public func PUSH_CHAT_TITLE_EDITED(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[1578]!, self._r[1578]!, [_1, _2]) + } + public var GroupPermission_NoPinMessages: String { return self._s[1579]! } + public func Notification_VoiceChatStarted(_ _1: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[1580]!, self._r[1580]!, [_1]) + } + public func Notification_CreatedChat(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[1581]!, self._r[1581]!, [_0]) + } + public var FastTwoStepSetup_HintHelp: String { return self._s[1582]! } + public var VoiceOver_SilentPostOff: String { return self._s[1583]! } + public var WallpaperSearch_ColorRed: String { return self._s[1584]! } + public var Watch_ConnectionDescription: String { return self._s[1585]! } + public var Notification_Exceptions_AddException: String { return self._s[1586]! } + public var LocalGroup_IrrelevantWarning: String { return self._s[1587]! } + public var VoiceOver_MessageContextDelete: String { return self._s[1588]! } + public var LogoutOptions_AlternativeOptionsSection: String { return self._s[1589]! } + public var Passport_PasswordPlaceholder: String { return self._s[1590]! } + public var TwoStepAuth_RecoveryEmailAddDescription: String { return self._s[1591]! } + public var Stats_MessageInteractionsTitle: String { return self._s[1592]! } + public var Appearance_ThemeCarouselClassic: String { return self._s[1593]! } + public var TwoFactorSetup_Email_SkipConfirmationText: String { return self._s[1595]! } + public var Channel_AdminLog_PinMessages: String { return self._s[1596]! } + public var Passport_Address_AddRentalAgreement: String { return self._s[1598]! } + public var Watch_Message_Game: String { return self._s[1599]! } + public var PrivacyLastSeenSettings_NeverShareWith: String { return self._s[1600]! } + public var PrivacyPolicy_DeclineLastWarning: String { return self._s[1601]! } + public var EditTheme_FileReadError: String { return self._s[1602]! } + public var Group_ErrorAddBlocked: String { return self._s[1603]! } + public var CallSettings_UseLessDataLongDescription: String { return self._s[1604]! } public func PUSH_MESSAGE_PHOTO(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1541]!, self._r[1541]!, [_1]) + return formatWithArgumentRanges(self._s[1606]!, self._r[1606]!, [_1]) } + public var GroupRemoved_ViewChannelInfo: String { return self._s[1607]! } public func UserInfo_BlockConfirmation(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1542]!, self._r[1542]!, [_0]) + return formatWithArgumentRanges(self._s[1608]!, self._r[1608]!, [_0]) } - public var CheckoutInfo_ShippingInfoAddress2Placeholder: String { return self._s[1543]! } - public var TwoFactorSetup_EmailVerification_Action: String { return self._s[1544]! } + public var CheckoutInfo_ShippingInfoAddress2Placeholder: String { return self._s[1609]! } + public var TwoFactorSetup_EmailVerification_Action: String { return self._s[1610]! } public func Username_LinkHint(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1545]!, self._r[1545]!, [_0]) + return formatWithArgumentRanges(self._s[1611]!, self._r[1611]!, [_0]) } - public var ConversationProfile_ErrorCreatingConversation: String { return self._s[1546]! } - public var Bot_GroupStatusReadsHistory: String { return self._s[1547]! } - public var PhotoEditor_CurvesRed: String { return self._s[1548]! } - public var InstantPage_TapToOpenLink: String { return self._s[1549]! } - public var InviteLink_PeopleJoinedShortNoneExpired: String { return self._s[1550]! } - public var FastTwoStepSetup_PasswordHelp: String { return self._s[1551]! } - public var Conversation_DiscussionNotStarted: String { return self._s[1552]! } - public var Notification_CallMissedShort: String { return self._s[1553]! } + public var ConversationProfile_ErrorCreatingConversation: String { return self._s[1612]! } + public var Bot_GroupStatusReadsHistory: String { return self._s[1613]! } + public var PhotoEditor_CurvesRed: String { return self._s[1614]! } + public var InstantPage_TapToOpenLink: String { return self._s[1615]! } + public var InviteLink_PeopleJoinedShortNoneExpired: String { return self._s[1616]! } + public var FastTwoStepSetup_PasswordHelp: String { return self._s[1617]! } + public var Conversation_DiscussionNotStarted: String { return self._s[1618]! } + public var Notification_CallMissedShort: String { return self._s[1619]! } public func Notification_JoinedGroupByLink(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1554]!, self._r[1554]!, [_0]) + return formatWithArgumentRanges(self._s[1620]!, self._r[1620]!, [_0]) } - public var Conversation_DeleteMessagesForEveryone: String { return self._s[1555]! } - public var Permissions_SiriTitle_v0: String { return self._s[1556]! } - public var GroupInfo_AddUserLeftError: String { return self._s[1557]! } - public var Conversation_SendMessage_SendSilently: String { return self._s[1558]! } - public var Paint_Duplicate: String { return self._s[1559]! } - public var AttachmentMenu_WebSearch: String { return self._s[1560]! } - public var Bot_Stop: String { return self._s[1562]! } - public var Conversation_PrivateChannelTimeLimitedAlertTitle: String { return self._s[1563]! } - public var ReportGroupLocation_Report: String { return self._s[1564]! } - public var Compose_Create: String { return self._s[1565]! } - public var Stats_GroupViewers: String { return self._s[1566]! } - public var AutoDownloadSettings_Channels: String { return self._s[1567]! } - public var PhotoEditor_QualityHigh: String { return self._s[1568]! } - public var VoiceChat_Leave: String { return self._s[1569]! } - public var Call_Speaker: String { return self._s[1570]! } + public var Conversation_DeleteMessagesForEveryone: String { return self._s[1621]! } + public var VoiceChat_UnpinVideo: String { return self._s[1622]! } + public var Permissions_SiriTitle_v0: String { return self._s[1623]! } + public var GroupInfo_AddUserLeftError: String { return self._s[1624]! } + public var Conversation_SendMessage_SendSilently: String { return self._s[1625]! } + public var Paint_Duplicate: String { return self._s[1626]! } + public var AttachmentMenu_WebSearch: String { return self._s[1627]! } + public var Bot_Stop: String { return self._s[1629]! } + public var Conversation_PrivateChannelTimeLimitedAlertTitle: String { return self._s[1630]! } + public var ReportGroupLocation_Report: String { return self._s[1631]! } + public var Compose_Create: String { return self._s[1632]! } + public var Stats_GroupViewers: String { return self._s[1633]! } + public var AutoDownloadSettings_Channels: String { return self._s[1634]! } + public var PhotoEditor_QualityHigh: String { return self._s[1635]! } + public var VoiceChat_Leave: String { return self._s[1636]! } + public var Call_Speaker: String { return self._s[1637]! } public func ChatList_LeaveGroupConfirmation(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1571]!, self._r[1571]!, [_0]) + return formatWithArgumentRanges(self._s[1638]!, self._r[1638]!, [_0]) } - public var Conversation_CloudStorage_ChatStatus: String { return self._s[1572]! } - public var Chat_AttachmentMultipleFilesDisabled: String { return self._s[1573]! } - public var ChatList_Context_AddToFolder: String { return self._s[1574]! } - public var InviteLink_QRCode_Info: String { return self._s[1575]! } - public var AutoremoveSetup_Title: String { return self._s[1576]! } - public var ChatList_DeleteForAllMembersConfirmationText: String { return self._s[1577]! } - public var Conversation_Unblock: String { return self._s[1578]! } - public var SettingsSearch_Synonyms_Proxy_UseForCalls: String { return self._s[1579]! } + public var Conversation_CloudStorage_ChatStatus: String { return self._s[1639]! } + public var Chat_AttachmentMultipleFilesDisabled: String { return self._s[1640]! } + public var ChatList_Context_AddToFolder: String { return self._s[1641]! } + public var InviteLink_QRCode_Info: String { return self._s[1642]! } + public var AutoremoveSetup_Title: String { return self._s[1643]! } + public var ChatList_DeleteForAllMembersConfirmationText: String { return self._s[1644]! } + public var Conversation_Unblock: String { return self._s[1645]! } + public var SettingsSearch_Synonyms_Proxy_UseForCalls: String { return self._s[1646]! } public func Time_PreciseDate_m8(_ _1: String, _ _2: String, _ _3: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1580]!, self._r[1580]!, [_1, _2, _3]) + return formatWithArgumentRanges(self._s[1647]!, self._r[1647]!, [_1, _2, _3]) } - public var Conversation_ContextMenuReply: String { return self._s[1581]! } - public var Contacts_SearchLabel: String { return self._s[1582]! } - public var Forward_ErrorPublicQuizDisabledInChannels: String { return self._s[1583]! } - public var Stats_GroupMessagesTitle: String { return self._s[1585]! } - public var Notification_CallCanceled: String { return self._s[1586]! } - public var VoiceOver_Chat_Selected: String { return self._s[1587]! } - public var NotificationsSound_Tremolo: String { return self._s[1589]! } - public var VoiceOver_AuthSessions_CurrentSession: String { return self._s[1590]! } - public var ChatList_Search_NoResultsDescription: String { return self._s[1591]! } - public var AccessDenied_PhotosAndVideos: String { return self._s[1592]! } - public var LogoutOptions_ClearCacheText: String { return self._s[1593]! } + public var Conversation_ContextMenuReply: String { return self._s[1648]! } + public var Contacts_SearchLabel: String { return self._s[1649]! } + public var Forward_ErrorPublicQuizDisabledInChannels: String { return self._s[1650]! } + public var Stats_GroupMessagesTitle: String { return self._s[1652]! } + public var VoiceChat_NoiseSuppression: String { return self._s[1653]! } + public var Notification_CallCanceled: String { return self._s[1654]! } + public var VoiceOver_Chat_Selected: String { return self._s[1655]! } + public var NotificationsSound_Tremolo: String { return self._s[1657]! } + public var VoiceOver_AuthSessions_CurrentSession: String { return self._s[1658]! } + public var ChatList_Search_NoResultsDescription: String { return self._s[1659]! } + public var AccessDenied_PhotosAndVideos: String { return self._s[1660]! } + public var LogoutOptions_ClearCacheText: String { return self._s[1661]! } public func VoiceChat_DisplayAsSuccess(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1595]!, self._r[1595]!, [_0]) + return formatWithArgumentRanges(self._s[1663]!, self._r[1663]!, [_0]) } - public var VoiceOver_Chat_Sticker: String { return self._s[1596]! } - public var ChatListFolder_NameUnread: String { return self._s[1597]! } - public var PeerInfo_ButtonMessage: String { return self._s[1599]! } - public var InfoPlist_NSPhotoLibraryAddUsageDescription: String { return self._s[1600]! } - public var BlockedUsers_SelectUserTitle: String { return self._s[1601]! } - public var ChatSettings_Other: String { return self._s[1602]! } - public var UserInfo_NotificationsEnabled: String { return self._s[1603]! } - public var CreatePoll_OptionsHeader: String { return self._s[1604]! } - public var Appearance_RemoveThemeColorConfirmation: String { return self._s[1607]! } - public var Channel_Moderator_Title: String { return self._s[1608]! } + public var VoiceOver_Chat_Sticker: String { return self._s[1664]! } + public var ChatListFolder_NameUnread: String { return self._s[1665]! } + public var PeerInfo_ButtonMessage: String { return self._s[1667]! } + public var InfoPlist_NSPhotoLibraryAddUsageDescription: String { return self._s[1668]! } + public var Settings_KeepPassword: String { return self._s[1669]! } + public var BlockedUsers_SelectUserTitle: String { return self._s[1670]! } + public var ChatSettings_Other: String { return self._s[1671]! } + public var UserInfo_NotificationsEnabled: String { return self._s[1672]! } + public var CreatePoll_OptionsHeader: String { return self._s[1673]! } + public var Appearance_RemoveThemeColorConfirmation: String { return self._s[1676]! } + public var Channel_Moderator_Title: String { return self._s[1677]! } public func Conversation_ForwardTooltip_Chat_Many(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1609]!, self._r[1609]!, [_0]) + return formatWithArgumentRanges(self._s[1678]!, self._r[1678]!, [_0]) } - public var Channel_AdminLog_MessageRestrictedForever: String { return self._s[1610]! } - public var WallpaperColors_Title: String { return self._s[1611]! } - public var InviteLink_InviteLink: String { return self._s[1613]! } - public var PrivacyPolicy_DeclineMessage: String { return self._s[1614]! } - public var AutoDownloadSettings_VoiceMessagesTitle: String { return self._s[1615]! } - public var Your_card_was_declined: String { return self._s[1616]! } - public var SettingsSearch_FAQ: String { return self._s[1618]! } - public var EditTheme_Expand_Preview_IncomingReplyName: String { return self._s[1619]! } - public var Conversation_ReportSpamConfirmation: String { return self._s[1620]! } - public var OwnershipTransfer_SecurityCheck: String { return self._s[1622]! } - public var PrivacySettings_DataSettingsHelp: String { return self._s[1623]! } - public var Settings_About_Help: String { return self._s[1624]! } + public func UserInfo_ContactForwardTooltip_ManyChats_One(_ _0: String, _ _1: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[1679]!, self._r[1679]!, [_0, _1]) + } + public var Channel_AdminLog_MessageRestrictedForever: String { return self._s[1680]! } + public var WallpaperColors_Title: String { return self._s[1681]! } + public var InviteLink_InviteLink: String { return self._s[1683]! } + public var PrivacyPolicy_DeclineMessage: String { return self._s[1684]! } + public var AutoDownloadSettings_VoiceMessagesTitle: String { return self._s[1685]! } + public var Your_card_was_declined: String { return self._s[1686]! } + public var SettingsSearch_FAQ: String { return self._s[1688]! } + public var EditTheme_Expand_Preview_IncomingReplyName: String { return self._s[1689]! } + public var Conversation_ReportSpamConfirmation: String { return self._s[1690]! } + public var OwnershipTransfer_SecurityCheck: String { return self._s[1692]! } + public var PrivacySettings_DataSettingsHelp: String { return self._s[1693]! } + public var Settings_About_Help: String { return self._s[1694]! } public func Channel_DiscussionGroup_HeaderGroupSet(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1625]!, self._r[1625]!, [_0]) + return formatWithArgumentRanges(self._s[1695]!, self._r[1695]!, [_0]) } - public var Settings_Proxy: String { return self._s[1626]! } - public var TwoStepAuth_ResetAccountConfirmation: String { return self._s[1627]! } - public var Passport_Identity_TypePassportUploadScan: String { return self._s[1629]! } - public var NotificationsSound_Bell: String { return self._s[1630]! } - public var PrivacySettings_Title: String { return self._s[1632]! } - public var PrivacySettings_DataSettings: String { return self._s[1633]! } - public var ConversationMedia_Title: String { return self._s[1634]! } + public var Settings_Proxy: String { return self._s[1696]! } + public var TwoStepAuth_ResetAccountConfirmation: String { return self._s[1697]! } + public var Passport_Identity_TypePassportUploadScan: String { return self._s[1699]! } + public var NotificationsSound_Bell: String { return self._s[1700]! } + public var PrivacySettings_Title: String { return self._s[1702]! } + public var PrivacySettings_DataSettings: String { return self._s[1703]! } + public var ConversationMedia_Title: String { return self._s[1704]! } public func Channel_AdminLog_MessageAddedAdminName(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1635]!, self._r[1635]!, [_1]) + return formatWithArgumentRanges(self._s[1705]!, self._r[1705]!, [_1]) } public func Conversation_EncryptedPlaceholderTitleIncoming(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1636]!, self._r[1636]!, [_0]) + return formatWithArgumentRanges(self._s[1706]!, self._r[1706]!, [_0]) } - public var PrivacySettings_BlockedPeersEmpty: String { return self._s[1637]! } - public var ReportPeer_ReasonPornography: String { return self._s[1639]! } - public var Privacy_Calls: String { return self._s[1640]! } - public var TwoFactorSetup_Email_Text: String { return self._s[1641]! } - public var Conversation_EncryptedDescriptionTitle: String { return self._s[1642]! } + public var PrivacySettings_BlockedPeersEmpty: String { return self._s[1707]! } + public var ReportPeer_ReasonPornography: String { return self._s[1709]! } + public var Privacy_Calls: String { return self._s[1711]! } + public var TwoFactorSetup_Email_Text: String { return self._s[1712]! } + public var Conversation_EncryptedDescriptionTitle: String { return self._s[1713]! } public func VoiceOver_Chat_MusicTitle(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1643]!, self._r[1643]!, [_1, _2]) - } - public var Passport_Identity_FrontSideHelp: String { return self._s[1644]! } - public var InstantPage_VoiceOver_DecreaseFontSize: String { return self._s[1645]! } - public var GroupInfo_Permissions_SlowmodeHeader: String { return self._s[1647]! } - public var ContactList_Context_VideoCall: String { return self._s[1648]! } - public var Settings_SaveIncomingPhotos: String { return self._s[1649]! } - public var Passport_Identity_MiddleName: String { return self._s[1650]! } - public var MessagePoll_QuizNoUsers: String { return self._s[1651]! } - public func Channel_AdminLog_MutedParticipant(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1652]!, self._r[1652]!, [_1, _2]) - } - public var OldChannels_ChannelFormat: String { return self._s[1653]! } - public var Watch_Message_Call: String { return self._s[1654]! } - public var VoiceChat_OpenChannel: String { return self._s[1655]! } - public var Wallpaper_Title: String { return self._s[1656]! } - public var PasscodeSettings_TurnPasscodeOff: String { return self._s[1657]! } - public var IntentsSettings_SuggestedChatsSavedMessages: String { return self._s[1658]! } - public var ReportGroupLocation_Text: String { return self._s[1659]! } - public var InviteText_URL: String { return self._s[1660]! } - public var ClearCache_StorageServiceFiles: String { return self._s[1661]! } - public var MessageTimer_Custom: String { return self._s[1662]! } - public var Message_PinnedLocationMessage: String { return self._s[1663]! } - public func VoiceOver_Chat_ContactOrganization(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1664]!, self._r[1664]!, [_0]) - } - public var EditTheme_UploadNewTheme: String { return self._s[1665]! } - public var ChatImportActivity_ErrorLimitExceeded: String { return self._s[1668]! } - public func AutoDownloadSettings_UpToForAll(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1669]!, self._r[1669]!, [_0]) - } - public var Login_CodeSentCall: String { return self._s[1671]! } - public func Conversation_AutoremoveTimerSetUser(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1672]!, self._r[1672]!, [_1, _2]) - } - public var Conversation_Report: String { return self._s[1673]! } - public var NotificationSettings_ContactJoined: String { return self._s[1674]! } - public func PUSH_MESSAGE_SCREENSHOT(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1675]!, self._r[1675]!, [_1]) - } - public var StickerPacksSettings_ShowStickersButtonHelp: String { return self._s[1676]! } - public var BroadcastGroups_IntroText: String { return self._s[1677]! } - public var IntentsSettings_SuggestByAll: String { return self._s[1679]! } - public var StickerPacksSettings_ShowStickersButton: String { return self._s[1680]! } - public var AuthSessions_Title: String { return self._s[1681]! } - public func Notification_VoiceChatEnded(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1682]!, self._r[1682]!, [_0]) - } - public var Channel_AdminLog_TitleAllEvents: String { return self._s[1683]! } - public var KeyCommand_JumpToNextUnreadChat: String { return self._s[1684]! } - public var VoiceChat_YouCanNowSpeak: String { return self._s[1687]! } - public var Passport_Address_AddPassportRegistration: String { return self._s[1689]! } - public var AutoDownloadSettings_MaxVideoSize: String { return self._s[1690]! } - public var ExplicitContent_AlertTitle: String { return self._s[1691]! } - public var Channel_UpdatePhotoItem: String { return self._s[1692]! } - public var ChatList_AutoarchiveSuggestion_Text: String { return self._s[1694]! } - public var Channel_DiscussionGroup_LinkGroup: String { return self._s[1695]! } - public func Call_BatteryLow(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1696]!, self._r[1696]!, [_0]) - } - public var Login_HaveNotReceivedCodeInternal: String { return self._s[1697]! } - public var WallpaperPreview_PatternPaternApply: String { return self._s[1698]! } - public var Notifications_MessageNotificationsSound: String { return self._s[1699]! } - public var CommentsGroup_ErrorAccessDenied: String { return self._s[1700]! } - public var Appearance_AccentColor: String { return self._s[1702]! } - public var GroupInfo_SharedMedia: String { return self._s[1703]! } - public var Login_PhonePlaceholder: String { return self._s[1704]! } - public var Appearance_TextSize_Automatic: String { return self._s[1705]! } - public var EmptyGroupInfo_Line2: String { return self._s[1706]! } - public func PUSH_CHAT_CREATED(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1707]!, self._r[1707]!, [_1, _2]) - } - public var Conversation_ClearChannel: String { return self._s[1708]! } - public var Appearance_AppIconDefaultX: String { return self._s[1710]! } - public var EditProfile_NameAndPhotoOrVideoHelp: String { return self._s[1711]! } - public var CheckoutInfo_ShippingInfoPostcodePlaceholder: String { return self._s[1712]! } - public var Notifications_GroupNotificationsHelp: String { return self._s[1713]! } - public func PUSH_CHAT_MESSAGE_NOTEXT(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(self._s[1714]!, self._r[1714]!, [_1, _2]) } - public var ChatList_EmptyChatListEditFilter: String { return self._s[1715]! } - public var ChatSettings_ConnectionType_UseProxy: String { return self._s[1718]! } - public var Chat_PinnedMessagesHiddenText: String { return self._s[1719]! } - public func Message_PinnedGenericMessage(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1720]!, self._r[1720]!, [_0]) + public var Passport_Identity_FrontSideHelp: String { return self._s[1715]! } + public var InstantPage_VoiceOver_DecreaseFontSize: String { return self._s[1716]! } + public var GroupInfo_Permissions_SlowmodeHeader: String { return self._s[1718]! } + public var ContactList_Context_VideoCall: String { return self._s[1719]! } + public var Settings_SaveIncomingPhotos: String { return self._s[1720]! } + public var Passport_Identity_MiddleName: String { return self._s[1721]! } + public var MessagePoll_QuizNoUsers: String { return self._s[1722]! } + public func Channel_AdminLog_MutedParticipant(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[1723]!, self._r[1723]!, [_1, _2]) } - public func Location_ProximityTip(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1721]!, self._r[1721]!, [_0]) + public var OldChannels_ChannelFormat: String { return self._s[1724]! } + public var Watch_Message_Call: String { return self._s[1725]! } + public var VoiceChat_OpenChannel: String { return self._s[1726]! } + public var Wallpaper_Title: String { return self._s[1727]! } + public var PasscodeSettings_TurnPasscodeOff: String { return self._s[1728]! } + public var IntentsSettings_SuggestedChatsSavedMessages: String { return self._s[1729]! } + public var ReportGroupLocation_Text: String { return self._s[1730]! } + public var InviteText_URL: String { return self._s[1731]! } + public var ClearCache_StorageServiceFiles: String { return self._s[1732]! } + public var MessageTimer_Custom: String { return self._s[1733]! } + public var Message_PinnedLocationMessage: String { return self._s[1734]! } + public func VoiceOver_Chat_ContactOrganization(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[1735]!, self._r[1735]!, [_0]) } - public var UserInfo_NotificationsEnable: String { return self._s[1722]! } - public var Checkout_PayWithTouchId: String { return self._s[1723]! } - public var SharedMedia_ViewInChat: String { return self._s[1724]! } - public func Notification_CreatedChatWithTitle(_ _0: String, _ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1725]!, self._r[1725]!, [_0, _1]) + public var EditTheme_UploadNewTheme: String { return self._s[1736]! } + public var TwoFactorRemember_CheckPassword: String { return self._s[1739]! } + public var ChatImportActivity_ErrorLimitExceeded: String { return self._s[1740]! } + public func AutoDownloadSettings_UpToForAll(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[1741]!, self._r[1741]!, [_0]) } - public var ChatSettings_AutoDownloadSettings_OffForAll: String { return self._s[1726]! } - public func Channel_DiscussionGroup_PublicChannelLink(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1727]!, self._r[1727]!, [_1, _2]) + public var Login_CodeSentCall: String { return self._s[1743]! } + public func Conversation_AutoremoveTimerSetUser(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[1744]!, self._r[1744]!, [_1, _2]) } - public func Cache_Clear(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1729]!, self._r[1729]!, [_0]) - } - public var Conversation_PeerNearbyText: String { return self._s[1731]! } - public var Conversation_StopPollConfirmationTitle: String { return self._s[1732]! } - public var PhotoEditor_Skip: String { return self._s[1733]! } - public var SettingsSearch_Synonyms_Appearance_ChatBackground_SetColor: String { return self._s[1734]! } - public var ChatList_EmptyChatList: String { return self._s[1735]! } - public var Channel_BanUser_Unban: String { return self._s[1736]! } - public func Message_GenericForwardedPsa(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1737]!, self._r[1737]!, [_0]) - } - public var Appearance_TextSize_Apply: String { return self._s[1738]! } - public func Conversation_MessageViewCommentsFormat(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1739]!, self._r[1739]!, [_1, _2]) - } - public var Login_InfoFirstNamePlaceholder: String { return self._s[1740]! } - public var VoiceOver_Chat_YourSticker: String { return self._s[1741]! } - public var TwoStepAuth_HintPlaceholder: String { return self._s[1742]! } - public var TwoStepAuth_EmailSkip: String { return self._s[1744]! } - public var ChatList_UndoArchiveMultipleTitle: String { return self._s[1745]! } - public var TwoFactorSetup_Email_SkipConfirmationTitle: String { return self._s[1746]! } - public func PUSH_MESSAGE_QUIZ(_ _1: String) -> (String, [(Int, NSRange)]) { + public var Conversation_Report: String { return self._s[1745]! } + public var NotificationSettings_ContactJoined: String { return self._s[1746]! } + public func PUSH_MESSAGE_SCREENSHOT(_ _1: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(self._s[1747]!, self._r[1747]!, [_1]) } - public var VoiceOver_Chat_GoToOriginalMessage: String { return self._s[1749]! } - public var State_WaitingForNetwork: String { return self._s[1750]! } - public var AccessDenied_CameraRestricted: String { return self._s[1751]! } - public var ChatSettings_Appearance: String { return self._s[1752]! } - public var ScheduledMessages_BotActionUnavailable: String { return self._s[1753]! } - public var GroupInfo_InviteLink_CopyAlert_Success: String { return self._s[1754]! } - public var Channel_DiscussionGroupAdd: String { return self._s[1755]! } - public var Conversation_SelectMessages: String { return self._s[1757]! } - public var Map_NoPlacesNearby: String { return self._s[1758]! } - public var AuthSessions_IncompleteAttemptsInfo: String { return self._s[1759]! } - public var GroupRemoved_Title: String { return self._s[1760]! } - public var TwoStepAuth_EnterPasswordHelp: String { return self._s[1762]! } - public var VoiceChat_Mute: String { return self._s[1763]! } - public var Paint_Marker: String { return self._s[1764]! } - public var Widget_ChatsGalleryTitle: String { return self._s[1765]! } - public func AddContact_ContactWillBeSharedAfterMutual(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1766]!, self._r[1766]!, [_1]) + public var StickerPacksSettings_ShowStickersButtonHelp: String { return self._s[1748]! } + public var BroadcastGroups_IntroText: String { return self._s[1749]! } + public var IntentsSettings_SuggestByAll: String { return self._s[1751]! } + public var StickerPacksSettings_ShowStickersButton: String { return self._s[1752]! } + public var AuthSessions_Title: String { return self._s[1753]! } + public func Notification_VoiceChatEnded(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[1754]!, self._r[1754]!, [_0]) } - public var SocksProxySetup_ShareProxyList: String { return self._s[1767]! } - public var GroupInfo_InvitationLinkDoesNotExist: String { return self._s[1768]! } - public func VoiceOver_Chat_Size(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1769]!, self._r[1769]!, [_0]) + public var Settings_Tips: String { return self._s[1755]! } + public var Channel_AdminLog_TitleAllEvents: String { return self._s[1756]! } + public var WallpaperPreview_WallpaperColors: String { return self._s[1757]! } + public var KeyCommand_JumpToNextUnreadChat: String { return self._s[1758]! } + public var VoiceChat_YouCanNowSpeak: String { return self._s[1761]! } + public var Passport_Address_AddPassportRegistration: String { return self._s[1763]! } + public func UserInfo_LinkForwardTooltip_ManyChats_One(_ _0: String, _ _1: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[1764]!, self._r[1764]!, [_0, _1]) } - public var EditTheme_ErrorInvalidCharacters: String { return self._s[1770]! } - public var Appearance_ThemePreview_ChatList_7_Name: String { return self._s[1771]! } - public var Notifications_GroupNotificationsAlert: String { return self._s[1772]! } - public var SocksProxySetup_ShareQRCode: String { return self._s[1773]! } - public var Compose_NewGroup: String { return self._s[1774]! } - public func Passport_Address_UploadOneOfScan(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1775]!, self._r[1775]!, [_0]) + public var AutoDownloadSettings_MaxVideoSize: String { return self._s[1765]! } + public var ExplicitContent_AlertTitle: String { return self._s[1766]! } + public var Channel_UpdatePhotoItem: String { return self._s[1768]! } + public var ChatList_AutoarchiveSuggestion_Text: String { return self._s[1770]! } + public var Channel_DiscussionGroup_LinkGroup: String { return self._s[1771]! } + public func Call_BatteryLow(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[1772]!, self._r[1772]!, [_0]) } - public var Location_LiveLocationRequired_Description: String { return self._s[1777]! } - public var Conversation_ClearGroupHistory: String { return self._s[1778]! } - public var GroupInfo_InviteLink_Help: String { return self._s[1781]! } - public var VoiceOver_BotKeyboard: String { return self._s[1782]! } - public var Channel_BanUser_BlockFor: String { return self._s[1783]! } - public var Bot_Start: String { return self._s[1784]! } - public var Your_card_has_expired: String { return self._s[1785]! } - public var Channel_About_Title: String { return self._s[1786]! } - public var VoiceChat_EditTitleTitle: String { return self._s[1787]! } - public var Passport_Identity_ExpiryDatePlaceholder: String { return self._s[1788]! } - public var SettingsSearch_Synonyms_Notifications_MessageNotificationsExceptions: String { return self._s[1790]! } - public var Conversation_FileDropbox: String { return self._s[1791]! } - public var ChatList_Search_NoResultsFitlerMusic: String { return self._s[1792]! } - public var Month_GenNovember: String { return self._s[1793]! } - public var IntentsSettings_SuggestByShare: String { return self._s[1794]! } - public func Call_PrivacyErrorMessage(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1795]!, self._r[1795]!, [_0]) + public var Login_HaveNotReceivedCodeInternal: String { return self._s[1773]! } + public var WallpaperPreview_PatternPaternApply: String { return self._s[1774]! } + public var Notifications_MessageNotificationsSound: String { return self._s[1775]! } + public var CommentsGroup_ErrorAccessDenied: String { return self._s[1776]! } + public var Appearance_AccentColor: String { return self._s[1778]! } + public var GroupInfo_SharedMedia: String { return self._s[1779]! } + public var Login_PhonePlaceholder: String { return self._s[1780]! } + public var Appearance_TextSize_Automatic: String { return self._s[1781]! } + public var EmptyGroupInfo_Line2: String { return self._s[1782]! } + public func PUSH_CHAT_CREATED(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[1783]!, self._r[1783]!, [_1, _2]) } - public var StickerPack_Add: String { return self._s[1796]! } - public var Theme_ErrorNotFound: String { return self._s[1797]! } - public var Wallpaper_SearchShort: String { return self._s[1799]! } - public var Channel_BanUser_PermissionsHeader: String { return self._s[1800]! } - public var ConversationProfile_UsersTooMuchError: String { return self._s[1801]! } - public var ChatList_FolderAllChats: String { return self._s[1802]! } - public var VoiceChat_EndConfirmationEnd: String { return self._s[1803]! } - public var Passport_Authorize: String { return self._s[1804]! } - public func Channel_AdminLog_MessageChangedLinkedChannel(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { + public var VoiceChat_TapToAddPhotoOrBio: String { return self._s[1784]! } + public var Conversation_ClearChannel: String { return self._s[1785]! } + public var Conversation_MessageDoesntExist: String { return self._s[1786]! } + public var Appearance_AppIconDefaultX: String { return self._s[1788]! } + public var EditProfile_NameAndPhotoOrVideoHelp: String { return self._s[1789]! } + public var CheckoutInfo_ShippingInfoPostcodePlaceholder: String { return self._s[1790]! } + public var Notifications_GroupNotificationsHelp: String { return self._s[1791]! } + public func PUSH_CHAT_MESSAGE_NOTEXT(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[1792]!, self._r[1792]!, [_1, _2]) + } + public var ChatList_EmptyChatListEditFilter: String { return self._s[1793]! } + public var ChatSettings_ConnectionType_UseProxy: String { return self._s[1796]! } + public var Chat_PinnedMessagesHiddenText: String { return self._s[1797]! } + public func Message_PinnedGenericMessage(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[1798]!, self._r[1798]!, [_0]) + } + public func Location_ProximityTip(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[1799]!, self._r[1799]!, [_0]) + } + public var UserInfo_NotificationsEnable: String { return self._s[1800]! } + public var Checkout_PayWithTouchId: String { return self._s[1801]! } + public var SharedMedia_ViewInChat: String { return self._s[1802]! } + public func Notification_CreatedChatWithTitle(_ _0: String, _ _1: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[1803]!, self._r[1803]!, [_0, _1]) + } + public var ChatSettings_AutoDownloadSettings_OffForAll: String { return self._s[1804]! } + public func Channel_DiscussionGroup_PublicChannelLink(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(self._s[1805]!, self._r[1805]!, [_1, _2]) } - public var GroupInfo_GroupHistoryVisible: String { return self._s[1806]! } + public func Cache_Clear(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[1807]!, self._r[1807]!, [_0]) + } + public var Conversation_PeerNearbyText: String { return self._s[1809]! } + public var Conversation_StopPollConfirmationTitle: String { return self._s[1810]! } + public var PhotoEditor_Skip: String { return self._s[1811]! } + public var SettingsSearch_Synonyms_Appearance_ChatBackground_SetColor: String { return self._s[1812]! } + public var ChatList_EmptyChatList: String { return self._s[1813]! } + public var Channel_BanUser_Unban: String { return self._s[1814]! } + public func Message_GenericForwardedPsa(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[1815]!, self._r[1815]!, [_0]) + } + public var Appearance_TextSize_Apply: String { return self._s[1816]! } + public func Conversation_MessageViewCommentsFormat(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[1817]!, self._r[1817]!, [_1, _2]) + } + public var Login_InfoFirstNamePlaceholder: String { return self._s[1818]! } + public var VoiceOver_Chat_YourSticker: String { return self._s[1819]! } + public var TwoStepAuth_HintPlaceholder: String { return self._s[1820]! } + public var TwoStepAuth_EmailSkip: String { return self._s[1822]! } + public var ChatList_UndoArchiveMultipleTitle: String { return self._s[1823]! } + public var TwoFactorSetup_Email_SkipConfirmationTitle: String { return self._s[1824]! } + public func PUSH_MESSAGE_QUIZ(_ _1: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[1825]!, self._r[1825]!, [_1]) + } + public var VoiceOver_Chat_GoToOriginalMessage: String { return self._s[1827]! } + public var State_WaitingForNetwork: String { return self._s[1828]! } + public var AccessDenied_CameraRestricted: String { return self._s[1829]! } + public var ChatSettings_Appearance: String { return self._s[1830]! } + public var ScheduledMessages_BotActionUnavailable: String { return self._s[1831]! } + public var GroupInfo_InviteLink_CopyAlert_Success: String { return self._s[1832]! } + public var Channel_DiscussionGroupAdd: String { return self._s[1833]! } + public var Conversation_SelectMessages: String { return self._s[1835]! } + public var Map_NoPlacesNearby: String { return self._s[1836]! } + public var AuthSessions_IncompleteAttemptsInfo: String { return self._s[1837]! } + public var GroupRemoved_Title: String { return self._s[1838]! } + public var ImportStickerPack_RemoveFromImport: String { return self._s[1839]! } + public var TwoStepAuth_EnterPasswordHelp: String { return self._s[1841]! } + public var VoiceChat_Mute: String { return self._s[1842]! } + public var Paint_Marker: String { return self._s[1843]! } + public var Widget_ChatsGalleryTitle: String { return self._s[1844]! } + public func AddContact_ContactWillBeSharedAfterMutual(_ _1: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[1845]!, self._r[1845]!, [_1]) + } + public var SocksProxySetup_ShareProxyList: String { return self._s[1846]! } + public var GroupInfo_InvitationLinkDoesNotExist: String { return self._s[1847]! } + public func VoiceOver_Chat_Size(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[1848]!, self._r[1848]!, [_0]) + } + public var EditTheme_ErrorInvalidCharacters: String { return self._s[1849]! } + public var Appearance_ThemePreview_ChatList_7_Name: String { return self._s[1850]! } + public var Settings_CheckPasswordTitle: String { return self._s[1851]! } + public var Notifications_GroupNotificationsAlert: String { return self._s[1852]! } + public var SocksProxySetup_ShareQRCode: String { return self._s[1853]! } + public var Compose_NewGroup: String { return self._s[1855]! } + public func Passport_Address_UploadOneOfScan(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[1856]!, self._r[1856]!, [_0]) + } + public var Location_LiveLocationRequired_Description: String { return self._s[1858]! } + public var Conversation_ClearGroupHistory: String { return self._s[1859]! } + public var GroupInfo_InviteLink_Help: String { return self._s[1862]! } + public var VoiceOver_BotKeyboard: String { return self._s[1863]! } + public var Channel_BanUser_BlockFor: String { return self._s[1864]! } + public var Bot_Start: String { return self._s[1865]! } + public var Your_card_has_expired: String { return self._s[1866]! } + public var Channel_About_Title: String { return self._s[1867]! } + public var VoiceChat_EditTitleTitle: String { return self._s[1868]! } + public var Passport_Identity_ExpiryDatePlaceholder: String { return self._s[1869]! } + public var SettingsSearch_Synonyms_Notifications_MessageNotificationsExceptions: String { return self._s[1871]! } + public var Conversation_FileDropbox: String { return self._s[1872]! } + public var ChatList_Search_NoResultsFitlerMusic: String { return self._s[1873]! } + public var Month_GenNovember: String { return self._s[1874]! } + public var IntentsSettings_SuggestByShare: String { return self._s[1875]! } + public func Call_PrivacyErrorMessage(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[1876]!, self._r[1876]!, [_0]) + } + public var StickerPack_Add: String { return self._s[1877]! } + public var Theme_ErrorNotFound: String { return self._s[1878]! } + public var Wallpaper_SearchShort: String { return self._s[1880]! } + public var Channel_BanUser_PermissionsHeader: String { return self._s[1881]! } + public var ConversationProfile_UsersTooMuchError: String { return self._s[1882]! } + public var ChatList_FolderAllChats: String { return self._s[1883]! } + public var VoiceChat_EndConfirmationEnd: String { return self._s[1884]! } + public var Passport_Authorize: String { return self._s[1885]! } + public func Channel_AdminLog_MessageChangedLinkedChannel(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[1886]!, self._r[1886]!, [_1, _2]) + } + public var GroupInfo_GroupHistoryVisible: String { return self._s[1887]! } public func PUSH_MESSAGE_VIDEO(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1807]!, self._r[1807]!, [_1]) + return formatWithArgumentRanges(self._s[1888]!, self._r[1888]!, [_1]) } - public var LocalGroup_ButtonTitle: String { return self._s[1808]! } - public var VoiceOver_Stickers: String { return self._s[1810]! } - public var UserInfo_GroupsInCommon: String { return self._s[1811]! } - public var LoginPassword_Title: String { return self._s[1813]! } - public var Wallpaper_Set: String { return self._s[1814]! } - public var Stats_InteractionsTitle: String { return self._s[1815]! } + public var LocalGroup_ButtonTitle: String { return self._s[1889]! } + public var VoiceOver_Stickers: String { return self._s[1891]! } + public var UserInfo_GroupsInCommon: String { return self._s[1892]! } + public var LoginPassword_Title: String { return self._s[1894]! } + public var Wallpaper_Set: String { return self._s[1895]! } + public var Stats_InteractionsTitle: String { return self._s[1896]! } public func SecretGIF_NotViewedYet(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1817]!, self._r[1817]!, [_0]) + return formatWithArgumentRanges(self._s[1898]!, self._r[1898]!, [_0]) } - public var Conversation_MessageDialogEdit: String { return self._s[1818]! } - public var Paint_Outlined: String { return self._s[1819]! } + public var Conversation_MessageDialogEdit: String { return self._s[1899]! } + public var Paint_Outlined: String { return self._s[1900]! } public func Login_ResetAccountProtected_Text(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1820]!, self._r[1820]!, [_0]) + return formatWithArgumentRanges(self._s[1901]!, self._r[1901]!, [_0]) } public func Conversation_SetReminder_RemindTomorrow(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1821]!, self._r[1821]!, [_0]) + return formatWithArgumentRanges(self._s[1902]!, self._r[1902]!, [_0]) } - public var Invite_LargeRecipientsCountWarning: String { return self._s[1822]! } - public var Passport_Address_Street1Placeholder: String { return self._s[1823]! } - public var Appearance_ColorThemeNight: String { return self._s[1824]! } - public var ChannelInfo_Stats: String { return self._s[1825]! } - public var Widget_ShortcutsGalleryTitle: String { return self._s[1826]! } - public var TwoStepAuth_RecoveryTitle: String { return self._s[1827]! } - public var MediaPicker_TimerTooltip: String { return self._s[1828]! } - public var ChatImportActivity_ErrorNotAdmin: String { return self._s[1829]! } - public var Common_ChoosePhoto: String { return self._s[1830]! } - public var Media_LimitedAccessTitle: String { return self._s[1831]! } - public var ChatSettings_AutoDownloadVideos: String { return self._s[1832]! } - public var PeerInfo_PaneGroups: String { return self._s[1833]! } - public var SocksProxySetup_UsernamePlaceholder: String { return self._s[1835]! } - public var ChangePhoneNumberNumber_Title: String { return self._s[1836]! } - public var ContactInfo_PhoneLabelMobile: String { return self._s[1837]! } - public var OldChannels_ChannelsHeader: String { return self._s[1838]! } - public var MuteFor_Forever: String { return self._s[1839]! } - public var Passport_Address_PostcodePlaceholder: String { return self._s[1840]! } - public var SettingsSearch_Synonyms_Appearance_ChatBackground: String { return self._s[1841]! } - public var MessagePoll_LabelAnonymous: String { return self._s[1842]! } - public var ContactInfo_Job: String { return self._s[1843]! } - public var Passport_Language_mk: String { return self._s[1844]! } - public var EditTheme_ShortLink: String { return self._s[1845]! } - public var AutoDownloadSettings_PhotosTitle: String { return self._s[1848]! } - public var Month_GenApril: String { return self._s[1850]! } - public var Channel_DiscussionGroup_HeaderLabel: String { return self._s[1852]! } - public var NetworkUsageSettings_TotalSection: String { return self._s[1853]! } - public var EditTheme_Create_Preview_OutgoingText: String { return self._s[1854]! } - public var EditTheme_Title: String { return self._s[1855]! } - public var Conversation_LinkDialogCopy: String { return self._s[1856]! } + public var Invite_LargeRecipientsCountWarning: String { return self._s[1903]! } + public var Passport_Address_Street1Placeholder: String { return self._s[1904]! } + public var Appearance_ColorThemeNight: String { return self._s[1905]! } + public var ChannelInfo_Stats: String { return self._s[1906]! } + public var Widget_ShortcutsGalleryTitle: String { return self._s[1907]! } + public var TwoStepAuth_RecoveryTitle: String { return self._s[1908]! } + public var MediaPicker_TimerTooltip: String { return self._s[1909]! } + public var ChatImportActivity_ErrorNotAdmin: String { return self._s[1910]! } + public var TwoFactorRemember_Title: String { return self._s[1911]! } + public var Common_ChoosePhoto: String { return self._s[1912]! } + public var Media_LimitedAccessTitle: String { return self._s[1913]! } + public var ChatSettings_AutoDownloadVideos: String { return self._s[1914]! } + public var PeerInfo_PaneGroups: String { return self._s[1915]! } + public var SocksProxySetup_UsernamePlaceholder: String { return self._s[1917]! } + public var ChangePhoneNumberNumber_Title: String { return self._s[1918]! } + public var ContactInfo_PhoneLabelMobile: String { return self._s[1919]! } + public var OldChannels_ChannelsHeader: String { return self._s[1920]! } + public var MuteFor_Forever: String { return self._s[1921]! } + public var Passport_Address_PostcodePlaceholder: String { return self._s[1922]! } + public var SettingsSearch_Synonyms_Appearance_ChatBackground: String { return self._s[1924]! } + public var MessagePoll_LabelAnonymous: String { return self._s[1925]! } + public var ContactInfo_Job: String { return self._s[1926]! } + public var Passport_Language_mk: String { return self._s[1927]! } + public var EditTheme_ShortLink: String { return self._s[1928]! } + public var AutoDownloadSettings_PhotosTitle: String { return self._s[1931]! } + public var Month_GenApril: String { return self._s[1933]! } + public var Channel_DiscussionGroup_HeaderLabel: String { return self._s[1935]! } + public var NetworkUsageSettings_TotalSection: String { return self._s[1936]! } + public var EditTheme_Create_Preview_OutgoingText: String { return self._s[1937]! } + public var EditTheme_Title: String { return self._s[1938]! } + public var Conversation_LinkDialogCopy: String { return self._s[1939]! } public func Channel_AdminLog_MessageInvitedNameUsername(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1857]!, self._r[1857]!, [_1, _2]) + return formatWithArgumentRanges(self._s[1940]!, self._r[1940]!, [_1, _2]) } - public var Passport_ForgottenPassword: String { return self._s[1858]! } - public var WallpaperSearch_Recent: String { return self._s[1859]! } - public var ChatSettings_Title: String { return self._s[1864]! } - public var Appearance_ReduceMotionInfo: String { return self._s[1865]! } + public var Passport_ForgottenPassword: String { return self._s[1941]! } + public var WallpaperSearch_Recent: String { return self._s[1942]! } + public var ChatSettings_Title: String { return self._s[1947]! } + public var Appearance_ReduceMotionInfo: String { return self._s[1948]! } public func StickerPackActionInfo_AddedText(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1866]!, self._r[1866]!, [_0]) + return formatWithArgumentRanges(self._s[1949]!, self._r[1949]!, [_0]) } - public var SocksProxySetup_UseForCallsHelp: String { return self._s[1867]! } - public var LastSeen_WithinAMonth: String { return self._s[1868]! } - public var VoiceChat_Live: String { return self._s[1869]! } - public var PeerInfo_ButtonCall: String { return self._s[1870]! } - public var SettingsSearch_Synonyms_Appearance_Title: String { return self._s[1871]! } - public var Group_Username_InvalidStartsWithNumber: String { return self._s[1872]! } - public var Call_AudioRouteHide: String { return self._s[1873]! } - public var DialogList_SavedMessages: String { return self._s[1874]! } - public var ChatList_Context_Mute: String { return self._s[1875]! } - public var Conversation_StatusKickedFromChannel: String { return self._s[1876]! } + public var SocksProxySetup_UseForCallsHelp: String { return self._s[1950]! } + public var LastSeen_WithinAMonth: String { return self._s[1951]! } + public var VoiceChat_Live: String { return self._s[1952]! } + public var PeerInfo_ButtonCall: String { return self._s[1953]! } + public var SettingsSearch_Synonyms_Appearance_Title: String { return self._s[1954]! } + public var Group_Username_InvalidStartsWithNumber: String { return self._s[1955]! } + public var Call_AudioRouteHide: String { return self._s[1956]! } + public var DialogList_SavedMessages: String { return self._s[1957]! } + public var ChatList_Context_Mute: String { return self._s[1958]! } + public var Conversation_StatusKickedFromChannel: String { return self._s[1959]! } public func Notification_Exceptions_MutedUntil(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1877]!, self._r[1877]!, [_0]) + return formatWithArgumentRanges(self._s[1960]!, self._r[1960]!, [_0]) } - public var VoiceChat_StatusMutedForYou: String { return self._s[1878]! } - public var Passport_Language_et: String { return self._s[1879]! } - public var Conversation_MessageLeaveCommentShort: String { return self._s[1880]! } - public var PhotoEditor_CropReset: String { return self._s[1881]! } - public var Privacy_GroupsAndChannels_AlwaysAllow: String { return self._s[1882]! } - public var SocksProxySetup_HostnamePlaceholder: String { return self._s[1883]! } - public var CreateGroup_ErrorLocatedGroupsTooMuch: String { return self._s[1884]! } - public var WallpaperSearch_ColorWhite: String { return self._s[1887]! } - public var Channel_AdminLog_CanEditMessages: String { return self._s[1889]! } - public var Privacy_PaymentsClearInfoDoneHelp: String { return self._s[1890]! } - public var Channel_Username_InvalidStartsWithNumber: String { return self._s[1892]! } - public var CheckoutInfo_ReceiverInfoName: String { return self._s[1894]! } - public var Map_YouAreHere: String { return self._s[1896]! } - public var Core_ServiceUserStatus: String { return self._s[1897]! } - public var Channel_Setup_TypePrivateHelp: String { return self._s[1900]! } - public var VoiceChat_StartRecording: String { return self._s[1901]! } - public var SettingsSearch_Synonyms_Notifications_BadgeCountUnreadMessages: String { return self._s[1902]! } - public var MediaPicker_Videos: String { return self._s[1904]! } - public var Map_LiveLocationFor15Minutes: String { return self._s[1906]! } - public var Passport_Identity_TranslationsHelp: String { return self._s[1907]! } - public var SharedMedia_CategoryMedia: String { return self._s[1908]! } + public var VoiceChat_StatusMutedForYou: String { return self._s[1961]! } + public var Passport_Language_et: String { return self._s[1962]! } + public var Conversation_MessageLeaveCommentShort: String { return self._s[1963]! } + public var PhotoEditor_CropReset: String { return self._s[1964]! } + public var Privacy_GroupsAndChannels_AlwaysAllow: String { return self._s[1965]! } + public var SocksProxySetup_HostnamePlaceholder: String { return self._s[1966]! } + public var CreateGroup_ErrorLocatedGroupsTooMuch: String { return self._s[1967]! } + public var WallpaperSearch_ColorWhite: String { return self._s[1970]! } + public var Channel_AdminLog_CanEditMessages: String { return self._s[1972]! } + public var Privacy_PaymentsClearInfoDoneHelp: String { return self._s[1973]! } + public var Channel_Username_InvalidStartsWithNumber: String { return self._s[1975]! } + public var CheckoutInfo_ReceiverInfoName: String { return self._s[1977]! } + public var Map_YouAreHere: String { return self._s[1979]! } + public var Core_ServiceUserStatus: String { return self._s[1980]! } + public var Channel_Setup_TypePrivateHelp: String { return self._s[1983]! } + public var VoiceChat_StartRecording: String { return self._s[1984]! } + public var SettingsSearch_Synonyms_Notifications_BadgeCountUnreadMessages: String { return self._s[1985]! } + public var MediaPicker_Videos: String { return self._s[1987]! } + public var Map_LiveLocationFor15Minutes: String { return self._s[1989]! } + public var Passport_Identity_TranslationsHelp: String { return self._s[1990]! } + public var SharedMedia_CategoryMedia: String { return self._s[1991]! } public func MediaPicker_Nof(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1909]!, self._r[1909]!, [_0]) + return formatWithArgumentRanges(self._s[1992]!, self._r[1992]!, [_0]) } - public var ChatSettings_AutoPlayGifs: String { return self._s[1910]! } - public var Passport_Identity_CountryPlaceholder: String { return self._s[1911]! } - public var Bot_GroupStatusDoesNotReadHistory: String { return self._s[1912]! } - public var Conversation_JoinVoiceChatAsListener: String { return self._s[1913]! } - public var Notification_Exceptions_RemoveFromExceptions: String { return self._s[1914]! } + public var ChatSettings_AutoPlayGifs: String { return self._s[1993]! } + public var Passport_Identity_CountryPlaceholder: String { return self._s[1994]! } + public var Bot_GroupStatusDoesNotReadHistory: String { return self._s[1995]! } + public var Conversation_JoinVoiceChatAsListener: String { return self._s[1996]! } + public var Notification_Exceptions_RemoveFromExceptions: String { return self._s[1997]! } public func Chat_SlowmodeTooltip(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1915]!, self._r[1915]!, [_0]) + return formatWithArgumentRanges(self._s[1998]!, self._r[1998]!, [_0]) } - public var Web_Error: String { return self._s[1916]! } - public var PhotoEditor_SkinTool: String { return self._s[1917]! } - public var ApplyLanguage_UnsufficientDataTitle: String { return self._s[1918]! } - public var AutoremoveSetup_TimerInfoChat: String { return self._s[1919]! } - public var ChatSettings_ConnectionType_UseSocks5: String { return self._s[1921]! } - public var PasscodeSettings_Help: String { return self._s[1922]! } - public var Appearance_ColorTheme: String { return self._s[1923]! } + public var Web_Error: String { return self._s[1999]! } + public var PhotoEditor_SkinTool: String { return self._s[2000]! } + public var ApplyLanguage_UnsufficientDataTitle: String { return self._s[2001]! } + public var AutoremoveSetup_TimerInfoChat: String { return self._s[2002]! } + public var ChatSettings_ConnectionType_UseSocks5: String { return self._s[2004]! } + public var PasscodeSettings_Help: String { return self._s[2005]! } + public var Appearance_ColorTheme: String { return self._s[2006]! } public func Channel_AdminLog_MessageRestrictedNewSetting(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1924]!, self._r[1924]!, [_0]) + return formatWithArgumentRanges(self._s[2007]!, self._r[2007]!, [_0]) } - public var InviteLink_DeleteAllRevokedLinks: String { return self._s[1925]! } + public var InviteLink_DeleteAllRevokedLinks: String { return self._s[2008]! } public func PUSH_PINNED_GEO(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1926]!, self._r[1926]!, [_1]) + return formatWithArgumentRanges(self._s[2009]!, self._r[2009]!, [_1]) } - public var InviteLink_QRCode_Title: String { return self._s[1927]! } - public var GroupInfo_LeftStatus: String { return self._s[1928]! } - public var EditTheme_Preview: String { return self._s[1929]! } - public var Watch_Suggestion_WhatsUp: String { return self._s[1930]! } + public var InviteLink_QRCode_Title: String { return self._s[2010]! } + public var GroupInfo_LeftStatus: String { return self._s[2011]! } + public var EditTheme_Preview: String { return self._s[2012]! } + public var Watch_Suggestion_WhatsUp: String { return self._s[2013]! } public func AutoDownloadSettings_PreloadVideoInfo(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1931]!, self._r[1931]!, [_0]) + return formatWithArgumentRanges(self._s[2014]!, self._r[2014]!, [_0]) } - public var NotificationsSound_Keys: String { return self._s[1932]! } - public var VoiceChat_StatusWantsToSpeak: String { return self._s[1933]! } - public var PasscodeSettings_UnlockWithTouchId: String { return self._s[1934]! } - public var ChatList_Context_MarkAsUnread: String { return self._s[1935]! } - public var DialogList_AdNoticeAlert: String { return self._s[1936]! } - public var UserInfo_Invite: String { return self._s[1937]! } - public var Checkout_Email: String { return self._s[1938]! } - public var Stats_GroupActionsTitle: String { return self._s[1939]! } - public var Coub_TapForSound: String { return self._s[1940]! } - public var Conversation_AutoremoveTimerRemovedUserYou: String { return self._s[1941]! } - public var Theme_ThemeChangedText: String { return self._s[1942]! } - public var Call_ExternalCallInProgressMessage: String { return self._s[1943]! } - public var AutoremoveSetup_TimerInfoChannel: String { return self._s[1944]! } - public var Settings_ApplyProxyAlertEnable: String { return self._s[1945]! } - public var ScheduledMessages_ScheduledToday: String { return self._s[1946]! } - public var Channel_AdminLog_DefaultRestrictionsUpdated: String { return self._s[1947]! } + public var NotificationsSound_Keys: String { return self._s[2015]! } + public var VoiceChat_StatusWantsToSpeak: String { return self._s[2016]! } + public var PasscodeSettings_UnlockWithTouchId: String { return self._s[2017]! } + public var ChatList_Context_MarkAsUnread: String { return self._s[2018]! } + public var DialogList_AdNoticeAlert: String { return self._s[2019]! } + public var UserInfo_Invite: String { return self._s[2020]! } + public var Checkout_Email: String { return self._s[2021]! } + public var Stats_GroupActionsTitle: String { return self._s[2022]! } + public var Coub_TapForSound: String { return self._s[2023]! } + public var Conversation_AutoremoveTimerRemovedUserYou: String { return self._s[2024]! } + public var Theme_ThemeChangedText: String { return self._s[2025]! } + public var Call_ExternalCallInProgressMessage: String { return self._s[2026]! } + public var AutoremoveSetup_TimerInfoChannel: String { return self._s[2027]! } + public var Settings_ApplyProxyAlertEnable: String { return self._s[2028]! } + public var ScheduledMessages_ScheduledToday: String { return self._s[2029]! } + public var Channel_AdminLog_DefaultRestrictionsUpdated: String { return self._s[2030]! } public func VoiceChat_InviteMemberToChannelFirstText(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1948]!, self._r[1948]!, [_1, _2]) + return formatWithArgumentRanges(self._s[2031]!, self._r[2031]!, [_1, _2]) } - public var Call_ReportIncludeLogDescription: String { return self._s[1949]! } - public var Settings_FrequentlyAskedQuestions: String { return self._s[1951]! } - public var Call_VoiceOver_VoiceCallMissed: String { return self._s[1952]! } - public var Channel_MessagePhotoRemoved: String { return self._s[1953]! } - public var Passport_Email_Delete: String { return self._s[1954]! } + public var Call_ReportIncludeLogDescription: String { return self._s[2032]! } + public var Settings_FrequentlyAskedQuestions: String { return self._s[2034]! } + public var Call_VoiceOver_VoiceCallMissed: String { return self._s[2035]! } + public var Channel_MessagePhotoRemoved: String { return self._s[2036]! } + public var Passport_Email_Delete: String { return self._s[2037]! } public func PUSH_PINNED_PHOTO(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1955]!, self._r[1955]!, [_1]) + return formatWithArgumentRanges(self._s[2038]!, self._r[2038]!, [_1]) } - public var NotificationSettings_ShowNotificationsAllAccountsInfoOn: String { return self._s[1956]! } + public var NotificationSettings_ShowNotificationsAllAccountsInfoOn: String { return self._s[2039]! } public func Conversation_AutoremoveTimerRemovedUser(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1957]!, self._r[1957]!, [_1]) + return formatWithArgumentRanges(self._s[2040]!, self._r[2040]!, [_1]) } - public var Channel_AdminLog_CanAddAdmins: String { return self._s[1958]! } - public var SocksProxySetup_FailedToConnect: String { return self._s[1960]! } - public var SettingsSearch_Synonyms_Data_NetworkUsage: String { return self._s[1961]! } - public var Common_of: String { return self._s[1962]! } - public var VoiceChat_CreateNewVoiceChatText: String { return self._s[1963]! } - public var VoiceChat_StartRecordingStart: String { return self._s[1964]! } - public var PeerInfo_ButtonUnmute: String { return self._s[1967]! } + public var Channel_AdminLog_CanAddAdmins: String { return self._s[2041]! } + public var SocksProxySetup_FailedToConnect: String { return self._s[2043]! } + public var SettingsSearch_Synonyms_Data_NetworkUsage: String { return self._s[2044]! } + public var Common_of: String { return self._s[2045]! } + public var VoiceChat_CreateNewVoiceChatText: String { return self._s[2046]! } + public var VoiceChat_StartRecordingStart: String { return self._s[2047]! } + public var PeerInfo_ButtonUnmute: String { return self._s[2050]! } public func ChatSettings_AutoDownloadSettings_TypeFile(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[1968]!, self._r[1968]!, [_0]) + return formatWithArgumentRanges(self._s[2051]!, self._r[2051]!, [_0]) } - public var ChatList_AddChatsToFolder: String { return self._s[1969]! } - public var Login_ResetAccountProtected_LimitExceeded: String { return self._s[1970]! } - public var Settings_Title: String { return self._s[1972]! } - public var AutoDownloadSettings_Contacts: String { return self._s[1974]! } - public var Appearance_BubbleCornersSetting: String { return self._s[1975]! } - public var InviteLink_OtherAdminsLinks: String { return self._s[1976]! } - public var Privacy_Calls_AlwaysAllow: String { return self._s[1977]! } - public var Privacy_Forwards_AlwaysAllow_Title: String { return self._s[1979]! } - public var WallpaperPreview_CropBottomText: String { return self._s[1980]! } - public var SecretTimer_VideoDescription: String { return self._s[1981]! } - public var VoiceOver_Chat_AnimatedSticker: String { return self._s[1982]! } - public var WallpaperPreview_Blurred: String { return self._s[1983]! } - public var SettingsSearch_Synonyms_Notifications_GroupNotificationsExceptions: String { return self._s[1984]! } - public var ChatListFolder_ExcludedSectionHeader: String { return self._s[1986]! } - public var Conversation_CancelForwardSelectChat: String { return self._s[1987]! } - public var DialogList_PasscodeLockHelp: String { return self._s[1988]! } - public var SocksProxySetup_SecretPlaceholder: String { return self._s[1989]! } - public var NetworkUsageSettings_CallDataSection: String { return self._s[1990]! } - public var TwoStepAuth_PasswordRemovePassportConfirmation: String { return self._s[1991]! } - public var Passport_FieldAddressTranslationHelp: String { return self._s[1992]! } - public var SocksProxySetup_Connection: String { return self._s[1993]! } - public var Passport_Address_TypePassportRegistration: String { return self._s[1994]! } - public var Contacts_PermissionsAllowInSettings: String { return self._s[1995]! } - public var Conversation_Unpin: String { return self._s[1996]! } - public var Notifications_MessageNotificationsExceptionsHelp: String { return self._s[1997]! } - public var TwoFactorSetup_Hint_Placeholder: String { return self._s[1998]! } - public var Call_ReportSkip: String { return self._s[1999]! } + public var Privacy_ContactsReset_ContactsDeleted: String { return self._s[2052]! } + public var ChatList_AddChatsToFolder: String { return self._s[2053]! } + public var Login_ResetAccountProtected_LimitExceeded: String { return self._s[2054]! } + public var Settings_Title: String { return self._s[2056]! } + public var AutoDownloadSettings_Contacts: String { return self._s[2058]! } + public var Appearance_BubbleCornersSetting: String { return self._s[2059]! } + public var InviteLink_OtherAdminsLinks: String { return self._s[2060]! } + public var Privacy_Calls_AlwaysAllow: String { return self._s[2061]! } + public var Privacy_Forwards_AlwaysAllow_Title: String { return self._s[2063]! } + public var WallpaperPreview_CropBottomText: String { return self._s[2064]! } + public var SecretTimer_VideoDescription: String { return self._s[2065]! } + public var VoiceOver_Chat_AnimatedSticker: String { return self._s[2066]! } + public var WallpaperPreview_Blurred: String { return self._s[2067]! } + public var SettingsSearch_Synonyms_Notifications_GroupNotificationsExceptions: String { return self._s[2068]! } + public var ChatListFolder_ExcludedSectionHeader: String { return self._s[2070]! } + public var Conversation_CancelForwardSelectChat: String { return self._s[2071]! } + public var DialogList_PasscodeLockHelp: String { return self._s[2072]! } + public var SocksProxySetup_SecretPlaceholder: String { return self._s[2073]! } + public var NetworkUsageSettings_CallDataSection: String { return self._s[2074]! } + public var TwoStepAuth_PasswordRemovePassportConfirmation: String { return self._s[2075]! } + public var Passport_FieldAddressTranslationHelp: String { return self._s[2076]! } + public var SocksProxySetup_Connection: String { return self._s[2077]! } + public var Passport_Address_TypePassportRegistration: String { return self._s[2078]! } + public var Contacts_PermissionsAllowInSettings: String { return self._s[2079]! } + public var Conversation_Unpin: String { return self._s[2080]! } + public var Notifications_MessageNotificationsExceptionsHelp: String { return self._s[2081]! } + public var TwoFactorSetup_Hint_Placeholder: String { return self._s[2082]! } + public var Call_ReportSkip: String { return self._s[2083]! } public func VoiceOver_Chat_PhotoFrom(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2000]!, self._r[2000]!, [_0]) + return formatWithArgumentRanges(self._s[2084]!, self._r[2084]!, [_0]) } public func VoiceOver_Chat_Caption(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2002]!, self._r[2002]!, [_0]) + return formatWithArgumentRanges(self._s[2086]!, self._r[2086]!, [_0]) } - public var AutoNightTheme_Automatic: String { return self._s[2003]! } - public var Passport_Language_az: String { return self._s[2005]! } + public var AutoNightTheme_Automatic: String { return self._s[2087]! } + public var Passport_Language_az: String { return self._s[2089]! } public func Conversation_AutoremoveChanged(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2006]!, self._r[2006]!, [_0]) + return formatWithArgumentRanges(self._s[2090]!, self._r[2090]!, [_0]) } - public var SettingsSearch_Synonyms_Data_Storage_ClearCache: String { return self._s[2007]! } - public var Watch_UserInfo_Unmute: String { return self._s[2008]! } - public var Channel_Stickers_YourStickers: String { return self._s[2009]! } - public var Channel_DiscussionGroup_UnlinkChannel: String { return self._s[2010]! } - public var PeerInfo_AutoremoveMessagesDisabled: String { return self._s[2011]! } - public var Tour_Text1: String { return self._s[2012]! } - public var Common_Delete: String { return self._s[2013]! } - public var Settings_EditPhoto: String { return self._s[2014]! } - public var Common_Edit: String { return self._s[2015]! } + public var SettingsSearch_Synonyms_Data_Storage_ClearCache: String { return self._s[2091]! } + public var Watch_UserInfo_Unmute: String { return self._s[2092]! } + public var Channel_Stickers_YourStickers: String { return self._s[2093]! } + public var Channel_DiscussionGroup_UnlinkChannel: String { return self._s[2094]! } + public var PeerInfo_AutoremoveMessagesDisabled: String { return self._s[2095]! } + public var Tour_Text1: String { return self._s[2096]! } + public var Common_Delete: String { return self._s[2097]! } + public var Settings_EditPhoto: String { return self._s[2098]! } + public var Common_Edit: String { return self._s[2099]! } + public var ShareMenu_ShareTo: String { return self._s[2101]! } + public var Passport_Identity_ExpiryDate: String { return self._s[2102]! } public func Channel_AdminLog_MutedNewMembers(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2017]!, self._r[2017]!, [_1]) + return formatWithArgumentRanges(self._s[2103]!, self._r[2103]!, [_1]) } - public var Passport_Identity_ExpiryDate: String { return self._s[2018]! } - public var ShareMenu_ShareTo: String { return self._s[2019]! } - public var Preview_DeleteGif: String { return self._s[2020]! } - public var WallpaperPreview_PatternPaternDiscard: String { return self._s[2021]! } - public var ChatSettings_AutoDownloadUsingCellular: String { return self._s[2022]! } - public var Conversation_ViewReply: String { return self._s[2023]! } - public var Stats_LoadingText: String { return self._s[2024]! } - public var Channel_EditAdmin_PermissinAddAdminOn: String { return self._s[2025]! } - public var CheckoutInfo_ReceiverInfoEmailPlaceholder: String { return self._s[2026]! } - public var Channel_AdminLog_CanChangeInfo: String { return self._s[2027]! } + public var Preview_DeleteGif: String { return self._s[2104]! } + public var WallpaperPreview_PatternPaternDiscard: String { return self._s[2105]! } + public var ChatSettings_AutoDownloadUsingCellular: String { return self._s[2106]! } + public var Conversation_ViewReply: String { return self._s[2107]! } + public var Stats_LoadingText: String { return self._s[2108]! } + public var Channel_EditAdmin_PermissinAddAdminOn: String { return self._s[2109]! } + public var CheckoutInfo_ReceiverInfoEmailPlaceholder: String { return self._s[2110]! } + public var Channel_AdminLog_CanChangeInfo: String { return self._s[2111]! } public func Passport_Phone_UseTelegramNumber(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2028]!, self._r[2028]!, [_0]) + return formatWithArgumentRanges(self._s[2112]!, self._r[2112]!, [_0]) } public func Time_MonthOfYear_m2(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2029]!, self._r[2029]!, [_0]) - } - public func VoiceOver_Chat_VideoMessageFrom(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2031]!, self._r[2031]!, [_0]) - } - public var Passport_Address_OneOfTypeRentalAgreement: String { return self._s[2032]! } - public var InviteLink_Share: String { return self._s[2034]! } - public func Conversation_ImportProgress(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2036]!, self._r[2036]!, [_0]) - } - public var IntentsSettings_MainAccount: String { return self._s[2037]! } - public var Group_MessagePhotoRemoved: String { return self._s[2040]! } - public var Conversation_ContextMenuSelect: String { return self._s[2041]! } - public var GroupInfo_Permissions_Exceptions: String { return self._s[2043]! } - public var GroupRemoved_UsersSectionTitle: String { return self._s[2044]! } - public var Contacts_PermissionsEnable: String { return self._s[2045]! } - public var Channel_EditAdmin_PermissionDeleteMessagesOfOthers: String { return self._s[2046]! } - public var Common_NotNow: String { return self._s[2047]! } - public var Notification_CreatedChannel: String { return self._s[2048]! } - public var Stats_ViewsBySourceTitle: String { return self._s[2050]! } - public var InviteLink_ContextShare: String { return self._s[2051]! } - public var Appearance_AppIconClassic: String { return self._s[2052]! } - public var PhotoEditor_QualityTool: String { return self._s[2053]! } - public var ClearCache_ClearCache: String { return self._s[2054]! } - public var TwoFactorSetup_Password_PlaceholderConfirmPassword: String { return self._s[2055]! } - public var AutoDownloadSettings_Videos: String { return self._s[2056]! } - public var GroupPermission_Duration: String { return self._s[2057]! } - public var ChatList_Read: String { return self._s[2058]! } - public func Group_OwnershipTransfer_DescriptionInfo(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2059]!, self._r[2059]!, [_1, _2]) - } - public var CallFeedback_Send: String { return self._s[2060]! } - public var Channel_Stickers_Searching: String { return self._s[2061]! } - public var ScheduledMessages_ReminderNotification: String { return self._s[2062]! } - public var FastTwoStepSetup_HintSection: String { return self._s[2063]! } - public var ChatSettings_AutoDownloadVideoMessages: String { return self._s[2064]! } - public var EditTheme_CreateTitle: String { return self._s[2065]! } - public var Application_Name: String { return self._s[2066]! } - public var Paint_Stickers: String { return self._s[2067]! } - public var Appearance_ThemePreview_Chat_1_Text: String { return self._s[2068]! } - public var Call_StatusFailed: String { return self._s[2069]! } - public var Stickers_FavoriteStickers: String { return self._s[2070]! } - public var ClearCache_Clear: String { return self._s[2071]! } - public var Passport_Language_mn: String { return self._s[2072]! } - public var WallpaperPreview_PreviewTopText: String { return self._s[2073]! } - public var LogoutOptions_ClearCacheTitle: String { return self._s[2074]! } - public var Call_VoiceOver_VideoCallOutgoing: String { return self._s[2076]! } - public var TwoFactorSetup_Hint_Text: String { return self._s[2078]! } - public var WallpaperPreview_PatternIntensity: String { return self._s[2079]! } - public var CheckoutInfo_ErrorShippingNotAvailable: String { return self._s[2080]! } - public var Passport_Address_AddBankStatement: String { return self._s[2081]! } - public func Conversation_TitleRepliesFormat(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2084]!, self._r[2084]!, [_1, _2]) - } - public var ChatListFolderSettings_RecommendedNewFolder: String { return self._s[2085]! } - public var UserInfo_ShareContact: String { return self._s[2086]! } - public var Passport_Identity_NamePlaceholder: String { return self._s[2087]! } - public var Channel_ErrorAdminsTooMuch: String { return self._s[2089]! } - public var Call_RateCall: String { return self._s[2090]! } - public var Contacts_AccessDeniedError: String { return self._s[2091]! } - public var Invite_ChannelsTooMuch: String { return self._s[2092]! } - public var CheckoutInfo_ShippingInfoPostcode: String { return self._s[2093]! } - public var Channel_BanUser_PermissionReadMessages: String { return self._s[2094]! } - public var InviteLink_Create_TimeLimitInfo: String { return self._s[2095]! } - public var Cache_NoLimit: String { return self._s[2097]! } - public var Conversation_EmptyPlaceholder: String { return self._s[2101]! } - public var Privacy_GroupsAndChannels_AlwaysAllow_Placeholder: String { return self._s[2102]! } - public var GroupRemoved_RemoveInfo: String { return self._s[2104]! } - public var Notification_Exceptions_MessagePreviewAlwaysOff: String { return self._s[2105]! } - public var Privacy_Calls_IntegrationHelp: String { return self._s[2106]! } - public func PUSH_VIDEO_CALL_MISSED(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2107]!, self._r[2107]!, [_1]) - } - public var VoiceOver_Media_PlaybackRateFast: String { return self._s[2108]! } - public var Theme_ThemeChanged: String { return self._s[2109]! } - public var Privacy_GroupsAndChannels_NeverAllow: String { return self._s[2111]! } - public var AutoDownloadSettings_MediaTypes: String { return self._s[2112]! } - public func Notification_PinnedDocumentMessage(_ _0: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(self._s[2113]!, self._r[2113]!, [_0]) } - public var Channel_AdminLog_InfoPanelTitle: String { return self._s[2114]! } - public var Passport_Language_da: String { return self._s[2116]! } - public var Chat_SlowmodeSendError: String { return self._s[2117]! } - public var Application_Update: String { return self._s[2119]! } - public var SocksProxySetup_SaveProxy: String { return self._s[2120]! } - public func PUSH_AUTH_REGION(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2121]!, self._r[2121]!, [_1, _2]) + public func VoiceOver_Chat_VideoMessageFrom(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[2115]!, self._r[2115]!, [_0]) } - public var Privacy_AddNewPeer: String { return self._s[2123]! } - public var Channel_DiscussionGroup_MakeHistoryPublicProceed: String { return self._s[2125]! } - public var Channel_Members_Title: String { return self._s[2126]! } - public var StickerPacks_ActionDelete: String { return self._s[2127]! } - public var Settings_LogoutConfirmationText: String { return self._s[2128]! } - public var Chat_UnsendMyMessages: String { return self._s[2129]! } - public var PeerInfo_ReportProfilePhoto: String { return self._s[2130]! } - public var Conversation_EditingMessageMediaEditCurrentVideo: String { return self._s[2132]! } - public var ChatListFilter_AddChatsTitle: String { return self._s[2133]! } - public var Passport_FloodError: String { return self._s[2134]! } - public var NotificationSettings_ContactJoinedInfo: String { return self._s[2135]! } - public var SettingsSearch_Synonyms_Privacy_Data_SecretChatLinkPreview: String { return self._s[2136]! } - public var CallSettings_TabIconDescription: String { return self._s[2137]! } - public var Group_Setup_HistoryHeader: String { return self._s[2139]! } - public func Channel_AdminLog_AllowedNewMembersToSpeak(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2140]!, self._r[2140]!, [_1]) + public var Passport_Address_OneOfTypeRentalAgreement: String { return self._s[2116]! } + public var InviteLink_Share: String { return self._s[2118]! } + public func Conversation_ImportProgress(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[2120]!, self._r[2120]!, [_0]) } - public var TwoStepAuth_EmailTitle: String { return self._s[2141]! } - public var GroupInfo_Permissions_Removed: String { return self._s[2142]! } - public var DialogList_ClearHistoryConfirmation: String { return self._s[2143]! } - public var Contacts_Title: String { return self._s[2145]! } - public func Notification_Invited(_ _0: String, _ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2146]!, self._r[2146]!, [_0, _1]) + public var IntentsSettings_MainAccount: String { return self._s[2121]! } + public var Group_MessagePhotoRemoved: String { return self._s[2124]! } + public var Conversation_ContextMenuSelect: String { return self._s[2125]! } + public var GroupInfo_Permissions_Exceptions: String { return self._s[2127]! } + public var GroupRemoved_UsersSectionTitle: String { return self._s[2128]! } + public var Contacts_PermissionsEnable: String { return self._s[2129]! } + public var Channel_EditAdmin_PermissionDeleteMessagesOfOthers: String { return self._s[2130]! } + public var Common_NotNow: String { return self._s[2131]! } + public var Notification_CreatedChannel: String { return self._s[2132]! } + public var Stats_ViewsBySourceTitle: String { return self._s[2134]! } + public var InviteLink_ContextShare: String { return self._s[2135]! } + public var Appearance_AppIconClassic: String { return self._s[2136]! } + public var PhotoEditor_QualityTool: String { return self._s[2137]! } + public var ClearCache_ClearCache: String { return self._s[2138]! } + public var TwoFactorSetup_Password_PlaceholderConfirmPassword: String { return self._s[2139]! } + public var AutoDownloadSettings_Videos: String { return self._s[2140]! } + public var GroupPermission_Duration: String { return self._s[2141]! } + public var ChatList_Read: String { return self._s[2142]! } + public func Group_OwnershipTransfer_DescriptionInfo(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[2143]!, self._r[2143]!, [_1, _2]) } - public var ChatList_PeerTypeBot: String { return self._s[2149]! } - public func Channel_AdminLog_SetSlowmode(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2150]!, self._r[2150]!, [_1, _2]) + public func ScheduleVoiceChat_ScheduleTomorrow(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[2144]!, self._r[2144]!, [_0]) } - public var Appearance_ThemePreview_Chat_6_Text: String { return self._s[2151]! } - public func Time_PreciseDate_m1(_ _1: String, _ _2: String, _ _3: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2152]!, self._r[2152]!, [_1, _2, _3]) + public var CallFeedback_Send: String { return self._s[2145]! } + public var Channel_Stickers_Searching: String { return self._s[2146]! } + public var ScheduledMessages_ReminderNotification: String { return self._s[2147]! } + public var FastTwoStepSetup_HintSection: String { return self._s[2148]! } + public var ChatSettings_AutoDownloadVideoMessages: String { return self._s[2149]! } + public var EditTheme_CreateTitle: String { return self._s[2151]! } + public var Application_Name: String { return self._s[2152]! } + public var Paint_Stickers: String { return self._s[2153]! } + public var Appearance_ThemePreview_Chat_1_Text: String { return self._s[2154]! } + public var Call_StatusFailed: String { return self._s[2155]! } + public var Stickers_FavoriteStickers: String { return self._s[2156]! } + public var ClearCache_Clear: String { return self._s[2157]! } + public var Passport_Language_mn: String { return self._s[2158]! } + public var WallpaperPreview_PreviewTopText: String { return self._s[2159]! } + public var LogoutOptions_ClearCacheTitle: String { return self._s[2160]! } + public var Call_VoiceOver_VideoCallOutgoing: String { return self._s[2162]! } + public var TwoFactorSetup_Hint_Text: String { return self._s[2164]! } + public var WallpaperPreview_PatternIntensity: String { return self._s[2165]! } + public var CheckoutInfo_ErrorShippingNotAvailable: String { return self._s[2166]! } + public var Passport_Address_AddBankStatement: String { return self._s[2167]! } + public func Conversation_TitleRepliesFormat(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[2170]!, self._r[2170]!, [_1, _2]) } - public var Camera_PhotoMode: String { return self._s[2154]! } - public func PUSH_MESSAGE_GAME_SCORE(_ _1: String, _ _2: String, _ _3: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2155]!, self._r[2155]!, [_1, _2, _3]) - } - public var ContactInfo_PhoneLabelPager: String { return self._s[2156]! } - public var SettingsSearch_Synonyms_FAQ: String { return self._s[2157]! } - public var Call_CallAgain: String { return self._s[2158]! } - public var TwoStepAuth_PasswordSet: String { return self._s[2159]! } - public func Channel_Management_RestrictedBy(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2160]!, self._r[2160]!, [_0]) - } - public var GroupInfo_InviteLink_RevokeAlert_Success: String { return self._s[2161]! } - public var ClearCache_FreeSpaceDescription: String { return self._s[2162]! } - public var Permissions_ContactsAllowInSettings_v0: String { return self._s[2163]! } - public var Group_LeaveGroup: String { return self._s[2164]! } - public var Channel_Setup_LinkTypePrivate: String { return self._s[2166]! } - public var GroupInfo_LabelAdmin: String { return self._s[2168]! } - public var CheckoutInfo_ErrorStateInvalid: String { return self._s[2170]! } - public var Notification_PassportValuePersonalDetails: String { return self._s[2171]! } - public func WebSearch_SearchNoResultsDescription(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2172]!, self._r[2172]!, [_0]) - } - public var Stats_GroupNewMembersBySourceTitle: String { return self._s[2173]! } - public var Appearance_Preview: String { return self._s[2174]! } - public var VoiceOver_Chat_Contact: String { return self._s[2175]! } - public var Passport_Language_th: String { return self._s[2176]! } - public var PhotoEditor_CropAspectRatioOriginal: String { return self._s[2178]! } - public var LastSeen_Offline: String { return self._s[2181]! } - public var Map_OpenInHereMaps: String { return self._s[2182]! } - public var SettingsSearch_Synonyms_Data_AutoplayVideos: String { return self._s[2183]! } - public var InviteLink_ContextEdit: String { return self._s[2185]! } - public var AutoDownloadSettings_Reset: String { return self._s[2186]! } - public var Conversation_SendMessage_SetReminder: String { return self._s[2187]! } - public var Channel_AdminLog_EmptyMessageText: String { return self._s[2188]! } - public func AddContact_StatusSuccess(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2189]!, self._r[2189]!, [_0]) - } - public func AuthCode_Alert(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2190]!, self._r[2190]!, [_0]) - } - public var Passport_Identity_EditDriversLicense: String { return self._s[2191]! } - public var ChatListFolder_NameNonMuted: String { return self._s[2192]! } - public var Username_Placeholder: String { return self._s[2193]! } - public func PUSH_ALBUM(_ _1: String) -> (String, [(Int, NSRange)]) { + public var ChatListFolderSettings_RecommendedNewFolder: String { return self._s[2171]! } + public var UserInfo_ShareContact: String { return self._s[2172]! } + public var Passport_Identity_NamePlaceholder: String { return self._s[2173]! } + public var Channel_ErrorAdminsTooMuch: String { return self._s[2175]! } + public var Call_RateCall: String { return self._s[2176]! } + public var Contacts_AccessDeniedError: String { return self._s[2177]! } + public var Invite_ChannelsTooMuch: String { return self._s[2178]! } + public var CheckoutInfo_ShippingInfoPostcode: String { return self._s[2179]! } + public var Channel_BanUser_PermissionReadMessages: String { return self._s[2180]! } + public var InviteLink_Create_TimeLimitInfo: String { return self._s[2181]! } + public var Cache_NoLimit: String { return self._s[2184]! } + public var Conversation_EmptyPlaceholder: String { return self._s[2185]! } + public var Privacy_GroupsAndChannels_AlwaysAllow_Placeholder: String { return self._s[2189]! } + public var Notification_Exceptions_MessagePreviewAlwaysOff: String { return self._s[2190]! } + public var GroupRemoved_RemoveInfo: String { return self._s[2191]! } + public var Privacy_PaymentsClear_AllInfoCleared: String { return self._s[2192]! } + public var Privacy_Calls_IntegrationHelp: String { return self._s[2193]! } + public func PUSH_VIDEO_CALL_MISSED(_ _1: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(self._s[2194]!, self._r[2194]!, [_1]) } - public var Passport_Language_it: String { return self._s[2195]! } - public var Checkout_NewCard_SaveInfo: String { return self._s[2196]! } - public func Channel_OwnershipTransfer_DescriptionInfo(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2197]!, self._r[2197]!, [_1, _2]) + public var VoiceOver_Media_PlaybackRateFast: String { return self._s[2195]! } + public var Theme_ThemeChanged: String { return self._s[2196]! } + public var Privacy_GroupsAndChannels_NeverAllow: String { return self._s[2198]! } + public var AutoDownloadSettings_MediaTypes: String { return self._s[2199]! } + public func Notification_PinnedDocumentMessage(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[2200]!, self._r[2200]!, [_0]) } - public var NotificationsSound_Pulse: String { return self._s[2198]! } - public var VoiceOver_DismissContextMenu: String { return self._s[2200]! } - public var MessagePoll_NoVotes: String { return self._s[2203]! } - public var Message_Wallpaper: String { return self._s[2204]! } - public var Conversation_JoinVoiceChat: String { return self._s[2205]! } - public var Appearance_Other: String { return self._s[2206]! } - public var Passport_Identity_NativeNameHelp: String { return self._s[2208]! } - public var Group_PublicLink_Placeholder: String { return self._s[2212]! } - public var Appearance_ThemePreview_ChatList_2_Text: String { return self._s[2213]! } - public var VoiceOver_Recording_StopAndPreview: String { return self._s[2214]! } - public var ChatListFolder_NameBots: String { return self._s[2215]! } - public var Conversation_StopPollConfirmation: String { return self._s[2216]! } - public var UserInfo_DeleteContact: String { return self._s[2217]! } - public func Time_MonthOfYear_m11(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2218]!, self._r[2218]!, [_0]) + public var Channel_AdminLog_InfoPanelTitle: String { return self._s[2201]! } + public var Passport_Language_da: String { return self._s[2203]! } + public var Chat_SlowmodeSendError: String { return self._s[2204]! } + public var Application_Update: String { return self._s[2206]! } + public var SocksProxySetup_SaveProxy: String { return self._s[2207]! } + public func PUSH_AUTH_REGION(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[2208]!, self._r[2208]!, [_1, _2]) } - public var Wallpaper_Wallpaper: String { return self._s[2220]! } - public func PUSH_MESSAGE_NOTEXT(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2221]!, self._r[2221]!, [_1]) + public var Privacy_AddNewPeer: String { return self._s[2210]! } + public var Channel_DiscussionGroup_MakeHistoryPublicProceed: String { return self._s[2212]! } + public var Channel_Members_Title: String { return self._s[2213]! } + public var StickerPacks_ActionDelete: String { return self._s[2214]! } + public var Conversation_ScheduledVoiceChat: String { return self._s[2215]! } + public var Settings_LogoutConfirmationText: String { return self._s[2217]! } + public var Chat_UnsendMyMessages: String { return self._s[2218]! } + public var PeerInfo_ReportProfilePhoto: String { return self._s[2219]! } + public var Conversation_EditingMessageMediaEditCurrentVideo: String { return self._s[2221]! } + public var ChatListFilter_AddChatsTitle: String { return self._s[2222]! } + public var Passport_FloodError: String { return self._s[2223]! } + public var NotificationSettings_ContactJoinedInfo: String { return self._s[2224]! } + public var SettingsSearch_Synonyms_Privacy_Data_SecretChatLinkPreview: String { return self._s[2225]! } + public var CallSettings_TabIconDescription: String { return self._s[2226]! } + public var Group_Setup_HistoryHeader: String { return self._s[2228]! } + public func Channel_AdminLog_AllowedNewMembersToSpeak(_ _1: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[2229]!, self._r[2229]!, [_1]) } - public var LoginPassword_ForgotPassword: String { return self._s[2222]! } - public var FeaturedStickerPacks_Title: String { return self._s[2223]! } - public var Paint_Pen: String { return self._s[2224]! } - public var Channel_AdminLogFilter_EventsInfo: String { return self._s[2225]! } - public var ChatListFolderSettings_Info: String { return self._s[2226]! } - public var FastTwoStepSetup_HintPlaceholder: String { return self._s[2227]! } - public var PhotoEditor_CurvesAll: String { return self._s[2229]! } - public func Time_PreciseDate_m12(_ _1: String, _ _2: String, _ _3: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2231]!, self._r[2231]!, [_1, _2, _3]) + public var TwoStepAuth_EmailTitle: String { return self._s[2230]! } + public var GroupInfo_Permissions_Removed: String { return self._s[2231]! } + public var DialogList_ClearHistoryConfirmation: String { return self._s[2232]! } + public var Contacts_Title: String { return self._s[2234]! } + public func Notification_Invited(_ _0: String, _ _1: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[2235]!, self._r[2235]!, [_0, _1]) } - public var Passport_Address_TypeRentalAgreement: String { return self._s[2233]! } - public var Message_ImageExpired: String { return self._s[2234]! } - public var Call_ConnectionErrorMessage: String { return self._s[2235]! } - public var SearchImages_NoImagesFound: String { return self._s[2237]! } - public var PeerInfo_PaneGifs: String { return self._s[2238]! } - public var Passport_DeletePersonalDetailsConfirmation: String { return self._s[2239]! } - public var EnterPasscode_RepeatNewPasscode: String { return self._s[2240]! } - public var PhotoEditor_VignetteTool: String { return self._s[2241]! } - public var Passport_Language_dz: String { return self._s[2242]! } - public var Notifications_ChannelNotificationsHelp: String { return self._s[2243]! } - public var Conversation_BlockUser: String { return self._s[2244]! } - public var GroupPermission_PermissionDisabledByDefault: String { return self._s[2247]! } - public var Group_OwnershipTransfer_ErrorAdminsTooMuch: String { return self._s[2249]! } - public func Time_MonthOfYear_m8(_ _0: String) -> (String, [(Int, NSRange)]) { + public var ChatList_PeerTypeBot: String { return self._s[2238]! } + public func Channel_AdminLog_SetSlowmode(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[2239]!, self._r[2239]!, [_1, _2]) + } + public var Appearance_ThemePreview_Chat_6_Text: String { return self._s[2240]! } + public func Time_PreciseDate_m1(_ _1: String, _ _2: String, _ _3: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[2241]!, self._r[2241]!, [_1, _2, _3]) + } + public var Camera_PhotoMode: String { return self._s[2243]! } + public func PUSH_MESSAGE_GAME_SCORE(_ _1: String, _ _2: String, _ _3: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[2244]!, self._r[2244]!, [_1, _2, _3]) + } + public var ContactInfo_PhoneLabelPager: String { return self._s[2245]! } + public var SettingsSearch_Synonyms_FAQ: String { return self._s[2246]! } + public var Call_CallAgain: String { return self._s[2247]! } + public var TwoStepAuth_PasswordSet: String { return self._s[2248]! } + public var VoiceChat_EditDescriptionPlaceholder: String { return self._s[2249]! } + public func Channel_Management_RestrictedBy(_ _0: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(self._s[2250]!, self._r[2250]!, [_0]) } - public var KeyCommand_NewMessage: String { return self._s[2251]! } - public var EditTheme_Edit_Preview_IncomingReplyText: String { return self._s[2254]! } + public var GroupInfo_InviteLink_RevokeAlert_Success: String { return self._s[2251]! } + public var ClearCache_FreeSpaceDescription: String { return self._s[2252]! } + public var Permissions_ContactsAllowInSettings_v0: String { return self._s[2253]! } + public var Group_LeaveGroup: String { return self._s[2254]! } + public var Channel_Setup_LinkTypePrivate: String { return self._s[2256]! } + public var GroupInfo_LabelAdmin: String { return self._s[2258]! } + public var CheckoutInfo_ErrorStateInvalid: String { return self._s[2260]! } + public var Notification_PassportValuePersonalDetails: String { return self._s[2261]! } + public func WebSearch_SearchNoResultsDescription(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[2262]!, self._r[2262]!, [_0]) + } + public var Stats_GroupNewMembersBySourceTitle: String { return self._s[2263]! } + public var Appearance_Preview: String { return self._s[2264]! } + public var VoiceOver_Chat_Contact: String { return self._s[2265]! } + public var Passport_Language_th: String { return self._s[2266]! } + public var PhotoEditor_CropAspectRatioOriginal: String { return self._s[2268]! } + public var LastSeen_Offline: String { return self._s[2271]! } + public var Map_OpenInHereMaps: String { return self._s[2272]! } + public var SettingsSearch_Synonyms_Data_AutoplayVideos: String { return self._s[2273]! } + public var InviteLink_ContextEdit: String { return self._s[2275]! } + public var AutoDownloadSettings_Reset: String { return self._s[2276]! } + public var Conversation_SendMessage_SetReminder: String { return self._s[2277]! } + public var Channel_AdminLog_EmptyMessageText: String { return self._s[2278]! } + public func AddContact_StatusSuccess(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[2279]!, self._r[2279]!, [_0]) + } + public func AuthCode_Alert(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[2280]!, self._r[2280]!, [_0]) + } + public var Passport_Identity_EditDriversLicense: String { return self._s[2281]! } + public var ChatListFolder_NameNonMuted: String { return self._s[2282]! } + public var Username_Placeholder: String { return self._s[2283]! } + public func PUSH_ALBUM(_ _1: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[2284]!, self._r[2284]!, [_1]) + } + public var Passport_Language_it: String { return self._s[2285]! } + public var Checkout_NewCard_SaveInfo: String { return self._s[2286]! } + public func Channel_OwnershipTransfer_DescriptionInfo(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[2287]!, self._r[2287]!, [_1, _2]) + } + public var NotificationsSound_Pulse: String { return self._s[2288]! } + public var VoiceOver_DismissContextMenu: String { return self._s[2290]! } + public var MessagePoll_NoVotes: String { return self._s[2293]! } + public var Message_Wallpaper: String { return self._s[2294]! } + public var Conversation_JoinVoiceChat: String { return self._s[2295]! } + public var Appearance_Other: String { return self._s[2296]! } + public var Passport_Identity_NativeNameHelp: String { return self._s[2298]! } + public var Group_PublicLink_Placeholder: String { return self._s[2302]! } + public var Appearance_ThemePreview_ChatList_2_Text: String { return self._s[2303]! } + public var VoiceOver_Recording_StopAndPreview: String { return self._s[2304]! } + public var ChatListFolder_NameBots: String { return self._s[2305]! } + public var Conversation_StopPollConfirmation: String { return self._s[2306]! } + public var UserInfo_DeleteContact: String { return self._s[2307]! } + public func Time_MonthOfYear_m11(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[2308]!, self._r[2308]!, [_0]) + } + public var Wallpaper_Wallpaper: String { return self._s[2310]! } + public func PUSH_MESSAGE_NOTEXT(_ _1: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[2311]!, self._r[2311]!, [_1]) + } + public var LoginPassword_ForgotPassword: String { return self._s[2312]! } + public var FeaturedStickerPacks_Title: String { return self._s[2313]! } + public var Paint_Pen: String { return self._s[2314]! } + public var Channel_AdminLogFilter_EventsInfo: String { return self._s[2315]! } + public var ChatListFolderSettings_Info: String { return self._s[2316]! } + public var FastTwoStepSetup_HintPlaceholder: String { return self._s[2317]! } + public var PhotoEditor_CurvesAll: String { return self._s[2319]! } + public func Time_PreciseDate_m12(_ _1: String, _ _2: String, _ _3: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[2321]!, self._r[2321]!, [_1, _2, _3]) + } + public var Passport_Address_TypeRentalAgreement: String { return self._s[2323]! } + public var Message_ImageExpired: String { return self._s[2324]! } + public var Call_ConnectionErrorMessage: String { return self._s[2325]! } + public var SearchImages_NoImagesFound: String { return self._s[2327]! } + public var PeerInfo_PaneGifs: String { return self._s[2328]! } + public var Passport_DeletePersonalDetailsConfirmation: String { return self._s[2329]! } + public var EnterPasscode_RepeatNewPasscode: String { return self._s[2330]! } + public var PhotoEditor_VignetteTool: String { return self._s[2331]! } + public var Passport_Language_dz: String { return self._s[2332]! } + public var Notifications_ChannelNotificationsHelp: String { return self._s[2333]! } + public var Conversation_BlockUser: String { return self._s[2334]! } + public var GroupPermission_PermissionDisabledByDefault: String { return self._s[2337]! } + public var TwoStepAuth_CancelResetText: String { return self._s[2339]! } + public var Group_OwnershipTransfer_ErrorAdminsTooMuch: String { return self._s[2340]! } + public func Time_MonthOfYear_m8(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[2341]!, self._r[2341]!, [_0]) + } + public var KeyCommand_NewMessage: String { return self._s[2342]! } + public var EditTheme_Edit_Preview_IncomingReplyText: String { return self._s[2345]! } public func PUSH_CHAT_MESSAGE_GEO(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2256]!, self._r[2256]!, [_1, _2]) + return formatWithArgumentRanges(self._s[2347]!, self._r[2347]!, [_1, _2]) } - public var ContactList_Context_StartSecretChat: String { return self._s[2257]! } - public var VoiceOver_Chat_File: String { return self._s[2258]! } - public var ChatList_EditFolder: String { return self._s[2260]! } - public var Appearance_BubbleCorners_Title: String { return self._s[2261]! } - public var PeerInfo_PaneAudio: String { return self._s[2262]! } - public var ChatListFolder_CategoryContacts: String { return self._s[2264]! } - public var VoiceOver_ScheduledMessages: String { return self._s[2265]! } + public var ContactList_Context_StartSecretChat: String { return self._s[2348]! } + public var VoiceOver_Chat_File: String { return self._s[2349]! } + public var ChatList_EditFolder: String { return self._s[2351]! } + public var Appearance_BubbleCorners_Title: String { return self._s[2352]! } + public var PeerInfo_PaneAudio: String { return self._s[2353]! } + public var ChatListFolder_CategoryContacts: String { return self._s[2355]! } + public var VoiceOver_ScheduledMessages: String { return self._s[2356]! } public func Login_InvalidPhoneEmailBody(_ _1: String, _ _2: String, _ _3: String, _ _4: String, _ _5: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2266]!, self._r[2266]!, [_1, _2, _3, _4, _5]) + return formatWithArgumentRanges(self._s[2357]!, self._r[2357]!, [_1, _2, _3, _4, _5]) } - public var ChatList_PeerTypeChannel: String { return self._s[2267]! } - public var VoiceOver_Navigation_Search: String { return self._s[2268]! } - public var Settings_Search: String { return self._s[2269]! } - public var WallpaperSearch_ColorYellow: String { return self._s[2270]! } - public var Login_PhoneBannedError: String { return self._s[2271]! } - public var KeyCommand_JumpToNextChat: String { return self._s[2272]! } - public var Passport_Language_fa: String { return self._s[2273]! } - public var Settings_About: String { return self._s[2274]! } - public var AutoDownloadSettings_MaxFileSize: String { return self._s[2275]! } - public var Channel_AdminLog_InfoPanelChannelAlertText: String { return self._s[2276]! } - public var AutoDownloadSettings_DataUsageHigh: String { return self._s[2277]! } + public var ChatList_PeerTypeChannel: String { return self._s[2358]! } + public var VoiceOver_Navigation_Search: String { return self._s[2359]! } + public var Settings_Search: String { return self._s[2360]! } + public var WallpaperSearch_ColorYellow: String { return self._s[2361]! } + public var Login_PhoneBannedError: String { return self._s[2362]! } + public var KeyCommand_JumpToNextChat: String { return self._s[2363]! } + public var Passport_Language_fa: String { return self._s[2364]! } + public var Settings_About: String { return self._s[2365]! } + public var AutoDownloadSettings_MaxFileSize: String { return self._s[2366]! } + public var Channel_AdminLog_InfoPanelChannelAlertText: String { return self._s[2367]! } + public var AutoDownloadSettings_DataUsageHigh: String { return self._s[2368]! } public func PUSH_CHAT_MESSAGE_TEXT(_ _1: String, _ _2: String, _ _3: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2278]!, self._r[2278]!, [_1, _2, _3]) + return formatWithArgumentRanges(self._s[2369]!, self._r[2369]!, [_1, _2, _3]) } - public var Common_OK: String { return self._s[2279]! } - public var Contacts_SortBy: String { return self._s[2280]! } - public var AutoNightTheme_PreferredTheme: String { return self._s[2281]! } + public var Common_OK: String { return self._s[2370]! } + public var Contacts_SortBy: String { return self._s[2371]! } + public var ImportStickerPack_LinkTaken: String { return self._s[2372]! } + public var AutoNightTheme_PreferredTheme: String { return self._s[2373]! } public func AutoDownloadSettings_OnFor(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2283]!, self._r[2283]!, [_0]) + return formatWithArgumentRanges(self._s[2375]!, self._r[2375]!, [_0]) } - public var CallFeedback_IncludeLogs: String { return self._s[2286]! } + public var CallFeedback_IncludeLogs: String { return self._s[2378]! } public func External_OpenIn(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2287]!, self._r[2287]!, [_0]) + return formatWithArgumentRanges(self._s[2379]!, self._r[2379]!, [_0]) } - public var Passcode_AppLockedAlert: String { return self._s[2289]! } - public var TwoStepAuth_SetupPasswordTitle: String { return self._s[2290]! } - public var Channel_NotificationLoading: String { return self._s[2292]! } - public var Passport_Identity_DocumentNumber: String { return self._s[2293]! } - public var VoiceOver_Chat_PagePreview: String { return self._s[2294]! } - public var VoiceOver_Chat_OpenHint: String { return self._s[2295]! } - public var Weekday_ShortFriday: String { return self._s[2296]! } - public var Conversation_TitleMute: String { return self._s[2297]! } - public var SettingsSearch_Synonyms_Notifications_GroupNotificationsSound: String { return self._s[2298]! } - public var ScheduledMessages_PollUnavailable: String { return self._s[2299]! } - public var DialogList_LanguageTooltip: String { return self._s[2301]! } - public var BroadcastGroups_IntroTitle: String { return self._s[2302]! } - public var Channel_AdminLogFilter_EventsPinned: String { return self._s[2303]! } + public var ImportStickerPack_ChooseLink: String { return self._s[2381]! } + public var Passcode_AppLockedAlert: String { return self._s[2382]! } + public var TwoStepAuth_SetupPasswordTitle: String { return self._s[2383]! } + public var Channel_NotificationLoading: String { return self._s[2385]! } + public var Passport_Identity_DocumentNumber: String { return self._s[2386]! } + public var VoiceOver_Chat_PagePreview: String { return self._s[2387]! } + public var VoiceOver_Chat_OpenHint: String { return self._s[2388]! } + public var Weekday_ShortFriday: String { return self._s[2389]! } + public var Conversation_TitleMute: String { return self._s[2390]! } + public var SettingsSearch_Synonyms_Notifications_GroupNotificationsSound: String { return self._s[2391]! } + public var ScheduledMessages_PollUnavailable: String { return self._s[2392]! } + public var DialogList_LanguageTooltip: String { return self._s[2394]! } + public var BroadcastGroups_IntroTitle: String { return self._s[2395]! } + public var Channel_AdminLogFilter_EventsPinned: String { return self._s[2396]! } public func DialogList_SingleUploadingVideoSuffix(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2304]!, self._r[2304]!, [_0]) + return formatWithArgumentRanges(self._s[2397]!, self._r[2397]!, [_0]) } - public var TwoStepAuth_SetupResendEmailCodeAlert: String { return self._s[2306]! } - public var Privacy_Calls_AlwaysAllow_Title: String { return self._s[2307]! } - public var Settings_EditVideo: String { return self._s[2308]! } - public var VoiceOver_Common_Off: String { return self._s[2309]! } - public var Stickers_FrequentlyUsed: String { return self._s[2310]! } - public var GroupPermission_Title: String { return self._s[2311]! } - public var AccessDenied_VideoMessageCamera: String { return self._s[2312]! } - public var Appearance_ThemeCarouselDay: String { return self._s[2313]! } + public var TwoStepAuth_SetupResendEmailCodeAlert: String { return self._s[2399]! } + public var Privacy_Calls_AlwaysAllow_Title: String { return self._s[2400]! } + public var Settings_EditVideo: String { return self._s[2401]! } + public var VoiceOver_Common_Off: String { return self._s[2402]! } + public var Stickers_FrequentlyUsed: String { return self._s[2403]! } + public var GroupPermission_Title: String { return self._s[2404]! } + public var AccessDenied_VideoMessageCamera: String { return self._s[2405]! } + public var Appearance_ThemeCarouselDay: String { return self._s[2406]! } public func PUSH_CHAT_MESSAGE_AUDIO(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2314]!, self._r[2314]!, [_1, _2]) + return formatWithArgumentRanges(self._s[2407]!, self._r[2407]!, [_1, _2]) } - public var Passport_Identity_DocumentNumberPlaceholder: String { return self._s[2315]! } - public var Tour_Title6: String { return self._s[2316]! } - public var EmptyGroupInfo_Title: String { return self._s[2317]! } + public var Passport_Identity_DocumentNumberPlaceholder: String { return self._s[2408]! } + public var Tour_Title6: String { return self._s[2409]! } + public var EmptyGroupInfo_Title: String { return self._s[2410]! } public func Channel_AdminLog_MessageToggleSignaturesOn(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2318]!, self._r[2318]!, [_0]) + return formatWithArgumentRanges(self._s[2411]!, self._r[2411]!, [_0]) } - public var Passport_Language_sk: String { return self._s[2319]! } - public var VoiceOver_Chat_YourAnonymousPoll: String { return self._s[2320]! } - public var Preview_SaveToCameraRoll: String { return self._s[2321]! } + public var Passport_Language_sk: String { return self._s[2412]! } + public var VoiceOver_Chat_YourAnonymousPoll: String { return self._s[2413]! } + public var TwoFactorRemember_WrongPassword: String { return self._s[2414]! } + public var Preview_SaveToCameraRoll: String { return self._s[2415]! } public func VoiceChat_YouCanNowSpeakIn(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2322]!, self._r[2322]!, [_0]) + return formatWithArgumentRanges(self._s[2416]!, self._r[2416]!, [_0]) } - public var LogoutOptions_SetPasscodeTitle: String { return self._s[2323]! } - public var Passport_Address_TypeUtilityBillUploadScan: String { return self._s[2324]! } - public var Conversation_ContextMenuMore: String { return self._s[2325]! } - public var Conversation_ForwardAuthorHiddenTooltip: String { return self._s[2326]! } - public var Channel_AdminLog_CanBeAnonymous: String { return self._s[2327]! } - public var CallFeedback_ReasonSilentLocal: String { return self._s[2329]! } + public var LogoutOptions_SetPasscodeTitle: String { return self._s[2417]! } + public var Passport_Address_TypeUtilityBillUploadScan: String { return self._s[2418]! } + public var Conversation_ContextMenuMore: String { return self._s[2419]! } + public var Conversation_ForwardAuthorHiddenTooltip: String { return self._s[2420]! } + public var Channel_AdminLog_CanBeAnonymous: String { return self._s[2421]! } + public var CallFeedback_ReasonSilentLocal: String { return self._s[2423]! } public func Channel_AdminLog_UnmutedMutedParticipant(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2330]!, self._r[2330]!, [_1, _2]) + return formatWithArgumentRanges(self._s[2424]!, self._r[2424]!, [_1, _2]) } - public var UserInfo_NotificationsDisable: String { return self._s[2331]! } + public var UserInfo_NotificationsDisable: String { return self._s[2425]! } public func Channel_AdminLog_EmptyFilterQueryText(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2333]!, self._r[2333]!, [_0]) + return formatWithArgumentRanges(self._s[2427]!, self._r[2427]!, [_0]) } - public var SettingsSearch_Synonyms_EditProfile_Bio: String { return self._s[2334]! } + public var SettingsSearch_Synonyms_EditProfile_Bio: String { return self._s[2428]! } public func Date_ChatDateHeader(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2336]!, self._r[2336]!, [_1, _2]) + return formatWithArgumentRanges(self._s[2430]!, self._r[2430]!, [_1, _2]) } - public var WallpaperSearch_ColorPrefix: String { return self._s[2337]! } + public var WallpaperSearch_ColorPrefix: String { return self._s[2431]! } public func Message_ForwardedPsa_covid(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2338]!, self._r[2338]!, [_0]) + return formatWithArgumentRanges(self._s[2432]!, self._r[2432]!, [_0]) } - public var Conversation_RestrictedMedia: String { return self._s[2340]! } - public var Group_MessageVideoUpdated: String { return self._s[2341]! } - public var NetworkUsageSettings_ResetStatsConfirmation: String { return self._s[2342]! } - public var GroupInfo_DeleteAndExit: String { return self._s[2343]! } - public var TwoFactorSetup_Email_Action: String { return self._s[2344]! } - public var Media_ShareThisVideo: String { return self._s[2346]! } - public var DialogList_Replies: String { return self._s[2348]! } + public var VoiceChat_NoiseSuppressionDisabled: String { return self._s[2434]! } + public var Conversation_RestrictedMedia: String { return self._s[2435]! } + public var Group_MessageVideoUpdated: String { return self._s[2436]! } + public var NetworkUsageSettings_ResetStatsConfirmation: String { return self._s[2437]! } + public var GroupInfo_DeleteAndExit: String { return self._s[2438]! } + public var TwoFactorSetup_Email_Action: String { return self._s[2439]! } + public var TwoFactorSetup_ResetDone_TitleNoPassword: String { return self._s[2440]! } + public var Media_ShareThisVideo: String { return self._s[2442]! } + public var DialogList_Replies: String { return self._s[2444]! } public func Conversation_Moderate_DeleteAllMessages(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2349]!, self._r[2349]!, [_0]) + return formatWithArgumentRanges(self._s[2445]!, self._r[2445]!, [_0]) } - public var CheckoutInfo_ShippingInfoAddress1: String { return self._s[2350]! } - public var Watch_Suggestion_OnMyWay: String { return self._s[2351]! } - public var CheckoutInfo_ShippingInfoAddress2: String { return self._s[2352]! } + public var CheckoutInfo_ShippingInfoAddress1: String { return self._s[2446]! } + public var Watch_Suggestion_OnMyWay: String { return self._s[2447]! } + public var ImportStickerPack_ImportingStickers: String { return self._s[2448]! } + public var CheckoutInfo_ShippingInfoAddress2: String { return self._s[2449]! } public func PUSH_PINNED_POLL(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2353]!, self._r[2353]!, [_1, _2]) + return formatWithArgumentRanges(self._s[2450]!, self._r[2450]!, [_1, _2]) } public func GroupInfo_InvitationLinkAcceptChannel(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2354]!, self._r[2354]!, [_0]) - } - public var Channel_EditAdmin_PermissinAddAdminOff: String { return self._s[2355]! } - public var ChatAdmins_AllMembersAreAdminsOnHelp: String { return self._s[2356]! } - public var ChatList_Search_NoResultsFitlerMedia: String { return self._s[2357]! } - public var Channel_Members_InviteLink: String { return self._s[2358]! } - public var Conversation_TapAndHoldToRecord: String { return self._s[2359]! } - public var WatchRemote_AlertText: String { return self._s[2360]! } - public func Channel_DiscussionGroup_PrivateChannelLink(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2361]!, self._r[2361]!, [_1, _2]) - } - public var Conversation_Pin: String { return self._s[2362]! } - public var InfoPlist_NSMicrophoneUsageDescription: String { return self._s[2363]! } - public var Stickers_RemoveFromFavorites: String { return self._s[2364]! } - public var Conversation_CancelForwardTitle: String { return self._s[2365]! } - public func Notification_PinnedPollMessage(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2366]!, self._r[2366]!, [_0]) - } - public var Appearance_AppIconFilled: String { return self._s[2367]! } - public var StickerPack_ErrorNotFound: String { return self._s[2368]! } - public func Channel_AdminLog_MessageRestrictedName(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2369]!, self._r[2369]!, [_1]) - } - public var Passport_Identity_AddIdentityCard: String { return self._s[2370]! } - public func PUSH_CHANNEL_MESSAGE_DOC(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2372]!, self._r[2372]!, [_1]) - } - public var Call_Camera: String { return self._s[2373]! } - public var GroupInfo_InviteLink_RevokeAlert_Text: String { return self._s[2374]! } - public var Group_Location_Info: String { return self._s[2375]! } - public var Watch_LastSeen_WithinAMonth: String { return self._s[2376]! } - public var UserInfo_NotificationsDefaultEnabled: String { return self._s[2377]! } - public func DialogList_PinLimitError(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2378]!, self._r[2378]!, [_0]) - } - public var Weekday_Yesterday: String { return self._s[2379]! } - public var TwoStepAuth_SetupPasswordEnterPasswordNew: String { return self._s[2380]! } - public var InviteLink_Create_UsersLimit: String { return self._s[2381]! } - public var ArchivedPacksAlert_Title: String { return self._s[2382]! } - public var PeerInfo_PaneMembers: String { return self._s[2383]! } - public var PhotoEditor_SelectCoverFrame: String { return self._s[2384]! } - public func Location_ProximityAlertSetTextGroup(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2385]!, self._r[2385]!, [_0]) - } - public var ContactInfo_PhoneLabelMain: String { return self._s[2386]! } - public func Time_PreciseDate_m7(_ _1: String, _ _2: String, _ _3: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2387]!, self._r[2387]!, [_1, _2, _3]) - } - public var TwoFactorSetup_EmailVerification_ChangeAction: String { return self._s[2388]! } - public var Channel_DiscussionGroup: String { return self._s[2389]! } - public var EditTheme_Edit_Preview_IncomingReplyName: String { return self._s[2390]! } - public var InviteLink_Create_TimeLimit: String { return self._s[2392]! } - public var Channel_EditAdmin_PermissionsHeader: String { return self._s[2393]! } - public var VoiceOver_MessageContextForward: String { return self._s[2394]! } - public var SocksProxySetup_TypeNone: String { return self._s[2395]! } - public var CreatePoll_MultipleChoiceQuizAlert: String { return self._s[2397]! } - public var ProfilePhoto_OpenInEditor: String { return self._s[2399]! } - public var WallpaperSearch_ColorPurple: String { return self._s[2400]! } - public var ChatListFolder_IncludeChatsTitle: String { return self._s[2401]! } - public var Group_Username_InvalidTooShort: String { return self._s[2402]! } - public var Location_ProximityNotification_DistanceM: String { return self._s[2403]! } - public var VoiceChat_EditTitleText: String { return self._s[2404]! } - public func Login_EmailPhoneBody(_ _0: String, _ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2405]!, self._r[2405]!, [_0, _1, _2]) - } - public var Passport_Language_tk: String { return self._s[2406]! } - public var ConvertToSupergroup_Title: String { return self._s[2407]! } - public var Channel_BanUser_PermissionEmbedLinks: String { return self._s[2408]! } - public var Cache_KeepMediaHelp: String { return self._s[2409]! } - public var Channel_Management_Title: String { return self._s[2410]! } - public func PUSH_MESSAGE_PHOTO_SECRET(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2411]!, self._r[2411]!, [_1]) - } - public var Conversation_ForwardChats: String { return self._s[2412]! } - public var Passport_Language_bg: String { return self._s[2413]! } - public var SocksProxySetup_TypeSocks: String { return self._s[2414]! } - public var Permissions_PrivacyPolicy: String { return self._s[2415]! } - public var VoiceOver_Chat_YourMusic: String { return self._s[2416]! } - public var SettingsSearch_Synonyms_Notifications_ResetAllNotifications: String { return self._s[2417]! } - public var Conversation_EmptyGifPanelPlaceholder: String { return self._s[2418]! } - public var Conversation_ContextMenuOpenChannel: String { return self._s[2419]! } - public var Report_AdditionalDetailsPlaceholder: String { return self._s[2420]! } - public var Activity_UploadingVideo: String { return self._s[2421]! } - public var PrivacyPolicy_AgeVerificationAgree: String { return self._s[2423]! } - public var Widget_LongTapToEdit: String { return self._s[2424]! } - public var VoiceChat_InviteLink_Listener: String { return self._s[2426]! } - public var SocksProxySetup_Credentials: String { return self._s[2427]! } - public var Preview_SaveGif: String { return self._s[2428]! } - public var Cache_Photos: String { return self._s[2429]! } - public var Channel_AdminLogFilter_EventsCalls: String { return self._s[2430]! } - public var Conversation_ContextMenuCancelEditing: String { return self._s[2431]! } - public var Contacts_FailedToSendInvitesMessage: String { return self._s[2432]! } - public func VoiceChat_RemoveAndBanPeerConfirmation(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2433]!, self._r[2433]!, [_1, _2]) - } - public var Passport_Language_lt: String { return self._s[2434]! } - public var Passport_DeleteDocument: String { return self._s[2436]! } - public var GroupInfo_SetGroupPhotoStop: String { return self._s[2437]! } - public func Location_ProximityNotification_NotifyLong(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2438]!, self._r[2438]!, [_1, _2]) - } - public var AccessDenied_VideoMessageMicrophone: String { return self._s[2439]! } - public func PeopleNearby_VisibleUntil(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2440]!, self._r[2440]!, [_0]) - } - public var AccessDenied_VideoCallCamera: String { return self._s[2441]! } - public func Channel_AdminLog_MessageDeleted(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2442]!, self._r[2442]!, [_0]) - } - public var PhotoEditor_SharpenTool: String { return self._s[2443]! } - public func PUSH_CHANNEL_MESSAGE_AUDIO(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2444]!, self._r[2444]!, [_1]) - } - public var DialogList_Unpin: String { return self._s[2445]! } - public var Stickers_NoStickersFound: String { return self._s[2446]! } - public var UserInfo_AddContact: String { return self._s[2448]! } - public func AddContact_SharedContactExceptionInfo(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2450]!, self._r[2450]!, [_0]) - } - public func Notification_PinnedLocationMessage(_ _0: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(self._s[2451]!, self._r[2451]!, [_0]) } - public var CallFeedback_VideoReasonDistorted: String { return self._s[2452]! } - public var Tour_Text2: String { return self._s[2453]! } + public var Channel_EditAdmin_PermissinAddAdminOff: String { return self._s[2452]! } + public var ChatAdmins_AllMembersAreAdminsOnHelp: String { return self._s[2453]! } + public var ChatList_Search_NoResultsFitlerMedia: String { return self._s[2454]! } + public var Channel_Members_InviteLink: String { return self._s[2455]! } + public var Conversation_TapAndHoldToRecord: String { return self._s[2456]! } + public var WatchRemote_AlertText: String { return self._s[2457]! } + public func Channel_DiscussionGroup_PrivateChannelLink(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[2458]!, self._r[2458]!, [_1, _2]) + } + public var Conversation_Pin: String { return self._s[2459]! } + public var InfoPlist_NSMicrophoneUsageDescription: String { return self._s[2460]! } + public var Stickers_RemoveFromFavorites: String { return self._s[2461]! } + public var Conversation_CancelForwardTitle: String { return self._s[2462]! } + public func Notification_PinnedPollMessage(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[2463]!, self._r[2463]!, [_0]) + } + public var Appearance_AppIconFilled: String { return self._s[2464]! } + public var StickerPack_ErrorNotFound: String { return self._s[2465]! } + public func Channel_AdminLog_MessageRestrictedName(_ _1: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[2466]!, self._r[2466]!, [_1]) + } + public var Passport_Identity_AddIdentityCard: String { return self._s[2467]! } + public func PUSH_CHANNEL_MESSAGE_DOC(_ _1: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[2469]!, self._r[2469]!, [_1]) + } + public var Call_Camera: String { return self._s[2470]! } + public var GroupInfo_InviteLink_RevokeAlert_Text: String { return self._s[2471]! } + public var Group_Location_Info: String { return self._s[2472]! } + public var Watch_LastSeen_WithinAMonth: String { return self._s[2473]! } + public var UserInfo_NotificationsDefaultEnabled: String { return self._s[2474]! } + public func DialogList_PinLimitError(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[2475]!, self._r[2475]!, [_0]) + } + public var Weekday_Yesterday: String { return self._s[2476]! } + public var TwoStepAuth_SetupPasswordEnterPasswordNew: String { return self._s[2477]! } + public var InviteLink_Create_UsersLimit: String { return self._s[2478]! } + public func Notification_VoiceChatScheduledTodayChannel(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[2479]!, self._r[2479]!, [_0]) + } + public var ArchivedPacksAlert_Title: String { return self._s[2480]! } + public var PeerInfo_PaneMembers: String { return self._s[2481]! } + public var PhotoEditor_SelectCoverFrame: String { return self._s[2482]! } + public func Location_ProximityAlertSetTextGroup(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[2483]!, self._r[2483]!, [_0]) + } + public var ContactInfo_PhoneLabelMain: String { return self._s[2484]! } + public func Time_PreciseDate_m7(_ _1: String, _ _2: String, _ _3: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[2485]!, self._r[2485]!, [_1, _2, _3]) + } + public var TwoFactorSetup_EmailVerification_ChangeAction: String { return self._s[2486]! } + public var Channel_DiscussionGroup: String { return self._s[2487]! } + public var EditTheme_Edit_Preview_IncomingReplyName: String { return self._s[2488]! } + public var InviteLink_Create_TimeLimit: String { return self._s[2490]! } + public var Channel_EditAdmin_PermissionsHeader: String { return self._s[2491]! } + public var VoiceOver_MessageContextForward: String { return self._s[2492]! } + public var SocksProxySetup_TypeNone: String { return self._s[2493]! } + public var CreatePoll_MultipleChoiceQuizAlert: String { return self._s[2495]! } + public var ProfilePhoto_OpenInEditor: String { return self._s[2497]! } + public var WallpaperSearch_ColorPurple: String { return self._s[2498]! } + public var ChatListFolder_IncludeChatsTitle: String { return self._s[2499]! } + public var Group_Username_InvalidTooShort: String { return self._s[2500]! } + public var Location_ProximityNotification_DistanceM: String { return self._s[2501]! } + public var VoiceChat_EditTitleText: String { return self._s[2502]! } + public func Login_EmailPhoneBody(_ _0: String, _ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[2503]!, self._r[2503]!, [_0, _1, _2]) + } + public var Passport_Language_tk: String { return self._s[2504]! } + public var ConvertToSupergroup_Title: String { return self._s[2505]! } + public var Channel_BanUser_PermissionEmbedLinks: String { return self._s[2506]! } + public var Cache_KeepMediaHelp: String { return self._s[2507]! } + public var Channel_Management_Title: String { return self._s[2508]! } + public func PUSH_MESSAGE_PHOTO_SECRET(_ _1: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[2509]!, self._r[2509]!, [_1]) + } + public var Conversation_ForwardChats: String { return self._s[2510]! } + public var Passport_Language_bg: String { return self._s[2511]! } + public var SocksProxySetup_TypeSocks: String { return self._s[2512]! } + public var Permissions_PrivacyPolicy: String { return self._s[2513]! } + public var VoiceOver_Chat_YourMusic: String { return self._s[2514]! } + public var SettingsSearch_Synonyms_Notifications_ResetAllNotifications: String { return self._s[2515]! } + public var Conversation_EmptyGifPanelPlaceholder: String { return self._s[2516]! } + public var Conversation_ContextMenuOpenChannel: String { return self._s[2517]! } + public var Report_AdditionalDetailsPlaceholder: String { return self._s[2518]! } + public var Activity_UploadingVideo: String { return self._s[2519]! } + public var PrivacyPolicy_AgeVerificationAgree: String { return self._s[2521]! } + public var Widget_LongTapToEdit: String { return self._s[2522]! } + public var VoiceChat_InviteLink_Listener: String { return self._s[2524]! } + public var SocksProxySetup_Credentials: String { return self._s[2525]! } + public var Preview_SaveGif: String { return self._s[2526]! } + public var Cache_Photos: String { return self._s[2527]! } + public var Channel_AdminLogFilter_EventsCalls: String { return self._s[2528]! } + public var Conversation_ContextMenuCancelEditing: String { return self._s[2529]! } + public var Contacts_FailedToSendInvitesMessage: String { return self._s[2530]! } + public func VoiceChat_RemoveAndBanPeerConfirmation(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[2531]!, self._r[2531]!, [_1, _2]) + } + public var Passport_Language_lt: String { return self._s[2532]! } + public var Passport_DeleteDocument: String { return self._s[2534]! } + public var GroupInfo_SetGroupPhotoStop: String { return self._s[2535]! } + public func Location_ProximityNotification_NotifyLong(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[2536]!, self._r[2536]!, [_1, _2]) + } + public var AccessDenied_VideoMessageMicrophone: String { return self._s[2537]! } + public func PeopleNearby_VisibleUntil(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[2538]!, self._r[2538]!, [_0]) + } + public var AccessDenied_VideoCallCamera: String { return self._s[2539]! } + public func Channel_AdminLog_MessageDeleted(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[2540]!, self._r[2540]!, [_0]) + } + public var PhotoEditor_SharpenTool: String { return self._s[2541]! } + public func PUSH_CHANNEL_MESSAGE_AUDIO(_ _1: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[2542]!, self._r[2542]!, [_1]) + } + public var DialogList_Unpin: String { return self._s[2543]! } + public var Stickers_NoStickersFound: String { return self._s[2544]! } + public var UserInfo_AddContact: String { return self._s[2546]! } + public func AddContact_SharedContactExceptionInfo(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[2548]!, self._r[2548]!, [_0]) + } + public func Notification_PinnedLocationMessage(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[2549]!, self._r[2549]!, [_0]) + } + public var CallFeedback_VideoReasonDistorted: String { return self._s[2550]! } + public var Tour_Text2: String { return self._s[2551]! } public func Conversation_TitleCommentsFormat(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2455]!, self._r[2455]!, [_1, _2]) + return formatWithArgumentRanges(self._s[2553]!, self._r[2553]!, [_1, _2]) } - public var InviteLink_DeleteAllRevokedLinksAlert_Text: String { return self._s[2457]! } - public var Paint_Delete: String { return self._s[2458]! } + public var InviteLink_DeleteAllRevokedLinksAlert_Text: String { return self._s[2555]! } + public var Paint_Delete: String { return self._s[2556]! } public func Call_VoiceChatInProgressMessage(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2459]!, self._r[2459]!, [_1, _2]) + return formatWithArgumentRanges(self._s[2557]!, self._r[2557]!, [_1, _2]) } - public var SettingsSearch_Synonyms_Notifications_InAppNotificationsVibrate: String { return self._s[2460]! } + public var SettingsSearch_Synonyms_Notifications_InAppNotificationsVibrate: String { return self._s[2558]! } public func PrivacySettings_LastSeenEverybodyMinus(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2462]!, self._r[2462]!, [_0]) + return formatWithArgumentRanges(self._s[2560]!, self._r[2560]!, [_0]) } - public var Privacy_Calls_NeverAllow_Title: String { return self._s[2463]! } - public var Notification_CallOutgoingShort: String { return self._s[2464]! } - public var Checkout_PasswordEntry_Title: String { return self._s[2465]! } - public var Channel_AdminLogFilter_AdminsAll: String { return self._s[2466]! } - public var Notification_MessageLifetime1m: String { return self._s[2467]! } - public var BlockedUsers_AddNew: String { return self._s[2469]! } - public var FastTwoStepSetup_EmailSection: String { return self._s[2470]! } - public var Settings_SaveEditedPhotos: String { return self._s[2471]! } - public var GroupInfo_GroupNamePlaceholder: String { return self._s[2472]! } - public var Channel_AboutItem: String { return self._s[2473]! } - public var GroupInfo_InviteLink_RevokeLink: String { return self._s[2474]! } - public var Privacy_Calls_P2PNever: String { return self._s[2476]! } - public var Passport_Language_uk: String { return self._s[2477]! } - public var NetworkUsageSettings_Wifi: String { return self._s[2478]! } - public var Conversation_Moderate_Report: String { return self._s[2479]! } - public var Wallpaper_ResetWallpapersConfirmation: String { return self._s[2480]! } - public var VoiceOver_Chat_SeenByRecipients: String { return self._s[2481]! } - public var Permissions_SiriText_v0: String { return self._s[2482]! } - public var Theme_Colors_Background: String { return self._s[2483]! } - public var Notification_CallMissed: String { return self._s[2484]! } - public var Stats_ZoomOut: String { return self._s[2485]! } - public var Profile_AddToExisting: String { return self._s[2486]! } - public var Passport_FieldAddressUploadHelp: String { return self._s[2489]! } - public var VoiceChat_RemovePeerRemove: String { return self._s[2490]! } - public var Undo_DeletedChannel: String { return self._s[2491]! } + public var Privacy_Calls_NeverAllow_Title: String { return self._s[2561]! } + public var Notification_CallOutgoingShort: String { return self._s[2562]! } + public var Checkout_PasswordEntry_Title: String { return self._s[2563]! } + public var Channel_AdminLogFilter_AdminsAll: String { return self._s[2564]! } + public var Notification_MessageLifetime1m: String { return self._s[2565]! } + public var BlockedUsers_AddNew: String { return self._s[2567]! } + public var FastTwoStepSetup_EmailSection: String { return self._s[2568]! } + public var Settings_SaveEditedPhotos: String { return self._s[2569]! } + public var GroupInfo_GroupNamePlaceholder: String { return self._s[2570]! } + public func ImportStickerPack_Of(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[2571]!, self._r[2571]!, [_1, _2]) + } + public var Channel_AboutItem: String { return self._s[2572]! } + public var GroupInfo_InviteLink_RevokeLink: String { return self._s[2573]! } + public var Privacy_Calls_P2PNever: String { return self._s[2575]! } + public var Passport_Language_uk: String { return self._s[2576]! } + public var NetworkUsageSettings_Wifi: String { return self._s[2577]! } + public var Conversation_Moderate_Report: String { return self._s[2578]! } + public var Wallpaper_ResetWallpapersConfirmation: String { return self._s[2579]! } + public var VoiceOver_Chat_SeenByRecipients: String { return self._s[2580]! } + public var Permissions_SiriText_v0: String { return self._s[2581]! } + public var Theme_Colors_Background: String { return self._s[2582]! } + public var Notification_CallMissed: String { return self._s[2583]! } + public var Stats_ZoomOut: String { return self._s[2584]! } + public var Profile_AddToExisting: String { return self._s[2585]! } + public var Passport_FieldAddressUploadHelp: String { return self._s[2588]! } + public var VoiceChat_RemovePeerRemove: String { return self._s[2589]! } + public var Undo_DeletedChannel: String { return self._s[2590]! } public func Channel_AdminLog_MessagePinned(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2492]!, self._r[2492]!, [_0]) + return formatWithArgumentRanges(self._s[2591]!, self._r[2591]!, [_0]) } - public var Login_ResetAccountProtected_TimerTitle: String { return self._s[2493]! } - public var Map_LiveLocationGroupDescription: String { return self._s[2494]! } - public var Passport_InfoFAQ_URL: String { return self._s[2495]! } - public var IntentsSettings_SuggestedChats: String { return self._s[2498]! } + public var Login_ResetAccountProtected_TimerTitle: String { return self._s[2592]! } + public var Map_LiveLocationGroupDescription: String { return self._s[2593]! } + public var Passport_InfoFAQ_URL: String { return self._s[2594]! } + public var IntentsSettings_SuggestedChats: String { return self._s[2597]! } public func PUSH_MESSAGE_DOC(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2499]!, self._r[2499]!, [_1]) + return formatWithArgumentRanges(self._s[2598]!, self._r[2598]!, [_1]) } - public var State_connecting: String { return self._s[2500]! } - public var Passport_Identity_Country: String { return self._s[2501]! } - public var Passport_PasswordDescription: String { return self._s[2502]! } - public var ChatList_PsaLabel_covid: String { return self._s[2503]! } + public var State_connecting: String { return self._s[2599]! } + public var Passport_Identity_Country: String { return self._s[2600]! } + public var Passport_PasswordDescription: String { return self._s[2601]! } + public var ChatList_PsaLabel_covid: String { return self._s[2602]! } public func PUSH_MESSAGE(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2504]!, self._r[2504]!, [_1]) + return formatWithArgumentRanges(self._s[2603]!, self._r[2603]!, [_1]) } - public var Contacts_AddPeopleNearby: String { return self._s[2505]! } - public var OwnershipTransfer_SetupTwoStepAuth: String { return self._s[2506]! } - public var ClearCache_Description: String { return self._s[2507]! } - public var Localization_LanguageName: String { return self._s[2508]! } + public var Contacts_AddPeopleNearby: String { return self._s[2604]! } + public var OwnershipTransfer_SetupTwoStepAuth: String { return self._s[2605]! } + public var ClearCache_Description: String { return self._s[2606]! } + public var Localization_LanguageName: String { return self._s[2607]! } public func UserInfo_UnblockConfirmation(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2509]!, self._r[2509]!, [_0]) + return formatWithArgumentRanges(self._s[2608]!, self._r[2608]!, [_0]) } - public var Conversation_AddMembers: String { return self._s[2510]! } - public var ChatList_TabIconFoldersTooltipEmptyFolders: String { return self._s[2511]! } - public var UserInfo_CreateNewContact: String { return self._s[2512]! } - public var Channel_Stickers_NotFound: String { return self._s[2514]! } - public var Message_FakeAccount: String { return self._s[2515]! } - public var Watch_Message_Poll: String { return self._s[2516]! } - public var Group_Members_Title: String { return self._s[2517]! } - public var Privacy_Forwards_WhoCanForward: String { return self._s[2518]! } + public var Conversation_AddMembers: String { return self._s[2609]! } + public var ChatList_TabIconFoldersTooltipEmptyFolders: String { return self._s[2610]! } + public var UserInfo_CreateNewContact: String { return self._s[2611]! } + public var Channel_Stickers_NotFound: String { return self._s[2613]! } + public var Message_FakeAccount: String { return self._s[2614]! } + public var Watch_Message_Poll: String { return self._s[2615]! } + public var Group_Members_Title: String { return self._s[2616]! } + public var Privacy_Forwards_WhoCanForward: String { return self._s[2617]! } public func Notification_Kicked(_ _0: String, _ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2519]!, self._r[2519]!, [_0, _1]) + return formatWithArgumentRanges(self._s[2618]!, self._r[2618]!, [_0, _1]) } - public var BroadcastGroups_Convert: String { return self._s[2520]! } - public var Login_InfoDeletePhoto: String { return self._s[2521]! } - public var Appearance_ThemePreview_ChatList_6_Name: String { return self._s[2522]! } - public var InstantPage_FeedbackButton: String { return self._s[2523]! } - public var Appearance_PreviewReplyText: String { return self._s[2524]! } - public var Passport_FieldPhoneHelp: String { return self._s[2525]! } - public var Group_ErrorAddTooMuchBots: String { return self._s[2526]! } - public var Media_SendingOptionsTooltip: String { return self._s[2527]! } - public var ScheduledMessages_ScheduledOnline: String { return self._s[2528]! } - public var Notifications_Badge: String { return self._s[2529]! } - public var VoiceOver_Chat_VideoMessage: String { return self._s[2530]! } - public var TwoStepAuth_RecoveryCodeExpired: String { return self._s[2531]! } + public var VoiceChat_CancelConfirmationText: String { return self._s[2619]! } + public var BroadcastGroups_Convert: String { return self._s[2620]! } + public var Login_InfoDeletePhoto: String { return self._s[2621]! } + public var Appearance_ThemePreview_ChatList_6_Name: String { return self._s[2622]! } + public var InstantPage_FeedbackButton: String { return self._s[2623]! } + public var Appearance_PreviewReplyText: String { return self._s[2624]! } + public var Passport_FieldPhoneHelp: String { return self._s[2625]! } + public var Group_ErrorAddTooMuchBots: String { return self._s[2626]! } + public var Media_SendingOptionsTooltip: String { return self._s[2627]! } + public var ScheduledMessages_ScheduledOnline: String { return self._s[2628]! } + public var Notifications_Badge: String { return self._s[2629]! } + public var VoiceOver_Chat_VideoMessage: String { return self._s[2630]! } + public var TwoStepAuth_RecoveryCodeExpired: String { return self._s[2631]! } public func Notification_PinnedPhotoMessage(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2533]!, self._r[2533]!, [_0]) + return formatWithArgumentRanges(self._s[2633]!, self._r[2633]!, [_0]) } - public var Passport_InfoLearnMore: String { return self._s[2534]! } - public var EnterPasscode_EnterTitle: String { return self._s[2535]! } - public var Appearance_EditTheme: String { return self._s[2536]! } - public var EditTheme_Expand_BottomInfo: String { return self._s[2537]! } - public var Stats_FollowersTitle: String { return self._s[2538]! } - public var Passport_Identity_SurnamePlaceholder: String { return self._s[2539]! } - public var Channel_Subscribers_Title: String { return self._s[2540]! } - public var Group_ErrorSupergroupConversionNotPossible: String { return self._s[2541]! } - public var ChatImportActivity_ErrorGeneric: String { return self._s[2542]! } - public var EditTheme_ThemeTemplateAlertTitle: String { return self._s[2543]! } - public var EditTheme_Create_Preview_IncomingText: String { return self._s[2544]! } - public var Conversation_AddToReadingList: String { return self._s[2545]! } + public var Passport_InfoLearnMore: String { return self._s[2634]! } + public var EnterPasscode_EnterTitle: String { return self._s[2635]! } + public var Appearance_EditTheme: String { return self._s[2636]! } + public var EditTheme_Expand_BottomInfo: String { return self._s[2637]! } + public var Stats_FollowersTitle: String { return self._s[2638]! } + public var Passport_Identity_SurnamePlaceholder: String { return self._s[2639]! } + public var Channel_Subscribers_Title: String { return self._s[2640]! } + public var Group_ErrorSupergroupConversionNotPossible: String { return self._s[2641]! } + public var ChatImportActivity_ErrorGeneric: String { return self._s[2642]! } + public var EditTheme_ThemeTemplateAlertTitle: String { return self._s[2643]! } + public var EditTheme_Create_Preview_IncomingText: String { return self._s[2644]! } + public var Conversation_AddToReadingList: String { return self._s[2645]! } + public var VoiceChat_EditBioPlaceholder: String { return self._s[2646]! } public func Notifications_ExceptionsChangeSound(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2546]!, self._r[2546]!, [_0]) + return formatWithArgumentRanges(self._s[2647]!, self._r[2647]!, [_0]) } - public var Group_AdminLog_EmptyText: String { return self._s[2547]! } - public var Passport_Identity_EditInternalPassport: String { return self._s[2548]! } - public var Watch_Location_Current: String { return self._s[2549]! } - public var PrivacyPolicy_Title: String { return self._s[2550]! } - public var Privacy_GroupsAndChannels_CustomHelp: String { return self._s[2557]! } - public var Channel_TypeSetup_Title: String { return self._s[2561]! } - public var Appearance_PreviewReplyAuthor: String { return self._s[2562]! } - public var Passport_Language_ja: String { return self._s[2563]! } - public var ReportPeer_ReasonSpam: String { return self._s[2564]! } - public var Widget_GalleryDescription: String { return self._s[2565]! } - public var Privacy_PaymentsClearInfoHelp: String { return self._s[2566]! } - public var Conversation_EditingMessageMediaEditCurrentPhoto: String { return self._s[2568]! } - public var Channel_AdminLog_ChangeInfo: String { return self._s[2569]! } - public var ChatListFolder_NameNonContacts: String { return self._s[2570]! } + public var Group_AdminLog_EmptyText: String { return self._s[2648]! } + public var Passport_Identity_EditInternalPassport: String { return self._s[2649]! } + public var Watch_Location_Current: String { return self._s[2650]! } + public var Appearance_AppIconNew1: String { return self._s[2651]! } + public var PrivacyPolicy_Title: String { return self._s[2652]! } + public var Privacy_GroupsAndChannels_CustomHelp: String { return self._s[2659]! } + public var Channel_TypeSetup_Title: String { return self._s[2663]! } + public var Appearance_PreviewReplyAuthor: String { return self._s[2664]! } + public var Passport_Language_ja: String { return self._s[2665]! } + public var ReportPeer_ReasonSpam: String { return self._s[2666]! } + public var Widget_GalleryDescription: String { return self._s[2667]! } + public var Privacy_PaymentsClearInfoHelp: String { return self._s[2668]! } + public var VoiceChat_ChangePhoto: String { return self._s[2670]! } + public var Conversation_EditingMessageMediaEditCurrentPhoto: String { return self._s[2671]! } + public var Channel_AdminLog_ChangeInfo: String { return self._s[2672]! } + public var ChatListFolder_NameNonContacts: String { return self._s[2673]! } public func InviteLink_ExpiresIn(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2571]!, self._r[2571]!, [_0]) + return formatWithArgumentRanges(self._s[2674]!, self._r[2674]!, [_0]) } - public var Call_Audio: String { return self._s[2572]! } - public var PhotoEditor_CurvesGreen: String { return self._s[2573]! } - public var ChatList_Search_NoResultsFitlerFiles: String { return self._s[2574]! } - public var Settings_PrivacySettings: String { return self._s[2575]! } - public var InviteLink_UsageLimitReached: String { return self._s[2576]! } - public var Stats_Followers: String { return self._s[2577]! } - public var Notifications_AddExceptionTitle: String { return self._s[2578]! } - public var TwoFactorSetup_Password_Title: String { return self._s[2579]! } - public var ChannelMembers_WhoCanAddMembersAllHelp: String { return self._s[2580]! } - public var OldChannels_NoticeText: String { return self._s[2581]! } - public var Conversation_SavedMessages: String { return self._s[2582]! } - public var Intents_ErrorLockedText: String { return self._s[2583]! } + public var Call_Audio: String { return self._s[2675]! } + public var PhotoEditor_CurvesGreen: String { return self._s[2676]! } + public var ChatList_Search_NoResultsFitlerFiles: String { return self._s[2677]! } + public var Settings_PrivacySettings: String { return self._s[2678]! } + public var InviteLink_UsageLimitReached: String { return self._s[2679]! } + public var Stats_Followers: String { return self._s[2680]! } + public var Notifications_AddExceptionTitle: String { return self._s[2681]! } + public var TwoFactorSetup_Password_Title: String { return self._s[2682]! } + public var ChannelMembers_WhoCanAddMembersAllHelp: String { return self._s[2683]! } + public var OldChannels_NoticeText: String { return self._s[2684]! } + public var Conversation_SavedMessages: String { return self._s[2685]! } + public var Intents_ErrorLockedText: String { return self._s[2686]! } public func Conversation_PeerNearbyTitle(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2585]!, self._r[2585]!, [_1, _2]) + return formatWithArgumentRanges(self._s[2688]!, self._r[2688]!, [_1, _2]) } - public var Passport_Address_TypeResidentialAddress: String { return self._s[2586]! } - public var Appearance_ThemeNightBlue: String { return self._s[2587]! } - public var Notification_ChannelInviterSelf: String { return self._s[2588]! } - public var Conversation_ForwardTooltip_SavedMessages_Many: String { return self._s[2589]! } - public var InviteLink_Create_TimeLimitExpiryDateNever: String { return self._s[2591]! } - public var Watch_UserInfo_Service: String { return self._s[2592]! } - public var ChatList_Context_Back: String { return self._s[2593]! } - public var Passport_Email_Title: String { return self._s[2594]! } - public var Stats_GroupTopAdmin_Promote: String { return self._s[2595]! } + public var Passport_Address_TypeResidentialAddress: String { return self._s[2689]! } + public var Appearance_ThemeNightBlue: String { return self._s[2690]! } + public var Notification_ChannelInviterSelf: String { return self._s[2691]! } + public var Conversation_ForwardTooltip_SavedMessages_Many: String { return self._s[2692]! } + public var InviteLink_Create_TimeLimitExpiryDateNever: String { return self._s[2694]! } + public var Watch_UserInfo_Service: String { return self._s[2695]! } + public var ChatList_Context_Back: String { return self._s[2696]! } + public var Passport_Email_Title: String { return self._s[2697]! } + public var ImportStickerPack_AddToExistingStickerSet: String { return self._s[2698]! } + public var Stats_GroupTopAdmin_Promote: String { return self._s[2699]! } public func PUSH_PINNED_INVOICE(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2596]!, self._r[2596]!, [_1]) + return formatWithArgumentRanges(self._s[2700]!, self._r[2700]!, [_1]) } - public var Conversation_UnsupportedMedia: String { return self._s[2597]! } - public var Passport_Address_OneOfTypePassportRegistration: String { return self._s[2598]! } - public var Privacy_TopPeersHelp: String { return self._s[2600]! } - public var Privacy_Forwards_AlwaysLink: String { return self._s[2601]! } - public var Notifications_Badge_CountUnreadMessages_InfoOn: String { return self._s[2602]! } - public var Permissions_NotificationsTitle_v0: String { return self._s[2603]! } + public var Conversation_UnsupportedMedia: String { return self._s[2701]! } + public var Passport_Address_OneOfTypePassportRegistration: String { return self._s[2702]! } + public var Privacy_TopPeersHelp: String { return self._s[2704]! } + public var Privacy_Forwards_AlwaysLink: String { return self._s[2705]! } + public var Notifications_Badge_CountUnreadMessages_InfoOn: String { return self._s[2706]! } + public var Permissions_NotificationsTitle_v0: String { return self._s[2707]! } public func Location_ProximityNotification_AlreadyClose(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2604]!, self._r[2604]!, [_0]) + return formatWithArgumentRanges(self._s[2708]!, self._r[2708]!, [_0]) } - public var Notification_PassportValueProofOfAddress: String { return self._s[2605]! } - public var Map_Map: String { return self._s[2606]! } - public var WallpaperSearch_ColorBlue: String { return self._s[2607]! } - public var Privacy_Calls_CustomShareHelp: String { return self._s[2608]! } - public var PhotoEditor_BlurToolRadial: String { return self._s[2609]! } - public var ChatList_Search_FilterMusic: String { return self._s[2610]! } - public var SettingsSearch_Synonyms_Data_AutoplayGifs: String { return self._s[2611]! } - public var Privacy_PaymentsClear_ShippingInfo: String { return self._s[2612]! } - public var Settings_LogoutConfirmationTitle: String { return self._s[2614]! } + public var Notification_PassportValueProofOfAddress: String { return self._s[2709]! } + public var Map_Map: String { return self._s[2710]! } + public var WallpaperSearch_ColorBlue: String { return self._s[2711]! } + public var Privacy_Calls_CustomShareHelp: String { return self._s[2712]! } + public var PhotoEditor_BlurToolRadial: String { return self._s[2713]! } + public var ChatList_Search_FilterMusic: String { return self._s[2714]! } + public var SettingsSearch_Synonyms_Data_AutoplayGifs: String { return self._s[2715]! } + public var Privacy_PaymentsClear_ShippingInfo: String { return self._s[2716]! } + public var Settings_LogoutConfirmationTitle: String { return self._s[2718]! } public func PUSH_CHANNEL_MESSAGE_VIDEOS(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2615]!, self._r[2615]!, [_1, _2]) + return formatWithArgumentRanges(self._s[2719]!, self._r[2719]!, [_1, _2]) } public func Notification_ChangedGroupPhoto(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2616]!, self._r[2616]!, [_0]) + return formatWithArgumentRanges(self._s[2720]!, self._r[2720]!, [_0]) } - public var Channel_Username_RevokeExistingUsernamesInfo: String { return self._s[2617]! } - public var Group_Username_CreatePublicLinkHelp: String { return self._s[2618]! } - public var VoiceOver_ChatList_MessageEmpty: String { return self._s[2621]! } - public var GroupInfo_Location: String { return self._s[2622]! } - public var Passport_Language_ka: String { return self._s[2623]! } + public var Channel_Username_RevokeExistingUsernamesInfo: String { return self._s[2721]! } + public var Group_Username_CreatePublicLinkHelp: String { return self._s[2722]! } + public var VoiceOver_ChatList_MessageEmpty: String { return self._s[2724]! } + public var GroupInfo_Location: String { return self._s[2725]! } + public var Passport_Language_ka: String { return self._s[2726]! } public func TwoStepAuth_SetupPendingEmail(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2624]!, self._r[2624]!, [_0]) - } - public var Conversation_ContextMenuOpenChannelProfile: String { return self._s[2625]! } - public var ChatImport_SelectionConfirmationAlertTitle: String { return self._s[2627]! } - public var ScheduledMessages_ClearAllConfirmation: String { return self._s[2629]! } - public var DialogList_SearchSectionRecent: String { return self._s[2630]! } - public var Passport_Address_OneOfTypeTemporaryRegistration: String { return self._s[2631]! } - public var Conversation_Timer_Send: String { return self._s[2632]! } - public func VoiceOver_ScrollStatus(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2634]!, self._r[2634]!, [_1, _2]) - } - public var ChatState_Updating: String { return self._s[2635]! } - public var ChannelMembers_WhoCanAddMembers: String { return self._s[2636]! } - public var ChannelInfo_DeleteGroup: String { return self._s[2637]! } - public var TwoStepAuth_RecoveryFailed: String { return self._s[2638]! } - public var Channel_OwnershipTransfer_EnterPassword: String { return self._s[2639]! } - public var InviteLink_Create_TimeLimitExpiryTime: String { return self._s[2640]! } - public var ChannelInfo_InviteLink_RevokeAlert_Text: String { return self._s[2641]! } - public var ChatList_Search_NoResults: String { return self._s[2642]! } - public var ChatListFolderSettings_AddRecommended: String { return self._s[2644]! } - public var ChangePhoneNumberCode_Called: String { return self._s[2645]! } - public var PeerInfo_GroupAboutItem: String { return self._s[2646]! } - public var VoiceOver_SelfDestructTimerOff: String { return self._s[2648]! } - public func Channel_AdminLog_DeletedInviteLink(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2649]!, self._r[2649]!, [_1, _2]) - } - public func LiveLocationUpdated_YesterdayAt(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2650]!, self._r[2650]!, [_0]) - } - public var PrivacySettings_AuthSessions: String { return self._s[2651]! } - public var Passport_Address_Postcode: String { return self._s[2652]! } - public var VoiceOver_Chat_YourVideoMessage: String { return self._s[2653]! } - public func VoiceChat_ForwardTooltip_ManyChats(_ _0: String, _ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2654]!, self._r[2654]!, [_0, _1]) - } - public var Passport_Address_Street2Placeholder: String { return self._s[2655]! } - public var Group_Location_Title: String { return self._s[2656]! } - public var SettingsSearch_Synonyms_Data_AutoDownloadReset: String { return self._s[2657]! } - public var PeopleNearby_UsersEmpty: String { return self._s[2658]! } - public var Conversation_ContextMenuSpeak: String { return self._s[2660]! } - public var SettingsSearch_Synonyms_Data_Title: String { return self._s[2661]! } - public func Checkout_PasswordEntry_Text(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2663]!, self._r[2663]!, [_0]) - } - public var Proxy_TooltipUnavailable: String { return self._s[2664]! } - public var Map_Search: String { return self._s[2665]! } - public var AutoDownloadSettings_TypeContacts: String { return self._s[2666]! } - public var Conversation_SearchByName_Prefix: String { return self._s[2667]! } - public func Channel_AdminLog_MessageToggleSignaturesOff(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2668]!, self._r[2668]!, [_0]) - } - public var TwoStepAuth_EmailAddSuccess: String { return self._s[2669]! } - public var ProfilePhoto_MainPhoto: String { return self._s[2670]! } - public var SettingsSearch_Synonyms_Notifications_InAppNotificationsSound: String { return self._s[2671]! } - public var SharedMedia_EmptyMusicText: String { return self._s[2672]! } - public var ChatSettings_AutoDownloadPhotos: String { return self._s[2673]! } - public var NetworkUsageSettings_BytesReceived: String { return self._s[2674]! } - public var Channel_AdminLog_EmptyText: String { return self._s[2675]! } - public var Channel_BanUser_PermissionSendMessages: String { return self._s[2676]! } - public var Undo_ChatDeletedForBothSides: String { return self._s[2677]! } - public var Notifications_GroupNotifications: String { return self._s[2678]! } - public var AccessDenied_SaveMedia: String { return self._s[2679]! } - public var InviteLink_Create_Revoke: String { return self._s[2680]! } - public var GroupInfo_LabelOwner: String { return self._s[2681]! } - public var Passport_Language_id: String { return self._s[2682]! } - public var ChatSettings_AutoDownloadTitle: String { return self._s[2683]! } - public var Conversation_UnpinMessageAlert: String { return self._s[2684]! } - public func LiveLocationUpdated_TodayAt(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2685]!, self._r[2685]!, [_0]) - } - public func Call_RemoteVideoPaused(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2686]!, self._r[2686]!, [_0]) - } - public var TwoFactorSetup_Done_Text: String { return self._s[2687]! } - public func LastSeen_AtDate(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2688]!, self._r[2688]!, [_0]) - } - public var NetworkUsageSettings_BytesSent: String { return self._s[2689]! } - public var Conversation_AudioRateTooltipNormal: String { return self._s[2690]! } - public var OwnershipTransfer_Transfer: String { return self._s[2691]! } - public func Notification_Exceptions_Sound(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2692]!, self._r[2692]!, [_0]) - } - public var Passport_Language_pt: String { return self._s[2693]! } - public var PrivacySettings_WebSessions: String { return self._s[2694]! } - public var PrivacyPolicy_DeclineDeleteNow: String { return self._s[2696]! } - public var TwoFactorSetup_Hint_Title: String { return self._s[2697]! } - public func Notification_Joined(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2698]!, self._r[2698]!, [_0]) - } - public var Group_Username_RemoveExistingUsernamesInfo: String { return self._s[2699]! } - public var PrivacyLastSeenSettings_CustomShareSettings_Delete: String { return self._s[2700]! } - public var AutoNightTheme_Scheduled: String { return self._s[2701]! } - public var CreatePoll_ExplanationHeader: String { return self._s[2702]! } - public var Calls_TabTitle: String { return self._s[2703]! } - public var VoiceChat_RecordingInProgress: String { return self._s[2704]! } - public var ChatList_UndoArchiveHiddenText: String { return self._s[2705]! } - public var Notification_VideoCallCanceled: String { return self._s[2706]! } - public var Login_CodeSentInternal: String { return self._s[2707]! } - public var SettingsSearch_Synonyms_Proxy_AddProxy: String { return self._s[2708]! } - public var Call_RecordingDisabledMessage: String { return self._s[2710]! } - public func VoiceChat_RemovedPeerText(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2711]!, self._r[2711]!, [_0]) - } - public var Conversation_UsersTooMuchError: String { return self._s[2713]! } - public var AutoDownloadSettings_TypeChannels: String { return self._s[2714]! } - public var Channel_Info_Stickers: String { return self._s[2715]! } - public var Passport_DeleteAddressConfirmation: String { return self._s[2716]! } - public func Conversation_PeerNearbyDistance(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2717]!, self._r[2717]!, [_1, _2]) - } - public var ChannelMembers_WhoCanAddMembers_Admins: String { return self._s[2718]! } - public func Call_StatusOngoing(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2719]!, self._r[2719]!, [_0]) - } - public var Passport_DiscardMessageTitle: String { return self._s[2720]! } - public var Call_VoiceOver_VideoCallIncoming: String { return self._s[2721]! } - public var Localization_LanguageOther: String { return self._s[2722]! } - public var Conversation_EncryptionCanceled: String { return self._s[2723]! } - public var ChatSettings_AutomaticPhotoDownload: String { return self._s[2724]! } - public var ReportPeer_ReasonFake: String { return self._s[2726]! } - public func Notification_SecretChatMessageScreenshot(_ _0: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(self._s[2727]!, self._r[2727]!, [_0]) } - public var Target_InviteToGroupErrorAlreadyInvited: String { return self._s[2729]! } - public var SocksProxySetup_SavedProxies: String { return self._s[2730]! } - public var InviteLink_Create_UsersLimitNumberOfUsers: String { return self._s[2731]! } - public func ApplyLanguage_ChangeLanguageAlreadyActive(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2732]!, self._r[2732]!, [_1]) + public var Conversation_ContextMenuOpenChannelProfile: String { return self._s[2728]! } + public var ChatImport_SelectionConfirmationAlertTitle: String { return self._s[2730]! } + public var ScheduledMessages_ClearAllConfirmation: String { return self._s[2732]! } + public var DialogList_SearchSectionRecent: String { return self._s[2733]! } + public var Passport_Address_OneOfTypeTemporaryRegistration: String { return self._s[2734]! } + public var Conversation_Timer_Send: String { return self._s[2735]! } + public func VoiceOver_ScrollStatus(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[2737]!, self._r[2737]!, [_1, _2]) } - public var Conversation_ScamWarning: String { return self._s[2734]! } - public var Channel_AdminLog_InfoPanelAlertTitle: String { return self._s[2735]! } - public var LocalGroup_Title: String { return self._s[2736]! } - public var SettingsSearch_Synonyms_Notifications_MessageNotificationsAlert: String { return self._s[2738]! } - public var SettingsSearch_Synonyms_Privacy_PasscodeAndFaceId: String { return self._s[2739]! } - public var VoiceChat_SelectAccount: String { return self._s[2740]! } - public var Login_PhoneFloodError: String { return self._s[2741]! } - public var Conversation_PinMessageAlert_PinAndNotifyMembers: String { return self._s[2742]! } - public var Username_InvalidTaken: String { return self._s[2744]! } - public var SocksProxySetup_AddProxy: String { return self._s[2746]! } - public var PrivacyLastSeenSettings_WhoCanSeeMyTimestamp: String { return self._s[2747]! } - public var MediaPicker_UngroupDescription: String { return self._s[2748]! } - public var Login_CodeExpired: String { return self._s[2749]! } - public var Localization_ChooseLanguage: String { return self._s[2750]! } - public var Checkout_NewCard_PostcodePlaceholder: String { return self._s[2751]! } - public func ChangePhone_ErrorOccupied(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2752]!, self._r[2752]!, [_0]) + public var ChatState_Updating: String { return self._s[2738]! } + public var ChannelMembers_WhoCanAddMembers: String { return self._s[2739]! } + public var ChannelInfo_DeleteGroup: String { return self._s[2740]! } + public var TwoStepAuth_RecoveryFailed: String { return self._s[2741]! } + public var Channel_OwnershipTransfer_EnterPassword: String { return self._s[2742]! } + public var InviteLink_Create_TimeLimitExpiryTime: String { return self._s[2743]! } + public var ChannelInfo_InviteLink_RevokeAlert_Text: String { return self._s[2744]! } + public var ChatList_Search_NoResults: String { return self._s[2745]! } + public var ChatListFolderSettings_AddRecommended: String { return self._s[2747]! } + public var ChangePhoneNumberCode_Called: String { return self._s[2748]! } + public var PeerInfo_GroupAboutItem: String { return self._s[2749]! } + public var VoiceOver_SelfDestructTimerOff: String { return self._s[2751]! } + public func Channel_AdminLog_DeletedInviteLink(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[2752]!, self._r[2752]!, [_1, _2]) } - public func Channel_DiscussionGroup_HeaderSet(_ _0: String) -> (String, [(Int, NSRange)]) { + public func LiveLocationUpdated_YesterdayAt(_ _0: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(self._s[2753]!, self._r[2753]!, [_0]) } - public var ReportPeer_ReasonOther_Title: String { return self._s[2755]! } - public var Conversation_ScheduleMessage_Title: String { return self._s[2756]! } - public func VoiceChat_UserInvited(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2757]!, self._r[2757]!, [_0]) + public var PrivacySettings_AuthSessions: String { return self._s[2754]! } + public var Passport_Address_Postcode: String { return self._s[2755]! } + public var VoiceOver_Chat_YourVideoMessage: String { return self._s[2756]! } + public func VoiceChat_ForwardTooltip_ManyChats(_ _0: String, _ _1: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[2757]!, self._r[2757]!, [_0, _1]) } - public var PeerInfo_ButtonDiscuss: String { return self._s[2758]! } - public var SettingsSearch_Synonyms_Notifications_BadgeIncludeMutedPublicGroups: String { return self._s[2759]! } - public var Call_StatusNoAnswer: String { return self._s[2760]! } - public var ScheduledMessages_DeleteMany: String { return self._s[2762]! } - public var Channel_DiscussionGroupInfo: String { return self._s[2763]! } - public var Conversation_UnarchiveDone: String { return self._s[2764]! } - public var LogoutOptions_AddAccountText: String { return self._s[2765]! } - public var Message_PinnedContactMessage: String { return self._s[2766]! } - public func ChatList_DeleteAndLeaveGroupConfirmation(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2767]!, self._r[2767]!, [_0]) + public var Passport_Address_Street2Placeholder: String { return self._s[2758]! } + public var Group_Location_Title: String { return self._s[2759]! } + public var SettingsSearch_Synonyms_Data_AutoDownloadReset: String { return self._s[2760]! } + public var PeopleNearby_UsersEmpty: String { return self._s[2761]! } + public var Conversation_ContextMenuSpeak: String { return self._s[2763]! } + public var SettingsSearch_Synonyms_Data_Title: String { return self._s[2764]! } + public func Checkout_PasswordEntry_Text(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[2766]!, self._r[2766]!, [_0]) } - public func FileSize_GB(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2769]!, self._r[2769]!, [_0]) + public var Proxy_TooltipUnavailable: String { return self._s[2767]! } + public var Map_Search: String { return self._s[2768]! } + public var VoiceChat_CancelConfirmationTitle: String { return self._s[2769]! } + public var AutoDownloadSettings_TypeContacts: String { return self._s[2770]! } + public var Conversation_SearchByName_Prefix: String { return self._s[2771]! } + public func Channel_AdminLog_MessageToggleSignaturesOff(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[2772]!, self._r[2772]!, [_0]) } - public var Stats_GroupLanguagesTitle: String { return self._s[2770]! } - public var Passport_FieldAddressHelp: String { return self._s[2771]! } - public func Passport_FieldOneOf_Or(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2772]!, self._r[2772]!, [_1, _2]) - } - public var ChatSettings_OpenLinksIn: String { return self._s[2774]! } - public var TwoFactorSetup_Hint_SkipAction: String { return self._s[2775]! } - public var Message_Photo: String { return self._s[2776]! } - public var Media_LimitedAccessManage: String { return self._s[2778]! } - public var MediaPicker_AddCaption: String { return self._s[2779]! } - public var LogoutOptions_Title: String { return self._s[2780]! } - public func PUSH_PINNED_GIF(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2781]!, self._r[2781]!, [_1]) - } - public var Conversation_StatusKickedFromGroup: String { return self._s[2782]! } - public var Channel_AdminLogFilter_AdminsTitle: String { return self._s[2783]! } - public var ChatList_DeleteSavedMessagesConfirmationTitle: String { return self._s[2784]! } - public var Channel_AdminLogFilter_Title: String { return self._s[2785]! } - public var Passport_Address_TypeRentalAgreementUploadScan: String { return self._s[2786]! } - public var Compose_GroupTokenListPlaceholder: String { return self._s[2787]! } - public var Notifications_MessageNotificationsExceptions: String { return self._s[2788]! } - public var ChannelIntro_Title: String { return self._s[2789]! } - public var Stats_Message_Views: String { return self._s[2790]! } - public var Stickers_Install: String { return self._s[2791]! } - public func VoiceOver_Chat_FileFrom(_ _0: String) -> (String, [(Int, NSRange)]) { + public var TwoStepAuth_EmailAddSuccess: String { return self._s[2773]! } + public var ProfilePhoto_MainPhoto: String { return self._s[2774]! } + public var SettingsSearch_Synonyms_Notifications_InAppNotificationsSound: String { return self._s[2775]! } + public var SharedMedia_EmptyMusicText: String { return self._s[2776]! } + public var ChatSettings_AutoDownloadPhotos: String { return self._s[2777]! } + public var NetworkUsageSettings_BytesReceived: String { return self._s[2778]! } + public var Channel_AdminLog_EmptyText: String { return self._s[2779]! } + public var ImportStickerPack_InProgress: String { return self._s[2780]! } + public var Channel_BanUser_PermissionSendMessages: String { return self._s[2781]! } + public var Undo_ChatDeletedForBothSides: String { return self._s[2782]! } + public var Notifications_GroupNotifications: String { return self._s[2783]! } + public var AccessDenied_SaveMedia: String { return self._s[2784]! } + public var InviteLink_Create_Revoke: String { return self._s[2785]! } + public var GroupInfo_LabelOwner: String { return self._s[2786]! } + public var TwoFactorSetup_PasswordRecovery_Action: String { return self._s[2787]! } + public var Passport_Language_id: String { return self._s[2789]! } + public var ChatSettings_AutoDownloadTitle: String { return self._s[2790]! } + public var Conversation_UnpinMessageAlert: String { return self._s[2791]! } + public func LiveLocationUpdated_TodayAt(_ _0: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(self._s[2792]!, self._r[2792]!, [_0]) } - public var EditTheme_Create_Preview_IncomingReplyText: String { return self._s[2793]! } - public var Conversation_SwipeToReplyHintTitle: String { return self._s[2795]! } - public var Settings_Username: String { return self._s[2798]! } - public var FastTwoStepSetup_Title: String { return self._s[2799]! } - public var Notifications_Badge_CountUnreadMessages_InfoOff: String { return self._s[2800]! } - public var SettingsSearch_Synonyms_Privacy_Title: String { return self._s[2801]! } - public var Passport_Identity_IssueDatePlaceholder: String { return self._s[2803]! } - public var CallFeedback_ReasonEcho: String { return self._s[2804]! } - public func Time_MonthOfYear_m1(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2805]!, self._r[2805]!, [_0]) + public func Call_RemoteVideoPaused(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[2793]!, self._r[2793]!, [_0]) } - public var Conversation_OpenBotLinkTitle: String { return self._s[2806]! } - public var SocksProxySetup_Title: String { return self._s[2807]! } - public var CallFeedback_Success: String { return self._s[2808]! } - public var WallpaperPreview_SwipeTopText: String { return self._s[2810]! } - public var InstantPage_AutoNightTheme: String { return self._s[2812]! } - public var Watch_Conversation_Reply: String { return self._s[2813]! } - public var VoiceChat_Share: String { return self._s[2815]! } - public var Chat_PanelUnpinAllMessages: String { return self._s[2816]! } - public var WallpaperPreview_Pattern: String { return self._s[2817]! } - public var CheckoutInfo_ReceiverInfoEmail: String { return self._s[2818]! } - public func Conversation_DeleteMessagesFor(_ _0: String) -> (String, [(Int, NSRange)]) { + public var TwoFactorSetup_Done_Text: String { return self._s[2794]! } + public func LastSeen_AtDate(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[2795]!, self._r[2795]!, [_0]) + } + public var NetworkUsageSettings_BytesSent: String { return self._s[2796]! } + public var Conversation_AudioRateTooltipNormal: String { return self._s[2797]! } + public var VoiceChat_EditDescriptionSuccess: String { return self._s[2798]! } + public var OwnershipTransfer_Transfer: String { return self._s[2799]! } + public func Notification_Exceptions_Sound(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[2800]!, self._r[2800]!, [_0]) + } + public var Passport_Language_pt: String { return self._s[2801]! } + public var PrivacySettings_WebSessions: String { return self._s[2802]! } + public var PrivacyPolicy_DeclineDeleteNow: String { return self._s[2804]! } + public var TwoFactorSetup_Hint_Title: String { return self._s[2805]! } + public func Notification_Joined(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[2806]!, self._r[2806]!, [_0]) + } + public var Group_Username_RemoveExistingUsernamesInfo: String { return self._s[2807]! } + public var PrivacyLastSeenSettings_CustomShareSettings_Delete: String { return self._s[2808]! } + public var AutoNightTheme_Scheduled: String { return self._s[2809]! } + public var CreatePoll_ExplanationHeader: String { return self._s[2810]! } + public var Calls_TabTitle: String { return self._s[2811]! } + public var VoiceChat_RecordingInProgress: String { return self._s[2812]! } + public var ChatList_UndoArchiveHiddenText: String { return self._s[2813]! } + public var Notification_VideoCallCanceled: String { return self._s[2814]! } + public var Login_CodeSentInternal: String { return self._s[2815]! } + public var SettingsSearch_Synonyms_Proxy_AddProxy: String { return self._s[2816]! } + public var Call_RecordingDisabledMessage: String { return self._s[2818]! } + public func VoiceChat_RemovedPeerText(_ _0: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(self._s[2819]!, self._r[2819]!, [_0]) } - public var AutoDownloadSettings_TypeGroupChats: String { return self._s[2820]! } - public var VoiceOver_Chat_GroupInfo: String { return self._s[2821]! } - public var DialogList_SavedMessagesTooltip: String { return self._s[2823]! } - public var Update_Title: String { return self._s[2824]! } - public var Conversation_ShareMyPhoneNumber: String { return self._s[2825]! } - public var WallpaperPreview_CropTopText: String { return self._s[2828]! } - public var Channel_EditMessageErrorGeneric: String { return self._s[2829]! } - public var AccessDenied_LocationAlwaysDenied: String { return self._s[2830]! } - public var ChatListFolder_DiscardCancel: String { return self._s[2831]! } - public var Message_PinnedPhotoMessage: String { return self._s[2832]! } - public var Appearance_ThemeDayClassic: String { return self._s[2833]! } - public var SocksProxySetup_ProxySocks5: String { return self._s[2834]! } - public var VoiceChat_DisplayAsInfo: String { return self._s[2836]! } - public var AccessDenied_Wallpapers: String { return self._s[2841]! } + public var Conversation_UsersTooMuchError: String { return self._s[2821]! } + public var AutoDownloadSettings_TypeChannels: String { return self._s[2822]! } + public var VoiceChat_StopScreenSharingShort: String { return self._s[2823]! } + public var Channel_Info_Stickers: String { return self._s[2824]! } + public var Passport_DeleteAddressConfirmation: String { return self._s[2825]! } + public func Conversation_PeerNearbyDistance(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[2826]!, self._r[2826]!, [_1, _2]) + } + public var ChannelMembers_WhoCanAddMembers_Admins: String { return self._s[2827]! } + public func Call_StatusOngoing(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[2828]!, self._r[2828]!, [_0]) + } + public var Passport_DiscardMessageTitle: String { return self._s[2829]! } + public var Call_VoiceOver_VideoCallIncoming: String { return self._s[2830]! } + public var Localization_LanguageOther: String { return self._s[2831]! } + public var Conversation_EncryptionCanceled: String { return self._s[2832]! } + public var ChatSettings_AutomaticPhotoDownload: String { return self._s[2833]! } + public var ReportPeer_ReasonFake: String { return self._s[2835]! } + public func Notification_SecretChatMessageScreenshot(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[2836]!, self._r[2836]!, [_0]) + } + public var Target_InviteToGroupErrorAlreadyInvited: String { return self._s[2838]! } + public var SocksProxySetup_SavedProxies: String { return self._s[2839]! } + public var InviteLink_Create_UsersLimitNumberOfUsers: String { return self._s[2840]! } + public func ApplyLanguage_ChangeLanguageAlreadyActive(_ _1: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[2841]!, self._r[2841]!, [_1]) + } + public var Conversation_ScamWarning: String { return self._s[2843]! } + public var Channel_AdminLog_InfoPanelAlertTitle: String { return self._s[2844]! } + public var LocalGroup_Title: String { return self._s[2845]! } + public var SettingsSearch_Synonyms_Notifications_MessageNotificationsAlert: String { return self._s[2847]! } + public var SettingsSearch_Synonyms_Privacy_PasscodeAndFaceId: String { return self._s[2848]! } + public var VoiceChat_SelectAccount: String { return self._s[2849]! } + public var Login_PhoneFloodError: String { return self._s[2850]! } + public var Conversation_PinMessageAlert_PinAndNotifyMembers: String { return self._s[2851]! } + public var Username_InvalidTaken: String { return self._s[2853]! } + public var SocksProxySetup_AddProxy: String { return self._s[2855]! } + public var PrivacyLastSeenSettings_WhoCanSeeMyTimestamp: String { return self._s[2856]! } + public var MediaPicker_UngroupDescription: String { return self._s[2857]! } + public var Login_CodeExpired: String { return self._s[2858]! } + public var Localization_ChooseLanguage: String { return self._s[2859]! } + public var Checkout_NewCard_PostcodePlaceholder: String { return self._s[2860]! } + public func ChangePhone_ErrorOccupied(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[2861]!, self._r[2861]!, [_0]) + } + public func Channel_DiscussionGroup_HeaderSet(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[2862]!, self._r[2862]!, [_0]) + } + public var ReportPeer_ReasonOther_Title: String { return self._s[2864]! } + public var Conversation_ScheduleMessage_Title: String { return self._s[2865]! } + public func VoiceChat_UserInvited(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[2866]!, self._r[2866]!, [_0]) + } + public var PeerInfo_ButtonDiscuss: String { return self._s[2867]! } + public var SettingsSearch_Synonyms_Notifications_BadgeIncludeMutedPublicGroups: String { return self._s[2868]! } + public var Call_StatusNoAnswer: String { return self._s[2869]! } + public var ScheduledMessages_DeleteMany: String { return self._s[2871]! } + public var Channel_DiscussionGroupInfo: String { return self._s[2872]! } + public var Conversation_UnarchiveDone: String { return self._s[2873]! } + public var LogoutOptions_AddAccountText: String { return self._s[2874]! } + public var Message_PinnedContactMessage: String { return self._s[2875]! } + public func ChatList_DeleteAndLeaveGroupConfirmation(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[2876]!, self._r[2876]!, [_0]) + } + public var VoiceChat_EditBioTitle: String { return self._s[2878]! } + public func FileSize_GB(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[2879]!, self._r[2879]!, [_0]) + } + public var Stats_GroupLanguagesTitle: String { return self._s[2880]! } + public var Passport_FieldAddressHelp: String { return self._s[2881]! } + public func Passport_FieldOneOf_Or(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[2882]!, self._r[2882]!, [_1, _2]) + } + public var ChatSettings_OpenLinksIn: String { return self._s[2884]! } + public var TwoFactorSetup_Hint_SkipAction: String { return self._s[2885]! } + public var Message_Photo: String { return self._s[2886]! } + public var Media_LimitedAccessManage: String { return self._s[2888]! } + public var MediaPicker_AddCaption: String { return self._s[2889]! } + public var LogoutOptions_Title: String { return self._s[2890]! } + public func PUSH_PINNED_GIF(_ _1: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[2891]!, self._r[2891]!, [_1]) + } + public var Conversation_StatusKickedFromGroup: String { return self._s[2892]! } + public var Channel_AdminLogFilter_AdminsTitle: String { return self._s[2893]! } + public var ChatList_DeleteSavedMessagesConfirmationTitle: String { return self._s[2894]! } + public var Channel_AdminLogFilter_Title: String { return self._s[2895]! } + public var Passport_Address_TypeRentalAgreementUploadScan: String { return self._s[2896]! } + public var Compose_GroupTokenListPlaceholder: String { return self._s[2897]! } + public var Notifications_MessageNotificationsExceptions: String { return self._s[2898]! } + public var ChannelIntro_Title: String { return self._s[2899]! } + public var Stats_Message_Views: String { return self._s[2900]! } + public var Stickers_Install: String { return self._s[2901]! } + public func VoiceOver_Chat_FileFrom(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[2902]!, self._r[2902]!, [_0]) + } + public var EditTheme_Create_Preview_IncomingReplyText: String { return self._s[2903]! } + public var Conversation_SwipeToReplyHintTitle: String { return self._s[2905]! } + public var Settings_Username: String { return self._s[2908]! } + public var FastTwoStepSetup_Title: String { return self._s[2909]! } + public var Notifications_Badge_CountUnreadMessages_InfoOff: String { return self._s[2910]! } + public var SettingsSearch_Synonyms_Privacy_Title: String { return self._s[2911]! } + public var Passport_Identity_IssueDatePlaceholder: String { return self._s[2913]! } + public var CallFeedback_ReasonEcho: String { return self._s[2914]! } + public func Time_MonthOfYear_m1(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[2915]!, self._r[2915]!, [_0]) + } + public var Conversation_OpenBotLinkTitle: String { return self._s[2916]! } + public var SocksProxySetup_Title: String { return self._s[2917]! } + public var CallFeedback_Success: String { return self._s[2918]! } + public var WallpaperPreview_SwipeTopText: String { return self._s[2920]! } + public var InstantPage_AutoNightTheme: String { return self._s[2922]! } + public var Watch_Conversation_Reply: String { return self._s[2923]! } + public var VoiceChat_Share: String { return self._s[2925]! } + public var VoiceChat_AddPhoto: String { return self._s[2926]! } + public var Chat_PanelUnpinAllMessages: String { return self._s[2927]! } + public var WallpaperPreview_Pattern: String { return self._s[2928]! } + public var CheckoutInfo_ReceiverInfoEmail: String { return self._s[2929]! } + public func Conversation_DeleteMessagesFor(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[2930]!, self._r[2930]!, [_0]) + } + public var AutoDownloadSettings_TypeGroupChats: String { return self._s[2931]! } + public var VoiceOver_Chat_GroupInfo: String { return self._s[2932]! } + public var DialogList_SavedMessagesTooltip: String { return self._s[2934]! } + public var Update_Title: String { return self._s[2935]! } + public var Conversation_ShareMyPhoneNumber: String { return self._s[2936]! } + public var WallpaperPreview_CropTopText: String { return self._s[2939]! } + public var Channel_EditMessageErrorGeneric: String { return self._s[2940]! } + public var AccessDenied_LocationAlwaysDenied: String { return self._s[2941]! } + public var ChatListFolder_DiscardCancel: String { return self._s[2942]! } + public var Message_PinnedPhotoMessage: String { return self._s[2943]! } + public var Appearance_ThemeDayClassic: String { return self._s[2944]! } + public var VoiceChat_ChangeName: String { return self._s[2945]! } + public var SocksProxySetup_ProxySocks5: String { return self._s[2947]! } + public var VoiceChat_DisplayAsInfo: String { return self._s[2949]! } + public var AccessDenied_Wallpapers: String { return self._s[2954]! } public func Channel_AdminLog_MessageChangedGroupAbout(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2842]!, self._r[2842]!, [_0]) + return formatWithArgumentRanges(self._s[2955]!, self._r[2955]!, [_0]) } - public var Weekday_Sunday: String { return self._s[2843]! } - public var SettingsSearch_Synonyms_Privacy_GroupsAndChannels: String { return self._s[2845]! } - public var PeopleNearby_MakeVisibleDescription: String { return self._s[2846]! } - public var AccessDenied_LocationDisabled: String { return self._s[2847]! } - public var Tour_Text3: String { return self._s[2848]! } - public var AuthSessions_AddDevice_ScanTitle: String { return self._s[2849]! } + public var Weekday_Sunday: String { return self._s[2956]! } + public var SettingsSearch_Synonyms_Privacy_GroupsAndChannels: String { return self._s[2958]! } + public var PeopleNearby_MakeVisibleDescription: String { return self._s[2959]! } + public var AccessDenied_LocationDisabled: String { return self._s[2960]! } + public var Tour_Text3: String { return self._s[2961]! } + public var AuthSessions_AddDevice_ScanTitle: String { return self._s[2962]! } public func Time_TodayAt(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2850]!, self._r[2850]!, [_0]) + return formatWithArgumentRanges(self._s[2963]!, self._r[2963]!, [_0]) } - public var Privacy_SecretChatsLinkPreviewsHelp: String { return self._s[2851]! } - public var Conversation_ClearCache: String { return self._s[2852]! } - public var StickerPacksSettings_ArchivedMasks_Info: String { return self._s[2853]! } - public var ChatList_Tabs_AllChats: String { return self._s[2854]! } - public var DialogList_RecentTitlePeople: String { return self._s[2855]! } - public var Stickers_AddToFavorites: String { return self._s[2856]! } - public var ChatList_Context_RemoveFromFolder: String { return self._s[2857]! } - public var VoiceChat_CancelSpeakRequest: String { return self._s[2858]! } - public var Settings_RemoveVideo: String { return self._s[2859]! } - public var PhotoEditor_CropAspectRatioSquare: String { return self._s[2860]! } - public var ConversationProfile_LeaveDeleteAndExit: String { return self._s[2861]! } - public var VoiceOver_Chat_YourFile: String { return self._s[2862]! } - public var SettingsSearch_Synonyms_Privacy_Forwards: String { return self._s[2864]! } - public var Group_OwnershipTransfer_ErrorPrivacyRestricted: String { return self._s[2865]! } - public var Channel_AdminLog_AddMembers: String { return self._s[2866]! } - public var Map_SendThisLocation: String { return self._s[2868]! } - public var TwoStepAuth_EmailSkipAlert: String { return self._s[2870]! } - public var IntentsSettings_SuggestedChatsPrivateChats: String { return self._s[2871]! } - public var CloudStorage_Title: String { return self._s[2872]! } - public var TwoFactorSetup_Password_Action: String { return self._s[2873]! } - public var TwoStepAuth_ConfirmationText: String { return self._s[2874]! } - public var Passport_Address_EditTemporaryRegistration: String { return self._s[2876]! } - public var Undo_LeftGroup: String { return self._s[2877]! } - public var Conversation_StopLiveLocation: String { return self._s[2878]! } - public var NotificationSettings_ShowNotificationsFromAccountsSection: String { return self._s[2879]! } - public var Message_PinnedInvoice: String { return self._s[2880]! } - public var ApplyLanguage_LanguageNotSupportedError: String { return self._s[2881]! } + public var Privacy_SecretChatsLinkPreviewsHelp: String { return self._s[2964]! } + public var Conversation_ClearCache: String { return self._s[2965]! } + public var StickerPacksSettings_ArchivedMasks_Info: String { return self._s[2966]! } + public var ChatList_Tabs_AllChats: String { return self._s[2967]! } + public var DialogList_RecentTitlePeople: String { return self._s[2968]! } + public var Stickers_AddToFavorites: String { return self._s[2969]! } + public var ChatList_Context_RemoveFromFolder: String { return self._s[2970]! } + public var VoiceChat_CancelSpeakRequest: String { return self._s[2971]! } + public var Settings_RemoveVideo: String { return self._s[2972]! } + public var PhotoEditor_CropAspectRatioSquare: String { return self._s[2973]! } + public var ConversationProfile_LeaveDeleteAndExit: String { return self._s[2974]! } + public var VoiceOver_Chat_YourFile: String { return self._s[2975]! } + public var SettingsSearch_Synonyms_Privacy_Forwards: String { return self._s[2977]! } + public var Group_OwnershipTransfer_ErrorPrivacyRestricted: String { return self._s[2978]! } + public var VoiceChat_TapToAddBio: String { return self._s[2979]! } + public var Channel_AdminLog_AddMembers: String { return self._s[2980]! } + public var Map_SendThisLocation: String { return self._s[2982]! } + public var TwoStepAuth_EmailSkipAlert: String { return self._s[2984]! } + public var IntentsSettings_SuggestedChatsPrivateChats: String { return self._s[2985]! } + public var CloudStorage_Title: String { return self._s[2986]! } + public var TwoFactorSetup_Password_Action: String { return self._s[2987]! } + public var TwoStepAuth_ConfirmationText: String { return self._s[2988]! } + public var Passport_Address_EditTemporaryRegistration: String { return self._s[2990]! } + public var Undo_LeftGroup: String { return self._s[2991]! } + public var Conversation_StopLiveLocation: String { return self._s[2992]! } + public var NotificationSettings_ShowNotificationsFromAccountsSection: String { return self._s[2993]! } + public var Message_PinnedInvoice: String { return self._s[2994]! } + public var ApplyLanguage_LanguageNotSupportedError: String { return self._s[2995]! } public func PUSH_CHAT_MESSAGE(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2883]!, self._r[2883]!, [_1, _2]) + return formatWithArgumentRanges(self._s[2997]!, self._r[2997]!, [_1, _2]) } public func Notification_PinnedAudioMessage(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2884]!, self._r[2884]!, [_0]) + return formatWithArgumentRanges(self._s[2998]!, self._r[2998]!, [_0]) } - public var Weekday_Tuesday: String { return self._s[2885]! } - public var ChangePhoneNumberCode_Code: String { return self._s[2886]! } - public var VoiceOver_Chat_YourMessage: String { return self._s[2887]! } - public var Calls_CallTabDescription: String { return self._s[2888]! } - public var ChatImport_SelectionErrorNotAdmin: String { return self._s[2889]! } - public var SocksProxySetup_UseProxy: String { return self._s[2891]! } - public var SettingsSearch_Synonyms_Stickers_Title: String { return self._s[2892]! } - public var PasscodeSettings_AlphanumericCode: String { return self._s[2893]! } - public var VoiceOver_Chat_YourVideo: String { return self._s[2894]! } - public var ChannelMembers_WhoCanAddMembersAdminsHelp: String { return self._s[2896]! } - public var SettingsSearch_Synonyms_Privacy_DeleteAccountIfAwayFor: String { return self._s[2897]! } - public var Exceptions_AddToExceptions: String { return self._s[2898]! } - public var UserInfo_Title: String { return self._s[2899]! } - public var Passport_DeleteDocumentConfirmation: String { return self._s[2901]! } - public var ChatList_Unmute: String { return self._s[2903]! } - public var SettingsSearch_Synonyms_Privacy_Data_ContactsSync: String { return self._s[2904]! } + public var TwoStepAuth_RecoveryUnavailableResetTitle: String { return self._s[2999]! } + public var Weekday_Tuesday: String { return self._s[3000]! } + public var ChangePhoneNumberCode_Code: String { return self._s[3001]! } + public var VoiceOver_Chat_YourMessage: String { return self._s[3002]! } + public var Calls_CallTabDescription: String { return self._s[3003]! } + public var ChatImport_SelectionErrorNotAdmin: String { return self._s[3004]! } + public var SocksProxySetup_UseProxy: String { return self._s[3006]! } + public var SettingsSearch_Synonyms_Stickers_Title: String { return self._s[3007]! } + public var PasscodeSettings_AlphanumericCode: String { return self._s[3008]! } + public var VoiceOver_Chat_YourVideo: String { return self._s[3009]! } + public var ChannelMembers_WhoCanAddMembersAdminsHelp: String { return self._s[3011]! } + public var SettingsSearch_Synonyms_Privacy_DeleteAccountIfAwayFor: String { return self._s[3012]! } + public var Exceptions_AddToExceptions: String { return self._s[3013]! } + public var UserInfo_Title: String { return self._s[3014]! } + public var Passport_DeleteDocumentConfirmation: String { return self._s[3016]! } + public var VoiceChat_EditDescription: String { return self._s[3018]! } + public var ChatList_Unmute: String { return self._s[3019]! } + public var SettingsSearch_Synonyms_Privacy_Data_ContactsSync: String { return self._s[3020]! } public func Channel_AdminLog_MessageChangedAutoremoveTimeoutSet(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2905]!, self._r[2905]!, [_1, _2]) + return formatWithArgumentRanges(self._s[3021]!, self._r[3021]!, [_1, _2]) } - public var Stats_GroupTopPostersTitle: String { return self._s[2906]! } - public var Username_CheckingUsername: String { return self._s[2907]! } - public var WallpaperColors_SetCustomColor: String { return self._s[2908]! } - public var PeerSelection_ImportIntoNewGroup: String { return self._s[2912]! } - public var Location_ProximityAlertSetTitle: String { return self._s[2913]! } - public var AuthSessions_AddedDeviceTerminate: String { return self._s[2914]! } - public var Conversation_JoinVoiceChatAsSpeaker: String { return self._s[2915]! } - public var Privacy_ProfilePhoto_CustomHelp: String { return self._s[2916]! } - public var Settings_ChangePhoneNumber: String { return self._s[2917]! } - public var PeerInfo_PaneLinks: String { return self._s[2918]! } - public var Appearance_ThemePreview_ChatList_1_Text: String { return self._s[2921]! } - public var Channel_EditAdmin_PermissionInviteSubscribers: String { return self._s[2923]! } - public func PUSH_CHAT_VOICECHAT_INVITE_YOU(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2924]!, self._r[2924]!, [_1]) + public var Stats_GroupTopPostersTitle: String { return self._s[3022]! } + public var Username_CheckingUsername: String { return self._s[3024]! } + public var WallpaperColors_SetCustomColor: String { return self._s[3025]! } + public var PeerSelection_ImportIntoNewGroup: String { return self._s[3029]! } + public var Location_ProximityAlertSetTitle: String { return self._s[3030]! } + public var AuthSessions_AddedDeviceTerminate: String { return self._s[3031]! } + public var Conversation_JoinVoiceChatAsSpeaker: String { return self._s[3032]! } + public var Privacy_ProfilePhoto_CustomHelp: String { return self._s[3033]! } + public var Settings_ChangePhoneNumber: String { return self._s[3034]! } + public var PeerInfo_PaneLinks: String { return self._s[3035]! } + public var Appearance_ThemePreview_ChatList_1_Text: String { return self._s[3038]! } + public var Channel_EditAdmin_PermissionInviteSubscribers: String { return self._s[3040]! } + public func PUSH_CHAT_VOICECHAT_INVITE_YOU(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[3041]!, self._r[3041]!, [_1, _2]) } - public var LogoutOptions_ChangePhoneNumberText: String { return self._s[2925]! } - public var VoiceOver_Media_PlaybackPause: String { return self._s[2926]! } - public var BroadcastGroups_ConfirmationAlert_Title: String { return self._s[2927]! } - public var Stats_FollowersBySourceTitle: String { return self._s[2929]! } + public var LogoutOptions_ChangePhoneNumberText: String { return self._s[3042]! } + public var VoiceOver_Media_PlaybackPause: String { return self._s[3043]! } + public var VoiceChat_CancelConfirmationEnd: String { return self._s[3044]! } + public var BroadcastGroups_ConfirmationAlert_Title: String { return self._s[3045]! } + public var Stats_FollowersBySourceTitle: String { return self._s[3047]! } public func Conversation_ScheduleMessage_SendOn(_ _0: String, _ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2930]!, self._r[2930]!, [_0, _1]) + return formatWithArgumentRanges(self._s[3048]!, self._r[3048]!, [_0, _1]) } - public var Compose_NewEncryptedChatTitle: String { return self._s[2931]! } - public var Channel_CommentsGroup_Header: String { return self._s[2933]! } + public var Compose_NewEncryptedChatTitle: String { return self._s[3049]! } + public var Channel_CommentsGroup_Header: String { return self._s[3051]! } public func ShareFileTip_Text(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2937]!, self._r[2937]!, [_0]) + return formatWithArgumentRanges(self._s[3055]!, self._r[3055]!, [_0]) } public func PUSH_MESSAGE_AUDIO(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2938]!, self._r[2938]!, [_1]) + return formatWithArgumentRanges(self._s[3056]!, self._r[3056]!, [_1]) } - public var Group_Setup_BasicHistoryHiddenHelp: String { return self._s[2940]! } + public var Group_Setup_BasicHistoryHiddenHelp: String { return self._s[3058]! } public func TwoStepAuth_RecoveryEmailUnavailable(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2941]!, self._r[2941]!, [_0]) - } - public var Conversation_ReportMessages: String { return self._s[2942]! } - public var Conversation_OpenBotLinkOpen: String { return self._s[2943]! } - public var VoiceOver_Chat_RecordModeVoiceMessage: String { return self._s[2944]! } - public var PrivacySettings_LastSeen: String { return self._s[2946]! } - public var SettingsSearch_Synonyms_Privacy_Passcode: String { return self._s[2947]! } - public var Theme_Colors_Proceed: String { return self._s[2948]! } - public var UserInfo_ScamBotWarning: String { return self._s[2949]! } - public var LogoutOptions_LogOut: String { return self._s[2951]! } - public var Conversation_SendMessage: String { return self._s[2952]! } - public var Conversation_CancelForwardCancelForward: String { return self._s[2953]! } - public var Passport_Address_Region: String { return self._s[2955]! } - public var MediaPicker_CameraRoll: String { return self._s[2957]! } - public func VoiceOver_Chat_ForwardedFrom(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2959]!, self._r[2959]!, [_0]) - } - public var Call_ReportSend: String { return self._s[2961]! } - public var VoiceOver_ChatList_Message: String { return self._s[2962]! } - public var Month_ShortJune: String { return self._s[2963]! } - public var AutoDownloadSettings_GroupChats: String { return self._s[2964]! } - public func Channel_AdminLog_CaptionEdited(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2967]!, self._r[2967]!, [_0]) - } - public var TwoStepAuth_DisableSuccess: String { return self._s[2968]! } - public var Cache_KeepMedia: String { return self._s[2969]! } - public func Date_ChatDateHeaderYear(_ _1: String, _ _2: String, _ _3: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2970]!, self._r[2970]!, [_1, _2, _3]) - } - public var Appearance_LargeEmoji: String { return self._s[2971]! } - public func Notification_NewAuthDetected(_ _1: String, _ _2: String, _ _3: String, _ _4: String, _ _5: String, _ _6: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2972]!, self._r[2972]!, [_1, _2, _3, _4, _5, _6]) - } - public var Chat_AttachmentMultipleForwardDisabled: String { return self._s[2973]! } - public var Call_CameraConfirmationText: String { return self._s[2974]! } - public func AuthSessions_AppUnofficial(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2976]!, self._r[2976]!, [_0]) - } - public var DialogList_SearchSectionChats: String { return self._s[2977]! } - public var VoiceOver_MessageContextReport: String { return self._s[2979]! } - public var VoiceChat_RemovePeer: String { return self._s[2980]! } - public var ChatListFolder_ExcludeChatsTitle: String { return self._s[2981]! } - public var InviteLink_ContextCopy: String { return self._s[2982]! } - public var NotificationsSound_Tritone: String { return self._s[2984]! } - public var Notifications_InAppNotificationsPreview: String { return self._s[2987]! } - public var Stats_GroupTopAdmin_Actions: String { return self._s[2988]! } - public var PeerInfo_AddToContacts: String { return self._s[2989]! } - public var VoiceChat_OpenChat: String { return self._s[2990]! } - public var AccessDenied_Title: String { return self._s[2991]! } - public var InviteLink_QRCode_InfoChannel: String { return self._s[2992]! } - public var Tour_Title1: String { return self._s[2993]! } - public var VoiceOver_AttachMedia: String { return self._s[2994]! } - public func SharedMedia_SearchNoResultsDescription(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2996]!, self._r[2996]!, [_0]) - } - public var Chat_Gifs_SavedSectionHeader: String { return self._s[2997]! } - public var LogoutOptions_ChangePhoneNumberTitle: String { return self._s[2998]! } - public func Passport_Scans_ScanIndex(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[2999]!, self._r[2999]!, [_0]) - } - public var Channel_AdminLog_MessagePreviousLink: String { return self._s[3000]! } - public var OldChannels_Title: String { return self._s[3001]! } - public var LoginPassword_FloodError: String { return self._s[3002]! } - public var ChatImportActivity_InProgress: String { return self._s[3004]! } - public var Checkout_ErrorPaymentFailed: String { return self._s[3005]! } - public func Time_MonthOfYear_m7(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3006]!, self._r[3006]!, [_0]) - } - public var VoiceOver_Media_PlaybackPlay: String { return self._s[3009]! } - public var Passport_CorrectErrors: String { return self._s[3011]! } - public func PUSH_CHAT_PHOTO_EDITED(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3012]!, self._r[3012]!, [_1, _2]) - } - public var ChatListFolderSettings_Title: String { return self._s[3013]! } - public func AutoDownloadSettings_UpToFor(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3014]!, self._r[3014]!, [_1, _2]) - } - public var PhotoEditor_HighlightsTool: String { return self._s[3015]! } - public var Contacts_NotRegisteredSection: String { return self._s[3018]! } - public func Call_VoiceChatInProgressCallMessage(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3019]!, self._r[3019]!, [_1, _2]) - } - public func PUSH_PINNED_DOC(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3020]!, self._r[3020]!, [_1]) - } - public var InviteLink_Create_UsersLimitInfo: String { return self._s[3021]! } - public var User_DeletedAccount: String { return self._s[3022]! } - public var Conversation_ViewContactDetails: String { return self._s[3023]! } - public var Conversation_Dice_u1F3B3: String { return self._s[3024]! } - public var WebSearch_GIFs: String { return self._s[3025]! } - public var ChatList_DeleteSavedMessagesConfirmationAction: String { return self._s[3026]! } - public var Appearance_PreviewOutgoingText: String { return self._s[3027]! } - public var Calls_CallTabTitle: String { return self._s[3028]! } - public var Call_VoiceChatInProgressTitle: String { return self._s[3029]! } - public func LastSeen_YesterdayAt(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3030]!, self._r[3030]!, [_0]) - } - public var Channel_Status: String { return self._s[3031]! } - public var Conversation_SendMessageErrorGroupRestricted: String { return self._s[3033]! } - public var VoiceOver_Chat_OptionSelected: String { return self._s[3034]! } - public var SettingsSearch_Synonyms_Notifications_ChannelNotificationsAlert: String { return self._s[3035]! } - public func ClearCache_Success(_ _0: String, _ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3036]!, self._r[3036]!, [_0, _1]) - } - public var Passport_Identity_ExpiryDateNone: String { return self._s[3038]! } - public var Your_cards_expiration_month_is_invalid: String { return self._s[3040]! } - public var Month_ShortDecember: String { return self._s[3041]! } - public var Username_Help: String { return self._s[3042]! } - public var Login_InfoAvatarAdd: String { return self._s[3043]! } - public var Month_ShortMay: String { return self._s[3044]! } - public var DialogList_UnknownPinLimitError: String { return self._s[3045]! } - public var PasscodeSettings_AutoLock_IfAwayFor_5hours: String { return self._s[3046]! } - public var TwoStepAuth_EnabledSuccess: String { return self._s[3047]! } - public var VoiceChat_AskedToSpeak: String { return self._s[3048]! } - public var Weekday_ShortSunday: String { return self._s[3049]! } - public var Channel_Username_InvalidTooShort: String { return self._s[3050]! } - public var AuthSessions_TerminateSession: String { return self._s[3051]! } - public var Passport_Identity_FilesTitle: String { return self._s[3052]! } - public func Notification_PinnedRoundMessage(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3053]!, self._r[3053]!, [_0]) - } - public var PeopleNearby_MakeVisible: String { return self._s[3055]! } - public func Conversation_RestrictedMediaTimed(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3056]!, self._r[3056]!, [_0]) - } - public var Widget_UpdatedAt: String { return self._s[3057]! } - public func Notification_MessageLifetimeChanged(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3058]!, self._r[3058]!, [_1, _2]) - } - public func GroupInfo_AddParticipantConfirmation(_ _0: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(self._s[3059]!, self._r[3059]!, [_0]) } - public var PrivacyPolicy_DeclineDeclineAndDelete: String { return self._s[3060]! } - public var Conversation_ContextMenuForward: String { return self._s[3061]! } - public var Channel_AdminLog_CanManageCalls: String { return self._s[3062]! } - public func PUSH_CHAT_MESSAGE_QUIZ(_ _1: String, _ _2: String, _ _3: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3064]!, self._r[3064]!, [_1, _2, _3]) - } - public var Notification_GroupInviterSelf: String { return self._s[3066]! } - public var Privacy_Forwards_NeverLink: String { return self._s[3067]! } - public var AuthSessions_CurrentSession: String { return self._s[3068]! } - public var Passport_Address_EditPassportRegistration: String { return self._s[3069]! } - public var ChannelInfo_DeleteChannelConfirmation: String { return self._s[3070]! } - public var ChatSearch_ResultsTooltip: String { return self._s[3072]! } - public var CheckoutInfo_Pay: String { return self._s[3073]! } - public func Conversation_PinMessagesFor(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3075]!, self._r[3075]!, [_0]) - } - public var GroupInfo_AddParticipant: String { return self._s[3076]! } - public var GroupPermission_ApplyAlertAction: String { return self._s[3077]! } - public func Channel_AdminLog_MessageChangedChannelUsername(_ _0: String) -> (String, [(Int, NSRange)]) { + public var Conversation_ReportMessages: String { return self._s[3060]! } + public var Conversation_OpenBotLinkOpen: String { return self._s[3061]! } + public var VoiceOver_Chat_RecordModeVoiceMessage: String { return self._s[3062]! } + public var PrivacySettings_LastSeen: String { return self._s[3064]! } + public var SettingsSearch_Synonyms_Privacy_Passcode: String { return self._s[3065]! } + public var Theme_Colors_Proceed: String { return self._s[3066]! } + public var UserInfo_ScamBotWarning: String { return self._s[3067]! } + public var LogoutOptions_LogOut: String { return self._s[3069]! } + public var Conversation_SendMessage: String { return self._s[3070]! } + public var Conversation_CancelForwardCancelForward: String { return self._s[3071]! } + public var VoiceChat_Scheduled: String { return self._s[3073]! } + public var Passport_Address_Region: String { return self._s[3074]! } + public var MediaPicker_CameraRoll: String { return self._s[3076]! } + public func VoiceOver_Chat_ForwardedFrom(_ _0: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(self._s[3078]!, self._r[3078]!, [_0]) } - public var Localization_LanguageCustom: String { return self._s[3079]! } - public var SettingsSearch_Synonyms_Passport: String { return self._s[3080]! } - public var Settings_UsernameEmpty: String { return self._s[3081]! } - public var Settings_FAQ_URL: String { return self._s[3082]! } - public var ChatList_UndoArchiveText1: String { return self._s[3083]! } - public var Common_Select: String { return self._s[3085]! } - public var Notification_MessageLifetimeRemovedOutgoing: String { return self._s[3086]! } - public var Notification_PassportValueAddress: String { return self._s[3087]! } - public var Conversation_MessageDialogDelete: String { return self._s[3088]! } - public var Map_OpenInYandexNavigator: String { return self._s[3090]! } - public var DialogList_SearchSectionDialogs: String { return self._s[3091]! } - public var AccessDenied_Contacts: String { return self._s[3092]! } - public var SettingsSearch_Synonyms_Privacy_Data_DeleteDrafts: String { return self._s[3094]! } - public var Passport_ScanPassportHelp: String { return self._s[3095]! } - public var Chat_PinnedListPreview_HidePinnedMessages: String { return self._s[3096]! } - public var ChatListFolder_NameChannels: String { return self._s[3097]! } - public var Appearance_ThemePreview_Chat_5_Text: String { return self._s[3098]! } + public var Call_ReportSend: String { return self._s[3080]! } + public var VoiceOver_ChatList_Message: String { return self._s[3081]! } + public var Month_ShortJune: String { return self._s[3082]! } + public var AutoDownloadSettings_GroupChats: String { return self._s[3083]! } + public func Channel_AdminLog_CaptionEdited(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[3086]!, self._r[3086]!, [_0]) + } + public var TwoStepAuth_DisableSuccess: String { return self._s[3087]! } + public var Cache_KeepMedia: String { return self._s[3088]! } + public func Date_ChatDateHeaderYear(_ _1: String, _ _2: String, _ _3: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[3089]!, self._r[3089]!, [_1, _2, _3]) + } + public var Appearance_LargeEmoji: String { return self._s[3090]! } + public func Notification_NewAuthDetected(_ _1: String, _ _2: String, _ _3: String, _ _4: String, _ _5: String, _ _6: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[3091]!, self._r[3091]!, [_1, _2, _3, _4, _5, _6]) + } + public var Chat_AttachmentMultipleForwardDisabled: String { return self._s[3092]! } + public var Privacy_PaymentsClear_PaymentInfoCleared: String { return self._s[3093]! } + public var Call_CameraConfirmationText: String { return self._s[3094]! } + public func AuthSessions_AppUnofficial(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[3096]!, self._r[3096]!, [_0]) + } + public var DialogList_SearchSectionChats: String { return self._s[3097]! } + public var VoiceOver_MessageContextReport: String { return self._s[3099]! } + public var VoiceChat_RemovePeer: String { return self._s[3100]! } + public var ChatListFolder_ExcludeChatsTitle: String { return self._s[3101]! } + public var InviteLink_ContextCopy: String { return self._s[3102]! } + public var NotificationsSound_Tritone: String { return self._s[3104]! } + public var VoiceChat_YouAreSharingScreen: String { return self._s[3106]! } + public var Notifications_InAppNotificationsPreview: String { return self._s[3108]! } + public var Stats_GroupTopAdmin_Actions: String { return self._s[3109]! } + public var TwoFactorSetup_PasswordRecovery_SkipAlertText: String { return self._s[3110]! } + public var TwoStepAuth_ResetAction: String { return self._s[3111]! } + public var PeerInfo_AddToContacts: String { return self._s[3112]! } + public var VoiceChat_OpenChat: String { return self._s[3113]! } + public var AccessDenied_Title: String { return self._s[3114]! } + public var InviteLink_QRCode_InfoChannel: String { return self._s[3115]! } + public var Tour_Title1: String { return self._s[3116]! } + public var VoiceOver_AttachMedia: String { return self._s[3117]! } + public func SharedMedia_SearchNoResultsDescription(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[3119]!, self._r[3119]!, [_0]) + } + public var Chat_Gifs_SavedSectionHeader: String { return self._s[3120]! } + public var Privacy_DeleteDrafts_DraftsDeleted: String { return self._s[3121]! } + public var LogoutOptions_ChangePhoneNumberTitle: String { return self._s[3122]! } + public func Passport_Scans_ScanIndex(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[3123]!, self._r[3123]!, [_0]) + } + public var Channel_AdminLog_MessagePreviousLink: String { return self._s[3124]! } + public var OldChannels_Title: String { return self._s[3125]! } + public var LoginPassword_FloodError: String { return self._s[3126]! } + public var ChatImportActivity_InProgress: String { return self._s[3128]! } + public var Checkout_ErrorPaymentFailed: String { return self._s[3129]! } + public func Time_MonthOfYear_m7(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[3130]!, self._r[3130]!, [_0]) + } + public var VoiceOver_Media_PlaybackPlay: String { return self._s[3133]! } + public var Passport_CorrectErrors: String { return self._s[3135]! } + public func PUSH_CHAT_PHOTO_EDITED(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[3136]!, self._r[3136]!, [_1, _2]) + } + public var ChatListFolderSettings_Title: String { return self._s[3137]! } + public func AutoDownloadSettings_UpToFor(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[3138]!, self._r[3138]!, [_1, _2]) + } + public var PhotoEditor_HighlightsTool: String { return self._s[3139]! } + public var Contacts_NotRegisteredSection: String { return self._s[3142]! } + public func Call_VoiceChatInProgressCallMessage(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[3143]!, self._r[3143]!, [_1, _2]) + } + public func PUSH_PINNED_DOC(_ _1: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[3144]!, self._r[3144]!, [_1]) + } + public var InviteLink_Create_UsersLimitInfo: String { return self._s[3145]! } + public var User_DeletedAccount: String { return self._s[3146]! } + public var Conversation_ViewContactDetails: String { return self._s[3147]! } + public var Conversation_Dice_u1F3B3: String { return self._s[3148]! } + public var WebSearch_GIFs: String { return self._s[3149]! } + public var ChatList_DeleteSavedMessagesConfirmationAction: String { return self._s[3150]! } + public var Appearance_PreviewOutgoingText: String { return self._s[3151]! } + public var Calls_CallTabTitle: String { return self._s[3152]! } + public var Call_VoiceChatInProgressTitle: String { return self._s[3153]! } + public var Checkout_OptionalTipItem: String { return self._s[3154]! } + public func LastSeen_YesterdayAt(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[3155]!, self._r[3155]!, [_0]) + } + public var Channel_Status: String { return self._s[3156]! } + public var Conversation_SendMessageErrorGroupRestricted: String { return self._s[3158]! } + public var VoiceOver_Chat_OptionSelected: String { return self._s[3159]! } + public var SettingsSearch_Synonyms_Notifications_ChannelNotificationsAlert: String { return self._s[3160]! } + public func ClearCache_Success(_ _0: String, _ _1: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[3161]!, self._r[3161]!, [_0, _1]) + } + public var Passport_Identity_ExpiryDateNone: String { return self._s[3163]! } + public var Your_cards_expiration_month_is_invalid: String { return self._s[3165]! } + public var Month_ShortDecember: String { return self._s[3166]! } + public var Username_Help: String { return self._s[3167]! } + public var Login_InfoAvatarAdd: String { return self._s[3168]! } + public var Month_ShortMay: String { return self._s[3169]! } + public var DialogList_UnknownPinLimitError: String { return self._s[3170]! } + public var PasscodeSettings_AutoLock_IfAwayFor_5hours: String { return self._s[3171]! } + public var TwoStepAuth_EnabledSuccess: String { return self._s[3172]! } + public var VoiceChat_StopScreenSharing: String { return self._s[3173]! } + public var VoiceChat_AskedToSpeak: String { return self._s[3174]! } + public var Weekday_ShortSunday: String { return self._s[3175]! } + public var Channel_Username_InvalidTooShort: String { return self._s[3176]! } + public var AuthSessions_TerminateSession: String { return self._s[3177]! } + public var Passport_Identity_FilesTitle: String { return self._s[3178]! } + public func Notification_PinnedRoundMessage(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[3179]!, self._r[3179]!, [_0]) + } + public var PeopleNearby_MakeVisible: String { return self._s[3181]! } + public func Conversation_RestrictedMediaTimed(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[3182]!, self._r[3182]!, [_0]) + } + public var Widget_UpdatedAt: String { return self._s[3183]! } + public func Notification_MessageLifetimeChanged(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[3184]!, self._r[3184]!, [_1, _2]) + } + public func GroupInfo_AddParticipantConfirmation(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[3185]!, self._r[3185]!, [_0]) + } + public var PrivacyPolicy_DeclineDeclineAndDelete: String { return self._s[3186]! } + public var VoiceChat_VideoPreviewShareScreen: String { return self._s[3187]! } + public var ImportStickerPack_ChooseStickerSet: String { return self._s[3189]! } + public var Conversation_ContextMenuForward: String { return self._s[3190]! } + public var Channel_AdminLog_CanManageCalls: String { return self._s[3191]! } + public func PUSH_CHAT_MESSAGE_QUIZ(_ _1: String, _ _2: String, _ _3: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[3193]!, self._r[3193]!, [_1, _2, _3]) + } + public var Notification_GroupInviterSelf: String { return self._s[3195]! } + public var Privacy_Forwards_NeverLink: String { return self._s[3196]! } + public var AuthSessions_CurrentSession: String { return self._s[3197]! } + public var Passport_Address_EditPassportRegistration: String { return self._s[3198]! } + public var ChannelInfo_DeleteChannelConfirmation: String { return self._s[3199]! } + public var ChatSearch_ResultsTooltip: String { return self._s[3201]! } + public var CheckoutInfo_Pay: String { return self._s[3202]! } + public func Conversation_PinMessagesFor(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[3204]!, self._r[3204]!, [_0]) + } + public var GroupInfo_AddParticipant: String { return self._s[3205]! } + public var GroupPermission_ApplyAlertAction: String { return self._s[3206]! } + public func Channel_AdminLog_MessageChangedChannelUsername(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[3207]!, self._r[3207]!, [_0]) + } + public var Localization_LanguageCustom: String { return self._s[3208]! } + public var SettingsSearch_Synonyms_Passport: String { return self._s[3209]! } + public var Settings_UsernameEmpty: String { return self._s[3210]! } + public var Settings_FAQ_URL: String { return self._s[3211]! } + public var ChatList_UndoArchiveText1: String { return self._s[3212]! } + public var Common_Select: String { return self._s[3214]! } + public var Notification_MessageLifetimeRemovedOutgoing: String { return self._s[3215]! } + public var Notification_PassportValueAddress: String { return self._s[3216]! } + public var Conversation_MessageDialogDelete: String { return self._s[3217]! } + public var Map_OpenInYandexNavigator: String { return self._s[3219]! } + public var DialogList_SearchSectionDialogs: String { return self._s[3220]! } + public var AccessDenied_Contacts: String { return self._s[3221]! } + public var SettingsSearch_Synonyms_Privacy_Data_DeleteDrafts: String { return self._s[3223]! } + public var Passport_ScanPassportHelp: String { return self._s[3224]! } + public var Chat_PinnedListPreview_HidePinnedMessages: String { return self._s[3225]! } + public var ChatListFolder_NameChannels: String { return self._s[3226]! } + public var Appearance_ThemePreview_Chat_5_Text: String { return self._s[3227]! } public func Channel_OwnershipTransfer_TransferCompleted(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3099]!, self._r[3099]!, [_1, _2]) + return formatWithArgumentRanges(self._s[3228]!, self._r[3228]!, [_1, _2]) } - public var Checkout_ErrorInvoiceAlreadyPaid: String { return self._s[3100]! } + public var Checkout_ErrorInvoiceAlreadyPaid: String { return self._s[3229]! } public func VoiceChat_InviteMemberToGroupFirstText(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3101]!, self._r[3101]!, [_1, _2]) + return formatWithArgumentRanges(self._s[3230]!, self._r[3230]!, [_1, _2]) } - public var Conversation_GifTooltip: String { return self._s[3102]! } - public var Widget_MessageAutoremoveTimerUpdated: String { return self._s[3103]! } - public var Passport_Identity_TypeDriversLicenseUploadScan: String { return self._s[3105]! } - public var VoiceChat_Connecting: String { return self._s[3106]! } - public var AutoDownloadSettings_OffForAll: String { return self._s[3107]! } + public var Conversation_GifTooltip: String { return self._s[3231]! } + public var Widget_MessageAutoremoveTimerUpdated: String { return self._s[3232]! } + public var Passport_Identity_TypeDriversLicenseUploadScan: String { return self._s[3234]! } + public var VoiceChat_Connecting: String { return self._s[3235]! } + public var AutoDownloadSettings_OffForAll: String { return self._s[3236]! } public func Channel_AdminLog_CreatedInviteLink(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3108]!, self._r[3108]!, [_1, _2]) + return formatWithArgumentRanges(self._s[3237]!, self._r[3237]!, [_1, _2]) } - public var Privacy_GroupsAndChannels_InviteToChannelMultipleError: String { return self._s[3109]! } - public var AutoDownloadSettings_PreloadVideo: String { return self._s[3110]! } - public var CreatePoll_Quiz: String { return self._s[3111]! } - public var TwoFactorSetup_Email_Placeholder: String { return self._s[3113]! } - public var Watch_Message_Invoice: String { return self._s[3114]! } - public var Settings_AddAnotherAccount_Help: String { return self._s[3115]! } - public var Watch_Message_Unsupported: String { return self._s[3116]! } + public var Privacy_GroupsAndChannels_InviteToChannelMultipleError: String { return self._s[3238]! } + public var AutoDownloadSettings_PreloadVideo: String { return self._s[3239]! } + public var CreatePoll_Quiz: String { return self._s[3240]! } + public var TwoFactorSetup_Email_Placeholder: String { return self._s[3242]! } + public var Watch_Message_Invoice: String { return self._s[3243]! } + public var Settings_AddAnotherAccount_Help: String { return self._s[3244]! } + public var Watch_Message_Unsupported: String { return self._s[3245]! } public func Call_CameraOff(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3118]!, self._r[3118]!, [_0]) + return formatWithArgumentRanges(self._s[3247]!, self._r[3247]!, [_0]) } - public var AuthSessions_TerminateOtherSessions: String { return self._s[3119]! } - public var CreatePoll_AllOptionsAdded: String { return self._s[3121]! } - public var TwoStepAuth_RecoveryEmailTitle: String { return self._s[3122]! } - public var Call_IncomingVoiceCall: String { return self._s[3123]! } + public var AuthSessions_TerminateOtherSessions: String { return self._s[3248]! } + public var CreatePoll_AllOptionsAdded: String { return self._s[3250]! } + public var TwoStepAuth_RecoveryEmailTitle: String { return self._s[3251]! } + public var Call_IncomingVoiceCall: String { return self._s[3252]! } public func Channel_AdminLog_MessageTransferedNameUsername(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3124]!, self._r[3124]!, [_1, _2]) + return formatWithArgumentRanges(self._s[3253]!, self._r[3253]!, [_1, _2]) } - public var PrivacySettings_DeleteAccountHelp: String { return self._s[3125]! } - public var Passport_Address_TypePassportRegistrationUploadScan: String { return self._s[3126]! } - public var Group_EditAdmin_RankOwnerPlaceholder: String { return self._s[3127]! } - public var Group_ErrorAccessDenied: String { return self._s[3128]! } - public var PasscodeSettings_HelpTop: String { return self._s[3129]! } - public var Watch_ChatList_NoConversationsTitle: String { return self._s[3130]! } - public var AddContact_SharedContactException: String { return self._s[3131]! } - public var AccessDenied_MicrophoneRestricted: String { return self._s[3132]! } - public var Privacy_TopPeers: String { return self._s[3133]! } - public var Web_OpenExternal: String { return self._s[3134]! } - public var Group_ErrorSendRestrictedStickers: String { return self._s[3135]! } - public var Channel_Management_LabelAdministrator: String { return self._s[3136]! } + public var PrivacySettings_DeleteAccountHelp: String { return self._s[3254]! } + public var Passport_Address_TypePassportRegistrationUploadScan: String { return self._s[3255]! } + public var Group_EditAdmin_RankOwnerPlaceholder: String { return self._s[3256]! } + public var Group_ErrorAccessDenied: String { return self._s[3257]! } + public var PasscodeSettings_HelpTop: String { return self._s[3258]! } + public var Watch_ChatList_NoConversationsTitle: String { return self._s[3259]! } + public var AddContact_SharedContactException: String { return self._s[3260]! } + public var AccessDenied_MicrophoneRestricted: String { return self._s[3261]! } + public var Privacy_TopPeers: String { return self._s[3262]! } + public var Web_OpenExternal: String { return self._s[3263]! } + public var Group_ErrorSendRestrictedStickers: String { return self._s[3264]! } + public var Channel_Management_LabelAdministrator: String { return self._s[3265]! } public func ChangePhoneNumberCode_CallTimer(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3137]!, self._r[3137]!, [_0]) + return formatWithArgumentRanges(self._s[3266]!, self._r[3266]!, [_0]) } - public var Conversation_PhoneCopied: String { return self._s[3138]! } - public var Permissions_Skip: String { return self._s[3139]! } - public var Notifications_GroupNotificationsExceptions: String { return self._s[3140]! } + public var Conversation_PhoneCopied: String { return self._s[3267]! } + public var Permissions_Skip: String { return self._s[3268]! } + public var Notifications_GroupNotificationsExceptions: String { return self._s[3269]! } public func VoiceChat_ForwardTooltip_TwoChats(_ _0: String, _ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3141]!, self._r[3141]!, [_0, _1]) + return formatWithArgumentRanges(self._s[3270]!, self._r[3270]!, [_0, _1]) } - public var PeopleNearby_Title: String { return self._s[3142]! } - public var GroupInfo_SharedMediaNone: String { return self._s[3143]! } + public var PeopleNearby_Title: String { return self._s[3271]! } + public var GroupInfo_SharedMediaNone: String { return self._s[3272]! } public func PUSH_MESSAGE_GEOLIVE(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3145]!, self._r[3145]!, [_1]) + return formatWithArgumentRanges(self._s[3274]!, self._r[3274]!, [_1]) } - public var Profile_MessageLifetime1w: String { return self._s[3146]! } + public var Profile_MessageLifetime1w: String { return self._s[3275]! } public func Time_PreciseDate_m6(_ _1: String, _ _2: String, _ _3: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3147]!, self._r[3147]!, [_1, _2, _3]) + return formatWithArgumentRanges(self._s[3276]!, self._r[3276]!, [_1, _2, _3]) } - public var WebBrowser_DefaultBrowser: String { return self._s[3148]! } - public var Conversation_PinOlderMessageAlertTitle: String { return self._s[3150]! } - public var EditTheme_Edit_BottomInfo: String { return self._s[3151]! } - public var Privacy_Forwards_Preview: String { return self._s[3152]! } - public var Settings_EditAccount: String { return self._s[3153]! } + public var WebBrowser_DefaultBrowser: String { return self._s[3277]! } + public var Conversation_PinOlderMessageAlertTitle: String { return self._s[3279]! } + public var EditTheme_Edit_BottomInfo: String { return self._s[3280]! } + public var Privacy_Forwards_Preview: String { return self._s[3281]! } + public var Settings_EditAccount: String { return self._s[3282]! } public func Conversation_RestrictedInlineTimed(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3154]!, self._r[3154]!, [_0]) + return formatWithArgumentRanges(self._s[3283]!, self._r[3283]!, [_0]) } - public var TwoFactorSetup_Intro_Title: String { return self._s[3155]! } + public var TwoFactorSetup_Intro_Title: String { return self._s[3284]! } public func Channel_AdminLog_MessagePromotedName(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3157]!, self._r[3157]!, [_1]) + return formatWithArgumentRanges(self._s[3286]!, self._r[3286]!, [_1]) } - public var PeerInfo_ButtonVideoCall: String { return self._s[3158]! } + public var PeerInfo_ButtonVideoCall: String { return self._s[3287]! } public func DialogList_SingleUploadingPhotoSuffix(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3159]!, self._r[3159]!, [_0]) - } - public var Login_InfoHelp: String { return self._s[3160]! } - public var Notification_SecretChatMessageScreenshotSelf: String { return self._s[3161]! } - public var VoiceChat_SpeakPermissionEveryone: String { return self._s[3162]! } - public var Profile_MessageLifetime1d: String { return self._s[3163]! } - public var Group_UpgradeConfirmation: String { return self._s[3164]! } - public func PUSH_PINNED_STICKER(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3165]!, self._r[3165]!, [_1, _2]) - } - public var Appearance_RemoveThemeColor: String { return self._s[3166]! } - public var Channel_AdminLog_TitleSelectedEvents: String { return self._s[3167]! } - public func Call_AnsweringWithAccount(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3168]!, self._r[3168]!, [_0]) - } - public var UserInfo_BotSettings: String { return self._s[3169]! } - public func Notification_ChannelInviter(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3171]!, self._r[3171]!, [_0]) - } - public var Permissions_ContactsText_v0: String { return self._s[3172]! } - public var Conversation_PinMessagesForMe: String { return self._s[3173]! } - public var VoiceChat_PanelJoin: String { return self._s[3174]! } - public var Conversation_DiscussionStarted: String { return self._s[3176]! } - public var SettingsSearch_Synonyms_Privacy_TwoStepAuth: String { return self._s[3177]! } - public var SharedMedia_SearchNoResults: String { return self._s[3179]! } - public func Login_EmailPhoneSubject(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3181]!, self._r[3181]!, [_0]) - } - public func Conversation_ShareMyPhoneNumber_StatusSuccess(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3183]!, self._r[3183]!, [_0]) - } - public var ReportPeer_ReasonOther_Placeholder: String { return self._s[3184]! } - public var ContactInfo_PhoneLabelHomeFax: String { return self._s[3185]! } - public var Call_AudioRouteHeadphones: String { return self._s[3186]! } - public func PUSH_AUTH_UNKNOWN(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3188]!, self._r[3188]!, [_1]) - } - public var Passport_Identity_FilesView: String { return self._s[3189]! } - public var TwoStepAuth_SetupEmail: String { return self._s[3190]! } - public var Widget_ApplicationStartRequired: String { return self._s[3191]! } - public var PhotoEditor_Original: String { return self._s[3192]! } - public var Call_YourMicrophoneOff: String { return self._s[3193]! } - public var Permissions_ContactsAllow_v0: String { return self._s[3194]! } - public var Conversation_CardNumberCopied: String { return self._s[3195]! } - public var Notification_Exceptions_PreviewAlwaysOn: String { return self._s[3196]! } - public var PrivacyPolicy_Decline: String { return self._s[3197]! } - public var SettingsSearch_Synonyms_ChatFolders: String { return self._s[3198]! } - public var TwoStepAuth_PasswordRemoveConfirmation: String { return self._s[3199]! } - public var ChatListFolder_IncludeSectionInfo: String { return self._s[3200]! } - public func Map_DirectionsDriveEta(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3201]!, self._r[3201]!, [_0]) - } - public var Passport_Identity_Name: String { return self._s[3202]! } - public var WallpaperPreview_PatternTitle: String { return self._s[3204]! } - public var VoiceOver_Chat_RecordModeVideoMessage: String { return self._s[3205]! } - public var WallpaperSearch_ColorOrange: String { return self._s[3207]! } - public var Appearance_ThemePreview_ChatList_5_Name: String { return self._s[3208]! } - public var GroupInfo_Permissions_SlowmodeInfo: String { return self._s[3209]! } - public var Your_cards_security_code_is_invalid: String { return self._s[3210]! } - public var IntentsSettings_ResetAll: String { return self._s[3211]! } - public var SettingsSearch_Synonyms_Calls_CallTab: String { return self._s[3213]! } - public var Group_EditAdmin_TransferOwnership: String { return self._s[3214]! } - public var ChatList_DeleteForAllSubscribers: String { return self._s[3215]! } - public var Notification_Exceptions_Add: String { return self._s[3216]! } - public var Group_DeleteGroup: String { return self._s[3217]! } - public var Cache_Help: String { return self._s[3218]! } - public var Call_AudioRouteMute: String { return self._s[3219]! } - public var VoiceOver_Chat_YourVoiceMessage: String { return self._s[3220]! } - public var SocksProxySetup_ProxyEnabled: String { return self._s[3221]! } - public func VoiceChat_Status_MembersFormat(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3222]!, self._r[3222]!, [_1, _2]) - } - public func ApplyLanguage_UnsufficientDataText(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3223]!, self._r[3223]!, [_1]) - } - public func Call_CallInProgressMessage(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3224]!, self._r[3224]!, [_1, _2]) - } - public var AutoDownloadSettings_VideoMessagesTitle: String { return self._s[3225]! } - public var Channel_BanUser_PermissionAddMembers: String { return self._s[3226]! } - public func PUSH_CHAT_VOICECHAT_INVITE(_ _1: String, _ _2: String, _ _3: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3227]!, self._r[3227]!, [_1, _2, _3]) - } - public var Contacts_MemberSearchSectionTitleGroup: String { return self._s[3228]! } - public var TwoStepAuth_RecoveryCodeHelp: String { return self._s[3229]! } - public var ClearCache_StorageFree: String { return self._s[3230]! } - public func DialogList_SingleRecordingVideoMessageSuffix(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3231]!, self._r[3231]!, [_0]) - } - public var Privacy_Forwards_CustomHelp: String { return self._s[3232]! } - public func Channel_AdminLog_EditedInviteLink(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3234]!, self._r[3234]!, [_1, _2]) - } - public var Group_ErrorAddTooMuchAdmins: String { return self._s[3235]! } - public var DialogList_Typing: String { return self._s[3236]! } - public func Login_EmailCodeSubject(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3237]!, self._r[3237]!, [_0]) - } - public var Target_SelectGroup: String { return self._s[3238]! } - public var AuthSessions_IncompleteAttempts: String { return self._s[3239]! } - public func Notification_ProximityReached(_ _1: String, _ _2: String, _ _3: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3240]!, self._r[3240]!, [_1, _2, _3]) - } - public var Chat_PinnedListPreview_ShowAllMessages: String { return self._s[3241]! } - public var TwoStepAuth_EmailChangeSuccess: String { return self._s[3242]! } - public func Settings_CheckPhoneNumberTitle(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3243]!, self._r[3243]!, [_0]) - } - public var Channel_AdminLog_CanSendMessages: String { return self._s[3244]! } - public var TwoFactorSetup_EmailVerification_Title: String { return self._s[3245]! } - public var ChatSettings_TextSize: String { return self._s[3246]! } - public var Channel_AdminLogFilter_EventsEditedMessages: String { return self._s[3248]! } - public var Map_SendThisPlace: String { return self._s[3249]! } - public var Conversation_TextCopied: String { return self._s[3250]! } - public var Login_PhoneNumberAlreadyAuthorized: String { return self._s[3251]! } - public var ContactInfo_BirthdayLabel: String { return self._s[3252]! } - public var Call_ShareStats: String { return self._s[3253]! } - public var ChatList_UndoArchiveRevealedText: String { return self._s[3255]! } - public var Notifications_GroupNotificationsPreview: String { return self._s[3256]! } - public var Settings_Support: String { return self._s[3257]! } - public var GroupInfo_ChannelListNamePlaceholder: String { return self._s[3258]! } - public func EmptyGroupInfo_Line1(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3260]!, self._r[3260]!, [_0]) - } - public var Watch_Conversation_GroupInfo: String { return self._s[3261]! } - public var Tour_Text4: String { return self._s[3262]! } - public var UserInfo_FakeUserWarning: String { return self._s[3264]! } - public var PasscodeSettings_AutoLock: String { return self._s[3265]! } - public var Channel_BanList_BlockedTitle: String { return self._s[3266]! } - public var Bot_DescriptionTitle: String { return self._s[3267]! } - public var Map_LocationTitle: String { return self._s[3268]! } - public var ChatListFolder_ExcludeSectionInfo: String { return self._s[3269]! } - public var Conversation_HashtagCopied: String { return self._s[3270]! } - public func Notification_MessageLifetimeChangedOutgoing(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3271]!, self._r[3271]!, [_1]) - } - public var Login_EmailNotConfiguredError: String { return self._s[3272]! } - public var AutoDownloadSettings_LimitBySize: String { return self._s[3273]! } - public var PrivacySettings_LastSeenNobody: String { return self._s[3274]! } - public var Permissions_CellularDataText_v0: String { return self._s[3275]! } - public var Conversation_EncryptionProcessing: String { return self._s[3276]! } - public var GroupPermission_Delete: String { return self._s[3277]! } - public var Contacts_SortByName: String { return self._s[3278]! } - public var TwoStepAuth_RecoveryUnavailable: String { return self._s[3279]! } - public var Compose_ChannelTokenListPlaceholder: String { return self._s[3280]! } - public var Group_Management_AddModeratorHelp: String { return self._s[3282]! } - public var SettingsSearch_Synonyms_EditProfile_Logout: String { return self._s[3283]! } - public var Forward_ErrorPublicPollDisabledInChannels: String { return self._s[3284]! } - public var CallFeedback_IncludeLogsInfo: String { return self._s[3286]! } - public func PUSH_CHANNEL_MESSAGE_QUIZ(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3287]!, self._r[3287]!, [_1]) - } - public func SecretVideo_NotViewedYet(_ _0: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(self._s[3288]!, self._r[3288]!, [_0]) } - public var ChatList_Context_Delete: String { return self._s[3289]! } - public var VoiceChat_InviteMember: String { return self._s[3290]! } - public var PrivacyPhoneNumberSettings_CustomDisabledHelp: String { return self._s[3291]! } - public var Conversation_Processing: String { return self._s[3292]! } - public var TwoStepAuth_EmailCodeExpired: String { return self._s[3293]! } - public var ChatSettings_Stickers: String { return self._s[3294]! } - public var AppleWatch_ReplyPresetsHelp: String { return self._s[3295]! } - public var Passport_Language_cs: String { return self._s[3296]! } - public var GroupInfo_InvitationLinkGroupFull: String { return self._s[3298]! } - public var Conversation_Contact: String { return self._s[3299]! } - public var Passport_Identity_ReverseSideHelp: String { return self._s[3300]! } - public var SocksProxySetup_PasteFromClipboard: String { return self._s[3301]! } - public var Theme_Unsupported: String { return self._s[3302]! } - public var Privacy_TopPeersWarning: String { return self._s[3303]! } - public var InviteLink_Title: String { return self._s[3305]! } + public var Login_InfoHelp: String { return self._s[3289]! } + public var Notification_SecretChatMessageScreenshotSelf: String { return self._s[3290]! } + public var VoiceChat_SpeakPermissionEveryone: String { return self._s[3291]! } + public var Profile_MessageLifetime1d: String { return self._s[3292]! } + public var Group_UpgradeConfirmation: String { return self._s[3293]! } + public func PUSH_PINNED_STICKER(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[3294]!, self._r[3294]!, [_1, _2]) + } + public var Appearance_RemoveThemeColor: String { return self._s[3295]! } + public var Channel_AdminLog_TitleSelectedEvents: String { return self._s[3296]! } + public func Call_AnsweringWithAccount(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[3297]!, self._r[3297]!, [_0]) + } + public var UserInfo_BotSettings: String { return self._s[3298]! } + public func Notification_ChannelInviter(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[3300]!, self._r[3300]!, [_0]) + } + public var Permissions_ContactsText_v0: String { return self._s[3301]! } + public var Conversation_PinMessagesForMe: String { return self._s[3302]! } + public var VoiceChat_PanelJoin: String { return self._s[3303]! } + public var Conversation_DiscussionStarted: String { return self._s[3305]! } + public var SettingsSearch_Synonyms_Privacy_TwoStepAuth: String { return self._s[3306]! } + public var SharedMedia_SearchNoResults: String { return self._s[3308]! } + public func Login_EmailPhoneSubject(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[3310]!, self._r[3310]!, [_0]) + } + public func Conversation_ShareMyPhoneNumber_StatusSuccess(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[3312]!, self._r[3312]!, [_0]) + } + public var ReportPeer_ReasonOther_Placeholder: String { return self._s[3313]! } + public func TwoStepAuth_ResetPendingText(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[3314]!, self._r[3314]!, [_0]) + } + public var ContactInfo_PhoneLabelHomeFax: String { return self._s[3315]! } + public var Call_AudioRouteHeadphones: String { return self._s[3316]! } + public func Notification_VoiceChatScheduledTomorrowChannel(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[3318]!, self._r[3318]!, [_0]) + } + public func PUSH_AUTH_UNKNOWN(_ _1: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[3319]!, self._r[3319]!, [_1]) + } + public var Passport_Identity_FilesView: String { return self._s[3320]! } + public var TwoStepAuth_SetupEmail: String { return self._s[3321]! } + public var Widget_ApplicationStartRequired: String { return self._s[3322]! } + public var PhotoEditor_Original: String { return self._s[3323]! } + public var Call_YourMicrophoneOff: String { return self._s[3324]! } + public var Permissions_ContactsAllow_v0: String { return self._s[3325]! } + public var Conversation_CardNumberCopied: String { return self._s[3326]! } + public var Notification_Exceptions_PreviewAlwaysOn: String { return self._s[3327]! } + public var PrivacyPolicy_Decline: String { return self._s[3328]! } + public var SettingsSearch_Synonyms_ChatFolders: String { return self._s[3329]! } + public var TwoStepAuth_PasswordRemoveConfirmation: String { return self._s[3330]! } + public var ChatListFolder_IncludeSectionInfo: String { return self._s[3331]! } + public func Map_DirectionsDriveEta(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[3332]!, self._r[3332]!, [_0]) + } + public var Passport_Identity_Name: String { return self._s[3333]! } + public var WallpaperPreview_PatternTitle: String { return self._s[3335]! } + public var VoiceOver_Chat_RecordModeVideoMessage: String { return self._s[3336]! } + public var WallpaperSearch_ColorOrange: String { return self._s[3338]! } + public var Appearance_ThemePreview_ChatList_5_Name: String { return self._s[3339]! } + public var GroupInfo_Permissions_SlowmodeInfo: String { return self._s[3340]! } + public var Your_cards_security_code_is_invalid: String { return self._s[3341]! } + public var IntentsSettings_ResetAll: String { return self._s[3342]! } + public var SettingsSearch_Synonyms_Calls_CallTab: String { return self._s[3344]! } + public var Group_EditAdmin_TransferOwnership: String { return self._s[3345]! } + public var ChatList_DeleteForAllSubscribers: String { return self._s[3346]! } + public var Notification_Exceptions_Add: String { return self._s[3347]! } + public var Group_DeleteGroup: String { return self._s[3348]! } + public var Cache_Help: String { return self._s[3349]! } + public var Call_AudioRouteMute: String { return self._s[3350]! } + public var VoiceOver_Chat_YourVoiceMessage: String { return self._s[3351]! } + public var SocksProxySetup_ProxyEnabled: String { return self._s[3352]! } + public func VoiceChat_Status_MembersFormat(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[3353]!, self._r[3353]!, [_1, _2]) + } + public func ApplyLanguage_UnsufficientDataText(_ _1: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[3354]!, self._r[3354]!, [_1]) + } + public func Call_CallInProgressMessage(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[3355]!, self._r[3355]!, [_1, _2]) + } + public var AutoDownloadSettings_VideoMessagesTitle: String { return self._s[3356]! } + public var Channel_BanUser_PermissionAddMembers: String { return self._s[3357]! } + public func PUSH_CHAT_VOICECHAT_INVITE(_ _1: String, _ _2: String, _ _3: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[3358]!, self._r[3358]!, [_1, _2, _3]) + } + public var Contacts_MemberSearchSectionTitleGroup: String { return self._s[3359]! } + public var TwoStepAuth_RecoveryCodeHelp: String { return self._s[3360]! } + public var ClearCache_StorageFree: String { return self._s[3361]! } + public func DialogList_SingleRecordingVideoMessageSuffix(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[3362]!, self._r[3362]!, [_0]) + } + public var Privacy_Forwards_CustomHelp: String { return self._s[3363]! } + public func Channel_AdminLog_EditedInviteLink(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[3365]!, self._r[3365]!, [_1, _2]) + } + public var Group_ErrorAddTooMuchAdmins: String { return self._s[3366]! } + public var DialogList_Typing: String { return self._s[3367]! } + public func Login_EmailCodeSubject(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[3368]!, self._r[3368]!, [_0]) + } + public var Target_SelectGroup: String { return self._s[3369]! } + public var AuthSessions_IncompleteAttempts: String { return self._s[3370]! } + public var TwoStepAuth_RecoveryEmailResetText: String { return self._s[3371]! } + public var TwoFactorRemember_Done_Text: String { return self._s[3372]! } + public func Notification_ProximityReached(_ _1: String, _ _2: String, _ _3: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[3373]!, self._r[3373]!, [_1, _2, _3]) + } + public var Chat_PinnedListPreview_ShowAllMessages: String { return self._s[3374]! } + public var TwoStepAuth_EmailChangeSuccess: String { return self._s[3375]! } + public func Settings_CheckPhoneNumberTitle(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[3376]!, self._r[3376]!, [_0]) + } + public var Channel_AdminLog_CanSendMessages: String { return self._s[3377]! } + public var TwoFactorSetup_EmailVerification_Title: String { return self._s[3378]! } + public var ChatSettings_TextSize: String { return self._s[3379]! } + public var Channel_AdminLogFilter_EventsEditedMessages: String { return self._s[3381]! } + public var Map_SendThisPlace: String { return self._s[3382]! } + public var Conversation_TextCopied: String { return self._s[3383]! } + public var Login_PhoneNumberAlreadyAuthorized: String { return self._s[3384]! } + public var ContactInfo_BirthdayLabel: String { return self._s[3385]! } + public var Call_ShareStats: String { return self._s[3386]! } + public func PUSH_CHAT_VOICECHAT_END(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[3388]!, self._r[3388]!, [_1, _2]) + } + public var ChatList_UndoArchiveRevealedText: String { return self._s[3389]! } + public var Notifications_GroupNotificationsPreview: String { return self._s[3390]! } + public var Settings_Support: String { return self._s[3391]! } + public var GroupInfo_ChannelListNamePlaceholder: String { return self._s[3392]! } + public func EmptyGroupInfo_Line1(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[3394]!, self._r[3394]!, [_0]) + } + public var Watch_Conversation_GroupInfo: String { return self._s[3395]! } + public var Tour_Text4: String { return self._s[3396]! } + public var VoiceChat_CancelReminder: String { return self._s[3397]! } + public var Calls_StartNewCall: String { return self._s[3398]! } + public var UserInfo_FakeUserWarning: String { return self._s[3400]! } + public var PasscodeSettings_AutoLock: String { return self._s[3401]! } + public var Channel_BanList_BlockedTitle: String { return self._s[3402]! } + public var Bot_DescriptionTitle: String { return self._s[3404]! } + public var Map_LocationTitle: String { return self._s[3405]! } + public var ChatListFolder_ExcludeSectionInfo: String { return self._s[3406]! } + public var Conversation_HashtagCopied: String { return self._s[3407]! } + public func Notification_MessageLifetimeChangedOutgoing(_ _1: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[3408]!, self._r[3408]!, [_1]) + } + public var VoiceChat_ReminderNotify: String { return self._s[3409]! } + public var Login_EmailNotConfiguredError: String { return self._s[3410]! } + public var AutoDownloadSettings_LimitBySize: String { return self._s[3411]! } + public var PrivacySettings_LastSeenNobody: String { return self._s[3412]! } + public var Permissions_CellularDataText_v0: String { return self._s[3413]! } + public var Conversation_EncryptionProcessing: String { return self._s[3414]! } + public var GroupPermission_Delete: String { return self._s[3416]! } + public var Contacts_SortByName: String { return self._s[3417]! } + public var TwoStepAuth_RecoveryUnavailable: String { return self._s[3418]! } + public var Compose_ChannelTokenListPlaceholder: String { return self._s[3419]! } + public var Group_Management_AddModeratorHelp: String { return self._s[3421]! } + public var SettingsSearch_Synonyms_EditProfile_Logout: String { return self._s[3422]! } + public var Forward_ErrorPublicPollDisabledInChannels: String { return self._s[3423]! } + public var CallFeedback_IncludeLogsInfo: String { return self._s[3425]! } + public func PUSH_CHANNEL_MESSAGE_QUIZ(_ _1: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[3426]!, self._r[3426]!, [_1]) + } + public func SecretVideo_NotViewedYet(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[3427]!, self._r[3427]!, [_0]) + } + public var ChatList_Context_Delete: String { return self._s[3428]! } + public var VoiceChat_InviteMember: String { return self._s[3429]! } + public var PrivacyPhoneNumberSettings_CustomDisabledHelp: String { return self._s[3430]! } + public var Conversation_Processing: String { return self._s[3431]! } + public var TwoStepAuth_EmailCodeExpired: String { return self._s[3432]! } + public var ChatSettings_Stickers: String { return self._s[3433]! } + public var AppleWatch_ReplyPresetsHelp: String { return self._s[3434]! } + public var Passport_Language_cs: String { return self._s[3435]! } + public var GroupInfo_InvitationLinkGroupFull: String { return self._s[3437]! } + public var Conversation_Contact: String { return self._s[3438]! } + public var Passport_Identity_ReverseSideHelp: String { return self._s[3439]! } + public var SocksProxySetup_PasteFromClipboard: String { return self._s[3441]! } + public var Theme_Unsupported: String { return self._s[3442]! } + public var Privacy_TopPeersWarning: String { return self._s[3443]! } + public func Conversation_ScheduledVoiceChatStartsTodayShort(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[3444]!, self._r[3444]!, [_0]) + } + public var InviteLink_Title: String { return self._s[3446]! } public func UserInfo_BlockConfirmationTitle(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3306]!, self._r[3306]!, [_0]) + return formatWithArgumentRanges(self._s[3447]!, self._r[3447]!, [_0]) } - public var Conversation_SilentBroadcastTooltipOn: String { return self._s[3307]! } - public var TwoStepAuth_RemovePassword: String { return self._s[3308]! } - public var Settings_CheckPhoneNumberText: String { return self._s[3309]! } - public var PeopleNearby_Users: String { return self._s[3310]! } - public var Appearance_TextSize_UseSystem: String { return self._s[3311]! } - public var Settings_SetProfilePhoto: String { return self._s[3312]! } - public var Conversation_ContextMenuBan: String { return self._s[3313]! } - public var KeyCommand_ScrollUp: String { return self._s[3314]! } - public var Settings_ChatSettings: String { return self._s[3316]! } - public var CallList_RecentCallsHeader: String { return self._s[3317]! } + public var Conversation_SilentBroadcastTooltipOn: String { return self._s[3448]! } + public var TwoStepAuth_RemovePassword: String { return self._s[3449]! } + public var Settings_CheckPhoneNumberText: String { return self._s[3450]! } + public var PeopleNearby_Users: String { return self._s[3451]! } + public var Appearance_TextSize_UseSystem: String { return self._s[3452]! } + public var Settings_SetProfilePhoto: String { return self._s[3453]! } + public var Conversation_ContextMenuBan: String { return self._s[3454]! } + public var KeyCommand_ScrollUp: String { return self._s[3455]! } + public var Settings_ChatSettings: String { return self._s[3457]! } + public var CallList_RecentCallsHeader: String { return self._s[3458]! } public func PUSH_CHAT_MESSAGE_VIDEO(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3318]!, self._r[3318]!, [_1, _2]) + return formatWithArgumentRanges(self._s[3459]!, self._r[3459]!, [_1, _2]) } - public var Stats_GroupTopInvitersTitle: String { return self._s[3319]! } - public var Passport_Phone_EnterOtherNumber: String { return self._s[3320]! } - public var VoiceChat_StartRecordingTitle: String { return self._s[3321]! } - public var Passport_Identity_MiddleNamePlaceholder: String { return self._s[3323]! } - public var Passport_Address_OneOfTypeBankStatement: String { return self._s[3324]! } - public var VoiceOver_ChatList_MessageRead: String { return self._s[3325]! } - public var Stats_GroupTopPoster_Promote: String { return self._s[3326]! } - public var Cache_Title: String { return self._s[3327]! } + public var Stats_GroupTopInvitersTitle: String { return self._s[3460]! } + public var Passport_Phone_EnterOtherNumber: String { return self._s[3461]! } + public var VoiceChat_StartRecordingTitle: String { return self._s[3462]! } + public func Notification_VoiceChatScheduledToday(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[3463]!, self._r[3463]!, [_1, _2]) + } + public var Passport_Identity_MiddleNamePlaceholder: String { return self._s[3465]! } + public var Passport_Address_OneOfTypeBankStatement: String { return self._s[3466]! } + public var VoiceOver_ChatList_MessageRead: String { return self._s[3468]! } + public var Stats_GroupTopPoster_Promote: String { return self._s[3471]! } + public var Cache_Title: String { return self._s[3472]! } public func Conversation_AutoremoveTimerSetToastText(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3328]!, self._r[3328]!, [_0]) + return formatWithArgumentRanges(self._s[3473]!, self._r[3473]!, [_0]) } - public var Clipboard_SendPhoto: String { return self._s[3329]! } - public var Notifications_ExceptionsMessagePlaceholder: String { return self._s[3331]! } - public var TwoStepAuth_EnterPasswordForgot: String { return self._s[3332]! } - public var WatchRemote_AlertTitle: String { return self._s[3335]! } - public var Appearance_ReduceMotion: String { return self._s[3336]! } + public var Clipboard_SendPhoto: String { return self._s[3474]! } + public var Notifications_ExceptionsMessagePlaceholder: String { return self._s[3476]! } + public var TwoStepAuth_EnterPasswordForgot: String { return self._s[3477]! } + public var WatchRemote_AlertTitle: String { return self._s[3480]! } + public var Appearance_ReduceMotion: String { return self._s[3481]! } public func PUSH_CHAT_MESSAGE_ROUND(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3339]!, self._r[3339]!, [_1, _2]) + return formatWithArgumentRanges(self._s[3484]!, self._r[3484]!, [_1, _2]) } - public var Notifications_PermissionsSuppressWarningText: String { return self._s[3340]! } - public var ChatList_UndoArchiveHiddenTitle: String { return self._s[3341]! } - public var Passport_Identity_TypePersonalDetails: String { return self._s[3342]! } + public var Notifications_PermissionsSuppressWarningText: String { return self._s[3485]! } + public var ChatList_UndoArchiveHiddenTitle: String { return self._s[3486]! } + public var Passport_Identity_TypePersonalDetails: String { return self._s[3487]! } public func Call_CallInProgressVoiceChatMessage(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3343]!, self._r[3343]!, [_1, _2]) + return formatWithArgumentRanges(self._s[3488]!, self._r[3488]!, [_1, _2]) } public func Passport_Identity_UploadOneOfScan(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3345]!, self._r[3345]!, [_0]) + return formatWithArgumentRanges(self._s[3490]!, self._r[3490]!, [_0]) } - public var ChatListFolder_DiscardConfirmation: String { return self._s[3346]! } + public var ChatListFolder_DiscardConfirmation: String { return self._s[3491]! } public func Conversation_RestrictedStickersTimed(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3347]!, self._r[3347]!, [_0]) + return formatWithArgumentRanges(self._s[3493]!, self._r[3493]!, [_0]) } - public var InstantPage_Search: String { return self._s[3348]! } - public var ChatState_WaitingForNetwork: String { return self._s[3349]! } - public var GroupInfo_Sound: String { return self._s[3350]! } - public var NotificationsSound_Telegraph: String { return self._s[3351]! } - public var NotificationsSound_Hello: String { return self._s[3352]! } - public var VoiceChat_LeaveConfirmation: String { return self._s[3353]! } - public var Passport_FieldIdentityDetailsHelp: String { return self._s[3354]! } - public var Group_Members_AddMemberBotErrorNotAllowed: String { return self._s[3355]! } - public var Conversation_HoldForVideo: String { return self._s[3356]! } - public var Conversation_PinOlderMessageAlertText: String { return self._s[3357]! } - public var Appearance_ShareTheme: String { return self._s[3358]! } - public var TwoStepAuth_SetupHint: String { return self._s[3359]! } - public var Stats_GrowthTitle: String { return self._s[3362]! } - public var GroupInfo_InviteLink_ShareLink: String { return self._s[3363]! } - public var Conversation_DefaultRestrictedMedia: String { return self._s[3364]! } - public var Channel_EditAdmin_PermissionPostMessages: String { return self._s[3365]! } - public var GroupPermission_NoSendMessages: String { return self._s[3368]! } - public var Conversation_SetReminder_Title: String { return self._s[3369]! } - public var Privacy_Calls_CustomHelp: String { return self._s[3370]! } - public var CheckoutInfo_ErrorPostcodeInvalid: String { return self._s[3371]! } + public var InstantPage_Search: String { return self._s[3494]! } + public var ChatState_WaitingForNetwork: String { return self._s[3495]! } + public var GroupInfo_Sound: String { return self._s[3496]! } + public var NotificationsSound_Telegraph: String { return self._s[3497]! } + public func VoiceChat_ParticipantIsSpeaking(_ _1: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[3498]!, self._r[3498]!, [_1]) + } + public var NotificationsSound_Hello: String { return self._s[3499]! } + public var VoiceChat_LeaveConfirmation: String { return self._s[3500]! } + public var UserInfo_LinkForwardTooltip_SavedMessages_One: String { return self._s[3501]! } + public var Passport_FieldIdentityDetailsHelp: String { return self._s[3502]! } + public var Group_Members_AddMemberBotErrorNotAllowed: String { return self._s[3503]! } + public var Conversation_HoldForVideo: String { return self._s[3504]! } + public var Conversation_PinOlderMessageAlertText: String { return self._s[3505]! } + public var Appearance_ShareTheme: String { return self._s[3506]! } + public var TwoStepAuth_SetupHint: String { return self._s[3507]! } + public var Stats_GrowthTitle: String { return self._s[3510]! } + public var GroupInfo_InviteLink_ShareLink: String { return self._s[3511]! } + public var Conversation_DefaultRestrictedMedia: String { return self._s[3512]! } + public var Channel_EditAdmin_PermissionPostMessages: String { return self._s[3513]! } + public var GroupPermission_NoSendMessages: String { return self._s[3516]! } + public var Conversation_SetReminder_Title: String { return self._s[3517]! } + public var Privacy_Calls_CustomHelp: String { return self._s[3518]! } + public var CheckoutInfo_ErrorPostcodeInvalid: String { return self._s[3519]! } public func ClearCache_StorageTitle(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3372]!, self._r[3372]!, [_0]) + return formatWithArgumentRanges(self._s[3520]!, self._r[3520]!, [_0]) } - public var InviteLinks_InviteLinkExpired: String { return self._s[3374]! } - public var Undo_SecretChatDeleted: String { return self._s[3375]! } - public var PhotoEditor_ContrastTool: String { return self._s[3376]! } - public var Privacy_Forwards: String { return self._s[3377]! } - public var AuthSessions_LoggedInWithTelegram: String { return self._s[3378]! } - public var KeyCommand_SendMessage: String { return self._s[3380]! } - public var Conversation_PrivateMessageLinkCopiedLong: String { return self._s[3381]! } + public var InviteLinks_InviteLinkExpired: String { return self._s[3522]! } + public var Undo_SecretChatDeleted: String { return self._s[3523]! } + public var PhotoEditor_ContrastTool: String { return self._s[3524]! } + public var Privacy_Forwards: String { return self._s[3525]! } + public var AuthSessions_LoggedInWithTelegram: String { return self._s[3526]! } + public var KeyCommand_SendMessage: String { return self._s[3528]! } + public var Conversation_PrivateMessageLinkCopiedLong: String { return self._s[3529]! } public func InstantPage_RelatedArticleAuthorAndDateTitle(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3382]!, self._r[3382]!, [_1, _2]) + return formatWithArgumentRanges(self._s[3530]!, self._r[3530]!, [_1, _2]) } - public var GroupPermission_NoSendGifs: String { return self._s[3383]! } - public var Notification_MessageLifetime2s: String { return self._s[3384]! } - public var Message_Theme: String { return self._s[3385]! } - public var Conversation_Dice_u1F3AF: String { return self._s[3388]! } + public var VoiceChat_VideoPaused: String { return self._s[3531]! } + public var GroupPermission_NoSendGifs: String { return self._s[3532]! } + public func Notification_VoiceChatEndedGroup(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[3533]!, self._r[3533]!, [_1, _2]) + } + public var Notification_MessageLifetime2s: String { return self._s[3534]! } + public var Message_Theme: String { return self._s[3535]! } + public var Conversation_Dice_u1F3AF: String { return self._s[3538]! } public func DialogList_SinglePlayingGameSuffix(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3389]!, self._r[3389]!, [_0]) + return formatWithArgumentRanges(self._s[3539]!, self._r[3539]!, [_0]) } - public var Group_UpgradeNoticeHeader: String { return self._s[3391]! } - public var PeerInfo_BioExpand: String { return self._s[3392]! } - public var Passport_DeletePersonalDetails: String { return self._s[3393]! } - public var Widget_NoUsers: String { return self._s[3394]! } - public var TwoStepAuth_AddHintTitle: String { return self._s[3395]! } - public var Login_TermsOfServiceDecline: String { return self._s[3396]! } - public var CreatePoll_QuizTip: String { return self._s[3398]! } - public var Watch_LastSeen_WithinAWeek: String { return self._s[3399]! } - public var MessagePoll_SubmitVote: String { return self._s[3401]! } - public var ChatSettings_AutoDownloadEnabled: String { return self._s[3402]! } - public var Passport_Address_EditRentalAgreement: String { return self._s[3403]! } - public var Conversation_SearchByName_Placeholder: String { return self._s[3404]! } - public var Conversation_UpdateTelegram: String { return self._s[3405]! } + public var Group_UpgradeNoticeHeader: String { return self._s[3541]! } + public var PeerInfo_BioExpand: String { return self._s[3542]! } + public var Passport_DeletePersonalDetails: String { return self._s[3543]! } + public var Widget_NoUsers: String { return self._s[3544]! } + public var TwoStepAuth_AddHintTitle: String { return self._s[3545]! } + public var VoiceChat_VideoPreviewDescription: String { return self._s[3546]! } + public var Login_TermsOfServiceDecline: String { return self._s[3547]! } + public var VoiceChat_UnmuteSuggestion: String { return self._s[3548]! } + public var CreatePoll_QuizTip: String { return self._s[3550]! } + public var Watch_LastSeen_WithinAWeek: String { return self._s[3551]! } + public var MessagePoll_SubmitVote: String { return self._s[3553]! } + public var ChatSettings_AutoDownloadEnabled: String { return self._s[3554]! } + public var Passport_Address_EditRentalAgreement: String { return self._s[3555]! } + public var Conversation_SearchByName_Placeholder: String { return self._s[3556]! } + public var Conversation_UpdateTelegram: String { return self._s[3557]! } public func FileSize_KB(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3406]!, self._r[3406]!, [_0]) - } - public var UserInfo_About_Placeholder: String { return self._s[3407]! } - public var CallSettings_Always: String { return self._s[3408]! } - public var ChannelInfo_ScamChannelWarning: String { return self._s[3409]! } - public var VoiceChat_MutedByAdminHelp: String { return self._s[3410]! } - public var Login_TermsOfServiceHeader: String { return self._s[3411]! } - public var KeyCommand_ChatInfo: String { return self._s[3412]! } - public var MessagePoll_LabelPoll: String { return self._s[3413]! } - public var Paint_Clear: String { return self._s[3414]! } - public var PeerInfo_ButtonMute: String { return self._s[3415]! } - public var LastSeen_WithinAWeek: String { return self._s[3416]! } - public var Invitation_JoinVoiceChatAsSpeaker: String { return self._s[3417]! } - public var Passport_Identity_FrontSide: String { return self._s[3418]! } - public var Stickers_GroupStickers: String { return self._s[3419]! } - public var ChangePhoneNumberNumber_NumberPlaceholder: String { return self._s[3420]! } - public func Map_SearchNoResultsDescription(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3421]!, self._r[3421]!, [_0]) - } - public var VoiceOver_BotCommands: String { return self._s[3422]! } - public func PUSH_MESSAGE_GEO(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3425]!, self._r[3425]!, [_1]) - } - public var SocksProxySetup_ProxyStatusConnected: String { return self._s[3426]! } - public var Chat_MultipleTextMessagesDisabled: String { return self._s[3427]! } - public var InviteLink_ContextDelete: String { return self._s[3428]! } - public func Notification_LeftChat(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3429]!, self._r[3429]!, [_0]) - } - public var WebSearch_SearchNoResults: String { return self._s[3431]! } - public var Channel_DiscussionGroup_Create: String { return self._s[3432]! } - public var Passport_Language_es: String { return self._s[3433]! } - public var EnterPasscode_EnterCurrentPasscode: String { return self._s[3434]! } - public var Map_LiveLocationShowAll: String { return self._s[3435]! } - public var Cache_MaximumCacheSizeHelp: String { return self._s[3437]! } - public var Map_OpenInGoogleMaps: String { return self._s[3438]! } - public var CheckoutInfo_ErrorNameInvalid: String { return self._s[3440]! } - public var EditTheme_Create_BottomInfo: String { return self._s[3441]! } - public var PhotoEditor_BlurToolLinear: String { return self._s[3442]! } - public func Channel_AdminLog_MessageEdited(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3443]!, self._r[3443]!, [_0]) - } - public var Passport_Phone_Delete: String { return self._s[3444]! } - public var Channel_Username_CreatePrivateLinkHelp: String { return self._s[3445]! } - public var PrivacySettings_PrivacyTitle: String { return self._s[3446]! } - public var CheckoutInfo_ReceiverInfoNamePlaceholder: String { return self._s[3447]! } - public func EncryptionKey_Description(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3448]!, self._r[3448]!, [_1, _2]) - } - public var LogoutOptions_LogOutInfo: String { return self._s[3449]! } - public var Cache_ByPeerHeader: String { return self._s[3451]! } - public var Username_InvalidCharacters: String { return self._s[3452]! } - public var Checkout_ShippingAddress: String { return self._s[3453]! } - public func PUSH_CHAT_MESSAGE_GAME_SCORE(_ _1: String, _ _2: String, _ _3: String, _ _4: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3454]!, self._r[3454]!, [_1, _2, _3, _4]) - } - public var VoiceChat_LeaveAndEndVoiceChat: String { return self._s[3456]! } - public var Conversation_AddContact: String { return self._s[3457]! } - public var Passport_Address_EditUtilityBill: String { return self._s[3458]! } - public var InviteLink_ContextGetQRCode: String { return self._s[3459]! } - public var Conversation_ChecksTooltip_Delivered: String { return self._s[3461]! } - public func Channel_AdminLog_MessageAddedAdminNameUsername(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3462]!, self._r[3462]!, [_1, _2]) - } - public var Message_Video: String { return self._s[3463]! } - public func Watch_Time_ShortYesterdayAt(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3464]!, self._r[3464]!, [_0]) - } - public func Conversation_Megabytes(_ _0: Float) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3465]!, self._r[3465]!, ["\(_0)"]) - } - public var InviteLink_ReactivateLink: String { return self._s[3466]! } - public var Passport_Language_km: String { return self._s[3467]! } - public func PUSH_MESSAGE_CHANNEL_MESSAGE_GAME_SCORE(_ _1: String, _ _2: String, _ _3: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3468]!, self._r[3468]!, [_1, _2, _3]) - } - public var EmptyGroupInfo_Line4: String { return self._s[3469]! } - public var Conversation_SendMessageErrorTooMuchScheduled: String { return self._s[3471]! } - public var Notification_CallCanceledShort: String { return self._s[3472]! } - public var PhotoEditor_FadeTool: String { return self._s[3473]! } - public var Group_PublicLink_Info: String { return self._s[3474]! } - public var Contacts_DeselectAll: String { return self._s[3475]! } - public var Conversation_Moderate_Delete: String { return self._s[3476]! } - public var TwoStepAuth_RecoveryCodeInvalid: String { return self._s[3477]! } - public var NotificationsSound_Note: String { return self._s[3480]! } - public func Message_PaymentSent(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3481]!, self._r[3481]!, [_0]) - } - public var Appearance_ThemePreview_ChatList_7_Text: String { return self._s[3482]! } - public var Channel_EditAdmin_PermissionInviteViaLink: String { return self._s[3484]! } - public var DialogList_SearchSectionGlobal: String { return self._s[3485]! } - public var AccessDenied_Settings: String { return self._s[3486]! } - public var Passport_Identity_TypeIdentityCardUploadScan: String { return self._s[3487]! } - public var AuthSessions_EmptyTitle: String { return self._s[3488]! } - public var TwoStepAuth_PasswordChangeSuccess: String { return self._s[3489]! } - public var GroupInfo_GroupType: String { return self._s[3490]! } - public var Calls_Missed: String { return self._s[3491]! } - public var Contacts_VoiceOver_AddContact: String { return self._s[3492]! } - public var UserInfo_GenericPhoneLabel: String { return self._s[3494]! } - public var Passport_Language_uz: String { return self._s[3495]! } - public var Conversation_StopQuizConfirmationTitle: String { return self._s[3496]! } - public var PhotoEditor_BlurToolPortrait: String { return self._s[3497]! } - public var Map_ChooseLocationTitle: String { return self._s[3498]! } - public var Checkout_EnterPassword: String { return self._s[3499]! } - public var GroupInfo_ConvertToSupergroup: String { return self._s[3500]! } - public var AutoNightTheme_UpdateLocation: String { return self._s[3501]! } - public var NetworkUsageSettings_Title: String { return self._s[3502]! } - public var Location_ProximityAlertCancelled: String { return self._s[3503]! } - public var SettingsSearch_Synonyms_ChatSettings_IntentsSettings: String { return self._s[3504]! } - public var Message_PinnedLiveLocationMessage: String { return self._s[3505]! } - public var Compose_NewChannel: String { return self._s[3506]! } - public var Privacy_PaymentsClearInfo: String { return self._s[3508]! } - public func PUSH_MESSAGE_POLL(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3509]!, self._r[3509]!, [_1]) - } - public var Notification_Exceptions_AlwaysOn: String { return self._s[3510]! } - public var Privacy_GroupsAndChannels_WhoCanAddMe: String { return self._s[3511]! } - public var AutoNightTheme_AutomaticSection: String { return self._s[3514]! } - public var WallpaperSearch_ColorBrown: String { return self._s[3515]! } - public var Appearance_AppIconDefault: String { return self._s[3516]! } - public var StickerSettings_ContextInfo: String { return self._s[3519]! } - public var Channel_AddBotErrorNoRights: String { return self._s[3520]! } - public var Passport_FieldPhone: String { return self._s[3522]! } - public var Contacts_PermissionsTitle: String { return self._s[3523]! } - public var TwoFactorSetup_Email_SkipConfirmationSkip: String { return self._s[3524]! } - public func Notification_JoinedChat(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3525]!, self._r[3525]!, [_0]) - } - public var Bot_Unblock: String { return self._s[3526]! } - public var PasscodeSettings_SimplePasscode: String { return self._s[3527]! } - public var InviteLink_InviteLinkCopiedText: String { return self._s[3528]! } - public var Passport_PasswordHelp: String { return self._s[3529]! } - public var Watch_Conversation_UserInfo: String { return self._s[3530]! } - public func Channel_AdminLog_MessageChangedGroupGeoLocation(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3534]!, self._r[3534]!, [_0]) - } - public var State_Connecting: String { return self._s[3536]! } - public var Passport_Address_TypeTemporaryRegistration: String { return self._s[3537]! } - public var TextFormat_AddLinkPlaceholder: String { return self._s[3538]! } - public var Conversation_Dice_u1F3B2: String { return self._s[3539]! } - public func Call_StatusBar(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3540]!, self._r[3540]!, [_0]) - } - public var Conversation_SendingOptionsTooltip: String { return self._s[3541]! } - public var ChatList_UndoArchiveTitle: String { return self._s[3542]! } - public var ChatList_EmptyChatListNewMessage: String { return self._s[3543]! } - public var WallpaperSearch_ColorGreen: String { return self._s[3545]! } - public var PhotoEditor_BlurToolOff: String { return self._s[3546]! } - public var Conversation_AutoremoveOff: String { return self._s[3547]! } - public var SocksProxySetup_PortPlaceholder: String { return self._s[3548]! } - public var Weekday_Saturday: String { return self._s[3549]! } - public var DialogList_Unread: String { return self._s[3550]! } - public var Watch_LastSeen_ALongTimeAgo: String { return self._s[3551]! } - public var Stats_GroupPosters: String { return self._s[3552]! } - public func PUSH_ENCRYPTION_REQUEST(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3553]!, self._r[3553]!, [_1]) - } - public var Conversation_AlsoClearCacheTitle: String { return self._s[3554]! } - public func Conversation_ForwardTooltip_TwoChats_One(_ _0: String, _ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3555]!, self._r[3555]!, [_0, _1]) - } - public func Target_ShareGameConfirmationGroup(_ _0: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(self._s[3558]!, self._r[3558]!, [_0]) } - public var ReportPeer_ReasonChildAbuse: String { return self._s[3559]! } + public var UserInfo_About_Placeholder: String { return self._s[3559]! } + public var CallSettings_Always: String { return self._s[3560]! } + public var ChannelInfo_ScamChannelWarning: String { return self._s[3561]! } + public var VoiceChat_MutedByAdminHelp: String { return self._s[3562]! } + public var Login_TermsOfServiceHeader: String { return self._s[3563]! } + public var KeyCommand_ChatInfo: String { return self._s[3564]! } + public var MessagePoll_LabelPoll: String { return self._s[3565]! } + public var Paint_Clear: String { return self._s[3566]! } + public var PeerInfo_ButtonMute: String { return self._s[3567]! } + public var LastSeen_WithinAWeek: String { return self._s[3568]! } + public var Invitation_JoinVoiceChatAsSpeaker: String { return self._s[3569]! } + public var Passport_Identity_FrontSide: String { return self._s[3570]! } + public var Stickers_GroupStickers: String { return self._s[3571]! } + public var ChangePhoneNumberNumber_NumberPlaceholder: String { return self._s[3572]! } + public func Map_SearchNoResultsDescription(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[3573]!, self._r[3573]!, [_0]) + } + public var VoiceOver_BotCommands: String { return self._s[3574]! } + public func PUSH_MESSAGE_GEO(_ _1: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[3577]!, self._r[3577]!, [_1]) + } + public var SocksProxySetup_ProxyStatusConnected: String { return self._s[3578]! } + public var Chat_MultipleTextMessagesDisabled: String { return self._s[3579]! } + public var InviteLink_ContextDelete: String { return self._s[3580]! } + public func Notification_LeftChat(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[3581]!, self._r[3581]!, [_0]) + } + public var WebSearch_SearchNoResults: String { return self._s[3583]! } + public var Channel_DiscussionGroup_Create: String { return self._s[3584]! } + public var Passport_Language_es: String { return self._s[3585]! } + public var EnterPasscode_EnterCurrentPasscode: String { return self._s[3586]! } + public var Map_LiveLocationShowAll: String { return self._s[3587]! } + public var Cache_MaximumCacheSizeHelp: String { return self._s[3589]! } + public var Map_OpenInGoogleMaps: String { return self._s[3590]! } + public var CheckoutInfo_ErrorNameInvalid: String { return self._s[3592]! } + public var EditTheme_Create_BottomInfo: String { return self._s[3593]! } + public var PhotoEditor_BlurToolLinear: String { return self._s[3594]! } + public func Channel_AdminLog_MessageEdited(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[3595]!, self._r[3595]!, [_0]) + } + public var Passport_Phone_Delete: String { return self._s[3596]! } + public var Channel_Username_CreatePrivateLinkHelp: String { return self._s[3597]! } + public var PrivacySettings_PrivacyTitle: String { return self._s[3598]! } + public var CheckoutInfo_ReceiverInfoNamePlaceholder: String { return self._s[3599]! } + public func EncryptionKey_Description(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[3600]!, self._r[3600]!, [_1, _2]) + } + public var LogoutOptions_LogOutInfo: String { return self._s[3601]! } + public var Cache_ByPeerHeader: String { return self._s[3603]! } + public var Username_InvalidCharacters: String { return self._s[3604]! } + public var Checkout_ShippingAddress: String { return self._s[3606]! } + public func PUSH_CHAT_MESSAGE_GAME_SCORE(_ _1: String, _ _2: String, _ _3: String, _ _4: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[3607]!, self._r[3607]!, [_1, _2, _3, _4]) + } + public var VoiceChat_LeaveAndEndVoiceChat: String { return self._s[3609]! } + public var Conversation_AddContact: String { return self._s[3610]! } + public var Passport_Address_EditUtilityBill: String { return self._s[3611]! } + public var InviteLink_ContextGetQRCode: String { return self._s[3612]! } + public var Conversation_ChecksTooltip_Delivered: String { return self._s[3614]! } + public func Channel_AdminLog_MessageAddedAdminNameUsername(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[3615]!, self._r[3615]!, [_1, _2]) + } + public var Message_Video: String { return self._s[3616]! } + public func Watch_Time_ShortYesterdayAt(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[3617]!, self._r[3617]!, [_0]) + } + public func Conversation_Megabytes(_ _0: Float) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[3618]!, self._r[3618]!, ["\(_0)"]) + } + public var InviteLink_ReactivateLink: String { return self._s[3619]! } + public var Passport_Language_km: String { return self._s[3621]! } + public func PUSH_MESSAGE_CHANNEL_MESSAGE_GAME_SCORE(_ _1: String, _ _2: String, _ _3: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[3622]!, self._r[3622]!, [_1, _2, _3]) + } + public var EmptyGroupInfo_Line4: String { return self._s[3623]! } + public var Conversation_SendMessageErrorTooMuchScheduled: String { return self._s[3625]! } + public var Notification_CallCanceledShort: String { return self._s[3626]! } + public var PhotoEditor_FadeTool: String { return self._s[3627]! } + public var Group_PublicLink_Info: String { return self._s[3628]! } + public var Contacts_DeselectAll: String { return self._s[3629]! } + public var Conversation_Moderate_Delete: String { return self._s[3631]! } + public var TwoStepAuth_RecoveryCodeInvalid: String { return self._s[3632]! } + public var NotificationsSound_Note: String { return self._s[3635]! } + public func Message_PaymentSent(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[3636]!, self._r[3636]!, [_0]) + } + public var Appearance_ThemePreview_ChatList_7_Text: String { return self._s[3637]! } + public var Channel_EditAdmin_PermissionInviteViaLink: String { return self._s[3639]! } + public var DialogList_SearchSectionGlobal: String { return self._s[3640]! } + public var AccessDenied_Settings: String { return self._s[3641]! } + public var Passport_Identity_TypeIdentityCardUploadScan: String { return self._s[3642]! } + public var AuthSessions_EmptyTitle: String { return self._s[3643]! } + public var TwoStepAuth_PasswordChangeSuccess: String { return self._s[3644]! } + public var GroupInfo_GroupType: String { return self._s[3645]! } + public var Calls_Missed: String { return self._s[3646]! } + public var Contacts_VoiceOver_AddContact: String { return self._s[3647]! } + public var UserInfo_GenericPhoneLabel: String { return self._s[3649]! } + public var Passport_Language_uz: String { return self._s[3650]! } + public var Conversation_StopQuizConfirmationTitle: String { return self._s[3651]! } + public var PhotoEditor_BlurToolPortrait: String { return self._s[3652]! } + public var VoiceChat_CreateNewVoiceChatStartNow: String { return self._s[3653]! } + public var Map_ChooseLocationTitle: String { return self._s[3654]! } + public var Checkout_EnterPassword: String { return self._s[3655]! } + public var GroupInfo_ConvertToSupergroup: String { return self._s[3656]! } + public var AutoNightTheme_UpdateLocation: String { return self._s[3657]! } + public var NetworkUsageSettings_Title: String { return self._s[3658]! } + public var Location_ProximityAlertCancelled: String { return self._s[3659]! } + public var SettingsSearch_Synonyms_ChatSettings_IntentsSettings: String { return self._s[3660]! } + public var Message_PinnedLiveLocationMessage: String { return self._s[3661]! } + public var Compose_NewChannel: String { return self._s[3662]! } + public var Privacy_PaymentsClearInfo: String { return self._s[3664]! } + public func PUSH_MESSAGE_POLL(_ _1: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[3665]!, self._r[3665]!, [_1]) + } + public var Notification_Exceptions_AlwaysOn: String { return self._s[3666]! } + public var Privacy_GroupsAndChannels_WhoCanAddMe: String { return self._s[3667]! } + public var AutoNightTheme_AutomaticSection: String { return self._s[3670]! } + public var WallpaperSearch_ColorBrown: String { return self._s[3671]! } + public var Appearance_AppIconDefault: String { return self._s[3672]! } + public var StickerSettings_ContextInfo: String { return self._s[3675]! } + public var Channel_AddBotErrorNoRights: String { return self._s[3676]! } + public var Passport_FieldPhone: String { return self._s[3678]! } + public var Contacts_PermissionsTitle: String { return self._s[3679]! } + public var TwoFactorSetup_Email_SkipConfirmationSkip: String { return self._s[3680]! } + public func Notification_JoinedChat(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[3681]!, self._r[3681]!, [_0]) + } + public var Bot_Unblock: String { return self._s[3682]! } + public var PasscodeSettings_SimplePasscode: String { return self._s[3683]! } + public var InviteLink_InviteLinkCopiedText: String { return self._s[3684]! } + public var Passport_PasswordHelp: String { return self._s[3685]! } + public var TwoFactorSetup_PasswordRecovery_PlaceholderConfirmPassword: String { return self._s[3686]! } + public var Watch_Conversation_UserInfo: String { return self._s[3687]! } + public func Channel_AdminLog_MessageChangedGroupGeoLocation(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[3691]!, self._r[3691]!, [_0]) + } + public var State_Connecting: String { return self._s[3693]! } + public var Passport_Address_TypeTemporaryRegistration: String { return self._s[3694]! } + public var TextFormat_AddLinkPlaceholder: String { return self._s[3695]! } + public var Conversation_Dice_u1F3B2: String { return self._s[3696]! } + public func Call_StatusBar(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[3697]!, self._r[3697]!, [_0]) + } + public var Conversation_SendingOptionsTooltip: String { return self._s[3698]! } + public var ChatList_UndoArchiveTitle: String { return self._s[3699]! } + public var ChatList_EmptyChatListNewMessage: String { return self._s[3700]! } + public var WallpaperSearch_ColorGreen: String { return self._s[3702]! } + public var PhotoEditor_BlurToolOff: String { return self._s[3703]! } + public var Conversation_AutoremoveOff: String { return self._s[3704]! } + public var SocksProxySetup_PortPlaceholder: String { return self._s[3705]! } + public var Weekday_Saturday: String { return self._s[3706]! } + public var DialogList_Unread: String { return self._s[3707]! } + public var Watch_LastSeen_ALongTimeAgo: String { return self._s[3708]! } + public var Stats_GroupPosters: String { return self._s[3709]! } + public func PUSH_ENCRYPTION_REQUEST(_ _1: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[3710]!, self._r[3710]!, [_1]) + } + public var Conversation_AlsoClearCacheTitle: String { return self._s[3711]! } + public func Conversation_ForwardTooltip_TwoChats_One(_ _0: String, _ _1: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[3712]!, self._r[3712]!, [_0, _1]) + } + public func Target_ShareGameConfirmationGroup(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[3715]!, self._r[3715]!, [_0]) + } + public var ReportPeer_ReasonChildAbuse: String { return self._s[3716]! } public func Channel_AdminLog_MessageUnkickedNameUsername(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3560]!, self._r[3560]!, [_1, _2]) + return formatWithArgumentRanges(self._s[3717]!, self._r[3717]!, [_1, _2]) } - public var InfoPlist_NSContactsUsageDescription: String { return self._s[3561]! } - public var Conversation_EmailCopied: String { return self._s[3563]! } - public var AutoNightTheme_UseSunsetSunrise: String { return self._s[3564]! } - public var Channel_OwnershipTransfer_ChangeOwner: String { return self._s[3565]! } - public var Call_VoiceOver_VoiceCallCanceled: String { return self._s[3566]! } - public var Passport_Language_dv: String { return self._s[3567]! } - public var GroupPermission_AddSuccess: String { return self._s[3569]! } - public var Passport_Email_Help: String { return self._s[3570]! } - public var Call_ReportPlaceholder: String { return self._s[3571]! } - public var CreatePoll_AddOption: String { return self._s[3572]! } - public var MessagePoll_LabelAnonymousQuiz: String { return self._s[3574]! } - public var PeerInfo_ButtonLeave: String { return self._s[3575]! } - public var PhotoEditor_TiltShift: String { return self._s[3578]! } - public var SecretGif_Title: String { return self._s[3580]! } - public var GroupInfo_InviteLinks: String { return self._s[3581]! } - public var PhotoEditor_QualityVeryLow: String { return self._s[3582]! } - public var SocksProxySetup_Connecting: String { return self._s[3584]! } - public var PrivacySettings_PasscodeAndFaceId: String { return self._s[3585]! } - public var ContactInfo_PhoneLabelWork: String { return self._s[3586]! } - public var Stats_GroupTopHoursTitle: String { return self._s[3587]! } - public var Compose_NewMessage: String { return self._s[3588]! } - public var VoiceOver_Common_SwitchHint: String { return self._s[3589]! } - public var NotificationsSound_Synth: String { return self._s[3590]! } - public var ChatImport_UserErrorNotMutual: String { return self._s[3591]! } - public var Conversation_FileOpenIn: String { return self._s[3592]! } - public var AutoDownloadSettings_WifiTitle: String { return self._s[3593]! } - public var UserInfo_SendMessage: String { return self._s[3594]! } - public var Checkout_PayWithFaceId: String { return self._s[3595]! } + public var InfoPlist_NSContactsUsageDescription: String { return self._s[3718]! } + public var Conversation_EmailCopied: String { return self._s[3720]! } + public var AutoNightTheme_UseSunsetSunrise: String { return self._s[3721]! } + public var Channel_OwnershipTransfer_ChangeOwner: String { return self._s[3722]! } + public var Call_VoiceOver_VoiceCallCanceled: String { return self._s[3723]! } + public var VoiceChat_LateBy: String { return self._s[3724]! } + public var Passport_Language_dv: String { return self._s[3725]! } + public var TwoFactorSetup_PasswordRecovery_Text: String { return self._s[3726]! } + public var GroupPermission_AddSuccess: String { return self._s[3728]! } + public var Passport_Email_Help: String { return self._s[3729]! } + public var Call_ReportPlaceholder: String { return self._s[3730]! } + public var CreatePoll_AddOption: String { return self._s[3731]! } + public var MessagePoll_LabelAnonymousQuiz: String { return self._s[3733]! } + public var PeerInfo_ButtonLeave: String { return self._s[3734]! } + public var PhotoEditor_TiltShift: String { return self._s[3737]! } + public var SecretGif_Title: String { return self._s[3739]! } + public var GroupInfo_InviteLinks: String { return self._s[3740]! } + public var PhotoEditor_QualityVeryLow: String { return self._s[3741]! } + public var SocksProxySetup_Connecting: String { return self._s[3743]! } + public var PrivacySettings_PasscodeAndFaceId: String { return self._s[3744]! } + public var ContactInfo_PhoneLabelWork: String { return self._s[3745]! } + public var Stats_GroupTopHoursTitle: String { return self._s[3746]! } + public var Compose_NewMessage: String { return self._s[3747]! } + public var VoiceOver_Common_SwitchHint: String { return self._s[3748]! } + public var NotificationsSound_Synth: String { return self._s[3749]! } + public var ChatImport_UserErrorNotMutual: String { return self._s[3750]! } + public var Conversation_FileOpenIn: String { return self._s[3751]! } + public var AutoDownloadSettings_WifiTitle: String { return self._s[3752]! } + public var UserInfo_SendMessage: String { return self._s[3753]! } + public var Checkout_PayWithFaceId: String { return self._s[3754]! } public func Map_LiveLocationShortHour(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3596]!, self._r[3596]!, [_0]) + return formatWithArgumentRanges(self._s[3755]!, self._r[3755]!, [_0]) } - public var TextFormat_Strikethrough: String { return self._s[3597]! } - public var SettingsSearch_Synonyms_Notifications_DisplayNamesOnLockScreen: String { return self._s[3598]! } - public var Conversation_ViewChannel: String { return self._s[3599]! } + public var TextFormat_Strikethrough: String { return self._s[3756]! } + public var SettingsSearch_Synonyms_Notifications_DisplayNamesOnLockScreen: String { return self._s[3757]! } + public var Conversation_ViewChannel: String { return self._s[3758]! } public func Message_ForwardedMessage(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3600]!, self._r[3600]!, [_0]) + return formatWithArgumentRanges(self._s[3759]!, self._r[3759]!, [_0]) } - public var Channel_Stickers_Placeholder: String { return self._s[3601]! } - public var Channel_OwnershipTransfer_PasswordPlaceholder: String { return self._s[3602]! } - public var Message_ScamAccount: String { return self._s[3603]! } - public var Camera_FlashAuto: String { return self._s[3604]! } - public var Conversation_EncryptedDescription1: String { return self._s[3605]! } - public var LocalGroup_Text: String { return self._s[3606]! } - public var SettingsSearch_Synonyms_Data_Storage_KeepMedia: String { return self._s[3607]! } - public var UserInfo_FirstNamePlaceholder: String { return self._s[3608]! } - public var Conversation_SendMessageErrorFlood: String { return self._s[3609]! } - public var Conversation_EncryptedDescription2: String { return self._s[3610]! } - public var Conversation_CancelForwardText: String { return self._s[3611]! } - public var Notification_GroupActivated: String { return self._s[3612]! } - public var LastSeen_Lately: String { return self._s[3613]! } - public var Conversation_EncryptedDescription3: String { return self._s[3614]! } - public var SettingsSearch_Synonyms_Privacy_ProfilePhoto: String { return self._s[3615]! } - public var Conversation_SwipeToReplyHintText: String { return self._s[3616]! } - public var Conversation_EncryptedDescription4: String { return self._s[3617]! } - public var SharedMedia_EmptyTitle: String { return self._s[3618]! } - public var Appearance_CreateTheme: String { return self._s[3619]! } - public var Stats_SharesPerPost: String { return self._s[3620]! } - public var Contacts_TabTitle: String { return self._s[3621]! } - public var Weekday_ShortThursday: String { return self._s[3622]! } - public var MessageTimer_Forever: String { return self._s[3623]! } - public var ChatListFolder_CategoryArchived: String { return self._s[3624]! } - public var Channel_EditAdmin_PermissionDeleteMessages: String { return self._s[3625]! } - public var EditTheme_Create_TopInfo: String { return self._s[3627]! } + public var Channel_Stickers_Placeholder: String { return self._s[3760]! } + public var Channel_OwnershipTransfer_PasswordPlaceholder: String { return self._s[3761]! } + public var Message_ScamAccount: String { return self._s[3762]! } + public var Camera_FlashAuto: String { return self._s[3763]! } + public var Conversation_EncryptedDescription1: String { return self._s[3764]! } + public var LocalGroup_Text: String { return self._s[3765]! } + public var SettingsSearch_Synonyms_Data_Storage_KeepMedia: String { return self._s[3766]! } + public var UserInfo_FirstNamePlaceholder: String { return self._s[3767]! } + public var Conversation_SendMessageErrorFlood: String { return self._s[3768]! } + public var Conversation_EncryptedDescription2: String { return self._s[3769]! } + public var Conversation_CancelForwardText: String { return self._s[3770]! } + public var Notification_GroupActivated: String { return self._s[3771]! } + public var LastSeen_Lately: String { return self._s[3772]! } + public var Conversation_EncryptedDescription3: String { return self._s[3773]! } + public var SettingsSearch_Synonyms_Privacy_ProfilePhoto: String { return self._s[3774]! } + public var TwoStepAuth_RecoveryUnavailableResetText: String { return self._s[3775]! } + public var Conversation_SwipeToReplyHintText: String { return self._s[3776]! } + public var Conversation_EncryptedDescription4: String { return self._s[3777]! } + public var SharedMedia_EmptyTitle: String { return self._s[3778]! } + public var Appearance_CreateTheme: String { return self._s[3780]! } + public var Stats_SharesPerPost: String { return self._s[3781]! } + public var Contacts_TabTitle: String { return self._s[3782]! } + public var Weekday_ShortThursday: String { return self._s[3783]! } + public var MessageTimer_Forever: String { return self._s[3784]! } + public var ChatListFolder_CategoryArchived: String { return self._s[3785]! } + public var Channel_EditAdmin_PermissionDeleteMessages: String { return self._s[3786]! } + public var EditTheme_Create_TopInfo: String { return self._s[3788]! } + public var TwoFactorRemember_Forgot: String { return self._s[3789]! } public func VoiceOver_ChatList_MessageFrom(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3628]!, self._r[3628]!, [_0]) + return formatWithArgumentRanges(self._s[3790]!, self._r[3790]!, [_0]) } - public var Month_GenDecember: String { return self._s[3629]! } - public var EnterPasscode_EnterPasscode: String { return self._s[3630]! } - public var SettingsSearch_Synonyms_Appearance_LargeEmoji: String { return self._s[3631]! } - public var PeopleNearby_CreateGroup: String { return self._s[3633]! } - public var Group_EditAdmin_PermissionChangeInfo: String { return self._s[3634]! } - public var Paint_ClearConfirm: String { return self._s[3635]! } - public var ChatList_ReadAll: String { return self._s[3636]! } - public var ChatSettings_IntentsSettings: String { return self._s[3637]! } - public var Passport_PassportInformation: String { return self._s[3639]! } - public var Login_CheckOtherSessionMessages: String { return self._s[3641]! } - public var Location_ProximityNotification_DistanceMI: String { return self._s[3644]! } - public var PhotoEditor_ExposureTool: String { return self._s[3645]! } - public var Group_Username_CreatePrivateLinkHelp: String { return self._s[3646]! } - public var SettingsSearch_Synonyms_Watch: String { return self._s[3647]! } - public var Stats_GroupTopPoster_History: String { return self._s[3648]! } - public var UserInfo_AddPhone: String { return self._s[3649]! } - public var Media_SendWithTimer: String { return self._s[3651]! } - public var SettingsSearch_Synonyms_Notifications_Title: String { return self._s[3652]! } - public var Channel_EditAdmin_PermissionEnabledByDefault: String { return self._s[3653]! } - public var GroupInfo_GroupHistoryShort: String { return self._s[3654]! } - public var PasscodeSettings_AutoLock_Disabled: String { return self._s[3655]! } - public var ChatList_Context_Unarchive: String { return self._s[3657]! } + public var Month_GenDecember: String { return self._s[3791]! } + public var EnterPasscode_EnterPasscode: String { return self._s[3792]! } + public var SettingsSearch_Synonyms_Appearance_LargeEmoji: String { return self._s[3793]! } + public var PeopleNearby_CreateGroup: String { return self._s[3795]! } + public var Group_EditAdmin_PermissionChangeInfo: String { return self._s[3796]! } + public var Paint_ClearConfirm: String { return self._s[3797]! } + public var ChatList_ReadAll: String { return self._s[3798]! } + public var ChatSettings_IntentsSettings: String { return self._s[3799]! } + public var Passport_PassportInformation: String { return self._s[3801]! } + public var Login_CheckOtherSessionMessages: String { return self._s[3803]! } + public var Location_ProximityNotification_DistanceMI: String { return self._s[3806]! } + public var PhotoEditor_ExposureTool: String { return self._s[3807]! } + public var Group_Username_CreatePrivateLinkHelp: String { return self._s[3808]! } + public var SettingsSearch_Synonyms_Watch: String { return self._s[3809]! } + public var Stats_GroupTopPoster_History: String { return self._s[3810]! } + public var UserInfo_AddPhone: String { return self._s[3811]! } + public var Media_SendWithTimer: String { return self._s[3813]! } + public var SettingsSearch_Synonyms_Notifications_Title: String { return self._s[3814]! } + public var Channel_EditAdmin_PermissionEnabledByDefault: String { return self._s[3815]! } + public var GroupInfo_GroupHistoryShort: String { return self._s[3816]! } + public var PasscodeSettings_AutoLock_Disabled: String { return self._s[3817]! } + public var ChatList_Context_Unarchive: String { return self._s[3819]! } public func DialogList_LiveLocationSharingTo(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3658]!, self._r[3658]!, [_0]) + return formatWithArgumentRanges(self._s[3820]!, self._r[3820]!, [_0]) } - public var BlockedUsers_Title: String { return self._s[3660]! } - public var TwoStepAuth_EmailPlaceholder: String { return self._s[3661]! } - public var Media_ShareThisPhoto: String { return self._s[3662]! } - public var Notifications_DisplayNamesOnLockScreen: String { return self._s[3663]! } - public var Conversation_FilePhotoOrVideo: String { return self._s[3664]! } - public var Appearance_ThemePreview_Chat_2_ReplyName: String { return self._s[3668]! } - public var CallFeedback_ReasonNoise: String { return self._s[3670]! } - public var WebBrowser_Title: String { return self._s[3671]! } + public var BlockedUsers_Title: String { return self._s[3822]! } + public var TwoStepAuth_EmailPlaceholder: String { return self._s[3823]! } + public var Media_ShareThisPhoto: String { return self._s[3824]! } + public var Notifications_DisplayNamesOnLockScreen: String { return self._s[3825]! } + public var Conversation_FilePhotoOrVideo: String { return self._s[3826]! } + public var Appearance_ThemePreview_Chat_2_ReplyName: String { return self._s[3830]! } + public var CallFeedback_ReasonNoise: String { return self._s[3832]! } + public var WebBrowser_Title: String { return self._s[3833]! } public func Checkout_SavePasswordTimeoutAndTouchId(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3672]!, self._r[3672]!, [_0]) + return formatWithArgumentRanges(self._s[3834]!, self._r[3834]!, [_0]) } - public var Notification_MessageLifetime5s: String { return self._s[3674]! } - public var Passport_Address_AddResidentialAddress: String { return self._s[3675]! } - public var Profile_MessageLifetime1m: String { return self._s[3677]! } - public var Passport_ScanPassport: String { return self._s[3678]! } - public var Stats_LoadingTitle: String { return self._s[3679]! } - public var Passport_Address_AddTemporaryRegistration: String { return self._s[3681]! } - public var Permissions_NotificationsAllow_v0: String { return self._s[3682]! } - public var Login_InvalidFirstNameError: String { return self._s[3683]! } - public var Undo_ChatCleared: String { return self._s[3685]! } + public var Notification_MessageLifetime5s: String { return self._s[3836]! } + public var Passport_Address_AddResidentialAddress: String { return self._s[3837]! } + public var Profile_MessageLifetime1m: String { return self._s[3839]! } + public var Passport_ScanPassport: String { return self._s[3840]! } + public var Stats_LoadingTitle: String { return self._s[3841]! } + public var Passport_Address_AddTemporaryRegistration: String { return self._s[3843]! } + public var Permissions_NotificationsAllow_v0: String { return self._s[3844]! } + public var Login_InvalidFirstNameError: String { return self._s[3845]! } + public var Undo_ChatCleared: String { return self._s[3847]! } public func ApplyLanguage_ChangeLanguageUnofficialText(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3687]!, self._r[3687]!, [_1, _2]) + return formatWithArgumentRanges(self._s[3849]!, self._r[3849]!, [_1, _2]) } - public var Conversation_PinMessageAlertPin: String { return self._s[3688]! } + public var Conversation_PinMessageAlertPin: String { return self._s[3850]! } public func Login_PhoneBannedEmailBody(_ _1: String, _ _2: String, _ _3: String, _ _4: String, _ _5: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3689]!, self._r[3689]!, [_1, _2, _3, _4, _5]) + return formatWithArgumentRanges(self._s[3851]!, self._r[3851]!, [_1, _2, _3, _4, _5]) } public func PUSH_MESSAGE_FWD(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3690]!, self._r[3690]!, [_1]) + return formatWithArgumentRanges(self._s[3852]!, self._r[3852]!, [_1]) } - public var Share_MultipleMessagesDisabled: String { return self._s[3691]! } - public var TwoStepAuth_EmailInvalid: String { return self._s[3692]! } - public var EnterPasscode_ChangeTitle: String { return self._s[3694]! } - public var VoiceChat_InviteLink_Speaker: String { return self._s[3695]! } - public var CallSettings_RecentCalls: String { return self._s[3696]! } - public var GroupInfo_DeactivatedStatus: String { return self._s[3697]! } - public var AuthSessions_OtherSessions: String { return self._s[3698]! } - public var PrivacyLastSeenSettings_CustomHelp: String { return self._s[3699]! } - public var Tour_Text5: String { return self._s[3700]! } - public var Login_PadPhoneHelp: String { return self._s[3701]! } - public var Wallpaper_PhotoLibrary: String { return self._s[3703]! } - public var Conversation_ViewGroup: String { return self._s[3704]! } - public var PeopleNearby_MakeVisibleTitle: String { return self._s[3706]! } - public var VoiceOver_Chat_YourContact: String { return self._s[3707]! } - public var Watch_AuthRequired: String { return self._s[3708]! } - public var VoiceOver_Chat_ForwardedFromYou: String { return self._s[3710]! } - public var Conversation_ForwardContacts: String { return self._s[3711]! } - public var Conversation_InputTextPlaceholder: String { return self._s[3712]! } + public var Share_MultipleMessagesDisabled: String { return self._s[3853]! } + public var TwoStepAuth_EmailInvalid: String { return self._s[3854]! } + public var EnterPasscode_ChangeTitle: String { return self._s[3856]! } + public var VoiceChat_InviteLink_Speaker: String { return self._s[3857]! } + public var CallSettings_RecentCalls: String { return self._s[3858]! } + public var GroupInfo_DeactivatedStatus: String { return self._s[3859]! } + public var AuthSessions_OtherSessions: String { return self._s[3860]! } + public var PrivacyLastSeenSettings_CustomHelp: String { return self._s[3861]! } + public var Tour_Text5: String { return self._s[3862]! } + public var Login_PadPhoneHelp: String { return self._s[3863]! } + public var Wallpaper_PhotoLibrary: String { return self._s[3866]! } + public var Conversation_ViewGroup: String { return self._s[3867]! } + public var PeopleNearby_MakeVisibleTitle: String { return self._s[3869]! } + public var VoiceOver_Chat_YourContact: String { return self._s[3870]! } + public var Watch_AuthRequired: String { return self._s[3871]! } + public var VoiceOver_Chat_ForwardedFromYou: String { return self._s[3873]! } + public var Conversation_ForwardContacts: String { return self._s[3874]! } + public var Conversation_InputTextPlaceholder: String { return self._s[3875]! } public func PUSH_CHANNEL_MESSAGE_PHOTO(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3713]!, self._r[3713]!, [_1]) + return formatWithArgumentRanges(self._s[3876]!, self._r[3876]!, [_1]) } public func Conversation_MessageViaUser(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3714]!, self._r[3714]!, [_0]) + return formatWithArgumentRanges(self._s[3877]!, self._r[3877]!, [_0]) } - public var Channel_Setup_TypePrivate: String { return self._s[3715]! } + public var Channel_Setup_TypePrivate: String { return self._s[3878]! } public func Conversation_NoticeInvitedByInChannel(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3716]!, self._r[3716]!, [_0]) + return formatWithArgumentRanges(self._s[3879]!, self._r[3879]!, [_0]) } - public var InviteLink_Create_TimeLimitExpiryDate: String { return self._s[3717]! } - public var InfoPlist_NSSiriUsageDescription: String { return self._s[3718]! } - public var AutoDownloadSettings_Delimeter: String { return self._s[3719]! } - public var EmptyGroupInfo_Subtitle: String { return self._s[3720]! } - public var UserInfo_StartSecretChatStart: String { return self._s[3721]! } + public var Checkout_OptionalTipItemPlaceholder: String { return self._s[3880]! } + public var InviteLink_Create_TimeLimitExpiryDate: String { return self._s[3881]! } + public var InfoPlist_NSSiriUsageDescription: String { return self._s[3882]! } + public var AutoDownloadSettings_Delimeter: String { return self._s[3883]! } + public var EmptyGroupInfo_Subtitle: String { return self._s[3884]! } + public var UserInfo_StartSecretChatStart: String { return self._s[3885]! } public func GroupPermission_AddedInfo(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3722]!, self._r[3722]!, [_1, _2]) - } - public func Channel_AdminLog_MessageRestricted(_ _0: String, _ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3723]!, self._r[3723]!, [_0, _1, _2]) - } - public func Conversation_ForwardTooltip_TwoChats_Many(_ _0: String, _ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3724]!, self._r[3724]!, [_0, _1]) - } - public var PrivacySettings_AutoArchiveTitle: String { return self._s[3725]! } - public var GroupInfo_InviteLink_LinkSection: String { return self._s[3726]! } - public var FastTwoStepSetup_EmailPlaceholder: String { return self._s[3727]! } - public var StickerPacksSettings_ArchivedMasks: String { return self._s[3729]! } - public var NewContact_Title: String { return self._s[3732]! } - public var Appearance_ThemeCarouselTintedNight: String { return self._s[3733]! } - public var VoiceChat_StatusSpeaking: String { return self._s[3734]! } - public var Notifications_PermissionsKeepDisabled: String { return self._s[3735]! } - public func Time_YesterdayAt(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3736]!, self._r[3736]!, [_0]) - } - public func AutoNightTheme_LocationHelp(_ _0: String, _ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3737]!, self._r[3737]!, [_0, _1]) - } - public var Chat_SlowmodeTooltipPending: String { return self._s[3738]! } - public func Time_MediumDate(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3740]!, self._r[3740]!, [_1, _2]) - } - public var ContactInfo_PhoneLabelHome: String { return self._s[3741]! } - public var CallFeedback_ReasonInterruption: String { return self._s[3742]! } - public var Passport_Identity_OneOfTypeDriversLicense: String { return self._s[3743]! } - public func PUSH_MESSAGE_DOCS(_ _1: String, _ _2: Int) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3746]!, self._r[3746]!, [_1, "\(_2)"]) - } - public var Conversation_MessageEditedLabel: String { return self._s[3747]! } - public var CallList_ActiveVoiceChatsHeader: String { return self._s[3748]! } - public var SocksProxySetup_PasswordPlaceholder: String { return self._s[3749]! } - public var ChatList_Context_AddToContacts: String { return self._s[3750]! } - public var Passport_Language_is: String { return self._s[3751]! } - public var Notification_PassportValueProofOfIdentity: String { return self._s[3752]! } - public var PhotoEditor_CurvesBlue: String { return self._s[3753]! } - public func FileSize_MB(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3754]!, self._r[3754]!, [_0]) - } - public var SocksProxySetup_Username: String { return self._s[3755]! } - public var Login_SmsRequestState3: String { return self._s[3756]! } - public var Message_PinnedVideoMessage: String { return self._s[3757]! } - public var SharedMedia_TitleLink: String { return self._s[3758]! } - public var Passport_FieldIdentity: String { return self._s[3759]! } - public var GroupInfo_Permissions_BroadcastConvert: String { return self._s[3761]! } - public func Conversation_EncryptedPlaceholderTitleOutgoing(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3764]!, self._r[3764]!, [_0]) - } - public var DialogList_ProxyConnectionIssuesTooltip: String { return self._s[3767]! } - public var ReportSpam_DeleteThisChat: String { return self._s[3768]! } - public var Checkout_NewCard_CardholderNamePlaceholder: String { return self._s[3769]! } - public var Passport_Identity_DateOfBirth: String { return self._s[3770]! } - public var Call_StatusIncoming: String { return self._s[3771]! } - public var ChatAdmins_AdminLabel: String { return self._s[3772]! } - public func InstantPage_OpenInBrowser(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3773]!, self._r[3773]!, [_0]) - } - public func Time_MonthOfYear_m10(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3775]!, self._r[3775]!, [_0]) - } - public var Message_PinnedAnimationMessage: String { return self._s[3776]! } - public var Conversation_ReportSpamAndLeave: String { return self._s[3777]! } - public var Preview_CopyAddress: String { return self._s[3778]! } - public var MediaPlayer_UnknownTrack: String { return self._s[3780]! } - public var Login_CancelSignUpConfirmation: String { return self._s[3781]! } - public var Map_OpenInYandexMaps: String { return self._s[3783]! } - public func Time_PreciseDate_m11(_ _1: String, _ _2: String, _ _3: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3786]!, self._r[3786]!, [_1, _2, _3]) - } - public var GroupRemoved_Remove: String { return self._s[3787]! } - public var ChatListFolder_TitleCreate: String { return self._s[3788]! } - public func InstantPage_AuthorAndDateTitle(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3790]!, self._r[3790]!, [_1, _2]) - } - public var Watch_UserInfo_MuteTitle: String { return self._s[3791]! } - public var Group_UpgradeNoticeText2: String { return self._s[3793]! } - public var Stats_GroupGrowthTitle: String { return self._s[3794]! } - public var CreatePoll_CancelConfirmation: String { return self._s[3797]! } - public var Month_GenOctober: String { return self._s[3798]! } - public var Conversation_TitleCommentsEmpty: String { return self._s[3799]! } - public var Settings_Appearance: String { return self._s[3800]! } - public func Time_MonthOfYear_m6(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3801]!, self._r[3801]!, [_0]) - } - public var UserInfo_AddToExisting: String { return self._s[3802]! } - public var Call_PhoneCallInProgressMessage: String { return self._s[3804]! } - public var Map_HomeAndWorkInfo: String { return self._s[3805]! } - public var InstantPage_VoiceOver_ResetFontSize: String { return self._s[3806]! } - public var Paint_Arrow: String { return self._s[3807]! } - public var InviteLink_CreatePrivateLinkHelp: String { return self._s[3808]! } - public func DialogList_MultipleTypingPair(_ _0: String, _ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3809]!, self._r[3809]!, [_0, _1]) - } - public var CancelResetAccount_Title: String { return self._s[3810]! } - public var NotificationsSound_Circles: String { return self._s[3811]! } - public var Notifications_GroupNotificationsExceptionsHelp: String { return self._s[3812]! } - public var ChatState_Connecting: String { return self._s[3814]! } - public var Profile_MessageLifetime5s: String { return self._s[3815]! } - public func DialogList_AwaitingEncryption(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3816]!, self._r[3816]!, [_0]) - } - public var PrivacyPolicy_AgeVerificationTitle: String { return self._s[3817]! } - public var Channel_Username_CreatePublicLinkHelp: String { return self._s[3818]! } - public var AutoNightTheme_ScheduledTo: String { return self._s[3819]! } - public var Conversation_DefaultRestrictedStickers: String { return self._s[3821]! } - public var TwoStepAuth_ConfirmationTitle: String { return self._s[3822]! } - public func Chat_UnsendMyMessagesAlertTitle(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3823]!, self._r[3823]!, [_0]) - } - public var Passport_Phone_Help: String { return self._s[3824]! } - public var Privacy_ContactsSync: String { return self._s[3825]! } - public var CheckoutInfo_ReceiverInfoPhone: String { return self._s[3826]! } - public var Channel_AdminLogFilter_EventsLeavingSubscribers: String { return self._s[3828]! } - public var Map_SendMyCurrentLocation: String { return self._s[3829]! } - public var Map_AddressOnMap: String { return self._s[3830]! } - public var BroadcastGroups_ConfirmationAlert_Convert: String { return self._s[3832]! } - public var DialogList_SearchLabel: String { return self._s[3833]! } - public var Notification_Exceptions_NewException_NotificationHeader: String { return self._s[3834]! } - public var GroupInfo_FakeGroupWarning: String { return self._s[3835]! } - public var Conversation_ChecksTooltip_Read: String { return self._s[3837]! } - public var ConversationProfile_UnknownAddMemberError: String { return self._s[3838]! } - public var ChatList_Search_ShowMore: String { return self._s[3839]! } - public var DialogList_EncryptionRejected: String { return self._s[3840]! } - public var VoiceChat_InviteLinkCopiedText: String { return self._s[3841]! } - public var DialogList_DeleteBotConfirmation: String { return self._s[3842]! } - public var VoiceChat_StartRecordingText: String { return self._s[3843]! } - public var Privacy_TopPeersDelete: String { return self._s[3844]! } - public var AttachmentMenu_SendAsFile: String { return self._s[3846]! } - public var ChatList_GenericPsaAlert: String { return self._s[3848]! } - public var SecretTimer_ImageDescription: String { return self._s[3850]! } - public func Conversation_SetReminder_RemindOn(_ _0: String, _ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3851]!, self._r[3851]!, [_0, _1]) - } - public var ChatSettings_TextSizeUnits: String { return self._s[3852]! } - public var Notification_RenamedGroup: String { return self._s[3854]! } - public var Tour_Title2: String { return self._s[3855]! } - public var Settings_CopyUsername: String { return self._s[3856]! } - public var Compose_NewEncryptedChat: String { return self._s[3857]! } - public var Conversation_CloudStorageInfo_Title: String { return self._s[3858]! } - public var Month_ShortSeptember: String { return self._s[3859]! } - public var AutoDownloadSettings_OnForAll: String { return self._s[3860]! } - public var ChatList_DeleteForEveryoneConfirmationText: String { return self._s[3861]! } - public var Call_StatusConnecting: String { return self._s[3863]! } - public var Privacy_GroupsAndChannels_NeverAllow_Placeholder: String { return self._s[3864]! } - public var Map_ShareLiveLocationHelp: String { return self._s[3865]! } - public var Cache_Files: String { return self._s[3866]! } - public var Notifications_Reset: String { return self._s[3867]! } - public func Settings_KeepPhoneNumber(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3868]!, self._r[3868]!, [_0]) - } - public var Privacy_GroupsAndChannels_AlwaysAllow_Title: String { return self._s[3869]! } - public func Conversation_OpenBotLinkLogin(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3870]!, self._r[3870]!, [_1, _2]) - } - public var Notification_CallIncomingShort: String { return self._s[3871]! } - public var UserInfo_BotPrivacy: String { return self._s[3874]! } - public var Appearance_BubbleCorners_Apply: String { return self._s[3875]! } - public var WebSearch_RecentClearConfirmation: String { return self._s[3876]! } - public var Conversation_ContextMenuLookUp: String { return self._s[3878]! } - public var Calls_RatingTitle: String { return self._s[3879]! } - public var SecretImage_Title: String { return self._s[3880]! } - public var Weekday_Monday: String { return self._s[3881]! } - public func Passport_PrivacyPolicy(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3882]!, self._r[3882]!, [_1, _2]) - } - public var KeyCommand_JumpToPreviousChat: String { return self._s[3883]! } - public var VoiceChat_InviteLink_CopySpeakerLink: String { return self._s[3884]! } - public var Invitation_JoinVoiceChatAsListener: String { return self._s[3885]! } - public func DialogList_SearchSubtitleFormat(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(self._s[3886]!, self._r[3886]!, [_1, _2]) } - public var Stats_GroupMembers: String { return self._s[3887]! } - public var Camera_Retake: String { return self._s[3888]! } - public var Conversation_SearchPlaceholder: String { return self._s[3890]! } - public func Passport_Identity_NativeNameGenericHelp(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3891]!, self._r[3891]!, [_0]) + public func Channel_AdminLog_MessageRestricted(_ _0: String, _ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[3887]!, self._r[3887]!, [_0, _1, _2]) } - public var Channel_DiscussionGroup_Info: String { return self._s[3892]! } - public var SocksProxySetup_Hostname: String { return self._s[3893]! } - public var PrivacyLastSeenSettings_EmpryUsersPlaceholder: String { return self._s[3894]! } - public var Privacy_DeleteDrafts: String { return self._s[3896]! } - public func Checkout_LiabilityAlert(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3897]!, self._r[3897]!, [_1, _1, _1, _2]) + public func Conversation_ForwardTooltip_TwoChats_Many(_ _0: String, _ _1: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[3888]!, self._r[3888]!, [_0, _1]) } - public var Login_CancelPhoneVerification: String { return self._s[3899]! } - public var TwoStepAuth_ResetAccountHelp: String { return self._s[3900]! } - public var VoiceOver_Chat_Profile: String { return self._s[3901]! } - public func SocksProxySetup_ProxyStatusPing(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3902]!, self._r[3902]!, [_0]) + public var PrivacySettings_AutoArchiveTitle: String { return self._s[3889]! } + public var GroupInfo_InviteLink_LinkSection: String { return self._s[3890]! } + public var FastTwoStepSetup_EmailPlaceholder: String { return self._s[3891]! } + public var StickerPacksSettings_ArchivedMasks: String { return self._s[3893]! } + public var NewContact_Title: String { return self._s[3896]! } + public var Appearance_ThemeCarouselTintedNight: String { return self._s[3897]! } + public var VoiceChat_StatusSpeaking: String { return self._s[3898]! } + public var Notifications_PermissionsKeepDisabled: String { return self._s[3899]! } + public func Time_YesterdayAt(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[3900]!, self._r[3900]!, [_0]) } - public var TwoStepAuth_EmailSent: String { return self._s[3903]! } - public var Cache_Indexing: String { return self._s[3904]! } - public var Notifications_ExceptionsNone: String { return self._s[3905]! } - public var MessagePoll_LabelQuiz: String { return self._s[3906]! } - public var Call_EncryptionKey_Title: String { return self._s[3907]! } - public var Common_Yes: String { return self._s[3908]! } - public var Channel_ErrorAddBlocked: String { return self._s[3909]! } - public var Month_GenJanuary: String { return self._s[3910]! } - public var Checkout_NewCard_Title: String { return self._s[3911]! } - public func TwoStepAuth_EnterPasswordHint(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3912]!, self._r[3912]!, [_0]) + public func AutoNightTheme_LocationHelp(_ _0: String, _ _1: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[3901]!, self._r[3901]!, [_0, _1]) } - public var Conversation_InputTextPlaceholderReply: String { return self._s[3914]! } - public var PasscodeSettings_AutoLock_IfAwayFor_1hour: String { return self._s[3915]! } - public var Conversation_SendDice: String { return self._s[3916]! } - public func ChatSettings_AutoDownloadSettings_TypeVideo(_ _0: String) -> (String, [(Int, NSRange)]) { + public var Chat_SlowmodeTooltipPending: String { return self._s[3902]! } + public func Time_MediumDate(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[3904]!, self._r[3904]!, [_1, _2]) + } + public var ContactInfo_PhoneLabelHome: String { return self._s[3905]! } + public var CallFeedback_ReasonInterruption: String { return self._s[3906]! } + public var Passport_Identity_OneOfTypeDriversLicense: String { return self._s[3907]! } + public var Conversation_MessageEditedLabel: String { return self._s[3910]! } + public var CallList_ActiveVoiceChatsHeader: String { return self._s[3911]! } + public var SocksProxySetup_PasswordPlaceholder: String { return self._s[3912]! } + public var ChatList_Context_AddToContacts: String { return self._s[3913]! } + public var Passport_Language_is: String { return self._s[3914]! } + public var Notification_PassportValueProofOfIdentity: String { return self._s[3915]! } + public var PhotoEditor_CurvesBlue: String { return self._s[3916]! } + public func FileSize_MB(_ _0: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(self._s[3917]!, self._r[3917]!, [_0]) } - public func VoiceOver_Chat_VideoFrom(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3918]!, self._r[3918]!, [_0]) + public var SocksProxySetup_Username: String { return self._s[3918]! } + public var Login_SmsRequestState3: String { return self._s[3919]! } + public var Message_PinnedVideoMessage: String { return self._s[3920]! } + public var SharedMedia_TitleLink: String { return self._s[3921]! } + public var Passport_FieldIdentity: String { return self._s[3922]! } + public var GroupInfo_Permissions_BroadcastConvert: String { return self._s[3924]! } + public func Conversation_EncryptedPlaceholderTitleOutgoing(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[3927]!, self._r[3927]!, [_0]) } - public var Weekday_Wednesday: String { return self._s[3919]! } - public var ReportPeer_ReasonOther_Send: String { return self._s[3920]! } - public var PasscodeSettings_EncryptDataHelp: String { return self._s[3921]! } - public var PrivacyLastSeenSettings_CustomShareSettingsHelp: String { return self._s[3922]! } - public var OldChannels_NoticeTitle: String { return self._s[3923]! } - public var TwoStepAuth_ChangeEmail: String { return self._s[3924]! } - public var PasscodeSettings_PasscodeOptions: String { return self._s[3925]! } - public var InfoPlist_NSPhotoLibraryUsageDescription: String { return self._s[3926]! } - public var Passport_Address_AddUtilityBill: String { return self._s[3927]! } - public func Time_PreciseDate_m5(_ _1: String, _ _2: String, _ _3: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3929]!, self._r[3929]!, [_1, _2, _3]) + public var DialogList_ProxyConnectionIssuesTooltip: String { return self._s[3930]! } + public var ReportSpam_DeleteThisChat: String { return self._s[3931]! } + public var Checkout_NewCard_CardholderNamePlaceholder: String { return self._s[3932]! } + public var Passport_Identity_DateOfBirth: String { return self._s[3933]! } + public var Call_StatusIncoming: String { return self._s[3934]! } + public var ChatAdmins_AdminLabel: String { return self._s[3935]! } + public func InstantPage_OpenInBrowser(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[3936]!, self._r[3936]!, [_0]) } - public var TwoFactorSetup_EmailVerification_ResendAction: String { return self._s[3931]! } - public var Stats_GroupTopAdminsTitle: String { return self._s[3932]! } - public var Paint_Regular: String { return self._s[3933]! } - public var Message_Contact: String { return self._s[3934]! } - public var NetworkUsageSettings_MediaVideoDataSection: String { return self._s[3935]! } - public var VoiceOver_Chat_YourPhoto: String { return self._s[3936]! } - public var Notification_Mute1hMin: String { return self._s[3937]! } - public func Login_BannedPhoneSubject(_ _0: String) -> (String, [(Int, NSRange)]) { + public func Time_MonthOfYear_m10(_ _0: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(self._s[3938]!, self._r[3938]!, [_0]) } - public var Profile_MessageLifetime1h: String { return self._s[3939]! } - public var TwoStepAuth_GenericHelp: String { return self._s[3940]! } - public var TextFormat_Monospace: String { return self._s[3941]! } - public var VoiceOver_Media_PlaybackRateChange: String { return self._s[3943]! } - public var Conversation_DeleteMessagesForMe: String { return self._s[3944]! } - public var ChatList_DeleteChat: String { return self._s[3945]! } - public var Channel_OwnershipTransfer_EnterPasswordText: String { return self._s[3948]! } + public var Message_PinnedAnimationMessage: String { return self._s[3939]! } + public var VoiceChat_TapToViewCameraVideo: String { return self._s[3940]! } + public var Conversation_ReportSpamAndLeave: String { return self._s[3941]! } + public var Preview_CopyAddress: String { return self._s[3942]! } + public var MediaPlayer_UnknownTrack: String { return self._s[3944]! } + public var Login_CancelSignUpConfirmation: String { return self._s[3945]! } + public var Map_OpenInYandexMaps: String { return self._s[3947]! } + public func Time_PreciseDate_m11(_ _1: String, _ _2: String, _ _3: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[3950]!, self._r[3950]!, [_1, _2, _3]) + } + public var GroupRemoved_Remove: String { return self._s[3951]! } + public var ChatListFolder_TitleCreate: String { return self._s[3952]! } + public func InstantPage_AuthorAndDateTitle(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[3954]!, self._r[3954]!, [_1, _2]) + } + public var Watch_UserInfo_MuteTitle: String { return self._s[3955]! } + public func UserInfo_LinkForwardTooltip_TwoChats_One(_ _0: String, _ _1: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[3957]!, self._r[3957]!, [_0, _1]) + } + public var Group_UpgradeNoticeText2: String { return self._s[3958]! } + public var Stats_GroupGrowthTitle: String { return self._s[3959]! } + public var CreatePoll_CancelConfirmation: String { return self._s[3962]! } + public var Month_GenOctober: String { return self._s[3963]! } + public var Conversation_TitleCommentsEmpty: String { return self._s[3964]! } + public var Settings_Appearance: String { return self._s[3965]! } + public func Time_MonthOfYear_m6(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[3966]!, self._r[3966]!, [_0]) + } + public var UserInfo_AddToExisting: String { return self._s[3967]! } + public var Call_PhoneCallInProgressMessage: String { return self._s[3969]! } + public var Map_HomeAndWorkInfo: String { return self._s[3970]! } + public var VoiceChat_ContextAudio: String { return self._s[3971]! } + public var InstantPage_VoiceOver_ResetFontSize: String { return self._s[3972]! } + public var Paint_Arrow: String { return self._s[3973]! } + public var InviteLink_CreatePrivateLinkHelp: String { return self._s[3974]! } + public func DialogList_MultipleTypingPair(_ _0: String, _ _1: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[3975]!, self._r[3975]!, [_0, _1]) + } + public var CancelResetAccount_Title: String { return self._s[3976]! } + public var NotificationsSound_Circles: String { return self._s[3977]! } + public var Notifications_GroupNotificationsExceptionsHelp: String { return self._s[3978]! } + public var ChatState_Connecting: String { return self._s[3980]! } + public var Profile_MessageLifetime5s: String { return self._s[3981]! } + public func DialogList_AwaitingEncryption(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[3982]!, self._r[3982]!, [_0]) + } + public var PrivacyPolicy_AgeVerificationTitle: String { return self._s[3983]! } + public var Channel_Username_CreatePublicLinkHelp: String { return self._s[3984]! } + public var AutoNightTheme_ScheduledTo: String { return self._s[3985]! } + public var Conversation_DefaultRestrictedStickers: String { return self._s[3987]! } + public var TwoStepAuth_ConfirmationTitle: String { return self._s[3988]! } + public func Chat_UnsendMyMessagesAlertTitle(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[3989]!, self._r[3989]!, [_0]) + } + public var Passport_Phone_Help: String { return self._s[3990]! } + public var Privacy_ContactsSync: String { return self._s[3991]! } + public var CheckoutInfo_ReceiverInfoPhone: String { return self._s[3992]! } + public var Channel_AdminLogFilter_EventsLeavingSubscribers: String { return self._s[3994]! } + public var Map_SendMyCurrentLocation: String { return self._s[3995]! } + public var Map_AddressOnMap: String { return self._s[3996]! } + public var BroadcastGroups_ConfirmationAlert_Convert: String { return self._s[3998]! } + public var DialogList_SearchLabel: String { return self._s[3999]! } + public var Notification_Exceptions_NewException_NotificationHeader: String { return self._s[4000]! } + public var GroupInfo_FakeGroupWarning: String { return self._s[4001]! } + public var Conversation_ChecksTooltip_Read: String { return self._s[4003]! } + public var TwoFactorRemember_Placeholder: String { return self._s[4005]! } + public var ConversationProfile_UnknownAddMemberError: String { return self._s[4006]! } + public var ChatList_Search_ShowMore: String { return self._s[4007]! } + public var DialogList_EncryptionRejected: String { return self._s[4008]! } + public var VoiceChat_InviteLinkCopiedText: String { return self._s[4009]! } + public var DialogList_DeleteBotConfirmation: String { return self._s[4010]! } + public var VoiceChat_StartRecordingText: String { return self._s[4011]! } + public var Privacy_TopPeersDelete: String { return self._s[4012]! } + public var AttachmentMenu_SendAsFile: String { return self._s[4014]! } + public var ChatList_GenericPsaAlert: String { return self._s[4016]! } + public var SecretTimer_ImageDescription: String { return self._s[4018]! } + public func Conversation_SetReminder_RemindOn(_ _0: String, _ _1: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[4019]!, self._r[4019]!, [_0, _1]) + } + public var VoiceChat_EditNameSuccess: String { return self._s[4020]! } + public var ChatSettings_TextSizeUnits: String { return self._s[4021]! } + public var Notification_RenamedGroup: String { return self._s[4023]! } + public var Tour_Title2: String { return self._s[4024]! } + public var Settings_CopyUsername: String { return self._s[4025]! } + public var Compose_NewEncryptedChat: String { return self._s[4026]! } + public var Conversation_CloudStorageInfo_Title: String { return self._s[4027]! } + public var VoiceChat_SetReminder: String { return self._s[4028]! } + public var Month_ShortSeptember: String { return self._s[4029]! } + public var AutoDownloadSettings_OnForAll: String { return self._s[4030]! } + public var ChatList_DeleteForEveryoneConfirmationText: String { return self._s[4031]! } + public var VoiceChat_StartNow: String { return self._s[4032]! } + public var Call_StatusConnecting: String { return self._s[4034]! } + public var Privacy_GroupsAndChannels_NeverAllow_Placeholder: String { return self._s[4035]! } + public var Map_ShareLiveLocationHelp: String { return self._s[4036]! } + public var Cache_Files: String { return self._s[4037]! } + public var Notifications_Reset: String { return self._s[4038]! } + public func Settings_KeepPhoneNumber(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[4039]!, self._r[4039]!, [_0]) + } + public var Privacy_GroupsAndChannels_AlwaysAllow_Title: String { return self._s[4040]! } + public func Conversation_OpenBotLinkLogin(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[4041]!, self._r[4041]!, [_1, _2]) + } + public var Notification_CallIncomingShort: String { return self._s[4042]! } + public var UserInfo_BotPrivacy: String { return self._s[4045]! } + public var Appearance_BubbleCorners_Apply: String { return self._s[4046]! } + public var WebSearch_RecentClearConfirmation: String { return self._s[4047]! } + public var Conversation_ContextMenuLookUp: String { return self._s[4049]! } + public var Calls_RatingTitle: String { return self._s[4050]! } + public var SecretImage_Title: String { return self._s[4051]! } + public var Weekday_Monday: String { return self._s[4052]! } + public func Passport_PrivacyPolicy(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[4053]!, self._r[4053]!, [_1, _2]) + } + public var KeyCommand_JumpToPreviousChat: String { return self._s[4054]! } + public var VoiceChat_InviteLink_CopySpeakerLink: String { return self._s[4055]! } + public var Invitation_JoinVoiceChatAsListener: String { return self._s[4056]! } + public func DialogList_SearchSubtitleFormat(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[4057]!, self._r[4057]!, [_1, _2]) + } + public var Stats_GroupMembers: String { return self._s[4058]! } + public var Camera_Retake: String { return self._s[4059]! } + public var Conversation_SearchPlaceholder: String { return self._s[4061]! } + public func Passport_Identity_NativeNameGenericHelp(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[4062]!, self._r[4062]!, [_0]) + } + public var Channel_DiscussionGroup_Info: String { return self._s[4063]! } + public var SocksProxySetup_Hostname: String { return self._s[4064]! } + public var PrivacyLastSeenSettings_EmpryUsersPlaceholder: String { return self._s[4065]! } + public var Privacy_DeleteDrafts: String { return self._s[4067]! } + public var Login_CancelPhoneVerification: String { return self._s[4069]! } + public var TwoStepAuth_ResetAccountHelp: String { return self._s[4070]! } + public var VoiceOver_Chat_Profile: String { return self._s[4071]! } + public func SocksProxySetup_ProxyStatusPing(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[4072]!, self._r[4072]!, [_0]) + } + public var TwoStepAuth_EmailSent: String { return self._s[4073]! } + public var Cache_Indexing: String { return self._s[4074]! } + public var Notifications_ExceptionsNone: String { return self._s[4075]! } + public var MessagePoll_LabelQuiz: String { return self._s[4076]! } + public var Call_EncryptionKey_Title: String { return self._s[4077]! } + public var Common_Yes: String { return self._s[4078]! } + public var Channel_ErrorAddBlocked: String { return self._s[4079]! } + public var Month_GenJanuary: String { return self._s[4080]! } + public var Checkout_NewCard_Title: String { return self._s[4081]! } + public func TwoStepAuth_EnterPasswordHint(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[4082]!, self._r[4082]!, [_0]) + } + public var Conversation_InputTextPlaceholderReply: String { return self._s[4084]! } + public var PasscodeSettings_AutoLock_IfAwayFor_1hour: String { return self._s[4085]! } + public var Conversation_SendDice: String { return self._s[4086]! } + public func ChatSettings_AutoDownloadSettings_TypeVideo(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[4087]!, self._r[4087]!, [_0]) + } + public func VoiceOver_Chat_VideoFrom(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[4088]!, self._r[4088]!, [_0]) + } + public var Weekday_Wednesday: String { return self._s[4089]! } + public var ReportPeer_ReasonOther_Send: String { return self._s[4090]! } + public var PasscodeSettings_EncryptDataHelp: String { return self._s[4091]! } + public var PrivacyLastSeenSettings_CustomShareSettingsHelp: String { return self._s[4092]! } + public var OldChannels_NoticeTitle: String { return self._s[4093]! } + public var TwoStepAuth_ChangeEmail: String { return self._s[4094]! } + public var PasscodeSettings_PasscodeOptions: String { return self._s[4095]! } + public var InfoPlist_NSPhotoLibraryUsageDescription: String { return self._s[4096]! } + public var Passport_Address_AddUtilityBill: String { return self._s[4097]! } + public func Time_PreciseDate_m5(_ _1: String, _ _2: String, _ _3: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[4099]!, self._r[4099]!, [_1, _2, _3]) + } + public var TwoFactorSetup_EmailVerification_ResendAction: String { return self._s[4101]! } + public var Stats_GroupTopAdminsTitle: String { return self._s[4102]! } + public var Paint_Regular: String { return self._s[4104]! } + public var Message_Contact: String { return self._s[4105]! } + public var NetworkUsageSettings_MediaVideoDataSection: String { return self._s[4106]! } + public var VoiceOver_Chat_YourPhoto: String { return self._s[4107]! } + public var Notification_Mute1hMin: String { return self._s[4108]! } + public func Login_BannedPhoneSubject(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[4109]!, self._r[4109]!, [_0]) + } + public var Profile_MessageLifetime1h: String { return self._s[4110]! } + public var TwoStepAuth_GenericHelp: String { return self._s[4111]! } + public var TwoFactorSetup_PasswordRecovery_Skip: String { return self._s[4112]! } + public var TextFormat_Monospace: String { return self._s[4113]! } + public var VoiceOver_Media_PlaybackRateChange: String { return self._s[4115]! } + public var Conversation_DeleteMessagesForMe: String { return self._s[4116]! } + public var ChatList_DeleteChat: String { return self._s[4117]! } + public var Channel_OwnershipTransfer_EnterPasswordText: String { return self._s[4120]! } public func Settings_ApplyProxyAlertCredentials(_ _1: String, _ _2: String, _ _3: String, _ _4: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3949]!, self._r[3949]!, [_1, _2, _3, _4]) + return formatWithArgumentRanges(self._s[4121]!, self._r[4121]!, [_1, _2, _3, _4]) } - public var Login_CancelPhoneVerificationStop: String { return self._s[3950]! } - public var Appearance_ThemePreview_ChatList_4_Name: String { return self._s[3951]! } - public var MediaPicker_MomentsDateRangeSameMonthYearFormat: String { return self._s[3952]! } + public var Login_CancelPhoneVerificationStop: String { return self._s[4122]! } + public var Appearance_ThemePreview_ChatList_4_Name: String { return self._s[4123]! } + public var MediaPicker_MomentsDateRangeSameMonthYearFormat: String { return self._s[4124]! } public func Channel_AdminLog_MessageToggleInvitesOn(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3953]!, self._r[3953]!, [_0]) - } - public var Notifications_Badge_IncludeChannels: String { return self._s[3954]! } - public var InviteLink_CreatePrivateLinkHelpChannel: String { return self._s[3955]! } - public var StickerPack_ViewPack: String { return self._s[3958]! } - public var FastTwoStepSetup_PasswordConfirmationPlaceholder: String { return self._s[3960]! } - public var EditTheme_Expand_Preview_IncomingText: String { return self._s[3961]! } - public var Notifications_Title: String { return self._s[3962]! } - public var Conversation_InputTextPlaceholderComment: String { return self._s[3963]! } - public var GroupInfo_PublicLink: String { return self._s[3964]! } - public var VoiceOver_DiscardPreparedContent: String { return self._s[3965]! } - public var Conversation_Moderate_Ban: String { return self._s[3969]! } - public var InviteLink_Manage: String { return self._s[3970]! } - public var InstantPage_FontNewYork: String { return self._s[3971]! } - public func Activity_RemindAboutGroup(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3972]!, self._r[3972]!, [_0]) - } - public var TextFormat_Underline: String { return self._s[3973]! } - public func DownloadingStatus(_ _0: String, _ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3974]!, self._r[3974]!, [_0, _1]) - } - public func PUSH_PINNED_ROUND(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3975]!, self._r[3975]!, [_1]) - } - public var PollResults_Collapse: String { return self._s[3977]! } - public var Contacts_GlobalSearch: String { return self._s[3978]! } - public func Conversation_EncryptionWaiting(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3980]!, self._r[3980]!, [_0]) - } - public var Channel_Management_LabelEditor: String { return self._s[3981]! } - public var SettingsSearch_Synonyms_Stickers_FeaturedPacks: String { return self._s[3983]! } - public var Conversation_Theme: String { return self._s[3984]! } - public func PUSH_CHANNEL_MESSAGE_DOCS(_ _1: String, _ _2: Int) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3985]!, self._r[3985]!, [_1, "\(_2)"]) - } - public var Conversation_LinkDialogSave: String { return self._s[3986]! } - public var EnterPasscode_TouchId: String { return self._s[3987]! } - public var Conversation_VoiceChatMediaRecordingRestricted: String { return self._s[3988]! } - public var Group_ErrorAdminsTooMuch: String { return self._s[3989]! } - public var Stats_MessageOverview: String { return self._s[3990]! } - public var Privacy_Calls_P2PAlways: String { return self._s[3992]! } - public var Message_Sticker: String { return self._s[3993]! } - public var Conversation_Mute: String { return self._s[3996]! } - public var VoiceChat_AnonymousDisabledAlertText: String { return self._s[3997]! } - public var ContactInfo_Title: String { return self._s[3998]! } - public func PUSH_CHANNEL_MESSAGE_CONTACT(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[3999]!, self._r[3999]!, [_1]) - } - public var Channel_Setup_TypeHeader: String { return self._s[4000]! } - public var AuthSessions_LogOut: String { return self._s[4001]! } - public var ChatSettings_AutoDownloadReset: String { return self._s[4002]! } - public var Group_Info_Members: String { return self._s[4004]! } - public var ChatListFolderSettings_NewFolder: String { return self._s[4005]! } - public var Appearance_ThemePreview_ChatList_3_AuthorName: String { return self._s[4006]! } - public var CreatePoll_Title: String { return self._s[4007]! } - public var EditTheme_EditTitle: String { return self._s[4008]! } - public var ChatListFolderSettings_RecommendedFoldersSection: String { return self._s[4009]! } - public var TwoStepAuth_SetPassword: String { return self._s[4010]! } - public func Login_InvalidPhoneEmailSubject(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[4011]!, self._r[4011]!, [_0]) - } - public var BlockedUsers_Info: String { return self._s[4012]! } - public var AuthSessions_Sessions: String { return self._s[4013]! } - public var Group_EditAdmin_RankTitle: String { return self._s[4014]! } - public var Common_ActionNotAllowedError: String { return self._s[4015]! } - public var WebPreview_GettingLinkInfo: String { return self._s[4016]! } - public var Appearance_AppIconFilledX: String { return self._s[4017]! } - public var Passport_Email_EmailPlaceholder: String { return self._s[4018]! } - public var FeaturedStickers_OtherSection: String { return self._s[4019]! } - public var VoiceChat_RecordingStarted: String { return self._s[4020]! } - public var EditTheme_Edit_Preview_OutgoingText: String { return self._s[4021]! } - public var Profile_Username: String { return self._s[4022]! } - public var Appearance_RemoveTheme: String { return self._s[4023]! } - public var TwoStepAuth_SetupPasswordConfirmPassword: String { return self._s[4024]! } - public var Message_PinnedStickerMessage: String { return self._s[4025]! } - public var AccessDenied_VideoMicrophone: String { return self._s[4026]! } - public var WallpaperPreview_CustomColorBottomText: String { return self._s[4027]! } - public var Passport_Address_RegionPlaceholder: String { return self._s[4028]! } - public var Conversation_VoiceChat: String { return self._s[4029]! } - public var SettingsSearch_Synonyms_Data_Storage_Title: String { return self._s[4030]! } - public var TwoStepAuth_Title: String { return self._s[4031]! } - public var VoiceOver_Chat_YourAnimatedSticker: String { return self._s[4032]! } - public var Checkout_WebConfirmation_Title: String { return self._s[4033]! } - public var AutoDownloadSettings_VoiceMessagesInfo: String { return self._s[4034]! } - public var ChatListFolder_CategoryGroups: String { return self._s[4036]! } - public var Stats_GroupTopInviter_Promote: String { return self._s[4037]! } - public var Conversation_EditingPhotoPanelTitle: String { return self._s[4038]! } - public var Month_GenJuly: String { return self._s[4039]! } - public var Passport_Identity_Gender: String { return self._s[4040]! } - public var Channel_DiscussionGroup_UnlinkGroup: String { return self._s[4041]! } - public var Notification_Exceptions_DeleteAll: String { return self._s[4042]! } - public var VoiceChat_StopRecording: String { return self._s[4043]! } - public func Conversation_FileHowToText(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[4044]!, self._r[4044]!, [_0]) - } - public func Channel_AdminLog_MessageAdmin(_ _0: String, _ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[4045]!, self._r[4045]!, [_0, _1, _2]) - } - public var Login_CodeSentSms: String { return self._s[4046]! } - public func VoiceOver_Chat_ReplyFrom(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[4047]!, self._r[4047]!, [_0]) - } - public var Login_CallRequestState2: String { return self._s[4048]! } - public var Channel_DiscussionGroup_Header: String { return self._s[4049]! } - public func Channel_AdminLog_MessageToggleInvitesOff(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[4050]!, self._r[4050]!, [_0]) - } - public var Passport_Language_ms: String { return self._s[4051]! } - public var PeopleNearby_MakeInvisible: String { return self._s[4053]! } - public var ChatList_Search_FilterVoice: String { return self._s[4055]! } - public var Camera_TapAndHoldForVideo: String { return self._s[4057]! } - public var Permissions_NotificationsAllowInSettings_v0: String { return self._s[4058]! } - public func Notification_LeftChannel(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[4059]!, self._r[4059]!, [_0]) - } - public func Call_VoiceChatInProgressMessageCall(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[4060]!, self._r[4060]!, [_1, _2]) - } - public var Map_Locating: String { return self._s[4061]! } - public func Checkout_SavePasswordTimeout(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[4063]!, self._r[4063]!, [_0]) - } - public var Passport_Identity_TypeInternalPassport: String { return self._s[4065]! } - public var Appearance_ThemePreview_Chat_4_Text: String { return self._s[4066]! } - public var SettingsSearch_Synonyms_EditProfile_Username: String { return self._s[4067]! } - public var Stickers_Installed: String { return self._s[4068]! } - public var Notifications_PermissionsAllowInSettings: String { return self._s[4069]! } - public var StickerPackActionInfo_RemovedTitle: String { return self._s[4070]! } - public var CallSettings_Never: String { return self._s[4072]! } - public var Channel_Setup_TypePublicHelp: String { return self._s[4073]! } - public func ChatList_DeleteForEveryone(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[4075]!, self._r[4075]!, [_0]) - } - public var Message_Game: String { return self._s[4076]! } - public var Call_Message: String { return self._s[4077]! } - public func PUSH_CHANNEL_MESSAGE_VIDEO(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[4078]!, self._r[4078]!, [_1]) - } - public var ChannelIntro_Text: String { return self._s[4079]! } - public var StickerPack_Send: String { return self._s[4080]! } - public var Share_AuthDescription: String { return self._s[4081]! } - public var PasscodeSettings_AutoLock_IfAwayFor_5minutes: String { return self._s[4082]! } - public var CallFeedback_WhatWentWrong: String { return self._s[4083]! } - public var Common_Create: String { return self._s[4086]! } - public var Passport_Language_hy: String { return self._s[4087]! } - public var CreatePoll_Explanation: String { return self._s[4088]! } - public var GroupPermission_AddMembersNotAvailable: String { return self._s[4089]! } - public var ChatImport_CreateGroupAlertImportAction: String { return self._s[4090]! } - public var PeerInfo_ButtonVoiceChat: String { return self._s[4091]! } - public var Undo_ChatClearedForBothSides: String { return self._s[4092]! } - public var DialogList_NoMessagesTitle: String { return self._s[4093]! } - public var GroupInfo_Title: String { return self._s[4095]! } - public var Channel_AdminLog_CanBanUsers: String { return self._s[4096]! } - public var PhoneNumberHelp_Help: String { return self._s[4097]! } - public var TwoStepAuth_AdditionalPassword: String { return self._s[4098]! } - public var Settings_Logout: String { return self._s[4099]! } - public var Privacy_PaymentsTitle: String { return self._s[4100]! } - public var StickerPacksSettings_StickerPacksSection: String { return self._s[4101]! } - public var Tour_Text6: String { return self._s[4102]! } - public var ChatImportActivity_Title: String { return self._s[4104]! } - public var Channel_Username_Help: String { return self._s[4105]! } - public var VoiceOver_Chat_RecordModeVoiceMessageInfo: String { return self._s[4106]! } - public var AttachmentMenu_Poll: String { return self._s[4107]! } - public var EditTheme_Create_Preview_IncomingReplyName: String { return self._s[4108]! } - public var Conversation_ReportSpamChannelConfirmation: String { return self._s[4109]! } - public var Passport_DeletePassport: String { return self._s[4110]! } - public var Login_Code: String { return self._s[4111]! } - public var Notification_SecretChatScreenshot: String { return self._s[4112]! } - public var Login_CodeFloodError: String { return self._s[4113]! } - public func Notification_PinnedAnimationMessage(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[4114]!, self._r[4114]!, [_0]) - } - public func Channel_Username_UsernameIsAvailable(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[4115]!, self._r[4115]!, [_0]) - } - public var Watch_Stickers_Recents: String { return self._s[4116]! } - public var Generic_ErrorMoreInfo: String { return self._s[4117]! } - public func Call_AccountIsLoggedOnCurrentDevice(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[4118]!, self._r[4118]!, [_0]) - } - public var AutoDownloadSettings_DataUsage: String { return self._s[4119]! } - public var Conversation_ViewTheme: String { return self._s[4120]! } - public var Contacts_InviteSearchLabel: String { return self._s[4121]! } - public var Settings_CancelUpload: String { return self._s[4123]! } - public var Settings_AppLanguage_Unofficial: String { return self._s[4124]! } - public func ChatList_ClearChatConfirmation(_ _0: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(self._s[4125]!, self._r[4125]!, [_0]) } - public var ChatList_AddFolder: String { return self._s[4126]! } - public var Conversation_Location: String { return self._s[4128]! } - public var Appearance_BubbleCorners_AdjustAdjacent: String { return self._s[4129]! } - public var DialogList_AdLabel: String { return self._s[4130]! } - public func Time_TomorrowAt(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[4132]!, self._r[4132]!, [_0]) + public var Notifications_Badge_IncludeChannels: String { return self._s[4126]! } + public var InviteLink_CreatePrivateLinkHelpChannel: String { return self._s[4127]! } + public var StickerPack_ViewPack: String { return self._s[4130]! } + public var FastTwoStepSetup_PasswordConfirmationPlaceholder: String { return self._s[4132]! } + public var EditTheme_Expand_Preview_IncomingText: String { return self._s[4133]! } + public var Notifications_Title: String { return self._s[4134]! } + public var Conversation_InputTextPlaceholderComment: String { return self._s[4135]! } + public var GroupInfo_PublicLink: String { return self._s[4136]! } + public func ScheduleVoiceChat_GroupText(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[4137]!, self._r[4137]!, [_0]) } - public var Message_InvoiceLabel: String { return self._s[4133]! } - public var Channel_TooMuchBots: String { return self._s[4134]! } - public func Channel_AdminLog_MessageRemovedChannelUsername(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[4136]!, self._r[4136]!, [_0]) + public var VoiceOver_DiscardPreparedContent: String { return self._s[4138]! } + public var Conversation_Moderate_Ban: String { return self._s[4142]! } + public var InviteLink_Manage: String { return self._s[4143]! } + public var InstantPage_FontNewYork: String { return self._s[4144]! } + public func Activity_RemindAboutGroup(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[4145]!, self._r[4145]!, [_0]) } - public var Call_IncomingVideoCall: String { return self._s[4137]! } - public var Conversation_LiveLocation: String { return self._s[4138]! } - public var VoiceChat_AskedToSpeakHelp: String { return self._s[4139]! } - public var TwoStepAuth_SetupPasswordEnterPasswordChange: String { return self._s[4140]! } - public var Passport_Identity_EditPassport: String { return self._s[4141]! } - public var Permissions_CellularDataTitle_v0: String { return self._s[4143]! } - public var ChatList_Search_NoResultsFitlerVoice: String { return self._s[4144]! } - public var GroupInfo_Permissions_AddException: String { return self._s[4145]! } - public func VoiceChat_RemovePeerConfirmationChannel(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[4147]!, self._r[4147]!, [_0]) + public var TextFormat_Underline: String { return self._s[4146]! } + public func DownloadingStatus(_ _0: String, _ _1: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[4147]!, self._r[4147]!, [_0, _1]) } - public var Channel_AdminLog_CanInviteUsers: String { return self._s[4148]! } - public var Channel_MessageVideoUpdated: String { return self._s[4149]! } - public var GroupInfo_Permissions_EditingDisabled: String { return self._s[4150]! } - public var AutoremoveSetup_TimeSectionHeader: String { return self._s[4153]! } - public var AccessDenied_Camera: String { return self._s[4154]! } - public func Target_InviteToGroupConfirmation(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[4155]!, self._r[4155]!, [_0]) + public func PUSH_PINNED_ROUND(_ _1: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[4148]!, self._r[4148]!, [_1]) } - public var Theme_Context_ChangeColors: String { return self._s[4156]! } - public var PrivacySettings_TwoStepAuth: String { return self._s[4157]! } - public var Privacy_Forwards_PreviewMessageText: String { return self._s[4158]! } - public var Login_CodeExpiredError: String { return self._s[4159]! } - public var State_ConnectingToProxy: String { return self._s[4160]! } - public var TextFormat_Link: String { return self._s[4161]! } - public var Passport_Language_lv: String { return self._s[4163]! } - public var Conversation_AutoremoveTimerRemovedGroup: String { return self._s[4164]! } - public var AccessDenied_VoiceMicrophone: String { return self._s[4165]! } - public var WallpaperPreview_SwipeBottomText: String { return self._s[4166]! } - public var ProfilePhoto_SetMainVideo: String { return self._s[4167]! } - public var AutoDownloadSettings_Cellular: String { return self._s[4169]! } - public var ChatSettings_AutoDownloadVoiceMessages: String { return self._s[4170]! } - public func Channel_AdminLog_MessageKickedNameUsername(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[4171]!, self._r[4171]!, [_1, _2]) + public var PollResults_Collapse: String { return self._s[4150]! } + public var Contacts_GlobalSearch: String { return self._s[4151]! } + public func Conversation_EncryptionWaiting(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[4153]!, self._r[4153]!, [_0]) } - public var ChatList_EmptyChatListFilterTitle: String { return self._s[4172]! } - public var Checkout_PayNone: String { return self._s[4173]! } - public var NotificationsSound_Complete: String { return self._s[4175]! } - public var TwoStepAuth_ConfirmEmailCodePlaceholder: String { return self._s[4176]! } - public var InviteLink_CreateInfo: String { return self._s[4177]! } - public var AuthSessions_DevicesTitle: String { return self._s[4178]! } - public func DialogList_MultipleTyping(_ _0: String, _ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[4179]!, self._r[4179]!, [_0, _1]) + public var Channel_Management_LabelEditor: String { return self._s[4154]! } + public var SettingsSearch_Synonyms_Stickers_FeaturedPacks: String { return self._s[4156]! } + public var Conversation_Theme: String { return self._s[4157]! } + public func PUSH_CHANNEL_MESSAGE_DOCS(_ _1: String, _ _2: Int) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[4158]!, self._r[4158]!, [_1, "\(_2)"]) } - public var Message_LiveLocation: String { return self._s[4180]! } - public var Watch_Suggestion_BRB: String { return self._s[4181]! } - public var Channel_BanUser_Title: String { return self._s[4182]! } - public var SettingsSearch_Synonyms_Privacy_Data_Title: String { return self._s[4183]! } - public var Conversation_Dice_u1F3C0: String { return self._s[4184]! } - public var Conversation_ClearSelfHistory: String { return self._s[4185]! } - public var ProfilePhoto_OpenGallery: String { return self._s[4186]! } - public var PrivacySettings_LastSeenTitle: String { return self._s[4187]! } - public var Weekday_Thursday: String { return self._s[4188]! } - public var BroadcastListInfo_AddRecipient: String { return self._s[4189]! } - public var Privacy_ProfilePhoto: String { return self._s[4191]! } - public var StickerPacksSettings_ArchivedPacks_Info: String { return self._s[4192]! } - public func Channel_AdminLog_MessageChangedUnlinkedGroup(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[4193]!, self._r[4193]!, [_1, _2]) + public var Conversation_LinkDialogSave: String { return self._s[4159]! } + public var EnterPasscode_TouchId: String { return self._s[4160]! } + public var Conversation_VoiceChatMediaRecordingRestricted: String { return self._s[4161]! } + public var Group_ErrorAdminsTooMuch: String { return self._s[4162]! } + public var Stats_MessageOverview: String { return self._s[4163]! } + public var Privacy_Calls_P2PAlways: String { return self._s[4165]! } + public var Message_Sticker: String { return self._s[4166]! } + public var TwoFactorSetup_PasswordRecovery_SkipAlertTitle: String { return self._s[4167]! } + public var Conversation_Mute: String { return self._s[4170]! } + public var VoiceChat_AnonymousDisabledAlertText: String { return self._s[4171]! } + public var ContactInfo_Title: String { return self._s[4172]! } + public func PUSH_CHANNEL_MESSAGE_CONTACT(_ _1: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[4173]!, self._r[4173]!, [_1]) } - public var Message_Audio: String { return self._s[4194]! } - public var Conversation_Info: String { return self._s[4195]! } - public var Cache_Videos: String { return self._s[4196]! } - public var Appearance_ThemePreview_ChatList_6_Text: String { return self._s[4197]! } - public var Channel_ErrorAddTooMuch: String { return self._s[4198]! } - public func ChatList_DeleteSecretChatConfirmation(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[4199]!, self._r[4199]!, [_0]) + public var Channel_Setup_TypeHeader: String { return self._s[4174]! } + public var AuthSessions_LogOut: String { return self._s[4175]! } + public var ChatSettings_AutoDownloadReset: String { return self._s[4176]! } + public var VoiceChat_PinVideo: String { return self._s[4177]! } + public var Group_Info_Members: String { return self._s[4179]! } + public var ChatListFolderSettings_NewFolder: String { return self._s[4180]! } + public var Appearance_ThemePreview_ChatList_3_AuthorName: String { return self._s[4181]! } + public var CreatePoll_Title: String { return self._s[4182]! } + public var EditTheme_EditTitle: String { return self._s[4183]! } + public var ChatListFolderSettings_RecommendedFoldersSection: String { return self._s[4184]! } + public var TwoStepAuth_SetPassword: String { return self._s[4185]! } + public func Login_InvalidPhoneEmailSubject(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[4186]!, self._r[4186]!, [_0]) } - public var ChannelMembers_ChannelAdminsTitle: String { return self._s[4201]! } - public var ScheduledMessages_Title: String { return self._s[4203]! } - public var ShareFileTip_Title: String { return self._s[4206]! } - public var Chat_Gifs_TrendingSectionHeader: String { return self._s[4207]! } - public var ChatList_RemoveFolderConfirmation: String { return self._s[4208]! } - public func PUSH_CHAT_MESSAGE_GEOLIVE(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[4209]!, self._r[4209]!, [_1, _2]) + public var BlockedUsers_Info: String { return self._s[4187]! } + public var AuthSessions_Sessions: String { return self._s[4188]! } + public var Group_EditAdmin_RankTitle: String { return self._s[4189]! } + public var Common_ActionNotAllowedError: String { return self._s[4190]! } + public var WebPreview_GettingLinkInfo: String { return self._s[4191]! } + public var Appearance_AppIconFilledX: String { return self._s[4192]! } + public var Passport_Email_EmailPlaceholder: String { return self._s[4193]! } + public var FeaturedStickers_OtherSection: String { return self._s[4194]! } + public var VoiceChat_RecordingStarted: String { return self._s[4195]! } + public var EditTheme_Edit_Preview_OutgoingText: String { return self._s[4196]! } + public var Profile_Username: String { return self._s[4197]! } + public var TwoFactorRemember_Done_Title: String { return self._s[4198]! } + public var Settings_TipsUsername: String { return self._s[4199]! } + public var Appearance_RemoveTheme: String { return self._s[4200]! } + public var TwoStepAuth_SetupPasswordConfirmPassword: String { return self._s[4201]! } + public var Message_PinnedStickerMessage: String { return self._s[4202]! } + public var AccessDenied_VideoMicrophone: String { return self._s[4203]! } + public var WallpaperPreview_CustomColorBottomText: String { return self._s[4204]! } + public var Passport_Address_RegionPlaceholder: String { return self._s[4205]! } + public var Conversation_VoiceChat: String { return self._s[4206]! } + public var VoiceChat_EditBioSuccess: String { return self._s[4207]! } + public var ImportStickerPack_LinkAvailable: String { return self._s[4208]! } + public var SettingsSearch_Synonyms_Data_Storage_Title: String { return self._s[4209]! } + public var TwoStepAuth_Title: String { return self._s[4210]! } + public var VoiceOver_Chat_YourAnimatedSticker: String { return self._s[4211]! } + public var Checkout_WebConfirmation_Title: String { return self._s[4212]! } + public var AutoDownloadSettings_VoiceMessagesInfo: String { return self._s[4213]! } + public var ChatListFolder_CategoryGroups: String { return self._s[4215]! } + public var Stats_GroupTopInviter_Promote: String { return self._s[4216]! } + public var Conversation_EditingPhotoPanelTitle: String { return self._s[4217]! } + public var Month_GenJuly: String { return self._s[4218]! } + public var Passport_Identity_Gender: String { return self._s[4219]! } + public var Channel_DiscussionGroup_UnlinkGroup: String { return self._s[4220]! } + public var Notification_Exceptions_DeleteAll: String { return self._s[4221]! } + public var VoiceChat_StopRecording: String { return self._s[4222]! } + public func Conversation_FileHowToText(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[4223]!, self._r[4223]!, [_0]) } - public var Conversation_ContextViewStats: String { return self._s[4211]! } - public var Channel_DiscussionGroup_SearchPlaceholder: String { return self._s[4212]! } - public var PasscodeSettings_Title: String { return self._s[4213]! } - public var Channel_AdminLog_SendPolls: String { return self._s[4214]! } - public var LastSeen_ALongTimeAgo: String { return self._s[4215]! } - public func PUSH_CHANNEL_MESSAGE_GIF(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[4216]!, self._r[4216]!, [_1]) + public func Channel_AdminLog_MessageAdmin(_ _0: String, _ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[4224]!, self._r[4224]!, [_0, _1, _2]) } - public var SettingsSearch_Synonyms_Notifications_BadgeIncludeMutedChannels: String { return self._s[4217]! } - public var ChannelInfo_FakeChannelWarning: String { return self._s[4218]! } - public var CallFeedback_VideoReasonLowQuality: String { return self._s[4219]! } - public var Conversation_PinnedPreviousMessage: String { return self._s[4220]! } - public var SocksProxySetup_AddProxyTitle: String { return self._s[4221]! } - public var Passport_Identity_AddInternalPassport: String { return self._s[4222]! } - public func ChatList_RemovedFromFolderTooltip(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[4223]!, self._r[4223]!, [_1, _2]) + public var Login_CodeSentSms: String { return self._s[4225]! } + public func VoiceOver_Chat_ReplyFrom(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[4226]!, self._r[4226]!, [_0]) } - public func Conversation_SetReminder_RemindToday(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[4224]!, self._r[4224]!, [_0]) + public var Login_CallRequestState2: String { return self._s[4227]! } + public var Channel_DiscussionGroup_Header: String { return self._s[4228]! } + public func Channel_AdminLog_MessageToggleInvitesOff(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[4229]!, self._r[4229]!, [_0]) } - public var Passport_Identity_GenderFemale: String { return self._s[4225]! } - public var Location_ProximityNotification_DistanceKM: String { return self._s[4228]! } - public var ConvertToSupergroup_HelpTitle: String { return self._s[4229]! } - public func Message_ImportedDateFormat(_ _1: String, _ _2: String, _ _3: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[4230]!, self._r[4230]!, [_1, _2, _3]) - } - public var VoiceChat_Audio: String { return self._s[4231]! } - public var SharedMedia_TitleAll: String { return self._s[4232]! } - public var Settings_Context_Logout: String { return self._s[4233]! } - public var GroupInfo_SetGroupPhotoDelete: String { return self._s[4236]! } - public var Settings_About_Title: String { return self._s[4237]! } - public var StickerSettings_ContextHide: String { return self._s[4238]! } - public func AutoDownloadSettings_UpTo(_ _0: String) -> (String, [(Int, NSRange)]) { + public var Passport_Language_ms: String { return self._s[4230]! } + public var PeopleNearby_MakeInvisible: String { return self._s[4232]! } + public var ImportStickerPack_CreateStickerSet: String { return self._s[4234]! } + public var ChatList_Search_FilterVoice: String { return self._s[4235]! } + public var Camera_TapAndHoldForVideo: String { return self._s[4237]! } + public var Permissions_NotificationsAllowInSettings_v0: String { return self._s[4238]! } + public func Notification_LeftChannel(_ _0: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(self._s[4239]!, self._r[4239]!, [_0]) } - public func Conversation_LiveLocationYouAndOther(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[4240]!, self._r[4240]!, [_0]) + public func Call_VoiceChatInProgressMessageCall(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[4240]!, self._r[4240]!, [_1, _2]) } - public var ChatImport_SelectionConfirmationAlertImportAction: String { return self._s[4242]! } - public var Common_Cancel: String { return self._s[4243]! } - public var CallFeedback_Title: String { return self._s[4245]! } - public func Notification_PinnedContactMessage(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[4246]!, self._r[4246]!, [_0]) + public var Map_Locating: String { return self._s[4241]! } + public func Checkout_SavePasswordTimeout(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[4243]!, self._r[4243]!, [_0]) } - public var Conversation_StickerAddedToFavorites: String { return self._s[4247]! } - public var Activity_UploadingVideoMessage: String { return self._s[4249]! } - public var MediaPicker_Send: String { return self._s[4250]! } - public var PasscodeSettings_AutoLock_IfAwayFor_1minute: String { return self._s[4251]! } - public var Conversation_LiveLocationYou: String { return self._s[4252]! } - public var Notifications_ExceptionsUnmuted: String { return self._s[4253]! } - public func Channel_AdminLog_MessageGroupPreHistoryHidden(_ _0: String) -> (String, [(Int, NSRange)]) { + public var Passport_Identity_TypeInternalPassport: String { return self._s[4245]! } + public var Appearance_ThemePreview_Chat_4_Text: String { return self._s[4246]! } + public var SettingsSearch_Synonyms_EditProfile_Username: String { return self._s[4247]! } + public var Stickers_Installed: String { return self._s[4248]! } + public var Notifications_PermissionsAllowInSettings: String { return self._s[4249]! } + public var StickerPackActionInfo_RemovedTitle: String { return self._s[4250]! } + public var CallSettings_Never: String { return self._s[4252]! } + public var Channel_Setup_TypePublicHelp: String { return self._s[4253]! } + public func ChatList_DeleteForEveryone(_ _0: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(self._s[4255]!, self._r[4255]!, [_0]) } + public var Message_Game: String { return self._s[4256]! } + public var Call_Message: String { return self._s[4257]! } + public func PUSH_CHANNEL_MESSAGE_VIDEO(_ _1: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[4258]!, self._r[4258]!, [_1]) + } + public var ChannelIntro_Text: String { return self._s[4259]! } + public var VoiceChat_NoiseSuppressionEnabled: String { return self._s[4260]! } + public var StickerPack_Send: String { return self._s[4261]! } + public var Share_AuthDescription: String { return self._s[4262]! } + public var PasscodeSettings_AutoLock_IfAwayFor_5minutes: String { return self._s[4263]! } + public var CallFeedback_WhatWentWrong: String { return self._s[4264]! } + public var Common_Create: String { return self._s[4267]! } + public var Passport_Language_hy: String { return self._s[4268]! } + public var CreatePoll_Explanation: String { return self._s[4269]! } + public var GroupPermission_AddMembersNotAvailable: String { return self._s[4270]! } + public var ChatImport_CreateGroupAlertImportAction: String { return self._s[4271]! } + public var PeerInfo_ButtonVoiceChat: String { return self._s[4272]! } + public var Undo_ChatClearedForBothSides: String { return self._s[4273]! } + public var DialogList_NoMessagesTitle: String { return self._s[4274]! } + public var GroupInfo_Title: String { return self._s[4276]! } + public func ScheduleVoiceChat_ScheduleToday(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[4277]!, self._r[4277]!, [_0]) + } + public var UserInfo_ContactForwardTooltip_SavedMessages_One: String { return self._s[4278]! } + public var Channel_AdminLog_CanBanUsers: String { return self._s[4279]! } + public var PhoneNumberHelp_Help: String { return self._s[4280]! } + public var TwoStepAuth_AdditionalPassword: String { return self._s[4281]! } + public var Settings_Logout: String { return self._s[4282]! } + public var Privacy_PaymentsTitle: String { return self._s[4283]! } + public var StickerPacksSettings_StickerPacksSection: String { return self._s[4284]! } + public var Tour_Text6: String { return self._s[4285]! } + public var ChatImportActivity_Title: String { return self._s[4287]! } + public var Channel_Username_Help: String { return self._s[4288]! } + public var VoiceOver_Chat_RecordModeVoiceMessageInfo: String { return self._s[4289]! } + public var AttachmentMenu_Poll: String { return self._s[4290]! } + public var EditTheme_Create_Preview_IncomingReplyName: String { return self._s[4291]! } + public var Conversation_ReportSpamChannelConfirmation: String { return self._s[4292]! } + public var Passport_DeletePassport: String { return self._s[4293]! } + public var Login_Code: String { return self._s[4294]! } + public var Notification_SecretChatScreenshot: String { return self._s[4295]! } + public var VoiceChat_AddBio: String { return self._s[4296]! } + public var Login_CodeFloodError: String { return self._s[4297]! } + public func Notification_PinnedAnimationMessage(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[4298]!, self._r[4298]!, [_0]) + } + public func Channel_Username_UsernameIsAvailable(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[4299]!, self._r[4299]!, [_0]) + } + public var Watch_Stickers_Recents: String { return self._s[4300]! } + public var Generic_ErrorMoreInfo: String { return self._s[4301]! } + public func Call_AccountIsLoggedOnCurrentDevice(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[4302]!, self._r[4302]!, [_0]) + } + public var AutoDownloadSettings_DataUsage: String { return self._s[4303]! } + public var Conversation_ViewTheme: String { return self._s[4304]! } + public var Contacts_InviteSearchLabel: String { return self._s[4305]! } + public var Settings_CancelUpload: String { return self._s[4307]! } + public var Settings_AppLanguage_Unofficial: String { return self._s[4308]! } + public func ChatList_ClearChatConfirmation(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[4309]!, self._r[4309]!, [_0]) + } + public var ChatList_AddFolder: String { return self._s[4310]! } + public var Conversation_Location: String { return self._s[4312]! } + public var Appearance_BubbleCorners_AdjustAdjacent: String { return self._s[4313]! } + public var DialogList_AdLabel: String { return self._s[4314]! } + public func Time_TomorrowAt(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[4316]!, self._r[4316]!, [_0]) + } + public var Message_InvoiceLabel: String { return self._s[4317]! } + public var Channel_TooMuchBots: String { return self._s[4318]! } + public func Channel_AdminLog_MessageRemovedChannelUsername(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[4320]!, self._r[4320]!, [_0]) + } + public var Call_IncomingVideoCall: String { return self._s[4321]! } + public var Conversation_LiveLocation: String { return self._s[4322]! } + public var VoiceChat_AskedToSpeakHelp: String { return self._s[4323]! } + public var TwoStepAuth_SetupPasswordEnterPasswordChange: String { return self._s[4324]! } + public var Passport_Identity_EditPassport: String { return self._s[4325]! } + public var Permissions_CellularDataTitle_v0: String { return self._s[4327]! } + public var ChatList_Search_NoResultsFitlerVoice: String { return self._s[4328]! } + public var GroupInfo_Permissions_AddException: String { return self._s[4329]! } + public func VoiceChat_RemovePeerConfirmationChannel(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[4331]!, self._r[4331]!, [_0]) + } + public var Channel_AdminLog_CanInviteUsers: String { return self._s[4332]! } + public var Channel_MessageVideoUpdated: String { return self._s[4333]! } + public var GroupInfo_Permissions_EditingDisabled: String { return self._s[4334]! } + public var AutoremoveSetup_TimeSectionHeader: String { return self._s[4337]! } + public var AccessDenied_Camera: String { return self._s[4338]! } + public func Target_InviteToGroupConfirmation(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[4339]!, self._r[4339]!, [_0]) + } + public var Theme_Context_ChangeColors: String { return self._s[4340]! } + public var PrivacySettings_TwoStepAuth: String { return self._s[4341]! } + public var Privacy_Forwards_PreviewMessageText: String { return self._s[4342]! } + public var Login_CodeExpiredError: String { return self._s[4343]! } + public var State_ConnectingToProxy: String { return self._s[4344]! } + public var TextFormat_Link: String { return self._s[4345]! } + public var Passport_Language_lv: String { return self._s[4347]! } + public var Conversation_AutoremoveTimerRemovedGroup: String { return self._s[4348]! } + public var AccessDenied_VoiceMicrophone: String { return self._s[4349]! } + public var WallpaperPreview_SwipeBottomText: String { return self._s[4350]! } + public var ProfilePhoto_SetMainVideo: String { return self._s[4351]! } + public var AutoDownloadSettings_Cellular: String { return self._s[4353]! } + public var ChatSettings_AutoDownloadVoiceMessages: String { return self._s[4354]! } + public var Calls_NoVoiceAndVideoCallsPlaceholder: String { return self._s[4355]! } + public func Channel_AdminLog_MessageKickedNameUsername(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[4356]!, self._r[4356]!, [_1, _2]) + } + public var ChatList_EmptyChatListFilterTitle: String { return self._s[4357]! } + public var Checkout_PayNone: String { return self._s[4358]! } + public var NotificationsSound_Complete: String { return self._s[4360]! } + public var TwoStepAuth_ConfirmEmailCodePlaceholder: String { return self._s[4361]! } + public var InviteLink_CreateInfo: String { return self._s[4362]! } + public var AuthSessions_DevicesTitle: String { return self._s[4363]! } + public func DialogList_MultipleTyping(_ _0: String, _ _1: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[4364]!, self._r[4364]!, [_0, _1]) + } + public var Message_LiveLocation: String { return self._s[4365]! } + public var Watch_Suggestion_BRB: String { return self._s[4366]! } + public var Channel_BanUser_Title: String { return self._s[4367]! } + public var SettingsSearch_Synonyms_Privacy_Data_Title: String { return self._s[4368]! } + public var Conversation_Dice_u1F3C0: String { return self._s[4369]! } + public var Conversation_ClearSelfHistory: String { return self._s[4370]! } + public var ProfilePhoto_OpenGallery: String { return self._s[4371]! } + public var PrivacySettings_LastSeenTitle: String { return self._s[4372]! } + public var Weekday_Thursday: String { return self._s[4373]! } + public var BroadcastListInfo_AddRecipient: String { return self._s[4374]! } + public var Privacy_ProfilePhoto: String { return self._s[4376]! } + public var StickerPacksSettings_ArchivedPacks_Info: String { return self._s[4377]! } + public func Channel_AdminLog_MessageChangedUnlinkedGroup(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[4378]!, self._r[4378]!, [_1, _2]) + } + public var Message_Audio: String { return self._s[4379]! } + public var Conversation_Info: String { return self._s[4380]! } + public var Cache_Videos: String { return self._s[4381]! } + public var Appearance_ThemePreview_ChatList_6_Text: String { return self._s[4382]! } + public var Channel_ErrorAddTooMuch: String { return self._s[4383]! } + public var TwoFactorSetup_ResetDone_Text: String { return self._s[4384]! } + public func ChatList_DeleteSecretChatConfirmation(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[4385]!, self._r[4385]!, [_0]) + } + public var VoiceChat_EditBio: String { return self._s[4386]! } + public var ChannelMembers_ChannelAdminsTitle: String { return self._s[4388]! } + public var VoiceChat_ShareScreen: String { return self._s[4391]! } + public var ScheduledMessages_Title: String { return self._s[4392]! } + public var ShareFileTip_Title: String { return self._s[4395]! } + public var Chat_Gifs_TrendingSectionHeader: String { return self._s[4396]! } + public var ChatList_RemoveFolderConfirmation: String { return self._s[4397]! } + public func PUSH_CHAT_MESSAGE_GEOLIVE(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[4398]!, self._r[4398]!, [_1, _2]) + } + public var Conversation_ContextViewStats: String { return self._s[4400]! } + public var Channel_DiscussionGroup_SearchPlaceholder: String { return self._s[4401]! } + public var PasscodeSettings_Title: String { return self._s[4402]! } + public var Channel_AdminLog_SendPolls: String { return self._s[4403]! } + public var LastSeen_ALongTimeAgo: String { return self._s[4404]! } + public func PUSH_CHANNEL_MESSAGE_GIF(_ _1: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[4405]!, self._r[4405]!, [_1]) + } + public var SettingsSearch_Synonyms_Notifications_BadgeIncludeMutedChannels: String { return self._s[4406]! } + public var ChannelInfo_FakeChannelWarning: String { return self._s[4407]! } + public var CallFeedback_VideoReasonLowQuality: String { return self._s[4408]! } + public var Conversation_PinnedPreviousMessage: String { return self._s[4409]! } + public var SocksProxySetup_AddProxyTitle: String { return self._s[4410]! } + public var Passport_Identity_AddInternalPassport: String { return self._s[4411]! } + public func ChatList_RemovedFromFolderTooltip(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[4412]!, self._r[4412]!, [_1, _2]) + } + public func Conversation_SetReminder_RemindToday(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[4413]!, self._r[4413]!, [_0]) + } + public var Passport_Identity_GenderFemale: String { return self._s[4414]! } + public var Location_ProximityNotification_DistanceKM: String { return self._s[4417]! } + public var ConvertToSupergroup_HelpTitle: String { return self._s[4418]! } + public func Message_ImportedDateFormat(_ _1: String, _ _2: String, _ _3: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[4419]!, self._r[4419]!, [_1, _2, _3]) + } + public var VoiceChat_Audio: String { return self._s[4420]! } + public var SharedMedia_TitleAll: String { return self._s[4421]! } + public var Settings_Context_Logout: String { return self._s[4422]! } + public var GroupInfo_SetGroupPhotoDelete: String { return self._s[4425]! } + public var Settings_About_Title: String { return self._s[4426]! } + public var StickerSettings_ContextHide: String { return self._s[4427]! } + public func AutoDownloadSettings_UpTo(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[4428]!, self._r[4428]!, [_0]) + } + public func Conversation_LiveLocationYouAndOther(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[4429]!, self._r[4429]!, [_0]) + } + public var ChatImport_SelectionConfirmationAlertImportAction: String { return self._s[4431]! } + public var Common_Cancel: String { return self._s[4432]! } + public var CallFeedback_Title: String { return self._s[4434]! } + public func Notification_PinnedContactMessage(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[4435]!, self._r[4435]!, [_0]) + } + public var Conversation_StickerAddedToFavorites: String { return self._s[4436]! } + public var Activity_UploadingVideoMessage: String { return self._s[4438]! } + public var MediaPicker_Send: String { return self._s[4439]! } + public var PasscodeSettings_AutoLock_IfAwayFor_1minute: String { return self._s[4440]! } + public var Conversation_LiveLocationYou: String { return self._s[4441]! } + public var Notifications_ExceptionsUnmuted: String { return self._s[4442]! } + public func Channel_AdminLog_MessageGroupPreHistoryHidden(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[4444]!, self._r[4444]!, [_0]) + } public func PUSH_CHAT_ADD_YOU(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[4256]!, self._r[4256]!, [_1, _2]) + return formatWithArgumentRanges(self._s[4445]!, self._r[4445]!, [_1, _2]) } - public var Conversation_ViewBackground: String { return self._s[4257]! } - public var ChatSettings_PrivateChats: String { return self._s[4260]! } - public var Conversation_ErrorInaccessibleMessage: String { return self._s[4261]! } - public var BroadcastGroups_LimitAlert_LearnMore: String { return self._s[4262]! } - public var Appearance_ThemeNight: String { return self._s[4263]! } - public var Common_Search: String { return self._s[4264]! } - public var TwoStepAuth_ReEnterPasswordTitle: String { return self._s[4265]! } - public var ChangePhoneNumberNumber_Help: String { return self._s[4267]! } - public var InviteLink_QRCode_Share: String { return self._s[4268]! } - public var Stickers_SuggestAdded: String { return self._s[4270]! } - public var Conversation_DiscardVoiceMessageDescription: String { return self._s[4273]! } - public var Widget_UpdatedTodayAt: String { return self._s[4274]! } - public var NetworkUsageSettings_Cellular: String { return self._s[4275]! } - public var CheckoutInfo_Title: String { return self._s[4276]! } - public var Conversation_ShareBotLocationConfirmationTitle: String { return self._s[4277]! } - public var Channel_BotDoesntSupportGroups: String { return self._s[4278]! } + public var Checkout_PaymentLiabilityAlert: String { return self._s[4446]! } + public var Conversation_ViewBackground: String { return self._s[4447]! } + public var ChatSettings_PrivateChats: String { return self._s[4450]! } + public var Conversation_ErrorInaccessibleMessage: String { return self._s[4451]! } + public var BroadcastGroups_LimitAlert_LearnMore: String { return self._s[4452]! } + public var Appearance_ThemeNight: String { return self._s[4453]! } + public var Common_Search: String { return self._s[4454]! } + public var TwoStepAuth_ReEnterPasswordTitle: String { return self._s[4455]! } + public var ChangePhoneNumberNumber_Help: String { return self._s[4457]! } + public var InviteLink_QRCode_Share: String { return self._s[4458]! } + public var Stickers_SuggestAdded: String { return self._s[4460]! } + public func VoiceChat_VideoParticipantsLimitExceeded(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[4462]!, self._r[4462]!, [_0]) + } + public var Conversation_DiscardVoiceMessageDescription: String { return self._s[4464]! } + public var Widget_UpdatedTodayAt: String { return self._s[4465]! } + public var NetworkUsageSettings_Cellular: String { return self._s[4466]! } + public var CheckoutInfo_Title: String { return self._s[4467]! } + public var Conversation_ShareBotLocationConfirmationTitle: String { return self._s[4468]! } + public var Channel_BotDoesntSupportGroups: String { return self._s[4469]! } public func DialogList_SingleRecordingAudioSuffix(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[4279]!, self._r[4279]!, [_0]) + return formatWithArgumentRanges(self._s[4470]!, self._r[4470]!, [_0]) } - public var MaskStickerSettings_Info: String { return self._s[4281]! } - public var GroupRemoved_DeleteUser: String { return self._s[4283]! } - public var Contacts_ShareTelegram: String { return self._s[4284]! } - public var Group_UpgradeNoticeText1: String { return self._s[4285]! } + public var MaskStickerSettings_Info: String { return self._s[4472]! } + public var GroupRemoved_DeleteUser: String { return self._s[4474]! } + public var Contacts_ShareTelegram: String { return self._s[4475]! } + public var Group_UpgradeNoticeText1: String { return self._s[4476]! } public func PUSH_PHONE_CALL_REQUEST(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[4286]!, self._r[4286]!, [_1]) + return formatWithArgumentRanges(self._s[4477]!, self._r[4477]!, [_1]) } - public var PrivacyLastSeenSettings_Title: String { return self._s[4287]! } - public var SettingsSearch_Synonyms_Support: String { return self._s[4291]! } - public var PhotoEditor_TintTool: String { return self._s[4292]! } - public var ChatImportActivity_OpenApp: String { return self._s[4294]! } - public var GroupPermission_NoSendPolls: String { return self._s[4295]! } - public var NotificationsSound_None: String { return self._s[4296]! } + public var PrivacyLastSeenSettings_Title: String { return self._s[4478]! } + public var SettingsSearch_Synonyms_Support: String { return self._s[4482]! } + public var PhotoEditor_TintTool: String { return self._s[4483]! } + public var ChatImportActivity_OpenApp: String { return self._s[4485]! } + public var GroupPermission_NoSendPolls: String { return self._s[4486]! } + public var NotificationsSound_None: String { return self._s[4487]! } public func LOCAL_CHANNEL_MESSAGE_FWDS(_ _1: String, _ _2: Int) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[4297]!, self._r[4297]!, [_1, "\(_2)"]) + return formatWithArgumentRanges(self._s[4488]!, self._r[4488]!, [_1, "\(_2)"]) } - public var CheckoutInfo_ShippingInfoCityPlaceholder: String { return self._s[4300]! } + public var CheckoutInfo_ShippingInfoCityPlaceholder: String { return self._s[4491]! } public func Conversation_AutoremoveTimerSetChannel(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[4302]!, self._r[4302]!, [_1]) + return formatWithArgumentRanges(self._s[4493]!, self._r[4493]!, [_1]) } - public var ExplicitContent_AlertChannel: String { return self._s[4303]! } - public var Conversation_ClousStorageInfo_Description1: String { return self._s[4304]! } - public var Contacts_SortedByPresence: String { return self._s[4305]! } - public var WallpaperSearch_ColorGray: String { return self._s[4306]! } - public var Channel_AdminLogFilter_EventsNewSubscribers: String { return self._s[4307]! } - public var Conversation_ReportSpam: String { return self._s[4308]! } - public var ChatList_Search_NoResultsFilter: String { return self._s[4311]! } - public var WallpaperSearch_ColorBlack: String { return self._s[4312]! } - public var ArchivedChats_IntroTitle3: String { return self._s[4313]! } - public var InviteLink_DeleteAllRevokedLinksAlert_Action: String { return self._s[4314]! } + public var ExplicitContent_AlertChannel: String { return self._s[4494]! } + public var Conversation_ClousStorageInfo_Description1: String { return self._s[4495]! } + public var Contacts_SortedByPresence: String { return self._s[4496]! } + public var WallpaperSearch_ColorGray: String { return self._s[4497]! } + public var Channel_AdminLogFilter_EventsNewSubscribers: String { return self._s[4498]! } + public var Conversation_ReportSpam: String { return self._s[4499]! } + public var ChatList_Search_NoResultsFilter: String { return self._s[4502]! } + public var WallpaperSearch_ColorBlack: String { return self._s[4503]! } + public var ArchivedChats_IntroTitle3: String { return self._s[4504]! } + public var InviteLink_DeleteAllRevokedLinksAlert_Action: String { return self._s[4505]! } public func VoiceChat_PeerJoinedText(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[4315]!, self._r[4315]!, [_0]) + return formatWithArgumentRanges(self._s[4506]!, self._r[4506]!, [_0]) } - public var Conversation_DefaultRestrictedText: String { return self._s[4316]! } - public var Settings_Devices: String { return self._s[4317]! } - public var Call_AudioRouteSpeaker: String { return self._s[4318]! } - public var GroupInfo_InviteLink_CopyLink: String { return self._s[4319]! } - public var Passport_Address_Country: String { return self._s[4321]! } - public var Cache_MaximumCacheSize: String { return self._s[4322]! } - public var Chat_PanelHidePinnedMessages: String { return self._s[4323]! } - public var Notifications_Badge_IncludePublicGroups: String { return self._s[4324]! } - public var ChatSettings_AutoDownloadUsingWiFi: String { return self._s[4326]! } - public var Login_TermsOfServiceLabel: String { return self._s[4327]! } - public var Calls_NoMissedCallsPlacehoder: String { return self._s[4328]! } - public var SocksProxySetup_RequiredCredentials: String { return self._s[4329]! } - public var VoiceOver_MessageContextOpenMessageMenu: String { return self._s[4330]! } - public var AutoNightTheme_ScheduledFrom: String { return self._s[4331]! } - public var ChatSettings_AutoDownloadDocuments: String { return self._s[4332]! } - public var ConvertToSupergroup_Note: String { return self._s[4334]! } - public var Settings_SetNewProfilePhotoOrVideo: String { return self._s[4335]! } - public var PrivacySettings_PasscodeAndTouchId: String { return self._s[4336]! } - public var Common_More: String { return self._s[4337]! } - public var ShareMenu_SelectChats: String { return self._s[4339]! } + public var Conversation_DefaultRestrictedText: String { return self._s[4507]! } + public var Settings_Devices: String { return self._s[4508]! } + public var Call_AudioRouteSpeaker: String { return self._s[4509]! } + public var GroupInfo_InviteLink_CopyLink: String { return self._s[4510]! } + public var VoiceChat_StartsIn: String { return self._s[4511]! } + public var VoiceChat_CreateNewVoiceChatSchedule: String { return self._s[4512]! } + public var VoiceChat_EditDescriptionTitle: String { return self._s[4514]! } + public var Passport_Address_Country: String { return self._s[4515]! } + public var Cache_MaximumCacheSize: String { return self._s[4516]! } + public var Chat_PanelHidePinnedMessages: String { return self._s[4517]! } + public var Notifications_Badge_IncludePublicGroups: String { return self._s[4518]! } + public var ChatSettings_AutoDownloadUsingWiFi: String { return self._s[4520]! } + public var Login_TermsOfServiceLabel: String { return self._s[4521]! } + public var Calls_NoMissedCallsPlacehoder: String { return self._s[4522]! } + public var SocksProxySetup_RequiredCredentials: String { return self._s[4523]! } + public var VoiceOver_MessageContextOpenMessageMenu: String { return self._s[4524]! } + public var AutoNightTheme_ScheduledFrom: String { return self._s[4525]! } + public var ChatSettings_AutoDownloadDocuments: String { return self._s[4526]! } + public var ConvertToSupergroup_Note: String { return self._s[4528]! } + public var Settings_SetNewProfilePhotoOrVideo: String { return self._s[4529]! } + public var PrivacySettings_PasscodeAndTouchId: String { return self._s[4530]! } + public var Common_More: String { return self._s[4531]! } + public var ShareMenu_SelectChats: String { return self._s[4533]! } public func Conversation_ScheduleMessage_SendToday(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[4340]!, self._r[4340]!, [_0]) + return formatWithArgumentRanges(self._s[4534]!, self._r[4534]!, [_0]) } public func Channel_AdminLog_MessageRemovedGroupStickerPack(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[4341]!, self._r[4341]!, [_0]) + return formatWithArgumentRanges(self._s[4535]!, self._r[4535]!, [_0]) } - public var Contacts_PermissionsKeepDisabled: String { return self._s[4343]! } + public var Contacts_PermissionsKeepDisabled: String { return self._s[4537]! } + public var VoiceChat_EditBioText: String { return self._s[4538]! } public func Call_ParticipantVersionOutdatedError(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[4344]!, self._r[4344]!, [_0]) + return formatWithArgumentRanges(self._s[4539]!, self._r[4539]!, [_0]) } - public var WatchRemote_AlertOpen: String { return self._s[4345]! } + public var WatchRemote_AlertOpen: String { return self._s[4540]! } public func PUSH_CHAT_ADD_MEMBER(_ _1: String, _ _2: String, _ _3: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[4346]!, self._r[4346]!, [_1, _2, _3]) + return formatWithArgumentRanges(self._s[4541]!, self._r[4541]!, [_1, _2, _3]) } - public var Channel_Members_AddMembersHelp: String { return self._s[4347]! } - public var Shortcut_SwitchAccount: String { return self._s[4348]! } - public var Map_LiveLocationFor8Hours: String { return self._s[4349]! } + public var Channel_Members_AddMembersHelp: String { return self._s[4542]! } + public var Shortcut_SwitchAccount: String { return self._s[4543]! } + public var Map_LiveLocationFor8Hours: String { return self._s[4544]! } public func AutoNightTheme_AutomaticHelp(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[4350]!, self._r[4350]!, [_0]) + return formatWithArgumentRanges(self._s[4545]!, self._r[4545]!, [_0]) } - public var Compose_NewGroupTitle: String { return self._s[4351]! } - public var DialogList_You: String { return self._s[4352]! } - public var Call_VoiceOver_VoiceCallOutgoing: String { return self._s[4353]! } - public var ReportPeer_ReasonViolence: String { return self._s[4354]! } + public var Compose_NewGroupTitle: String { return self._s[4546]! } + public var DialogList_You: String { return self._s[4547]! } + public var Call_VoiceOver_VoiceCallOutgoing: String { return self._s[4548]! } + public var ReportPeer_ReasonViolence: String { return self._s[4549]! } public func PUSH_CHANNEL_MESSAGE_STICKER(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[4355]!, self._r[4355]!, [_1, _2]) + return formatWithArgumentRanges(self._s[4550]!, self._r[4550]!, [_1, _2]) } - public var VoiceChat_Reconnecting: String { return self._s[4357]! } - public var KeyCommand_ScrollDown: String { return self._s[4360]! } - public var ChatSettings_DownloadInBackground: String { return self._s[4361]! } - public var Wallpaper_ResetWallpapers: String { return self._s[4362]! } - public var Channel_BanList_RestrictedTitle: String { return self._s[4363]! } - public var ArchivedChats_IntroText3: String { return self._s[4364]! } - public var HashtagSearch_AllChats: String { return self._s[4366]! } - public var VoiceChat_EndVoiceChat: String { return self._s[4367]! } - public var Conversation_MessageCopied: String { return self._s[4369]! } - public var Channel_Info_BlackList: String { return self._s[4370]! } - public var Contacts_SearchUsersAndGroupsLabel: String { return self._s[4371]! } - public var PrivacyPhoneNumberSettings_DiscoveryHeader: String { return self._s[4372]! } - public var Paint_Neon: String { return self._s[4374]! } - public var SettingsSearch_Synonyms_AppLanguage: String { return self._s[4375]! } - public var AutoDownloadSettings_AutoDownload: String { return self._s[4376]! } + public var VoiceChat_Reconnecting: String { return self._s[4552]! } + public var KeyCommand_ScrollDown: String { return self._s[4555]! } + public var ChatSettings_DownloadInBackground: String { return self._s[4556]! } + public var Wallpaper_ResetWallpapers: String { return self._s[4557]! } + public var Channel_BanList_RestrictedTitle: String { return self._s[4558]! } + public var ArchivedChats_IntroText3: String { return self._s[4559]! } + public var HashtagSearch_AllChats: String { return self._s[4561]! } + public var VoiceChat_EndVoiceChat: String { return self._s[4562]! } + public var Conversation_MessageCopied: String { return self._s[4564]! } + public var Channel_Info_BlackList: String { return self._s[4565]! } + public var Contacts_SearchUsersAndGroupsLabel: String { return self._s[4566]! } + public var PrivacyPhoneNumberSettings_DiscoveryHeader: String { return self._s[4567]! } + public var Paint_Neon: String { return self._s[4569]! } + public var SettingsSearch_Synonyms_AppLanguage: String { return self._s[4570]! } + public var AutoDownloadSettings_AutoDownload: String { return self._s[4571]! } + public var ImportStickerPack_CreateNewStickerSet: String { return self._s[4572]! } public func Notification_PinnedVideoMessage(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[4378]!, self._r[4378]!, [_0]) + return formatWithArgumentRanges(self._s[4574]!, self._r[4574]!, [_0]) } - public var Map_StopLiveLocation: String { return self._s[4379]! } - public var SettingsSearch_Synonyms_Data_SaveEditedPhotos: String { return self._s[4380]! } - public var Channel_Username_InvalidCharacters: String { return self._s[4381]! } - public var InstantPage_Reference: String { return self._s[4383]! } - public var Group_Members_AddMembers: String { return self._s[4385]! } - public var ChatList_HideAction: String { return self._s[4386]! } - public var Conversation_FileICloudDrive: String { return self._s[4388]! } + public var Map_StopLiveLocation: String { return self._s[4575]! } + public var SettingsSearch_Synonyms_Data_SaveEditedPhotos: String { return self._s[4576]! } + public var Channel_Username_InvalidCharacters: String { return self._s[4577]! } + public var InstantPage_Reference: String { return self._s[4579]! } + public var Group_Members_AddMembers: String { return self._s[4581]! } + public func Conversation_ScheduledVoiceChatStartsOn(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[4582]!, self._r[4582]!, [_0]) + } + public var ChatList_HideAction: String { return self._s[4583]! } + public var Conversation_FileICloudDrive: String { return self._s[4585]! } public func PUSH_PINNED_GEOLIVE(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[4389]!, self._r[4389]!, [_1]) + return formatWithArgumentRanges(self._s[4586]!, self._r[4586]!, [_1]) } - public var Passport_PasswordReset: String { return self._s[4391]! } - public var ChatList_Context_UnhideArchive: String { return self._s[4393]! } - public var ConvertToSupergroup_HelpText: String { return self._s[4394]! } - public var Calls_AddTab: String { return self._s[4395]! } - public var TwoStepAuth_ConfirmEmailResendCode: String { return self._s[4396]! } - public var SettingsSearch_Synonyms_Stickers_SuggestStickers: String { return self._s[4397]! } - public var Privacy_GroupsAndChannels: String { return self._s[4400]! } - public var Conversation_UsernameCopied: String { return self._s[4401]! } - public var AutoNightTheme_Disabled: String { return self._s[4402]! } - public var CreatePoll_MultipleChoice: String { return self._s[4403]! } + public var Passport_PasswordReset: String { return self._s[4588]! } + public var ChatList_Context_UnhideArchive: String { return self._s[4590]! } + public var ConvertToSupergroup_HelpText: String { return self._s[4591]! } + public var Calls_AddTab: String { return self._s[4592]! } + public var TwoStepAuth_ConfirmEmailResendCode: String { return self._s[4594]! } + public var SettingsSearch_Synonyms_Stickers_SuggestStickers: String { return self._s[4595]! } + public var Privacy_GroupsAndChannels: String { return self._s[4598]! } + public var Conversation_UsernameCopied: String { return self._s[4599]! } + public var AutoNightTheme_Disabled: String { return self._s[4600]! } + public var CreatePoll_MultipleChoice: String { return self._s[4601]! } public func PINNED_INVOICE(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[4404]!, self._r[4404]!, [_1]) + return formatWithArgumentRanges(self._s[4602]!, self._r[4602]!, [_1]) } - public var Watch_Bot_Restart: String { return self._s[4406]! } + public var Watch_Bot_Restart: String { return self._s[4604]! } public func Conversation_Kilobytes(_ _0: Int) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[4407]!, self._r[4407]!, ["\(_0)"]) + return formatWithArgumentRanges(self._s[4605]!, self._r[4605]!, ["\(_0)"]) } - public var GroupInfo_ScamGroupWarning: String { return self._s[4409]! } - public var Conversation_EditingMessagePanelMedia: String { return self._s[4410]! } - public var Appearance_PreviewIncomingText: String { return self._s[4411]! } - public var ChatSettings_WidgetSettings: String { return self._s[4412]! } - public var Notifications_ChannelNotificationsExceptionsHelp: String { return self._s[4413]! } - public var ChatList_UndoArchiveRevealedTitle: String { return self._s[4415]! } - public var Stats_GroupOverview: String { return self._s[4417]! } - public var ScheduledMessages_EditTime: String { return self._s[4420]! } - public var Month_GenFebruary: String { return self._s[4421]! } - public var ChatList_AutoarchiveSuggestion_OpenSettings: String { return self._s[4422]! } - public var Stickers_ClearRecent: String { return self._s[4423]! } - public var InviteLink_Create_UsersLimitNumberOfUsersUnlimited: String { return self._s[4424]! } - public var TwoStepAuth_EnterPasswordPassword: String { return self._s[4425]! } - public var Stats_Message_PublicShares: String { return self._s[4426]! } + public var GroupInfo_ScamGroupWarning: String { return self._s[4607]! } + public var Conversation_EditingMessagePanelMedia: String { return self._s[4608]! } + public var Appearance_PreviewIncomingText: String { return self._s[4609]! } + public var ChatSettings_WidgetSettings: String { return self._s[4610]! } + public var Notifications_ChannelNotificationsExceptionsHelp: String { return self._s[4611]! } + public var ChatList_UndoArchiveRevealedTitle: String { return self._s[4613]! } + public var Stats_GroupOverview: String { return self._s[4615]! } + public var ScheduledMessages_EditTime: String { return self._s[4618]! } + public var Month_GenFebruary: String { return self._s[4619]! } + public var ChatList_AutoarchiveSuggestion_OpenSettings: String { return self._s[4620]! } + public var Stickers_ClearRecent: String { return self._s[4621]! } + public var InviteLink_Create_UsersLimitNumberOfUsersUnlimited: String { return self._s[4622]! } + public var TwoStepAuth_EnterPasswordPassword: String { return self._s[4623]! } + public var Stats_Message_PublicShares: String { return self._s[4624]! } public func Checkout_PayPrice(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[4427]!, self._r[4427]!, [_0]) + return formatWithArgumentRanges(self._s[4625]!, self._r[4625]!, [_0]) } - public var Login_TermsOfServiceSignupDecline: String { return self._s[4428]! } - public var CheckoutInfo_ErrorCityInvalid: String { return self._s[4429]! } - public var VoiceOver_Chat_PlayHint: String { return self._s[4430]! } - public var ChatAdmins_AllMembersAreAdminsOffHelp: String { return self._s[4431]! } - public var CheckoutInfo_ShippingInfoTitle: String { return self._s[4433]! } - public var CreatePoll_Create: String { return self._s[4434]! } - public var ChatList_Search_FilterLinks: String { return self._s[4435]! } - public var Your_cards_number_is_invalid: String { return self._s[4436]! } - public var Month_ShortApril: String { return self._s[4437]! } - public var SocksProxySetup_UseForCalls: String { return self._s[4438]! } - public var Conversation_EditingCaptionPanelTitle: String { return self._s[4439]! } - public var SocksProxySetup_Status: String { return self._s[4440]! } - public var VoiceChat_UnmuteForMe: String { return self._s[4441]! } - public var ChannelInfo_DeleteGroupConfirmation: String { return self._s[4442]! } - public var ChatListFolder_CategoryBots: String { return self._s[4443]! } - public var Passport_FieldIdentitySelfieHelp: String { return self._s[4445]! } - public var GroupInfo_BroadcastListNamePlaceholder: String { return self._s[4446]! } - public var Chat_PinnedListPreview_UnpinAllMessages: String { return self._s[4447]! } - public var Wallpaper_ResetWallpapersInfo: String { return self._s[4448]! } - public var Conversation_TitleUnmute: String { return self._s[4449]! } - public var Group_Setup_TypeHeader: String { return self._s[4450]! } + public var Login_TermsOfServiceSignupDecline: String { return self._s[4626]! } + public var CheckoutInfo_ErrorCityInvalid: String { return self._s[4627]! } + public var VoiceOver_Chat_PlayHint: String { return self._s[4628]! } + public var ChatAdmins_AllMembersAreAdminsOffHelp: String { return self._s[4629]! } + public var CheckoutInfo_ShippingInfoTitle: String { return self._s[4631]! } + public var CreatePoll_Create: String { return self._s[4632]! } + public var ChatList_Search_FilterLinks: String { return self._s[4633]! } + public var Your_cards_number_is_invalid: String { return self._s[4634]! } + public var Month_ShortApril: String { return self._s[4635]! } + public var SocksProxySetup_UseForCalls: String { return self._s[4636]! } + public var Conversation_EditingCaptionPanelTitle: String { return self._s[4637]! } + public var SocksProxySetup_Status: String { return self._s[4638]! } + public var VoiceChat_UnmuteForMe: String { return self._s[4639]! } + public var ChannelInfo_DeleteGroupConfirmation: String { return self._s[4640]! } + public var ChatListFolder_CategoryBots: String { return self._s[4641]! } + public var Passport_FieldIdentitySelfieHelp: String { return self._s[4643]! } + public var GroupInfo_BroadcastListNamePlaceholder: String { return self._s[4644]! } + public var Chat_PinnedListPreview_UnpinAllMessages: String { return self._s[4645]! } + public var Wallpaper_ResetWallpapersInfo: String { return self._s[4646]! } + public var Conversation_TitleUnmute: String { return self._s[4647]! } + public var Group_Setup_TypeHeader: String { return self._s[4648]! } public func Conversation_ForwardTooltip_ManyChats_One(_ _0: String, _ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[4451]!, self._r[4451]!, [_0, _1]) + return formatWithArgumentRanges(self._s[4649]!, self._r[4649]!, [_0, _1]) } - public var Stats_ViewsPerPost: String { return self._s[4452]! } - public var CheckoutInfo_ShippingInfoCountry: String { return self._s[4453]! } - public var Passport_Identity_TranslationHelp: String { return self._s[4454]! } + public var Stats_ViewsPerPost: String { return self._s[4650]! } + public var CheckoutInfo_ShippingInfoCountry: String { return self._s[4651]! } + public var Passport_Identity_TranslationHelp: String { return self._s[4652]! } public func PUSH_CHANNEL_MESSAGE_FWD(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[4455]!, self._r[4455]!, [_1]) + return formatWithArgumentRanges(self._s[4653]!, self._r[4653]!, [_1]) } - public var GroupInfo_Administrators_Title: String { return self._s[4456]! } + public var GroupInfo_Administrators_Title: String { return self._s[4654]! } public func Channel_AdminLog_MessageRankName(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[4457]!, self._r[4457]!, [_1, _2]) + return formatWithArgumentRanges(self._s[4655]!, self._r[4655]!, [_1, _2]) } public func PUSH_CHAT_MESSAGE_POLL(_ _1: String, _ _2: String, _ _3: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[4458]!, self._r[4458]!, [_1, _2, _3]) + return formatWithArgumentRanges(self._s[4656]!, self._r[4656]!, [_1, _2, _3]) } - public var CheckoutInfo_ShippingInfoState: String { return self._s[4459]! } - public var Passport_Language_my: String { return self._s[4461]! } - public var PrivacyLastSeenSettings_AlwaysShareWith_Title: String { return self._s[4462]! } - public var Map_PlacesNearby: String { return self._s[4463]! } - public var Channel_About_Help: String { return self._s[4464]! } - public var LogoutOptions_AddAccountTitle: String { return self._s[4465]! } - public var ChatSettings_AutomaticAudioDownload: String { return self._s[4466]! } - public var Channel_Username_Title: String { return self._s[4467]! } - public var Activity_RecordingVideoMessage: String { return self._s[4468]! } + public var CheckoutInfo_ShippingInfoState: String { return self._s[4657]! } + public var Passport_Language_my: String { return self._s[4659]! } + public var PrivacyLastSeenSettings_AlwaysShareWith_Title: String { return self._s[4660]! } + public var VoiceChat_Unpin: String { return self._s[4661]! } + public var Map_PlacesNearby: String { return self._s[4662]! } + public var Channel_About_Help: String { return self._s[4663]! } + public var LogoutOptions_AddAccountTitle: String { return self._s[4664]! } + public var ChatSettings_AutomaticAudioDownload: String { return self._s[4665]! } + public var Channel_Username_Title: String { return self._s[4666]! } + public var Activity_RecordingVideoMessage: String { return self._s[4667]! } public func StickerPackActionInfo_RemovedText(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[4469]!, self._r[4469]!, [_0]) + return formatWithArgumentRanges(self._s[4668]!, self._r[4668]!, [_0]) } - public var CheckoutInfo_ShippingInfoCity: String { return self._s[4470]! } - public var Passport_DiscardMessageDescription: String { return self._s[4471]! } - public var Conversation_LinkDialogOpen: String { return self._s[4472]! } - public var ChatList_Context_HideArchive: String { return self._s[4473]! } + public var CheckoutInfo_ShippingInfoCity: String { return self._s[4669]! } + public var Passport_DiscardMessageDescription: String { return self._s[4670]! } + public var Conversation_LinkDialogOpen: String { return self._s[4671]! } + public var ChatList_Context_HideArchive: String { return self._s[4672]! } public func Message_AuthorPinnedGame(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[4474]!, self._r[4474]!, [_0]) + return formatWithArgumentRanges(self._s[4673]!, self._r[4673]!, [_0]) } - public var Privacy_GroupsAndChannels_CustomShareHelp: String { return self._s[4475]! } - public var Conversation_Admin: String { return self._s[4476]! } - public var DialogList_TabTitle: String { return self._s[4477]! } + public var Privacy_GroupsAndChannels_CustomShareHelp: String { return self._s[4674]! } + public var Conversation_Admin: String { return self._s[4675]! } + public var DialogList_TabTitle: String { return self._s[4676]! } public func PUSH_CHAT_ALBUM(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[4478]!, self._r[4478]!, [_1, _2]) + return formatWithArgumentRanges(self._s[4677]!, self._r[4677]!, [_1, _2]) } - public var Notifications_PermissionsUnreachableText: String { return self._s[4479]! } - public var Passport_Identity_GenderMale: String { return self._s[4481]! } + public var Notifications_PermissionsUnreachableText: String { return self._s[4678]! } + public var Passport_Identity_GenderMale: String { return self._s[4680]! } public func VoiceChat_EditTitleSuccess(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[4483]!, self._r[4483]!, [_0]) + return formatWithArgumentRanges(self._s[4682]!, self._r[4682]!, [_0]) } - public var SettingsSearch_Synonyms_Privacy_BlockedUsers: String { return self._s[4484]! } - public var PhoneNumberHelp_Alert: String { return self._s[4485]! } - public var EnterPasscode_EnterNewPasscodeChange: String { return self._s[4486]! } - public var Notifications_InAppNotifications: String { return self._s[4487]! } + public var SettingsSearch_Synonyms_Privacy_BlockedUsers: String { return self._s[4683]! } + public var PhoneNumberHelp_Alert: String { return self._s[4684]! } + public var EnterPasscode_EnterNewPasscodeChange: String { return self._s[4685]! } + public var Notifications_InAppNotifications: String { return self._s[4686]! } public func Update_AppVersion(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[4488]!, self._r[4488]!, [_0]) - } - public var Notification_VideoCallOutgoing: String { return self._s[4489]! } - public var Login_InvalidCodeError: String { return self._s[4490]! } - public var Conversation_PrivateChannelTimeLimitedAlertJoin: String { return self._s[4491]! } - public func LastSeen_TodayAt(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[4493]!, self._r[4493]!, [_0]) - } - public var Conversation_InputTextCaptionPlaceholder: String { return self._s[4494]! } - public var ReportPeer_Report: String { return self._s[4495]! } - public var Camera_FlashOff: String { return self._s[4498]! } - public var Conversation_InputTextBroadcastPlaceholder: String { return self._s[4501]! } - public var PrivacyPolicy_DeclineTitle: String { return self._s[4504]! } - public var SettingsSearch_Synonyms_Privacy_PasscodeAndTouchId: String { return self._s[4505]! } - public var Passport_FieldEmail: String { return self._s[4506]! } - public func Channel_AdminLog_MessageKickedName(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[4507]!, self._r[4507]!, [_1]) - } - public var Notifications_ExceptionsResetToDefaults: String { return self._s[4508]! } - public var PeerInfo_PaneVoiceAndVideo: String { return self._s[4509]! } - public var Group_OwnershipTransfer_Title: String { return self._s[4510]! } - public var Conversation_DefaultRestrictedInline: String { return self._s[4511]! } - public var Login_PhoneNumberHelp: String { return self._s[4513]! } - public var Channel_AdminLogFilter_EventsNewMembers: String { return self._s[4514]! } - public var Conversation_PinnedQuiz: String { return self._s[4515]! } - public var CreateGroup_SoftUserLimitAlert: String { return self._s[4516]! } - public var Login_PhoneNumberAlreadyAuthorizedSwitch: String { return self._s[4517]! } - public var Group_MessagePhotoUpdated: String { return self._s[4518]! } - public var LoginPassword_PasswordPlaceholder: String { return self._s[4519]! } - public var BroadcastGroups_ConfirmationAlert_Text: String { return self._s[4520]! } - public var Passport_Identity_Translations: String { return self._s[4522]! } - public var ChatAdmins_AllMembersAreAdmins: String { return self._s[4523]! } - public var ChannelInfo_DeleteChannel: String { return self._s[4525]! } - public var PasscodeSettings_HelpBottom: String { return self._s[4526]! } - public var Channel_Members_AddMembers: String { return self._s[4527]! } - public var AutoDownloadSettings_LastDelimeter: String { return self._s[4528]! } - public var Notification_Exceptions_DeleteAllConfirmation: String { return self._s[4530]! } - public var Conversation_HoldForAudio: String { return self._s[4531]! } - public var Media_LimitedAccessChangeSettings: String { return self._s[4533]! } - public var Watch_LastSeen_Lately: String { return self._s[4534]! } - public var ChatList_Context_MarkAsRead: String { return self._s[4535]! } - public var Conversation_PinnedMessage: String { return self._s[4536]! } - public var SettingsSearch_Synonyms_Appearance_ColorTheme: String { return self._s[4537]! } - public var VoiceChat_StopRecordingStop: String { return self._s[4539]! } - public var Passport_UpdateRequiredError: String { return self._s[4540]! } - public var PrivacySettings_Passcode: String { return self._s[4541]! } - public func Call_EmojiDescription(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[4542]!, self._r[4542]!, [_0]) - } - public var AutoNightTheme_NotAvailable: String { return self._s[4543]! } - public var Conversation_PressVolumeButtonForSound: String { return self._s[4544]! } - public var VoiceOver_Common_On: String { return self._s[4545]! } - public var LoginPassword_InvalidPasswordError: String { return self._s[4546]! } - public var ChatListFolder_IncludedSectionHeader: String { return self._s[4547]! } - public var Channel_SignMessages_Help: String { return self._s[4548]! } - public var ChatList_DeleteForEveryoneConfirmationTitle: String { return self._s[4549]! } - public var Conversation_TitleNoComments: String { return self._s[4550]! } - public var MediaPicker_LivePhotoDescription: String { return self._s[4551]! } - public var GroupInfo_Permissions: String { return self._s[4552]! } - public var GroupPermission_NoSendLinks: String { return self._s[4555]! } - public var Passport_Identity_ResidenceCountry: String { return self._s[4556]! } - public var Appearance_ThemeCarouselNightBlue: String { return self._s[4558]! } - public var ChatList_ArchiveAction: String { return self._s[4559]! } - public func Channel_AdminLog_DisabledSlowmode(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[4560]!, self._r[4560]!, [_0]) - } - public var GroupInfo_GroupHistory: String { return self._s[4561]! } - public func Channel_Management_ErrorNotMember(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[4563]!, self._r[4563]!, [_0]) - } - public var Privacy_Forwards_LinkIfAllowed: String { return self._s[4565]! } - public var Channel_Info_Banned: String { return self._s[4566]! } - public var Paint_RecentStickers: String { return self._s[4567]! } - public var VoiceOver_MessageContextSend: String { return self._s[4568]! } - public var Group_ErrorNotMutualContact: String { return self._s[4569]! } - public var ReportPeer_ReasonOther: String { return self._s[4571]! } - public var Channel_BanUser_PermissionChangeGroupInfo: String { return self._s[4572]! } - public var SocksProxySetup_ShareQRCodeInfo: String { return self._s[4574]! } - public var KeyCommand_Find: String { return self._s[4575]! } - public func Channel_MessageTitleUpdated(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[4576]!, self._r[4576]!, [_0]) - } - public var ChatList_Context_Unmute: String { return self._s[4577]! } - public var Chat_SlowmodeAttachmentLimitReached: String { return self._s[4578]! } - public var Stickers_GroupStickersHelp: String { return self._s[4579]! } - public var Checkout_Title: String { return self._s[4580]! } - public var Activity_RecordingAudio: String { return self._s[4581]! } - public var SettingsSearch_Synonyms_Notifications_GroupNotificationsPreview: String { return self._s[4582]! } - public var BlockedUsers_BlockTitle: String { return self._s[4583]! } - public var DialogList_SavedMessagesHelp: String { return self._s[4585]! } - public var Calls_All: String { return self._s[4586]! } - public var Settings_FAQ_Button: String { return self._s[4588]! } - public var Conversation_Dice_u1F3B0: String { return self._s[4590]! } - public func Time_MonthOfYear_m5(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[4591]!, self._r[4591]!, [_0]) - } - public var Conversation_ReportGroupLocation: String { return self._s[4592]! } - public var Passport_Scans_Upload: String { return self._s[4593]! } - public var Channel_EditAdmin_PermissionPinMessages: String { return self._s[4595]! } - public var ChatList_UnarchiveAction: String { return self._s[4596]! } - public var Stats_GroupTopInviter_History: String { return self._s[4597]! } - public var GroupInfo_Permissions_Title: String { return self._s[4598]! } - public var VoiceChat_CreateNewVoiceChatStart: String { return self._s[4599]! } - public var Passport_Language_el: String { return self._s[4600]! } - public var Channel_DiscussionMessageUnavailable: String { return self._s[4601]! } - public var GroupInfo_ActionPromote: String { return self._s[4602]! } - public var Group_OwnershipTransfer_ErrorLocatedGroupsTooMuch: String { return self._s[4603]! } - public var Media_LimitedAccessSelectMore: String { return self._s[4604]! } - public func TwoStepAuth_PendingEmailHelp(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[4605]!, self._r[4605]!, [_0]) - } - public var VoiceOver_Chat_Reply: String { return self._s[4606]! } - public var Month_GenMay: String { return self._s[4607]! } - public var DialogList_DeleteBotConversationConfirmation: String { return self._s[4608]! } - public var Chat_PsaTooltip_covid: String { return self._s[4609]! } - public var Watch_Suggestion_CantTalk: String { return self._s[4610]! } - public var Privacy_GroupsAndChannels_NeverAllow_Title: String { return self._s[4611]! } - public var AppUpgrade_Running: String { return self._s[4612]! } - public var PasscodeSettings_UnlockWithFaceId: String { return self._s[4615]! } - public var Notification_Exceptions_PreviewAlwaysOff: String { return self._s[4616]! } - public var SharedMedia_EmptyText: String { return self._s[4617]! } - public var Passport_Address_EditResidentialAddress: String { return self._s[4618]! } - public var SettingsSearch_Synonyms_Notifications_GroupNotificationsAlert: String { return self._s[4619]! } - public var Message_PinnedGame: String { return self._s[4620]! } - public var KeyCommand_SearchInChat: String { return self._s[4621]! } - public var Appearance_ThemeCarouselNewNight: String { return self._s[4622]! } - public var ChatList_Search_FilterMedia: String { return self._s[4623]! } - public var Message_PinnedAudioMessage: String { return self._s[4624]! } - public var ChannelInfo_ConfirmLeave: String { return self._s[4626]! } - public func Channel_AdminLog_MessagePromotedNameUsername(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[4627]!, self._r[4627]!, [_1, _2]) - } - public var SocksProxySetup_ProxyStatusUnavailable: String { return self._s[4628]! } - public var InviteLink_Create: String { return self._s[4629]! } - public func Passport_Email_CodeHelp(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[4630]!, self._r[4630]!, [_0]) - } - public func Message_PinnedTextMessage(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[4631]!, self._r[4631]!, [_0]) - } - public var Settings_AddAccount: String { return self._s[4632]! } - public var Channel_AdminLog_CanDeleteMessages: String { return self._s[4633]! } - public var Conversation_DiscardVoiceMessageTitle: String { return self._s[4634]! } - public var Channel_JoinChannel: String { return self._s[4635]! } - public var Watch_UserInfo_Unblock: String { return self._s[4636]! } - public var PhoneLabel_Title: String { return self._s[4637]! } - public var VoiceChat_EditPermissions: String { return self._s[4639]! } - public var Group_Setup_HistoryHiddenHelp: String { return self._s[4640]! } - public var Privacy_ProfilePhoto_AlwaysShareWith_Title: String { return self._s[4641]! } - public func Login_PhoneGenericEmailBody(_ _1: String, _ _2: String, _ _3: String, _ _4: String, _ _5: String, _ _6: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[4642]!, self._r[4642]!, [_1, _2, _3, _4, _5, _6]) - } - public var Channel_AddBotErrorHaveRights: String { return self._s[4643]! } - public var ChatList_TabIconFoldersTooltipNonEmptyFolders: String { return self._s[4644]! } - public var DialogList_EncryptionProcessing: String { return self._s[4645]! } - public var ChatList_Search_FilterChats: String { return self._s[4646]! } - public var WatchRemote_NotificationText: String { return self._s[4647]! } - public var EditTheme_ChangeColors: String { return self._s[4648]! } - public var GroupRemoved_ViewUserInfo: String { return self._s[4649]! } - public var CallSettings_OnMobile: String { return self._s[4651]! } - public var Month_ShortFebruary: String { return self._s[4653]! } - public var VoiceOver_MessageContextReply: String { return self._s[4654]! } - public var AutoremoveSetup_TimerValueNever: String { return self._s[4655]! } - public var Group_Location_ChangeLocation: String { return self._s[4657]! } - public func PUSH_VIDEO_CALL_REQUEST(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[4658]!, self._r[4658]!, [_1]) - } - public var Passport_Address_TypeBankStatementUploadScan: String { return self._s[4659]! } - public var VoiceOver_Media_PlaybackStop: String { return self._s[4660]! } - public var SettingsSearch_Synonyms_Data_SaveIncomingPhotos: String { return self._s[4661]! } - public func Channel_AdminLog_MessageRestrictedUntil(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[4663]!, self._r[4663]!, [_0]) - } - public var PhotoEditor_WarmthTool: String { return self._s[4664]! } - public var Login_InfoAvatarPhoto: String { return self._s[4665]! } - public var Notification_Exceptions_NewException_MessagePreviewHeader: String { return self._s[4666]! } - public var Permissions_CellularDataAllowInSettings_v0: String { return self._s[4667]! } - public var Map_PlacesInThisArea: String { return self._s[4668]! } - public var VoiceOver_Chat_ContactEmail: String { return self._s[4669]! } - public var Notifications_InAppNotificationsSounds: String { return self._s[4670]! } - public func PUSH_PINNED_NOTEXT(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[4671]!, self._r[4671]!, [_1]) - } - public var PeerInfo_ReportProfileVideo: String { return self._s[4672]! } - public var ShareMenu_Send: String { return self._s[4673]! } - public var Username_InvalidStartsWithNumber: String { return self._s[4674]! } - public func Channel_AdminLog_StartedVoiceChat(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[4675]!, self._r[4675]!, [_1]) - } - public var Appearance_AppIconClassicX: String { return self._s[4676]! } - public var Report_Report: String { return self._s[4677]! } - public func PUSH_CHANNEL_MESSAGE_ROUND(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[4678]!, self._r[4678]!, [_1]) - } - public var Conversation_StopPoll: String { return self._s[4679]! } - public var InfoPlist_NSLocationAlwaysUsageDescription: String { return self._s[4681]! } - public var Passport_Identity_EditIdentityCard: String { return self._s[4682]! } - public var Appearance_ThemePreview_ChatList_3_Name: String { return self._s[4683]! } - public var Conversation_Timer_Title: String { return self._s[4684]! } - public var Common_Next: String { return self._s[4685]! } - public var Notification_Exceptions_NewException: String { return self._s[4686]! } - public func Generic_OpenHiddenLinkAlert(_ _0: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(self._s[4687]!, self._r[4687]!, [_0]) } - public var AccessDenied_CallMicrophone: String { return self._s[4688]! } - public var VoiceChat_UnmutePeer: String { return self._s[4689]! } - public var ChatImportActivity_Retry: String { return self._s[4690]! } - public var SettingsSearch_Synonyms_Data_AutoDownloadUsingCellular: String { return self._s[4691]! } - public var ChangePhoneNumberCode_Help: String { return self._s[4692]! } - public var Passport_Identity_OneOfTypeIdentityCard: String { return self._s[4693]! } - public var Channel_AdminLogFilter_EventsLeaving: String { return self._s[4694]! } - public var BlockedUsers_LeavePrefix: String { return self._s[4695]! } + public var Notification_VideoCallOutgoing: String { return self._s[4688]! } + public var Login_InvalidCodeError: String { return self._s[4689]! } + public var Conversation_PrivateChannelTimeLimitedAlertJoin: String { return self._s[4690]! } + public func LastSeen_TodayAt(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[4692]!, self._r[4692]!, [_0]) + } + public var Conversation_InputTextCaptionPlaceholder: String { return self._s[4693]! } + public var ReportPeer_Report: String { return self._s[4694]! } + public var Camera_FlashOff: String { return self._s[4697]! } + public var Conversation_InputTextBroadcastPlaceholder: String { return self._s[4700]! } + public func Notification_VoiceChatScheduledTomorrow(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[4701]!, self._r[4701]!, [_1, _2]) + } + public var PrivacyPolicy_DeclineTitle: String { return self._s[4704]! } + public var SettingsSearch_Synonyms_Privacy_PasscodeAndTouchId: String { return self._s[4705]! } + public var Passport_FieldEmail: String { return self._s[4706]! } + public func Channel_AdminLog_MessageKickedName(_ _1: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[4707]!, self._r[4707]!, [_1]) + } + public var Notifications_ExceptionsResetToDefaults: String { return self._s[4708]! } + public var PeerInfo_PaneVoiceAndVideo: String { return self._s[4709]! } + public var Group_OwnershipTransfer_Title: String { return self._s[4710]! } + public var Conversation_DefaultRestrictedInline: String { return self._s[4711]! } + public var Login_PhoneNumberHelp: String { return self._s[4713]! } + public var Channel_AdminLogFilter_EventsNewMembers: String { return self._s[4714]! } + public var Conversation_PinnedQuiz: String { return self._s[4715]! } + public var CreateGroup_SoftUserLimitAlert: String { return self._s[4716]! } + public var Login_PhoneNumberAlreadyAuthorizedSwitch: String { return self._s[4717]! } + public var Group_MessagePhotoUpdated: String { return self._s[4718]! } + public var LoginPassword_PasswordPlaceholder: String { return self._s[4719]! } + public var BroadcastGroups_ConfirmationAlert_Text: String { return self._s[4720]! } + public var Passport_Identity_Translations: String { return self._s[4722]! } + public var ChatAdmins_AllMembersAreAdmins: String { return self._s[4723]! } + public var ChannelInfo_DeleteChannel: String { return self._s[4725]! } + public var PasscodeSettings_HelpBottom: String { return self._s[4726]! } + public var Channel_Members_AddMembers: String { return self._s[4727]! } + public var AutoDownloadSettings_LastDelimeter: String { return self._s[4728]! } + public var Notification_Exceptions_DeleteAllConfirmation: String { return self._s[4730]! } + public var Conversation_HoldForAudio: String { return self._s[4731]! } + public var Media_LimitedAccessChangeSettings: String { return self._s[4733]! } + public var Watch_LastSeen_Lately: String { return self._s[4734]! } + public var ChatList_Context_MarkAsRead: String { return self._s[4735]! } + public var Conversation_PinnedMessage: String { return self._s[4736]! } + public var SettingsSearch_Synonyms_Appearance_ColorTheme: String { return self._s[4737]! } + public var VoiceChat_StopRecordingStop: String { return self._s[4739]! } + public var Passport_UpdateRequiredError: String { return self._s[4740]! } + public var PrivacySettings_Passcode: String { return self._s[4741]! } + public func Call_EmojiDescription(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[4742]!, self._r[4742]!, [_0]) + } + public var AutoNightTheme_NotAvailable: String { return self._s[4743]! } + public var Conversation_PressVolumeButtonForSound: String { return self._s[4744]! } + public var VoiceOver_Common_On: String { return self._s[4745]! } + public var LoginPassword_InvalidPasswordError: String { return self._s[4746]! } + public var ChatListFolder_IncludedSectionHeader: String { return self._s[4747]! } + public var Channel_SignMessages_Help: String { return self._s[4748]! } + public var ChatList_DeleteForEveryoneConfirmationTitle: String { return self._s[4749]! } + public var Conversation_TitleNoComments: String { return self._s[4750]! } + public var MediaPicker_LivePhotoDescription: String { return self._s[4751]! } + public var GroupInfo_Permissions: String { return self._s[4752]! } + public var GroupPermission_NoSendLinks: String { return self._s[4755]! } + public func Conversation_ScheduledVoiceChatStartsTomorrow(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[4756]!, self._r[4756]!, [_0]) + } + public var Passport_Identity_ResidenceCountry: String { return self._s[4757]! } + public var Appearance_ThemeCarouselNightBlue: String { return self._s[4759]! } + public var ChatList_ArchiveAction: String { return self._s[4760]! } + public func Channel_AdminLog_DisabledSlowmode(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[4761]!, self._r[4761]!, [_0]) + } + public var GroupInfo_GroupHistory: String { return self._s[4762]! } + public func Channel_Management_ErrorNotMember(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[4764]!, self._r[4764]!, [_0]) + } + public var Privacy_Forwards_LinkIfAllowed: String { return self._s[4766]! } + public var Channel_Info_Banned: String { return self._s[4767]! } + public var Paint_RecentStickers: String { return self._s[4768]! } + public var VoiceOver_MessageContextSend: String { return self._s[4769]! } + public var Group_ErrorNotMutualContact: String { return self._s[4770]! } + public var ReportPeer_ReasonOther: String { return self._s[4772]! } + public var Channel_BanUser_PermissionChangeGroupInfo: String { return self._s[4773]! } + public var SocksProxySetup_ShareQRCodeInfo: String { return self._s[4775]! } + public var KeyCommand_Find: String { return self._s[4776]! } + public func Channel_MessageTitleUpdated(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[4777]!, self._r[4777]!, [_0]) + } + public var ChatList_Context_Unmute: String { return self._s[4778]! } + public var Chat_SlowmodeAttachmentLimitReached: String { return self._s[4779]! } + public var TwoFactorSetup_ResetDone_Action: String { return self._s[4780]! } + public var Stickers_GroupStickersHelp: String { return self._s[4781]! } + public var Checkout_Title: String { return self._s[4782]! } + public var Activity_RecordingAudio: String { return self._s[4783]! } + public var SettingsSearch_Synonyms_Notifications_GroupNotificationsPreview: String { return self._s[4784]! } + public var BlockedUsers_BlockTitle: String { return self._s[4785]! } + public var DialogList_SavedMessagesHelp: String { return self._s[4787]! } + public var Calls_All: String { return self._s[4788]! } + public var Settings_FAQ_Button: String { return self._s[4790]! } + public var Conversation_Dice_u1F3B0: String { return self._s[4792]! } + public func Time_MonthOfYear_m5(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[4793]!, self._r[4793]!, [_0]) + } + public var Conversation_ReportGroupLocation: String { return self._s[4794]! } + public var Passport_Scans_Upload: String { return self._s[4795]! } + public var Channel_EditAdmin_PermissionPinMessages: String { return self._s[4797]! } + public var ChatList_UnarchiveAction: String { return self._s[4798]! } + public var Stats_GroupTopInviter_History: String { return self._s[4799]! } + public var GroupInfo_Permissions_Title: String { return self._s[4800]! } + public var VoiceChat_CreateNewVoiceChatStart: String { return self._s[4801]! } + public var Passport_Language_el: String { return self._s[4802]! } + public var Channel_DiscussionMessageUnavailable: String { return self._s[4803]! } + public func UserInfo_ContactForwardTooltip_TwoChats_One(_ _0: String, _ _1: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[4804]!, self._r[4804]!, [_0, _1]) + } + public var GroupInfo_ActionPromote: String { return self._s[4805]! } + public var Group_OwnershipTransfer_ErrorLocatedGroupsTooMuch: String { return self._s[4806]! } + public var Media_LimitedAccessSelectMore: String { return self._s[4807]! } + public func TwoStepAuth_PendingEmailHelp(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[4808]!, self._r[4808]!, [_0]) + } + public var VoiceOver_Chat_Reply: String { return self._s[4809]! } + public var Month_GenMay: String { return self._s[4810]! } + public var DialogList_DeleteBotConversationConfirmation: String { return self._s[4811]! } + public var Chat_PsaTooltip_covid: String { return self._s[4812]! } + public var Watch_Suggestion_CantTalk: String { return self._s[4813]! } + public var Privacy_GroupsAndChannels_NeverAllow_Title: String { return self._s[4814]! } + public var AppUpgrade_Running: String { return self._s[4815]! } + public var PasscodeSettings_UnlockWithFaceId: String { return self._s[4818]! } + public var Notification_Exceptions_PreviewAlwaysOff: String { return self._s[4819]! } + public var SharedMedia_EmptyText: String { return self._s[4820]! } + public var Passport_Address_EditResidentialAddress: String { return self._s[4821]! } + public var SettingsSearch_Synonyms_Notifications_GroupNotificationsAlert: String { return self._s[4822]! } + public var Message_PinnedGame: String { return self._s[4823]! } + public var KeyCommand_SearchInChat: String { return self._s[4824]! } + public var Appearance_ThemeCarouselNewNight: String { return self._s[4825]! } + public var ChatList_Search_FilterMedia: String { return self._s[4826]! } + public var Message_PinnedAudioMessage: String { return self._s[4827]! } + public var ChannelInfo_ConfirmLeave: String { return self._s[4829]! } + public func Channel_AdminLog_MessagePromotedNameUsername(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[4830]!, self._r[4830]!, [_1, _2]) + } + public var SocksProxySetup_ProxyStatusUnavailable: String { return self._s[4831]! } + public var InviteLink_Create: String { return self._s[4832]! } + public func Passport_Email_CodeHelp(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[4833]!, self._r[4833]!, [_0]) + } + public func Message_PinnedTextMessage(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[4834]!, self._r[4834]!, [_0]) + } + public var Settings_AddAccount: String { return self._s[4835]! } + public var Channel_AdminLog_CanDeleteMessages: String { return self._s[4836]! } + public var Conversation_DiscardVoiceMessageTitle: String { return self._s[4837]! } + public var Channel_JoinChannel: String { return self._s[4838]! } + public var Watch_UserInfo_Unblock: String { return self._s[4839]! } + public var PhoneLabel_Title: String { return self._s[4840]! } + public var VoiceChat_EditPermissions: String { return self._s[4842]! } + public var Group_Setup_HistoryHiddenHelp: String { return self._s[4843]! } + public var Privacy_ProfilePhoto_AlwaysShareWith_Title: String { return self._s[4844]! } + public func Login_PhoneGenericEmailBody(_ _1: String, _ _2: String, _ _3: String, _ _4: String, _ _5: String, _ _6: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[4845]!, self._r[4845]!, [_1, _2, _3, _4, _5, _6]) + } + public var Channel_AddBotErrorHaveRights: String { return self._s[4846]! } + public var ChatList_TabIconFoldersTooltipNonEmptyFolders: String { return self._s[4847]! } + public var DialogList_EncryptionProcessing: String { return self._s[4848]! } + public var ChatList_Search_FilterChats: String { return self._s[4849]! } + public var WatchRemote_NotificationText: String { return self._s[4850]! } + public var EditTheme_ChangeColors: String { return self._s[4852]! } + public var GroupRemoved_ViewUserInfo: String { return self._s[4853]! } + public var CallSettings_OnMobile: String { return self._s[4855]! } + public var Month_ShortFebruary: String { return self._s[4857]! } + public var VoiceOver_MessageContextReply: String { return self._s[4858]! } + public var AutoremoveSetup_TimerValueNever: String { return self._s[4859]! } + public var Group_Location_ChangeLocation: String { return self._s[4861]! } + public func PUSH_VIDEO_CALL_REQUEST(_ _1: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[4862]!, self._r[4862]!, [_1]) + } + public var Passport_Address_TypeBankStatementUploadScan: String { return self._s[4863]! } + public var VoiceOver_Media_PlaybackStop: String { return self._s[4864]! } + public var SettingsSearch_Synonyms_Data_SaveIncomingPhotos: String { return self._s[4865]! } + public func Channel_AdminLog_MessageRestrictedUntil(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[4867]!, self._r[4867]!, [_0]) + } + public var PhotoEditor_WarmthTool: String { return self._s[4868]! } + public var Login_InfoAvatarPhoto: String { return self._s[4869]! } + public var Notification_Exceptions_NewException_MessagePreviewHeader: String { return self._s[4870]! } + public var Permissions_CellularDataAllowInSettings_v0: String { return self._s[4871]! } + public var Map_PlacesInThisArea: String { return self._s[4872]! } + public var VoiceOver_Chat_ContactEmail: String { return self._s[4873]! } + public var Notifications_InAppNotificationsSounds: String { return self._s[4874]! } + public func PUSH_PINNED_NOTEXT(_ _1: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[4875]!, self._r[4875]!, [_1]) + } + public var PeerInfo_ReportProfileVideo: String { return self._s[4876]! } + public var ShareMenu_Send: String { return self._s[4877]! } + public var Username_InvalidStartsWithNumber: String { return self._s[4878]! } + public func Channel_AdminLog_StartedVoiceChat(_ _1: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[4879]!, self._r[4879]!, [_1]) + } + public var Appearance_AppIconClassicX: String { return self._s[4880]! } + public var Report_Report: String { return self._s[4881]! } + public func PUSH_CHANNEL_MESSAGE_ROUND(_ _1: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[4882]!, self._r[4882]!, [_1]) + } + public var Conversation_StopPoll: String { return self._s[4883]! } + public var InfoPlist_NSLocationAlwaysUsageDescription: String { return self._s[4885]! } + public var Passport_Identity_EditIdentityCard: String { return self._s[4886]! } + public var Appearance_ThemePreview_ChatList_3_Name: String { return self._s[4887]! } + public var Conversation_Timer_Title: String { return self._s[4888]! } + public var Common_Next: String { return self._s[4889]! } + public var Notification_Exceptions_NewException: String { return self._s[4890]! } + public func Generic_OpenHiddenLinkAlert(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[4891]!, self._r[4891]!, [_0]) + } + public var AccessDenied_CallMicrophone: String { return self._s[4892]! } + public var VoiceChat_UnmutePeer: String { return self._s[4893]! } + public var ChatImportActivity_Retry: String { return self._s[4894]! } + public var SettingsSearch_Synonyms_Data_AutoDownloadUsingCellular: String { return self._s[4895]! } + public var ChangePhoneNumberCode_Help: String { return self._s[4896]! } + public var Passport_Identity_OneOfTypeIdentityCard: String { return self._s[4897]! } + public var Channel_AdminLogFilter_EventsLeaving: String { return self._s[4898]! } + public var BlockedUsers_LeavePrefix: String { return self._s[4899]! } public func Passport_RequestHeader(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[4696]!, self._r[4696]!, [_0]) + return formatWithArgumentRanges(self._s[4900]!, self._r[4900]!, [_0]) } - public var Group_About_Help: String { return self._s[4697]! } - public var TwoStepAuth_ChangePasswordDescription: String { return self._s[4698]! } - public var Tour_Title3: String { return self._s[4699]! } - public var Watch_Conversation_Unblock: String { return self._s[4700]! } - public var Watch_UserInfo_Block: String { return self._s[4701]! } - public var Notifications_ChannelNotificationsAlert: String { return self._s[4702]! } - public var TwoFactorSetup_Hint_Action: String { return self._s[4703]! } - public var IntentsSettings_SuggestedChatsInfo: String { return self._s[4704]! } - public var TextFormat_AddLinkTitle: String { return self._s[4705]! } - public var GroupInfo_InviteLink_RevokeAlert_Revoke: String { return self._s[4706]! } - public var TwoStepAuth_EnterPasswordTitle: String { return self._s[4707]! } - public var FastTwoStepSetup_PasswordSection: String { return self._s[4709]! } - public var Compose_ChannelMembers: String { return self._s[4710]! } - public var Conversation_ForwardTitle: String { return self._s[4711]! } - public var Conversation_PinnedPoll: String { return self._s[4714]! } + public var Group_About_Help: String { return self._s[4901]! } + public var TwoStepAuth_ChangePasswordDescription: String { return self._s[4902]! } + public var Tour_Title3: String { return self._s[4903]! } + public var Watch_Conversation_Unblock: String { return self._s[4904]! } + public var Watch_UserInfo_Block: String { return self._s[4905]! } + public var Notifications_ChannelNotificationsAlert: String { return self._s[4906]! } + public var TwoFactorSetup_Hint_Action: String { return self._s[4907]! } + public var IntentsSettings_SuggestedChatsInfo: String { return self._s[4908]! } + public var TextFormat_AddLinkTitle: String { return self._s[4909]! } + public var GroupInfo_InviteLink_RevokeAlert_Revoke: String { return self._s[4910]! } + public func Notification_VoiceChatScheduled(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[4911]!, self._r[4911]!, [_1, _2]) + } + public var TwoStepAuth_EnterPasswordTitle: String { return self._s[4912]! } + public var FastTwoStepSetup_PasswordSection: String { return self._s[4914]! } + public var Compose_ChannelMembers: String { return self._s[4915]! } + public var Conversation_ForwardTitle: String { return self._s[4916]! } + public var Conversation_PinnedPoll: String { return self._s[4919]! } public func VoiceOver_Chat_AnonymousPollFrom(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[4715]!, self._r[4715]!, [_0]) + return formatWithArgumentRanges(self._s[4920]!, self._r[4920]!, [_0]) } - public var SettingsSearch_Synonyms_EditProfile_AddAccount: String { return self._s[4716]! } - public var Conversation_ContextMenuStickerPackAdd: String { return self._s[4717]! } - public var Stats_Overview: String { return self._s[4718]! } - public var Map_HomeAndWorkTitle: String { return self._s[4719]! } + public var SettingsSearch_Synonyms_EditProfile_AddAccount: String { return self._s[4921]! } + public var Conversation_ContextMenuStickerPackAdd: String { return self._s[4923]! } + public var Stats_Overview: String { return self._s[4924]! } + public var Map_HomeAndWorkTitle: String { return self._s[4925]! } public func Time_PreciseDate_m4(_ _1: String, _ _2: String, _ _3: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[4720]!, self._r[4720]!, [_1, _2, _3]) + return formatWithArgumentRanges(self._s[4926]!, self._r[4926]!, [_1, _2, _3]) } - public var Passport_Address_CityPlaceholder: String { return self._s[4721]! } - public var InfoPlist_NSLocationAlwaysAndWhenInUseUsageDescription: String { return self._s[4722]! } - public var Privacy_PhoneNumber: String { return self._s[4723]! } - public var ChatList_Search_FilterFiles: String { return self._s[4724]! } - public var ChatList_DeleteForEveryoneConfirmationAction: String { return self._s[4725]! } - public var ChannelIntro_CreateChannel: String { return self._s[4726]! } - public var Conversation_InputTextAnonymousPlaceholder: String { return self._s[4727]! } + public var Passport_Address_CityPlaceholder: String { return self._s[4927]! } + public var InfoPlist_NSLocationAlwaysAndWhenInUseUsageDescription: String { return self._s[4928]! } + public var Privacy_PhoneNumber: String { return self._s[4929]! } + public var ChatList_Search_FilterFiles: String { return self._s[4930]! } + public var ChatList_DeleteForEveryoneConfirmationAction: String { return self._s[4931]! } + public var ChannelIntro_CreateChannel: String { return self._s[4932]! } + public var Conversation_InputTextAnonymousPlaceholder: String { return self._s[4933]! } public func Login_EmailCodeBody(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[4728]!, self._r[4728]!, [_0]) + return formatWithArgumentRanges(self._s[4934]!, self._r[4934]!, [_0]) } - public var Weekday_ShortMonday: String { return self._s[4729]! } - public var Passport_Language_ar: String { return self._s[4731]! } - public var SettingsSearch_Synonyms_EditProfile_Title: String { return self._s[4732]! } - public var TwoFactorSetup_Done_Title: String { return self._s[4733]! } - public var Calls_RatingFeedback: String { return self._s[4734]! } - public var SettingsSearch_Synonyms_Notifications_ChannelNotificationsPreview: String { return self._s[4735]! } - public var AutoDownloadSettings_ResetSettings: String { return self._s[4738]! } + public var Weekday_ShortMonday: String { return self._s[4935]! } + public var Passport_Language_ar: String { return self._s[4937]! } + public var SettingsSearch_Synonyms_EditProfile_Title: String { return self._s[4938]! } + public var TwoFactorSetup_Done_Title: String { return self._s[4939]! } + public var Calls_RatingFeedback: String { return self._s[4940]! } + public var SettingsSearch_Synonyms_Notifications_ChannelNotificationsPreview: String { return self._s[4941]! } + public var AutoDownloadSettings_ResetSettings: String { return self._s[4944]! } public func VoiceOver_SelfDestructTimerOn(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[4739]!, self._r[4739]!, [_0]) + return formatWithArgumentRanges(self._s[4945]!, self._r[4945]!, [_0]) } - public var Watch_Compose_Send: String { return self._s[4740]! } - public var PasscodeSettings_ChangePasscode: String { return self._s[4741]! } - public var WebSearch_RecentSectionClear: String { return self._s[4742]! } + public var Watch_Compose_Send: String { return self._s[4946]! } + public var PasscodeSettings_ChangePasscode: String { return self._s[4947]! } + public var WebSearch_RecentSectionClear: String { return self._s[4948]! } public func Contacts_AccessDeniedHelpPortrait(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[4743]!, self._r[4743]!, [_0]) + return formatWithArgumentRanges(self._s[4949]!, self._r[4949]!, [_0]) } - public var WallpaperSearch_ColorTeal: String { return self._s[4744]! } - public var Wallpaper_SetCustomBackgroundInfo: String { return self._s[4745]! } - public var Permissions_ContactsTitle_v0: String { return self._s[4746]! } - public var Checkout_PasswordEntry_Pay: String { return self._s[4748]! } - public var Settings_SavedMessages: String { return self._s[4749]! } - public var TwoStepAuth_ReEnterPasswordDescription: String { return self._s[4750]! } - public var Month_ShortMarch: String { return self._s[4751]! } - public var Message_Location: String { return self._s[4752]! } + public var WallpaperSearch_ColorTeal: String { return self._s[4950]! } + public var Wallpaper_SetCustomBackgroundInfo: String { return self._s[4951]! } + public var Permissions_ContactsTitle_v0: String { return self._s[4952]! } + public var Checkout_PasswordEntry_Pay: String { return self._s[4954]! } + public var Settings_SavedMessages: String { return self._s[4955]! } + public var TwoStepAuth_ReEnterPasswordDescription: String { return self._s[4956]! } + public var Month_ShortMarch: String { return self._s[4957]! } + public var Message_Location: String { return self._s[4958]! } public func PUSH_MESSAGE_GIF(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[4753]!, self._r[4753]!, [_1]) + return formatWithArgumentRanges(self._s[4959]!, self._r[4959]!, [_1]) } public func Channel_AdminLog_MessageRemovedAdminName(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[4754]!, self._r[4754]!, [_1]) + return formatWithArgumentRanges(self._s[4960]!, self._r[4960]!, [_1]) } public func Notification_CallTimeFormat(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[4755]!, self._r[4755]!, [_1, _2]) + return formatWithArgumentRanges(self._s[4961]!, self._r[4961]!, [_1, _2]) } - public var VoiceOver_Chat_VoiceMessage: String { return self._s[4757]! } + public var VoiceOver_Chat_VoiceMessage: String { return self._s[4963]! } public func Channel_AdminLog_MessageChangedUnlinkedChannel(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[4758]!, self._r[4758]!, [_1, _2]) + return formatWithArgumentRanges(self._s[4964]!, self._r[4964]!, [_1, _2]) } - public var GroupPermission_NoSendMedia: String { return self._s[4759]! } - public var Conversation_ClousStorageInfo_Description2: String { return self._s[4760]! } - public var SharedMedia_CategoryDocs: String { return self._s[4761]! } - public var Appearance_RemoveThemeConfirmation: String { return self._s[4762]! } - public var Paint_Framed: String { return self._s[4763]! } - public var Channel_Setup_LinkTypePublic: String { return self._s[4764]! } - public var Channel_EditAdmin_PermissionAddAdmins: String { return self._s[4765]! } - public var Passport_Identity_DoesNotExpire: String { return self._s[4766]! } + public var GroupPermission_NoSendMedia: String { return self._s[4965]! } + public var Conversation_ClousStorageInfo_Description2: String { return self._s[4966]! } + public var SharedMedia_CategoryDocs: String { return self._s[4967]! } + public var Appearance_RemoveThemeConfirmation: String { return self._s[4968]! } + public var Paint_Framed: String { return self._s[4969]! } + public var Channel_Setup_LinkTypePublic: String { return self._s[4970]! } + public var Channel_EditAdmin_PermissionAddAdmins: String { return self._s[4971]! } + public var Passport_Identity_DoesNotExpire: String { return self._s[4972]! } public func ChatImport_SelectionConfirmationUserWithTitle(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[4767]!, self._r[4767]!, [_1, _2]) + return formatWithArgumentRanges(self._s[4973]!, self._r[4973]!, [_1, _2]) } - public var Channel_SignMessages: String { return self._s[4768]! } - public var Contacts_AccessDeniedHelpON: String { return self._s[4769]! } - public var Conversation_ContextMenuStickerPackInfo: String { return self._s[4770]! } + public var TwoStepAuth_RecoveryUnavailableResetAction: String { return self._s[4974]! } + public var Channel_SignMessages: String { return self._s[4975]! } + public var Contacts_AccessDeniedHelpON: String { return self._s[4976]! } + public var Conversation_ContextMenuStickerPackInfo: String { return self._s[4977]! } public func PUSH_CHAT_LEFT(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[4771]!, self._r[4771]!, [_1, _2]) + return formatWithArgumentRanges(self._s[4978]!, self._r[4978]!, [_1, _2]) } - public var InviteLink_Create_TimeLimitNoLimit: String { return self._s[4772]! } - public var GroupInfo_UpgradeButton: String { return self._s[4773]! } - public var Channel_EditAdmin_PermissionInviteMembers: String { return self._s[4774]! } - public var AutoDownloadSettings_Files: String { return self._s[4775]! } + public var InviteLink_Create_TimeLimitNoLimit: String { return self._s[4979]! } + public var ImportStickerPack_ChooseName: String { return self._s[4980]! } + public var GroupInfo_UpgradeButton: String { return self._s[4981]! } + public var Channel_EditAdmin_PermissionInviteMembers: String { return self._s[4982]! } + public func Conversation_ScheduledVoiceChatStartsTomorrowShort(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[4983]!, self._r[4983]!, [_0]) + } + public var AutoDownloadSettings_Files: String { return self._s[4984]! } public func Notification_ChangedGroupName(_ _0: String, _ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[4776]!, self._r[4776]!, [_0, _1]) + return formatWithArgumentRanges(self._s[4985]!, self._r[4985]!, [_0, _1]) } - public var Login_SendCodeViaSms: String { return self._s[4778]! } - public var Update_UpdateApp: String { return self._s[4779]! } - public var Channel_Setup_TypePublic: String { return self._s[4780]! } - public var Watch_Compose_CreateMessage: String { return self._s[4781]! } + public var Login_SendCodeViaSms: String { return self._s[4987]! } + public var Update_UpdateApp: String { return self._s[4988]! } + public var Channel_Setup_TypePublic: String { return self._s[4989]! } + public var Watch_Compose_CreateMessage: String { return self._s[4990]! } public func PUSH_CHAT_MESSAGE_VIDEOS(_ _1: String, _ _2: String, _ _3: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[4782]!, self._r[4782]!, [_1, _2, _3]) + return formatWithArgumentRanges(self._s[4991]!, self._r[4991]!, [_1, _2, _3]) } - public var StickerPacksSettings_ManagingHelp: String { return self._s[4783]! } - public var VoiceOver_Chat_Video: String { return self._s[4784]! } - public var Forward_ChannelReadOnly: String { return self._s[4785]! } - public var StickerPack_HideStickers: String { return self._s[4786]! } - public var ChatListFolder_NameContacts: String { return self._s[4787]! } - public var Profile_BotInfo: String { return self._s[4788]! } - public var Document_TargetConfirmationFormat: String { return self._s[4789]! } - public var GroupInfo_InviteByLink: String { return self._s[4790]! } - public var Channel_AdminLog_BanSendStickersAndGifs: String { return self._s[4791]! } - public var Watch_Stickers_RecentPlaceholder: String { return self._s[4792]! } - public var Broadcast_AdminLog_EmptyText: String { return self._s[4793]! } - public var Passport_NotLoggedInMessage: String { return self._s[4794]! } - public var Conversation_StopQuizConfirmation: String { return self._s[4795]! } - public var Checkout_PaymentMethod: String { return self._s[4796]! } - public var ChatList_ArchivedChatsTitle: String { return self._s[4800]! } - public var TwoStepAuth_SetupPasswordConfirmFailed: String { return self._s[4801]! } - public var VoiceOver_Chat_RecordPreviewVoiceMessage: String { return self._s[4802]! } - public var PrivacyLastSeenSettings_GroupsAndChannelsHelp: String { return self._s[4803]! } - public var SettingsSearch_Synonyms_Privacy_Data_ContactsReset: String { return self._s[4804]! } - public var Conversation_GigagroupDescription: String { return self._s[4805]! } - public var Camera_Title: String { return self._s[4806]! } - public var Map_Directions: String { return self._s[4807]! } - public var Stats_MessagePublicForwardsTitle: String { return self._s[4809]! } - public var Privacy_ProfilePhoto_WhoCanSeeMyPhoto: String { return self._s[4810]! } - public var Profile_EncryptionKey: String { return self._s[4811]! } + public var StickerPacksSettings_ManagingHelp: String { return self._s[4992]! } + public var VoiceOver_Chat_Video: String { return self._s[4993]! } + public var Forward_ChannelReadOnly: String { return self._s[4994]! } + public var StickerPack_HideStickers: String { return self._s[4995]! } + public var ChatListFolder_NameContacts: String { return self._s[4996]! } + public var Profile_BotInfo: String { return self._s[4997]! } + public var Document_TargetConfirmationFormat: String { return self._s[4998]! } + public var GroupInfo_InviteByLink: String { return self._s[4999]! } + public var Channel_AdminLog_BanSendStickersAndGifs: String { return self._s[5000]! } + public var Watch_Stickers_RecentPlaceholder: String { return self._s[5001]! } + public var Broadcast_AdminLog_EmptyText: String { return self._s[5002]! } + public var Passport_NotLoggedInMessage: String { return self._s[5003]! } + public var Conversation_StopQuizConfirmation: String { return self._s[5004]! } + public var Checkout_PaymentMethod: String { return self._s[5005]! } + public var ChatList_ArchivedChatsTitle: String { return self._s[5010]! } + public var TwoStepAuth_SetupPasswordConfirmFailed: String { return self._s[5011]! } + public var VoiceOver_Chat_RecordPreviewVoiceMessage: String { return self._s[5012]! } + public var PrivacyLastSeenSettings_GroupsAndChannelsHelp: String { return self._s[5013]! } + public var SettingsSearch_Synonyms_Privacy_Data_ContactsReset: String { return self._s[5014]! } + public var Conversation_GigagroupDescription: String { return self._s[5015]! } + public var Camera_Title: String { return self._s[5016]! } + public var Map_Directions: String { return self._s[5017]! } + public var Stats_MessagePublicForwardsTitle: String { return self._s[5019]! } + public var Privacy_ProfilePhoto_WhoCanSeeMyPhoto: String { return self._s[5020]! } + public var Profile_EncryptionKey: String { return self._s[5021]! } public func LOCAL_CHAT_MESSAGE_FWDS(_ _1: String, _ _2: Int) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[4812]!, self._r[4812]!, [_1, "\(_2)"]) + return formatWithArgumentRanges(self._s[5022]!, self._r[5022]!, [_1, "\(_2)"]) } + public var VoiceChat_VideoPreviewShareCamera: String { return self._s[5023]! } public func Compatibility_SecretMediaVersionTooLow(_ _0: String, _ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[4813]!, self._r[4813]!, [_0, _1]) + return formatWithArgumentRanges(self._s[5024]!, self._r[5024]!, [_0, _1]) + } + public var Passport_Identity_TypePassport: String { return self._s[5025]! } + public var CreatePoll_QuizOptionsHeader: String { return self._s[5027]! } + public var Common_No: String { return self._s[5028]! } + public var Conversation_SendMessage_ScheduleMessage: String { return self._s[5029]! } + public var SettingsSearch_Synonyms_Privacy_LastSeen: String { return self._s[5030]! } + public var Settings_AboutEmpty: String { return self._s[5031]! } + public var TwoStepAuth_FloodError: String { return self._s[5033]! } + public var SettingsSearch_Synonyms_Appearance_TextSize: String { return self._s[5034]! } + public func Notification_VoiceChatScheduledChannel(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[5035]!, self._r[5035]!, [_0]) } - public var Passport_Identity_TypePassport: String { return self._s[4814]! } - public var CreatePoll_QuizOptionsHeader: String { return self._s[4816]! } - public var Common_No: String { return self._s[4817]! } - public var Conversation_SendMessage_ScheduleMessage: String { return self._s[4818]! } - public var SettingsSearch_Synonyms_Privacy_LastSeen: String { return self._s[4819]! } - public var Settings_AboutEmpty: String { return self._s[4820]! } - public var TwoStepAuth_FloodError: String { return self._s[4822]! } - public var SettingsSearch_Synonyms_Appearance_TextSize: String { return self._s[4823]! } public func Channel_AdminLog_MessageUnkickedName(_ _1: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[4825]!, self._r[4825]!, [_1]) + return formatWithArgumentRanges(self._s[5037]!, self._r[5037]!, [_1]) } - public var Notification_Exceptions_MessagePreviewAlwaysOn: String { return self._s[4828]! } - public var Conversation_Edit: String { return self._s[4829]! } - public var CheckoutInfo_SaveInfo: String { return self._s[4831]! } - public var VoiceOver_Chat_AnonymousPoll: String { return self._s[4832]! } - public var Call_CameraTooltip: String { return self._s[4834]! } - public var InstantPage_FeedbackButtonShort: String { return self._s[4835]! } - public var Contacts_InviteToTelegram: String { return self._s[4836]! } - public var Notifications_ResetAllNotifications: String { return self._s[4837]! } - public var Calls_NewCall: String { return self._s[4838]! } - public var VoiceOver_Chat_Music: String { return self._s[4841]! } - public var Channel_AdminLogFilter_EventsInviteLinks: String { return self._s[4842]! } - public var Channel_Members_AddAdminErrorNotAMember: String { return self._s[4843]! } - public var Channel_Edit_AboutItem: String { return self._s[4844]! } - public var Message_VideoExpired: String { return self._s[4845]! } - public var Passport_Address_TypeTemporaryRegistrationUploadScan: String { return self._s[4846]! } + public var Notification_Exceptions_MessagePreviewAlwaysOn: String { return self._s[5040]! } + public var Conversation_Edit: String { return self._s[5041]! } + public var CheckoutInfo_SaveInfo: String { return self._s[5043]! } + public var VoiceOver_Chat_AnonymousPoll: String { return self._s[5044]! } + public var Call_CameraTooltip: String { return self._s[5046]! } + public var InstantPage_FeedbackButtonShort: String { return self._s[5047]! } + public var Contacts_InviteToTelegram: String { return self._s[5048]! } + public var Notifications_ResetAllNotifications: String { return self._s[5049]! } + public var Calls_NewCall: String { return self._s[5050]! } + public var VoiceOver_Chat_Music: String { return self._s[5053]! } + public var Channel_AdminLogFilter_EventsInviteLinks: String { return self._s[5054]! } + public var Channel_Members_AddAdminErrorNotAMember: String { return self._s[5055]! } + public var Channel_Edit_AboutItem: String { return self._s[5056]! } + public var Message_VideoExpired: String { return self._s[5057]! } + public var Passport_Address_TypeTemporaryRegistrationUploadScan: String { return self._s[5058]! } + public var Settings_TryEnterPassword: String { return self._s[5059]! } public func PUSH_CHAT_RETURNED(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[4847]!, self._r[4847]!, [_1, _2]) + return formatWithArgumentRanges(self._s[5060]!, self._r[5060]!, [_1, _2]) } - public var NotificationsSound_Input: String { return self._s[4849]! } - public var Notifications_ClassicTones: String { return self._s[4850]! } - public var Conversation_StatusTyping: String { return self._s[4851]! } - public var Checkout_ErrorProviderAccountInvalid: String { return self._s[4852]! } - public var ChatSettings_AutoDownloadSettings_Delimeter: String { return self._s[4853]! } - public var SettingsSearch_Synonyms_Notifications_BadgeIncludeMutedChats: String { return self._s[4854]! } - public var Conversation_MessageLeaveComment: String { return self._s[4855]! } - public var UserInfo_TapToCall: String { return self._s[4856]! } - public var EnterPasscode_EnterNewPasscodeNew: String { return self._s[4857]! } - public var Conversation_ClearAll: String { return self._s[4859]! } - public var UserInfo_NotificationsDefault: String { return self._s[4860]! } - public var Location_ProximityGroupTip: String { return self._s[4861]! } - public var Map_ChooseAPlace: String { return self._s[4862]! } - public var GroupInfo_AddParticipantTitle: String { return self._s[4864]! } - public var ChatList_PeerTypeNonContact: String { return self._s[4865]! } - public var Conversation_SlideToCancel: String { return self._s[4866]! } - public var Month_ShortJuly: String { return self._s[4867]! } - public var SocksProxySetup_ProxyType: String { return self._s[4868]! } + public var NotificationsSound_Input: String { return self._s[5062]! } + public var Notifications_ClassicTones: String { return self._s[5063]! } + public var Conversation_StatusTyping: String { return self._s[5064]! } + public var Checkout_ErrorProviderAccountInvalid: String { return self._s[5065]! } + public var ChatSettings_AutoDownloadSettings_Delimeter: String { return self._s[5066]! } + public var SettingsSearch_Synonyms_Notifications_BadgeIncludeMutedChats: String { return self._s[5067]! } + public var Conversation_MessageLeaveComment: String { return self._s[5068]! } + public var UserInfo_TapToCall: String { return self._s[5069]! } + public var EnterPasscode_EnterNewPasscodeNew: String { return self._s[5070]! } + public func ScheduleVoiceChat_ScheduleOn(_ _0: String, _ _1: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[5071]!, self._r[5071]!, [_0, _1]) + } + public var Conversation_ClearAll: String { return self._s[5073]! } + public var UserInfo_NotificationsDefault: String { return self._s[5074]! } + public func TwoFactorSetup_ResetFloodWait(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[5075]!, self._r[5075]!, [_0]) + } + public var Location_ProximityGroupTip: String { return self._s[5076]! } + public var Map_ChooseAPlace: String { return self._s[5077]! } + public var GroupInfo_AddParticipantTitle: String { return self._s[5079]! } + public var ChatList_PeerTypeNonContact: String { return self._s[5080]! } + public var Conversation_SlideToCancel: String { return self._s[5081]! } + public var Month_ShortJuly: String { return self._s[5082]! } + public var SocksProxySetup_ProxyType: String { return self._s[5083]! } public func ChatList_DeleteChatConfirmation(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[4869]!, self._r[4869]!, [_0]) + return formatWithArgumentRanges(self._s[5084]!, self._r[5084]!, [_0]) } - public var StickerPacks_ActionArchive: String { return self._s[4870]! } - public var ChatList_EditFolders: String { return self._s[4871]! } - public var TwoStepAuth_SetPasswordHelp: String { return self._s[4872]! } - public var ScheduledMessages_RemindersTitle: String { return self._s[4874]! } + public var StickerPacks_ActionArchive: String { return self._s[5085]! } + public var ChatList_EditFolders: String { return self._s[5086]! } + public var TwoStepAuth_SetPasswordHelp: String { return self._s[5087]! } + public var ScheduledMessages_RemindersTitle: String { return self._s[5089]! } public func GroupPermission_ApplyAlertText(_ _0: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[4875]!, self._r[4875]!, [_0]) + return formatWithArgumentRanges(self._s[5090]!, self._r[5090]!, [_0]) } - public var Permissions_PeopleNearbyTitle_v0: String { return self._s[4876]! } - public var Your_cards_expiration_year_is_invalid: String { return self._s[4877]! } - public var UserInfo_ShareMyContactInfo: String { return self._s[4879]! } - public var Passport_DeleteAddress: String { return self._s[4881]! } - public var Passport_DeletePassportConfirmation: String { return self._s[4882]! } - public var Passport_Identity_ReverseSide: String { return self._s[4883]! } - public var CheckoutInfo_ErrorEmailInvalid: String { return self._s[4884]! } - public var Login_InfoLastNamePlaceholder: String { return self._s[4885]! } - public var InviteLink_CreatedBy: String { return self._s[4886]! } - public var Passport_FieldAddress: String { return self._s[4887]! } - public var SettingsSearch_Synonyms_Calls_Title: String { return self._s[4888]! } - public var Passport_Identity_ResidenceCountryPlaceholder: String { return self._s[4891]! } - public var VoiceChat_Panel_TapToJoin: String { return self._s[4892]! } - public var Map_Home: String { return self._s[4893]! } - public var PollResults_Title: String { return self._s[4896]! } + public var Permissions_PeopleNearbyTitle_v0: String { return self._s[5091]! } + public var Your_cards_expiration_year_is_invalid: String { return self._s[5092]! } + public var UserInfo_ShareMyContactInfo: String { return self._s[5094]! } + public func Conversation_ScheduledVoiceChatStartsOnShort(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(self._s[5096]!, self._r[5096]!, [_0]) + } + public var Passport_DeleteAddress: String { return self._s[5097]! } + public var Passport_DeletePassportConfirmation: String { return self._s[5098]! } + public var Passport_Identity_ReverseSide: String { return self._s[5099]! } + public var CheckoutInfo_ErrorEmailInvalid: String { return self._s[5101]! } + public var Login_InfoLastNamePlaceholder: String { return self._s[5102]! } + public var InviteLink_CreatedBy: String { return self._s[5103]! } + public var Passport_FieldAddress: String { return self._s[5104]! } + public var SettingsSearch_Synonyms_Calls_Title: String { return self._s[5105]! } + public var Passport_Identity_ResidenceCountryPlaceholder: String { return self._s[5108]! } + public var VoiceChat_Panel_TapToJoin: String { return self._s[5109]! } + public var Map_Home: String { return self._s[5110]! } + public var PollResults_Title: String { return self._s[5113]! } public func InviteLink_OtherPermanentLinkInfo(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(self._s[4897]!, self._r[4897]!, [_1, _2]) + return formatWithArgumentRanges(self._s[5114]!, self._r[5114]!, [_1, _2]) } - public var ArchivedChats_IntroText2: String { return self._s[4899]! } - public var PasscodeSettings_SimplePasscodeHelp: String { return self._s[4900]! } - public var VoiceOver_Chat_ContactPhoneNumber: String { return self._s[4901]! } - public var VoiceChat_Muted: String { return self._s[4903]! } - public var CallFeedback_ReasonSilentRemote: String { return self._s[4904]! } - public var Passport_Identity_AddPersonalDetails: String { return self._s[4905]! } - public var Conversation_AutoremoveActionEnable: String { return self._s[4907]! } - public var Group_Info_AdminLog: String { return self._s[4908]! } - public var ChatSettings_AutoPlayTitle: String { return self._s[4909]! } - public var Appearance_Animations: String { return self._s[4910]! } - public var Appearance_TextSizeSetting: String { return self._s[4911]! } - public func Chat_MessagesUnpinned(_ value: Int32) -> String { + public var ArchivedChats_IntroText2: String { return self._s[5116]! } + public var VoiceChat_VideoPreviewTitle: String { return self._s[5117]! } + public var PasscodeSettings_SimplePasscodeHelp: String { return self._s[5118]! } + public var VoiceOver_Chat_ContactPhoneNumber: String { return self._s[5119]! } + public var VoiceChat_Muted: String { return self._s[5121]! } + public var CallFeedback_ReasonSilentRemote: String { return self._s[5122]! } + public var Passport_Identity_AddPersonalDetails: String { return self._s[5123]! } + public var Conversation_AutoremoveActionEnable: String { return self._s[5125]! } + public var Group_Info_AdminLog: String { return self._s[5126]! } + public var ChatSettings_AutoPlayTitle: String { return self._s[5127]! } + public var Appearance_Animations: String { return self._s[5128]! } + public var Appearance_TextSizeSetting: String { return self._s[5129]! } + public func Conversation_StatusOnline(_ 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 Media_ShareItem(_ 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[1 * 6 + Int(form.rawValue)]!, stringValue) } - public func MessageTimer_ShortDays(_ value: Int32) -> String { + public func ForwardedPolls(_ 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 PUSH_MESSAGE_FWDS(_ selector: Int32, _ _1: String, _ _2: Int32) -> String { - let form = getPluralizationForm(self.lc, selector) - return String(format: self._ps[3 * 6 + Int(form.rawValue)]!, _1, _2) + public func Contacts_InviteContacts(_ 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_ParticipantCount(_ value: Int32) -> String { + public func Call_Hours(_ 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 Watch_UserInfo_Mute(_ 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[5 * 6 + Int(form.rawValue)]!, stringValue) } - public func Notifications_ExceptionMuteExpires_Minutes(_ value: Int32) -> String { + public func ChatList_DeleteConfirmation(_ 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 SharedMedia_File(_ value: Int32) -> String { + public func Map_ETAHours(_ 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 Notification_GameScoreSimple(_ value: Int32) -> String { + public func Media_SharePhoto(_ 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 StickerPack_AddStickerCount(_ value: Int32) -> String { + public func Conversation_AutoremoveRemainingDays(_ 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 GroupInfo_ShowMoreMembers(_ value: Int32) -> String { + public func Stats_MessageForwards(_ 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 Watch_LastSeen_MinutesAgo(_ value: Int32) -> String { + public func StickerPacks_ArchiveStickerPacksConfirmation(_ 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 ForwardedPhotos(_ value: Int32) -> String { + public func InviteLink_PeopleCanJoin(_ 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 Chat_TitlePinnedMessages(_ value: Int32) -> String { + public func ChatList_MessageVideos(_ 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 ChatList_MessageMusic(_ value: Int32) -> String { + 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 VoiceOver_Chat_PollVotes(_ 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 Stats_GroupTopInviterInvites(_ 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_SendVideo(_ 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 Media_SharePhoto(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[18 * 6 + Int(form.rawValue)]!, stringValue) - } - public func PUSH_CHANNEL_MESSAGE_VIDEOS(_ selector: Int32, _ _1: String, _ _2: Int32) -> String { + public func PUSH_MESSAGE_VIDEOS(_ selector: Int32, _ _1: String, _ _2: Int32) -> String { let form = getPluralizationForm(self.lc, selector) - return String(format: self._ps[19 * 6 + Int(form.rawValue)]!, _1, _2) - } - 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[20 * 6 + Int(form.rawValue)]!, stringValue) - } - public func Conversation_MessageViewComments(_ 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 PUSH_CHANNEL_MESSAGE_FWDS(_ selector: Int32, _ _1: String, _ _2: Int32) -> String { - let form = getPluralizationForm(self.lc, selector) - return String(format: self._ps[22 * 6 + Int(form.rawValue)]!, _1, _2) - } - public func Map_ETAMinutes(_ 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 InviteText_ContactsCountText(_ 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 LastSeen_HoursAgo(_ 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 Notification_GameScoreSelfExtended(_ 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 UserCount(_ 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 Notifications_ExceptionMuteExpires_Days(_ 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 PUSH_MESSAGES(_ selector: Int32, _ _1: String, _ _2: Int32) -> String { - let form = getPluralizationForm(self.lc, selector) - return String(format: self._ps[29 * 6 + Int(form.rawValue)]!, _1, _2) - } - public func Stats_MessageForwards(_ 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 ForwardedAudios(_ 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 MessageTimer_ShortWeeks(_ 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 Conversation_StatusSubscribers(_ 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 ChatList_MessageFiles(_ 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 QuickSend_Photos(_ 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) + return String(format: self._ps[15 * 6 + Int(form.rawValue)]!, _1, _2) } public func Chat_DeleteMessagesConfirmation(_ 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 GroupInfo_ParticipantCount(_ 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 MessageTimer_Days(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[18 * 6 + Int(form.rawValue)]!, stringValue) + } + public func VoiceChat_InviteLink_InviteSpeakers(_ 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 ScheduledIn_Hours(_ 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 Notifications_ExceptionMuteExpires_Hours(_ 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 VoiceOver_Chat_PollVotes(_ 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 InviteText_ContactsCountText(_ 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 ChatListFilter_ShowMoreChats(_ 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 InviteLink_PeopleJoinedShort(_ 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 SharedMedia_Link(_ 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 ServiceMessage_GameScoreSelfSimple(_ 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 PUSH_CHAT_MESSAGE_PHOTOS(_ selector: Int32, _ _2: String, _ _1: String, _ _3: Int32) -> String { + let form = getPluralizationForm(self.lc, selector) + return String(format: self._ps[28 * 6 + Int(form.rawValue)]!, _2, _1, _3) + } + public func ScheduledIn_Days(_ 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 LiveLocation_MenuChatsCount(_ 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 ForwardedAuthorsOthers(_ selector: Int32, _ _0: String, _ _1: String) -> String { + let form = getPluralizationForm(self.lc, selector) + return String(format: self._ps[31 * 6 + Int(form.rawValue)]!, _0, _1) + } + public func PUSH_MESSAGES(_ selector: Int32, _ _1: String, _ _2: Int32) -> String { + let form = getPluralizationForm(self.lc, selector) + return String(format: self._ps[32 * 6 + Int(form.rawValue)]!, _1, _2) + } + public func UserCount(_ 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 StickerPack_StickerCount(_ 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 Stats_GroupTopPosterMessages(_ 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 AttachmentMenu_SendGif(_ 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 PUSH_CHAT_MESSAGE_DOCS_FIX1(_ selector: Int32, _ _2: String, _ _1: String, _ _3: Int32) -> String { - let form = getPluralizationForm(self.lc, selector) - return String(format: self._ps[37 * 6 + Int(form.rawValue)]!, _2, _1, _3) + public func PasscodeSettings_FailedAttempts(_ 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 OldChannels_InactiveWeek(_ value: Int32) -> String { + public func MuteFor_Hours(_ 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 PeopleNearby_ShowMorePeople(_ value: Int32) -> String { + public func MessageTimer_Weeks(_ 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 Theme_UsersCount(_ value: Int32) -> String { + public func Stats_GroupTopInviterInvites(_ 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 ForwardedGifs(_ value: Int32) -> String { + public func LiveLocationUpdated_MinutesAgo(_ 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) @@ -5652,621 +5891,660 @@ public final class PresentationStrings: Equatable { let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) return String(format: self._ps[42 * 6 + Int(form.rawValue)]!, stringValue) } - public func ForwardedVideos(_ value: Int32) -> String { + 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[43 * 6 + Int(form.rawValue)]!, stringValue) } - public func Notification_GameScoreSelfSimple(_ value: Int32) -> String { + public func Conversation_ContextMenuSelectAll(_ 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 Notifications_Exceptions(_ value: Int32) -> String { + 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[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 ServiceMessage_GameScoreSelfSimple(_ value: Int32) -> String { + public func InstantPage_Views(_ 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 MessageTimer_Minutes(_ 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 PUSH_MESSAGE_DOCS(_ selector: Int32, _ _1: String, _ _2: Int32) -> String { - let form = getPluralizationForm(self.lc, selector) - return String(format: self._ps[49 * 6 + Int(form.rawValue)]!, _1, _2) - } - public func StickerPack_RemoveMaskCount(_ 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 VoiceChat_Panel_Members(_ 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) + return String(format: self._ps[46 * 6 + Int(form.rawValue)]!, stringValue) } public func Stats_GroupShowMoreTopAdmins(_ 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 AttachmentMenu_SendPhoto(_ 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 AttachmentMenu_SendGif(_ 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 VoiceOver_Chat_PollOptionCount(_ 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 VoiceOver_Chat_MessagesSelected(_ 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 OldChannels_GroupFormat(_ 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 MessageTimer_Days(_ 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 SharedMedia_Photo(_ 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 Forward_ConfirmMultipleFiles(_ 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 Media_ShareVideo(_ 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 ChatListFilter_ShowMoreChats(_ 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_StatusOnline(_ 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 LastSeen_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 VoiceOver_Chat_UnreadMessages(_ 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 PUSH_CHAT_MESSAGE_FWDS(_ selector: Int32, _ _2: String, _ _1: String, _ _3: Int32) -> String { - let form = getPluralizationForm(self.lc, selector) - return String(format: self._ps[66 * 6 + Int(form.rawValue)]!, _2, _1, _3) - } - public func Stats_GroupShowMoreTopInviters(_ 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 MessageTimer_Months(_ 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 LiveLocationUpdated_MinutesAgo(_ 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 ChatList_DeleteConfirmation(_ 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 ForwardedVideoMessages(_ 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 ChatList_Search_Messages(_ 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 InviteLink_PeopleJoinedShort(_ 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 InviteLink_PeopleJoined(_ 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 PasscodeSettings_FailedAttempts(_ 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_CHAT_MESSAGE_VIDEOS(_ selector: Int32, _ _2: String, _ _1: String, _ _3: Int32) -> String { - let form = getPluralizationForm(self.lc, selector) - return String(format: self._ps[76 * 6 + Int(form.rawValue)]!, _2, _1, _3) - } - public func Conversation_AutoremoveRemainingDays(_ 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) - } - public func ForwardedAuthorsOthers(_ selector: Int32, _ _0: String, _ _1: String) -> String { - let form = getPluralizationForm(self.lc, selector) - return String(format: self._ps[78 * 6 + Int(form.rawValue)]!, _0, _1) + return String(format: self._ps[47 * 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[79 * 6 + Int(form.rawValue)]!, stringValue) + return String(format: self._ps[48 * 6 + Int(form.rawValue)]!, stringValue) } - public func MuteExpires_Hours(_ value: Int32) -> String { + public func GroupInfo_ShowMoreMembers(_ 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 AttachmentMenu_SendItem(_ 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 Invitation_Members(_ 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 MessageTimer_ShortMinutes(_ 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 MessageTimer_ShortSeconds(_ 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 Conversation_TitleComments(_ 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 Stats_GroupTopPosterChars(_ 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 Stats_GroupTopPosterMessages(_ 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 ForwardedFiles(_ 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 Contacts_InviteContacts(_ 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 VoiceChat_InviteLink_InviteListeners(_ 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 PrivacyLastSeenSettings_AddUsers(_ 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) + return String(format: self._ps[49 * 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[92 * 6 + Int(form.rawValue)]!, _1, _2) + return String(format: self._ps[50 * 6 + Int(form.rawValue)]!, _1, _2) } - public func Notification_GameScoreExtended(_ value: Int32) -> String { + public func AttachmentMenu_SendVideo(_ 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 PollResults_ShowMore(_ 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 Conversation_ContextViewReplies(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[95 * 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[96 * 6 + Int(form.rawValue)]!, _2, _1, _3) - } - public func Passport_Scans(_ 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 InviteLink_InviteLinks(_ 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 StickerPack_StickerCount(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[99 * 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[100 * 6 + Int(form.rawValue)]!, stringValue) - } - public func OldChannels_InactiveMonth(_ 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 LiveLocation_MenuChatsCount(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[102 * 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[103 * 6 + Int(form.rawValue)]!, stringValue) - } - public func InviteLink_PeopleCanJoin(_ 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 ForwardedLocations(_ 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 Notifications_ExceptionMuteExpires_Hours(_ 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 Conversation_StatusMembers(_ 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 ChatList_MessageVideos(_ 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 MuteExpires_Minutes(_ 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 DialogList_LiveLocationChatsCount(_ 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 ChatList_DeletedChats(_ 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 Conversation_LiveLocationMembersCount(_ 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 Watch_LastSeen_HoursAgo(_ 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 MessagePoll_VotedCount(_ 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 Conversation_TitleReplies(_ 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 Wallpaper_DeleteConfirmation(_ 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 MessageTimer_Hours(_ 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 VoiceChat_InviteLink_InviteSpeakers(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[118 * 6 + Int(form.rawValue)]!, stringValue) - } - public func PUSH_CHANNEL_MESSAGE_PHOTOS(_ selector: Int32, _ _1: String, _ _2: Int32) -> String { - let form = getPluralizationForm(self.lc, selector) - return String(format: self._ps[119 * 6 + Int(form.rawValue)]!, _1, _2) - } - public func MessageTimer_ShortHours(_ 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 Map_ETAHours(_ 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 Stats_GroupTopAdminKicks(_ 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 Stats_GroupTopAdminDeletions(_ 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 PUSH_MESSAGE_VIDEOS(_ selector: Int32, _ _1: String, _ _2: Int32) -> String { - let form = getPluralizationForm(self.lc, selector) - return String(format: self._ps[124 * 6 + Int(form.rawValue)]!, _1, _2) - } - public func OldChannels_Leave(_ 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 OldChannels_InactiveYear(_ 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 func VoiceChat_Status_Members(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[127 * 6 + Int(form.rawValue)]!, stringValue) - } - public func Call_ShortSeconds(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[128 * 6 + Int(form.rawValue)]!, stringValue) - } - public func StickerPack_RemoveStickerCount(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[129 * 6 + Int(form.rawValue)]!, stringValue) - } - public func InviteLink_PeopleRemaining(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[130 * 6 + Int(form.rawValue)]!, stringValue) - } - public func MessagePoll_QuizCount(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[131 * 6 + Int(form.rawValue)]!, stringValue) - } - public func Call_Hours(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[132 * 6 + Int(form.rawValue)]!, stringValue) - } - public func StickerPacks_ArchiveStickerPacksConfirmation(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[133 * 6 + Int(form.rawValue)]!, stringValue) - } - public func Stats_MessageViews(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[134 * 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[135 * 6 + Int(form.rawValue)]!, stringValue) - } - public func Call_Seconds(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[136 * 6 + Int(form.rawValue)]!, stringValue) - } - public func ServiceMessage_GameScoreSelfExtended(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[137 * 6 + Int(form.rawValue)]!, stringValue) - } - public func Stats_GroupTopAdminBans(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[138 * 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[139 * 6 + Int(form.rawValue)]!, stringValue) + return String(format: self._ps[51 * 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[140 * 6 + Int(form.rawValue)]!, stringValue) + return String(format: self._ps[52 * 6 + Int(form.rawValue)]!, stringValue) } - public func PUSH_CHANNEL_MESSAGES(_ selector: Int32, _ _1: String, _ _2: 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[53 * 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[54 * 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[55 * 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[56 * 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[57 * 6 + Int(form.rawValue)]!, stringValue) + } + public func OldChannels_InactiveWeek(_ 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 Stats_GroupTopAdminBans(_ 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 VoiceOver_Chat_UnreadMessages(_ 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 Stats_GroupTopPosterChars(_ 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_DeletedChats(_ 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 DialogList_LiveLocationChatsCount(_ 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 PUSH_CHANNEL_MESSAGE_PHOTOS(_ selector: Int32, _ _1: String, _ _2: Int32) -> String { let form = getPluralizationForm(self.lc, selector) - return String(format: self._ps[141 * 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[142 * 6 + Int(form.rawValue)]!, stringValue) - } - public func InstantPage_Views(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[143 * 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[144 * 6 + Int(form.rawValue)]!, stringValue) - } - public func Stats_GroupShowMoreTopPosters(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[145 * 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[146 * 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[147 * 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[148 * 6 + Int(form.rawValue)]!, stringValue) - } - public func SharedMedia_Link(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[149 * 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[150 * 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[151 * 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[152 * 6 + Int(form.rawValue)]!, stringValue) - } - public func Conversation_ContextMenuSelectAll(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[153 * 6 + Int(form.rawValue)]!, stringValue) - } - public func Call_Days(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[154 * 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[155 * 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[156 * 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[157 * 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[158 * 6 + Int(form.rawValue)]!, _2, _1, _3) - } - public func Conversation_SelectedMessages(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[159 * 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[160 * 6 + Int(form.rawValue)]!, stringValue) - } - public func MessageTimer_Weeks(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[161 * 6 + Int(form.rawValue)]!, stringValue) - } - public func ForwardedContacts(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[162 * 6 + Int(form.rawValue)]!, stringValue) - } - public func StickerPacks_DeleteStickerPacksConfirmation(_ value: Int32) -> String { - let form = getPluralizationForm(self.lc, value) - let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[163 * 6 + Int(form.rawValue)]!, stringValue) + return String(format: self._ps[64 * 6 + Int(form.rawValue)]!, _1, _2) } public func SharedMedia_Video(_ value: Int32) -> String { let form = getPluralizationForm(self.lc, value) let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[164 * 6 + Int(form.rawValue)]!, stringValue) + return String(format: self._ps[65 * 6 + Int(form.rawValue)]!, stringValue) } - public func ForwardedStickers(_ value: Int32) -> String { + public func PUSH_MESSAGE_FILES(_ selector: Int32, _ _1: String, _ _2: Int32) -> String { + let form = getPluralizationForm(self.lc, selector) + return String(format: self._ps[66 * 6 + Int(form.rawValue)]!, _1, _2) + } + public func PUSH_CHANNEL_MESSAGE_ROUNDS(_ selector: Int32, _ _1: String, _ _2: Int32) -> String { + let form = getPluralizationForm(self.lc, selector) + return String(format: self._ps[67 * 6 + Int(form.rawValue)]!, _1, _2) + } + public func VoiceChat_InviteLink_InviteListeners(_ value: Int32) -> String { let form = getPluralizationForm(self.lc, value) let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) - return String(format: self._ps[165 * 6 + Int(form.rawValue)]!, stringValue) + return String(format: self._ps[68 * 6 + Int(form.rawValue)]!, stringValue) } - public func PUSH_CHANNEL_MESSAGE_DOCS(_ selector: Int32, _ _1: String, _ _2: Int32) -> String { + public func Stats_GroupTopAdminDeletions(_ 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 ImportStickerPack_StickerCount(_ 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 Call_Seconds(_ 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 ForwardedVideoMessages(_ 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 ForwardedGifs(_ 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 ServiceMessage_GameScoreSimple(_ 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 PUSH_MESSAGE_ROUNDS(_ selector: Int32, _ _1: String, _ _2: Int32) -> String { let form = getPluralizationForm(self.lc, selector) - return String(format: self._ps[166 * 6 + Int(form.rawValue)]!, _1, _2) + return String(format: self._ps[75 * 6 + Int(form.rawValue)]!, _1, _2) + } + public func Call_ShortSeconds(_ 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 MessageTimer_Seconds(_ 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) + } + public func Chat_TitlePinnedMessages(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[78 * 6 + Int(form.rawValue)]!, stringValue) + } + public func ScheduledIn_Seconds(_ 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 Media_ShareVideo(_ 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 ForwardedContacts(_ 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 Conversation_ContextViewReplies(_ 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 PUSH_CHAT_MESSAGE_DOCS_FIX1(_ selector: Int32, _ _2: String, _ _1: String, _ _3: Int32) -> String { + let form = getPluralizationForm(self.lc, selector) + return String(format: self._ps[83 * 6 + Int(form.rawValue)]!, _2, _1, _3) + } + public func MessagePoll_QuizCount(_ 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 MessageTimer_Months(_ 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 SharedMedia_File(_ 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 Stats_GroupShowMoreTopPosters(_ 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 Notifications_ExceptionMuteExpires_Days(_ 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 Conversation_StatusSubscribers(_ 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 PollResults_ShowMore(_ 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 Forward_ConfirmMultipleFiles(_ 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 VoiceOver_Chat_ContactEmailCount(_ 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 OldChannels_InactiveYear(_ 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 Conversation_TitleReplies(_ 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 Stats_GroupShowMoreTopInviters(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[95 * 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[96 * 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[97 * 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[98 * 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[99 * 6 + Int(form.rawValue)]!, stringValue) + } + public func VoiceOver_Chat_MessagesSelected(_ 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 Conversation_MessageViewComments(_ 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 Conversation_TitleComments(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[102 * 6 + Int(form.rawValue)]!, stringValue) + } + public func MessageTimer_ShortHours(_ 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) + } + public func PUSH_CHANNEL_MESSAGE_FWDS(_ selector: Int32, _ _1: String, _ _2: Int32) -> String { + let form = getPluralizationForm(self.lc, selector) + return String(format: self._ps[104 * 6 + Int(form.rawValue)]!, _1, _2) + } + public func Theme_UsersCount(_ 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 ScheduledIn_Years(_ 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 ChatList_SelectedChats(_ 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 Map_ETAMinutes(_ 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_RemoveMaskCount(_ 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 VoiceChat_Status_Members(_ 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 ForwardedFiles(_ 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 Passport_Scans(_ 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 Call_Days(_ 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 ForwardedAudios(_ 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 MessageTimer_Hours(_ 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 Invitation_Members(_ 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 ForwardedLocations(_ 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 ChatList_Search_Messages(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[118 * 6 + Int(form.rawValue)]!, stringValue) + } + public func MuteExpires_Minutes(_ 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 VoiceOver_Chat_PollOptionCount(_ 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 OldChannels_GroupFormat(_ 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 ScheduledIn_Minutes(_ 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 ScheduledIn_Weeks(_ 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 InviteLink_PeopleJoined(_ 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 MessageTimer_ShortDays(_ 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 PUSH_CHAT_MESSAGE_VIDEOS(_ selector: Int32, _ _2: String, _ _1: String, _ _3: Int32) -> String { + let form = getPluralizationForm(self.lc, selector) + return String(format: self._ps[126 * 6 + Int(form.rawValue)]!, _2, _1, _3) + } + 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[127 * 6 + Int(form.rawValue)]!, stringValue) + } + public func Conversation_LiveLocationMembersCount(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[128 * 6 + Int(form.rawValue)]!, stringValue) + } + public func MessageTimer_ShortMinutes(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[129 * 6 + Int(form.rawValue)]!, stringValue) } public func ChatList_MessagePhotos(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[130 * 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[131 * 6 + Int(form.rawValue)]!, stringValue) + } + public func Notification_GameScoreSelfSimple(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[132 * 6 + Int(form.rawValue)]!, stringValue) + } + public func VoiceChat_Panel_Members(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[133 * 6 + Int(form.rawValue)]!, stringValue) + } + public func InviteLink_InviteLinks(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[134 * 6 + Int(form.rawValue)]!, stringValue) + } + public func ForwardedVideos(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[135 * 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[136 * 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[137 * 6 + Int(form.rawValue)]!, _1, _2) + } + public func MessageTimer_Years(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[138 * 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[139 * 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[140 * 6 + Int(form.rawValue)]!, stringValue) + } + public func InviteLink_PeopleRemaining(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[141 * 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[142 * 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[143 * 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[144 * 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[145 * 6 + Int(form.rawValue)]!, _1, _2) + } + public func Stats_MessageViews(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[146 * 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[147 * 6 + Int(form.rawValue)]!, _2, _1, _3) + } + public func MessageTimer_Minutes(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[148 * 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[149 * 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[150 * 6 + Int(form.rawValue)]!, stringValue) + } + public func OldChannels_InactiveMonth(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[151 * 6 + Int(form.rawValue)]!, stringValue) + } + public func ChatList_MessageMusic(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[152 * 6 + Int(form.rawValue)]!, stringValue) + } + public func OldChannels_Leave(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[153 * 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[154 * 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[155 * 6 + Int(form.rawValue)]!, _2, _1, _3) + } + public func MessageTimer_ShortSeconds(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[156 * 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[157 * 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[158 * 6 + Int(form.rawValue)]!, stringValue) + } + public func ChatList_MessageFiles(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[159 * 6 + Int(form.rawValue)]!, stringValue) + } + public func ServiceMessage_GameScoreSelfExtended(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[160 * 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[161 * 6 + Int(form.rawValue)]!, stringValue) + } + public func StickerPacks_DeleteStickerPacksConfirmation(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[162 * 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[163 * 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[164 * 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[165 * 6 + Int(form.rawValue)]!, _2, _1, _3) + } + public func ScheduledIn_Months(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[166 * 6 + Int(form.rawValue)]!, stringValue) + } + public func StickerPack_RemoveStickerCount(_ value: Int32) -> String { let form = getPluralizationForm(self.lc, value) let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) return String(format: self._ps[167 * 6 + Int(form.rawValue)]!, stringValue) } - public func PUSH_CHANNEL_MESSAGE_ROUNDS(_ selector: Int32, _ _1: String, _ _2: 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[168 * 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[169 * 6 + Int(form.rawValue)]!, stringValue) + } + public func Chat_MessagesUnpinned(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[170 * 6 + Int(form.rawValue)]!, stringValue) + } + public func LastSeen_HoursAgo(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[171 * 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[172 * 6 + Int(form.rawValue)]!, stringValue) + } + public func Media_ShareItem(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[173 * 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[174 * 6 + Int(form.rawValue)]!, stringValue) + } + public func Stats_GroupTopAdminKicks(_ value: Int32) -> String { + let form = getPluralizationForm(self.lc, value) + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[175 * 6 + Int(form.rawValue)]!, stringValue) + } + public func PUSH_CHANNEL_MESSAGE_DOCS(_ selector: Int32, _ _1: String, _ _2: Int32) -> String { let form = getPluralizationForm(self.lc, selector) - return String(format: self._ps[168 * 6 + Int(form.rawValue)]!, _1, _2) + return String(format: self._ps[176 * 6 + Int(form.rawValue)]!, _1, _2) } public init(primaryComponent: PresentationStringsComponent, secondaryComponent: PresentationStringsComponent?, groupingSeparator: String) { diff --git a/submodules/TelegramPresentationData/Sources/PresentationTheme.swift b/submodules/TelegramPresentationData/Sources/PresentationTheme.swift index 6a170942b0..c80310bd04 100644 --- a/submodules/TelegramPresentationData/Sources/PresentationTheme.swift +++ b/submodules/TelegramPresentationData/Sources/PresentationTheme.swift @@ -117,7 +117,8 @@ public final class PresentationThemeRootNavigationBar { public let secondaryTextColor: UIColor public let controlColor: UIColor public let accentTextColor: UIColor - public let backgroundColor: UIColor + public let blurredBackgroundColor: UIColor + public let opaqueBackgroundColor: UIColor public let separatorColor: UIColor public let badgeBackgroundColor: UIColor public let badgeStrokeColor: UIColor @@ -129,14 +130,15 @@ public final class PresentationThemeRootNavigationBar { public let clearButtonBackgroundColor: UIColor public let clearButtonForegroundColor: UIColor - public init(buttonColor: UIColor, disabledButtonColor: UIColor, primaryTextColor: UIColor, secondaryTextColor: UIColor, controlColor: UIColor, accentTextColor: UIColor, backgroundColor: UIColor, separatorColor: UIColor, badgeBackgroundColor: UIColor, badgeStrokeColor: UIColor, badgeTextColor: UIColor, segmentedBackgroundColor: UIColor, segmentedForegroundColor: UIColor, segmentedTextColor: UIColor, segmentedDividerColor: UIColor, clearButtonBackgroundColor: UIColor, clearButtonForegroundColor: UIColor) { + public init(buttonColor: UIColor, disabledButtonColor: UIColor, primaryTextColor: UIColor, secondaryTextColor: UIColor, controlColor: UIColor, accentTextColor: UIColor, blurredBackgroundColor: UIColor, opaqueBackgroundColor: UIColor, separatorColor: UIColor, badgeBackgroundColor: UIColor, badgeStrokeColor: UIColor, badgeTextColor: UIColor, segmentedBackgroundColor: UIColor, segmentedForegroundColor: UIColor, segmentedTextColor: UIColor, segmentedDividerColor: UIColor, clearButtonBackgroundColor: UIColor, clearButtonForegroundColor: UIColor) { self.buttonColor = buttonColor self.disabledButtonColor = disabledButtonColor self.primaryTextColor = primaryTextColor self.secondaryTextColor = secondaryTextColor self.controlColor = controlColor self.accentTextColor = accentTextColor - self.backgroundColor = backgroundColor + self.blurredBackgroundColor = blurredBackgroundColor + self.opaqueBackgroundColor = opaqueBackgroundColor self.separatorColor = separatorColor self.badgeBackgroundColor = badgeBackgroundColor self.badgeStrokeColor = badgeStrokeColor @@ -149,10 +151,10 @@ public final class PresentationThemeRootNavigationBar { self.clearButtonForegroundColor = clearButtonForegroundColor } - 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, clearButtonBackgroundColor: UIColor? = nil, clearButtonForegroundColor: UIColor? = nil) -> PresentationThemeRootNavigationBar { + public func withUpdated(buttonColor: UIColor? = nil, disabledButtonColor: UIColor? = nil, primaryTextColor: UIColor? = nil, secondaryTextColor: UIColor? = nil, controlColor: UIColor? = nil, accentTextColor: UIColor? = nil, blurredBackgroundColor: UIColor? = nil, opaqueBackgroundColor: 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, clearButtonBackgroundColor: UIColor? = nil, clearButtonForegroundColor: UIColor? = nil) -> PresentationThemeRootNavigationBar { let resolvedClearButtonBackgroundColor = clearButtonBackgroundColor ?? self.clearButtonBackgroundColor let resolvedClearButtonForegroundColor = clearButtonForegroundColor ?? self.clearButtonForegroundColor - 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, clearButtonBackgroundColor: resolvedClearButtonBackgroundColor, clearButtonForegroundColor: resolvedClearButtonForegroundColor) + 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, blurredBackgroundColor: blurredBackgroundColor ?? self.blurredBackgroundColor, opaqueBackgroundColor: opaqueBackgroundColor ?? self.opaqueBackgroundColor, 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, clearButtonBackgroundColor: resolvedClearButtonBackgroundColor, clearButtonForegroundColor: resolvedClearButtonForegroundColor) } } @@ -406,6 +408,25 @@ public final class PresentationInputFieldTheme { } public final class PresentationThemeList { + public final class PaymentOption { + public let inactiveFillColor: UIColor + public let inactiveForegroundColor: UIColor + public let activeFillColor: UIColor + public let activeForegroundColor: UIColor + + public init( + inactiveFillColor: UIColor, + inactiveForegroundColor: UIColor, + activeFillColor: UIColor, + activeForegroundColor: UIColor + ) { + self.inactiveFillColor = inactiveFillColor + self.inactiveForegroundColor = inactiveForegroundColor + self.activeFillColor = activeFillColor + self.activeForegroundColor = activeForegroundColor + } + } + public let blocksBackgroundColor: UIColor public let plainBackgroundColor: UIColor public let itemPrimaryTextColor: UIColor @@ -437,8 +458,42 @@ public final class PresentationThemeList { public let inputClearButtonColor: UIColor public let itemBarChart: PresentationThemeItemBarChart public let itemInputField: PresentationInputFieldTheme + public let paymentOption: PaymentOption - 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, itemInputField: PresentationInputFieldTheme) { + 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, + itemInputField: PresentationInputFieldTheme, + paymentOption: PaymentOption + ) { self.blocksBackgroundColor = blocksBackgroundColor self.plainBackgroundColor = plainBackgroundColor self.itemPrimaryTextColor = itemPrimaryTextColor @@ -470,10 +525,11 @@ public final class PresentationThemeList { self.inputClearButtonColor = inputClearButtonColor self.itemBarChart = itemBarChart self.itemInputField = itemInputField + self.paymentOption = paymentOption } - 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, itemInputField: PresentationInputFieldTheme? = 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, itemInputField: itemInputField ?? self.itemInputField) + 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, itemInputField: PresentationInputFieldTheme? = nil, paymentOption: PaymentOption? = 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, itemInputField: itemInputField ?? self.itemInputField, paymentOption: paymentOption ?? self.paymentOption) } } diff --git a/submodules/TelegramPresentationData/Sources/PresentationThemeCodable.swift b/submodules/TelegramPresentationData/Sources/PresentationThemeCodable.swift index 850e04a645..6cee46cdab 100644 --- a/submodules/TelegramPresentationData/Sources/PresentationThemeCodable.swift +++ b/submodules/TelegramPresentationData/Sources/PresentationThemeCodable.swift @@ -65,11 +65,10 @@ extension TelegramWallpaper: Codable { } } - self = .gradient(topColor.argb, bottomColor.argb, WallpaperSettings(blur: blur, motion: motion, rotation: rotation)) + self = .gradient(nil, [topColor.argb, bottomColor.argb], WallpaperSettings(blur: blur, motion: motion, rotation: rotation)) } else { var slug: String? - var color: UInt32? - var bottomColor: UInt32? + var colors: [UInt32] = [] var intensity: Int32? var rotation: Int32? @@ -83,11 +82,7 @@ extension TelegramWallpaper: Codable { 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 - } + colors.append(value.rgb) } else if component.count <= 3, let value = Int32(component) { if intensity == nil { if value >= 0 && value <= 100 { @@ -104,7 +99,7 @@ extension TelegramWallpaper: Codable { } } 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: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "", size: nil, attributes: []), settings: WallpaperSettings(blur: blur, motion: motion, color: color, bottomColor: bottomColor, intensity: intensity, rotation: rotation)) + self = .file(id: 0, accessHash: 0, isCreator: false, isDefault: false, isPattern: !colors.isEmpty, isDark: false, slug: slug, file: TelegramMediaFile(fileId: MediaId(namespace: 0, id: 0), partialReference: nil, resource: WallpaperDataResource(slug: slug), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "", size: nil, attributes: []), settings: WallpaperSettings(blur: blur, motion: motion, colors: colors, intensity: intensity, rotation: rotation)) } else { throw PresentationThemeDecodingError.generic } @@ -123,10 +118,11 @@ extension TelegramWallpaper: Codable { try container.encode("builtin") case let .color(color): try container.encode(String(format: "%06x", color)) - case let .gradient(topColor, bottomColor, settings): + case let .gradient(_, colors, settings): var components: [String] = [] - components.append(String(format: "%06x", topColor)) - components.append(String(format: "%06x", bottomColor)) + for color in colors { + components.append(String(format: "%06x", color)) + } if let rotation = settings.rotation { components.append("\(rotation)") } @@ -141,14 +137,20 @@ extension TelegramWallpaper: Codable { var components: [String] = [] components.append(file.slug) if self.isPattern { - if let color = file.settings.color { - components.append(String(format: "%06x", color)) + if file.settings.colors.count >= 1 { + components.append(String(format: "%06x", file.settings.colors[0])) } if let intensity = file.settings.intensity { components.append("\(intensity)") } - if let bottomColor = file.settings.bottomColor { - components.append(String(format: "%06x", bottomColor)) + if file.settings.colors.count >= 2 { + components.append(String(format: "%06x", file.settings.colors[1])) + } + if file.settings.colors.count >= 3 { + components.append(String(format: "%06x", file.settings.colors[2])) + } + if file.settings.colors.count >= 4 { + components.append(String(format: "%06x", file.settings.colors[3])) } if let rotation = file.settings.rotation, rotation != 0 { components.append("\(rotation)") @@ -407,10 +409,19 @@ extension PresentationThemeRootNavigationBar: Codable { case segmentedDivider case clearButtonBackground case clearButtonForeground + case opaqueBackground } public convenience init(from decoder: Decoder) throws { let values = try decoder.container(keyedBy: CodingKeys.self) + let blurredBackgroundColor = try decodeColor(values, .background) + + let opaqueBackgroundColor: UIColor + if blurredBackgroundColor.alpha >= 0.99 { + opaqueBackgroundColor = blurredBackgroundColor + } else { + opaqueBackgroundColor = (try? decodeColor(values, .opaqueBackground)) ?? blurredBackgroundColor + } self.init( buttonColor: try decodeColor(values, .button), @@ -419,7 +430,8 @@ extension PresentationThemeRootNavigationBar: Codable { secondaryTextColor: try decodeColor(values, .secondaryText), controlColor: try decodeColor(values, .control), accentTextColor: try decodeColor(values, .accentText), - backgroundColor: try decodeColor(values, .background), + blurredBackgroundColor: blurredBackgroundColor, + opaqueBackgroundColor: opaqueBackgroundColor, separatorColor: try decodeColor(values, .separator), badgeBackgroundColor: try decodeColor(values, .badgeFill), badgeStrokeColor: try decodeColor(values, .badgeStroke), @@ -441,7 +453,8 @@ extension PresentationThemeRootNavigationBar: Codable { try encodeColor(&values, self.secondaryTextColor, .secondaryText) try encodeColor(&values, self.controlColor, .control) try encodeColor(&values, self.accentTextColor, .accentText) - try encodeColor(&values, self.backgroundColor, .background) + try encodeColor(&values, self.blurredBackgroundColor, .background) + try encodeColor(&values, self.opaqueBackgroundColor, .opaqueBackground) try encodeColor(&values, self.separatorColor, .separator) try encodeColor(&values, self.badgeBackgroundColor, .badgeFill) try encodeColor(&values, self.badgeStrokeColor, .badgeStroke) @@ -745,6 +758,33 @@ extension PresentationInputFieldTheme: Codable { } } +extension PresentationThemeList.PaymentOption: Codable { + enum CodingKeys: String, CodingKey { + case inactiveFill + case inactiveForeground + case activeFill + case activeForeground + } + + public convenience init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + self.init( + inactiveFillColor: try decodeColor(values, .inactiveFill), + inactiveForegroundColor: try decodeColor(values, .inactiveForeground), + activeFillColor: try decodeColor(values, .activeFill), + activeForegroundColor: try decodeColor(values, .activeForeground) + ) + } + + public func encode(to encoder: Encoder) throws { + var values = encoder.container(keyedBy: CodingKeys.self) + try encodeColor(&values, self.activeFillColor, .inactiveFill) + try encodeColor(&values, self.activeForegroundColor, .inactiveForeground) + try encodeColor(&values, self.activeFillColor, .activeFill) + try encodeColor(&values, self.activeForegroundColor, .activeForeground) + } +} + extension PresentationThemeList: Codable { enum CodingKeys: String, CodingKey { case blocksBg @@ -778,6 +818,7 @@ extension PresentationThemeList: Codable { case inputClearButton case itemBarChart case itemInputField + case paymentOption } public convenience init(from decoder: Decoder) throws { @@ -789,6 +830,8 @@ extension PresentationThemeList: Codable { } else { freePlainInputField = try values.decode(PresentationInputFieldTheme.self, forKey: .freeInputField) } + + let freeTextSuccessColor = try decodeColor(values, .freeTextSuccess) self.init( blocksBackgroundColor: try decodeColor(values, .blocksBg), @@ -808,7 +851,7 @@ extension PresentationThemeList: Codable { sectionHeaderTextColor: try decodeColor(values, .sectionHeaderText), freeTextColor: try decodeColor(values, .freeText), freeTextErrorColor: try decodeColor(values, .freeTextError), - freeTextSuccessColor: try decodeColor(values, .freeTextSuccess), + freeTextSuccessColor: freeTextSuccessColor, freeMonoIconColor: try decodeColor(values, .freeMonoIcon), itemSwitchColors: try values.decode(PresentationThemeSwitch.self, forKey: .switch), itemDisclosureActions: try values.decode(PresentationThemeItemDisclosureActions.self, forKey: .disclosureActions), @@ -821,7 +864,13 @@ extension PresentationThemeList: Codable { pageIndicatorInactiveColor: try decodeColor(values, .pageIndicatorInactive), inputClearButtonColor: try decodeColor(values, .inputClearButton), itemBarChart: try values.decode(PresentationThemeItemBarChart.self, forKey: .itemBarChart), - itemInputField: try values.decode(PresentationInputFieldTheme.self, forKey: .itemInputField) + itemInputField: try values.decode(PresentationInputFieldTheme.self, forKey: .itemInputField), + paymentOption: (try? values.decode(PresentationThemeList.PaymentOption.self, forKey: .paymentOption)) ?? PresentationThemeList.PaymentOption( + inactiveFillColor: freeTextSuccessColor.withMultipliedAlpha(0.3), + inactiveForegroundColor: freeTextSuccessColor, + activeFillColor: freeTextSuccessColor, + activeForegroundColor: UIColor(rgb: 0xffffff) + ) ) } @@ -1846,9 +1895,9 @@ extension PresentationTheme: Codable { if let decoder = decoder as? PresentationThemeDecoding { let serviceBackgroundColor = decoder.serviceBackgroundColor ?? defaultServiceBackgroundColor decoder.referenceTheme = makeDefaultPresentationTheme(reference: referenceTheme, serviceBackgroundColor: serviceBackgroundColor) - index = decoder.reference?.index ?? arc4random64() + index = decoder.reference?.index ?? Int64.random(in: Int64.min ... Int64.max) } else { - index = arc4random64() + index = Int64.random(in: Int64.min ... Int64.max) } self.init(name: (try? values.decode(PresentationThemeName.self, forKey: .name)) ?? .custom("Untitled"), diff --git a/submodules/TelegramPresentationData/Sources/PresentationThemeEssentialGraphics.swift b/submodules/TelegramPresentationData/Sources/PresentationThemeEssentialGraphics.swift index 66b3464aab..b898ce12f1 100644 --- a/submodules/TelegramPresentationData/Sources/PresentationThemeEssentialGraphics.swift +++ b/submodules/TelegramPresentationData/Sources/PresentationThemeEssentialGraphics.swift @@ -192,55 +192,50 @@ public final class PrincipalThemeEssentialGraphics { public let hasWallpaper: Bool - init(mediaBox: MediaBox, presentationTheme: PresentationTheme, wallpaper initialWallpaper: TelegramWallpaper, preview: Bool = false, knockoutMode: Bool, bubbleCorners: PresentationChatBubbleCorners) { + init(presentationTheme: PresentationTheme, wallpaper initialWallpaper: TelegramWallpaper, preview: Bool = false, bubbleCorners: PresentationChatBubbleCorners) { let theme = presentationTheme.chat - var wallpaper = initialWallpaper + let wallpaper = initialWallpaper self.hasWallpaper = !wallpaper.isEmpty 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 + + 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 + context.clear(CGRect(origin: CGPoint(), size: size)) + 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 { - 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 - - 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 - } + 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 + context.clear(CGRect(origin: CGPoint(), size: size)) + 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 } let incomingKnockout = self.incomingBubbleGradientImage != nil @@ -253,7 +248,7 @@ public final class PrincipalThemeEssentialGraphics { let emptyImage = UIImage() if preview { - 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.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.chatMessageBackgroundIncomingExtractedMaskImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: true, fillColor: UIColor.black, strokeColor: UIColor.clear, neighbors: .extracted, 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.chatMessageBackgroundIncomingExtractedImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: true, fillColor: incoming.fill, strokeColor: incoming.stroke, neighbors: .extracted, theme: theme, wallpaper: wallpaper, knockout: incomingKnockout, extendedEdges: true) @@ -270,30 +265,33 @@ public final class PrincipalThemeEssentialGraphics { 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.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 = emptyImage - self.chatMessageBackgroundIncomingMergedTopSideMaskImage = emptyImage - self.chatMessageBackgroundIncomingMergedTopSideImage = emptyImage - self.chatMessageBackgroundIncomingMergedTopSideOutlineImage = emptyImage - self.chatMessageBackgroundIncomingMergedTopSideShadowImage = emptyImage + 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 = emptyImage - self.chatMessageBackgroundIncomingMergedBottomMaskImage = emptyImage + 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 = emptyImage - self.chatMessageBackgroundIncomingMergedBothMaskImage = emptyImage - self.chatMessageBackgroundIncomingMergedBothImage = emptyImage - self.chatMessageBackgroundIncomingMergedBothOutlineImage = emptyImage - self.chatMessageBackgroundIncomingMergedBothShadowImage = emptyImage + 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 = emptyImage - self.chatMessageBackgroundIncomingMergedSideMaskImage = emptyImage - self.chatMessageBackgroundIncomingMergedSideImage = emptyImage - self.chatMessageBackgroundIncomingMergedSideOutlineImage = emptyImage - self.chatMessageBackgroundIncomingMergedSideShadowImage = emptyImage + 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.chatMessageBackgroundIncomingMergedSideHighlightedImage = emptyImage self.chatMessageBackgroundOutgoingHighlightedImage = emptyImage 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) @@ -301,10 +299,10 @@ public final class PrincipalThemeEssentialGraphics { 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.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 = emptyImage 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) @@ -316,10 +314,9 @@ public final class PrincipalThemeEssentialGraphics { 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.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.chatMessageBackgroundOutgoingMergedSideHighlightedImage = emptyImage self.checkMediaFullImage = emptyImage self.checkMediaPartialImage = emptyImage @@ -505,12 +502,16 @@ public final class PrincipalThemeAdditionalGraphics { public let chatEmptyItemBackgroundImage: UIImage public let chatLoadingIndicatorBackgroundImage: UIImage - public let chatBubbleShareButtonImage: UIImage + public let chatBubbleNavigateButtonImage: UIImage public let chatBubbleActionButtonIncomingMiddleImage: UIImage + public let chatBubbleActionButtonMiddleMaskImage: UIImage public let chatBubbleActionButtonIncomingBottomLeftImage: UIImage + public let chatBubbleActionButtonBottomLeftMaskImage: UIImage public let chatBubbleActionButtonIncomingBottomRightImage: UIImage + public let chatBubbleActionButtonBottomRightMaskImage: UIImage public let chatBubbleActionButtonIncomingBottomSingleImage: UIImage + public let chatBubbleActionButtonBottomSingleMaskImage: UIImage public let chatBubbleActionButtonOutgoingMiddleImage: UIImage public let chatBubbleActionButtonOutgoingBottomLeftImage: UIImage public let chatBubbleActionButtonOutgoingBottomRightImage: UIImage @@ -521,12 +522,14 @@ public final class PrincipalThemeAdditionalGraphics { public let chatBubbleActionButtonIncomingShareIconImage: UIImage public let chatBubbleActionButtonIncomingPhoneIconImage: UIImage public let chatBubbleActionButtonIncomingLocationIconImage: UIImage + public let chatBubbleActionButtonIncomingPaymentIconImage: UIImage public let chatBubbleActionButtonOutgoingMessageIconImage: UIImage public let chatBubbleActionButtonOutgoingLinkIconImage: UIImage public let chatBubbleActionButtonOutgoingShareIconImage: UIImage public let chatBubbleActionButtonOutgoingPhoneIconImage: UIImage public let chatBubbleActionButtonOutgoingLocationIconImage: UIImage + public let chatBubbleActionButtonOutgoingPaymentIconImage: UIImage public let chatEmptyItemLockIcon: UIImage public let emptyChatListCheckIcon: UIImage @@ -550,12 +553,15 @@ public final class PrincipalThemeAdditionalGraphics { self.chatEmptyItemBackgroundImage = generateStretchableFilledCircleImage(radius: 14.0, color: serviceColor.fill)! self.chatLoadingIndicatorBackgroundImage = generateStretchableFilledCircleImage(diameter: 30.0, color: serviceColor.fill)! - 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, bubbleCorners: bubbleCorners) + self.chatBubbleActionButtonMiddleMaskImage = messageBubbleActionButtonImage(color: .white, strokeColor: .clear, 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.chatBubbleActionButtonBottomLeftMaskImage = messageBubbleActionButtonImage(color: .black, strokeColor: .clear, 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.chatBubbleActionButtonBottomRightMaskImage = messageBubbleActionButtonImage(color: .black, strokeColor: .clear, 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.chatBubbleActionButtonBottomSingleMaskImage = messageBubbleActionButtonImage(color: .black, strokeColor: .clear, 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) @@ -565,11 +571,13 @@ public final class PrincipalThemeAdditionalGraphics { self.chatBubbleActionButtonIncomingShareIconImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/BotShare"), color: bubbleVariableColor(variableColor: theme.message.incoming.actionButtonsTextColor, wallpaper: wallpaper))! self.chatBubbleActionButtonIncomingPhoneIconImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/BotPhone"), color: bubbleVariableColor(variableColor: theme.message.incoming.actionButtonsTextColor, wallpaper: wallpaper))! self.chatBubbleActionButtonIncomingLocationIconImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/BotLocation"), color: bubbleVariableColor(variableColor: theme.message.incoming.actionButtonsTextColor, wallpaper: wallpaper))! + self.chatBubbleActionButtonIncomingPaymentIconImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/BotPayment"), color: bubbleVariableColor(variableColor: theme.message.incoming.actionButtonsTextColor, wallpaper: wallpaper))! self.chatBubbleActionButtonOutgoingMessageIconImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/BotMessage"), color: bubbleVariableColor(variableColor: theme.message.outgoing.actionButtonsTextColor, wallpaper: wallpaper))! self.chatBubbleActionButtonOutgoingLinkIconImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/BotLink"), color: bubbleVariableColor(variableColor: theme.message.outgoing.actionButtonsTextColor, wallpaper: wallpaper))! self.chatBubbleActionButtonOutgoingShareIconImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/BotShare"), color: bubbleVariableColor(variableColor: theme.message.outgoing.actionButtonsTextColor, wallpaper: wallpaper))! self.chatBubbleActionButtonOutgoingPhoneIconImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/BotPhone"), color: bubbleVariableColor(variableColor: theme.message.outgoing.actionButtonsTextColor, wallpaper: wallpaper))! self.chatBubbleActionButtonOutgoingLocationIconImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/BotLocation"), color: bubbleVariableColor(variableColor: theme.message.outgoing.actionButtonsTextColor, wallpaper: wallpaper))! + self.chatBubbleActionButtonOutgoingPaymentIconImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/BotPayment"), color: bubbleVariableColor(variableColor: theme.message.outgoing.actionButtonsTextColor, wallpaper: wallpaper))! self.chatEmptyItemLockIcon = generateImage(CGSize(width: 9.0, height: 13.0), rotatedContext: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) diff --git a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift index b9a7ae0f89..c065b4ce41 100644 --- a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift +++ b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift @@ -239,9 +239,10 @@ public enum PresentationResourceKey: Int32 { case groupInfoMembersIcon case emptyChatListCheckIcon - - case chatFreeCommentButtonBackground + case chatFreeCommentButtonIcon + case chatFreeNavigateButtonIcon + case chatFreeShareButtonIcon } public enum PresentationResourceParameterKey: Hashable { diff --git a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChat.swift b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChat.swift index 30634afe59..6de22efbc3 100644 --- a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChat.swift +++ b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChat.swift @@ -86,10 +86,10 @@ public struct PresentationResourcesChat { }) } - public static func principalGraphics(mediaBox: MediaBox, knockoutWallpaper: Bool, theme: PresentationTheme, wallpaper: TelegramWallpaper, bubbleCorners: PresentationChatBubbleCorners) -> PrincipalThemeEssentialGraphics { + public static func principalGraphics(theme: PresentationTheme, wallpaper: TelegramWallpaper, bubbleCorners: PresentationChatBubbleCorners) -> PrincipalThemeEssentialGraphics { let hasWallpaper = !wallpaper.isEmpty 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) + return PrincipalThemeEssentialGraphics(presentationTheme: theme, wallpaper: wallpaper, preview: theme.preview, bubbleCorners: bubbleCorners) }) as! PrincipalThemeEssentialGraphics } @@ -532,7 +532,14 @@ public struct PresentationResourcesChat { public static func chatInputTextFieldTimerImage(_ theme: PresentationTheme) -> UIImage? { return theme.image(PresentationResourceKey.chatInputTextFieldTimerImage.rawValue, { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Text/AccessoryIconTimer"), color: theme.chat.inputPanel.inputControlColor) + if let image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Text/AccessoryIconTimer"), color: theme.chat.inputPanel.inputControlColor) { + return generateImage(CGSize(width: image.size.width, height: image.size.height + 1.0), contextGenerator: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.draw(image.cgImage!, in: CGRect(origin: CGPoint(x: 0.0, y: 2.0), size: image.size)) + }) + } else { + return nil + } }) } @@ -546,8 +553,6 @@ public struct PresentationResourcesChat { return theme.image(PresentationResourceKey.chatHistoryNavigationButtonImage.rawValue, { theme in return generateImage(CGSize(width: 38.0, height: 38.0), contextGenerator: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) - context.setFillColor(theme.chat.historyNavigation.fillColor.cgColor) - context.fillEllipse(in: CGRect(origin: CGPoint(x: 0.5, y: 0.5), size: CGSize(width: size.width - 1.0, height: size.height - 1.0))) context.setLineWidth(0.5) context.setStrokeColor(theme.chat.historyNavigation.strokeColor.cgColor) context.strokeEllipse(in: CGRect(origin: CGPoint(x: 0.25, y: 0.25), size: CGSize(width: size.width - 0.5, height: size.height - 0.5))) @@ -658,7 +663,7 @@ public struct PresentationResourcesChat { return theme.image(PresentationResourceKey.chatInstantMessageInfoBackgroundImage.rawValue, { theme in return generateImage(CGSize(width: 24.0, height: 24.0), rotatedContext: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) - context.setFillColor(theme.chat.message.mediaDateAndStatusFillColor.cgColor) + context.setFillColor(theme.chat.message.mediaDateAndStatusFillColor.withAlphaComponent(0.3).cgColor) context.fillEllipse(in: CGRect(origin: CGPoint(), size: size)) })?.stretchableImage(withLeftCapWidth: 12, topCapHeight: 12) }) @@ -1088,16 +1093,21 @@ public struct PresentationResourcesChat { }) } - public static func chatFreeCommentButtonBackground(_ theme: PresentationTheme, wallpaper: TelegramWallpaper) -> UIImage? { - return theme.image(PresentationResourceKey.chatFreeCommentButtonBackground.rawValue, { _ in - let strokeColor = bubbleVariableColor(variableColor: theme.chat.message.shareButtonStrokeColor, wallpaper: wallpaper) - return generateStretchableFilledCircleImage(diameter: 30.0, color: bubbleVariableColor(variableColor: theme.chat.message.shareButtonFillColor, wallpaper: wallpaper), strokeColor: strokeColor, strokeWidth: strokeColor.alpha.isZero ? nil : 1.0) - }) - } - public static func chatFreeCommentButtonIcon(_ theme: PresentationTheme, wallpaper: TelegramWallpaper) -> UIImage? { return theme.image(PresentationResourceKey.chatFreeCommentButtonIcon.rawValue, { _ in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/FreeRepliesIcon"), color: bubbleVariableColor(variableColor: theme.chat.message.shareButtonForegroundColor, wallpaper: wallpaper)) }) } + + public static func chatFreeNavigateButtonIcon(_ theme: PresentationTheme, wallpaper: TelegramWallpaper) -> UIImage? { + return theme.image(PresentationResourceKey.chatFreeNavigateButtonIcon.rawValue, { _ in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/NavigateToMessageIcon"), color: bubbleVariableColor(variableColor: theme.chat.message.shareButtonForegroundColor, wallpaper: wallpaper)) + }) + } + + public static func chatFreeShareButtonIcon(_ theme: PresentationTheme, wallpaper: TelegramWallpaper) -> UIImage? { + return theme.image(PresentationResourceKey.chatFreeShareButtonIcon.rawValue, { _ in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/ShareIcon"), color: bubbleVariableColor(variableColor: theme.chat.message.shareButtonForegroundColor, wallpaper: wallpaper)) + }) + } } diff --git a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesSettings.swift b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesSettings.swift index 3ae4d727d1..21d6342e2f 100644 --- a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesSettings.swift +++ b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesSettings.swift @@ -56,6 +56,7 @@ public struct PresentationResourcesSettings { public static let support = renderIcon(name: "Settings/MenuIcons/Support") public static let faq = renderIcon(name: "Settings/MenuIcons/Faq") + public static let tips = renderIcon(name: "Settings/MenuIcons/Tips") public static let addAccount = renderIcon(name: "Settings/MenuIcons/AddAccount") public static let setPasscode = renderIcon(name: "Settings/MenuIcons/SetPasscode") diff --git a/submodules/TelegramPresentationData/Sources/WallpaperUtils.swift b/submodules/TelegramPresentationData/Sources/WallpaperUtils.swift index 23bc0158dc..f3afab6527 100644 --- a/submodules/TelegramPresentationData/Sources/WallpaperUtils.swift +++ b/submodules/TelegramPresentationData/Sources/WallpaperUtils.swift @@ -8,8 +8,8 @@ public extension TelegramWallpaper { switch self { case .image: return false - case let .file(file): - if self.isPattern, file.settings.color == 0xffffff || file.settings.color == 0xffffffff { + case let .file(_, _, _, _, _, _, _, _, settings): + if self.isPattern, settings.colors.count == 1 && (settings.colors[0] == 0xffffff || settings.colors[0] == 0xffffffff) { return true } else { return false diff --git a/submodules/TelegramStringFormatting/Sources/CurrencyFormat.swift b/submodules/TelegramStringFormatting/Sources/CurrencyFormat.swift index c673cecd03..b4aab4c33d 100644 --- a/submodules/TelegramStringFormatting/Sources/CurrencyFormat.swift +++ b/submodules/TelegramStringFormatting/Sources/CurrencyFormat.swift @@ -46,6 +46,83 @@ private func loadCurrencyFormatterEntries() -> [String: CurrencyFormatterEntry] private let currencyFormatterEntries = loadCurrencyFormatterEntries() +public func setupCurrencyNumberFormatter(currency: String) -> NumberFormatter { + guard let entry = currencyFormatterEntries[currency] ?? currencyFormatterEntries["USD"] else { + preconditionFailure() + } + + var result = "" + if entry.symbolOnLeft { + result.append("¤") + if entry.spaceBetweenAmountAndSymbol { + result.append(" ") + } + } + + result.append("#") + + if entry.decimalDigits != 0 { + result.append(entry.decimalSeparator) + } + + for _ in 0 ..< entry.decimalDigits { + result.append("#") + } + if entry.decimalDigits != 0 { + result.append("0") + } + + if !entry.symbolOnLeft { + if entry.spaceBetweenAmountAndSymbol { + result.append(" ") + } + result.append("¤") + } + + let numberFormatter = NumberFormatter() + + numberFormatter.numberStyle = .currency + + numberFormatter.positiveFormat = result + numberFormatter.negativeFormat = "-\(result)" + + numberFormatter.currencySymbol = "" + numberFormatter.currencyDecimalSeparator = entry.decimalSeparator + numberFormatter.currencyGroupingSeparator = entry.thousandsSeparator + + numberFormatter.minimumFractionDigits = entry.decimalDigits + numberFormatter.maximumFractionDigits = entry.decimalDigits + numberFormatter.minimumIntegerDigits = 1 + + return numberFormatter +} + +public func fractionalToCurrencyAmount(value: Double, currency: String) -> Int64? { + guard let entry = currencyFormatterEntries[currency] ?? currencyFormatterEntries["USD"] else { + return nil + } + var factor: Double = 1.0 + for _ in 0 ..< entry.decimalDigits { + factor *= 10.0 + } + if value > Double(Int64.max) / factor { + return nil + } else { + return Int64(value * factor) + } +} + +public func currencyToFractionalAmount(value: Int64, currency: String) -> Double? { + guard let entry = currencyFormatterEntries[currency] ?? currencyFormatterEntries["USD"] else { + return nil + } + var factor: Double = 1.0 + for _ in 0 ..< entry.decimalDigits { + factor *= 10.0 + } + return Double(value) / factor +} + public func formatCurrencyAmount(_ amount: Int64, currency: String) -> String { if let entry = currencyFormatterEntries[currency] ?? currencyFormatterEntries["USD"] { var result = "" @@ -68,7 +145,9 @@ public func formatCurrencyAmount(_ amount: Int64, currency: String) -> String { } } result.append("\(integerPart)") - result.append(entry.decimalSeparator) + if !fractional.isEmpty { + result.append(entry.decimalSeparator) + } for i in 0 ..< fractional.count { result.append(fractional[fractional.count - i - 1]) } @@ -89,3 +168,61 @@ public func formatCurrencyAmount(_ amount: Int64, currency: String) -> String { return formatter.string(from: (Float(amount) * 0.01) as NSNumber) ?? "" } } + +public func formatCurrencyAmountCustom(_ amount: Int64, currency: String) -> (String, String, Bool) { + if let entry = currencyFormatterEntries[currency] ?? currencyFormatterEntries["USD"] { + var result = "" + if amount < 0 { + result.append("-") + } + /*if entry.symbolOnLeft { + result.append(entry.symbol) + if entry.spaceBetweenAmountAndSymbol { + result.append(" ") + } + }*/ + var integerPart = abs(amount) + var fractional: [Character] = [] + for _ in 0 ..< entry.decimalDigits { + let part = integerPart % 10 + integerPart /= 10 + if let scalar = UnicodeScalar(UInt32(part + 48)) { + fractional.append(Character(scalar)) + } + } + result.append("\(integerPart)") + if !fractional.isEmpty { + result.append(entry.decimalSeparator) + } + for i in 0 ..< fractional.count { + result.append(fractional[fractional.count - i - 1]) + } + /*if !entry.symbolOnLeft { + if entry.spaceBetweenAmountAndSymbol { + result.append(" ") + } + result.append(entry.symbol) + }*/ + + return (result, entry.symbol, entry.symbolOnLeft) + } else { + return ("", "", false) + } +} + +public struct CurrencyFormat { + public var symbol: String + public var symbolOnLeft: Bool + public var decimalSeparator: String + public var decimalDigits: Int + + public init?(currency: String) { + guard let entry = currencyFormatterEntries[currency] else { + return nil + } + self.symbol = entry.symbol + self.symbolOnLeft = entry.symbolOnLeft + self.decimalSeparator = entry.decimalSeparator + self.decimalDigits = entry.decimalDigits + } +} diff --git a/submodules/TelegramStringFormatting/Sources/DataSizeFormat.swift b/submodules/TelegramStringFormatting/Sources/DataSizeFormat.swift new file mode 100644 index 0000000000..576b4baf43 --- /dev/null +++ b/submodules/TelegramStringFormatting/Sources/DataSizeFormat.swift @@ -0,0 +1,17 @@ +import Foundation +import TelegramCore +import TelegramPresentationData + +public extension DataSizeStringFormatting { + init(presentationData: PresentationData) { + self.init(decimalSeparator: presentationData.dateTimeFormat.decimalSeparator, byte: presentationData.strings.FileSize_B(_:), kilobyte: presentationData.strings.FileSize_KB(_:), megabyte: presentationData.strings.FileSize_MB(_:), gigabyte: presentationData.strings.FileSize_GB(_:)) + } + + init (chatPresentationData: ChatPresentationData) { + self.init(decimalSeparator: chatPresentationData.dateTimeFormat.decimalSeparator, byte: chatPresentationData.strings.FileSize_B(_:), kilobyte: chatPresentationData.strings.FileSize_KB(_:), megabyte: chatPresentationData.strings.FileSize_MB(_:), gigabyte: chatPresentationData.strings.FileSize_GB(_:)) + } + + init (strings: PresentationStrings, decimalSeparator: String) { + self.init(decimalSeparator: decimalSeparator, byte: strings.FileSize_B(_:), kilobyte: strings.FileSize_KB(_:), megabyte: strings.FileSize_MB(_:), gigabyte: strings.FileSize_GB(_:)) + } +} diff --git a/submodules/TelegramStringFormatting/Sources/DateFormat.swift b/submodules/TelegramStringFormatting/Sources/DateFormat.swift index fedb29e4d1..ebf1f1c351 100644 --- a/submodules/TelegramStringFormatting/Sources/DateFormat.swift +++ b/submodules/TelegramStringFormatting/Sources/DateFormat.swift @@ -54,11 +54,12 @@ public func stringForMediumDate(timestamp: Int32, strings: PresentationStrings, let dateString: String let separator = dateTimeFormat.dateSeparator let suffix = dateTimeFormat.dateSuffix + let displayYear = dateTimeFormat.requiresFullYear ? year - 100 + 2000 : year - 100 switch dateTimeFormat.dateFormat { case .monthFirst: - dateString = String(format: "%02d%@%02d%@%02d%@", month, separator, day, separator, year - 100, suffix) + dateString = String(format: "%02d%@%02d%@%02d%@", month, separator, day, separator, displayYear, suffix) case .dayFirst: - dateString = String(format: "%02d%@%02d%@%02d%@", day, separator, month, separator, year - 100, suffix) + dateString = String(format: "%02d%@%02d%@%02d%@", day, separator, month, separator, displayYear, suffix) } let timeString = stringForShortTimestamp(hours: Int32(timeinfo.tm_hour), minutes: Int32(timeinfo.tm_min), dateTimeFormat: dateTimeFormat) diff --git a/submodules/TelegramStringFormatting/Sources/MessageContentKind.swift b/submodules/TelegramStringFormatting/Sources/MessageContentKind.swift index 6b0549106e..d2689b2a6b 100644 --- a/submodules/TelegramStringFormatting/Sources/MessageContentKind.swift +++ b/submodules/TelegramStringFormatting/Sources/MessageContentKind.swift @@ -24,6 +24,7 @@ public enum MessageContentKindKey { case poll case restricted case dice + case invoice } public enum MessageContentKind: Equatable { @@ -44,6 +45,7 @@ public enum MessageContentKind: Equatable { case poll(String) case restricted(String) case dice(String) + case invoice(String) public var key: MessageContentKindKey { switch self { @@ -81,11 +83,13 @@ public enum MessageContentKind: Equatable { return .restricted case .dice: return .dice + case .invoice: + return .invoice } } } -public func messageContentKind(contentSettings: ContentSettings, message: Message, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, accountPeerId: PeerId) -> MessageContentKind { +public func messageContentKind(contentSettings: ContentSettings, message: Message, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, dateTimeFormat: PresentationDateTimeFormat, accountPeerId: PeerId) -> MessageContentKind { for attribute in message.attributes { if let attribute = attribute as? RestrictedContentMessageAttribute { if let text = attribute.platformText(platform: "ios", contentSettings: contentSettings) { @@ -95,14 +99,14 @@ public func messageContentKind(contentSettings: ContentSettings, message: Messag } } for media in message.media { - if let kind = mediaContentKind(media, message: message, strings: strings, nameDisplayOrder: nameDisplayOrder, accountPeerId: accountPeerId) { + if let kind = mediaContentKind(media, message: message, strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: dateTimeFormat, accountPeerId: accountPeerId) { return kind } } return .text(message.text) } -public func mediaContentKind(_ media: Media, message: Message? = nil, strings: PresentationStrings? = nil, nameDisplayOrder: PresentationPersonNameOrder? = nil, accountPeerId: PeerId? = nil) -> MessageContentKind? { +public func mediaContentKind(_ media: Media, message: Message? = nil, strings: PresentationStrings? = nil, nameDisplayOrder: PresentationPersonNameOrder? = nil, dateTimeFormat: PresentationDateTimeFormat? = nil, accountPeerId: PeerId? = nil) -> MessageContentKind? { switch media { case let expiredMedia as TelegramMediaExpiredContent: switch expiredMedia.data { @@ -163,7 +167,7 @@ public func mediaContentKind(_ media: Media, message: Message? = nil, strings: P } case _ as TelegramMediaAction: if let message = message, let strings = strings, let nameDisplayOrder = nameDisplayOrder, let accountPeerId = accountPeerId { - return .text(plainServiceMessageString(strings: strings, nameDisplayOrder: nameDisplayOrder, message: message, accountPeerId: accountPeerId, forChatList: false) ?? "") + return .text(plainServiceMessageString(strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: dateTimeFormat ?? PresentationDateTimeFormat(timeFormat: .military, dateFormat: .dayFirst, dateSeparator: ".", dateSuffix: "", requiresFullYear: false, decimalSeparator: ".", groupingSeparator: ""), message: message, accountPeerId: accountPeerId, forChatList: false) ?? "") } else { return nil } @@ -171,6 +175,8 @@ public func mediaContentKind(_ media: Media, message: Message? = nil, strings: P return .poll(poll.text) case let dice as TelegramMediaDice: return .dice(dice.emoji) + case let invoice as TelegramMediaInvoice: + return .invoice(invoice.title) default: return nil } @@ -220,11 +226,13 @@ public func stringForMediaKind(_ kind: MessageContentKind, strings: Presentation return (text, false) case let .dice(emoji): return (emoji, true) + case let .invoice(text): + return (text, true) } } -public func descriptionStringForMessage(contentSettings: ContentSettings, message: Message, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, accountPeerId: PeerId) -> (String, Bool) { - let contentKind = messageContentKind(contentSettings: contentSettings, message: message, strings: strings, nameDisplayOrder: nameDisplayOrder, accountPeerId: accountPeerId) +public func descriptionStringForMessage(contentSettings: ContentSettings, message: Message, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, dateTimeFormat: PresentationDateTimeFormat, accountPeerId: PeerId) -> (String, Bool) { + let contentKind = messageContentKind(contentSettings: contentSettings, message: message, strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: dateTimeFormat, accountPeerId: accountPeerId) if !message.text.isEmpty && ![.expiredImage, .expiredVideo].contains(contentKind.key) { return (foldLineBreaks(message.text), false) } diff --git a/submodules/TelegramStringFormatting/Sources/PresenceStrings.swift b/submodules/TelegramStringFormatting/Sources/PresenceStrings.swift index 5a30f40144..6847f5cd6d 100644 --- a/submodules/TelegramStringFormatting/Sources/PresenceStrings.swift +++ b/submodules/TelegramStringFormatting/Sources/PresenceStrings.swift @@ -7,11 +7,12 @@ import TelegramPresentationData public func stringForTimestamp(day: Int32, month: Int32, year: Int32, dateTimeFormat: PresentationDateTimeFormat) -> String { let separator = dateTimeFormat.dateSeparator let suffix = dateTimeFormat.dateSuffix + let displayYear = dateTimeFormat.requiresFullYear ? year - 100 + 2000 : year - 100 switch dateTimeFormat.dateFormat { case .monthFirst: - return String(format: "%02d%@%02d%@%02d%@", month, separator, day, separator, year - 100, suffix) + return String(format: "%02d%@%02d%@%02d%@", month, separator, day, separator, displayYear, suffix) case .dayFirst: - return String(format: "%02d%@%02d%@%02d%@", day, separator, month, separator, year - 100, suffix) + return String(format: "%02d%@%02d%@%02d%@", day, separator, month, separator, displayYear, suffix) } } @@ -125,21 +126,38 @@ public func stringForUserPresence(strings: PresentationStrings, day: RelativeTim return dayString } -private func humanReadableStringForTimestamp(strings: PresentationStrings, day: RelativeTimestampFormatDay, dateTimeFormat: PresentationDateTimeFormat, hours: Int32, minutes: Int32) -> String { - let dayString: String +private func humanReadableStringForTimestamp(strings: PresentationStrings, day: RelativeTimestampFormatDay, dateTimeFormat: PresentationDateTimeFormat, hours: Int32, minutes: Int32, format: HumanReadableStringFormat? = nil) -> (String, [(Int, NSRange)]) { + let result: (String, [(Int, NSRange)]) switch day { case .today: - dayString = strings.Time_TodayAt(stringForShortTimestamp(hours: hours, minutes: minutes, dateTimeFormat: dateTimeFormat)).0 + let string = stringForShortTimestamp(hours: hours, minutes: minutes, dateTimeFormat: dateTimeFormat) + result = format?.todayFormatString(string) ?? strings.Time_TodayAt(string) case .yesterday: - dayString = strings.Time_YesterdayAt(stringForShortTimestamp(hours: hours, minutes: minutes, dateTimeFormat: dateTimeFormat)).0 + let string = stringForShortTimestamp(hours: hours, minutes: minutes, dateTimeFormat: dateTimeFormat) + result = format?.yesterdayFormatString(string) ?? strings.Time_YesterdayAt(string) case .tomorrow: - dayString = strings.Time_TomorrowAt(stringForShortTimestamp(hours: hours, minutes: minutes, dateTimeFormat: dateTimeFormat)).0 + let string = stringForShortTimestamp(hours: hours, minutes: minutes, dateTimeFormat: dateTimeFormat) + result = format?.tomorrowFormatString(string) ?? strings.Time_TomorrowAt(string) } - return dayString + return result } -public func humanReadableStringForTimestamp(strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, timestamp: Int32) -> String { +public struct HumanReadableStringFormat { + let dateFormatString: (String) -> (String, [(Int, NSRange)]) + let tomorrowFormatString: (String) -> (String, [(Int, NSRange)]) + let todayFormatString: (String) -> (String, [(Int, NSRange)]) + let yesterdayFormatString: (String) -> (String, [(Int, NSRange)]) + + public init(dateFormatString: @escaping (String) -> (String, [(Int, NSRange)]), tomorrowFormatString: @escaping (String) -> (String, [(Int, NSRange)]), todayFormatString: @escaping (String) -> (String, [(Int, NSRange)]), yesterdayFormatString: @escaping (String) -> (String, [(Int, NSRange)]) = { ($0, []) }) { + self.dateFormatString = dateFormatString + self.tomorrowFormatString = tomorrowFormatString + self.todayFormatString = todayFormatString + self.yesterdayFormatString = yesterdayFormatString + } +} + +public func humanReadableStringForTimestamp(strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, timestamp: Int32, alwaysShowTime: Bool = false, allowYesterday: Bool = true, format: HumanReadableStringFormat? = nil) -> (String, [(Int, NSRange)]) { var t: time_t = time_t(timestamp) var timeinfo: tm = tm() localtime_r(&t, &timeinfo) @@ -150,11 +168,17 @@ public func humanReadableStringForTimestamp(strings: PresentationStrings, dateTi localtime_r(&now, &timeinfoNow) if timeinfo.tm_year != timeinfoNow.tm_year { - return "\(stringForTimestamp(day: timeinfo.tm_mday, month: timeinfo.tm_mon + 1, year: timeinfo.tm_year, dateTimeFormat: dateTimeFormat))" + let string: String + if alwaysShowTime { + string = stringForMediumDate(timestamp: timestamp, strings: strings, dateTimeFormat: dateTimeFormat) + } else { + string = stringForTimestamp(day: timeinfo.tm_mday, month: timeinfo.tm_mon + 1, year: timeinfo.tm_year, dateTimeFormat: dateTimeFormat) + } + return format?.dateFormatString(string) ?? (string, []) } let dayDifference = timeinfo.tm_yday - timeinfoNow.tm_yday - if dayDifference == 0 || dayDifference == -1 || dayDifference == 1 { + if dayDifference == 0 || (dayDifference == -1 && allowYesterday) || dayDifference == 1 { let day: RelativeTimestampFormatDay if dayDifference == 0 { day = .today @@ -163,9 +187,15 @@ public func humanReadableStringForTimestamp(strings: PresentationStrings, dateTi } else { day = .tomorrow } - return humanReadableStringForTimestamp(strings: strings, day: day, dateTimeFormat: dateTimeFormat, hours: timeinfo.tm_hour, minutes: timeinfo.tm_min) + return humanReadableStringForTimestamp(strings: strings, day: day, dateTimeFormat: dateTimeFormat, hours: timeinfo.tm_hour, minutes: timeinfo.tm_min, format: format) } else { - return "\(stringForTimestamp(day: timeinfo.tm_mday, month: timeinfo.tm_mon + 1, year: timeinfo.tm_year, dateTimeFormat: dateTimeFormat))" + let string: String + if alwaysShowTime { + string = stringForMediumDate(timestamp: timestamp, strings: strings, dateTimeFormat: dateTimeFormat) + } else { + string = stringForTimestamp(day: timeinfo.tm_mday, month: timeinfo.tm_mon + 1, year: timeinfo.tm_year, dateTimeFormat: dateTimeFormat) + } + return format?.dateFormatString(string) ?? (string, []) } } diff --git a/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift b/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift index 80778fe1ff..71fa764773 100644 --- a/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift +++ b/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift @@ -27,11 +27,11 @@ private func peerMentionsAttributes(primaryTextColor: UIColor, peerIds: [(Int, P return result } -public func plainServiceMessageString(strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, message: Message, accountPeerId: PeerId, forChatList: Bool) -> String? { - return universalServiceMessageString(presentationData: nil, strings: strings, nameDisplayOrder: nameDisplayOrder, message: message, accountPeerId: accountPeerId, forChatList: forChatList)?.string +public func plainServiceMessageString(strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, dateTimeFormat: PresentationDateTimeFormat, message: Message, accountPeerId: PeerId, forChatList: Bool) -> String? { + return universalServiceMessageString(presentationData: nil, strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: dateTimeFormat, message: message, accountPeerId: accountPeerId, forChatList: forChatList)?.string } -public func universalServiceMessageString(presentationData: (PresentationTheme, TelegramWallpaper)?, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, message: Message, accountPeerId: PeerId, forChatList: Bool) -> NSAttributedString? { +public func universalServiceMessageString(presentationData: (PresentationTheme, TelegramWallpaper)?, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, dateTimeFormat: PresentationDateTimeFormat, message: Message, accountPeerId: PeerId, forChatList: Bool) -> NSAttributedString? { var attributedString: NSAttributedString? let primaryTextColor: UIColor @@ -446,10 +446,25 @@ public func universalServiceMessageString(presentationData: (PresentationTheme, } } attributedString = NSAttributedString(string: titleString, font: titleFont, textColor: primaryTextColor) - case let .groupPhoneCall(_, _, duration): - if let duration = duration { - let titleString = strings.Notification_VoiceChatEnded(callDurationString(strings: strings, value: duration)).0 - attributedString = NSAttributedString(string: titleString, font: titleFont, textColor: primaryTextColor) + case let .groupPhoneCall(_, _, scheduleDate, duration): + if let scheduleDate = scheduleDate { + if message.author?.id.namespace == Namespaces.Peer.CloudChannel { + let titleString = humanReadableStringForTimestamp(strings: strings, dateTimeFormat: dateTimeFormat, timestamp: scheduleDate, alwaysShowTime: true, allowYesterday: false, format: HumanReadableStringFormat(dateFormatString: { strings.Notification_VoiceChatScheduledChannel($0) }, tomorrowFormatString: { strings.Notification_VoiceChatScheduledTomorrowChannel($0) }, todayFormatString: { strings.Notification_VoiceChatScheduledTodayChannel($0) })) + attributedString = NSAttributedString(string: titleString.0, font: titleFont, textColor: primaryTextColor) + } else { + let titleString = humanReadableStringForTimestamp(strings: strings, dateTimeFormat: dateTimeFormat, timestamp: scheduleDate, alwaysShowTime: true, allowYesterday: false, format: HumanReadableStringFormat(dateFormatString: { strings.Notification_VoiceChatScheduled(authorName, $0) }, tomorrowFormatString: { strings.Notification_VoiceChatScheduledTomorrow(authorName, $0) }, todayFormatString: { strings.Notification_VoiceChatScheduledToday(authorName, $0) })) + let attributePeerIds: [(Int, PeerId?)] = [(0, message.author?.id)] + attributedString = addAttributesToStringWithRanges(titleString, body: bodyAttributes, argumentAttributes: peerMentionsAttributes(primaryTextColor: primaryTextColor, peerIds: attributePeerIds)) + } + } else if let duration = duration { + if message.author?.id.namespace == Namespaces.Peer.CloudChannel { + let titleString = strings.Notification_VoiceChatEnded(callDurationString(strings: strings, value: duration)).0 + attributedString = NSAttributedString(string: titleString, font: titleFont, textColor: primaryTextColor) + } else { + let attributePeerIds: [(Int, PeerId?)] = [(0, message.author?.id)] + let titleString = strings.Notification_VoiceChatEndedGroup(authorName, callDurationString(strings: strings, value: duration)) + attributedString = addAttributesToStringWithRanges(titleString, body: bodyAttributes, argumentAttributes: peerMentionsAttributes(primaryTextColor: primaryTextColor, peerIds: attributePeerIds)) + } } else { if message.author?.id.namespace == Namespaces.Peer.CloudChannel { let titleString = strings.Notification_VoiceChatStartedChannel diff --git a/submodules/TelegramUI/BUILD b/submodules/TelegramUI/BUILD index cb39ede213..e3bd8fe635 100644 --- a/submodules/TelegramUI/BUILD +++ b/submodules/TelegramUI/BUILD @@ -1,5 +1,11 @@ load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") +load( + "@build_configuration//:variables.bzl", + "telegram_bundle_id", +) + + filegroup( name = "TelegramUIResources", srcs = glob([ @@ -15,6 +21,17 @@ filegroup( visibility = ["//visibility:public"], ) +internal_bundle_ids = [ + "org.telegram.Telegram-iOS", +] + +available_appcenter_targets = [ + "@appcenter_sdk//:AppCenter", + "@appcenter_sdk//:AppCenterCrashes", +] + +appcenter_targets = available_appcenter_targets if telegram_bundle_id in internal_bundle_ids else [] + swift_library( name = "TelegramUI", module_name = "TelegramUI", @@ -180,7 +197,6 @@ swift_library( "//submodules/GridMessageSelectionNode:GridMessageSelectionNode", "//submodules/InstantPageCache:InstantPageCache", "//submodules/PersistentStringHash:PersistentStringHash", - "//submodules/MessageReactionListUI:MessageReactionListUI", "//submodules/SegmentedControlNode:SegmentedControlNode", "//submodules/AppBundle:AppBundle", "//submodules/Markdown:Markdown", @@ -218,7 +234,17 @@ swift_library( "//submodules/DatePickerNode:DatePickerNode", "//submodules/ConfettiEffect:ConfettiEffect", "//submodules/Speak:Speak", - ], + "//submodules/PeerInfoAvatarListNode:PeerInfoAvatarListNode", + "//submodules/DebugSettingsUI:DebugSettingsUI", + "//submodules/ImportStickerPackUI:ImportStickerPackUI", + "//submodules/GradientBackground:GradientBackground", + "//submodules/WallpaperBackgroundNode:WallpaperBackgroundNode", + ] + select({ + "@build_bazel_rules_apple//apple:ios_armv7": [], + "@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets, + "//build-system:ios_sim_arm64": [], + "@build_bazel_rules_apple//apple:ios_x86_64": [], + }), visibility = [ "//visibility:public", ], diff --git a/submodules/TelegramUI/Images.xcassets/Call/CallEnableHD.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Call List/AlertAccentIcon.imageset/Contents.json similarity index 75% rename from submodules/TelegramUI/Images.xcassets/Call/CallEnableHD.imageset/Contents.json rename to submodules/TelegramUI/Images.xcassets/Call List/AlertAccentIcon.imageset/Contents.json index 8c4a6d824e..274390d56b 100644 --- a/submodules/TelegramUI/Images.xcassets/Call/CallEnableHD.imageset/Contents.json +++ b/submodules/TelegramUI/Images.xcassets/Call List/AlertAccentIcon.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "ic_menu_hdon.pdf", + "filename" : "callstab_2.pdf", "idiom" : "universal" } ], diff --git a/submodules/TelegramUI/Images.xcassets/Call List/AlertAccentIcon.imageset/callstab_2.pdf b/submodules/TelegramUI/Images.xcassets/Call List/AlertAccentIcon.imageset/callstab_2.pdf new file mode 100644 index 0000000000..4c8932027d Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Call List/AlertAccentIcon.imageset/callstab_2.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Call List/AlertIcon.imageset/CallsTabBarInfo@2x.png b/submodules/TelegramUI/Images.xcassets/Call List/AlertIcon.imageset/CallsTabBarInfo@2x.png deleted file mode 100644 index 9e3bf3bdab..0000000000 Binary files a/submodules/TelegramUI/Images.xcassets/Call List/AlertIcon.imageset/CallsTabBarInfo@2x.png and /dev/null differ diff --git a/submodules/TelegramUI/Images.xcassets/Call List/AlertIcon.imageset/CallsTabBarInfo@3x.png b/submodules/TelegramUI/Images.xcassets/Call List/AlertIcon.imageset/CallsTabBarInfo@3x.png deleted file mode 100644 index a566321806..0000000000 Binary files a/submodules/TelegramUI/Images.xcassets/Call List/AlertIcon.imageset/CallsTabBarInfo@3x.png and /dev/null differ diff --git a/submodules/TelegramUI/Images.xcassets/Call List/AlertIcon.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Call List/AlertIcon.imageset/Contents.json index 3ce0fe0ea8..5b72b42397 100644 --- a/submodules/TelegramUI/Images.xcassets/Call List/AlertIcon.imageset/Contents.json +++ b/submodules/TelegramUI/Images.xcassets/Call List/AlertIcon.imageset/Contents.json @@ -1,22 +1,12 @@ { "images" : [ { - "idiom" : "universal", - "scale" : "1x" - }, - { - "idiom" : "universal", - "filename" : "CallsTabBarInfo@2x.png", - "scale" : "2x" - }, - { - "idiom" : "universal", - "filename" : "CallsTabBarInfo@3x.png", - "scale" : "3x" + "filename" : "callstab_1.pdf", + "idiom" : "universal" } ], "info" : { - "version" : 1, - "author" : "xcode" + "author" : "xcode", + "version" : 1 } -} \ No newline at end of file +} diff --git a/submodules/TelegramUI/Images.xcassets/Call List/AlertIcon.imageset/callstab_1.pdf b/submodules/TelegramUI/Images.xcassets/Call List/AlertIcon.imageset/callstab_1.pdf new file mode 100644 index 0000000000..3c229852b6 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Call List/AlertIcon.imageset/callstab_1.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Call List/Contents.json b/submodules/TelegramUI/Images.xcassets/Call List/Contents.json index 38f0c81fc2..6e965652df 100644 --- a/submodules/TelegramUI/Images.xcassets/Call List/Contents.json +++ b/submodules/TelegramUI/Images.xcassets/Call List/Contents.json @@ -1,9 +1,9 @@ { "info" : { - "version" : 1, - "author" : "xcode" + "author" : "xcode", + "version" : 1 }, "properties" : { "provides-namespace" : true } -} \ No newline at end of file +} diff --git a/submodules/TelegramUI/Images.xcassets/Call/CallAirpodsMaxButton.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Call/CallAirpodsMaxButton.imageset/Contents.json new file mode 100644 index 0000000000..795a18ba91 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Call/CallAirpodsMaxButton.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "ic_call_airpodsmax.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Call/CallAirpodsMaxButton.imageset/ic_call_airpodsmax.pdf b/submodules/TelegramUI/Images.xcassets/Call/CallAirpodsMaxButton.imageset/ic_call_airpodsmax.pdf new file mode 100644 index 0000000000..3661b17240 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Call/CallAirpodsMaxButton.imageset/ic_call_airpodsmax.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Call/CallDisableHD.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Call/CallShareButton.imageset/Contents.json similarity index 75% rename from submodules/TelegramUI/Images.xcassets/Call/CallDisableHD.imageset/Contents.json rename to submodules/TelegramUI/Images.xcassets/Call/CallShareButton.imageset/Contents.json index 9deae5fa49..7a8982f200 100644 --- a/submodules/TelegramUI/Images.xcassets/Call/CallDisableHD.imageset/Contents.json +++ b/submodules/TelegramUI/Images.xcassets/Call/CallShareButton.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "ic_menu_hdoff.pdf", + "filename" : "callshare (1).pdf", "idiom" : "universal" } ], diff --git a/submodules/TelegramUI/Images.xcassets/Settings/MenuIcons/Wallet.imageset/ic_wallet.pdf b/submodules/TelegramUI/Images.xcassets/Call/CallShareButton.imageset/callshare (1).pdf similarity index 63% rename from submodules/TelegramUI/Images.xcassets/Settings/MenuIcons/Wallet.imageset/ic_wallet.pdf rename to submodules/TelegramUI/Images.xcassets/Call/CallShareButton.imageset/callshare (1).pdf index 768f6dadc4..03f94787bb 100644 Binary files a/submodules/TelegramUI/Images.xcassets/Settings/MenuIcons/Wallet.imageset/ic_wallet.pdf and b/submodules/TelegramUI/Images.xcassets/Call/CallShareButton.imageset/callshare (1).pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Call/CallSwitchCameraButton.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Call/CallSwitchCameraButton.imageset/Contents.json index 958ae1e032..5c38ab0966 100644 --- a/submodules/TelegramUI/Images.xcassets/Call/CallSwitchCameraButton.imageset/Contents.json +++ b/submodules/TelegramUI/Images.xcassets/Call/CallSwitchCameraButton.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "ic_call_flip.pdf", + "filename" : "ic_flip (1).pdf", "idiom" : "universal" } ], diff --git a/submodules/TelegramUI/Images.xcassets/Call/CallSwitchCameraButton.imageset/ic_flip (1).pdf b/submodules/TelegramUI/Images.xcassets/Call/CallSwitchCameraButton.imageset/ic_flip (1).pdf new file mode 100644 index 0000000000..da09156412 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Call/CallSwitchCameraButton.imageset/ic_flip (1).pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Call/CallCameraHDButton.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Call/Context Menu/Audio.imageset/Contents.json similarity index 74% rename from submodules/TelegramUI/Images.xcassets/Call/CallCameraHDButton.imageset/Contents.json rename to submodules/TelegramUI/Images.xcassets/Call/Context Menu/Audio.imageset/Contents.json index 05d9903d16..0355088028 100644 --- a/submodules/TelegramUI/Images.xcassets/Call/CallCameraHDButton.imageset/Contents.json +++ b/submodules/TelegramUI/Images.xcassets/Call/Context Menu/Audio.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "ic_call_camerahd.pdf", + "filename" : "ic_menuaudio.pdf", "idiom" : "universal" } ], diff --git a/submodules/TelegramUI/Images.xcassets/Call/CallCameraHDButton.imageset/ic_call_camerahd.pdf b/submodules/TelegramUI/Images.xcassets/Call/Context Menu/Audio.imageset/ic_menuaudio.pdf similarity index 61% rename from submodules/TelegramUI/Images.xcassets/Call/CallCameraHDButton.imageset/ic_call_camerahd.pdf rename to submodules/TelegramUI/Images.xcassets/Call/Context Menu/Audio.imageset/ic_menuaudio.pdf index 51d0299b88..045166f1fb 100644 Binary files a/submodules/TelegramUI/Images.xcassets/Call/CallCameraHDButton.imageset/ic_call_camerahd.pdf and b/submodules/TelegramUI/Images.xcassets/Call/Context Menu/Audio.imageset/ic_menuaudio.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Call/Context Menu/ChangeName.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Call/Context Menu/ChangeName.imageset/Contents.json new file mode 100644 index 0000000000..11bfc73127 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Call/Context Menu/ChangeName.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "ic_changename.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Call/CallDisableHD.imageset/ic_menu_hdoff.pdf b/submodules/TelegramUI/Images.xcassets/Call/Context Menu/ChangeName.imageset/ic_changename.pdf similarity index 65% rename from submodules/TelegramUI/Images.xcassets/Call/CallDisableHD.imageset/ic_menu_hdoff.pdf rename to submodules/TelegramUI/Images.xcassets/Call/Context Menu/ChangeName.imageset/ic_changename.pdf index ccb2a9a6bf..fc6696b38e 100644 Binary files a/submodules/TelegramUI/Images.xcassets/Call/CallDisableHD.imageset/ic_menu_hdoff.pdf and b/submodules/TelegramUI/Images.xcassets/Call/Context Menu/ChangeName.imageset/ic_changename.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Call/Context Menu/Noise.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Call/Context Menu/Noise.imageset/Contents.json new file mode 100644 index 0000000000..d4b4c572fd --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Call/Context Menu/Noise.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "ic_noisecancellation.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Call/Context Menu/Noise.imageset/ic_noisecancellation.pdf b/submodules/TelegramUI/Images.xcassets/Call/Context Menu/Noise.imageset/ic_noisecancellation.pdf new file mode 100644 index 0000000000..aa4ab67532 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Call/Context Menu/Noise.imageset/ic_noisecancellation.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Call/Context Menu/ShareScreen.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Call/Context Menu/ShareScreen.imageset/Contents.json new file mode 100644 index 0000000000..d3d53e234f --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Call/Context Menu/ShareScreen.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "ic_sharescreen.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Call/Context Menu/ShareScreen.imageset/ic_sharescreen.pdf b/submodules/TelegramUI/Images.xcassets/Call/Context Menu/ShareScreen.imageset/ic_sharescreen.pdf new file mode 100644 index 0000000000..bbaa0f43e8 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Call/Context Menu/ShareScreen.imageset/ic_sharescreen.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Call/Volume.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Call/PanelIcon.imageset/Contents.json similarity index 77% rename from submodules/TelegramUI/Images.xcassets/Call/Volume.imageset/Contents.json rename to submodules/TelegramUI/Images.xcassets/Call/PanelIcon.imageset/Contents.json index affbec4133..8919c4d81a 100644 --- a/submodules/TelegramUI/Images.xcassets/Call/Volume.imageset/Contents.json +++ b/submodules/TelegramUI/Images.xcassets/Call/PanelIcon.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "volsmall.pdf", + "filename" : "ic_panel.pdf", "idiom" : "universal" } ], diff --git a/submodules/TelegramUI/Images.xcassets/Call/PanelIcon.imageset/ic_panel.pdf b/submodules/TelegramUI/Images.xcassets/Call/PanelIcon.imageset/ic_panel.pdf new file mode 100644 index 0000000000..71666d9684 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Call/PanelIcon.imageset/ic_panel.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Call/Pause.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Call/Pause.imageset/Contents.json new file mode 100644 index 0000000000..3aaecc1942 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Call/Pause.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "wait.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Call/Pause.imageset/wait.pdf b/submodules/TelegramUI/Images.xcassets/Call/Pause.imageset/wait.pdf new file mode 100644 index 0000000000..8173b4d777 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Call/Pause.imageset/wait.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Call/Pin.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Call/Pin.imageset/Contents.json new file mode 100644 index 0000000000..ed8e0a0bf5 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Call/Pin.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "pin.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Call/Pin.imageset/pin.pdf b/submodules/TelegramUI/Images.xcassets/Call/Pin.imageset/pin.pdf new file mode 100644 index 0000000000..b9d4123329 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Call/Pin.imageset/pin.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Call/ScreenSharePhone.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Call/ScreenSharePhone.imageset/Contents.json new file mode 100644 index 0000000000..80dbbc8204 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Call/ScreenSharePhone.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "sharingscreen (1).pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Call/ScreenSharePhone.imageset/sharingscreen (1).pdf b/submodules/TelegramUI/Images.xcassets/Call/ScreenSharePhone.imageset/sharingscreen (1).pdf new file mode 100644 index 0000000000..9ef729b5ca Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Call/ScreenSharePhone.imageset/sharingscreen (1).pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Call/ScreenShareTablet.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Call/ScreenShareTablet.imageset/Contents.json new file mode 100644 index 0000000000..f9643c7932 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Call/ScreenShareTablet.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "sharingscreenipad (1).pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Call/ScreenShareTablet.imageset/sharingscreenipad (1).pdf b/submodules/TelegramUI/Images.xcassets/Call/ScreenShareTablet.imageset/sharingscreenipad (1).pdf new file mode 100644 index 0000000000..5929efeb34 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Call/ScreenShareTablet.imageset/sharingscreenipad (1).pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Call/Speaking.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Call/Speaking.imageset/Contents.json new file mode 100644 index 0000000000..d18942689f --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Call/Speaking.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "ic_vc_volume.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Call/CallSwitchCameraButton.imageset/ic_call_flip.pdf b/submodules/TelegramUI/Images.xcassets/Call/Speaking.imageset/ic_vc_volume.pdf similarity index 57% rename from submodules/TelegramUI/Images.xcassets/Call/CallSwitchCameraButton.imageset/ic_call_flip.pdf rename to submodules/TelegramUI/Images.xcassets/Call/Speaking.imageset/ic_vc_volume.pdf index 3a34ee5aa7..bbebd6cba6 100644 Binary files a/submodules/TelegramUI/Images.xcassets/Call/CallSwitchCameraButton.imageset/ic_call_flip.pdf and b/submodules/TelegramUI/Images.xcassets/Call/Speaking.imageset/ic_vc_volume.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Call/StatusScreen.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Call/StatusScreen.imageset/Contents.json new file mode 100644 index 0000000000..a12e93d182 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Call/StatusScreen.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "ic_voicesharing.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Call/StatusScreen.imageset/ic_voicesharing.pdf b/submodules/TelegramUI/Images.xcassets/Call/StatusScreen.imageset/ic_voicesharing.pdf new file mode 100644 index 0000000000..e19c33373d Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Call/StatusScreen.imageset/ic_voicesharing.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Call/StatusVideo.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Call/StatusVideo.imageset/Contents.json new file mode 100644 index 0000000000..9acec56798 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Call/StatusVideo.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "ic_voicecamera.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Call/CallEnableHD.imageset/ic_menu_hdon.pdf b/submodules/TelegramUI/Images.xcassets/Call/StatusVideo.imageset/ic_voicecamera.pdf similarity index 69% rename from submodules/TelegramUI/Images.xcassets/Call/CallEnableHD.imageset/ic_menu_hdon.pdf rename to submodules/TelegramUI/Images.xcassets/Call/StatusVideo.imageset/ic_voicecamera.pdf index 374497845e..9302da2be7 100644 Binary files a/submodules/TelegramUI/Images.xcassets/Call/CallEnableHD.imageset/ic_menu_hdon.pdf and b/submodules/TelegramUI/Images.xcassets/Call/StatusVideo.imageset/ic_voicecamera.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Call/StatusVolume.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Call/StatusVolume.imageset/Contents.json new file mode 100644 index 0000000000..7f670ccc62 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Call/StatusVolume.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "ic_voicevolumeon.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Call/StatusVolume.imageset/ic_voicevolumeon.pdf b/submodules/TelegramUI/Images.xcassets/Call/StatusVolume.imageset/ic_voicevolumeon.pdf new file mode 100644 index 0000000000..c6c6fd8475 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Call/StatusVolume.imageset/ic_voicevolumeon.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Call/StatusVolumeOff.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Call/StatusVolumeOff.imageset/Contents.json new file mode 100644 index 0000000000..95ca49b9d3 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Call/StatusVolumeOff.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "ic_voicevolumeoff.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Call/StatusVolumeOff.imageset/ic_voicevolumeoff.pdf b/submodules/TelegramUI/Images.xcassets/Call/StatusVolumeOff.imageset/ic_voicevolumeoff.pdf new file mode 100644 index 0000000000..bcc068ce30 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Call/StatusVolumeOff.imageset/ic_voicevolumeoff.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Call/SwitchCameraIcon.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Call/SwitchCameraIcon.imageset/Contents.json new file mode 100644 index 0000000000..e6f1b93d3e --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Call/SwitchCameraIcon.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "ic_cam_flip.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Call/Volume.imageset/volsmall.pdf b/submodules/TelegramUI/Images.xcassets/Call/SwitchCameraIcon.imageset/ic_cam_flip.pdf similarity index 69% rename from submodules/TelegramUI/Images.xcassets/Call/Volume.imageset/volsmall.pdf rename to submodules/TelegramUI/Images.xcassets/Call/SwitchCameraIcon.imageset/ic_cam_flip.pdf index 49993ba2bf..5084218a29 100644 Binary files a/submodules/TelegramUI/Images.xcassets/Call/Volume.imageset/volsmall.pdf and b/submodules/TelegramUI/Images.xcassets/Call/SwitchCameraIcon.imageset/ic_cam_flip.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Call/Unpin.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Call/Unpin.imageset/Contents.json new file mode 100644 index 0000000000..9e8d864ed6 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Call/Unpin.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "unpin.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Call/Unpin.imageset/unpin.pdf b/submodules/TelegramUI/Images.xcassets/Call/Unpin.imageset/unpin.pdf new file mode 100644 index 0000000000..fd3566cc37 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Call/Unpin.imageset/unpin.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Call/VideoUnavailable.imageset/CameraOff (1).pdf b/submodules/TelegramUI/Images.xcassets/Call/VideoUnavailable.imageset/CameraOff (1).pdf new file mode 100644 index 0000000000..a126545177 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Call/VideoUnavailable.imageset/CameraOff (1).pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Call/VideoUnavailable.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Call/VideoUnavailable.imageset/Contents.json new file mode 100644 index 0000000000..0fa6f58cca --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Call/VideoUnavailable.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "CameraOff (1).pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Camera.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Camera.imageset/Contents.json new file mode 100644 index 0000000000..d23d380699 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Camera.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "ic_menu_camera.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Camera.imageset/ic_menu_camera.pdf b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Camera.imageset/ic_menu_camera.pdf new file mode 100644 index 0000000000..d5ca0a2949 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Camera.imageset/ic_menu_camera.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/StarIconEmpty.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/StarIconEmpty.imageset/Contents.json deleted file mode 100644 index eba3e80b71..0000000000 --- a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/StarIconEmpty.imageset/Contents.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x" - }, - { - "idiom" : "universal", - "filename" : "ic_favesticker@2x.png", - "scale" : "2x" - }, - { - "idiom" : "universal", - "filename" : "ic_favesticker@3x.png", - "scale" : "3x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/StarIconEmpty.imageset/ic_favesticker@2x.png b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/StarIconEmpty.imageset/ic_favesticker@2x.png deleted file mode 100644 index 1a2b5d1035..0000000000 Binary files a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/StarIconEmpty.imageset/ic_favesticker@2x.png and /dev/null differ diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/StarIconEmpty.imageset/ic_favesticker@3x.png b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/StarIconEmpty.imageset/ic_favesticker@3x.png deleted file mode 100644 index b8aa993271..0000000000 Binary files a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/StarIconEmpty.imageset/ic_favesticker@3x.png and /dev/null differ diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/StarIconFilled.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/StarIconFilled.imageset/Contents.json deleted file mode 100644 index 7d96366e1c..0000000000 --- a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/StarIconFilled.imageset/Contents.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x" - }, - { - "idiom" : "universal", - "filename" : "ic_favedsticker@2x.png", - "scale" : "2x" - }, - { - "idiom" : "universal", - "filename" : "ic_favedsticker@3x.png", - "scale" : "3x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/StarIconFilled.imageset/ic_favedsticker@2x.png b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/StarIconFilled.imageset/ic_favedsticker@2x.png deleted file mode 100644 index 46f6daf33e..0000000000 Binary files a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/StarIconFilled.imageset/ic_favedsticker@2x.png and /dev/null differ diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/StarIconFilled.imageset/ic_favedsticker@3x.png b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/StarIconFilled.imageset/ic_favedsticker@3x.png deleted file mode 100644 index 16beedb0ec..0000000000 Binary files a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/StarIconFilled.imageset/ic_favedsticker@3x.png and /dev/null differ diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Message/BotPayment.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Message/BotPayment.imageset/Contents.json new file mode 100644 index 0000000000..8d74185285 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Message/BotPayment.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "card.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Message/BotPayment.imageset/card.pdf b/submodules/TelegramUI/Images.xcassets/Chat/Message/BotPayment.imageset/card.pdf new file mode 100644 index 0000000000..3e219e9e05 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Chat/Message/BotPayment.imageset/card.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Media Gallery/BackwardButton.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Gallery/BackwardButton.imageset/Contents.json index b3a939aaae..cf3c0409f8 100644 --- a/submodules/TelegramUI/Images.xcassets/Media Gallery/BackwardButton.imageset/Contents.json +++ b/submodules/TelegramUI/Images.xcassets/Media Gallery/BackwardButton.imageset/Contents.json @@ -1,12 +1,12 @@ { "images" : [ { - "idiom" : "universal", - "filename" : "ic_goback15.pdf" + "filename" : "back.pdf", + "idiom" : "universal" } ], "info" : { - "version" : 1, - "author" : "xcode" + "author" : "xcode", + "version" : 1 } -} \ No newline at end of file +} diff --git a/submodules/TelegramUI/Images.xcassets/Media Gallery/BackwardButton.imageset/ic_goback15.pdf b/submodules/TelegramUI/Images.xcassets/Media Gallery/BackwardButton.imageset/back.pdf similarity index 51% rename from submodules/TelegramUI/Images.xcassets/Media Gallery/BackwardButton.imageset/ic_goback15.pdf rename to submodules/TelegramUI/Images.xcassets/Media Gallery/BackwardButton.imageset/back.pdf index 4f2e9efd40..6d608954d4 100644 Binary files a/submodules/TelegramUI/Images.xcassets/Media Gallery/BackwardButton.imageset/ic_goback15.pdf and b/submodules/TelegramUI/Images.xcassets/Media Gallery/BackwardButton.imageset/back.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Media Gallery/ForwardButton.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Gallery/ForwardButton.imageset/Contents.json index 8d6aff79d8..925c41a2a6 100644 --- a/submodules/TelegramUI/Images.xcassets/Media Gallery/ForwardButton.imageset/Contents.json +++ b/submodules/TelegramUI/Images.xcassets/Media Gallery/ForwardButton.imageset/Contents.json @@ -1,12 +1,12 @@ { "images" : [ { - "idiom" : "universal", - "filename" : "ic_go15.pdf" + "filename" : "forward.pdf", + "idiom" : "universal" } ], "info" : { - "version" : 1, - "author" : "xcode" + "author" : "xcode", + "version" : 1 } -} \ No newline at end of file +} diff --git a/submodules/TelegramUI/Images.xcassets/Media Gallery/ForwardButton.imageset/ic_go15.pdf b/submodules/TelegramUI/Images.xcassets/Media Gallery/ForwardButton.imageset/forward.pdf similarity index 51% rename from submodules/TelegramUI/Images.xcassets/Media Gallery/ForwardButton.imageset/ic_go15.pdf rename to submodules/TelegramUI/Images.xcassets/Media Gallery/ForwardButton.imageset/forward.pdf index 181570be22..b9c89b04b3 100644 Binary files a/submodules/TelegramUI/Images.xcassets/Media Gallery/ForwardButton.imageset/ic_go15.pdf and b/submodules/TelegramUI/Images.xcassets/Media Gallery/ForwardButton.imageset/forward.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Media Gallery/Fullscreen.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Gallery/Fullscreen.imageset/Contents.json new file mode 100644 index 0000000000..5ef9354a34 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Media Gallery/Fullscreen.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "ic_fullscreen.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Media Gallery/Fullscreen.imageset/ic_fullscreen.pdf b/submodules/TelegramUI/Images.xcassets/Media Gallery/Fullscreen.imageset/ic_fullscreen.pdf new file mode 100644 index 0000000000..a0a4542075 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Media Gallery/Fullscreen.imageset/ic_fullscreen.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Media Gallery/Minimize.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Gallery/Minimize.imageset/Contents.json new file mode 100644 index 0000000000..657bd7f7bb --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Media Gallery/Minimize.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "ic_smallscreen.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Media Gallery/Minimize.imageset/ic_smallscreen.pdf b/submodules/TelegramUI/Images.xcassets/Media Gallery/Minimize.imageset/ic_smallscreen.pdf new file mode 100644 index 0000000000..f743475ce6 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Media Gallery/Minimize.imageset/ic_smallscreen.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 deleted file mode 100644 index d2ce704e18..0000000000 --- a/submodules/TelegramUI/Images.xcassets/Media Gallery/SoundOff.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "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 deleted file mode 100644 index 8051a2a4e0..0000000000 Binary files a/submodules/TelegramUI/Images.xcassets/Media Gallery/SoundOff.imageset/soundoff (2).pdf and /dev/null 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 deleted file mode 100644 index 1d93d33a26..0000000000 --- a/submodules/TelegramUI/Images.xcassets/Media Gallery/SoundOn.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "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 deleted file mode 100644 index 20236a226b..0000000000 Binary files a/submodules/TelegramUI/Images.xcassets/Media Gallery/SoundOn.imageset/soundon (2).pdf and /dev/null differ diff --git a/submodules/TelegramUI/Images.xcassets/Settings/MenuIcons/Contents.json b/submodules/TelegramUI/Images.xcassets/Settings/MenuIcons/Contents.json index 38f0c81fc2..6e965652df 100644 --- a/submodules/TelegramUI/Images.xcassets/Settings/MenuIcons/Contents.json +++ b/submodules/TelegramUI/Images.xcassets/Settings/MenuIcons/Contents.json @@ -1,9 +1,9 @@ { "info" : { - "version" : 1, - "author" : "xcode" + "author" : "xcode", + "version" : 1 }, "properties" : { "provides-namespace" : true } -} \ No newline at end of file +} diff --git a/submodules/TelegramUI/Images.xcassets/Settings/MenuIcons/Tips.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Settings/MenuIcons/Tips.imageset/Contents.json new file mode 100644 index 0000000000..d64de0bbcb --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Settings/MenuIcons/Tips.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "tips_30.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Settings/MenuIcons/Tips.imageset/tips_30.pdf b/submodules/TelegramUI/Images.xcassets/Settings/MenuIcons/Tips.imageset/tips_30.pdf new file mode 100644 index 0000000000..4c56ae4a56 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Settings/MenuIcons/Tips.imageset/tips_30.pdf @@ -0,0 +1,113 @@ +%PDF-1.7 + +1 0 obj + << >> +endobj + +2 0 obj + << /Length 3 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 0.000000 0.000000 cm +0.964706 0.768627 0.262745 scn +0.000000 18.799999 m +0.000000 22.720367 0.000000 24.680552 0.762954 26.177933 c +1.434068 27.495068 2.504932 28.565931 3.822066 29.237045 c +5.319448 30.000000 7.279633 30.000000 11.200000 30.000000 c +18.799999 30.000000 l +22.720367 30.000000 24.680552 30.000000 26.177933 29.237045 c +27.495068 28.565931 28.565931 27.495068 29.237045 26.177933 c +30.000000 24.680552 30.000000 22.720367 30.000000 18.799999 c +30.000000 11.200001 l +30.000000 7.279633 30.000000 5.319448 29.237045 3.822067 c +28.565931 2.504932 27.495068 1.434069 26.177933 0.762955 c +24.680552 0.000000 22.720367 0.000000 18.799999 0.000000 c +11.200000 0.000000 l +7.279633 0.000000 5.319448 0.000000 3.822066 0.762955 c +2.504932 1.434069 1.434068 2.504932 0.762954 3.822067 c +0.000000 5.319448 0.000000 7.279633 0.000000 11.200001 c +0.000000 18.799999 l +h +f +n +Q +q +1.000000 0.000000 -0.000000 1.000000 8.500000 5.000000 cm +1.000000 1.000000 1.000000 scn +0.000000 12.500000 m +0.000000 16.089851 2.910149 19.000000 6.500000 19.000000 c +10.089851 19.000000 13.000000 16.089851 13.000000 12.500000 c +13.000000 9.326989 11.698906 7.884006 10.621044 6.688600 c +10.292052 6.323730 9.983858 5.981926 9.739806 5.621033 c +9.164136 4.769756 8.876301 4.344118 8.752400 4.243239 c +8.663695 4.171017 8.625642 4.138249 8.582385 4.115283 c +8.539128 4.092316 8.490668 4.079148 8.381149 4.046125 c +8.228177 4.000000 7.977118 4.000000 7.475001 4.000000 c +5.525000 4.000000 l +5.022882 4.000000 4.771823 4.000000 4.618851 4.046125 c +4.509332 4.079148 4.460872 4.092316 4.417615 4.115283 c +4.374358 4.138249 4.336305 4.171017 4.247600 4.243239 c +4.123699 4.344119 3.835864 4.769756 3.260195 5.621032 c +3.016143 5.981926 2.707948 6.323730 2.378956 6.688601 c +1.301094 7.884007 0.000000 9.326990 0.000000 12.500000 c +h +5.500000 3.000000 m +4.947715 3.000000 4.500000 2.552284 4.500000 2.000000 c +4.500000 0.895432 5.395431 0.000000 6.500000 0.000000 c +7.604569 0.000000 8.500000 0.895430 8.500000 2.000000 c +8.500000 2.552284 8.052285 3.000000 7.500000 3.000000 c +5.500000 3.000000 l +h +f* +n +Q + +endstream +endobj + +3 0 obj + 2149 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 30.000000 30.000000 ] + /Resources 1 0 R + /Contents 2 0 R + /Parent 5 0 R + >> +endobj + +5 0 obj + << /Kids [ 4 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +6 0 obj + << /Type /Catalog + /Pages 5 0 R + >> +endobj + +xref +0 7 +0000000000 65535 f +0000000010 00000 n +0000000034 00000 n +0000002239 00000 n +0000002262 00000 n +0000002435 00000 n +0000002509 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +2568 +%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Settings/MenuIcons/Wallet.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Settings/MenuIcons/Wallet.imageset/Contents.json deleted file mode 100644 index 490f9cdd8e..0000000000 --- a/submodules/TelegramUI/Images.xcassets/Settings/MenuIcons/Wallet.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "filename" : "ic_wallet.pdf" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/submodules/TelegramUI/Resources/5789658100176783156-m.resource b/submodules/TelegramUI/Resources/5789658100176783156-m.resource new file mode 100644 index 0000000000..ba42f37225 Binary files /dev/null and b/submodules/TelegramUI/Resources/5789658100176783156-m.resource differ diff --git a/submodules/TelegramUI/Resources/Animations/CallsPlaceholder.tgs b/submodules/TelegramUI/Resources/Animations/CallsPlaceholder.tgs new file mode 100644 index 0000000000..8f3ab56cc9 Binary files /dev/null and b/submodules/TelegramUI/Resources/Animations/CallsPlaceholder.tgs differ diff --git a/submodules/TelegramUI/Resources/Animations/anim_cameraoff.json b/submodules/TelegramUI/Resources/Animations/anim_cameraoff.json new file mode 100644 index 0000000000..42acbe0966 --- /dev/null +++ b/submodules/TelegramUI/Resources/Animations/anim_cameraoff.json @@ -0,0 +1 @@ +{"v":"5.5.7","meta":{"g":"LottieFiles AE 0.1.20","a":"","k":"","d":"","tc":""},"fr":60,"ip":0,"op":11,"w":100,"h":100,"nm":"Camera Off","ddd":0,"assets":[{"id":"comp_0","layers":[{"ddd":0,"ind":2,"ty":4,"nm":"Line","parent":5,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.3,"y":1},"o":{"x":0.4,"y":0},"t":0,"s":[-37.845,-19,0],"to":[0,0,0],"ti":[0,0,0]},{"t":10,"s":[-20.845,0,0]}],"ix":2},"a":{"a":0,"k":[-20.845,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]],"o":[[0,0],[0,0]],"v":[[-148.295,-127.55],[106.605,127.55]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":20,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","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":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.3],"y":[1]},"o":{"x":[0.4],"y":[0]},"t":0,"s":[0]},{"t":10,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":180,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Lens","parent":5,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[129.155,0,0],"ix":2},"a":{"a":0,"k":[129.155,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":[[-5.175,-4.14],[0,-3.645],[0,0],[6.627,0],[2.277,2.847],[0,0],[0,6.812],[0,0],[-4.256,5.319],[0,0]],"o":[[2.847,2.277],[0,0],[0,6.627],[-3.645,0],[0,0],[-4.256,-5.319],[0,0],[0,-6.812],[0,0],[4.14,-5.175]],"v":[[154.651,-72.661],[159.155,-63.291],[159.155,63.291],[147.155,75.291],[137.785,70.787],[105.729,30.717],[99.155,11.977],[99.155,-11.977],[105.729,-30.717],[137.785,-70.787]],"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":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":180,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"Bottom","parent":5,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[-42.326,21.481,0],"ix":2},"a":{"a":0,"k":[-42.326,21.481,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.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":2,"s":[{"i":[[-0.356,4.018],[-7.24,7.49],[-29.064,-26.935],[-4.055,-3.802],[-16.419,-20.572],[7.432,0.162],[0,0],[7.333,3.922],[3.922,7.333],[0,20.059]],"o":[[0,0],[1.418,-1.467],[3.856,3.573],[47.36,44.399],[-5.621,7.372],[-4.82,-0.105],[-20.059,0],[-7.333,-3.922],[-3.922,-7.333],[0,0]],"v":[[-125.308,-62.038],[-116.76,-87.24],[-64.836,-40.48],[-52.953,-29.401],[68.996,93.003],[41.193,104.463],[-68.158,105],[-102.824,98.99],[-119.835,81.979],[-125.845,47.313]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":3,"s":[{"i":[[-0.356,4.018],[0,0],[-2.914,-3.02],[3.203,18.901],[-16.419,-20.572],[7.432,0.162],[0,0],[7.333,3.922],[3.922,7.333],[0,20.059]],"o":[[0,0],[0,0],[7.336,7.48],[47.36,44.399],[-5.621,7.372],[-4.82,-0.105],[-20.059,0],[-7.333,-3.922],[-3.922,-7.333],[0,0]],"v":[[-125.308,-62.038],[-125.26,-62.49],[-101.586,-39.98],[-78.203,-53.401],[68.996,93.003],[41.193,104.463],[-68.158,105],[-102.824,98.99],[-119.835,81.979],[-125.845,47.313]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":4,"s":[{"i":[[-0.356,4.018],[0,0],[-2.914,-3.02],[3.203,18.901],[-16.419,-20.572],[7.432,0.162],[0,0],[7.333,3.922],[3.922,7.333],[0,20.059]],"o":[[0,0],[0,0],[7.336,7.48],[47.36,44.399],[-5.621,7.372],[-4.82,-0.105],[-20.059,0],[-7.333,-3.922],[-3.922,-7.333],[0,0]],"v":[[-125.308,-62.038],[-125.26,-62.49],[-44.586,20.02],[-17.703,6.599],[68.996,93.003],[41.193,104.463],[-68.158,105],[-102.824,98.99],[-119.835,81.979],[-125.845,47.313]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":5,"s":[{"i":[[-0.356,4.018],[0,0],[-2.914,-3.02],[3.203,18.901],[-16.419,-20.572],[7.432,0.162],[0,0],[7.333,3.922],[3.922,7.333],[0,20.059]],"o":[[0,0],[0,0],[7.336,7.48],[47.36,44.399],[-5.621,7.372],[-4.82,-0.105],[-20.059,0],[-7.333,-3.922],[-3.922,-7.333],[0,0]],"v":[[-125.308,-62.038],[-125.26,-62.49],[6.914,71.02],[32.297,54.599],[68.996,93.003],[41.193,104.463],[-68.158,105],[-102.824,98.99],[-119.835,81.979],[-125.845,47.313]],"c":true}]},{"t":6,"s":[{"i":[[-0.356,4.018],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[7.333,3.922],[3.922,7.333],[0,20.059]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[-4.018,0.356],[-20.059,0],[-7.333,-3.922],[-3.922,-7.333],[0,0]],"v":[[-125.308,-62.038],[-125.143,-61.873],[-63.893,-0.623],[-61.477,1.793],[40.474,103.744],[41.193,104.463],[-68.158,105],[-102.824,98.989],[-119.835,81.979],[-125.845,47.313]],"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":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":180,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"Top","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[245.702,245.695,0],"ix":2},"a":{"a":0,"k":[-10.298,-10.305,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.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":2,"s":[{"i":[[-0.17,0.322],[-20.059,0],[0,0],[-7.333,-3.922],[-3.922,-7.333],[0,-20.059],[0,0],[10.875,-10.515],[40.241,37.623],[5.139,4.807]],"o":[[9,-17.056],[0,0],[20.059,0],[7.333,3.922],[3.922,7.333],[0,0],[0,20.059],[0,0],[-5.054,-4.726],[-48.111,-45.004]],"v":[[-119.25,-83.569],[-68.158,-105],[26.468,-105],[61.134,-98.99],[78.145,-81.979],[84.155,-47.313],[84.155,47.313],[69,93.015],[-6.846,22.171],[-22.167,7.843]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":3,"s":[{"i":[[-0.171,0.322],[-20.059,0],[0,0],[-7.333,-3.922],[-3.922,-7.333],[0,-20.059],[0,0],[10.875,-10.515],[40.241,37.623],[4.273,5.592]],"o":[[7,-13.181],[0,0],[20.059,0],[7.333,3.922],[3.922,7.333],[0,0],[0,20.059],[0,0],[-5.054,-4.726],[-38.083,-49.843]],"v":[[-116,-86.069],[-68.158,-105],[26.468,-105],[61.134,-98.99],[78.145,-81.979],[84.155,-47.313],[84.155,47.313],[69,93.015],[-6.846,22.171],[-22.167,7.843]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":4,"s":[{"i":[[-0.151,0.328],[-20.059,0],[0,0],[-7.333,-3.922],[-3.922,-7.333],[0,-20.059],[0,0],[10.875,-10.515],[42.676,39.907],[3.439,3.217]],"o":[[5.25,-11.431],[0,0],[20.059,0],[7.333,3.922],[3.922,7.333],[0,0],[0,20.059],[0,0],[-3.414,-3.192],[-47.763,-44.68]],"v":[[-113.75,-86.569],[-68.158,-105],[26.468,-105],[61.134,-98.99],[78.145,-81.979],[84.155,-47.313],[84.155,47.313],[69,93.015],[-12.959,16.456],[-22.747,0.083]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":5,"s":[{"i":[[-0.139,0.337],[-20.059,0],[0,0],[-7.333,-3.922],[-3.922,-7.333],[0,-20.059],[0,0],[10.875,-10.515],[40.241,37.623],[5.139,4.807]],"o":[[5.75,-13.931],[0,0],[20.059,0],[7.333,3.922],[3.922,7.333],[0,0],[0,20.059],[0,0],[-5.054,-4.726],[-48.111,-45.004]],"v":[[-113.75,-85.569],[-68.158,-105],[26.468,-105],[61.134,-98.99],[78.145,-81.979],[84.155,-47.313],[84.155,47.313],[69,93.015],[-6.846,22.171],[-22.167,7.843]],"c":true}]},{"t":6,"s":[{"i":[[-0.309,0.192],[-20.059,0],[0,0],[-7.333,-3.922],[-3.922,-7.333],[0,-20.059],[0,0],[6,-9.099],[38.889,38.884],[4.966,4.968]],"o":[[9,-5.583],[0,0],[20.059,0],[7.333,3.922],[3.922,7.333],[0,0],[0,20.059],[0,0],[-4.885,-4.884],[-46.487,-46.506]],"v":[[-104.75,-97.944],[-68.158,-105],[26.468,-105],[61.134,-98.99],[78.145,-81.979],[84.155,-47.313],[84.155,47.313],[77,84.39],[3.682,11.151],[-11.124,-3.656]],"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":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":180,"st":0,"bm":0}]}],"layers":[{"ddd":0,"ind":1,"ty":0,"nm":"Pre-comp 1","refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[50,50,0],"ix":2},"a":{"a":0,"k":[256,256,0],"ix":1},"s":{"a":0,"k":[13.6,13.6,100],"ix":6}},"ao":0,"w":512,"h":512,"ip":0,"op":180,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/submodules/TelegramUI/Resources/Animations/anim_cameraon.json b/submodules/TelegramUI/Resources/Animations/anim_cameraon.json new file mode 100644 index 0000000000..b10d3f1c7a --- /dev/null +++ b/submodules/TelegramUI/Resources/Animations/anim_cameraon.json @@ -0,0 +1 @@ +{"v":"5.5.7","meta":{"g":"LottieFiles AE 0.1.20","a":"","k":"","d":"","tc":""},"fr":60,"ip":0,"op":10,"w":100,"h":100,"nm":"Camera On","ddd":0,"assets":[{"id":"comp_0","layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Line","parent":4,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.3,"y":1},"o":{"x":0.4,"y":0},"t":0,"s":[-20.845,0,0],"to":[0,0,0],"ti":[0,0,0]},{"t":21,"s":[-24.845,-5,0]}],"ix":2},"a":{"a":0,"k":[-20.845,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]],"o":[[0,0],[0,0]],"v":[[-148.295,-127.55],[106.605,127.55]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":20,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","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":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.3],"y":[1]},"o":{"x":[0.4],"y":[0]},"t":0,"s":[0]},{"t":10,"s":[100]}],"ix":1},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":9,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Lens","parent":4,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[129.155,0,0],"ix":2},"a":{"a":0,"k":[129.155,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":[[-5.175,-4.14],[0,-3.645],[0,0],[6.627,0],[2.277,2.847],[0,0],[0,6.812],[0,0],[-4.256,5.319],[0,0]],"o":[[2.847,2.277],[0,0],[0,6.627],[-3.645,0],[0,0],[-4.256,-5.319],[0,0],[0,-6.812],[0,0],[4.14,-5.175]],"v":[[154.651,-72.661],[159.155,-63.291],[159.155,63.291],[147.155,75.291],[137.785,70.787],[105.729,30.717],[99.155,11.977],[99.155,-11.977],[105.729,-30.717],[137.785,-70.787]],"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":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":180,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Bottom","parent":4,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[-42.326,21.481,0],"ix":2},"a":{"a":0,"k":[-42.326,21.481,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.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":2,"s":[{"i":[[-0.356,4.018],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[7.333,3.922],[3.922,7.333],[0,20.059]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[-4.018,0.356],[-20.059,0],[-7.333,-3.922],[-3.922,-7.333],[0,0]],"v":[[-125.308,-62.038],[-125.143,-61.873],[-63.893,-0.623],[-61.477,1.793],[40.474,103.744],[41.193,104.463],[-68.158,105],[-102.824,98.989],[-119.835,81.979],[-125.845,47.313]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":3,"s":[{"i":[[-0.356,4.018],[-7.615,8.365],[-29.064,-26.935],[-15.579,-17.792],[0,0],[-0.193,0.287],[0,0],[7.333,3.922],[3.922,7.333],[0,20.059]],"o":[[0,0],[1.374,-1.509],[-10.414,-0.27],[11.953,13.651],[0,0],[-4.82,-0.105],[-20.059,0],[-7.333,-3.922],[-3.922,-7.333],[0,0]],"v":[[-125.308,-62.038],[-117.135,-86.615],[-74.336,-55.98],[-86.953,-23.151],[41.746,104.378],[41.193,104.463],[-68.158,105],[-102.824,98.99],[-119.835,81.979],[-125.845,47.313]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":4,"s":[{"i":[[-0.356,4.018],[-8.24,8.99],[-29.064,-26.935],[-12.968,-13.455],[0,0],[-0.193,0.287],[0,0],[7.333,3.922],[3.922,7.333],[0,20.059]],"o":[[0,0],[1.379,-1.504],[-10.414,-0.27],[11.953,12.401],[0,0],[-4.82,-0.105],[-20.059,0],[-7.333,-3.922],[-3.922,-7.333],[0,0]],"v":[[-125.308,-62.038],[-116.385,-86.865],[-19.336,1.77],[-30.953,31.099],[41.746,104.378],[41.193,104.463],[-68.158,105],[-102.824,98.99],[-119.835,81.979],[-125.845,47.313]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":5,"s":[{"i":[[-0.356,4.018],[-7.24,7.49],[-29.064,-26.935],[-12.568,-13.829],[0,0],[-0.193,0.287],[0,0],[7.333,3.922],[3.922,7.333],[0,20.059]],"o":[[0,0],[1.418,-1.467],[-10.414,-0.27],[9.453,10.401],[0,0],[-4.82,-0.105],[-20.059,0],[-7.333,-3.922],[-3.922,-7.333],[0,0]],"v":[[-125.308,-62.038],[-116.51,-87.24],[26.664,47.27],[15.047,76.599],[41.746,104.378],[41.193,104.463],[-68.158,105],[-102.824,98.99],[-119.835,81.979],[-125.845,47.313]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":6,"s":[{"i":[[-0.356,4.018],[-8.615,9.49],[-29.064,-26.935],[-6.797,-11.099],[0,0],[-0.193,0.287],[0,0],[7.333,3.922],[3.922,7.333],[0,20.059]],"o":[[0,0],[1.371,-1.511],[-10.414,-0.27],[1.518,2.478],[0,0],[-4.82,-0.105],[-20.059,0],[-7.333,-3.922],[-3.922,-7.333],[0,0]],"v":[[-125.308,-62.038],[-116.385,-86.99],[57.414,76.77],[40.297,101.849],[41.746,104.378],[41.193,104.463],[-68.158,105],[-102.824,98.99],[-119.835,81.979],[-125.845,47.313]],"c":true}]},{"t":7,"s":[{"i":[[-0.356,4.018],[-7.24,7.49],[-29.064,-26.935],[-4.055,-3.802],[-16.419,-20.572],[7.432,0.162],[0,0],[7.333,3.922],[3.922,7.333],[0,20.059]],"o":[[0,0],[1.418,-1.467],[3.856,3.573],[47.36,44.399],[-5.621,7.372],[-4.82,-0.105],[-20.059,0],[-7.333,-3.922],[-3.922,-7.333],[0,0]],"v":[[-125.308,-62.038],[-116.76,-87.24],[-64.836,-40.48],[-52.953,-29.401],[68.996,93.003],[41.193,104.463],[-68.158,105],[-102.824,98.99],[-119.835,81.979],[-125.845,47.313]],"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":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":180,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"Top","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[245.702,245.695,0],"ix":2},"a":{"a":0,"k":[-10.298,-10.305,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.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":2,"s":[{"i":[[-0.606,0.376],[-5.516,1.066],[-10.029,0],[0,0],[0,0],[-7.333,-3.922],[-3.922,-7.333],[0,-20.059],[0,0],[6,-9.099]],"o":[[4.5,-2.791],[5.516,-1.066],[0,0],[0,0],[20.059,0],[7.333,3.922],[3.922,7.333],[0,0],[0,20.059],[0,0]],"v":[[-104.75,-97.944],[-90.601,-103.566],[-68.158,-105],[-20.845,-105],[26.468,-105],[61.134,-98.99],[78.145,-81.979],[84.155,-47.313],[84.155,47.313],[77,84.39]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":3,"s":[{"i":[[-0.333,0.63],[-8.797,2.851],[-10.029,0],[0,0],[0,0],[-7.333,-3.922],[-3.922,-7.333],[0,-20.059],[0,0],[10.875,-10.515]],"o":[[4.5,-8.528],[8.797,-2.851],[0,0],[0,0],[20.059,0],[7.333,3.922],[3.922,7.333],[0,0],[0,20.059],[0,0]],"v":[[-117.875,-85.069],[-97.164,-101.43],[-68.158,-105],[-20.845,-105],[26.468,-105],[61.134,-98.99],[78.145,-81.979],[84.155,-47.313],[84.155,47.313],[69.5,93.515]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":4,"s":[{"i":[[-0.333,0.63],[-8.641,2.663],[-10.029,0],[0,0],[0,0],[-7.333,-3.922],[-3.922,-7.333],[0,-20.059],[0,0],[10.875,-10.515]],"o":[[4.5,-8.528],[8.641,-2.663],[0,0],[0,0],[20.059,0],[7.333,3.922],[3.922,7.333],[0,0],[0,20.059],[0,0]],"v":[[-117.25,-85.819],[-96.851,-101.805],[-68.158,-105],[-20.845,-105],[26.468,-105],[61.134,-98.99],[78.145,-81.979],[84.155,-47.313],[84.155,47.313],[69.5,93.515]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":5,"s":[{"i":[[-0.333,0.63],[-8.766,2.726],[-10.029,0],[0,0],[0,0],[-7.333,-3.922],[-3.922,-7.333],[0,-20.059],[0,0],[10.875,-10.515]],"o":[[4.5,-8.528],[8.766,-2.726],[0,0],[0,0],[20.059,0],[7.333,3.922],[3.922,7.333],[0,0],[0,20.059],[0,0]],"v":[[-117.75,-85.569],[-97.101,-101.68],[-68.158,-105],[-20.845,-105],[26.468,-105],[61.134,-98.99],[78.145,-81.979],[84.155,-47.313],[84.155,47.313],[69.5,93.515]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":6,"s":[{"i":[[-0.399,0.59],[-8.531,2.898],[-10.029,0],[0,0],[0,0],[-7.333,-3.922],[-3.922,-7.333],[0,-20.059],[0,0],[10.875,-10.515]],"o":[[5.813,-8.591],[8.531,-2.898],[0,0],[0,0],[20.059,0],[7.333,3.922],[3.922,7.333],[0,0],[0,20.059],[0,0]],"v":[[-118.125,-84.819],[-96.304,-101.352],[-68.158,-105],[-20.845,-105],[26.468,-105],[61.134,-98.99],[78.145,-81.979],[84.155,-47.313],[84.155,47.313],[69.5,93.515]],"c":true}]},{"t":7,"s":[{"i":[[-0.17,0.322],[-20.059,0],[0,0],[-7.333,-3.922],[-3.922,-7.333],[0,-20.059],[0,0],[10.875,-10.515],[40.241,37.623],[5.139,4.807]],"o":[[9,-17.056],[0,0],[20.059,0],[7.333,3.922],[3.922,7.333],[0,0],[0,20.059],[0,0],[-5.054,-4.726],[-48.111,-45.004]],"v":[[-119.25,-83.569],[-68.158,-105],[26.468,-105],[61.134,-98.99],[78.145,-81.979],[84.155,-47.313],[84.155,47.313],[69,93.015],[-6.846,22.171],[-22.167,7.843]],"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":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":180,"st":0,"bm":0}]}],"layers":[{"ddd":0,"ind":1,"ty":0,"nm":"Pre-comp 1","refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[50,50,0],"ix":2},"a":{"a":0,"k":[256,256,0],"ix":1},"s":{"a":0,"k":[13.6,13.6,100],"ix":6}},"ao":0,"w":512,"h":512,"ip":0,"op":180,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/submodules/TelegramUI/Resources/Animations/anim_closemenu.json b/submodules/TelegramUI/Resources/Animations/anim_closemenu.json new file mode 100644 index 0000000000..09ad0b611c --- /dev/null +++ b/submodules/TelegramUI/Resources/Animations/anim_closemenu.json @@ -0,0 +1 @@ +{"v":"5.7.8","fr":60,"ip":0,"op":20,"w":30,"h":30,"nm":"CloseMenu","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"3","parent":2,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[180]},{"t":15,"s":[0]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[0,0,0],"to":[0,5,0],"ti":[0,-5,0]},{"t":15,"s":[0,30,0]}],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-5,5],[5,-5]],"c":false}]},{"t":15,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-6.5,0],[6.5,0]],"c":false}]}],"ix":2},"nm":"Контур 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":1.66,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Обводка 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[600,600],"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":"Преобразовать"}],"nm":"1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":20,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[90]},{"t":15,"s":[0]}],"ix":10},"p":{"a":0,"k":[15,15,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[16.667,16.667,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-6.5,0],[6.5,0]],"c":false},"ix":2},"nm":"Контур 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":1.66,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Обводка 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[600,600],"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":"Преобразовать"}],"nm":"2","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[50]},{"t":5,"s":[0]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[50]},{"t":5,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Обрезать контуры 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":20,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"1","parent":2,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[90]},{"t":15,"s":[0]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[0,0,0],"to":[0,-5,0],"ti":[0,5,0]},{"t":15,"s":[0,-30,0]}],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-5,5],[5,-5]],"c":false}]},{"t":15,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-6.5,0],[6.5,0]],"c":false}]}],"ix":2},"nm":"Контур 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":1.66,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Обводка 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[600,600],"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":"Преобразовать"}],"nm":"1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":20,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/submodules/TelegramUI/Resources/Animations/anim_menuclose.json b/submodules/TelegramUI/Resources/Animations/anim_menuclose.json new file mode 100644 index 0000000000..c29ed679ce --- /dev/null +++ b/submodules/TelegramUI/Resources/Animations/anim_menuclose.json @@ -0,0 +1 @@ +{"v":"5.7.8","fr":60,"ip":0,"op":20,"w":30,"h":30,"nm":"MenuClose","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"3","parent":2,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[0]},{"t":15,"s":[180]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[0,30,0],"to":[0,-5,0],"ti":[0,5,0]},{"t":15,"s":[0,0,0]}],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-6.5,0],[6.5,0]],"c":false}]},{"t":15,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-5,5],[5,-5]],"c":false}]}],"ix":2},"nm":"Контур 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":1.66,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Обводка 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[600,600],"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":"Преобразовать"}],"nm":"1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":20,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[0]},{"t":15,"s":[90]}],"ix":10},"p":{"a":0,"k":[15,15,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[16.667,16.667,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-6.5,0],[6.5,0]],"c":false},"ix":2},"nm":"Контур 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":1.66,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Обводка 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[600,600],"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":"Преобразовать"}],"nm":"2","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[0]},{"t":5,"s":[50]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[100]},{"t":5,"s":[50]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Обрезать контуры 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":20,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"1","parent":2,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[0]},{"t":15,"s":[90]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[0,-30,0],"to":[0,5,0],"ti":[0,-5,0]},{"t":15,"s":[0,0,0]}],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-6.5,0],[6.5,0]],"c":false}]},{"t":15,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-5,5],[5,-5]],"c":false}]}],"ix":2},"nm":"Контур 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":1.66,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Обводка 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[600,600],"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":"Преобразовать"}],"nm":"1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":20,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/submodules/TelegramUI/Resources/Animations/anim_payment.json b/submodules/TelegramUI/Resources/Animations/anim_payment.json new file mode 100644 index 0000000000..d0dee855cb --- /dev/null +++ b/submodules/TelegramUI/Resources/Animations/anim_payment.json @@ -0,0 +1 @@ +{"v":"5.5.7","meta":{"g":"LottieFiles AE 0.1.20","a":"","k":"","d":"","tc":""},"fr":60,"ip":0,"op":63,"w":512,"h":512,"nm":"Card 3","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Coin 3","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":14,"s":[50]},{"t":62,"s":[140.566]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.3,"y":0.848},"o":{"x":0.05,"y":0},"t":17,"s":[270.605,259.865,0],"to":[0,0,0],"ti":[40.605,0.865,0]},{"i":{"x":0.7,"y":1},"o":{"x":0.6,"y":0.053},"t":42,"s":[215.605,71.865,0],"to":[-40.605,-0.865,0],"ti":[0,0,0]},{"t":62,"s":[165.605,457.865,0]}],"ix":2},"a":{"a":0,"k":[43.395,127.865,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":59,"s":[-100,100,100]},{"t":62,"s":[0,0,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":17,"s":[{"i":[[0,-19.405],[19.405,0],[0,19.405],[-19.405,0]],"o":[[0,19.405],[-19.405,0],[0,-19.405],[19.405,0]],"v":[[78.53,127.865],[43.395,163],[8.259,127.865],[43.395,92.729]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":27,"s":[{"i":[[0,-19.405],[10.585,0.549],[0,19.405],[-9.164,0.795]],"o":[[0,19.405],[-9.164,-0.476],[0,-19.405],[9.063,-0.787]],"v":[[51.981,127.865],[43.395,163],[34.809,127.865],[43.395,92.729]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":37,"s":[{"i":[[0,-19.405],[19.405,0],[0,19.405],[-19.405,0]],"o":[[0,19.405],[-19.405,0],[0,-19.405],[19.405,0]],"v":[[78.53,127.865],[43.395,163],[8.259,127.865],[43.395,92.729]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":47,"s":[{"i":[[0,-19.405],[10.585,0.549],[0,19.405],[-9.164,0.795]],"o":[[0,19.405],[-9.164,-0.476],[0,-19.405],[9.063,-0.787]],"v":[[51.981,127.865],[43.395,163],[34.809,127.865],[43.395,92.729]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":57,"s":[{"i":[[0,-19.405],[19.405,0],[0,19.405],[-19.405,0]],"o":[[0,19.405],[-19.405,0],[0,-19.405],[19.405,0]],"v":[[78.53,127.865],[43.395,163],[8.259,127.865],[43.395,92.729]],"c":true}]},{"t":62,"s":[{"i":[[0,-19.405],[14.995,0.275],[0,19.405],[-14.284,0.398]],"o":[[0,19.405],[-14.284,-0.238],[0,-19.405],[14.234,-0.393]],"v":[[65.256,127.865],[43.395,163],[21.534,127.865],[43.395,92.729]],"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":"Group 2","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":19,"op":194,"st":12,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Coin 2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":2,"s":[0]},{"t":55,"s":[200]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.3,"y":0.872},"o":{"x":0.05,"y":0},"t":5,"s":[197.605,259.865,0],"to":[0,0,0],"ti":[57.605,-1.135,0]},{"i":{"x":0.7,"y":1},"o":{"x":0.6,"y":0.048},"t":30,"s":[132.605,40.865,0],"to":[-57.605,1.135,0],"ti":[0,0,0]},{"t":50,"s":[33.605,459.865,0]}],"ix":2},"a":{"a":0,"k":[43.395,127.865,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":47,"s":[-100,100,100]},{"t":50,"s":[0,0,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":5,"s":[{"i":[[0,-19.405],[19.405,0],[0,19.405],[-19.405,0]],"o":[[0,19.405],[-19.405,0],[0,-19.405],[19.405,0]],"v":[[78.53,127.865],[43.395,163],[8.259,127.865],[43.395,92.729]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":12,"s":[{"i":[[0,-19.405],[13.231,0.385],[0,19.405],[-12.236,0.557]],"o":[[0,19.405],[-12.236,-0.333],[0,-19.405],[12.166,-0.551]],"v":[[59.946,127.865],[44.829,157.471],[26.844,127.865],[43.395,92.729]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":15,"s":[{"i":[[0,-19.405],[10.585,0.549],[0,19.405],[-9.164,0.795]],"o":[[0,19.405],[-9.164,-0.476],[0,-19.405],[9.063,-0.787]],"v":[[51.981,127.865],[43.395,163],[34.809,127.865],[43.395,92.729]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":25,"s":[{"i":[[0,-19.405],[19.405,0],[0,19.405],[-19.405,0]],"o":[[0,19.405],[-19.405,0],[0,-19.405],[19.405,0]],"v":[[78.53,127.865],[43.395,163],[8.259,127.865],[43.395,92.729]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":35,"s":[{"i":[[0,-19.405],[10.585,0.549],[0,19.405],[-9.164,0.795]],"o":[[0,19.405],[-9.164,-0.476],[0,-19.405],[9.063,-0.787]],"v":[[51.981,127.865],[43.395,163],[34.809,127.865],[43.395,92.729]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":45,"s":[{"i":[[0,-19.405],[19.405,0],[0,19.405],[-19.405,0]],"o":[[0,19.405],[-19.405,0],[0,-19.405],[19.405,0]],"v":[[78.53,127.865],[43.395,163],[8.259,127.865],[43.395,92.729]],"c":true}]},{"t":55,"s":[{"i":[[0,-19.405],[10.585,0.549],[0,19.405],[-9.164,0.795]],"o":[[0,19.405],[-9.164,-0.476],[0,-19.405],[9.063,-0.787]],"v":[[51.981,127.865],[43.395,163],[34.809,127.865],[43.395,92.729]],"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":"Group 2","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":12,"op":182,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Coin","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":11,"s":[0]},{"t":61,"s":[178]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.3,"y":0.872},"o":{"x":0.05,"y":0},"t":11,"s":[314.395,259.865,0],"to":[0,0,0],"ti":[-57.605,-1.135,0]},{"i":{"x":0.7,"y":1},"o":{"x":0.6,"y":0.048},"t":36,"s":[379.395,40.865,0],"to":[57.605,1.135,0],"ti":[0,0,0]},{"t":56,"s":[478.395,459.865,0]}],"ix":2},"a":{"a":0,"k":[43.395,127.865,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":53,"s":[100,100,100]},{"t":56,"s":[0,0,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":11,"s":[{"i":[[0,-19.405],[19.405,0],[0,19.405],[-19.405,0]],"o":[[0,19.405],[-19.405,0],[0,-19.405],[19.405,0]],"v":[[78.53,127.865],[43.395,163],[8.259,127.865],[43.395,92.729]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":15,"s":[{"i":[[0.921,-19.383],[15.877,0.22],[0,19.405],[-15.308,0.318]],"o":[[-0.423,8.902],[-15.308,-0.19],[0,-19.405],[15.268,-0.315]],"v":[[67.911,127.865],[40.876,136.815],[18.879,127.865],[43.395,92.729]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":17,"s":[{"i":[[0.614,-19.39],[14.113,0.33],[0,19.405],[-13.26,0.477]],"o":[[-0.282,12.403],[-13.26,-0.285],[0,-19.405],[13.2,-0.472]],"v":[[62.601,127.865],[42.796,162.033],[24.189,127.865],[43.395,92.729]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":21,"s":[{"i":[[0,-19.405],[10.585,0.549],[0,19.405],[-9.164,0.795]],"o":[[0,19.405],[-9.164,-0.476],[0,-19.405],[9.063,-0.787]],"v":[[51.981,127.865],[43.395,163],[34.809,127.865],[43.395,92.729]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":31,"s":[{"i":[[0,-19.405],[19.405,0],[0,19.405],[-19.405,0]],"o":[[0,19.405],[-19.405,0],[0,-19.405],[19.405,0]],"v":[[78.53,127.865],[43.395,163],[8.259,127.865],[43.395,92.729]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":41,"s":[{"i":[[0,-19.405],[10.585,0.549],[0,19.405],[-9.164,0.795]],"o":[[0,19.405],[-9.164,-0.476],[0,-19.405],[9.063,-0.787]],"v":[[51.981,127.865],[43.395,163],[34.809,127.865],[43.395,92.729]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":51,"s":[{"i":[[0,-19.405],[19.405,0],[0,19.405],[-19.405,0]],"o":[[0,19.405],[-19.405,0],[0,-19.405],[19.405,0]],"v":[[78.53,127.865],[43.395,163],[8.259,127.865],[43.395,92.729]],"c":true}]},{"t":61,"s":[{"i":[[0,-19.405],[10.585,0.549],[0,19.405],[-9.164,0.795]],"o":[[0,19.405],[-9.164,-0.476],[0,-19.405],[9.063,-0.787]],"v":[[51.981,127.865],[43.395,163],[34.809,127.865],[43.395,92.729]],"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":"Group 2","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":15,"op":183,"st":1,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"Card","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.4,"y":1},"o":{"x":0.29,"y":0},"t":0,"s":[256,376,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.71,"y":1},"o":{"x":0.6,"y":0},"t":8,"s":[256,350,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.3,"y":0.587},"o":{"x":0.2,"y":0},"t":18,"s":[256,448,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.7,"y":1},"o":{"x":0.6,"y":0.058},"t":40,"s":[256,424,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.71,"y":1},"o":{"x":0.3,"y":0},"t":50,"s":[256,357,0],"to":[0,0,0],"ti":[0,0,0]},{"t":59,"s":[256,376,0]}],"ix":2},"a":{"a":0,"k":[256,376,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.2,0.2,0.2],"y":[1,1,1]},"o":{"x":[0.29,0.29,0.29],"y":[0,0,0]},"t":0,"s":[60,0,100]},{"i":{"x":[0.71,0.71,0.71],"y":[1,1,1]},"o":{"x":[0.6,0.6,0.6],"y":[0,0,0]},"t":10,"s":[98,102,100]},{"i":{"x":[0.42,0.42,0.42],"y":[1,1,1]},"o":{"x":[0.2,0.2,0.2],"y":[0,0,0]},"t":20,"s":[102,97,100]},{"i":{"x":[0.3,0.3,0.3],"y":[1.413,1.413,1.413]},"o":{"x":[0.3,0.3,0.3],"y":[0,0,0]},"t":32,"s":[100,100,100]},{"i":{"x":[0.7,0.7,0.7],"y":[1,1,1]},"o":{"x":[0.6,0.6,0.6],"y":[0.058,0.058,-0.058]},"t":42,"s":[100,100,100]},{"i":{"x":[0.71,0.71,0.71],"y":[1,1,1]},"o":{"x":[0.3,0.3,0.3],"y":[0,0,0]},"t":52,"s":[98,102,100]},{"t":61,"s":[100,100,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-4.142,0],[0,0],[0,4.142],[0,0],[15.188,0],[0,0],[0,-15.188],[0,0]],"o":[[0,0],[4.142,0],[0,0],[0,-15.188],[0,0],[-15.188,0],[0,0],[0,4.142]],"v":[[-150,-65],[150,-65],[157.5,-72.5],[157.5,-92.5],[130,-120],[-130,-120],[-157.5,-92.5],[-157.5,-72.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-Copy","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[256,163.5],"ix":2},"a":{"a":0,"k":[0,-92.5],"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":"Card Top","np":1,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-4.142,0],[0,0],[0,-4.142],[0,0],[19.33,0],[0,0],[0,19.33],[0,0]],"o":[[0,0],[4.142,0],[0,0],[0,19.33],[0,0],[-19.33,0],[0,0],[0,-4.142]],"v":[[-150,-20],[150,-20],[157.5,-12.5],[157.5,85],[122.5,120],[-122.5,120],[-157.5,85],[-157.5,-12.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},{"ty":"tr","p":{"a":0,"k":[256,267],"ix":2},"a":{"a":0,"k":[0,11],"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":"Card Bottom","np":1,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":183,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/submodules/TelegramUI/Resources/Animations/anim_playpause.tgs b/submodules/TelegramUI/Resources/Animations/anim_playpause.tgs new file mode 100644 index 0000000000..d6e7b0953d Binary files /dev/null and b/submodules/TelegramUI/Resources/Animations/anim_playpause.tgs differ diff --git a/submodules/TelegramUI/Resources/Animations/anim_profileleave.json b/submodules/TelegramUI/Resources/Animations/anim_profileleave.json new file mode 100644 index 0000000000..cb43bdf987 --- /dev/null +++ b/submodules/TelegramUI/Resources/Animations/anim_profileleave.json @@ -0,0 +1 @@ +{"v":"5.5.7","meta":{"g":"LottieFiles AE 0.1.20","a":"","k":"","d":"","tc":""},"fr":60,"ip":0,"op":48,"w":512,"h":512,"nm":"Comp 1","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Arrow","parent":2,"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.05,"y":0},"t":0,"s":[43.05,-0.05,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.5,"y":1},"o":{"x":0.6,"y":0},"t":19,"s":[76.129,-5.159,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.37,"y":1},"o":{"x":0.3,"y":0},"t":33,"s":[22.05,-0.05,0],"to":[0,0,0],"ti":[0,0,0]},{"t":41,"s":[43.05,-0.05,0]}],"ix":2},"a":{"a":0,"k":[43.05,-0.05,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.1,"y":1},"o":{"x":0.05,"y":0},"t":0,"s":[{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[75.8,-38.2],[113.3,-0.3],[75.3,38.1]],"c":false}]},{"i":{"x":0.5,"y":1},"o":{"x":0.6,"y":0},"t":22,"s":[{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[76.192,-36.598],[101.074,-7.417],[86.947,28.824]],"c":false}]},{"t":35,"s":[{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[75.8,-38.2],[113.3,-0.3],[75.3,38.1]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0,0,0,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":26,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","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":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.1,"y":1},"o":{"x":0.05,"y":0},"t":0,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-27.2,-0.1],[113.3,-0.1]],"c":false}]},{"i":{"x":0.5,"y":1},"o":{"x":0.6,"y":0},"t":22,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[30.629,7.629],[98.926,-7.724]],"c":false}]},{"t":35,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-27.2,-0.1],[113.3,-0.1]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0,0,0,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":26,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","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":"Group 2","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":180,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Door","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.7],"y":[1]},"o":{"x":[0.3],"y":[0]},"t":0,"s":[0]},{"i":{"x":[0.1],"y":[1]},"o":{"x":[0.1],"y":[0]},"t":5,"s":[2]},{"i":{"x":[0.5],"y":[1]},"o":{"x":[0.6],"y":[0]},"t":24,"s":[-1]},{"i":{"x":[0.5],"y":[1]},"o":{"x":[0.3],"y":[0]},"t":34,"s":[-3]},{"i":{"x":[0.7],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":42,"s":[2]},{"t":47,"s":[0]}],"ix":10},"p":{"a":0,"k":[222.55,376.8,0],"ix":2},"a":{"a":0,"k":[-33.45,120.8,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.1,"y":1},"o":{"x":0.05,"y":0},"t":0,"s":[{"i":[[0,0],[0,0],[14,0],[0,0],[0,-14],[0,0],[-14,0],[0,0],[0,13.6],[0,0]],"o":[[0,0],[0,-14],[0,0],[-14,0],[0,0],[0,14],[0,0],[13.6,0],[0,0],[0,0]],"v":[[43.3,-76.5],[43.3,-89.6],[17.9,-115],[-84.9,-115],[-110.3,-89.6],[-110.3,89.5],[-85,114.8],[18.7,114.8],[43.4,90.1],[43.4,76.3]],"c":false}]},{"i":{"x":0.5,"y":1},"o":{"x":0.6,"y":0},"t":21,"s":[{"i":[[0,0],[0,0],[9.861,0],[0,0],[0,-14],[0,0],[-9.861,0],[0,0],[0,13.6],[0,0]],"o":[[0,0],[0,-14],[0,0],[-9.861,0],[0,0],[0,14],[0,0],[9.58,0],[0,0],[0,0]],"v":[[25.262,-46.7],[25.33,-89.8],[7.438,-115.2],[-65.109,-85.2],[-83,-59.8],[-83,119.3],[-65.179,144.6],[8.002,114.6],[25.4,89.9],[25.044,47.1]],"c":false}]},{"i":{"x":0.5,"y":1},"o":{"x":0.3,"y":0},"t":33,"s":[{"i":[[0,0],[0,0],[13.034,0],[0,0],[0,-14.731],[0,0],[-13.034,0],[0,0],[0,14.31],[0,0]],"o":[[0,0],[0,-14.731],[0,0],[-13.034,0],[0,0],[0,14.731],[0,0],[12.662,0],[0,0],[0,0]],"v":[[38.007,-80.49],[38.007,-94.274],[14.359,-121],[-81.352,-121],[-105,-94.274],[-105,94.179],[-81.445,120.8],[15.103,120.8],[38.1,94.81],[38.1,80.29]],"c":false}]},{"t":41,"s":[{"i":[[0,0],[0,0],[14,0],[0,0],[0,-14],[0,0],[-14,0],[0,0],[0,13.6],[0,0]],"o":[[0,0],[0,-14],[0,0],[-14,0],[0,0],[0,14],[0,0],[13.6,0],[0,0],[0,0]],"v":[[43.3,-76.5],[43.3,-89.6],[17.9,-115],[-84.9,-115],[-110.3,-89.6],[-110.3,89.5],[-85,114.8],[18.7,114.8],[43.4,90.1],[43.4,76.3]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0,0,0,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":26,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","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":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":180,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/submodules/TelegramUI/Resources/Animations/anim_profilemore.json b/submodules/TelegramUI/Resources/Animations/anim_profilemore.json new file mode 100644 index 0000000000..92fdec58c1 --- /dev/null +++ b/submodules/TelegramUI/Resources/Animations/anim_profilemore.json @@ -0,0 +1 @@ +{"v":"5.5.7","meta":{"g":"LottieFiles AE 0.1.20","a":"","k":"","d":"","tc":""},"fr":60,"ip":0,"op":45,"w":512,"h":512,"nm":"Comp 1","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Point 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.6,"y":1},"o":{"x":0.4,"y":0},"t":0,"s":[153.6,256,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.6,"y":1},"o":{"x":0.4,"y":0},"t":10,"s":[153.6,204,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.6,"y":1},"o":{"x":0.4,"y":0},"t":20,"s":[153.6,276,0],"to":[0,0,0],"ti":[0,0,0]},{"t":30,"s":[153.6,256,0]}],"ix":2},"a":{"a":0,"k":[-102.4,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.6,0.6,0.6],"y":[1,1,1]},"o":{"x":[0.4,0.4,0.4],"y":[0,0,0]},"t":5,"s":[100,100,100]},{"i":{"x":[0.6,0.6,0.6],"y":[1,1,1]},"o":{"x":[0.4,0.4,0.4],"y":[0,0,0]},"t":15,"s":[125,125,100]},{"i":{"x":[0.6,0.6,0.6],"y":[1,1,1]},"o":{"x":[0.4,0.4,0.4],"y":[0,0,0]},"t":25,"s":[95,95,100]},{"t":35,"s":[100,100,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-17.673,0],[0,-17.673],[17.673,0],[0,17.673]],"o":[[17.673,0],[0,17.673],[-17.673,0],[0,-17.673]],"v":[[-102.4,-32],[-70.4,0],[-102.4,32],[-134.4,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,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":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":180,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Point 2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.6,"y":1},"o":{"x":0.4,"y":0},"t":5,"s":[256,256,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.6,"y":1},"o":{"x":0.4,"y":0},"t":15,"s":[256,204,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.6,"y":1},"o":{"x":0.4,"y":0},"t":25,"s":[256,276,0],"to":[0,0,0],"ti":[0,0,0]},{"t":35,"s":[256,256,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.6,0.6,0.6],"y":[1,1,1]},"o":{"x":[0.4,0.4,0.4],"y":[0,0,0]},"t":10,"s":[100,100,100]},{"i":{"x":[0.6,0.6,0.6],"y":[1,1,1]},"o":{"x":[0.4,0.4,0.4],"y":[0,0,0]},"t":20,"s":[125,125,100]},{"i":{"x":[0.6,0.6,0.6],"y":[1,1,1]},"o":{"x":[0.4,0.4,0.4],"y":[0,0,0]},"t":30,"s":[95,95,100]},{"t":40,"s":[100,100,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-17.673,0],[0,-17.673],[17.673,0],[0,17.673]],"o":[[17.673,0],[0,17.673],[-17.673,0],[0,-17.673]],"v":[[0,-32],[32,0],[0,32],[-32,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,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":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":180,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Point 3","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.6,"y":1},"o":{"x":0.4,"y":0},"t":10,"s":[358.4,256,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.6,"y":1},"o":{"x":0.4,"y":0},"t":20,"s":[358.4,204,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.6,"y":1},"o":{"x":0.4,"y":0},"t":30,"s":[358.4,276,0],"to":[0,0,0],"ti":[0,0,0]},{"t":40,"s":[358.4,256,0]}],"ix":2},"a":{"a":0,"k":[102.4,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.6,0.6,0.6],"y":[1,1,1]},"o":{"x":[0.4,0.4,0.4],"y":[0,0,0]},"t":15,"s":[100,100,100]},{"i":{"x":[0.6,0.6,0.6],"y":[1,1,1]},"o":{"x":[0.4,0.4,0.4],"y":[0,0,0]},"t":25,"s":[125,125,100]},{"i":{"x":[0.6,0.6,0.6],"y":[1,1,1]},"o":{"x":[0.4,0.4,0.4],"y":[0,0,0]},"t":35,"s":[95,95,100]},{"t":44,"s":[100,100,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-17.673,0],[0,-17.673],[17.673,0],[0,17.673]],"o":[[17.673,0],[0,17.673],[-17.673,0],[0,-17.673]],"v":[[102.4,-32],[134.4,0],[102.4,32],[70.4,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,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":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":180,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/submodules/TelegramUI/Resources/Animations/anim_profilemute.json b/submodules/TelegramUI/Resources/Animations/anim_profilemute.json new file mode 100644 index 0000000000..dd68bee0b7 --- /dev/null +++ b/submodules/TelegramUI/Resources/Animations/anim_profilemute.json @@ -0,0 +1 @@ +{"v":"5.5.7","meta":{"g":"LottieFiles AE 0.1.20","a":"","k":"","d":"","tc":""},"fr":60,"ip":0,"op":37,"w":512,"h":512,"nm":"Comp 1","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Line","parent":3,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.4,"y":1},"o":{"x":0.3,"y":0},"t":3,"s":[-14.236,-12.181,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.7,"y":1},"o":{"x":0.3,"y":0},"t":17,"s":[18.2,19.9,0],"to":[0,0,0],"ti":[0,0,0]},{"t":25,"s":[6.2,8.4,0]}],"ix":2},"a":{"a":0,"k":[6.2,8.4,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]],"o":[[0,0],[0,0]],"v":[[-102.6,-100.5],[115,117.3]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.4],"y":[1]},"o":{"x":[0.3],"y":[0]},"t":3,"s":[0]},{"t":17,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":0,"k":[0,0,0,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":18,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","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":"Group 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":180,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Middle","parent":3,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[-34.15,23.2,0],"ix":2},"a":{"a":0,"k":[-34.15,23.2,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.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":8,"s":[{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,7.1],[-2.7,2.4],[0,0],[-1.2,16.3]],"o":[[0,0],[0,0],[0,0],[0,0],[-7.1,0],[0,-3.6],[0,0],[12.1,-11.1],[0,0]],"v":[[-78.2,-54.55],[50.226,64.601],[52.497,66.708],[74.8,87.4],[-103.8,87.2],[-116.6,74.4],[-112.4,65],[-104.2,57.5],[-83.6,14.9]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":9,"s":[{"i":[[0,0],[-3.653,-3.294],[-5.525,13.962],[0,0],[0,0],[0,7.1],[-2.7,2.4],[0,0],[-1.2,16.3]],"o":[[0,0],[3.653,3.294],[4.195,2.447],[0,0],[-7.1,0],[0,-3.6],[0,0],[12.1,-11.1],[0,0]],"v":[[-79.034,-46.903],[-53.116,-21.384],[-29.754,-30.355],[79.924,87.426],[-103.8,87.2],[-116.6,74.4],[-112.4,65],[-104.2,57.5],[-83.6,14.9]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":10,"s":[{"i":[[0,0],[-0.456,-0.411],[-8.27,20.578],[0,0],[0,0],[0,7.1],[-2.7,2.4],[0,0],[-1.2,16.3]],"o":[[0,0],[0.456,0.411],[7.125,4.156],[0,0],[-7.1,0],[0,-3.6],[0,0],[12.1,-11.1],[0,0]],"v":[[-79.617,-41.562],[-16.001,19.968],[12.155,10.038],[81.126,87.722],[-103.8,87.2],[-116.6,74.4],[-112.4,65],[-104.2,57.5],[-83.6,14.9]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":11,"s":[{"i":[[0,0],[0,0],[-11.064,22.792],[0,0],[0,0],[0,7.1],[-2.7,2.4],[0,0],[-1.2,16.3]],"o":[[0,0],[0,0],[7.543,4.4],[0,0],[-7.1,0],[0,-3.6],[0,0],[12.1,-11.1],[0,0]],"v":[[-79.7,-40.8],[13.701,50.721],[44.112,42.695],[81.297,87.764],[-103.8,87.2],[-116.6,74.4],[-112.4,65],[-104.2,57.5],[-83.6,14.9]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":12,"s":[{"i":[[0,0],[0,0],[-2.767,-2.505],[0,0],[0,0],[0,7.1],[-2.7,2.4],[0,0],[-1.2,16.3]],"o":[[0,0],[0,0],[6.956,6.247],[0,0],[-7.1,0],[0,-3.6],[0,0],[12.1,-11.1],[0,0]],"v":[[-79.7,-40.8],[27.323,65.744],[35.335,71.99],[53.148,87.283],[-103.8,87.2],[-116.6,74.4],[-112.4,65],[-104.2,57.5],[-83.6,14.9]],"c":true}]},{"t":13,"s":[{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,7.1],[-2.7,2.4],[0,0],[-1.2,16.3]],"o":[[0,0],[0,0],[0,0],[0,0],[-7.1,0],[0,-3.6],[0,0],[12.1,-11.1],[0,0]],"v":[[-79.7,-40.8],[27.741,66.641],[29.641,68.541],[48.3,87.2],[-103.8,87.2],[-116.6,74.4],[-112.4,65],[-104.2,57.5],[-83.6,14.9]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,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":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":7,"op":180,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Top","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.5],"y":[1]},"o":{"x":[0.3],"y":[0]},"t":2,"s":[0]},{"i":{"x":[0.302],"y":[1]},"o":{"x":[0.3],"y":[0]},"t":8,"s":[-3]},{"i":{"x":[0.7],"y":[1]},"o":{"x":[0.3],"y":[0]},"t":19,"s":[5]},{"i":{"x":[0.7],"y":[1]},"o":{"x":[0.3],"y":[0]},"t":28,"s":[-3]},{"t":36,"s":[0]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.4,"y":1},"o":{"x":0.3,"y":0},"t":0,"s":[255.082,261,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.4,"y":1},"o":{"x":0.3,"y":0},"t":6,"s":[255.082,241,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.7,"y":1},"o":{"x":0.3,"y":0},"t":17,"s":[255.082,281,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.7,"y":1},"o":{"x":0.3,"y":0},"t":26,"s":[255.082,247,0],"to":[0,0,0],"ti":[0,0,0]},{"t":34,"s":[255.082,261,0]}],"ix":2},"a":{"a":0,"k":[-0.918,5,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.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":8,"s":[{"i":[[-18.8,0],[-1.1,-17.9],[0,0],[0,0],[-1.4,-19.6],[0,0],[-12.1,-11],[0,0],[4.8,-5.2],[3.6,0],[0,0],[0,0],[0,0],[0,0],[-6.9,2.6],[0,0]],"o":[[18.1,0],[0,0],[0,0],[18.2,7.3],[0,0],[1.2,16.3],[0,0],[5.2,4.8],[-2.4,2.6],[0,0],[0,0],[0,0],[0,0],[3.7,-22.8],[0,0],[-0.1,-18.9]],"v":[[-0.2,-130.4],[33.9,-98.4],[34,-96.2],[46.9,-91],[79,-47.1],[83.4,15],[104,57.6],[112.2,65.1],[113,83.2],[103.6,87.4],[71.672,87.301],[50.558,76.645],[47.845,74.129],[-77.95,-53.7],[-47.1,-91],[-34.2,-96.2]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":9,"s":[{"i":[[-18.8,0],[-1.1,-17.9],[0,0],[0,0],[-1.4,-19.6],[0,0],[-12.1,-11],[0,0],[4.8,-5.2],[3.6,0],[0,0],[2.996,5.407],[0,0],[0,0],[-6.9,2.6],[0,0]],"o":[[18.1,0],[0,0],[0,0],[18.2,7.3],[0,0],[1.2,16.3],[0,0],[5.2,4.8],[-2.4,2.6],[0,0],[0,0],[-3.828,-2.942],[0,0],[2.866,-12.122],[0,0],[-0.1,-18.9]],"v":[[-0.2,-130.4],[33.9,-98.4],[34,-96.2],[46.9,-91],[79,-47.1],[83.4,15],[104,57.6],[112.2,65.1],[113,83.2],[103.6,87.4],[77.248,87.333],[-20.395,-19.416],[-32.09,-33.406],[-68.304,-69.815],[-47.1,-91],[-34.2,-96.2]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":10,"s":[{"i":[[-18.8,0],[-1.1,-17.9],[0,0],[0,0],[-1.4,-19.6],[0,0],[-12.1,-11],[0,0],[4.8,-5.2],[3.6,0],[0,0],[5.088,9.185],[0,0],[0,0],[-6.9,2.6],[0,0]],"o":[[18.1,0],[0,0],[0,0],[18.2,7.3],[0,0],[1.2,16.3],[0,0],[5.2,4.8],[-2.4,2.6],[0,0],[0,0],[1.015,-4.019],[0,0],[2.283,-4.664],[0,0],[-0.1,-18.9]],"v":[[-0.2,-130.4],[33.9,-98.4],[34,-96.2],[46.9,-91],[79,-47.1],[83.4,15],[104,57.6],[112.2,65.1],[113,83.2],[103.6,87.4],[79.481,87.357],[13.203,11.744],[6.417,-2.958],[-67.338,-75.841],[-47.1,-91],[-34.2,-96.2]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":11,"s":[{"i":[[-18.8,0],[-1.1,-17.9],[0,0],[0,0],[-1.4,-19.6],[0,0],[-12.1,-11],[0,0],[4.8,-5.2],[3.6,0],[0,0],[5.386,9.724],[0,0],[0,0],[-6.9,2.6],[0,0]],"o":[[18.1,0],[0,0],[0,0],[18.2,7.3],[0,0],[1.2,16.3],[0,0],[5.2,4.8],[-2.4,2.6],[0,0],[0,0],[4.232,-6.783],[0,0],[2.2,-3.6],[0,0],[-0.1,-18.9]],"v":[[-0.2,-130.4],[33.9,-98.4],[34,-96.2],[46.9,-91],[79,-47.1],[83.4,15],[104,57.6],[112.2,65.1],[113,83.2],[103.6,87.4],[79.8,87.36],[42.716,46.351],[40.339,31.849],[-67.2,-76.7],[-47.1,-91],[-34.2,-96.2]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":12,"s":[{"i":[[-18.8,0],[-1.1,-17.9],[0,0],[0,0],[-1.4,-19.6],[0,0],[-12.1,-11],[0,0],[4.8,-5.2],[3.6,0],[0,0],[0,0],[0,0],[0,0],[-6.9,2.6],[0,0]],"o":[[18.1,0],[0,0],[0,0],[18.2,7.3],[0,0],[1.2,16.3],[0,0],[5.2,4.8],[-2.4,2.6],[0,0],[0,0],[0,0],[0,0],[2.2,-3.6],[0,0],[-0.1,-18.9]],"v":[[-0.2,-130.4],[33.9,-98.4],[34,-96.2],[46.9,-91],[79,-47.1],[83.4,15],[104,57.6],[112.2,65.1],[113,83.2],[103.6,87.4],[93.023,87.394],[76.068,68.529],[72.041,63.833],[-67.2,-76.7],[-47.1,-91],[-34.2,-96.2]],"c":true}]},{"t":13,"s":[{"i":[[-18.8,0],[-1.1,-17.9],[0,0],[0,0],[-1.4,-19.6],[0,0],[-12.1,-11],[0,0],[4.8,-5.2],[3.6,0],[0,0],[0,0],[0,0],[0,0],[-6.9,2.6],[0,0]],"o":[[18.1,0],[0,0],[0,0],[18.2,7.3],[0,0],[1.2,16.3],[0,0],[5.2,4.8],[-2.4,2.6],[0,0],[0,0],[0,0],[0,0],[2.2,-3.6],[0,0],[-0.1,-18.9]],"v":[[-0.2,-130.4],[33.9,-98.4],[34,-96.2],[46.9,-91],[79,-47.1],[83.4,15],[104,57.6],[112.2,65.1],[113,83.2],[103.6,87.4],[95.3,87.4],[79.202,71.144],[76.303,68.216],[-67.2,-76.7],[-47.1,-91],[-34.2,-96.2]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,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":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":7,"op":180,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"Bottom","parent":3,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[-0.2,117.55,0],"ix":2},"a":{"a":0,"k":[-0.2,117.55,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],[17.8,0],[6.4,15.6]],"o":[[-6.4,15.5],[-17.8,0],[0,0]],"v":[[39.3,104.3],[-0.2,130.8],[-39.7,104.3]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,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":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":180,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"EXAMPLE","parent":3,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0.232,0.2,0],"ix":2},"a":{"a":0,"k":[0.232,0.2,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":[[-18.9,0],[-1.1,-17.9],[0,0],[0,0],[-1.4,-19.6],[0,0],[-12.1,-11],[0,0],[4.8,-5.2],[3.6,0],[0,0],[0,7.1],[-2.7,2.4],[0,0],[-1.2,16.3],[0,0],[-18.3,7.3],[0,0]],"o":[[18.1,0],[0,0],[0,0],[18.2,7.3],[0,0],[1.2,16.3],[0,0],[5.2,4.8],[-2.4,2.6],[0,0],[-7.1,0],[0,-3.6],[0,0],[12.1,-11.1],[0,0],[1.4,-19.6],[0,0],[-0.1,-19]],"v":[[0.2,-130.4],[34.3,-98.4],[34.4,-96.2],[47.3,-91],[79.4,-47.1],[83.8,15],[104.4,57.6],[112.6,65.1],[113.4,83.2],[104,87.4],[-103.5,87.4],[-116.3,74.6],[-112.1,65.2],[-103.9,57.7],[-83.3,15.1],[-78.9,-47],[-46.8,-90.9],[-33.9,-96.1]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,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":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":7,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/submodules/TelegramUI/Resources/Animations/anim_profileunmute.json b/submodules/TelegramUI/Resources/Animations/anim_profileunmute.json new file mode 100644 index 0000000000..ebfea4f952 --- /dev/null +++ b/submodules/TelegramUI/Resources/Animations/anim_profileunmute.json @@ -0,0 +1 @@ +{"v":"5.5.7","meta":{"g":"LottieFiles AE 0.1.20","a":"","k":"","d":"","tc":""},"fr":60,"ip":0,"op":37,"w":512,"h":512,"nm":"Comp 1","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Line","parent":3,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.4,"y":1},"o":{"x":0.3,"y":0},"t":1,"s":[6.2,8.4,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.3,"y":1},"o":{"x":0.3,"y":0},"t":7,"s":[17.813,19.808,0],"to":[0,0,0],"ti":[0,0,0]},{"t":18,"s":[-8.985,-6.628,0]}],"ix":2},"a":{"a":0,"k":[6.2,8.4,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]],"o":[[0,0],[0,0]],"v":[[-102.6,-100.5],[115,117.3]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.3],"y":[1]},"o":{"x":[0.3],"y":[0]},"t":7,"s":[100]},{"t":18,"s":[0]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":0,"k":[0,0,0,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":18,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","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":"Group 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":136,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Middle","parent":3,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[-34.15,23.2,0],"ix":2},"a":{"a":0,"k":[-34.15,23.2,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.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":9,"s":[{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,7.1],[-2.7,2.4],[0,0],[-1.2,16.3]],"o":[[0,0],[0,0],[0,0],[0,0],[-7.1,0],[0,-3.6],[0,0],[12.1,-11.1],[0,0]],"v":[[-79.7,-40.8],[27.741,66.641],[29.641,68.541],[48.3,87.2],[-103.8,87.2],[-116.6,74.4],[-112.4,65],[-104.2,57.5],[-83.6,14.9]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":10,"s":[{"i":[[0,0],[0,0],[-11.064,22.792],[0,0],[0,0],[0,7.1],[-2.7,2.4],[0,0],[-1.2,16.3]],"o":[[0,0],[0,0],[7.543,4.4],[0,0],[-7.1,0],[0,-3.6],[0,0],[12.1,-11.1],[0,0]],"v":[[-79.7,-40.8],[13.701,50.721],[44.112,42.695],[81.297,87.764],[-103.8,87.2],[-116.6,74.4],[-112.4,65],[-104.2,57.5],[-83.6,14.9]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":11,"s":[{"i":[[0,0],[-0.456,-0.411],[-7.14,22.508],[0,0],[0,0],[0,7.1],[-2.7,2.4],[0,0],[-1.2,16.3]],"o":[[0,0],[0.456,0.411],[7.125,4.156],[0,0],[-7.1,0],[0,-3.6],[0,0],[12.1,-11.1],[0,0]],"v":[[-79.617,-41.562],[-36.617,-1.022],[-8.461,-10.952],[81.126,87.722],[-103.8,87.2],[-116.6,74.4],[-112.4,65],[-104.2,57.5],[-83.6,14.9]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":12,"s":[{"i":[[0,0],[-0.304,-0.274],[-3.842,19.542],[0,0],[0,0],[0,7.1],[-2.7,2.4],[0,0],[-1.2,16.3]],"o":[[0,0],[0.304,0.274],[4.75,2.771],[0,0],[-7.1,0],[0,-3.6],[0,0],[12.1,-11.1],[0,0]],"v":[[-79.145,-45.891],[-67.552,-34.675],[-46.665,-45.462],[79.017,87.614],[-103.8,87.2],[-116.6,74.4],[-112.4,65],[-104.2,57.5],[-83.6,14.9]],"c":true}]},{"t":13,"s":[{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,7.1],[-2.7,2.4],[0,0],[-1.2,16.3]],"o":[[0,0],[0,0],[0,0],[0,0],[-7.1,0],[0,-3.6],[0,0],[12.1,-11.1],[0,0]],"v":[[-78.2,-54.55],[50.226,64.601],[52.497,66.708],[74.8,87.4],[-103.8,87.2],[-116.6,74.4],[-112.4,65],[-104.2,57.5],[-83.6,14.9]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,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":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":13,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Top","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.4],"y":[1]},"o":{"x":[0.3],"y":[0]},"t":1,"s":[0]},{"i":{"x":[0.3],"y":[1]},"o":{"x":[0.3],"y":[0]},"t":7,"s":[3]},{"i":{"x":[0.4],"y":[1]},"o":{"x":[0.3],"y":[0]},"t":20,"s":[-5]},{"i":{"x":[0.7],"y":[1]},"o":{"x":[0.3],"y":[0]},"t":29,"s":[3]},{"t":36,"s":[0]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.4,"y":1},"o":{"x":0.3,"y":0},"t":0,"s":[255.082,261,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.3,"y":1},"o":{"x":0.3,"y":0},"t":5,"s":[255.082,281,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.4,"y":1},"o":{"x":0.3,"y":0},"t":18,"s":[255.082,237,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.7,"y":1},"o":{"x":0.3,"y":0},"t":27,"s":[255.082,283,0],"to":[0,0,0],"ti":[0,0,0]},{"t":34,"s":[255.082,261,0]}],"ix":2},"a":{"a":0,"k":[-0.918,5,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.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":9,"s":[{"i":[[-18.8,0],[-1.1,-17.9],[0,0],[0,0],[-1.4,-19.6],[0,0],[-12.1,-11],[0,0],[4.8,-5.2],[3.6,0],[0,0],[0,0],[0,0],[0,0],[-6.9,2.6],[0,0]],"o":[[18.1,0],[0,0],[0,0],[18.2,7.3],[0,0],[1.2,16.3],[0,0],[5.2,4.8],[-2.4,2.6],[0,0],[0,0],[0,0],[0,0],[2.2,-3.6],[0,0],[-0.1,-18.9]],"v":[[-0.2,-130.4],[33.9,-98.4],[34,-96.2],[46.9,-91],[79,-47.1],[83.4,15],[104,57.6],[112.2,65.1],[113,83.2],[103.6,87.4],[95.3,87.4],[79.202,71.144],[76.303,68.216],[-67.2,-76.7],[-47.1,-91],[-34.2,-96.2]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":10,"s":[{"i":[[-18.8,0],[-1.1,-17.9],[0,0],[0,0],[-1.4,-19.6],[0,0],[-12.1,-11],[0,0],[4.8,-5.2],[3.6,0],[0,0],[5.386,9.724],[0,0],[0,0],[-6.9,2.6],[0,0]],"o":[[18.1,0],[0,0],[0,0],[18.2,7.3],[0,0],[1.2,16.3],[0,0],[5.2,4.8],[-2.4,2.6],[0,0],[0,0],[4.232,-6.783],[0,0],[2.2,-3.6],[0,0],[-0.1,-18.9]],"v":[[-0.2,-130.4],[33.9,-98.4],[34,-96.2],[46.9,-91],[79,-47.1],[83.4,15],[104,57.6],[112.2,65.1],[113,83.2],[103.6,87.4],[79.8,87.36],[42.716,46.351],[40.339,31.849],[-67.2,-76.7],[-47.1,-91],[-34.2,-96.2]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":11,"s":[{"i":[[-18.8,0],[-1.1,-17.9],[0,0],[0,0],[-1.4,-19.6],[0,0],[-12.1,-11],[0,0],[4.8,-5.2],[3.6,0],[0,0],[5.088,9.185],[0,0],[0,0],[-6.9,2.6],[0,0]],"o":[[18.1,0],[0,0],[0,0],[18.2,7.3],[0,0],[1.2,16.3],[0,0],[5.2,4.8],[-2.4,2.6],[0,0],[0,0],[1.015,-4.019],[0,0],[2.283,-4.664],[0,0],[-0.1,-18.9]],"v":[[-0.2,-130.4],[33.9,-98.4],[34,-96.2],[46.9,-91],[79,-47.1],[83.4,15],[104,57.6],[112.2,65.1],[113,83.2],[103.6,87.4],[79.481,87.357],[-8.726,-11.611],[-10.415,-21.132],[-67.338,-75.841],[-47.1,-91],[-34.2,-96.2]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":12,"s":[{"i":[[-18.8,0],[-1.1,-17.9],[0,0],[0,0],[-1.4,-19.6],[0,0],[-12.1,-11],[0,0],[4.8,-5.2],[3.6,0],[0,0],[3.392,6.123],[0,0],[0,0],[-6.9,2.6],[0,0]],"o":[[18.1,0],[0,0],[0,0],[18.2,7.3],[0,0],[1.2,16.3],[0,0],[5.2,4.8],[-2.4,2.6],[0,0],[0,0],[0.676,-2.679],[0,0],[2.755,-10.709],[0,0],[-0.1,-18.9]],"v":[[-0.2,-130.4],[33.9,-98.4],[34,-96.2],[46.9,-91],[79,-47.1],[83.4,15],[104,57.6],[112.2,65.1],[113,83.2],[103.6,87.4],[77.671,87.338],[10.934,17.854],[2.793,9.156],[-70.875,-68.461],[-47.1,-91],[-34.2,-96.2]],"c":true}]},{"t":13,"s":[{"i":[[-18.8,0],[-1.1,-17.9],[0,0],[0,0],[-1.4,-19.6],[0,0],[-12.1,-11],[0,0],[4.8,-5.2],[3.6,0],[0,0],[0,0],[0,0],[0,0],[-6.9,2.6],[0,0]],"o":[[18.1,0],[0,0],[0,0],[18.2,7.3],[0,0],[1.2,16.3],[0,0],[5.2,4.8],[-2.4,2.6],[0,0],[0,0],[0,0],[0,0],[3.7,-22.8],[0,0],[-0.1,-18.9]],"v":[[-0.2,-130.4],[33.9,-98.4],[34,-96.2],[46.9,-91],[79,-47.1],[83.4,15],[104,57.6],[112.2,65.1],[113,83.2],[103.6,87.4],[74.05,87.3],[58.993,73.332],[56.28,70.816],[-77.95,-53.7],[-47.1,-91],[-34.2,-96.2]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,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":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":13,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"Bottom","parent":3,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[-0.2,117.55,0],"ix":2},"a":{"a":0,"k":[-0.2,117.55,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],[17.8,0],[6.4,15.6]],"o":[[-6.4,15.5],[-17.8,0],[0,0]],"v":[[39.3,104.3],[-0.2,130.8],[-39.7,104.3]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,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":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":180,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"EXAMPLE","parent":3,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0.232,0.2,0],"ix":2},"a":{"a":0,"k":[0.232,0.2,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":[[-18.9,0],[-1.1,-17.9],[0,0],[0,0],[-1.4,-19.6],[0,0],[-12.1,-11],[0,0],[4.8,-5.2],[3.6,0],[0,0],[0,7.1],[-2.7,2.4],[0,0],[-1.2,16.3],[0,0],[-18.3,7.3],[0,0]],"o":[[18.1,0],[0,0],[0,0],[18.2,7.3],[0,0],[1.2,16.3],[0,0],[5.2,4.8],[-2.4,2.6],[0,0],[-7.1,0],[0,-3.6],[0,0],[12.1,-11.1],[0,0],[1.4,-19.6],[0,0],[-0.1,-19]],"v":[[0.2,-130.4],[34.3,-98.4],[34.4,-96.2],[47.3,-91],[79.4,-47.1],[83.8,15],[104.4,57.6],[112.6,65.1],[113.4,83.2],[104,87.4],[-103.5,87.4],[-116.3,74.6],[-112.1,65.2],[-103.9,57.7],[-83.3,15.1],[-78.9,-47],[-46.8,-90.9],[-33.9,-96.1]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,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":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":13,"op":180,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/submodules/TelegramUI/Resources/Animations/anim_profilevc.json b/submodules/TelegramUI/Resources/Animations/anim_profilevc.json new file mode 100644 index 0000000000..66ae10a5fa --- /dev/null +++ b/submodules/TelegramUI/Resources/Animations/anim_profilevc.json @@ -0,0 +1 @@ +{"v":"5.5.7","meta":{"g":"LottieFiles AE 0.1.20","a":"","k":"","d":"","tc":""},"fr":60,"ip":0,"op":27,"w":512,"h":512,"nm":"VoiceChat 2","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Line 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[153.7,256.05,0],"ix":2},"a":{"a":0,"k":[-102.3,0.05,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.6,"y":1},"o":{"x":0.4,"y":0},"t":0,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-102.3,-50.8],[-102.3,50.9]],"c":false}]},{"i":{"x":0.4,"y":1},"o":{"x":0.4,"y":0},"t":11,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-102.1,-133],[-102.1,132.6]],"c":false}]},{"t":23,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-102.3,-50.8],[-102.3,50.9]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0,0,0,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":43,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","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":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":180,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Line 2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[256,256,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.6,"y":1},"o":{"x":0.4,"y":0},"t":2,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0,-115.5],[0,115.5]],"c":false}]},{"i":{"x":0.4,"y":1},"o":{"x":0.4,"y":0},"t":13,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0,-45],[0,45]],"c":false}]},{"t":25,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0,-115.5],[0,115.5]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0,0,0,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":43,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","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":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":180,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Line 3","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[358.5,256.05,0],"ix":2},"a":{"a":0,"k":[102.5,0.05,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.6,"y":1},"o":{"x":0.4,"y":0},"t":4,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[102.5,-50.8],[102.5,50.9]],"c":false}]},{"i":{"x":0.4,"y":1},"o":{"x":0.4,"y":0},"t":15,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[102.5,-131.4],[102.5,131]],"c":false}]},{"t":26,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[102.5,-50.8],[102.5,50.9]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0,0,0,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":43,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","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":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":180,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/submodules/TelegramUI/Resources/Animations/anim_savemedia.tgs b/submodules/TelegramUI/Resources/Animations/anim_savemedia.tgs new file mode 100644 index 0000000000..83f2104743 Binary files /dev/null and b/submodules/TelegramUI/Resources/Animations/anim_savemedia.tgs differ diff --git a/submodules/TelegramUI/Resources/Animations/anim_shareddownload.tgs b/submodules/TelegramUI/Resources/Animations/anim_shareddownload.tgs new file mode 100644 index 0000000000..51384f078e Binary files /dev/null and b/submodules/TelegramUI/Resources/Animations/anim_shareddownload.tgs differ diff --git a/submodules/TelegramUI/Resources/PresentationStrings.mapping b/submodules/TelegramUI/Resources/PresentationStrings.mapping index 567c315a5f..72fa0b81fc 100644 Binary files a/submodules/TelegramUI/Resources/PresentationStrings.mapping and b/submodules/TelegramUI/Resources/PresentationStrings.mapping differ diff --git a/submodules/TelegramUI/Resources/currencies.json b/submodules/TelegramUI/Resources/currencies.json index 5e332a88fa..93730660d1 100644 --- a/submodules/TelegramUI/Resources/currencies.json +++ b/submodules/TelegramUI/Resources/currencies.json @@ -267,7 +267,7 @@ "decimalSeparator": ",", "symbolOnLeft": true, "spaceBetweenAmountAndSymbol": true, - "decimalDigits": 2 + "decimalDigits": 0 }, "CNY": { "code": "CNY", @@ -1356,7 +1356,7 @@ "decimalSeparator": ",", "symbolOnLeft": false, "spaceBetweenAmountAndSymbol": true, - "decimalDigits": 1 + "decimalDigits": 0 }, "VUV": { "code": "VUV", diff --git a/submodules/TelegramUI/Resources/fqv01SQemVIBAAAApND8LDRUhRU.tgv b/submodules/TelegramUI/Resources/fqv01SQemVIBAAAApND8LDRUhRU.tgv new file mode 100644 index 0000000000..a106f042d6 Binary files /dev/null and b/submodules/TelegramUI/Resources/fqv01SQemVIBAAAApND8LDRUhRU.tgv differ diff --git a/submodules/TelegramUI/Sounds/MessageSent.caf b/submodules/TelegramUI/Sounds/MessageSent.caf deleted file mode 100644 index 68164c0ea9..0000000000 Binary files a/submodules/TelegramUI/Sounds/MessageSent.caf and /dev/null differ diff --git a/submodules/TelegramUI/Sounds/MessageSent.mp3 b/submodules/TelegramUI/Sounds/MessageSent.mp3 new file mode 100644 index 0000000000..0a25e8bf23 Binary files /dev/null and b/submodules/TelegramUI/Sounds/MessageSent.mp3 differ diff --git a/submodules/TelegramUI/Sounds/notification.caf b/submodules/TelegramUI/Sounds/notification.caf deleted file mode 100644 index f3f16ae66f..0000000000 Binary files a/submodules/TelegramUI/Sounds/notification.caf and /dev/null differ diff --git a/submodules/TelegramUI/Sounds/notification.mp3 b/submodules/TelegramUI/Sounds/notification.mp3 new file mode 100644 index 0000000000..46480d6da1 Binary files /dev/null and b/submodules/TelegramUI/Sounds/notification.mp3 differ diff --git a/submodules/TelegramUI/Sources/AccessoryPanelNode.swift b/submodules/TelegramUI/Sources/AccessoryPanelNode.swift index 067fe48fc8..28f7651ef3 100644 --- a/submodules/TelegramUI/Sources/AccessoryPanelNode.swift +++ b/submodules/TelegramUI/Sources/AccessoryPanelNode.swift @@ -4,6 +4,7 @@ import AsyncDisplayKit import TelegramPresentationData class AccessoryPanelNode: ASDisplayNode { + var originalFrameBeforeDismissed: CGRect? var dismiss: (() -> Void)? var interfaceInteraction: ChatPanelInterfaceInteraction? diff --git a/submodules/TelegramUI/Sources/AccountContext.swift b/submodules/TelegramUI/Sources/AccountContext.swift index 2429cb67fe..ad8e0acac8 100644 --- a/submodules/TelegramUI/Sources/AccountContext.swift +++ b/submodules/TelegramUI/Sources/AccountContext.swift @@ -109,9 +109,10 @@ public final class AccountContextImpl: AccountContext { return self.sharedContextImpl } public let account: Account + public let engine: TelegramEngine public let fetchManager: FetchManager - private let prefetchManager: PrefetchManager? + public let prefetchManager: PrefetchManager? public var keyShortcutsController: KeyShortcutsController? @@ -156,21 +157,22 @@ public final class AccountContextImpl: AccountContext { public let cachedGroupCallContexts: AccountGroupCallContextCache - public init(sharedContext: SharedAccountContextImpl, account: Account, /*tonContext: StoredTonContext?, */limitsConfiguration: LimitsConfiguration, contentSettings: ContentSettings, appConfiguration: AppConfiguration, temp: Bool = false) + public init(sharedContext: SharedAccountContextImpl, account: Account, limitsConfiguration: LimitsConfiguration, contentSettings: ContentSettings, appConfiguration: AppConfiguration, temp: Bool = false) { self.sharedContextImpl = sharedContext self.account = account + self.engine = TelegramEngine(account: account) self.downloadedMediaStoreManager = DownloadedMediaStoreManagerImpl(postbox: account.postbox, accountManager: sharedContext.accountManager) if let locationManager = self.sharedContextImpl.locationManager { - self.liveLocationManager = LiveLocationManagerImpl(postbox: account.postbox, network: account.network, accountPeerId: account.peerId, viewTracker: account.viewTracker, stateManager: account.stateManager, locationManager: locationManager, inForeground: sharedContext.applicationBindings.applicationInForeground) + self.liveLocationManager = LiveLocationManagerImpl(engine: self.engine, account: account, locationManager: locationManager, inForeground: sharedContext.applicationBindings.applicationInForeground) } else { self.liveLocationManager = nil } self.fetchManager = FetchManagerImpl(postbox: account.postbox, storeManager: self.downloadedMediaStoreManager) if sharedContext.applicationBindings.isMainApp && !temp { - self.prefetchManager = PrefetchManager(sharedContext: sharedContext, account: account, fetchManager: self.fetchManager) + self.prefetchManager = PrefetchManagerImpl(sharedContext: sharedContext, account: account, engine: self.engine, fetchManager: self.fetchManager) self.wallpaperUploadManager = WallpaperUploadManagerImpl(sharedContext: sharedContext, account: account, presentationData: sharedContext.presentationData) self.themeUpdateManager = ThemeUpdateManagerImpl(sharedContext: sharedContext, account: account) } else { @@ -180,7 +182,7 @@ public final class AccountContextImpl: AccountContext { } if let locationManager = self.sharedContextImpl.locationManager, sharedContext.applicationBindings.isMainApp && !temp { - self.peersNearbyManager = PeersNearbyManagerImpl(account: account, locationManager: locationManager, inForeground: sharedContext.applicationBindings.applicationInForeground) + self.peersNearbyManager = PeersNearbyManagerImpl(account: account, engine: self.engine, locationManager: locationManager, inForeground: sharedContext.applicationBindings.applicationInForeground) } else { self.peersNearbyManager = nil } @@ -236,7 +238,7 @@ public final class AccountContextImpl: AccountContext { }) } - account.callSessionManager.updateVersions(versions: PresentationCallManagerImpl.voipVersions(includeExperimental: true, includeReference: false).map { version, supportsVideo -> CallSessionManagerImplementationVersion in + account.callSessionManager.updateVersions(versions: PresentationCallManagerImpl.voipVersions(includeExperimental: true, includeReference: sharedContext.immediateExperimentalUISettings.experimentalCompatibility).map { version, supportsVideo -> CallSessionManagerImplementationVersion in CallSessionManagerImplementationVersion(version: version, supportsVideo: supportsVideo) }) } @@ -293,13 +295,17 @@ public final class AccountContextImpl: AccountContext { public func applyMaxReadIndex(for location: ChatLocation, contextHolder: Atomic, messageIndex: MessageIndex) { switch location { case .peer: - let _ = applyMaxReadIndexInteractively(postbox: self.account.postbox, stateManager: self.account.stateManager, index: messageIndex).start() + let _ = self.engine.messages.applyMaxReadIndexInteractively(index: messageIndex).start() case let .replyThread(data): let context = chatLocationContext(holder: contextHolder, account: self.account, data: data) context.applyMaxReadIndex(messageIndex: messageIndex) } } + public func scheduleGroupCall(peerId: PeerId) { + let _ = self.sharedContext.callManager?.scheduleGroupCall(context: self, peerId: peerId, endCurrentIfAny: true) + } + public func joinGroupCall(peerId: PeerId, invite: String?, requestJoinAsPeerId: ((@escaping (PeerId?) -> Void) -> Void)?, activeCall: CachedChannelData.ActiveCall) { let callResult = self.sharedContext.callManager?.joinGroupCall(context: self, peerId: peerId, invite: invite, requestJoinAsPeerId: requestJoinAsPeerId, initialCall: activeCall, endCurrentIfAny: false) if let callResult = callResult, case let .alreadyInProgress(currentPeerId) = callResult { diff --git a/submodules/TelegramUI/Sources/AppDelegate.swift b/submodules/TelegramUI/Sources/AppDelegate.swift index e4b387d2d8..91aec62095 100644 --- a/submodules/TelegramUI/Sources/AppDelegate.swift +++ b/submodules/TelegramUI/Sources/AppDelegate.swift @@ -32,33 +32,48 @@ import TelegramIntents import AccountUtils import CoreSpotlight import LightweightAccountData - -#if canImport(BackgroundTasks) +import TelegramAudio +import DebugSettingsUI import BackgroundTasks + +#if canImport(AppCenter) +import AppCenter +import AppCenterCrashes #endif private let handleVoipNotifications = false private var testIsLaunched = false -private func encodeText(_ string: String, _ key: Int) -> String { - var result = "" - for c in string.unicodeScalars { - result.append(Character(UnicodeScalar(UInt32(Int(c.value) + key))!)) +private func isKeyboardWindow(window: NSObject) -> Bool { + let typeName = NSStringFromClass(type(of: window)) + if #available(iOS 9.0, *) { + if typeName.hasPrefix("UI") && typeName.hasSuffix("RemoteKeyboardWindow") { + return true + } + } else { + if typeName.hasPrefix("UI") && typeName.hasSuffix("TextEffectsWindow") { + return true + } } - return result + return false } -private let keyboardViewClass: AnyClass? = NSClassFromString(encodeText("VJJoqvuTfuIptuWjfx", -1))! -private let keyboardViewContainerClass: AnyClass? = NSClassFromString(encodeText("VJJoqvuTfuDpoubjofsWjfx", -1))! - -private let keyboardWindowClass: AnyClass? = { - if #available(iOS 9.0, *) { - return NSClassFromString(encodeText("VJSfnpufLfzcpbseXjoepx", -1)) - } else { - return NSClassFromString(encodeText("VJUfyuFggfdutXjoepx", -1)) +private func isKeyboardView(view: NSObject) -> Bool { + let typeName = NSStringFromClass(type(of: view)) + if typeName.hasPrefix("UI") && typeName.hasSuffix("InputSetHostView") { + return true } -}() + return false +} + +private func isKeyboardViewContainer(view: NSObject) -> Bool { + let typeName = NSStringFromClass(type(of: view)) + if typeName.hasPrefix("UI") && typeName.hasSuffix("InputSetContainerView") { + return true + } + return false +} private class ApplicationStatusBarHost: StatusBarHost { private let application = UIApplication.shared @@ -84,20 +99,20 @@ private class ApplicationStatusBarHost: StatusBarHost { } func setStatusBarStyle(_ style: UIStatusBarStyle, animated: Bool) { - self.application.setStatusBarStyle(style, animated: animated) + if self.shouldChangeStatusBarStyle?(style) ?? true { + self.application.setStatusBarStyle(style, animated: animated) + } } + var shouldChangeStatusBarStyle: ((UIStatusBarStyle) -> Bool)? + func setStatusBarHidden(_ value: Bool, animated: Bool) { self.application.setStatusBarHidden(value, with: animated ? .fade : .none) } var keyboardWindow: UIWindow? { - guard let keyboardWindowClass = keyboardWindowClass else { - return nil - } - for window in UIApplication.shared.windows { - if window.isKind(of: keyboardWindowClass) { + if isKeyboardWindow(window: window) { return window } } @@ -105,14 +120,14 @@ private class ApplicationStatusBarHost: StatusBarHost { } var keyboardView: UIView? { - guard let keyboardWindow = self.keyboardWindow, let keyboardViewContainerClass = keyboardViewContainerClass, let keyboardViewClass = keyboardViewClass else { + guard let keyboardWindow = self.keyboardWindow else { return nil } for view in keyboardWindow.subviews { - if view.isKind(of: keyboardViewContainerClass) { + if isKeyboardViewContainer(view: view) { for subview in view.subviews { - if subview.isKind(of: keyboardViewClass) { + if isKeyboardView(view: subview) { return subview } } @@ -276,13 +291,13 @@ final class SharedApplicationContext { var peerId: PeerId? if let fromId = payload["from_id"] { let fromIdValue = fromId as! NSString - peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: Int32(fromIdValue.intValue)) + peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt32Value(Int32(fromIdValue.intValue))) } else if let fromId = payload["chat_id"] { let fromIdValue = fromId as! NSString - peerId = PeerId(namespace: Namespaces.Peer.CloudGroup, id: Int32(fromIdValue.intValue)) + peerId = PeerId(namespace: Namespaces.Peer.CloudGroup, id: PeerId.Id._internalFromInt32Value(Int32(fromIdValue.intValue))) } else if let fromId = payload["channel_id"] { let fromIdValue = fromId as! NSString - peerId = PeerId(namespace: Namespaces.Peer.CloudChannel, id: Int32(fromIdValue.intValue)) + peerId = PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt32Value(Int32(fromIdValue.intValue))) } if let msgId = payload["msg_id"] { @@ -433,11 +448,16 @@ final class SharedApplicationContext { let deviceSpecificEncryptionParameters = BuildConfig.deviceSpecificEncryptionParameters(rootPath, baseAppBundleId: baseAppBundleId) let encryptionParameters = ValueBoxEncryptionParameters(forceEncryptionIfNoSet: false, key: ValueBoxEncryptionParameters.Key(data: deviceSpecificEncryptionParameters.key)!, salt: ValueBoxEncryptionParameters.Salt(data: deviceSpecificEncryptionParameters.salt)!) - TempBox.initializeShared(basePath: rootPath, processType: "app", launchSpecificId: arc4random64()) + TempBox.initializeShared(basePath: rootPath, processType: "app", launchSpecificId: Int64.random(in: Int64.min ... Int64.max)) let logsPath = rootPath + "/logs" let _ = try? FileManager.default.createDirectory(atPath: logsPath, withIntermediateDirectories: true, attributes: nil) Logger.setSharedLogger(Logger(rootPath: rootPath, basePath: logsPath)) + + setManagedAudioSessionLogger({ s in + Logger.shared.log("ManagedAudioSession", s) + Logger.shared.shortLog("ManagedAudioSession", s) + }) if let contents = try? FileManager.default.contentsOfDirectory(at: URL(fileURLWithPath: rootPath + "/accounts-metadata"), includingPropertiesForKeys: nil, options: [.skipsSubdirectoryDescendants]) { for url in contents { @@ -479,14 +499,7 @@ final class SharedApplicationContext { telegramUIDeclareEncodables() GlobalExperimentalSettings.isAppStoreBuild = buildConfig.isAppStoreBuild - GlobalExperimentalSettings.enableFeed = false - #if DEBUG - //GlobalExperimentalSettings.enableFeed = true - #if targetEnvironment(simulator) - //GlobalTelegramCoreConfiguration.readMessages = false - #endif - #endif self.window?.makeKeyAndVisible() @@ -494,7 +507,7 @@ final class SharedApplicationContext { initializeAccountManagement() - let applicationBindings = TelegramApplicationBindings(isMainApp: true, containerPath: appGroupUrl.path, appSpecificScheme: buildConfig.appSpecificUrlScheme, openUrl: { url in + let applicationBindings = TelegramApplicationBindings(isMainApp: true, appBundleId: baseAppBundleId, containerPath: appGroupUrl.path, appSpecificScheme: buildConfig.appSpecificUrlScheme, openUrl: { url in var parsedUrl = URL(string: url) if let parsed = parsedUrl { if parsed.scheme == nil || parsed.scheme!.isEmpty { @@ -633,6 +646,8 @@ final class SharedApplicationContext { }, getAvailableAlternateIcons: { if #available(iOS 10.3, *) { var icons = [PresentationAppIcon(name: "Blue", imageName: "BlueIcon", isDefault: buildConfig.isAppStoreBuild), + PresentationAppIcon(name: "New2", imageName: "New2_180x180"), + PresentationAppIcon(name: "New1", imageName: "New1_180x180"), PresentationAppIcon(name: "Black", imageName: "BlackIcon"), PresentationAppIcon(name: "BlueClassic", imageName: "BlueClassicIcon"), PresentationAppIcon(name: "BlackClassic", imageName: "BlackClassicIcon"), @@ -662,6 +677,10 @@ final class SharedApplicationContext { } else { completion(false) } + }, forceOrientation: { orientation in + let value = orientation.rawValue + UIDevice.current.setValue(value, forKey: "orientation") + UINavigationController.attemptRotationToDeviceOrientation() }) let accountManagerSignal = Signal { subscriber in @@ -774,7 +793,6 @@ final class SharedApplicationContext { self.mainWindow?.hostView.containerView.backgroundColor = initialPresentationDataAndSettings.presentationData.theme.chatList.backgroundColor let legacyBasePath = appGroupUrl.path - let legacyCache = LegacyCache(path: legacyBasePath + "/Caches") let presentationDataPromise = Promise() let appLockContext = AppLockContextImpl(rootPath: rootPath, window: self.mainWindow!, rootController: self.window?.rootViewController, applicationBindings: applicationBindings, accountManager: accountManager, presentationDataSignal: presentationDataPromise.get(), lockIconInitialFrame: { @@ -782,7 +800,7 @@ final class SharedApplicationContext { }) var setPresentationCall: ((PresentationCall?) -> Void)? - let sharedContext = SharedAccountContextImpl(mainWindow: self.mainWindow, basePath: rootPath, encryptionParameters: encryptionParameters, accountManager: accountManager, appLockContext: appLockContext, applicationBindings: applicationBindings, initialPresentationDataAndSettings: initialPresentationDataAndSettings, networkArguments: networkArguments, rootPath: rootPath, legacyBasePath: legacyBasePath, legacyCache: legacyCache, apsNotificationToken: self.notificationTokenPromise.get() |> map(Optional.init), voipNotificationToken: self.voipTokenPromise.get() |> map(Optional.init), setNotificationCall: { call in + let sharedContext = SharedAccountContextImpl(mainWindow: self.mainWindow, sharedContainerPath: legacyBasePath, basePath: rootPath, encryptionParameters: encryptionParameters, accountManager: accountManager, appLockContext: appLockContext, applicationBindings: applicationBindings, initialPresentationDataAndSettings: initialPresentationDataAndSettings, networkArguments: networkArguments, rootPath: rootPath, legacyBasePath: legacyBasePath, apsNotificationToken: self.notificationTokenPromise.get() |> map(Optional.init), voipNotificationToken: self.voipTokenPromise.get() |> map(Optional.init), setNotificationCall: { call in setPresentationCall?(call) }, navigateToChat: { accountId, peerId, messageId in self.openChatWhenReady(accountId: accountId, peerId: peerId, messageId: messageId) @@ -803,6 +821,15 @@ final class SharedApplicationContext { } } }) + + /*self.mainWindow.debugAction = { + self.mainWindow.debugAction = nil + + let presentationData = sharedContext.currentPresentationData.with { $0 } + let navigationController = NavigationController(mode: .single, theme: NavigationControllerTheme(presentationTheme: presentationData.theme)) + navigationController.viewControllers = [debugController(sharedContext: sharedContext, context: nil)] + self.mainWindow.present(navigationController, on: .root) + }*/ presentationDataPromise.set(sharedContext.presentationData) @@ -832,7 +859,7 @@ final class SharedApplicationContext { } var exists = false strongSelf.mainWindow.forEachViewController({ controller in - if controller is ThemeSettingsCrossfadeController || controller is ThemeSettingsController { + if controller is ThemeSettingsCrossfadeController || controller is ThemeSettingsController || controller is ThemePreviewController { exists = true } return true @@ -1073,6 +1100,8 @@ final class SharedApplicationContext { print("Application: context took \(readyTime) to become ready") } print("Launch to ready took \((CFAbsoluteTimeGetCurrent() - launchStartTime) * 1000.0) ms") + + self.mainWindow.debugAction = nil self.mainWindow.viewController = context.rootController if firstTime { @@ -1312,6 +1341,14 @@ final class SharedApplicationContext { }*/ self.maybeCheckForUpdates() + + #if canImport(AppCenter) + if !buildConfig.isAppStoreBuild, let appCenterId = buildConfig.appCenterId, !appCenterId.isEmpty { + AppCenter.start(withAppSecret: buildConfig.appCenterId, services: [ + Crashes.self + ]) + } + #endif return true } @@ -1672,7 +1709,7 @@ final class SharedApplicationContext { if let startCallContacts = startCallContacts { let startCall: (Int32) -> Void = { userId in - self.startCallWhenReady(accountId: nil, peerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: userId), isVideo: startCallIsVideo) + self.startCallWhenReady(accountId: nil, peerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt32Value(userId)), isVideo: startCallIsVideo) } func cleanPhoneNumber(_ text: String) -> String { @@ -1734,7 +1771,7 @@ final class SharedApplicationContext { return result } |> deliverOnMainQueue).start(next: { peerId in if let peerId = peerId { - startCall(peerId.id) + startCall(peerId.id._internalGetInt32Value()) } }) processed = true @@ -1749,7 +1786,7 @@ final class SharedApplicationContext { 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) { - self.openChatWhenReady(accountId: nil, peerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: userId), activateInput: true) + self.openChatWhenReady(accountId: nil, peerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt32Value(userId)), activateInput: true) } } } @@ -1950,9 +1987,9 @@ final class SharedApplicationContext { |> deliverOnMainQueue |> mapToSignal { account -> Signal in if let messageId = messageIdFromNotification(peerId: peerId, notification: response.notification) { - let _ = applyMaxReadIndexInteractively(postbox: account.postbox, stateManager: account.stateManager, index: MessageIndex(id: messageId, timestamp: 0)).start() + let _ = TelegramEngine(account: account).messages.applyMaxReadIndexInteractively(index: MessageIndex(id: messageId, timestamp: 0)).start() } - return enqueueMessages(account: account, peerId: peerId, messages: [EnqueueMessage.message(text: text, attributes: [], mediaReference: nil, replyToMessageId: nil, localGroupingKey: nil)]) + return enqueueMessages(account: account, peerId: peerId, messages: [EnqueueMessage.message(text: text, attributes: [], mediaReference: nil, replyToMessageId: nil, localGroupingKey: nil, correlationId: nil)]) |> map { messageIds -> MessageId? in if messageIds.isEmpty { return nil @@ -2293,7 +2330,7 @@ private func accountIdFromNotification(_ notification: UNNotification, sharedCon |> take(1) |> map { _, accounts, _ -> AccountRecordId? in for (_, account, _) in accounts { - if Int(account.peerId.id) == userId { + if Int(account.peerId.id._internalGetInt32Value()) == userId { return account.id } } @@ -2315,16 +2352,16 @@ private func peerIdFromNotification(_ notification: UNNotification) -> PeerId? { var peerId: PeerId? if let fromId = payload["from_id"] { let fromIdValue = fromId as! NSString - peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: Int32(fromIdValue.intValue)) + peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt32Value(Int32(fromIdValue.intValue))) } else if let fromId = payload["chat_id"] { let fromIdValue = fromId as! NSString - peerId = PeerId(namespace: Namespaces.Peer.CloudGroup, id: Int32(fromIdValue.intValue)) + peerId = PeerId(namespace: Namespaces.Peer.CloudGroup, id: PeerId.Id._internalFromInt32Value(Int32(fromIdValue.intValue))) } else if let fromId = payload["channel_id"] { let fromIdValue = fromId as! NSString - peerId = PeerId(namespace: Namespaces.Peer.CloudChannel, id: Int32(fromIdValue.intValue)) + peerId = PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt32Value(Int32(fromIdValue.intValue))) } else if let fromId = payload["encryption_id"] { let fromIdValue = fromId as! NSString - peerId = PeerId(namespace: Namespaces.Peer.SecretChat, id: Int32(fromIdValue.intValue)) + peerId = PeerId(namespace: Namespaces.Peer.SecretChat, id: PeerId.Id._internalFromInt32Value(Int32(fromIdValue.intValue))) } return peerId } diff --git a/submodules/TelegramUI/Sources/ApplicationContext.swift b/submodules/TelegramUI/Sources/ApplicationContext.swift index e401742013..20adc4ffc6 100644 --- a/submodules/TelegramUI/Sources/ApplicationContext.swift +++ b/submodules/TelegramUI/Sources/ApplicationContext.swift @@ -36,6 +36,8 @@ final class UnauthorizedApplicationContext { let isReady = Promise() var authorizationCompleted: Bool = false + + private var serviceNotificationEventsDisposable: Disposable? init(apiId: Int32, apiHash: String, sharedContext: SharedAccountContextImpl, account: UnauthorizedAccount, otherAccountPhoneNumbers: ((String, AccountRecordId, Bool)?, [(String, AccountRecordId, Bool)])) { self.sharedContext = sharedContext @@ -71,6 +73,20 @@ final class UnauthorizedApplicationContext { }, { result in ApplicationSpecificNotice.setPermissionWarning(accountManager: sharedContext.accountManager, permission: .cellularData, value: 0) }) + + self.serviceNotificationEventsDisposable = (account.serviceNotificationEvents + |> deliverOnMainQueue).start(next: { [weak self] text in + if let strongSelf = self { + let presentationData = strongSelf.sharedContext.currentPresentationData.with { $0 } + let alertController = textAlertController(sharedContext: strongSelf.sharedContext, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]) + + (strongSelf.rootController.viewControllers.last as? ViewController)?.present(alertController, in: .window(.root)) + } + }) + } + + deinit { + self.serviceNotificationEventsDisposable?.dispose() } } @@ -356,7 +372,7 @@ final class AuthorizedApplicationContext { if inAppNotificationSettings.displayPreviews { let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } - strongSelf.notificationController.enqueue(ChatMessageNotificationItem(context: strongSelf.context, strings: presentationData.strings, nameDisplayOrder: presentationData.nameDisplayOrder, messages: messages, tapAction: { + strongSelf.notificationController.enqueue(ChatMessageNotificationItem(context: strongSelf.context, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, messages: messages, tapAction: { if let strongSelf = self { var foundOverlay = false strongSelf.mainWindow.forEachViewController({ controller in @@ -445,11 +461,11 @@ final class AuthorizedApplicationContext { guard let strongSelf = self else { return } - let _ = (acceptTermsOfService(account: strongSelf.context.account, id: termsOfServiceUpdate.id) + let _ = (strongSelf.context.engine.accountData.acceptTermsOfService(id: termsOfServiceUpdate.id) |> deliverOnMainQueue).start(completed: { controller?.dismiss() if let strongSelf = self, let botName = botName { - strongSelf.termsOfServiceProceedToBotDisposable.set((resolvePeerByName(account: strongSelf.context.account, name: botName, ageLimit: 10) |> take(1) |> deliverOnMainQueue).start(next: { peerId in + strongSelf.termsOfServiceProceedToBotDisposable.set((strongSelf.context.engine.peers.resolvePeerByName(name: botName, ageLimit: 10) |> take(1) |> deliverOnMainQueue).start(next: { peerId in if let strongSelf = self, let peerId = peerId { self?.rootController.pushViewController(ChatControllerImpl(context: strongSelf.context, chatLocation: .peer(peerId))) } @@ -464,7 +480,7 @@ final class AuthorizedApplicationContext { } let accountId = strongSelf.context.account.id let accountManager = strongSelf.context.sharedContext.accountManager - let _ = (deleteAccount(account: strongSelf.context.account) + let _ = (strongSelf.context.engine.auth.deleteAccount() |> deliverOnMainQueue).start(error: { _ in guard let strongSelf = self else { return @@ -743,7 +759,7 @@ final class AuthorizedApplicationContext { } let navigateToMessage = { - strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: strongSelf.rootController, context: strongSelf.context, chatLocation: .peer(messageId.peerId), subject: .message(id: messageId, highlight: true))) + strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: strongSelf.rootController, context: strongSelf.context, chatLocation: .peer(messageId.peerId), subject: .message(id: messageId, highlight: true, timecode: nil))) } if chatIsVisible { @@ -763,6 +779,7 @@ final class AuthorizedApplicationContext { self.rootController.setForceInCallStatusBar((self.context.sharedContext as! SharedAccountContextImpl).currentCallStatusBarNode) if let groupCallController = self.context.sharedContext.currentGroupCallController as? VoiceChatController { if let overlayController = groupCallController.currentOverlayController { + groupCallController.parentNavigationController = self.rootController self.rootController.presentOverlay(controller: overlayController, inGlobal: true, blockInteraction: false) } } @@ -821,7 +838,7 @@ final class AuthorizedApplicationContext { if visiblePeerId != peerId || messageId != nil { if self.rootController.rootTabController != nil { - self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: self.rootController, context: self.context, chatLocation: .peer(peerId), subject: messageId.flatMap { .message(id: $0, highlight: true) }, activateInput: activateInput)) + self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: self.rootController, context: self.context, chatLocation: .peer(peerId), subject: messageId.flatMap { .message(id: $0, highlight: true, timecode: nil) }, activateInput: activateInput)) } else { self.scheduledOpenChatWithPeerId = (peerId, messageId, activateInput) } diff --git a/submodules/TelegramUI/Sources/ApplicationShortcutItem.swift b/submodules/TelegramUI/Sources/ApplicationShortcutItem.swift index ad690d29d0..6579fe691b 100644 --- a/submodules/TelegramUI/Sources/ApplicationShortcutItem.swift +++ b/submodules/TelegramUI/Sources/ApplicationShortcutItem.swift @@ -1,6 +1,7 @@ import Foundation import UIKit import TelegramPresentationData +import DeviceAccess enum ApplicationShortcutItemType: String { case search @@ -44,12 +45,18 @@ func applicationShortcutItems(strings: PresentationStrings, otherAccountName: St ApplicationShortcutItem(type: .savedMessages, title: strings.Conversation_SavedMessages, subtitle: nil), ApplicationShortcutItem(type: .account, title: strings.Shortcut_SwitchAccount, subtitle: otherAccountName) ] - } else { + } else if DeviceAccess.isCameraAccessAuthorized() { 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) ] + } else { + 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) + ] } } diff --git a/submodules/TelegramUI/Sources/AudioRecordningToneData.swift b/submodules/TelegramUI/Sources/AudioRecordningToneData.swift index 36fc878c01..1241bb3d97 100644 --- a/submodules/TelegramUI/Sources/AudioRecordningToneData.swift +++ b/submodules/TelegramUI/Sources/AudioRecordningToneData.swift @@ -11,7 +11,7 @@ private func loadAudioRecordingToneData() -> Data? { AVLinearPCMIsBigEndianKey: false as NSNumber ] - guard let url = Bundle.main.url(forResource: "begin_record", withExtension: "caf") else { + guard let url = Bundle.main.url(forResource: "begin_record", withExtension: "mp3") else { return nil } diff --git a/submodules/TelegramUI/Sources/AuthorizationSequenceAwaitingAccountResetController.swift b/submodules/TelegramUI/Sources/AuthorizationSequenceAwaitingAccountResetController.swift index 9f5b6e184c..360fe2ba62 100644 --- a/submodules/TelegramUI/Sources/AuthorizationSequenceAwaitingAccountResetController.swift +++ b/submodules/TelegramUI/Sources/AuthorizationSequenceAwaitingAccountResetController.swift @@ -85,7 +85,7 @@ final class AuthorizationSequenceAwaitingAccountResetController: ViewController override 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.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition) } @objc func logoutPressed() { diff --git a/submodules/TelegramUI/Sources/AuthorizationSequenceCodeEntryController.swift b/submodules/TelegramUI/Sources/AuthorizationSequenceCodeEntryController.swift index 8841d92eb3..c3df7fed14 100644 --- a/submodules/TelegramUI/Sources/AuthorizationSequenceCodeEntryController.swift +++ b/submodules/TelegramUI/Sources/AuthorizationSequenceCodeEntryController.swift @@ -119,7 +119,7 @@ final class AuthorizationSequenceCodeEntryController: ViewController { override 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.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition) } @objc func nextPressed() { diff --git a/submodules/TelegramUI/Sources/AuthorizationSequenceController.swift b/submodules/TelegramUI/Sources/AuthorizationSequenceController.swift index 99808d4147..8d1675a6a4 100644 --- a/submodules/TelegramUI/Sources/AuthorizationSequenceController.swift +++ b/submodules/TelegramUI/Sources/AuthorizationSequenceController.swift @@ -17,6 +17,7 @@ import SettingsUI import PhoneNumberFormat import LegacyComponents import LegacyMediaPickerUI +import PasswordSetupUI private enum InnerState: Equatable { case state(UnauthorizedAccountStateContents) @@ -25,7 +26,7 @@ private enum InnerState: Equatable { public final class AuthorizationSequenceController: NavigationController, MFMailComposeViewControllerDelegate { static func navigationBarTheme(_ theme: PresentationTheme) -> NavigationBarTheme { - return NavigationBarTheme(buttonColor: theme.intro.accentTextColor, disabledButtonColor: theme.intro.disabledTextColor, primaryTextColor: theme.intro.primaryTextColor, backgroundColor: .clear, separatorColor: .clear, badgeBackgroundColor: theme.rootController.navigationBar.badgeBackgroundColor, badgeStrokeColor: theme.rootController.navigationBar.badgeStrokeColor, badgeTextColor: theme.rootController.navigationBar.badgeTextColor) + return NavigationBarTheme(buttonColor: theme.intro.accentTextColor, disabledButtonColor: theme.intro.disabledTextColor, primaryTextColor: theme.intro.primaryTextColor, backgroundColor: .clear, enableBackgroundBlur: false, separatorColor: .clear, badgeBackgroundColor: theme.rootController.navigationBar.badgeBackgroundColor, badgeStrokeColor: theme.rootController.navigationBar.badgeStrokeColor, badgeTextColor: theme.rootController.navigationBar.badgeTextColor) } private let sharedContext: SharedAccountContext @@ -110,7 +111,7 @@ 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.presentationData.theme) + controller = AuthorizationSequenceSplashController(accountManager: self.sharedContext.accountManager, account: self.account, theme: self.presentationData.theme) controller.nextPressed = { [weak self] strings in if let strongSelf = self { if let strings = strings { @@ -177,14 +178,14 @@ public final class AuthorizationSequenceController: NavigationController, MFMail controller.inProgress = false let text: String - var actions: [TextAlertAction] = [ - TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {}) - ] + var actions: [TextAlertAction] = [] switch error { case .limitExceeded: text = strongSelf.presentationData.strings.Login_CodeFloodError + actions.append(TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})) case .invalidPhoneNumber: text = strongSelf.presentationData.strings.Login_InvalidPhoneError + actions.append(TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_OK, action: {})) actions.append(TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Login_PhoneNumberHelp, action: { [weak controller] in guard let strongSelf = self, let controller = controller else { return @@ -200,8 +201,10 @@ public final class AuthorizationSequenceController: NavigationController, MFMail })) case .phoneLimitExceeded: text = strongSelf.presentationData.strings.Login_PhoneFloodError + actions.append(TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})) case .phoneBanned: text = strongSelf.presentationData.strings.Login_PhoneBannedError + actions.append(TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_OK, action: {})) actions.append(TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Login_PhoneNumberHelp, action: { [weak controller] in guard let strongSelf = self, let controller = controller else { return @@ -217,6 +220,7 @@ public final class AuthorizationSequenceController: NavigationController, MFMail })) case let .generic(info): text = strongSelf.presentationData.strings.Login_UnknownError + actions.append(TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_OK, action: {})) actions.append(TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Login_PhoneNumberHelp, action: { [weak controller] in guard let strongSelf = self, let controller = controller else { return @@ -238,6 +242,7 @@ public final class AuthorizationSequenceController: NavigationController, MFMail })) case .timeout: text = strongSelf.presentationData.strings.Login_NetworkError + actions.append(TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})) 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 @@ -481,26 +486,26 @@ public final class AuthorizationSequenceController: NavigationController, MFMail controller.forgot = { [weak self, weak controller] in if let strongSelf = self, let strongController = controller { strongController.inProgress = true - strongSelf.actionDisposable.set((requestPasswordRecovery(account: strongSelf.account) - |> deliverOnMainQueue).start(next: { option in + strongSelf.actionDisposable.set((TelegramEngineUnauthorized(account: strongSelf.account).auth.requestTwoStepVerificationPasswordRecoveryCode() + |> deliverOnMainQueue).start(next: { pattern in if let strongSelf = self, let strongController = controller { strongController.inProgress = false - switch option { - case let .email(pattern): - let _ = (strongSelf.account.postbox.transaction { transaction -> Void in - if let state = transaction.getState() as? UnauthorizedAccountState, case let .passwordEntry(hint, number, code, _, syncContacts) = state.contents { - transaction.setState(UnauthorizedAccountState(isTestingEnvironment: strongSelf.account.testingEnvironment, masterDatacenterId: strongSelf.account.masterDatacenterId, contents: .passwordRecovery(hint: hint, number: number, code: code, emailPattern: pattern, syncContacts: syncContacts))) - } - }).start() - case .none: - 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 - } + + let _ = (strongSelf.account.postbox.transaction { transaction -> Void in + if let state = transaction.getState() as? UnauthorizedAccountState, case let .passwordEntry(hint, number, code, _, syncContacts) = state.contents { + transaction.setState(UnauthorizedAccountState(isTestingEnvironment: strongSelf.account.testingEnvironment, masterDatacenterId: strongSelf.account.masterDatacenterId, contents: .passwordRecovery(hint: hint, number: number, code: code, emailPattern: pattern, syncContacts: syncContacts))) + } + }).start() } }, error: { error in - if let strongController = controller { - strongController.inProgress = false + guard let strongController = controller else { + return } + + strongController.inProgress = false + + 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 })) } } @@ -537,66 +542,32 @@ public final class AuthorizationSequenceController: NavigationController, MFMail return controller } - private func passwordRecoveryController(emailPattern: String, syncContacts: Bool) -> AuthorizationSequencePasswordRecoveryController { - var currentController: AuthorizationSequencePasswordRecoveryController? + private func passwordRecoveryController(emailPattern: String, syncContacts: Bool) -> TwoFactorDataInputScreen { + var currentController: TwoFactorDataInputScreen? for c in self.viewControllers { - if let c = c as? AuthorizationSequencePasswordRecoveryController { + if let c = c as? TwoFactorDataInputScreen { currentController = c break } } - let controller: AuthorizationSequencePasswordRecoveryController + let controller: TwoFactorDataInputScreen if let currentController = currentController { controller = currentController } else { - controller = AuthorizationSequencePasswordRecoveryController(strings: self.presentationData.strings, theme: self.presentationData.theme, back: { [weak self] in - guard let strongSelf = self else { - return - } - let countryCode = defaultCountryCode() - - let _ = (strongSelf.account.postbox.transaction { transaction -> Void in - transaction.setState(UnauthorizedAccountState(isTestingEnvironment: strongSelf.account.testingEnvironment, masterDatacenterId: strongSelf.account.masterDatacenterId, contents: .phoneEntry(countryCode: countryCode, number: ""))) - }).start() - }) - controller.recoverWithCode = { [weak self, weak controller] code in - if let strongSelf = self { - controller?.inProgress = true - - strongSelf.actionDisposable.set((performPasswordRecovery(accountManager: strongSelf.sharedContext.accountManager, account: strongSelf.account, code: code, syncContacts: syncContacts) |> deliverOnMainQueue).start(error: { error in - Queue.mainQueue().async { - if let strongSelf = self, let controller = controller { - controller.inProgress = false - - let text: String - switch error { - case .limitExceeded: - text = strongSelf.presentationData.strings.LoginPassword_FloodError - case .invalidCode: - text = strongSelf.presentationData.strings.Login_InvalidCodeError - case .expired: - text = strongSelf.presentationData.strings.Login_CodeExpiredError - } - - 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.noAccess = { [weak self, weak controller] in - if let strongSelf = self, let controller = controller { - 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 { - transaction.setState(UnauthorizedAccountState(isTestingEnvironment: account.testingEnvironment, masterDatacenterId: account.masterDatacenterId, contents: .passwordEntry(hint: hint, number: number, code: code, suggestReset: true, syncContacts: syncContacts))) - } - }).start() - } - } + controller = TwoFactorDataInputScreen(sharedContext: self.sharedContext, engine: .unauthorized(TelegramEngineUnauthorized(account: self.account)), mode: .passwordRecoveryEmail(emailPattern: emailPattern, mode: .notAuthorized(syncContacts: syncContacts)), stateUpdated: { _ in + }, presentation: .default) + } + controller.passwordRecoveryFailed = { [weak self] in + guard let strongSelf = self else { + return + } + + let _ = (strongSelf.account.postbox.transaction { transaction -> Void in + if let state = transaction.getState() as? UnauthorizedAccountState, case let .passwordRecovery(hint, number, code, _, syncContacts) = state.contents { + transaction.setState(UnauthorizedAccountState(isTestingEnvironment: strongSelf.account.testingEnvironment, masterDatacenterId: strongSelf.account.masterDatacenterId, contents: .passwordEntry(hint: hint, number: number, code: code, suggestReset: true, syncContacts: syncContacts))) + } + }).start() } - controller.updateData(emailPattern: emailPattern) return controller } @@ -714,7 +685,7 @@ public final class AuthorizationSequenceController: NavigationController, MFMail var value = stat() if stat(result.fileURL.path, &value) == 0 { if let data = try? Data(contentsOf: result.fileURL) { - let resource = LocalFileMediaResource(fileId: arc4random64()) + let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) account.postbox.mediaBox.storeResourceData(resource.id, data: data, synchronous: true) subscriber.putNext(resource) } @@ -734,7 +705,7 @@ public final class AuthorizationSequenceController: NavigationController, MFMail } |> mapToSignal { resource -> Signal in if let resource = resource { - return uploadedPeerVideo(postbox: account.postbox, network: account.network, messageMediaPreuploadManager: nil, resource: resource) |> map(Optional.init) + return TelegramEngineUnauthorized(account: account).auth.uploadedPeerVideo(resource: resource) |> map(Optional.init) } else { return .single(nil) } @@ -813,7 +784,7 @@ public final class AuthorizationSequenceController: NavigationController, MFMail } controllers.append(self.passwordEntryController(hint: hint, suggestReset: suggestReset, syncContacts: syncContacts)) self.setViewControllers(controllers, animated: !self.viewControllers.isEmpty) - case let .passwordRecovery(_, _, _, emailPattern, syncContacts): + case let .passwordRecovery(hint, _, _, emailPattern, syncContacts): var controllers: [ViewController] = [] if !self.otherAccountPhoneNumbers.1.isEmpty { controllers.append(self.splashController()) diff --git a/submodules/TelegramUI/Sources/AuthorizationSequencePasswordEntryController.swift b/submodules/TelegramUI/Sources/AuthorizationSequencePasswordEntryController.swift index 1f40397b78..76cac93c42 100644 --- a/submodules/TelegramUI/Sources/AuthorizationSequencePasswordEntryController.swift +++ b/submodules/TelegramUI/Sources/AuthorizationSequencePasswordEntryController.swift @@ -116,7 +116,7 @@ final class AuthorizationSequencePasswordEntryController: ViewController { override 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.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition) } @objc func nextPressed() { @@ -129,9 +129,9 @@ final class AuthorizationSequencePasswordEntryController: ViewController { } func forgotPressed() { - if self.suggestReset { + /*if self.suggestReset { 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 { + } else*/ if self.didForgotWithNoRecovery { 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/Sources/AuthorizationSequencePasswordRecoveryController.swift b/submodules/TelegramUI/Sources/AuthorizationSequencePasswordRecoveryController.swift index 4b9a2bc5a4..7897059ee7 100644 --- a/submodules/TelegramUI/Sources/AuthorizationSequencePasswordRecoveryController.swift +++ b/submodules/TelegramUI/Sources/AuthorizationSequencePasswordRecoveryController.swift @@ -93,7 +93,7 @@ final class AuthorizationSequencePasswordRecoveryController: ViewController { override 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.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition) } @objc func nextPressed() { diff --git a/submodules/TelegramUI/Sources/AuthorizationSequencePhoneEntryController.swift b/submodules/TelegramUI/Sources/AuthorizationSequencePhoneEntryController.swift index 9ad2b45410..e8d01d97ed 100644 --- a/submodules/TelegramUI/Sources/AuthorizationSequencePhoneEntryController.swift +++ b/submodules/TelegramUI/Sources/AuthorizationSequencePhoneEntryController.swift @@ -12,6 +12,7 @@ import AccountContext import CountrySelectionUI import SettingsUI import PhoneNumberFormat +import DebugSettingsUI final class AuthorizationSequencePhoneEntryController: ViewController { private var controllerNode: AuthorizationSequencePhoneEntryControllerNode { @@ -139,7 +140,7 @@ final class AuthorizationSequencePhoneEntryController: ViewController { self?.nextPressed() } - loadServerCountryCodes(accountManager: sharedContext.accountManager, network: account.network, completion: { [weak self] in + loadServerCountryCodes(accountManager: sharedContext.accountManager, engine: TelegramEngineUnauthorized(account: self.account), completion: { [weak self] in if let strongSelf = self { strongSelf.controllerNode.updateCountryCode() } @@ -161,7 +162,7 @@ final class AuthorizationSequencePhoneEntryController: ViewController { override 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.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition) } @objc func nextPressed() { diff --git a/submodules/TelegramUI/Sources/AuthorizationSequencePhoneEntryControllerNode.swift b/submodules/TelegramUI/Sources/AuthorizationSequencePhoneEntryControllerNode.swift index d6320febd5..cf198be15b 100644 --- a/submodules/TelegramUI/Sources/AuthorizationSequencePhoneEntryControllerNode.swift +++ b/submodules/TelegramUI/Sources/AuthorizationSequencePhoneEntryControllerNode.swift @@ -438,14 +438,14 @@ final class AuthorizationSequencePhoneEntryControllerNode: ASDisplayNode { |> 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 (_, activeAccounts, _) = activeAccountsAndInfo + let activeProductionUserIds = activeAccounts.map({ $0.1 }).filter({ !$0.testingEnvironment }).map({ $0.peerId.id }) + let 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) + return TelegramEngineUnauthorized(account: account).auth.exportAuthTransferToken(accountManager: sharedContext.accountManager, otherAccountUserIds: account.testingEnvironment ? allTestingUserIds : allProductionUserIds, syncContacts: true) } self.exportTokenDisposable.set((tokenSignal diff --git a/submodules/TelegramUI/Sources/AuthorizationSequenceSignUpController.swift b/submodules/TelegramUI/Sources/AuthorizationSequenceSignUpController.swift index 6b5a18f1b6..ddba34b80a 100644 --- a/submodules/TelegramUI/Sources/AuthorizationSequenceSignUpController.swift +++ b/submodules/TelegramUI/Sources/AuthorizationSequenceSignUpController.swift @@ -138,7 +138,7 @@ final class AuthorizationSequenceSignUpController: ViewController { override 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.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition) } @objc func nextPressed() { diff --git a/submodules/TelegramUI/Sources/AuthorizationSequenceSplashController.swift b/submodules/TelegramUI/Sources/AuthorizationSequenceSplashController.swift index 184601231f..26cc9a83cd 100644 --- a/submodules/TelegramUI/Sources/AuthorizationSequenceSplashController.swift +++ b/submodules/TelegramUI/Sources/AuthorizationSequenceSplashController.swift @@ -17,8 +17,7 @@ final class AuthorizationSequenceSplashController: ViewController { } private let accountManager: AccountManager - private let postbox: Postbox - private let network: Network + private let account: UnauthorizedAccount private let theme: PresentationTheme private let controller: RMIntroViewController @@ -30,14 +29,13 @@ final class AuthorizationSequenceSplashController: ViewController { private let suggestedLocalization = Promise() private let activateLocalizationDisposable = MetaDisposable() - init(accountManager: AccountManager, postbox: Postbox, network: Network, theme: PresentationTheme) { + init(accountManager: AccountManager, account: UnauthorizedAccount, theme: PresentationTheme) { self.accountManager = accountManager - self.postbox = postbox - self.network = network + self.account = account self.theme = theme self.suggestedLocalization.set(.single(nil) - |> then(currentlySuggestedLocalization(network: network, extractKeys: ["Login.ContinueWithLocalization"]))) + |> then(TelegramEngineUnauthorized(account: self.account).localization.currentlySuggestedLocalization(extractKeys: ["Login.ContinueWithLocalization"]))) let suggestedLocalization = self.suggestedLocalization let localizationSignal = SSignal(generator: { subscriber in @@ -176,7 +174,7 @@ final class AuthorizationSequenceSplashController: ViewController { } if let suggestedCode = suggestedCode { - _ = markSuggestedLocalizationAsSeenInteractively(postbox: strongSelf.postbox, languageCode: suggestedCode).start() + _ = TelegramEngineUnauthorized(account: strongSelf.account).localization.markSuggestedLocalizationAsSeenInteractively(languageCode: suggestedCode).start() } if currentCode == code { @@ -186,9 +184,8 @@ final class AuthorizationSequenceSplashController: ViewController { strongSelf.controller.isEnabled = false let accountManager = strongSelf.accountManager - let postbox = strongSelf.postbox - strongSelf.activateLocalizationDisposable.set(downloadAndApplyLocalization(accountManager: accountManager, postbox: postbox, network: strongSelf.network, languageCode: code).start(completed: { + strongSelf.activateLocalizationDisposable.set(TelegramEngineUnauthorized(account: strongSelf.account).localization.downloadAndApplyLocalization(accountManager: accountManager, languageCode: code).start(completed: { let _ = (accountManager.transaction { transaction -> PresentationStrings? in let localizationSettings: LocalizationSettings? if let current = transaction.getSharedData(SharedDataKeys.localizationSettings) as? LocalizationSettings { diff --git a/submodules/TelegramUI/Sources/ChatChannelSubscriberInputPanelNode.swift b/submodules/TelegramUI/Sources/ChatChannelSubscriberInputPanelNode.swift index 66d808929d..8c7a5d526d 100644 --- a/submodules/TelegramUI/Sources/ChatChannelSubscriberInputPanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatChannelSubscriberInputPanelNode.swift @@ -38,7 +38,7 @@ private func titleAndColorForAction(_ action: SubscriberAction, theme: Presentat } } -private func actionForPeer(peer: Peer, interfaceState: ChatPresentationInterfaceState, isMuted: Bool) -> SubscriberAction? { +private func actionForPeer(peer: Peer, interfaceState: ChatPresentationInterfaceState, isJoining: Bool, isMuted: Bool) -> SubscriberAction? { if case .pinnedMessages = interfaceState.subject { var canManagePin = false if let channel = peer as? TelegramChannel { @@ -64,6 +64,13 @@ private func actionForPeer(peer: Peer, interfaceState: ChatPresentationInterface } } else { if let channel = peer as? TelegramChannel { + if case .broadcast = channel.info, isJoining { + if isMuted { + return .unmuteNotifications + } else { + return .muteNotifications + } + } switch channel.participationStatus { case .kicked: return .kicked @@ -102,10 +109,11 @@ final class ChatChannelSubscriberInputPanelNode: ChatInputPanelNode { private let actionDisposable = MetaDisposable() private let badgeDisposable = MetaDisposable() + private var isJoining: Bool = false private var presentationInterfaceState: ChatPresentationInterfaceState? - private var layoutData: (CGFloat, CGFloat, CGFloat)? + private var layoutData: (CGFloat, CGFloat, CGFloat, UIEdgeInsets, CGFloat, Bool, LayoutMetrics)? override init() { self.button = HighlightableButtonNode() @@ -168,14 +176,34 @@ 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, hash: nil) + var delayActivity = false + if let peer = peer as? TelegramChannel, case .broadcast = peer.info { + delayActivity = true + } + + if delayActivity { + Queue.mainQueue().after(1.5) { + if self.isJoining { + self.activityIndicator.isHidden = false + self.activityIndicator.startAnimating() + } + } + } else { + self.activityIndicator.isHidden = false + self.activityIndicator.startAnimating() + } + + self.isJoining = true + if let (width, leftInset, rightInset, additionalSideInsets, maxHeight, isSecondary, metrics) = self.layoutData, let presentationInterfaceState = self.presentationInterfaceState { + let _ = self.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, additionalSideInsets: additionalSideInsets, maxHeight: maxHeight, isSecondary: isSecondary, transition: .immediate, interfaceState: presentationInterfaceState, metrics: metrics, force: true) + } + self.actionDisposable.set((context.peerChannelMemberCategoriesContextsManager.join(engine: context.engine, peerId: peer.id, hash: nil) |> afterDisposed { [weak self] in Queue.mainQueue().async { if let strongSelf = self { strongSelf.activityIndicator.isHidden = true strongSelf.activityIndicator.stopAnimating() + strongSelf.isJoining = false } } }).start(error: { [weak self] error in @@ -206,7 +234,7 @@ final class ChatChannelSubscriberInputPanelNode: ChatInputPanelNode { 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()) + self.actionDisposable.set(context.engine.peers.togglePeerMuted(peerId: peer.id).start()) } case .hidePinnedMessages, .unpinMessages: self.interfaceInteraction?.unpinAllMessages() @@ -220,9 +248,13 @@ final class ChatChannelSubscriberInputPanelNode: ChatInputPanelNode { } override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, additionalSideInsets: UIEdgeInsets, maxHeight: CGFloat, isSecondary: Bool, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics) -> CGFloat { - self.layoutData = (width, leftInset, rightInset) + return self.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, additionalSideInsets: additionalSideInsets, maxHeight: maxHeight, isSecondary: isSecondary, transition: transition, interfaceState: interfaceState, metrics: metrics, force: false) + } + + private func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, additionalSideInsets: UIEdgeInsets, maxHeight: CGFloat, isSecondary: Bool, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics, force: Bool) -> CGFloat { + self.layoutData = (width, leftInset, rightInset, additionalSideInsets, maxHeight, isSecondary, metrics) - if self.presentationInterfaceState != interfaceState { + if self.presentationInterfaceState != interfaceState || force { let previousState = self.presentationInterfaceState self.presentationInterfaceState = interfaceState @@ -231,16 +263,17 @@ final class ChatChannelSubscriberInputPanelNode: ChatInputPanelNode { self.helpButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/Help"), color: interfaceState.theme.chat.inputPanel.panelControlAccentColor), for: .normal) } - if let peer = interfaceState.renderedPeer?.peer, previousState?.renderedPeer?.peer == nil || !peer.isEqual(previousState!.renderedPeer!.peer!) || previousState?.theme !== interfaceState.theme || previousState?.strings !== interfaceState.strings || previousState?.peerIsMuted != interfaceState.peerIsMuted || previousState?.pinnedMessage != interfaceState.pinnedMessage { + if let peer = interfaceState.renderedPeer?.peer, previousState?.renderedPeer?.peer == nil || !peer.isEqual(previousState!.renderedPeer!.peer!) || previousState?.theme !== interfaceState.theme || previousState?.strings !== interfaceState.strings || previousState?.peerIsMuted != interfaceState.peerIsMuted || previousState?.pinnedMessage != interfaceState.pinnedMessage || force { - if let action = actionForPeer(peer: peer, interfaceState: interfaceState, isMuted: interfaceState.peerIsMuted) { + if let action = actionForPeer(peer: peer, interfaceState: interfaceState, isJoining: self.isJoining, isMuted: interfaceState.peerIsMuted) { let previousAction = self.action self.action = action let (title, color) = titleAndColorForAction(action, theme: interfaceState.theme, strings: interfaceState.strings) var offset: CGFloat = 30.0 - if let previousAction = previousAction, previousAction == .muteNotifications && action == .unmuteNotifications || previousAction == .unmuteNotifications && action == .muteNotifications { - if previousAction == .muteNotifications { + + if let previousAction = previousAction, [.join, .muteNotifications].contains(previousAction) && action == .unmuteNotifications || [.join, .unmuteNotifications].contains(previousAction) && action == .muteNotifications { + if [.join, .muteNotifications].contains(previousAction) { offset *= -1.0 } if let snapshotView = self.button.view.snapshotContentTree() { diff --git a/submodules/TelegramUI/Sources/ChatContextResultPeekContentNode.swift b/submodules/TelegramUI/Sources/ChatContextResultPeekContentNode.swift index 4c2703271f..eaced07470 100644 --- a/submodules/TelegramUI/Sources/ChatContextResultPeekContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatContextResultPeekContentNode.swift @@ -9,13 +9,14 @@ import SwiftSignalKit import AVFoundation import PhotoResources import AppBundle +import ContextUI final class ChatContextResultPeekContent: PeekControllerContent { let account: Account let contextResult: ChatContextResult - let menu: [PeekControllerMenuItem] + let menu: [ContextMenuItem] - init(account: Account, contextResult: ChatContextResult, menu: [PeekControllerMenuItem]) { + init(account: Account, contextResult: ChatContextResult, menu: [ContextMenuItem]) { self.account = account self.contextResult = contextResult self.menu = menu @@ -25,11 +26,11 @@ final class ChatContextResultPeekContent: PeekControllerContent { return .contained } - func menuActivation() -> PeerkControllerMenuActivation { + func menuActivation() -> PeerControllerMenuActivation { return .drag } - func menuItems() -> [PeekControllerMenuItem] { + func menuItems() -> [ContextMenuItem] { return self.menu } @@ -162,7 +163,7 @@ private final class ChatContextResultPeekNode: ASDisplayNode, PeekControllerCont imageDimensions = externalReference.content?.dimensions?.cgSize if let content = externalReference.content, externalReference.type == "gif", let thumbnailResource = imageResource , let dimensions = content.dimensions { - videoFileReference = .standalone(media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: content.resource, previewRepresentations: [TelegramMediaImageRepresentation(dimensions: dimensions, resource: thumbnailResource, progressiveSizes: [])], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: dimensions, flags: [])])) + videoFileReference = .standalone(media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: content.resource, previewRepresentations: [TelegramMediaImageRepresentation(dimensions: dimensions, resource: thumbnailResource, progressiveSizes: [], immediateThumbnailData: nil)], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: dimensions, flags: [])])) imageResource = nil } case let .internalReference(internalReference): @@ -224,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, progressiveSizes: []) + let tmpRepresentation = TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: Int32(fittedImageDimensions.width * 2.0), height: Int32(fittedImageDimensions.height * 2.0)), resource: imageResource, progressiveSizes: [], immediateThumbnailData: 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 { diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index f39b397e6a..85de671d29 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -47,7 +47,6 @@ import RaiseToListen import UrlHandling import ReactionSelectionNode import AvatarNode -import MessageReactionListUI import AppBundle import LocalizedPeerData import PhoneNumberFormat @@ -64,6 +63,12 @@ import ChatHistoryImportTasks import Markdown import TelegramPermissionsUI import Speak +import UniversalMediaPlayer +import WallpaperBackgroundNode + +#if DEBUG +import os.signpost +#endif extension ChatLocation { var peerId: PeerId { @@ -101,13 +106,13 @@ private enum ChatRecordingActivity { } public enum NavigateToMessageLocation { - case id(MessageId) + case id(MessageId, Double?) case index(MessageIndex) case upperBound(PeerId) var messageId: MessageId? { switch self { - case let .id(id): + case let .id(id, _): return id case let .index(index): return index.id @@ -118,7 +123,7 @@ public enum NavigateToMessageLocation { var peerId: PeerId { switch self { - case let .id(id): + case let .id(id, _): return id.peerId case let .index(index): return index.id.peerId @@ -173,6 +178,37 @@ enum ChatLoadingMessageSubject { case pinnedMessage } +#if DEBUG +private final class SignpostData { + @available(iOSApplicationExtension 12.0, iOS 12.0, *) + final class Impl { + let signpostLog: OSLog + let signpostId: OSSignpostID + + init() { + self.signpostLog = OSLog( + subsystem: "org.telegram.Telegram-iOS", + category: "ChatAppear" + ) + self.signpostId = OSSignpostID(log: self.signpostLog) + } + } + + private static var _impl: AnyObject? = { + if #available(iOSApplicationExtension 12.0, iOS 12.0, *) { + return Impl() + } else { + return nil + } + }() + + @available(iOSApplicationExtension 12.0, iOS 12.0, *) + static var impl: Impl { + return self._impl! as! Impl + } +} +#endif + public final class ChatControllerImpl: TelegramBaseController, ChatController, GalleryHiddenMediaTarget, UIDropInteractionDelegate { private var validLayout: ContainerViewLayout? @@ -199,6 +235,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G private let cachedDataReady = Promise() private var didSetCachedDataReady = false + + private let wallpaperReady = Promise() private var presentationInterfaceState: ChatPresentationInterfaceState private var presentationInterfaceStatePromise: ValuePromise @@ -214,7 +252,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G private let galleryHiddenMesageAndMediaDisposable = MetaDisposable() private let temporaryHiddenGalleryMediaDisposable = MetaDisposable() - + + private let chatBackgroundNode: WallpaperBackgroundNode private var controllerInteraction: ChatControllerInteraction? private var interfaceInteraction: ChatPanelInterfaceInteraction? @@ -355,6 +394,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G private weak var sendMessageActionsController: ChatSendMessageActionSheetController? private var searchResultsController: ChatSearchResultsController? + + private weak var currentPinchController: PinchController? + private weak var currentPinchSourceItemNode: ListViewItemNode? private var screenCaptureManager: ScreenCaptureDetectionManager? private let chatAdditionalDataDisposable = MetaDisposable() @@ -414,7 +456,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G private var importStateDisposable: Disposable? - public init(context: AccountContext, chatLocation: ChatLocation, chatLocationContextHolder: Atomic = Atomic(value: nil), subject: ChatControllerSubject? = nil, botStart: ChatControllerInitialBotStart? = nil, mode: ChatControllerPresentationMode = .standard(previewing: false), peekData: ChatPeekTimeout? = nil, peerNearbyData: ChatPeerNearbyData? = nil, greetingData: ChatGreetingData? = nil) { + public init(context: AccountContext, chatLocation: ChatLocation, chatLocationContextHolder: Atomic = Atomic(value: nil), subject: ChatControllerSubject? = nil, botStart: ChatControllerInitialBotStart? = nil, mode: ChatControllerPresentationMode = .standard(previewing: false), peekData: ChatPeekTimeout? = nil, peerNearbyData: ChatPeerNearbyData? = nil) { let _ = ChatControllerCount.modify { value in return value + 1 } @@ -425,6 +467,16 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G self.subject = subject self.botStart = botStart self.peekData = peekData + + var useSharedAnimationPhase = false + switch mode { + case .standard(false): + useSharedAnimationPhase = true + default: + break + } + self.chatBackgroundNode = WallpaperBackgroundNode(context: context, useSharedAnimationPhase: useSharedAnimationPhase) + self.wallpaperReady.set(self.chatBackgroundNode.isReady) var locationBroadcastPanelSource: LocationBroadcastPanelSource var groupCallPanelSource: GroupCallPanelSource @@ -459,7 +511,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G self.stickerSettings = ChatInterfaceStickerSettings(loopAnimatedStickers: false) - 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, subject: subject, peerNearbyData: peerNearbyData, greetingData: greetingData, pendingUnpinnedAllMessages: false, activeGroupCallInfo: nil, hasActiveGroupCall: false, importState: nil) + 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, subject: subject, peerNearbyData: peerNearbyData, greetingData: context.prefetchManager?.preloadedGreetingSticker, pendingUnpinnedAllMessages: false, activeGroupCallInfo: nil, hasActiveGroupCall: false, importState: nil) self.presentationInterfaceStatePromise = ValuePromise(self.presentationInterfaceState) var mediaAccessoryPanelVisibility = MediaAccessoryPanelVisibility.none @@ -514,12 +566,18 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G var openMessageByAction: Bool = false for media in message.media { + if let file = media as? TelegramMediaFile, file.isInstantVideo { + if strongSelf.chatDisplayNode.isInputViewFocused { + strongSelf.returnInputViewFocus = true + strongSelf.chatDisplayNode.dismissInput() + } + } if let action = media as? TelegramMediaAction { switch action.action { case .pinnedMessageUpdated: for attribute in message.attributes { if let attribute = attribute as? ReplyMessageAttribute { - strongSelf.navigateToMessage(from: message.id, to: .id(attribute.messageId)) + strongSelf.navigateToMessage(from: message.id, to: .id(attribute.messageId, nil)) break } } @@ -528,13 +586,13 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G case .gameScore: for attribute in message.attributes { if let attribute = attribute as? ReplyMessageAttribute { - strongSelf.navigateToMessage(from: message.id, to: .id(attribute.messageId)) + strongSelf.navigateToMessage(from: message.id, to: .id(attribute.messageId, nil)) break } } case .groupPhoneCall, .inviteToGroupPhoneCall: if let activeCall = strongSelf.presentationInterfaceState.activeGroupCallInfo?.activeCall { - strongSelf.joinGroupCall(peerId: message.id.peerId, invite: nil, activeCall: CachedChannelData.ActiveCall(id: activeCall.id, accessHash: activeCall.accessHash, title: activeCall.title)) + strongSelf.joinGroupCall(peerId: message.id.peerId, invite: nil, activeCall: CachedChannelData.ActiveCall(id: activeCall.id, accessHash: activeCall.accessHash, title: activeCall.title, scheduleTimestamp: activeCall.scheduleTimestamp, subscribedToScheduled: activeCall.subscribedToScheduled)) } else { var canManageGroupCalls = false if let channel = strongSelf.presentationInterfaceState.renderedPeer?.chatMainPeer as? TelegramChannel { @@ -552,7 +610,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } if canManageGroupCalls { - strongSelf.present(textAlertController(context: strongSelf.context, title: nil, text: strongSelf.presentationData.strings.VoiceChat_CreateNewVoiceChatText, actions: [TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.VoiceChat_CreateNewVoiceChatStart, action: { + strongSelf.present(textAlertController(context: strongSelf.context, title: nil, text: strongSelf.presentationData.strings.VoiceChat_CreateNewVoiceChatText, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.VoiceChat_CreateNewVoiceChatStartNow, action: { if let strongSelf = self { var dismissStatus: (() -> Void)? let statusController = OverlayStatusController(theme: strongSelf.presentationData.theme, type: .loading(cancelled: { @@ -563,12 +621,12 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G statusController?.dismiss() } strongSelf.present(statusController, in: .window(.root)) - strongSelf.createVoiceChatDisposable.set((createGroupCall(account: strongSelf.context.account, peerId: message.id.peerId) + strongSelf.createVoiceChatDisposable.set((strongSelf.context.engine.calls.createGroupCall(peerId: message.id.peerId, title: nil, scheduleDate: nil) |> deliverOnMainQueue).start(next: { [weak self] info in guard let strongSelf = self else { return } - strongSelf.joinGroupCall(peerId: message.id.peerId, invite: nil, activeCall: CachedChannelData.ActiveCall(id: info.id, accessHash: info.accessHash, title: info.title)) + strongSelf.joinGroupCall(peerId: message.id.peerId, invite: nil, activeCall: CachedChannelData.ActiveCall(id: info.id, accessHash: info.accessHash, title: info.title, scheduleTimestamp: info.scheduleTimestamp, subscribedToScheduled: info.subscribedToScheduled)) }, error: { [weak self] error in dismissStatus?() @@ -578,7 +636,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G let text: String switch error { - case .generic: + case .generic, .scheduledTooLate: text = strongSelf.presentationData.strings.Login_UnknownError case .anonymousNotAllowed: text = strongSelf.presentationData.strings.VoiceChat_AnonymousDisabledAlertText @@ -588,7 +646,11 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G dismissStatus?() })) } - })]), in: .window(.root)) + }), TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.VoiceChat_CreateNewVoiceChatSchedule, action: { + if let strongSelf = self { + strongSelf.context.scheduleGroupCall(peerId: message.id.peerId) + } + }), TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {})], actionLayout: .vertical), in: .window(.root)) } } return true @@ -618,6 +680,15 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if canSetupAutoremoveTimeout { strongSelf.presentAutoremoveSetup() } + case .paymentSent: + strongSelf.present(BotReceiptController(context: strongSelf.context, messageId: message.id), in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + /*for attribute in message.attributes { + if let attribute = attribute as? ReplyMessageAttribute { + //strongSelf.navigateToMessage(from: message.id, to: .id(attribute.messageId)) + break + } + }*/ + return true default: break } @@ -664,7 +735,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G }, enqueueMessage: { message in self?.sendMessages([message]) }, sendSticker: canSendMessagesToChat(strongSelf.presentationInterfaceState) ? { fileReference, sourceNode, sourceRect in - return self?.controllerInteraction?.sendSticker(fileReference, nil, false, sourceNode, sourceRect) ?? false + return self?.controllerInteraction?.sendSticker(fileReference, false, false, nil, false, sourceNode, sourceRect) ?? false } : nil, setupTemporaryHiddenMedia: { signal, centralIndex, galleryMedia in if let strongSelf = self { strongSelf.temporaryHiddenGalleryMediaDisposable.set((signal |> deliverOnMainQueue).start(next: { entry in @@ -812,7 +883,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G break } } - let _ = combineLatest(queue: .mainQueue(), contextMenuForChatPresentationInterfaceState(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) + let _ = combineLatest(queue: .mainQueue(), contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState: strongSelf.presentationInterfaceState, context: strongSelf.context, messages: updatedMessages, controllerInteraction: strongSelf.controllerInteraction, selectAll: selectAll, interfaceInteraction: strongSelf.interfaceInteraction), strongSelf.context.engine.stickers.loadedStickerPack(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 @@ -903,12 +974,37 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G strongSelf.window?.presentInGlobalOverlay(controller) }) } + }, activateMessagePinch: { [weak self] sourceNode in + guard let strongSelf = self else { + return + } + + var sourceItemNode: ListViewItemNode? + strongSelf.chatDisplayNode.historyNode.forEachItemNode { itemNode in + guard let itemNode = itemNode as? ListViewItemNode else { + return + } + if sourceNode.view.isDescendant(of: itemNode.view) { + sourceItemNode = itemNode + } + } + + let pinchController = PinchController(sourceNode: sourceNode, getContentAreaInScreenSpace: { + guard let strongSelf = self else { + return CGRect() + } + + return strongSelf.chatDisplayNode.view.convert(strongSelf.chatDisplayNode.frameForVisibleArea(), to: nil) + }) + strongSelf.currentPinchController = pinchController + strongSelf.currentPinchSourceItemNode = sourceItemNode + strongSelf.window?.presentInGlobalOverlay(pinchController) }, openMessageContextActions: { message, node, rect, gesture in gesture?.cancel() }, navigateToMessage: { [weak self] fromId, id in - self?.navigateToMessage(from: fromId, to: .id(id), forceInCurrentChat: fromId.peerId == id.peerId) + self?.navigateToMessage(from: fromId, to: .id(id, nil), forceInCurrentChat: fromId.peerId == id.peerId) }, navigateToMessageStandalone: { [weak self] id in - self?.navigateToMessage(from: nil, to: .id(id), forceInCurrentChat: false) + self?.navigateToMessage(from: nil, to: .id(id, nil), forceInCurrentChat: false) }, tapMessage: nil, clickThroughMessage: { [weak self] in self?.chatDisplayNode.dismissInput() }, toggleMessagesSelection: { [weak self] ids, value in @@ -918,12 +1014,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withToggledSelectedMessages(ids, value: value) } }) if let selectionState = strongSelf.presentationInterfaceState.interfaceState.selectionState { let count = selectionState.selectedIds.count - let text: String - if count == 1 { - text = "1 message selected" - } else { - text = "\(count) messages selected" - } + let text = strongSelf.presentationData.strings.VoiceOver_Chat_MessagesSelected(Int32(count)) DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.1, execute: { UIAccessibility.post(notification: UIAccessibility.Notification.announcement, argument: text as NSString) }) @@ -956,14 +1047,14 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G $0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil) } }) } - }) + }, nil) var attributes: [MessageAttribute] = [] let entities = generateTextEntities(text, enabledTypes: .all) if !entities.isEmpty { attributes.append(TextEntitiesMessageAttribute(entities: entities)) } - strongSelf.sendMessages([.message(text: text, attributes: attributes, mediaReference: nil, replyToMessageId: strongSelf.presentationInterfaceState.interfaceState.replyMessageId, localGroupingKey: nil)]) - }, sendSticker: { [weak self] fileReference, query, clearInput, sourceNode, sourceRect in + strongSelf.sendMessages([.message(text: text, attributes: attributes, mediaReference: nil, replyToMessageId: strongSelf.presentationInterfaceState.interfaceState.replyMessageId, localGroupingKey: nil, correlationId: nil)]) + }, sendSticker: { [weak self] fileReference, silentPosting, schedule, query, clearInput, sourceNode, sourceRect in guard let strongSelf = self else { return false } @@ -972,7 +1063,24 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G strongSelf.interfaceInteraction?.displaySlowmodeTooltip(sourceNode, sourceRect) return false } - + + var attributes: [MessageAttribute] = [] + if let query = query { + attributes.append(EmojiSearchQueryMessageAttribute(query: query)) + } + + let correlationId = Int64.random(in: 0 ..< Int64.max) + + var replyPanel: ReplyAccessoryPanelNode? + if let accessoryPanelNode = strongSelf.chatDisplayNode.accessoryPanelNode as? ReplyAccessoryPanelNode { + replyPanel = accessoryPanelNode + } + + var shouldAnimateMessageTransition = strongSelf.chatDisplayNode.shouldAnimateMessageTransition + if sourceNode is ChatEmptyNodeStickerContentNode { + shouldAnimateMessageTransition = true + } + strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ if let strongSelf = self { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { current in @@ -990,20 +1098,55 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } return current } - + return current }) } - }) - - var attributes: [MessageAttribute] = [] - if let query = query { - attributes.append(EmojiSearchQueryMessageAttribute(query: query)) + }, shouldAnimateMessageTransition ? correlationId : nil) + + if shouldAnimateMessageTransition { + if let sourceNode = sourceNode as? ChatMediaInputStickerGridItemNode { + strongSelf.chatDisplayNode.messageTransitionNode.add(correlationId: correlationId, source: .stickerMediaInput(input: .inputPanel(itemNode: sourceNode), replyPanel: replyPanel), initiated: { + guard let strongSelf = self else { + return + } + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { current in + var current = current + current = current.updatedInputMode { current in + if case let .media(mode, maybeExpanded) = current, maybeExpanded != nil { + return .media(mode: mode, expanded: nil) + } + return current + } + + return current + }) + }) + } else if let sourceNode = sourceNode as? HorizontalStickerGridItemNode { + strongSelf.chatDisplayNode.messageTransitionNode.add(correlationId: correlationId, source: .stickerMediaInput(input: .mediaPanel(itemNode: sourceNode), replyPanel: replyPanel), initiated: {}) + } else if let sourceNode = sourceNode as? StickerPaneSearchStickerItemNode { + strongSelf.chatDisplayNode.messageTransitionNode.add(correlationId: correlationId, source: .stickerMediaInput(input: .inputPanelSearch(itemNode: sourceNode), replyPanel: replyPanel), initiated: {}) + } else if let sourceNode = sourceNode as? ChatEmptyNodeStickerContentNode { + strongSelf.chatDisplayNode.messageTransitionNode.add(correlationId: correlationId, source: .stickerMediaInput(input: .emptyPanel(itemNode: sourceNode), replyPanel: nil), initiated: {}) + } } - strongSelf.sendMessages([.message(text: "", attributes: attributes, mediaReference: fileReference.abstract, replyToMessageId: strongSelf.presentationInterfaceState.interfaceState.replyMessageId, localGroupingKey: nil)]) + let messages: [EnqueueMessage] = [.message(text: "", attributes: attributes, mediaReference: fileReference.abstract, replyToMessageId: strongSelf.presentationInterfaceState.interfaceState.replyMessageId, localGroupingKey: nil, correlationId: correlationId)] + if silentPosting { + let transformedMessages = strongSelf.transformEnqueueMessages(messages, silentPosting: silentPosting) + strongSelf.sendMessages(transformedMessages) + } else if schedule { + strongSelf.presentScheduleTimePicker(completion: { [weak self] scheduleTime in + if let strongSelf = self { + let transformedMessages = strongSelf.transformEnqueueMessages(messages, silentPosting: false, scheduleTime: scheduleTime) + strongSelf.sendMessages(transformedMessages) + } + }) + } else { + strongSelf.sendMessages(messages) + } return true - }, sendGif: { [weak self] fileReference, sourceNode, sourceRect in + }, sendGif: { [weak self] fileReference, sourceNode, sourceRect, silentPosting, schedule in if let strongSelf = self { if let _ = strongSelf.presentationInterfaceState.slowmodeState, strongSelf.presentationInterfaceState.subject != .scheduledMessages { strongSelf.interfaceInteraction?.displaySlowmodeTooltip(sourceNode, sourceRect) @@ -1021,11 +1164,25 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } }) } - }) - strongSelf.sendMessages([.message(text: "", attributes: [], mediaReference: fileReference.abstract, replyToMessageId: strongSelf.presentationInterfaceState.interfaceState.replyMessageId, localGroupingKey: nil)]) + }, nil) + + var messages = [EnqueueMessage.message(text: "", attributes: [], mediaReference: fileReference.abstract, replyToMessageId: strongSelf.presentationInterfaceState.interfaceState.replyMessageId, localGroupingKey: nil, correlationId: nil)] + if silentPosting { + messages = strongSelf.transformEnqueueMessages(messages, silentPosting: true) + strongSelf.sendMessages(messages) + } else if schedule { + strongSelf.presentScheduleTimePicker(completion: { [weak self] scheduleTime in + if let strongSelf = self { + let transformedMessages = strongSelf.transformEnqueueMessages(messages, silentPosting: false, scheduleTime: scheduleTime) + strongSelf.sendMessages(transformedMessages) + } + }) + } else { + strongSelf.sendMessages(messages) + } } return true - }, sendBotContextResultAsGif: { [weak self] collection, result, sourceNode, sourceRect in + }, sendBotContextResultAsGif: { [weak self] collection, result, sourceNode, sourceRect, silentPosting in guard let strongSelf = self else { return false } @@ -1037,7 +1194,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return false } - strongSelf.enqueueChatContextResult(collection, result, hideVia: true, closeMediaInput: true) + strongSelf.enqueueChatContextResult(collection, result, hideVia: true, closeMediaInput: true, silentPosting: silentPosting) return true }, requestMessageActionCallback: { [weak self] messageId, data, isGame, requiresPassword in @@ -1142,9 +1299,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } - let account = strongSelf.context.account + let context = strongSelf.context if requiresPassword { - strongSelf.messageActionCallbackDisposable.set(((requestMessageActionCallbackPasswordCheck(account: account, messageId: messageId, isGame: isGame, data: data) + strongSelf.messageActionCallbackDisposable.set(((strongSelf.context.engine.messages.requestMessageActionCallbackPasswordCheck(messageId: messageId, isGame: isGame, data: data) |> afterDisposed { updateProgress() }) @@ -1152,7 +1309,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G let controller = ownershipTransferController(context: context, initialError: error, present: { c, a in strongSelf.present(c, in: .window(.root), with: a) }, commit: { password in - return requestMessageActionCallback(account: account, messageId: messageId, isGame: isGame, password: password, data: data) + return context.engine.messages.requestMessageActionCallback(messageId: messageId, isGame: isGame, password: password, data: data) |> afterDisposed { updateProgress() } @@ -1162,7 +1319,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G strongSelf.present(controller, in: .window(.root)) })) } else { - strongSelf.messageActionCallbackDisposable.set(((requestMessageActionCallback(account: account, messageId: messageId, isGame: isGame, password: nil, data: data) + strongSelf.messageActionCallbackDisposable.set(((context.engine.messages.requestMessageActionCallback(messageId: messageId, isGame: isGame, password: nil, data: data) |> afterDisposed { updateProgress() }) @@ -1195,7 +1352,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return $0 } }) - strongSelf.messageActionUrlAuthDisposable.set(((combineLatest(strongSelf.context.account.postbox.loadedPeerWithId(strongSelf.context.account.peerId), requestMessageActionUrlAuth(account: strongSelf.context.account, subject: subject) |> afterDisposed { + strongSelf.messageActionUrlAuthDisposable.set(((combineLatest(strongSelf.context.account.postbox.loadedPeerWithId(strongSelf.context.account.peerId), strongSelf.context.engine.messages.requestMessageActionUrlAuth(subject: subject) |> afterDisposed { Queue.mainQueue().async { if let strongSelf = self { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { @@ -1244,7 +1401,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } }) - strongSelf.messageActionUrlAuthDisposable.set(((acceptMessageActionUrlAuth(account: strongSelf.context.account, subject: subject, allowWriteAccess: allowWriteAccess) |> afterDisposed { + strongSelf.messageActionUrlAuthDisposable.set(((strongSelf.context.engine.messages.acceptMessageActionUrlAuth(subject: subject, allowWriteAccess: allowWriteAccess) |> afterDisposed { Queue.mainQueue().async { if let strongSelf = self { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { @@ -1321,7 +1478,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G |> deliverOnMainQueue).start(next: { coordinate in if let strongSelf = self { if let coordinate = coordinate { - strongSelf.sendMessages([.message(text: "", attributes: [], mediaReference: .standalone(media: TelegramMediaMap(latitude: coordinate.latitude, longitude: coordinate.longitude, heading: nil, accuracyRadius: nil, geoPlace: nil, venue: nil, liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil)), replyToMessageId: nil, localGroupingKey: nil)]) + strongSelf.sendMessages([.message(text: "", attributes: [], mediaReference: .standalone(media: TelegramMediaMap(latitude: coordinate.latitude, longitude: coordinate.longitude, heading: nil, accuracyRadius: nil, geoPlace: nil, venue: nil, liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil)), replyToMessageId: nil, localGroupingKey: nil, correlationId: nil)]) } else { strongSelf.present(textAlertController(context: strongSelf.context, title: nil, text: strongSelf.presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {})]), in: .window(.root)) } @@ -1345,7 +1502,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G let _ = (strongSelf.context.account.postbox.loadedPeerWithId(strongSelf.context.account.peerId) |> deliverOnMainQueue).start(next: { peer in if let peer = peer as? TelegramUser, let phone = peer.phone, !phone.isEmpty { - strongSelf.sendMessages([.message(text: "", attributes: [], mediaReference: .standalone(media: TelegramMediaContact(firstName: peer.firstName ?? "", lastName: peer.lastName ?? "", phoneNumber: phone, peerId: peer.id, vCardData: nil)), replyToMessageId: nil, localGroupingKey: nil)]) + strongSelf.sendMessages([.message(text: "", attributes: [], mediaReference: .standalone(media: TelegramMediaContact(firstName: peer.firstName ?? "", lastName: peer.lastName ?? "", phoneNumber: phone, peerId: peer.id, vCardData: nil)), replyToMessageId: nil, localGroupingKey: nil, correlationId: nil)]) } }) } @@ -1353,7 +1510,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } }, sendBotCommand: { [weak self] messageId, command in if let strongSelf = self, canSendMessagesToChat(strongSelf.presentationInterfaceState) { - strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({}) + strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({}, nil) var postAsReply = false if !command.contains("@") { switch strongSelf.chatLocation { @@ -1364,6 +1521,13 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G case .replyThread: postAsReply = true } + + if let messageId = messageId, let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(messageId) { + if let author = message.author as? TelegramUser, author.botInfo != nil { + } else { + postAsReply = false + } + } } strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ @@ -1372,13 +1536,13 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G $0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil).withUpdatedComposeInputState(ChatTextInputState(inputText: NSAttributedString(string: ""))).withUpdatedComposeDisableUrlPreview(nil) } }) } - }) + }, nil) var attributes: [MessageAttribute] = [] let entities = generateTextEntities(command, enabledTypes: .all) if !entities.isEmpty { attributes.append(TextEntitiesMessageAttribute(entities: entities)) } - strongSelf.sendMessages([.message(text: command, attributes: attributes, mediaReference: nil, replyToMessageId: (postAsReply && messageId != nil) ? messageId! : nil, localGroupingKey: nil)]) + strongSelf.sendMessages([.message(text: command, attributes: attributes, mediaReference: nil, replyToMessageId: (postAsReply && messageId != nil) ? messageId! : nil, localGroupingKey: nil, correlationId: nil)]) } }, openInstantPage: { [weak self] message, associatedData in if let strongSelf = self, strongSelf.isNodeLoaded, let navigationController = strongSelf.effectiveNavigationController, let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(message.id) { @@ -1436,6 +1600,11 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G self?.commitPurposefulAction() } } + shareController.actionCompleted = { [weak self] in + if let strongSelf = self { + strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .linkCopied(text: strongSelf.presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current) + } + } shareController.completed = { [weak self] peerIds in if let strongSelf = self { let _ = (strongSelf.context.account.postbox.transaction { transaction -> [Peer] in @@ -1449,7 +1618,6 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } |> deliverOnMainQueue).start(next: { [weak self] peers in if let strongSelf = self { let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } - let text: String var savedMessages = false if peerIds.count == 1, let peerId = peerIds.first, peerId == strongSelf.context.account.peerId { @@ -1615,7 +1783,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G actionSheet?.dismissAnimated() UIPasteboard.general.string = mention - let content: UndoOverlayContent = .copy(text: presentationData.strings.Conversation_UsernameCopied) + let content: UndoOverlayContent = .copy(text: presentationData.strings.Conversation_TextCopied) self?.present(UndoOverlayController(presentationData: presentationData, content: content, elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current) })) } @@ -1640,7 +1808,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G actionSheet?.dismissAnimated() UIPasteboard.general.string = mention - let content: UndoOverlayContent = .copy(text: presentationData.strings.Conversation_TextCopied) + let content: UndoOverlayContent = .copy(text: presentationData.strings.Conversation_UsernameCopied) self?.present(UndoOverlayController(presentationData: presentationData, content: content, elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current) }) ]), ActionSheetItemGroup(items: [ @@ -1658,7 +1826,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.ShareMenu_Send, color: .accent, action: { [weak actionSheet] in actionSheet?.dismissAnimated() if let strongSelf = self { - strongSelf.sendMessages([.message(text: command, attributes: [], mediaReference: nil, replyToMessageId: nil, localGroupingKey: nil)]) + strongSelf.sendMessages([.message(text: command, attributes: [], mediaReference: nil, replyToMessageId: nil, localGroupingKey: nil, correlationId: nil)]) } })) } @@ -1713,7 +1881,17 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G guard let message = message else { return } + let context = strongSelf.context + let chatPresentationInterfaceState = strongSelf.presentationInterfaceState let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData) + + let isCopyLink: Bool + if message.id.namespace == Namespaces.Message.Cloud, let _ = message.peers[message.id.peerId] as? TelegramChannel, !(message.media.first is TelegramMediaAction) { + isCopyLink = true + } else { + isCopyLink = false + } + actionSheet.setItemGroups([ActionSheetItemGroup(items: [ ActionSheetTextItem(title: text), ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogOpen, color: .accent, action: { [weak actionSheet] in @@ -1722,12 +1900,52 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G strongSelf.controllerInteraction?.seekToTimecode(message, timecode, true) } }), - ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogCopy, color: .accent, action: { [weak actionSheet] in + ActionSheetButtonItem(title: isCopyLink ? strongSelf.presentationData.strings.Conversation_ContextMenuCopyLink : strongSelf.presentationData.strings.Conversation_LinkDialogCopy, color: .accent, action: { [weak actionSheet] in actionSheet?.dismissAnimated() - UIPasteboard.general.string = text - - let content: UndoOverlayContent = .copy(text: presentationData.strings.Conversation_TextCopied) - self?.present(UndoOverlayController(presentationData: presentationData, content: content, elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current) + if isCopyLink, let channel = message.peers[message.id.peerId] as? TelegramChannel { + var threadMessageId: MessageId? + + if case let .replyThread(replyThreadMessage) = chatPresentationInterfaceState.chatLocation { + threadMessageId = replyThreadMessage.messageId + } + let _ = (context.engine.messages.exportMessageLink(peerId: message.id.peerId, messageId: message.id, isThread: threadMessageId != nil) + |> map { result -> String? in + return result + } + |> deliverOnMainQueue).start(next: { link in + if let link = link { + UIPasteboard.general.string = link + "?t=\(Int32(timecode))" + + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + + var warnAboutPrivate = false + if case .peer = chatPresentationInterfaceState.chatLocation { + if channel.addressName == nil { + warnAboutPrivate = true + } + } + Queue.mainQueue().after(0.2, { + let content: UndoOverlayContent + if warnAboutPrivate { + content = .linkCopied(text: presentationData.strings.Conversation_PrivateMessageLinkCopiedLong) + } else { + content = .linkCopied(text: presentationData.strings.Conversation_LinkCopied) + } + self?.present(UndoOverlayController(presentationData: presentationData, content: content, elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current) + }) + } else { + UIPasteboard.general.string = text + + let content: UndoOverlayContent = .copy(text: presentationData.strings.Conversation_TextCopied) + self?.present(UndoOverlayController(presentationData: presentationData, content: content, elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current) + } + }) + } else { + UIPasteboard.general.string = text + + let content: UndoOverlayContent = .copy(text: presentationData.strings.Conversation_TextCopied) + self?.present(UndoOverlayController(presentationData: presentationData, content: content, elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current) + } }) ]), ActionSheetItemGroup(items: [ ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in @@ -1741,7 +1959,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return } - var signal = getBankCardInfo(account: strongSelf.context.account, cardNumber: number) + var signal = strongSelf.context.engine.payments.getBankCardInfo(cardNumber: number) let disposable: MetaDisposable if let current = strongSelf.bankCardDisposable { disposable = current @@ -1817,9 +2035,30 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if let invoice = media as? TelegramMediaInvoice { strongSelf.chatDisplayNode.dismissInput() if let receiptMessageId = invoice.receiptMessageId { - strongSelf.present(BotReceiptController(context: strongSelf.context, invoice: invoice, messageId: receiptMessageId), in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + strongSelf.present(BotReceiptController(context: strongSelf.context, messageId: receiptMessageId), in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) } else { - strongSelf.present(BotCheckoutController(context: strongSelf.context, invoice: invoice, messageId: messageId), in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + let inputData = Promise() + inputData.set(BotCheckoutController.InputData.fetch(context: strongSelf.context, messageId: message.id) + |> map(Optional.init) + |> `catch` { _ -> Signal in + return .single(nil) + }) + strongSelf.present(BotCheckoutController(context: strongSelf.context, invoice: invoice, messageId: messageId, inputData: inputData, completed: { currencyValue, receiptMessageId in + guard let strongSelf = self else { + return + } + strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .paymentSent(currencyValue: currencyValue, itemTitle: invoice.title), elevatedLayout: false, action: { action in + guard let strongSelf = self, let receiptMessageId = receiptMessageId else { + return false + } + + if case .info = action { + strongSelf.present(BotReceiptController(context: strongSelf.context, messageId: receiptMessageId), in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + return true + } + return false + }), in: .current) + }), in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) } } } @@ -1987,7 +2226,7 @@ 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(account: strongSelf.context.account, messageIds: [id], type: .forLocalPeer).start() + let _ = strongSelf.context.engine.messages.deleteMessagesInteractively(messageIds: [id], type: .forLocalPeer).start() } f(.dismissWithoutContent) }))) @@ -2048,7 +2287,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G disposables = DisposableDict() strongSelf.selectMessagePollOptionDisposables = disposables } - let signal = requestMessageSelectPollOption(account: strongSelf.context.account, messageId: id, opaqueIdentifiers: opaqueIdentifiers) + let signal = strongSelf.context.engine.messages.requestMessageSelectPollOption(messageId: id, opaqueIdentifiers: opaqueIdentifiers) disposables.set((signal |> deliverOnMainQueue).start(next: { resultPoll in guard let strongSelf = self, let resultPoll = resultPoll else { @@ -2218,7 +2457,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } return } else { - let _ = sendScheduledMessageNowInteractively(postbox: strongSelf.context.account.postbox, messageId: messageIds.first!).start() + let _ = strongSelf.context.engine.messages.sendScheduledMessageNowInteractively(messageId: messageIds.first!).start() } } }, editScheduledMessagesTime: { [weak self] messageIds in @@ -2239,7 +2478,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G break } } - strongSelf.editMessageDisposable.set((requestEditMessage(account: strongSelf.context.account, messageId: messageId, text: message.text, media: .keep, entities: entities, disableUrlPreview: false, scheduleTime: time) |> deliverOnMainQueue).start(next: { result in + strongSelf.editMessageDisposable.set((strongSelf.context.engine.messages.requestEditMessage(messageId: messageId, text: message.text, media: .keep, entities: entities, disableUrlPreview: false, scheduleTime: time) |> deliverOnMainQueue).start(next: { result in }, error: { error in })) } @@ -2291,29 +2530,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } else { let _ = updateMessageReactionsInteractively(postbox: strongSelf.context.account.postbox, messageId: messageId, reaction: nil).start() } - }, openMessageReactions: { [weak self] messageId 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 strongSelf = self, let message = message else { - return - } - var initialReactions: [MessageReaction] = [] - for attribute in message.attributes { - if let attribute = attribute as? ReactionsMessageAttribute { - initialReactions = attribute.reactions - } - } - - if !initialReactions.isEmpty { - strongSelf.chatDisplayNode.dismissInput() - strongSelf.present(MessageReactionListController(context: strongSelf.context, messageId: message.id, initialReactions: initialReactions), in: .window(.root)) - } - }) + }, openMessageReactions: { _ in }, displayImportedMessageTooltip: { [weak self] _ in guard let strongSelf = self else { return @@ -2337,6 +2554,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G $0.withUpdatedMessageActionsState({ value in var value = value value.closedButtonKeyboardMessageId = message.id + value.dismissedButtonKeyboardMessageId = message.id return value }) }) @@ -2385,8 +2603,6 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if !onlyHaptic { strongSelf.chatDisplayNode.animateQuizCorrectOptionSelected() } - }, greetingStickerNode: { [weak self] in - return self?.chatDisplayNode.greetingStickerNode }, openPeerContextMenu: { [weak self] peer, messageId, node, rect, gesture in guard let strongSelf = self else { return @@ -2418,7 +2634,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } var items: [ContextMenuItem] = [ .action(ContextMenuActionItem(text: isChannel ? strongSelf.presentationData.strings.Conversation_ContextMenuOpenChannelProfile : strongSelf.presentationData.strings.Conversation_ContextMenuOpenProfile, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Info"), color: theme.actionSheet.primaryTextColor) + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/User"), color: theme.actionSheet.primaryTextColor) }, action: { _, f in f(.dismissWithoutContent) self?.openPeer(peerId: peer.id, navigation: .info, fromMessage: nil) @@ -2481,7 +2697,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G strongSelf.openMessageReplies(messageId: threadMessageId, displayProgressInMessage: message.id, isChannelPost: true, atMessage: attribute.messageId, displayModalProgress: false) } } else { - strongSelf.navigateToMessage(from: nil, to: .id(attribute.messageId)) + strongSelf.navigateToMessage(from: nil, to: .id(attribute.messageId, nil)) } break } @@ -2571,6 +2787,11 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return true }), in: .current) } + }, isAnimatingMessage: { [weak self] stableId in + guard let strongSelf = self else { + return false + } + return strongSelf.chatDisplayNode.messageTransitionNode.isAnimatingMessage(stableId: stableId) }, requestMessageUpdate: { [weak self] id in if let strongSelf = self { strongSelf.chatDisplayNode.historyNode.requestMessageUpdate(id) @@ -2578,7 +2799,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G }, cancelInteractiveKeyboardGestures: { [weak self] in (self?.view.window as? WindowHost)?.cancelInteractiveKeyboardGestures() self?.chatDisplayNode.cancelInteractiveKeyboardGestures() - }, automaticMediaDownloadSettings: self.automaticMediaDownloadSettings, pollActionState: ChatInterfacePollActionState(), stickerSettings: self.stickerSettings) + }, automaticMediaDownloadSettings: self.automaticMediaDownloadSettings, pollActionState: ChatInterfacePollActionState(), stickerSettings: self.stickerSettings, presentationContext: ChatPresentationContext(backgroundNode: self.chatBackgroundNode)) self.controllerInteraction = controllerInteraction @@ -2590,11 +2811,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G self.navigationBar?.userInfo = PeerInfoNavigationSourceTag(peerId: peerId) } } - self.navigationBar?.allowsCustomTransition = { [weak self] in - guard let strongSelf = self else { - return false - } - return !strongSelf.chatDisplayNode.hasEmbeddedTitleContent + self.navigationBar?.allowsCustomTransition = { + return true } self.chatTitleView = ChatTitleView(account: self.context.account, theme: self.presentationData.theme, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameDisplayOrder: self.presentationData.nameDisplayOrder) @@ -2734,10 +2952,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G |> 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) + return context.peerChannelMemberCategoriesContextsManager.recentOnline(account: context.account, 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) + return context.peerChannelMemberCategoriesContextsManager.recentOnlineSmall(engine: context.engine, postbox: context.account.postbox, network: context.account.network, accountPeerId: context.account.peerId, peerId: peerId) |> map(Optional.init) } } else { @@ -2896,8 +3114,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G strongSelf.peerView = peerView if wasGroupChannel != isGroupChannel { if let isGroupChannel = isGroupChannel, isGroupChannel { - let (recentDisposable, _) = strongSelf.context.peerChannelMemberCategoriesContextsManager.recent(postbox: strongSelf.context.account.postbox, network: strongSelf.context.account.network, accountPeerId: context.account.peerId, peerId: peerView.peerId, updated: { _ in }) - let (adminsDisposable, _) = strongSelf.context.peerChannelMemberCategoriesContextsManager.admins(postbox: strongSelf.context.account.postbox, network: strongSelf.context.account.network, accountPeerId: context.account.peerId, peerId: peerView.peerId, updated: { _ in }) + let (recentDisposable, _) = strongSelf.context.peerChannelMemberCategoriesContextsManager.recent(engine: strongSelf.context.engine, postbox: strongSelf.context.account.postbox, network: strongSelf.context.account.network, accountPeerId: context.account.peerId, peerId: peerView.peerId, updated: { _ in }) + let (adminsDisposable, _) = strongSelf.context.peerChannelMemberCategoriesContextsManager.admins(engine: strongSelf.context.engine, postbox: strongSelf.context.account.postbox, network: strongSelf.context.account.network, accountPeerId: context.account.peerId, peerId: peerView.peerId, updated: { _ in }) let disposable = DisposableSet() disposable.add(recentDisposable) disposable.add(adminsDisposable) @@ -2974,6 +3192,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } var hasBots: Bool = false + var hasBotCommands: Bool = false var autoremoveTimeout: Int32? if let peer = peerView.peers[peerView.peerId] { if let cachedGroupData = peerView.cachedData as? CachedGroupData { @@ -2996,6 +3215,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if case let .known(value) = cachedUserData.autoremoveTimeout { autoremoveTimeout = value?.effectiveValue } + if let botInfo = cachedUserData.botInfo, !botInfo.commands.isEmpty { + hasBotCommands = true + } } } @@ -3071,7 +3293,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G strongSelf.updateChatPresentationInterfaceState(animated: animated, interactive: false, { return $0.updatedPeer { _ in return renderedPeer - }.updatedIsNotAccessible(isNotAccessible).updatedContactStatus(contactStatus).updatedHasBots(hasBots).updatedIsArchived(isArchived).updatedPeerIsMuted(peerIsMuted).updatedPeerDiscussionId(peerDiscussionId).updatedPeerGeoLocation(peerGeoLocation).updatedExplicitelyCanPinMessages(explicitelyCanPinMessages).updatedHasScheduledMessages(hasScheduledMessages) + }.updatedIsNotAccessible(isNotAccessible).updatedContactStatus(contactStatus).updatedHasBots(hasBots).updatedHasBotCommands(hasBotCommands).updatedIsArchived(isArchived).updatedPeerIsMuted(peerIsMuted).updatedPeerDiscussionId(peerDiscussionId).updatedPeerGeoLocation(peerGeoLocation).updatedExplicitelyCanPinMessages(explicitelyCanPinMessages).updatedHasScheduledMessages(hasScheduledMessages) .updatedAutoremoveTimeout(autoremoveTimeout) }) if !strongSelf.didSetChatLocationInfoReady { @@ -3709,13 +3931,6 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G var isScrolled: Bool } - let messageRangeEdge: Signal = self.context.sharedContext.accountManager.sharedData(keys: Set([ApplicationSpecificSharedDataKeys.experimentalUISettings])) - |> map { sharedData -> Bool in - let experimentalSettings: ExperimentalUISettings = (sharedData.entries[ApplicationSpecificSharedDataKeys.experimentalUISettings] as? ExperimentalUISettings) ?? ExperimentalUISettings.defaultSettings - return experimentalSettings.snapPinListToTop - } - |> distinctUntilChanged - let referenceMessage: Signal if latest { referenceMessage = .single(nil) @@ -3723,16 +3938,11 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G referenceMessage = combineLatest( queue: Queue.mainQueue(), self.scrolledToMessageId.get(), - self.chatDisplayNode.historyNode.topVisibleMessageRange.get(), - messageRangeEdge + self.chatDisplayNode.historyNode.topVisibleMessageRange.get() ) - |> map { scrolledToMessageId, topVisibleMessageRange, messageRangeEdge -> ReferenceMessage? in + |> map { scrolledToMessageId, topVisibleMessageRange -> ReferenceMessage? in let topVisibleMessage: MessageId? - if messageRangeEdge { - topVisibleMessage = topVisibleMessageRange?.lowerBound - } else { - topVisibleMessage = topVisibleMessageRange?.upperBound - } + topVisibleMessage = topVisibleMessageRange?.upperBound if let scrolledToMessageId = scrolledToMessageId { if let topVisibleMessage = topVisibleMessage { @@ -3986,27 +4196,18 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } override public func loadDisplayNode() { - self.displayNode = ChatControllerNode(context: self.context, chatLocation: self.chatLocation, chatLocationContextHolder: self.chatLocationContextHolder, subject: self.subject, controllerInteraction: self.controllerInteraction!, chatPresentationInterfaceState: self.presentationInterfaceState, automaticMediaDownloadSettings: self.automaticMediaDownloadSettings, navigationBar: self.navigationBar, controller: self) + self.displayNode = ChatControllerNode(context: self.context, chatLocation: self.chatLocation, chatLocationContextHolder: self.chatLocationContextHolder, subject: self.subject, controllerInteraction: self.controllerInteraction!, chatPresentationInterfaceState: self.presentationInterfaceState, automaticMediaDownloadSettings: self.automaticMediaDownloadSettings, navigationBar: self.navigationBar, backgroundNode: self.chatBackgroundNode, controller: self) + + if let currentItem = self.tempVoicePlaylistCurrentItem { + self.chatDisplayNode.historyNode.voicePlaylistItemChanged(nil, currentItem) + } self.chatDisplayNode.historyNode.didScrollWithOffset = { [weak self] offset, transition, itemNode in guard let strongSelf = self else { return } - for (tooltipScreen, tooltipItemNode) in strongSelf.currentMessageTooltipScreens { - if let itemNode = itemNode { - if itemNode === tooltipItemNode { - tooltipScreen.addRelativeScrollingOffset(-offset, transition: transition) - } - } else { - tooltipScreen.addRelativeScrollingOffset(-offset, transition: transition) - } - } - } - - self.chatDisplayNode.historyNode.didScrollWithOffset = { [weak self] offset, _, _ in - guard let strongSelf = self else { - return - } + + //print("didScrollWithOffset offset: \(offset), itemNode: \(String(describing: itemNode))") if offset > 0.0 { if var scrolledToMessageIdValue = strongSelf.scrolledToMessageIdValue { @@ -4016,6 +4217,25 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } else if offset < 0.0 { strongSelf.scrolledToMessageIdValue = nil } + + if let currentPinchSourceItemNode = strongSelf.currentPinchSourceItemNode { + if let itemNode = itemNode { + if itemNode === currentPinchSourceItemNode { + strongSelf.currentPinchController?.addRelativeContentOffset(CGPoint(x: 0.0, y: -offset), transition: transition) + } + } else { + strongSelf.currentPinchController?.addRelativeContentOffset(CGPoint(x: 0.0, y: -offset), transition: transition) + } + } + + strongSelf.chatDisplayNode.messageTransitionNode.addExternalOffset(offset: offset, transition: transition, itemNode: itemNode) + } + + self.chatDisplayNode.historyNode.addContentOffset = { [weak self] offset, itemNode in + guard let strongSelf = self else { + return + } + strongSelf.chatDisplayNode.messageTransitionNode.addContentOffset(offset: offset, itemNode: itemNode) } if case .pinnedMessages = self.presentationInterfaceState.subject { @@ -4322,9 +4542,17 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G //effectiveCachedDataReady = .single(true) effectiveCachedDataReady = self.cachedDataReady.get() } - self.ready.set(combineLatest(self.chatDisplayNode.historyNode.historyState.get(), self._chatLocationInfoReady.get(), effectiveCachedDataReady, initialData) |> map { _, chatLocationInfoReady, cachedDataReady, _ in - return chatLocationInfoReady && cachedDataReady - }) + self.ready.set(combineLatest(queue: .mainQueue(), + self.chatDisplayNode.historyNode.historyState.get(), + self._chatLocationInfoReady.get(), + effectiveCachedDataReady, + initialData, + self.wallpaperReady.get() + ) + |> map { _, chatLocationInfoReady, cachedDataReady, _, wallpaperReady in + return chatLocationInfoReady && cachedDataReady && wallpaperReady + } + |> distinctUntilChanged) if self.context.sharedContext.immediateExperimentalUISettings.crashOnLongQueries { let _ = (self.ready.get() @@ -4365,9 +4593,11 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G strongSelf.chatDisplayNode.updatePlainInputSeparatorAlpha(plainInputSeparatorAlpha, transition: .animated(duration: 0.2, curve: .easeInOut)) } - self.chatDisplayNode.historyNode.scrolledToIndex = { [weak self] toIndex in + self.chatDisplayNode.historyNode.scrolledToIndex = { [weak self] toIndex, initial in if let strongSelf = self, case let .message(index) = toIndex { - if let controllerInteraction = strongSelf.controllerInteraction { + if case let .message(messageId, _, _) = strongSelf.subject, initial, messageId != index.id { + strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .info(text: strongSelf.presentationData.strings.Conversation_MessageDoesntExist), elevatedLayout: false, action: { _ in return true }), in: .current) + } else if let controllerInteraction = strongSelf.controllerInteraction { if let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(index.id) { let highlightedState = ChatInterfaceHighlightedState(messageStableId: message.stableId) controllerInteraction.highlightedState = highlightedState @@ -4382,6 +4612,12 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } })) + + if case let .message(_, _, maybeTimecode) = strongSelf.subject, let timecode = maybeTimecode, initial { + Queue.mainQueue().after(0.2) { + let _ = strongSelf.controllerInteraction?.openMessage(message, .timecode(timecode)) + } + } } } } @@ -4404,8 +4640,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G self?.requestLayout(transition: transition) } - self.chatDisplayNode.setupSendActionOnViewUpdate = { [weak self] f in - self?.chatDisplayNode.historyNode.layoutActionOnViewTransition = { [weak self] transition in + self.chatDisplayNode.setupSendActionOnViewUpdate = { [weak self] f, messageCorrelationId in + //print("setup layoutActionOnViewTransition") + + self?.chatDisplayNode.historyNode.layoutActionOnViewTransition = ({ [weak self] transition in f() if let strongSelf = self, let validLayout = strongSelf.validLayout { var mappedTransition: (ChatHistoryListViewTransition, ListViewUpdateSizeAndInsets?)? @@ -4416,7 +4654,11 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } else { isScheduledMessages = false } - strongSelf.chatDisplayNode.containerLayoutUpdated(validLayout, navigationBarHeight: strongSelf.navigationHeight, transition: .animated(duration: 0.2, curve: .easeInOut), listViewTransaction: { updateSizeAndInsets, _, _, _ in + let duration: Double = strongSelf.chatDisplayNode.messageTransitionNode.hasScheduledTransitions ? ChatMessageTransitionNode.animationDuration : 0.18 + let curve: ContainedViewLayoutTransitionCurve = strongSelf.chatDisplayNode.messageTransitionNode.hasScheduledTransitions ? ChatMessageTransitionNode.verticalAnimationCurve : .easeInOut + let controlPoints: (Float, Float, Float, Float) = strongSelf.chatDisplayNode.messageTransitionNode.hasScheduledTransitions ? ChatMessageTransitionNode.verticalAnimationControlPoints : (0.5, 0.33, 0.0, 0.0) + + strongSelf.chatDisplayNode.containerLayoutUpdated(validLayout, navigationBarHeight: strongSelf.navigationLayout(layout: validLayout).navigationFrame.maxY, transition: .animated(duration: duration, curve: curve), listViewTransaction: { updateSizeAndInsets, _, _, _ in var options = transition.options let _ = options.insert(.Synchronous) let _ = options.insert(.LowLatency) @@ -4442,9 +4684,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G var scrollToItem: ListViewScrollToItem? if isScheduledMessages, let insertedIndex = insertedIndex { - scrollToItem = ListViewScrollToItem(index: insertedIndex, position: .visible, animated: true, curve: .Default(duration: 0.2), directionHint: .Down) + scrollToItem = ListViewScrollToItem(index: insertedIndex, position: .visible, animated: true, curve: .Custom(duration: duration, controlPoints.0, controlPoints.1, controlPoints.2, controlPoints.3), directionHint: .Down) } else if transition.historyView.originalView.laterId == nil { - scrollToItem = ListViewScrollToItem(index: 0, position: .top(0.0), animated: true, curve: .Default(duration: 0.2), directionHint: .Up) + scrollToItem = ListViewScrollToItem(index: 0, position: .top(0.0), animated: true, curve: .Custom(duration: duration, controlPoints.0, controlPoints.1, controlPoints.2, controlPoints.3), directionHint: .Up) } var stationaryItemRange: (Int, Int)? @@ -4453,6 +4695,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } mappedTransition = (ChatHistoryListViewTransition(historyView: transition.historyView, deleteItems: deleteItems, insertItems: insertItems, updateItems: transition.updateItems, options: options, scrollToItem: scrollToItem, stationaryItemRange: stationaryItemRange, initialData: transition.initialData, keyboardButtonsMessage: transition.keyboardButtonsMessage, cachedData: transition.cachedData, cachedDataMessages: transition.cachedDataMessages, readStateData: transition.readStateData, scrolledToIndex: transition.scrolledToIndex, scrolledToSomeIndex: transition.scrolledToSomeIndex, peerType: transition.peerType, networkType: transition.networkType, animateIn: false, reason: transition.reason, flashIndicators: transition.flashIndicators), updateSizeAndInsets) + }, updateExtraNavigationBarBackgroundHeight: { value in + strongSelf.additionalNavigationBarBackgroundHeight = value }) if let mappedTransition = mappedTransition { @@ -4460,11 +4704,23 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } return (transition, nil) - } + }, messageCorrelationId) } self.chatDisplayNode.sendMessages = { [weak self] messages, silentPosting, scheduleTime, isAnyMessageTextPartitioned in if let strongSelf = self { + var correlationIds: [Int64] = [] + for message in messages { + switch message { + case let .message(_, _, _, _, _, correlationId): + if let correlationId = correlationId { + correlationIds.append(correlationId) + } + default: + break + } + } + //print("sendMessages \(correlationIds)") let peerId = strongSelf.chatLocation.peerId strongSelf.commitPurposefulAction() @@ -4502,12 +4758,12 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G var forwardedMessages: [[EnqueueMessage]] = [] var forwardSourcePeerIds = Set() for message in transformedMessages { - if case let .forward(source, _, _) = message { + if case let .forward(source, _, _, _) = message { forwardSourcePeerIds.insert(source.peerId) var added = false if var last = forwardedMessages.last { - if let currentMessage = last.first, case let .forward(currentSource, _, _) = currentMessage, currentSource.peerId == source.peerId { + if let currentMessage = last.first, case let .forward(currentSource, _, _, _) = currentMessage, currentSource.peerId == source.peerId { last.append(message) added = true } @@ -4547,11 +4803,13 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G }) donateSendMessageIntent(account: strongSelf.context.account, sharedContext: strongSelf.context.sharedContext, intentContext: .chat, peerIds: [peerId]) + + strongSelf.updateChatPresentationInterfaceState(interactive: true, { $0.updatedShowCommands(false) }) } } - self.chatDisplayNode.requestUpdateChatInterfaceState = { [weak self] animated, saveInterfaceState, f in - self?.updateChatPresentationInterfaceState(animated: animated, interactive: true, saveInterfaceState: saveInterfaceState, { $0.updatedInterfaceState(f) }) + self.chatDisplayNode.requestUpdateChatInterfaceState = { [weak self] transition, saveInterfaceState, f in + self?.updateChatPresentationInterfaceState(transition: transition, interactive: true, saveInterfaceState: saveInterfaceState, { $0.updatedInterfaceState(f) }) } self.chatDisplayNode.requestUpdateInterfaceState = { [weak self] transition, interactive, f in @@ -4644,7 +4902,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G self.chatDisplayNode.navigateButtons.downPressed = { [weak self] in if let strongSelf = self, strongSelf.isNodeLoaded { if let messageId = strongSelf.historyNavigationStack.removeLast() { - strongSelf.navigateToMessage(from: nil, to: .id(messageId.id), rememberInStack: false) + strongSelf.navigateToMessage(from: nil, to: .id(messageId.id, nil), rememberInStack: false) } else { if case .known = strongSelf.chatDisplayNode.historyNode.visibleContentOffset() { strongSelf.chatDisplayNode.historyNode.scrollToEndOfHistory() @@ -4661,13 +4919,13 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G self.chatDisplayNode.navigateButtons.mentionsPressed = { [weak self] in if let strongSelf = self, strongSelf.isNodeLoaded, case let .peer(peerId) = strongSelf.chatLocation { - let signal = earliestUnseenPersonalMentionMessage(account: strongSelf.context.account, peerId: peerId) + let signal = strongSelf.context.engine.messages.earliestUnseenPersonalMentionMessage(peerId: peerId) strongSelf.navigationActionDisposable.set((signal |> deliverOnMainQueue).start(next: { result in if let strongSelf = self { switch result { case let .result(messageId): if let messageId = messageId { - strongSelf.navigateToMessage(from: nil, to: .id(messageId)) + strongSelf.navigateToMessage(from: nil, to: .id(messageId, nil)) } case .loading: break @@ -4700,20 +4958,27 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } let interfaceInteraction = ChatPanelInterfaceInteraction(setupReplyMessage: { [weak self] messageId, completion in - if let strongSelf = self, strongSelf.isNodeLoaded, canSendMessagesToChat(strongSelf.presentationInterfaceState) { - let _ = strongSelf.presentVoiceMessageDiscardAlert(action: { - if let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(messageId) { - strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState({ $0.withUpdatedReplyMessageId(message.id) }).updatedSearch(nil) }, completion: completion) - strongSelf.updateItemNodesSearchTextHighlightStates() - strongSelf.chatDisplayNode.ensureInputViewFocused() - } else { + guard let strongSelf = self, strongSelf.isNodeLoaded else { + return + } + if let messageId = messageId { + if canSendMessagesToChat(strongSelf.presentationInterfaceState) { + let _ = strongSelf.presentVoiceMessageDiscardAlert(action: { + if let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(messageId) { + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState({ $0.withUpdatedReplyMessageId(message.id) }).updatedSearch(nil).updatedShowCommands(false) }, completion: completion) + strongSelf.updateItemNodesSearchTextHighlightStates() + strongSelf.chatDisplayNode.ensureInputViewFocused() + } else { + completion(.immediate) + } + }, alertAction: { completion(.immediate) - } - }, alertAction: { + }, delay: true) + } else { completion(.immediate) - }, delay: true) + } } else { - completion(.immediate) + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState({ $0.withUpdatedReplyMessageId(nil) }) }, completion: completion) } }, setupEditMessage: { [weak self] messageId, completion in if let strongSelf = self, strongSelf.isNodeLoaded { @@ -4761,6 +5026,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G updated = updated.updatedInputMode({ _ in return .text }) + updated = updated.updatedShowCommands(false) return updated }, completion: completion) @@ -4772,7 +5038,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G }, beginMessageSelection: { [weak self] messageIds, completion in if let strongSelf = self, strongSelf.isNodeLoaded { let _ = strongSelf.presentVoiceMessageDiscardAlert(action: { - strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withUpdatedSelectedMessages(messageIds) } }, completion: completion) + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withUpdatedSelectedMessages(messageIds) }.updatedShowCommands(false) }, completion: completion) if let selectionState = strongSelf.presentationInterfaceState.interfaceState.selectionState { let count = selectionState.selectedIds.count @@ -4822,7 +5088,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G items.append(ActionSheetButtonItem(title: presentationData.strings.Report_Report, color: .accent, font: .bold, enabled: true, action: { dismissAction() strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withoutSelectionState() } }, completion: { _ in - let _ = (reportPeerMessages(account: strongSelf.context.account, messageIds: Array(messageIds), reason: reportReason, message: message) + let _ = (strongSelf.context.engine.peers.reportPeerMessages(messageIds: Array(messageIds), reason: reportReason, message: message) |> deliverOnMainQueue).start(completed: { [weak self] in if let strongSelf = self, let path = getAppBundle().path(forResource: "PoliceCar", ofType: "tgs") { strongSelf.present(UndoOverlayController(presentationData: presentationData, content: .emoji(path: path, text: presentationData.strings.Report_Succeed), elevatedLayout: false, action: { _ in return false }), in: .current) @@ -4887,12 +5153,12 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G guard let strongSelf = self else { return } - let _ = requestUpdatePeerIsBlocked(account: strongSelf.context.account, peerId: peer.id, isBlocked: true).start() - let account = strongSelf.context.account - let _ = (strongSelf.context.account.postbox.transaction { transasction -> Void in - deleteAllMessagesWithForwardAuthor(transaction: transasction, mediaBox: account.postbox.mediaBox, peerId: message.id.peerId, forwardAuthorId: peer.id, namespace: Namespaces.Message.Cloud) + let _ = strongSelf.context.engine.privacy.requestUpdatePeerIsBlocked(peerId: peer.id, isBlocked: true).start() + let context = strongSelf.context + let _ = (context.account.postbox.transaction { transasction -> Void in + context.engine.messages.deleteAllMessagesWithForwardAuthor(transaction: transasction, peerId: message.id.peerId, forwardAuthorId: peer.id, namespace: Namespaces.Message.Cloud) }).start() - let _ = reportRepliesMessage(account: strongSelf.context.account, messageId: message.id, deleteMessage: true, deleteHistory: true, reportSpam: reportSpam).start() + let _ = strongSelf.context.engine.peers.reportRepliesMessage(messageId: message.id, deleteMessage: true, deleteHistory: true, reportSpam: reportSpam).start() }) ] as [ActionSheetItem]) @@ -4921,10 +5187,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } if isAction && (actions.options == .deleteGlobally || actions.options == .deleteLocally) { - let _ = deleteMessagesInteractively(account: strongSelf.context.account, messageIds: Array(messageIds), type: actions.options == .deleteLocally ? .forLocalPeer : .forEveryone).start() + let _ = strongSelf.context.engine.messages.deleteMessagesInteractively(messageIds: Array(messageIds), type: actions.options == .deleteLocally ? .forLocalPeer : .forEveryone).start() completion(.dismissWithoutContent) } else if (messages.first?.flags.isSending ?? false) { - let _ = deleteMessagesInteractively(account: strongSelf.context.account, messageIds: Array(messageIds), type: .forEveryone, deleteAllInGroup: true).start() + let _ = strongSelf.context.engine.messages.deleteMessagesInteractively(messageIds: Array(messageIds), type: .forEveryone, deleteAllInGroup: true).start() completion(.dismissWithoutContent) } else { if actions.options.intersection([.deleteLocally, .deleteGlobally]).isEmpty { @@ -5218,11 +5484,11 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G threadId = makeMessageThreadId(replyThreadMessage.messageId) } - strongSelf.messageIndexDisposable.set((searchMessageIdByTimestamp(account: strongSelf.context.account, peerId: peerId, threadId: threadId, timestamp: timestamp) |> deliverOnMainQueue).start(next: { messageId in + strongSelf.messageIndexDisposable.set((strongSelf.context.engine.messages.searchMessageIdByTimestamp(peerId: peerId, threadId: threadId, timestamp: timestamp) |> deliverOnMainQueue).start(next: { messageId in if let strongSelf = self { strongSelf.loadingMessage.set(.single(nil)) if let messageId = messageId { - strongSelf.navigateToMessage(from: nil, to: .id(messageId), forceInCurrentChat: true) + strongSelf.navigateToMessage(from: nil, to: .id(messageId, nil), forceInCurrentChat: true) } } })) @@ -5250,7 +5516,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G strongSelf.updateItemNodesSearchTextHighlightStates() } }, navigateToMessage: { [weak self] messageId, dropStack, forceInCurrentChat, statusSubject in - self?.navigateToMessage(from: nil, to: .id(messageId), forceInCurrentChat: forceInCurrentChat, dropStack: dropStack, statusSubject: statusSubject) + self?.navigateToMessage(from: nil, to: .id(messageId, nil), forceInCurrentChat: forceInCurrentChat, dropStack: dropStack, statusSubject: statusSubject) }, navigateToChat: { [weak self] peerId in guard let strongSelf = self else { return @@ -5268,7 +5534,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G }, togglePeerNotifications: { [weak self] in if let strongSelf = self { let peerId = strongSelf.chatLocation.peerId - let _ = togglePeerMuted(account: strongSelf.context.account, peerId: peerId).start() + let _ = strongSelf.context.engine.peers.togglePeerMuted(peerId: peerId).start() } }, sendContextResult: { [weak self] results, result, node, rect in guard let strongSelf = self else { @@ -5301,13 +5567,16 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G $0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil).withUpdatedComposeInputState(ChatTextInputState(inputText: NSAttributedString(string: ""))).withUpdatedComposeDisableUrlPreview(nil) } }) } - }) + }, nil) var attributes: [MessageAttribute] = [] let entities = generateTextEntities(messageText, enabledTypes: .all) if !entities.isEmpty { attributes.append(TextEntitiesMessageAttribute(entities: entities)) } - strongSelf.sendMessages([.message(text: messageText, attributes: attributes, mediaReference: nil, replyToMessageId: replyMessageId, localGroupingKey: nil)]) + strongSelf.sendMessages([.message(text: messageText, attributes: attributes, mediaReference: nil, replyToMessageId: replyMessageId, localGroupingKey: nil, correlationId: nil)]) + strongSelf.interfaceInteraction?.updateShowCommands { _ in + return false + } } } }, sendBotStart: { [weak self] payload in @@ -5517,7 +5786,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return } - if let location = location, location.y < strongSelf.navigationHeight { + if let location = location, location.y < strongSelf.navigationLayout(layout: layout).navigationFrame.maxY { return } @@ -5589,7 +5858,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if let peer = peer as? TelegramSecretChat { let controller = ChatSecretAutoremoveTimerActionSheetController(context: strongSelf.context, currentValue: peer.messageAutoremoveTimeout == nil ? 0 : peer.messageAutoremoveTimeout!, applyValue: { value in if let strongSelf = self { - let _ = setSecretChatMessageAutoremoveTimeoutInteractively(account: strongSelf.context.account, peerId: peer.id, timeout: value == 0 ? nil : value).start() + let _ = strongSelf.context.engine.peers.setChatMessageAutoremoveTimeoutInteractively(peerId: peer.id, timeout: value == 0 ? nil : value).start() } }) strongSelf.present(controller, in: .window(.root)) @@ -5647,9 +5916,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } } - }, sendSticker: { [weak self] file, sourceNode, sourceRect in + }, sendSticker: { [weak self] file, clearInput, sourceNode, sourceRect in if let strongSelf = self, canSendMessagesToChat(strongSelf.presentationInterfaceState) { - return strongSelf.controllerInteraction?.sendSticker(file, nil, true, sourceNode, sourceRect) ?? false + return strongSelf.controllerInteraction?.sendSticker(file, false, false, nil, clearInput, sourceNode, sourceRect) ?? false } else { return false } @@ -5668,7 +5937,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G disposable = MetaDisposable() strongSelf.unpinMessageDisposable = disposable } - disposable.set(requestUpdatePinnedMessage(account: strongSelf.context.account, peerId: currentPeerId, update: .pin(id: messageId, silent: !notify, forThisPeerOnlyIfPossible: forThisPeerOnlyIfPossible)).start(completed: { + disposable.set(strongSelf.context.engine.messages.requestUpdatePinnedMessage(peerId: currentPeerId, update: .pin(id: messageId, silent: !notify, forThisPeerOnlyIfPossible: forThisPeerOnlyIfPossible)).start(completed: { guard let strongSelf = self else { return } @@ -5837,7 +6106,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G action: { action in switch action { case .commit: - disposable.set((requestUpdatePinnedMessage(account: strongSelf.context.account, peerId: peer.id, update: .clear(id: id)) + disposable.set((strongSelf.context.engine.messages.requestUpdatePinnedMessage(peerId: peer.id, update: .clear(id: id)) |> deliverOnMainQueue).start(error: { _ in guard let strongSelf = self else { return @@ -5887,7 +6156,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } switch action { case .commit: - let _ = (requestUpdatePinnedMessage(account: strongSelf.context.account, peerId: peer.id, update: .clear(id: id)) + let _ = (strongSelf.context.engine.messages.requestUpdatePinnedMessage(peerId: peer.id, update: .clear(id: id)) |> deliverOnMainQueue).start(completed: { Queue.mainQueue().after(1.0, { guard let strongSelf = self else { @@ -5907,7 +6176,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G in: .current ) } else { - disposable.set((requestUpdatePinnedMessage(account: strongSelf.context.account, peerId: peer.id, update: .clear(id: id)) + disposable.set((strongSelf.context.engine.messages.requestUpdatePinnedMessage(peerId: peer.id, update: .clear(id: id)) |> deliverOnMainQueue).start()) } } @@ -6048,7 +6317,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G |> switchToLatest |> deliverOnMainQueue).start(next: { [weak self] added in if let strongSelf = self { - strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .sticker(account: strongSelf.context.account, file: stickerFile, text: added ? strongSelf.presentationData.strings.Conversation_StickerAddedToFavorites : strongSelf.presentationData.strings.Conversation_StickerRemovedFromFavorites), elevatedLayout: false, action: { _ in return false }), in: .current) + strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .sticker(context: strongSelf.context, file: stickerFile, text: added ? strongSelf.presentationData.strings.Conversation_StickerAddedToFavorites : strongSelf.presentationData.strings.Conversation_StickerRemovedFromFavorites), elevatedLayout: false, action: { _ in return false }), in: .current) } }) } @@ -6126,7 +6395,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, opaqueIdentifiers: []) + let signal = strongSelf.context.engine.messages.requestMessageSelectPollOption(messageId: id, opaqueIdentifiers: []) |> afterDisposed { [weak controller] in Queue.mainQueue().async { controller?.dismiss() @@ -6188,7 +6457,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 = requestClosePoll(postbox: strongSelf.context.account.postbox, network: strongSelf.context.account.network, stateManager: strongSelf.context.account.stateManager, messageId: id) + let signal = strongSelf.context.engine.messages.requestClosePoll(messageId: id) |> afterDisposed { [weak controller] in Queue.mainQueue().async { controller?.dismiss() @@ -6285,7 +6554,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G guard let strongSelf = self else { return } - strongSelf.reportIrrelvantGeoDisposable = (TelegramCore.reportPeer(account: strongSelf.context.account, peerId: peerId, reason: .irrelevantLocation, message: "") + strongSelf.reportIrrelvantGeoDisposable = (strongSelf.context.engine.peers.reportPeer(peerId: peerId, reason: .irrelevantLocation, message: "") |> deliverOnMainQueue).start(completed: { [weak self] in if let strongSelf = self { strongSelf.reportIrrelvantGeoNoticePromise.set(.single(true)) @@ -6335,7 +6604,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G 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 + let controller = ChatSendMessageActionSheetController(context: strongSelf.context, controllerInteraction: strongSelf.controllerInteraction, interfaceState: strongSelf.presentationInterfaceState, gesture: gesture, sourceSendButton: node, textInputNode: textInputNode, completion: { [weak self] in if let strongSelf = self { strongSelf.supportedOrientations = previousSupportedOrientations } @@ -6395,7 +6664,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } if let navigationController = strongSelf.effectiveNavigationController { - let subject: ChatControllerSubject? = sourceMessageId.flatMap { ChatControllerSubject.message(id: $0, highlight: true) } + let subject: ChatControllerSubject? = sourceMessageId.flatMap { ChatControllerSubject.message(id: $0, highlight: true, timecode: nil) } strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .replyThread(replyThreadResult), subject: subject, keepStack: .always)) } }, activatePinnedListPreview: { [weak self] node, gesture in @@ -6471,6 +6740,12 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if let strongSelf = self { strongSelf.controllerInteraction?.editMessageMedia(messageId, draw) } + }, updateShowCommands: { [weak self] f in + if let strongSelf = self { + strongSelf.updateChatPresentationInterfaceState(interactive: true, { + return $0.updatedShowCommands(f($0.showCommands)) + }) + } }, statuses: ChatPanelInterfaceInteractionStatuses(editingMessage: self.editingMessage.get(), startingBot: self.startingBot.get(), unblockingPeer: self.unblockingPeer.get(), searching: self.searching.get(), loadingMessage: self.loadingMessage.get(), inlineSearch: self.performingInlineSearch.get())) do { @@ -6631,45 +6906,6 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G })) } - self.chatDisplayNode.updateHasEmbeddedTitleContent = { [weak self] in - guard let strongSelf = self else { - return - } - - let hasEmbeddedTitleContent = strongSelf.chatDisplayNode.hasEmbeddedTitleContent - let isEmbeddedTitleContentHidden = strongSelf.chatDisplayNode.isEmbeddedTitleContentHidden - - if strongSelf.hasEmbeddedTitleContent != hasEmbeddedTitleContent { - strongSelf.hasEmbeddedTitleContent = hasEmbeddedTitleContent - - if strongSelf.hasEmbeddedTitleContent { - strongSelf.statusBar.statusBarStyle = .White - } else { - strongSelf.statusBar.statusBarStyle = strongSelf.presentationData.theme.rootController.statusBarStyle.style - } - - if let navigationBar = strongSelf.navigationBar { - if let navigationBarCopy = navigationBar.view.snapshotContentTree() { - navigationBar.view.superview?.insertSubview(navigationBarCopy, aboveSubview: navigationBar.view) - navigationBarCopy.alpha = 0.0 - navigationBarCopy.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, completion: { [weak navigationBarCopy] _ in - navigationBarCopy?.removeFromSuperview() - }) - } - } - strongSelf.updateNavigationBarPresentation() - } - - if strongSelf.isEmbeddedTitleContentHidden != isEmbeddedTitleContentHidden { - strongSelf.isEmbeddedTitleContentHidden = isEmbeddedTitleContentHidden - - if let navigationBar = strongSelf.navigationBar { - let transition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .easeInOut) - transition.updateAlpha(node: navigationBar, alpha: isEmbeddedTitleContentHidden ? 0.0 : 1.0) - } - } - } - self.interfaceInteraction = interfaceInteraction if let search = self.focusOnSearchAfterAppearance { @@ -6773,6 +7009,17 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } override public func viewWillAppear(_ animated: Bool) { + #if DEBUG + if #available(iOSApplicationExtension 12.0, iOS 12.0, *) { + os_signpost( + .begin, + log: SignpostData.impl.signpostLog, + name: "Appear", + signpostID: SignpostData.impl.signpostId + ) + } + #endif + super.viewWillAppear(animated) if self.willAppear { @@ -6795,7 +7042,20 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } + private var returnInputViewFocus = false + override public func viewDidAppear(_ animated: Bool) { + #if DEBUG + if #available(iOSApplicationExtension 12.0, iOS 12.0, *) { + os_signpost( + .end, + log: SignpostData.impl.signpostLog, + name: "Appear", + signpostID: SignpostData.impl.signpostId + ) + } + #endif + super.viewDidAppear(animated) self.didAppear = true @@ -6808,7 +7068,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G 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.recentlyUsedInlineBotsDisposable = (self.context.engine.peers.recentlyUsedInlineBots() |> deliverOnMainQueue).start(next: { [weak self] peers in self?.recentlyUsedInlineBotsValue = peers.filter({ $0.1 >= 0.14 }).map({ $0.0 }) }) @@ -6862,16 +7122,26 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G strongSelf.voicePlaylistDidEndTimestamp = CACurrentMediaTime() raiseToListen.activateBasedOnProximity(delay: 0.0) } + + if strongSelf.returnInputViewFocus { + strongSelf.returnInputViewFocus = false + strongSelf.chatDisplayNode.ensureInputViewFocused() + } } self.tempVoicePlaylistItemChanged = { [weak self] previousItem, currentItem in guard let strongSelf = self, case let .peer(peerId) = strongSelf.chatLocation else { return } - if let currentItem = currentItem?.id as? PeerMessagesMediaPlaylistItemId, let previousItem = previousItem?.id as? PeerMessagesMediaPlaylistItemId, previousItem.messageId.peerId == peerId, currentItem.messageId.peerId == peerId, currentItem.messageId != previousItem.messageId { - if strongSelf.chatDisplayNode.historyNode.isMessageVisibleOnScreen(currentItem.messageId) { - strongSelf.navigateToMessage(from: nil, to: .id(currentItem.messageId), scrollPosition: .center(.bottom), rememberInStack: false, animated: true, completion: nil) - } - } + + strongSelf.chatDisplayNode.historyNode.voicePlaylistItemChanged(previousItem, currentItem) +// if let currentItem = currentItem?.id as? PeerMessagesMediaPlaylistItemId { +// self.controllerInteraction?.currentlyPlayingMessageId = currentItem.messageId +// if let previousItem = previousItem?.id as? PeerMessagesMediaPlaylistItemId, previousItem.messageId.peerId == peerId, currentItem.messageId.peerId == peerId, currentItem.messageId != previousItem.messageId { +// if strongSelf.chatDisplayNode.historyNode.isMessageVisibleOnScreen(currentItem.messageId) { +// strongSelf.navigateToMessage(from: nil, to: .id(currentItem.messageId, nil), scrollPosition: .center(.bottom), rememberInStack: false, animated: true, completion: nil) +// } +// } +// } } } @@ -6896,7 +7166,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G 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() + let _ = strongSelf.context.engine.messages.addSecretChatMessageScreenshot(peerId: peerId).start() return true } else { return false @@ -6905,7 +7175,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } if case let .peer(peerId) = self.chatLocation { - let _ = checkPeerChatServiceActions(postbox: self.context.account.postbox, peerId: peerId).start() + let _ = self.context.engine.peers.checkPeerChatServiceActions(peerId: peerId).start() } if self.chatDisplayNode.frameForInputActionButton() != nil { @@ -7032,7 +7302,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return } strongSelf.peekTimerDisposable.set( - (joinChatInteractively(with: peekData.linkData, account: strongSelf.context.account) + (strongSelf.context.engine.peers.joinChatInteractively(with: peekData.linkData) |> deliverOnMainQueue).start(next: { peerId in guard let strongSelf = self else { return @@ -7061,7 +7331,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G })) } - self.checksTooltipDisposable.set((getServerProvidedSuggestions(postbox: self.context.account.postbox) + self.checksTooltipDisposable.set((getServerProvidedSuggestions(account: self.context.account) |> deliverOnMainQueue).start(next: { [weak self] values in guard let strongSelf = self else { return @@ -7233,8 +7503,21 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return canManagePin } + + private var suspendNavigationBarLayout: Bool = false + private var suspendedNavigationBarLayout: ContainerViewLayout? + private var additionalNavigationBarBackgroundHeight: CGFloat = 0.0 + + override public func updateNavigationBarLayout(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + if self.suspendNavigationBarLayout { + self.suspendedNavigationBarLayout = layout + return + } + self.applyNavigationBarLayout(layout, navigationLayout: self.navigationLayout(layout: layout), additionalBackgroundHeight: self.additionalNavigationBarBackgroundHeight, transition: transition) + } override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + self.suspendNavigationBarLayout = true super.containerLayoutUpdated(layout, transition: transition) self.validLayout = layout @@ -7252,25 +7535,30 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } - self.chatDisplayNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationHeight, transition: transition, listViewTransaction: { updateSizeAndInsets, additionalScrollDistance, scrollToTop, completion in + self.chatDisplayNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition, listViewTransaction: { updateSizeAndInsets, additionalScrollDistance, scrollToTop, completion in self.chatDisplayNode.historyNode.updateLayout(transition: transition, updateSizeAndInsets: updateSizeAndInsets, additionalScrollDistance: additionalScrollDistance, scrollToTop: scrollToTop, completion: completion) + }, updateExtraNavigationBarBackgroundHeight: { value in + self.additionalNavigationBarBackgroundHeight = value }) - } - - override public func updateToInterfaceOrientation(_ orientation: UIInterfaceOrientation) { - guard let layout = self.validLayout, case .compact = layout.metrics.widthClass else { - return - } - let hasOverlayNodes = self.context.sharedContext.mediaManager.overlayMediaManager.controller?.hasNodes ?? false - if self.validLayout != nil && orientation.isLandscape && !hasOverlayNodes && self.traceVisibility() && isTopmostChatController(self) { - var completed = false - self.chatDisplayNode.historyNode.forEachVisibleItemNode { itemNode in - if !completed, let itemNode = itemNode as? ChatMessageItemView, let message = itemNode.item?.message, let (_, soundEnabled, _, _, _) = itemNode.playMediaWithSound(), soundEnabled { - let _ = self.controllerInteraction?.openMessage(message, .landscape) - completed = true + + if case .compact = layout.metrics.widthClass { + let hasOverlayNodes = self.context.sharedContext.mediaManager.overlayMediaManager.controller?.hasNodes ?? false + if self.validLayout != nil && layout.size.width > layout.size.height && !hasOverlayNodes && self.traceVisibility() && isTopmostChatController(self) { + var completed = false + self.chatDisplayNode.historyNode.forEachVisibleItemNode { itemNode in + if !completed, let itemNode = itemNode as? ChatMessageItemView, let message = itemNode.item?.message, let (_, soundEnabled, _, _, _) = itemNode.playMediaWithSound(), soundEnabled { + let _ = self.controllerInteraction?.openMessage(message, .landscape) + completed = true + } } } } + + self.suspendNavigationBarLayout = false + if let suspendedNavigationBarLayout = self.suspendedNavigationBarLayout { + self.suspendedNavigationBarLayout = suspendedNavigationBarLayout + self.applyNavigationBarLayout(suspendedNavigationBarLayout, navigationLayout: self.navigationLayout(layout: layout), additionalBackgroundHeight: self.additionalNavigationBarBackgroundHeight, transition: transition) + } } func updateChatPresentationInterfaceState(animated: Bool = true, interactive: Bool, saveInterfaceState: Bool = false, _ f: (ChatPresentationInterfaceState) -> ChatPresentationInterfaceState, completion: @escaping (ContainedViewLayoutTransition) -> Void = { _ in }) { @@ -7281,9 +7569,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G var completion = externalCompletion var temporaryChatPresentationInterfaceState = f(self.presentationInterfaceState) - if self.presentationInterfaceState.keyboardButtonsMessage?.visibleButtonKeyboardMarkup != temporaryChatPresentationInterfaceState.keyboardButtonsMessage?.visibleButtonKeyboardMarkup { + if self.presentationInterfaceState.keyboardButtonsMessage?.visibleButtonKeyboardMarkup != temporaryChatPresentationInterfaceState.keyboardButtonsMessage?.visibleButtonKeyboardMarkup || self.presentationInterfaceState.keyboardButtonsMessage?.id != temporaryChatPresentationInterfaceState.keyboardButtonsMessage?.id { if let keyboardButtonsMessage = temporaryChatPresentationInterfaceState.keyboardButtonsMessage, let _ = keyboardButtonsMessage.visibleButtonKeyboardMarkup { - if self.presentationInterfaceState.interfaceState.editMessage == nil && self.presentationInterfaceState.interfaceState.composeInputState.inputText.length == 0 && keyboardButtonsMessage.id != temporaryChatPresentationInterfaceState.interfaceState.messageActionsState.closedButtonKeyboardMessageId && temporaryChatPresentationInterfaceState.botStartPayload == nil { + if self.presentationInterfaceState.interfaceState.editMessage == nil && self.presentationInterfaceState.interfaceState.composeInputState.inputText.length == 0 && keyboardButtonsMessage.id != temporaryChatPresentationInterfaceState.interfaceState.messageActionsState.closedButtonKeyboardMessageId && keyboardButtonsMessage.id != temporaryChatPresentationInterfaceState.interfaceState.messageActionsState.dismissedButtonKeyboardMessageId && temporaryChatPresentationInterfaceState.botStartPayload == nil { temporaryChatPresentationInterfaceState = temporaryChatPresentationInterfaceState.updatedInputMode({ _ in return .inputButtons }) @@ -7661,6 +7949,12 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G itemNode.updateSelectionState(animated: animated) } } + + self.chatDisplayNode.historyNode.forEachItemHeaderNode{ itemHeaderNode in + if let avatarNode = itemHeaderNode as? ChatMessageAvatarHeaderNode { + avatarNode.updateSelectionState(animated: animated) + } + } } private func updatePollTooltipMessageState(animated: Bool) { @@ -7725,7 +8019,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G self.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withoutSelectionState() } }) case .clearHistory: if case let .peer(peerId) = self.chatLocation { - let account = self.context.account + let context = self.context let beginClear: (InteractiveHistoryClearingType) -> Void = { [weak self] type in guard let strongSelf = self else { @@ -7745,7 +8039,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G strongSelf.present(UndoOverlayController(presentationData: strongSelf.context.sharedContext.currentPresentationData.with { $0 }, content: .removedChat(text: statusText), elevatedLayout: false, action: { value in if value == .commit { - let _ = clearHistoryInteractively(postbox: account.postbox, peerId: peerId, type: type).start(completed: { + let _ = context.engine.messages.clearHistoryInteractively(peerId: peerId, type: type).start(completed: { self?.chatDisplayNode.historyNode.historyAppearsCleared = false }) return true @@ -8042,7 +8336,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } let peerId = peer.id - let _ = (collectCacheUsageStats(account: strongSelf.context.account, peerId: peer.id) + let _ = (strongSelf.context.engine.resources.collectCacheUsageStats(peerId: peer.id) |> deliverOnMainQueue).start(next: { [weak self, weak controller] result in controller?.dismiss() @@ -8069,7 +8363,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if filteredSize == 0 { title = presentationData.strings.Cache_ClearNone } else { - title = presentationData.strings.Cache_Clear("\(dataSizeString(filteredSize, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator))").0 + title = presentationData.strings.Cache_Clear("\(dataSizeString(filteredSize, formatting: DataSizeStringFormatting(presentationData: presentationData)))").0 } if let item = item as? ActionSheetButtonItem { @@ -8122,7 +8416,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G totalSize += categorySize if categorySize > 1024 { let index = itemIndex - items.append(ActionSheetCheckboxItem(title: stringForCategory(strings: presentationData.strings, category: categoryId), label: dataSizeString(categorySize, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator), value: true, action: { value in + items.append(ActionSheetCheckboxItem(title: stringForCategory(strings: presentationData.strings, category: categoryId), label: dataSizeString(categorySize, formatting: DataSizeStringFormatting(presentationData: presentationData)), value: true, action: { value in toggleCheck(categoryId, index) })) itemIndex += 1 @@ -8134,7 +8428,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if items.isEmpty { strongSelf.presentClearCacheSuggestion() } else { - items.append(ActionSheetButtonItem(title: presentationData.strings.Cache_Clear("\(dataSizeString(totalSize, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator))").0, action: { + items.append(ActionSheetButtonItem(title: presentationData.strings.Cache_Clear("\(dataSizeString(totalSize, formatting: DataSizeStringFormatting(presentationData: presentationData)))").0, action: { let clearCategories = sizeIndex.keys.filter({ sizeIndex[$0]!.0 }) var clearMediaIds = Set() @@ -8161,7 +8455,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } - var signal = clearCachedMediaResources(account: strongSelf.context.account, mediaResourceIds: clearResourceIds) + var signal = strongSelf.context.engine.resources.clearCachedMediaResources(mediaResourceIds: clearResourceIds) var cancelImpl: (() -> Void)? let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } @@ -8192,7 +8486,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G disposable.set((signal |> deliverOnMainQueue).start(completed: { [weak self] in if let strongSelf = self, let _ = strongSelf.validLayout { - strongSelf.present(UndoOverlayController(presentationData: presentationData, content: .succeed(text: presentationData.strings.ClearCache_Success("\(dataSizeString(selectedSize, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator))", stringForDeviceType()).0), elevatedLayout: false, action: { _ in return false }), in: .current) + strongSelf.present(UndoOverlayController(presentationData: presentationData, content: .succeed(text: presentationData.strings.ClearCache_Success("\(dataSizeString(selectedSize, formatting: DataSizeStringFormatting(presentationData: presentationData)))", stringForDeviceType()).0), elevatedLayout: false, action: { _ in return false }), in: .current) } })) @@ -8247,7 +8541,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } private func editMessageMediaWithMessages(_ messages: [EnqueueMessage]) { - if let message = messages.first, case let .message(text, _, maybeMediaReference, _, _) = message, let mediaReference = maybeMediaReference { + if let message = messages.first, case let .message(text, _, maybeMediaReference, _, _, _) = message, let mediaReference = maybeMediaReference { self.updateChatPresentationInterfaceState(animated: true, interactive: true, { state in var state = state if let editMessageState = state.editMessageState, case let .media(options) = editMessageState.content, !options.isEmpty { @@ -8270,7 +8564,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G private func editMessageMediaWithLegacySignals(_ signals: [Any]) { let _ = (legacyAssetPickerEnqueueMessages(account: self.context.account, signals: signals) |> deliverOnMainQueue).start(next: { [weak self] messages in - self?.editMessageMediaWithMessages(messages) + self?.editMessageMediaWithMessages(messages.map { $0.message }) }) } @@ -8479,14 +8773,27 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G done(time) }) } - }, sendMessagesWithSignals: { [weak self] signals, silentPosting, scheduleTime in + }, sendMessagesWithSignals: { [weak self] signals, silentPosting, scheduleTime, getAnimatedTransitionSource, completion in + guard let strongSelf = self else { + completion() + return + } if !inputText.string.isEmpty { //strongSelf.clearInputText() } if editMediaOptions != nil { - self?.editMessageMediaWithLegacySignals(signals!) + strongSelf.editMessageMediaWithLegacySignals(signals!) + completion() } else { - self?.enqueueMediaMessages(signals: signals, silentPosting: silentPosting, scheduleTime: scheduleTime > 0 ? scheduleTime : nil) + let immediateCompletion = getAnimatedTransitionSource == nil + strongSelf.enqueueMediaMessages(signals: signals, silentPosting: silentPosting, scheduleTime: scheduleTime > 0 ? scheduleTime : nil, getAnimatedTransitionSource: getAnimatedTransitionSource, completion: { + if !immediateCompletion { + completion() + } + }) + if immediateCompletion { + completion() + } } }, selectRecentlyUsedInlineBot: { [weak self] peer in if let strongSelf = self, let addressName = peer.addressName { @@ -8523,7 +8830,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G let presentationDisposable = strongSelf.context.sharedContext.presentationData.start(next: { [weak controller] presentationData in if let controller = controller { - controller.pallete = legacyMenuPaletteFromTheme(presentationData.theme) + controller.pallete = legacyMenuPaletteFromTheme(presentationData.theme, forceDark: false) } }) legacyController.disposables.add(presentationDisposable) @@ -8581,17 +8888,17 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } if fileTypes.music != fileTypes.other { - groupingKey = arc4random64() + groupingKey = Int64.random(in: Int64.min ... Int64.max) } var messages: [EnqueueMessage] = [] for item in results { if let item = item { - let fileId = arc4random64() + let fileId = Int64.random(in: Int64.min ... Int64.max) let mimeType = guessMimeTypeByFileExtension((item.fileName as NSString).pathExtension) var previewRepresentations: [TelegramMediaImageRepresentation] = [] if mimeType.hasPrefix("image/") || mimeType == "application/pdf" { - previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 320, height: 320), resource: ICloudFileResource(urlData: item.urlData, thumbnail: true), progressiveSizes: [])) + previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 320, height: 320), resource: ICloudFileResource(urlData: item.urlData, thumbnail: true), progressiveSizes: [], immediateThumbnailData: nil)) } var attributes: [TelegramMediaFileAttribute] = [] attributes.append(.FileName(fileName: item.fileName)) @@ -8600,11 +8907,11 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: fileId), partialReference: nil, resource: ICloudFileResource(urlData: item.urlData, thumbnail: false), previewRepresentations: previewRepresentations, videoThumbnails: [], immediateThumbnailData: nil, mimeType: mimeType, size: item.fileSize, attributes: attributes) - let message: EnqueueMessage = .message(text: "", attributes: [], mediaReference: .standalone(media: file), replyToMessageId: replyMessageId, localGroupingKey: groupingKey) + let message: EnqueueMessage = .message(text: "", attributes: [], mediaReference: .standalone(media: file), replyToMessageId: replyMessageId, localGroupingKey: groupingKey, correlationId: nil) messages.append(message) } if let _ = groupingKey, messages.count % 10 == 0 { - groupingKey = arc4random64() + groupingKey = Int64.random(in: Int64.min ... Int64.max) } } @@ -8618,7 +8925,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G $0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil) } }) } - }) + }, nil) strongSelf.sendMessages(messages) } } @@ -8838,14 +9145,14 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return } let replyMessageId = strongSelf.presentationInterfaceState.interfaceState.replyMessageId - let message: EnqueueMessage = .message(text: "", attributes: [], mediaReference: .standalone(media: location), replyToMessageId: replyMessageId, localGroupingKey: nil) + let message: EnqueueMessage = .message(text: "", attributes: [], mediaReference: .standalone(media: location), replyToMessageId: replyMessageId, localGroupingKey: nil, correlationId: nil) strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ if let strongSelf = self { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { $0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil) } }) } - }) + }, nil) strongSelf.sendMessages([message]) }) strongSelf.effectiveNavigationController?.pushViewController(controller) @@ -8854,58 +9161,37 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } private func presentContactPicker() { - let contactsController = ContactSelectionControllerImpl(ContactSelectionControllerParams(context: self.context, title: { $0.Contacts_Title }, displayDeviceContacts: true)) + let contactsController = ContactSelectionControllerImpl(ContactSelectionControllerParams(context: self.context, title: { $0.Contacts_Title }, displayDeviceContacts: true, multipleSelection: true)) contactsController.navigationPresentation = .modal self.chatDisplayNode.dismissInput() self.effectiveNavigationController?.pushViewController(contactsController) self.controllerNavigationDisposable.set((contactsController.result - |> deliverOnMainQueue).start(next: { [weak self] peer in - if let strongSelf = self, let (peer, _) = peer { - let dataSignal: Signal<(Peer?, DeviceContactExtendedData?), NoError> - switch peer { - case let .peer(contact, _, _): - 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: [], note: "") - let context = strongSelf.context - dataSignal = (strongSelf.context.sharedContext.contactDataManager?.basicData() ?? .single([:])) - |> take(1) - |> mapToSignal { basicData -> Signal<(Peer?, DeviceContactExtendedData?), NoError> 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 - } + |> deliverOnMainQueue).start(next: { [weak self] peers in + if let strongSelf = self, let (peers, _) = peers { + if peers.count > 1 { + var enqueueMessages: [EnqueueMessage] = [] + for peer in peers { + var media: TelegramMediaContact? + switch peer { + case let .peer(contact, _, _): + guard let contact = contact as? TelegramUser, let phoneNumber = contact.phone else { + continue } - } - - if let stableId = stableId { - return (context.sharedContext.contactDataManager?.extendedData(stableId: stableId) ?? .single(nil)) - |> take(1) - |> map { extendedData -> (Peer?, DeviceContactExtendedData?) in - return (contact, extendedData) + 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 phone = contactData.basicData.phoneNumbers[0].value + media = TelegramMediaContact(firstName: contactData.basicData.firstName, lastName: contactData.basicData.lastName, phoneNumber: phone, peerId: contact.id, vCardData: nil) + case let .deviceContact(_, basicData): + guard !basicData.phoneNumbers.isEmpty else { + continue } - } else { - return .single((contact, contactData)) - } + let contactData = DeviceContactExtendedData(basicData: basicData, middleName: "", prefix: "", suffix: "", organization: "", jobTitle: "", department: "", emailAddresses: [], urls: [], addresses: [], birthdayDate: nil, socialProfiles: [], instantMessagingProfiles: [], note: "") + + let phone = contactData.basicData.phoneNumbers[0].value + media = TelegramMediaContact(firstName: contactData.basicData.firstName, lastName: contactData.basicData.lastName, phoneNumber: phone, peerId: nil, vCardData: nil) } - case let .deviceContact(id, _): - dataSignal = (strongSelf.context.sharedContext.contactDataManager?.extendedData(stableId: id) ?? .single(nil)) - |> take(1) - |> map { extendedData -> (Peer?, DeviceContactExtendedData?) in - return (nil, extendedData) - } - } - strongSelf.controllerNavigationDisposable.set((dataSignal - |> deliverOnMainQueue).start(next: { peerAndContactData in - if let strongSelf = self, let contactData = peerAndContactData.1, contactData.basicData.phoneNumbers.count != 0 { - if contactData.isPrimitive { - let phone = contactData.basicData.phoneNumbers[0].value - let media = TelegramMediaContact(firstName: contactData.basicData.firstName, lastName: contactData.basicData.lastName, phoneNumber: phone, peerId: peerAndContactData.0?.id, vCardData: nil) + + if let media = media { let replyMessageId = strongSelf.presentationInterfaceState.interfaceState.replyMessageId strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ if let strongSelf = self { @@ -8913,33 +9199,93 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G $0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil) } }) } - }) - let message = EnqueueMessage.message(text: "", attributes: [], mediaReference: .standalone(media: media), replyToMessageId: replyMessageId, localGroupingKey: nil) - strongSelf.sendMessages([message]) - } else { - let contactController = strongSelf.context.sharedContext.makeDeviceContactInfoController(context: strongSelf.context, subject: .filter(peer: peerAndContactData.0, contactId: nil, contactData: contactData, completion: { peer, contactData in - guard let strongSelf = self, !contactData.basicData.phoneNumbers.isEmpty else { - return - } - let phone = contactData.basicData.phoneNumbers[0].value - if let vCardData = contactData.serializedVCard() { - let media = TelegramMediaContact(firstName: contactData.basicData.firstName, lastName: contactData.basicData.lastName, phoneNumber: phone, peerId: peer?.id, vCardData: vCardData) - let replyMessageId = strongSelf.presentationInterfaceState.interfaceState.replyMessageId - strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ - if let strongSelf = self { - strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { - $0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil) } - }) - } - }) - let message = EnqueueMessage.message(text: "", attributes: [], mediaReference: .standalone(media: media), replyToMessageId: replyMessageId, localGroupingKey: nil) - strongSelf.sendMessages([message]) - } - }), completed: nil, cancelled: nil) - strongSelf.effectiveNavigationController?.pushViewController(contactController) + }, nil) + let message = EnqueueMessage.message(text: "", attributes: [], mediaReference: .standalone(media: media), replyToMessageId: replyMessageId, localGroupingKey: nil, correlationId: nil) + enqueueMessages.append(message) } } - })) + strongSelf.sendMessages(enqueueMessages) + } else if let peer = peers.first { + let dataSignal: Signal<(Peer?, DeviceContactExtendedData?), NoError> + switch peer { + case let .peer(contact, _, _): + 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: [], note: "") + let context = strongSelf.context + dataSignal = (strongSelf.context.sharedContext.contactDataManager?.basicData() ?? .single([:])) + |> take(1) + |> mapToSignal { basicData -> Signal<(Peer?, DeviceContactExtendedData?), NoError> 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 -> (Peer?, DeviceContactExtendedData?) in + return (contact, extendedData) + } + } else { + return .single((contact, contactData)) + } + } + case let .deviceContact(id, _): + dataSignal = (strongSelf.context.sharedContext.contactDataManager?.extendedData(stableId: id) ?? .single(nil)) + |> take(1) + |> map { extendedData -> (Peer?, DeviceContactExtendedData?) in + return (nil, extendedData) + } + } + strongSelf.controllerNavigationDisposable.set((dataSignal + |> deliverOnMainQueue).start(next: { peerAndContactData in + if let strongSelf = self, let contactData = peerAndContactData.1, contactData.basicData.phoneNumbers.count != 0 { + if contactData.isPrimitive { + let phone = contactData.basicData.phoneNumbers[0].value + let media = TelegramMediaContact(firstName: contactData.basicData.firstName, lastName: contactData.basicData.lastName, phoneNumber: phone, peerId: peerAndContactData.0?.id, vCardData: nil) + let replyMessageId = strongSelf.presentationInterfaceState.interfaceState.replyMessageId + strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ + if let strongSelf = self { + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { + $0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil) } + }) + } + }, nil) + let message = EnqueueMessage.message(text: "", attributes: [], mediaReference: .standalone(media: media), replyToMessageId: replyMessageId, localGroupingKey: nil, correlationId: nil) + strongSelf.sendMessages([message]) + } else { + let contactController = strongSelf.context.sharedContext.makeDeviceContactInfoController(context: strongSelf.context, subject: .filter(peer: peerAndContactData.0, contactId: nil, contactData: contactData, completion: { peer, contactData in + guard let strongSelf = self, !contactData.basicData.phoneNumbers.isEmpty else { + return + } + let phone = contactData.basicData.phoneNumbers[0].value + if let vCardData = contactData.serializedVCard() { + let media = TelegramMediaContact(firstName: contactData.basicData.firstName, lastName: contactData.basicData.lastName, phoneNumber: phone, peerId: peer?.id, vCardData: vCardData) + let replyMessageId = strongSelf.presentationInterfaceState.interfaceState.replyMessageId + strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ + if let strongSelf = self { + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { + $0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil) } + }) + } + }, nil) + let message = EnqueueMessage.message(text: "", attributes: [], mediaReference: .standalone(media: media), replyToMessageId: replyMessageId, localGroupingKey: nil, correlationId: nil) + strongSelf.sendMessages([message]) + } + }), completed: nil, cancelled: nil) + strongSelf.effectiveNavigationController?.pushViewController(contactController) + } + } + })) + } } })) } @@ -9250,7 +9596,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G $0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil) } }) } - }) + }, nil) strongSelf.sendMessages([message.withUpdatedReplyToMessageId(replyMessageId)]) })) } @@ -9304,9 +9650,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } if let value = value { - self.present(UndoOverlayController(presentationData: self.presentationData, content: .dice(dice: dice, account: self.context.account, text: value, action: canSendMessagesToChat(self.presentationInterfaceState) ? self.presentationData.strings.Conversation_SendDice : nil), elevatedLayout: false, action: { [weak self] action in + self.present(UndoOverlayController(presentationData: self.presentationData, content: .dice(dice: dice, context: self.context, text: value, action: canSendMessagesToChat(self.presentationInterfaceState) ? self.presentationData.strings.Conversation_SendDice : nil), elevatedLayout: false, action: { [weak self] action in if let strongSelf = self, canSendMessagesToChat(strongSelf.presentationInterfaceState), action == .undo { - strongSelf.sendMessages([.message(text: "", attributes: [], mediaReference: AnyMediaReference.standalone(media: TelegramMediaDice(emoji: dice.emoji)), replyToMessageId: nil, localGroupingKey: nil)]) + strongSelf.sendMessages([.message(text: "", attributes: [], mediaReference: AnyMediaReference.standalone(media: TelegramMediaDice(emoji: dice.emoji)), replyToMessageId: nil, localGroupingKey: nil, correlationId: nil)]) } return false }), in: .current) @@ -9327,9 +9673,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if let defaultReplyMessageId = defaultReplyMessageId { switch message { - case let .message(text, attributes, mediaReference, replyToMessageId, localGroupingKey): + case let .message(text, attributes, mediaReference, replyToMessageId, localGroupingKey, correlationId): if replyToMessageId == nil { - message = .message(text: text, attributes: attributes, mediaReference: mediaReference, replyToMessageId: defaultReplyMessageId, localGroupingKey: localGroupingKey) + message = .message(text: text, attributes: attributes, mediaReference: mediaReference, replyToMessageId: defaultReplyMessageId, localGroupingKey: localGroupingKey, correlationId: correlationId) } case .forward: break @@ -9379,6 +9725,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G }) donateSendMessageIntent(account: self.context.account, sharedContext: self.context.sharedContext, intentContext: .chat, peerIds: [peerId]) + + self.updateChatPresentationInterfaceState(interactive: true, { $0.updatedShowCommands(false) }) } else { self.presentScheduleTimePicker(dismissByTapOutside: false, completion: { [weak self] time in if let strongSelf = self { @@ -9388,11 +9736,35 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } - private func enqueueMediaMessages(signals: [Any]?, silentPosting: Bool, scheduleTime: Int32? = nil) { + private func enqueueMediaMessages(signals: [Any]?, silentPosting: Bool, scheduleTime: Int32? = nil, getAnimatedTransitionSource: ((String) -> UIView?)? = nil, completion: @escaping () -> Void = {}) { self.enqueueMediaMessageDisposable.set((legacyAssetPickerEnqueueMessages(account: self.context.account, signals: signals!) - |> deliverOnMainQueue).start(next: { [weak self] messages in + |> deliverOnMainQueue).start(next: { [weak self] items in if let strongSelf = self { - let messages = strongSelf.transformEnqueueMessages(messages, silentPosting: silentPosting, scheduleTime: scheduleTime) + var completionImpl: (() -> Void)? = completion + + var usedCorrelationId: Int64? + + var mappedMessages: [EnqueueMessage] = [] + for item in items { + var message = item.message + if let uniqueId = item.uniqueId { + let correlationId = Int64.random(in: 0 ..< Int64.max) + message = message.withUpdatedCorrelationId(correlationId) + + if items.count == 1, let getAnimatedTransitionSource = getAnimatedTransitionSource { + usedCorrelationId = correlationId + completionImpl = nil + strongSelf.chatDisplayNode.messageTransitionNode.add(correlationId: correlationId, source: .mediaInput(ChatMessageTransitionNode.Source.MediaInput(extractSnapshot: { + return getAnimatedTransitionSource(uniqueId) + })), initiated: { + completion() + }) + } + } + mappedMessages.append(message) + } + + let messages = strongSelf.transformEnqueueMessages(mappedMessages, silentPosting: silentPosting, scheduleTime: scheduleTime) let replyMessageId = strongSelf.presentationInterfaceState.interfaceState.replyMessageId strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ if let strongSelf = self { @@ -9400,7 +9772,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G $0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil) } }) } - }) + completionImpl?() + }, usedCorrelationId) + strongSelf.sendMessages(messages.map { $0.withUpdatedReplyToMessageId(replyMessageId) }) } })) @@ -9463,7 +9837,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G $0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil) } }) } - }) + }, nil) strongSelf.sendMessages([message].map { $0.withUpdatedReplyToMessageId(replyMessageId) }) } })) @@ -9479,7 +9853,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G $0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil) } }) } - }) + }, nil) strongSelf.sendMessages([message].map { $0.withUpdatedReplyToMessageId(replyMessageId) }) } })) @@ -9489,7 +9863,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G let size = image.size.aspectFitted(CGSize(width: 512.0, height: 512.0)) self.enqueueMediaMessageDisposable.set((convertToWebP(image: image, targetSize: size, targetBoundingSize: size, quality: 0.9) |> deliverOnMainQueue).start(next: { [weak self] data in if let strongSelf = self, !data.isEmpty { - let resource = LocalFileMediaResource(fileId: arc4random64()) + let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) strongSelf.context.account.postbox.mediaBox.storeResourceData(resource.id, data: data) var fileAttributes: [TelegramMediaFileAttribute] = [] @@ -9497,8 +9871,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G fileAttributes.append(.Sticker(displayText: "", packReference: nil, maskData: nil)) fileAttributes.append(.ImageSize(size: PixelDimensions(size))) - let media = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: arc4random64()), partialReference: nil, resource: resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "image/webp", size: data.count, attributes: fileAttributes) - let message = EnqueueMessage.message(text: "", attributes: [], mediaReference: .standalone(media: media), replyToMessageId: nil, localGroupingKey: nil) + let media = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: Int64.random(in: Int64.min ... Int64.max)), partialReference: nil, resource: resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "image/webp", size: data.count, attributes: fileAttributes) + let message = EnqueueMessage.message(text: "", attributes: [], mediaReference: .standalone(media: media), replyToMessageId: nil, localGroupingKey: nil, correlationId: nil) let replyMessageId = strongSelf.presentationInterfaceState.interfaceState.replyMessageId strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ @@ -9507,17 +9881,22 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G $0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil) } }) } - }) + }, nil) strongSelf.sendMessages([message].map { $0.withUpdatedReplyToMessageId(replyMessageId) }) } })) } - private func enqueueChatContextResult(_ results: ChatContextResultCollection, _ result: ChatContextResult, hideVia: Bool = false, closeMediaInput: Bool = false) { + private func enqueueChatContextResult(_ results: ChatContextResultCollection, _ result: ChatContextResult, hideVia: Bool = false, closeMediaInput: Bool = false, silentPosting: Bool = false) { + if !canSendMessagesToChat(self.presentationInterfaceState) { + return + } + let peerId = self.chatLocation.peerId - - if let message = outgoingMessageWithChatContextResult(to: peerId, results: results, result: result, hideVia: hideVia), canSendMessagesToChat(self.presentationInterfaceState) { - let replyMessageId = self.presentationInterfaceState.interfaceState.replyMessageId + + let replyMessageId = self.presentationInterfaceState.interfaceState.replyMessageId + + if self.context.engine.messages.enqueueOutgoingMessageWithChatContextResult(to: peerId, results: results, result: result, replyToMessageId: replyMessageId, hideVia: hideVia, silentPosting: silentPosting) { self.chatDisplayNode.setupSendActionOnViewUpdate({ [weak self] in if let strongSelf = self { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { state in @@ -9538,8 +9917,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return state }) } - }) - self.sendMessages([message.withUpdatedReplyToMessageId(replyMessageId)]) + }, nil) } } @@ -9608,17 +9986,42 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G isScheduledMessages = true } - self.videoRecorder.set(.single(legacyInstantVideoController(theme: self.presentationData.theme, panelFrame: self.view.convert(currentInputPanelFrame, to: nil), context: self.context, peerId: peerId, slowmodeState: !isScheduledMessages ? self.presentationInterfaceState.slowmodeState : nil, hasSchedule: !isScheduledMessages && peerId.namespace != Namespaces.Peer.SecretChat, 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: !isScheduledMessages ? self.presentationInterfaceState.slowmodeState : nil, hasSchedule: !isScheduledMessages && peerId.namespace != Namespaces.Peer.SecretChat, send: { [weak self] videoController, message in if let strongSelf = self { + guard let message = message else { + strongSelf.videoRecorder.set(.single(nil)) + return + } + let replyMessageId = strongSelf.presentationInterfaceState.interfaceState.replyMessageId + let correlationId = Int64.random(in: 0 ..< Int64.max) + let updatedMessage = message + .withUpdatedReplyToMessageId(replyMessageId) + .withUpdatedCorrelationId(correlationId) + + var usedCorrelationId = false + + if strongSelf.chatDisplayNode.shouldAnimateMessageTransition, let extractedView = videoController.extractVideoSnapshot() { + usedCorrelationId = true + strongSelf.chatDisplayNode.messageTransitionNode.add(correlationId: correlationId, source: .videoMessage(ChatMessageTransitionNode.Source.VideoMessage(view: extractedView)), initiated: { [weak videoController] in + videoController?.hideVideoSnapshot() + guard let strongSelf = self else { + return + } + strongSelf.videoRecorder.set(.single(nil)) + }) + } else { + strongSelf.videoRecorder.set(.single(nil)) + } + strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ if let strongSelf = self { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { $0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil) } }) } - }) - let updatedMessage = message.withUpdatedReplyToMessageId(replyMessageId) + }, usedCorrelationId ? correlationId : nil) + strongSelf.sendMessages([updatedMessage]) } }, displaySlowmodeTooltip: { [weak self] node, rect in @@ -9656,7 +10059,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G switch updatedAction { case .dismiss: self.chatDisplayNode.updateRecordedMediaDeleted(true) - break + self.audioRecorder.set(.single(nil)) case .preview: self.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInputTextPanelState { panelState in @@ -9674,7 +10077,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } }) } else if let waveform = data.waveform { - let resource = LocalFileMediaResource(fileId: arc4random64(), size: data.compressedData.count) + let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max), size: data.compressedData.count) strongSelf.context.account.postbox.mediaBox.storeResourceData(resource.id, data: data.compressedData) @@ -9687,6 +10090,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } }) + self.audioRecorder.set(.single(nil)) case .send: self.chatDisplayNode.updateRecordedMediaDeleted(false) let _ = (audioRecorderValue.takenRecordedData() @@ -9695,8 +10099,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if data.duration < 0.5 { strongSelf.recorderFeedback?.error() strongSelf.recorderFeedback = nil + strongSelf.audioRecorder.set(.single(nil)) } else { - let randomId = arc4random64() + let randomId = Int64.random(in: Int64.min ... Int64.max) let resource = LocalFileMediaResource(fileId: randomId) strongSelf.context.account.postbox.mediaBox.storeResourceData(resource.id, data: data.compressedData) @@ -9705,16 +10110,31 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if let waveform = data.waveform { waveformBuffer = MemoryBuffer(data: waveform) } - + + let correlationId = Int64.random(in: 0 ..< Int64.max) + var usedCorrelationId = false + + if strongSelf.chatDisplayNode.shouldAnimateMessageTransition, let textInputPanelNode = strongSelf.chatDisplayNode.textInputPanelNode, let micButton = textInputPanelNode.micButton { + usedCorrelationId = true + strongSelf.chatDisplayNode.messageTransitionNode.add(correlationId: correlationId, source: .audioMicInput(ChatMessageTransitionNode.Source.AudioMicInput(micButton: micButton)), initiated: { + guard let strongSelf = self else { + return + } + strongSelf.audioRecorder.set(.single(nil)) + }) + } else { + strongSelf.audioRecorder.set(.single(nil)) + } + strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ if let strongSelf = self { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { $0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil) } }) } - }) + }, usedCorrelationId ? correlationId : nil) - strongSelf.sendMessages([.message(text: "", attributes: [], mediaReference: .standalone(media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: randomId), partialReference: nil, resource: resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "audio/ogg", size: data.compressedData.count, attributes: [.Audio(isVoice: true, duration: Int(data.duration), title: nil, performer: nil, waveform: waveformBuffer)])), replyToMessageId: strongSelf.presentationInterfaceState.interfaceState.replyMessageId, localGroupingKey: nil)]) + strongSelf.sendMessages([.message(text: "", attributes: [], mediaReference: .standalone(media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: randomId), partialReference: nil, resource: resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "audio/ogg", size: data.compressedData.count, attributes: [.Audio(isVoice: true, duration: Int(data.duration), title: nil, performer: nil, waveform: waveformBuffer)])), replyToMessageId: strongSelf.presentationInterfaceState.interfaceState.replyMessageId, localGroupingKey: nil, correlationId: correlationId)]) strongSelf.recorderFeedback?.tap() strongSelf.recorderFeedback = nil @@ -9722,12 +10142,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } }) } - self.audioRecorder.set(.single(nil)) } else if let videoRecorderValue = self.videoRecorderValue { if case .send = updatedAction { self.chatDisplayNode.updateRecordedMediaDeleted(false) videoRecorderValue.completeVideo() - self.videoRecorder.set(.single(nil)) } else { if case .dismiss = updatedAction { self.chatDisplayNode.updateRecordedMediaDeleted(true) @@ -9808,9 +10226,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G $0.updatedRecordedMediaPreview(nil).updatedInterfaceState { $0.withUpdatedReplyMessageId(nil) } }) } - }) + }, nil) - let messages: [EnqueueMessage] = [.message(text: "", attributes: [], mediaReference: .standalone(media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: arc4random64()), partialReference: nil, resource: recordedMediaPreview.resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "audio/ogg", size: Int(recordedMediaPreview.fileSize), attributes: [.Audio(isVoice: true, duration: Int(recordedMediaPreview.duration), title: nil, performer: nil, waveform: waveformBuffer)])), replyToMessageId: self.presentationInterfaceState.interfaceState.replyMessageId, localGroupingKey: nil)] + let messages: [EnqueueMessage] = [.message(text: "", attributes: [], mediaReference: .standalone(media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: Int64.random(in: Int64.min ... Int64.max)), partialReference: nil, resource: recordedMediaPreview.resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "audio/ogg", size: Int(recordedMediaPreview.fileSize), attributes: [.Audio(isVoice: true, duration: Int(recordedMediaPreview.duration), title: nil, performer: nil, waveform: waveformBuffer)])), replyToMessageId: self.presentationInterfaceState.interfaceState.replyMessageId, localGroupingKey: nil, correlationId: nil)] let transformedMessages: [EnqueueMessage] if let silentPosting = silentPosting { @@ -9899,7 +10317,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G self.searchDisposable = searchDisposable } - let search = searchMessages(account: self.context.account, location: searchState.location, query: searchState.query, state: nil, limit: limit) + let search = self.context.engine.messages.searchMessages(location: searchState.location, query: searchState.query, state: nil, limit: limit) |> delay(0.2, queue: Queue.mainQueue()) self.searchResult.set(search |> map { (result, state) -> (SearchMessagesResult, SearchMessagesState, SearchMessagesLocation)? in @@ -9953,7 +10371,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G searchDisposable = MetaDisposable() self.searchDisposable = searchDisposable } - searchDisposable.set((searchMessages(account: self.context.account, location: searchState.location, query: searchState.query, state: loadMoreState, limit: limit) + searchDisposable.set((self.context.engine.messages.searchMessages(location: searchState.location, query: searchState.query, state: loadMoreState, limit: limit) |> delay(0.2, queue: Queue.mainQueue()) |> deliverOnMainQueue).start(next: { [weak self] results, updatedState in guard let strongSelf = self else { @@ -10205,9 +10623,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G let subject: ChatControllerSubject? if let atMessageId = atMessageId { - subject = .message(id: atMessageId, highlight: true) + subject = .message(id: atMessageId, highlight: true, timecode: nil) } else if let index = result.scrollToLowerBoundMessage { - subject = .message(id: index.id, highlight: false) + subject = .message(id: index.id, highlight: false, timecode: nil) } else { subject = nil } @@ -10271,11 +10689,11 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if isPinnedMessages, let messageId = messageLocation.messageId { if let navigationController = self.effectiveNavigationController { self.dismiss() - self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(messageId.peerId), subject: .message(id: messageId, highlight: true), keepStack: .always)) + self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(messageId.peerId), subject: .message(id: messageId, highlight: true, timecode: nil), keepStack: .always)) } } else if case let .peer(peerId) = self.chatLocation, let messageId = messageLocation.messageId, (messageId.peerId != peerId && !forceInCurrentChat) || (isScheduledMessages && messageId.id != 0 && !Namespaces.Message.allScheduled.contains(messageId.namespace)) { if let navigationController = self.effectiveNavigationController { - self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(messageId.peerId), subject: .message(id: messageId, highlight: true), keepStack: .always)) + self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(messageId.peerId), subject: .message(id: messageId, highlight: true, timecode: nil), keepStack: .always)) } } else if forceInCurrentChat { if let _ = fromId, let fromIndex = fromIndex, rememberInStack { @@ -10297,13 +10715,17 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G self.messageIndexDisposable.set(nil) self.chatDisplayNode.historyNode.scrollToMessage(from: scrollFromIndex, to: message.index, animated: animated, scrollPosition: scrollPosition) completion?() + + if case let .id(_, maybeTimecode) = messageLocation, let timecode = maybeTimecode { + let _ = self.controllerInteraction?.openMessage(message, .timecode(timecode)) + } } else if case let .index(index) = messageLocation, index.id.id == 0, index.timestamp > 0, case .scheduledMessages = self.presentationInterfaceState.subject { self.chatDisplayNode.historyNode.scrollToMessage(from: scrollFromIndex, to: index, animated: animated, scrollPosition: scrollPosition) } else { self.loadingMessage.set(.single(statusSubject) |> delay(0.1, queue: .mainQueue())) let searchLocation: ChatHistoryInitialSearchLocation switch messageLocation { - case let .id(id): + case let .id(id, _): searchLocation = .id(id) case let .index(index): searchLocation = .index(index) @@ -10396,7 +10818,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if let fromIndex = fromIndex { let searchLocation: ChatHistoryInitialSearchLocation switch messageLocation { - case let .id(id): + case let .id(id, _): searchLocation = .id(id) case let .index(index): searchLocation = .index(index) @@ -10430,7 +10852,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G completion?() } else { if let navigationController = strongSelf.effectiveNavigationController { - strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(messageLocation.peerId), subject: messageLocation.messageId.flatMap { .message(id: $0, highlight: true) })) + strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(messageLocation.peerId), subject: messageLocation.messageId.flatMap { .message(id: $0, highlight: true, timecode: nil) })) } completion?() } @@ -10442,7 +10864,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G })) } else { if let navigationController = self.effectiveNavigationController { - self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(messageLocation.peerId), subject: messageLocation.messageId.flatMap { .message(id: $0, highlight: true) })) + self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(messageLocation.peerId), subject: messageLocation.messageId.flatMap { .message(id: $0, highlight: true, timecode: nil) })) } completion?() } @@ -10481,7 +10903,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G var attemptSelectionImpl: ((Peer) -> Void)? let controller = self.context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: self.context, filter: filter, attemptSelection: { peer in attemptSelectionImpl?(peer) - })) + }, multipleSelection: true)) let context = self.context attemptSelectionImpl = { [weak controller] peer in guard let controller = controller else { @@ -10496,6 +10918,82 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } 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.multiplePeersSelected = { [weak self, weak controller] peers, messageText in + guard let strongSelf = self, let strongController = controller else { + return + } + strongController.dismiss() + + for peer in peers { + var result: [EnqueueMessage] = [] + if messageText.string.count > 0 { + let inputText = convertMarkdownToAttributes(messageText) + for text in breakChatInputText(trimChatInputText(inputText)) { + if text.length != 0 { + var attributes: [MessageAttribute] = [] + let entities = generateTextEntities(text.string, enabledTypes: .all, currentEntities: generateChatInputTextEntities(text)) + if !entities.isEmpty { + attributes.append(TextEntitiesMessageAttribute(entities: entities)) + } + result.append(.message(text: text.string, attributes: attributes, mediaReference: nil, replyToMessageId: nil, localGroupingKey: nil, correlationId: nil)) + } + } + } + + result.append(contentsOf: messages.map { message -> EnqueueMessage in + return .forward(source: message.id, grouping: .auto, attributes: [], correlationId: nil) + }) + + let _ = (enqueueMessages(account: strongSelf.context.account, peerId: peer.id, messages: result) + |> deliverOnMainQueue).start(next: { 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) + }) + if strongSelf.shareStatusDisposable == nil { + strongSelf.shareStatusDisposable = MetaDisposable() + } + strongSelf.shareStatusDisposable?.set((combineLatest(signals) + |> deliverOnMainQueue).start()) + } + }) + + let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } + let text: String + var savedMessages = false + if peers.count == 1, let peerId = peers.first?.id, peerId == strongSelf.context.account.peerId { + text = messages.count == 1 ? presentationData.strings.Conversation_ForwardTooltip_SavedMessages_One : presentationData.strings.Conversation_ForwardTooltip_SavedMessages_Many + savedMessages = true + } else { + if peers.count == 1, let peer = peers.first { + let peerName = peer.id == strongSelf.context.account.peerId ? presentationData.strings.DialogList_SavedMessages : peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + text = messages.count == 1 ? presentationData.strings.Conversation_ForwardTooltip_Chat_One(peerName).0 : presentationData.strings.Conversation_ForwardTooltip_Chat_Many(peerName).0 + } else if peers.count == 2, let firstPeer = peers.first, let secondPeer = peers.last { + let firstPeerName = firstPeer.id == strongSelf.context.account.peerId ? presentationData.strings.DialogList_SavedMessages : firstPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + let secondPeerName = secondPeer.id == strongSelf.context.account.peerId ? presentationData.strings.DialogList_SavedMessages : secondPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + text = messages.count == 1 ? presentationData.strings.Conversation_ForwardTooltip_TwoChats_One(firstPeerName, secondPeerName).0 : presentationData.strings.Conversation_ForwardTooltip_TwoChats_Many(firstPeerName, secondPeerName).0 + } else if let peer = peers.first { + let peerName = peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + text = messages.count == 1 ? presentationData.strings.Conversation_ForwardTooltip_ManyChats_One(peerName, "\(peers.count - 1)").0 : presentationData.strings.Conversation_ForwardTooltip_ManyChats_Many(peerName, "\(peers.count - 1)").0 + } else { + text = "" + } + } + + strongSelf.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: savedMessages, text: text), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), in: .current) + } + } controller.peerSelected = { [weak self, weak controller] peer in let peerId = peer.id @@ -10528,7 +11026,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G }), in: .current) let _ = (enqueueMessages(account: strongSelf.context.account, peerId: peerId, messages: messages.map { message -> EnqueueMessage in - return .forward(source: message.id, grouping: .auto, attributes: []) + return .forward(source: message.id, grouping: .auto, attributes: [], correlationId: nil) }) |> deliverOnMainQueue).start(next: { messageIds in if let strongSelf = self { @@ -10755,7 +11253,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G disposable = MetaDisposable() self.resolvePeerByNameDisposable = disposable } - var resolveSignal = resolvePeerByName(account: self.context.account, name: name, ageLimit: 10) + var resolveSignal = self.context.engine.peers.resolvePeerByName(name: name, ageLimit: 10) var cancelImpl: (() -> Void)? let presentationData = self.presentationData @@ -10821,7 +11319,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G let account = self.context.account var resolveSignal: Signal if let peerName = peerName { - resolveSignal = resolvePeerByName(account: self.context.account, name: peerName) + resolveSignal = self.context.engine.peers.resolvePeerByName(name: peerName) |> mapToSignal { peerId -> Signal in if let peerId = peerId { return account.postbox.loadedPeerWithId(peerId) @@ -10881,12 +11379,12 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if let user = self.presentationInterfaceState.renderedPeer?.peer as? TelegramUser, user.botInfo != nil { restartBot = true } - self.editMessageDisposable.set((requestUpdatePeerIsBlocked(account: self.context.account, peerId: peerId, isBlocked: false) + self.editMessageDisposable.set((self.context.engine.privacy.requestUpdatePeerIsBlocked(peerId: peerId, isBlocked: false) |> afterDisposed({ [weak self] in Queue.mainQueue().async { unblockingPeer.set(false) if let strongSelf = self, restartBot { - let _ = enqueueMessages(account: strongSelf.context.account, peerId: peerId, messages: [.message(text: "/start", attributes: [], mediaReference: nil, replyToMessageId: nil, localGroupingKey: nil)]).start() + let _ = enqueueMessages(account: strongSelf.context.account, peerId: peerId, messages: [.message(text: "/start", attributes: [], mediaReference: nil, replyToMessageId: nil, localGroupingKey: nil, correlationId: nil)]).start() } } })).start()) @@ -10951,17 +11449,15 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G guard let strongSelf = self else { return } - let _ = requestUpdatePeerIsBlocked(account: strongSelf.context.account, peerId: peer.id, isBlocked: true).start() + let _ = strongSelf.context.engine.privacy.requestUpdatePeerIsBlocked(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, requestRemoteHistoryRemoval: true) - }).start() + let _ = strongSelf.context.engine.peers.terminateSecretChat(peerId: chatPeer.id, requestRemoteHistoryRemoval: true).start() } if deleteChat { - let _ = removePeerChat(account: strongSelf.context.account, peerId: chatPeer.id, reportChatSpam: reportSpam).start() + let _ = strongSelf.context.engine.peers.removePeerChat(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, message: "").start() + let _ = strongSelf.context.engine.peers.reportPeer(peerId: peer.id, reason: .spam, message: "").start() } }) ] as [ActionSheetItem]) @@ -11031,7 +11527,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G guard let strongSelf = self else { return } - let _ = (acceptAndShareContact(account: strongSelf.context.account, peerId: peer.id) + let _ = (strongSelf.context.engine.contacts.acceptAndShareContact(peerId: peer.id) |> deliverOnMainQueue).start(error: { _ in guard let strongSelf = self else { return @@ -11081,7 +11577,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } else { dismissPeerId = peerId } - self.editMessageDisposable.set((dismissPeerStatusOptions(account: self.context.account, peerId: dismissPeerId) + self.editMessageDisposable.set((self.context.engine.peers.dismissPeerStatusOptions(peerId: dismissPeerId) |> afterDisposed({ Queue.mainQueue().async { } @@ -11094,10 +11590,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } self.commitPurposefulAction() self.chatDisplayNode.historyNode.disconnect() - let _ = removePeerChat(account: self.context.account, peerId: peerId, reportChatSpam: reportChatSpam).start() + let _ = self.context.engine.peers.removePeerChat(peerId: peerId, reportChatSpam: reportChatSpam).start() self.effectiveNavigationController?.popToRoot(animated: true) - let _ = requestUpdatePeerIsBlocked(account: self.context.account, peerId: peerId, isBlocked: true).start() + let _ = self.context.engine.privacy.requestUpdatePeerIsBlocked(peerId: peerId, isBlocked: true).start() } private func startBot(_ payload: String?) { @@ -11107,7 +11603,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G let startingBot = self.startingBot startingBot.set(true) - self.editMessageDisposable.set((requestStartBot(account: self.context.account, botPeerId: peerId, payload: payload) |> deliverOnMainQueue |> afterDisposed({ + self.editMessageDisposable.set((self.context.engine.messages.requestStartBot(botPeerId: peerId, payload: payload) |> deliverOnMainQueue |> afterDisposed({ startingBot.set(false) })).start(completed: { [weak self] in if let strongSelf = self { @@ -11124,8 +11620,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G switch navigation { case let .chat(_, subject, peekData): if case .peer(peerId) = strongSelf.chatLocation { - if let subject = subject, case let .message(messageId, _) = subject { - strongSelf.navigateToMessage(from: nil, to: .id(messageId)) + if let subject = subject, case let .message(messageId, _, timecode) = subject { + strongSelf.navigateToMessage(from: nil, to: .id(messageId, timecode)) } } else if let navigationController = strongSelf.effectiveNavigationController { strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(peerId), subject: subject, keepStack: .always, peekData: peekData)) @@ -11151,9 +11647,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G default: break } - }, sendFile: nil, - sendSticker: { [weak self] f, sourceNode, sourceRect in - return self?.interfaceInteraction?.sendSticker(f, sourceNode, sourceRect) ?? false + }, sendFile: nil, + sendSticker: { [weak self] f, sourceNode, sourceRect in + return self?.interfaceInteraction?.sendSticker(f, true, sourceNode, sourceRect) ?? false }, requestMessageActionUrlAuth: { [weak self] subject in if case let .url(url) = subject { self?.controllerInteraction?.requestMessageActionUrlAuth(url, subject) @@ -11223,7 +11719,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } - openUserGeneratedUrl(context: self.context, url: url, concealed: concealed, skipUrlAuth: skipUrlAuth, present: { [weak self] c in + openUserGeneratedUrl(context: self.context, peerId: self.peerView?.peerId, url: url, concealed: concealed, skipUrlAuth: skipUrlAuth, present: { [weak self] c in self?.present(c, in: .window(.root)) }, openResolved: { [weak self] resolved in self?.openResolved(resolved) @@ -11325,7 +11821,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if #available(iOSApplicationExtension 9.0, iOS 9.0, *) { let controller = SFSafariViewController(url: parsedUrl) if #available(iOSApplicationExtension 10.0, iOS 10.0, *) { - controller.preferredBarTintColor = self.presentationData.theme.rootController.navigationBar.backgroundColor + controller.preferredBarTintColor = self.presentationData.theme.rootController.navigationBar.opaqueBackgroundColor controller.preferredControlTintColor = self.presentationData.theme.rootController.navigationBar.accentTextColor } return (controller, sourceRect) @@ -11337,52 +11833,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return nil } - func previewingCommit(_ viewControllerToCommit: UIViewController) { - if let gallery = viewControllerToCommit as? AvatarGalleryController { - self.chatDisplayNode.dismissInput() - gallery.setHintWillBePresentedInPreviewingContext(false) - self.present(gallery, in: .window(.root), with: AvatarGalleryControllerPresentationArguments(animated: false, transitionArguments: { _ in - return nil - })) - } else if let gallery = viewControllerToCommit as? GalleryController { - self.chatDisplayNode.dismissInput() - gallery.setHintWillBePresentedInPreviewingContext(false) - - self.present(gallery, in: .window(.root), with: GalleryControllerPresentationArguments(animated: false, transitionArguments: { [weak self] messageId, media in - if let strongSelf = self { - 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) { - selectedTransitionNode = result - } - } - } - if let selectedTransitionNode = selectedTransitionNode { - return GalleryTransitionArguments(transitionNode: selectedTransitionNode, addToTransitionSurface: { view in - if let strongSelf = self { - strongSelf.chatDisplayNode.historyNode.view.superview?.insertSubview(view, aboveSubview: strongSelf.chatDisplayNode.historyNode.view) - } - }) - } - } - return nil - })) - } - - if #available(iOSApplicationExtension 9.0, iOS 9.0, *) { - if let safariController = viewControllerToCommit as? SFSafariViewController { - if let window = self.effectiveNavigationController?.view.window { - window.rootViewController?.present(safariController, animated: true) - } - } - } - } - private func presentBanMessageOptions(accountPeerId: PeerId, author: Peer, messageIds: Set, options: ChatAvailableMessageActionOptions) { let peerId = self.chatLocation.peerId do { - self.navigationActionDisposable.set((fetchChannelParticipant(account: self.context.account, peerId: peerId, participantId: author.id) + self.navigationActionDisposable.set((self.context.engine.peers.fetchChannelParticipant(peerId: peerId, participantId: author.id) |> deliverOnMainQueue).start(next: { [weak self] participant in if let strongSelf = self { let canBan = participant?.canBeBannedBy(peerId: accountPeerId) ?? true @@ -11436,16 +11890,16 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if let strongSelf = self { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withoutSelectionState() } }) if actions.contains(3) { - let mediaBox = strongSelf.context.account.postbox.mediaBox - let _ = strongSelf.context.account.postbox.transaction({ transaction -> Void in - deleteAllMessagesWithAuthor(transaction: transaction, mediaBox: mediaBox, peerId: peerId, authorId: author.id, namespace: Namespaces.Message.Cloud) + let context = strongSelf.context + let _ = context.account.postbox.transaction({ transaction -> Void in + context.engine.messages.deleteAllMessagesWithAuthor(transaction: transaction, peerId: peerId, authorId: author.id, namespace: Namespaces.Message.Cloud) }).start() - let _ = clearAuthorHistory(account: strongSelf.context.account, peerId: peerId, memberId: author.id).start() + let _ = strongSelf.context.engine.messages.clearAuthorHistory(peerId: peerId, memberId: author.id).start() } else if actions.contains(0) { - let _ = deleteMessagesInteractively(account: strongSelf.context.account, messageIds: Array(messageIds), type: .forEveryone).start() + let _ = strongSelf.context.engine.messages.deleteMessagesInteractively(messageIds: Array(messageIds), type: .forEveryone).start() } if actions.contains(1) { - let _ = removePeerMember(account: strongSelf.context.account, peerId: peerId, memberId: author.id).start() + let _ = strongSelf.context.engine.peers.removePeerMember(peerId: peerId, memberId: author.id).start() } } })) @@ -11462,7 +11916,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } - private func presentDeleteMessageOptions(messageIds: Set, options: ChatAvailableMessageActionOptions, contextController: ContextController?, completion: @escaping (ContextMenuActionResult) -> Void) { + private func presentDeleteMessageOptions(messageIds: Set, options: ChatAvailableMessageActionOptions, contextController: ContextControllerProtocol?, completion: @escaping (ContextMenuActionResult) -> Void) { let actionSheet = ActionSheetController(presentationData: self.presentationData) var items: [ActionSheetItem] = [] var personalPeerName: String? @@ -11480,7 +11934,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(account: strongSelf.context.account, messageIds: Array(messageIds), type: .forEveryone).start() + let _ = strongSelf.context.engine.messages.deleteMessagesInteractively(messageIds: Array(messageIds), type: .forEveryone).start() } })) } @@ -11507,7 +11961,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(account: strongSelf.context.account, messageIds: Array(messageIds), type: .forEveryone).start() + let _ = strongSelf.context.engine.messages.deleteMessagesInteractively(messageIds: Array(messageIds), type: .forEveryone).start() f(.dismissWithoutContent) } }))) @@ -11515,7 +11969,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(account: strongSelf.context.account, messageIds: Array(messageIds), type: .forEveryone).start() + let _ = strongSelf.context.engine.messages.deleteMessagesInteractively(messageIds: Array(messageIds), type: .forEveryone).start() } })) } @@ -11537,7 +11991,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(account: strongSelf.context.account, messageIds: Array(messageIds), type: unsendPersonalMessages ? .forEveryone : .forLocalPeer).start() + let _ = strongSelf.context.engine.messages.deleteMessagesInteractively(messageIds: Array(messageIds), type: unsendPersonalMessages ? .forEveryone : .forLocalPeer).start() f(.dismissWithoutContent) } }))) @@ -11545,7 +11999,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(account: strongSelf.context.account, messageIds: Array(messageIds), type: unsendPersonalMessages ? .forEveryone : .forLocalPeer).start() + let _ = strongSelf.context.engine.messages.deleteMessagesInteractively(messageIds: Array(messageIds), type: unsendPersonalMessages ? .forEveryone : .forLocalPeer).start() } })) } @@ -12122,7 +12576,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G switch action { case .commit: - let _ = (requestUnpinAllMessages(account: strongSelf.context.account, peerId: strongSelf.chatLocation.peerId) + let _ = (strongSelf.context.engine.messages.requestUnpinAllMessages(peerId: strongSelf.chatLocation.peerId) |> deliverOnMainQueue).start(error: { _ in }, completed: { guard let strongSelf = self else { diff --git a/submodules/TelegramUI/Sources/ChatControllerInteraction.swift b/submodules/TelegramUI/Sources/ChatControllerInteraction.swift index 2863f406e8..c842ab6271 100644 --- a/submodules/TelegramUI/Sources/ChatControllerInteraction.swift +++ b/submodules/TelegramUI/Sources/ChatControllerInteraction.swift @@ -53,6 +53,7 @@ public final class ChatControllerInteraction { let openPeer: (PeerId?, ChatControllerInteractionNavigateToPeer, Message?) -> Void let openPeerMention: (String) -> Void let openMessageContextMenu: (Message, Bool, ASDisplayNode, CGRect, UIGestureRecognizer?) -> Void + let activateMessagePinch: (PinchSourceContainerNode) -> Void let openMessageContextActions: (Message, ASDisplayNode, CGRect, ContextGesture?) -> Void let navigateToMessage: (MessageId, MessageId) -> Void let navigateToMessageStandalone: (MessageId) -> Void @@ -61,9 +62,9 @@ public final class ChatControllerInteraction { let toggleMessagesSelection: ([MessageId], Bool) -> Void let sendCurrentMessage: (Bool) -> Void let sendMessage: (String) -> Void - let sendSticker: (FileMediaReference, String?, Bool, ASDisplayNode, CGRect) -> Bool - let sendGif: (FileMediaReference, ASDisplayNode, CGRect) -> Bool - let sendBotContextResultAsGif: (ChatContextResultCollection, ChatContextResult, ASDisplayNode, CGRect) -> Bool + let sendSticker: (FileMediaReference, Bool, Bool, String?, Bool, ASDisplayNode, CGRect) -> Bool + let sendGif: (FileMediaReference, ASDisplayNode, CGRect, Bool, Bool) -> Bool + let sendBotContextResultAsGif: (ChatContextResultCollection, ChatContextResult, ASDisplayNode, CGRect, Bool) -> Bool let requestMessageActionCallback: (MessageId, MemoryBuffer?, Bool, Bool) -> Void let requestMessageActionUrlAuth: (String, MessageActionUrlSubject) -> Void let activateSwitchInline: (PeerId?, String) -> Void @@ -113,7 +114,6 @@ public final class ChatControllerInteraction { let displayPsa: (String, ASDisplayNode) -> Void let displayDiceTooltip: (TelegramMediaDice) -> Void let animateDiceSuccess: (Bool) -> Void - let greetingStickerNode: () -> (ASDisplayNode, ASDisplayNode, ASDisplayNode, (@escaping () -> Void) -> Void)? let openPeerContextMenu: (Peer, MessageId?, ASDisplayNode, CGRect, ContextGesture?) -> Void let openMessageReplies: (MessageId, Bool, Bool) -> Void let openReplyThreadOriginalMessage: (Message) -> Void @@ -121,6 +121,7 @@ public final class ChatControllerInteraction { let editMessageMedia: (MessageId, Bool) -> Void let copyText: (String) -> Void let displayUndo: (UndoOverlayContent) -> Void + let isAnimatingMessage: (UInt32) -> Bool let requestMessageUpdate: (MessageId) -> Void let cancelInteractiveKeyboardGestures: () -> Void @@ -138,12 +139,14 @@ public final class ChatControllerInteraction { var searchTextHighightState: (String, [MessageIndex])? var seenOneTimeAnimatedMedia = Set() var currentMessageWithLoadingReplyThread: MessageId? + let presentationContext: ChatPresentationContext init( openMessage: @escaping (Message, ChatControllerInteractionOpenMessageMode) -> Bool, openPeer: @escaping (PeerId?, ChatControllerInteractionNavigateToPeer, Message?) -> Void, openPeerMention: @escaping (String) -> Void, openMessageContextMenu: @escaping (Message, Bool, ASDisplayNode, CGRect, UIGestureRecognizer?) -> Void, + activateMessagePinch: @escaping (PinchSourceContainerNode) -> Void, openMessageContextActions: @escaping (Message, ASDisplayNode, CGRect, ContextGesture?) -> Void, navigateToMessage: @escaping (MessageId, MessageId) -> Void, navigateToMessageStandalone: @escaping (MessageId) -> Void, @@ -152,9 +155,9 @@ public final class ChatControllerInteraction { toggleMessagesSelection: @escaping ([MessageId], Bool) -> Void, sendCurrentMessage: @escaping (Bool) -> Void, sendMessage: @escaping (String) -> Void, - sendSticker: @escaping (FileMediaReference, String?, Bool, ASDisplayNode, CGRect) -> Bool, - sendGif: @escaping (FileMediaReference, ASDisplayNode, CGRect) -> Bool, - sendBotContextResultAsGif: @escaping (ChatContextResultCollection, ChatContextResult, ASDisplayNode, CGRect) -> Bool, + sendSticker: @escaping (FileMediaReference, Bool, Bool, String?, Bool, ASDisplayNode, CGRect) -> Bool, + sendGif: @escaping (FileMediaReference, ASDisplayNode, CGRect, Bool, Bool) -> Bool, + sendBotContextResultAsGif: @escaping (ChatContextResultCollection, ChatContextResult, ASDisplayNode, CGRect, Bool) -> Bool, requestMessageActionCallback: @escaping (MessageId, MemoryBuffer?, Bool, Bool) -> Void, requestMessageActionUrlAuth: @escaping (String, MessageActionUrlSubject) -> Void, activateSwitchInline: @escaping (PeerId?, String) -> Void, @@ -204,7 +207,6 @@ public final class ChatControllerInteraction { displayPsa: @escaping (String, ASDisplayNode) -> Void, displayDiceTooltip: @escaping (TelegramMediaDice) -> Void, animateDiceSuccess: @escaping (Bool) -> Void, - greetingStickerNode: @escaping () -> (ASDisplayNode, ASDisplayNode, ASDisplayNode, (@escaping () -> Void) -> Void)?, openPeerContextMenu: @escaping (Peer, MessageId?, ASDisplayNode, CGRect, ContextGesture?) -> Void, openMessageReplies: @escaping (MessageId, Bool, Bool) -> Void, openReplyThreadOriginalMessage: @escaping (Message) -> Void, @@ -212,16 +214,19 @@ public final class ChatControllerInteraction { editMessageMedia: @escaping (MessageId, Bool) -> Void, copyText: @escaping (String) -> Void, displayUndo: @escaping (UndoOverlayContent) -> Void, + isAnimatingMessage: @escaping (UInt32) -> Bool, requestMessageUpdate: @escaping (MessageId) -> Void, cancelInteractiveKeyboardGestures: @escaping () -> Void, automaticMediaDownloadSettings: MediaAutoDownloadSettings, pollActionState: ChatInterfacePollActionState, - stickerSettings: ChatInterfaceStickerSettings + stickerSettings: ChatInterfaceStickerSettings, + presentationContext: ChatPresentationContext ) { self.openMessage = openMessage self.openPeer = openPeer self.openPeerMention = openPeerMention self.openMessageContextMenu = openMessageContextMenu + self.activateMessagePinch = activateMessagePinch self.openMessageContextActions = openMessageContextActions self.navigateToMessage = navigateToMessage self.navigateToMessageStandalone = navigateToMessageStandalone @@ -282,7 +287,6 @@ public final class ChatControllerInteraction { self.openMessagePollResults = openMessagePollResults self.displayDiceTooltip = displayDiceTooltip self.animateDiceSuccess = animateDiceSuccess - self.greetingStickerNode = greetingStickerNode self.openPeerContextMenu = openPeerContextMenu self.openMessageReplies = openMessageReplies self.openReplyThreadOriginalMessage = openReplyThreadOriginalMessage @@ -290,6 +294,7 @@ public final class ChatControllerInteraction { self.editMessageMedia = editMessageMedia self.copyText = copyText self.displayUndo = displayUndo + self.isAnimatingMessage = isAnimatingMessage self.requestMessageUpdate = requestMessageUpdate self.cancelInteractiveKeyboardGestures = cancelInteractiveKeyboardGestures @@ -297,11 +302,13 @@ public final class ChatControllerInteraction { self.pollActionState = pollActionState self.stickerSettings = stickerSettings + + self.presentationContext = presentationContext } static var `default`: ChatControllerInteraction { return ChatControllerInteraction(openMessage: { _, _ in - return false }, openPeer: { _, _, _ in }, openPeerMention: { _ in }, openMessageContextMenu: { _, _, _, _, _ in }, openMessageContextActions: { _, _, _, _ in }, navigateToMessage: { _, _ in }, navigateToMessageStandalone: { _ in }, tapMessage: nil, clickThroughMessage: { }, toggleMessagesSelection: { _, _ in }, sendCurrentMessage: { _ in }, sendMessage: { _ in }, sendSticker: { _, _, _, _, _ in return false }, sendGif: { _, _, _ in return false }, sendBotContextResultAsGif: { _, _, _, _ 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 }, activateMessagePinch: { _ in }, openMessageContextActions: { _, _, _, _ in }, navigateToMessage: { _, _ in }, navigateToMessageStandalone: { _ in }, tapMessage: nil, clickThroughMessage: { }, toggleMessagesSelection: { _, _ in }, sendCurrentMessage: { _ in }, sendMessage: { _ in }, sendSticker: { _, _, _, _, _, _, _ in return false }, sendGif: { _, _, _, _, _ in return false }, sendBotContextResultAsGif: { _, _, _, _, _ 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: { @@ -335,8 +342,6 @@ public final class ChatControllerInteraction { }, displayPsa: { _, _ in }, displayDiceTooltip: { _ in }, animateDiceSuccess: { _ in - }, greetingStickerNode: { - return nil }, openPeerContextMenu: { _, _, _, _, _ in }, openMessageReplies: { _, _, _ in }, openReplyThreadOriginalMessage: { _ in @@ -344,9 +349,14 @@ public final class ChatControllerInteraction { }, editMessageMedia: { _, _ in }, copyText: { _ in }, displayUndo: { _ in + }, isAnimatingMessage: { _ in + return false }, requestMessageUpdate: { _ in }, cancelInteractiveKeyboardGestures: { }, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings, - pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(loopAnimatedStickers: false)) + pollActionState: ChatInterfacePollActionState(), + stickerSettings: ChatInterfaceStickerSettings(loopAnimatedStickers: false), + presentationContext: ChatPresentationContext(backgroundNode: nil) + ) } } diff --git a/submodules/TelegramUI/Sources/ChatControllerNode.swift b/submodules/TelegramUI/Sources/ChatControllerNode.swift index 3c4e4dbec0..19193334b4 100644 --- a/submodules/TelegramUI/Sources/ChatControllerNode.swift +++ b/submodules/TelegramUI/Sources/ChatControllerNode.swift @@ -16,6 +16,7 @@ import TelegramUniversalVideoContent import ChatInterfaceState import FastBlur import ConfettiEffect +import WallpaperBackgroundNode final class VideoNavigationControllerDropContentItem: NavigationControllerDropContentItem { let itemNode: OverlayMediaItemNode @@ -55,227 +56,10 @@ private struct ChatControllerNodeDerivedLayoutState { var inputContextPanelsFrame: CGRect var inputContextPanelsOverMainPanelFrame: CGRect var inputNodeHeight: CGFloat? + var inputNodeAdditionalHeight: CGFloat? var upperInputPositionBound: CGFloat? } -private final class ChatEmbeddedTitleContentNode: ASDisplayNode { - private let context: AccountContext - private let backgroundNode: ASDisplayNode - private let statusBarBackgroundNode: ASDisplayNode - private let videoNode: OverlayUniversalVideoNode - private let disableInternalAnimationIn: Bool - private let isUIHiddenUpdated: () -> Void - private let unembedWhenPortrait: (OverlayMediaItemNode) -> Bool - - private var validLayout: (CGSize, CGFloat, CGFloat, CGFloat)? - - private let dismissed: () -> Void - private let interactiveExtensionUpdated: (ContainedViewLayoutTransition) -> Void - - private(set) var interactiveExtension: CGFloat = 0.0 - private var freezeInteractiveExtension = false - - private(set) var isUIHidden: Bool = false - - var unembedOnLeave: Bool = true - - init(context: AccountContext, videoNode: OverlayUniversalVideoNode, disableInternalAnimationIn: Bool, interactiveExtensionUpdated: @escaping (ContainedViewLayoutTransition) -> Void, dismissed: @escaping () -> Void, isUIHiddenUpdated: @escaping () -> Void, unembedWhenPortrait: @escaping (OverlayMediaItemNode) -> Bool) { - self.dismissed = dismissed - self.interactiveExtensionUpdated = interactiveExtensionUpdated - self.isUIHiddenUpdated = isUIHiddenUpdated - self.unembedWhenPortrait = unembedWhenPortrait - self.disableInternalAnimationIn = disableInternalAnimationIn - - self.context = context - - self.backgroundNode = ASDisplayNode() - self.backgroundNode.backgroundColor = .black - - self.statusBarBackgroundNode = ASDisplayNode() - self.statusBarBackgroundNode.backgroundColor = .black - - self.videoNode = videoNode - - super.init() - - self.clipsToBounds = true - - self.addSubnode(self.backgroundNode) - self.addSubnode(self.statusBarBackgroundNode) - - self.view.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:)))) - - self.videoNode.controlsAreShowingUpdated = { [weak self] value in - guard let strongSelf = self else { - return - } - strongSelf.isUIHidden = !value - strongSelf.isUIHiddenUpdated() - } - } - - @objc private func panGesture(_ recognizer: UIPanGestureRecognizer) { - switch recognizer.state { - case .began: - break - case .changed: - let translation = recognizer.translation(in: self.view) - - func rubberBandingOffset(offset: CGFloat, bandingStart: CGFloat) -> CGFloat { - let bandedOffset = offset - bandingStart - let range: CGFloat = 600.0 - let coefficient: CGFloat = 0.4 - return bandingStart + (1.0 - (1.0 / ((bandedOffset * coefficient / range) + 1.0))) * range - } - - let offset = rubberBandingOffset(offset: translation.y, bandingStart: 0.0) - - if translation.y > 80.0 { - self.freezeInteractiveExtension = true - self.expandIntoPiP() - } else { - self.interactiveExtension = max(0.0, offset) - self.interactiveExtensionUpdated(.immediate) - } - case .cancelled, .ended: - if !freezeInteractiveExtension { - self.interactiveExtension = 0.0 - self.interactiveExtensionUpdated(.animated(duration: 0.3, curve: .spring)) - } - default: - break - } - } - - func calculateHeight(width: CGFloat) -> CGFloat { - return self.videoNode.content.dimensions.aspectFilled(CGSize(width: width, height: 16.0)).height - } - - func updateLayout(size: CGSize, actualHeight: CGFloat, topInset: CGFloat, interactiveExtension: CGFloat, transition: ContainedViewLayoutTransition, transitionSurface: ASDisplayNode?, navigationBar: NavigationBar?) { - let isFirstTime = self.validLayout == nil - - self.validLayout = (size, actualHeight, topInset, interactiveExtension) - let videoSize = CGSize(width: size.width, height: actualHeight) - - let videoFrame = CGRect(origin: CGPoint(x: 0.0, y: topInset + interactiveExtension + floor((size.height - actualHeight) / 2.0)), size: CGSize(width: videoSize.width, height: videoSize.height - topInset - interactiveExtension)) - - if isFirstTime, let transitionSurface = transitionSurface { - let sourceFrame = self.videoNode.view.convert(self.videoNode.bounds, to: transitionSurface.view) - let targetFrame = self.view.convert(videoFrame, to: transitionSurface.view) - - var navigationBarCopy: UIView? - var navigationBarContainer: UIView? - var nodeTransition = transition - if self.disableInternalAnimationIn { - nodeTransition = .immediate - } else { - self.context.sharedContext.mediaManager.setOverlayVideoNode(nil) - transitionSurface.addSubnode(self.videoNode) - - navigationBarCopy = navigationBar?.view.snapshotView(afterScreenUpdates: true) - let navigationBarContainerValue = UIView() - navigationBarContainer = navigationBarContainerValue - navigationBarContainerValue.frame = targetFrame - navigationBarContainerValue.clipsToBounds = true - transitionSurface.view.addSubview(navigationBarContainerValue) - } - - if !self.disableInternalAnimationIn { - navigationBarContainer?.layer.animateFrame(from: sourceFrame, to: targetFrame, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring) - } - - if !self.disableInternalAnimationIn { - if let navigationBar = navigationBar, let navigationBarCopy = navigationBarCopy { - let navigationFrame = navigationBar.view.convert(navigationBar.bounds, to: transitionSurface.view) - let navigationSourceFrame = navigationFrame.offsetBy(dx: -sourceFrame.minX, dy: -sourceFrame.minY) - let navigationTargetFrame = navigationFrame.offsetBy(dx: -targetFrame.minX, dy: -targetFrame.minY) - navigationBarCopy.frame = navigationTargetFrame - navigationBarContainer?.addSubview(navigationBarCopy) - - navigationBarCopy.layer.animateFrame(from: navigationSourceFrame, to: navigationTargetFrame, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring) - navigationBarCopy.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring) - } - } - - self.videoNode.updateRoundCorners(false, transition: nodeTransition) - if !self.disableInternalAnimationIn { - self.videoNode.showControls() - } - - self.videoNode.updateLayout(targetFrame.size, transition: nodeTransition) - self.videoNode.frame = targetFrame - if self.disableInternalAnimationIn { - self.insertSubnode(self.videoNode, belowSubnode: self.statusBarBackgroundNode) - } else { - self.videoNode.layer.animateFrame(from: sourceFrame, to: targetFrame, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, completion: { [weak self] _ in - guard let strongSelf = self else { - return - } - navigationBarContainer?.removeFromSuperview() - strongSelf.insertSubnode(strongSelf.videoNode, belowSubnode: strongSelf.statusBarBackgroundNode) - if let (size, actualHeight, topInset, interactiveExtension) = strongSelf.validLayout { - strongSelf.updateLayout(size: size, actualHeight: actualHeight, topInset: topInset, interactiveExtension: interactiveExtension, transition: .immediate, transitionSurface: nil, navigationBar: nil) - } - }) - self.backgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) - } - - self.videoNode.customClose = { [weak self] in - guard let strongSelf = self else { - return - } - strongSelf.videoNode.customClose = nil - strongSelf.dismissed() - } - } - transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(), size: size)) - transition.updateFrame(node: self.statusBarBackgroundNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: topInset))) - - if self.videoNode.supernode == self { - self.videoNode.layer.transform = CATransform3DIdentity - transition.updateFrame(node: self.videoNode, frame: videoFrame) - } - } - - func expand(intoLandscape: Bool) { - if intoLandscape { - let unembedWhenPortrait = self.unembedWhenPortrait - self.videoNode.customUnembedWhenPortrait = { videoNode in - unembedWhenPortrait(videoNode) - } - } - self.videoNode.expand() - } - - func expandIntoPiP() { - let transition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .spring) - - self.videoNode.customExpand = nil - self.videoNode.customClose = nil - - let previousFrame = self.videoNode.frame - self.context.sharedContext.mediaManager.setOverlayVideoNode(self.videoNode) - self.videoNode.updateRoundCorners(true, transition: transition) - - if let targetSuperview = self.videoNode.view.superview { - let sourceFrame = self.view.convert(previousFrame, to: targetSuperview) - let targetFrame = self.videoNode.frame - self.videoNode.frame = sourceFrame - self.videoNode.updateLayout(sourceFrame.size, transition: .immediate) - - transition.updateFrame(node: self.videoNode, frame: targetFrame) - self.videoNode.updateLayout(targetFrame.size, transition: transition) - } - - self.dismissed() - } -} - -enum ChatEmbeddedTitlePeekContent: Equatable { - case none - case peek -} - class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { let context: AccountContext let chatLocation: ChatLocation @@ -283,8 +67,6 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { private weak var controller: ChatControllerImpl? let navigationBar: NavigationBar? - private let navigationBarBackroundNode: ASDisplayNode - private let navigationBarSeparatorNode: ASDisplayNode private var backgroundEffectNode: ASDisplayNode? private var containerBackgroundNode: ASImageNode? @@ -299,7 +81,6 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { } let backgroundNode: WallpaperBackgroundNode - let backgroundImageDisposable = MetaDisposable() let historyNode: ChatHistoryListNode var blurredHistoryNode: ASImageNode? let reactionContainerNode: ReactionSelectionParentNode @@ -316,7 +97,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { private var searchNavigationNode: ChatSearchNavigationContentNode? - private let inputPanelBackgroundNode: ASDisplayNode + private let inputPanelBackgroundNode: NavigationBackgroundNode private let inputPanelBackgroundSeparatorNode: ASDisplayNode private var plainInputSeparatorAlpha: CGFloat? private var usePlainInputSeparator: Bool @@ -329,7 +110,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { private var inputPanelNode: ChatInputPanelNode? private weak var currentDismissedInputPanelNode: ASDisplayNode? private var secondaryInputPanelNode: ChatInputPanelNode? - private var accessoryPanelNode: AccessoryPanelNode? + private(set) var accessoryPanelNode: AccessoryPanelNode? private var inputContextPanelNode: ChatInputContextPanelNode? private let inputContextPanelContainer: ChatControllerTitlePanelNodeContainer private var overlayContextPanelNode: ChatInputContextPanelNode? @@ -337,12 +118,13 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { private var inputNode: ChatInputNode? private var disappearingNode: ChatInputNode? - private var textInputPanelNode: ChatTextInputPanelNode? + private(set) var textInputPanelNode: ChatTextInputPanelNode? private var inputMediaNode: ChatMediaInputNode? let navigateButtons: ChatHistoryNavigationButtons private var ignoreUpdateHeight = false + private var overrideUpdateTextInputHeightTransition: ContainedViewLayoutTransition? private var animateInAsOverlayCompletion: (() -> Void)? private var dismissAsOverlayCompletion: (() -> Void)? @@ -377,14 +159,14 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { } } - var requestUpdateChatInterfaceState: (Bool, Bool, (ChatInterfaceState) -> ChatInterfaceState) -> Void = { _, _, _ in } + var requestUpdateChatInterfaceState: (ContainedViewLayoutTransition, Bool, (ChatInterfaceState) -> ChatInterfaceState) -> Void = { _, _, _ in } var requestUpdateInterfaceState: (ContainedViewLayoutTransition, Bool, (ChatPresentationInterfaceState) -> ChatPresentationInterfaceState) -> Void = { _, _, _ in } var sendMessages: ([EnqueueMessage], Bool?, Int32?, Bool) -> Void = { _, _, _, _ in } var displayAttachmentMenu: () -> Void = { } var paste: (ChatTextInputPanelPasteData) -> Void = { _ in } var updateTypingActivity: (Bool) -> Void = { _ in } var dismissUrlPreview: () -> Void = { } - var setupSendActionOnViewUpdate: (@escaping () -> Void) -> Void = { _ in } + var setupSendActionOnViewUpdate: (@escaping () -> Void, Int64?) -> Void = { _, _ in } var requestLayout: (ContainedViewLayoutTransition) -> Void = { _ in } var dismissAsOverlay: () -> Void = { } @@ -393,6 +175,10 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { private var expandedInputDimNode: ASDisplayNode? private var dropDimNode: ASDisplayNode? + + let messageTransitionNode: ChatMessageTransitionNode + + private let presentationContextMarker = ASDisplayNode() private var containerLayoutAndNavigationBarHeight: (ContainerViewLayout, CGFloat)? @@ -443,16 +229,8 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { private var displayVideoUnmuteTipDisposable: Disposable? private var onLayoutCompletions: [(ContainedViewLayoutTransition) -> Void] = [] - - private var embeddedTitlePeekContent: ChatEmbeddedTitlePeekContent = .none - private var embeddedTitleContentNode: ChatEmbeddedTitleContentNode? - private var dismissedEmbeddedTitleContentNode: ChatEmbeddedTitleContentNode? - var hasEmbeddedTitleContent: Bool { - return self.embeddedTitleContentNode != nil - } - private var didProcessExperimentalEmbedUrl: String? - - init(context: AccountContext, chatLocation: ChatLocation, chatLocationContextHolder: Atomic, subject: ChatControllerSubject?, controllerInteraction: ChatControllerInteraction, chatPresentationInterfaceState: ChatPresentationInterfaceState, automaticMediaDownloadSettings: MediaAutoDownloadSettings, navigationBar: NavigationBar?, controller: ChatControllerImpl?) { + + init(context: AccountContext, chatLocation: ChatLocation, chatLocationContextHolder: Atomic, subject: ChatControllerSubject?, controllerInteraction: ChatControllerInteraction, chatPresentationInterfaceState: ChatPresentationInterfaceState, automaticMediaDownloadSettings: MediaAutoDownloadSettings, navigationBar: NavigationBar?, backgroundNode: WallpaperBackgroundNode, controller: ChatControllerImpl?) { self.context = context self.chatLocation = chatLocation self.controllerInteraction = controllerInteraction @@ -461,33 +239,42 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { self.navigationBar = navigationBar self.controller = controller - self.backgroundNode = WallpaperBackgroundNode() - self.backgroundNode.displaysAsynchronously = false + self.backgroundNode = backgroundNode self.titleAccessoryPanelContainer = ChatControllerTitlePanelNodeContainer() self.titleAccessoryPanelContainer.clipsToBounds = true self.inputContextPanelContainer = ChatControllerTitlePanelNodeContainer() - - self.historyNode = ChatHistoryListNode(context: context, chatLocation: chatLocation, chatLocationContextHolder: chatLocationContextHolder, tagMask: nil, subject: subject, controllerInteraction: controllerInteraction, selectedMessages: self.selectedMessagesPromise.get()) + + var getMessageTransitionNode: (() -> ChatMessageTransitionNode?)? + self.historyNode = ChatHistoryListNode(context: context, chatLocation: chatLocation, chatLocationContextHolder: chatLocationContextHolder, tagMask: nil, subject: subject, controllerInteraction: controllerInteraction, selectedMessages: self.selectedMessagesPromise.get(), messageTransitionNode: { + return getMessageTransitionNode?() + }) self.historyNode.rotated = true self.historyNodeContainer = ASDisplayNode() self.historyNodeContainer.addSubnode(self.historyNode) + + var getContentAreaInScreenSpaceImpl: (() -> CGRect)? + var onTransitionEventImpl: ((ContainedViewLayoutTransition) -> Void)? + self.messageTransitionNode = ChatMessageTransitionNode(listNode: self.historyNode, getContentAreaInScreenSpace: { + return getContentAreaInScreenSpaceImpl?() ?? CGRect() + }, onTransitionEvent: { transition in + onTransitionEventImpl?(transition) + }) self.reactionContainerNode = ReactionSelectionParentNode(account: context.account, theme: chatPresentationInterfaceState.theme) self.loadingNode = ChatLoadingNode(theme: self.chatPresentationInterfaceState.theme, chatWallpaper: self.chatPresentationInterfaceState.chatWallpaper, bubbleCorners: self.chatPresentationInterfaceState.bubbleCorners) - - self.inputPanelBackgroundNode = ASDisplayNode() + 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.inputPanelBackgroundNode = NavigationBackgroundNode(color: self.chatPresentationInterfaceState.theme.chat.inputPanel.panelBackgroundColorNoWallpaper) self.usePlainInputSeparator = true } else { - self.inputPanelBackgroundNode.backgroundColor = self.chatPresentationInterfaceState.theme.chat.inputPanel.panelBackgroundColor + self.inputPanelBackgroundNode = NavigationBackgroundNode(color: self.chatPresentationInterfaceState.theme.chat.inputPanel.panelBackgroundColor) self.usePlainInputSeparator = false self.plainInputSeparatorAlpha = nil } - self.inputPanelBackgroundNode.isLayerBacked = true + self.inputPanelBackgroundNode.isUserInteractionEnabled = false self.inputPanelBackgroundSeparatorNode = ASDisplayNode() self.inputPanelBackgroundSeparatorNode.backgroundColor = self.chatPresentationInterfaceState.theme.chat.inputPanel.panelSeparatorColor @@ -496,19 +283,35 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { self.navigateButtons = ChatHistoryNavigationButtons(theme: self.chatPresentationInterfaceState.theme, dateTimeFormat: self.chatPresentationInterfaceState.dateTimeFormat) self.navigateButtons.accessibilityElementsHidden = true - self.navigationBarBackroundNode = ASDisplayNode() - self.navigationBarBackroundNode.backgroundColor = chatPresentationInterfaceState.theme.rootController.navigationBar.backgroundColor - - self.navigationBarSeparatorNode = ASDisplayNode() - self.navigationBarSeparatorNode.backgroundColor = chatPresentationInterfaceState.theme.rootController.navigationBar.separatorColor - super.init() + + getContentAreaInScreenSpaceImpl = { [weak self] in + guard let strongSelf = self else { + return CGRect() + } + + return strongSelf.view.convert(strongSelf.frameForVisibleArea(), to: nil) + } + + onTransitionEventImpl = { [weak self] transition in + guard let strongSelf = self else { + return + } + if (strongSelf.context.sharedContext.currentPresentationData.with({ $0 })).reduceMotion { + return + } + strongSelf.backgroundNode.animateEvent(transition: transition) + } + + getMessageTransitionNode = { [weak self] in + return self?.messageTransitionNode + } self.controller?.presentationContext.topLevelSubview = { [weak self] in guard let strongSelf = self else { return nil } - return strongSelf.titleAccessoryPanelContainer.view + return strongSelf.presentationContextMarker.view } self.setViewBlock({ @@ -554,12 +357,6 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { } } - self.backgroundImageDisposable.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, _) = image { - strongSelf.backgroundNode.image = image - } - })) - self.interactiveEmojisDisposable = (self.context.account.postbox.preferencesView(keys: [PreferencesKeys.appConfiguration]) |> map { preferencesView -> InteractiveEmojiConfiguration in let appConfiguration: AppConfiguration = preferencesView.values[PreferencesKeys.appConfiguration] as? AppConfiguration ?? .defaultValue @@ -570,33 +367,52 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { strongSelf.interactiveEmojis = emojis } }) - - 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.backgroundNode.update(wallpaper: chatPresentationInterfaceState.chatWallpaper) + self.backgroundNode.updateBubbleTheme(bubbleTheme: chatPresentationInterfaceState.theme, bubbleCorners: chatPresentationInterfaceState.bubbleCorners) + + var backgroundColors: [UInt32] = [] + switch chatPresentationInterfaceState.chatWallpaper { + case let .file(_, _, _, _, isPattern, _, _, _, settings): + if isPattern { + backgroundColors = settings.colors + } + case let .gradient(_, colors, _): + backgroundColors = colors + case let .color(color): + backgroundColors = [color] + default: + break + } + if !backgroundColors.isEmpty { + let averageColor = UIColor.average(of: backgroundColors.map(UIColor.init(rgb:))) + if averageColor.hsb.b >= 0.3 { + self.historyNode.verticalScrollIndicatorColor = UIColor(white: 0.0, alpha: 0.3) + } else { + self.historyNode.verticalScrollIndicatorColor = UIColor(white: 1.0, alpha: 0.3) + } + } else { + self.historyNode.verticalScrollIndicatorColor = UIColor(white: 0.5, alpha: 0.8) + } self.historyNode.enableExtractedBackgrounds = true self.addSubnode(self.backgroundNode) self.addSubnode(self.historyNodeContainer) self.addSubnode(self.navigateButtons) - self.addSubnode(self.titleAccessoryPanelContainer) - + self.addSubnode(self.inputPanelBackgroundNode) self.addSubnode(self.inputPanelBackgroundSeparatorNode) - + self.addSubnode(self.inputContextPanelContainer) - - self.addSubnode(self.navigationBarBackroundNode) - self.addSubnode(self.navigationBarSeparatorNode) - if !self.context.sharedContext.immediateExperimentalUISettings.playerEmbedding { - self.navigationBarBackroundNode.isHidden = true - self.navigationBarSeparatorNode.isHidden = true + + if let navigationBar = self.navigationBar { + self.addSubnode(navigationBar) } + + self.addSubnode(self.messageTransitionNode) + self.addSubnode(self.presentationContextMarker) + + self.navigationBar?.additionalContentNode.addSubnode(self.titleAccessoryPanelContainer) self.historyNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) @@ -607,7 +423,15 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { self.textInputPanelNode?.updateHeight = { [weak self] animated in if let strongSelf = self, let _ = strongSelf.inputPanelNode as? ChatTextInputPanelNode, !strongSelf.ignoreUpdateHeight { if strongSelf.scheduledLayoutTransitionRequest == nil { - strongSelf.scheduleLayoutTransitionRequest(animated ? .animated(duration: 0.1, curve: .easeInOut) : .immediate) + let transition: ContainedViewLayoutTransition + if !animated { + transition = .immediate + } else if let overrideUpdateTextInputHeightTransition = strongSelf.overrideUpdateTextInputHeightTransition { + transition = overrideUpdateTextInputHeightTransition + } else { + transition = .animated(duration: 0.1, curve: .easeInOut) + } + strongSelf.scheduleLayoutTransitionRequest(transition) } } } @@ -634,7 +458,6 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { } deinit { - self.backgroundImageDisposable.dispose() self.interactiveEmojisDisposable?.dispose() self.openStickersDisposable?.dispose() self.displayVideoUnmuteTipDisposable?.dispose() @@ -721,7 +544,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { private func updateIsEmpty(_ emptyType: ChatHistoryNodeLoadState.EmptyType?, animated: Bool) { self.emptyType = emptyType if let emptyType = emptyType, self.emptyNode == nil { - let emptyNode = ChatEmptyNode(account: self.context.account, interaction: self.interfaceInteraction) + let emptyNode = ChatEmptyNode(context: self.context, interaction: self.interfaceInteraction) if let (size, insets) = self.validEmptyNodeLayout { emptyNode.updateLayout(interfaceState: self.chatPresentationInterfaceState, emptyType: emptyType, size: size, insets: insets, transition: .immediate) } @@ -743,37 +566,27 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { } } - var greetingStickerNode: (ASDisplayNode, ASDisplayNode, ASDisplayNode, (@escaping () -> Void) -> Void)? { - if let greetingStickerNode = self.emptyNode?.greetingStickerNode { - let historyNode = self.historyNode - historyNode.alpha = 0.0 - return (greetingStickerNode, self, self.historyNode, { completion in - historyNode.alpha = 1.0 - historyNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, completion: { _ in - completion() - }) - }) - } else { - return nil - } - } - private var isInFocus: Bool = false - func inFocusUpdated(isInFocus: Bool) { self.isInFocus = isInFocus self.inputMediaNode?.simulateUpdateLayout(isVisible: isInFocus) } - func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition protoTransition: ContainedViewLayoutTransition, listViewTransaction: - (ListViewUpdateSizeAndInsets, CGFloat, Bool, @escaping () -> Void) -> Void) { + func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition protoTransition: ContainedViewLayoutTransition, listViewTransaction: (ListViewUpdateSizeAndInsets, CGFloat, Bool, @escaping () -> Void) -> Void, updateExtraNavigationBarBackgroundHeight: (CGFloat) -> Void) { let transition: ContainedViewLayoutTransition if let _ = self.scheduledAnimateInAsOverlayFromNode { transition = .immediate } else { transition = protoTransition } + + var previousListBottomInset: CGFloat? + if !self.historyNode.frame.isEmpty { + previousListBottomInset = self.historyNode.insets.top + } + + self.messageTransitionNode.frame = CGRect(origin: CGPoint(), size: layout.size) self.scheduledLayoutTransitionRequest = nil if case .overlay = self.chatPresentationInterfaceState.mode { @@ -907,6 +720,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { var dismissedTitleAccessoryPanelNode: ChatTitleAccessoryPanelNode? var immediatelyLayoutTitleAccessoryPanelNodeAndAnimateAppearance = false var titleAccessoryPanelHeight: CGFloat? + var titleAccessoryPanelBackgroundHeight: CGFloat? if let titleAccessoryPanelNode = titlePanelForChatPresentationInterfaceState(self.chatPresentationInterfaceState, context: self.context, currentPanel: self.titleAccessoryPanelNode, interfaceInteraction: self.interfaceInteraction) { if self.titleAccessoryPanelNode != titleAccessoryPanelNode { dismissedTitleAccessoryPanelNode = self.titleAccessoryPanelNode @@ -915,7 +729,9 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { self.titleAccessoryPanelContainer.addSubnode(titleAccessoryPanelNode) } - titleAccessoryPanelHeight = titleAccessoryPanelNode.updateLayout(width: layout.size.width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, transition: immediatelyLayoutTitleAccessoryPanelNodeAndAnimateAppearance ? .immediate : transition, interfaceState: self.chatPresentationInterfaceState) + let layoutResult = titleAccessoryPanelNode.updateLayout(width: layout.size.width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, transition: immediatelyLayoutTitleAccessoryPanelNodeAndAnimateAppearance ? .immediate : transition, interfaceState: self.chatPresentationInterfaceState) + titleAccessoryPanelHeight = layoutResult.insetHeight + titleAccessoryPanelBackgroundHeight = layoutResult.backgroundHeight } else if let titleAccessoryPanelNode = self.titleAccessoryPanelNode { dismissedTitleAccessoryPanelNode = titleAccessoryPanelNode self.titleAccessoryPanelNode = nil @@ -951,7 +767,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { inputPanelNodeBaseHeight += secondaryInputPanelNode.minimalHeight(interfaceState: self.chatPresentationInterfaceState, metrics: layout.metrics) } - let maximumInputNodeHeight = layout.size.height - max(navigationBarHeight, layout.safeInsets.top) - inputPanelNodeBaseHeight + let maximumInputNodeHeight = layout.size.height - max(navigationBarHeight + (titleAccessoryPanelBackgroundHeight ?? 0.0), layout.safeInsets.top) - inputPanelNodeBaseHeight var dismissedInputNode: ChatInputNode? var immediatelyLayoutInputNodeAndAnimateAppearance = false @@ -1007,135 +823,13 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { let statusBarHeight = layout.insets(options: [.statusBar]).top - func extractExperimentalPlaylistUrl(_ text: String) -> String? { - let prefix = "stream: " - if text.hasPrefix(prefix) { - if let url = URL(string: String(text[text.index(text.startIndex, offsetBy: prefix.count)...])), url.absoluteString.hasSuffix(".m3u8") { - return url.absoluteString - } else { - return nil - } - } else { - return nil - } - } - - if let pinnedMessage = self.chatPresentationInterfaceState.pinnedMessage, self.context.sharedContext.immediateExperimentalUISettings.playerEmbedding, self.context.sharedContext.immediateExperimentalUISettings.playlistPlayback, self.embeddedTitleContentNode == nil, let url = extractExperimentalPlaylistUrl(pinnedMessage.message.text), self.didProcessExperimentalEmbedUrl != url { - self.didProcessExperimentalEmbedUrl = url - let context = self.context - let baseNavigationController = self.controller?.navigationController as? NavigationController - let mediaManager = self.context.sharedContext.mediaManager - var expandImpl: (() -> Void)? - let content = PlatformVideoContent(id: .instantPage(MediaId(namespace: 0, id: 0), MediaId(namespace: 0, id: 0)), content: .url(url), streamVideo: true, loopVideo: false) - let overlayNode = OverlayUniversalVideoNode(postbox: self.context.account.postbox, audioSession: self.context.sharedContext.mediaManager.audioSession, manager: self.context.sharedContext.mediaManager.universalVideoManager, content: content, expand: { - expandImpl?() - }, close: { [weak mediaManager] in - mediaManager?.setOverlayVideoNode(nil) - }) - self.embeddedTitleContentNode = ChatEmbeddedTitleContentNode(context: self.context, videoNode: overlayNode, disableInternalAnimationIn: true, interactiveExtensionUpdated: { [weak self] transition in - guard let strongSelf = self else { - return - } - strongSelf.requestLayout(transition) - }, dismissed: { [weak self] in - guard let strongSelf = self else { - return - } - if let embeddedTitleContentNode = strongSelf.embeddedTitleContentNode { - strongSelf.embeddedTitleContentNode = nil - strongSelf.dismissedEmbeddedTitleContentNode = embeddedTitleContentNode - strongSelf.requestLayout(.animated(duration: 0.25, curve: .spring)) - strongSelf.updateHasEmbeddedTitleContent?() - } - }, isUIHiddenUpdated: { [weak self] in - self?.updateHasEmbeddedTitleContent?() - }, unembedWhenPortrait: { [weak self] itemNode in - guard let strongSelf = self, let itemNode = itemNode as? OverlayUniversalVideoNode else { - return false - } - strongSelf.unembedWhenPortrait(contentNode: itemNode) - return true - }) - self.embeddedTitleContentNode?.unembedOnLeave = false - self.updateHasEmbeddedTitleContent?() - overlayNode.controlPlay() - } - - if self.chatPresentationInterfaceState.pinnedMessage == nil { - self.didProcessExperimentalEmbedUrl = nil - } - - if let embeddedTitleContentNode = self.embeddedTitleContentNode, embeddedTitleContentNode.supernode != nil { - if layout.size.width > layout.size.height { - self.embeddedTitleContentNode = nil - self.dismissedEmbeddedTitleContentNode = embeddedTitleContentNode - embeddedTitleContentNode.expand(intoLandscape: true) - self.updateHasEmbeddedTitleContent?() - } - } - - if let embeddedTitleContentNode = self.embeddedTitleContentNode { - let defaultEmbeddedSize = CGSize(width: layout.size.width, height: min(400.0, embeddedTitleContentNode.calculateHeight(width: layout.size.width)) + statusBarHeight + embeddedTitleContentNode.interactiveExtension) - - let embeddedSize: CGSize - if let inputHeight = layout.inputHeight, inputHeight > 100.0 { - embeddedSize = CGSize(width: defaultEmbeddedSize.width, height: floor(defaultEmbeddedSize.height * 0.6)) - } else { - embeddedSize = defaultEmbeddedSize - } - - if embeddedTitleContentNode.supernode == nil { - self.insertSubnode(embeddedTitleContentNode, aboveSubnode: self.navigationBarBackroundNode) - - var previousTopInset = insets.top - if case .overlay = self.chatPresentationInterfaceState.mode { - previousTopInset = 44.0 - } else { - previousTopInset += navigationBarHeight - } - - if case .peek = self.embeddedTitlePeekContent { - previousTopInset += 32.0 - } - - embeddedTitleContentNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: layout.size.width, height: previousTopInset)) - transition.updateFrame(node: embeddedTitleContentNode, frame: CGRect(origin: CGPoint(), size: embeddedSize)) - embeddedTitleContentNode.updateLayout(size: embeddedSize, actualHeight: defaultEmbeddedSize.height, topInset: statusBarHeight, interactiveExtension: embeddedTitleContentNode.interactiveExtension, transition: .immediate, transitionSurface: self, navigationBar: self.navigationBar) - } else { - transition.updateFrame(node: embeddedTitleContentNode, frame: CGRect(origin: CGPoint(), size: embeddedSize)) - embeddedTitleContentNode.updateLayout(size: embeddedSize, actualHeight: defaultEmbeddedSize.height, topInset: statusBarHeight, interactiveExtension: embeddedTitleContentNode.interactiveExtension, transition: transition, transitionSurface: self, navigationBar: self.navigationBar) - } - - insets.top += embeddedSize.height + + if case .overlay = self.chatPresentationInterfaceState.mode { + insets.top = 44.0 } else { - if case .overlay = self.chatPresentationInterfaceState.mode { - insets.top = 44.0 - } else { - insets.top += navigationBarHeight - } - - if case .peek = self.embeddedTitlePeekContent { - insets.top += 32.0 - } + insets.top += navigationBarHeight } - if let dismissedEmbeddedTitleContentNode = self.dismissedEmbeddedTitleContentNode { - self.dismissedEmbeddedTitleContentNode = nil - if transition.isAnimated { - dismissedEmbeddedTitleContentNode.alpha = 0.0 - dismissedEmbeddedTitleContentNode.layer.allowsGroupOpacity = true - dismissedEmbeddedTitleContentNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, completion: { [weak dismissedEmbeddedTitleContentNode] _ in - dismissedEmbeddedTitleContentNode?.removeFromSupernode() - }) - transition.updateFrame(node: dismissedEmbeddedTitleContentNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: layout.size.width, height: insets.top))) - } else { - dismissedEmbeddedTitleContentNode.removeFromSupernode() - } - } - - transition.updateFrame(node: self.navigationBarBackroundNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: layout.size.width, height: insets.top))) - transition.updateFrame(node: self.navigationBarSeparatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: insets.top), size: CGSize(width: layout.size.width, height: UIScreenPixel))) - var wrappingInsets = UIEdgeInsets() if case .overlay = self.chatPresentationInterfaceState.mode { let containerWidth = horizontalContainerFillingSizeForLayout(layout: layout, sideInset: 8.0 + layout.safeInsets.left) @@ -1150,7 +844,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { var dismissedInputPanelNode: ASDisplayNode? var dismissedSecondaryInputPanelNode: ASDisplayNode? - var dismissedAccessoryPanelNode: ASDisplayNode? + var dismissedAccessoryPanelNode: AccessoryPanelNode? var dismissedInputContextPanelNode: ChatInputContextPanelNode? var dismissedOverlayContextPanelNode: ChatInputContextPanelNode? @@ -1201,7 +895,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { self.insertSubnode(inputPanelNode, aboveSubnode: self.inputPanelBackgroundNode) } } else { - let inputPanelHeight = inputPanelNode.updateLayout(width: layout.size.width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, additionalSideInsets: layout.additionalInsets, maxHeight: layout.size.height - insets.top - insets.bottom, isSecondary: false, transition: transition, interfaceState: self.chatPresentationInterfaceState, metrics: layout.metrics) + let inputPanelHeight = inputPanelNode.updateLayout(width: layout.size.width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, additionalSideInsets: layout.additionalInsets, maxHeight: layout.size.height - insets.top - insets.bottom - 120.0, isSecondary: false, transition: transition, interfaceState: self.chatPresentationInterfaceState, metrics: layout.metrics) inputPanelSize = CGSize(width: layout.size.width, height: inputPanelHeight) } } else { @@ -1241,6 +935,8 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { titleAccessoryPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: layout.size.width, height: panelHeight)) insets.top += panelHeight } + + updateExtraNavigationBarBackgroundHeight(titleAccessoryPanelBackgroundHeight ?? 0.0) var importStatusPanelFrame: CGRect? if let _ = self.chatImportStatusPanel, let panelHeight = importStatusPanelHeight { @@ -1256,6 +952,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)) @@ -1286,15 +983,15 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { if let inputPanelNode = self.inputPanelNode { self.insertSubnode(accessoryPanelNode, belowSubnode: inputPanelNode) } else { - self.insertSubnode(accessoryPanelNode, aboveSubnode: self.navigateButtons) + self.insertSubnode(accessoryPanelNode, aboveSubnode: self.inputPanelBackgroundNode) } accessoryPanelNode.dismiss = { [weak self, weak accessoryPanelNode] in if let strongSelf = self, let accessoryPanelNode = accessoryPanelNode, strongSelf.accessoryPanelNode === accessoryPanelNode { if let _ = accessoryPanelNode as? ReplyAccessoryPanelNode { - strongSelf.requestUpdateChatInterfaceState(true, false, { $0.withUpdatedReplyMessageId(nil) }) + strongSelf.requestUpdateChatInterfaceState(.animated(duration: 0.4, curve: .spring), false, { $0.withUpdatedReplyMessageId(nil) }) } else if let _ = accessoryPanelNode as? ForwardAccessoryPanelNode { - strongSelf.requestUpdateChatInterfaceState(true, false, { $0.withUpdatedForwardMessageIds(nil) }) + strongSelf.requestUpdateChatInterfaceState(.animated(duration: 0.4, curve: .spring), false, { $0.withUpdatedForwardMessageIds(nil) }) } else if let _ = accessoryPanelNode as? EditAccessoryPanelNode { strongSelf.interfaceInteraction?.setupEditMessage(nil, { _ in }) } else if let _ = accessoryPanelNode as? WebpagePreviewAccessoryPanelNode { @@ -1534,7 +1231,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { } var apparentInputPanelFrame = inputPanelFrame - var apparentSecondaryInputPanelFrame = secondaryInputPanelFrame + let 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 { @@ -1551,6 +1248,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { let previousInputPanelBackgroundFrame = self.inputPanelBackgroundNode.frame transition.updateFrame(node: self.inputPanelBackgroundNode, frame: apparentInputBackgroundFrame) + self.inputPanelBackgroundNode.update(size: CGSize(width: apparentInputBackgroundFrame.size.width, height: apparentInputBackgroundFrame.size.height + 41.0 + 31.0), transition: transition) transition.updateFrame(node: self.inputPanelBackgroundSeparatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: apparentInputBackgroundFrame.origin.y), size: CGSize(width: apparentInputBackgroundFrame.size.width, height: UIScreenPixel))) transition.updateFrame(node: self.navigateButtons, frame: apparentNavigateButtonsFrame) @@ -1743,6 +1441,9 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { if let inputPanelFrame = inputPanelFrame { transitionTargetY = inputPanelFrame.minY } + + dismissedAccessoryPanelNode.originalFrameBeforeDismissed = dismissedAccessoryPanelNode.frame + transition.updateFrame(node: dismissedAccessoryPanelNode, frame: CGRect(origin: CGPoint(x: 0.0, y: transitionTargetY), size: dismissedAccessoryPanelNode.frame.size), completion: { _ in frameCompleted = true completed() @@ -1863,8 +1564,19 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { } self.updatePlainInputSeparator(transition: transition) - - self.derivedLayoutState = ChatControllerNodeDerivedLayoutState(inputContextPanelsFrame: inputContextPanelsFrame, inputContextPanelsOverMainPanelFrame: inputContextPanelsOverMainPanelFrame, inputNodeHeight: inputNodeHeightAndOverflow?.0, upperInputPositionBound: inputNodeHeightAndOverflow?.0 != nil ? self.upperInputPositionBound : nil) + + let listBottomInset = self.historyNode.insets.top + if let previousListBottomInset = previousListBottomInset, listBottomInset != previousListBottomInset { + if abs(listBottomInset - previousListBottomInset) > 80.0 { + if (self.context.sharedContext.currentPresentationData.with({ $0 })).reduceMotion { + return + } + self.backgroundNode.animateEvent(transition: transition) + } + //self.historyNode.didScrollWithOffset?(listBottomInset - previousListBottomInset, transition, nil) + } + + self.derivedLayoutState = ChatControllerNodeDerivedLayoutState(inputContextPanelsFrame: inputContextPanelsFrame, inputContextPanelsOverMainPanelFrame: inputContextPanelsOverMainPanelFrame, inputNodeHeight: inputNodeHeightAndOverflow?.0, inputNodeAdditionalHeight: inputNodeHeightAndOverflow?.1, upperInputPositionBound: inputNodeHeightAndOverflow?.0 != nil ? self.upperInputPositionBound : nil) //self.notifyTransitionCompletionListeners(transition: transition) } @@ -1905,18 +1617,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { let themeUpdated = self.chatPresentationInterfaceState.theme !== chatPresentationInterfaceState.theme if self.chatPresentationInterfaceState.chatWallpaper != chatPresentationInterfaceState.chatWallpaper { - self.backgroundImageDisposable.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 + self.backgroundNode.update(wallpaper: chatPresentationInterfaceState.chatWallpaper) } self.historyNode.verticalScrollIndicatorColor = UIColor(white: 0.5, alpha: 0.8) @@ -1929,18 +1630,17 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { if themeUpdated { 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.inputPanelBackgroundNode.updateColor(color: self.chatPresentationInterfaceState.theme.chat.inputPanel.panelBackgroundColorNoWallpaper, transition: .immediate) self.usePlainInputSeparator = true } else { - self.inputPanelBackgroundNode.backgroundColor = self.chatPresentationInterfaceState.theme.chat.inputPanel.panelBackgroundColor + self.inputPanelBackgroundNode.updateColor(color: self.chatPresentationInterfaceState.theme.chat.inputPanel.panelBackgroundColor, transition: .immediate) self.usePlainInputSeparator = false self.plainInputSeparatorAlpha = nil } self.updatePlainInputSeparator(transition: .immediate) self.inputPanelBackgroundSeparatorNode.backgroundColor = self.chatPresentationInterfaceState.theme.chat.inputPanel.panelSeparatorColor - - self.navigationBarBackroundNode.backgroundColor = chatPresentationInterfaceState.theme.rootController.navigationBar.backgroundColor - self.navigationBarSeparatorNode.backgroundColor = chatPresentationInterfaceState.theme.rootController.navigationBar.separatorColor + + self.backgroundNode.updateBubbleTheme(bubbleTheme: chatPresentationInterfaceState.theme, bubbleCorners: chatPresentationInterfaceState.bubbleCorners) } let keepSendButtonEnabled = chatPresentationInterfaceState.interfaceState.forwardMessageIds != nil || chatPresentationInterfaceState.interfaceState.editMessage != nil @@ -1953,7 +1653,10 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { } if let textInputPanelNode = self.textInputPanelNode, updateInputTextState { + let previous = self.overrideUpdateTextInputHeightTransition + self.overrideUpdateTextInputHeightTransition = transition textInputPanelNode.updateInputTextState(chatPresentationInterfaceState.interfaceState.effectiveInputState, keepSendButtonEnabled: keepSendButtonEnabled, extendedSearchLayout: extendedSearchLayout, accessoryItems: chatPresentationInterfaceState.inputTextPanelState.accessoryItems, animated: transition.isAnimated) + self.overrideUpdateTextInputHeightTransition = previous } else { self.textInputPanelNode?.updateKeepSendButtonEnabled(keepSendButtonEnabled: keepSendButtonEnabled, extendedSearchLayout: extendedSearchLayout, animated: transition.isAnimated) } @@ -2208,7 +1911,10 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { } func frameForVisibleArea() -> CGRect { - let rect = CGRect(origin: CGPoint(x: self.visibleAreaInset.left, y: self.visibleAreaInset.top), size: CGSize(width: self.bounds.size.width - self.visibleAreaInset.left - self.visibleAreaInset.right, height: self.bounds.size.height - self.visibleAreaInset.top - self.visibleAreaInset.bottom)) + var rect = CGRect(origin: CGPoint(x: self.visibleAreaInset.left, y: self.visibleAreaInset.top), size: CGSize(width: self.bounds.size.width - self.visibleAreaInset.left - self.visibleAreaInset.right, height: self.bounds.size.height - self.visibleAreaInset.top - self.visibleAreaInset.bottom)) + if let inputContextPanelNode = self.inputContextPanelNode, let topItemFrame = inputContextPanelNode.topItemFrame { + rect.size.height = topItemFrame.minY + } if let containerNode = self.containerNode { return containerNode.view.convert(rect, to: self.view) } else { @@ -2468,6 +2174,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { if let (layout, navigationHeight) = self.validLayout { self.containerLayoutUpdated(layout, navigationBarHeight: navigationHeight, transition: transition, listViewTransaction: { updateSizeAndInsets, additionalScrollDistance, scrollToTop, completion in self.historyNode.updateLayout(transition: transition, updateSizeAndInsets: updateSizeAndInsets, additionalScrollDistance: additionalScrollDistance, scrollToTop: scrollToTop, completion: completion) + }, updateExtraNavigationBarBackgroundHeight: { _ in }) } } @@ -2569,8 +2276,8 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { func sendCurrentMessage(silentPosting: Bool? = nil, scheduleTime: Int32? = nil, completion: @escaping () -> Void = {}) { if let textInputPanelNode = self.inputPanelNode as? ChatTextInputPanelNode { - if textInputPanelNode.textInputNode?.isFirstResponder() ?? false { - Keyboard.applyAutocorrection() + if let textInputNode = textInputPanelNode.textInputNode, textInputNode.isFirstResponder() { + Keyboard.applyAutocorrection(textView: textInputNode.textView) } var effectivePresentationInterfaceState = self.chatPresentationInterfaceState @@ -2607,7 +2314,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { let trimmedInputText = effectiveInputText.string.trimmingCharacters(in: .whitespacesAndNewlines) let peerId = effectivePresentationInterfaceState.chatLocation.peerId if peerId.namespace != Namespaces.Peer.SecretChat, let interactiveEmojis = self.interactiveEmojis, interactiveEmojis.emojis.contains(trimmedInputText) { - messages.append(.message(text: "", attributes: [], mediaReference: AnyMediaReference.standalone(media: TelegramMediaDice(emoji: trimmedInputText)), replyToMessageId: self.chatPresentationInterfaceState.interfaceState.replyMessageId, localGroupingKey: nil)) + messages.append(.message(text: "", attributes: [], mediaReference: AnyMediaReference.standalone(media: TelegramMediaDice(emoji: trimmedInputText)), replyToMessageId: self.chatPresentationInterfaceState.interfaceState.replyMessageId, localGroupingKey: nil, correlationId: nil)) } else { let inputText = convertMarkdownToAttributes(effectiveInputText) @@ -2624,7 +2331,18 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { } else { webpage = self.chatPresentationInterfaceState.urlPreview?.1 } - messages.append(.message(text: text.string, attributes: attributes, mediaReference: webpage.flatMap(AnyMediaReference.standalone), replyToMessageId: self.chatPresentationInterfaceState.interfaceState.replyMessageId, localGroupingKey: nil)) + + messages.append(.message(text: text.string, attributes: attributes, mediaReference: webpage.flatMap(AnyMediaReference.standalone), replyToMessageId: self.chatPresentationInterfaceState.interfaceState.replyMessageId, localGroupingKey: nil, correlationId: nil)) + + #if DEBUG + if text.string == "sleep" { + messages.removeAll() + + for i in 0 ..< 5 { + messages.append(.message(text: "sleep\(i)", attributes: [], mediaReference: nil, replyToMessageId: nil, localGroupingKey: nil, correlationId: nil)) + } + } + #endif } } @@ -2642,22 +2360,40 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { } if !messages.isEmpty || self.chatPresentationInterfaceState.interfaceState.forwardMessageIds != nil { + if let forwardMessageIds = self.chatPresentationInterfaceState.interfaceState.forwardMessageIds { + for id in forwardMessageIds { + messages.append(.forward(source: id, grouping: .auto, attributes: [], correlationId: nil)) + } + } + + var usedCorrelationId: Int64? + + if !messages.isEmpty, case .message = messages[messages.count - 1] { + let correlationId = Int64.random(in: 0 ..< Int64.max) + messages[messages.count - 1] = messages[messages.count - 1].withUpdatedCorrelationId(correlationId) + + var replyPanel: ReplyAccessoryPanelNode? + if let accessoryPanelNode = self.accessoryPanelNode as? ReplyAccessoryPanelNode { + replyPanel = accessoryPanelNode + } + if self.shouldAnimateMessageTransition, let inputPanelNode = self.inputPanelNode as? ChatTextInputPanelNode, let textInput = inputPanelNode.makeSnapshotForTransition() { + usedCorrelationId = correlationId + let source: ChatMessageTransitionNode.Source = .textInput(textInput: textInput, replyPanel: replyPanel) + self.messageTransitionNode.add(correlationId: correlationId, source: source, initiated: { + }) + } + } + self.setupSendActionOnViewUpdate({ [weak self] in if let strongSelf = self, let textInputPanelNode = strongSelf.inputPanelNode as? ChatTextInputPanelNode { strongSelf.ignoreUpdateHeight = true textInputPanelNode.text = "" - strongSelf.requestUpdateChatInterfaceState(false, true, { $0.withUpdatedReplyMessageId(nil).withUpdatedForwardMessageIds(nil).withUpdatedComposeDisableUrlPreview(nil) }) + strongSelf.requestUpdateChatInterfaceState(.immediate, true, { $0.withUpdatedReplyMessageId(nil).withUpdatedForwardMessageIds(nil).withUpdatedComposeDisableUrlPreview(nil) }) strongSelf.ignoreUpdateHeight = false } - }) + }, usedCorrelationId) completion() - if let forwardMessageIds = self.chatPresentationInterfaceState.interfaceState.forwardMessageIds { - for id in forwardMessageIds { - messages.append(.forward(source: id, grouping: .auto, attributes: [])) - } - } - self.sendMessages(messages, silentPosting, scheduleTime, messages.count > 1) } } @@ -2707,124 +2443,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { self.view.insertSubview(ConfettiView(frame: self.view.bounds), aboveSubview: self.historyNode.view) } - func updateEmbeddedTitlePeekContent(content: NavigationControllerDropContent?) { - if !self.context.sharedContext.immediateExperimentalUISettings.playerEmbedding { - return - } - - guard let (_, navigationHeight) = self.validLayout else { - return - } - var peekContent: ChatEmbeddedTitlePeekContent = .none - if let content = content, let item = content.item as? VideoNavigationControllerDropContentItem, let _ = item.itemNode as? OverlayUniversalVideoNode { - if content.position.y < navigationHeight + 32.0 { - peekContent = .peek - } - } - if self.embeddedTitlePeekContent != peekContent { - self.embeddedTitlePeekContent = peekContent - self.requestLayout(.animated(duration: 0.3, curve: .spring)) - } - } - - var isEmbeddedTitleContentHidden: Bool { - if let embeddedTitleContentNode = self.embeddedTitleContentNode { - return embeddedTitleContentNode.isUIHidden - } else { - return false - } - } - - var updateHasEmbeddedTitleContent: (() -> Void)? - - func acceptEmbeddedTitlePeekContent(content: NavigationControllerDropContent) -> Bool { - if !self.context.sharedContext.immediateExperimentalUISettings.playerEmbedding { - return false - } - - guard let (_, navigationHeight) = self.validLayout else { - return false - } - if content.position.y >= navigationHeight + 32.0 { - return false - } - if let item = content.item as? VideoNavigationControllerDropContentItem, let itemNode = item.itemNode as? OverlayUniversalVideoNode { - let embeddedTitleContentNode = ChatEmbeddedTitleContentNode(context: self.context, videoNode: itemNode, disableInternalAnimationIn: false, interactiveExtensionUpdated: { [weak self] transition in - guard let strongSelf = self else { - return - } - strongSelf.requestLayout(transition) - }, dismissed: { [weak self] in - guard let strongSelf = self else { - return - } - if let embeddedTitleContentNode = strongSelf.embeddedTitleContentNode { - strongSelf.embeddedTitleContentNode = nil - strongSelf.dismissedEmbeddedTitleContentNode = embeddedTitleContentNode - strongSelf.requestLayout(.animated(duration: 0.25, curve: .spring)) - strongSelf.updateHasEmbeddedTitleContent?() - } - }, isUIHiddenUpdated: { [weak self] in - self?.updateHasEmbeddedTitleContent?() - }, unembedWhenPortrait: { [weak self] itemNode in - guard let strongSelf = self, let itemNode = itemNode as? OverlayUniversalVideoNode else { - return false - } - strongSelf.unembedWhenPortrait(contentNode: itemNode) - return true - }) - self.embeddedTitleContentNode = embeddedTitleContentNode - self.embeddedTitlePeekContent = .none - self.updateHasEmbeddedTitleContent?() - DispatchQueue.main.async { - self.requestLayout(.animated(duration: 0.25, curve: .spring)) - } - - return true - } - return false - } - - private func unembedWhenPortrait(contentNode: OverlayUniversalVideoNode) { - let embeddedTitleContentNode = ChatEmbeddedTitleContentNode(context: self.context, videoNode: contentNode, disableInternalAnimationIn: true, interactiveExtensionUpdated: { [weak self] transition in - guard let strongSelf = self else { - return - } - strongSelf.requestLayout(transition) - }, dismissed: { [weak self] in - guard let strongSelf = self else { - return - } - if let embeddedTitleContentNode = strongSelf.embeddedTitleContentNode { - strongSelf.embeddedTitleContentNode = nil - strongSelf.dismissedEmbeddedTitleContentNode = embeddedTitleContentNode - strongSelf.requestLayout(.animated(duration: 0.25, curve: .spring)) - strongSelf.updateHasEmbeddedTitleContent?() - } - }, isUIHiddenUpdated: { [weak self] in - self?.updateHasEmbeddedTitleContent?() - }, unembedWhenPortrait: { [weak self] itemNode in - guard let strongSelf = self, let itemNode = itemNode as? OverlayUniversalVideoNode else { - return false - } - strongSelf.unembedWhenPortrait(contentNode: itemNode) - return true - }) - - self.embeddedTitleContentNode = embeddedTitleContentNode - self.embeddedTitlePeekContent = .none - self.updateHasEmbeddedTitleContent?() - self.requestLayout(.immediate) - } - func willNavigateAway() { - if let embeddedTitleContentNode = self.embeddedTitleContentNode, embeddedTitleContentNode.unembedOnLeave { - self.embeddedTitleContentNode = nil - self.dismissedEmbeddedTitleContentNode = embeddedTitleContentNode - embeddedTitleContentNode.expandIntoPiP() - self.requestLayout(.animated(duration: 0.25, curve: .spring)) - self.updateHasEmbeddedTitleContent?() - } } func updateIsBlurred(_ isBlurred: Bool) { @@ -2861,4 +2480,23 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { } self.historyNode.isHidden = isBlurred } + + var shouldAnimateMessageTransition: Bool { + if (self.context.sharedContext.currentPresentationData.with({ $0 })).reduceMotion { + return false + } + + if self.chatPresentationInterfaceState.showCommands { + return false + } + + switch self.historyNode.visibleContentOffset() { + case let .known(value) where value < 20.0: + return true + case .none: + return true + default: + return false + } + } } diff --git a/submodules/TelegramUI/Sources/ChatEmptyNode.swift b/submodules/TelegramUI/Sources/ChatEmptyNode.swift index 76bf2723ba..e5ea648a42 100644 --- a/submodules/TelegramUI/Sources/ChatEmptyNode.swift +++ b/submodules/TelegramUI/Sources/ChatEmptyNode.swift @@ -10,6 +10,7 @@ import TelegramPresentationData import AppBundle import LocalizedPeerData import TelegramStringFormatting +import AccountContext private protocol ChatEmptyNodeContent { func updateLayout(interfaceState: ChatPresentationInterfaceState, size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize @@ -66,34 +67,28 @@ private final class ChatEmptyNodeRegularChatContent: ASDisplayNode, ChatEmptyNod } } -private final class ChatEmptyNodeGreetingChatContent: ASDisplayNode, ChatEmptyNodeContent, UIGestureRecognizerDelegate { - private let account: Account +protocol ChatEmptyNodeStickerContentNode: ASDisplayNode { + var stickerNode: ChatMediaInputStickerGridItemNode { get } +} + +final class ChatEmptyNodeGreetingChatContent: ASDisplayNode, ChatEmptyNodeStickerContentNode, ChatEmptyNodeContent, UIGestureRecognizerDelegate { + private let context: AccountContext private let interaction: ChatPanelInterfaceInteraction? private let titleNode: ImmediateTextNode private let textNode: ImmediateTextNode private var stickerItem: ChatMediaInputStickerGridItem? - private let stickerNode: ChatMediaInputStickerGridItemNode + let stickerNode: ChatMediaInputStickerGridItemNode private var currentTheme: PresentationTheme? private var currentStrings: PresentationStrings? private var didSetupSticker = false private let disposable = MetaDisposable() - - var greetingStickerNode: ASDisplayNode? { - if let animationNode = self.stickerNode.animationNode, animationNode.supernode === stickerNode { - return animationNode - } else if self.stickerNode.imageNode.supernode === stickerNode { - return self.stickerNode.imageNode - } else { - return nil - } - } - - init(account: Account, interaction: ChatPanelInterfaceInteraction?) { - self.account = account + + init(context: AccountContext, interaction: ChatPanelInterfaceInteraction?) { + self.context = context self.interaction = interaction self.titleNode = ImmediateTextNode() @@ -139,7 +134,7 @@ private final class ChatEmptyNodeGreetingChatContent: ASDisplayNode, ChatEmptyNo guard let stickerItem = self.stickerItem else { return } - let _ = self.interaction?.sendSticker(.standalone(media: stickerItem.stickerItem.file), self.stickerNode, self.stickerNode.bounds) + let _ = self.interaction?.sendSticker(.standalone(media: stickerItem.stickerItem.file), false, self, self.stickerNode.bounds) } func updateLayout(interfaceState: ChatPresentationInterfaceState, size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize { @@ -160,9 +155,9 @@ private final class ChatEmptyNodeGreetingChatContent: ASDisplayNode, ChatEmptyNo } else if !self.didSetupSticker { let sticker: Signal if let preloadedSticker = interfaceState.greetingData?.sticker { - sticker = .single(preloadedSticker) + sticker = preloadedSticker } else { - sticker = randomGreetingSticker(account: self.account) + sticker = self.context.engine.stickers.randomGreetingSticker() |> map { item -> TelegramMediaFile? in return item?.file } @@ -195,7 +190,7 @@ private final class ChatEmptyNodeGreetingChatContent: ASDisplayNode, ChatEmptyNo let index = ItemCollectionItemIndex(index: 0, id: 0) let collectionId = ItemCollectionId(namespace: 0, id: 0) let stickerPackItem = StickerPackItem(index: index, file: sticker, indexKeys: []) - let item = ChatMediaInputStickerGridItem(account: strongSelf.account, collectionId: collectionId, stickerPackInfo: nil, index: ItemCollectionViewEntryIndex(collectionIndex: 0, collectionId: collectionId, itemIndex: index), stickerItem: stickerPackItem, canManagePeerSpecificPack: nil, interfaceInteraction: nil, inputNodeInteraction: inputNodeInteraction, hasAccessory: false, theme: interfaceState.theme, large: true, selected: {}) + let item = ChatMediaInputStickerGridItem(account: strongSelf.context.account, collectionId: collectionId, stickerPackInfo: nil, index: ItemCollectionViewEntryIndex(collectionIndex: 0, collectionId: collectionId, itemIndex: index), stickerItem: stickerPackItem, canManagePeerSpecificPack: nil, interfaceInteraction: nil, inputNodeInteraction: inputNodeInteraction, hasAccessory: false, theme: interfaceState.theme, large: true, selected: {}) strongSelf.stickerItem = item strongSelf.stickerNode.updateLayout(item: item, size: stickerSize, isVisible: true, synchronousLoads: true) strongSelf.stickerNode.isVisibleInGrid = true @@ -233,15 +228,15 @@ private final class ChatEmptyNodeGreetingChatContent: ASDisplayNode, ChatEmptyNo } } -private final class ChatEmptyNodeNearbyChatContent: ASDisplayNode, ChatEmptyNodeContent, UIGestureRecognizerDelegate { - private let account: Account +final class ChatEmptyNodeNearbyChatContent: ASDisplayNode, ChatEmptyNodeStickerContentNode, ChatEmptyNodeContent, UIGestureRecognizerDelegate { + private let context: AccountContext private let interaction: ChatPanelInterfaceInteraction? private let titleNode: ImmediateTextNode private let textNode: ImmediateTextNode private var stickerItem: ChatMediaInputStickerGridItem? - private let stickerNode: ChatMediaInputStickerGridItemNode + let stickerNode: ChatMediaInputStickerGridItemNode private var currentTheme: PresentationTheme? private var currentStrings: PresentationStrings? @@ -249,18 +244,8 @@ private final class ChatEmptyNodeNearbyChatContent: ASDisplayNode, ChatEmptyNode private var didSetupSticker = false private let disposable = MetaDisposable() - var greetingStickerNode: ASDisplayNode? { - if let animationNode = self.stickerNode.animationNode, animationNode.supernode === stickerNode { - return animationNode - } else if self.stickerNode.imageNode.supernode === stickerNode { - return self.stickerNode.imageNode - } else { - return nil - } - } - - init(account: Account, interaction: ChatPanelInterfaceInteraction?) { - self.account = account + init(context: AccountContext, interaction: ChatPanelInterfaceInteraction?) { + self.context = context self.interaction = interaction self.titleNode = ImmediateTextNode() @@ -306,7 +291,7 @@ private final class ChatEmptyNodeNearbyChatContent: ASDisplayNode, ChatEmptyNode guard let stickerItem = self.stickerItem else { return } - let _ = self.interaction?.sendSticker(.standalone(media: stickerItem.stickerItem.file), self.stickerNode, self.stickerNode.bounds) + let _ = self.interaction?.sendSticker(.standalone(media: stickerItem.stickerItem.file), false, self, self.stickerNode.bounds) } func updateLayout(interfaceState: ChatPresentationInterfaceState, size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize { @@ -337,9 +322,9 @@ private final class ChatEmptyNodeNearbyChatContent: ASDisplayNode, ChatEmptyNode } else if !self.didSetupSticker { let sticker: Signal if let preloadedSticker = interfaceState.greetingData?.sticker { - sticker = .single(preloadedSticker) + sticker = preloadedSticker } else { - sticker = randomGreetingSticker(account: self.account) + sticker = self.context.engine.stickers.randomGreetingSticker() |> map { item -> TelegramMediaFile? in return item?.file } @@ -372,7 +357,7 @@ private final class ChatEmptyNodeNearbyChatContent: ASDisplayNode, ChatEmptyNode let index = ItemCollectionItemIndex(index: 0, id: 0) let collectionId = ItemCollectionId(namespace: 0, id: 0) let stickerPackItem = StickerPackItem(index: index, file: sticker, indexKeys: []) - let item = ChatMediaInputStickerGridItem(account: strongSelf.account, collectionId: collectionId, stickerPackInfo: nil, index: ItemCollectionViewEntryIndex(collectionIndex: 0, collectionId: collectionId, itemIndex: index), stickerItem: stickerPackItem, canManagePeerSpecificPack: nil, interfaceInteraction: nil, inputNodeInteraction: inputNodeInteraction, hasAccessory: false, theme: interfaceState.theme, large: true, selected: {}) + let item = ChatMediaInputStickerGridItem(account: strongSelf.context.account, collectionId: collectionId, stickerPackInfo: nil, index: ItemCollectionViewEntryIndex(collectionIndex: 0, collectionId: collectionId, itemIndex: index), stickerItem: stickerPackItem, canManagePeerSpecificPack: nil, interfaceInteraction: nil, inputNodeInteraction: inputNodeInteraction, hasAccessory: false, theme: interfaceState.theme, large: true, selected: {}) strongSelf.stickerItem = item strongSelf.stickerNode.updateLayout(item: item, size: stickerSize, isVisible: true, synchronousLoads: true) strongSelf.stickerNode.isVisibleInGrid = true @@ -783,24 +768,21 @@ private enum ChatEmptyNodeContentType { } final class ChatEmptyNode: ASDisplayNode { - private let account: Account + private let context: AccountContext private let interaction: ChatPanelInterfaceInteraction? - private let backgroundNode: ASImageNode + private let backgroundNode: NavigationBackgroundNode private var currentTheme: PresentationTheme? private var currentStrings: PresentationStrings? private var content: (ChatEmptyNodeContentType, ASDisplayNode & ChatEmptyNodeContent)? - init(account: Account, interaction: ChatPanelInterfaceInteraction?) { - self.account = account + init(context: AccountContext, interaction: ChatPanelInterfaceInteraction?) { + self.context = context self.interaction = interaction - self.backgroundNode = ASImageNode() - self.backgroundNode.isLayerBacked = true - self.backgroundNode.displayWithoutProcessing = true - self.backgroundNode.displaysAsynchronously = false + self.backgroundNode = NavigationBackgroundNode(color: .clear) super.init() @@ -813,9 +795,8 @@ final class ChatEmptyNode: ASDisplayNode { if self.currentTheme !== interfaceState.theme || self.currentStrings !== interfaceState.strings { self.currentTheme = interfaceState.theme self.currentStrings = interfaceState.strings - - let graphics = PresentationResourcesChat.additionalGraphics(interfaceState.theme, wallpaper: interfaceState.chatWallpaper, bubbleCorners: interfaceState.bubbleCorners) - self.backgroundNode.image = graphics.chatEmptyItemBackgroundImage + + self.backgroundNode.updateColor(color: selectDateFillStaticColor(theme: interfaceState.theme, wallpaper: interfaceState.chatWallpaper), enableBlur: dateFillNeedsBlur(theme: interfaceState.theme, wallpaper: interfaceState.chatWallpaper), transition: .immediate) } var isScheduledMessages = false @@ -827,7 +808,7 @@ final class ChatEmptyNode: ASDisplayNode { if case .replyThread = interfaceState.chatLocation { contentType = .regular } else if let peer = interfaceState.renderedPeer?.peer, !isScheduledMessages { - if peer.id == self.account.peerId { + if peer.id == self.context.account.peerId { contentType = .cloud } else if let _ = peer as? TelegramSecretChat { contentType = .secret @@ -852,6 +833,7 @@ final class ChatEmptyNode: ASDisplayNode { contentType = .regular } + var updateGreetingSticker = false var contentTransition = transition if self.content?.0 != contentType { var animateContentIn = false @@ -872,9 +854,10 @@ final class ChatEmptyNode: ASDisplayNode { case .cloud: node = ChatEmptyNodeCloudChatContent() case .peerNearby: - node = ChatEmptyNodeNearbyChatContent(account: self.account, interaction: self.interaction) + node = ChatEmptyNodeNearbyChatContent(context: self.context, interaction: self.interaction) case .greeting: - node = ChatEmptyNodeGreetingChatContent(account: self.account, interaction: self.interaction) + node = ChatEmptyNodeGreetingChatContent(context: self.context, interaction: self.interaction) + updateGreetingSticker = true } self.content = (contentType, node) self.addSubnode(node) @@ -892,6 +875,10 @@ final class ChatEmptyNode: ASDisplayNode { var contentSize = CGSize() if let contentNode = self.content?.1 { contentSize = contentNode.updateLayout(interfaceState: interfaceState, size: displayRect.size, transition: contentTransition) + + if updateGreetingSticker { + self.context.prefetchManager?.prepareNextGreetingSticker() + } } let contentFrame = CGRect(origin: CGPoint(x: displayRect.minX + floor((displayRect.width - contentSize.width) / 2.0), y: displayRect.minY + floor((displayRect.height - contentSize.height) / 2.0)), size: contentSize) @@ -900,18 +887,6 @@ final class ChatEmptyNode: ASDisplayNode { } transition.updateFrame(node: self.backgroundNode, frame: contentFrame) - } - - var greetingStickerNode: ASDisplayNode? { - if let (_, node) = self.content { - if let node = node as? ChatEmptyNodeGreetingChatContent { - return node.greetingStickerNode - } else if let node = node as? ChatEmptyNodeNearbyChatContent { - return node.greetingStickerNode - } - } - return nil + self.backgroundNode.update(size: self.backgroundNode.bounds.size, cornerRadius: min(20.0, self.backgroundNode.bounds.height / 2.0), transition: transition) } } - - diff --git a/submodules/TelegramUI/Sources/ChatHistoryEntriesForView.swift b/submodules/TelegramUI/Sources/ChatHistoryEntriesForView.swift index 26c42c8961..99eaf5d0e5 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryEntriesForView.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryEntriesForView.swift @@ -99,7 +99,7 @@ func chatHistoryEntriesForView(location: ChatLocation, view: MessageHistoryView, } else { selection = .none } - groupBucket.append((message, isRead, selection, ChatMessageEntryAttributes(rank: adminRank, isContact: entry.attributes.authorIsContact, contentTypeHint: contentTypeHint, updatingMedia: updatingMedia[message.id]))) + groupBucket.append((message, isRead, selection, ChatMessageEntryAttributes(rank: adminRank, isContact: entry.attributes.authorIsContact, contentTypeHint: contentTypeHint, updatingMedia: updatingMedia[message.id], isPlaying: false))) } else { let selection: ChatHistoryMessageSelection if let selectedMessages = selectedMessages { @@ -107,7 +107,7 @@ func chatHistoryEntriesForView(location: ChatLocation, view: MessageHistoryView, } else { selection = .none } - entries.append(.MessageEntry(message, presentationData, isRead, entry.monthLocation, selection, ChatMessageEntryAttributes(rank: adminRank, isContact: entry.attributes.authorIsContact, contentTypeHint: contentTypeHint, updatingMedia: updatingMedia[message.id]))) + entries.append(.MessageEntry(message, presentationData, isRead, entry.monthLocation, selection, ChatMessageEntryAttributes(rank: adminRank, isContact: entry.attributes.authorIsContact, contentTypeHint: contentTypeHint, updatingMedia: updatingMedia[message.id], isPlaying: message.index == associatedData.currentlyPlayingMessageId))) } } else { let selection: ChatHistoryMessageSelection @@ -116,7 +116,7 @@ func chatHistoryEntriesForView(location: ChatLocation, view: MessageHistoryView, } else { selection = .none } - entries.append(.MessageEntry(message, presentationData, isRead, entry.monthLocation, selection, ChatMessageEntryAttributes(rank: adminRank, isContact: entry.attributes.authorIsContact, contentTypeHint: contentTypeHint, updatingMedia: updatingMedia[message.id]))) + entries.append(.MessageEntry(message, presentationData, isRead, entry.monthLocation, selection, ChatMessageEntryAttributes(rank: adminRank, isContact: entry.attributes.authorIsContact, contentTypeHint: contentTypeHint, updatingMedia: updatingMedia[message.id], isPlaying: message.index == associatedData.currentlyPlayingMessageId))) } } @@ -167,11 +167,11 @@ func chatHistoryEntriesForView(location: ChatLocation, view: MessageHistoryView, if messages.count > 1, let groupInfo = messages[0].groupInfo { var groupMessages: [(Message, Bool, ChatHistoryMessageSelection, ChatMessageEntryAttributes)] = [] for message in messages { - groupMessages.append((message, false, .none, ChatMessageEntryAttributes(rank: adminRank, isContact: false, contentTypeHint: contentTypeHint, updatingMedia: updatingMedia[message.id]))) + groupMessages.append((message, false, .none, ChatMessageEntryAttributes(rank: adminRank, isContact: false, contentTypeHint: contentTypeHint, updatingMedia: updatingMedia[message.id], isPlaying: false))) } entries.insert(.MessageGroupEntry(groupInfo, groupMessages, presentationData), at: 0) } else { - entries.insert(.MessageEntry(messages[0], presentationData, false, nil, selection, ChatMessageEntryAttributes(rank: adminRank, isContact: false, contentTypeHint: contentTypeHint, updatingMedia: updatingMedia[messages[0].id])), at: 0) + entries.insert(.MessageEntry(messages[0], presentationData, false, nil, selection, ChatMessageEntryAttributes(rank: adminRank, isContact: false, contentTypeHint: contentTypeHint, updatingMedia: updatingMedia[messages[0].id], isPlaying: false)), at: 0) } let replyCount = view.entries.isEmpty ? 0 : 1 diff --git a/submodules/TelegramUI/Sources/ChatHistoryEntry.swift b/submodules/TelegramUI/Sources/ChatHistoryEntry.swift index a3ffe24b0a..95ae7fa8f8 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryEntry.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryEntry.swift @@ -17,12 +17,14 @@ public struct ChatMessageEntryAttributes: Equatable { let isContact: Bool let contentTypeHint: ChatMessageEntryContentType let updatingMedia: ChatUpdatingMessageMedia? + let isPlaying: Bool - init(rank: CachedChannelAdminRank?, isContact: Bool, contentTypeHint: ChatMessageEntryContentType, updatingMedia: ChatUpdatingMessageMedia?) { + init(rank: CachedChannelAdminRank?, isContact: Bool, contentTypeHint: ChatMessageEntryContentType, updatingMedia: ChatUpdatingMessageMedia?, isPlaying: Bool) { self.rank = rank self.isContact = isContact self.contentTypeHint = contentTypeHint self.updatingMedia = updatingMedia + self.isPlaying = isPlaying } public init() { @@ -30,6 +32,7 @@ public struct ChatMessageEntryAttributes: Equatable { self.isContact = false self.contentTypeHint = .generic self.updatingMedia = nil + self.isPlaying = false } } diff --git a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift index 55e28543fe..74be953d83 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift @@ -17,6 +17,7 @@ import AppBundle import ListMessageItem import AccountContext import ChatInterfaceState +import ChatListUI extension ChatReplyThreadMessage { var effectiveTopId: MessageId { @@ -30,70 +31,6 @@ struct ChatTopVisibleMessageRange: Equatable { var isLast: Bool } -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) - - let touchesArray = Array(touches) - if touchesArray.count == 2, let firstTouch = touchesArray.first, let secondTouch = touchesArray.last { - let firstLocation = firstTouch.location(in: self.view) - let secondLocation = secondTouch.location(in: self.view) - - func distance(_ v1: CGPoint, _ v2: CGPoint) -> CGFloat { - let dx = v1.x - v2.x - let dy = v1.y - v2.y - return sqrt(dx * dx + dy * dy) - } - if distance(firstLocation, secondLocation) > 70 { - self.state = .failed - } - } - } - } - - - 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 (abs(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 { @@ -168,45 +105,45 @@ struct ChatHistoryViewTransitionUpdateEntry { } struct ChatHistoryViewTransition { - let historyView: ChatHistoryView - let deleteItems: [ListViewDeleteItem] - let insertEntries: [ChatHistoryViewTransitionInsertEntry] - let updateEntries: [ChatHistoryViewTransitionUpdateEntry] - let options: ListViewDeleteAndInsertOptions - let scrollToItem: ListViewScrollToItem? - let stationaryItemRange: (Int, Int)? - let initialData: InitialMessageHistoryData? - let keyboardButtonsMessage: Message? - let cachedData: CachedPeerData? - let cachedDataMessages: [MessageId: Message]? - let readStateData: [PeerId: ChatHistoryCombinedInitialReadStateData]? - let scrolledToIndex: MessageHistoryAnchorIndex? - let scrolledToSomeIndex: Bool - let animateIn: Bool - let reason: ChatHistoryViewTransitionReason - let flashIndicators: Bool + var historyView: ChatHistoryView + var deleteItems: [ListViewDeleteItem] + var insertEntries: [ChatHistoryViewTransitionInsertEntry] + var updateEntries: [ChatHistoryViewTransitionUpdateEntry] + var options: ListViewDeleteAndInsertOptions + var scrollToItem: ListViewScrollToItem? + var stationaryItemRange: (Int, Int)? + var initialData: InitialMessageHistoryData? + var keyboardButtonsMessage: Message? + var cachedData: CachedPeerData? + var cachedDataMessages: [MessageId: Message]? + var readStateData: [PeerId: ChatHistoryCombinedInitialReadStateData]? + var scrolledToIndex: MessageHistoryAnchorIndex? + var scrolledToSomeIndex: Bool + var animateIn: Bool + var reason: ChatHistoryViewTransitionReason + var flashIndicators: Bool } struct ChatHistoryListViewTransition { - let historyView: ChatHistoryView - let deleteItems: [ListViewDeleteItem] - let insertItems: [ListViewInsertItem] - let updateItems: [ListViewUpdateItem] - let options: ListViewDeleteAndInsertOptions - let scrollToItem: ListViewScrollToItem? - let stationaryItemRange: (Int, Int)? - let initialData: InitialMessageHistoryData? - let keyboardButtonsMessage: Message? - let cachedData: CachedPeerData? - let cachedDataMessages: [MessageId: Message]? - let readStateData: [PeerId: ChatHistoryCombinedInitialReadStateData]? - let scrolledToIndex: MessageHistoryAnchorIndex? - let scrolledToSomeIndex: Bool - let peerType: MediaAutoDownloadPeerType - let networkType: MediaAutoDownloadNetworkType - let animateIn: Bool - let reason: ChatHistoryViewTransitionReason - let flashIndicators: Bool + var historyView: ChatHistoryView + var deleteItems: [ListViewDeleteItem] + var insertItems: [ListViewInsertItem] + var updateItems: [ListViewUpdateItem] + var options: ListViewDeleteAndInsertOptions + var scrollToItem: ListViewScrollToItem? + var stationaryItemRange: (Int, Int)? + var initialData: InitialMessageHistoryData? + var keyboardButtonsMessage: Message? + var cachedData: CachedPeerData? + var cachedDataMessages: [MessageId: Message]? + var readStateData: [PeerId: ChatHistoryCombinedInitialReadStateData]? + var scrolledToIndex: MessageHistoryAnchorIndex? + var scrolledToSomeIndex: Bool + var peerType: MediaAutoDownloadPeerType + var networkType: MediaAutoDownloadNetworkType + var animateIn: Bool + var reason: ChatHistoryViewTransitionReason + var flashIndicators: Bool } private func maxMessageIndexForEntries(_ view: ChatHistoryView, indexRange: (Int, Int)) -> (incoming: MessageIndex?, overall: MessageIndex?) { @@ -306,7 +243,7 @@ private func mappedInsertEntries(context: AccountContext, chatLocation: ChatLoca case let .UnreadEntry(_, presentationData): return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatUnreadItem(index: entry.entry.index, presentationData: presentationData, context: context), directionHint: entry.directionHint) case let .ReplyCountEntry(_, isComments, count, presentationData): - return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatReplyCountItem(index: entry.entry.index, isComments: isComments, count: count, presentationData: presentationData, context: context), directionHint: entry.directionHint) + return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatReplyCountItem(index: entry.entry.index, isComments: isComments, count: count, presentationData: presentationData, context: context, controllerInteraction: controllerInteraction), directionHint: entry.directionHint) case let .ChatInfoEntry(title, text, presentationData): return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatBotInfoItem(title: title, text: text, controllerInteraction: controllerInteraction, presentationData: presentationData), directionHint: entry.directionHint) case let .SearchEntry(theme, strings): @@ -351,7 +288,7 @@ private func mappedUpdateEntries(context: AccountContext, chatLocation: ChatLoca case let .UnreadEntry(_, presentationData): return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatUnreadItem(index: entry.entry.index, presentationData: presentationData, context: context), directionHint: entry.directionHint) case let .ReplyCountEntry(_, isComments, count, presentationData): - return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatReplyCountItem(index: entry.entry.index, isComments: isComments, count: count, presentationData: presentationData, context: context), directionHint: entry.directionHint) + return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatReplyCountItem(index: entry.entry.index, isComments: isComments, count: count, presentationData: presentationData, context: context, controllerInteraction: controllerInteraction), directionHint: entry.directionHint) case let .ChatInfoEntry(title, text, presentationData): return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatBotInfoItem(title: title, text: text, controllerInteraction: controllerInteraction, presentationData: presentationData), directionHint: entry.directionHint) case let .SearchEntry(theme, strings): @@ -374,7 +311,7 @@ private final class ChatHistoryTransactionOpaqueState { } } -private func extractAssociatedData(chatLocation: ChatLocation, view: MessageHistoryView, automaticDownloadNetworkType: MediaAutoDownloadNetworkType, animatedEmojiStickers: [String: [StickerPackItem]], subject: ChatControllerSubject?) -> ChatMessageItemAssociatedData { +private func extractAssociatedData(chatLocation: ChatLocation, view: MessageHistoryView, automaticDownloadNetworkType: MediaAutoDownloadNetworkType, animatedEmojiStickers: [String: [StickerPackItem]], subject: ChatControllerSubject?, currentlyPlayingMessageId: MessageIndex?) -> ChatMessageItemAssociatedData { var automaticMediaDownloadPeerType: MediaAutoDownloadPeerType = .channel var contactsPeerIds: Set = Set() var channelDiscussionGroup: ChatMessageItemAssociatedData.ChannelDiscussionGroupStatus = .unknown @@ -423,8 +360,7 @@ private func extractAssociatedData(chatLocation: ChatLocation, view: MessageHist } } - let associatedData = ChatMessageItemAssociatedData(automaticDownloadPeerType: automaticMediaDownloadPeerType, automaticDownloadNetworkType: automaticDownloadNetworkType, isRecentActions: false, subject: subject, contactsPeerIds: contactsPeerIds, channelDiscussionGroup: channelDiscussionGroup, animatedEmojiStickers: animatedEmojiStickers) - return associatedData + return ChatMessageItemAssociatedData(automaticDownloadPeerType: automaticMediaDownloadPeerType, automaticDownloadNetworkType: automaticDownloadNetworkType, isRecentActions: false, subject: subject, contactsPeerIds: contactsPeerIds, channelDiscussionGroup: channelDiscussionGroup, animatedEmojiStickers: animatedEmojiStickers, currentlyPlayingMessageId: currentlyPlayingMessageId) } private extension ChatHistoryLocationInput { @@ -481,12 +417,12 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { private let historyDisposable = MetaDisposable() private let readHistoryDisposable = MetaDisposable() - private let messageViewQueue = Queue(name: "ChatHistoryListNode processing") + //private let messageViewQueue = Queue(name: "ChatHistoryListNode processing") private var dequeuedInitialTransitionOnLayout = false private var enqueuedHistoryViewTransitions: [ChatHistoryListViewTransition] = [] private var hasActiveTransition = false - var layoutActionOnViewTransition: ((ChatHistoryListViewTransition) -> (ChatHistoryListViewTransition, ListViewUpdateSizeAndInsets?))? + var layoutActionOnViewTransition: ((ChatHistoryListViewTransition) -> (ChatHistoryListViewTransition, ListViewUpdateSizeAndInsets?), Int64?)? public let historyState = ValuePromise() public var currentHistoryState: ChatHistoryNodeHistoryState? @@ -512,6 +448,8 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { let canReadHistory = Promise() private var canReadHistoryValue: Bool = false private var canReadHistoryDisposable: Disposable? + + private var messageIdsScheduledForMarkAsSeen = Set() private var chatHistoryLocationValue: ChatHistoryLocationInput? { didSet { @@ -544,7 +482,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { private var maxVisibleMessageIndexReported: MessageIndex? var maxVisibleMessageIndexUpdated: ((MessageIndex) -> Void)? - var scrolledToIndex: ((MessageHistoryAnchorIndex) -> Void)? + var scrolledToIndex: ((MessageHistoryAnchorIndex, Bool) -> Void)? var scrolledToSomeIndex: (() -> Void)? var beganDragging: (() -> Void)? @@ -589,7 +527,10 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { } } } - + + private let currentlyPlayingMessageIdPromise = ValuePromise(nil) + private var appliedPlayingMessageId: MessageIndex? = nil + private(set) var isScrollAtBottomPosition = false public var isScrollAtBottomPositionUpdated: (() -> Void)? @@ -609,7 +550,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { private let clientId: Atomic - public init(context: AccountContext, chatLocation: ChatLocation, chatLocationContextHolder: Atomic, tagMask: MessageTags?, source: ChatHistoryListSource = .default, subject: ChatControllerSubject?, controllerInteraction: ChatControllerInteraction, selectedMessages: Signal?, NoError>, mode: ChatHistoryListMode = .bubbles) { + public init(context: AccountContext, chatLocation: ChatLocation, chatLocationContextHolder: Atomic, tagMask: MessageTags?, source: ChatHistoryListSource = .default, subject: ChatControllerSubject?, controllerInteraction: ChatControllerInteraction, selectedMessages: Signal?, NoError>, mode: ChatHistoryListMode = .bubbles, messageTransitionNode: @escaping () -> ChatMessageTransitionNode? = { nil }) { var tagMask = tagMask var appendMessagesFromTheSameGroup = false if case .pinnedMessages = subject { @@ -626,7 +567,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.chatFontSize, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, disableAnimations: presentationData.disableAnimations, largeEmoji: presentationData.largeEmoji, chatBubbleCorners: presentationData.chatBubbleCorners, 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: true, largeEmoji: presentationData.largeEmoji, chatBubbleCorners: presentationData.chatBubbleCorners, animatedEmojiScale: 1.0) self.chatPresentationDataPromise = Promise(self.currentPresentationData) @@ -669,12 +610,11 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { self.messageMentionProcessingManager.process = { [weak self, weak context] messageIds in if let strongSelf = self { - let _ = (strongSelf.canReadHistory.get() - |> take(1)).start(next: { [weak context] canReadHistory in - if canReadHistory { - context?.account.viewTracker.updateMarkMentionsSeenForMessageIds(messageIds: messageIds) - } - }) + if strongSelf.canReadHistoryValue { + context?.account.viewTracker.updateMarkMentionsSeenForMessageIds(messageIds: messageIds) + } else { + strongSelf.messageIdsScheduledForMarkAsSeen.formUnion(messageIds) + } } } @@ -750,7 +690,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { scrollPosition = nil } - return (ChatHistoryViewUpdate.HistoryView(view: MessageHistoryView(tagMask: nil, namespaces: .all, entries: messages.reversed().map { MessageHistoryEntry(message: $0, isRead: false, location: nil, monthLocation: nil, attributes: MutableMessageHistoryEntryAttributes(authorIsContact: false)) }, holeEarlier: hasMore), type: .Generic(type: ViewUpdateType.Initial), scrollPosition: scrollPosition, flashIndicators: false, originalScrollPosition: nil, initialData: ChatHistoryCombinedInitialData(initialData: nil, buttonKeyboardMessage: nil, cachedData: nil, cachedDataMessages: nil, readStateData: nil), id: 0), version, nil) + return (ChatHistoryViewUpdate.HistoryView(view: MessageHistoryView(tagMask: nil, namespaces: .all, entries: messages.reversed().map { MessageHistoryEntry(message: $0, isRead: false, location: nil, monthLocation: nil, attributes: MutableMessageHistoryEntryAttributes(authorIsContact: false)) }, holeEarlier: hasMore, holeLater: false, isLoading: false), type: .Generic(type: ViewUpdateType.Initial), scrollPosition: scrollPosition, flashIndicators: false, originalScrollPosition: nil, initialData: ChatHistoryCombinedInitialData(initialData: nil, buttonKeyboardMessage: nil, cachedData: nil, cachedDataMessages: nil, readStateData: nil), id: 0), version, nil) } } else { historyViewUpdate = self.chatHistoryLocationPromise.get() @@ -790,7 +730,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { } |> distinctUntilChanged - let animatedEmojiStickers = loadedStickerPack(postbox: context.account.postbox, network: context.account.network, reference: .animatedEmoji, forceActualized: false) + let animatedEmojiStickers = context.engine.stickers.loadedStickerPack(reference: .animatedEmoji, forceActualized: false) |> map { animatedEmoji -> [String: [StickerPackItem]] in var animatedEmojiStickers: [String: [StickerPackItem]] = [:] switch animatedEmoji { @@ -894,8 +834,9 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { self.pendingRemovedMessagesPromise.get(), animatedEmojiStickers, customChannelDiscussionReadState, - customThreadOutgoingReadState - ).start(next: { [weak self] update, chatPresentationData, selectedMessages, updatingMedia, networkType, historyAppearsCleared, pendingUnpinnedAllMessages, pendingRemovedMessages, animatedEmojiStickers, customChannelDiscussionReadState, customThreadOutgoingReadState in + customThreadOutgoingReadState, + self.currentlyPlayingMessageIdPromise.get() + ).start(next: { [weak self] update, chatPresentationData, selectedMessages, updatingMedia, networkType, historyAppearsCleared, pendingUnpinnedAllMessages, pendingRemovedMessages, animatedEmojiStickers, customChannelDiscussionReadState, customThreadOutgoingReadState, currentlyPlayingMessageId in func applyHole() { Queue.mainQueue().async { if let strongSelf = self { @@ -906,11 +847,11 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { let historyView = (strongSelf.opaqueTransactionState as? ChatHistoryTransactionOpaqueState)?.historyView let displayRange = strongSelf.displayedItemRange if let filteredEntries = historyView?.filteredEntries, let visibleRange = displayRange.visibleRange { - let lastEntry = filteredEntries[filteredEntries.count - 1 - visibleRange.lastIndex] + let firstEntry = filteredEntries[filteredEntries.count - 1 - visibleRange.firstIndex] - strongSelf.chatHistoryLocationValue = ChatHistoryLocationInput(content: .Navigation(index: .message(lastEntry.index), anchorIndex: .message(lastEntry.index), count: historyMessageCount, highlight: false), id: (strongSelf.chatHistoryLocationValue?.id).flatMap({ $0 + 1 }) ?? 0) + strongSelf.chatHistoryLocationValue = ChatHistoryLocationInput(content: .Navigation(index: .message(firstEntry.index), anchorIndex: .message(firstEntry.index), count: historyMessageCount, highlight: false), id: (strongSelf.chatHistoryLocationValue?.id).flatMap({ $0 + 1 }) ?? 0) } else { - if let subject = subject, case let .message(messageId, highlight) = subject { + if let subject = subject, case let .message(messageId, highlight, _) = subject { strongSelf.chatHistoryLocationValue = ChatHistoryLocationInput(content: .InitialSearch(location: .id(messageId), count: 60, highlight: highlight), id: (strongSelf.chatHistoryLocationValue?.id).flatMap({ $0 + 1 }) ?? 0) } else if let subject = subject, case let .pinnedMessages(maybeMessageId) = subject, let messageId = maybeMessageId { strongSelf.chatHistoryLocationValue = ChatHistoryLocationInput(content: .InitialSearch(location: .id(messageId), count: 60, highlight: true), id: (strongSelf.chatHistoryLocationValue?.id).flatMap({ $0 + 1 }) ?? 0) @@ -978,7 +919,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { reverse = reverseValue } - let associatedData = extractAssociatedData(chatLocation: chatLocation, view: view, automaticDownloadNetworkType: networkType, animatedEmojiStickers: animatedEmojiStickers, subject: subject) + let associatedData = extractAssociatedData(chatLocation: chatLocation, view: view, automaticDownloadNetworkType: networkType, animatedEmojiStickers: animatedEmojiStickers, subject: subject, currentlyPlayingMessageId: currentlyPlayingMessageId) 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, pendingUnpinnedAllMessages: pendingUnpinnedAllMessages, pendingRemovedMessages: pendingRemovedMessages, associatedData: associatedData, updatingMedia: updatingMedia, customChannelDiscussionReadState: customChannelDiscussionReadState, customThreadOutgoingReadState: customThreadOutgoingReadState) let lastHeaderId = filteredEntries.last.flatMap { listMessageDateHeaderId(timestamp: $0.index.timestamp) } ?? 0 @@ -993,7 +934,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { } assert(update.1 >= previousVersion) } - + if scrollPosition == nil, let originalScrollPosition = originalScrollPosition { switch originalScrollPosition { case let .index(index, position, _, _, highlight): @@ -1032,12 +973,22 @@ 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) + + var scrollAnimationCurve: ListViewAnimationCurve? = nil + if let strongSelf = self, strongSelf.appliedPlayingMessageId != currentlyPlayingMessageId, let currentlyPlayingMessageId = currentlyPlayingMessageId { + updatedScrollPosition = .index(index: .message(currentlyPlayingMessageId), position: .center(.bottom), directionHint: .Down, animated: true, highlight: true) + scrollAnimationCurve = .Spring(duration: 0.4) + } + + let rawTransition = preparedChatHistoryViewTransition(from: previous, to: processedView, reason: reason, reverse: reverse, chatLocation: chatLocation, controllerInteraction: controllerInteraction, scrollPosition: updatedScrollPosition, scrollAnimationCurve: scrollAnimationCurve, initialData: initialData?.initialData, keyboardButtonsMessage: view.topTaggedMessages.first, cachedData: initialData?.cachedData, cachedDataMessages: initialData?.cachedDataMessages, readStateData: initialData?.readStateData, flashIndicators: flashIndicators, updatedMessageSelection: previousSelectedMessages != selectedMessages, messageTransitionNode: messageTransitionNode()) 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 } + if strongSelf.appliedPlayingMessageId != currentlyPlayingMessageId { + strongSelf.appliedPlayingMessageId = currentlyPlayingMessageId + } strongSelf.enqueueHistoryViewTransition(mappedTransition) } } @@ -1073,16 +1024,22 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { self.readHistoryDisposable.set(readHistory.start()) - self.canReadHistoryDisposable = (self.canReadHistory.get() |> deliverOnMainQueue).start(next: { [weak self] value in + self.canReadHistoryDisposable = (self.canReadHistory.get() |> deliverOnMainQueue).start(next: { [weak self, weak context] value in if let strongSelf = self { if strongSelf.canReadHistoryValue != value { strongSelf.canReadHistoryValue = value strongSelf.updateReadHistoryActions() + + if strongSelf.canReadHistoryValue && !strongSelf.messageIdsScheduledForMarkAsSeen.isEmpty { + let messageIds = strongSelf.messageIdsScheduledForMarkAsSeen + strongSelf.messageIdsScheduledForMarkAsSeen.removeAll() + context?.account.viewTracker.updateMarkMentionsSeenForMessageIds(messageIds: messageIds) + } } } }) - if let subject = subject, case let .message(messageId, highlight) = subject { + if let subject = subject, case let .message(messageId, highlight, _) = subject { self.chatHistoryLocationValue = ChatHistoryLocationInput(content: .InitialSearch(location: .id(messageId), count: 60, highlight: highlight), id: 0) } else if let subject = subject, case let .pinnedMessages(maybeMessageId) = subject, let messageId = maybeMessageId { self.chatHistoryLocationValue = ChatHistoryLocationInput(content: .InitialSearch(location: .id(messageId), count: 60, highlight: true), id: 0) @@ -1132,16 +1089,18 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { let animatedEmojiConfig = ChatHistoryAnimatedEmojiConfiguration.with(appConfiguration: appConfiguration) - if previousTheme !== presentationData.theme || previousStrings !== presentationData.strings || previousWallpaper != presentationData.chatWallpaper || previousDisableAnimations != presentationData.disableAnimations || previousAnimatedEmojiScale != animatedEmojiConfig.scale { + if previousTheme !== presentationData.theme || previousStrings !== presentationData.strings || previousWallpaper != presentationData.chatWallpaper || previousAnimatedEmojiScale != animatedEmojiConfig.scale { let themeData = ChatPresentationThemeData(theme: presentationData.theme, wallpaper: presentationData.chatWallpaper) - 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) + let chatPresentationData = ChatPresentationData(theme: themeData, fontSize: presentationData.chatFontSize, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, disableAnimations: true, largeEmoji: presentationData.largeEmoji, chatBubbleCorners: presentationData.chatBubbleCorners, animatedEmojiScale: animatedEmojiConfig.scale) strongSelf.currentPresentationData = chatPresentationData - strongSelf.dynamicBounceEnabled = !presentationData.disableAnimations + strongSelf.dynamicBounceEnabled = false strongSelf.forEachItemHeaderNode { itemHeaderNode in if let dateNode = itemHeaderNode as? ChatMessageDateHeaderNode { dateNode.updatePresentationData(chatPresentationData, context: context) + } else if let avatarNode = itemHeaderNode as? ChatMessageAvatarHeaderNode { + avatarNode.updatePresentationData(chatPresentationData, context: context) } else if let dateNode = itemHeaderNode as? ListMessageDateHeaderNode { dateNode.updateThemeAndStrings(theme: presentationData.theme, strings: presentationData.strings) } @@ -1181,13 +1140,13 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { } |> distinctUntilChanged(isEqual: { $0 == $1 }) |> mapToSignal { messageId -> Signal in if let messageId = messageId { - return getMessagesLoadIfNecessary([messageId], postbox: context.account.postbox, network: context.account.network, accountPeerId: context.account.peerId) |> map { _ -> Void in return Void() } + return context.engine.messages.getMessagesLoadIfNecessary([messageId]) |> map { _ -> Void in return Void() } } else { return .complete() } }).start() - self.beganInteractiveDragging = { [weak self] in + self.beganInteractiveDragging = { [weak self] _ in self?.isInteractivelyScrollingValue = true self?.isInteractivelyScrollingPromise.set(true) self?.beganDragging?() @@ -1582,7 +1541,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { public func scrollToEndOfHistory() { self.beganDragging?() switch self.visibleContentOffset() { - case .known(0.0): + case let .known(value) where value <= CGFloat.ulpOfOne: break default: let locationInput = ChatHistoryLocationInput(content: .Scroll(index: .upperBound, anchorIndex: .upperBound, sourceIndex: .lowerBound, scrollPosition: .top(0.0), animated: true, highlight: false), id: self.takeNextHistoryLocationId()) @@ -1765,8 +1724,8 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { let transition = self.enqueuedHistoryViewTransitions.removeFirst() let animated = transition.options.contains(.AnimateInsertion) - - let completion: (ListViewDisplayedItemRange) -> Void = { [weak self] visibleRange in + + let completion: (Bool, ListViewDisplayedItemRange) -> Void = { [weak self] wasTransformed, visibleRange in if let strongSelf = self { strongSelf.historyView = transition.historyView @@ -1774,7 +1733,6 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { if let historyView = strongSelf.historyView { if historyView.filteredEntries.isEmpty { if let firstEntry = historyView.originalView.entries.first { - var isPeerJoined = false var emptyType = ChatHistoryNodeLoadState.EmptyType.generic for media in firstEntry.message.media { if let action = media as? TelegramMediaAction { @@ -1882,24 +1840,79 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { if let scrolledToIndex = transition.scrolledToIndex { if let strongSelf = self { - strongSelf.scrolledToIndex?(scrolledToIndex) + let isInitial: Bool + if case .Initial = transition.reason { + isInitial = true + } else { + isInitial = false + } + strongSelf.scrolledToIndex?(scrolledToIndex, isInitial) } } else if transition.scrolledToSomeIndex { self?.scrolledToSomeIndex?() } + + if let currentSendAnimationCorrelationId = strongSelf.currentSendAnimationCorrelationId { + var foundItemNode: ChatMessageItemView? + strongSelf.forEachItemNode { itemNode in + if let itemNode = itemNode as? ChatMessageItemView, let item = itemNode.item { + for (message, _) in item.content { + for attribute in message.attributes { + if let attribute = attribute as? OutgoingMessageInfoAttribute { + if attribute.correlationId == currentSendAnimationCorrelationId { + foundItemNode = itemNode + } + } + } + } + } + } + + if let foundItemNode = foundItemNode { + strongSelf.currentSendAnimationCorrelationId = nil + strongSelf.animationCorrelationMessageFound?(foundItemNode, currentSendAnimationCorrelationId) + } + } strongSelf.hasActiveTransition = false strongSelf.dequeueHistoryViewTransitions() } } - if let layoutActionOnViewTransition = self.layoutActionOnViewTransition { - self.layoutActionOnViewTransition = nil + if let (layoutActionOnViewTransition, layoutCorrelationId) = self.layoutActionOnViewTransition { + var foundCorrelationMessage = false + if let layoutCorrelationId = layoutCorrelationId { + itemSearch: for item in transition.insertItems { + if let messageItem = item.item as? ChatMessageItem { + for (message, _) in messageItem.content { + for attribute in message.attributes { + if let attribute = attribute as? OutgoingMessageInfoAttribute { + if attribute.correlationId == layoutCorrelationId { + foundCorrelationMessage = true + break itemSearch + } + } + } + } + } + } + } else { + foundCorrelationMessage = true + } + + if foundCorrelationMessage { + self.layoutActionOnViewTransition = nil + } + let (mappedTransition, updateSizeAndInsets) = layoutActionOnViewTransition(transition) - - self.transaction(deleteIndices: mappedTransition.deleteItems, insertIndicesAndItems: transition.insertItems, updateIndicesAndItems: transition.updateItems, options: mappedTransition.options, scrollToItem: mappedTransition.scrollToItem, updateSizeAndInsets: updateSizeAndInsets, stationaryItemRange: mappedTransition.stationaryItemRange, updateOpaqueState: ChatHistoryTransactionOpaqueState(historyView: transition.historyView), completion: completion) + + self.transaction(deleteIndices: mappedTransition.deleteItems, insertIndicesAndItems: transition.insertItems, updateIndicesAndItems: transition.updateItems, options: mappedTransition.options, scrollToItem: mappedTransition.scrollToItem, updateSizeAndInsets: updateSizeAndInsets, stationaryItemRange: mappedTransition.stationaryItemRange, updateOpaqueState: ChatHistoryTransactionOpaqueState(historyView: transition.historyView), completion: { result in + completion(true, result) + }) } else { - self.transaction(deleteIndices: transition.deleteItems, insertIndicesAndItems: transition.insertItems, updateIndicesAndItems: transition.updateItems, options: transition.options, scrollToItem: transition.scrollToItem, stationaryItemRange: transition.stationaryItemRange, updateOpaqueState: ChatHistoryTransactionOpaqueState(historyView: transition.historyView), completion: completion) + self.transaction(deleteIndices: transition.deleteItems, insertIndicesAndItems: transition.insertItems, updateIndicesAndItems: transition.updateItems, options: transition.options, scrollToItem: transition.scrollToItem, stationaryItemRange: transition.stationaryItemRange, updateOpaqueState: ChatHistoryTransactionOpaqueState(historyView: transition.historyView), completion: { result in + completion(false, result) + }) } if transition.flashIndicators { @@ -1941,7 +1954,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { } else if self.interactiveReadActionDisposable == nil { if case let .peer(peerId) = self.chatLocation { if !self.context.sharedContext.immediateExperimentalUISettings.skipReadHistory { - self.interactiveReadActionDisposable = installInteractiveReadMessagesAction(postbox: self.context.account.postbox, stateManager: self.context.account.stateManager, peerId: peerId) + self.interactiveReadActionDisposable = self.context.engine.messages.installInteractiveReadMessagesAction(peerId: peerId) } } } @@ -2105,6 +2118,56 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { } } + func requestMessageUpdate(stableId: UInt32) { + if let historyView = self.historyView { + var messageItem: ChatMessageItem? + self.forEachItemNode({ itemNode in + if let itemNode = itemNode as? ChatMessageItemView, let item = itemNode.item { + for (message, _) in item.content { + if message.stableId == stableId { + messageItem = item + break + } + } + } + }) + + if let messageItem = messageItem { + let associatedData = messageItem.associatedData + + loop: for i in 0 ..< historyView.filteredEntries.count { + switch historyView.filteredEntries[i] { + case let .MessageEntry(message, presentationData, read, _, selection, attributes): + if message.stableId == stableId { + let index = historyView.filteredEntries.count - 1 - i + let item: ListViewItem + 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(_, _, displayHeaders, hintLinks, isGlobalSearch): + let displayHeader: Bool + switch displayHeaders { + case .none: + displayHeader = false + case .all: + displayHeader = true + case .allButLast: + displayHeader = listMessageDateHeaderId(timestamp: message.timestamp) != historyView.lastHeaderId + } + item = ListMessageItem(presentationData: presentationData, context: self.context, chatLocation: self.chatLocation, interaction: ListMessageItemInteraction(controllerInteraction: self.controllerInteraction), message: message, selection: selection, displayHeader: displayHeader, hintIsLink: hintLinks, isGlobalSearchResult: isGlobalSearch) + } + 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 }) + break loop + } + default: + break + } + } + } + } + } + private func messagesAtPoint(_ point: CGPoint) -> [Message]? { var resultMessages: [Message]? self.forEachVisibleItemNode { itemNode in @@ -2239,4 +2302,20 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { }) self.selectionScrollDisplayLink?.isPaused = false } + + + func voicePlaylistItemChanged(_ previousItem: SharedMediaPlaylistItem?, _ currentItem: SharedMediaPlaylistItem?) -> Void { + if let currentItem = currentItem?.id as? PeerMessagesMediaPlaylistItemId { + self.currentlyPlayingMessageIdPromise.set(currentItem.messageIndex) + } else { + self.currentlyPlayingMessageIdPromise.set(nil) + } + } + + private var currentSendAnimationCorrelationId: Int64? + func setCurrentSendAnimationCorrelationId(_ value: Int64?) { + self.currentSendAnimationCorrelationId = value + } + + var animationCorrelationMessageFound: ((ChatMessageItemView, Int64?) -> Void)? } diff --git a/submodules/TelegramUI/Sources/ChatHistoryNavigationButtonNode.swift b/submodules/TelegramUI/Sources/ChatHistoryNavigationButtonNode.swift index 4105b4aeea..ed35ecf4e2 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryNavigationButtonNode.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryNavigationButtonNode.swift @@ -12,6 +12,7 @@ enum ChatHistoryNavigationButtonType { } class ChatHistoryNavigationButtonNode: ASControlNode { + private let backgroundNode: NavigationBackgroundNode private let imageNode: ASImageNode private let badgeBackgroundNode: ASImageNode private let badgeTextNode: ASTextNode @@ -42,6 +43,8 @@ class ChatHistoryNavigationButtonNode: ASControlNode { init(theme: PresentationTheme, type: ChatHistoryNavigationButtonType) { self.theme = theme self.type = type + + self.backgroundNode = NavigationBackgroundNode(color: theme.chat.inputPanel.panelBackgroundColor) self.imageNode = ASImageNode() self.imageNode.displayWithoutProcessing = true @@ -65,7 +68,11 @@ class ChatHistoryNavigationButtonNode: ASControlNode { self.badgeTextNode.displaysAsynchronously = false super.init() - + + self.addSubnode(self.backgroundNode) + self.backgroundNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 38.0, height: 38.0)) + self.backgroundNode.update(size: self.backgroundNode.bounds.size, cornerRadius: 38.0 / 2.0, transition: .immediate) + self.addSubnode(self.imageNode) self.imageNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 38.0, height: 38.0)) @@ -78,7 +85,8 @@ class ChatHistoryNavigationButtonNode: ASControlNode { func updateTheme(theme: PresentationTheme) { if self.theme !== theme { self.theme = theme - + + self.backgroundNode.updateColor(color: theme.chat.inputPanel.panelBackgroundColor, transition: .immediate) switch self.type { case .down: self.imageNode.image = PresentationResourcesChat.chatHistoryNavigationButtonImage(theme) diff --git a/submodules/TelegramUI/Sources/ChatHistoryNavigationButtons.swift b/submodules/TelegramUI/Sources/ChatHistoryNavigationButtons.swift index 0cbd2d86a5..888f2dcfe1 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryNavigationButtons.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryNavigationButtons.swift @@ -59,10 +59,12 @@ final class ChatHistoryNavigationButtons: ASDisplayNode { self.mentionsButton = ChatHistoryNavigationButtonNode(theme: theme, type: .mentions) self.mentionsButton.alpha = 0.0 + self.mentionsButton.isHidden = true self.mentionsButtonTapNode = ASDisplayNode() self.downButton = ChatHistoryNavigationButtonNode(theme: theme, type: .down) self.downButton.alpha = 0.0 + self.downButton.isHidden = true super.init() @@ -100,19 +102,32 @@ final class ChatHistoryNavigationButtons: ASDisplayNode { if self.displayDownButton { mentionsOffset = buttonSize.height + 12.0 + + self.downButton.isHidden = false transition.updateAlpha(node: self.downButton, alpha: 1.0) transition.updateTransformScale(node: self.downButton, scale: 1.0) } else { - transition.updateAlpha(node: self.downButton, alpha: 0.0) + transition.updateAlpha(node: self.downButton, alpha: 0.0, completion: { [weak self] completed in + guard let strongSelf = self, completed else { + return + } + strongSelf.downButton.isHidden = true + }) transition.updateTransformScale(node: self.downButton, scale: 0.2) } if self.mentionCount != 0 { + self.mentionsButton.isHidden = false transition.updateAlpha(node: self.mentionsButton, alpha: 1.0) transition.updateTransformScale(node: self.mentionsButton, scale: 1.0) self.mentionsButtonTapNode.isHidden = false } else { - transition.updateAlpha(node: self.mentionsButton, alpha: 0.0) + transition.updateAlpha(node: self.mentionsButton, alpha: 0.0, completion: { [weak self] completed in + guard let strongSelf = self, completed else { + return + } + strongSelf.mentionsButton.isHidden = true + }) transition.updateTransformScale(node: self.mentionsButton, scale: 0.2) self.mentionsButtonTapNode.isHidden = true } diff --git a/submodules/TelegramUI/Sources/ChatHistorySearchContainerNode.swift b/submodules/TelegramUI/Sources/ChatHistorySearchContainerNode.swift index 7f99889553..50ef7caa52 100644 --- a/submodules/TelegramUI/Sources/ChatHistorySearchContainerNode.swift +++ b/submodules/TelegramUI/Sources/ChatHistorySearchContainerNode.swift @@ -194,7 +194,7 @@ final class ChatHistorySearchContainerNode: SearchDisplayControllerContentNode { if let strongSelf = self { let signal: Signal<([ChatHistorySearchEntry], [MessageId: Message])?, NoError> if let query = query, !query.isEmpty { - let foundRemoteMessages: Signal<[Message], NoError> = searchMessages(account: context.account, location: .peer(peerId: peerId, fromId: nil, tags: tagMask, topMsgId: nil, minDate: nil, maxDate: nil), query: query, state: nil) + let foundRemoteMessages: Signal<[Message], NoError> = context.engine.messages.searchMessages(location: .peer(peerId: peerId, fromId: nil, tags: tagMask, topMsgId: nil, minDate: nil, maxDate: nil), query: query, state: nil) |> map { $0.0.messages } |> delay(0.2, queue: Queue.concurrentDefaultQueue()) @@ -231,7 +231,7 @@ final class ChatHistorySearchContainerNode: SearchDisplayControllerContentNode { } })) - self.listNode.beganInteractiveDragging = { [weak self] in + self.listNode.beganInteractiveDragging = { [weak self] _ in self?.dismissInput?() } diff --git a/submodules/TelegramUI/Sources/ChatHistoryViewForLocation.swift b/submodules/TelegramUI/Sources/ChatHistoryViewForLocation.swift index 85e1547ae8..ce80e0760b 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryViewForLocation.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryViewForLocation.swift @@ -153,6 +153,12 @@ func chatHistoryViewForLocation(_ location: ChatHistoryLocationInput, context: A } else if view.isAddedToChatList, let historyScrollState = (initialData?.chatInterfaceState as? ChatInterfaceState)?.historyScrollState, tagMask == nil { scrollPosition = .positionRestoration(index: historyScrollState.messageIndex, relativeOffset: CGFloat(historyScrollState.relativeOffset)) } else { + if case .peer = chatLocation, !view.isAddedToChatList { + if view.holeEarlier && view.entries.count <= 2 { + fadeIn = true + return .Loading(initialData: combinedInitialData, type: .Generic(type: updateType)) + } + } if view.entries.isEmpty && (view.holeEarlier || view.holeLater) { fadeIn = true return .Loading(initialData: combinedInitialData, type: .Generic(type: updateType)) @@ -335,7 +341,7 @@ func fetchAndPreloadReplyThreadInfo(context: AccountContext, subject: ReplyThrea let message: Signal switch subject { case .channelPost(let messageId), .groupMessage(let messageId): - message = fetchChannelReplyThreadMessage(account: context.account, messageId: messageId, atMessageId: atMessageId) + message = context.engine.messages.fetchChannelReplyThreadMessage(messageId: messageId, atMessageId: atMessageId) } return message diff --git a/submodules/TelegramUI/Sources/ChatHoleItem.swift b/submodules/TelegramUI/Sources/ChatHoleItem.swift index b88cf37698..761ec425b4 100644 --- a/submodules/TelegramUI/Sources/ChatHoleItem.swift +++ b/submodules/TelegramUI/Sources/ChatHoleItem.swift @@ -108,14 +108,6 @@ class ChatHoleItemNode: ListViewItemNode { } } - /*override public func header() -> ListViewItemHeader? { - if let item = self.item { - return item.header - } else { - return nil - } - }*/ - override public func animateAdded(_ currentTimestamp: Double, duration: Double) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) } diff --git a/submodules/TelegramUI/Sources/ChatImportStatusPanel.swift b/submodules/TelegramUI/Sources/ChatImportStatusPanel.swift index f3f3653ec6..0af27ac9e3 100644 --- a/submodules/TelegramUI/Sources/ChatImportStatusPanel.swift +++ b/submodules/TelegramUI/Sources/ChatImportStatusPanel.swift @@ -28,7 +28,7 @@ final class ChatImportStatusPanel: ASDisplayNode { if self.theme !== presentationData.theme.theme { self.theme = presentationData.theme.theme - 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 graphics = PresentationResourcesChat.principalGraphics(theme: presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper, bubbleCorners: presentationData.chatBubbleCorners) self.backgroundNode.image = graphics.dateFloatingBackground self.secondaryBackgroundNode.image = graphics.dateFloatingBackground } diff --git a/submodules/TelegramUI/Sources/ChatInfoTitlePanelNode.swift b/submodules/TelegramUI/Sources/ChatInfoTitlePanelNode.swift index 6d54ce28f0..811fd6b4eb 100644 --- a/submodules/TelegramUI/Sources/ChatInfoTitlePanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatInfoTitlePanelNode.swift @@ -126,33 +126,27 @@ private final class ChatInfoTitlePanelButtonNode: HighlightableButtonNode { final class ChatInfoTitlePanelNode: ChatTitleAccessoryPanelNode { private var theme: PresentationTheme? - - private let backgroundNode: ASDisplayNode + private let separatorNode: ASDisplayNode private var buttons: [(ChatInfoTitleButton, ChatInfoTitlePanelButtonNode)] = [] override init() { - self.backgroundNode = ASDisplayNode() - self.backgroundNode.isLayerBacked = true - self.separatorNode = ASDisplayNode() self.separatorNode.isLayerBacked = true super.init() - - self.addSubnode(self.backgroundNode) + self.addSubnode(self.separatorNode) } - override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) -> CGFloat { + override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) -> LayoutResult { let themeUpdated = self.theme !== interfaceState.theme self.theme = interfaceState.theme let panelHeight: CGFloat = 55.0 if themeUpdated { - self.backgroundNode.backgroundColor = interfaceState.theme.chat.historyNavigation.fillColor - self.separatorNode.backgroundColor = interfaceState.theme.chat.historyNavigation.strokeColor + self.separatorNode.backgroundColor = interfaceState.theme.rootController.navigationBar.separatorColor } let updatedButtons: [ChatInfoTitleButton] @@ -205,11 +199,9 @@ final class ChatInfoTitlePanelNode: ChatTitleAccessoryPanelNode { } } - transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: width, height: panelHeight))) + transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: width, height: UIScreenPixel))) - transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: panelHeight - UIScreenPixel), size: CGSize(width: width, height: UIScreenPixel))) - - return panelHeight + return LayoutResult(backgroundHeight: panelHeight, insetHeight: panelHeight) } @objc func buttonPressed(_ node: HighlightableButtonNode) { diff --git a/submodules/TelegramUI/Sources/ChatInputContextPanelNode.swift b/submodules/TelegramUI/Sources/ChatInputContextPanelNode.swift index 60c68d88c1..1d8c0129e0 100644 --- a/submodules/TelegramUI/Sources/ChatInputContextPanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatInputContextPanelNode.swift @@ -34,4 +34,8 @@ class ChatInputContextPanelNode: ASDisplayNode { func animateOut(completion: @escaping () -> Void) { completion() } + + var topItemFrame: CGRect? { + return nil + } } diff --git a/submodules/TelegramUI/Sources/ChatInstantVideoMessageDurationNode.swift b/submodules/TelegramUI/Sources/ChatInstantVideoMessageDurationNode.swift index 49b16264d4..0387adf15b 100644 --- a/submodules/TelegramUI/Sources/ChatInstantVideoMessageDurationNode.swift +++ b/submodules/TelegramUI/Sources/ChatInstantVideoMessageDurationNode.swift @@ -5,7 +5,7 @@ import SwiftSignalKit import Display import UniversalMediaPlayer -private let textFont = Font.regular(11.0) +private let textFont = Font.with(size: 11.0, design: .regular, weight: .regular, traits: [.monospacedNumbers]) private struct ChatInstantVideoMessageDurationNodeState: Equatable { let hours: Int32? @@ -35,28 +35,25 @@ private struct ChatInstantVideoMessageDurationNodeState: Equatable { private final class ChatInstantVideoMessageDurationNodeParameters: NSObject { let state: ChatInstantVideoMessageDurationNodeState let isSeen: Bool - let backgroundColor: UIColor let textColor: UIColor - init(state: ChatInstantVideoMessageDurationNodeState, isSeen: Bool, backgroundColor: UIColor, textColor: UIColor) { + init(state: ChatInstantVideoMessageDurationNodeState, isSeen: Bool, textColor: UIColor) { self.state = state self.isSeen = isSeen - self.backgroundColor = backgroundColor self.textColor = textColor super.init() } } -final class ChatInstantVideoMessageDurationNode: ASDisplayNode { +final class ChatInstantVideoMessageDurationNode: ASImageNode { private var textColor: UIColor - private var fillColor: UIColor var defaultDuration: Double? { didSet { if self.defaultDuration != oldValue { self.updateTimestamp() - self.setNeedsDisplay() + self.updateContents() } } } @@ -64,7 +61,7 @@ final class ChatInstantVideoMessageDurationNode: ASDisplayNode { var isSeen: Bool = false { didSet { if self.isSeen != oldValue { - self.setNeedsDisplay() + self.updateContents() } } } @@ -87,7 +84,7 @@ final class ChatInstantVideoMessageDurationNode: ASDisplayNode { private var state = ChatInstantVideoMessageDurationNodeState() { didSet { if self.state != oldValue { - self.setNeedsDisplay() + self.updateContents() } } } @@ -104,10 +101,12 @@ final class ChatInstantVideoMessageDurationNode: ASDisplayNode { } } } + + var size: CGSize = CGSize() + var sizeUpdated: ((CGSize) -> Void)? - init(textColor: UIColor, fillColor: UIColor) { + init(textColor: UIColor) { self.textColor = textColor - self.fillColor = fillColor super.init() @@ -128,11 +127,10 @@ final class ChatInstantVideoMessageDurationNode: ASDisplayNode { self.updateTimer?.invalidate() } - func updateTheme(textColor: UIColor, fillColor: UIColor) { - if !self.textColor.isEqual(textColor) || !self.fillColor.isEqual(textColor) { + func updateTheme(textColor: UIColor) { + if !self.textColor.isEqual(textColor) { self.textColor = textColor - self.fillColor = fillColor - self.setNeedsDisplay() + self.updateContents() } } @@ -168,12 +166,22 @@ final class ChatInstantVideoMessageDurationNode: ASDisplayNode { self.state = ChatInstantVideoMessageDurationNodeState() } } - - override func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol? { - return ChatInstantVideoMessageDurationNodeParameters(state: self.state, isSeen: self.isSeen, backgroundColor: self.fillColor, textColor: self.textColor) + + private func updateContents() { + let image = self.generateContents(withParameters: self.getParameters(), isCancelled: { return false }) + let previousSize = self.image?.size + self.image = image + if let image = image, previousSize != image.size { + self.size = image.size + self.sizeUpdated?(image.size) + } } - @objc override public class func display(withParameters: Any?, isCancelled: () -> Bool) -> UIImage? { + private func getParameters() -> NSObjectProtocol? { + return ChatInstantVideoMessageDurationNodeParameters(state: self.state, isSeen: self.isSeen, textColor: self.textColor) + } + + private func generateContents(withParameters: Any?, isCancelled: () -> Bool) -> UIImage? { guard let parameters = withParameters as? ChatInstantVideoMessageDurationNodeParameters else { return nil } @@ -196,20 +204,14 @@ final class ChatInstantVideoMessageDurationNode: ASDisplayNode { return generateImage(imageSize, rotatedContext: { size, context in context.clear(CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: size)) - context.setBlendMode(.copy) - context.setFillColor(parameters.backgroundColor.cgColor) - - context.fillEllipse(in: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.height, height: size.height))) - context.fillEllipse(in: CGRect(origin: CGPoint(x: size.width - size.height, y: 0.0), size: CGSize(width: size.height, height: size.height))) - context.fill(CGRect(origin: CGPoint(x: size.height / 2.0, y: 0.0), size: CGSize(width: size.width - size.height, height: size.height))) + context.setBlendMode(.normal) if !parameters.isSeen { context.setFillColor(parameters.textColor.cgColor) let diameter: CGFloat = 4.0 context.fillEllipse(in: CGRect(origin: CGPoint(x: size.width - size.height + floor((size.height - diameter) / 2.0), y: floor((size.height - diameter) / 2.0)), size: CGSize(width: diameter, height: diameter))) } - - context.setBlendMode(.normal) + UIGraphicsPushContext(context) string.draw(at: CGPoint(x: floor((size.width - unseenInset - textRect.size.width) / 2.0) + textRect.origin.x, y: 2.0 + textRect.origin.y + UIScreenPixel)) UIGraphicsPopContext() diff --git a/submodules/TelegramUI/Sources/ChatInterfaceInputContextPanels.swift b/submodules/TelegramUI/Sources/ChatInterfaceInputContextPanels.swift index 9c72469569..4171fca4b1 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceInputContextPanels.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceInputContextPanels.swift @@ -30,8 +30,17 @@ func inputContextPanelForChatPresentationIntefaceState(_ chatPresentationInterfa return nil } + if chatPresentationInterfaceState.showCommands, let renderedPeer = chatPresentationInterfaceState.renderedPeer { + if let currentPanel = currentPanel as? CommandMenuChatInputContextPanelNode { + return currentPanel + } else { + let panel = CommandMenuChatInputContextPanelNode(context: context, theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings, fontSize: chatPresentationInterfaceState.fontSize, peerId: renderedPeer.peerId) + panel.interfaceInteraction = interfaceInteraction + return panel + } + } + guard let inputQueryResult = chatPresentationInterfaceState.inputQueryResults.values.sorted(by: { lhs, rhs in - let (lhsP, lhsHasItems) = inputQueryResultPriority(lhs) let (rhsP, rhsHasItems) = inputQueryResultPriority(rhs) if lhsHasItems != rhsHasItems { diff --git a/submodules/TelegramUI/Sources/ChatInterfaceInputContexts.swift b/submodules/TelegramUI/Sources/ChatInterfaceInputContexts.swift index f52460975e..fbe8b8c8fd 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceInputContexts.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceInputContexts.swift @@ -208,7 +208,7 @@ func inputTextPanelStateForChatPresentationInterfaceState(_ chatPresentationInte let inputQueries = inputContextQueriesForChatPresentationIntefaceState(chatPresentationInterfaceState) for inputQuery in inputQueries { if case let .contextRequest(addressName, query) = inputQuery, query.isEmpty { - let baseFontSize: CGFloat = max(17.0, chatPresentationInterfaceState.fontSize.baseDisplaySize) + let baseFontSize: CGFloat = max(chatTextInputMinFontSize, chatPresentationInterfaceState.fontSize.baseDisplaySize) let string = NSMutableAttributedString() string.append(NSAttributedString(string: "@" + addressName, font: Font.regular(baseFontSize), textColor: UIColor.clear)) @@ -286,7 +286,7 @@ func inputTextPanelStateForChatPresentationInterfaceState(_ chatPresentationInte if !extendedSearchLayout { if case .scheduledMessages = chatPresentationInterfaceState.subject { } else if chatPresentationInterfaceState.renderedPeer?.peerId != context.account.peerId { - if let peer = chatPresentationInterfaceState.renderedPeer?.peer as? TelegramSecretChat { + if let peer = chatPresentationInterfaceState.renderedPeer?.peer as? TelegramSecretChat, chatPresentationInterfaceState.interfaceState.composeInputState.inputText.length == 0 { accessoryItems.append(.messageAutoremoveTimeout(peer.messageAutoremoveTimeout)) } else if currentAutoremoveTimeout != nil && chatPresentationInterfaceState.interfaceState.composeInputState.inputText.length == 0 { accessoryItems.append(.messageAutoremoveTimeout(currentAutoremoveTimeout)) @@ -312,13 +312,14 @@ func inputTextPanelStateForChatPresentationInterfaceState(_ chatPresentationInte stickersEnabled = false } } - if let peer = chatPresentationInterfaceState.renderedPeer?.peer as? TelegramUser, let _ = peer.botInfo { - accessoryItems.append(.commands) - } else if chatPresentationInterfaceState.hasBots { +// if let peer = chatPresentationInterfaceState.renderedPeer?.peer as? TelegramUser, let _ = peer.botInfo { +// accessoryItems.append(.commands) +// } else + if chatPresentationInterfaceState.hasBots { accessoryItems.append(.commands) } accessoryItems.append(.stickers(stickersEnabled)) - if let message = chatPresentationInterfaceState.keyboardButtonsMessage, let _ = message.visibleButtonKeyboardMarkup { + if let message = chatPresentationInterfaceState.keyboardButtonsMessage, let _ = message.visibleButtonKeyboardMarkup, chatPresentationInterfaceState.interfaceState.messageActionsState.dismissedButtonKeyboardMessageId != message.id { accessoryItems.append(.inputButtons) } } diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateAccessoryPanels.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateAccessoryPanels.swift index 38957c8645..85c468508d 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateAccessoryPanels.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateAccessoryPanels.swift @@ -32,7 +32,7 @@ func accessoryPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceS editPanelNode.updateThemeAndStrings(theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings) return editPanelNode } else { - let panelNode = EditAccessoryPanelNode(context: context, messageId: editMessage.messageId, theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings, nameDisplayOrder: chatPresentationInterfaceState.nameDisplayOrder) + let panelNode = EditAccessoryPanelNode(context: context, messageId: editMessage.messageId, theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings, nameDisplayOrder: chatPresentationInterfaceState.nameDisplayOrder, dateTimeFormat: chatPresentationInterfaceState.dateTimeFormat) panelNode.interfaceInteraction = interfaceInteraction return panelNode } @@ -63,7 +63,7 @@ func accessoryPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceS replyPanelNode.updateThemeAndStrings(theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings) return replyPanelNode } else { - let panelNode = ReplyAccessoryPanelNode(context: context, messageId: replyMessageId, theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings, nameDisplayOrder: chatPresentationInterfaceState.nameDisplayOrder) + let panelNode = ReplyAccessoryPanelNode(context: context, messageId: replyMessageId, theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings, nameDisplayOrder: chatPresentationInterfaceState.nameDisplayOrder, dateTimeFormat: chatPresentationInterfaceState.dateTimeFormat) panelNode.interfaceInteraction = interfaceInteraction return panelNode } diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift index 8716e00cf2..06abbb38fd 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift @@ -137,10 +137,6 @@ private func canEditMessage(accountPeerId: PeerId, limitsConfiguration: LimitsCo return false } - -private let starIconEmpty = UIImage(bundleImageName: "Chat/Context Menu/StarIconEmpty")?.precomposed() -private let starIconFilled = UIImage(bundleImageName: "Chat/Context Menu/StarIconFilled")?.precomposed() - func canReplyInChat(_ chatPresentationInterfaceState: ChatPresentationInterfaceState) -> Bool { guard let peer = chatPresentationInterfaceState.renderedPeer?.peer else { return false @@ -473,9 +469,9 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState if let strongController = controller { strongController.dismiss() - let id = arc4random64() + let id = Int64.random(in: Int64.min ... Int64.max) let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: id), partialReference: nil, resource: LocalFileReferenceMediaResource(localFilePath: logPath, randomId: id), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "application/text", size: nil, attributes: [.FileName(fileName: "CallStats.log")]) - let message: EnqueueMessage = .message(text: "", attributes: [], mediaReference: .standalone(media: file), replyToMessageId: nil, localGroupingKey: nil) + let message: EnqueueMessage = .message(text: "", attributes: [], mediaReference: .standalone(media: file), replyToMessageId: nil, localGroupingKey: nil, correlationId: nil) let _ = enqueueMessages(account: context.account, peerId: peerId, messages: [message]).start() } @@ -554,13 +550,21 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState } else { let copyTextWithEntities = { var messageEntities: [MessageTextEntity]? + var restrictedText: String? for attribute in message.attributes { if let attribute = attribute as? TextEntitiesMessageAttribute { messageEntities = attribute.entities - break + } + if let attribute = attribute as? RestrictedContentMessageAttribute { + restrictedText = attribute.platformText(platform: "ios", contentSettings: context.currentContentSettings.with { $0 }) ?? "" } } - storeMessageTextInPasteboard(message.text, entities: messageEntities) + + if let restrictedText = restrictedText { + storeMessageTextInPasteboard(restrictedText, entities: nil) + } else { + storeMessageTextInPasteboard(message.text, entities: messageEntities) + } Queue.mainQueue().after(0.2, { let content: UndoOverlayContent = .copy(text: chatPresentationInterfaceState.strings.Conversation_MessageCopied) @@ -793,7 +797,7 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState if case let .replyThread(replyThreadMessage) = chatPresentationInterfaceState.chatLocation { threadMessageId = replyThreadMessage.messageId } - let _ = (exportMessageLink(account: context.account, peerId: message.id.peerId, messageId: message.id, isThread: threadMessageId != nil) + let _ = (context.engine.messages.exportMessageLink(peerId: message.id.peerId, messageId: message.id, isThread: threadMessageId != nil) |> map { result -> String? in return result } @@ -1219,7 +1223,7 @@ func chatAvailableMessageActionsImpl(postbox: Postbox, accountPeerId: PeerId, me if canDeleteGlobally { optionsMap[id]!.insert(.deleteGlobally) } - if user.botInfo != nil && !user.id.isReplies && !isAction { + if user.botInfo != nil && message.flags.contains(.Incoming) && !user.id.isReplies && !isAction { optionsMap[id]!.insert(.report) } } else if let _ = peer as? TelegramSecretChat { @@ -1264,14 +1268,14 @@ func chatAvailableMessageActionsImpl(postbox: Postbox, accountPeerId: PeerId, me final class ChatDeleteMessageContextItem: ContextMenuCustomItem { fileprivate let timestamp: Double - fileprivate let action: (ContextController, @escaping (ContextMenuActionResult) -> Void) -> Void + fileprivate let action: (ContextControllerProtocol, @escaping (ContextMenuActionResult) -> Void) -> Void - init(timestamp: Double, action: @escaping (ContextController, @escaping (ContextMenuActionResult) -> Void) -> Void) { + init(timestamp: Double, action: @escaping (ContextControllerProtocol, @escaping (ContextMenuActionResult) -> Void) -> Void) { self.timestamp = timestamp self.action = action } - func node(presentationData: PresentationData, getController: @escaping () -> ContextController?, actionSelected: @escaping (ContextMenuActionResult) -> Void) -> ContextMenuCustomNode { + func node(presentationData: PresentationData, getController: @escaping () -> ContextControllerProtocol?, actionSelected: @escaping (ContextMenuActionResult) -> Void) -> ContextMenuCustomNode { return ChatDeleteMessageContextItemNode(presentationData: presentationData, item: self, getController: getController, actionSelected: actionSelected) } } @@ -1281,7 +1285,7 @@ private let textFont = Font.regular(17.0) private final class ChatDeleteMessageContextItemNode: ASDisplayNode, ContextMenuCustomNode, ContextActionNodeProtocol { private let item: ChatDeleteMessageContextItem private let presentationData: PresentationData - private let getController: () -> ContextController? + private let getController: () -> ContextControllerProtocol? private let actionSelected: (ContextMenuActionResult) -> Void private let backgroundNode: ASDisplayNode @@ -1296,7 +1300,7 @@ private final class ChatDeleteMessageContextItemNode: ASDisplayNode, ContextMenu private var pointerInteraction: PointerInteraction? - init(presentationData: PresentationData, item: ChatDeleteMessageContextItem, getController: @escaping () -> ContextController?, actionSelected: @escaping (ContextMenuActionResult) -> Void) { + init(presentationData: PresentationData, item: ChatDeleteMessageContextItem, getController: @escaping () -> ContextControllerProtocol?, actionSelected: @escaping (ContextMenuActionResult) -> Void) { self.item = item self.presentationData = presentationData self.getController = getController diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateContextQueries.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateContextQueries.swift index 32ceb31d47..3863d95b40 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateContextQueries.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateContextQueries.swift @@ -127,7 +127,7 @@ private func updatedContextQueryResultStateForQuery(context: AccountContext, pee case .installed: scope = [.installed] } - return searchStickers(account: context.account, query: query.basicEmoji.0, scope: scope) + return context.engine.stickers.searchStickers(query: query.basicEmoji.0, scope: scope) |> castError(ChatContextQueryError.self) } |> map { stickers -> (ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult? in @@ -149,7 +149,7 @@ private func updatedContextQueryResultStateForQuery(context: AccountContext, pee signal = .single({ _ in return .hashtags([]) }) } - let hashtags: Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, ChatContextQueryError> = recentlyUsedHashtags(postbox: context.account.postbox) + let hashtags: Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, ChatContextQueryError> = context.engine.messages.recentlyUsedHashtags() |> map { hashtags -> (ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult? in let normalizedQuery = query.lowercased() var result: [String] = [] @@ -178,7 +178,7 @@ private func updatedContextQueryResultStateForQuery(context: AccountContext, pee signal = .single({ _ in return .mentions([]) }) } - let inlineBots: Signal<[(Peer, Double)], NoError> = types.contains(.contextBots) ? recentlyUsedInlineBots(postbox: context.account.postbox) : .single([]) + let inlineBots: Signal<[(Peer, Double)], NoError> = types.contains(.contextBots) ? context.engine.peers.recentlyUsedInlineBots() : .single([]) let participants = combineLatest(inlineBots, searchPeerMembers(context: context, peerId: peer.id, chatLocation: chatLocation, query: query, scope: .mention)) |> map { inlineBots, peers -> (ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult? in let filteredInlineBots = inlineBots.sorted(by: { $0.1 > $1.1 }).filter { peer, rating in @@ -233,7 +233,7 @@ private func updatedContextQueryResultStateForQuery(context: AccountContext, pee signal = .single({ _ in return .commands([]) }) } - let commands = peerCommands(account: context.account, id: peer.id) + let commands = context.engine.peers.peerCommands(id: peer.id) |> map { commands -> (ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult? in let filteredCommands = commands.commands.filter { command in if command.command.text.hasPrefix(normalizedQuery) { @@ -264,7 +264,7 @@ private func updatedContextQueryResultStateForQuery(context: AccountContext, pee } let chatPeer = peer - let contextBot = resolvePeerByName(account: context.account, name: addressName) + let contextBot = context.engine.peers.resolvePeerByName(name: addressName) |> mapToSignal { peerId -> Signal in if let peerId = peerId { return context.account.postbox.loadedPeerWithId(peerId) @@ -279,7 +279,7 @@ private func updatedContextQueryResultStateForQuery(context: AccountContext, pee |> castError(ChatContextQueryError.self) |> mapToSignal { peer -> Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, ChatContextQueryError> in if let user = peer as? TelegramUser, let botInfo = user.botInfo, let _ = botInfo.inlinePlaceholder { - let contextResults = requestChatContextResults(account: context.account, botId: user.id, peerId: chatPeer.id, query: query, location: context.sharedContext.locationManager.flatMap { locationManager -> Signal<(Double, Double)?, NoError> in + let contextResults = context.engine.messages.requestChatContextResults(botId: user.id, peerId: chatPeer.id, query: query, location: context.sharedContext.locationManager.flatMap { locationManager -> Signal<(Double, Double)?, NoError> in return `deferred` { Queue.mainQueue().async { requestBotLocationStatus(user.id) @@ -338,13 +338,13 @@ 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 < 2) + var signal = context.engine.stickers.searchEmojiKeywords(inputLanguageCode: languageCode, query: query, completeMatch: query.count < 2) if !languageCode.lowercased().hasPrefix("en") { signal = signal |> mapToSignal { keywords in return .single(keywords) |> then( - searchEmojiKeywords(postbox: context.account.postbox, inputLanguageCode: "en-US", query: query, completeMatch: query.count < 3) + context.engine.stickers.searchEmojiKeywords(inputLanguageCode: "en-US", query: query, completeMatch: query.count < 3) |> map { englishKeywords in return keywords + englishKeywords } diff --git a/submodules/TelegramUI/Sources/ChatInterfaceTitlePanelNodes.swift b/submodules/TelegramUI/Sources/ChatInterfaceTitlePanelNodes.swift index 87f4587397..84830a68a0 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceTitlePanelNodes.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceTitlePanelNodes.swift @@ -32,9 +32,12 @@ func titlePanelForChatPresentationInterfaceState(_ chatPresentationInterfaceStat loop: for context in chatPresentationInterfaceState.titlePanelContexts.reversed() { switch context { case .pinnedMessage: - if let pinnedMessage = chatPresentationInterfaceState.pinnedMessage, pinnedMessage.topMessageId != chatPresentationInterfaceState.interfaceState.messageActionsState.closedPinnedMessageId, !chatPresentationInterfaceState.pendingUnpinnedAllMessages { - selectedContext = context - break loop + if case .pinnedMessages = chatPresentationInterfaceState.subject { + } else { + if let pinnedMessage = chatPresentationInterfaceState.pinnedMessage, pinnedMessage.topMessageId != chatPresentationInterfaceState.interfaceState.messageActionsState.closedPinnedMessageId, !chatPresentationInterfaceState.pendingUnpinnedAllMessages { + selectedContext = context + break loop + } } case .chatInfo, .requestInProgress, .toastAlert: selectedContext = context diff --git a/submodules/TelegramUI/Sources/ChatLoadingNode.swift b/submodules/TelegramUI/Sources/ChatLoadingNode.swift index 6cb7638a0b..fac49f4b8b 100644 --- a/submodules/TelegramUI/Sources/ChatLoadingNode.swift +++ b/submodules/TelegramUI/Sources/ChatLoadingNode.swift @@ -8,18 +8,12 @@ import TelegramPresentationData import ActivityIndicator final class ChatLoadingNode: ASDisplayNode { - private let backgroundNode: ASImageNode + private let backgroundNode: NavigationBackgroundNode private let activityIndicator: ActivityIndicator private let offset: CGPoint 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, bubbleCorners: bubbleCorners) - self.backgroundNode.image = graphics.chatLoadingIndicatorBackgroundImage + self.backgroundNode = NavigationBackgroundNode(color: selectDateFillStaticColor(theme: theme, wallpaper: chatWallpaper), enableBlur: dateFillNeedsBlur(theme: theme, wallpaper: chatWallpaper)) let serviceColor = serviceMessageColorComponents(theme: theme, wallpaper: chatWallpaper) self.activityIndicator = ActivityIndicator(type: .custom(serviceColor.primaryText, 22.0, 2.0, false), speed: .regular) @@ -37,10 +31,10 @@ final class ChatLoadingNode: ASDisplayNode { func updateLayout(size: CGSize, insets: UIEdgeInsets, transition: ContainedViewLayoutTransition) { let displayRect = CGRect(origin: CGPoint(x: 0.0, y: insets.top), size: CGSize(width: size.width, height: size.height - insets.top - insets.bottom)) - - if let image = self.backgroundNode.image { - transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(x: displayRect.minX + floor((displayRect.width - image.size.width) / 2.0), y: displayRect.minY + floor((displayRect.height - image.size.height) / 2.0)), size: image.size)) - } + + let backgroundSize: CGFloat = 30.0 + transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(x: displayRect.minX + floor((displayRect.width - backgroundSize) / 2.0), y: displayRect.minY + floor((displayRect.height - backgroundSize) / 2.0)), size: CGSize(width: backgroundSize, height: backgroundSize))) + self.backgroundNode.update(size: self.backgroundNode.bounds.size, cornerRadius: self.backgroundNode.bounds.height / 2.0, transition: transition) let activitySize = self.activityIndicator.measure(size) transition.updateFrame(node: self.activityIndicator, frame: CGRect(origin: CGPoint(x: displayRect.minX + floor((displayRect.width - activitySize.width) / 2.0) + self.offset.x, y: displayRect.minY + floor((displayRect.height - activitySize.height) / 2.0) + self.offset.y), size: activitySize)) diff --git a/submodules/TelegramUI/Sources/ChatMediaInputGifPane.swift b/submodules/TelegramUI/Sources/ChatMediaInputGifPane.swift index e87bfc7406..df19a573e0 100644 --- a/submodules/TelegramUI/Sources/ChatMediaInputGifPane.swift +++ b/submodules/TelegramUI/Sources/ChatMediaInputGifPane.swift @@ -8,6 +8,7 @@ import SyncCore import SwiftSignalKit import TelegramPresentationData import ContextUI +import AccountContext private func fixListScrolling(_ multiplexedNode: MultiplexedVideoNode) { let searchBarHeight: CGFloat = 56.0 @@ -35,7 +36,7 @@ final class ChatMediaInputGifPaneTrendingState { } final class ChatMediaInputGifPane: ChatMediaInputPane, UIScrollViewDelegate { - private let account: Account + private let context: AccountContext private var theme: PresentationTheme private var strings: PresentationStrings private let controllerInteraction: ChatControllerInteraction @@ -70,8 +71,8 @@ final class ChatMediaInputGifPane: ChatMediaInputPane, UIScrollViewDelegate { private var isLoadingMore: Bool = false private var nextOffset: String? - init(account: Account, theme: PresentationTheme, strings: PresentationStrings, controllerInteraction: ChatControllerInteraction, paneDidScroll: @escaping (ChatMediaInputPane, ChatMediaInputPaneScrollState, ContainedViewLayoutTransition) -> Void, fixPaneScroll: @escaping (ChatMediaInputPane, ChatMediaInputPaneScrollState) -> Void, openGifContextMenu: @escaping (MultiplexedVideoNodeFile, ASDisplayNode, CGRect, ContextGesture, Bool) -> Void) { - self.account = account + init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, controllerInteraction: ChatControllerInteraction, paneDidScroll: @escaping (ChatMediaInputPane, ChatMediaInputPaneScrollState, ContainedViewLayoutTransition) -> Void, fixPaneScroll: @escaping (ChatMediaInputPane, ChatMediaInputPaneScrollState) -> Void, openGifContextMenu: @escaping (MultiplexedVideoNodeFile, ASDisplayNode, CGRect, ContextGesture, Bool) -> Void) { + self.context = context self.theme = theme self.strings = strings self.controllerInteraction = controllerInteraction @@ -208,7 +209,7 @@ final class ChatMediaInputGifPane: ChatMediaInputPane, UIScrollViewDelegate { func initializeIfNeeded() { if self.multiplexedNode == nil { - self.trendingPromise.set(paneGifSearchForQuery(account: account, query: "", offset: nil, incompleteResults: true, delayRequest: false, updateActivity: nil) + self.trendingPromise.set(paneGifSearchForQuery(context: self.context, query: "", offset: nil, incompleteResults: true, delayRequest: false, updateActivity: nil) |> map { items -> ChatMediaInputGifPaneTrendingState? in if let items = items { return ChatMediaInputGifPaneTrendingState(files: items.files, nextOffset: items.nextOffset) @@ -217,7 +218,7 @@ final class ChatMediaInputGifPane: ChatMediaInputPane, UIScrollViewDelegate { } }) - let multiplexedNode = MultiplexedVideoNode(account: self.account, theme: self.theme, strings: self.strings) + let multiplexedNode = MultiplexedVideoNode(account: self.context.account, theme: self.theme, strings: self.strings) self.multiplexedNode = multiplexedNode if let layout = self.validLayout { multiplexedNode.frame = CGRect(origin: CGPoint(), size: layout.0) @@ -235,9 +236,9 @@ final class ChatMediaInputGifPane: ChatMediaInputPane, UIScrollViewDelegate { multiplexedNode.fileSelected = { [weak self] file, sourceNode, sourceRect in if let (collection, result) = file.contextResult { - let _ = self?.controllerInteraction.sendBotContextResultAsGif(collection, result, sourceNode, sourceRect) + let _ = self?.controllerInteraction.sendBotContextResultAsGif(collection, result, sourceNode, sourceRect, false) } else { - let _ = self?.controllerInteraction.sendGif(file.file, sourceNode, sourceRect) + let _ = self?.controllerInteraction.sendGif(file.file, sourceNode, sourceRect, false, false) } } @@ -289,7 +290,7 @@ final class ChatMediaInputGifPane: ChatMediaInputPane, UIScrollViewDelegate { let filesSignal: Signal<(MultiplexedVideoNodeFiles, String?), NoError> switch self.mode { case .recent: - filesSignal = combineLatest(self.trendingPromise.get(), self.account.postbox.combinedView(keys: [.orderedItemList(id: Namespaces.OrderedItemList.CloudRecentGifs)])) + filesSignal = combineLatest(self.trendingPromise.get(), self.context.account.postbox.combinedView(keys: [.orderedItemList(id: Namespaces.OrderedItemList.CloudRecentGifs)])) |> map { trending, view -> (MultiplexedVideoNodeFiles, String?) in var recentGifs: OrderedItemListView? if let orderedView = view.views[.orderedItemList(id: Namespaces.OrderedItemList.CloudRecentGifs)] { @@ -311,7 +312,7 @@ final class ChatMediaInputGifPane: ChatMediaInputPane, UIScrollViewDelegate { } case .trending: if let searchOffset = searchOffset { - filesSignal = paneGifSearchForQuery(account: self.account, query: "", offset: searchOffset, incompleteResults: true, delayRequest: false, updateActivity: nil) + filesSignal = paneGifSearchForQuery(context: self.context, query: "", offset: searchOffset, incompleteResults: true, delayRequest: false, updateActivity: nil) |> map { result -> (MultiplexedVideoNodeFiles, String?) in let canLoadMore: Bool if let result = result { @@ -328,7 +329,7 @@ final class ChatMediaInputGifPane: ChatMediaInputPane, UIScrollViewDelegate { } } case let .emojiSearch(emoji): - filesSignal = paneGifSearchForQuery(account: self.account, query: emoji, offset: searchOffset, incompleteResults: true, staleCachedResults: searchOffset == nil, delayRequest: false, updateActivity: nil) + filesSignal = paneGifSearchForQuery(context: self.context, query: emoji, offset: searchOffset, incompleteResults: true, staleCachedResults: searchOffset == nil, delayRequest: false, updateActivity: nil) |> map { result -> (MultiplexedVideoNodeFiles, String?) in let canLoadMore: Bool if let result = result { diff --git a/submodules/TelegramUI/Sources/ChatMediaInputMetaSectionItemNode.swift b/submodules/TelegramUI/Sources/ChatMediaInputMetaSectionItemNode.swift index 4df8a95697..3490c85396 100644 --- a/submodules/TelegramUI/Sources/ChatMediaInputMetaSectionItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMediaInputMetaSectionItemNode.swift @@ -21,17 +21,19 @@ final class ChatMediaInputMetaSectionItem: ListViewItem { let inputNodeInteraction: ChatMediaInputNodeInteraction let type: ChatMediaInputMetaSectionItemType let theme: PresentationTheme + let expanded: Bool let selectedItem: () -> Void var selectable: Bool { return true } - init(inputNodeInteraction: ChatMediaInputNodeInteraction, type: ChatMediaInputMetaSectionItemType, theme: PresentationTheme, selected: @escaping () -> Void) { + init(inputNodeInteraction: ChatMediaInputNodeInteraction, type: ChatMediaInputMetaSectionItemType, theme: PresentationTheme, expanded: Bool, selected: @escaping () -> Void) { self.inputNodeInteraction = inputNodeInteraction self.type = type self.selectedItem = selected self.theme = theme + self.expanded = expanded } func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) { @@ -40,11 +42,11 @@ final class ChatMediaInputMetaSectionItem: ListViewItem { Queue.mainQueue().async { node.inputNodeInteraction = self.inputNodeInteraction node.setItem(item: self) - node.updateTheme(theme: self.theme) + node.updateTheme(theme: self.theme, expanded: self.expanded) node.updateIsHighlighted() node.updateAppearanceTransition(transition: .immediate) - node.contentSize = CGSize(width: 41.0, height: 41.0) + node.contentSize = self.expanded ? expandedBoundingSize : boundingSize node.insets = ChatMediaInputNode.setupPanelIconInsets(item: self, previousItem: previousItem, nextItem: nextItem) completion(node, { @@ -58,9 +60,9 @@ final class ChatMediaInputMetaSectionItem: 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: node().insets), { _ in + completion(ListViewItemNodeLayout(contentSize: self.expanded ? expandedBoundingSize : boundingSize, insets: node().insets), { _ in (node() as? ChatMediaInputMetaSectionItemNode)?.setItem(item: self) - (node() as? ChatMediaInputMetaSectionItemNode)?.updateTheme(theme: self.theme) + (node() as? ChatMediaInputMetaSectionItemNode)?.updateTheme(theme: self.theme, expanded: self.expanded) }) } } @@ -70,16 +72,22 @@ final class ChatMediaInputMetaSectionItem: ListViewItem { } } -private let boundingSize = CGSize(width: 41.0, height: 41.0) -private let boundingImageSize = CGSize(width: 30.0, height: 30.0) -private let highlightSize = CGSize(width: 35.0, height: 35.0) +private let boundingSize = CGSize(width: 72.0, height: 41.0) +private let expandedBoundingSize = CGSize(width: 72.0, height: 72.0) +private let boundingImageScale: CGFloat = 0.625 +private let highlightSize = CGSize(width: 56.0, height: 56.0) private let verticalOffset: CGFloat = 3.0 + UIScreenPixel final class ChatMediaInputMetaSectionItemNode: ListViewItemNode { + private let containerNode: ASDisplayNode + private let scalingNode: ASDisplayNode private let imageNode: ASImageNode private let textNodeContainer: ASDisplayNode private let textNode: ImmediateTextNode private let highlightNode: ASImageNode + private let titleNode: ImmediateTextNode + + private var currentExpanded = false var item: ChatMediaInputMetaSectionItem? var currentCollectionId: ItemCollectionId? @@ -88,6 +96,11 @@ final class ChatMediaInputMetaSectionItemNode: ListViewItemNode { var theme: PresentationTheme? init() { + self.containerNode = ASDisplayNode() + self.containerNode.transform = CATransform3DMakeRotation(CGFloat.pi / 2.0, 0.0, 0.0, 1.0) + + self.scalingNode = ASDisplayNode() + self.highlightNode = ASImageNode() self.highlightNode.isLayerBacked = true self.highlightNode.isHidden = true @@ -105,22 +118,17 @@ final class ChatMediaInputMetaSectionItemNode: ListViewItemNode { self.textNodeContainer.addSubnode(self.textNode) self.textNodeContainer.isUserInteractionEnabled = false - self.highlightNode.frame = CGRect(origin: CGPoint(x: floor((boundingSize.width - highlightSize.width) / 2.0) + verticalOffset, y: floor((boundingSize.height - highlightSize.height) / 2.0)), size: highlightSize) - - self.imageNode.transform = CATransform3DMakeRotation(CGFloat.pi / 2.0, 0.0, 0.0, 1.0) - - self.textNodeContainer.transform = CATransform3DMakeRotation(CGFloat.pi / 2.0, 0.0, 0.0, 1.0) + self.titleNode = ImmediateTextNode() super.init(layerBacked: false, dynamicBounce: false) - self.addSubnode(self.highlightNode) - self.addSubnode(self.imageNode) - self.addSubnode(self.textNodeContainer) + self.addSubnode(self.containerNode) + self.containerNode.addSubnode(self.scalingNode) - let imageSize = CGSize(width: 26.0, height: 26.0) - self.imageNode.frame = CGRect(origin: CGPoint(x: floor((boundingSize.width - imageSize.width) / 2.0) + verticalOffset, y: floor((boundingSize.height - imageSize.height) / 2.0) + UIScreenPixel), size: imageSize) - - self.textNodeContainer.frame = CGRect(origin: CGPoint(x: floor((boundingSize.width - imageSize.width) / 2.0) + verticalOffset, y: floor((boundingSize.height - imageSize.height) / 2.0) + 1.0), size: imageSize) + self.scalingNode.addSubnode(self.highlightNode) + self.scalingNode.addSubnode(self.titleNode) + self.scalingNode.addSubnode(self.imageNode) + self.scalingNode.addSubnode(self.textNodeContainer) } override func didLoad() { @@ -139,25 +147,60 @@ final class ChatMediaInputMetaSectionItemNode: ListViewItemNode { } } - func updateTheme(theme: PresentationTheme) { + func updateTheme(theme: PresentationTheme, expanded: Bool) { + let imageSize = CGSize(width: 26.0 * 1.6, height: 26.0 * 1.6) + self.imageNode.frame = CGRect(origin: CGPoint(x: floor((expandedBoundingSize.width - imageSize.width) / 2.0), y: floor((expandedBoundingSize.height - imageSize.height) / 2.0) + UIScreenPixel), size: imageSize) + + self.textNodeContainer.frame = CGRect(origin: CGPoint(x: floor((expandedBoundingSize.width - imageSize.width) / 2.0) + verticalOffset, y: floor((expandedBoundingSize.height - imageSize.height) / 2.0) + 1.0), size: imageSize) + if self.theme !== theme { self.theme = theme self.highlightNode.image = PresentationResourcesChat.chatMediaInputPanelHighlightedIconImage(theme) + var title = "" if let item = self.item { switch item.type { case .savedStickers: self.imageNode.image = PresentationResourcesChat.chatInputMediaPanelSavedStickersIcon(theme) + title = "Favorites" case .recentStickers: self.imageNode.image = PresentationResourcesChat.chatInputMediaPanelRecentStickersIcon(theme) + title = "Recent" case .stickersMode: self.imageNode.image = PresentationResourcesChat.chatInputMediaPanelStickersModeIcon(theme) + title = "Stickers" case .savedGifs: self.imageNode.image = PresentationResourcesChat.chatInputMediaPanelRecentStickersIcon(theme) + title = "GIFs" case .trendingGifs: self.imageNode.image = PresentationResourcesChat.chatInputMediaPanelTrendingGifsIcon(theme) + title = "Trending" case let .gifEmoji(emoji): var emoji = emoji + switch emoji { + case "😡": + title = "Angry" + case "😮": + title = "Surprised" + case "😂": + title = "Joy" + case "😘": + title = "Kiss" + case "😍": + title = "Hearts" + case "👍": + title = "Thumbs Up" + case "👎": + title = "Thumbs Down" + case "🙄": + title = "Roll-eyes" + case "😎": + title = "Cool" + case "🥳": + title = "Party" + default: + break + } if emoji == "🥳" { if #available(iOSApplicationExtension 12.1, iOS 12.1, *) { } else { @@ -165,12 +208,34 @@ final class ChatMediaInputMetaSectionItemNode: ListViewItemNode { } } self.imageNode.image = nil - self.textNode.attributedText = NSAttributedString(string: emoji, font: Font.regular(27.0), textColor: .black) + self.textNode.attributedText = NSAttributedString(string: emoji, font: Font.regular(43.0), textColor: .black) let textSize = self.textNode.updateLayout(CGSize(width: 100.0, height: 100.0)) self.textNode.frame = CGRect(origin: CGPoint(x: floor((self.textNodeContainer.bounds.width - textSize.width) / 2.0), y: floor((self.textNodeContainer.bounds.height - textSize.height) / 2.0)), size: textSize) } } + self.titleNode.attributedText = NSAttributedString(string: title, font: Font.regular(11.0), textColor: theme.chat.inputPanel.primaryTextColor) } + + self.containerNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: expandedBoundingSize) + self.scalingNode.bounds = CGRect(origin: CGPoint(), size: expandedBoundingSize) + + let boundsSize = expanded ? expandedBoundingSize : CGSize(width: boundingSize.height, height: boundingSize.height) + let expandScale: CGFloat = expanded ? 1.0 : boundingImageScale + let expandTransition: ContainedViewLayoutTransition = self.currentExpanded != expanded ? .animated(duration: 0.3, curve: .spring) : .immediate + expandTransition.updateTransformScale(node: self.scalingNode, scale: expandScale) + expandTransition.updatePosition(node: self.scalingNode, position: CGPoint(x: boundsSize.width / 2.0, y: boundsSize.height / 2.0 + (expanded ? -2.0 : 3.0))) + + expandTransition.updateAlpha(node: self.titleNode, alpha: expanded ? 1.0 : 0.0) + let titleSize = self.titleNode.updateLayout(CGSize(width: expandedBoundingSize.width - 8.0, height: expandedBoundingSize.height)) + + let titleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((expandedBoundingSize.width - titleSize.width) / 2.0), y: expandedBoundingSize.height - titleSize.height + 2.0), size: titleSize) + let displayTitleFrame = expanded ? titleFrame : CGRect(origin: CGPoint(x: titleFrame.minX, y: self.imageNode.position.y - titleFrame.size.height), size: titleFrame.size) + expandTransition.updateFrameAsPositionAndBounds(node: self.titleNode, frame: displayTitleFrame) + expandTransition.updateTransformScale(node: self.titleNode, scale: expanded ? 1.0 : 0.001) + + self.currentExpanded = expanded + + expandTransition.updateFrame(node: self.highlightNode, frame: expanded ? titleFrame.insetBy(dx: -7.0, dy: -2.0) : CGRect(origin: CGPoint(x: self.imageNode.position.x - highlightSize.width / 2.0, y: self.imageNode.position.y - highlightSize.height / 2.0), size: highlightSize)) } func updateIsHighlighted() { diff --git a/submodules/TelegramUI/Sources/ChatMediaInputNode.swift b/submodules/TelegramUI/Sources/ChatMediaInputNode.swift index 96404e4872..c514206dbb 100644 --- a/submodules/TelegramUI/Sources/ChatMediaInputNode.swift +++ b/submodules/TelegramUI/Sources/ChatMediaInputNode.swift @@ -34,6 +34,7 @@ struct ChatMediaInputPanelTransition { let deletions: [ListViewDeleteItem] let insertions: [ListViewInsertItem] let updates: [ListViewUpdateItem] + let scrollToItem: ListViewScrollToItem? } struct ChatMediaInputGridTransition { @@ -47,14 +48,14 @@ struct ChatMediaInputGridTransition { let animated: Bool } -func preparedChatMediaInputPanelEntryTransition(context: AccountContext, from fromEntries: [ChatMediaInputPanelEntry], to toEntries: [ChatMediaInputPanelEntry], inputNodeInteraction: ChatMediaInputNodeInteraction) -> ChatMediaInputPanelTransition { +func preparedChatMediaInputPanelEntryTransition(context: AccountContext, from fromEntries: [ChatMediaInputPanelEntry], to toEntries: [ChatMediaInputPanelEntry], inputNodeInteraction: ChatMediaInputNodeInteraction, scrollToItem: ListViewScrollToItem?) -> 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(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) + return ChatMediaInputPanelTransition(deletions: deletions, insertions: insertions, updates: updates, scrollToItem: scrollToItem) } func preparedChatMediaInputGridEntryTransition(account: Account, view: ItemCollectionsView, from fromEntries: [ChatMediaInputGridEntry], to toEntries: [ChatMediaInputGridEntry], update: StickerPacksCollectionUpdate, interfaceInteraction: ChatControllerInteraction, inputNodeInteraction: ChatMediaInputNodeInteraction, trendingInteraction: TrendingPaneInteraction) -> ChatMediaInputGridTransition { @@ -152,16 +153,16 @@ func preparedChatMediaInputGridEntryTransition(account: Account, view: ItemColle return ChatMediaInputGridTransition(deletions: deletions, insertions: insertions, updates: updates, updateFirstIndexInSectionOffset: firstIndexInSectionOffset, stationaryItems: stationaryItems, scrollToItem: scrollToItem, updateOpaqueState: opaqueState, animated: animated) } -func chatMediaInputPanelEntries(view: ItemCollectionsView, savedStickers: OrderedItemListView?, recentStickers: OrderedItemListView?, peerSpecificPack: PeerSpecificPackData?, canInstallPeerSpecificPack: CanInstallPeerSpecificPack, hasUnreadTrending: Bool?, theme: PresentationTheme, hasGifs: Bool = true, hasSettings: Bool = true) -> [ChatMediaInputPanelEntry] { +func chatMediaInputPanelEntries(view: ItemCollectionsView, savedStickers: OrderedItemListView?, recentStickers: OrderedItemListView?, peerSpecificPack: PeerSpecificPackData?, canInstallPeerSpecificPack: CanInstallPeerSpecificPack, hasUnreadTrending: Bool?, theme: PresentationTheme, hasGifs: Bool = true, hasSettings: Bool = true, expanded: Bool = false) -> [ChatMediaInputPanelEntry] { var entries: [ChatMediaInputPanelEntry] = [] if hasGifs { - entries.append(.recentGifs(theme)) + entries.append(.recentGifs(theme, expanded)) } if let hasUnreadTrending = hasUnreadTrending { - entries.append(.trending(hasUnreadTrending, theme)) + entries.append(.trending(hasUnreadTrending, theme, expanded)) } if let savedStickers = savedStickers, !savedStickers.items.isEmpty { - entries.append(.savedStickers(theme)) + entries.append(.savedStickers(theme, expanded)) } var savedStickerIds = Set() if let savedStickers = savedStickers, !savedStickers.items.isEmpty { @@ -182,40 +183,40 @@ func chatMediaInputPanelEntries(view: ItemCollectionsView, savedStickers: Ordere } } if found { - entries.append(.recentPacks(theme)) + entries.append(.recentPacks(theme, expanded)) } } if let peerSpecificPack = peerSpecificPack { - entries.append(.peerSpecific(theme: theme, peer: peerSpecificPack.peer)) + entries.append(.peerSpecific(theme: theme, peer: peerSpecificPack.peer, expanded: expanded)) } else if case let .available(peer, false) = canInstallPeerSpecificPack { - entries.append(.peerSpecific(theme: theme, peer: peer)) + entries.append(.peerSpecific(theme: theme, peer: peer, expanded: expanded)) } var index = 0 for (_, info, item) in view.collectionInfos { if let info = info as? StickerPackCollectionInfo, item != nil { - entries.append(.stickerPack(index: index, info: info, topItem: item as? StickerPackItem, theme: theme)) + entries.append(.stickerPack(index: index, info: info, topItem: item as? StickerPackItem, theme: theme, expanded: expanded)) index += 1 } } if peerSpecificPack == nil, case let .available(peer, true) = canInstallPeerSpecificPack { - entries.append(.peerSpecific(theme: theme, peer: peer)) + entries.append(.peerSpecific(theme: theme, peer: peer, expanded: expanded)) } if hasSettings { - entries.append(.settings(theme)) + entries.append(.settings(theme, expanded)) } return entries } -func chatMediaInputPanelGifModeEntries(theme: PresentationTheme, reactions: [String]) -> [ChatMediaInputPanelEntry] { +func chatMediaInputPanelGifModeEntries(theme: PresentationTheme, reactions: [String], expanded: Bool) -> [ChatMediaInputPanelEntry] { var entries: [ChatMediaInputPanelEntry] = [] - entries.append(.stickersMode(theme)) - entries.append(.savedGifs(theme)) - entries.append(.trendingGifs(theme)) + entries.append(.stickersMode(theme, expanded)) + entries.append(.savedGifs(theme, expanded)) + entries.append(.trendingGifs(theme, expanded)) for reaction in reactions { - entries.append(.gifEmotion(entries.count, theme, reaction)) + entries.append(.gifEmotion(entries.count, theme, reaction, expanded)) } return entries @@ -422,23 +423,26 @@ final class ChatMediaInputNode: ChatInputNode { private var inputNodeInteraction: ChatMediaInputNodeInteraction! private var trendingInteraction: TrendingPaneInteraction? - + private let collectionListPanel: ASDisplayNode private let collectionListSeparator: ASDisplayNode private let collectionListContainer: CollectionListContainerNode + private weak var peekController: PeekController? + private let disposable = MetaDisposable() private let listView: ListView private let gifListView: ListView private var searchContainerNode: PaneSearchContainerNode? private let searchContainerNodeLoadedDisposable = MetaDisposable() - + + private let paneClippingContainer: ASDisplayNode + private let panesBackgroundNode: ASDisplayNode private let stickerPane: ChatMediaInputStickerPane private var animatingStickerPaneOut = false private let gifPane: ChatMediaInputGifPane private var animatingGifPaneOut = false - //private let trendingPane: ChatMediaInputTrendingPane private var animatingTrendingPaneOut = false private var panRecognizer: UIPanGestureRecognizer? @@ -448,6 +452,15 @@ final class ChatMediaInputNode: ChatInputNode { private var currentView: ItemCollectionsView? private let dismissedPeerSpecificStickerPack = Promise() + private var panelCollapseScrollToIndex: Int? + private let panelExpandedPromise = ValuePromise(false) + private var panelExpanded: Bool = false { + didSet { + self.panelExpandedPromise.set(self.panelExpanded) + } + } + private var panelCollapseTimer: SwiftSignalKit.Timer? + var requestDisableStickerAnimations: ((Bool) -> Void)? private var validLayout: (CGFloat, CGFloat, CGFloat, CGFloat, CGFloat, CGFloat, CGFloat, CGFloat, ChatPresentationInterfaceState, DeviceMetrics, Bool)? @@ -473,18 +486,17 @@ final class ChatMediaInputNode: ChatInputNode { self.strings = strings self.fontSize = fontSize self.gifPaneIsActiveUpdated = gifPaneIsActiveUpdated + + self.paneClippingContainer = ASDisplayNode() + self.paneClippingContainer.clipsToBounds = true + + self.panesBackgroundNode = ASDisplayNode() self.themeAndStringsPromise = Promise((theme, strings)) - + self.collectionListPanel = ASDisplayNode() self.collectionListPanel.clipsToBounds = true - 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 self.collectionListSeparator.backgroundColor = theme.chat.inputMediaPanel.panelSeparatorColor @@ -493,6 +505,7 @@ final class ChatMediaInputNode: ChatInputNode { self.collectionListContainer.clipsToBounds = true self.listView = ListView() +// self.listView.clipsToBounds = false self.listView.transform = CATransform3DMakeRotation(-CGFloat(Double.pi / 2.0), 0.0, 0.0, 1.0) self.listView.scroller.panGestureRecognizer.cancelsTouchesInView = false self.listView.accessibilityPageScrolledString = { row, count in @@ -500,6 +513,7 @@ final class ChatMediaInputNode: ChatInputNode { } self.gifListView = ListView() +// self.gifListView.clipsToBounds = false self.gifListView.transform = CATransform3DMakeRotation(-CGFloat(Double.pi / 2.0), 0.0, 0.0, 1.0) self.gifListView.scroller.panGestureRecognizer.cancelsTouchesInView = false self.gifListView.accessibilityPageScrolledString = { row, count in @@ -515,7 +529,7 @@ final class ChatMediaInputNode: ChatInputNode { }, fixPaneScroll: { pane, state in fixPaneScrollImpl?(pane, state) }) - self.gifPane = ChatMediaInputGifPane(account: context.account, theme: theme, strings: strings, controllerInteraction: controllerInteraction, paneDidScroll: { pane, state, transition in + self.gifPane = ChatMediaInputGifPane(context: context, theme: theme, strings: strings, controllerInteraction: controllerInteraction, paneDidScroll: { pane, state, transition in paneDidScrollImpl?(pane, state, transition) }, fixPaneScroll: { pane, state in fixPaneScrollImpl?(pane, state) @@ -524,11 +538,8 @@ 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) + self.paneArrangement = ChatMediaInputPaneArrangement(panes: [.gifs, .stickers], currentIndex: 1, indexTransition: 0.0) super.init() @@ -543,13 +554,12 @@ final class ChatMediaInputNode: ChatInputNode { sendSticker: { fileReference, sourceNode, sourceRect in if let strongSelf = self { - return strongSelf.controllerInteraction.sendSticker(fileReference, nil, false, sourceNode, sourceRect) + return strongSelf.controllerInteraction.sendSticker(fileReference, false, false, nil, false, sourceNode, sourceRect) } else { return false } } )) - //strongSelf.setCurrentPane(.trending, transition: .animated(duration: 0.25, curve: .spring)) } else if collectionId.namespace == ChatMediaInputPanelAuxiliaryNamespace.savedStickers.rawValue { strongSelf.setCurrentPane(.stickers, transition: .animated(duration: 0.25, curve: .spring), collectionIdHint: collectionId.namespace) strongSelf.currentStickerPacksCollectionPosition = .navigate(index: nil, collectionId: collectionId) @@ -689,8 +699,10 @@ final class ChatMediaInputNode: ChatInputNode { return false } - self.backgroundColor = theme.chat.inputMediaPanel.stickersBackgroundColor.withAlphaComponent(1.0) - + self.panesBackgroundNode.backgroundColor = theme.chat.inputMediaPanel.stickersBackgroundColor.withAlphaComponent(1.0) + + self.addSubnode(self.paneClippingContainer) + self.paneClippingContainer.addSubnode(panesBackgroundNode) self.collectionListPanel.addSubnode(self.listView) self.collectionListPanel.addSubnode(self.gifListView) self.gifListView.isHidden = true @@ -760,7 +772,7 @@ final class ChatMediaInputNode: ChatInputNode { return false }) - peerSpecificPack = combineLatest(peerSpecificStickerPack(postbox: context.account.postbox, network: context.account.network, peerId: peerId), context.account.postbox.multiplePeersView([peerId]), self.dismissedPeerSpecificStickerPack.get()) + peerSpecificPack = combineLatest(context.engine.peers.peerSpecificStickerPack(peerId: peerId), context.account.postbox.multiplePeersView([peerId]), self.dismissedPeerSpecificStickerPack.get()) |> map { packData, peersView, dismissedPeerSpecificPack -> (PeerSpecificPackData?, CanInstallPeerSpecificPack) in if let peer = peersView.peers[peerId] { var canInstall: CanInstallPeerSpecificPack = .none @@ -780,17 +792,17 @@ final class ChatMediaInputNode: ChatInputNode { } let trendingInteraction = TrendingPaneInteraction(installPack: { [weak self] info in - guard let strongSelf = self, let info = info as? StickerPackCollectionInfo else { + guard let info = info as? StickerPackCollectionInfo else { return } - let _ = (loadedStickerPack(postbox: strongSelf.context.account.postbox, network: strongSelf.context.account.network, reference: .id(id: info.id.id, accessHash: info.accessHash), forceActualized: false) + let _ = (context.engine.stickers.loadedStickerPack(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) + return context.engine.stickers.addStickerPackInteractively(info: info, items: items) } case .fetching: break @@ -814,7 +826,7 @@ final class ChatMediaInputNode: ChatInputNode { 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, nil, false, sourceNode, sourceRect) + return strongSelf.controllerInteraction.sendSticker(fileReference, false, false, nil, false, sourceNode, sourceRect) } else { return false } @@ -845,8 +857,8 @@ final class ChatMediaInputNode: ChatInputNode { let previousView = Atomic(value: nil) let transitionQueue = Queue() - let transitions = combineLatest(queue: transitionQueue, itemCollectionsView, peerSpecificPack, context.account.viewTracker.featuredStickerPacks(), self.themeAndStringsPromise.get(), reactions) - |> map { viewAndUpdate, peerSpecificPack, trendingPacks, themeAndStrings, reactions -> (ItemCollectionsView, ChatMediaInputPanelTransition, ChatMediaInputPanelTransition, Bool, ChatMediaInputGridTransition, Bool) in + let transitions = combineLatest(queue: transitionQueue, itemCollectionsView, peerSpecificPack, context.account.viewTracker.featuredStickerPacks(), self.themeAndStringsPromise.get(), reactions, self.panelExpandedPromise.get()) + |> map { viewAndUpdate, peerSpecificPack, trendingPacks, themeAndStrings, reactions, panelExpanded -> (ItemCollectionsView, ChatMediaInputPanelTransition, ChatMediaInputPanelTransition, Bool, ChatMediaInputGridTransition, Bool) in let (view, viewUpdate) = viewAndUpdate let previous = previousView.swap(view) var update = viewUpdate @@ -881,8 +893,8 @@ final class ChatMediaInputNode: ChatInputNode { } } - let panelEntries = chatMediaInputPanelEntries(view: view, savedStickers: savedStickers, recentStickers: recentStickers, peerSpecificPack: peerSpecificPack.0, canInstallPeerSpecificPack: peerSpecificPack.1, hasUnreadTrending: hasUnreadTrending, theme: theme) - let gifPaneEntries = chatMediaInputPanelGifModeEntries(theme: theme, reactions: reactions) + let panelEntries = chatMediaInputPanelEntries(view: view, savedStickers: savedStickers, recentStickers: recentStickers, peerSpecificPack: peerSpecificPack.0, canInstallPeerSpecificPack: peerSpecificPack.1, hasUnreadTrending: hasUnreadTrending, theme: theme, expanded: panelExpanded) + let gifPaneEntries = chatMediaInputPanelGifModeEntries(theme: theme, reactions: reactions, expanded: panelExpanded) var gridEntries = chatMediaInputGridEntries(view: view, savedStickers: savedStickers, recentStickers: recentStickers, peerSpecificPack: peerSpecificPack.0, canInstallPeerSpecificPack: peerSpecificPack.1, strings: strings, theme: theme) if view.higher == nil { @@ -900,9 +912,9 @@ final class ChatMediaInputNode: ChatInputNode { } } } - + let (previousPanelEntries, previousGifPaneEntries, previousGridEntries) = previousEntries.swap((panelEntries, gifPaneEntries, gridEntries)) - return (view, preparedChatMediaInputPanelEntryTransition(context: context, from: previousPanelEntries, to: panelEntries, inputNodeInteraction: inputNodeInteraction), preparedChatMediaInputPanelEntryTransition(context: context, from: previousGifPaneEntries, to: gifPaneEntries, inputNodeInteraction: inputNodeInteraction), previousPanelEntries.isEmpty, preparedChatMediaInputGridEntryTransition(account: context.account, view: view, from: previousGridEntries, to: gridEntries, update: update, interfaceInteraction: controllerInteraction, inputNodeInteraction: inputNodeInteraction, trendingInteraction: trendingInteraction), previousGridEntries.isEmpty) + return (view, preparedChatMediaInputPanelEntryTransition(context: context, from: previousPanelEntries, to: panelEntries, inputNodeInteraction: inputNodeInteraction, scrollToItem: nil), preparedChatMediaInputPanelEntryTransition(context: context, from: previousGifPaneEntries, to: gifPaneEntries, inputNodeInteraction: inputNodeInteraction, scrollToItem: nil), 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 @@ -969,7 +981,6 @@ 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) @@ -982,11 +993,59 @@ final class ChatMediaInputNode: ChatInputNode { openGifContextMenuImpl = { [weak self] file, sourceNode, sourceRect, gesture, isSaved in self?.openGifContextMenu(file: file, sourceNode: sourceNode, sourceRect: sourceRect, gesture: gesture, isSaved: isSaved) } + + self.listView.beganInteractiveDragging = { [weak self] position in + if let strongSelf = self, false { + if !strongSelf.panelExpanded, let index = strongSelf.listView.itemIndexAtPoint(CGPoint(x: 0.0, y: position.y)) { + strongSelf.panelCollapseScrollToIndex = index + } + strongSelf.updateIsExpanded(true) + } + } + + self.listView.didEndScrolling = { [weak self] in + if let strongSelf = self, false { + strongSelf.setupCollapseTimer() + } + } + + self.gifListView.beganInteractiveDragging = { [weak self] position in + if let strongSelf = self, false { + if !strongSelf.panelExpanded, let index = strongSelf.gifListView.itemIndexAtPoint(CGPoint(x: 0.0, y: position.y)) { + strongSelf.panelCollapseScrollToIndex = index + } + strongSelf.updateIsExpanded(true) + } + } + + self.gifListView.didEndScrolling = { [weak self] in + if let strongSelf = self, false { + strongSelf.setupCollapseTimer() + } + } } deinit { self.disposable.dispose() self.searchContainerNodeLoadedDisposable.dispose() + self.panelCollapseTimer?.invalidate() + } + + private func updateIsExpanded(_ isExpanded: Bool) { + self.panelCollapseTimer?.invalidate() + + self.panelExpanded = isExpanded + self.updatePaneClippingContainer(size: self.paneClippingContainer.bounds.size, offset: self.currentCollectionListPanelOffset(), transition: .animated(duration: 0.3, curve: .spring)) + } + + private func setupCollapseTimer() { + self.panelCollapseTimer?.invalidate() + + let timer = SwiftSignalKit.Timer(timeout: 1.5, repeat: false, completion: { [weak self] in + self?.updateIsExpanded(false) + }, queue: Queue.mainQueue()) + self.panelCollapseTimer = timer + timer.start() } private func openGifContextMenu(file: MultiplexedVideoNodeFile, sourceNode: ASDisplayNode, sourceRect: CGRect, gesture: ContextGesture, isSaved: Bool) { @@ -1008,7 +1067,7 @@ final class ChatMediaInputNode: ChatInputNode { return } - let message = Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: PeerId(namespace: 0, id: 0), namespace: Namespaces.Message.Local, id: 0), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 0, flags: [], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: nil, text: "", attributes: [], media: [file.file.media], peers: SimpleDictionary(), associatedMessages: SimpleDictionary(), associatedMessageIds: []) + let message = Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: PeerId(0), namespace: Namespaces.Message.Local, id: 0), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 0, flags: [], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: nil, text: "", attributes: [], media: [file.file.media], peers: SimpleDictionary(), associatedMessages: SimpleDictionary(), associatedMessageIds: []) let gallery = GalleryController(context: strongSelf.context, source: .standaloneMessage(message), streamSingleVideo: true, replaceRootController: { _, _ in }, baseNavigationController: nil) @@ -1020,11 +1079,45 @@ final class ChatMediaInputNode: ChatInputNode { }, action: { _, f in f(.default) if isSaved { - let _ = self?.controllerInteraction.sendGif(file.file, sourceNode, sourceRect) + let _ = self?.controllerInteraction.sendGif(file.file, sourceNode, sourceRect, false, false) } else if let (collection, result) = file.contextResult { - let _ = self?.controllerInteraction.sendBotContextResultAsGif(collection, result, sourceNode, sourceRect) + let _ = self?.controllerInteraction.sendBotContextResultAsGif(collection, result, sourceNode, sourceRect, false) } }))) + + if let (_, _, _, _, _, _, _, _, interfaceState, _, _) = strongSelf.validLayout { + var isScheduledMessages = false + if case .scheduledMessages = interfaceState.subject { + isScheduledMessages = true + } + if !isScheduledMessages { + if case let .peer(peerId) = interfaceState.chatLocation { + if peerId != self?.context.account.peerId && peerId.namespace != Namespaces.Peer.SecretChat { + items.append(.action(ContextMenuActionItem(text: strongSelf.strings.Conversation_SendMessage_SendSilently, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Menu/SilentIcon"), color: theme.actionSheet.primaryTextColor) + }, action: { _, f in + f(.default) + if isSaved { + let _ = self?.controllerInteraction.sendGif(file.file, sourceNode, sourceRect, true, false) + } else if let (collection, result) = file.contextResult { + let _ = self?.controllerInteraction.sendBotContextResultAsGif(collection, result, sourceNode, sourceRect, true) + } + }))) + } + + if isSaved { + items.append(.action(ContextMenuActionItem(text: strongSelf.strings.Conversation_SendMessage_ScheduleMessage, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Menu/ScheduleIcon"), color: theme.actionSheet.primaryTextColor) + }, action: { _, f in + f(.default) + + let _ = self?.controllerInteraction.sendGif(file.file, sourceNode, sourceRect, false, true) + }))) + } + } + } + } + if isSaved || isGifSaved { items.append(.action(ContextMenuActionItem(text: strongSelf.strings.Conversation_ContextMenuDelete, textColor: .destructive, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.actionSheet.destructiveActionTextColor) @@ -1061,20 +1154,13 @@ final class ChatMediaInputNode: ChatInputNode { self.theme = theme self.strings = strings - 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.withAlphaComponent(1.0) + self.panesBackgroundNode.backgroundColor = theme.chat.inputMediaPanel.stickersBackgroundColor.withAlphaComponent(1.0) self.searchContainerNode?.updateThemeAndStrings(theme: theme, strings: strings) self.stickerPane.updateThemeAndStrings(theme: theme, strings: strings) self.gifPane.updateThemeAndStrings(theme: theme, strings: strings) - //self.trendingPane.updateThemeAndStrings(theme: theme, strings: strings) self.themeAndStringsPromise.set(.single((theme, strings))) } @@ -1098,16 +1184,48 @@ final class ChatMediaInputNode: ChatInputNode { |> deliverOnMainQueue |> map { isStarred -> (ASDisplayNode, PeekControllerContent)? in if let strongSelf = self { - var menuItems: [PeekControllerMenuItem] = [] - menuItems = [ - PeekControllerMenuItem(title: strongSelf.strings.StickerPack_Send, color: .accent, font: .bold, action: { node, rect in - if let strongSelf = self { - return strongSelf.controllerInteraction.sendSticker(.standalone(media: item.file), nil, false, node, rect) - } else { - return false + var menuItems: [ContextMenuItem] = [] + if let (_, _, _, _, _, _, _, _, interfaceState, _, _) = strongSelf.validLayout { + var isScheduledMessages = false + if case .scheduledMessages = interfaceState.subject { + isScheduledMessages = true + } + if !isScheduledMessages { + if case let .peer(peerId) = interfaceState.chatLocation { + if peerId != self?.context.account.peerId && peerId.namespace != Namespaces.Peer.SecretChat { + menuItems.append(.action(ContextMenuActionItem(text: strongSelf.strings.Conversation_SendMessage_SendSilently, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Menu/SilentIcon"), color: theme.actionSheet.primaryTextColor) + }, action: { _, f in + if let strongSelf = self, let peekController = strongSelf.peekController { + if let animationNode = (peekController.contentNode as? StickerPreviewPeekContentNode)?.animationNode { + let _ = strongSelf.controllerInteraction.sendSticker(.standalone(media: item.file), true, false, nil, false, animationNode, animationNode.bounds) + } else if let imageNode = (peekController.contentNode as? StickerPreviewPeekContentNode)?.imageNode { + let _ = strongSelf.controllerInteraction.sendSticker(.standalone(media: item.file), true, false, nil, false, imageNode, imageNode.bounds) + } + } + f(.default) + }))) + } + + menuItems.append(.action(ContextMenuActionItem(text: strongSelf.strings.Conversation_SendMessage_ScheduleMessage, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Menu/ScheduleIcon"), color: theme.actionSheet.primaryTextColor) + }, action: { _, f in + if let strongSelf = self, let peekController = strongSelf.peekController { + if let animationNode = (peekController.contentNode as? StickerPreviewPeekContentNode)?.animationNode { + let _ = strongSelf.controllerInteraction.sendSticker(.standalone(media: item.file), false, true, nil, false, animationNode, animationNode.bounds) + } else if let imageNode = (peekController.contentNode as? StickerPreviewPeekContentNode)?.imageNode { + let _ = strongSelf.controllerInteraction.sendSticker(.standalone(media: item.file), false, true, nil, false, imageNode, imageNode.bounds) + } + } + f(.default) + }))) } - }), - PeekControllerMenuItem(title: isStarred ? strongSelf.strings.Stickers_RemoveFromFavorites : strongSelf.strings.Stickers_AddToFavorites, color: isStarred ? .destructive : .accent, action: { _, _ in + } + } + menuItems.append( + .action(ContextMenuActionItem(text: isStarred ? strongSelf.strings.Stickers_RemoveFromFavorites : strongSelf.strings.Stickers_AddToFavorites, icon: { theme in generateTintedImage(image: isStarred ? UIImage(bundleImageName: "Chat/Context Menu/Unstar") : UIImage(bundleImageName: "Chat/Context Menu/Rate"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in + f(.default) + if let strongSelf = self { if isStarred { let _ = removeSavedSticker(postbox: strongSelf.context.account.postbox, mediaId: item.file.fileId).start() @@ -1115,9 +1233,13 @@ final class ChatMediaInputNode: ChatInputNode { let _ = addSavedSticker(postbox: strongSelf.context.account.postbox, network: strongSelf.context.account.network, file: item.file).start() } } - return true - }), - PeekControllerMenuItem(title: strongSelf.strings.StickerPack_ViewPack, color: .accent, action: { _, _ in + })) + ) + menuItems.append(.action(ContextMenuActionItem(text: strongSelf.strings.StickerPack_ViewPack, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Sticker"), color: theme.actionSheet.primaryTextColor) + }, action: { _, f in + f(.default) + if let strongSelf = self { loop: for attribute in item.file.attributes { switch attribute { @@ -1125,7 +1247,7 @@ final class ChatMediaInputNode: ChatInputNode { if let packReference = packReference { 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, nil, false, sourceNode, sourceRect) + return strongSelf.controllerInteraction.sendSticker(file, false, false, nil, false, sourceNode, sourceRect) } else { return false } @@ -1140,10 +1262,7 @@ final class ChatMediaInputNode: ChatInputNode { } } } - 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 { return nil @@ -1154,7 +1273,7 @@ final class ChatMediaInputNode: ChatInputNode { } } } else { - panes = [strongSelf.gifPane, strongSelf.stickerPane/*, strongSelf.trendingPane*/] + panes = [strongSelf.gifPane, strongSelf.stickerPane] } let panelPoint = strongSelf.view.convert(point, to: strongSelf.collectionListPanel.view) if panelPoint.y < strongSelf.collectionListPanel.frame.maxY { @@ -1182,16 +1301,49 @@ final class ChatMediaInputNode: ChatInputNode { |> deliverOnMainQueue |> map { isStarred -> (ASDisplayNode, PeekControllerContent)? in if let strongSelf = self { - var menuItems: [PeekControllerMenuItem] = [] - menuItems = [ - PeekControllerMenuItem(title: strongSelf.strings.StickerPack_Send, color: .accent, font: .bold, action: { node, rect in - if let strongSelf = self { - return strongSelf.controllerInteraction.sendSticker(.standalone(media: item.file), nil, false, node, rect) - } else { - return false + var menuItems: [ContextMenuItem] = [] + if let (_, _, _, _, _, _, _, _, interfaceState, _, _) = strongSelf.validLayout { + var isScheduledMessages = false + if case .scheduledMessages = interfaceState.subject { + isScheduledMessages = true + } + if !isScheduledMessages { + if case let .peer(peerId) = interfaceState.chatLocation { + if peerId != self?.context.account.peerId && peerId.namespace != Namespaces.Peer.SecretChat { + menuItems.append(.action(ContextMenuActionItem(text: strongSelf.strings.Conversation_SendMessage_SendSilently, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Menu/SilentIcon"), color: theme.actionSheet.primaryTextColor) + }, action: { _, f in + if let strongSelf = self, let peekController = strongSelf.peekController { + if let animationNode = (peekController.contentNode as? StickerPreviewPeekContentNode)?.animationNode { + let _ = strongSelf.controllerInteraction.sendSticker(.standalone(media: item.file), true, false, nil, false, animationNode, animationNode.bounds) + } else if let imageNode = (peekController.contentNode as? StickerPreviewPeekContentNode)?.imageNode { + let _ = strongSelf.controllerInteraction.sendSticker(.standalone(media: item.file), true, false, nil, false, imageNode, imageNode.bounds) + } + } + f(.default) + }))) + } + + menuItems.append(.action(ContextMenuActionItem(text: strongSelf.strings.Conversation_SendMessage_ScheduleMessage, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Menu/ScheduleIcon"), color: theme.actionSheet.primaryTextColor) + }, action: { _, f in + if let strongSelf = self, let peekController = strongSelf.peekController { + if let animationNode = (peekController.contentNode as? StickerPreviewPeekContentNode)?.animationNode { + let _ = strongSelf.controllerInteraction.sendSticker(.standalone(media: item.file), false, true, nil, false, animationNode, animationNode.bounds) + } else if let imageNode = (peekController.contentNode as? StickerPreviewPeekContentNode)?.imageNode { + let _ = strongSelf.controllerInteraction.sendSticker(.standalone(media: item.file), false, true, nil, false, imageNode, imageNode.bounds) + } + } + f(.default) + }))) } - }), - PeekControllerMenuItem(title: isStarred ? strongSelf.strings.Stickers_RemoveFromFavorites : strongSelf.strings.Stickers_AddToFavorites, color: isStarred ? .destructive : .accent, action: { _, _ in + } + } + + menuItems.append( + .action(ContextMenuActionItem(text: isStarred ? strongSelf.strings.Stickers_RemoveFromFavorites : strongSelf.strings.Stickers_AddToFavorites, icon: { theme in generateTintedImage(image: isStarred ? UIImage(bundleImageName: "Chat/Context Menu/Unstar") : UIImage(bundleImageName: "Chat/Context Menu/Rate"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in + f(.default) + if let strongSelf = self { if isStarred { let _ = removeSavedSticker(postbox: strongSelf.context.account.postbox, mediaId: item.file.fileId).start() @@ -1199,35 +1351,38 @@ final class ChatMediaInputNode: ChatInputNode { let _ = addSavedSticker(postbox: strongSelf.context.account.postbox, network: strongSelf.context.account.network, file: item.file).start() } } - return true - }), - PeekControllerMenuItem(title: strongSelf.strings.StickerPack_ViewPack, color: .accent, action: { _, _ in + })) + ) + menuItems.append( + .action(ContextMenuActionItem(text: strongSelf.strings.StickerPack_ViewPack, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Sticker"), color: theme.actionSheet.primaryTextColor) + }, action: { _, f in + f(.default) + if let strongSelf = self { loop: for attribute in item.file.attributes { switch attribute { - case let .Sticker(_, packReference, _): - if let packReference = packReference { - 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, nil, false, sourceNode, sourceRect) - } else { - return false - } - }) - - strongSelf.controllerInteraction.navigationController()?.view.window?.endEditing(true) - strongSelf.controllerInteraction.presentController(controller, nil) - } - break loop - default: - break + case let .Sticker(_, packReference, _): + if let packReference = packReference { + 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, false, nil, false, sourceNode, sourceRect) + } else { + return false + } + }) + + strongSelf.controllerInteraction.navigationController()?.view.window?.endEditing(true) + strongSelf.controllerInteraction.presentController(controller, nil) + } + break loop + default: + break } } } - 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 { return nil @@ -1241,13 +1396,15 @@ final class ChatMediaInputNode: ChatInputNode { return nil }, present: { [weak self] content, sourceNode in if let strongSelf = self { - let controller = PeekController(theme: PeekControllerTheme(presentationTheme: strongSelf.theme), content: content, sourceNode: { + let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } + let controller = PeekController(presentationData: presentationData, content: content, sourceNode: { return sourceNode }) controller.visibilityUpdated = { [weak self] visible in self?.requestDisableStickerAnimations?(visible) self?.simulateUpdateLayout(isVisible: !visible) } + strongSelf.peekController = controller strongSelf.controllerInteraction.presentGlobalOverlayController(controller, nil) return controller } @@ -1268,19 +1425,11 @@ 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: transition, interfaceState: interfaceState, deviceMetrics: deviceMetrics, isVisible: isVisible) self.updateAppearanceTransition(transition: transition) @@ -1297,23 +1446,7 @@ final class ChatMediaInputNode: ChatInputNode { } else if let collectionIdHint = collectionIdHint { self.setHighlightedItemCollectionId(ItemCollectionId(namespace: collectionIdHint, id: 0)) } - /*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) @@ -1326,10 +1459,6 @@ final class ChatMediaInputNode: ChatInputNode { if self.paneArrangement.panes[self.paneArrangement.currentIndex] == .gifs { self.inputNodeInteraction.highlightedItemCollectionId = collectionId } - } else if collectionId.namespace == ChatMediaInputPanelAuxiliaryNamespace.trending.rawValue { - /*if self.paneArrangement.panes[self.paneArrangement.currentIndex] == .trending { - self.inputNodeInteraction.highlightedItemCollectionId = collectionId - }*/ } else { self.inputNodeInteraction.highlightedStickerItemCollectionId = collectionId if self.paneArrangement.panes[self.paneArrangement.currentIndex] == .stickers { @@ -1370,31 +1499,56 @@ final class ChatMediaInputNode: ChatInputNode { } itemNode.updateIsHighlighted() if itemNode.currentCollectionId == collectionId { - self.listView.ensureItemNodeVisible(itemNode) + if self.panelExpanded, let targetIndex = self.listView.indexOf(itemNode: itemNode) { + self.panelCollapseScrollToIndex = targetIndex + self.updateIsExpanded(false) + } else { + self.listView.ensureItemNodeVisible(itemNode) + } ensuredNodeVisible = true } } else if let itemNode = itemNode as? ChatMediaInputMetaSectionItemNode { itemNode.updateIsHighlighted() if itemNode.currentCollectionId == collectionId { - self.listView.ensureItemNodeVisible(itemNode) + if self.panelExpanded, let targetIndex = self.listView.indexOf(itemNode: itemNode) { + self.panelCollapseScrollToIndex = targetIndex + self.updateIsExpanded(false) + } else { + self.listView.ensureItemNodeVisible(itemNode) + } ensuredNodeVisible = true } } else if let itemNode = itemNode as? ChatMediaInputRecentGifsItemNode { itemNode.updateIsHighlighted() if itemNode.currentCollectionId == collectionId { - self.listView.ensureItemNodeVisible(itemNode) + if self.panelExpanded, let targetIndex = self.listView.indexOf(itemNode: itemNode) { + self.panelCollapseScrollToIndex = targetIndex + self.updateIsExpanded(false) + } else { + self.listView.ensureItemNodeVisible(itemNode) + } ensuredNodeVisible = true } } else if let itemNode = itemNode as? ChatMediaInputTrendingItemNode { itemNode.updateIsHighlighted() if itemNode.currentCollectionId == collectionId { - self.listView.ensureItemNodeVisible(itemNode) + if self.panelExpanded, let targetIndex = self.listView.indexOf(itemNode: itemNode) { + self.panelCollapseScrollToIndex = targetIndex + self.updateIsExpanded(false) + } else { + self.listView.ensureItemNodeVisible(itemNode) + } ensuredNodeVisible = true } } else if let itemNode = itemNode as? ChatMediaInputPeerSpecificItemNode { itemNode.updateIsHighlighted() if itemNode.currentCollectionId == collectionId { - self.listView.ensureItemNodeVisible(itemNode) + if self.panelExpanded, let targetIndex = self.listView.indexOf(itemNode: itemNode) { + self.panelCollapseScrollToIndex = targetIndex + self.updateIsExpanded(false) + } else { + self.listView.ensureItemNodeVisible(itemNode) + } ensuredNodeVisible = true } } @@ -1405,7 +1559,12 @@ final class ChatMediaInputNode: ChatInputNode { let firstVisibleIndex = currentView.collectionInfos.firstIndex(where: { id, _, _ in return id == firstVisibleCollectionId }) if let targetIndex = targetIndex, let firstVisibleIndex = firstVisibleIndex { let toRight = targetIndex > firstVisibleIndex - self.listView.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [], scrollToItem: ListViewScrollToItem(index: targetIndex, position: toRight ? .bottom(0.0) : .top(0.0), animated: true, curve: .Default(duration: nil), directionHint: toRight ? .Down : .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil) + if self.panelExpanded { + self.panelCollapseScrollToIndex = targetIndex + self.updateIsExpanded(false) + } else { + self.listView.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [], scrollToItem: ListViewScrollToItem(index: targetIndex, position: toRight ? .bottom(0.0) : .top(0.0), animated: true, curve: .Default(duration: nil), directionHint: toRight ? .Down : .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil) + } } } } @@ -1417,8 +1576,6 @@ final class ChatMediaInputNode: ChatInputNode { return self.stickerPane.collectionListPanelOffset case .gifs: return self.gifPane.collectionListPanelOffset - /*case .trending: - return self.trendingPane.collectionListPanelOffset*/ } } @@ -1512,7 +1669,6 @@ final class ChatMediaInputNode: ChatInputNode { } self.stickerPane.collectionListPanelOffset = 0.0 self.gifPane.collectionListPanelOffset = 0.0 - //self.trendingPane.collectionListPanelOffset = 0.0 self.updateAppearanceTransition(transition: transition) } else { panelHeight = standardInputHeight @@ -1543,11 +1699,6 @@ final class ChatMediaInputNode: ChatInputNode { } } case .trending: - /*self.trendingPane.gridNode.forEachItemNode { itemNode in - if let itemNode = itemNode as? PaneSearchBarPlaceholderNode { - placeholderNode = itemNode - } - }*/ break } } @@ -1567,14 +1718,14 @@ final class ChatMediaInputNode: ChatInputNode { transition.updateFrame(node: self.collectionListPanel, frame: CGRect(origin: CGPoint(x: 0.0, y: collectionListPanelOffset), size: CGSize(width: width, height: 41.0))) transition.updateFrame(node: self.collectionListSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: 41.0 + collectionListPanelOffset), size: CGSize(width: width, height: separatorHeight))) - self.listView.bounds = CGRect(x: 0.0, y: 0.0, width: 41.0, height: width) + self.listView.bounds = CGRect(x: 0.0, y: 0.0, width: 41.0 + 31.0 + 20.0, height: width) transition.updatePosition(node: self.listView, position: CGPoint(x: width / 2.0, y: (41.0 - collectionListPanelOffset) / 2.0)) - self.gifListView.bounds = CGRect(x: 0.0, y: 0.0, width: 41.0, height: width) + self.gifListView.bounds = CGRect(x: 0.0, y: 0.0, width: 41.0 + 31.0 + 20.0, height: width) transition.updatePosition(node: self.gifListView, position: CGPoint(x: width / 2.0, y: (41.0 - collectionListPanelOffset) / 2.0)) 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) + let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: CGSize(width: 41.0 + 31.0 + 20.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 }) @@ -1597,11 +1748,7 @@ final class ChatMediaInputNode: ChatInputNode { case .gifs: if self.gifPane.supernode == nil { if !displaySearch { - if let searchContainerNode = self.searchContainerNode { - self.insertSubnode(self.gifPane, belowSubnode: searchContainerNode) - } else { - self.insertSubnode(self.gifPane, belowSubnode: self.collectionListContainer) - } + self.paneClippingContainer.addSubnode(self.gifPane) if self.searchContainerNode == nil { self.gifPane.frame = CGRect(origin: CGPoint(x: -width, y: 0.0), size: CGSize(width: width, height: panelHeight)) } @@ -1613,11 +1760,7 @@ final class ChatMediaInputNode: ChatInputNode { } case .stickers: if self.stickerPane.supernode == nil { - if let searchContainerNode = self.searchContainerNode { - self.insertSubnode(self.stickerPane, belowSubnode: searchContainerNode) - } else { - self.insertSubnode(self.stickerPane, belowSubnode: self.collectionListContainer) - } + self.paneClippingContainer.addSubnode(self.stickerPane) self.stickerPane.frame = CGRect(origin: CGPoint(x: width, y: 0.0), size: CGSize(width: width, height: panelHeight)) } if self.stickerPane.frame != paneFrame { @@ -1630,7 +1773,6 @@ final class ChatMediaInputNode: ChatInputNode { self.gifPane.updateLayout(size: CGSize(width: width - leftInset - rightInset, height: panelHeight), topInset: 41.0, bottomInset: bottomInset, isExpanded: isExpanded, isVisible: isVisible, deviceMetrics: deviceMetrics, transition: transition) self.trendingInteraction?.itemContext.canPlayMedia = isVisible self.stickerPane.updateLayout(size: CGSize(width: width - leftInset - rightInset, height: panelHeight), topInset: 41.0, bottomInset: bottomInset, isExpanded: isExpanded, isVisible: isVisible && visiblePanes.contains(where: { $0.0 == .stickers }), deviceMetrics: deviceMetrics, transition: transition) - //self.trendingPane.updateLayout(size: CGSize(width: width - leftInset - rightInset, height: panelHeight), topInset: 41.0, bottomInset: bottomInset, isExpanded: isExpanded, isVisible: isVisible, deviceMetrics: deviceMetrics, transition: transition) if self.gifPane.supernode != nil { if !visiblePanes.contains(where: { $0.0 == .gifs }) { @@ -1682,31 +1824,6 @@ final class ChatMediaInputNode: ChatInputNode { self.animatingStickerPaneOut = false } - /*if self.trendingPane.supernode != nil { - if !visiblePanes.contains(where: { $0.0 == .trending }) { - if case .animated = transition { - if !self.animatingTrendingPaneOut { - self.animatingTrendingPaneOut = true - var toLeft = false - if let index = self.paneArrangement.panes.firstIndex(of: .trending), index < self.paneArrangement.currentIndex { - toLeft = true - } - transition.animatePosition(node: self.trendingPane, to: CGPoint(x: (toLeft ? -width : width) + width / 2.0, y: self.trendingPane.layer.position.y), removeOnCompletion: false, completion: { [weak self] value in - if let strongSelf = self, value { - strongSelf.animatingTrendingPaneOut = false - strongSelf.trendingPane.removeFromSupernode() - } - }) - } - } else { - self.animatingTrendingPaneOut = false - self.trendingPane.removeFromSupernode() - } - } - } else { - self.animatingTrendingPaneOut = false - }*/ - if !displaySearch, let searchContainerNode = self.searchContainerNode { self.searchContainerNode = nil self.searchContainerNodeLoadedDisposable.set(nil) @@ -1729,12 +1846,6 @@ final class ChatMediaInputNode: ChatInputNode { } } case .trending: - /*self.trendingPane.gridNode.forEachItemNode { itemNode in - if let itemNode = itemNode as? PaneSearchBarPlaceholderNode { - placeholderNode = itemNode - } - } - paneIsEmpty = true*/ break } } @@ -1759,6 +1870,10 @@ final class ChatMediaInputNode: ChatInputNode { self?.gifPane.initializeIfNeeded() }) } + + self.updatePaneClippingContainer(size: CGSize(width: width, height: panelHeight), offset: self.currentCollectionListPanelOffset(), transition: transition) + + transition.updateFrame(node: self.panesBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: width, height: panelHeight))) return (standardInputHeight, max(0.0, panelHeight - standardInputHeight)) } @@ -1771,7 +1886,20 @@ final class ChatMediaInputNode: ChatInputNode { } else { options.insert(.AnimateInsertion) } - self.listView.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, updateOpaqueState: nil, completion: { [weak self] _ in + + var scrollToItem: ListViewScrollToItem? + if let targetIndex = self.panelCollapseScrollToIndex { + var position: ListViewScrollPosition + if self.panelExpanded { + position = .center(.top) + } else { + position = .top(self.listView.frame.height / 2.0 + 96.0) + } + scrollToItem = ListViewScrollToItem(index: targetIndex, position: position, animated: true, curve: .Default(duration: nil), directionHint: .Down) + self.panelCollapseScrollToIndex = nil + } + + self.listView.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, scrollToItem: scrollToItem, updateOpaqueState: nil, completion: { [weak self] _ in if let strongSelf = self { strongSelf.enqueueGridTransition(gridTransition, firstTime: gridFirstTime) if !strongSelf.didSetReady { @@ -1809,7 +1937,6 @@ final class ChatMediaInputNode: ChatInputNode { } self.searchContainerNode?.contentNode.updatePreviewing(animated: animated) - //self.trendingPane.updatePreviewing(animated: animated) } } @@ -1826,11 +1953,6 @@ final class ChatMediaInputNode: ChatInputNode { self.animatingStickerPaneOut = false self.stickerPane.removeFromSupernode() } - /*self.trendingPane.layer.removeAllAnimations() - if self.animatingTrendingPaneOut { - self.animatingTrendingPaneOut = false - self.trendingPane.removeFromSupernode() - }*/ case .changed: if let (width, leftInset, rightInset, bottomInset, standardInputHeight, inputHeight, maximumHeight, inputPanelHeight, interfaceState, deviceMetrics, isVisible) = self.validLayout { let translationX = -recognizer.translation(in: self.view).x @@ -1895,13 +2017,31 @@ final class ChatMediaInputNode: ChatInputNode { } } - let collectionListPanelOffset = self.currentCollectionListPanelOffset() + var collectionListPanelOffset = self.currentCollectionListPanelOffset() + if self.panelExpanded { + collectionListPanelOffset = 0.0 + } + + var listPanelOffset = collectionListPanelOffset * 2.0 self.updateAppearanceTransition(transition: transition) - transition.updateFrame(node: self.collectionListPanel, frame: CGRect(origin: CGPoint(x: 0.0, y: collectionListPanelOffset), size: self.collectionListPanel.bounds.size)) - transition.updateFrame(node: self.collectionListSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: 41.0 + collectionListPanelOffset), size: self.collectionListSeparator.bounds.size)) - transition.updatePosition(node: self.listView, position: CGPoint(x: self.listView.position.x, y: (41.0 - collectionListPanelOffset) / 2.0)) - transition.updatePosition(node: self.gifListView, position: CGPoint(x: self.gifListView.position.x, y: (41.0 - collectionListPanelOffset) / 2.0)) + transition.updateFrame(node: self.collectionListPanel, frame: CGRect(origin: CGPoint(x: 0.0, y: listPanelOffset), size: self.collectionListPanel.bounds.size)) + transition.updatePosition(node: self.listView, position: CGPoint(x: self.listView.position.x, y: (41.0 - listPanelOffset) / 2.0)) + transition.updatePosition(node: self.gifListView, position: CGPoint(x: self.gifListView.position.x, y: (41.0 - listPanelOffset) / 2.0)) + + self.updatePaneClippingContainer(size: self.paneClippingContainer.bounds.size, offset: collectionListPanelOffset, transition: transition) + } + + private func updatePaneClippingContainer(size: CGSize, offset: CGFloat, transition: ContainedViewLayoutTransition) { + var offset = offset + var additionalOffset: CGFloat = 0.0 + if self.panelExpanded { + offset = 0.0 + additionalOffset = 31.0 + } + transition.updateFrame(node: self.collectionListSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: offset + 41.0 + additionalOffset), size: self.collectionListSeparator.bounds.size)) + transition.updateFrame(node: self.paneClippingContainer, frame: CGRect(origin: CGPoint(x: 0.0, y: offset + 41.0 + additionalOffset), size: size)) + transition.updateSublayerTransformOffset(layer: self.paneClippingContainer.layer, offset: CGPoint(x: 0.0, y: -offset - 41.0 - additionalOffset)) } private func fixPaneScroll(pane: ChatMediaInputPane, state: ChatMediaInputPaneScrollState) { @@ -1915,14 +2055,18 @@ final class ChatMediaInputNode: ChatInputNode { } } - let collectionListPanelOffset = self.currentCollectionListPanelOffset() + var collectionListPanelOffset = self.currentCollectionListPanelOffset() + if self.panelExpanded { + collectionListPanelOffset = 0.0 + } let transition = ContainedViewLayoutTransition.animated(duration: 0.25, curve: .spring) self.updateAppearanceTransition(transition: transition) transition.updateFrame(node: self.collectionListPanel, frame: CGRect(origin: CGPoint(x: 0.0, y: collectionListPanelOffset), size: self.collectionListPanel.bounds.size)) - transition.updateFrame(node: self.collectionListSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: 41.0 + collectionListPanelOffset), size: self.collectionListSeparator.bounds.size)) transition.updatePosition(node: self.listView, position: CGPoint(x: self.listView.position.x, y: (41.0 - collectionListPanelOffset) / 2.0)) transition.updatePosition(node: self.gifListView, position: CGPoint(x: self.gifListView.position.x, y: (41.0 - collectionListPanelOffset) / 2.0)) + + self.updatePaneClippingContainer(size: self.paneClippingContainer.bounds.size, offset: collectionListPanelOffset, transition: transition) } override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { diff --git a/submodules/TelegramUI/Sources/ChatMediaInputPanelEntries.swift b/submodules/TelegramUI/Sources/ChatMediaInputPanelEntries.swift index 25336491a5..bb8b80ea07 100644 --- a/submodules/TelegramUI/Sources/ChatMediaInputPanelEntries.swift +++ b/submodules/TelegramUI/Sources/ChatMediaInputPanelEntries.swift @@ -32,18 +32,18 @@ enum ChatMediaInputPanelEntryStableId: Hashable { } enum ChatMediaInputPanelEntry: Comparable, Identifiable { - case recentGifs(PresentationTheme) - case savedStickers(PresentationTheme) - case recentPacks(PresentationTheme) - case trending(Bool, PresentationTheme) - case settings(PresentationTheme) - case peerSpecific(theme: PresentationTheme, peer: Peer) - case stickerPack(index: Int, info: StickerPackCollectionInfo, topItem: StickerPackItem?, theme: PresentationTheme) + case recentGifs(PresentationTheme, Bool) + case savedStickers(PresentationTheme, Bool) + case recentPacks(PresentationTheme, Bool) + case trending(Bool, PresentationTheme, Bool) + case settings(PresentationTheme, Bool) + case peerSpecific(theme: PresentationTheme, peer: Peer, expanded: Bool) + case stickerPack(index: Int, info: StickerPackCollectionInfo, topItem: StickerPackItem?, theme: PresentationTheme, expanded: Bool) - case stickersMode(PresentationTheme) - case savedGifs(PresentationTheme) - case trendingGifs(PresentationTheme) - case gifEmotion(Int, PresentationTheme, String) + case stickersMode(PresentationTheme, Bool) + case savedGifs(PresentationTheme, Bool) + case trendingGifs(PresentationTheme, Bool) + case gifEmotion(Int, PresentationTheme, String, Bool) var stableId: ChatMediaInputPanelEntryStableId { switch self { @@ -59,7 +59,7 @@ enum ChatMediaInputPanelEntry: Comparable, Identifiable { return .settings case .peerSpecific: return .peerSpecific - case let .stickerPack(_, info, _, _): + case let .stickerPack(_, info, _, _, _): return .stickerPack(info.id.id) case .stickersMode: return .stickersMode @@ -67,75 +67,75 @@ enum ChatMediaInputPanelEntry: Comparable, Identifiable { return .savedGifs case .trendingGifs: return .trendingGifs - case let .gifEmotion(_, _, emoji): + case let .gifEmotion(_, _, emoji, _): return .gifEmotion(emoji) } } static func ==(lhs: ChatMediaInputPanelEntry, rhs: ChatMediaInputPanelEntry) -> Bool { switch lhs { - case let .recentGifs(lhsTheme): - if case let .recentGifs(rhsTheme) = rhs, lhsTheme === rhsTheme { + case let .recentGifs(lhsTheme, lhsExpanded): + if case let .recentGifs(rhsTheme, rhsExpanded) = rhs, lhsTheme === rhsTheme, lhsExpanded == rhsExpanded { return true } else { return false } - case let .savedStickers(lhsTheme): - if case let .savedStickers(rhsTheme) = rhs, lhsTheme === rhsTheme { + case let .savedStickers(lhsTheme, lhsExpanded): + if case let .savedStickers(rhsTheme, rhsExpanded) = rhs, lhsTheme === rhsTheme, lhsExpanded == rhsExpanded { return true } else { return false } - case let .recentPacks(lhsTheme): - if case let .recentPacks(rhsTheme) = rhs, lhsTheme === rhsTheme { + case let .recentPacks(lhsTheme, lhsExpanded): + if case let .recentPacks(rhsTheme, rhsExpanded) = rhs, lhsTheme === rhsTheme, lhsExpanded == rhsExpanded { return true } else { return false } - case let .trending(lhsElevated, lhsTheme): - if case let .trending(rhsElevated, rhsTheme) = rhs, lhsTheme === rhsTheme, lhsElevated == rhsElevated { + case let .trending(lhsElevated, lhsTheme, lhsExpanded): + if case let .trending(rhsElevated, rhsTheme, rhsExpanded) = rhs, lhsTheme === rhsTheme, lhsElevated == rhsElevated, lhsExpanded == rhsExpanded { return true } else { return false } - case let .settings(lhsTheme): - if case let .settings(rhsTheme) = rhs, lhsTheme === rhsTheme { + case let .settings(lhsTheme, lhsExpanded): + if case let .settings(rhsTheme, rhsExpanded) = rhs, lhsTheme === rhsTheme, lhsExpanded == rhsExpanded { return true } else { return false } - case let .peerSpecific(lhsTheme, lhsPeer): - if case let .peerSpecific(rhsTheme, rhsPeer) = rhs, lhsTheme === rhsTheme, lhsPeer.isEqual(rhsPeer) { + case let .peerSpecific(lhsTheme, lhsPeer, lhsExpanded): + if case let .peerSpecific(rhsTheme, rhsPeer, rhsExpanded) = rhs, lhsTheme === rhsTheme, lhsPeer.isEqual(rhsPeer), lhsExpanded == rhsExpanded { return true } else { return false } - case let .stickerPack(index, info, topItem, lhsTheme): - if case let .stickerPack(rhsIndex, rhsInfo, rhsTopItem, rhsTheme) = rhs, index == rhsIndex, info == rhsInfo, topItem == rhsTopItem, lhsTheme === rhsTheme { + case let .stickerPack(index, info, topItem, lhsTheme, lhsExpanded): + if case let .stickerPack(rhsIndex, rhsInfo, rhsTopItem, rhsTheme, rhsExpanded) = rhs, index == rhsIndex, info == rhsInfo, topItem == rhsTopItem, lhsTheme === rhsTheme, lhsExpanded == rhsExpanded { return true } else { return false } - case let .stickersMode(lhsTheme): - if case let .stickersMode(rhsTheme) = rhs, lhsTheme === rhsTheme { + case let .stickersMode(lhsTheme, lhsExpanded): + if case let .stickersMode(rhsTheme, rhsExpanded) = rhs, lhsTheme === rhsTheme, lhsExpanded == rhsExpanded { return true } else { return false } - case let .savedGifs(lhsTheme): - if case let .savedGifs(rhsTheme) = rhs, lhsTheme === rhsTheme { + case let .savedGifs(lhsTheme, lhsExpanded): + if case let .savedGifs(rhsTheme, rhsExpanded) = rhs, lhsTheme === rhsTheme, lhsExpanded == rhsExpanded { return true } else { return false } - case let .trendingGifs(lhsTheme): - if case let .trendingGifs(rhsTheme) = rhs, lhsTheme === rhsTheme { + case let .trendingGifs(lhsTheme, lhsExpanded): + if case let .trendingGifs(rhsTheme, rhsExpanded) = rhs, lhsTheme === rhsTheme, lhsExpanded == rhsExpanded { return true } else { return false } - case let .gifEmotion(lhsIndex, lhsTheme, lhsEmoji): - if case let .gifEmotion(rhsIndex, rhsTheme, rhsEmoji) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsEmoji == rhsEmoji { + case let .gifEmotion(lhsIndex, lhsTheme, lhsEmoji, lhsExpanded): + if case let .gifEmotion(rhsIndex, rhsTheme, rhsEmoji, rhsExpanded) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsEmoji == rhsEmoji, lhsExpanded == rhsExpanded { return true } else { return false @@ -156,7 +156,7 @@ enum ChatMediaInputPanelEntry: Comparable, Identifiable { switch rhs { case .recentGifs, savedStickers: return false - case let .trending(elevated, _) where elevated: + case let .trending(elevated, _, _) where elevated: return false default: return true @@ -165,7 +165,7 @@ enum ChatMediaInputPanelEntry: Comparable, Identifiable { switch rhs { case .recentGifs, .savedStickers, recentPacks: return false - case let .trending(elevated, _) where elevated: + case let .trending(elevated, _, _) where elevated: return false default: return true @@ -174,16 +174,16 @@ enum ChatMediaInputPanelEntry: Comparable, Identifiable { switch rhs { case .recentGifs, .savedStickers, recentPacks, .peerSpecific: return false - case let .trending(elevated, _) where elevated: + case let .trending(elevated, _, _) where elevated: return false default: return true } - case let .stickerPack(lhsIndex, lhsInfo, _, _): + case let .stickerPack(lhsIndex, lhsInfo, _, _, _): switch rhs { case .recentGifs, .savedStickers, .recentPacks, .peerSpecific: return false - case let .trending(elevated, _): + case let .trending(elevated, _, _): if elevated { return false } else { @@ -191,7 +191,7 @@ enum ChatMediaInputPanelEntry: Comparable, Identifiable { } case .settings: return true - case let .stickerPack(rhsIndex, rhsInfo, _, _): + case let .stickerPack(rhsIndex, rhsInfo, _, _, _): if lhsIndex == rhsIndex { return lhsInfo.id.id < rhsInfo.id.id } else { @@ -200,7 +200,7 @@ enum ChatMediaInputPanelEntry: Comparable, Identifiable { default: return true } - case let .trending(elevated, _): + case let .trending(elevated, _, _): if elevated { switch rhs { case .recentGifs, .trending: @@ -231,11 +231,11 @@ enum ChatMediaInputPanelEntry: Comparable, Identifiable { default: return true } - case let .gifEmotion(lhsIndex, _, _): + case let .gifEmotion(lhsIndex, _, _, _): switch rhs { case .stickersMode, .savedGifs, .trendingGifs: return false - case let .gifEmotion(rhsIndex, _, _): + case let .gifEmotion(rhsIndex, _, _, _): return lhsIndex < rhsIndex default: return true @@ -251,53 +251,53 @@ enum ChatMediaInputPanelEntry: Comparable, Identifiable { func item(context: AccountContext, inputNodeInteraction: ChatMediaInputNodeInteraction) -> ListViewItem { switch self { - case let .recentGifs(theme): - return ChatMediaInputRecentGifsItem(inputNodeInteraction: inputNodeInteraction, theme: theme, selected: { + case let .recentGifs(theme, expanded): + return ChatMediaInputRecentGifsItem(inputNodeInteraction: inputNodeInteraction, theme: theme, expanded: expanded, selected: { let collectionId = ItemCollectionId(namespace: ChatMediaInputPanelAuxiliaryNamespace.recentGifs.rawValue, id: 0) inputNodeInteraction.navigateToCollectionId(collectionId) }) - case let .savedStickers(theme): - return ChatMediaInputMetaSectionItem(inputNodeInteraction: inputNodeInteraction, type: .savedStickers, theme: theme, selected: { + case let .savedStickers(theme, expanded): + return ChatMediaInputMetaSectionItem(inputNodeInteraction: inputNodeInteraction, type: .savedStickers, theme: theme, expanded: expanded, selected: { let collectionId = ItemCollectionId(namespace: ChatMediaInputPanelAuxiliaryNamespace.savedStickers.rawValue, id: 0) inputNodeInteraction.navigateToCollectionId(collectionId) }) - case let .recentPacks(theme): - return ChatMediaInputMetaSectionItem(inputNodeInteraction: inputNodeInteraction, type: .recentStickers, theme: theme, selected: { + case let .recentPacks(theme, expanded): + return ChatMediaInputMetaSectionItem(inputNodeInteraction: inputNodeInteraction, type: .recentStickers, theme: theme, expanded: expanded, selected: { let collectionId = ItemCollectionId(namespace: ChatMediaInputPanelAuxiliaryNamespace.recentStickers.rawValue, id: 0) inputNodeInteraction.navigateToCollectionId(collectionId) }) - case let .trending(elevated, theme): - return ChatMediaInputTrendingItem(inputNodeInteraction: inputNodeInteraction, elevated: elevated, theme: theme, selected: { + case let .trending(elevated, theme, expanded): + return ChatMediaInputTrendingItem(inputNodeInteraction: inputNodeInteraction, elevated: elevated, theme: theme, expanded: expanded, selected: { let collectionId = ItemCollectionId(namespace: ChatMediaInputPanelAuxiliaryNamespace.trending.rawValue, id: 0) inputNodeInteraction.navigateToCollectionId(collectionId) }) - case let .settings(theme): - return ChatMediaInputSettingsItem(inputNodeInteraction: inputNodeInteraction, theme: theme, selected: { + case let .settings(theme, expanded): + return ChatMediaInputSettingsItem(inputNodeInteraction: inputNodeInteraction, theme: theme, expanded: expanded, selected: { inputNodeInteraction.openSettings() }) - case let .peerSpecific(theme, peer): + case let .peerSpecific(theme, peer, expanded): let collectionId = ItemCollectionId(namespace: ChatMediaInputPanelAuxiliaryNamespace.peerSpecific.rawValue, id: 0) - return ChatMediaInputPeerSpecificItem(context: context, inputNodeInteraction: inputNodeInteraction, collectionId: collectionId, peer: peer, theme: theme, selected: { + return ChatMediaInputPeerSpecificItem(context: context, inputNodeInteraction: inputNodeInteraction, collectionId: collectionId, peer: peer, theme: theme, expanded: expanded, selected: { inputNodeInteraction.navigateToCollectionId(collectionId) }) - case let .stickerPack(index, info, topItem, theme): - return ChatMediaInputStickerPackItem(account: context.account, inputNodeInteraction: inputNodeInteraction, collectionId: info.id, collectionInfo: info, stickerPackItem: topItem, index: index, theme: theme, selected: { + case let .stickerPack(index, info, topItem, theme, expanded): + return ChatMediaInputStickerPackItem(account: context.account, inputNodeInteraction: inputNodeInteraction, collectionId: info.id, collectionInfo: info, stickerPackItem: topItem, index: index, theme: theme, expanded: expanded, selected: { inputNodeInteraction.navigateToCollectionId(info.id) }) - case let .stickersMode(theme): - return ChatMediaInputMetaSectionItem(inputNodeInteraction: inputNodeInteraction, type: .stickersMode, theme: theme, selected: { + case let .stickersMode(theme, expanded): + return ChatMediaInputMetaSectionItem(inputNodeInteraction: inputNodeInteraction, type: .stickersMode, theme: theme, expanded: expanded, selected: { inputNodeInteraction.navigateBackToStickers() }) - case let .savedGifs(theme): - return ChatMediaInputMetaSectionItem(inputNodeInteraction: inputNodeInteraction, type: .savedGifs, theme: theme, selected: { + case let .savedGifs(theme, expanded): + return ChatMediaInputMetaSectionItem(inputNodeInteraction: inputNodeInteraction, type: .savedGifs, theme: theme, expanded: expanded, selected: { inputNodeInteraction.setGifMode(.recent) }) - case let .trendingGifs(theme): - return ChatMediaInputMetaSectionItem(inputNodeInteraction: inputNodeInteraction, type: .trendingGifs, theme: theme, selected: { + case let .trendingGifs(theme, expanded): + return ChatMediaInputMetaSectionItem(inputNodeInteraction: inputNodeInteraction, type: .trendingGifs, theme: theme, expanded: expanded, selected: { inputNodeInteraction.setGifMode(.trending) }) - case let .gifEmotion(_, theme, emoji): - return ChatMediaInputMetaSectionItem(inputNodeInteraction: inputNodeInteraction, type: .gifEmoji(emoji), theme: theme, selected: { + case let .gifEmotion(_, theme, emoji, expanded): + return ChatMediaInputMetaSectionItem(inputNodeInteraction: inputNodeInteraction, type: .gifEmoji(emoji), theme: theme, expanded: expanded, selected: { inputNodeInteraction.setGifMode(.emojiSearch(emoji)) }) } diff --git a/submodules/TelegramUI/Sources/ChatMediaInputPeerSpecificItem.swift b/submodules/TelegramUI/Sources/ChatMediaInputPeerSpecificItem.swift index 834fcb7b06..c2945daf59 100644 --- a/submodules/TelegramUI/Sources/ChatMediaInputPeerSpecificItem.swift +++ b/submodules/TelegramUI/Sources/ChatMediaInputPeerSpecificItem.swift @@ -15,6 +15,7 @@ final class ChatMediaInputPeerSpecificItem: ListViewItem { let inputNodeInteraction: ChatMediaInputNodeInteraction let collectionId: ItemCollectionId let peer: Peer + let expanded: Bool let selectedItem: () -> Void let theme: PresentationTheme @@ -22,25 +23,26 @@ final class ChatMediaInputPeerSpecificItem: ListViewItem { return true } - init(context: AccountContext, inputNodeInteraction: ChatMediaInputNodeInteraction, collectionId: ItemCollectionId, peer: Peer, theme: PresentationTheme, selected: @escaping () -> Void) { + init(context: AccountContext, inputNodeInteraction: ChatMediaInputNodeInteraction, collectionId: ItemCollectionId, peer: Peer, theme: PresentationTheme, expanded: Bool, selected: @escaping () -> Void) { self.context = context self.inputNodeInteraction = inputNodeInteraction self.collectionId = collectionId self.peer = peer self.selectedItem = selected + self.expanded = expanded self.theme = theme } 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 = ChatMediaInputPeerSpecificItemNode() - node.contentSize = boundingSize + node.contentSize = self.expanded ? expandedBoundingSize : boundingSize node.insets = ChatMediaInputNode.setupPanelIconInsets(item: self, previousItem: previousItem, nextItem: nextItem) node.inputNodeInteraction = self.inputNodeInteraction Queue.mainQueue().async { completion(node, { return (nil, { _ in - node.updateItem(context: self.context, peer: self.peer, collectionId: self.collectionId, theme: self.theme) + node.updateItem(context: self.context, peer: self.peer, collectionId: self.collectionId, theme: self.theme, expanded: self.expanded) node.updateAppearanceTransition(transition: .immediate) }) }) @@ -50,8 +52,8 @@ 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(context: self.context, peer: self.peer, collectionId: self.collectionId, theme: self.theme) + completion(ListViewItemNodeLayout(contentSize: self.expanded ? expandedBoundingSize : boundingSize, insets: ChatMediaInputNode.setupPanelIconInsets(item: self, previousItem: previousItem, nextItem: nextItem)), { _ in + (node() as? ChatMediaInputPeerSpecificItemNode)?.updateItem(context: self.context, peer: self.peer, collectionId: self.collectionId, theme: self.theme, expanded: self.expanded) }) } } @@ -61,55 +63,94 @@ final class ChatMediaInputPeerSpecificItem: ListViewItem { } } -private let avatarFont = avatarPlaceholderFont(size: 12.0) -private let boundingSize = CGSize(width: 41.0, height: 41.0) -private let boundingImageSize = CGSize(width: 28.0, height: 28.0) -private let highlightSize = CGSize(width: 35.0, height: 35.0) +private let avatarFont = avatarPlaceholderFont(size: 19.0) +private let boundingSize = CGSize(width: 72.0, height: 41.0) +private let expandedBoundingSize = CGSize(width: 72.0, height: 72.0) +private let boundingImageSize = CGSize(width: 45.0, height: 45.0) +private let boundingImageScale: CGFloat = 0.625 +private let highlightSize = CGSize(width: 56.0, height: 56.0) private let verticalOffset: CGFloat = 3.0 final class ChatMediaInputPeerSpecificItemNode: ListViewItemNode { + private let containerNode: ASDisplayNode + private let scalingNode: ASDisplayNode private let avatarNode: AvatarNode private let highlightNode: ASImageNode + private let titleNode: ImmediateTextNode var inputNodeInteraction: ChatMediaInputNodeInteraction? var currentCollectionId: ItemCollectionId? + private var currentExpanded = false private var theme: PresentationTheme? private let stickerFetchedDisposable = MetaDisposable() - init() { + init() { + self.containerNode = ASDisplayNode() + self.containerNode.transform = CATransform3DMakeRotation(CGFloat.pi / 2.0, 0.0, 0.0, 1.0) + + self.scalingNode = ASDisplayNode() + self.highlightNode = ASImageNode() self.highlightNode.isLayerBacked = true self.highlightNode.isHidden = true self.avatarNode = AvatarNode(font: avatarFont) self.avatarNode.isLayerBacked = !smartInvertColorsEnabled() - self.avatarNode.transform = CATransform3DMakeRotation(CGFloat.pi / 2.0, 0.0, 0.0, 1.0) - - let imageSize = CGSize(width: 26.0, height: 26.0) - self.avatarNode.frame = CGRect(origin: CGPoint(x: floor((boundingSize.width - imageSize.width) / 2.0) + verticalOffset, y: floor((boundingSize.height - imageSize.height) / 2.0)), size: imageSize) - - self.highlightNode.frame = CGRect(origin: CGPoint(x: floor((boundingSize.width - highlightSize.width) / 2.0) + verticalOffset, y: floor((boundingSize.height - highlightSize.height) / 2.0)), size: highlightSize) + + self.titleNode = ImmediateTextNode() super.init(layerBacked: false, dynamicBounce: false) - self.addSubnode(self.highlightNode) - self.addSubnode(self.avatarNode) + self.addSubnode(self.containerNode) + self.containerNode.addSubnode(self.scalingNode) + + self.scalingNode.addSubnode(self.highlightNode) + self.scalingNode.addSubnode(self.titleNode) + self.scalingNode.addSubnode(self.avatarNode) } deinit { self.stickerFetchedDisposable.dispose() } - func updateItem(context: AccountContext, peer: Peer, collectionId: ItemCollectionId, theme: PresentationTheme) { + func updateItem(context: AccountContext, peer: Peer, collectionId: ItemCollectionId, theme: PresentationTheme, expanded: Bool) { self.currentCollectionId = collectionId if self.theme !== theme { self.theme = theme self.highlightNode.image = PresentationResourcesChat.chatMediaInputPanelHighlightedIconImage(theme) + + self.titleNode.attributedText = NSAttributedString(string: peer.compactDisplayTitle, font: Font.regular(11.0), textColor: theme.chat.inputPanel.primaryTextColor) } + self.containerNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: expandedBoundingSize) + self.scalingNode.bounds = CGRect(origin: CGPoint(), size: expandedBoundingSize) + + let boundsSize = expanded ? expandedBoundingSize : CGSize(width: boundingSize.height, height: boundingSize.height) + let imageSize = CGSize(width: 26.0 * 1.6, height: 26.0 * 1.6) + + let expandScale: CGFloat = expanded ? 1.0 : boundingImageScale + let expandTransition: ContainedViewLayoutTransition = self.currentExpanded != expanded ? .animated(duration: 0.3, curve: .spring) : .immediate + expandTransition.updateTransformScale(node: self.scalingNode, scale: expandScale) + expandTransition.updatePosition(node: self.scalingNode, position: CGPoint(x: boundsSize.width / 2.0, y: boundsSize.height / 2.0 + (expanded ? -2.0 : 3.0))) + + expandTransition.updateAlpha(node: self.titleNode, alpha: expanded ? 1.0 : 0.0) + let titleSize = self.titleNode.updateLayout(CGSize(width: expandedBoundingSize.width - 8.0, height: expandedBoundingSize.height)) + + let titleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((expandedBoundingSize.width - titleSize.width) / 2.0), y: expandedBoundingSize.height - titleSize.height + 2.0), size: titleSize) + let displayTitleFrame = expanded ? titleFrame : CGRect(origin: CGPoint(x: titleFrame.minX, y: self.avatarNode.position.y - titleFrame.size.height), size: titleFrame.size) + expandTransition.updateFrameAsPositionAndBounds(node: self.titleNode, frame: displayTitleFrame) + expandTransition.updateTransformScale(node: self.titleNode, scale: expanded ? 1.0 : 0.001) + + self.currentExpanded = expanded + + self.avatarNode.bounds = CGRect(origin: CGPoint(), size: imageSize) + self.avatarNode.position = CGPoint(x: expandedBoundingSize.height / 2.0, y: expandedBoundingSize.width / 2.0) + self.avatarNode.frame = self.avatarNode.frame + expandTransition.updateFrame(node: self.highlightNode, frame: expanded ? titleFrame.insetBy(dx: -7.0, dy: -2.0) : CGRect(origin: CGPoint(x: self.avatarNode.position.x - highlightSize.width / 2.0, y: self.avatarNode.position.y - highlightSize.height / 2.0), size: highlightSize)) + self.avatarNode.setPeer(context: context, theme: theme, peer: peer) } diff --git a/submodules/TelegramUI/Sources/ChatMediaInputRecentGifsItem.swift b/submodules/TelegramUI/Sources/ChatMediaInputRecentGifsItem.swift index d0926be1a2..76ae81d613 100644 --- a/submodules/TelegramUI/Sources/ChatMediaInputRecentGifsItem.swift +++ b/submodules/TelegramUI/Sources/ChatMediaInputRecentGifsItem.swift @@ -11,28 +11,30 @@ import TelegramPresentationData final class ChatMediaInputRecentGifsItem: ListViewItem { let inputNodeInteraction: ChatMediaInputNodeInteraction let selectedItem: () -> Void + let expanded: Bool let theme: PresentationTheme var selectable: Bool { return true } - init(inputNodeInteraction: ChatMediaInputNodeInteraction, theme: PresentationTheme, selected: @escaping () -> Void) { + init(inputNodeInteraction: ChatMediaInputNodeInteraction, theme: PresentationTheme, expanded: Bool, selected: @escaping () -> Void) { self.inputNodeInteraction = inputNodeInteraction self.selectedItem = selected self.theme = theme + self.expanded = expanded } 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 = ChatMediaInputRecentGifsItemNode() - node.contentSize = CGSize(width: 41.0, height: 41.0) + node.contentSize = self.expanded ? expandedBoundingSize : boundingSize node.insets = ChatMediaInputNode.setupPanelIconInsets(item: self, previousItem: previousItem, nextItem: nextItem) node.inputNodeInteraction = self.inputNodeInteraction - node.updateTheme(theme: self.theme) node.updateIsHighlighted() node.updateAppearanceTransition(transition: .immediate) Queue.mainQueue().async { + node.updateTheme(theme: self.theme, expanded: self.expanded) completion(node, { return (nil, { _ in }) }) @@ -42,8 +44,8 @@ final class ChatMediaInputRecentGifsItem: 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? ChatMediaInputRecentGifsItemNode)?.updateTheme(theme: self.theme) + completion(ListViewItemNodeLayout(contentSize: self.expanded ? expandedBoundingSize : boundingSize, insets: ChatMediaInputNode.setupPanelIconInsets(item: self, previousItem: previousItem, nextItem: nextItem)), { _ in + (node() as? ChatMediaInputRecentGifsItemNode)?.updateTheme(theme: self.theme, expanded: self.expanded) }) } } @@ -53,14 +55,20 @@ final class ChatMediaInputRecentGifsItem: ListViewItem { } } -private let boundingSize = CGSize(width: 41.0, height: 41.0) -private let boundingImageSize = CGSize(width: 30.0, height: 30.0) -private let highlightSize = CGSize(width: 35.0, height: 35.0) +private let boundingSize = CGSize(width: 72.0, height: 41.0) +private let expandedBoundingSize = CGSize(width: 72.0, height: 72.0) +private let boundingImageScale: CGFloat = 0.625 +private let highlightSize = CGSize(width: 56.0, height: 56.0) private let verticalOffset: CGFloat = 3.0 + UIScreenPixel final class ChatMediaInputRecentGifsItemNode: ListViewItemNode { + private let containerNode: ASDisplayNode + private let scalingNode: ASDisplayNode private let imageNode: ASImageNode private let highlightNode: ASImageNode + private let titleNode: ImmediateTextNode + + private var currentExpanded = false var currentCollectionId: ItemCollectionId? var inputNodeInteraction: ChatMediaInputNodeInteraction? @@ -68,40 +76,68 @@ final class ChatMediaInputRecentGifsItemNode: ListViewItemNode { var theme: PresentationTheme? init() { + self.containerNode = ASDisplayNode() + self.containerNode.transform = CATransform3DMakeRotation(CGFloat.pi / 2.0, 0.0, 0.0, 1.0) + + self.scalingNode = ASDisplayNode() + self.highlightNode = ASImageNode() self.highlightNode.isLayerBacked = true self.highlightNode.isHidden = true self.imageNode = ASImageNode() self.imageNode.isLayerBacked = true - self.imageNode.contentMode = .center - self.imageNode.contentsScale = UIScreenScale - self.highlightNode.frame = CGRect(origin: CGPoint(x: floor((boundingSize.width - highlightSize.width) / 2.0) + verticalOffset, y: floor((boundingSize.height - highlightSize.height) / 2.0)), size: highlightSize) - - self.imageNode.transform = CATransform3DMakeRotation(CGFloat.pi / 2.0, 0.0, 0.0, 1.0) + self.titleNode = ImmediateTextNode() super.init(layerBacked: false, dynamicBounce: false) - self.addSubnode(self.highlightNode) - self.addSubnode(self.imageNode) + self.addSubnode(self.containerNode) + self.containerNode.addSubnode(self.scalingNode) + + self.scalingNode.addSubnode(self.highlightNode) + self.scalingNode.addSubnode(self.titleNode) + self.scalingNode.addSubnode(self.imageNode) self.currentCollectionId = ItemCollectionId(namespace: ChatMediaInputPanelAuxiliaryNamespace.recentGifs.rawValue, id: 0) - - let imageSize = CGSize(width: 26.0, height: 26.0) - self.imageNode.frame = CGRect(origin: CGPoint(x: floor((boundingSize.width - imageSize.width) / 2.0) + verticalOffset, y: floor((boundingSize.height - imageSize.height) / 2.0) + UIScreenPixel), size: imageSize) } deinit { } - func updateTheme(theme: PresentationTheme) { + func updateTheme(theme: PresentationTheme, expanded: Bool) { if self.theme !== theme { self.theme = theme self.highlightNode.image = PresentationResourcesChat.chatMediaInputPanelHighlightedIconImage(theme) self.imageNode.image = PresentationResourcesChat.chatInputMediaPanelRecentGifsIconImage(theme) + + self.titleNode.attributedText = NSAttributedString(string: "GIFs", font: Font.regular(11.0), textColor: theme.chat.inputPanel.primaryTextColor) } + + let imageSize = CGSize(width: 26.0 * 1.6, height: 26.0 * 1.6) + self.imageNode.frame = CGRect(origin: CGPoint(x: floor((expandedBoundingSize.width - imageSize.width) / 2.0), y: floor((expandedBoundingSize.height - imageSize.height) / 2.0) + UIScreenPixel), size: imageSize) + + self.containerNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: expandedBoundingSize) + self.scalingNode.bounds = CGRect(origin: CGPoint(), size: expandedBoundingSize) + + let boundsSize = expanded ? expandedBoundingSize : CGSize(width: boundingSize.height, height: boundingSize.height) + let expandScale: CGFloat = expanded ? 1.0 : boundingImageScale + let expandTransition: ContainedViewLayoutTransition = self.currentExpanded != expanded ? .animated(duration: 0.3, curve: .spring) : .immediate + expandTransition.updateTransformScale(node: self.scalingNode, scale: expandScale) + expandTransition.updatePosition(node: self.scalingNode, position: CGPoint(x: boundsSize.width / 2.0, y: boundsSize.height / 2.0 + (expanded ? -2.0 : 3.0))) + + expandTransition.updateAlpha(node: self.titleNode, alpha: expanded ? 1.0 : 0.0) + let titleSize = self.titleNode.updateLayout(CGSize(width: expandedBoundingSize.width - 8.0, height: expandedBoundingSize.height)) + + let titleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((expandedBoundingSize.width - titleSize.width) / 2.0), y: expandedBoundingSize.height - titleSize.height + 2.0), size: titleSize) + let displayTitleFrame = expanded ? titleFrame : CGRect(origin: CGPoint(x: titleFrame.minX, y: self.imageNode.position.y - titleFrame.size.height), size: titleFrame.size) + expandTransition.updateFrameAsPositionAndBounds(node: self.titleNode, frame: displayTitleFrame) + expandTransition.updateTransformScale(node: self.titleNode, scale: expanded ? 1.0 : 0.001) + + self.currentExpanded = expanded + + expandTransition.updateFrame(node: self.highlightNode, frame: expanded ? titleFrame.insetBy(dx: -7.0, dy: -2.0) : CGRect(origin: CGPoint(x: self.imageNode.position.x - highlightSize.width / 2.0, y: self.imageNode.position.y - highlightSize.height / 2.0), size: highlightSize)) } func updateIsHighlighted() { diff --git a/submodules/TelegramUI/Sources/ChatMediaInputSettingsItem.swift b/submodules/TelegramUI/Sources/ChatMediaInputSettingsItem.swift index 15a69c1d51..a1e60e847a 100644 --- a/submodules/TelegramUI/Sources/ChatMediaInputSettingsItem.swift +++ b/submodules/TelegramUI/Sources/ChatMediaInputSettingsItem.swift @@ -11,27 +11,29 @@ import TelegramPresentationData final class ChatMediaInputSettingsItem: ListViewItem { let inputNodeInteraction: ChatMediaInputNodeInteraction let selectedItem: () -> Void + let expanded: Bool let theme: PresentationTheme var selectable: Bool { return true } - init(inputNodeInteraction: ChatMediaInputNodeInteraction, theme: PresentationTheme, selected: @escaping () -> Void) { + init(inputNodeInteraction: ChatMediaInputNodeInteraction, theme: PresentationTheme, expanded: Bool, selected: @escaping () -> Void) { self.inputNodeInteraction = inputNodeInteraction self.selectedItem = selected self.theme = theme + self.expanded = expanded } 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 = ChatMediaInputSettingsItemNode() - node.contentSize = CGSize(width: 41.0, height: 41.0) + node.contentSize = self.expanded ? expandedBoundingSize : boundingSize node.insets = ChatMediaInputNode.setupPanelIconInsets(item: self, previousItem: previousItem, nextItem: nextItem) node.inputNodeInteraction = self.inputNodeInteraction - node.updateTheme(theme: self.theme) node.updateAppearanceTransition(transition: .immediate) Queue.mainQueue().async { + node.updateTheme(theme: self.theme, expanded: self.expanded) completion(node, { return (nil, { _ in }) }) @@ -41,8 +43,8 @@ final class ChatMediaInputSettingsItem: 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? ChatMediaInputSettingsItemNode)?.updateTheme(theme: self.theme) + completion(ListViewItemNodeLayout(contentSize: self.expanded ? expandedBoundingSize : boundingSize, insets: ChatMediaInputNode.setupPanelIconInsets(item: self, previousItem: previousItem, nextItem: nextItem)), { _ in + (node() as? ChatMediaInputSettingsItemNode)?.updateTheme(theme: self.theme, expanded: self.expanded) }) } } @@ -52,14 +54,19 @@ final class ChatMediaInputSettingsItem: ListViewItem { } } -private let boundingSize = CGSize(width: 41.0, height: 41.0) -private let boundingImageSize = CGSize(width: 30.0, height: 30.0) -private let highlightSize = CGSize(width: 35.0, height: 35.0) +private let boundingSize = CGSize(width: 72.0, height: 41.0) +private let expandedBoundingSize = CGSize(width: 72.0, height: 72.0) +private let boundingImageScale: CGFloat = 0.625 private let verticalOffset: CGFloat = 3.0 + UIScreenPixel final class ChatMediaInputSettingsItemNode: ListViewItemNode { + private let containerNode: ASDisplayNode + private let scalingNode: ASDisplayNode private let buttonNode: HighlightableButtonNode private let imageNode: ASImageNode + private let titleNode: ImmediateTextNode + + private var currentExpanded = false var currentCollectionId: ItemCollectionId? var inputNodeInteraction: ChatMediaInputNodeInteraction? @@ -67,37 +74,59 @@ final class ChatMediaInputSettingsItemNode: ListViewItemNode { var theme: PresentationTheme? init() { + self.containerNode = ASDisplayNode() + self.containerNode.transform = CATransform3DMakeRotation(CGFloat.pi / 2.0, 0.0, 0.0, 1.0) + + self.scalingNode = ASDisplayNode() + self.buttonNode = HighlightableButtonNode() self.imageNode = ASImageNode() self.imageNode.isLayerBacked = true - self.imageNode.contentMode = .center - self.imageNode.contentsScale = UIScreenScale - - self.buttonNode.frame = CGRect(origin: CGPoint(), size: boundingSize) - - self.imageNode.transform = CATransform3DMakeRotation(CGFloat.pi / 2.0, 0.0, 0.0, 1.0) - self.imageNode.contentMode = .center - self.imageNode.contentsScale = UIScreenScale + self.titleNode = ImmediateTextNode() + super.init(layerBacked: false, dynamicBounce: false) - self.addSubnode(self.buttonNode) - self.buttonNode.addSubnode(self.imageNode) + self.addSubnode(self.containerNode) + self.containerNode.addSubnode(self.scalingNode) - let imageSize = CGSize(width: 26.0, height: 26.0) - self.imageNode.frame = CGRect(origin: CGPoint(x: floor((boundingSize.width - imageSize.width) / 2.0) + verticalOffset, y: floor((boundingSize.height - imageSize.height) / 2.0) + UIScreenPixel), size: imageSize) + self.scalingNode.addSubnode(self.buttonNode) + self.scalingNode.addSubnode(self.imageNode) } - - deinit { - } - - func updateTheme(theme: PresentationTheme) { + + func updateTheme(theme: PresentationTheme, expanded: Bool) { + let imageSize = CGSize(width: 26.0 * 1.6, height: 26.0 * 1.6) + self.imageNode.frame = CGRect(origin: CGPoint(x: floor((expandedBoundingSize.width - imageSize.width) / 2.0), y: floor((expandedBoundingSize.height - imageSize.height) / 2.0) + UIScreenPixel), size: imageSize) + if self.theme !== theme { self.theme = theme self.imageNode.image = PresentationResourcesChat.chatInputMediaPanelSettingsIconImage(theme) + + self.titleNode.attributedText = NSAttributedString(string: "Settings", font: Font.regular(11.0), textColor: theme.chat.inputPanel.primaryTextColor) } + + self.containerNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: expandedBoundingSize) + self.scalingNode.bounds = CGRect(origin: CGPoint(), size: expandedBoundingSize) + self.buttonNode.frame = self.scalingNode.bounds + + let boundsSize = expanded ? expandedBoundingSize : CGSize(width: boundingSize.height, height: boundingSize.height) + let expandScale: CGFloat = expanded ? 1.0 : boundingImageScale + let expandTransition: ContainedViewLayoutTransition = self.currentExpanded != expanded ? .animated(duration: 0.3, curve: .spring) : .immediate + expandTransition.updateTransformScale(node: self.scalingNode, scale: expandScale) + expandTransition.updatePosition(node: self.scalingNode, position: CGPoint(x: boundsSize.width / 2.0, y: boundsSize.height / 2.0 + (expanded ? -2.0 : 3.0))) + + expandTransition.updateAlpha(node: self.titleNode, alpha: expanded ? 1.0 : 0.0) + let titleSize = self.titleNode.updateLayout(CGSize(width: expandedBoundingSize.width - 8.0, height: expandedBoundingSize.height)) + + let titleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((expandedBoundingSize.width - titleSize.width) / 2.0), y: expandedBoundingSize.height - titleSize.height + 2.0), size: titleSize) + let displayTitleFrame = expanded ? titleFrame : CGRect(origin: CGPoint(x: titleFrame.minX, y: self.imageNode.position.y - titleFrame.size.height), size: titleFrame.size) + expandTransition.updateFrameAsPositionAndBounds(node: self.titleNode, frame: displayTitleFrame) + expandTransition.updateTransformScale(node: self.titleNode, scale: expanded ? 1.0 : 0.001) + + self.currentExpanded = expanded + } func updateAppearanceTransition(transition: ContainedViewLayoutTransition) { diff --git a/submodules/TelegramUI/Sources/ChatMediaInputStickerGridItem.swift b/submodules/TelegramUI/Sources/ChatMediaInputStickerGridItem.swift index e4aa206ec3..96b8f7ebc8 100644 --- a/submodules/TelegramUI/Sources/ChatMediaInputStickerGridItem.swift +++ b/submodules/TelegramUI/Sources/ChatMediaInputStickerGridItem.swift @@ -174,8 +174,8 @@ final class ChatMediaInputStickerGridItemNode: GridItemNode { private var currentState: (Account, StickerPackItem, CGSize)? private var currentSize: CGSize? let imageNode: TransformImageNode - var animationNode: AnimatedStickerNode? - private var placeholderNode: StickerShimmerEffectNode? + private(set) var animationNode: AnimatedStickerNode? + private(set) var placeholderNode: StickerShimmerEffectNode? private var didSetUpAnimationNode = false private var item: ChatMediaInputStickerGridItem? @@ -308,9 +308,13 @@ final class ChatMediaInputStickerGridItemNode: GridItemNode { if let (_, _, mediaDimensions) = self.currentState { let imageSize = mediaDimensions.aspectFitted(boundingSize) self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets()))() - self.imageNode.frame = CGRect(origin: CGPoint(x: floor((size.width - imageSize.width) / 2.0), y: (size.height - imageSize.height) / 2.0), size: imageSize) + if self.imageNode.supernode === self { + self.imageNode.frame = CGRect(origin: CGPoint(x: floor((size.width - imageSize.width) / 2.0), y: (size.height - imageSize.height) / 2.0), size: imageSize) + } if let animationNode = self.animationNode { - animationNode.frame = CGRect(origin: CGPoint(x: floor((size.width - imageSize.width) / 2.0), y: (size.height - imageSize.height) / 2.0), size: imageSize) + if animationNode.supernode === self { + animationNode.frame = CGRect(origin: CGPoint(x: floor((size.width - imageSize.width) / 2.0), y: (size.height - imageSize.height) / 2.0), size: imageSize) + } animationNode.updateLayout(size: imageSize) } } @@ -318,7 +322,9 @@ final class ChatMediaInputStickerGridItemNode: GridItemNode { if let placeholderNode = self.placeholderNode { let placeholderFrame = CGRect(origin: CGPoint(x: floor((size.width - boundingSize.width) / 2.0), y: floor((size.height - boundingSize.height) / 2.0)), size: boundingSize) - placeholderNode.frame = placeholderFrame + if placeholderNode.supernode === self { + placeholderNode.frame = placeholderFrame + } let theme = item.theme placeholderNode.update(backgroundColor: theme.chat.inputMediaPanel.stickersBackgroundColor.withAlphaComponent(1.0), foregroundColor: theme.chat.inputMediaPanel.stickersSectionTextColor.blitOver(theme.chat.inputMediaPanel.stickersBackgroundColor, alpha: 0.15), shimmeringColor: theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.3), data: item.stickerItem.file.immediateThumbnailData, size: placeholderFrame.size) @@ -336,7 +342,7 @@ final class ChatMediaInputStickerGridItemNode: GridItemNode { return } if let interfaceInteraction = self.interfaceInteraction, let (_, item, _) = self.currentState, case .ended = recognizer.state { - let _ = interfaceInteraction.sendSticker(.standalone(media: item.file), nil, false, self, self.bounds) + let _ = interfaceInteraction.sendSticker(.standalone(media: item.file), false, false, nil, false, self, self.bounds) self.imageNode.layer.animateAlpha(from: 0.5, to: 1.0, duration: 1.0) } } diff --git a/submodules/TelegramUI/Sources/ChatMediaInputStickerPackItem.swift b/submodules/TelegramUI/Sources/ChatMediaInputStickerPackItem.swift index cc1ce57342..49aea9ed90 100644 --- a/submodules/TelegramUI/Sources/ChatMediaInputStickerPackItem.swift +++ b/submodules/TelegramUI/Sources/ChatMediaInputStickerPackItem.swift @@ -22,12 +22,13 @@ final class ChatMediaInputStickerPackItem: ListViewItem { let selectedItem: () -> Void let index: Int let theme: PresentationTheme + let expanded: Bool var selectable: Bool { return true } - init(account: Account, inputNodeInteraction: ChatMediaInputNodeInteraction, collectionId: ItemCollectionId, collectionInfo: StickerPackCollectionInfo, stickerPackItem: StickerPackItem?, index: Int, theme: PresentationTheme, selected: @escaping () -> Void) { + init(account: Account, inputNodeInteraction: ChatMediaInputNodeInteraction, collectionId: ItemCollectionId, collectionInfo: StickerPackCollectionInfo, stickerPackItem: StickerPackItem?, index: Int, theme: PresentationTheme, expanded: Bool, selected: @escaping () -> Void) { self.account = account self.inputNodeInteraction = inputNodeInteraction self.collectionId = collectionId @@ -36,18 +37,19 @@ final class ChatMediaInputStickerPackItem: ListViewItem { self.selectedItem = selected self.index = index self.theme = theme + self.expanded = expanded } 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 = ChatMediaInputStickerPackItemNode() - node.contentSize = boundingSize + node.contentSize = self.expanded ? expandedBoundingSize : boundingSize node.insets = ChatMediaInputNode.setupPanelIconInsets(item: self, previousItem: previousItem, nextItem: nextItem) node.inputNodeInteraction = self.inputNodeInteraction Queue.mainQueue().async { completion(node, { return (nil, { _ in - node.updateStickerPackItem(account: self.account, info: self.collectionInfo, item: self.stickerPackItem, collectionId: self.collectionId, theme: self.theme) + node.updateStickerPackItem(account: self.account, info: self.collectionInfo, item: self.stickerPackItem, collectionId: self.collectionId, theme: self.theme, expanded: self.expanded) node.updateAppearanceTransition(transition: .immediate) }) }) @@ -57,8 +59,8 @@ final class ChatMediaInputStickerPackItem: 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? ChatMediaInputStickerPackItemNode)?.updateStickerPackItem(account: self.account, info: self.collectionInfo, item: self.stickerPackItem, collectionId: self.collectionId, theme: self.theme) + completion(ListViewItemNodeLayout(contentSize: self.expanded ? expandedBoundingSize : boundingSize, insets: ChatMediaInputNode.setupPanelIconInsets(item: self, previousItem: previousItem, nextItem: nextItem)), { _ in + (node() as? ChatMediaInputStickerPackItemNode)?.updateStickerPackItem(account: self.account, info: self.collectionInfo, item: self.stickerPackItem, collectionId: self.collectionId, theme: self.theme, expanded: self.expanded) }) } } @@ -68,20 +70,26 @@ final class ChatMediaInputStickerPackItem: ListViewItem { } } -private let boundingSize = CGSize(width: 41.0, height: 41.0) -private let boundingImageSize = CGSize(width: 28.0, height: 28.0) -private let highlightSize = CGSize(width: 35.0, height: 35.0) -private let verticalOffset: CGFloat = 3.0 +private let boundingSize = CGSize(width: 72.0, height: 41.0) +private let expandedBoundingSize = CGSize(width: 72.0, height: 72.0) +private let boundingImageSize = CGSize(width: 45.0, height: 45.0) +private let boundingImageScale: CGFloat = 0.625 +private let highlightSize = CGSize(width: 56.0, height: 56.0) +private let verticalOffset: CGFloat = -3.0 final class ChatMediaInputStickerPackItemNode: ListViewItemNode { + private let containerNode: ASDisplayNode + private let scalingNode: ASDisplayNode private let imageNode: TransformImageNode private var animatedStickerNode: AnimatedStickerNode? private var placeholderNode: StickerShimmerEffectNode? private let highlightNode: ASImageNode + private let titleNode: ImmediateTextNode var inputNodeInteraction: ChatMediaInputNodeInteraction? var currentCollectionId: ItemCollectionId? private var currentThumbnailItem: StickerPackThumbnailItem? + private var currentExpanded = false private var theme: PresentationTheme? private let stickerFetchedDisposable = MetaDisposable() @@ -102,6 +110,11 @@ final class ChatMediaInputStickerPackItemNode: ListViewItemNode { } init() { + self.containerNode = ASDisplayNode() + self.containerNode.transform = CATransform3DMakeRotation(CGFloat.pi / 2.0, 0.0, 0.0, 1.0) + + self.scalingNode = ASDisplayNode() + self.highlightNode = ASImageNode() self.highlightNode.isLayerBacked = true self.highlightNode.isHidden = true @@ -110,18 +123,19 @@ final class ChatMediaInputStickerPackItemNode: ListViewItemNode { self.imageNode.isLayerBacked = !smartInvertColorsEnabled() self.placeholderNode = StickerShimmerEffectNode() - self.placeholderNode?.transform = CATransform3DMakeRotation(CGFloat.pi / 2.0, 0.0, 0.0, 1.0) - - self.highlightNode.frame = CGRect(origin: CGPoint(x: floor((boundingSize.width - highlightSize.width) / 2.0) + verticalOffset - UIScreenPixel, y: floor((boundingSize.height - highlightSize.height) / 2.0) - UIScreenPixel), size: highlightSize) - - self.imageNode.transform = CATransform3DMakeRotation(CGFloat.pi / 2.0, 0.0, 0.0, 1.0) - + + self.titleNode = ImmediateTextNode() + super.init(layerBacked: false, dynamicBounce: false) - self.addSubnode(self.highlightNode) - self.addSubnode(self.imageNode) + self.addSubnode(self.containerNode) + self.containerNode.addSubnode(self.scalingNode) + + self.scalingNode.addSubnode(self.highlightNode) + self.scalingNode.addSubnode(self.titleNode) + self.scalingNode.addSubnode(self.imageNode) if let placeholderNode = self.placeholderNode { - self.addSubnode(placeholderNode) + self.scalingNode.addSubnode(placeholderNode) } var firstTime = true @@ -157,11 +171,13 @@ final class ChatMediaInputStickerPackItemNode: ListViewItemNode { } } - func updateStickerPackItem(account: Account, info: StickerPackCollectionInfo, item: StickerPackItem?, collectionId: ItemCollectionId, theme: PresentationTheme) { + func updateStickerPackItem(account: Account, info: StickerPackCollectionInfo, item: StickerPackItem?, collectionId: ItemCollectionId, theme: PresentationTheme, expanded: Bool) { self.currentCollectionId = collectionId + var themeUpdated = false if self.theme !== theme { self.theme = theme + themeUpdated = true self.highlightNode.image = PresentationResourcesChat.chatMediaInputPanelHighlightedIconImage(theme) } @@ -181,27 +197,31 @@ final class ChatMediaInputStickerPackItemNode: ListViewItemNode { 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, progressiveSizes: [])) + thumbnailItem = .still(TelegramMediaImageRepresentation(dimensions: dimensions, resource: resource, progressiveSizes: [], immediateThumbnailData: nil)) resourceReference = MediaResourceReference.media(media: .standalone(media: item.file), resource: resource) } } + if themeUpdated || self.titleNode.attributedText?.string != info.title { + self.titleNode.attributedText = NSAttributedString(string: info.title, font: Font.regular(11.0), textColor: theme.chat.inputPanel.primaryTextColor) + } + + let boundsSize = expanded ? expandedBoundingSize : CGSize(width: boundingSize.height, height: boundingSize.height) + var imageSize = boundingImageSize + if self.currentThumbnailItem != thumbnailItem { self.currentThumbnailItem = thumbnailItem if let thumbnailItem = thumbnailItem { switch thumbnailItem { case let .still(representation): - let imageSize = representation.dimensions.cgSize.aspectFitted(boundingImageSize) + imageSize = representation.dimensions.cgSize.aspectFitted(boundingImageSize) let imageApply = self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets())) imageApply() self.imageNode.setSignal(chatMessageStickerPackThumbnail(postbox: account.postbox, resource: representation.resource, nilIfEmpty: true)) - self.imageNode.frame = CGRect(origin: CGPoint(x: floor((boundingSize.width - imageSize.width) / 2.0) + verticalOffset, y: floor((boundingSize.height - imageSize.height) / 2.0)), size: imageSize) case let .animated(resource): - let imageSize = boundingImageSize let imageApply = self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets())) imageApply() self.imageNode.setSignal(chatMessageStickerPackThumbnail(postbox: account.postbox, resource: resource, animated: true, nilIfEmpty: true)) - self.imageNode.frame = CGRect(origin: CGPoint(x: floor((boundingSize.width - imageSize.width) / 2.0) + verticalOffset, y: floor((boundingSize.height - imageSize.height) / 2.0)), size: imageSize) let loopAnimatedStickers = self.inputNodeInteraction?.stickerSettings?.loopAnimatedStickers ?? false self.imageNode.isHidden = loopAnimatedStickers @@ -212,18 +232,14 @@ final class ChatMediaInputStickerPackItemNode: ListViewItemNode { } else { animatedStickerNode = AnimatedStickerNode() self.animatedStickerNode = animatedStickerNode - animatedStickerNode.transform = CATransform3DMakeRotation(CGFloat.pi / 2.0, 0.0, 0.0, 1.0) if let placeholderNode = self.placeholderNode { - self.insertSubnode(animatedStickerNode, belowSubnode: placeholderNode) + self.scalingNode.insertSubnode(animatedStickerNode, belowSubnode: placeholderNode) } else { - self.addSubnode(animatedStickerNode) + self.scalingNode.addSubnode(animatedStickerNode) } animatedStickerNode.setup(source: AnimatedStickerResourceSource(account: account, resource: resource), width: 80, height: 80, mode: .cached) } animatedStickerNode.visibility = self.visibilityStatus && loopAnimatedStickers - if let animatedStickerNode = self.animatedStickerNode { - animatedStickerNode.frame = CGRect(origin: CGPoint(x: floor((boundingSize.width - imageSize.width) / 2.0) + verticalOffset, y: floor((boundingSize.height - imageSize.height) / 2.0)), size: imageSize) - } } if let resourceReference = resourceReference { self.stickerFetchedDisposable.set(fetchedMediaResource(mediaBox: account.postbox.mediaBox, reference: resourceReference).start()) @@ -232,16 +248,43 @@ final class ChatMediaInputStickerPackItemNode: ListViewItemNode { if let placeholderNode = self.placeholderNode { let imageSize = boundingImageSize - let placeholderFrame = CGRect(origin: CGPoint(x: floor((boundingSize.width - imageSize.width) / 2.0) + verticalOffset, y: floor((boundingSize.height - imageSize.height) / 2.0)), size: imageSize) - placeholderNode.frame = placeholderFrame - placeholderNode.update(backgroundColor: nil, foregroundColor: theme.chat.inputMediaPanel.stickersSectionTextColor.blitOver(theme.chat.inputPanel.panelBackgroundColor, alpha: 0.4), shimmeringColor: theme.chat.inputMediaPanel.panelHighlightedIconBackgroundColor.withMultipliedAlpha(0.2), data: info.immediateThumbnailData, size: imageSize, imageSize: CGSize(width: 100.0, height: 100.0)) } self.updateIsHighlighted() } + + self.containerNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: expandedBoundingSize) + self.scalingNode.bounds = CGRect(origin: CGPoint(), size: expandedBoundingSize) + + let expandScale: CGFloat = expanded ? 1.0 : boundingImageScale + let expandTransition: ContainedViewLayoutTransition = self.currentExpanded != expanded ? .animated(duration: 0.3, curve: .spring) : .immediate + expandTransition.updateTransformScale(node: self.scalingNode, scale: expandScale) + expandTransition.updatePosition(node: self.scalingNode, position: CGPoint(x: boundsSize.width / 2.0, y: boundsSize.height / 2.0 + (expanded ? -2.0 : 3.0))) + + expandTransition.updateAlpha(node: self.titleNode, alpha: expanded ? 1.0 : 0.0) + let titleSize = self.titleNode.updateLayout(CGSize(width: expandedBoundingSize.width - 8.0, height: expandedBoundingSize.height)) + + let titleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((expandedBoundingSize.width - titleSize.width) / 2.0), y: expandedBoundingSize.height - titleSize.height + 2.0), size: titleSize) + let displayTitleFrame = expanded ? titleFrame : CGRect(origin: CGPoint(x: titleFrame.minX, y: self.imageNode.position.y - titleFrame.size.height), size: titleFrame.size) + expandTransition.updateFrameAsPositionAndBounds(node: self.titleNode, frame: displayTitleFrame) + expandTransition.updateTransformScale(node: self.titleNode, scale: expanded ? 1.0 : 0.001) + + self.currentExpanded = expanded + + self.imageNode.bounds = CGRect(origin: CGPoint(), size: imageSize) + self.imageNode.position = CGPoint(x: expandedBoundingSize.height / 2.0, y: expandedBoundingSize.width / 2.0) + if let animatedStickerNode = self.animatedStickerNode { + animatedStickerNode.frame = self.imageNode.frame + animatedStickerNode.updateLayout(size: self.imageNode.frame.size) + } + if let placeholderNode = self.placeholderNode { + placeholderNode.bounds = CGRect(origin: CGPoint(), size: boundingImageSize) + placeholderNode.position = self.imageNode.position + } + expandTransition.updateFrame(node: self.highlightNode, frame: expanded ? titleFrame.insetBy(dx: -7.0, dy: -2.0) : CGRect(origin: CGPoint(x: self.imageNode.position.x - highlightSize.width / 2.0, y: self.imageNode.position.y - highlightSize.height / 2.0), size: highlightSize)) } - + override func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) { if let placeholderNode = self.placeholderNode { placeholderNode.updateAbsoluteRect(rect, within: containerSize) diff --git a/submodules/TelegramUI/Sources/ChatMediaInputTrendingItem.swift b/submodules/TelegramUI/Sources/ChatMediaInputTrendingItem.swift index 4b25c0ab63..1c25f037f8 100644 --- a/submodules/TelegramUI/Sources/ChatMediaInputTrendingItem.swift +++ b/submodules/TelegramUI/Sources/ChatMediaInputTrendingItem.swift @@ -12,29 +12,31 @@ final class ChatMediaInputTrendingItem: ListViewItem { let inputNodeInteraction: ChatMediaInputNodeInteraction let selectedItem: () -> Void let elevated: Bool + let expanded: Bool let theme: PresentationTheme var selectable: Bool { return true } - init(inputNodeInteraction: ChatMediaInputNodeInteraction, elevated: Bool, theme: PresentationTheme, selected: @escaping () -> Void) { + init(inputNodeInteraction: ChatMediaInputNodeInteraction, elevated: Bool, theme: PresentationTheme, expanded: Bool, selected: @escaping () -> Void) { self.inputNodeInteraction = inputNodeInteraction self.elevated = elevated self.selectedItem = selected + self.expanded = expanded self.theme = theme } 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 = ChatMediaInputTrendingItemNode() - node.contentSize = CGSize(width: 41.0, height: 41.0) + node.contentSize = self.expanded ? expandedBoundingSize : boundingSize node.insets = ChatMediaInputNode.setupPanelIconInsets(item: self, previousItem: previousItem, nextItem: nextItem) node.inputNodeInteraction = self.inputNodeInteraction - node.updateTheme(elevated: self.elevated, theme: self.theme) node.updateIsHighlighted() node.updateAppearanceTransition(transition: .immediate) Queue.mainQueue().async { + node.updateTheme(elevated: self.elevated, theme: self.theme, expanded: self.expanded) completion(node, { return (nil, { _ in }) }) @@ -44,8 +46,8 @@ final class ChatMediaInputTrendingItem: 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? ChatMediaInputTrendingItemNode)?.updateTheme(elevated: self.elevated, theme: self.theme) + completion(ListViewItemNodeLayout(contentSize: self.expanded ? expandedBoundingSize : boundingSize, insets: ChatMediaInputNode.setupPanelIconInsets(item: self, previousItem: previousItem, nextItem: nextItem)), { _ in + (node() as? ChatMediaInputTrendingItemNode)?.updateTheme(elevated: self.elevated, theme: self.theme, expanded: self.expanded) }) } } @@ -55,14 +57,20 @@ final class ChatMediaInputTrendingItem: ListViewItem { } } -private let boundingSize = CGSize(width: 41.0, height: 41.0) -private let boundingImageSize = CGSize(width: 30.0, height: 30.0) -private let highlightSize = CGSize(width: 35.0, height: 35.0) +private let boundingSize = CGSize(width: 72.0, height: 41.0) +private let expandedBoundingSize = CGSize(width: 72.0, height: 72.0) +private let boundingImageScale: CGFloat = 0.625 +private let highlightSize = CGSize(width: 56.0, height: 56.0) private let verticalOffset: CGFloat = 3.0 + UIScreenPixel final class ChatMediaInputTrendingItemNode: ListViewItemNode { + private let containerNode: ASDisplayNode + private let scalingNode: ASDisplayNode private let imageNode: ASImageNode private let highlightNode: ASImageNode + private let titleNode: ImmediateTextNode + + private var currentExpanded = false var currentCollectionId: ItemCollectionId? var inputNodeInteraction: ChatMediaInputNodeInteraction? @@ -73,37 +81,43 @@ final class ChatMediaInputTrendingItemNode: ListViewItemNode { let badgeBackground: ASImageNode init() { + self.containerNode = ASDisplayNode() + self.containerNode.transform = CATransform3DMakeRotation(CGFloat.pi / 2.0, 0.0, 0.0, 1.0) + + self.scalingNode = ASDisplayNode() + self.highlightNode = ASImageNode() self.highlightNode.isLayerBacked = true self.highlightNode.isHidden = true self.imageNode = ASImageNode() self.imageNode.isLayerBacked = true - self.imageNode.contentMode = .center - self.imageNode.contentsScale = UIScreenScale self.badgeBackground = ASImageNode() self.badgeBackground.displaysAsynchronously = false self.badgeBackground.displayWithoutProcessing = true self.badgeBackground.isHidden = true - self.highlightNode.frame = CGRect(origin: CGPoint(x: floor((boundingSize.width - highlightSize.width) / 2.0) + verticalOffset, y: floor((boundingSize.height - highlightSize.height) / 2.0)), size: highlightSize) - - self.imageNode.transform = CATransform3DMakeRotation(CGFloat.pi / 2.0, 0.0, 0.0, 1.0) + self.titleNode = ImmediateTextNode() super.init(layerBacked: false, dynamicBounce: false) - self.addSubnode(self.highlightNode) - self.addSubnode(self.imageNode) - self.addSubnode(self.badgeBackground) + self.addSubnode(self.containerNode) + self.containerNode.addSubnode(self.scalingNode) + + self.scalingNode.addSubnode(self.highlightNode) + self.scalingNode.addSubnode(self.titleNode) + self.scalingNode.addSubnode(self.imageNode) + self.scalingNode.addSubnode(self.badgeBackground) self.currentCollectionId = ItemCollectionId(namespace: ChatMediaInputPanelAuxiliaryNamespace.trending.rawValue, id: 0) } - deinit { - } - - func updateTheme(elevated: Bool, theme: PresentationTheme) { + func updateTheme(elevated: Bool, theme: PresentationTheme, expanded: Bool) { + let imageSize = CGSize(width: 26.0 * 1.85, height: 26.0 * 1.85) + let imageFrame = CGRect(origin: CGPoint(x: floor((expandedBoundingSize.width - imageSize.width) / 2.0), y: floor((expandedBoundingSize.height - imageSize.height) / 2.0) + UIScreenPixel), size: imageSize) + self.imageNode.frame = imageFrame + if self.theme !== theme { self.theme = theme @@ -111,19 +125,37 @@ final class ChatMediaInputTrendingItemNode: ListViewItemNode { self.imageNode.image = PresentationResourcesChat.chatInputMediaPanelTrendingIconImage(theme) self.badgeBackground.image = generateFilledCircleImage(diameter: 10.0, color: theme.chat.inputPanel.mediaRecordingDotColor) - let imageSize = CGSize(width: 26.0, height: 26.0) - let imageFrame = CGRect(origin: CGPoint(x: floor((boundingSize.width - imageSize.width) / 2.0) + verticalOffset, y: floor((boundingSize.height - imageSize.height) / 2.0) + UIScreenPixel), size: imageSize) - self.imageNode.frame = imageFrame - if let image = self.badgeBackground.image { - self.badgeBackground.frame = CGRect(origin: CGPoint(x: imageFrame.maxX - image.size.width - 1.0, y: imageFrame.maxY - image.size.width + 1.0), size: image.size) + self.badgeBackground.frame = CGRect(origin: CGPoint(x: floor(imageFrame.maxX - image.size.width - 7.0), y: 18.0), size: image.size) } + + self.titleNode.attributedText = NSAttributedString(string: "Trending", font: Font.regular(11.0), textColor: theme.chat.inputPanel.primaryTextColor) } if self.elevated != elevated { self.elevated = elevated self.badgeBackground.isHidden = !self.elevated } + + self.containerNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: expandedBoundingSize) + self.scalingNode.bounds = CGRect(origin: CGPoint(), size: expandedBoundingSize) + + let boundsSize = expanded ? expandedBoundingSize : CGSize(width: boundingSize.height, height: boundingSize.height) + let expandScale: CGFloat = expanded ? 1.0 : boundingImageScale + let expandTransition: ContainedViewLayoutTransition = self.currentExpanded != expanded ? .animated(duration: 0.3, curve: .spring) : .immediate + expandTransition.updateTransformScale(node: self.scalingNode, scale: expandScale) + expandTransition.updatePosition(node: self.scalingNode, position: CGPoint(x: boundsSize.width / 2.0, y: boundsSize.height / 2.0 + (expanded ? -2.0 : 3.0))) + + expandTransition.updateAlpha(node: self.titleNode, alpha: expanded ? 1.0 : 0.0) + let titleSize = self.titleNode.updateLayout(CGSize(width: expandedBoundingSize.width - 8.0, height: expandedBoundingSize.height)) + + let titleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((expandedBoundingSize.width - titleSize.width) / 2.0), y: expandedBoundingSize.height - titleSize.height + 2.0), size: titleSize) + let displayTitleFrame = expanded ? titleFrame : CGRect(origin: CGPoint(x: titleFrame.minX, y: self.imageNode.position.y - titleFrame.size.height), size: titleFrame.size) + expandTransition.updateFrameAsPositionAndBounds(node: self.titleNode, frame: displayTitleFrame) + + self.currentExpanded = expanded + + expandTransition.updateFrame(node: self.highlightNode, frame: expanded ? titleFrame.insetBy(dx: -7.0, dy: -2.0) : CGRect(origin: CGPoint(x: self.imageNode.position.x - highlightSize.width / 2.0, y: self.imageNode.position.y - highlightSize.height / 2.0), size: highlightSize)) } func updateIsHighlighted() { diff --git a/submodules/TelegramUI/Sources/ChatMediaInputTrendingPane.swift b/submodules/TelegramUI/Sources/ChatMediaInputTrendingPane.swift index dd111fbed5..603f0fb294 100644 --- a/submodules/TelegramUI/Sources/ChatMediaInputTrendingPane.swift +++ b/submodules/TelegramUI/Sources/ChatMediaInputTrendingPane.swift @@ -245,23 +245,22 @@ final class ChatMediaInputTrendingPane: ChatMediaInputPane { let interaction = TrendingPaneInteraction(installPack: { [weak self] info in if let strongSelf = self, let info = info as? StickerPackCollectionInfo { - 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) + let context = strongSelf.context + var installSignal = context.engine.stickers.loadedStickerPack(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) + return preloadedStickerPackThumbnail(account: context.account, info: info, items: items) |> filter { $0 } |> ignoreValues |> then( - addStickerPackInteractively(postbox: strongSelf.context.account.postbox, info: info, items: items) + context.engine.stickers.addStickerPackInteractively(info: info, items: items) |> ignoreValues ) |> mapToSignal { _ -> Signal<(StickerPackCollectionInfo, [ItemCollectionItem]), NoError> in - return .complete() } |> then(.single((info, items))) } @@ -273,8 +272,7 @@ final class ChatMediaInputTrendingPane: ChatMediaInputPane { return .complete() } |> deliverOnMainQueue - - let context = strongSelf.context + var cancelImpl: (() -> Void)? let progressSignal = Signal { subscriber in let presentationData = context.sharedContext.currentPresentationData.with { $0 } @@ -318,7 +316,7 @@ final class ChatMediaInputTrendingPane: ChatMediaInputPane { } 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 + 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, context: strongSelf.context), elevatedLayout: false, animateInAsReplacement: animateInAsReplacement, action: { _ in return true })) })) @@ -329,7 +327,7 @@ final class ChatMediaInputTrendingPane: ChatMediaInputPane { 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, nil, false, sourceNode, sourceRect) + return strongSelf.controllerInteraction.sendSticker(fileReference, false, false, nil, false, sourceNode, sourceRect) } else { return false } @@ -340,6 +338,7 @@ final class ChatMediaInputTrendingPane: ChatMediaInputPane { openSearch: { [weak self] in self?.inputNodeInteraction?.toggleSearch(true, .trending, "") }) + interaction.itemContext.canPlayMedia = true let isPane = self.isPane let previousEntries = Atomic<[TrendingPaneEntry]?>(value: nil) diff --git a/submodules/TelegramUI/Sources/ChatMessageActionButtonsNode.swift b/submodules/TelegramUI/Sources/ChatMessageActionButtonsNode.swift index ab5189f89b..6bc1c9f14f 100644 --- a/submodules/TelegramUI/Sources/ChatMessageActionButtonsNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageActionButtonsNode.swift @@ -11,7 +11,8 @@ import AccountContext private let titleFont = Font.medium(16.0) private final class ChatMessageActionButtonNode: ASDisplayNode { - private let backgroundNode: ASImageNode + private let backgroundBlurNode: NavigationBackgroundNode + private let backgroundMaskNode: ASImageNode private var titleNode: TextNode? private var iconNode: ASImageNode? private var buttonView: HighlightTrackingButton? @@ -25,20 +26,21 @@ private final class ChatMessageActionButtonNode: ASDisplayNode { private let accessibilityArea: AccessibilityAreaNode override init() { - self.backgroundNode = ASImageNode() - self.backgroundNode.displayWithoutProcessing = true - self.backgroundNode.displaysAsynchronously = false - self.backgroundNode.isLayerBacked = true - self.backgroundNode.alpha = 1.0 - self.backgroundNode.isUserInteractionEnabled = false + self.backgroundBlurNode = NavigationBackgroundNode(color: .clear) + self.backgroundBlurNode.isUserInteractionEnabled = false + + self.backgroundMaskNode = ASImageNode() + self.backgroundMaskNode.isUserInteractionEnabled = false self.accessibilityArea = AccessibilityAreaNode() self.accessibilityArea.accessibilityTraits = .button super.init() - self.addSubnode(self.backgroundNode) + self.addSubnode(self.backgroundBlurNode) self.addSubnode(self.accessibilityArea) + + //self.backgroundBlurNode.view.mask = backgroundMaskNode.view self.accessibilityArea.activate = { [weak self] in self?.buttonPressed() @@ -57,11 +59,11 @@ private final class ChatMessageActionButtonNode: ASDisplayNode { buttonView.highligthedChanged = { [weak self] highlighted in if let strongSelf = self { if highlighted { - strongSelf.backgroundNode.layer.removeAnimation(forKey: "opacity") - strongSelf.backgroundNode.alpha = 0.55 + strongSelf.backgroundBlurNode.layer.removeAnimation(forKey: "opacity") + strongSelf.backgroundBlurNode.alpha = 0.55 } else { - strongSelf.backgroundNode.alpha = 1.0 - strongSelf.backgroundNode.layer.animateAlpha(from: 0.55, to: 1.0, duration: 0.2) + strongSelf.backgroundBlurNode.alpha = 1.0 + strongSelf.backgroundBlurNode.layer.animateAlpha(from: 0.55, to: 1.0, duration: 0.2) } } } @@ -98,11 +100,13 @@ private final class ChatMessageActionButtonNode: ASDisplayNode { case .url, .urlAuth: iconImage = incoming ? graphics.chatBubbleActionButtonIncomingLinkIconImage : graphics.chatBubbleActionButtonOutgoingLinkIconImage case .requestPhone: - iconImage = incoming ? graphics.chatBubbleActionButtonIncomingPhoneIconImage : graphics.chatBubbleActionButtonOutgoingLinkIconImage + iconImage = incoming ? graphics.chatBubbleActionButtonIncomingPhoneIconImage : graphics.chatBubbleActionButtonOutgoingPhoneIconImage case .requestMap: - iconImage = incoming ? graphics.chatBubbleActionButtonIncomingLocationIconImage : graphics.chatBubbleActionButtonOutgoingLinkIconImage + iconImage = incoming ? graphics.chatBubbleActionButtonIncomingLocationIconImage : graphics.chatBubbleActionButtonOutgoingLocationIconImage case .switchInline: - iconImage = incoming ? graphics.chatBubbleActionButtonIncomingShareIconImage : graphics.chatBubbleActionButtonOutgoingLinkIconImage + iconImage = incoming ? graphics.chatBubbleActionButtonIncomingShareIconImage : graphics.chatBubbleActionButtonOutgoingShareIconImage + case .payment: + iconImage = incoming ? graphics.chatBubbleActionButtonIncomingPaymentIconImage : graphics.chatBubbleActionButtonOutgoingPaymentIconImage default: iconImage = nil } @@ -123,17 +127,17 @@ private final class ChatMessageActionButtonNode: ASDisplayNode { let messageTheme = incoming ? theme.theme.chat.message.incoming : theme.theme.chat.message.outgoing let (titleSize, titleApply) = titleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: title, font: titleFont, textColor: bubbleVariableColor(variableColor: messageTheme.actionButtonsTextColor, wallpaper: theme.wallpaper)), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: max(44.0, constrainedWidth - minimumSideInset - minimumSideInset), height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets(top: 1.0, left: 0.0, bottom: 1.0, right: 0.0))) - - let backgroundImage: UIImage? + + let backgroundMaskImage: UIImage? switch position { - case .middle: - backgroundImage = incoming ? graphics.chatBubbleActionButtonIncomingMiddleImage : graphics.chatBubbleActionButtonOutgoingMiddleImage - case .bottomLeft: - backgroundImage = incoming ? graphics.chatBubbleActionButtonIncomingBottomLeftImage : graphics.chatBubbleActionButtonOutgoingBottomLeftImage - case .bottomRight: - backgroundImage = incoming ? graphics.chatBubbleActionButtonIncomingBottomRightImage : graphics.chatBubbleActionButtonOutgoingBottomRightImage - case .bottomSingle: - backgroundImage = incoming ? graphics.chatBubbleActionButtonIncomingBottomSingleImage : graphics.chatBubbleActionButtonOutgoingBottomSingleImage + case .middle: + backgroundMaskImage = graphics.chatBubbleActionButtonMiddleMaskImage + case .bottomLeft: + backgroundMaskImage = graphics.chatBubbleActionButtonBottomLeftMaskImage + case .bottomRight: + backgroundMaskImage = graphics.chatBubbleActionButtonBottomRightMaskImage + case .bottomSingle: + backgroundMaskImage = graphics.chatBubbleActionButtonBottomSingleMaskImage } return (titleSize.size.width + sideInset + sideInset, { width in @@ -154,8 +158,12 @@ private final class ChatMessageActionButtonNode: ASDisplayNode { node.longTapRecognizer?.isEnabled = false } - node.backgroundNode.image = backgroundImage - node.backgroundNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: max(0.0, width), height: 42.0)) + node.backgroundMaskNode.image = backgroundMaskImage + node.backgroundMaskNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: max(0.0, width), height: 42.0)) + + node.backgroundBlurNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: max(0.0, width), height: 42.0)) + node.backgroundBlurNode.update(size: node.backgroundBlurNode.bounds.size, cornerRadius: bubbleCorners.auxiliaryRadius, transition: .immediate) + node.backgroundBlurNode.updateColor(color: selectDateFillStaticColor(theme: theme.theme, wallpaper: theme.wallpaper), enableBlur: dateFillNeedsBlur(theme: theme.theme, wallpaper: theme.wallpaper), transition: .immediate) if iconImage != nil { if node.iconNode == nil { diff --git a/submodules/TelegramUI/Sources/ChatMessageActionItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageActionItemNode.swift index 4eb1e5df7a..1c098b049b 100644 --- a/submodules/TelegramUI/Sources/ChatMessageActionItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageActionItemNode.swift @@ -17,14 +17,17 @@ import TelegramStringFormatting import UniversalMediaPlayer import TelegramUniversalVideoContent import GalleryUI +import WallpaperBackgroundNode -private func attributedServiceMessageString(theme: ChatPresentationThemeData, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, message: Message, accountPeerId: PeerId) -> NSAttributedString? { - return universalServiceMessageString(presentationData: (theme.theme, theme.wallpaper), strings: strings, nameDisplayOrder: nameDisplayOrder, message: message, accountPeerId: accountPeerId, forChatList: false) +private func attributedServiceMessageString(theme: ChatPresentationThemeData, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, dateTimeFormat: PresentationDateTimeFormat, message: Message, accountPeerId: PeerId) -> NSAttributedString? { + return universalServiceMessageString(presentationData: (theme.theme, theme.wallpaper), strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: dateTimeFormat, message: message, accountPeerId: accountPeerId, forChatList: false) } class ChatMessageActionBubbleContentNode: ChatMessageBubbleContentNode { let labelNode: TextNode - let filledBackgroundNode: LinkHighlightingNode + var backgroundNode: WallpaperBackgroundNode.BubbleBackgroundNode? + var backgroundColorNode: ASDisplayNode + let backgroundMaskNode: ASImageNode var linkHighlightingNode: LinkHighlightingNode? private let mediaBackgroundNode: ASImageNode @@ -33,21 +36,24 @@ class ChatMessageActionBubbleContentNode: ChatMessageBubbleContentNode { private var videoContent: NativeVideoContent? private var videoStartTimestamp: Double? private let fetchDisposable = MetaDisposable() + + private var cachedMaskBackgroundImage: (CGPoint, UIImage, [CGRect])? + private var absoluteRect: (CGRect, CGSize)? required init() { self.labelNode = TextNode() self.labelNode.isUserInteractionEnabled = false self.labelNode.displaysAsynchronously = false - - self.filledBackgroundNode = LinkHighlightingNode(color: .clear) + + self.backgroundColorNode = ASDisplayNode() + self.backgroundMaskNode = ASImageNode() self.mediaBackgroundNode = ASImageNode() self.mediaBackgroundNode.displaysAsynchronously = false self.mediaBackgroundNode.displayWithoutProcessing = true super.init() - - self.addSubnode(self.filledBackgroundNode) + self.addSubnode(self.labelNode) } @@ -124,7 +130,8 @@ class ChatMessageActionBubbleContentNode: ChatMessageBubbleContentNode { override func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize) -> (ChatMessageBubbleContentProperties, unboundSize: CGSize?, maxWidth: CGFloat, layout: (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool) -> Void))) { let makeLabelLayout = TextNode.asyncLayout(self.labelNode) - let backgroundLayout = self.filledBackgroundNode.asyncLayout() + + let cachedMaskBackgroundImage = self.cachedMaskBackgroundImage return { item, layoutConstants, _, _, _ in let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: true, headerSpacing: 0.0, hidesBackground: .always, forceFullCorners: false, forceAlignment: .center) @@ -132,7 +139,7 @@ class ChatMessageActionBubbleContentNode: ChatMessageBubbleContentNode { let backgroundImage = PresentationResourcesChat.chatActionPhotoBackgroundImage(item.presentationData.theme.theme, wallpaper: !item.presentationData.theme.wallpaper.isEmpty) return (contentProperties, nil, CGFloat.greatestFiniteMagnitude, { constrainedSize, position in - let attributedString = attributedServiceMessageString(theme: item.presentationData.theme, strings: item.presentationData.strings, nameDisplayOrder: item.presentationData.nameDisplayOrder, message: item.message, accountPeerId: item.context.account.peerId) + let attributedString = attributedServiceMessageString(theme: item.presentationData.theme, strings: item.presentationData.strings, nameDisplayOrder: item.presentationData.nameDisplayOrder, dateTimeFormat: item.presentationData.dateTimeFormat, message: item.message, accountPeerId: item.context.account.peerId) var image: TelegramMediaImage? for media in item.message.media { @@ -172,7 +179,15 @@ class ChatMessageActionBubbleContentNode: ChatMessageBubbleContentNode { } let serviceColor = serviceMessageColorComponents(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper) - let backgroundApply = backgroundLayout(serviceColor.fill, labelRects, 10.0, 10.0, 0.0) + + let backgroundMaskImage: (CGPoint, UIImage)? + var backgroundMaskUpdated = false + if let (currentOffset, currentImage, currentRects) = cachedMaskBackgroundImage, currentRects == labelRects { + backgroundMaskImage = (currentOffset, currentImage) + } else { + backgroundMaskImage = LinkHighlightingNode.generateImage(color: .black, inset: 0.0, innerRadius: 10.0, outerRadius: 10.0, rects: labelRects) + backgroundMaskUpdated = true + } var backgroundSize = CGSize(width: labelLayout.size.width + 8.0 + 8.0, height: labelLayout.size.height + 4.0) @@ -260,17 +275,76 @@ class ChatMessageActionBubbleContentNode: ChatMessageBubbleContentNode { } let _ = apply() - let _ = backgroundApply() let labelFrame = CGRect(origin: CGPoint(x: 8.0, y: image != nil ? 2 : floorToScreenPixels((backgroundSize.height - labelLayout.size.height) / 2.0) - 1.0), size: labelLayout.size) strongSelf.labelNode.frame = labelFrame - strongSelf.filledBackgroundNode.frame = labelFrame.offsetBy(dx: 0.0, dy: -11.0) + strongSelf.backgroundColorNode.backgroundColor = selectDateFillStaticColor(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper) + + let baseBackgroundFrame = labelFrame.offsetBy(dx: 0.0, dy: -11.0) + + if let (offset, image) = backgroundMaskImage { + if strongSelf.backgroundNode == nil { + if let backgroundNode = item.controllerInteraction.presentationContext.backgroundNode?.makeBubbleBackground(for: .free) { + strongSelf.backgroundNode = backgroundNode + backgroundNode.addSubnode(strongSelf.backgroundColorNode) + strongSelf.insertSubnode(backgroundNode, at: 0) + } + } + + if backgroundMaskUpdated, let backgroundNode = strongSelf.backgroundNode { + if labelRects.count == 1 { + backgroundNode.clipsToBounds = true + backgroundNode.cornerRadius = labelRects[0].height / 2.0 + backgroundNode.view.mask = nil + } else { + backgroundNode.clipsToBounds = false + backgroundNode.cornerRadius = 0.0 + backgroundNode.view.mask = strongSelf.backgroundMaskNode.view + } + } + + if let backgroundNode = strongSelf.backgroundNode { + backgroundNode.frame = CGRect(origin: CGPoint(x: baseBackgroundFrame.minX + offset.x, y: baseBackgroundFrame.minY + offset.y), size: image.size) + if let (rect, size) = strongSelf.absoluteRect { + strongSelf.updateAbsoluteRect(rect, within: size) + } + } + strongSelf.backgroundMaskNode.image = image + strongSelf.backgroundMaskNode.frame = CGRect(origin: CGPoint(), size: image.size) + + strongSelf.backgroundColorNode.frame = CGRect(origin: CGPoint(), size: image.size) + + strongSelf.cachedMaskBackgroundImage = (offset, image, labelRects) + } } }) }) }) } } + + override func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) { + self.absoluteRect = (rect, containerSize) + + if let backgroundNode = self.backgroundNode { + var backgroundFrame = backgroundNode.frame + backgroundFrame.origin.x += rect.minX + backgroundFrame.origin.y += rect.minY + backgroundNode.update(rect: backgroundFrame, within: containerSize) + } + } + + override func applyAbsoluteOffset(value: CGPoint, animationCurve: ContainedViewLayoutTransitionCurve, duration: Double) { + if let backgroundNode = self.backgroundNode { + backgroundNode.offset(value: value, animationCurve: animationCurve, duration: duration) + } + } + + override func applyAbsoluteOffsetSpring(value: CGFloat, duration: Double, damping: CGFloat) { + if let backgroundNode = self.backgroundNode { + backgroundNode.offsetSpring(value: value, duration: duration, damping: damping) + } + } override func updateTouchesAtPoint(_ point: CGPoint?) { if let item = self.item { @@ -347,7 +421,7 @@ class ChatMessageActionBubbleContentNode: ChatMessageBubbleContentNode { return .openMessage } - if self.filledBackgroundNode.frame.contains(point.offsetBy(dx: 0.0, dy: -10.0)) { + if let backgroundNode = self.backgroundNode, backgroundNode.frame.contains(point.offsetBy(dx: 0.0, dy: -10.0)) { return .openMessage } else { return .none diff --git a/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift index fe7c0e93fa..9735da77e8 100644 --- a/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift @@ -22,6 +22,7 @@ import ManagedAnimationNode import SlotMachineAnimationNode import UniversalMediaPlayer import ShimmerEffect +import WallpaperBackgroundNode private let nameFont = Font.medium(14.0) private let inlineBotPrefixFont = Font.regular(14.0) @@ -29,19 +30,31 @@ private let inlineBotNameFont = nameFont protocol GenericAnimatedStickerNode: ASDisplayNode { func setOverlayColor(_ color: UIColor?, animated: Bool) + + var currentFrameIndex: Int { get } + func setFrameIndex(_ frameIndex: Int) } extension AnimatedStickerNode: GenericAnimatedStickerNode { - + func setFrameIndex(_ frameIndex: Int) { + self.stop() + self.play(fromIndex: frameIndex) + } } extension SlotMachineAnimationNode: GenericAnimatedStickerNode { - + var currentFrameIndex: Int { + return 0 + } + + func setFrameIndex(_ frameIndex: Int) { + } } class ChatMessageShareButton: HighlightableButtonNode { - private let backgroundNode: ASImageNode + private let backgroundNode: NavigationBackgroundNode private let iconNode: ASImageNode + private var iconOffset = CGPoint() private var theme: PresentationTheme? private var isReplies: Bool = false @@ -49,7 +62,7 @@ class ChatMessageShareButton: HighlightableButtonNode { private var textNode: ImmediateTextNode? init() { - self.backgroundNode = ASImageNode() + self.backgroundNode = NavigationBackgroundNode(color: .clear) self.iconNode = ASImageNode() super.init(pointerStyle: nil) @@ -62,7 +75,7 @@ class ChatMessageShareButton: HighlightableButtonNode { fatalError("init(coder:) has not been implemented") } - func update(presentationData: ChatPresentationData, chatLocation: ChatLocation, subject: ChatControllerSubject?, message: Message, account: Account) -> CGSize { + func update(presentationData: ChatPresentationData, chatLocation: ChatLocation, subject: ChatControllerSubject?, message: Message, account: Account, disableComments: Bool = false) -> CGSize { var isReplies = false var replyCount = 0 if let channel = message.peers[message.id.peerId] as? TelegramChannel, case .broadcast = channel.info { @@ -78,26 +91,30 @@ class ChatMessageShareButton: HighlightableButtonNode { replyCount = 0 isReplies = false } + if disableComments { + replyCount = 0 + isReplies = false + } if self.theme !== presentationData.theme.theme || self.isReplies != isReplies { self.theme = presentationData.theme.theme self.isReplies = isReplies - - let graphics = PresentationResourcesChat.additionalGraphics(presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper, bubbleCorners: presentationData.chatBubbleCorners) - var updatedShareButtonBackground: UIImage? + var updatedIconImage: UIImage? + var updatedIconOffset = CGPoint() if case .pinnedMessages = subject { - updatedShareButtonBackground = graphics.chatBubbleNavigateButtonImage + updatedIconImage = PresentationResourcesChat.chatFreeNavigateButtonIcon(presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper) } else if isReplies { - updatedShareButtonBackground = PresentationResourcesChat.chatFreeCommentButtonBackground(presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper) updatedIconImage = PresentationResourcesChat.chatFreeCommentButtonIcon(presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper) } else if message.id.peerId.isRepliesOrSavedMessages(accountPeerId: account.peerId) { - updatedShareButtonBackground = graphics.chatBubbleNavigateButtonImage + updatedIconImage = PresentationResourcesChat.chatFreeNavigateButtonIcon(presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper) + updatedIconOffset = CGPoint(x: UIScreenPixel, y: 1.0) } else { - updatedShareButtonBackground = graphics.chatBubbleShareButtonImage + updatedIconImage = PresentationResourcesChat.chatFreeShareButtonIcon(presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper) } - self.backgroundNode.image = updatedShareButtonBackground + self.backgroundNode.updateColor(color: selectDateFillStaticColor(theme: presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper), enableBlur: dateFillNeedsBlur(theme: presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper), transition: .immediate) self.iconNode.image = updatedIconImage + self.iconOffset = updatedIconOffset } var size = CGSize(width: 30.0, height: 30.0) var offsetIcon = false @@ -133,27 +150,25 @@ class ChatMessageShareButton: HighlightableButtonNode { textNode.removeFromSupernode() } self.backgroundNode.frame = CGRect(origin: CGPoint(), size: size) + self.backgroundNode.update(size: self.backgroundNode.bounds.size, cornerRadius: min(self.backgroundNode.bounds.width, self.backgroundNode.bounds.height) / 2.0, transition: .immediate) if let image = self.iconNode.image { - self.iconNode.frame = CGRect(origin: CGPoint(x: floor((size.width - image.size.width) / 2.0), y: floor((size.width - image.size.width) / 2.0) - (offsetIcon ? 1.0 : 0.0)), size: image.size) + self.iconNode.frame = CGRect(origin: CGPoint(x: floor((size.width - image.size.width) / 2.0) + self.iconOffset.x, y: floor((size.width - image.size.width) / 2.0) - (offsetIcon ? 1.0 : 0.0) + self.iconOffset.y), size: image.size) } return size } } class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { - private let contextSourceNode: ContextExtractedContentContainingNode + let contextSourceNode: ContextExtractedContentContainingNode private let containerNode: ContextControllerSourceNode let imageNode: TransformImageNode - private var placeholderNode: StickerShimmerEffectNode - private var animationNode: GenericAnimatedStickerNode? + private var enableSynchronousImageApply: Bool = false + private var backgroundNode: WallpaperBackgroundNode.BubbleBackgroundNode? + private(set) var placeholderNode: StickerShimmerEffectNode + private(set) var animationNode: GenericAnimatedStickerNode? private var didSetUpAnimationNode = false private var isPlaying = false - private var animateGreeting = false - private var animatingGreeting = false - private weak var greetingStickerParentNode: ASDisplayNode? - private weak var greetingStickerListNode: ASDisplayNode? - private var greetingCompletion: ((@escaping () -> Void) -> Void)? - + private var swipeToReplyNode: ChatMessageSwipeToReplyNode? private var swipeToReplyFeedback: HapticFeedback? @@ -167,12 +182,12 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { private let disposable = MetaDisposable() private var forwardInfoNode: ChatMessageForwardInfoNode? - private var forwardBackgroundNode: ASImageNode? + private var forwardBackgroundNode: NavigationBackgroundNode? private var viaBotNode: TextNode? private let dateAndStatusNode: ChatMessageDateAndStatusNode private var replyInfoNode: ChatMessageReplyInfoNode? - private var replyBackgroundNode: ASImageNode? + private var replyBackgroundNode: NavigationBackgroundNode? private var actionButtonsNode: ChatMessageActionButtonsNode? @@ -249,11 +264,16 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { return } if image != nil { - if firstTime && !strongSelf.placeholderNode.isEmpty && !strongSelf.animateGreeting && !strongSelf.animatingGreeting { - strongSelf.animationNode?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) - strongSelf.removePlaceholder(animated: true) + if firstTime && !strongSelf.placeholderNode.isEmpty { + if strongSelf.enableSynchronousImageApply { + strongSelf.removePlaceholder(animated: false) + } else { + strongSelf.imageNode.alpha = 0.0 + } } else { - strongSelf.removePlaceholder(animated: true) + if strongSelf.setupTimestamp == nil { + strongSelf.removePlaceholder(animated: true) + } } firstTime = false } @@ -290,10 +310,10 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { } private func removePlaceholder(animated: Bool) { + self.placeholderNode.alpha = 0.0 if !animated { self.placeholderNode.removeFromSupernode() } else { - self.placeholderNode.alpha = 0.0 self.placeholderNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, completion: { [weak self] _ in self?.placeholderNode.removeFromSupernode() }) @@ -377,6 +397,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { } } + private var setupTimestamp: Double? private func setupNode(item: ChatMessageItem) { guard self.animationNode == nil else { return @@ -405,46 +426,46 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { self.animationNode = animationNode } } else { - let animationNode: AnimatedStickerNode - if let (node, parentNode, listNode, greetingCompletion) = item.controllerInteraction.greetingStickerNode(), let greetingStickerNode = node as? AnimatedStickerNode { - animationNode = greetingStickerNode - self.imageNode.alpha = 0.0 - self.animateGreeting = true - self.greetingStickerParentNode = parentNode - self.greetingStickerListNode = listNode - self.greetingCompletion = greetingCompletion - } else { - animationNode = AnimatedStickerNode() - animationNode.started = { [weak self] in - if let strongSelf = self { - strongSelf.imageNode.alpha = 0.0 - - if let item = strongSelf.item { - if let _ = strongSelf.emojiFile { - item.controllerInteraction.seenOneTimeAnimatedMedia.insert(item.message.id) + let animationNode = AnimatedStickerNode() + animationNode.started = { [weak self] in + if let strongSelf = self { + strongSelf.imageNode.alpha = 0.0 + if !strongSelf.enableSynchronousImageApply { + let current = CACurrentMediaTime() + if let setupTimestamp = strongSelf.setupTimestamp, current - setupTimestamp > 0.3 { + if !strongSelf.placeholderNode.alpha.isZero { + strongSelf.animationNode?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + strongSelf.removePlaceholder(animated: true) } + } else { + strongSelf.removePlaceholder(animated: false) + } + } + + if let item = strongSelf.item { + if let _ = strongSelf.emojiFile { + item.controllerInteraction.seenOneTimeAnimatedMedia.insert(item.message.id) } } } } - self.animationNode = animationNode } - if let animationNode = self.animationNode, !self.animateGreeting { + if let animationNode = self.animationNode { self.contextSourceNode.contentNode.insertSubnode(animationNode, aboveSubnode: self.placeholderNode) } } - override func setupItem(_ item: ChatMessageItem) { - super.setupItem(item) + override func setupItem(_ item: ChatMessageItem, synchronousLoad: Bool) { + super.setupItem(item, synchronousLoad: synchronousLoad) for media in item.message.media { if let telegramFile = media as? TelegramMediaFile { if self.telegramFile?.id != telegramFile.id { self.telegramFile = telegramFile let dimensions = telegramFile.dimensions ?? PixelDimensions(width: 512, height: 512) - self.imageNode.setSignal(chatMessageAnimatedSticker(postbox: item.context.account.postbox, file: telegramFile, small: false, size: dimensions.cgSize.aspectFitted(CGSize(width: 384.0, height: 384.0)), thumbnail: false)) + self.imageNode.setSignal(chatMessageAnimatedSticker(postbox: item.context.account.postbox, file: telegramFile, small: false, size: dimensions.cgSize.aspectFitted(CGSize(width: 384.0, height: 384.0)), thumbnail: false, synchronousLoad: synchronousLoad), attemptSynchronously: synchronousLoad) self.updateVisibility() self.disposable.set(freeMediaFileInteractiveFetched(account: item.context.account, fileReference: .message(message: MessageReference(item.message), media: telegramFile)).start()) } @@ -485,7 +506,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { 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.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, synchronousLoad: synchronousLoad), attemptSynchronously: synchronousLoad) self.disposable.set(freeMediaFileInteractiveFetched(account: item.context.account, fileReference: .standalone(media: emojiFile)).start()) } self.updateVisibility() @@ -510,7 +531,10 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { } } - animationNode.visibility = isPlaying && !alreadySeen + if isPlaying && self.setupTimestamp == nil { + self.setupTimestamp = CACurrentMediaTime() + } + animationNode.visibility = isPlaying if self.didSetUpAnimationNode && alreadySeen { if let emojiFile = self.emojiFile, emojiFile.resource is LocalFileReferenceMediaResource { @@ -545,7 +569,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { fitzModifier = EmojiFitzModifier(emoji: fitz) } } - + 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)) @@ -574,6 +598,16 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { rect.origin.y = containerSize.height - rect.maxY + self.insets.top self.placeholderNode.updateAbsoluteRect(CGRect(origin: CGPoint(x: rect.minX + self.placeholderNode.frame.minX, y: rect.minY + self.placeholderNode.frame.minY), size: self.placeholderNode.frame.size), within: containerSize) + + if let backgroundNode = self.backgroundNode { + backgroundNode.update(rect: CGRect(origin: CGPoint(x: rect.minX + self.placeholderNode.frame.minX, y: rect.minY + self.placeholderNode.frame.minY), size: self.placeholderNode.frame.size), within: containerSize) + } + } + } + + override func applyAbsoluteOffset(value: CGPoint, animationCurve: ContainedViewLayoutTransitionCurve, duration: Double) { + if let backgroundNode = self.backgroundNode { + backgroundNode.offset(value: value, animationCurve: animationCurve, duration: duration) } } @@ -619,13 +653,10 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { let actionButtonsLayout = ChatMessageActionButtonsNode.asyncLayout(self.actionButtonsNode) let makeForwardInfoLayout = ChatMessageForwardInfoNode.asyncLayout(self.forwardInfoNode) - let currentForwardBackgroundNode = self.forwardBackgroundNode let viaBotLayout = TextNode.asyncLayout(self.viaBotNode) let makeReplyInfoLayout = ChatMessageReplyInfoNode.asyncLayout(self.replyInfoNode) - let currentReplyBackgroundNode = self.replyBackgroundNode let currentShareButtonNode = self.shareButtonNode - let currentItem = self.item let currentForwardInfo = self.appliedForwardInfo return { item, params, mergedTop, mergedBottom, dateHeaderAtBottom in @@ -820,8 +851,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { var viaBotApply: (TextNodeLayout, () -> TextNode)? var replyInfoApply: (CGSize, () -> ChatMessageReplyInfoNode)? - var updatedReplyBackgroundNode: ASImageNode? - var replyBackgroundImage: UIImage? + var needsReplyBackground = false var replyMarkup: ReplyMarkupMessageAttribute? var ignoreForward = self.telegramDice == nil @@ -887,14 +917,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { } if replyInfoApply != nil || viaBotApply != nil { - if let currentReplyBackgroundNode = currentReplyBackgroundNode { - updatedReplyBackgroundNode = currentReplyBackgroundNode - } else { - updatedReplyBackgroundNode = ASImageNode() - } - - let graphics = PresentationResourcesChat.additionalGraphics(item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper, bubbleCorners: item.presentationData.chatBubbleCorners) - replyBackgroundImage = graphics.chatFreeformContentAdditionalInfoBackgroundImage + needsReplyBackground = true } var updatedShareButtonNode: ChatMessageShareButton? @@ -914,8 +937,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { var forwardPsaType: String? var forwardInfoSizeApply: (CGSize, (CGFloat) -> ChatMessageForwardInfoNode)? - var updatedForwardBackgroundNode: ASImageNode? - var forwardBackgroundImage: UIImage? + var needsForwardBackground = false if !ignoreForward, let forwardInfo = item.message.forwardInfo { forwardPsaType = forwardInfo.psaType @@ -940,15 +962,8 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { } let availableWidth = max(60.0, availableContentWidth + 6.0) forwardInfoSizeApply = makeForwardInfoLayout(item.presentationData, item.presentationData.strings, .standalone, forwardSource, forwardAuthorSignature, forwardPsaType, CGSize(width: availableWidth, height: CGFloat.greatestFiniteMagnitude)) - - if let currentForwardBackgroundNode = currentForwardBackgroundNode { - updatedForwardBackgroundNode = currentForwardBackgroundNode - } else { - updatedForwardBackgroundNode = ASImageNode() - } - - let graphics = PresentationResourcesChat.additionalGraphics(item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper, bubbleCorners: item.presentationData.chatBubbleCorners) - forwardBackgroundImage = graphics.chatServiceBubbleFillImage + + needsForwardBackground = true } var maxContentWidth = imageSize.width @@ -1002,65 +1017,33 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { } if let file = file, let immediateThumbnailData = file.immediateThumbnailData { + if strongSelf.backgroundNode == nil { + if let backgroundNode = item.controllerInteraction.presentationContext.backgroundNode?.makeBubbleBackground(for: .free) { + strongSelf.backgroundNode = backgroundNode + strongSelf.placeholderNode.addBackdropNode(backgroundNode) + + if let (rect, size) = strongSelf.absoluteRect { + strongSelf.updateAbsoluteRect(rect, within: size) + } + } + } + let foregroundColor = bubbleVariableColor(variableColor: item.presentationData.theme.theme.chat.message.stickerPlaceholderColor, wallpaper: item.presentationData.theme.wallpaper) let shimmeringColor = bubbleVariableColor(variableColor: item.presentationData.theme.theme.chat.message.stickerPlaceholderShimmerColor, wallpaper: item.presentationData.theme.wallpaper) strongSelf.placeholderNode.update(backgroundColor: nil, foregroundColor: foregroundColor, shimmeringColor: shimmeringColor, data: immediateThumbnailData, size: animationNodeFrame.size, imageSize: file.dimensions?.cgSize ?? CGSize(width: 512.0, height: 512.0)) strongSelf.placeholderNode.frame = animationNodeFrame } - if let animationNode = strongSelf.animationNode, let parentNode = strongSelf.greetingStickerParentNode, strongSelf.animateGreeting { - strongSelf.animateGreeting = false - strongSelf.animatingGreeting = true - - let initialFrame = animationNode.view.convert(animationNode.bounds, to: parentNode.view) - parentNode.addSubnode(animationNode) - animationNode.frame = initialFrame - - var targetPositionY = initialFrame.center.y - if let listNode = strongSelf.greetingStickerListNode as? ListView { - targetPositionY = listNode.frame.height - listNode.insets.top - animationNodeFrame.height / 2.0 - 12.0 - } - let targetPosition = CGPoint(x: animationNodeFrame.midX, y: targetPositionY) - - let targetScale = animationNodeFrame.width / initialFrame.width - animationNode.layer.animateScale(from: 1.0, to: targetScale, duration: 0.3, removeOnCompletion: false) - - animationNode.layer.animatePosition(from: initialFrame.center, to: targetPosition, duration: 0.4, mediaTimingFunction: CAMediaTimingFunction(controlPoints: 0.3, 0.0, 0.0, 1.0), removeOnCompletion: false, completion: { [weak self] finished in - if let strongSelf = self { - let initialDateNodeFrame = strongSelf.dateAndStatusNode.frame - if strongSelf.animatingGreeting { - if strongSelf.dateAndStatusNode.supernode !== parentNode { - let dateNodeFrame = strongSelf.dateAndStatusNode.view.convert(strongSelf.dateAndStatusNode.bounds, to: parentNode.view) - parentNode.addSubnode(strongSelf.dateAndStatusNode) - strongSelf.dateAndStatusNode.frame = dateNodeFrame - - strongSelf.dateAndStatusNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) - } - } - - strongSelf.greetingCompletion?({ - animationNode.layer.removeAllAnimations() - strongSelf.animationNode?.frame = animationNodeFrame - strongSelf.contextSourceNode.contentNode.insertSubnode(animationNode, aboveSubnode: strongSelf.imageNode) - - strongSelf.contextSourceNode.contentNode.addSubnode(strongSelf.dateAndStatusNode) - strongSelf.dateAndStatusNode.frame = initialDateNodeFrame - - if let animationNode = strongSelf.animationNode as? AnimatedStickerNode { - animationNode.updateLayout(size: updatedContentFrame.insetBy(dx: imageInset, dy: imageInset).size) - } - strongSelf.animatingGreeting = false - }) - } - }) - - } else if strongSelf.animationNode?.supernode === strongSelf.contextSourceNode.contentNode { + if strongSelf.animationNode?.supernode === strongSelf.contextSourceNode.contentNode { strongSelf.animationNode?.frame = animationNodeFrame } if let animationNode = strongSelf.animationNode as? AnimatedStickerNode, strongSelf.animationNode?.supernode === strongSelf.contextSourceNode.contentNode { animationNode.updateLayout(size: updatedContentFrame.insetBy(dx: imageInset, dy: imageInset).size) } + + strongSelf.enableSynchronousImageApply = true imageApply() + strongSelf.enableSynchronousImageApply = false strongSelf.contextSourceNode.contentRect = strongSelf.imageNode.frame strongSelf.containerNode.targetNodeForActivationProgressContentRect = strongSelf.contextSourceNode.contentRect @@ -1082,22 +1065,19 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { } dateAndStatusApply(false) - let dateAndStatusFrame = CGRect(origin: CGPoint(x: max(displayLeftInset, updatedImageFrame.maxX - dateAndStatusSize.width - 4.0), y: updatedImageFrame.maxY - dateAndStatusSize.height - 4.0), size: dateAndStatusSize) - if strongSelf.dateAndStatusNode.supernode != strongSelf.greetingStickerParentNode { - strongSelf.dateAndStatusNode.frame = dateAndStatusFrame - } - - if let updatedReplyBackgroundNode = updatedReplyBackgroundNode { - if strongSelf.replyBackgroundNode == nil { - strongSelf.replyBackgroundNode = updatedReplyBackgroundNode - strongSelf.addSubnode(updatedReplyBackgroundNode) - updatedReplyBackgroundNode.image = replyBackgroundImage + strongSelf.dateAndStatusNode.frame = CGRect(origin: CGPoint(x: max(displayLeftInset, updatedImageFrame.maxX - dateAndStatusSize.width - 4.0), y: updatedImageFrame.maxY - dateAndStatusSize.height - 4.0), size: dateAndStatusSize) + + if needsReplyBackground { + if let replyBackgroundNode = strongSelf.replyBackgroundNode { + replyBackgroundNode.updateColor(color: selectDateFillStaticColor(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper), enableBlur: dateFillNeedsBlur(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper), transition: .immediate) } else { - strongSelf.replyBackgroundNode?.image = replyBackgroundImage + let replyBackgroundNode = NavigationBackgroundNode(color: selectDateFillStaticColor(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper), enableBlur: dateFillNeedsBlur(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper)) + strongSelf.replyBackgroundNode = replyBackgroundNode + strongSelf.contextSourceNode.contentNode.addSubnode(replyBackgroundNode) } } else if let replyBackgroundNode = strongSelf.replyBackgroundNode { - replyBackgroundNode.removeFromSupernode() strongSelf.replyBackgroundNode = nil + replyBackgroundNode.removeFromSupernode() } if let (viaBotLayout, viaBotApply) = viaBotApply { @@ -1108,7 +1088,10 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { } let viaBotFrame = CGRect(origin: CGPoint(x: (!incoming ? (params.leftInset + layoutConstants.bubble.edgeInset + 15.0) : (params.width - params.rightInset - viaBotLayout.size.width - layoutConstants.bubble.edgeInset - 14.0)), y: 8.0), size: viaBotLayout.size) viaBotNode.frame = viaBotFrame - strongSelf.replyBackgroundNode?.frame = CGRect(origin: CGPoint(x: viaBotFrame.minX - 6.0, y: viaBotFrame.minY - 2.0 - UIScreenPixel), size: CGSize(width: viaBotFrame.size.width + 11.0, height: viaBotFrame.size.height + 5.0)) + if let replyBackgroundNode = strongSelf.replyBackgroundNode { + replyBackgroundNode.frame = CGRect(origin: CGPoint(x: viaBotFrame.minX - 6.0, y: viaBotFrame.minY - 2.0 - UIScreenPixel), size: CGSize(width: viaBotFrame.size.width + 11.0, height: viaBotFrame.size.height + 5.0)) + replyBackgroundNode.update(size: replyBackgroundNode.bounds.size, cornerRadius: 8.0, transition: .immediate) + } } else if let viaBotNode = strongSelf.viaBotNode { viaBotNode.removeFromSupernode() strongSelf.viaBotNode = nil @@ -1118,7 +1101,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { let replyInfoNode = replyInfoApply() if strongSelf.replyInfoNode == nil { strongSelf.replyInfoNode = replyInfoNode - strongSelf.addSubnode(replyInfoNode) + strongSelf.contextSourceNode.contentNode.addSubnode(replyInfoNode) } var viaBotSize = CGSize() if let viaBotNode = strongSelf.viaBotNode { @@ -1131,7 +1114,10 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { } } replyInfoNode.frame = replyInfoFrame - strongSelf.replyBackgroundNode?.frame = CGRect(origin: CGPoint(x: replyInfoFrame.minX - 4.0, y: replyInfoFrame.minY - viaBotSize.height - 2.0), size: CGSize(width: max(replyInfoFrame.size.width, viaBotSize.width) + 8.0, height: replyInfoFrame.size.height + viaBotSize.height + 5.0)) + if let replyBackgroundNode = strongSelf.replyBackgroundNode { + replyBackgroundNode.frame = CGRect(origin: CGPoint(x: replyInfoFrame.minX - 4.0, y: replyInfoFrame.minY - viaBotSize.height - 2.0), size: CGSize(width: max(replyInfoFrame.size.width, viaBotSize.width) + 8.0, height: replyInfoFrame.size.height + viaBotSize.height + 5.0)) + replyBackgroundNode.update(size: replyBackgroundNode.bounds.size, cornerRadius: 8.0, transition: .immediate) + } if let _ = item.controllerInteraction.selectionState, isEmoji { replyInfoNode.alpha = 0.0 @@ -1172,16 +1158,15 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { deliveryFailedNode?.removeFromSupernode() }) } - - if let updatedForwardBackgroundNode = updatedForwardBackgroundNode { - if strongSelf.forwardBackgroundNode == nil { - strongSelf.forwardBackgroundNode = updatedForwardBackgroundNode - strongSelf.addSubnode(updatedForwardBackgroundNode) - updatedForwardBackgroundNode.image = forwardBackgroundImage + + if needsForwardBackground { + if let forwardBackgroundNode = strongSelf.forwardBackgroundNode { + forwardBackgroundNode.updateColor(color: selectDateFillStaticColor(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper), enableBlur: dateFillNeedsBlur(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper), transition: .immediate) + } else { + let forwardBackgroundNode = NavigationBackgroundNode(color: selectDateFillStaticColor(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper), enableBlur: dateFillNeedsBlur(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper)) + strongSelf.forwardBackgroundNode = forwardBackgroundNode + strongSelf.addSubnode(forwardBackgroundNode) } - } else if let forwardBackgroundNode = strongSelf.forwardBackgroundNode { - forwardBackgroundNode.removeFromSupernode() - strongSelf.forwardBackgroundNode = nil } if let (forwardInfoSize, forwardInfoApply) = forwardInfoSizeApply { @@ -1192,7 +1177,10 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { } let forwardInfoFrame = CGRect(origin: CGPoint(x: (!incoming ? (params.leftInset + layoutConstants.bubble.edgeInset + 12.0) : (params.width - params.rightInset - forwardInfoSize.width - layoutConstants.bubble.edgeInset - 12.0)), y: 8.0), size: forwardInfoSize) forwardInfoNode.frame = forwardInfoFrame - strongSelf.forwardBackgroundNode?.frame = CGRect(origin: CGPoint(x: forwardInfoFrame.minX - 6.0, y: forwardInfoFrame.minY - 2.0), size: CGSize(width: forwardInfoFrame.size.width + 10.0, height: forwardInfoFrame.size.height + 4.0)) + if let forwardBackgroundNode = strongSelf.forwardBackgroundNode { + forwardBackgroundNode.frame = CGRect(origin: CGPoint(x: forwardInfoFrame.minX - 6.0, y: forwardInfoFrame.minY - 2.0), size: CGSize(width: forwardInfoFrame.size.width + 10.0, height: forwardInfoFrame.size.height + 4.0)) + forwardBackgroundNode.update(size: forwardBackgroundNode.bounds.size, cornerRadius: 8.0, transition: .immediate) + } } else if let forwardInfoNode = strongSelf.forwardInfoNode { forwardInfoNode.removeFromSupernode() strongSelf.forwardInfoNode = nil @@ -1290,7 +1278,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { for attribute in item.content.firstMessage.attributes { if let attribute = attribute as? SourceReferenceMessageAttribute { openPeerId = attribute.messageId.peerId - navigate = .chat(textInputState: nil, subject: .message(id: attribute.messageId, highlight: true), peekData: nil) + navigate = .chat(textInputState: nil, subject: .message(id: attribute.messageId, highlight: true, timecode: nil), peekData: nil) } } @@ -1510,7 +1498,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { if translation.x < -45.0, self.swipeToReplyNode == nil, let item = self.item { self.swipeToReplyFeedback?.impact() - 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), action: ChatMessageSwipeToReplyNode.Action(self.currentSwipeAction)) + let swipeToReplyNode = ChatMessageSwipeToReplyNode(fillColor: selectDateFillStaticColor(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper), enableBlur: dateFillNeedsBlur(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper), foregroundColor: bubbleVariableColor(variableColor: item.presentationData.theme.theme.chat.message.shareButtonForegroundColor, wallpaper: item.presentationData.theme.wallpaper), action: ChatMessageSwipeToReplyNode.Action(self.currentSwipeAction)) self.swipeToReplyNode = swipeToReplyNode self.addSubnode(swipeToReplyNode) animateReplyNodeIn = true @@ -1583,6 +1571,15 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { if case let .replyThread(replyThreadMessage) = item.chatLocation, replyThreadMessage.effectiveTopId == item.message.id { return } + + let transition: ContainedViewLayoutTransition = animated ? .animated(duration: 0.2, curve: .easeInOut) : .immediate + let replyAlpha: CGFloat = item.controllerInteraction.selectionState == nil ? 1.0 : 0.0 + if let replyInfoNode = self.replyInfoNode { + transition.updateAlpha(node: replyInfoNode, alpha: replyAlpha) + } + if let replyBackgroundNode = self.replyBackgroundNode { + transition.updateAlpha(node: replyBackgroundNode, alpha: replyAlpha) + } if let selectionState = item.controllerInteraction.selectionState { var selected = false @@ -1669,6 +1666,10 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { } } } + + override func cancelInsertionAnimations() { + self.layer.removeAllAnimations() + } override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { super.animateInsertion(currentTimestamp, duration: duration, short: short) @@ -1707,6 +1708,168 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { override func addAccessoryItemNode(_ accessoryItemNode: ListViewAccessoryItemNode) { self.contextSourceNode.contentNode.addSubnode(accessoryItemNode) } + + func animateContentFromTextInputField(textInput: ChatMessageTransitionNode.Source.TextInput, transition: CombinedTransition) { + guard let _ = self.item else { + return + } + + let localSourceContentFrame = self.contextSourceNode.contentNode.view.convert(textInput.contentView.frame.offsetBy(dx: self.contextSourceNode.contentRect.minX, dy: self.contextSourceNode.contentRect.minY), to: self.contextSourceNode.contentNode.view) + textInput.contentView.frame = localSourceContentFrame + + self.contextSourceNode.contentNode.view.addSubview(textInput.contentView) + + let sourceCenter = CGPoint( + x: localSourceContentFrame.minX + 11.2, + y: localSourceContentFrame.midY - 1.8 + ) + let localSourceCenter = CGPoint( + x: sourceCenter.x - localSourceContentFrame.minX, + y: sourceCenter.y - localSourceContentFrame.minY + ) + let localSourceOffset = CGPoint( + x: localSourceCenter.x - localSourceContentFrame.width / 2.0, + y: localSourceCenter.y - localSourceContentFrame.height / 2.0 + ) + + let sourceScale: CGFloat = 28.0 / self.imageNode.frame.height + + let offset = CGPoint( + x: sourceCenter.x - self.imageNode.frame.midX, + y: sourceCenter.y - self.imageNode.frame.midY + ) + + transition.animatePositionAdditive(layer: self.imageNode.layer, offset: offset) + transition.horizontal.animateTransformScale(node: self.imageNode, from: sourceScale) + if let animationNode = self.animationNode { + transition.animatePositionAdditive(layer: animationNode.layer, offset: offset) + transition.horizontal.animateTransformScale(node: animationNode, from: sourceScale) + } + transition.animatePositionAdditive(layer: self.placeholderNode.layer, offset: offset) + transition.horizontal.animateTransformScale(node: self.placeholderNode, from: sourceScale) + + let inverseScale = 1.0 / sourceScale + + transition.animatePositionAdditive(layer: textInput.contentView.layer, offset: CGPoint(), to: CGPoint( + x: -offset.x - localSourceOffset.x * (inverseScale - 1.0), + y: -offset.y - localSourceOffset.y * (inverseScale - 1.0) + ), removeOnCompletion: false) + transition.horizontal.updateTransformScale(layer: textInput.contentView.layer, scale: 1.0 / sourceScale) + + textInput.contentView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false, completion: { _ in + textInput.contentView.removeFromSuperview() + }) + + self.imageNode.layer.animateAlpha(from: 0.0, to: self.imageNode.alpha, duration: 0.1) + if let animationNode = self.animationNode { + animationNode.layer.animateAlpha(from: 0.0, to: animationNode.alpha, duration: 0.1) + } + self.placeholderNode.layer.animateAlpha(from: 0.0, to: self.placeholderNode.alpha, duration: 0.1) + + self.dateAndStatusNode.layer.animateAlpha(from: 0.0, to: self.dateAndStatusNode.alpha, duration: 0.15, delay: 0.16) + } + + func animateContentFromStickerGridItem(stickerSource: ChatMessageTransitionNode.Sticker, transition: CombinedTransition) { + guard let _ = self.item else { + return + } + + let localSourceContentFrame = CGRect( + origin: CGPoint( + x: self.imageNode.frame.minX + self.imageNode.frame.size.width / 2.0 - stickerSource.imageNode.frame.size.width / 2.0, + y: self.imageNode.frame.minY + self.imageNode.frame.size.height / 2.0 - stickerSource.imageNode.frame.size.height / 2.0 + ), + size: stickerSource.imageNode.frame.size + ) + + var snapshotView: UIView? + if let animationNode = stickerSource.animationNode { + snapshotView = animationNode.view.snapshotContentTree() + } else { + snapshotView = stickerSource.imageNode.view.snapshotContentTree() + } + snapshotView?.frame = localSourceContentFrame + + if let snapshotView = snapshotView { + self.contextSourceNode.contentNode.view.addSubview(snapshotView) + } + + let sourceCenter = CGPoint( + x: localSourceContentFrame.midX, + y: localSourceContentFrame.midY + ) + let localSourceCenter = CGPoint( + x: sourceCenter.x - localSourceContentFrame.minX, + y: sourceCenter.y - localSourceContentFrame.minY + ) + let localSourceOffset = CGPoint( + x: localSourceCenter.x - localSourceContentFrame.width / 2.0, + y: localSourceCenter.y - localSourceContentFrame.height / 2.0 + ) + + let sourceScale: CGFloat = stickerSource.imageNode.frame.height / self.imageNode.frame.height + + let offset = CGPoint( + x: sourceCenter.x - self.imageNode.frame.midX, + y: sourceCenter.y - self.imageNode.frame.midY + ) + + transition.animatePositionAdditive(layer: self.imageNode.layer, offset: offset) + transition.horizontal.animateTransformScale(node: self.imageNode, from: sourceScale) + if let animationNode = self.animationNode { + transition.animatePositionAdditive(layer: animationNode.layer, offset: offset) + transition.horizontal.animateTransformScale(node: animationNode, from: sourceScale) + } + transition.animatePositionAdditive(layer: self.placeholderNode.layer, offset: offset) + transition.horizontal.animateTransformScale(node: self.placeholderNode, from: sourceScale) + + let inverseScale = 1.0 / sourceScale + + if let snapshotView = snapshotView { + transition.animatePositionAdditive(layer: snapshotView.layer, offset: CGPoint(), to: CGPoint( + x: -offset.x - localSourceOffset.x * (inverseScale - 1.0), + y: -offset.y - localSourceOffset.y * (inverseScale - 1.0) + ), removeOnCompletion: false) + transition.horizontal.updateTransformScale(layer: snapshotView.layer, scale: 1.0 / sourceScale) + + snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.08, removeOnCompletion: false, completion: { [weak snapshotView] _ in + snapshotView?.removeFromSuperview() + }) + + self.imageNode.layer.animateAlpha(from: 0.0, to: self.imageNode.alpha, duration: 0.05) + if let animationNode = self.animationNode { + animationNode.layer.animateAlpha(from: 0.0, to: animationNode.alpha, duration: 0.05) + } + self.placeholderNode.layer.animateAlpha(from: 0.0, to: self.placeholderNode.alpha, duration: 0.05) + } + + self.dateAndStatusNode.layer.animateAlpha(from: 0.0, to: self.dateAndStatusNode.alpha, duration: 0.15, delay: 0.16) + + if let animationNode = stickerSource.animationNode { + animationNode.layer.animateScale(from: 0.1, to: 1.0, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring) + animationNode.layer.animateAlpha(from: 0.0, to: animationNode.alpha, duration: 0.4) + } + + stickerSource.imageNode.layer.animateScale(from: 0.1, to: 1.0, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring) + stickerSource.imageNode.layer.animateAlpha(from: 0.0, to: stickerSource.imageNode.alpha, duration: 0.4) + + if let placeholderNode = stickerSource.placeholderNode { + placeholderNode.layer.animateScale(from: 0.1, to: 1.0, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring) + placeholderNode.layer.animateAlpha(from: 0.0, to: placeholderNode.alpha, duration: 0.4) + } + } + + func animateReplyPanel(sourceReplyPanel: ChatMessageTransitionNode.ReplyPanel, transition: CombinedTransition) { + if let replyInfoNode = self.replyInfoNode { + let localRect = self.contextSourceNode.contentNode.view.convert(sourceReplyPanel.relativeSourceRect, to: replyInfoNode.view) + + let offset = replyInfoNode.animateFromInputPanel(sourceReplyPanel: sourceReplyPanel, localRect: localRect, transition: transition) + if let replyBackgroundNode = self.replyBackgroundNode { + transition.animatePositionAdditive(layer: replyBackgroundNode.layer, offset: offset) + replyBackgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1) + } + } + } } struct AnimatedEmojiSoundsConfiguration { diff --git a/submodules/TelegramUI/Sources/ChatMessageAttachedContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageAttachedContentNode.swift index 12e233aa60..d01b40ed2e 100644 --- a/submodules/TelegramUI/Sources/ChatMessageAttachedContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageAttachedContentNode.swift @@ -271,7 +271,7 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { self.addSubnode(self.statusNode) } - func asyncLayout() -> (_ presentationData: ChatPresentationData, _ automaticDownloadSettings: MediaAutoDownloadSettings, _ associatedData: ChatMessageItemAssociatedData, _ attributes: ChatMessageEntryAttributes, _ context: AccountContext, _ controllerInteraction: ChatControllerInteraction, _ message: Message, _ messageRead: Bool, _ chatLocation: ChatLocation, _ 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, _ chatLocation: ChatLocation, _ title: String?, _ subtitle: NSAttributedString?, _ text: String?, _ entities: [MessageTextEntity]?, _ media: (Media, ChatMessageAttachedContentNodeMediaFlags)?, _ mediaBadge: String?, _ actionIcon: ChatMessageAttachedContentActionIcon?, _ actionTitle: String?, _ displayLine: Bool, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ 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() @@ -284,7 +284,7 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { let currentAdditionalImageBadgeNode = self.additionalImageBadgeNode - return { presentationData, automaticDownloadSettings, associatedData, attributes, context, controllerInteraction, message, messageRead, chatLocation, title, subtitle, text, entities, mediaAndFlags, mediaBadge, actionIcon, actionTitle, displayLine, layoutConstants, constrainedSize in + return { presentationData, automaticDownloadSettings, associatedData, attributes, context, controllerInteraction, message, messageRead, chatLocation, title, subtitle, text, entities, mediaAndFlags, mediaBadge, actionIcon, actionTitle, displayLine, layoutConstants, preparePosition, constrainedSize in let isPreview = presentationData.isPreview let fontSize: CGFloat = floor(presentationData.fontSize.baseDisplaySize * 15.0 / 17.0) @@ -412,24 +412,117 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { textString = string.attributedSubstring(from: NSMakeRange(0, 1000)) } - var automaticPlayback = false - - var skipStandardStatus = false - var isReplyThread = false if case .replyThread = chatLocation { isReplyThread = true } + + var skipStandardStatus = false + var isImage = false + var isFile = false + + var automaticPlayback = false + + var textStatusType: ChatMessageDateAndStatusType? + var imageStatusType: ChatMessageDateAndStatusType? + var additionalImageBadgeContent: ChatMessageInteractiveMediaBadgeContent? + + 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 { + isImage = true + } else if file.isInstantVideo { + isImage = true + } else if file.isVideo { + isImage = true + } else if file.isSticker || file.isAnimatedSticker { + isImage = true + } else { + isFile = true + } + } else if let _ = media as? TelegramMediaImage { + if !flags.contains(.preferMediaInline) { + isImage = true + } + } else if let _ = media as? TelegramMediaWebFile { + isImage = true + } else if let _ = media as? WallpaperPreviewMedia { + isImage = true + } + } + + if preferMediaBeforeText { + isImage = false + } + + let statusInText = !isImage + + switch preparePosition { + case .linear(_, .None), .linear(_, .Neighbour(true, _, _)): + if let count = webpageGalleryMediaCount { + additionalImageBadgeContent = .text(inset: 0.0, backgroundColor: presentationData.theme.theme.chat.message.mediaDateAndStatusFillColor, foregroundColor: presentationData.theme.theme.chat.message.mediaDateAndStatusTextColor, text: NSAttributedString(string: presentationData.strings.Items_NOfM("1", "\(count)").0)) + skipStandardStatus = isImage + } else if let mediaBadge = mediaBadge { + additionalImageBadgeContent = .text(inset: 0.0, backgroundColor: presentationData.theme.theme.chat.message.mediaDateAndStatusFillColor, foregroundColor: presentationData.theme.theme.chat.message.mediaDateAndStatusTextColor, text: NSAttributedString(string: mediaBadge)) + } else { + skipStandardStatus = isFile + } + + if !skipStandardStatus { + if message.effectivelyIncoming(context.account.peerId) { + if isImage { + imageStatusType = .ImageIncoming + } else { + textStatusType = .BubbleIncoming + } + } else { + if message.flags.contains(.Failed) { + if isImage { + imageStatusType = .ImageOutgoing(.Failed) + } else { + textStatusType = .BubbleOutgoing(.Failed) + } + } else if (message.flags.isSending && !message.isSentOrAcknowledged) || attributes.updatingMedia != nil { + if isImage { + imageStatusType = .ImageOutgoing(.Sending) + } else { + textStatusType = .BubbleOutgoing(.Sending) + } + } else { + if isImage { + imageStatusType = .ImageOutgoing(.Sent(read: messageRead)) + } else { + textStatusType = .BubbleOutgoing(.Sent(read: messageRead)) + } + } + } + } + default: + break + } + + let imageDateAndStatus = imageStatusType.flatMap { statusType -> ChatMessageDateAndStatus in + ChatMessageDateAndStatus( + type: statusType, + edited: edited, + viewCount: viewCount, + dateReplies: dateReplies, + dateReactions: dateReactions, + isPinned: message.tags.contains(.pinned) && !associatedData.isInPinnedListMode && !isReplyThread, + dateText: dateText + ) + } 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, attributes, file, .full, associatedData.automaticDownloadPeerType, .constrained(CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height)), layoutConstants, contentMode) + let (_, initialImageWidth, refineLayout) = contentImageLayout(context, presentationData, presentationData.dateTimeFormat, message, attributes, file, imageDateAndStatus, .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 displaySize = CGSize(width: 212.0, height: 212.0) 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, chatLocation: chatLocation, presentationData: presentationData, associatedData: associatedData, attributes: attributes, isItemPinned: message.tags.contains(.pinned) && !isReplyThread, isItemEdited: false), 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, chatLocation: chatLocation, presentationData: presentationData, associatedData: associatedData, attributes: attributes, isItemPinned: message.tags.contains(.pinned) && !isReplyThread, isItemEdited: false), constrainedSize.width - horizontalInsets.left - horizontalInsets.right, displaySize, displaySize, 0.0, .bubble, automaticDownload) initialWidth = videoLayout.contentSize.width + videoLayout.overflowLeft + videoLayout.overflowRight contentInstantVideoSizeAndApply = (videoLayout, apply) } else if file.isVideo { @@ -455,12 +548,12 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { } } - 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) + let (_, initialImageWidth, refineLayout) = contentImageLayout(context, presentationData, presentationData.dateTimeFormat, message, attributes, file, imageDateAndStatus, 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, attributes, 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, presentationData.dateTimeFormat, message, attributes, file, imageDateAndStatus, 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 { @@ -485,7 +578,7 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { } 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, attributes, 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, presentationData.dateTimeFormat, message, attributes, image, imageDateAndStatus, 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 { @@ -497,11 +590,11 @@ 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, attributes, 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, presentationData.dateTimeFormat, message, attributes, image, imageDateAndStatus, 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, attributes, wallpaper, .full, associatedData.automaticDownloadPeerType, .constrained(CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height)), layoutConstants, contentMode) + let (_, initialImageWidth, refineLayout) = contentImageLayout(context, presentationData, presentationData.dateTimeFormat, message, attributes, wallpaper, imageDateAndStatus, .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 { @@ -527,60 +620,12 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { break } - var statusInText = false - var statusSizeAndApply: (CGSize, (Bool) -> Void)? - + let textConstrainedSize = CGSize(width: constrainedSize.width - insets.left - insets.right, height: constrainedSize.height - insets.top - insets.bottom) - - var additionalImageBadgeContent: ChatMessageInteractiveMediaBadgeContent? - - switch position { - case .linear(_, .None), .linear(_, .Neighbour(true, _, _)): - let imageMode = !((refineContentImageLayout == nil && refineContentFileLayout == nil && contentInstantVideoSizeAndApply == nil) || preferMediaBeforeText) - statusInText = !imageMode - - if let count = webpageGalleryMediaCount { - additionalImageBadgeContent = .text(inset: 0.0, backgroundColor: presentationData.theme.theme.chat.message.mediaDateAndStatusFillColor, foregroundColor: presentationData.theme.theme.chat.message.mediaDateAndStatusTextColor, text: NSAttributedString(string: presentationData.strings.Items_NOfM("1", "\(count)").0)) - skipStandardStatus = imageMode - } else if let mediaBadge = mediaBadge { - additionalImageBadgeContent = .text(inset: 0.0, backgroundColor: presentationData.theme.theme.chat.message.mediaDateAndStatusFillColor, foregroundColor: presentationData.theme.theme.chat.message.mediaDateAndStatusTextColor, text: NSAttributedString(string: mediaBadge)) - } - - if !skipStandardStatus { - let statusType: ChatMessageDateAndStatusType - if message.effectivelyIncoming(context.account.peerId) { - if imageMode { - statusType = .ImageIncoming - } else { - statusType = .BubbleIncoming - } - } else { - if message.flags.contains(.Failed) { - if imageMode { - statusType = .ImageOutgoing(.Failed) - } else { - statusType = .BubbleOutgoing(.Failed) - } - } else if (message.flags.isSending && !message.isSentOrAcknowledged) || attributes.updatingMedia != nil { - if imageMode { - statusType = .ImageOutgoing(.Sending) - } else { - statusType = .BubbleOutgoing(.Sending) - } - } else { - if imageMode { - statusType = .ImageOutgoing(.Sent(read: messageRead)) - } else { - statusType = .BubbleOutgoing(.Sent(read: messageRead)) - } - } - } - - statusSizeAndApply = statusLayout(context, presentationData, edited, viewCount, dateText, statusType, textConstrainedSize, dateReactions, dateReplies, message.tags.contains(.pinned) && !associatedData.isInPinnedListMode && !isReplyThread, message.isSelfExpiring) - } - default: - break + + if let textStatusType = textStatusType { + statusSizeAndApply = statusLayout(context, presentationData, edited, viewCount, dateText, textStatusType, textConstrainedSize, dateReactions, dateReplies, message.tags.contains(.pinned) && !associatedData.isInPinnedListMode && !isReplyThread, message.isSelfExpiring) } var updatedAdditionalImageBadge: ChatMessageInteractiveMediaBadge? @@ -823,6 +868,9 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { let contentImageNode = contentImageApply(transition, synchronousLoads) if strongSelf.contentImageNode !== contentImageNode { strongSelf.contentImageNode = contentImageNode + contentImageNode.activatePinch = { sourceNode in + controllerInteraction.activateMessagePinch(sourceNode) + } strongSelf.addSubnode(contentImageNode) contentImageNode.activateLocalContent = { [weak strongSelf] mode in if let strongSelf = strongSelf { diff --git a/submodules/TelegramUI/Sources/ChatMessageBackground.swift b/submodules/TelegramUI/Sources/ChatMessageBackground.swift index 36bcc4941c..c618c7d7b9 100644 --- a/submodules/TelegramUI/Sources/ChatMessageBackground.swift +++ b/submodules/TelegramUI/Sources/ChatMessageBackground.swift @@ -3,6 +3,7 @@ import UIKit import AsyncDisplayKit import Display import TelegramPresentationData +import WallpaperBackgroundNode enum ChatMessageBackgroundMergeType: Equatable { case None, Side, Top(side: Bool), Bottom, Both, Extracted @@ -65,6 +66,7 @@ class ChatMessageBackground: ASDisplayNode { private var maskMode: Bool? private let imageNode: ASImageNode private let outlineImageNode: ASImageNode + private weak var backgroundNode: WallpaperBackgroundNode? var hasImage: Bool { self.imageNode.image != nil @@ -92,19 +94,20 @@ class ChatMessageBackground: ASDisplayNode { } func setMaskMode(_ maskMode: Bool) { - 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) + if let type = self.type, let hasWallpaper = self.hasWallpaper, let highlighted = self.currentHighlighted, let graphics = self.graphics, let backgroundNode = self.backgroundNode { + self.setType(type: type, highlighted: highlighted, graphics: graphics, maskMode: maskMode, hasWallpaper: hasWallpaper, transition: .immediate, backgroundNode: backgroundNode) } } - func setType(type: ChatMessageBackgroundType, highlighted: Bool, graphics: PrincipalThemeEssentialGraphics, maskMode: Bool, hasWallpaper: Bool, transition: ContainedViewLayoutTransition) { + func setType(type: ChatMessageBackgroundType, highlighted: Bool, graphics: PrincipalThemeEssentialGraphics, maskMode: Bool, hasWallpaper: Bool, transition: ContainedViewLayoutTransition, backgroundNode: WallpaperBackgroundNode?) { let previousType = self.type - if let currentType = previousType, currentType == type, self.currentHighlighted == highlighted, self.graphics === graphics, self.maskMode == maskMode, self.hasWallpaper == hasWallpaper { + if let currentType = previousType, currentType == type, self.currentHighlighted == highlighted, self.graphics === graphics, backgroundNode === self.backgroundNode, self.maskMode == maskMode, self.hasWallpaper == hasWallpaper { return } self.type = type self.currentHighlighted = highlighted self.graphics = graphics + self.backgroundNode = backgroundNode self.hasWallpaper = hasWallpaper let image: UIImage? @@ -113,7 +116,7 @@ class ChatMessageBackground: ASDisplayNode { case .none: image = nil case let .incoming(mergeType): - if maskMode && graphics.incomingBubbleGradientImage != nil { + if maskMode, let backgroundNode = backgroundNode, backgroundNode.hasBubbleBackground(for: .incoming) { image = nil } else { switch mergeType { @@ -136,7 +139,7 @@ class ChatMessageBackground: ASDisplayNode { } } case let .outgoing(mergeType): - if maskMode && graphics.outgoingBubbleGradientImage != nil { + if maskMode, let backgroundNode = backgroundNode, backgroundNode.hasBubbleBackground(for: .outgoing) { image = nil } else { switch mergeType { @@ -226,13 +229,32 @@ class ChatMessageBackground: ASDisplayNode { } } 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) + if (previousContents as AnyObject) !== image.cgImage { + self.imageNode.layer.animate(from: previousContents as AnyObject, to: image.cgImage! as AnyObject, keyPath: "contents", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, duration: 0.42) + } } } self.imageNode.image = image self.outlineImageNode.image = outlineImage } + + func animateFrom(sourceView: UIView, transition: CombinedTransition) { + if transition.isAnimated { + self.imageNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1) + self.outlineImageNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1) + + self.view.addSubview(sourceView) + + sourceView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false, completion: { [weak sourceView] _ in + sourceView?.removeFromSuperview() + }) + + transition.animateFrame(layer: self.imageNode.layer, from: sourceView.frame) + transition.animateFrame(layer: self.outlineImageNode.layer, from: sourceView.frame) + transition.updateFrame(layer: sourceView.layer, frame: CGRect(origin: self.imageNode.frame.origin, size: CGSize(width: self.imageNode.frame.width - 7.0, height: self.imageNode.frame.height))) + } + } } final class ChatMessageShadowNode: ASDisplayNode { diff --git a/submodules/TelegramUI/Sources/ChatMessageBubbleBackdrop.swift b/submodules/TelegramUI/Sources/ChatMessageBubbleBackdrop.swift index 925d55c693..f4a2c7d66b 100644 --- a/submodules/TelegramUI/Sources/ChatMessageBubbleBackdrop.swift +++ b/submodules/TelegramUI/Sources/ChatMessageBubbleBackdrop.swift @@ -3,6 +3,7 @@ import AsyncDisplayKit import Display import Postbox import TelegramPresentationData +import WallpaperBackgroundNode private let maskInset: CGFloat = 1.0 @@ -54,17 +55,21 @@ func bubbleMaskForType(_ type: ChatMessageBackgroundType, graphics: PrincipalThe } final class ChatMessageBubbleBackdrop: ASDisplayNode { - private let backgroundContent: ASDisplayNode + private var backgroundContent: WallpaperBackgroundNode.BubbleBackgroundNode? private var currentType: ChatMessageBackgroundType? private var currentMaskMode: Bool? private var theme: ChatPresentationThemeData? private var essentialGraphics: PrincipalThemeEssentialGraphics? + private weak var backgroundNode: WallpaperBackgroundNode? private var maskView: UIImageView? + private var fixedMaskMode: Bool? + + private var absolutePosition: (CGRect, CGSize)? var hasImage: Bool { - return self.backgroundContent.contents != nil + return self.backgroundContent != nil } override var frame: CGRect { @@ -75,30 +80,40 @@ final class ChatMessageBubbleBackdrop: ASDisplayNode { maskView.frame = maskFrame } } + if let backgroundContent = self.backgroundContent { + backgroundContent.frame = self.bounds + if let (rect, containerSize) = self.absolutePosition { + var backgroundFrame = backgroundContent.frame + backgroundFrame.origin.x += rect.minX + backgroundFrame.origin.y += rect.minY + backgroundContent.update(rect: backgroundFrame, within: containerSize) + } + } } } override init() { - self.backgroundContent = ASDisplayNode() - super.init() self.clipsToBounds = true - - self.addSubnode(self.backgroundContent) } func setMaskMode(_ maskMode: Bool, mediaBox: MediaBox) { - if let currentType = self.currentType, let theme = self.theme, let essentialGraphics = self.essentialGraphics { - self.setType(type: currentType, theme: theme, mediaBox: mediaBox, essentialGraphics: essentialGraphics, maskMode: maskMode) + if let currentType = self.currentType, let theme = self.theme, let essentialGraphics = self.essentialGraphics, let backgroundNode = self.backgroundNode { + self.setType(type: currentType, theme: theme, essentialGraphics: essentialGraphics, maskMode: maskMode, backgroundNode: backgroundNode) } } - func setType(type: ChatMessageBackgroundType, theme: ChatPresentationThemeData, mediaBox: MediaBox, essentialGraphics: PrincipalThemeEssentialGraphics, maskMode: Bool) { - if self.currentType != type || self.theme != theme || self.currentMaskMode != maskMode || self.essentialGraphics !== essentialGraphics { + func setType(type: ChatMessageBackgroundType, theme: ChatPresentationThemeData, essentialGraphics: PrincipalThemeEssentialGraphics, maskMode inputMaskMode: Bool, backgroundNode: WallpaperBackgroundNode?) { + let maskMode = self.fixedMaskMode ?? inputMaskMode + + if self.currentType != type || self.theme != theme || self.currentMaskMode != maskMode || self.essentialGraphics !== essentialGraphics || self.backgroundNode !== backgroundNode { + let typeUpdated = self.currentType != type || self.theme != theme || self.currentMaskMode != maskMode || self.backgroundNode !== backgroundNode + self.currentType = type self.theme = theme self.essentialGraphics = essentialGraphics + self.backgroundNode = backgroundNode if maskMode != self.currentMaskMode { self.currentMaskMode = maskMode @@ -120,14 +135,51 @@ final class ChatMessageBubbleBackdrop: ASDisplayNode { } } } - - switch type { - case .none: - self.backgroundContent.contents = nil - case .incoming: - self.backgroundContent.contents = essentialGraphics.incomingBubbleGradientImage?.cgImage - case .outgoing: - self.backgroundContent.contents = essentialGraphics.outgoingBubbleGradientImage?.cgImage + + if let backgroundContent = self.backgroundContent { + backgroundContent.frame = self.bounds + if let (rect, containerSize) = self.absolutePosition { + var backgroundFrame = backgroundContent.frame + backgroundFrame.origin.x += rect.minX + backgroundFrame.origin.y += rect.minY + backgroundContent.update(rect: backgroundFrame, within: containerSize) + } + } + + if typeUpdated { + if let backgroundContent = self.backgroundContent { + self.backgroundContent = nil + backgroundContent.removeFromSupernode() + } + + switch type { + case .none: + break + case .incoming: + if let backgroundContent = backgroundNode?.makeBubbleBackground(for: .incoming) { + backgroundContent.frame = self.bounds + if let (rect, containerSize) = self.absolutePosition { + var backgroundFrame = backgroundContent.frame + backgroundFrame.origin.x += rect.minX + backgroundFrame.origin.y += rect.minY + backgroundContent.update(rect: backgroundFrame, within: containerSize) + } + self.backgroundContent = backgroundContent + self.insertSubnode(backgroundContent, at: 0) + } + case .outgoing: + if let backgroundContent = backgroundNode?.makeBubbleBackground(for: .outgoing) { + backgroundContent.frame = self.bounds + if let (rect, containerSize) = self.absolutePosition { + var backgroundFrame = backgroundContent.frame + backgroundFrame.origin.x += rect.minX + backgroundFrame.origin.y += rect.minY + backgroundContent.update(rect: backgroundFrame, within: containerSize) + } + self.backgroundContent = backgroundContent + self.insertSubnode(backgroundContent, at: 0) + } + } } if let maskView = self.maskView { @@ -137,22 +189,66 @@ final class ChatMessageBubbleBackdrop: ASDisplayNode { } func update(rect: CGRect, within containerSize: CGSize) { - self.backgroundContent.frame = CGRect(origin: CGPoint(x: -rect.minX, y: -rect.minY), size: containerSize) + self.absolutePosition = (rect, containerSize) + if let backgroundContent = self.backgroundContent { + var backgroundFrame = backgroundContent.frame + backgroundFrame.origin.x += rect.minX + backgroundFrame.origin.y += rect.minY + backgroundContent.update(rect: backgroundFrame, within: containerSize) + } } - func offset(value: CGFloat, animationCurve: ContainedViewLayoutTransitionCurve, duration: Double) { - let transition: ContainedViewLayoutTransition = .animated(duration: duration, curve: animationCurve) - transition.animatePositionAdditive(node: self.backgroundContent, offset: CGPoint(x: 0.0, y: -value)) + func offset(value: CGPoint, animationCurve: ContainedViewLayoutTransitionCurve, duration: Double) { + self.backgroundContent?.offset(value: value, animationCurve: animationCurve, duration: duration) } func offsetSpring(value: CGFloat, duration: Double, damping: CGFloat) { - self.backgroundContent.layer.animateSpring(from: NSValue(cgPoint: CGPoint(x: 0.0, y: value)), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: duration, initialVelocity: 0.0, damping: damping, additive: true) + self.backgroundContent?.offsetSpring(value: value, duration: duration, damping: damping) } - func updateFrame(_ value: CGRect, transition: ContainedViewLayoutTransition) { + func updateFrame(_ value: CGRect, transition: ContainedViewLayoutTransition, completion: @escaping () -> Void = {}) { if let maskView = self.maskView { - 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(view: maskView, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: value.size.width, height: value.size.height)).insetBy(dx: -maskInset, dy: -maskInset)) + } + if let backgroundContent = self.backgroundContent { + transition.updateFrame(layer: backgroundContent.layer, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: value.size.width, height: value.size.height))) + if let (rect, containerSize) = self.absolutePosition { + var backgroundFrame = backgroundContent.frame + backgroundFrame.origin.x += rect.minX + backgroundFrame.origin.y += rect.minY + backgroundContent.update(rect: backgroundFrame, within: containerSize, transition: transition) + } + } + transition.updateFrame(node: self, frame: value, completion: { _ in + completion() + }) + } + + func updateFrame(_ value: CGRect, transition: CombinedTransition, completion: @escaping () -> Void = {}) { + if let maskView = self.maskView { + transition.updateFrame(layer: maskView.layer, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: value.size.width, height: value.size.height)).insetBy(dx: -maskInset, dy: -maskInset)) + } + if let backgroundContent = self.backgroundContent { + transition.updateFrame(layer: backgroundContent.layer, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: value.size.width, height: value.size.height))) + if let (rect, containerSize) = self.absolutePosition { + var backgroundFrame = backgroundContent.frame + backgroundFrame.origin.x += rect.minX + backgroundFrame.origin.y += rect.minY + backgroundContent.update(rect: backgroundFrame, within: containerSize, transition: transition) + } + } + transition.updateFrame(layer: self.layer, frame: value, completion: { _ in + completion() + }) + } + + func animateFrom(sourceView: UIView, mediaBox: MediaBox, transition: CombinedTransition) { + if transition.isAnimated { + let previousFrame = self.frame + self.updateFrame(CGRect(origin: CGPoint(x: previousFrame.minX, y: sourceView.frame.minY), size: sourceView.frame.size), transition: .immediate) + self.updateFrame(previousFrame, transition: transition) + + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1) } - transition.updateFrame(node: self, frame: value) } } diff --git a/submodules/TelegramUI/Sources/ChatMessageBubbleContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageBubbleContentNode.swift index 8f716ff1eb..c91e035fe0 100644 --- a/submodules/TelegramUI/Sources/ChatMessageBubbleContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageBubbleContentNode.swift @@ -201,6 +201,15 @@ class ChatMessageBubbleContentNode: ASDisplayNode { func updateIsExtractedToContextPreview(_ value: Bool) { } + + func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) { + } + + func applyAbsoluteOffset(value: CGPoint, animationCurve: ContainedViewLayoutTransitionCurve, duration: Double) { + } + + func applyAbsoluteOffsetSpring(value: CGFloat, duration: Double, damping: CGFloat) { + } func reactionTargetNode(value: String) -> (ASDisplayNode, ASDisplayNode)? { return nil diff --git a/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift index 820fecee5a..20c41358ed 100644 --- a/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift @@ -22,6 +22,8 @@ import PersistentStringHash import GridMessageSelectionNode import AppBundle import Markdown +import WallpaperBackgroundNode +import SwiftSignalKit enum InternalBubbleTapAction { case action(() -> Void) @@ -35,6 +37,17 @@ private struct BubbleItemAttributes { var neighborSpacing: ChatMessageBubbleRelativePosition.NeighbourSpacing } +private final class ChatMessageBubbleClippingNode: ASDisplayNode { + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + let result = self.view.hitTest(point, with: event) + if result === self.view { + return nil + } else { + return result + } + } +} + private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([(Message, AnyClass, ChatMessageEntryAttributes, BubbleItemAttributes)], Bool) { var result: [(Message, AnyClass, ChatMessageEntryAttributes, BubbleItemAttributes)] = [] var skipText = false @@ -217,6 +230,14 @@ private enum ContentNodeOperation { case insert(index: Int, node: ChatMessageBubbleContentNode) } +class ChatPresentationContext { + weak var backgroundNode: WallpaperBackgroundNode? + + init(backgroundNode: WallpaperBackgroundNode?) { + self.backgroundNode = backgroundNode + } +} + class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode { class ContentContainer { let contentMessageStableId: UInt32 @@ -226,7 +247,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode var backgroundNode: ChatMessageBackground? var selectionBackgroundNode: ASDisplayNode? - private var currentParams: (size: CGSize, contentOrigin: CGPoint, presentationData: ChatPresentationData, graphics: PrincipalThemeEssentialGraphics, backgroundType: ChatMessageBackgroundType, mediaBox: MediaBox, messageSelection: Bool?, selectionInsets: UIEdgeInsets)? + private var currentParams: (size: CGSize, contentOrigin: CGPoint, presentationData: ChatPresentationData, graphics: PrincipalThemeEssentialGraphics, backgroundType: ChatMessageBackgroundType, presentationContext: ChatPresentationContext, mediaBox: MediaBox, messageSelection: Bool?, selectionInsets: UIEdgeInsets)? init(contentMessageStableId: UInt32) { self.contentMessageStableId = contentMessageStableId @@ -245,7 +266,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode backgroundWallpaperNode.update(rect: mappedRect, within: containerSize) } - fileprivate func applyAbsoluteOffset(value: CGFloat, animationCurve: ContainedViewLayoutTransitionCurve, duration: Double) { + fileprivate func applyAbsoluteOffset(value: CGPoint, animationCurve: ContainedViewLayoutTransitionCurve, duration: Double) { guard let backgroundWallpaperNode = self.backgroundWallpaperNode else { return } @@ -290,8 +311,8 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode transition.updateAlpha(node: backgroundNode, alpha: 1.0) transition.updateAlpha(node: backgroundWallpaperNode, alpha: 1.0) - backgroundNode.setType(type: type, highlighted: false, graphics: currentParams.graphics, maskMode: true, hasWallpaper: currentParams.presentationData.theme.wallpaper.hasWallpaper, transition: .immediate) - backgroundWallpaperNode.setType(type: type, theme: currentParams.presentationData.theme, mediaBox: currentParams.mediaBox, essentialGraphics: currentParams.graphics, maskMode: true) + backgroundNode.setType(type: type, highlighted: false, graphics: currentParams.graphics, maskMode: true, hasWallpaper: currentParams.presentationData.theme.wallpaper.hasWallpaper, transition: .immediate, backgroundNode: currentParams.presentationContext.backgroundNode) + backgroundWallpaperNode.setType(type: type, theme: currentParams.presentationData.theme, essentialGraphics: currentParams.graphics, maskMode: true, backgroundNode: currentParams.presentationContext.backgroundNode) } if let currentParams = self.currentParams { @@ -324,8 +345,8 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode fileprivate func isExtractedToContextPreviewUpdated(_ isExtractedToContextPreview: Bool) { } - fileprivate func update(size: CGSize, contentOrigin: CGPoint, selectionInsets: UIEdgeInsets, index: Int, presentationData: ChatPresentationData, graphics: PrincipalThemeEssentialGraphics, backgroundType: ChatMessageBackgroundType, mediaBox: MediaBox, messageSelection: Bool?) { - self.currentParams = (size, contentOrigin, presentationData, graphics, backgroundType, mediaBox, messageSelection, selectionInsets) + fileprivate func update(size: CGSize, contentOrigin: CGPoint, selectionInsets: UIEdgeInsets, index: Int, presentationData: ChatPresentationData, graphics: PrincipalThemeEssentialGraphics, backgroundType: ChatMessageBackgroundType, presentationContext: ChatPresentationContext, mediaBox: MediaBox, messageSelection: Bool?) { + self.currentParams = (size, contentOrigin, presentationData, graphics, backgroundType, presentationContext, mediaBox, messageSelection, selectionInsets) let bounds = CGRect(origin: CGPoint(), size: size) var incoming: Bool = false @@ -362,12 +383,12 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode } } - private let mainContextSourceNode: ContextExtractedContentContainingNode + let mainContextSourceNode: ContextExtractedContentContainingNode private let mainContainerNode: ContextControllerSourceNode private let backgroundWallpaperNode: ChatMessageBubbleBackdrop private let backgroundNode: ChatMessageBackground private let shadowNode: ChatMessageShadowNode - private var transitionClippingNode: ASDisplayNode? + private var clippingNode: ChatMessageBubbleClippingNode override var extractedBackgroundNode: ASDisplayNode? { return self.shadowNode @@ -393,7 +414,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode private var mosaicStatusNode: ChatMessageDateAndStatusNode? private var actionButtonsNode: ChatMessageActionButtonsNode? - private var shareButtonNode: HighlightableButtonNode? + private var shareButtonNode: ChatMessageShareButton? private let messageAccessibilityArea: AccessibilityAreaNode @@ -430,6 +451,10 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode self.backgroundNode = ChatMessageBackground() self.shadowNode = ChatMessageShadowNode() + + self.clippingNode = ChatMessageBubbleClippingNode() + self.clippingNode.clipsToBounds = true + self.messageAccessibilityArea = AccessibilityAreaNode() super.init(layerBacked: false) @@ -481,7 +506,8 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode self.mainContextSourceNode.contentNode.addSubnode(self.backgroundWallpaperNode) self.mainContextSourceNode.contentNode.addSubnode(self.backgroundNode) - self.mainContextSourceNode.contentNode.addSubnode(self.contentContainersWrapperNode) + self.mainContextSourceNode.contentNode.addSubnode(self.clippingNode) + self.clippingNode.addSubnode(self.contentContainersWrapperNode) self.addSubnode(self.messageAccessibilityArea) self.messageAccessibilityArea.activate = { [weak self] in @@ -553,6 +579,40 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } + + override func cancelInsertionAnimations() { + self.shadowNode.layer.removeAllAnimations() + + func process(node: ASDisplayNode) { + if node === self.accessoryItemNode { + return + } + + if node !== self { + switch node { + case let node as ContextExtractedContentContainingNode: + process(node: node.contentNode) + return + case _ as ContextControllerSourceNode, _ as ContextExtractedContentNode: + break + default: + node.layer.removeAllAnimations() + node.layer.allowsGroupOpacity = false + return + } + } + + guard let subnodes = node.subnodes else { + return + } + + for subnode in subnodes { + process(node: subnode) + } + } + + process(node: self) + } override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { super.animateInsertion(currentTimestamp, duration: duration, short: short) @@ -614,6 +674,63 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode } } } + + func animateContentFromTextInputField(textInput: ChatMessageTransitionNode.Source.TextInput, transition: CombinedTransition) { + guard let item = self.item else { + return + } + let widthDifference = self.backgroundNode.frame.width - textInput.backgroundView.frame.width + let heightDifference = self.backgroundNode.frame.height - textInput.backgroundView.frame.height + + transition.animateFrame(layer: self.clippingNode.layer, from: CGRect(origin: CGPoint(x: self.clippingNode.frame.minX, y: textInput.backgroundView.frame.minY), size: textInput.backgroundView.frame.size)) + + transition.vertical.animateOffsetAdditive(layer: self.clippingNode.layer, offset: textInput.backgroundView.frame.minY - self.clippingNode.frame.minY) + + self.backgroundWallpaperNode.animateFrom(sourceView: textInput.backgroundView, mediaBox: item.context.account.postbox.mediaBox, transition: transition) + self.backgroundNode.animateFrom(sourceView: textInput.backgroundView, transition: transition) + + for contentNode in self.contentNodes { + if let contentNode = contentNode as? ChatMessageTextBubbleContentNode { + let localSourceContentFrame = self.mainContextSourceNode.contentNode.view.convert(textInput.contentView.frame.offsetBy(dx: self.mainContextSourceNode.contentRect.minX, dy: self.mainContextSourceNode.contentRect.minY), to: contentNode.view) + textInput.contentView.frame = localSourceContentFrame + contentNode.animateFrom(sourceView: textInput.contentView, scrollOffset: textInput.scrollOffset, widthDifference: widthDifference, transition: transition) + } else if let contentNode = contentNode as? ChatMessageWebpageBubbleContentNode { + transition.vertical.animatePositionAdditive(node: contentNode, offset: CGPoint(x: 0.0, y: heightDifference)) + } + } + } + + func animateReplyPanel(sourceReplyPanel: ChatMessageTransitionNode.ReplyPanel, transition: CombinedTransition) { + if let replyInfoNode = self.replyInfoNode { + let localRect = self.mainContextSourceNode.contentNode.view.convert(sourceReplyPanel.relativeSourceRect, to: replyInfoNode.view) + let _ = replyInfoNode.animateFromInputPanel(sourceReplyPanel: sourceReplyPanel, unclippedTransitionNode: self.mainContextSourceNode.contentNode, localRect: localRect, transition: transition) + } + } + + func animateFromMicInput(micInputNode: UIView, transition: CombinedTransition) -> ContextExtractedContentContainingNode? { + for contentNode in self.contentNodes { + if let contentNode = contentNode as? ChatMessageFileBubbleContentNode { + let statusContainerNode = contentNode.interactiveFileNode.statusContainerNode + let scale = statusContainerNode.contentRect.height / 100.0 + micInputNode.transform = CGAffineTransform(scaleX: scale, y: scale) + micInputNode.center = CGPoint(x: statusContainerNode.contentRect.midX, y: statusContainerNode.contentRect.midY) + statusContainerNode.contentNode.view.addSubview(micInputNode) + + transition.horizontal.updateAlpha(layer: micInputNode.layer, alpha: 0.0, completion: { [weak micInputNode] _ in + micInputNode?.removeFromSuperview() + }) + + transition.horizontal.animateTransformScale(node: statusContainerNode.contentNode, from: 1.0 / scale) + + return statusContainerNode + } + } + return nil + } + + func animateContentFromMediaInput(snapshotView: UIView, transition: CombinedTransition) { + self.mainContextSourceNode.contentNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1) + } override func didLoad() { super.didLoad() @@ -950,8 +1067,6 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode let mosaicStatusLayout = ChatMessageDateAndStatusNode.asyncLayout(self.mosaicStatusNode) - let currentShareButtonNode = self.shareButtonNode - let layoutConstants = self.layoutConstants let currentItem = self.appliedItem @@ -971,7 +1086,6 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode replyInfoLayout: replyInfoLayout, actionButtonsLayout: actionButtonsLayout, mosaicStatusLayout: mosaicStatusLayout, - currentShareButtonNode: currentShareButtonNode, layoutConstants: layoutConstants, currentItem: currentItem, currentForwardInfo: currentForwardInfo, @@ -988,7 +1102,6 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode replyInfoLayout: (ChatPresentationData, PresentationStrings, AccountContext, ChatMessageReplyInfoType, Message, CGSize) -> (CGSize, () -> ChatMessageReplyInfoNode), actionButtonsLayout: (AccountContext, ChatPresentationThemeData, PresentationChatBubbleCorners, PresentationStrings, ReplyMarkupMessageAttribute, Message, CGFloat) -> (minWidth: CGFloat, layout: (CGFloat) -> (CGSize, (Bool) -> ChatMessageActionButtonsNode)), mosaicStatusLayout: (AccountContext, ChatPresentationData, Bool, Int?, String, ChatMessageDateAndStatusType, CGSize, [MessageReaction], Int, Bool, Bool) -> (CGSize, (Bool) -> ChatMessageDateAndStatusNode), - currentShareButtonNode: HighlightableButtonNode?, layoutConstants: ChatMessageItemLayoutConstants, currentItem: ChatMessageItem?, currentForwardInfo: (Peer?, String?)?, @@ -1041,7 +1154,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode ignoreForward = true effectiveAuthor = forwardInfo.author if effectiveAuthor == nil, let authorSignature = forwardInfo.authorSignature { - effectiveAuthor = TelegramUser(id: PeerId(namespace: Namespaces.Peer.Empty, id: Int32(clamping: authorSignature.persistentHashValue)), accessHash: nil, firstName: authorSignature, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: UserInfoFlags()) + effectiveAuthor = TelegramUser(id: PeerId(namespace: Namespaces.Peer.Empty, id: PeerId.Id._internalFromInt32Value(Int32(clamping: authorSignature.persistentHashValue))), accessHash: nil, firstName: authorSignature, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: UserInfoFlags()) } } displayAuthorInfo = !mergedTop.merged && incoming && effectiveAuthor != nil @@ -1057,7 +1170,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode displayAuthorInfo = !mergedTop.merged && incoming } else if let forwardInfo = item.content.firstMessage.forwardInfo, forwardInfo.flags.contains(.isImported), let authorSignature = forwardInfo.authorSignature { ignoreForward = true - effectiveAuthor = TelegramUser(id: PeerId(namespace: Namespaces.Peer.Empty, id: Int32(clamping: authorSignature.persistentHashValue)), accessHash: nil, firstName: authorSignature, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: UserInfoFlags()) + effectiveAuthor = TelegramUser(id: PeerId(namespace: Namespaces.Peer.Empty, id: PeerId.Id._internalFromInt32Value(Int32(clamping: authorSignature.persistentHashValue))), accessHash: nil, firstName: authorSignature, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: UserInfoFlags()) displayAuthorInfo = !mergedTop.merged && incoming } else { effectiveAuthor = firstMessage.author @@ -1282,8 +1395,10 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode var backgroundHiding: ChatMessageBubbleContentBackgroundHiding? var hasSolidWallpaper = false switch item.presentationData.theme.wallpaper { - case .color, .gradient: + case .color: hasSolidWallpaper = true + case let .gradient(_, colors, _): + hasSolidWallpaper = colors.count <= 2 default: break } @@ -1437,12 +1552,12 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode if initialDisplayHeader && displayAuthorInfo { if let peer = firstMessage.peers[firstMessage.id.peerId] as? TelegramChannel, case .broadcast = peer.info { authorNameString = peer.displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder) - authorNameColor = chatMessagePeerIdColors[Int(peer.id.id % 7)] + authorNameColor = chatMessagePeerIdColors[Int(peer.id.id._internalGetInt32Value() % 7)] } else if let effectiveAuthor = effectiveAuthor { authorNameString = effectiveAuthor.displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder) if incoming { - authorNameColor = chatMessagePeerIdColors[Int(effectiveAuthor.id.id % 7)] + authorNameColor = chatMessagePeerIdColors[Int(effectiveAuthor.id.id._internalGetInt32Value() % 7)] } else { authorNameColor = item.presentationData.theme.theme.chat.message.outgoing.accentTextColor } @@ -2035,41 +2150,9 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode layoutInsets.top += layoutConstants.timestampHeaderHeight } - var updatedShareButtonBackground: UIImage? - - var updatedShareButtonNode: HighlightableButtonNode? - if needShareButton { - 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, bubbleCorners: item.presentationData.chatBubbleCorners) - if case .pinnedMessages = item.associatedData.subject { - updatedShareButtonBackground = graphics.chatBubbleNavigateButtonImage - } else if item.message.id.peerId.isRepliesOrSavedMessages(accountPeerId: item.context.account.peerId) { - updatedShareButtonBackground = graphics.chatBubbleNavigateButtonImage - } else { - updatedShareButtonBackground = graphics.chatBubbleShareButtonImage - } - } - } else { - let buttonNode = HighlightableButtonNode() - let buttonIcon: UIImage? - let graphics = PresentationResourcesChat.additionalGraphics(item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper, bubbleCorners: item.presentationData.chatBubbleCorners) - if case .pinnedMessages = item.associatedData.subject { - buttonIcon = graphics.chatBubbleNavigateButtonImage - } else if item.message.id.peerId.isRepliesOrSavedMessages(accountPeerId: item.context.account.peerId) { - buttonIcon = graphics.chatBubbleNavigateButtonImage - } else { - buttonIcon = graphics.chatBubbleShareButtonImage - } - buttonNode.setBackgroundImage(buttonIcon, for: [.normal]) - updatedShareButtonNode = buttonNode - } - } - 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, bubbleCorners: item.presentationData.chatBubbleCorners) + let graphics = PresentationResourcesChat.principalGraphics(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper, bubbleCorners: item.presentationData.chatBubbleCorners) var updatedMergedTop = mergedBottom var updatedMergedBottom = mergedTop @@ -2099,6 +2182,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode hideBackground: hideBackground, incoming: incoming, graphics: graphics, + presentationContext: item.controllerInteraction.presentationContext, bubbleContentWidth: bubbleContentWidth, backgroundFrame: backgroundFrame, deliveryFailedInset: deliveryFailedInset, @@ -2120,8 +2204,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode contentContainerNodeFrames: contentContainerNodeFrames, mosaicStatusOrigin: mosaicStatusOrigin, mosaicStatusSizeAndApply: mosaicStatusSizeAndApply, - updatedShareButtonNode: updatedShareButtonNode, - updatedShareButtonBackground: updatedShareButtonBackground + needsShareButton: needShareButton ) }) } @@ -2139,6 +2222,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode hideBackground: Bool, incoming: Bool, graphics: PrincipalThemeEssentialGraphics, + presentationContext: ChatPresentationContext, bubbleContentWidth: CGFloat, backgroundFrame: CGRect, deliveryFailedInset: CGFloat, @@ -2160,8 +2244,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode contentContainerNodeFrames: [(UInt32, CGRect, Bool?, CGFloat)], mosaicStatusOrigin: CGPoint?, mosaicStatusSizeAndApply: (CGSize, (Bool) -> ChatMessageDateAndStatusNode)?, - updatedShareButtonNode: HighlightableButtonNode?, - updatedShareButtonBackground: UIImage? + needsShareButton: Bool ) -> Void { guard let strongSelf = selfReference.value else { return @@ -2197,9 +2280,14 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode backgroundType = .incoming(mergeType) } 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.backgroundNode.setType(type: backgroundType, highlighted: strongSelf.highlightedState, graphics: graphics, maskMode: strongSelf.backgroundMaskMode, hasWallpaper: hasWallpaper, transition: transition, backgroundNode: presentationContext.backgroundNode) + strongSelf.backgroundWallpaperNode.setType(type: backgroundType, theme: item.presentationData.theme, essentialGraphics: graphics, maskMode: strongSelf.backgroundMaskMode, backgroundNode: presentationContext.backgroundNode) strongSelf.shadowNode.setType(type: backgroundType, hasWallpaper: hasWallpaper, graphics: graphics) + if case .none = backgroundType { + strongSelf.clippingNode.clipsToBounds = false + } else { + strongSelf.clippingNode.clipsToBounds = true + } strongSelf.backgroundType = backgroundType @@ -2241,7 +2329,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode if !nameNode.isNodeLoaded { nameNode.isUserInteractionEnabled = false } - strongSelf.mainContextSourceNode.contentNode.addSubnode(nameNode) + strongSelf.clippingNode.addSubnode(nameNode) } nameNode.frame = CGRect(origin: CGPoint(x: contentOrigin.x + layoutConstants.text.bubbleInsets.left, y: layoutConstants.bubble.contentInsets.top + nameNodeOriginY), size: nameNodeSizeApply.0) nameNode.displaysAsynchronously = !item.presentationData.isPreview @@ -2253,7 +2341,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode } else { credibilityIconNode = ASImageNode() strongSelf.credibilityIconNode = credibilityIconNode - strongSelf.mainContextSourceNode.contentNode.addSubnode(credibilityIconNode) + strongSelf.clippingNode.addSubnode(credibilityIconNode) } credibilityIconNode.frame = CGRect(origin: CGPoint(x: nameNode.frame.maxX + 4.0, y: nameNode.frame.minY), size: credibilityIconImage.size) credibilityIconNode.image = credibilityIconImage @@ -2269,7 +2357,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode if !adminBadgeNode.isNodeLoaded { adminBadgeNode.isUserInteractionEnabled = false } - strongSelf.mainContextSourceNode.contentNode.addSubnode(adminBadgeNode) + strongSelf.clippingNode.addSubnode(adminBadgeNode) adminBadgeNode.frame = adminBadgeFrame } else { let previousAdminBadgeFrame = adminBadgeNode.frame @@ -2291,7 +2379,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode strongSelf.forwardInfoNode = forwardInfoNode var animateFrame = true if forwardInfoNode.supernode == nil { - strongSelf.mainContextSourceNode.contentNode.addSubnode(forwardInfoNode) + strongSelf.clippingNode.addSubnode(forwardInfoNode) animateFrame = false forwardInfoNode.openPsa = { [weak strongSelf] type, sourceNode in guard let strongSelf = strongSelf, let item = strongSelf.item else { @@ -2316,7 +2404,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode strongSelf.replyInfoNode = replyInfoNode var animateFrame = true if replyInfoNode.supernode == nil { - strongSelf.mainContextSourceNode.contentNode.addSubnode(replyInfoNode) + strongSelf.clippingNode.addSubnode(replyInfoNode) animateFrame = false } let previousReplyInfoNodeFrame = replyInfoNode.frame @@ -2474,7 +2562,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode selectionInsets.bottom = groupOverlap / 2.0 } - contentContainer?.update(size: relativeFrame.size, contentOrigin: contentOrigin, selectionInsets: selectionInsets, index: index, presentationData: item.presentationData, graphics: graphics, backgroundType: backgroundType, mediaBox: item.context.account.postbox.mediaBox, messageSelection: itemSelection) + contentContainer?.update(size: relativeFrame.size, contentOrigin: contentOrigin, selectionInsets: selectionInsets, index: index, presentationData: item.presentationData, graphics: graphics, backgroundType: backgroundType, presentationContext: item.controllerInteraction.presentationContext, mediaBox: item.context.account.postbox.mediaBox, messageSelection: itemSelection) index += 1 } @@ -2518,12 +2606,15 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode updatedContentNodes.append(contentNode) let contextSourceNode: ContextExtractedContentContainingNode + let containerSupernode: ASDisplayNode if isAttachent { contextSourceNode = strongSelf.mainContextSourceNode + containerSupernode = strongSelf.clippingNode } else { contextSourceNode = strongSelf.contentContainers.first(where: { $0.contentMessageStableId == contentNodeMessage.stableId })?.sourceNode ?? strongSelf.mainContextSourceNode + containerSupernode = strongSelf.contentContainers.first(where: { $0.contentMessageStableId == contentNodeMessage.stableId })?.sourceNode.contentNode ?? strongSelf.clippingNode } - contextSourceNode.contentNode.addSubnode(contentNode) + containerSupernode.addSubnode(contentNode) contentNode.visibility = strongSelf.visibility contentNode.updateIsTextSelectionActive = { [weak contextSourceNode] value in @@ -2599,7 +2690,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode if mosaicStatusNode !== strongSelf.mosaicStatusNode { strongSelf.mosaicStatusNode?.removeFromSupernode() strongSelf.mosaicStatusNode = mosaicStatusNode - strongSelf.mainContextSourceNode.contentNode.addSubnode(mosaicStatusNode) + strongSelf.clippingNode.addSubnode(mosaicStatusNode) } let absoluteOrigin = mosaicStatusOrigin.offsetBy(dx: contentOrigin.x, dy: contentOrigin.y) mosaicStatusNode.frame = CGRect(origin: CGPoint(x: absoluteOrigin.x - layoutConstants.image.statusInsets.right - size.width, y: absoluteOrigin.y - layoutConstants.image.statusInsets.bottom - size.height), size: size) @@ -2607,32 +2698,28 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode strongSelf.mosaicStatusNode = nil mosaicStatusNode.removeFromSupernode() } - - if let updatedShareButtonNode = updatedShareButtonNode { - if updatedShareButtonNode !== strongSelf.shareButtonNode { - if let shareButtonNode = strongSelf.shareButtonNode { - shareButtonNode.removeFromSupernode() - } - strongSelf.shareButtonNode = updatedShareButtonNode - strongSelf.insertSubnode(updatedShareButtonNode, belowSubnode: strongSelf.messageAccessibilityArea) - updatedShareButtonNode.addTarget(strongSelf, action: #selector(strongSelf.shareButtonPressed), forControlEvents: .touchUpInside) - } - if let updatedShareButtonBackground = updatedShareButtonBackground { - strongSelf.shareButtonNode?.setBackgroundImage(updatedShareButtonBackground, for: [.normal]) + + if needsShareButton { + if strongSelf.shareButtonNode == nil { + let shareButtonNode = ChatMessageShareButton() + strongSelf.shareButtonNode = shareButtonNode + strongSelf.insertSubnode(shareButtonNode, belowSubnode: strongSelf.messageAccessibilityArea) + shareButtonNode.addTarget(strongSelf, action: #selector(strongSelf.shareButtonPressed), forControlEvents: .touchUpInside) } + } else if let shareButtonNode = strongSelf.shareButtonNode { - shareButtonNode.removeFromSupernode() strongSelf.shareButtonNode = nil + shareButtonNode.removeFromSupernode() } if case .System = animation, !strongSelf.mainContextSourceNode.isExtractedToContextPreview { if !strongSelf.backgroundNode.frame.equalTo(backgroundFrame) { strongSelf.backgroundFrameTransition = (strongSelf.backgroundNode.frame, backgroundFrame) - strongSelf.enableTransitionClippingNode() } if let shareButtonNode = strongSelf.shareButtonNode { let currentBackgroundFrame = strongSelf.backgroundNode.frame - shareButtonNode.frame = CGRect(origin: CGPoint(x: currentBackgroundFrame.maxX + 8.0, y: currentBackgroundFrame.maxY - 30.0), size: CGSize(width: 29.0, height: 29.0)) + let buttonSize = shareButtonNode.update(presentationData: item.presentationData, chatLocation: item.chatLocation, subject: item.associatedData.subject, message: item.message, account: item.context.account, disableComments: true) + shareButtonNode.frame = CGRect(origin: CGPoint(x: currentBackgroundFrame.maxX + 8.0, y: currentBackgroundFrame.maxY - buttonSize.width - 1.0), size: buttonSize) } } else { if let _ = strongSelf.backgroundFrameTransition { @@ -2641,17 +2728,23 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode } strongSelf.messageAccessibilityArea.frame = backgroundFrame if let shareButtonNode = strongSelf.shareButtonNode { - shareButtonNode.frame = CGRect(origin: CGPoint(x: backgroundFrame.maxX + 8.0, y: backgroundFrame.maxY - 30.0), size: CGSize(width: 29.0, height: 29.0)) + let buttonSize = shareButtonNode.update(presentationData: item.presentationData, chatLocation: item.chatLocation, subject: item.associatedData.subject, message: item.message, account: item.context.account, disableComments: true) + shareButtonNode.frame = CGRect(origin: CGPoint(x: backgroundFrame.maxX + 8.0, y: backgroundFrame.maxY - buttonSize.width - 1.0), size: buttonSize) } - strongSelf.disableTransitionClippingNode() if case .System = animation, strongSelf.mainContextSourceNode.isExtractedToContextPreview { transition.updateFrame(node: strongSelf.backgroundNode, frame: backgroundFrame) + + transition.updateFrame(node: strongSelf.clippingNode, frame: backgroundFrame) + transition.updateBounds(node: strongSelf.clippingNode, bounds: CGRect(origin: CGPoint(x: backgroundFrame.minX, y: backgroundFrame.minY), size: backgroundFrame.size)) + 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.clippingNode.frame = backgroundFrame + strongSelf.clippingNode.bounds = CGRect(origin: CGPoint(x: backgroundFrame.minX, y: backgroundFrame.minY), size: backgroundFrame.size) strongSelf.backgroundNode.updateLayout(size: backgroundFrame.size, transition: .immediate) strongSelf.backgroundWallpaperNode.frame = backgroundFrame strongSelf.shadowNode.updateLayout(backgroundFrame: backgroundFrame, transition: .immediate) @@ -2709,7 +2802,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode strongSelf.updateSearchTextHighlightState() - if let (awaitingAppliedReaction, f) = strongSelf.awaitingAppliedReaction { + /*if let (awaitingAppliedReaction, f) = strongSelf.awaitingAppliedReaction { var bounds = strongSelf.bounds let offset = bounds.origin.x bounds.origin.x = 0.0 @@ -2746,7 +2839,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode } strongSelf.reactionRecognizer?.complete(into: targetNode, hideTarget: hideTarget)*/ f() - } + }*/ } override func updateAccessibilityData(_ accessibilityData: ChatMessageAccessibilityData) { @@ -2789,52 +2882,6 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode } } - private func enableTransitionClippingNode() { - if self.transitionClippingNode == nil { - let node = ASDisplayNode() - node.clipsToBounds = true - var backgroundFrame = self.backgroundNode.frame - backgroundFrame = backgroundFrame.insetBy(dx: 0.0, dy: 1.0) - node.frame = backgroundFrame - node.bounds = CGRect(origin: CGPoint(x: backgroundFrame.origin.x, y: backgroundFrame.origin.y), size: backgroundFrame.size) - if let forwardInfoNode = self.forwardInfoNode { - node.addSubnode(forwardInfoNode) - } - if let replyInfoNode = self.replyInfoNode { - node.addSubnode(replyInfoNode) - } - if !self.contentContainers.isEmpty { - node.addSubnode(self.contentContainersWrapperNode) - } else { - for contentNode in self.contentNodes { - node.addSubnode(contentNode) - } - } - self.mainContextSourceNode.contentNode.addSubnode(node) - self.transitionClippingNode = node - } - } - - private func disableTransitionClippingNode() { - if let transitionClippingNode = self.transitionClippingNode { - if let forwardInfoNode = self.forwardInfoNode { - self.mainContextSourceNode.contentNode.addSubnode(forwardInfoNode) - } - if let replyInfoNode = self.replyInfoNode { - self.mainContextSourceNode.contentNode.addSubnode(replyInfoNode) - } - if !self.contentContainers.isEmpty { - self.mainContextSourceNode.contentNode.addSubnode(self.contentContainersWrapperNode) - } else { - for contentNode in self.contentNodes { - self.mainContextSourceNode.contentNode.addSubnode(contentNode) - } - } - transitionClippingNode.removeFromSupernode() - self.transitionClippingNode = nil - } - } - override func shouldAnimateHorizontalFrameTransition() -> Bool { if let _ = self.backgroundFrameTransition { return true @@ -2849,6 +2896,10 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode if let backgroundFrameTransition = self.backgroundFrameTransition { let backgroundFrame = CGRect.interpolator()(backgroundFrameTransition.0, backgroundFrameTransition.1, progress) as! CGRect self.backgroundNode.frame = backgroundFrame + + self.clippingNode.frame = backgroundFrame + self.clippingNode.bounds = CGRect(origin: CGPoint(x: backgroundFrame.minX, y: backgroundFrame.minY), size: backgroundFrame.size) + self.backgroundNode.updateLayout(size: backgroundFrame.size, transition: .immediate) self.backgroundWallpaperNode.frame = backgroundFrame self.shadowNode.updateLayout(backgroundFrame: backgroundFrame, transition: .immediate) @@ -2871,20 +2922,9 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode } self.messageAccessibilityArea.frame = backgroundFrame - if let shareButtonNode = self.shareButtonNode { - shareButtonNode.frame = CGRect(origin: CGPoint(x: backgroundFrame.maxX + 8.0, y: backgroundFrame.maxY - 30.0), size: CGSize(width: 29.0, height: 29.0)) - } - - if let transitionClippingNode = self.transitionClippingNode { - var fixedBackgroundFrame = backgroundFrame - fixedBackgroundFrame = fixedBackgroundFrame.insetBy(dx: 0.0, dy: self.backgroundNode.type == ChatMessageBackgroundType.none ? 0.0 : 1.0) - - transitionClippingNode.frame = fixedBackgroundFrame - transitionClippingNode.bounds = CGRect(origin: CGPoint(x: fixedBackgroundFrame.origin.x, y: fixedBackgroundFrame.origin.y), size: fixedBackgroundFrame.size) - - if progress >= 1.0 - CGFloat.ulpOfOne { - self.disableTransitionClippingNode() - } + if let item = self.item, let shareButtonNode = self.shareButtonNode { + let buttonSize = shareButtonNode.update(presentationData: item.presentationData, chatLocation: item.chatLocation, subject: item.associatedData.subject, message: item.message, account: item.context.account, disableComments: true) + shareButtonNode.frame = CGRect(origin: CGPoint(x: backgroundFrame.maxX + 8.0, y: backgroundFrame.maxY - buttonSize.width - 1.0), size: buttonSize) } if CGFloat(1.0).isLessThanOrEqualTo(progress) { @@ -2971,7 +3011,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode for attribute in item.content.firstMessage.attributes { if let attribute = attribute as? SourceReferenceMessageAttribute { openPeerId = attribute.messageId.peerId - navigate = .chat(textInputState: nil, subject: .message(id: attribute.messageId, highlight: true), peekData: nil) + navigate = .chat(textInputState: nil, subject: .message(id: attribute.messageId, highlight: true, timecode: nil), peekData: nil) } } @@ -3564,10 +3604,10 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode 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, bubbleCorners: item.presentationData.chatBubbleCorners) + let graphics = PresentationResourcesChat.principalGraphics(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper, bubbleCorners: item.presentationData.chatBubbleCorners) let hasWallpaper = item.presentationData.theme.wallpaper.hasWallpaper - self.backgroundNode.setType(type: backgroundType, highlighted: highlighted, graphics: graphics, maskMode: self.mainContextSourceNode.isExtractedToContextPreview, hasWallpaper: hasWallpaper, transition: animated ? .animated(duration: 0.3, curve: .easeInOut) : .immediate) + self.backgroundNode.setType(type: backgroundType, highlighted: highlighted, graphics: graphics, maskMode: self.mainContextSourceNode.isExtractedToContextPreview, hasWallpaper: hasWallpaper, transition: animated ? .animated(duration: 0.3, curve: .easeInOut) : .immediate, backgroundNode: item.controllerInteraction.presentationContext.backgroundNode) } } } @@ -3606,7 +3646,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode if translation.x < -45.0, self.swipeToReplyNode == nil, let item = self.item { self.swipeToReplyFeedback?.impact() - 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), action: ChatMessageSwipeToReplyNode.Action(self.currentSwipeAction)) + let swipeToReplyNode = ChatMessageSwipeToReplyNode(fillColor: selectDateFillStaticColor(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper), enableBlur: dateFillNeedsBlur(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper), foregroundColor: bubbleVariableColor(variableColor: item.presentationData.theme.theme.chat.message.shareButtonForegroundColor, wallpaper: item.presentationData.theme.wallpaper), action: ChatMessageSwipeToReplyNode.Action(self.currentSwipeAction)) self.swipeToReplyNode = swipeToReplyNode self.insertSubnode(swipeToReplyNode, belowSubnode: self.messageAccessibilityArea) animateReplyNodeIn = true @@ -3683,22 +3723,35 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode } private func updateAbsoluteRectInternal(_ rect: CGRect, within containerSize: CGSize) { - let mappedRect = CGRect(origin: CGPoint(x: rect.minX + self.backgroundWallpaperNode.frame.minX, y: rect.minY + self.backgroundWallpaperNode.frame.minY), size: rect.size) - self.backgroundWallpaperNode.update(rect: mappedRect, within: containerSize) - } - - override func applyAbsoluteOffset(value: CGFloat, animationCurve: ContainedViewLayoutTransitionCurve, duration: Double) { - if !self.mainContextSourceNode.isExtractedToContextPreview { - self.applyAbsoluteOffsetInternal(value: -value, animationCurve: animationCurve, duration: duration) + var backgroundWallpaperFrame = self.backgroundWallpaperNode.frame + backgroundWallpaperFrame.origin.x += rect.minX + backgroundWallpaperFrame.origin.y += rect.minY + self.backgroundWallpaperNode.update(rect: backgroundWallpaperFrame, within: containerSize) + for contentNode in self.contentNodes { + contentNode.updateAbsoluteRect(CGRect(origin: CGPoint(x: rect.minX + contentNode.frame.minX, y: rect.minY + contentNode.frame.minY), size: rect.size), within: containerSize) } } - private func applyAbsoluteOffsetInternal(value: CGFloat, animationCurve: ContainedViewLayoutTransitionCurve, duration: Double) { + override func applyAbsoluteOffset(value: CGPoint, animationCurve: ContainedViewLayoutTransitionCurve, duration: Double) { + if !self.mainContextSourceNode.isExtractedToContextPreview { + self.applyAbsoluteOffsetInternal(value: CGPoint(x: -value.x, y: -value.y), animationCurve: animationCurve, duration: duration) + } + } + + private func applyAbsoluteOffsetInternal(value: CGPoint, animationCurve: ContainedViewLayoutTransitionCurve, duration: Double) { self.backgroundWallpaperNode.offset(value: value, animationCurve: animationCurve, duration: duration) + + for contentNode in self.contentNodes { + contentNode.applyAbsoluteOffset(value: value, animationCurve: animationCurve, duration: duration) + } } private func applyAbsoluteOffsetSpringInternal(value: CGFloat, duration: Double, damping: CGFloat) { self.backgroundWallpaperNode.offsetSpring(value: value, duration: duration, damping: damping) + + for contentNode in self.contentNodes { + contentNode.applyAbsoluteOffsetSpring(value: value, duration: duration, damping: damping) + } } override func getMessageContextSourceNode(stableId: UInt32?) -> ContextExtractedContentContainingNode? { diff --git a/submodules/TelegramUI/Sources/ChatMessageContactBubbleContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageContactBubbleContentNode.swift index 8f475534f7..becd445ba4 100644 --- a/submodules/TelegramUI/Sources/ChatMessageContactBubbleContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageContactBubbleContentNode.swift @@ -90,6 +90,9 @@ class ChatMessageContactBubbleContentNode: ChatMessageBubbleContentNode { } else { displayName = selectedContact.lastName } + if displayName.isEmpty { + displayName = item.presentationData.strings.Message_Contact + } let info: String if let previousContact = previousContact, previousContact.isEqual(to: selectedContact), let contactInfo = previousContactInfo { diff --git a/submodules/TelegramUI/Sources/ChatMessageDateAndStatusNode.swift b/submodules/TelegramUI/Sources/ChatMessageDateAndStatusNode.swift index 2803acb223..d7856d1496 100644 --- a/submodules/TelegramUI/Sources/ChatMessageDateAndStatusNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageDateAndStatusNode.swift @@ -151,6 +151,7 @@ private final class StatusReactionNode: ASDisplayNode { class ChatMessageDateAndStatusNode: ASDisplayNode { private var backgroundNode: ASImageNode? + private var blurredBackgroundNode: NavigationBackgroundNode? private var checkSentNode: ASImageNode? private var checkReadNode: ASImageNode? private var clockFrameNode: ASImageNode? @@ -227,6 +228,7 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { return { context, presentationData, edited, impressionCount, dateText, type, constrainedSize, reactions, replyCount, isPinned, hasAutoremove in let dateColor: UIColor var backgroundImage: UIImage? + var blurredBackgroundColor: (UIColor, Bool)? var outgoingStatus: ChatMessageDateAndStatusOutgoingType? var leftInset: CGFloat @@ -236,11 +238,10 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { let clockMinImage: UIImage? var impressionImage: UIImage? var repliesImage: UIImage? - var selfExpiringImage: UIImage? 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, bubbleCorners: presentationData.chatBubbleCorners) + let graphics = PresentationResourcesChat.principalGraphics(theme: presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper, bubbleCorners: presentationData.chatBubbleCorners) let isDefaultWallpaper = serviceMessageColorHasDefaultWallpaper(presentationData.theme.wallpaper) let offset: CGFloat = -UIScreenPixel @@ -326,7 +327,8 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { case .FreeIncoming: let serviceColor = serviceMessageColorComponents(theme: presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper) dateColor = serviceColor.primaryText - backgroundImage = graphics.dateAndStatusFreeBackground + + blurredBackgroundColor = (selectDateFillStaticColor(theme: presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper), dateFillNeedsBlur(theme: presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper)) leftInset = 0.0 loadedCheckFullImage = PresentationResourcesChat.chatFreeFullCheck(presentationData.theme.theme, size: checkSize, isDefaultWallpaper: isDefaultWallpaper) loadedCheckPartialImage = PresentationResourcesChat.chatFreePartialCheck(presentationData.theme.theme, size: checkSize, isDefaultWallpaper: isDefaultWallpaper) @@ -347,7 +349,8 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { let serviceColor = serviceMessageColorComponents(theme: presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper) dateColor = serviceColor.primaryText outgoingStatus = status - backgroundImage = graphics.dateAndStatusFreeBackground + //backgroundImage = graphics.dateAndStatusFreeBackground + blurredBackgroundColor = (selectDateFillStaticColor(theme: presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper), dateFillNeedsBlur(theme: presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper)) leftInset = 0.0 loadedCheckFullImage = PresentationResourcesChat.chatFreeFullCheck(presentationData.theme.theme, size: checkSize, isDefaultWallpaper: isDefaultWallpaper) loadedCheckPartialImage = PresentationResourcesChat.chatFreePartialCheck(presentationData.theme.theme, size: checkSize, isDefaultWallpaper: isDefaultWallpaper) @@ -416,24 +419,10 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { currentRepliesIcon = nil } - var selfExpiringIconSize = CGSize() - if let selfExpiringImage = selfExpiringImage { - if currentSelfExpiringIcon == nil { - let iconNode = ASImageNode() - iconNode.isLayerBacked = true - iconNode.displayWithoutProcessing = true - iconNode.displaysAsynchronously = false - currentSelfExpiringIcon = iconNode - } - selfExpiringIconSize = selfExpiringImage.size - } else { - currentSelfExpiringIcon = nil - } - if let outgoingStatus = outgoingStatus { switch outgoingStatus { case .Sending: - statusWidth = 13.0 + statusWidth = floor(floor(presentationData.fontSize.baseDisplaySize * 13.0 / 17.0)) if checkReadNode == nil { checkReadNode = ASImageNode() @@ -536,6 +525,8 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { currentBackgroundNode = backgroundNode } backgroundInsets = UIEdgeInsets(top: 2.0, left: 7.0, bottom: 2.0, right: 7.0) + } else if blurredBackgroundColor != nil { + backgroundInsets = UIEdgeInsets(top: 2.0, left: 7.0, bottom: 2.0, right: 7.0) } let reactionSize: CGFloat = 14.0 @@ -584,10 +575,6 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { reactionInset += 12.0 } - if !selfExpiringIconSize.width.isZero { - reactionInset += selfExpiringIconSize.width + 1.0 - } - leftInset += reactionInset let layoutSize = CGSize(width: leftInset + impressionWidth + date.size.width + statusWidth + backgroundInsets.left + backgroundInsets.right, height: date.size.height + backgroundInsets.top + backgroundInsets.bottom) @@ -621,6 +608,27 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { strongSelf.backgroundNode = nil } } + + if let blurredBackgroundColor = blurredBackgroundColor { + if let blurredBackgroundNode = strongSelf.blurredBackgroundNode { + blurredBackgroundNode.updateColor(color: blurredBackgroundColor.0, enableBlur: blurredBackgroundColor.1, transition: .immediate) + let transition: ContainedViewLayoutTransition = animated ? .animated(duration: 0.4, curve: .spring) : .immediate + if let previousLayoutSize = previousLayoutSize { + blurredBackgroundNode.frame = blurredBackgroundNode.frame.offsetBy(dx: layoutSize.width - previousLayoutSize.width, dy: 0.0) + } + transition.updateFrame(node: blurredBackgroundNode, frame: CGRect(origin: CGPoint(), size: layoutSize)) + blurredBackgroundNode.update(size: blurredBackgroundNode.bounds.size, cornerRadius: blurredBackgroundNode.bounds.height / 2.0, transition: transition) + } else { + let blurredBackgroundNode = NavigationBackgroundNode(color: blurredBackgroundColor.0, enableBlur: blurredBackgroundColor.1) + strongSelf.blurredBackgroundNode = blurredBackgroundNode + strongSelf.insertSubnode(blurredBackgroundNode, at: 0) + blurredBackgroundNode.frame = CGRect(origin: CGPoint(), size: layoutSize) + blurredBackgroundNode.update(size: blurredBackgroundNode.bounds.size, cornerRadius: blurredBackgroundNode.bounds.height / 2.0, transition: .immediate) + } + } else if let blurredBackgroundNode = strongSelf.blurredBackgroundNode { + strongSelf.blurredBackgroundNode = nil + blurredBackgroundNode.removeFromSupernode() + } strongSelf.dateNode.displaysAsynchronously = !presentationData.isPreview let _ = dateApply() @@ -834,34 +842,6 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { } } - if let currentSelfExpiringIcon = currentSelfExpiringIcon { - currentSelfExpiringIcon.displaysAsynchronously = !presentationData.isPreview - if currentSelfExpiringIcon.image !== selfExpiringImage { - currentSelfExpiringIcon.image = selfExpiringImage - } - if currentSelfExpiringIcon.supernode == nil { - strongSelf.selfExpiringIcon = currentSelfExpiringIcon - strongSelf.addSubnode(currentSelfExpiringIcon) - if animated { - currentSelfExpiringIcon.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) - } - } - currentSelfExpiringIcon.frame = CGRect(origin: CGPoint(x: reactionOffset - 2.0, y: backgroundInsets.top + offset + floor((date.size.height - selfExpiringIconSize.height) / 2.0)), size: selfExpiringIconSize) - reactionOffset += 9.0 - } else if let selfExpiringIcon = strongSelf.selfExpiringIcon { - strongSelf.selfExpiringIcon = nil - if animated { - if let previousLayoutSize = previousLayoutSize { - selfExpiringIcon.frame = selfExpiringIcon.frame.offsetBy(dx: layoutSize.width - previousLayoutSize.width, dy: 0.0) - } - selfExpiringIcon.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak selfExpiringIcon] _ in - selfExpiringIcon?.removeFromSupernode() - }) - } else { - selfExpiringIcon.removeFromSupernode() - } - } - if let (layout, apply) = replyCountLayoutAndApply { let node = apply() if strongSelf.replyCountNode !== node { diff --git a/submodules/TelegramUI/Sources/ChatMessageDateHeader.swift b/submodules/TelegramUI/Sources/ChatMessageDateHeader.swift index 61eb90e62c..4dec9bcd94 100644 --- a/submodules/TelegramUI/Sources/ChatMessageDateHeader.swift +++ b/submodules/TelegramUI/Sources/ChatMessageDateHeader.swift @@ -6,6 +6,7 @@ import TelegramPresentationData import Postbox import SyncCore import AccountContext +import AvatarNode private let timezoneOffset: Int32 = { let nowTimestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) @@ -22,7 +23,7 @@ final class ChatMessageDateHeader: ListViewItemHeader { private let roundedTimestamp: Int32 private let scheduled: Bool - let id: Int64 + let id: ListViewItemNode.HeaderId let presentationData: ChatPresentationData let context: AccountContext let action: ((Int32) -> Void)? @@ -33,21 +34,16 @@ final class ChatMessageDateHeader: ListViewItemHeader { self.presentationData = presentationData self.context = context self.action = action - if timestamp == scheduleWhenOnlineTimestamp { - self.roundedTimestamp = scheduleWhenOnlineTimestamp - } else if timestamp == Int32.max { - self.roundedTimestamp = timestamp / (granularity) * (granularity) - } else { - self.roundedTimestamp = ((timestamp + timezoneOffset) / (granularity)) * (granularity) - } - self.id = Int64(self.roundedTimestamp) + self.roundedTimestamp = dateHeaderTimestampId(timestamp: timestamp) + self.id = ListViewItemNode.HeaderId(space: 0, id: Int64(self.roundedTimestamp)) } let stickDirection: ListViewItemHeaderStickDirection = .bottom + let stickOverInsets: Bool = true let height: CGFloat = 34.0 - func node() -> ListViewItemHeaderNode { + func node(synchronousLoad: Bool) -> ListViewItemHeaderNode { return ChatMessageDateHeaderNode(localTimestamp: self.roundedTimestamp, scheduled: self.scheduled, presentationData: self.presentationData, context: self.context, action: self.action) } @@ -90,9 +86,19 @@ private func monthAtIndex(_ index: Int, strings: PresentationStrings) -> String } } +private func dateHeaderTimestampId(timestamp: Int32) -> Int32 { + if timestamp == scheduleWhenOnlineTimestamp { + return timestamp + } else if timestamp == Int32.max { + return timestamp / (granularity) * (granularity) + } else { + return ((timestamp + timezoneOffset) / (granularity)) * (granularity) + } +} + final class ChatMessageDateHeaderNode: ListViewItemHeaderNode { let labelNode: TextNode - let backgroundNode: ASImageNode + let backgroundNode: NavigationBackgroundNode let stickBackgroundNode: ASImageNode let activateArea: AccessibilityAreaNode @@ -116,10 +122,8 @@ final class ChatMessageDateHeaderNode: ListViewItemHeaderNode { self.labelNode.isUserInteractionEnabled = false self.labelNode.displaysAsynchronously = !presentationData.isPreview - self.backgroundNode = ASImageNode() - self.backgroundNode.isLayerBacked = true - self.backgroundNode.displayWithoutProcessing = true - self.backgroundNode.displaysAsynchronously = false + self.backgroundNode = NavigationBackgroundNode(color: .clear) + self.backgroundNode.isUserInteractionEnabled = false self.stickBackgroundNode = ASImageNode() self.stickBackgroundNode.isLayerBacked = true @@ -165,12 +169,12 @@ final class ChatMessageDateHeaderNode: ListViewItemHeaderNode { 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 + let graphics = PresentationResourcesChat.principalGraphics(theme: presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper, bubbleCorners: presentationData.chatBubbleCorners) + + self.backgroundNode.updateColor(color: selectDateFillStaticColor(theme: presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper), enableBlur: dateFillNeedsBlur(theme: presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper), transition: .immediate) self.stickBackgroundNode.image = graphics.dateFloatingBackground self.stickBackgroundNode.alpha = 0.0 - self.backgroundNode.addSubnode(self.stickBackgroundNode) + self.addSubnode(self.backgroundNode) self.addSubnode(self.labelNode) @@ -198,9 +202,9 @@ final class ChatMessageDateHeaderNode: ListViewItemHeaderNode { 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 + let graphics = PresentationResourcesChat.principalGraphics(theme: presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper, bubbleCorners: presentationData.chatBubbleCorners) + + self.backgroundNode.updateColor(color: selectDateFillStaticColor(theme: presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper), enableBlur: dateFillNeedsBlur(theme: presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper), transition: .immediate) self.stickBackgroundNode.image = graphics.dateFloatingBackground let titleFont = Font.medium(min(18.0, floor(presentationData.fontSize.baseDisplaySize * 13.0 / 17.0))) @@ -218,13 +222,8 @@ final class ChatMessageDateHeaderNode: ListViewItemHeaderNode { 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) + func updateBackgroundColor(color: UIColor, enableBlur: Bool) { + self.backgroundNode.updateColor(color: color, enableBlur: enableBlur, transition: .immediate) } override func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat) { @@ -237,6 +236,7 @@ final class ChatMessageDateHeaderNode: ListViewItemHeaderNode { let backgroundFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - backgroundSize.width) / 2.0), y: (34.0 - chatDateSize) / 2.0), size: backgroundSize) self.stickBackgroundNode.frame = CGRect(origin: CGPoint(), size: backgroundFrame.size) self.backgroundNode.frame = backgroundFrame + self.backgroundNode.update(size: backgroundFrame.size, cornerRadius: backgroundFrame.size.height / 2.0, transition: .immediate) self.labelNode.frame = CGRect(origin: CGPoint(x: backgroundFrame.origin.x + chatDateInset, y: backgroundFrame.origin.y + floorToScreenPixels((backgroundSize.height - labelSize.height) / 2.0)), size: labelSize) self.activateArea.frame = backgroundFrame @@ -305,3 +305,173 @@ final class ChatMessageDateHeaderNode: ListViewItemHeaderNode { } } } + +final class ChatMessageAvatarHeader: ListViewItemHeader { + struct Id: Hashable { + var peerId: PeerId + var timestampId: Int32 + } + + let id: ListViewItemNode.HeaderId + let peerId: PeerId + let peer: Peer? + let messageReference: MessageReference? + let presentationData: ChatPresentationData + let context: AccountContext + let controllerInteraction: ChatControllerInteraction + + init(timestamp: Int32, peerId: PeerId, peer: Peer?, messageReference: MessageReference?, presentationData: ChatPresentationData, context: AccountContext, controllerInteraction: ChatControllerInteraction) { + self.peerId = peerId + self.peer = peer + self.messageReference = messageReference + self.presentationData = presentationData + self.context = context + self.controllerInteraction = controllerInteraction + self.id = ListViewItemNode.HeaderId(space: 1, id: Id(peerId: peerId, timestampId: dateHeaderTimestampId(timestamp: timestamp))) + } + + let stickDirection: ListViewItemHeaderStickDirection = .top + let stickOverInsets: Bool = false + + let height: CGFloat = 38.0 + + func node(synchronousLoad: Bool) -> ListViewItemHeaderNode { + return ChatMessageAvatarHeaderNode(peerId: self.peerId, peer: self.peer, messageReference: self.messageReference, presentationData: self.presentationData, context: self.context, controllerInteraction: self.controllerInteraction, synchronousLoad: synchronousLoad) + } + + func updateNode(_ node: ListViewItemHeaderNode, previous: ListViewItemHeader?, next: ListViewItemHeader?) { + guard let node = node as? ChatMessageAvatarHeaderNode, let next = next as? ChatMessageAvatarHeader else { + return + } + node.updatePresentationData(next.presentationData, context: next.context) + } +} + +private let avatarFont = avatarPlaceholderFont(size: 16.0) + +final class ChatMessageAvatarHeaderNode: ListViewItemHeaderNode { + private let peerId: PeerId + private let messageReference: MessageReference? + private let peer: Peer? + + private let containerNode: ContextControllerSourceNode + private let avatarNode: AvatarNode + private var presentationData: ChatPresentationData + private let context: AccountContext + private let controllerInteraction: ChatControllerInteraction + + init(peerId: PeerId, peer: Peer?, messageReference: MessageReference?, presentationData: ChatPresentationData, context: AccountContext, controllerInteraction: ChatControllerInteraction, synchronousLoad: Bool) { + self.peerId = peerId + self.peer = peer + self.messageReference = messageReference + self.presentationData = presentationData + self.context = context + self.controllerInteraction = controllerInteraction + + self.containerNode = ContextControllerSourceNode() + + self.avatarNode = AvatarNode(font: avatarFont) + + super.init(layerBacked: false, dynamicBounce: true, isRotated: true, seeThrough: false) + + self.transform = CATransform3DMakeRotation(CGFloat.pi, 0.0, 0.0, 1.0) + + self.addSubnode(self.containerNode) + self.containerNode.addSubnode(self.avatarNode) + + if let peer = peer { + self.setPeer(context: context, theme: presentationData.theme.theme, synchronousLoad: synchronousLoad, peer: peer, authorOfMessage: messageReference, emptyColor: .black) + } + + self.containerNode.activated = { [weak self] gesture, _ in + guard let strongSelf = self, let peer = strongSelf.peer else { + return + } + var messageId: MessageId? + if let messageReference = messageReference, case let .message(m) = messageReference.content { + messageId = m.id + } + strongSelf.controllerInteraction.openPeerContextMenu(peer, messageId, strongSelf.containerNode, strongSelf.containerNode.bounds, gesture) + } + + self.updateSelectionState(animated: false) + } + + func setCustomLetters(context: AccountContext, theme: PresentationTheme, synchronousLoad: Bool, letters: [String], emptyColor: UIColor) { + self.containerNode.isGestureEnabled = false + + self.avatarNode.setCustomLetters(letters, icon: !letters.isEmpty ? nil : .phone) + } + + func setPeer(context: AccountContext, theme: PresentationTheme, synchronousLoad: Bool, peer: Peer, authorOfMessage: MessageReference?, emptyColor: UIColor) { + self.containerNode.isGestureEnabled = peer.smallProfileImage != nil + + var overrideImage: AvatarNodeImageOverride? + if peer.isDeleted { + overrideImage = .deletedIcon + } + 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)) + } + + override func didLoad() { + super.didLoad() + + self.avatarNode.view.addGestureRecognizer(ListViewTapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) + } + + func updatePresentationData(_ presentationData: ChatPresentationData, context: AccountContext) { + self.presentationData = presentationData + + self.setNeedsLayout() + } + + override func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat) { + self.containerNode.frame = CGRect(origin: CGPoint(x: leftInset + 3.0, y: 0.0), size: CGSize(width: 38.0, height: 38.0)) + self.avatarNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 38.0, height: 38.0)) + } + + override func animateRemoved(duration: Double) { + self.alpha = 0.0 + self.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration, removeOnCompletion: false) + self.avatarNode.layer.animateScale(from: 1.0, to: 0.2, duration: duration, removeOnCompletion: false) + } + + override func animateAdded(duration: Double) { + self.layer.animateAlpha(from: 0.0, to: self.alpha, duration: 0.2) + self.avatarNode.layer.animateScale(from: 0.2, to: 1.0, duration: 0.2) + } + + override func updateStickDistanceFactor(_ factor: CGFloat, transition: ContainedViewLayoutTransition) { + } + + override func updateFlashingOnScrolling(_ isFlashingOnScrolling: Bool, animated: Bool) { + } + + func updateSelectionState(animated: Bool) { + let offset: CGFloat = self.controllerInteraction.selectionState != nil ? 42.0 : 0.0 + + let previousSubnodeTransform = self.subnodeTransform + self.subnodeTransform = CATransform3DMakeTranslation(offset, 0.0, 0.0); + if animated { + self.layer.animate(from: NSValue(caTransform3D: previousSubnodeTransform), to: NSValue(caTransform3D: self.subnodeTransform), keyPath: "sublayerTransform", timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, duration: 0.2) + } + } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if !self.bounds.contains(point) { + return nil + } + let result = self.containerNode.view.hitTest(self.view.convert(point, to: self.containerNode.view), with: event) + return result + } + + override func touchesCancelled(_ touches: Set?, with event: UIEvent?) { + super.touchesCancelled(touches, with: event) + } + + @objc func tapGesture(_ recognizer: ListViewTapGestureRecognizer) { + if case .ended = recognizer.state { + self.controllerInteraction.openPeer(self.peerId, .info, nil) + } + } +} diff --git a/submodules/TelegramUI/Sources/ChatMessageEventLogPreviousDescriptionContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageEventLogPreviousDescriptionContentNode.swift index 110dc38a36..5b3cbb1970 100644 --- a/submodules/TelegramUI/Sources/ChatMessageEventLogPreviousDescriptionContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageEventLogPreviousDescriptionContentNode.swift @@ -25,7 +25,7 @@ final class ChatMessageEventLogPreviousDescriptionContentNode: ChatMessageBubble 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 contentNodeLayout = self.contentNode.asyncLayout() - return { item, layoutConstants, _, _, constrainedSize in + return { item, layoutConstants, preparePosition, _, constrainedSize in var messageEntities: [MessageTextEntity]? for attribute in item.message.attributes { @@ -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.attributes, item.context, item.controllerInteraction, item.message, true, .peer(item.message.id.peerId), 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, .peer(item.message.id.peerId), title, nil, text, messageEntities, mediaAndFlags, nil, nil, nil, true, layoutConstants, preparePosition, constrainedSize) let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: false, headerSpacing: 8.0, hidesBackground: .never, forceFullCorners: false, forceAlignment: .none) diff --git a/submodules/TelegramUI/Sources/ChatMessageEventLogPreviousLinkContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageEventLogPreviousLinkContentNode.swift index c0ff0ffa39..c481ff165c 100644 --- a/submodules/TelegramUI/Sources/ChatMessageEventLogPreviousLinkContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageEventLogPreviousLinkContentNode.swift @@ -25,7 +25,7 @@ final class ChatMessageEventLogPreviousLinkContentNode: ChatMessageBubbleContent 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 contentNodeLayout = self.contentNode.asyncLayout() - return { item, layoutConstants, _, _, constrainedSize in + return { item, layoutConstants, preparePosition, _, constrainedSize in var messageEntities: [MessageTextEntity]? for attribute in item.message.attributes { @@ -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.attributes, item.context, item.controllerInteraction, item.message, true, .peer(item.message.id.peerId), 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, .peer(item.message.id.peerId), title, nil, text, messageEntities, mediaAndFlags, nil, nil, nil, true, layoutConstants, preparePosition, constrainedSize) let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: false, headerSpacing: 8.0, hidesBackground: .never, forceFullCorners: false, forceAlignment: .none) diff --git a/submodules/TelegramUI/Sources/ChatMessageEventLogPreviousMessageContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageEventLogPreviousMessageContentNode.swift index 0d638f3fd4..b0e5ba0891 100644 --- a/submodules/TelegramUI/Sources/ChatMessageEventLogPreviousMessageContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageEventLogPreviousMessageContentNode.swift @@ -25,7 +25,7 @@ final class ChatMessageEventLogPreviousMessageContentNode: ChatMessageBubbleCont 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 contentNodeLayout = self.contentNode.asyncLayout() - return { item, layoutConstants, _, _, constrainedSize in + return { item, layoutConstants, preparePosition, _, constrainedSize in var messageEntities: [MessageTextEntity]? for attribute in item.message.attributes { @@ -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.attributes, item.context, item.controllerInteraction, item.message, true, .peer(item.message.id.peerId), 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, .peer(item.message.id.peerId), title, nil, text, messageEntities, mediaAndFlags, nil, nil, nil, true, layoutConstants, preparePosition, constrainedSize) let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: false, headerSpacing: 8.0, hidesBackground: .never, forceFullCorners: false, forceAlignment: .none) diff --git a/submodules/TelegramUI/Sources/ChatMessageFileBubbleContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageFileBubbleContentNode.swift index 749a02796f..4e374f4aa3 100644 --- a/submodules/TelegramUI/Sources/ChatMessageFileBubbleContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageFileBubbleContentNode.swift @@ -9,7 +9,7 @@ import SyncCore import TelegramUIPreferences class ChatMessageFileBubbleContentNode: ChatMessageBubbleContentNode { - private let interactiveFileNode: ChatMessageInteractiveFileNode + let interactiveFileNode: ChatMessageInteractiveFileNode override var visibility: ListViewItemNodeVisibility { didSet { diff --git a/submodules/TelegramUI/Sources/ChatMessageGameBubbleContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageGameBubbleContentNode.swift index de1c237da2..240ee141fa 100644 --- a/submodules/TelegramUI/Sources/ChatMessageGameBubbleContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageGameBubbleContentNode.swift @@ -45,7 +45,7 @@ final class ChatMessageGameBubbleContentNode: ChatMessageBubbleContentNode { 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 contentNodeLayout = self.contentNode.asyncLayout() - return { item, layoutConstants, _, _, constrainedSize in + return { item, layoutConstants, preparePosition, _, constrainedSize in var game: TelegramMediaGame? var messageEntities: [MessageTextEntity]? @@ -78,7 +78,7 @@ final class ChatMessageGameBubbleContentNode: ChatMessageBubbleContentNode { } } - let (initialWidth, continueLayout) = contentNodeLayout(item.presentationData, item.controllerInteraction.automaticMediaDownloadSettings, item.associatedData, item.attributes, item.context, item.controllerInteraction, item.message, item.read, .peer(item.message.id.peerId), 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, .peer(item.message.id.peerId), title, nil, item.message.text.isEmpty ? text : item.message.text, item.message.text.isEmpty ? nil : messageEntities, mediaAndFlags, nil, nil, nil, true, layoutConstants, preparePosition, constrainedSize) let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: false, headerSpacing: 8.0, hidesBackground: .never, forceFullCorners: false, forceAlignment: .none) diff --git a/submodules/TelegramUI/Sources/ChatMessageInstantVideoItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageInstantVideoItemNode.swift index 30c9380450..9dc3b1afb5 100644 --- a/submodules/TelegramUI/Sources/ChatMessageInstantVideoItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageInstantVideoItemNode.swift @@ -20,7 +20,7 @@ private let inlineBotPrefixFont = Font.regular(14.0) private let inlineBotNameFont = nameFont class ChatMessageInstantVideoItemNode: ChatMessageItemView { - private let contextSourceNode: ContextExtractedContentContainingNode + let contextSourceNode: ContextExtractedContentContainingNode private let containerNode: ContextControllerSourceNode private let interactiveVideoNode: ChatMessageInteractiveInstantVideoNode @@ -31,8 +31,12 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView { private var swipeToReplyNode: ChatMessageSwipeToReplyNode? private var swipeToReplyFeedback: HapticFeedback? + private var appliedParams: ListViewItemLayoutParams? private var appliedItem: ChatMessageItem? private var appliedForwardInfo: (Peer?, String?)? + private var appliedHasAvatar = false + private var appliedCurrentlyPlaying = false + private var appliedAutomaticDownload = false private var forwardInfoNode: ChatMessageForwardInfoNode? private var forwardBackgroundNode: ASImageNode? @@ -217,6 +221,7 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView { let currentItem = self.appliedItem let currentForwardInfo = self.appliedForwardInfo + let currentPlaying = self.appliedCurrentlyPlaying return { item, params, mergedTop, mergedBottom, dateHeaderAtBottom in let accessibilityData = ChatMessageAccessibilityData(item: item, isSelected: nil) @@ -318,7 +323,13 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView { deliveryFailedInset += 24.0 } - let displaySize = layoutConstants.instantVideo.dimensions + var isPlaying = false + var displaySize = layoutConstants.instantVideo.dimensions + let maximumDisplaySize = CGSize(width: params.width - 20.0, height: params.width - 20.0) + if item.associatedData.currentlyPlayingMessageId == item.message.index { + isPlaying = true + displaySize = maximumDisplaySize + } var automaticDownload = true for media in item.message.media { @@ -332,7 +343,7 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView { isReplyThread = true } - let (videoLayout, videoApply) = makeVideoLayout(ChatMessageBubbleContentItem(context: item.context, controllerInteraction: item.controllerInteraction, message: item.message, read: item.read, chatLocation: item.chatLocation, presentationData: item.presentationData, associatedData: item.associatedData, attributes: item.content.firstMessageAttributes, isItemPinned: item.message.tags.contains(.pinned) && !isReplyThread, isItemEdited: false), 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, chatLocation: item.chatLocation, presentationData: item.presentationData, associatedData: item.associatedData, attributes: item.content.firstMessageAttributes, isItemPinned: item.message.tags.contains(.pinned) && !isReplyThread, isItemEdited: false), params.width - params.leftInset - params.rightInset - avatarInset, displaySize, maximumDisplaySize, isPlaying ? 1.0 : 0.0, .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) @@ -495,30 +506,40 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView { return (ListViewItemNodeLayout(contentSize: layoutSize, insets: layoutInsets), { [weak self] animation, _ in if let strongSelf = self { - strongSelf.contextSourceNode.frame = CGRect(origin: CGPoint(), size: layoutSize) - strongSelf.containerNode.frame = CGRect(origin: CGPoint(), size: layoutSize) - strongSelf.contextSourceNode.contentNode.frame = CGRect(origin: CGPoint(), size: layoutSize) - strongSelf.messageAccessibilityArea.frame = CGRect(origin: CGPoint(), size: layoutSize) - - strongSelf.appliedItem = item - strongSelf.appliedForwardInfo = (forwardSource, forwardAuthorSignature) - - strongSelf.updateAccessibilityData(accessibilityData) - let transition: ContainedViewLayoutTransition if animation.isAnimated { transition = .animated(duration: 0.2, curve: .spring) } else { transition = .immediate } - strongSelf.interactiveVideoNode.frame = videoFrame + + strongSelf.contextSourceNode.frame = CGRect(origin: CGPoint(), size: layoutSize) + strongSelf.containerNode.frame = CGRect(origin: CGPoint(), size: layoutSize) + strongSelf.contextSourceNode.contentNode.frame = CGRect(origin: CGPoint(), size: layoutSize) + strongSelf.messageAccessibilityArea.frame = CGRect(origin: CGPoint(), size: layoutSize) + + strongSelf.appliedParams = params + strongSelf.appliedItem = item + strongSelf.appliedHasAvatar = hasAvatar + strongSelf.appliedForwardInfo = (forwardSource, forwardAuthorSignature) + strongSelf.appliedCurrentlyPlaying = isPlaying + + strongSelf.updateAccessibilityData(accessibilityData) + + + let videoLayoutData: ChatMessageInstantVideoItemLayoutData if incoming { videoLayoutData = .constrained(left: 0.0, right: max(0.0, availableContentWidth - videoFrame.width)) } else { videoLayoutData = .constrained(left: max(0.0, availableContentWidth - videoFrame.width), right: 0.0) } - videoApply(videoLayoutData, transition) + + if currentItem != nil && currentPlaying != isPlaying { + } else { + strongSelf.interactiveVideoNode.frame = videoFrame + videoApply(videoLayoutData, transition) + } strongSelf.contextSourceNode.contentRect = videoFrame strongSelf.containerNode.targetNodeForActivationProgressContentRect = strongSelf.contextSourceNode.contentRect @@ -731,7 +752,7 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView { for attribute in item.content.firstMessage.attributes { if let attribute = attribute as? SourceReferenceMessageAttribute { openPeerId = attribute.messageId.peerId - navigate = .chat(textInputState: nil, subject: .message(id: attribute.messageId, highlight: true), peekData: nil) + navigate = .chat(textInputState: nil, subject: .message(id: attribute.messageId, highlight: true, timecode: nil), peekData: nil) } } @@ -850,7 +871,7 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView { if translation.x < -45.0, self.swipeToReplyNode == nil, let item = self.item { self.swipeToReplyFeedback?.impact() - 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), action: ChatMessageSwipeToReplyNode.Action(self.currentSwipeAction)) + let swipeToReplyNode = ChatMessageSwipeToReplyNode(fillColor: selectDateFillStaticColor(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper), enableBlur: dateFillNeedsBlur(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper), foregroundColor: bubbleVariableColor(variableColor: item.presentationData.theme.theme.chat.message.shareButtonForegroundColor, wallpaper: item.presentationData.theme.wallpaper), action: ChatMessageSwipeToReplyNode.Action(self.currentSwipeAction)) self.swipeToReplyNode = swipeToReplyNode self.addSubnode(swipeToReplyNode) animateReplyNodeIn = true @@ -985,6 +1006,10 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView { } } } + + override func cancelInsertionAnimations() { + self.layer.removeAllAnimations() + } override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { super.animateInsertion(currentTimestamp, duration: duration, short: short) @@ -1003,7 +1028,12 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) } - + + func animateFromSnapshot(snapshotView: UIView, transition: CombinedTransition) { + snapshotView.frame = self.interactiveVideoNode.view.convert(snapshotView.frame, from: self.contextSourceNode.contentNode.view) + self.interactiveVideoNode.animateFromSnapshot(snapshotView: snapshotView, transition: transition) + } + override func playMediaWithSound() -> ((Double?) -> Void, Bool, Bool, Bool, ASDisplayNode?)? { return self.interactiveVideoNode.playMediaWithSound() } @@ -1015,4 +1045,75 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView { override func addAccessoryItemNode(_ accessoryItemNode: ListViewAccessoryItemNode) { self.contextSourceNode.contentNode.addSubnode(accessoryItemNode) } + + override func animateFrameTransition(_ progress: CGFloat, _ currentValue: CGFloat) { + super.animateFrameTransition(progress, currentValue) + + guard let item = self.appliedItem, let params = self.appliedParams, progress > 0.0, let (initialHeight, targetHeight) = self.apparentHeightTransition else { + return + } + + let layoutConstants = chatMessageItemLayoutConstants(self.layoutConstants, params: params, presentationData: item.presentationData) + let incoming = item.message.effectivelyIncoming(item.context.account.peerId) + + var isReplyThread = false + if case .replyThread = item.chatLocation { + isReplyThread = true + } + + var isPlaying = false + var displaySize = layoutConstants.instantVideo.dimensions + let maximumDisplaySize = CGSize(width: params.width - 20.0, height: params.width - 20.0) + if item.associatedData.currentlyPlayingMessageId == item.message.index { + isPlaying = true + } + + let avatarInset: CGFloat + if self.appliedHasAvatar { + avatarInset = layoutConstants.avatarDiameter + } else { + avatarInset = 0.0 + } + + let isFailed = item.content.firstMessage.effectivelyFailed(timestamp: item.context.account.network.getApproximateRemoteTimestamp()) + var deliveryFailedInset: CGFloat = 0.0 + if isFailed { + deliveryFailedInset += 24.0 + } + + let makeVideoLayout = self.interactiveVideoNode.asyncLayout() + + let initialSize: CGSize + let targetSize: CGSize + let animationProgress: CGFloat = (currentValue - initialHeight) / (targetHeight - initialHeight) + let scaleProgress: CGFloat + if currentValue < targetHeight { + initialSize = displaySize + targetSize = maximumDisplaySize + scaleProgress = animationProgress + } else if currentValue > targetHeight { + initialSize = maximumDisplaySize + targetSize = displaySize + scaleProgress = 1.0 - animationProgress + } else { + initialSize = isPlaying ? maximumDisplaySize : displaySize + targetSize = initialSize + scaleProgress = isPlaying ? 1.0 : 0.0 + } + displaySize = CGSize(width: initialSize.width + (targetSize.width - initialSize.width) * animationProgress, height: initialSize.height + (targetSize.height - initialSize.height) * animationProgress) + + let (videoLayout, videoApply) = makeVideoLayout(ChatMessageBubbleContentItem(context: item.context, controllerInteraction: item.controllerInteraction, message: item.message, read: item.read, chatLocation: item.chatLocation, presentationData: item.presentationData, associatedData: item.associatedData, attributes: item.content.firstMessageAttributes, isItemPinned: item.message.tags.contains(.pinned) && !isReplyThread, isItemEdited: false), params.width - params.leftInset - params.rightInset - avatarInset, displaySize, maximumDisplaySize, scaleProgress, .free, self.appliedAutomaticDownload) + + let availableContentWidth = params.width - params.leftInset - params.rightInset - layoutConstants.bubble.edgeInset * 2.0 - avatarInset - layoutConstants.bubble.contentInsets.left + 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) + self.interactiveVideoNode.frame = videoFrame + + let videoLayoutData: ChatMessageInstantVideoItemLayoutData + if incoming { + videoLayoutData = .constrained(left: 0.0, right: max(0.0, availableContentWidth - videoFrame.width)) + } else { + videoLayoutData = .constrained(left: max(0.0, availableContentWidth - videoFrame.width), right: 0.0) + } + videoApply(videoLayoutData, .immediate) + } } diff --git a/submodules/TelegramUI/Sources/ChatMessageInteractiveFileNode.swift b/submodules/TelegramUI/Sources/ChatMessageInteractiveFileNode.swift index 357d812b1b..6cdc96120a 100644 --- a/submodules/TelegramUI/Sources/ChatMessageInteractiveFileNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageInteractiveFileNode.swift @@ -17,6 +17,7 @@ import FileMediaResourceStatus import CheckNode import MusicAlbumArtResources import AudioBlob +import ContextUI private struct FetchControls { let fetch: () -> Void @@ -38,8 +39,9 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { private let consumableContentNode: ASImageNode private var iconNode: TransformImageNode? + let statusContainerNode: ContextExtractedContentContainingNode private var statusNode: SemanticStatusNode? - private var playbackAudioLevelView: VoiceBlobView? + private var playbackAudioLevelNode: VoiceBlobNode? private var streamingStatusNode: SemanticStatusNode? private var tapRecognizer: UITapGestureRecognizer? @@ -71,7 +73,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { guard self.visibility != oldValue else { return } if !self.visibility { - self.playbackAudioLevelView?.stopAnimating() + self.playbackAudioLevelNode?.stopAnimating() } } } @@ -129,6 +131,8 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { self.dateAndStatusNode = ChatMessageDateAndStatusNode() self.consumableContentNode = ASImageNode() + + self.statusContainerNode = ContextExtractedContentContainingNode() super.init() @@ -136,6 +140,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { self.addSubnode(self.descriptionNode) self.addSubnode(self.fetchingTextNode) self.addSubnode(self.fetchingCompactTextNode) + self.addSubnode(self.statusContainerNode) } deinit { @@ -175,7 +180,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { case let .fetchStatus(fetchStatus): if let context = self.context, let message = self.message, message.flags.isSending { let _ = context.account.postbox.transaction({ transaction -> Void in - deleteMessages(transaction: transaction, mediaBox: context.account.postbox.mediaBox, ids: [message.id]) + context.engine.messages.deleteMessages(transaction: transaction, ids: [message.id]) }).start() } else { switch fetchStatus { @@ -222,7 +227,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { return { context, presentationData, message, associatedData, chatLocation, attributes, isPinned, forcedIsEdited, file, automaticDownload, incoming, isRecentActions, forcedResourceStatus, dateAndStatusType, messageSelection, 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 descriptionFont = Font.with(size: floor(presentationData.fontSize.baseDisplaySize * 13.0 / 17.0), design: .regular, weight: .regular, traits: [.monospacedNumbers]) let durationFont = Font.regular(floor(presentationData.fontSize.baseDisplaySize * 11.0 / 17.0)) var updateImageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>? @@ -384,7 +389,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { if let performer = performer { descriptionText = performer } else if let size = file.size { - descriptionText = dataSizeString(size, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator) + descriptionText = dataSizeString(size, formatting: DataSizeStringFormatting(chatPresentationData: presentationData)) } else { descriptionText = "" } @@ -408,7 +413,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { } else if !isVoice { let descriptionText: String if let size = file.size { - descriptionText = dataSizeString(size, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator) + descriptionText = dataSizeString(size, formatting: DataSizeStringFormatting(chatPresentationData: presentationData)) } else { descriptionText = "" } @@ -463,7 +468,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, bubbleCorners: presentationData.chatBubbleCorners) + let principalGraphics = PresentationResourcesChat.principalGraphics(theme: presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper, bubbleCorners: presentationData.chatBubbleCorners) fileIconImage = incoming ? principalGraphics.radialIndicatorFileIconIncoming : principalGraphics.radialIndicatorFileIconOutgoing } @@ -666,7 +671,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { return } strongSelf.inputAudioLevel = CGFloat(value) - strongSelf.playbackAudioLevelView?.updateLevel(CGFloat(value)) + strongSelf.playbackAudioLevelNode?.updateLevel(CGFloat(value)) })) } @@ -683,12 +688,17 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { strongSelf.waveformNode.displaysAsynchronously = !presentationData.isPreview strongSelf.statusNode?.displaysAsynchronously = !presentationData.isPreview - strongSelf.statusNode?.frame = progressFrame - strongSelf.playbackAudioLevelView?.frame = progressFrame.insetBy(dx: -12.0, dy: -12.0) + strongSelf.statusNode?.frame = CGRect(origin: CGPoint(), size: progressFrame.size) + + strongSelf.statusContainerNode.frame = progressFrame + strongSelf.statusContainerNode.contentRect = CGRect(origin: CGPoint(), size: progressFrame.size) + strongSelf.statusContainerNode.contentNode.frame = CGRect(origin: CGPoint(), size: progressFrame.size) + + strongSelf.playbackAudioLevelNode?.frame = progressFrame.insetBy(dx: -12.0, dy: -12.0) strongSelf.progressFrame = progressFrame strongSelf.streamingCacheStatusFrame = streamingCacheStatusFrame strongSelf.fileIconImage = fileIconImage - + if let updatedFetchControls = updatedFetchControls { let _ = strongSelf.fetchControls.swap(updatedFetchControls) if automaticDownload { @@ -818,9 +828,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: 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) + let compactString = dataSizeString(Int(Float(size) * progress), forceDecimal: true, formatting: DataSizeStringFormatting(chatPresentationData: presentationData)) + let descriptionFont = Font.with(size: floor(presentationData.fontSize.baseDisplaySize * 13.0 / 17.0), design: .regular, weight: .regular, traits: [.monospacedNumbers]) + downloadingStrings = ("\(compactString) / \(dataSizeString(size, forceDecimal: true, formatting: DataSizeStringFormatting(chatPresentationData: presentationData)))", compactString, descriptionFont) } default: break @@ -937,23 +947,28 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { } let statusNode = SemanticStatusNode(backgroundNodeColor: backgroundNodeColor, foregroundNodeColor: foregroundNodeColor, image: image, overlayForegroundNodeColor: presentationData.theme.theme.chat.message.mediaOverlayControlColors.foregroundColor) self.statusNode = statusNode - statusNode.frame = progressFrame - self.addSubnode(statusNode) + + self.statusContainerNode.contentNode.insertSubnode(statusNode, at: 0) + self.statusContainerNode.frame = progressFrame + self.statusContainerNode.contentRect = CGRect(origin: CGPoint(), size: progressFrame.size) + self.statusContainerNode.contentNode.frame = CGRect(origin: CGPoint(), size: progressFrame.size) + statusNode.frame = CGRect(origin: CGPoint(), size: progressFrame.size) } else if let statusNode = self.statusNode { statusNode.backgroundNodeColor = backgroundNodeColor } - if state != .none && isVoice && self.playbackAudioLevelView == nil && false { + if state != .none && isVoice && self.playbackAudioLevelNode == nil { let blobFrame = progressFrame.insetBy(dx: -12.0, dy: -12.0) - let playbackAudioLevelView = VoiceBlobView( - frame: blobFrame, + let playbackAudioLevelNode = VoiceBlobNode( maxLevel: 0.3, smallBlobRange: (0, 0), mediumBlobRange: (0.7, 0.8), bigBlobRange: (0.8, 0.9) ) - self.playbackAudioLevelView = playbackAudioLevelView - self.view.addSubview(playbackAudioLevelView) + playbackAudioLevelNode.isUserInteractionEnabled = false + playbackAudioLevelNode.frame = blobFrame + self.playbackAudioLevelNode = playbackAudioLevelNode + self.insertSubnode(playbackAudioLevelNode, belowSubnode: self.statusContainerNode) let maskRect = CGRect(origin: .zero, size: blobFrame.size) let playbackMaskLayer = CAShapeLayer() @@ -963,9 +978,9 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { maskPath.append(UIBezierPath(roundedRect: maskRect.insetBy(dx: 12, dy: 12), cornerRadius: 22)) maskPath.append(UIBezierPath(rect: maskRect)) playbackMaskLayer.path = maskPath.cgPath - playbackAudioLevelView.layer.mask = playbackMaskLayer + playbackAudioLevelNode.layer.mask = playbackMaskLayer } - self.playbackAudioLevelView?.setColor(presentationData.theme.theme.chat.inputPanel.actionControlFillColor) + self.playbackAudioLevelNode?.setColor(messageTheme.mediaActiveControlColor) if streamingState != .none && self.streamingStatusNode == nil { let streamingStatusNode = SemanticStatusNode(backgroundNodeColor: backgroundNodeColor, foregroundNodeColor: foregroundNodeColor) @@ -992,9 +1007,9 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { switch state { case .pause: - self.playbackAudioLevelView?.startAnimating() + self.playbackAudioLevelNode?.startAnimating() default: - self.playbackAudioLevelView?.stopAnimating() + self.playbackAudioLevelNode?.stopAnimating() } } diff --git a/submodules/TelegramUI/Sources/ChatMessageInteractiveInstantVideoNode.swift b/submodules/TelegramUI/Sources/ChatMessageInteractiveInstantVideoNode.swift index b11555a80f..b263ed4117 100644 --- a/submodules/TelegramUI/Sources/ChatMessageInteractiveInstantVideoNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageInteractiveInstantVideoNode.swift @@ -39,7 +39,7 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { private var statusNode: RadialStatusNode? private var playbackStatusNode: InstantVideoRadialStatusNode? - private var videoFrame: CGRect? + private(set) var videoFrame: CGRect? private var item: ChatMessageBubbleContentItem? private var automaticDownload: Bool? @@ -47,7 +47,8 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { private var secretProgressIcon: UIImage? private let fetchDisposable = MetaDisposable() - + + private var durationBackgroundNode: NavigationBackgroundNode? private var durationNode: ChatInstantVideoMessageDurationNode? private let dateAndStatusNode: ChatMessageDateAndStatusNode @@ -82,6 +83,8 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { } } + private var animating = false + override init() { self.secretVideoPlaceholderBackground = ASImageNode() self.secretVideoPlaceholderBackground.isLayerBacked = true @@ -129,7 +132,7 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { self.view.addGestureRecognizer(recognizer) } - func asyncLayout() -> (_ item: ChatMessageBubbleContentItem, _ width: CGFloat, _ displaySize: CGSize, _ statusType: ChatMessageInteractiveInstantVideoNodeStatusType, _ automaticDownload: Bool) -> (ChatMessageInstantVideoItemLayoutResult, (ChatMessageInstantVideoItemLayoutData, ContainedViewLayoutTransition) -> Void) { + func asyncLayout() -> (_ item: ChatMessageBubbleContentItem, _ width: CGFloat, _ displaySize: CGSize, _ maximumDisplaySize: CGSize, _ scaleProgress: CGFloat, _ statusType: ChatMessageInteractiveInstantVideoNodeStatusType, _ automaticDownload: Bool) -> (ChatMessageInstantVideoItemLayoutResult, (ChatMessageInstantVideoItemLayoutData, ContainedViewLayoutTransition) -> Void) { let previousFile = self.media let currentItem = self.item @@ -137,7 +140,7 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { let makeDateAndStatusLayout = self.dateAndStatusNode.asyncLayout() - return { item, width, displaySize, statusDisplayType, automaticDownload in + return { item, width, displaySize, maximumDisplaySize, scaleProgress, statusDisplayType, automaticDownload in var secretVideoPlaceholderBackgroundImage: UIImage? var updatedInfoBackgroundImage: UIImage? var updatedMuteIconImage: UIImage? @@ -162,7 +165,8 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { secretVideoPlaceholderBackgroundImage = PresentationResourcesChat.chatInstantVideoBackgroundImage(theme.theme, wallpaper: !theme.wallpaper.isEmpty) } - let imageSize = displaySize + let imageSize = maximumDisplaySize + let imageScale = displaySize.width / maximumDisplaySize.width let updatedMessageId = item.message.id != currentItem?.message.id @@ -293,20 +297,24 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { let (dateAndStatusSize, dateAndStatusApply) = makeDateAndStatusLayout(item.context, item.presentationData, edited && !sentViaBot, viewCount, dateText, statusType, CGSize(width: max(1.0, maxDateAndStatusWidth), height: CGFloat.greatestFiniteMagnitude), dateReactions, dateReplies, item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread, item.message.isSelfExpiring) - var contentSize = imageSize + var displayVideoFrame = videoFrame + displayVideoFrame.size.width *= imageScale + displayVideoFrame.size.height *= imageScale + + var contentSize = displayVideoFrame.size var dateAndStatusOverflow = false - if case .bubble = statusDisplayType, videoFrame.maxX + dateAndStatusSize.width > width { + if case .bubble = statusDisplayType, displayVideoFrame.maxX + dateAndStatusSize.width > width { contentSize.height += dateAndStatusSize.height + 2.0 contentSize.width = max(contentSize.width, dateAndStatusSize.width) dateAndStatusOverflow = true } - let result = ChatMessageInstantVideoItemLayoutResult(contentSize: contentSize, overflowLeft: 0.0, overflowRight: dateAndStatusOverflow ? 0.0 : (max(0.0, floor(videoFrame.midX) + 55.0 + dateAndStatusSize.width - videoFrame.width))) + let result = ChatMessageInstantVideoItemLayoutResult(contentSize: contentSize, overflowLeft: 0.0, overflowRight: dateAndStatusOverflow ? 0.0 : (max(0.0, floorToScreenPixels(videoFrame.midX) + 55.0 + dateAndStatusSize.width - videoFrame.width))) return (result, { [weak self] layoutData, transition in if let strongSelf = self { strongSelf.item = item - strongSelf.videoFrame = videoFrame + strongSelf.videoFrame = displayVideoFrame strongSelf.secretProgressIcon = secretProgressIcon strongSelf.automaticDownload = automaticDownload @@ -326,10 +334,10 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { if let infoBackgroundImage = strongSelf.infoBackgroundNode.image, let muteImage = strongSelf.muteIconNode.image { let infoWidth = muteImage.size.width - let infoBackgroundFrame = CGRect(origin: CGPoint(x: floor(videoFrame.minX + (videoFrame.size.width - infoWidth) / 2.0), y: videoFrame.maxY - infoBackgroundImage.size.height - 8.0), size: CGSize(width: infoWidth, height: infoBackgroundImage.size.height)) - transition.updateFrame(node: strongSelf.infoBackgroundNode, frame: infoBackgroundFrame) + let infoBackgroundFrame = CGRect(origin: CGPoint(x: floorToScreenPixels(displayVideoFrame.minX + (displayVideoFrame.size.width - infoWidth) / 2.0), y: displayVideoFrame.maxY - infoBackgroundImage.size.height - 8.0), size: CGSize(width: infoWidth, height: infoBackgroundImage.size.height)) + strongSelf.infoBackgroundNode.frame = infoBackgroundFrame let muteIconFrame = CGRect(origin: CGPoint(x: infoBackgroundFrame.width - muteImage.size.width, y: 0.0), size: muteImage.size) - transition.updateFrame(node: strongSelf.muteIconNode, frame: muteIconFrame) + strongSelf.muteIconNode.frame = muteIconFrame } if let updatedFile = updatedFile, updatedMedia { @@ -339,47 +347,70 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { strongSelf.fetchedThumbnailDisposable.set(nil) } } - + dateAndStatusApply(false) switch layoutData { case let .unconstrained(width): let dateAndStatusOrigin: CGPoint if dateAndStatusOverflow { - dateAndStatusOrigin = CGPoint(x: videoFrame.minX - 4.0, y: videoFrame.maxY + 2.0) + dateAndStatusOrigin = CGPoint(x: displayVideoFrame.minX - 4.0, y: displayVideoFrame.maxY + 2.0) } else { - dateAndStatusOrigin = CGPoint(x: min(floor(videoFrame.midX) + 55.0, width - dateAndStatusSize.width - 4.0), y: videoFrame.height - dateAndStatusSize.height) + dateAndStatusOrigin = CGPoint(x: min(floorToScreenPixels(displayVideoFrame.midX) + 55.0 + 25.0 * scaleProgress, width - dateAndStatusSize.width - 4.0), y: displayVideoFrame.height - dateAndStatusSize.height) } strongSelf.dateAndStatusNode.frame = CGRect(origin: dateAndStatusOrigin, size: dateAndStatusSize) case let .constrained(_, right): - strongSelf.dateAndStatusNode.frame = CGRect(origin: CGPoint(x: min(floor(videoFrame.midX) + 55.0, videoFrame.maxX + right - dateAndStatusSize.width - 4.0), y: videoFrame.maxY - dateAndStatusSize.height), size: dateAndStatusSize) + strongSelf.dateAndStatusNode.frame = CGRect(origin: CGPoint(x: min(floorToScreenPixels(displayVideoFrame.midX) + 55.0 + 25.0 * scaleProgress, displayVideoFrame.maxX + right - dateAndStatusSize.width - 4.0), y: displayVideoFrame.maxY - dateAndStatusSize.height), size: dateAndStatusSize) } - + var updatedPlayerStatusSignal: Signal? if let telegramFile = updatedFile { if updatedMedia { let durationTextColor: UIColor - let durationFillColor: UIColor + let durationBlurColor: (UIColor, Bool)? switch statusDisplayType { case .free: let serviceColor = serviceMessageColorComponents(theme: theme.theme, wallpaper: theme.wallpaper) durationTextColor = serviceColor.primaryText - durationFillColor = serviceColor.fill + durationBlurColor = (selectDateFillStaticColor(theme: theme.theme, wallpaper: theme.wallpaper), dateFillNeedsBlur(theme: theme.theme, wallpaper: theme.wallpaper)) case .bubble: - durationFillColor = .clear + durationBlurColor = nil if item.message.effectivelyIncoming(item.context.account.peerId) { durationTextColor = theme.theme.chat.message.incoming.secondaryTextColor } else { durationTextColor = theme.theme.chat.message.outgoing.secondaryTextColor } } + + if let durationBlurColor = durationBlurColor { + if let durationBackgroundNode = strongSelf.durationBackgroundNode { + durationBackgroundNode.updateColor(color: durationBlurColor.0, enableBlur: durationBlurColor.1, transition: .immediate) + } else { + let durationBackgroundNode = NavigationBackgroundNode(color: durationBlurColor.0, enableBlur: durationBlurColor.1) + strongSelf.durationBackgroundNode = durationBackgroundNode + strongSelf.addSubnode(durationBackgroundNode) + } + } else if let durationBackgroundNode = strongSelf.durationBackgroundNode { + strongSelf.durationBackgroundNode = nil + durationBackgroundNode.removeFromSupernode() + } + let durationNode: ChatInstantVideoMessageDurationNode if let current = strongSelf.durationNode { durationNode = current - current.updateTheme(textColor: durationTextColor, fillColor: durationFillColor) + current.updateTheme(textColor: durationTextColor) } else { - durationNode = ChatInstantVideoMessageDurationNode(textColor: durationTextColor, fillColor: durationFillColor) + durationNode = ChatInstantVideoMessageDurationNode(textColor: durationTextColor) strongSelf.durationNode = durationNode strongSelf.addSubnode(durationNode) + durationNode.sizeUpdated = { [weak strongSelf] size in + guard let strongSelf = strongSelf else { + return + } + if let durationBackgroundNode = strongSelf.durationBackgroundNode, let durationNode = strongSelf.durationNode { + durationBackgroundNode.frame = CGRect(origin: CGPoint(x: durationNode.frame.maxX - size.width, y: durationNode.frame.minY), size: size) + durationBackgroundNode.update(size: size, cornerRadius: size.height / 2.0, transition: .immediate) + } + } } durationNode.defaultDuration = telegramFile.duration.flatMap(Double.init) @@ -447,17 +478,24 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { } if let durationNode = strongSelf.durationNode { - durationNode.frame = CGRect(origin: CGPoint(x: videoFrame.midX - 56.0, y: videoFrame.maxY - 18.0), size: CGSize(width: 1.0, height: 1.0)) + durationNode.frame = CGRect(origin: CGPoint(x: displayVideoFrame.midX - 56.0 - 25.0 * scaleProgress, y: displayVideoFrame.maxY - 18.0), size: CGSize(width: 1.0, height: 1.0)) durationNode.isSeen = !notConsumed + let size = durationNode.size + if let durationBackgroundNode = strongSelf.durationBackgroundNode, size.width > 1.0 { + durationBackgroundNode.frame = CGRect(origin: CGPoint(x: durationNode.frame.maxX - size.width, y: durationNode.frame.minY), size: size) + durationBackgroundNode.update(size: size, cornerRadius: size.height / 2.0, transition: .immediate) + } } if let videoNode = strongSelf.videoNode { - videoNode.frame = videoFrame + videoNode.bounds = CGRect(origin: CGPoint(), size: videoFrame.size) + videoNode.transform = CATransform3DMakeScale(imageScale, imageScale, 1.0) + videoNode.position = displayVideoFrame.center videoNode.updateLayout(size: arguments.boundingSize, transition: .immediate) } - strongSelf.secretVideoPlaceholderBackground.frame = videoFrame + strongSelf.secretVideoPlaceholderBackground.frame = displayVideoFrame - let placeholderFrame = videoFrame.insetBy(dx: 2.0, dy: 2.0) + let placeholderFrame = displayVideoFrame.insetBy(dx: 2.0, dy: 2.0) strongSelf.secretVideoPlaceholder.frame = placeholderFrame let makeSecretPlaceholderLayout = strongSelf.secretVideoPlaceholder.asyncLayout() let arguments = TransformImageArguments(corners: ImageCorners(radius: placeholderFrame.size.width / 2.0), imageSize: placeholderFrame.size, boundingSize: placeholderFrame.size, intrinsicInsets: UIEdgeInsets()) @@ -570,7 +608,7 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { if self.statusNode == nil { let statusNode = RadialStatusNode(backgroundNodeColor: item.presentationData.theme.theme.chat.message.mediaOverlayControlColors.fillColor) self.isUserInteractionEnabled = false - statusNode.frame = CGRect(origin: CGPoint(x: videoFrame.origin.x + floor((videoFrame.size.width - 50.0) / 2.0), y: videoFrame.origin.y + floor((videoFrame.size.height - 50.0) / 2.0)), size: CGSize(width: 50.0, height: 50.0)) + statusNode.frame = CGRect(origin: CGPoint(x: videoFrame.origin.x + floorToScreenPixels((videoFrame.size.width - 50.0) / 2.0), y: videoFrame.origin.y + floorToScreenPixels((videoFrame.size.height - 50.0) / 2.0)), size: CGSize(width: 50.0, height: 50.0)) self.statusNode = statusNode self.addSubnode(statusNode) } @@ -642,7 +680,7 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { if let current = self.playbackStatusNode { playbackStatusNode = current } else { - playbackStatusNode = InstantVideoRadialStatusNode(color: UIColor(white: 1.0, alpha: 0.8)) + playbackStatusNode = InstantVideoRadialStatusNode(color: UIColor(white: 1.0, alpha: 0.6)) self.addSubnode(playbackStatusNode) self.playbackStatusNode = playbackStatusNode } @@ -741,7 +779,7 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { if item.message.flags.isSending { let messageId = item.message.id let _ = item.context.account.postbox.transaction({ transaction -> Void in - deleteMessages(transaction: transaction, mediaBox: item.context.account.postbox.mediaBox, ids: [messageId]) + item.context.engine.messages.deleteMessages(transaction: transaction, ids: [messageId]) }).start() } else { messageMediaFileCancelInteractiveFetch(context: item.context, messageId: item.message.id, file: file) @@ -771,16 +809,16 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { return nil } - static func asyncLayout(_ node: ChatMessageInteractiveInstantVideoNode?) -> (_ item: ChatMessageBubbleContentItem, _ width: CGFloat, _ displaySize: CGSize, _ statusType: ChatMessageInteractiveInstantVideoNodeStatusType, _ automaticDownload: Bool) -> (ChatMessageInstantVideoItemLayoutResult, (ChatMessageInstantVideoItemLayoutData, ContainedViewLayoutTransition) -> ChatMessageInteractiveInstantVideoNode) { + static func asyncLayout(_ node: ChatMessageInteractiveInstantVideoNode?) -> (_ item: ChatMessageBubbleContentItem, _ width: CGFloat, _ displaySize: CGSize, _ maximumDisplaySize: CGSize, _ scaleProgress: CGFloat, _ statusType: ChatMessageInteractiveInstantVideoNodeStatusType, _ automaticDownload: Bool) -> (ChatMessageInstantVideoItemLayoutResult, (ChatMessageInstantVideoItemLayoutData, ContainedViewLayoutTransition) -> ChatMessageInteractiveInstantVideoNode) { let makeLayout = node?.asyncLayout() - return { item, width, displaySize, statusType, automaticDownload in + return { item, width, displaySize, maximumDisplaySize, scaleProgress, statusType, automaticDownload in var createdNode: ChatMessageInteractiveInstantVideoNode? let sizeAndApplyLayout: (ChatMessageInstantVideoItemLayoutResult, (ChatMessageInstantVideoItemLayoutData, ContainedViewLayoutTransition) -> Void) if let makeLayout = makeLayout { - sizeAndApplyLayout = makeLayout(item, width, displaySize, statusType, automaticDownload) + sizeAndApplyLayout = makeLayout(item, width, displaySize, maximumDisplaySize, scaleProgress, statusType, automaticDownload) } else { let node = ChatMessageInteractiveInstantVideoNode() - sizeAndApplyLayout = node.asyncLayout()(item, width, displaySize, statusType, automaticDownload) + sizeAndApplyLayout = node.asyncLayout()(item, width, displaySize, maximumDisplaySize, scaleProgress, statusType, automaticDownload) createdNode = node } return (sizeAndApplyLayout.0, { [weak node] layoutData, transition in @@ -832,5 +870,28 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { return nil } } + + func animateFromSnapshot(snapshotView: UIView, transition: CombinedTransition) { + guard let videoFrame = self.videoFrame else { + return + } + + let scale = videoFrame.height / snapshotView.frame.height + snapshotView.transform = CGAffineTransform(scaleX: scale, y: scale) + snapshotView.center = CGPoint(x: videoFrame.midX, y: videoFrame.midY) + + self.view.addSubview(snapshotView) + + transition.horizontal.updateAlpha(layer: snapshotView.layer, alpha: 0.0, completion: { [weak snapshotView] _ in + snapshotView?.removeFromSuperview() + }) + + transition.horizontal.animateTransformScale(node: self, from: 1.0 / scale) + + self.dateAndStatusNode.layer.animateAlpha(from: 0.0, to: self.dateAndStatusNode.alpha, duration: 0.15, delay: 0.18) + if let durationNode = self.durationNode { + durationNode.layer.animateAlpha(from: 0.0, to: durationNode.alpha, duration: 0.15, delay: 0.18) + } + } } diff --git a/submodules/TelegramUI/Sources/ChatMessageInteractiveMediaNode.swift b/submodules/TelegramUI/Sources/ChatMessageInteractiveMediaNode.swift index 845d91e9e9..37419dec04 100644 --- a/submodules/TelegramUI/Sources/ChatMessageInteractiveMediaNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageInteractiveMediaNode.swift @@ -22,6 +22,7 @@ import TelegramAnimatedStickerNode import LocalMediaResources import WallpaperResources import ChatMessageInteractiveMediaBadge +import ContextUI private struct FetchControls { let fetch: (Bool) -> Void @@ -64,9 +65,23 @@ enum InteractiveMediaNodePlayWithSoundMode { case loop } +struct ChatMessageDateAndStatus { + var type: ChatMessageDateAndStatusType + var edited: Bool + var viewCount: Int? + var dateReplies: Int + var dateReactions: [MessageReaction] + var isPinned: Bool + var dateText: String +} + final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitionNode { + private let pinchContainerNode: PinchSourceContainerNode private let imageNode: TransformImageNode private var currentImageArguments: TransformImageArguments? + private var currentHighQualityImageSignal: (Signal<(TransformImageArguments) -> DrawingContext?, NoError>, CGSize)? + private var highQualityImageNode: TransformImageNode? + private var videoNode: UniversalVideoNode? private var videoContent: NativeVideoContent? private var animatedStickerNode: AnimatedStickerNode? @@ -75,6 +90,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio var decoration: UniversalVideoDecoration? { return self.videoNodeDecoration } + let dateAndStatusNode: ChatMessageDateAndStatusNode private var badgeNode: ChatMessageInteractiveMediaBadge? private var tapRecognizer: UITapGestureRecognizer? @@ -134,15 +150,103 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio } var activateLocalContent: (InteractiveMediaNodeActivateContent) -> Void = { _ in } + var activatePinch: ((PinchSourceContainerNode) -> Void)? override init() { + self.pinchContainerNode = PinchSourceContainerNode() + + self.dateAndStatusNode = ChatMessageDateAndStatusNode() + self.imageNode = TransformImageNode() self.imageNode.contentAnimations = [.subsequentUpdates] super.init() + + self.addSubnode(self.pinchContainerNode) self.imageNode.displaysAsynchronously = false - self.addSubnode(self.imageNode) + self.pinchContainerNode.contentNode.addSubnode(self.imageNode) + + self.pinchContainerNode.activate = { [weak self] sourceNode in + guard let strongSelf = self else { + return + } + strongSelf.activatePinch?(sourceNode) + } + + self.pinchContainerNode.scaleUpdated = { [weak self] scale, transition in + guard let strongSelf = self else { + return + } + + let factor: CGFloat = max(0.0, min(1.0, (scale - 1.0) * 8.0)) + + transition.updateAlpha(node: strongSelf.dateAndStatusNode, alpha: 1.0 - factor) + + if abs(scale - 1.0) > CGFloat.ulpOfOne { + var highQualityImageNode: TransformImageNode? + if let current = strongSelf.highQualityImageNode { + highQualityImageNode = current + } else if let (currentHighQualityImageSignal, nativeImageSize) = strongSelf.currentHighQualityImageSignal, let currentImageArguments = strongSelf.currentImageArguments { + let imageNode = TransformImageNode() + imageNode.frame = strongSelf.imageNode.frame + + let corners = currentImageArguments.corners + if isRoundEqualCorners(corners) { + imageNode.cornerRadius = corners.topLeft.radius + imageNode.layer.mask = nil + } else { + imageNode.cornerRadius = 0 + + let boundingSize: CGSize = CGSize(width: max(corners.topLeft.radius, corners.bottomLeft.radius) + max(corners.topRight.radius, corners.bottomRight.radius), height: max(corners.topLeft.radius, corners.topRight.radius) + max(corners.bottomLeft.radius, corners.bottomRight.radius)) + let size: CGSize = CGSize(width: boundingSize.width + corners.extendedEdges.left + corners.extendedEdges.right, height: boundingSize.height + corners.extendedEdges.top + corners.extendedEdges.bottom) + let arguments = TransformImageArguments(corners: corners, imageSize: size, boundingSize: boundingSize, intrinsicInsets: UIEdgeInsets()) + let context = DrawingContext(size: size, clear: true) + context.withContext { ctx in + ctx.setFillColor(UIColor.black.cgColor) + ctx.fill(arguments.drawingRect) + } + addCorners(context, arguments: arguments) + + if let maskImage = context.generateImage() { + let mask = CALayer() + mask.contents = maskImage.cgImage + mask.contentsScale = maskImage.scale + mask.contentsCenter = CGRect(x: max(corners.topLeft.radius, corners.bottomLeft.radius) / maskImage.size.width, y: max(corners.topLeft.radius, corners.topRight.radius) / maskImage.size.height, width: (maskImage.size.width - max(corners.topLeft.radius, corners.bottomLeft.radius) - max(corners.topRight.radius, corners.bottomRight.radius)) / maskImage.size.width, height: (maskImage.size.height - max(corners.topLeft.radius, corners.topRight.radius) - max(corners.bottomLeft.radius, corners.bottomRight.radius)) / maskImage.size.height) + + imageNode.layer.mask = mask + imageNode.layer.mask?.frame = imageNode.bounds + } + } + + strongSelf.pinchContainerNode.contentNode.insertSubnode(imageNode, aboveSubnode: strongSelf.imageNode) + + let scaleFactor = nativeImageSize.height / currentImageArguments.imageSize.height + + let apply = imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: CGSize(width: currentImageArguments.imageSize.width * scaleFactor, height: currentImageArguments.imageSize.height * scaleFactor), boundingSize: CGSize(width: currentImageArguments.boundingSize.width * scaleFactor, height: currentImageArguments.boundingSize.height * scaleFactor), intrinsicInsets: UIEdgeInsets(top: currentImageArguments.intrinsicInsets.top * scaleFactor, left: currentImageArguments.intrinsicInsets.left * scaleFactor, bottom: currentImageArguments.intrinsicInsets.bottom * scaleFactor, right: currentImageArguments.intrinsicInsets.right * scaleFactor))) + let _ = apply() + imageNode.setSignal(currentHighQualityImageSignal, attemptSynchronously: false) + + highQualityImageNode = imageNode + strongSelf.highQualityImageNode = imageNode + } + if let highQualityImageNode = highQualityImageNode { + transition.updateAlpha(node: highQualityImageNode, alpha: factor) + } + } else if let highQualityImageNode = strongSelf.highQualityImageNode { + strongSelf.highQualityImageNode = nil + transition.updateAlpha(node: highQualityImageNode, alpha: 0.0, completion: { [weak highQualityImageNode] _ in + highQualityImageNode?.removeFromSupernode() + }) + } + + if let badgeNode = strongSelf.badgeNode { + transition.updateAlpha(node: badgeNode, alpha: 1.0 - factor) + } + if let statusNode = strongSelf.statusNode { + transition.updateAlpha(node: statusNode, alpha: 1.0 - factor) + } + } } deinit { @@ -195,7 +299,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio case .Fetching: if let context = self.context, let message = self.message, message.flags.isSending { let _ = context.account.postbox.transaction({ transaction -> Void in - deleteMessages(transaction: transaction, mediaBox: context.account.postbox.mediaBox, ids: [message.id]) + context.engine.messages.deleteMessages(transaction: transaction, ids: [message.id]) }).start() } else if let media = media, let context = self.context, let message = message { if let media = media as? TelegramMediaFile { @@ -242,10 +346,11 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio } } - 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))) { + func asyncLayout() -> (_ context: AccountContext, _ presentationData: ChatPresentationData, _ dateTimeFormat: PresentationDateTimeFormat, _ message: Message, _ attributes: ChatMessageEntryAttributes, _ media: Media, _ dateAndStatus: ChatMessageDateAndStatus?, _ 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() + let statusLayout = self.dateAndStatusNode.asyncLayout() let currentVideoNode = self.videoNode let currentAnimatedStickerNode = self.animatedStickerNode @@ -255,7 +360,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio let currentAutomaticDownload = self.automaticDownload let currentAutomaticPlayback = self.automaticPlayback - return { [weak self] context, theme, strings, dateTimeFormat, message, attributes, media, automaticDownload, peerType, sizeCalculation, layoutConstants, contentMode in + return { [weak self] context, presentationData, dateTimeFormat, message, attributes, media, dateAndStatus, automaticDownload, peerType, sizeCalculation, layoutConstants, contentMode in var nativeSize: CGSize let isSecretMedia = message.containsSecretMedia @@ -359,6 +464,15 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio case .unconstrained: nativeSize = unboundSize } + + var statusSize = CGSize() + var statusApply: ((Bool) -> Void)? + + if let dateAndStatus = dateAndStatus { + let (size, apply) = statusLayout(context, presentationData, dateAndStatus.edited, dateAndStatus.viewCount, dateAndStatus.dateText, dateAndStatus.type, CGSize(width: nativeSize.width - 30.0, height: CGFloat.greatestFiniteMagnitude), dateAndStatus.dateReactions, dateAndStatus.dateReplies, dateAndStatus.isPinned, message.isSelfExpiring) + statusSize = size + statusApply = apply + } let maxWidth: CGFloat if isSecretMedia { @@ -367,7 +481,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio maxWidth = maxDimensions.width } if isSecretMedia { - let _ = PresentationResourcesChat.chatBubbleSecretMediaIcon(theme) + let _ = PresentationResourcesChat.chatBubbleSecretMediaIcon(presentationData.theme.theme) } return (nativeSize, maxWidth, { constrainedSize, automaticPlayback, wideLayout, corners in @@ -416,7 +530,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio drawingSize = nativeSize.aspectFilled(boundingSize) } - var updateImageSignal: ((Bool) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError>)? + var updateImageSignal: ((Bool, Bool) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError>)? var updatedStatusSignal: Signal<(MediaResourceStatus, MediaResourceStatus?), NoError>? var updatedFetchControls: FetchControls? @@ -453,16 +567,31 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio if isSticker { emptyColor = .clear } else { - emptyColor = message.effectivelyIncoming(context.account.peerId) ? theme.chat.message.incoming.mediaPlaceholderColor : theme.chat.message.outgoing.mediaPlaceholderColor + emptyColor = message.effectivelyIncoming(context.account.peerId) ? presentationData.theme.theme.chat.message.incoming.mediaPlaceholderColor : presentationData.theme.theme.chat.message.outgoing.mediaPlaceholderColor } if let wallpaper = media as? WallpaperPreviewMedia { - if case let .file(_, patternColor, patternBottomColor, rotation, _, _) = wallpaper.content { + if case let .file(_, patternColors, rotation, intensity, _, _) = wallpaper.content { var colors: [UIColor] = [] - colors.append(patternColor ?? UIColor(rgb: 0xd6e2ee, alpha: 0.5)) - if let patternBottomColor = patternBottomColor { - colors.append(patternBottomColor) + var customPatternColor: UIColor? = nil + var bakePatternAlpha: CGFloat = 1.0 + if let intensity = intensity, intensity < 0 { + if patternColors.isEmpty { + colors.append(UIColor(rgb: 0xd6e2ee, alpha: 0.5)) + } else { + colors.append(contentsOf: patternColors.map(UIColor.init(rgb:))) + } + customPatternColor = UIColor(white: 0.0, alpha: 1.0 - CGFloat(abs(intensity))) + } else { + if patternColors.isEmpty { + colors.append(UIColor(rgb: 0xd6e2ee, alpha: 0.5)) + } else { + colors.append(contentsOf: patternColors.map(UIColor.init(rgb:))) + } + let isLight = UIColor.average(of: patternColors.map(UIColor.init(rgb:))).hsb.b > 0.3 + customPatternColor = isLight ? .black : .white + bakePatternAlpha = CGFloat(intensity ?? 50) / 100.0 } - patternArguments = PatternWallpaperArguments(colors: colors, rotation: rotation) + patternArguments = PatternWallpaperArguments(colors: colors, rotation: rotation, customPatternColor: customPatternColor, bakePatternAlpha: bakePatternAlpha) } } @@ -475,12 +604,12 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio replaceAnimatedStickerNode = true } if isSecretMedia { - updateImageSignal = { synchronousLoad in + updateImageSignal = { synchronousLoad, _ in return chatSecretPhoto(account: context.account, photoReference: .message(message: MessageReference(message), media: image)) } } else { - updateImageSignal = { synchronousLoad in - return chatMessagePhoto(postbox: context.account.postbox, photoReference: .message(message: MessageReference(message), media: image), synchronousLoad: synchronousLoad) + updateImageSignal = { synchronousLoad, highQuality in + return chatMessagePhoto(postbox: context.account.postbox, photoReference: .message(message: MessageReference(message), media: image), synchronousLoad: synchronousLoad, highQuality: highQuality) } } @@ -505,7 +634,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio if hasCurrentAnimatedStickerNode { replaceAnimatedStickerNode = true } - updateImageSignal = { synchronousLoad in + updateImageSignal = { synchronousLoad, _ in return chatWebFileImage(account: context.account, file: image) } @@ -518,22 +647,22 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio }) } else if let file = media as? TelegramMediaFile { if isSecretMedia { - updateImageSignal = { synchronousLoad in + updateImageSignal = { synchronousLoad, _ in return chatSecretMessageVideo(account: context.account, videoReference: .message(message: MessageReference(message), media: file)) } } else { if file.isAnimatedSticker { let dimensions = file.dimensions ?? PixelDimensions(width: 512, height: 512) - updateImageSignal = { synchronousLoad in + updateImageSignal = { synchronousLoad, _ in return chatMessageAnimatedSticker(postbox: context.account.postbox, file: file, small: false, size: dimensions.cgSize.aspectFitted(CGSize(width: 400.0, height: 400.0))) } } else if file.isSticker { - updateImageSignal = { synchronousLoad in + updateImageSignal = { synchronousLoad, _ in return chatMessageSticker(account: context.account, file: file, small: false) } } else { onlyFullSizeVideoThumbnail = isSendingUpdated - updateImageSignal = { synchronousLoad in + updateImageSignal = { synchronousLoad, _ in return mediaGridMessageVideo(postbox: context.account.postbox, videoReference: .message(message: MessageReference(message), media: file), onlyFullSize: currentMedia?.id?.namespace == Namespaces.Media.LocalFile, autoFetchFullSizeThumbnail: true) } } @@ -598,7 +727,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio } }) } else if let wallpaper = media as? WallpaperPreviewMedia { - updateImageSignal = { synchronousLoad in + updateImageSignal = { synchronousLoad, _ in switch wallpaper.content { case let .file(file, _, _, _, isTheme, _): if isTheme { @@ -606,10 +735,17 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio } else { 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, progressiveSizes: []), reference: AnyMediaReference.message(message: MessageReference(message), media: file).resourceReference(file.resource))) + representations.append(ImageRepresentationWithReference(representation: .init(dimensions: PixelDimensions(width: 1440, height: 2960), resource: file.resource, progressiveSizes: [], immediateThumbnailData: nil), 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) + return patternWallpaperImage(account: context.account, accountManager: context.sharedContext.accountManager, representations: representations, mode: .screen) + |> mapToSignal { value -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> in + if let value = value { + return .single(value) + } else { + return .complete() + } + } } 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) } @@ -618,8 +754,8 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio return themeImage(account: context.account, accountManager: context.sharedContext.accountManager, source: .settings(settings)) case let .color(color): return solidColorImage(color) - case let .gradient(topColor, bottomColor, rotation): - return gradientImage([topColor, bottomColor], rotation: rotation ?? 0) + case let .gradient(colors, rotation): + return gradientImage(colors.map(UIColor.init(rgb:)), rotation: rotation ?? 0) } } @@ -692,27 +828,52 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio strongSelf.attributes = attributes strongSelf.media = media strongSelf.wideLayout = wideLayout - strongSelf.themeAndStrings = (theme, strings, dateTimeFormat.decimalSeparator) + strongSelf.themeAndStrings = (presentationData.theme.theme, presentationData.strings, dateTimeFormat.decimalSeparator) strongSelf.sizeCalculation = sizeCalculation strongSelf.automaticPlayback = automaticPlayback strongSelf.automaticDownload = automaticDownload if let previousArguments = strongSelf.currentImageArguments { if previousArguments.imageSize == arguments.imageSize { - strongSelf.imageNode.frame = imageFrame + strongSelf.pinchContainerNode.frame = imageFrame + strongSelf.pinchContainerNode.update(size: imageFrame.size, transition: .immediate) + strongSelf.imageNode.frame = CGRect(origin: CGPoint(), size: imageFrame.size) } else { - transition.updateFrame(node: strongSelf.imageNode, frame: imageFrame) + transition.updateFrame(node: strongSelf.pinchContainerNode, frame: imageFrame) + transition.updateFrame(node: strongSelf.imageNode, frame: CGRect(origin: CGPoint(), size: imageFrame.size)) + strongSelf.pinchContainerNode.update(size: imageFrame.size, transition: transition) + } } else { - strongSelf.imageNode.frame = imageFrame + strongSelf.pinchContainerNode.frame = imageFrame + strongSelf.pinchContainerNode.update(size: imageFrame.size, transition: .immediate) + strongSelf.imageNode.frame = CGRect(origin: CGPoint(), size: imageFrame.size) } strongSelf.currentImageArguments = arguments imageApply() + + if let statusApply = statusApply { + if strongSelf.dateAndStatusNode.supernode == nil { + strongSelf.pinchContainerNode.contentNode.addSubnode(strongSelf.dateAndStatusNode) + } + var hasAnimation = true + if transition.isAnimated { + hasAnimation = false + } + statusApply(hasAnimation) + + let dateAndStatusFrame = CGRect(origin: CGPoint(x: imageFrame.width - layoutConstants.image.statusInsets.right - statusSize.width, y: imageFrame.height - layoutConstants.image.statusInsets.bottom - statusSize.height), size: statusSize) + + strongSelf.dateAndStatusNode.frame = dateAndStatusFrame + strongSelf.dateAndStatusNode.bounds = CGRect(origin: CGPoint(), size: dateAndStatusFrame.size) + } else if strongSelf.dateAndStatusNode.supernode != nil { + strongSelf.dateAndStatusNode.removeFromSupernode() + } if let statusNode = strongSelf.statusNode { var statusFrame = statusNode.frame - statusFrame.origin.x = floor(imageFrame.midX - statusFrame.width / 2.0) - statusFrame.origin.y = floor(imageFrame.midY - statusFrame.height / 2.0) + statusFrame.origin.x = floor(imageFrame.width / 2.0 - statusFrame.width / 2.0) + statusFrame.origin.y = floor(imageFrame.height / 2.0 - statusFrame.height / 2.0) statusNode.frame = statusFrame } @@ -776,7 +937,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio let dimensions = updatedAnimatedStickerFile.dimensions ?? PixelDimensions(width: 512, height: 512) let fittedDimensions = dimensions.cgSize.aspectFitted(CGSize(width: 384.0, height: 384.0)) animatedStickerNode.setup(source: AnimatedStickerResourceSource(account: context.account, resource: updatedAnimatedStickerFile.resource), width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), mode: .cached) - strongSelf.insertSubnode(animatedStickerNode, aboveSubnode: strongSelf.imageNode) + strongSelf.pinchContainerNode.contentNode.insertSubnode(animatedStickerNode, aboveSubnode: strongSelf.imageNode) animatedStickerNode.visibility = strongSelf.visibility } } @@ -787,7 +948,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio } videoNode.updateLayout(size: arguments.drawingSize, transition: .immediate) - videoNode.frame = imageFrame + videoNode.frame = CGRect(origin: CGPoint(), size: imageFrame.size) if strongSelf.visibility { if !videoNode.canAttachContent { @@ -807,7 +968,20 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio } if let updateImageSignal = updateImageSignal { - strongSelf.imageNode.setSignal(updateImageSignal(synchronousLoads), attemptSynchronously: synchronousLoads) + strongSelf.imageNode.setSignal(updateImageSignal(synchronousLoads, false), attemptSynchronously: synchronousLoads) + + var imageDimensions: CGSize? + if let image = media as? TelegramMediaImage, let dimensions = largestImageRepresentation(image.representations)?.dimensions { + imageDimensions = dimensions.cgSize + } else if let file = media as? TelegramMediaFile, let dimensions = file.dimensions { + imageDimensions = dimensions.cgSize + } else if let image = media as? TelegramMediaWebFile, let dimensions = image.dimensions { + imageDimensions = dimensions.cgSize + } + + if let imageDimensions = imageDimensions { + strongSelf.currentHighQualityImageSignal = (updateImageSignal(false, true), imageDimensions) + } } if let _ = secretBeginTimeAndTimeout { @@ -837,7 +1011,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio |> deliverOnMainQueue).start(next: { [weak strongSelf] status in displayLinkDispatcher.dispatch { if let strongSelf = strongSelf, let videoNode = strongSelf.videoNode { - strongSelf.insertSubnode(videoNode, aboveSubnode: strongSelf.imageNode) + strongSelf.pinchContainerNode.contentNode.insertSubnode(videoNode, aboveSubnode: strongSelf.imageNode) } } })) @@ -898,6 +1072,8 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio } strongSelf.updateStatus(animated: synchronousLoads) + + strongSelf.pinchContainerNode.isPinchGestureEnabled = !isSecretMedia } }) }) @@ -997,10 +1173,10 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio if progressRequired { if self.statusNode == nil { let statusNode = RadialStatusNode(backgroundNodeColor: theme.chat.message.mediaOverlayControlColors.fillColor) - let imagePosition = self.imageNode.position - statusNode.frame = CGRect(origin: CGPoint(x: floor(imagePosition.x - radialStatusSize / 2.0), y: floor(imagePosition.y - radialStatusSize / 2.0)), size: CGSize(width: radialStatusSize, height: radialStatusSize)) + let imageSize = self.imageNode.bounds.size + statusNode.frame = CGRect(origin: CGPoint(x: floor(imageSize.width / 2.0 - radialStatusSize / 2.0), y: floor(imageSize.height / 2.0 - radialStatusSize / 2.0)), size: CGSize(width: radialStatusSize, height: radialStatusSize)) self.statusNode = statusNode - self.addSubnode(statusNode) + self.pinchContainerNode.contentNode.addSubnode(statusNode) } } else { if let statusNode = self.statusNode { @@ -1077,6 +1253,8 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio let gifTitle = game != nil ? strings.Message_Game.uppercased() : strings.Message_Animation.uppercased() + let formatting = DataSizeStringFormatting(strings: strings, decimalSeparator: decimalSeparator) + switch fetchStatus { case let .Fetching(_, progress): let adjustedProgress = max(progress, 0.027) @@ -1093,11 +1271,8 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio if let file = self.media as? TelegramMediaFile { if wideLayout { if let size = file.size { - let sizeString = "\(dataSizeString(Int(Float(size) * progress), forceDecimal: true, decimalSeparator: decimalSeparator)) / \(dataSizeString(size, forceDecimal: true, decimalSeparator: decimalSeparator))" - if file.isAnimated { - badgeContent = .mediaDownload(backgroundColor: messageTheme.mediaDateAndStatusFillColor, foregroundColor: messageTheme.mediaDateAndStatusTextColor, duration: "\(gifTitle)", size: nil, muted: false, active: false) - } - else if let duration = file.duration, !message.flags.contains(.Unsent) { + let sizeString = "\(dataSizeString(Int(Float(size) * progress), forceDecimal: true, formatting: formatting)) / \(dataSizeString(size, forceDecimal: true, formatting: formatting))" + if let duration = file.duration, !message.flags.contains(.Unsent) { let durationString = file.isAnimated ? gifTitle : stringForDuration(playerDuration > 0 ? playerDuration : duration, position: playerPosition) if isMediaStreamable(message: message, media: file) { badgeContent = .mediaDownload(backgroundColor: messageTheme.mediaDateAndStatusFillColor, foregroundColor: messageTheme.mediaDateAndStatusTextColor, duration: durationString, size: active ? sizeString : nil, muted: muted, active: active) @@ -1116,7 +1291,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio state = automaticPlayback ? .none : state } } else { - badgeContent = .mediaDownload(backgroundColor: messageTheme.mediaDateAndStatusFillColor, foregroundColor: messageTheme.mediaDateAndStatusTextColor, duration: "\(dataSizeString(Int(Float(size) * progress), forceDecimal: true, decimalSeparator: decimalSeparator)) / \(dataSizeString(size, forceDecimal: true, decimalSeparator: decimalSeparator))", size: nil, muted: false, active: false) + badgeContent = .mediaDownload(backgroundColor: messageTheme.mediaDateAndStatusFillColor, foregroundColor: messageTheme.mediaDateAndStatusTextColor, duration: "\(dataSizeString(Int(Float(size) * progress), forceDecimal: true, formatting: formatting)) / \(dataSizeString(size, forceDecimal: true, formatting: formatting))", size: nil, muted: false, active: false) } } else if let _ = file.duration { if file.isAnimated { @@ -1130,7 +1305,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio } } else { if isMediaStreamable(message: message, media: file), let size = file.size { - let sizeString = "\(dataSizeString(Int(Float(size) * progress), forceDecimal: true, decimalSeparator: decimalSeparator)) / \(dataSizeString(size, forceDecimal: true, decimalSeparator: decimalSeparator))" + let sizeString = "\(dataSizeString(Int(Float(size) * progress), forceDecimal: true, formatting: formatting)) / \(dataSizeString(size, forceDecimal: true, formatting: formatting))" if message.flags.contains(.Unsent), let duration = file.duration { let durationString = stringForDuration(playerDuration > 0 ? playerDuration : duration, position: playerPosition) @@ -1158,7 +1333,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio let durationString = stringForDuration(playerDuration > 0 ? playerDuration : duration, position: playerPosition) if automaticPlayback, let size = file.size { - let sizeString = "\(dataSizeString(Int(Float(size) * progress), forceDecimal: true, decimalSeparator: decimalSeparator)) / \(dataSizeString(size, forceDecimal: true, decimalSeparator: decimalSeparator))" + let sizeString = "\(dataSizeString(Int(Float(size) * progress), forceDecimal: true, formatting: formatting)) / \(dataSizeString(size, forceDecimal: true, formatting: formatting))" mediaDownloadState = .fetching(progress: progress) badgeContent = .mediaDownload(backgroundColor: messageTheme.mediaDateAndStatusFillColor, foregroundColor: messageTheme.mediaDateAndStatusTextColor, duration: durationString, size: active ? sizeString : nil, muted: muted, active: active) } else { @@ -1205,15 +1380,15 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio case .Remote: state = .download(messageTheme.mediaOverlayControlColors.foregroundColor) if let file = self.media as? TelegramMediaFile { - if file.isAnimated && (!automaticDownload || !automaticPlayback) { - let string = "\(gifTitle) " + dataSizeString(file.size ?? 0, decimalSeparator: decimalSeparator) + if false, file.isAnimated && (!automaticDownload || !automaticPlayback) { + let string = "\(gifTitle) " + dataSizeString(file.size ?? 0, formatting: formatting) badgeContent = .mediaDownload(backgroundColor: messageTheme.mediaDateAndStatusFillColor, foregroundColor: messageTheme.mediaDateAndStatusTextColor, duration: string, size: nil, muted: false, active: false) } else { let durationString = file.isAnimated ? gifTitle : stringForDuration(playerDuration > 0 ? playerDuration : (file.duration ?? 0), position: playerPosition) if wideLayout { if isMediaStreamable(message: message, media: file) { state = automaticPlayback ? .none : .play(messageTheme.mediaOverlayControlColors.foregroundColor) - badgeContent = .mediaDownload(backgroundColor: messageTheme.mediaDateAndStatusFillColor, foregroundColor: messageTheme.mediaDateAndStatusTextColor, duration: durationString, size: dataSizeString(file.size ?? 0, decimalSeparator: decimalSeparator), muted: muted, active: true) + badgeContent = .mediaDownload(backgroundColor: messageTheme.mediaDateAndStatusFillColor, foregroundColor: messageTheme.mediaDateAndStatusTextColor, duration: durationString, size: dataSizeString(file.size ?? 0, formatting: formatting), muted: muted, active: true) mediaDownloadState = .remote } else { state = automaticPlayback ? .none : state @@ -1276,7 +1451,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio } } self.badgeNode = badgeNode - self.addSubnode(badgeNode) + self.pinchContainerNode.contentNode.addSubnode(badgeNode) animated = false } @@ -1301,12 +1476,12 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio } } - 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))) { + static func asyncLayout(_ node: ChatMessageInteractiveMediaNode?) -> (_ context: AccountContext, _ presentationData: ChatPresentationData, _ dateTimeFormat: PresentationDateTimeFormat, _ message: Message, _ attributes: ChatMessageEntryAttributes, _ media: Media, _ dateAndStatus: ChatMessageDateAndStatus?, _ 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, attributes, media, automaticDownload, peerType, sizeCalculation, layoutConstants, contentMode in + return { context, presentationData, dateTimeFormat, message, attributes, media, dateAndStatus, automaticDownload, peerType, sizeCalculation, layoutConstants, contentMode in var imageNode: ChatMessageInteractiveMediaNode - 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))) + var imageLayout: (_ context: AccountContext, _ presentationData: ChatPresentationData, _ dateTimeFormat: PresentationDateTimeFormat, _ message: Message, _ attributes: ChatMessageEntryAttributes, _ media: Media, _ dateAndStatus: ChatMessageDateAndStatus?, _ 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 @@ -1316,7 +1491,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio imageLayout = imageNode.asyncLayout() } - let (unboundSize, initialWidth, continueLayout) = imageLayout(context, theme, strings, dateTimeFormat, message, attributes, media, automaticDownload, peerType, sizeCalculation, layoutConstants, contentMode) + let (unboundSize, initialWidth, continueLayout) = imageLayout(context, presentationData, dateTimeFormat, message, attributes, media, dateAndStatus, automaticDownload, peerType, sizeCalculation, layoutConstants, contentMode) return (unboundSize, initialWidth, { constrainedSize, automaticPlayback, wideLayout, corners in let (finalWidth, finalLayout) = continueLayout(constrainedSize, automaticPlayback, wideLayout, corners) diff --git a/submodules/TelegramUI/Sources/ChatMessageInvoiceBubbleContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageInvoiceBubbleContentNode.swift index c30749172b..b38c677bcc 100644 --- a/submodules/TelegramUI/Sources/ChatMessageInvoiceBubbleContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageInvoiceBubbleContentNode.swift @@ -38,7 +38,7 @@ final class ChatMessageInvoiceBubbleContentNode: ChatMessageBubbleContentNode { 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 contentNodeLayout = self.contentNode.asyncLayout() - return { item, layoutConstants, _, _, constrainedSize in + return { item, layoutConstants, preparePosition, _, constrainedSize in var invoice: TelegramMediaInvoice? for media in item.message.media { if let media = media as? TelegramMediaInvoice { @@ -74,7 +74,7 @@ final class ChatMessageInvoiceBubbleContentNode: ChatMessageBubbleContentNode { } } - let (initialWidth, continueLayout) = contentNodeLayout(item.presentationData, automaticDownloadSettings, item.associatedData, item.attributes, item.context, item.controllerInteraction, item.message, item.read, item.chatLocation, 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, item.chatLocation, title, subtitle, text, nil, mediaAndFlags, nil, nil, nil, false, layoutConstants, preparePosition, constrainedSize) let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: false, headerSpacing: 8.0, hidesBackground: .never, forceFullCorners: false, forceAlignment: .none) diff --git a/submodules/TelegramUI/Sources/ChatMessageItem.swift b/submodules/TelegramUI/Sources/ChatMessageItem.swift index d699fd11a9..897e312c27 100644 --- a/submodules/TelegramUI/Sources/ChatMessageItem.swift +++ b/submodules/TelegramUI/Sources/ChatMessageItem.swift @@ -192,9 +192,8 @@ func chatItemsHaveCommonDateHeader(_ lhs: ListViewItem, _ rhs: ListViewItem?) - let lhsHeader: ChatMessageDateHeader? let rhsHeader: ChatMessageDateHeader? if let lhs = lhs as? ChatMessageItem { - lhsHeader = lhs.header + lhsHeader = lhs.dateHeader } else if let _ = lhs as? ChatHoleItem { - //lhsHeader = lhs.header lhsHeader = nil } else if let lhs = lhs as? ChatUnreadItem { lhsHeader = lhs.header @@ -205,7 +204,7 @@ func chatItemsHaveCommonDateHeader(_ lhs: ListViewItem, _ rhs: ListViewItem?) - } if let rhs = rhs { if let rhs = rhs as? ChatMessageItem { - rhsHeader = rhs.header + rhsHeader = rhs.dateHeader } else if let _ = rhs as? ChatHoleItem { //rhsHeader = rhs.header rhsHeader = nil @@ -257,8 +256,11 @@ public final class ChatMessageItem: ListViewItem, CustomStringConvertible { let effectiveAuthorId: PeerId? let additionalContent: ChatMessageItemAdditionalContent? - public let accessoryItem: ListViewAccessoryItem? - let header: ChatMessageDateHeader + //public let accessoryItem: ListViewAccessoryItem? + let dateHeader: ChatMessageDateHeader + let avatarHeader: ChatMessageAvatarHeader? + + let headers: [ListViewItemHeader] var message: Message { switch self.content { @@ -288,7 +290,7 @@ public final class ChatMessageItem: ListViewItem, CustomStringConvertible { self.disableDate = disableDate self.additionalContent = additionalContent - var accessoryItem: ListViewAccessoryItem? + var avatarHeader: ChatMessageAvatarHeader? let incoming = content.effectivelyIncoming(self.context.account.peerId) var effectiveAuthor: Peer? @@ -302,7 +304,7 @@ public final class ChatMessageItem: ListViewItem, CustomStringConvertible { if let forwardInfo = content.firstMessage.forwardInfo { effectiveAuthor = forwardInfo.author if effectiveAuthor == nil, let authorSignature = forwardInfo.authorSignature { - effectiveAuthor = TelegramUser(id: PeerId(namespace: Namespaces.Peer.Empty, id: Int32(clamping: authorSignature.persistentHashValue)), accessHash: nil, firstName: authorSignature, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: UserInfoFlags()) + effectiveAuthor = TelegramUser(id: PeerId(namespace: Namespaces.Peer.Empty, id: PeerId.Id._internalFromInt32Value(Int32(clamping: authorSignature.persistentHashValue))), accessHash: nil, firstName: authorSignature, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: UserInfoFlags()) } } displayAuthorInfo = incoming && effectiveAuthor != nil @@ -325,7 +327,7 @@ public final class ChatMessageItem: ListViewItem, CustomStringConvertible { isScheduledMessages = true } - self.header = ChatMessageDateHeader(timestamp: content.index.timestamp, scheduled: isScheduledMessages, presentationData: presentationData, context: context, action: { timestamp in + self.dateHeader = ChatMessageDateHeader(timestamp: content.index.timestamp, scheduled: isScheduledMessages, presentationData: presentationData, context: context, action: { timestamp in var calendar = NSCalendar.current calendar.timeZone = TimeZone(abbreviation: "UTC")! let date = Date(timeIntervalSince1970: TimeInterval(timestamp)) @@ -355,11 +357,18 @@ public final class ChatMessageItem: ListViewItem, CustomStringConvertible { } if !hasActionMedia && !isBroadcastChannel { if let effectiveAuthor = effectiveAuthor { - accessoryItem = ChatMessageAvatarAccessoryItem(context: context, peerId: effectiveAuthor.id, peer: effectiveAuthor, messageReference: MessageReference(message), messageTimestamp: content.index.timestamp, forwardInfo: message.forwardInfo, emptyColor: presentationData.theme.theme.chat.message.incoming.bubble.withoutWallpaper.fill, controllerInteraction: controllerInteraction) + //accessoryItem = ChatMessageAvatarAccessoryItem(context: context, peerId: effectiveAuthor.id, peer: effectiveAuthor, messageReference: MessageReference(message), messageTimestamp: content.index.timestamp, forwardInfo: message.forwardInfo, emptyColor: presentationData.theme.theme.chat.message.incoming.bubble.withoutWallpaper.fill, controllerInteraction: controllerInteraction) + avatarHeader = ChatMessageAvatarHeader(timestamp: content.index.timestamp, peerId: effectiveAuthor.id, peer: effectiveAuthor, messageReference: MessageReference(message), presentationData: presentationData, context: context, controllerInteraction: controllerInteraction) } } } - self.accessoryItem = accessoryItem + self.avatarHeader = avatarHeader + + var headers: [ListViewItemHeader] = [self.dateHeader] + if let avatarHeader = self.avatarHeader { + headers.append(avatarHeader) + } + self.headers = headers } public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) { @@ -434,7 +443,7 @@ public final class ChatMessageItem: ListViewItem, CustomStringConvertible { let configure = { let node = (viewClassName as! ChatMessageItemView.Type).init() - node.setupItem(self) + node.setupItem(self, synchronousLoad: synchronousLoads) let nodeLayout = node.asyncLayout() let (top, bottom, dateAtBottom) = self.mergedWithItems(top: previousItem, bottom: nextItem) @@ -467,25 +476,25 @@ public final class ChatMessageItem: ListViewItem, CustomStringConvertible { var mergedBottom: ChatMessageMerge = .none var dateAtBottom = false if let top = top as? ChatMessageItem { - if top.header.id != self.header.id { + if top.dateHeader.id != self.dateHeader.id { mergedBottom = .none } else { mergedBottom = messagesShouldBeMerged(accountPeerId: self.context.account.peerId, message, top.message) } } if let bottom = bottom as? ChatMessageItem { - if bottom.header.id != self.header.id { + if bottom.dateHeader.id != self.dateHeader.id { mergedTop = .none dateAtBottom = true } else { mergedTop = messagesShouldBeMerged(accountPeerId: self.context.account.peerId, bottom.message, message) } } else if let bottom = bottom as? ChatUnreadItem { - if bottom.header.id != self.header.id { + if bottom.header.id != self.dateHeader.id { dateAtBottom = true } } else if let bottom = bottom as? ChatReplyCountItem { - if bottom.header.id != self.header.id { + if bottom.header.id != self.dateHeader.id { dateAtBottom = true } } else if let _ = bottom as? ChatHoleItem { @@ -500,7 +509,7 @@ public final class ChatMessageItem: ListViewItem, CustomStringConvertible { 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? ChatMessageItemView { - nodeValue.setupItem(self) + nodeValue.setupItem(self, synchronousLoad: false) let nodeLayout = nodeValue.asyncLayout() diff --git a/submodules/TelegramUI/Sources/ChatMessageItemView.swift b/submodules/TelegramUI/Sources/ChatMessageItemView.swift index d4a3177ae2..c335e2e79a 100644 --- a/submodules/TelegramUI/Sources/ChatMessageItemView.swift +++ b/submodules/TelegramUI/Sources/ChatMessageItemView.swift @@ -207,7 +207,7 @@ final class ChatMessageAccessibilityData { if let chatPeer = message.peers[item.message.id.peerId] { let authorName = message.author?.displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder) - let (_, _, messageText) = chatListItemStrings(strings: item.presentationData.strings, nameDisplayOrder: item.presentationData.nameDisplayOrder, messages: [message], chatPeer: RenderedPeer(peer: chatPeer), accountPeerId: item.context.account.peerId) + let (_, _, messageText) = chatListItemStrings(strings: item.presentationData.strings, nameDisplayOrder: item.presentationData.nameDisplayOrder, dateTimeFormat: item.presentationData.dateTimeFormat, messages: [message], chatPeer: RenderedPeer(peer: chatPeer), accountPeerId: item.context.account.peerId) var text = messageText @@ -705,7 +705,7 @@ public class ChatMessageItemView: ListViewItemNode { self.frame = CGRect() } - func setupItem(_ item: ChatMessageItem) { + func setupItem(_ item: ChatMessageItem, synchronousLoad: Bool) { self.item = item } @@ -729,6 +729,9 @@ public class ChatMessageItemView: ListViewItemNode { avatarNode.frame = CGRect(origin: CGPoint(x: leftInset + 3.0, y: self.apparentFrame.height - 38.0 - self.insets.top - 2.0 - UIScreenPixel), size: CGSize(width: 38.0, height: 38.0)) } } + + func cancelInsertionAnimations() { + } override public func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { if short { @@ -798,9 +801,9 @@ public class ChatMessageItemView: ListViewItemNode { return nil } - override public func header() -> ListViewItemHeader? { + override public func headers() -> [ListViewItemHeader]? { if let item = self.item { - return item.header + return item.headers } else { return nil } diff --git a/submodules/TelegramUI/Sources/ChatMessageMediaBubbleContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageMediaBubbleContentNode.swift index 9a6d8bd125..d564507543 100644 --- a/submodules/TelegramUI/Sources/ChatMessageMediaBubbleContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageMediaBubbleContentNode.swift @@ -17,7 +17,6 @@ class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode { } private let interactiveImageNode: ChatMessageInteractiveMediaNode - private let dateAndStatusNode: ChatMessageDateAndStatusNode private var selectionNode: GridMessageSelectionNode? private var highlightedState: Bool = false @@ -32,7 +31,6 @@ class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode { required init() { self.interactiveImageNode = ChatMessageInteractiveMediaNode() - self.dateAndStatusNode = ChatMessageDateAndStatusNode() super.init() @@ -54,6 +52,13 @@ class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode { } } } + + self.interactiveImageNode.activatePinch = { [weak self] sourceNode in + guard let strongSelf = self, let _ = strongSelf.item else { + return + } + strongSelf.item?.controllerInteraction.activateMessagePinch(sourceNode) + } } required init?(coder aDecoder: NSCoder) { @@ -62,7 +67,6 @@ class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode { 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 interactiveImageLayout = self.interactiveImageNode.asyncLayout() - let statusLayout = self.dateAndStatusNode.asyncLayout() return { item, layoutConstants, preparePosition, selection, constrainedSize in var selectedMedia: Media? @@ -142,8 +146,81 @@ class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode { bubbleInsets = UIEdgeInsets() sizeCalculation = .unconstrained } + + var edited = false + if item.attributes.updatingMedia != nil { + edited = true + } + var viewCount: Int? + var dateReplies = 0 + for attribute in item.message.attributes { + if let attribute = attribute as? EditedMessageAttribute { + if case .mosaic = preparePosition { + } else { + edited = !attribute.isHidden + } + } else if let attribute = attribute as? ViewCountMessageAttribute { + viewCount = attribute.count + } else if let attribute = attribute as? ReplyThreadMessageAttribute, case .peer = item.chatLocation { + if let channel = item.message.peers[item.message.id.peerId] as? TelegramChannel, case .group = channel.info { + dateReplies = Int(attribute.count) + } + } + } + + var dateReactions: [MessageReaction] = [] + var dateReactionCount = 0 + if let reactionsAttribute = mergedMessageReactions(attributes: item.message.attributes), !reactionsAttribute.reactions.isEmpty { + for reaction in reactionsAttribute.reactions { + if reaction.isSelected { + dateReactions.insert(reaction, at: 0) + } else { + dateReactions.append(reaction) + } + dateReactionCount += Int(reaction.count) + } + } + + let dateText = stringForMessageTimestampStatus(accountPeerId: item.context.account.peerId, message: item.message, dateTimeFormat: item.presentationData.dateTimeFormat, nameDisplayOrder: item.presentationData.nameDisplayOrder, strings: item.presentationData.strings, reactionCount: dateReactionCount) + + let statusType: ChatMessageDateAndStatusType? + switch preparePosition { + case .linear(_, .None), .linear(_, .Neighbour(true, _, _)): + if item.message.effectivelyIncoming(item.context.account.peerId) { + statusType = .ImageIncoming + } else { + if item.message.flags.contains(.Failed) { + statusType = .ImageOutgoing(.Failed) + } else if (item.message.flags.isSending && !item.message.isSentOrAcknowledged) || item.attributes.updatingMedia != nil { + statusType = .ImageOutgoing(.Sending) + } else { + statusType = .ImageOutgoing(.Sent(read: item.read)) + } + } + case .mosaic: + statusType = nil + default: + statusType = nil + } + + var isReplyThread = false + if case .replyThread = item.chatLocation { + isReplyThread = true + } + + let dateAndStatus = statusType.flatMap { statusType -> ChatMessageDateAndStatus in + ChatMessageDateAndStatus( + type: statusType, + edited: edited, + viewCount: viewCount, + dateReplies: dateReplies, + dateReactions: dateReactions, + isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread, + dateText: dateText + ) + } - 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 (unboundSize, initialWidth, refineLayout) = interactiveImageLayout(item.context, item.presentationData, item.presentationData.dateTimeFormat, item.message, item.attributes, selectedMedia!, dateAndStatus, automaticDownload, item.associatedData.automaticDownloadPeerType, sizeCalculation, layoutConstants, contentMode) let forceFullCorners = false let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: true, headerSpacing: 7.0, hidesBackground: .emptyWallpaper, forceFullCorners: forceFullCorners, forceAlignment: .none) @@ -169,82 +246,9 @@ class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode { return (refinedWidth + bubbleInsets.left + bubbleInsets.right, { boundingWidth in let (imageSize, imageApply) = finishLayout(boundingWidth - bubbleInsets.left - bubbleInsets.right) - var edited = false - if item.attributes.updatingMedia != nil { - edited = true - } - var viewCount: Int? - var dateReplies = 0 - for attribute in item.message.attributes { - if let attribute = attribute as? EditedMessageAttribute { - if case .mosaic = preparePosition { - } else { - edited = !attribute.isHidden - } - } else if let attribute = attribute as? ViewCountMessageAttribute { - viewCount = attribute.count - } else if let attribute = attribute as? ReplyThreadMessageAttribute, case .peer = item.chatLocation { - if let channel = item.message.peers[item.message.id.peerId] as? TelegramChannel, case .group = channel.info { - dateReplies = Int(attribute.count) - } - } - } - - var dateReactions: [MessageReaction] = [] - var dateReactionCount = 0 - if let reactionsAttribute = mergedMessageReactions(attributes: item.message.attributes), !reactionsAttribute.reactions.isEmpty { - for reaction in reactionsAttribute.reactions { - if reaction.isSelected { - dateReactions.insert(reaction, at: 0) - } else { - dateReactions.append(reaction) - } - dateReactionCount += Int(reaction.count) - } - } - - let dateText = stringForMessageTimestampStatus(accountPeerId: item.context.account.peerId, message: item.message, dateTimeFormat: item.presentationData.dateTimeFormat, nameDisplayOrder: item.presentationData.nameDisplayOrder, strings: item.presentationData.strings, reactionCount: dateReactionCount) - - let statusType: ChatMessageDateAndStatusType? - switch position { - case .linear(_, .None), .linear(_, .Neighbour(true, _, _)): - if item.message.effectivelyIncoming(item.context.account.peerId) { - statusType = .ImageIncoming - } else { - if item.message.flags.contains(.Failed) { - statusType = .ImageOutgoing(.Failed) - } else if (item.message.flags.isSending && !item.message.isSentOrAcknowledged) || item.attributes.updatingMedia != nil { - statusType = .ImageOutgoing(.Sending) - } else { - statusType = .ImageOutgoing(.Sent(read: item.read)) - } - } - case .mosaic: - statusType = nil - default: - statusType = nil - } - let imageLayoutSize = CGSize(width: imageSize.width + bubbleInsets.left + bubbleInsets.right, height: imageSize.height + bubbleInsets.top + bubbleInsets.bottom) - var statusSize = CGSize() - var statusApply: ((Bool) -> Void)? - - if let statusType = statusType { - var isReplyThread = false - if case .replyThread = item.chatLocation { - isReplyThread = true - } - - let (size, apply) = statusLayout(item.context, item.presentationData, edited, viewCount, dateText, statusType, CGSize(width: imageSize.width - 30.0, height: CGFloat.greatestFiniteMagnitude), dateReactions, dateReplies, item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread, item.message.isSelfExpiring) - statusSize = size - statusApply = apply - } - - var layoutWidth = imageLayoutSize.width - if case .constrained = sizeCalculation { - layoutWidth = max(layoutWidth, statusSize.width + bubbleInsets.left + bubbleInsets.right + layoutConstants.image.statusInsets.left + layoutConstants.image.statusInsets.right) - } + let layoutWidth = imageLayoutSize.width let layoutSize = CGSize(width: layoutWidth, height: imageLayoutSize.height) @@ -262,24 +266,6 @@ class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode { transition.updateFrame(node: strongSelf.interactiveImageNode, frame: imageFrame) - if let statusApply = statusApply { - if strongSelf.dateAndStatusNode.supernode == nil { - strongSelf.interactiveImageNode.addSubnode(strongSelf.dateAndStatusNode) - } - var hasAnimation = true - if case .None = animation { - hasAnimation = false - } - statusApply(hasAnimation) - - let dateAndStatusFrame = CGRect(origin: CGPoint(x: layoutSize.width - bubbleInsets.right - layoutConstants.image.statusInsets.right - statusSize.width, y: layoutSize.height - bubbleInsets.bottom - layoutConstants.image.statusInsets.bottom - statusSize.height), size: statusSize) - - strongSelf.dateAndStatusNode.frame = dateAndStatusFrame - strongSelf.dateAndStatusNode.bounds = CGRect(origin: CGPoint(), size: dateAndStatusFrame.size) - } else if strongSelf.dateAndStatusNode.supernode != nil { - strongSelf.dateAndStatusNode.removeFromSupernode() - } - imageApply(transition, synchronousLoads) if let selection = selection { @@ -310,14 +296,14 @@ class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode { } if let forwardInfo = item.message.forwardInfo, forwardInfo.flags.contains(.isImported) { - strongSelf.dateAndStatusNode.pressed = { + strongSelf.interactiveImageNode.dateAndStatusNode.pressed = { guard let strongSelf = self else { return } - item.controllerInteraction.displayImportedMessageTooltip(strongSelf.dateAndStatusNode) + item.controllerInteraction.displayImportedMessageTooltip(strongSelf.interactiveImageNode.dateAndStatusNode) } } else { - strongSelf.dateAndStatusNode.pressed = nil + strongSelf.interactiveImageNode.dateAndStatusNode.pressed = nil } } }) @@ -356,7 +342,7 @@ class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode { self.interactiveImageNode.isHidden = mediaHidden self.interactiveImageNode.updateIsHidden(mediaHidden) - if let automaticPlayback = self.automaticPlayback { + /*if let automaticPlayback = self.automaticPlayback { if !automaticPlayback { self.dateAndStatusNode.isHidden = false } else if self.dateAndStatusNode.isHidden != mediaHidden { @@ -367,7 +353,7 @@ class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode { self.dateAndStatusNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) } } - } + }*/ return mediaHidden } @@ -416,9 +402,9 @@ class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode { } override func reactionTargetNode(value: String) -> (ASDisplayNode, ASDisplayNode)? { - if !self.dateAndStatusNode.isHidden { + /*if !self.dateAndStatusNode.isHidden { return self.dateAndStatusNode.reactionNode(value: value) - } + }*/ return nil } } diff --git a/submodules/TelegramUI/Sources/ChatMessageNotificationItem.swift b/submodules/TelegramUI/Sources/ChatMessageNotificationItem.swift index 97ceb3beed..a6c20c8cb8 100644 --- a/submodules/TelegramUI/Sources/ChatMessageNotificationItem.swift +++ b/submodules/TelegramUI/Sources/ChatMessageNotificationItem.swift @@ -18,6 +18,7 @@ import TelegramStringFormatting public final class ChatMessageNotificationItem: NotificationItem { let context: AccountContext let strings: PresentationStrings + let dateTimeFormat: PresentationDateTimeFormat let nameDisplayOrder: PresentationPersonNameOrder let messages: [Message] let tapAction: () -> Bool @@ -27,9 +28,10 @@ public final class ChatMessageNotificationItem: NotificationItem { return messages.first?.id.peerId } - public init(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, messages: [Message], tapAction: @escaping () -> Bool, expandAction: @escaping (() -> (ASDisplayNode?, () -> Void)) -> Void) { + public init(context: AccountContext, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, messages: [Message], tapAction: @escaping () -> Bool, expandAction: @escaping (() -> (ASDisplayNode?, () -> Void)) -> Void) { self.context = context self.strings = strings + self.dateTimeFormat = dateTimeFormat self.nameDisplayOrder = nameDisplayOrder self.messages = messages self.tapAction = tapAction @@ -181,7 +183,7 @@ final class ChatMessageNotificationItemNode: NotificationItemNode { if message.containsSecretMedia { imageDimensions = nil } - messageText = descriptionStringForMessage(contentSettings: item.context.currentContentSettings.with { $0 }, message: 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, dateTimeFormat: item.dateTimeFormat, 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 { @@ -218,9 +220,9 @@ final class ChatMessageNotificationItemNode: NotificationItemNode { } } } else if item.messages[0].groupingKey != nil { - 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 + var kind = messageContentKind(contentSettings: item.context.currentContentSettings.with { $0 }, message: item.messages[0], strings: presentationData.strings, nameDisplayOrder: presentationData.nameDisplayOrder, dateTimeFormat: presentationData.dateTimeFormat, accountPeerId: item.context.account.peerId).key for i in 1 ..< item.messages.count { - let nextKind = messageContentKind(contentSettings: item.context.currentContentSettings.with { $0 }, message: 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, dateTimeFormat: presentationData.dateTimeFormat, accountPeerId: item.context.account.peerId) if kind != nextKind.key { kind = .text break @@ -339,7 +341,7 @@ final class ChatMessageNotificationItemNode: NotificationItemNode { messageText = rawText } case .file: - let rawText = presentationData.strings.PUSH_MESSAGE_DOCS(Int32(item.messages.count), peer.displayTitle(strings: item.strings, displayOrder: item.nameDisplayOrder), Int32(item.messages.count)) + let rawText = presentationData.strings.PUSH_MESSAGE_FILES(Int32(item.messages.count), peer.displayTitle(strings: item.strings, displayOrder: item.nameDisplayOrder), Int32(item.messages.count)) if let index = rawText.firstIndex(of: "|") { title = String(rawText[rawText.startIndex ..< index]) messageText = String(rawText[rawText.index(after: index)...]) diff --git a/submodules/TelegramUI/Sources/ChatMessageReplyInfoNode.swift b/submodules/TelegramUI/Sources/ChatMessageReplyInfoNode.swift index af0467d891..240ba43779 100644 --- a/submodules/TelegramUI/Sources/ChatMessageReplyInfoNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageReplyInfoNode.swift @@ -65,7 +65,7 @@ class ChatMessageReplyInfoNode: ASDisplayNode { } } - let (textString, isMedia) = descriptionStringForMessage(contentSettings: context.currentContentSettings.with { $0 }, message: 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, dateTimeFormat: presentationData.dateTimeFormat, 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 @@ -229,4 +229,115 @@ class ChatMessageReplyInfoNode: ASDisplayNode { }) } } + + func animateFromInputPanel(sourceReplyPanel: ChatMessageTransitionNode.ReplyPanel, unclippedTransitionNode: ASDisplayNode? = nil, localRect: CGRect, transition: CombinedTransition) -> CGPoint { + let sourceParentNode = ASDisplayNode() + + let sourceParentOffset: CGPoint + + if let unclippedTransitionNode = unclippedTransitionNode { + unclippedTransitionNode.addSubnode(sourceParentNode) + sourceParentNode.frame = sourceReplyPanel.relativeSourceRect + sourceParentOffset = self.view.convert(CGPoint(), to: sourceParentNode.view) + sourceParentNode.clipsToBounds = true + + let panelOffset = sourceReplyPanel.relativeTargetRect.minY - sourceReplyPanel.relativeSourceRect.minY + + sourceParentNode.frame = sourceParentNode.frame.offsetBy(dx: 0.0, dy: panelOffset) + sourceParentNode.bounds = sourceParentNode.bounds.offsetBy(dx: 0.0, dy: panelOffset) + transition.vertical.animatePositionAdditive(layer: sourceParentNode.layer, offset: CGPoint(x: 0.0, y: -panelOffset)) + transition.vertical.animateOffsetAdditive(layer: sourceParentNode.layer, offset: -panelOffset) + } else { + self.addSubnode(sourceParentNode) + sourceParentOffset = CGPoint() + } + + sourceParentNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false, completion: { [weak sourceParentNode] _ in + sourceParentNode?.removeFromSupernode() + }) + + if let titleNode = self.titleNode { + let offset = CGPoint( + x: localRect.minX + sourceReplyPanel.titleNode.frame.minX - titleNode.frame.minX, + y: localRect.minY + sourceReplyPanel.titleNode.frame.midY - titleNode.frame.midY + ) + + transition.horizontal.animatePositionAdditive(node: titleNode, offset: CGPoint(x: offset.x, y: 0.0)) + transition.vertical.animatePositionAdditive(node: titleNode, offset: CGPoint(x: 0.0, y: offset.y)) + + sourceParentNode.addSubnode(sourceReplyPanel.titleNode) + + titleNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1) + + sourceReplyPanel.titleNode.frame = sourceReplyPanel.titleNode.frame + .offsetBy(dx: sourceParentOffset.x, dy: sourceParentOffset.y) + .offsetBy(dx: localRect.minX - offset.x, dy: localRect.minY - offset.y) + transition.horizontal.animatePositionAdditive(node: sourceReplyPanel.titleNode, offset: CGPoint(x: offset.x, y: 0.0), removeOnCompletion: false) + transition.vertical.animatePositionAdditive(node: sourceReplyPanel.titleNode, offset: CGPoint(x: 0.0, y: offset.y), removeOnCompletion: false) + } + + if let textNode = self.textNode { + let offset = CGPoint( + x: localRect.minX + sourceReplyPanel.textNode.frame.minX - textNode.frame.minX, + y: localRect.minY + sourceReplyPanel.textNode.frame.midY - textNode.frame.midY + ) + + transition.horizontal.animatePositionAdditive(node: textNode, offset: CGPoint(x: offset.x, y: 0.0)) + transition.vertical.animatePositionAdditive(node: textNode, offset: CGPoint(x: 0.0, y: offset.y)) + + sourceParentNode.addSubnode(sourceReplyPanel.textNode) + + textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1) + + sourceReplyPanel.textNode.frame = sourceReplyPanel.textNode.frame + .offsetBy(dx: sourceParentOffset.x, dy: sourceParentOffset.y) + .offsetBy(dx: localRect.minX - offset.x, dy: localRect.minY - offset.y) + transition.horizontal.animatePositionAdditive(node: sourceReplyPanel.textNode, offset: CGPoint(x: offset.x, y: 0.0), removeOnCompletion: false) + transition.vertical.animatePositionAdditive(node: sourceReplyPanel.textNode, offset: CGPoint(x: 0.0, y: offset.y), removeOnCompletion: false) + } + + if let imageNode = self.imageNode { + let offset = CGPoint( + x: localRect.minX + sourceReplyPanel.imageNode.frame.midX - imageNode.frame.midX, + y: localRect.minY + sourceReplyPanel.imageNode.frame.midY - imageNode.frame.midY + ) + + transition.horizontal.animatePositionAdditive(node: imageNode, offset: CGPoint(x: offset.x, y: 0.0)) + transition.vertical.animatePositionAdditive(node: imageNode, offset: CGPoint(x: 0.0, y: offset.y)) + + sourceParentNode.addSubnode(sourceReplyPanel.imageNode) + + imageNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1) + + sourceReplyPanel.imageNode.frame = sourceReplyPanel.imageNode.frame + .offsetBy(dx: sourceParentOffset.x, dy: sourceParentOffset.y) + .offsetBy(dx: localRect.minX - offset.x, dy: localRect.minY - offset.y) + transition.horizontal.animatePositionAdditive(node: sourceReplyPanel.imageNode, offset: CGPoint(x: offset.x, y: 0.0), removeOnCompletion: false) + transition.vertical.animatePositionAdditive(node: sourceReplyPanel.imageNode, offset: CGPoint(x: 0.0, y: offset.y), removeOnCompletion: false) + } + + do { + let lineNode = self.lineNode + + let offset = CGPoint( + x: localRect.minX + sourceReplyPanel.lineNode.frame.minX - lineNode.frame.minX, + y: localRect.minY + sourceReplyPanel.lineNode.frame.minY - lineNode.frame.minY + ) + + transition.horizontal.animatePositionAdditive(node: lineNode, offset: CGPoint(x: offset.x, y: 0.0)) + transition.vertical.animatePositionAdditive(node: lineNode, offset: CGPoint(x: 0.0, y: offset.y)) + + sourceParentNode.addSubnode(sourceReplyPanel.lineNode) + + lineNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1) + + sourceReplyPanel.lineNode.frame = sourceReplyPanel.lineNode.frame + .offsetBy(dx: sourceParentOffset.x, dy: sourceParentOffset.y) + .offsetBy(dx: localRect.minX - offset.x, dy: localRect.minY - offset.y) + transition.horizontal.animatePositionAdditive(node: sourceReplyPanel.lineNode, offset: CGPoint(x: offset.x, y: 0.0), removeOnCompletion: false) + transition.vertical.animatePositionAdditive(node: sourceReplyPanel.lineNode, offset: CGPoint(x: 0.0, y: offset.y), removeOnCompletion: false) + + return offset + } + } } diff --git a/submodules/TelegramUI/Sources/ChatMessageStickerItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageStickerItemNode.swift index 3a5ee9c215..43b041813b 100644 --- a/submodules/TelegramUI/Sources/ChatMessageStickerItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageStickerItemNode.swift @@ -13,15 +13,17 @@ import StickerResources import ContextUI import Markdown import ShimmerEffect +import WallpaperBackgroundNode private let nameFont = Font.medium(14.0) private let inlineBotPrefixFont = Font.regular(14.0) private let inlineBotNameFont = nameFont class ChatMessageStickerItemNode: ChatMessageItemView { - private let contextSourceNode: ContextExtractedContentContainingNode + let contextSourceNode: ContextExtractedContentContainingNode private let containerNode: ContextControllerSourceNode let imageNode: TransformImageNode + private var backgroundNode: WallpaperBackgroundNode.BubbleBackgroundNode? private var placeholderNode: StickerShimmerEffectNode var textNode: TextNode? @@ -38,7 +40,7 @@ class ChatMessageStickerItemNode: ChatMessageItemView { private var viaBotNode: TextNode? private let dateAndStatusNode: ChatMessageDateAndStatusNode private var replyInfoNode: ChatMessageReplyInfoNode? - private var replyBackgroundNode: ASImageNode? + private var replyBackgroundNode: NavigationBackgroundNode? private var actionButtonsNode: ChatMessageActionButtonsNode? @@ -49,6 +51,8 @@ class ChatMessageStickerItemNode: ChatMessageItemView { private var currentSwipeToReplyTranslation: CGFloat = 0.0 private var currentSwipeAction: ChatControllerInteractionSwipeAction? + + private var enableSynchronousImageApply: Bool = false required init() { self.contextSourceNode = ContextExtractedContentContainingNode() @@ -68,9 +72,13 @@ class ChatMessageStickerItemNode: ChatMessageItemView { } if image != nil { if firstTime && !strongSelf.placeholderNode.isEmpty { - strongSelf.imageNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, completion: { [weak self] _ in - self?.removePlaceholder(animated: false) - }) + if strongSelf.enableSynchronousImageApply { + strongSelf.removePlaceholder(animated: false) + } else { + strongSelf.imageNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, completion: { [weak self] _ in + self?.removePlaceholder(animated: false) + }) + } } else { strongSelf.removePlaceholder(animated: true) } @@ -214,15 +222,15 @@ class ChatMessageStickerItemNode: ChatMessageItemView { self.view.addGestureRecognizer(replyRecognizer) } - override func setupItem(_ item: ChatMessageItem) { - super.setupItem(item) + override func setupItem(_ item: ChatMessageItem, synchronousLoad: Bool) { + super.setupItem(item, synchronousLoad: synchronousLoad) for media in item.message.media { if let telegramFile = media as? TelegramMediaFile { if self.telegramFile != telegramFile { - let signal = chatMessageSticker(account: item.context.account, file: telegramFile, small: false, onlyFullSize: self.telegramFile != nil) + let signal = chatMessageSticker(account: item.context.account, file: telegramFile, small: false, onlyFullSize: self.telegramFile != nil, synchronousLoad: synchronousLoad) self.telegramFile = telegramFile - self.imageNode.setSignal(signal) + self.imageNode.setSignal(signal, attemptSynchronously: synchronousLoad) self.fetchDisposable.set(freeMediaFileInteractiveFetched(account: item.context.account, fileReference: .message(message: MessageReference(item.message), media: telegramFile)).start()) } @@ -243,6 +251,16 @@ class ChatMessageStickerItemNode: ChatMessageItemView { rect.origin.y = containerSize.height - rect.maxY + self.insets.top self.placeholderNode.updateAbsoluteRect(CGRect(origin: CGPoint(x: rect.minX + placeholderNode.frame.minX, y: rect.minY + placeholderNode.frame.minY), size: placeholderNode.frame.size), within: containerSize) + + if let backgroundNode = self.backgroundNode { + backgroundNode.update(rect: CGRect(origin: CGPoint(x: rect.minX + self.placeholderNode.frame.minX, y: rect.minY + self.placeholderNode.frame.minY), size: self.placeholderNode.frame.size), within: containerSize) + } + } + } + + override func applyAbsoluteOffset(value: CGPoint, animationCurve: ContainedViewLayoutTransitionCurve, duration: Double) { + if let backgroundNode = self.backgroundNode { + backgroundNode.offset(value: value, animationCurve: animationCurve, duration: duration) } } @@ -288,9 +306,7 @@ class ChatMessageStickerItemNode: ChatMessageItemView { let viaBotLayout = TextNode.asyncLayout(self.viaBotNode) let makeReplyInfoLayout = ChatMessageReplyInfoNode.asyncLayout(self.replyInfoNode) - let currentReplyBackgroundNode = self.replyBackgroundNode let currentShareButtonNode = self.shareButtonNode - let currentItem = self.item return { item, params, mergedTop, mergedBottom, dateHeaderAtBottom in let accessibilityData = ChatMessageAccessibilityData(item: item, isSelected: nil) @@ -479,8 +495,6 @@ class ChatMessageStickerItemNode: ChatMessageItemView { var viaBotApply: (TextNodeLayout, () -> TextNode)? var replyInfoApply: (CGSize, () -> ChatMessageReplyInfoNode)? - var updatedReplyBackgroundNode: ASImageNode? - var replyBackgroundImage: UIImage? var replyMarkup: ReplyMarkupMessageAttribute? var availableWidth = max(60.0, params.width - params.leftInset - params.rightInset - max(imageSize.width, 160.0) - 20.0 - layoutConstants.bubble.edgeInset * 2.0 - avatarInset - layoutConstants.bubble.contentInsets.left) @@ -529,16 +543,11 @@ class ChatMessageStickerItemNode: ChatMessageItemView { } } } + + var needsReplyBackground = false if replyInfoApply != nil || viaBotApply != nil { - if let currentReplyBackgroundNode = currentReplyBackgroundNode { - updatedReplyBackgroundNode = currentReplyBackgroundNode - } else { - updatedReplyBackgroundNode = ASImageNode() - } - - let graphics = PresentationResourcesChat.additionalGraphics(item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper, bubbleCorners: item.presentationData.chatBubbleCorners) - replyBackgroundImage = graphics.chatFreeformContentAdditionalInfoBackgroundImage + needsReplyBackground = true } var updatedShareButtonNode: ChatMessageShareButton? @@ -642,9 +651,22 @@ class ChatMessageStickerItemNode: ChatMessageItemView { strongSelf.updateAccessibilityData(accessibilityData) transition.updateFrame(node: strongSelf.imageNode, frame: updatedImageFrame) + strongSelf.enableSynchronousImageApply = true imageApply() + strongSelf.enableSynchronousImageApply = false if let immediateThumbnailData = telegramFile?.immediateThumbnailData { + if strongSelf.backgroundNode == nil { + if let backgroundNode = item.controllerInteraction.presentationContext.backgroundNode?.makeBubbleBackground(for: .free) { + strongSelf.backgroundNode = backgroundNode + strongSelf.placeholderNode.addBackdropNode(backgroundNode) + + if let (rect, size) = strongSelf.absoluteRect { + strongSelf.updateAbsoluteRect(rect, within: size) + } + } + } + let foregroundColor = bubbleVariableColor(variableColor: item.presentationData.theme.theme.chat.message.stickerPlaceholderColor, wallpaper: item.presentationData.theme.wallpaper) let shimmeringColor = bubbleVariableColor(variableColor: item.presentationData.theme.theme.chat.message.stickerPlaceholderShimmerColor, wallpaper: item.presentationData.theme.wallpaper) @@ -681,13 +703,13 @@ class ChatMessageStickerItemNode: ChatMessageItemView { strongSelf.shareButtonNode = nil } - if let updatedReplyBackgroundNode = updatedReplyBackgroundNode { - if strongSelf.replyBackgroundNode == nil { - strongSelf.replyBackgroundNode = updatedReplyBackgroundNode - strongSelf.addSubnode(updatedReplyBackgroundNode) - updatedReplyBackgroundNode.image = replyBackgroundImage + if needsReplyBackground { + if let replyBackgroundNode = strongSelf.replyBackgroundNode { + replyBackgroundNode.updateColor(color: selectDateFillStaticColor(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper), enableBlur: dateFillNeedsBlur(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper), transition: .immediate) } else { - strongSelf.replyBackgroundNode?.image = replyBackgroundImage + let replyBackgroundNode = NavigationBackgroundNode(color: selectDateFillStaticColor(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper), enableBlur: dateFillNeedsBlur(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper)) + strongSelf.replyBackgroundNode = replyBackgroundNode + strongSelf.contextSourceNode.contentNode.addSubnode(replyBackgroundNode) } } else if let replyBackgroundNode = strongSelf.replyBackgroundNode { replyBackgroundNode.removeFromSupernode() @@ -701,7 +723,10 @@ class ChatMessageStickerItemNode: ChatMessageItemView { strongSelf.addSubnode(viaBotNode) } viaBotNode.frame = viaBotFrame - strongSelf.replyBackgroundNode?.frame = CGRect(origin: CGPoint(x: viaBotFrame.minX - 6.0, y: viaBotFrame.minY - 2.0 - UIScreenPixel), size: CGSize(width: viaBotFrame.size.width + 11.0, height: viaBotFrame.size.height + 5.0)) + if let replyBackgroundNode = strongSelf.replyBackgroundNode { + replyBackgroundNode.frame = CGRect(origin: CGPoint(x: viaBotFrame.minX - 6.0, y: viaBotFrame.minY - 2.0 - UIScreenPixel), size: CGSize(width: viaBotFrame.size.width + 11.0, height: viaBotFrame.size.height + 5.0)) + replyBackgroundNode.update(size: replyBackgroundNode.bounds.size, cornerRadius: 8.0, transition: .immediate) + } } else if let viaBotNode = strongSelf.viaBotNode { viaBotNode.removeFromSupernode() strongSelf.viaBotNode = nil @@ -711,10 +736,13 @@ class ChatMessageStickerItemNode: ChatMessageItemView { let replyInfoNode = replyInfoApply() if strongSelf.replyInfoNode == nil { strongSelf.replyInfoNode = replyInfoNode - strongSelf.addSubnode(replyInfoNode) + strongSelf.contextSourceNode.contentNode.addSubnode(replyInfoNode) } replyInfoNode.frame = replyInfoFrame - strongSelf.replyBackgroundNode?.frame = replyBackgroundFrame ?? CGRect() + if let replyBackgroundNode = strongSelf.replyBackgroundNode, let replyBackgroundFrame = replyBackgroundFrame { + replyBackgroundNode.frame = replyBackgroundFrame + replyBackgroundNode.update(size: replyBackgroundNode.bounds.size, cornerRadius: 8.0, transition: .immediate) + } if isEmoji && !incoming { if let _ = item.controllerInteraction.selectionState { @@ -856,7 +884,7 @@ class ChatMessageStickerItemNode: ChatMessageItemView { for attribute in item.content.firstMessage.attributes { if let attribute = attribute as? SourceReferenceMessageAttribute { openPeerId = attribute.messageId.peerId - navigate = .chat(textInputState: nil, subject: .message(id: attribute.messageId, highlight: true), peekData: nil) + navigate = .chat(textInputState: nil, subject: .message(id: attribute.messageId, highlight: true, timecode: nil), peekData: nil) } } @@ -980,7 +1008,7 @@ class ChatMessageStickerItemNode: ChatMessageItemView { if translation.x < -45.0, self.swipeToReplyNode == nil, let item = self.item { self.swipeToReplyFeedback?.impact() - 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), action: ChatMessageSwipeToReplyNode.Action(self.currentSwipeAction)) + let swipeToReplyNode = ChatMessageSwipeToReplyNode(fillColor: selectDateFillStaticColor(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper), enableBlur: dateFillNeedsBlur(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper), foregroundColor: bubbleVariableColor(variableColor: item.presentationData.theme.theme.chat.message.shareButtonForegroundColor, wallpaper: item.presentationData.theme.wallpaper), action: ChatMessageSwipeToReplyNode.Action(self.currentSwipeAction)) self.swipeToReplyNode = swipeToReplyNode self.addSubnode(swipeToReplyNode) animateReplyNodeIn = true @@ -1059,6 +1087,15 @@ class ChatMessageStickerItemNode: ChatMessageItemView { if let item = self.item, item.presentationData.largeEmoji && messageIsElligibleForLargeEmoji(item.message) { isEmoji = true } + + let transition: ContainedViewLayoutTransition = animated ? .animated(duration: 0.2, curve: .easeInOut) : .immediate + let replyAlpha: CGFloat = item.controllerInteraction.selectionState == nil ? 1.0 : 0.0 + if let replyInfoNode = self.replyInfoNode { + transition.updateAlpha(node: replyInfoNode, alpha: replyAlpha) + } + if let replyBackgroundNode = self.replyBackgroundNode { + transition.updateAlpha(node: replyBackgroundNode, alpha: replyAlpha) + } if let selectionState = item.controllerInteraction.selectionState { let selected = selectionState.selectedIds.contains(item.message.id) @@ -1160,6 +1197,10 @@ class ChatMessageStickerItemNode: ChatMessageItemView { } } } + + override func cancelInsertionAnimations() { + self.layer.removeAllAnimations() + } override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { super.animateInsertion(currentTimestamp, duration: duration, short: short) @@ -1186,4 +1227,152 @@ class ChatMessageStickerItemNode: ChatMessageItemView { override func addAccessoryItemNode(_ accessoryItemNode: ListViewAccessoryItemNode) { self.contextSourceNode.contentNode.addSubnode(accessoryItemNode) } + + func animateContentFromTextInputField(textInput: ChatMessageTransitionNode.Source.TextInput, transition: CombinedTransition) { + guard let _ = self.item else { + return + } + + let localSourceContentFrame = self.contextSourceNode.contentNode.view.convert(textInput.contentView.frame.offsetBy(dx: self.contextSourceNode.contentRect.minX, dy: self.contextSourceNode.contentRect.minY), to: self.contextSourceNode.contentNode.view) + textInput.contentView.frame = localSourceContentFrame + + self.contextSourceNode.contentNode.view.addSubview(textInput.contentView) + + let sourceCenter = CGPoint( + x: localSourceContentFrame.minX + 11.2, + y: localSourceContentFrame.midY - 1.8 + ) + let localSourceCenter = CGPoint( + x: sourceCenter.x - localSourceContentFrame.minX, + y: sourceCenter.y - localSourceContentFrame.minY + ) + let localSourceOffset = CGPoint( + x: localSourceCenter.x - localSourceContentFrame.width / 2.0, + y: localSourceCenter.y - localSourceContentFrame.height / 2.0 + ) + + let sourceScale: CGFloat = 28.0 / self.imageNode.frame.height + + let offset = CGPoint( + x: sourceCenter.x - self.imageNode.frame.midX, + y: sourceCenter.y - self.imageNode.frame.midY + ) + + transition.animatePositionAdditive(layer: self.imageNode.layer, offset: offset) + transition.horizontal.animateTransformScale(node: self.imageNode, from: sourceScale) + transition.animatePositionAdditive(layer: self.placeholderNode.layer, offset: offset) + transition.horizontal.animateTransformScale(node: self.placeholderNode, from: sourceScale) + + let inverseScale = 1.0 / sourceScale + + transition.animatePositionAdditive(layer: textInput.contentView.layer, offset: CGPoint(), to: CGPoint( + x: -offset.x - localSourceOffset.x * (inverseScale - 1.0), + y: -offset.y - localSourceOffset.y * (inverseScale - 1.0) + ), removeOnCompletion: false) + transition.horizontal.updateTransformScale(layer: textInput.contentView.layer, scale: 1.0 / sourceScale) + + textInput.contentView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false, completion: { _ in + textInput.contentView.removeFromSuperview() + }) + + self.imageNode.layer.animateAlpha(from: 0.0, to: self.imageNode.alpha, duration: 0.1) + self.placeholderNode.layer.animateAlpha(from: 0.0, to: self.placeholderNode.alpha, duration: 0.1) + + self.dateAndStatusNode.layer.animateAlpha(from: 0.0, to: self.dateAndStatusNode.alpha, duration: 0.15, delay: 0.16) + } + + func animateContentFromStickerGridItem(stickerSource: ChatMessageTransitionNode.Sticker, transition: CombinedTransition) { + guard let _ = self.item else { + return + } + + let localSourceContentFrame = CGRect( + origin: CGPoint( + x: self.imageNode.frame.minX + self.imageNode.frame.size.width / 2.0 - stickerSource.imageNode.frame.size.width / 2.0, + y: self.imageNode.frame.minY + self.imageNode.frame.size.height / 2.0 - stickerSource.imageNode.frame.size.height / 2.0 + ), + size: stickerSource.imageNode.frame.size + ) + + var snapshotView: UIView? + if let animationNode = stickerSource.animationNode { + snapshotView = animationNode.view.snapshotContentTree() + } else { + snapshotView = stickerSource.imageNode.view.snapshotContentTree() + } + snapshotView?.frame = localSourceContentFrame + + if let snapshotView = snapshotView { + self.contextSourceNode.contentNode.view.addSubview(snapshotView) + } + + let sourceCenter = CGPoint( + x: localSourceContentFrame.midX, + y: localSourceContentFrame.midY + ) + let localSourceCenter = CGPoint( + x: sourceCenter.x - localSourceContentFrame.minX, + y: sourceCenter.y - localSourceContentFrame.minY + ) + let localSourceOffset = CGPoint( + x: localSourceCenter.x - localSourceContentFrame.width / 2.0, + y: localSourceCenter.y - localSourceContentFrame.height / 2.0 + ) + + let sourceScale: CGFloat = stickerSource.imageNode.frame.height / self.imageNode.frame.height + + let offset = CGPoint( + x: sourceCenter.x - self.imageNode.frame.midX, + y: sourceCenter.y - self.imageNode.frame.midY + ) + + transition.animatePositionAdditive(layer: self.imageNode.layer, offset: offset) + transition.horizontal.animateTransformScale(node: self.imageNode, from: sourceScale) + transition.animatePositionAdditive(layer: self.placeholderNode.layer, offset: offset) + transition.horizontal.animateTransformScale(node: self.placeholderNode, from: sourceScale) + + let inverseScale = 1.0 / sourceScale + + if let snapshotView = snapshotView { + transition.animatePositionAdditive(layer: snapshotView.layer, offset: CGPoint(), to: CGPoint( + x: -offset.x - localSourceOffset.x * (inverseScale - 1.0), + y: -offset.y - localSourceOffset.y * (inverseScale - 1.0) + ), removeOnCompletion: false) + transition.horizontal.updateTransformScale(layer: snapshotView.layer, scale: 1.0 / sourceScale) + + snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.06, removeOnCompletion: false, completion: { [weak snapshotView] _ in + snapshotView?.removeFromSuperview() + }) + + self.imageNode.layer.animateAlpha(from: 0.0, to: self.imageNode.alpha, duration: 0.03) + self.placeholderNode.layer.animateAlpha(from: 0.0, to: self.placeholderNode.alpha, duration: 0.03) + } + + self.dateAndStatusNode.layer.animateAlpha(from: 0.0, to: self.dateAndStatusNode.alpha, duration: 0.15, delay: 0.16) + + if let animationNode = stickerSource.animationNode { + animationNode.layer.animateScale(from: 0.1, to: 1.0, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring) + animationNode.layer.animateAlpha(from: 0.0, to: animationNode.alpha, duration: 0.4) + } + + stickerSource.imageNode.layer.animateScale(from: 0.1, to: 1.0, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring) + stickerSource.imageNode.layer.animateAlpha(from: 0.0, to: stickerSource.imageNode.alpha, duration: 0.4) + + if let placeholderNode = stickerSource.placeholderNode { + placeholderNode.layer.animateScale(from: 0.1, to: 1.0, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring) + placeholderNode.layer.animateAlpha(from: 0.0, to: placeholderNode.alpha, duration: 0.4) + } + } + + func animateReplyPanel(sourceReplyPanel: ChatMessageTransitionNode.ReplyPanel, transition: CombinedTransition) { + if let replyInfoNode = self.replyInfoNode { + let localRect = self.contextSourceNode.contentNode.view.convert(sourceReplyPanel.relativeSourceRect, to: replyInfoNode.view) + + let offset = replyInfoNode.animateFromInputPanel(sourceReplyPanel: sourceReplyPanel, localRect: localRect, transition: transition) + if let replyBackgroundNode = self.replyBackgroundNode { + transition.animatePositionAdditive(layer: replyBackgroundNode.layer, offset: offset) + replyBackgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1) + } + } + } } diff --git a/submodules/TelegramUI/Sources/ChatMessageSwipeToReplyNode.swift b/submodules/TelegramUI/Sources/ChatMessageSwipeToReplyNode.swift index 4d9041526b..6ba2ffb591 100644 --- a/submodules/TelegramUI/Sources/ChatMessageSwipeToReplyNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageSwipeToReplyNode.swift @@ -11,25 +11,18 @@ final class ChatMessageSwipeToReplyNode: ASDisplayNode { case unlike } - private let backgroundNode: ASImageNode + private let backgroundNode: NavigationBackgroundNode + private let foregroundNode: ASImageNode - init(fillColor: UIColor, strokeColor: UIColor, foregroundColor: UIColor, action: ChatMessageSwipeToReplyNode.Action) { - self.backgroundNode = ASImageNode() - self.backgroundNode.isLayerBacked = true - self.backgroundNode.image = generateImage(CGSize(width: 33.0, height: 33.0), rotatedContext: { size, context in + init(fillColor: UIColor, enableBlur: Bool, foregroundColor: UIColor, action: ChatMessageSwipeToReplyNode.Action) { + self.backgroundNode = NavigationBackgroundNode(color: fillColor, enableBlur: enableBlur) + self.backgroundNode.isUserInteractionEnabled = false + + self.foregroundNode = ASImageNode() + self.foregroundNode.isUserInteractionEnabled = false + + self.foregroundNode.image = generateImage(CGSize(width: 33.0, height: 33.0), rotatedContext: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) - context.setFillColor(fillColor.cgColor) - context.fillEllipse(in: CGRect(origin: CGPoint(), size: size)) - - let lineWidth: CGFloat = 1.0 - let halfLineWidth = lineWidth / 2.0 - var strokeAlpha: CGFloat = 0.0 - strokeColor.getRed(nil, green: nil, blue: nil, alpha: &strokeAlpha) - if !strokeAlpha.isZero { - context.setStrokeColor(strokeColor.cgColor) - context.setLineWidth(lineWidth) - context.strokeEllipse(in: CGRect(origin: CGPoint(x: halfLineWidth, y: halfLineWidth), size: CGSize(width: size.width - lineWidth, height: size.width - lineWidth))) - } switch action { case .reply: @@ -65,7 +58,10 @@ final class ChatMessageSwipeToReplyNode: ASDisplayNode { super.init() self.addSubnode(self.backgroundNode) + self.addSubnode(self.foregroundNode) self.backgroundNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 33.0, height: 33.0)) + self.backgroundNode.update(size: self.backgroundNode.bounds.size, cornerRadius: self.backgroundNode.bounds.height / 2.0, transition: .immediate) + self.foregroundNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 33.0, height: 33.0)) } } diff --git a/submodules/TelegramUI/Sources/ChatMessageTextBubbleContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageTextBubbleContentNode.swift index c018521b50..d63be62314 100644 --- a/submodules/TelegramUI/Sources/ChatMessageTextBubbleContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageTextBubbleContentNode.swift @@ -640,4 +640,24 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { override func getStatusNode() -> ASDisplayNode? { return self.statusNode } + + func animateFrom(sourceView: UIView, scrollOffset: CGFloat, widthDifference: CGFloat, transition: CombinedTransition) { + self.view.addSubview(sourceView) + + sourceView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.1, removeOnCompletion: false, completion: { [weak sourceView] _ in + sourceView?.removeFromSuperview() + }) + self.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.08) + + let offset = CGPoint( + x: sourceView.frame.minX - (self.textNode.frame.minX - 0.0), + y: sourceView.frame.minY - (self.textNode.frame.minY - 3.0) - scrollOffset + ) + + transition.vertical.animatePositionAdditive(node: self.textNode, offset: offset) + transition.updatePosition(layer: sourceView.layer, position: CGPoint(x: sourceView.layer.position.x - offset.x, y: sourceView.layer.position.y - offset.y)) + + self.statusNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) + transition.horizontal.animatePositionAdditive(node: self.statusNode, offset: CGPoint(x: -widthDifference, y: 0.0)) + } } diff --git a/submodules/TelegramUI/Sources/ChatMessageTransitionNode.swift b/submodules/TelegramUI/Sources/ChatMessageTransitionNode.swift new file mode 100644 index 0000000000..ab733459c0 --- /dev/null +++ b/submodules/TelegramUI/Sources/ChatMessageTransitionNode.swift @@ -0,0 +1,694 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display +import ContextUI +import AnimatedStickerNode +import SwiftSignalKit + +private final class OverlayTransitionContainerNode: ViewControllerTracingNode { + override init() { + super.init() + } + + deinit { + } + + override func didLoad() { + super.didLoad() + } + + func updateLayout(layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + return nil + } +} + +private final class OverlayTransitionContainerController: ViewController, StandalonePresentableController { + private let _ready = Promise() + override public var ready: Promise { + return self._ready + } + + private var controllerNode: OverlayTransitionContainerNode { + return self.displayNode as! OverlayTransitionContainerNode + } + + private var wasDismissed: Bool = false + + init() { + super.init(navigationBarPresentationData: nil) + + self.statusBar.statusBarStyle = .Ignore + } + + required init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + } + + override public func loadDisplayNode() { + self.displayNode = OverlayTransitionContainerNode() + + self.displayNodeDidLoad() + + self._ready.set(.single(true)) + } + + override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + super.containerLayoutUpdated(layout, transition: transition) + + self.controllerNode.updateLayout(layout: layout, transition: transition) + } + + override public func viewDidAppear(_ animated: Bool) { + if self.ignoreAppearanceMethodInvocations() { + return + } + super.viewDidAppear(animated) + } + + override public func dismiss(completion: (() -> Void)? = nil) { + if !self.wasDismissed { + self.wasDismissed = true + self.presentingViewController?.dismiss(animated: false, completion: nil) + completion?() + } + } +} + +public final class ChatMessageTransitionNode: ASDisplayNode { + static let animationDuration: Double = 0.3 + + static let verticalAnimationControlPoints: (Float, Float, Float, Float) = (0.19919472913616398, 0.010644531250000006, 0.27920937042459737, 0.91025390625) + static let verticalAnimationCurve: ContainedViewLayoutTransitionCurve = .custom(verticalAnimationControlPoints.0, verticalAnimationControlPoints.1, verticalAnimationControlPoints.2, verticalAnimationControlPoints.3) + static let horizontalAnimationCurve: ContainedViewLayoutTransitionCurve = .custom(0.23, 1.0, 0.32, 1.0) + + final class ReplyPanel { + let titleNode: ASDisplayNode + let textNode: ASDisplayNode + let lineNode: ASDisplayNode + let imageNode: ASDisplayNode + let relativeSourceRect: CGRect + let relativeTargetRect: CGRect + + init(titleNode: ASDisplayNode, textNode: ASDisplayNode, lineNode: ASDisplayNode, imageNode: ASDisplayNode, relativeSourceRect: CGRect, relativeTargetRect: CGRect) { + self.titleNode = titleNode + self.textNode = textNode + self.lineNode = lineNode + self.imageNode = imageNode + self.relativeSourceRect = relativeSourceRect + self.relativeTargetRect = relativeTargetRect + } + } + + final class Sticker { + let imageNode: TransformImageNode + let animationNode: GenericAnimatedStickerNode? + let placeholderNode: ASDisplayNode? + let relativeSourceRect: CGRect + + init(imageNode: TransformImageNode, animationNode: GenericAnimatedStickerNode?, placeholderNode: ASDisplayNode?, relativeSourceRect: CGRect) { + self.imageNode = imageNode + self.animationNode = animationNode + self.placeholderNode = placeholderNode + self.relativeSourceRect = relativeSourceRect + } + } + + enum Source { + final class TextInput { + let backgroundView: UIView + let contentView: UIView + let sourceRect: CGRect + let scrollOffset: CGFloat + + init(backgroundView: UIView, contentView: UIView, sourceRect: CGRect, scrollOffset: CGFloat) { + self.backgroundView = backgroundView + self.contentView = contentView + self.sourceRect = sourceRect + self.scrollOffset = scrollOffset + } + } + + enum StickerInput { + case inputPanel(itemNode: ChatMediaInputStickerGridItemNode) + case mediaPanel(itemNode: HorizontalStickerGridItemNode) + case inputPanelSearch(itemNode: StickerPaneSearchStickerItemNode) + case emptyPanel(itemNode: ChatEmptyNodeStickerContentNode) + } + + final class AudioMicInput { + let micButton: ChatTextInputMediaRecordingButton + + init(micButton: ChatTextInputMediaRecordingButton) { + self.micButton = micButton + } + } + + final class VideoMessage { + let view: UIView + + init(view: UIView) { + self.view = view + } + } + + final class MediaInput { + let extractSnapshot: () -> UIView? + + init(extractSnapshot: @escaping () -> UIView?) { + self.extractSnapshot = extractSnapshot + } + } + + case textInput(textInput: TextInput, replyPanel: ReplyAccessoryPanelNode?) + case stickerMediaInput(input: StickerInput, replyPanel: ReplyAccessoryPanelNode?) + case audioMicInput(AudioMicInput) + case videoMessage(VideoMessage) + case mediaInput(MediaInput) + } + + private final class AnimatingItemNode: ASDisplayNode { + let itemNode: ChatMessageItemView + private let contextSourceNode: ContextExtractedContentContainingNode + private let source: ChatMessageTransitionNode.Source + private let getContentAreaInScreenSpace: () -> CGRect + + private let scrollingContainer: ASDisplayNode + private let containerNode: ASDisplayNode + private let clippingNode: ASDisplayNode + + weak var overlayController: OverlayTransitionContainerController? + + var animationEnded: (() -> Void)? + var updateAfterCompletion: Bool = false + + init(itemNode: ChatMessageItemView, contextSourceNode: ContextExtractedContentContainingNode, source: ChatMessageTransitionNode.Source, getContentAreaInScreenSpace: @escaping () -> CGRect) { + self.itemNode = itemNode + self.getContentAreaInScreenSpace = getContentAreaInScreenSpace + + self.clippingNode = ASDisplayNode() + self.clippingNode.clipsToBounds = true + + self.scrollingContainer = ASDisplayNode() + self.containerNode = ASDisplayNode() + self.contextSourceNode = contextSourceNode + self.source = source + + super.init() + + self.addSubnode(self.clippingNode) + self.clippingNode.addSubnode(self.scrollingContainer) + self.scrollingContainer.addSubnode(self.containerNode) + } + + deinit { + self.contextSourceNode.addSubnode(self.contextSourceNode.contentNode) + } + + func updateLayout(size: CGSize) { + self.clippingNode.frame = CGRect(origin: CGPoint(), size: size) + } + + func beginAnimation() { + let verticalDuration: Double = ChatMessageTransitionNode.animationDuration + let horizontalDuration: Double = verticalDuration + let delay: Double = 0.0 + + var updatedContentAreaInScreenSpace = self.getContentAreaInScreenSpace() + updatedContentAreaInScreenSpace.size.width = updatedContentAreaInScreenSpace.origin.x + self.clippingNode.bounds.width + updatedContentAreaInScreenSpace.origin.x = 0.0 + + let clippingOffset = updatedContentAreaInScreenSpace.minY - self.clippingNode.frame.minY + self.clippingNode.frame = CGRect(origin: CGPoint(x: 0.0, y: updatedContentAreaInScreenSpace.minY), size: CGSize(width: updatedContentAreaInScreenSpace.size.width, height: self.clippingNode.bounds.height)) + self.clippingNode.bounds = CGRect(origin: CGPoint(x: 0.0, y: clippingOffset), size: self.clippingNode.bounds.size) + + switch self.source { + case let .textInput(initialTextInput, replyPanel): + self.contextSourceNode.isExtractedToContextPreview = true + self.contextSourceNode.isExtractedToContextPreviewUpdated?(true) + + var currentContentRect = self.contextSourceNode.contentRect + let contextSourceNode = self.contextSourceNode + self.contextSourceNode.layoutUpdated = { [weak self, weak contextSourceNode] size in + guard let strongSelf = self, let contextSourceNode = contextSourceNode, strongSelf.contextSourceNode === contextSourceNode else { + return + } + let updatedContentRect = contextSourceNode.contentRect + let deltaY = updatedContentRect.height - currentContentRect.height + if !deltaY.isZero { + currentContentRect = updatedContentRect + strongSelf.addContentOffset(offset: deltaY, itemNode: nil) + } + } + + self.containerNode.addSubnode(self.contextSourceNode.contentNode) + + let targetAbsoluteRect = self.contextSourceNode.view.convert(self.contextSourceNode.contentRect, to: self.view) + + let sourceRect = self.view.convert(initialTextInput.sourceRect, from: nil) + let sourceBackgroundAbsoluteRect = initialTextInput.backgroundView.frame.offsetBy(dx: sourceRect.minX, dy: sourceRect.minY) + let sourceAbsoluteRect = CGRect(origin: CGPoint(x: sourceBackgroundAbsoluteRect.minX, y: sourceBackgroundAbsoluteRect.maxY - self.contextSourceNode.contentRect.height), size: self.contextSourceNode.contentRect.size) + + let textInput = ChatMessageTransitionNode.Source.TextInput(backgroundView: initialTextInput.backgroundView, contentView: initialTextInput.contentView, sourceRect: sourceRect, scrollOffset: initialTextInput.scrollOffset) + + textInput.backgroundView.frame = CGRect(origin: CGPoint(x: 0.0, y: sourceAbsoluteRect.height - sourceBackgroundAbsoluteRect.height), size: textInput.backgroundView.bounds.size) + textInput.contentView.frame = textInput.contentView.frame.offsetBy(dx: 0.0, dy: sourceAbsoluteRect.height - sourceBackgroundAbsoluteRect.height) + + var sourceReplyPanel: ReplyPanel? + if let replyPanel = replyPanel, let replyPanelParentView = replyPanel.view.superview { + let replyPanelFrame = replyPanel.originalFrameBeforeDismissed ?? replyPanel.frame + var replySourceAbsoluteFrame = replyPanelParentView.convert(replyPanelFrame, to: self.view) + + replySourceAbsoluteFrame.origin.x -= sourceAbsoluteRect.minX - self.contextSourceNode.contentRect.minX + replySourceAbsoluteFrame.origin.y -= sourceAbsoluteRect.minY - self.contextSourceNode.contentRect.minY + + var globalTargetFrame = replySourceAbsoluteFrame.offsetBy(dx: 0.0, dy: replyPanelFrame.height) + + globalTargetFrame.origin.x += sourceAbsoluteRect.minX - targetAbsoluteRect.minX + globalTargetFrame.origin.y += sourceAbsoluteRect.minY - targetAbsoluteRect.minY + + sourceReplyPanel = ReplyPanel(titleNode: replyPanel.titleNode, textNode: replyPanel.textNode, lineNode: replyPanel.lineNode, imageNode: replyPanel.imageNode, relativeSourceRect: replySourceAbsoluteFrame, relativeTargetRect: globalTargetFrame) + } + + self.itemNode.cancelInsertionAnimations() + + let horizontalCurve = ChatMessageTransitionNode.horizontalAnimationCurve + let horizontalTransition: ContainedViewLayoutTransition = .animated(duration: horizontalDuration, curve: horizontalCurve) + let verticalCurve = ChatMessageTransitionNode.verticalAnimationCurve + let verticalTransition: ContainedViewLayoutTransition = .animated(duration: verticalDuration, curve: verticalCurve) + + let combinedTransition = CombinedTransition(horizontal: horizontalTransition, vertical: verticalTransition) + + self.containerNode.frame = targetAbsoluteRect.offsetBy(dx: -self.contextSourceNode.contentRect.minX, dy: -self.contextSourceNode.contentRect.minY) + self.contextSourceNode.updateAbsoluteRect?(self.containerNode.frame, UIScreen.main.bounds.size) + self.containerNode.layer.animatePosition(from: CGPoint(x: 0.0, y: sourceAbsoluteRect.maxY - targetAbsoluteRect.maxY), to: CGPoint(), duration: verticalDuration, delay: delay, mediaTimingFunction: verticalCurve.mediaTimingFunction, additive: true, force: true, completion: { [weak self] _ in + guard let strongSelf = self else { + return + } + strongSelf.endAnimation() + }) + self.containerNode.layer.animatePosition(from: CGPoint(x: sourceAbsoluteRect.minX - targetAbsoluteRect.minX, y: 0.0), to: CGPoint(), duration: horizontalDuration, delay: delay, mediaTimingFunction: horizontalCurve.mediaTimingFunction, additive: true) + self.contextSourceNode.applyAbsoluteOffset?(CGPoint(x: sourceAbsoluteRect.minX - targetAbsoluteRect.minX, y: 0.0), horizontalCurve, horizontalDuration) + self.contextSourceNode.applyAbsoluteOffset?(CGPoint(x: 0.0, y: sourceAbsoluteRect.maxY - targetAbsoluteRect.maxY), verticalCurve, verticalDuration) + + if let itemNode = self.itemNode as? ChatMessageBubbleItemNode { + itemNode.animateContentFromTextInputField(textInput: textInput, transition: combinedTransition) + if let sourceReplyPanel = sourceReplyPanel { + itemNode.animateReplyPanel(sourceReplyPanel: sourceReplyPanel, transition: combinedTransition) + } + } else if let itemNode = self.itemNode as? ChatMessageAnimatedStickerItemNode { + itemNode.animateContentFromTextInputField(textInput: textInput, transition: combinedTransition) + if let sourceReplyPanel = sourceReplyPanel { + itemNode.animateReplyPanel(sourceReplyPanel: sourceReplyPanel, transition: combinedTransition) + } + } else if let itemNode = self.itemNode as? ChatMessageStickerItemNode { + itemNode.animateContentFromTextInputField(textInput: textInput, transition: combinedTransition) + if let sourceReplyPanel = sourceReplyPanel { + itemNode.animateReplyPanel(sourceReplyPanel: sourceReplyPanel, transition: combinedTransition) + } + } + case let .stickerMediaInput(stickerMediaInput, replyPanel): + self.itemNode.cancelInsertionAnimations() + + self.contextSourceNode.isExtractedToContextPreview = true + self.contextSourceNode.isExtractedToContextPreviewUpdated?(true) + + self.containerNode.addSubnode(self.contextSourceNode.contentNode) + + let stickerSource: Sticker + let sourceAbsoluteRect: CGRect + switch stickerMediaInput { + case let .inputPanel(sourceItemNode): + stickerSource = Sticker(imageNode: sourceItemNode.imageNode, animationNode: sourceItemNode.animationNode, placeholderNode: sourceItemNode.placeholderNode, relativeSourceRect: sourceItemNode.imageNode.frame) + sourceAbsoluteRect = sourceItemNode.view.convert(stickerSource.imageNode.frame, to: self.view) + case let .mediaPanel(sourceItemNode): + stickerSource = Sticker(imageNode: sourceItemNode.imageNode, animationNode: sourceItemNode.animationNode, placeholderNode: sourceItemNode.placeholderNode, relativeSourceRect: sourceItemNode.imageNode.frame) + sourceAbsoluteRect = sourceItemNode.view.convert(stickerSource.imageNode.frame, to: self.view) + case let .inputPanelSearch(sourceItemNode): + stickerSource = Sticker(imageNode: sourceItemNode.imageNode, animationNode: sourceItemNode.animationNode, placeholderNode: nil, relativeSourceRect: sourceItemNode.imageNode.frame) + sourceAbsoluteRect = sourceItemNode.view.convert(stickerSource.imageNode.frame, to: self.view) + case let .emptyPanel(sourceItemNode): + stickerSource = Sticker(imageNode: sourceItemNode.stickerNode.imageNode, animationNode: sourceItemNode.stickerNode.animationNode, placeholderNode: nil, relativeSourceRect: sourceItemNode.stickerNode.imageNode.frame) + sourceAbsoluteRect = sourceItemNode.stickerNode.view.convert(sourceItemNode.stickerNode.imageNode.frame, to: self.view) + } + + let targetAbsoluteRect = self.contextSourceNode.view.convert(self.contextSourceNode.contentRect, to: self.view) + + var sourceReplyPanel: ReplyPanel? + if let replyPanel = replyPanel, let replyPanelParentView = replyPanel.view.superview { + var replySourceAbsoluteFrame = replyPanelParentView.convert(replyPanel.originalFrameBeforeDismissed ?? replyPanel.frame, to: self.view) + replySourceAbsoluteFrame.origin.x -= sourceAbsoluteRect.midX - self.contextSourceNode.contentRect.midX + replySourceAbsoluteFrame.origin.y -= sourceAbsoluteRect.midY - self.contextSourceNode.contentRect.midY + + sourceReplyPanel = ReplyPanel(titleNode: replyPanel.titleNode, textNode: replyPanel.textNode, lineNode: replyPanel.lineNode, imageNode: replyPanel.imageNode, relativeSourceRect: replySourceAbsoluteFrame, relativeTargetRect: replySourceAbsoluteFrame.offsetBy(dx: 0.0, dy: replySourceAbsoluteFrame.height)) + } + + let combinedTransition = CombinedTransition(horizontal: .animated(duration: horizontalDuration, curve: ChatMessageTransitionNode.horizontalAnimationCurve), vertical: .animated(duration: verticalDuration, curve: ChatMessageTransitionNode.verticalAnimationCurve)) + + if let itemNode = self.itemNode as? ChatMessageAnimatedStickerItemNode { + itemNode.animateContentFromStickerGridItem(stickerSource: stickerSource, transition: combinedTransition) + if let sourceAnimationNode = stickerSource.animationNode { + itemNode.animationNode?.setFrameIndex(sourceAnimationNode.currentFrameIndex) + } + if let sourceReplyPanel = sourceReplyPanel { + itemNode.animateReplyPanel(sourceReplyPanel: sourceReplyPanel, transition: combinedTransition) + } + } else if let itemNode = self.itemNode as? ChatMessageStickerItemNode { + itemNode.animateContentFromStickerGridItem(stickerSource: stickerSource, transition: combinedTransition) + if let sourceReplyPanel = sourceReplyPanel { + itemNode.animateReplyPanel(sourceReplyPanel: sourceReplyPanel, transition: combinedTransition) + } + } + + self.containerNode.frame = targetAbsoluteRect.offsetBy(dx: -self.contextSourceNode.contentRect.minX, dy: -self.contextSourceNode.contentRect.minY) + self.contextSourceNode.updateAbsoluteRect?(self.containerNode.frame, UIScreen.main.bounds.size) + self.containerNode.layer.animatePosition(from: CGPoint(x: 0.0, y: sourceAbsoluteRect.midY - targetAbsoluteRect.midY), to: CGPoint(), duration: verticalDuration, delay: delay, mediaTimingFunction: ChatMessageTransitionNode.verticalAnimationCurve.mediaTimingFunction, additive: true, force: true, completion: { [weak self] _ in + guard let strongSelf = self else { + return + } + strongSelf.endAnimation() + }) + self.contextSourceNode.applyAbsoluteOffset?(CGPoint(x: sourceAbsoluteRect.midX - targetAbsoluteRect.midX, y: 0.0), ChatMessageTransitionNode.horizontalAnimationCurve, horizontalDuration) + self.contextSourceNode.applyAbsoluteOffset?(CGPoint(x: 0.0, y: sourceAbsoluteRect.midY - targetAbsoluteRect.midY), ChatMessageTransitionNode.verticalAnimationCurve, verticalDuration) + self.containerNode.layer.animatePosition(from: CGPoint(x: sourceAbsoluteRect.midX - targetAbsoluteRect.midX, y: 0.0), to: CGPoint(), duration: horizontalDuration, delay: delay, mediaTimingFunction: ChatMessageTransitionNode.horizontalAnimationCurve.mediaTimingFunction, additive: true) + + switch stickerMediaInput { + case .inputPanel: + break + case let .mediaPanel(sourceItemNode): + sourceItemNode.isHidden = true + case let .inputPanelSearch(sourceItemNode): + sourceItemNode.isHidden = true + case let .emptyPanel(sourceItemNode): + sourceItemNode.isHidden = true + } + case let .audioMicInput(audioMicInput): + if let (container, localRect) = audioMicInput.micButton.contentContainer { + let snapshotView = container.snapshotView(afterScreenUpdates: false) + if let snapshotView = snapshotView { + let sourceAbsoluteRect = container.convert(localRect, to: self.view) + snapshotView.frame = sourceAbsoluteRect + + container.isHidden = true + + let combinedTransition = CombinedTransition(horizontal: .animated(duration: horizontalDuration, curve: ChatMessageTransitionNode.horizontalAnimationCurve), vertical: .animated(duration: verticalDuration, curve: ChatMessageTransitionNode.verticalAnimationCurve)) + + if let itemNode = self.itemNode as? ChatMessageBubbleItemNode { + if let contextContainer = itemNode.animateFromMicInput(micInputNode: snapshotView, transition: combinedTransition) { + self.containerNode.addSubnode(contextContainer.contentNode) + + let targetAbsoluteRect = contextContainer.view.convert(contextContainer.contentRect, to: self.view) + + self.containerNode.frame = targetAbsoluteRect.offsetBy(dx: -contextContainer.contentRect.minX, dy: -contextContainer.contentRect.minY) + contextContainer.updateAbsoluteRect?(self.containerNode.frame, UIScreen.main.bounds.size) + self.containerNode.layer.animatePosition(from: CGPoint(x: 0.0, y: sourceAbsoluteRect.midY - targetAbsoluteRect.midY), to: CGPoint(), duration: verticalDuration, delay: delay, mediaTimingFunction: ChatMessageTransitionNode.verticalAnimationCurve.mediaTimingFunction, additive: true, force: true, completion: { [weak self, weak contextContainer, weak container] _ in + guard let strongSelf = self else { + return + } + if let contextContainer = contextContainer { + contextContainer.isExtractedToContextPreview = false + contextContainer.isExtractedToContextPreviewUpdated?(false) + contextContainer.addSubnode(contextContainer.contentNode) + } + + container?.isHidden = false + + strongSelf.endAnimation() + }) + + self.containerNode.layer.animatePosition(from: CGPoint(x: sourceAbsoluteRect.midX - targetAbsoluteRect.midX, y: 0.0), to: CGPoint(), duration: horizontalDuration, delay: delay, mediaTimingFunction: ChatMessageTransitionNode.horizontalAnimationCurve.mediaTimingFunction, additive: true) + } + } + } + } + case let .videoMessage(videoMessage): + let combinedTransition = CombinedTransition(horizontal: .animated(duration: horizontalDuration, curve: ChatMessageTransitionNode.horizontalAnimationCurve), vertical: .animated(duration: verticalDuration, curve: ChatMessageTransitionNode.verticalAnimationCurve)) + + if let itemNode = self.itemNode as? ChatMessageInstantVideoItemNode { + itemNode.cancelInsertionAnimations() + + self.contextSourceNode.isExtractedToContextPreview = true + self.contextSourceNode.isExtractedToContextPreviewUpdated?(true) + + self.containerNode.addSubnode(self.contextSourceNode.contentNode) + + let sourceAbsoluteRect = videoMessage.view.frame + let targetAbsoluteRect = self.contextSourceNode.view.convert(self.contextSourceNode.contentRect, to: self.view) + + videoMessage.view.frame = videoMessage.view.frame.offsetBy(dx: targetAbsoluteRect.midX - sourceAbsoluteRect.midX, dy: targetAbsoluteRect.midY - sourceAbsoluteRect.midY) + + self.containerNode.frame = targetAbsoluteRect.offsetBy(dx: -self.contextSourceNode.contentRect.minX, dy: -self.contextSourceNode.contentRect.minY) + self.containerNode.layer.animatePosition(from: CGPoint(x: 0.0, y: sourceAbsoluteRect.midY - targetAbsoluteRect.midY), to: CGPoint(), duration: horizontalDuration, delay: delay, mediaTimingFunction: ChatMessageTransitionNode.horizontalAnimationCurve.mediaTimingFunction, additive: true, force: true) + + self.containerNode.layer.animatePosition(from: CGPoint(x: sourceAbsoluteRect.midX - targetAbsoluteRect.midX, y: 0.0), to: CGPoint(), duration: verticalDuration, delay: delay, mediaTimingFunction: ChatMessageTransitionNode.verticalAnimationCurve.mediaTimingFunction, additive: true, completion: { [weak self] _ in + guard let strongSelf = self else { + return + } + + strongSelf.endAnimation() + }) + + itemNode.animateFromSnapshot(snapshotView: videoMessage.view, transition: combinedTransition) + } + case let .mediaInput(mediaInput): + if let snapshotView = mediaInput.extractSnapshot() { + if let itemNode = self.itemNode as? ChatMessageBubbleItemNode { + itemNode.cancelInsertionAnimations() + + self.contextSourceNode.isExtractedToContextPreview = true + self.contextSourceNode.isExtractedToContextPreviewUpdated?(true) + + self.containerNode.addSubnode(self.contextSourceNode.contentNode) + + let targetAbsoluteRect = self.contextSourceNode.view.convert(self.contextSourceNode.contentRect, to: self.view) + let sourceBackgroundAbsoluteRect = snapshotView.frame + let sourceAbsoluteRect = CGRect(origin: CGPoint(x: sourceBackgroundAbsoluteRect.midX - self.contextSourceNode.contentRect.size.width / 2.0, y: sourceBackgroundAbsoluteRect.midY - self.contextSourceNode.contentRect.size.height / 2.0), size: self.contextSourceNode.contentRect.size) + + let combinedTransition = CombinedTransition(horizontal: .animated(duration: horizontalDuration, curve: ChatMessageTransitionNode.horizontalAnimationCurve), vertical: .animated(duration: verticalDuration, curve: ChatMessageTransitionNode.verticalAnimationCurve)) + + if let itemNode = self.itemNode as? ChatMessageBubbleItemNode { + itemNode.animateContentFromMediaInput(snapshotView: snapshotView, transition: combinedTransition) + } + + self.containerNode.frame = targetAbsoluteRect.offsetBy(dx: -self.contextSourceNode.contentRect.minX, dy: -self.contextSourceNode.contentRect.minY) + + snapshotView.center = targetAbsoluteRect.center.offsetBy(dx: -self.containerNode.frame.minX, dy: -self.containerNode.frame.minY) + self.containerNode.view.addSubview(snapshotView) + + self.contextSourceNode.updateAbsoluteRect?(self.containerNode.frame, UIScreen.main.bounds.size) + + self.containerNode.layer.animatePosition(from: CGPoint(x: 0.0, y: sourceAbsoluteRect.midY - targetAbsoluteRect.midY), to: CGPoint(), duration: horizontalDuration, delay: delay, mediaTimingFunction: ChatMessageTransitionNode.horizontalAnimationCurve.mediaTimingFunction, additive: true, force: true) + self.containerNode.layer.animatePosition(from: CGPoint(x: sourceAbsoluteRect.midX - targetAbsoluteRect.midX, y: 0.0), to: CGPoint(), duration: verticalDuration, delay: delay, mediaTimingFunction: ChatMessageTransitionNode.verticalAnimationCurve.mediaTimingFunction, additive: true, force: true, completion: { [weak self] _ in + guard let strongSelf = self else { + return + } + strongSelf.endAnimation() + }) + + combinedTransition.horizontal.animateTransformScale(node: self.contextSourceNode.contentNode, from: CGPoint(x: sourceBackgroundAbsoluteRect.width / targetAbsoluteRect.width, y: sourceBackgroundAbsoluteRect.height / targetAbsoluteRect.height)) + + combinedTransition.horizontal.updateTransformScale(layer: snapshotView.layer, scale: CGPoint(x: 1.0 / (sourceBackgroundAbsoluteRect.width / targetAbsoluteRect.width), y: 1.0 / (sourceBackgroundAbsoluteRect.height / targetAbsoluteRect.height))) + + snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.12, removeOnCompletion: false, completion: { [weak snapshotView] _ in + snapshotView?.removeFromSuperview() + }) + + self.contextSourceNode.applyAbsoluteOffset?(CGPoint(x: sourceAbsoluteRect.minX - targetAbsoluteRect.minX, y: 0.0), ChatMessageTransitionNode.horizontalAnimationCurve, horizontalDuration) + self.contextSourceNode.applyAbsoluteOffset?(CGPoint(x: 0.0, y: sourceAbsoluteRect.maxY - targetAbsoluteRect.maxY), ChatMessageTransitionNode.verticalAnimationCurve, verticalDuration) + } + } + } + } + + private func endAnimation() { + self.contextSourceNode.isExtractedToContextPreview = false + self.contextSourceNode.isExtractedToContextPreviewUpdated?(false) + + self.animationEnded?() + } + + func addExternalOffset(offset: CGFloat, transition: ContainedViewLayoutTransition, itemNode: ListViewItemNode?) { + var applyOffset = false + if let itemNode = itemNode { + if itemNode === self.itemNode { + applyOffset = true + } + } else { + applyOffset = true + } + if applyOffset { + if transition.isAnimated { + assert(true) + } + self.scrollingContainer.bounds = self.scrollingContainer.bounds.offsetBy(dx: 0.0, dy: -offset) + transition.animateOffsetAdditive(node: self.scrollingContainer, offset: offset) + } + } + + func addContentOffset(offset: CGFloat, itemNode: ListViewItemNode?) { + var applyOffset = false + if let itemNode = itemNode { + if itemNode === self.itemNode { + applyOffset = true + } + } else { + applyOffset = true + } + if applyOffset { + self.scrollingContainer.bounds = self.scrollingContainer.bounds.offsetBy(dx: 0.0, dy: offset) + } + } + } + + private let listNode: ChatHistoryListNode + private let getContentAreaInScreenSpace: () -> CGRect + private let onTransitionEvent: (ContainedViewLayoutTransition) -> Void + + private var currentPendingItem: (Int64, Source, () -> Void)? + + private var animatingItemNodes: [AnimatingItemNode] = [] + + var hasScheduledTransitions: Bool { + return self.currentPendingItem != nil + } + + var hasOngoingTransitions: Bool { + return !self.animatingItemNodes.isEmpty + } + + init(listNode: ChatHistoryListNode, getContentAreaInScreenSpace: @escaping () -> CGRect, onTransitionEvent: @escaping (ContainedViewLayoutTransition) -> Void) { + self.listNode = listNode + self.getContentAreaInScreenSpace = getContentAreaInScreenSpace + self.onTransitionEvent = onTransitionEvent + + super.init() + + self.listNode.animationCorrelationMessageFound = { [weak self] itemNode, correlationId in + guard let strongSelf = self, let (currentId, currentSource, initiated) = strongSelf.currentPendingItem else { + return + } + if currentId == correlationId { + strongSelf.currentPendingItem = nil + strongSelf.beginAnimation(itemNode: itemNode, source: currentSource) + initiated() + } + } + } + + func add(correlationId: Int64, source: Source, initiated: @escaping () -> Void) { + self.currentPendingItem = (correlationId, source, initiated) + self.listNode.setCurrentSendAnimationCorrelationId(correlationId) + } + + private func beginAnimation(itemNode: ChatMessageItemView, source: Source) { + var contextSourceNode: ContextExtractedContentContainingNode? + if let itemNode = itemNode as? ChatMessageBubbleItemNode { + contextSourceNode = itemNode.mainContextSourceNode + } else if let itemNode = itemNode as? ChatMessageStickerItemNode { + contextSourceNode = itemNode.contextSourceNode + } else if let itemNode = itemNode as? ChatMessageAnimatedStickerItemNode { + contextSourceNode = itemNode.contextSourceNode + } else if let itemNode = itemNode as? ChatMessageInstantVideoItemNode { + contextSourceNode = itemNode.contextSourceNode + } + + if let contextSourceNode = contextSourceNode { + let animatingItemNode = AnimatingItemNode(itemNode: itemNode, contextSourceNode: contextSourceNode, source: source, getContentAreaInScreenSpace: self.getContentAreaInScreenSpace) + animatingItemNode.updateLayout(size: self.bounds.size) + + self.animatingItemNodes.append(animatingItemNode) + switch source { + case .audioMicInput, .videoMessage, .mediaInput: + let overlayController = OverlayTransitionContainerController() + overlayController.displayNode.addSubnode(animatingItemNode) + animatingItemNode.overlayController = overlayController + itemNode.item?.context.sharedContext.mainWindow?.presentInGlobalOverlay(overlayController) + default: + self.addSubnode(animatingItemNode) + } + + animatingItemNode.animationEnded = { [weak self, weak animatingItemNode] in + guard let strongSelf = self, let animatingItemNode = animatingItemNode else { + return + } + animatingItemNode.removeFromSupernode() + animatingItemNode.overlayController?.dismiss() + if let index = strongSelf.animatingItemNodes.firstIndex(where: { $0 === animatingItemNode }) { + strongSelf.animatingItemNodes.remove(at: index) + } + + if animatingItemNode.updateAfterCompletion, let item = animatingItemNode.itemNode.item { + for (message, _) in item.content { + strongSelf.listNode.requestMessageUpdate(stableId: message.stableId) + break + } + } + } + + animatingItemNode.frame = self.bounds + animatingItemNode.beginAnimation() + + self.onTransitionEvent(.animated(duration: ChatMessageTransitionNode.animationDuration, curve: ChatMessageTransitionNode.verticalAnimationCurve)) + } + } + + override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + return nil + } + + func addExternalOffset(offset: CGFloat, transition: ContainedViewLayoutTransition, itemNode: ListViewItemNode?) { + for animatingItemNode in self.animatingItemNodes { + animatingItemNode.addExternalOffset(offset: offset, transition: transition, itemNode: itemNode) + } + } + + func addContentOffset(offset: CGFloat, itemNode: ListViewItemNode?) { + for animatingItemNode in self.animatingItemNodes { + animatingItemNode.addContentOffset(offset: offset, itemNode: itemNode) + } + } + + func isAnimatingMessage(stableId: UInt32) -> Bool { + for itemNode in self.animatingItemNodes { + if let item = itemNode.itemNode.item { + for (message, _) in item.content { + if message.stableId == stableId { + return true + } + } + } + } + return false + } + + func scheduleUpdateMessageAfterAnimationCompleted(stableId: UInt32) { + for itemNode in self.animatingItemNodes { + if let item = itemNode.itemNode.item { + for (message, _) in item.content { + if message.stableId == stableId { + itemNode.updateAfterCompletion = true + } + } + } + } + } + + func hasScheduledUpdateMessageAfterAnimationCompleted(stableId: UInt32) -> Bool { + for itemNode in self.animatingItemNodes { + if let item = itemNode.itemNode.item { + for (message, _) in item.content { + if message.stableId == stableId { + return itemNode.updateAfterCompletion + } + } + } + } + return false + } +} diff --git a/submodules/TelegramUI/Sources/ChatMessageWebpageBubbleContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageWebpageBubbleContentNode.swift index 2fefe2b11c..e40eecd6e8 100644 --- a/submodules/TelegramUI/Sources/ChatMessageWebpageBubbleContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageWebpageBubbleContentNode.swift @@ -86,7 +86,7 @@ final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContentNode { 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 contentNodeLayout = self.contentNode.asyncLayout() - return { item, layoutConstants, _, _, constrainedSize in + return { item, layoutConstants, preparePosition, _, constrainedSize in var webPage: TelegramMediaWebpage? var webPageContent: TelegramMediaWebpageLoadedContent? for media in item.message.media { @@ -171,18 +171,18 @@ final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContentNode { mediaAndFlags = (webpage.image ?? file, [.preferMediaBeforeText]) } } else if webpage.type == "telegram_background" { - var topColor: UIColor? - var bottomColor: UIColor? + var colors: [UInt32] = [] 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) + var intensity: Int32? + if let wallpaper = parseWallpaperUrl(webpage.url), case let .slug(_, _, colorsValue, intensityValue, rotationValue) = wallpaper { + colors = colorsValue rotation = rotationValue + intensity = intensityValue } - let media = WallpaperPreviewMedia(content: .file(file, topColor, bottomColor, rotation, false, false)) + let media = WallpaperPreviewMedia(content: .file(file: file, colors: colors, rotation: rotation, intensity: intensity, false, false)) mediaAndFlags = (media, [.preferMediaAspectFilled]) if let fileSize = file.size { - badge = dataSizeString(fileSize, decimalSeparator: item.presentationData.dateTimeFormat.decimalSeparator) + badge = dataSizeString(fileSize, formatting: DataSizeStringFormatting(chatPresentationData: item.presentationData)) } } else { mediaAndFlags = (file, []) @@ -207,25 +207,24 @@ final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContentNode { } } else if let type = webpage.type { if type == "telegram_background" { - var topColor: UIColor? + var colors: [UInt32] = [] 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 + colors = [color.rgb] + } else if case let .gradient(colorsValue, rotationValue) = wallpaper { + colors = colorsValue rotation = rotationValue } } var content: WallpaperPreviewMediaContent? - if let topColor = topColor { - if let bottomColor = bottomColor { - content = .gradient(topColor, bottomColor, rotation) + if !colors.isEmpty { + if colors.count >= 2 { + content = .gradient(colors, rotation) } else { - content = .color(topColor) + content = .color(UIColor(rgb: colors[0])) } } if let content = content { @@ -254,7 +253,7 @@ final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContentNode { file = contentFile } if let file = file { - let media = WallpaperPreviewMedia(content: .file(file, nil, nil, nil, true, isSupported)) + let media = WallpaperPreviewMedia(content: .file(file: file, colors: [], rotation: nil, intensity: nil, true, isSupported)) mediaAndFlags = (media, ChatMessageAttachedContentNodeMediaFlags()) } else if let settings = settings { let media = WallpaperPreviewMedia(content: .themeSettings(settings)) @@ -301,7 +300,7 @@ final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContentNode { } } - let (initialWidth, continueLayout) = contentNodeLayout(item.presentationData, item.controllerInteraction.automaticMediaDownloadSettings, item.associatedData, item.attributes, item.context, item.controllerInteraction, item.message, item.read, item.chatLocation, 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, item.chatLocation, title, subtitle, text, entities, mediaAndFlags, badge, actionIcon, actionTitle, true, layoutConstants, preparePosition, constrainedSize) let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: false, headerSpacing: 8.0, hidesBackground: .never, forceFullCorners: false, forceAlignment: .none) diff --git a/submodules/TelegramUI/Sources/ChatPanelInterfaceInteraction.swift b/submodules/TelegramUI/Sources/ChatPanelInterfaceInteraction.swift index a3866669e2..34d50532be 100644 --- a/submodules/TelegramUI/Sources/ChatPanelInterfaceInteraction.swift +++ b/submodules/TelegramUI/Sources/ChatPanelInterfaceInteraction.swift @@ -50,14 +50,14 @@ enum ChatPanelRestrictionInfoDisplayType { } final class ChatPanelInterfaceInteraction { - let setupReplyMessage: (MessageId, @escaping (ContainedViewLayoutTransition) -> Void) -> Void + let setupReplyMessage: (MessageId?, @escaping (ContainedViewLayoutTransition) -> Void) -> Void let setupEditMessage: (MessageId?, @escaping (ContainedViewLayoutTransition) -> Void) -> Void let beginMessageSelection: ([MessageId], @escaping (ContainedViewLayoutTransition) -> Void) -> Void let deleteSelectedMessages: () -> Void let reportSelectedMessages: () -> Void - let reportMessages: ([Message], ContextController?) -> Void - let blockMessageAuthor: (Message, ContextController?) -> Void - let deleteMessages: ([Message], ContextController?, @escaping (ContextMenuActionResult) -> Void) -> Void + let reportMessages: ([Message], ContextControllerProtocol?) -> Void + let blockMessageAuthor: (Message, ContextControllerProtocol?) -> Void + let deleteMessages: ([Message], ContextControllerProtocol?, @escaping (ContextMenuActionResult) -> Void) -> Void let forwardSelectedMessages: () -> Void let forwardCurrentForwardMessages: () -> Void let forwardMessages: ([Message]) -> Void @@ -92,10 +92,10 @@ final class ChatPanelInterfaceInteraction { let displayVideoUnmuteTip: (CGPoint?) -> Void let switchMediaRecordingMode: () -> Void let setupMessageAutoremoveTimeout: () -> Void - let sendSticker: (FileMediaReference, ASDisplayNode, CGRect) -> Bool + let sendSticker: (FileMediaReference, Bool, ASDisplayNode, CGRect) -> Bool let unblockPeer: () -> Void - let pinMessage: (MessageId, ContextController?) -> Void - let unpinMessage: (MessageId, Bool, ContextController?) -> Void + let pinMessage: (MessageId, ContextControllerProtocol?) -> Void + let unpinMessage: (MessageId, Bool, ContextControllerProtocol?) -> Void let unpinAllMessages: () -> Void let openPinnedList: (MessageId) -> Void let shareAccountContact: () -> Void @@ -130,17 +130,18 @@ final class ChatPanelInterfaceInteraction { let joinGroupCall: (CachedChannelData.ActiveCall) -> Void let presentInviteMembers: () -> Void let presentGigagroupHelp: () -> Void + let updateShowCommands: ((Bool) -> Bool) -> Void let statuses: ChatPanelInterfaceInteractionStatuses? init( - setupReplyMessage: @escaping (MessageId, @escaping (ContainedViewLayoutTransition) -> Void) -> Void, + 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, - blockMessageAuthor: @escaping (Message, ContextController?) -> Void, - deleteMessages: @escaping ([Message], ContextController?, @escaping (ContextMenuActionResult) -> Void) -> Void, + reportMessages: @escaping ([Message], ContextControllerProtocol?) -> Void, + blockMessageAuthor: @escaping (Message, ContextControllerProtocol?) -> Void, + deleteMessages: @escaping ([Message], ContextControllerProtocol?, @escaping (ContextMenuActionResult) -> Void) -> Void, forwardSelectedMessages: @escaping () -> Void, forwardCurrentForwardMessages: @escaping () -> Void, forwardMessages: @escaping ([Message]) -> Void, @@ -175,10 +176,10 @@ final class ChatPanelInterfaceInteraction { displayVideoUnmuteTip: @escaping (CGPoint?) -> Void, switchMediaRecordingMode: @escaping () -> Void, setupMessageAutoremoveTimeout: @escaping () -> Void, - sendSticker: @escaping (FileMediaReference, ASDisplayNode, CGRect) -> Bool, + sendSticker: @escaping (FileMediaReference, Bool, ASDisplayNode, CGRect) -> Bool, unblockPeer: @escaping () -> Void, - pinMessage: @escaping (MessageId, ContextController?) -> Void, - unpinMessage: @escaping (MessageId, Bool, ContextController?) -> Void, + pinMessage: @escaping (MessageId, ContextControllerProtocol?) -> Void, + unpinMessage: @escaping (MessageId, Bool, ContextControllerProtocol?) -> Void, unpinAllMessages: @escaping () -> Void, openPinnedList: @escaping (MessageId) -> Void, shareAccountContact: @escaping () -> Void, @@ -213,6 +214,7 @@ final class ChatPanelInterfaceInteraction { presentInviteMembers: @escaping () -> Void, presentGigagroupHelp: @escaping () -> Void, editMessageMedia: @escaping (MessageId, Bool) -> Void, + updateShowCommands: @escaping ((Bool) -> Bool) -> Void, statuses: ChatPanelInterfaceInteractionStatuses? ) { self.setupReplyMessage = setupReplyMessage @@ -295,6 +297,7 @@ final class ChatPanelInterfaceInteraction { self.joinGroupCall = joinGroupCall self.presentInviteMembers = presentInviteMembers self.presentGigagroupHelp = presentGigagroupHelp + self.updateShowCommands = updateShowCommands self.statuses = statuses } } diff --git a/submodules/TelegramUI/Sources/ChatPinnedMessageTitlePanelNode.swift b/submodules/TelegramUI/Sources/ChatPinnedMessageTitlePanelNode.swift index ac47ca0360..dd30292821 100644 --- a/submodules/TelegramUI/Sources/ChatPinnedMessageTitlePanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatPinnedMessageTitlePanelNode.swift @@ -54,7 +54,7 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode { private let imageNode: TransformImageNode private let imageNodeContainer: ASDisplayNode - + private let separatorNode: ASDisplayNode private var currentLayout: (CGFloat, CGFloat, CGFloat)? @@ -182,7 +182,7 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode { private var theme: PresentationTheme? - override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) -> CGFloat { + override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) -> LayoutResult { let panelHeight: CGFloat = 50.0 var themeUpdated = false @@ -193,8 +193,7 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode { self.theme = interfaceState.theme self.closeButton.setImage(PresentationResourcesChat.chatInputPanelCloseIconImage(interfaceState.theme), for: []) self.listButton.setImage(PresentationResourcesChat.chatInputPanelPinnedListIconImage(interfaceState.theme), for: []) - self.backgroundColor = interfaceState.theme.chat.historyNavigation.fillColor - self.separatorNode.backgroundColor = interfaceState.theme.chat.historyNavigation.strokeColor + self.separatorNode.backgroundColor = interfaceState.theme.rootController.navigationBar.separatorColor } if self.statusDisposable == nil, let interfaceInteraction = self.interfaceInteraction, let statuses = interfaceInteraction.statuses { @@ -269,7 +268,7 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode { self.currentMessage = interfaceState.pinnedMessage if let currentMessage = self.currentMessage, let currentLayout = self.currentLayout { - self.enqueueTransition(width: currentLayout.0, panelHeight: panelHeight, leftInset: currentLayout.1, rightInset: currentLayout.2, transition: .immediate, animation: messageUpdatedAnimation, pinnedMessage: currentMessage, theme: interfaceState.theme, strings: interfaceState.strings, nameDisplayOrder: interfaceState.nameDisplayOrder, accountPeerId: self.context.account.peerId, firstTime: previousMessageWasNil, isReplyThread: isReplyThread) + self.enqueueTransition(width: currentLayout.0, panelHeight: panelHeight, leftInset: currentLayout.1, rightInset: currentLayout.2, transition: .immediate, animation: messageUpdatedAnimation, pinnedMessage: currentMessage, theme: interfaceState.theme, strings: interfaceState.strings, nameDisplayOrder: interfaceState.nameDisplayOrder, dateTimeFormat: interfaceState.dateTimeFormat, accountPeerId: self.context.account.peerId, firstTime: previousMessageWasNil, isReplyThread: isReplyThread) } } @@ -295,7 +294,7 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode { self.buttonsContainer.frame = CGRect(origin: CGPoint(x: width - buttonsContainerSize.width - rightInset, y: 0.0), size: buttonsContainerSize) let closeButtonSize = self.closeButton.measure(CGSize(width: 100.0, height: 100.0)) - transition.updateFrame(node: self.closeButton, frame: CGRect(origin: CGPoint(x: buttonsContainerSize.width - closeButtonSize.width, y: 19.0), size: closeButtonSize)) + transition.updateFrame(node: self.closeButton, frame: CGRect(origin: CGPoint(x: buttonsContainerSize.width - closeButtonSize.width + 1.0, y: 19.0), size: closeButtonSize)) let listButtonSize = self.listButton.measure(CGSize(width: 100.0, height: 100.0)) transition.updateFrame(node: self.listButton, frame: CGRect(origin: CGPoint(x: buttonsContainerSize.width - listButtonSize.width + 4.0, y: 13.0), size: listButtonSize)) @@ -304,7 +303,7 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode { transition.updateFrame(node: self.activityIndicatorContainer, frame: CGRect(origin: CGPoint(x: width - rightInset - indicatorSize.width + 5.0, y: 15.0), size: indicatorSize)) transition.updateFrame(node: self.activityIndicator, frame: CGRect(origin: CGPoint(), size: indicatorSize)) - transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: panelHeight - UIScreenPixel), size: CGSize(width: width, height: UIScreenPixel))) + transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: width, height: UIScreenPixel))) self.tapButton.frame = CGRect(origin: CGPoint(), size: CGSize(width: width - rightInset - closeButtonSize.width - 4.0, height: panelHeight)) self.clippingContainer.frame = CGRect(origin: CGPoint(), size: CGSize(width: width, height: panelHeight)) @@ -314,14 +313,14 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode { self.currentLayout = (width, leftInset, rightInset) if let currentMessage = self.currentMessage { - self.enqueueTransition(width: width, panelHeight: panelHeight, leftInset: leftInset, rightInset: rightInset, transition: .immediate, animation: .none, pinnedMessage: currentMessage, theme: interfaceState.theme, strings: interfaceState.strings, nameDisplayOrder: interfaceState.nameDisplayOrder, accountPeerId: interfaceState.accountPeerId, firstTime: true, isReplyThread: isReplyThread) + self.enqueueTransition(width: width, panelHeight: panelHeight, leftInset: leftInset, rightInset: rightInset, transition: .immediate, animation: .none, pinnedMessage: currentMessage, theme: interfaceState.theme, strings: interfaceState.strings, nameDisplayOrder: interfaceState.nameDisplayOrder, dateTimeFormat: interfaceState.dateTimeFormat, accountPeerId: interfaceState.accountPeerId, firstTime: true, isReplyThread: isReplyThread) } } - return panelHeight + return LayoutResult(backgroundHeight: panelHeight, insetHeight: panelHeight) } - private func enqueueTransition(width: CGFloat, panelHeight: CGFloat, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition, animation: PinnedMessageAnimation?, pinnedMessage: ChatPinnedMessage, theme: PresentationTheme, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, accountPeerId: PeerId, firstTime: Bool, isReplyThread: Bool) { + private func enqueueTransition(width: CGFloat, panelHeight: CGFloat, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition, animation: PinnedMessageAnimation?, pinnedMessage: ChatPinnedMessage, theme: PresentationTheme, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, dateTimeFormat: PresentationDateTimeFormat, accountPeerId: PeerId, firstTime: Bool, isReplyThread: Bool) { let message = pinnedMessage.message var animationTransition: ContainedViewLayoutTransition = .immediate @@ -470,7 +469,7 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode { } let (titleLayout, titleApply) = makeTitleLayout(CGSize(width: width - textLineInset - contentLeftInset - rightInset - textRightInset, height: CGFloat.greatestFiniteMagnitude), titleStrings) - 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))) + let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: foldLineBreaks(descriptionStringForMessage(contentSettings: context.currentContentSettings.with { $0 }, message: message, strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: dateTimeFormat, 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/Sources/ChatPresentationInterfaceState.swift b/submodules/TelegramUI/Sources/ChatPresentationInterfaceState.swift index 75d15bebdc..27475b3b98 100644 --- a/submodules/TelegramUI/Sources/ChatPresentationInterfaceState.swift +++ b/submodules/TelegramUI/Sources/ChatPresentationInterfaceState.swift @@ -352,6 +352,8 @@ final class ChatPresentationInterfaceState: Equatable { let hasActiveGroupCall: Bool let importState: ChatPresentationImportState? let reportReason: ReportReason? + let showCommands: Bool + let hasBotCommands: Bool init(chatWallpaper: TelegramWallpaper, theme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, limitsConfiguration: LimitsConfiguration, fontSize: PresentationFontSize, bubbleCorners: PresentationChatBubbleCorners, accountPeerId: PeerId, mode: ChatControllerPresentationMode, chatLocation: ChatLocation, subject: ChatControllerSubject?, peerNearbyData: ChatPeerNearbyData?, greetingData: ChatGreetingData?, pendingUnpinnedAllMessages: Bool, activeGroupCallInfo: ChatActiveGroupCallInfo?, hasActiveGroupCall: Bool, importState: ChatPresentationImportState?) { self.interfaceState = ChatInterfaceState() @@ -404,9 +406,11 @@ final class ChatPresentationInterfaceState: Equatable { self.hasActiveGroupCall = hasActiveGroupCall self.importState = importState self.reportReason = nil + self.showCommands = false + self.hasBotCommands = false } - 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: ChatPinnedMessage?, 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, autoremoveTimeout: Int32?, subject: ChatControllerSubject?, peerNearbyData: ChatPeerNearbyData?, greetingData: ChatGreetingData?, pendingUnpinnedAllMessages: Bool, activeGroupCallInfo: ChatActiveGroupCallInfo?, hasActiveGroupCall: Bool, importState: ChatPresentationImportState?, reportReason: ReportReason?) { + 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: ChatPinnedMessage?, 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, autoremoveTimeout: Int32?, subject: ChatControllerSubject?, peerNearbyData: ChatPeerNearbyData?, greetingData: ChatGreetingData?, pendingUnpinnedAllMessages: Bool, activeGroupCallInfo: ChatActiveGroupCallInfo?, hasActiveGroupCall: Bool, importState: ChatPresentationImportState?, reportReason: ReportReason?, showCommands: Bool, hasBotCommands: Bool) { self.interfaceState = interfaceState self.chatLocation = chatLocation self.renderedPeer = renderedPeer @@ -457,6 +461,8 @@ final class ChatPresentationInterfaceState: Equatable { self.hasActiveGroupCall = hasActiveGroupCall self.importState = importState self.reportReason = reportReason + self.showCommands = showCommands + self.hasBotCommands = hasBotCommands } static func ==(lhs: ChatPresentationInterfaceState, rhs: ChatPresentationInterfaceState) -> Bool { @@ -622,35 +628,41 @@ final class ChatPresentationInterfaceState: Equatable { if lhs.reportReason != rhs.reportReason { return false } + if lhs.showCommands != rhs.showCommands { + return false + } + if lhs.hasBotCommands != rhs.hasBotCommands { + return false + } return true } 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, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason) + 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, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands) } 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, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason) + 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, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands) } 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, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason) + 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, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands) } 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, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason) + 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, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands) } 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, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason) + 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, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands) } 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, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason) + 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, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands) } 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, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason) + 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, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands) } func updatedInputQueryResult(queryKind: ChatPresentationInputQueryKind, _ f: (ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?) -> ChatPresentationInterfaceState { @@ -662,143 +674,151 @@ final class ChatPresentationInterfaceState: Equatable { 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, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason) + 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, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands) } 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, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason) + 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, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands) } 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, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason) + 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, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands) } 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, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason) + 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, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands) } 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, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason) + 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, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands) } 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, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason) + 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, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands) } 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, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason) + 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, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands) } 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, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason) + 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, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands) } func updatedPinnedMessage(_ pinnedMessage: ChatPinnedMessage?) -> 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, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason) + 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, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands) } 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, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason) + 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, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands) } 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, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason) + 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, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands) } 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, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason) + 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, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands) } 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, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason) + 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, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands) } 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, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason) + 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, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands) } 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, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason) + 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, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands) } 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, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason) + 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, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands) } 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, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason) + 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, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands) } 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, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason) + 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, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands) } 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, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason) + 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, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands) } 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, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason) + 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, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands) } 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, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason) + 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, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands) } 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, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason) + 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, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands) } 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, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason) + 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, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands) } 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, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason) + 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, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands) } 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, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason) + 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, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands) } 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, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason) + 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, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands) } 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, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason) + 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, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands) } 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, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason) + 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, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands) } 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, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason) + 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, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands) } func updatedAutoremoveTimeout(_ autoremoveTimeout: Int32?) -> 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: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason) + 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: self.hasScheduledMessages, autoremoveTimeout: autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands) } func updatedPendingUnpinnedAllMessages(_ pendingUnpinnedAllMessages: 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, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason) + 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: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands) } func updatedActiveGroupCallInfo(_ activeGroupCallInfo: ChatActiveGroupCallInfo?) -> 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: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason) + 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: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands) } func updatedHasActiveGroupCall(_ hasActiveGroupCall: 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, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason) + 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: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands) } func updatedImportState(_ importState: ChatPresentationImportState?) -> 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: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: importState, reportReason: self.reportReason) + 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: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands) } func updatedReportReason(_ reportReason: ReportReason?) -> 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: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: reportReason) + 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: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands) + } + + func updatedShowCommands(_ showCommands: 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, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: showCommands, hasBotCommands: self.hasBotCommands) + } + + func updatedHasBotCommands(_ hasBotCommands: 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, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: hasBotCommands) } } diff --git a/submodules/TelegramUI/Sources/ChatRecentActionsController.swift b/submodules/TelegramUI/Sources/ChatRecentActionsController.swift index de27437a8d..b601fbe5de 100644 --- a/submodules/TelegramUI/Sources/ChatRecentActionsController.swift +++ b/submodules/TelegramUI/Sources/ChatRecentActionsController.swift @@ -98,7 +98,7 @@ final class ChatRecentActionsController: TelegramBaseController { }, displayVideoUnmuteTip: { _ in }, switchMediaRecordingMode: { }, setupMessageAutoremoveTimeout: { - }, sendSticker: { _, _, _ in + }, sendSticker: { _, _, _, _ in return false }, unblockPeer: { }, pinMessage: { _, _ in @@ -138,7 +138,7 @@ final class ChatRecentActionsController: TelegramBaseController { }, presentInviteMembers: { }, presentGigagroupHelp: { }, editMessageMedia: { _, _ in - }, statuses: nil) + }, updateShowCommands: { _ in }, statuses: nil) self.navigationItem.titleView = self.titleView @@ -204,7 +204,7 @@ final class ChatRecentActionsController: TelegramBaseController { childrenLayout.intrinsicInsets.bottom += 49.0 self.presentationContext.containerLayoutUpdated(childrenLayout, transition: transition) - self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationHeight, transition: transition) + self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition) } @objc func activateSearch() { diff --git a/submodules/TelegramUI/Sources/ChatRecentActionsControllerNode.swift b/submodules/TelegramUI/Sources/ChatRecentActionsControllerNode.swift index ef144d46f8..b24da72856 100644 --- a/submodules/TelegramUI/Sources/ChatRecentActionsControllerNode.swift +++ b/submodules/TelegramUI/Sources/ChatRecentActionsControllerNode.swift @@ -23,6 +23,7 @@ import PeerInfoUI import InviteLinksUI import UndoUI import TelegramCallsUI +import WallpaperBackgroundNode private final class ChatRecentActionsListOpaqueState { let entries: [ChatRecentActionsEntry] @@ -57,8 +58,8 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { private var state: ChatRecentActionsControllerState private var containerLayout: (ContainerViewLayout, CGFloat)? - private let backgroundNode: ASDisplayNode - private let panelBackgroundNode: ASDisplayNode + private let backgroundNode: WallpaperBackgroundNode + private let panelBackgroundNode: NavigationBackgroundNode private let panelSeparatorNode: ASDisplayNode private let panelButtonNode: HighlightableButtonNode @@ -98,18 +99,17 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { self.automaticMediaDownloadSettings = context.sharedContext.currentAutomaticMediaDownloadSettings.with { $0 } - self.backgroundNode = ASDisplayNode() - self.backgroundNode.isLayerBacked = true + self.backgroundNode = WallpaperBackgroundNode(context: context) + self.backgroundNode.isUserInteractionEnabled = false - self.panelBackgroundNode = ASDisplayNode() - self.panelBackgroundNode.backgroundColor = self.presentationData.theme.chat.inputPanel.panelBackgroundColor + self.panelBackgroundNode = NavigationBackgroundNode(color: self.presentationData.theme.chat.inputPanel.panelBackgroundColor) self.panelSeparatorNode = ASDisplayNode() self.panelSeparatorNode.backgroundColor = self.presentationData.theme.chat.inputPanel.panelSeparatorColor self.panelButtonNode = HighlightableButtonNode() self.panelButtonNode.setTitle(self.presentationData.strings.Channel_AdminLog_InfoPanelTitle, with: Font.regular(17.0), with: self.presentationData.theme.chat.inputPanel.panelControlAccentColor, for: []) self.listNode = ListView() - self.listNode.dynamicBounceEnabled = !self.presentationData.disableAnimations + self.listNode.dynamicBounceEnabled = false self.listNode.transform = CATransform3DMakeRotation(CGFloat(Double.pi), 0.0, 0.0, 1.0) self.listNode.accessibilityPageScrolledString = { row, count in return presentationData.strings.VoiceOver_ScrollStatus(row, count).0 @@ -121,13 +121,14 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { 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.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.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: true, 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) + self.eventLogContext = self.context.engine.peers.channelAdminEventLog(peerId: self.peer.id) super.init() - self.backgroundNode.contents = chatControllerBackgroundImage(theme: self.state.theme, wallpaper: self.state.chatWallpaper, mediaBox: context.sharedContext.accountManager.mediaBox, knockoutMode: context.sharedContext.immediateExperimentalUISettings.knockoutWallpaper)?.cgImage + self.backgroundNode.update(wallpaper: self.state.chatWallpaper) + self.backgroundNode.updateBubbleTheme(bubbleTheme: self.presentationData.theme, bubbleCorners: self.presentationData.chatBubbleCorners) self.addSubnode(self.backgroundNode) self.addSubnode(self.listNode) @@ -139,7 +140,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { self.panelButtonNode.addTarget(self, action: #selector(self.infoButtonPressed), forControlEvents: .touchUpInside) - let (adminsDisposable, _) = self.context.peerChannelMemberCategoriesContextsManager.admins(postbox: self.context.account.postbox, network: self.context.account.network, accountPeerId: context.account.peerId, peerId: self.peer.id, searchQuery: nil, updated: { [weak self] state in + let (adminsDisposable, _) = self.context.peerChannelMemberCategoriesContextsManager.admins(engine: self.context.engine, postbox: self.context.account.postbox, network: self.context.account.network, accountPeerId: context.account.peerId, peerId: self.peer.id, searchQuery: nil, updated: { [weak self] state in self?.adminsState = state }) self.adminsDisposable = adminsDisposable @@ -167,8 +168,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.InviteLink_ContextRevoke, color: .destructive, action: { [weak actionSheet] in actionSheet?.dismissAnimated() if let strongSelf = self { - let _ = (revokePeerExportedInvitation(account: strongSelf.context.account, peerId: peer.id, link: invite.link) - + let _ = (strongSelf.context.engine.peers.revokePeerExportedInvitation(peerId: peer.id, link: invite.link) |> deliverOnMainQueue).start(completed: { [weak self] in self?.eventLogContext.reload() }) @@ -260,9 +260,10 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { self?.openPeerMention(name) }, openMessageContextMenu: { [weak self] message, selectAll, node, frame, _ in self?.openMessageContextMenu(message: message, selectAll: selectAll, node: node, frame: frame) + }, activateMessagePinch: { _ in }, openMessageContextActions: { _, _, _, _ in }, navigateToMessage: { _, _ in }, navigateToMessageStandalone: { _ in - }, tapMessage: nil, clickThroughMessage: { }, toggleMessagesSelection: { _, _ in }, sendCurrentMessage: { _ in }, sendMessage: { _ in }, sendSticker: { _, _, _, _, _ in return false }, sendGif: { _, _, _ in return false }, sendBotContextResultAsGif: { _, _, _, _ in return false }, requestMessageActionCallback: { _, _, _, _ in }, requestMessageActionUrlAuth: { _, _ in }, activateSwitchInline: { _, _ in }, openUrl: { [weak self] url, _, _, _ in + }, tapMessage: nil, clickThroughMessage: { }, toggleMessagesSelection: { _, _ in }, sendCurrentMessage: { _ in }, sendMessage: { _ in }, sendSticker: { _, _, _, _, _, _, _ in return false }, sendGif: { _, _, _, _, _ in return false }, sendBotContextResultAsGif: { _, _, _, _, _ 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() { @@ -281,7 +282,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { } let resolveSignal: Signal if let peerName = peerName { - resolveSignal = resolvePeerByName(account: strongSelf.context.account, name: peerName) + resolveSignal = strongSelf.context.engine.peers.resolvePeerByName(name: peerName) |> mapToSignal { peerId -> Signal in if let peerId = peerId { return context.account.postbox.loadedPeerWithId(peerId) @@ -527,8 +528,6 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { }, displayPsa: { _, _ in }, displayDiceTooltip: { _ in }, animateDiceSuccess: { _ in - }, greetingStickerNode: { - return nil }, openPeerContextMenu: { _, _, _, _, _ in }, openMessageReplies: { _, _, _ in }, openReplyThreadOriginalMessage: { _ in @@ -536,10 +535,12 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { }, editMessageMedia: { _, _ in }, copyText: { _ in }, displayUndo: { _ in + }, isAnimatingMessage: { _ in + return false }, requestMessageUpdate: { _ in }, cancelInteractiveKeyboardGestures: { }, automaticMediaDownloadSettings: self.automaticMediaDownloadSettings, - pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(loopAnimatedStickers: false)) + pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(loopAnimatedStickers: false), presentationContext: ChatPresentationContext(backgroundNode: self.backgroundNode)) self.controllerInteraction = controllerInteraction self.listNode.displayedItemRangeChanged = { [weak self] displayedRange, opaqueTransactionState in @@ -611,7 +612,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.chatFontSize, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, disableAnimations: presentationData.disableAnimations, largeEmoji: presentationData.largeEmoji, chatBubbleCorners: presentationData.chatBubbleCorners))) + 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: true, largeEmoji: presentationData.largeEmoji, chatBubbleCorners: presentationData.chatBubbleCorners))) strongSelf.updateThemeAndStrings(theme: presentationData.theme, strings: presentationData.strings) } @@ -630,7 +631,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { } func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) { - self.panelBackgroundNode.backgroundColor = theme.chat.inputPanel.panelBackgroundColor + self.panelBackgroundNode.updateColor(color: theme.chat.inputPanel.panelBackgroundColor, transition: .immediate) self.panelSeparatorNode.backgroundColor = theme.chat.inputPanel.panelSeparatorColor self.panelButtonNode.setTitle(presentationData.strings.Channel_AdminLog_InfoPanelTitle, with: Font.regular(17.0), with: theme.chat.inputPanel.panelControlAccentColor, for: []) } @@ -646,10 +647,12 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { let cleanInsets = layout.insets(options: []) transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(), size: layout.size)) + self.backgroundNode.updateLayout(size: self.backgroundNode.bounds.size, transition: transition) let intrinsicPanelHeight: CGFloat = 47.0 let panelHeight = intrinsicPanelHeight + cleanInsets.bottom transition.updateFrame(node: self.panelBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - panelHeight), size: CGSize(width: layout.size.width, height: panelHeight))) + self.panelBackgroundNode.update(size: self.panelBackgroundNode.bounds.size, transition: transition) transition.updateFrame(node: self.panelSeparatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - panelHeight), size: CGSize(width: layout.size.width, height: UIScreenPixel))) transition.updateFrame(node: self.panelButtonNode, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - panelHeight), size: CGSize(width: layout.size.width, height: intrinsicPanelHeight))) @@ -785,7 +788,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { private func openPeerMention(_ name: String) { let postbox = self.context.account.postbox - self.navigationActionDisposable.set((resolvePeerByName(account: self.context.account, name: name, ageLimit: 10) + self.navigationActionDisposable.set((self.context.engine.peers.resolvePeerByName(name: name, ageLimit: 10) |> take(1) |> mapToSignal { peerId -> Signal in if let peerId = peerId { @@ -835,7 +838,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { if canBan { actions.append(ContextMenuAction(content: .text(title: self.presentationData.strings.Conversation_ContextMenuBan, accessibilityLabel: self.presentationData.strings.Conversation_ContextMenuBan), action: { [weak self] in if let strongSelf = self { - strongSelf.banDisposables.set((fetchChannelParticipant(account: strongSelf.context.account, peerId: strongSelf.peer.id, participantId: author.id) + strongSelf.banDisposables.set((strongSelf.context.engine.peers.fetchChannelParticipant(peerId: strongSelf.peer.id, participantId: author.id) |> deliverOnMainQueue).start(next: { participant in if let strongSelf = self { strongSelf.presentController(channelBannedMemberController(context: strongSelf.context, peerId: strongSelf.peer.id, memberId: author.id, initialParticipant: participant, updated: { _ in }, upgradedToSupergroup: { _, f in f() }), .window(.root), ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) @@ -880,7 +883,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { } private func openUrl(_ url: String) { - self.navigationActionDisposable.set((self.context.sharedContext.resolveUrl(account: self.context.account, url: url, skipUrlAuth: true) |> deliverOnMainQueue).start(next: { [weak self] result in + self.navigationActionDisposable.set((self.context.sharedContext.resolveUrl(context: self.context, peerId: nil, url: url, skipUrlAuth: true) |> deliverOnMainQueue).start(next: { [weak self] result in if let strongSelf = self { switch result { case let .externalUrl(url): @@ -901,13 +904,13 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { break case .groupBotStart: break - case let .channelMessage(peerId, messageId): + case let .channelMessage(peerId, messageId, timecode): if let navigationController = strongSelf.getNavigationController() { - strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(peerId), subject: .message(id: messageId, highlight: true))) + strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(peerId), subject: .message(id: messageId, highlight: true, timecode: timecode))) } case let .replyThreadMessage(replyThreadMessage, messageId): if let navigationController = strongSelf.getNavigationController() { - strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .replyThread(replyThreadMessage), subject: .message(id: messageId, highlight: true))) + strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .replyThread(replyThreadMessage), subject: .message(id: messageId, highlight: true, timecode: nil))) } case let .stickerPack(name): let packReference: StickerPackReference = .name(name) @@ -948,8 +951,9 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { break case let .joinVoiceChat(peerId, invite): strongSelf.presentController(VoiceChatJoinScreen(context: strongSelf.context, peerId: peerId, invite: invite, join: { call in - }), .window(.root), nil) + case .importStickers: + break } } })) diff --git a/submodules/TelegramUI/Sources/ChatRecentActionsFilterController.swift b/submodules/TelegramUI/Sources/ChatRecentActionsFilterController.swift index d4ee155c36..c667bf02b1 100644 --- a/submodules/TelegramUI/Sources/ChatRecentActionsFilterController.swift +++ b/submodules/TelegramUI/Sources/ChatRecentActionsFilterController.swift @@ -449,7 +449,7 @@ public func channelRecentActionsFilterController(context: AccountContext, peer: adminsPromise.set(.single(nil)) - let (membersDisposable, _) = context.peerChannelMemberCategoriesContextsManager.admins(postbox: context.account.postbox, network: context.account.network, accountPeerId: context.account.peerId, peerId: peer.id) { membersState in + let (membersDisposable, _) = context.peerChannelMemberCategoriesContextsManager.admins(engine: context.engine, postbox: context.account.postbox, network: context.account.network, accountPeerId: context.account.peerId, peerId: peer.id) { membersState in if case .loading = membersState.loadingState, membersState.list.isEmpty { adminsPromise.set(.single(nil)) } else { diff --git a/submodules/TelegramUI/Sources/ChatRecentActionsHistoryTransition.swift b/submodules/TelegramUI/Sources/ChatRecentActionsHistoryTransition.swift index 1197ebff43..5cc933f1a5 100644 --- a/submodules/TelegramUI/Sources/ChatRecentActionsHistoryTransition.swift +++ b/submodules/TelegramUI/Sources/ChatRecentActionsHistoryTransition.swift @@ -286,7 +286,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { case .header: var peers = SimpleDictionary() var author: Peer? - if self.entry.event.peerId == PeerId(namespace: Namespaces.Peer.CloudUser, id: 136817688) { + if self.entry.event.peerId == PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt32Value(136817688)) { author = message?.effectiveAuthor } else if let peer = self.entry.peers[self.entry.event.peerId] { author = peer diff --git a/submodules/TelegramUI/Sources/ChatRecentActionsSearchNavigationContentNode.swift b/submodules/TelegramUI/Sources/ChatRecentActionsSearchNavigationContentNode.swift index e591d1c031..b052b828fc 100644 --- a/submodules/TelegramUI/Sources/ChatRecentActionsSearchNavigationContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatRecentActionsSearchNavigationContentNode.swift @@ -26,7 +26,7 @@ final class ChatRecentActionsSearchNavigationContentNode: NavigationBarContentNo self.cancel = cancel - self.searchBar = SearchBarNode(theme: SearchBarNodeTheme(theme: theme, hasSeparator: false), strings: strings, fieldStyle: .modern) + self.searchBar = SearchBarNode(theme: SearchBarNodeTheme(theme: theme, hasSeparator: false), strings: strings, fieldStyle: .modern, displayBackground: false) let placeholderText = strings.Common_Search self.searchBar.placeholderString = NSAttributedString(string: placeholderText, font: searchBarFont, textColor: theme.rootController.navigationSearchBar.inputPlaceholderTextColor) diff --git a/submodules/TelegramUI/Sources/ChatRecordingPreviewInputPanelNode.swift b/submodules/TelegramUI/Sources/ChatRecordingPreviewInputPanelNode.swift index b00eec3f7c..5be394c3cd 100644 --- a/submodules/TelegramUI/Sources/ChatRecordingPreviewInputPanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatRecordingPreviewInputPanelNode.swift @@ -11,14 +11,7 @@ import UniversalMediaPlayer import AppBundle import ContextUI import AnimationUI - -private func generatePauseIcon(_ theme: PresentationTheme) -> UIImage? { - return generateTintedImage(image: UIImage(bundleImageName: "GlobalMusicPlayer/MinimizedPause"), color: theme.chat.inputPanel.actionControlForegroundColor) -} - -private func generatePlayIcon(_ theme: PresentationTheme) -> UIImage? { - return generateTintedImage(image: UIImage(bundleImageName: "GlobalMusicPlayer/MinimizedPlay"), color: theme.chat.inputPanel.actionControlForegroundColor) -} +import ManagedAnimationNode extension AudioWaveformNode: CustomMediaPlayerScrubbingForegroundNode { @@ -30,7 +23,7 @@ final class ChatRecordingPreviewInputPanelNode: ChatInputPanelNode { let sendButton: HighlightTrackingButtonNode private var sendButtonRadialStatusNode: ChatSendButtonRadialStatusNode? let playButton: HighlightableButtonNode - let pauseButton: HighlightableButtonNode + private let playPauseIconNode: PlayPauseIconNode private let waveformButton: ASButtonNode let waveformBackgroundNode: ASImageNode @@ -76,13 +69,10 @@ final class ChatRecordingPreviewInputPanelNode: ChatInputPanelNode { self.playButton = HighlightableButtonNode() self.playButton.displaysAsynchronously = false - self.playButton.setImage(generatePlayIcon(theme), for: []) - self.playButton.isUserInteractionEnabled = false - self.pauseButton = HighlightableButtonNode() - self.pauseButton.displaysAsynchronously = false - self.pauseButton.setImage(generatePauseIcon(theme), for: []) - self.pauseButton.isHidden = true - self.pauseButton.isUserInteractionEnabled = false + + self.playPauseIconNode = PlayPauseIconNode() + self.playPauseIconNode.enqueueState(.play, animated: false) + self.playPauseIconNode.customColor = theme.chat.inputPanel.actionControlForegroundColor self.waveformButton = ASButtonNode() self.waveformButton.accessibilityTraits.insert(.startsMediaSession) @@ -106,9 +96,9 @@ final class ChatRecordingPreviewInputPanelNode: ChatInputPanelNode { self.addSubnode(self.sendButton) self.addSubnode(self.waveformScubberNode) self.addSubnode(self.playButton) - self.addSubnode(self.pauseButton) self.addSubnode(self.durationLabel) self.addSubnode(self.waveformButton) + self.playButton.addSubnode(self.playPauseIconNode) self.sendButton.highligthedChanged = { [weak self] highlighted in if let strongSelf = self { @@ -168,6 +158,9 @@ final class ChatRecordingPreviewInputPanelNode: ChatInputPanelNode { if let context = self.context { let mediaManager = context.sharedContext.mediaManager let mediaPlayer = MediaPlayer(audioSessionManager: mediaManager.audioSession, postbox: context.account.postbox, resourceReference: .standalone(resource: recordedMediaPreview.resource), streamable: .none, video: false, preferSoftwareDecoding: false, enableSound: true, fetchAutomatically: true) + mediaPlayer.actionAtEnd = .action{ [weak mediaPlayer] in + mediaPlayer?.seek(timestamp: 0.0) + } self.mediaPlayer = mediaPlayer self.durationLabel.defaultDuration = Double(recordedMediaPreview.duration) self.durationLabel.status = mediaPlayer.status @@ -177,11 +170,10 @@ final class ChatRecordingPreviewInputPanelNode: ChatInputPanelNode { if let strongSelf = self { switch status.status { case .playing, .buffering(_, true, _, _): - strongSelf.playButton.isHidden = true + strongSelf.playPauseIconNode.enqueueState(.pause, animated: true) default: - strongSelf.playButton.isHidden = false + strongSelf.playPauseIconNode.enqueueState(.play, animated: true) } - strongSelf.pauseButton.isHidden = !strongSelf.playButton.isHidden } })) } @@ -223,7 +215,8 @@ final class ChatRecordingPreviewInputPanelNode: ChatInputPanelNode { } transition.updateFrame(node: self.playButton, frame: CGRect(origin: CGPoint(x: leftInset + 52.0, y: 10.0), size: CGSize(width: 26.0, height: 26.0))) - transition.updateFrame(node: self.pauseButton, frame: CGRect(origin: CGPoint(x: leftInset + 50.0, y: 10.0), size: CGSize(width: 26.0, height: 26.0))) + self.playPauseIconNode.frame = CGRect(origin: CGPoint(x: -2.0, y: -1.0), size: CGSize(width: 26.0, height: 26.0)) + let waveformBackgroundFrame = CGRect(origin: CGPoint(x: leftInset + 45.0, y: 7.0 - UIScreenPixel), size: CGSize(width: width - leftInset - rightInset - 90.0, height: 33.0)) transition.updateFrame(node: self.waveformBackgroundNode, frame: waveformBackgroundFrame) transition.updateFrame(node: self.waveformButton, frame: CGRect(origin: CGPoint(x: leftInset + 45.0, y: 0.0), size: CGSize(width: width - leftInset - rightInset - 90.0, height: panelHeight))) @@ -231,7 +224,7 @@ final class ChatRecordingPreviewInputPanelNode: ChatInputPanelNode { transition.updateFrame(node: self.durationLabel, frame: CGRect(origin: CGPoint(x: width - rightInset - 90.0 - 4.0, y: 15.0), size: CGSize(width: 35.0, height: 20.0))) prevInputPanelNode?.frame = CGRect(origin: .zero, size: CGSize(width: width, height: panelHeight)) - if let prevTextInputPanelNode = prevInputPanelNode as? ChatTextInputPanelNode { + if let prevTextInputPanelNode = self.prevInputPanelNode as? ChatTextInputPanelNode { self.prevInputPanelNode = nil if let audioRecordingDotNode = prevTextInputPanelNode.audioRecordingDotNode { @@ -259,11 +252,8 @@ final class ChatRecordingPreviewInputPanelNode: ChatInputPanelNode { self.playButton.layer.animateScale(from: 0.01, to: 1.0, duration: 0.3, delay: 0.1) self.playButton.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, delay: 0.1) - - self.pauseButton.layer.animateScale(from: 0.01, to: 1.0, duration: 0.3, delay: 0.1) - self.pauseButton.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, delay: 0.1) - - self.durationLabel.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + + self.durationLabel.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3, delay: 0.1) self.waveformScubberNode.layer.animateScaleY(from: 0.1, to: 1.0, duration: 0.3, delay: 0.1) self.waveformScubberNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, delay: 0.1) @@ -312,3 +302,52 @@ final class ChatRecordingPreviewInputPanelNode: ChatInputPanelNode { } } +private enum PlayPauseIconNodeState: Equatable { + case play + case pause +} + +private final class PlayPauseIconNode: ManagedAnimationNode { + private let duration: Double = 0.35 + private var iconState: PlayPauseIconNodeState = .pause + + init() { + super.init(size: CGSize(width: 28.0, height: 28.0)) + + self.trackTo(item: ManagedAnimationItem(source: .local("anim_playpause"), frames: .range(startFrame: 41, endFrame: 41), duration: 0.01)) + } + + func enqueueState(_ state: PlayPauseIconNodeState, animated: Bool) { + guard self.iconState != state else { + return + } + + let previousState = self.iconState + self.iconState = state + + switch previousState { + case .pause: + switch state { + case .play: + if animated { + self.trackTo(item: ManagedAnimationItem(source: .local("anim_playpause"), frames: .range(startFrame: 41, endFrame: 83), duration: self.duration)) + } else { + self.trackTo(item: ManagedAnimationItem(source: .local("anim_playpause"), frames: .range(startFrame: 0, endFrame: 0), duration: 0.01)) + } + case .pause: + break + } + case .play: + switch state { + case .pause: + if animated { + self.trackTo(item: ManagedAnimationItem(source: .local("anim_playpause"), frames: .range(startFrame: 0, endFrame: 41), duration: self.duration)) + } else { + self.trackTo(item: ManagedAnimationItem(source: .local("anim_playpause"), frames: .range(startFrame: 41, endFrame: 41), duration: 0.01)) + } + case .play: + break + } + } + } +} diff --git a/submodules/TelegramUI/Sources/ChatReplyCountItem.swift b/submodules/TelegramUI/Sources/ChatReplyCountItem.swift index 5d99ecb98a..36aee95303 100644 --- a/submodules/TelegramUI/Sources/ChatReplyCountItem.swift +++ b/submodules/TelegramUI/Sources/ChatReplyCountItem.swift @@ -6,6 +6,7 @@ import Display import SwiftSignalKit import TelegramPresentationData import AccountContext +import WallpaperBackgroundNode private let titleFont = UIFont.systemFont(ofSize: 13.0) @@ -15,20 +16,22 @@ class ChatReplyCountItem: ListViewItem { let count: Int let presentationData: ChatPresentationData let header: ChatMessageDateHeader + let controllerInteraction: ChatControllerInteraction - init(index: MessageIndex, isComments: Bool, count: Int, presentationData: ChatPresentationData, context: AccountContext) { + init(index: MessageIndex, isComments: Bool, count: Int, presentationData: ChatPresentationData, context: AccountContext, controllerInteraction: ChatControllerInteraction) { self.index = index self.isComments = isComments self.count = count self.presentationData = presentationData self.header = ChatMessageDateHeader(timestamp: index.timestamp, scheduled: false, presentationData: presentationData, context: context) + self.controllerInteraction = controllerInteraction } 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 = ChatReplyCountItemNode() - node.layoutForParams(params, item: self, previousItem: previousItem, nextItem: nextItem) Queue.mainQueue().async { + node.layoutForParams(params, item: self, previousItem: previousItem, nextItem: nextItem) completion(node, { return (nil, { _ in }) }) @@ -60,22 +63,24 @@ class ChatReplyCountItem: ListViewItem { class ChatReplyCountItemNode: ListViewItemNode { var item: ChatReplyCountItem? - let labelNode: TextNode - let filledBackgroundNode: LinkHighlightingNode + private let labelNode: TextNode + private var backgroundNode: WallpaperBackgroundNode.BubbleBackgroundNode? + private let backgroundColorNode: ASDisplayNode private var theme: ChatPresentationThemeData? private let layoutConstants = ChatMessageItemLayoutConstants.default + + private var absoluteRect: (CGRect, CGSize)? init() { self.labelNode = TextNode() self.labelNode.isUserInteractionEnabled = false - - self.filledBackgroundNode = LinkHighlightingNode(color: .clear) + + self.backgroundColorNode = ASDisplayNode() super.init(layerBacked: false, dynamicBounce: true, rotated: true) - - self.addSubnode(self.filledBackgroundNode) + self.addSubnode(self.labelNode) self.transform = CATransform3DMakeRotation(CGFloat.pi, 0.0, 0.0, 1.0) @@ -106,7 +111,6 @@ class ChatReplyCountItemNode: ListViewItemNode { func asyncLayout() -> (_ item: ChatReplyCountItem, _ params: ListViewItemLayoutParams, _ dateAtBottom: Bool) -> (ListViewItemNodeLayout, () -> Void) { let makeLabelLayout = TextNode.asyncLayout(self.labelNode) - let backgroundLayout = self.filledBackgroundNode.asyncLayout() let layoutConstants = self.layoutConstants @@ -145,9 +149,6 @@ class ChatReplyCountItemNode: ListViewItemNode { labelRects[i].origin.x = floor((labelLayout.size.width - labelRects[i].width) / 2.0) } - let serviceColor = serviceMessageColorComponents(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper) - let backgroundApply = backgroundLayout(serviceColor.fill, labelRects, 10.0, 10.0, 0.0) - let backgroundSize = CGSize(width: labelLayout.size.width + 8.0 + 8.0, height: labelLayout.size.height + 4.0) return (ListViewItemNodeLayout(contentSize: CGSize(width: params.width, height: backgroundSize.height), insets: UIEdgeInsets(top: 6.0 + (dateAtBottom ? layoutConstants.timestampHeaderHeight : 0.0), left: 0.0, bottom: 5.0, right: 0.0)), { [weak self] in @@ -156,19 +157,63 @@ class ChatReplyCountItemNode: ListViewItemNode { strongSelf.theme = item.presentationData.theme let _ = apply() - let _ = backgroundApply() + + if strongSelf.backgroundNode == nil { + if let backgroundNode = item.controllerInteraction.presentationContext.backgroundNode?.makeBubbleBackground(for: .free) { + strongSelf.backgroundNode = backgroundNode + backgroundNode.addSubnode(strongSelf.backgroundColorNode) + strongSelf.insertSubnode(backgroundNode, at: 0) + } + } let labelFrame = CGRect(origin: CGPoint(x: floor((params.width - backgroundSize.width) / 2.0) + 8.0, y: floorToScreenPixels((backgroundSize.height - labelLayout.size.height) / 2.0) - 1.0), size: labelLayout.size) strongSelf.labelNode.frame = labelFrame - strongSelf.filledBackgroundNode.frame = labelFrame.offsetBy(dx: 0.0, dy: -11.0) + + strongSelf.backgroundColorNode.backgroundColor = selectDateFillStaticColor(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper) + + let baseBackgroundFrame = CGRect(origin: CGPoint(x: labelFrame.minX - 6.0, y: labelFrame.minY - 2.0), size: CGSize(width: labelFrame.width + 6.0 * 2.0, height: labelFrame.height + 2.0 * 2.0)) + + if let backgroundNode = strongSelf.backgroundNode { + backgroundNode.frame = baseBackgroundFrame + + backgroundNode.clipsToBounds = true + backgroundNode.cornerRadius = baseBackgroundFrame.height / 2.0 + + if let (rect, size) = strongSelf.absoluteRect { + strongSelf.updateAbsoluteRect(rect, within: size) + } + } + + strongSelf.backgroundColorNode.frame = CGRect(origin: CGPoint(), size: baseBackgroundFrame.size) } }) } } + + override func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) { + var rect = rect + rect.origin.y = containerSize.height - rect.maxY + self.insets.top + + self.absoluteRect = (rect, containerSize) + + if let backgroundNode = self.backgroundNode { + var backgroundFrame = backgroundNode.frame + backgroundFrame.origin.x += rect.minX + backgroundFrame.origin.y += rect.minY + + backgroundNode.update(rect: backgroundFrame, within: containerSize) + } + } + + override func applyAbsoluteOffset(value: CGPoint, animationCurve: ContainedViewLayoutTransitionCurve, duration: Double) { + if let backgroundNode = self.backgroundNode { + backgroundNode.offset(value: CGPoint(x: value.x, y: -value.y), animationCurve: animationCurve, duration: duration) + } + } - override public func header() -> ListViewItemHeader? { + override public func headers() -> [ListViewItemHeader]? { if let item = self.item { - return item.header + return [item.header] } else { return nil } diff --git a/submodules/TelegramUI/Sources/ChatReportPeerTitlePanelNode.swift b/submodules/TelegramUI/Sources/ChatReportPeerTitlePanelNode.swift index 38e200a72b..e9aa05efae 100644 --- a/submodules/TelegramUI/Sources/ChatReportPeerTitlePanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatReportPeerTitlePanelNode.swift @@ -106,21 +106,19 @@ private final class ChatInfoTitlePanelInviteInfoNode: ASDisplayNode { private var theme: PresentationTheme? private let labelNode: ImmediateTextNode - private let filledBackgroundFillNode: LinkHighlightingNode - private let filledBackgroundNode: LinkHighlightingNode + + private let backgroundNode: NavigationBackgroundNode init(openInvitePeer: @escaping () -> Void) { self.labelNode = ImmediateTextNode() self.labelNode.maximumNumberOfLines = 1 self.labelNode.textAlignment = .center - - self.filledBackgroundFillNode = LinkHighlightingNode(color: .clear) - self.filledBackgroundNode = LinkHighlightingNode(color: .clear) + + self.backgroundNode = NavigationBackgroundNode(color: .clear) super.init() - self.addSubnode(self.filledBackgroundFillNode) - self.addSubnode(self.filledBackgroundNode) + self.addSubnode(self.backgroundNode) self.addSubnode(self.labelNode) self.labelNode.highlightAttributeAction = { attributes in @@ -191,19 +189,15 @@ private final class ChatInfoTitlePanelInviteInfoNode: ASDisplayNode { labelRects[i].origin.x = floor((labelLayout.size.width - labelRects[i].width) / 2.0) } - let backgroundLayout = self.filledBackgroundNode.asyncLayout() - let backgroundFillLayout = self.filledBackgroundFillNode.asyncLayout() - let backgroundApply = backgroundLayout(theme.chat.serviceMessage.components.withDefaultWallpaper.dateFillStatic, labelRects, 10.0, 10.0, 0.0) - let backgroundFillApply = backgroundFillLayout(theme.chat.serviceMessage.components.withDefaultWallpaper.dateFillFloating, labelRects, 10.0, 10.0, 0.0) - backgroundApply() - backgroundFillApply() - let backgroundSize = CGSize(width: labelLayout.size.width + 8.0 + 8.0, height: labelLayout.size.height + 4.0) let labelFrame = CGRect(origin: CGPoint(x: floor((width - labelLayout.size.width) / 2.0), y: topInset + floorToScreenPixels((backgroundSize.height - labelLayout.size.height) / 2.0) - 1.0), size: labelLayout.size) self.labelNode.frame = labelFrame - self.filledBackgroundNode.frame = labelFrame.offsetBy(dx: 0.0, dy: -11.0) - self.filledBackgroundFillNode.frame = labelFrame.offsetBy(dx: 0.0, dy: -11.0) + + let backgroundFrame = labelFrame.offsetBy(dx: 0.0, dy: 1.0).insetBy(dx: -5.0, dy: -2.0) + self.backgroundNode.updateColor(color: selectDateFillStaticColor(theme: theme, wallpaper: wallpaper), enableBlur: dateFillNeedsBlur(theme: theme, wallpaper: wallpaper), transition: .immediate) + transition.updateFrame(node: self.backgroundNode, frame: backgroundFrame) + self.backgroundNode.update(size: self.backgroundNode.bounds.size, cornerRadius: self.backgroundNode.bounds.size.height / 2.0, transition: transition) return topInset + backgroundSize.height + bottomInset } @@ -305,7 +299,6 @@ private final class ChatInfoTitlePanelPeerNearbyInfoNode: ASDisplayNode { } final class ChatReportPeerTitlePanelNode: ChatTitleAccessoryPanelNode { - private let backgroundNode: ASDisplayNode private let separatorNode: ASDisplayNode private let closeButton: HighlightableButtonNode @@ -317,8 +310,6 @@ final class ChatReportPeerTitlePanelNode: ChatTitleAccessoryPanelNode { private var peerNearbyInfoNode: ChatInfoTitlePanelPeerNearbyInfoNode? override init() { - self.backgroundNode = ASDisplayNode() - self.backgroundNode.isLayerBacked = true self.separatorNode = ASDisplayNode() self.separatorNode.isLayerBacked = true @@ -327,23 +318,21 @@ final class ChatReportPeerTitlePanelNode: ChatTitleAccessoryPanelNode { self.closeButton.displaysAsynchronously = false super.init() - - self.addSubnode(self.backgroundNode) + self.addSubnode(self.separatorNode) self.closeButton.addTarget(self, action: #selector(self.closePressed), forControlEvents: [.touchUpInside]) self.addSubnode(self.closeButton) } - override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) -> CGFloat { + override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) -> LayoutResult { if interfaceState.theme !== self.theme { self.theme = interfaceState.theme self.closeButton.setImage(PresentationResourcesChat.chatInputPanelEncircledCloseIconImage(interfaceState.theme), for: []) - self.backgroundNode.backgroundColor = interfaceState.theme.chat.historyNavigation.fillColor - self.separatorNode.backgroundColor = interfaceState.theme.chat.historyNavigation.strokeColor + self.separatorNode.backgroundColor = interfaceState.theme.rootController.navigationBar.separatorColor } - + var panelHeight: CGFloat = 40.0 let contentRightInset: CGFloat = 14.0 + rightInset @@ -437,10 +426,9 @@ final class ChatReportPeerTitlePanelNode: ChatTitleAccessoryPanelNode { } } } - - transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: width, height: panelHeight))) - - transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: panelHeight - UIScreenPixel), size: CGSize(width: width, height: UIScreenPixel))) + + let initialPanelHeight = panelHeight + transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: width, height: UIScreenPixel))) var chatPeer: Peer? if let renderedPeer = interfaceState.renderedPeer { @@ -502,7 +490,7 @@ final class ChatReportPeerTitlePanelNode: ChatTitleAccessoryPanelNode { }) } - return panelHeight + return LayoutResult(backgroundHeight: initialPanelHeight, insetHeight: panelHeight) } @objc func buttonPressed(_ view: UIButton) { @@ -535,6 +523,11 @@ final class ChatReportPeerTitlePanelNode: ChatTitleAccessoryPanelNode { if let result = self.closeButton.hitTest(CGPoint(x: point.x - self.closeButton.frame.minX, y: point.y - self.closeButton.frame.minY), with: event) { return result } + if let inviteInfoNode = self.inviteInfoNode { + if let result = inviteInfoNode.view.hitTest(self.view.convert(point, to: inviteInfoNode.view), with: event) { + return result + } + } return super.hitTest(point, with: event) } } diff --git a/submodules/TelegramUI/Sources/ChatRequestInProgressTitlePanelNode.swift b/submodules/TelegramUI/Sources/ChatRequestInProgressTitlePanelNode.swift index 8d3795cad8..f7e0421690 100644 --- a/submodules/TelegramUI/Sources/ChatRequestInProgressTitlePanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatRequestInProgressTitlePanelNode.swift @@ -19,12 +19,12 @@ final class ChatRequestInProgressTitlePanelNode: ChatTitleAccessoryPanelNode { self.titleNode.maximumNumberOfLines = 1 super.init() - + self.addSubnode(self.titleNode) self.addSubnode(self.separatorNode) } - override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) -> CGFloat { + override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) -> LayoutResult { if interfaceState.strings !== self.strings { self.strings = interfaceState.strings @@ -34,8 +34,8 @@ final class ChatRequestInProgressTitlePanelNode: ChatTitleAccessoryPanelNode { if interfaceState.theme !== self.theme { self.theme = interfaceState.theme - self.backgroundColor = interfaceState.theme.chat.historyNavigation.fillColor - self.separatorNode.backgroundColor = interfaceState.theme.chat.historyNavigation.strokeColor + + self.separatorNode.backgroundColor = interfaceState.theme.rootController.navigationBar.separatorColor } let panelHeight: CGFloat = 40.0 @@ -43,8 +43,8 @@ final class ChatRequestInProgressTitlePanelNode: ChatTitleAccessoryPanelNode { let titleSize = self.titleNode.updateLayout(CGSize(width: width - leftInset - rightInset, height: 100.0)) transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: floor((width - titleSize.width) / 2.0), y: floor((panelHeight - titleSize.height) / 2.0)), size: titleSize)) - transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: panelHeight - UIScreenPixel), size: CGSize(width: width, height: UIScreenPixel))) + transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: width, height: UIScreenPixel))) - return panelHeight + return LayoutResult(backgroundHeight: panelHeight, insetHeight: panelHeight) } } diff --git a/submodules/TelegramUI/Sources/ChatScheduleTimeController.swift b/submodules/TelegramUI/Sources/ChatScheduleTimeController.swift index c310880aa2..f60cd888f2 100644 --- a/submodules/TelegramUI/Sources/ChatScheduleTimeController.swift +++ b/submodules/TelegramUI/Sources/ChatScheduleTimeController.swift @@ -107,6 +107,6 @@ final class ChatScheduleTimeController: 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.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition) } } diff --git a/submodules/TelegramUI/Sources/ChatScheduleTimeControllerNode.swift b/submodules/TelegramUI/Sources/ChatScheduleTimeControllerNode.swift index 7324f017ea..fee537ce93 100644 --- a/submodules/TelegramUI/Sources/ChatScheduleTimeControllerNode.swift +++ b/submodules/TelegramUI/Sources/ChatScheduleTimeControllerNode.swift @@ -262,12 +262,12 @@ class ChatScheduleTimeControllerNode: ViewControllerTracingNode, UIScrollViewDel } } + private let calendar = Calendar(identifier: .gregorian) private func updateButtonTitle() { guard let date = self.pickerView?.date else { return } - let calendar = Calendar(identifier: .gregorian) let time = stringForMessageTimestamp(timestamp: Int32(date.timeIntervalSince1970), dateTimeFormat: self.presentationData.dateTimeFormat) switch mode { case .scheduledMessages: @@ -314,10 +314,16 @@ class ChatScheduleTimeControllerNode: ViewControllerTracingNode, UIScrollViewDel self.dimNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) let offset = self.bounds.size.height - self.contentBackgroundNode.frame.minY - let dimPosition = self.dimNode.layer.position - self.dimNode.layer.animatePosition(from: CGPoint(x: dimPosition.x, y: dimPosition.y - offset), to: dimPosition, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring) - self.layer.animateBoundsOriginYAdditive(from: -offset, to: 0.0, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring) + + let transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring) + let targetBounds = self.bounds + self.bounds = self.bounds.offsetBy(dx: 0.0, dy: -offset) + self.dimNode.position = CGPoint(x: dimPosition.x, y: dimPosition.y - offset) + transition.animateView({ + self.bounds = targetBounds + self.dimNode.position = dimPosition + }) } func animateOut(completion: (() -> Void)? = nil) { diff --git a/submodules/TelegramUI/Sources/ChatSearchNavigationContentNode.swift b/submodules/TelegramUI/Sources/ChatSearchNavigationContentNode.swift index 733969305f..5875966f27 100644 --- a/submodules/TelegramUI/Sources/ChatSearchNavigationContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatSearchNavigationContentNode.swift @@ -29,7 +29,7 @@ final class ChatSearchNavigationContentNode: NavigationBarContentNode { self.chatLocation = chatLocation self.interaction = interaction - self.searchBar = SearchBarNode(theme: SearchBarNodeTheme(theme: theme, hasSeparator: false), strings: strings, fieldStyle: .modern) + self.searchBar = SearchBarNode(theme: SearchBarNodeTheme(theme: theme, hasBackground: false, hasSeparator: false), strings: strings, fieldStyle: .modern) let placeholderText: String switch chatLocation { case .peer, .replyThread: @@ -96,7 +96,7 @@ final class ChatSearchNavigationContentNode: NavigationBarContentNode { func update(presentationInterfaceState: ChatPresentationInterfaceState) { if let search = presentationInterfaceState.search { - self.searchBar.updateThemeAndStrings(theme: SearchBarNodeTheme(theme: presentationInterfaceState.theme, hasSeparator: false), strings: presentationInterfaceState.strings) + self.searchBar.updateThemeAndStrings(theme: SearchBarNodeTheme(theme: presentationInterfaceState.theme, hasBackground: false, hasSeparator: false), strings: presentationInterfaceState.strings) switch search.domain { case .everything: diff --git a/submodules/TelegramUI/Sources/ChatSearchResultsContollerNode.swift b/submodules/TelegramUI/Sources/ChatSearchResultsContollerNode.swift index 645f19bec5..fb249f2872 100644 --- a/submodules/TelegramUI/Sources/ChatSearchResultsContollerNode.swift +++ b/submodules/TelegramUI/Sources/ChatSearchResultsContollerNode.swift @@ -140,7 +140,7 @@ class ChatSearchResultsControllerNode: ViewControllerTracingNode, UIScrollViewDe let presentationData = context.sharedContext.currentPresentationData.with { $0 } self.presentationData = presentationData - 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.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: true)) self.listNode = ListView() self.listNode.verticalScrollIndicatorColor = self.presentationData.theme.list.scrollIndicatorColor @@ -175,6 +175,7 @@ class ChatSearchResultsControllerNode: ViewControllerTracingNode, UIScrollViewDe }, peerSelected: { _, _ in }, disabledPeerSelected: { _ in }, togglePeerSelected: { _ in + }, togglePeersSelection: { _, _ in }, additionalCategorySelected: { _ in }, messageSelected: { [weak self] peer, message, _ in if let strongSelf = self { @@ -205,7 +206,7 @@ class ChatSearchResultsControllerNode: ViewControllerTracingNode, UIScrollViewDe switch item.content { case let .peer(peer): if let message = peer.messages.first { - let chatController = strongSelf.context.sharedContext.makeChatController(context: strongSelf.context, chatLocation: .peer(peer.peer.peerId), subject: .message(id: message.id, highlight: true), botStart: nil, mode: .standard(previewing: true)) + let chatController = strongSelf.context.sharedContext.makeChatController(context: strongSelf.context, chatLocation: .peer(peer.peer.peerId), subject: .message(id: message.id, highlight: true, timecode: nil), 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) @@ -256,7 +257,7 @@ class ChatSearchResultsControllerNode: ViewControllerTracingNode, UIScrollViewDe private func loadMore() { self.isLoadingMore = true - self.loadMoreDisposable.set((searchMessages(account: self.context.account, location: self.location, query: self.searchQuery, state: self.searchState) + self.loadMoreDisposable.set((self.context.engine.messages.searchMessages(location: self.location, query: self.searchQuery, state: self.searchState) |> deliverOnMainQueue).start(next: { [weak self] (updatedResult, updatedState) in guard let strongSelf = self else { return @@ -303,9 +304,8 @@ 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, 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.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: true))) self.listNode.forEachItemHeaderNode({ itemHeaderNode in if let itemHeaderNode = itemHeaderNode as? ChatListSearchItemHeaderNode { diff --git a/submodules/TelegramUI/Sources/ChatSearchResultsController.swift b/submodules/TelegramUI/Sources/ChatSearchResultsController.swift index 313a5eb0dd..09b180cb4e 100644 --- a/submodules/TelegramUI/Sources/ChatSearchResultsController.swift +++ b/submodules/TelegramUI/Sources/ChatSearchResultsController.swift @@ -79,7 +79,7 @@ final class ChatSearchResultsController: 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.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition) } @objc private func donePressed() { diff --git a/submodules/TelegramUI/Sources/ChatSendMessageActionSheetController.swift b/submodules/TelegramUI/Sources/ChatSendMessageActionSheetController.swift index 0123da2cd2..f434f57f5a 100644 --- a/submodules/TelegramUI/Sources/ChatSendMessageActionSheetController.swift +++ b/submodules/TelegramUI/Sources/ChatSendMessageActionSheetController.swift @@ -17,7 +17,7 @@ final class ChatSendMessageActionSheetController: ViewController { private let controllerInteraction: ChatControllerInteraction? private let interfaceState: ChatPresentationInterfaceState private let gesture: ContextGesture - private let sendButtonFrame: CGRect + private let sourceSendButton: ASDisplayNode private let textInputNode: EditableTextNode private let completion: () -> Void @@ -29,12 +29,12 @@ final class ChatSendMessageActionSheetController: ViewController { private let hapticFeedback = HapticFeedback() - init(context: AccountContext, controllerInteraction: ChatControllerInteraction?, interfaceState: ChatPresentationInterfaceState, gesture: ContextGesture, sendButtonFrame: CGRect, textInputNode: EditableTextNode, completion: @escaping () -> Void) { + init(context: AccountContext, controllerInteraction: ChatControllerInteraction?, interfaceState: ChatPresentationInterfaceState, gesture: ContextGesture, sourceSendButton: ASDisplayNode, textInputNode: EditableTextNode, completion: @escaping () -> Void) { self.context = context self.controllerInteraction = controllerInteraction self.interfaceState = interfaceState self.gesture = gesture - self.sendButtonFrame = sendButtonFrame + self.sourceSendButton = sourceSendButton self.textInputNode = textInputNode self.completion = completion @@ -76,7 +76,7 @@ final class ChatSendMessageActionSheetController: ViewController { canSchedule = !isSecret } - self.displayNode = ChatSendMessageActionSheetControllerNode(context: self.context, reminders: reminders, gesture: gesture, sendButtonFrame: self.sendButtonFrame, textInputNode: self.textInputNode, forwardedCount: forwardedCount, send: { [weak self] in + self.displayNode = ChatSendMessageActionSheetControllerNode(context: self.context, reminders: reminders, gesture: gesture, sourceSendButton: self.sourceSendButton, textInputNode: self.textInputNode, forwardedCount: forwardedCount, send: { [weak self] in self?.controllerInteraction?.sendCurrentMessage(false) self?.dismiss(cancel: false) }, sendSilently: { [weak self] in diff --git a/submodules/TelegramUI/Sources/ChatSendMessageActionSheetControllerNode.swift b/submodules/TelegramUI/Sources/ChatSendMessageActionSheetControllerNode.swift index d12f9fd3ca..eb103b5b12 100644 --- a/submodules/TelegramUI/Sources/ChatSendMessageActionSheetControllerNode.swift +++ b/submodules/TelegramUI/Sources/ChatSendMessageActionSheetControllerNode.swift @@ -154,7 +154,7 @@ private final class ActionSheetItemNode: ASDisplayNode { final class ChatSendMessageActionSheetControllerNode: ViewControllerTracingNode, UIScrollViewDelegate { private let context: AccountContext private var presentationData: PresentationData - private let sendButtonFrame: CGRect + private let sourceSendButton: ASDisplayNode private let textFieldFrame: CGRect private let textInputNode: EditableTextNode private let accessoryPanelNode: AccessoryPanelNode? @@ -163,9 +163,6 @@ final class ChatSendMessageActionSheetControllerNode: ViewControllerTracingNode, private let send: (() -> Void)? private let cancel: (() -> Void)? - private let textCoverNode: ASDisplayNode - private let buttonCoverNode: ASDisplayNode - private let effectView: UIVisualEffectView private let dimNode: ASDisplayNode @@ -181,10 +178,14 @@ final class ChatSendMessageActionSheetControllerNode: ViewControllerTracingNode, private var validLayout: ContainerViewLayout? - init(context: AccountContext, reminders: Bool, gesture: ContextGesture, sendButtonFrame: CGRect, textInputNode: EditableTextNode, forwardedCount: Int?, send: (() -> Void)?, sendSilently: (() -> Void)?, schedule: (() -> Void)?, cancel: (() -> Void)?) { + private var sendButtonFrame: CGRect { + return self.sourceSendButton.view.convert(self.sourceSendButton.bounds, to: nil) + } + + init(context: AccountContext, reminders: Bool, gesture: ContextGesture, sourceSendButton: ASDisplayNode, 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 + self.sourceSendButton = sourceSendButton self.textFieldFrame = textInputNode.convert(textInputNode.bounds, to: nil) self.textInputNode = textInputNode self.accessoryPanelNode = nil @@ -192,10 +193,7 @@ final class ChatSendMessageActionSheetControllerNode: ViewControllerTracingNode, self.send = send self.cancel = cancel - - self.textCoverNode = ASDisplayNode() - self.buttonCoverNode = ASDisplayNode() - + self.effectView = UIVisualEffectView() if #available(iOS 9.0, *) { } else { @@ -249,13 +247,7 @@ final class ChatSendMessageActionSheetControllerNode: ViewControllerTracingNode, self.contentNodes = contentNodes super.init() - - self.textCoverNode.backgroundColor = self.presentationData.theme.chat.inputPanel.inputBackgroundColor - self.addSubnode(self.textCoverNode) - - self.buttonCoverNode.backgroundColor = self.presentationData.theme.chat.inputPanel.panelBackgroundColor - self.addSubnode(self.buttonCoverNode) - + self.sendButtonNode.setImage(PresentationResourcesChat.chatInputPanelSendButtonImage(self.presentationData.theme), for: []) self.sendButtonNode.addTarget(self, action: #selector(sendButtonPressed), forControlEvents: .touchUpInside) @@ -370,8 +362,6 @@ final class ChatSendMessageActionSheetControllerNode: ViewControllerTracingNode, self.dimNode.backgroundColor = presentationData.theme.contextMenu.dimColor self.contentContainerNode.backgroundColor = self.presentationData.theme.contextMenu.backgroundColor - self.textCoverNode.backgroundColor = self.presentationData.theme.chat.inputPanel.inputBackgroundColor - self.buttonCoverNode.backgroundColor = self.presentationData.theme.chat.inputPanel.panelBackgroundColor self.sendButtonNode.setImage(PresentationResourcesChat.chatInputPanelSendButtonImage(self.presentationData.theme), for: []) if let toAttributedText = self.textInputNode.attributedText?.mutableCopy() as? NSMutableAttributedString { @@ -406,6 +396,9 @@ final class ChatSendMessageActionSheetControllerNode: ViewControllerTracingNode, self.fromMessageTextNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false) self.toMessageTextNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3, removeOnCompletion: false) + self.textInputNode.isHidden = true + self.sourceSendButton.isHidden = true + if let layout = self.validLayout { let duration = 0.4 @@ -466,8 +459,8 @@ final class ChatSendMessageActionSheetControllerNode: ViewControllerTracingNode, let intermediateCompletion: () -> Void = { [weak self] in if completedEffect && completedButton && completedBubble && completedAlpha { - self?.textCoverNode.isHidden = true - self?.buttonCoverNode.isHidden = true + self?.textInputNode.isHidden = false + self?.sourceSendButton.isHidden = false completion() } } @@ -494,7 +487,7 @@ final class ChatSendMessageActionSheetControllerNode: ViewControllerTracingNode, intermediateCompletion() }) } else { - self.textCoverNode.isHidden = true + self.textInputNode.isHidden = false self.messageClipNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { _ in completedAlpha = true intermediateCompletion() @@ -510,7 +503,7 @@ final class ChatSendMessageActionSheetControllerNode: ViewControllerTracingNode, }) if !cancel { - self.buttonCoverNode.isHidden = true + self.sourceSendButton.isHidden = false self.sendButtonNode.layer.animateScale(from: 1.0, to: 0.2, duration: 0.2, timingFunction: CAMediaTimingFunctionName.linear.rawValue, removeOnCompletion: false) self.sendButtonNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, timingFunction: CAMediaTimingFunctionName.linear.rawValue, removeOnCompletion: false) } @@ -565,10 +558,7 @@ final class ChatSendMessageActionSheetControllerNode: ViewControllerTracingNode, func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { self.validLayout = layout - - transition.updateFrame(node: self.textCoverNode, frame: self.textFieldFrame) - 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/Sources/ChatTextInputMediaRecordingButton.swift b/submodules/TelegramUI/Sources/ChatTextInputMediaRecordingButton.swift index d476cb3509..144f3db74e 100644 --- a/submodules/TelegramUI/Sources/ChatTextInputMediaRecordingButton.swift +++ b/submodules/TelegramUI/Sources/ChatTextInputMediaRecordingButton.swift @@ -97,7 +97,7 @@ private final class ChatTextInputMediaRecordingButtonPresenterControllerNode: Vi private final class ChatTextInputMediaRecordingButtonPresenter : NSObject, TGModernConversationInputMicButtonPresentation { private let account: Account? private let presentController: (ViewController) -> Void - private let container: ChatTextInputMediaRecordingButtonPresenterContainer + let container: ChatTextInputMediaRecordingButtonPresenterContainer private var presentationController: ChatTextInputMediaRecordingButtonPresenterController? init(account: Account, presentController: @escaping (ViewController) -> Void) { @@ -176,6 +176,16 @@ final class ChatTextInputMediaRecordingButton: TGModernConversationInputMicButto private(set) var cancelTranslation: CGFloat = 0.0 private var micLevelDisposable: MetaDisposable? + + private weak var currentPresenter: UIView? + + var contentContainer: (UIView, CGRect)? { + if let _ = self.currentPresenter { + return (self.micDecoration, self.micDecoration.bounds) + } else { + return nil + } + } var audioRecorder: ManagedAudioRecorder? { didSet { @@ -410,7 +420,9 @@ final class ChatTextInputMediaRecordingButton: TGModernConversationInputMicButto } func micButtonPresenter() -> TGModernConversationInputMicButtonPresentation! { - return ChatTextInputMediaRecordingButtonPresenter(account: self.account!, presentController: self.presentController) + let presenter = ChatTextInputMediaRecordingButtonPresenter(account: self.account!, presentController: self.presentController) + self.currentPresenter = presenter.view() + return presenter } func micButtonDecoration() -> (UIView & TGModernConversationInputMicButtonDecoration)! { @@ -427,7 +439,8 @@ final class ChatTextInputMediaRecordingButton: TGModernConversationInputMicButto override func animateIn() { super.animateIn() - + + micDecoration.isHidden = false micDecoration.startAnimating() innerIconView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false) diff --git a/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift b/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift index b764d05659..fe1c49909e 100644 --- a/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift @@ -105,7 +105,7 @@ private final class AccessoryItemIconButtonNode: HighlightTrackingButtonNode { if let timeout = timeout { return (nil, shortTimeIntervalString(strings: strings, value: timeout), strings.VoiceOver_SelfDestructTimerOn(timeIntervalString(strings: strings, value: timeout)).0, 1.0, UIEdgeInsets()) } else { - return (PresentationResourcesChat.chatInputTextFieldTimerImage(theme), nil, strings.VoiceOver_SelfDestructTimerOff, 1.0, UIEdgeInsets(top: 0.0, left: 0.0, bottom: 1.0, right: 0.0)) + return (PresentationResourcesChat.chatInputTextFieldTimerImage(theme), nil, strings.VoiceOver_SelfDestructTimerOff, 1.0, UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0)) } case .scheduledMessages: return (PresentationResourcesChat.chatInputTextFieldScheduleImage(theme), nil, strings.VoiceOver_ScheduledMessages, 1.0, UIEdgeInsets()) @@ -128,7 +128,9 @@ private final class AccessoryItemIconButtonNode: HighlightTrackingButtonNode { func updateLayout(size: CGSize) { if let image = self.imageNode.image { - self.imageNode.frame = CGRect(origin: CGPoint(x: floor((size.width - image.size.width) / 2.0), y: floor((size.height - image.size.height) / 2.0) - self.imageEdgeInsets.bottom), size: image.size) + let bottomInset: CGFloat = 0.0 + let imageFrame = CGRect(origin: CGPoint(x: floor((size.width - image.size.width) / 2.0), y: floor((size.height - image.size.height) / 2.0) - bottomInset), size: image.size) + self.imageNode.frame = imageFrame } } @@ -137,8 +139,12 @@ private final class AccessoryItemIconButtonNode: HighlightTrackingButtonNode { } } +let chatTextInputMinFontSize: CGFloat = 5.0 + +private let minInputFontSize = chatTextInputMinFontSize + private func calclulateTextFieldMinHeight(_ presentationInterfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics) -> CGFloat { - let baseFontSize = max(17.0, presentationInterfaceState.fontSize.baseDisplaySize) + let baseFontSize = max(minInputFontSize, presentationInterfaceState.fontSize.baseDisplaySize) var result: CGFloat if baseFontSize.isEqual(to: 26.0) { result = 42.0 @@ -161,21 +167,46 @@ private func calclulateTextFieldMinHeight(_ presentationInterfaceState: ChatPres return result } +private func calculateTextFieldRealInsets(_ presentationInterfaceState: ChatPresentationInterfaceState) -> UIEdgeInsets { + let baseFontSize = max(minInputFontSize, presentationInterfaceState.fontSize.baseDisplaySize) + let top: CGFloat + let bottom: CGFloat + if baseFontSize.isEqual(to: 14.0) { + top = 2.0 + bottom = 1.0 + } else if baseFontSize.isEqual(to: 15.0) { + top = 1.0 + bottom = 1.0 + } else if baseFontSize.isEqual(to: 16.0) { + top = 0.5 + bottom = 0.0 + } else { + top = 0.0 + bottom = 0.0 + } + return UIEdgeInsets(top: 4.5 + top, left: 0.0, bottom: 5.5 + bottom, right: 0.0) +} + private var currentTextInputBackgroundImage: (UIColor, UIColor, CGFloat, UIImage)? -private func textInputBackgroundImage(backgroundColor: UIColor, strokeColor: UIColor, diameter: CGFloat) -> UIImage? { - if let current = currentTextInputBackgroundImage { +private func textInputBackgroundImage(backgroundColor: UIColor?, inputBackgroundColor: UIColor?, strokeColor: UIColor, diameter: CGFloat) -> UIImage? { + if let backgroundColor = backgroundColor, let current = currentTextInputBackgroundImage { if current.0.isEqual(backgroundColor) && current.1.isEqual(strokeColor) && current.2.isEqual(to: diameter) { return current.3 } } let image = generateImage(CGSize(width: diameter, height: diameter), rotatedContext: { size, context in - context.setFillColor(backgroundColor.cgColor) - context.fill(CGRect(x: 0.0, y: 0.0, width: diameter, height: diameter)) - - context.setBlendMode(.clear) - context.setFillColor(UIColor.clear.cgColor) + context.clear(CGRect(x: 0.0, y: 0.0, width: diameter, height: diameter)) + + if let inputBackgroundColor = inputBackgroundColor { + context.setBlendMode(.normal) + context.setFillColor(inputBackgroundColor.cgColor) + } else { + context.setBlendMode(.clear) + context.setFillColor(UIColor.clear.cgColor) + } context.fillEllipse(in: CGRect(x: 0.0, y: 0.0, width: diameter, height: diameter)) + context.setBlendMode(.normal) context.setStrokeColor(strokeColor.cgColor) let strokeWidth: CGFloat = 1.0 @@ -183,7 +214,9 @@ private func textInputBackgroundImage(backgroundColor: UIColor, strokeColor: UIC context.strokeEllipse(in: CGRect(x: strokeWidth / 2.0, y: strokeWidth / 2.0, width: diameter - strokeWidth, height: diameter - strokeWidth)) })?.stretchableImage(withLeftCapWidth: Int(diameter) / 2, topCapHeight: Int(diameter) / 2) if let image = image { - currentTextInputBackgroundImage = (backgroundColor, strokeColor, diameter, image) + if let backgroundColor = backgroundColor { + currentTextInputBackgroundImage = (backgroundColor, strokeColor, diameter, image) + } return image } else { return nil @@ -201,14 +234,22 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { var textPlaceholderNode: ImmediateTextNode var contextPlaceholderNode: TextNode? var slowmodePlaceholderNode: ChatTextInputSlowmodePlaceholderNode? + let textInputContainerBackgroundNode: ASImageNode let textInputContainer: ASDisplayNode var textInputNode: EditableTextNode? let textInputBackgroundNode: ASImageNode + private var transparentTextInputBackgroundImage: UIImage? let actionButtons: ChatTextInputActionButtonsNode var mediaRecordingAccessibilityArea: AccessibilityAreaNode? private let counterTextNode: ImmediateTextNode + let menuButton: HighlightTrackingButtonNode + private let menuButtonBackgroundNode: ASDisplayNode + private let menuButtonClippingNode: ASDisplayNode + private let menuButtonIconNode: AnimationNode + private let menuButtonTextNode: ImmediateTextNode + let attachmentButton: HighlightableButtonNode let attachmentButtonDisabledNode: HighlightableButtonNode let searchLayoutClearButton: HighlightableButton @@ -216,6 +257,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { private var searchActivityIndicator: ActivityIndicator? var audioRecordingInfoContainerNode: ASDisplayNode? var audioRecordingDotNode: AnimationNode? + var audioRecordingDotNodeDismissed = false var audioRecordingTimeNode: ChatTextInputAudioRecordingTimeNode? var audioRecordingCancelIndicator: ChatTextInputAudioRecordingCancelIndicator? var animatingBinNode: AnimationNode? @@ -223,6 +265,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { private var accessoryItemButtons: [(ChatTextInputAccessoryItem, AccessoryItemIconButtonNode)] = [] private var validLayout: (CGFloat, CGFloat, CGFloat, UIEdgeInsets, CGFloat, LayoutMetrics, Bool)? + private var leftMenuInset: CGFloat = 0.0 var displayAttachmentMenu: () -> Void = { } var sendMessage: () -> Void = { } @@ -281,6 +324,10 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { self.actionButtons.micButton.account = self.context?.account } } + + var micButton: ChatTextInputMediaRecordingButton? { + return self.actionButtons.micButton + } private let statusDisposable = MetaDisposable() override var interfaceInteraction: ChatPanelInterfaceInteraction? { @@ -345,7 +392,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { if let presentationInterfaceState = self.presentationInterfaceState { textColor = presentationInterfaceState.theme.chat.inputPanel.inputTextColor accentTextColor = presentationInterfaceState.theme.chat.inputPanel.panelControlAccentColor - baseFontSize = max(17.0, presentationInterfaceState.fontSize.baseDisplaySize) + baseFontSize = max(minInputFontSize, presentationInterfaceState.fontSize.baseDisplaySize) } textInputNode.attributedText = textAttributedStringForStateText(state.inputText, fontSize: baseFontSize, textColor: textColor, accentTextColor: accentTextColor, writingDirection: nil) textInputNode.selectedRange = NSMakeRange(state.selectionRange.lowerBound, state.selectionRange.count) @@ -373,7 +420,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { var baseFontSize: CGFloat = 17.0 if let presentationInterfaceState = self.presentationInterfaceState { textColor = presentationInterfaceState.theme.chat.inputPanel.inputTextColor - baseFontSize = max(17.0, presentationInterfaceState.fontSize.baseDisplaySize) + baseFontSize = max(minInputFontSize, presentationInterfaceState.fontSize.baseDisplaySize) } textInputNode.attributedText = NSAttributedString(string: value, font: Font.regular(baseFontSize), textColor: textColor) self.editableTextNodeDidUpdateText(textInputNode) @@ -382,16 +429,19 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { } private let textInputViewInternalInsets = UIEdgeInsets(top: 1.0, left: 13.0, bottom: 1.0, right: 13.0) - private let textInputViewRealInsets = UIEdgeInsets(top: 4.5, left: 0.0, bottom: 5.5, right: 0.0) private let accessoryButtonSpacing: CGFloat = 0.0 private let accessoryButtonInset: CGFloat = 2.0 init(presentationInterfaceState: ChatPresentationInterfaceState, presentController: @escaping (ViewController) -> Void) { self.presentationInterfaceState = presentationInterfaceState + + self.textInputContainerBackgroundNode = ASImageNode() + self.textInputContainerBackgroundNode.isUserInteractionEnabled = false + self.textInputContainerBackgroundNode.displaysAsynchronously = false self.textInputContainer = ASDisplayNode() + self.textInputContainer.addSubnode(self.textInputContainerBackgroundNode) self.textInputContainer.clipsToBounds = true - self.textInputContainer.backgroundColor = presentationInterfaceState.theme.chat.inputPanel.inputBackgroundColor self.textInputBackgroundNode = ASImageNode() self.textInputBackgroundNode.displaysAsynchronously = false @@ -399,6 +449,18 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { self.textPlaceholderNode = ImmediateTextNode() self.textPlaceholderNode.maximumNumberOfLines = 1 self.textPlaceholderNode.isUserInteractionEnabled = false + + self.menuButton = HighlightTrackingButtonNode() + self.menuButton.clipsToBounds = true + self.menuButton.cornerRadius = 16.0 + self.menuButton.accessibilityLabel = presentationInterfaceState.strings.Conversation_InputMenu + self.menuButtonBackgroundNode = ASDisplayNode() + self.menuButtonClippingNode = ASDisplayNode() + self.menuButtonClippingNode.clipsToBounds = true + + self.menuButtonIconNode = AnimationNode(animation: "anim_menuclose", colors: ["1.1.Обводка 1": presentationInterfaceState.theme.chat.inputPanel.actionControlForegroundColor, "2.2.Обводка 1": presentationInterfaceState.theme.chat.inputPanel.actionControlForegroundColor, "3.1.Обводка 1": presentationInterfaceState.theme.chat.inputPanel.actionControlForegroundColor]) + self.menuButtonTextNode = ImmediateTextNode() + self.attachmentButton = HighlightableButtonNode(pointerStyle: .circle) self.attachmentButton.accessibilityLabel = presentationInterfaceState.strings.VoiceOver_AttachMedia self.attachmentButton.accessibilityTraits = [.button] @@ -415,6 +477,19 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { super.init() + self.menuButton.addTarget(self, action: #selector(self.menuButtonPressed), forControlEvents: .touchUpInside) + self.menuButton.highligthedChanged = { [weak self] highlighted in + if let strongSelf = self { + if highlighted { + let transition: ContainedViewLayoutTransition = .animated(duration: 0.3, curve: .spring) + transition.updateTransformScale(node: strongSelf.menuButton, scale: 0.85) + } else { + let transition: ContainedViewLayoutTransition = .animated(duration: 0.5, curve: .spring) + transition.updateTransformScale(node: strongSelf.menuButton, scale: 1.0) + } + } + } + self.attachmentButton.addTarget(self, action: #selector(self.attachmentButtonPressed), forControlEvents: .touchUpInside) self.attachmentButtonDisabledNode.addTarget(self, action: #selector(self.attachmentButtonPressed), forControlEvents: .touchUpInside) @@ -496,6 +571,12 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { self.addSubnode(self.textPlaceholderNode) + self.menuButton.addSubnode(self.menuButtonBackgroundNode) + self.menuButton.addSubnode(self.menuButtonClippingNode) + self.menuButtonClippingNode.addSubnode(self.menuButtonTextNode) + self.menuButton.addSubnode(self.menuButtonIconNode) + + self.addSubnode(self.menuButton) self.addSubnode(self.attachmentButton) self.addSubnode(self.attachmentButtonDisabledNode) @@ -539,7 +620,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { if let presentationInterfaceState = self.presentationInterfaceState { textColor = presentationInterfaceState.theme.chat.inputPanel.inputTextColor tintColor = presentationInterfaceState.theme.list.itemAccentColor - baseFontSize = max(17.0, presentationInterfaceState.fontSize.baseDisplaySize) + baseFontSize = max(minInputFontSize, presentationInterfaceState.fontSize.baseDisplaySize) keyboardAppearance = presentationInterfaceState.theme.rootController.keyboardColor.keyboardAppearance } @@ -550,13 +631,12 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { paragraphStyle.maximumLineHeight = 20.0 paragraphStyle.minimumLineHeight = 20.0 - textInputNode.typingAttributes = [NSAttributedString.Key.font.rawValue: Font.regular(max(17.0, baseFontSize)), NSAttributedString.Key.foregroundColor.rawValue: textColor, NSAttributedString.Key.paragraphStyle.rawValue: paragraphStyle] + textInputNode.typingAttributes = [NSAttributedString.Key.font.rawValue: Font.regular(max(minInputFontSize, baseFontSize)), NSAttributedString.Key.foregroundColor.rawValue: textColor, NSAttributedString.Key.paragraphStyle.rawValue: paragraphStyle] textInputNode.clipsToBounds = false textInputNode.textView.clipsToBounds = false textInputNode.delegate = self textInputNode.hitTestSlop = UIEdgeInsets(top: -5.0, left: -5.0, bottom: -5.0, right: -5.0) textInputNode.keyboardAppearance = keyboardAppearance - textInputNode.textContainerInset = UIEdgeInsets(top: self.textInputViewRealInsets.top, left: 0.0, bottom: self.textInputViewRealInsets.bottom, right: 0.0) textInputNode.tintColor = tintColor textInputNode.textView.scrollIndicatorInsets = UIEdgeInsets(top: 9.0, left: 0.0, bottom: 9.0, right: -13.0) self.textInputContainer.addSubnode(textInputNode) @@ -565,6 +645,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { if let presentationInterfaceState = self.presentationInterfaceState { refreshChatTextInputTypingAttributes(textInputNode, theme: presentationInterfaceState.theme, baseFontSize: baseFontSize) + textInputNode.textContainerInset = calculateTextFieldRealInsets(presentationInterfaceState) } if !self.textInputContainer.bounds.size.width.isZero { @@ -582,7 +663,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { accessoryButtonsWidth += button.buttonWidth } - textInputNode.frame = CGRect(origin: CGPoint(x: self.textInputViewInternalInsets.left, y: self.textInputViewInternalInsets.top), size: CGSize(width: textInputFrame.size.width - (self.textInputViewInternalInsets.left + self.textInputViewInternalInsets.right + accessoryButtonsWidth), height: textInputFrame.size.height - self.textInputViewInternalInsets.top - self.textInputViewInternalInsets.bottom)) + textInputNode.frame = CGRect(origin: CGPoint(x: self.textInputViewInternalInsets.left, y: self.textInputViewInternalInsets.top), size: CGSize(width: textInputFrame.size.width - (self.textInputViewInternalInsets.left + self.textInputViewInternalInsets.right), height: textInputFrame.size.height - self.textInputViewInternalInsets.top - self.textInputViewInternalInsets.bottom)) } self.textInputBackgroundNode.isUserInteractionEnabled = false @@ -631,12 +712,13 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { let textFieldHeight: CGFloat if let textInputNode = self.textInputNode { - let measuredHeight = textInputNode.measure(CGSize(width: width - textFieldInsets.left - textFieldInsets.right - self.textInputViewInternalInsets.left - self.textInputViewInternalInsets.right - accessoryButtonsWidth, height: CGFloat.greatestFiniteMagnitude)) + let maxTextWidth = width - textFieldInsets.left - textFieldInsets.right - self.textInputViewInternalInsets.left - self.textInputViewInternalInsets.right + let measuredHeight = textInputNode.measure(CGSize(width: maxTextWidth, height: CGFloat.greatestFiniteMagnitude)) let unboundTextFieldHeight = max(textFieldMinHeight, ceil(measuredHeight.height)) let maxNumberOfLines = min(12, (Int(fieldMaxHeight - 11.0) - 33) / 22) - let updatedMaxHeight = (CGFloat(maxNumberOfLines) * 22.0 + 10.0) + let updatedMaxHeight = (CGFloat(maxNumberOfLines) * (22.0 + 2.0) + 10.0) textFieldHeight = max(textFieldMinHeight, min(updatedMaxHeight, unboundTextFieldHeight)) } else { @@ -673,8 +755,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, additionalSideInsets: UIEdgeInsets, maxHeight: CGFloat, isSecondary: Bool, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics) -> CGFloat { let previousAdditionalSideInsets = self.validLayout?.3 self.validLayout = (width, leftInset, rightInset, additionalSideInsets, maxHeight, metrics, isSecondary) - let baseWidth = width - leftInset - rightInset - + var transition = transition var additionalOffset: CGFloat = 0.0 if let previousAdditionalSideInsets = previousAdditionalSideInsets, previousAdditionalSideInsets.right != additionalSideInsets.right { @@ -720,12 +801,22 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { self.attachmentButton.accessibilityTraits = (!isSlowmodeActive || isMediaEnabled) ? [.button] : [.button, .notEnabled] self.attachmentButtonDisabledNode.isHidden = !isSlowmodeActive || isMediaEnabled + var menuTextSize = self.menuButtonTextNode.frame.size if self.presentationInterfaceState != interfaceState { let previousState = self.presentationInterfaceState self.presentationInterfaceState = interfaceState let themeUpdated = previousState?.theme !== interfaceState.theme + if let previousShowCommands = previousState?.showCommands, previousShowCommands != interfaceState.showCommands { + if interfaceState.showCommands { + self.menuButtonIconNode.setAnimation(name: "anim_menuclose", colors: ["1.1.Обводка 1": interfaceState.theme.chat.inputPanel.actionControlForegroundColor, "2.2.Обводка 1": interfaceState.theme.chat.inputPanel.actionControlForegroundColor, "3.1.Обводка 1": interfaceState.theme.chat.inputPanel.actionControlForegroundColor]) + } else { + self.menuButtonIconNode.setAnimation(name: "anim_closemenu", colors: ["1.1.Обводка 1": interfaceState.theme.chat.inputPanel.actionControlForegroundColor, "2.2.Обводка 1": interfaceState.theme.chat.inputPanel.actionControlForegroundColor, "3.1.Обводка 1": interfaceState.theme.chat.inputPanel.actionControlForegroundColor]) + } + self.menuButtonIconNode.playOnce() + } + var updateSendButtonIcon = false if (previousState?.interfaceState.editMessage != nil) != (interfaceState.interfaceState.editMessage != nil) { updateSendButtonIcon = true @@ -735,7 +826,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { if self.theme == nil || !self.theme!.chat.inputPanel.inputTextColor.isEqual(interfaceState.theme.chat.inputPanel.inputTextColor) { let textColor = interfaceState.theme.chat.inputPanel.inputTextColor - let baseFontSize = max(17.0, interfaceState.fontSize.baseDisplaySize) + let baseFontSize = max(minInputFontSize, interfaceState.fontSize.baseDisplaySize) if let textInputNode = self.textInputNode { if let text = textInputNode.attributedText?.string { @@ -756,10 +847,12 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { textInputNode.keyboardAppearance = keyboardAppearance } - self.textInputContainer.backgroundColor = interfaceState.theme.chat.inputPanel.inputBackgroundColor - self.theme = interfaceState.theme + self.menuButtonBackgroundNode.backgroundColor = interfaceState.theme.chat.inputPanel.actionControlFillColor + self.menuButtonTextNode.attributedText = NSAttributedString(string: interfaceState.strings.Conversation_InputMenu, font: Font.with(size: 16.0, design: .round, weight: .medium, traits: []), textColor: interfaceState.theme.chat.inputPanel.actionControlForegroundColor) + self.menuButton.accessibilityLabel = interfaceState.strings.Conversation_InputMenu + menuTextSize = self.menuButtonTextNode.updateLayout(CGSize(width: width, height: 44.0)) if isEditingMedia { self.attachmentButton.setImage(PresentationResourcesChat.chatInputPanelEditAttachmentButtonImage(interfaceState.theme), for: []) @@ -779,7 +872,9 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { backgroundColor = interfaceState.theme.chat.inputPanel.panelBackgroundColor } - self.textInputBackgroundNode.image = textInputBackgroundImage(backgroundColor: backgroundColor, strokeColor: interfaceState.theme.chat.inputPanel.inputStrokeColor, diameter: minimalInputHeight) + self.textInputBackgroundNode.image = textInputBackgroundImage(backgroundColor: backgroundColor, inputBackgroundColor: nil, strokeColor: interfaceState.theme.chat.inputPanel.inputStrokeColor, diameter: minimalInputHeight) + self.transparentTextInputBackgroundImage = textInputBackgroundImage(backgroundColor: nil, inputBackgroundColor: interfaceState.theme.chat.inputPanel.inputBackgroundColor, strokeColor: interfaceState.theme.chat.inputPanel.inputStrokeColor, diameter: minimalInputHeight) + self.textInputContainerBackgroundNode.image = generateStretchableFilledCircleImage(diameter: minimalInputHeight, color: interfaceState.theme.chat.inputPanel.inputBackgroundColor) self.searchLayoutClearImageNode.image = PresentationResourcesChat.chatInputTextFieldClearImage(interfaceState.theme) @@ -807,11 +902,14 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { } } } + + let dismissedButtonMessageUpdated = interfaceState.interfaceState.messageActionsState.dismissedButtonKeyboardMessageId != previousState?.interfaceState.messageActionsState.dismissedButtonKeyboardMessageId + let replyMessageUpdated = interfaceState.interfaceState.replyMessageId != previousState?.interfaceState.replyMessageId - if let peer = interfaceState.renderedPeer?.peer, previousState?.renderedPeer?.peer == nil || !peer.isEqual(previousState!.renderedPeer!.peer!) || previousState?.interfaceState.silentPosting != interfaceState.interfaceState.silentPosting || themeUpdated || !self.initializedPlaceholder { + if let peer = interfaceState.renderedPeer?.peer, previousState?.renderedPeer?.peer == nil || !peer.isEqual(previousState!.renderedPeer!.peer!) || previousState?.interfaceState.silentPosting != interfaceState.interfaceState.silentPosting || themeUpdated || !self.initializedPlaceholder || previousState?.keyboardButtonsMessage?.id != interfaceState.keyboardButtonsMessage?.id || previousState?.keyboardButtonsMessage?.visibleReplyMarkupPlaceholder != interfaceState.keyboardButtonsMessage?.visibleReplyMarkupPlaceholder || dismissedButtonMessageUpdated || replyMessageUpdated { self.initializedPlaceholder = true - let placeholder: String + var placeholder: String if let channel = peer as? TelegramChannel, case .broadcast = channel.info { if interfaceState.interfaceState.silentPosting { placeholder = interfaceState.strings.Conversation_InputTextSilentBroadcastPlaceholder @@ -829,9 +927,19 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { } else { placeholder = interfaceState.strings.Conversation_InputTextPlaceholder } + + if let keyboardButtonsMessage = interfaceState.keyboardButtonsMessage, interfaceState.interfaceState.messageActionsState.dismissedButtonKeyboardMessageId != keyboardButtonsMessage.id { + if keyboardButtonsMessage.requestsSetupReply && keyboardButtonsMessage.id != interfaceState.interfaceState.replyMessageId { + } else { + if let placeholderValue = interfaceState.keyboardButtonsMessage?.visibleReplyMarkupPlaceholder, !placeholderValue.isEmpty { + placeholder = placeholderValue + } + } + } + if self.currentPlaceholder != placeholder || themeUpdated { self.currentPlaceholder = placeholder - let baseFontSize = max(17.0, interfaceState.fontSize.baseDisplaySize) + let baseFontSize = max(minInputFontSize, interfaceState.fontSize.baseDisplaySize) self.textPlaceholderNode.attributedText = NSAttributedString(string: placeholder, font: Font.regular(baseFontSize), textColor: interfaceState.theme.chat.inputPanel.inputPlaceholderColor) self.textInputNode?.textView.accessibilityHint = placeholder let placeholderSize = self.textPlaceholderNode.updateLayout(CGSize(width: 320.0, height: CGFloat.greatestFiniteMagnitude)) @@ -936,14 +1044,68 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { self.accessoryItemButtons = updatedButtons } + let mediaRecordingState = interfaceState.inputTextPanelState.mediaRecordingState + + var inputHasText = false + if let textInputNode = self.textInputNode, let attributedText = textInputNode.attributedText, attributedText.length != 0 { + inputHasText = true + } + + var hasMenuButton = false + var menuButtonExpanded = false + if let peer = interfaceState.renderedPeer?.peer as? TelegramUser, let _ = peer.botInfo, interfaceState.hasBotCommands && interfaceState.editMessageState == nil { + hasMenuButton = true + + if !inputHasText { + switch interfaceState.inputMode { + case .none, .inputButtons: + menuButtonExpanded = true + default: + break + } + } + } + if mediaRecordingState != nil { + hasMenuButton = false + } + + let leftMenuInset: CGFloat + let menuCollapsedButtonWidth: CGFloat = 38.0 + let menuButtonWidth = menuTextSize.width + 47.0 + if hasMenuButton { + let menuButtonSpacing: CGFloat = 10.0 + if menuButtonExpanded { + leftMenuInset = menuButtonWidth + menuButtonSpacing + } else { + leftMenuInset = menuCollapsedButtonWidth + menuButtonSpacing + } + } else { + leftMenuInset = 0.0 + } + self.leftMenuInset = leftMenuInset + + let baseWidth = width - leftInset - leftMenuInset - rightInset let (accessoryButtonsWidth, textFieldHeight) = self.calculateTextFieldMetrics(width: baseWidth, maxHeight: maxHeight, metrics: metrics) let panelHeight = self.panelHeight(textFieldHeight: textFieldHeight, metrics: metrics) + + let menuButtonHeight: CGFloat = 33.0 + let menuButtonFrame = CGRect(x: leftInset + 10.0, y: panelHeight - minimalHeight + floorToScreenPixels((minimalHeight - menuButtonHeight) / 2.0), width: menuButtonExpanded ? menuButtonWidth : menuCollapsedButtonWidth, height: menuButtonHeight) + transition.updateFrameAsPositionAndBounds(node: self.menuButton, frame: menuButtonFrame) + transition.updateFrame(node: self.menuButtonBackgroundNode, frame: CGRect(origin: CGPoint(), size: menuButtonFrame.size)) + transition.updateFrame(node: self.menuButtonClippingNode, frame: CGRect(origin: CGPoint(x: 19.0, y: 0.0), size: CGSize(width: menuButtonWidth - 19.0, height: menuButtonFrame.height))) + transition.updateFrame(node: self.menuButtonTextNode, frame: CGRect(origin: CGPoint(x: 16.0, y: 7.0 - UIScreenPixel), size: menuTextSize)) + transition.updateAlpha(node: self.menuButtonTextNode, alpha: menuButtonExpanded ? 1.0 : 0.0) + transition.updateFrame(node: self.menuButtonIconNode, frame: CGRect(x: 4.0 + UIScreenPixel, y: 1.0 + UIScreenPixel, width: 30.0, height: 30.0)) + let showMenuButton = hasMenuButton && interfaceState.recordedMediaPreview == nil + transition.updateTransformScale(node: self.menuButton, scale: showMenuButton ? 1.0 : 0.001) + transition.updateAlpha(node: self.menuButton, alpha: showMenuButton ? 1.0 : 0.0) + self.menuButton.isUserInteractionEnabled = hasMenuButton + self.actionButtons.micButton.updateMode(mode: interfaceState.interfaceState.mediaRecordingMode, animated: transition.isAnimated) var hideMicButton = false var audioRecordingItemsAlpha: CGFloat = 1 - let mediaRecordingState = interfaceState.inputTextPanelState.mediaRecordingState if mediaRecordingState != nil || interfaceState.recordedMediaPreview != nil { audioRecordingItemsAlpha = 0 @@ -970,7 +1132,6 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { } } - var animateCancelSlideIn = false let audioRecordingCancelIndicator: ChatTextInputAudioRecordingCancelIndicator if let currentAudioRecordingCancelIndicator = self.audioRecordingCancelIndicator { @@ -1090,7 +1251,8 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { self.audioRecordingDotNode?.removeFromSupernode() audioRecordingDotNode = AnimationNode(animation: "BinRed") self.audioRecordingDotNode = audioRecordingDotNode - self.addSubnode(audioRecordingDotNode) + self.audioRecordingDotNodeDismissed = false + self.insertSubnode(audioRecordingDotNode, belowSubnode: self.menuButton) self.animatingBinNode?.removeFromSupernode() self.animatingBinNode = nil } @@ -1128,6 +1290,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { audioRecordingCancelIndicator.layer.animateAlpha(from: CGFloat(audioRecordingCancelIndicator.layer.presentation()?.opacity ?? 1), to: 0, duration: 0.15, delay: 0, removeOnCompletion: false) } } else { + var update = self.actionButtons.micButton.audioRecorder != nil || self.actionButtons.micButton.videoRecordingStatus != nil self.actionButtons.micButton.audioRecorder = nil self.actionButtons.micButton.videoRecordingStatus = nil transition.updateAlpha(layer: self.textInputBackgroundNode.layer, alpha: 1.0) @@ -1146,7 +1309,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { } if let audioRecordingDotNode = self.audioRecordingDotNode { - let dismissDotNode = { [weak audioRecordingDotNode, weak attachmentButton, weak self] in + let dismissDotNode = { [weak audioRecordingDotNode, weak self] in guard let audioRecordingDotNode = audioRecordingDotNode, audioRecordingDotNode === self?.audioRecordingDotNode else { return } self?.audioRecordingDotNode = nil @@ -1156,23 +1319,34 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { audioRecordingDotNode?.removeFromSupernode() } - attachmentButton?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15, delay: 0, removeOnCompletion: false) - attachmentButton?.layer.animateScale(from: 0.3, to: 1.0, duration: 0.15, delay: 0, removeOnCompletion: false) + self?.attachmentButton.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15, delay: 0, removeOnCompletion: false) + self?.attachmentButton.layer.animateScale(from: 0.3, to: 1.0, duration: 0.15, delay: 0, removeOnCompletion: false) } - audioRecordingDotNode.layer.removeAllAnimations() + if update && !self.audioRecordingDotNodeDismissed { + audioRecordingDotNode.layer.removeAllAnimations() + } if self.isMediaDeleted { if self.prevInputPanelNode is ChatRecordingPreviewInputPanelNode { self.audioRecordingDotNode?.removeFromSupernode() self.audioRecordingDotNode = nil } else { + if !self.audioRecordingDotNodeDismissed { + audioRecordingDotNode.layer.removeAllAnimations() + } audioRecordingDotNode.completion = dismissDotNode audioRecordingDotNode.play() + update = true } } else { dismissDotNode() } + + if update && !self.audioRecordingDotNodeDismissed { + self.audioRecordingDotNode?.layer.animatePosition(from: CGPoint(), to: CGPoint(x: leftMenuInset, y: 0.0), duration: 0.15, removeOnCompletion: false, additive: true) + self.audioRecordingDotNodeDismissed = true + } } if let audioRecordingTimeNode = self.audioRecordingTimeNode { @@ -1195,6 +1369,9 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { } } + var leftInset = leftInset + leftInset += leftMenuInset + transition.updateFrame(layer: self.attachmentButton.layer, frame: CGRect(origin: CGPoint(x: leftInset + 2.0 - UIScreenPixel, y: panelHeight - minimalHeight), size: CGSize(width: 40.0, height: minimalHeight))) transition.updateFrame(node: self.attachmentButtonDisabledNode, frame: self.attachmentButton.frame) @@ -1260,13 +1437,19 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { if let image = self.searchLayoutClearImageNode.image { self.searchLayoutClearImageNode.frame = CGRect(origin: CGPoint(x: floor((searchLayoutClearButtonSize.width - image.size.width) / 2.0), y: floor((searchLayoutClearButtonSize.height - image.size.height) / 2.0)), size: image.size) } + + var textInputViewRealInsets = UIEdgeInsets() + if let presentationInterfaceState = self.presentationInterfaceState { + textInputViewRealInsets = calculateTextFieldRealInsets(presentationInterfaceState) + } let textInputFrame = CGRect(x: leftInset + textFieldInsets.left, y: textFieldInsets.top, width: baseWidth - textFieldInsets.left - textFieldInsets.right + textInputBackgroundWidthOffset, height: panelHeight - textFieldInsets.top - textFieldInsets.bottom) transition.updateFrame(node: self.textInputContainer, frame: textInputFrame) + transition.updateFrame(node: self.textInputContainerBackgroundNode, frame: CGRect(origin: CGPoint(), size: textInputFrame.size)) transition.updateAlpha(node: self.textInputContainer, alpha: audioRecordingItemsAlpha) if let textInputNode = self.textInputNode { - let textFieldFrame = CGRect(origin: CGPoint(x: self.textInputViewInternalInsets.left, y: self.textInputViewInternalInsets.top), size: CGSize(width: textInputFrame.size.width - (self.textInputViewInternalInsets.left + self.textInputViewInternalInsets.right + accessoryButtonsWidth), height: textInputFrame.size.height - self.textInputViewInternalInsets.top - textInputViewInternalInsets.bottom)) + let textFieldFrame = CGRect(origin: CGPoint(x: self.textInputViewInternalInsets.left, y: self.textInputViewInternalInsets.top), size: CGSize(width: textInputFrame.size.width - (self.textInputViewInternalInsets.left + self.textInputViewInternalInsets.right), height: textInputFrame.size.height - self.textInputViewInternalInsets.top - textInputViewInternalInsets.bottom)) let shouldUpdateLayout = textFieldFrame.size != textInputNode.frame.size transition.updateFrame(node: textInputNode, frame: textFieldFrame) if shouldUpdateLayout { @@ -1292,7 +1475,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { let _ = placeholderApply() - contextPlaceholderNode.frame = CGRect(origin: CGPoint(x: leftInset + textFieldInsets.left + self.textInputViewInternalInsets.left, y: textFieldInsets.top + self.textInputViewInternalInsets.top + self.textInputViewRealInsets.top + UIScreenPixel), size: placeholderSize.size) + contextPlaceholderNode.frame = CGRect(origin: CGPoint(x: leftInset + textFieldInsets.left + self.textInputViewInternalInsets.left, y: textFieldInsets.top + self.textInputViewInternalInsets.top + textInputViewRealInsets.top + UIScreenPixel), size: placeholderSize.size) contextPlaceholderNode.alpha = audioRecordingItemsAlpha } else if let contextPlaceholderNode = self.contextPlaceholderNode { self.contextPlaceholderNode = nil @@ -1309,7 +1492,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { self.slowmodePlaceholderNode = slowmodePlaceholderNode self.insertSubnode(slowmodePlaceholderNode, aboveSubnode: self.textPlaceholderNode) } - let placeholderFrame = CGRect(origin: CGPoint(x: leftInset + textFieldInsets.left + self.textInputViewInternalInsets.left, y: textFieldInsets.top + self.textInputViewInternalInsets.top + self.textInputViewRealInsets.top + UIScreenPixel), size: CGSize(width: width - leftInset - rightInset - textFieldInsets.left - textFieldInsets.right - self.textInputViewInternalInsets.left - self.textInputViewInternalInsets.right - accessoryButtonsWidth, height: 30.0)) + let placeholderFrame = CGRect(origin: CGPoint(x: leftInset + textFieldInsets.left + self.textInputViewInternalInsets.left, y: textFieldInsets.top + self.textInputViewInternalInsets.top + textInputViewRealInsets.top + UIScreenPixel), size: CGSize(width: width - leftInset - rightInset - textFieldInsets.left - textFieldInsets.right - self.textInputViewInternalInsets.left - self.textInputViewInternalInsets.right - accessoryButtonsWidth, height: 30.0)) slowmodePlaceholderNode.updateState(slowmodeState) slowmodePlaceholderNode.frame = placeholderFrame slowmodePlaceholderNode.alpha = audioRecordingItemsAlpha @@ -1318,12 +1501,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { self.slowmodePlaceholderNode = nil slowmodePlaceholderNode.removeFromSupernode() } - - var inputHasText = false - if let textInputNode = self.textInputNode, let attributedText = textInputNode.attributedText, attributedText.length != 0 { - inputHasText = true - } - + if (interfaceState.slowmodeState != nil && !isScheduledMessages && interfaceState.editMessageState == nil) || interfaceState.inputTextPanelState.contextPlaceholder != nil { self.textPlaceholderNode.isHidden = true self.slowmodePlaceholderNode?.isHidden = inputHasText @@ -1332,7 +1510,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { self.slowmodePlaceholderNode?.isHidden = true } - transition.updateFrame(node: self.textPlaceholderNode, frame: CGRect(origin: CGPoint(x: leftInset + textFieldInsets.left + self.textInputViewInternalInsets.left, y: textFieldInsets.top + self.textInputViewInternalInsets.top + self.textInputViewRealInsets.top + UIScreenPixel), size: self.textPlaceholderNode.frame.size)) + transition.updateFrame(node: self.textPlaceholderNode, frame: CGRect(origin: CGPoint(x: leftInset + textFieldInsets.left + self.textInputViewInternalInsets.left, y: textFieldInsets.top + self.textInputViewInternalInsets.top + textInputViewRealInsets.top + UIScreenPixel), size: self.textPlaceholderNode.frame.size)) transition.updateAlpha(node: self.textPlaceholderNode, alpha: audioRecordingItemsAlpha) transition.updateFrame(layer: self.textInputBackgroundNode.layer, frame: CGRect(x: leftInset + textFieldInsets.left, y: textFieldInsets.top, width: baseWidth - textFieldInsets.left - textFieldInsets.right + textInputBackgroundWidthOffset, height: panelHeight - textFieldInsets.top - textFieldInsets.bottom)) @@ -1369,13 +1547,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { } } - var hasText = false - if let textInputNode = self.textInputNode, let attributedText = textInputNode.attributedText, attributedText.length != 0 { - hasText = true - hideMicButton = true - } - - if self.extendedSearchLayout { + if inputHasText || self.extendedSearchLayout { hideMicButton = true } @@ -1391,9 +1563,9 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { } self.actionButtons.micButton.fadeDisabled = mediaInputDisabled - self.updateActionButtons(hasText: hasText, hideMicButton: hideMicButton, animated: transition.isAnimated) + self.updateActionButtons(hasText: inputHasText, hideMicButton: hideMicButton, animated: transition.isAnimated) - if let prevInputPanelNode = prevInputPanelNode { + if let prevInputPanelNode = self.prevInputPanelNode { prevInputPanelNode.frame = CGRect(origin: .zero, size: prevInputPanelNode.frame.size) } if let prevPreviewInputPanelNode = self.prevInputPanelNode as? ChatRecordingPreviewInputPanelNode { @@ -1406,7 +1578,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { func animatePosition(for previewSubnode: ASDisplayNode) { previewSubnode.layer.animatePosition( from: previewSubnode.position, - to: CGPoint(x: previewSubnode.position.x - 20, y: previewSubnode.position.y), + to: CGPoint(x: leftMenuInset.isZero ? previewSubnode.position.x - 20 : leftMenuInset + previewSubnode.frame.width / 2.0, y: previewSubnode.position.y), duration: 0.15 ) } @@ -1415,7 +1587,6 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { animatePosition(for: prevPreviewInputPanelNode.waveformScubberNode) animatePosition(for: prevPreviewInputPanelNode.durationLabel) animatePosition(for: prevPreviewInputPanelNode.playButton) - animatePosition(for: prevPreviewInputPanelNode.pauseButton) } func animateAlpha(for previewSubnode: ASDisplayNode) { @@ -1430,7 +1601,6 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { animateAlpha(for: prevPreviewInputPanelNode.waveformScubberNode) animateAlpha(for: prevPreviewInputPanelNode.durationLabel) animateAlpha(for: prevPreviewInputPanelNode.playButton) - animateAlpha(for: prevPreviewInputPanelNode.pauseButton) let binNode = prevPreviewInputPanelNode.binNode self.animatingBinNode = binNode @@ -1456,12 +1626,21 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { } else { dismissBin() } + + prevPreviewInputPanelNode.deleteButton.layer.animatePosition(from: CGPoint(), to: CGPoint(x: leftMenuInset, y: 0.0), duration: 0.15, removeOnCompletion: false, additive: true) prevPreviewInputPanelNode.sendButton.layer.animateScale(from: 1.0, to: 0.3, duration: 0.15, removeOnCompletion: false) prevPreviewInputPanelNode.sendButton.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false) - actionButtons.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15, delay: 0, removeOnCompletion: false) - actionButtons.layer.animateScale(from: 0.3, to: 1.0, duration: 0.15, delay: 0, removeOnCompletion: false) + self.actionButtons.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15, delay: 0, removeOnCompletion: false) + self.actionButtons.layer.animateScale(from: 0.3, to: 1.0, duration: 0.15, delay: 0, removeOnCompletion: false) + + if hasMenuButton { + self.menuButton.alpha = 1.0 + self.menuButton.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15, delay: 0, removeOnCompletion: false) + self.menuButton.transform = CATransform3DIdentity + self.menuButton.layer.animateScale(from: 0.3, to: 1.0, duration: 0.15, delay: 0, removeOnCompletion: false) + } prevPreviewInputPanelNode.sendButton.layer.animateScale(from: 1.0, to: 0.3, duration: 0.15, removeOnCompletion: false) prevPreviewInputPanelNode.sendButton.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false) @@ -1476,7 +1655,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { @objc func editableTextNodeDidUpdateText(_ editableTextNode: ASEditableTextNode) { if let textInputNode = self.textInputNode, let presentationInterfaceState = self.presentationInterfaceState { - let baseFontSize = max(17.0, presentationInterfaceState.fontSize.baseDisplaySize) + let baseFontSize = max(minInputFontSize, presentationInterfaceState.fontSize.baseDisplaySize) refreshChatTextInputAttributes(textInputNode, theme: presentationInterfaceState.theme, baseFontSize: baseFontSize) refreshChatTextInputTypingAttributes(textInputNode, theme: presentationInterfaceState.theme, baseFontSize: baseFontSize) @@ -1508,7 +1687,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { composeButtonsOffset = 44.0 } - let (_, textFieldHeight) = self.calculateTextFieldMetrics(width: width - leftInset - rightInset, maxHeight: maxHeight, metrics: metrics) + let (_, textFieldHeight) = self.calculateTextFieldMetrics(width: width - leftInset - rightInset - self.leftMenuInset, maxHeight: maxHeight, metrics: metrics) let panelHeight = self.panelHeight(textFieldHeight: textFieldHeight, metrics: metrics) var textFieldMinHeight: CGFloat = 33.0 if let presentationInterfaceState = self.presentationInterfaceState { @@ -1695,7 +1874,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { private func updateTextHeight(animated: Bool) { if let (width, leftInset, rightInset, additionalSideInsets, maxHeight, metrics, _) = self.validLayout { - let (_, textFieldHeight) = self.calculateTextFieldMetrics(width: width - leftInset - rightInset - additionalSideInsets.right, maxHeight: maxHeight, metrics: metrics) + let (_, textFieldHeight) = self.calculateTextFieldMetrics(width: width - leftInset - rightInset - additionalSideInsets.right - self.leftMenuInset, maxHeight: maxHeight, metrics: metrics) let panelHeight = self.panelHeight(textFieldHeight: textFieldHeight, metrics: metrics) if !self.bounds.size.height.isEqual(to: panelHeight) { self.updateHeight(animated) @@ -1760,7 +1939,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { UIMenuController.shared.update() } - let baseFontSize = max(17.0, presentationInterfaceState.fontSize.baseDisplaySize) + let baseFontSize = max(minInputFontSize, presentationInterfaceState.fontSize.baseDisplaySize) refreshChatTextInputTypingAttributes(textInputNode, theme: presentationInterfaceState.theme, baseFontSize: baseFontSize) } } @@ -1903,7 +2082,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { if let presentationInterfaceState = self.presentationInterfaceState { textColor = presentationInterfaceState.theme.chat.inputPanel.inputTextColor accentTextColor = presentationInterfaceState.theme.chat.inputPanel.panelControlAccentColor - baseFontSize = max(17.0, presentationInterfaceState.fontSize.baseDisplaySize) + baseFontSize = max(minInputFontSize, presentationInterfaceState.fontSize.baseDisplaySize) } let cleanReplacementString = textAttributedStringForStateText(NSAttributedString(string: cleanText), fontSize: baseFontSize, textColor: textColor, accentTextColor: accentTextColor, writingDirection: nil) string.replaceCharacters(in: range, with: cleanReplacementString) @@ -2009,6 +2188,13 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { self.sendMessage() } + @objc func menuButtonPressed() { + self.hapticFeedback.impact(.light) + self.interfaceInteraction?.updateShowCommands { value in + return !value + } + } + @objc func attachmentButtonPressed() { self.displayAttachmentMenu() } @@ -2154,5 +2340,46 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { } return nil } + + func makeSnapshotForTransition() -> ChatMessageTransitionNode.Source.TextInput? { + guard let backgroundImage = self.transparentTextInputBackgroundImage else { + return nil + } + guard let textInputNode = self.textInputNode else { + return nil + } + + let backgroundView = UIImageView(image: backgroundImage) + backgroundView.frame = self.textInputBackgroundNode.frame + + func updateIsCaretHidden(view: UIView, isHidden: Bool) { + return; + if String(describing: type(of: view)).contains("TextSelectionView") { + view.isHidden = isHidden + } else { + for subview in view.subviews { + updateIsCaretHidden(view: subview, isHidden: isHidden) + } + } + } + + updateIsCaretHidden(view: textInputNode.view, isHidden: true) + + guard let contentView = textInputNode.view.snapshotView(afterScreenUpdates: true) else { + updateIsCaretHidden(view: textInputNode.view, isHidden: false) + return nil + } + + updateIsCaretHidden(view: textInputNode.view, isHidden: false) + + contentView.frame = textInputNode.frame + + return ChatMessageTransitionNode.Source.TextInput( + backgroundView: backgroundView, + contentView: contentView, + sourceRect: self.view.convert(self.bounds, to: nil), + scrollOffset: textInputNode.textView.contentOffset.y + ) + } } diff --git a/submodules/TelegramUI/Sources/ChatTimerScreen.swift b/submodules/TelegramUI/Sources/ChatTimerScreen.swift index 2ccb917a2b..b229c1e287 100644 --- a/submodules/TelegramUI/Sources/ChatTimerScreen.swift +++ b/submodules/TelegramUI/Sources/ChatTimerScreen.swift @@ -101,7 +101,7 @@ final class ChatTimerScreen: 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.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition) } } @@ -401,10 +401,16 @@ class ChatTimerScreenNode: ViewControllerTracingNode, UIScrollViewDelegate, UIPi self.dimNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) let offset = self.bounds.size.height - self.contentBackgroundNode.frame.minY - let dimPosition = self.dimNode.layer.position - self.dimNode.layer.animatePosition(from: CGPoint(x: dimPosition.x, y: dimPosition.y - offset), to: dimPosition, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring) - self.layer.animateBoundsOriginYAdditive(from: -offset, to: 0.0, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring) + + let transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring) + let targetBounds = self.bounds + self.bounds = self.bounds.offsetBy(dx: 0.0, dy: -offset) + self.dimNode.position = CGPoint(x: dimPosition.x, y: dimPosition.y - offset) + transition.animateView({ + self.bounds = targetBounds + self.dimNode.position = dimPosition + }) } func animateOut(completion: (() -> Void)? = nil) { diff --git a/submodules/TelegramUI/Sources/ChatTitleAccessoryPanelNode.swift b/submodules/TelegramUI/Sources/ChatTitleAccessoryPanelNode.swift index 7fa5e72cd4..1f26975cf2 100644 --- a/submodules/TelegramUI/Sources/ChatTitleAccessoryPanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatTitleAccessoryPanelNode.swift @@ -4,9 +4,14 @@ import Display import AsyncDisplayKit class ChatTitleAccessoryPanelNode: ASDisplayNode { + struct LayoutResult { + var backgroundHeight: CGFloat + var insetHeight: CGFloat + } + var interfaceInteraction: ChatPanelInterfaceInteraction? - func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) -> CGFloat { - return 0.0 + func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) -> LayoutResult { + preconditionFailure() } } diff --git a/submodules/TelegramUI/Sources/ChatTitleView.swift b/submodules/TelegramUI/Sources/ChatTitleView.swift index fc9cefb143..f55c4a9b95 100644 --- a/submodules/TelegramUI/Sources/ChatTitleView.swift +++ b/submodules/TelegramUI/Sources/ChatTitleView.swift @@ -18,6 +18,9 @@ import PhoneNumberFormat import ChatTitleActivityNode import AnimatedCountLabelNode +private let titleFont = Font.with(size: 17.0, design: .regular, weight: .semibold, traits: [.monospacedNumbers]) +private let subtitleFont = Font.regular(13.0) + enum ChatTitleContent { enum ReplyThreadType { case comments @@ -111,24 +114,24 @@ final class ChatTitleView: UIView, NavigationBarTitleView { case let .peer(peerView, _, isScheduledMessages): if peerView.peerId.isReplies { let typeText: String = self.strings.DialogList_Replies - segments = [.text(0, NSAttributedString(string: typeText, font: Font.medium(17.0), textColor: titleTheme.rootController.navigationBar.primaryTextColor))] + segments = [.text(0, NSAttributedString(string: typeText, font: titleFont, textColor: titleTheme.rootController.navigationBar.primaryTextColor))] isEnabled = false } else if isScheduledMessages { if peerView.peerId == self.account.peerId { - segments = [.text(0, NSAttributedString(string: self.strings.ScheduledMessages_RemindersTitle, font: Font.medium(17.0), textColor: titleTheme.rootController.navigationBar.primaryTextColor))] + segments = [.text(0, NSAttributedString(string: self.strings.ScheduledMessages_RemindersTitle, font: titleFont, textColor: titleTheme.rootController.navigationBar.primaryTextColor))] } else { - segments = [.text(0, NSAttributedString(string: self.strings.ScheduledMessages_Title, font: Font.medium(17.0), textColor: titleTheme.rootController.navigationBar.primaryTextColor))] + segments = [.text(0, NSAttributedString(string: self.strings.ScheduledMessages_Title, font: titleFont, textColor: titleTheme.rootController.navigationBar.primaryTextColor))] } isEnabled = false } else { if let peer = peerViewMainPeer(peerView) { if peerView.peerId == self.account.peerId { - segments = [.text(0, NSAttributedString(string: self.strings.Conversation_SavedMessages, font: Font.medium(17.0), textColor: titleTheme.rootController.navigationBar.primaryTextColor))] + segments = [.text(0, NSAttributedString(string: self.strings.Conversation_SavedMessages, font: titleFont, textColor: titleTheme.rootController.navigationBar.primaryTextColor))] } else { if !peerView.peerIsContact, let user = peer as? TelegramUser, !user.flags.contains(.isSupport), user.botInfo == nil, let phone = user.phone, !phone.isEmpty { - segments = [.text(0, NSAttributedString(string: formatPhoneNumber(phone), font: Font.medium(17.0), textColor: titleTheme.rootController.navigationBar.primaryTextColor))] + segments = [.text(0, NSAttributedString(string: formatPhoneNumber(phone), font: titleFont, textColor: titleTheme.rootController.navigationBar.primaryTextColor))] } else { - segments = [.text(0, NSAttributedString(string: peer.displayTitle(strings: self.strings, displayOrder: self.nameDisplayOrder), font: Font.medium(17.0), textColor: titleTheme.rootController.navigationBar.primaryTextColor))] + segments = [.text(0, NSAttributedString(string: peer.displayTitle(strings: self.strings, displayOrder: self.nameDisplayOrder), font: titleFont, textColor: titleTheme.rootController.navigationBar.primaryTextColor))] } } if peer.isFake { @@ -147,7 +150,7 @@ final class ChatTitleView: UIView, NavigationBarTitleView { } } case let .replyThread(type, count): - let textFont = Font.medium(17.0) + let textFont = titleFont let textColor = titleTheme.rootController.navigationBar.primaryTextColor if count > 0 { @@ -215,8 +218,7 @@ final class ChatTitleView: UIView, NavigationBarTitleView { isEnabled = false case let .custom(text, _, enabled): - let font = Font.with(size: 17.0, design: .regular, weight: .medium, traits: .monospacedNumbers) - segments = [.text(0, NSAttributedString(string: text, font: font, textColor: titleTheme.rootController.navigationBar.primaryTextColor))] + segments = [.text(0, NSAttributedString(string: text, font: titleFont, textColor: titleTheme.rootController.navigationBar.primaryTextColor))] isEnabled = enabled } @@ -306,7 +308,7 @@ final class ChatTitleView: UIView, NavigationBarTitleView { case .online: infoText = "" } - state = .info(NSAttributedString(string: infoText, font: Font.regular(13.0), textColor: titleTheme.rootController.navigationBar.secondaryTextColor), .generic) + state = .info(NSAttributedString(string: infoText, font: subtitleFont, textColor: titleTheme.rootController.navigationBar.secondaryTextColor), .generic) case .online: if let (peerId, inputActivities) = self.inputActivities, !inputActivities.isEmpty, inputActivitiesAllowed { var stringValue = "" @@ -353,7 +355,7 @@ final class ChatTitleView: UIView, NavigationBarTitleView { } } let color = titleTheme.rootController.navigationBar.accentTextColor - let string = NSAttributedString(string: stringValue, font: Font.regular(13.0), textColor: color) + let string = NSAttributedString(string: stringValue, font: subtitleFont, textColor: color) switch mergedActivity { case .typingText: state = .typingText(string, color) @@ -375,23 +377,23 @@ final class ChatTitleView: UIView, NavigationBarTitleView { if let peer = peerViewMainPeer(peerView) { let servicePeer = isServicePeer(peer) if peer.id == self.account.peerId || isScheduledMessages || peer.id.isReplies { - let string = NSAttributedString(string: "", font: Font.regular(13.0), textColor: titleTheme.rootController.navigationBar.secondaryTextColor) + let string = NSAttributedString(string: "", font: subtitleFont, textColor: titleTheme.rootController.navigationBar.secondaryTextColor) state = .info(string, .generic) } else if let user = peer as? TelegramUser { if user.isDeleted { state = .none } else if servicePeer { - let string = NSAttributedString(string: "", font: Font.regular(13.0), textColor: titleTheme.rootController.navigationBar.secondaryTextColor) + let string = NSAttributedString(string: "", font: subtitleFont, textColor: titleTheme.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: titleTheme.rootController.navigationBar.secondaryTextColor) + let string = NSAttributedString(string: statusText, font: subtitleFont, textColor: titleTheme.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: titleTheme.rootController.navigationBar.secondaryTextColor) + let string = NSAttributedString(string: statusText, font: subtitleFont, textColor: titleTheme.rootController.navigationBar.secondaryTextColor) state = .info(string, .generic) } else if let peer = peerViewMainPeer(peerView) { let timestamp = CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970 @@ -403,10 +405,10 @@ final class ChatTitleView: UIView, NavigationBarTitleView { 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 ? titleTheme.rootController.navigationBar.accentTextColor : titleTheme.rootController.navigationBar.secondaryTextColor) + let attributedString = NSAttributedString(string: string, font: subtitleFont, textColor: activity ? titleTheme.rootController.navigationBar.accentTextColor : titleTheme.rootController.navigationBar.secondaryTextColor) state = .info(attributedString, activity ? .online : .lastSeenTime) } else { - let string = NSAttributedString(string: "", font: Font.regular(13.0), textColor: titleTheme.rootController.navigationBar.secondaryTextColor) + let string = NSAttributedString(string: "", font: subtitleFont, textColor: titleTheme.rootController.navigationBar.secondaryTextColor) state = .info(string, .generic) } } else if let group = peer as? TelegramGroup { @@ -428,11 +430,11 @@ final class ChatTitleView: UIView, NavigationBarTitleView { if onlineCount > 1 { let string = NSMutableAttributedString() - string.append(NSAttributedString(string: "\(strings.Conversation_StatusMembers(Int32(group.participantCount))), ", font: Font.regular(13.0), textColor: titleTheme.rootController.navigationBar.secondaryTextColor)) - string.append(NSAttributedString(string: strings.Conversation_StatusOnline(Int32(onlineCount)), font: Font.regular(13.0), textColor: titleTheme.rootController.navigationBar.secondaryTextColor)) + string.append(NSAttributedString(string: "\(strings.Conversation_StatusMembers(Int32(group.participantCount))), ", font: subtitleFont, textColor: titleTheme.rootController.navigationBar.secondaryTextColor)) + string.append(NSAttributedString(string: strings.Conversation_StatusOnline(Int32(onlineCount)), font: subtitleFont, textColor: titleTheme.rootController.navigationBar.secondaryTextColor)) state = .info(string, .generic) } else { - let string = NSAttributedString(string: strings.Conversation_StatusMembers(Int32(group.participantCount)), font: Font.regular(13.0), textColor: titleTheme.rootController.navigationBar.secondaryTextColor) + let string = NSAttributedString(string: strings.Conversation_StatusMembers(Int32(group.participantCount)), font: subtitleFont, textColor: titleTheme.rootController.navigationBar.secondaryTextColor) state = .info(string, .generic) } } else if let channel = peer as? TelegramChannel { @@ -440,17 +442,17 @@ final class ChatTitleView: UIView, NavigationBarTitleView { if memberCount == 0 { let string: NSAttributedString if case .group = channel.info { - string = NSAttributedString(string: strings.Group_Status, font: Font.regular(13.0), textColor: titleTheme.rootController.navigationBar.secondaryTextColor) + string = NSAttributedString(string: strings.Group_Status, font: subtitleFont, textColor: titleTheme.rootController.navigationBar.secondaryTextColor) } else { - string = NSAttributedString(string: strings.Channel_Status, font: Font.regular(13.0), textColor: titleTheme.rootController.navigationBar.secondaryTextColor) + string = NSAttributedString(string: strings.Channel_Status, font: subtitleFont, textColor: titleTheme.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: titleTheme.rootController.navigationBar.secondaryTextColor)) - string.append(NSAttributedString(string: strings.Conversation_StatusOnline(Int32(onlineMemberCount)), font: Font.regular(13.0), textColor: titleTheme.rootController.navigationBar.secondaryTextColor)) + string.append(NSAttributedString(string: "\(strings.Conversation_StatusMembers(Int32(memberCount))), ", font: subtitleFont, textColor: titleTheme.rootController.navigationBar.secondaryTextColor)) + string.append(NSAttributedString(string: strings.Conversation_StatusOnline(Int32(onlineMemberCount)), font: subtitleFont, textColor: titleTheme.rootController.navigationBar.secondaryTextColor)) state = .info(string, .generic) } else { let membersString: String @@ -459,24 +461,24 @@ final class ChatTitleView: UIView, NavigationBarTitleView { } else { membersString = strings.Conversation_StatusSubscribers(memberCount) } - let string = NSAttributedString(string: membersString, font: Font.regular(13.0), textColor: titleTheme.rootController.navigationBar.secondaryTextColor) + let string = NSAttributedString(string: membersString, font: subtitleFont, textColor: titleTheme.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: titleTheme.rootController.navigationBar.secondaryTextColor) + let string = NSAttributedString(string: strings.Group_Status, font: subtitleFont, textColor: titleTheme.rootController.navigationBar.secondaryTextColor) state = .info(string, .generic) case .broadcast: - let string = NSAttributedString(string: strings.Channel_Status, font: Font.regular(13.0), textColor: titleTheme.rootController.navigationBar.secondaryTextColor) + let string = NSAttributedString(string: strings.Channel_Status, font: subtitleFont, textColor: titleTheme.rootController.navigationBar.secondaryTextColor) state = .info(string, .generic) } } } } case let .custom(_, subtitle?, _): - let string = NSAttributedString(string: subtitle, font: Font.regular(13.0), textColor: titleTheme.rootController.navigationBar.secondaryTextColor) + let string = NSAttributedString(string: subtitle, font: subtitleFont, textColor: titleTheme.rootController.navigationBar.secondaryTextColor) state = .info(string, .generic) default: break diff --git a/submodules/TelegramUI/Sources/ChatToastAlertPanelNode.swift b/submodules/TelegramUI/Sources/ChatToastAlertPanelNode.swift index 66eea3e6ab..a29e529ead 100644 --- a/submodules/TelegramUI/Sources/ChatToastAlertPanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatToastAlertPanelNode.swift @@ -34,23 +34,22 @@ final class ChatToastAlertPanelNode: ChatTitleAccessoryPanelNode { self.titleNode.insets = UIEdgeInsets(top: 2.0, left: 2.0, bottom: 2.0, right: 2.0) super.init() - + self.addSubnode(self.titleNode) self.addSubnode(self.separatorNode) } - override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) -> CGFloat { + override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) -> LayoutResult { let panelHeight: CGFloat = 40.0 self.textColor = interfaceState.theme.rootController.navigationBar.primaryTextColor - self.backgroundColor = interfaceState.theme.chat.historyNavigation.fillColor self.separatorNode.backgroundColor = interfaceState.theme.chat.historyNavigation.strokeColor - transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: panelHeight - UIScreenPixel), size: CGSize(width: width, height: UIScreenPixel))) + transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: width, height: UIScreenPixel))) let titleSize = self.titleNode.updateLayout(CGSize(width: width - leftInset - rightInset - 20.0, height: 100.0)) self.titleNode.frame = CGRect(origin: CGPoint(x: floor((width - titleSize.width) / 2.0), y: floor((panelHeight - titleSize.height) / 2.0)), size: titleSize) - return panelHeight + return LayoutResult(backgroundHeight: panelHeight, insetHeight: panelHeight) } } diff --git a/submodules/TelegramUI/Sources/ChatUnreadItem.swift b/submodules/TelegramUI/Sources/ChatUnreadItem.swift index e36c4d07fa..23a2f37304 100644 --- a/submodules/TelegramUI/Sources/ChatUnreadItem.swift +++ b/submodules/TelegramUI/Sources/ChatUnreadItem.swift @@ -147,9 +147,9 @@ class ChatUnreadItemNode: ListViewItemNode { } } - override public func header() -> ListViewItemHeader? { + override public func headers() -> [ListViewItemHeader]? { if let item = self.item { - return item.header + return [item.header] } else { return nil } diff --git a/submodules/TelegramUI/Sources/CommandChatInputContextPanelNode.swift b/submodules/TelegramUI/Sources/CommandChatInputContextPanelNode.swift index abecef6dfa..a215268547 100644 --- a/submodules/TelegramUI/Sources/CommandChatInputContextPanelNode.swift +++ b/submodules/TelegramUI/Sources/CommandChatInputContextPanelNode.swift @@ -174,7 +174,10 @@ final class CommandChatInputContextPanelNode: ChatInputContextPanelNode { if let topItemOffset = topItemOffset { let position = strongSelf.listView.layer.position - strongSelf.listView.layer.animatePosition(from: CGPoint(x: position.x, y: position.y + (strongSelf.listView.bounds.size.height - topItemOffset)), to: position, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring) + strongSelf.listView.position = CGPoint(x: position.x, y: position.y + (strongSelf.listView.bounds.size.height - topItemOffset)) + ContainedViewLayoutTransition.animated(duration: 0.3, curve: .spring).animateView { + strongSelf.listView.position = position + } } } }) @@ -239,4 +242,14 @@ final class CommandChatInputContextPanelNode: ChatInputContextPanelNode { let listViewFrame = self.listView.frame return self.listView.hitTest(CGPoint(x: point.x - listViewFrame.minX, y: point.y - listViewFrame.minY), with: event) } + + override var topItemFrame: CGRect? { + var topItemFrame: CGRect? + self.listView.forEachItemNode { itemNode in + if topItemFrame == nil { + topItemFrame = itemNode.frame + } + } + return topItemFrame + } } diff --git a/submodules/TelegramUI/Sources/CommandMenuChatInputContextPanelNode.swift b/submodules/TelegramUI/Sources/CommandMenuChatInputContextPanelNode.swift new file mode 100644 index 0000000000..dfd628d004 --- /dev/null +++ b/submodules/TelegramUI/Sources/CommandMenuChatInputContextPanelNode.swift @@ -0,0 +1,273 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Postbox +import SwiftSignalKit +import TelegramCore +import SyncCore +import Display +import TelegramPresentationData +import TelegramUIPreferences +import MergeLists +import AccountContext + +private struct CommandMenuChatInputContextPanelEntryStableId: Hashable { + let command: PeerCommand +} + +private struct CommandMenuChatInputContextPanelEntry: Comparable, Identifiable { + let index: Int + let command: PeerCommand + let theme: PresentationTheme + + var stableId: CommandMenuChatInputContextPanelEntryStableId { + return CommandMenuChatInputContextPanelEntryStableId(command: self.command) + } + + func withUpdatedTheme(_ theme: PresentationTheme) -> CommandMenuChatInputContextPanelEntry { + return CommandMenuChatInputContextPanelEntry(index: self.index, command: self.command, theme: theme) + } + + static func ==(lhs: CommandMenuChatInputContextPanelEntry, rhs: CommandMenuChatInputContextPanelEntry) -> Bool { + return lhs.index == rhs.index && lhs.command == rhs.command && lhs.theme === rhs.theme + } + + static func <(lhs: CommandMenuChatInputContextPanelEntry, rhs: CommandMenuChatInputContextPanelEntry) -> Bool { + return lhs.index < rhs.index + } + + func item(context: AccountContext, fontSize: PresentationFontSize, commandSelected: @escaping (PeerCommand, Bool) -> Void) -> ListViewItem { + return CommandMenuChatInputPanelItem(context: context, theme: self.theme, fontSize: fontSize, command: self.command, commandSelected: commandSelected) + } +} + +private struct CommandMenuChatInputContextPanelTransition { + let deletions: [ListViewDeleteItem] + let insertions: [ListViewInsertItem] + let updates: [ListViewUpdateItem] +} + +private func preparedTransition(from fromEntries: [CommandMenuChatInputContextPanelEntry], to toEntries: [CommandMenuChatInputContextPanelEntry], context: AccountContext, fontSize: PresentationFontSize, commandSelected: @escaping (PeerCommand, Bool) -> Void) -> CommandMenuChatInputContextPanelTransition { + 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, 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 CommandMenuChatInputContextPanelTransition(deletions: deletions, insertions: insertions, updates: updates) +} + +final class CommandMenuChatInputContextPanelNode: ChatInputContextPanelNode { + private let listView: ListView + private var currentEntries: [CommandMenuChatInputContextPanelEntry]? + + private var enqueuedTransitions: [(CommandMenuChatInputContextPanelTransition, Bool)] = [] + private var validLayout: (CGSize, CGFloat, CGFloat, CGFloat)? + + private let disposable = MetaDisposable() + + init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, fontSize: PresentationFontSize, peerId: PeerId) { + self.listView = ListView() + self.listView.clipsToBounds = false + self.listView.isOpaque = false + self.listView.stackFromBottom = true +// self.listView.keepBottomItemOverscrollBackground = theme.list.plainBackgroundColor + self.listView.limitHitTestToNodes = true + self.listView.view.disablesInteractiveTransitionGestureRecognizer = true + self.listView.accessibilityPageScrolledString = { row, count in + return strings.VoiceOver_ScrollStatus(row, count).0 + } + + super.init(context: context, theme: theme, strings: strings, fontSize: fontSize) + + self.isOpaque = false + self.clipsToBounds = true + + self.addSubnode(self.listView) + + self.disposable.set((context.engine.peers.peerCommands(id: peerId) + |> deliverOnMainQueue).start(next: { [weak self] results in + if let strongSelf = self { + strongSelf.updateResults(results.commands) + } + })) + } + + deinit { + self.disposable.dispose() + } + + func updateResults(_ results: [PeerCommand]) { + var entries: [CommandMenuChatInputContextPanelEntry] = [] + var index = 0 + var stableIds = Set() + for command in results { + let entry = CommandMenuChatInputContextPanelEntry(index: index, command: command, theme: self.theme) + if stableIds.contains(entry.stableId) { + continue + } + stableIds.insert(entry.stableId) + entries.append(entry) + index += 1 + } + self.prepareTransition(from: self.currentEntries ?? [], to: entries) + } + + private func prepareTransition(from: [CommandMenuChatInputContextPanelEntry]? , to: [CommandMenuChatInputContextPanelEntry]) { + let firstTime = self.currentEntries == nil + 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) + } else { + interfaceInteraction.updateShowCommands { _ in return false } + interfaceInteraction.updateTextInputStateAndMode { textInputState, inputMode in + var commandQueryRange: NSRange? + inner: for (range, type, _) in textInputStateContextQueryRangeAndType(textInputState) { + if type == [.command] { + commandQueryRange = range + break inner + } + } + if let range = commandQueryRange { + let inputText = NSMutableAttributedString(attributedString: textInputState.inputText) + + let replacementText = command.command.text + " " + inputText.replaceCharacters(in: range, with: replacementText) + + let selectionPosition = range.lowerBound + (replacementText as NSString).length + + return (ChatTextInputState(inputText: inputText, selectionRange: selectionPosition ..< selectionPosition), inputMode) + } else { + let inputText = NSMutableAttributedString(string: "/" + command.command.text + " ") + let selectionPosition = (inputText.string as NSString).length + 1 + return (ChatTextInputState(inputText: inputText, selectionRange: selectionPosition ..< selectionPosition), inputMode) + } + } + } + } + }) + self.currentEntries = to + self.enqueueTransition(transition, firstTime: firstTime) + } + + private func enqueueTransition(_ transition: CommandMenuChatInputContextPanelTransition, firstTime: Bool) { + enqueuedTransitions.append((transition, firstTime)) + + if self.validLayout != nil { + while !self.enqueuedTransitions.isEmpty { + self.dequeueTransition() + } + } + } + + private func dequeueTransition() { + if let validLayout = self.validLayout, let (transition, firstTime) = self.enqueuedTransitions.first { + self.enqueuedTransitions.remove(at: 0) + + 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) + insets.left = validLayout.1 + insets.right = validLayout.2 + + let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: self.listView.bounds.size, insets: insets, duration: 0.0, curve: .Default(duration: nil)) + + self.listView.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, updateSizeAndInsets: updateSizeAndInsets, updateOpaqueState: nil, completion: { [weak self] _ in + if let strongSelf = self, firstTime { + var topItemOffset: CGFloat? + strongSelf.listView.forEachItemNode { itemNode in + if topItemOffset == nil { + topItemOffset = itemNode.frame.minY + } + } + + if let topItemOffset = topItemOffset { + let position = strongSelf.listView.layer.position + strongSelf.listView.position = CGPoint(x: position.x, y: position.y + (strongSelf.listView.bounds.size.height - topItemOffset)) + ContainedViewLayoutTransition.animated(duration: 0.3, curve: .spring).animateView { + strongSelf.listView.position = position + } + } + } + }) + } + } + + private func topInsetForLayout(size: CGSize) -> CGFloat { + let minimumItemHeights: CGFloat = floor(MentionChatInputPanelItemNode.itemHeight * 4.7) + return max(size.height - minimumItemHeights, 0.0) + } + + 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) + + var insets = UIEdgeInsets() + insets.top = self.topInsetForLayout(size: size) + 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)) + + 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 }) + + if !hadValidLayout { + while !self.enqueuedTransitions.isEmpty { + self.dequeueTransition() + } + } + + if self.theme !== interfaceState.theme { + 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) + } + } + + override func animateOut(completion: @escaping () -> Void) { + var topItemOffset: CGFloat? + self.listView.forEachItemNode { itemNode in + if topItemOffset == nil { + topItemOffset = itemNode.frame.minY + } + } + + if let topItemOffset = topItemOffset { + let position = self.listView.layer.position + self.listView.layer.animatePosition(from: position, to: CGPoint(x: position.x, y: position.y + (self.listView.bounds.size.height - topItemOffset)), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in + completion() + }) + } else { + completion() + } + } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + let listViewFrame = self.listView.frame + return self.listView.hitTest(CGPoint(x: point.x - listViewFrame.minX, y: point.y - listViewFrame.minY), with: event) + } + + override var topItemFrame: CGRect? { + var topItemFrame: CGRect? + self.listView.forEachItemNode { itemNode in + if topItemFrame == nil { + topItemFrame = itemNode.frame + } + } + return topItemFrame + } +} diff --git a/submodules/TelegramUI/Sources/CommandMenuChatInputPanelItem.swift b/submodules/TelegramUI/Sources/CommandMenuChatInputPanelItem.swift new file mode 100644 index 0000000000..6f7c52ec01 --- /dev/null +++ b/submodules/TelegramUI/Sources/CommandMenuChatInputPanelItem.swift @@ -0,0 +1,273 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display +import TelegramCore +import SyncCore +import SwiftSignalKit +import Postbox +import TelegramPresentationData +import TelegramUIPreferences +import AccountContext + +final class CommandMenuChatInputPanelItem: ListViewItem { + 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(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 + } + + public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) { + let configure = { () -> Void in + let node = CommandMenuChatInputPanelItemNode() + + let nodeLayout = node.asyncLayout() + let (top, bottom) = (previousItem != nil, nextItem != nil) + let (layout, apply) = nodeLayout(self, params, top, bottom) + + node.contentSize = layout.contentSize + node.insets = layout.insets + + Queue.mainQueue().async { + completion(node, { + return (nil, { _ in apply(.None) }) + }) + } + } + if Thread.isMainThread { + async { + configure() + } + } else { + configure() + } + } + + 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? CommandMenuChatInputPanelItemNode { + let nodeLayout = nodeValue.asyncLayout() + + async { + let (top, bottom) = (previousItem != nil, nextItem != nil) + + let (layout, apply) = nodeLayout(self, params, top, bottom) + Queue.mainQueue().async { + completion(layout, { _ in + apply(animation) + }) + } + } + } else { + assertionFailure() + } + } + } + + func selected(listView: ListView) { + self.commandSelected(self.command, true) + } +} + +private extension String { + func capitalizeFirstLetter() -> String { + return self.prefix(1).capitalized + self.dropFirst() + } +} + +private let backgroundCornerRadius: CGFloat = 10.0 +private let shadowBlur: CGFloat = 10.0 + +let shadowImage = generateImage(CGSize(width: (backgroundCornerRadius + shadowBlur) * 2.0, height: backgroundCornerRadius + shadowBlur), rotatedContext: { size, context in + let diameter = backgroundCornerRadius * 2.0 + let shadow = UIColor(white: 0.0, alpha: 0.5) + context.clear(CGRect(origin: CGPoint(), size: size)) + + context.saveGState() + context.setFillColor(shadow.cgColor) + context.setShadow(offset: CGSize(), blur: shadowBlur, color: shadow.cgColor) + + context.fillEllipse(in: CGRect(origin: CGPoint(x: shadowBlur, y: shadowBlur), size: CGSize(width: diameter, height: diameter))) + + context.setFillColor(UIColor.clear.cgColor) + context.setBlendMode(.copy) + + context.fillEllipse(in: CGRect(origin: CGPoint(x: shadowBlur, y: shadowBlur), size: CGSize(width: diameter, height: diameter))) + + context.restoreGState() +})?.stretchableImage(withLeftCapWidth: Int(backgroundCornerRadius + shadowBlur), topCapHeight: 0) + +final class CommandMenuChatInputPanelItemNode: ListViewItemNode { + static let itemHeight: CGFloat = 44.0 + + private var item: CommandMenuChatInputPanelItem? + private let textNode: TextNode + private let commandNode: TextNode + private let separatorNode: ASDisplayNode + private let clippingNode: ASDisplayNode + private let shadowNode: ASImageNode + private let backgroundNode: ASDisplayNode + private let highlightedBackgroundNode: ASDisplayNode + + init() { + self.textNode = TextNode() + self.commandNode = TextNode() + + self.separatorNode = ASDisplayNode() + self.separatorNode.isLayerBacked = true + + self.highlightedBackgroundNode = ASDisplayNode() + self.highlightedBackgroundNode.isLayerBacked = true + + self.clippingNode = ASDisplayNode() + self.clippingNode.clipsToBounds = true + + self.shadowNode = ASImageNode() + self.shadowNode.displaysAsynchronously = false + self.shadowNode.contentMode = .scaleToFill + self.shadowNode.image = shadowImage + + self.backgroundNode = ASDisplayNode() + self.backgroundNode.clipsToBounds = true + + super.init(layerBacked: false, dynamicBounce: false) + + self.addSubnode(self.clippingNode) + self.clippingNode.addSubnode(self.shadowNode) + self.clippingNode.addSubnode(self.backgroundNode) + + self.backgroundNode.addSubnode(self.textNode) + self.backgroundNode.addSubnode(self.commandNode) + self.backgroundNode.addSubnode(self.separatorNode) + } + + override func didLoad() { + super.didLoad() + + let gestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(self.longPressed(_:))) + gestureRecognizer.minimumPressDuration = 0.3 + self.view.addGestureRecognizer(gestureRecognizer) + } + + @objc private func longPressed(_ gestureRecognizer: UILongPressGestureRecognizer) { + switch gestureRecognizer.state { + case .began: + if let item = self.item { + item.commandSelected(item.command, false) + } + default: + break + } + } + + override func layoutForParams(_ params: ListViewItemLayoutParams, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) { + if let item = item as? CommandMenuChatInputPanelItem { + let doLayout = self.asyncLayout() + let merged = (top: previousItem != nil, bottom: nextItem != nil) + let (layout, apply) = doLayout(item, params, merged.top, merged.bottom) + self.contentSize = layout.contentSize + self.insets = layout.insets + apply(.None) + } + } + + func asyncLayout() -> (_ item: CommandMenuChatInputPanelItem, _ params: ListViewItemLayoutParams, _ mergedTop: Bool, _ mergedBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation) -> Void) { + let makeTextLayout = TextNode.asyncLayout(self.textNode) + let makeCommandLayout = TextNode.asyncLayout(self.commandNode) + + return { [weak self] item, params, mergedTop, mergedBottom in + let textFont = Font.regular(floor(item.fontSize.baseDisplaySize)) + let commandFont = Font.regular(floor(item.fontSize.baseDisplaySize * 14.0 / 17.0)) + + let leftInset: CGFloat = 16.0 + params.leftInset + let rightInset: CGFloat = 16.0 + params.rightInset + + let textString: NSAttributedString + let commandString: NSAttributedString + if item.command.command.description.isEmpty { + textString = NSAttributedString(string: item.command.command.text.capitalizeFirstLetter(), font: textFont, textColor: item.theme.list.itemPrimaryTextColor) + commandString = NSAttributedString(string: "/" + item.command.command.text, font: commandFont, textColor: item.theme.list.itemSecondaryTextColor) + } else { + textString = NSAttributedString(string: item.command.command.description.capitalizeFirstLetter(), font: textFont, textColor: item.theme.list.itemPrimaryTextColor) + commandString = NSAttributedString(string: "/" + item.command.command.text, font: commandFont, textColor: item.theme.list.itemSecondaryTextColor) + } + + let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: textString, backgroundColor: nil, maximumNumberOfLines: 2, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - rightInset - 130.0, height: 100.0), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + + let (commandLayout, commandApply) = makeCommandLayout(TextNodeLayoutArguments(attributedString: commandString, backgroundColor: nil, maximumNumberOfLines: 2, truncationType: .end, constrainedSize: CGSize(width: 120.0, height: 100.0), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + + let nodeLayout = ListViewItemNodeLayout(contentSize: CGSize(width: params.width, height: max(CommandMenuChatInputPanelItemNode.itemHeight, textLayout.size.height + 14.0)), insets: UIEdgeInsets()) + + return (nodeLayout, { _ in + if let strongSelf = self { + strongSelf.item = item + + strongSelf.separatorNode.backgroundColor = item.theme.list.itemPlainSeparatorColor + strongSelf.backgroundNode.backgroundColor = item.theme.list.plainBackgroundColor + strongSelf.highlightedBackgroundNode.backgroundColor = item.theme.list.itemHighlightedBackgroundColor + + let _ = textApply() + let _ = commandApply() + + strongSelf.textNode.frame = CGRect(origin: CGPoint(x: leftInset, y: floor((nodeLayout.contentSize.height - textLayout.size.height) / 2.0)), size: textLayout.size) + strongSelf.commandNode.frame = CGRect(origin: CGPoint(x: params.width - rightInset - commandLayout.size.width, y: floor((nodeLayout.contentSize.height - commandLayout.size.height) / 2.0)), size: commandLayout.size) + + strongSelf.separatorNode.isHidden = !mergedBottom + + 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)) + + if !mergedTop { + strongSelf.shadowNode.isHidden = false + strongSelf.shadowNode.frame = CGRect(origin: CGPoint(x: -shadowBlur, y: 0.0), size: CGSize(width: nodeLayout.size.width + shadowBlur * 2.0, height: backgroundCornerRadius + shadowBlur)) + strongSelf.clippingNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -shadowBlur), size: CGSize(width: nodeLayout.size.width, height: nodeLayout.size.height + shadowBlur)) + strongSelf.backgroundNode.cornerRadius = backgroundCornerRadius + strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: shadowBlur), size: CGSize(width: nodeLayout.size.width, height: nodeLayout.size.height + backgroundCornerRadius)) + } else { + strongSelf.shadowNode.isHidden = true + strongSelf.clippingNode.frame = CGRect(origin: CGPoint(), size: nodeLayout.size) + strongSelf.backgroundNode.cornerRadius = 0.0 + strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(), size: nodeLayout.size) + } + } + }) + } + } + + 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 { + self.backgroundNode.insertSubnode(self.highlightedBackgroundNode, at: 0) + } + } 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() + } + } + } + } +} diff --git a/submodules/TelegramUI/Sources/ComposeController.swift b/submodules/TelegramUI/Sources/ComposeController.swift index 584bcbe4e6..cf1f47b27f 100644 --- a/submodules/TelegramUI/Sources/ComposeController.swift +++ b/submodules/TelegramUI/Sources/ComposeController.swift @@ -157,11 +157,11 @@ public class ComposeControllerImpl: ViewController, ComposeController { let controller = ContactSelectionControllerImpl(ContactSelectionControllerParams(context: strongSelf.context, autoDismiss: false, title: { $0.Compose_NewEncryptedChatTitle })) strongSelf.createActionDisposable.set((controller.result |> take(1) - |> deliverOnMainQueue).start(next: { [weak controller] peer in - if let strongSelf = self, let (contactPeer, _) = peer, case let .peer(peer, _, _) = contactPeer { + |> deliverOnMainQueue).start(next: { [weak controller] result in + if let strongSelf = self, let (contactPeers, _) = result, case let .peer(peer, _, _) = contactPeers.first { controller?.dismissSearch() controller?.displayNavigationActivity = true - strongSelf.createActionDisposable.set((createSecretChat(account: strongSelf.context.account, peerId: peer.id) |> deliverOnMainQueue).start(next: { peerId in + strongSelf.createActionDisposable.set((strongSelf.context.engine.peers.createSecretChat(peerId: peer.id) |> deliverOnMainQueue).start(next: { peerId in if let strongSelf = self, let controller = controller { controller.displayNavigationActivity = false (controller.navigationController as? NavigationController)?.replaceAllButRootController(ChatControllerImpl(context: strongSelf.context, chatLocation: .peer(peerId)), animated: true) @@ -284,7 +284,7 @@ public class ComposeControllerImpl: ViewController, ComposeController { override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { super.containerLayoutUpdated(layout, transition: transition) - self.contactsNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationInsetHeight, actualNavigationBarHeight: self.navigationHeight, transition: transition) + self.contactsNode.containerLayoutUpdated(layout, navigationBarHeight: self.cleanNavigationHeight, actualNavigationBarHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition) } private func activateSearch() { diff --git a/submodules/TelegramUI/Sources/ContactMultiselectionController.swift b/submodules/TelegramUI/Sources/ContactMultiselectionController.swift index d3f55ee3e1..9f7af80a89 100644 --- a/submodules/TelegramUI/Sources/ContactMultiselectionController.swift +++ b/submodules/TelegramUI/Sources/ContactMultiselectionController.swift @@ -216,7 +216,7 @@ class ContactMultiselectionControllerImpl: ViewController, ContactMultiselection } override func loadDisplayNode() { - self.displayNode = ContactMultiselectionControllerNode(context: self.context, mode: self.mode, options: self.options, filters: self.filters) + self.displayNode = ContactMultiselectionControllerNode(navigationBar: self.navigationBar, context: self.context, mode: self.mode, options: self.options, filters: self.filters) switch self.contactsNode.contentNode { case let .contacts(contactsNode): self._listReady.set(contactsNode.ready) @@ -525,11 +525,31 @@ class ContactMultiselectionControllerImpl: ViewController, ContactMultiselection break } } + + private var suspendNavigationBarLayout: Bool = false + private var suspendedNavigationBarLayout: ContainerViewLayout? + private var additionalNavigationBarBackgroundHeight: CGFloat = 0.0 + + override public func updateNavigationBarLayout(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + if self.suspendNavigationBarLayout { + self.suspendedNavigationBarLayout = layout + return + } + self.applyNavigationBarLayout(layout, navigationLayout: self.navigationLayout(layout: layout), additionalBackgroundHeight: self.additionalNavigationBarBackgroundHeight, transition: transition) + } override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + self.suspendNavigationBarLayout = true + super.containerLayoutUpdated(layout, transition: transition) - self.contactsNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationHeight, actualNavigationBarHeight: self.navigationHeight, transition: transition) + self.additionalNavigationBarBackgroundHeight = self.contactsNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, actualNavigationBarHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition) + + self.suspendNavigationBarLayout = false + if let suspendedNavigationBarLayout = self.suspendedNavigationBarLayout { + self.suspendedNavigationBarLayout = suspendedNavigationBarLayout + self.applyNavigationBarLayout(suspendedNavigationBarLayout, navigationLayout: self.navigationLayout(layout: layout), additionalBackgroundHeight: self.additionalNavigationBarBackgroundHeight, transition: transition) + } } @objc func cancelPressed() { diff --git a/submodules/TelegramUI/Sources/ContactMultiselectionControllerNode.swift b/submodules/TelegramUI/Sources/ContactMultiselectionControllerNode.swift index 7c42ff4435..c96e0e47d2 100644 --- a/submodules/TelegramUI/Sources/ContactMultiselectionControllerNode.swift +++ b/submodules/TelegramUI/Sources/ContactMultiselectionControllerNode.swift @@ -43,6 +43,7 @@ enum ContactMultiselectionContentNode { } final class ContactMultiselectionControllerNode: ASDisplayNode { + private let navigationBar: NavigationBar? let contentNode: ContactMultiselectionContentNode let tokenListNode: EditableTokenListNode var searchResultsNode: ContactListNode? @@ -67,7 +68,9 @@ final class ContactMultiselectionControllerNode: ASDisplayNode { private var presentationData: PresentationData private var presentationDataDisposable: Disposable? - init(context: AccountContext, mode: ContactMultiselectionControllerMode, options: [ContactListAdditionalOption], filters: [ContactListFilter]) { + init(navigationBar: NavigationBar?, context: AccountContext, mode: ContactMultiselectionControllerMode, options: [ContactListAdditionalOption], filters: [ContactListFilter]) { + self.navigationBar = navigationBar + self.context = context let presentationData = context.sharedContext.currentPresentationData.with { $0 } self.presentationData = presentationData @@ -109,7 +112,7 @@ final class ContactMultiselectionControllerNode: ASDisplayNode { self.contentNode = .contacts(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.itemCheckColors.foregroundColor, selectedBackgroundColor: self.presentationData.theme.list.itemCheckColors.fillColor, accentColor: self.presentationData.theme.list.itemAccentColor, keyboardColor: self.presentationData.theme.rootController.keyboardColor), placeholder: placeholder) + self.tokenListNode = EditableTokenListNode(theme: EditableTokenListNodeTheme(backgroundColor: .clear, 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() @@ -120,7 +123,7 @@ final class ContactMultiselectionControllerNode: ASDisplayNode { self.backgroundColor = self.presentationData.theme.chatList.backgroundColor self.addSubnode(self.contentNode.node) - self.addSubnode(self.tokenListNode) + self.navigationBar?.additionalContentNode.addSubnode(self.tokenListNode) switch self.contentNode { case let .contacts(contactsNode): @@ -255,7 +258,7 @@ final class ContactMultiselectionControllerNode: ASDisplayNode { } } - func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, actualNavigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { + func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, actualNavigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat { self.containerLayout = (layout, navigationBarHeight, actualNavigationBarHeight) var insets = layout.insets(options: [.input]) @@ -288,6 +291,8 @@ final class ContactMultiselectionControllerNode: ASDisplayNode { searchResultsNode.containerLayoutUpdated(ContainerViewLayout(size: layout.size, metrics: layout.metrics, deviceMetrics: layout.deviceMetrics, intrinsicInsets: insets, safeInsets: layout.safeInsets, additionalInsets: layout.additionalInsets, statusBarHeight: layout.statusBarHeight, inputHeight: layout.inputHeight, inputHeightIsInteractivellyChanging: layout.inputHeightIsInteractivellyChanging, inVoiceOver: layout.inVoiceOver), headerInsets: headerInsets, transition: transition) searchResultsNode.frame = CGRect(origin: CGPoint(), size: layout.size) } + + return tokenListHeight } func animateIn() { diff --git a/submodules/TelegramUI/Sources/ContactSelectionController.swift b/submodules/TelegramUI/Sources/ContactSelectionController.swift index f677f76506..317f3cfeef 100644 --- a/submodules/TelegramUI/Sources/ContactSelectionController.swift +++ b/submodules/TelegramUI/Sources/ContactSelectionController.swift @@ -35,14 +35,15 @@ class ContactSelectionControllerImpl: ViewController, ContactSelectionController private let options: [ContactListAdditionalOption] private let displayDeviceContacts: Bool private let displayCallIcons: Bool + private let multipleSelection: Bool private var _ready = Promise() override var ready: Promise { return self._ready } - private let _result = Promise<(ContactListPeer, ContactListAction)?>() - var result: Signal<(ContactListPeer, ContactListAction)?, NoError> { + private let _result = Promise<([ContactListPeer], ContactListAction)?>() + var result: Signal<([ContactListPeer], ContactListAction)?, NoError> { return self._result.get() } @@ -77,6 +78,7 @@ class ContactSelectionControllerImpl: ViewController, ContactSelectionController self.displayDeviceContacts = params.displayDeviceContacts self.displayCallIcons = params.displayCallIcons self.confirmation = params.confirmation + self.multipleSelection = params.multipleSelection self.presentationData = context.sharedContext.currentPresentationData.with { $0 } @@ -118,6 +120,10 @@ class ContactSelectionControllerImpl: ViewController, ContactSelectionController self?.activateSearch() }) self.navigationBar?.setContentNode(self.searchContentNode, animated: false) + + if params.multipleSelection { + self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Select, style: .plain, target: self, action: #selector(self.beginSelection)) + } } required init(coder aDecoder: NSCoder) { @@ -129,6 +135,11 @@ class ContactSelectionControllerImpl: ViewController, ContactSelectionController self.presentationDataDisposable?.dispose() } + @objc private func beginSelection() { + self.navigationItem.rightBarButtonItem = nil + self.contactsNode.beginSelection() + } + private func updateThemeAndStrings() { self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style self.navigationBar?.updatePresentationData(NavigationBarPresentationData(presentationData: self.presentationData)) @@ -145,7 +156,7 @@ class ContactSelectionControllerImpl: ViewController, ContactSelectionController } override func loadDisplayNode() { - self.displayNode = ContactSelectionControllerNode(context: self.context, options: self.options, displayDeviceContacts: self.displayDeviceContacts, displayCallIcons: self.displayCallIcons) + self.displayNode = ContactSelectionControllerNode(context: self.context, options: self.options, displayDeviceContacts: self.displayDeviceContacts, displayCallIcons: self.displayCallIcons, multipleSelection: self.multipleSelection) self._ready.set(self.contactsNode.contactListNode.ready) self.contactsNode.navigationBar = self.navigationBar @@ -165,7 +176,7 @@ class ContactSelectionControllerImpl: ViewController, ContactSelectionController self.contactsNode.contactListNode.openPeer = { [weak self] peer, action in self?.openPeer(peer: peer, action: action) } - + self.contactsNode.contactListNode.suppressPermissionWarning = { [weak self] in if let strongSelf = self { strongSelf.context.sharedContext.presentContactsWarningSuppression(context: strongSelf.context, present: { c, a in @@ -191,6 +202,16 @@ class ContactSelectionControllerImpl: ViewController, ContactSelectionController return false } } + + self.contactsNode.requestMultipleAction = { [weak self] in + if let strongSelf = self { + let selectedPeers = strongSelf.contactsNode.contactListNode.selectedPeers + strongSelf._result.set(.single((selectedPeers, .generic))) + if strongSelf.autoDismiss { + strongSelf.dismiss() + } + } + } self.displayNodeDidLoad() } @@ -236,7 +257,7 @@ class ContactSelectionControllerImpl: ViewController, ContactSelectionController override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { super.containerLayoutUpdated(layout, transition: transition) - self.contactsNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationInsetHeight, actualNavigationBarHeight: self.navigationHeight, transition: transition) + self.contactsNode.containerLayoutUpdated(layout, navigationBarHeight: self.cleanNavigationHeight, actualNavigationBarHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition) } private func activateSearch() { @@ -263,7 +284,7 @@ class ContactSelectionControllerImpl: ViewController, ContactSelectionController self.confirmationDisposable.set((self.confirmation(peer) |> deliverOnMainQueue).start(next: { [weak self] value in if let strongSelf = self { if value { - strongSelf._result.set(.single((peer, action))) + strongSelf._result.set(.single(([peer], action))) if strongSelf.autoDismiss { strongSelf.dismiss() } diff --git a/submodules/TelegramUI/Sources/ContactSelectionControllerNode.swift b/submodules/TelegramUI/Sources/ContactSelectionControllerNode.swift index 629a327409..acf802b1bf 100644 --- a/submodules/TelegramUI/Sources/ContactSelectionControllerNode.swift +++ b/submodules/TelegramUI/Sources/ContactSelectionControllerNode.swift @@ -10,6 +10,7 @@ import AccountContext import SearchBarNode import ContactListUI import SearchUI +import SolidRoundedButtonNode final class ContactSelectionControllerNode: ASDisplayNode { var displayProgress: Bool = false { @@ -30,27 +31,37 @@ final class ContactSelectionControllerNode: ASDisplayNode { private let context: AccountContext private var searchDisplayController: SearchDisplayController? - private var containerLayout: (ContainerViewLayout, CGFloat)? + private var containerLayout: (ContainerViewLayout, CGFloat, CGFloat)? var navigationBar: NavigationBar? var requestDeactivateSearch: (() -> Void)? var requestOpenPeerFromSearch: ((ContactListPeer) -> Void)? + var requestMultipleAction: (() -> Void)? var dismiss: (() -> Void)? var presentationData: PresentationData var presentationDataDisposable: Disposable? - init(context: AccountContext, options: [ContactListAdditionalOption], displayDeviceContacts: Bool, displayCallIcons: Bool) { + private let countPanelNode: ContactSelectionCountPanelNode + + private var selectionState: ContactListNodeGroupSelectionState? + + init(context: AccountContext, options: [ContactListAdditionalOption], displayDeviceContacts: Bool, displayCallIcons: Bool, multipleSelection: Bool) { self.context = context self.presentationData = context.sharedContext.currentPresentationData.with { $0 } self.displayDeviceContacts = displayDeviceContacts self.displayCallIcons = displayCallIcons - self.contactListNode = ContactListNode(context: context, presentation: .single(.natural(options: options, includeChatList: false)), displayCallIcons: displayCallIcons) + self.contactListNode = ContactListNode(context: context, presentation: .single(.natural(options: options, includeChatList: false)), displayCallIcons: displayCallIcons, multipleSelection: multipleSelection) self.dimNode = ASDisplayNode() + var shareImpl: (() -> Void)? + self.countPanelNode = ContactSelectionCountPanelNode(theme: self.presentationData.theme, strings: self.presentationData.strings, action: { + shareImpl?() + }) + super.init() self.setViewBlock({ @@ -76,12 +87,37 @@ final class ContactSelectionControllerNode: ASDisplayNode { self.dimNode.alpha = 0.0 self.dimNode.isUserInteractionEnabled = false self.addSubnode(self.dimNode) + + self.addSubnode(self.countPanelNode) + + self.contactListNode.selectionStateUpdated = { [weak self] selectionState in + if let strongSelf = self { + strongSelf.countPanelNode.count = selectionState?.selectedPeerIndices.count ?? 0 + let previousState = strongSelf.selectionState + strongSelf.selectionState = selectionState + if previousState?.selectedPeerIndices.isEmpty != strongSelf.selectionState?.selectedPeerIndices.isEmpty { + if let (layout, navigationHeight, actualNavigationHeight) = strongSelf.containerLayout { + strongSelf.containerLayoutUpdated(layout, navigationBarHeight: navigationHeight, actualNavigationBarHeight: actualNavigationHeight, transition: .animated(duration: 0.3, curve: .spring)) + } + } + } + } + + shareImpl = { [weak self] in + self?.requestMultipleAction?() + } } deinit { self.presentationDataDisposable?.dispose() } + func beginSelection() { + self.contactListNode.updateSelectionState({ _ in + return ContactListNodeGroupSelectionState() + }) + } + private func updateTheme() { self.backgroundColor = self.presentationData.theme.chatList.backgroundColor self.searchDisplayController?.updatePresentationData(presentationData) @@ -89,7 +125,7 @@ final class ContactSelectionControllerNode: ASDisplayNode { } func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, actualNavigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { - self.containerLayout = (layout, navigationBarHeight) + self.containerLayout = (layout, navigationBarHeight, actualNavigationBarHeight) transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(), size: layout.size)) @@ -107,13 +143,21 @@ final class ContactSelectionControllerNode: ASDisplayNode { self.contactListNode.frame = CGRect(origin: CGPoint(), size: layout.size) + let countPanelHeight = self.countPanelNode.updateLayout(width: layout.size.width, sideInset: layout.safeInsets.left, bottomInset: layout.intrinsicInsets.bottom, transition: transition) + if (self.selectionState?.selectedPeerIndices.isEmpty ?? true) { + transition.updateFrame(node: self.countPanelNode, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height), size: CGSize(width: layout.size.width, height: countPanelHeight))) + } else { + insets.bottom += countPanelHeight + transition.updateFrame(node: self.countPanelNode, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - countPanelHeight), size: CGSize(width: layout.size.width, height: countPanelHeight))) + } + if let searchDisplayController = self.searchDisplayController { searchDisplayController.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition) } } func activateSearch(placeholderNode: SearchBarPlaceholderNode) { - guard let (containerLayout, navigationBarHeight) = self.containerLayout, let navigationBar = self.navigationBar, self.searchDisplayController == nil else { + guard let (containerLayout, navigationBarHeight, _) = self.containerLayout, let navigationBar = self.navigationBar, self.searchDisplayController == nil else { return } @@ -124,7 +168,35 @@ final class ContactSelectionControllerNode: ASDisplayNode { categories.insert(.global) } self.searchDisplayController = SearchDisplayController(presentationData: self.presentationData, contentNode: ContactsSearchContainerNode(context: self.context, onlyWriteable: false, categories: categories, addContact: nil, openPeer: { [weak self] peer in - self?.requestOpenPeerFromSearch?(peer) + if let strongSelf = self { + var updated = false + strongSelf.contactListNode.updateSelectionState { state -> ContactListNodeGroupSelectionState? in + if let state = state { + updated = true + var foundPeers = state.foundPeers + var selectedPeerMap = state.selectedPeerMap + selectedPeerMap[peer.id] = peer + var exists = false + for foundPeer in foundPeers { + if peer.id == foundPeer.id { + exists = true + break + } + } + if !exists { + foundPeers.insert(peer, at: 0) + } + return state.withToggledPeerId(peer.id).withFoundPeers(foundPeers).withSelectedPeerMap(selectedPeerMap) + } else { + return nil + } + } + if updated { + strongSelf.requestDeactivateSearch?() + } else { + strongSelf.requestOpenPeerFromSearch?(peer) + } + } }, contextAction: nil), cancel: { [weak self] in if let requestDeactivateSearch = self?.requestDeactivateSearch { requestDeactivateSearch() @@ -169,3 +241,123 @@ final class ContactSelectionControllerNode: ASDisplayNode { }) } } + +final class ContactSelectionCountPanelNode: ASDisplayNode { + private let theme: PresentationTheme + private let strings: PresentationStrings + + private let separatorNode: ASDisplayNode + + private let button: HighlightTrackingButtonNode + private let badgeLabel: TextNode + private var badgeText: NSAttributedString? + private let badgeBackground: ASImageNode + + private let action: (() -> Void) + + private var validLayout: (CGFloat, CGFloat, CGFloat)? + + var count: Int = 0 { + didSet { + if self.count != oldValue && self.count > 0 { + self.badgeText = NSAttributedString(string: "\(count)", font: Font.regular(14.0), textColor: self.theme.actionSheet.opaqueItemBackgroundColor, paragraphAlignment: .center) + self.badgeLabel.isHidden = false + self.badgeBackground.isHidden = false + + if let (width, sideInset, bottomInset) = self.validLayout { + let _ = self.updateLayout(width: width, sideInset: sideInset, bottomInset: bottomInset, transition: .immediate) + } + } + } + } + + init(theme: PresentationTheme, strings: PresentationStrings, action: @escaping () -> Void) { + self.theme = theme + self.strings = strings + self.action = action + + self.separatorNode = ASDisplayNode() + self.separatorNode.backgroundColor = theme.rootController.navigationBar.separatorColor + + self.badgeLabel = TextNode() + self.badgeLabel.isHidden = true + self.badgeLabel.isUserInteractionEnabled = false + self.badgeLabel.displaysAsynchronously = false + + self.badgeBackground = ASImageNode() + self.badgeBackground.isHidden = true + self.badgeBackground.isLayerBacked = true + self.badgeBackground.displaysAsynchronously = false + self.badgeBackground.displayWithoutProcessing = true + + self.badgeBackground.image = generateStretchableFilledCircleImage(diameter: 22.0, color: theme.actionSheet.controlAccentColor) + + self.button = HighlightTrackingButtonNode() + self.button.setTitle(strings.ShareMenu_Send, with: Font.medium(17.0), with: theme.actionSheet.controlAccentColor, for: .normal) + + super.init() + + self.backgroundColor = theme.rootController.navigationBar.opaqueBackgroundColor + + self.addSubnode(self.badgeBackground) + self.addSubnode(self.badgeLabel) + self.addSubnode(self.button) + + self.addSubnode(self.separatorNode) + + self.button.highligthedChanged = { [weak self] highlighted in + if let strongSelf = self { + if highlighted { + strongSelf.badgeBackground.layer.removeAnimation(forKey: "opacity") + strongSelf.badgeBackground.alpha = 0.4 + + strongSelf.badgeLabel.layer.removeAnimation(forKey: "opacity") + strongSelf.badgeLabel.alpha = 0.4 + + strongSelf.button.titleNode.layer.removeAnimation(forKey: "opacity") + strongSelf.button.titleNode.alpha = 0.4 + } else { + strongSelf.badgeBackground.alpha = 1.0 + strongSelf.badgeBackground.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) + + strongSelf.badgeLabel.alpha = 1.0 + strongSelf.badgeLabel.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) + + strongSelf.button.titleNode.alpha = 1.0 + strongSelf.button.titleNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) + } + } + } + self.button.addTarget(self, action: #selector(self.pressed), forControlEvents: .touchUpInside) + } + + @objc private func pressed() { + self.action() + } + + func updateLayout(width: CGFloat, sideInset: CGFloat, bottomInset: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat { + self.validLayout = (width, sideInset, bottomInset) + let topInset: CGFloat = 9.0 + var bottomInset = bottomInset + bottomInset += topInset - (bottomInset.isZero ? 0.0 : 4.0) + + let height = 44.0 + bottomInset + + self.button.frame = CGRect(x: sideInset, y: 0.0, width: width - sideInset * 2.0, height: 44.0) + + if !self.badgeLabel.isHidden { + let (badgeLayout, badgeApply) = TextNode.asyncLayout(self.badgeLabel)(TextNodeLayoutArguments(attributedString: self.badgeText, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 100.0, height: 100.0), alignment: .left, lineSpacing: 0.0, cutout: nil, insets: UIEdgeInsets())) + let _ = badgeApply() + + let backgroundSize = CGSize(width: max(22.0, badgeLayout.size.width + 10.0 + 1.0), height: 22.0) + let backgroundFrame = CGRect(origin: CGPoint(x: self.button.titleNode.frame.maxX + 6.0, y: self.button.bounds.size.height - 33.0), size: backgroundSize) + + self.badgeBackground.frame = backgroundFrame + self.badgeLabel.frame = CGRect(origin: CGPoint(x: floorToScreenPixels(backgroundFrame.midX - badgeLayout.size.width / 2.0), y: backgroundFrame.minY + 3.0), size: badgeLayout.size) + } + + transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: width, height: UIScreenPixel))) + + return height + } +} diff --git a/submodules/TelegramUI/Sources/CreateChannelController.swift b/submodules/TelegramUI/Sources/CreateChannelController.swift index 2e57f10698..d93801c7bf 100644 --- a/submodules/TelegramUI/Sources/CreateChannelController.swift +++ b/submodules/TelegramUI/Sources/CreateChannelController.swift @@ -197,7 +197,7 @@ private func CreateChannelEntries(presentationData: PresentationData, state: Cre let groupInfoState = ItemListAvatarAndNameInfoItemState(editingName: state.editingName, updatingName: nil) - 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) + let peer = TelegramGroup(id: PeerId(namespace: .max, id: PeerId.Id._internalFromInt32Value(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)) @@ -258,7 +258,7 @@ public func createChannelController(context: AccountContext) -> ViewController { } endEditingImpl?() - actionsDisposable.add((createChannel(account: context.account, title: title, description: description.isEmpty ? nil : description) + actionsDisposable.add((context.engine.peers.createChannel(title: title, description: description.isEmpty ? nil : description) |> deliverOnMainQueue |> afterDisposed { Queue.mainQueue().async { @@ -273,7 +273,7 @@ public func createChannelController(context: AccountContext) -> ViewController { return $0.avatar } if let _ = updatingAvatar { - let _ = updatePeerPhoto(postbox: context.account.postbox, network: context.account.network, stateManager: context.account.stateManager, accountPeerId: context.account.peerId, peerId: peerId, photo: uploadedAvatar.get(), video: uploadedVideoAvatar?.0.get(), videoStartTimestamp: uploadedVideoAvatar?.1, mapResourceToAvatarSizes: { resource, representations in + let _ = context.engine.peers.updatePeerPhoto(peerId: peerId, photo: uploadedAvatar.get(), video: uploadedVideoAvatar?.0.get(), videoStartTimestamp: uploadedVideoAvatar?.1, mapResourceToAvatarSizes: { resource, representations in return mapResourceToAvatarSizes(postbox: context.account.postbox, resource: resource, representations: representations) }).start() } @@ -326,10 +326,10 @@ public func createChannelController(context: AccountContext) -> ViewController { let completedChannelPhotoImpl: (UIImage) -> Void = { image in if let data = image.jpegData(compressionQuality: 0.6) { - let resource = LocalFileMediaResource(fileId: arc4random64()) + let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) context.account.postbox.mediaBox.storeResourceData(resource.id, data: data) - let representation = TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 640, height: 640), resource: resource, progressiveSizes: []) - uploadedAvatar.set(uploadedPeerPhoto(postbox: context.account.postbox, network: context.account.network, resource: resource)) + let representation = TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 640, height: 640), resource: resource, progressiveSizes: [], immediateThumbnailData: nil) + uploadedAvatar.set(context.engine.peers.uploadedPeerPhoto(resource: resource)) uploadedVideoAvatar = nil updateState { current in var current = current @@ -341,9 +341,9 @@ public func createChannelController(context: AccountContext) -> ViewController { let completedChannelVideoImpl: (UIImage, Any?, TGVideoEditAdjustments?) -> Void = { image, asset, adjustments in if let data = image.jpegData(compressionQuality: 0.6) { - let photoResource = LocalFileMediaResource(fileId: arc4random64()) + let photoResource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) context.account.postbox.mediaBox.storeResourceData(photoResource.id, data: data) - let representation = TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 640, height: 640), resource: photoResource, progressiveSizes: []) + let representation = TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 640, height: 640), resource: photoResource, progressiveSizes: [], immediateThumbnailData: nil) updateState { state in var state = state state.avatar = .image(representation, true) @@ -363,7 +363,7 @@ public func createChannelController(context: AccountContext) -> ViewController { return nil } } - let uploadInterface = LegacyLiveUploadInterface(account: context.account) + let uploadInterface = LegacyLiveUploadInterface(context: context) let signal: SSignal if let asset = asset as? AVAsset { signal = TGMediaVideoConverter.convert(asset, adjustments: adjustments, watcher: uploadInterface, entityRenderer: entityRenderer)! @@ -407,7 +407,7 @@ public func createChannelController(context: AccountContext) -> ViewController { if let liveUploadData = result.liveUploadData as? LegacyLiveUploadInterfaceResult { resource = LocalFileMediaResource(fileId: liveUploadData.id) } else { - resource = LocalFileMediaResource(fileId: arc4random64()) + resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) } context.account.postbox.mediaBox.storeResourceData(resource.id, data: data, synchronous: true) subscriber.putNext(resource) @@ -427,7 +427,7 @@ public func createChannelController(context: AccountContext) -> ViewController { } } - uploadedAvatar.set(uploadedPeerPhoto(postbox: context.account.postbox, network: context.account.network, resource: photoResource)) + uploadedAvatar.set(context.engine.peers.uploadedPeerPhoto(resource: photoResource)) let promise = Promise() promise.set(signal @@ -436,7 +436,7 @@ public func createChannelController(context: AccountContext) -> ViewController { } |> mapToSignal { resource -> Signal in if let resource = resource { - return uploadedPeerVideo(postbox: context.account.postbox, network: context.account.network, messageMediaPreuploadManager: context.account.messageMediaPreuploadManager, resource: resource) |> map(Optional.init) + return context.engine.peers.uploadedPeerVideo(resource: resource) |> map(Optional.init) } else { return .single(nil) } diff --git a/submodules/TelegramUI/Sources/CreateGroupController.swift b/submodules/TelegramUI/Sources/CreateGroupController.swift index 3fe9f78ff9..daf2740e82 100644 --- a/submodules/TelegramUI/Sources/CreateGroupController.swift +++ b/submodules/TelegramUI/Sources/CreateGroupController.swift @@ -298,7 +298,7 @@ private func createGroupEntries(presentationData: PresentationData, state: Creat let groupInfoState = ItemListAvatarAndNameInfoItemState(editingName: state.editingName, updatingName: nil) - 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) + let peer = TelegramGroup(id: PeerId(namespace: .max, id: PeerId.Id._internalFromInt32Value(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)) @@ -398,7 +398,7 @@ public func createGroupControllerImpl(context: AccountContext, peerIds: [PeerId] }) } - venuesPromise.set(nearbyVenues(account: context.account, latitude: latitude, longitude: longitude) + venuesPromise.set(nearbyVenues(context: context, latitude: latitude, longitude: longitude) |> map(Optional.init)) } @@ -425,9 +425,9 @@ public func createGroupControllerImpl(context: AccountContext, peerIds: [PeerId] let createSignal: Signal switch mode { case .generic: - createSignal = createGroup(account: context.account, title: title, peerIds: peerIds) + createSignal = context.engine.peers.createGroup(title: title, peerIds: peerIds) case .supergroup: - createSignal = createSupergroup(account: context.account, title: title, description: nil) + createSignal = context.engine.peers.createSupergroup(title: title, description: nil) |> map(Optional.init) |> mapError { error -> CreateGroupError in switch error { @@ -454,7 +454,7 @@ public func createGroupControllerImpl(context: AccountContext, peerIds: [PeerId] guard let address = address else { return .complete() } - return createSupergroup(account: context.account, title: title, description: nil, location: (location.latitude, location.longitude, address)) + return context.engine.peers.createSupergroup(title: title, description: nil, location: (location.latitude, location.longitude, address)) |> map(Optional.init) |> mapError { error -> CreateGroupError in switch error { @@ -482,7 +482,7 @@ public func createGroupControllerImpl(context: AccountContext, peerIds: [PeerId] return $0.avatar } if let _ = updatingAvatar { - return updatePeerPhoto(postbox: context.account.postbox, network: context.account.network, stateManager: context.account.stateManager, accountPeerId: context.account.peerId, peerId: peerId, photo: uploadedAvatar.get(), video: uploadedVideoAvatar?.0.get(), videoStartTimestamp: uploadedVideoAvatar?.1, mapResourceToAvatarSizes: { resource, representations in + return context.engine.peers.updatePeerPhoto(peerId: peerId, photo: uploadedAvatar.get(), video: uploadedVideoAvatar?.0.get(), videoStartTimestamp: uploadedVideoAvatar?.1, mapResourceToAvatarSizes: { resource, representations in return mapResourceToAvatarSizes(postbox: context.account.postbox, resource: resource, representations: representations) }) |> ignoreValues @@ -573,10 +573,10 @@ public func createGroupControllerImpl(context: AccountContext, peerIds: [PeerId] let completedGroupPhotoImpl: (UIImage) -> Void = { image in if let data = image.jpegData(compressionQuality: 0.6) { - let resource = LocalFileMediaResource(fileId: arc4random64()) + let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) context.account.postbox.mediaBox.storeResourceData(resource.id, data: data) - let representation = TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 640, height: 640), resource: resource, progressiveSizes: []) - uploadedAvatar.set(uploadedPeerPhoto(postbox: context.account.postbox, network: context.account.network, resource: resource)) + let representation = TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 640, height: 640), resource: resource, progressiveSizes: [], immediateThumbnailData: nil) + uploadedAvatar.set(context.engine.peers.uploadedPeerPhoto(resource: resource)) uploadedVideoAvatar = nil updateState { current in var current = current @@ -588,9 +588,9 @@ public func createGroupControllerImpl(context: AccountContext, peerIds: [PeerId] let completedGroupVideoImpl: (UIImage, Any?, TGVideoEditAdjustments?) -> Void = { image, asset, adjustments in if let data = image.jpegData(compressionQuality: 0.6) { - let photoResource = LocalFileMediaResource(fileId: arc4random64()) + let photoResource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) context.account.postbox.mediaBox.storeResourceData(photoResource.id, data: data) - let representation = TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 640, height: 640), resource: photoResource, progressiveSizes: []) + let representation = TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 640, height: 640), resource: photoResource, progressiveSizes: [], immediateThumbnailData: nil) updateState { state in var state = state state.avatar = .image(representation, true) @@ -611,7 +611,7 @@ public func createGroupControllerImpl(context: AccountContext, peerIds: [PeerId] return nil } } - let uploadInterface = LegacyLiveUploadInterface(account: context.account) + let uploadInterface = LegacyLiveUploadInterface(context: context) let signal: SSignal if let asset = asset as? AVAsset { signal = TGMediaVideoConverter.convert(asset, adjustments: adjustments, watcher: uploadInterface, entityRenderer: entityRenderer)! @@ -655,7 +655,7 @@ public func createGroupControllerImpl(context: AccountContext, peerIds: [PeerId] if let liveUploadData = result.liveUploadData as? LegacyLiveUploadInterfaceResult { resource = LocalFileMediaResource(fileId: liveUploadData.id) } else { - resource = LocalFileMediaResource(fileId: arc4random64()) + resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) } context.account.postbox.mediaBox.storeResourceData(resource.id, data: data, synchronous: true) subscriber.putNext(resource) @@ -675,7 +675,7 @@ public func createGroupControllerImpl(context: AccountContext, peerIds: [PeerId] } } - uploadedAvatar.set(uploadedPeerPhoto(postbox: context.account.postbox, network: context.account.network, resource: photoResource)) + uploadedAvatar.set(context.engine.peers.uploadedPeerPhoto(resource: photoResource)) let promise = Promise() promise.set(signal @@ -684,7 +684,7 @@ public func createGroupControllerImpl(context: AccountContext, peerIds: [PeerId] } |> mapToSignal { resource -> Signal in if let resource = resource { - return uploadedPeerVideo(postbox: context.account.postbox, network: context.account.network, messageMediaPreuploadManager: context.account.messageMediaPreuploadManager, resource: resource) |> map(Optional.init) + return context.engine.peers.uploadedPeerVideo(resource: resource) |> map(Optional.init) } else { return .single(nil) } diff --git a/submodules/TelegramUI/Sources/DisabledContextResultsChatInputContextPanelNode.swift b/submodules/TelegramUI/Sources/DisabledContextResultsChatInputContextPanelNode.swift index cb96e43f19..a97efe5ac4 100644 --- a/submodules/TelegramUI/Sources/DisabledContextResultsChatInputContextPanelNode.swift +++ b/submodules/TelegramUI/Sources/DisabledContextResultsChatInputContextPanelNode.swift @@ -72,7 +72,10 @@ final class DisabledContextResultsChatInputContextPanelNode: ChatInputContextPan func animateIn() { let position = self.containerNode.layer.position - self.containerNode.layer.animatePosition(from: CGPoint(x: position.x, y: position.y + (self.containerNode.bounds.height)), to: position, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring) + self.containerNode.position = CGPoint(x: position.x, y: position.y + (self.containerNode.bounds.height)) + ContainedViewLayoutTransition.animated(duration: 0.3, curve: .spring).animateView { + self.containerNode.position = position + } } override func animateOut(completion: @escaping () -> Void) { diff --git a/submodules/TelegramUI/Sources/DocumentPreviewController.swift b/submodules/TelegramUI/Sources/DocumentPreviewController.swift index c389573d96..971ca25c4e 100644 --- a/submodules/TelegramUI/Sources/DocumentPreviewController.swift +++ b/submodules/TelegramUI/Sources/DocumentPreviewController.swift @@ -40,7 +40,7 @@ final class DocumentPreviewController: UINavigationController, QLPreviewControll super.init(nibName: nil, bundle: nil) - self.navigationBar.barTintColor = theme.rootController.navigationBar.backgroundColor + self.navigationBar.barTintColor = theme.rootController.navigationBar.opaqueBackgroundColor self.navigationBar.tintColor = theme.rootController.navigationBar.accentTextColor self.navigationBar.shadowImage = generateImage(CGSize(width: 1.0, height: 1.0), rotatedContext: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) @@ -51,8 +51,7 @@ final class DocumentPreviewController: UINavigationController, QLPreviewControll self.navigationBar.titleTextAttributes = [NSAttributedString.Key.font: Font.semibold(17.0), NSAttributedString.Key.foregroundColor: theme.rootController.navigationBar.primaryTextColor] let controller = QLPreviewController(nibName: nil, bundle: nil) - controller.navigation_setDismiss({ [weak self] in - //self?.cancelPressed() + controller.navigation_setDismiss({ }, rootController: self) controller.delegate = self controller.dataSource = self @@ -201,9 +200,9 @@ final class CompactDocumentPreviewController: QLPreviewController, QLPreviewCont func presentDocumentPreviewController(rootController: UIViewController, theme: PresentationTheme, strings: PresentationStrings, postbox: Postbox, file: TelegramMediaFile) { if #available(iOSApplicationExtension 9.0, iOS 9.0, *) { let navigationBar = UINavigationBar.appearance(whenContainedInInstancesOf: [QLPreviewController.self]) - navigationBar.barTintColor = theme.rootController.navigationBar.backgroundColor + navigationBar.barTintColor = theme.rootController.navigationBar.opaqueBackgroundColor navigationBar.setBackgroundImage(generateImage(CGSize(width: 1.0, height: 1.0), rotatedContext: { size, context in - context.setFillColor(theme.rootController.navigationBar.backgroundColor.cgColor) + context.setFillColor(theme.rootController.navigationBar.opaqueBackgroundColor.cgColor) context.fill(CGRect(origin: CGPoint(), size: size)) }), for: .default) navigationBar.isTranslucent = true diff --git a/submodules/TelegramUI/Sources/DrawingStickersScreen.swift b/submodules/TelegramUI/Sources/DrawingStickersScreen.swift index 3e53710d0a..9aebe8dbb9 100644 --- a/submodules/TelegramUI/Sources/DrawingStickersScreen.swift +++ b/submodules/TelegramUI/Sources/DrawingStickersScreen.swift @@ -109,8 +109,9 @@ private final class DrawingStickersScreenNode: ViewControllerTracingNode { var selectStickerImpl: ((FileMediaReference, ASDisplayNode, CGRect) -> Bool)? self.controllerInteraction = ChatControllerInteraction(openMessage: { _, _ in - return false }, openPeer: { _, _, _ in }, openPeerMention: { _ in }, openMessageContextMenu: { _, _, _, _, _ in }, openMessageContextActions: { _, _, _, _ in }, navigateToMessage: { _, _ in }, navigateToMessageStandalone: { _ in - }, tapMessage: nil, clickThroughMessage: { }, toggleMessagesSelection: { _, _ in }, sendCurrentMessage: { _ in }, sendMessage: { _ in }, sendSticker: { fileReference, _, _, node, rect in return selectStickerImpl?(fileReference, node, rect) ?? false }, sendGif: { _, _, _ in return false }, sendBotContextResultAsGif: { _, _, _, _ 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 }, activateMessagePinch: { _ in + }, openMessageContextActions: { _, _, _, _ in }, navigateToMessage: { _, _ in }, navigateToMessageStandalone: { _ in + }, tapMessage: nil, clickThroughMessage: { }, toggleMessagesSelection: { _, _ in }, sendCurrentMessage: { _ in }, sendMessage: { _ in }, sendSticker: { fileReference, _, _, _, _, node, rect in return selectStickerImpl?(fileReference, node, rect) ?? false }, sendGif: { _, _, _, _, _ in return false }, sendBotContextResultAsGif: { _, _, _, _, _ 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: { @@ -144,8 +145,6 @@ private final class DrawingStickersScreenNode: ViewControllerTracingNode { }, displayPsa: { _, _ in }, displayDiceTooltip: { _ in }, animateDiceSuccess: { _ in - }, greetingStickerNode: { - return nil }, openPeerContextMenu: { _, _, _, _, _ in }, openMessageReplies: { _, _, _ in }, openReplyThreadOriginalMessage: { _ in @@ -153,10 +152,12 @@ private final class DrawingStickersScreenNode: ViewControllerTracingNode { }, editMessageMedia: { _, _ in }, copyText: { _ in }, displayUndo: { _ in + }, isAnimatingMessage: { _ in + return false }, requestMessageUpdate: { _ in }, cancelInteractiveKeyboardGestures: { }, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings, - pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(loopAnimatedStickers: true)) + pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(loopAnimatedStickers: true), presentationContext: ChatPresentationContext(backgroundNode: nil)) self.blurView = UIVisualEffectView(effect: UIBlurEffect(style: .dark)) @@ -227,7 +228,7 @@ private final class DrawingStickersScreenNode: ViewControllerTracingNode { sendSticker: { fileReference, sourceNode, sourceRect in if let strongSelf = self { - return strongSelf.controllerInteraction.sendSticker(fileReference, nil, false, sourceNode, sourceRect) + return strongSelf.controllerInteraction.sendSticker(fileReference, false, false, nil, false, sourceNode, sourceRect) } else { return false } @@ -344,7 +345,7 @@ private final class DrawingStickersScreenNode: ViewControllerTracingNode { sendSticker: { fileReference, sourceNode, sourceRect in if let strongSelf = self { - return strongSelf.controllerInteraction.sendSticker(fileReference, nil, false, sourceNode, sourceRect) + return strongSelf.controllerInteraction.sendSticker(fileReference, false, false, nil, false, sourceNode, sourceRect) } else { return false } @@ -607,24 +608,8 @@ private final class DrawingStickersScreenNode: ViewControllerTracingNode { let panelEntries = chatMediaInputPanelEntries(view: view, savedStickers: savedStickers, recentStickers: recentStickers, peerSpecificPack: nil, canInstallPeerSpecificPack: .none, hasUnreadTrending: nil, theme: theme, hasGifs: false, hasSettings: false) let gridEntries = chatMediaInputGridEntries(view: view, savedStickers: savedStickers, recentStickers: recentStickers, peerSpecificPack: nil, canInstallPeerSpecificPack: .none, hasSearch: false, hasAccessories: false, strings: strings, theme: theme) -// if view.higher == nil { -// var hasTopSeparator = true -// if gridEntries.count == 1, case .search = gridEntries[0] { -// hasTopSeparator = false -// } -// -// 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: hasTopSeparator))) -// hasTopSeparator = true -// index += 1 -// } -// } -// } - let (previousPanelEntries, previousGridEntries) = previousStickerEntries.swap((panelEntries, gridEntries)) - return (view, preparedChatMediaInputPanelEntryTransition(context: context, from: previousPanelEntries, to: panelEntries, inputNodeInteraction: stickersInputNodeInteraction), previousPanelEntries.isEmpty, preparedChatMediaInputGridEntryTransition(account: context.account, view: view, from: previousGridEntries, to: gridEntries, update: update, interfaceInteraction: controllerInteraction, inputNodeInteraction: stickersInputNodeInteraction, trendingInteraction: trendingInteraction), previousGridEntries.isEmpty) + return (view, preparedChatMediaInputPanelEntryTransition(context: context, from: previousPanelEntries, to: panelEntries, inputNodeInteraction: stickersInputNodeInteraction, scrollToItem: nil), previousPanelEntries.isEmpty, preparedChatMediaInputGridEntryTransition(account: context.account, view: view, from: previousGridEntries, to: gridEntries, update: update, interfaceInteraction: controllerInteraction, inputNodeInteraction: stickersInputNodeInteraction, trendingInteraction: trendingInteraction), previousGridEntries.isEmpty) } self.disposable.set((stickerTransitions @@ -659,7 +644,7 @@ private final class DrawingStickersScreenNode: ViewControllerTracingNode { let gridEntries = chatMediaInputGridEntries(view: view, savedStickers: nil, recentStickers: nil, peerSpecificPack: nil, canInstallPeerSpecificPack: .none, hasSearch: false, hasAccessories: false, strings: strings, theme: theme) let (previousPanelEntries, previousGridEntries) = previousMaskEntries.swap((panelEntries, gridEntries)) - return (view, preparedChatMediaInputPanelEntryTransition(context: context, from: previousPanelEntries, to: panelEntries, inputNodeInteraction: masksInputNodeInteraction), previousPanelEntries.isEmpty, preparedChatMediaInputGridEntryTransition(account: context.account, view: view, from: previousGridEntries, to: gridEntries, update: update, interfaceInteraction: controllerInteraction, inputNodeInteraction: masksInputNodeInteraction, trendingInteraction: trendingInteraction), previousGridEntries.isEmpty) + return (view, preparedChatMediaInputPanelEntryTransition(context: context, from: previousPanelEntries, to: panelEntries, inputNodeInteraction: masksInputNodeInteraction, scrollToItem: nil), previousPanelEntries.isEmpty, preparedChatMediaInputGridEntryTransition(account: context.account, view: view, from: previousGridEntries, to: gridEntries, update: update, interfaceInteraction: controllerInteraction, inputNodeInteraction: masksInputNodeInteraction, trendingInteraction: trendingInteraction), previousGridEntries.isEmpty) } self.maskDisposable.set((maskTransitions diff --git a/submodules/TelegramUI/Sources/EditAccessoryPanelNode.swift b/submodules/TelegramUI/Sources/EditAccessoryPanelNode.swift index e13b940f39..50eab52af5 100644 --- a/submodules/TelegramUI/Sources/EditAccessoryPanelNode.swift +++ b/submodules/TelegramUI/Sources/EditAccessoryPanelNode.swift @@ -15,6 +15,7 @@ import PhotoResources import TelegramStringFormatting final class EditAccessoryPanelNode: AccessoryPanelNode { + let dateTimeFormat: PresentationDateTimeFormat let messageId: MessageId let closeButton: ASButtonNode @@ -67,12 +68,13 @@ final class EditAccessoryPanelNode: AccessoryPanelNode { var strings: PresentationStrings var nameDisplayOrder: PresentationPersonNameOrder - init(context: AccountContext, messageId: MessageId, theme: PresentationTheme, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder) { + init(context: AccountContext, messageId: MessageId, theme: PresentationTheme, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, dateTimeFormat: PresentationDateTimeFormat) { self.context = context self.messageId = messageId self.theme = theme self.strings = strings self.nameDisplayOrder = nameDisplayOrder + self.dateTimeFormat = dateTimeFormat self.closeButton = ASButtonNode() self.closeButton.accessibilityLabel = strings.VoiceOver_DiscardPreparedContent @@ -159,7 +161,7 @@ final class EditAccessoryPanelNode: AccessoryPanelNode { if let currentEditMediaReference = self.currentEditMediaReference { effectiveMessage = effectiveMessage.withUpdatedMedia([currentEditMediaReference.media]) } - (text, _) = descriptionStringForMessage(contentSettings: context.currentContentSettings.with { $0 }, message: 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, dateTimeFormat: self.dateTimeFormat, accountPeerId: self.context.account.peerId) } var updatedMediaReference: AnyMediaReference? @@ -231,7 +233,8 @@ final class EditAccessoryPanelNode: AccessoryPanelNode { if let currentEditMediaReference = self.currentEditMediaReference { effectiveMessage = effectiveMessage.withUpdatedMedia([currentEditMediaReference.media]) } - switch messageContentKind(contentSettings: self.context.currentContentSettings.with { $0 }, message: effectiveMessage, strings: strings, nameDisplayOrder: nameDisplayOrder, accountPeerId: self.context.account.peerId) { + let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } + switch messageContentKind(contentSettings: self.context.currentContentSettings.with { $0 }, message: effectiveMessage, strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: presentationData.dateTimeFormat, accountPeerId: self.context.account.peerId) { case .text: isMedia = false default: diff --git a/submodules/TelegramUI/Sources/EditableTokenListNode.swift b/submodules/TelegramUI/Sources/EditableTokenListNode.swift index 15ae33ed4b..440a6a7e3f 100644 --- a/submodules/TelegramUI/Sources/EditableTokenListNode.swift +++ b/submodules/TelegramUI/Sources/EditableTokenListNode.swift @@ -112,6 +112,7 @@ private final class CaretIndicatorNode: ASImageNode { final class EditableTokenListNode: ASDisplayNode, UITextFieldDelegate { private let theme: EditableTokenListNodeTheme + private let backgroundNode: NavigationBackgroundNode private let scrollNode: ASScrollNode private let placeholderNode: ASTextNode private var tokenNodes: [TokenNode] = [] @@ -127,6 +128,8 @@ final class EditableTokenListNode: ASDisplayNode, UITextFieldDelegate { init(theme: EditableTokenListNodeTheme, placeholder: String) { self.theme = theme + + self.backgroundNode = NavigationBackgroundNode(color: theme.backgroundColor) self.scrollNode = ASScrollNode() self.scrollNode.view.alwaysBounceVertical = true @@ -157,9 +160,9 @@ final class EditableTokenListNode: ASDisplayNode, UITextFieldDelegate { self.separatorNode.backgroundColor = theme.separatorColor super.init() + self.addSubnode(self.backgroundNode) self.addSubnode(self.scrollNode) - - self.backgroundColor = theme.backgroundColor + self.addSubnode(self.separatorNode) self.scrollNode.addSubnode(self.placeholderNode) self.scrollNode.addSubnode(self.textFieldScrollNode) @@ -306,8 +309,7 @@ final class EditableTokenListNode: ASDisplayNode, UITextFieldDelegate { let contentHeight = currentOffset.y + 29.0 + verticalInset let nodeHeight = min(contentHeight, 110.0) - let separatorHeight = UIScreenPixel - transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: nodeHeight - separatorHeight), size: CGSize(width: width, height: separatorHeight))) + transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: width, height: UIScreenPixel))) transition.updateFrame(node: self.scrollNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: width, height: nodeHeight))) if !abs(previousContentHeight - contentHeight).isLess(than: CGFloat.ulpOfOne) { @@ -322,6 +324,9 @@ final class EditableTokenListNode: ASDisplayNode, UITextFieldDelegate { } } self.scrollNode.view.contentSize = CGSize(width: width, height: contentHeight) + + transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: width, height: nodeHeight))) + self.backgroundNode.update(size: self.backgroundNode.bounds.size, transition: transition) return nodeHeight } diff --git a/submodules/TelegramUI/Sources/EmojiResources.swift b/submodules/TelegramUI/Sources/EmojiResources.swift index 730c085b42..4396a939f9 100644 --- a/submodules/TelegramUI/Sources/EmojiResources.swift +++ b/submodules/TelegramUI/Sources/EmojiResources.swift @@ -9,6 +9,7 @@ import WebPBinding import MediaResources import Emoji import AppBundle +import AccountContext public struct EmojiThumbnailResourceId: MediaResourceId { public let emoji: String @@ -284,17 +285,17 @@ private final class Buffer { var data = Data() } -func fetchEmojiSpriteResource(postbox: Postbox, network: Network, resource: EmojiSpriteResource) -> Signal { +func fetchEmojiSpriteResource(account: Account, resource: EmojiSpriteResource) -> Signal { let packName = "P\(resource.packId)_by_AEStickerBot" - return loadedStickerPack(postbox: postbox, network: network, reference: .name(packName), forceActualized: false) + return TelegramEngine(account: account).stickers.loadedStickerPack(reference: .name(packName), forceActualized: false) |> castError(MediaResourceDataFetchError.self) |> mapToSignal { result -> Signal in switch result { case let .result(_, items, _): if let sticker = items[Int(resource.stickerId)] as? StickerPackItem { return Signal { subscriber in - guard let fetchResource = postbox.mediaBox.fetchResource else { + guard let fetchResource = account.postbox.mediaBox.fetchResource else { return EmptyDisposable } diff --git a/submodules/TelegramUI/Sources/FeaturedStickersScreen.swift b/submodules/TelegramUI/Sources/FeaturedStickersScreen.swift index 44b44075f2..66a14b33b4 100644 --- a/submodules/TelegramUI/Sources/FeaturedStickersScreen.swift +++ b/submodules/TelegramUI/Sources/FeaturedStickersScreen.swift @@ -15,6 +15,7 @@ import OverlayStatusController import PresentationDataUtils import SearchBarNode import UndoUI +import ContextUI private final class FeaturedInteraction { let installPack: (ItemCollectionInfo, Bool) -> Void @@ -212,6 +213,8 @@ private final class FeaturedStickersScreenNode: ViewControllerTracingNode { private var searchNode: FeaturedPaneSearchContentNode? + private weak var peekController: PeekController? + private let _ready = Promise() var ready: Promise { return self._ready @@ -258,7 +261,7 @@ private final class FeaturedStickersScreenNode: ViewControllerTracingNode { } } if !addedRead.isEmpty { - let _ = markFeaturedStickerPacksAsSeenInteractively(postbox: strongSelf.context.account.postbox, ids: addedRead).start() + let _ = strongSelf.context.engine.stickers.markFeaturedStickerPacksAsSeenInteractively(ids: addedRead).start() } if bottomIndex >= strongSelf.gridNode.items.count - 15 { @@ -295,84 +298,9 @@ private final class FeaturedStickersScreenNode: ViewControllerTracingNode { } let account = strongSelf.context.account if install { - let _ = addStickerPackInteractively(postbox: strongSelf.context.account.postbox, info: info, items: []).start() - /*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 - } - |> then(.single((info, items))) - } - 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?.controller?.present(controller, in: .window(.root)) - 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 - }))*/ - }))*/ + let _ = strongSelf.context.engine.stickers.addStickerPackInteractively(info: info, items: []).start() } else { - let _ = (removeStickerPackInteractively(postbox: account.postbox, id: info.id, option: .delete) + let _ = (strongSelf.context.engine.stickers.removeStickerPackInteractively(id: info.id, option: .delete) |> deliverOnMainQueue).start(next: { _ in }) } @@ -535,16 +463,21 @@ private final class FeaturedStickersScreenNode: ViewControllerTracingNode { |> deliverOnMainQueue |> map { isStarred -> (ASDisplayNode, PeekControllerContent)? in if let strongSelf = self { - var menuItems: [PeekControllerMenuItem] = [] + var menuItems: [ContextMenuItem] = [] menuItems = [ - PeekControllerMenuItem(title: strongSelf.presentationData.strings.StickerPack_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 + .action(ContextMenuActionItem(text: strongSelf.presentationData.strings.StickerPack_Send, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Resend"), color: theme.contextMenu.primaryColor) }, action: { _, f in + if let strongSelf = self, let peekController = strongSelf.peekController { + if let animationNode = (peekController.contentNode as? StickerPreviewPeekContentNode)?.animationNode { + let _ = strongSelf.sendSticker?(.standalone(media: item.file), animationNode, animationNode.bounds) + } else if let imageNode = (peekController.contentNode as? StickerPreviewPeekContentNode)?.imageNode { + let _ = strongSelf.sendSticker?(.standalone(media: item.file), imageNode, imageNode.bounds) + } } - }), - PeekControllerMenuItem(title: isStarred ? strongSelf.presentationData.strings.Stickers_RemoveFromFavorites : strongSelf.presentationData.strings.Stickers_AddToFavorites, color: isStarred ? .destructive : .accent, action: { _, _ in + f(.default) + })), + .action(ContextMenuActionItem(text: isStarred ? strongSelf.presentationData.strings.Stickers_RemoveFromFavorites : strongSelf.presentationData.strings.Stickers_AddToFavorites, icon: { theme in generateTintedImage(image: isStarred ? UIImage(bundleImageName: "Chat/Context Menu/Unstar") : UIImage(bundleImageName: "Chat/Context Menu/Rate"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in + f(.default) + if let strongSelf = self { if isStarred { let _ = removeSavedSticker(postbox: strongSelf.context.account.postbox, mediaId: item.file.fileId).start() @@ -552,9 +485,10 @@ private final class FeaturedStickersScreenNode: ViewControllerTracingNode { let _ = addSavedSticker(postbox: strongSelf.context.account.postbox, network: strongSelf.context.account.network, file: item.file).start() } } - return true - }), - PeekControllerMenuItem(title: strongSelf.presentationData.strings.StickerPack_ViewPack, color: .accent, action: { _, _ in + })), + .action(ContextMenuActionItem(text: strongSelf.presentationData.strings.StickerPack_ViewPack, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Sticker"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in + f(.default) + if let strongSelf = self { loop: for attribute in item.file.attributes { switch attribute { @@ -577,9 +511,7 @@ private final class FeaturedStickersScreenNode: ViewControllerTracingNode { } } } - return true - }), - PeekControllerMenuItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { _, _ in return true }) + })) ] return (itemNode, StickerPreviewPeekContent(account: strongSelf.context.account, item: item, menu: menuItems)) } else { @@ -599,16 +531,17 @@ private final class FeaturedStickersScreenNode: ViewControllerTracingNode { |> deliverOnMainQueue |> map { isStarred -> (ASDisplayNode, PeekControllerContent)? in if let strongSelf = self { - var menuItems: [PeekControllerMenuItem] = [] + var menuItems: [ContextMenuItem] = [] menuItems = [ - PeekControllerMenuItem(title: strongSelf.presentationData.strings.StickerPack_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 + .action(ContextMenuActionItem(text: strongSelf.presentationData.strings.StickerPack_Send, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Resend"), color: theme.contextMenu.primaryColor) }, action: { _, f in + if let strongSelf = self, let peekController = strongSelf.peekController, let animationNode = (peekController.contentNode as? StickerPreviewPeekContentNode)?.animationNode { + let _ = strongSelf.sendSticker?(.standalone(media: item.file), animationNode, animationNode.bounds) } - }), - PeekControllerMenuItem(title: isStarred ? strongSelf.presentationData.strings.Stickers_RemoveFromFavorites : strongSelf.presentationData.strings.Stickers_AddToFavorites, color: isStarred ? .destructive : .accent, action: { _, _ in + f(.default) + })), + .action(ContextMenuActionItem(text: isStarred ? strongSelf.presentationData.strings.Stickers_RemoveFromFavorites : strongSelf.presentationData.strings.Stickers_AddToFavorites, icon: { theme in generateTintedImage(image: isStarred ? UIImage(bundleImageName: "Chat/Context Menu/Unstar") : UIImage(bundleImageName: "Chat/Context Menu/Rate"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in + f(.default) + if let strongSelf = self { if isStarred { let _ = removeSavedSticker(postbox: strongSelf.context.account.postbox, mediaId: item.file.fileId).start() @@ -616,34 +549,33 @@ private final class FeaturedStickersScreenNode: ViewControllerTracingNode { let _ = addSavedSticker(postbox: strongSelf.context.account.postbox, network: strongSelf.context.account.network, file: item.file).start() } } - return true - }), - PeekControllerMenuItem(title: strongSelf.presentationData.strings.StickerPack_ViewPack, color: .accent, action: { _, _ in + })), + .action(ContextMenuActionItem(text: strongSelf.presentationData.strings.StickerPack_ViewPack, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Sticker"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in + f(.default) + if let strongSelf = self { loop: for attribute in item.file.attributes { switch attribute { - case let .Sticker(_, packReference, _): - if let packReference = packReference { - let controller = StickerPackScreen(context: strongSelf.context, mainStickerPack: packReference, stickerPacks: [packReference], parentNavigationController: strongSelf.controller?.navigationController as? NavigationController, sendSticker: { file, sourceNode, sourceRect in - if let strongSelf = self { - return strongSelf.sendSticker?(file, sourceNode, sourceRect) ?? false - } else { - return false - } - }) - - strongSelf.controller?.view.endEditing(true) - strongSelf.controller?.present(controller, in: .window(.root)) - } - break loop - default: - break + case let .Sticker(_, packReference, _): + if let packReference = packReference { + let controller = StickerPackScreen(context: strongSelf.context, mainStickerPack: packReference, stickerPacks: [packReference], parentNavigationController: strongSelf.controller?.navigationController as? NavigationController, sendSticker: { file, sourceNode, sourceRect in + if let strongSelf = self { + return strongSelf.sendSticker?(file, sourceNode, sourceRect) ?? false + } else { + return false + } + }) + + strongSelf.controller?.view.endEditing(true) + strongSelf.controller?.present(controller, in: .window(.root)) + } + break loop + default: + break } } } - return true - }), - 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 { @@ -654,9 +586,10 @@ private final class FeaturedStickersScreenNode: ViewControllerTracingNode { return nil }, present: { [weak self] content, sourceNode in if let strongSelf = self { - let controller = PeekController(theme: PeekControllerTheme(presentationTheme: strongSelf.presentationData.theme), content: content, sourceNode: { + let controller = PeekController(presentationData: strongSelf.presentationData, content: content, sourceNode: { return sourceNode }) + strongSelf.peekController = controller strongSelf.controller?.presentInGlobalOverlay(controller) return controller } @@ -885,7 +818,7 @@ final class FeaturedStickersScreen: ViewController { override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { super.containerLayoutUpdated(layout, transition: transition) - self.controllerNode.containerLayoutUpdated(layout, navigationHeight: self.navigationHeight, transition: transition) + self.controllerNode.containerLayoutUpdated(layout, navigationHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition) } } @@ -1165,85 +1098,9 @@ private final class FeaturedPaneSearchContentNode: ASDisplayNode { } let account = strongSelf.context.account if install { - let _ = addStickerPackInteractively(postbox: strongSelf.context.account.postbox, info: info, items: []).start() - /*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() - } - |> then(.single((info, items))) - } - 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?.controller?.present(controller, in: .window(.root)) - 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.controller?.navigationController as? 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 - }))*/ - }))*/ + let _ = strongSelf.context.engine.stickers.addStickerPackInteractively(info: info, items: []).start() } else { - let _ = (removeStickerPackInteractively(postbox: account.postbox, id: info.id, option: .delete) + let _ = (strongSelf.context.engine.stickers.removeStickerPackInteractively(id: info.id, option: .delete) |> deliverOnMainQueue).start(next: { _ in }) } @@ -1268,22 +1125,22 @@ private final class FeaturedPaneSearchContentNode: ASDisplayNode { func updateText(_ text: String, languageCode: String?) { let signal: Signal<([(String?, FoundStickerItem)], FoundStickerSets, Bool, FoundStickerSets?)?, NoError> if !text.isEmpty { - let account = self.context.account + let context = self.context let stickers: Signal<[(String?, FoundStickerItem)], NoError> = Signal { subscriber in var signals: Signal<[Signal<(String?, [FoundStickerItem]), NoError>], NoError> = .single([]) let query = text.trimmingCharacters(in: .whitespacesAndNewlines) if query.isSingleEmoji { - signals = .single([searchStickers(account: account, query: text.basicEmoji.0) + signals = .single([context.engine.stickers.searchStickers(query: text.basicEmoji.0) |> map { (nil, $0) }]) } else if query.count > 1, let languageCode = languageCode, !languageCode.isEmpty && languageCode != "emoji" { - var signal = searchEmojiKeywords(postbox: account.postbox, inputLanguageCode: languageCode, query: query.lowercased(), completeMatch: query.count < 3) + var signal = context.engine.stickers.searchEmojiKeywords(inputLanguageCode: languageCode, query: query.lowercased(), completeMatch: query.count < 3) if !languageCode.lowercased().hasPrefix("en") { signal = signal |> mapToSignal { keywords in return .single(keywords) |> then( - searchEmojiKeywords(postbox: account.postbox, inputLanguageCode: "en-US", query: query.lowercased(), completeMatch: query.count < 3) + context.engine.stickers.searchEmojiKeywords(inputLanguageCode: "en-US", query: query.lowercased(), completeMatch: query.count < 3) |> map { englishKeywords in return keywords + englishKeywords } @@ -1296,7 +1153,7 @@ private final class FeaturedPaneSearchContentNode: ASDisplayNode { var signals: [Signal<(String?, [FoundStickerItem]), NoError>] = [] let emoticons = keywords.flatMap { $0.emoticons } for emoji in emoticons { - signals.append(searchStickers(account: self.context.account, query: emoji.basicEmoji.0) + signals.append(context.engine.stickers.searchStickers(query: emoji.basicEmoji.0) |> take(1) |> map { (emoji, $0) }) } @@ -1320,8 +1177,8 @@ private final class FeaturedPaneSearchContentNode: ASDisplayNode { }) } - let local = searchStickerSets(postbox: context.account.postbox, query: text) - let remote = searchStickerSetsRemotely(network: context.account.network, query: text) + let local = context.engine.stickers.searchStickerSets(query: text) + let remote = context.engine.stickers.searchStickerSetsRemotely(query: text) |> delay(0.2, queue: Queue.mainQueue()) let rawPacks = local |> mapToSignal { result -> Signal<(FoundStickerSets, Bool, FoundStickerSets?), NoError> in diff --git a/submodules/TelegramUI/Sources/FetchCachedRepresentations.swift b/submodules/TelegramUI/Sources/FetchCachedRepresentations.swift index 71a0c3e920..3c67d897f4 100644 --- a/submodules/TelegramUI/Sources/FetchCachedRepresentations.swift +++ b/submodules/TelegramUI/Sources/FetchCachedRepresentations.swift @@ -16,9 +16,9 @@ import LocationResources import ImageBlur import TelegramAnimatedStickerNode import WallpaperResources -import Svg import GZip import TelegramUniversalVideoContent +import GradientBackground public func fetchCachedResourceRepresentation(account: Account, resource: MediaResource, representation: CachedMediaResourceRepresentation) -> Signal { if let representation = representation as? CachedStickerAJpegRepresentation { @@ -67,22 +67,6 @@ public func fetchCachedResourceRepresentation(account: Account, resource: MediaR } return fetchCachedBlurredWallpaperRepresentation(resource: resource, resourceData: data, representation: representation) } - } else if let representation = representation as? CachedPatternWallpaperMaskRepresentation { - return account.postbox.mediaBox.resourceData(resource, option: .complete(waitUntilFetchStatus: false)) - |> mapToSignal { data -> Signal in - if !data.complete { - return .complete() - } - return fetchCachedPatternWallpaperMaskRepresentation(resource: resource, resourceData: data, representation: representation) - } - } else if let representation = representation as? CachedPatternWallpaperRepresentation { - return account.postbox.mediaBox.resourceData(resource, option: .complete(waitUntilFetchStatus: false)) - |> mapToSignal { data -> Signal in - if !data.complete { - return .complete() - } - 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)) |> mapToSignal { data -> Signal in @@ -167,8 +151,14 @@ private func videoFirstFrameData(account: Account, resource: MediaResource, chun private func fetchCachedStickerAJpegRepresentation(account: Account, resource: MediaResource, resourceData: MediaResourceData, representation: CachedStickerAJpegRepresentation) -> Signal { return Signal({ subscriber in if let data = try? Data(contentsOf: URL(fileURLWithPath: resourceData.path), options: [.mappedIfSafe]) { - if let image = WebP.convert(fromWebP: data) { - let path = NSTemporaryDirectory() + "\(arc4random64())" + var image: UIImage? + if let webpImage = WebP.convert(fromWebP: data) { + image = webpImage + } else if let pngImage = UIImage(data: data) { + image = pngImage + } + if let image = image { + let path = NSTemporaryDirectory() + "\(Int64.random(in: Int64.min ... Int64.max))" let url = URL(fileURLWithPath: path) let colorData = NSMutableData() @@ -245,7 +235,7 @@ private func fetchCachedScaledImageRepresentation(resource: MediaResource, resou 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 path = NSTemporaryDirectory() + "\(Int64.random(in: Int64.min ... Int64.max))" let url = URL(fileURLWithPath: path) let size: CGSize @@ -318,7 +308,7 @@ private func fetchCachedVideoFirstFrameRepresentation(account: Account, resource let fullSizeImage = try imageGenerator.copyCGImage(at: CMTime(seconds: 0.0, preferredTimescale: asset.duration.timescale), actualTime: nil) - let path = NSTemporaryDirectory() + "\(arc4random64())" + let path = NSTemporaryDirectory() + "\(Int64.random(in: Int64.min ... Int64.max))" let url = URL(fileURLWithPath: path) if let colorDestination = CGImageDestinationCreateWithURL(url as CFURL, kUTTypeJPEG, 1, nil) { @@ -352,7 +342,7 @@ private func fetchCachedScaledVideoFirstFrameRepresentation(account: Account, re return Signal({ subscriber in if let data = try? Data(contentsOf: URL(fileURLWithPath: firstFrame.path), options: [.mappedIfSafe]) { if let image = UIImage(data: data) { - let path = NSTemporaryDirectory() + "\(arc4random64())" + let path = NSTemporaryDirectory() + "\(Int64.random(in: Int64.min ... Int64.max))" let url = URL(fileURLWithPath: path) let size = representation.size @@ -387,7 +377,7 @@ private func fetchCachedBlurredWallpaperRepresentation(resource: MediaResource, 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 path = NSTemporaryDirectory() + "\(Int64.random(in: Int64.min ... Int64.max))" let url = URL(fileURLWithPath: path) if let colorImage = blurredImage(image, radius: 45.0), let colorDestination = CGImageDestinationCreateWithURL(url as CFURL, kUTTypeJPEG, 1, nil) { @@ -410,173 +400,6 @@ private func fetchCachedBlurredWallpaperRepresentation(resource: MediaResource, }) |> runOn(Queue.concurrentDefaultQueue()) } -private func fetchCachedPatternWallpaperMaskRepresentation(resource: MediaResource, resourceData: MediaResourceData, representation: CachedPatternWallpaperMaskRepresentation) -> Signal { - return Signal({ subscriber in - 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 == " runOn(Queue.concurrentDefaultQueue()) -} - -private func fetchCachedPatternWallpaperRepresentation(resource: MediaResource, resourceData: MediaResourceData, representation: CachedPatternWallpaperRepresentation) -> Signal { - return Signal({ subscriber 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()) -} - public func fetchCachedSharedResourceRepresentation(accountManager: AccountManager, resource: MediaResource, representation: CachedMediaResourceRepresentation) -> Signal { if let representation = representation as? CachedScaledImageRepresentation { return accountManager.mediaBox.resourceData(resource, option: .complete(waitUntilFetchStatus: false)) @@ -594,22 +417,6 @@ public func fetchCachedSharedResourceRepresentation(accountManager: AccountManag } return fetchCachedBlurredWallpaperRepresentation(resource: resource, resourceData: data, representation: representation) } - } else if let representation = representation as? CachedPatternWallpaperMaskRepresentation { - return accountManager.mediaBox.resourceData(resource, option: .complete(waitUntilFetchStatus: false)) - |> mapToSignal { data -> Signal in - if !data.complete { - return .complete() - } - return fetchCachedPatternWallpaperMaskRepresentation(resource: resource, resourceData: data, representation: representation) - } - } else if let representation = representation as? CachedPatternWallpaperRepresentation { - return accountManager.mediaBox.resourceData(resource, option: .complete(waitUntilFetchStatus: false)) - |> mapToSignal { data -> Signal in - if !data.complete { - return .complete() - } - return fetchCachedPatternWallpaperRepresentation(resource: resource, resourceData: data, representation: representation) - } } else { return .never() } @@ -619,7 +426,7 @@ private func fetchCachedBlurredWallpaperRepresentation(account: Account, resourc 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 path = NSTemporaryDirectory() + "\(Int64.random(in: Int64.min ... Int64.max))" let url = URL(fileURLWithPath: path) if let colorImage = blurredImage(image, radius: 45.0), let colorDestination = CGImageDestinationCreateWithURL(url as CFURL, kUTTypeJPEG, 1, nil) { @@ -652,7 +459,7 @@ private func fetchCachedAlbumArtworkRepresentation(account: Account, resource: M switch result { case let .artworkData(data): if let image = UIImage(data: data) { - let path = NSTemporaryDirectory() + "\(arc4random64())" + let path = NSTemporaryDirectory() + "\(Int64.random(in: Int64.min ... Int64.max))" let url = URL(fileURLWithPath: path) var size = image.size @@ -694,7 +501,7 @@ private func fetchEmojiThumbnailRepresentation(account: Account, resource: Media return .never() } return Signal({ subscriber in - let path = NSTemporaryDirectory() + "\(arc4random64())" + let path = NSTemporaryDirectory() + "\(Int64.random(in: Int64.min ... Int64.max))" let url = URL(fileURLWithPath: path) let nsString = (resource.emoji as NSString) @@ -803,7 +610,7 @@ private func fetchEmojiRepresentation(account: Account, resource: MediaResource, |> mapToSignal { data in return Signal({ subscriber in if let data = data, let image = UIImage(data: data) { - let path = NSTemporaryDirectory() + "\(arc4random64())" + let path = NSTemporaryDirectory() + "\(Int64.random(in: Int64.min ... Int64.max))" let url = URL(fileURLWithPath: path) let size = CGSize(width: 160.0, height: 160.0) diff --git a/submodules/TelegramUI/Sources/FetchManager.swift b/submodules/TelegramUI/Sources/FetchManager.swift index b14f780da1..b6a80bd23b 100644 --- a/submodules/TelegramUI/Sources/FetchManager.swift +++ b/submodules/TelegramUI/Sources/FetchManager.swift @@ -6,6 +6,7 @@ import SwiftSignalKit import Postbox import TelegramUIPreferences import AccountContext +import UniversalMediaPlayer private struct FetchManagerLocationEntryId: Hashable { let location: FetchManagerLocation diff --git a/submodules/TelegramUI/Sources/ForwardAccessoryPanelNode.swift b/submodules/TelegramUI/Sources/ForwardAccessoryPanelNode.swift index cf920d8030..74f6ab3e69 100644 --- a/submodules/TelegramUI/Sources/ForwardAccessoryPanelNode.swift +++ b/submodules/TelegramUI/Sources/ForwardAccessoryPanelNode.swift @@ -65,6 +65,8 @@ func textStringForForwardedMessage(_ message: Message, strings: PresentationStri return (strings.ForwardedPolls(1), true) case let dice as TelegramMediaDice: return (dice.emoji, true) + case let invoice as TelegramMediaInvoice: + return (invoice.title, true) default: break } @@ -86,6 +88,8 @@ final class ForwardAccessoryPanelNode: AccessoryPanelNode { let context: AccountContext var theme: PresentationTheme var strings: PresentationStrings + + private var validLayout: (size: CGSize, interfaceState: ChatPresentationInterfaceState)? init(context: AccountContext, messageIds: [MessageId], theme: PresentationTheme, strings: PresentationStrings) { self.context = context @@ -100,7 +104,7 @@ final class ForwardAccessoryPanelNode: AccessoryPanelNode { self.closeButton.displaysAsynchronously = false self.lineNode = ASImageNode() - self.lineNode.displayWithoutProcessing = true + self.lineNode.displayWithoutProcessing = false self.lineNode.displaysAsynchronously = false self.lineNode.image = PresentationResourcesChat.chatInputPanelVerticalSeparatorLineImage(theme) @@ -156,8 +160,10 @@ final class ForwardAccessoryPanelNode: AccessoryPanelNode { headerString = "Forward messages" } strongSelf.actionArea.accessibilityLabel = "\(headerString). From: \(authors).\n\(text)" - - strongSelf.setNeedsLayout() + + if let (size, interfaceState) = strongSelf.validLayout { + strongSelf.updateState(size: size, interfaceState: interfaceState) + } } })) } @@ -189,34 +195,36 @@ final class ForwardAccessoryPanelNode: AccessoryPanelNode { self.textNode.attributedText = NSAttributedString(string: text, font: Font.regular(15.0), textColor: self.theme.chat.inputPanel.primaryTextColor) } - self.setNeedsLayout() + if let (size, interfaceState) = self.validLayout { + self.updateState(size: size, interfaceState: interfaceState) + } } } - + override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize { return CGSize(width: constrainedSize.width, height: 45.0) } - - override func layout() { - super.layout() - - let bounds = self.bounds + + override func updateState(size: CGSize, interfaceState: ChatPresentationInterfaceState) { + self.validLayout = (size, interfaceState) + + let bounds = CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: 45.0)) let leftInset: CGFloat = 55.0 let textLineInset: CGFloat = 10.0 let rightInset: CGFloat = 55.0 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) self.closeButton.frame = closeButtonFrame - + self.actionArea.frame = CGRect(origin: CGPoint(x: leftInset, y: 2.0), size: CGSize(width: closeButtonFrame.minX - leftInset, height: bounds.height)) - + self.lineNode.frame = CGRect(origin: CGPoint(x: leftInset, y: 8.0), size: CGSize(width: 2.0, height: bounds.size.height - 10.0)) - + let titleSize = self.titleNode.updateLayout(CGSize(width: bounds.size.width - leftInset - textLineInset - rightInset - textRightInset, height: bounds.size.height)) self.titleNode.frame = CGRect(origin: CGPoint(x: leftInset + textLineInset, y: 7.0), size: titleSize) - + let textSize = self.textNode.updateLayout(CGSize(width: bounds.size.width - leftInset - textLineInset - rightInset - textRightInset, height: bounds.size.height)) self.textNode.frame = CGRect(origin: CGPoint(x: leftInset + textLineInset, y: 25.0), size: textSize) } diff --git a/submodules/TelegramUI/Sources/GifPaneSearchContentNode.swift b/submodules/TelegramUI/Sources/GifPaneSearchContentNode.swift index 69b9cbf440..6c79d3ebfc 100644 --- a/submodules/TelegramUI/Sources/GifPaneSearchContentNode.swift +++ b/submodules/TelegramUI/Sources/GifPaneSearchContentNode.swift @@ -25,17 +25,17 @@ class PaneGifSearchForQueryResult { } } -func paneGifSearchForQuery(account: Account, query: String, offset: String?, incompleteResults: Bool = false, staleCachedResults: Bool = false, delayRequest: Bool = true, updateActivity: ((Bool) -> Void)?) -> Signal { - let contextBot = account.postbox.transaction { transaction -> String in +func paneGifSearchForQuery(context: AccountContext, query: String, offset: String?, incompleteResults: Bool = false, staleCachedResults: Bool = false, delayRequest: Bool = true, updateActivity: ((Bool) -> Void)?) -> Signal { + let contextBot = context.account.postbox.transaction { transaction -> String in let configuration = currentSearchBotsConfiguration(transaction: transaction) return configuration.gifBotUsername ?? "gif" } |> mapToSignal { botName -> Signal in - return resolvePeerByName(account: account, name: botName) + return context.engine.peers.resolvePeerByName(name: botName) } |> mapToSignal { peerId -> Signal in if let peerId = peerId { - return account.postbox.loadedPeerWithId(peerId) + return context.account.postbox.loadedPeerWithId(peerId) |> map { peer -> Peer? in return peer } @@ -46,7 +46,7 @@ func paneGifSearchForQuery(account: Account, query: String, offset: String?, inc } |> mapToSignal { peer -> Signal<(ChatPresentationInputQueryResult?, Bool, Bool), 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, offset: offset ?? "", incompleteResults: incompleteResults, staleCachedResults: staleCachedResults, limit: 1) + let results = requestContextResults(context: context, botId: user.id, query: query, peerId: context.account.peerId, offset: offset ?? "", incompleteResults: incompleteResults, staleCachedResults: staleCachedResults, limit: 1) |> map { results -> (ChatPresentationInputQueryResult?, Bool, Bool) in return (.contextRequestResult(user, results?.results), results != nil, results?.isStale ?? false) } @@ -101,7 +101,8 @@ func paneGifSearchForQuery(account: Account, query: String, offset: String?, inc previews.append(TelegramMediaImageRepresentation( dimensions: dimensions, resource: thumbnailResource, - progressiveSizes: [] + progressiveSizes: [], + immediateThumbnailData: nil )) } } @@ -202,7 +203,7 @@ final class GifPaneSearchContentNode: ASDisplayNode & PaneSearchContentNode { let signal: Signal<([MultiplexedVideoNodeFile], String?)?, NoError> if !text.isEmpty { - signal = paneGifSearchForQuery(account: self.context.account, query: text, offset: "", updateActivity: self.updateActivity) + signal = paneGifSearchForQuery(context: self.context, query: text, offset: "", updateActivity: self.updateActivity) |> map { result -> ([MultiplexedVideoNodeFile], String?)? in if let result = result { return (result.files, result.nextOffset) @@ -251,7 +252,7 @@ final class GifPaneSearchContentNode: ASDisplayNode & PaneSearchContentNode { self.isLoadingNextResults = true let signal: Signal<([MultiplexedVideoNodeFile], String?)?, NoError> - signal = paneGifSearchForQuery(account: self.context.account, query: text, offset: nextOffsetValue, updateActivity: self.updateActivity) + signal = paneGifSearchForQuery(context: self.context, query: text, offset: nextOffsetValue, updateActivity: self.updateActivity) |> map { result -> ([MultiplexedVideoNodeFile], String?)? in if let result = result { return (result.files, result.nextOffset) @@ -344,9 +345,9 @@ final class GifPaneSearchContentNode: ASDisplayNode & PaneSearchContentNode { multiplexedNode.fileSelected = { [weak self] file, sourceNode, sourceRect in if let (collection, result) = file.contextResult { - let _ = self?.controllerInteraction.sendBotContextResultAsGif(collection, result, sourceNode, sourceRect) + let _ = self?.controllerInteraction.sendBotContextResultAsGif(collection, result, sourceNode, sourceRect, false) } else { - let _ = self?.controllerInteraction.sendGif(file.file, sourceNode, sourceRect) + let _ = self?.controllerInteraction.sendGif(file.file, sourceNode, sourceRect, false, false) } } diff --git a/submodules/TelegramUI/Sources/HashtagChatInputContextPanelNode.swift b/submodules/TelegramUI/Sources/HashtagChatInputContextPanelNode.swift index c462c8fdc3..13d813efca 100644 --- a/submodules/TelegramUI/Sources/HashtagChatInputContextPanelNode.swift +++ b/submodules/TelegramUI/Sources/HashtagChatInputContextPanelNode.swift @@ -141,7 +141,7 @@ final class HashtagChatInputContextPanelNode: ChatInputContextPanelNode { } }, removeRequested: { [weak self] text in if let strongSelf = self { - let _ = removeRecentlyUsedHashtag(postbox: strongSelf.context.account.postbox, string: text).start() + let _ = strongSelf.context.engine.messages.removeRecentlyUsedHashtag(string: text).start() strongSelf.revealedHashtag = nil } }) @@ -190,7 +190,10 @@ final class HashtagChatInputContextPanelNode: ChatInputContextPanelNode { if let topItemOffset = topItemOffset { let position = strongSelf.listView.layer.position - strongSelf.listView.layer.animatePosition(from: CGPoint(x: position.x, y: position.y + (strongSelf.listView.bounds.size.height - topItemOffset)), to: position, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring) + strongSelf.listView.position = CGPoint(x: position.x, y: position.y + (strongSelf.listView.bounds.size.height - topItemOffset)) + ContainedViewLayoutTransition.animated(duration: 0.3, curve: .spring).animateView { + strongSelf.listView.position = position + } } } }) @@ -256,5 +259,14 @@ final class HashtagChatInputContextPanelNode: ChatInputContextPanelNode { let listViewFrame = self.listView.frame return self.listView.hitTest(CGPoint(x: point.x - listViewFrame.minX, y: point.y - listViewFrame.minY), with: event) } + + override var topItemFrame: CGRect? { + var topItemFrame: CGRect? + self.listView.forEachItemNode { itemNode in + if topItemFrame == nil { + topItemFrame = itemNode.frame + } + } + return topItemFrame + } } - diff --git a/submodules/TelegramUI/Sources/HorizontalListContextResultsChatInputContextPanelNode.swift b/submodules/TelegramUI/Sources/HorizontalListContextResultsChatInputContextPanelNode.swift index 962fb53c3a..81340bfc95 100644 --- a/submodules/TelegramUI/Sources/HorizontalListContextResultsChatInputContextPanelNode.swift +++ b/submodules/TelegramUI/Sources/HorizontalListContextResultsChatInputContextPanelNode.swift @@ -11,6 +11,7 @@ import TelegramUIPreferences import MergeLists import AccountContext import StickerPackPreviewUI +import ContextUI private struct ChatContextResultStableId: Hashable { let result: ChatContextResult @@ -145,19 +146,23 @@ final class HorizontalListContextResultsChatInputContextPanelNode: ChatInputCont strongSelf.listView.forEachItemNode { itemNode in if itemNode.frame.contains(convertedPoint), let itemNode = itemNode as? HorizontalListContextResultsChatInputPanelItemNode, let item = itemNode.item { if case let .internalReference(internalReference) = item.result, let file = internalReference.file, file.isSticker { - var menuItems: [PeekControllerMenuItem] = [] - menuItems.append(PeekControllerMenuItem(title: strongSelf.strings.StickerPack_Send, color: .accent, font: .bold, action: { _, _ in - return item.resultSelected(item.result, itemNode, itemNode.bounds) - })) + var menuItems: [ContextMenuItem] = [] + menuItems.append(.action(ContextMenuActionItem(text: strongSelf.strings.StickerPack_Send, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Resend"), color: theme.contextMenu.primaryColor) }, action: { _, f in + f(.default) + + let _ = item.resultSelected(item.result, itemNode, itemNode.bounds) + }))) for case let .Sticker(_, packReference, _) in file.attributes { guard let packReference = packReference else { continue } - menuItems.append(PeekControllerMenuItem(title: strongSelf.strings.StickerPack_ViewPack, color: .accent, action: { _, _ in + menuItems.append(.action(ContextMenuActionItem(text: strongSelf.strings.StickerPack_ViewPack, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Sticker"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in + f(.default) + if let strongSelf = self { 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 + return strongSelf.interfaceInteraction?.sendSticker(file, false, sourceNode, sourceRect) ?? false } else { return false } @@ -166,21 +171,29 @@ final class HorizontalListContextResultsChatInputContextPanelNode: ChatInputCont strongSelf.interfaceInteraction?.getNavigationController()?.view.window?.endEditing(true) strongSelf.interfaceInteraction?.presentController(controller, nil) } - return true - })) + }))) } selectedItemNodeAndContent = (itemNode, StickerPreviewPeekContent(account: item.account, item: .found(FoundStickerItem(file: file, stringRepresentations: [])), menu: menuItems)) } else { - var menuItems: [PeekControllerMenuItem] = [] + var menuItems: [ContextMenuItem] = [] if case let .internalReference(internalReference) = item.result, let file = internalReference.file, file.isAnimated { - menuItems.append(PeekControllerMenuItem(title: strongSelf.strings.Preview_SaveGif, color: .accent, action: { _, _ in + menuItems.append(.action(ContextMenuActionItem(text: strongSelf.strings.Preview_SaveGif, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Save"), color: theme.actionSheet.primaryTextColor) + }, action: { _, f in + f(.dismissWithoutContent) + + guard let strongSelf = self else { + return + } let _ = addSavedGif(postbox: strongSelf.context.account.postbox, fileReference: .standalone(media: file)).start() - return true - })) + }))) } - menuItems.append(PeekControllerMenuItem(title: strongSelf.strings.ShareMenu_Send, color: .accent, font: .bold, action: { _, _ in - return item.resultSelected(item.result, itemNode, itemNode.bounds) - })) + menuItems.append(.action(ContextMenuActionItem(text: strongSelf.strings.ShareMenu_Send, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Resend"), color: theme.actionSheet.primaryTextColor) + }, action: { _, f in + f(.default) + item.resultSelected(item.result, itemNode, itemNode.bounds) + }))) selectedItemNodeAndContent = (itemNode, ChatContextResultPeekContent(account: item.account, contextResult: item.result, menu: menuItems)) } } @@ -190,7 +203,8 @@ final class HorizontalListContextResultsChatInputContextPanelNode: ChatInputCont return nil }, present: { [weak self] content, sourceNode in if let strongSelf = self { - let controller = PeekController(theme: PeekControllerTheme(presentationTheme: strongSelf.theme), content: content, sourceNode: { + let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } + let controller = PeekController(presentationData: presentationData, content: content, sourceNode: { return sourceNode }) strongSelf.interfaceInteraction?.presentGlobalOverlayController(controller, nil) @@ -220,7 +234,7 @@ final class HorizontalListContextResultsChatInputContextPanelNode: ChatInputCont let geoPoint = currentProcessedResults.geoPoint.flatMap { geoPoint -> (Double, Double) in return (geoPoint.latitude, geoPoint.longitude) } - self.loadMoreDisposable.set((requestChatContextResults(account: self.context.account, botId: currentProcessedResults.botId, peerId: currentProcessedResults.peerId, query: currentProcessedResults.query, location: .single(geoPoint), offset: nextOffset) + self.loadMoreDisposable.set((self.context.engine.messages.requestChatContextResults(botId: currentProcessedResults.botId, peerId: currentProcessedResults.peerId, query: currentProcessedResults.query, location: .single(geoPoint), offset: nextOffset) |> map { results -> ChatContextResultCollection? in return results?.results } @@ -303,12 +317,17 @@ final class HorizontalListContextResultsChatInputContextPanelNode: ChatInputCont self.listView.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, updateSizeAndInsets: nil, updateOpaqueState: HorizontalListContextResultsOpaqueState(entryCount: transition.entryCount, hasMore: transition.hasMore), completion: { [weak self] _ in if let strongSelf = self, firstTime { let position = strongSelf.listView.position - strongSelf.listView.isHidden = false - strongSelf.listView.layer.animatePosition(from: CGPoint(x: position.x, y: position.y + strongSelf.listView.bounds.size.width), to: position, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring) - - strongSelf.separatorNode.isHidden = false let separatorPosition = strongSelf.separatorNode.layer.position - strongSelf.separatorNode.layer.animatePosition(from: CGPoint(x: separatorPosition.x, y: separatorPosition.y + strongSelf.listView.bounds.size.width), to: separatorPosition, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring) + + strongSelf.listView.isHidden = false + strongSelf.separatorNode.isHidden = false + + strongSelf.listView.position = CGPoint(x: position.x, y: position.y + strongSelf.listView.bounds.size.width) + strongSelf.separatorNode.position = CGPoint(x: separatorPosition.x, y: separatorPosition.y + strongSelf.listView.bounds.size.width) + ContainedViewLayoutTransition.animated(duration: 0.3, curve: .spring).animateView { + strongSelf.listView.position = position + strongSelf.separatorNode.position = separatorPosition + } } }) } diff --git a/submodules/TelegramUI/Sources/HorizontalListContextResultsChatInputPanelItem.swift b/submodules/TelegramUI/Sources/HorizontalListContextResultsChatInputPanelItem.swift index b02346b004..840fa30b8d 100644 --- a/submodules/TelegramUI/Sources/HorizontalListContextResultsChatInputPanelItem.swift +++ b/submodules/TelegramUI/Sources/HorizontalListContextResultsChatInputPanelItem.swift @@ -352,7 +352,7 @@ final class HorizontalListContextResultsChatInputPanelItemNode: ListViewItemNode if let stickerFile = stickerFile { 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, progressiveSizes: []) + let tmpRepresentation = TelegramMediaImageRepresentation(dimensions: PixelDimensions(CGSize(width: fittedImageDimensions.width * 2.0, height: fittedImageDimensions.height * 2.0)), resource: imageResource, progressiveSizes: [], immediateThumbnailData: 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), synchronousLoad: true) } diff --git a/submodules/TelegramUI/Sources/HorizontalStickerGridItem.swift b/submodules/TelegramUI/Sources/HorizontalStickerGridItem.swift index 3f7d1c7fe6..c8072be6b2 100755 --- a/submodules/TelegramUI/Sources/HorizontalStickerGridItem.swift +++ b/submodules/TelegramUI/Sources/HorizontalStickerGridItem.swift @@ -49,9 +49,9 @@ final class HorizontalStickerGridItem: GridItem { final class HorizontalStickerGridItemNode: GridItemNode { private var currentState: (Account, HorizontalStickerGridItem, CGSize)? - private let imageNode: TransformImageNode - private var animationNode: AnimatedStickerNode? - private var placeholderNode: StickerShimmerEffectNode? + let imageNode: TransformImageNode + private(set) var animationNode: AnimatedStickerNode? + private(set) var placeholderNode: StickerShimmerEffectNode? private let stickerFetchedDisposable = MetaDisposable() diff --git a/submodules/TelegramUI/Sources/HorizontalStickersChatContextPanelNode.swift b/submodules/TelegramUI/Sources/HorizontalStickersChatContextPanelNode.swift index 462ba94af1..0ab47cce87 100755 --- a/submodules/TelegramUI/Sources/HorizontalStickersChatContextPanelNode.swift +++ b/submodules/TelegramUI/Sources/HorizontalStickersChatContextPanelNode.swift @@ -11,6 +11,7 @@ import TelegramUIPreferences import MergeLists import AccountContext import StickerPackPreviewUI +import ContextUI final class HorizontalStickersChatContextPanelInteraction { var previewedStickerItem: StickerPackItem? @@ -69,7 +70,7 @@ private struct StickerEntry: Identifiable, Comparable { return HorizontalStickerGridItem(account: account, file: self.file, theme: theme, isPreviewed: { item in return false//stickersInteraction.previewedStickerItem == item }, sendSticker: { file, node, rect in - let _ = interfaceInteraction.sendSticker(file, node, rect) + let _ = interfaceInteraction.sendSticker(file, true, node, rect) }) } } @@ -174,12 +175,16 @@ final class HorizontalStickersChatContextPanelNode: ChatInputContextPanelNode { |> deliverOnMainQueue |> map { isStarred -> (ASDisplayNode, PeekControllerContent)? in if let strongSelf = self, let controllerInteraction = strongSelf.controllerInteraction { - var menuItems: [PeekControllerMenuItem] = [] + var menuItems: [ContextMenuItem] = [] menuItems = [ - PeekControllerMenuItem(title: strongSelf.strings.StickerPack_Send, color: .accent, font: .bold, action: { _, _ in - return controllerInteraction.sendSticker(.standalone(media: item.file), nil, true, itemNode, itemNode.bounds) - }), - PeekControllerMenuItem(title: isStarred ? strongSelf.strings.Stickers_RemoveFromFavorites : strongSelf.strings.Stickers_AddToFavorites, color: isStarred ? .destructive : .accent, action: { _, _ in + .action(ContextMenuActionItem(text: strongSelf.strings.StickerPack_Send, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Resend"), color: theme.contextMenu.primaryColor) }, action: { _, f in + f(.default) + + let _ = controllerInteraction.sendSticker(.standalone(media: item.file), false, false, nil, true, itemNode, itemNode.bounds) + })), + .action(ContextMenuActionItem(text: isStarred ? strongSelf.strings.Stickers_RemoveFromFavorites : strongSelf.strings.Stickers_AddToFavorites, icon: { theme in generateTintedImage(image: isStarred ? UIImage(bundleImageName: "Chat/Context Menu/Unstar") : UIImage(bundleImageName: "Chat/Context Menu/Rate"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in + f(.default) + if let strongSelf = self { if isStarred { let _ = removeSavedSticker(postbox: strongSelf.context.account.postbox, mediaId: item.file.fileId).start() @@ -187,9 +192,10 @@ final class HorizontalStickersChatContextPanelNode: ChatInputContextPanelNode { let _ = addSavedSticker(postbox: strongSelf.context.account.postbox, network: strongSelf.context.account.network, file: item.file).start() } } - return true - }), - PeekControllerMenuItem(title: strongSelf.strings.StickerPack_ViewPack, color: .accent, action: { _, _ in + })), + .action(ContextMenuActionItem(text: strongSelf.strings.StickerPack_ViewPack, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Sticker"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in + f(.default) + if let strongSelf = self, let controllerInteraction = strongSelf.controllerInteraction { loop: for attribute in item.file.attributes { switch attribute { @@ -197,7 +203,7 @@ final class HorizontalStickersChatContextPanelNode: ChatInputContextPanelNode { if let packReference = packReference { 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, nil, true, sourceNode, sourceRect) + return controllerInteraction.sendSticker(file, false, false, nil, true, sourceNode, sourceRect) } else { return false } @@ -211,11 +217,8 @@ final class HorizontalStickersChatContextPanelNode: ChatInputContextPanelNode { break } } - return true } - 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 { @@ -227,7 +230,8 @@ final class HorizontalStickersChatContextPanelNode: ChatInputContextPanelNode { return nil }, present: { [weak self] content, sourceNode in if let strongSelf = self { - let controller = PeekController(theme: PeekControllerTheme(presentationTheme: strongSelf.theme), content: content, sourceNode: { + let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } + let controller = PeekController(presentationData: presentationData, content: content, sourceNode: { return sourceNode }) strongSelf.interfaceInteraction?.presentGlobalOverlayController(controller, nil) diff --git a/submodules/TelegramUI/Sources/InChatPrefetchManager.swift b/submodules/TelegramUI/Sources/InChatPrefetchManager.swift index ad8384ef9e..0c22fb6851 100644 --- a/submodules/TelegramUI/Sources/InChatPrefetchManager.swift +++ b/submodules/TelegramUI/Sources/InChatPrefetchManager.swift @@ -6,6 +6,7 @@ import SyncCore import TelegramUIPreferences import AccountContext import PhotoResources +import UniversalMediaPlayer private final class PrefetchMediaContext { let fetchDisposable = MetaDisposable() diff --git a/submodules/TelegramUI/Sources/InlineReactionSearchPanel.swift b/submodules/TelegramUI/Sources/InlineReactionSearchPanel.swift index 817dcfd341..83151604e4 100644 --- a/submodules/TelegramUI/Sources/InlineReactionSearchPanel.swift +++ b/submodules/TelegramUI/Sources/InlineReactionSearchPanel.swift @@ -10,6 +10,7 @@ import TelegramPresentationData import TelegramUIPreferences import AccountContext import StickerPackPreviewUI +import ContextUI private final class InlineReactionSearchStickersNode: ASDisplayNode, UIScrollViewDelegate { private final class DisplayItem { @@ -38,7 +39,7 @@ private final class InlineReactionSearchStickersNode: ASDisplayNode, UIScrollVie var previewedStickerItem: StickerPackItem? - var updateBackgroundOffset: ((CGFloat, ContainedViewLayoutTransition) -> Void)? + var updateBackgroundOffset: ((CGFloat, Bool, ContainedViewLayoutTransition) -> Void)? var sendSticker: ((FileMediaReference, ASDisplayNode, CGRect) -> Void)? var getControllerInteraction: (() -> ChatControllerInteraction?)? @@ -88,12 +89,16 @@ private final class InlineReactionSearchStickersNode: ASDisplayNode, UIScrollVie |> deliverOnMainQueue |> map { isStarred -> (ASDisplayNode, PeekControllerContent)? in if let strongSelf = self, let controllerInteraction = strongSelf.getControllerInteraction?() { - var menuItems: [PeekControllerMenuItem] = [] + var menuItems: [ContextMenuItem] = [] menuItems = [ - PeekControllerMenuItem(title: strongSelf.strings.StickerPack_Send, color: .accent, font: .bold, action: { _, _ in - return controllerInteraction.sendSticker(.standalone(media: item.file), nil, true, itemNode, itemNode.bounds) - }), - PeekControllerMenuItem(title: isStarred ? strongSelf.strings.Stickers_RemoveFromFavorites : strongSelf.strings.Stickers_AddToFavorites, color: isStarred ? .destructive : .accent, action: { _, _ in + .action(ContextMenuActionItem(text: strongSelf.strings.StickerPack_Send, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Resend"), color: theme.contextMenu.primaryColor) }, action: { _, f in + f(.default) + + let _ = controllerInteraction.sendSticker(.standalone(media: item.file), false, false, nil, true, itemNode, itemNode.bounds) + })), + .action(ContextMenuActionItem(text: isStarred ? strongSelf.strings.Stickers_RemoveFromFavorites : strongSelf.strings.Stickers_AddToFavorites, icon: { theme in generateTintedImage(image: isStarred ? UIImage(bundleImageName: "Chat/Context Menu/Unstar") : UIImage(bundleImageName: "Chat/Context Menu/Rate"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in + f(.default) + if let strongSelf = self { if isStarred { let _ = removeSavedSticker(postbox: strongSelf.context.account.postbox, mediaId: item.file.fileId).start() @@ -101,9 +106,10 @@ private final class InlineReactionSearchStickersNode: ASDisplayNode, UIScrollVie let _ = addSavedSticker(postbox: strongSelf.context.account.postbox, network: strongSelf.context.account.network, file: item.file).start() } } - return true - }), - PeekControllerMenuItem(title: strongSelf.strings.StickerPack_ViewPack, color: .accent, action: { _, _ in + })), + .action(ContextMenuActionItem(text: strongSelf.strings.StickerPack_ViewPack, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Sticker"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in + f(.default) + if let strongSelf = self, let controllerInteraction = strongSelf.getControllerInteraction?() { loop: for attribute in item.file.attributes { switch attribute { @@ -111,7 +117,7 @@ private final class InlineReactionSearchStickersNode: ASDisplayNode, UIScrollVie if let packReference = packReference { 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.getControllerInteraction?() { - return controllerInteraction.sendSticker(file, nil, true, sourceNode, sourceRect) + return controllerInteraction.sendSticker(file, false, false, nil, true, sourceNode, sourceRect) } else { return false } @@ -125,12 +131,8 @@ private final class InlineReactionSearchStickersNode: ASDisplayNode, UIScrollVie break } } - return true } - 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 { return nil @@ -141,7 +143,8 @@ private final class InlineReactionSearchStickersNode: ASDisplayNode, UIScrollVie return nil }, present: { [weak self] content, sourceNode in if let strongSelf = self { - let controller = PeekController(theme: PeekControllerTheme(presentationTheme: strongSelf.theme), content: content, sourceNode: { + let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } + let controller = PeekController(presentationData: presentationData, content: content, sourceNode: { return sourceNode }) strongSelf.getControllerInteraction?()?.presentGlobalOverlayController(controller, nil) @@ -172,13 +175,13 @@ private final class InlineReactionSearchStickersNode: ASDisplayNode, UIScrollVie func scrollViewDidScroll(_ scrollView: UIScrollView) { if !self.ignoreScrolling { self.updateVisibleItems(synchronous: false) - self.updateBackground(transition: .immediate) + self.updateBackground(animateIn: false, transition: .immediate) } } - private func updateBackground(transition: ContainedViewLayoutTransition) { + private func updateBackground(animateIn: Bool, transition: ContainedViewLayoutTransition) { if let topInset = self.topInset { - self.updateBackgroundOffset?(max(0.0, -self.scrollNode.view.contentOffset.y + topInset), transition) + self.updateBackgroundOffset?(max(0.0, -self.scrollNode.view.contentOffset.y + topInset), animateIn, transition) } } @@ -225,7 +228,7 @@ private final class InlineReactionSearchStickersNode: ASDisplayNode, UIScrollVie let currentBackgroundOffset = max(0.0, -self.scrollNode.view.contentOffset.y + topInset) if abs(currentBackgroundOffset - previousBackgroundOffset) > .ulpOfOne { transition.animateOffsetAdditive(node: self.scrollNode, offset: currentBackgroundOffset - previousBackgroundOffset) - self.updateBackground(transition: transition) + self.updateBackground(animateIn: false, transition: transition) } } else { self.animateInOnLayout = true @@ -244,7 +247,7 @@ private final class InlineReactionSearchStickersNode: ASDisplayNode, UIScrollVie self.validLayout = size if self.animateInOnLayout { - self.updateBackgroundOffset?(size.height, .immediate) + self.updateBackgroundOffset?(size.height, false, .immediate) } var synchronous = false @@ -262,12 +265,18 @@ private final class InlineReactionSearchStickersNode: ASDisplayNode, UIScrollVie var backgroundTransition = transition + var animateIn = false if self.animateInOnLayout { + animateIn = true self.animateInOnLayout = false backgroundTransition = .animated(duration: 0.3, curve: .spring) if let topInset = self.topInset { let currentBackgroundOffset = max(0.0, -self.scrollNode.view.contentOffset.y + topInset) - backgroundTransition.animateOffsetAdditive(node: self.scrollNode, offset: currentBackgroundOffset - size.height) + let bounds = self.scrollNode.bounds + self.scrollNode.bounds = bounds.offsetBy(dx: 0.0, dy: currentBackgroundOffset - size.height) + backgroundTransition.animateView { + self.scrollNode.bounds = bounds + } } } else { if let previousBackgroundOffset = previousBackgroundOffset, let topInset = self.topInset { @@ -278,7 +287,7 @@ private final class InlineReactionSearchStickersNode: ASDisplayNode, UIScrollVie } } - self.updateBackground(transition: backgroundTransition) + self.updateBackground(animateIn: animateIn, transition: backgroundTransition) } private func updateItemsLayout(width: CGFloat) { @@ -376,7 +385,7 @@ private final class InlineReactionSearchStickersNode: ASDisplayNode, UIScrollVie } } -private let backroundDiameter: CGFloat = 20.0 +private let backgroundDiameter: CGFloat = 20.0 private let shadowBlur: CGFloat = 6.0 final class InlineReactionSearchPanel: ChatInputContextPanelNode { @@ -399,8 +408,8 @@ final class InlineReactionSearchPanel: ChatInputContextPanelNode { self.backgroundNode = ASDisplayNode() - let shadowImage = generateImage(CGSize(width: backroundDiameter + shadowBlur * 2.0, height: floor(backroundDiameter / 2.0 + shadowBlur)), rotatedContext: { size, context in - let diameter = backroundDiameter + let shadowImage = generateImage(CGSize(width: backgroundDiameter + shadowBlur * 2.0, height: floor(backgroundDiameter / 2.0 + shadowBlur)), rotatedContext: { size, context in + let diameter = backgroundDiameter let shadow = UIColor(white: 0.0, alpha: 0.5) context.clear(CGRect(origin: CGPoint(), size: size)) @@ -419,7 +428,7 @@ final class InlineReactionSearchPanel: ChatInputContextPanelNode { context.setFillColor(theme.list.plainBackgroundColor.cgColor) context.fillEllipse(in: CGRect(origin: CGPoint(x: shadowBlur, y: shadowBlur), size: CGSize(width: diameter, height: diameter))) - })?.stretchableImage(withLeftCapWidth: Int(backroundDiameter / 2.0 + shadowBlur), topCapHeight: 0) + })?.stretchableImage(withLeftCapWidth: Int(backgroundDiameter / 2.0 + shadowBlur), topCapHeight: 0) self.backgroundTopLeftNode = ASImageNode() self.backgroundTopLeftNode.image = shadowImage @@ -457,23 +466,28 @@ final class InlineReactionSearchPanel: ChatInputContextPanelNode { return self?.controllerInteraction } - self.stickersNode.updateBackgroundOffset = { [weak self] offset, transition in + self.stickersNode.updateBackgroundOffset = { [weak self] offset, animateIn, transition in guard let strongSelf = self, let (_, _) = strongSelf.validLayout else { return } - transition.updateFrame(node: strongSelf.backgroundContainerNode, frame: CGRect(origin: CGPoint(x: 0.0, y: offset), size: CGSize()), beginWithCurrentState: false) - + if animateIn { + transition.animateView { + strongSelf.backgroundContainerNode.frame = CGRect(origin: CGPoint(x: 0.0, y: offset), size: CGSize()) + } + } else { + transition.updateFrame(node: strongSelf.backgroundContainerNode, frame: CGRect(origin: CGPoint(x: 0.0, y: offset), size: CGSize()), beginWithCurrentState: false) + } let cornersTransitionDistance: CGFloat = 20.0 let cornersTransition: CGFloat = max(0.0, min(1.0, (cornersTransitionDistance - offset) / cornersTransitionDistance)) - transition.updateSublayerTransformScaleAndOffset(node: strongSelf.backgroundTopLeftContainerNode, scale: 1.0, offset: CGPoint(x: -cornersTransition * backroundDiameter, y: 0.0), beginWithCurrentState: true) - transition.updateSublayerTransformScaleAndOffset(node: strongSelf.backgroundTopRightContainerNode, scale: 1.0, offset: CGPoint(x: cornersTransition * backroundDiameter, y: 0.0), beginWithCurrentState: true) + transition.updateSublayerTransformScaleAndOffset(node: strongSelf.backgroundTopLeftContainerNode, scale: 1.0, offset: CGPoint(x: -cornersTransition * backgroundDiameter, y: 0.0), beginWithCurrentState: true) + transition.updateSublayerTransformScaleAndOffset(node: strongSelf.backgroundTopRightContainerNode, scale: 1.0, offset: CGPoint(x: cornersTransition * backgroundDiameter, y: 0.0), beginWithCurrentState: true) } self.stickersNode.sendSticker = { [weak self] file, node, rect in guard let strongSelf = self else { return } - let _ = strongSelf.controllerInteraction?.sendSticker(file, strongSelf.query, true, node, rect) + let _ = strongSelf.controllerInteraction?.sendSticker(file, false, false, strongSelf.query, true, node, rect) } self.view.disablesInteractiveTransitionGestureRecognizer = true @@ -495,13 +509,13 @@ final class InlineReactionSearchPanel: ChatInputContextPanelNode { transition.updateFrame(node: self.containerNode, frame: CGRect(origin: CGPoint(), size: size)) - transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: backroundDiameter / 2.0), size: size)) + transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: backgroundDiameter / 2.0), size: size)) - transition.updateFrame(node: self.backgroundTopLeftContainerNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -shadowBlur), size: CGSize(width: size.width / 2.0, height: backroundDiameter / 2.0 + shadowBlur))) - transition.updateFrame(node: self.backgroundTopRightContainerNode, frame: CGRect(origin: CGPoint(x: size.width / 2.0, y: -shadowBlur), size: CGSize(width: size.width - size.width / 2.0, height: backroundDiameter / 2.0 + shadowBlur))) + transition.updateFrame(node: self.backgroundTopLeftContainerNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -shadowBlur), size: CGSize(width: size.width / 2.0, height: backgroundDiameter / 2.0 + shadowBlur))) + transition.updateFrame(node: self.backgroundTopRightContainerNode, frame: CGRect(origin: CGPoint(x: size.width / 2.0, y: -shadowBlur), size: CGSize(width: size.width - size.width / 2.0, height: backgroundDiameter / 2.0 + shadowBlur))) - transition.updateFrame(node: self.backgroundTopLeftNode, frame: CGRect(origin: CGPoint(x: -shadowBlur, y: 0.0), size: CGSize(width: size.width + shadowBlur * 2.0, height: backroundDiameter / 2.0 + shadowBlur))) - transition.updateFrame(node: self.backgroundTopRightNode, frame: CGRect(origin: CGPoint(x: -shadowBlur - size.width / 2.0, y: 0.0), size: CGSize(width: size.width + shadowBlur * 2.0, height: backroundDiameter / 2.0 + shadowBlur))) + transition.updateFrame(node: self.backgroundTopLeftNode, frame: CGRect(origin: CGPoint(x: -shadowBlur, y: 0.0), size: CGSize(width: size.width + shadowBlur * 2.0, height: backgroundDiameter / 2.0 + shadowBlur))) + transition.updateFrame(node: self.backgroundTopRightNode, frame: CGRect(origin: CGPoint(x: -shadowBlur - size.width / 2.0, y: 0.0), size: CGSize(width: size.width + shadowBlur * 2.0, height: backgroundDiameter / 2.0 + shadowBlur))) transition.updateFrame(node: self.stickersNode, frame: CGRect(origin: CGPoint(x: leftInset, y: 0.0), size: CGSize(width: size.width - leftInset * 2.0, height: size.height))) self.stickersNode.update(size: CGSize(width: size.width - leftInset * 2.0, height: size.height), transition: transition) diff --git a/submodules/TelegramUI/Sources/InstantVideoRadialStatusNode.swift b/submodules/TelegramUI/Sources/InstantVideoRadialStatusNode.swift index fdf26c0233..ff02ac1547 100644 --- a/submodules/TelegramUI/Sources/InstantVideoRadialStatusNode.swift +++ b/submodules/TelegramUI/Sources/InstantVideoRadialStatusNode.swift @@ -93,7 +93,7 @@ final class InstantVideoRadialStatusNode: ASDisplayNode { let lineWidth: CGFloat = 4.0 - let pathDiameter = bounds.size.width - lineWidth + let pathDiameter = bounds.size.width - lineWidth - 8.0 let path = UIBezierPath(arcCenter: CGPoint(x: bounds.size.width / 2.0, y: bounds.size.height / 2.0), radius: pathDiameter / 2.0, startAngle: startAngle, endAngle: endAngle, clockwise:true) path.lineWidth = lineWidth diff --git a/submodules/TelegramUI/Sources/LegacyCache.swift b/submodules/TelegramUI/Sources/LegacyCache.swift deleted file mode 100644 index 947b6e4b55..0000000000 --- a/submodules/TelegramUI/Sources/LegacyCache.swift +++ /dev/null @@ -1,14 +0,0 @@ -import Foundation -import LegacyComponents - -public final class LegacyCache { - private let impl: TGCache - - public init(path: String) { - self.impl = TGCache(cachesPath: path) - } - - public func path(forCachedData id: String) -> String? { - return self.impl.path(forCachedData: id) - } -} diff --git a/submodules/TelegramUI/Sources/LegacyCamera.swift b/submodules/TelegramUI/Sources/LegacyCamera.swift index c253078988..6a92bb8726 100644 --- a/submodules/TelegramUI/Sources/LegacyCamera.swift +++ b/submodules/TelegramUI/Sources/LegacyCamera.swift @@ -123,7 +123,10 @@ func presentedLegacyCamera(context: AccountContext, peer: Peer, chatLocation: Ch controller.finishedWithResults = { [weak menuController, weak legacyController] overlayController, selectionContext, editingContext, currentItem, silentPosting, scheduleTime in if let selectionContext = selectionContext, let editingContext = editingContext { - let signals = TGCameraController.resultSignals(for: selectionContext, editingContext: editingContext, currentItem: currentItem, storeAssets: saveCapturedPhotos && !isSecretChat, saveEditedPhotos: saveCapturedPhotos && !isSecretChat, descriptionGenerator: legacyAssetPickerItemGenerator()) + let nativeGenerator = legacyAssetPickerItemGenerator() + let signals = TGCameraController.resultSignals(for: selectionContext, editingContext: editingContext, currentItem: currentItem, storeAssets: saveCapturedPhotos && !isSecretChat, saveEditedPhotos: saveCapturedPhotos && !isSecretChat, descriptionGenerator: { _1, _2, _3, _4 in + nativeGenerator(_1, _2, _3, _4, nil) + }) sendMessagesWithSignals(signals, silentPosting, scheduleTime) } @@ -139,7 +142,7 @@ func presentedLegacyCamera(context: AccountContext, peer: Peer, chatLocation: Ch if let timer = timer { description["timer"] = timer } - if let item = legacyAssetPickerItemGenerator()(description, caption, entities, nil) { + if let item = legacyAssetPickerItemGenerator()(description, caption, entities, nil, nil) { sendMessagesWithSignals([SSignal.single(item)], false, 0) } } @@ -164,7 +167,7 @@ func presentedLegacyCamera(context: AccountContext, peer: Peer, chatLocation: Ch if let timer = timer { description["timer"] = timer } - if let item = legacyAssetPickerItemGenerator()(description, caption, entities, nil) { + if let item = legacyAssetPickerItemGenerator()(description, caption, entities, nil, nil) { sendMessagesWithSignals([SSignal.single(item)], false, 0) } } @@ -217,16 +220,19 @@ func presentedLegacyShortcutCamera(context: AccountContext, saveCapturedMedia: B controller.finishedWithResults = { [weak controller, weak parentController, weak legacyController] overlayController, selectionContext, editingContext, currentItem, _, _ in if let selectionContext = selectionContext, let editingContext = editingContext { - let signals = TGCameraController.resultSignals(for: selectionContext, editingContext: editingContext, currentItem: currentItem, storeAssets: saveCapturedMedia, saveEditedPhotos: saveEditedPhotos, descriptionGenerator: legacyAssetPickerItemGenerator()) + let nativeGenerator = legacyAssetPickerItemGenerator() + let signals = TGCameraController.resultSignals(for: selectionContext, editingContext: editingContext, currentItem: currentItem, storeAssets: saveCapturedMedia, saveEditedPhotos: saveEditedPhotos, descriptionGenerator: { _1, _2, _3, _4 in + nativeGenerator(_1, _2, _3, _4, nil) + }) if let parentController = parentController { parentController.present(ShareController(context: context, subject: .fromExternal({ peerIds, text, account in return legacyAssetPickerEnqueueMessages(account: account, signals: signals!) - |> `catch` { _ -> Signal<[EnqueueMessage], NoError> in + |> `catch` { _ -> Signal<[LegacyAssetPickerEnqueueMessage], NoError> in return .single([]) } |> mapToSignal { messages -> Signal in let resultSignals = peerIds.map({ peerId in - return enqueueMessages(account: account, peerId: peerId, messages: messages) + return enqueueMessages(account: account, peerId: peerId, messages: messages.map { $0.message }) |> mapToSignal { _ -> Signal in return .complete() } diff --git a/submodules/TelegramUI/Sources/LegacyInstantVideoController.swift b/submodules/TelegramUI/Sources/LegacyInstantVideoController.swift index 85d02cb3e2..ce69f1bad7 100644 --- a/submodules/TelegramUI/Sources/LegacyInstantVideoController.swift +++ b/submodules/TelegramUI/Sources/LegacyInstantVideoController.swift @@ -35,8 +35,9 @@ final class InstantVideoController: LegacyController, StandalonePresentableContr private let micLevelValue = ValuePromise(0.0) private let durationValue = ValuePromise(0.0) let audioStatus: InstantVideoControllerRecordingStatus - - private var dismissedVideo = false + + private var completed = false + private var dismissed = false override init(presentation: LegacyControllerPresentation, theme: PresentationTheme?, strings: PresentationStrings? = nil, initialLayout: ContainerViewLayout? = nil) { self.audioStatus = InstantVideoControllerRecordingStatus(micLevel: self.micLevelValue.get(), duration: self.durationValue.get()) @@ -53,6 +54,9 @@ final class InstantVideoController: LegacyController, StandalonePresentableContr func bindCaptureController(_ captureController: TGVideoMessageCaptureController?) { self.captureController = captureController if let captureController = captureController { + captureController.view.disablesInteractiveKeyboardGestureRecognizer = true + captureController.view.disablesInteractiveTransitionGestureRecognizer = true + captureController.micLevel = { [weak self] (level: CGFloat) -> Void in self?.micLevelValue.set(Float(level)) } @@ -61,8 +65,8 @@ final class InstantVideoController: LegacyController, StandalonePresentableContr } captureController.onDismiss = { [weak self] _, isCancelled in guard let strongSelf = self else { return } - if !strongSelf.dismissedVideo { - self?.dismissedVideo = true + if !strongSelf.dismissed { + self?.dismissed = true self?.onDismiss?(isCancelled) } } @@ -73,18 +77,33 @@ final class InstantVideoController: LegacyController, StandalonePresentableContr } func dismissVideo() { - if let captureController = self.captureController, !self.dismissedVideo { - self.dismissedVideo = true - captureController.dismiss() + if let captureController = self.captureController, !self.dismissed { + self.dismissed = true + captureController.dismiss(true) } } + + func extractVideoSnapshot() -> UIView? { + self.captureController?.extractVideoContent() + } + + func hideVideoSnapshot() { + self.captureController?.hideVideoContent() + } func completeVideo() { - if let captureController = self.captureController, !self.dismissedVideo { - self.dismissedVideo = true + if let captureController = self.captureController, !self.completed { + self.completed = true captureController.complete() } } + + func dismissAnimated() { + if let captureController = self.captureController, !self.dismissed { + self.dismissed = true + captureController.dismiss(false) + } + } func stopVideo() -> Bool { if let captureController = self.captureController { @@ -108,10 +127,10 @@ final class InstantVideoController: LegacyController, StandalonePresentableContr func legacyInputMicPalette(from theme: PresentationTheme) -> TGModernConversationInputMicPallete { let inputPanelTheme = theme.chat.inputPanel - 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) + return TGModernConversationInputMicPallete(dark: theme.overallDarkAppearance, buttonColor: inputPanelTheme.actionControlFillColor, iconColor: inputPanelTheme.actionControlForegroundColor, backgroundColor: theme.rootController.navigationBar.opaqueBackgroundColor, 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?, hasSchedule: Bool, send: @escaping (EnqueueMessage) -> Void, displaySlowmodeTooltip: @escaping (ASDisplayNode, CGRect) -> Void, presentSchedulePicker: @escaping (@escaping (Int32) -> Void) -> Void) -> InstantVideoController { +func legacyInstantVideoController(theme: PresentationTheme, panelFrame: CGRect, context: AccountContext, peerId: PeerId, slowmodeState: ChatSlowmodeState?, hasSchedule: Bool, send: @escaping (InstantVideoController, 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) @@ -125,7 +144,7 @@ func legacyInstantVideoController(theme: PresentationTheme, panelFrame: CGRect, legacyController.view.disablesInteractiveTransitionGestureRecognizer = true var uploadInterface: LegacyLiveUploadInterface? if peerId.namespace != Namespaces.Peer.SecretChat { - uploadInterface = LegacyLiveUploadInterface(account: context.account) + uploadInterface = LegacyLiveUploadInterface(context: context) } var slowmodeValidUntil: Int32 = 0 @@ -147,8 +166,13 @@ func legacyInstantVideoController(theme: PresentationTheme, panelFrame: CGRect, done?(time) } } - controller.finishedWithVideo = { videoUrl, previewImage, _, duration, dimensions, liveUploadData, adjustments, isSilent, scheduleTimestamp in + controller.finishedWithVideo = { [weak legacyController] videoUrl, previewImage, _, duration, dimensions, liveUploadData, adjustments, isSilent, scheduleTimestamp in + guard let legacyController = legacyController else { + return + } + guard let videoUrl = videoUrl else { + send(legacyController, nil) return } @@ -157,12 +181,12 @@ func legacyInstantVideoController(theme: PresentationTheme, panelFrame: CGRect, var previewRepresentations: [TelegramMediaImageRepresentation] = [] if let previewImage = previewImage { - let resource = LocalFileMediaResource(fileId: arc4random64()) + let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) let thumbnailSize = finalDimensions.aspectFitted(CGSize(width: 320.0, height: 320.0)) let thumbnailImage = TGScaleImageToPixelSize(previewImage, thumbnailSize)! if let thumbnailData = thumbnailImage.jpegData(compressionQuality: 0.4) { context.account.postbox.mediaBox.storeResourceData(resource.id, data: thumbnailData) - previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(thumbnailSize), resource: resource, progressiveSizes: [])) + previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(thumbnailSize), resource: resource, progressiveSizes: [], immediateThumbnailData: nil)) } } @@ -188,7 +212,7 @@ func legacyInstantVideoController(theme: PresentationTheme, panelFrame: CGRect, resource = LocalFileMediaResource(fileId: liveUploadData.id) context.account.postbox.mediaBox.storeResourceData(resource.id, data: data, synchronous: true) } else { - resource = LocalFileVideoMediaResource(randomId: arc4random64(), path: videoUrl.path, adjustments: resourceAdjustments) + resource = LocalFileVideoMediaResource(randomId: Int64.random(in: Int64.min ... Int64.max), path: videoUrl.path, adjustments: resourceAdjustments) } if let previewImage = previewImage { @@ -197,8 +221,8 @@ func legacyInstantVideoController(theme: PresentationTheme, panelFrame: CGRect, } } - let media = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: arc4random64()), partialReference: nil, resource: resource, previewRepresentations: previewRepresentations, videoThumbnails: [], immediateThumbnailData: nil, mimeType: "video/mp4", size: nil, attributes: [.FileName(fileName: "video.mp4"), .Video(duration: Int(finalDuration), size: PixelDimensions(finalDimensions), flags: [.instantRoundVideo])]) - var message: EnqueueMessage = .message(text: "", attributes: [], mediaReference: .standalone(media: media), replyToMessageId: nil, localGroupingKey: nil) + let media = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: Int64.random(in: Int64.min ... Int64.max)), partialReference: nil, resource: resource, previewRepresentations: previewRepresentations, videoThumbnails: [], immediateThumbnailData: nil, mimeType: "video/mp4", size: nil, attributes: [.FileName(fileName: "video.mp4"), .Video(duration: Int(finalDuration), size: PixelDimensions(finalDimensions), flags: [.instantRoundVideo])]) + var message: EnqueueMessage = .message(text: "", attributes: [], mediaReference: .standalone(media: media), replyToMessageId: nil, localGroupingKey: nil, correlationId: nil) let scheduleTime: Int32? = scheduleTimestamp > 0 ? scheduleTimestamp : nil @@ -220,7 +244,7 @@ func legacyInstantVideoController(theme: PresentationTheme, panelFrame: CGRect, return attributes } - send(message) + send(legacyController, message) } controller.didDismiss = { [weak legacyController] in if let legacyController = legacyController { diff --git a/submodules/TelegramUI/Sources/ManagedAudioRecorder.swift b/submodules/TelegramUI/Sources/ManagedAudioRecorder.swift index 58c481129a..2bbd55a7a9 100644 --- a/submodules/TelegramUI/Sources/ManagedAudioRecorder.swift +++ b/submodules/TelegramUI/Sources/ManagedAudioRecorder.swift @@ -136,7 +136,7 @@ private func rendererInputProc(refCon: UnsafeMutableRawPointer, ioActionFlags: U } private let beginToneData: TonePlayerData? = { - guard let url = Bundle.main.url(forResource: "begin_record", withExtension: "caf") else { + guard let url = Bundle.main.url(forResource: "begin_record", withExtension: "mp3") else { return nil } return loadTonePlayerData(path: url.path) diff --git a/submodules/TelegramUI/Sources/ManagedDiceAnimationNode.swift b/submodules/TelegramUI/Sources/ManagedDiceAnimationNode.swift index 40b9492a53..6fb6235cb3 100644 --- a/submodules/TelegramUI/Sources/ManagedDiceAnimationNode.swift +++ b/submodules/TelegramUI/Sources/ManagedDiceAnimationNode.swift @@ -137,7 +137,7 @@ final class ManagedDiceAnimationNode: ManagedAnimationNode, GenericAnimatedStick let appConfiguration: AppConfiguration = preferencesView.values[PreferencesKeys.appConfiguration] as? AppConfiguration ?? .defaultValue return InteractiveEmojiConfiguration.with(appConfiguration: appConfiguration) }) - self.emojis.set(loadedStickerPack(postbox: context.account.postbox, network: context.account.network, reference: .dice(emoji), forceActualized: false) + self.emojis.set(context.engine.stickers.loadedStickerPack(reference: .dice(emoji), forceActualized: false) |> mapToSignal { stickerPack -> Signal<[TelegramMediaFile], NoError> in switch stickerPack { case let .result(_, items, _): @@ -206,4 +206,11 @@ final class ManagedDiceAnimationNode: ManagedAnimationNode, GenericAnimatedStick func setOverlayColor(_ color: UIColor?, animated: Bool) { } + + func setFrameIndex(_ frameIndex: Int) { + } + + var currentFrameIndex: Int { + return 0 + } } diff --git a/submodules/TelegramUI/Sources/MediaManager.swift b/submodules/TelegramUI/Sources/MediaManager.swift index 717f765e27..3a1d3e1b1f 100644 --- a/submodules/TelegramUI/Sources/MediaManager.swift +++ b/submodules/TelegramUI/Sources/MediaManager.swift @@ -417,7 +417,7 @@ public final class MediaManagerImpl: NSObject, MediaManager { if let (account, stateOrLoading, type) = accountStateAndType { switch type { case .music: - minimumStoreDuration = 15.0 * 60.0 + minimumStoreDuration = 10.0 * 60.0 case .voice: minimumStoreDuration = 5.0 * 60.0 case .file: diff --git a/submodules/TelegramUI/Sources/MentionChatInputContextPanelNode.swift b/submodules/TelegramUI/Sources/MentionChatInputContextPanelNode.swift index 09d87b239e..1e908b3475 100644 --- a/submodules/TelegramUI/Sources/MentionChatInputContextPanelNode.swift +++ b/submodules/TelegramUI/Sources/MentionChatInputContextPanelNode.swift @@ -165,7 +165,7 @@ final class MentionChatInputContextPanelNode: ChatInputContextPanelNode { } }, removeRequested: { [weak self] peerId in if let strongSelf = self { - let _ = removeRecentlyUsedInlineBot(account: strongSelf.context.account, peerId: peerId).start() + let _ = strongSelf.context.engine.peers.removeRecentlyUsedInlineBot(peerId: peerId).start() strongSelf.revealedPeerId = nil strongSelf.currentResults = strongSelf.currentResults.filter { $0.id != peerId } @@ -217,7 +217,10 @@ final class MentionChatInputContextPanelNode: ChatInputContextPanelNode { if let topItemOffset = topItemOffset { let position = strongSelf.listView.layer.position - strongSelf.listView.layer.animatePosition(from: CGPoint(x: position.x, y: position.y + (strongSelf.listView.bounds.size.height - topItemOffset)), to: position, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring) + strongSelf.listView.position = CGPoint(x: position.x, y: position.y + (strongSelf.listView.bounds.size.height - topItemOffset)) + ContainedViewLayoutTransition.animated(duration: 0.3, curve: .spring).animateView { + strongSelf.listView.position = position + } } } }) @@ -284,4 +287,14 @@ final class MentionChatInputContextPanelNode: ChatInputContextPanelNode { let listViewFrame = self.listView.frame return self.listView.hitTest(CGPoint(x: point.x - listViewFrame.minX, y: point.y - listViewFrame.minY), with: event) } + + override var topItemFrame: CGRect? { + var topItemFrame: CGRect? + self.listView.forEachItemNode { itemNode in + if topItemFrame == nil { + topItemFrame = itemNode.frame + } + } + return topItemFrame + } } diff --git a/submodules/TelegramUI/Sources/NavigateToChatController.swift b/submodules/TelegramUI/Sources/NavigateToChatController.swift index 4f6e15974f..f464fc7811 100644 --- a/submodules/TelegramUI/Sources/NavigateToChatController.swift +++ b/submodules/TelegramUI/Sources/NavigateToChatController.swift @@ -20,10 +20,10 @@ public func navigateToChatControllerImpl(_ params: NavigateToChatControllerParam if let updateTextInputState = params.updateTextInputState { controller.updateTextInputState(updateTextInputState) } - if let subject = params.subject, case let .message(messageId, _) = subject { + if let subject = params.subject, case let .message(messageId, _, timecode) = subject { let navigationController = params.navigationController let animated = params.animated - controller.navigateToMessage(messageLocation: .id(messageId), animated: isFirst, completion: { [weak navigationController, weak controller] in + controller.navigateToMessage(messageLocation: .id(messageId, timecode), animated: isFirst, completion: { [weak navigationController, weak controller] in if let navigationController = navigationController, let controller = controller { let _ = navigationController.popToViewController(controller, animated: animated) } @@ -71,7 +71,7 @@ public func navigateToChatControllerImpl(_ params: NavigateToChatControllerParam }) } } else { - controller = ChatControllerImpl(context: params.context, chatLocation: params.chatLocation, chatLocationContextHolder: params.chatLocationContextHolder, subject: params.subject, botStart: params.botStart, peekData: params.peekData, peerNearbyData: params.peerNearbyData, greetingData: params.greetingData) + controller = ChatControllerImpl(context: params.context, chatLocation: params.chatLocation, chatLocationContextHolder: params.chatLocationContextHolder, subject: params.subject, botStart: params.botStart, peekData: params.peekData, peerNearbyData: params.peerNearbyData) } controller.purposefulAction = params.purposefulAction if let search = params.activateMessageSearch { diff --git a/submodules/TelegramUI/Sources/NotificationContentContext.swift b/submodules/TelegramUI/Sources/NotificationContentContext.swift index d09f5968b0..e2e5e653f5 100644 --- a/submodules/TelegramUI/Sources/NotificationContentContext.swift +++ b/submodules/TelegramUI/Sources/NotificationContentContext.swift @@ -32,27 +32,8 @@ private func setupSharedLogger(rootPath: String, path: String) { } } -private func parseFileLocationResource(_ dict: [AnyHashable: Any]) -> TelegramMediaResource? { - guard let datacenterId = dict["datacenterId"] as? Int32 else { - return nil - } - guard let volumeId = dict["volumeId"] as? Int64 else { - return nil - } - guard let localId = dict["localId"] as? Int32 else { - return nil - } - guard let secret = dict["secret"] as? Int64 else { - return nil - } - var fileReference: Data? - if let fileReferenceString = dict["fileReference"] as? String { - fileReference = dataWithHexString(fileReferenceString) - } - return CloudFileMediaResource(datacenterId: Int(datacenterId), volumeId: volumeId, localId: localId, secret: secret, size: nil, fileReference: fileReference) -} - public struct NotificationViewControllerInitializationData { + public let appBundleId: String public let appGroupPath: String public let apiId: Int32 public let apiHash: String @@ -61,7 +42,8 @@ public struct NotificationViewControllerInitializationData { public let appVersion: String public let bundleData: Data? - public init(appGroupPath: String, apiId: Int32, apiHash: String, languagesCategory: String, encryptionParameters: (Data, Data), appVersion: String, bundleData: Data?) { + public init(appBundleId: String, appGroupPath: String, apiId: Int32, apiHash: String, languagesCategory: String, encryptionParameters: (Data, Data), appVersion: String, bundleData: Data?) { + self.appBundleId = appBundleId self.appGroupPath = appGroupPath self.apiId = apiId self.apiHash = apiHash @@ -102,7 +84,7 @@ public final class NotificationViewControllerImpl { let rootPath = rootPathForBasePath(self.initializationData.appGroupPath) performAppGroupUpgrades(appGroupPath: self.initializationData.appGroupPath, rootPath: rootPath) - TempBox.initializeShared(basePath: rootPath, processType: "notification-content", launchSpecificId: arc4random64()) + TempBox.initializeShared(basePath: rootPath, processType: "notification-content", launchSpecificId: Int64.random(in: Int64.min ... Int64.max)) let logsPath = rootPath + "/notificationcontent-logs" let _ = try? FileManager.default.createDirectory(atPath: logsPath, withIntermediateDirectories: true, attributes: nil) @@ -123,7 +105,7 @@ public final class NotificationViewControllerImpl { }) semaphore.wait() - let applicationBindings = TelegramApplicationBindings(isMainApp: false, containerPath: self.initializationData.appGroupPath, appSpecificScheme: "tgapp", openUrl: { _ in + let applicationBindings = TelegramApplicationBindings(isMainApp: false, appBundleId: self.initializationData.appBundleId, containerPath: self.initializationData.appGroupPath, appSpecificScheme: "tgapp", openUrl: { _ in }, openUniversalUrl: { _, completion in completion.completion(false) return @@ -146,6 +128,7 @@ public final class NotificationViewControllerImpl { return nil }, requestSetAlternateIconName: { _, f in f(false) + }, forceOrientation: { _ in }) let presentationDataPromise = Promise() @@ -154,7 +137,7 @@ public final class NotificationViewControllerImpl { 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, apiHash: self.initializationData.apiHash, languagesCategory: self.initializationData.languagesCategory, appVersion: self.initializationData.appVersion, voipMaxLayer: 0, voipVersions: [], 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, sharedContainerPath: self.initializationData.appGroupPath, 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, voipVersions: [], appData: .single(self.initializationData.bundleData), autolockDeadine: .single(nil), encryptionProvider: OpenSSLEncryptionProvider()), rootPath: rootPath, legacyBasePath: nil, apsNotificationToken: .never(), voipNotificationToken: .never(), setNotificationCall: { _ in }, navigateToChat: { _, _, _ in }) presentationDataPromise.set(sharedAccountContext!.presentationData) } diff --git a/submodules/TelegramUI/Sources/OpenChatMessage.swift b/submodules/TelegramUI/Sources/OpenChatMessage.swift index 80e2087933..7d9a6c640d 100644 --- a/submodules/TelegramUI/Sources/OpenChatMessage.swift +++ b/submodules/TelegramUI/Sources/OpenChatMessage.swift @@ -67,7 +67,7 @@ func openChatMessageImpl(_ params: OpenChatMessageParams) -> Bool { params.dismissInput() let controllerParams = LocationViewParams(sendLiveLocation: { location in - let outMessage: EnqueueMessage = .message(text: "", attributes: [], mediaReference: .standalone(media: location), replyToMessageId: nil, localGroupingKey: nil) + let outMessage: EnqueueMessage = .message(text: "", attributes: [], mediaReference: .standalone(media: location), replyToMessageId: nil, localGroupingKey: nil, correlationId: nil) params.enqueueMessage(outMessage) }, stopLiveLocation: { messageId in params.context.liveLocationManager?.cancelLiveLocation(peerId: messageId?.peerId ?? params.message.id.peerId) @@ -92,13 +92,13 @@ func openChatMessageImpl(_ params: OpenChatMessageParams) -> Bool { } 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 + 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, context: params.context), 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 + 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, context: params.context), 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() + let _ = params.context.engine.stickers.addStickerPackInteractively(info: info, items: items, positionInList: positionInList).start() } return true })) @@ -166,6 +166,9 @@ func openChatMessageImpl(_ params: OpenChatMessageParams) -> Bool { params.dismissInput() let _ = (gallery |> deliverOnMainQueue).start(next: { gallery in + gallery.centralItemUpdated = { messageId in + params.centralItemUpdated?(messageId) + } params.present(gallery, GalleryControllerPresentationArguments(transitionArguments: { messageId, media in let selectedTransitionNode = params.transitionNode(messageId, media) if let selectedTransitionNode = selectedTransitionNode { @@ -250,17 +253,17 @@ func openChatInstantPage(context: AccountContext, message: Message, sourcePeerTy func openChatWallpaper(context: AccountContext, message: Message, present: @escaping (ViewController, Any?) -> Void) { for media in message.media { if let webpage = media as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content { - let _ = (context.sharedContext.resolveUrl(account: context.account, url: content.url, skipUrlAuth: true) + let _ = (context.sharedContext.resolveUrl(context: context, peerId: nil, url: content.url, skipUrlAuth: true) |> deliverOnMainQueue).start(next: { resolvedUrl in if case let .wallpaper(parameter) = resolvedUrl { let source: WallpaperListSource switch parameter { - case let .slug(slug, options, firstColor, secondColor, intensity, rotation): - source = .slug(slug, content.file, options, firstColor, secondColor, intensity, rotation, message) + case let .slug(slug, options, colors, intensity, rotation): + source = .slug(slug, content.file, options, colors, intensity, rotation, message) case let .color(color): - 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) + source = .wallpaper(.color(color.argb), nil, [], nil, nil, message) + case let .gradient(colors, rotation): + source = .wallpaper(.gradient(nil, colors, WallpaperSettings(rotation: rotation)), nil, [], nil, rotation, message) } let controller = WallpaperGalleryController(context: context, source: source) @@ -274,7 +277,7 @@ func openChatWallpaper(context: AccountContext, message: Message, present: @esca func openChatTheme(context: AccountContext, message: Message, pushController: @escaping (ViewController) -> Void, present: @escaping (ViewController, Any?) -> Void) { for media in message.media { if let webpage = media as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content { - let _ = (context.sharedContext.resolveUrl(account: context.account, url: content.url, skipUrlAuth: true) + let _ = (context.sharedContext.resolveUrl(context: context, peerId: nil, url: content.url, skipUrlAuth: true) |> deliverOnMainQueue).start(next: { resolvedUrl in var file: TelegramMediaFile? var settings: TelegramThemeSettings? diff --git a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift index ab9554b03b..b368cf9faf 100644 --- a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift +++ b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift @@ -23,6 +23,7 @@ import ShareController import ChatInterfaceState import TelegramCallsUI import UndoUI +import ImportStickerPackUI private func defaultNavigationForPeerId(_ peerId: PeerId?, navigation: ChatControllerInteractionNavigateToPeer) -> ChatControllerInteractionNavigateToPeer { if case .default = navigation { @@ -66,18 +67,18 @@ func openResolvedUrlImpl(_ resolvedUrl: ResolvedUrl, context: AccountContext, ur if payload.isEmpty { if peerId.namespace == Namespaces.Peer.CloudGroup { - let _ = (addGroupMember(account: context.account, peerId: peerId, memberId: botPeerId) + let _ = (context.engine.peers.addGroupMember(peerId: peerId, memberId: botPeerId) |> deliverOnMainQueue).start(completed: { controller?.dismiss() }) } else { - let _ = (addChannelMember(account: context.account, peerId: peerId, memberId: botPeerId) + let _ = (context.engine.peers.addChannelMember(peerId: peerId, memberId: botPeerId) |> deliverOnMainQueue).start(completed: { controller?.dismiss() }) } } else { - let _ = (requestStartBotInGroup(account: context.account, botPeerId: botPeerId, groupPeerId: peerId, payload: payload) + let _ = (context.engine.messages.requestStartBotInGroup(botPeerId: botPeerId, groupPeerId: peerId, payload: payload) |> deliverOnMainQueue).start(next: { result in if let navigationController = navigationController { context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peerId))) @@ -96,8 +97,8 @@ func openResolvedUrlImpl(_ resolvedUrl: ResolvedUrl, context: AccountContext, ur } dismissInput() navigationController?.pushViewController(controller) - case let .channelMessage(peerId, messageId): - openPeer(peerId, .chat(textInputState: nil, subject: .message(id: messageId, highlight: true), peekData: nil)) + case let .channelMessage(peerId, messageId, timecode): + openPeer(peerId, .chat(textInputState: nil, subject: .message(id: messageId, highlight: true, timecode: timecode), peekData: nil)) case let .replyThreadMessage(replyThreadMessage, messageId): if let navigationController = navigationController { let _ = ChatControllerImpl.openMessageReplies(context: context, navigationController: navigationController, present: { c, a in @@ -184,7 +185,7 @@ func openResolvedUrlImpl(_ resolvedUrl: ResolvedUrl, context: AccountContext, ur let presentationData = context.sharedContext.currentPresentationData.with { $0 } let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: nil)) present(controller, nil) - let _ = (requestCancelAccountResetData(network: context.account.network, hash: hash) + let _ = (context.engine.auth.requestCancelAccountResetData(hash: hash) |> deliverOnMainQueue).start(next: { [weak controller] data in controller?.dismiss() present(confirmPhoneNumberCodeController(context: context, phoneNumber: phone, codeData: data), ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) @@ -241,7 +242,7 @@ func openResolvedUrlImpl(_ resolvedUrl: ResolvedUrl, context: AccountContext, ur if let to = to { if to.hasPrefix("@") { - let _ = (resolvePeerByName(account: context.account, name: String(to[to.index(to.startIndex, offsetBy: 1)...])) + let _ = (context.engine.peers.resolvePeerByName(name: String(to[to.index(to.startIndex, offsetBy: 1)...])) |> deliverOnMainQueue).start(next: { peerId in if let peerId = peerId { let _ = (context.account.postbox.loadedPeerWithId(peerId) @@ -293,30 +294,28 @@ func openResolvedUrlImpl(_ resolvedUrl: ResolvedUrl, context: AccountContext, ur let signal: Signal var options: WallpaperPresentationOptions? - var topColor: UIColor? - var bottomColor: UIColor? + var colors: [UInt32] = [] var intensity: Int32? var rotation: Int32? switch parameter { - case let .slug(slug, wallpaperOptions, firstColor, secondColor, intensityValue, rotationValue): + case let .slug(slug, wallpaperOptions, colorsValue, intensityValue, rotationValue): signal = getWallpaper(network: context.account.network, slug: slug) options = wallpaperOptions - topColor = firstColor - bottomColor = secondColor + colors = colorsValue intensity = intensityValue rotation = rotationValue controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: nil)) present(controller!, nil) case let .color(color): signal = .single(.color(color.argb)) - case let .gradient(topColor, bottomColor, rotation): - signal = .single(.gradient(topColor.argb, bottomColor.argb, WallpaperSettings(rotation: rotation))) + case let .gradient(colors, rotation): + signal = .single(.gradient(nil, colors, WallpaperSettings(rotation: rotation))) } let _ = (signal |> deliverOnMainQueue).start(next: { [weak controller] wallpaper in controller?.dismiss() - let galleryController = WallpaperGalleryController(context: context, source: .wallpaper(wallpaper, options, topColor, bottomColor, intensity, rotation, nil)) + let galleryController = WallpaperGalleryController(context: context, source: .wallpaper(wallpaper, options, colors, intensity, rotation, nil)) present(galleryController, nil) }, error: { [weak controller] error in controller?.dismiss() @@ -399,7 +398,7 @@ func openResolvedUrlImpl(_ resolvedUrl: ResolvedUrl, context: AccountContext, ur 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) { + if let theme = makePresentationTheme(mediaBox: context.sharedContext.accountManager.mediaBox, themeReference: .builtin(PresentationBuiltinThemeReference(baseTheme: settings.baseTheme)), accentColor: UIColor(argb: settings.accentColor), backgroundColors: [], 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) } @@ -439,8 +438,8 @@ func openResolvedUrlImpl(_ resolvedUrl: ResolvedUrl, context: AccountContext, ur 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 activeSessionsContext = context.engine.privacy.activeSessions() + let webSessionsContext = context.engine.privacy.webSessions() let otherSessionCount = activeSessionsContext.state |> map { state -> Int in return state.sessions.filter({ !$0.isCurrent }).count @@ -480,5 +479,16 @@ func openResolvedUrlImpl(_ resolvedUrl: ResolvedUrl, context: AccountContext, ur }), on: .root, blockInteraction: false, completion: {}) })) } + case .importStickers: + dismissInput() + if let navigationController = navigationController, let data = UIPasteboard.general.data(forPasteboardType: "org.telegram.third-party.stickerset"), let stickerPack = ImportStickerPack(data: data), !stickerPack.stickers.isEmpty { + for controller in navigationController.overlayControllers { + if controller is ImportStickerPackController { + controller.dismiss() + } + } + let controller = ImportStickerPackController(context: context, stickerPack: stickerPack, parentNavigationController: navigationController) + present(controller, nil) + } } } diff --git a/submodules/TelegramUI/Sources/OpenUrl.swift b/submodules/TelegramUI/Sources/OpenUrl.swift index dca0401241..106fd25f93 100644 --- a/submodules/TelegramUI/Sources/OpenUrl.swift +++ b/submodules/TelegramUI/Sources/OpenUrl.swift @@ -96,7 +96,7 @@ public func parseSecureIdUrl(_ url: URL) -> ParsedSecureIdUrl? { return nil } - return ParsedSecureIdUrl(peerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: botId), scope: scope, publicKey: publicKey, callbackUrl: callbackUrl, opaquePayload: opaquePayload, opaqueNonce: opaqueNonce) + return ParsedSecureIdUrl(peerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt32Value(botId)), scope: scope, publicKey: publicKey, callbackUrl: callbackUrl, opaquePayload: opaquePayload, opaqueNonce: opaqueNonce) } } } @@ -229,7 +229,7 @@ func openExternalUrlImpl(context: AccountContext, urlContext: OpenURLContext, ur } let handleInternalUrl: (String) -> Void = { url in - let _ = (context.sharedContext.resolveUrl(account: context.account, url: url, skipUrlAuth: true) + let _ = (context.sharedContext.resolveUrl(context: context, peerId: nil, url: url, skipUrlAuth: true) |> deliverOnMainQueue).start(next: handleResolvedUrl) } @@ -439,7 +439,7 @@ func openExternalUrlImpl(context: AccountContext, urlContext: OpenURLContext, ur } if valid { - if let botId = botId, let scope = scope, let publicKey = publicKey, let callbackUrl = callbackUrl { + if let botId = botId, let scope = scope, let publicKey = publicKey { if scope.hasPrefix("{") && scope.hasSuffix("}") { opaquePayload = Data() if opaqueNonce.isEmpty { @@ -451,7 +451,7 @@ func openExternalUrlImpl(context: AccountContext, urlContext: OpenURLContext, ur 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)) + let controller = SecureIdAuthController(context: context, mode: .form(peerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt32Value(botId)), scope: scope, publicKey: publicKey, callbackUrl: callbackUrl, opaquePayload: opaquePayload, opaqueNonce: opaqueNonce)) if let navigationController = navigationController { context.sharedContext.applicationBindings.dismissNativeController() @@ -478,7 +478,7 @@ func openExternalUrlImpl(context: AccountContext, urlContext: OpenURLContext, ur 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 transaction.getPeer(PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt32Value(idValue))) } |> deliverOnMainQueue).start(next: { peer in if let peer = peer, let controller = context.sharedContext.makePeerInfoController(context: context, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false) { @@ -678,7 +678,9 @@ func openExternalUrlImpl(context: AccountContext, urlContext: OpenURLContext, ur } } } else { - if parsedUrl.host == "settings" { + if parsedUrl.host == "importStickers" { + handleResolvedUrl(.importStickers) + } else if parsedUrl.host == "settings" { if let path = parsedUrl.pathComponents.last { var section: ResolvedUrlSettingsSection? switch path { @@ -730,7 +732,7 @@ func openExternalUrlImpl(context: AccountContext, urlContext: OpenURLContext, ur if let window = navigationController?.view.window { let controller = SFSafariViewController(url: parsedUrl) if #available(iOSApplicationExtension 10.0, iOS 10.0, *) { - controller.preferredBarTintColor = presentationData.theme.rootController.navigationBar.backgroundColor + controller.preferredBarTintColor = presentationData.theme.rootController.navigationBar.opaqueBackgroundColor controller.preferredControlTintColor = presentationData.theme.rootController.navigationBar.accentTextColor } window.rootViewController?.present(controller, animated: true) diff --git a/submodules/TelegramUI/Sources/OverlayAudioPlayerController.swift b/submodules/TelegramUI/Sources/OverlayAudioPlayerController.swift index 55908db048..c0050e3893 100644 --- a/submodules/TelegramUI/Sources/OverlayAudioPlayerController.swift +++ b/submodules/TelegramUI/Sources/OverlayAudioPlayerController.swift @@ -8,6 +8,7 @@ import SwiftSignalKit import TelegramUIPreferences import AccountContext import ShareController +import UndoUI final class OverlayAudioPlayerControllerImpl: ViewController, OverlayAudioPlayerController { private let context: AccountContext @@ -68,6 +69,46 @@ final class OverlayAudioPlayerControllerImpl: ViewController, OverlayAudioPlayer strongSelf.dismiss() } }, externalShare: true) + shareController.completed = { [weak self] peerIds in + if let strongSelf = self { + let _ = (strongSelf.context.account.postbox.transaction { transaction -> [Peer] in + var peers: [Peer] = [] + for peerId in peerIds { + if let peer = transaction.getPeer(peerId) { + peers.append(peer) + } + } + return peers + } |> deliverOnMainQueue).start(next: { [weak self] peers in + if let strongSelf = self { + let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } + + let text: String + var savedMessages = false + if peerIds.count == 1, let peerId = peerIds.first, peerId == strongSelf.context.account.peerId { + text = presentationData.strings.Conversation_ForwardTooltip_SavedMessages_One + savedMessages = true + } else { + if peers.count == 1, let peer = peers.first { + let peerName = peer.id == strongSelf.context.account.peerId ? presentationData.strings.DialogList_SavedMessages : peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + text = presentationData.strings.Conversation_ForwardTooltip_Chat_One(peerName).0 + } else if peers.count == 2, let firstPeer = peers.first, let secondPeer = peers.last { + let firstPeerName = firstPeer.id == strongSelf.context.account.peerId ? presentationData.strings.DialogList_SavedMessages : firstPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + let secondPeerName = secondPeer.id == strongSelf.context.account.peerId ? presentationData.strings.DialogList_SavedMessages : secondPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + text = presentationData.strings.Conversation_ForwardTooltip_TwoChats_One(firstPeerName, secondPeerName).0 + } else if let peer = peers.first { + let peerName = peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + text = presentationData.strings.Conversation_ForwardTooltip_ManyChats_One(peerName, "\(peers.count - 1)").0 + } else { + text = "" + } + } + + strongSelf.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: savedMessages, text: text), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), in: .current) + } + }) + } + } strongSelf.controllerNode.view.endEditing(true) strongSelf.present(shareController, in: .window(.root)) } diff --git a/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift b/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift index 193982ef4b..748d69ccf2 100644 --- a/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift +++ b/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift @@ -70,6 +70,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, UIGestu }, openPeer: { _, _, _ in }, openPeerMention: { _ in }, openMessageContextMenu: { _, _, _, _, _ in + }, activateMessagePinch: { _ in }, openMessageContextActions: { _, _, _, _ in }, navigateToMessage: { _, _ in }, navigateToMessageStandalone: { _ in @@ -77,11 +78,11 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, UIGestu }, toggleMessagesSelection: { _, _ in }, sendCurrentMessage: { _ in }, sendMessage: { _ in - }, sendSticker: { _, _, _, _, _ in + }, sendSticker: { _, _, _, _, _, _, _ in return false - }, sendGif: { _, _, _ in + }, sendGif: { _, _, _, _, _ in return false - }, sendBotContextResultAsGif: { _, _, _, _ in + }, sendBotContextResultAsGif: { _, _, _, _, _ in return false }, requestMessageActionCallback: { _, _, _, _ in }, requestMessageActionUrlAuth: { _, _ in @@ -136,8 +137,6 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, UIGestu }, displayPsa: { _, _ in }, displayDiceTooltip: { _ in }, animateDiceSuccess: { _ in - }, greetingStickerNode: { - return nil }, openPeerContextMenu: { _, _, _, _, _ in }, openMessageReplies: { _, _, _ in }, openReplyThreadOriginalMessage: { _ in @@ -145,9 +144,11 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, UIGestu }, editMessageMedia: { _, _ in }, copyText: { _ in }, displayUndo: { _ in + }, isAnimatingMessage: { _ in + return false }, requestMessageUpdate: { _ in }, cancelInteractiveKeyboardGestures: { - }, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings, pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(loopAnimatedStickers: false)) + }, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings, pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(loopAnimatedStickers: false), presentationContext: ChatPresentationContext(backgroundNode: nil)) self.dimNode = ASDisplayNode() self.dimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.5) @@ -186,7 +187,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, UIGestu self.isGlobalSearch = false } - self.historyNode = ChatHistoryListNode(context: context, chatLocation: .peer(peerId), chatLocationContextHolder: chatLocationContextHolder, tagMask: tagMask, source: source, subject: .message(id: initialMessageId, highlight: true), controllerInteraction: self.controllerInteraction, selectedMessages: .single(nil), mode: .list(search: false, reversed: self.currentIsReversed, displayHeaders: .none, hintLinks: false, isGlobalSearch: self.isGlobalSearch)) + self.historyNode = ChatHistoryListNode(context: context, chatLocation: .peer(peerId), chatLocationContextHolder: chatLocationContextHolder, tagMask: tagMask, source: source, subject: .message(id: initialMessageId, highlight: true, timecode: nil), controllerInteraction: self.controllerInteraction, selectedMessages: .single(nil), mode: .list(search: false, reversed: self.currentIsReversed, displayHeaders: .none, hintLinks: false, isGlobalSearch: self.isGlobalSearch)) super.init() @@ -262,7 +263,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, UIGestu self.contentNode.addSubnode(self.historyNode) self.contentNode.addSubnode(self.controlsNode) - self.historyNode.beganInteractiveDragging = { [weak self] in + self.historyNode.beganInteractiveDragging = { [weak self] _ in self?.controlsNode.collapse() } @@ -357,7 +358,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, UIGestu 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 { + if let replacementHistoryNode = self.replacementHistoryNode { let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: listNodeSize, insets: insets, duration: 0.0, curve: .Default(duration: nil)) replacementHistoryNode.updateLayout(transition: transition, updateSizeAndInsets: updateSizeAndInsets) } @@ -527,7 +528,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, UIGestu } let chatLocationContextHolder = Atomic(value: nil) - let historyNode = ChatHistoryListNode(context: self.context, chatLocation: .peer(self.peerId), chatLocationContextHolder: chatLocationContextHolder, tagMask: tagMask, subject: .message(id: messageId, highlight: true), controllerInteraction: self.controllerInteraction, selectedMessages: .single(nil), mode: .list(search: false, reversed: self.currentIsReversed, displayHeaders: .none, hintLinks: false, isGlobalSearch: self.isGlobalSearch)) + let historyNode = ChatHistoryListNode(context: self.context, chatLocation: .peer(self.peerId), chatLocationContextHolder: chatLocationContextHolder, tagMask: tagMask, subject: .message(id: messageId, highlight: true, timecode: nil), controllerInteraction: self.controllerInteraction, selectedMessages: .single(nil), mode: .list(search: false, reversed: self.currentIsReversed, displayHeaders: .none, hintLinks: false, isGlobalSearch: self.isGlobalSearch)) historyNode.preloadPages = true historyNode.stackFromBottom = true historyNode.updateFloatingHeaderOffset = { [weak self] offset, _ in @@ -627,7 +628,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, UIGestu } } - self.historyNode.beganInteractiveDragging = { [weak self] in + self.historyNode.beganInteractiveDragging = { [weak self] _ in self?.controlsNode.collapse() } diff --git a/submodules/TelegramUI/Sources/OverlayInstantVideoDecoration.swift b/submodules/TelegramUI/Sources/OverlayInstantVideoDecoration.swift index fb02894766..1cab57392c 100644 --- a/submodules/TelegramUI/Sources/OverlayInstantVideoDecoration.swift +++ b/submodules/TelegramUI/Sources/OverlayInstantVideoDecoration.swift @@ -37,7 +37,7 @@ final class OverlayInstantVideoDecoration: UniversalVideoDecoration { self.contentContainerNode.clipsToBounds = true self.foregroundContainerNode = ASDisplayNode() - self.progressNode = InstantVideoRadialStatusNode(color: UIColor(white: 1.0, alpha: 0.8)) + self.progressNode = InstantVideoRadialStatusNode(color: UIColor(white: 1.0, alpha: 0.6)) self.foregroundContainerNode.addSubnode(self.progressNode) self.foregroundNode = self.foregroundContainerNode } diff --git a/submodules/TelegramUI/Sources/OverlayPlayerControlsNode.swift b/submodules/TelegramUI/Sources/OverlayPlayerControlsNode.swift index 6caa842a03..96391f630f 100644 --- a/submodules/TelegramUI/Sources/OverlayPlayerControlsNode.swift +++ b/submodules/TelegramUI/Sources/OverlayPlayerControlsNode.swift @@ -12,6 +12,7 @@ import TelegramUIPreferences import AccountContext import PhotoResources import AppBundle +import ManagedAnimationNode private func generateBackground(theme: PresentationTheme) -> UIImage? { return generateImage(CGSize(width: 20.0, height: 10.0 + 8.0), rotatedContext: { size, context in @@ -114,6 +115,7 @@ final class OverlayPlayerControlsNode: ASDisplayNode { private var currentIsPaused: Bool? private let playPauseButton: IconButtonNode + private let playPauseIconNode: PlayPauseIconNode private var currentOrder: MusicPlaybackSettingsOrder? private let orderButton: IconButtonNode @@ -211,6 +213,8 @@ final class OverlayPlayerControlsNode: ASDisplayNode { self.playPauseButton = IconButtonNode() self.playPauseButton.displaysAsynchronously = false + self.playPauseIconNode = PlayPauseIconNode() + 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) @@ -240,6 +244,7 @@ final class OverlayPlayerControlsNode: ASDisplayNode { self.addSubnode(self.backwardButton) self.addSubnode(self.forwardButton) self.addSubnode(self.playPauseButton) + self.playPauseButton.addSubnode(self.playPauseIconNode) self.addSubnode(self.separatorNode) @@ -323,10 +328,12 @@ final class OverlayPlayerControlsNode: ASDisplayNode { if strongSelf.wasPlaying { isPaused = false } + + let isFirstTime = strongSelf.currentIsPaused == nil if strongSelf.currentIsPaused != isPaused { strongSelf.currentIsPaused = isPaused - strongSelf.updatePlayPauseButton(paused: isPaused) + strongSelf.updatePlayPauseButton(paused: isPaused, animated: !isFirstTime) } strongSelf.playPauseButton.isEnabled = true @@ -548,7 +555,7 @@ final class OverlayPlayerControlsNode: ASDisplayNode { 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) + self.updatePlayPauseButton(paused: isPaused, animated: false) } if let order = self.currentOrder { self.updateOrderButton(order) @@ -611,11 +618,12 @@ final class OverlayPlayerControlsNode: ASDisplayNode { } } - private func updatePlayPauseButton(paused: Bool) { + private func updatePlayPauseButton(paused: Bool, animated: Bool) { + self.playPauseIconNode.customColor = self.presentationData.theme.list.itemPrimaryTextColor if paused { - self.playPauseButton.icon = generateTintedImage(image: UIImage(bundleImageName: "GlobalMusicPlayer/Play"), color: self.presentationData.theme.list.itemPrimaryTextColor) + self.playPauseIconNode.enqueueState(.play, animated: animated) } else { - self.playPauseButton.icon = generateTintedImage(image: UIImage(bundleImageName: "GlobalMusicPlayer/Pause"), color: self.presentationData.theme.list.itemPrimaryTextColor) + self.playPauseIconNode.enqueueState(.pause, animated: animated) } } @@ -726,7 +734,7 @@ final class OverlayPlayerControlsNode: ASDisplayNode { copyView.frame = previousAlbumArtNodeFrame copyView.center = largeAlbumArtFrame.center self.view.insertSubview(copyView, belowSubview: largeAlbumArtNode.view) - transition.animatePositionAdditive(layer: copyView.layer, offset: CGPoint(x: previousAlbumArtNodeFrame.center.x - largeAlbumArtFrame.center.x, y: previousAlbumArtNodeFrame.center.y - largeAlbumArtFrame.center.y), completion: { [weak copyView] in + transition.animatePositionAdditive(layer: copyView.layer, offset: CGPoint(x: previousAlbumArtNodeFrame.center.x - largeAlbumArtFrame.center.x, y: previousAlbumArtNodeFrame.center.y - largeAlbumArtFrame.center.y), completion: { [weak copyView] _ in copyView?.removeFromSuperview() }) //copyView.layer.animatePosition(from: CGPoint(x: -50.0, y: 0.0), to: CGPoint(), duration: 0.15, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, additive: true) @@ -779,7 +787,10 @@ final class OverlayPlayerControlsNode: ASDisplayNode { transition.updateFrame(node: self.backwardButton, frame: CGRect(origin: buttonsRect.origin, size: buttonSize)) transition.updateFrame(node: self.forwardButton, frame: CGRect(origin: CGPoint(x: buttonsRect.maxX - buttonSize.width, y: buttonsRect.minY), size: buttonSize)) - transition.updateFrame(node: self.playPauseButton, frame: CGRect(origin: CGPoint(x: buttonsRect.minX + floor((buttonsRect.width - buttonSize.width) / 2.0), y: buttonsRect.minY), size: buttonSize)) + + let playPauseFrame = CGRect(origin: CGPoint(x: buttonsRect.minX + floor((buttonsRect.width - buttonSize.width) / 2.0), y: buttonsRect.minY), size: buttonSize) + transition.updateFrame(node: self.playPauseButton, frame: playPauseFrame) + transition.updateFrame(node: self.playPauseIconNode, frame: CGRect(origin: CGPoint(x: -6.0, y: -6.0), size: CGSize(width: 76.0, height: 76.0))) return panelHeight } @@ -892,3 +903,53 @@ final class OverlayPlayerControlsNode: ASDisplayNode { return result } } + +private enum PlayPauseIconNodeState: Equatable { + case play + case pause +} + +private final class PlayPauseIconNode: ManagedAnimationNode { + private let duration: Double = 0.35 + private var iconState: PlayPauseIconNodeState = .pause + + init() { + super.init(size: CGSize(width: 76.0, height: 76.0)) + + self.trackTo(item: ManagedAnimationItem(source: .local("anim_playpause"), frames: .range(startFrame: 41, endFrame: 41), duration: 0.01)) + } + + func enqueueState(_ state: PlayPauseIconNodeState, animated: Bool) { + guard self.iconState != state else { + return + } + + let previousState = self.iconState + self.iconState = state + + switch previousState { + case .pause: + switch state { + case .play: + if animated { + self.trackTo(item: ManagedAnimationItem(source: .local("anim_playpause"), frames: .range(startFrame: 41, endFrame: 83), duration: self.duration)) + } else { + self.trackTo(item: ManagedAnimationItem(source: .local("anim_playpause"), frames: .range(startFrame: 0, endFrame: 0), duration: 0.01)) + } + case .pause: + break + } + case .play: + switch state { + case .pause: + if animated { + self.trackTo(item: ManagedAnimationItem(source: .local("anim_playpause"), frames: .range(startFrame: 0, endFrame: 41), duration: self.duration)) + } else { + self.trackTo(item: ManagedAnimationItem(source: .local("anim_playpause"), frames: .range(startFrame: 41, endFrame: 41), duration: 0.01)) + } + case .play: + break + } + } + } +} diff --git a/submodules/TelegramUI/Sources/PeerInfo/ListItems/PeerInfoScreenActionItem.swift b/submodules/TelegramUI/Sources/PeerInfo/ListItems/PeerInfoScreenActionItem.swift index 8b31b49345..959fa02d61 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/ListItems/PeerInfoScreenActionItem.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/ListItems/PeerInfoScreenActionItem.swift @@ -120,7 +120,11 @@ private final class PeerInfoScreenActionItemNode: PeerInfoScreenItemNode { self.iconNode.removeFromSupernode() } - transition.updateFrame(node: self.textNode, frame: textFrame) + if self.textNode.frame.width != textFrame.width { + self.textNode.frame = textFrame + } else { + 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) diff --git a/submodules/TelegramUI/Sources/PeerInfo/ListItems/PeerInfoScreenInfoItem.swift b/submodules/TelegramUI/Sources/PeerInfo/ListItems/PeerInfoScreenInfoItem.swift new file mode 100644 index 0000000000..9c345239af --- /dev/null +++ b/submodules/TelegramUI/Sources/PeerInfo/ListItems/PeerInfoScreenInfoItem.swift @@ -0,0 +1,108 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display +import TelegramPresentationData +import ItemListPeerItem +import SwiftSignalKit +import AccountContext +import Postbox +import SyncCore +import TelegramCore +import ItemListUI + +final class PeerInfoScreenInfoItem: PeerInfoScreenItem { + let id: AnyHashable + let title: String + let text: InfoListItemText + let linkAction: ((InfoListItemLinkAction) -> Void)? + + init( + id: AnyHashable, + title: String, + text: InfoListItemText, + linkAction: ((InfoListItemLinkAction) -> Void)? + ) { + self.id = id + self.title = title + self.text = text + self.linkAction = linkAction + } + + func node() -> PeerInfoScreenItemNode { + return PeerInfoScreenInfoItemNode() + } +} + +private final class PeerInfoScreenInfoItemNode: PeerInfoScreenItemNode { + private let bottomSeparatorNode: ASDisplayNode + + private var item: PeerInfoScreenInfoItem? + private var itemNode: InfoItemNode? + + override init() { + self.bottomSeparatorNode = ASDisplayNode() + self.bottomSeparatorNode.isLayerBacked = true + + super.init() + + self.addSubnode(self.bottomSeparatorNode) + } + + override func update(width: CGFloat, safeInsets: UIEdgeInsets, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, transition: ContainedViewLayoutTransition) -> CGFloat { + guard let item = item as? PeerInfoScreenInfoItem else { + return 10.0 + } + + self.item = item + + + let sideInset: CGFloat = 16.0 + safeInsets.left + + self.bottomSeparatorNode.backgroundColor = presentationData.theme.list.itemBlocksSeparatorColor + + let infoItem = InfoListItem(presentationData: ItemListPresentationData(presentationData), title: item.title, text: item.text, style: .blocks, linkAction: { link in + item.linkAction?(link) + }, closeAction: nil) + let params = ListViewItemLayoutParams(width: width, leftInset: safeInsets.left, rightInset: safeInsets.right, availableHeight: 1000.0) + + let itemNode: InfoItemNode + if let current = self.itemNode { + itemNode = current + infoItem.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? + infoItem.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! InfoItemNode + self.itemNode = itemNode + self.addSubnode(itemNode) + } + + let height = itemNode.contentSize.height + + transition.updateFrame(node: itemNode, frame: CGRect(origin: CGPoint(), size: itemNode.bounds.size)) + + var separatorInset: CGFloat = sideInset + if bottomItem != nil { + separatorInset += 49.0 + } + + transition.updateFrame(node: self.bottomSeparatorNode, frame: CGRect(origin: CGPoint(x: separatorInset, 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/Sources/PeerInfo/Panes/PeerInfoGroupsInCommonPaneNode.swift b/submodules/TelegramUI/Sources/PeerInfo/Panes/PeerInfoGroupsInCommonPaneNode.swift index b2bc8455e3..f8ede6a784 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/Panes/PeerInfoGroupsInCommonPaneNode.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/Panes/PeerInfoGroupsInCommonPaneNode.swift @@ -124,6 +124,9 @@ final class PeerInfoGroupsInCommonPaneNode: ASDisplayNode, PeerInfoPaneNode { self.disposable?.dispose() } + func ensureMessageIsVisible(id: MessageId) { + } + 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: .Default(duration: nil), directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) @@ -143,7 +146,7 @@ final class PeerInfoGroupsInCommonPaneNode: ASDisplayNode, PeerInfoPaneNode { var scrollToItem: ListViewScrollToItem? if isScrollingLockedAtTop { switch self.listNode.visibleContentOffset() { - case .known(0.0): + case let .known(value) where value <= CGFloat.ulpOfOne: break default: scrollToItem = ListViewScrollToItem(index: 0, position: .top(0.0), animated: true, curve: .Spring(duration: duration), directionHint: .Up) diff --git a/submodules/TelegramUI/Sources/PeerInfo/Panes/PeerInfoListPaneNode.swift b/submodules/TelegramUI/Sources/PeerInfo/Panes/PeerInfoListPaneNode.swift index d90a5a3c1d..8ed2fdda00 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/Panes/PeerInfoListPaneNode.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/Panes/PeerInfoListPaneNode.swift @@ -138,6 +138,10 @@ final class PeerInfoListPaneNode: ASDisplayNode, PeerInfoPaneNode { self.playlistPreloadDisposable?.dispose() } + func ensureMessageIsVisible(id: MessageId) { + + } + func scrollToTop() -> Bool { let offset = self.listNode.visibleContentOffset() switch offset { @@ -201,7 +205,7 @@ final class PeerInfoListPaneNode: ASDisplayNode, PeerInfoPaneNode { }) } - let mediaAccessoryPanel = MediaNavigationAccessoryPanel(context: self.context) + let mediaAccessoryPanel = MediaNavigationAccessoryPanel(context: self.context, displayBackground: true) mediaAccessoryPanel.containerNode.headerNode.displayScrubber = item.playbackData?.type != .instantVideo mediaAccessoryPanel.close = { [weak self] in if let strongSelf = self, let (_, _, _, _, type, _) = strongSelf.playlistStateAndType { @@ -387,7 +391,9 @@ final class PeerInfoListPaneNode: ASDisplayNode, PeerInfoPaneNode { self.listNode.updateLayout(transition: transition, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: size, insets: UIEdgeInsets(top: topPanelHeight, left: sideInset, bottom: bottomInset, right: sideInset), duration: duration, curve: curve)) if isScrollingLockedAtTop { switch self.listNode.visibleContentOffset() { - case .known(0.0), .none: + case let .known(value) where value <= CGFloat.ulpOfOne: + break + case .none: break default: self.listNode.scrollToEndOfHistory() diff --git a/submodules/TelegramUI/Sources/PeerInfo/Panes/PeerInfoMembersPane.swift b/submodules/TelegramUI/Sources/PeerInfo/Panes/PeerInfoMembersPane.swift index e7b27c8f4e..0cdb4c218b 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/Panes/PeerInfoMembersPane.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/Panes/PeerInfoMembersPane.swift @@ -168,6 +168,9 @@ final class PeerInfoMembersPaneNode: ASDisplayNode, PeerInfoPaneNode { deinit { } + func ensureMessageIsVisible(id: MessageId) { + } + 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: .Default(duration: nil), directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) @@ -188,7 +191,7 @@ final class PeerInfoMembersPaneNode: ASDisplayNode, PeerInfoPaneNode { var scrollToItem: ListViewScrollToItem? if isScrollingLockedAtTop { switch self.listNode.visibleContentOffset() { - case .known(0.0): + case let .known(value) where value <= CGFloat.ulpOfOne: break default: scrollToItem = ListViewScrollToItem(index: 0, position: .top(0.0), animated: true, curve: .Spring(duration: duration), directionHint: .Up) diff --git a/submodules/TelegramUI/Sources/PeerInfo/Panes/PeerInfoVisualMediaPaneNode.swift b/submodules/TelegramUI/Sources/PeerInfo/Panes/PeerInfoVisualMediaPaneNode.swift index 43b87e4f25..6dfd9daf04 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/Panes/PeerInfoVisualMediaPaneNode.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/Panes/PeerInfoVisualMediaPaneNode.swift @@ -872,6 +872,21 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro self.animationTimer?.invalidate() } + func ensureMessageIsVisible(id: MessageId) { + let activeRect = self.scrollNode.bounds + for item in self.mediaItems { + if item.message.id == id { + if let itemNode = self.visibleMediaItems[item.message.stableId] { + if !activeRect.contains(itemNode.frame) { + let targetContentOffset = CGPoint(x: 0.0, y: max(-self.scrollNode.view.contentInset.top, itemNode.frame.minY - (self.scrollNode.frame.height - itemNode.frame.height) / 2.0)) + self.scrollNode.view.setContentOffset(targetContentOffset, animated: false) + } + } + break + } + } + } + private func requestHistoryAroundVisiblePosition() { if self.isRequestingView { return diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoData.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoData.swift index 3d9378ce6d..c4b9964353 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoData.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoData.swift @@ -112,6 +112,7 @@ final class PeerInfoState { final class TelegramGlobalSettings { let suggestPhoneNumberConfirmation: Bool + let suggestPasswordConfirmation: Bool let accountsAndPeers: [(Account, Peer, Int32)] let activeSessionsContext: ActiveSessionsContext? let webSessionsContext: WebSessionsContext? @@ -130,6 +131,7 @@ final class TelegramGlobalSettings { init( suggestPhoneNumberConfirmation: Bool, + suggestPasswordConfirmation: Bool, accountsAndPeers: [(Account, Peer, Int32)], activeSessionsContext: ActiveSessionsContext?, webSessionsContext: WebSessionsContext?, @@ -147,6 +149,7 @@ final class TelegramGlobalSettings { enableQRLogin: Bool ) { self.suggestPhoneNumberConfirmation = suggestPhoneNumberConfirmation + self.suggestPasswordConfirmation = suggestPasswordConfirmation self.accountsAndPeers = accountsAndPeers self.activeSessionsContext = activeSessionsContext self.webSessionsContext = webSessionsContext @@ -338,40 +341,6 @@ private func peerInfoScreenInputData(context: AccountContext, peerId: PeerId, is |> distinctUntilChanged } -private func peerInfoProfilePhotos(context: AccountContext, peerId: PeerId) -> Signal { - return context.account.postbox.combinedView(keys: [.basicPeer(peerId)]) - |> mapToSignal { view -> Signal in - guard let peer = (view.views[.basicPeer(peerId)] as? BasicPeerView)?.peer else { - return .single(nil) - } - return initialAvatarGalleryEntries(account: context.account, peer: peer) - |> map { entries in - return entries.first - } - } - |> distinctUntilChanged - |> mapToSignal { firstEntry -> Signal<(Bool, [AvatarGalleryEntry]), NoError> in - if let firstEntry = firstEntry { - return context.account.postbox.loadedPeerWithId(peerId) - |> mapToSignal { peer -> Signal<(Bool, [AvatarGalleryEntry]), NoError>in - return fetchedAvatarGalleryEntries(account: context.account, peer: peer, firstEntry: firstEntry) - } - } else { - return .single((true, [])) - } - } - |> map { items -> Any in - return items - } -} - -func peerInfoProfilePhotosWithCache(context: AccountContext, peerId: PeerId) -> Signal<(Bool, [AvatarGalleryEntry]), NoError> { - return context.peerChannelMemberCategoriesContextsManager.profilePhotos(postbox: context.account.postbox, network: context.account.network, peerId: peerId, fetch: peerInfoProfilePhotos(context: context, peerId: peerId)) - |> map { items -> (Bool, [AvatarGalleryEntry]) in - return items as? (Bool, [AvatarGalleryEntry]) ?? (true, []) - } -} - func keepPeerInfoScreenDataHot(context: AccountContext, peerId: PeerId) -> Signal { return peerInfoScreenInputData(context: context, peerId: peerId, isSettings: false) |> mapToSignal { inputData -> Signal in @@ -431,9 +400,10 @@ func peerInfoScreenSettingsData(context: AccountContext, peerId: PeerId, account combineLatest(context.account.viewTracker.featuredStickerPacks(), archivedStickerPacks), hasPassport, (context.watchManager?.watchAppInstalled ?? .single(false)), - context.account.postbox.preferencesView(keys: [PreferencesKeys.appConfiguration]) + context.account.postbox.preferencesView(keys: [PreferencesKeys.appConfiguration]), + getServerProvidedSuggestions(account: context.account) ) - |> map { peerView, accountsAndPeers, accountSessions, privacySettings, sharedPreferences, notifications, stickerPacks, hasPassport, hasWatchApp, accountPreferences -> PeerInfoScreenData in + |> map { peerView, accountsAndPeers, accountSessions, privacySettings, sharedPreferences, notifications, stickerPacks, hasPassport, hasWatchApp, accountPreferences, suggestions -> PeerInfoScreenData in let (notificationExceptions, notificationsAuthorizationStatus, notificationsWarningSuppressed) = notifications let (featuredStickerPacks, archivedStickerPacks) = stickerPacks @@ -450,7 +420,7 @@ func peerInfoScreenSettingsData(context: AccountContext, peerId: PeerId, account enableQRLogin = true } - let globalSettings = TelegramGlobalSettings(suggestPhoneNumberConfirmation: false, accountsAndPeers: accountsAndPeers, activeSessionsContext: accountSessions?.0, webSessionsContext: accountSessions?.2, otherSessionsCount: accountSessions?.1, proxySettings: proxySettings, notificationAuthorizationStatus: notificationsAuthorizationStatus, notificationWarningSuppressed: notificationsWarningSuppressed, notificationExceptions: notificationExceptions, inAppNotificationSettings: inAppNotificationSettings, privacySettings: privacySettings, unreadTrendingStickerPacks: unreadTrendingStickerPacks, archivedStickerPacks: archivedStickerPacks, hasPassport: hasPassport, hasWatchApp: hasWatchApp, enableQRLogin: enableQRLogin) + let globalSettings = TelegramGlobalSettings(suggestPhoneNumberConfirmation: suggestions.contains(.validatePhoneNumber), suggestPasswordConfirmation: suggestions.contains(.validatePassword), accountsAndPeers: accountsAndPeers, activeSessionsContext: accountSessions?.0, webSessionsContext: accountSessions?.2, otherSessionsCount: accountSessions?.1, proxySettings: proxySettings, notificationAuthorizationStatus: notificationsAuthorizationStatus, notificationWarningSuppressed: notificationsWarningSuppressed, notificationExceptions: notificationExceptions, inAppNotificationSettings: inAppNotificationSettings, privacySettings: privacySettings, unreadTrendingStickerPacks: unreadTrendingStickerPacks, archivedStickerPacks: archivedStickerPacks, hasPassport: hasPassport, hasWatchApp: hasWatchApp, enableQRLogin: enableQRLogin) return PeerInfoScreenData( peer: peerView.peers[peerId], @@ -679,7 +649,7 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen canManageInvitations = true } if canManageInvitations { - let invitationsContext = PeerExportedInvitationsContext(account: context.account, peerId: peerId, adminId: nil, revoked: false, forceUpdate: true) + let invitationsContext = context.engine.peers.peerExportedInvitations(peerId: peerId, adminId: nil, revoked: false, forceUpdate: true) invitationsContextPromise.set(.single(invitationsContext)) invitationsStatePromise.set(invitationsContext.state |> map(Optional.init)) } @@ -722,10 +692,10 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen |> 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) + return context.peerChannelMemberCategoriesContextsManager.recentOnline(account: context.account, 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) + return context.peerChannelMemberCategoriesContextsManager.recentOnlineSmall(engine: context.engine, postbox: context.account.postbox, network: context.account.network, accountPeerId: context.account.peerId, peerId: peerId) |> map(Optional.init) } } else { @@ -842,7 +812,7 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen canManageInvitations = true } if canManageInvitations { - let invitationsContext = PeerExportedInvitationsContext(account: context.account, peerId: peerId, adminId: nil, revoked: false, forceUpdate: true) + let invitationsContext = context.engine.peers.peerExportedInvitations(peerId: peerId, adminId: nil, revoked: false, forceUpdate: true) invitationsContextPromise.set(.single(invitationsContext)) invitationsStatePromise.set(invitationsContext.state |> map(Optional.init)) } @@ -910,7 +880,9 @@ func availableActionsForMemberOfPeer(accountPeerId: PeerId, peer: Peer?, member: } else if member.id != accountPeerId { if let channel = peer as? TelegramChannel { if channel.flags.contains(.isCreator) { - result.insert(.restrict) + if !channel.flags.contains(.isGigagroup) { + result.insert(.restrict) + } result.insert(.promote) } else { switch member { @@ -921,13 +893,15 @@ func availableActionsForMemberOfPeer(accountPeerId: PeerId, peer: Peer?, member: case let .member(member): if let adminInfo = member.adminInfo { if adminInfo.promotedBy == accountPeerId { - result.insert(.restrict) + if !channel.flags.contains(.isGigagroup) { + result.insert(.restrict) + } if channel.hasPermission(.addAdmins) { result.insert(.promote) } } } else { - if channel.hasPermission(.banMembers) { + if channel.hasPermission(.banMembers) && !channel.flags.contains(.isGigagroup) { result.insert(.restrict) } if channel.hasPermission(.addAdmins) { @@ -976,7 +950,27 @@ func availableActionsForMemberOfPeer(accountPeerId: PeerId, peer: Peer?, member: return result } -func peerInfoHeaderButtons(peer: Peer?, cachedData: CachedPeerData?, isOpenedFromChat: Bool, videoCallsEnabled: Bool, isSecretChat: Bool, isContact: Bool) -> [PeerInfoHeaderButtonKey] { +func peerInfoHeaderButtonIsHiddenWhileExpanded(buttonKey: PeerInfoHeaderButtonKey, isOpenedFromChat: Bool) -> Bool { + var hiddenWhileExpanded = false + if isOpenedFromChat { + switch buttonKey { + case .message, .search, .videoCall, .addMember, .leave, .discussion: + hiddenWhileExpanded = true + default: + hiddenWhileExpanded = false + } + } else { + switch buttonKey { + case .search, .call, .videoCall, .addMember, .leave, .discussion: + hiddenWhileExpanded = true + default: + hiddenWhileExpanded = false + } + } + return hiddenWhileExpanded +} + +func peerInfoHeaderButtons(peer: Peer?, cachedData: CachedPeerData?, isOpenedFromChat: Bool, isExpanded: Bool, videoCallsEnabled: Bool, isSecretChat: Bool, isContact: Bool) -> [PeerInfoHeaderButtonKey] { var result: [PeerInfoHeaderButtonKey] = [] if let user = peer as? TelegramUser { if !isOpenedFromChat { @@ -1020,7 +1014,7 @@ func peerInfoHeaderButtons(peer: Peer?, cachedData: CachedPeerData?, isOpenedFro if channel.flags.contains(.hasVoiceChat) { hasVoiceChat = true } - if channel.flags.contains(.isCreator) || channel.hasPermission(.manageCalls) { + if channel.flags.contains(.isCreator) { displayMore = true } switch channel.info { @@ -1032,7 +1026,6 @@ func peerInfoHeaderButtons(peer: Peer?, cachedData: CachedPeerData?, isOpenedFro hasDiscussion = true } case .group: - displayLeave = false if channel.flags.contains(.isCreator) || channel.hasPermission(.inviteMembers) { result.append(.addMember) } @@ -1049,30 +1042,22 @@ func peerInfoHeaderButtons(peer: Peer?, cachedData: CachedPeerData?, isOpenedFro default: displayLeave = false } - if canViewStats { - displayLeave = false - } result.append(.mute) - if hasVoiceChat { + if hasVoiceChat || canStartVoiceChat { result.append(.voiceChat) } if hasDiscussion { result.append(.discussion) } result.append(.search) - if displayLeave { + if displayLeave && result.count < 4 { result.append(.leave) } - if displayLeave && !channel.flags.contains(.isCreator) { - if let _ = channel.adminRights { - displayMore = false - } - } var canReport = true if channel.isVerified || channel.adminRights != nil || channel.flags.contains(.isCreator) { canReport = false } - if !canReport && !canViewStats && !canStartVoiceChat { + if !canReport && !canViewStats { displayMore = false } if displayMore { @@ -1085,10 +1070,18 @@ func peerInfoHeaderButtons(peer: Peer?, cachedData: CachedPeerData?, isOpenedFro var isPublic = false var isCreator = false var hasVoiceChat = false + var canStartVoiceChat = false if group.flags.contains(.hasVoiceChat) { hasVoiceChat = true } + if !hasVoiceChat { + if case .creator = group.role { + canStartVoiceChat = true + } else if case let .admin(rights, _) = group.role, rights.rights.contains(.canManageCalls) { + canStartVoiceChat = true + } + } if case .creator = group.role { isCreator = true @@ -1107,18 +1100,22 @@ func peerInfoHeaderButtons(peer: Peer?, cachedData: CachedPeerData?, isOpenedFro if !group.hasBannedPermission(.banAddMembers) { canAddMembers = true } - if canAddMembers { result.append(.addMember) } - result.append(.mute) - if hasVoiceChat { + if hasVoiceChat || canStartVoiceChat { result.append(.voiceChat) } result.append(.search) result.append(.more) } + if isExpanded && result.count > 3 { + result = result.filter { !peerInfoHeaderButtonIsHiddenWhileExpanded(buttonKey: $0, isOpenedFromChat: isOpenedFromChat) } + if !result.contains(.more) { + result.append(.more) + } + } return result } diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoHeaderNode.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoHeaderNode.swift index 5a8ac7ecc5..662ae05eb1 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoHeaderNode.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoHeaderNode.swift @@ -19,6 +19,9 @@ import GalleryUI import UniversalMediaPlayer import RadialStatusNode import TelegramUIPreferences +import PeerInfoAvatarListNode +import AnimationUI +import ContextUI enum PeerInfoHeaderButtonKey: Hashable { case message @@ -52,7 +55,9 @@ final class PeerInfoHeaderButtonNode: HighlightableButtonNode { let referenceNode: ContextReferenceContentNode let containerNode: ContextControllerSourceNode private let backgroundNode: ASImageNode + private let iconNode: ASImageNode private let textNode: ImmediateTextNode + private var animationNode: AnimationNode? private var theme: PresentationTheme? private var icon: PeerInfoHeaderButtonIcon? @@ -70,6 +75,10 @@ final class PeerInfoHeaderButtonNode: HighlightableButtonNode { self.backgroundNode.displaysAsynchronously = false self.backgroundNode.displayWithoutProcessing = true + self.iconNode = ASImageNode() + self.iconNode.displaysAsynchronously = false + self.iconNode.displayWithoutProcessing = true + self.textNode = ImmediateTextNode() self.textNode.displaysAsynchronously = false @@ -79,6 +88,7 @@ final class PeerInfoHeaderButtonNode: HighlightableButtonNode { self.containerNode.addSubnode(self.referenceNode) self.referenceNode.addSubnode(self.backgroundNode) + self.referenceNode.addSubnode(self.iconNode) self.addSubnode(self.containerNode) self.addSubnode(self.textNode) @@ -104,49 +114,109 @@ final class PeerInfoHeaderButtonNode: HighlightableButtonNode { } @objc private func buttonPressed() { + switch self.icon { + case .voiceChat, .more, .leave: + self.animationNode?.playOnce() + default: + break + } self.action(self, nil) } func update(size: CGSize, text: String, icon: PeerInfoHeaderButtonIcon, isActive: Bool, isExpanded: Bool, presentationData: PresentationData, transition: ContainedViewLayoutTransition) { - if self.theme != presentationData.theme || self.icon != icon || self.isActive != isActive { + let previousIcon = self.icon + let iconUpdated = self.icon != icon + let isActiveUpdated = self.isActive != isActive + self.isActive = isActive + if self.theme != presentationData.theme || self.icon != icon { self.theme = presentationData.theme self.icon = icon - let isActiveUpdated = self.isActive != isActive - self.isActive = isActive - var isGestureEnabled = false if [.mute, .voiceChat, .more].contains(icon) { isGestureEnabled = true } self.containerNode.isGestureEnabled = isGestureEnabled - - if isActiveUpdated, !self.containerNode.alpha.isZero { - if let snapshotView = self.backgroundNode.view.snapshotContentTree() { - snapshotView.frame = self.backgroundNode.view.frame - self.view.addSubview(snapshotView) - - snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak snapshotView] _ in - snapshotView?.removeFromSuperview() - }) - } - if !isExpanded, let snapshotView = self.textNode.view.snapshotContentTree() { - snapshotView.frame = self.textNode.view.frame - self.view.addSubview(snapshotView) - - snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak snapshotView] _ in - snapshotView?.removeFromSuperview() - }) - } + + let animationName: String? + var colors: [String: UIColor] = [:] + var playOnce = false + var seekToEnd = false + let iconColor = presentationData.theme.list.itemCheckColors.foregroundColor + switch icon { + case .voiceChat: + animationName = "anim_profilevc" + colors = ["Line 3.Group 1.Stroke 1": iconColor, + "Line 1.Group 1.Stroke 1": iconColor, + "Line 2.Group 1.Stroke 1": iconColor] + case .mute: + animationName = "anim_profileunmute" + colors = ["Middle.Group 1.Fill 1": iconColor, + "Top.Group 1.Fill 1": iconColor, + "Bottom.Group 1.Fill 1": iconColor, + "EXAMPLE.Group 1.Fill 1": iconColor, + "Line.Group 1.Stroke 1": iconColor] + if previousIcon == .unmute { + playOnce = true + } else { + seekToEnd = true + } + case .unmute: + animationName = "anim_profilemute" + colors = ["Middle.Group 1.Fill 1": iconColor, + "Top.Group 1.Fill 1": iconColor, + "Bottom.Group 1.Fill 1": iconColor, + "EXAMPLE.Group 1.Fill 1": iconColor, + "Line.Group 1.Stroke 1": iconColor] + if previousIcon == .mute { + playOnce = true + } else { + seekToEnd = true + } + case .more: + animationName = "anim_profilemore" + colors = ["Point 2.Group 1.Fill 1": iconColor, + "Point 3.Group 1.Fill 1": iconColor, + "Point 1.Group 1.Fill 1": iconColor] + case .leave: + animationName = "anim_profileleave" + colors = ["Arrow.Group 2.Stroke 1": iconColor, + "Door.Group 1.Stroke 1": iconColor, + "Arrow.Group 1.Stroke 1": iconColor] + default: + animationName = nil } - self.backgroundNode.image = generateImage(CGSize(width: 40.0, height: 40.0), contextGenerator: { size, context in + if let animationName = animationName { + let animationNode: AnimationNode + if let current = self.animationNode { + animationNode = current + if iconUpdated { + animationNode.setAnimation(name: animationName, colors: colors) + } + } else { + animationNode = AnimationNode(animation: animationName, colors: colors, scale: 1.0) + self.referenceNode.addSubnode(animationNode) + self.animationNode = animationNode + } + animationNode.frame = CGRect(origin: CGPoint(), size: size) + } else if let animationNode = self.animationNode { + self.animationNode = nil + animationNode.removeFromSupernode() + } + + if playOnce { + self.animationNode?.play() + } else if seekToEnd { + self.animationNode?.seekToEnd() + } + + self.backgroundNode.image = generateFilledCircleImage(diameter: 40.0, color: presentationData.theme.list.itemAccentColor) + self.iconNode.image = generateImage(CGSize(width: 40.0, height: 40.0), contextGenerator: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) - context.setFillColor(isActive ? presentationData.theme.list.itemAccentColor.cgColor : presentationData.theme.list.itemDisabledTextColor.cgColor) - context.fillEllipse(in: CGRect(origin: CGPoint(), size: size)) context.setBlendMode(.normal) context.setFillColor(presentationData.theme.list.itemCheckColors.foregroundColor.cgColor) - let imageName: String + let imageName: String? switch icon { case .message: imageName = "Peer Info/ButtonMessage" @@ -155,21 +225,21 @@ final class PeerInfoHeaderButtonNode: HighlightableButtonNode { case .videoCall: imageName = "Peer Info/ButtonVideo" case .voiceChat: - imageName = "Peer Info/ButtonVoiceChat" + imageName = nil case .mute: - imageName = "Peer Info/ButtonMute" + imageName = nil case .unmute: - imageName = "Peer Info/ButtonUnmute" + imageName = nil case .more: - imageName = "Peer Info/ButtonMore" + imageName = nil case .addMember: imageName = "Peer Info/ButtonAddMember" case .search: imageName = "Peer Info/ButtonSearch" case .leave: - imageName = "Peer Info/ButtonLeave" + imageName = nil } - if let image = generateTintedImage(image: UIImage(bundleImageName: imageName), color: .white) { + if let imageName = imageName, let image = generateTintedImage(image: UIImage(bundleImageName: imageName), color: .white) { 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) @@ -177,14 +247,24 @@ final class PeerInfoHeaderButtonNode: HighlightableButtonNode { }) } - self.textNode.attributedText = NSAttributedString(string: text, font: Font.regular(12.0), textColor: isActive ? presentationData.theme.list.itemAccentColor : presentationData.theme.list.itemDisabledTextColor) + let alpha: CGFloat = isActive ? 1.0 : 0.3 + if isActiveUpdated, !self.containerNode.alpha.isZero { + let alphaTransition = ContainedViewLayoutTransition.animated(duration: 0.2, curve: .easeInOut) + alphaTransition.updateAlpha(node: self.backgroundNode, alpha: isActive ? 1.0 : 0.3) + if !isExpanded { + alphaTransition.updateAlpha(node: self.textNode, alpha: isActive ? 1.0 : 0.3) + } + } + + self.textNode.attributedText = NSAttributedString(string: text, font: Font.regular(12.0), textColor: presentationData.theme.list.itemAccentColor) self.accessibilityLabel = text 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.updateFrame(node: self.iconNode, 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) + transition.updateAlpha(node: self.textNode, alpha: isExpanded ? 0.0 : alpha) self.referenceNode.frame = self.containerNode.bounds } @@ -206,1095 +286,6 @@ final class PeerInfoHeaderNavigationTransition { } } -enum PeerInfoAvatarListItem: Equatable { - case topImage([ImageRepresentationWithReference], [VideoRepresentationWithReference], Data?) - case image(TelegramMediaImageReference?, [ImageRepresentationWithReference], [VideoRepresentationWithReference], Data?) - - 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) - } - } - - var videoRepresentations: [VideoRepresentationWithReference] { - switch self { - case let .topImage(_, videoRepresentations, _): - return videoRepresentations - case let .image(_, _, videoRepresentations, _): - return videoRepresentations - } - } - - init(entry: AvatarGalleryEntry) { - switch entry { - case let .topImage(representations, videoRepresentations, _, _, immediateThumbnailData, _): - self = .topImage(representations, videoRepresentations, immediateThumbnailData) - case let .image(_, reference, representations, videoRepresentations, _, _, _, _, immediateThumbnailData, _): - self = .image(reference, representations, videoRepresentations, immediateThumbnailData) - } - } -} - -final class PeerInfoAvatarListItemNode: ASDisplayNode { - private let context: AccountContext - private let peer: Peer - let imageNode: TransformImageNode - private var videoNode: UniversalVideoNode? - private var videoContent: NativeVideoContent? - private var videoStartTimestamp: Double? - private let playbackStartDisposable = MetaDisposable() - private let statusDisposable = MetaDisposable() - private let preloadDisposable = MetaDisposable() - private let statusNode: RadialStatusNode - - private var playerStatus: MediaPlayerStatus? - private var isLoading = ValuePromise(false) - private var loadingProgress = ValuePromise(nil) - private var loadingProgressDisposable = MetaDisposable() - private var hasProgress = false - - let isReady = Promise() - private var didSetReady: Bool = false - - var item: PeerInfoAvatarListItem? - - private var statusPromise = Promise<(MediaPlayerStatus?, Double?)?>() - var mediaStatus: Signal<(MediaPlayerStatus?, Double?)?, NoError> { - get { - return self.statusPromise.get() - } - } - - var delayCentralityLose = false - var isCentral: Bool? = nil { - didSet { - guard self.isCentral != oldValue, let isCentral = self.isCentral else { - return - } - if isCentral { - self.setupVideoPlayback() - self.preloadDisposable.set(nil) - } else { - if let videoNode = self.videoNode { - self.playbackStartDisposable.set(nil) - self.statusPromise.set(.single(nil)) - self.videoNode = nil - if self.delayCentralityLose { - Queue.mainQueue().after(0.5) { - videoNode.removeFromSupernode() - } - } else { - videoNode.removeFromSupernode() - } - } - if let videoContent = self.videoContent { - let duration: Double = (self.videoStartTimestamp ?? 0.0) + 4.0 - self.preloadDisposable.set(preloadVideoResource(postbox: self.context.account.postbox, resourceReference: videoContent.fileReference.resourceReference(videoContent.fileReference.media.resource), duration: duration).start()) - } - } - } - } - - init(context: AccountContext, peer: Peer) { - self.context = context - self.peer = peer - self.imageNode = TransformImageNode() - - self.statusNode = RadialStatusNode(backgroundNodeColor: UIColor(rgb: 0x000000, alpha: 0.3)) - self.statusNode.isUserInteractionEnabled = false - - super.init() - - self.clipsToBounds = true - - self.imageNode.contentAnimations = [.firstUpdate, .subsequentUpdates] - self.addSubnode(self.imageNode) - self.addSubnode(self.statusNode) - - self.loadingProgressDisposable.set((combineLatest(self.isLoading.get() - |> mapToSignal { value -> Signal in - if value { - return .single(value) |> delay(0.5, queue: Queue.mainQueue()) - } else { - return .single(value) - } - } |> distinctUntilChanged, self.loadingProgress.get() |> distinctUntilChanged)).start(next: { [weak self] isLoading, progress in - guard let strongSelf = self else { - return - } - if isLoading, let progress = progress { - strongSelf.hasProgress = true - strongSelf.statusNode.transitionToState(.progress(color: .white, lineWidth: nil, value: CGFloat(max(0.027, progress)), cancelEnabled: false, animateRotation: true), completion: {}) - } else if strongSelf.hasProgress { - strongSelf.hasProgress = false - strongSelf.statusNode.transitionToState(.progress(color: .white, lineWidth: nil, value: 1.0, cancelEnabled: false, animateRotation: true), completion: { [weak self] in - guard let strongSelf = self else { - return - } - if !strongSelf.hasProgress { - Queue.mainQueue().after(0.3) { - strongSelf.statusNode.transitionToState(.none, completion: {}) - } - } - }) - } - })) - } - - deinit { - self.statusDisposable.dispose() - self.playbackStartDisposable.dispose() - self.preloadDisposable.dispose() - } - - private func updateStatus() { - guard let videoContent = self.videoContent else { - return - } - - var bufferingProgress: Float? - if isMediaStreamable(resource: videoContent.fileReference.media.resource) { - if let playerStatus = self.playerStatus { - if case let .buffering(_, _, progress, _) = playerStatus.status { - bufferingProgress = progress - } else if case .playing = playerStatus.status { - bufferingProgress = nil - } - } else { - bufferingProgress = nil - } - } - self.loadingProgress.set(bufferingProgress) - self.isLoading.set(bufferingProgress != nil) - } - - func updateTransitionFraction(_ fraction: CGFloat, transition: ContainedViewLayoutTransition) { - if let videoNode = self.videoNode { - if case .immediate = transition, fraction == 1.0 { - return - } - transition.updateAlpha(node: videoNode, alpha: 1.0 - fraction) - } - } - - private func setupVideoPlayback() { - guard let videoContent = self.videoContent, let isCentral = self.isCentral, isCentral, self.videoNode == nil else { - return - } - - let mediaManager = self.context.sharedContext.mediaManager - let videoNode = UniversalVideoNode(postbox: self.context.account.postbox, audioSession: mediaManager.audioSession, manager: mediaManager.universalVideoManager, decoration: GalleryVideoDecoration(), content: videoContent, priority: .secondaryOverlay) - videoNode.isUserInteractionEnabled = false - videoNode.canAttachContent = true - videoNode.isHidden = true - - if let _ = self.videoStartTimestamp { - self.playbackStartDisposable.set((videoNode.status - |> map { status -> Bool in - if let status = status, case .playing = status.status { - return true - } else { - return false - } - } - |> filter { playing in - return playing - } - |> take(1) - |> deliverOnMainQueue).start(completed: { [weak self] in - if let strongSelf = self { - Queue.mainQueue().after(0.1) { - strongSelf.videoNode?.isHidden = false - } - } - })) - } else { - self.playbackStartDisposable.set(nil) - videoNode.isHidden = false - } - videoNode.play() - - self.videoNode = videoNode - let videoStartTimestamp = self.videoStartTimestamp - self.statusPromise.set(videoNode.status |> map { ($0, videoStartTimestamp) }) - - self.statusDisposable.set((self.mediaStatus - |> deliverOnMainQueue).start(next: { [weak self] mediaStatus in - if let strongSelf = self { - if let mediaStatusAndStartTimestamp = mediaStatus { - strongSelf.playerStatus = mediaStatusAndStartTimestamp.0 - } - strongSelf.updateStatus() - } - })) - - self.insertSubnode(videoNode, belowSubnode: self.statusNode) - - self.isReady.set(videoNode.ready |> map { return true }) - } - - func setup(item: PeerInfoAvatarListItem, synchronous: Bool) { - self.item = item - - let representations: [ImageRepresentationWithReference] - let videoRepresentations: [VideoRepresentationWithReference] - let immediateThumbnailData: Data? - var id: Int64 - switch item { - case let .topImage(topRepresentations, videoRepresentationsValue, immediateThumbnail): - representations = topRepresentations - videoRepresentations = videoRepresentationsValue - immediateThumbnailData = immediateThumbnail - id = Int64(self.peer.id.id) - if let resource = videoRepresentations.first?.representation.resource as? CloudPhotoSizeMediaResource { - id = id &+ resource.photoId - } - case let .image(reference, imageRepresentations, videoRepresentationsValue, immediateThumbnail): - representations = imageRepresentations - videoRepresentations = videoRepresentationsValue - immediateThumbnailData = immediateThumbnail - if case let .cloud(imageId, _, _) = reference { - id = imageId - } else { - id = Int64(self.peer.id.id) - } - } - self.imageNode.setSignal(chatAvatarGalleryPhoto(account: self.context.account, representations: representations, immediateThumbnailData: immediateThumbnailData, autoFetchFullSize: true, attemptSynchronously: synchronous), attemptSynchronously: synchronous, dispatchOnDisplayLink: false) - - if let video = videoRepresentations.last, let peerReference = PeerReference(self.peer) { - let videoFileReference = FileMediaReference.avatarList(peer: peerReference, media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: video.representation.resource, previewRepresentations: representations.map { $0.representation }, videoThumbnails: [], immediateThumbnailData: immediateThumbnailData, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: video.representation.dimensions, flags: [])])) - let videoContent = NativeVideoContent(id: .profileVideo(id, nil), fileReference: videoFileReference, streamVideo: isMediaStreamable(resource: video.representation.resource) ? .conservative : .none, loopVideo: true, enableSound: false, fetchAutomatically: true, onlyFullSizeThumbnail: false, useLargeThumbnail: true, autoFetchFullSizeThumbnail: true, startTimestamp: video.representation.startTimestamp, continuePlayingWithoutSoundOnLostAudioSession: false, placeholderColor: .clear) - - if videoContent.id != self.videoContent?.id { - self.videoContent = videoContent - self.videoStartTimestamp = video.representation.startTimestamp - self.setupVideoPlayback() - } - } else { - if let videoNode = self.videoNode { - self.videoContent = nil - self.videoStartTimestamp = nil - self.videoNode = nil - - videoNode.removeFromSupernode() - } - - self.statusPromise.set(.single(nil)) - - self.statusDisposable.set(nil) - - self.imageNode.imageUpdated = { [weak self] _ in - guard let strongSelf = self else { - return - } - if !strongSelf.didSetReady { - strongSelf.didSetReady = true - strongSelf.isReady.set(.single(true)) - } - } - } - } - - 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() - let imageFrame = CGRect(origin: CGPoint(x: floor((size.width - imageSize.width) / 2.0), y: floor((size.height - imageSize.height) / 2.0)), size: imageSize) - transition.updateFrame(node: self.imageNode, frame: imageFrame) - - transition.updateFrame(node: self.statusNode, frame: CGRect(origin: CGPoint(x: floor((size.width - 50.0) / 2.0), y: floor((size.height - 50.0) / 2.0)), size: CGSize(width: 50.0, height: 50.0))) - - if let videoNode = self.videoNode { - videoNode.updateLayout(size: imageSize, transition: .immediate) - videoNode.frame = imageFrame - } - } -} - -private class PeerInfoAvatarListLoadingStripNode: ASImageNode { - private var currentInHierarchy = false - - let imageNode = ASImageNode() - - override init() { - super.init() - - self.addSubnode(self.imageNode) - } - - override public var isHidden: Bool { - didSet { - self.updateAnimation() - } - } - private var isAnimating = false { - didSet { - if self.isAnimating != oldValue { - if self.isAnimating { - let basicAnimation = CABasicAnimation(keyPath: "opacity") - basicAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut) - basicAnimation.duration = 0.45 - basicAnimation.fromValue = 0.1 - basicAnimation.toValue = 0.75 - basicAnimation.repeatCount = Float.infinity - basicAnimation.autoreverses = true - - self.imageNode.layer.add(basicAnimation, forKey: "loading") - } else { - self.imageNode.layer.removeAnimation(forKey: "loading") - } - } - } - } - - private func updateAnimation() { - self.isAnimating = !self.isHidden && self.currentInHierarchy - } - - override public func willEnterHierarchy() { - super.willEnterHierarchy() - - self.currentInHierarchy = true - self.updateAnimation() - } - - override public func didExitHierarchy() { - super.didExitHierarchy() - - self.currentInHierarchy = false - self.updateAnimation() - } - - override func layout() { - super.layout() - - self.imageNode.frame = self.bounds - } -} - -final class PeerInfoAvatarListContainerNode: ASDisplayNode { - private let context: AccountContext - var peer: Peer? - - 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 var activeStripNode: ASImageNode - private var loadingStripNode: PeerInfoAvatarListLoadingStripNode - private let activeStripImage: UIImage - private var appliedStripNodeCurrentIndex: Int? - var currentIndex: Int = 0 - private var transitionFraction: CGFloat = 0.0 - - private var validLayout: CGSize? - var isCollapsing = false - private var isExpanded = false - - private let disposable = MetaDisposable() - private let positionDisposable = MetaDisposable() - private var initializedList = false - private var ignoreNextProfilePhotoUpdate = false - var itemsUpdated: (([PeerInfoAvatarListItem]) -> Void)? - var currentIndexUpdated: (() -> Void)? - - 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 - } - } - - private var playerUpdateTimer: SwiftSignalKit.Timer? - private var playerStatus: (MediaPlayerStatus?, Double?)? { - didSet { - if self.playerStatus?.0 != oldValue?.0 || self.playerStatus?.1 != oldValue?.1 { - if let (playerStatus, _) = self.playerStatus, let status = playerStatus, case .playing = status.status { - self.ensureHasTimer() - } else { - self.stopTimer() - } - self.updateStatus() - } - } - } - - private func ensureHasTimer() { - if self.playerUpdateTimer == nil { - let timer = SwiftSignalKit.Timer(timeout: 0.016, repeat: true, completion: { [weak self] in - self?.updateStatus() - }, queue: Queue.mainQueue()) - self.playerUpdateTimer = timer - timer.start() - } - } - - private var playbackProgress: CGFloat? - private var loading: Bool = false - private func updateStatus() { - var position: CGFloat = 1.0 - var loading = false - if let (status, videoStartTimestamp) = self.playerStatus, let playerStatus = status { - var playerPosition: Double - if case .buffering = playerStatus.status { - loading = true - } - if !playerStatus.generationTimestamp.isZero, case .playing = playerStatus.status { - playerPosition = playerStatus.timestamp + (CACurrentMediaTime() - playerStatus.generationTimestamp) - } else { - playerPosition = playerStatus.timestamp - } - - if let videoStartTimestamp = videoStartTimestamp, false { - playerPosition -= videoStartTimestamp - if playerPosition < 0.0 { - playerPosition = playerStatus.duration + playerPosition - } - } - - if playerStatus.duration.isZero { - position = 0.0 - } else { - position = CGFloat(playerPosition / playerStatus.duration) - } - } else { - self.playbackProgress = nil - } - - if let size = self.validLayout { - self.playbackProgress = position - self.loading = loading - self.updateStrips(size: size, itemsAdded: false, stripTransition: .animated(duration: 0.3, curve: .spring)) - } - } - - private func stopTimer() { - self.playerUpdateTimer?.invalidate() - self.playerUpdateTimer = 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.activeStripNode = ASImageNode() - self.activeStripNode.image = self.activeStripImage - - self.loadingStripNode = PeerInfoAvatarListLoadingStripNode() - self.loadingStripNode.imageNode.image = self.activeStripImage - - 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() - self.positionDisposable.dispose() - } - - override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { - return super.hitTest(point, with: event) - } - - func selectFirstItem() { - let previousIndex = self.currentIndex - self.currentIndex = 0 - if self.currentIndex != previousIndex { - self.currentIndexUpdated?() - } - 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.firstIndex(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 { - let previousIndex = self.currentIndex - self.currentIndex -= 1 - if self.currentIndex != previousIndex { - self.currentIndexUpdated?() - } - self.updateItems(size: size, transition: .immediate, stripTransition: .animated(duration: 0.3, curve: .spring), synchronous: true) - } else if self.items.count > 1 { - let previousIndex = self.currentIndex - self.currentIndex = self.items.count - 1 - if self.currentIndex != previousIndex { - self.currentIndexUpdated?() - } - self.updateItems(size: size, transition: .immediate, stripTransition: .animated(duration: 0.3, curve: .spring), synchronous: true) - } - } else { - if self.currentIndex < self.items.count - 1 { - let previousIndex = self.currentIndex - self.currentIndex += 1 - if self.currentIndex != previousIndex { - self.currentIndexUpdated?() - } - self.updateItems(size: size, transition: .immediate, stripTransition: .animated(duration: 0.3, curve: .spring), synchronous: true) - } else if self.items.count > 1 { - let previousIndex = self.currentIndex - self.currentIndex = 0 - if self.currentIndex != previousIndex { - self.currentIndexUpdated?() - } - self.updateItems(size: size, transition: .immediate, stripTransition: .animated(duration: 0.3, curve: .spring), synchronous: true) - } - } - } - } - default: - break - } - } - - private var pageChangedByPan = false - @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: Bool? - if abs(velocity.x) > 10.0 { - directionIsToRight = velocity.x < 0.0 - } else if abs(self.transitionFraction) > 0.5 { - directionIsToRight = self.transitionFraction < 0.0 - } - var updatedIndex = self.currentIndex - if let directionIsToRight = directionIsToRight { - if directionIsToRight { - updatedIndex = min(updatedIndex + 1, self.items.count - 1) - } else { - updatedIndex = max(updatedIndex - 1, 0) - } - } - let previousIndex = self.currentIndex - self.currentIndex = updatedIndex - if self.currentIndex != previousIndex { - self.pageChangedByPan = true - self.currentIndexUpdated?() - } - 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)) - self.pageChangedByPan = false - } - default: - break - } - } - - func setMainItem(_ item: PeerInfoAvatarListItem) { - guard case let .image(image) = item else { - return - } - var items: [PeerInfoAvatarListItem] = [] - var entries: [AvatarGalleryEntry] = [] - for entry in self.galleryEntries { - switch entry { - case let .topImage(representations, videoRepresentations, _, _, immediateThumbnailData, _): - entries.append(entry) - items.append(.topImage(representations, videoRepresentations, immediateThumbnailData)) - case let .image(_, reference, representations, videoRepresentations, _, _, _, _, immediateThumbnailData, _): - if image.0 == reference { - entries.insert(entry, at: 0) - items.insert(.image(reference, representations, videoRepresentations, immediateThumbnailData), at: 0) - } else { - entries.append(entry) - items.append(.image(reference, representations, videoRepresentations, immediateThumbnailData)) - } - } - } - self.galleryEntries = normalizeEntries(entries) - self.items = items - self.itemsUpdated?(items) - let previousIndex = self.currentIndex - self.currentIndex = 0 - if self.currentIndex != previousIndex { - self.currentIndexUpdated?() - } - self.ignoreNextProfilePhotoUpdate = true - if let size = self.validLayout { - self.updateItems(size: size, update: true, transition: .immediate, stripTransition: .immediate, synchronous: true) - } - } - - func deleteItem(_ item: PeerInfoAvatarListItem) -> Bool { - guard case let .image(image) = item else { - return false - } - - var items: [PeerInfoAvatarListItem] = [] - var entries: [AvatarGalleryEntry] = [] - let previousIndex = self.currentIndex - - var index = 0 - var deletedIndex: Int? - for entry in self.galleryEntries { - switch entry { - case let .topImage(representations, videoRepresentations, _, _, immediateThumbnailData, _): - entries.append(entry) - items.append(.topImage(representations, videoRepresentations, immediateThumbnailData)) - case let .image(_, reference, representations, videoRepresentations, _, _, _, _, immediateThumbnailData, _): - if image.0 != reference { - entries.append(entry) - items.append(.image(reference, representations, videoRepresentations, immediateThumbnailData)) - } else { - deletedIndex = index - } - } - index += 1 - } - - - if let peer = self.peer, peer is TelegramGroup || peer is TelegramChannel, deletedIndex == 0 { - self.galleryEntries = [] - self.items = [] - self.itemsUpdated?([]) - self.currentIndex = 0 - if let size = self.validLayout { - self.updateItems(size: size, update: true, transition: .immediate, stripTransition: .immediate, synchronous: true) - } - return true - } - - self.galleryEntries = normalizeEntries(entries) - self.items = items - self.itemsUpdated?(items) - self.currentIndex = max(0, previousIndex - 1) - if self.currentIndex != previousIndex { - self.currentIndexUpdated?() - } - self.ignoreNextProfilePhotoUpdate = true - if let size = self.validLayout { - self.updateItems(size: size, update: true, transition: .immediate, stripTransition: .immediate, synchronous: true) - } - - return items.count == 0 - } - - func update(size: CGSize, peer: Peer?, isExpanded: Bool, transition: ContainedViewLayoutTransition) { - self.validLayout = size - let previousExpanded = self.isExpanded - self.isExpanded = isExpanded - if !isExpanded && previousExpanded { - self.isCollapsing = true - } - 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] (complete, entries) in - guard let strongSelf = self else { - return - } - - if strongSelf.galleryEntries.count > 1, entries.count == 1 && !complete { - return - } - - var entries = entries - var synchronous = false - if !strongSelf.galleryEntries.isEmpty, let updated = entries.first, case let .image(image) = updated, !image.3.isEmpty, let previous = strongSelf.galleryEntries.first, case let .topImage(topImage) = previous { - let firstEntry = AvatarGalleryEntry.image(image.0, image.1, topImage.0, image.3, image.4, image.5, image.6, image.7, image.8, image.9) - entries.remove(at: 0) - entries.insert(firstEntry, at: 0) - synchronous = true - } - - if strongSelf.ignoreNextProfilePhotoUpdate { - if entries.count == 1, let first = entries.first, case .topImage = first { - return - } else { - strongSelf.ignoreNextProfilePhotoUpdate = false - synchronous = true - } - } - - var items: [PeerInfoAvatarListItem] = [] - for entry in entries { - items.append(PeerInfoAvatarListItem(entry: entry)) - } - strongSelf.galleryEntries = entries - strongSelf.items = items - strongSelf.itemsUpdated?(items) - if let size = strongSelf.validLayout { - strongSelf.updateItems(size: size, update: true, transition: .immediate, stripTransition: .immediate, synchronous: synchronous) - } - if items.isEmpty { - if !strongSelf.didSetReady { - strongSelf.didSetReady = true - strongSelf.isReady.set(.single(true)) - } - } - })) - } - self.updateItems(size: size, transition: transition, stripTransition: transition) - } - - private func updateStrips(size: CGSize, itemsAdded: Bool, stripTransition: ContainedViewLayoutTransition) { - 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 - 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) - } - } - self.stripContainerNode.addSubnode(self.activeStripNode) - self.stripContainerNode.addSubnode(self.loadingStripNode) - } - if self.appliedStripNodeCurrentIndex != self.currentIndex || itemsAdded { - if !self.itemNodes.isEmpty { - self.appliedStripNodeCurrentIndex = self.currentIndex - } - - if let currentItemNode = self.currentItemNode { - self.positionDisposable.set((currentItemNode.mediaStatus - |> deliverOnMainQueue).start(next: { [weak self] statusAndVideoStartTimestamp in - if let strongSelf = self { - strongSelf.playerStatus = statusAndVideoStartTimestamp - } - })) - } else { - self.positionDisposable.set(nil) - } - } - 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 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 self.currentIndex >= 0 && self.currentIndex < self.stripNodes.count { - var frame = self.stripNodes[self.currentIndex].frame - stripTransition.updateFrame(node: self.loadingStripNode, frame: frame) - if let playbackProgress = self.playbackProgress { - frame.size.width = max(frame.size.height, frame.size.width * playbackProgress) - } - stripTransition.updateFrameAdditive(node: self.activeStripNode, frame: frame) - stripTransition.updateAlpha(node: self.activeStripNode, alpha: self.loading ? 0.0 : 1.0) - stripTransition.updateAlpha(node: self.loadingStripNode, alpha: self.loading ? 1.0 : 0.0) - - self.activeStripNode.isHidden = self.stripNodes.count < 2 - self.loadingStripNode.isHidden = self.stripNodes.count < 2 || !self.loading - } - } - - private func updateItems(size: CGSize, update: Bool = false, transition: ContainedViewLayoutTransition, stripTransition: ContainedViewLayoutTransition, synchronous: Bool = false) { - var validIds: [WrappedMediaResourceId] = [] - var addedItemNodesForAdditiveTransition: [PeerInfoAvatarListItemNode] = [] - var additiveTransitionOffset: CGFloat = 0.0 - var itemsAdded = false - if self.currentIndex >= 0 && self.currentIndex < self.items.count { - let preloadSpan: Int = 2 - for i in max(0, self.currentIndex - preloadSpan) ... min(self.currentIndex + preloadSpan, self.items.count - 1) { - validIds.append(self.items[i].id) - var itemNode: PeerInfoAvatarListItemNode? - var wasAdded = false - if let current = self.itemNodes[self.items[i].id] { - itemNode = current - if update { - current.setup(item: self.items[i], synchronous: synchronous && i == self.currentIndex) - } - } else if let peer = self.peer { - wasAdded = true - let addedItemNode = PeerInfoAvatarListItemNode(context: self.context, peer: peer) - itemNode = addedItemNode - addedItemNode.setup(item: self.items[i], synchronous: (i == 0 && i == self.currentIndex) || (synchronous && i == self.currentIndex)) - self.itemNodes[self.items[i].id] = addedItemNode - self.contentNode.addSubnode(addedItemNode) - } - if let itemNode = itemNode { - itemNode.delayCentralityLose = self.pageChangedByPan - itemNode.isCentral = i == self.currentIndex - itemNode.delayCentralityLose = false - - 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 { - itemsAdded = true - 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: .immediate) - } - } - } - } - 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() - } - } - - self.updateStrips(size: size, itemsAdded: itemsAdded, stripTransition: stripTransition) - - 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 @@ -1374,7 +365,7 @@ final class PeerInfoAvatarTransformContainerNode: ASDisplayNode { } var removedPhotoResourceIds = Set() - func update(peer: Peer?, item: PeerInfoAvatarListItem?, theme: PresentationTheme, avatarSize: CGFloat, isExpanded: Bool) { + func update(peer: Peer?, item: PeerInfoAvatarListItem?, theme: PresentationTheme, avatarSize: CGFloat, isExpanded: Bool, isSettings: Bool) { if let peer = peer { let previousItem = self.item var item = item @@ -1415,11 +406,16 @@ final class PeerInfoAvatarTransformContainerNode: ASDisplayNode { let immediateThumbnailData: Data? var id: Int64 switch item { + case .custom: + representations = [] + videoRepresentations = [] + immediateThumbnailData = nil + id = 0 case let .topImage(topRepresentations, videoRepresentationsValue, immediateThumbnail): representations = topRepresentations videoRepresentations = videoRepresentationsValue immediateThumbnailData = immediateThumbnail - id = Int64(peer.id.id) + id = Int64(peer.id.id._internalGetInt32Value()) if let resource = videoRepresentations.first?.representation.resource as? CloudPhotoSizeMediaResource { id = id &+ resource.photoId } @@ -1430,11 +426,11 @@ final class PeerInfoAvatarTransformContainerNode: ASDisplayNode { if case let .cloud(imageId, _, _) = reference { id = imageId } else { - id = Int64(peer.id.id) + id = Int64(peer.id.id._internalGetInt32Value()) } } - self.containerNode.isGestureEnabled = true + self.containerNode.isGestureEnabled = !isSettings if let video = videoRepresentations.last, let peerReference = PeerReference(peer) { let videoFileReference = FileMediaReference.avatarList(peer: peerReference, media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: video.representation.resource, previewRepresentations: representations.map { $0.representation }, videoThumbnails: [], immediateThumbnailData: immediateThumbnailData, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: video.representation.dimensions, flags: [])])) @@ -1705,11 +701,16 @@ final class PeerInfoEditingAvatarNode: ASDisplayNode { let immediateThumbnailData: Data? var id: Int64 switch item { + case .custom: + representations = [] + videoRepresentations = [] + immediateThumbnailData = nil + id = 0 case let .topImage(topRepresentations, videoRepresentationsValue, immediateThumbnail): representations = topRepresentations videoRepresentations = videoRepresentationsValue immediateThumbnailData = immediateThumbnail - id = Int64(peer.id.id) + id = Int64(peer.id.id._internalGetInt32Value()) if let resource = videoRepresentations.first?.representation.resource as? CloudPhotoSizeMediaResource { id = id &+ resource.photoId } @@ -1720,7 +721,7 @@ final class PeerInfoEditingAvatarNode: ASDisplayNode { if case let .cloud(imageId, _, _) = reference { id = imageId } else { - id = Int64(peer.id.id) + id = Int64(peer.id.id._internalGetInt32Value()) } } @@ -1780,6 +781,8 @@ final class PeerInfoEditingAvatarNode: ASDisplayNode { } final class PeerInfoAvatarListNode: ASDisplayNode { + private let isSettings: Bool + let pinchSourceNode: PinchSourceContainerNode let avatarContainerNode: PeerInfoAvatarTransformContainerNode let listContainerTransformNode: ASDisplayNode let listContainerNode: PeerInfoAvatarListContainerNode @@ -1790,8 +793,13 @@ final class PeerInfoAvatarListNode: ASDisplayNode { var item: PeerInfoAvatarListItem? var itemsUpdated: (([PeerInfoAvatarListItem]) -> Void)? + var animateOverlaysFadeIn: (() -> Void)? - init(context: AccountContext, readyWhenGalleryLoads: Bool) { + init(context: AccountContext, readyWhenGalleryLoads: Bool, isSettings: Bool) { + self.isSettings = isSettings + + self.pinchSourceNode = PinchSourceContainerNode() + self.avatarContainerNode = PeerInfoAvatarTransformContainerNode(context: context) self.listContainerTransformNode = ASDisplayNode() self.listContainerNode = PeerInfoAvatarListContainerNode(context: context) @@ -1799,10 +807,11 @@ final class PeerInfoAvatarListNode: ASDisplayNode { self.listContainerNode.isHidden = true super.init() - - self.addSubnode(self.avatarContainerNode) + + self.addSubnode(self.pinchSourceNode) + self.pinchSourceNode.contentNode.addSubnode(self.avatarContainerNode) self.listContainerTransformNode.addSubnode(self.listContainerNode) - self.addSubnode(self.listContainerTransformNode) + self.pinchSourceNode.contentNode.addSubnode(self.listContainerTransformNode) let avatarReady = (self.avatarContainerNode.avatarNode.ready |> mapToSignal { _ -> Signal in @@ -1840,15 +849,34 @@ final class PeerInfoAvatarListNode: ASDisplayNode { strongSelf.item = items.first strongSelf.itemsUpdated?(items) if let (peer, theme, avatarSize, isExpanded) = strongSelf.arguments { - strongSelf.avatarContainerNode.update(peer: peer, item: strongSelf.item, theme: theme, avatarSize: avatarSize, isExpanded: isExpanded) + strongSelf.avatarContainerNode.update(peer: peer, item: strongSelf.item, theme: theme, avatarSize: avatarSize, isExpanded: isExpanded, isSettings: strongSelf.isSettings) } } } + + self.pinchSourceNode.activate = { [weak self] sourceNode in + guard let _ = self else { + return + } + let pinchController = PinchController(sourceNode: sourceNode, getContentAreaInScreenSpace: { + return UIScreen.main.bounds + }) + context.sharedContext.mainWindow?.presentInGlobalOverlay(pinchController) + } + + self.pinchSourceNode.animatedOut = { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.animateOverlaysFadeIn?() + } } func update(size: CGSize, avatarSize: CGFloat, isExpanded: Bool, peer: Peer?, theme: PresentationTheme, transition: ContainedViewLayoutTransition) { self.arguments = (peer, theme, avatarSize, isExpanded) - self.avatarContainerNode.update(peer: peer, item: self.item, theme: theme, avatarSize: avatarSize, isExpanded: isExpanded) + self.pinchSourceNode.update(size: size, transition: transition) + self.pinchSourceNode.frame = CGRect(origin: CGPoint(), size: size) + self.avatarContainerNode.update(peer: peer, item: self.item, theme: theme, avatarSize: avatarSize, isExpanded: isExpanded, isSettings: self.isSettings) } override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { @@ -2627,10 +1655,11 @@ final class PeerInfoHeaderNode: ASDisplayNode { let usernameNodeRawContainer: ASDisplayNode let usernameNode: MultiScaleTextNode var buttonNodes: [PeerInfoHeaderButtonKey: PeerInfoHeaderButtonNode] = [:] - private let backgroundNode: ASDisplayNode - private let expandedBackgroundNode: ASDisplayNode + private let backgroundNode: NavigationBackgroundNode + private let expandedBackgroundNode: NavigationBackgroundNode let separatorNode: ASDisplayNode let navigationBackgroundNode: ASDisplayNode + let navigationBackgroundBackgroundNode: ASDisplayNode var navigationTitle: String? let navigationTitleNode: ImmediateTextNode let navigationSeparatorNode: ASDisplayNode @@ -2641,6 +1670,7 @@ final class PeerInfoHeaderNode: ASDisplayNode { var requestOpenAvatarForEditing: ((Bool) -> Void)? var cancelUpload: (() -> Void)? var requestUpdateLayout: (() -> Void)? + var animateOverlaysFadeIn: (() -> Void)? var displayAvatarContextMenu: ((ASDisplayNode, ContextGesture?) -> Void)? var displayCopyContextMenu: ((ASDisplayNode, Bool, Bool) -> Void)? @@ -2654,7 +1684,7 @@ final class PeerInfoHeaderNode: ASDisplayNode { self.isSettings = isSettings self.videoCallsEnabled = VideoCallsConfiguration(appConfiguration: context.currentAppConfiguration.with { $0 }).areVideoCallsEnabled - self.avatarListNode = PeerInfoAvatarListNode(context: context, readyWhenGalleryLoads: avatarInitiallyExpanded) + self.avatarListNode = PeerInfoAvatarListNode(context: context, readyWhenGalleryLoads: avatarInitiallyExpanded, isSettings: isSettings) self.titleNodeContainer = ASDisplayNode() self.titleNodeRawContainer = ASDisplayNode() @@ -2692,7 +1722,11 @@ final class PeerInfoHeaderNode: ASDisplayNode { self.avatarOverlayNode.isUserInteractionEnabled = false self.navigationBackgroundNode = ASDisplayNode() + self.navigationBackgroundNode.isHidden = true self.navigationBackgroundNode.isUserInteractionEnabled = false + + self.navigationBackgroundBackgroundNode = ASDisplayNode() + self.navigationBackgroundBackgroundNode.isUserInteractionEnabled = false self.navigationTitleNode = ImmediateTextNode() @@ -2700,10 +1734,12 @@ final class PeerInfoHeaderNode: ASDisplayNode { self.navigationButtonContainer = PeerInfoHeaderNavigationButtonContainerNode() - self.backgroundNode = ASDisplayNode() - self.backgroundNode.isLayerBacked = true - self.expandedBackgroundNode = ASDisplayNode() - self.expandedBackgroundNode.isLayerBacked = true + self.backgroundNode = NavigationBackgroundNode(color: .clear) + self.backgroundNode.isHidden = true + self.backgroundNode.isUserInteractionEnabled = false + self.expandedBackgroundNode = NavigationBackgroundNode(color: .clear) + self.expandedBackgroundNode.isHidden = false + self.expandedBackgroundNode.isUserInteractionEnabled = false self.separatorNode = ASDisplayNode() self.separatorNode.isLayerBacked = true @@ -2730,6 +1766,7 @@ final class PeerInfoHeaderNode: ASDisplayNode { self.addSubnode(self.editingContentNode) self.addSubnode(self.avatarOverlayNode) self.addSubnode(self.navigationBackgroundNode) + self.navigationBackgroundNode.addSubnode(self.navigationBackgroundBackgroundNode) self.navigationBackgroundNode.addSubnode(self.navigationTitleNode) self.navigationBackgroundNode.addSubnode(self.navigationSeparatorNode) self.addSubnode(self.navigationButtonContainer) @@ -2755,6 +1792,17 @@ final class PeerInfoHeaderNode: ASDisplayNode { } strongSelf.editingContentNode.avatarNode.update(peer: peer, item: strongSelf.avatarListNode.item, updatingAvatar: state.updatingAvatar, uploadProgress: state.avatarUploadProgress, theme: presentationData.theme, avatarSize: avatarSize, isEditing: state.isEditing) } + + self.avatarListNode.animateOverlaysFadeIn = { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.navigationButtonContainer.layer.animateAlpha(from: 0.0, to: strongSelf.navigationButtonContainer.alpha, duration: 0.25) + strongSelf.avatarListNode.listContainerNode.shadowNode.layer.animateAlpha(from: 0.0, to: strongSelf.avatarListNode.listContainerNode.shadowNode.alpha, duration: 0.25) + strongSelf.avatarListNode.listContainerNode.controlsContainerNode.layer.animateAlpha(from: 0.0, to: strongSelf.avatarListNode.listContainerNode.controlsContainerNode.alpha, duration: 0.25) + + strongSelf.animateOverlaysFadeIn?() + } } override func didLoad() { @@ -2903,24 +1951,24 @@ final class PeerInfoHeaderNode: ASDisplayNode { var transitionSourceTitleFrame = CGRect() var transitionSourceSubtitleFrame = CGRect() - self.backgroundNode.backgroundColor = presentationData.theme.list.itemBlocksBackgroundColor - self.expandedBackgroundNode.backgroundColor = presentationData.theme.rootController.navigationBar.backgroundColor + self.backgroundNode.updateColor(color: presentationData.theme.rootController.navigationBar.blurredBackgroundColor, transition: .immediate) if let navigationTransition = self.navigationTransition, let sourceAvatarNode = (navigationTransition.sourceNavigationBar.rightButtonNode.singleCustomNode as? ChatAvatarNavigationNode)?.avatarNode { - transitionSourceHeight = navigationTransition.sourceNavigationBar.bounds.height + transitionSourceHeight = navigationTransition.sourceNavigationBar.backgroundNode.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) + + self.expandedBackgroundNode.updateColor(color: presentationData.theme.rootController.navigationBar.blurredBackgroundColor.mixedWith(presentationData.theme.list.itemBlocksBackgroundColor, alpha: 1.0 - transitionFraction), forceKeepBlur: true, transition: transition) 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 / (112.0 + avatarSize))) - transition.updateAlpha(node: self.expandedBackgroundNode, alpha: backgroundTransitionFraction) + + self.expandedBackgroundNode.updateColor(color: presentationData.theme.rootController.navigationBar.opaqueBackgroundColor.mixedWith(presentationData.theme.list.itemBlocksBackgroundColor, alpha: 1.0 - backgroundTransitionFraction), forceKeepBlur: true, transition: transition) } self.avatarListNode.avatarContainerNode.updateTransitionFraction(transitionFraction, transition: transition) @@ -2928,17 +1976,22 @@ final class PeerInfoHeaderNode: ASDisplayNode { self.avatarOverlayNode.updateTransitionFraction(transitionFraction, transition: transition) if self.navigationTitle != presentationData.strings.EditProfile_Title || themeUpdated { - self.navigationTitleNode.attributedText = NSAttributedString(string: presentationData.strings.EditProfile_Title, font: Font.bold(17.0), textColor: presentationData.theme.rootController.navigationBar.primaryTextColor) + self.navigationTitleNode.attributedText = NSAttributedString(string: presentationData.strings.EditProfile_Title, font: Font.semibold(17.0), textColor: presentationData.theme.rootController.navigationBar.primaryTextColor) } let navigationTitleSize = self.navigationTitleNode.updateLayout(CGSize(width: width, height: navigationHeight)) self.navigationTitleNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((width - navigationTitleSize.width) / 2.0), y: navigationHeight - 44.0 + floorToScreenPixels((44.0 - navigationTitleSize.height) / 2.0)), size: navigationTitleSize) self.navigationBackgroundNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: width, height: navigationHeight)) + self.navigationBackgroundBackgroundNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: width, height: navigationHeight)) self.navigationSeparatorNode.frame = CGRect(origin: CGPoint(x: 0.0, y: navigationHeight), size: CGSize(width: width, height: UIScreenPixel)) - self.navigationBackgroundNode.backgroundColor = presentationData.theme.rootController.navigationBar.backgroundColor + self.navigationBackgroundBackgroundNode.backgroundColor = presentationData.theme.rootController.navigationBar.opaqueBackgroundColor self.navigationSeparatorNode.backgroundColor = presentationData.theme.rootController.navigationBar.separatorColor - transition.updateAlpha(node: self.navigationBackgroundNode, alpha: state.isEditing && self.isSettings ? min(1.0, contentOffset / (navigationHeight * 0.5)) : 0.0) + + let separatorAlpha: CGFloat = state.isEditing && self.isSettings ? min(1.0, contentOffset / (navigationHeight * 0.5)) : 0.0 + transition.updateAlpha(node: self.navigationBackgroundBackgroundNode, alpha: 1.0 - separatorAlpha) + transition.updateAlpha(node: self.navigationSeparatorNode, alpha: separatorAlpha) + self.separatorNode.backgroundColor = presentationData.theme.list.itemBlocksSeparatorColor let defaultButtonSize: CGFloat = 40.0 @@ -2946,7 +1999,7 @@ final class PeerInfoHeaderNode: ASDisplayNode { let expandedAvatarListHeight = min(width, containerHeight - expandedAvatarControlsHeight) let expandedAvatarListSize = CGSize(width: width, height: expandedAvatarListHeight) - let buttonKeys: [PeerInfoHeaderButtonKey] = self.isSettings ? [] : peerInfoHeaderButtons(peer: peer, cachedData: cachedData, isOpenedFromChat: self.isOpenedFromChat, videoCallsEnabled: self.videoCallsEnabled, isSecretChat: isSecretChat, isContact: isContact) + let buttonKeys: [PeerInfoHeaderButtonKey] = self.isSettings ? [] : peerInfoHeaderButtons(peer: peer, cachedData: cachedData, isOpenedFromChat: self.isOpenedFromChat, isExpanded: false, videoCallsEnabled: self.videoCallsEnabled, isSecretChat: isSecretChat, isContact: isContact) var isVerified = false let titleString: NSAttributedString @@ -2958,11 +2011,11 @@ final class PeerInfoHeaderNode: ASDisplayNode { if let peer = peer { if peer.id == self.context.account.peerId && !self.isSettings { - titleString = NSAttributedString(string: presentationData.strings.Conversation_SavedMessages, font: Font.medium(24.0), textColor: presentationData.theme.list.itemPrimaryTextColor) + titleString = NSAttributedString(string: presentationData.strings.Conversation_SavedMessages, font: Font.semibold(24.0), textColor: presentationData.theme.list.itemPrimaryTextColor) } else if peer.id == self.context.account.peerId && !self.isSettings { - titleString = NSAttributedString(string: presentationData.strings.DialogList_Replies, font: Font.medium(24.0), textColor: presentationData.theme.list.itemPrimaryTextColor) + titleString = NSAttributedString(string: presentationData.strings.DialogList_Replies, font: Font.semibold(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) + titleString = NSAttributedString(string: peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), font: Font.semibold(24.0), textColor: presentationData.theme.list.itemPrimaryTextColor) } if self.isSettings, let user = peer as? TelegramUser { @@ -2988,7 +2041,7 @@ final class PeerInfoHeaderNode: ASDisplayNode { usernameString = 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) + titleString = NSAttributedString(string: " ", font: Font.semibold(24.0), textColor: presentationData.theme.list.itemPrimaryTextColor) subtitleString = NSAttributedString(string: " ", font: Font.regular(15.0), textColor: presentationData.theme.list.itemSecondaryTextColor) usernameString = NSAttributedString(string: "", font: Font.regular(15.0), textColor: presentationData.theme.list.itemSecondaryTextColor) } @@ -3416,21 +2469,7 @@ final class PeerInfoHeaderNode: ASDisplayNode { let hiddenWhileExpanded: Bool if buttonKeys.count > 3 { - if self.isOpenedFromChat { - switch buttonKey { - case .message, .search, .mute: - hiddenWhileExpanded = true - default: - hiddenWhileExpanded = false - } - } else { - switch buttonKey { - case .mute, .search, .videoCall: - hiddenWhileExpanded = true - default: - hiddenWhileExpanded = false - } - } + hiddenWhileExpanded = peerInfoHeaderButtonIsHiddenWhileExpanded(buttonKey: buttonKey, isOpenedFromChat: self.isOpenedFromChat) } else { hiddenWhileExpanded = false } @@ -3491,11 +2530,15 @@ final class PeerInfoHeaderNode: ASDisplayNode { if additive { transition.updateFrameAdditive(node: self.backgroundNode, frame: backgroundFrame) + self.backgroundNode.update(size: self.backgroundNode.bounds.size, transition: transition) transition.updateFrameAdditive(node: self.expandedBackgroundNode, frame: backgroundFrame) + self.expandedBackgroundNode.update(size: self.expandedBackgroundNode.bounds.size, transition: transition) transition.updateFrameAdditive(node: self.separatorNode, frame: separatorFrame) } else { transition.updateFrame(node: self.backgroundNode, frame: backgroundFrame) + self.backgroundNode.update(size: self.backgroundNode.bounds.size, transition: transition) transition.updateFrame(node: self.expandedBackgroundNode, frame: backgroundFrame) + self.expandedBackgroundNode.update(size: self.expandedBackgroundNode.bounds.size, transition: transition) transition.updateFrame(node: self.separatorNode, frame: separatorFrame) } diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoMembers.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoMembers.swift index a8c0ba2799..d2caec111c 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoMembers.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoMembers.swift @@ -147,7 +147,7 @@ private final class PeerInfoMembersContextImpl { 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 + let (disposable, control) = context.peerChannelMemberCategoriesContextsManager.recent(engine: context.engine, 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 @@ -230,10 +230,10 @@ private final class PeerInfoMembersContextImpl { 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)) + signal = context.peerChannelMemberCategoriesContextsManager.updateMemberBannedRights(engine: self.context.engine, 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) + signal = self.context.engine.peers.removePeerMember(peerId: self.peerId, memberId: memberId) |> ignoreValues } let completed: () -> Void = { [weak self] in diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoPaneContainerNode.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoPaneContainerNode.swift index 5a1e4ce3d1..22b50b5d73 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoPaneContainerNode.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoPaneContainerNode.swift @@ -24,6 +24,7 @@ protocol PeerInfoPaneNode: ASDisplayNode { func addToTransitionSurface(view: UIView) func updateHiddenMedia() func updateSelectedMessages(animated: Bool) + func ensureMessageIsVisible(id: MessageId) } final class PeerInfoPaneWrapper { @@ -432,7 +433,7 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, UIGestureRecognizerDelegat weak var parentController: ViewController? - private let coveringBackgroundNode: ASDisplayNode + private let coveringBackgroundNode: NavigationBackgroundNode private let separatorNode: ASDisplayNode private let tabsContainerNode: PeerInfoPaneTabsContainerNode private let tabsSeparatorNode: ASDisplayNode @@ -476,8 +477,8 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, UIGestureRecognizerDelegat self.separatorNode = ASDisplayNode() self.separatorNode.isLayerBacked = true - self.coveringBackgroundNode = ASDisplayNode() - self.coveringBackgroundNode.isLayerBacked = true + self.coveringBackgroundNode = NavigationBackgroundNode(color: .clear) + self.coveringBackgroundNode.isUserInteractionEnabled = false self.tabsContainerNode = PeerInfoPaneTabsContainerNode() @@ -691,7 +692,7 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, UIGestureRecognizerDelegat transition.updateAlpha(node: self.coveringBackgroundNode, alpha: expansionFraction) self.backgroundColor = presentationData.theme.list.itemBlocksBackgroundColor - self.coveringBackgroundNode.backgroundColor = presentationData.theme.rootController.navigationBar.backgroundColor + self.coveringBackgroundNode.updateColor(color: presentationData.theme.rootController.navigationBar.opaqueBackgroundColor, transition: .immediate) self.separatorNode.backgroundColor = presentationData.theme.list.itemBlocksSeparatorColor self.tabsSeparatorNode.backgroundColor = presentationData.theme.list.itemBlocksSeparatorColor @@ -699,6 +700,7 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, UIGestureRecognizerDelegat 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))) + self.coveringBackgroundNode.update(size: self.coveringBackgroundNode.bounds.size, transition: transition) transition.updateFrame(node: self.tabsSeparatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: tabsHeight - UIScreenPixel), size: CGSize(width: size.width, height: UIScreenPixel))) diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift index 44d6c182a4..2ab5172acb 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift @@ -59,6 +59,8 @@ import MediaResources import HashtagSearchUI import ActionSheetPeerItem import TelegramCallsUI +import PeerInfoAvatarListNode +import PasswordSetupUI protocol PeerInfoScreenItem: class { var id: AnyHashable { get } @@ -345,7 +347,7 @@ final class PeerInfoSelectionPanelNode: ASDisplayNode { let selectionPanel: ChatMessageSelectionInputPanelNode let separatorNode: ASDisplayNode - let backgroundNode: ASDisplayNode + let backgroundNode: NavigationBackgroundNode init(context: AccountContext, peerId: PeerId, deleteMessages: @escaping () -> Void, shareMessages: @escaping () -> Void, forwardMessages: @escaping () -> Void, reportMessages: @escaping () -> Void) { self.context = context @@ -358,11 +360,10 @@ final class PeerInfoSelectionPanelNode: ASDisplayNode { let presentationData = context.sharedContext.currentPresentationData.with { $0 } self.separatorNode = ASDisplayNode() - self.backgroundNode = ASDisplayNode() + self.backgroundNode = NavigationBackgroundNode(color: presentationData.theme.rootController.navigationBar.blurredBackgroundColor) 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 @@ -412,7 +413,7 @@ final class PeerInfoSelectionPanelNode: ASDisplayNode { }, displayVideoUnmuteTip: { _ in }, switchMediaRecordingMode: { }, setupMessageAutoremoveTimeout: { - }, sendSticker: { _, _, _ in + }, sendSticker: { _, _, _, _ in return false }, unblockPeer: { }, pinMessage: { _, _ in @@ -452,7 +453,7 @@ final class PeerInfoSelectionPanelNode: ASDisplayNode { }, presentInviteMembers: { }, presentGigagroupHelp: { }, editMessageMedia: { _, _ in - }, statuses: nil) + }, updateShowCommands: { _ in }, statuses: nil) self.selectionPanel.interfaceInteraction = interfaceInteraction @@ -464,7 +465,7 @@ final class PeerInfoSelectionPanelNode: ASDisplayNode { } func update(layout: ContainerViewLayout, presentationData: PresentationData, transition: ContainedViewLayoutTransition) -> CGFloat { - self.backgroundNode.backgroundColor = presentationData.theme.rootController.navigationBar.backgroundColor + self.backgroundNode.updateColor(color: presentationData.theme.rootController.navigationBar.blurredBackgroundColor, transition: .immediate) 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), subject: nil, peerNearbyData: nil, greetingData: nil, pendingUnpinnedAllMessages: false, activeGroupCallInfo: nil, hasActiveGroupCall: false, importState: nil) @@ -475,6 +476,7 @@ final class PeerInfoSelectionPanelNode: ASDisplayNode { let panelHeightWithInset = panelHeight + layout.intrinsicInsets.bottom transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: layout.size.width, height: panelHeightWithInset))) + self.backgroundNode.update(size: self.backgroundNode.bounds.size, transition: transition) transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: layout.size.width, height: UIScreenPixel))) return panelHeightWithInset @@ -523,10 +525,12 @@ private enum PeerInfoSettingsSection { case watch case support case faq + case tips case phoneNumber case username case addAccount case logout + case rememberPassword } private final class PeerInfoInteraction { @@ -566,6 +570,7 @@ private final class PeerInfoInteraction { let accountContextMenu: (AccountRecordId, ASDisplayNode, ContextGesture?) -> Void let updateBio: (String) -> Void let openDeletePeer: () -> Void + let openFaq: (String?) -> Void init( openUsername: @escaping (String) -> Void, @@ -603,7 +608,8 @@ private final class PeerInfoInteraction { logoutAccount: @escaping (AccountRecordId) -> Void, accountContextMenu: @escaping (AccountRecordId, ASDisplayNode, ContextGesture?) -> Void, updateBio: @escaping (String) -> Void, - openDeletePeer: @escaping () -> Void + openDeletePeer: @escaping () -> Void, + openFaq: @escaping (String?) -> Void ) { self.openUsername = openUsername self.openPhone = openPhone @@ -641,6 +647,7 @@ private final class PeerInfoInteraction { self.accountContextMenu = accountContextMenu self.updateBio = updateBio self.openDeletePeer = openDeletePeer + self.openFaq = openFaq } } @@ -690,16 +697,26 @@ private func settingsItems(data: PeerInfoScreenData?, context: AccountContext, p if let settings = data.globalSettings { if settings.suggestPhoneNumberConfirmation, let peer = data.peer as? TelegramUser { - // - // entries.append(.phoneInfo(presentationData.theme, presentationData.strings.Settings_CheckPhoneNumberTitle(phoneNumber).0, presentationData.strings.Settings_CheckPhoneNumberText)) - // entries.append(.keepPhone(presentationData.theme, presentationData.strings.Settings_KeepPhoneNumber(phoneNumber).0)) - // entries.append(.changePhone(presentationData.theme, presentationData.strings.Settings_ChangePhoneNumber)) let phoneNumber = formatPhoneNumber(peer.phone ?? "") - items[.phone]!.append(PeerInfoScreenActionItem(id: 2, text: presentationData.strings.Settings_KeepPhoneNumber(phoneNumber).0, action: { - interaction.openSettings(.addAccount) + items[.phone]!.append(PeerInfoScreenInfoItem(id: 0, title: presentationData.strings.Settings_CheckPhoneNumberTitle(phoneNumber).0, text: .markdown(presentationData.strings.Settings_CheckPhoneNumberText), linkAction: { link in + if case .tap = link { + interaction.openFaq("q-i-have-a-new-phone-number-what-do-i-do") + } + })) + items[.phone]!.append(PeerInfoScreenActionItem(id: 1, text: presentationData.strings.Settings_KeepPhoneNumber(phoneNumber).0, action: { + let _ = dismissServerProvidedSuggestion(account: context.account, suggestion: .validatePhoneNumber).start() })) items[.phone]!.append(PeerInfoScreenActionItem(id: 2, text: presentationData.strings.Settings_ChangePhoneNumber, action: { - interaction.openSettings(.addAccount) + interaction.openSettings(.phoneNumber) + })) + } else if settings.suggestPasswordConfirmation { + items[.phone]!.append(PeerInfoScreenInfoItem(id: 0, title: presentationData.strings.Settings_CheckPasswordTitle, text: .markdown(presentationData.strings.Settings_CheckPasswordText), linkAction: { _ in + })) + items[.phone]!.append(PeerInfoScreenActionItem(id: 1, text: presentationData.strings.Settings_KeepPassword, action: { + let _ = dismissServerProvidedSuggestion(account: context.account, suggestion: .validatePassword).start() + })) + items[.phone]!.append(PeerInfoScreenActionItem(id: 2, text: presentationData.strings.Settings_TryEnterPassword, action: { + interaction.openSettings(.rememberPassword) })) } @@ -822,6 +839,9 @@ private func settingsItems(data: PeerInfoScreenData?, context: AccountContext, p items[.support]!.append(PeerInfoScreenDisclosureItem(id: 1, text: presentationData.strings.Settings_FAQ, icon: PresentationResourcesSettings.faq, action: { interaction.openSettings(.faq) })) + items[.support]!.append(PeerInfoScreenDisclosureItem(id: 2, text: presentationData.strings.Settings_Tips, icon: PresentationResourcesSettings.tips, action: { + interaction.openSettings(.tips) + })) var result: [(AnyHashable, [PeerInfoScreenItem])] = [] for section in SettingsSection.allCases { @@ -1216,7 +1236,7 @@ private func editingItems(data: PeerInfoScreenData?, context: AccountContext, pr })) } - if channel.flags.contains(.isCreator) || (channel.adminRights != nil && channel.hasPermission(.pinMessages)) { + if channel.flags.contains(.isCreator) || (channel.adminRights != nil && channel.hasPermission(.sendMessages)) { let messagesShouldHaveSignatures: Bool switch channel.info { case let .broadcast(info): @@ -1284,7 +1304,7 @@ private func editingItems(data: PeerInfoScreenData?, context: AccountContext, pr } } else { if cachedData.flags.contains(.canChangeUsername) { - items[.peerPublicSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemUsername, label: .text(isPublic ? presentationData.strings.Channel_Setup_TypePublic : presentationData.strings.Channel_Setup_TypePrivate), text: presentationData.strings.GroupInfo_GroupType, icon: UIImage(bundleImageName: "Chat/Info/GroupTypeIcon"), action: { + items[.peerPublicSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemUsername, label: .text(isPublic ? presentationData.strings.Group_Setup_TypePublic : presentationData.strings.Group_Setup_TypePrivate), text: presentationData.strings.GroupInfo_GroupType, icon: UIImage(bundleImageName: "Chat/Info/GroupTypeIcon"), action: { interaction.editingOpenPublicLinkSetup() })) } @@ -1520,6 +1540,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD private let selectAddMemberDisposable = MetaDisposable() private let addMemberDisposable = MetaDisposable() private let preloadHistoryDisposable = MetaDisposable() + private var shareStatusDisposable: MetaDisposable? private let editAvatarDisposable = MetaDisposable() private let updateAvatarDisposable = MetaDisposable() @@ -1527,9 +1548,6 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD private var groupMembersSearchContext: GroupMembersSearchContext? - private let preloadedSticker = Promise(nil) - private let preloadStickerDisposable = MetaDisposable() - private let displayAsPeersPromise = Promise<[FoundPeer]>([]) fileprivate let accountsAndPeers = Promise<[(Account, Peer, Int32)]>() @@ -1541,6 +1559,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD private let hasTwoStepAuth = Promise(nil) private let hasPassport = Promise(false) private let supportPeerDisposable = MetaDisposable() + private let tipsPeerDisposable = MetaDisposable() private let cachedFaq = Promise(nil) private let _ready = Promise() @@ -1680,6 +1699,9 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD }, openDeletePeer: { [weak self] in self?.openDeletePeer() + }, + openFaq: { [weak self] anchor in + self?.openFaq(anchor: anchor) } ) @@ -1725,7 +1747,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD c.dismiss(completion: { if let strongSelf = self, let navigationController = strongSelf.controller?.navigationController as? NavigationController { let currentPeerId = strongSelf.peerId - strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(currentPeerId), subject: .message(id: message.id, highlight: true), keepStack: .always, useExisting: false, purposefulAction: { + strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(currentPeerId), subject: .message(id: message.id, highlight: true, timecode: nil), keepStack: .always, useExisting: false, purposefulAction: { var viewControllers = navigationController.viewControllers var indexesToRemove = Set() var keptCurrentChatController = false @@ -1801,7 +1823,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD c.dismiss(completion: { if let strongSelf = self { strongSelf.headerNode.navigationButtonContainer.performAction?(.selectionDone) - let _ = deleteMessagesInteractively(account: strongSelf.context.account, messageIds: Array(messageIds), type: .forEveryone).start() + let _ = strongSelf.context.engine.messages.deleteMessagesInteractively(messageIds: Array(messageIds), type: .forEveryone).start() } }) }))) @@ -1820,7 +1842,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD c.dismiss(completion: { if let strongSelf = self { strongSelf.headerNode.navigationButtonContainer.performAction?(.selectionDone) - let _ = deleteMessagesInteractively(account: strongSelf.context.account, messageIds: Array(messageIds), type: .forLocalPeer).start() + let _ = strongSelf.context.engine.messages.deleteMessagesInteractively(messageIds: Array(messageIds), type: .forLocalPeer).start() } }) }))) @@ -1847,6 +1869,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD let controller = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .extracted(MessageContextExtractedContentSource(sourceNode: node)), items: .single(items), reactionItems: [], recognizer: nil, gesture: gesture) strongSelf.controller?.window?.presentInGlobalOverlay(controller) }) + }, activateMessagePinch: { _ in }, openMessageContextActions: { [weak self] message, node, rect, gesture in guard let strongSelf = self else { gesture?.cancel() @@ -1870,7 +1893,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD c.dismiss(completion: { if let strongSelf = self, let navigationController = strongSelf.controller?.navigationController as? NavigationController { let currentPeerId = strongSelf.peerId - strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(currentPeerId), subject: .message(id: message.id, highlight: true), keepStack: .always, useExisting: false, purposefulAction: { + strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(currentPeerId), subject: .message(id: message.id, highlight: true, timecode: nil), keepStack: .always, useExisting: false, purposefulAction: { var viewControllers = navigationController.viewControllers var indexesToRemove = Set() var keptCurrentChatController = false @@ -1934,7 +1957,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD c.dismiss(completion: { if let strongSelf = self { strongSelf.headerNode.navigationButtonContainer.performAction?(.selectionDone) - let _ = deleteMessagesInteractively(account: strongSelf.context.account, messageIds: Array(messageIds), type: .forEveryone).start() + let _ = strongSelf.context.engine.messages.deleteMessagesInteractively(messageIds: Array(messageIds), type: .forEveryone).start() } }) }))) @@ -1953,7 +1976,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD c.dismiss(completion: { if let strongSelf = self { strongSelf.headerNode.navigationButtonContainer.performAction?(.selectionDone) - let _ = deleteMessagesInteractively(account: strongSelf.context.account, messageIds: Array(messageIds), type: .forLocalPeer).start() + let _ = strongSelf.context.engine.messages.deleteMessagesInteractively(messageIds: Array(messageIds), type: .forLocalPeer).start() } }) }))) @@ -2016,11 +2039,11 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD strongSelf.paneContainerNode.updateSelectedMessageIds(strongSelf.state.selectedMessageIds, animated: true) }, sendCurrentMessage: { _ in }, sendMessage: { _ in - }, sendSticker: { _, _, _, _, _ in + }, sendSticker: { _, _, _, _, _, _, _ in return false - }, sendGif: { _, _, _ in + }, sendGif: { _, _, _, _, _ in return false - }, sendBotContextResultAsGif: { _, _, _, _ in + }, sendBotContextResultAsGif: { _, _, _, _, _ in return false }, requestMessageActionCallback: { _, _, _, _ in }, requestMessageActionUrlAuth: { _, _ in @@ -2148,8 +2171,6 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD }, displayPsa: { _, _ in }, displayDiceTooltip: { _ in }, animateDiceSuccess: { _ in - }, greetingStickerNode: { - return nil }, openPeerContextMenu: { _, _, _, _, _ in }, openMessageReplies: { _, _, _ in }, openReplyThreadOriginalMessage: { _ in @@ -2157,10 +2178,12 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD }, editMessageMedia: { _, _ in }, copyText: { _ in }, displayUndo: { _ in + }, isAnimatingMessage: { _ in + return false }, requestMessageUpdate: { _ in }, cancelInteractiveKeyboardGestures: { }, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings, - pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(loopAnimatedStickers: false)) + pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(loopAnimatedStickers: false), presentationContext: ChatPresentationContext(backgroundNode: nil)) self.hiddenMediaDisposable = context.sharedContext.mediaManager.galleryHiddenMediaManager.hiddenIds().start(next: { [weak self] ids in guard let strongSelf = self else { return @@ -2331,7 +2354,9 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD self?.updateProfileVideo(image, asset: asset, adjustments: adjustments) } galleryController.removedEntry = { [weak self] entry in - let _ = self?.headerNode.avatarListNode.listContainerNode.deleteItem(PeerInfoAvatarListItem(entry: entry)) + if let item = PeerInfoAvatarListItem(entry: entry) { + let _ = self?.headerNode.avatarListNode.listContainerNode.deleteItem(item) + } } strongSelf.hiddenAvatarRepresentationDisposable.set((galleryController.hiddenMedia |> deliverOnMainQueue).start(next: { entry in self?.headerNode.updateAvatarIsHidden(entry: entry) @@ -2387,6 +2412,13 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD strongSelf.openAvatarForEditing() } } + + self.headerNode.animateOverlaysFadeIn = { [weak self] in + guard let strongSelf = self, let navigationBar = strongSelf.controller?.navigationBar else { + return + } + navigationBar.layer.animateAlpha(from: 0.0, to: navigationBar.alpha, duration: 0.25) + } self.headerNode.requestUpdateLayout = { [weak self] in guard let strongSelf = self else { @@ -2451,12 +2483,12 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD var updateNameSignal: Signal = .complete() var hasProgress = false if peer.firstName != firstName || peer.lastName != lastName { - updateNameSignal = updateAccountPeerName(account: context.account, firstName: firstName, lastName: lastName) + updateNameSignal = context.engine.accountData.updateAccountPeerName(firstName: firstName, lastName: lastName) hasProgress = true } var updateBioSignal: Signal = .complete() if let bio = bio, bio != cachedData.about { - updateBioSignal = updateAbout(account: context.account, about: bio) + updateBioSignal = context.engine.accountData.updateAbout(about: bio) |> `catch` { _ -> Signal in return .complete() } @@ -2515,7 +2547,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD } strongSelf.controller?.present(statusController, in: .window(.root)) - strongSelf.activeActionDisposable.set((updateContactName(account: context.account, peerId: peer.id, firstName: firstName, lastName: lastName) + strongSelf.activeActionDisposable.set((context.engine.contacts.updateContactName(peerId: peer.id, firstName: firstName, lastName: lastName) |> deliverOnMainQueue).start(error: { _ in dismissStatus?() @@ -2577,7 +2609,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD var hasProgress = false if title != group.title { updateDataSignals.append( - updatePeerTitle(account: strongSelf.context.account, peerId: group.id, title: title) + strongSelf.context.engine.peers.updatePeerTitle(peerId: group.id, title: title) |> ignoreValues |> mapError { _ in return Void() } ) @@ -2585,7 +2617,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD } if description != (data.cachedData as? CachedGroupData)?.about { updateDataSignals.append( - updatePeerDescription(account: strongSelf.context.account, peerId: group.id, description: description.isEmpty ? nil : description) + strongSelf.context.engine.peers.updatePeerDescription(peerId: group.id, description: description.isEmpty ? nil : description) |> ignoreValues |> mapError { _ in return Void() } ) @@ -2635,7 +2667,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD var hasProgress = false if title != channel.title { updateDataSignals.append( - updatePeerTitle(account: strongSelf.context.account, peerId: channel.id, title: title) + strongSelf.context.engine.peers.updatePeerTitle(peerId: channel.id, title: title) |> ignoreValues |> mapError { _ in return Void() } ) @@ -2643,7 +2675,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD } if description != (data.cachedData as? CachedChannelData)?.about { updateDataSignals.append( - updatePeerDescription(account: strongSelf.context.account, peerId: channel.id, description: description.isEmpty ? nil : description) + strongSelf.context.engine.peers.updatePeerDescription(peerId: channel.id, description: description.isEmpty ? nil : description) |> ignoreValues |> mapError { _ in return Void() } ) @@ -2709,6 +2741,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD strongSelf.chatInterfaceInteraction.selectionState = strongSelf.state.selectedMessageIds.flatMap { ChatInterfaceSelectionState(selectedIds: $0) } strongSelf.paneContainerNode.updateSelectedMessageIds(strongSelf.state.selectedMessageIds, animated: true) case .search: + strongSelf.headerNode.navigationButtonContainer.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue) strongSelf.activateSearch() case .editPhoto, .editVideo: break @@ -2719,12 +2752,12 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD if self.isSettings { self.notificationExceptions.set(.single(NotificationExceptionsList(peers: [:], settings: [:])) |> then( - notificationExceptionsList(postbox: context.account.postbox, network: context.account.network) + context.engine.peers.notificationExceptionsList() |> map(Optional.init) )) - self.privacySettings.set(.single(nil) |> then(requestAccountPrivacySettings(account: context.account) |> map(Optional.init))) - self.archivedPacks.set(.single(nil) |> then(archivedStickerPacks(account: context.account) |> map(Optional.init))) - self.hasPassport.set(.single(false) |> then(twoStepAuthData(context.account.network) + self.privacySettings.set(.single(nil) |> then(context.engine.privacy.requestAccountPrivacySettings() |> map(Optional.init))) + self.archivedPacks.set(.single(nil) |> then(context.engine.stickers.archivedStickerPacks() |> map(Optional.init))) + self.hasPassport.set(.single(false) |> then(context.engine.auth.twoStepAuthData() |> map { value -> Bool in return value.hasSecretValues } @@ -2753,7 +2786,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD if copyUsername, let username = user.username, !username.isEmpty { actions.append(ContextMenuAction(content: .text(title: strongSelf.presentationData.strings.Settings_CopyUsername, accessibilityLabel: strongSelf.presentationData.strings.Settings_CopyUsername), action: { [weak self] in - UIPasteboard.general.string = username + UIPasteboard.general.string = "@\(username)" if let strongSelf = self { let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } @@ -2773,14 +2806,20 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD } } else { screenData = peerInfoScreenData(context: context, peerId: peerId, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, isSettings: self.isSettings, ignoreGroupInCommon: ignoreGroupInCommon) - + self.headerNode.displayAvatarContextMenu = { [weak self] node, gesture in guard let strongSelf = self, let peer = strongSelf.data?.peer else { return } + var currentIsVideo = false + let item = strongSelf.headerNode.avatarListNode.listContainerNode.currentItemNode?.item + if let item = item, case let .image(image) = item { + currentIsVideo = !image.2.isEmpty + } + let items: [ContextMenuItem] = [ - .action(ContextMenuActionItem(text: strongSelf.presentationData.strings.PeerInfo_ReportProfilePhoto, icon: { theme in + .action(ContextMenuActionItem(text: currentIsVideo ? strongSelf.presentationData.strings.PeerInfo_ReportProfileVideo : strongSelf.presentationData.strings.PeerInfo_ReportProfilePhoto, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Report"), color: theme.actionSheet.primaryTextColor) }, action: { [weak self] c, f in if let strongSelf = self, let parent = strongSelf.controller { @@ -2798,7 +2837,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD } if [Namespaces.Peer.CloudGroup, Namespaces.Peer.CloudChannel].contains(peerId.namespace) { - self.displayAsPeersPromise.set(cachedGroupCallDisplayAsAvailablePeers(account: context.account, peerId: peerId)) + self.displayAsPeersPromise.set(context.engine.calls.cachedGroupCallDisplayAsAvailablePeers(peerId: peerId)) } } @@ -2817,24 +2856,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD if let _ = nearbyPeerDistance { self.preloadHistoryDisposable.set(self.context.account.addAdditionalPreloadHistoryPeerId(peerId: peerId)) - self.preloadedSticker.set(.single(nil) - |> then(randomGreetingSticker(account: context.account) - |> map { item in - return item?.file - })) - - self.preloadStickerDisposable.set((self.preloadedSticker.get() - |> mapToSignal { sticker -> Signal in - if let sticker = sticker { - let _ = freeMediaFileInteractiveFetched(account: context.account, fileReference: .standalone(media: sticker)).start() - return chatMessageAnimationData(postbox: context.account.postbox, resource: sticker.resource, fitzModifier: nil, width: 384, height: 384, synchronousLoad: false) - |> mapToSignal { _ -> Signal in - return .complete() - } - } else { - return .complete() - } - }).start()) + self.context.prefetchManager?.prepareNextGreetingSticker() } } @@ -2849,10 +2871,12 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD self.selectAddMemberDisposable.dispose() self.addMemberDisposable.dispose() self.preloadHistoryDisposable.dispose() - self.preloadStickerDisposable.dispose() self.resolvePeerByNameDisposable?.dispose() self.navigationActionDisposable.dispose() self.enqueueMediaMessageDisposable.dispose() + self.supportPeerDisposable.dispose() + self.tipsPeerDisposable.dispose() + self.shareStatusDisposable?.dispose() } override func didLoad() { @@ -2916,19 +2940,24 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD var currentCallsPrivate: Bool? var previousVideoCallsAvailable: Bool? = true var currentVideoCallsAvailable: Bool? - + if let previousCachedData = previousData?.cachedData as? CachedUserData, let cachedData = data.cachedData as? CachedUserData { - previousCallsPrivate = previousCachedData.callsPrivate ?? false + previousCallsPrivate = previousCachedData.callsPrivate currentCallsPrivate = cachedData.callsPrivate - previousVideoCallsAvailable = previousCachedData.videoCallsAvailable ?? true + previousVideoCallsAvailable = previousCachedData.videoCallsAvailable currentVideoCallsAvailable = cachedData.videoCallsAvailable } + if let previousSuggestPhoneNumberConfirmation = previousData?.globalSettings?.suggestPhoneNumberConfirmation, previousSuggestPhoneNumberConfirmation != data.globalSettings?.suggestPhoneNumberConfirmation { + infoUpdated = true + } + if let previousSuggestPasswordConfirmation = previousData?.globalSettings?.suggestPasswordConfirmation, previousSuggestPasswordConfirmation != data.globalSettings?.suggestPasswordConfirmation { + infoUpdated = true + } if previousCallsPrivate != currentCallsPrivate || previousVideoCallsAvailable != currentVideoCallsAvailable { infoUpdated = true } - if (previousCall == nil) != (currentCall == nil) { infoUpdated = true } @@ -3090,7 +3119,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD strongSelf.enqueueMediaMessageDisposable.set((legacyAssetPickerEnqueueMessages(account: strongSelf.context.account, signals: signals!) |> deliverOnMainQueue).start(next: { [weak self] messages in if let strongSelf = self { - let _ = enqueueMessages(account: strongSelf.context.account, peerId: strongSelf.peerId, messages: messages).start() + let _ = enqueueMessages(account: strongSelf.context.account, peerId: strongSelf.peerId, messages: messages.map { $0.message }).start() } })) } @@ -3099,7 +3128,10 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD }) } }) - }))) + }), centralItemUpdated: { [weak self] messageId in + self?.paneContainerNode.requestExpandTabs?() + self?.paneContainerNode.currentPane?.node.ensureMessageIsVisible(id: messageId) + })) } private func openResolved(_ result: ResolvedUrl) { guard let navigationController = self.controller?.navigationController as? NavigationController else { @@ -3141,7 +3173,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD } private func openUrl(url: String, concealed: Bool, external: Bool) { - openUserGeneratedUrl(context: self.context, url: url, concealed: concealed, present: { [weak self] c in + openUserGeneratedUrl(context: self.context, peerId: self.peerId, url: url, concealed: concealed, present: { [weak self] c in self?.controller?.present(c, in: .window(.root)) }, openResolved: { [weak self] tempResolved in guard let strongSelf = self else { @@ -3211,7 +3243,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD disposable = MetaDisposable() self.resolvePeerByNameDisposable = disposable } - var resolveSignal = resolvePeerByName(account: self.context.account, name: name, ageLimit: 10) + var resolveSignal = self.context.engine.peers.resolvePeerByName(name: name, ageLimit: 10) var cancelImpl: (() -> Void)? let presentationData = self.presentationData @@ -3275,7 +3307,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD let account = self.context.account var resolveSignal: Signal if let peerName = peerName { - resolveSignal = resolvePeerByName(account: self.context.account, name: peerName) + resolveSignal = self.context.engine.peers.resolvePeerByName(name: peerName) |> mapToSignal { peerId -> Signal in if let peerId = peerId { return account.postbox.loadedPeerWithId(peerId) @@ -3330,24 +3362,18 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD switch key { case .message: if let navigationController = controller.navigationController as? NavigationController { - let _ = (self.preloadedSticker.get() - |> take(1) - |> deliverOnMainQueue).start(next: { [weak self] sticker in - if let strongSelf = self { - strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(strongSelf.peerId), keepStack: strongSelf.nearbyPeerDistance != nil ? .always : .default, peerNearbyData: strongSelf.nearbyPeerDistance.flatMap({ ChatPeerNearbyData(distance: $0) }), greetingData: strongSelf.nearbyPeerDistance != nil ? sticker.flatMap({ ChatGreetingData(sticker: $0) }) : nil, completion: { _ in - if strongSelf.nearbyPeerDistance != nil { - var viewControllers = navigationController.viewControllers - viewControllers = viewControllers.filter { controller in - if controller is PeerInfoScreen { - return false - } - return true - } - navigationController.setViewControllers(viewControllers, animated: false) + self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(self.peerId), keepStack: self.nearbyPeerDistance != nil ? .always : .default, peerNearbyData: self.nearbyPeerDistance.flatMap({ ChatPeerNearbyData(distance: $0) }), completion: { [weak self] _ in + if let strongSelf = self, strongSelf.nearbyPeerDistance != nil { + var viewControllers = navigationController.viewControllers + viewControllers = viewControllers.filter { controller in + if controller is PeerInfoScreen { + return false } - })) + return true + } + navigationController.setViewControllers(viewControllers, animated: false) } - }) + })) } case .discussion: if let cachedData = self.data?.cachedData as? CachedChannelData, case let .known(maybeLinkedDiscussionPeerId) = cachedData.linkedDiscussionPeerId, let linkedDiscussionPeerId = maybeLinkedDiscussionPeerId { @@ -3360,10 +3386,10 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD case .videoCall: self.requestCall(isVideo: true) case .voiceChat: - self.requestCall(isVideo: false) + self.requestCall(isVideo: false, gesture: gesture) case .mute: if let notificationSettings = self.data?.notificationSettings, case .muted = notificationSettings.muteState { - let _ = updatePeerMuteSetting(account: self.context.account, peerId: self.peerId, muteInterval: nil).start() + let _ = self.context.engine.peers.updatePeerMuteSetting(peerId: self.peerId, muteInterval: nil).start() } else { self.state = self.state.withHighlightedButton(.mute) if let (layout, navigationHeight) = self.validLayout { @@ -3389,7 +3415,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD }, action: { _, f in f(.dismissWithoutContent) - let _ = updatePeerMuteSetting(account: self.context.account, peerId: self.peerId, muteInterval: delay).start() + let _ = self.context.engine.peers.updatePeerMuteSetting(peerId: self.peerId, muteInterval: delay).start() }))) } @@ -3406,16 +3432,16 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD let context = strongSelf.context let updatePeerSound: (PeerId, PeerMessageSound) -> Signal = { peerId, sound in - return updatePeerNotificationSoundInteractive(account: context.account, peerId: peerId, sound: sound) |> deliverOnMainQueue + return context.engine.peers.updatePeerNotificationSoundInteractive(peerId: peerId, sound: sound) |> deliverOnMainQueue } let updatePeerNotificationInterval: (PeerId, Int32?) -> Signal = { peerId, muteInterval in - return updatePeerMuteSetting(account: context.account, peerId: peerId, muteInterval: muteInterval) |> deliverOnMainQueue + return context.engine.peers.updatePeerMuteSetting(peerId: peerId, muteInterval: muteInterval) |> deliverOnMainQueue } let updatePeerDisplayPreviews:(PeerId, PeerNotificationDisplayPreviews) -> Signal = { peerId, displayPreviews in - return updatePeerDisplayPreviewsSetting(account: context.account, peerId: peerId, displayPreviews: displayPreviews) |> deliverOnMainQueue + return context.engine.peers.updatePeerDisplayPreviewsSetting(peerId: peerId, displayPreviews: displayPreviews) |> deliverOnMainQueue } let exceptionController = notificationPeerExceptionController(context: context, peer: peer, mode: .users([:]), edit: true, updatePeerSound: { peerId, sound in @@ -3470,10 +3496,27 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD mainItemsImpl = { var items: [ContextMenuItem] = [] - let headerButtons = peerInfoHeaderButtons(peer: peer, cachedData: data.cachedData, isOpenedFromChat: self.isOpenedFromChat, videoCallsEnabled: self.videoCallsEnabled, isSecretChat: self.peerId.namespace == Namespaces.Peer.SecretChat, isContact: self.data?.isContact ?? false) + let allHeaderButtons = Set(peerInfoHeaderButtons(peer: peer, cachedData: data.cachedData, isOpenedFromChat: self.isOpenedFromChat, isExpanded: false, videoCallsEnabled: self.videoCallsEnabled, isSecretChat: self.peerId.namespace == Namespaces.Peer.SecretChat, isContact: self.data?.isContact ?? false)) + let headerButtons = Set(peerInfoHeaderButtons(peer: peer, cachedData: data.cachedData, isOpenedFromChat: self.isOpenedFromChat, isExpanded: self.headerNode.isAvatarExpanded, videoCallsEnabled: self.videoCallsEnabled, isSecretChat: self.peerId.namespace == Namespaces.Peer.SecretChat, isContact: self.data?.isContact ?? false)) - let hasSearch = !headerButtons.contains(.search) || (self.headerNode.isAvatarExpanded && self.peerId.namespace == Namespaces.Peer.CloudUser) - if hasSearch { + let filteredButtons = allHeaderButtons.subtracting(headerButtons) + if filteredButtons.contains(.addMember) { + items.append(.action(ContextMenuActionItem(text: presentationData.strings.PeerInfo_ButtonAddMember, icon: { theme in + generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/AddUser"), color: theme.contextMenu.primaryColor) + }, action: { [weak self] _, f in + f(.dismissWithoutContent) + self?.openAddMember() + }))) + } + if filteredButtons.contains(.call) { + items.append(.action(ContextMenuActionItem(text: presentationData.strings.PeerInfo_ButtonCall, icon: { theme in + generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Call"), color: theme.contextMenu.primaryColor) + }, action: { [weak self] _, f in + f(.dismissWithoutContent) + self?.requestCall(isVideo: false) + }))) + } + if filteredButtons.contains(.search) { items.append(.action(ContextMenuActionItem(text: presentationData.strings.ChatSearch_SearchPlaceholder, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Search"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in @@ -3538,6 +3581,46 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD if let strongSelf = self, 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))) + shareController.completed = { [weak self] peerIds in + if let strongSelf = self { + let _ = (strongSelf.context.account.postbox.transaction { transaction -> [Peer] in + var peers: [Peer] = [] + for peerId in peerIds { + if let peer = transaction.getPeer(peerId) { + peers.append(peer) + } + } + return peers + } |> deliverOnMainQueue).start(next: { [weak self] peers in + if let strongSelf = self { + let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } + + let text: String + var savedMessages = false + if peerIds.count == 1, let peerId = peerIds.first, peerId == strongSelf.context.account.peerId { + text = presentationData.strings.UserInfo_ContactForwardTooltip_SavedMessages_One + savedMessages = true + } else { + if peers.count == 1, let peer = peers.first { + let peerName = peer.id == strongSelf.context.account.peerId ? presentationData.strings.DialogList_SavedMessages : peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + text = presentationData.strings.UserInfo_ContactForwardTooltip_Chat_One(peerName).0 + } else if peers.count == 2, let firstPeer = peers.first, let secondPeer = peers.last { + let firstPeerName = firstPeer.id == strongSelf.context.account.peerId ? presentationData.strings.DialogList_SavedMessages : firstPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + let secondPeerName = secondPeer.id == strongSelf.context.account.peerId ? presentationData.strings.DialogList_SavedMessages : secondPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + text = presentationData.strings.UserInfo_ContactForwardTooltip_TwoChats_One(firstPeerName, secondPeerName).0 + } else if let peer = peers.first { + let peerName = peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + text = presentationData.strings.UserInfo_ContactForwardTooltip_ManyChats_One(peerName, "\(peers.count - 1)").0 + } else { + text = "" + } + } + + strongSelf.controller?.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: savedMessages, text: text), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), in: .window(.root)) + } + }) + } + } strongSelf.controller?.present(shareController, in: .window(.root)) } }))) @@ -3576,20 +3659,6 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD } } } else if let channel = peer as? TelegramChannel { - if !channel.flags.contains(.hasVoiceChat) { - if channel.flags.contains(.isCreator) || channel.hasPermission(.manageCalls) { - items.append(.action(ContextMenuActionItem(text: presentationData.strings.ChannelInfo_CreateVoiceChat, icon: { theme in - generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/VoiceChat"), color: theme.contextMenu.primaryColor) - }, action: { [weak self] c, f in - self?.requestCall(isVideo: false, contextController: c, result: f, backAction: { c in - if let mainItemsImpl = mainItemsImpl { - c.setItems(mainItemsImpl()) - } - }) - }))) - } - } - if let cachedData = self.data?.cachedData as? CachedChannelData, cachedData.flags.contains(.canViewStats) { items.append(.action(ContextMenuActionItem(text: presentationData.strings.ChannelInfo_Stats, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Statistics"), color: theme.contextMenu.primaryColor) @@ -3636,19 +3705,17 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD self?.openDeletePeer() }))) } else { - if !headerButtons.contains(.leave) { - if case .member = channel.participationStatus { - if !items.isEmpty { - items.append(.separator) - } - items.append(.action(ContextMenuActionItem(text: presentationData.strings.Channel_LeaveChannel, textColor: .destructive, icon: { theme in - generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor) - }, action: { [weak self] _, f in - f(.dismissWithoutContent) - - self?.openLeavePeer() - }))) + if case .member = channel.participationStatus, !headerButtons.contains(.leave) { + if !items.isEmpty { + items.append(.separator) } + items.append(.action(ContextMenuActionItem(text: presentationData.strings.Channel_LeaveChannel, textColor: .destructive, icon: { theme in + generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Logout"), color: theme.contextMenu.destructiveColor) + }, action: { [weak self] _, f in + f(.dismissWithoutContent) + + self?.openLeavePeer() + }))) } } case .group: @@ -3664,12 +3731,12 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD self?.openDeletePeer() }))) } else { - if case .member = channel.participationStatus { + if case .member = channel.participationStatus, !headerButtons.contains(.leave) { if !items.isEmpty { items.append(.separator) } items.append(.action(ContextMenuActionItem(text: presentationData.strings.Group_LeaveGroup, textColor: .destructive, icon: { theme in - generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor) + generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Logout"), color: theme.contextMenu.destructiveColor) }, action: { [weak self] _, f in f(.dismissWithoutContent) @@ -3679,28 +3746,12 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD } } } else if let group = peer as? TelegramGroup { - var canManageGroupCalls = false - if case .creator = group.role { - canManageGroupCalls = true - } else if case let .admin(rights, _) = group.role { - if rights.rights.contains(.canManageCalls) { - canManageGroupCalls = true - } - } - if canManageGroupCalls, !group.flags.contains(.hasVoiceChat) { - items.append(.action(ContextMenuActionItem(text: presentationData.strings.ChannelInfo_CreateVoiceChat, icon: { theme in - generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/VoiceChat"), color: theme.contextMenu.primaryColor) - }, action: { [weak self] c, f in - self?.requestCall(isVideo: false, contextController: c, result: f) - }))) - } - - if case .Member = group.membership { + if case .Member = group.membership, !headerButtons.contains(.leave) { if !items.isEmpty { items.append(.separator) } items.append(.action(ContextMenuActionItem(text: presentationData.strings.Group_LeaveGroup, textColor: .destructive, icon: { theme in - generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor) + generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Logout"), color: theme.contextMenu.destructiveColor) }, action: { [weak self] _, f in f(.dismissWithoutContent) @@ -3737,24 +3788,18 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD private func openChatWithMessageSearch() { if let navigationController = (self.controller?.navigationController as? NavigationController) { - let _ = (self.preloadedSticker.get() - |> take(1) - |> deliverOnMainQueue).start(next: { [weak self] sticker in - if let strongSelf = self { - strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(strongSelf.peerId), keepStack: strongSelf.nearbyPeerDistance != nil ? .always : .default, activateMessageSearch: (.everything, ""), peerNearbyData: strongSelf.nearbyPeerDistance.flatMap({ ChatPeerNearbyData(distance: $0) }), greetingData: strongSelf.nearbyPeerDistance != nil ? sticker.flatMap({ ChatGreetingData(sticker: $0) }) : nil, completion: { _ in - if strongSelf.nearbyPeerDistance != nil { - var viewControllers = navigationController.viewControllers - viewControllers = viewControllers.filter { controller in - if controller is PeerInfoScreen { - return false - } - return true - } - navigationController.setViewControllers(viewControllers, animated: false) + self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(self.peerId), keepStack: self.nearbyPeerDistance != nil ? .always : .default, activateMessageSearch: (.everything, ""), peerNearbyData: self.nearbyPeerDistance.flatMap({ ChatPeerNearbyData(distance: $0) }), completion: { [weak self] _ in + if let strongSelf = self, strongSelf.nearbyPeerDistance != nil { + var viewControllers = navigationController.viewControllers + viewControllers = viewControllers.filter { controller in + if controller is PeerInfoScreen { + return false } - })) + return true + } + navigationController.setViewControllers(viewControllers, animated: false) } - }) + })) } } @@ -3800,7 +3845,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD guard let strongSelf = self else { return } - var createSignal = createSecretChat(account: strongSelf.context.account, peerId: peerId) + var createSignal = strongSelf.context.engine.peers.createSecretChat(peerId: peerId) var cancelImpl: (() -> Void)? let progressSignal = Signal { subscriber in if let strongSelf = self { @@ -3860,6 +3905,46 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD private func openUsername(value: String) { let shareController = ShareController(context: self.context, subject: .url("https://t.me/\(value)")) + shareController.completed = { [weak self] peerIds in + if let strongSelf = self { + let _ = (strongSelf.context.account.postbox.transaction { transaction -> [Peer] in + var peers: [Peer] = [] + for peerId in peerIds { + if let peer = transaction.getPeer(peerId) { + peers.append(peer) + } + } + return peers + } |> deliverOnMainQueue).start(next: { [weak self] peers in + if let strongSelf = self { + let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } + + let text: String + var savedMessages = false + if peerIds.count == 1, let peerId = peerIds.first, peerId == strongSelf.context.account.peerId { + text = presentationData.strings.UserInfo_LinkForwardTooltip_SavedMessages_One + savedMessages = true + } else { + if peers.count == 1, let peer = peers.first { + let peerName = peer.id == strongSelf.context.account.peerId ? presentationData.strings.DialogList_SavedMessages : peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + text = presentationData.strings.UserInfo_LinkForwardTooltip_Chat_One(peerName).0 + } else if peers.count == 2, let firstPeer = peers.first, let secondPeer = peers.last { + let firstPeerName = firstPeer.id == strongSelf.context.account.peerId ? presentationData.strings.DialogList_SavedMessages : firstPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + let secondPeerName = secondPeer.id == strongSelf.context.account.peerId ? presentationData.strings.DialogList_SavedMessages : secondPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + text = presentationData.strings.UserInfo_LinkForwardTooltip_TwoChats_One(firstPeerName, secondPeerName).0 + } else if let peer = peers.first { + let peerName = peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + text = presentationData.strings.UserInfo_LinkForwardTooltip_ManyChats_One(peerName, "\(peers.count - 1)").0 + } else { + text = "" + } + } + + strongSelf.controller?.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: savedMessages, text: text), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), in: .window(.root)) + } + }) + } + } shareController.actionCompleted = { [weak self] in if let strongSelf = self { let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } @@ -3870,7 +3955,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD self.controller?.present(shareController, in: .window(.root)) } - private func requestCall(isVideo: Bool, gesture: ContextGesture? = nil, contextController: ContextController? = nil, result: ((ContextMenuActionResult) -> Void)? = nil, backAction: ((ContextController) -> Void)? = nil) { + private func requestCall(isVideo: Bool, gesture: ContextGesture? = nil, contextController: ContextControllerProtocol? = nil, result: ((ContextMenuActionResult) -> Void)? = nil, backAction: ((ContextControllerProtocol) -> Void)? = nil) { let peerId = self.peerId let requestCall: (PeerId?, CachedChannelData.ActiveCall?) -> Void = { [weak self] defaultJoinAsPeerId, activeCall in if let activeCall = activeCall { @@ -3885,14 +3970,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD } }, activeCall: activeCall) } else { - if let defaultJoinAsPeerId = defaultJoinAsPeerId { - result?(.dismissWithoutContent) - self?.createAndJoinGroupCall(peerId: peerId, joinAsPeerId: defaultJoinAsPeerId) - } else { - self?.openVoiceChatDisplayAsPeerSelection(completion: { joinAsPeerId in - self?.createAndJoinGroupCall(peerId: peerId, joinAsPeerId: joinAsPeerId) - }, gesture: gesture, contextController: contextController, result: result, backAction: backAction) - } + self?.openVoiceChatOptions(defaultJoinAsPeerId: defaultJoinAsPeerId, gesture: gesture, contextController: contextController) } } @@ -3915,6 +3993,10 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD self.context.requestCall(peerId: peer.id, isVideo: isVideo, completion: {}) } + private func scheduleGroupCall() { + self.context.scheduleGroupCall(peerId: self.peerId) + } + private func createAndJoinGroupCall(peerId: PeerId, joinAsPeerId: PeerId?) { if let _ = self.context.sharedContext.callManager { let startCall: (Bool) -> Void = { [weak self] endCurrentIfAny in @@ -3922,26 +4004,40 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD return } - var dismissStatus: (() -> Void)? - let statusController = OverlayStatusController(theme: strongSelf.presentationData.theme, type: .loading(cancelled: { - dismissStatus?() - })) - dismissStatus = { [weak self, weak statusController] in - self?.activeActionDisposable.set(nil) - statusController?.dismiss() + var cancelImpl: (() -> Void)? + let presentationData = strongSelf.presentationData + let progressSignal = Signal { [weak self] subscriber in + let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: { + cancelImpl?() + })) + self?.controller?.present(controller, in: .window(.root)) + return ActionDisposable { [weak controller] in + Queue.mainQueue().async() { + controller?.dismiss() + } + } } - strongSelf.controller?.present(statusController, in: .window(.root)) - strongSelf.activeActionDisposable.set((createGroupCall(account: strongSelf.context.account, peerId: peerId) + |> runOn(Queue.mainQueue()) + |> delay(0.15, queue: Queue.mainQueue()) + let progressDisposable = progressSignal.start() + let createSignal = strongSelf.context.engine.calls.createGroupCall(peerId: peerId, title: nil, scheduleDate: nil) + |> afterDisposed { + Queue.mainQueue().async { + progressDisposable.dispose() + } + } + cancelImpl = { [weak self] in + self?.activeActionDisposable.set(nil) + } + strongSelf.activeActionDisposable.set((createSignal |> deliverOnMainQueue).start(next: { [weak self] info in guard let strongSelf = self else { return } strongSelf.context.joinGroupCall(peerId: peerId, invite: nil, requestJoinAsPeerId: { result in result(joinAsPeerId) - }, activeCall: CachedChannelData.ActiveCall(id: info.id, accessHash: info.accessHash, title: info.title)) + }, activeCall: CachedChannelData.ActiveCall(id: info.id, accessHash: info.accessHash, title: info.title, scheduleTimestamp: nil, subscribedToScheduled: false)) }, error: { [weak self] error in - dismissStatus?() - guard let strongSelf = self else { return } @@ -3949,14 +4045,12 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD let text: String switch error { - case .generic: + case .generic, .scheduledTooLate: text = strongSelf.presentationData.strings.Login_UnknownError case .anonymousNotAllowed: text = strongSelf.presentationData.strings.VoiceChat_AnonymousDisabledAlertText } strongSelf.controller?.present(textAlertController(context: strongSelf.context, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) - }, completed: { [weak self] in - dismissStatus?() })) } @@ -4019,7 +4113,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD guard let strongSelf = self else { return } - let _ = updatePeerNotificationSoundInteractive(account: strongSelf.context.account, peerId: strongSelf.peerId, sound: sound).start() + let _ = strongSelf.context.engine.peers.updatePeerNotificationSoundInteractive(peerId: strongSelf.peerId, sound: sound).start() }) soundController.navigationPresentation = .modal strongSelf.controller?.push(soundController) @@ -4027,7 +4121,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD guard let strongSelf = self else { return } - let _ = updatePeerMuteSetting(account: strongSelf.context.account, peerId: strongSelf.peerId, muteInterval: value).start() + let _ = strongSelf.context.engine.peers.updatePeerMuteSetting(peerId: strongSelf.peerId, muteInterval: value).start() }) strongSelf.view.endEditing(true) strongSelf.controller?.present(muteSettingsController, in: .window(.root)) @@ -4050,7 +4144,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD guard let strongSelf = self else { return } - let _ = updatePeerNotificationSoundInteractive(account: strongSelf.context.account, peerId: strongSelf.peerId, sound: sound).start() + let _ = strongSelf.context.engine.peers.updatePeerNotificationSoundInteractive(peerId: strongSelf.peerId, sound: sound).start() }) strongSelf.controller?.push(soundController) }) @@ -4062,7 +4156,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD guard let strongSelf = self, let peer = peer else { return } - let _ = updatePeerDisplayPreviewsSetting(account: strongSelf.context.account, peerId: peer.id, displayPreviews: value ? .show : .hide).start() + let _ = strongSelf.context.engine.peers.updatePeerDisplayPreviewsSetting(peerId: peer.id, displayPreviews: value ? .show : .hide).start() }) } @@ -4090,7 +4184,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD deleteContactFromDevice = .complete() } - var deleteSignal = deleteContactPeerInteractively(account: strongSelf.context.account, peerId: peer.id) + var deleteSignal = strongSelf.context.engine.contacts.deleteContactPeerInteractively(peerId: peer.id) |> then(deleteContactFromDevice) let progressSignal = Signal { subscriber in @@ -4121,7 +4215,9 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD |> deliverOnMainQueue).start(completed: { [weak self] in if let strongSelf = self, let peer = strongSelf.data?.peer { let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } - strongSelf.controller?.present(UndoOverlayController(presentationData: presentationData, content: .info(text: presentationData.strings.Conversation_DeletedFromContacts(peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).0), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root)) + let controller = UndoOverlayController(presentationData: presentationData, content: .info(text: presentationData.strings.Conversation_DeletedFromContacts(peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).0), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }) + controller.keepOnParentDismissal = true + strongSelf.controller?.present(controller, in: .window(.root)) strongSelf.controller?.dismiss() } @@ -4139,24 +4235,18 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD private func openChat() { if let navigationController = self.controller?.navigationController as? NavigationController { - let _ = (self.preloadedSticker.get() - |> take(1) - |> deliverOnMainQueue).start(next: { [weak self] sticker in - if let strongSelf = self { - strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(strongSelf.peerId), keepStack: strongSelf.nearbyPeerDistance != nil ? .always : .default, peerNearbyData: strongSelf.nearbyPeerDistance.flatMap({ ChatPeerNearbyData(distance: $0) }), greetingData: strongSelf.nearbyPeerDistance != nil ? sticker.flatMap({ ChatGreetingData(sticker: $0) }) : nil, completion: { _ in - if strongSelf.nearbyPeerDistance != nil { - var viewControllers = navigationController.viewControllers - viewControllers = viewControllers.filter { controller in - if controller is PeerInfoScreen { - return false - } - return true - } - navigationController.setViewControllers(viewControllers, animated: false) + self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(self.peerId), keepStack: self.nearbyPeerDistance != nil ? .always : .default, peerNearbyData: self.nearbyPeerDistance.flatMap({ ChatPeerNearbyData(distance: $0) }), completion: { [weak self] _ in + if let strongSelf = self, strongSelf.nearbyPeerDistance != nil { + var viewControllers = navigationController.viewControllers + viewControllers = viewControllers.filter { controller in + if controller is PeerInfoScreen { + return false } - })) + return true + } + navigationController.setViewControllers(viewControllers, animated: false) } - }) + })) } } @@ -4184,9 +4274,9 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD 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()) + strongSelf.activeActionDisposable.set(strongSelf.context.engine.privacy.requestUpdatePeerIsBlocked(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() + let _ = enqueueMessages(account: strongSelf.context.account, peerId: peer.id, messages: [.message(text: "/start", attributes: [], mediaReference: nil, replyToMessageId: nil, localGroupingKey: nil, correlationId: nil)]).start() if let navigationController = strongSelf.controller?.navigationController as? NavigationController { strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(peer.id))) } @@ -4209,12 +4299,12 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD return } - strongSelf.activeActionDisposable.set(requestUpdatePeerIsBlocked(account: strongSelf.context.account, peerId: peer.id, isBlocked: true).start()) + strongSelf.activeActionDisposable.set(strongSelf.context.engine.privacy.requestUpdatePeerIsBlocked(peerId: peer.id, isBlocked: true).start()) if deleteChat { - let _ = removePeerChat(account: strongSelf.context.account, peerId: strongSelf.peerId, reportChatSpam: reportSpam).start() + let _ = strongSelf.context.engine.peers.removePeerChat(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, message: "").start() + let _ = strongSelf.context.engine.peers.reportPeer(peerId: strongSelf.peerId, reason: .spam, message: "").start() } deleteSendMessageIntents(peerId: strongSelf.peerId) @@ -4235,7 +4325,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD guard let strongSelf = self else { return } - strongSelf.activeActionDisposable.set(requestUpdatePeerIsBlocked(account: strongSelf.context.account, peerId: peer.id, isBlocked: block).start()) + strongSelf.activeActionDisposable.set(strongSelf.context.engine.privacy.requestUpdatePeerIsBlocked(peerId: peer.id, isBlocked: block).start()) })]), in: .window(.root)) } } @@ -4257,7 +4347,90 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD controller.push(statsController) } - private func openVoiceChatDisplayAsPeerSelection(completion: @escaping (PeerId) -> Void, gesture: ContextGesture? = nil, contextController: ContextController? = nil, result: ((ContextMenuActionResult) -> Void)? = nil, backAction: ((ContextController) -> Void)? = nil) { + private func openVoiceChatOptions(defaultJoinAsPeerId: PeerId?, gesture: ContextGesture? = nil, contextController: ContextControllerProtocol? = nil) { + let context = self.context + let peerId = self.peerId + let defaultJoinAsPeerId = defaultJoinAsPeerId ?? self.context.account.peerId + let currentAccountPeer = self.context.account.postbox.loadedPeerWithId(self.context.account.peerId) + |> map { peer in + return [FoundPeer(peer: peer, subscribers: nil)] + } + let _ = (combineLatest(queue: Queue.mainQueue(), currentAccountPeer, self.displayAsPeersPromise.get() |> take(1)) + |> map { currentAccountPeer, availablePeers -> [FoundPeer] in + var result = currentAccountPeer + result.append(contentsOf: availablePeers) + return result + }).start(next: { [weak self] peers in + guard let strongSelf = self else { + return + } + + var items: [ContextMenuItem] = [] + + if peers.count > 1 { + var selectedPeer: FoundPeer? + for peer in peers { + if peer.peer.id == defaultJoinAsPeerId { + selectedPeer = peer + } + } + if let peer = selectedPeer { + let avatarSize = CGSize(width: 28.0, height: 28.0) + items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_DisplayAs, textLayout: .secondLineWithValue(peer.peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)), icon: { _ in nil }, iconSource: ContextMenuActionItemIconSource(size: avatarSize, signal: peerAvatarCompleteImage(account: strongSelf.context.account, peer: peer.peer, size: avatarSize)), action: { c, f in + guard let strongSelf = self else { + return + } + + strongSelf.openVoiceChatDisplayAsPeerSelection(completion: { joinAsPeerId in + let _ = context.engine.calls.updateGroupCallJoinAsPeer(peerId: peerId, joinAs: joinAsPeerId).start() + self?.openVoiceChatOptions(defaultJoinAsPeerId: joinAsPeerId, gesture: nil, contextController: c) + }, gesture: gesture, contextController: c, result: f, backAction: { [weak self] c in + self?.openVoiceChatOptions(defaultJoinAsPeerId: defaultJoinAsPeerId, gesture: nil, contextController: c) + }) + + }))) + items.append(.separator) + } + } + + items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.ChannelInfo_CreateVoiceChat, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/VoiceChat"), color: theme.contextMenu.primaryColor) }, action: { _, f in + f(.dismissWithoutContent) + + self?.createAndJoinGroupCall(peerId: peerId, joinAsPeerId: defaultJoinAsPeerId) + }))) + + items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.ChannelInfo_ScheduleVoiceChat, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Schedule"), color: theme.contextMenu.primaryColor) }, action: { _, f in + f(.dismissWithoutContent) + + self?.scheduleGroupCall() + }))) + + if let contextController = contextController { + contextController.setItems(.single(items)) + } else { + strongSelf.state = strongSelf.state.withHighlightedButton(.voiceChat) + if let (layout, navigationHeight) = strongSelf.validLayout { + strongSelf.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: .immediate, additive: false) + } + + if let sourceNode = strongSelf.headerNode.buttonNodes[.voiceChat]?.referenceNode, let controller = strongSelf.controller { + let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .reference(PeerInfoContextReferenceContentSource(controller: controller, sourceNode: sourceNode)), items: .single(items), reactionItems: [], gesture: gesture) + contextController.dismissed = { [weak self] in + if let strongSelf = self { + strongSelf.state = strongSelf.state.withHighlightedButton(nil) + if let (layout, navigationHeight) = strongSelf.validLayout { + strongSelf.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: .immediate, additive: false) + } + } + } + controller.presentInGlobalOverlay(contextController) + } + } + }) + } + + private func openVoiceChatDisplayAsPeerSelection(completion: @escaping (PeerId) -> Void, gesture: ContextGesture? = nil, contextController: ContextControllerProtocol? = nil, result: ((ContextMenuActionResult) -> Void)? = nil, backAction: ((ContextControllerProtocol) -> Void)? = nil) { + let dismissOnSelection = contextController == nil let currentAccountPeer = self.context.account.postbox.loadedPeerWithId(context.account.peerId) |> map { peer in return [FoundPeer(peer: peer, subscribers: nil)] @@ -4279,7 +4452,10 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD var isGroup = false for peer in peers { - if let peer = peer.peer as? TelegramChannel, case .group = peer.info { + if peer.peer is TelegramGroup { + isGroup = true + break + } else if let peer = peer.peer as? TelegramChannel, case .group = peer.info { isGroup = true break } @@ -4304,8 +4480,9 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD let avatarSize = CGSize(width: 28.0, height: 28.0) let avatarSignal = peerAvatarCompleteImage(account: strongSelf.context.account, peer: peer.peer, size: avatarSize) items.append(.action(ContextMenuActionItem(text: peer.peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder), textLayout: subtitle.flatMap { .secondLineWithValue($0) } ?? .singleLine, icon: { _ in nil }, iconSource: ContextMenuActionItemIconSource(size: avatarSize, signal: avatarSignal), action: { _, f in - f(.dismissWithoutContent) - + if dismissOnSelection { + f(.dismissWithoutContent) + } completion(peer.peer.id) }))) @@ -4349,7 +4526,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD }) } - private func openReport(user: Bool, contextController: ContextController?, backAction: ((ContextController) -> Void)?) { + private func openReport(user: Bool, contextController: ContextControllerProtocol?, backAction: ((ContextControllerProtocol) -> Void)?) { guard let controller = self.controller else { return } @@ -4386,6 +4563,46 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD } if let peer = peer as? TelegramUser, let username = peer.username { let shareController = ShareController(context: strongSelf.context, subject: .url("https://t.me/\(username)")) + shareController.completed = { [weak self] peerIds in + if let strongSelf = self { + let _ = (strongSelf.context.account.postbox.transaction { transaction -> [Peer] in + var peers: [Peer] = [] + for peerId in peerIds { + if let peer = transaction.getPeer(peerId) { + peers.append(peer) + } + } + return peers + } |> deliverOnMainQueue).start(next: { [weak self] peers in + if let strongSelf = self { + let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } + + let text: String + var savedMessages = false + if peerIds.count == 1, let peerId = peerIds.first, peerId == strongSelf.context.account.peerId { + text = presentationData.strings.UserInfo_LinkForwardTooltip_SavedMessages_One + savedMessages = true + } else { + if peers.count == 1, let peer = peers.first { + let peerName = peer.id == strongSelf.context.account.peerId ? presentationData.strings.DialogList_SavedMessages : peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + text = presentationData.strings.UserInfo_LinkForwardTooltip_Chat_One(peerName).0 + } else if peers.count == 2, let firstPeer = peers.first, let secondPeer = peers.last { + let firstPeerName = firstPeer.id == strongSelf.context.account.peerId ? presentationData.strings.DialogList_SavedMessages : firstPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + let secondPeerName = secondPeer.id == strongSelf.context.account.peerId ? presentationData.strings.DialogList_SavedMessages : secondPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + text = presentationData.strings.UserInfo_LinkForwardTooltip_TwoChats_One(firstPeerName, secondPeerName).0 + } else if let peer = peers.first { + let peerName = peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + text = presentationData.strings.UserInfo_LinkForwardTooltip_ManyChats_One(peerName, "\(peers.count - 1)").0 + } else { + text = "" + } + } + + strongSelf.controller?.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: savedMessages, text: text), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), in: .window(.root)) + } + }) + } + } shareController.actionCompleted = { [weak self] in if let strongSelf = self { let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } @@ -4429,7 +4646,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD 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() + let _ = enqueueMessages(account: strongSelf.context.account, peerId: peer.id, messages: [.message(text: text, attributes: [], mediaReference: nil, replyToMessageId: nil, localGroupingKey: nil, correlationId: nil)]).start() if let navigationController = strongSelf.controller?.navigationController as? NavigationController { strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(strongSelf.peerId))) @@ -4454,7 +4671,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD } private func editingToggleMessageSignatures(value: Bool) { - self.toggleShouldChannelMessagesSignaturesDisposable.set(toggleShouldChannelMessagesSignatures(account: self.context.account, peerId: self.peerId, enabled: value).start()) + self.toggleShouldChannelMessagesSignaturesDisposable.set(self.context.engine.peers.toggleShouldChannelMessagesSignatures(peerId: self.peerId, enabled: value).start()) } private func openParticipantsSection(section: PeerInfoParticipantsSection) { @@ -4781,7 +4998,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD if case let .image(reference, _, _, _) = item { if let reference = reference { if remove { - let _ = removeAccountPhoto(network: self.context.account.network, reference: reference).start() + let _ = self.context.engine.accountData.removeAccountPhoto(reference: reference).start() } let dismiss = self.headerNode.avatarListNode.listContainerNode.deleteItem(item) if dismiss { @@ -4811,9 +5028,9 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD } self.scrollNode.view.setContentOffset(CGPoint(), animated: false) - let resource = LocalFileMediaResource(fileId: arc4random64()) + let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) self.context.account.postbox.mediaBox.storeResourceData(resource.id, data: data) - let representation = TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 640, height: 640), resource: resource, progressiveSizes: []) + let representation = TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 640, height: 640), resource: resource, progressiveSizes: [], immediateThumbnailData: nil) self.state = self.state.withUpdatingAvatar(.image(representation)) if let (layout, navigationHeight) = self.validLayout { @@ -4822,9 +5039,9 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD self.headerNode.ignoreCollapse = false let postbox = self.context.account.postbox - let signal = self.isSettings ? updateAccountPhoto(account: self.context.account, resource: resource, videoResource: nil, videoStartTimestamp: nil, mapResourceToAvatarSizes: { resource, representations in + let signal = self.isSettings ? self.context.engine.accountData.updateAccountPhoto(resource: resource, videoResource: nil, videoStartTimestamp: nil, mapResourceToAvatarSizes: { resource, representations in return mapResourceToAvatarSizes(postbox: postbox, resource: resource, representations: representations) - }) : updatePeerPhoto(postbox: postbox, network: self.context.account.network, stateManager: self.context.account.stateManager, accountPeerId: self.context.account.peerId, peerId: self.peerId, photo: uploadedPeerPhoto(postbox: postbox, network: self.context.account.network, resource: resource), mapResourceToAvatarSizes: { resource, representations in + }) : self.context.engine.peers.updatePeerPhoto(peerId: self.peerId, photo: self.context.engine.peers.uploadedPeerPhoto(resource: resource), mapResourceToAvatarSizes: { resource, representations in return mapResourceToAvatarSizes(postbox: postbox, resource: resource, representations: representations) }) self.updateAvatarDisposable.set((signal @@ -4856,9 +5073,9 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD } self.scrollNode.view.setContentOffset(CGPoint(), animated: false) - let photoResource = LocalFileMediaResource(fileId: arc4random64()) + let photoResource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) self.context.account.postbox.mediaBox.storeResourceData(photoResource.id, data: data) - let representation = TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 640, height: 640), resource: photoResource, progressiveSizes: []) + let representation = TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 640, height: 640), resource: photoResource, progressiveSizes: [], immediateThumbnailData: nil) self.state = self.state.withUpdatingAvatar(.image(representation)) if let (layout, navigationHeight) = self.validLayout { @@ -4872,6 +5089,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD } let account = self.context.account + let context = self.context let signal = Signal { [weak self] subscriber in let entityRenderer: LegacyPaintEntityRenderer? = adjustments.flatMap { adjustments in if let paintingData = adjustments.paintingData, paintingData.hasAnimation { @@ -4880,7 +5098,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD return nil } } - let uploadInterface = LegacyLiveUploadInterface(account: account) + let uploadInterface = LegacyLiveUploadInterface(context: context) let signal: SSignal if let asset = asset as? AVAsset { signal = TGMediaVideoConverter.convert(asset, adjustments: adjustments, watcher: uploadInterface, entityRenderer: entityRenderer)! @@ -4924,7 +5142,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD if let liveUploadData = result.liveUploadData as? LegacyLiveUploadInterfaceResult { resource = LocalFileMediaResource(fileId: liveUploadData.id) } else { - resource = LocalFileMediaResource(fileId: arc4random64()) + resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) } account.postbox.mediaBox.storeResourceData(resource.id, data: data, synchronous: true) subscriber.putNext(resource) @@ -4956,11 +5174,11 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD self.updateAvatarDisposable.set((signal |> mapToSignal { videoResource -> Signal in if isSettings { - return updateAccountPhoto(account: account, resource: photoResource, videoResource: videoResource, videoStartTimestamp: videoStartTimestamp, mapResourceToAvatarSizes: { resource, representations in + return context.engine.accountData.updateAccountPhoto(resource: photoResource, videoResource: videoResource, videoStartTimestamp: videoStartTimestamp, mapResourceToAvatarSizes: { resource, representations in return mapResourceToAvatarSizes(postbox: account.postbox, resource: resource, representations: representations) }) } else { - return updatePeerPhoto(postbox: account.postbox, network: account.network, stateManager: account.stateManager, accountPeerId: account.peerId, peerId: peerId, photo: uploadedPeerPhoto(postbox: account.postbox, network: account.network, resource: photoResource), video: uploadedPeerVideo(postbox: account.postbox, network: account.network, messageMediaPreuploadManager: account.messageMediaPreuploadManager, resource: videoResource) |> map(Optional.init), videoStartTimestamp: videoStartTimestamp, mapResourceToAvatarSizes: { resource, representations in + return context.engine.peers.updatePeerPhoto(peerId: peerId, photo: context.engine.peers.uploadedPeerPhoto(resource: photoResource), video: context.engine.peers.uploadedPeerVideo(resource: videoResource) |> map(Optional.init), videoStartTimestamp: videoStartTimestamp, mapResourceToAvatarSizes: { resource, representations in return mapResourceToAvatarSizes(postbox: account.postbox, resource: resource, representations: representations) }) } @@ -5081,7 +5299,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD } } 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 + strongSelf.updateAvatarDisposable.set((strongSelf.context.engine.peers.updatePeerPhoto(peerId: strongSelf.peerId, photo: nil, mapResourceToAvatarSizes: { resource, representations in return mapResourceToAvatarSizes(postbox: postbox, resource: resource, representations: representations) }) |> deliverOnMainQueue).start(next: { result in @@ -5205,7 +5423,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD self.controller?.push(watchSettingsController(context: self.context)) case .support: let supportPeer = Promise() - supportPeer.set(supportPeerId(account: context.account)) + supportPeer.set(context.engine.peers.supportPeerId()) self.controller?.present(textAlertController(context: self.context, title: nil, text: self.presentationData.strings.Settings_FAQ_Intro, actions: [ TextAlertAction(type: .genericAction, title: presentationData.strings.Settings_FAQ_Button, action: { [weak self] in @@ -5219,6 +5437,8 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD })]), in: .window(.root)) case .faq: self.openFaq() + case .tips: + self.openTips() case .phoneNumber: if let user = self.data?.peer as? TelegramUser, let phoneNumber = user.phone { self.controller?.push(ChangePhoneNumberIntroController(context: self.context, phoneNumber: phoneNumber)) @@ -5233,16 +5453,39 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD self.controller?.push(logoutOptionsController(context: self.context, navigationController: navigationController, canAddAccounts: accounts.count + 1 < maximumNumberOfAccounts, phoneNumber: phoneNumber)) } } + case .rememberPassword: + let context = self.context + let controller = TwoFactorDataInputScreen(sharedContext: self.context.sharedContext, engine: .authorized(self.context.engine), mode: .rememberPassword, stateUpdated: { _ in + }, presentation: .modalInLargeLayout) + controller.twoStepAuthSettingsController = { configuration in + return twoStepVerificationUnlockSettingsController(context: context, mode: .access(intro: false, data: .single(TwoStepVerificationUnlockSettingsControllerData.access(configuration: TwoStepVerificationAccessConfiguration(configuration: configuration, password: nil))))) + } + controller.passwordRemembered = { + let _ = dismissServerProvidedSuggestion(account: context.account, suggestion: .validatePassword).start() + } + self.controller?.push(controller) } } private func openFaq(anchor: String? = nil) { - let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: nil)) - self.controller?.present(controller, in: .window(.root)) + let presentationData = self.presentationData + let progressSignal = Signal { [weak self] subscriber in + let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: nil)) + self?.controller?.present(controller, in: .window(.root)) + return ActionDisposable { [weak controller] in + Queue.mainQueue().async() { + controller?.dismiss() + } + } + } + |> runOn(Queue.mainQueue()) + |> delay(0.15, queue: Queue.mainQueue()) + let progressDisposable = progressSignal.start() + let _ = (self.cachedFaq.get() |> take(1) - |> deliverOnMainQueue).start(next: { [weak self, weak controller] resolvedUrl in - controller?.dismiss() + |> deliverOnMainQueue).start(next: { [weak self] resolvedUrl in + progressDisposable.dispose() if let strongSelf = self, let resolvedUrl = resolvedUrl { var resolvedUrl = resolvedUrl @@ -5257,6 +5500,20 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD }) } + private func openTips() { + let controller = OverlayStatusController(theme: self.presentationData.theme, type: .loading(cancelled: nil)) + self.controller?.present(controller, in: .window(.root)) + + let context = self.context + let navigationController = self.controller?.navigationController as? NavigationController + self.tipsPeerDisposable.set((self.context.engine.peers.resolvePeerByName(name: self.presentationData.strings.Settings_TipsUsername) |> deliverOnMainQueue).start(next: { [weak controller] peerId in + controller?.dismiss() + if let peerId = peerId, let navigationController = navigationController { + context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peerId))) + } + })) + } + fileprivate func switchToAccount(id: AccountRecordId) { self.accountsAndPeers.set(.never()) self.context.sharedContext.switchToAccount(id: id, fromSettingsController: nil, withChatListController: nil) @@ -5368,7 +5625,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD actionSheet?.dismissAnimated() if let strongSelf = self { strongSelf.headerNode.navigationButtonContainer.performAction?(.selectionDone) - let _ = deleteMessagesInteractively(account: strongSelf.context.account, messageIds: Array(messageIds), type: .forEveryone).start() + let _ = strongSelf.context.engine.messages.deleteMessagesInteractively(messageIds: Array(messageIds), type: .forEveryone).start() } })) } @@ -5385,7 +5642,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD actionSheet?.dismissAnimated() if let strongSelf = self { strongSelf.headerNode.navigationButtonContainer.performAction?(.selectionDone) - let _ = deleteMessagesInteractively(account: strongSelf.context.account, messageIds: Array(messageIds), type: .forLocalPeer).start() + let _ = strongSelf.context.engine.messages.deleteMessagesInteractively(messageIds: Array(messageIds), type: .forLocalPeer).start() } })) } @@ -5403,7 +5660,83 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD 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])) + let peerSelectionController = self.context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: self.context, filter: [.onlyWriteable, .excludeDisabled], multipleSelection: true)) + peerSelectionController.multiplePeersSelected = { [weak self, weak peerSelectionController] peers, messageText in + guard let strongSelf = self, let strongController = peerSelectionController else { + return + } + strongController.dismiss() + + for peer in peers { + var result: [EnqueueMessage] = [] + if messageText.string.count > 0 { + let inputText = convertMarkdownToAttributes(messageText) + for text in breakChatInputText(trimChatInputText(inputText)) { + if text.length != 0 { + var attributes: [MessageAttribute] = [] + let entities = generateTextEntities(text.string, enabledTypes: .all, currentEntities: generateChatInputTextEntities(text)) + if !entities.isEmpty { + attributes.append(TextEntitiesMessageAttribute(entities: entities)) + } + result.append(.message(text: text.string, attributes: attributes, mediaReference: nil, replyToMessageId: nil, localGroupingKey: nil, correlationId: nil)) + } + } + } + + result.append(contentsOf: messageIds.map { messageId -> EnqueueMessage in + return .forward(source: messageId, grouping: .auto, attributes: [], correlationId: nil) + }) + + let _ = (enqueueMessages(account: strongSelf.context.account, peerId: peer.id, messages: result) + |> deliverOnMainQueue).start(next: { 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) + }) + if strongSelf.shareStatusDisposable == nil { + strongSelf.shareStatusDisposable = MetaDisposable() + } + strongSelf.shareStatusDisposable?.set((combineLatest(signals) + |> deliverOnMainQueue).start()) + } + }) + + let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } + let text: String + var savedMessages = false + if peers.count == 1, let peerId = peers.first?.id, peerId == strongSelf.context.account.peerId { + text = messageIds.count == 1 ? presentationData.strings.Conversation_ForwardTooltip_SavedMessages_One : presentationData.strings.Conversation_ForwardTooltip_SavedMessages_Many + savedMessages = true + } else { + if peers.count == 1, let peer = peers.first { + let peerName = peer.id == strongSelf.context.account.peerId ? presentationData.strings.DialogList_SavedMessages : peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + text = messageIds.count == 1 ? presentationData.strings.Conversation_ForwardTooltip_Chat_One(peerName).0 : presentationData.strings.Conversation_ForwardTooltip_Chat_Many(peerName).0 + } else if peers.count == 2, let firstPeer = peers.first, let secondPeer = peers.last { + let firstPeerName = firstPeer.id == strongSelf.context.account.peerId ? presentationData.strings.DialogList_SavedMessages : firstPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + let secondPeerName = secondPeer.id == strongSelf.context.account.peerId ? presentationData.strings.DialogList_SavedMessages : secondPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + text = messageIds.count == 1 ? presentationData.strings.Conversation_ForwardTooltip_TwoChats_One(firstPeerName, secondPeerName).0 : presentationData.strings.Conversation_ForwardTooltip_TwoChats_Many(firstPeerName, secondPeerName).0 + } else if let peer = peers.first { + let peerName = peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + text = messageIds.count == 1 ? presentationData.strings.Conversation_ForwardTooltip_ManyChats_One(peerName, "\(peers.count - 1)").0 : presentationData.strings.Conversation_ForwardTooltip_ManyChats_Many(peerName, "\(peers.count - 1)").0 + } else { + text = "" + } + } + + strongSelf.controller?.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: savedMessages, text: text), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), in: .current) + } + } peerSelectionController.peerSelected = { [weak self, weak peerSelectionController] peer in let peerId = peer.id @@ -5415,7 +5748,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD 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: []) + return .forward(source: id, grouping: .auto, attributes: [], correlationId: nil) }) |> deliverOnMainQueue).start(next: { [weak self] messageIds in if let strongSelf = self { @@ -5434,12 +5767,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD |> 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)) - })) + |> deliverOnMainQueue).start()) } }) if let peerSelectionController = peerSelectionController { @@ -5482,7 +5810,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD if self.isSettings { if let settings = self.data?.globalSettings { - self.searchDisplayController = SearchDisplayController(presentationData: self.presentationData, mode: .list, placeholder: self.presentationData.strings.Settings_Search, hasSeparator: true, contentNode: SettingsSearchContainerNode(context: self.context, openResult: { [weak self] result in + self.searchDisplayController = SearchDisplayController(presentationData: self.presentationData, mode: .list, placeholder: self.presentationData.strings.Settings_Search, hasBackground: true, hasSeparator: true, contentNode: SettingsSearchContainerNode(context: self.context, openResult: { [weak self] result in if let strongSelf = self, let navigationController = strongSelf.controller?.navigationController as? NavigationController { result.present(strongSelf.context, navigationController, { [weak self] mode, controller in if let strongSelf = self { @@ -5512,7 +5840,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD }) } } else 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, forceTheme: nil, peerId: self.peerId, mode: .searchMembers, filters: [], searchContext: self.groupMembersSearchContext, openPeer: { [weak self] peer, participant in + self.searchDisplayController = SearchDisplayController(presentationData: self.presentationData, mode: .list, placeholder: self.presentationData.strings.Common_Search, hasBackground: true, hasSeparator: true, contentNode: ChannelMembersSearchContainerNode(context: self.context, forceTheme: nil, 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 @@ -5533,7 +5861,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD } } - 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.searchDisplayController = SearchDisplayController(presentationData: self.presentationData, mode: .list, placeholder: self.presentationData.strings.Common_Search, hasBackground: true, contentNode: ChatHistorySearchContainerNode(context: self.context, peerId: self.peerId, tagMask: tagMask, interfaceInteraction: self.chatInterfaceInteraction), cancel: { [weak self] in self?.deactivateSearch() }) } @@ -5657,7 +5985,9 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD } for sectionId in removeRegularSections { if let sectionNode = self.regularSections.removeValue(forKey: sectionId) { - sectionNode.removeFromSupernode() + transition.updateAlpha(node: sectionNode, alpha: 0.0, completion: { [weak sectionNode] _ in + sectionNode?.removeFromSupernode() + }) } } @@ -5928,8 +6258,6 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD private var canOpenAvatarByDragging = false - private let velocityKey: String = encodeText("`wfsujdbmWfmpdjuz", -1) - func scrollViewDidScroll(_ scrollView: UIScrollView) { if self.ignoreScrolling { return @@ -5938,7 +6266,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD if !self.state.isEditing { if self.canAddVelocity { self.previousVelocityM1 = self.previousVelocity - if let value = (scrollView.value(forKey: self.velocityKey) as? NSNumber)?.doubleValue { + if let value = (scrollView.value(forKey: (["_", "verticalVelocity"] as [String]).joined()) as? NSNumber)?.doubleValue { self.previousVelocity = CGFloat(value) } } @@ -6038,6 +6366,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD disabledButtonColor: baseNavigationBarPresentationData.theme.disabledButtonColor, primaryTextColor: baseNavigationBarPresentationData.theme.primaryTextColor, backgroundColor: .clear, + enableBackgroundBlur: false, separatorColor: .clear, badgeBackgroundColor: baseNavigationBarPresentationData.theme.badgeBackgroundColor, badgeStrokeColor: baseNavigationBarPresentationData.theme.badgeStrokeColor, @@ -6171,6 +6500,7 @@ public final class PeerInfoScreenImpl: ViewController, PeerInfoScreen { disabledButtonColor: baseNavigationBarPresentationData.theme.disabledButtonColor, primaryTextColor: baseNavigationBarPresentationData.theme.primaryTextColor, backgroundColor: .clear, + enableBackgroundBlur: false, separatorColor: .clear, badgeBackgroundColor: baseNavigationBarPresentationData.theme.badgeBackgroundColor, badgeStrokeColor: baseNavigationBarPresentationData.theme.badgeStrokeColor, @@ -6179,8 +6509,8 @@ public final class PeerInfoScreenImpl: ViewController, PeerInfoScreen { if isSettings { let activeSessionsContextAndCountSignal = deferred { () -> Signal<(ActiveSessionsContext, Int, WebSessionsContext)?, NoError> in - let activeSessionsContext = ActiveSessionsContext(account: context.account) - let webSessionsContext = WebSessionsContext(account: context.account) + let activeSessionsContext = context.engine.privacy.activeSessions() + let webSessionsContext = context.engine.privacy.webSessions() let otherSessionCount = activeSessionsContext.state |> map { state -> Int in return state.sessions.filter({ !$0.isCurrent }).count @@ -6336,14 +6666,16 @@ public final class PeerInfoScreenImpl: ViewController, PeerInfoScreen { icon = UIImage(bundleImageName: "Chat List/Tabs/IconSettings") } - let tabBarItem: Signal<(String, UIImage?, UIImage?, String?), NoError> = combineLatest(queue: .mainQueue(), self.context.sharedContext.presentationData, notificationsAuthorizationStatus.get(), notificationsWarningSuppressed.get(), accountTabBarAvatar, accountTabBarAvatarBadge) - |> map { presentationData, notificationsAuthorizationStatus, notificationsWarningSuppressed, accountTabBarAvatar, accountTabBarAvatarBadge -> (String, UIImage?, UIImage?, String?) in + let tabBarItem: Signal<(String, UIImage?, UIImage?, String?), NoError> = combineLatest(queue: .mainQueue(), self.context.sharedContext.presentationData, notificationsAuthorizationStatus.get(), notificationsWarningSuppressed.get(), getServerProvidedSuggestions(account: self.context.account), accountTabBarAvatar, accountTabBarAvatarBadge) + |> map { presentationData, notificationsAuthorizationStatus, notificationsWarningSuppressed, suggestions, accountTabBarAvatar, accountTabBarAvatarBadge -> (String, UIImage?, UIImage?, String?) in let notificationsWarning = shouldDisplayNotificationsPermissionWarning(status: notificationsAuthorizationStatus, suppressed: notificationsWarningSuppressed) + let phoneNumberWarning = suggestions.contains(.validatePhoneNumber) + let passwordWarning = suggestions.contains(.validatePassword) var otherAccountsBadge: String? if accountTabBarAvatarBadge > 0 { otherAccountsBadge = compactNumericCountString(Int(accountTabBarAvatarBadge), decimalSeparator: presentationData.dateTimeFormat.decimalSeparator) } - return (presentationData.strings.Settings_Title, accountTabBarAvatar?.0 ?? icon, accountTabBarAvatar?.1 ?? icon, notificationsWarning ? "!" : otherAccountsBadge) + return (presentationData.strings.Settings_Title, accountTabBarAvatar?.0 ?? icon, accountTabBarAvatar?.1 ?? icon, notificationsWarning || phoneNumberWarning || passwordWarning ? "!" : otherAccountsBadge) } self.tabBarItemDisposable = (tabBarItem |> deliverOnMainQueue).start(next: { [weak self] title, image, selectedImage, badgeValue in @@ -6440,10 +6772,30 @@ public final class PeerInfoScreenImpl: ViewController, PeerInfoScreen { } } + private func dismissAllTooltips() { + self.window?.forEachController({ controller in + if let controller = controller as? UndoOverlayController, !controller.keepOnParentDismissal { + controller.dismissWithCommitAction() + } + }) + self.forEachController({ controller in + if let controller = controller as? UndoOverlayController, !controller.keepOnParentDismissal { + controller.dismissWithCommitAction() + } + return true + }) + } + + override public func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + + self.dismissAllTooltips() + } + override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { super.containerLayoutUpdated(layout, transition: transition) - let navigationHeight = self.isSettings ? (self.navigationBar?.frame.height ?? 0.0) : self.navigationHeight + let navigationHeight = self.isSettings ? (self.navigationBar?.frame.height ?? 0.0) : self.navigationLayout(layout: layout).navigationFrame.maxY self.validLayout = (layout, navigationHeight) self.controllerNode.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: transition) @@ -6543,7 +6895,7 @@ final class PeerInfoNavigationSourceTag { 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 @@ -6585,6 +6937,8 @@ private final class PeerInfoNavigationTransitionNode: ASDisplayNode, CustomNavig bottomNavigationBar.isHidden = true if let topNavigationBar = self.topNavigationBar, let bottomNavigationBar = self.bottomNavigationBar { + self.addSubnode(bottomNavigationBar.additionalContentNode) + if let previousBackButtonArrow = bottomNavigationBar.makeTransitionBackArrowNode(accentColor: self.presentationData.theme.rootController.navigationBar.accentTextColor) { self.previousBackButtonArrow = previousBackButtonArrow self.addSubnode(previousBackButtonArrow) @@ -6667,8 +7021,9 @@ private final class PeerInfoNavigationTransitionNode: ASDisplayNode, CustomNavig 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) + var topHeight = topNavigationBar.backgroundNode.bounds.height if let (layout, _) = self.screenNode.validLayout { - let _ = self.headerNode.update(width: layout.size.width, containerHeight: layout.size.height, containerInset: layout.safeInsets.left, statusBarHeight: layout.statusBarHeight ?? 0.0, navigationHeight: topNavigationBar.bounds.height, isModalOverlay: layout.isModalOverlay, isMediaOnly: false, 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, isSecretChat: self.screenNode.peerId.namespace == Namespaces.Peer.SecretChat, isContact: self.screenNode.data?.isContact ?? false, isSettings: self.screenNode.isSettings, state: self.screenNode.state, transition: transition, additive: false) + topHeight = self.headerNode.update(width: layout.size.width, containerHeight: layout.size.height, containerInset: layout.safeInsets.left, statusBarHeight: layout.statusBarHeight ?? 0.0, navigationHeight: topNavigationBar.bounds.height, isModalOverlay: layout.isModalOverlay, isMediaOnly: false, 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, isSecretChat: self.screenNode.peerId.namespace == Namespaces.Peer.SecretChat, isContact: self.screenNode.data?.isContact ?? false, isSettings: self.screenNode.isSettings, 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 @@ -6688,6 +7043,21 @@ private final class PeerInfoNavigationTransitionNode: ASDisplayNode, CustomNavig transition.updateAlpha(node: previousStatusNode, alpha: fraction) transition.updateAlpha(node: self.headerNode.navigationButtonContainer, alpha: (1.0 - fraction)) + + if case let .animated(duration, _) = transition, (bottomNavigationBar.additionalContentNode.alpha.isZero || bottomNavigationBar.additionalContentNode.alpha == 1.0) { + bottomNavigationBar.additionalContentNode.alpha = fraction + if fraction.isZero { + bottomNavigationBar.additionalContentNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15) + } else { + transition.updateAlpha(node: bottomNavigationBar.additionalContentNode, alpha: fraction) + } + } else { + transition.updateAlpha(node: bottomNavigationBar.additionalContentNode, alpha: fraction) + } + + let bottomHeight = bottomNavigationBar.backgroundNode.bounds.height + + transition.updateSublayerTransformOffset(layer: bottomNavigationBar.additionalContentNode.layer, offset: CGPoint(x: 0.0, y: (1.0 - fraction) * (topHeight - bottomHeight))) } } @@ -6695,6 +7065,14 @@ private final class PeerInfoNavigationTransitionNode: ASDisplayNode, CustomNavig guard let topNavigationBar = self.topNavigationBar, let bottomNavigationBar = self.bottomNavigationBar else { return } + + topNavigationBar.additionalContentNode.alpha = 1.0 + ContainedViewLayoutTransition.immediate.updateSublayerTransformOffset(layer: topNavigationBar.additionalContentNode.layer, offset: CGPoint()) + topNavigationBar.reattachAdditionalContentNode() + + bottomNavigationBar.additionalContentNode.alpha = 1.0 + ContainedViewLayoutTransition.immediate.updateSublayerTransformOffset(layer: bottomNavigationBar.additionalContentNode.layer, offset: CGPoint()) + bottomNavigationBar.reattachAdditionalContentNode() topNavigationBar.isHidden = false bottomNavigationBar.isHidden = false @@ -6703,14 +7081,6 @@ private final class PeerInfoNavigationTransitionNode: ASDisplayNode, CustomNavig } } -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? @@ -6859,14 +7229,14 @@ func presentAddMembers(context: AccountContext, parentController: ViewController 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) + return context.peerChannelMemberCategoriesContextsManager.addMember(engine: context.engine, peerId: groupPeer.id, memberId: memberId) |> map { _ -> Void in } |> `catch` { _ -> Signal in return .complete() } } else { - return addGroupMember(account: context.account, peerId: groupPeer.id, memberId: memberId) + return context.engine.peers.addGroupMember(peerId: groupPeer.id, memberId: memberId) |> deliverOnMainQueue |> `catch` { error -> Signal in switch error { @@ -6897,7 +7267,7 @@ func presentAddMembers(context: AccountContext, parentController: ViewController }) return .complete() case .groupFull: - let signal = convertGroupToSupergroup(account: context.account, peerId: groupPeer.id) + let signal = context.engine.peers.convertGroupToSupergroup(peerId: groupPeer.id) |> map(Optional.init) |> `catch` { error -> Signal in switch error { @@ -6914,7 +7284,7 @@ func presentAddMembers(context: AccountContext, parentController: ViewController guard let upgradedPeerId = upgradedPeerId else { return .single(nil) } - return context.peerChannelMemberCategoriesContextsManager.addMember(account: context.account, peerId: upgradedPeerId, memberId: memberId) + return context.peerChannelMemberCategoriesContextsManager.addMember(engine: context.engine, peerId: upgradedPeerId, memberId: memberId) |> `catch` { _ -> Signal in return .complete() } @@ -6950,11 +7320,11 @@ func presentAddMembers(context: AccountContext, parentController: ViewController |> castError(AddChannelMemberError.self) |> mapToSignal { view -> Signal in if memberIds.count == 1 { - return context.peerChannelMemberCategoriesContextsManager.addMember(account: context.account, peerId: groupPeer.id, memberId: memberIds[0]) + return context.peerChannelMemberCategoriesContextsManager.addMember(engine: context.engine, peerId: groupPeer.id, memberId: memberIds[0]) |> map { _ -> Void in } } else { - return context.peerChannelMemberCategoriesContextsManager.addMembers(account: context.account, peerId: groupPeer.id, memberIds: memberIds) |> map { _ in + return context.peerChannelMemberCategoriesContextsManager.addMembers(engine: context.engine, peerId: groupPeer.id, memberIds: memberIds) |> map { _ in } } } @@ -6968,8 +7338,8 @@ func presentAddMembers(context: AccountContext, parentController: ViewController parentController?.push(contactsController) if let contactsController = contactsController as? ContactSelectionController { selectAddMemberDisposable.set((contactsController.result - |> deliverOnMainQueue).start(next: { [weak contactsController] memberPeer in - guard let (memberPeer, _) = memberPeer else { + |> deliverOnMainQueue).start(next: { [weak contactsController] result in + guard let (peers, _) = result, let memberPeer = peers.first else { return } @@ -7019,7 +7389,7 @@ func presentAddMembers(context: AccountContext, parentController: ViewController } contactsController?.dismiss() - },completed: { + }, completed: { contactsController?.dismiss() })) })) diff --git a/submodules/TelegramUI/Sources/PeerMediaCollectionSectionsNode.swift b/submodules/TelegramUI/Sources/PeerMediaCollectionSectionsNode.swift index 536bab0372..983769ee0e 100644 --- a/submodules/TelegramUI/Sources/PeerMediaCollectionSectionsNode.swift +++ b/submodules/TelegramUI/Sources/PeerMediaCollectionSectionsNode.swift @@ -35,7 +35,7 @@ final class PeerMediaCollectionSectionsNode: ASDisplayNode { super.init() - self.backgroundColor = self.theme.rootController.navigationBar.backgroundColor + self.backgroundColor = self.theme.rootController.navigationBar.opaqueBackgroundColor self.segmentedControlNode.selectedIndexChanged = { [weak self] index in self?.indexUpdated?(index) @@ -57,7 +57,7 @@ final class PeerMediaCollectionSectionsNode: ASDisplayNode { if interfaceState.theme !== self.theme { self.theme = interfaceState.theme self.separatorNode.backgroundColor = self.theme.rootController.navigationBar.separatorColor - self.backgroundColor = self.theme.rootController.navigationBar.backgroundColor + self.backgroundColor = self.theme.rootController.navigationBar.opaqueBackgroundColor self.segmentedControlNode.updateTheme(SegmentedControlTheme(theme: self.theme)) } diff --git a/submodules/TelegramUI/Sources/PeerMessagesMediaPlaylist.swift b/submodules/TelegramUI/Sources/PeerMessagesMediaPlaylist.swift index 28f65a474c..631e63ce85 100644 --- a/submodules/TelegramUI/Sources/PeerMessagesMediaPlaylist.swift +++ b/submodules/TelegramUI/Sources/PeerMessagesMediaPlaylist.swift @@ -59,7 +59,7 @@ final class MessageMediaPlaylistItem: SharedMediaPlaylistItem { let message: Message init(message: Message) { - self.id = PeerMessagesMediaPlaylistItemId(messageId: message.id) + self.id = PeerMessagesMediaPlaylistItemId(messageId: message.id, messageIndex: message.index) self.message = message } @@ -808,7 +808,7 @@ final class PeerMessagesMediaPlaylist: SharedMediaPlaylist { default: break } - let _ = markMessageContentAsConsumedInteractively(postbox: self.context.account.postbox, messageId: item.message.id).start() + let _ = self.context.engine.messages.markMessageContentAsConsumedInteractively(messageId: item.message.id).start() } } } diff --git a/submodules/TelegramUI/Sources/PeerSelectionController.swift b/submodules/TelegramUI/Sources/PeerSelectionController.swift index bb319bd801..8a3a59fce2 100644 --- a/submodules/TelegramUI/Sources/PeerSelectionController.swift +++ b/submodules/TelegramUI/Sources/PeerSelectionController.swift @@ -20,6 +20,7 @@ public final class PeerSelectionControllerImpl: ViewController, PeerSelectionCon private var customTitle: String? public var peerSelected: ((Peer) -> Void)? + public var multiplePeersSelected: (([Peer], NSAttributedString) -> Void)? private let filter: ChatListNodePeersFilter private let attemptSelection: ((Peer) -> Void)? @@ -124,6 +125,10 @@ public final class PeerSelectionControllerImpl: ViewController, PeerSelectionCon self?.activateSearch() }) self.navigationBar?.setContentNode(self.searchContentNode, animated: false) + + if params.multipleSelection { + self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Select, style: .plain, target: self, action: #selector(self.beginSelection)) + } } required public init(coder aDecoder: NSCoder) { @@ -152,6 +157,10 @@ public final class PeerSelectionControllerImpl: ViewController, PeerSelectionCon self.peerSelectionNode.navigationBar = self.navigationBar + self.peerSelectionNode.requestSend = { [weak self] peers, text in + self?.multiplePeersSelected?(peers, text) + } + self.peerSelectionNode.requestDeactivateSearch = { [weak self] in self?.deactivateSearch() } @@ -217,7 +226,12 @@ public final class PeerSelectionControllerImpl: ViewController, PeerSelectionCon override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { super.containerLayoutUpdated(layout, transition: transition) - self.peerSelectionNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationInsetHeight, actualNavigationBarHeight: self.navigationHeight, transition: transition) + self.peerSelectionNode.containerLayoutUpdated(layout, navigationBarHeight: self.cleanNavigationHeight, actualNavigationBarHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition) + } + + @objc private func beginSelection() { + self.navigationItem.rightBarButtonItem = nil + self.peerSelectionNode.beginSelection() } @objc func cancelPressed() { diff --git a/submodules/TelegramUI/Sources/PeerSelectionControllerNode.swift b/submodules/TelegramUI/Sources/PeerSelectionControllerNode.swift index 63b2298c9f..39e7fdb50e 100644 --- a/submodules/TelegramUI/Sources/PeerSelectionControllerNode.swift +++ b/submodules/TelegramUI/Sources/PeerSelectionControllerNode.swift @@ -21,6 +21,9 @@ final class PeerSelectionControllerNode: ASDisplayNode { private let filter: ChatListNodePeersFilter private let hasGlobalSearch: Bool + private var presentationInterfaceState: ChatPresentationInterfaceState + private var interfaceInteraction: ChatPanelInterfaceInteraction? + var inProgress: Bool = false { didSet { @@ -29,10 +32,12 @@ final class PeerSelectionControllerNode: ASDisplayNode { var navigationBar: NavigationBar? - private let toolbarBackgroundNode: ASDisplayNode? + private let toolbarBackgroundNode: NavigationBackgroundNode? private let toolbarSeparatorNode: ASDisplayNode? private let segmentedControlNode: SegmentedControlNode? + private var textInputPanelNode: PeerSelectionTextInputPanelNode? + var contactListNode: ContactListNode? let chatListNode: ChatListNode @@ -51,6 +56,7 @@ final class PeerSelectionControllerNode: ASDisplayNode { var requestOpenDisabledPeer: ((Peer) -> Void)? var requestOpenPeerFromSearch: ((Peer) -> Void)? var requestOpenMessageFromSearch: ((Peer, MessageId) -> Void)? + var requestSend: (([Peer], NSAttributedString) -> Void)? private var presentationData: PresentationData private var presentationDataDisposable: Disposable? @@ -70,9 +76,10 @@ final class PeerSelectionControllerNode: ASDisplayNode { let presentationData = context.sharedContext.currentPresentationData.with { $0 } self.presentationData = presentationData + self.presentationInterfaceState = ChatPresentationInterfaceState(chatWallpaper: .builtin(WallpaperSettings()), theme: self.presentationData.theme, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameDisplayOrder: self.presentationData.nameDisplayOrder, limitsConfiguration: self.context.currentLimitsConfiguration.with { $0 }, fontSize: self.presentationData.chatFontSize, bubbleCorners: self.presentationData.chatBubbleCorners, accountPeerId: self.context.account.peerId, mode: .standard(previewing: false), chatLocation: .peer(PeerId(0)), subject: nil, peerNearbyData: nil, greetingData: nil, pendingUnpinnedAllMessages: false, activeGroupCallInfo: nil, hasActiveGroupCall: false, importState: nil) + if hasChatListSelector && hasContactSelector { - self.toolbarBackgroundNode = ASDisplayNode() - self.toolbarBackgroundNode?.backgroundColor = self.presentationData.theme.rootController.navigationBar.backgroundColor + self.toolbarBackgroundNode = NavigationBackgroundNode(color: self.presentationData.theme.rootController.navigationBar.blurredBackgroundColor) self.toolbarSeparatorNode = ASDisplayNode() self.toolbarSeparatorNode?.backgroundColor = self.presentationData.theme.rootController.navigationBar.separatorColor @@ -94,7 +101,7 @@ final class PeerSelectionControllerNode: ASDisplayNode { chatListcategories.append(ChatListNodeAdditionalCategory(id: 0, icon: PresentationResourcesItemList.createGroupIcon(self.presentationData.theme), title: self.presentationData.strings.PeerSelection_ImportIntoNewGroup, appearance: .action)) } - self.chatListNode = ChatListNode(context: context, groupId: .root, previewing: false, fillPreloadItems: false, mode: .peers(filter: filter, isSelecting: false, additionalCategories: chatListcategories, chatListFilters: nil), theme: self.presentationData.theme, fontSize: presentationData.listsFontSize, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameSortOrder: presentationData.nameSortOrder, nameDisplayOrder: presentationData.nameDisplayOrder, disableAnimations: presentationData.disableAnimations) + self.chatListNode = ChatListNode(context: context, groupId: .root, previewing: false, fillPreloadItems: false, mode: .peers(filter: filter, isSelecting: false, additionalCategories: chatListcategories, chatListFilters: nil), theme: self.presentationData.theme, fontSize: presentationData.listsFontSize, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameSortOrder: presentationData.nameSortOrder, nameDisplayOrder: presentationData.nameDisplayOrder, disableAnimations: true) super.init() @@ -108,6 +115,9 @@ final class PeerSelectionControllerNode: ASDisplayNode { self.backgroundColor = self.presentationData.theme.chatList.backgroundColor + self.chatListNode.selectionCountChanged = { [weak self] count in + self?.textInputPanelNode?.updateSendButtonEnabled(count > 0, animated: true) + } self.chatListNode.accessibilityPageScrolledString = { row, count in return presentationData.strings.VoiceOver_ScrollStatus(row, count).0 } @@ -164,6 +174,112 @@ final class PeerSelectionControllerNode: ASDisplayNode { if !hasChatListSelector && hasContactSelector { self.indexChanged(1) } + + self.interfaceInteraction = ChatPanelInterfaceInteraction(setupReplyMessage: { _, _ in + }, setupEditMessage: { _, _ in + }, beginMessageSelection: { _, _ in + }, deleteSelectedMessages: { + }, reportSelectedMessages: { + }, reportMessages: { _, _ in + }, blockMessageAuthor: { _, _ in + }, deleteMessages: { _, _, f in + f(.default) + }, forwardSelectedMessages: { + }, forwardCurrentForwardMessages: { + }, forwardMessages: { _ in + }, shareSelectedMessages: { + }, updateTextInputStateAndMode: { [weak self] f in + if let strongSelf = self { + strongSelf.updateChatPresentationInterfaceState(animated: true, { state in + let (updatedState, updatedMode) = f(state.interfaceState.effectiveInputState, state.inputMode) + return state.updatedInterfaceState { interfaceState in + return interfaceState.withUpdatedEffectiveInputState(updatedState) + }.updatedInputMode({ _ in updatedMode }) + }) + } + }, updateInputModeAndDismissedButtonKeyboardMessageId: { [weak self] f in + if let strongSelf = self { + strongSelf.updateChatPresentationInterfaceState(animated: true, { + let (updatedInputMode, updatedClosedButtonKeyboardMessageId) = f($0) + return $0.updatedInputMode({ _ in return updatedInputMode }).updatedInterfaceState({ + $0.withUpdatedMessageActionsState({ value in + var value = value + value.closedButtonKeyboardMessageId = updatedClosedButtonKeyboardMessageId + return value + }) + }) + }) + } + }, openStickers: { + }, editMessage: { + }, beginMessageSearch: { _, _ in + }, dismissMessageSearch: { + }, updateMessageSearch: { _ in + }, openSearchResults: { + }, navigateMessageSearch: { _ in + }, openCalendarSearch: { + }, toggleMembersSearch: { _ in + }, navigateToMessage: { _, _, _, _ in + }, navigateToChat: { _ in + }, navigateToProfile: { _ in + }, openPeerInfo: { + }, togglePeerNotifications: { + }, sendContextResult: { _, _, _, _ in + return false + }, sendBotCommand: { _, _ in + }, sendBotStart: { _ in + }, botSwitchChatWithPayload: { _, _ in + }, beginMediaRecording: { _ in + }, finishMediaRecording: { _ in + }, stopMediaRecording: { + }, lockMediaRecording: { + }, deleteRecordedMedia: { + }, sendRecordedMedia: { _ in + }, displayRestrictedInfo: { _, _ in + }, displayVideoUnmuteTip: { _ in + }, switchMediaRecordingMode: { + }, setupMessageAutoremoveTimeout: { + }, sendSticker: { _, _, _, _ in + return false + }, unblockPeer: { + }, pinMessage: { _, _ in + }, unpinMessage: { _, _, _ in + }, unpinAllMessages: { + }, openPinnedList: { _ in + }, shareAccountContact: { + }, reportPeer: { + }, presentPeerContact: { + }, dismissReportPeer: { + }, deleteChat: { + }, beginCall: { _ in + }, 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: { + }, openPeersNearby: { + }, displaySearchResultsTooltip: { _, _ in + }, unarchivePeer: { + }, scrollToTop: { + }, viewReplies: { _, _ in + }, activatePinnedListPreview: { _, _ in + }, joinGroupCall: { _ in + }, presentInviteMembers: { + }, presentGigagroupHelp: { + }, editMessageMedia: { _, _ in + }, updateShowCommands: { _ in }, statuses: nil) self.readyValue.set(self.chatListNode.ready) } @@ -172,12 +288,95 @@ final class PeerSelectionControllerNode: ASDisplayNode { self.presentationDataDisposable?.dispose() } + private func updateChatPresentationInterfaceState(animated: Bool = true, _ f: (ChatPresentationInterfaceState) -> ChatPresentationInterfaceState, completion: @escaping (ContainedViewLayoutTransition) -> Void = { _ in }) { + self.updateChatPresentationInterfaceState(transition: animated ? .animated(duration: 0.4, curve: .spring) : .immediate, f, completion: completion) + } + + private func updateChatPresentationInterfaceState(transition: ContainedViewLayoutTransition, _ f: (ChatPresentationInterfaceState) -> ChatPresentationInterfaceState, completion externalCompletion: @escaping (ContainedViewLayoutTransition) -> Void = { _ in }) { + let presentationInterfaceState = f(self.presentationInterfaceState) + let updateInputTextState = self.presentationInterfaceState.interfaceState.effectiveInputState != presentationInterfaceState.interfaceState.effectiveInputState + + self.presentationInterfaceState = presentationInterfaceState + + if let textInputPanelNode = self.textInputPanelNode, updateInputTextState { + textInputPanelNode.updateInputTextState(presentationInterfaceState.interfaceState.effectiveInputState, animated: transition.isAnimated) + } + + if let (layout, navigationBarHeight, actualNavigationBarHeight) = self.containerLayout { + self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, actualNavigationBarHeight: actualNavigationBarHeight, transition: transition) + } + } + + func beginSelection() { + if let _ = self.textInputPanelNode { + } else { + let textInputPanelNode = PeerSelectionTextInputPanelNode(presentationInterfaceState: self.presentationInterfaceState, presentController: { [weak self] c in self?.present(c, nil) }) + textInputPanelNode.interfaceInteraction = self.interfaceInteraction + textInputPanelNode.sendMessage = { [weak self] in + guard let strongSelf = self else { + return + } + + if strongSelf.contactListActive { + strongSelf.contactListNode?.multipleSelection = true + let selectedContactPeers = strongSelf.contactListNode?.selectedPeers ?? [] + let effectiveInputText = strongSelf.presentationInterfaceState.interfaceState.composeInputState.inputText + var selectedPeers: [Peer] = [] + for contactPeer in selectedContactPeers { + if case let .peer(peer, _, _) = contactPeer { + selectedPeers.append(peer) + } + } + if !selectedPeers.isEmpty { + strongSelf.requestSend?(selectedPeers, effectiveInputText) + } + } else { + var selectedPeerIds: [PeerId] = [] + var selectedPeerMap: [PeerId: Peer] = [:] + strongSelf.chatListNode.updateState { state in + selectedPeerIds = Array(state.selectedPeerIds) + selectedPeerMap = state.selectedPeerMap + return state + } + if !selectedPeerIds.isEmpty { + let effectiveInputText = strongSelf.presentationInterfaceState.interfaceState.composeInputState.inputText + var selectedPeers: [Peer] = [] + for peerId in selectedPeerIds { + if let peer = selectedPeerMap[peerId] { + selectedPeers.append(peer) + } + } + strongSelf.requestSend?(selectedPeers, effectiveInputText) + } + } + } + self.addSubnode(textInputPanelNode) + self.textInputPanelNode = textInputPanelNode + + if let (layout, navigationBarHeight, actualNavigationBarHeight) = self.containerLayout { + self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, actualNavigationBarHeight: actualNavigationBarHeight, transition: .animated(duration: 0.3, curve: .spring)) + } + } + + if self.contactListActive { + self.contactListNode?.updateSelectionState({ _ in + return ContactListNodeGroupSelectionState() + }) + } else { + self.chatListNode.updateState { state in + var state = state + state.editing = true + return state + } + } + } + private func updateThemeAndStrings() { self.backgroundColor = self.presentationData.theme.chatList.backgroundColor self.searchDisplayController?.updatePresentationData(self.presentationData) - 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.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: true) - self.toolbarBackgroundNode?.backgroundColor = self.presentationData.theme.rootController.navigationBar.backgroundColor + self.toolbarBackgroundNode?.updateColor(color: self.presentationData.theme.rootController.navigationBar.blurredBackgroundColor, transition: .immediate) self.toolbarSeparatorNode?.backgroundColor = self.presentationData.theme.rootController.navigationBar.separatorColor self.segmentedControlNode?.updateTheme(SegmentedControlTheme(theme: self.presentationData.theme)) } @@ -186,19 +385,48 @@ final class PeerSelectionControllerNode: ASDisplayNode { self.containerLayout = (layout, navigationBarHeight, actualNavigationBarHeight) let cleanInsets = layout.insets(options: []) + var insets = layout.insets(options: [.input]) var toolbarHeight: CGFloat = cleanInsets.bottom - + var textPanelHeight: CGFloat? + + if let textInputPanelNode = self.textInputPanelNode { + var panelTransition = transition + if textInputPanelNode.frame.width.isZero { + panelTransition = .immediate + } + var panelHeight = textInputPanelNode.updateLayout(width: layout.size.width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, additionalSideInsets: UIEdgeInsets(), maxHeight: layout.size.height / 2.0, isSecondary: false, transition: panelTransition, interfaceState: self.presentationInterfaceState, metrics: layout.metrics) + if self.searchDisplayController == nil { + panelHeight += insets.bottom + } else { + panelHeight += cleanInsets.bottom + } + textPanelHeight = panelHeight + + let panelFrame = CGRect(x: 0.0, y: layout.size.height - panelHeight, width: layout.size.width, height: panelHeight) + if textInputPanelNode.frame.width.isZero { + var initialPanelFrame = panelFrame + initialPanelFrame.origin.y = layout.size.height + textInputPanelNode.frame = initialPanelFrame + } + transition.updateFrame(node: textInputPanelNode, frame: panelFrame) + } + if let segmentedControlNode = self.segmentedControlNode, let toolbarBackgroundNode = self.toolbarBackgroundNode, let toolbarSeparatorNode = self.toolbarSeparatorNode { - toolbarHeight += 44.0 + if let textPanelHeight = textPanelHeight { + toolbarHeight = textPanelHeight + } else { + toolbarHeight += 44.0 + } transition.updateFrame(node: toolbarBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - toolbarHeight), size: CGSize(width: layout.size.width, height: toolbarHeight))) + toolbarBackgroundNode.update(size: toolbarBackgroundNode.bounds.size, transition: transition) transition.updateFrame(node: toolbarSeparatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - toolbarHeight), size: CGSize(width: layout.size.width, height: UIScreenPixel))) let controlSize = segmentedControlNode.updateLayout(.sizeToFit(maximumWidth: layout.size.width, minimumWidth: 200.0, height: 32.0), transition: transition) - transition.updateFrame(node: segmentedControlNode, frame: CGRect(origin: CGPoint(x: floor((layout.size.width - controlSize.width) / 2.0), y: layout.size.height - toolbarHeight + floor((44.0 - controlSize.height) / 2.0)), size: controlSize)) + let controlOrigin = layout.size.height - (textPanelHeight == nil ? toolbarHeight : 0.0) + floor((44.0 - controlSize.height) / 2.0) + transition.updateFrame(node: segmentedControlNode, frame: CGRect(origin: CGPoint(x: floor((layout.size.width - controlSize.width) / 2.0), y: controlOrigin), size: controlSize)) } - - var insets = layout.insets(options: [.input]) + insets.top += navigationBarHeight insets.bottom = max(insets.bottom, cleanInsets.bottom + 44.0) insets.left += layout.safeInsets.left @@ -239,7 +467,45 @@ final class PeerSelectionControllerNode: ASDisplayNode { if self.chatListNode.supernode != nil { self.searchDisplayController = SearchDisplayController(presentationData: self.presentationData, contentNode: ChatListSearchContainerNode(context: self.context, filter: self.filter, groupId: .root, displaySearchFilters: false, openPeer: { [weak self] peer, _ in - if let requestOpenPeerFromSearch = self?.requestOpenPeerFromSearch { + guard let strongSelf = self else { + return + } + var updated = false + var count = 0 + strongSelf.chatListNode.updateState { state in + if state.editing { + updated = true + var state = state + var foundPeers = state.foundPeers + var selectedPeerMap = state.selectedPeerMap + selectedPeerMap[peer.id] = peer + var exists = false + for foundPeer in foundPeers { + if peer.id == foundPeer.id { + exists = true + break + } + } + if !exists { + foundPeers.insert(peer, at: 0) + } + if state.selectedPeerIds.contains(peer.id) { + state.selectedPeerIds.remove(peer.id) + } else { + state.selectedPeerIds.insert(peer.id) + } + state.foundPeers = foundPeers + state.selectedPeerMap = selectedPeerMap + count = state.selectedPeerIds.count + return state + } else { + return state + } + } + if updated { + strongSelf.textInputPanelNode?.updateSendButtonEnabled(count > 0, animated: true) + strongSelf.requestDeactivateSearch?() + } else if let requestOpenPeerFromSearch = strongSelf.requestOpenPeerFromSearch { requestOpenPeerFromSearch(peer) } }, openDisabledPeer: { [weak self] peer in @@ -276,17 +542,48 @@ final class PeerSelectionControllerNode: ASDisplayNode { } self.searchDisplayController = SearchDisplayController(presentationData: self.presentationData, contentNode: ContactsSearchContainerNode(context: self.context, onlyWriteable: true, categories: categories, addContact: nil, openPeer: { [weak self] peer in if let strongSelf = self { - switch peer { - case let .peer(peer, _, _): - let _ = (strongSelf.context.account.postbox.transaction { transaction -> Peer? in - return transaction.getPeer(peer.id) - } |> deliverOnMainQueue).start(next: { peer in - if let strongSelf = self, let peer = peer { - strongSelf.requestOpenPeerFromSearch?(peer) + var updated = false + var count = 0 + strongSelf.contactListNode?.updateSelectionState { state -> ContactListNodeGroupSelectionState? in + if let state = state { + updated = true + var foundPeers = state.foundPeers + var selectedPeerMap = state.selectedPeerMap + selectedPeerMap[peer.id] = peer + var exists = false + for foundPeer in foundPeers { + if peer.id == foundPeer.id { + exists = true + break } - }) - case .deviceContact: - break + } + if !exists { + foundPeers.insert(peer, at: 0) + } + let updatedState = state.withToggledPeerId(peer.id).withFoundPeers(foundPeers).withSelectedPeerMap(selectedPeerMap) + count = updatedState.selectedPeerIndices.count + return updatedState + } else { + return nil + } + } + + if updated { + strongSelf.textInputPanelNode?.updateSendButtonEnabled(count > 0, animated: true) + strongSelf.requestDeactivateSearch?() + } else { + switch peer { + case let .peer(peer, _, _): + let _ = (strongSelf.context.account.postbox.transaction { transaction -> Peer? in + return transaction.getPeer(peer.id) + } |> deliverOnMainQueue).start(next: { peer in + if let strongSelf = self, let peer = peer { + strongSelf.requestOpenPeerFromSearch?(peer) + } + }) + case .deviceContact: + break + } } } }, contextAction: nil), cancel: { [weak self] in @@ -342,6 +639,11 @@ final class PeerSelectionControllerNode: ASDisplayNode { let contactListNode = ContactListNode(context: context, presentation: .single(.natural(options: [], includeChatList: false))) self.contactListNode = contactListNode contactListNode.enableUpdates = true + contactListNode.selectionStateUpdated = { [weak self] selectionState in + if let strongSelf = self { + strongSelf.textInputPanelNode?.updateSendButtonEnabled((selectionState?.selectedPeerIndices.count ?? 0) > 0, animated: true) + } + } contactListNode.activateSearch = { [weak self] in self?.requestActivateSearch?() } @@ -394,7 +696,7 @@ final class PeerSelectionControllerNode: ASDisplayNode { } else if let contactListNode = self.contactListNode { contactListNode.enableUpdates = false - self.insertSubnode(chatListNode, aboveSubnode: contactListNode) + self.insertSubnode(self.chatListNode, aboveSubnode: contactListNode) contactListNode.removeFromSupernode() } } diff --git a/submodules/TelegramUI/Sources/PeerSelectionTextInputPanelNode.swift b/submodules/TelegramUI/Sources/PeerSelectionTextInputPanelNode.swift new file mode 100644 index 0000000000..08e7a34f55 --- /dev/null +++ b/submodules/TelegramUI/Sources/PeerSelectionTextInputPanelNode.swift @@ -0,0 +1,935 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import SwiftSignalKit +import Postbox +import TelegramCore +import SyncCore +import MobileCoreServices +import TelegramPresentationData +import TextFormat +import AccountContext +import TouchDownGesture +import ActivityIndicator +import Speak + +private let counterFont = Font.with(size: 14.0, design: .regular, traits: [.monospacedNumbers]) +private let minInputFontSize = chatTextInputMinFontSize + +private func calclulateTextFieldMinHeight(_ presentationInterfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics) -> CGFloat { + let baseFontSize = max(minInputFontSize, presentationInterfaceState.fontSize.baseDisplaySize) + var result: CGFloat + if baseFontSize.isEqual(to: 26.0) { + result = 42.0 + } else if baseFontSize.isEqual(to: 23.0) { + result = 38.0 + } else if baseFontSize.isEqual(to: 17.0) { + result = 31.0 + } else if baseFontSize.isEqual(to: 19.0) { + result = 33.0 + } else if baseFontSize.isEqual(to: 21.0) { + result = 35.0 + } else { + result = 31.0 + } + + if case .regular = metrics.widthClass { + result = max(33.0, result) + } + + return result +} + +private func calculateTextFieldRealInsets(_ presentationInterfaceState: ChatPresentationInterfaceState) -> UIEdgeInsets { + let baseFontSize = max(minInputFontSize, presentationInterfaceState.fontSize.baseDisplaySize) + let top: CGFloat + let bottom: CGFloat + if baseFontSize.isEqual(to: 14.0) { + top = 2.0 + bottom = 1.0 + } else if baseFontSize.isEqual(to: 15.0) { + top = 1.0 + bottom = 1.0 + } else if baseFontSize.isEqual(to: 16.0) { + top = 0.5 + bottom = 0.0 + } else { + top = 0.0 + bottom = 0.0 + } + return UIEdgeInsets(top: 4.5 + top, left: 0.0, bottom: 5.5 + bottom, right: 0.0) +} + +private var currentTextInputBackgroundImage: (UIColor, UIColor, CGFloat, UIImage)? +private func textInputBackgroundImage(backgroundColor: UIColor?, inputBackgroundColor: UIColor?, strokeColor: UIColor, diameter: CGFloat) -> UIImage? { + if let backgroundColor = backgroundColor, let current = currentTextInputBackgroundImage { + if current.0.isEqual(backgroundColor) && current.1.isEqual(strokeColor) && current.2.isEqual(to: diameter) { + return current.3 + } + } + + let image = generateImage(CGSize(width: diameter, height: diameter), rotatedContext: { size, context in + context.clear(CGRect(x: 0.0, y: 0.0, width: diameter, height: diameter)) + + if let inputBackgroundColor = inputBackgroundColor { + context.setBlendMode(.normal) + context.setFillColor(inputBackgroundColor.cgColor) + } else { + context.setBlendMode(.clear) + context.setFillColor(UIColor.clear.cgColor) + } + context.fillEllipse(in: CGRect(x: 0.0, y: 0.0, width: diameter, height: diameter)) + + context.setBlendMode(.normal) + context.setStrokeColor(strokeColor.cgColor) + let strokeWidth: CGFloat = 1.0 + context.setLineWidth(strokeWidth) + context.strokeEllipse(in: CGRect(x: strokeWidth / 2.0, y: strokeWidth / 2.0, width: diameter - strokeWidth, height: diameter - strokeWidth)) + })?.stretchableImage(withLeftCapWidth: Int(diameter) / 2, topCapHeight: Int(diameter) / 2) + if let image = image { + if let backgroundColor = backgroundColor { + currentTextInputBackgroundImage = (backgroundColor, strokeColor, diameter, image) + } + return image + } else { + return nil + } +} + +class PeerSelectionTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { + var textPlaceholderNode: ImmediateTextNode + let textInputContainerBackgroundNode: ASImageNode + let textInputContainer: ASDisplayNode + var textInputNode: EditableTextNode? + + let textInputBackgroundNode: ASImageNode + private var transparentTextInputBackgroundImage: UIImage? + let actionButtons: ChatTextInputActionButtonsNode + private let counterTextNode: ImmediateTextNode + + private var validLayout: (CGFloat, CGFloat, CGFloat, UIEdgeInsets, CGFloat, LayoutMetrics, Bool)? + + var sendMessage: () -> Void = { } + var updateHeight: (Bool) -> Void = { _ in } + + private var updatingInputState = false + + private var currentPlaceholder: String? + + private var presentationInterfaceState: ChatPresentationInterfaceState? + private var initializedPlaceholder = false + + private let inputMenu = ChatTextInputMenu() + + private var theme: PresentationTheme? + private var strings: PresentationStrings? + + private let hapticFeedback = HapticFeedback() + + var inputTextState: ChatTextInputState { + if let textInputNode = self.textInputNode { + let selectionRange: Range = textInputNode.selectedRange.location ..< (textInputNode.selectedRange.location + textInputNode.selectedRange.length) + return ChatTextInputState(inputText: stateAttributedStringForText(textInputNode.attributedText ?? NSAttributedString()), selectionRange: selectionRange) + } else { + return ChatTextInputState() + } + } + + var storedInputLanguage: String? + var effectiveInputLanguage: String? { + if let textInputNode = textInputNode, textInputNode.isFirstResponder() { + return textInputNode.textInputMode.primaryLanguage + } else { + return self.storedInputLanguage + } + } + + 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 + } + } + + var micButton: ChatTextInputMediaRecordingButton? { + return self.actionButtons.micButton + } + + func updateSendButtonEnabled(_ enabled: Bool, animated: Bool) { + self.actionButtons.isUserInteractionEnabled = enabled + + let transition: ContainedViewLayoutTransition = animated ? .animated(duration: 0.2, curve: .easeInOut) : .immediate + transition.updateAlpha(node: self.actionButtons, alpha: enabled ? 1.0 : 0.3) + } + + func updateInputTextState(_ state: ChatTextInputState, animated: Bool) { + if state.inputText.length != 0 && self.textInputNode == nil { + self.loadTextInputNode() + } + + if let textInputNode = self.textInputNode, let _ = self.presentationInterfaceState { + self.updatingInputState = true + + var textColor: UIColor = .black + var accentTextColor: UIColor = .blue + var baseFontSize: CGFloat = 17.0 + if let presentationInterfaceState = self.presentationInterfaceState { + textColor = presentationInterfaceState.theme.chat.inputPanel.inputTextColor + accentTextColor = presentationInterfaceState.theme.chat.inputPanel.panelControlAccentColor + baseFontSize = max(minInputFontSize, presentationInterfaceState.fontSize.baseDisplaySize) + } + textInputNode.attributedText = textAttributedStringForStateText(state.inputText, fontSize: baseFontSize, textColor: textColor, accentTextColor: accentTextColor, writingDirection: nil) + textInputNode.selectedRange = NSMakeRange(state.selectionRange.lowerBound, state.selectionRange.count) + self.updatingInputState = false + self.updateTextNodeText(animated: animated) + } + } + + var text: String { + get { + return self.textInputNode?.attributedText?.string ?? "" + } set(value) { + if let textInputNode = self.textInputNode { + var textColor: UIColor = .black + var baseFontSize: CGFloat = 17.0 + if let presentationInterfaceState = self.presentationInterfaceState { + textColor = presentationInterfaceState.theme.chat.inputPanel.inputTextColor + baseFontSize = max(minInputFontSize, presentationInterfaceState.fontSize.baseDisplaySize) + } + textInputNode.attributedText = NSAttributedString(string: value, font: Font.regular(baseFontSize), textColor: textColor) + self.editableTextNodeDidUpdateText(textInputNode) + } + } + } + + private let textInputViewInternalInsets = UIEdgeInsets(top: 1.0, left: 13.0, bottom: 1.0, right: 13.0) + + init(presentationInterfaceState: ChatPresentationInterfaceState, presentController: @escaping (ViewController) -> Void) { + self.textInputContainerBackgroundNode = ASImageNode() + self.textInputContainerBackgroundNode.isUserInteractionEnabled = false + self.textInputContainerBackgroundNode.displaysAsynchronously = false + + self.textInputContainer = ASDisplayNode() + self.textInputContainer.addSubnode(self.textInputContainerBackgroundNode) + self.textInputContainer.clipsToBounds = true + + self.textInputBackgroundNode = ASImageNode() + self.textInputBackgroundNode.displaysAsynchronously = false + self.textInputBackgroundNode.displayWithoutProcessing = true + self.textPlaceholderNode = ImmediateTextNode() + self.textPlaceholderNode.maximumNumberOfLines = 1 + self.textPlaceholderNode.isUserInteractionEnabled = false + + self.actionButtons = ChatTextInputActionButtonsNode(theme: presentationInterfaceState.theme, strings: presentationInterfaceState.strings, presentController: presentController) + self.counterTextNode = ImmediateTextNode() + self.counterTextNode.textAlignment = .center + + super.init() + + self.actionButtons.sendButtonLongPressed = { [weak self] node, gesture in + self?.interfaceInteraction?.displaySendMessageOptions(node, gesture) + } + + self.actionButtons.sendButton.addTarget(self, action: #selector(self.sendButtonPressed), forControlEvents: .touchUpInside) + self.actionButtons.sendButton.alpha = 1.0 + self.actionButtons.micButton.alpha = 0.0 + self.actionButtons.expandMediaInputButton.alpha = 0.0 + self.actionButtons.updateAccessibility() + + self.addSubnode(self.textInputContainer) + self.addSubnode(self.textInputBackgroundNode) + + self.addSubnode(self.textPlaceholderNode) + + self.addSubnode(self.actionButtons) + self.addSubnode(self.counterTextNode) + + self.textInputBackgroundNode.clipsToBounds = true + let recognizer = TouchDownGestureRecognizer(target: self, action: #selector(self.textInputBackgroundViewTap(_:))) + recognizer.touchDown = { [weak self] in + if let strongSelf = self { + strongSelf.ensureFocused() + } + } + self.textInputBackgroundNode.isUserInteractionEnabled = true + self.textInputBackgroundNode.view.addGestureRecognizer(recognizer) + + self.updateSendButtonEnabled(false, animated: false) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func loadTextInputNodeIfNeeded() { + if self.textInputNode == nil { + self.loadTextInputNode() + } + } + + private func loadTextInputNode() { + let textInputNode = EditableTextNode() + textInputNode.initialPrimaryLanguage = self.presentationInterfaceState?.interfaceState.inputLanguage + var textColor: UIColor = .black + var tintColor: UIColor = .blue + var baseFontSize: CGFloat = 17.0 + var keyboardAppearance: UIKeyboardAppearance = UIKeyboardAppearance.default + if let presentationInterfaceState = self.presentationInterfaceState { + textColor = presentationInterfaceState.theme.chat.inputPanel.inputTextColor + tintColor = presentationInterfaceState.theme.list.itemAccentColor + baseFontSize = max(minInputFontSize, presentationInterfaceState.fontSize.baseDisplaySize) + keyboardAppearance = presentationInterfaceState.theme.rootController.keyboardColor.keyboardAppearance + } + + let paragraphStyle = NSMutableParagraphStyle() + paragraphStyle.lineSpacing = 1.0 + paragraphStyle.lineHeightMultiple = 1.0 + paragraphStyle.paragraphSpacing = 1.0 + paragraphStyle.maximumLineHeight = 20.0 + paragraphStyle.minimumLineHeight = 20.0 + + textInputNode.typingAttributes = [NSAttributedString.Key.font.rawValue: Font.regular(max(minInputFontSize, baseFontSize)), NSAttributedString.Key.foregroundColor.rawValue: textColor, NSAttributedString.Key.paragraphStyle.rawValue: paragraphStyle] + textInputNode.clipsToBounds = false + textInputNode.textView.clipsToBounds = false + textInputNode.delegate = self + textInputNode.hitTestSlop = UIEdgeInsets(top: -5.0, left: -5.0, bottom: -5.0, right: -5.0) + textInputNode.keyboardAppearance = keyboardAppearance + textInputNode.tintColor = tintColor + textInputNode.textView.scrollIndicatorInsets = UIEdgeInsets(top: 9.0, left: 0.0, bottom: 9.0, right: -13.0) + self.textInputContainer.addSubnode(textInputNode) + textInputNode.view.disablesInteractiveTransitionGestureRecognizer = true + self.textInputNode = textInputNode + + if let presentationInterfaceState = self.presentationInterfaceState { + refreshChatTextInputTypingAttributes(textInputNode, theme: presentationInterfaceState.theme, baseFontSize: baseFontSize) + textInputNode.textContainerInset = calculateTextFieldRealInsets(presentationInterfaceState) + } + + if !self.textInputContainer.bounds.size.width.isZero { + let textInputFrame = self.textInputContainer.frame + + textInputNode.frame = CGRect(origin: CGPoint(x: self.textInputViewInternalInsets.left, y: self.textInputViewInternalInsets.top), size: CGSize(width: textInputFrame.size.width - (self.textInputViewInternalInsets.left + self.textInputViewInternalInsets.right), height: textInputFrame.size.height - self.textInputViewInternalInsets.top - self.textInputViewInternalInsets.bottom)) + } + + self.textInputBackgroundNode.isUserInteractionEnabled = false + self.textInputBackgroundNode.view.removeGestureRecognizer(self.textInputBackgroundNode.view.gestureRecognizers![0]) + + let recognizer = TouchDownGestureRecognizer(target: self, action: #selector(self.textInputBackgroundViewTap(_:))) + recognizer.touchDown = { [weak self] in + if let strongSelf = self { + strongSelf.ensureFocused() + } + } + textInputNode.view.addGestureRecognizer(recognizer) + + textInputNode.textView.accessibilityHint = self.textPlaceholderNode.attributedText?.string + } + + private func textFieldMaxHeight(_ maxHeight: CGFloat, metrics: LayoutMetrics) -> CGFloat { + let textFieldInsets = self.textFieldInsets(metrics: metrics) + return max(33.0, maxHeight - (textFieldInsets.top + textFieldInsets.bottom + self.textInputViewInternalInsets.top + self.textInputViewInternalInsets.bottom)) + } + + private func calculateTextFieldMetrics(width: CGFloat, maxHeight: CGFloat, metrics: LayoutMetrics) -> (accessoryButtonsWidth: CGFloat, textFieldHeight: CGFloat) { + let textFieldInsets = self.textFieldInsets(metrics: metrics) + + let fieldMaxHeight = textFieldMaxHeight(maxHeight, metrics: metrics) + + var textFieldMinHeight: CGFloat = 35.0 + if let presentationInterfaceState = self.presentationInterfaceState { + textFieldMinHeight = calclulateTextFieldMinHeight(presentationInterfaceState, metrics: metrics) + } + + let textFieldHeight: CGFloat + if let textInputNode = self.textInputNode { + let maxTextWidth = width - textFieldInsets.left - textFieldInsets.right - self.textInputViewInternalInsets.left - self.textInputViewInternalInsets.right + let measuredHeight = textInputNode.measure(CGSize(width: maxTextWidth, height: CGFloat.greatestFiniteMagnitude)) + let unboundTextFieldHeight = max(textFieldMinHeight, ceil(measuredHeight.height)) + + let maxNumberOfLines = min(12, (Int(fieldMaxHeight - 11.0) - 33) / 22) + + let updatedMaxHeight = (CGFloat(maxNumberOfLines) * (22.0 + 2.0) + 10.0) + + textFieldHeight = max(textFieldMinHeight, min(updatedMaxHeight, unboundTextFieldHeight)) + } else { + textFieldHeight = textFieldMinHeight + } + + return (0.0, textFieldHeight) + } + + private func textFieldInsets(metrics: LayoutMetrics) -> UIEdgeInsets { + var insets = UIEdgeInsets(top: 6.0, left: 6.0, bottom: 6.0, right: 42.0) + if case .regular = metrics.widthClass, case .regular = metrics.heightClass { + insets.top += 1.0 + insets.bottom += 1.0 + } + return insets + } + + private func panelHeight(textFieldHeight: CGFloat, metrics: LayoutMetrics) -> CGFloat { + let textFieldInsets = self.textFieldInsets(metrics: metrics) + let result = textFieldHeight + textFieldInsets.top + textFieldInsets.bottom + self.textInputViewInternalInsets.top + self.textInputViewInternalInsets.bottom + return result + } + + override func minimalHeight(interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics) -> CGFloat { + let textFieldMinHeight = calclulateTextFieldMinHeight(interfaceState, metrics: metrics) + var minimalHeight: CGFloat = 14.0 + textFieldMinHeight + if case .regular = metrics.widthClass, case .regular = metrics.heightClass { + minimalHeight += 2.0 + } + return minimalHeight + } + + override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, additionalSideInsets: UIEdgeInsets, maxHeight: CGFloat, isSecondary: Bool, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics) -> CGFloat { + let previousAdditionalSideInsets = self.validLayout?.3 + self.validLayout = (width, leftInset, rightInset, additionalSideInsets, maxHeight, metrics, isSecondary) + + var transition = transition + var additionalOffset: CGFloat = 0.0 + if let previousAdditionalSideInsets = previousAdditionalSideInsets, previousAdditionalSideInsets.right != additionalSideInsets.right { + additionalOffset = (previousAdditionalSideInsets.right - additionalSideInsets.right) / 3.0 + + if case .animated = transition { + transition = .animated(duration: 0.2, curve: .easeInOut) + } + } + + if self.presentationInterfaceState != interfaceState { + let previousState = self.presentationInterfaceState + self.presentationInterfaceState = interfaceState + + let themeUpdated = previousState?.theme !== interfaceState.theme + + var updateSendButtonIcon = false + if (previousState?.interfaceState.editMessage != nil) != (interfaceState.interfaceState.editMessage != nil) { + updateSendButtonIcon = true + } + if self.theme !== interfaceState.theme { + updateSendButtonIcon = true + + if self.theme == nil || !self.theme!.chat.inputPanel.inputTextColor.isEqual(interfaceState.theme.chat.inputPanel.inputTextColor) { + let textColor = interfaceState.theme.chat.inputPanel.inputTextColor + let baseFontSize = max(minInputFontSize, interfaceState.fontSize.baseDisplaySize) + + if let textInputNode = self.textInputNode { + if let text = textInputNode.attributedText?.string { + let range = textInputNode.selectedRange + textInputNode.attributedText = NSAttributedString(string: text, font: Font.regular(baseFontSize), textColor: textColor) + textInputNode.selectedRange = range + } + textInputNode.typingAttributes = [NSAttributedString.Key.font.rawValue: Font.regular(baseFontSize), NSAttributedString.Key.foregroundColor.rawValue: textColor] + } + } + + let keyboardAppearance = interfaceState.theme.rootController.keyboardColor.keyboardAppearance + if let textInputNode = self.textInputNode, textInputNode.keyboardAppearance != keyboardAppearance, textInputNode.isFirstResponder() { + if textInputNode.isCurrentlyEmoji() { + textInputNode.initialPrimaryLanguage = "emoji" + textInputNode.resetInitialPrimaryLanguage() + } + textInputNode.keyboardAppearance = keyboardAppearance + } + + self.theme = interfaceState.theme + + self.actionButtons.updateTheme(theme: interfaceState.theme) + + let textFieldMinHeight = calclulateTextFieldMinHeight(interfaceState, metrics: metrics) + let minimalInputHeight: CGFloat = 2.0 + textFieldMinHeight + + 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, inputBackgroundColor: nil, strokeColor: interfaceState.theme.chat.inputPanel.inputStrokeColor, diameter: minimalInputHeight) + self.transparentTextInputBackgroundImage = textInputBackgroundImage(backgroundColor: nil, inputBackgroundColor: interfaceState.theme.chat.inputPanel.inputBackgroundColor, strokeColor: interfaceState.theme.chat.inputPanel.inputStrokeColor, diameter: minimalInputHeight) + self.textInputContainerBackgroundNode.image = generateStretchableFilledCircleImage(diameter: minimalInputHeight, color: interfaceState.theme.chat.inputPanel.inputBackgroundColor) + } else { + if self.strings !== interfaceState.strings { + self.strings = interfaceState.strings + self.inputMenu.updateStrings(interfaceState.strings) + } + } + + if themeUpdated || !self.initializedPlaceholder { + self.initializedPlaceholder = true + + let placeholder = interfaceState.strings.Conversation_InputTextPlaceholder + + if self.currentPlaceholder != placeholder || themeUpdated { + self.currentPlaceholder = placeholder + let baseFontSize = max(minInputFontSize, interfaceState.fontSize.baseDisplaySize) + self.textPlaceholderNode.attributedText = NSAttributedString(string: placeholder, font: Font.regular(baseFontSize), textColor: interfaceState.theme.chat.inputPanel.inputPlaceholderColor) + self.textInputNode?.textView.accessibilityHint = placeholder + let placeholderSize = self.textPlaceholderNode.updateLayout(CGSize(width: 320.0, height: CGFloat.greatestFiniteMagnitude)) + if transition.isAnimated, let snapshotLayer = self.textPlaceholderNode.layer.snapshotContentTree() { + self.textPlaceholderNode.supernode?.layer.insertSublayer(snapshotLayer, above: self.textPlaceholderNode.layer) + snapshotLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.22, removeOnCompletion: false, completion: { [weak snapshotLayer] _ in + snapshotLayer?.removeFromSuperlayer() + }) + self.textPlaceholderNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.18) + } + self.textPlaceholderNode.frame = CGRect(origin: self.textPlaceholderNode.frame.origin, size: placeholderSize) + } + + self.actionButtons.sendButtonLongPressEnabled = true + } + + let sendButtonHasApplyIcon = interfaceState.interfaceState.editMessage != nil + + if updateSendButtonIcon { + if !self.actionButtons.animatingSendButton { + 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.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) + + 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 { + self.actionButtons.sendButton.setImage(PresentationResourcesChat.chatInputPanelApplyButtonImage(interfaceState.theme), for: []) + } else { + self.actionButtons.sendButton.setImage(PresentationResourcesChat.chatInputPanelSendButtonImage(interfaceState.theme), for: []) + } + } + } + } + + var textFieldMinHeight: CGFloat = 33.0 + if let presentationInterfaceState = self.presentationInterfaceState { + textFieldMinHeight = calclulateTextFieldMinHeight(presentationInterfaceState, metrics: metrics) + } + let minimalHeight: CGFloat = 14.0 + textFieldMinHeight + let minimalInputHeight: CGFloat = 2.0 + textFieldMinHeight + + var animatedTransition = true + if case .immediate = transition { + animatedTransition = false + } + + let baseWidth = width - leftInset - rightInset + let (accessoryButtonsWidth, textFieldHeight) = self.calculateTextFieldMetrics(width: baseWidth, maxHeight: maxHeight, metrics: metrics) + let panelHeight = self.panelHeight(textFieldHeight: textFieldHeight, metrics: metrics) + + var composeButtonsOffset: CGFloat = 0.0 + var textInputBackgroundWidthOffset: CGFloat = 0.0 + + self.updateCounterTextNode(transition: transition) + + let actionButtonsFrame = CGRect(origin: CGPoint(x: width - rightInset - 43.0 - UIScreenPixel + composeButtonsOffset, y: panelHeight - minimalHeight), size: CGSize(width: 44.0, height: minimalHeight)) + transition.updateFrame(node: self.actionButtons, frame: actionButtonsFrame) + + if let presentationInterfaceState = self.presentationInterfaceState { + self.actionButtons.updateLayout(size: CGSize(width: 44.0, height: minimalHeight), transition: transition, interfaceState: presentationInterfaceState) + } + + let searchLayoutClearButtonSize = CGSize(width: 44.0, height: minimalHeight) + var textFieldInsets = self.textFieldInsets(metrics: metrics) + if additionalSideInsets.right > 0.0 { + textFieldInsets.right += additionalSideInsets.right / 3.0 + } + + var textInputViewRealInsets = UIEdgeInsets() + if let presentationInterfaceState = self.presentationInterfaceState { + textInputViewRealInsets = calculateTextFieldRealInsets(presentationInterfaceState) + } + + let textInputFrame = CGRect(x: leftInset + textFieldInsets.left, y: textFieldInsets.top, width: baseWidth - textFieldInsets.left - textFieldInsets.right + textInputBackgroundWidthOffset, height: panelHeight - textFieldInsets.top - textFieldInsets.bottom) + transition.updateFrame(node: self.textInputContainer, frame: textInputFrame) + transition.updateFrame(node: self.textInputContainerBackgroundNode, frame: CGRect(origin: CGPoint(), size: textInputFrame.size)) + + if let textInputNode = self.textInputNode { + let textFieldFrame = CGRect(origin: CGPoint(x: self.textInputViewInternalInsets.left, y: self.textInputViewInternalInsets.top), size: CGSize(width: textInputFrame.size.width - (self.textInputViewInternalInsets.left + self.textInputViewInternalInsets.right), height: textInputFrame.size.height - self.textInputViewInternalInsets.top - textInputViewInternalInsets.bottom)) + let shouldUpdateLayout = textFieldFrame.size != textInputNode.frame.size + transition.updateFrame(node: textInputNode, frame: textFieldFrame) + if shouldUpdateLayout { + textInputNode.layout() + } + } + + var inputHasText = false + if let textInputNode = self.textInputNode, let attributedText = textInputNode.attributedText, attributedText.length != 0 { + inputHasText = true + } + + self.textPlaceholderNode.isHidden = inputHasText + + transition.updateFrame(node: self.textPlaceholderNode, frame: CGRect(origin: CGPoint(x: leftInset + textFieldInsets.left + self.textInputViewInternalInsets.left, y: textFieldInsets.top + self.textInputViewInternalInsets.top + textInputViewRealInsets.top + UIScreenPixel), size: self.textPlaceholderNode.frame.size)) + transition.updateFrame(layer: self.textInputBackgroundNode.layer, frame: CGRect(x: leftInset + textFieldInsets.left, y: textFieldInsets.top, width: baseWidth - textFieldInsets.left - textFieldInsets.right + textInputBackgroundWidthOffset, height: panelHeight - textFieldInsets.top - textFieldInsets.bottom)) + + self.actionButtons.updateAccessibility() + + if let prevInputPanelNode = self.prevInputPanelNode { + prevInputPanelNode.frame = CGRect(origin: .zero, size: prevInputPanelNode.frame.size) + } + + return panelHeight + } + + override func canHandleTransition(from prevInputPanelNode: ChatInputPanelNode?) -> Bool { + return false + } + + @objc func editableTextNodeDidUpdateText(_ editableTextNode: ASEditableTextNode) { + if let textInputNode = self.textInputNode, let presentationInterfaceState = self.presentationInterfaceState { + let baseFontSize = max(minInputFontSize, presentationInterfaceState.fontSize.baseDisplaySize) + refreshChatTextInputAttributes(textInputNode, theme: presentationInterfaceState.theme, baseFontSize: baseFontSize) + refreshChatTextInputTypingAttributes(textInputNode, theme: presentationInterfaceState.theme, baseFontSize: baseFontSize) + + let inputTextState = self.inputTextState + + self.interfaceInteraction?.updateTextInputStateAndMode({ _, inputMode in return (inputTextState, inputMode) }) + self.interfaceInteraction?.updateInputLanguage({ _ in return textInputNode.textInputMode.primaryLanguage }) + self.updateTextNodeText(animated: true) + + self.updateCounterTextNode(transition: .immediate) + } + } + + private func updateCounterTextNode(transition: ContainedViewLayoutTransition) { + if let textInputNode = self.textInputNode, let presentationInterfaceState = self.presentationInterfaceState, let editMessage = presentationInterfaceState.interfaceState.editMessage, let inputTextMaxLength = editMessage.inputTextMaxLength { + let textCount = Int32(textInputNode.textView.text.count) + let counterColor: UIColor = textCount > inputTextMaxLength ? presentationInterfaceState.theme.chat.inputPanel.panelControlDestructiveColor : presentationInterfaceState.theme.chat.inputPanel.panelControlColor + + let remainingCount = max(-999, inputTextMaxLength - textCount) + let counterText = remainingCount >= 5 ? "" : "\(remainingCount)" + self.counterTextNode.attributedText = NSAttributedString(string: counterText, font: counterFont, textColor: counterColor) + } else { + self.counterTextNode.attributedText = NSAttributedString(string: "", font: counterFont, textColor: .black) + } + + if let (width, leftInset, rightInset, _, maxHeight, metrics, _) = self.validLayout { + var composeButtonsOffset: CGFloat = 0.0 + + let (_, textFieldHeight) = self.calculateTextFieldMetrics(width: width - leftInset - rightInset, maxHeight: maxHeight, metrics: metrics) + let panelHeight = self.panelHeight(textFieldHeight: textFieldHeight, metrics: metrics) + var textFieldMinHeight: CGFloat = 33.0 + if let presentationInterfaceState = self.presentationInterfaceState { + textFieldMinHeight = calclulateTextFieldMinHeight(presentationInterfaceState, metrics: metrics) + } + let minimalHeight: CGFloat = 14.0 + textFieldMinHeight + + let counterSize = self.counterTextNode.updateLayout(CGSize(width: 44.0, height: 44.0)) + let actionButtonsOriginX = width - rightInset - 43.0 - UIScreenPixel + composeButtonsOffset + let counterFrame = CGRect(origin: CGPoint(x: actionButtonsOriginX, y: panelHeight - minimalHeight - counterSize.height + 3.0), size: CGSize(width: width - actionButtonsOriginX - rightInset, height: counterSize.height)) + transition.updateFrame(node: self.counterTextNode, frame: counterFrame) + } + } + + private func updateTextNodeText(animated: Bool) { + var inputHasText = false + if let textInputNode = self.textInputNode, let attributedText = textInputNode.attributedText, attributedText.length != 0 { + inputHasText = true + } + + if let _ = self.presentationInterfaceState { + self.textPlaceholderNode.isHidden = inputHasText + } + + self.updateTextHeight(animated: animated) + } + + private func updateTextHeight(animated: Bool) { + if let (width, leftInset, rightInset, additionalSideInsets, maxHeight, metrics, _) = self.validLayout { + let (_, textFieldHeight) = self.calculateTextFieldMetrics(width: width - leftInset - rightInset - additionalSideInsets.right, maxHeight: maxHeight, metrics: metrics) + let panelHeight = self.panelHeight(textFieldHeight: textFieldHeight, metrics: metrics) + if !self.bounds.size.height.isEqual(to: panelHeight) { + self.updateHeight(animated) + } + } + } + + @objc func editableTextNodeShouldReturn(_ editableTextNode: ASEditableTextNode) -> Bool { + if self.actionButtons.sendButton.supernode != nil && !self.actionButtons.sendButton.isHidden && !self.actionButtons.sendButton.alpha.isZero { + self.sendButtonPressed() + } + return false + } + + private func applyUpdateSendButtonIcon() { + if let interfaceState = self.presentationInterfaceState { + let sendButtonHasApplyIcon = interfaceState.interfaceState.editMessage != nil + + if sendButtonHasApplyIcon != self.actionButtons.sendButtonHasApplyIcon { + self.actionButtons.sendButtonHasApplyIcon = sendButtonHasApplyIcon + if self.actionButtons.sendButtonHasApplyIcon { + self.actionButtons.sendButton.setImage(PresentationResourcesChat.chatInputPanelApplyButtonImage(interfaceState.theme), for: []) + } else { + if case .scheduledMessages = interfaceState.subject { + self.actionButtons.sendButton.setImage(PresentationResourcesChat.chatInputPanelScheduleButtonImage(interfaceState.theme), for: []) + } else { + self.actionButtons.sendButton.setImage(PresentationResourcesChat.chatInputPanelSendButtonImage(interfaceState.theme), for: []) + } + } + } + } + } + + @objc func editableTextNodeDidChangeSelection(_ editableTextNode: ASEditableTextNode, fromSelectedRange: NSRange, toSelectedRange: NSRange, dueToEditing: Bool) { + if !dueToEditing && !self.updatingInputState { + let inputTextState = self.inputTextState + self.interfaceInteraction?.updateTextInputStateAndMode({ _, inputMode in return (inputTextState, inputMode) }) + } + + if let textInputNode = self.textInputNode, let presentationInterfaceState = self.presentationInterfaceState { + if case .format = self.inputMenu.state { + self.inputMenu.deactivate() + UIMenuController.shared.update() + } + + let baseFontSize = max(minInputFontSize, presentationInterfaceState.fontSize.baseDisplaySize) + refreshChatTextInputTypingAttributes(textInputNode, theme: presentationInterfaceState.theme, baseFontSize: baseFontSize) + } + } + + @objc func editableTextNodeDidBeginEditing(_ editableTextNode: ASEditableTextNode) { + self.interfaceInteraction?.updateInputModeAndDismissedButtonKeyboardMessageId({ state in + return (.text, state.keyboardButtonsMessage?.id) + }) + self.inputMenu.activate() + } + + func editableTextNodeDidFinishEditing(_ editableTextNode: ASEditableTextNode) { + self.storedInputLanguage = editableTextNode.textInputMode.primaryLanguage + self.inputMenu.deactivate() + } + + func editableTextNodeTarget(forAction action: Selector) -> ASEditableTextNodeTargetForAction? { + if action == Selector(("_accessibilitySpeak:")) { + if case .format = self.inputMenu.state { + return ASEditableTextNodeTargetForAction(target: nil) + } else if let textInputNode = self.textInputNode, textInputNode.selectedRange.length > 0 { + return ASEditableTextNodeTargetForAction(target: self) + } else { + return ASEditableTextNodeTargetForAction(target: nil) + } + } else if action == Selector(("_accessibilitySpeakSpellOut:")) { + if case .format = self.inputMenu.state { + return ASEditableTextNodeTargetForAction(target: nil) + } else if let textInputNode = self.textInputNode, textInputNode.selectedRange.length > 0 { + return nil + } else { + return ASEditableTextNodeTargetForAction(target: nil) + } + } + else if action == Selector("_accessibilitySpeakLanguageSelection:") || action == Selector("_accessibilityPauseSpeaking:") || action == Selector("_accessibilitySpeakSentence:") { + return ASEditableTextNodeTargetForAction(target: nil) + } else if action == Selector(("_showTextStyleOptions:")) { + if case .general = self.inputMenu.state { + if let textInputNode = self.textInputNode, textInputNode.attributedText == nil || textInputNode.attributedText!.length == 0 || textInputNode.selectedRange.length == 0 { + return ASEditableTextNodeTargetForAction(target: nil) + } + return ASEditableTextNodeTargetForAction(target: self) + } else { + return ASEditableTextNodeTargetForAction(target: nil) + } + } else if action == #selector(self.formatAttributesBold(_:)) || action == #selector(self.formatAttributesItalic(_:)) || action == #selector(self.formatAttributesMonospace(_:)) || action == #selector(self.formatAttributesLink(_:)) || action == #selector(self.formatAttributesStrikethrough(_:)) || action == #selector(self.formatAttributesUnderline(_:)) { + if case .format = self.inputMenu.state { + return ASEditableTextNodeTargetForAction(target: self) + } else { + return ASEditableTextNodeTargetForAction(target: nil) + } + } + if case .format = self.inputMenu.state { + return ASEditableTextNodeTargetForAction(target: nil) + } + return nil + } + + @objc func _accessibilitySpeak(_ sender: Any) { + var text = "" + self.interfaceInteraction?.updateTextInputStateAndMode { current, inputMode in + text = current.inputText.attributedSubstring(from: NSMakeRange(current.selectionRange.lowerBound, current.selectionRange.count)).string + return (current, inputMode) + } + speakText(text) + + if #available(iOS 13.0, *) { + UIMenuController.shared.hideMenu() + } else { + UIMenuController.shared.isMenuVisible = false + UIMenuController.shared.update() + } + } + + @objc func _showTextStyleOptions(_ sender: Any) { + if let textInputNode = self.textInputNode { + 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)) + } + } + + @objc func formatAttributesBold(_ sender: Any) { + self.inputMenu.back() + self.interfaceInteraction?.updateTextInputStateAndMode { current, inputMode in + return (chatTextInputAddFormattingAttribute(current, attribute: ChatTextInputAttributes.bold), inputMode) + } + } + + @objc func formatAttributesItalic(_ sender: Any) { + self.inputMenu.back() + self.interfaceInteraction?.updateTextInputStateAndMode { current, inputMode in + return (chatTextInputAddFormattingAttribute(current, attribute: ChatTextInputAttributes.italic), inputMode) + } + } + + @objc func formatAttributesMonospace(_ sender: Any) { + self.inputMenu.back() + self.interfaceInteraction?.updateTextInputStateAndMode { current, inputMode in + return (chatTextInputAddFormattingAttribute(current, attribute: ChatTextInputAttributes.monospace), inputMode) + } + } + + @objc func formatAttributesLink(_ sender: Any) { + self.inputMenu.back() + self.interfaceInteraction?.openLinkEditing() + } + + @objc func formatAttributesStrikethrough(_ sender: Any) { + self.inputMenu.back() + self.interfaceInteraction?.updateTextInputStateAndMode { current, inputMode in + return (chatTextInputAddFormattingAttribute(current, attribute: ChatTextInputAttributes.strikethrough), inputMode) + } + } + + @objc func formatAttributesUnderline(_ sender: Any) { + self.inputMenu.back() + self.interfaceInteraction?.updateTextInputStateAndMode { current, inputMode in + return (chatTextInputAddFormattingAttribute(current, attribute: ChatTextInputAttributes.underline), inputMode) + } + } + + @objc func editableTextNode(_ editableTextNode: ASEditableTextNode, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool { + var cleanText = text + let removeSequences: [String] = ["\u{202d}", "\u{202c}"] + for sequence in removeSequences { + inner: while true { + if let range = cleanText.range(of: sequence) { + cleanText.removeSubrange(range) + } else { + break inner + } + } + } + + if cleanText != text { + let string = NSMutableAttributedString(attributedString: editableTextNode.attributedText ?? NSAttributedString()) + var textColor: UIColor = .black + var accentTextColor: UIColor = .blue + var baseFontSize: CGFloat = 17.0 + if let presentationInterfaceState = self.presentationInterfaceState { + textColor = presentationInterfaceState.theme.chat.inputPanel.inputTextColor + accentTextColor = presentationInterfaceState.theme.chat.inputPanel.panelControlAccentColor + baseFontSize = max(minInputFontSize, presentationInterfaceState.fontSize.baseDisplaySize) + } + let cleanReplacementString = textAttributedStringForStateText(NSAttributedString(string: cleanText), fontSize: baseFontSize, textColor: textColor, accentTextColor: accentTextColor, writingDirection: nil) + string.replaceCharacters(in: range, with: cleanReplacementString) + self.textInputNode?.attributedText = string + self.textInputNode?.selectedRange = NSMakeRange(range.lowerBound + cleanReplacementString.length, 0) + self.updateTextNodeText(animated: true) + return false + } + return true + } + + @objc func editableTextNodeShouldCopy(_ editableTextNode: ASEditableTextNode) -> Bool { + self.interfaceInteraction?.updateTextInputStateAndMode { current, inputMode in + storeInputTextInPasteboard(current.inputText.attributedSubstring(from: NSMakeRange(current.selectionRange.lowerBound, current.selectionRange.count))) + return (current, inputMode) + } + return false + } + + @objc func editableTextNodeShouldPaste(_ editableTextNode: ASEditableTextNode) -> Bool { + let pasteboard = UIPasteboard.general + + var attributedString: NSAttributedString? + if let data = pasteboard.data(forPasteboardType: kUTTypeRTF as String) { + attributedString = chatInputStateStringFromRTF(data, type: NSAttributedString.DocumentType.rtf) + } else if let data = pasteboard.data(forPasteboardType: "com.apple.flat-rtfd") { + attributedString = chatInputStateStringFromRTF(data, type: NSAttributedString.DocumentType.rtfd) + } + + if let attributedString = attributedString { + self.interfaceInteraction?.updateTextInputStateAndMode { current, inputMode in + if let inputText = current.inputText.mutableCopy() as? NSMutableAttributedString { + inputText.replaceCharacters(in: NSMakeRange(current.selectionRange.lowerBound, current.selectionRange.count), with: attributedString) + let updatedRange = current.selectionRange.lowerBound + attributedString.length + return (ChatTextInputState(inputText: inputText, selectionRange: updatedRange ..< updatedRange), inputMode) + } else { + return (ChatTextInputState(inputText: attributedString), inputMode) + } + } + return false + } + return true + } + + @objc func sendButtonPressed() { + if let textInputNode = self.textInputNode, let presentationInterfaceState = self.presentationInterfaceState, let editMessage = presentationInterfaceState.interfaceState.editMessage, let inputTextMaxLength = editMessage.inputTextMaxLength { + let textCount = Int32(textInputNode.textView.text.count) + let remainingCount = inputTextMaxLength - textCount + + if remainingCount < 0 { + textInputNode.layer.addShakeAnimation() + self.hapticFeedback.error() + return + } + } + + self.sendMessage() + } + + @objc func textInputBackgroundViewTap(_ recognizer: UITapGestureRecognizer) { + if case .ended = recognizer.state { + self.ensureFocused() + } + } + + var isFocused: Bool { + return self.textInputNode?.isFirstResponder() ?? false + } + + func ensureUnfocused() { + self.textInputNode?.resignFirstResponder() + } + + func ensureFocused() { + if self.textInputNode == nil { + self.loadTextInputNode() + } + + self.textInputNode?.becomeFirstResponder() + } + + func frameForInputActionButton() -> CGRect? { + if !self.actionButtons.alpha.isZero { + if self.actionButtons.micButton.alpha.isZero { + return self.actionButtons.frame.insetBy(dx: 0.0, dy: 6.0).offsetBy(dx: 4.0, dy: 0.0) + } else { + return self.actionButtons.frame.insetBy(dx: 0.0, dy: 6.0).offsetBy(dx: 2.0, dy: 0.0) + } + } + return nil + } +} diff --git a/submodules/TelegramUI/Sources/PeersNearbyManager.swift b/submodules/TelegramUI/Sources/PeersNearbyManager.swift index f16bb84040..ab79fd03b1 100644 --- a/submodules/TelegramUI/Sources/PeersNearbyManager.swift +++ b/submodules/TelegramUI/Sources/PeersNearbyManager.swift @@ -7,23 +7,27 @@ import TelegramApi import DeviceLocationManager import CoreLocation import AccountContext +import DeviceAccess private let locationUpdateTimePeriod: Double = 1.0 * 60.0 * 60.0 private let locationDistanceUpdateThreshold: Double = 1000 final class PeersNearbyManagerImpl: PeersNearbyManager { private let account: Account + private let engine: TelegramEngine private let locationManager: DeviceLocationManager private let inForeground: Signal private var preferencesDisposable: Disposable? private var locationDisposable = MetaDisposable() private var updateDisposable = MetaDisposable() + private var accessDisposable: Disposable? private var previousLocation: CLLocation? - init(account: Account, locationManager: DeviceLocationManager, inForeground: Signal) { + init(account: Account, engine: TelegramEngine, locationManager: DeviceLocationManager, inForeground: Signal) { self.account = account + self.engine = engine self.locationManager = locationManager self.inForeground = inForeground @@ -32,17 +36,34 @@ final class PeersNearbyManagerImpl: PeersNearbyManager { let state = view.values[PreferencesKeys.peersNearby] as? PeersNearbyState ?? .default return state.visibilityExpires } + |> deliverOnMainQueue |> distinctUntilChanged).start(next: { [weak self] visibility in if let strongSelf = self { strongSelf.visibilityUpdated(visible: visibility != nil) } }) + + self.accessDisposable = (DeviceAccess.authorizationStatus(applicationInForeground: nil, siriAuthorization: nil, subject: .location(.live)) + |> deliverOnMainQueue).start(next: { [weak self] status in + guard let strongSelf = self else { + return + } + switch status { + case .denied: + let _ = strongSelf.engine.peersNearby.updatePeersNearbyVisibility(update: .invisible, background: false).start() + strongSelf.locationDisposable.set(nil) + strongSelf.updateDisposable.set(nil) + default: + break + } + }) } deinit { self.preferencesDisposable?.dispose() self.locationDisposable.dispose() self.updateDisposable.dispose() + self.accessDisposable?.dispose() } private func visibilityUpdated(visible: Bool) { @@ -77,9 +98,9 @@ final class PeersNearbyManagerImpl: PeersNearbyManager { } private func updateLocation(_ location: CLLocation) { - self.updateDisposable.set(updatePeersNearbyVisibility(account: self.account, update: .location(latitude: location.coordinate.latitude, longitude: location.coordinate.longitude), background: true).start(error: { [weak self] _ in + self.updateDisposable.set(self.engine.peersNearby.updatePeersNearbyVisibility(update: .location(latitude: location.coordinate.latitude, longitude: location.coordinate.longitude), background: true).start(error: { [weak self] _ in if let strongSelf = self { - let _ = updatePeersNearbyVisibility(account: strongSelf.account, update: .invisible, background: false).start() + let _ = strongSelf.engine.peersNearby.updatePeersNearbyVisibility(update: .invisible, background: false).start() strongSelf.locationDisposable.set(nil) strongSelf.updateDisposable.set(nil) } diff --git a/submodules/TelegramUI/Sources/PollResultsController.swift b/submodules/TelegramUI/Sources/PollResultsController.swift index a3c23ca54a..69c0c154b0 100644 --- a/submodules/TelegramUI/Sources/PollResultsController.swift +++ b/submodules/TelegramUI/Sources/PollResultsController.swift @@ -252,7 +252,7 @@ private func pollResultsControllerEntries(presentationData: PresentationData, po 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 fakeUser = TelegramUser(id: PeerId(namespace: .max, id: PeerId.Id._internalFromInt32Value(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)) } @@ -323,7 +323,7 @@ public func pollResultsController(context: AccountContext, messageId: MessageId, let actionsDisposable = DisposableSet() - let resultsContext = PollResultsContext(account: context.account, messageId: messageId, poll: poll) + let resultsContext = context.engine.messages.pollResults(messageId: messageId, poll: poll) let arguments = PollResultsControllerArguments(context: context, collapseOption: { optionId in diff --git a/submodules/TelegramUI/Sources/PrefetchManager.swift b/submodules/TelegramUI/Sources/PrefetchManager.swift index 6c0fa0a1e7..88d8f64684 100644 --- a/submodules/TelegramUI/Sources/PrefetchManager.swift +++ b/submodules/TelegramUI/Sources/PrefetchManager.swift @@ -6,7 +6,9 @@ import SyncCore import TelegramUIPreferences import AccountContext import PhotoResources +import StickerResources import Emoji +import UniversalMediaPlayer private final class PrefetchMediaContext { let fetchDisposable = MetaDisposable() @@ -20,18 +22,23 @@ public enum PrefetchMediaItem { case animatedEmojiSticker(TelegramMediaFile) } -private final class PrefetchManagerImpl { +private final class PrefetchManagerInnerImpl { private let queue: Queue private let account: Account + private let engine: TelegramEngine private let fetchManager: FetchManager private var listDisposable: Disposable? private var contexts: [MediaId: PrefetchMediaContext] = [:] - - init(queue: Queue, sharedContext: SharedAccountContext, account: Account, fetchManager: FetchManager) { + + private let preloadGreetingStickerDisposable = MetaDisposable() + fileprivate let preloadedGreetingStickerPromise = Promise(nil) + + init(queue: Queue, sharedContext: SharedAccountContext, account: Account, engine: TelegramEngine, fetchManager: FetchManager) { self.queue = queue self.account = account + self.engine = engine self.fetchManager = fetchManager let networkType = account.networkType @@ -51,7 +58,7 @@ private final class PrefetchManagerImpl { return view.values[PreferencesKeys.appConfiguration] as? AppConfiguration ?? .defaultValue } - let orderedPreloadMedia = combineLatest(account.viewTracker.orderedPreloadMedia, loadedStickerPack(postbox: account.postbox, network: account.network, reference: .animatedEmoji, forceActualized: false), appConfiguration) + let orderedPreloadMedia = combineLatest(account.viewTracker.orderedPreloadMedia, TelegramEngine(account: account).stickers.loadedStickerPack(reference: .animatedEmoji, forceActualized: false), appConfiguration) |> map { orderedPreloadMedia, stickerPack, appConfiguration -> [PrefetchMediaItem] in let emojiSounds = AnimatedEmojiSoundsConfiguration.with(appConfiguration: appConfiguration, account: account) let chatHistoryMediaItems = orderedPreloadMedia.map { PrefetchMediaItem.chatHistory($0) } @@ -202,7 +209,7 @@ private final class PrefetchManagerImpl { context = PrefetchMediaContext() self.contexts[id] = context - let priority: FetchManagerPriority = .backgroundPrefetch(locationOrder: HistoryPreloadIndex(index: nil, hasUnread: false, isMuted: false, isPriority: true), localOrder: MessageIndex(id: MessageId(peerId: PeerId(namespace: 0, id: 0), namespace: 0, id: order), timestamp: 0)) + let priority: FetchManagerPriority = .backgroundPrefetch(locationOrder: HistoryPreloadIndex(index: nil, hasUnread: false, isMuted: false, isPriority: true), localOrder: MessageIndex(id: MessageId(peerId: PeerId(0), namespace: 0, id: order), timestamp: 0)) if case .full = automaticDownload { let fetchSignal = freeMediaFileInteractiveFetched(fetchManager: self.fetchManager, fileReference: .standalone(media: media), priority: priority) @@ -225,18 +232,63 @@ private final class PrefetchManagerImpl { } } } + + fileprivate func prepareNextGreetingSticker() { + let account = self.account + let engine = self.engine + self.preloadedGreetingStickerPromise.set(.single(nil) + |> then(engine.stickers.randomGreetingSticker() + |> map { item in + return item?.file + })) + + self.preloadGreetingStickerDisposable.set((self.preloadedGreetingStickerPromise.get() + |> mapToSignal { sticker -> Signal in + if let sticker = sticker { + let _ = freeMediaFileInteractiveFetched(account: account, fileReference: .standalone(media: sticker)).start() + return chatMessageAnimationData(postbox: account.postbox, resource: sticker.resource, fitzModifier: nil, width: 384, height: 384, synchronousLoad: false) + |> mapToSignal { _ -> Signal in + return .complete() + } + } else { + return .complete() + } + }).start()) + } } -final class PrefetchManager { +final class PrefetchManagerImpl: PrefetchManager { private let queue: Queue - private let impl: QueueLocalObject + private let impl: QueueLocalObject + private let uuid = Atomic(value: UUID()) - init(sharedContext: SharedAccountContext, account: Account, fetchManager: FetchManager) { + init(sharedContext: SharedAccountContext, account: Account, engine: TelegramEngine, fetchManager: FetchManager) { let queue = Queue.mainQueue() self.queue = queue self.impl = QueueLocalObject(queue: queue, generate: { - return PrefetchManagerImpl(queue: queue, sharedContext: sharedContext, account: account, fetchManager: fetchManager) + return PrefetchManagerInnerImpl(queue: queue, sharedContext: sharedContext, account: account, engine: engine, fetchManager: fetchManager) }) } + + var preloadedGreetingSticker: ChatGreetingData { + let signal: Signal = Signal { subscriber in + let disposable = MetaDisposable() + self.impl.with { impl in + disposable.set((impl.preloadedGreetingStickerPromise.get() |> take(1)).start(next: { file in + subscriber.putNext(file) + subscriber.putCompletion() + })) + } + return disposable + } + return ChatGreetingData(uuid: uuid.with { $0 }, sticker: signal) + } + + func prepareNextGreetingSticker() { + let _ = uuid.swap(UUID()) + self.impl.with { impl in + impl.prepareNextGreetingSticker() + } + } } diff --git a/submodules/TelegramUI/Sources/PreparedChatHistoryViewTransition.swift b/submodules/TelegramUI/Sources/PreparedChatHistoryViewTransition.swift index 2b11046ca0..386b553b7f 100644 --- a/submodules/TelegramUI/Sources/PreparedChatHistoryViewTransition.swift +++ b/submodules/TelegramUI/Sources/PreparedChatHistoryViewTransition.swift @@ -7,14 +7,60 @@ import Display import MergeLists import AccountContext -func preparedChatHistoryViewTransition(from fromView: ChatHistoryView?, to toView: ChatHistoryView, reason: ChatHistoryViewTransitionReason, reverse: Bool, chatLocation: ChatLocation, controllerInteraction: ChatControllerInteraction, scrollPosition: ChatHistoryViewScrollPosition?, initialData: InitialMessageHistoryData?, keyboardButtonsMessage: Message?, cachedData: CachedPeerData?, cachedDataMessages: [MessageId: Message]?, readStateData: [PeerId: ChatHistoryCombinedInitialReadStateData]?, flashIndicators: Bool, updatedMessageSelection: Bool) -> ChatHistoryViewTransition { - let mergeResult: (deleteIndices: [Int], indicesAndItems: [(Int, ChatHistoryEntry, Int?)], updateIndices: [(Int, ChatHistoryEntry, Int)]) +func preparedChatHistoryViewTransition(from fromView: ChatHistoryView?, to toView: ChatHistoryView, reason: ChatHistoryViewTransitionReason, reverse: Bool, chatLocation: ChatLocation, controllerInteraction: ChatControllerInteraction, scrollPosition: ChatHistoryViewScrollPosition?, scrollAnimationCurve: ListViewAnimationCurve?, initialData: InitialMessageHistoryData?, keyboardButtonsMessage: Message?, cachedData: CachedPeerData?, cachedDataMessages: [MessageId: Message]?, readStateData: [PeerId: ChatHistoryCombinedInitialReadStateData]?, flashIndicators: Bool, updatedMessageSelection: Bool, messageTransitionNode: ChatMessageTransitionNode?) -> ChatHistoryViewTransition { + var mergeResult: (deleteIndices: [Int], indicesAndItems: [(Int, ChatHistoryEntry, Int?)], updateIndices: [(Int, ChatHistoryEntry, Int)]) let allUpdated = fromView?.associatedData != toView.associatedData if reverse { mergeResult = mergeListsStableWithUpdatesReversed(leftList: fromView?.filteredEntries ?? [], rightList: toView.filteredEntries, allUpdated: allUpdated) } else { mergeResult = mergeListsStableWithUpdates(leftList: fromView?.filteredEntries ?? [], rightList: toView.filteredEntries, allUpdated: allUpdated) } + + if let messageTransitionNode = messageTransitionNode, messageTransitionNode.hasOngoingTransitions, let previousEntries = fromView?.filteredEntries { + for i in 0 ..< mergeResult.updateIndices.count { + switch mergeResult.updateIndices[i].1 { + case let .MessageEntry(message, presentationData, flag, monthLocation, messageSelection, entryAttributes): + if messageTransitionNode.isAnimatingMessage(stableId: message.stableId) { + var updatedMessage = message + mediaLoop: for media in message.media { + if let webpage = media as? TelegramMediaWebpage, case .Loaded = webpage.content { + var filterMedia = false + switch previousEntries[mergeResult.updateIndices[i].2] { + case let .MessageEntry(previousMessage, _, _, _, _, _): + if previousMessage.media.contains(where: { value in + if let value = value as? TelegramMediaWebpage, case .Loaded = value.content { + return true + } else { + return false + } + }) { + if messageTransitionNode.hasScheduledUpdateMessageAfterAnimationCompleted(stableId: message.stableId) { + filterMedia = true + } + } else { + filterMedia = true + } + default: + break + } + + if filterMedia { + updatedMessage = message.withUpdatedMedia(message.media.filter { + $0 !== media + }) + messageTransitionNode.scheduleUpdateMessageAfterAnimationCompleted(stableId: message.stableId) + } + + break mediaLoop + } + } + mergeResult.updateIndices[i].1 = .MessageEntry(updatedMessage, presentationData, flag, monthLocation, messageSelection, entryAttributes) + } + default: + break + } + } + } var adjustedDeleteIndices: [ListViewDeleteItem] = [] let previousCount: Int @@ -91,13 +137,15 @@ func preparedChatHistoryViewTransition(from fromView: ChatHistoryView?, to toVie var scrolledToIndex: MessageHistoryAnchorIndex? var scrolledToSomeIndex = false + let curve: ListViewAnimationCurve = scrollAnimationCurve ?? .Default(duration: nil) + if let scrollPosition = scrollPosition { switch scrollPosition { case let .unread(unreadIndex): var index = toView.filteredEntries.count - 1 for entry in toView.filteredEntries { if case .UnreadEntry = entry { - scrollToItem = ListViewScrollToItem(index: index, position: .bottom(0.0), animated: false, curve: .Default(duration: nil), directionHint: .Down) + scrollToItem = ListViewScrollToItem(index: index, position: .bottom(0.0), animated: false, curve: curve, directionHint: .Down) break } index -= 1 @@ -107,7 +155,7 @@ func preparedChatHistoryViewTransition(from fromView: ChatHistoryView?, to toVie var index = toView.filteredEntries.count - 1 for entry in toView.filteredEntries { if entry.index >= unreadIndex { - scrollToItem = ListViewScrollToItem(index: index, position: .bottom(0.0), animated: false, curve: .Default(duration: nil), directionHint: .Down) + scrollToItem = ListViewScrollToItem(index: index, position: .bottom(0.0), animated: false, curve: curve, directionHint: .Down) break } index -= 1 @@ -118,7 +166,7 @@ func preparedChatHistoryViewTransition(from fromView: ChatHistoryView?, to toVie var index = 0 for entry in toView.filteredEntries.reversed() { if entry.index < unreadIndex { - scrollToItem = ListViewScrollToItem(index: index, position: .bottom(0.0), animated: false, curve: .Default(duration: nil), directionHint: .Down) + scrollToItem = ListViewScrollToItem(index: index, position: .bottom(0.0), animated: false, curve: curve, directionHint: .Down) break } index += 1 @@ -128,7 +176,7 @@ func preparedChatHistoryViewTransition(from fromView: ChatHistoryView?, to toVie var index = toView.filteredEntries.count - 1 for entry in toView.filteredEntries { if entry.index >= scrollIndex { - scrollToItem = ListViewScrollToItem(index: index, position: .top(relativeOffset), animated: false, curve: .Default(duration: nil), directionHint: .Down) + scrollToItem = ListViewScrollToItem(index: index, position: .top(relativeOffset), animated: false, curve: curve, directionHint: .Down) break } index -= 1 @@ -138,7 +186,7 @@ func preparedChatHistoryViewTransition(from fromView: ChatHistoryView?, to toVie var index = 0 for entry in toView.filteredEntries.reversed() { if entry.index < scrollIndex { - scrollToItem = ListViewScrollToItem(index: index, position: .top(0.0), animated: false, curve: .Default(duration: nil), directionHint: .Down) + scrollToItem = ListViewScrollToItem(index: index, position: .top(0.0), animated: false, curve: curve, directionHint: .Down) break } index += 1 @@ -151,7 +199,7 @@ func preparedChatHistoryViewTransition(from fromView: ChatHistoryView?, to toVie var index = toView.filteredEntries.count - 1 for entry in toView.filteredEntries { if scrollIndex.isLessOrEqual(to: entry.index) { - scrollToItem = ListViewScrollToItem(index: index, position: position, animated: animated, curve: .Default(duration: nil), directionHint: directionHint) + scrollToItem = ListViewScrollToItem(index: index, position: position, animated: animated, curve: curve, directionHint: directionHint) break } index -= 1 @@ -162,7 +210,7 @@ func preparedChatHistoryViewTransition(from fromView: ChatHistoryView?, to toVie for entry in toView.filteredEntries.reversed() { if !scrollIndex.isLess(than: entry.index) { scrolledToSomeIndex = true - scrollToItem = ListViewScrollToItem(index: index, position: position, animated: animated, curve: .Default(duration: nil), directionHint: directionHint) + scrollToItem = ListViewScrollToItem(index: index, position: position, animated: animated, curve: curve, directionHint: directionHint) break } index += 1 diff --git a/submodules/TelegramUI/Sources/ReplyAccessoryPanelNode.swift b/submodules/TelegramUI/Sources/ReplyAccessoryPanelNode.swift index 59fbe8fffb..afb5c86d93 100644 --- a/submodules/TelegramUI/Sources/ReplyAccessoryPanelNode.swift +++ b/submodules/TelegramUI/Sources/ReplyAccessoryPanelNode.swift @@ -29,7 +29,7 @@ final class ReplyAccessoryPanelNode: AccessoryPanelNode { var theme: PresentationTheme - init(context: AccountContext, messageId: MessageId, theme: PresentationTheme, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder) { + init(context: AccountContext, messageId: MessageId, theme: PresentationTheme, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, dateTimeFormat: PresentationDateTimeFormat) { self.messageId = messageId self.theme = theme @@ -48,10 +48,12 @@ final class ReplyAccessoryPanelNode: AccessoryPanelNode { self.titleNode = ImmediateTextNode() self.titleNode.maximumNumberOfLines = 1 self.titleNode.displaysAsynchronously = false + self.titleNode.insets = UIEdgeInsets(top: 3.0, left: 0.0, bottom: 3.0, right: 0.0) self.textNode = ImmediateTextNode() self.textNode.maximumNumberOfLines = 1 self.textNode.displaysAsynchronously = false + self.textNode.insets = UIEdgeInsets(top: 3.0, left: 0.0, bottom: 3.0, right: 0.0) self.imageNode = TransformImageNode() self.imageNode.contentAnimations = [.subsequentUpdates] @@ -73,7 +75,15 @@ final class ReplyAccessoryPanelNode: AccessoryPanelNode { self.messageDisposable.set((context.account.postbox.messageView(messageId) |> deliverOnMainQueue).start(next: { [weak self] messageView in if let strongSelf = self { + if messageView.message == nil { + Queue.mainQueue().justDispatch { + strongSelf.interfaceInteraction?.setupReplyMessage(nil, { _ in }) + } + return + } + let message = messageView.message + var authorName = "" var text = "" if let forwardInfo = message?.forwardInfo, forwardInfo.flags.contains(.isImported) { @@ -86,7 +96,7 @@ final class ReplyAccessoryPanelNode: AccessoryPanelNode { authorName = author.displayTitle(strings: strings, displayOrder: nameDisplayOrder) } if let message = message { - (text, _) = descriptionStringForMessage(contentSettings: context.currentContentSettings.with { $0 }, message: message, strings: strings, nameDisplayOrder: nameDisplayOrder, accountPeerId: context.account.peerId) + (text, _) = descriptionStringForMessage(contentSettings: context.currentContentSettings.with { $0 }, message: message, strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: dateTimeFormat, accountPeerId: context.account.peerId) } var updatedMediaReference: AnyMediaReference? @@ -152,7 +162,7 @@ final class ReplyAccessoryPanelNode: AccessoryPanelNode { let isMedia: Bool if let message = message { - switch messageContentKind(contentSettings: context.currentContentSettings.with { $0 }, message: message, strings: strings, nameDisplayOrder: nameDisplayOrder, accountPeerId: context.account.peerId) { + switch messageContentKind(contentSettings: context.currentContentSettings.with { $0 }, message: message, strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: dateTimeFormat, accountPeerId: context.account.peerId) { case .text: isMedia = false default: @@ -162,8 +172,8 @@ final class ReplyAccessoryPanelNode: AccessoryPanelNode { isMedia = false } - strongSelf.titleNode.attributedText = NSAttributedString(string: authorName, font: Font.medium(15.0), textColor: strongSelf.theme.chat.inputPanel.panelControlAccentColor) - strongSelf.textNode.attributedText = NSAttributedString(string: text, font: Font.regular(15.0), textColor: isMedia ? strongSelf.theme.chat.inputPanel.secondaryTextColor : strongSelf.theme.chat.inputPanel.primaryTextColor) + strongSelf.titleNode.attributedText = NSAttributedString(string: authorName, font: Font.medium(14.0), textColor: strongSelf.theme.chat.inputPanel.panelControlAccentColor) + strongSelf.textNode.attributedText = NSAttributedString(string: text, font: Font.regular(14.0), textColor: isMedia ? strongSelf.theme.chat.inputPanel.secondaryTextColor : strongSelf.theme.chat.inputPanel.primaryTextColor) let headerString: String if let message = message, message.flags.contains(.Incoming), let author = message.author { @@ -239,20 +249,28 @@ final class ReplyAccessoryPanelNode: AccessoryPanelNode { self.closeButton.frame = closeButtonFrame self.actionArea.frame = CGRect(origin: CGPoint(x: leftInset, y: 2.0), size: CGSize(width: closeButtonFrame.minX - leftInset, height: bounds.height)) - - self.lineNode.frame = CGRect(origin: CGPoint(x: leftInset, y: 8.0), size: CGSize(width: 2.0, height: bounds.size.height - 10.0)) + + if self.lineNode.supernode == self { + self.lineNode.frame = CGRect(origin: CGPoint(x: leftInset, y: 8.0), size: CGSize(width: 2.0, height: bounds.size.height - 10.0)) + } var imageTextInset: CGFloat = 0.0 if !self.imageNode.isHidden { imageTextInset = 9.0 + 35.0 } - self.imageNode.frame = CGRect(origin: CGPoint(x: leftInset + 9.0, y: 8.0), size: CGSize(width: 35.0, height: 35.0)) + if self.imageNode.supernode == self { + self.imageNode.frame = CGRect(origin: CGPoint(x: leftInset + 9.0, y: 8.0), size: CGSize(width: 35.0, height: 35.0)) + } let titleSize = self.titleNode.updateLayout(CGSize(width: bounds.size.width - leftInset - textLineInset - rightInset - textRightInset - imageTextInset, height: bounds.size.height)) - self.titleNode.frame = CGRect(origin: CGPoint(x: leftInset + textLineInset + imageTextInset, y: 7.0), size: titleSize) + if self.titleNode.supernode == self { + self.titleNode.frame = CGRect(origin: CGPoint(x: leftInset + textLineInset + imageTextInset - self.titleNode.insets.left, y: 7.0 - self.titleNode.insets.top), size: titleSize) + } let textSize = self.textNode.updateLayout(CGSize(width: bounds.size.width - leftInset - textLineInset - rightInset - textRightInset - imageTextInset, height: bounds.size.height)) - self.textNode.frame = CGRect(origin: CGPoint(x: leftInset + textLineInset + imageTextInset, y: 25.0), size: textSize) + if self.textNode.supernode == self { + self.textNode.frame = CGRect(origin: CGPoint(x: leftInset + textLineInset + imageTextInset - self.textNode.insets.left, y: 25.0 - self.textNode.insets.top), size: textSize) + } } @objc func closePressed() { diff --git a/submodules/TelegramUI/Sources/ServiceSoundManager.swift b/submodules/TelegramUI/Sources/ServiceSoundManager.swift index 6a856ca430..363c12ec75 100644 --- a/submodules/TelegramUI/Sources/ServiceSoundManager.swift +++ b/submodules/TelegramUI/Sources/ServiceSoundManager.swift @@ -20,8 +20,8 @@ public final class ServiceSoundManager { init() { self.queue.async { - self.messageDeliverySound = loadSystemSoundFromBundle(name: "MessageSent.caf") - self.incomingMessageSound = loadSystemSoundFromBundle(name: "notification.caf") + self.messageDeliverySound = loadSystemSoundFromBundle(name: "MessageSent.mp3") + self.incomingMessageSound = loadSystemSoundFromBundle(name: "notification.mp3") } } diff --git a/submodules/TelegramUI/Sources/ShareExtensionContext.swift b/submodules/TelegramUI/Sources/ShareExtensionContext.swift index 024c9efe42..99bb75a188 100644 --- a/submodules/TelegramUI/Sources/ShareExtensionContext.swift +++ b/submodules/TelegramUI/Sources/ShareExtensionContext.swift @@ -23,6 +23,7 @@ import PresentationDataUtils import ChatImportUI import ZipArchive import ActivityIndicator +import DebugSettingsUI private let inForeground = ValuePromise(false, ignoreRepeated: true) @@ -54,6 +55,7 @@ private enum ShareAuthorizationError { } public struct ShareRootControllerInitializationData { + public let appBundleId: String public let appGroupPath: String public let apiId: Int32 public let apiHash: String @@ -62,7 +64,8 @@ public struct ShareRootControllerInitializationData { public let appVersion: String public let bundleData: Data? - public init(appGroupPath: String, apiId: Int32, apiHash: String, languagesCategory: String, encryptionParameters: (Data, Data), appVersion: String, bundleData: Data?) { + public init(appBundleId: String, appGroupPath: String, apiId: Int32, apiHash: String, languagesCategory: String, encryptionParameters: (Data, Data), appVersion: String, bundleData: Data?) { + self.appBundleId = appBundleId self.appGroupPath = appGroupPath self.apiId = apiId self.apiHash = apiHash @@ -168,14 +171,14 @@ public class ShareRootControllerImpl { let rootPath = rootPathForBasePath(self.initializationData.appGroupPath) performAppGroupUpgrades(appGroupPath: self.initializationData.appGroupPath, rootPath: rootPath) - TempBox.initializeShared(basePath: rootPath, processType: "share", launchSpecificId: arc4random64()) + TempBox.initializeShared(basePath: rootPath, processType: "share", launchSpecificId: Int64.random(in: Int64.min ... Int64.max)) let logsPath = rootPath + "/share-logs" let _ = try? FileManager.default.createDirectory(atPath: logsPath, withIntermediateDirectories: true, attributes: nil) setupSharedLogger(rootPath: rootPath, path: logsPath) - let applicationBindings = TelegramApplicationBindings(isMainApp: false, containerPath: self.initializationData.appGroupPath, appSpecificScheme: "tg", openUrl: { _ in + let applicationBindings = TelegramApplicationBindings(isMainApp: false, appBundleId: self.initializationData.appBundleId, containerPath: self.initializationData.appGroupPath, appSpecificScheme: "tg", openUrl: { _ in }, openUniversalUrl: { _, completion in completion.completion(false) return @@ -198,6 +201,7 @@ public class ShareRootControllerImpl { return nil }, requestSetAlternateIconName: { _, f in f(false) + }, forceOrientation: { _ in }) let internalContext: InternalContext @@ -228,7 +232,7 @@ public class ShareRootControllerImpl { 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, apiHash: self.initializationData.apiHash, languagesCategory: self.initializationData.languagesCategory, appVersion: self.initializationData.appVersion, voipMaxLayer: 0, voipVersions: [], 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, sharedContainerPath: self.initializationData.appGroupPath, 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, voipVersions: [], appData: .single(self.initializationData.bundleData), autolockDeadine: .single(nil), encryptionProvider: OpenSSLEncryptionProvider()), rootPath: rootPath, legacyBasePath: nil, apsNotificationToken: .never(), voipNotificationToken: .never(), setNotificationCall: { _ in }, navigateToChat: { _, _, _ in }) presentationDataPromise.set(sharedContext.presentationData) internalContext = InternalContext(sharedContext: sharedContext) globalInternalContext = internalContext @@ -392,6 +396,16 @@ public class ShareRootControllerImpl { shareController.dismissed = { _ in self?.getExtensionContext()?.completeRequest(returningItems: nil, completionHandler: nil) } + shareController.debugAction = { + guard let strongSelf = self else { + return + } + let presentationData = internalContext.sharedContext.currentPresentationData.with { $0 } + let navigationController = NavigationController(mode: .single, theme: NavigationControllerTheme(presentationTheme: presentationData.theme)) + strongSelf.navigationController = navigationController + navigationController.viewControllers = [debugController(sharedContext: context.sharedContext, context: context)] + strongSelf.mainWindow?.present(navigationController, on: .root) + } cancelImpl = { [weak shareController] in shareController?.dismiss(completion: { [weak self] in @@ -429,7 +443,7 @@ public class ShareRootControllerImpl { let fileExtension = (fileName as NSString).pathExtension var archivePathValue: String? - var otherEntries: [(SSZipEntry, String, ChatHistoryImport.MediaType)] = [] + var otherEntries: [(SSZipEntry, String, TelegramEngine.HistoryImport.MediaType)] = [] var mainFile: TempBoxFile? let appConfiguration = context.currentAppConfiguration.with({ $0 }) @@ -514,7 +528,7 @@ public class ShareRootControllerImpl { } else { let entryFileName = (entryPath as NSString).lastPathComponent if !entryFileName.isEmpty { - let mediaType: ChatHistoryImport.MediaType + let mediaType: TelegramEngine.HistoryImport.MediaType let fullRange = NSRange(entryFileName.startIndex ..< entryFileName.endIndex, in: entryFileName) if photoRegex.firstMatch(in: entryFileName, options: [], range: fullRange) != nil { mediaType = .photo @@ -616,7 +630,8 @@ public class ShareRootControllerImpl { super.containerLayoutUpdated(layout, transition: transition) let indicatorSize = self.activityIndicator.measure(CGSize(width: 100.0, height: 100.0)) - transition.updateFrame(node: self.activityIndicator, frame: CGRect(origin: CGPoint(x: floor((layout.size.width - indicatorSize.width) / 2.0), y: self.navigationHeight + floor((layout.size.height - self.navigationHeight - indicatorSize.height) / 2.0)), size: indicatorSize)) + let navigationHeight = self.navigationLayout(layout: layout).navigationFrame.maxY + transition.updateFrame(node: self.activityIndicator, frame: CGRect(origin: CGPoint(x: floor((layout.size.width - indicatorSize.width) / 2.0), y: navigationHeight + floor((layout.size.height - navigationHeight - indicatorSize.height) / 2.0)), size: indicatorSize)) } } @@ -626,7 +641,7 @@ public class ShareRootControllerImpl { navigationController.viewControllers = [TempController(context: context)] strongSelf.mainWindow?.present(navigationController, on: .root) - let _ = (ChatHistoryImport.getInfo(account: context.account, header: mainFileHeader) + let _ = (context.engine.historyImport.getInfo(header: mainFileHeader) |> deliverOnMainQueue).start(next: { parseInfo in switch parseInfo { case let .group(groupTitle): @@ -680,7 +695,7 @@ public class ShareRootControllerImpl { strongSelf.mainWindow?.present(controller, on: .root) } else { controller.inProgress = true - let _ = (ChatHistoryImport.checkPeerImport(account: context.account, peerId: peer.id) + let _ = (context.engine.historyImport.checkPeerImport(peerId: peer.id) |> deliverOnMainQueue).start(next: { result in controller.inProgress = false @@ -763,7 +778,7 @@ public class ShareRootControllerImpl { resolvedGroupTitle = "Group" } let controller = standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: presentationData.strings.ChatImport_CreateGroupAlertTitle, text: presentationData.strings.ChatImport_CreateGroupAlertText(resolvedGroupTitle).0, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.ChatImport_CreateGroupAlertImportAction, action: { - var signal: Signal = createSupergroup(account: context.account, title: resolvedGroupTitle, description: nil, isForHistoryImport: true) + var signal: Signal = context.engine.peers.createSupergroup(title: resolvedGroupTitle, description: nil, isForHistoryImport: true) |> map(Optional.init) |> `catch` { _ -> Signal in return .single(nil) @@ -831,7 +846,7 @@ public class ShareRootControllerImpl { attemptSelectionImpl = { [weak controller] peer in controller?.inProgress = true - let _ = (ChatHistoryImport.checkPeerImport(account: context.account, peerId: peer.id) + let _ = (context.engine.historyImport.checkPeerImport(peerId: peer.id) |> deliverOnMainQueue).start(next: { result in controller?.inProgress = false @@ -906,7 +921,7 @@ public class ShareRootControllerImpl { attemptSelectionImpl = { [weak controller] peer in controller?.inProgress = true - let _ = (ChatHistoryImport.checkPeerImport(account: context.account, peerId: peer.id) + let _ = (context.engine.historyImport.checkPeerImport(peerId: peer.id) |> deliverOnMainQueue).start(next: { result in controller?.inProgress = false @@ -1007,7 +1022,7 @@ public class ShareRootControllerImpl { resolvedGroupTitle = "Group" } let controller = standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: presentationData.strings.ChatImport_CreateGroupAlertTitle, text: presentationData.strings.ChatImport_CreateGroupAlertText(resolvedGroupTitle).0, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.ChatImport_CreateGroupAlertImportAction, action: { - var signal: Signal = createSupergroup(account: context.account, title: resolvedGroupTitle, description: nil, isForHistoryImport: true) + var signal: Signal = context.engine.peers.createSupergroup(title: resolvedGroupTitle, description: nil, isForHistoryImport: true) |> map(Optional.init) |> `catch` { _ -> Signal in return .single(nil) diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index e740766246..7e98762c38 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -24,6 +24,7 @@ import AlertUI import PresentationDataUtils import LocationUI import AppLock +import WallpaperBackgroundNode private final class AccountUserInterfaceInUseContext { let subscribers = Bag<(Bool) -> Void>() @@ -55,6 +56,7 @@ private var testHasInstance = false public final class SharedAccountContextImpl: SharedAccountContext { public let mainWindow: Window1? public let applicationBindings: TelegramApplicationBindings + public let sharedContainerPath: String public let basePath: String public let accountManager: AccountManager public let appLockContext: AppLockContext @@ -160,7 +162,7 @@ public final class SharedAccountContextImpl: SharedAccountContext { 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 }) { + public init(mainWindow: Window1?, sharedContainerPath: String, basePath: String, encryptionParameters: ValueBoxEncryptionParameters, accountManager: AccountManager, appLockContext: AppLockContext, applicationBindings: TelegramApplicationBindings, initialPresentationDataAndSettings: InitialPresentationDataAndSettings, networkArguments: NetworkInitializationArguments, rootPath: String, legacyBasePath: String?, apsNotificationToken: Signal, voipNotificationToken: Signal, setNotificationCall: @escaping (PresentationCall?) -> Void, navigateToChat: @escaping (AccountRecordId, PeerId, MessageId?) -> Void, displayUpgradeProgress: @escaping (Float?) -> Void = { _ in }) { assert(Queue.mainQueue().isCurrent()) precondition(!testHasInstance) @@ -168,6 +170,7 @@ public final class SharedAccountContextImpl: SharedAccountContext { self.mainWindow = mainWindow self.applicationBindings = applicationBindings + self.sharedContainerPath = sharedContainerPath self.basePath = basePath self.accountManager = accountManager self.navigateToChatImpl = navigateToChat @@ -864,7 +867,7 @@ public final class SharedAccountContextImpl: SharedAccountContext { guard let token = token else { return .complete() } - return unregisterNotificationToken(account: account, token: token, type: .aps(encrypt: false), otherAccountUserIds: (account.testingEnvironment ? allTestingUserIds : allProductionUserIds).filter({ $0 != account.peerId.id })) + return TelegramEngine(account: account).accountData.unregisterNotificationToken(token: token, type: .aps(encrypt: false), otherAccountUserIds: (account.testingEnvironment ? allTestingUserIds : allProductionUserIds).filter({ $0 != account.peerId.id })) } appliedVoip = self.voipNotificationToken |> distinctUntilChanged(isEqual: { $0 == $1 }) @@ -872,7 +875,7 @@ public final class SharedAccountContextImpl: SharedAccountContext { guard let token = token else { return .complete() } - return unregisterNotificationToken(account: account, token: token, type: .voip, otherAccountUserIds: (account.testingEnvironment ? allTestingUserIds : allProductionUserIds).filter({ $0 != account.peerId.id })) + return TelegramEngine(account: account).accountData.unregisterNotificationToken(token: token, type: .voip, otherAccountUserIds: (account.testingEnvironment ? allTestingUserIds : allProductionUserIds).filter({ $0 != account.peerId.id })) } } else { appliedAps = self.apsNotificationToken @@ -887,7 +890,7 @@ public final class SharedAccountContextImpl: SharedAccountContext { } else { encrypt = false } - return registerNotificationToken(account: account, token: token, type: .aps(encrypt: encrypt), sandbox: sandbox, otherAccountUserIds: (account.testingEnvironment ? activeTestingUserIds : activeProductionUserIds).filter({ $0 != account.peerId.id }), excludeMutedChats: !settings.includeMuted) + return TelegramEngine(account: account).accountData.registerNotificationToken(token: token, type: .aps(encrypt: encrypt), sandbox: sandbox, otherAccountUserIds: (account.testingEnvironment ? activeTestingUserIds : activeProductionUserIds).filter({ $0 != account.peerId.id }), excludeMutedChats: !settings.includeMuted) } appliedVoip = self.voipNotificationToken |> distinctUntilChanged(isEqual: { $0 == $1 }) @@ -895,7 +898,7 @@ public final class SharedAccountContextImpl: SharedAccountContext { guard let token = token else { return .complete() } - return registerNotificationToken(account: account, token: token, type: .voip, sandbox: sandbox, otherAccountUserIds: (account.testingEnvironment ? activeTestingUserIds : activeProductionUserIds).filter({ $0 != account.peerId.id }), excludeMutedChats: !settings.includeMuted) + return TelegramEngine(account: account).accountData.registerNotificationToken(token: token, type: .voip, sandbox: sandbox, otherAccountUserIds: (account.testingEnvironment ? activeTestingUserIds : activeProductionUserIds).filter({ $0 != account.peerId.id }), excludeMutedChats: !settings.includeMuted) } } @@ -1121,7 +1124,7 @@ public final class SharedAccountContextImpl: SharedAccountContext { if !found { let controllerParams = LocationViewParams(sendLiveLocation: { location in - let outMessage: EnqueueMessage = .message(text: "", attributes: [], mediaReference: .standalone(media: location), replyToMessageId: nil, localGroupingKey: nil) + let outMessage: EnqueueMessage = .message(text: "", attributes: [], mediaReference: .standalone(media: location), replyToMessageId: nil, localGroupingKey: nil, correlationId: nil) // params.enqueueMessage(outMessage) }, stopLiveLocation: { messageId in if let messageId = messageId { @@ -1144,8 +1147,8 @@ public final class SharedAccountContextImpl: SharedAccountContext { } } - public func resolveUrl(account: Account, url: String, skipUrlAuth: Bool) -> Signal { - return resolveUrlImpl(account: account, url: url, skipUrlAuth: skipUrlAuth) + public func resolveUrl(context: AccountContext, peerId: PeerId?, url: String, skipUrlAuth: Bool) -> Signal { + return resolveUrlImpl(context: context, peerId: peerId, url: url, skipUrlAuth: skipUrlAuth) } public func openResolvedUrl(_ resolvedUrl: ResolvedUrl, context: AccountContext, urlContext: OpenURLContext, navigationController: NavigationController?, openPeer: @escaping (PeerId, ChatControllerInteractionNavigateToPeer) -> Void, sendFile: ((FileMediaReference) -> Void)?, sendSticker: ((FileMediaReference, ASDisplayNode, CGRect) -> Bool)?, requestMessageActionUrlAuth: ((MessageActionUrlSubject) -> Void)?, joinVoiceChat: ((PeerId, String?, CachedChannelData.ActiveCall) -> Void)?, present: @escaping (ViewController, Any?) -> Void, dismissInput: @escaping () -> Void, contentContext: Any?) { @@ -1216,67 +1219,65 @@ public final class SharedAccountContextImpl: SharedAccountContext { return PeerSelectionControllerImpl(params) } - public func makeChatMessagePreviewItem(context: AccountContext, messages: [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 { + public func makeChatMessagePreviewItem(context: AccountContext, messages: [Message], theme: PresentationTheme, strings: PresentationStrings, wallpaper: TelegramWallpaper, fontSize: PresentationFontSize, chatBubbleCorners: PresentationChatBubbleCorners, dateTimeFormat: PresentationDateTimeFormat, nameOrder: PresentationPersonNameOrder, forcedResourceStatus: FileMediaResourceStatus?, tapMessage: ((Message) -> Void)? = nil, clickThroughMessage: (() -> Void)? = nil, backgroundNode: ASDisplayNode?) -> 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 }, navigateToMessageStandalone: { _ in - }, tapMessage: { message in - tapMessage?(message) - }, clickThroughMessage: { - clickThroughMessage?() - }, toggleMessagesSelection: { _, _ in }, sendCurrentMessage: { _ in }, sendMessage: { _ in }, sendSticker: { _, _, _, _, _ in return false }, sendGif: { _, _, _ in return false }, sendBotContextResultAsGif: { _, _, _, _ 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 .none - }, navigateToFirstDateMessage: { _ in - }, requestRedeliveryOfFailedMessages: { _ in - }, addContact: { _ in - }, rateCall: { _, _, _ in - }, requestSelectMessagePollOptions: { _, _ in - }, requestOpenMessagePollResults: { _, _ in - }, openAppStorePage: { - }, displayMessageTooltip: { _, _, _, _ in - }, seekToTimecode: { _, _, _ in - }, scheduleCurrentMessage: { - }, sendScheduledMessagesNow: { _ in - }, editScheduledMessagesTime: { _ in - }, performTextSelectionAction: { _, _, _ in - }, updateMessageLike: { _, _ in - }, openMessageReactions: { _ in - }, displayImportedMessageTooltip: { _ in - }, displaySwipeToReplyHint: { - }, dismissReplyMarkupMessage: { _ in - }, openMessagePollResults: { _, _ in - }, openPollCreation: { _ in - }, displayPollSolution: { _, _ in - }, displayPsa: { _, _ in - }, displayDiceTooltip: { _ in - }, animateDiceSuccess: { _ in - }, greetingStickerNode: { - return nil - }, openPeerContextMenu: { _, _, _, _, _ in - }, openMessageReplies: { _, _, _ in - }, openReplyThreadOriginalMessage: { _ in - }, openMessageStats: { _ in - }, editMessageMedia: { _, _ in - }, copyText: { _ in - }, displayUndo: { _ in - }, requestMessageUpdate: { _ in - }, cancelInteractiveKeyboardGestures: { - }, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings, - pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(loopAnimatedStickers: false)) - } else { - controllerInteraction = defaultChatControllerInteraction - } + + controllerInteraction = ChatControllerInteraction(openMessage: { _, _ in + return false }, openPeer: { _, _, _ in }, openPeerMention: { _ in }, openMessageContextMenu: { _, _, _, _, _ in }, activateMessagePinch: { _ in + }, openMessageContextActions: { _, _, _, _ in }, navigateToMessage: { _, _ in }, navigateToMessageStandalone: { _ in + }, tapMessage: { message in + tapMessage?(message) + }, clickThroughMessage: { + clickThroughMessage?() + }, toggleMessagesSelection: { _, _ in }, sendCurrentMessage: { _ in }, sendMessage: { _ in }, sendSticker: { _, _, _, _, _, _, _ in return false }, sendGif: { _, _, _, _, _ in return false }, sendBotContextResultAsGif: { _, _, _, _, _ 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 .none + }, navigateToFirstDateMessage: { _ in + }, requestRedeliveryOfFailedMessages: { _ in + }, addContact: { _ in + }, rateCall: { _, _, _ in + }, requestSelectMessagePollOptions: { _, _ in + }, requestOpenMessagePollResults: { _, _ in + }, openAppStorePage: { + }, displayMessageTooltip: { _, _, _, _ in + }, seekToTimecode: { _, _, _ in + }, scheduleCurrentMessage: { + }, sendScheduledMessagesNow: { _ in + }, editScheduledMessagesTime: { _ in + }, performTextSelectionAction: { _, _, _ in + }, updateMessageLike: { _, _ in + }, openMessageReactions: { _ in + }, displayImportedMessageTooltip: { _ in + }, displaySwipeToReplyHint: { + }, dismissReplyMarkupMessage: { _ in + }, openMessagePollResults: { _, _ in + }, openPollCreation: { _ in + }, displayPollSolution: { _, _ in + }, displayPsa: { _, _ in + }, displayDiceTooltip: { _ in + }, animateDiceSuccess: { _ in + }, openPeerContextMenu: { _, _, _, _, _ in + }, openMessageReplies: { _, _, _ in + }, openReplyThreadOriginalMessage: { _ in + }, openMessageStats: { _ in + }, editMessageMedia: { _, _ in + }, copyText: { _ in + }, displayUndo: { _ in + }, isAnimatingMessage: { _ in + return false + }, requestMessageUpdate: { _ in + }, cancelInteractiveKeyboardGestures: { + }, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings, + pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(loopAnimatedStickers: false), presentationContext: ChatPresentationContext(backgroundNode: backgroundNode as? WallpaperBackgroundNode)) let content: ChatMessageItemContent let chatLocation: ChatLocation @@ -1342,7 +1343,7 @@ public final class SharedAccountContextImpl: SharedAccountContext { present(WalletSplashScreen(context: WalletContextImpl(context: context, tonContext: tonContext), mode: .created(walletInfo, nil), walletCreatedPreloadState: nil)) } case let .send(address, amount, comment): - present(walletSendScreen(context: WalletContextImpl(context: context, tonContext: tonContext), randomId: arc4random64(), walletInfo: walletInfo, address: address, amount: amount, comment: comment)) + present(walletSendScreen(context: WalletContextImpl(context: context, tonContext: tonContext), randomId: Int64.random(in: Int64.min ... Int64.max), walletInfo: walletInfo, address: address, amount: amount, comment: comment)) } }) @@ -1390,7 +1391,7 @@ public final class SharedAccountContextImpl: SharedAccountContext { } public func makeRecentSessionsController(context: AccountContext, activeSessionsContext: ActiveSessionsContext) -> ViewController & RecentSessionsController { - return recentSessionsController(context: context, activeSessionsContext: activeSessionsContext, webSessionsContext: WebSessionsContext(account: context.account), websitesOnly: false) + return recentSessionsController(context: context, activeSessionsContext: activeSessionsContext, webSessionsContext: context.engine.privacy.webSessions(), websitesOnly: false) } public func makePrivacyAndSecurityController(context: AccountContext) -> ViewController { diff --git a/submodules/TelegramUI/Sources/SharedNotificationManager.swift b/submodules/TelegramUI/Sources/SharedNotificationManager.swift index 2c6796bd7b..603a877a6f 100644 --- a/submodules/TelegramUI/Sources/SharedNotificationManager.swift +++ b/submodules/TelegramUI/Sources/SharedNotificationManager.swift @@ -250,13 +250,13 @@ public final class SharedNotificationManager { var peerId: PeerId? if let fromId = payload["from_id"] { let fromIdValue = fromId as! NSString - peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: Int32(fromIdValue.intValue)) + peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt32Value(Int32(fromIdValue.intValue))) } else if let fromId = payload["chat_id"] { let fromIdValue = fromId as! NSString - peerId = PeerId(namespace: Namespaces.Peer.CloudGroup, id: Int32(fromIdValue.intValue)) + peerId = PeerId(namespace: Namespaces.Peer.CloudGroup, id: PeerId.Id._internalFromInt32Value(Int32(fromIdValue.intValue))) } else if let fromId = payload["channel_id"] { let fromIdValue = fromId as! NSString - peerId = PeerId(namespace: Namespaces.Peer.CloudChannel, id: Int32(fromIdValue.intValue)) + peerId = PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt32Value(Int32(fromIdValue.intValue))) } if let peerId = peerId { if let messageIds = payload["messages"] as? String { @@ -314,13 +314,13 @@ public final class SharedNotificationManager { if let fromId = payload["from_id"] { let fromIdValue = fromId as! NSString - peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: Int32(fromIdValue.intValue)) + peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt32Value(Int32(fromIdValue.intValue))) } else if let fromId = payload["chat_id"] { let fromIdValue = fromId as! NSString - peerId = PeerId(namespace: Namespaces.Peer.CloudGroup, id: Int32(fromIdValue.intValue)) + peerId = PeerId(namespace: Namespaces.Peer.CloudGroup, id: PeerId.Id._internalFromInt32Value(Int32(fromIdValue.intValue))) } else if let fromId = payload["channel_id"] { let fromIdValue = fromId as! NSString - peerId = PeerId(namespace: Namespaces.Peer.CloudChannel, id: Int32(fromIdValue.intValue)) + peerId = PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt32Value(Int32(fromIdValue.intValue))) } if let msgId = payload["msg_id"] { @@ -332,7 +332,7 @@ public final class SharedNotificationManager { let randomIdValue = randomId as! NSString var peerId: PeerId? if let encryptionIdString = payload["encryption_id"] as? String, let encryptionId = Int32(encryptionIdString) { - peerId = PeerId(namespace: Namespaces.Peer.SecretChat, id: encryptionId) + peerId = PeerId(namespace: Namespaces.Peer.SecretChat, id: PeerId.Id._internalFromInt32Value(encryptionId)) } notificationRequestId = .globallyUniqueId(randomIdValue.longLongValue, peerId) } else { @@ -344,13 +344,13 @@ public final class SharedNotificationManager { if let fromId = payload["from_id"] { let fromIdValue = fromId as! NSString - peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: Int32(fromIdValue.intValue)) + peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt32Value(Int32(fromIdValue.intValue))) } else if let fromId = payload["chat_id"] { let fromIdValue = fromId as! NSString - peerId = PeerId(namespace: Namespaces.Peer.CloudGroup, id: Int32(fromIdValue.intValue)) + peerId = PeerId(namespace: Namespaces.Peer.CloudGroup, id: PeerId.Id._internalFromInt32Value(Int32(fromIdValue.intValue))) } else if let fromId = payload["channel_id"] { let fromIdValue = fromId as! NSString - peerId = PeerId(namespace: Namespaces.Peer.CloudChannel, id: Int32(fromIdValue.intValue)) + peerId = PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt32Value(Int32(fromIdValue.intValue))) } if let peerId = peerId { @@ -397,7 +397,7 @@ public final class SharedNotificationManager { if !messagesDeleted.isEmpty { let _ = account.postbox.transaction(ignoreDisabled: true, { transaction -> Void in - deleteMessages(transaction: transaction, mediaBox: account.postbox.mediaBox, ids: messagesDeleted) + TelegramEngine(account: account).messages.deleteMessages(transaction: transaction, ids: messagesDeleted) }).start() } diff --git a/submodules/TelegramUI/Sources/StickerPaneSearchContentNode.swift b/submodules/TelegramUI/Sources/StickerPaneSearchContentNode.swift index 6f585cf323..d02c198e72 100644 --- a/submodules/TelegramUI/Sources/StickerPaneSearchContentNode.swift +++ b/submodules/TelegramUI/Sources/StickerPaneSearchContentNode.swift @@ -227,7 +227,7 @@ final class StickerPaneSearchContentNode: ASDisplayNode, PaneSearchContentNode { 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, nil, false, sourceNode, sourceRect) + return strongSelf.controllerInteraction.sendSticker(fileReference, false, false, nil, false, sourceNode, sourceRect) } else { return false } @@ -238,24 +238,23 @@ final class StickerPaneSearchContentNode: ASDisplayNode, PaneSearchContentNode { guard let strongSelf = self else { return } - let account = strongSelf.context.account + let context = strongSelf.context if install { - var installSignal = loadedStickerPack(postbox: strongSelf.context.account.postbox, network: strongSelf.context.account.network, reference: .id(id: info.id.id, accessHash: info.accessHash), forceActualized: false) + var installSignal = strongSelf.context.engine.stickers.loadedStickerPack(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) + return preloadedStickerPackThumbnail(account: context.account, info: info, items: items) |> filter { $0 } |> ignoreValues |> then( - addStickerPackInteractively(postbox: strongSelf.context.account.postbox, info: info, items: items) + context.engine.stickers.addStickerPackInteractively(info: info, items: items) |> ignoreValues ) |> mapToSignal { _ -> Signal<(StickerPackCollectionInfo, [ItemCollectionItem]), NoError> in - return .complete() } |> then(.single((info, items))) } @@ -312,18 +311,18 @@ final class StickerPaneSearchContentNode: ASDisplayNode, PaneSearchContentNode { } 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 + 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, context: strongSelf.context), elevatedLayout: false, animateInAsReplacement: animateInAsReplacement, action: { _ in return true })) })) } else { - let _ = (removeStickerPackInteractively(postbox: account.postbox, id: info.id, option: .delete) + let _ = (context.engine.stickers.removeStickerPackInteractively(id: info.id, option: .delete) |> deliverOnMainQueue).start(next: { _ in }) } }, sendSticker: { [weak self] file, sourceNode, sourceRect in if let strongSelf = self { - let _ = strongSelf.controllerInteraction.sendSticker(file, nil, false, sourceNode, sourceRect) + let _ = strongSelf.controllerInteraction.sendSticker(file, false, false, nil, false, sourceNode, sourceRect) } }, getItemIsPreviewed: { item in return inputNodeInteraction.previewedStickerPackItem == .pack(item) @@ -343,22 +342,22 @@ final class StickerPaneSearchContentNode: ASDisplayNode, PaneSearchContentNode { func updateText(_ text: String, languageCode: String?) { let signal: Signal<([(String?, FoundStickerItem)], FoundStickerSets, Bool, FoundStickerSets?)?, NoError> if !text.isEmpty { - let account = self.context.account + let context = self.context let stickers: Signal<[(String?, FoundStickerItem)], NoError> = Signal { subscriber in var signals: Signal<[Signal<(String?, [FoundStickerItem]), NoError>], NoError> = .single([]) let query = text.trimmingCharacters(in: .whitespacesAndNewlines) if query.isSingleEmoji { - signals = .single([searchStickers(account: account, query: text.basicEmoji.0) + signals = .single([context.engine.stickers.searchStickers(query: text.basicEmoji.0) |> map { (nil, $0) }]) } else if query.count > 1, let languageCode = languageCode, !languageCode.isEmpty && languageCode != "emoji" { - var signal = searchEmojiKeywords(postbox: account.postbox, inputLanguageCode: languageCode, query: query.lowercased(), completeMatch: query.count < 3) + var signal = context.engine.stickers.searchEmojiKeywords(inputLanguageCode: languageCode, query: query.lowercased(), completeMatch: query.count < 3) if !languageCode.lowercased().hasPrefix("en") { signal = signal |> mapToSignal { keywords in return .single(keywords) |> then( - searchEmojiKeywords(postbox: account.postbox, inputLanguageCode: "en-US", query: query.lowercased(), completeMatch: query.count < 3) + context.engine.stickers.searchEmojiKeywords(inputLanguageCode: "en-US", query: query.lowercased(), completeMatch: query.count < 3) |> map { englishKeywords in return keywords + englishKeywords } @@ -371,7 +370,7 @@ final class StickerPaneSearchContentNode: ASDisplayNode, PaneSearchContentNode { var signals: [Signal<(String?, [FoundStickerItem]), NoError>] = [] let emoticons = keywords.flatMap { $0.emoticons } for emoji in emoticons { - signals.append(searchStickers(account: self.context.account, query: emoji.basicEmoji.0) + signals.append(context.engine.stickers.searchStickers(query: emoji.basicEmoji.0) |> take(1) |> map { (emoji, $0) }) } @@ -395,8 +394,8 @@ final class StickerPaneSearchContentNode: ASDisplayNode, PaneSearchContentNode { }) } - let local = searchStickerSets(postbox: context.account.postbox, query: text) - let remote = searchStickerSetsRemotely(network: context.account.network, query: text) + let local = context.engine.stickers.searchStickerSets(query: text) + let remote = context.engine.stickers.searchStickerSetsRemotely(query: text) |> delay(0.2, queue: Queue.mainQueue()) let rawPacks = local |> mapToSignal { result -> Signal<(FoundStickerSets, Bool, FoundStickerSets?), NoError> in diff --git a/submodules/TelegramUI/Sources/StickerPaneSearchStickerItem.swift b/submodules/TelegramUI/Sources/StickerPaneSearchStickerItem.swift index 3d729acda4..31c9514759 100644 --- a/submodules/TelegramUI/Sources/StickerPaneSearchStickerItem.swift +++ b/submodules/TelegramUI/Sources/StickerPaneSearchStickerItem.swift @@ -107,8 +107,8 @@ private let textFont = Font.regular(20.0) final class StickerPaneSearchStickerItemNode: GridItemNode { private var currentState: (Account, FoundStickerItem, CGSize)? - private let imageNode: TransformImageNode - private var animationNode: AnimatedStickerNode? + let imageNode: TransformImageNode + private(set) var animationNode: AnimatedStickerNode? private let textNode: ASTextNode private let stickerFetchedDisposable = MetaDisposable() diff --git a/submodules/TelegramUI/Sources/StickersChatInputContextPanelItem.swift b/submodules/TelegramUI/Sources/StickersChatInputContextPanelItem.swift index 2431113e7e..b6d6abcde9 100644 --- a/submodules/TelegramUI/Sources/StickersChatInputContextPanelItem.swift +++ b/submodules/TelegramUI/Sources/StickersChatInputContextPanelItem.swift @@ -126,7 +126,7 @@ final class StickersChatInputContextPanelItemNode: ListViewItemNode { for i in 0 ..< self.nodes.count { if self.nodes[i].frame.contains(location) { let file = item.files[i] - item.interfaceInteraction.sendSticker(.standalone(media: file), self.nodes[i], self.nodes[i].bounds) + item.interfaceInteraction.sendSticker(.standalone(media: file), true, self.nodes[i], self.nodes[i].bounds) break } } diff --git a/submodules/TelegramUI/Sources/StickersChatInputContextPanelNode.swift b/submodules/TelegramUI/Sources/StickersChatInputContextPanelNode.swift index 7881a01849..1a3cf9a5f3 100644 --- a/submodules/TelegramUI/Sources/StickersChatInputContextPanelNode.swift +++ b/submodules/TelegramUI/Sources/StickersChatInputContextPanelNode.swift @@ -11,6 +11,7 @@ import TelegramUIPreferences import MergeLists import AccountContext import StickerPackPreviewUI +import ContextUI private struct StickersChatInputContextPanelEntryStableId: Hashable { let ids: [MediaId] @@ -130,12 +131,16 @@ final class StickersChatInputContextPanelNode: ChatInputContextPanelNode { |> deliverOnMainQueue |> map { isStarred -> (ASDisplayNode, PeekControllerContent)? in if let strongSelf = self, let controllerInteraction = strongSelf.controllerInteraction { - var menuItems: [PeekControllerMenuItem] = [] + var menuItems: [ContextMenuItem] = [] menuItems = [ - PeekControllerMenuItem(title: strongSelf.strings.StickerPack_Send, color: .accent, font: .bold, action: { _, _ in - return controllerInteraction.sendSticker(.standalone(media: item.file), nil, true, itemNode, itemNode.bounds) - }), - PeekControllerMenuItem(title: isStarred ? strongSelf.strings.Stickers_RemoveFromFavorites : strongSelf.strings.Stickers_AddToFavorites, color: isStarred ? .destructive : .accent, action: { _, _ in + .action(ContextMenuActionItem(text: strongSelf.strings.StickerPack_Send, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Resend"), color: theme.contextMenu.primaryColor) }, action: { _, f in + f(.default) + + let _ = controllerInteraction.sendSticker(.standalone(media: item.file), false, false, nil, true, itemNode, itemNode.bounds) + })), + .action(ContextMenuActionItem(text: isStarred ? strongSelf.strings.Stickers_RemoveFromFavorites : strongSelf.strings.Stickers_AddToFavorites, icon: { theme in generateTintedImage(image: isStarred ? UIImage(bundleImageName: "Chat/Context Menu/Unstar") : UIImage(bundleImageName: "Chat/Context Menu/Rate"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in + f(.default) + if let strongSelf = self { if isStarred { let _ = removeSavedSticker(postbox: strongSelf.context.account.postbox, mediaId: item.file.fileId).start() @@ -143,9 +148,10 @@ final class StickersChatInputContextPanelNode: ChatInputContextPanelNode { let _ = addSavedSticker(postbox: strongSelf.context.account.postbox, network: strongSelf.context.account.network, file: item.file).start() } } - return true - }), - PeekControllerMenuItem(title: strongSelf.strings.StickerPack_ViewPack, color: .accent, action: { _, _ in + })), + .action(ContextMenuActionItem(text: strongSelf.strings.StickerPack_ViewPack, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Sticker"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in + f(.default) + if let strongSelf = self, let controllerInteraction = strongSelf.controllerInteraction { loop: for attribute in item.file.attributes { switch attribute { @@ -153,11 +159,10 @@ final class StickersChatInputContextPanelNode: ChatInputContextPanelNode { if let packReference = packReference { 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, nil, true, sourceNode, sourceRect) + return controllerInteraction.sendSticker(file, false, false, nil, true, sourceNode, sourceRect) } else { return false } - }) controllerInteraction.navigationController()?.view.window?.endEditing(true) @@ -169,9 +174,7 @@ final class StickersChatInputContextPanelNode: ChatInputContextPanelNode { } } } - 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 { @@ -184,7 +187,8 @@ final class StickersChatInputContextPanelNode: ChatInputContextPanelNode { return nil }, present: { [weak self] content, sourceNode in if let strongSelf = self { - let controller = PeekController(theme: PeekControllerTheme(presentationTheme: strongSelf.theme), content: content, sourceNode: { + let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } + let controller = PeekController(presentationData: presentationData, content: content, sourceNode: { return sourceNode }) strongSelf.interfaceInteraction?.presentGlobalOverlayController(controller, nil) @@ -299,7 +303,10 @@ final class StickersChatInputContextPanelNode: ChatInputContextPanelNode { if let topItemOffset = topItemOffset { let position = strongSelf.listView.layer.position - strongSelf.listView.layer.animatePosition(from: CGPoint(x: position.x, y: position.y + (strongSelf.listView.bounds.size.height - topItemOffset)), to: position, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring) + strongSelf.listView.position = CGPoint(x: position.x, y: position.y + (strongSelf.listView.bounds.size.height - topItemOffset)) + ContainedViewLayoutTransition.animated(duration: 0.3, curve: .spring).animateView { + strongSelf.listView.position = position + } } } }) diff --git a/submodules/TelegramUI/Sources/TelegramAccountAuxiliaryMethods.swift b/submodules/TelegramUI/Sources/TelegramAccountAuxiliaryMethods.swift index ecb2de7a00..f60e0f42f5 100644 --- a/submodules/TelegramUI/Sources/TelegramAccountAuxiliaryMethods.swift +++ b/submodules/TelegramUI/Sources/TelegramAccountAuxiliaryMethods.swift @@ -9,6 +9,9 @@ import MusicAlbumArtResources import LocalMediaResources import LocationResources import ChatInterfaceState +import WallpaperResources +import AppBundle +import SwiftSignalKit public let telegramAccountAuxiliaryMethods = AccountAuxiliaryMethods(updatePeerChatInputState: { interfaceState, inputState -> PeerChatInterfaceState? in if interfaceState == nil { @@ -36,9 +39,59 @@ public let telegramAccountAuxiliaryMethods = AccountAuxiliaryMethods(updatePeerC } else if let resource = resource as? OpenInAppIconResource { return fetchOpenInAppIconResource(resource: resource) } else if let resource = resource as? EmojiSpriteResource { - return fetchEmojiSpriteResource(postbox: account.postbox, network: account.network, resource: resource) + return fetchEmojiSpriteResource(account: account, resource: resource) } else if let resource = resource as? VenueIconResource { return fetchVenueIconResource(account: account, resource: resource) + } else if let wallpaperResource = resource as? WallpaperDataResource { + let builtinWallpapers: [String] = [ + "fqv01SQemVIBAAAApND8LDRUhRU" + ] + if builtinWallpapers.contains(wallpaperResource.slug) { + if let url = getAppBundle().url(forResource: wallpaperResource.slug, withExtension: "tgv") { + return Signal { subscriber in + subscriber.putNext(.reset) + if let data = try? Data(contentsOf: url, options: .mappedRead) { + subscriber.putNext(.dataPart(resourceOffset: 0, data: data, range: 0 ..< data.count, complete: true)) + } + + return EmptyDisposable + } + } else { + return nil + } + } + return nil + } else if let cloudDocumentMediaResource = resource as? CloudDocumentMediaResource { + if cloudDocumentMediaResource.fileId == 5789658100176783156 { + if let url = getAppBundle().url(forResource: "fqv01SQemVIBAAAApND8LDRUhRU", withExtension: "tgv") { + return Signal { subscriber in + subscriber.putNext(.reset) + if let data = try? Data(contentsOf: url, options: .mappedRead) { + subscriber.putNext(.dataPart(resourceOffset: 0, data: data, range: 0 ..< data.count, complete: true)) + } + + return EmptyDisposable + } + } else { + return nil + } + } + } else if let cloudDocumentSizeMediaResource = resource as? CloudDocumentSizeMediaResource { + if cloudDocumentSizeMediaResource.documentId == 5789658100176783156 && cloudDocumentSizeMediaResource.sizeSpec == "m" { + if let url = getAppBundle().url(forResource: "5789658100176783156-m", withExtension: "resource") { + return Signal { subscriber in + subscriber.putNext(.reset) + if let data = try? Data(contentsOf: url, options: .mappedRead) { + subscriber.putNext(.dataPart(resourceOffset: 0, data: data, range: 0 ..< data.count, complete: true)) + } + + return EmptyDisposable + } + } else { + return nil + } + } + return nil } return nil }, fetchResourceMediaReferenceHash: { resource in diff --git a/submodules/TelegramUI/Sources/TelegramRootController.swift b/submodules/TelegramUI/Sources/TelegramRootController.swift index 42f7e00bfc..2b96a8aa10 100644 --- a/submodules/TelegramUI/Sources/TelegramRootController.swift +++ b/submodules/TelegramUI/Sources/TelegramRootController.swift @@ -14,6 +14,7 @@ import ChatListUI import SettingsUI import AppBundle import DatePickerNode +import DebugSettingsUI public final class TelegramRootController: NavigationController { private let context: AccountContext diff --git a/submodules/TelegramUI/Sources/TextLinkHandling.swift b/submodules/TelegramUI/Sources/TextLinkHandling.swift index 8d4a2796a4..3ae0393bb1 100644 --- a/submodules/TelegramUI/Sources/TextLinkHandling.swift +++ b/submodules/TelegramUI/Sources/TextLinkHandling.swift @@ -51,16 +51,16 @@ func handleTextLinkActionImpl(context: AccountContext, peerId: PeerId?, navigate } let openLinkImpl: (String) -> Void = { [weak controller] url in - navigateDisposable.set((context.sharedContext.resolveUrl(account: context.account, url: url, skipUrlAuth: true) |> deliverOnMainQueue).start(next: { result in + navigateDisposable.set((context.sharedContext.resolveUrl(context: context, peerId: peerId, url: url, skipUrlAuth: true) |> deliverOnMainQueue).start(next: { result in if let controller = controller { switch result { case let .externalUrl(url): context.sharedContext.applicationBindings.openUrl(url) case let .peer(peerId, navigation): openResolvedPeerImpl(peerId, navigation) - case let .channelMessage(peerId, messageId): + case let .channelMessage(peerId, messageId, timecode): if let navigationController = controller.navigationController as? NavigationController { - context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peerId), subject: .message(id: messageId, highlight: true))) + context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peerId), subject: .message(id: messageId, highlight: true, timecode: timecode))) } case let .replyThreadMessage(replyThreadMessage, messageId): if let navigationController = controller.navigationController as? NavigationController { @@ -91,7 +91,7 @@ func handleTextLinkActionImpl(context: AccountContext, peerId: PeerId?, navigate } let openPeerMentionImpl: (String) -> Void = { mention in - navigateDisposable.set((resolvePeerByName(account: context.account, name: mention, ageLimit: 10) |> take(1) |> deliverOnMainQueue).start(next: { peerId in + navigateDisposable.set((context.engine.peers.resolvePeerByName(name: mention, ageLimit: 10) |> take(1) |> deliverOnMainQueue).start(next: { peerId in openResolvedPeerImpl(peerId, .default) })) } diff --git a/submodules/TelegramUI/Sources/ThemeUpdateManager.swift b/submodules/TelegramUI/Sources/ThemeUpdateManager.swift index 3ac35c77b0..58dc72a6d5 100644 --- a/submodules/TelegramUI/Sources/ThemeUpdateManager.swift +++ b/submodules/TelegramUI/Sources/ThemeUpdateManager.swift @@ -104,7 +104,7 @@ final class ThemeUpdateManagerImpl: ThemeUpdateManager { |> mapToSignal { wallpaper -> Signal<(PresentationThemeReference, PresentationTheme?), NoError> in if let wallpaper = wallpaper, case let .file(_, _, _, _, _, _, slug, file, _) = wallpaper { var convertedRepresentations: [ImageRepresentationWithReference] = [] - convertedRepresentations.append(ImageRepresentationWithReference(representation: TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 100, height: 100), resource: file.resource, progressiveSizes: []), reference: .wallpaper(wallpaper: .slug(slug), resource: file.resource))) + convertedRepresentations.append(ImageRepresentationWithReference(representation: TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 100, height: 100), resource: file.resource, progressiveSizes: [], immediateThumbnailData: nil), reference: .wallpaper(wallpaper: .slug(slug), resource: file.resource))) return wallpaperDatas(account: account, accountManager: accountManager, fileReference: .standalone(media: 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 { @@ -139,7 +139,7 @@ final class ThemeUpdateManagerImpl: ThemeUpdateManager { theme = updatedTheme } - 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) + 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, reduceMotion: current.reduceMotion) }) }).start() } diff --git a/submodules/TelegramUI/Sources/TransformOutgoingMessageMedia.swift b/submodules/TelegramUI/Sources/TransformOutgoingMessageMedia.swift index 525586e6dc..39ae2767dd 100644 --- a/submodules/TelegramUI/Sources/TransformOutgoingMessageMedia.swift +++ b/submodules/TelegramUI/Sources/TransformOutgoingMessageMedia.swift @@ -59,7 +59,7 @@ public func transformOutgoingMessageMedia(postbox: Postbox, network: Network, me }*/ let imageDimensions = CGSize(width: image.size.width * image.scale, height: image.size.height * image.scale) - let thumbnailResource = LocalFileMediaResource(fileId: arc4random64()) + let thumbnailResource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) postbox.mediaBox.storeResourceData(thumbnailResource.id, data: thumbnailData) let scaledImageSize = CGSize(width: scaledImage.size.width * scaledImage.scale, height: scaledImage.size.height * scaledImage.scale) @@ -75,7 +75,7 @@ public func transformOutgoingMessageMedia(postbox: Postbox, network: Network, me } } attributes.append(.ImageSize(size: PixelDimensions(imageDimensions))) - let updatedFile = file.withUpdatedSize(data.size).withUpdatedPreviewRepresentations([TelegramMediaImageRepresentation(dimensions: PixelDimensions(scaledImageSize), resource: thumbnailResource, progressiveSizes: [])]).withUpdatedAttributes(attributes) + let updatedFile = file.withUpdatedSize(data.size).withUpdatedPreviewRepresentations([TelegramMediaImageRepresentation(dimensions: PixelDimensions(scaledImageSize), resource: thumbnailResource, progressiveSizes: [], immediateThumbnailData: nil)]).withUpdatedAttributes(attributes) subscriber.putNext(.standalone(media: updatedFile)) subscriber.putCompletion() } else { @@ -99,12 +99,12 @@ public func transformOutgoingMessageMedia(postbox: Postbox, network: Network, me } else if file.mimeType.hasPrefix("video/") { return Signal { subscriber in if let scaledImage = generateVideoFirstFrame(data.path, maxDimensions: CGSize(width: 320.0, height: 320.0)), let thumbnailData = scaledImage.jpegData(compressionQuality: 0.6) { - let thumbnailResource = LocalFileMediaResource(fileId: arc4random64()) + let thumbnailResource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) postbox.mediaBox.storeResourceData(thumbnailResource.id, data: thumbnailData) let scaledImageSize = CGSize(width: scaledImage.size.width * scaledImage.scale, height: scaledImage.size.height * scaledImage.scale) - let updatedFile = file.withUpdatedSize(data.size).withUpdatedPreviewRepresentations([TelegramMediaImageRepresentation(dimensions: PixelDimensions(scaledImageSize), resource: thumbnailResource, progressiveSizes: [])]) + let updatedFile = file.withUpdatedSize(data.size).withUpdatedPreviewRepresentations([TelegramMediaImageRepresentation(dimensions: PixelDimensions(scaledImageSize), resource: thumbnailResource, progressiveSizes: [], immediateThumbnailData: nil)]) subscriber.putNext(.standalone(media: updatedFile)) subscriber.putCompletion() } else { @@ -158,9 +158,9 @@ public func transformOutgoingMessageMedia(postbox: Postbox, network: Network, me if let fullImage = UIImage(contentsOfFile: data.path), let smallestImage = generateScaledImage(image: fullImage, size: smallestSize, scale: 1.0), let smallestData = compressImageToJPEG(smallestImage, quality: 0.7) { var representations = image.representations - let thumbnailResource = LocalFileMediaResource(fileId: arc4random64()) + let thumbnailResource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) postbox.mediaBox.storeResourceData(thumbnailResource.id, data: smallestData) - representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(smallestSize), resource: thumbnailResource, progressiveSizes: [])) + representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(smallestSize), resource: thumbnailResource, progressiveSizes: [], immediateThumbnailData: nil)) 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/Sources/UpgradedAccounts.swift b/submodules/TelegramUI/Sources/UpgradedAccounts.swift index 1198e7b2fb..765e2f7d7a 100644 --- a/submodules/TelegramUI/Sources/UpgradedAccounts.swift +++ b/submodules/TelegramUI/Sources/UpgradedAccounts.swift @@ -122,7 +122,6 @@ public func upgradedAccounts(accountManager: AccountManager, rootPath: String, e } |> ignoreValues |> mapToSignal { _ -> Signal in - return .complete() } } var signal: Signal = .complete() @@ -166,9 +165,6 @@ public func upgradedAccounts(accountManager: AccountManager, rootPath: String, e 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 wallpaper.isPattern { - if let color = file.settings.color, let intensity = file.settings.intensity { - 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 { let _ = accountManager.mediaBox.cachedResourceRepresentation(file.file.resource, representation: CachedBlurredWallpaperRepresentation(), complete: true, fetch: true).start() diff --git a/submodules/TelegramUI/Sources/VerticalListContextResultsChatInputContextPanelNode.swift b/submodules/TelegramUI/Sources/VerticalListContextResultsChatInputContextPanelNode.swift index 57e14a22fc..fd1c032325 100644 --- a/submodules/TelegramUI/Sources/VerticalListContextResultsChatInputContextPanelNode.swift +++ b/submodules/TelegramUI/Sources/VerticalListContextResultsChatInputContextPanelNode.swift @@ -257,7 +257,10 @@ final class VerticalListContextResultsChatInputContextPanelNode: ChatInputContex if let topItemOffset = topItemOffset { let position = strongSelf.listView.layer.position - strongSelf.listView.layer.animatePosition(from: CGPoint(x: position.x, y: position.y + (strongSelf.listView.bounds.size.height - topItemOffset)), to: position, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring) + strongSelf.listView.position = CGPoint(x: position.x, y: position.y + (strongSelf.listView.bounds.size.height - topItemOffset)) + ContainedViewLayoutTransition.animated(duration: 0.3, curve: .spring).animateView { + strongSelf.listView.position = position + } } strongSelf.listView.isHidden = false @@ -337,7 +340,7 @@ final class VerticalListContextResultsChatInputContextPanelNode: ChatInputContex let geoPoint = currentProcessedResults.geoPoint.flatMap { geoPoint -> (Double, Double) in return (geoPoint.latitude, geoPoint.longitude) } - self.loadMoreDisposable.set((requestChatContextResults(account: self.context.account, botId: currentProcessedResults.botId, peerId: currentProcessedResults.peerId, query: currentProcessedResults.query, location: .single(geoPoint), offset: nextOffset) + self.loadMoreDisposable.set((self.context.engine.messages.requestChatContextResults(botId: currentProcessedResults.botId, peerId: currentProcessedResults.peerId, query: currentProcessedResults.query, location: .single(geoPoint), offset: nextOffset) |> map { results -> ChatContextResultCollection? in return results?.results } @@ -363,4 +366,14 @@ final class VerticalListContextResultsChatInputContextPanelNode: ChatInputContex strongSelf.updateInternalResults(mergedResults) })) } + + override var topItemFrame: CGRect? { + var topItemFrame: CGRect? + self.listView.forEachItemNode { itemNode in + if topItemFrame == nil { + topItemFrame = itemNode.frame + } + } + return topItemFrame + } } diff --git a/submodules/TelegramUI/Sources/VerticalListContextResultsChatInputPanelItem.swift b/submodules/TelegramUI/Sources/VerticalListContextResultsChatInputPanelItem.swift index 3a3b06298e..a361ce4861 100644 --- a/submodules/TelegramUI/Sources/VerticalListContextResultsChatInputPanelItem.swift +++ b/submodules/TelegramUI/Sources/VerticalListContextResultsChatInputPanelItem.swift @@ -251,7 +251,7 @@ final class VerticalListContextResultsChatInputPanelItemNode: ListViewItemNode { if let stickerFile = stickerFile { updateIconImageSignal = chatMessageSticker(account: item.account, file: stickerFile, small: false, fetched: true) } else { - let tmpRepresentation = TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 55, height: 55), resource: imageResource, progressiveSizes: []) + let tmpRepresentation = TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 55, height: 55), resource: imageResource, progressiveSizes: [], immediateThumbnailData: 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)) } diff --git a/submodules/TelegramUI/Sources/WallpaperPreviewMedia.swift b/submodules/TelegramUI/Sources/WallpaperPreviewMedia.swift index 6d6433875d..7f32ba3261 100644 --- a/submodules/TelegramUI/Sources/WallpaperPreviewMedia.swift +++ b/submodules/TelegramUI/Sources/WallpaperPreviewMedia.swift @@ -5,9 +5,9 @@ import TelegramCore import SyncCore enum WallpaperPreviewMediaContent: Equatable { - case file(TelegramMediaFile, UIColor?, UIColor?, Int32?, Bool, Bool) + case file(file: TelegramMediaFile, colors: [UInt32], rotation: Int32?, intensity: Int32?, Bool, Bool) case color(UIColor) - case gradient(UIColor, UIColor, Int32?) + case gradient([UInt32], Int32?) case themeSettings(TelegramThemeSettings) } diff --git a/submodules/TelegramUI/Sources/WallpaperUploadManager.swift b/submodules/TelegramUI/Sources/WallpaperUploadManager.swift index 3d5211e7f6..00a84853a5 100644 --- a/submodules/TelegramUI/Sources/WallpaperUploadManager.swift +++ b/submodules/TelegramUI/Sources/WallpaperUploadManager.swift @@ -130,7 +130,7 @@ final class WallpaperUploadManagerImpl: WallpaperUploadManager { 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) + 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, reduceMotion: current.reduceMotion) })).start() } diff --git a/submodules/TelegramUI/Sources/Weak.swift b/submodules/TelegramUI/Sources/Weak.swift index 435c4bdb7e..fbf287572c 100644 --- a/submodules/TelegramUI/Sources/Weak.swift +++ b/submodules/TelegramUI/Sources/Weak.swift @@ -1,12 +1,2 @@ import Foundation -final class Weak { - private weak var _value: T? - var value: T? { - return self._value - } - - init(_ value: T) { - self._value = value - } -} diff --git a/submodules/TelegramUIPreferences/Sources/ExperimentalUISettings.swift b/submodules/TelegramUIPreferences/Sources/ExperimentalUISettings.swift index d685310c04..1ae9c83d4c 100644 --- a/submodules/TelegramUIPreferences/Sources/ExperimentalUISettings.swift +++ b/submodules/TelegramUIPreferences/Sources/ExperimentalUISettings.swift @@ -14,8 +14,9 @@ public struct ExperimentalUISettings: Equatable, PreferencesEntry { public var preferredVideoCodec: String? public var disableVideoAspectScaling: Bool public var enableVoipTcp: Bool - public var snapPinListToTop: Bool - public var demoAudioStream: Bool + public var demoVideoChats: Bool + public var experimentalCompatibility: Bool + public var enableNoiseSuppression: Bool public static var defaultSettings: ExperimentalUISettings { return ExperimentalUISettings( @@ -30,8 +31,9 @@ public struct ExperimentalUISettings: Equatable, PreferencesEntry { preferredVideoCodec: nil, disableVideoAspectScaling: false, enableVoipTcp: false, - snapPinListToTop: false, - demoAudioStream: false + demoVideoChats: false, + experimentalCompatibility: false, + enableNoiseSuppression: false ) } @@ -47,8 +49,9 @@ public struct ExperimentalUISettings: Equatable, PreferencesEntry { preferredVideoCodec: String?, disableVideoAspectScaling: Bool, enableVoipTcp: Bool, - snapPinListToTop: Bool, - demoAudioStream: Bool + demoVideoChats: Bool, + experimentalCompatibility: Bool, + enableNoiseSuppression: Bool ) { self.keepChatNavigationStack = keepChatNavigationStack self.skipReadHistory = skipReadHistory @@ -61,8 +64,9 @@ public struct ExperimentalUISettings: Equatable, PreferencesEntry { self.preferredVideoCodec = preferredVideoCodec self.disableVideoAspectScaling = disableVideoAspectScaling self.enableVoipTcp = enableVoipTcp - self.snapPinListToTop = snapPinListToTop - self.demoAudioStream = demoAudioStream + self.demoVideoChats = demoVideoChats + self.experimentalCompatibility = experimentalCompatibility + self.enableNoiseSuppression = enableNoiseSuppression } public init(decoder: PostboxDecoder) { @@ -77,8 +81,9 @@ public struct ExperimentalUISettings: Equatable, PreferencesEntry { self.preferredVideoCodec = decoder.decodeOptionalStringForKey("preferredVideoCodec") self.disableVideoAspectScaling = decoder.decodeInt32ForKey("disableVideoAspectScaling", orElse: 0) != 0 self.enableVoipTcp = decoder.decodeInt32ForKey("enableVoipTcp", orElse: 0) != 0 - self.snapPinListToTop = decoder.decodeInt32ForKey("snapPinListToTop", orElse: 0) != 0 - self.demoAudioStream = decoder.decodeInt32ForKey("demoAudioStream", orElse: 0) != 0 + self.demoVideoChats = decoder.decodeInt32ForKey("demoVideoChats", orElse: 0) != 0 + self.experimentalCompatibility = decoder.decodeInt32ForKey("experimentalCompatibility", orElse: 0) != 0 + self.enableNoiseSuppression = decoder.decodeInt32ForKey("enableNoiseSuppression", orElse: 0) != 0 } public func encode(_ encoder: PostboxEncoder) { @@ -95,8 +100,9 @@ public struct ExperimentalUISettings: Equatable, PreferencesEntry { } encoder.encodeInt32(self.disableVideoAspectScaling ? 1 : 0, forKey: "disableVideoAspectScaling") encoder.encodeInt32(self.enableVoipTcp ? 1 : 0, forKey: "enableVoipTcp") - encoder.encodeInt32(self.snapPinListToTop ? 1 : 0, forKey: "snapPinListToTop") - encoder.encodeInt32(self.demoAudioStream ? 1 : 0, forKey: "demoAudioStream") + encoder.encodeInt32(self.demoVideoChats ? 1 : 0, forKey: "demoVideoChats") + encoder.encodeInt32(self.experimentalCompatibility ? 1 : 0, forKey: "experimentalCompatibility") + encoder.encodeInt32(self.enableNoiseSuppression ? 1 : 0, forKey: "enableNoiseSuppression") } public func isEqual(to: PreferencesEntry) -> Bool { diff --git a/submodules/TelegramUIPreferences/Sources/PresentationThemeSettings.swift b/submodules/TelegramUIPreferences/Sources/PresentationThemeSettings.swift index 57f98c02d8..8e6e92315a 100644 --- a/submodules/TelegramUIPreferences/Sources/PresentationThemeSettings.swift +++ b/submodules/TelegramUIPreferences/Sources/PresentationThemeSettings.swift @@ -568,7 +568,7 @@ public struct PresentationThemeSettings: PreferencesEntry { public var chatBubbleSettings: PresentationChatBubbleSettings public var automaticThemeSwitchSetting: AutomaticThemeSwitchSetting public var largeEmoji: Bool - public var disableAnimations: Bool + public var reduceMotion: Bool private func wallpaperResources(_ wallpaper: TelegramWallpaper) -> [MediaResourceId] { switch wallpaper { @@ -606,10 +606,10 @@ public struct PresentationThemeSettings: PreferencesEntry { } public static var defaultSettings: PresentationThemeSettings { - 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) + return PresentationThemeSettings(theme: .builtin(.dayClassic), themeSpecificAccentColors: [:], themeSpecificChatWallpapers: [:], useSystemFont: true, fontSize: .regular, listsFontSize: .regular, chatBubbleSettings: .default, automaticThemeSwitchSetting: AutomaticThemeSwitchSetting(trigger: .system, theme: .builtin(.night)), largeEmoji: true, reduceMotion: false) } - public init(theme: PresentationThemeReference, themeSpecificAccentColors: [Int64: PresentationThemeAccentColor], themeSpecificChatWallpapers: [Int64: TelegramWallpaper], useSystemFont: Bool, fontSize: PresentationFontSize, listsFontSize: PresentationFontSize, chatBubbleSettings: PresentationChatBubbleSettings, automaticThemeSwitchSetting: AutomaticThemeSwitchSetting, largeEmoji: Bool, disableAnimations: Bool) { + public init(theme: PresentationThemeReference, themeSpecificAccentColors: [Int64: PresentationThemeAccentColor], themeSpecificChatWallpapers: [Int64: TelegramWallpaper], useSystemFont: Bool, fontSize: PresentationFontSize, listsFontSize: PresentationFontSize, chatBubbleSettings: PresentationChatBubbleSettings, automaticThemeSwitchSetting: AutomaticThemeSwitchSetting, largeEmoji: Bool, reduceMotion: Bool) { self.theme = theme self.themeSpecificAccentColors = themeSpecificAccentColors self.themeSpecificChatWallpapers = themeSpecificChatWallpapers @@ -619,7 +619,7 @@ public struct PresentationThemeSettings: PreferencesEntry { self.chatBubbleSettings = chatBubbleSettings self.automaticThemeSwitchSetting = automaticThemeSwitchSetting self.largeEmoji = largeEmoji - self.disableAnimations = disableAnimations + self.reduceMotion = reduceMotion } public init(decoder: PostboxDecoder) { @@ -644,7 +644,7 @@ public struct PresentationThemeSettings: PreferencesEntry { 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) + self.reduceMotion = decoder.decodeBoolForKey("reduceMotion", orElse: false) } public func encode(_ encoder: PostboxEncoder) { @@ -661,7 +661,7 @@ public struct PresentationThemeSettings: PreferencesEntry { encoder.encodeObject(self.chatBubbleSettings, forKey: "chatBubbleSettings") encoder.encodeObject(self.automaticThemeSwitchSetting, forKey: "automaticThemeSwitchSetting") encoder.encodeBool(self.largeEmoji, forKey: "largeEmoji") - encoder.encodeBool(self.disableAnimations, forKey: "disableAnimations") + encoder.encodeBool(self.reduceMotion, forKey: "reduceMotion") } public func isEqual(to: PreferencesEntry) -> Bool { @@ -673,43 +673,43 @@ public struct PresentationThemeSettings: PreferencesEntry { } public static func ==(lhs: PresentationThemeSettings, rhs: PresentationThemeSettings) -> Bool { - 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 + 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.reduceMotion == rhs.reduceMotion } 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) + 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, reduceMotion: self.reduceMotion) } 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) + 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, reduceMotion: self.reduceMotion) } 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) + 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, reduceMotion: self.reduceMotion) } 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) + 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, reduceMotion: self.reduceMotion) } 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) + 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, reduceMotion: self.reduceMotion) } 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) + 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, reduceMotion: self.reduceMotion) } 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) + 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, reduceMotion: self.reduceMotion) } 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) + 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, reduceMotion: self.reduceMotion) } - 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) + public func withUpdatedReduceMotion(_ reduceMotion: 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, reduceMotion: reduceMotion) } } diff --git a/submodules/TelegramUniversalVideoContent/Sources/NativeVideoContent.swift b/submodules/TelegramUniversalVideoContent/Sources/NativeVideoContent.swift index 672c0e7060..3a2f8162b4 100644 --- a/submodules/TelegramUniversalVideoContent/Sources/NativeVideoContent.swift +++ b/submodules/TelegramUniversalVideoContent/Sources/NativeVideoContent.swift @@ -10,6 +10,7 @@ import TelegramAudio import UniversalMediaPlayer import AccountContext import PhotoResources +import UIKitRuntimeUtils public enum NativeVideoContentId: Hashable { case message(UInt32, MediaId) diff --git a/submodules/TelegramVoip/Sources/GroupCallContext.swift b/submodules/TelegramVoip/Sources/GroupCallContext.swift index 728a7e8b4e..cea1f85d45 100644 --- a/submodules/TelegramVoip/Sources/GroupCallContext.swift +++ b/submodules/TelegramVoip/Sources/GroupCallContext.swift @@ -28,20 +28,20 @@ private final class ContextQueueImpl: NSObject, OngoingCallThreadLocalContextQue } } -private protocol BroadcastPartSource: class { +private protocol BroadcastPartSource: AnyObject { func requestPart(timestampMilliseconds: Int64, durationMilliseconds: Int64, completion: @escaping (OngoingGroupCallBroadcastPart) -> Void, rejoinNeeded: @escaping () -> Void) -> Disposable } private final class NetworkBroadcastPartSource: BroadcastPartSource { private let queue: Queue - private let account: Account + private let engine: TelegramEngine private let callId: Int64 private let accessHash: Int64 private var dataSource: AudioBroadcastDataSource? - init(queue: Queue, account: Account, callId: Int64, accessHash: Int64) { + init(queue: Queue, engine: TelegramEngine, callId: Int64, accessHash: Int64) { self.queue = queue - self.account = account + self.engine = engine self.callId = callId self.accessHash = accessHash } @@ -58,12 +58,12 @@ private final class NetworkBroadcastPartSource: BroadcastPartSource { if let dataSourceValue = self.dataSource { dataSource = .single(dataSourceValue) } else { - dataSource = getAudioBroadcastDataSource(account: self.account, callId: self.callId, accessHash: self.accessHash) + dataSource = self.engine.calls.getAudioBroadcastDataSource(callId: self.callId, accessHash: self.accessHash) } - - let account = self.account + let callId = self.callId let accessHash = self.accessHash + let engine = self.engine let queue = self.queue let signal = dataSource @@ -71,7 +71,7 @@ private final class NetworkBroadcastPartSource: BroadcastPartSource { |> mapToSignal { [weak self] dataSource -> Signal in if let dataSource = dataSource { self?.dataSource = dataSource - return getAudioBroadcastPart(dataSource: dataSource, callId: callId, accessHash: accessHash, timestampIdMilliseconds: timestampIdMilliseconds, durationMilliseconds: durationMilliseconds) + return engine.calls.getAudioBroadcastPart(dataSource: dataSource, callId: callId, accessHash: accessHash, timestampIdMilliseconds: timestampIdMilliseconds, durationMilliseconds: durationMilliseconds) |> map(Optional.init) } else { return .single(nil) @@ -117,12 +117,12 @@ private final class OngoingGroupCallBroadcastPartTaskImpl : NSObject, OngoingGro public final class OngoingGroupCallContext { public struct AudioStreamData { - public var account: Account + public var engine: TelegramEngine public var callId: Int64 public var accessHash: Int64 - public init(account: Account, callId: Int64, accessHash: Int64) { - self.account = account + public init(engine: TelegramEngine, callId: Int64, accessHash: Int64) { + self.engine = engine self.callId = callId self.accessHash = accessHash } @@ -134,6 +134,12 @@ public final class OngoingGroupCallContext { case broadcast } + public enum VideoContentType { + case none + case generic + case screencast + } + public struct NetworkState: Equatable { public var isConnected: Bool public var isTransitioningFromBroadcastToRtc: Bool @@ -143,6 +149,164 @@ public final class OngoingGroupCallContext { case local case source(UInt32) } + + public struct MediaChannelDescription { + public enum Kind { + case audio + case video + } + + public var kind: Kind + public var audioSsrc: UInt32 + public var videoDescription: String? + + public init(kind: Kind, audioSsrc: UInt32, videoDescription: String?) { + self.kind = kind + self.audioSsrc = audioSsrc + self.videoDescription = videoDescription + } + } + + public struct VideoChannel: Equatable { + public enum Quality { + case thumbnail + case medium + case full + } + + public struct SsrcGroup: Equatable { + public var semantics: String + public var ssrcs: [UInt32] + + public init(semantics: String, ssrcs: [UInt32]) { + self.semantics = semantics + self.ssrcs = ssrcs + } + } + + public var audioSsrc: UInt32 + public var endpointId: String + public var ssrcGroups: [SsrcGroup] + public var minQuality: Quality + public var maxQuality: Quality + + public init(audioSsrc: UInt32, endpointId: String, ssrcGroups: [SsrcGroup], minQuality: Quality, maxQuality: Quality) { + self.audioSsrc = audioSsrc + self.endpointId = endpointId + self.ssrcGroups = ssrcGroups + self.minQuality = minQuality + self.maxQuality = maxQuality + } + } + + public final class VideoFrameData { + public final class NativeBuffer { + public let pixelBuffer: CVPixelBuffer + + init(pixelBuffer: CVPixelBuffer) { + self.pixelBuffer = pixelBuffer + } + } + + public final class NV12Buffer { + private let wrapped: CallVideoFrameNV12Buffer + + public var width: Int { + return Int(self.wrapped.width) + } + + public var height: Int { + return Int(self.wrapped.height) + } + + public var y: Data { + return self.wrapped.y + } + + public var strideY: Int { + return Int(self.wrapped.strideY) + } + + public var uv: Data { + return self.wrapped.uv + } + + public var strideUV: Int { + return Int(self.wrapped.strideUV) + } + + init(wrapped: CallVideoFrameNV12Buffer) { + self.wrapped = wrapped + } + } + + public final class I420Buffer { + private let wrapped: CallVideoFrameI420Buffer + + public var width: Int { + return Int(self.wrapped.width) + } + + public var height: Int { + return Int(self.wrapped.height) + } + + public var y: Data { + return self.wrapped.y + } + + public var strideY: Int { + return Int(self.wrapped.strideY) + } + + public var u: Data { + return self.wrapped.u + } + + public var strideU: Int { + return Int(self.wrapped.strideU) + } + + public var v: Data { + return self.wrapped.v + } + + public var strideV: Int { + return Int(self.wrapped.strideV) + } + + init(wrapped: CallVideoFrameI420Buffer) { + self.wrapped = wrapped + } + } + + public enum Buffer { + case native(NativeBuffer) + case nv12(NV12Buffer) + case i420(I420Buffer) + } + + public let buffer: Buffer + public let width: Int + public let height: Int + public let orientation: OngoingCallVideoOrientation + + init(frameData: CallVideoFrameData) { + if let nativeBuffer = frameData.buffer as? CallVideoFrameNativePixelBuffer { + self.buffer = .native(NativeBuffer(pixelBuffer: nativeBuffer.pixelBuffer)) + } else if let nv12Buffer = frameData.buffer as? CallVideoFrameNV12Buffer { + self.buffer = .nv12(NV12Buffer(wrapped: nv12Buffer)) + } else if let i420Buffer = frameData.buffer as? CallVideoFrameI420Buffer { + self.buffer = .i420(I420Buffer(wrapped: i420Buffer)) + } else { + preconditionFailure() + } + + self.width = Int(frameData.width) + self.height = Int(frameData.height) + self.orientation = OngoingCallVideoOrientation(frameData.orientation) + } + } private final class Impl { let queue: Queue @@ -153,26 +317,36 @@ public final class OngoingGroupCallContext { let joinPayload = Promise<(String, UInt32)>() let networkState = ValuePromise(NetworkState(isConnected: false, isTransitioningFromBroadcastToRtc: false), ignoreRepeated: true) let isMuted = ValuePromise(true, ignoreRepeated: true) + let isNoiseSuppressionEnabled = ValuePromise(true, ignoreRepeated: true) let audioLevels = ValuePipe<[(AudioLevelKey, Float, Bool)]>() - - let videoSources = ValuePromise>(Set(), ignoreRepeated: true) + + private var currentRequestedVideoChannels: [VideoChannel] = [] private var broadcastPartsSource: BroadcastPartSource? - init(queue: Queue, inputDeviceId: String, outputDeviceId: String, video: OngoingCallVideoCapturer?, participantDescriptionsRequired: @escaping (Set) -> Void, audioStreamData: AudioStreamData?, rejoinNeeded: @escaping () -> Void) { + init(queue: Queue, inputDeviceId: String, outputDeviceId: String, video: OngoingCallVideoCapturer?, requestMediaChannelDescriptions: @escaping (Set, @escaping ([MediaChannelDescription]) -> Void) -> Disposable, audioStreamData: AudioStreamData?, rejoinNeeded: @escaping () -> Void, outgoingAudioBitrateKbit: Int32?, videoContentType: VideoContentType, enableNoiseSuppression: Bool) { self.queue = queue var networkStateUpdatedImpl: ((GroupCallNetworkState) -> Void)? var audioLevelsUpdatedImpl: (([NSNumber]) -> Void)? if let audioStreamData = audioStreamData { - let broadcastPartsSource = NetworkBroadcastPartSource(queue: queue, account: audioStreamData.account, callId: audioStreamData.callId, accessHash: audioStreamData.accessHash) + let broadcastPartsSource = NetworkBroadcastPartSource(queue: queue, engine: audioStreamData.engine, callId: audioStreamData.callId, accessHash: audioStreamData.accessHash) self.broadcastPartsSource = broadcastPartsSource } let broadcastPartsSource = self.broadcastPartsSource - let videoSources = self.videoSources + let _videoContentType: OngoingGroupCallVideoContentType + switch videoContentType { + case .generic: + _videoContentType = .generic + case .screencast: + _videoContentType = .screencast + case .none: + _videoContentType = .none + } + self.context = GroupCallThreadLocalContext( queue: ContextQueueImpl(queue: queue), networkStateUpdated: { state in @@ -184,11 +358,37 @@ public final class OngoingGroupCallContext { inputDeviceId: inputDeviceId, outputDeviceId: outputDeviceId, videoCapturer: video?.impl, - incomingVideoSourcesUpdated: { ssrcs in - videoSources.set(Set(ssrcs.map { $0.uint32Value })) - }, - participantDescriptionsRequired: { ssrcs in - participantDescriptionsRequired(Set(ssrcs.map { $0.uint32Value })) + requestMediaChannelDescriptions: { ssrcs, completion in + final class OngoingGroupCallMediaChannelDescriptionTaskImpl : NSObject, OngoingGroupCallMediaChannelDescriptionTask { + private let disposable: Disposable + + init(disposable: Disposable) { + self.disposable = disposable + } + + func cancel() { + self.disposable.dispose() + } + } + + let disposable = requestMediaChannelDescriptions(Set(ssrcs.map { $0.uint32Value }), { channels in + completion(channels.map { channel -> OngoingGroupCallMediaChannelDescription in + let mappedType: OngoingGroupCallMediaChannelType + switch channel.kind { + case .audio: + mappedType = .audio + case .video: + mappedType = .video + } + return OngoingGroupCallMediaChannelDescription( + type: mappedType, + audioSsrc: channel.audioSsrc, + videoDescription: channel.videoDescription + ) + }) + }) + + return OngoingGroupCallMediaChannelDescriptionTaskImpl(disposable: disposable) }, requestBroadcastPart: { timestampMilliseconds, durationMilliseconds, completion in let disposable = MetaDisposable() @@ -200,7 +400,10 @@ public final class OngoingGroupCallContext { } return OngoingGroupCallBroadcastPartTaskImpl(disposable: disposable) - } + }, + outgoingAudioBitrateKbit: outgoingAudioBitrateKbit ?? 32, + videoContentType: _videoContentType, + enableNoiseSuppression: enableNoiseSuppression ) let queue = self.queue @@ -247,10 +450,8 @@ public final class OngoingGroupCallContext { deinit { } - func setJoinResponse(payload: String, participants: [(UInt32, String?)]) { - self.context.setJoinResponsePayload(payload, participants: participants.map { participant -> OngoingGroupCallParticipantDescription in - return OngoingGroupCallParticipantDescription(audioSsrc: participant.0, jsonParams: participant.1) - }) + func setJoinResponse(payload: String) { + self.context.setJoinResponsePayload(payload) } func addSsrcs(ssrcs: [UInt32]) { @@ -264,22 +465,51 @@ public final class OngoingGroupCallContext { return ssrc as NSNumber }) } + + func removeIncomingVideoSource(_ ssrc: UInt32) { + self.context.removeIncomingVideoSource(ssrc) + } func setVolume(ssrc: UInt32, volume: Double) { self.context.setVolumeForSsrc(ssrc, volume: volume) } - - func setFullSizeVideoSsrc(ssrc: UInt32?) { - self.context.setFullSizeVideoSsrc(ssrc ?? 0) - } - - func addParticipants(participants: [(UInt32, String?)]) { - if participants.isEmpty { - return + + func setRequestedVideoChannels(_ channels: [VideoChannel]) { + if self.currentRequestedVideoChannels != channels { + self.currentRequestedVideoChannels = channels + + self.context.setRequestedVideoChannels(channels.map { channel -> OngoingGroupCallRequestedVideoChannel in + let mappedMinQuality: OngoingGroupCallRequestedVideoQuality + switch channel.minQuality { + case .thumbnail: + mappedMinQuality = .thumbnail + case .medium: + mappedMinQuality = .medium + case .full: + mappedMinQuality = .full + } + let mappedMaxQuality: OngoingGroupCallRequestedVideoQuality + switch channel.maxQuality { + case .thumbnail: + mappedMaxQuality = .thumbnail + case .medium: + mappedMaxQuality = .medium + case .full: + mappedMaxQuality = .full + } + return OngoingGroupCallRequestedVideoChannel( + audioSsrc: channel.audioSsrc, + endpointId: channel.endpointId, + ssrcGroups: channel.ssrcGroups.map { group in + return OngoingGroupCallSsrcGroup( + semantics: group.semantics, + ssrcs: group.ssrcs.map { $0 as NSNumber }) + }, + minQuality: mappedMinQuality, + maxQuality: mappedMaxQuality + ) + }) } - self.context.addParticipants(participants.map { participant -> OngoingGroupCallParticipantDescription in - return OngoingGroupCallParticipantDescription(audioSsrc: participant.0, jsonParams: participant.1) - }) } func stop() { @@ -317,6 +547,11 @@ public final class OngoingGroupCallContext { self.isMuted.set(isMuted) self.context.setIsMuted(isMuted) } + + func setIsNoiseSuppressionEnabled(_ isNoiseSuppressionEnabled: Bool) { + self.isNoiseSuppressionEnabled.set(isNoiseSuppressionEnabled) + self.context.setIsNoiseSuppressionEnabled(isNoiseSuppressionEnabled) + } func requestVideo(_ capturer: OngoingCallVideoCapturer?) { let queue = self.queue @@ -350,79 +585,149 @@ public final class OngoingGroupCallContext { self.context.switchAudioOutput(deviceId) } - func makeIncomingVideoView(source: UInt32, completion: @escaping (OngoingCallContextPresentationCallVideoView?) -> Void) { - self.context.makeIncomingVideoView(withSsrc: source, completion: { view in - if let view = view { + func makeIncomingVideoView(endpointId: String, requestClone: Bool, completion: @escaping (OngoingCallContextPresentationCallVideoView?, OngoingCallContextPresentationCallVideoView?) -> Void) { + self.context.makeIncomingVideoView(withEndpointId: endpointId, requestClone: requestClone, completion: { mainView, cloneView in + if let mainView = mainView { #if os(iOS) - completion(OngoingCallContextPresentationCallVideoView( - view: view, - setOnFirstFrameReceived: { [weak view] f in - view?.setOnFirstFrameReceived(f) + let mainVideoView = OngoingCallContextPresentationCallVideoView( + view: mainView, + setOnFirstFrameReceived: { [weak mainView] f in + mainView?.setOnFirstFrameReceived(f) }, - getOrientation: { [weak view] in - if let view = view { - return OngoingCallVideoOrientation(view.orientation) + getOrientation: { [weak mainView] in + if let mainView = mainView { + return OngoingCallVideoOrientation(mainView.orientation) } else { return .rotation0 } }, - getAspect: { [weak view] in - if let view = view { - return view.aspect + getAspect: { [weak mainView] in + if let mainView = mainView { + return mainView.aspect } else { return 0.0 } }, - setOnOrientationUpdated: { [weak view] f in - view?.setOnOrientationUpdated { value, aspect in + setOnOrientationUpdated: { [weak mainView] f in + mainView?.setOnOrientationUpdated { value, aspect in f?(OngoingCallVideoOrientation(value), aspect) } }, - setOnIsMirroredUpdated: { [weak view] f in - view?.setOnIsMirroredUpdated { value in + setOnIsMirroredUpdated: { [weak mainView] f in + mainView?.setOnIsMirroredUpdated { value in f?(value) } + }, + updateIsEnabled: { [weak mainView] value in + mainView?.updateIsEnabled(value) } - )) + ) + var cloneVideoView: OngoingCallContextPresentationCallVideoView? + if let cloneView = cloneView { + cloneVideoView = OngoingCallContextPresentationCallVideoView( + view: cloneView, + setOnFirstFrameReceived: { [weak cloneView] f in + cloneView?.setOnFirstFrameReceived(f) + }, + getOrientation: { [weak cloneView] in + if let cloneView = cloneView { + return OngoingCallVideoOrientation(cloneView.orientation) + } else { + return .rotation0 + } + }, + getAspect: { [weak cloneView] in + if let cloneView = cloneView { + return cloneView.aspect + } else { + return 0.0 + } + }, + setOnOrientationUpdated: { [weak cloneView] f in + cloneView?.setOnOrientationUpdated { value, aspect in + f?(OngoingCallVideoOrientation(value), aspect) + } + }, + setOnIsMirroredUpdated: { [weak cloneView] f in + cloneView?.setOnIsMirroredUpdated { value in + f?(value) + } + }, + updateIsEnabled: { [weak cloneView] value in + cloneView?.updateIsEnabled(value) + } + ) + } + completion(mainVideoView, cloneVideoView) #else - completion(OngoingCallContextPresentationCallVideoView( - view: view, - setOnFirstFrameReceived: { [weak view] f in - view?.setOnFirstFrameReceived(f) + let mainVideoView = OngoingCallContextPresentationCallVideoView( + view: mainView, + setOnFirstFrameReceived: { [weak mainView] f in + mainView?.setOnFirstFrameReceived(f) }, - getOrientation: { [weak view] in - if let view = view { - return OngoingCallVideoOrientation(view.orientation) + getOrientation: { [weak mainView] in + if let mainView = mainView { + return OngoingCallVideoOrientation(mainView.orientation) } else { return .rotation0 } }, - getAspect: { [weak view] in - if let view = view { - return view.aspect + getAspect: { [weak mainView] in + if let mainView = mainView { + return mainView.aspect } else { return 0.0 } }, - setOnOrientationUpdated: { [weak view] f in - view?.setOnOrientationUpdated { value, aspect in + setOnOrientationUpdated: { [weak mainView] f in + mainView?.setOnOrientationUpdated { value, aspect in f?(OngoingCallVideoOrientation(value), aspect) } - }, setVideoContentMode: { [weak view] mode in - view?.setVideoContentMode(mode) + }, setVideoContentMode: { [weak mainView] mode in + mainView?.setVideoContentMode(mode) }, - setOnIsMirroredUpdated: { [weak view] f in - view?.setOnIsMirroredUpdated { value in + setOnIsMirroredUpdated: { [weak mainView] f in + mainView?.setOnIsMirroredUpdated { value in f?(value) } + }, setIsPaused: { [weak mainView] paused in + mainView?.setIsPaused(paused) + }, renderToSize: { [weak mainView] size, animated in + mainView?.render(to: size, animated: animated) } - )) + ) + completion(mainVideoView, nil) #endif } else { - completion(nil) + completion(nil, nil) } }) } + + func video(endpointId: String) -> Signal { + let queue = self.queue + return Signal { [weak self] subscriber in + let disposable = MetaDisposable() + + queue.async { + guard let strongSelf = self else { + return + } + let innerDisposable = strongSelf.context.addVideoOutput(withEndpointId: endpointId) { videoFrameData in + subscriber.putNext(OngoingGroupCallContext.VideoFrameData(frameData: videoFrameData)) + } + disposable.set(ActionDisposable { + innerDisposable.dispose() + }) + } + + return disposable + } + } + + func addExternalAudioData(data: Data) { + self.context.addExternalAudioData(data) + } } private let queue = Queue() @@ -475,12 +780,12 @@ public final class OngoingGroupCallContext { return disposable } } - - public var videoSources: Signal, NoError> { + + public var isNoiseSuppressionEnabled: Signal { return Signal { subscriber in let disposable = MetaDisposable() self.impl.with { impl in - disposable.set(impl.videoSources.get().start(next: { value in + disposable.set(impl.isNoiseSuppressionEnabled.get().start(next: { value in subscriber.putNext(value) })) } @@ -488,10 +793,10 @@ public final class OngoingGroupCallContext { } } - public init(inputDeviceId: String = "", outputDeviceId: String = "", video: OngoingCallVideoCapturer?, participantDescriptionsRequired: @escaping (Set) -> Void, audioStreamData: AudioStreamData?, rejoinNeeded: @escaping () -> Void) { + public init(inputDeviceId: String = "", outputDeviceId: String = "", video: OngoingCallVideoCapturer?, requestMediaChannelDescriptions: @escaping (Set, @escaping ([MediaChannelDescription]) -> Void) -> Disposable, audioStreamData: AudioStreamData?, rejoinNeeded: @escaping () -> Void, outgoingAudioBitrateKbit: Int32?, videoContentType: VideoContentType, enableNoiseSuppression: Bool) { let queue = self.queue self.impl = QueueLocalObject(queue: queue, generate: { - return Impl(queue: queue, inputDeviceId: inputDeviceId, outputDeviceId: outputDeviceId, video: video, participantDescriptionsRequired: participantDescriptionsRequired, audioStreamData: audioStreamData, rejoinNeeded: rejoinNeeded) + return Impl(queue: queue, inputDeviceId: inputDeviceId, outputDeviceId: outputDeviceId, video: video, requestMediaChannelDescriptions: requestMediaChannelDescriptions, audioStreamData: audioStreamData, rejoinNeeded: rejoinNeeded, outgoingAudioBitrateKbit: outgoingAudioBitrateKbit, videoContentType: videoContentType, enableNoiseSuppression: enableNoiseSuppression) }) } @@ -506,6 +811,12 @@ public final class OngoingGroupCallContext { impl.setIsMuted(isMuted) } } + + public func setIsNoiseSuppressionEnabled(_ isNoiseSuppressionEnabled: Bool) { + self.impl.with { impl in + impl.setIsNoiseSuppressionEnabled(isNoiseSuppressionEnabled) + } + } public func requestVideo(_ capturer: OngoingCallVideoCapturer?) { self.impl.with { impl in @@ -529,9 +840,9 @@ public final class OngoingGroupCallContext { impl.switchAudioOutput(deviceId) } } - public func setJoinResponse(payload: String, participants: [(UInt32, String?)]) { + public func setJoinResponse(payload: String) { self.impl.with { impl in - impl.setJoinResponse(payload: payload, participants: participants) + impl.setJoinResponse(payload: payload) } } @@ -546,22 +857,22 @@ public final class OngoingGroupCallContext { impl.removeSsrcs(ssrcs: ssrcs) } } + + public func removeIncomingVideoSource(_ ssrc: UInt32) { + self.impl.with { impl in + impl.removeIncomingVideoSource(ssrc) + } + } public func setVolume(ssrc: UInt32, volume: Double) { self.impl.with { impl in impl.setVolume(ssrc: ssrc, volume: volume) } } - - public func setFullSizeVideoSsrc(ssrc: UInt32?) { + + public func setRequestedVideoChannels(_ channels: [VideoChannel]) { self.impl.with { impl in - impl.setFullSizeVideoSsrc(ssrc: ssrc) - } - } - - public func addParticipants(participants: [(UInt32, String?)]) { - self.impl.with { impl in - impl.addParticipants(participants: participants) + impl.setRequestedVideoChannels(channels) } } @@ -571,9 +882,27 @@ public final class OngoingGroupCallContext { } } - public func makeIncomingVideoView(source: UInt32, completion: @escaping (OngoingCallContextPresentationCallVideoView?) -> Void) { + public func makeIncomingVideoView(endpointId: String, requestClone: Bool, completion: @escaping (OngoingCallContextPresentationCallVideoView?, OngoingCallContextPresentationCallVideoView?) -> Void) { self.impl.with { impl in - impl.makeIncomingVideoView(source: source, completion: completion) + impl.makeIncomingVideoView(endpointId: endpointId, requestClone: requestClone, completion: completion) + } + } + + public func video(endpointId: String) -> Signal { + return Signal { subscriber in + let disposable = MetaDisposable() + self.impl.with { impl in + disposable.set(impl.video(endpointId: endpointId).start(next: { value in + subscriber.putNext(value) + })) + } + return disposable + } + } + + public func addExternalAudioData(data: Data) { + self.impl.with { impl in + impl.addExternalAudioData(data: data) } } } diff --git a/submodules/TelegramVoip/Sources/IpcGroupCallContext.swift b/submodules/TelegramVoip/Sources/IpcGroupCallContext.swift new file mode 100644 index 0000000000..b8b123abbc --- /dev/null +++ b/submodules/TelegramVoip/Sources/IpcGroupCallContext.swift @@ -0,0 +1,792 @@ +import Foundation +import SwiftSignalKit +import CoreMedia +import ImageIO + +private struct PayloadDescription: Codable { + var id: UInt32 + var timestamp: Int32 +} + +private struct JoinPayload: Codable { + var id: UInt32 + var string: String +} + +private struct JoinResponsePayload: Codable { + var id: UInt32 + var string: String +} + +private struct KeepaliveInfo: Codable { + var id: UInt32 + var timestamp: Int32 +} + +private struct CutoffPayload: Codable { + var id: UInt32 + var timestamp: Int32 +} + +private let checkInterval: Double = 0.2 +private let keepaliveTimeout: Double = 2.0 + +private func payloadDescriptionPath(basePath: String) -> String { + return basePath + "/currentPayloadDescription.json" +} + +private func joinPayloadPath(basePath: String) -> String { + return basePath + "/joinPayload.json" +} + +private func joinResponsePayloadPath(basePath: String) -> String { + return basePath + "/joinResponsePayload.json" +} + +private func keepaliveInfoPath(basePath: String) -> String { + return basePath + "/keepaliveInfo.json" +} + +private func cutoffPayloadPath(basePath: String) -> String { + return basePath + "/cutoffPayload.json" +} + +private func broadcastAppSocketPath(basePath: String) -> String { + return basePath + "/0" +} + +private final class FdReadConnection { + private final class PendingData { + var data: Data + var offset: Int = 0 + + init(count: Int) { + self.data = Data(bytesNoCopy: malloc(count)!, count: count, deallocator: .free) + } + } + + private let queue: Queue + let fd: Int32 + private let didRead: ((Data) -> Void)? + private let channel: DispatchSourceRead + + private var currendData: PendingData? + + init(queue: Queue, fd: Int32, didRead: ((Data) -> Void)?) { + assert(queue.isCurrent()) + self.queue = queue + self.fd = fd + self.didRead = didRead + + self.channel = DispatchSource.makeReadSource(fileDescriptor: fd, queue: queue.queue) + self.channel.setEventHandler(handler: { [weak self] in + guard let strongSelf = self else { + return + } + + while true { + if let currendData = strongSelf.currendData { + let offset = currendData.offset + let count = currendData.data.count - offset + let bytesRead = currendData.data.withUnsafeMutableBytes { bytes -> Int in + return Darwin.read(fd, bytes.baseAddress!.advanced(by: offset), min(8129, count)) + } + if bytesRead <= 0 { + break + } else { + currendData.offset += bytesRead + if currendData.offset == currendData.data.count { + strongSelf.currendData = nil + strongSelf.didRead?(currendData.data) + } + } + } else { + var length: Int32 = 0 + let bytesRead = read(fd, &length, 4) + if bytesRead < 0 { + break + } else { + assert(bytesRead == 4) + assert(length > 0 && length <= 30 * 1024 * 1024) + strongSelf.currendData = PendingData(count: Int(length)) + } + } + } + }) + self.channel.resume() + } + + deinit { + assert(self.queue.isCurrent()) + self.channel.cancel() + } +} + +private final class FdWriteConnection { + private final class PendingData { + let data: Data + var didWriteHeader: Bool = false + var offset: Int = 0 + + init(data: Data) { + self.data = data + } + } + + private let queue: Queue + let fd: Int32 + private let channel: DispatchSourceWrite + private var isResumed = false + + private let bufferSize: Int + private let buffer: UnsafeMutableRawPointer + + private var currentData: PendingData? + private var nextDataList: [Data] = [] + + init(queue: Queue, fd: Int32) { + assert(queue.isCurrent()) + self.queue = queue + self.fd = fd + + self.bufferSize = 8192 + self.buffer = malloc(self.bufferSize) + + self.channel = DispatchSource.makeWriteSource(fileDescriptor: fd, queue: queue.queue) + self.channel.setEventHandler(handler: { [weak self] in + guard let strongSelf = self else { + return + } + + while true { + if let currentData = strongSelf.currentData { + if !currentData.didWriteHeader { + var length: Int32 = Int32(currentData.data.count) + let writtenBytes = Darwin.write(fd, &length, 4) + if writtenBytes > 0 { + assert(writtenBytes == 4) + currentData.didWriteHeader = true + } else { + strongSelf.channel.suspend() + strongSelf.isResumed = false + break + } + } else { + let offset = currentData.offset + let count = currentData.data.count - offset + let writtenBytes = currentData.data.withUnsafeBytes { bytes -> Int in + return Darwin.write(fd, bytes.baseAddress!.advanced(by: offset), min(count, strongSelf.bufferSize)) + } + if writtenBytes > 0 { + currentData.offset += writtenBytes + if currentData.offset == currentData.data.count { + strongSelf.currentData = nil + + if !strongSelf.nextDataList.isEmpty { + let nextData = strongSelf.nextDataList.removeFirst() + strongSelf.currentData = PendingData(data: nextData) + } else { + strongSelf.channel.suspend() + strongSelf.isResumed = false + break + } + } + } else { + strongSelf.channel.suspend() + strongSelf.isResumed = false + break + } + } + } else { + strongSelf.channel.suspend() + strongSelf.isResumed = false + break + } + } + }) + } + + deinit { + assert(self.queue.isCurrent()) + + if !self.isResumed { + self.channel.resume() + } + self.channel.cancel() + + free(self.buffer) + } + + func addData(data: Data) { + if self.currentData == nil { + self.currentData = PendingData(data: data) + } else { + var totalBytes = 0 + for data in self.nextDataList { + totalBytes += data.count + } + if totalBytes < 1 * 1024 * 1024 { + self.nextDataList.append(data) + } + } + + if !self.isResumed { + self.isResumed = true + self.channel.resume() + } + } +} + +private final class NamedPipeReaderImpl { + private let queue: Queue + private var connection: FdReadConnection? + + init(queue: Queue, path: String, didRead: @escaping (Data) -> Void) { + self.queue = queue + + unlink(path) + mkfifo(path, 0o666) + let fd = open(path, O_RDONLY | O_NONBLOCK, S_IRUSR | S_IWUSR) + if fd != -1 { + self.connection = FdReadConnection(queue: self.queue, fd: fd, didRead: { data in + didRead(data) + }) + } + } +} + +private final class NamedPipeReader { + private let queue = Queue() + let impl: QueueLocalObject + + init(path: String, didRead: @escaping (Data) -> Void) { + let queue = self.queue + self.impl = QueueLocalObject(queue: queue, generate: { + return NamedPipeReaderImpl(queue: queue, path: path, didRead: didRead) + }) + } +} + +private final class NamedPipeWriterImpl { + private let queue: Queue + private var connection: FdWriteConnection? + + init(queue: Queue, path: String) { + self.queue = queue + + let fd = open(path, O_WRONLY | O_NONBLOCK, S_IRUSR | S_IWUSR) + if fd != -1 { + self.connection = FdWriteConnection(queue: self.queue, fd: fd) + } + } + + func addData(data: Data) { + guard let connection = self.connection else { + return + } + connection.addData(data: data) + } +} + +private final class NamedPipeWriter { + private let queue = Queue() + private let impl: QueueLocalObject + + init(path: String) { + let queue = self.queue + self.impl = QueueLocalObject(queue: queue, generate: { + return NamedPipeWriterImpl(queue: queue, path: path) + }) + } + + func addData(data: Data) { + self.impl.with { impl in + impl.addData(data: data) + } + } +} + +private final class MappedFile { + let path: String + private var handle: Int32 + private var currentSize: Int + private(set) var memory: UnsafeMutableRawPointer + + init?(path: String, createIfNotExists: Bool) { + self.path = path + + var flags: Int32 = O_RDWR | O_APPEND + if createIfNotExists { + flags |= O_CREAT + } + self.handle = open(path, flags, S_IRUSR | S_IWUSR) + + if self.handle < 0 { + return nil + } + + var value = stat() + stat(path, &value) + self.currentSize = Int(value.st_size) + + self.memory = mmap(nil, self.currentSize, PROT_READ | PROT_WRITE, MAP_SHARED, self.handle, 0) + } + + deinit { + munmap(self.memory, self.currentSize) + close(self.handle) + } + + var size: Int { + get { + return self.currentSize + } set(value) { + if value != self.currentSize { + munmap(self.memory, self.currentSize) + ftruncate(self.handle, off_t(value)) + self.currentSize = value + self.memory = mmap(nil, self.currentSize, PROT_READ | PROT_WRITE, MAP_SHARED, self.handle, 0) + } + } + } + + func synchronize() { + msync(self.memory, self.currentSize, MS_ASYNC) + } + + func write(at range: Range, from data: UnsafeRawPointer) { + memcpy(self.memory.advanced(by: range.lowerBound), data, range.count) + } + + func read(at range: Range, to data: UnsafeMutableRawPointer) { + memcpy(data, self.memory.advanced(by: range.lowerBound), range.count) + } + + func clear() { + memset(self.memory, 0, self.currentSize) + } +} + +public final class IpcGroupCallBufferAppContext { + private let basePath: String + private var audioServer: NamedPipeReader? + + private let id: UInt32 + + private let isActivePromise = ValuePromise(false, ignoreRepeated: true) + public var isActive: Signal { + return self.isActivePromise.get() + } + private var isActiveCheckTimer: SwiftSignalKit.Timer? + + private let framesPipe = ValuePipe<(CVPixelBuffer, CGImagePropertyOrientation)>() + public var frames: Signal<(CVPixelBuffer, CGImagePropertyOrientation), NoError> { + return self.framesPipe.signal() + } + + private let audioDataPipe = ValuePipe() + public var audioData: Signal { + return self.audioDataPipe.signal() + } + + private var framePollTimer: SwiftSignalKit.Timer? + private var mappedFile: MappedFile? + + private var callActiveInfoTimer: SwiftSignalKit.Timer? + + public init(basePath: String) { + self.basePath = basePath + let _ = try? FileManager.default.createDirectory(atPath: basePath, withIntermediateDirectories: true, attributes: nil) + + self.id = UInt32.random(in: 0 ..< UInt32.max) + + let dataPath = broadcastAppSocketPath(basePath: basePath) + "-data-\(self.id)" + let audioDataPath = broadcastAppSocketPath(basePath: basePath) + "-audio-\(self.id)" + + if let mappedFile = MappedFile(path: dataPath, createIfNotExists: true) { + self.mappedFile = mappedFile + if mappedFile.size < 10 * 1024 * 1024 { + mappedFile.size = 10 * 1024 * 1024 + } + } + + let audioDataPipe = self.audioDataPipe + self.audioServer = NamedPipeReader(path: audioDataPath, didRead: { data in + audioDataPipe.putNext(data) + }) + + let framePollTimer = SwiftSignalKit.Timer(timeout: 1.0 / 30.0, repeat: true, completion: { [weak self] in + guard let strongSelf = self, let mappedFile = strongSelf.mappedFile else { + return + } + + var orientationValue: Int32 = 0 + mappedFile.read(at: 0 ..< 4, to: &orientationValue) + let orientation = CGImagePropertyOrientation(rawValue: UInt32(bitPattern: orientationValue)) ?? .up + let data = Data(bytesNoCopy: mappedFile.memory.advanced(by: 4), count: mappedFile.size - 4, deallocator: .none) + if let frame = deserializePixelBuffer(data: data) { + strongSelf.framesPipe.putNext((frame, orientation)) + } + }, queue: .mainQueue()) + self.framePollTimer = framePollTimer + framePollTimer.start() + + self.updateCallIsActive() + + let callActiveInfoTimer = SwiftSignalKit.Timer(timeout: 1.0, repeat: true, completion: { [weak self] in + self?.updateCallIsActive() + }, queue: .mainQueue()) + self.callActiveInfoTimer = callActiveInfoTimer + callActiveInfoTimer.start() + + let isActiveCheckTimer = SwiftSignalKit.Timer(timeout: 1.0, repeat: true, completion: { [weak self] in + self?.updateKeepaliveInfo() + }, queue: .mainQueue()) + self.isActiveCheckTimer = isActiveCheckTimer + isActiveCheckTimer.start() + } + + deinit { + self.framePollTimer?.invalidate() + self.callActiveInfoTimer?.invalidate() + self.isActiveCheckTimer?.invalidate() + if let mappedFile = self.mappedFile { + self.mappedFile = nil + let _ = try? FileManager.default.removeItem(atPath: mappedFile.path) + } + } + + private func updateCallIsActive() { + let timestamp = Int32(Date().timeIntervalSince1970) + let payloadDescription = PayloadDescription( + id: self.id, + timestamp: timestamp + ) + guard let payloadDescriptionData = try? JSONEncoder().encode(payloadDescription) else { + return + } + guard let _ = try? payloadDescriptionData.write(to: URL(fileURLWithPath: payloadDescriptionPath(basePath: self.basePath)), options: .atomic) else { + return + } + } + + private func updateKeepaliveInfo() { + let filePath = keepaliveInfoPath(basePath: self.basePath) + guard let keepaliveInfoData = try? Data(contentsOf: URL(fileURLWithPath: filePath)) else { + return + } + guard let keepaliveInfo = try? JSONDecoder().decode(KeepaliveInfo.self, from: keepaliveInfoData) else { + return + } + if keepaliveInfo.id != self.id { + self.isActivePromise.set(false) + return + } + let timestamp = Int32(Date().timeIntervalSince1970) + if keepaliveInfo.timestamp < timestamp - Int32(keepaliveTimeout) { + self.isActivePromise.set(false) + return + } + + self.isActivePromise.set(true) + } + + public func stopScreencast() { + let timestamp = Int32(Date().timeIntervalSince1970) + let cutoffPayload = CutoffPayload( + id: self.id, + timestamp: timestamp + ) + guard let cutoffPayloadData = try? JSONEncoder().encode(cutoffPayload) else { + return + } + guard let _ = try? cutoffPayloadData.write(to: URL(fileURLWithPath: cutoffPayloadPath(basePath: self.basePath)), options: .atomic) else { + return + } + } +} + +public final class IpcGroupCallBufferBroadcastContext { + public enum Status { + public enum FinishReason { + case screencastEnded + case callEnded + case error + } + case finished(FinishReason) + } + + private let basePath: String + private let client: NamedPipeWriter + private var timer: SwiftSignalKit.Timer? + + private let statusPromise = Promise() + public var status: Signal { + return self.statusPromise.get() + } + + private var mappedFile: MappedFile? + private var currentId: UInt32? + private var audioClient: NamedPipeWriter? + + private var callActiveInfoTimer: SwiftSignalKit.Timer? + private var keepaliveInfoTimer: SwiftSignalKit.Timer? + private var screencastCutoffTimer: SwiftSignalKit.Timer? + + public init(basePath: String) { + self.basePath = basePath + let _ = try? FileManager.default.createDirectory(atPath: basePath, withIntermediateDirectories: true, attributes: nil) + + self.client = NamedPipeWriter(path: broadcastAppSocketPath(basePath: basePath)) + + let callActiveInfoTimer = SwiftSignalKit.Timer(timeout: 1.0, repeat: true, completion: { [weak self] in + self?.updateCallIsActive() + }, queue: .mainQueue()) + self.callActiveInfoTimer = callActiveInfoTimer + callActiveInfoTimer.start() + + let screencastCutoffTimer = SwiftSignalKit.Timer(timeout: 1.0, repeat: true, completion: { [weak self] in + self?.updateScreencastCutoff() + }, queue: .mainQueue()) + self.screencastCutoffTimer = screencastCutoffTimer + screencastCutoffTimer.start() + } + + deinit { + self.endActiveIndication() + + self.callActiveInfoTimer?.invalidate() + self.keepaliveInfoTimer?.invalidate() + self.screencastCutoffTimer?.invalidate() + } + + private func updateScreencastCutoff() { + let filePath = cutoffPayloadPath(basePath: self.basePath) + guard let cutoffPayloadData = try? Data(contentsOf: URL(fileURLWithPath: filePath)) else { + return + } + + guard let cutoffPayload = try? JSONDecoder().decode(CutoffPayload.self, from: cutoffPayloadData) else { + return + } + + let timestamp = Int32(Date().timeIntervalSince1970) + if let currentId = self.currentId, currentId == cutoffPayload.id && cutoffPayload.timestamp > timestamp - 10 { + self.statusPromise.set(.single(.finished(.screencastEnded))) + return + } + } + + private func updateCallIsActive() { + let filePath = payloadDescriptionPath(basePath: self.basePath) + guard let payloadDescriptionData = try? Data(contentsOf: URL(fileURLWithPath: filePath)) else { + self.statusPromise.set(.single(.finished(.error))) + return + } + + guard let payloadDescription = try? JSONDecoder().decode(PayloadDescription.self, from: payloadDescriptionData) else { + self.statusPromise.set(.single(.finished(.error))) + return + } + let timestamp = Int32(Date().timeIntervalSince1970) + if payloadDescription.timestamp < timestamp - 4 { + self.statusPromise.set(.single(.finished(.callEnded))) + return + } + + if let currentId = self.currentId { + if currentId != payloadDescription.id { + self.statusPromise.set(.single(.finished(.callEnded))) + } + } else { + self.currentId = payloadDescription.id + + let dataPath = broadcastAppSocketPath(basePath: basePath) + "-data-\(payloadDescription.id)" + let audioDataPath = broadcastAppSocketPath(basePath: basePath) + "-audio-\(payloadDescription.id)" + + if let mappedFile = MappedFile(path: dataPath, createIfNotExists: false) { + self.mappedFile = mappedFile + if mappedFile.size < 10 * 1024 * 1024 { + mappedFile.size = 10 * 1024 * 1024 + } + } + + self.audioClient = NamedPipeWriter(path: audioDataPath) + + self.writeKeepaliveInfo() + + let keepaliveInfoTimer = SwiftSignalKit.Timer(timeout: 1.0, repeat: true, completion: { [weak self] in + self?.writeKeepaliveInfo() + }, queue: .mainQueue()) + self.keepaliveInfoTimer = keepaliveInfoTimer + keepaliveInfoTimer.start() + } + } + + public func setCurrentFrame(data: Data, orientation: CGImagePropertyOrientation) { + if let mappedFile = self.mappedFile, mappedFile.size >= data.count { + let _ = data.withUnsafeBytes { bytes in + var orientationValue = Int32(bitPattern: orientation.rawValue) + memmove(mappedFile.memory, &orientationValue, 4) + memcpy(mappedFile.memory.advanced(by: 4), bytes.baseAddress!, data.count) + } + } + } + + public func writeAudioData(data: Data) { + self.audioClient?.addData(data: data) + } + + private func writeKeepaliveInfo() { + guard let currentId = self.currentId else { + preconditionFailure() + } + let keepaliveInfo = KeepaliveInfo( + id: currentId, + timestamp: Int32(Date().timeIntervalSince1970) + ) + guard let keepaliveInfoData = try? JSONEncoder().encode(keepaliveInfo) else { + preconditionFailure() + } + guard let _ = try? keepaliveInfoData.write(to: URL(fileURLWithPath: keepaliveInfoPath(basePath: self.basePath)), options: .atomic) else { + preconditionFailure() + } + } + + private func endActiveIndication() { + let _ = try? FileManager.default.removeItem(atPath: keepaliveInfoPath(basePath: self.basePath)) + } +} + +public func serializePixelBuffer(buffer: CVPixelBuffer) -> Data? { + let pixelFormat = CVPixelBufferGetPixelFormatType(buffer) + switch pixelFormat { + case kCVPixelFormatType_420YpCbCr8BiPlanarFullRange, kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange: + let status = CVPixelBufferLockBaseAddress(buffer, .readOnly) + if status != kCVReturnSuccess { + return nil + } + defer { + CVPixelBufferUnlockBaseAddress(buffer, .readOnly) + } + + let width = CVPixelBufferGetWidth(buffer) + let height = CVPixelBufferGetHeight(buffer) + + guard let yPlane = CVPixelBufferGetBaseAddressOfPlane(buffer, 0) else { + return nil + } + let yStride = CVPixelBufferGetBytesPerRowOfPlane(buffer, 0) + let yPlaneSize = yStride * height + + guard let uvPlane = CVPixelBufferGetBaseAddressOfPlane(buffer, 1) else { + return nil + } + let uvStride = CVPixelBufferGetBytesPerRowOfPlane(buffer, 1) + let uvPlaneSize = uvStride * (height / 2) + + let headerSize: Int = 4 + 4 + 4 + 4 + 4 + + let dataSize = headerSize + yPlaneSize + uvPlaneSize + let resultBytes = malloc(dataSize)! + + var pixelFormatValue = pixelFormat + memcpy(resultBytes.advanced(by: 0), &pixelFormatValue, 4) + var widthValue = Int32(width) + memcpy(resultBytes.advanced(by: 4), &widthValue, 4) + var heightValue = Int32(height) + memcpy(resultBytes.advanced(by: 4 + 4), &heightValue, 4) + var yStrideValue = Int32(yStride) + memcpy(resultBytes.advanced(by: 4 + 4 + 4), &yStrideValue, 4) + var uvStrideValue = Int32(uvStride) + memcpy(resultBytes.advanced(by: 4 + 4 + 4 + 4), &uvStrideValue, 4) + + memcpy(resultBytes.advanced(by: headerSize), yPlane, yPlaneSize) + memcpy(resultBytes.advanced(by: headerSize + yPlaneSize), uvPlane, uvPlaneSize) + + return Data(bytesNoCopy: resultBytes, count: dataSize, deallocator: .free) + default: + return nil + } +} + +public func deserializePixelBuffer(data: Data) -> CVPixelBuffer? { + if data.count < 4 + 4 + 4 + 4 { + return nil + } + let count = data.count + return data.withUnsafeBytes { bytes -> CVPixelBuffer? in + let dataBytes = bytes.baseAddress! + + var pixelFormat: UInt32 = 0 + memcpy(&pixelFormat, dataBytes.advanced(by: 0), 4) + + switch pixelFormat { + case kCVPixelFormatType_420YpCbCr8BiPlanarFullRange, kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange: + break + default: + return nil + } + + var width: Int32 = 0 + memcpy(&width, dataBytes.advanced(by: 4), 4) + var height: Int32 = 0 + memcpy(&height, dataBytes.advanced(by: 4 + 4), 4) + var yStride: Int32 = 0 + memcpy(&yStride, dataBytes.advanced(by: 4 + 4 + 4), 4) + var uvStride: Int32 = 0 + memcpy(&uvStride, dataBytes.advanced(by: 4 + 4 + 4 + 4), 4) + + if width < 0 || width > 8192 { + return nil + } + if height < 0 || height > 8192 { + return nil + } + + let headerSize: Int = 4 + 4 + 4 + 4 + 4 + + let yPlaneSize = Int(yStride * height) + let uvPlaneSize = Int(uvStride * height / 2) + let dataSize = headerSize + yPlaneSize + uvPlaneSize + + if dataSize > count { + return nil + } + + var buffer: CVPixelBuffer? = nil + CVPixelBufferCreate(nil, Int(width), Int(height), pixelFormat, nil, &buffer) + if let buffer = buffer { + let status = CVPixelBufferLockBaseAddress(buffer, []) + if status != kCVReturnSuccess { + return nil + } + defer { + CVPixelBufferUnlockBaseAddress(buffer, []) + } + + guard let destYPlane = CVPixelBufferGetBaseAddressOfPlane(buffer, 0) else { + return nil + } + let destYStride = CVPixelBufferGetBytesPerRowOfPlane(buffer, 0) + if destYStride != Int(yStride) { + return nil + } + + guard let destUvPlane = CVPixelBufferGetBaseAddressOfPlane(buffer, 1) else { + return nil + } + let destUvStride = CVPixelBufferGetBytesPerRowOfPlane(buffer, 1) + if destUvStride != Int(uvStride) { + return nil + } + + memcpy(destYPlane, dataBytes.advanced(by: headerSize), yPlaneSize) + memcpy(destUvPlane, dataBytes.advanced(by: headerSize + yPlaneSize), uvPlaneSize) + + return buffer + } else { + return nil + } + } +} diff --git a/submodules/TelegramVoip/Sources/OngoingCallContext.swift b/submodules/TelegramVoip/Sources/OngoingCallContext.swift index 332f932042..f46761cea9 100644 --- a/submodules/TelegramVoip/Sources/OngoingCallContext.swift +++ b/submodules/TelegramVoip/Sources/OngoingCallContext.swift @@ -334,55 +334,144 @@ extension OngoingCallThreadLocalContext: OngoingCallThreadLocalContextProtocol { public final class OngoingCallVideoCapturer { internal let impl: OngoingCallThreadLocalContextVideoCapturer + + private let isActivePromise = ValuePromise(true, ignoreRepeated: true) + public var isActive: Signal { + return self.isActivePromise.get() + } - public init(keepLandscape: Bool = false) { - self.impl = OngoingCallThreadLocalContextVideoCapturer(deviceId: "", keepLandscape: keepLandscape) + public init(keepLandscape: Bool = false, isCustom: Bool = false) { + if isCustom { + self.impl = OngoingCallThreadLocalContextVideoCapturer.withExternalSampleBufferProvider() + } else { + self.impl = OngoingCallThreadLocalContextVideoCapturer(deviceId: "", keepLandscape: keepLandscape) + } + let isActivePromise = self.isActivePromise + self.impl.setOnIsActiveUpdated({ value in + isActivePromise.set(value) + }) } public func switchVideoInput(isFront: Bool) { self.impl.switchVideoInput(isFront ? "" : "back") } - public func makeOutgoingVideoView(completion: @escaping (OngoingCallContextPresentationCallVideoView?) -> Void) { - self.impl.makeOutgoingVideoView { view in - if let view = view { - completion(OngoingCallContextPresentationCallVideoView( - view: view, - setOnFirstFrameReceived: { [weak view] f in - view?.setOnFirstFrameReceived(f) + public func makeOutgoingVideoView(requestClone: Bool, completion: @escaping (OngoingCallContextPresentationCallVideoView?, OngoingCallContextPresentationCallVideoView?) -> Void) { + self.impl.makeOutgoingVideoView(requestClone, completion: { mainView, cloneView in + if let mainView = mainView { + let mainVideoView = OngoingCallContextPresentationCallVideoView( + view: mainView, + setOnFirstFrameReceived: { [weak mainView] f in + mainView?.setOnFirstFrameReceived(f) }, - getOrientation: { [weak view] in - if let view = view { - return OngoingCallVideoOrientation(view.orientation) + getOrientation: { [weak mainView] in + if let mainView = mainView { + return OngoingCallVideoOrientation(mainView.orientation) } else { return .rotation0 } }, - getAspect: { [weak view] in - if let view = view { - return view.aspect + getAspect: { [weak mainView] in + if let mainView = mainView { + return mainView.aspect } else { return 0.0 } }, - setOnOrientationUpdated: { [weak view] f in - view?.setOnOrientationUpdated { value, aspect in + setOnOrientationUpdated: { [weak mainView] f in + mainView?.setOnOrientationUpdated { value, aspect in f?(OngoingCallVideoOrientation(value), aspect) } }, - setOnIsMirroredUpdated: { [weak view] f in - view?.setOnIsMirroredUpdated(f) + setOnIsMirroredUpdated: { [weak mainView] f in + mainView?.setOnIsMirroredUpdated(f) + }, + updateIsEnabled: { [weak mainView] value in + mainView?.updateIsEnabled(value) } - )) + ) + var cloneVideoView: OngoingCallContextPresentationCallVideoView? + if let cloneView = cloneView { + cloneVideoView = OngoingCallContextPresentationCallVideoView( + view: cloneView, + setOnFirstFrameReceived: { [weak cloneView] f in + cloneView?.setOnFirstFrameReceived(f) + }, + getOrientation: { [weak cloneView] in + if let cloneView = cloneView { + return OngoingCallVideoOrientation(cloneView.orientation) + } else { + return .rotation0 + } + }, + getAspect: { [weak cloneView] in + if let cloneView = cloneView { + return cloneView.aspect + } else { + return 0.0 + } + }, + setOnOrientationUpdated: { [weak cloneView] f in + cloneView?.setOnOrientationUpdated { value, aspect in + f?(OngoingCallVideoOrientation(value), aspect) + } + }, + setOnIsMirroredUpdated: { [weak cloneView] f in + cloneView?.setOnIsMirroredUpdated(f) + }, + updateIsEnabled: { [weak cloneView] value in + cloneView?.updateIsEnabled(value) + } + ) + } + completion(mainVideoView, cloneVideoView) } else { - completion(nil) + completion(nil, nil) } - } + }) } public func setIsVideoEnabled(_ value: Bool) { self.impl.setIsVideoEnabled(value) } + + public func injectPixelBuffer(_ pixelBuffer: CVPixelBuffer, rotation: CGImagePropertyOrientation) { + var videoRotation: OngoingCallVideoOrientation = .rotation0 + switch rotation { + case .up: + videoRotation = .rotation0 + case .left: + videoRotation = .rotation90 + case .right: + videoRotation = .rotation270 + case .down: + videoRotation = .rotation180 + default: + videoRotation = .rotation0 + } + self.impl.submitPixelBuffer(pixelBuffer, rotation: videoRotation.orientation) + } + + public func video() -> Signal { + let queue = Queue.mainQueue() + return Signal { [weak self] subscriber in + let disposable = MetaDisposable() + + queue.async { + guard let strongSelf = self else { + return + } + let innerDisposable = strongSelf.impl.addVideoOutput { videoFrameData in + subscriber.putNext(OngoingGroupCallContext.VideoFrameData(frameData: videoFrameData)) + } + disposable.set(ActionDisposable { + innerDisposable.dispose() + }) + } + + return disposable + } + } } extension OngoingCallThreadLocalContextWebrtc: OngoingCallThreadLocalContextProtocol { @@ -487,6 +576,21 @@ extension OngoingCallVideoOrientation { self = .rotation0 } } + + var orientation: OngoingCallVideoOrientationWebrtc { + switch self { + case .rotation0: + return .orientation0 + case .rotation90: + return .orientation90 + case .rotation180: + return .orientation180 + case .rotation270: + return .orientation270 + @unknown default: + return .orientation0 + } + } } public final class OngoingCallContextPresentationCallVideoView { @@ -496,6 +600,7 @@ public final class OngoingCallContextPresentationCallVideoView { public let getAspect: () -> CGFloat public let setOnOrientationUpdated: (((OngoingCallVideoOrientation, CGFloat) -> Void)?) -> Void public let setOnIsMirroredUpdated: (((Bool) -> Void)?) -> Void + public let updateIsEnabled: (Bool) -> Void public init( view: UIView, @@ -503,7 +608,8 @@ public final class OngoingCallContextPresentationCallVideoView { getOrientation: @escaping () -> OngoingCallVideoOrientation, getAspect: @escaping () -> CGFloat, setOnOrientationUpdated: @escaping (((OngoingCallVideoOrientation, CGFloat) -> Void)?) -> Void, - setOnIsMirroredUpdated: @escaping (((Bool) -> Void)?) -> Void + setOnIsMirroredUpdated: @escaping (((Bool) -> Void)?) -> Void, + updateIsEnabled: @escaping (Bool) -> Void ) { self.view = view self.setOnFirstFrameReceived = setOnFirstFrameReceived @@ -511,6 +617,7 @@ public final class OngoingCallContextPresentationCallVideoView { self.getAspect = getAspect self.setOnOrientationUpdated = setOnOrientationUpdated self.setOnIsMirroredUpdated = setOnIsMirroredUpdated + self.updateIsEnabled = updateIsEnabled } } @@ -729,19 +836,20 @@ public final class OngoingCallContext { } }) } + + strongSelf.signalingDataDisposable = callSessionManager.beginReceivingCallSignalingData(internalId: internalId, { [weak self] dataList in + queue.async { + self?.withContext { context in + if let context = context as? OngoingCallThreadLocalContextWebrtc { + for data in dataList { + context.addSignaling(data) + } + } + } + } + }) } })) - - self.signalingDataDisposable = (callSessionManager.callSignalingData(internalId: internalId)).start(next: { [weak self] data in - print("data received") - queue.async { - self?.withContext { context in - if let context = context as? OngoingCallThreadLocalContextWebrtc { - context.addSignaling(data) - } - } - } - }) } deinit { @@ -817,7 +925,7 @@ public final class OngoingCallContext { if let callId = callId, !statsLogPath.isEmpty, let data = try? Data(contentsOf: URL(fileURLWithPath: statsLogPath)), let dataString = String(data: data, encoding: .utf8) { debugLogValue.set(.single(dataString)) if sendDebugLogs { - let _ = saveCallDebugLog(network: self.account.network, callId: callId, log: dataString).start() + let _ = TelegramEngine(account: self.account).calls.saveCallDebugLog(callId: callId, log: dataString).start() } } } @@ -905,6 +1013,9 @@ public final class OngoingCallContext { view?.setOnIsMirroredUpdated { value in f?(value) } + }, + updateIsEnabled: { [weak view] value in + view?.updateIsEnabled(value) } )) } else { diff --git a/submodules/TemporaryCachedPeerDataManager/Sources/ChannelMemberCategoryListContext.swift b/submodules/TemporaryCachedPeerDataManager/Sources/ChannelMemberCategoryListContext.swift index 0b20956993..682d5ea4c6 100644 --- a/submodules/TemporaryCachedPeerDataManager/Sources/ChannelMemberCategoryListContext.swift +++ b/submodules/TemporaryCachedPeerDataManager/Sources/ChannelMemberCategoryListContext.swift @@ -111,6 +111,7 @@ private extension CachedChannelAdminRank { } private final class ChannelMemberSingleCategoryListContext: ChannelMemberCategoryListContext { + private let engine: TelegramEngine private let postbox: Postbox private let network: Network private let accountPeerId: PeerId @@ -147,7 +148,8 @@ private final class ChannelMemberSingleCategoryListContext: ChannelMemberCategor private var headUpdateTimer: SwiftSignalKit.Timer? - init(postbox: Postbox, network: Network, accountPeerId: PeerId, peerId: PeerId, category: ChannelMemberListCategory) { + init(engine: TelegramEngine, postbox: Postbox, network: Network, accountPeerId: PeerId, peerId: PeerId, category: ChannelMemberListCategory) { + self.engine = engine self.postbox = postbox self.network = network self.accountPeerId = accountPeerId @@ -230,7 +232,7 @@ private final class ChannelMemberSingleCategoryListContext: ChannelMemberCategor case let .banned(query): requestCategory = .banned(query.flatMap(ChannelMembersCategoryFilter.search) ?? .all) } - return channelMembers(postbox: self.postbox, network: self.network, accountPeerId: self.accountPeerId, peerId: self.peerId, category: requestCategory, offset: offset, limit: count, hash: hash) |> map { members in + return self.engine.peers.channelMembers(peerId: self.peerId, category: requestCategory, offset: offset, limit: count, hash: hash) |> map { members in switch requestCategory { case .admins: if let query = adminQuery { @@ -315,7 +317,7 @@ private final class ChannelMemberSingleCategoryListContext: ChannelMemberCategor for i in 0 ..< min(strongSelf.listStateValue.list.count, Int(initialBatchSize)) { let peerId = strongSelf.listStateValue.list[i].peer.id - hash = (hash &* 20261) &+ UInt32(peerId.id) + hash = (hash &* 20261) &+ UInt32(bitPattern: peerId.id._internalGetInt32Value()) } hash = hash % 0x7FFFFFFF strongSelf.headUpdateDisposable.set((strongSelf.loadSignal(offset: 0, count: initialBatchSize, hash: Int32(bitPattern: hash)) @@ -590,9 +592,9 @@ private final class ChannelMemberMultiCategoryListContext: ChannelMemberCategory } } - init(postbox: Postbox, network: Network, accountPeerId: PeerId, peerId: PeerId, categories: [ChannelMemberListCategory]) { + init(engine: TelegramEngine, postbox: Postbox, network: Network, accountPeerId: PeerId, peerId: PeerId, categories: [ChannelMemberListCategory]) { self.contexts = categories.map { category in - return ChannelMemberSingleCategoryListContext(postbox: postbox, network: network, accountPeerId: accountPeerId, peerId: peerId, category: category) + return ChannelMemberSingleCategoryListContext(engine: engine, postbox: postbox, network: network, accountPeerId: accountPeerId, peerId: peerId, category: category) } } @@ -698,6 +700,7 @@ private final class PeerChannelMemberContextWithSubscribers { } final class PeerChannelMemberCategoriesContext { + private let engine: TelegramEngine private let postbox: Postbox private let network: Network private let accountPeerId: PeerId @@ -706,7 +709,8 @@ final class PeerChannelMemberCategoriesContext { private var contexts: [PeerChannelMemberContextKey: PeerChannelMemberContextWithSubscribers] = [:] - init(postbox: Postbox, network: Network, accountPeerId: PeerId, peerId: PeerId, becameEmpty: @escaping (Bool) -> Void) { + init(engine: TelegramEngine, postbox: Postbox, network: Network, accountPeerId: PeerId, peerId: PeerId, becameEmpty: @escaping (Bool) -> Void) { + self.engine = engine self.postbox = postbox self.network = network self.accountPeerId = accountPeerId @@ -755,13 +759,13 @@ final class PeerChannelMemberCategoriesContext { default: mappedCategory = .recent } - context = ChannelMemberSingleCategoryListContext(postbox: self.postbox, network: self.network, accountPeerId: self.accountPeerId, peerId: self.peerId, category: mappedCategory) + context = ChannelMemberSingleCategoryListContext(engine: self.engine, postbox: self.postbox, network: self.network, accountPeerId: self.accountPeerId, peerId: self.peerId, category: mappedCategory) case let .restrictedAndBanned(query): - context = ChannelMemberMultiCategoryListContext(postbox: self.postbox, network: self.network, accountPeerId: self.accountPeerId, peerId: self.peerId, categories: [.restricted(query), .banned(query)]) + context = ChannelMemberMultiCategoryListContext(engine: self.engine, postbox: self.postbox, network: self.network, accountPeerId: self.accountPeerId, peerId: self.peerId, categories: [.restricted(query), .banned(query)]) case let .restricted(query): - context = ChannelMemberSingleCategoryListContext(postbox: self.postbox, network: self.network, accountPeerId: self.accountPeerId, peerId: self.peerId, category: .restricted(query)) + context = ChannelMemberSingleCategoryListContext(engine: self.engine, postbox: self.postbox, network: self.network, accountPeerId: self.accountPeerId, peerId: self.peerId, category: .restricted(query)) case let .banned(query): - context = ChannelMemberSingleCategoryListContext(postbox: self.postbox, network: self.network, accountPeerId: self.accountPeerId, peerId: self.peerId, category: .banned(query)) + context = ChannelMemberSingleCategoryListContext(engine: self.engine, postbox: self.postbox, network: self.network, accountPeerId: self.accountPeerId, peerId: self.peerId, category: .banned(query)) } let contextWithSubscribers = PeerChannelMemberContextWithSubscribers(context: context, emptyTimeout: emptyTimeout, becameEmpty: { [weak self] in assert(Queue.mainQueue().isCurrent()) diff --git a/submodules/TemporaryCachedPeerDataManager/Sources/PeerChannelMemberCategoriesContextsManager.swift b/submodules/TemporaryCachedPeerDataManager/Sources/PeerChannelMemberCategoriesContextsManager.swift index 2f5b294faf..b9372785ab 100644 --- a/submodules/TemporaryCachedPeerDataManager/Sources/PeerChannelMemberCategoriesContextsManager.swift +++ b/submodules/TemporaryCachedPeerDataManager/Sources/PeerChannelMemberCategoriesContextsManager.swift @@ -57,12 +57,12 @@ private final class PeerChannelMemberCategoriesContextsManagerImpl { 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) { + func getContext(engine: TelegramEngine, postbox: Postbox, network: Network, accountPeerId: PeerId, peerId: PeerId, key: PeerChannelMemberContextKey, requestUpdate: Bool, updated: @escaping (ChannelMemberListState) -> Void) -> (Disposable, PeerChannelMemberCategoryControl) { if let current = self.contexts[peerId] { return current.getContext(key: key, requestUpdate: requestUpdate, updated: updated) } else { var becameEmptyImpl: ((Bool) -> Void)? - let context = PeerChannelMemberCategoriesContext(postbox: postbox, network: network, accountPeerId: accountPeerId, peerId: peerId, becameEmpty: { value in + let context = PeerChannelMemberCategoriesContext(engine: engine, postbox: postbox, network: network, accountPeerId: accountPeerId, peerId: peerId, becameEmpty: { value in becameEmptyImpl?(value) }) becameEmptyImpl = { [weak self, weak context] value in @@ -78,7 +78,7 @@ private final class PeerChannelMemberCategoriesContextsManagerImpl { } } - func recentOnline(postbox: Postbox, network: Network, accountPeerId: PeerId, peerId: PeerId, updated: @escaping (Int32) -> Void) -> Disposable { + func recentOnline(account: Account, accountPeerId: PeerId, peerId: PeerId, updated: @escaping (Int32) -> Void) -> Disposable { let context: PeerChannelMembersOnlineContext if let current = self.onlineContexts[peerId] { context = current @@ -88,7 +88,7 @@ private final class PeerChannelMemberCategoriesContextsManagerImpl { self.onlineContexts[peerId] = context let signal = ( - chatOnlineMembers(postbox: postbox, network: network, peerId: peerId) + TelegramEngine(account: account).peers.chatOnlineMembers(peerId: peerId) |> then( .complete() |> delay(30.0, queue: .mainQueue()) @@ -286,10 +286,10 @@ public final class PeerChannelMemberCategoriesContextsManager { } } - private func getContext(postbox: Postbox, network: Network, accountPeerId: PeerId, peerId: PeerId, key: PeerChannelMemberContextKey, requestUpdate: Bool, updated: @escaping (ChannelMemberListState) -> Void) -> (Disposable, PeerChannelMemberCategoryControl?) { + private func getContext(engine: TelegramEngine, postbox: Postbox, network: Network, accountPeerId: PeerId, peerId: PeerId, key: PeerChannelMemberContextKey, requestUpdate: Bool, updated: @escaping (ChannelMemberListState) -> Void) -> (Disposable, PeerChannelMemberCategoryControl?) { assert(Queue.mainQueue().isCurrent()) if let (disposable, control) = self.impl.syncWith({ impl in - return impl.getContext(postbox: postbox, network: network, accountPeerId: accountPeerId, peerId: peerId, key: key, requestUpdate: requestUpdate, updated: updated) + return impl.getContext(engine: engine, postbox: postbox, network: network, accountPeerId: accountPeerId, peerId: peerId, key: key, requestUpdate: requestUpdate, updated: updated) }) { return (disposable, control) } else { @@ -317,47 +317,47 @@ public final class PeerChannelMemberCategoriesContextsManager { } } - public func recent(postbox: Postbox, network: Network, accountPeerId: PeerId, peerId: PeerId, searchQuery: String? = nil, requestUpdate: Bool = true, updated: @escaping (ChannelMemberListState) -> Void) -> (Disposable, PeerChannelMemberCategoryControl?) { + public func recent(engine: TelegramEngine, postbox: Postbox, network: Network, accountPeerId: PeerId, peerId: PeerId, searchQuery: String? = nil, requestUpdate: Bool = true, updated: @escaping (ChannelMemberListState) -> Void) -> (Disposable, PeerChannelMemberCategoryControl?) { let key: PeerChannelMemberContextKey if let searchQuery = searchQuery { key = .recentSearch(searchQuery) } else { key = .recent } - return self.getContext(postbox: postbox, network: network, accountPeerId: accountPeerId, peerId: peerId, key: key, requestUpdate: requestUpdate, updated: updated) + return self.getContext(engine: engine, postbox: postbox, network: network, accountPeerId: accountPeerId, peerId: peerId, key: key, requestUpdate: requestUpdate, updated: updated) } - public func mentions(postbox: Postbox, network: Network, accountPeerId: PeerId, peerId: PeerId, threadMessageId: MessageId?, searchQuery: String? = nil, requestUpdate: Bool = true, updated: @escaping (ChannelMemberListState) -> Void) -> (Disposable, PeerChannelMemberCategoryControl?) { + public func mentions(engine: TelegramEngine, postbox: Postbox, network: Network, accountPeerId: PeerId, peerId: PeerId, threadMessageId: MessageId?, searchQuery: String? = nil, requestUpdate: Bool = true, updated: @escaping (ChannelMemberListState) -> Void) -> (Disposable, PeerChannelMemberCategoryControl?) { let key: PeerChannelMemberContextKey = .mentions(threadId: threadMessageId, query: searchQuery) - return self.getContext(postbox: postbox, network: network, accountPeerId: accountPeerId, peerId: peerId, key: key, requestUpdate: requestUpdate, updated: updated) + return self.getContext(engine: engine, postbox: postbox, network: network, accountPeerId: accountPeerId, peerId: peerId, key: key, requestUpdate: requestUpdate, updated: updated) } - public func admins(postbox: Postbox, network: Network, accountPeerId: PeerId, peerId: PeerId, searchQuery: String? = nil, updated: @escaping (ChannelMemberListState) -> Void) -> (Disposable, PeerChannelMemberCategoryControl?) { - return self.getContext(postbox: postbox, network: network, accountPeerId: accountPeerId, peerId: peerId, key: .admins(searchQuery), requestUpdate: true, updated: updated) + public func admins(engine: TelegramEngine, postbox: Postbox, network: Network, accountPeerId: PeerId, peerId: PeerId, searchQuery: String? = nil, updated: @escaping (ChannelMemberListState) -> Void) -> (Disposable, PeerChannelMemberCategoryControl?) { + return self.getContext(engine: engine, postbox: postbox, network: network, accountPeerId: accountPeerId, peerId: peerId, key: .admins(searchQuery), requestUpdate: true, updated: updated) } - public func contacts(postbox: Postbox, network: Network, accountPeerId: PeerId, peerId: PeerId, searchQuery: String? = nil, updated: @escaping (ChannelMemberListState) -> Void) -> (Disposable, PeerChannelMemberCategoryControl?) { - return self.getContext(postbox: postbox, network: network, accountPeerId: accountPeerId, peerId: peerId, key: .contacts(searchQuery), requestUpdate: true, updated: updated) + public func contacts(engine: TelegramEngine, postbox: Postbox, network: Network, accountPeerId: PeerId, peerId: PeerId, searchQuery: String? = nil, updated: @escaping (ChannelMemberListState) -> Void) -> (Disposable, PeerChannelMemberCategoryControl?) { + return self.getContext(engine: engine, postbox: postbox, network: network, accountPeerId: accountPeerId, peerId: peerId, key: .contacts(searchQuery), requestUpdate: true, updated: updated) } - public func bots(postbox: Postbox, network: Network, accountPeerId: PeerId, peerId: PeerId, searchQuery: String? = nil, updated: @escaping (ChannelMemberListState) -> Void) -> (Disposable, PeerChannelMemberCategoryControl?) { - return self.getContext(postbox: postbox, network: network, accountPeerId: accountPeerId, peerId: peerId, key: .bots(searchQuery), requestUpdate: true, updated: updated) + public func bots(engine: TelegramEngine, postbox: Postbox, network: Network, accountPeerId: PeerId, peerId: PeerId, searchQuery: String? = nil, updated: @escaping (ChannelMemberListState) -> Void) -> (Disposable, PeerChannelMemberCategoryControl?) { + return self.getContext(engine: engine, postbox: postbox, network: network, accountPeerId: accountPeerId, peerId: peerId, key: .bots(searchQuery), requestUpdate: true, updated: updated) } - public func restricted(postbox: Postbox, network: Network, accountPeerId: PeerId, peerId: PeerId, searchQuery: String? = nil, updated: @escaping (ChannelMemberListState) -> Void) -> (Disposable, PeerChannelMemberCategoryControl?) { - return self.getContext(postbox: postbox, network: network, accountPeerId: accountPeerId, peerId: peerId, key: .restricted(searchQuery), requestUpdate: true, updated: updated) + public func restricted(engine: TelegramEngine, postbox: Postbox, network: Network, accountPeerId: PeerId, peerId: PeerId, searchQuery: String? = nil, updated: @escaping (ChannelMemberListState) -> Void) -> (Disposable, PeerChannelMemberCategoryControl?) { + return self.getContext(engine: engine, postbox: postbox, network: network, accountPeerId: accountPeerId, peerId: peerId, key: .restricted(searchQuery), requestUpdate: true, updated: updated) } - public func banned(postbox: Postbox, network: Network, accountPeerId: PeerId, peerId: PeerId, searchQuery: String? = nil, updated: @escaping (ChannelMemberListState) -> Void) -> (Disposable, PeerChannelMemberCategoryControl?) { - return self.getContext(postbox: postbox, network: network, accountPeerId: accountPeerId, peerId: peerId, key: .banned(searchQuery), requestUpdate: true, updated: updated) + public func banned(engine: TelegramEngine, postbox: Postbox, network: Network, accountPeerId: PeerId, peerId: PeerId, searchQuery: String? = nil, updated: @escaping (ChannelMemberListState) -> Void) -> (Disposable, PeerChannelMemberCategoryControl?) { + return self.getContext(engine: engine, postbox: postbox, network: network, accountPeerId: accountPeerId, peerId: peerId, key: .banned(searchQuery), requestUpdate: true, updated: updated) } - public func restrictedAndBanned(postbox: Postbox, network: Network, accountPeerId: PeerId, peerId: PeerId, searchQuery: String? = nil, updated: @escaping (ChannelMemberListState) -> Void) -> (Disposable, PeerChannelMemberCategoryControl?) { - return self.getContext(postbox: postbox, network: network, accountPeerId: accountPeerId, peerId: peerId, key: .restrictedAndBanned(searchQuery), requestUpdate: true, updated: updated) + public func restrictedAndBanned(engine: TelegramEngine, postbox: Postbox, network: Network, accountPeerId: PeerId, peerId: PeerId, searchQuery: String? = nil, updated: @escaping (ChannelMemberListState) -> Void) -> (Disposable, PeerChannelMemberCategoryControl?) { + return self.getContext(engine: engine, postbox: postbox, network: network, accountPeerId: accountPeerId, peerId: peerId, key: .restrictedAndBanned(searchQuery), requestUpdate: true, updated: updated) } - public func updateMemberBannedRights(account: Account, peerId: PeerId, memberId: PeerId, bannedRights: TelegramChatBannedRights?) -> Signal { - return updateChannelMemberBannedRights(account: account, peerId: peerId, memberId: memberId, rights: bannedRights) + public func updateMemberBannedRights(engine: TelegramEngine, peerId: PeerId, memberId: PeerId, bannedRights: TelegramChatBannedRights?) -> Signal { + return engine.peers.updateChannelMemberBannedRights(peerId: peerId, memberId: memberId, rights: bannedRights) |> deliverOnMainQueue |> beforeNext { [weak self] (previous, updated, isMember) in if let strongSelf = self { @@ -378,8 +378,8 @@ public final class PeerChannelMemberCategoriesContextsManager { } } - public func updateMemberAdminRights(account: Account, peerId: PeerId, memberId: PeerId, adminRights: TelegramChatAdminRights?, rank: String?) -> Signal { - return updateChannelAdminRights(account: account, peerId: peerId, adminId: memberId, rights: adminRights, rank: rank) + public func updateMemberAdminRights(engine: TelegramEngine, peerId: PeerId, memberId: PeerId, adminRights: TelegramChatAdminRights?, rank: String?) -> Signal { + return engine.peers.updateChannelAdminRights(peerId: peerId, adminId: memberId, rights: adminRights, rank: rank) |> map(Optional.init) |> deliverOnMainQueue |> beforeNext { [weak self] result in @@ -398,8 +398,8 @@ public final class PeerChannelMemberCategoriesContextsManager { } } - public func transferOwnership(account: Account, peerId: PeerId, memberId: PeerId, password: String) -> Signal { - return updateChannelOwnership(account: account, accountStateManager: account.stateManager, channelId: peerId, memberId: memberId, password: password) + public func transferOwnership(engine: TelegramEngine, peerId: PeerId, memberId: PeerId, password: String) -> Signal { + return engine.peers.updateChannelOwnership(channelId: peerId, memberId: memberId, password: password) |> map(Optional.init) |> deliverOnMainQueue |> beforeNext { [weak self] results in @@ -418,8 +418,8 @@ public final class PeerChannelMemberCategoriesContextsManager { } } - public func join(account: Account, peerId: PeerId, hash: String?) -> Signal { - return joinChannel(account: account, peerId: peerId, hash: hash) + public func join(engine: TelegramEngine, peerId: PeerId, hash: String?) -> Signal { + return engine.peers.joinChannel(peerId: peerId, hash: hash) |> deliverOnMainQueue |> beforeNext { [weak self] result in if let strongSelf = self, let updated = result { @@ -435,8 +435,8 @@ public final class PeerChannelMemberCategoriesContextsManager { |> ignoreValues } - public func addMember(account: Account, peerId: PeerId, memberId: PeerId) -> Signal { - return addChannelMember(account: account, peerId: peerId, memberId: memberId) + public func addMember(engine: TelegramEngine, peerId: PeerId, memberId: PeerId) -> Signal { + return engine.peers.addChannelMember(peerId: peerId, memberId: memberId) |> deliverOnMainQueue |> beforeNext { [weak self] result in if let strongSelf = self { @@ -453,9 +453,9 @@ public final class PeerChannelMemberCategoriesContextsManager { |> ignoreValues } - public func addMembers(account: Account, peerId: PeerId, memberIds: [PeerId]) -> Signal { + public func addMembers(engine: TelegramEngine, peerId: PeerId, memberIds: [PeerId]) -> Signal { let signals: [Signal<(ChannelParticipant?, RenderedChannelParticipant)?, AddChannelMemberError>] = memberIds.map({ memberId in - return addChannelMember(account: account, peerId: peerId, memberId: memberId) + return engine.peers.addChannelMember(peerId: peerId, memberId: memberId) |> map(Optional.init) |> `catch` { error -> Signal<(ChannelParticipant?, RenderedChannelParticipant)?, AddChannelMemberError> in return .fail(error) @@ -483,7 +483,7 @@ public final class PeerChannelMemberCategoriesContextsManager { } } - public func recentOnline(postbox: Postbox, network: Network, accountPeerId: PeerId, peerId: PeerId) -> Signal { + public func recentOnline(account: Account, accountPeerId: PeerId, peerId: PeerId) -> Signal { return Signal { [weak self] subscriber in guard let strongSelf = self else { subscriber.putNext(0) @@ -491,7 +491,7 @@ public final class PeerChannelMemberCategoriesContextsManager { return EmptyDisposable } let disposable = strongSelf.impl.syncWith({ impl -> Disposable in - return impl.recentOnline(postbox: postbox, network: network, accountPeerId: accountPeerId, peerId: peerId, updated: { value in + return impl.recentOnline(account: account, accountPeerId: accountPeerId, peerId: peerId, updated: { value in subscriber.putNext(value) }) }) @@ -500,11 +500,11 @@ public final class PeerChannelMemberCategoriesContextsManager { |> runOn(Queue.mainQueue()) } - public func recentOnlineSmall(postbox: Postbox, network: Network, accountPeerId: PeerId, peerId: PeerId) -> Signal { + public func recentOnlineSmall(engine: TelegramEngine, postbox: Postbox, network: Network, accountPeerId: PeerId, peerId: PeerId) -> Signal { return Signal { [weak self] subscriber in var previousIds: Set? let statusesDisposable = MetaDisposable() - let disposableAndControl = self?.recent(postbox: postbox, network: network, accountPeerId: accountPeerId, peerId: peerId, updated: { state in + let disposableAndControl = self?.recent(engine: engine, postbox: postbox, network: network, accountPeerId: accountPeerId, peerId: peerId, updated: { state in var idList: [PeerId] = [] for item in state.list { idList.append(item.peer.id) diff --git a/submodules/TgVoipWebrtc/BUILD b/submodules/TgVoipWebrtc/BUILD index 2a720c8217..e7da0a33c9 100644 --- a/submodules/TgVoipWebrtc/BUILD +++ b/submodules/TgVoipWebrtc/BUILD @@ -16,10 +16,19 @@ objc_library( "tgcalls/tgcalls/platform/tdesktop/**", "tgcalls/tgcalls/platform/android/**", "tgcalls/tgcalls/platform/windows/**", + "tgcalls/tgcalls/platform/uwp/**", + "tgcalls/tgcalls/platform/darwin/macOS/**", "tgcalls/tgcalls/platform/darwin/VideoCameraCapturerMac.*", "tgcalls/tgcalls/platform/darwin/VideoMetalViewMac.*", + "tgcalls/tgcalls/platform/darwin/VideoSampleBufferViewMac.*", "tgcalls/tgcalls/platform/darwin/GLVideoViewMac.*", "tgcalls/tgcalls/platform/darwin/ScreenCapturer.*", + "tgcalls/tgcalls/platform/darwin/DesktopSharingCapturer.*", + "tgcalls/tgcalls/platform/darwin/DesktopCaptureSourceViewMac.*", + "tgcalls/tgcalls/platform/darwin/DesktopCaptureSourceView.*", + "tgcalls/tgcalls/platform/darwin/TGCMIODevice.*", + "tgcalls/tgcalls/platform/darwin/TGCMIOCapturer.*", + "tgcalls/tgcalls/platform/darwin/VideoCMIOCapture.*", "tgcalls/tgcalls/desktop_capturer/**", ]), hdrs = glob([ @@ -55,6 +64,7 @@ objc_library( "//third-party/ogg:ogg", "//third-party/opusfile:opusfile", "//submodules/ffmpeg:ffmpeg", + "//third-party/rnnoise:rnnoise", ], sdk_frameworks = [ "Foundation", diff --git a/submodules/TgVoipWebrtc/PublicHeaders/TgVoipWebrtc/OngoingCallThreadLocalContext.h b/submodules/TgVoipWebrtc/PublicHeaders/TgVoipWebrtc/OngoingCallThreadLocalContext.h index 9bc06c6265..87c1645925 100644 --- a/submodules/TgVoipWebrtc/PublicHeaders/TgVoipWebrtc/OngoingCallThreadLocalContext.h +++ b/submodules/TgVoipWebrtc/PublicHeaders/TgVoipWebrtc/OngoingCallThreadLocalContext.h @@ -5,6 +5,7 @@ #if TARGET_OS_IOS #import +#import #else #import #define UIView NSView @@ -100,20 +101,91 @@ typedef NS_ENUM(int32_t, OngoingCallDataSavingWebrtc) { - (void)setOnFirstFrameReceived:(void (^ _Nullable)(float))onFirstFrameReceived; - (void)setOnOrientationUpdated:(void (^ _Nullable)(OngoingCallVideoOrientationWebrtc, CGFloat))onOrientationUpdated; - (void)setOnIsMirroredUpdated:(void (^ _Nullable)(bool))onIsMirroredUpdated; +- (void)updateIsEnabled:(bool)isEnabled; #if defined(WEBRTC_MAC) && !defined(WEBRTC_IOS) - (void)setVideoContentMode:(CALayerContentsGravity _Nonnull )mode; - (void)setForceMirrored:(bool)forceMirrored; +- (void)setIsPaused:(bool)paused; +- (void)renderToSize:(NSSize)size animated: (bool)animated; #endif @end +@interface GroupCallDisposable : NSObject + +- (void)dispose; + +@end + +@protocol CallVideoFrameBuffer + +@end + +@interface CallVideoFrameNativePixelBuffer : NSObject + +@property (nonatomic, readonly) CVPixelBufferRef _Nonnull pixelBuffer; + +@end + +@interface CallVideoFrameNV12Buffer : NSObject + +@property (nonatomic, readonly) int width; +@property (nonatomic, readonly) int height; + +@property (nonatomic, strong, readonly) NSData * _Nonnull y; +@property (nonatomic, readonly) int strideY; + +@property (nonatomic, strong, readonly) NSData * _Nonnull uv; +@property (nonatomic, readonly) int strideUV; + +@end + +@interface CallVideoFrameI420Buffer : NSObject + +@property (nonatomic, readonly) int width; +@property (nonatomic, readonly) int height; + +@property (nonatomic, strong, readonly) NSData * _Nonnull y; +@property (nonatomic, readonly) int strideY; + +@property (nonatomic, strong, readonly) NSData * _Nonnull u; +@property (nonatomic, readonly) int strideU; + +@property (nonatomic, strong, readonly) NSData * _Nonnull v; +@property (nonatomic, readonly) int strideV; + +@end + +@interface CallVideoFrameData : NSObject + +@property (nonatomic, strong, readonly) id _Nonnull buffer; +@property (nonatomic, readonly) int width; +@property (nonatomic, readonly) int height; +@property (nonatomic, readonly) OngoingCallVideoOrientationWebrtc orientation; + +@end + @interface OngoingCallThreadLocalContextVideoCapturer : NSObject - (instancetype _Nonnull)initWithDeviceId:(NSString * _Nonnull)deviceId keepLandscape:(bool)keepLandscape; +#if TARGET_OS_IOS ++ (instancetype _Nonnull)capturerWithExternalSampleBufferProvider; +#endif + - (void)switchVideoInput:(NSString * _Nonnull)deviceId; - (void)setIsVideoEnabled:(bool)isVideoEnabled; -- (void)makeOutgoingVideoView:(void (^_Nonnull)(UIView * _Nullable))completion; +- (void)makeOutgoingVideoView:(bool)requestClone completion:(void (^_Nonnull)(UIView * _Nullable, UIView * _Nullable))completion; + +- (void)setOnFatalError:(dispatch_block_t _Nullable)onError; +- (void)setOnPause:(void (^ _Nullable)(bool))onPause; +- (void)setOnIsActiveUpdated:(void (^_Nonnull)(bool))onIsActiveUpdated; + +#if TARGET_OS_IOS +- (void)submitPixelBuffer:(CVPixelBufferRef _Nonnull)pixelBuffer rotation:(OngoingCallVideoOrientationWebrtc)rotation; +#endif + +- (GroupCallDisposable * _Nonnull)addVideoOutput:(void (^_Nonnull)(CallVideoFrameData * _Nonnull))sink; @end @@ -156,12 +228,20 @@ typedef struct { bool isTransitioningFromBroadcastToRtc; } GroupCallNetworkState; -@interface OngoingGroupCallParticipantDescription : NSObject +typedef NS_ENUM(int32_t, OngoingGroupCallMediaChannelType) { + OngoingGroupCallMediaChannelTypeAudio, + OngoingGroupCallMediaChannelTypeVideo +}; +@interface OngoingGroupCallMediaChannelDescription : NSObject + +@property (nonatomic, readonly) OngoingGroupCallMediaChannelType type; @property (nonatomic, readonly) uint32_t audioSsrc; -@property (nonatomic, strong, readonly) NSString * _Nullable jsonParams; +@property (nonatomic, strong, readonly) NSString * _Nullable videoDescription; -- (instancetype _Nonnull)initWithAudioSsrc:(uint32_t)audioSsrc jsonParams:(NSString * _Nullable)jsonParams; +- (instancetype _Nonnull)initWithType:(OngoingGroupCallMediaChannelType)type + audioSsrc:(uint32_t)audioSsrc + videoDescription:(NSString * _Nullable)videoDescription; @end @@ -171,6 +251,12 @@ typedef struct { @end +@protocol OngoingGroupCallMediaChannelDescriptionTask + +- (void)cancel; + +@end + typedef NS_ENUM(int32_t, OngoingCallConnectionMode) { OngoingCallConnectionModeNone, OngoingCallConnectionModeRtc, @@ -183,6 +269,12 @@ typedef NS_ENUM(int32_t, OngoingGroupCallBroadcastPartStatus) { OngoingGroupCallBroadcastPartStatusResyncNeeded }; +typedef NS_ENUM(int32_t, OngoingGroupCallVideoContentType) { + OngoingGroupCallVideoContentTypeNone, + OngoingGroupCallVideoContentTypeGeneric, + OngoingGroupCallVideoContentTypeScreencast, +}; + @interface OngoingGroupCallBroadcastPart : NSObject @property (nonatomic, readonly) int64_t timestampMilliseconds; @@ -194,28 +286,70 @@ typedef NS_ENUM(int32_t, OngoingGroupCallBroadcastPartStatus) { @end +typedef NS_ENUM(int32_t, OngoingGroupCallRequestedVideoQuality) { + OngoingGroupCallRequestedVideoQualityThumbnail, + OngoingGroupCallRequestedVideoQualityMedium, + OngoingGroupCallRequestedVideoQualityFull, +}; + +@interface OngoingGroupCallSsrcGroup : NSObject + +@property (nonatomic, strong, readonly) NSString * _Nonnull semantics; +@property (nonatomic, strong, readonly) NSArray * _Nonnull ssrcs; + +- (instancetype _Nonnull)initWithSemantics:(NSString * _Nonnull)semantics ssrcs:(NSArray * _Nonnull)ssrcs; + +@end + +@interface OngoingGroupCallRequestedVideoChannel : NSObject + +@property (nonatomic, readonly) uint32_t audioSsrc; +@property (nonatomic, strong, readonly) NSString * _Nonnull endpointId; +@property (nonatomic, strong, readonly) NSArray * _Nonnull ssrcGroups; + +@property (nonatomic, readonly) OngoingGroupCallRequestedVideoQuality minQuality; +@property (nonatomic, readonly) OngoingGroupCallRequestedVideoQuality maxQuality; + +- (instancetype _Nonnull)initWithAudioSsrc:(uint32_t)audioSsrc endpointId:(NSString * _Nonnull)endpointId ssrcGroups:(NSArray * _Nonnull)ssrcGroups minQuality:(OngoingGroupCallRequestedVideoQuality)minQuality maxQuality:(OngoingGroupCallRequestedVideoQuality)maxQuality; + +@end + @interface GroupCallThreadLocalContext : NSObject -- (instancetype _Nonnull)initWithQueue:(id _Nonnull)queue networkStateUpdated:(void (^ _Nonnull)(GroupCallNetworkState))networkStateUpdated audioLevelsUpdated:(void (^ _Nonnull)(NSArray * _Nonnull))audioLevelsUpdated inputDeviceId:(NSString * _Nonnull)inputDeviceId outputDeviceId:(NSString * _Nonnull)outputDeviceId videoCapturer:(OngoingCallThreadLocalContextVideoCapturer * _Nullable)videoCapturer incomingVideoSourcesUpdated:(void (^ _Nonnull)(NSArray * _Nonnull))incomingVideoSourcesUpdated participantDescriptionsRequired:(void (^ _Nonnull)(NSArray * _Nonnull))participantDescriptionsRequired requestBroadcastPart:(id _Nonnull (^ _Nonnull)(int64_t, int64_t, void (^ _Nonnull)(OngoingGroupCallBroadcastPart * _Nullable)))requestBroadcastPart; +- (instancetype _Nonnull)initWithQueue:(id _Nonnull)queue + networkStateUpdated:(void (^ _Nonnull)(GroupCallNetworkState))networkStateUpdated + audioLevelsUpdated:(void (^ _Nonnull)(NSArray * _Nonnull))audioLevelsUpdated + inputDeviceId:(NSString * _Nonnull)inputDeviceId + outputDeviceId:(NSString * _Nonnull)outputDeviceId + videoCapturer:(OngoingCallThreadLocalContextVideoCapturer * _Nullable)videoCapturer + requestMediaChannelDescriptions:(id _Nonnull (^ _Nonnull)(NSArray * _Nonnull, void (^ _Nonnull)(NSArray * _Nonnull)))requestMediaChannelDescriptions + requestBroadcastPart:(id _Nonnull (^ _Nonnull)(int64_t, int64_t, void (^ _Nonnull)(OngoingGroupCallBroadcastPart * _Nullable)))requestBroadcastPart + outgoingAudioBitrateKbit:(int32_t)outgoingAudioBitrateKbit + videoContentType:(OngoingGroupCallVideoContentType)videoContentType + enableNoiseSuppression:(bool)enableNoiseSuppression; - (void)stop; - (void)setConnectionMode:(OngoingCallConnectionMode)connectionMode keepBroadcastConnectedIfWasEnabled:(bool)keepBroadcastConnectedIfWasEnabled; - (void)emitJoinPayload:(void (^ _Nonnull)(NSString * _Nonnull, uint32_t))completion; -- (void)setJoinResponsePayload:(NSString * _Nonnull)payload participants:(NSArray * _Nonnull)participants; +- (void)setJoinResponsePayload:(NSString * _Nonnull)payload; - (void)removeSsrcs:(NSArray * _Nonnull)ssrcs; -- (void)addParticipants:(NSArray * _Nonnull)participants; +- (void)removeIncomingVideoSource:(uint32_t)ssrc; - (void)setIsMuted:(bool)isMuted; +- (void)setIsNoiseSuppressionEnabled:(bool)isNoiseSuppressionEnabled; - (void)requestVideo:(OngoingCallThreadLocalContextVideoCapturer * _Nullable)videoCapturer completion:(void (^ _Nonnull)(NSString * _Nonnull, uint32_t))completion; - (void)disableVideo:(void (^ _Nonnull)(NSString * _Nonnull, uint32_t))completion; - (void)setVolumeForSsrc:(uint32_t)ssrc volume:(double)volume; -- (void)setFullSizeVideoSsrc:(uint32_t)ssrc; +- (void)setRequestedVideoChannels:(NSArray * _Nonnull)requestedVideoChannels; - (void)switchAudioOutput:(NSString * _Nonnull)deviceId; - (void)switchAudioInput:(NSString * _Nonnull)deviceId; -- (void)makeIncomingVideoViewWithSsrc:(uint32_t)ssrc completion:(void (^_Nonnull)(UIView * _Nullable))completion; +- (void)makeIncomingVideoViewWithEndpointId:(NSString * _Nonnull)endpointId requestClone:(bool)requestClone completion:(void (^_Nonnull)(UIView * _Nullable, UIView * _Nullable))completion; +- (GroupCallDisposable * _Nonnull)addVideoOutputWithEndpointId:(NSString * _Nonnull)endpointId sink:(void (^_Nonnull)(CallVideoFrameData * _Nonnull))sink; + +- (void)addExternalAudioData:(NSData * _Nonnull)data; @end diff --git a/submodules/TgVoipWebrtc/Sources/OngoingCallThreadLocalContext.mm b/submodules/TgVoipWebrtc/Sources/OngoingCallThreadLocalContext.mm index 948fd87723..f78eae73ac 100644 --- a/submodules/TgVoipWebrtc/Sources/OngoingCallThreadLocalContext.mm +++ b/submodules/TgVoipWebrtc/Sources/OngoingCallThreadLocalContext.mm @@ -6,25 +6,35 @@ #import "Instance.h" #import "InstanceImpl.h" -#import "reference/InstanceImplReference.h" +#import "v2/InstanceV2Impl.h" #include "StaticThreads.h" #import "VideoCaptureInterface.h" +#import "platform/darwin/VideoCameraCapturer.h" #ifndef WEBRTC_IOS #import "platform/darwin/VideoMetalViewMac.h" #import "platform/darwin/GLVideoViewMac.h" +#import "platform/darwin/VideoSampleBufferViewMac.h" #define UIViewContentModeScaleAspectFill kCAGravityResizeAspectFill #define UIViewContentModeScaleAspect kCAGravityResizeAspect #else #import "platform/darwin/VideoMetalView.h" #import "platform/darwin/GLVideoView.h" +#import "platform/darwin/VideoSampleBufferView.h" +#import "platform/darwin/VideoCaptureView.h" +#import "platform/darwin/CustomExternalCapturer.h" #endif #import "group/GroupInstanceImpl.h" #import "group/GroupInstanceCustomImpl.h" +#import "VideoCaptureInterfaceImpl.h" + +#include "sdk/objc/native/src/objc_frame_buffer.h" +#import "components/video_frame_buffer/RTCCVPixelBuffer.h" + @implementation OngoingCallConnectionDescriptionWebrtc - (instancetype _Nonnull)initWithConnectionId:(int64_t)connectionId hasStun:(bool)hasStun hasTurn:(bool)hasTurn ip:(NSString * _Nonnull)ip port:(int32_t)port username:(NSString * _Nonnull)username password:(NSString * _Nonnull)password { @@ -43,8 +53,26 @@ @end +@interface IsProcessingCustomSampleBufferFlag : NSObject + +@property (nonatomic) bool value; + +@end + +@implementation IsProcessingCustomSampleBufferFlag + +- (instancetype)init { + self = [super init]; + if (self != nil) { + } + return self; +} + +@end + @interface OngoingCallThreadLocalContextVideoCapturer () { std::shared_ptr _interface; + IsProcessingCustomSampleBufferFlag *_isProcessingCustomSampleBuffer; } @end @@ -97,6 +125,10 @@ } } +- (void)updateIsEnabled:(bool)isEnabled { + [self setEnabled:isEnabled]; +} + @end @interface GLVideoView (VideoViewImpl) @@ -140,16 +172,284 @@ } } +- (void)updateIsEnabled:(bool)__unused isEnabled { +} + +@end + +@interface VideoSampleBufferView (VideoViewImpl) + +@property (nonatomic, readwrite) OngoingCallVideoOrientationWebrtc orientation; +@property (nonatomic, readonly) CGFloat aspect; + +@end + +@implementation VideoSampleBufferView (VideoViewImpl) + +- (OngoingCallVideoOrientationWebrtc)orientation { + return (OngoingCallVideoOrientationWebrtc)self.internalOrientation; +} + +- (CGFloat)aspect { + return self.internalAspect; +} + +- (void)setOrientation:(OngoingCallVideoOrientationWebrtc)orientation { + [self setInternalOrientation:(int)orientation]; +} + +- (void)setOnOrientationUpdated:(void (^ _Nullable)(OngoingCallVideoOrientationWebrtc, CGFloat))onOrientationUpdated { + if (onOrientationUpdated) { + [self internalSetOnOrientationUpdated:^(int value, CGFloat aspect) { + onOrientationUpdated((OngoingCallVideoOrientationWebrtc)value, aspect); + }]; + } else { + [self internalSetOnOrientationUpdated:nil]; + } +} + +- (void)setOnIsMirroredUpdated:(void (^ _Nullable)(bool))onIsMirroredUpdated { + if (onIsMirroredUpdated) { + [self internalSetOnIsMirroredUpdated:^(bool value) { + onIsMirroredUpdated(value); + }]; + } else { + [self internalSetOnIsMirroredUpdated:nil]; + } +} + +- (void)updateIsEnabled:(bool)isEnabled { + [self setEnabled:isEnabled]; +} + +@end + +@interface GroupCallDisposable () { + dispatch_block_t _block; +} + +@end + +@implementation GroupCallDisposable + +- (instancetype)initWithBlock:(dispatch_block_t _Nonnull)block { + self = [super init]; + if (self != nil) { + _block = [block copy]; + } + return self; +} + +- (void)dispose { + if (_block) { + _block(); + } +} + +@end + +@implementation CallVideoFrameNativePixelBuffer + +- (instancetype)initWithPixelBuffer:(CVPixelBufferRef)pixelBuffer { + self = [super init]; + if (self != nil) { + assert(pixelBuffer != nil); + + _pixelBuffer = CVPixelBufferRetain(pixelBuffer); + } + return self; +} + +- (void)dealloc { + CVPixelBufferRelease(_pixelBuffer); +} + +@end + +@implementation CallVideoFrameNV12Buffer + +- (instancetype)initWithBuffer:(rtc::scoped_refptr)nv12Buffer { + self = [super init]; + if (self != nil) { + _width = nv12Buffer->width(); + _height = nv12Buffer->height(); + + _strideY = nv12Buffer->StrideY(); + _strideUV = nv12Buffer->StrideUV(); + + _y = [[NSData alloc] initWithBytesNoCopy:(void *)nv12Buffer->DataY() length:nv12Buffer->StrideY() * _height deallocator:^(__unused void * _Nonnull bytes, __unused NSUInteger length) { + nv12Buffer.get(); + }]; + + _uv = [[NSData alloc] initWithBytesNoCopy:(void *)nv12Buffer->DataUV() length:nv12Buffer->StrideUV() * _height deallocator:^(__unused void * _Nonnull bytes, __unused NSUInteger length) { + nv12Buffer.get(); + }]; + } + return self; +} + +@end + +@implementation CallVideoFrameI420Buffer + +- (instancetype)initWithBuffer:(rtc::scoped_refptr)i420Buffer { + self = [super init]; + if (self != nil) { + _width = i420Buffer->width(); + _height = i420Buffer->height(); + + _strideY = i420Buffer->StrideY(); + _strideU = i420Buffer->StrideU(); + _strideV = i420Buffer->StrideV(); + + _y = [[NSData alloc] initWithBytesNoCopy:(void *)i420Buffer->DataY() length:i420Buffer->StrideY() * _height deallocator:^(__unused void * _Nonnull bytes, __unused NSUInteger length) { + i420Buffer.get(); + }]; + + _u = [[NSData alloc] initWithBytesNoCopy:(void *)i420Buffer->DataU() length:i420Buffer->StrideU() * _height deallocator:^(__unused void * _Nonnull bytes, __unused NSUInteger length) { + i420Buffer.get(); + }]; + + _v = [[NSData alloc] initWithBytesNoCopy:(void *)i420Buffer->DataV() length:i420Buffer->StrideV() * _height deallocator:^(__unused void * _Nonnull bytes, __unused NSUInteger length) { + i420Buffer.get(); + }]; + } + return self; +} + +@end + +@interface CallVideoFrameData () { +} + +@end + +@implementation CallVideoFrameData + +- (instancetype)initWithBuffer:(id)buffer frame:(webrtc::VideoFrame const &)frame { + self = [super init]; + if (self != nil) { + _buffer = buffer; + + _width = frame.width(); + _height = frame.height(); + + switch (frame.rotation()) { + case webrtc::kVideoRotation_0: { + _orientation = OngoingCallVideoOrientation0; + break; + } + case webrtc::kVideoRotation_90: { + _orientation = OngoingCallVideoOrientation90; + break; + } + case webrtc::kVideoRotation_180: { + _orientation = OngoingCallVideoOrientation180; + break; + } + case webrtc::kVideoRotation_270: { + _orientation = OngoingCallVideoOrientation270; + break; + } + default: { + _orientation = OngoingCallVideoOrientation0; + break; + } + } + } + return self; +} + +@end + +namespace { + +class GroupCallVideoSinkAdapter : public rtc::VideoSinkInterface { +public: + GroupCallVideoSinkAdapter(void (^frameReceived)(webrtc::VideoFrame const &)) { + _frameReceived = [frameReceived copy]; + } + + void OnFrame(const webrtc::VideoFrame& nativeVideoFrame) override { + @autoreleasepool { + if (_frameReceived) { + _frameReceived(nativeVideoFrame); + } + } + } + +private: + void (^_frameReceived)(webrtc::VideoFrame const &); +}; + +} + +@interface GroupCallVideoSink : NSObject { + std::shared_ptr _adapter; +} + +@end + +@implementation GroupCallVideoSink + +- (instancetype)initWithSink:(void (^_Nonnull)(CallVideoFrameData * _Nonnull))sink { + self = [super init]; + if (self != nil) { + void (^storedSink)(CallVideoFrameData * _Nonnull) = [sink copy]; + + _adapter.reset(new GroupCallVideoSinkAdapter(^(webrtc::VideoFrame const &videoFrame) { + id mappedBuffer = nil; + + if (videoFrame.video_frame_buffer()->type() == webrtc::VideoFrameBuffer::Type::kNative) { + id nativeBuffer = static_cast(videoFrame.video_frame_buffer().get())->wrapped_frame_buffer(); + if ([nativeBuffer isKindOfClass:[RTC_OBJC_TYPE(RTCCVPixelBuffer) class]]) { + RTCCVPixelBuffer *pixelBuffer = (RTCCVPixelBuffer *)nativeBuffer; + mappedBuffer = [[CallVideoFrameNativePixelBuffer alloc] initWithPixelBuffer:pixelBuffer.pixelBuffer]; + } + } else if (videoFrame.video_frame_buffer()->type() == webrtc::VideoFrameBuffer::Type::kNV12) { + rtc::scoped_refptr nv12Buffer = (webrtc::NV12BufferInterface *)videoFrame.video_frame_buffer().get(); + mappedBuffer = [[CallVideoFrameNV12Buffer alloc] initWithBuffer:nv12Buffer]; + } else if (videoFrame.video_frame_buffer()->type() == webrtc::VideoFrameBuffer::Type::kI420) { + rtc::scoped_refptr i420Buffer = (webrtc::I420BufferInterface *)videoFrame.video_frame_buffer().get(); + mappedBuffer = [[CallVideoFrameI420Buffer alloc] initWithBuffer:i420Buffer]; + } + + if (storedSink && mappedBuffer) { + storedSink([[CallVideoFrameData alloc] initWithBuffer:mappedBuffer frame:videoFrame]); + } + })); + } + return self; +} + +- (std::shared_ptr>)sink { + return _adapter; +} + @end @interface OngoingCallThreadLocalContextVideoCapturer () { bool _keepLandscape; + std::shared_ptr> _croppingBuffer; + + int _nextSinkId; + NSMutableDictionary *_sinks; } @end @implementation OngoingCallThreadLocalContextVideoCapturer +- (instancetype _Nonnull)initWithInterface:(std::shared_ptr)interface { + self = [super init]; + if (self != nil) { + _interface = interface; + _isProcessingCustomSampleBuffer = [[IsProcessingCustomSampleBufferFlag alloc] init]; + _croppingBuffer = std::make_shared>(); + } + return self; +} + - (instancetype _Nonnull)initWithDeviceId:(NSString * _Nonnull)deviceId keepLandscape:(bool)keepLandscape { self = [super init]; if (self != nil) { @@ -164,10 +464,89 @@ return self; } +#if TARGET_OS_IOS + +tgcalls::VideoCaptureInterfaceObject *GetVideoCaptureAssumingSameThread(tgcalls::VideoCaptureInterface *videoCapture) { + return videoCapture + ? static_cast(videoCapture)->object()->getSyncAssumingSameThread() + : nullptr; +} + ++ (instancetype _Nonnull)capturerWithExternalSampleBufferProvider { + std::shared_ptr interface = tgcalls::VideoCaptureInterface::Create(tgcalls::StaticThreads::getThreads(), ":ios_custom"); + return [[OngoingCallThreadLocalContextVideoCapturer alloc] initWithInterface:interface]; +} +#endif - (void)dealloc { } +#if TARGET_OS_IOS +- (void)submitPixelBuffer:(CVPixelBufferRef _Nonnull)pixelBuffer rotation:(OngoingCallVideoOrientationWebrtc)rotation { + if (!pixelBuffer) { + return; + } + + RTCVideoRotation videoRotation = RTCVideoRotation_0; + switch (rotation) { + case OngoingCallVideoOrientation0: + videoRotation = RTCVideoRotation_0; + break; + case OngoingCallVideoOrientation90: + videoRotation = RTCVideoRotation_90; + break; + case OngoingCallVideoOrientation180: + videoRotation = RTCVideoRotation_180; + break; + case OngoingCallVideoOrientation270: + videoRotation = RTCVideoRotation_270; + break; + } + + if (_isProcessingCustomSampleBuffer.value) { + return; + } + _isProcessingCustomSampleBuffer.value = true; + + tgcalls::StaticThreads::getThreads()->getMediaThread()->PostTask(RTC_FROM_HERE, [interface = _interface, pixelBuffer = CFRetain(pixelBuffer), croppingBuffer = _croppingBuffer, videoRotation = videoRotation, isProcessingCustomSampleBuffer = _isProcessingCustomSampleBuffer]() { + auto capture = GetVideoCaptureAssumingSameThread(interface.get()); + auto source = capture->source(); + if (source) { + [CustomExternalCapturer passPixelBuffer:(CVPixelBufferRef)pixelBuffer rotation:videoRotation toSource:source croppingBuffer:*croppingBuffer]; + } + CFRelease(pixelBuffer); + isProcessingCustomSampleBuffer.value = false; + }); +} + +#endif + +- (GroupCallDisposable * _Nonnull)addVideoOutput:(void (^_Nonnull)(CallVideoFrameData * _Nonnull))sink { + int sinkId = _nextSinkId; + _nextSinkId += 1; + + GroupCallVideoSink *storedSink = [[GroupCallVideoSink alloc] initWithSink:sink]; + _sinks[@(sinkId)] = storedSink; + + auto sinkReference = [storedSink sink]; + + tgcalls::StaticThreads::getThreads()->getMediaThread()->PostTask(RTC_FROM_HERE, [interface = _interface, sinkReference]() { + interface->setOutput(sinkReference); + }); + + __weak OngoingCallThreadLocalContextVideoCapturer *weakSelf = self; + return [[GroupCallDisposable alloc] initWithBlock:^{ + dispatch_async(dispatch_get_main_queue(), ^{ + __strong OngoingCallThreadLocalContextVideoCapturer *strongSelf = weakSelf; + if (!strongSelf) { + return; + } + + [strongSelf->_sinks removeObjectForKey:@(sinkId)]; + }); + }]; +} + - (void)switchVideoInput:(NSString * _Nonnull)deviceId { std::string resolvedId = deviceId.UTF8String; if (_keepLandscape) { @@ -184,31 +563,93 @@ return _interface; } -- (void)makeOutgoingVideoView:(void (^_Nonnull)(UIView * _Nullable))completion { - std::shared_ptr interface = _interface; - dispatch_async(dispatch_get_main_queue(), ^{ - if ([VideoMetalView isSupported]) { - VideoMetalView *remoteRenderer = [[VideoMetalView alloc] initWithFrame:CGRectZero]; - remoteRenderer.videoContentMode = UIViewContentModeScaleAspectFill; - - std::shared_ptr> sink = [remoteRenderer getSink]; - interface->setOutput(sink); - - completion(remoteRenderer); - } else { - GLVideoView *remoteRenderer = [[GLVideoView alloc] initWithFrame:CGRectZero]; -#ifndef WEBRTC_IOS - remoteRenderer.videoContentMode = UIViewContentModeScaleAspectFill; +-(void)setOnFatalError:(dispatch_block_t _Nullable)onError { +#if TARGET_OS_IOS +#else + _interface->setOnFatalError(onError); #endif +} - std::shared_ptr> sink = [remoteRenderer getSink]; - interface->setOutput(sink); - - completion(remoteRenderer); +-(void)setOnPause:(void (^)(bool))onPause { +#if TARGET_OS_IOS +#else + _interface->setOnPause(onPause); +#endif +} + +- (void)setOnIsActiveUpdated:(void (^)(bool))onIsActiveUpdated { + _interface->setOnIsActiveUpdated([onIsActiveUpdated](bool isActive) { + if (onIsActiveUpdated) { + onIsActiveUpdated(isActive); } }); } +- (void)makeOutgoingVideoView:(bool)requestClone completion:(void (^_Nonnull)(UIView * _Nullable, UIView * _Nullable))completion { + __weak OngoingCallThreadLocalContextVideoCapturer *weakSelf = self; + + void (^makeDefault)() = ^{ + dispatch_async(dispatch_get_main_queue(), ^{ + __strong OngoingCallThreadLocalContextVideoCapturer *strongSelf = weakSelf; + if (!strongSelf) { + return; + } + std::shared_ptr interface = strongSelf->_interface; + + if (false && requestClone) { + VideoSampleBufferView *remoteRenderer = [[VideoSampleBufferView alloc] initWithFrame:CGRectZero]; + remoteRenderer.videoContentMode = UIViewContentModeScaleAspectFill; + + std::shared_ptr> sink = [remoteRenderer getSink]; + interface->setOutput(sink); + + VideoSampleBufferView *cloneRenderer = nil; + if (requestClone) { + cloneRenderer = [[VideoSampleBufferView alloc] initWithFrame:CGRectZero]; + cloneRenderer.videoContentMode = UIViewContentModeScaleAspectFill; +#ifdef WEBRTC_IOS + [remoteRenderer setCloneTarget:cloneRenderer]; +#endif + } + + completion(remoteRenderer, cloneRenderer); + } else if ([VideoMetalView isSupported]) { + VideoMetalView *remoteRenderer = [[VideoMetalView alloc] initWithFrame:CGRectZero]; + remoteRenderer.videoContentMode = UIViewContentModeScaleAspectFill; + + VideoMetalView *cloneRenderer = nil; + if (requestClone) { + cloneRenderer = [[VideoMetalView alloc] initWithFrame:CGRectZero]; +#ifdef WEBRTC_IOS + cloneRenderer.videoContentMode = UIViewContentModeScaleToFill; + [remoteRenderer setClone:cloneRenderer]; +#else + cloneRenderer.videoContentMode = kCAGravityResizeAspectFill; +#endif + } + + std::shared_ptr> sink = [remoteRenderer getSink]; + + interface->setOutput(sink); + + completion(remoteRenderer, cloneRenderer); + } else { + GLVideoView *remoteRenderer = [[GLVideoView alloc] initWithFrame:CGRectZero]; + #ifndef WEBRTC_IOS + remoteRenderer.videoContentMode = UIViewContentModeScaleAspectFill; + #endif + + std::shared_ptr> sink = [remoteRenderer getSink]; + interface->setOutput(sink); + + completion(remoteRenderer, nil); + } + }); + }; + + makeDefault(); +} + @end @interface OngoingCallThreadLocalContextWebrtcTerminationResult : NSObject @@ -332,8 +773,14 @@ static void (*InternalVoipLoggingFunction)(NSString *) = NULL; return 92; } -+ (NSArray * _Nonnull)versionsWithIncludeReference:(bool)__unused includeReference { - return @[@"2.7.7", @"3.0.0"]; ++ (NSArray * _Nonnull)versionsWithIncludeReference:(bool)includeReference { + NSMutableArray *list = [[NSMutableArray alloc] init]; + [list addObject:@"2.7.7"]; + [list addObject:@"3.0.0"]; + if (includeReference) { + [list addObject:@"4.0.0"]; + } + return list; } + (tgcalls::ProtocolVersion)protocolVersionFromLibraryVersion:(NSString *)version { @@ -427,7 +874,7 @@ static void (*InternalVoipLoggingFunction)(NSString *) = NULL; .enableNS = true, .enableAGC = true, .enableCallUpgrade = false, - .logPath = std::string(logPath.length == 0 ? "" : logPath.UTF8String), + .logPath = std::string(logPath.length == 0 ? "" : logPath.UTF8String), .statsLogPath = std::string(statsLogPath.length == 0 ? "" : statsLogPath.UTF8String), .maxApiLayer = [OngoingCallThreadLocalContextWebrtc maxLayer], .enableHighBitrateVideo = true, @@ -444,10 +891,9 @@ static void (*InternalVoipLoggingFunction)(NSString *) = NULL; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ tgcalls::Register(); + tgcalls::Register(); }); - - _tgVoip = tgcalls::Meta::Create([version UTF8String], (tgcalls::Descriptor){ .config = config, .persistentState = (tgcalls::PersistentState){ derivedStateValue }, @@ -839,32 +1285,96 @@ private: id _task; }; +class RequestMediaChannelDescriptionTaskImpl : public tgcalls::RequestMediaChannelDescriptionTask { +public: + RequestMediaChannelDescriptionTaskImpl(id task) { + _task = task; + } + + virtual ~RequestMediaChannelDescriptionTaskImpl() { + } + + virtual void cancel() override { + [_task cancel]; + } + +private: + id _task; +}; + } @interface GroupCallThreadLocalContext () { id _queue; - + std::unique_ptr _instance; OngoingCallThreadLocalContextVideoCapturer *_videoCapturer; - + void (^_networkStateUpdated)(GroupCallNetworkState); + + int _nextSinkId; + NSMutableDictionary *_sinks; } @end @implementation GroupCallThreadLocalContext -- (instancetype _Nonnull)initWithQueue:(id _Nonnull)queue networkStateUpdated:(void (^ _Nonnull)(GroupCallNetworkState))networkStateUpdated audioLevelsUpdated:(void (^ _Nonnull)(NSArray * _Nonnull))audioLevelsUpdated inputDeviceId:(NSString * _Nonnull)inputDeviceId outputDeviceId:(NSString * _Nonnull)outputDeviceId videoCapturer:(OngoingCallThreadLocalContextVideoCapturer * _Nullable)videoCapturer incomingVideoSourcesUpdated:(void (^ _Nonnull)(NSArray * _Nonnull))incomingVideoSourcesUpdated participantDescriptionsRequired:(void (^ _Nonnull)(NSArray * _Nonnull))participantDescriptionsRequired requestBroadcastPart:(id _Nonnull (^ _Nonnull)(int64_t, int64_t, void (^ _Nonnull)(OngoingGroupCallBroadcastPart * _Nullable)))requestBroadcastPart { +- (instancetype _Nonnull)initWithQueue:(id _Nonnull)queue + networkStateUpdated:(void (^ _Nonnull)(GroupCallNetworkState))networkStateUpdated + audioLevelsUpdated:(void (^ _Nonnull)(NSArray * _Nonnull))audioLevelsUpdated + inputDeviceId:(NSString * _Nonnull)inputDeviceId + outputDeviceId:(NSString * _Nonnull)outputDeviceId + videoCapturer:(OngoingCallThreadLocalContextVideoCapturer * _Nullable)videoCapturer + requestMediaChannelDescriptions:(id _Nonnull (^ _Nonnull)(NSArray * _Nonnull, void (^ _Nonnull)(NSArray * _Nonnull)))requestMediaChannelDescriptions + requestBroadcastPart:(id _Nonnull (^ _Nonnull)(int64_t, int64_t, void (^ _Nonnull)(OngoingGroupCallBroadcastPart * _Nullable)))requestBroadcastPart + outgoingAudioBitrateKbit:(int32_t)outgoingAudioBitrateKbit + videoContentType:(OngoingGroupCallVideoContentType)videoContentType + enableNoiseSuppression:(bool)enableNoiseSuppression { self = [super init]; if (self != nil) { _queue = queue; + + _sinks = [[NSMutableDictionary alloc] init]; _networkStateUpdated = [networkStateUpdated copy]; _videoCapturer = videoCapturer; + tgcalls::VideoContentType _videoContentType; + switch (videoContentType) { + case OngoingGroupCallVideoContentTypeGeneric: { + _videoContentType = tgcalls::VideoContentType::Generic; + break; + } + case OngoingGroupCallVideoContentTypeScreencast: { + _videoContentType = tgcalls::VideoContentType::Screencast; + break; + } + case OngoingGroupCallVideoContentTypeNone: { + _videoContentType = tgcalls::VideoContentType::None; + break; + } + default: { + _videoContentType = tgcalls::VideoContentType::None; + break; + } + } + + std::vector videoCodecPreferences; + + int minOutgoingVideoBitrateKbit = 500; + bool disableOutgoingAudioProcessing = false; + + tgcalls::GroupConfig config; + config.need_log = false; +#if DEBUG + config.need_log = true; +#endif + __weak GroupCallThreadLocalContext *weakSelf = self; _instance.reset(new tgcalls::GroupInstanceCustomImpl((tgcalls::GroupInstanceDescriptor){ .threads = tgcalls::StaticThreads::getThreads(), + .config = config, .networkStateUpdated = [weakSelf, queue, networkStateUpdated](tgcalls::GroupNetworkState networkState) { [queue dispatch:^{ __strong GroupCallThreadLocalContext *strongSelf = weakSelf; @@ -889,20 +1399,6 @@ private: .initialInputDeviceId = inputDeviceId.UTF8String, .initialOutputDeviceId = outputDeviceId.UTF8String, .videoCapture = [_videoCapturer getInterface], - .incomingVideoSourcesUpdated = [incomingVideoSourcesUpdated](std::vector const &ssrcs) { - NSMutableArray *mappedSources = [[NSMutableArray alloc] init]; - for (auto it : ssrcs) { - [mappedSources addObject:@(it)]; - } - incomingVideoSourcesUpdated(mappedSources); - }, - .participantDescriptionsRequired = [participantDescriptionsRequired](std::vector const &ssrcs) { - NSMutableArray *mappedSources = [[NSMutableArray alloc] init]; - for (auto it : ssrcs) { - [mappedSources addObject:@(it)]; - } - participantDescriptionsRequired(mappedSources); - }, .requestBroadcastPart = [requestBroadcastPart](int64_t timestampMilliseconds, int64_t durationMilliseconds, std::function completion) -> std::shared_ptr { id task = requestBroadcastPart(timestampMilliseconds, durationMilliseconds, ^(OngoingGroupCallBroadcastPart * _Nullable part) { tgcalls::BroadcastPart parsedPart; @@ -937,7 +1433,45 @@ private: completion(std::move(parsedPart)); }); return std::make_shared(task); - } + }, + .outgoingAudioBitrateKbit = outgoingAudioBitrateKbit, + .disableOutgoingAudioProcessing = disableOutgoingAudioProcessing, + .videoContentType = _videoContentType, + .videoCodecPreferences = videoCodecPreferences, + .initialEnableNoiseSuppression = enableNoiseSuppression, + .requestMediaChannelDescriptions = [requestMediaChannelDescriptions](std::vector const &ssrcs, std::function &&)> completion) -> std::shared_ptr { + NSMutableArray *mappedSsrcs = [[NSMutableArray alloc] init]; + for (auto ssrc : ssrcs) { + [mappedSsrcs addObject:[NSNumber numberWithUnsignedInt:ssrc]]; + } + id task = requestMediaChannelDescriptions(mappedSsrcs, ^(NSArray *channels) { + std::vector mappedChannels; + for (OngoingGroupCallMediaChannelDescription *channel in channels) { + tgcalls::MediaChannelDescription mappedChannel; + switch (channel.type) { + case OngoingGroupCallMediaChannelTypeAudio: { + mappedChannel.type = tgcalls::MediaChannelDescription::Type::Audio; + break; + } + case OngoingGroupCallMediaChannelTypeVideo: { + mappedChannel.type = tgcalls::MediaChannelDescription::Type::Video; + break; + } + default: { + continue; + } + } + mappedChannel.audioSsrc = channel.audioSsrc; + mappedChannel.videoInformation = channel.videoDescription.UTF8String ?: ""; + mappedChannels.push_back(std::move(mappedChannel)); + } + + completion(std::move(mappedChannels)); + }); + + return std::make_shared(task); + }, + .minOutgoingVideoBitrateKbit = minOutgoingVideoBitrateKbit })); } return self; @@ -950,102 +1484,6 @@ private: } } -static void processJoinPayload(tgcalls::GroupJoinPayload &payload, void (^ _Nonnull completion)(NSString * _Nonnull, uint32_t)) { - NSMutableDictionary *dict = [[NSMutableDictionary alloc] init]; - - int32_t signedSsrc = *(int32_t *)&payload.ssrc; - - dict[@"ssrc"] = @(signedSsrc); - dict[@"ufrag"] = [NSString stringWithUTF8String:payload.ufrag.c_str()]; - dict[@"pwd"] = [NSString stringWithUTF8String:payload.pwd.c_str()]; - - NSMutableArray *fingerprints = [[NSMutableArray alloc] init]; - for (auto &fingerprint : payload.fingerprints) { - [fingerprints addObject:@{ - @"hash": [NSString stringWithUTF8String:fingerprint.hash.c_str()], - @"fingerprint": [NSString stringWithUTF8String:fingerprint.fingerprint.c_str()], - @"setup": [NSString stringWithUTF8String:fingerprint.setup.c_str()] - }]; - } - - dict[@"fingerprints"] = fingerprints; - - NSMutableArray *parsedVideoSsrcGroups = [[NSMutableArray alloc] init]; - NSMutableArray *parsedVideoSources = [[NSMutableArray alloc] init]; - for (auto &group : payload.videoSourceGroups) { - NSMutableDictionary *parsedGroup = [[NSMutableDictionary alloc] init]; - parsedGroup[@"semantics"] = [NSString stringWithUTF8String:group.semantics.c_str()]; - NSMutableArray *sources = [[NSMutableArray alloc] init]; - for (auto &source : group.ssrcs) { - [sources addObject:@(source)]; - if (![parsedVideoSources containsObject:@(source)]) { - [parsedVideoSources addObject:@(source)]; - } - } - parsedGroup[@"sources"] = sources; - [parsedVideoSsrcGroups addObject:parsedGroup]; - } - if (parsedVideoSsrcGroups.count != 0) { - dict[@"ssrc-groups"] = parsedVideoSsrcGroups; - } - - NSMutableArray *videoPayloadTypes = [[NSMutableArray alloc] init]; - for (auto &payloadType : payload.videoPayloadTypes) { - NSMutableDictionary *parsedType = [[NSMutableDictionary alloc] init]; - parsedType[@"id"] = @(payloadType.id); - NSString *name = [NSString stringWithUTF8String:payloadType.name.c_str()]; - parsedType[@"name"] = name; - parsedType[@"clockrate"] = @(payloadType.clockrate); - if (![name isEqualToString:@"rtx"]) { - parsedType[@"channels"] = @(payloadType.channels); - } - - NSMutableDictionary *parsedParameters = [[NSMutableDictionary alloc] init]; - for (auto &it : payloadType.parameters) { - NSString *key = [NSString stringWithUTF8String:it.first.c_str()]; - NSString *value = [NSString stringWithUTF8String:it.second.c_str()]; - parsedParameters[key] = value; - } - if (parsedParameters.count != 0) { - parsedType[@"parameters"] = parsedParameters; - } - - if (![name isEqualToString:@"rtx"]) { - NSMutableArray *parsedFbs = [[NSMutableArray alloc] init]; - for (auto &it : payloadType.feedbackTypes) { - NSMutableDictionary *parsedFb = [[NSMutableDictionary alloc] init]; - parsedFb[@"type"] = [NSString stringWithUTF8String:it.type.c_str()]; - if (it.subtype.size() != 0) { - parsedFb[@"subtype"] = [NSString stringWithUTF8String:it.subtype.c_str()]; - } - [parsedFbs addObject:parsedFb]; - } - parsedType[@"rtcp-fbs"] = parsedFbs; - } - - [videoPayloadTypes addObject:parsedType]; - } - if (videoPayloadTypes.count != 0) { - dict[@"payload-types"] = videoPayloadTypes; - } - - NSMutableArray *parsedExtensions = [[NSMutableArray alloc] init]; - for (auto &it : payload.videoExtensionMap) { - NSMutableDictionary *parsedExtension = [[NSMutableDictionary alloc] init]; - parsedExtension[@"id"] = @(it.first); - parsedExtension[@"uri"] = [NSString stringWithUTF8String:it.second.c_str()]; - [parsedExtensions addObject:parsedExtension]; - } - if (parsedExtensions.count != 0) { - dict[@"rtp-hdrexts"] = parsedExtensions; - } - - NSData *data = [NSJSONSerialization dataWithJSONObject:dict options:0 error:nil]; - NSString *string = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; - - completion(string, payload.ssrc); -} - - (void)setConnectionMode:(OngoingCallConnectionMode)connectionMode keepBroadcastConnectedIfWasEnabled:(bool)keepBroadcastConnectedIfWasEnabled { if (_instance) { tgcalls::GroupConnectionMode mappedConnectionMode; @@ -1073,168 +1511,15 @@ static void processJoinPayload(tgcalls::GroupJoinPayload &payload, void (^ _Nonn - (void)emitJoinPayload:(void (^ _Nonnull)(NSString * _Nonnull, uint32_t))completion { if (_instance) { - _instance->emitJoinPayload([completion](tgcalls::GroupJoinPayload payload) { - processJoinPayload(payload, completion); + _instance->emitJoinPayload([completion](tgcalls::GroupJoinPayload const &payload) { + completion([NSString stringWithUTF8String:payload.json.c_str()], payload.audioSsrc); }); } } -- (void)setJoinResponsePayload:(NSString * _Nonnull)payload participants:(NSArray * _Nonnull)participants { - tgcalls::GroupJoinResponsePayload result; - - NSData *payloadData = [payload dataUsingEncoding:NSUTF8StringEncoding]; - if (payloadData == nil) { - return; - } - - NSDictionary *dict = [NSJSONSerialization JSONObjectWithData:payloadData options:0 error:nil]; - if (![dict isKindOfClass:[NSDictionary class]]) { - return; - } - - NSDictionary *transport = dict[@"transport"]; - if (![transport isKindOfClass:[NSDictionary class]]) { - return; - } - - NSString *pwd = transport[@"pwd"]; - if (![pwd isKindOfClass:[NSString class]]) { - return; - } - - NSString *ufrag = transport[@"ufrag"]; - if (![ufrag isKindOfClass:[NSString class]]) { - return; - } - - result.pwd = [pwd UTF8String]; - result.ufrag = [ufrag UTF8String]; - - NSArray *fingerprintsValue = transport[@"fingerprints"]; - if (![fingerprintsValue isKindOfClass:[NSArray class]]) { - //return; - } - - for (NSDictionary *fingerprintValue in fingerprintsValue) { - if (![fingerprintValue isKindOfClass:[NSDictionary class]]) { - continue; - } - NSString *hashValue = fingerprintValue[@"hash"]; - if (![hashValue isKindOfClass:[NSString class]]) { - continue; - } - NSString *fingerprint = fingerprintValue[@"fingerprint"]; - if (![fingerprint isKindOfClass:[NSString class]]) { - continue; - } - NSString *setup = fingerprintValue[@"setup"]; - if (![setup isKindOfClass:[NSString class]]) { - continue; - } - tgcalls::GroupJoinPayloadFingerprint parsed; - parsed.fingerprint = [fingerprint UTF8String]; - parsed.setup = [setup UTF8String]; - parsed.hash = [hashValue UTF8String]; - result.fingerprints.push_back(parsed); - } - - NSArray *candidatesValue = transport[@"candidates"]; - if (![candidatesValue isKindOfClass:[NSArray class]]) { - return; - } - - for (NSDictionary *candidateValue in candidatesValue) { - if (![candidateValue isKindOfClass:[NSDictionary class]]) { - continue; - } - - NSString *portValue = candidateValue[@"port"]; - if (![portValue isKindOfClass:[NSString class]]) { - continue; - } - NSString *protocolValue = candidateValue[@"protocol"]; - if (![protocolValue isKindOfClass:[NSString class]]) { - continue; - } - NSString *networkValue = candidateValue[@"network"]; - if (![networkValue isKindOfClass:[NSString class]]) { - continue; - } - NSString *generationValue = candidateValue[@"generation"]; - if (![generationValue isKindOfClass:[NSString class]]) { - continue; - } - NSString *idValue = candidateValue[@"id"]; - if (![idValue isKindOfClass:[NSString class]]) { - continue; - } - NSString *componentValue = candidateValue[@"component"]; - if (![componentValue isKindOfClass:[NSString class]]) { - continue; - } - NSString *foundationValue = candidateValue[@"foundation"]; - if (![foundationValue isKindOfClass:[NSString class]]) { - continue; - } - NSString *priorityValue = candidateValue[@"priority"]; - if (![priorityValue isKindOfClass:[NSString class]]) { - continue; - } - NSString *ipValue = candidateValue[@"ip"]; - if (![ipValue isKindOfClass:[NSString class]]) { - continue; - } - NSString *typeValue = candidateValue[@"type"]; - if (![typeValue isKindOfClass:[NSString class]]) { - continue; - } - - NSString *tcpTypeValue = candidateValue[@"tcptype"]; - if (![tcpTypeValue isKindOfClass:[NSString class]]) { - tcpTypeValue = @""; - } - NSString *relAddrValue = candidateValue[@"rel-addr"]; - if (![relAddrValue isKindOfClass:[NSString class]]) { - relAddrValue = @""; - } - NSString *relPortValue = candidateValue[@"rel-port"]; - if (![relPortValue isKindOfClass:[NSString class]]) { - relPortValue = @""; - } - - tgcalls::GroupJoinResponseCandidate candidate; - - candidate.port = [portValue UTF8String]; - candidate.protocol = [protocolValue UTF8String]; - candidate.network = [networkValue UTF8String]; - candidate.generation = [generationValue UTF8String]; - candidate.id = [idValue UTF8String]; - candidate.component = [componentValue UTF8String]; - candidate.foundation = [foundationValue UTF8String]; - candidate.priority = [priorityValue UTF8String]; - candidate.ip = [ipValue UTF8String]; - candidate.type = [typeValue UTF8String]; - - candidate.tcpType = [tcpTypeValue UTF8String]; - candidate.relAddr = [relAddrValue UTF8String]; - candidate.relPort = [relPortValue UTF8String]; - - result.candidates.push_back(candidate); - } - - std::vector parsedParticipants; - for (OngoingGroupCallParticipantDescription *participant in participants) { - tgcalls::GroupParticipantDescription parsedParticipant; - parsedParticipant.audioSsrc = participant.audioSsrc; - - if (participant.jsonParams.length != 0) { - [self parseJsonIntoParticipant:participant.jsonParams participant:parsedParticipant]; - } - parsedParticipants.push_back(parsedParticipant); - } - +- (void)setJoinResponsePayload:(NSString * _Nonnull)payload { if (_instance) { - _instance->setJoinResponsePayload(result, std::move(parsedParticipants)); + _instance->setJoinResponsePayload(payload.UTF8String); } } @@ -1248,152 +1533,9 @@ static void processJoinPayload(tgcalls::GroupJoinPayload &payload, void (^ _Nonn } } -- (void)parseJsonIntoParticipant:(NSString *)payload participant:(tgcalls::GroupParticipantDescription &)participant { - NSData *payloadData = [payload dataUsingEncoding:NSUTF8StringEncoding]; - if (payloadData == nil) { - return; - } - - NSDictionary *dict = [NSJSONSerialization JSONObjectWithData:payloadData options:0 error:nil]; - if (![dict isKindOfClass:[NSDictionary class]]) { - return; - } - - NSString *endpointId = dict[@"endpoint"]; - if (![endpointId isKindOfClass:[NSString class]]) { - return; - } - - participant.endpointId = [endpointId UTF8String]; - - NSArray *ssrcGroups = dict[@"ssrc-groups"]; - if ([ssrcGroups isKindOfClass:[NSArray class]]) { - for (NSDictionary *group in ssrcGroups) { - if (![group isKindOfClass:[NSDictionary class]]) { - continue; - } - NSString *semantics = group[@"semantics"]; - if (![semantics isKindOfClass:[NSString class]]) { - continue; - } - NSArray *sources = group[@"sources"]; - if (![sources isKindOfClass:[NSArray class]]) { - continue; - } - tgcalls::GroupJoinPayloadVideoSourceGroup groupDesc; - for (NSNumber *nSsrc in sources) { - if ([nSsrc isKindOfClass:[NSNumber class]]) { - groupDesc.ssrcs.push_back([nSsrc unsignedIntValue]); - } - } - groupDesc.semantics = [semantics UTF8String]; - participant.videoSourceGroups.push_back(groupDesc); - } - } - - NSArray *hdrExts = dict[@"rtp-hdrexts"]; - if ([hdrExts isKindOfClass:[NSArray class]]) { - for (NSDictionary *extDict in hdrExts) { - if (![extDict isKindOfClass:[NSDictionary class]]) { - continue; - } - NSNumber *nId = extDict[@"id"]; - if (![nId isKindOfClass:[NSNumber class]]) { - continue; - } - NSString *uri = extDict[@"uri"]; - if (![uri isKindOfClass:[NSString class]]) { - continue; - } - participant.videoExtensionMap.push_back(std::make_pair((uint32_t)[nId unsignedIntValue], (std::string)[uri UTF8String])); - } - } - - NSArray *payloadTypes = dict[@"payload-types"]; - if ([payloadTypes isKindOfClass:[NSArray class]]) { - for (NSDictionary *payloadDict in payloadTypes) { - if (![payloadDict isKindOfClass:[NSDictionary class]]) { - continue; - } - NSNumber *nId = payloadDict[@"id"]; - if (![nId isKindOfClass:[NSNumber class]]) { - continue; - } - NSNumber *nClockrate = payloadDict[@"clockrate"]; - if (nClockrate != nil && ![nClockrate isKindOfClass:[NSNumber class]]) { - continue; - } - NSNumber *nChannels = payloadDict[@"channels"]; - if (nChannels != nil && ![nChannels isKindOfClass:[NSNumber class]]) { - continue; - } - NSString *name = payloadDict[@"name"]; - if (![name isKindOfClass:[NSString class]]) { - continue; - } - - tgcalls::GroupJoinPayloadVideoPayloadType parsedPayload; - parsedPayload.id = [nId unsignedIntValue]; - parsedPayload.clockrate = [nClockrate unsignedIntValue]; - parsedPayload.channels = [nChannels unsignedIntValue]; - parsedPayload.name = [name UTF8String]; - - NSArray *fbs = payloadDict[@"rtcp-fbs"]; - if ([fbs isKindOfClass:[NSArray class]]) { - for (NSDictionary *fbDict in fbs) { - if (![fbDict isKindOfClass:[NSDictionary class]]) { - continue; - } - NSString *type = fbDict[@"type"]; - if (![type isKindOfClass:[NSString class]]) { - continue; - } - - NSString *subtype = fbDict[@"subtype"]; - if (subtype != nil && ![subtype isKindOfClass:[NSString class]]) { - continue; - } - - tgcalls::GroupJoinPayloadVideoPayloadFeedbackType parsedFeedback; - parsedFeedback.type = [type UTF8String]; - if (subtype != nil) { - parsedFeedback.subtype = [subtype UTF8String]; - } - parsedPayload.feedbackTypes.push_back(parsedFeedback); - } - } - - NSDictionary *parameters = payloadDict[@"parameters"]; - if ([parameters isKindOfClass:[NSDictionary class]]) { - for (NSString *nKey in parameters) { - if (![nKey isKindOfClass:[NSString class]]) { - continue; - } - NSString *value = parameters[nKey]; - if (![value isKindOfClass:[NSString class]]) { - continue; - } - parsedPayload.parameters.push_back(std::make_pair((std::string)[nKey UTF8String], (std::string)[value UTF8String])); - } - } - participant.videoPayloadTypes.push_back(parsedPayload); - } - } -} - -- (void)addParticipants:(NSArray * _Nonnull)participants { +- (void)removeIncomingVideoSource:(uint32_t)ssrc { if (_instance) { - std::vector parsedParticipants; - for (OngoingGroupCallParticipantDescription *participant in participants) { - tgcalls::GroupParticipantDescription parsedParticipant; - parsedParticipant.audioSsrc = participant.audioSsrc; - - if (participant.jsonParams.length != 0) { - [self parseJsonIntoParticipant:participant.jsonParams participant:parsedParticipant]; - } - parsedParticipants.push_back(parsedParticipant); - } - _instance->addParticipants(std::move(parsedParticipants)); + _instance->removeIncomingVideoSource(ssrc); } } @@ -1403,19 +1545,21 @@ static void processJoinPayload(tgcalls::GroupJoinPayload &payload, void (^ _Nonn } } +- (void)setIsNoiseSuppressionEnabled:(bool)isNoiseSuppressionEnabled { + if (_instance) { + _instance->setIsNoiseSuppressionEnabled(isNoiseSuppressionEnabled); + } +} + - (void)requestVideo:(OngoingCallThreadLocalContextVideoCapturer * _Nullable)videoCapturer completion:(void (^ _Nonnull)(NSString * _Nonnull, uint32_t))completion { if (_instance) { - _instance->setVideoCapture([videoCapturer getInterface], [completion](auto payload){ - processJoinPayload(payload, completion); - }); + _instance->setVideoCapture([videoCapturer getInterface]); } } - (void)disableVideo:(void (^ _Nonnull)(NSString * _Nonnull, uint32_t))completion { if (_instance) { - _instance->setVideoCapture(nullptr, [completion](auto payload){ - processJoinPayload(payload, completion); - }); + _instance->setVideoCapture(nullptr); } } @@ -1425,9 +1569,58 @@ static void processJoinPayload(tgcalls::GroupJoinPayload &payload, void (^ _Nonn } } -- (void)setFullSizeVideoSsrc:(uint32_t)ssrc { +- (void)setRequestedVideoChannels:(NSArray * _Nonnull)requestedVideoChannels { if (_instance) { - _instance->setFullSizeVideoSsrc(ssrc); + std::vector mappedChannels; + for (OngoingGroupCallRequestedVideoChannel *channel : requestedVideoChannels) { + tgcalls::VideoChannelDescription description; + description.audioSsrc = channel.audioSsrc; + description.endpointId = channel.endpointId.UTF8String ?: ""; + for (OngoingGroupCallSsrcGroup *group in channel.ssrcGroups) { + tgcalls::MediaSsrcGroup parsedGroup; + parsedGroup.semantics = group.semantics.UTF8String ?: ""; + for (NSNumber *ssrc in group.ssrcs) { + parsedGroup.ssrcs.push_back([ssrc unsignedIntValue]); + } + description.ssrcGroups.push_back(std::move(parsedGroup)); + } + switch (channel.minQuality) { + case OngoingGroupCallRequestedVideoQualityThumbnail: { + description.minQuality = tgcalls::VideoChannelDescription::Quality::Thumbnail; + break; + } + case OngoingGroupCallRequestedVideoQualityMedium: { + description.minQuality = tgcalls::VideoChannelDescription::Quality::Medium; + break; + } + case OngoingGroupCallRequestedVideoQualityFull: { + description.minQuality = tgcalls::VideoChannelDescription::Quality::Full; + break; + } + default: { + break; + } + } + switch (channel.maxQuality) { + case OngoingGroupCallRequestedVideoQualityThumbnail: { + description.maxQuality = tgcalls::VideoChannelDescription::Quality::Thumbnail; + break; + } + case OngoingGroupCallRequestedVideoQualityMedium: { + description.maxQuality = tgcalls::VideoChannelDescription::Quality::Medium; + break; + } + case OngoingGroupCallRequestedVideoQualityFull: { + description.maxQuality = tgcalls::VideoChannelDescription::Quality::Full; + break; + } + default: { + break; + } + } + mappedChannels.push_back(std::move(description)); + } + _instance->setRequestedVideoChannels(std::move(mappedChannels)); } } @@ -1442,29 +1635,70 @@ static void processJoinPayload(tgcalls::GroupJoinPayload &payload, void (^ _Nonn } } -- (void)makeIncomingVideoViewWithSsrc:(uint32_t)ssrc completion:(void (^_Nonnull)(UIView * _Nullable))completion { +- (void)makeIncomingVideoViewWithEndpointId:(NSString * _Nonnull)endpointId requestClone:(bool)requestClone completion:(void (^_Nonnull)(UIView * _Nullable, UIView * _Nullable))completion { if (_instance) { __weak GroupCallThreadLocalContext *weakSelf = self; id queue = _queue; dispatch_async(dispatch_get_main_queue(), ^{ - if ([VideoMetalView isSupported]) { + BOOL useSampleBuffer = NO; +#ifdef WEBRTC_IOS + useSampleBuffer = YES; +#endif + if (useSampleBuffer) { + VideoSampleBufferView *remoteRenderer = [[VideoSampleBufferView alloc] initWithFrame:CGRectZero]; + remoteRenderer.videoContentMode = UIViewContentModeScaleAspectFill; + + std::shared_ptr> sink = [remoteRenderer getSink]; + + VideoSampleBufferView *cloneRenderer = nil; + if (requestClone) { + cloneRenderer = [[VideoSampleBufferView alloc] initWithFrame:CGRectZero]; + cloneRenderer.videoContentMode = UIViewContentModeScaleAspectFill; +#ifdef WEBRTC_IOS + [remoteRenderer setCloneTarget:cloneRenderer]; +#endif + } + + [queue dispatch:^{ + __strong GroupCallThreadLocalContext *strongSelf = weakSelf; + if (strongSelf && strongSelf->_instance) { + strongSelf->_instance->addIncomingVideoOutput(endpointId.UTF8String, sink); + } + }]; + + completion(remoteRenderer, cloneRenderer); + } else if ([VideoMetalView isSupported]) { VideoMetalView *remoteRenderer = [[VideoMetalView alloc] initWithFrame:CGRectZero]; -#if TARGET_OS_IPHONE +#ifdef WEBRTC_IOS remoteRenderer.videoContentMode = UIViewContentModeScaleToFill; #else - remoteRenderer.videoContentMode = UIViewContentModeScaleAspect; + remoteRenderer.videoContentMode = kCAGravityResizeAspectFill; #endif + + VideoMetalView *cloneRenderer = nil; + if (requestClone) { + cloneRenderer = [[VideoMetalView alloc] initWithFrame:CGRectZero]; +#ifdef WEBRTC_IOS + cloneRenderer.videoContentMode = UIViewContentModeScaleToFill; +#else + cloneRenderer.videoContentMode = kCAGravityResizeAspectFill; +#endif + } std::shared_ptr> sink = [remoteRenderer getSink]; + std::shared_ptr> cloneSink = [cloneRenderer getSink]; [queue dispatch:^{ __strong GroupCallThreadLocalContext *strongSelf = weakSelf; if (strongSelf && strongSelf->_instance) { - strongSelf->_instance->addIncomingVideoOutput(ssrc, sink); + strongSelf->_instance->addIncomingVideoOutput(endpointId.UTF8String, sink); + if (cloneSink) { + strongSelf->_instance->addIncomingVideoOutput(endpointId.UTF8String, cloneSink); + } } }]; - completion(remoteRenderer); + completion(remoteRenderer, cloneRenderer); } else { GLVideoView *remoteRenderer = [[GLVideoView alloc] initWithFrame:CGRectZero]; // [remoteRenderer setVideoContentMode:kCAGravityResizeAspectFill]; @@ -1473,25 +1707,62 @@ static void processJoinPayload(tgcalls::GroupJoinPayload &payload, void (^ _Nonn [queue dispatch:^{ __strong GroupCallThreadLocalContext *strongSelf = weakSelf; if (strongSelf && strongSelf->_instance) { - strongSelf->_instance->addIncomingVideoOutput(ssrc, sink); + strongSelf->_instance->addIncomingVideoOutput(endpointId.UTF8String, sink); } }]; - completion(remoteRenderer); + completion(remoteRenderer, nil); } }); } } +- (GroupCallDisposable * _Nonnull)addVideoOutputWithEndpointId:(NSString * _Nonnull)endpointId sink:(void (^_Nonnull)(CallVideoFrameData * _Nonnull))sink { + int sinkId = _nextSinkId; + _nextSinkId += 1; + + GroupCallVideoSink *storedSink = [[GroupCallVideoSink alloc] initWithSink:sink]; + _sinks[@(sinkId)] = storedSink; + + if (_instance) { + _instance->addIncomingVideoOutput(endpointId.UTF8String, [storedSink sink]); + } + + __weak GroupCallThreadLocalContext *weakSelf = self; + id queue = _queue; + return [[GroupCallDisposable alloc] initWithBlock:^{ + [queue dispatch:^{ + __strong GroupCallThreadLocalContext *strongSelf = weakSelf; + if (!strongSelf) { + return; + } + + [strongSelf->_sinks removeObjectForKey:@(sinkId)]; + }]; + }]; +} + +- (void)addExternalAudioData:(NSData * _Nonnull)data { + if (_instance) { + std::vector samples; + samples.resize(data.length); + [data getBytes:samples.data() length:data.length]; + _instance->addExternalAudioSamples(std::move(samples)); + } +} + @end -@implementation OngoingGroupCallParticipantDescription +@implementation OngoingGroupCallMediaChannelDescription -- (instancetype _Nonnull)initWithAudioSsrc:(uint32_t)audioSsrc jsonParams:(NSString * _Nullable)jsonParams { +- (instancetype _Nonnull)initWithType:(OngoingGroupCallMediaChannelType)type + audioSsrc:(uint32_t)audioSsrc + videoDescription:(NSString * _Nullable)videoDescription { self = [super init]; if (self != nil) { + _type = type; _audioSsrc = audioSsrc; - _jsonParams = jsonParams; + _videoDescription = videoDescription; } return self; } @@ -1512,3 +1783,32 @@ static void processJoinPayload(tgcalls::GroupJoinPayload &payload, void (^ _Nonn } @end + +@implementation OngoingGroupCallSsrcGroup + +- (instancetype)initWithSemantics:(NSString * _Nonnull)semantics ssrcs:(NSArray * _Nonnull)ssrcs { + self = [super init]; + if (self != nil) { + _semantics = semantics; + _ssrcs = ssrcs; + } + return self; +} + +@end + +@implementation OngoingGroupCallRequestedVideoChannel + +- (instancetype)initWithAudioSsrc:(uint32_t)audioSsrc endpointId:(NSString * _Nonnull)endpointId ssrcGroups:(NSArray * _Nonnull)ssrcGroups minQuality:(OngoingGroupCallRequestedVideoQuality)minQuality maxQuality:(OngoingGroupCallRequestedVideoQuality)maxQuality { + self = [super init]; + if (self != nil) { + _audioSsrc = audioSsrc; + _endpointId = endpointId; + _ssrcGroups = ssrcGroups; + _minQuality = minQuality; + _maxQuality = maxQuality; + } + return self; +} + +@end diff --git a/submodules/TgVoipWebrtc/tgcalls b/submodules/TgVoipWebrtc/tgcalls index 1c16a122c5..ef79634980 160000 --- a/submodules/TgVoipWebrtc/tgcalls +++ b/submodules/TgVoipWebrtc/tgcalls @@ -1 +1 @@ -Subproject commit 1c16a122c5cad736c4545fd1e37539adc4c4cb52 +Subproject commit ef796349808b187b80b75ea1876b940f2882fcbb diff --git a/submodules/TooltipUI/Sources/TooltipScreen.swift b/submodules/TooltipUI/Sources/TooltipScreen.swift index fab46460a7..49b47ecb42 100644 --- a/submodules/TooltipUI/Sources/TooltipScreen.swift +++ b/submodules/TooltipUI/Sources/TooltipScreen.swift @@ -44,6 +44,8 @@ private final class TooltipScreenNode: ViewControllerTracingNode { private let backgroundContainerNode: ASDisplayNode private let backgroundNode: ASImageNode private var effectView: UIView? + private var gradientNode: ASDisplayNode? + private var arrowGradientNode: ASDisplayNode? private let arrowNode: ASImageNode private let arrowContainer: ASDisplayNode private var arrowEffectView: UIView? @@ -121,7 +123,7 @@ private final class TooltipScreenNode: ViewControllerTracingNode { self.arrowContainer = ASDisplayNode() let fontSize: CGFloat - if style == .light { + if case .light = style { self.effectView = UIVisualEffectView(effect: UIBlurEffect(style: .light)) self.backgroundContainerNode.clipsToBounds = true self.backgroundContainerNode.cornerRadius = 14.0 @@ -133,6 +135,38 @@ private final class TooltipScreenNode: ViewControllerTracingNode { self.arrowEffectView = UIVisualEffectView(effect: UIBlurEffect(style: .light)) self.arrowContainer.view.addSubview(self.arrowEffectView!) + let maskLayer = CAShapeLayer() + if let path = try? svgPath("M85.882251,0 C79.5170552,0 73.4125613,2.52817247 68.9116882,7.02834833 L51.4264069,24.5109211 C46.7401154,29.1964866 39.1421356,29.1964866 34.4558441,24.5109211 L16.9705627,7.02834833 C12.4696897,2.52817247 6.36519576,0 0,0 L85.882251,0 ", scale: CGPoint(x: 0.333333, y: 0.333333), offset: CGPoint()) { + maskLayer.path = path.cgPath + } + maskLayer.frame = CGRect(origin: CGPoint(), size: arrowSize) + self.arrowContainer.layer.mask = maskLayer + } else if case let .gradient(leftColor, rightColor) = style { + self.gradientNode = ASDisplayNode() + self.gradientNode?.setLayerBlock({ + let layer = CAGradientLayer() + layer.colors = [leftColor.cgColor, rightColor.cgColor] + layer.startPoint = CGPoint() + layer.endPoint = CGPoint(x: 1.0, y: 0.0) + return layer + }) + self.arrowGradientNode = ASDisplayNode() + self.arrowGradientNode?.setLayerBlock({ + let layer = CAGradientLayer() + layer.colors = [leftColor.cgColor, rightColor.cgColor] + layer.startPoint = CGPoint() + layer.endPoint = CGPoint(x: 1.0, y: 0.0) + return layer + }) + self.backgroundContainerNode.clipsToBounds = true + self.backgroundContainerNode.cornerRadius = 14.0 + if #available(iOS 13.0, *) { + self.backgroundContainerNode.layer.cornerCurve = .continuous + } + fontSize = 17.0 + + self.arrowContainer.addSubnode(self.arrowGradientNode!) + let maskLayer = CAShapeLayer() if let path = try? svgPath("M85.882251,0 C79.5170552,0 73.4125613,2.52817247 68.9116882,7.02834833 L51.4264069,24.5109211 C46.7401154,29.1964866 39.1421356,29.1964866 34.4558441,24.5109211 L16.9705627,7.02834833 C12.4696897,2.52817247 6.36519576,0 0,0 L85.882251,0 ", scale: CGPoint(x: 0.333333, y: 0.333333), offset: CGPoint()) { maskLayer.path = path.cgPath @@ -178,7 +212,12 @@ private final class TooltipScreenNode: ViewControllerTracingNode { self.containerNode.addSubnode(self.backgroundContainerNode) self.arrowContainer.addSubnode(self.arrowNode) self.backgroundNode.addSubnode(self.arrowContainer) - if let effectView = self.effectView { + if let gradientNode = self.gradientNode { + self.backgroundContainerNode.addSubnode(gradientNode) + self.containerNode.addSubnode(self.arrowContainer) + self.arrowNode.removeFromSupernode() + } + else if let effectView = self.effectView { self.backgroundContainerNode.view.addSubview(effectView) if let _ = self.arrowEffectView { self.containerNode.addSubnode(self.arrowContainer) @@ -259,7 +298,7 @@ private final class TooltipScreenNode: ViewControllerTracingNode { let sideInset: CGFloat = 13.0 + layout.safeInsets.left let bottomInset: CGFloat = 10.0 - let contentInset: CGFloat = 9.0 + let contentInset: CGFloat = 11.0 let contentVerticalInset: CGFloat = 11.0 let animationSize: CGSize let animationInset: CGFloat @@ -288,7 +327,7 @@ private final class TooltipScreenNode: ViewControllerTracingNode { let backgroundHeight: CGFloat switch self.tooltipStyle { - case .default: + case .default, .gradient: backgroundHeight = max(animationSize.height, textSize.height) + contentVerticalInset * 2.0 case .light: backgroundHeight = max(28.0, max(animationSize.height, textSize.height) + 4.0 * 2.0) @@ -331,6 +370,9 @@ private final class TooltipScreenNode: ViewControllerTracingNode { if let effectView = self.effectView { transition.updateFrame(view: effectView, frame: CGRect(origin: CGPoint(), size: backgroundFrame.size)) } + if let gradientNode = self.gradientNode { + transition.updateFrame(node: gradientNode, frame: CGRect(origin: CGPoint(), size: backgroundFrame.size)) + } if let image = self.arrowNode.image, case let .point(rect, arrowPosition) = self.location { let arrowSize = image.size let arrowCenterX = rect.midX @@ -348,8 +390,10 @@ private final class TooltipScreenNode: ViewControllerTracingNode { transition.updateFrame(node: self.arrowContainer, frame: arrowFrame.offsetBy(dx: -backgroundFrame.minX, dy: 0.0)) - self.arrowNode.frame = CGRect(origin: CGPoint(), size: arrowSize) - self.arrowEffectView?.frame = CGRect(origin: CGPoint(), size: arrowSize) + let arrowBounds = CGRect(origin: CGPoint(), size: arrowSize) + self.arrowNode.frame = arrowBounds + self.arrowEffectView?.frame = arrowBounds + self.arrowGradientNode?.frame = CGRect(origin: CGPoint(x: -arrowFrame.minX + backgroundFrame.minX, y: 0.0), size: backgroundFrame.size) case .right: arrowFrame = CGRect(origin: CGPoint(x: backgroundFrame.width + arrowSize.height, y: rect.midY), size: CGSize(width: arrowSize.height, height: arrowSize.width)) @@ -357,8 +401,10 @@ private final class TooltipScreenNode: ViewControllerTracingNode { transition.updateFrame(node: self.arrowContainer, frame: arrowFrame.offsetBy(dx: 0.0, dy: -backgroundFrame.minY - floorToScreenPixels((backgroundFrame.height - arrowSize.width) / 2.0))) - self.arrowNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -0.5), size: arrowSize) - self.arrowEffectView?.frame = CGRect(origin: CGPoint(x: 0.0, y: -0.5), size: arrowSize) + let arrowBounds = CGRect(origin: CGPoint(x: 0.0, y: -0.5), size: arrowSize) + self.arrowNode.frame = arrowBounds + self.arrowEffectView?.frame = arrowBounds + self.arrowGradientNode?.frame = arrowBounds } } else { self.arrowNode.isHidden = true @@ -508,6 +554,7 @@ public final class TooltipScreen: ViewController { public enum Style { case `default` case light + case gradient(UIColor, UIColor) } public let text: String diff --git a/submodules/UIKitRuntimeUtils/Source/UIKitRuntimeUtils/UIKitUtils.m b/submodules/UIKitRuntimeUtils/Source/UIKitRuntimeUtils/UIKitUtils.m index 76eac05f37..0b33273d6b 100644 --- a/submodules/UIKitRuntimeUtils/Source/UIKitRuntimeUtils/UIKitUtils.m +++ b/submodules/UIKitRuntimeUtils/Source/UIKitRuntimeUtils/UIKitUtils.m @@ -84,38 +84,10 @@ CGFloat springAnimationValueAtImpl(CABasicAnimation * _Nonnull animation, CGFloa @interface CustomBlurEffect : UIBlurEffect -/*@property (nonatomic) double blurRadius; -@property (nonatomic) double colorBurnTintAlpha; -@property (nonatomic) double colorBurnTintLevel; -@property (nonatomic, retain) UIColor *colorTint; -@property (nonatomic) double colorTintAlpha; -@property (nonatomic) bool darkenWithSourceOver; -@property (nonatomic) double darkeningTintAlpha; -@property (nonatomic) double darkeningTintHue; -@property (nonatomic) double darkeningTintSaturation; -@property (nonatomic) double grayscaleTintAlpha; -@property (nonatomic) double grayscaleTintLevel; -@property (nonatomic) bool lightenGrayscaleWithSourceOver; -@property (nonatomic) double saturationDeltaFactor; -@property (nonatomic) double scale; -@property (nonatomic) double zoom;*/ - + (id)effectWithStyle:(long long)arg1; @end -static NSString *encodeText(NSString *string, int key) { - NSMutableString *result = [[NSMutableString alloc] init]; - - for (int i = 0; i < (int)[string length]; i++) { - unichar c = [string characterAtIndex:i]; - c += key; - [result appendString:[NSString stringWithCharacters:&c length:1]]; - } - - return result; -} - static void setField(CustomBlurEffect *object, NSString *name, double value) { SEL selector = NSSelectorFromString(name); NSMethodSignature *signature = [[object class] instanceMethodSignatureForSelector:selector]; @@ -145,7 +117,7 @@ static void setNilField(CustomBlurEffect *object, NSString *name) { [inv invoke]; } -static void setBoolField(CustomBlurEffect *object, NSString *name, BOOL value) { +static void setBoolField(NSObject *object, NSString *name, BOOL value) { SEL selector = NSSelectorFromString(name); NSMethodSignature *signature = [[object class] instanceMethodSignatureForSelector:selector]; if (signature == nil) { @@ -170,18 +142,17 @@ UIBlurEffect *makeCustomZoomBlurEffectImpl(bool isLight) { NSString *string = [@[@"_", @"UI", @"Custom", @"BlurEffect"] componentsJoinedByString:@""]; CustomBlurEffect *result = (CustomBlurEffect *)[NSClassFromString(string) effectWithStyle:0]; - setField(result, encodeText(@"tfuCmvsSbejvt;", -1), 10.0); - //setField(result, encodeText(@"tfu[ppn;", -1), 0.015); - setNilField(result, encodeText(@"tfuDpmpsUjou;", -1)); - setField(result, encodeText(@"tfuDpmpsUjouBmqib;", -1), 0.0); - setField(result, encodeText(@"tfuEbslfojohUjouBmqib;", -1), 0.0); - setField(result, encodeText(@"tfuHsbztdbmfUjouBmqib;", -1), 0.0); - setField(result, encodeText(@"tfuTbuvsbujpoEfmubGbdups;", -1), 1.0); + setField(result, [@[@"set", @"BlurRadius", @":"] componentsJoinedByString:@""], 10.0); + setNilField(result, [@[@"set", @"Color", @"Tint", @":"] componentsJoinedByString:@""]); + setField(result, [@[@"set", @"Color", @"Tint", @"Alpha", @":"] componentsJoinedByString:@""], 0.0); + setField(result, [@[@"set", @"Darkening", @"Tint", @"Alpha", @":"] componentsJoinedByString:@""], 0.0); + setField(result, [@[@"set", @"Grayscale", @"Tint", @"Alpha", @":"] componentsJoinedByString:@""], 0.0); + setField(result, [@[@"set", @"Saturation", @"Delta", @"Factor", @":"] componentsJoinedByString:@""], 1.0); if ([UIScreen mainScreen].scale > 2.5f) { - setField(result, encodeText(@"setScale:", 0), 0.3); + setField(result, @"setScale:", 0.3); } else { - setField(result, encodeText(@"setScale:", 0), 0.5); + setField(result, @"setScale:", 0.5); } return result; @@ -191,7 +162,9 @@ UIBlurEffect *makeCustomZoomBlurEffectImpl(bool isLight) { } void applySmoothRoundedCornersImpl(CALayer * _Nonnull layer) { - if (@available(iOS 11.0, *)) { - setBoolField(layer, encodeText(@"tfuDpoujovpvtDpsofst;", -1), true); + if (@available(iOS 13.0, *)) { + layer.cornerCurve = kCACornerCurveContinuous; + } else { + setBoolField(layer, [@[@"set", @"Continuous", @"Corners", @":"] componentsJoinedByString:@""], true); } } diff --git a/submodules/UIKitRuntimeUtils/Source/UIKitRuntimeUtils/UIViewController+Navigation.h b/submodules/UIKitRuntimeUtils/Source/UIKitRuntimeUtils/UIViewController+Navigation.h index ac34185f2b..e34181f313 100644 --- a/submodules/UIKitRuntimeUtils/Source/UIKitRuntimeUtils/UIViewController+Navigation.h +++ b/submodules/UIKitRuntimeUtils/Source/UIKitRuntimeUtils/UIViewController+Navigation.h @@ -23,7 +23,7 @@ typedef NS_OPTIONS(NSUInteger, UIResponderDisableAutomaticKeyboardHandling) { @property (nonatomic) bool disablesInteractiveTransitionGestureRecognizer; @property (nonatomic) bool disablesInteractiveKeyboardGestureRecognizer; @property (nonatomic) bool disablesInteractiveModalDismiss; -@property (nonatomic, copy) bool (^ disablesInteractiveTransitionGestureRecognizerNow)(); +@property (nonatomic, copy) bool (^ _Nullable disablesInteractiveTransitionGestureRecognizerNow)(); @property (nonatomic) UIResponderDisableAutomaticKeyboardHandling disableAutomaticKeyboardHandling; @@ -34,10 +34,24 @@ typedef NS_OPTIONS(NSUInteger, UIResponderDisableAutomaticKeyboardHandling) { @end -void applyKeyboardAutocorrection(); +void applyKeyboardAutocorrection(UITextView * _Nonnull textView); @interface AboveStatusBarWindow : UIWindow @property (nonatomic, copy) UIInterfaceOrientationMask (^ _Nullable supportedOrientations)(void); @end + +/*@interface _UIPortalView : UIView + +- (void)setSourceView:(UIView * _Nullable)sourceView; +- (bool)hidesSourceView; +- (void)setHidesSourceView:(bool)arg1; +- (void)setMatchesAlpha:(bool)arg1; +- (void)setMatchesPosition:(bool)arg1; +- (void)setMatchesTransform:(bool)arg1; +- (bool)matchesTransform; +- (bool)matchesPosition; +- (bool)matchesAlpha; + +@end*/ diff --git a/submodules/UIKitRuntimeUtils/Source/UIKitRuntimeUtils/UIViewController+Navigation.m b/submodules/UIKitRuntimeUtils/Source/UIKitRuntimeUtils/UIViewController+Navigation.m index a330e45585..4000be50b1 100644 --- a/submodules/UIKitRuntimeUtils/Source/UIKitRuntimeUtils/UIViewController+Navigation.m +++ b/submodules/UIKitRuntimeUtils/Source/UIKitRuntimeUtils/UIViewController+Navigation.m @@ -316,28 +316,15 @@ static NSString *TGEncodeText(NSString *string, int key) return result; } -void applyKeyboardAutocorrection() { - static Class keyboardClass = NULL; - static SEL currentInstanceSelector = NULL; - static SEL applyVariantSelector = NULL; - - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - keyboardClass = NSClassFromString(TGEncodeText(@"VJLfzcpbse", -1)); - - currentInstanceSelector = NSSelectorFromString(TGEncodeText(@"bdujwfLfzcpbse", -1)); - applyVariantSelector = NSSelectorFromString(TGEncodeText(@"bddfquBvupdpssfdujpo", -1)); - }); - - if ([keyboardClass respondsToSelector:currentInstanceSelector]) - { -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Warc-performSelector-leaks" - id currentInstance = [keyboardClass performSelector:currentInstanceSelector]; - if ([currentInstance respondsToSelector:applyVariantSelector]) - [currentInstance performSelector:applyVariantSelector]; -#pragma clang diagnostic pop +void applyKeyboardAutocorrection(UITextView * _Nonnull textView) { + NSRange rangeCopy = textView.selectedRange; + NSRange fakeRange = rangeCopy; + if (fakeRange.location != 0) { + fakeRange.location--; } + [textView unmarkText]; + [textView setSelectedRange:fakeRange]; + [textView setSelectedRange:rangeCopy]; } @interface AboveStatusBarWindowController : UIViewController diff --git a/submodules/UndoUI/Sources/UndoOverlayController.swift b/submodules/UndoUI/Sources/UndoOverlayController.swift index ab34091394..124e6d17a8 100644 --- a/submodules/UndoUI/Sources/UndoOverlayController.swift +++ b/submodules/UndoUI/Sources/UndoOverlayController.swift @@ -17,8 +17,8 @@ public enum UndoOverlayContent { 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) - case dice(dice: TelegramMediaDice, account: Account, text: String, action: String?) + case stickersModified(title: String, text: String, undo: Bool, info: StickerPackCollectionInfo, topItem: ItemCollectionItem?, context: AccountContext) + case dice(dice: TelegramMediaDice, context: AccountContext, text: String, action: String?) case chatAddedToFolder(chatTitle: String, folderTitle: String) case chatRemovedFromFolder(chatTitle: String, folderTitle: String) case messagesUnpinned(title: String, text: String, undo: Bool, isHidden: Bool) @@ -35,8 +35,10 @@ public enum UndoOverlayContent { case voiceChatRecording(text: String) case voiceChatFlag(text: String) case voiceChatCanSpeak(text: String) - case sticker(account: Account, file: TelegramMediaFile, text: String) + case sticker(context: AccountContext, file: TelegramMediaFile, text: String) case copy(text: String) + case mediaSaved(text: String) + case paymentSent(currencyValue: String, itemTitle: String) } public enum UndoOverlayAction { @@ -55,6 +57,8 @@ public final class UndoOverlayController: ViewController { private var didPlayPresentationAnimation = false private var dismissed = false + public var keepOnParentDismissal = false + public init(presentationData: PresentationData, content: UndoOverlayContent, elevatedLayout: Bool, animateInAsReplacement: Bool = false, action: @escaping (UndoOverlayAction) -> Bool) { self.presentationData = presentationData self.content = content diff --git a/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift b/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift index 3c9459503a..eca7b1c5a6 100644 --- a/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift +++ b/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift @@ -17,6 +17,7 @@ import SlotMachineAnimationNode import AnimationUI import StickerResources import AvatarNode +import AccountContext final class UndoOverlayControllerNode: ViewControllerTracingNode { private let elevatedLayout: Bool @@ -265,6 +266,23 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { string.addAttribute(.font, value: Font.regular(14.0), range: range) } + self.textNode.attributedText = string + displayUndo = false + self.originalRemainingSeconds = 5 + case let .paymentSent(currencyValue, itemTitle): + self.avatarNode = nil + self.iconNode = nil + self.iconCheckNode = nil + self.animationNode = AnimationNode(animation: "anim_payment", colors: ["info1.info1.stroke": self.animationBackgroundColor, "info2.info2.Fill": self.animationBackgroundColor], scale: 1.0) + self.animatedStickerNode = nil + + let (rawString, attributes) = presentationData.strings.Checkout_SuccessfulTooltip(currencyValue, itemTitle) + + let string = NSMutableAttributedString(attributedString: NSAttributedString(string: rawString, font: Font.regular(14.0), textColor: .white)) + for (_, range) in attributes { + string.addAttribute(.font, value: Font.semibold(14.0), range: range) + } + self.textNode.attributedText = string displayUndo = false self.originalRemainingSeconds = 5 @@ -313,7 +331,7 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { self.textNode.maximumNumberOfLines = 2 displayUndo = false self.originalRemainingSeconds = 5 - case let .stickersModified(title, text, undo, info, topItem, account): + case let .stickersModified(title, text, undo, info, topItem, context): self.avatarNode = nil self.iconNode = nil self.iconCheckNode = nil @@ -344,7 +362,7 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { 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, progressiveSizes: [])) + thumbnailItem = .still(TelegramMediaImageRepresentation(dimensions: dimensions, resource: resource, progressiveSizes: [], immediateThumbnailData: nil)) resourceReference = MediaResourceReference.media(media: .standalone(media: item.file), resource: resource) } } @@ -360,14 +378,14 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { let stillImageSize = representation.dimensions.cgSize.aspectFitted(imageBoundingSize) self.stickerImageSize = stillImageSize - updatedImageSignal = chatMessageStickerPackThumbnail(postbox: account.postbox, resource: representation.resource) + updatedImageSignal = chatMessageStickerPackThumbnail(postbox: context.account.postbox, resource: representation.resource) case let .animated(resource): self.stickerImageSize = imageBoundingSize - updatedImageSignal = chatMessageStickerPackThumbnail(postbox: account.postbox, resource: resource, animated: true) + updatedImageSignal = chatMessageStickerPackThumbnail(postbox: context.account.postbox, resource: resource, animated: true) } if let resourceReference = resourceReference { - updatedFetchSignal = fetchedMediaResource(mediaBox: account.postbox.mediaBox, reference: resourceReference) + updatedFetchSignal = fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, reference: resourceReference) } } else { updatedImageSignal = .single({ _ in return nil }) @@ -398,10 +416,10 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { case let .animated(resource): let animatedStickerNode = AnimatedStickerNode() self.animatedStickerNode = animatedStickerNode - animatedStickerNode.setup(source: AnimatedStickerResourceSource(account: account, resource: resource), width: 80, height: 80, mode: .cached) + animatedStickerNode.setup(source: AnimatedStickerResourceSource(account: context.account, resource: resource), width: 80, height: 80, mode: .cached) } } - case let .dice(dice, account, text, action): + case let .dice(dice, context, text, action): self.avatarNode = nil self.iconNode = nil self.iconCheckNode = nil @@ -441,14 +459,14 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { let animatedStickerNode = AnimatedStickerNode() self.animatedStickerNode = animatedStickerNode - let _ = (loadedStickerPack(postbox: account.postbox, network: account.network, reference: .dice(dice.emoji), forceActualized: false) + let _ = (context.engine.stickers.loadedStickerPack(reference: .dice(dice.emoji), forceActualized: false) |> deliverOnMainQueue).start(next: { stickerPack in if let value = dice.value { switch stickerPack { case let .result(_, items, _): let item = items[Int(value)] if let item = item as? StickerPackItem { - animatedStickerNode.setup(source: AnimatedStickerResourceSource(account: account, resource: item.file.resource), width: 120, height: 120, playbackMode: .once, mode: .direct(cachePathPrefix: nil)) + animatedStickerNode.setup(source: AnimatedStickerResourceSource(account: context.account, resource: item.file.resource), width: 120, height: 120, playbackMode: .once, mode: .direct(cachePathPrefix: nil)) } default: break @@ -475,7 +493,7 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { displayUndo = false self.originalRemainingSeconds = 3 case let .invitedToVoiceChat(context, peer, text): - self.avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 16.0)) + self.avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 15.0)) self.iconNode = nil self.iconCheckNode = nil self.animationNode = nil @@ -596,7 +614,7 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { displayUndo = false self.originalRemainingSeconds = 3 - case let .sticker(account, file, text): + case let .sticker(context, file, text): self.avatarNode = nil self.iconNode = nil self.iconCheckNode = nil @@ -618,7 +636,7 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { thumbnailItem = .animated(file.resource) resourceReference = MediaResourceReference.media(media: .standalone(media: file), resource: file.resource) } else if let dimensions = file.dimensions, let resource = chatMessageStickerResource(file: file, small: true) as? TelegramMediaResource { - thumbnailItem = .still(TelegramMediaImageRepresentation(dimensions: dimensions, resource: resource, progressiveSizes: [])) + thumbnailItem = .still(TelegramMediaImageRepresentation(dimensions: dimensions, resource: resource, progressiveSizes: [], immediateThumbnailData: nil)) resourceReference = MediaResourceReference.media(media: .standalone(media: file), resource: resource) } @@ -633,14 +651,14 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { let stillImageSize = representation.dimensions.cgSize.aspectFitted(imageBoundingSize) self.stickerImageSize = stillImageSize - updatedImageSignal = chatMessageStickerPackThumbnail(postbox: account.postbox, resource: representation.resource) + updatedImageSignal = chatMessageStickerPackThumbnail(postbox: context.account.postbox, resource: representation.resource) case let .animated(resource): self.stickerImageSize = imageBoundingSize - updatedImageSignal = chatMessageStickerPackThumbnail(postbox: account.postbox, resource: resource, animated: true) + updatedImageSignal = chatMessageStickerPackThumbnail(postbox: context.account.postbox, resource: resource, animated: true) } if let resourceReference = resourceReference { - updatedFetchSignal = fetchedMediaResource(mediaBox: account.postbox.mediaBox, reference: resourceReference) + updatedFetchSignal = fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, reference: resourceReference) } } else { updatedImageSignal = .single({ _ in return nil }) @@ -671,7 +689,7 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { case let .animated(resource): let animatedStickerNode = AnimatedStickerNode() self.animatedStickerNode = animatedStickerNode - animatedStickerNode.setup(source: AnimatedStickerResourceSource(account: account, resource: resource), width: 80, height: 80, mode: .cached) + animatedStickerNode.setup(source: AnimatedStickerResourceSource(account: context.account, resource: resource), width: 80, height: 80, mode: .cached) } } case let .copy(text): @@ -687,6 +705,27 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { self.textNode.attributedText = attributedText self.textNode.maximumNumberOfLines = 2 + displayUndo = false + self.originalRemainingSeconds = 3 + case let .mediaSaved(text): + self.avatarNode = nil + self.iconNode = nil + self.iconCheckNode = nil + self.animationNode = nil + + let animatedStickerNode = AnimatedStickerNode() + self.animatedStickerNode = animatedStickerNode + if let path = getAppBundle().path(forResource: "anim_savemedia", ofType: "tgs") { + animatedStickerNode.setup(source: AnimatedStickerNodeLocalFileSource(path: path), width: 80, height: 80, playbackMode: .once, mode: .direct(cachePathPrefix: nil)) + animatedStickerNode.visibility = true + } + + 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 = false self.originalRemainingSeconds = 3 } @@ -717,7 +756,7 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { switch content { case .removedChat: self.panelWrapperNode.addSubnode(self.timerTextNode) - case .archivedChat, .hidArchive, .revealedArchive, .autoDelete, .succeed, .emoji, .swipeToReply, .actionSucceeded, .stickersModified, .chatAddedToFolder, .chatRemovedFromFolder, .messagesUnpinned, .setProximityAlert, .invitedToVoiceChat, .linkCopied, .banned, .importedMessage, .audioRate, .forward, .gigagroupConversion, .linkRevoked, .voiceChatRecording, .voiceChatFlag, .voiceChatCanSpeak, .sticker, .copy: + case .archivedChat, .hidArchive, .revealedArchive, .autoDelete, .succeed, .emoji, .swipeToReply, .actionSucceeded, .stickersModified, .chatAddedToFolder, .chatRemovedFromFolder, .messagesUnpinned, .setProximityAlert, .invitedToVoiceChat, .linkCopied, .banned, .importedMessage, .audioRate, .forward, .gigagroupConversion, .linkRevoked, .voiceChatRecording, .voiceChatFlag, .voiceChatCanSpeak, .sticker, .copy, .mediaSaved, .paymentSent: break case .dice: self.panelWrapperNode.clipsToBounds = true @@ -767,9 +806,8 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { override func didLoad() { super.didLoad() - if self.panelNode.backgroundColor == .clear { - self.panelNode.view.addSubview(self.effectView) - } + + self.panelNode.view.addSubview(self.effectView) } @objc private func buttonPressed() { @@ -843,6 +881,9 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { let factor: CGFloat = 0.07 verticalOffset = -3.0 preferredSize = CGSize(width: floor(iconSize.width * factor), height: floor(iconSize.height * factor)) + } else if case .paymentSent = self.content { + let factor: CGFloat = 0.08 + preferredSize = CGSize(width: floor(iconSize.width * factor), height: floor(iconSize.height * factor)) } else { preferredSize = iconSize } @@ -894,7 +935,7 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { 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)) + self.buttonNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: undoButtonFrame.minX, height: contentHeight)) var textContentHeight = textSize.height var textOffset: CGFloat = 0.0 diff --git a/submodules/UrlHandling/BUILD b/submodules/UrlHandling/BUILD index f4a6173414..fbece7a1ec 100644 --- a/submodules/UrlHandling/BUILD +++ b/submodules/UrlHandling/BUILD @@ -14,6 +14,7 @@ swift_library( "//submodules/MtProtoKit:MtProtoKit", "//submodules/AccountContext:AccountContext", "//submodules/TelegramUIPreferences:TelegramUIPreferences", + "//submodules/TelegramNotices:TelegramNotices", ], visibility = [ "//visibility:public", diff --git a/submodules/UrlHandling/Sources/UrlHandling.swift b/submodules/UrlHandling/Sources/UrlHandling.swift index 0f832ef5b3..7bd01c2d33 100644 --- a/submodules/UrlHandling/Sources/UrlHandling.swift +++ b/submodules/UrlHandling/Sources/UrlHandling.swift @@ -7,6 +7,7 @@ import SyncCore import MtProtoKit import TelegramPresentationData import TelegramUIPreferences +import TelegramNotices import AccountContext private let baseTelegramMePaths = ["telegram.me", "t.me", "telegram.dog"] @@ -15,7 +16,7 @@ private let baseTelegraPhPaths = ["telegra.ph/", "te.legra.ph/", "graph.org/", " public enum ParsedInternalPeerUrlParameter { case botStart(String) case groupBotStart(String) - case channelMessage(Int32) + case channelMessage(Int32, Double?) case replyThread(Int32, Int32) case voiceChat(String?) } @@ -23,7 +24,7 @@ public enum ParsedInternalPeerUrlParameter { public enum ParsedInternalUrl { case peerName(String, ParsedInternalPeerUrlParameter?) case peerId(PeerId) - case privateMessage(messageId: MessageId, threadId: Int32?) + case privateMessage(messageId: MessageId, threadId: Int32?, timecode: Double?) case stickerPack(String) case join(String) case localization(String) @@ -145,7 +146,7 @@ public func parseInternalUrl(query: String) -> ParsedInternalUrl? { } } } else if pathComponents[0].hasPrefix(phonebookUsernamePathPrefix), let idValue = Int32(String(pathComponents[0][pathComponents[0].index(pathComponents[0].startIndex, offsetBy: phonebookUsernamePathPrefix.count)...])) { - return .peerId(PeerId(namespace: Namespaces.Peer.CloudUser, id: idValue)) + return .peerId(PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt32Value(idValue))) } else if pathComponents[0].hasPrefix("+") || pathComponents[0].hasPrefix("%20") { return .join(String(pathComponents[0].dropFirst())) } @@ -185,7 +186,7 @@ public func parseInternalUrl(query: String) -> ParsedInternalUrl? { let parameter: WallpaperUrlParameter 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 { + } 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 { @@ -196,17 +197,43 @@ public func parseInternalUrl(query: String) -> ParsedInternalUrl? { } } } - 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) + if component.contains("~") { + let components = component.components(separatedBy: "~") + + var colors: [UInt32] = [] + if components.count >= 2 && components.count <= 4 { + colors = components.compactMap { component in + return UIColor(hexString: component)?.rgb + } + } + + if !colors.isEmpty { + parameter = .gradient(colors, rotation) + } else { + return nil + } } else { - return nil + let components = component.components(separatedBy: "-") + if components.count == 2, let topColor = UIColor(hexString: components[0]), let bottomColor = UIColor(hexString: components[1]) { + parameter = .gradient([topColor.rgb, bottomColor.rgb], rotation) + } else { + return nil + } + } + } else if component.contains("~") { + let components = component.components(separatedBy: "~") + if components.count >= 1 && components.count <= 4 { + let colors = components.compactMap { component in + return UIColor(hexString: component)?.rgb + } + parameter = .gradient(colors, nil) + } else { + parameter = .color(UIColor(rgb: 0xffffff)) } } else { var options: WallpaperPresentationOptions = [] var intensity: Int32? - var topColor: UIColor? - var bottomColor: UIColor? + var colors: [UInt32] = [] var rotation: Int32? if let queryItems = components.queryItems { for queryItem in queryItems { @@ -224,12 +251,18 @@ public func parseInternalUrl(query: String) -> ParsedInternalUrl? { } } else if queryItem.name == "bg_color" { if [6, 8].contains(value.count), value.rangeOfCharacter(from: CharacterSet(charactersIn: "0123456789abcdefABCDEF").inverted) == nil, let color = UIColor(hexString: value) { - topColor = color + colors = [color.rgb] } 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 + colors = [topColorValue.rgb, bottomColorValue.rgb] + } + } else if value.contains("~") { + let components = value.components(separatedBy: "~") + if components.count >= 2 && components.count <= 4 { + colors = components.compactMap { component in + return UIColor(hexString: component)?.rgb + } } } } else if queryItem.name == "intensity" { @@ -240,7 +273,7 @@ public func parseInternalUrl(query: String) -> ParsedInternalUrl? { } } } - parameter = .slug(component, options, topColor, bottomColor, intensity, rotation) + parameter = .slug(component, options, colors, intensity, rotation) } return .wallpaper(parameter) } else if pathComponents[0] == "addtheme" { @@ -248,48 +281,55 @@ public func parseInternalUrl(query: String) -> ParsedInternalUrl? { } else if pathComponents.count == 3 && pathComponents[0] == "c" { if let channelId = Int32(pathComponents[1]), let messageId = Int32(pathComponents[2]) { var threadId: Int32? + var timecode: Double? if let queryItems = components.queryItems { for queryItem in queryItems { if let value = queryItem.value { if queryItem.name == "thread" { if let intValue = Int32(value) { threadId = intValue - break + } + } else if queryItem.name == "t" { + if let doubleValue = Double(value) { + timecode = doubleValue } } } } } - return .privateMessage(messageId: MessageId(peerId: PeerId(namespace: Namespaces.Peer.CloudChannel, id: channelId), namespace: Namespaces.Message.Cloud, id: messageId), threadId: threadId) + return .privateMessage(messageId: MessageId(peerId: PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt32Value(channelId)), namespace: Namespaces.Message.Cloud, id: messageId), threadId: threadId, timecode: timecode) } else { return nil } - } else if let value = Int(pathComponents[1]) { + } else if let value = Int32(pathComponents[1]) { var threadId: Int32? var commentId: Int32? + var timecode: Double? if let queryItems = components.queryItems { for queryItem in queryItems { if let value = queryItem.value { if queryItem.name == "thread" { if let intValue = Int32(value) { threadId = intValue - break } } else if queryItem.name == "comment" { if let intValue = Int32(value) { commentId = intValue - break + } + } else if queryItem.name == "t" { + if let doubleValue = Double(value) { + timecode = doubleValue } } } } } if let threadId = threadId { - return .peerName(peerName, .replyThread(threadId, Int32(value))) + return .peerName(peerName, .replyThread(threadId, value)) } else if let commentId = commentId { - return .peerName(peerName, .replyThread(Int32(value), commentId)) + return .peerName(peerName, .replyThread(value, commentId)) } else { - return .peerName(peerName, .channelMessage(Int32(value))) + return .peerName(peerName, .channelMessage(value, timecode)) } } else { return nil @@ -302,13 +342,13 @@ public func parseInternalUrl(query: String) -> ParsedInternalUrl? { return nil } -private func resolveInternalUrl(account: Account, url: ParsedInternalUrl) -> Signal { +private func resolveInternalUrl(context: AccountContext, url: ParsedInternalUrl) -> Signal { switch url { case let .peerName(name, parameter): - return resolvePeerByName(account: account, name: name) + return context.engine.peers.resolvePeerByName(name: name) |> take(1) |> mapToSignal { peerId -> Signal in - return account.postbox.transaction { transaction -> Peer? in + return context.account.postbox.transaction { transaction -> Peer? in if let peerId = peerId { return transaction.getPeer(peerId) } else { @@ -324,18 +364,18 @@ private func resolveInternalUrl(account: Account, url: ParsedInternalUrl) -> Sig return .single(.botStart(peerId: peer.id, payload: payload)) case let .groupBotStart(payload): return .single(.groupBotStart(peerId: peer.id, payload: payload)) - case let .channelMessage(id): - return .single(.channelMessage(peerId: peer.id, messageId: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: id))) + case let .channelMessage(id, timecode): + return .single(.channelMessage(peerId: peer.id, messageId: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: id), timecode: timecode)) case let .replyThread(id, replyId): let replyThreadMessageId = MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: id) - return fetchChannelReplyThreadMessage(account: account, messageId: replyThreadMessageId, atMessageId: nil) + return context.engine.messages.fetchChannelReplyThreadMessage(messageId: replyThreadMessageId, atMessageId: nil) |> map(Optional.init) |> `catch` { _ -> Signal in return .single(nil) } |> map { result -> ResolvedUrl? in guard let result = result else { - return .channelMessage(peerId: peer.id, messageId: replyThreadMessageId) + return .channelMessage(peerId: peer.id, messageId: replyThreadMessageId, timecode: nil) } return .replyThreadMessage(replyThreadMessage: result, messageId: MessageId(peerId: result.messageId.peerId, namespace: Namespaces.Message.Cloud, id: replyId)) } @@ -354,7 +394,7 @@ private func resolveInternalUrl(account: Account, url: ParsedInternalUrl) -> Sig } } case let .peerId(peerId): - return account.postbox.transaction { transaction -> Peer? in + return context.account.postbox.transaction { transaction -> Peer? in return transaction.getPeer(peerId) } |> mapToSignal { peer -> Signal in @@ -364,8 +404,8 @@ private func resolveInternalUrl(account: Account, url: ParsedInternalUrl) -> Sig return .single(.inaccessiblePeer) } } - case let .privateMessage(messageId, threadId): - return account.postbox.transaction { transaction -> Peer? in + case let .privateMessage(messageId, threadId, timecode): + return context.account.postbox.transaction { transaction -> Peer? in return transaction.getPeer(messageId.peerId) } |> mapToSignal { peer -> Signal in @@ -373,26 +413,26 @@ private func resolveInternalUrl(account: Account, url: ParsedInternalUrl) -> Sig if let peer = peer { foundPeer = .single(peer) } else { - foundPeer = findChannelById(postbox: account.postbox, network: account.network, channelId: messageId.peerId.id) + foundPeer = TelegramEngine(account: context.account).peers.findChannelById(channelId: messageId.peerId.id._internalGetInt32Value()) } return foundPeer |> mapToSignal { foundPeer -> Signal in if let foundPeer = foundPeer { if let threadId = threadId { let replyThreadMessageId = MessageId(peerId: foundPeer.id, namespace: Namespaces.Message.Cloud, id: threadId) - return fetchChannelReplyThreadMessage(account: account, messageId: replyThreadMessageId, atMessageId: nil) + return context.engine.messages.fetchChannelReplyThreadMessage(messageId: replyThreadMessageId, atMessageId: nil) |> map(Optional.init) |> `catch` { _ -> Signal in return .single(nil) } |> map { result -> ResolvedUrl? in guard let result = result else { - return .channelMessage(peerId: foundPeer.id, messageId: replyThreadMessageId) + return .channelMessage(peerId: foundPeer.id, messageId: replyThreadMessageId, timecode: timecode) } return .replyThreadMessage(replyThreadMessage: result, messageId: messageId) } } else { - return .single(.peer(foundPeer.id, .chat(textInputState: nil, subject: .message(id: messageId, highlight: true), peekData: nil))) + return .single(.peer(foundPeer.id, .chat(textInputState: nil, subject: .message(id: messageId, highlight: true, timecode: timecode), peekData: nil))) } } else { return .single(.inaccessiblePeer) @@ -408,7 +448,7 @@ private func resolveInternalUrl(account: Account, url: ParsedInternalUrl) -> Sig case let .proxy(host, port, username, password, secret): return .single(.proxy(host: host, port: port, username: username, password: password, secret: secret)) case let .internalInstantView(url): - return resolveInstantViewUrl(account: account, url: url) + return resolveInstantViewUrl(account: context.account, url: url) |> map(Optional.init) case let .confirmationCode(code): return .single(.confirmationCode(code)) @@ -525,60 +565,72 @@ private struct UrlHandlingConfiguration { } } -public func resolveUrlImpl(account: Account, url: String, skipUrlAuth: Bool) -> Signal { +public func resolveUrlImpl(context: AccountContext, peerId: PeerId?, url: String, skipUrlAuth: Bool) -> Signal { let schemes = ["http://", "https://", ""] - return account.postbox.transaction { transaction -> Signal in - let appConfiguration: AppConfiguration = transaction.getPreferencesEntry(key: PreferencesKeys.appConfiguration) as? AppConfiguration ?? AppConfiguration.defaultValue - let urlHandlingConfiguration = UrlHandlingConfiguration.with(appConfiguration: appConfiguration) - - var url = url - if !url.contains("://") && !url.hasPrefix("tel:") && !url.hasPrefix("mailto:") && !url.hasPrefix("calshow:") { - if !(url.hasPrefix("http") || url.hasPrefix("https")) { - url = "http://\(url)" + return ApplicationSpecificNotice.getSecretChatLinkPreviews(accountManager: context.sharedContext.accountManager) + |> mapToSignal { linkPreviews -> Signal in + return context.account.postbox.transaction { transaction -> Signal in + let appConfiguration: AppConfiguration = transaction.getPreferencesEntry(key: PreferencesKeys.appConfiguration) as? AppConfiguration ?? AppConfiguration.defaultValue + let urlHandlingConfiguration = UrlHandlingConfiguration.with(appConfiguration: appConfiguration) + + var skipUrlAuth = skipUrlAuth + if let peerId = peerId, peerId.namespace == Namespaces.Peer.SecretChat { + if let linkPreviews = linkPreviews, linkPreviews { + } else { + skipUrlAuth = true + } } - } - if let urlValue = URL(string: url), let host = urlValue.host?.lowercased() { - if urlHandlingConfiguration.domains.contains(host), var components = URLComponents(string: url) { - components.scheme = "https" - var queryItems = components.queryItems ?? [] - queryItems.append(URLQueryItem(name: "autologin_token", value: urlHandlingConfiguration.token)) - components.queryItems = queryItems - url = components.url?.absoluteString ?? url - } else if !skipUrlAuth && urlHandlingConfiguration.urlAuthDomains.contains(host) { - return .single(.urlAuth(url)) + + var url = url + if !url.contains("://") && !url.hasPrefix("tel:") && !url.hasPrefix("mailto:") && !url.hasPrefix("calshow:") { + if !(url.hasPrefix("http") || url.hasPrefix("https")) { + url = "http://\(url)" + } } - } - - 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...])) { - return resolveInternalUrl(account: account, url: internalUrl) - |> map { resolved -> ResolvedUrl in - if let resolved = resolved { - return resolved - } else { - return .externalUrl(url) + + if let urlValue = URL(string: url), let host = urlValue.host?.lowercased() { + if urlHandlingConfiguration.domains.contains(host), var components = URLComponents(string: url) { + components.scheme = "https" + var queryItems = components.queryItems ?? [] + queryItems.append(URLQueryItem(name: "autologin_token", value: urlHandlingConfiguration.token)) + components.queryItems = queryItems + url = components.url?.absoluteString ?? url + } else if !skipUrlAuth && urlHandlingConfiguration.urlAuthDomains.contains(host) { + return .single(.urlAuth(url)) + } + } + + 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...])) { + return resolveInternalUrl(context: context, url: internalUrl) + |> map { resolved -> ResolvedUrl in + if let resolved = resolved { + return resolved + } else { + return .externalUrl(url) + } } + } else { + return .single(.externalUrl(url)) } - } else { - return .single(.externalUrl(url)) } } } - } - for basePath in baseTelegraPhPaths { - for scheme in schemes { - let basePrefix = scheme + basePath - if url.lowercased().hasPrefix(basePrefix) { - return resolveInstantViewUrl(account: account, url: url) + for basePath in baseTelegraPhPaths { + for scheme in schemes { + let basePrefix = scheme + basePath + if url.lowercased().hasPrefix(basePrefix) { + return resolveInstantViewUrl(account: context.account, url: url) + } } } - } - return .single(.externalUrl(url)) - } |> switchToLatest + return .single(.externalUrl(url)) + } |> switchToLatest + } } public func resolveInstantViewUrl(account: Account, url: String) -> Signal { diff --git a/submodules/UrlWhitelist/Sources/UrlWhitelist.swift b/submodules/UrlWhitelist/Sources/UrlWhitelist.swift index fac3604964..7f7c8ef7f3 100644 --- a/submodules/UrlWhitelist/Sources/UrlWhitelist.swift +++ b/submodules/UrlWhitelist/Sources/UrlWhitelist.swift @@ -31,7 +31,7 @@ public func parseUrl(url: String, wasConcealed: Bool) -> (string: String, concea latin.insert(charactersIn: "a"..."z") latin.insert(charactersIn: "0"..."9") var punctuation = CharacterSet() - punctuation.insert(charactersIn: ".-/+_") + punctuation.insert(charactersIn: ".-/+_?=") var hasLatin = false var hasNonLatin = false for c in rawHost { diff --git a/submodules/MessageReactionListUI/BUILD b/submodules/WallpaperBackgroundNode/BUILD similarity index 66% rename from submodules/MessageReactionListUI/BUILD rename to submodules/WallpaperBackgroundNode/BUILD index 36ff8e35c1..65b2180e3e 100644 --- a/submodules/MessageReactionListUI/BUILD +++ b/submodules/WallpaperBackgroundNode/BUILD @@ -1,22 +1,25 @@ load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") swift_library( - name = "MessageReactionListUI", - module_name = "MessageReactionListUI", + name = "WallpaperBackgroundNode", + module_name = "WallpaperBackgroundNode", srcs = glob([ - "Sources/**/*.swift", + "Sources/**/*.swift", ]), + copts = [ + ], deps = [ - "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", - "//submodules/TelegramCore:TelegramCore", - "//submodules/SyncCore:SyncCore", - "//submodules/Postbox:Postbox", "//submodules/AsyncDisplayKit:AsyncDisplayKit", "//submodules/Display:Display", + "//submodules/GradientBackground:GradientBackground", "//submodules/TelegramPresentationData:TelegramPresentationData", + "//submodules/SyncCore:SyncCore", + "//submodules/TelegramCore:TelegramCore", + "//submodules/Postbox:Postbox", "//submodules/AccountContext:AccountContext", - "//submodules/MergeLists:MergeLists", - "//submodules/ItemListPeerItem:ItemListPeerItem", + "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", + "//submodules/WallpaperResources:WallpaperResources", + "//submodules/FastBlur:FastBlur", ], visibility = [ "//visibility:public", diff --git a/submodules/WallpaperBackgroundNode/Sources/WallpaperBackgroundNode.swift b/submodules/WallpaperBackgroundNode/Sources/WallpaperBackgroundNode.swift new file mode 100644 index 0000000000..830dd0505a --- /dev/null +++ b/submodules/WallpaperBackgroundNode/Sources/WallpaperBackgroundNode.swift @@ -0,0 +1,818 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display +import GradientBackground +import TelegramPresentationData +import SyncCore +import TelegramCore +import AccountContext +import SwiftSignalKit +import WallpaperResources +import Postbox +import FastBlur + +private let motionAmount: CGFloat = 32.0 + +private func generateBlurredContents(image: UIImage) -> UIImage? { + let size = image.size.aspectFitted(CGSize(width: 64.0, height: 64.0)) + let context = DrawingContext(size: size, scale: 1.0, opaque: true, clear: false) + context.withFlippedContext { c in + c.draw(image.cgImage!, in: CGRect(origin: CGPoint(), size: size)) + } + + telegramFastBlurMore(Int32(context.size.width), Int32(context.size.height), Int32(context.bytesPerRow), context.bytes) + telegramFastBlurMore(Int32(context.size.width), Int32(context.size.height), Int32(context.bytesPerRow), context.bytes) + + adjustSaturationInContext(context: context, saturation: 1.7) + + return context.generateImage() +} + +public final class WallpaperBackgroundNode: ASDisplayNode { + public final class BubbleBackgroundNode: ASDisplayNode { + public enum BubbleType { + case incoming + case outgoing + case free + } + + private let bubbleType: BubbleType + private let contentNode: ASImageNode + + private var cleanWallpaperNode: ASDisplayNode? + private var gradientWallpaperNode: GradientBackgroundNode.CloneNode? + private weak var backgroundNode: WallpaperBackgroundNode? + private var index: SparseBag.Index? + + private var currentLayout: (rect: CGRect, containerSize: CGSize)? + + public override var frame: CGRect { + didSet { + if oldValue.size != self.bounds.size { + self.contentNode.frame = self.bounds + if let cleanWallpaperNode = self.cleanWallpaperNode { + cleanWallpaperNode.frame = self.bounds + } + if let gradientWallpaperNode = self.gradientWallpaperNode { + gradientWallpaperNode.frame = self.bounds + } + } + } + } + + init(backgroundNode: WallpaperBackgroundNode, bubbleType: BubbleType) { + self.backgroundNode = backgroundNode + self.bubbleType = bubbleType + + self.contentNode = ASImageNode() + self.contentNode.isUserInteractionEnabled = false + + super.init() + + self.addSubnode(self.contentNode) + + self.index = backgroundNode.bubbleBackgroundNodeReferences.add(BubbleBackgroundNodeReference(node: self)) + } + + deinit { + if let index = self.index, let backgroundNode = self.backgroundNode { + backgroundNode.bubbleBackgroundNodeReferences.remove(index) + } + } + + func updateContents() { + guard let backgroundNode = self.backgroundNode else { + return + } + + if let bubbleTheme = backgroundNode.bubbleTheme, let bubbleCorners = backgroundNode.bubbleCorners { + let wallpaper = backgroundNode.wallpaper ?? bubbleTheme.chat.defaultWallpaper + + let graphics = PresentationResourcesChat.principalGraphics(theme: bubbleTheme, wallpaper: wallpaper, bubbleCorners: bubbleCorners) + var needsCleanBackground = false + switch self.bubbleType { + case .incoming: + self.contentNode.image = graphics.incomingBubbleGradientImage + if graphics.incomingBubbleGradientImage == nil { + self.contentNode.backgroundColor = bubbleTheme.chat.message.incoming.bubble.withWallpaper.fill + } else { + self.contentNode.backgroundColor = nil + } + needsCleanBackground = bubbleTheme.chat.message.incoming.bubble.withWallpaper.fill.alpha <= 0.99 || bubbleTheme.chat.message.incoming.bubble.withWallpaper.gradientFill.alpha <= 0.99 + case .outgoing: + self.contentNode.image = graphics.outgoingBubbleGradientImage + if graphics.outgoingBubbleGradientImage == nil { + self.contentNode.backgroundColor = bubbleTheme.chat.message.outgoing.bubble.withWallpaper.fill + } else { + self.contentNode.backgroundColor = nil + } + needsCleanBackground = bubbleTheme.chat.message.outgoing.bubble.withWallpaper.fill.alpha <= 0.99 || bubbleTheme.chat.message.outgoing.bubble.withWallpaper.gradientFill.alpha <= 0.99 + case .free: + self.contentNode.image = nil + self.contentNode.backgroundColor = nil + needsCleanBackground = true + } + + var isInvertedGradient = false + var hasComplexGradient = false + switch wallpaper { + case let .file(_, _, _, _, _, _, _, _, settings): + hasComplexGradient = settings.colors.count >= 3 + if let intensity = settings.intensity, intensity < 0 { + isInvertedGradient = true + } + case let .gradient(_, colors, _): + hasComplexGradient = colors.count >= 3 + default: + break + } + + var needsGradientBackground = false + var needsWallpaperBackground = false + + if isInvertedGradient { + switch self.bubbleType { + case .free: + needsCleanBackground = false + case .incoming, .outgoing: + break + } + } + + if needsCleanBackground { + if hasComplexGradient { + needsGradientBackground = backgroundNode.gradientBackgroundNode != nil + } else { + needsWallpaperBackground = true + } + } + + if needsWallpaperBackground { + if self.cleanWallpaperNode == nil { + let cleanWallpaperNode = ASImageNode() + self.cleanWallpaperNode = cleanWallpaperNode + cleanWallpaperNode.frame = self.bounds + self.insertSubnode(cleanWallpaperNode, at: 0) + } + if let blurredBackgroundContents = backgroundNode.blurredBackgroundContents { + self.cleanWallpaperNode?.contents = blurredBackgroundContents.cgImage + self.cleanWallpaperNode?.backgroundColor = backgroundNode.contentNode.backgroundColor + } else { + self.cleanWallpaperNode?.contents = backgroundNode.contentNode.contents + self.cleanWallpaperNode?.backgroundColor = backgroundNode.contentNode.backgroundColor + } + } else { + if let cleanWallpaperNode = self.cleanWallpaperNode { + self.cleanWallpaperNode = nil + cleanWallpaperNode.removeFromSupernode() + } + } + + if needsGradientBackground, let gradientBackgroundNode = backgroundNode.gradientBackgroundNode { + if self.gradientWallpaperNode == nil { + let gradientWallpaperNode = GradientBackgroundNode.CloneNode(parentNode: gradientBackgroundNode) + gradientWallpaperNode.frame = self.bounds + self.gradientWallpaperNode = gradientWallpaperNode + self.insertSubnode(gradientWallpaperNode, at: 0) + } + } else { + if let gradientWallpaperNode = self.gradientWallpaperNode { + self.gradientWallpaperNode = nil + gradientWallpaperNode.removeFromSupernode() + } + } + } else { + self.contentNode.image = nil + if let cleanWallpaperNode = self.cleanWallpaperNode { + self.cleanWallpaperNode = nil + cleanWallpaperNode.removeFromSupernode() + } + } + + if let (rect, containerSize) = self.currentLayout { + self.update(rect: rect, within: containerSize) + } + } + + public func update(rect: CGRect, within containerSize: CGSize, transition: ContainedViewLayoutTransition = .immediate) { + self.currentLayout = (rect, containerSize) + + let shiftedContentsRect = CGRect(origin: CGPoint(x: rect.minX / containerSize.width, y: rect.minY / containerSize.height), size: CGSize(width: rect.width / containerSize.width, height: rect.height / containerSize.height)) + + transition.updateFrame(layer: self.contentNode.layer, frame: self.bounds) + self.contentNode.layer.contentsRect = shiftedContentsRect + if let cleanWallpaperNode = self.cleanWallpaperNode { + transition.updateFrame(layer: cleanWallpaperNode.layer, frame: self.bounds) + cleanWallpaperNode.layer.contentsRect = shiftedContentsRect + } + if let gradientWallpaperNode = self.gradientWallpaperNode { + transition.updateFrame(layer: gradientWallpaperNode.layer, frame: self.bounds) + gradientWallpaperNode.layer.contentsRect = shiftedContentsRect + } + } + + public func update(rect: CGRect, within containerSize: CGSize, transition: CombinedTransition) { + self.currentLayout = (rect, containerSize) + + let shiftedContentsRect = CGRect(origin: CGPoint(x: rect.minX / containerSize.width, y: rect.minY / containerSize.height), size: CGSize(width: rect.width / containerSize.width, height: rect.height / containerSize.height)) + + transition.updateFrame(layer: self.contentNode.layer, frame: self.bounds) + self.contentNode.layer.contentsRect = shiftedContentsRect + if let cleanWallpaperNode = self.cleanWallpaperNode { + transition.updateFrame(layer: cleanWallpaperNode.layer, frame: self.bounds) + cleanWallpaperNode.layer.contentsRect = shiftedContentsRect + } + if let gradientWallpaperNode = self.gradientWallpaperNode { + transition.updateFrame(layer: gradientWallpaperNode.layer, frame: self.bounds) + gradientWallpaperNode.layer.contentsRect = shiftedContentsRect + } + } + + public func offset(value: CGPoint, animationCurve: ContainedViewLayoutTransitionCurve, duration: Double) { + guard let (_, containerSize) = self.currentLayout else { + return + } + let transition: ContainedViewLayoutTransition = .animated(duration: duration, curve: animationCurve) + + let scaledOffset = CGPoint(x: value.x / containerSize.width, y: value.y / containerSize.height) + transition.animateContentsRectPositionAdditive(layer: self.contentNode.layer, offset: scaledOffset) + + if let cleanWallpaperNode = self.cleanWallpaperNode { + transition.animateContentsRectPositionAdditive(layer: cleanWallpaperNode.layer, offset: scaledOffset) + } + if let gradientWallpaperNode = self.gradientWallpaperNode { + transition.animateContentsRectPositionAdditive(layer: gradientWallpaperNode.layer, offset: scaledOffset) + } + } + + public func offsetSpring(value: CGFloat, duration: Double, damping: CGFloat) { + guard let (_, containerSize) = self.currentLayout else { + return + } + + let scaledOffset = CGPoint(x: 0.0, y: -value / containerSize.height) + + self.contentNode.layer.animateSpring(from: NSValue(cgPoint: scaledOffset), to: NSValue(cgPoint: CGPoint()), keyPath: "contentsRect.position", duration: duration, initialVelocity: 0.0, damping: damping, additive: true) + if let cleanWallpaperNode = self.cleanWallpaperNode { + cleanWallpaperNode.layer.animateSpring(from: NSValue(cgPoint: scaledOffset), to: NSValue(cgPoint: CGPoint()), keyPath: "contentsRect.position", duration: duration, initialVelocity: 0.0, damping: damping, additive: true) + } + if let gradientWallpaperNode = self.gradientWallpaperNode { + gradientWallpaperNode.layer.animateSpring(from: NSValue(cgPoint: scaledOffset), to: NSValue(cgPoint: CGPoint()), keyPath: "contentsRect.position", duration: duration, initialVelocity: 0.0, damping: damping, additive: true) + } + } + } + + private final class BubbleBackgroundNodeReference { + weak var node: BubbleBackgroundNode? + + init(node: BubbleBackgroundNode) { + self.node = node + } + } + + private let context: AccountContext + private let useSharedAnimationPhase: Bool + + private let contentNode: ASDisplayNode + private var blurredBackgroundContents: UIImage? + + private var gradientBackgroundNode: GradientBackgroundNode? + private let patternImageNode: ASImageNode + private var isGeneratingPatternImage: Bool = false + + private var validLayout: CGSize? + private var wallpaper: TelegramWallpaper? + private var isSettingUpWallpaper: Bool = false + + private struct CachedValidPatternImage { + let generate: (TransformImageArguments) -> DrawingContext? + let generated: ValidPatternGeneratedImage + let image: UIImage + } + + private static var cachedValidPatternImage: CachedValidPatternImage? + + private struct ValidPatternImage { + let wallpaper: TelegramWallpaper + let generate: (TransformImageArguments) -> DrawingContext? + } + private var validPatternImage: ValidPatternImage? + + private struct ValidPatternGeneratedImage: Equatable { + let wallpaper: TelegramWallpaper + let size: CGSize + let patternColor: UInt32 + let backgroundColor: UInt32 + let invertPattern: Bool + } + private var validPatternGeneratedImage: ValidPatternGeneratedImage? + + private let patternImageDisposable = MetaDisposable() + + private var bubbleTheme: PresentationTheme? + private var bubbleCorners: PresentationChatBubbleCorners? + private var bubbleBackgroundNodeReferences = SparseBag() + + private let wallpaperDisposable = MetaDisposable() + + private let imageDisposable = MetaDisposable() + + private var motionEnabled: Bool = false { + didSet { + if oldValue != self.motionEnabled { + if self.motionEnabled { + 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.contentNode.view.addMotionEffect(group) + } else { + for effect in self.contentNode.view.motionEffects { + self.contentNode.view.removeMotionEffect(effect) + } + } + if !self.frame.isEmpty { + self.updateScale() + } + } + } + } + + public var rotation: CGFloat = 0.0 { + didSet { + 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) + } + } + + private var imageContentMode: UIView.ContentMode { + didSet { + self.contentNode.contentMode = self.imageContentMode + } + } + + private func updateScale() { + if self.motionEnabled { + let scale = (self.frame.width + motionAmount * 2.0) / self.frame.width + self.contentNode.transform = CATransform3DMakeScale(scale, scale, 1.0) + } else { + self.contentNode.transform = CATransform3DIdentity + } + } + + private struct PatternKey: Equatable { + var mediaId: MediaId + var isLight: Bool + } + private static var cachedSharedPattern: (PatternKey, UIImage)? + + private let _isReady = ValuePromise(false, ignoreRepeated: true) + public var isReady: Signal { + return self._isReady.get() + } + + public init(context: AccountContext, useSharedAnimationPhase: Bool = false) { + self.context = context + self.useSharedAnimationPhase = useSharedAnimationPhase + self.imageContentMode = .scaleAspectFill + + self.contentNode = ASDisplayNode() + self.contentNode.contentMode = self.imageContentMode + + self.patternImageNode = ASImageNode() + + super.init() + + self.clipsToBounds = true + self.contentNode.frame = self.bounds + self.addSubnode(self.contentNode) + self.addSubnode(self.patternImageNode) + } + + deinit { + self.patternImageDisposable.dispose() + self.wallpaperDisposable.dispose() + self.imageDisposable.dispose() + } + + public func update(wallpaper: TelegramWallpaper) { + if self.wallpaper == wallpaper { + return + } + self.wallpaper = wallpaper + + var gradientColors: [UInt32] = [] + var gradientAngle: Int32 = 0 + + if case let .color(color) = wallpaper { + gradientColors = [color] + self._isReady.set(true) + } else if case let .gradient(_, colors, settings) = wallpaper { + gradientColors = colors + gradientAngle = settings.rotation ?? 0 + self._isReady.set(true) + } else if case let .file(_, _, _, _, isPattern, _, _, _, settings) = wallpaper, isPattern { + gradientColors = settings.colors + gradientAngle = settings.rotation ?? 0 + } + + if gradientColors.count >= 3 { + let mappedColors = gradientColors.map { color -> UIColor in + return UIColor(rgb: color) + } + if self.gradientBackgroundNode == nil { + let gradientBackgroundNode = createGradientBackgroundNode(colors: mappedColors, useSharedAnimationPhase: self.useSharedAnimationPhase) + self.gradientBackgroundNode = gradientBackgroundNode + self.insertSubnode(gradientBackgroundNode, aboveSubnode: self.contentNode) + gradientBackgroundNode.addSubnode(self.patternImageNode) + } + self.gradientBackgroundNode?.updateColors(colors: mappedColors) + + self.contentNode.backgroundColor = nil + self.contentNode.contents = nil + self.blurredBackgroundContents = nil + self.motionEnabled = false + self.wallpaperDisposable.set(nil) + } else { + if let gradientBackgroundNode = self.gradientBackgroundNode { + self.gradientBackgroundNode = nil + gradientBackgroundNode.removeFromSupernode() + self.insertSubnode(self.patternImageNode, aboveSubnode: self.contentNode) + } + + self.motionEnabled = wallpaper.settings?.motion ?? false + + if gradientColors.count >= 2 { + self.contentNode.backgroundColor = nil + let image = generateImage(CGSize(width: 100.0, height: 200.0), rotatedContext: { size, context in + let gradientColors = [UIColor(rgb: gradientColors[0]).cgColor, UIColor(rgb: gradientColors[1]).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: size.width / 2.0, y: size.height / 2.0) + context.rotate(by: CGFloat(gradientAngle) * CGFloat.pi / 180.0) + context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0) + + context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: size.height), options: [.drawsBeforeStartLocation, .drawsAfterEndLocation]) + }) + self.contentNode.contents = image?.cgImage + self.blurredBackgroundContents = image + self.wallpaperDisposable.set(nil) + } else if gradientColors.count >= 1 { + self.contentNode.backgroundColor = UIColor(rgb: gradientColors[0]) + self.contentNode.contents = nil + self.blurredBackgroundContents = nil + self.wallpaperDisposable.set(nil) + } else { + self.contentNode.backgroundColor = .white + if let image = chatControllerBackgroundImage(theme: nil, wallpaper: wallpaper, mediaBox: self.context.sharedContext.accountManager.mediaBox, knockoutMode: false) { + self.contentNode.contents = image.cgImage + self.blurredBackgroundContents = generateBlurredContents(image: image) + self.wallpaperDisposable.set(nil) + Queue.mainQueue().justDispatch { + self._isReady.set(true) + } + } else if let image = chatControllerBackgroundImage(theme: nil, wallpaper: wallpaper, mediaBox: self.context.account.postbox.mediaBox, knockoutMode: false) { + self.contentNode.contents = image.cgImage + self.blurredBackgroundContents = generateBlurredContents(image: image) + self.wallpaperDisposable.set(nil) + Queue.mainQueue().justDispatch { + self._isReady.set(true) + } + } else { + self.wallpaperDisposable.set((chatControllerBackgroundImageSignal(wallpaper: wallpaper, mediaBox: self.context.sharedContext.accountManager.mediaBox, accountMediaBox: self.context.account.postbox.mediaBox) + |> deliverOnMainQueue).start(next: { [weak self] image in + guard let strongSelf = self else { + return + } + strongSelf.contentNode.contents = image?.0?.cgImage + if let image = image?.0 { + strongSelf.blurredBackgroundContents = generateBlurredContents(image: image) + } else { + strongSelf.blurredBackgroundContents = nil + } + strongSelf._isReady.set(true) + })) + } + self.contentNode.isHidden = false + } + } + + if let size = self.validLayout { + self.updateLayout(size: size, transition: .immediate) + self.updateBubbles() + } + } + + public func _internalUpdateIsSettingUpWallpaper() { + self.isSettingUpWallpaper = true + } + + private func updatePatternPresentation() { + guard let wallpaper = self.wallpaper else { + return + } + + switch wallpaper { + case let .file(_, _, _, _, isPattern, _, _, _, settings) where isPattern: + let brightness = UIColor.average(of: settings.colors.map(UIColor.init(rgb:))).hsb.b + let patternIsBlack = brightness <= 0.01 + + let intensity = CGFloat(settings.intensity ?? 50) / 100.0 + if intensity < 0 { + self.patternImageNode.alpha = 1.0 + self.patternImageNode.layer.compositingFilter = nil + } else { + self.patternImageNode.alpha = intensity + if patternIsBlack { + self.patternImageNode.layer.compositingFilter = nil + } else { + self.patternImageNode.layer.compositingFilter = "softLightBlendMode" + } + } + self.patternImageNode.isHidden = false + let invertPattern = intensity < 0 + if invertPattern { + self.backgroundColor = .black + let contentAlpha = abs(intensity) + self.gradientBackgroundNode?.contentView.alpha = contentAlpha + self.contentNode.alpha = contentAlpha + if self.patternImageNode.image != nil { + self.patternImageNode.backgroundColor = nil + } else { + self.patternImageNode.backgroundColor = .black + } + } else { + self.backgroundColor = nil + self.gradientBackgroundNode?.contentView.alpha = 1.0 + self.contentNode.alpha = 1.0 + self.patternImageNode.backgroundColor = nil + } + default: + self.patternImageDisposable.set(nil) + self.validPatternImage = nil + self.patternImageNode.isHidden = true + self.patternImageNode.backgroundColor = nil + self.backgroundColor = nil + self.gradientBackgroundNode?.contentView.alpha = 1.0 + self.contentNode.alpha = 1.0 + } + } + + private func loadPatternForSizeIfNeeded(size: CGSize, transition: ContainedViewLayoutTransition) { + guard let wallpaper = self.wallpaper else { + return + } + + var invertPattern: Bool = false + var patternIsLight: Bool = false + + switch wallpaper { + case let .file(_, _, _, _, isPattern, _, slug, file, settings) where isPattern: + var updated = true + let brightness = UIColor.average(of: settings.colors.map(UIColor.init(rgb:))).hsb.b + patternIsLight = brightness > 0.3 + if let previousWallpaper = self.validPatternImage?.wallpaper { + switch previousWallpaper { + case let .file(_, _, _, _, _, _, _, previousFile, _): + if file.id == previousFile.id { + updated = false + } + default: + break + } + } + + if updated { + self.validPatternGeneratedImage = nil + self.validPatternImage = nil + + if let cachedValidPatternImage = WallpaperBackgroundNode.cachedValidPatternImage, cachedValidPatternImage.generated.wallpaper == wallpaper { + self.validPatternImage = ValidPatternImage(wallpaper: cachedValidPatternImage.generated.wallpaper, generate: cachedValidPatternImage.generate) + } else { + func reference(for resource: MediaResource, media: Media, message: Message?) -> MediaResourceReference { + if let message = message { + return .media(media: .message(message: MessageReference(message), media: media), resource: resource) + } + return .wallpaper(wallpaper: .slug(slug), resource: resource) + } + + var convertedRepresentations: [ImageRepresentationWithReference] = [] + for representation in file.previewRepresentations { + convertedRepresentations.append(ImageRepresentationWithReference(representation: representation, reference: reference(for: representation.resource, media: file, message: nil))) + } + let dimensions = file.dimensions ?? PixelDimensions(width: 2000, height: 4000) + convertedRepresentations.append(ImageRepresentationWithReference(representation: .init(dimensions: dimensions, resource: file.resource, progressiveSizes: [], immediateThumbnailData: nil), reference: reference(for: file.resource, media: file, message: nil))) + + let signal = patternWallpaperImage(account: self.context.account, accountManager: self.context.sharedContext.accountManager, representations: convertedRepresentations, mode: .screen, autoFetchFullSize: true) + self.patternImageDisposable.set((signal + |> deliverOnMainQueue).start(next: { [weak self] generator in + guard let strongSelf = self else { + return + } + if let generator = generator { + strongSelf.validPatternImage = ValidPatternImage(wallpaper: wallpaper, generate: generator) + strongSelf.validPatternGeneratedImage = nil + if let size = strongSelf.validLayout { + strongSelf.loadPatternForSizeIfNeeded(size: size, transition: .immediate) + } else { + strongSelf._isReady.set(true) + } + } else { + strongSelf._isReady.set(true) + } + })) + } + } + let intensity = CGFloat(settings.intensity ?? 50) / 100.0 + invertPattern = intensity < 0 + default: + self.updatePatternPresentation() + } + + if let validPatternImage = self.validPatternImage { + let patternBackgroundColor: UIColor + let patternColor: UIColor + if invertPattern { + patternColor = .clear + patternBackgroundColor = .clear + if self.patternImageNode.image == nil { + self.patternImageNode.backgroundColor = .black + } else { + self.patternImageNode.backgroundColor = nil + } + } else { + if patternIsLight { + patternColor = .black + } else { + patternColor = .white + } + patternBackgroundColor = .clear + self.patternImageNode.backgroundColor = nil + } + + let updatedGeneratedImage = ValidPatternGeneratedImage(wallpaper: validPatternImage.wallpaper, size: size, patternColor: patternColor.rgb, backgroundColor: patternBackgroundColor.rgb, invertPattern: invertPattern) + + if self.validPatternGeneratedImage != updatedGeneratedImage { + self.validPatternGeneratedImage = updatedGeneratedImage + + if let cachedValidPatternImage = WallpaperBackgroundNode.cachedValidPatternImage, cachedValidPatternImage.generated == updatedGeneratedImage { + self.patternImageNode.image = cachedValidPatternImage.image + self.updatePatternPresentation() + } else { + let patternArguments = TransformImageArguments(corners: ImageCorners(), imageSize: size, boundingSize: size, intrinsicInsets: UIEdgeInsets(), custom: PatternWallpaperArguments(colors: [patternBackgroundColor], rotation: nil, customPatternColor: patternColor, preview: false), scale: min(2.0, UIScreenScale)) + if self.useSharedAnimationPhase || self.patternImageNode.image == nil { + if let drawingContext = validPatternImage.generate(patternArguments) { + if let image = drawingContext.generateImage() { + self.patternImageNode.image = image + self.updatePatternPresentation() + + if self.useSharedAnimationPhase { + WallpaperBackgroundNode.cachedValidPatternImage = CachedValidPatternImage(generate: validPatternImage.generate, generated: updatedGeneratedImage, image: image) + } + } else { + self.updatePatternPresentation() + } + } else { + self.updatePatternPresentation() + } + } else { + self.isGeneratingPatternImage = true + DispatchQueue.global(qos: .userInteractive).async { [weak self] in + let image = validPatternImage.generate(patternArguments)?.generateImage() + Queue.mainQueue().async { + guard let strongSelf = self else { + return + } + strongSelf.isGeneratingPatternImage = false + strongSelf.patternImageNode.image = image + strongSelf.updatePatternPresentation() + + if let image = image, strongSelf.useSharedAnimationPhase { + WallpaperBackgroundNode.cachedValidPatternImage = CachedValidPatternImage(generate: validPatternImage.generate, generated: updatedGeneratedImage, image: image) + } + } + } + } + } + + self._isReady.set(true) + } else { + if !self.isGeneratingPatternImage { + self.updatePatternPresentation() + } + } + } else { + if !self.isGeneratingPatternImage { + self.updatePatternPresentation() + } + } + + transition.updateFrame(node: self.patternImageNode, frame: CGRect(origin: CGPoint(), size: size)) + } + + public func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) { + let isFirstLayout = self.validLayout == nil + self.validLayout = size + + 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 let gradientBackgroundNode = self.gradientBackgroundNode { + transition.updateFrame(node: gradientBackgroundNode, frame: CGRect(origin: CGPoint(), size: size)) + gradientBackgroundNode.updateLayout(size: size, transition: transition) + } + + self.loadPatternForSizeIfNeeded(size: size, transition: transition) + + if isFirstLayout && !self.frame.isEmpty { + self.updateScale() + } + } + + public func animateEvent(transition: ContainedViewLayoutTransition, extendAnimation: Bool = false) { + self.gradientBackgroundNode?.animateEvent(transition: transition, extendAnimation: extendAnimation) + } + + public func updateBubbleTheme(bubbleTheme: PresentationTheme, bubbleCorners: PresentationChatBubbleCorners) { + if self.bubbleTheme !== bubbleTheme || self.bubbleCorners != bubbleCorners { + self.bubbleTheme = bubbleTheme + self.bubbleCorners = bubbleCorners + + self.updateBubbles() + } + } + + private func updateBubbles() { + for reference in self.bubbleBackgroundNodeReferences { + reference.node?.updateContents() + } + } + + public func hasBubbleBackground(for type: WallpaperBackgroundNode.BubbleBackgroundNode.BubbleType) -> Bool { + guard let bubbleTheme = self.bubbleTheme, let bubbleCorners = self.bubbleCorners else { + return false + } + if self.wallpaper == nil && !self.isSettingUpWallpaper { + return false + } + + var hasPlainWallpaper = false + let graphicsWallpaper: TelegramWallpaper + if let wallpaper = self.wallpaper { + switch wallpaper { + case .color: + hasPlainWallpaper = true + default: + break + } + graphicsWallpaper = wallpaper + } else { + graphicsWallpaper = bubbleTheme.chat.defaultWallpaper + } + + let graphics = PresentationResourcesChat.principalGraphics(theme: bubbleTheme, wallpaper: graphicsWallpaper, bubbleCorners: bubbleCorners) + switch type { + case .incoming: + if graphics.incomingBubbleGradientImage != nil { + return true + } + if bubbleTheme.chat.message.incoming.bubble.withWallpaper.fill.alpha <= 0.99 { + return !hasPlainWallpaper + } + case .outgoing: + if graphics.outgoingBubbleGradientImage != nil { + return true + } + if bubbleTheme.chat.message.outgoing.bubble.withWallpaper.fill.alpha <= 0.99 { + return !hasPlainWallpaper + } + case .free: + return true + } + + return false + } + + public func makeBubbleBackground(for type: WallpaperBackgroundNode.BubbleBackgroundNode.BubbleType) -> WallpaperBackgroundNode.BubbleBackgroundNode? { + if !self.hasBubbleBackground(for: type) { + return nil + } + let node = WallpaperBackgroundNode.BubbleBackgroundNode(backgroundNode: self, bubbleType: type) + node.updateContents() + return node + } +} diff --git a/submodules/WallpaperResources/BUILD b/submodules/WallpaperResources/BUILD index e5aa740c94..4a369c7786 100644 --- a/submodules/WallpaperResources/BUILD +++ b/submodules/WallpaperResources/BUILD @@ -20,6 +20,8 @@ swift_library( "//submodules/PersistentStringHash:PersistentStringHash", "//submodules/AppBundle:AppBundle", "//submodules/Svg:Svg", + "//submodules/GZip:GZip", + "//submodules/GradientBackground:GradientBackground", ], visibility = [ "//visibility:public", diff --git a/submodules/WallpaperResources/Sources/WallpaperCache.swift b/submodules/WallpaperResources/Sources/WallpaperCache.swift index 5d01c54c3d..7c234b790f 100644 --- a/submodules/WallpaperResources/Sources/WallpaperCache.swift +++ b/submodules/WallpaperResources/Sources/WallpaperCache.swift @@ -47,7 +47,13 @@ public func cachedWallpaper(account: Account, slug: String, settings: WallpaperS let key = ValueBoxKey(length: 8) key.setInt64(0, value: Int64(bitPattern: slug.persistentHashValue)) let id = ItemCacheEntryId(collectionId: ApplicationSpecificItemCacheCollectionId.cachedWallpapers, key: key) - if let wallpaper = wallpaper { + if var wallpaper = wallpaper { + switch wallpaper { + case let .file(id, accessHash, isCreator, isDefault, isPattern, isDark, slug, file, settings): + wallpaper = .file(id: id, accessHash: accessHash, isCreator: isCreator, isDefault: isDefault, isPattern: isPattern, isDark: isDark, slug: slug, file: file.withUpdatedResource(WallpaperDataResource(slug: slug)), settings: settings) + default: + break + } let entry = CachedWallpaper(wallpaper: wallpaper) transaction.putItemCacheEntry(id: id, entry: entry, collectionSpec: collectionSpec) if let settings = settings { diff --git a/submodules/WallpaperResources/Sources/WallpaperResources.swift b/submodules/WallpaperResources/Sources/WallpaperResources.swift index d98b166d07..d654e55376 100644 --- a/submodules/WallpaperResources/Sources/WallpaperResources.swift +++ b/submodules/WallpaperResources/Sources/WallpaperResources.swift @@ -15,6 +15,8 @@ import TelegramPresentationData import TelegramUIPreferences import AppBundle import Svg +import GradientBackground +import GZip 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 }) { @@ -86,7 +88,7 @@ public func wallpaperDatas(account: Account, accountManager: AccountManager, fil } } else { let fetchedThumbnail: Signal - if let _ = decodedThumbnailData { + if let _ = decodedThumbnailData, false { fetchedThumbnail = .complete() } else { fetchedThumbnail = fetchedMediaResource(mediaBox: account.postbox.mediaBox, reference: representations[smallestIndex].reference) @@ -95,7 +97,7 @@ public func wallpaperDatas(account: Account, accountManager: AccountManager, fil let fetchedFullSize = fetchedMediaResource(mediaBox: account.postbox.mediaBox, reference: representations[largestIndex].reference) let thumbnailData: Signal - if let decodedThumbnailData = decodedThumbnailData { + if let decodedThumbnailData = decodedThumbnailData, false { thumbnailData = .single(decodedThumbnailData) } else { thumbnailData = Signal { subscriber in @@ -174,7 +176,7 @@ public func wallpaperDatas(account: Account, accountManager: AccountManager, fil } } -public func wallpaperImage(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<(TransformImageArguments) -> DrawingContext?, NoError> { +public func wallpaperImage(account: Account, accountManager: AccountManager, fileReference: FileMediaReference? = nil, representations: [ImageRepresentationWithReference], alwaysShowThumbnailFirst: Bool = false, thumbnail: Bool = false, onlyFullSize: Bool = false, autoFetchFullSize: Bool = false, blurred: Bool = false, synchronousLoad: Bool = false) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { let signal = wallpaperDatas(account: account, accountManager: accountManager, fileReference: fileReference, representations: representations, alwaysShowThumbnailFirst: alwaysShowThumbnailFirst, thumbnail: thumbnail, onlyFullSize: onlyFullSize, autoFetchFullSize: autoFetchFullSize, synchronousLoad: synchronousLoad) return signal @@ -213,6 +215,39 @@ public func wallpaperImage(account: Account, accountManager: AccountManager, fil } } } + + if blurred, let fullSizeImageValue = fullSizeImage { + let thumbnailSize = CGSize(width: fullSizeImageValue.width, height: fullSizeImageValue.height) + + let initialThumbnailContextFittingSize = fittedSize.fitted(CGSize(width: 90.0, height: 90.0)) + + let thumbnailContextSize = thumbnailSize.aspectFitted(initialThumbnailContextFittingSize) + let thumbnailContext = DrawingContext(size: thumbnailContextSize, scale: 1.0) + thumbnailContext.withFlippedContext { c in + c.draw(fullSizeImageValue, in: CGRect(origin: CGPoint(), size: thumbnailContextSize)) + } + telegramFastBlurMore(Int32(thumbnailContextSize.width), Int32(thumbnailContextSize.height), Int32(thumbnailContext.bytesPerRow), thumbnailContext.bytes) + + var thumbnailContextFittingSize = CGSize(width: floor(arguments.drawingSize.width * 0.5), height: floor(arguments.drawingSize.width * 0.5)) + if thumbnailContextFittingSize.width < 150.0 || thumbnailContextFittingSize.height < 150.0 { + thumbnailContextFittingSize = thumbnailContextFittingSize.aspectFilled(CGSize(width: 150.0, height: 150.0)) + } + + if false, thumbnailContextFittingSize.width > thumbnailContextSize.width { + let additionalContextSize = thumbnailContextFittingSize + let additionalBlurContext = DrawingContext(size: additionalContextSize, scale: 1.0) + additionalBlurContext.withFlippedContext { c in + c.interpolationQuality = .default + if let image = thumbnailContext.generateImage()?.cgImage { + c.draw(image, in: CGRect(origin: CGPoint(), size: additionalContextSize)) + } + } + imageFastBlur(Int32(additionalContextSize.width), Int32(additionalContextSize.height), Int32(additionalBlurContext.bytesPerRow), additionalBlurContext.bytes) + fullSizeImage = additionalBlurContext.generateImage()?.cgImage + } else { + fullSizeImage = thumbnailContext.generateImage()?.cgImage + } + } var thumbnailImage: CGImage? if let thumbnailData = thumbnailData, let imageSource = CGImageSourceCreateWithData(thumbnailData as CFData, nil), let image = CGImageSourceCreateImageAtIndex(imageSource, 0, nil) { @@ -300,75 +335,71 @@ public struct PatternWallpaperArguments: TransformImageCustomArguments { let colors: [UIColor] let rotation: Int32? let preview: Bool + let customPatternColor: UIColor? + let bakePatternAlpha: CGFloat - public init(colors: [UIColor], rotation: Int32?, preview: Bool = false) { + public init(colors: [UIColor], rotation: Int32?, customPatternColor: UIColor? = nil, preview: Bool = false, bakePatternAlpha: CGFloat = 1.0) { self.colors = colors self.rotation = rotation + self.customPatternColor = customPatternColor self.preview = preview + self.bakePatternAlpha = bakePatternAlpha } public func serialized() -> NSArray { let array = NSMutableArray() array.addObjects(from: self.colors) array.add(NSNumber(value: self.rotation ?? 0)) + if let customPatternColor = customPatternColor { + array.add(NSNumber(value: customPatternColor.argb)) + } array.add(NSNumber(value: self.preview)) + array.add(NSNumber(value: Double(self.bakePatternAlpha))) 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 }) { - - let size: CGSize? - switch mode { - case .thumbnail: - size = largestRepresentation.dimensions.cgSize.fitted(CGSize(width: 640.0, height: 640.0)) - default: - size = nil +private func patternWallpaperDatas(account: Account, accountManager: AccountManager, representations: [ImageRepresentationWithReference], mode: PatternWallpaperDrawMode, autoFetchFullSize: Bool = false) -> Signal<(Data?, Bool), NoError> { + var targetRepresentation: ImageRepresentationWithReference? + switch mode { + case .thumbnail: + if let representation = smallestImageRepresentation(representations.map({ $0.representation })) { + targetRepresentation = representations[representations.firstIndex(where: { $0.representation == representation })!] } - let maybeFullSize = combineLatest(accountManager.mediaBox.cachedResourceRepresentation(largestRepresentation.resource, representation: CachedPatternWallpaperMaskRepresentation(size: size), complete: false, fetch: false), account.postbox.mediaBox.cachedResourceRepresentation(largestRepresentation.resource, representation: CachedPatternWallpaperMaskRepresentation(size: size), complete: false, fetch: false)) + case .screen: + if let representation = largestImageRepresentation(representations.map({ $0.representation })) { + targetRepresentation = representations[representations.firstIndex(where: { $0.representation == representation })!] + } + } + + if let targetRepresentation = targetRepresentation { + let maybeFullSize = combineLatest( + accountManager.mediaBox.resourceData(targetRepresentation.representation.resource), + account.postbox.mediaBox.resourceData(targetRepresentation.representation.resource) + ) let signal = maybeFullSize |> take(1) - |> mapToSignal { maybeSharedData, maybeData -> Signal<(Data?, Data?, Bool), NoError> in + |> mapToSignal { maybeSharedData, maybeData -> Signal<(Data?, Bool), NoError> in if maybeSharedData.complete { if let loadedData = try? Data(contentsOf: URL(fileURLWithPath: maybeSharedData.path), options: [.mappedRead]) { - return .single((nil, loadedData, true)) + return .single((loadedData, true)) } else { - return .single((nil, nil, true)) + return .single(( nil, true)) } } else if maybeData.complete { let loadedData: Data? = try? Data(contentsOf: URL(fileURLWithPath: maybeData.path), options: []) - return .single((nil, loadedData, true)) + return .single((loadedData, true)) } else { - let fetchedThumbnail = fetchedMediaResource(mediaBox: account.postbox.mediaBox, reference: representations[smallestIndex].reference) - let fetchedFullSize = fetchedMediaResource(mediaBox: account.postbox.mediaBox, reference: representations[largestIndex].reference) - - let thumbnailData = Signal { subscriber in - let fetchedDisposable = fetchedThumbnail.start() - let thumbnailDisposable = account.postbox.mediaBox.cachedResourceRepresentation(representations[smallestIndex].representation.resource, representation: CachedPatternWallpaperMaskRepresentation(size: size), complete: false, fetch: true).start(next: { next in - subscriber.putNext(next.size == 0 ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: [])) - - if next.complete, let data = try? Data(contentsOf: URL(fileURLWithPath: next.path), options: .mappedRead) { - accountManager.mediaBox.storeResourceData(representations[smallestIndex].representation.resource.id, data: data) - let _ = accountManager.mediaBox.cachedResourceRepresentation(representations[smallestIndex].representation.resource, representation: CachedPatternWallpaperMaskRepresentation(size: size), complete: false, fetch: true).start() - } - }, error: subscriber.putError, completed: subscriber.putCompletion) - - return ActionDisposable { - fetchedDisposable.dispose() - thumbnailDisposable.dispose() - } - } - - let fullSizeData = Signal<(Data?, Bool), NoError> { subscriber in + let fetchedFullSize = fetchedMediaResource(mediaBox: account.postbox.mediaBox, reference: targetRepresentation.reference) + + let accountFullSizeData = Signal<(Data?, Bool), NoError> { subscriber in let fetchedFullSizeDisposable = fetchedFullSize.start() - let fullSizeDisposable = account.postbox.mediaBox.cachedResourceRepresentation(representations[largestIndex].representation.resource, representation: CachedPatternWallpaperMaskRepresentation(size: size), complete: false, fetch: true).start(next: { next in + let fullSizeDisposable = account.postbox.mediaBox.resourceData(targetRepresentation.representation.resource).start(next: { next in subscriber.putNext((next.size == 0 ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: []), next.complete)) if next.complete, let data = try? Data(contentsOf: URL(fileURLWithPath: next.path), options: .mappedRead) { - accountManager.mediaBox.storeResourceData(representations[largestIndex].representation.resource.id, data: data) - let _ = accountManager.mediaBox.cachedResourceRepresentation(representations[largestIndex].representation.resource, representation: CachedPatternWallpaperMaskRepresentation(size: size), complete: false, fetch: true).start() + accountManager.mediaBox.storeResourceData(targetRepresentation.representation.resource.id, data: data) } }, error: subscriber.putError, completed: subscriber.putCompletion) @@ -377,12 +408,41 @@ private func patternWallpaperDatas(account: Account, accountManager: AccountMana fullSizeDisposable.dispose() } } - - return thumbnailData |> mapToSignal { thumbnailData in - return fullSizeData |> map { (fullSizeData, complete) in - return (thumbnailData, fullSizeData, complete) + + let sharedFullSizeData = Signal<(Data?, Bool), NoError> { subscriber in + let fullSizeDisposable = accountManager.mediaBox.resourceData(targetRepresentation.representation.resource).start(next: { next in + subscriber.putNext((next.size == 0 ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: []), next.complete)) + }, error: subscriber.putError, completed: subscriber.putCompletion) + + return ActionDisposable { + fullSizeDisposable.dispose() } } + + let fullSizeData = combineLatest(accountFullSizeData, sharedFullSizeData) + |> map { accountFullSizeData, sharedFullSizeData -> (Data?, Bool) in + if accountFullSizeData.0 != nil { + return accountFullSizeData + } else { + return sharedFullSizeData + } + } + |> distinctUntilChanged(isEqual: { lhs, rhs in + if lhs.0 == nil && rhs.0 == nil { + return true + } else { + return false + } + }) + |> take(until: { value in + if value.0 != nil { + return SignalTakeAction(passthrough: true, complete: true) + } else { + return SignalTakeAction(passthrough: true, complete: false) + } + }) + + return fullSizeData } } @@ -392,42 +452,28 @@ private func patternWallpaperDatas(account: Account, accountManager: AccountMana } } -public func patternWallpaperImage(account: Account, accountManager: AccountManager, representations: [ImageRepresentationWithReference], mode: PatternWallpaperDrawMode, autoFetchFullSize: Bool = false) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { +public func patternWallpaperImage(account: Account, accountManager: AccountManager, representations: [ImageRepresentationWithReference], mode: PatternWallpaperDrawMode, autoFetchFullSize: Bool = false) -> Signal<((TransformImageArguments) -> DrawingContext?)?, NoError> { return patternWallpaperDatas(account: account, accountManager: accountManager, representations: representations, mode: mode, autoFetchFullSize: autoFetchFullSize) - |> mapToSignal { (thumbnailData, fullSizeData, fullSizeComplete) in - return patternWallpaperImageInternal(thumbnailData: thumbnailData, fullSizeData: fullSizeData, fullSizeComplete: fullSizeComplete, mode: mode) + |> mapToSignal { fullSizeData, fullSizeComplete in + return patternWallpaperImageInternal(fullSizeData: fullSizeData, fullSizeComplete: fullSizeComplete, mode: mode) } } -public func patternWallpaperImageInternal(thumbnailData: Data?, fullSizeData: Data?, fullSizeComplete: Bool, mode: PatternWallpaperDrawMode) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { +private func patternWallpaperImageInternal(fullSizeData: Data?, fullSizeComplete: Bool, mode: PatternWallpaperDrawMode) -> Signal<((TransformImageArguments) -> DrawingContext?)?, NoError> { var prominent = false if case .thumbnail = mode { prominent = true } - var scale: CGFloat = 0.0 + let scale: CGFloat = 0.0 - return .single((thumbnailData, fullSizeData, fullSizeComplete)) - |> map { (thumbnailData, fullSizeData, fullSizeComplete) in - 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.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 .single((fullSizeData, fullSizeComplete)) + |> map { fullSizeData, fullSizeComplete in return { arguments in var scale = scale + if scale.isZero { + scale = arguments.scale ?? UIScreenScale + } let drawingRect = arguments.drawingRect @@ -441,13 +487,26 @@ public func patternWallpaperImageInternal(thumbnailData: Data?, fullSizeData: Da let color = combinedColor.withAlphaComponent(1.0) let intensity = combinedColor.alpha - let context = DrawingContext(size: arguments.drawingSize, scale: fullSizeImage == nil ? 1.0 : scale, clear: !arguments.corners.isEmpty) + let context = DrawingContext(size: arguments.drawingSize, scale: scale, clear: !arguments.corners.isEmpty) context.withFlippedContext { c in c.setBlendMode(.copy) - + if colors.count == 1 { - c.setFillColor(color.cgColor) - c.fill(arguments.drawingRect) + if customArguments.colors[0].alpha.isZero { + c.clear(arguments.drawingRect) + } else { + c.setFillColor(color.cgColor) + c.fill(arguments.drawingRect) + } + } else if colors.count >= 3 { + let image = GradientBackgroundNode.generatePreview(size: CGSize(width: 60.0, height: 60.0), colors: colors) + c.translateBy(x: drawingRect.midX, y: drawingRect.midY) + c.scaleBy(x: 1.0, y: -1.0) + c.translateBy(x: -drawingRect.midX, y: -drawingRect.midY) + c.draw(image.cgImage!, in: drawingRect) + c.translateBy(x: drawingRect.midX, y: drawingRect.midY) + c.scaleBy(x: 1.0, y: -1.0) + c.translateBy(x: -drawingRect.midX, y: -drawingRect.midY) } else { let gradientColors = colors.map { $0.cgColor } as CFArray let delta: CGFloat = 1.0 / (CGFloat(colors.count) - 1.0) @@ -467,44 +526,86 @@ public func patternWallpaperImageInternal(thumbnailData: Data?, fullSizeData: Da 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 + + let overlayImage = generateImage(arguments.drawingRect.size, rotatedContext: { size, c in + c.clear(CGRect(origin: CGPoint(), size: size)) + var image: UIImage? + if let fullSizeData = fullSizeData, let unpackedData = TGGUnzipData(fullSizeData, 2 * 1024 * 1024) { + image = drawSvgImage(unpackedData, CGSize(width: size.width * context.scale, height: size.height * context.scale), .black, .white) + } else if let fullSizeData = fullSizeData { + image = UIImage(data: fullSizeData) } - 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) + + if let customPatternColor = customArguments.customPatternColor, customPatternColor.alpha < 1.0 { + c.setBlendMode(.copy) + c.setFillColor(UIColor.black.cgColor) + c.fill(CGRect(origin: CGPoint(), size: size)) } 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]) + c.setBlendMode(.normal) } + + if let image = image { + var fittedSize = image.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 + } + 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.interpolationQuality = customArguments.preview ? .low : .medium + c.clip(to: fittedRect, mask: image.cgImage!) + + if let customPatternColor = customArguments.customPatternColor { + c.setFillColor(customPatternColor.cgColor) + c.fill(CGRect(origin: CGPoint(), size: arguments.drawingRect.size)) + } else if colors.count >= 3 && customArguments.customPatternColor == nil { + c.setFillColor(UIColor(white: 0.0, alpha: 0.5).cgColor) + c.fill(CGRect(origin: CGPoint(), size: arguments.drawingRect.size)) + } else if colors.count == 1 { + c.setFillColor(customArguments.customPatternColor?.cgColor ?? patternColor(for: color, intensity: intensity, prominent: prominent).cgColor) + c.fill(CGRect(origin: CGPoint(), size: arguments.drawingRect.size)) + } 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]) + } + } + }) + if let customPatternColor = customArguments.customPatternColor, customPatternColor.alpha < 1.0 { + c.setBlendMode(.normal) + } else if customArguments.colors.count == 1 && customArguments.colors[0].alpha.isZero { + c.setBlendMode(.normal) + } else { + c.setBlendMode(.softLight) + } + if let overlayImage = overlayImage { + if customArguments.bakePatternAlpha != 1.0 { + c.setAlpha(customArguments.bakePatternAlpha) + } + c.translateBy(x: drawingRect.midX, y: drawingRect.midY) + c.scaleBy(x: 1.0, y: -1.0) + c.translateBy(x: -drawingRect.midX, y: -drawingRect.midY) + c.draw(overlayImage.cgImage!, in: drawingRect) + c.translateBy(x: drawingRect.midX, y: drawingRect.midY) + c.scaleBy(x: 1.0, y: -1.0) + c.translateBy(x: -drawingRect.midX, y: -drawingRect.midY) + c.setAlpha(1.0) } } addCorners(context, arguments: arguments) @@ -550,6 +651,55 @@ public func solidColorImage(_ color: UIColor) -> Signal<(TransformImageArguments }) } +public func drawWallpaperGradientImage(_ colors: [UIColor], rotation: Int32? = nil, context: CGContext, size: CGSize) { + guard !colors.isEmpty else { + return + } + guard colors.count > 1 else { + context.setFillColor(colors[0].cgColor) + context.fill(CGRect(origin: CGPoint(), size: size)) + return + } + + let drawingRect = CGRect(origin: CGPoint(), size: size) + + let c = context + + if colors.count >= 3 { + let image = GradientBackgroundNode.generatePreview(size: CGSize(width: 60.0, height: 60.0), colors: colors) + c.translateBy(x: drawingRect.midX, y: drawingRect.midY) + c.scaleBy(x: 1.0, y: -1.0) + c.translateBy(x: -drawingRect.midX, y: -drawingRect.midY) + c.draw(image.cgImage!, in: drawingRect) + c.translateBy(x: drawingRect.midX, y: drawingRect.midY) + c.scaleBy(x: 1.0, y: -1.0) + c.translateBy(x: -drawingRect.midX, y: -drawingRect.midY) + } else { + 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.saveGState() + 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) + } + + c.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: drawingRect.height), options: [.drawsBeforeStartLocation, .drawsAfterEndLocation]) + + if rotation != nil { + c.restoreGState() + } + } +} + public func gradientImage(_ colors: [UIColor], rotation: Int32? = nil) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { guard !colors.isEmpty else { return .complete() @@ -563,25 +713,38 @@ public func gradientImage(_ colors: [UIColor], rotation: Int32? = nil) -> Signal } return .single({ arguments in let context = DrawingContext(size: arguments.drawingSize, clear: !arguments.corners.isEmpty) + + let drawingRect = arguments.drawingRect 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 colors.count >= 3 { + let image = GradientBackgroundNode.generatePreview(size: CGSize(width: 60.0, height: 60.0), colors: colors) + c.translateBy(x: drawingRect.midX, y: drawingRect.midY) + c.scaleBy(x: 1.0, y: -1.0) + c.translateBy(x: -drawingRect.midX, y: -drawingRect.midY) + c.draw(image.cgImage!, in: drawingRect) + c.translateBy(x: drawingRect.midX, y: drawingRect.midY) + c.scaleBy(x: 1.0, y: -1.0) + c.translateBy(x: -drawingRect.midX, y: -drawingRect.midY) + } else { + let gradientColors = colors.map { $0.withAlphaComponent(1.0).cgColor } as CFArray + let delta: CGFloat = 1.0 / (CGFloat(colors.count) - 1.0) - 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) + 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]) } - - 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) @@ -601,7 +764,7 @@ private func builtinWallpaperData() -> Signal { } |> runOn(Queue.concurrentDefaultQueue()) } -public func settingsBuiltinWallpaperImage(account: Account) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { +public func settingsBuiltinWallpaperImage(account: Account, thumbnail: Bool = false) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { return builtinWallpaperData() |> map { fullSizeImage in return { arguments in let context = DrawingContext(size: arguments.drawingSize, clear: true) @@ -619,6 +782,11 @@ public func settingsBuiltinWallpaperImage(account: Account) -> Signal<(Transform context.withFlippedContext { c in c.setBlendMode(.copy) + if thumbnail { + c.translateBy(x: fittedRect.midX, y: fittedRect.midY) + c.scaleBy(x: 3.4, y: 3.4) + c.translateBy(x: -fittedRect.midX, y: -fittedRect.midY) + } if let fullSizeImage = fullSizeImage.cgImage { c.interpolationQuality = .medium drawImage(context: c, image: fullSizeImage, orientation: .up, in: fittedRect) @@ -759,25 +927,39 @@ public func drawThemeImage(context c: CGContext, theme: PresentationTheme, wallp 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) - - if let image = wallpaperImage, let cgImage = image.cgImage { - 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 .gradient(_, colors, _): + if colors.count >= 3 { + let image = GradientBackgroundNode.generatePreview(size: CGSize(width: 60.0, height: 60.0), colors: colors.map(UIColor.init(rgb:))) + c.draw(image.cgImage!, in: drawingRect) + } else if colors.count >= 2 { + let gradientColors = colors.map({ UIColor(rgb: $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]) + } else if colors.count >= 1 { + let gradientColors = [UIColor(rgb: colors[0]), UIColor(rgb: colors[0])] 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 let .file(file): + if file.isPattern, let intensity = file.settings.intensity, intensity < 0 { + c.setFillColor(UIColor.black.cgColor) + c.fill(CGRect(origin: CGPoint(), size: size)) + } else { + if let image = wallpaperImage, let cgImage = image.cgImage { + 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)) + } + } + c.setFillColor(theme.chatList.backgroundColor.cgColor) default: break } - c.setFillColor(theme.rootController.navigationBar.backgroundColor.cgColor) + c.setFillColor(theme.rootController.navigationBar.opaqueBackgroundColor.cgColor) 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) @@ -960,31 +1142,36 @@ public func themeImage(account: Account, accountManager: AccountManager, source: } } 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)) + theme = .single((makePresentationTheme(mediaBox: accountManager.mediaBox, themeReference: .builtin(PresentationBuiltinThemeReference(baseTheme: settings.baseTheme)), accentColor: UIColor(argb: settings.accentColor), backgroundColors: [], bubbleColors: settings.messageColors.flatMap { (UIColor(argb: $0.top), UIColor(argb: $0.bottom)) }, wallpaper: settings.wallpaper, serviceBackgroundColor: nil, preview: false), nil)) + } + + enum WallpaperImage { + case image(UIImage) + case pattern(data: Data, colors: [UInt32], intensity: Int32) } let data = theme - |> mapToSignal { (theme, thumbnailData) -> Signal<(PresentationTheme?, UIImage?, Data?), NoError> in + |> mapToSignal { (theme, thumbnailData) -> Signal<(PresentationTheme?, WallpaperImage?, 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 + |> mapToSignal { wallpaper -> Signal<(PresentationTheme?, WallpaperImage?, 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, progressiveSizes: []), reference: .wallpaper(wallpaper: .slug(file.slug), resource: file.file.resource))) + convertedRepresentations.append(ImageRepresentationWithReference(representation: TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 100, height: 100), resource: file.file.resource, progressiveSizes: [], immediateThumbnailData: nil), 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 + |> mapToSignal { _, fullSizeData, complete -> Signal<(PresentationTheme?, WallpaperImage?, Data?), 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, 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) + if wallpaper.wallpaper.isPattern, !file.settings.colors.isEmpty, let intensity = file.settings.intensity { + return accountManager.mediaBox.resourceData(file.file.resource) |> 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)) + if data.complete, let imageData = try? Data(contentsOf: URL(fileURLWithPath: data.path)) { + return .single((theme, .pattern(data: imageData, colors: file.settings.colors, intensity: intensity), thumbnailData)) } else { return .complete() } @@ -993,13 +1180,13 @@ public func themeImage(account: Account, accountManager: AccountManager, source: return accountManager.mediaBox.cachedResourceRepresentation(file.file.resource, representation: CachedBlurredWallpaperRepresentation(), 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)) + return .single((theme, .image(image), thumbnailData)) } else { return .complete() } } } else if let image = UIImage(data: fullSizeData) { - return .single((theme, image, thumbnailData)) + return .single((theme, .image(image), thumbnailData)) } else { return .complete() } @@ -1067,8 +1254,27 @@ public func themeImage(account: Account, accountManager: AccountManager, source: if let theme = theme { context.withFlippedContext { c in c.setBlendMode(.normal) - - drawThemeImage(context: c, theme: theme, wallpaperImage: wallpaperImage, size: arguments.drawingSize) + + switch wallpaperImage { + case let .image(image): + drawThemeImage(context: c, theme: theme, wallpaperImage: image, size: arguments.drawingSize) + case let .pattern(data, colors, intensity): + let wallpaperImage = generateImage(arguments.drawingSize, rotatedContext: { size, context in + drawWallpaperGradientImage(colors.map(UIColor.init(rgb:)), context: context, size: size) + if let unpackedData = TGGUnzipData(data, 2 * 1024 * 1024), let image = drawSvgImage(unpackedData, arguments.drawingSize, .clear, .black) { + context.setBlendMode(.softLight) + context.setAlpha(abs(CGFloat(intensity)) / 100.0) + context.draw(image.cgImage!, in: CGRect(origin: CGPoint(), size: arguments.drawingSize)) + } else if let image = UIImage(data: data) { + context.setBlendMode(.softLight) + context.setAlpha(abs(CGFloat(intensity)) / 100.0) + context.draw(image.cgImage!, in: CGRect(origin: CGPoint(), size: arguments.drawingSize)) + } + }) + drawThemeImage(context: c, theme: theme, wallpaperImage: wallpaperImage, size: arguments.drawingSize) + case .none: + drawThemeImage(context: c, theme: theme, wallpaperImage: nil, size: arguments.drawingSize) + } c.setStrokeColor(theme.rootController.navigationBar.separatorColor.cgColor) c.setLineWidth(2.0) @@ -1084,8 +1290,8 @@ public func themeImage(account: Account, accountManager: AccountManager, source: } 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 colorsSignal: Signal<((UIColor, UIColor?, [UInt32]), (UIColor, UIColor), (UIColor, UIColor), UIImage?, Int32?), NoError> + if false, case let .builtin(theme) = theme { let incomingColor: UIColor let outgoingColor: (UIColor, UIColor) var accentColor = color?.color @@ -1097,8 +1303,10 @@ public func themeIconImage(account: Account, accountManager: AccountManager, the 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) } + topBackgroundColor = file.settings.colors.first.flatMap { UIColor(rgb: $0) } ?? UIColor(rgb: 0xd6e2ee) + if file.settings.colors.count >= 2 { + bottomBackgroundColor = UIColor(rgb: file.settings.colors[1]) + } } else { if let bubbleColors = bubbleColors { topBackgroundColor = UIColor(rgb: 0xd6e2ee) @@ -1116,7 +1324,20 @@ public func themeIconImage(account: Account, accountManager: AccountManager, the outgoingColor = (bubbleColor, bubbleColor) } } else { - topBackgroundColor = UIColor(rgb: 0xd6e2ee) + if let wallpaper = wallpaper, case let .file(file) = wallpaper { + topBackgroundColor = file.settings.colors.first.flatMap { UIColor(rgb: $0) } ?? UIColor(rgb: 0xd6e2ee) + if file.settings.colors.count >= 2 { + bottomBackgroundColor = UIColor(rgb: file.settings.colors[1]) + } + } else if let wallpaper = wallpaper, case let .gradient(_, colors, _) = wallpaper { + topBackgroundColor = colors.first.flatMap { UIColor(rgb: $0) } ?? UIColor(rgb: 0xd6e2ee) + if colors.count >= 2 { + bottomBackgroundColor = UIColor(rgb: colors[1]) + } + } else { + topBackgroundColor = defaultBuiltinWallpaperGradientColors[0] + bottomBackgroundColor = defaultBuiltinWallpaperGradientColors[1] + } outgoingColor = (UIColor(rgb: 0xe1ffc7), UIColor(rgb: 0xe1ffc7)) } case .day: @@ -1140,42 +1361,61 @@ public func themeIconImage(account: Account, accountManager: AccountManager, the let accentBubbleColor = accentColor.withMultiplied(hue: 1.019, saturation: 0.731, brightness: 0.59) outgoingColor = bubbleColors ?? (accentBubbleColor, accentBubbleColor) } - + + var colors: [UInt32] = [] var rotation: Int32? if let wallpaper = wallpaper { switch wallpaper { case let .color(color): + colors = [color] topBackgroundColor = UIColor(rgb: color) - case let .gradient(topColor, bottomColor, settings): - topBackgroundColor = UIColor(rgb: topColor) - bottomBackgroundColor = UIColor(rgb: bottomColor) + case let .gradient(_, colorsValue, settings): + colors = colorsValue + if colors.count >= 1 { + topBackgroundColor = UIColor(rgb: colors[0]) + } + if colors.count >= 2 { + bottomBackgroundColor = UIColor(rgb: colors[1]) + } 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) } + if file.isPattern, let intensity = file.settings.intensity, intensity < 0 { + colors = [0x000000] + topBackgroundColor = .black + bottomBackgroundColor = .black + } else { + colors = file.settings.colors + if !file.settings.colors.isEmpty { + topBackgroundColor = UIColor(rgb: file.settings.colors[0]) + if file.settings.colors.count >= 2 { + bottomBackgroundColor = UIColor(rgb: file.settings.colors[1]) + } + } + rotation = file.settings.rotation } - rotation = file.settings.rotation default: + colors = [0xd6e2ee] topBackgroundColor = UIColor(rgb: 0xd6e2ee) } + } else { + colors = defaultBuiltinWallpaperGradientColors.map(\.rgb) } - colorsSignal = .single(((topBackgroundColor, bottomBackgroundColor), (incomingColor, incomingColor), outgoingColor, nil, rotation)) + colorsSignal = .single(((topBackgroundColor, bottomBackgroundColor, colors), (incomingColor, incomingColor), outgoingColor, nil, rotation)) } else { - var resource: MediaResource? var reference: MediaResourceReference? - var defaultWallpaper: TelegramWallpaper? if case let .local(theme) = theme { 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) } - + let themeSignal: Signal - if case let .cloud(theme) = theme, let settings = theme.theme.settings { + if case let .builtin(theme) = theme { + themeSignal = .single(makeDefaultPresentationTheme(reference: theme, serviceBackgroundColor: nil)) + } else 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) + let theme = makePresentationTheme(mediaBox: accountManager.mediaBox, themeReference: .builtin(PresentationBuiltinThemeReference(baseTheme: settings.baseTheme)), accentColor: UIColor(argb: settings.accentColor), backgroundColors: [], bubbleColors: settings.messageColors.flatMap { (UIColor(argb: $0.top), UIColor(argb: $0.bottom)) }, wallpaper: settings.wallpaper, serviceBackgroundColor: nil, preview: false) subscriber.putNext(theme) subscriber.putCompletion() @@ -1195,42 +1435,57 @@ public func themeIconImage(account: Account, accountManager: AccountManager, the } colorsSignal = themeSignal - |> mapToSignal { theme -> Signal<((UIColor, UIColor?), (UIColor, UIColor), (UIColor, UIColor), UIImage?, Int32?), NoError> in + |> mapToSignal { theme -> Signal<((UIColor, UIColor?, [UInt32]), (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 wallpaperSignal: Signal<((UIColor, UIColor?, [UInt32]), (UIColor, UIColor), (UIColor, UIColor), UIImage?, Int32?), NoError> = .complete() var rotation: Int32? - var backgroundColor: (UIColor, UIColor?) + var backgroundColor: (UIColor, UIColor?, [UInt32]) 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 { + let wallpaper = wallpaper ?? theme.chat.defaultWallpaper + switch wallpaper { case .builtin: - backgroundColor = (UIColor(rgb: 0xd6e2ee), nil) + 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)) + backgroundColor = (UIColor(rgb: color), nil, []) + case let .gradient(_, colors, settings): + if colors.count >= 2 { + backgroundColor = (UIColor(rgb: colors[0]), UIColor(rgb: colors[1]), colors) + } else { + backgroundColor = (.white, nil, []) + } rotation = settings.rotation case .image: - backgroundColor = (.black, nil) + 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) }) + if file.isPattern, let intensity = file.settings.intensity, intensity < 0 { + backgroundColor = (.black, nil, []) + } else if !file.settings.colors.isEmpty { + var bottomColor: UIColor? + if file.settings.colors.count >= 2 { + bottomColor = UIColor(rgb: file.settings.colors[1]) + } + backgroundColor = (UIColor(rgb: file.settings.colors[0]), bottomColor, file.settings.colors) } else { - backgroundColor = (theme.chatList.backgroundColor, nil) + 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) }) + if !file.settings.colors.isEmpty { + var bottomColor: UIColor? + if file.settings.colors.count >= 2 { + bottomColor = UIColor(rgb: file.settings.colors[1]) + } + effectiveBackgroundColor = (UIColor(rgb: file.settings.colors[0]), bottomColor, file.settings.colors) } var convertedRepresentations: [ImageRepresentationWithReference] = [] - convertedRepresentations.append(ImageRepresentationWithReference(representation: TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 100, height: 100), resource: file.file.resource, progressiveSizes: []), reference: .wallpaper(wallpaper: .slug(file.slug), resource: file.file.resource))) + convertedRepresentations.append(ImageRepresentationWithReference(representation: TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 100, height: 100), resource: file.file.resource, progressiveSizes: [], immediateThumbnailData: nil), 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 + |> mapToSignal { _, fullSizeData, complete -> Signal<((UIColor, UIColor?, [UInt32]), (UIColor, UIColor), (UIColor, UIColor), UIImage?, Int32?), NoError> in guard complete, let fullSizeData = fullSizeData else { return .complete() } @@ -1238,9 +1493,10 @@ public func themeIconImage(account: Account, accountManager: AccountManager, the 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 + if !file.settings.colors.isEmpty, let intensity = file.settings.intensity { + if intensity < 0 { + return .single(((.black, nil, []), incomingColor, outgoingColor, nil, rotation)) + } else { return .single((effectiveBackgroundColor, incomingColor, outgoingColor, nil, rotation)) } } else { @@ -1280,7 +1536,10 @@ public func themeIconImage(account: Account, accountManager: AccountManager, the let drawingRect = arguments.drawingRect context.withContext { c in - if let secondBackgroundColor = colors.0.1 { + if colors.0.2.count >= 3 { + let image = GradientBackgroundNode.generatePreview(size: CGSize(width: 60.0, height: 60.0), colors: colors.0.2.map(UIColor.init(rgb:))) + c.draw(image.cgImage!, in: drawingRect) + } else 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() @@ -1339,3 +1598,100 @@ public func themeIconImage(account: Account, accountManager: AccountManager, the } } } + +public func wallpaperThumbnail(account: Account, accountManager: AccountManager, fileReference: FileMediaReference, wallpaper: TelegramWallpaper, synchronousLoad: Bool) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { + switch wallpaper { + case let .file(_, _, _, _, _, _, _, file, settings): + guard let thumbnail = smallestImageRepresentation(file.previewRepresentations) else { + return .single({ _ in nil }) + } + let signal: Signal = Signal { subscriber in + let data = account.postbox.mediaBox.resourceData(thumbnail.resource).start(next: { data in + if data.complete { + if let fileData = try? Data(contentsOf: URL(fileURLWithPath: data.path)) { + subscriber.putNext(fileData) + } + } + }) + let fetch = fetchedMediaResource(mediaBox: account.postbox.mediaBox, reference: fileReference.resourceReference(thumbnail.resource)).start() + + return ActionDisposable { + data.dispose() + fetch.dispose() + } + } + return signal + |> map { thumbnailData in + return { arguments in + let drawingRect = arguments.drawingRect + + var thumbnailImage: CGImage? + if let thumbnailData = thumbnailData { + if let imageSource = CGImageSourceCreateWithData(thumbnailData as CFData, nil), let image = CGImageSourceCreateImageAtIndex(imageSource, 0, nil) { + thumbnailImage = image + } + } + + let context = DrawingContext(size: arguments.boundingSize, clear: true) + + context.withFlippedContext { c in + let colors = settings.colors.map(UIColor.init(rgb:)) + + if colors.count == 1 { + c.setFillColor(colors[0].cgColor) + c.fill(arguments.drawingRect) + } else if settings.colors.count >= 3 { + let image = GradientBackgroundNode.generatePreview(size: CGSize(width: 60.0, height: 60.0), colors: colors) + c.translateBy(x: drawingRect.midX, y: drawingRect.midY) + c.scaleBy(x: 1.0, y: -1.0) + c.translateBy(x: -drawingRect.midX, y: -drawingRect.midY) + c.draw(image.cgImage!, in: drawingRect) + c.translateBy(x: drawingRect.midX, y: drawingRect.midY) + c.scaleBy(x: 1.0, y: -1.0) + c.translateBy(x: -drawingRect.midX, y: -drawingRect.midY) + } else if settings.colors.count >= 2 { + let gradientColors = settings.colors.map { UIColor(rgb: $0).cgColor } as CFArray + let delta: CGFloat = 1.0 / (CGFloat(settings.colors.count) - 1.0) + + var locations: [CGFloat] = [] + for i in 0 ..< settings.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(settings.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() + } + + if let thumbnailImage = thumbnailImage { + let fittedSize = CGSize(width: thumbnailImage.width, height: thumbnailImage.height).aspectFilled(arguments.boundingSize) + let fittedRect = CGRect(origin: CGPoint(x: (arguments.boundingSize.width - fittedSize.width) / 2.0, y: (arguments.boundingSize.height - fittedSize.height) / 2.0), size: fittedSize) + + c.clip(to: fittedRect, mask: thumbnailImage) + + c.setBlendMode(.softLight) + + if UIColor.average(of: colors).hsb.b > 0.3 { + c.setFillColor(UIColor(white: 0.0, alpha: 0.6).cgColor) + } else { + c.setFillColor(UIColor(white: 1.0, alpha: 0.6).cgColor) + } + c.fill(fittedRect) + } + } + + addCorners(context, arguments: arguments) + + return context + } + } + default: + return .single({ _ in nil }) + } +} diff --git a/submodules/WatchBridge/Sources/WatchBridge.swift b/submodules/WatchBridge/Sources/WatchBridge.swift index e01c5941be..08a9ccc38c 100644 --- a/submodules/WatchBridge/Sources/WatchBridge.swift +++ b/submodules/WatchBridge/Sources/WatchBridge.swift @@ -9,11 +9,11 @@ import PhoneNumberFormat func makePeerIdFromBridgeIdentifier(_ identifier: Int64) -> PeerId? { if identifier < 0 && identifier > Int32.min { - return PeerId(namespace: Namespaces.Peer.CloudGroup, id: Int32(clamping: -identifier)) + return PeerId(namespace: Namespaces.Peer.CloudGroup, id: PeerId.Id._internalFromInt32Value(Int32(clamping: -identifier))) } else if identifier < Int64(Int32.min) * 2 && identifier > Int64(Int32.min) * 3 { - return PeerId(namespace: Namespaces.Peer.CloudChannel, id: Int32(clamping: Int64(Int32.min) &* 2 &- identifier)) + return PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt32Value(Int32(clamping: Int64(Int32.min) &* 2 &- identifier))) } else if identifier > 0 && identifier < Int32.max { - return PeerId(namespace: Namespaces.Peer.CloudUser, id: Int32(clamping: identifier)) + return PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt32Value(Int32(clamping: identifier))) } else { return nil } @@ -22,11 +22,11 @@ func makePeerIdFromBridgeIdentifier(_ identifier: Int64) -> PeerId? { func makeBridgeIdentifier(_ peerId: PeerId) -> Int64 { switch peerId.namespace { case Namespaces.Peer.CloudGroup: - return -Int64(peerId.id) + return -Int64(peerId.id._internalGetInt32Value()) case Namespaces.Peer.CloudChannel: - return Int64(Int32.min) * 2 - Int64(peerId.id) + return Int64(Int32.min) * 2 - Int64(peerId.id._internalGetInt32Value()) default: - return Int64(peerId.id) + return Int64(peerId.id._internalGetInt32Value()) } } diff --git a/submodules/WatchBridge/Sources/WatchCommunicationManager.swift b/submodules/WatchBridge/Sources/WatchCommunicationManager.swift index 18b3f617ad..629b456a41 100644 --- a/submodules/WatchBridge/Sources/WatchCommunicationManager.swift +++ b/submodules/WatchBridge/Sources/WatchCommunicationManager.swift @@ -89,7 +89,7 @@ public final class WatchCommunicationManager { } if let context = appContext { strongSelf.accountContext.set(.single(context.context)) - strongSelf.server.setAuthorized(true, userId: context.context.account.peerId.id) + strongSelf.server.setAuthorized(true, userId: context.context.account.peerId.id._internalGetInt32Value()) strongSelf.server.setMicAccessAllowed(false) strongSelf.server.pushContext() strongSelf.server.setMicAccessAllowed(true) diff --git a/submodules/WatchBridge/Sources/WatchRequestHandlers.swift b/submodules/WatchBridge/Sources/WatchRequestHandlers.swift index 33c95fe481..81249019eb 100644 --- a/submodules/WatchBridge/Sources/WatchRequestHandlers.swift +++ b/submodules/WatchBridge/Sources/WatchRequestHandlers.swift @@ -123,7 +123,7 @@ final class WatchChatMessagesHandler: WatchRequestHandler { |> mapToSignal({ context -> Signal in if let context = context { let messageId = MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: args.messageId) - return applyMaxReadIndexInteractively(postbox: context.account.postbox, stateManager: context.account.stateManager, index: MessageIndex(id: messageId, timestamp: 0)) + return context.engine.messages.applyMaxReadIndexInteractively(index: MessageIndex(id: messageId, timestamp: 0)) } else { return .complete() } @@ -144,7 +144,7 @@ final class WatchChatMessagesHandler: WatchRequestHandler { |> take(1) |> mapToSignal({ context -> Signal<(Message, PresentationData)?, NoError> in if let context = context { - let messageSignal = downloadMessage(postbox: context.account.postbox, network: context.account.network, messageId: MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: args.messageId)) + let messageSignal = context.engine.messages.downloadMessage(messageId: MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: args.messageId)) |> map { message -> (Message, PresentationData)? in if let message = message { return (message, context.sharedContext.currentPresentationData.with { $0 }) @@ -200,17 +200,17 @@ final class WatchSendMessageHandler: WatchRequestHandler { if args.replyToMid != 0, let peerId = peerId { replyMessageId = MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: args.replyToMid) } - messageSignal = .single((.message(text: args.text, attributes: [], mediaReference: nil, replyToMessageId: replyMessageId, localGroupingKey: nil), peerId)) + messageSignal = .single((.message(text: args.text, attributes: [], mediaReference: nil, replyToMessageId: replyMessageId, localGroupingKey: nil, correlationId: nil), peerId)) } else if let args = subscription as? TGBridgeSendLocationMessageSubscription, let location = args.location { let peerId = makePeerIdFromBridgeIdentifier(args.peerId) let map = TelegramMediaMap(latitude: location.latitude, longitude: location.longitude, heading: nil, accuracyRadius: nil, geoPlace: nil, venue: makeVenue(from: location.venue), liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil) - messageSignal = .single((.message(text: "", attributes: [], mediaReference: .standalone(media: map), replyToMessageId: nil, localGroupingKey: nil), peerId)) + messageSignal = .single((.message(text: "", attributes: [], mediaReference: .standalone(media: map), replyToMessageId: nil, localGroupingKey: nil, correlationId: nil), peerId)) } else if let args = subscription as? TGBridgeSendStickerMessageSubscription { let peerId = makePeerIdFromBridgeIdentifier(args.peerId) messageSignal = mediaForSticker(documentId: args.document.documentId, account: context.account) |> map({ media -> (EnqueueMessage?, PeerId?) in if let media = media { - return (.message(text: "", attributes: [], mediaReference: .standalone(media: media), replyToMessageId: nil, localGroupingKey: nil), peerId) + return (.message(text: "", attributes: [], mediaReference: .standalone(media: media), replyToMessageId: nil, localGroupingKey: nil, correlationId: nil), peerId) } else { return (nil, nil) } @@ -218,7 +218,7 @@ final class WatchSendMessageHandler: WatchRequestHandler { } else if let args = subscription as? TGBridgeSendForwardedMessageSubscription { let peerId = makePeerIdFromBridgeIdentifier(args.targetPeerId) if let forwardPeerId = makePeerIdFromBridgeIdentifier(args.peerId) { - messageSignal = .single((.forward(source: MessageId(peerId: forwardPeerId, namespace: Namespaces.Message.Cloud, id: args.messageId), grouping: .none, attributes: []), peerId)) + messageSignal = .single((.forward(source: MessageId(peerId: forwardPeerId, namespace: Namespaces.Message.Cloud, id: args.messageId), grouping: .none, attributes: [], correlationId: nil), peerId)) } } @@ -404,7 +404,7 @@ final class WatchMediaHandler: WatchRequestHandler { if let imageData = imageData { return imageData |> map { data -> UIImage? in - if let data = data, let image = generateImage(targetSize, contextGenerator: { size, context -> Void in + if let (data, _) = data, let image = generateImage(targetSize, contextGenerator: { size, context -> Void in if let imageSource = CGImageSourceCreateWithData(data as CFData, nil), let dataImage = CGImageSourceCreateImageAtIndex(imageSource, 0, nil) { context.setBlendMode(.copy) context.draw(dataImage, in: CGRect(origin: CGPoint(), size: targetSize)) @@ -728,7 +728,7 @@ final class WatchAudioHandler: WatchRequestHandler { replyMessageId = MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: replyToMid) } - let _ = enqueueMessages(account: context.account, peerId: peerId, messages: [.message(text: "", attributes: [], mediaReference: .standalone(media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: randomId), partialReference: nil, resource: resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "audio/ogg", size: data.count, attributes: [.Audio(isVoice: true, duration: Int(duration), title: nil, performer: nil, waveform: nil)])), replyToMessageId: replyMessageId, localGroupingKey: nil)]).start() + let _ = enqueueMessages(account: context.account, peerId: peerId, messages: [.message(text: "", attributes: [], mediaReference: .standalone(media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: randomId), partialReference: nil, resource: resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "audio/ogg", size: data.count, attributes: [.Audio(isVoice: true, duration: Int(duration), title: nil, performer: nil, waveform: nil)])), replyToMessageId: replyMessageId, localGroupingKey: nil, correlationId: nil)]).start() } }) } else { @@ -749,13 +749,13 @@ final class WatchLocationHandler: WatchRequestHandler { |> take(1) |> mapToSignal({ context -> Signal<[ChatContextResultMessage], NoError> in if let context = context { - return resolvePeerByName(account: context.account, name: "foursquare") + return context.engine.peers.resolvePeerByName(name: "foursquare") |> take(1) |> mapToSignal { peerId -> Signal in guard let peerId = peerId else { return .single(nil) } - return requestChatContextResults(account: context.account, botId: peerId, peerId: context.account.peerId, query: "", location: .single((args.coordinate.latitude, args.coordinate.longitude)), offset: "") + return context.engine.messages.requestChatContextResults(botId: peerId, peerId: context.account.peerId, query: "", location: .single((args.coordinate.latitude, args.coordinate.longitude)), offset: "") |> map { results -> ChatContextResultCollection? in return results?.results } @@ -841,9 +841,9 @@ final class WatchPeerSettingsHandler: WatchRequestHandler { var signal: Signal? if let args = subscription as? TGBridgePeerUpdateNotificationSettingsSubscription, let peerId = makePeerIdFromBridgeIdentifier(args.peerId) { - signal = togglePeerMuted(account: context.account, peerId: peerId) + signal = context.engine.peers.togglePeerMuted(peerId: peerId) } else if let args = subscription as? TGBridgePeerUpdateBlockStatusSubscription, let peerId = makePeerIdFromBridgeIdentifier(args.peerId) { - signal = requestUpdatePeerIsBlocked(account: context.account, peerId: peerId, isBlocked: args.blocked) + signal = context.engine.privacy.requestUpdatePeerIsBlocked(peerId: peerId, isBlocked: args.blocked) } if let signal = signal { diff --git a/submodules/WebPBinding/Sources/UIImage+WebP.m b/submodules/WebPBinding/Sources/UIImage+WebP.m index 3f541a37c7..c4a07eed72 100644 --- a/submodules/WebPBinding/Sources/UIImage+WebP.m +++ b/submodules/WebPBinding/Sources/UIImage+WebP.m @@ -19,7 +19,7 @@ const struct { int width, height; } targetContextSize = { width, height}; - size_t targetBytesPerRow = ((4 * (int)targetContextSize.width) + 15) & (~15); + size_t targetBytesPerRow = ((4 * (int)targetContextSize.width) + 31) & (~31); void *targetMemory = malloc((int)(targetBytesPerRow * targetContextSize.height)); diff --git a/submodules/WebSearchUI/Sources/LegacyWebSearchGallery.swift b/submodules/WebSearchUI/Sources/LegacyWebSearchGallery.swift index f30273f4a3..d45a671e63 100644 --- a/submodules/WebSearchUI/Sources/LegacyWebSearchGallery.swift +++ b/submodules/WebSearchUI/Sources/LegacyWebSearchGallery.swift @@ -261,9 +261,9 @@ func legacyWebSearchItem(account: Account, result: ChatContextResult) -> LegacyW var representations: [TelegramMediaImageRepresentation] = [] if let thumbnailResource = thumbnailResource, let thumbnailDimensions = thumbnailDimensions { - representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(thumbnailDimensions), resource: thumbnailResource, progressiveSizes: [])) + representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(thumbnailDimensions), resource: thumbnailResource, progressiveSizes: [], immediateThumbnailData: nil)) } - representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(imageDimensions), resource: imageResource, progressiveSizes: [])) + representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(imageDimensions), resource: imageResource, progressiveSizes: [], immediateThumbnailData: 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 @@ -466,7 +466,7 @@ public func legacyEnqueueWebSearchMessages(_ selectionState: TGMediaSelectionCon } } - return legacyAssetPickerItemGenerator()(dict, nil, nil, nil) as Any + return legacyAssetPickerItemGenerator()(dict, nil, nil, nil, nil) as Any } else { return SSignal.complete() } diff --git a/submodules/WebSearchUI/Sources/WebSearchBadgeNode.swift b/submodules/WebSearchUI/Sources/WebSearchBadgeNode.swift index 36dd99037d..c52090e332 100644 --- a/submodules/WebSearchUI/Sources/WebSearchBadgeNode.swift +++ b/submodules/WebSearchUI/Sources/WebSearchBadgeNode.swift @@ -12,7 +12,7 @@ final class WebSearchBadgeNode: ASDisplayNode { private let textNode: ASTextNode private let backgroundNode: ASImageNode - private let font: UIFont = Font.with(size: 17.0, design: .round, traits: [.bold]) + private let font: UIFont = Font.with(size: 17.0, design: .round, weight: .bold) var text: String = "" { didSet { diff --git a/submodules/WebSearchUI/Sources/WebSearchController.swift b/submodules/WebSearchUI/Sources/WebSearchController.swift index 2e872a9aa8..3fc012211b 100644 --- a/submodules/WebSearchUI/Sources/WebSearchController.swift +++ b/submodules/WebSearchUI/Sources/WebSearchController.swift @@ -10,8 +10,8 @@ import LegacyComponents import TelegramUIPreferences import AccountContext -public func requestContextResults(account: Account, botId: PeerId, query: String, peerId: PeerId, offset: String = "", existingResults: ChatContextResultCollection? = nil, incompleteResults: Bool = false, staleCachedResults: Bool = false, limit: Int = 60) -> Signal { - return requestChatContextResults(account: account, botId: botId, peerId: peerId, query: query, offset: offset, incompleteResults: incompleteResults, staleCachedResults: staleCachedResults) +public func requestContextResults(context: AccountContext, botId: PeerId, query: String, peerId: PeerId, offset: String = "", existingResults: ChatContextResultCollection? = nil, incompleteResults: Bool = false, staleCachedResults: Bool = false, limit: Int = 60) -> Signal { + return context.engine.messages.requestChatContextResults(botId: botId, peerId: peerId, query: query, offset: offset, incompleteResults: incompleteResults, staleCachedResults: staleCachedResults) |> `catch` { error -> Signal in return .single(nil) } @@ -40,7 +40,7 @@ public func requestContextResults(account: Account, botId: PeerId, query: String updated = true } if let collection = collection, collection.results.count < limit, let nextOffset = collection.nextOffset, updated { - let nextResults = requestContextResults(account: account, botId: botId, query: query, peerId: peerId, offset: nextOffset, existingResults: collection, limit: limit) + let nextResults = requestContextResults(context: context, botId: botId, query: query, peerId: peerId, offset: nextOffset, existingResults: collection, limit: limit) if collection.results.count > 10 { return .single(RequestChatContextResultsResult(results: collection, isStale: resultsStruct?.isStale ?? false)) |> then(nextResults) @@ -172,7 +172,7 @@ public final class WebSearchController: ViewController { self.interfaceState = self.interfaceState.withUpdatedQuery(query) } - super.init(navigationBarPresentationData: NavigationBarPresentationData(theme: NavigationBarTheme(rootControllerTheme: presentationData.theme).withUpdatedSeparatorColor(presentationData.theme.rootController.navigationBar.backgroundColor), strings: NavigationBarStrings(presentationStrings: presentationData.strings))) + super.init(navigationBarPresentationData: NavigationBarPresentationData(theme: NavigationBarTheme(rootControllerTheme: presentationData.theme).withUpdatedSeparatorColor(presentationData.theme.rootController.navigationBar.opaqueBackgroundColor), strings: NavigationBarStrings(presentationStrings: presentationData.strings))) self.statusBar.statusBarStyle = presentationData.theme.rootController.statusBarStyle.style self.scrollToTop = { [weak self] in @@ -445,7 +445,8 @@ public final class WebSearchController: ViewController { } let account = self.context.account - let contextBot = resolvePeerByName(account: account, name: name) + let context = self.context + let contextBot = self.context.engine.peers.resolvePeerByName(name: name) |> mapToSignal { peerId -> Signal in if let peerId = peerId { return account.postbox.loadedPeerWithId(peerId) @@ -459,7 +460,7 @@ public final class WebSearchController: ViewController { } |> 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: peerId, limit: 64) + let results = requestContextResults(context: context, botId: user.id, query: query, peerId: peerId, limit: 64) |> map { results -> (ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult? in return { _ in return .contextRequestResult(user, results?.results) @@ -509,6 +510,6 @@ public final class WebSearchController: ViewController { self.validLayout = layout - self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationHeight, transition: transition) + self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition) } } diff --git a/submodules/WebSearchUI/Sources/WebSearchControllerNode.swift b/submodules/WebSearchUI/Sources/WebSearchControllerNode.swift index 8a13114399..450ee647df 100644 --- a/submodules/WebSearchUI/Sources/WebSearchControllerNode.swift +++ b/submodules/WebSearchUI/Sources/WebSearchControllerNode.swift @@ -284,7 +284,7 @@ class WebSearchControllerNode: ASDisplayNode { } }) - self.recentQueriesNode.beganInteractiveDragging = { [weak self] in + self.recentQueriesNode.beganInteractiveDragging = { [weak self] _ in self?.dismissInput?() } @@ -350,10 +350,10 @@ class WebSearchControllerNode: ASDisplayNode { } if themeUpdated { - self.segmentedBackgroundNode.backgroundColor = self.theme.rootController.navigationBar.backgroundColor + self.segmentedBackgroundNode.backgroundColor = self.theme.rootController.navigationBar.opaqueBackgroundColor self.segmentedSeparatorNode.backgroundColor = self.theme.rootController.navigationBar.separatorColor self.segmentedControlNode.updateTheme(SegmentedControlTheme(theme: self.theme)) - self.toolbarBackgroundNode.backgroundColor = self.theme.rootController.navigationBar.backgroundColor + self.toolbarBackgroundNode.backgroundColor = self.theme.rootController.navigationBar.opaqueBackgroundColor self.toolbarSeparatorNode.backgroundColor = self.theme.rootController.navigationBar.separatorColor } @@ -577,7 +577,7 @@ class WebSearchControllerNode: ASDisplayNode { let geoPoint = currentProcessedResults.geoPoint.flatMap { geoPoint -> (Double, Double) in return (geoPoint.latitude, geoPoint.longitude) } - self.loadMoreDisposable.set((requestChatContextResults(account: self.context.account, botId: currentProcessedResults.botId, peerId: currentProcessedResults.peerId, query: currentProcessedResults.query, location: .single(geoPoint), offset: nextOffset) + self.loadMoreDisposable.set((self.context.engine.messages.requestChatContextResults(botId: currentProcessedResults.botId, peerId: currentProcessedResults.peerId, query: currentProcessedResults.query, location: .single(geoPoint), offset: nextOffset) |> deliverOnMainQueue).start(next: { [weak self] nextResults in guard let strongSelf = self, let nextResults = nextResults else { return diff --git a/submodules/WebSearchUI/Sources/WebSearchGalleryController.swift b/submodules/WebSearchUI/Sources/WebSearchGalleryController.swift index 41aaa251b8..5f27d7b151 100644 --- a/submodules/WebSearchUI/Sources/WebSearchGalleryController.swift +++ b/submodules/WebSearchUI/Sources/WebSearchGalleryController.swift @@ -39,7 +39,7 @@ struct WebSearchGalleryEntry: Equatable { switch self.result { case let .externalReference(externalReference): if let content = externalReference.content, externalReference.type == "gif", let thumbnailResource = externalReference.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, progressiveSizes: [])], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: dimensions, flags: [])])) + 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, progressiveSizes: [], immediateThumbnailData: nil)], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: dimensions, flags: [])])) 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(internalReference): @@ -62,7 +62,7 @@ final class WebSearchGalleryControllerPresentationArguments { } class WebSearchGalleryController: ViewController { - private static let navigationTheme = NavigationBarTheme(buttonColor: .white, disabledButtonColor: UIColor(rgb: 0x525252), primaryTextColor: .white, backgroundColor: .clear, separatorColor: .clear, badgeBackgroundColor: .clear, badgeStrokeColor: .clear, badgeTextColor: .clear) + private static let navigationTheme = NavigationBarTheme(buttonColor: .white, disabledButtonColor: UIColor(rgb: 0x525252), primaryTextColor: .white, backgroundColor: .clear, enableBackgroundBlur: false, separatorColor: .clear, badgeBackgroundColor: .clear, badgeStrokeColor: .clear, badgeTextColor: .clear) private var galleryNode: GalleryControllerNode { return self.displayNode as! GalleryControllerNode @@ -342,6 +342,6 @@ class WebSearchGalleryController: ViewController { super.containerLayoutUpdated(layout, transition: transition) self.galleryNode.frame = CGRect(origin: CGPoint(), size: layout.size) - self.galleryNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationHeight, transition: transition) + self.galleryNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition) } } diff --git a/submodules/WebSearchUI/Sources/WebSearchItem.swift b/submodules/WebSearchUI/Sources/WebSearchItem.swift index 519f74677d..cea2a3ec71 100644 --- a/submodules/WebSearchUI/Sources/WebSearchItem.swift +++ b/submodules/WebSearchUI/Sources/WebSearchItem.swift @@ -132,10 +132,10 @@ final class WebSearchItemNode: GridItemNode { var representations: [TelegramMediaImageRepresentation] = [] if let thumbnailResource = thumbnailResource, let thumbnailDimensions = thumbnailDimensions { - representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(thumbnailDimensions), resource: thumbnailResource, progressiveSizes: [])) + representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(thumbnailDimensions), resource: thumbnailResource, progressiveSizes: [], immediateThumbnailData: nil)) } if let imageResource = imageResource, let imageDimensions = imageDimensions { - representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(imageDimensions), resource: imageResource, progressiveSizes: [])) + representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(imageDimensions), resource: imageResource, progressiveSizes: [], immediateThumbnailData: nil)) } if !representations.isEmpty { let tmpImage = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: representations, immediateThumbnailData: immediateThumbnailData, reference: nil, partialReference: nil, flags: []) diff --git a/submodules/WebSearchUI/Sources/WebSearchRecentQueryItem.swift b/submodules/WebSearchUI/Sources/WebSearchRecentQueryItem.swift index 2ee0f99011..d7cc659b6b 100644 --- a/submodules/WebSearchUI/Sources/WebSearchRecentQueryItem.swift +++ b/submodules/WebSearchUI/Sources/WebSearchRecentQueryItem.swift @@ -200,9 +200,9 @@ class WebSearchRecentQueryItemNode: ItemListRevealOptionsItemNode { self.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration * 0.5, removeOnCompletion: false) } - override public func header() -> ListViewItemHeader? { + override public func headers() -> [ListViewItemHeader]? { if let item = self.item { - return item.header + return item.header.flatMap { [$0] } } else { return nil } diff --git a/submodules/ffmpeg/BUILD b/submodules/ffmpeg/BUILD index 34a373f3c7..441192876b 100644 --- a/submodules/ffmpeg/BUILD +++ b/submodules/ffmpeg/BUILD @@ -192,12 +192,12 @@ genrule( PATH="$$PATH:$$YASM_DIR" "$$SOURCE_PATH/build-ffmpeg-bazel.sh" "$$VARIANT" "$$BUILD_ARCH" "$$BUILD_DIR" "$$SOURCE_PATH" """ + "\n" + "\n".join([ - "cp \"$$BUILD_DIR/FFmpeg-iOS/include/{header_path}\" \"$(location Public/ffmpeg/{header_path})\"".format(header_path = header_path) for header_path in ffmpeg_header_paths + "cp \"$$BUILD_DIR/FFmpeg-iOS/include/{header_path}\" \"$(location Public/third_party/ffmpeg/{header_path})\"".format(header_path = header_path) for header_path in ffmpeg_header_paths ]) + "\n" + "\n".join([ "cp \"$$BUILD_DIR/FFmpeg-iOS/lib/{lib}\" \"$(location {lib})\"".format(lib = lib) for lib in ffmpeg_libs ]), outs = [ - "Public/ffmpeg/{}".format(header_path) for header_path in ffmpeg_header_paths + "Public/third_party/ffmpeg/{}".format(header_path) for header_path in ffmpeg_header_paths ] + ffmpeg_libs, tools = [ "//third-party/yasm:yasm.tar", @@ -219,9 +219,10 @@ objc_library( name = "ffmpeg", module_name = "ffmpeg", enable_modules = True, - hdrs = ["Public/ffmpeg/" + x for x in ffmpeg_header_paths], + hdrs = ["Public/third_party/ffmpeg/" + x for x in ffmpeg_header_paths], includes = [ - "Public/ffmpeg", + "Public", + "Public/third_party/ffmpeg", ], sdk_dylibs = [ "libbz2", diff --git a/submodules/ffmpeg/Sources/FFMpeg/build-ffmpeg-bazel.sh b/submodules/ffmpeg/Sources/FFMpeg/build-ffmpeg-bazel.sh index 7391972e92..8b4aff7cf7 100755 --- a/submodules/ffmpeg/Sources/FFMpeg/build-ffmpeg-bazel.sh +++ b/submodules/ffmpeg/Sources/FFMpeg/build-ffmpeg-bazel.sh @@ -57,6 +57,8 @@ CONFIGURE_FLAGS="--enable-cross-compile --disable-programs \ #--enable-hwaccel=h264_videotoolbox,hevc_videotoolbox \ +EXTRA_CFLAGS="-DCONFIG_SAFE_BITSTREAM_READER=1" + if [ "$1" = "debug" ]; then CONFIGURE_FLAGS="$CONFIGURE_FLAGS --disable-optimizations --disable-stripping" @@ -121,7 +123,7 @@ then LIBOPUS_PATH="$SOURCE_DIR/libopus" - CFLAGS="-arch $ARCH" + CFLAGS="$EXTRA_CFLAGS -arch $ARCH" if [ "$RAW_ARCH" = "i386" -o "$RAW_ARCH" = "x86_64" ] then PLATFORM="iPhoneSimulator" diff --git a/submodules/rlottie/rlottie b/submodules/rlottie/rlottie index c567dad74f..7fdc010d9e 160000 --- a/submodules/rlottie/rlottie +++ b/submodules/rlottie/rlottie @@ -1 +1 @@ -Subproject commit c567dad74ffcea9df820809b2cdef90f544d68ca +Subproject commit 7fdc010d9e71a0c39d3f63d421d1bef762b6a034 diff --git a/third-party/AppCenter/AppCenter.BUILD b/third-party/AppCenter/AppCenter.BUILD new file mode 100644 index 0000000000..ce677b8e72 --- /dev/null +++ b/third-party/AppCenter/AppCenter.BUILD @@ -0,0 +1,15 @@ +load("@build_bazel_rules_apple//apple:apple.bzl", + "apple_static_framework_import", +) + +apple_static_framework_import( + name = "AppCenter", + framework_imports = glob(["AppCenter-SDK-Apple/iOS/AppCenter.framework/**"]), + visibility = ["//visibility:public"], +) + +apple_static_framework_import( + name = "AppCenterCrashes", + framework_imports = glob(["AppCenter-SDK-Apple/iOS/AppCenterCrashes.framework/**"]), + visibility = ["//visibility:public"], +) diff --git a/third-party/AppCenter/BUILD b/third-party/AppCenter/BUILD new file mode 100644 index 0000000000..e69de29bb2 diff --git a/third-party/libvpx/0001-Support-arm64-simulator.patch b/third-party/libvpx/0001-Support-arm64-simulator.patch index d085a693f2..1a37c4aae6 100644 --- a/third-party/libvpx/0001-Support-arm64-simulator.patch +++ b/third-party/libvpx/0001-Support-arm64-simulator.patch @@ -1,113 +1,59 @@ -From 012fe706a281cc5e9586dc0ad7b0c59baf84feb1 Mon Sep 17 00:00:00 2001 +From 75cd2d63c8f39466706fcf635ddffa4686968a33 Mon Sep 17 00:00:00 2001 From: Ali <> -Date: Wed, 30 Dec 2020 00:13:06 +0400 -Subject: [PATCH] Support arm64 simulator +Date: Mon, 17 May 2021 02:17:45 +0400 +Subject: [PATCH] Support arm64 iOS simulator --- - build/make/configure.sh | 73 ++++++++++++++++++++++++++++++++++++++++- - configure | 3 +- - 2 files changed, 74 insertions(+), 2 deletions(-) + build/make/configure.sh | 18 ++++++++++++++---- + configure | 1 + + 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/build/make/configure.sh b/build/make/configure.sh -index c4e938fc7..ccd7066a5 100644 +index 81d30a16c..eb15e4b07 100644 --- a/build/make/configure.sh +++ b/build/make/configure.sh -@@ -852,6 +852,14 @@ process_common_toolchain() { - # Handle darwin variants. Newer SDKs allow targeting older - # platforms, so use the newest one available. - case ${toolchain} in -+ arm64-iphonesimulator*) -+ add_cflags "-miphonesimulator-version-min=${IOS_VERSION_MIN}" -+ iphonesimulator_sdk_dir="$(show_darwin_sdk_path iphonesimulator)" -+ if [ -d "${iphonesimulator_sdk_dir}" ]; then -+ add_cflags "-isysroot ${iphonesimulator_sdk_dir}" -+ add_ldflags "-isysroot ${iphonesimulator_sdk_dir}" -+ fi -+ ;; - arm*-darwin-*) - add_cflags "-miphoneos-version-min=${IOS_VERSION_MIN}" - iphoneos_sdk_dir="$(show_darwin_sdk_path iphoneos)" -@@ -945,7 +953,7 @@ process_common_toolchain() { - - # Process ARM architecture variants - case ${toolchain} in -- arm*) -+ arm*|arm64-iphonesimulator-gcc) - # on arm, isa versions are supersets - case ${tgt_isa} in - arm64|armv8) -@@ -1155,6 +1163,69 @@ EOF - asm_conversion_cmd="${source_path_mk}/build/make/ads2gas_apple.pl" +@@ -1095,9 +1095,14 @@ EOF + soft_enable unit_tests ;; -+ iphonesimulator*) -+ if ! enabled external_build; then -+ XCRUN_FIND="xcrun --sdk iphonesimulator --find" -+ CXX="$(${XCRUN_FIND} clang++)" -+ CC="$(${XCRUN_FIND} clang)" -+ AR="$(${XCRUN_FIND} ar)" -+ AS="$(${XCRUN_FIND} as)" -+ STRIP="$(${XCRUN_FIND} strip)" -+ NM="$(${XCRUN_FIND} nm)" -+ RANLIB="$(${XCRUN_FIND} ranlib)" -+ AS_SFX=.S -+ LD="${CXX:-$(${XCRUN_FIND} ld)}" -+ -+ # ASFLAGS is written here instead of using check_add_asflags -+ # because we need to overwrite all of ASFLAGS and purge the -+ # options that were put in above -+ ASFLAGS="-arch ${tgt_isa} -g" -+ -+ add_cflags -arch ${tgt_isa} -+ add_ldflags -arch ${tgt_isa} -+ -+ add_cflags --target=arm64-apple-ios7.0-simulator -+ add_ldflags --target=arm64-apple-ios7.0-simulator -+ -+ alt_libc="$(show_darwin_sdk_path iphonesimulator)" -+ if [ -d "${alt_libc}" ]; then -+ add_cflags -isysroot ${alt_libc} +- darwin) ++ darwin|iphonesimulator) + if ! enabled external_build; then +- XCRUN_FIND="xcrun --sdk iphoneos --find" ++ sdk_platform_name="iphoneos" ++ if [ "${tgt_os}" == "iphonesimulator" ]; then ++ sdk_platform_name="iphonesimulator" + fi + -+ if [ "${LD}" = "${CXX}" ]; then -+ add_ldflags -miphonesimulator-version-min="${IOS_VERSION_MIN}" -+ else -+ add_ldflags -ios_version_min "${IOS_VERSION_MIN}" ++ XCRUN_FIND="xcrun --sdk $sdk_platform_name --find" + CXX="$(${XCRUN_FIND} clang++)" + CC="$(${XCRUN_FIND} clang)" + AR="$(${XCRUN_FIND} ar)" +@@ -1116,7 +1121,12 @@ EOF + add_cflags -arch ${tgt_isa} + add_ldflags -arch ${tgt_isa} + +- alt_libc="$(show_darwin_sdk_path iphoneos)" ++ if [ "${tgt_os}" == "iphonesimulator" ]; then ++ add_cflags --target=arm64-apple-ios7.0-simulator ++ add_ldflags --target=arm64-apple-ios7.0-simulator + fi + -+ for d in lib usr/lib usr/lib/system; do -+ try_dir="${alt_libc}/${d}" -+ [ -d "${try_dir}" ] && add_ldflags -L"${try_dir}" -+ done -+ -+ case ${tgt_isa} in -+ armv7|armv7s|armv8|arm64) -+ if enabled neon && ! check_xcode_minimum_version; then -+ soft_disable neon -+ log_echo " neon disabled: upgrade Xcode (need v6.3+)." -+ if enabled neon_asm; then -+ soft_disable neon_asm -+ log_echo " neon_asm disabled: upgrade Xcode (need v6.3+)." -+ fi -+ fi -+ ;; -+ esac -+ -+ if [ "$(show_darwin_sdk_major_version iphoneos)" -gt 8 ]; then -+ check_add_cflags -fembed-bitcode -+ check_add_asflags -fembed-bitcode -+ check_add_ldflags -fembed-bitcode -+ fi -+ fi -+ -+ asm_conversion_cmd="${source_path}/build/make/ads2gas_apple.pl" -+ ;; -+ - linux*) - enable_feature linux - if enabled rvct; then ++ alt_libc="$(show_darwin_sdk_path $sdk_platform_name)" + if [ -d "${alt_libc}" ]; then + add_cflags -isysroot ${alt_libc} + fi +@@ -1145,7 +1155,7 @@ EOF + ;; + esac + +- if [ "$(show_darwin_sdk_major_version iphoneos)" -gt 8 ]; then ++ if [ "$(show_darwin_sdk_major_version $sdk_platform_name)" -gt 8 ]; then + check_add_cflags -fembed-bitcode + check_add_asflags -fembed-bitcode + check_add_ldflags -fembed-bitcode diff --git a/configure b/configure -index f7e11aaf2..af625ee35 100755 +index da631a45e..1c6042038 100755 --- a/configure +++ b/configure @@ -100,6 +100,7 @@ EOF @@ -118,15 +64,6 @@ index f7e11aaf2..af625ee35 100755 all_platforms="${all_platforms} arm64-linux-gcc" all_platforms="${all_platforms} arm64-win64-gcc" all_platforms="${all_platforms} arm64-win64-vs15" -@@ -735,7 +736,7 @@ process_toolchain() { - soft_enable libyuv - # GTestLog must be modified to use Android logging utilities. - ;; -- *-darwin-*) -+ *-darwin-*|arm64-iphonesimulator-*) - check_add_cxxflags -std=c++11 - # iOS/ARM builds do not work with gtest. This does not match - # x86 targets. -- -2.24.3 (Apple Git-128) +2.30.1 (Apple Git-130) diff --git a/third-party/libvpx/BUILD b/third-party/libvpx/BUILD index 9ca8530ae4..ed92e4eb38 100644 --- a/third-party/libvpx/BUILD +++ b/third-party/libvpx/BUILD @@ -75,7 +75,7 @@ genrule( mkdir -p "$$BUILD_DIR/Public/libvpx" - sh $$BUILD_DIR/build-libvpx-bazel.sh $$BUILD_ARCH "$$BUILD_DIR/libvpx" "$$BUILD_DIR" + PATH="$$PATH:$$ABS_YASM_DIR" sh $$BUILD_DIR/build-libvpx-bazel.sh $$BUILD_ARCH "$$BUILD_DIR/libvpx" "$$BUILD_DIR" """ + "\n".join([ "cp -f \"$$BUILD_DIR/VPX.framework/Headers/vpx/{}\" \"$(location Public/vpx/{})\"".format(header, header) for header in headers diff --git a/third-party/libvpx/build-libvpx-bazel.sh b/third-party/libvpx/build-libvpx-bazel.sh index a2ac3011d2..4540198e99 100755 --- a/third-party/libvpx/build-libvpx-bazel.sh +++ b/third-party/libvpx/build-libvpx-bazel.sh @@ -16,8 +16,14 @@ devnull='> /dev/null 2>&1' BUILD_ROOT="_iosbuild" CONFIGURE_ARGS="--disable-docs --disable-examples - --disable-libyuv - --disable-unit-tests" + --disable-postproc + --disable-webm-io + --disable-vp9-highbitdepth + --disable-vp9-postproc + --disable-vp9-temporal-denoising + --disable-unit-tests + --enable-realtime-only + --enable-multi-res-encoding" DIST_DIR="_dist" FRAMEWORK_DIR="VPX.framework" FRAMEWORK_LIB="VPX.framework/VPX" diff --git a/third-party/libvpx/libvpx b/third-party/libvpx/libvpx index 3a38edea2c..002f14078f 160000 --- a/third-party/libvpx/libvpx +++ b/third-party/libvpx/libvpx @@ -1 +1 @@ -Subproject commit 3a38edea2cd114d53914cab017cab2e43a600031 +Subproject commit 002f14078ff6aa1f0c1548aa1902b3af9d42087e diff --git a/third-party/openh264/BUILD b/third-party/openh264/BUILD new file mode 100644 index 0000000000..0fde708c6f --- /dev/null +++ b/third-party/openh264/BUILD @@ -0,0 +1,270 @@ + +arm64_specific_sources = [ + "third_party/openh264/src/codec/encoder/core/arm64/intra_pred_aarch64_neon.S", + "third_party/openh264/src/codec/encoder/core/arm64/intra_pred_sad_3_opt_aarch64_neon.S", + "third_party/openh264/src/codec/encoder/core/arm64/memory_aarch64_neon.S", + "third_party/openh264/src/codec/encoder/core/arm64/pixel_aarch64_neon.S", + "third_party/openh264/src/codec/encoder/core/arm64/reconstruct_aarch64_neon.S", + "third_party/openh264/src/codec/encoder/core/arm64/svc_motion_estimation_aarch64_neon.S", + "third_party/openh264/src/codec/common/arm64/copy_mb_aarch64_neon.S", + "third_party/openh264/src/codec/common/arm64/deblocking_aarch64_neon.S", + "third_party/openh264/src/codec/common/arm64/expand_picture_aarch64_neon.S", + "third_party/openh264/src/codec/common/arm64/intra_pred_common_aarch64_neon.S", + "third_party/openh264/src/codec/common/arm64/mc_aarch64_neon.S", + "third_party/openh264/src/codec/processing/src/arm64/adaptive_quantization_aarch64_neon.S", + "third_party/openh264/src/codec/processing/src/arm64/down_sample_aarch64_neon.S", + "third_party/openh264/src/codec/processing/src/arm64/pixel_sad_aarch64_neon.S", + "third_party/openh264/src/codec/processing/src/arm64/vaa_calc_aarch64_neon.S", +] + +arm64_specific_textual_hdrs = [ + "third_party/openh264/src/codec/common/arm64/arm_arch64_common_macro.S", +] + +arm_specific_sources = [ + "third_party/openh264/src/codec/encoder/core/arm/intra_pred_neon.S", + "third_party/openh264/src/codec/encoder/core/arm/intra_pred_sad_3_opt_neon.S", + "third_party/openh264/src/codec/encoder/core/arm/memory_neon.S", + "third_party/openh264/src/codec/encoder/core/arm/pixel_neon.S", + "third_party/openh264/src/codec/encoder/core/arm/reconstruct_neon.S", + "third_party/openh264/src/codec/encoder/core/arm/svc_motion_estimation.S", + "third_party/openh264/src/codec/common/arm/copy_mb_neon.S", + "third_party/openh264/src/codec/common/arm/deblocking_neon.S", + "third_party/openh264/src/codec/common/arm/expand_picture_neon.S", + "third_party/openh264/src/codec/common/arm/intra_pred_common_neon.S", + "third_party/openh264/src/codec/common/arm/mc_neon.S", + "third_party/openh264/src/codec/processing/src/arm/adaptive_quantization.S", + "third_party/openh264/src/codec/processing/src/arm/down_sample_neon.S", + "third_party/openh264/src/codec/processing/src/arm/pixel_sad_neon.S", + "third_party/openh264/src/codec/processing/src/arm/vaa_calc_neon.S", +] + +arm_specific_textual_hdrs = [ + "third_party/openh264/src/codec/common/arm/arm_arch_common_macro.S", +] + +arm64_specific_copts = [ + "-DHAVE_NEON_AARCH64=1", + "-Ithird-party/openh264/third_party/openh264/src/codec/common/arm64", +] + +arm_specific_copts = [ + "-DHAVE_NEON=1", + "-Ithird-party/openh264/third_party/openh264/src/codec/common/arm", +] + +arch_specific_sources = select({ + "@build_bazel_rules_apple//apple:ios_armv7": arm_specific_sources, + "@build_bazel_rules_apple//apple:ios_arm64": arm64_specific_sources, + "//build-system:ios_sim_arm64": arm64_specific_sources, + "@build_bazel_rules_apple//apple:ios_x86_64": [], +}) + +arch_specific_copts = select({ + "@build_bazel_rules_apple//apple:ios_armv7": arm_specific_copts, + "@build_bazel_rules_apple//apple:ios_arm64": arm64_specific_copts, + "//build-system:ios_sim_arm64": arm64_specific_copts, + "@build_bazel_rules_apple//apple:ios_x86_64": [], +}) + +arch_specific_textual_hdrs = select({ + "@build_bazel_rules_apple//apple:ios_armv7": arm_specific_textual_hdrs, + "@build_bazel_rules_apple//apple:ios_arm64": arm64_specific_textual_hdrs, + "//build-system:ios_sim_arm64": arm64_specific_textual_hdrs, + "@build_bazel_rules_apple//apple:ios_x86_64": [], +}) + +all_sources = arch_specific_sources + [ + "third_party/openh264/src/codec/encoder/core/inc/as264_common.h", + "third_party/openh264/src/codec/encoder/core/inc/au_set.h", + "third_party/openh264/src/codec/encoder/core/inc/deblocking.h", + "third_party/openh264/src/codec/encoder/core/inc/decode_mb_aux.h", + "third_party/openh264/src/codec/encoder/core/inc/dq_map.h", + "third_party/openh264/src/codec/encoder/core/inc/encode_mb_aux.h", + "third_party/openh264/src/codec/encoder/core/inc/encoder_context.h", + "third_party/openh264/src/codec/encoder/core/inc/encoder.h", + "third_party/openh264/src/codec/encoder/core/inc/extern.h", + "third_party/openh264/src/codec/encoder/core/inc/get_intra_predictor.h", + "third_party/openh264/src/codec/encoder/core/inc/mb_cache.h", + "third_party/openh264/src/codec/encoder/core/inc/md.h", + "third_party/openh264/src/codec/encoder/core/inc/mt_defs.h", + "third_party/openh264/src/codec/encoder/core/inc/mv_pred.h", + "third_party/openh264/src/codec/encoder/core/inc/nal_encap.h", + "third_party/openh264/src/codec/encoder/core/inc/param_svc.h", + "third_party/openh264/src/codec/encoder/core/inc/parameter_sets.h", + "third_party/openh264/src/codec/encoder/core/inc/paraset_strategy.h", + "third_party/openh264/src/codec/encoder/core/inc/picture_handle.h", + "third_party/openh264/src/codec/encoder/core/inc/picture.h", + "third_party/openh264/src/codec/encoder/core/inc/rc.h", + "third_party/openh264/src/codec/encoder/core/inc/ref_list_mgr_svc.h", + "third_party/openh264/src/codec/encoder/core/inc/sample.h", + "third_party/openh264/src/codec/encoder/core/inc/set_mb_syn_cabac.h", + "third_party/openh264/src/codec/encoder/core/inc/set_mb_syn_cavlc.h", + "third_party/openh264/src/codec/encoder/core/inc/slice_multi_threading.h", + "third_party/openh264/src/codec/encoder/core/inc/slice.h", + "third_party/openh264/src/codec/encoder/core/inc/stat.h", + "third_party/openh264/src/codec/encoder/core/inc/svc_base_layer_md.h", + "third_party/openh264/src/codec/encoder/core/inc/svc_enc_frame.h", + "third_party/openh264/src/codec/encoder/core/inc/svc_enc_golomb.h", + "third_party/openh264/src/codec/encoder/core/inc/svc_enc_macroblock.h", + "third_party/openh264/src/codec/encoder/core/inc/svc_enc_slice_segment.h", + "third_party/openh264/src/codec/encoder/core/inc/svc_encode_mb.h", + "third_party/openh264/src/codec/encoder/core/inc/svc_encode_slice.h", + "third_party/openh264/src/codec/encoder/core/inc/svc_mode_decision.h", + "third_party/openh264/src/codec/encoder/core/inc/svc_motion_estimate.h", + "third_party/openh264/src/codec/encoder/core/inc/svc_set_mb_syn_cavlc.h", + "third_party/openh264/src/codec/encoder/core/inc/svc_set_mb_syn.h", + "third_party/openh264/src/codec/encoder/core/inc/vlc_encoder.h", + "third_party/openh264/src/codec/encoder/core/inc/wels_common_basis.h", + "third_party/openh264/src/codec/encoder/core/inc/wels_const.h", + "third_party/openh264/src/codec/encoder/core/inc/wels_func_ptr_def.h", + "third_party/openh264/src/codec/encoder/core/inc/wels_preprocess.h", + "third_party/openh264/src/codec/encoder/core/inc/wels_task_base.h", + "third_party/openh264/src/codec/encoder/core/inc/wels_task_encoder.h", + "third_party/openh264/src/codec/encoder/core/inc/wels_task_management.h", + "third_party/openh264/src/codec/encoder/core/inc/wels_transpose_matrix.h", + "third_party/openh264/src/codec/common/inc/crt_util_safe_x.h", + "third_party/openh264/src/codec/common/inc/typedefs.h", + "third_party/openh264/src/codec/common/inc/utils.h", + "third_party/openh264/src/codec/api/svc/codec_app_def.h", + "third_party/openh264/src/codec/api/svc/codec_def.h", + "third_party/openh264/src/codec/api/svc/codec_api.h", + "third_party/openh264/src/codec/common/inc/WelsTask.h", + "third_party/openh264/src/codec/common/inc/macros.h", + "third_party/openh264/src/codec/common/inc/wels_const_common.h", + "third_party/openh264/src/codec/common/inc/wels_common_defs.h", + "third_party/openh264/src/codec/common/inc/memory_align.h", + "third_party/openh264/src/codec/common/inc/expand_pic.h", + "third_party/openh264/src/codec/common/inc/mc.h", + "third_party/openh264/src/codec/common/inc/cpu_core.h", + "third_party/openh264/src/codec/common/inc/WelsThreadLib.h", + "third_party/openh264/src/codec/common/inc/WelsLock.h", + "third_party/openh264/src/codec/common/inc/WelsThreadPool.h", + "third_party/openh264/src/codec/common/inc/WelsTaskThread.h", + "third_party/openh264/src/codec/common/inc/WelsThread.h", + "third_party/openh264/src/codec/common/inc/WelsList.h", + "third_party/openh264/src/codec/common/inc/copy_mb.h", + "third_party/openh264/src/codec/common/inc/golomb_common.h", + "third_party/openh264/src/codec/common/inc/ls_defines.h", + "third_party/openh264/src/codec/common/inc/measure_time.h", + "third_party/openh264/src/codec/common/inc/deblocking_common.h", + "third_party/openh264/src/codec/common/inc/cpu.h", + "third_party/openh264/src/codec/api/svc/codec_ver.h", + "third_party/openh264/src/codec/common/inc/intra_pred_common.h", + "third_party/openh264/src/codec/common/inc/sad_common.h", +] + [ + "third_party/openh264/src/codec/encoder/core/src/au_set.cpp", + "third_party/openh264/src/codec/encoder/core/src/deblocking.cpp", + "third_party/openh264/src/codec/encoder/core/src/decode_mb_aux.cpp", + "third_party/openh264/src/codec/encoder/core/src/encode_mb_aux.cpp", + "third_party/openh264/src/codec/encoder/core/src/encoder_data_tables.cpp", + "third_party/openh264/src/codec/encoder/core/src/encoder_ext.cpp", + "third_party/openh264/src/codec/encoder/core/src/encoder.cpp", + "third_party/openh264/src/codec/encoder/core/src/get_intra_predictor.cpp", + "third_party/openh264/src/codec/encoder/core/src/md.cpp", + "third_party/openh264/src/codec/encoder/core/src/mv_pred.cpp", + "third_party/openh264/src/codec/encoder/core/src/nal_encap.cpp", + "third_party/openh264/src/codec/encoder/core/src/paraset_strategy.cpp", + "third_party/openh264/src/codec/encoder/core/src/picture_handle.cpp", + "third_party/openh264/src/codec/encoder/core/src/ratectl.cpp", + "third_party/openh264/src/codec/encoder/core/src/ref_list_mgr_svc.cpp", + "third_party/openh264/src/codec/encoder/core/src/sample.cpp", + "third_party/openh264/src/codec/encoder/core/src/set_mb_syn_cabac.cpp", + "third_party/openh264/src/codec/encoder/core/src/set_mb_syn_cavlc.cpp", + "third_party/openh264/src/codec/encoder/core/src/slice_multi_threading.cpp", + "third_party/openh264/src/codec/encoder/core/src/svc_base_layer_md.cpp", + "third_party/openh264/src/codec/encoder/core/src/svc_enc_slice_segment.cpp", + "third_party/openh264/src/codec/encoder/core/src/svc_encode_mb.cpp", + "third_party/openh264/src/codec/encoder/core/src/svc_encode_slice.cpp", + "third_party/openh264/src/codec/encoder/core/src/svc_mode_decision.cpp", + "third_party/openh264/src/codec/encoder/core/src/svc_motion_estimate.cpp", + "third_party/openh264/src/codec/encoder/core/src/svc_set_mb_syn_cabac.cpp", + "third_party/openh264/src/codec/encoder/core/src/svc_set_mb_syn_cavlc.cpp", + "third_party/openh264/src/codec/encoder/core/src/wels_preprocess.cpp", + "third_party/openh264/src/codec/encoder/core/src/wels_task_base.cpp", + "third_party/openh264/src/codec/encoder/core/src/wels_task_encoder.cpp", + "third_party/openh264/src/codec/encoder/core/src/wels_task_management.cpp", +] + [ + "third_party/openh264/src/codec/encoder/plus/inc/welsEncoderExt.h", + "third_party/openh264/src/codec/common/inc/version.h", + "third_party/openh264/src/codec/common/inc/welsCodecTrace.h", + "third_party/openh264/src/codec/common/inc/asmdefs_mmi.h", +] + [ + "third_party/openh264/src/codec/encoder/plus/src/welsEncoderExt.cpp", + "third_party/openh264/src/codec/common/src/welsCodecTrace.cpp", + "third_party/openh264/src/codec/common/src/common_tables.cpp", + "third_party/openh264/src/codec/common/src/copy_mb.cpp", + "third_party/openh264/src/codec/common/src/cpu.cpp", + "third_party/openh264/src/codec/common/src/crt_util_safe_x.cpp", + "third_party/openh264/src/codec/common/src/deblocking_common.cpp", + "third_party/openh264/src/codec/common/src/expand_pic.cpp", + "third_party/openh264/src/codec/common/src/intra_pred_common.cpp", + "third_party/openh264/src/codec/common/src/mc.cpp", + "third_party/openh264/src/codec/common/src/memory_align.cpp", + "third_party/openh264/src/codec/common/src/sad_common.cpp", + "third_party/openh264/src/codec/common/src/WelsTaskThread.cpp", + "third_party/openh264/src/codec/common/src/WelsThread.cpp", + "third_party/openh264/src/codec/common/src/WelsThreadLib.cpp", + "third_party/openh264/src/codec/common/src/WelsThreadPool.cpp", + "third_party/openh264/src/codec/common/src/utils.cpp", +] + [ + "third_party/openh264/src/codec/processing/interface/IWelsVP.h", + "third_party/openh264/src/codec/processing/src/adaptivequantization/AdaptiveQuantization.cpp", + "third_party/openh264/src/codec/processing/src/adaptivequantization/AdaptiveQuantization.h", + "third_party/openh264/src/codec/processing/src/backgrounddetection/BackgroundDetection.cpp", + "third_party/openh264/src/codec/processing/src/backgrounddetection/BackgroundDetection.h", + "third_party/openh264/src/codec/processing/src/common/common.h", + "third_party/openh264/src/codec/processing/src/common/memory.cpp", + "third_party/openh264/src/codec/processing/src/common/memory.h", + "third_party/openh264/src/codec/processing/src/common/resource.h", + "third_party/openh264/src/codec/processing/src/common/typedef.h", + "third_party/openh264/src/codec/processing/src/common/util.h", + "third_party/openh264/src/codec/processing/src/common/WelsFrameWork.cpp", + "third_party/openh264/src/codec/processing/src/common/WelsFrameWork.h", + "third_party/openh264/src/codec/processing/src/common/WelsFrameWorkEx.cpp", + "third_party/openh264/src/codec/processing/src/complexityanalysis/ComplexityAnalysis.cpp", + "third_party/openh264/src/codec/processing/src/complexityanalysis/ComplexityAnalysis.h", + "third_party/openh264/src/codec/processing/src/denoise/denoise.cpp", + "third_party/openh264/src/codec/processing/src/denoise/denoise_filter.cpp", + "third_party/openh264/src/codec/processing/src/denoise/denoise.h", + "third_party/openh264/src/codec/processing/src/downsample/downsample.cpp", + "third_party/openh264/src/codec/processing/src/downsample/downsample.h", + "third_party/openh264/src/codec/processing/src/downsample/downsamplefuncs.cpp", + "third_party/openh264/src/codec/processing/src/imagerotate/imagerotate.cpp", + "third_party/openh264/src/codec/processing/src/imagerotate/imagerotate.h", + "third_party/openh264/src/codec/processing/src/imagerotate/imagerotatefuncs.cpp", + "third_party/openh264/src/codec/processing/src/scenechangedetection/SceneChangeDetection.cpp", + "third_party/openh264/src/codec/processing/src/scenechangedetection/SceneChangeDetection.h", + "third_party/openh264/src/codec/processing/src/scrolldetection/ScrollDetection.cpp", + "third_party/openh264/src/codec/processing/src/scrolldetection/ScrollDetection.h", + "third_party/openh264/src/codec/processing/src/scrolldetection/ScrollDetectionFuncs.cpp", + "third_party/openh264/src/codec/processing/src/scrolldetection/ScrollDetectionFuncs.h", + "third_party/openh264/src/codec/processing/src/vaacalc/vaacalcfuncs.cpp", + "third_party/openh264/src/codec/processing/src/vaacalc/vaacalculation.cpp", + "third_party/openh264/src/codec/processing/src/vaacalc/vaacalculation.h", +] + +cc_library( + name = "openh264", + srcs = all_sources, + hdrs = glob([ + ]), + textual_hdrs = arch_specific_textual_hdrs, + includes = [ + ], + copts = arch_specific_copts + [ + "-Ithird-party/openh264/third_party/openh264/src/codec/encoder/core/inc", + "-Ithird-party/openh264/third_party/openh264/src/codec/encoder/plus/inc", + "-Ithird-party/openh264/third_party/openh264/src/codec/decoder/plus/inc", + "-Ithird-party/openh264/third_party/openh264/src/codec/common/inc", + "-Ithird-party/openh264/third_party/openh264/src/codec/api/svc", + "-Ithird-party/openh264/third_party/openh264/src/codec/processing/interface", + "-Ithird-party/openh264/third_party/openh264/src/codec/processing/src/common", + "-Os", + ], + deps = [ + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/third-party/openh264/third_party/openh264/src/codec/api/meson.build b/third-party/openh264/third_party/openh264/src/codec/api/meson.build new file mode 100644 index 0000000000..0f133adecd --- /dev/null +++ b/third-party/openh264/third_party/openh264/src/codec/api/meson.build @@ -0,0 +1 @@ +subdir ('svc') diff --git a/third-party/openh264/third_party/openh264/src/codec/api/svc/codec_api.h b/third-party/openh264/third_party/openh264/src/codec/api/svc/codec_api.h new file mode 100644 index 0000000000..a1326c8f05 --- /dev/null +++ b/third-party/openh264/third_party/openh264/src/codec/api/svc/codec_api.h @@ -0,0 +1,592 @@ +/*! + *@page License + * + * \copy + * Copyright (c) 2013, Cisco Systems + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + * COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN + * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ + +#ifndef WELS_VIDEO_CODEC_SVC_API_H__ +#define WELS_VIDEO_CODEC_SVC_API_H__ + +#ifndef __cplusplus +#if defined(_MSC_VER) && (_MSC_VER < 1800) +typedef unsigned char bool; +#else +#include +#endif +#endif + +#include "codec_app_def.h" +#include "codec_def.h" + +#if defined(_WIN32) || defined(__cdecl) +#define EXTAPI __cdecl +#else +#define EXTAPI +#endif + +/** + * @file codec_api.h +*/ + +/** + * @page Overview + * * This page is for openh264 codec API usage. + * * For how to use the encoder,please refer to page UsageExampleForEncoder + * * For how to use the decoder,please refer to page UsageExampleForDecoder + * * For more detail about ISVEncoder,please refer to page ISVCEncoder + * * For more detail about ISVDecoder,please refer to page ISVCDecoder +*/ + +/** + * @page DecoderUsageExample + * + * @brief + * * An example for using the decoder for Decoding only or Parsing only + * + * Step 1:decoder declaration + * @code + * + * //decoder declaration + * ISVCDecoder *pSvcDecoder; + * //input: encoded bitstream start position; should include start code prefix + * unsigned char *pBuf =...; + * //input: encoded bit stream length; should include the size of start code prefix + * int iSize =...; + * //output: [0~2] for Y,U,V buffer for Decoding only + * unsigned char *pData[3] =...; + * //in-out: for Decoding only: declare and initialize the output buffer info, this should never co-exist with Parsing only + * SBufferInfo sDstBufInfo; + * memset(&sDstBufInfo, 0, sizeof(SBufferInfo)); + * //in-out: for Parsing only: declare and initialize the output bitstream buffer info for parse only, this should never co-exist with Decoding only + * SParserBsInfo sDstParseInfo; + * memset(&sDstParseInfo, 0, sizeof(SParserBsInfo)); + * sDstParseInfo.pDstBuff = new unsigned char[PARSE_SIZE]; //In Parsing only, allocate enough buffer to save transcoded bitstream for a frame + * + * @endcode + * + * Step 2:decoder creation + * @code + * WelsCreateDecoder(&pSvcDecoder); + * @endcode + * + * Step 3:declare required parameter, used to differentiate Decoding only and Parsing only + * @code + * SDecodingParam sDecParam = {0}; + * sDecParam.sVideoProperty.eVideoBsType = VIDEO_BITSTREAM_AVC; + * //for Parsing only, the assignment is mandatory + * sDecParam.bParseOnly = true; + * @endcode + * + * Step 4:initialize the parameter and decoder context, allocate memory + * @code + * pSvcDecoder->Initialize(&sDecParam); + * @endcode + * + * Step 5:do actual decoding process in slice level; + * this can be done in a loop until data ends + * @code + * //for Decoding only + * iRet = pSvcDecoder->DecodeFrameNoDelay(pBuf, iSize, pData, &sDstBufInfo); + * //or + * iRet = pSvcDecoder->DecodeFrame2(pBuf, iSize, pData, &sDstBufInfo); + * //for Parsing only + * iRet = pSvcDecoder->DecodeParser(pBuf, iSize, &sDstParseInfo); + * //decode failed + * If (iRet != 0){ + * //error handling (RequestIDR or something like that) + * } + * //for Decoding only, pData can be used for render. + * if (sDstBufInfo.iBufferStatus==1){ + * //output handling (pData[0], pData[1], pData[2]) + * } + * //for Parsing only, sDstParseInfo can be used for, e.g., HW decoding + * if (sDstBufInfo.iNalNum > 0){ + * //Hardware decoding sDstParseInfo; + * } + * //no-delay decoding can be realized by directly calling DecodeFrameNoDelay(), which is the recommended usage. + * //no-delay decoding can also be realized by directly calling DecodeFrame2() again with NULL input, as in the following. In this case, decoder would immediately reconstruct the input data. This can also be used similarly for Parsing only. Consequent decoding error and output indication should also be considered as above. + * iRet = pSvcDecoder->DecodeFrame2(NULL, 0, pData, &sDstBufInfo); + * //judge iRet, sDstBufInfo.iBufferStatus ... + * @endcode + * + * Step 6:uninitialize the decoder and memory free + * @code + * pSvcDecoder->Uninitialize(); + * @endcode + * + * Step 7:destroy the decoder + * @code + * DestroyDecoder(pSvcDecoder); + * @endcode + * +*/ + +/** + * @page EncoderUsageExample1 + * + * @brief + * * An example for using encoder with basic parameter + * + * Step1:setup encoder + * @code + * ISVCEncoder* encoder_; + * int rv = WelsCreateSVCEncoder (&encoder_); + * assert (rv == 0); + * assert (encoder_ != NULL); + * @endcode + * + * Step2:initilize with basic parameter + * @code + * SEncParamBase param; + * memset (¶m, 0, sizeof (SEncParamBase)); + * param.iUsageType = usageType; //from EUsageType enum + * param.fMaxFrameRate = frameRate; + * param.iPicWidth = width; + * param.iPicHeight = height; + * param.iTargetBitrate = 5000000; + * encoder_->Initialize (¶m); + * @endcode + * + * Step3:set option, set option during encoding process + * @code + * encoder_->SetOption (ENCODER_OPTION_TRACE_LEVEL, &g_LevelSetting); + * int videoFormat = videoFormatI420; + * encoder_->SetOption (ENCODER_OPTION_DATAFORMAT, &videoFormat); + * @endcode + * + * Step4: encode and store ouput bistream + * @code + * int frameSize = width * height * 3 / 2; + * BufferedData buf; + * buf.SetLength (frameSize); + * assert (buf.Length() == (size_t)frameSize); + * SFrameBSInfo info; + * memset (&info, 0, sizeof (SFrameBSInfo)); + * SSourcePicture pic; + * memset (&pic, 0, sizeof (SsourcePicture)); + * pic.iPicWidth = width; + * pic.iPicHeight = height; + * pic.iColorFormat = videoFormatI420; + * pic.iStride[0] = pic.iPicWidth; + * pic.iStride[1] = pic.iStride[2] = pic.iPicWidth >> 1; + * pic.pData[0] = buf.data(); + * pic.pData[1] = pic.pData[0] + width * height; + * pic.pData[2] = pic.pData[1] + (width * height >> 2); + * for(int num = 0;numEncodeFrame (&pic, &info); + * assert (rv == cmResultSuccess); + * if (info.eFrameType != videoFrameTypeSkip) { + * //output bitstream handling + * } + * } + * @endcode + * + * Step5:teardown encoder + * @code + * if (encoder_) { + * encoder_->Uninitialize(); + * WelsDestroySVCEncoder (encoder_); + * } + * @endcode + * + */ + +/** + * @page EncoderUsageExample2 + * + * @brief + * * An example for using the encoder with extension parameter. + * * The same operation on Step 1,3,4,5 with Example-1 + * + * Step 2:initialize with extension parameter + * @code + * SEncParamExt param; + * encoder_->GetDefaultParams (¶m); + * param.iUsageType = usageType; + * param.fMaxFrameRate = frameRate; + * param.iPicWidth = width; + * param.iPicHeight = height; + * param.iTargetBitrate = 5000000; + * param.bEnableDenoise = denoise; + * param.iSpatialLayerNum = layers; + * //SM_DYN_SLICE don't support multi-thread now + * if (sliceMode != SM_SINGLE_SLICE && sliceMode != SM_DYN_SLICE) + * param.iMultipleThreadIdc = 2; + * + * for (int i = 0; i < param.iSpatialLayerNum; i++) { + * param.sSpatialLayers[i].iVideoWidth = width >> (param.iSpatialLayerNum - 1 - i); + * param.sSpatialLayers[i].iVideoHeight = height >> (param.iSpatialLayerNum - 1 - i); + * param.sSpatialLayers[i].fFrameRate = frameRate; + * param.sSpatialLayers[i].iSpatialBitrate = param.iTargetBitrate; + * + * param.sSpatialLayers[i].sSliceCfg.uiSliceMode = sliceMode; + * if (sliceMode == SM_DYN_SLICE) { + * param.sSpatialLayers[i].sSliceCfg.sSliceArgument.uiSliceSizeConstraint = 600; + * param.uiMaxNalSize = 1500; + * } + * } + * param.iTargetBitrate *= param.iSpatialLayerNum; + * encoder_->InitializeExt (¶m); + * int videoFormat = videoFormatI420; + * encoder_->SetOption (ENCODER_OPTION_DATAFORMAT, &videoFormat); + * + * @endcode + */ + + + + +#ifdef __cplusplus +/** +* @brief Endocder definition +*/ +class ISVCEncoder { + public: + /** + * @brief Initialize the encoder + * @param pParam basic encoder parameter + * @return CM_RETURN: 0 - success; otherwise - failed; + */ + virtual int EXTAPI Initialize (const SEncParamBase* pParam) = 0; + + /** + * @brief Initilaize encoder by using extension parameters. + * @param pParam extension parameter for encoder + * @return CM_RETURN: 0 - success; otherwise - failed; + */ + virtual int EXTAPI InitializeExt (const SEncParamExt* pParam) = 0; + + /** + * @brief Get the default extension parameters. + * If you want to change some parameters of encoder, firstly you need to get the default encoding parameters, + * after that you can change part of parameters you want to. + * @param pParam extension parameter for encoder + * @return CM_RETURN: 0 - success; otherwise - failed; + * */ + virtual int EXTAPI GetDefaultParams (SEncParamExt* pParam) = 0; + /// uninitialize the encoder + virtual int EXTAPI Uninitialize() = 0; + + /** + * @brief Encode one frame + * @param kpSrcPic the pointer to the source luminance plane + * chrominance data: + * CbData = kpSrc + m_iMaxPicWidth * m_iMaxPicHeight; + * CrData = CbData + (m_iMaxPicWidth * m_iMaxPicHeight)/4; + * the application calling this interface needs to ensure the data validation between the location + * @param pBsInfo output bit stream + * @return 0 - success; otherwise -failed; + */ + virtual int EXTAPI EncodeFrame (const SSourcePicture* kpSrcPic, SFrameBSInfo* pBsInfo) = 0; + + /** + * @brief Encode the parameters from output bit stream + * @param pBsInfo output bit stream + * @return 0 - success; otherwise - failed; + */ + virtual int EXTAPI EncodeParameterSets (SFrameBSInfo* pBsInfo) = 0; + + /** + * @brief Force encoder to encoder frame as IDR if bIDR set as true + * @param bIDR true: force encoder to encode frame as IDR frame;false, return 1 and nothing to do + * @return 0 - success; otherwise - failed; + */ + virtual int EXTAPI ForceIntraFrame (bool bIDR, int iLayerId = -1) = 0; + + /** + * @brief Set option for encoder, detail option type, please refer to enumurate ENCODER_OPTION. + * @param pOption option for encoder such as InDataFormat, IDRInterval, SVC Encode Param, Frame Rate, Bitrate,... + * @return CM_RETURN: 0 - success; otherwise - failed; + */ + virtual int EXTAPI SetOption (ENCODER_OPTION eOptionId, void* pOption) = 0; + + /** + * @brief Get option for encoder, detail option type, please refer to enumurate ENCODER_OPTION. + * @param pOption option for encoder such as InDataFormat, IDRInterval, SVC Encode Param, Frame Rate, Bitrate,... + * @return CM_RETURN: 0 - success; otherwise - failed; + */ + virtual int EXTAPI GetOption (ENCODER_OPTION eOptionId, void* pOption) = 0; + virtual ~ISVCEncoder() {} +}; + + + +/** +* @brief Decoder definition +*/ +class ISVCDecoder { + public: + + /** + * @brief Initilaize decoder + * @param pParam parameter for decoder + * @return 0 - success; otherwise - failed; + */ + virtual long EXTAPI Initialize (const SDecodingParam* pParam) = 0; + + /// Uninitialize the decoder + virtual long EXTAPI Uninitialize() = 0; + + /** + * @brief Decode one frame + * @param pSrc the h264 stream to be decoded + * @param iSrcLen the length of h264 stream + * @param ppDst buffer pointer of decoded data (YUV) + * @param pStride output stride + * @param iWidth output width + * @param iHeight output height + * @return 0 - success; otherwise -failed; + */ + virtual DECODING_STATE EXTAPI DecodeFrame (const unsigned char* pSrc, + const int iSrcLen, + unsigned char** ppDst, + int* pStride, + int& iWidth, + int& iHeight) = 0; + + /** + * @brief For slice level DecodeFrameNoDelay() (4 parameters input), + * whatever the function return value is, the output data + * of I420 format will only be available when pDstInfo->iBufferStatus == 1,. + * This function will parse and reconstruct the input frame immediately if it is complete + * It is recommended as the main decoding function for H.264/AVC format input + * @param pSrc the h264 stream to be decoded + * @param iSrcLen the length of h264 stream + * @param ppDst buffer pointer of decoded data (YUV) + * @param pDstInfo information provided to API(width, height, etc.) + * @return 0 - success; otherwise -failed; + */ + virtual DECODING_STATE EXTAPI DecodeFrameNoDelay (const unsigned char* pSrc, + const int iSrcLen, + unsigned char** ppDst, + SBufferInfo* pDstInfo) = 0; + + /** + * @brief For slice level DecodeFrame2() (4 parameters input), + * whatever the function return value is, the output data + * of I420 format will only be available when pDstInfo->iBufferStatus == 1,. + * (e.g., in multi-slice cases, only when the whole picture + * is completely reconstructed, this variable would be set equal to 1.) + * @param pSrc the h264 stream to be decoded + * @param iSrcLen the length of h264 stream + * @param ppDst buffer pointer of decoded data (YUV) + * @param pDstInfo information provided to API(width, height, etc.) + * @return 0 - success; otherwise -failed; + */ + virtual DECODING_STATE EXTAPI DecodeFrame2 (const unsigned char* pSrc, + const int iSrcLen, + unsigned char** ppDst, + SBufferInfo* pDstInfo) = 0; + + + /** + * @brief This function gets a decoded ready frame remaining in buffers after the last frame has been decoded. + * Use GetOption with option DECODER_OPTION_NUM_OF_FRAMES_REMAINING_IN_BUFFER to get the number of frames remaining in buffers. + * Note that it is only applicable for profile_idc != 66 + * @param ppDst buffer pointer of decoded data (YUV) + * @param pDstInfo information provided to API(width, height, etc.) + * @return 0 - success; otherwise -failed; + */ + virtual DECODING_STATE EXTAPI FlushFrame (unsigned char** ppDst, + SBufferInfo* pDstInfo) = 0; + + /** + * @brief This function parse input bitstream only, and rewrite possible SVC syntax to AVC syntax + * @param pSrc the h264 stream to be decoded + * @param iSrcLen the length of h264 stream + * @param pDstInfo bit stream info + * @return 0 - success; otherwise -failed; + */ + virtual DECODING_STATE EXTAPI DecodeParser (const unsigned char* pSrc, + const int iSrcLen, + SParserBsInfo* pDstInfo) = 0; + + /** + * @brief This API does not work for now!! This is for future use to support non-I420 color format output. + * @param pSrc the h264 stream to be decoded + * @param iSrcLen the length of h264 stream + * @param pDst buffer pointer of decoded data (YUV) + * @param iDstStride output stride + * @param iDstLen bit stream info + * @param iWidth output width + * @param iHeight output height + * @param iColorFormat output color format + * @return to do ... + */ + virtual DECODING_STATE EXTAPI DecodeFrameEx (const unsigned char* pSrc, + const int iSrcLen, + unsigned char* pDst, + int iDstStride, + int& iDstLen, + int& iWidth, + int& iHeight, + int& iColorFormat) = 0; + + /** + * @brief Set option for decoder, detail option type, please refer to enumurate DECODER_OPTION. + * @param pOption option for decoder such as OutDataFormat, Eos Flag, EC method, ... + * @return CM_RETURN: 0 - success; otherwise - failed; + */ + virtual long EXTAPI SetOption (DECODER_OPTION eOptionId, void* pOption) = 0; + + /** + * @brief Get option for decoder, detail option type, please refer to enumurate DECODER_OPTION. + * @param pOption option for decoder such as OutDataFormat, Eos Flag, EC method, ... + * @return CM_RETURN: 0 - success; otherwise - failed; + */ + virtual long EXTAPI GetOption (DECODER_OPTION eOptionId, void* pOption) = 0; + virtual ~ISVCDecoder() {} +}; + + +extern "C" +{ +#else + +typedef struct ISVCEncoderVtbl ISVCEncoderVtbl; +typedef const ISVCEncoderVtbl* ISVCEncoder; +struct ISVCEncoderVtbl { + +int (*Initialize) (ISVCEncoder*, const SEncParamBase* pParam); +int (*InitializeExt) (ISVCEncoder*, const SEncParamExt* pParam); + +int (*GetDefaultParams) (ISVCEncoder*, SEncParamExt* pParam); + +int (*Uninitialize) (ISVCEncoder*); + +int (*EncodeFrame) (ISVCEncoder*, const SSourcePicture* kpSrcPic, SFrameBSInfo* pBsInfo); +int (*EncodeParameterSets) (ISVCEncoder*, SFrameBSInfo* pBsInfo); + +int (*ForceIntraFrame) (ISVCEncoder*, bool bIDR); + +int (*SetOption) (ISVCEncoder*, ENCODER_OPTION eOptionId, void* pOption); +int (*GetOption) (ISVCEncoder*, ENCODER_OPTION eOptionId, void* pOption); +}; + +typedef struct ISVCDecoderVtbl ISVCDecoderVtbl; +typedef const ISVCDecoderVtbl* ISVCDecoder; +struct ISVCDecoderVtbl { +long (*Initialize) (ISVCDecoder*, const SDecodingParam* pParam); +long (*Uninitialize) (ISVCDecoder*); + +DECODING_STATE (*DecodeFrame) (ISVCDecoder*, const unsigned char* pSrc, + const int iSrcLen, + unsigned char** ppDst, + int* pStride, + int* iWidth, + int* iHeight); + +DECODING_STATE (*DecodeFrameNoDelay) (ISVCDecoder*, const unsigned char* pSrc, + const int iSrcLen, + unsigned char** ppDst, + SBufferInfo* pDstInfo); + +DECODING_STATE (*DecodeFrame2) (ISVCDecoder*, const unsigned char* pSrc, + const int iSrcLen, + unsigned char** ppDst, + SBufferInfo* pDstInfo); + +DECODING_STATE (*FlushFrame) (ISVCDecoder*, unsigned char** ppDst, + SBufferInfo* pDstInfo); + +DECODING_STATE (*DecodeParser) (ISVCDecoder*, const unsigned char* pSrc, + const int iSrcLen, + SParserBsInfo* pDstInfo); + +DECODING_STATE (*DecodeFrameEx) (ISVCDecoder*, const unsigned char* pSrc, + const int iSrcLen, + unsigned char* pDst, + int iDstStride, + int* iDstLen, + int* iWidth, + int* iHeight, + int* iColorFormat); + +long (*SetOption) (ISVCDecoder*, DECODER_OPTION eOptionId, void* pOption); +long (*GetOption) (ISVCDecoder*, DECODER_OPTION eOptionId, void* pOption); +}; +#endif + +typedef void (*WelsTraceCallback) (void* ctx, int level, const char* string); + +/** @brief Create encoder + * @param ppEncoder encoder + * @return 0 - success; otherwise - failed; +*/ +int WelsCreateSVCEncoder (ISVCEncoder** ppEncoder); + + +/** @brief Destroy encoder +* @param pEncoder encoder + * @return void +*/ +void WelsDestroySVCEncoder (ISVCEncoder* pEncoder); + + +/** @brief Get the capability of decoder + * @param pDecCapability decoder capability + * @return 0 - success; otherwise - failed; +*/ +int WelsGetDecoderCapability (SDecoderCapability* pDecCapability); + + +/** @brief Create decoder + * @param ppDecoder decoder + * @return 0 - success; otherwise - failed; +*/ +long WelsCreateDecoder (ISVCDecoder** ppDecoder); + + +/** @brief Destroy decoder + * @param pDecoder decoder + * @return void +*/ +void WelsDestroyDecoder (ISVCDecoder* pDecoder); + +/** @brief Get codec version + * Note, old versions of Mingw (GCC < 4.7) are buggy and use an + * incorrect/different ABI for calling this function, making it + * incompatible with MSVC builds. + * @return The linked codec version +*/ +OpenH264Version WelsGetCodecVersion (void); + +/** @brief Get codec version + * @param pVersion struct to fill in with the version +*/ +void WelsGetCodecVersionEx (OpenH264Version* pVersion); + +#ifdef __cplusplus +} +#endif + +#endif//WELS_VIDEO_CODEC_SVC_API_H__ diff --git a/third-party/openh264/third_party/openh264/src/codec/api/svc/codec_app_def.h b/third-party/openh264/third_party/openh264/src/codec/api/svc/codec_app_def.h new file mode 100644 index 0000000000..bb3c3d67b7 --- /dev/null +++ b/third-party/openh264/third_party/openh264/src/codec/api/svc/codec_app_def.h @@ -0,0 +1,810 @@ +/*! + * \copy + * Copyright (c) 2013, Cisco Systems + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + * COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN + * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ + + + +#ifndef WELS_VIDEO_CODEC_APPLICATION_DEFINITION_H__ +#define WELS_VIDEO_CODEC_APPLICATION_DEFINITION_H__ +/** + * @file codec_app_def.h + * @brief Data and /or structures introduced in Cisco OpenH264 application +*/ + +#include "codec_def.h" +/* Constants */ +#define MAX_TEMPORAL_LAYER_NUM 4 +#define MAX_SPATIAL_LAYER_NUM 4 +#define MAX_QUALITY_LAYER_NUM 4 + +#define MAX_LAYER_NUM_OF_FRAME 128 +#define MAX_NAL_UNITS_IN_LAYER 128 ///< predetermined here, adjust it later if need + +#define MAX_RTP_PAYLOAD_LEN 1000 +#define AVERAGE_RTP_PAYLOAD_LEN 800 + + +#define SAVED_NALUNIT_NUM_TMP ( (MAX_SPATIAL_LAYER_NUM*MAX_QUALITY_LAYER_NUM) + 1 + MAX_SPATIAL_LAYER_NUM ) ///< SPS/PPS + SEI/SSEI + PADDING_NAL +#define MAX_SLICES_NUM_TMP ( ( MAX_NAL_UNITS_IN_LAYER - SAVED_NALUNIT_NUM_TMP ) / 3 ) + + +#define AUTO_REF_PIC_COUNT -1 ///< encoder selects the number of reference frame automatically +#define UNSPECIFIED_BIT_RATE 0 ///< to do: add detail comment + +/** + * @brief Struct of OpenH264 version + */ +/// +/// E.g. SDK version is 1.2.0.0, major version number is 1, minor version number is 2, and revision number is 0. +typedef struct _tagVersion { + unsigned int uMajor; ///< The major version number + unsigned int uMinor; ///< The minor version number + unsigned int uRevision; ///< The revision number + unsigned int uReserved; ///< The reserved number, it should be 0. +} OpenH264Version; + +/** +* @brief Decoding status +*/ +typedef enum { + /** + * Errors derived from bitstream parsing + */ + dsErrorFree = 0x00, ///< bit stream error-free + dsFramePending = 0x01, ///< need more throughput to generate a frame output, + dsRefLost = 0x02, ///< layer lost at reference frame with temporal id 0 + dsBitstreamError = 0x04, ///< error bitstreams(maybe broken internal frame) the decoder cared + dsDepLayerLost = 0x08, ///< dependented layer is ever lost + dsNoParamSets = 0x10, ///< no parameter set NALs involved + dsDataErrorConcealed = 0x20, ///< current data error concealed specified + dsRefListNullPtrs = 0x40, /// do not write any of the following information to the header + unsigned char + uiVideoFormat; // EVideoFormatSPS; 3 bits in header; 0-5 => component, kpal, ntsc, secam, mac, undef + bool bFullRange; // false => analog video data range [16, 235]; true => full data range [0,255] + bool bColorDescriptionPresent; // false => do not write any of the following three items to the header + unsigned char + uiColorPrimaries; // EColorPrimaries; 8 bits in header; 0 - 9 => ???, bt709, undef, ???, bt470m, bt470bg, + // smpte170m, smpte240m, film, bt2020 + unsigned char + uiTransferCharacteristics; // ETransferCharacteristics; 8 bits in header; 0 - 15 => ???, bt709, undef, ???, bt470m, bt470bg, smpte170m, + // smpte240m, linear, log100, log316, iec61966-2-4, bt1361e, iec61966-2-1, bt2020-10, bt2020-12 + unsigned char + uiColorMatrix; // EColorMatrix; 8 bits in header (corresponds to FFmpeg "colorspace"); 0 - 10 => GBR, bt709, + // undef, ???, fcc, bt470bg, smpte170m, smpte240m, YCgCo, bt2020nc, bt2020c + + bool bAspectRatioPresent; ///< aspect ratio present in VUI + ESampleAspectRatio eAspectRatio; ///< aspect ratio idc + unsigned short sAspectRatioExtWidth; ///< use if aspect ratio idc == 255 + unsigned short sAspectRatioExtHeight; ///< use if aspect ratio idc == 255 + +} SSpatialLayerConfig; + +/** +* @brief Encoder usage type +*/ +typedef enum { + CAMERA_VIDEO_REAL_TIME, ///< camera video for real-time communication + SCREEN_CONTENT_REAL_TIME, ///< screen content signal + CAMERA_VIDEO_NON_REAL_TIME, + SCREEN_CONTENT_NON_REAL_TIME, + INPUT_CONTENT_TYPE_ALL, +} EUsageType; + +/** +* @brief Enumulate the complexity mode +*/ +typedef enum { + LOW_COMPLEXITY = 0, ///< the lowest compleixty,the fastest speed, + MEDIUM_COMPLEXITY, ///< medium complexity, medium speed,medium quality + HIGH_COMPLEXITY ///< high complexity, lowest speed, high quality +} ECOMPLEXITY_MODE; + +/** + * @brief Enumulate for the stategy of SPS/PPS strategy + */ +typedef enum { + CONSTANT_ID = 0, ///< constant id in SPS/PPS + INCREASING_ID = 0x01, ///< SPS/PPS id increases at each IDR + SPS_LISTING = 0x02, ///< using SPS in the existing list if possible + SPS_LISTING_AND_PPS_INCREASING = 0x03, + SPS_PPS_LISTING = 0x06, +} EParameterSetStrategy; + +// TODO: Refine the parameters definition. +/** +* @brief SVC Encoding Parameters +*/ +typedef struct TagEncParamBase { + EUsageType + iUsageType; ///< application type; please refer to the definition of EUsageType + + int iPicWidth; ///< width of picture in luminance samples (the maximum of all layers if multiple spatial layers presents) + int iPicHeight; ///< height of picture in luminance samples((the maximum of all layers if multiple spatial layers presents) + int iTargetBitrate; ///< target bitrate desired, in unit of bps + RC_MODES iRCMode; ///< rate control mode + float fMaxFrameRate; ///< maximal input frame rate + +} SEncParamBase, *PEncParamBase; + +/** +* @brief SVC Encoding Parameters extention +*/ +typedef struct TagEncParamExt { + EUsageType + iUsageType; ///< same as in TagEncParamBase + + int iPicWidth; ///< same as in TagEncParamBase + int iPicHeight; ///< same as in TagEncParamBase + int iTargetBitrate; ///< same as in TagEncParamBase + RC_MODES iRCMode; ///< same as in TagEncParamBase + float fMaxFrameRate; ///< same as in TagEncParamBase + + int iTemporalLayerNum; ///< temporal layer number, max temporal layer = 4 + int iSpatialLayerNum; ///< spatial layer number,1<= iSpatialLayerNum <= MAX_SPATIAL_LAYER_NUM, MAX_SPATIAL_LAYER_NUM = 4 + SSpatialLayerConfig sSpatialLayers[MAX_SPATIAL_LAYER_NUM]; + + ECOMPLEXITY_MODE iComplexityMode; + unsigned int uiIntraPeriod; ///< period of Intra frame + int iNumRefFrame; ///< number of reference frame used + EParameterSetStrategy + eSpsPpsIdStrategy; ///< different stategy in adjust ID in SPS/PPS: 0- constant ID, 1-additional ID, 6-mapping and additional + bool bPrefixNalAddingCtrl; ///< false:not use Prefix NAL; true: use Prefix NAL + bool bEnableSSEI; ///< false:not use SSEI; true: use SSEI -- TODO: planning to remove the interface of SSEI + bool bSimulcastAVC; ///< (when encoding more than 1 spatial layer) false: use SVC syntax for higher layers; true: use Simulcast AVC + int iPaddingFlag; ///< 0:disable padding;1:padding + int iEntropyCodingModeFlag; ///< 0:CAVLC 1:CABAC. + + /* rc control */ + bool bEnableFrameSkip; ///< False: don't skip frame even if VBV buffer overflow.True: allow skipping frames to keep the bitrate within limits + int iMaxBitrate; ///< the maximum bitrate, in unit of bps, set it to UNSPECIFIED_BIT_RATE if not needed + int iMaxQp; ///< the maximum QP encoder supports + int iMinQp; ///< the minmum QP encoder supports + unsigned int uiMaxNalSize; ///< the maximum NAL size. This value should be not 0 for dynamic slice mode + + /*LTR settings*/ + bool bEnableLongTermReference; ///< 1: on, 0: off + int iLTRRefNum; ///< the number of LTR(long term reference),TODO: not supported to set it arbitrary yet + unsigned int iLtrMarkPeriod; ///< the LTR marked period that is used in feedback. + /* multi-thread settings*/ + unsigned short + iMultipleThreadIdc; ///< 1 # 0: auto(dynamic imp. internal encoder); 1: multiple threads imp. disabled; lager than 1: count number of threads; + bool bUseLoadBalancing; ///< only used when uiSliceMode=1 or 3, will change slicing of a picture during the run-time of multi-thread encoding, so the result of each run may be different + + /* Deblocking loop filter */ + int iLoopFilterDisableIdc; ///< 0: on, 1: off, 2: on except for slice boundaries + int iLoopFilterAlphaC0Offset; ///< AlphaOffset: valid range [-6, 6], default 0 + int iLoopFilterBetaOffset; ///< BetaOffset: valid range [-6, 6], default 0 + /*pre-processing feature*/ + bool bEnableDenoise; ///< denoise control + bool bEnableBackgroundDetection; ///< background detection control //VAA_BACKGROUND_DETECTION //BGD cmd + bool bEnableAdaptiveQuant; ///< adaptive quantization control + bool bEnableFrameCroppingFlag; ///< enable frame cropping flag: TRUE always in application + bool bEnableSceneChangeDetect; + + bool bIsLosslessLink; ///< LTR advanced setting +} SEncParamExt; + +/** +* @brief Define a new struct to show the property of video bitstream. +*/ +typedef struct { + unsigned int size; ///< size of the struct + VIDEO_BITSTREAM_TYPE eVideoBsType; ///< video stream type (AVC/SVC) +} SVideoProperty; + +/** +* @brief SVC Decoding Parameters, reserved here and potential applicable in the future +*/ +typedef struct TagSVCDecodingParam { + char* pFileNameRestructed; ///< file name of reconstructed frame used for PSNR calculation based debug + + unsigned int uiCpuLoad; ///< CPU load + unsigned char uiTargetDqLayer; ///< setting target dq layer id + + ERROR_CON_IDC eEcActiveIdc; ///< whether active error concealment feature in decoder + bool bParseOnly; ///< decoder for parse only, no reconstruction. When it is true, SPS/PPS size should not exceed SPS_PPS_BS_SIZE (128). Otherwise, it will return error info + + SVideoProperty sVideoProperty; ///< video stream property +} SDecodingParam, *PDecodingParam; + +/** +* @brief Bitstream inforamtion of a layer being encoded +*/ +typedef struct { + unsigned char uiTemporalId; + unsigned char uiSpatialId; + unsigned char uiQualityId; + EVideoFrameType eFrameType; + unsigned char uiLayerType; + + /** + * The sub sequence layers are ordered hierarchically based on their dependency on each other so that any picture in a layer shall not be + * predicted from any picture on any higher layer. + */ + int iSubSeqId; ///< refer to D.2.11 Sub-sequence information SEI message semantics + int iNalCount; ///< count number of NAL coded already + int* pNalLengthInByte; ///< length of NAL size in byte from 0 to iNalCount-1 + unsigned char* pBsBuf; ///< buffer of bitstream contained +} SLayerBSInfo, *PLayerBSInfo; + +/** +* @brief Frame bit stream info +*/ +typedef struct { + int iLayerNum; + SLayerBSInfo sLayerInfo[MAX_LAYER_NUM_OF_FRAME]; + + EVideoFrameType eFrameType; + int iFrameSizeInBytes; + long long uiTimeStamp; +} SFrameBSInfo, *PFrameBSInfo; + +/** +* @brief Structure for source picture +*/ +typedef struct Source_Picture_s { + int iColorFormat; ///< color space type + int iStride[4]; ///< stride for each plane pData + unsigned char* pData[4]; ///< plane pData + int iPicWidth; ///< luma picture width in x coordinate + int iPicHeight; ///< luma picture height in y coordinate + long long uiTimeStamp; ///< timestamp of the source picture, unit: millisecond +} SSourcePicture; +/** +* @brief Structure for bit rate info +*/ +typedef struct TagBitrateInfo { + LAYER_NUM iLayer; + int iBitrate; ///< the maximum bitrate +} SBitrateInfo; + +/** +* @brief Structure for dump layer info +*/ +typedef struct TagDumpLayer { + int iLayer; + char* pFileName; +} SDumpLayer; + +/** +* @brief Structure for profile info in layer +* +*/ +typedef struct TagProfileInfo { + int iLayer; + EProfileIdc uiProfileIdc; ///< the profile info +} SProfileInfo; + +/** +* @brief Structure for level info in layer +* +*/ +typedef struct TagLevelInfo { + int iLayer; + ELevelIdc uiLevelIdc; ///< the level info +} SLevelInfo; +/** +* @brief Structure for dilivery status +* +*/ +typedef struct TagDeliveryStatus { + bool bDeliveryFlag; ///< 0: the previous frame isn't delivered,1: the previous frame is delivered + int iDropFrameType; ///< the frame type that is dropped; reserved + int iDropFrameSize; ///< the frame size that is dropped; reserved +} SDeliveryStatus; + +/** +* @brief The capability of decoder, for SDP negotiation +*/ +typedef struct TagDecoderCapability { + int iProfileIdc; ///< profile_idc + int iProfileIop; ///< profile-iop + int iLevelIdc; ///< level_idc + int iMaxMbps; ///< max-mbps + int iMaxFs; ///< max-fs + int iMaxCpb; ///< max-cpb + int iMaxDpb; ///< max-dpb + int iMaxBr; ///< max-br + bool bRedPicCap; ///< redundant-pic-cap +} SDecoderCapability; + +/** +* @brief Structure for parse only output +*/ +typedef struct TagParserBsInfo { + int iNalNum; ///< total NAL number in current AU + int* pNalLenInByte; ///< each nal length + unsigned char* pDstBuff; ///< outputted dst buffer for parsed bitstream + int iSpsWidthInPixel; ///< required SPS width info + int iSpsHeightInPixel; ///< required SPS height info + unsigned long long uiInBsTimeStamp; ///< input BS timestamp + unsigned long long uiOutBsTimeStamp; ///< output BS timestamp +} SParserBsInfo, *PParserBsInfo; + +/** +* @brief Structure for encoder statistics +*/ +typedef struct TagVideoEncoderStatistics { + unsigned int uiWidth; ///< the width of encoded frame + unsigned int uiHeight; ///< the height of encoded frame + //following standard, will be 16x aligned, if there are multiple spatial, this is of the highest + float fAverageFrameSpeedInMs; ///< average_Encoding_Time + + // rate control related + float fAverageFrameRate; ///< the average frame rate in, calculate since encoding starts, supposed that the input timestamp is in unit of ms + float fLatestFrameRate; ///< the frame rate in, in the last second, supposed that the input timestamp is in unit of ms (? useful for checking BR, but is it easy to calculate? + unsigned int uiBitRate; ///< sendrate in Bits per second, calculated within the set time-window + unsigned int uiAverageFrameQP; ///< the average QP of last encoded frame + + unsigned int uiInputFrameCount; ///< number of frames + unsigned int uiSkippedFrameCount; ///< number of frames + + unsigned int uiResolutionChangeTimes; ///< uiResolutionChangeTimes + unsigned int uiIDRReqNum; ///< number of IDR requests + unsigned int uiIDRSentNum; ///< number of actual IDRs sent + unsigned int uiLTRSentNum; ///< number of LTR sent/marked + + long long iStatisticsTs; ///< Timestamp of updating the statistics + + unsigned long iTotalEncodedBytes; + unsigned long iLastStatisticsBytes; + unsigned long iLastStatisticsFrameCount; +} SEncoderStatistics; + +/** +* @brief Structure for decoder statistics +*/ +typedef struct TagVideoDecoderStatistics { + unsigned int uiWidth; ///< the width of encode/decode frame + unsigned int uiHeight; ///< the height of encode/decode frame + float fAverageFrameSpeedInMs; ///< average_Decoding_Time + float fActualAverageFrameSpeedInMs; ///< actual average_Decoding_Time, including freezing pictures + unsigned int uiDecodedFrameCount; ///< number of frames + unsigned int uiResolutionChangeTimes; ///< uiResolutionChangeTimes + unsigned int uiIDRCorrectNum; ///< number of correct IDR received + //EC on related + unsigned int + uiAvgEcRatio; ///< when EC is on, the average ratio of total EC areas, can be an indicator of reconstruction quality + unsigned int + uiAvgEcPropRatio; ///< when EC is on, the rough average ratio of propogate EC areas, can be an indicator of reconstruction quality + unsigned int uiEcIDRNum; ///< number of actual unintegrity IDR or not received but eced + unsigned int uiEcFrameNum; ///< + unsigned int uiIDRLostNum; ///< number of whole lost IDR + unsigned int + uiFreezingIDRNum; ///< number of freezing IDR with error (partly received), under resolution change + unsigned int uiFreezingNonIDRNum; ///< number of freezing non-IDR with error + int iAvgLumaQp; ///< average luma QP. default: -1, no correct frame outputted + int iSpsReportErrorNum; ///< number of Sps Invalid report + int iSubSpsReportErrorNum; ///< number of SubSps Invalid report + int iPpsReportErrorNum; ///< number of Pps Invalid report + int iSpsNoExistNalNum; ///< number of Sps NoExist Nal + int iSubSpsNoExistNalNum; ///< number of SubSps NoExist Nal + int iPpsNoExistNalNum; ///< number of Pps NoExist Nal + + unsigned int uiProfile; ///< Profile idc in syntax + unsigned int uiLevel; ///< level idc according to Annex A-1 + + int iCurrentActiveSpsId; ///< current active SPS id + int iCurrentActivePpsId; ///< current active PPS id + + unsigned int iStatisticsLogInterval; ///< frame interval of statistics log +} SDecoderStatistics; // in building, coming soon + +/** +* @brief Structure for sample aspect ratio (SAR) info in VUI +*/ +typedef struct TagVuiSarInfo { + unsigned int uiSarWidth; ///< SAR width + unsigned int uiSarHeight; ///< SAR height + bool bOverscanAppropriateFlag; ///< SAR overscan flag +} SVuiSarInfo, *PVuiSarInfo; + +#endif//WELS_VIDEO_CODEC_APPLICATION_DEFINITION_H__ diff --git a/third-party/openh264/third_party/openh264/src/codec/api/svc/codec_def.h b/third-party/openh264/third_party/openh264/src/codec/api/svc/codec_def.h new file mode 100644 index 0000000000..edde5f4a2e --- /dev/null +++ b/third-party/openh264/third_party/openh264/src/codec/api/svc/codec_def.h @@ -0,0 +1,216 @@ +/*! + * \copy + * Copyright (c) 2013, Cisco Systems + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + * COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN + * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ + +#ifndef WELS_VIDEO_CODEC_DEFINITION_H__ +#define WELS_VIDEO_CODEC_DEFINITION_H__ + +/** + * @file codec_def.h +*/ + +/** +* @brief Enumerate the type of video format +*/ +typedef enum { + videoFormatRGB = 1, ///< rgb color formats + videoFormatRGBA = 2, + videoFormatRGB555 = 3, + videoFormatRGB565 = 4, + videoFormatBGR = 5, + videoFormatBGRA = 6, + videoFormatABGR = 7, + videoFormatARGB = 8, + + videoFormatYUY2 = 20, ///< yuv color formats + videoFormatYVYU = 21, + videoFormatUYVY = 22, + videoFormatI420 = 23, ///< the same as IYUV + videoFormatYV12 = 24, + videoFormatInternal = 25, ///< only used in SVC decoder testbed + + videoFormatNV12 = 26, ///< new format for output by DXVA decoding + + videoFormatVFlip = 0x80000000 +} EVideoFormatType; + +/** +* @brief Enumerate video frame type +*/ +typedef enum { + videoFrameTypeInvalid, ///< encoder not ready or parameters are invalidate + videoFrameTypeIDR, ///< IDR frame in H.264 + videoFrameTypeI, ///< I frame type + videoFrameTypeP, ///< P frame type + videoFrameTypeSkip, ///< skip the frame based encoder kernel + videoFrameTypeIPMixed ///< a frame where I and P slices are mixing, not supported yet +} EVideoFrameType; + +/** +* @brief Enumerate return type +*/ +typedef enum { + cmResultSuccess, ///< successful + cmInitParaError, ///< parameters are invalid + cmUnknownReason, + cmMallocMemeError, ///< malloc a memory error + cmInitExpected, ///< initial action is expected + cmUnsupportedData +} CM_RETURN; + +/** +* @brief Enumulate the nal unit type +*/ +enum ENalUnitType { + NAL_UNKNOWN = 0, + NAL_SLICE = 1, + NAL_SLICE_DPA = 2, + NAL_SLICE_DPB = 3, + NAL_SLICE_DPC = 4, + NAL_SLICE_IDR = 5, ///< ref_idc != 0 + NAL_SEI = 6, ///< ref_idc == 0 + NAL_SPS = 7, + NAL_PPS = 8 + ///< ref_idc == 0 for 6,9,10,11,12 +}; + +/** +* @brief NRI: eNalRefIdc +*/ +enum ENalPriority { + NAL_PRIORITY_DISPOSABLE = 0, + NAL_PRIORITY_LOW = 1, + NAL_PRIORITY_HIGH = 2, + NAL_PRIORITY_HIGHEST = 3 +}; + +#define IS_PARAMETER_SET_NAL(eNalRefIdc, eNalType) \ +( (eNalRefIdc == NAL_PRIORITY_HIGHEST) && (eNalType == (NAL_SPS|NAL_PPS) || eNalType == NAL_SPS) ) + +#define IS_IDR_NAL(eNalRefIdc, eNalType) \ +( (eNalRefIdc == NAL_PRIORITY_HIGHEST) && (eNalType == NAL_SLICE_IDR) ) + +#define FRAME_NUM_PARAM_SET (-1) +#define FRAME_NUM_IDR 0 + +/** + * @brief eDeblockingIdc + */ +enum { + DEBLOCKING_IDC_0 = 0, + DEBLOCKING_IDC_1 = 1, + DEBLOCKING_IDC_2 = 2 +}; +#define DEBLOCKING_OFFSET (6) +#define DEBLOCKING_OFFSET_MINUS (-6) + +/* Error Tools definition */ +typedef unsigned short ERR_TOOL; + +/** + @brief to do +*/ +enum { + ET_NONE = 0x00, ///< NONE Error Tools + ET_IP_SCALE = 0x01, ///< IP Scalable + ET_FMO = 0x02, ///< Flexible Macroblock Ordering + ET_IR_R1 = 0x04, ///< Intra Refresh in predifined 2% MB + ET_IR_R2 = 0x08, ///< Intra Refresh in predifined 5% MB + ET_IR_R3 = 0x10, ///< Intra Refresh in predifined 10% MB + ET_FEC_HALF = 0x20, ///< Forward Error Correction in 50% redundency mode + ET_FEC_FULL = 0x40, ///< Forward Error Correction in 100% redundency mode + ET_RFS = 0x80 ///< Reference Frame Selection +}; + +/** +* @brief Information of coded Slice(=NAL)(s) +*/ +typedef struct SliceInformation { + unsigned char* pBufferOfSlices; ///< base buffer of coded slice(s) + int iCodedSliceCount; ///< number of coded slices + unsigned int* pLengthOfSlices; ///< array of slices length accordingly by number of slice + int iFecType; ///< FEC type[0, 50%FEC, 100%FEC] + unsigned char uiSliceIdx; ///< index of slice in frame [FMO: 0,..,uiSliceCount-1; No FMO: 0] + unsigned char uiSliceCount; ///< count number of slice in frame [FMO: 2-8; No FMO: 1] + char iFrameIndex; ///< index of frame[-1, .., idr_interval-1] + unsigned char uiNalRefIdc; ///< NRI, priority level of slice(NAL) + unsigned char uiNalType; ///< NAL type + unsigned char + uiContainingFinalNal; ///< whether final NAL is involved in buffer of coded slices, flag used in Pause feature in T27 +} SliceInfo, *PSliceInfo; + +/** +* @brief thresholds of the initial, maximal and minimal rate +*/ +typedef struct { + int iWidth; ///< frame width + int iHeight; ///< frame height + int iThresholdOfInitRate; ///< threshold of initial rate + int iThresholdOfMaxRate; ///< threshold of maximal rate + int iThresholdOfMinRate; ///< threshold of minimal rate + int iMinThresholdFrameRate; ///< min frame rate min + int iSkipFrameRate; ///< skip to frame rate min + int iSkipFrameStep; ///< how many frames to skip +} SRateThresholds, *PRateThresholds; + +/** +* @brief Structure for decoder memery +*/ +typedef struct TagSysMemBuffer { + int iWidth; ///< width of decoded pic for display + int iHeight; ///< height of decoded pic for display + int iFormat; ///< type is "EVideoFormatType" + int iStride[2]; ///< stride of 2 component +} SSysMEMBuffer; + +/** +* @brief Buffer info +*/ +typedef struct TagBufferInfo { + int iBufferStatus; ///< 0: one frame data is not ready; 1: one frame data is ready + unsigned long long uiInBsTimeStamp; ///< input BS timestamp + unsigned long long uiOutYuvTimeStamp; ///< output YUV timestamp, when bufferstatus is 1 + union { + SSysMEMBuffer sSystemBuffer; ///< memory info for one picture + } UsrData; ///< output buffer info + unsigned char* pDst[3]; //point to picture YUV data +} SBufferInfo; + + +/** +* @brief In a GOP, multiple of the key frame number, derived from +* the number of layers(index or array below) +*/ +static const char kiKeyNumMultiple[] = { + 1, 1, 2, 4, 8, 16, +}; + +#endif//WELS_VIDEO_CODEC_DEFINITION_H__ diff --git a/third-party/openh264/third_party/openh264/src/codec/api/svc/codec_ver.h b/third-party/openh264/third_party/openh264/src/codec/api/svc/codec_ver.h new file mode 100644 index 0000000000..a4e494f6b0 --- /dev/null +++ b/third-party/openh264/third_party/openh264/src/codec/api/svc/codec_ver.h @@ -0,0 +1,15 @@ +//The current file is auto-generated by script: generate_codec_ver.sh +#ifndef CODEC_VER_H +#define CODEC_VER_H + +#include "codec_app_def.h" + +static const OpenH264Version g_stCodecVersion = {2, 1, 0, 2002}; +static const char* const g_strCodecVer = "OpenH264 version:2.1.0.2002"; + +#define OPENH264_MAJOR (2) +#define OPENH264_MINOR (1) +#define OPENH264_REVISION (0) +#define OPENH264_RESERVED (2002) + +#endif // CODEC_VER_H diff --git a/third-party/openh264/third_party/openh264/src/codec/api/svc/meson.build b/third-party/openh264/third_party/openh264/src/codec/api/svc/meson.build new file mode 100644 index 0000000000..d1683fcaab --- /dev/null +++ b/third-party/openh264/third_party/openh264/src/codec/api/svc/meson.build @@ -0,0 +1,13 @@ +headers = [ + 'codec_api.h', + 'codec_app_def.h', + 'codec_def.h', + 'codec_ver.h', +] + +foreach header : headers + api_headers += [[header, files(header)]] +endforeach + +install_headers(headers, + subdir: 'wels') diff --git a/third-party/openh264/third_party/openh264/src/codec/build/android/.gitignore b/third-party/openh264/third_party/openh264/src/codec/build/android/.gitignore new file mode 100644 index 0000000000..2c1dd5fcbc --- /dev/null +++ b/third-party/openh264/third_party/openh264/src/codec/build/android/.gitignore @@ -0,0 +1,6 @@ +build.xml +local.properties +proguard-project.txt +gen +bin +project.properties diff --git a/third-party/openh264/third_party/openh264/src/codec/build/android/dec/AndroidManifest.xml b/third-party/openh264/third_party/openh264/src/codec/build/android/dec/AndroidManifest.xml new file mode 100644 index 0000000000..84428273f3 --- /dev/null +++ b/third-party/openh264/third_party/openh264/src/codec/build/android/dec/AndroidManifest.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + diff --git a/third-party/openh264/third_party/openh264/src/codec/build/android/dec/jni/Android.mk b/third-party/openh264/third_party/openh264/src/codec/build/android/dec/jni/Android.mk new file mode 100644 index 0000000000..959de7c211 --- /dev/null +++ b/third-party/openh264/third_party/openh264/src/codec/build/android/dec/jni/Android.mk @@ -0,0 +1,9 @@ + +LOCAL_PATH := $(call my-dir) +MY_LOCAL_PATH := $(LOCAL_PATH) + +# Step3 +#Generate the libwelsdecdemo.so file +include $(LOCAL_PATH)/welsdecdemo.mk +LOCAL_PATH := $(MY_LOCAL_PATH) + diff --git a/third-party/openh264/third_party/openh264/src/codec/build/android/dec/jni/Application.mk b/third-party/openh264/third_party/openh264/src/codec/build/android/dec/jni/Application.mk new file mode 100644 index 0000000000..175295f48a --- /dev/null +++ b/third-party/openh264/third_party/openh264/src/codec/build/android/dec/jni/Application.mk @@ -0,0 +1,6 @@ +ifeq ($(NDK_TOOLCHAIN_VERSION), clang) +APP_STL := c++_shared +else +APP_STL := stlport_shared +endif +APP_PLATFORM := android-12 diff --git a/third-party/openh264/third_party/openh264/src/codec/build/android/dec/jni/myjni.cpp b/third-party/openh264/third_party/openh264/src/codec/build/android/dec/jni/myjni.cpp new file mode 100644 index 0000000000..73edb6534c --- /dev/null +++ b/third-party/openh264/third_party/openh264/src/codec/build/android/dec/jni/myjni.cpp @@ -0,0 +1,24 @@ +#include +#include +#include +#include + +#define LOG_TAG "welsdec" +#define LOGI(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__) + +extern int DecMain (int argc, char* argv[]); +extern "C" +JNIEXPORT void JNICALL Java_com_wels_dec_WelsDecTest_DoDecoderTest +(JNIEnv* env, jobject thiz, jstring jsFileNameIn, jstring jsFileNameOut) { + /**************** Add the native codes/API *****************/ + char* argv[3]; + int argc = 3; + argv[0] = (char*) ("decConsole.exe"); + argv[1] = (char*) ((*env).GetStringUTFChars (jsFileNameIn, NULL)); + argv[2] = (char*) ((*env).GetStringUTFChars (jsFileNameOut, NULL)); + LOGI ("Start to run JNI module!+++"); + DecMain (argc, argv); + LOGI ("End to run JNI module!+++"); +} + + diff --git a/third-party/openh264/third_party/openh264/src/codec/build/android/dec/jni/welsdecdemo.mk b/third-party/openh264/third_party/openh264/src/codec/build/android/dec/jni/welsdecdemo.mk new file mode 100644 index 0000000000..09f51e4b44 --- /dev/null +++ b/third-party/openh264/third_party/openh264/src/codec/build/android/dec/jni/welsdecdemo.mk @@ -0,0 +1,47 @@ +# Generate the libwelsdecdemo.so file +LOCAL_PATH := $(call my-dir) + +include $(CLEAR_VARS) +LOCAL_MODULE := wels +LOCAL_SRC_FILES := ../../../../../libopenh264.so +ifneq (,$(wildcard $(LOCAL_PATH)/$(LOCAL_SRC_FILES))) +include $(PREBUILT_SHARED_LIBRARY) +endif + + + +include $(CLEAR_VARS) + +# +# Module Settings +# +LOCAL_MODULE := welsdecdemo + +# +# Source Files +# +CODEC_PATH := ../../../../ +CONSOLE_DEC_PATH := ../../../../console/dec +CONSOLE_COMMON_PATH := ../../../../console/common +LOCAL_SRC_FILES := \ + $(CONSOLE_DEC_PATH)/src/h264dec.cpp \ + $(CONSOLE_COMMON_PATH)/src/read_config.cpp \ + $(CONSOLE_DEC_PATH)/src/d3d9_utils.cpp \ + myjni.cpp +# +# Header Includes +# +LOCAL_C_INCLUDES := \ + $(LOCAL_PATH)/../../../../api/svc \ + $(LOCAL_PATH)/../../../../console/dec/inc \ + $(LOCAL_PATH)/../../../../console/common/inc \ + $(LOCAL_PATH)/../../../../common/inc +# +# Compile Flags and Link Libraries +# +LOCAL_CFLAGS := -DANDROID_NDK + +LOCAL_LDLIBS := -llog +LOCAL_SHARED_LIBRARIES := wels + +include $(BUILD_SHARED_LIBRARY) diff --git a/third-party/openh264/third_party/openh264/src/codec/build/android/dec/res/layout/main.xml b/third-party/openh264/third_party/openh264/src/codec/build/android/dec/res/layout/main.xml new file mode 100644 index 0000000000..76326b16d6 --- /dev/null +++ b/third-party/openh264/third_party/openh264/src/codec/build/android/dec/res/layout/main.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/third-party/openh264/third_party/openh264/src/codec/build/iOS/dec/demo/demo/en.lproj/MainStoryboard_iPhone.storyboard b/third-party/openh264/third_party/openh264/src/codec/build/iOS/dec/demo/demo/en.lproj/MainStoryboard_iPhone.storyboard new file mode 100644 index 0000000000..ffc00b3dd8 --- /dev/null +++ b/third-party/openh264/third_party/openh264/src/codec/build/iOS/dec/demo/demo/en.lproj/MainStoryboard_iPhone.storyboard @@ -0,0 +1,162 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/third-party/openh264/third_party/openh264/src/codec/build/iOS/dec/demo/demo/main.m b/third-party/openh264/third_party/openh264/src/codec/build/iOS/dec/demo/demo/main.m new file mode 100644 index 0000000000..99bec98e6d --- /dev/null +++ b/third-party/openh264/third_party/openh264/src/codec/build/iOS/dec/demo/demo/main.m @@ -0,0 +1,124 @@ +/*! + * \copy + * Copyright (c) 2013, Cisco Systems + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + * COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN + * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ + +#import + +#import "DEMOAppDelegate.h" + +extern int DecMain (int argc, char* argv[]); + +//redirect NSLog and stdout to logfile +void redirectLogToDocumentFile() { + NSArray* path = NSSearchPathForDirectoriesInDomains (NSDocumentDirectory, NSUserDomainMask, YES); + NSString* document = [path objectAtIndex:0]; + NSString* fileName = [NSString stringWithFormat:@"decPerf.log"]; + NSString* logPath = [document stringByAppendingPathComponent:fileName]; + + NSFileManager* defaultManager = [NSFileManager defaultManager]; + [defaultManager removeItemAtPath:logPath error:nil]; + + freopen ([logPath cStringUsingEncoding:NSASCIIStringEncoding], "a+", stdout); + freopen ([logPath cStringUsingEncoding:NSASCIIStringEncoding], "a+", stderr); +} + + +//run auto test to get encoder performance +int AutoTestDec() { + + + NSString* document = [[NSString alloc] init]; + NSArray* paths = NSSearchPathForDirectoriesInDomains (NSDocumentDirectory, NSUserDomainMask, YES); + if ([paths count] == 0) { + NSLog (@"could not find document path"); + return 2; + } + document = [paths objectAtIndex:0]; + + + NSString* decFilePath = [document stringByAppendingString:@"/DecoderPerfTestRes"]; + NSFileManager* manage = [NSFileManager defaultManager]; + + NSString* outYuvPath = [decFilePath stringByAppendingString:@"/yuv"]; + [manage removeItemAtPath:outYuvPath error:nil]; + [manage createDirectoryAtPath:outYuvPath withIntermediateDirectories:YES attributes:nil error: nil]; + + + NSArray* bitstreams = [manage subpathsAtPath:decFilePath]; + if (bitstreams == nil) { + NSLog (@"could not find any bitstream under decoderperfpath"); + return 1; + } + + redirectLogToDocumentFile(); //output to console, just comment this line + + for (int caseNO = 0; caseNO < [bitstreams count]; caseNO++) { + + NSString* caseName = [bitstreams objectAtIndex:caseNO]; + if ([caseName isEqual: @"yuv"]) { + break; + } + NSString* bitstream = [decFilePath stringByAppendingString:@"/"]; + bitstream = [bitstream stringByAppendingString:caseName]; + NSString* yuvFileName = [caseName stringByAppendingString:@".yuv"]; + NSString* tmpyuvFileName = [outYuvPath stringByAppendingString:@"/"]; + yuvFileName = [tmpyuvFileName stringByAppendingString:yuvFileName]; + + [manage createFileAtPath:yuvFileName contents:nil attributes:nil]; + + const char* argvv[] = { + "decConsole.exe", + [bitstream UTF8String], + [yuvFileName UTF8String] + }; + DecMain (sizeof (argvv) / sizeof (argvv[0]), (char**)&argvv[0]); + [manage removeItemAtPath:yuvFileName error:nil];//FOR limited devices spaces + fflush (stdout); // flush the content of stdout instantly + } + + + return 0; +} + +int main (int argc, char* argv[]) { + //***For auto testing of decoder performance, call auto test here, if you not want to do auto test, you can comment it manualy + + if (AutoTestDec() == 0) + NSLog (@"Auto testing running sucessfully"); + else + NSLog (@"Auto testing running failed"); + abort(); + //******** + + @autoreleasepool { + return UIApplicationMain (argc, argv, nil, NSStringFromClass ([DEMOAppDelegate class])); + } +} diff --git a/third-party/openh264/third_party/openh264/src/codec/build/iOS/enc/encDemo/encDemo/AppDelegate.h b/third-party/openh264/third_party/openh264/src/codec/build/iOS/enc/encDemo/encDemo/AppDelegate.h new file mode 100644 index 0000000000..8f3a936a70 --- /dev/null +++ b/third-party/openh264/third_party/openh264/src/codec/build/iOS/enc/encDemo/encDemo/AppDelegate.h @@ -0,0 +1,39 @@ +/*! + * \copy + * Copyright (c) 2013, Cisco Systems + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + * COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN + * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ + +#import + +@interface AppDelegate : UIResponder + + @property (strong, nonatomic) UIWindow* window; + +@end diff --git a/third-party/openh264/third_party/openh264/src/codec/build/iOS/enc/encDemo/encDemo/AppDelegate.m b/third-party/openh264/third_party/openh264/src/codec/build/iOS/enc/encDemo/encDemo/AppDelegate.m new file mode 100644 index 0000000000..fcfab2110e --- /dev/null +++ b/third-party/openh264/third_party/openh264/src/codec/build/iOS/enc/encDemo/encDemo/AppDelegate.m @@ -0,0 +1,63 @@ + +/*! + * \copy + * Copyright (c) 2013, Cisco Systems + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + * COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN + * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ + +#import "AppDelegate.h" + +@implementation AppDelegate + +- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions +{ + return YES; +} + +- (void)applicationWillResignActive:(UIApplication *)application +{ +} + +- (void)applicationDidEnterBackground:(UIApplication *)application +{ +} + +- (void)applicationWillEnterForeground:(UIApplication *)application +{ +} + +- (void)applicationDidBecomeActive:(UIApplication *)application +{ +} + +- (void)applicationWillTerminate:(UIApplication *)application +{ +} + +@end diff --git a/third-party/openh264/third_party/openh264/src/codec/build/iOS/enc/encDemo/encDemo/Base.lproj/Main_iPad.storyboard b/third-party/openh264/third_party/openh264/src/codec/build/iOS/enc/encDemo/encDemo/Base.lproj/Main_iPad.storyboard new file mode 100644 index 0000000000..39c0b14343 --- /dev/null +++ b/third-party/openh264/third_party/openh264/src/codec/build/iOS/enc/encDemo/encDemo/Base.lproj/Main_iPad.storyboard @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/third-party/openh264/third_party/openh264/src/codec/build/iOS/enc/encDemo/encDemo/Base.lproj/Main_iPhone.storyboard b/third-party/openh264/third_party/openh264/src/codec/build/iOS/enc/encDemo/encDemo/Base.lproj/Main_iPhone.storyboard new file mode 100644 index 0000000000..5d614171d7 --- /dev/null +++ b/third-party/openh264/third_party/openh264/src/codec/build/iOS/enc/encDemo/encDemo/Base.lproj/Main_iPhone.storyboard @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/third-party/openh264/third_party/openh264/src/codec/build/iOS/enc/encDemo/encDemo/Images.xcassets/AppIcon.appiconset/Contents.json b/third-party/openh264/third_party/openh264/src/codec/build/iOS/enc/encDemo/encDemo/Images.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000000..91bf9c14a7 --- /dev/null +++ b/third-party/openh264/third_party/openh264/src/codec/build/iOS/enc/encDemo/encDemo/Images.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,53 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "29x29", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "40x40", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "40x40", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "76x76", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "76x76", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/third-party/openh264/third_party/openh264/src/codec/build/iOS/enc/encDemo/encDemo/Images.xcassets/LaunchImage.launchimage/Contents.json b/third-party/openh264/third_party/openh264/src/codec/build/iOS/enc/encDemo/encDemo/Images.xcassets/LaunchImage.launchimage/Contents.json new file mode 100644 index 0000000000..6f870a4629 --- /dev/null +++ b/third-party/openh264/third_party/openh264/src/codec/build/iOS/enc/encDemo/encDemo/Images.xcassets/LaunchImage.launchimage/Contents.json @@ -0,0 +1,51 @@ +{ + "images" : [ + { + "orientation" : "portrait", + "idiom" : "iphone", + "extent" : "full-screen", + "minimum-system-version" : "7.0", + "scale" : "2x" + }, + { + "orientation" : "portrait", + "idiom" : "iphone", + "subtype" : "retina4", + "extent" : "full-screen", + "minimum-system-version" : "7.0", + "scale" : "2x" + }, + { + "orientation" : "portrait", + "idiom" : "ipad", + "extent" : "full-screen", + "minimum-system-version" : "7.0", + "scale" : "1x" + }, + { + "orientation" : "landscape", + "idiom" : "ipad", + "extent" : "full-screen", + "minimum-system-version" : "7.0", + "scale" : "1x" + }, + { + "orientation" : "portrait", + "idiom" : "ipad", + "extent" : "full-screen", + "minimum-system-version" : "7.0", + "scale" : "2x" + }, + { + "orientation" : "landscape", + "idiom" : "ipad", + "extent" : "full-screen", + "minimum-system-version" : "7.0", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/third-party/openh264/third_party/openh264/src/codec/build/iOS/enc/encDemo/encDemo/ViewController.h b/third-party/openh264/third_party/openh264/src/codec/build/iOS/enc/encDemo/encDemo/ViewController.h new file mode 100644 index 0000000000..c4f7b52561 --- /dev/null +++ b/third-party/openh264/third_party/openh264/src/codec/build/iOS/enc/encDemo/encDemo/ViewController.h @@ -0,0 +1,44 @@ +/*! + * \copy + * Copyright (c) 2013, Cisco Systems + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + * COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN + * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ + +#import + +@interface ViewController : UIViewController { + UILabel* statusText_; +} + +@property (retain, nonatomic) IBOutlet UILabel* statusText; + +- (IBAction) buttonPressed: (id)sender; + + +@end diff --git a/third-party/openh264/third_party/openh264/src/codec/build/iOS/enc/encDemo/encDemo/ViewController.m b/third-party/openh264/third_party/openh264/src/codec/build/iOS/enc/encDemo/encDemo/ViewController.m new file mode 100644 index 0000000000..f2331beb17 --- /dev/null +++ b/third-party/openh264/third_party/openh264/src/codec/build/iOS/enc/encDemo/encDemo/ViewController.m @@ -0,0 +1,89 @@ +/*! + * \copy + * Copyright (c) 2013, Cisco Systems + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + * COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN + * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ + +extern int EncMain(int argc, char **argv); + +#import "ViewController.h" + + +@interface ViewController () + +@end + +@implementation ViewController +@synthesize statusText=statusText_; + +- (void)viewDidLoad +{ + [super viewDidLoad]; + statusText_.text = @"Status: Ready for Go"; +} + +- (void)didReceiveMemoryWarning +{ + [super didReceiveMemoryWarning]; +} + +- (IBAction) buttonPressed:(id)sender +{ + NSBundle * bundle = [NSBundle mainBundle]; + NSString * encCfg = [bundle pathForResource:@"welsenc_ios" ofType:@"cfg"]; + NSString * dlayerCfg = [bundle pathForResource:@"layer2" ofType:@"cfg"]; + NSString * yuvFile = [bundle pathForResource:@"CiscoVT2people_320x192_12fps" ofType:@"yuv"]; + NSString * bsfile = [NSString stringWithFormat:@"%@/%@", [self getPathForWrite], @"test.264"]; + NSLog(@"WELS_INFO: enc config file: %@, yuv file %@", encCfg, yuvFile); + const char * argv[] = { + "dummy", + [encCfg UTF8String], + "-org", + [yuvFile UTF8String], + "-bf", + [bsfile UTF8String], + "-numl", + "1", + "-lconfig", + "0", + [dlayerCfg UTF8String], + }; + NSLog(@"WELS_INFO: enc config file: %@", encCfg); + EncMain(sizeof(argv)/sizeof(argv[0]), (char**)&argv[0]); + statusText_.text = @"Status: Test Over"; +} + +- (NSString*) getPathForWrite { + NSArray * pathes = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); + NSString * documentDirectory = [pathes objectAtIndex:0]; + return documentDirectory; +} + + +@end diff --git a/third-party/openh264/third_party/openh264/src/codec/build/iOS/enc/encDemo/encDemo/en.lproj/InfoPlist.strings b/third-party/openh264/third_party/openh264/src/codec/build/iOS/enc/encDemo/encDemo/en.lproj/InfoPlist.strings new file mode 100644 index 0000000000..477b28ff8f --- /dev/null +++ b/third-party/openh264/third_party/openh264/src/codec/build/iOS/enc/encDemo/encDemo/en.lproj/InfoPlist.strings @@ -0,0 +1,2 @@ +/* Localized versions of Info.plist keys */ + diff --git a/third-party/openh264/third_party/openh264/src/codec/build/iOS/enc/encDemo/encDemo/encDemo-Info.plist b/third-party/openh264/third_party/openh264/src/codec/build/iOS/enc/encDemo/encDemo/encDemo-Info.plist new file mode 100644 index 0000000000..3c9aaf9010 --- /dev/null +++ b/third-party/openh264/third_party/openh264/src/codec/build/iOS/enc/encDemo/encDemo/encDemo-Info.plist @@ -0,0 +1,49 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleDisplayName + ${PRODUCT_NAME} + CFBundleExecutable + ${EXECUTABLE_NAME} + CFBundleIdentifier + cisco.${PRODUCT_NAME:rfc1034identifier} + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + ${PRODUCT_NAME} + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + LSRequiresIPhoneOS + + UIMainStoryboardFile + Main_iPhone + UIMainStoryboardFile~ipad + Main_iPad + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/third-party/openh264/third_party/openh264/src/codec/build/iOS/enc/encDemo/encDemo/main.m b/third-party/openh264/third_party/openh264/src/codec/build/iOS/enc/encDemo/encDemo/main.m new file mode 100644 index 0000000000..fd27f11418 --- /dev/null +++ b/third-party/openh264/third_party/openh264/src/codec/build/iOS/enc/encDemo/encDemo/main.m @@ -0,0 +1,175 @@ +/*! + * \copy + * Copyright (c) 2013, Cisco Systems + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + * COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN + * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ + + +#import + +#import "AppDelegate.h" + +extern int EncMain (int argc, char** argv); + +//redirect NSLog and stdout to logfile +void redirectLogToDocumentFile() { + NSArray* path = NSSearchPathForDirectoriesInDomains (NSDocumentDirectory, NSUserDomainMask, YES); + NSString* document = [path objectAtIndex:0]; + NSString* fileName = [NSString stringWithFormat:@"encPerf.log"]; + NSString* logPath = [document stringByAppendingPathComponent:fileName]; + + NSFileManager* defaultManager = [NSFileManager defaultManager]; + [defaultManager removeItemAtPath:logPath error:nil]; + + freopen ([logPath cStringUsingEncoding:NSASCIIStringEncoding], "a+", stdout); + freopen ([logPath cStringUsingEncoding:NSASCIIStringEncoding], "a+", stderr); +} + +//to judge whether the path is needed case path +bool IsOneDeptDir (NSString* path) { + BOOL isDir = NO; + BOOL isOneDeptDir = NO; + NSFileManager* fileManager = [NSFileManager defaultManager]; + NSArray* dirPathArray = [fileManager subpathsAtPath:path]; + if ([dirPathArray count] == 0 || dirPathArray == nil) + isOneDeptDir = NO; + else { + for (NSString * dirPath in dirPathArray) { + NSString* tmpPath = [path stringByAppendingString:@"/"]; + tmpPath = [tmpPath stringByAppendingString:dirPath]; + [fileManager fileExistsAtPath:tmpPath isDirectory:&isDir]; + if (isDir) { + isOneDeptDir = YES; + break; + } + } + } + return isOneDeptDir; +} + +//run auto test to get encoder performance +int AutoTestEnc() { + NSString* document = [[NSString alloc] init]; + NSArray* paths = NSSearchPathForDirectoriesInDomains (NSDocumentDirectory, NSUserDomainMask, YES); + if ([paths count] == 0) { + NSLog (@"could not find document path"); + return 2; + } + document = [paths objectAtIndex:0]; + + NSString* encFilePath = [document stringByAppendingString:@"/EncoderPerfTestRes"]; + NSFileManager* manage = [NSFileManager defaultManager]; + + NSArray* cases = [manage subpathsAtPath:encFilePath]; + if (cases == nil) { + NSLog (@"could not find any test case under encoderperftest"); + return 1; + + } + redirectLogToDocumentFile(); + NSMutableArray* dirArray = [[NSMutableArray alloc] init]; + for (NSString * casePath in cases) { + + NSString* path = [encFilePath stringByAppendingPathComponent:casePath]; + if (IsOneDeptDir (path)) { + [dirArray addObject:casePath]; + } + + } + for (int caseNO = 0; caseNO < [dirArray count]; caseNO++) { + + NSString* caseName = [dirArray objectAtIndex:caseNO]; + NSString* caseFilePath = [encFilePath stringByAppendingString:@"/"]; + caseFilePath = [caseFilePath stringByAppendingString:caseName]; + [manage changeCurrentDirectoryPath:[caseFilePath stringByExpandingTildeInPath]]; + + NSString* welscfg = [caseFilePath stringByAppendingString:@"/welsenc.cfg"]; + NSString* layercfg = [caseFilePath stringByAppendingString:@"/layer2.cfg"]; + NSString* yuvFilePath = [caseFilePath stringByAppendingString:@"/yuv"]; + NSString* bitFilePath = [caseFilePath stringByAppendingString:@"/bit"]; + [manage removeItemAtPath:bitFilePath error:nil]; + [manage createDirectoryAtPath:bitFilePath withIntermediateDirectories:YES attributes:nil error:nil]; + + + NSArray* files = [manage subpathsAtPath:yuvFilePath]; + + [manage changeCurrentDirectoryPath:[bitFilePath stringByExpandingTildeInPath]]; + + for (int i = 0; i < [files count]; i++) { + NSString* yuvFileName = [files objectAtIndex:i]; + NSString* bitFileName = [yuvFileName stringByAppendingString:@".264"]; + + NSString* bitFileNamePath = [bitFilePath stringByAppendingString:@"/"]; + bitFileName = [bitFileNamePath stringByAppendingString:bitFileName]; + + + [manage createFileAtPath:bitFileName contents:nil attributes:nil]; + [manage changeCurrentDirectoryPath:[yuvFilePath stringByExpandingTildeInPath]]; + const char* argvv[] = { + "dummy", + [welscfg UTF8String], + "-org", + [yuvFileName UTF8String], + "-bf", + [bitFileName UTF8String], + "-numl", + "1", + "-lconfig", + "0", + [layercfg UTF8String] + }; + + NSLog (@"WELS_INFO: enc config file: %@", welscfg); + NSLog (@"WELS_INFO: enc yuv file: %@", yuvFileName); + EncMain (sizeof (argvv) / sizeof (argvv[0]), (char**)&argvv[0]); + fflush (stdout); // flush the content of stdout instantly + } + + } + + + return 0; +} + + +int main (int argc, char* argv[]) { + + + //***For auto testing of encoder performance, call auto test here, if you not want to do auto test, you can comment it manualy + + if (AutoTestEnc() == 0) + NSLog (@"Auto testing running sucessfully"); + else + NSLog (@"Auto testing running failed"); + abort(); + //************************ + @autoreleasepool { + return UIApplicationMain (argc, argv, nil, NSStringFromClass ([AppDelegate class])); + } +} diff --git a/third-party/openh264/third_party/openh264/src/codec/build/win32/.gitignore b/third-party/openh264/third_party/openh264/src/codec/build/win32/.gitignore new file mode 100644 index 0000000000..95ce52f658 --- /dev/null +++ b/third-party/openh264/third_party/openh264/src/codec/build/win32/.gitignore @@ -0,0 +1,15 @@ +# Files generated by Visual Studio 2008 +*.user +*.ncb +*.suo + +Win32 +x64 + +# Files generated by upgrading the project files +*.vcxproj* +Backup +UpgradeLog.htm + +# Files generated by Visual Studio 2012 +*.sdf diff --git a/third-party/openh264/third_party/openh264/src/codec/build/win32/dec/WelsDecCore.vcproj b/third-party/openh264/third_party/openh264/src/codec/build/win32/dec/WelsDecCore.vcproj new file mode 100644 index 0000000000..033522f244 --- /dev/null +++ b/third-party/openh264/third_party/openh264/src/codec/build/win32/dec/WelsDecCore.vcproj @@ -0,0 +1,996 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/third-party/openh264/third_party/openh264/src/codec/build/win32/dec/WelsDecPlus.vcproj b/third-party/openh264/third_party/openh264/src/codec/build/win32/dec/WelsDecPlus.vcproj new file mode 100644 index 0000000000..d2006867ef --- /dev/null +++ b/third-party/openh264/third_party/openh264/src/codec/build/win32/dec/WelsDecPlus.vcproj @@ -0,0 +1,395 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/third-party/openh264/third_party/openh264/src/codec/build/win32/dec/WelsDecoder.sln b/third-party/openh264/third_party/openh264/src/codec/build/win32/dec/WelsDecoder.sln new file mode 100644 index 0000000000..d7aad3d67f --- /dev/null +++ b/third-party/openh264/third_party/openh264/src/codec/build/win32/dec/WelsDecoder.sln @@ -0,0 +1,52 @@ + +Microsoft Visual Studio Solution File, Format Version 10.00 +# Visual Studio 2008 +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "WelsDecCore", "WelsDecCore.vcproj", "{01B4AE41-6AD6-4CAF-AEB3-C42F7F9121D5}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "WelsDecPlus", "WelsDecPlus.vcproj", "{1131558A-9986-4F4B-A13F-8B7F4C8438BF}" + ProjectSection(ProjectDependencies) = postProject + {01B4AE41-6AD6-4CAF-AEB3-C42F7F9121D5} = {01B4AE41-6AD6-4CAF-AEB3-C42F7F9121D5} + EndProjectSection +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "decConsole", "decConsole.vcproj", "{71973A8E-103D-4FB7-951F-55E35E7F60FA}" + ProjectSection(ProjectDependencies) = postProject + {1131558A-9986-4F4B-A13F-8B7F4C8438BF} = {1131558A-9986-4F4B-A13F-8B7F4C8438BF} + EndProjectSection +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Win32 = Debug|Win32 + Debug|x64 = Debug|x64 + Release|Win32 = Release|Win32 + Release|x64 = Release|x64 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {01B4AE41-6AD6-4CAF-AEB3-C42F7F9121D5}.Debug|Win32.ActiveCfg = Debug|Win32 + {01B4AE41-6AD6-4CAF-AEB3-C42F7F9121D5}.Debug|Win32.Build.0 = Debug|Win32 + {01B4AE41-6AD6-4CAF-AEB3-C42F7F9121D5}.Debug|x64.ActiveCfg = Debug|x64 + {01B4AE41-6AD6-4CAF-AEB3-C42F7F9121D5}.Debug|x64.Build.0 = Debug|x64 + {01B4AE41-6AD6-4CAF-AEB3-C42F7F9121D5}.Release|Win32.ActiveCfg = Release|Win32 + {01B4AE41-6AD6-4CAF-AEB3-C42F7F9121D5}.Release|Win32.Build.0 = Release|Win32 + {01B4AE41-6AD6-4CAF-AEB3-C42F7F9121D5}.Release|x64.ActiveCfg = Release|x64 + {01B4AE41-6AD6-4CAF-AEB3-C42F7F9121D5}.Release|x64.Build.0 = Release|x64 + {1131558A-9986-4F4B-A13F-8B7F4C8438BF}.Debug|Win32.ActiveCfg = Debug|Win32 + {1131558A-9986-4F4B-A13F-8B7F4C8438BF}.Debug|Win32.Build.0 = Debug|Win32 + {1131558A-9986-4F4B-A13F-8B7F4C8438BF}.Debug|x64.ActiveCfg = Debug|x64 + {1131558A-9986-4F4B-A13F-8B7F4C8438BF}.Debug|x64.Build.0 = Debug|x64 + {1131558A-9986-4F4B-A13F-8B7F4C8438BF}.Release|Win32.ActiveCfg = Release|Win32 + {1131558A-9986-4F4B-A13F-8B7F4C8438BF}.Release|Win32.Build.0 = Release|Win32 + {1131558A-9986-4F4B-A13F-8B7F4C8438BF}.Release|x64.ActiveCfg = Release|x64 + {1131558A-9986-4F4B-A13F-8B7F4C8438BF}.Release|x64.Build.0 = Release|x64 + {71973A8E-103D-4FB7-951F-55E35E7F60FA}.Debug|Win32.ActiveCfg = Debug|Win32 + {71973A8E-103D-4FB7-951F-55E35E7F60FA}.Debug|Win32.Build.0 = Debug|Win32 + {71973A8E-103D-4FB7-951F-55E35E7F60FA}.Debug|x64.ActiveCfg = Debug|x64 + {71973A8E-103D-4FB7-951F-55E35E7F60FA}.Debug|x64.Build.0 = Debug|x64 + {71973A8E-103D-4FB7-951F-55E35E7F60FA}.Release|Win32.ActiveCfg = Release|Win32 + {71973A8E-103D-4FB7-951F-55E35E7F60FA}.Release|Win32.Build.0 = Release|Win32 + {71973A8E-103D-4FB7-951F-55E35E7F60FA}.Release|x64.ActiveCfg = Release|x64 + {71973A8E-103D-4FB7-951F-55E35E7F60FA}.Release|x64.Build.0 = Release|x64 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/third-party/openh264/third_party/openh264/src/codec/build/win32/dec/decConsole.vcproj b/third-party/openh264/third_party/openh264/src/codec/build/win32/dec/decConsole.vcproj new file mode 100644 index 0000000000..2544cd4134 --- /dev/null +++ b/third-party/openh264/third_party/openh264/src/codec/build/win32/dec/decConsole.vcproj @@ -0,0 +1,389 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/third-party/openh264/third_party/openh264/src/codec/build/win32/enc/WelsEncCore.vcproj b/third-party/openh264/third_party/openh264/src/codec/build/win32/enc/WelsEncCore.vcproj new file mode 100644 index 0000000000..b30272d74d --- /dev/null +++ b/third-party/openh264/third_party/openh264/src/codec/build/win32/enc/WelsEncCore.vcproj @@ -0,0 +1,1498 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/third-party/openh264/third_party/openh264/src/codec/build/win32/enc/WelsEncPlus.vcproj b/third-party/openh264/third_party/openh264/src/codec/build/win32/enc/WelsEncPlus.vcproj new file mode 100644 index 0000000000..ab769557bf --- /dev/null +++ b/third-party/openh264/third_party/openh264/src/codec/build/win32/enc/WelsEncPlus.vcproj @@ -0,0 +1,405 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/third-party/openh264/third_party/openh264/src/codec/build/win32/enc/WelsEncoder.sln b/third-party/openh264/third_party/openh264/src/codec/build/win32/enc/WelsEncoder.sln new file mode 100644 index 0000000000..b75cb695ee --- /dev/null +++ b/third-party/openh264/third_party/openh264/src/codec/build/win32/enc/WelsEncoder.sln @@ -0,0 +1,66 @@ + +Microsoft Visual Studio Solution File, Format Version 10.00 +# Visual Studio 2008 +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "WelsEncCore", "WelsEncCore.vcproj", "{59208004-1774-4816-AC24-31FF44C324B4}" + ProjectSection(ProjectDependencies) = postProject + {E8DFAFA1-8DAC-4127-8D27-FBD5819EE562} = {E8DFAFA1-8DAC-4127-8D27-FBD5819EE562} + EndProjectSection +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "WelsEncPlus", "WelsEncPlus.vcproj", "{1E7B4E9A-986E-4167-8C70-6E4F60EAEE7F}" + ProjectSection(ProjectDependencies) = postProject + {59208004-1774-4816-AC24-31FF44C324B4} = {59208004-1774-4816-AC24-31FF44C324B4} + EndProjectSection +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "encConsole", "encConsole.vcproj", "{8509E2A8-2CBD-49E2-B564-3EFF1E927459}" + ProjectSection(ProjectDependencies) = postProject + {1E7B4E9A-986E-4167-8C70-6E4F60EAEE7F} = {1E7B4E9A-986E-4167-8C70-6E4F60EAEE7F} + {E8DFAFA1-8DAC-4127-8D27-FBD5819EE562} = {E8DFAFA1-8DAC-4127-8D27-FBD5819EE562} + EndProjectSection +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "WelsVP", "..\..\..\processing\build\win32\WelsVP.vcproj", "{E8DFAFA1-8DAC-4127-8D27-FBD5819EE562}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Win32 = Debug|Win32 + Debug|x64 = Debug|x64 + Release|Win32 = Release|Win32 + Release|x64 = Release|x64 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {59208004-1774-4816-AC24-31FF44C324B4}.Debug|Win32.ActiveCfg = Debug|Win32 + {59208004-1774-4816-AC24-31FF44C324B4}.Debug|Win32.Build.0 = Debug|Win32 + {59208004-1774-4816-AC24-31FF44C324B4}.Debug|x64.ActiveCfg = Debug|x64 + {59208004-1774-4816-AC24-31FF44C324B4}.Debug|x64.Build.0 = Debug|x64 + {59208004-1774-4816-AC24-31FF44C324B4}.Release|Win32.ActiveCfg = Release|Win32 + {59208004-1774-4816-AC24-31FF44C324B4}.Release|Win32.Build.0 = Release|Win32 + {59208004-1774-4816-AC24-31FF44C324B4}.Release|x64.ActiveCfg = Release|x64 + {59208004-1774-4816-AC24-31FF44C324B4}.Release|x64.Build.0 = Release|x64 + {1E7B4E9A-986E-4167-8C70-6E4F60EAEE7F}.Debug|Win32.ActiveCfg = Debug|Win32 + {1E7B4E9A-986E-4167-8C70-6E4F60EAEE7F}.Debug|Win32.Build.0 = Debug|Win32 + {1E7B4E9A-986E-4167-8C70-6E4F60EAEE7F}.Debug|x64.ActiveCfg = Debug|x64 + {1E7B4E9A-986E-4167-8C70-6E4F60EAEE7F}.Debug|x64.Build.0 = Debug|x64 + {1E7B4E9A-986E-4167-8C70-6E4F60EAEE7F}.Release|Win32.ActiveCfg = Release|Win32 + {1E7B4E9A-986E-4167-8C70-6E4F60EAEE7F}.Release|Win32.Build.0 = Release|Win32 + {1E7B4E9A-986E-4167-8C70-6E4F60EAEE7F}.Release|x64.ActiveCfg = Release|x64 + {1E7B4E9A-986E-4167-8C70-6E4F60EAEE7F}.Release|x64.Build.0 = Release|x64 + {8509E2A8-2CBD-49E2-B564-3EFF1E927459}.Debug|Win32.ActiveCfg = Debug|Win32 + {8509E2A8-2CBD-49E2-B564-3EFF1E927459}.Debug|Win32.Build.0 = Debug|Win32 + {8509E2A8-2CBD-49E2-B564-3EFF1E927459}.Debug|x64.ActiveCfg = Debug|x64 + {8509E2A8-2CBD-49E2-B564-3EFF1E927459}.Debug|x64.Build.0 = Debug|x64 + {8509E2A8-2CBD-49E2-B564-3EFF1E927459}.Release|Win32.ActiveCfg = Release|Win32 + {8509E2A8-2CBD-49E2-B564-3EFF1E927459}.Release|Win32.Build.0 = Release|Win32 + {8509E2A8-2CBD-49E2-B564-3EFF1E927459}.Release|x64.ActiveCfg = Release|x64 + {8509E2A8-2CBD-49E2-B564-3EFF1E927459}.Release|x64.Build.0 = Release|x64 + {E8DFAFA1-8DAC-4127-8D27-FBD5819EE562}.Debug|Win32.ActiveCfg = Debug|Win32 + {E8DFAFA1-8DAC-4127-8D27-FBD5819EE562}.Debug|Win32.Build.0 = Debug|Win32 + {E8DFAFA1-8DAC-4127-8D27-FBD5819EE562}.Debug|x64.ActiveCfg = Debug|x64 + {E8DFAFA1-8DAC-4127-8D27-FBD5819EE562}.Debug|x64.Build.0 = Debug|x64 + {E8DFAFA1-8DAC-4127-8D27-FBD5819EE562}.Release|Win32.ActiveCfg = Release|Win32 + {E8DFAFA1-8DAC-4127-8D27-FBD5819EE562}.Release|Win32.Build.0 = Release|Win32 + {E8DFAFA1-8DAC-4127-8D27-FBD5819EE562}.Release|x64.ActiveCfg = Release|x64 + {E8DFAFA1-8DAC-4127-8D27-FBD5819EE562}.Release|x64.Build.0 = Release|x64 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/third-party/openh264/third_party/openh264/src/codec/build/win32/enc/encConsole.vcproj b/third-party/openh264/third_party/openh264/src/codec/build/win32/enc/encConsole.vcproj new file mode 100644 index 0000000000..4714a24e45 --- /dev/null +++ b/third-party/openh264/third_party/openh264/src/codec/build/win32/enc/encConsole.vcproj @@ -0,0 +1,381 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/third-party/openh264/third_party/openh264/src/codec/build/windowsphone/.gitignore b/third-party/openh264/third_party/openh264/src/codec/build/windowsphone/.gitignore new file mode 100644 index 0000000000..9cb17a9bda --- /dev/null +++ b/third-party/openh264/third_party/openh264/src/codec/build/windowsphone/.gitignore @@ -0,0 +1,29 @@ +# Files generated by Visual Studio 2008 +*.user +*.ncb +*.suo +*.opensdf + +Win32 +x64 +ARM +Debug +Release + +# Files generated by upgrading the project files +Backup +Generated Files +UpgradeLog.htm + +# Files generated by Visual Studio 2013 +*.sdf +ipch +Draft + +# Files used in App +Bin +obj +res +*.dll +*.yuv +*.264 diff --git a/third-party/openh264/third_party/openh264/src/codec/build/windowsphone/all/CodecApp.sln b/third-party/openh264/third_party/openh264/src/codec/build/windowsphone/all/CodecApp.sln new file mode 100644 index 0000000000..3543986e35 --- /dev/null +++ b/third-party/openh264/third_party/openh264/src/codec/build/windowsphone/all/CodecApp.sln @@ -0,0 +1,76 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 2013 +VisualStudioVersion = 12.0.31101.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CodecApp", "CodecApp\CodecApp.csproj", "{5424CF29-908E-417E-93F3-F1CD81E5372C}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "CodecRTComponent", "CodecRTComponent.vcxproj", "{FE5BF241-F4EA-4B94-B36A-23511E5908EC}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|ARM = Debug|ARM + Debug|Mixed Platforms = Debug|Mixed Platforms + Debug|Win32 = Debug|Win32 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|ARM = Release|ARM + Release|Mixed Platforms = Release|Mixed Platforms + Release|Win32 = Release|Win32 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {5424CF29-908E-417E-93F3-F1CD81E5372C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5424CF29-908E-417E-93F3-F1CD81E5372C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5424CF29-908E-417E-93F3-F1CD81E5372C}.Debug|Any CPU.Deploy.0 = Debug|Any CPU + {5424CF29-908E-417E-93F3-F1CD81E5372C}.Debug|ARM.ActiveCfg = Debug|ARM + {5424CF29-908E-417E-93F3-F1CD81E5372C}.Debug|ARM.Build.0 = Debug|ARM + {5424CF29-908E-417E-93F3-F1CD81E5372C}.Debug|ARM.Deploy.0 = Debug|ARM + {5424CF29-908E-417E-93F3-F1CD81E5372C}.Debug|Mixed Platforms.ActiveCfg = Debug|x86 + {5424CF29-908E-417E-93F3-F1CD81E5372C}.Debug|Mixed Platforms.Build.0 = Debug|x86 + {5424CF29-908E-417E-93F3-F1CD81E5372C}.Debug|Mixed Platforms.Deploy.0 = Debug|x86 + {5424CF29-908E-417E-93F3-F1CD81E5372C}.Debug|Win32.ActiveCfg = Debug|x86 + {5424CF29-908E-417E-93F3-F1CD81E5372C}.Debug|Win32.Build.0 = Debug|x86 + {5424CF29-908E-417E-93F3-F1CD81E5372C}.Debug|Win32.Deploy.0 = Debug|x86 + {5424CF29-908E-417E-93F3-F1CD81E5372C}.Debug|x86.ActiveCfg = Debug|x86 + {5424CF29-908E-417E-93F3-F1CD81E5372C}.Debug|x86.Build.0 = Debug|x86 + {5424CF29-908E-417E-93F3-F1CD81E5372C}.Debug|x86.Deploy.0 = Debug|x86 + {5424CF29-908E-417E-93F3-F1CD81E5372C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5424CF29-908E-417E-93F3-F1CD81E5372C}.Release|Any CPU.Build.0 = Release|Any CPU + {5424CF29-908E-417E-93F3-F1CD81E5372C}.Release|Any CPU.Deploy.0 = Release|Any CPU + {5424CF29-908E-417E-93F3-F1CD81E5372C}.Release|ARM.ActiveCfg = Release|ARM + {5424CF29-908E-417E-93F3-F1CD81E5372C}.Release|ARM.Build.0 = Release|ARM + {5424CF29-908E-417E-93F3-F1CD81E5372C}.Release|ARM.Deploy.0 = Release|ARM + {5424CF29-908E-417E-93F3-F1CD81E5372C}.Release|Mixed Platforms.ActiveCfg = Release|x86 + {5424CF29-908E-417E-93F3-F1CD81E5372C}.Release|Mixed Platforms.Build.0 = Release|x86 + {5424CF29-908E-417E-93F3-F1CD81E5372C}.Release|Mixed Platforms.Deploy.0 = Release|x86 + {5424CF29-908E-417E-93F3-F1CD81E5372C}.Release|Win32.ActiveCfg = Release|x86 + {5424CF29-908E-417E-93F3-F1CD81E5372C}.Release|Win32.Build.0 = Release|x86 + {5424CF29-908E-417E-93F3-F1CD81E5372C}.Release|Win32.Deploy.0 = Release|x86 + {5424CF29-908E-417E-93F3-F1CD81E5372C}.Release|x86.ActiveCfg = Release|x86 + {5424CF29-908E-417E-93F3-F1CD81E5372C}.Release|x86.Build.0 = Release|x86 + {5424CF29-908E-417E-93F3-F1CD81E5372C}.Release|x86.Deploy.0 = Release|x86 + {FE5BF241-F4EA-4B94-B36A-23511E5908EC}.Debug|Any CPU.ActiveCfg = Debug|Win32 + {FE5BF241-F4EA-4B94-B36A-23511E5908EC}.Debug|ARM.ActiveCfg = Debug|ARM + {FE5BF241-F4EA-4B94-B36A-23511E5908EC}.Debug|ARM.Build.0 = Debug|ARM + {FE5BF241-F4EA-4B94-B36A-23511E5908EC}.Debug|Mixed Platforms.ActiveCfg = Debug|Win32 + {FE5BF241-F4EA-4B94-B36A-23511E5908EC}.Debug|Mixed Platforms.Build.0 = Debug|Win32 + {FE5BF241-F4EA-4B94-B36A-23511E5908EC}.Debug|Win32.ActiveCfg = Debug|Win32 + {FE5BF241-F4EA-4B94-B36A-23511E5908EC}.Debug|Win32.Build.0 = Debug|Win32 + {FE5BF241-F4EA-4B94-B36A-23511E5908EC}.Debug|x86.ActiveCfg = Debug|Win32 + {FE5BF241-F4EA-4B94-B36A-23511E5908EC}.Debug|x86.Build.0 = Debug|Win32 + {FE5BF241-F4EA-4B94-B36A-23511E5908EC}.Release|Any CPU.ActiveCfg = Release|Win32 + {FE5BF241-F4EA-4B94-B36A-23511E5908EC}.Release|ARM.ActiveCfg = Release|ARM + {FE5BF241-F4EA-4B94-B36A-23511E5908EC}.Release|ARM.Build.0 = Release|ARM + {FE5BF241-F4EA-4B94-B36A-23511E5908EC}.Release|Mixed Platforms.ActiveCfg = Release|Win32 + {FE5BF241-F4EA-4B94-B36A-23511E5908EC}.Release|Mixed Platforms.Build.0 = Release|Win32 + {FE5BF241-F4EA-4B94-B36A-23511E5908EC}.Release|Win32.ActiveCfg = Release|Win32 + {FE5BF241-F4EA-4B94-B36A-23511E5908EC}.Release|Win32.Build.0 = Release|Win32 + {FE5BF241-F4EA-4B94-B36A-23511E5908EC}.Release|x86.ActiveCfg = Release|Win32 + {FE5BF241-F4EA-4B94-B36A-23511E5908EC}.Release|x86.Build.0 = Release|Win32 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/third-party/openh264/third_party/openh264/src/codec/build/windowsphone/all/CodecApp/App.xaml b/third-party/openh264/third_party/openh264/src/codec/build/windowsphone/all/CodecApp/App.xaml new file mode 100644 index 0000000000..d1508a95bb --- /dev/null +++ b/third-party/openh264/third_party/openh264/src/codec/build/windowsphone/all/CodecApp/App.xaml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/third-party/openh264/third_party/openh264/src/codec/build/windowsphone/all/CodecApp/App.xaml.cs b/third-party/openh264/third_party/openh264/src/codec/build/windowsphone/all/CodecApp/App.xaml.cs new file mode 100644 index 0000000000..97c1852c9b --- /dev/null +++ b/third-party/openh264/third_party/openh264/src/codec/build/windowsphone/all/CodecApp/App.xaml.cs @@ -0,0 +1,223 @@ +using System; +using System.Diagnostics; +using System.Resources; +using System.Windows; +using System.Windows.Markup; +using System.Windows.Navigation; +using Microsoft.Phone.Controls; +using Microsoft.Phone.Shell; +using CodecApp.Resources; + +namespace CodecApp +{ + public partial class App : Application + { + /// + /// Provides easy access to the root frame of the Phone Application. + /// + /// The root frame of the Phone Application. + public static PhoneApplicationFrame RootFrame { get; private set; } + + /// + /// Constructor for the Application object. + /// + public App() + { + // Global handler for uncaught exceptions. + UnhandledException += Application_UnhandledException; + + // Standard XAML initialization + InitializeComponent(); + + // Phone-specific initialization + InitializePhoneApplication(); + + // Language display initialization + InitializeLanguage(); + + // Show graphics profiling information while debugging. + if (Debugger.IsAttached) + { + // Display the current frame rate counters. + Application.Current.Host.Settings.EnableFrameRateCounter = true; + + // Show the areas of the app that are being redrawn in each frame. + //Application.Current.Host.Settings.EnableRedrawRegions = true; + + // Enable non-production analysis visualization mode, + // which shows areas of a page that are handed off to GPU with a colored overlay. + //Application.Current.Host.Settings.EnableCacheVisualization = true; + + // Prevent the screen from turning off while under the debugger by disabling + // the application's idle detection. + // Caution:- Use this under debug mode only. Application that disables user idle detection will continue to run + // and consume battery power when the user is not using the phone. + PhoneApplicationService.Current.UserIdleDetectionMode = IdleDetectionMode.Disabled; + } + + } + + // Code to execute when the application is launching (eg, from Start) + // This code will not execute when the application is reactivated + private void Application_Launching(object sender, LaunchingEventArgs e) + { + } + + // Code to execute when the application is activated (brought to foreground) + // This code will not execute when the application is first launched + private void Application_Activated(object sender, ActivatedEventArgs e) + { + } + + // Code to execute when the application is deactivated (sent to background) + // This code will not execute when the application is closing + private void Application_Deactivated(object sender, DeactivatedEventArgs e) + { + } + + // Code to execute when the application is closing (eg, user hit Back) + // This code will not execute when the application is deactivated + private void Application_Closing(object sender, ClosingEventArgs e) + { + } + + // Code to execute if a navigation fails + private void RootFrame_NavigationFailed(object sender, NavigationFailedEventArgs e) + { + if (Debugger.IsAttached) + { + // A navigation has failed; break into the debugger + Debugger.Break(); + } + } + + // Code to execute on Unhandled Exceptions + private void Application_UnhandledException(object sender, ApplicationUnhandledExceptionEventArgs e) + { + if (Debugger.IsAttached) + { + // An unhandled exception has occurred; break into the debugger + Debugger.Break(); + } + } + + #region Phone application initialization + + // Avoid double-initialization + private bool phoneApplicationInitialized = false; + + // Do not add any additional code to this method + private void InitializePhoneApplication() + { + if (phoneApplicationInitialized) + return; + + // Create the frame but don't set it as RootVisual yet; this allows the splash + // screen to remain active until the application is ready to render. + RootFrame = new PhoneApplicationFrame(); + RootFrame.Navigated += CompleteInitializePhoneApplication; + + // Handle navigation failures + RootFrame.NavigationFailed += RootFrame_NavigationFailed; + + // Handle reset requests for clearing the backstack + RootFrame.Navigated += CheckForResetNavigation; + + // Ensure we don't initialize again + phoneApplicationInitialized = true; + } + + // Do not add any additional code to this method + private void CompleteInitializePhoneApplication(object sender, NavigationEventArgs e) + { + // Set the root visual to allow the application to render + if (RootVisual != RootFrame) + RootVisual = RootFrame; + + // Remove this handler since it is no longer needed + RootFrame.Navigated -= CompleteInitializePhoneApplication; + } + + private void CheckForResetNavigation(object sender, NavigationEventArgs e) + { + // If the app has received a 'reset' navigation, then we need to check + // on the next navigation to see if the page stack should be reset + if (e.NavigationMode == NavigationMode.Reset) + RootFrame.Navigated += ClearBackStackAfterReset; + } + + private void ClearBackStackAfterReset(object sender, NavigationEventArgs e) + { + // Unregister the event so it doesn't get called again + RootFrame.Navigated -= ClearBackStackAfterReset; + + // Only clear the stack for 'new' (forward) and 'refresh' navigations + if (e.NavigationMode != NavigationMode.New && e.NavigationMode != NavigationMode.Refresh) + return; + + // For UI consistency, clear the entire page stack + while (RootFrame.RemoveBackEntry() != null) + { + ; // do nothing + } + } + + #endregion + + // Initialize the app's font and flow direction as defined in its localized resource strings. + // + // To ensure that the font of your application is aligned with its supported languages and that the + // FlowDirection for each of those languages follows its traditional direction, ResourceLanguage + // and ResourceFlowDirection should be initialized in each resx file to match these values with that + // file's culture. For example: + // + // AppResources.es-ES.resx + // ResourceLanguage's value should be "es-ES" + // ResourceFlowDirection's value should be "LeftToRight" + // + // AppResources.ar-SA.resx + // ResourceLanguage's value should be "ar-SA" + // ResourceFlowDirection's value should be "RightToLeft" + // + // For more info on localizing Windows Phone apps see http://go.microsoft.com/fwlink/?LinkId=262072. + // + private void InitializeLanguage() + { + try + { + // Set the font to match the display language defined by the + // ResourceLanguage resource string for each supported language. + // + // Fall back to the font of the neutral language if the Display + // language of the phone is not supported. + // + // If a compiler error is hit then ResourceLanguage is missing from + // the resource file. + RootFrame.Language = XmlLanguage.GetLanguage(AppResources.ResourceLanguage); + + // Set the FlowDirection of all elements under the root frame based + // on the ResourceFlowDirection resource string for each + // supported language. + // + // If a compiler error is hit then ResourceFlowDirection is missing from + // the resource file. + FlowDirection flow = (FlowDirection)Enum.Parse(typeof(FlowDirection), AppResources.ResourceFlowDirection); + RootFrame.FlowDirection = flow; + } + catch + { + // If an exception is caught here it is most likely due to either + // ResourceLangauge not being correctly set to a supported language + // code or ResourceFlowDirection is set to a value other than LeftToRight + // or RightToLeft. + + if (Debugger.IsAttached) + { + Debugger.Break(); + } + + throw; + } + } + } +} \ No newline at end of file diff --git a/third-party/openh264/third_party/openh264/src/codec/build/windowsphone/all/CodecApp/Assets/AlignmentGrid.png b/third-party/openh264/third_party/openh264/src/codec/build/windowsphone/all/CodecApp/Assets/AlignmentGrid.png new file mode 100644 index 0000000000..f7d2e97804 Binary files /dev/null and b/third-party/openh264/third_party/openh264/src/codec/build/windowsphone/all/CodecApp/Assets/AlignmentGrid.png differ diff --git a/third-party/openh264/third_party/openh264/src/codec/build/windowsphone/all/CodecApp/Assets/ApplicationIcon.png b/third-party/openh264/third_party/openh264/src/codec/build/windowsphone/all/CodecApp/Assets/ApplicationIcon.png new file mode 100644 index 0000000000..7d95d4e081 Binary files /dev/null and b/third-party/openh264/third_party/openh264/src/codec/build/windowsphone/all/CodecApp/Assets/ApplicationIcon.png differ diff --git a/third-party/openh264/third_party/openh264/src/codec/build/windowsphone/all/CodecApp/Assets/BadgeLogo.png b/third-party/openh264/third_party/openh264/src/codec/build/windowsphone/all/CodecApp/Assets/BadgeLogo.png new file mode 100644 index 0000000000..eb5bb835e2 Binary files /dev/null and b/third-party/openh264/third_party/openh264/src/codec/build/windowsphone/all/CodecApp/Assets/BadgeLogo.png differ diff --git a/third-party/openh264/third_party/openh264/src/codec/build/windowsphone/all/CodecApp/Assets/Logo.png b/third-party/openh264/third_party/openh264/src/codec/build/windowsphone/all/CodecApp/Assets/Logo.png new file mode 100644 index 0000000000..f2e1f40a07 Binary files /dev/null and b/third-party/openh264/third_party/openh264/src/codec/build/windowsphone/all/CodecApp/Assets/Logo.png differ diff --git a/third-party/openh264/third_party/openh264/src/codec/build/windowsphone/all/CodecApp/Assets/SplashScreen.png b/third-party/openh264/third_party/openh264/src/codec/build/windowsphone/all/CodecApp/Assets/SplashScreen.png new file mode 100644 index 0000000000..4b9072882a Binary files /dev/null and b/third-party/openh264/third_party/openh264/src/codec/build/windowsphone/all/CodecApp/Assets/SplashScreen.png differ diff --git a/third-party/openh264/third_party/openh264/src/codec/build/windowsphone/all/CodecApp/Assets/SquareTile150x150.png b/third-party/openh264/third_party/openh264/src/codec/build/windowsphone/all/CodecApp/Assets/SquareTile150x150.png new file mode 100644 index 0000000000..ec1d121551 Binary files /dev/null and b/third-party/openh264/third_party/openh264/src/codec/build/windowsphone/all/CodecApp/Assets/SquareTile150x150.png differ diff --git a/third-party/openh264/third_party/openh264/src/codec/build/windowsphone/all/CodecApp/Assets/SquareTile71x71.png b/third-party/openh264/third_party/openh264/src/codec/build/windowsphone/all/CodecApp/Assets/SquareTile71x71.png new file mode 100644 index 0000000000..4754af3a7c Binary files /dev/null and b/third-party/openh264/third_party/openh264/src/codec/build/windowsphone/all/CodecApp/Assets/SquareTile71x71.png differ diff --git a/third-party/openh264/third_party/openh264/src/codec/build/windowsphone/all/CodecApp/Assets/StoreLogo.png b/third-party/openh264/third_party/openh264/src/codec/build/windowsphone/all/CodecApp/Assets/StoreLogo.png new file mode 100644 index 0000000000..12c5f4f655 Binary files /dev/null and b/third-party/openh264/third_party/openh264/src/codec/build/windowsphone/all/CodecApp/Assets/StoreLogo.png differ diff --git a/third-party/openh264/third_party/openh264/src/codec/build/windowsphone/all/CodecApp/Assets/Tiles/FlipCycleTileLarge.png b/third-party/openh264/third_party/openh264/src/codec/build/windowsphone/all/CodecApp/Assets/Tiles/FlipCycleTileLarge.png new file mode 100644 index 0000000000..e0c59ac014 Binary files /dev/null and b/third-party/openh264/third_party/openh264/src/codec/build/windowsphone/all/CodecApp/Assets/Tiles/FlipCycleTileLarge.png differ diff --git a/third-party/openh264/third_party/openh264/src/codec/build/windowsphone/all/CodecApp/Assets/Tiles/FlipCycleTileMedium.png b/third-party/openh264/third_party/openh264/src/codec/build/windowsphone/all/CodecApp/Assets/Tiles/FlipCycleTileMedium.png new file mode 100644 index 0000000000..e93b89d600 Binary files /dev/null and b/third-party/openh264/third_party/openh264/src/codec/build/windowsphone/all/CodecApp/Assets/Tiles/FlipCycleTileMedium.png differ diff --git a/third-party/openh264/third_party/openh264/src/codec/build/windowsphone/all/CodecApp/Assets/Tiles/FlipCycleTileSmall.png b/third-party/openh264/third_party/openh264/src/codec/build/windowsphone/all/CodecApp/Assets/Tiles/FlipCycleTileSmall.png new file mode 100644 index 0000000000..550b1b5e8d Binary files /dev/null and b/third-party/openh264/third_party/openh264/src/codec/build/windowsphone/all/CodecApp/Assets/Tiles/FlipCycleTileSmall.png differ diff --git a/third-party/openh264/third_party/openh264/src/codec/build/windowsphone/all/CodecApp/Assets/Tiles/IconicTileMediumLarge.png b/third-party/openh264/third_party/openh264/src/codec/build/windowsphone/all/CodecApp/Assets/Tiles/IconicTileMediumLarge.png new file mode 100644 index 0000000000..686e6b53f0 Binary files /dev/null and b/third-party/openh264/third_party/openh264/src/codec/build/windowsphone/all/CodecApp/Assets/Tiles/IconicTileMediumLarge.png differ diff --git a/third-party/openh264/third_party/openh264/src/codec/build/windowsphone/all/CodecApp/Assets/Tiles/IconicTileSmall.png b/third-party/openh264/third_party/openh264/src/codec/build/windowsphone/all/CodecApp/Assets/Tiles/IconicTileSmall.png new file mode 100644 index 0000000000..d4b5ede1b5 Binary files /dev/null and b/third-party/openh264/third_party/openh264/src/codec/build/windowsphone/all/CodecApp/Assets/Tiles/IconicTileSmall.png differ diff --git a/third-party/openh264/third_party/openh264/src/codec/build/windowsphone/all/CodecApp/Assets/WideLogo.png b/third-party/openh264/third_party/openh264/src/codec/build/windowsphone/all/CodecApp/Assets/WideLogo.png new file mode 100644 index 0000000000..082197b62b Binary files /dev/null and b/third-party/openh264/third_party/openh264/src/codec/build/windowsphone/all/CodecApp/Assets/WideLogo.png differ diff --git a/third-party/openh264/third_party/openh264/src/codec/build/windowsphone/all/CodecApp/CodecApp.csproj b/third-party/openh264/third_party/openh264/src/codec/build/windowsphone/all/CodecApp/CodecApp.csproj new file mode 100644 index 0000000000..6b219d8217 --- /dev/null +++ b/third-party/openh264/third_party/openh264/src/codec/build/windowsphone/all/CodecApp/CodecApp.csproj @@ -0,0 +1,210 @@ + + + + Debug + AnyCPU + 10.0.20506 + 2.0 + {5424CF29-908E-417E-93F3-F1CD81E5372C} + {C089C8C0-30E0-4E22-80C0-CE093F111A43};{fae04ec0-301f-11d3-bf4b-00c04f79efbc} + Library + Properties + CodecApp + CodecApp + WindowsPhone + v8.1 + + + true + + + true + true + CodecApp_$(Configuration)_$(Platform).xap + Properties\AppManifest.xml + CodecApp.App + true + 12.0 + true + + en-US + + + true + full + false + Bin\Debug + DEBUG;TRACE;SILVERLIGHT;WINDOWS_PHONE + true + true + prompt + 4 + + + pdbonly + true + Bin\Release + TRACE;SILVERLIGHT;WINDOWS_PHONE + true + true + prompt + 4 + + + true + full + false + Bin\x86\Debug + DEBUG;TRACE;SILVERLIGHT;WINDOWS_PHONE + true + true + prompt + 4 + + + + pdbonly + true + Bin\x86\Release + TRACE;SILVERLIGHT;WINDOWS_PHONE + true + true + prompt + 4 + + + + true + full + false + Bin\ARM\Debug + DEBUG;TRACE;SILVERLIGHT;WINDOWS_PHONE + true + true + prompt + 4 + + + + pdbonly + true + Bin\ARM\Release + TRACE;SILVERLIGHT;WINDOWS_PHONE + true + true + prompt + 4 + + + + + App.xaml + + + + MainPage.xaml + + + + True + True + AppResources.resx + + + + + Designer + MSBuild:Compile + + + Designer + MSBuild:Compile + + + + + + + + + + + + Always + + + Always + + + Always + + + Always + + + Always + + + Designer + + + + Designer + + + Always + + + Always + + + + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + + + PublicResXFileCodeGenerator + AppResources.Designer.cs + + + + + {fe5bf241-f4ea-4b94-b36a-23511e5908ec} + CodecRTComponent + + + + + + + + copy /y ..\..\..\..\..\..\..\..\bin\$(Platform)\$(Configuration)\openh264.dll ..\..\..\ +copy /y ..\..\..\..\..\..\..\..\res\*.yuv ..\..\..\ +copy /y ..\..\..\..\..\..\..\..\res\B*.264 ..\..\..\ + + + \ No newline at end of file diff --git a/third-party/openh264/third_party/openh264/src/codec/build/windowsphone/all/CodecApp/LocalizedStrings.cs b/third-party/openh264/third_party/openh264/src/codec/build/windowsphone/all/CodecApp/LocalizedStrings.cs new file mode 100644 index 0000000000..e18e5b6786 --- /dev/null +++ b/third-party/openh264/third_party/openh264/src/codec/build/windowsphone/all/CodecApp/LocalizedStrings.cs @@ -0,0 +1,14 @@ +using CodecApp.Resources; + +namespace CodecApp +{ + /// + /// Provides access to string resources. + /// + public class LocalizedStrings + { + private static AppResources _localizedResources = new AppResources(); + + public AppResources LocalizedResources { get { return _localizedResources; } } + } +} \ No newline at end of file diff --git a/third-party/openh264/third_party/openh264/src/codec/build/windowsphone/all/CodecApp/MainPage.xaml b/third-party/openh264/third_party/openh264/src/codec/build/windowsphone/all/CodecApp/MainPage.xaml new file mode 100644 index 0000000000..46191fa81b --- /dev/null +++ b/third-party/openh264/third_party/openh264/src/codec/build/windowsphone/all/CodecApp/MainPage.xaml @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + +