From da7b04a59271426f2d787b79424ad33508209e48 Mon Sep 17 00:00:00 2001 From: Ali <> Date: Tue, 3 Jan 2023 17:44:14 +0400 Subject: [PATCH] Storage usage improvements --- .../Sources/Node/ChatListNode.swift | 16 +- .../Node/ChatListStorageInfoItem.swift | 2 + .../Sources/DirectMediaImageCache.swift | 23 +- submodules/Postbox/Sources/MediaBox.swift | 208 +++++++++----- .../Sources/StorageBox/StorageBox.swift | 2 + .../Sources/Account/AccountManager.swift | 17 ++ .../TelegramCore/Sources/Authorization.swift | 18 +- .../Resources/CollectCacheUsageStats.swift | 42 ++- .../TelegramNotices/Sources/Notices.swift | 24 ++ .../Sources/AnimationCache.swift | 63 ++++- .../Sources/PieChartComponent.swift | 259 ++++++++++++++---- .../StoragePeerListPanelComponent.swift | 15 +- .../Sources/StorageUsageScreen.swift | 103 ++++--- .../ParticleStar.imageset/Contents.json | 12 + .../Storage/ParticleStar.imageset/Star.svg | 7 + .../TelegramUI/Sources/AccountContext.swift | 5 + .../TelegramUI/Sources/AppDelegate.swift | 59 ++-- 17 files changed, 679 insertions(+), 196 deletions(-) create mode 100644 submodules/TelegramUI/Images.xcassets/Settings/Storage/ParticleStar.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Settings/Storage/ParticleStar.imageset/Star.svg diff --git a/submodules/ChatListUI/Sources/Node/ChatListNode.swift b/submodules/ChatListUI/Sources/Node/ChatListNode.swift index dbed4fef69..2b2373e390 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListNode.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListNode.swift @@ -1341,12 +1341,20 @@ public final class ChatListNode: ListView { } let storageInfo: Signal - if "".isEmpty, case .chatList(groupId: .root) = location, chatListFilter == nil { - let storageBox = context.account.postbox.mediaBox.storageBox - storageInfo = storageBox.totalSize() + if case .chatList(groupId: .root) = location, chatListFilter == nil { + let totalSizeSignal = combineLatest(context.account.postbox.mediaBox.storageBox.totalSize(), context.account.postbox.mediaBox.cacheStorageBox.totalSize()) + |> map { a, b -> Int64 in + return a + b + } + + storageInfo = totalSizeSignal |> take(1) |> mapToSignal { initialSize -> Signal in + #if DEBUG + let fractionLimit: Double = 0.0001 + #else let fractionLimit: Double = 0.3 + #endif let systemAttributes = try? FileManager.default.attributesOfFileSystem(forPath: NSHomeDirectory() as String) let deviceFreeSpace = (systemAttributes?[FileAttributeKey.systemFreeSize] as? NSNumber)?.int64Value ?? 0 @@ -1375,7 +1383,7 @@ public final class ChatListNode: ListView { let state = Atomic(value: ReportState(lastSize: initialSize)) let updatedReportSize: Signal = Signal { subscriber in - let disposable = storageBox.totalSize().start(next: { size in + let disposable = totalSizeSignal.start(next: { size in let updatedSize = state.with { state -> Int64 in if abs(initialSize - size) > 50 * 1024 * 1024 { state.lastSize = size diff --git a/submodules/ChatListUI/Sources/Node/ChatListStorageInfoItem.swift b/submodules/ChatListUI/Sources/Node/ChatListStorageInfoItem.swift index 4b00cfe93c..0572ee0264 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListStorageInfoItem.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListStorageInfoItem.swift @@ -92,6 +92,8 @@ class ChatListStorageInfoItemNode: ListViewItemNode { self.addSubnode(self.titleNode) self.addSubnode(self.textNode) self.addSubnode(self.arrowNode) + + self.zPosition = 1.0 } override func didLoad() { diff --git a/submodules/DirectMediaImageCache/Sources/DirectMediaImageCache.swift b/submodules/DirectMediaImageCache/Sources/DirectMediaImageCache.swift index e27b4c49ce..0ae5bceca9 100644 --- a/submodules/DirectMediaImageCache/Sources/DirectMediaImageCache.swift +++ b/submodules/DirectMediaImageCache/Sources/DirectMediaImageCache.swift @@ -71,7 +71,16 @@ private func generateBlurredThumbnail(image: UIImage, adjustSaturation: Bool = f return thumbnailContext.generateImage() } -private func storeImage(context: DrawingContext, to path: String) -> UIImage? { +private func storeImage(context: DrawingContext, mediaBox: MediaBox, resourceId: MediaResourceId, imageType: DirectMediaImageCache.ImageType) -> UIImage? { + let representationId: String + switch imageType { + case .blurredThumbnail: + representationId = "blurred32" + case let .square(width): + representationId = "shm\(width)" + } + let path = mediaBox.cachedRepresentationPathForId(resourceId.stringRepresentation, representationId: representationId, keepDuration: .general) + if context.size.width <= 70.0 && context.size.height <= 70.0 { guard let file = ManagedFile(queue: nil, path: path, mode: .readwrite) else { return nil @@ -103,6 +112,9 @@ private func storeImage(context: DrawingContext, to path: String) -> UIImage? { vImageConvert_BGRA8888toRGB565(&source, &target, vImage_Flags(kvImageDoNotTile)) let _ = file.write(targetData, count: targetLength) + if let pathData = path.data(using: .utf8), let size = file.getSize() { + mediaBox.cacheStorageBox.update(id: pathData, size: size) + } return context.generateImage() } else { @@ -110,6 +122,9 @@ private func storeImage(context: DrawingContext, to path: String) -> UIImage? { return nil } let _ = try? resultData.write(to: URL(fileURLWithPath: path)) + if let pathData = path.data(using: .utf8) { + mediaBox.cacheStorageBox.update(id: pathData, size: Int64(resultData.count)) + } return image } } @@ -212,7 +227,7 @@ public final class DirectMediaImageCache { } } - private enum ImageType { + fileprivate enum ImageType { case blurredThumbnail case square(width: Int) } @@ -236,8 +251,6 @@ public final class DirectMediaImageCache { private func getLoadSignal(width: Int, userLocation: MediaResourceUserLocation, userContentType: MediaResourceUserContentType, resource: MediaResourceReference, resourceSizeLimit: Int64) -> Signal? { return Signal { subscriber in - let cachePath = self.getCachePath(resourceId: resource.resource.id, imageType: .square(width: width)) - let fetch = fetchedMediaResource( mediaBox: self.account.postbox.mediaBox, userLocation: userLocation, @@ -281,7 +294,7 @@ public final class DirectMediaImageCache { context.draw(image.cgImage!, in: imageRect) } - if let scaledImage = storeImage(context: scaledContext, to: cachePath) { + if let scaledImage = storeImage(context: scaledContext, mediaBox: self.account.postbox.mediaBox, resourceId: resource.resource.id, imageType: .square(width: width)) { subscriber.putNext(scaledImage) subscriber.putCompletion() } diff --git a/submodules/Postbox/Sources/MediaBox.swift b/submodules/Postbox/Sources/MediaBox.swift index 8068698b12..287bb9c68f 100644 --- a/submodules/Postbox/Sources/MediaBox.swift +++ b/submodules/Postbox/Sources/MediaBox.swift @@ -145,6 +145,7 @@ public final class MediaBox { private let timeBasedCleanup: TimeBasedCleanup public let storageBox: StorageBox + public let cacheStorageBox: StorageBox private let didRemoveResourcesPipe = ValuePipe() public var didRemoveResources: Signal { @@ -192,6 +193,9 @@ public final class MediaBox { self.storageBox = StorageBox(logger: StorageBox.Logger(impl: { string in postboxLog(string) }), basePath: basePath + "/storage") + self.cacheStorageBox = StorageBox(logger: StorageBox.Logger(impl: { string in + postboxLog(string) + }), basePath: basePath + "/cache-storage") self.timeBasedCleanup = TimeBasedCleanup(storageBox: self.storageBox, generalPaths: [ self.basePath + "/cache", @@ -878,6 +882,9 @@ public final class MediaBox { public func storeCachedResourceRepresentation(_ resource: MediaResource, representation: CachedMediaResourceRepresentation, data: Data) { self.dataQueue.async { let path = self.cachedRepresentationPathsForId(resource.id.stringRepresentation, representationId: representation.uniqueId, keepDuration: representation.keepDuration).complete + if let pathData = path.data(using: .utf8) { + self.cacheStorageBox.update(id: pathData, size: Int64(data.count)) + } let _ = try? data.write(to: URL(fileURLWithPath: path)) } } @@ -885,6 +892,9 @@ public final class MediaBox { public func storeCachedResourceRepresentation(_ resource: MediaResource, representationId: String, keepDuration: CachedMediaRepresentationKeepDuration, data: Data, completion: @escaping (String) -> Void = { _ in }) { self.dataQueue.async { let path = self.cachedRepresentationPathsForId(resource.id.stringRepresentation, representationId: representationId, keepDuration: keepDuration).complete + if let pathData = path.data(using: .utf8) { + self.cacheStorageBox.update(id: pathData, size: Int64(data.count)) + } let _ = try? data.write(to: URL(fileURLWithPath: path)) completion(path) } @@ -894,6 +904,9 @@ public final class MediaBox { self.dataQueue.async { let path = self.cachedRepresentationPathsForId(resourceId, representationId: representationId, keepDuration: keepDuration).complete let _ = try? data.write(to: URL(fileURLWithPath: path)) + if let pathData = path.data(using: .utf8) { + self.cacheStorageBox.update(id: pathData, size: Int64(data.count)) + } completion(path) } } @@ -902,6 +915,9 @@ public final class MediaBox { self.dataQueue.async { let path = self.cachedRepresentationPathsForId(resourceId, representationId: representationId, keepDuration: keepDuration).complete let _ = try? FileManager.default.moveItem(atPath: tempFile.path, toPath: path) + if let fileSize = fileSize(path), fileSize != 0, let pathData = path.data(using: .utf8) { + self.cacheStorageBox.update(id: pathData, size: fileSize) + } completion(path) } } @@ -983,6 +999,7 @@ public final class MediaBox { if !context.initialized { context.initialized = true + let cacheStorageBox = self.cacheStorageBox let signal = self.wrappedFetchCachedResourceRepresentation.get() |> take(1) |> mapToSignal { fetch in @@ -999,15 +1016,24 @@ public final class MediaBox { switch next { case let .temporaryPath(temporaryPath): rename(temporaryPath, paths.complete) + if let size = fileSize(paths.complete), let pathData = paths.complete.data(using: .utf8) { + cacheStorageBox.update(id: pathData, size: size) + } isDone = true case let .tempFile(tempFile): rename(tempFile.path, paths.complete) TempBox.shared.dispose(tempFile) + if let size = fileSize(paths.complete), let pathData = paths.complete.data(using: .utf8) { + cacheStorageBox.update(id: pathData, size: size) + } isDone = true case .reset: let file = ManagedFile(queue: strongSelf.dataQueue, path: paths.partial, mode: .readwrite) file?.truncate(count: 0) unlink(paths.complete) + if let pathData = paths.complete.data(using: .utf8) { + cacheStorageBox.update(id: pathData, size: 0) + } case let .data(dataPart): let file = ManagedFile(queue: strongSelf.dataQueue, path: paths.partial, mode: .append) let dataCount = dataPart.count @@ -1015,8 +1041,14 @@ public final class MediaBox { let bytes = rawBytes.baseAddress!.assumingMemoryBound(to: UInt8.self) let _ = file?.write(bytes, count: dataCount) } + if let file = file, let size = file.getSize(), let pathData = paths.complete.data(using: .utf8) { + cacheStorageBox.update(id: pathData, size: size) + } case .done: link(paths.partial, paths.complete) + if let size = fileSize(paths.complete), let pathData = paths.complete.data(using: .utf8) { + cacheStorageBox.update(id: pathData, size: size) + } isDone = true } @@ -1155,6 +1187,7 @@ public final class MediaBox { if !context.initialized { context.initialized = true + let cacheStorageBox = self.cacheStorageBox let signal = fetch() |> deliverOn(self.dataQueue) context.disposable.set(signal.start(next: { [weak self, weak context] next in @@ -1165,15 +1198,24 @@ public final class MediaBox { switch next { case let .temporaryPath(temporaryPath): rename(temporaryPath, paths.complete) + if let size = fileSize(paths.complete), let pathData = paths.complete.data(using: .utf8) { + cacheStorageBox.update(id: pathData, size: size) + } isDone = true case let .tempFile(tempFile): rename(tempFile.path, paths.complete) TempBox.shared.dispose(tempFile) + if let size = fileSize(paths.complete), let pathData = paths.complete.data(using: .utf8) { + cacheStorageBox.update(id: pathData, size: size) + } isDone = true case .reset: let file = ManagedFile(queue: strongSelf.dataQueue, path: paths.partial, mode: .readwrite) file?.truncate(count: 0) unlink(paths.complete) + if let pathData = paths.complete.data(using: .utf8) { + cacheStorageBox.update(id: pathData, size: 0) + } case let .data(dataPart): let file = ManagedFile(queue: strongSelf.dataQueue, path: paths.partial, mode: .append) let dataCount = dataPart.count @@ -1181,9 +1223,15 @@ public final class MediaBox { let bytes = rawBytes.baseAddress!.assumingMemoryBound(to: UInt8.self) let _ = file?.write(bytes, count: dataCount) } + if let file = file, let size = file.getSize(), let pathData = paths.complete.data(using: .utf8) { + cacheStorageBox.update(id: pathData, size: size) + } case .done: link(paths.partial, paths.complete) isDone = true + if let size = fileSize(paths.complete), let pathData = paths.complete.data(using: .utf8) { + cacheStorageBox.update(id: pathData, size: size) + } } if let strongSelf = self, let currentContext = strongSelf.cachedRepresentationContexts[key], currentContext === context { @@ -1277,7 +1325,7 @@ public final class MediaBox { } } - public func updateResourceIndex(lowImpact: Bool, completion: @escaping () -> Void) -> Disposable { + private func updateGeneralResourceIndex(lowImpact: Bool, completion: @escaping () -> Void) -> Disposable { let basePath = self.basePath let storageBox = self.storageBox @@ -1370,6 +1418,104 @@ public final class MediaBox { } } + /*private func updateCacheResourceIndex(pathPrefix: String, lowImpact: Bool, completion: @escaping () -> Void) -> Disposable { + let cacheStorageBox = self.cacheStorageBox + + var isCancelled: Bool = false + + let processQueue = Queue(name: "UpdateResourceIndex", qos: .background) + processQueue.async { + if isCancelled { + return + } + + let scanContext = ScanFilesContext(path: pathPrefix) + + func processStale(nextId: Data?) { + let _ = (storageBox.enumerateItems(startingWith: nextId, limit: 1000) + |> deliverOn(processQueue)).start(next: { ids, realNextId in + var staleIds: [Data] = [] + + for id in ids { + if let name = String(data: id, encoding: .utf8) { + if self.resourceUsage(id: MediaResourceId(name)) == 0 { + staleIds.append(id) + } + } else { + staleIds.append(id) + } + } + + if !staleIds.isEmpty { + storageBox.remove(ids: staleIds) + } + + if realNextId == nil { + completion() + } else { + if lowImpact { + processQueue.after(0.4, { + processStale(nextId: realNextId) + }) + } else { + processStale(nextId: realNextId) + } + } + }) + } + + func processNext() { + processQueue.async { + if isCancelled { + return + } + + let results = scanContext.nextBatch(count: 32000) + if results.isEmpty { + processStale(nextId: nil) + return + } + + storageBox.addEmptyReferencesIfNotReferenced(ids: results.map { name -> (id: Data, size: Int64) in + let resourceId = MediaBox.idForFileName(name: name) + let paths = self.storePathsForId(MediaResourceId(resourceId)) + var size: Int64 = 0 + if let value = fileSize(paths.complete) { + size = value + } else if let value = fileSize(paths.partial) { + size = value + } + return (resourceId.data(using: .utf8)!, size) + }, contentType: MediaResourceUserContentType.other.rawValue, completion: { addedCount in + if addedCount != 0 { + postboxLog("UpdateResourceIndex: added \(addedCount) unreferenced ids") + } + + if lowImpact { + processQueue.after(0.4, { + processNext() + }) + } else { + processNext() + } + }) + } + } + + processNext() + } + + return ActionDisposable { + isCancelled = true + } + }*/ + + public func updateResourceIndex(lowImpact: Bool, completion: @escaping () -> Void) -> Disposable { + return self.updateGeneralResourceIndex(lowImpact: lowImpact, completion: { + completion() + }) + } + public func collectAllResourceUsage() -> Signal<[(id: String?, path: String, size: Int64)], NoError> { return Signal { subscriber in self.dataQueue.async { @@ -1396,66 +1542,6 @@ public final class MediaBox { } } - /*var cacheResult: Int64 = 0 - - var excludePrefixes = Set() - for id in excludeIds { - let cachedRepresentationPrefix = self.fileNameForId(id) - - excludePrefixes.insert(cachedRepresentationPrefix) - } - - if let enumerator = FileManager.default.enumerator(at: URL(fileURLWithPath: self.basePath + "/cache"), includingPropertiesForKeys: [.fileSizeKey], options: [.skipsHiddenFiles, .skipsSubdirectoryDescendants], errorHandler: nil) { - loop: for url in enumerator { - if let url = url as? URL { - if let prefix = url.lastPathComponent.components(separatedBy: ":").first, excludePrefixes.contains(prefix) { - continue loop - } - - if let value = (try? url.resourceValues(forKeys: Set([.fileSizeKey])))?.fileSize, value != 0 { - paths.append("cache/" + url.lastPathComponent) - cacheResult += Int64(value) - } - } - } - } - - func processRecursive(directoryPath: String, subdirectoryPath: String) { - if let enumerator = FileManager.default.enumerator(at: URL(fileURLWithPath: directoryPath), includingPropertiesForKeys: [.fileSizeKey, .isDirectoryKey], options: [.skipsHiddenFiles, .skipsSubdirectoryDescendants], errorHandler: nil) { - loop: for url in enumerator { - if let url = url as? URL { - if let prefix = url.lastPathComponent.components(separatedBy: ":").first, excludePrefixes.contains(prefix) { - continue loop - } - - if let isDirectory = (try? url.resourceValues(forKeys: Set([.isDirectoryKey])))?.isDirectory, isDirectory { - processRecursive(directoryPath: url.path, subdirectoryPath: subdirectoryPath + "/\(url.lastPathComponent)") - } else if let value = (try? url.resourceValues(forKeys: Set([.fileSizeKey])))?.fileSize, value != 0 { - paths.append("\(subdirectoryPath)/" + url.lastPathComponent) - cacheResult += Int64(value) - } - } - } - } - } - - processRecursive(directoryPath: self.basePath + "/animation-cache", subdirectoryPath: "animation-cache") - - if let enumerator = FileManager.default.enumerator(at: URL(fileURLWithPath: self.basePath + "/short-cache"), includingPropertiesForKeys: [.fileSizeKey], options: [.skipsHiddenFiles, .skipsSubdirectoryDescendants], errorHandler: nil) { - loop: for url in enumerator { - if let url = url as? URL { - if let prefix = url.lastPathComponent.components(separatedBy: ":").first, excludePrefixes.contains(prefix) { - continue loop - } - - if let value = (try? url.resourceValues(forKeys: Set([.fileSizeKey])))?.fileSize, value != 0 { - paths.append("short-cache/" + url.lastPathComponent) - cacheResult += Int64(value) - } - } - } - }*/ - subscriber.putNext(result) subscriber.putCompletion() } diff --git a/submodules/Postbox/Sources/StorageBox/StorageBox.swift b/submodules/Postbox/Sources/StorageBox/StorageBox.swift index 930b8933a2..870dd8b58b 100644 --- a/submodules/Postbox/Sources/StorageBox/StorageBox.swift +++ b/submodules/Postbox/Sources/StorageBox/StorageBox.swift @@ -472,6 +472,8 @@ public final class StorageBox { for peerId in self.peerIdsReferencing(hashId: hashId) { self.internalAddSize(peerId: peerId, contentType: info.contentType, delta: sizeDelta) } + } else { + self.internalAdd(reference: StorageBox.Reference(peerId: 0, messageNamespace: 0, messageId: 0), to: id, contentType: 0, size: size) } self.valueBox.commit() diff --git a/submodules/TelegramCore/Sources/Account/AccountManager.swift b/submodules/TelegramCore/Sources/Account/AccountManager.swift index 153c54732b..9f1a69798c 100644 --- a/submodules/TelegramCore/Sources/Account/AccountManager.swift +++ b/submodules/TelegramCore/Sources/Account/AccountManager.swift @@ -449,10 +449,27 @@ private func cleanupAccount(networkArguments: NetworkInitializationArguments, ac break } + var cloudValue: [Data] = [] + if let list = NSUbiquitousKeyValueStore.default.object(forKey: "T_SLTokens") as? [String] { + cloudValue = list.compactMap { string -> Data? in + guard let stringData = string.data(using: .utf8) else { + return nil + } + return Data(base64Encoded: stringData) + } + } + for data in cloudValue { + if !tokens.contains(data) { + tokens.insert(data, at: 0) + } + } if tokens.count > 20 { tokens.removeLast(tokens.count - 20) } + NSUbiquitousKeyValueStore.default.set(tokens.map { $0.base64EncodedString() }, forKey: "T_SLTokens") + NSUbiquitousKeyValueStore.default.synchronize() + transaction.setStoredLoginTokens(tokens) }).start() account.shouldBeServiceTaskMaster.set(.single(.never)) diff --git a/submodules/TelegramCore/Sources/Authorization.swift b/submodules/TelegramCore/Sources/Authorization.swift index 6253c8107e..6a17f2e8dd 100644 --- a/submodules/TelegramCore/Sources/Authorization.swift +++ b/submodules/TelegramCore/Sources/Authorization.swift @@ -72,11 +72,27 @@ private func ~=(pattern: Regex, matchable: T) -> } public func sendAuthorizationCode(accountManager: AccountManager, account: UnauthorizedAccount, phoneNumber: String, apiId: Int32, apiHash: String, syncContacts: Bool) -> Signal { + var cloudValue: [Data] = [] + if let list = NSUbiquitousKeyValueStore.default.object(forKey: "T_SLTokens") as? [String] { + cloudValue = list.compactMap { string -> Data? in + guard let stringData = string.data(using: .utf8) else { + return nil + } + return Data(base64Encoded: stringData) + } + } return accountManager.transaction { transaction -> [Data] in return transaction.getStoredLoginTokens() } |> castError(AuthorizationCodeRequestError.self) - |> mapToSignal { authTokens -> Signal in + |> mapToSignal { localAuthTokens -> Signal in + var authTokens = localAuthTokens + for data in cloudValue { + if !authTokens.contains(data) { + authTokens.insert(data, at: 0) + } + } + var flags: Int32 = 0 flags |= 1 << 5 //allowMissedCall flags |= 1 << 6 //tokens diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Resources/CollectCacheUsageStats.swift b/submodules/TelegramCore/Sources/TelegramEngine/Resources/CollectCacheUsageStats.swift index 5f4b36260d..362bd1fc28 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Resources/CollectCacheUsageStats.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Resources/CollectCacheUsageStats.swift @@ -123,10 +123,19 @@ private extension StorageUsageStats { mappedCategory = .avatars case MediaResourceUserContentType.sticker.rawValue: mappedCategory = .stickers + case MediaResourceUserContentType.other.rawValue: + mappedCategory = .misc + case MediaResourceUserContentType.audioVideoMessage.rawValue: + mappedCategory = .misc default: mappedCategory = .misc } - mappedCategories[mappedCategory] = StorageUsageStats.CategoryData(size: value.size, messages: value.messages) + if mappedCategories[mappedCategory] == nil { + mappedCategories[mappedCategory] = StorageUsageStats.CategoryData(size: value.size, messages: value.messages) + } else { + mappedCategories[mappedCategory]?.size += value.size + mappedCategories[mappedCategory]?.messages.merge(value.messages, uniquingKeysWith: { lhs, _ in lhs}) + } } self.init(categories: mappedCategories) @@ -134,7 +143,7 @@ private extension StorageUsageStats { } func _internal_collectStorageUsageStats(account: Account) -> Signal { - let additionalStats = Signal { subscriber in + /*let additionalStats = Signal { subscriber in DispatchQueue.global().async { var totalSize: Int64 = 0 @@ -207,7 +216,9 @@ func _internal_collectStorageUsageStats(account: Account) -> Signal take(1) return combineLatest( additionalStats, @@ -264,6 +275,7 @@ func _internal_collectStorageUsageStats(account: Account) -> Signal Signal<[EngineMessage.Id: Message], NoError> { return account.postbox.transaction { transaction -> [EngineMessage.Id: Message] in var result: [EngineMessage.Id: Message] = [:] + var peerInChatList: [EnginePeer.Id: Bool] = [:] for (category, value) in stats.categories { if !categories.contains(category) { continue @@ -273,8 +285,23 @@ func _internal_renderStorageUsageStatsMessages(account: Account, stats: StorageU if result[id] == nil { if let message = existingMessages[id] { result[id] = message - } else if let message = transaction.getMessage(id) { - result[id] = message + } else { + var matches = false + if let peerInChatListValue = peerInChatList[id.peerId] { + if peerInChatListValue { + matches = true + } + } else { + let peerInChatListValue = transaction.getPeerChatListIndex(id.peerId) != nil + peerInChatList[id.peerId] = peerInChatListValue + if peerInChatListValue { + matches = true + } + } + + if matches, let message = transaction.getMessage(id) { + result[id] = message + } } } } @@ -327,6 +354,9 @@ func _internal_clearStorage(account: Account, peerId: EnginePeer.Id?, categories case .misc: mappedContentTypes.append(MediaResourceUserContentType.other.rawValue) mappedContentTypes.append(MediaResourceUserContentType.audioVideoMessage.rawValue) + + // Legacy value for Gif + mappedContentTypes.append(5) } } @@ -357,6 +387,8 @@ func _internal_clearStorage(account: Account, peerId: EnginePeer.Id?, categories } } + mediaBox.cacheStorageBox.reset() + subscriber.putCompletion() } else { subscriber.putCompletion() diff --git a/submodules/TelegramNotices/Sources/Notices.swift b/submodules/TelegramNotices/Sources/Notices.swift index 7ccc7097a8..17cb4cb159 100644 --- a/submodules/TelegramNotices/Sources/Notices.swift +++ b/submodules/TelegramNotices/Sources/Notices.swift @@ -165,6 +165,7 @@ private enum ApplicationSpecificGlobalNotice: Int32 { case forcedPasswordSetup = 31 case emojiTooltip = 32 case audioTranscriptionSuggestion = 33 + case clearStorageDismissedTipSize = 34 var key: ValueBoxKey { let v = ValueBoxKey(length: 4) @@ -349,6 +350,10 @@ private struct ApplicationSpecificNoticeKeys { static func audioTranscriptionSuggestion() -> NoticeEntryKey { return NoticeEntryKey(namespace: noticeNamespace(namespace: globalNamespace), key: ApplicationSpecificGlobalNotice.audioTranscriptionSuggestion.key) } + + static func clearStorageDismissedTipSize() -> NoticeEntryKey { + return NoticeEntryKey(namespace: noticeNamespace(namespace: globalNamespace), key: ApplicationSpecificGlobalNotice.clearStorageDismissedTipSize.key) + } } public struct ApplicationSpecificNotice { @@ -1087,6 +1092,25 @@ public struct ApplicationSpecificNotice { } } + public static func getClearStorageDismissedTipSize(accountManager: AccountManager) -> Signal { + return accountManager.transaction { transaction -> Int32 in + if let value = transaction.getNotice(ApplicationSpecificNoticeKeys.clearStorageDismissedTipSize())?.get(ApplicationSpecificCounterNotice.self) { + return value.value + } else { + return 0 + } + } + } + + public static func setClearStorageDismissedTipSize(accountManager: AccountManager, value: Int32) -> Signal { + return accountManager.transaction { transaction -> Void in + if let entry = CodableEntry(ApplicationSpecificCounterNotice(value: value)) { + transaction.setNotice(ApplicationSpecificNoticeKeys.clearStorageDismissedTipSize(), entry) + } + } + |> ignoreValues + } + public static func getInteractiveEmojiSyncTip(accountManager: AccountManager) -> Signal<(Int32, Int32), NoError> { return accountManager.transaction { transaction -> (Int32, Int32) in if let value = transaction.getNotice(ApplicationSpecificNoticeKeys.interactiveEmojiSyncTip())?.get(ApplicationSpecificTimestampAndCounterNotice.self) { diff --git a/submodules/TelegramUI/Components/AnimationCache/Sources/AnimationCache.swift b/submodules/TelegramUI/Components/AnimationCache/Sources/AnimationCache.swift index fe7e08a766..2d15c25129 100644 --- a/submodules/TelegramUI/Components/AnimationCache/Sources/AnimationCache.swift +++ b/submodules/TelegramUI/Components/AnimationCache/Sources/AnimationCache.swift @@ -14,6 +14,26 @@ private func alignUp(size: Int, align: Int) -> Int { return (size + alignmentMask) & ~alignmentMask } +private func fileSize(_ path: String, useTotalFileAllocatedSize: Bool = false) -> Int64? { + if useTotalFileAllocatedSize { + let url = URL(fileURLWithPath: path) + if let values = (try? url.resourceValues(forKeys: Set([.isRegularFileKey, .totalFileAllocatedSizeKey]))) { + if values.isRegularFile ?? false { + if let fileSize = values.totalFileAllocatedSize { + return Int64(fileSize) + } + } + } + } + + var value = stat() + if stat(path, &value) == 0 { + return value.st_size + } else { + return nil + } +} + public final class AnimationCacheItemFrame { public enum RequestedFormat { case rgba @@ -1246,7 +1266,7 @@ private func loadItem(path: String) throws -> AnimationCacheItem { }) } -private func adaptItemFromHigherResolution(currentQueue: Queue, itemPath: String, width: Int, height: Int, itemDirectoryPath: String, higherResolutionPath: String, allocateTempFile: @escaping () -> String) -> AnimationCacheItem? { +private func adaptItemFromHigherResolution(currentQueue: Queue, itemPath: String, width: Int, height: Int, itemDirectoryPath: String, higherResolutionPath: String, allocateTempFile: @escaping () -> String, updateStorageStats: @escaping (String, Int64) -> Void) -> AnimationCacheItem? { guard let higherResolutionItem = try? loadItem(path: higherResolutionPath) else { return nil } @@ -1286,6 +1306,10 @@ private func adaptItemFromHigherResolution(currentQueue: Queue, itemPath: String guard let _ = try? FileManager.default.moveItem(atPath: result.animationPath, toPath: itemPath) else { return nil } + if let size = fileSize(itemPath) { + updateStorageStats(itemPath, size) + } + guard let item = try? loadItem(path: itemPath) else { return nil } @@ -1295,7 +1319,7 @@ private func adaptItemFromHigherResolution(currentQueue: Queue, itemPath: String } } -private func generateFirstFrameFromItem(currentQueue: Queue, itemPath: String, animationItemPath: String, allocateTempFile: @escaping () -> String) -> Bool { +private func generateFirstFrameFromItem(currentQueue: Queue, itemPath: String, animationItemPath: String, allocateTempFile: @escaping () -> String, updateStorageStats: @escaping (String, Int64) -> Void) -> Bool { guard let animationItem = try? loadItem(path: animationItemPath) else { return false } @@ -1337,6 +1361,9 @@ private func generateFirstFrameFromItem(currentQueue: Queue, itemPath: String, a guard let _ = try? FileManager.default.moveItem(atPath: result.animationPath, toPath: itemPath) else { return false } + if let size = fileSize(itemPath) { + updateStorageStats(itemPath, size) + } return true } catch { return false @@ -1408,13 +1435,14 @@ public final class AnimationCacheImpl: AnimationCache { private let queue: Queue private let basePath: String private let allocateTempFile: () -> String + private let updateStorageStats: (String, Int64) -> Void private let fetchQueues: [Queue] private var nextFetchQueueIndex: Int = 0 private var itemContexts: [ItemKey: ItemContext] = [:] - init(queue: Queue, basePath: String, allocateTempFile: @escaping () -> String) { + init(queue: Queue, basePath: String, allocateTempFile: @escaping () -> String, updateStorageStats: @escaping (String, Int64) -> Void) { self.queue = queue let fetchQueueCount: Int @@ -1427,6 +1455,7 @@ public final class AnimationCacheImpl: AnimationCache { self.fetchQueues = (0 ..< fetchQueueCount).map { i in Queue(name: "AnimationCacheImpl-Fetch\(i)", qos: .default) } self.basePath = basePath self.allocateTempFile = allocateTempFile + self.updateStorageStats = updateStorageStats } deinit { @@ -1464,6 +1493,7 @@ public final class AnimationCacheImpl: AnimationCache { let fetchQueueIndex = self.nextFetchQueueIndex self.nextFetchQueueIndex += 1 let allocateTempFile = self.allocateTempFile + let updateStorageStats = self.updateStorageStats guard let writer = AnimationCacheItemWriterImpl(queue: self.fetchQueues[fetchQueueIndex % self.fetchQueues.count], allocateTempFile: self.allocateTempFile, completion: { [weak self, weak itemContext] result in queue.async { guard let strongSelf = self, let itemContext = itemContext, itemContext === strongSelf.itemContexts[key] else { @@ -1482,8 +1512,11 @@ public final class AnimationCacheImpl: AnimationCache { guard let _ = try? FileManager.default.moveItem(atPath: result.animationPath, toPath: itemPath) else { return } + if let size = fileSize(itemPath) { + updateStorageStats(itemPath, size) + } - let _ = generateFirstFrameFromItem(currentQueue: queue, itemPath: itemFirstFramePath, animationItemPath: itemPath, allocateTempFile: allocateTempFile) + let _ = generateFirstFrameFromItem(currentQueue: queue, itemPath: itemFirstFramePath, animationItemPath: itemPath, allocateTempFile: allocateTempFile, updateStorageStats: updateStorageStats) for f in itemContext.subscribers.copyItems() { guard let item = try? loadItem(path: itemPath) else { @@ -1522,7 +1555,7 @@ public final class AnimationCacheImpl: AnimationCache { } } - static func getFirstFrameSynchronously(basePath: String, sourceId: String, size: CGSize, allocateTempFile: @escaping () -> String) -> AnimationCacheItem? { + static func getFirstFrameSynchronously(basePath: String, sourceId: String, size: CGSize, allocateTempFile: @escaping () -> String, updateStorageStats: @escaping (String, Int64) -> Void) -> AnimationCacheItem? { let hashString = md5Hash(sourceId) let sourceIdPath = itemSubpath(hashString: hashString, width: Int(size.width), height: Int(size.height)) let itemDirectoryPath = "\(basePath)/\(sourceIdPath.directory)" @@ -1535,7 +1568,7 @@ public final class AnimationCacheImpl: AnimationCache { } if let adaptationItemPath = findHigherResolutionFileForAdaptation(itemDirectoryPath: itemDirectoryPath, baseName: "\(hashString)_", baseSuffix: "-f", width: Int(size.width), height: Int(size.height)) { - if let adaptedItem = adaptItemFromHigherResolution(currentQueue: .mainQueue(), itemPath: itemFirstFramePath, width: Int(size.width), height: Int(size.height), itemDirectoryPath: itemDirectoryPath, higherResolutionPath: adaptationItemPath, allocateTempFile: allocateTempFile) { + if let adaptedItem = adaptItemFromHigherResolution(currentQueue: .mainQueue(), itemPath: itemFirstFramePath, width: Int(size.width), height: Int(size.height), itemDirectoryPath: itemDirectoryPath, higherResolutionPath: adaptationItemPath, allocateTempFile: allocateTempFile, updateStorageStats: updateStorageStats) { return adaptedItem } } @@ -1543,7 +1576,7 @@ public final class AnimationCacheImpl: AnimationCache { return nil } - static func getFirstFrame(queue: Queue, basePath: String, sourceId: String, size: CGSize, allocateTempFile: @escaping () -> String, fetch: ((AnimationCacheFetchOptions) -> Disposable)?, completion: @escaping (AnimationCacheItemResult) -> Void) -> Disposable { + static func getFirstFrame(queue: Queue, basePath: String, sourceId: String, size: CGSize, allocateTempFile: @escaping () -> String, updateStorageStats: @escaping (String, Int64) -> Void, fetch: ((AnimationCacheFetchOptions) -> Disposable)?, completion: @escaping (AnimationCacheItemResult) -> Void) -> Disposable { let hashString = md5Hash(sourceId) let sourceIdPath = itemSubpath(hashString: hashString, width: Int(size.width), height: Int(size.height)) let itemDirectoryPath = "\(basePath)/\(sourceIdPath.directory)" @@ -1555,7 +1588,7 @@ public final class AnimationCacheImpl: AnimationCache { } if let adaptationItemPath = findHigherResolutionFileForAdaptation(itemDirectoryPath: itemDirectoryPath, baseName: "\(hashString)_", baseSuffix: "-f", width: Int(size.width), height: Int(size.height)) { - if let adaptedItem = adaptItemFromHigherResolution(currentQueue: .mainQueue(), itemPath: itemFirstFramePath, width: Int(size.width), height: Int(size.height), itemDirectoryPath: itemDirectoryPath, higherResolutionPath: adaptationItemPath, allocateTempFile: allocateTempFile) { + if let adaptedItem = adaptItemFromHigherResolution(currentQueue: .mainQueue(), itemPath: itemFirstFramePath, width: Int(size.width), height: Int(size.height), itemDirectoryPath: itemDirectoryPath, higherResolutionPath: adaptationItemPath, allocateTempFile: allocateTempFile, updateStorageStats: updateStorageStats) { completion(AnimationCacheItemResult(item: adaptedItem, isFinal: true)) return EmptyDisposable } @@ -1579,6 +1612,9 @@ public final class AnimationCacheImpl: AnimationCache { completion(AnimationCacheItemResult(item: nil, isFinal: true)) return } + if let size = fileSize(itemFirstFramePath) { + updateStorageStats(itemFirstFramePath, size) + } guard let item = try? loadItem(path: itemFirstFramePath) else { completion(AnimationCacheItemResult(item: nil, isFinal: true)) return @@ -1604,14 +1640,16 @@ public final class AnimationCacheImpl: AnimationCache { private let basePath: String private let impl: QueueLocalObject private let allocateTempFile: () -> String + private let updateStorageStats: (String, Int64) -> Void - public init(basePath: String, allocateTempFile: @escaping () -> String) { + public init(basePath: String, allocateTempFile: @escaping () -> String, updateStorageStats: @escaping (String, Int64) -> Void) { let queue = Queue() self.queue = queue self.basePath = basePath self.allocateTempFile = allocateTempFile + self.updateStorageStats = updateStorageStats self.impl = QueueLocalObject(queue: queue, generate: { - return Impl(queue: queue, basePath: basePath, allocateTempFile: allocateTempFile) + return Impl(queue: queue, basePath: basePath, allocateTempFile: allocateTempFile, updateStorageStats: updateStorageStats) }) } @@ -1634,7 +1672,7 @@ public final class AnimationCacheImpl: AnimationCache { } public func getFirstFrameSynchronously(sourceId: String, size: CGSize) -> AnimationCacheItem? { - return Impl.getFirstFrameSynchronously(basePath: self.basePath, sourceId: sourceId, size: size, allocateTempFile: self.allocateTempFile) + return Impl.getFirstFrameSynchronously(basePath: self.basePath, sourceId: sourceId, size: size, allocateTempFile: self.allocateTempFile, updateStorageStats: self.updateStorageStats) } public func getFirstFrame(queue: Queue, sourceId: String, size: CGSize, fetch: ((AnimationCacheFetchOptions) -> Disposable)?, completion: @escaping (AnimationCacheItemResult) -> Void) -> Disposable { @@ -1642,8 +1680,9 @@ public final class AnimationCacheImpl: AnimationCache { let basePath = self.basePath let allocateTempFile = self.allocateTempFile + let updateStorageStats = self.updateStorageStats queue.async { - disposable.set(Impl.getFirstFrame(queue: queue, basePath: basePath, sourceId: sourceId, size: size, allocateTempFile: allocateTempFile, fetch: fetch, completion: completion)) + disposable.set(Impl.getFirstFrame(queue: queue, basePath: basePath, sourceId: sourceId, size: size, allocateTempFile: allocateTempFile, updateStorageStats: updateStorageStats, fetch: fetch, completion: completion)) } return disposable diff --git a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/PieChartComponent.swift b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/PieChartComponent.swift index ad48e6b6da..d25e2a8a85 100644 --- a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/PieChartComponent.swift +++ b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/PieChartComponent.swift @@ -332,15 +332,18 @@ final class PieChartComponent: Component { private struct CalculatedLayout { var size: CGSize var sections: [CalculatedSection] + var isEmpty: Bool init(size: CGSize, sections: [CalculatedSection]) { self.size = size self.sections = sections + self.isEmpty = sections.isEmpty } init(interpolating start: CalculatedLayout, to end: CalculatedLayout, progress: CGFloat, size: CGSize) { self.size = size self.sections = [] + self.isEmpty = end.isEmpty for i in 0 ..< end.sections.count { let right = end.sections[i] @@ -370,16 +373,17 @@ final class PieChartComponent: Component { } } - init(size: CGSize, items: [ChartData.Item], selectedKey: AnyHashable?) { + init(size: CGSize, items: [ChartData.Item], selectedKey: AnyHashable?, isEmpty: Bool) { self.size = size self.sections = [] + self.isEmpty = isEmpty if items.isEmpty { return } - let innerDiameter: CGFloat = 100.0 - let spacing: CGFloat = 2.0 + let innerDiameter: CGFloat = isEmpty ? 90.0 : 100.0 + let spacing: CGFloat = isEmpty ? -0.5 : 2.0 let innerAngleSpacing: CGFloat = spacing / (innerDiameter * 0.5) var angles: [Double] = [] @@ -389,8 +393,8 @@ final class PieChartComponent: Component { angles.append(angle) } - let diameter: CGFloat = 200.0 - let reducedDiameter: CGFloat = 170.0 + let diameter: CGFloat = isEmpty ? (innerDiameter + 6.0 * 2.0) : 200.0 + let reducedDiameter: CGFloat = floor(0.85 * diameter) var anglesData: [ItemAngleData] = [] @@ -413,30 +417,8 @@ final class PieChartComponent: Component { let angleValue: CGFloat = angles[i] - var beforeSpacingFraction: CGFloat = 1.0 - var afterSpacingFraction: CGFloat = 1.0 - if item.mergeable { - let previousItem: ChartData.Item - if i == 0 { - previousItem = items[items.count - 1] - } else { - previousItem = items[i - 1] - } - - let nextItem: ChartData.Item - if i == items.count - 1 { - nextItem = items[0] - } else { - nextItem = items[i + 1] - } - - if previousItem.mergeable { - beforeSpacingFraction = item.mergeFactor * 1.0 + (1.0 - item.mergeFactor) * (-0.2) - } - if nextItem.mergeable { - afterSpacingFraction = item.mergeFactor * 1.0 + (1.0 - item.mergeFactor) * (-0.2) - } - } + let beforeSpacingFraction: CGFloat = 1.0 + let afterSpacingFraction: CGFloat = 1.0 let innerStartAngle = startAngle + innerAngleSpacing * 0.5 let arcInnerStartAngle = startAngle + innerAngleSpacing * 0.5 * beforeSpacingFraction @@ -453,9 +435,11 @@ final class PieChartComponent: Component { var arcOuterEndAngle = startAngle + angleValue - angleSpacing * 0.5 * afterSpacingFraction arcOuterEndAngle = max(arcOuterEndAngle, arcOuterStartAngle) + let itemColor: UIColor = isEmpty ? UIColor(rgb: 0x34C759) : item.color + self.sections.append(CalculatedSection( id: item.id, - color: item.color, + color: itemColor, innerAngle: arcInnerStartAngle ..< arcInnerEndAngle, outerAngle: arcOuterStartAngle ..< arcOuterEndAngle, innerRadius: innerDiameter * 0.5, @@ -705,10 +689,15 @@ final class PieChartComponent: Component { } private final class ParticleSet { + private let innerRadius: CGFloat + private let maxRadius: CGFloat private(set) var particles: [Particle] = [] - init() { - self.generateParticles(preAdvance: true) + init(innerRadius: CGFloat, maxRadius: CGFloat, preAdvance: Bool) { + self.innerRadius = innerRadius + self.maxRadius = maxRadius + + self.generateParticles(preAdvance: preAdvance) } private func generateParticles(preAdvance: Bool) { @@ -768,12 +757,13 @@ final class PieChartComponent: Component { func update(deltaTime: CGFloat) { let size = CGSize(width: 200.0, height: 200.0) - let radius2 = pow(size.width * 0.5 + 10.0, 2.0) + let radius = size.width * 0.5 + 10.0 for i in (0 ..< self.particles.count).reversed() { self.particles[i].update(deltaTime: deltaTime) let position = self.particles[i].position - if pow(position.x - size.width * 0.5, 2.0) + pow(position.y - size.height * 0.5, 2.0) > radius2 { + let distance = sqrt(pow(position.x - size.width * 0.5, 2.0) + pow(position.y - size.height * 0.5, 2.0)) + if distance > radius { self.particles.remove(at: i) } } @@ -901,7 +891,7 @@ final class PieChartComponent: Component { } } - func updateParticles(particleSet: ParticleSet) { + func updateParticles(particleSet: ParticleSet, alpha: CGFloat) { guard let particleImage = self.particleImage else { return } @@ -922,7 +912,93 @@ final class PieChartComponent: Component { particleLayer.position = particle.position particleLayer.transform = CATransform3DMakeScale(particle.scale, particle.scale, 1.0) - particleLayer.opacity = Float(particle.alpha) + particleLayer.opacity = Float(particle.alpha * alpha) + } + if particleSet.particles.count < self.particleLayers.count { + for i in particleSet.particles.count ..< self.particleLayers.count { + self.particleLayers[i].isHidden = true + } + } + } + } + + private final class DoneLayer: SimpleLayer { + private let maskShapeLayer: CAShapeLayer + private var particleImage: UIImage? + private var particleSet: ParticleSet? + private var particleLayers: [SimpleLayer] = [] + + override init() { + self.maskShapeLayer = CAShapeLayer() + self.maskShapeLayer.fillColor = UIColor.black.cgColor + self.maskShapeLayer.fillRule = .evenOdd + + super.init() + + self.particleImage = UIImage(bundleImageName: "Settings/Storage/ParticleStar")?.precomposed() + + let path = CGMutablePath() + + path.addRect(CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: 200.0, height: 200.0))) + path.addEllipse(in: CGRect(origin: CGPoint(x: floor((200.0 - 102.0) * 0.5), y: floor((200.0 - 102.0) * 0.5)), size: CGSize(width: 102.0, height: 102.0))) + + self.maskShapeLayer.path = path + self.mask = self.maskShapeLayer + + self.particleSet = ParticleSet(innerRadius: 45.0, maxRadius: 100.0, preAdvance: true) + } + + override init(layer: Any) { + self.maskShapeLayer = CAShapeLayer() + + super.init(layer: layer) + } + + required init(coder: NSCoder) { + preconditionFailure() + } + + func updateParticles(deltaTime: CGFloat) { + guard let particleSet = self.particleSet else { + return + } + particleSet.update(deltaTime: deltaTime) + + let size = CGSize(width: 200.0, height: 200.0) + + guard let particleImage = self.particleImage else { + return + } + for i in 0 ..< particleSet.particles.count { + let particle = particleSet.particles[i] + + let particleLayer: SimpleLayer + if i < self.particleLayers.count { + particleLayer = self.particleLayers[i] + particleLayer.isHidden = false + } else { + particleLayer = SimpleLayer() + particleLayer.contents = particleImage.cgImage + particleLayer.bounds = CGRect(origin: CGPoint(), size: particleImage.size) + self.particleLayers.append(particleLayer) + self.addSublayer(particleLayer) + + particleLayer.layerTintColor = UIColor(rgb: 0x34C759).cgColor + } + + particleLayer.position = particle.position + particleLayer.transform = CATransform3DMakeScale(particle.scale * 1.2, particle.scale * 1.2, 1.0) + + let distance = sqrt(pow(particle.position.x - size.width * 0.5, 2.0) + pow(particle.position.y - size.height * 0.5, 2.0)) + var mulAlpha: CGFloat = 1.0 + let outerDistanceNorm: CGFloat = 20.0 + if distance > 100.0 - outerDistanceNorm { + let outerDistanceFactor: CGFloat = (100.0 - distance) / outerDistanceNorm + let alphaFactor: CGFloat = max(0.0, min(1.0, outerDistanceFactor)) + mulAlpha = alphaFactor + } + + particleLayer.opacity = Float(particle.alpha * mulAlpha) } if particleSet.particles.count < self.particleLayers.count { for i in particleSet.particles.count ..< self.particleLayers.count { @@ -945,10 +1021,10 @@ final class PieChartComponent: Component { private var sectionLayers: [AnyHashable: SectionLayer] = [:] private let particleSet: ParticleSet - private var labels: [AnyHashable: ChartLabel] = [:] + private var doneLayer: DoneLayer? override init(frame: CGRect) { - self.particleSet = ParticleSet() + self.particleSet = ParticleSet(innerRadius: 50.0, maxRadius: 100.0, preAdvance: true) super.init(frame: frame) @@ -1008,6 +1084,7 @@ final class PieChartComponent: Component { if self.theme !== theme || self.data != data || self.selectedKey != selectedKey { self.theme = theme self.selectedKey = selectedKey + let previousData = self.data if animated, let previous = self.currentLayout { var initialState = previous @@ -1016,19 +1093,42 @@ final class PieChartComponent: Component { let mappedProgress = listViewAnimationCurveSystem(CGFloat(currentProgress)) initialState = CalculatedLayout(interpolating: currentAnimation.start, to: previous, progress: mappedProgress, size: previous.size) } - let targetLayout = CalculatedLayout( - size: CGSize(width: 200.0, height: 200.0), - items: data.items, - selectedKey: self.selectedKey - ) + + let targetLayout: CalculatedLayout + if let previousData = previousData, data.items.isEmpty { + targetLayout = CalculatedLayout( + size: CGSize(width: 200.0, height: 200.0), + items: previousData.items, + selectedKey: self.selectedKey, + isEmpty: true + ) + } else { + targetLayout = CalculatedLayout( + size: CGSize(width: 200.0, height: 200.0), + items: data.items, + selectedKey: self.selectedKey, + isEmpty: false + ) + } + self.currentLayout = targetLayout self.currentAnimation = (initialState, CACurrentMediaTime(), 0.4) } else { - self.currentLayout = CalculatedLayout( - size: CGSize(width: 200.0, height: 200.0), - items: data.items, - selectedKey: self.selectedKey - ) + if data.items.isEmpty { + self.currentLayout = CalculatedLayout( + size: CGSize(width: 200.0, height: 200.0), + items: [.init(id: .other, displayValue: 0.0, displaySize: 0, value: 1.0, color: .green, mergeable: false, mergeFactor: 1.0)], + selectedKey: self.selectedKey, + isEmpty: true + ) + } else { + self.currentLayout = CalculatedLayout( + size: CGSize(width: 200.0, height: 200.0), + items: data.items, + selectedKey: self.selectedKey, + isEmpty: data.items.isEmpty + ) + } } self.data = data @@ -1043,15 +1143,71 @@ final class PieChartComponent: Component { var validIds: [AnyHashable] = [] if let currentLayout = self.currentLayout { var effectiveLayout = currentLayout + var verticalOffset: CGFloat = 0.0 + var particleAlpha: CGFloat = 1.0 + var rotationAngle: CGFloat = 0.0 + let emptyRotationAngle: CGFloat = CGFloat.pi + let emptyVerticalOffset: CGFloat = (92.0 - 200.0) * 0.5 if let currentAnimation = self.currentAnimation { let currentProgress: Double = max(0.0, min(1.0, (CACurrentMediaTime() - currentAnimation.startTime) / currentAnimation.duration)) let mappedProgress = listViewAnimationCurveSystem(CGFloat(currentProgress)) effectiveLayout = CalculatedLayout(interpolating: currentAnimation.start, to: currentLayout, progress: mappedProgress, size: currentLayout.size) + let fromVerticalOffset: CGFloat + let fromRotationAngle: CGFloat + if currentAnimation.start.isEmpty { + fromVerticalOffset = emptyVerticalOffset + fromRotationAngle = emptyRotationAngle + } else { + fromVerticalOffset = 0.0 + fromRotationAngle = 0.0 + } + let toVerticalOffset: CGFloat + let toRotationAngle: CGFloat + if currentLayout.isEmpty { + toVerticalOffset = emptyVerticalOffset + toRotationAngle = emptyRotationAngle + } else { + toVerticalOffset = 0.0 + toRotationAngle = 0.0 + } + + verticalOffset = (1.0 - mappedProgress) * fromVerticalOffset + mappedProgress * toVerticalOffset + rotationAngle = (1.0 - mappedProgress) * fromRotationAngle + mappedProgress * toRotationAngle + + if currentLayout.isEmpty { + particleAlpha = 1.0 - mappedProgress + } + if currentProgress >= 1.0 - CGFloat.ulpOfOne { self.currentAnimation = nil } + } else { + if currentLayout.isEmpty { + verticalOffset = emptyVerticalOffset + particleAlpha = 0.0 + rotationAngle = emptyRotationAngle + } + } + + if currentLayout.isEmpty { + let doneLayer: DoneLayer + if let current = self.doneLayer { + doneLayer = current + } else { + doneLayer = DoneLayer() + self.doneLayer = doneLayer + self.layer.insertSublayer(doneLayer, at: 0) + } + doneLayer.updateParticles(deltaTime: deltaTime) + doneLayer.frame = CGRect(origin: CGPoint(x: 0.0, y: verticalOffset), size: CGSize(width: 200.0, height: 200.0)) + doneLayer.opacity = Float(1.0 - particleAlpha) + } else { + if let doneLayer = self.doneLayer { + self.doneLayer = nil + doneLayer.removeFromSuperlayer() + } } for section in effectiveLayout.sections { @@ -1066,9 +1222,12 @@ final class PieChartComponent: Component { self.layer.addSublayer(sectionLayer) } - sectionLayer.frame = CGRect(origin: CGPoint(), size: CGSize(width: 200.0, height: 200.0)) + let sectionLayerFrame = CGRect(origin: CGPoint(x: 0.0, y: verticalOffset), size: CGSize(width: 200.0, height: 200.0)) + sectionLayer.position = sectionLayerFrame.center + sectionLayer.bounds = CGRect(origin: CGPoint(), size: sectionLayerFrame.size) + sectionLayer.transform = CATransform3DMakeRotation(rotationAngle, 0.0, 0.0, 1.0) sectionLayer.update(size: sectionLayer.bounds.size, section: section) - sectionLayer.updateParticles(particleSet: self.particleSet) + sectionLayer.updateParticles(particleSet: self.particleSet, alpha: particleAlpha) } } diff --git a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StoragePeerListPanelComponent.swift b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StoragePeerListPanelComponent.swift index f7ccef89d7..b1f3e311bb 100644 --- a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StoragePeerListPanelComponent.swift +++ b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StoragePeerListPanelComponent.swift @@ -289,7 +289,11 @@ private final class PeerListItemComponent: Component { } else { clipStyle = .round } - self.avatarNode.setPeer(context: component.context, theme: component.theme, peer: peer, clipStyle: clipStyle, displayDimensions: CGSize(width: avatarSize, height: avatarSize)) + if peer.id == component.context.account.peerId { + self.avatarNode.setPeer(context: component.context, theme: component.theme, peer: peer, overrideImage: .savedMessagesIcon, clipStyle: clipStyle, displayDimensions: CGSize(width: avatarSize, height: avatarSize)) + } else { + self.avatarNode.setPeer(context: component.context, theme: component.theme, peer: peer, clipStyle: clipStyle, displayDimensions: CGSize(width: avatarSize, height: avatarSize)) + } } let labelSize = self.label.update( @@ -582,13 +586,20 @@ final class StoragePeerListPanelComponent: Component { itemSelectionState = .none } + let itemTitle: String + if item.peer.id == component.context.account.peerId { + itemTitle = environment.strings.DialogList_SavedMessages + } else { + itemTitle = item.peer.displayTitle(strings: environment.strings, displayOrder: .firstLast) + } + let _ = itemView.update( transition: itemTransition, component: AnyComponent(PeerListItemComponent( context: component.context, theme: environment.theme, sideInset: environment.containerInsets.left, - title: item.peer.displayTitle(strings: environment.strings, displayOrder: .firstLast), + title: itemTitle, peer: item.peer, label: dataSizeString(item.size, formatting: dataSizeFormatting), selectionState: itemSelectionState, diff --git a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsageScreen.swift b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsageScreen.swift index af8ffb7e3d..09144922bf 100644 --- a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsageScreen.swift +++ b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsageScreen.swift @@ -727,6 +727,8 @@ final class StorageUsageScreenComponent: Component { private var doneStatusCircle: SimpleShapeLayer? private var doneStatusNode: RadialStatusNode? + private let scrollContainerView: UIView + private let pieChartView = ComponentView() private let chartTotalLabel = ComponentView() private let categoriesView = ComponentView() @@ -777,6 +779,8 @@ final class StorageUsageScreenComponent: Component { self.navigationSeparatorLayerContainer = SimpleLayer() self.navigationSeparatorLayerContainer.opacity = 0.0 + self.scrollContainerView = UIView() + self.scrollView = ScrollViewImpl() self.keepDurationSectionContainerView = UIView() @@ -805,7 +809,9 @@ final class StorageUsageScreenComponent: Component { self.scrollView.clipsToBounds = true self.addSubview(self.scrollView) - self.scrollView.addSubview(self.keepDurationSectionContainerView) + self.scrollView.addSubview(self.scrollContainerView) + + self.scrollContainerView.addSubview(self.keepDurationSectionContainerView) self.scrollView.layer.addSublayer(self.headerProgressBackgroundLayer) self.scrollView.layer.addSublayer(self.headerProgressForegroundLayer) @@ -1070,9 +1076,10 @@ final class StorageUsageScreenComponent: Component { alphaTransition.setAlpha(view: self.scrollView, alpha: self.aggregatedData != nil ? 1.0 : 0.0) alphaTransition.setAlpha(view: self.headerOffsetContainer, alpha: self.aggregatedData != nil ? 1.0 : 0.0) } else if case .clearedItems = animationHint.value { - if let snapshotView = self.snapshotView(afterScreenUpdates: false) { - snapshotView.frame = self.bounds - self.addSubview(snapshotView) + if let snapshotView = self.scrollContainerView.snapshotView(afterScreenUpdates: false) { + snapshotView.frame = self.scrollContainerView.frame + self.scrollView.insertSubview(snapshotView, aboveSubview: self.scrollContainerView) + self.scrollContainerView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { [weak snapshotView] _ in snapshotView?.removeFromSuperview() }) @@ -1347,12 +1354,20 @@ final class StorageUsageScreenComponent: Component { chartItems.append(chartItem) } - chartItems.append(PieChartComponent.ChartData.Item(id: .other, displayValue: otherRealSum, displaySize: totalOtherSize, value: self.isOtherCategoryExpanded ? 0.0 : otherSum, color: Category.misc.color, mergeable: false, mergeFactor: 1.0)) + if !listCategories.isEmpty { + chartItems.append(PieChartComponent.ChartData.Item(id: .other, displayValue: otherRealSum, displaySize: totalOtherSize, value: self.isOtherCategoryExpanded ? 0.0 : otherSum, color: Category.misc.color, mergeable: false, mergeFactor: 1.0)) + } let chartData = PieChartComponent.ChartData(items: chartItems) self.pieChartView.parentState = state + + var pieChartTransition = transition + if transition.animation.isImmediate, let animationHint, case .clearedItems = animationHint.value { + pieChartTransition = Transition(animation: .curve(duration: 0.4, curve: .spring)) + } + let pieChartSize = self.pieChartView.update( - transition: transition, + transition: pieChartTransition, component: AnyComponent(PieChartComponent( theme: environment.theme, strings: environment.strings, @@ -1367,8 +1382,8 @@ final class StorageUsageScreenComponent: Component { self.scrollView.addSubview(pieChartComponentView) } - transition.setFrame(view: pieChartComponentView, frame: pieChartFrame) - transition.setAlpha(view: pieChartComponentView, alpha: listCategories.isEmpty ? 0.0 : 1.0) + pieChartTransition.setFrame(view: pieChartComponentView, frame: pieChartFrame) + //transition.setAlpha(view: pieChartComponentView, alpha: listCategories.isEmpty ? 0.0 : 1.0) } if let _ = self.aggregatedData, listCategories.isEmpty { let checkColor = UIColor(rgb: 0x34C759) @@ -1392,7 +1407,7 @@ final class StorageUsageScreenComponent: Component { } else { doneStatusCircle = SimpleShapeLayer() self.doneStatusCircle = doneStatusCircle - self.scrollView.layer.addSublayer(doneStatusCircle) + //self.scrollView.layer.addSublayer(doneStatusCircle) doneStatusCircle.opacity = 0.0 } @@ -1431,7 +1446,11 @@ final class StorageUsageScreenComponent: Component { if listCategories.isEmpty { headerText = environment.strings.StorageManagement_TitleCleared } else if let peer = component.peer { - headerText = peer.displayTitle(strings: environment.strings, displayOrder: .firstLast) + if peer.id == component.context.account.peerId { + headerText = environment.strings.DialogList_SavedMessages + } else { + headerText = peer.displayTitle(strings: environment.strings, displayOrder: .firstLast) + } } else { headerText = environment.strings.StorageManagement_Title } @@ -1527,7 +1546,7 @@ final class StorageUsageScreenComponent: Component { let headerDescriptionFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - headerDescriptionSize.width) / 2.0), y: contentHeight), size: headerDescriptionSize) if let headerDescriptionComponentView = self.headerDescriptionView.view { if headerDescriptionComponentView.superview == nil { - self.scrollView.addSubview(headerDescriptionComponentView) + self.scrollContainerView.addSubview(headerDescriptionComponentView) } transition.setFrame(view: headerDescriptionComponentView, frame: headerDescriptionFrame) } @@ -1562,14 +1581,14 @@ final class StorageUsageScreenComponent: Component { } else { chartAvatarNode = AvatarNode(font: avatarPlaceholderFont(size: 17.0)) self.chartAvatarNode = chartAvatarNode - if let pieChartComponentView = self.pieChartView.view { - self.scrollView.insertSubview(chartAvatarNode.view, belowSubview: pieChartComponentView) - } else { - self.scrollView.addSubview(chartAvatarNode.view) - } + self.scrollContainerView.addSubview(chartAvatarNode.view) chartAvatarNode.frame = avatarFrame - chartAvatarNode.setPeer(context: component.context, theme: environment.theme, peer: peer, displayDimensions: avatarSize) + if peer.id == component.context.account.peerId { + chartAvatarNode.setPeer(context: component.context, theme: environment.theme, peer: peer, overrideImage: .savedMessagesIcon, displayDimensions: avatarSize) + } else { + chartAvatarNode.setPeer(context: component.context, theme: environment.theme, peer: peer, displayDimensions: avatarSize) + } } transition.setAlpha(view: chartAvatarNode.view, alpha: listCategories.isEmpty ? 0.0 : 1.0) } else { @@ -1606,11 +1625,7 @@ final class StorageUsageScreenComponent: Component { ) if let chartTotalLabelView = self.chartTotalLabel.view { if chartTotalLabelView.superview == nil { - if let pieChartComponentView = self.pieChartView.view { - self.scrollView.insertSubview(chartTotalLabelView, belowSubview: pieChartComponentView) - } else { - self.scrollView.addSubview(chartTotalLabelView) - } + self.scrollContainerView.addSubview(chartTotalLabelView) } let totalLabelFrame = CGRect(origin: CGPoint(x: pieChartFrame.minX + floor((pieChartFrame.width - chartTotalLabelSize.width) / 2.0), y: pieChartFrame.minY + floor((pieChartFrame.height - chartTotalLabelSize.height) / 2.0)), size: chartTotalLabelSize) transition.setFrame(view: chartTotalLabelView, frame: totalLabelFrame) @@ -1674,7 +1689,7 @@ final class StorageUsageScreenComponent: Component { ) if let categoriesComponentView = self.categoriesView.view { if categoriesComponentView.superview == nil { - self.scrollView.addSubview(categoriesComponentView) + self.scrollContainerView.addSubview(categoriesComponentView) } transition.setFrame(view: categoriesComponentView, frame: CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: categoriesSize)) @@ -1697,7 +1712,7 @@ final class StorageUsageScreenComponent: Component { let categoriesDescriptionFrame = CGRect(origin: CGPoint(x: sideInset + 15.0, y: contentHeight), size: categoriesDescriptionSize) if let categoriesDescriptionComponentView = self.categoriesDescriptionView.view { if categoriesDescriptionComponentView.superview == nil { - self.scrollView.addSubview(categoriesDescriptionComponentView) + self.scrollContainerView.addSubview(categoriesDescriptionComponentView) } transition.setFrame(view: categoriesDescriptionComponentView, frame: categoriesDescriptionFrame) } @@ -1728,7 +1743,7 @@ final class StorageUsageScreenComponent: Component { let keepDurationTitleFrame = CGRect(origin: CGPoint(x: sideInset + 15.0, y: contentHeight), size: keepDurationTitleSize) if let keepDurationTitleComponentView = self.keepDurationTitleView.view { if keepDurationTitleComponentView.superview == nil { - self.scrollView.addSubview(keepDurationTitleComponentView) + self.scrollContainerView.addSubview(keepDurationTitleComponentView) } transition.setFrame(view: keepDurationTitleComponentView, frame: keepDurationTitleFrame) } @@ -1830,7 +1845,7 @@ final class StorageUsageScreenComponent: Component { let keepDurationDescriptionFrame = CGRect(origin: CGPoint(x: sideInset + 15.0, y: contentHeight), size: keepDurationDescriptionSize) if let keepDurationDescriptionComponentView = self.keepDurationDescriptionView.view { if keepDurationDescriptionComponentView.superview == nil { - self.scrollView.addSubview(keepDurationDescriptionComponentView) + self.scrollContainerView.addSubview(keepDurationDescriptionComponentView) } transition.setFrame(view: keepDurationDescriptionComponentView, frame: keepDurationDescriptionFrame) } @@ -1856,7 +1871,7 @@ final class StorageUsageScreenComponent: Component { let keepSizeTitleFrame = CGRect(origin: CGPoint(x: sideInset + 15.0, y: contentHeight), size: keepSizeTitleSize) if let keepSizeTitleComponentView = self.keepSizeTitleView.view { if keepSizeTitleComponentView.superview == nil { - self.scrollView.addSubview(keepSizeTitleComponentView) + self.scrollContainerView.addSubview(keepSizeTitleComponentView) } transition.setFrame(view: keepSizeTitleComponentView, frame: keepSizeTitleFrame) } @@ -1887,7 +1902,7 @@ final class StorageUsageScreenComponent: Component { let keepSizeFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: keepSizeSize) if let keepSizeComponentView = self.keepSizeView.view { if keepSizeComponentView.superview == nil { - self.scrollView.addSubview(keepSizeComponentView) + self.scrollContainerView.addSubview(keepSizeComponentView) } transition.setFrame(view: keepSizeComponentView, frame: keepSizeFrame) } @@ -1913,7 +1928,7 @@ final class StorageUsageScreenComponent: Component { let keepSizeDescriptionFrame = CGRect(origin: CGPoint(x: sideInset + 15.0, y: contentHeight), size: keepSizeDescriptionSize) if let keepSizeDescriptionComponentView = self.keepSizeDescriptionView.view { if keepSizeDescriptionComponentView.superview == nil { - self.scrollView.addSubview(keepSizeDescriptionComponentView) + self.scrollContainerView.addSubview(keepSizeDescriptionComponentView) } transition.setFrame(view: keepSizeDescriptionComponentView, frame: keepSizeDescriptionFrame) } @@ -2129,7 +2144,7 @@ final class StorageUsageScreenComponent: Component { ) if let panelContainerView = self.panelContainer.view { if panelContainerView.superview == nil { - self.scrollView.addSubview(panelContainerView) + self.scrollContainerView.addSubview(panelContainerView) } transition.setFrame(view: panelContainerView, frame: CGRect(origin: CGPoint(x: 0.0, y: contentHeight), size: panelContainerSize)) } @@ -2146,6 +2161,7 @@ final class StorageUsageScreenComponent: Component { if self.scrollView.contentSize != contentSize { self.scrollView.contentSize = contentSize } + transition.setFrame(view: self.scrollContainerView, frame: CGRect(origin: CGPoint(), size: contentSize)) var scrollViewBounds = self.scrollView.bounds scrollViewBounds.size = availableSize @@ -2181,7 +2197,7 @@ final class StorageUsageScreenComponent: Component { clearingNode.updateLayout(size: clearingSize, transition: .immediate) if animateIn { - clearingNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25, delay: 0.15) + clearingNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25, delay: 0.4) } } else { if let clearingNode = self.clearingNode { @@ -2190,10 +2206,10 @@ final class StorageUsageScreenComponent: Component { var delay: Double = 0.0 if let clearingDisplayTimestamp = self.clearingDisplayTimestamp { let timeDelta = CFAbsoluteTimeGetCurrent() - clearingDisplayTimestamp - if timeDelta < 0.12 { + if timeDelta < 0.4 { delay = 0.0 - } else if timeDelta < 0.4 { - delay = 0.4 + } else if timeDelta < 1.0 { + delay = 1.0 } } @@ -2414,7 +2430,9 @@ final class StorageUsageScreenComponent: Component { self.isClearing = false - self.state?.updated(transition: Transition(animation: .none).withUserData(AnimationHint(value: .clearedItems))) + if !firstTime { + self.state?.updated(transition: Transition(animation: .none).withUserData(AnimationHint(value: .clearedItems))) + } completion() }) @@ -2468,8 +2486,21 @@ final class StorageUsageScreenComponent: Component { var items: [ContextMenuItem] = [] + var openTitle: String = presentationData.strings.StorageManagement_OpenPhoto + for media in message.media { + if let _ = media as? TelegramMediaImage { + openTitle = presentationData.strings.StorageManagement_OpenPhoto + } else if let file = media as? TelegramMediaFile { + if file.isVideo { + openTitle = presentationData.strings.StorageManagement_OpenVideo + } else { + openTitle = presentationData.strings.StorageManagement_OpenFile + } + } + } + items.append(.action(ContextMenuActionItem( - text: presentationData.strings.StorageManagement_OpenPhoto, + text: openTitle, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Expand"), color: theme.contextMenu.primaryColor) }, action: { [weak self] c, _ in c.dismiss(completion: { [weak self] in diff --git a/submodules/TelegramUI/Images.xcassets/Settings/Storage/ParticleStar.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Settings/Storage/ParticleStar.imageset/Contents.json new file mode 100644 index 0000000000..3011bc172f --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Settings/Storage/ParticleStar.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Star.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Settings/Storage/ParticleStar.imageset/Star.svg b/submodules/TelegramUI/Images.xcassets/Settings/Storage/ParticleStar.imageset/Star.svg new file mode 100644 index 0000000000..62c15980fb --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Settings/Storage/ParticleStar.imageset/Star.svg @@ -0,0 +1,7 @@ + + + Icon / Video Copy 13 + + + + \ No newline at end of file diff --git a/submodules/TelegramUI/Sources/AccountContext.swift b/submodules/TelegramUI/Sources/AccountContext.swift index b269161226..24be73bed9 100644 --- a/submodules/TelegramUI/Sources/AccountContext.swift +++ b/submodules/TelegramUI/Sources/AccountContext.swift @@ -217,8 +217,13 @@ public final class AccountContextImpl: AccountContext { self.cachedGroupCallContexts = AccountGroupCallContextCacheImpl() self.meshAnimationCache = MeshAnimationCache(mediaBox: account.postbox.mediaBox) + let cacheStorageBox = self.account.postbox.mediaBox.cacheStorageBox self.animationCache = AnimationCacheImpl(basePath: self.account.postbox.mediaBox.basePath + "/animation-cache", allocateTempFile: { return TempBox.shared.tempFile(fileName: "file").path + }, updateStorageStats: { path, size in + if let pathData = path.data(using: .utf8) { + cacheStorageBox.update(id: pathData, size: size) + } }) self.animationRenderer = MultiAnimationRendererImpl() diff --git a/submodules/TelegramUI/Sources/AppDelegate.swift b/submodules/TelegramUI/Sources/AppDelegate.swift index ab49dcbb3a..8509275531 100644 --- a/submodules/TelegramUI/Sources/AppDelegate.swift +++ b/submodules/TelegramUI/Sources/AppDelegate.swift @@ -1306,26 +1306,10 @@ private func extractAccountManagerState(records: AccountRecordsView take(1) - |> deliverOnMainQueue).start(next: { sharedApplicationContext in - let _ = (sharedApplicationContext.sharedContext.activeAccountContexts - |> take(1) - |> deliverOnMainQueue).start(next: { activeAccounts in - var signals: Signal = .complete() - - for (_, context, _) in activeAccounts.accounts { - signals = signals |> then(context.account.cleanupTasks(lowImpact: false)) - } - - disposable.set(signals.start(completed: { - Logger.shared.log("App \(self.episodeId)", "Completed cleanup task") - - task.setTaskCompleted(success: true) - })) - }) + let disposable = self.runCacheReindexTasks(lowImpact: true, completion: { + Logger.shared.log("App \(self.episodeId)", "Completed cleanup task") + + task.setTaskCompleted(success: true) }) task.expirationHandler = { @@ -1351,8 +1335,43 @@ private func extractAccountManagerState(records: AccountRecordsView= minReindexTimestamp { + } else { + Logger.shared.log("App \(self.episodeId)", "Executing low-impact cache reindex in foreground") + let _ = self.runCacheReindexTasks(lowImpact: true, completion: { + Logger.shared.log("App \(self.episodeId)", "Executing low-impact cache reindex in foreground — done") + UserDefaults.standard.set(timestamp as NSNumber, forKey: "TelegramCacheIndexTimestamp") + }) + } + return true } + + private func runCacheReindexTasks(lowImpact: Bool, completion: @escaping () -> Void) -> Disposable { + let disposable = MetaDisposable() + + let _ = (self.sharedContextPromise.get() + |> take(1) + |> deliverOnMainQueue).start(next: { sharedApplicationContext in + let _ = (sharedApplicationContext.sharedContext.activeAccountContexts + |> take(1) + |> deliverOnMainQueue).start(next: { activeAccounts in + var signals: Signal = .complete() + + for (_, context, _) in activeAccounts.accounts { + signals = signals |> then(context.account.cleanupTasks(lowImpact: lowImpact)) + } + + disposable.set(signals.start(completed: { + completion() + })) + }) + }) + + return disposable + } private func resetBadge() { var resetOnce = true