From 2a2d468fd93eab38ed6d6cceb438fb43301e9d68 Mon Sep 17 00:00:00 2001
From: Isaac <>
Date: Fri, 15 Mar 2024 16:13:38 +0400
Subject: [PATCH] Add back changes

---
 .../ChatPresentationInterfaceState.swift      |  40 +-
 submodules/LocalAuth/Sources/LocalAuth.swift  |  38 +-
 .../MtProtoKit/MTRequestErrorContext.h        |   1 +
 .../Sources/MTRequestMessageService.m         |  28 +-
 .../Postbox/Sources/TimeBasedCleanup.swift    |   3 +-
 .../Sources/Account/Account.swift             |  24 +-
 .../Sources/Network/Download.swift            |  28 +-
 .../Sources/Network/MultipartFetch.swift      |  39 +-
 .../Sources/Network/MultipartUpload.swift     |  13 +-
 .../Network/MultiplexedRequestManager.swift   |  18 +-
 .../Sources/Network/Network.swift             | 100 +++-
 .../Settings/PeerContactSettings.swift        |   4 +-
 .../State/ChatHistoryPreloadManager.swift     |   4 +-
 .../SyncCore_PeerStatusSettings.swift         |   7 +-
 .../TelegramEngine/Data/PeersData.swift       |  28 ++
 .../TelegramEngine/Messages/BotWebView.swift  |  40 ++
 .../Peers/TelegramEnginePeers.swift           |   8 +
 .../StringForMessageTimestampStatus.swift     |  12 +
 .../StoryContentCaptionComponent.swift        |  30 +-
 .../StoryItemSetContainerComponent.swift      |   2 +-
 .../Contents.json                             |  12 +
 .../InlineTextRightArrow.imageset/more.pdf    |  79 +++
 .../Resources/Animations/anim_speed_low.tgs   | Bin 0 -> 3990 bytes
 .../TelegramUI/Sources/ChatController.swift   | 115 ++++-
 .../ChatInterfaceTitlePanelNodes.swift        |  54 ++-
 .../ChatManagingBotTitlePanelNode.swift       | 448 ++++++++++++++++++
 .../WebUI/Sources/WebAppController.swift      |  51 +-
 27 files changed, 1108 insertions(+), 118 deletions(-)
 create mode 100644 submodules/TelegramUI/Images.xcassets/Item List/InlineTextRightArrow.imageset/Contents.json
 create mode 100644 submodules/TelegramUI/Images.xcassets/Item List/InlineTextRightArrow.imageset/more.pdf
 create mode 100644 submodules/TelegramUI/Resources/Animations/anim_speed_low.tgs
 create mode 100644 submodules/TelegramUI/Sources/ChatManagingBotTitlePanelNode.swift

diff --git a/submodules/ChatPresentationInterfaceState/Sources/ChatPresentationInterfaceState.swift b/submodules/ChatPresentationInterfaceState/Sources/ChatPresentationInterfaceState.swift
index 75406cdcf6..3cc929b696 100644
--- a/submodules/ChatPresentationInterfaceState/Sources/ChatPresentationInterfaceState.swift
+++ b/submodules/ChatPresentationInterfaceState/Sources/ChatPresentationInterfaceState.swift
@@ -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
     }
 }
diff --git a/submodules/LocalAuth/Sources/LocalAuth.swift b/submodules/LocalAuth/Sources/LocalAuth.swift
index 7796fc355e..ad67c038e6 100644
--- a/submodules/LocalAuth/Sources/LocalAuth.swift
+++ b/submodules/LocalAuth/Sources/LocalAuth.swift
@@ -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
     }
 }
