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