Add back changes

This commit is contained in:
Isaac 2024-03-15 16:13:38 +04:00
parent 4e9b22ffce
commit 2a2d468fd9
27 changed files with 1108 additions and 118 deletions

View File

@ -231,17 +231,52 @@ public enum ChatRecordedMediaPreview: Equatable {
case video(Video)
}
public final class ChatManagingBot: Equatable {
public let bot: EnginePeer
public let isPaused: Bool
public let canReply: Bool
public let settingsUrl: String?
public init(bot: EnginePeer, isPaused: Bool, canReply: Bool, settingsUrl: String?) {
self.bot = bot
self.isPaused = isPaused
self.canReply = canReply
self.settingsUrl = settingsUrl
}
public static func ==(lhs: ChatManagingBot, rhs: ChatManagingBot) -> Bool {
if lhs === rhs {
return true
}
if lhs.bot != rhs.bot {
return false
}
if lhs.isPaused != rhs.isPaused {
return false
}
if lhs.canReply != rhs.canReply {
return false
}
if lhs.settingsUrl != rhs.settingsUrl {
return false
}
return true
}
}
public struct ChatContactStatus: Equatable {
public var canAddContact: Bool
public var canReportIrrelevantLocation: Bool
public var peerStatusSettings: PeerStatusSettings?
public var invitedBy: Peer?
public var managingBot: ChatManagingBot?
public init(canAddContact: Bool, canReportIrrelevantLocation: Bool, peerStatusSettings: PeerStatusSettings?, invitedBy: Peer?) {
public init(canAddContact: Bool, canReportIrrelevantLocation: Bool, peerStatusSettings: PeerStatusSettings?, invitedBy: Peer?, managingBot: ChatManagingBot?) {
self.canAddContact = canAddContact
self.canReportIrrelevantLocation = canReportIrrelevantLocation
self.peerStatusSettings = peerStatusSettings
self.invitedBy = invitedBy
self.managingBot = managingBot
}
public var isEmpty: Bool {
@ -270,6 +305,9 @@ public struct ChatContactStatus: Equatable {
if !arePeersEqual(lhs.invitedBy, rhs.invitedBy) {
return false
}
if lhs.managingBot != rhs.managingBot {
return false
}
return true
}
}

View File

@ -21,6 +21,23 @@ public struct LocalAuth {
case error(Error)
}
#if targetEnvironment(simulator)
public final class PrivateKey {
public let publicKeyRepresentation: Data
fileprivate init() {
self.publicKeyRepresentation = Data(count: 32)
}
public func encrypt(data: Data) -> Data? {
return data
}
public func decrypt(data: Data) -> DecryptionResult {
return .result(data)
}
}
#else
public final class PrivateKey {
private let privateKey: SecKey
private let publicKey: SecKey
@ -64,6 +81,7 @@ public struct LocalAuth {
return .result(result)
}
}
#endif
public static var biometricAuthentication: LocalAuthBiometricAuthentication? {
let context = LAContext()
@ -157,7 +175,18 @@ public struct LocalAuth {
return seedId;
}
public static func getPrivateKey(baseAppBundleId: String, keyId: Data) -> PrivateKey? {
public static func getOrCreatePrivateKey(baseAppBundleId: String, keyId: Data) -> PrivateKey? {
if let key = self.getPrivateKey(baseAppBundleId: baseAppBundleId, keyId: keyId) {
return key
} else {
return self.addPrivateKey(baseAppBundleId: baseAppBundleId, keyId: keyId)
}
}
private static func getPrivateKey(baseAppBundleId: String, keyId: Data) -> PrivateKey? {
#if targetEnvironment(simulator)
return PrivateKey()
#else
guard let bundleSeedId = self.bundleSeedId() else {
return nil
}
@ -196,6 +225,7 @@ public struct LocalAuth {
let result = PrivateKey(privateKey: privateKey, publicKey: publicKey, publicKeyRepresentation: publicKeyRepresentation as Data)
return result
#endif
}
public static func removePrivateKey(baseAppBundleId: String, keyId: Data) -> Bool {
@ -221,7 +251,10 @@ public struct LocalAuth {
return true
}
public static func addPrivateKey(baseAppBundleId: String, keyId: Data) -> PrivateKey? {
private static func addPrivateKey(baseAppBundleId: String, keyId: Data) -> PrivateKey? {
#if targetEnvironment(simulator)
return PrivateKey()
#else
guard let bundleSeedId = self.bundleSeedId() else {
return nil
}
@ -262,5 +295,6 @@ public struct LocalAuth {
let result = PrivateKey(privateKey: privateKey, publicKey: publicKey, publicKeyRepresentation: publicKeyRepresentation as Data)
return result
#endif
}
}

View File

@ -8,6 +8,7 @@
@property (nonatomic) NSUInteger internalServerErrorCount;
@property (nonatomic) NSUInteger floodWaitSeconds;
@property (nonatomic, strong) NSString *floodWaitErrorText;
@property (nonatomic) bool waitingForTokenExport;
@property (nonatomic, strong) id waitingForRequestToComplete;

View File

@ -808,7 +808,7 @@
}
restartRequest = true;
}
else if (rpcError.errorCode == 420 || [rpcError.errorDescription rangeOfString:@"FLOOD_WAIT_"].location != NSNotFound) {
else if (rpcError.errorCode == 420 || [rpcError.errorDescription rangeOfString:@"FLOOD_WAIT_"].location != NSNotFound || [rpcError.errorDescription rangeOfString:@"FLOOD_PREMIUM_WAIT_"].location != NSNotFound) {
if (request.errorContext == nil)
request.errorContext = [[MTRequestErrorContext alloc] init];
@ -821,6 +821,32 @@
if ([scanner scanInt:&errorWaitTime])
{
request.errorContext.floodWaitSeconds = errorWaitTime;
request.errorContext.floodWaitErrorText = rpcError.errorDescription;
if (request.shouldContinueExecutionWithErrorContext != nil)
{
if (request.shouldContinueExecutionWithErrorContext(request.errorContext))
{
restartRequest = true;
request.errorContext.minimalExecuteTime = MAX(request.errorContext.minimalExecuteTime, MTAbsoluteSystemTime() + (CFAbsoluteTime)errorWaitTime);
}
}
else
{
restartRequest = true;
request.errorContext.minimalExecuteTime = MAX(request.errorContext.minimalExecuteTime, MTAbsoluteSystemTime() + (CFAbsoluteTime)errorWaitTime);
}
}
} else if ([rpcError.errorDescription rangeOfString:@"FLOOD_PREMIUM_WAIT_"].location != NSNotFound) {
int errorWaitTime = 0;
NSScanner *scanner = [[NSScanner alloc] initWithString:rpcError.errorDescription];
[scanner scanUpToString:@"FLOOD_PREMIUM_WAIT_" intoString:nil];
[scanner scanString:@"FLOOD_PREMIUM_WAIT_" intoString:nil];
if ([scanner scanInt:&errorWaitTime])
{
request.errorContext.floodWaitSeconds = errorWaitTime;
request.errorContext.floodWaitErrorText = rpcError.errorDescription;
if (request.shouldContinueExecutionWithErrorContext != nil)
{

View File

@ -19,8 +19,9 @@ public func printOpenFiles() {
var flags: Int32 = 0
var fd: Int32 = 0
var buf = Data(count: Int(MAXPATHLEN) + 1)
let maxFd = min(1024, FD_SETSIZE)
while fd < FD_SETSIZE {
while fd < maxFd {
errno = 0;
flags = fcntl(fd, F_GETFD, 0);
if flags == -1 && errno != 0 {

View File

@ -144,13 +144,13 @@ public class UnauthorizedAccount {
return accountManager.transaction { transaction -> (LocalizationSettings?, ProxySettings?) in
return (transaction.getSharedData(SharedDataKeys.localizationSettings)?.get(LocalizationSettings.self), transaction.getSharedData(SharedDataKeys.proxySettings)?.get(ProxySettings.self))
}
|> mapToSignal { localizationSettings, proxySettings -> Signal<(LocalizationSettings?, ProxySettings?, NetworkSettings?), NoError> in
return self.postbox.transaction { transaction -> (LocalizationSettings?, ProxySettings?, NetworkSettings?) in
return (localizationSettings, proxySettings, transaction.getPreferencesEntry(key: PreferencesKeys.networkSettings)?.get(NetworkSettings.self))
|> mapToSignal { localizationSettings, proxySettings -> Signal<(LocalizationSettings?, ProxySettings?, NetworkSettings?, AppConfiguration), NoError> in
return self.postbox.transaction { transaction -> (LocalizationSettings?, ProxySettings?, NetworkSettings?, AppConfiguration) in
return (localizationSettings, proxySettings, transaction.getPreferencesEntry(key: PreferencesKeys.networkSettings)?.get(NetworkSettings.self), transaction.getPreferencesEntry(key: PreferencesKeys.appConfiguration)?.get(AppConfiguration.self) ?? .defaultValue)
}
}
|> mapToSignal { (localizationSettings, proxySettings, networkSettings) -> Signal<UnauthorizedAccount, NoError> in
return initializedNetwork(accountId: self.id, arguments: self.networkArguments, supplementary: false, datacenterId: Int(masterDatacenterId), keychain: keychain, basePath: self.basePath, testingEnvironment: self.testingEnvironment, languageCode: localizationSettings?.primaryComponent.languageCode, proxySettings: proxySettings, networkSettings: networkSettings, phoneNumber: nil, useRequestTimeoutTimers: false)
|> mapToSignal { localizationSettings, proxySettings, networkSettings, appConfiguration -> Signal<UnauthorizedAccount, NoError> in
return initializedNetwork(accountId: self.id, arguments: self.networkArguments, supplementary: false, datacenterId: Int(masterDatacenterId), keychain: keychain, basePath: self.basePath, testingEnvironment: self.testingEnvironment, languageCode: localizationSettings?.primaryComponent.languageCode, proxySettings: proxySettings, networkSettings: networkSettings, phoneNumber: nil, useRequestTimeoutTimers: false, appConfiguration: appConfiguration)
|> map { network in
let updated = UnauthorizedAccount(networkArguments: self.networkArguments, id: self.id, rootPath: self.rootPath, basePath: self.basePath, testingEnvironment: self.testingEnvironment, postbox: self.postbox, network: network)
updated.shouldBeServiceTaskMaster.set(self.shouldBeServiceTaskMaster.get())
@ -248,7 +248,7 @@ public func accountWithId(accountManager: AccountManager<TelegramAccountManagerT
if let accountState = accountState {
switch accountState {
case let unauthorizedState as UnauthorizedAccountState:
return initializedNetwork(accountId: id, arguments: networkArguments, supplementary: supplementary, datacenterId: Int(unauthorizedState.masterDatacenterId), keychain: keychain, basePath: path, testingEnvironment: unauthorizedState.isTestingEnvironment, languageCode: localizationSettings?.primaryComponent.languageCode, proxySettings: proxySettings, networkSettings: networkSettings, phoneNumber: nil, useRequestTimeoutTimers: useRequestTimeoutTimers)
return initializedNetwork(accountId: id, arguments: networkArguments, supplementary: supplementary, datacenterId: Int(unauthorizedState.masterDatacenterId), keychain: keychain, basePath: path, testingEnvironment: unauthorizedState.isTestingEnvironment, languageCode: localizationSettings?.primaryComponent.languageCode, proxySettings: proxySettings, networkSettings: networkSettings, phoneNumber: nil, useRequestTimeoutTimers: useRequestTimeoutTimers, appConfiguration: appConfig)
|> map { network -> AccountResult in
return .unauthorized(UnauthorizedAccount(networkArguments: networkArguments, id: id, rootPath: rootPath, basePath: path, testingEnvironment: unauthorizedState.isTestingEnvironment, postbox: postbox, network: network, shouldKeepAutoConnection: shouldKeepAutoConnection))
}
@ -257,7 +257,7 @@ public func accountWithId(accountManager: AccountManager<TelegramAccountManagerT
return (transaction.getPeer(authorizedState.peerId) as? TelegramUser)?.phone
}
|> mapToSignal { phoneNumber in
return initializedNetwork(accountId: id, arguments: networkArguments, supplementary: supplementary, datacenterId: Int(authorizedState.masterDatacenterId), keychain: keychain, basePath: path, testingEnvironment: authorizedState.isTestingEnvironment, languageCode: localizationSettings?.primaryComponent.languageCode, proxySettings: proxySettings, networkSettings: networkSettings, phoneNumber: phoneNumber, useRequestTimeoutTimers: useRequestTimeoutTimers)
return initializedNetwork(accountId: id, arguments: networkArguments, supplementary: supplementary, datacenterId: Int(authorizedState.masterDatacenterId), keychain: keychain, basePath: path, testingEnvironment: authorizedState.isTestingEnvironment, languageCode: localizationSettings?.primaryComponent.languageCode, proxySettings: proxySettings, networkSettings: networkSettings, phoneNumber: phoneNumber, useRequestTimeoutTimers: useRequestTimeoutTimers, appConfiguration: appConfig)
|> map { network -> AccountResult in
return .authorized(Account(accountManager: accountManager, id: id, basePath: path, testingEnvironment: authorizedState.isTestingEnvironment, postbox: postbox, network: network, networkArguments: networkArguments, peerId: authorizedState.peerId, auxiliaryMethods: auxiliaryMethods, supplementary: supplementary))
}
@ -267,7 +267,7 @@ public func accountWithId(accountManager: AccountManager<TelegramAccountManagerT
}
}
return initializedNetwork(accountId: id, arguments: networkArguments, supplementary: supplementary, datacenterId: 2, keychain: keychain, basePath: path, testingEnvironment: beginWithTestingEnvironment, languageCode: localizationSettings?.primaryComponent.languageCode, proxySettings: proxySettings, networkSettings: networkSettings, phoneNumber: nil, useRequestTimeoutTimers: useRequestTimeoutTimers)
return initializedNetwork(accountId: id, arguments: networkArguments, supplementary: supplementary, datacenterId: 2, keychain: keychain, basePath: path, testingEnvironment: beginWithTestingEnvironment, languageCode: localizationSettings?.primaryComponent.languageCode, proxySettings: proxySettings, networkSettings: networkSettings, phoneNumber: nil, useRequestTimeoutTimers: useRequestTimeoutTimers, appConfiguration: appConfig)
|> map { network -> AccountResult in
return .unauthorized(UnauthorizedAccount(networkArguments: networkArguments, id: id, rootPath: rootPath, basePath: path, testingEnvironment: beginWithTestingEnvironment, postbox: postbox, network: network, shouldKeepAutoConnection: shouldKeepAutoConnection))
}
@ -889,6 +889,11 @@ public func accountBackupData(postbox: Postbox) -> Signal<AccountBackupData?, No
}
}
public enum NetworkSpeedLimitedEvent {
case upload
case download
}
public class Account {
static let sharedQueue = Queue(name: "Account-Shared")
@ -1519,7 +1524,8 @@ public func standaloneStateManager(
proxySettings: proxySettings,
networkSettings: networkSettings,
phoneNumber: phoneNumber,
useRequestTimeoutTimers: false
useRequestTimeoutTimers: false,
appConfiguration: .defaultValue
)
|> map { network -> AccountStateManager? in
Logger.shared.log("StandaloneStateManager", "received network")

View File

@ -103,7 +103,7 @@ class Download: NSObject, MTRequestMessageServiceDelegate {
self.context.authTokenForDatacenter(withIdRequired: self.datacenterId, authToken:self.mtProto.requiredAuthToken, masterDatacenterId: self.mtProto.authTokenMasterDatacenterId)
}
static func uploadPart(multiplexedManager: MultiplexedRequestManager, datacenterId: Int, consumerId: Int64, tag: MediaResourceFetchTag?, fileId: Int64, index: Int, data: Data, asBigPart: Bool, bigTotalParts: Int? = nil, useCompression: Bool = false) -> Signal<Void, UploadPartError> {
static func uploadPart(multiplexedManager: MultiplexedRequestManager, datacenterId: Int, consumerId: Int64, tag: MediaResourceFetchTag?, fileId: Int64, index: Int, data: Data, asBigPart: Bool, bigTotalParts: Int? = nil, useCompression: Bool = false, onFloodWaitError: ((String) -> Void)? = nil) -> Signal<Void, UploadPartError> {
let saveFilePart: (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Bool>)
if asBigPart {
let totalParts: Int32
@ -117,7 +117,7 @@ class Download: NSObject, MTRequestMessageServiceDelegate {
saveFilePart = Api.functions.upload.saveFilePart(fileId: fileId, filePart: Int32(index), bytes: Buffer(data: data))
}
return multiplexedManager.request(to: .main(datacenterId), consumerId: consumerId, resourceId: nil, data: wrapMethodBody(saveFilePart, useCompression: useCompression), tag: tag, continueInBackground: true, expectedResponseSize: nil)
return multiplexedManager.request(to: .main(datacenterId), consumerId: consumerId, resourceId: nil, data: wrapMethodBody(saveFilePart, useCompression: useCompression), tag: tag, continueInBackground: true, onFloodWaitError: onFloodWaitError, expectedResponseSize: nil)
|> mapError { error -> UploadPartError in
if error.errorCode == 400 {
return .invalidMedia
@ -130,7 +130,7 @@ class Download: NSObject, MTRequestMessageServiceDelegate {
}
}
func uploadPart(fileId: Int64, index: Int, data: Data, asBigPart: Bool, bigTotalParts: Int? = nil, useCompression: Bool = false) -> Signal<Void, UploadPartError> {
func uploadPart(fileId: Int64, index: Int, data: Data, asBigPart: Bool, bigTotalParts: Int? = nil, useCompression: Bool = false, onFloodWaitError: ((String) -> Void)? = nil) -> Signal<Void, UploadPartError> {
return Signal<Void, MTRpcError> { subscriber in
let request = MTRequest()
@ -159,6 +159,13 @@ class Download: NSObject, MTRequestMessageServiceDelegate {
request.dependsOnPasswordEntry = false
request.shouldContinueExecutionWithErrorContext = { errorContext in
guard let errorContext = errorContext else {
return true
}
if let onFloodWaitError, errorContext.floodWaitSeconds > 0, let errorText = errorContext.floodWaitErrorText {
onFloodWaitError(errorText)
}
return true
}
@ -295,7 +302,7 @@ class Download: NSObject, MTRequestMessageServiceDelegate {
|> retryRequest
}
func request<T>(_ data: (FunctionDescription, Buffer, DeserializeFunctionResponse<T>), expectedResponseSize: Int32? = nil, automaticFloodWait: Bool = true) -> Signal<T, MTRpcError> {
func request<T>(_ data: (FunctionDescription, Buffer, DeserializeFunctionResponse<T>), expectedResponseSize: Int32? = nil, automaticFloodWait: Bool = true, onFloodWaitError: ((String) -> Void)? = nil) -> Signal<T, MTRpcError> {
return Signal { subscriber in
let request = MTRequest()
request.expectedResponseSize = expectedResponseSize ?? 0
@ -314,6 +321,9 @@ class Download: NSObject, MTRequestMessageServiceDelegate {
guard let errorContext = errorContext else {
return true
}
if let onFloodWaitError, errorContext.floodWaitSeconds > 0, let errorText = errorContext.floodWaitErrorText {
onFloodWaitError(errorText)
}
if errorContext.floodWaitSeconds > 0 && !automaticFloodWait {
return false
}
@ -344,7 +354,7 @@ class Download: NSObject, MTRequestMessageServiceDelegate {
}
}
func requestWithAdditionalData<T>(_ data: (FunctionDescription, Buffer, DeserializeFunctionResponse<T>), automaticFloodWait: Bool = true, failOnServerErrors: Bool = false, expectedResponseSize: Int32? = nil) -> Signal<(T, Double), (MTRpcError, Double)> {
func requestWithAdditionalData<T>(_ data: (FunctionDescription, Buffer, DeserializeFunctionResponse<T>), automaticFloodWait: Bool = true, onFloodWaitError: ((String) -> Void)? = nil, failOnServerErrors: Bool = false, expectedResponseSize: Int32? = nil) -> Signal<(T, Double), (MTRpcError, Double)> {
return Signal { subscriber in
let request = MTRequest()
request.expectedResponseSize = expectedResponseSize ?? 0
@ -363,6 +373,9 @@ class Download: NSObject, MTRequestMessageServiceDelegate {
guard let errorContext = errorContext else {
return true
}
if let onFloodWaitError, errorContext.floodWaitSeconds > 0, let errorText = errorContext.floodWaitErrorText {
onFloodWaitError(errorText)
}
if errorContext.floodWaitSeconds > 0 && !automaticFloodWait {
return false
}
@ -396,7 +409,7 @@ class Download: NSObject, MTRequestMessageServiceDelegate {
}
}
func rawRequest(_ data: (FunctionDescription, Buffer, (Buffer) -> Any?), automaticFloodWait: Bool = true, failOnServerErrors: Bool = false, logPrefix: String = "", expectedResponseSize: Int32? = nil) -> Signal<(Any, NetworkResponseInfo), (MTRpcError, Double)> {
func rawRequest(_ data: (FunctionDescription, Buffer, (Buffer) -> Any?), automaticFloodWait: Bool = true, onFloodWaitError: ((String) -> Void)? = nil, failOnServerErrors: Bool = false, logPrefix: String = "", expectedResponseSize: Int32? = nil) -> Signal<(Any, NetworkResponseInfo), (MTRpcError, Double)> {
let requestService = self.requestService
return Signal { subscriber in
let request = MTRequest()
@ -416,6 +429,9 @@ class Download: NSObject, MTRequestMessageServiceDelegate {
guard let errorContext = errorContext else {
return true
}
if let onFloodWaitError, errorContext.floodWaitSeconds > 0, let errorText = errorContext.floodWaitErrorText {
onFloodWaitError(errorText)
}
if errorContext.floodWaitSeconds > 0 && !automaticFloodWait {
return false
}

View File

@ -104,14 +104,14 @@ private struct DownloadWrapper {
self.useMainConnection = useMainConnection
}
func request<T>(_ data: (FunctionDescription, Buffer, DeserializeFunctionResponse<T>), tag: MediaResourceFetchTag?, continueInBackground: Bool, expectedResponseSize: Int32?) -> Signal<(T, NetworkResponseInfo), MTRpcError> {
func request<T>(_ data: (FunctionDescription, Buffer, DeserializeFunctionResponse<T>), tag: MediaResourceFetchTag?, continueInBackground: Bool, expectedResponseSize: Int32?, onFloodWaitError: @escaping (String) -> Void) -> Signal<(T, NetworkResponseInfo), MTRpcError> {
let target: MultiplexedRequestTarget
if self.isCdn {
target = .cdn(Int(self.datacenterId))
} else {
target = .main(Int(self.datacenterId))
}
return network.multiplexedRequestManager.requestWithAdditionalInfo(to: target, consumerId: self.consumerId, resourceId: self.resourceId, data: data, tag: tag, continueInBackground: continueInBackground, expectedResponseSize: expectedResponseSize)
return network.multiplexedRequestManager.requestWithAdditionalInfo(to: target, consumerId: self.consumerId, resourceId: self.resourceId, data: data, tag: tag, continueInBackground: continueInBackground, onFloodWaitError: onFloodWaitError, expectedResponseSize: expectedResponseSize)
|> mapError { error, _ -> MTRpcError in
return error
}
@ -192,7 +192,7 @@ private final class MultipartCdnHashSource {
clusterContext = ClusterContext(disposable: disposable)
self.clusterContexts[offset] = clusterContext
disposable.set((self.masterDownload.request(Api.functions.upload.getCdnFileHashes(fileToken: Buffer(data: self.fileToken), offset: offset), tag: nil, continueInBackground: self.continueInBackground, expectedResponseSize: nil)
disposable.set((self.masterDownload.request(Api.functions.upload.getCdnFileHashes(fileToken: Buffer(data: self.fileToken), offset: offset), tag: nil, continueInBackground: self.continueInBackground, expectedResponseSize: nil, onFloodWaitError: { _ in })
|> map { partHashes, _ -> [Int64: Data] in
var parsedPartHashes: [Int64: Data] = [:]
for part in partHashes {
@ -322,7 +322,7 @@ private enum MultipartFetchSource {
}
}
func request(offset: Int64, limit: Int64, tag: MediaResourceFetchTag?, resource: TelegramMediaResource, resourceReference: FetchResourceReference, fileReference: Data?, continueInBackground: Bool) -> Signal<(Data, NetworkResponseInfo), MultipartFetchDownloadError> {
func request(offset: Int64, limit: Int64, tag: MediaResourceFetchTag?, resource: TelegramMediaResource, resourceReference: FetchResourceReference, fileReference: Data?, continueInBackground: Bool, onFloodWaitError: @escaping (String) -> Void) -> Signal<(Data, NetworkResponseInfo), MultipartFetchDownloadError> {
var resourceReferenceValue: MediaResourceReference?
switch resourceReference {
case .forceRevalidate:
@ -348,7 +348,9 @@ private enum MultipartFetchSource {
case .revalidate:
return .fail(.revalidateMediaReference)
case let .location(parsedLocation):
return download.request(Api.functions.upload.getFile(flags: 0, location: parsedLocation, offset: offset, limit: Int32(limit)), tag: tag, continueInBackground: continueInBackground, expectedResponseSize: Int32(limit))
return download.request(Api.functions.upload.getFile(flags: 0, location: parsedLocation, offset: offset, limit: Int32(limit)), tag: tag, continueInBackground: continueInBackground, expectedResponseSize: Int32(limit), onFloodWaitError: { error in
onFloodWaitError(error)
})
|> mapError { error -> MultipartFetchDownloadError in
if error.errorDescription.hasPrefix("FILEREF_INVALID") || error.errorDescription.hasPrefix("FILE_REFERENCE_") {
return .revalidateMediaReference
@ -380,7 +382,9 @@ private enum MultipartFetchSource {
}
}
case let .web(_, location):
return download.request(Api.functions.upload.getWebFile(location: location, offset: Int32(offset), limit: Int32(limit)), tag: tag, continueInBackground: continueInBackground, expectedResponseSize: Int32(limit))
return download.request(Api.functions.upload.getWebFile(location: location, offset: Int32(offset), limit: Int32(limit)), tag: tag, continueInBackground: continueInBackground, expectedResponseSize: Int32(limit), onFloodWaitError: { error in
onFloodWaitError(error)
})
|> mapError { error -> MultipartFetchDownloadError in
if error.errorDescription == "WEBFILE_NOT_AVAILABLE" {
return .webfileNotAvailable
@ -404,7 +408,9 @@ private enum MultipartFetchSource {
updatedLength += 1
}
let part = download.request(Api.functions.upload.getCdnFile(fileToken: Buffer(data: fileToken), offset: offset, limit: Int32(updatedLength)), tag: nil, continueInBackground: continueInBackground, expectedResponseSize: Int32(updatedLength))
let part = download.request(Api.functions.upload.getCdnFile(fileToken: Buffer(data: fileToken), offset: offset, limit: Int32(updatedLength)), tag: nil, continueInBackground: continueInBackground, expectedResponseSize: Int32(updatedLength), onFloodWaitError: { error in
onFloodWaitError(error)
})
|> mapError { _ -> MultipartFetchDownloadError in
return .generic
}
@ -723,6 +729,13 @@ private final class MultipartFetchManager {
}
}
private func processFloodWaitError(error: String) {
if error.hasPrefix("FLOOD_PREMIUM_WAIT") {
self.network.addNetworkSpeedLimitedEvent(event: .download)
}
}
func checkState() {
guard let currentIntervals = self.currentIntervals else {
return
@ -836,7 +849,15 @@ private final class MultipartFetchManager {
}
let partSize: Int32 = Int32(downloadRange.upperBound - downloadRange.lowerBound)
let part = self.source.request(offset: downloadRange.lowerBound, limit: downloadRange.upperBound - downloadRange.lowerBound, tag: self.parameters?.tag, resource: self.resource, resourceReference: self.resourceReference, fileReference: self.fileReference, continueInBackground: self.continueInBackground)
let queue = self.queue
let part = self.source.request(offset: downloadRange.lowerBound, limit: downloadRange.upperBound - downloadRange.lowerBound, tag: self.parameters?.tag, resource: self.resource, resourceReference: self.resourceReference, fileReference: self.fileReference, continueInBackground: self.continueInBackground, onFloodWaitError: { [weak self] error in
queue.async {
guard let self else {
return
}
self.processFloodWaitError(error: error)
}
})
|> deliverOn(self.queue)
let partDisposable = MetaDisposable()
self.fetchingParts[downloadRange.lowerBound] = FetchingPart(size: Int64(downloadRange.count), disposable: partDisposable)
@ -919,7 +940,7 @@ private final class MultipartFetchManager {
case let .cdn(_, _, fileToken, _, _, _, masterDownload, _):
if !strongSelf.reuploadingToCdn {
strongSelf.reuploadingToCdn = true
let reupload: Signal<[Api.FileHash], NoError> = masterDownload.request(Api.functions.upload.reuploadCdnFile(fileToken: Buffer(data: fileToken), requestToken: Buffer(data: token)), tag: nil, continueInBackground: strongSelf.continueInBackground, expectedResponseSize: nil)
let reupload: Signal<[Api.FileHash], NoError> = masterDownload.request(Api.functions.upload.reuploadCdnFile(fileToken: Buffer(data: fileToken), requestToken: Buffer(data: token)), tag: nil, continueInBackground: strongSelf.continueInBackground, expectedResponseSize: nil, onFloodWaitError: { _ in })
|> map { result, _ -> [Api.FileHash] in
return result
}

View File

@ -470,12 +470,21 @@ func multipartUpload(network: Network, postbox: Postbox, source: MultipartUpload
fetchedResource = .complete()
}
let onFloodWaitError: (String) -> Void = { [weak network] error in
guard let network else {
return
}
if error.hasPrefix("FLOOD_PREMIUM_WAIT") {
network.addNetworkSpeedLimitedEvent(event: .upload)
}
}
let manager = MultipartUploadManager(headerSize: headerSize, data: dataSignal, encryptionKey: encryptionKey, hintFileSize: hintFileSize, hintFileIsLarge: hintFileIsLarge, forceNoBigParts: forceNoBigParts, useLargerParts: useLargerParts, increaseParallelParts: increaseParallelParts, uploadPart: { part in
switch uploadInterface {
case let .download(download):
return download.uploadPart(fileId: part.fileId, index: part.index, data: part.data, asBigPart: part.bigPart, bigTotalParts: part.bigTotalParts, useCompression: useCompression)
return download.uploadPart(fileId: part.fileId, index: part.index, data: part.data, asBigPart: part.bigPart, bigTotalParts: part.bigTotalParts, useCompression: useCompression, onFloodWaitError: onFloodWaitError)
case let .multiplexed(multiplexed, datacenterId, consumerId):
return Download.uploadPart(multiplexedManager: multiplexed, datacenterId: datacenterId, consumerId: consumerId, tag: nil, fileId: part.fileId, index: part.index, data: part.data, asBigPart: part.bigPart, bigTotalParts: part.bigTotalParts, useCompression: useCompression)
return Download.uploadPart(multiplexedManager: multiplexed, datacenterId: datacenterId, consumerId: consumerId, tag: nil, fileId: part.fileId, index: part.index, data: part.data, asBigPart: part.bigPart, bigTotalParts: part.bigTotalParts, useCompression: useCompression, onFloodWaitError: onFloodWaitError)
}
}, progress: { progress in
subscriber.putNext(.progress(progress))

View File

@ -33,12 +33,13 @@ private final class RequestData {
let tag: MediaResourceFetchTag?
let continueInBackground: Bool
let automaticFloodWait: Bool
let onFloodWaitError: ((String) -> Void)?
let expectedResponseSize: Int32?
let deserializeResponse: (Buffer) -> Any?
let completed: (Any, NetworkResponseInfo) -> Void
let error: (MTRpcError, Double) -> Void
init(id: Int32, consumerId: Int64, resourceId: String?, target: MultiplexedRequestTarget, functionDescription: FunctionDescription, payload: Buffer, tag: MediaResourceFetchTag?, continueInBackground: Bool, automaticFloodWait: Bool, expectedResponseSize: Int32?, deserializeResponse: @escaping (Buffer) -> Any?, completed: @escaping (Any, NetworkResponseInfo) -> Void, error: @escaping (MTRpcError, Double) -> Void) {
init(id: Int32, consumerId: Int64, resourceId: String?, target: MultiplexedRequestTarget, functionDescription: FunctionDescription, payload: Buffer, tag: MediaResourceFetchTag?, continueInBackground: Bool, automaticFloodWait: Bool, onFloodWaitError: ((String) -> Void)?, expectedResponseSize: Int32?, deserializeResponse: @escaping (Buffer) -> Any?, completed: @escaping (Any, NetworkResponseInfo) -> Void, error: @escaping (MTRpcError, Double) -> Void) {
self.id = id
self.consumerId = consumerId
self.resourceId = resourceId
@ -47,6 +48,7 @@ private final class RequestData {
self.tag = tag
self.continueInBackground = continueInBackground
self.automaticFloodWait = automaticFloodWait
self.onFloodWaitError = onFloodWaitError
self.expectedResponseSize = expectedResponseSize
self.payload = payload
self.deserializeResponse = deserializeResponse
@ -155,12 +157,12 @@ private final class MultiplexedRequestManagerContext {
}
}
func request(to target: MultiplexedRequestTarget, consumerId: Int64, resourceId: String?, data: (FunctionDescription, Buffer, (Buffer) -> Any?), tag: MediaResourceFetchTag?, continueInBackground: Bool, automaticFloodWait: Bool, expectedResponseSize: Int32?, completed: @escaping (Any, NetworkResponseInfo) -> Void, error: @escaping (MTRpcError, Double) -> Void) -> Disposable {
func request(to target: MultiplexedRequestTarget, consumerId: Int64, resourceId: String?, data: (FunctionDescription, Buffer, (Buffer) -> Any?), tag: MediaResourceFetchTag?, continueInBackground: Bool, automaticFloodWait: Bool, onFloodWaitError: ((String) -> Void)? = nil, expectedResponseSize: Int32?, completed: @escaping (Any, NetworkResponseInfo) -> Void, error: @escaping (MTRpcError, Double) -> Void) -> Disposable {
let targetKey = MultiplexedRequestTargetKey(target: target, continueInBackground: continueInBackground)
let requestId = self.nextId
self.nextId += 1
self.queuedRequests.append(RequestData(id: requestId, consumerId: consumerId, resourceId: resourceId, target: target, functionDescription: data.0, payload: data.1, tag: tag, continueInBackground: continueInBackground, automaticFloodWait: automaticFloodWait, expectedResponseSize: expectedResponseSize, deserializeResponse: { buffer in
self.queuedRequests.append(RequestData(id: requestId, consumerId: consumerId, resourceId: resourceId, target: target, functionDescription: data.0, payload: data.1, tag: tag, continueInBackground: continueInBackground, automaticFloodWait: automaticFloodWait, onFloodWaitError: onFloodWaitError, expectedResponseSize: expectedResponseSize, deserializeResponse: { buffer in
return data.2(buffer)
}, completed: { result, info in
completed(result, info)
@ -254,7 +256,7 @@ private final class MultiplexedRequestManagerContext {
let requestId = request.id
selectedContext.requests.append(ExecutingRequestData(requestId: requestId, disposable: disposable))
let queue = self.queue
disposable.set(selectedContext.worker.rawRequest((request.functionDescription, request.payload, request.deserializeResponse), automaticFloodWait: request.automaticFloodWait, expectedResponseSize: request.expectedResponseSize).start(next: { [weak self, weak selectedContext] result, info in
disposable.set(selectedContext.worker.rawRequest((request.functionDescription, request.payload, request.deserializeResponse), automaticFloodWait: request.automaticFloodWait, onFloodWaitError: request.onFloodWaitError, expectedResponseSize: request.expectedResponseSize).start(next: { [weak self, weak selectedContext] result, info in
queue.async {
guard let strongSelf = self else {
return
@ -354,13 +356,13 @@ final class MultiplexedRequestManager {
return disposable
}
func request<T>(to target: MultiplexedRequestTarget, consumerId: Int64, resourceId: String?, data: (FunctionDescription, Buffer, DeserializeFunctionResponse<T>), tag: MediaResourceFetchTag?, continueInBackground: Bool, automaticFloodWait: Bool = true, expectedResponseSize: Int32?) -> Signal<T, MTRpcError> {
func request<T>(to target: MultiplexedRequestTarget, consumerId: Int64, resourceId: String?, data: (FunctionDescription, Buffer, DeserializeFunctionResponse<T>), tag: MediaResourceFetchTag?, continueInBackground: Bool, automaticFloodWait: Bool = true, onFloodWaitError: ((String) -> Void)? = nil, expectedResponseSize: Int32?) -> Signal<T, MTRpcError> {
return Signal { subscriber in
let disposable = MetaDisposable()
self.context.with { context in
disposable.set(context.request(to: target, consumerId: consumerId, resourceId: resourceId, data: (data.0, data.1, { buffer in
return data.2.parse(buffer)
}), tag: tag, continueInBackground: continueInBackground, automaticFloodWait: automaticFloodWait, expectedResponseSize: expectedResponseSize, completed: { result, _ in
}), tag: tag, continueInBackground: continueInBackground, automaticFloodWait: automaticFloodWait, onFloodWaitError: onFloodWaitError, expectedResponseSize: expectedResponseSize, completed: { result, _ in
if let result = result as? T {
subscriber.putNext(result)
subscriber.putCompletion()
@ -375,13 +377,13 @@ final class MultiplexedRequestManager {
}
}
func requestWithAdditionalInfo<T>(to target: MultiplexedRequestTarget, consumerId: Int64, resourceId: String?, data: (FunctionDescription, Buffer, DeserializeFunctionResponse<T>), tag: MediaResourceFetchTag?, continueInBackground: Bool, automaticFloodWait: Bool = true, expectedResponseSize: Int32?) -> Signal<(T, NetworkResponseInfo), (MTRpcError, Double)> {
func requestWithAdditionalInfo<T>(to target: MultiplexedRequestTarget, consumerId: Int64, resourceId: String?, data: (FunctionDescription, Buffer, DeserializeFunctionResponse<T>), tag: MediaResourceFetchTag?, continueInBackground: Bool, automaticFloodWait: Bool = true, onFloodWaitError: ((String) -> Void)? = nil, expectedResponseSize: Int32?) -> Signal<(T, NetworkResponseInfo), (MTRpcError, Double)> {
return Signal { subscriber in
let disposable = MetaDisposable()
self.context.with { context in
disposable.set(context.request(to: target, consumerId: consumerId, resourceId: resourceId, data: (data.0, data.1, { buffer in
return data.2.parse(buffer)
}), tag: tag, continueInBackground: continueInBackground, automaticFloodWait: automaticFloodWait, expectedResponseSize: expectedResponseSize, completed: { result, info in
}), tag: tag, continueInBackground: continueInBackground, automaticFloodWait: automaticFloodWait, onFloodWaitError: onFloodWaitError, expectedResponseSize: expectedResponseSize, completed: { result, info in
if let result = result as? T {
subscriber.putNext((result, info))
subscriber.putCompletion()

View File

@ -459,7 +459,7 @@ public struct NetworkInitializationArguments {
private let cloudDataContext = Atomic<CloudDataContext?>(value: nil)
#endif
func initializedNetwork(accountId: AccountRecordId, arguments: NetworkInitializationArguments, supplementary: Bool, datacenterId: Int, keychain: Keychain, basePath: String, testingEnvironment: Bool, languageCode: String?, proxySettings: ProxySettings?, networkSettings: NetworkSettings?, phoneNumber: String?, useRequestTimeoutTimers: Bool) -> Signal<Network, NoError> {
func initializedNetwork(accountId: AccountRecordId, arguments: NetworkInitializationArguments, supplementary: Bool, datacenterId: Int, keychain: Keychain, basePath: String, testingEnvironment: Bool, languageCode: String?, proxySettings: ProxySettings?, networkSettings: NetworkSettings?, phoneNumber: String?, useRequestTimeoutTimers: Bool, appConfiguration: AppConfiguration) -> Signal<Network, NoError> {
return Signal { subscriber in
let queue = Queue()
queue.async {
@ -612,6 +612,11 @@ func initializedNetwork(accountId: AccountRecordId, arguments: NetworkInitializa
let useExperimentalFeatures = networkSettings?.useExperimentalDownload ?? false
let network = Network(queue: queue, datacenterId: datacenterId, context: context, mtProto: mtProto, requestService: requestService, connectionStatusDelegate: connectionStatusDelegate, _connectionStatus: connectionStatus, basePath: basePath, appDataDisposable: appDataDisposable, encryptionProvider: arguments.encryptionProvider, useRequestTimeoutTimers: useRequestTimeoutTimers, useBetaFeatures: arguments.useBetaFeatures, useExperimentalFeatures: useExperimentalFeatures)
if let data = appConfiguration.data, let notifyInterval = data["upload_premium_speedup_notify_period"] as? Double {
network.updateNetworkSpeedLimitedEventNotifyInterval(value: notifyInterval)
}
appDataUpdatedImpl = { [weak network] data in
guard let data = data else {
return
@ -734,6 +739,22 @@ public enum NetworkRequestResult<T> {
case progress(Float, Int32)
}
private final class NetworkSpeedLimitedEventState {
var notifyInterval: Double = 60.0 * 60.0
var lastNotifyTimestamp: Double = 0.0
func add(event: NetworkSpeedLimitedEvent) -> Bool {
let timestamp = CFAbsoluteTimeGetCurrent()
if self.lastNotifyTimestamp + self.notifyInterval < timestamp {
self.lastNotifyTimestamp = timestamp
return true
} else {
return false
}
}
}
public final class Network: NSObject, MTRequestMessageServiceDelegate {
public let encryptionProvider: EncryptionProvider
@ -766,6 +787,12 @@ public final class Network: NSObject, MTRequestMessageServiceDelegate {
return self._connectionStatus.get() |> distinctUntilChanged
}
public var networkSpeedLimitedEvents: Signal<NetworkSpeedLimitedEvent, NoError> {
return self.networkSpeedLimitedEventPipe.signal()
}
private let networkSpeedLimitedEventPipe = ValuePipe<NetworkSpeedLimitedEvent>()
private let networkSpeedLimitedEventState = Atomic<NetworkSpeedLimitedEventState>(value: NetworkSpeedLimitedEventState())
public func dropConnectionStatus() {
_connectionStatus.set(.single(.waitingForNetwork))
}
@ -826,18 +853,18 @@ public final class Network: NSObject, MTRequestMessageServiceDelegate {
let array = NSMutableArray()
if let result = result {
switch result {
case let .cdnConfig(publicKeys):
for key in publicKeys {
switch key {
case let .cdnPublicKey(dcId, publicKey):
if id == Int(dcId) {
let dict = NSMutableDictionary()
dict["key"] = publicKey
dict["fingerprint"] = MTRsaFingerprint(encryptionProvider, publicKey)
array.add(dict)
}
case let .cdnConfig(publicKeys):
for key in publicKeys {
switch key {
case let .cdnPublicKey(dcId, publicKey):
if id == Int(dcId) {
let dict = NSMutableDictionary()
dict["key"] = publicKey
dict["fingerprint"] = MTRsaFingerprint(encryptionProvider, publicKey)
array.add(dict)
}
}
}
}
}
return array
@ -867,12 +894,12 @@ public final class Network: NSObject, MTRequestMessageServiceDelegate {
let isCdn: Bool
let isMedia: Bool = true
switch target {
case let .main(id):
datacenterId = id
isCdn = false
case let .cdn(id):
datacenterId = id
isCdn = true
case let .main(id):
datacenterId = id
isCdn = false
case let .cdn(id):
datacenterId = id
isCdn = true
}
return strongSelf.makeWorker(datacenterId: datacenterId, isCdn: isCdn, isMedia: isMedia, tag: tag, continueInBackground: continueInBackground)
}
@ -880,7 +907,7 @@ public final class Network: NSObject, MTRequestMessageServiceDelegate {
})
let shouldKeepConnectionSignal = self.shouldKeepConnection.get()
|> distinctUntilChanged |> deliverOn(queue)
|> distinctUntilChanged |> deliverOn(queue)
self.shouldKeepConnectionDisposable.set(shouldKeepConnectionSignal.start(next: { [weak self] value in
if let strongSelf = self {
if value {
@ -967,11 +994,11 @@ public final class Network: NSObject, MTRequestMessageServiceDelegate {
self.context.addAddressForDatacenter(withId: Int(datacenterId), address: address)
/*let currentScheme = self.context.transportSchemeForDatacenter(withId: Int(datacenterId), media: false, isProxy: false)
if let currentScheme = currentScheme, currentScheme.address.isEqual(to: address) {
} else {
let scheme = MTTransportScheme(transport: MTTcpTransport.self, address: address, media: false)
self.context.updateTransportSchemeForDatacenter(withId: Int(datacenterId), transportScheme: scheme, media: false, isProxy: false)
}*/
if let currentScheme = currentScheme, currentScheme.address.isEqual(to: address) {
} else {
let scheme = MTTransportScheme(transport: MTTcpTransport.self, address: address, media: false)
self.context.updateTransportSchemeForDatacenter(withId: Int(datacenterId), transportScheme: scheme, media: false, isProxy: false)
}*/
let currentSchemes = self.context.transportSchemesForDatacenter(withId: Int(datacenterId), media: false, enforceMedia: false, isProxy: false)
var found = false
@ -988,7 +1015,7 @@ public final class Network: NSObject, MTRequestMessageServiceDelegate {
}
}
public func requestWithAdditionalInfo<T>(_ data: (FunctionDescription, Buffer, DeserializeFunctionResponse<T>), info: NetworkRequestAdditionalInfo, tag: NetworkRequestDependencyTag? = nil, automaticFloodWait: Bool = true) -> Signal<NetworkRequestResult<T>, MTRpcError> {
public func requestWithAdditionalInfo<T>(_ data: (FunctionDescription, Buffer, DeserializeFunctionResponse<T>), info: NetworkRequestAdditionalInfo, tag: NetworkRequestDependencyTag? = nil, automaticFloodWait: Bool = true, onFloodWaitError: ((String) -> Void)? = nil) -> Signal<NetworkRequestResult<T>, MTRpcError> {
let requestService = self.requestService
return Signal { subscriber in
let request = MTRequest()
@ -1006,6 +1033,9 @@ public final class Network: NSObject, MTRequestMessageServiceDelegate {
guard let errorContext = errorContext else {
return true
}
if let onFloodWaitError, errorContext.floodWaitSeconds > 0, let errorText = errorContext.floodWaitErrorText {
onFloodWaitError(errorText)
}
if errorContext.floodWaitSeconds > 0 && !automaticFloodWait {
return false
}
@ -1056,8 +1086,8 @@ public final class Network: NSObject, MTRequestMessageServiceDelegate {
}
}
}
public func request<T>(_ data: (FunctionDescription, Buffer, DeserializeFunctionResponse<T>), tag: NetworkRequestDependencyTag? = nil, automaticFloodWait: Bool = true) -> Signal<T, MTRpcError> {
public func request<T>(_ data: (FunctionDescription, Buffer, DeserializeFunctionResponse<T>), tag: NetworkRequestDependencyTag? = nil, automaticFloodWait: Bool = true, onFloodWaitError: ((String) -> Void)? = nil) -> Signal<T, MTRpcError> {
let requestService = self.requestService
return Signal { subscriber in
let request = MTRequest()
@ -1075,6 +1105,9 @@ public final class Network: NSObject, MTRequestMessageServiceDelegate {
guard let errorContext = errorContext else {
return true
}
if let onFloodWaitError, errorContext.floodWaitSeconds > 0, let errorText = errorContext.floodWaitErrorText {
onFloodWaitError(errorText)
}
if errorContext.floodWaitSeconds > 0 && !automaticFloodWait {
return false
}
@ -1113,6 +1146,21 @@ public final class Network: NSObject, MTRequestMessageServiceDelegate {
}
}
}
func updateNetworkSpeedLimitedEventNotifyInterval(value: Double) {
let _ = self.networkSpeedLimitedEventState.with { state in
state.notifyInterval = value
}
}
func addNetworkSpeedLimitedEvent(event: NetworkSpeedLimitedEvent) {
let notify = self.networkSpeedLimitedEventState.with { state in
return state.add(event: event)
}
if notify {
self.networkSpeedLimitedEventPipe.putNext(event)
}
}
}
public func retryRequest<T>(signal: Signal<T, MTRpcError>) -> Signal<T, NoError> {

View File

@ -35,7 +35,9 @@ extension PeerStatusSettings {
var managingBot: ManagingBot?
if let businessBotId {
managingBot = ManagingBot(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(businessBotId)), manageUrl: businessBotManageUrl)
let businessBotPaused = (flags & (1 << 11)) != 0
let businessBotCanReply = (flags & (1 << 12)) != 0
managingBot = ManagingBot(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(businessBotId)), manageUrl: businessBotManageUrl, isPaused: businessBotPaused, canReply: businessBotCanReply)
}
self = PeerStatusSettings(flags: result, geoDistance: geoDistance, requestChatTitle: requestChatTitle, requestChatDate: requestChatDate, requestChatIsChannel: (flags & (1 << 10)) != 0, managingBot: managingBot)

View File

@ -360,11 +360,11 @@ final class ChatHistoryPreloadManager {
guard let strongSelf = self else {
return
}
#if DEBUG
/*#if DEBUG
if "".isEmpty {
return
}
#endif
#endif*/
var indices: [(ChatHistoryPreloadIndex, Bool, Bool)] = []
for item in loadItems {

View File

@ -22,12 +22,15 @@ public struct PeerStatusSettings: PostboxCoding, Equatable {
public struct ManagingBot: Codable, Equatable {
public var id: PeerId
public var manageUrl: String?
public var isPaused: Bool
public var canReply: Bool
public init(id: PeerId, manageUrl: String?) {
public init(id: PeerId, manageUrl: String?, isPaused: Bool, canReply: Bool) {
self.id = id
self.manageUrl = manageUrl
self.isPaused = isPaused
self.canReply = canReply
}
}
public var flags: PeerStatusSettings.Flags

View File

@ -1585,6 +1585,34 @@ public extension TelegramEngine.EngineData.Item {
}
}
public struct ChatManagingBot: TelegramEngineDataItem, TelegramEngineMapKeyDataItem, PostboxViewDataItem {
public typealias Result = PeerStatusSettings.ManagingBot?
fileprivate var id: EnginePeer.Id
public var mapKey: EnginePeer.Id {
return self.id
}
public init(id: EnginePeer.Id) {
self.id = id
}
var key: PostboxViewKey {
return .cachedPeerData(peerId: self.id)
}
func extract(view: PostboxView) -> Result {
guard let view = view as? CachedPeerDataView else {
preconditionFailure()
}
if let cachedData = view.cachedPeerData as? CachedUserData {
return cachedData.peerStatusSettings?.managingBot
} else {
return nil
}
}
}
public struct BotBiometricsState: TelegramEngineDataItem, TelegramEngineMapKeyDataItem, PostboxViewDataItem {
public typealias Result = TelegramBotBiometricsState

View File

@ -358,3 +358,43 @@ func _internal_updateBotBiometricsState(account: Account, peerId: EnginePeer.Id,
}
|> ignoreValues
}
func _internal_toggleChatManagingBotIsPaused(account: Account, chatId: EnginePeer.Id) -> Signal<Never, NoError> {
return account.postbox.transaction { transaction -> Void in
transaction.updatePeerCachedData(peerIds: Set([chatId]), update: { _, current in
guard let current = current as? CachedUserData else {
return current
}
if var peerStatusSettings = current.peerStatusSettings {
if let managingBot = peerStatusSettings.managingBot {
peerStatusSettings.managingBot?.isPaused = !managingBot.isPaused
}
return current.withUpdatedPeerStatusSettings(peerStatusSettings)
} else {
return current
}
})
}
|> ignoreValues
}
func _internal_removeChatManagingBot(account: Account, chatId: EnginePeer.Id) -> Signal<Never, NoError> {
return account.postbox.transaction { transaction -> Void in
transaction.updatePeerCachedData(peerIds: Set([chatId]), update: { _, current in
guard let current = current as? CachedUserData else {
return current
}
if var peerStatusSettings = current.peerStatusSettings {
peerStatusSettings.managingBot = nil
return current.withUpdatedPeerStatusSettings(peerStatusSettings)
} else {
return current
}
})
}
|> ignoreValues
}

View File

@ -1494,6 +1494,14 @@ public extension TelegramEngine {
public func updateBotBiometricsState(peerId: EnginePeer.Id, update: @escaping (TelegramBotBiometricsState) -> TelegramBotBiometricsState) {
let _ = _internal_updateBotBiometricsState(account: self.account, peerId: peerId, update: update).startStandalone()
}
public func toggleChatManagingBotIsPaused(chatId: EnginePeer.Id) {
let _ = _internal_toggleChatManagingBotIsPaused(account: self.account, chatId: chatId).startStandalone()
}
public func removeChatManagingBot(chatId: EnginePeer.Id) {
let _ = _internal_removeChatManagingBot(account: self.account, chatId: chatId).startStandalone()
}
}
}

View File

@ -156,6 +156,18 @@ public func stringForMessageTimestampStatus(accountPeerId: PeerId, message: Mess
}
}
if authorTitle == nil {
for attribute in message.attributes {
if let attribute = attribute as? InlineBusinessBotMessageAttribute {
if let title = attribute.title {
authorTitle = title
} else if let peerId = attribute.peerId, let peer = message.peers[peerId] {
authorTitle = peer.debugDisplayTitle
}
}
}
}
if let subject = associatedData.subject, case let .messageOptions(_, _, info) = subject, case .forward = info {
authorTitle = nil
}

View File

@ -209,8 +209,31 @@ final class StoryContentCaptionComponent: Component {
override init(frame: CGRect) {
self.shadowGradientView = UIImageView()
if let image = StoryContentCaptionComponent.View.shadowImage {
self.shadowGradientView.image = image.stretchableImage(withLeftCapWidth: 0, topCapHeight: Int(image.size.height - 1.0))
if let _ = StoryContentCaptionComponent.View.shadowImage {
let height: CGFloat = 128.0
let baseGradientAlpha: CGFloat = 0.8
let numSteps = 8
let firstStep = 0
let firstLocation = 0.0
let colors = (0 ..< numSteps).map { i -> UIColor in
if i < firstStep {
return UIColor(white: 1.0, alpha: 1.0)
} else {
let step: CGFloat = CGFloat(i - firstStep) / CGFloat(numSteps - firstStep - 1)
let value: CGFloat = 1.0 - bezierPoint(0.42, 0.0, 0.58, 1.0, step)
return UIColor(white: 0.0, alpha: baseGradientAlpha * value)
}
}
let locations = (0 ..< numSteps).map { i -> CGFloat in
if i < firstStep {
return 0.0
} else {
let step: CGFloat = CGFloat(i - firstStep) / CGFloat(numSteps - firstStep - 1)
return (firstLocation + (1.0 - firstLocation) * step)
}
}
self.shadowGradientView.image = generateGradientImage(size: CGSize(width: 8.0, height: height), colors: colors.reversed(), locations: locations.reversed().map { 1.0 - $0 })!.stretchableImage(withLeftCapWidth: 0, topCapHeight: Int(height - 1.0))
}
self.scrollViewContainer = UIView()
@ -386,7 +409,8 @@ final class StoryContentCaptionComponent: Component {
transition.setBounds(view: self.textSelectionKnobContainer, bounds: CGRect(origin: CGPoint(x: 0.0, y: self.scrollView.bounds.minY), size: CGSize()))
let shadowOverflow: CGFloat = 58.0
let shadowHeight: CGFloat = self.shadowGradientView.image?.size.height ?? 100.0
let shadowOverflow: CGFloat = floor(shadowHeight * 0.6)
let shadowFrame = CGRect(origin: CGPoint(x: 0.0, y: -self.scrollView.contentOffset.y + itemLayout.containerSize.height - itemLayout.visibleTextHeight - itemLayout.verticalInset - shadowOverflow), size: CGSize(width: itemLayout.containerSize.width, height: itemLayout.visibleTextHeight + itemLayout.verticalInset + shadowOverflow))
let shadowGradientFrame = CGRect(origin: CGPoint(x: shadowFrame.minX, y: shadowFrame.minY), size: CGSize(width: shadowFrame.width, height: self.scrollView.contentSize.height + 1000.0))

View File

@ -2690,7 +2690,7 @@ public final class StoryItemSetContainerComponent: Component {
self.bottomContentGradientLayer.colors = colors
self.bottomContentGradientLayer.type = .axial
self.contentDimView.backgroundColor = UIColor(white: 0.0, alpha: 0.3)
self.contentDimView.backgroundColor = UIColor(white: 0.0, alpha: 0.8)
}
let wasPanning = self.component?.isPanning ?? false

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "more.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,79 @@
%PDF-1.7
1 0 obj
<< >>
endobj
2 0 obj
<< /Length 3 0 R >>
stream
/DeviceRGB CS
/DeviceRGB cs
q
1.000000 0.000000 -0.000000 1.000000 1.500000 1.335754 cm
0.000000 0.000000 0.000000 scn
5.252930 4.662109 m
5.252930 4.527832 5.199219 4.409668 5.097168 4.307617 c
0.843262 0.145020 l
0.746582 0.048340 0.628418 0.000000 0.488770 0.000000 c
0.214844 0.000000 0.000000 0.209473 0.000000 0.488770 c
0.000000 0.628418 0.053711 0.746582 0.139648 0.837891 c
4.049805 4.662109 l
0.139648 8.486328 l
0.053711 8.577637 0.000000 8.701172 0.000000 8.835449 c
0.000000 9.114746 0.214844 9.324219 0.488770 9.324219 c
0.628418 9.324219 0.746582 9.275879 0.843262 9.184570 c
5.097168 5.016602 l
5.199219 4.919922 5.252930 4.796387 5.252930 4.662109 c
h
f
n
Q
endstream
endobj
3 0 obj
675
endobj
4 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 8.000000 12.000000 ]
/Resources 1 0 R
/Contents 2 0 R
/Parent 5 0 R
>>
endobj
5 0 obj
<< /Kids [ 4 0 R ]
/Count 1
/Type /Pages
>>
endobj
6 0 obj
<< /Pages 5 0 R
/Type /Catalog
>>
endobj
xref
0 7
0000000000 65535 f
0000000010 00000 n
0000000034 00000 n
0000000765 00000 n
0000000787 00000 n
0000000959 00000 n
0000001033 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 6 0 R
/Size 7
>>
startxref
1092
%%EOF

View File

@ -588,6 +588,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
var performTextSelectionAction: ((Message?, Bool, NSAttributedString, TextSelectionAction) -> Void)?
var performOpenURL: ((Message?, String, Promise<Bool>?) -> Void)?
var networkSpeedEventsDisposable: Disposable?
public var alwaysShowSearchResultsAsList: Bool = false {
didSet {
self.presentationInterfaceState = self.presentationInterfaceState.updatedDisplayHistoryFilterAsList(self.alwaysShowSearchResultsAsList)
@ -4909,6 +4911,31 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
})
}
let managingBot: Signal<ChatManagingBot?, NoError>
if let peerId = self.chatLocation.peerId, peerId.namespace == Namespaces.Peer.CloudUser {
managingBot = self.context.engine.data.subscribe(
TelegramEngine.EngineData.Item.Peer.ChatManagingBot(id: peerId)
)
|> mapToSignal { result -> Signal<ChatManagingBot?, NoError> in
guard let result else {
return .single(nil)
}
return context.engine.data.subscribe(
TelegramEngine.EngineData.Item.Peer.Peer(id: result.id)
)
|> map { botPeer -> ChatManagingBot? in
guard let botPeer else {
return nil
}
return ChatManagingBot(bot: botPeer, isPaused: result.isPaused, canReply: result.canReply, settingsUrl: result.manageUrl)
}
}
|> distinctUntilChanged
} else {
managingBot = .single(nil)
}
do {
let peerId = chatLocationPeerId
if case let .peer(peerView) = self.chatLocationInfoData, let peerId = peerId {
@ -5297,10 +5324,11 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
threadInfo,
hasSearchTags,
hasSavedChats,
isPremiumRequiredForMessaging
).startStrict(next: { [weak self] peerView, globalNotificationSettings, onlineMemberCount, hasScheduledMessages, peerReportNotice, pinnedCount, threadInfo, hasSearchTags, hasSavedChats, isPremiumRequiredForMessaging in
isPremiumRequiredForMessaging,
managingBot
).startStrict(next: { [weak self] peerView, globalNotificationSettings, onlineMemberCount, hasScheduledMessages, peerReportNotice, pinnedCount, threadInfo, hasSearchTags, hasSavedChats, isPremiumRequiredForMessaging, managingBot in
if let strongSelf = self {
if strongSelf.peerView === peerView && strongSelf.reportIrrelvantGeoNotice == peerReportNotice && strongSelf.hasScheduledMessages == hasScheduledMessages && strongSelf.threadInfo == threadInfo && strongSelf.presentationInterfaceState.hasSearchTags == hasSearchTags && strongSelf.presentationInterfaceState.hasSavedChats == hasSavedChats && strongSelf.presentationInterfaceState.isPremiumRequiredForMessaging == isPremiumRequiredForMessaging {
if strongSelf.peerView === peerView && strongSelf.reportIrrelvantGeoNotice == peerReportNotice && strongSelf.hasScheduledMessages == hasScheduledMessages && strongSelf.threadInfo == threadInfo && strongSelf.presentationInterfaceState.hasSearchTags == hasSearchTags && strongSelf.presentationInterfaceState.hasSavedChats == hasSavedChats && strongSelf.presentationInterfaceState.isPremiumRequiredForMessaging == isPremiumRequiredForMessaging && managingBot == strongSelf.presentationInterfaceState.contactStatus?.managingBot {
return
}
@ -5395,7 +5423,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
var contactStatus: ChatContactStatus?
if let peer = peerView.peers[peerView.peerId] {
if let cachedData = peerView.cachedData as? CachedUserData {
contactStatus = ChatContactStatus(canAddContact: !peerView.peerIsContact, canReportIrrelevantLocation: false, peerStatusSettings: cachedData.peerStatusSettings, invitedBy: nil)
contactStatus = ChatContactStatus(canAddContact: !peerView.peerIsContact, canReportIrrelevantLocation: false, peerStatusSettings: cachedData.peerStatusSettings, invitedBy: nil, managingBot: managingBot)
} else if let cachedData = peerView.cachedData as? CachedGroupData {
var invitedBy: Peer?
if let invitedByPeerId = cachedData.invitedBy {
@ -5403,7 +5431,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
invitedBy = peer
}
}
contactStatus = ChatContactStatus(canAddContact: false, canReportIrrelevantLocation: false, peerStatusSettings: cachedData.peerStatusSettings, invitedBy: invitedBy)
contactStatus = ChatContactStatus(canAddContact: false, canReportIrrelevantLocation: false, peerStatusSettings: cachedData.peerStatusSettings, invitedBy: invitedBy, managingBot: managingBot)
} else if let cachedData = peerView.cachedData as? CachedChannelData {
var canReportIrrelevantLocation = true
if let peer = peerView.peers[peerView.peerId] as? TelegramChannel, peer.participationStatus == .member {
@ -5418,7 +5446,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
invitedBy = peer
}
}
contactStatus = ChatContactStatus(canAddContact: false, canReportIrrelevantLocation: canReportIrrelevantLocation, peerStatusSettings: cachedData.peerStatusSettings, invitedBy: invitedBy)
contactStatus = ChatContactStatus(canAddContact: false, canReportIrrelevantLocation: canReportIrrelevantLocation, peerStatusSettings: cachedData.peerStatusSettings, invitedBy: invitedBy, managingBot: managingBot)
}
var peers = SimpleDictionary<PeerId, Peer>()
@ -5521,6 +5549,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}
}
}
if let contactStatus = strongSelf.presentationInterfaceState.contactStatus, contactStatus.managingBot != nil {
didDisplayActionsPanel = true
}
if strongSelf.presentationInterfaceState.search != nil && strongSelf.presentationInterfaceState.hasSearchTags {
didDisplayActionsPanel = true
}
@ -5541,6 +5572,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}
}
}
if let contactStatus, contactStatus.managingBot != nil {
displayActionsPanel = true
}
if strongSelf.presentationInterfaceState.search != nil && hasSearchTags {
displayActionsPanel = true
}
@ -5874,9 +5908,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
hasScheduledMessages,
hasSearchTags,
hasSavedChats,
isPremiumRequiredForMessaging
isPremiumRequiredForMessaging,
managingBot
)
|> deliverOnMainQueue).startStrict(next: { [weak self] peerView, messageAndTopic, savedMessagesPeer, onlineMemberCount, hasScheduledMessages, hasSearchTags, hasSavedChats, isPremiumRequiredForMessaging in
|> deliverOnMainQueue).startStrict(next: { [weak self] peerView, messageAndTopic, savedMessagesPeer, onlineMemberCount, hasScheduledMessages, hasSearchTags, hasSavedChats, isPremiumRequiredForMessaging, managingBot in
if let strongSelf = self {
strongSelf.hasScheduledMessages = hasScheduledMessages
@ -5886,7 +5921,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
if let peer = peerView.peers[peerView.peerId] {
copyProtectionEnabled = peer.isCopyProtectionEnabled
if let cachedData = peerView.cachedData as? CachedUserData {
contactStatus = ChatContactStatus(canAddContact: !peerView.peerIsContact, canReportIrrelevantLocation: false, peerStatusSettings: cachedData.peerStatusSettings, invitedBy: nil)
contactStatus = ChatContactStatus(canAddContact: !peerView.peerIsContact, canReportIrrelevantLocation: false, peerStatusSettings: cachedData.peerStatusSettings, invitedBy: nil, managingBot: managingBot)
} else if let cachedData = peerView.cachedData as? CachedGroupData {
var invitedBy: Peer?
if let invitedByPeerId = cachedData.invitedBy {
@ -5894,7 +5929,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
invitedBy = peer
}
}
contactStatus = ChatContactStatus(canAddContact: false, canReportIrrelevantLocation: false, peerStatusSettings: cachedData.peerStatusSettings, invitedBy: invitedBy)
contactStatus = ChatContactStatus(canAddContact: false, canReportIrrelevantLocation: false, peerStatusSettings: cachedData.peerStatusSettings, invitedBy: invitedBy, managingBot: managingBot)
} else if let cachedData = peerView.cachedData as? CachedChannelData {
var canReportIrrelevantLocation = true
if let peer = peerView.peers[peerView.peerId] as? TelegramChannel, peer.participationStatus == .member {
@ -5907,7 +5942,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
invitedBy = peer
}
}
contactStatus = ChatContactStatus(canAddContact: false, canReportIrrelevantLocation: canReportIrrelevantLocation, peerStatusSettings: cachedData.peerStatusSettings, invitedBy: invitedBy)
contactStatus = ChatContactStatus(canAddContact: false, canReportIrrelevantLocation: canReportIrrelevantLocation, peerStatusSettings: cachedData.peerStatusSettings, invitedBy: invitedBy, managingBot: managingBot)
}
var peers = SimpleDictionary<PeerId, Peer>()
@ -6111,6 +6146,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}
}
}
if let contactStatus = strongSelf.presentationInterfaceState.contactStatus, contactStatus.managingBot != nil {
didDisplayActionsPanel = true
}
var displayActionsPanel = false
if let contactStatus = contactStatus, !contactStatus.isEmpty, let peerStatusSettings = contactStatus.peerStatusSettings {
@ -6128,6 +6166,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}
}
}
if let contactStatus, contactStatus.managingBot != nil {
displayActionsPanel = true
}
if displayActionsPanel != didDisplayActionsPanel {
animated = true
@ -6799,6 +6840,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
self.preloadSavedMessagesChatsDisposable?.dispose()
self.recorderDataDisposable.dispose()
self.displaySendWhenOnlineTipDisposable.dispose()
self.networkSpeedEventsDisposable?.dispose()
}
deallocate()
}
@ -11688,6 +11730,57 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}
}
var lastEventTimestamp: Double = 0.0
self.networkSpeedEventsDisposable = (self.context.account.network.networkSpeedLimitedEvents
|> deliverOnMainQueue).start(next: { [weak self] event in
guard let self else {
return
}
let timestamp = CFAbsoluteTimeGetCurrent()
if lastEventTimestamp + 10.0 < timestamp {
lastEventTimestamp = timestamp
} else {
return
}
//TODO:localize
let title: String
let text: String
switch event {
case .download:
var speedIncreaseFactor = 10
if let data = self.context.currentAppConfiguration.with({ $0 }).data, let value = data["upload_premium_speedup_download"] as? Double {
speedIncreaseFactor = Int(value)
}
title = "Download speed limited"
text = "Subscribe to [Telegram Premium]() and increase download speeds \(speedIncreaseFactor) times."
case .upload:
var speedIncreaseFactor = 10
if let data = self.context.currentAppConfiguration.with({ $0 }).data, let value = data["upload_premium_speedup_upload"] as? Double {
speedIncreaseFactor = Int(value)
}
title = "Upload speed limited"
text = "Subscribe to [Telegram Premium]() and increase upload speeds \(speedIncreaseFactor) times."
}
let content: UndoOverlayContent = .universal(animation: "anim_speed_low", scale: 0.066, colors: [:], title: title, text: text, customUndoText: nil, timeout: 5.0)
self.present(UndoOverlayController(presentationData: self.presentationData, content: content, elevatedLayout: false, position: .top, action: { [weak self] action in
guard let self else {
return false
}
switch action {
case .info:
let controller = context.sharedContext.makePremiumIntroController(context: self.context, source: .reactions, forceDark: false, dismissed: nil)
self.push(controller)
return true
default:
break
}
return false
}), in: .current)
})
self.displayNodeDidLoad()
}

View File

@ -117,32 +117,44 @@ func titlePanelForChatPresentationInterfaceState(_ chatPresentationInterfaceStat
}
var displayActionsPanel = false
if !chatPresentationInterfaceState.peerIsBlocked && !inhibitTitlePanelDisplay, let contactStatus = chatPresentationInterfaceState.contactStatus, let peerStatusSettings = contactStatus.peerStatusSettings {
if !peerStatusSettings.flags.isEmpty {
if contactStatus.canAddContact && peerStatusSettings.contains(.canAddContact) {
displayActionsPanel = true
} else if peerStatusSettings.contains(.canReport) || peerStatusSettings.contains(.canBlock) || peerStatusSettings.contains(.autoArchived) {
displayActionsPanel = true
} else if peerStatusSettings.contains(.canShareContact) {
displayActionsPanel = true
} else if contactStatus.canReportIrrelevantLocation && peerStatusSettings.contains(.canReportIrrelevantGeoLocation) {
displayActionsPanel = true
} else if peerStatusSettings.contains(.suggestAddMembers) {
if !chatPresentationInterfaceState.peerIsBlocked && !inhibitTitlePanelDisplay, let contactStatus = chatPresentationInterfaceState.contactStatus {
if let peerStatusSettings = contactStatus.peerStatusSettings {
if !peerStatusSettings.flags.isEmpty {
if contactStatus.canAddContact && peerStatusSettings.contains(.canAddContact) {
displayActionsPanel = true
} else if peerStatusSettings.contains(.canReport) || peerStatusSettings.contains(.canBlock) || peerStatusSettings.contains(.autoArchived) {
displayActionsPanel = true
} else if peerStatusSettings.contains(.canShareContact) {
displayActionsPanel = true
} else if contactStatus.canReportIrrelevantLocation && peerStatusSettings.contains(.canReportIrrelevantGeoLocation) {
displayActionsPanel = true
} else if peerStatusSettings.contains(.suggestAddMembers) {
displayActionsPanel = true
}
}
if peerStatusSettings.requestChatTitle != nil {
displayActionsPanel = true
}
}
if peerStatusSettings.requestChatTitle != nil {
displayActionsPanel = true
}
}
if displayActionsPanel && (selectedContext == nil || selectedContext! <= .pinnedMessage) {
if let currentPanel = currentPanel as? ChatReportPeerTitlePanelNode {
return currentPanel
} else if let controllerInteraction = controllerInteraction {
let panel = ChatReportPeerTitlePanelNode(context: context, animationCache: controllerInteraction.presentationContext.animationCache, animationRenderer: controllerInteraction.presentationContext.animationRenderer)
panel.interfaceInteraction = interfaceInteraction
return panel
if (selectedContext == nil || selectedContext! <= .pinnedMessage) {
if displayActionsPanel {
if let currentPanel = currentPanel as? ChatReportPeerTitlePanelNode {
return currentPanel
} else if let controllerInteraction = controllerInteraction {
let panel = ChatReportPeerTitlePanelNode(context: context, animationCache: controllerInteraction.presentationContext.animationCache, animationRenderer: controllerInteraction.presentationContext.animationRenderer)
panel.interfaceInteraction = interfaceInteraction
return panel
}
} else if !chatPresentationInterfaceState.peerIsBlocked && !inhibitTitlePanelDisplay, let contactStatus = chatPresentationInterfaceState.contactStatus, contactStatus.managingBot != nil {
if let currentPanel = currentPanel as? ChatManagingBotTitlePanelNode {
return currentPanel
} else {
let panel = ChatManagingBotTitlePanelNode(context: context)
panel.interfaceInteraction = interfaceInteraction
return panel
}
}
}

View File

@ -0,0 +1,448 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import TelegramPresentationData
import ChatPresentationInterfaceState
import ComponentFlow
import AvatarNode
import MultilineTextComponent
import PlainButtonComponent
import ComponentDisplayAdapters
import AccountContext
import TelegramCore
import BundleIconComponent
import ContextUI
import SwiftSignalKit
private final class ChatManagingBotTitlePanelComponent: Component {
let context: AccountContext
let theme: PresentationTheme
let strings: PresentationStrings
let insets: UIEdgeInsets
let peer: EnginePeer
let managesChat: Bool
let isPaused: Bool
let toggleIsPaused: () -> Void
let openSettings: (UIView) -> Void
init(
context: AccountContext,
theme: PresentationTheme,
strings: PresentationStrings,
insets: UIEdgeInsets,
peer: EnginePeer,
managesChat: Bool,
isPaused: Bool,
toggleIsPaused: @escaping () -> Void,
openSettings: @escaping (UIView) -> Void
) {
self.context = context
self.theme = theme
self.strings = strings
self.insets = insets
self.peer = peer
self.managesChat = managesChat
self.isPaused = isPaused
self.toggleIsPaused = toggleIsPaused
self.openSettings = openSettings
}
static func ==(lhs: ChatManagingBotTitlePanelComponent, rhs: ChatManagingBotTitlePanelComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.theme !== rhs.theme {
return false
}
if lhs.strings != rhs.strings {
return false
}
if lhs.insets != rhs.insets {
return false
}
if lhs.peer != rhs.peer {
return false
}
if lhs.managesChat != rhs.managesChat {
return false
}
if lhs.isPaused != rhs.isPaused {
return false
}
return true
}
final class View: UIView {
private let title = ComponentView<Empty>()
private let text = ComponentView<Empty>()
private var avatarNode: AvatarNode?
private let actionButton = ComponentView<Empty>()
private let settingsButton = ComponentView<Empty>()
private var component: ChatManagingBotTitlePanelComponent?
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(component: ChatManagingBotTitlePanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
self.component = component
let topInset: CGFloat = 6.0
let bottomInset: CGFloat = 6.0
let avatarDiameter: CGFloat = 36.0
let avatarTextSpacing: CGFloat = 10.0
let titleTextSpacing: CGFloat = 1.0
let leftInset: CGFloat = component.insets.left + 12.0
let rightInset: CGFloat = component.insets.right + 10.0
let actionAndSettingsButtonsSpacing: CGFloat = 8.0
//TODO:localize
let actionButtonSize = self.actionButton.update(
transition: transition,
component: AnyComponent(PlainButtonComponent(
content: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: component.isPaused ? "START" : "STOP", font: Font.semibold(15.0), textColor: component.theme.list.itemCheckColors.foregroundColor))
)),
background: AnyComponent(RoundedRectangle(
color: component.theme.list.itemCheckColors.fillColor,
cornerRadius: nil
)),
effectAlignment: .center,
contentInsets: UIEdgeInsets(top: 5.0, left: 12.0, bottom: 5.0, right: 12.0),
action: { [weak self] in
guard let self, let component = self.component else {
return
}
component.toggleIsPaused()
},
animateAlpha: true,
animateScale: false,
animateContents: false
)),
environment: {},
containerSize: CGSize(width: 150.0, height: 100.0)
)
let settingsButtonSize = self.settingsButton.update(
transition: transition,
component: AnyComponent(PlainButtonComponent(
content: AnyComponent(BundleIconComponent(
name: "Chat/Context Menu/Customize",
tintColor: component.theme.rootController.navigationBar.controlColor
)),
effectAlignment: .center,
minSize: CGSize(width: 1.0, height: 40.0),
contentInsets: UIEdgeInsets(top: 0.0, left: 2.0, bottom: 0.0, right: 2.0),
action: { [weak self] in
guard let self, let component = self.component else {
return
}
guard let settingsButtonView = self.settingsButton.view else {
return
}
component.openSettings(settingsButtonView)
},
animateAlpha: true,
animateScale: false,
animateContents: false
)),
environment: {},
containerSize: CGSize(width: 150.0, height: 100.0)
)
var maxTextWidth: CGFloat = availableSize.width - leftInset - avatarDiameter - avatarTextSpacing - rightInset - settingsButtonSize.width - 8.0
if component.managesChat {
maxTextWidth -= actionButtonSize.width - actionAndSettingsButtonsSpacing
}
let titleSize = self.title.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: component.peer.displayTitle(strings: component.strings, displayOrder: .firstLast), font: Font.semibold(16.0), textColor: component.theme.rootController.navigationBar.primaryTextColor))
)),
environment: {},
containerSize: CGSize(width: maxTextWidth, height: 100.0)
)
//TODO:localize
let textSize = self.text.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: component.managesChat ? "bot manages this chat" : "bot has access to this chat", font: Font.regular(15.0), textColor: component.theme.rootController.navigationBar.secondaryTextColor))
)),
environment: {},
containerSize: CGSize(width: maxTextWidth, height: 100.0)
)
let size = CGSize(width: availableSize.width, height: topInset + titleSize.height + titleTextSpacing + textSize.height + bottomInset)
let titleFrame = CGRect(origin: CGPoint(x: leftInset + avatarDiameter + avatarTextSpacing, y: topInset), size: titleSize)
if let titleView = self.title.view {
if titleView.superview == nil {
titleView.layer.anchorPoint = CGPoint()
self.addSubview(titleView)
}
titleView.bounds = CGRect(origin: CGPoint(), size: titleFrame.size)
transition.setPosition(view: titleView, position: titleFrame.origin)
}
let textFrame = CGRect(origin: CGPoint(x: titleFrame.minX, y: titleFrame.maxY + titleTextSpacing), size: textSize)
if let textView = self.text.view {
if textView.superview == nil {
textView.layer.anchorPoint = CGPoint()
self.addSubview(textView)
}
textView.bounds = CGRect(origin: CGPoint(), size: textFrame.size)
transition.setPosition(view: textView, position: textFrame.origin)
}
let avatarFrame = CGRect(origin: CGPoint(x: leftInset, y: floor((size.height - avatarDiameter) * 0.5)), size: CGSize(width: avatarDiameter, height: avatarDiameter))
let avatarNode: AvatarNode
if let current = self.avatarNode {
avatarNode = current
} else {
avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 15.0))
self.avatarNode = avatarNode
self.addSubview(avatarNode.view)
}
avatarNode.frame = avatarFrame
avatarNode.updateSize(size: avatarFrame.size)
avatarNode.setPeer(context: component.context, theme: component.theme, peer: component.peer)
let settingsButtonFrame = CGRect(origin: CGPoint(x: availableSize.width - rightInset - settingsButtonSize.width, y: floor((size.height - settingsButtonSize.height) * 0.5)), size: settingsButtonSize)
if let settingsButtonView = self.settingsButton.view {
if settingsButtonView.superview == nil {
self.addSubview(settingsButtonView)
}
transition.setFrame(view: settingsButtonView, frame: settingsButtonFrame)
}
let actionButtonFrame = CGRect(origin: CGPoint(x: settingsButtonFrame.minX - actionAndSettingsButtonsSpacing - actionButtonSize.width, y: floor((size.height - actionButtonSize.height) * 0.5)), size: actionButtonSize)
if let actionButtonView = self.actionButton.view {
if actionButtonView.superview == nil {
self.addSubview(actionButtonView)
}
transition.setFrame(view: actionButtonView, frame: actionButtonFrame)
transition.setAlpha(view: actionButtonView, alpha: component.managesChat ? 1.0 : 0.0)
}
return size
}
}
func makeView() -> View {
return View(frame: CGRect())
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
final class ChatManagingBotTitlePanelNode: ChatTitleAccessoryPanelNode {
private let context: AccountContext
private let separatorNode: ASDisplayNode
private let content = ComponentView<Empty>()
private var chatLocation: ChatLocation?
private var theme: PresentationTheme?
private var managingBot: ChatManagingBot?
init(context: AccountContext) {
self.context = context
self.separatorNode = ASDisplayNode()
self.separatorNode.isLayerBacked = true
super.init()
self.addSubnode(self.separatorNode)
}
private func toggleIsPaused() {
guard let chatPeerId = self.chatLocation?.peerId else {
return
}
let _ = self.context.engine.peers.toggleChatManagingBotIsPaused(chatId: chatPeerId)
}
private func openSettingsMenu(sourceView: UIView) {
guard let interfaceInteraction = self.interfaceInteraction else {
return
}
guard let chatController = interfaceInteraction.chatController() else {
return
}
guard let chatPeerId = self.chatLocation?.peerId else {
return
}
guard let managingBot = self.managingBot else {
return
}
let strings = self.context.sharedContext.currentPresentationData.with { $0 }.strings
let _ = strings
var items: [ContextMenuItem] = []
//TODO:localize
items.append(.action(ContextMenuActionItem(text: "Remove bot from this chat", textColor: .destructive, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Clear"), color: theme.contextMenu.destructiveColor)
}, action: { [weak self] _, a in
a(.default)
guard let self else {
return
}
self.context.engine.peers.removeChatManagingBot(chatId: chatPeerId)
})))
if let url = managingBot.settingsUrl {
items.append(.action(ContextMenuActionItem(text: "Manage Bot", icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Settings"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] _, a in
a(.default)
guard let self else {
return
}
let _ = (self.context.sharedContext.resolveUrl(context: self.context, peerId: nil, url: url, skipUrlAuth: false)
|> deliverOnMainQueue).start(next: { [weak self] result in
guard let self else {
return
}
guard let chatController = interfaceInteraction.chatController() else {
return
}
self.context.sharedContext.openResolvedUrl(
result,
context: self.context,
urlContext: .generic,
navigationController: chatController.navigationController as? NavigationController,
forceExternal: false,
openPeer: { [weak self] peer, navigation in
guard let self, let chatController = interfaceInteraction.chatController() else {
return
}
guard let navigationController = chatController.navigationController as? NavigationController else {
return
}
switch navigation {
case let .chat(_, subject, peekData):
self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(peer), subject: subject, peekData: peekData))
case let .withBotStartPayload(botStart):
self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(peer), botStart: botStart, keepStack: .always))
case let .withAttachBot(attachBotStart):
self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(peer), attachBotStart: attachBotStart))
case let .withBotApp(botAppStart):
self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(peer), botAppStart: botAppStart))
case .info:
let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peer.id))
|> deliverOnMainQueue).start(next: { [weak self] peer in
guard let self, let peer, let chatController = interfaceInteraction.chatController() else {
return
}
guard let navigationController = chatController.navigationController as? NavigationController else {
return
}
if let controller = self.context.sharedContext.makePeerInfoController(context: self.context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) {
navigationController.pushViewController(controller)
}
})
default:
break
}
},
sendFile: nil,
sendSticker: nil,
requestMessageActionUrlAuth: nil,
joinVoiceChat: nil,
present: { [weak chatController] c, a in
chatController?.present(c, in: .window(.root), with: a)
},
dismissInput: {
},
contentContext: nil,
progress: nil,
completion: nil
)
})
})))
}
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
let contextController = ContextController(presentationData: presentationData, source: .reference(HeaderContextReferenceContentSource(controller: chatController, sourceView: sourceView)), items: .single(ContextController.Items(content: .list(items))), gesture: nil)
interfaceInteraction.presentController(contextController, nil)
}
override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) -> LayoutResult {
self.chatLocation = interfaceState.chatLocation
self.managingBot = interfaceState.contactStatus?.managingBot
if interfaceState.theme !== self.theme {
self.theme = interfaceState.theme
self.separatorNode.backgroundColor = interfaceState.theme.rootController.navigationBar.separatorColor
}
transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: width, height: UIScreenPixel)))
if let managingBot = interfaceState.contactStatus?.managingBot {
let contentSize = self.content.update(
transition: Transition(transition),
component: AnyComponent(ChatManagingBotTitlePanelComponent(
context: self.context,
theme: interfaceState.theme,
strings: interfaceState.strings,
insets: UIEdgeInsets(top: 0.0, left: leftInset, bottom: 0.0, right: rightInset),
peer: managingBot.bot,
managesChat: managingBot.canReply,
isPaused: managingBot.isPaused,
toggleIsPaused: { [weak self] in
guard let self else {
return
}
self.toggleIsPaused()
},
openSettings: { [weak self] sourceView in
guard let self else {
return
}
self.openSettingsMenu(sourceView: sourceView)
}
)),
environment: {},
containerSize: CGSize(width: width, height: 1000.0)
)
if let contentView = self.content.view {
if contentView.superview == nil {
self.view.addSubview(contentView)
}
transition.updateFrame(view: contentView, frame: CGRect(origin: CGPoint(), size: contentSize))
}
return LayoutResult(backgroundHeight: contentSize.height, insetHeight: contentSize.height, hitTestSlop: 0.0)
} else {
return LayoutResult(backgroundHeight: 0.0, insetHeight: 0.0, hitTestSlop: 0.0)
}
}
}
private final class HeaderContextReferenceContentSource: ContextReferenceContentSource {
private let controller: ViewController
private let sourceView: UIView
init(controller: ViewController, sourceView: UIView) {
self.controller = controller
self.sourceView = sourceView
}
func transitionInfo() -> ContextControllerReferenceViewInfo? {
return ContextControllerReferenceViewInfo(referenceView: self.sourceView, contentAreaInScreenSpace: UIScreen.main.bounds)
}
}

View File

@ -1074,8 +1074,8 @@ public final class WebAppController: ViewController, AttachmentContainable {
self.requestBiometryAuth()
case "web_app_biometry_update_token":
var tokenData: Data?
if let json, let tokenDataValue = json["token"] as? Data {
tokenData = tokenDataValue
if let json, let tokenDataValue = json["token"] as? String, !tokenDataValue.isEmpty {
tokenData = tokenDataValue.data(using: .utf8)
}
self.requestBiometryUpdateToken(tokenData: tokenData)
default:
@ -1514,10 +1514,7 @@ public final class WebAppController: ViewController, AttachmentContainable {
let appBundleId = self.context.sharedContext.applicationBindings.appBundleId
Thread { [weak self] in
var key = LocalAuth.getPrivateKey(baseAppBundleId: appBundleId, keyId: keyId)
if key == nil {
key = LocalAuth.addPrivateKey(baseAppBundleId: appBundleId, keyId: keyId)
}
let key = LocalAuth.getOrCreatePrivateKey(baseAppBundleId: appBundleId, keyId: keyId)
let decryptedData: LocalAuth.DecryptionResult
if let key {
@ -1567,9 +1564,9 @@ public final class WebAppController: ViewController, AttachmentContainable {
data["status"] = isAuthorized ? "authorized" : "failed"
if isAuthorized {
if let tokenData {
data["token"] = tokenData
data["token"] = String(data: tokenData, encoding: .utf8) ?? ""
} else {
data["token"] = Data()
data["token"] = ""
}
}
@ -1593,10 +1590,7 @@ public final class WebAppController: ViewController, AttachmentContainable {
if let tokenData {
let appBundleId = self.context.sharedContext.applicationBindings.appBundleId
Thread { [weak self] in
var key = LocalAuth.getPrivateKey(baseAppBundleId: appBundleId, keyId: keyId)
if key == nil {
key = LocalAuth.addPrivateKey(baseAppBundleId: appBundleId, keyId: keyId)
}
let key = LocalAuth.getOrCreatePrivateKey(baseAppBundleId: appBundleId, keyId: keyId)
var encryptedData: TelegramBotBiometricsState.OpaqueToken?
if let key {
@ -1619,6 +1613,28 @@ public final class WebAppController: ViewController, AttachmentContainable {
state.opaqueToken = encryptedData
return state
})
var data: [String: Any] = [:]
data["status"] = "updated"
guard let jsonData = try? JSONSerialization.data(withJSONObject: data) else {
return
}
guard let jsonDataString = String(data: jsonData, encoding: .utf8) else {
return
}
self.webView?.sendEvent(name: "biometry_token_updated", data: jsonDataString)
} else {
var data: [String: Any] = [:]
data["status"] = "failed"
guard let jsonData = try? JSONSerialization.data(withJSONObject: data) else {
return
}
guard let jsonDataString = String(data: jsonData, encoding: .utf8) else {
return
}
self.webView?.sendEvent(name: "biometry_token_updated", data: jsonDataString)
}
}
}.start()
@ -1628,6 +1644,17 @@ public final class WebAppController: ViewController, AttachmentContainable {
state.opaqueToken = nil
return state
})
var data: [String: Any] = [:]
data["status"] = "removed"
guard let jsonData = try? JSONSerialization.data(withJSONObject: data) else {
return
}
guard let jsonDataString = String(data: jsonData, encoding: .utf8) else {
return
}
self.webView?.sendEvent(name: "biometry_token_updated", data: jsonDataString)
}
}
}