diff --git a/submodules/MtProtoKit/PublicHeaders/MtProtoKit/MTRequestErrorContext.h b/submodules/MtProtoKit/PublicHeaders/MtProtoKit/MTRequestErrorContext.h
index b1e9361e08..c7105cb69c 100644
--- a/submodules/MtProtoKit/PublicHeaders/MtProtoKit/MTRequestErrorContext.h
+++ b/submodules/MtProtoKit/PublicHeaders/MtProtoKit/MTRequestErrorContext.h
@@ -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;
diff --git a/submodules/MtProtoKit/Sources/MTRequestMessageService.m b/submodules/MtProtoKit/Sources/MTRequestMessageService.m
index 83457c3c61..a81adedb34 100644
--- a/submodules/MtProtoKit/Sources/MTRequestMessageService.m
+++ b/submodules/MtProtoKit/Sources/MTRequestMessageService.m
@@ -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)
                                     {
diff --git a/submodules/Postbox/Sources/TimeBasedCleanup.swift b/submodules/Postbox/Sources/TimeBasedCleanup.swift
index 1c1a0be7eb..d0b9fddc61 100644
--- a/submodules/Postbox/Sources/TimeBasedCleanup.swift
+++ b/submodules/Postbox/Sources/TimeBasedCleanup.swift
@@ -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 {
diff --git a/submodules/TelegramCore/Sources/Account/Account.swift b/submodules/TelegramCore/Sources/Account/Account.swift
index 6ed005ceef..1114aaa8bc 100644
--- a/submodules/TelegramCore/Sources/Account/Account.swift
+++ b/submodules/TelegramCore/Sources/Account/Account.swift
@@ -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")
diff --git a/submodules/TelegramCore/Sources/Network/Download.swift b/submodules/TelegramCore/Sources/Network/Download.swift
index 2ebe6cff22..7966a99445 100644
--- a/submodules/TelegramCore/Sources/Network/Download.swift
+++ b/submodules/TelegramCore/Sources/Network/Download.swift
@@ -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
                 }
diff --git a/submodules/TelegramCore/Sources/Network/MultipartFetch.swift b/submodules/TelegramCore/Sources/Network/MultipartFetch.swift
index 5c9a473a3a..83d0c4b40c 100644
--- a/submodules/TelegramCore/Sources/Network/MultipartFetch.swift
+++ b/submodules/TelegramCore/Sources/Network/MultipartFetch.swift
@@ -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
                                     }
diff --git a/submodules/TelegramCore/Sources/Network/MultipartUpload.swift b/submodules/TelegramCore/Sources/Network/MultipartUpload.swift
index 95331fcf9f..3f07e3bb5e 100644
--- a/submodules/TelegramCore/Sources/Network/MultipartUpload.swift
+++ b/submodules/TelegramCore/Sources/Network/MultipartUpload.swift
@@ -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))
diff --git a/submodules/TelegramCore/Sources/Network/MultiplexedRequestManager.swift b/submodules/TelegramCore/Sources/Network/MultiplexedRequestManager.swift
index 7e1a846480..1172b3c739 100644
--- a/submodules/TelegramCore/Sources/Network/MultiplexedRequestManager.swift
+++ b/submodules/TelegramCore/Sources/Network/MultiplexedRequestManager.swift
@@ -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()
diff --git a/submodules/TelegramCore/Sources/Network/Network.swift b/submodules/TelegramCore/Sources/Network/Network.swift
index 2914559d88..4d82d66fba 100644
--- a/submodules/TelegramCore/Sources/Network/Network.swift
+++ b/submodules/TelegramCore/Sources/Network/Network.swift
@@ -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> {
diff --git a/submodules/TelegramCore/Sources/Settings/PeerContactSettings.swift b/submodules/TelegramCore/Sources/Settings/PeerContactSettings.swift
index b6f68330b4..cc2ec49b8b 100644
--- a/submodules/TelegramCore/Sources/Settings/PeerContactSettings.swift
+++ b/submodules/TelegramCore/Sources/Settings/PeerContactSettings.swift
@@ -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)
diff --git a/submodules/TelegramCore/Sources/State/ChatHistoryPreloadManager.swift b/submodules/TelegramCore/Sources/State/ChatHistoryPreloadManager.swift
index acd550c523..37e1406e1f 100644
--- a/submodules/TelegramCore/Sources/State/ChatHistoryPreloadManager.swift
+++ b/submodules/TelegramCore/Sources/State/ChatHistoryPreloadManager.swift
@@ -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 {
diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_PeerStatusSettings.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_PeerStatusSettings.swift
index 8808b0857a..07748e0da2 100644
--- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_PeerStatusSettings.swift
+++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_PeerStatusSettings.swift
@@ -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
diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift b/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift
index 9ed2063e1b..ca97ac3ea4 100644
--- a/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift
+++ b/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift
@@ -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
 
diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/BotWebView.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/BotWebView.swift
index 55e5779f21..855940e197 100644
--- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/BotWebView.swift
+++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/BotWebView.swift
@@ -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
+}
diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift
index 6980bbab07..6b35401d3f 100644
--- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift
+++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift
@@ -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()
+        }
     }
 }
 
diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode/Sources/StringForMessageTimestampStatus.swift b/submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode/Sources/StringForMessageTimestampStatus.swift
index 69bc656b17..ef3ac7d6ff 100644
--- a/submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode/Sources/StringForMessageTimestampStatus.swift
+++ b/submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode/Sources/StringForMessageTimestampStatus.swift
@@ -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
     }
diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContentCaptionComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContentCaptionComponent.swift
index 42c75a2a00..7bc8d4a842 100644
--- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContentCaptionComponent.swift
+++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContentCaptionComponent.swift
@@ -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))
diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift
index 26853ef250..ab245eef4b 100644
--- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift
+++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift
@@ -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
diff --git a/submodules/TelegramUI/Images.xcassets/Item List/InlineTextRightArrow.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Item List/InlineTextRightArrow.imageset/Contents.json
new file mode 100644
index 0000000000..9d3f9124cb
--- /dev/null
+++ b/submodules/TelegramUI/Images.xcassets/Item List/InlineTextRightArrow.imageset/Contents.json	
@@ -0,0 +1,12 @@
+{
+  "images" : [
+    {
+      "filename" : "more.pdf",
+      "idiom" : "universal"
+    }
+  ],
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  }
+}
diff --git a/submodules/TelegramUI/Images.xcassets/Item List/InlineTextRightArrow.imageset/more.pdf b/submodules/TelegramUI/Images.xcassets/Item List/InlineTextRightArrow.imageset/more.pdf
new file mode 100644
index 0000000000..82da70ea58
--- /dev/null
+++ b/submodules/TelegramUI/Images.xcassets/Item List/InlineTextRightArrow.imageset/more.pdf	
@@ -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
\ No newline at end of file
diff --git a/submodules/TelegramUI/Resources/Animations/anim_speed_low.tgs b/submodules/TelegramUI/Resources/Animations/anim_speed_low.tgs
new file mode 100644
index 0000000000000000000000000000000000000000..bb9b6f86c602f47c75972fa7ea2e6c417b269d44
GIT binary patch
literal 3990
zcmV;H4{7ipiwFP!000021MOVdZX8Dv{1suIyBn2tm)?wB?Y;=Mu?O}+-~)F>S%P(e
zr0q2X{rg5_RySvc!y!35WMKt?$MjK^RT&i-nN`%M)%j0ntEV!oeqB9Xr72B*HLPBp
zuAXK$tloTFJ;jd?_>t1E`VBwC53ASx=lgef=kGs0d-mri&wu#W6TzD=U%u2=j?d0c
z&e8CTqha;-`1g}j{{CsX{pS5k?ezTj)zi=$`u?BKpFjEWyYHVpM>}VydjDs<_G$Hj
z-;VXgpYi?ku===h<%N%R2q_Ql2=|UR@3}#ovpZ+~j@X?~t2eyxUp_R&p?|?(81+I1
z{Nqa>E}oHGhPChg$@S}TiZSx7bM2pl_Gu~-2T8O8w>EmML;u>-E8Etuxxb+g2YpEI
zL?lfiq6_bLb+g^Xu5SF$zjk!vWADa=p_M~>2t)tc(L?A1X?1D|ue$gkz2tcuerRo1
z4{7LM2lfy{9|v2%@CELqWeyFIdh6&j0sFz$#79LY7n@X0hhga7?-cS|YscH#iGA&S
zcY6BaH!Pcv$EPRn&$XS66?D1>`O9Q(4KvTf>qARse-4+tazz7StAQCsV{wNzF~ej`
z2U{^?V7xq#bi*cQCyyy@dw0z0)k_SOhU|9I(^#fb%aGgTfthP*>aO^lotOsi*3l{H
z8g!n$KK^)OBu?P1emVu*-kh6#?+uxH<LvcnM%}J4Z~@aQS+kku@|xSQQQvv-Vgx%n
zF8bg=O_+0htOKt49Qip<4zXzT_`#1dISy`W8Jx<Y<9nWb(N|I{yg0=&pq*%+^pP=4
z5=EOW@#sixd%+jMcxXEm_5~)doLCR&3@^|}<EPfVHkxZ1(0z04*g6|V?b^(ajr5wl
zzvvWAA#ky0ZJ{Q{0#_0Z_*Dn*!&EEm@-m5Qef${U=%ZRXfcmHE(UZsfIwR61#{uGr
zSc48facCfKbl2ywxr{3krlj*U3Ww_LA<=fnAT|V_rxw``e~Tj8Rmnml|Fe2}e)`MF
z=Z%$wLDn5Uj(*+A625u)!pry!UK-<+v(5BGXB2pr%_6wx;(0p?KYKq_7&TVB#le+H
z0zOKDc|MyR0h$9|h2f$T$~MHFN!rNh!8}P_-sfmUyD6&~LDD|=UhCbN>c4r{*07U2
zU|oi$<AY8^o9Zd?Q%0M(mqWJ23y1N(lfzvbus{P^!SYV#St$FcCX~=Fd3G%-esdTd
zCh!gfD2CON+37L-GPS^Wgv6@1gSLUki%Ym&w3`E0BZO}pkbY&IQDCN|z&@BzY&eXO
zTt(lH$LFs#>3WUu`|9}Z*@-%nLHO0%mA<mBG*l|AiG}43Vm+>zTvWB$+yBd(w{Nd#
z|D4hP<94F8JN??wG_x(U#26N7dXe1ivhM8XOOKr$U%I4b|Ks%d{n@Jzr|(vukJe^t
zwG8fJI7g<s(@j-(*i_wrg4RAc-6OlVW+);;mmzVj*WF?;D0bGL*e3?>nhuGrd5G%Z
zb|yny8U5RyE+Us$|F)}_9_d9H$sd|^UDL}Pap1$cb1>Wa?JW}gof%{u?Ca(U-hmR@
zurU)eh#^7lIps5zhlwn?ak*ce33b=K;fj_P4m2bp4#h|g3vG&^ctd8VZr*p0BC1$o
z(vZ_1SR<@&Y;{|<`r+5(w|m$s-juE4CR_dO=0p{yfSk1XNqpN)R8Xo|*B4JbM50(j
zPYe0mhS=l^wLAE2gUjY+0$nAVHvylj1rSJU8^+)#A1@ThHQy1y8&y<witz?Lf!yD9
z^(@rj2>#KfWvxLQ1fK_FN>xm3L`XkmWKwU~Db;s@r7cgQVj&dh46pa3Kt+{-#XiWW
zd96x>exdF`=dLnIl<akm3e=>HK9<RcGSmuzku+<hhSCes)6y2S08t@Qx!#lKb>1#s
zB@mQIlf?TmtE3c+k>$n)v1B5!KnF=>r^PtJK!OAVxoV8oNd{4m8FsZj3C68FAFlB{
z8e#H8&#Sh=kSb%E*MVZ3snJ1RbLXi-CL>|i0bnj5FXNO{<w8TpplY?nWHPX5CXP9+
zCUFyFATGD$4~7dS3f3f(*)+AOK2}>Pbb%)1g)Veio>gi>EEYv5?lfTn*X)H@RfWSb
z%{U);fC7PDwG^M}KOMw^z#Xs_oD7B2pcewN1+P%bj#f0xAk3`NaZxe9n2QWB=)@-e
z!UF!}EZbnB)CR3z*svX%5@CzQ*_DFSgKf@LkVnE=ycTQlT*sQOJA_HiUV=qw_!BDz
z3%l7TkSr4|d(y{7!Jn0%LPdr`-vEQgUoEiZh(*`)DPRlUN&4|J*y0VqCJ=WpAlQO7
zTgTa0hWKXXfz;EO#_9kxfpoVpX(b$^!6~C(Bt~9y7E8w>$<I<-wwGCNz<Wirz(dhS
zX2bKg3X$uIPeWr8sc1tE1tY*hNYaS5s7OJkRg|X^V1;26Xz^xiEVBdY#5hvY2s{Cl
zdZRIdP4q&F{_(K3RR#qx3NrYMxh+W0N2)e&pVb%S<Q{j(Rhav%>l!nzIdv~6;c3^%
zfi?BJg>KUdBTR_QL?!1=YJqvF*u4-yaZ@U}2L%V&8+7Vtn4#@R5Y%KVf_Bbknw8y1
z(wb;T!Qep~XA{S6<2V~Rnw8^hrryYkPjmcerjE0%c#vEOW7W~!)^Rb`5SH>(;BAn&
zSoJhjt#tsK%^BiYBgg|HVv4mxkXoY5q$vqt!jKUxokC7R#%gVpN3L0omchJf_|>7$
z;TJG=&OFjIcmkb94oEHlbVMIu%$S7B2X=At>V09X?Zp^4hwPT&&|sfaQBX<j<rZmG
z*G^EZux3#WW#x7#6Bb|wE=9W+JtK5UV&NbwPIB`XT!u#sR$}A`LuWh9%-KpZi1yRd
z7MeI)cmT-E(s4F*3uI>MxY*iRj8AhBMb!xcCp!|cFmUPxMEnL{BLe2D5Qf-k2rGvo
ztfG@VM7(LQFzO>51yY~YB8xUEjO5|IASPSN977BA0EnbZkVLf$E=axdtrIR?O*jim
zjEh>z47yCN#u#m*_e^fn@4A{WE7VnnHry|Si_InCS4sq*m%88!t1b|pu^F(FibS)~
zOk6A+Sv@o!D;+{o_wpER950Nd%(DW#nK@qAnUX%)loHI&aWT}u1))&62tBA?10o|`
z*0`QR2Ui4XM;2-2Ds*MJ4qZ;f&=s%ZFTt!tOp;;R1qN1%X2TXq!VlCC2O%r$+dRV9
z6Mzg(n5)&SPE(zURa2*2z3ODlh6Oc~%^{h4bB%kV<8Dl$(J0IgolZ$$b$<}T+lu^9
zW>CdwS{78nRhfqyFpDZh)Xv3BvvM(#h(SJ)dSDvO%7buarjCoP-u9E=8znU}bzE#U
zU{QZ3T&A3FmPF<$>e}-iH*hxE#RE;Q<gFydkd+Qa7Iaq*Lk!Gq$g#KsrHf_eLw}*D
zu7v&f7a8S}{GxjSF-te#G$eiK6uM#ABH<2)a2+H|JDtsW^9G}wO@f7+V+<Yw*15G?
zZRsL3QzT360XkO};XtMr<vy51ce00)Y>SDeE|!|1i=7If${tgXkhF6y>)i%rX6(3F
z>oncqMvggBjU6wnRhS_w3|x$b95hFvEJc&3+8K?7b4>_yx{|>nIh;6?Lvco>MIq=^
z{Xq0L%E`d(QKjFE@n9-a(CNP~oRtln(P`YBO6Cl&++dlUDHfiO%DTCqYL9H1CSr<|
zHG<Zx6lVSw@ez+T9emSn5G=SeM{2U#hsvCWsVWQ30;iJFDipCzX_~rNYKAU$G9&RS
z_&SVe=4|Edf~46xF2*urEy_!+^eTsnvc+8EvrNYhmfxk;1yJQaJ0w51MTsL}6xEGW
z%gT2lEg;qqX(b+pw1&`Z0z!0=0r1>D9aRO^zM+WmROa~ihO>HI>B7nuweM&`V{k*p
zN~OAH6&F}$l>}7OO$_SXD8>9~l=rM%Cc06IaiF*TU1@pQuEnrZ=(;}Z4sloxB;25E
zv1ZNK?Hm_F83dB;*&-Du-v-9MW@)@CWUFLa2}PJPyIyT`%J8n2nD~$a(xtgD#<P|4
zw*4*26`T0E43}Lzgt+TjB~z7<EeAm+*mAFRF<fW-@JrI-^+GXi!0mo9sHev)#Y+3~
zAPdaf8K|WnS;{;ZOyTt<Fvgw!rdz0D#3zg<Oc<+Xr>xY9^4wey^K=uH-xr4&GfJMO
z0KBXYi*Y(8YZ<A8?TFwvtqB56PAxthXuFOD?tqw=M}k;Vh}ppsJm@S-ZPm!3CL=N(
zv!3f9bz}>5j}Efv?6k-<muaFIuferh<(T!QrRX~7#+3i#K@1|RCR~XgqTWWM<)xsg
zt2P1mim|Zo+?t}xKQOvH$7+(!3XGH%Jm(Ou1Xs9EaQSP%g&@L{4)om)c(F%{sOb7m
z#$5pR;P_%$A_U7)eAkna`@BU3WwD)+hXdGrfc;VF3&1Lx6U5pJ7fs2+%3oiez4ikv
zTiqTMtkt(aHh<-6n_S#@4LyTqD+4#jf`4dq<u5^BeXj|5Pxe-gFRUUX?N46=f$5)4
z`JQ9@KRm)}fe2|@mJ7{B%(Gj8plfsbd@#h)br6f;3lJDjF=)Lq^fMb@6@j&h#~xX?
zZ|SQLU@SgRfZ47-1(?>HAi$&@!eDEMBOh0`i)~QWz9$hUVFA5;4WMD(Xrx-&S3pW&
zASSNdHTNAgO$Wfe&VEAl7NGfd#{Jf(M*n*H;g^p(l&cPo-uRD8)8@g^=l|CSNB0ra
zzH^-1U$FwBf_=Wl6I<cH)1-UZ=LRQB8(x-mlX%F~-{p|$&8X!*51I0?U~2}TUSI=7
zmW?ZV5H-`V@&Jnapd+ZaI2OoSQ{oxa+z&YL;5`qm=`J%ImNX7zJ@F|#KWOKgvu589
z0J{SY3OYSDl<W+RFd@(j&Wf@zOs#PERm%Ll@Bl`v4+++5&ka&nUz{7XQ4??KmdIV3
zG=wm3Xo(#uXW6FfaL2#p&ODS_`Xg{lh@vYZcC<$QV=-PSbw+CqbtpVHe4w~`ysCTu
zYmt(cBhrt{a#v`qx?5v$PD9$Z=?k;r-Rf77b*r?-Z4b|gHxXm!?^gT&R;`BRFVOV&
ztv~MC`X3hNzq0b%f5p|V`ac?EtA)N31%6E1ZRD$(NLy`4Pg>}pyZ)yaF&*B<Tr`aS
zuK(!;n77{bf4yMhvYpnU-DE&Hxo-@#dU@h*i-P9v0R=p%a#s{!Wrn+=Aj@4*knW6u
w%)g`D!T=9W8wNI$8&CQ4P7%>_mpAX8kV?+JC3X*-*d2ZTA68jUM&L^T04{30rvLx|

literal 0
HcmV?d00001

diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift
index 57a80c4980..dd005cd94b 100644
--- a/submodules/TelegramUI/Sources/ChatController.swift
+++ b/submodules/TelegramUI/Sources/ChatController.swift
@@ -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()
     }
 
diff --git a/submodules/TelegramUI/Sources/ChatInterfaceTitlePanelNodes.swift b/submodules/TelegramUI/Sources/ChatInterfaceTitlePanelNodes.swift
index d90cc96063..694a5266a2 100644
--- a/submodules/TelegramUI/Sources/ChatInterfaceTitlePanelNodes.swift
+++ b/submodules/TelegramUI/Sources/ChatInterfaceTitlePanelNodes.swift
@@ -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
+            }
         }
     }
     
diff --git a/submodules/TelegramUI/Sources/ChatManagingBotTitlePanelNode.swift b/submodules/TelegramUI/Sources/ChatManagingBotTitlePanelNode.swift
new file mode 100644
index 0000000000..bab56dccb2
--- /dev/null
+++ b/submodules/TelegramUI/Sources/ChatManagingBotTitlePanelNode.swift
@@ -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)
+    }
+}
diff --git a/submodules/WebUI/Sources/WebAppController.swift b/submodules/WebUI/Sources/WebAppController.swift
index 4991caf7a8..eeeeb4784a 100644
--- a/submodules/WebUI/Sources/WebAppController.swift
+++ b/submodules/WebUI/Sources/WebAppController.swift
@@ -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)
             }
         }
     }