diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 279645c736..16bc1f175e 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -253,7 +253,7 @@ "PUSH_CHAT_REACT_INVOICE" = "%2$@|%1$@ %3$@ to your invoice"; "PUSH_CHAT_REACT_GIF" = "%2$@|%1$@ %3$@ to your GIF"; -"PUSH_MESSAGE_SUGGEST_USERPIC" = "%1$@ suggested you new profile photo"; +"PUSH_MESSAGE_SUGGEST_USERPIC" = "%1$@|suggested you new profile photo"; "PUSH_REMINDER_TITLE" = "🗓 Reminder"; diff --git a/submodules/Postbox/Sources/MediaResource.swift b/submodules/Postbox/Sources/MediaResource.swift index 272ef949bd..1811c565ed 100644 --- a/submodules/Postbox/Sources/MediaResource.swift +++ b/submodules/Postbox/Sources/MediaResource.swift @@ -57,6 +57,7 @@ public enum MediaResourceUserContentType: UInt8, Equatable { case file = 4 case sticker = 6 case avatar = 7 + case audioVideoMessage = 8 } public struct MediaResourceFetchParameters { diff --git a/submodules/Postbox/Sources/SqliteValueBox.swift b/submodules/Postbox/Sources/SqliteValueBox.swift index 188f963a75..3daa0769d2 100644 --- a/submodules/Postbox/Sources/SqliteValueBox.swift +++ b/submodules/Postbox/Sources/SqliteValueBox.swift @@ -230,6 +230,7 @@ public final class SqliteValueBox: ValueBox { } func internalClose() { + self.clearStatements() self.database = nil } diff --git a/submodules/Postbox/Sources/StorageBox/StorageBox.swift b/submodules/Postbox/Sources/StorageBox/StorageBox.swift index 749bfea747..aca2241b9a 100644 --- a/submodules/Postbox/Sources/StorageBox/StorageBox.swift +++ b/submodules/Postbox/Sources/StorageBox/StorageBox.swift @@ -766,7 +766,7 @@ public final class StorageBox { return allStats } - func remove(peerId: Int64?, contentTypes: [UInt8]) -> [Data] { + func remove(peerId: Int64?, contentTypes: [UInt8], excludeIds: [Data]) -> [Data] { var resultIds: [Data] = [] self.valueBox.begin() @@ -786,17 +786,20 @@ public final class StorageBox { return true }) + let excludeIds = Set(excludeIds) + if let peerId = peerId { var filteredHashIds: [Data] = [] self.valueBox.scan(self.idToReferenceTable, keys: { key in let id = key.getData(0, length: 16) - if scannedIds[id] == nil { + guard let realId = scannedIds[id] else { + return true + } + if excludeIds.contains(realId) { return true } let itemPeerId = key.getInt64(16) - //let messageNamespace: UInt8 = key.getUInt8(16 + 8) - //let messageId = key.getInt32(16 + 8 + 1) if itemPeerId == peerId { filteredHashIds.append(id) @@ -812,23 +815,20 @@ public final class StorageBox { } } else { for (hashId, id) in scannedIds { + if excludeIds.contains(id) { + continue + } self.internalRemove(hashId: hashId) resultIds.append(id) } } - if let peerId = peerId { - let _ = peerId - } else { - - } - self.valueBox.commit() return Array(resultIds) } - func remove(peerIds: Set) -> [Data] { + func remove(peerIds: Set, includeIds: [Data], excludeIds: [Data]) -> [Data] { var resultIds: [Data] = [] self.valueBox.begin() @@ -838,7 +838,16 @@ public final class StorageBox { scannedIds.formUnion(self.allInternal(peerId: peerId)) } + for id in includeIds { + scannedIds.insert(id) + } + + let excludedIds = Set(excludeIds) + for id in scannedIds { + if excludedIds.contains(id) { + continue + } self.internalRemove(hashId: md5Hash(id).data) resultIds.append(id) } @@ -936,16 +945,16 @@ public final class StorageBox { } } - public func remove(peerId: PeerId?, contentTypes: [UInt8], completion: @escaping ([Data]) -> Void) { + public func remove(peerId: PeerId?, contentTypes: [UInt8], excludeIds: [Data], completion: @escaping ([Data]) -> Void) { self.impl.with { impl in - let ids = impl.remove(peerId: peerId?.toInt64(), contentTypes: contentTypes) + let ids = impl.remove(peerId: peerId?.toInt64(), contentTypes: contentTypes, excludeIds: excludeIds) completion(ids) } } - public func remove(peerIds: Set, completion: @escaping ([Data]) -> Void) { + public func remove(peerIds: Set, includeIds: [Data], excludeIds: [Data], completion: @escaping ([Data]) -> Void) { self.impl.with { impl in - let ids = impl.remove(peerIds: peerIds) + let ids = impl.remove(peerIds: peerIds, includeIds: includeIds, excludeIds: excludeIds) completion(ids) } } diff --git a/submodules/Postbox/Sources/TimeBasedCleanup.swift b/submodules/Postbox/Sources/TimeBasedCleanup.swift index 0436501db8..e96254c4a6 100644 --- a/submodules/Postbox/Sources/TimeBasedCleanup.swift +++ b/submodules/Postbox/Sources/TimeBasedCleanup.swift @@ -39,7 +39,95 @@ public func printOpenFiles() { } } -private func scanFiles(at path: String, olderThan minTimestamp: Int32, inodes: inout [InodeInfo]) -> ScanFilesResult { +private final class TempScanDatabase { + private let queue: Queue + private let valueBox: SqliteValueBox + + private let accessTimeTable: ValueBoxTable + + private var nextId: Int32 = 0 + + private let accessTimeKey = ValueBoxKey(length: 4 + 4) + private let accessInfoBuffer = WriteBuffer() + + init?(queue: Queue, basePath: String) { + self.queue = queue + guard let valueBox = SqliteValueBox(basePath: basePath, queue: queue, isTemporary: true, isReadOnly: false, useCaches: true, removeDatabaseOnError: true, encryptionParameters: nil, upgradeProgress: { _ in }) else { + return nil + } + self.valueBox = valueBox + + self.accessTimeTable = ValueBoxTable(id: 2, keyType: .binary, compactValuesOnCreation: true) + } + + func begin() { + self.valueBox.begin() + } + + func commit() { + self.valueBox.commit() + } + + func dispose() { + self.valueBox.internalClose() + } + + func add(pathBuffer: UnsafeMutablePointer, pathSize: Int, size: Int64, timestamp: Int32) { + let id = self.nextId + self.nextId += 1 + + var size = size + self.accessInfoBuffer.reset() + self.accessInfoBuffer.write(&size, length: 8) + self.accessInfoBuffer.write(pathBuffer, length: pathSize) + + self.accessTimeKey.setInt32(0, value: timestamp) + self.accessTimeKey.setInt32(4, value: id) + self.valueBox.set(self.accessTimeTable, key: self.accessTimeKey, value: self.accessInfoBuffer) + } + + func topByAccessTime(_ f: (Int64, String) -> Bool) { + var startKey = ValueBoxKey(length: 4) + startKey.setInt32(0, value: 0) + + let endKey = ValueBoxKey(length: 4) + endKey.setInt32(0, value: Int32.max) + + while true { + var lastKey: ValueBoxKey? + self.valueBox.range(self.accessTimeTable, start: startKey, end: endKey, values: { key, value in + var result = true + withExtendedLifetime(value, { + let readBuffer = ReadBuffer(memoryBufferNoCopy: value) + + var size: Int64 = 0 + readBuffer.read(&size, offset: 0, length: 8) + + var pathData = Data(count: value.length - 8) + pathData.withUnsafeMutableBytes { buffer -> Void in + readBuffer.read(buffer.baseAddress!, offset: 0, length: buffer.count) + } + + if let path = String(data: pathData, encoding: .utf8) { + result = f(size, path) + } + }) + + lastKey = key + + return result + }, limit: 512) + + if let lastKey = lastKey { + startKey = lastKey + } else { + break + } + } + } +} + +private func scanFiles(at path: String, olderThan minTimestamp: Int32, includeSubdirectories: Bool, tempDatabase: TempScanDatabase) -> ScanFilesResult { var result = ScanFilesResult() if let dp = opendir(path) { @@ -63,21 +151,24 @@ private func scanFiles(at path: String, olderThan minTimestamp: Int32, inodes: i strncat(pathBuffer, "/", 1024) strncat(pathBuffer, &dirp.pointee.d_name.0, 1024) - //puts(pathBuffer) - //puts("\n") - var value = stat() if stat(pathBuffer, &value) == 0 { - if value.st_mtimespec.tv_sec < minTimestamp { - unlink(pathBuffer) - result.unlinkedCount += 1 + if (((value.st_mode) & S_IFMT) == S_IFDIR) { + if includeSubdirectories { + if let subPath = String(data: Data(bytes: pathBuffer, count: strnlen(pathBuffer, 1024)), encoding: .utf8) { + let subResult = scanFiles(at: subPath, olderThan: minTimestamp, includeSubdirectories: true, tempDatabase: tempDatabase) + result.totalSize += subResult.totalSize + result.unlinkedCount += subResult.unlinkedCount + } + } } else { - result.totalSize += UInt64(value.st_size) - inodes.append(InodeInfo( - inode: value.st_ino, - timestamp: Int32(clamping: value.st_mtimespec.tv_sec), - size: UInt32(clamping: value.st_size) - )) + if value.st_mtimespec.tv_sec < minTimestamp { + unlink(pathBuffer) + result.unlinkedCount += 1 + } else { + result.totalSize += UInt64(value.st_size) + tempDatabase.add(pathBuffer: pathBuffer, pathSize: strnlen(pathBuffer, 1024), size: Int64(value.st_size), timestamp: Int32(value.st_mtimespec.tv_sec)) + } } } } @@ -87,9 +178,8 @@ private func scanFiles(at path: String, olderThan minTimestamp: Int32, inodes: i return result } -private func mapFiles(paths: [String], inodes: inout [InodeInfo], removeSize: UInt64, mainStoragePath: String, storageBox: StorageBox) { +/*private func mapFiles(paths: [String], inodes: inout [InodeInfo], removeSize: UInt64, mainStoragePath: String, storageBox: StorageBox) { var removedSize: UInt64 = 0 - inodes.sort(by: { lhs, rhs in return lhs.timestamp < rhs.timestamp }) @@ -138,19 +228,25 @@ private func mapFiles(paths: [String], inodes: inout [InodeInfo], removeSize: UI var value = stat() if stat(pathBuffer, &value) == 0 { - if inodesToDelete.contains(value.st_ino) { - if isMainPath { - let nameLength = strnlen(&dirp.pointee.d_name.0, 1024) - let nameData = Data(bytesNoCopy: &dirp.pointee.d_name.0, count: Int(nameLength), deallocator: .none) - withExtendedLifetime(nameData, { - if let fileName = String(data: nameData, encoding: .utf8) { - if let idData = MediaBox.idForFileName(name: fileName).data(using: .utf8) { - unlinkedResourceIds.append(idData) - } - } - }) + if (((value.st_mode) & S_IFMT) == S_IFDIR) { + if let subPath = String(data: Data(bytes: pathBuffer, count: strnlen(pathBuffer, 1024)), encoding: .utf8) { + mapFiles(paths: <#T##[String]#>, inodes: &<#T##[InodeInfo]#>, removeSize: remov, mainStoragePath: mainStoragePath, storageBox: storageBox) + } + } else { + if inodesToDelete.contains(value.st_ino) { + if isMainPath { + let nameLength = strnlen(&dirp.pointee.d_name.0, 1024) + let nameData = Data(bytesNoCopy: &dirp.pointee.d_name.0, count: Int(nameLength), deallocator: .none) + withExtendedLifetime(nameData, { + if let fileName = String(data: nameData, encoding: .utf8) { + if let idData = MediaBox.idForFileName(name: fileName).data(using: .utf8) { + unlinkedResourceIds.append(idData) + } + } + }) + } + unlink(pathBuffer) } - unlink(pathBuffer) } } } @@ -161,7 +257,7 @@ private func mapFiles(paths: [String], inodes: inout [InodeInfo], removeSize: UI if !unlinkedResourceIds.isEmpty { storageBox.remove(ids: unlinkedResourceIds) } -} +}*/ private final class TimeBasedCleanupImpl { private let queue: Queue @@ -178,19 +274,6 @@ private final class TimeBasedCleanupImpl { private var gigabytesLimit: Int32? private let scheduledScanDisposable = MetaDisposable() - - private struct GeneralFile : Comparable, Equatable { - let file: String - let size: Int - let timestamp:Int32 - static func == (lhs: GeneralFile, rhs: GeneralFile) -> Bool { - return lhs.timestamp == rhs.timestamp && lhs.size == rhs.size && lhs.file == rhs.file - } - static func < (lhs: GeneralFile, rhs: GeneralFile) -> Bool { - return lhs.timestamp < rhs.timestamp - } - } - init(queue: Queue, storageBox: StorageBox, generalPaths: [String], totalSizeBasedPath: String, shortLivedPaths: [String]) { self.queue = queue self.storageBox = storageBox @@ -220,14 +303,22 @@ private final class TimeBasedCleanupImpl { let shortLivedPaths = self.shortLivedPaths let storageBox = self.storageBox let scanOnce = Signal { subscriber in - DispatchQueue.global(qos: .background).async { + let queue = Queue(name: "TimeBasedCleanupScan", qos: .background) + queue.async { + let tempDirectory = TempBox.shared.tempDirectory() + guard let tempDatabase = TempScanDatabase(queue: queue, basePath: tempDirectory.path) else { + postboxLog("TimeBasedCleanup: couldn't create temp database at \(tempDirectory.path)") + subscriber.putCompletion() + return + } + tempDatabase.begin() + var removedShortLivedCount: Int = 0 var removedGeneralCount: Int = 0 let removedGeneralLimitCount: Int = 0 let startTime = CFAbsoluteTimeGetCurrent() - var inodes: [InodeInfo] = [] var paths: [String] = [] let timestamp = Int32(Date().timeIntervalSince1970) @@ -241,7 +332,7 @@ private final class TimeBasedCleanupImpl { let oldestShortLivedTimestamp = timestamp - shortLived let oldestGeneralTimestamp = timestamp - general for path in shortLivedPaths { - let scanResult = scanFiles(at: path, olderThan: oldestShortLivedTimestamp, inodes: &inodes) + let scanResult = scanFiles(at: path, olderThan: oldestShortLivedTimestamp, includeSubdirectories: true, tempDatabase: tempDatabase) if !paths.contains(path) { paths.append(path) } @@ -251,7 +342,7 @@ private final class TimeBasedCleanupImpl { var totalLimitSize: UInt64 = 0 for path in generalPaths { - let scanResult = scanFiles(at: path, olderThan: oldestGeneralTimestamp, inodes: &inodes) + let scanResult = scanFiles(at: path, olderThan: oldestGeneralTimestamp, includeSubdirectories: true, tempDatabase: tempDatabase) if !paths.contains(path) { paths.append(path) } @@ -259,7 +350,7 @@ private final class TimeBasedCleanupImpl { totalLimitSize += scanResult.totalSize } do { - let scanResult = scanFiles(at: totalSizeBasedPath, olderThan: 0, inodes: &inodes) + let scanResult = scanFiles(at: totalSizeBasedPath, olderThan: 0, includeSubdirectories: false, tempDatabase: tempDatabase) if !paths.contains(totalSizeBasedPath) { paths.append(totalSizeBasedPath) } @@ -267,10 +358,40 @@ private final class TimeBasedCleanupImpl { totalLimitSize += scanResult.totalSize } + tempDatabase.commit() + + var unlinkedResourceIds: [Data] = [] + if totalLimitSize > bytesLimit { - mapFiles(paths: paths, inodes: &inodes, removeSize: totalLimitSize - bytesLimit, mainStoragePath: totalSizeBasedPath, storageBox: storageBox) + var remainingSize = Int64(totalLimitSize) + tempDatabase.topByAccessTime { size, filePath in + remainingSize -= size + + unlink(filePath) + + if (filePath as NSString).deletingLastPathComponent == totalSizeBasedPath { + let fileName = (filePath as NSString).lastPathComponent + if let idData = MediaBox.idForFileName(name: fileName).data(using: .utf8) { + unlinkedResourceIds.append(idData) + } + } + //let fileName = filePath.lastPathComponent + + if remainingSize >= bytesLimit { + return false + } + + return true + } } + if !unlinkedResourceIds.isEmpty { + storageBox.remove(ids: unlinkedResourceIds) + } + + tempDatabase.dispose() + TempBox.shared.dispose(tempDirectory) + if removedShortLivedCount != 0 || removedGeneralCount != 0 || removedGeneralLimitCount != 0 { postboxLog("[TimeBasedCleanup] \(CFAbsoluteTimeGetCurrent() - startTime) s removed \(removedShortLivedCount) short-lived files, \(removedGeneralCount) general files, \(removedGeneralLimitCount) limit files") } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Resources/CollectCacheUsageStats.swift b/submodules/TelegramCore/Sources/TelegramEngine/Resources/CollectCacheUsageStats.swift index a871b81bcc..8a8312fd0c 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Resources/CollectCacheUsageStats.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Resources/CollectCacheUsageStats.swift @@ -284,29 +284,42 @@ func _internal_renderStorageUsageStatsMessages(account: Account, stats: StorageU } } -func _internal_clearStorage(account: Account, peerId: EnginePeer.Id?, categories: [StorageUsageStats.CategoryKey]) -> Signal { +func _internal_clearStorage(account: Account, peerId: EnginePeer.Id?, categories: [StorageUsageStats.CategoryKey], excludeMessages: [Message]) -> Signal { let mediaBox = account.postbox.mediaBox return Signal { subscriber in - mediaBox.storageBox.remove(peerId: peerId, contentTypes: categories.map { item -> UInt8 in - let mappedItem: MediaResourceUserContentType + var resourceIds = Set() + for message in excludeMessages { + extractMediaResourceIds(message: message, resourceIds: &resourceIds) + } + var excludeIds: [Data] = [] + for resourceId in resourceIds { + if let data = resourceId.stringRepresentation.data(using: .utf8) { + excludeIds.append(data) + } + } + + var mappedContentTypes: [UInt8] = [] + for item in categories { switch item { case .photos: - mappedItem = .image + mappedContentTypes.append(MediaResourceUserContentType.image.rawValue) case .videos: - mappedItem = .video + mappedContentTypes.append(MediaResourceUserContentType.video.rawValue) case .files: - mappedItem = .file + mappedContentTypes.append(MediaResourceUserContentType.file.rawValue) case .music: - mappedItem = .audio + mappedContentTypes.append(MediaResourceUserContentType.audio.rawValue) case .stickers: - mappedItem = .sticker + mappedContentTypes.append(MediaResourceUserContentType.sticker.rawValue) case .avatars: - mappedItem = .avatar + mappedContentTypes.append(MediaResourceUserContentType.avatar.rawValue) case .misc: - mappedItem = .other + mappedContentTypes.append(MediaResourceUserContentType.other.rawValue) + mappedContentTypes.append(MediaResourceUserContentType.audioVideoMessage.rawValue) } - return mappedItem.rawValue - }, completion: { ids in + } + + mediaBox.storageBox.remove(peerId: peerId, contentTypes: mappedContentTypes, excludeIds: excludeIds, completion: { ids in var resourceIds: [MediaResourceId] = [] for id in ids { if let value = String(data: id, encoding: .utf8) { @@ -345,10 +358,32 @@ func _internal_clearStorage(account: Account, peerId: EnginePeer.Id?, categories } } -func _internal_clearStorage(account: Account, peerIds: Set) -> Signal { +func _internal_clearStorage(account: Account, peerIds: Set, includeMessages: [Message], excludeMessages: [Message]) -> Signal { let mediaBox = account.postbox.mediaBox return Signal { subscriber in - mediaBox.storageBox.remove(peerIds: peerIds, completion: { ids in + var includeResourceIds = Set() + for message in includeMessages { + extractMediaResourceIds(message: message, resourceIds: &includeResourceIds) + } + var includeIds: [Data] = [] + for resourceId in includeResourceIds { + if let data = resourceId.stringRepresentation.data(using: .utf8) { + includeIds.append(data) + } + } + + var excludeResourceIds = Set() + for message in excludeMessages { + extractMediaResourceIds(message: message, resourceIds: &excludeResourceIds) + } + var excludeIds: [Data] = [] + for resourceId in excludeResourceIds { + if let data = resourceId.stringRepresentation.data(using: .utf8) { + excludeIds.append(data) + } + } + + mediaBox.storageBox.remove(peerIds: peerIds, includeIds: includeIds, excludeIds: excludeIds, completion: { ids in var resourceIds: [MediaResourceId] = [] for id in ids { if let value = String(data: id, encoding: .utf8) { @@ -365,6 +400,47 @@ func _internal_clearStorage(account: Account, peerIds: Set) -> Si } } +private func extractMediaResourceIds(message: Message, resourceIds: inout Set) { + for media in message.media { + if let image = media as? TelegramMediaImage { + for representation in image.representations { + resourceIds.insert(representation.resource.id) + } + } else if let file = media as? TelegramMediaFile { + for representation in file.previewRepresentations { + resourceIds.insert(representation.resource.id) + } + resourceIds.insert(file.resource.id) + } else if let webpage = media as? TelegramMediaWebpage { + if case let .Loaded(content) = webpage.content { + if let image = content.image { + for representation in image.representations { + resourceIds.insert(representation.resource.id) + } + } + if let file = content.file { + for representation in file.previewRepresentations { + resourceIds.insert(representation.resource.id) + } + resourceIds.insert(file.resource.id) + } + } + } else if let game = media as? TelegramMediaGame { + if let image = game.image { + for representation in image.representations { + resourceIds.insert(representation.resource.id) + } + } + if let file = game.file { + for representation in file.previewRepresentations { + resourceIds.insert(representation.resource.id) + } + resourceIds.insert(file.resource.id) + } + } + } +} + func _internal_clearStorage(account: Account, messages: [Message]) -> Signal { let mediaBox = account.postbox.mediaBox @@ -372,44 +448,7 @@ func _internal_clearStorage(account: Account, messages: [Message]) -> Signal() for message in messages { - for media in message.media { - if let image = media as? TelegramMediaImage { - for representation in image.representations { - resourceIds.insert(representation.resource.id) - } - } else if let file = media as? TelegramMediaFile { - for representation in file.previewRepresentations { - resourceIds.insert(representation.resource.id) - } - resourceIds.insert(file.resource.id) - } else if let webpage = media as? TelegramMediaWebpage { - if case let .Loaded(content) = webpage.content { - if let image = content.image { - for representation in image.representations { - resourceIds.insert(representation.resource.id) - } - } - if let file = content.file { - for representation in file.previewRepresentations { - resourceIds.insert(representation.resource.id) - } - resourceIds.insert(file.resource.id) - } - } - } else if let game = media as? TelegramMediaGame { - if let image = game.image { - for representation in image.representations { - resourceIds.insert(representation.resource.id) - } - } - if let file = game.file { - for representation in file.previewRepresentations { - resourceIds.insert(representation.resource.id) - } - resourceIds.insert(file.resource.id) - } - } - } + extractMediaResourceIds(message: message, resourceIds: &resourceIds) } var removeIds: [Data] = [] diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Resources/TelegramEngineResources.swift b/submodules/TelegramCore/Sources/TelegramEngine/Resources/TelegramEngineResources.swift index 4f04bfab39..5da88dc13f 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Resources/TelegramEngineResources.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Resources/TelegramEngineResources.swift @@ -8,7 +8,9 @@ public typealias EngineTempBoxFile = TempBoxFile public extension MediaResourceUserContentType { init(file: TelegramMediaFile) { - if file.isMusic || file.isVoice { + if file.isInstantVideo || file.isVoice { + self = .audioVideoMessage + } else if file.isMusic { self = .audio } else if file.isSticker || file.isAnimatedSticker { self = .sticker @@ -231,12 +233,12 @@ public extension TelegramEngine { return _internal_renderStorageUsageStatsMessages(account: self.account, stats: stats, categories: categories, existingMessages: existingMessages) } - public func clearStorage(peerId: EnginePeer.Id?, categories: [StorageUsageStats.CategoryKey]) -> Signal { - return _internal_clearStorage(account: self.account, peerId: peerId, categories: categories) + public func clearStorage(peerId: EnginePeer.Id?, categories: [StorageUsageStats.CategoryKey], excludeMessages: [Message]) -> Signal { + return _internal_clearStorage(account: self.account, peerId: peerId, categories: categories, excludeMessages: excludeMessages) } - public func clearStorage(peerIds: Set) -> Signal { - _internal_clearStorage(account: self.account, peerIds: peerIds) + public func clearStorage(peerIds: Set, includeMessages: [Message], excludeMessages: [Message]) -> Signal { + _internal_clearStorage(account: self.account, peerIds: peerIds, includeMessages: includeMessages, excludeMessages: excludeMessages) } public func clearStorage(messages: [Message]) -> Signal { diff --git a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/PieChartComponent.swift b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/PieChartComponent.swift index 27d8396145..00aa1b8386 100644 --- a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/PieChartComponent.swift +++ b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/PieChartComponent.swift @@ -22,6 +22,7 @@ private func interpolateChartData(start: PieChartComponent.ChartData, end: PieCh for i in 0 ..< result.items.count { result.items[i].value = (1.0 - progress) * start.items[i].value + progress * end.items[i].value result.items[i].color = start.items[i].color.interpolateTo(end.items[i].color, fraction: progress) ?? end.items[i].color + result.items[i].mergeFactor = (1.0 - progress) * start.items[i].mergeFactor + progress * end.items[i].mergeFactor } return result @@ -139,12 +140,16 @@ final class PieChartComponent: Component { var displayValue: Double var value: Double var color: UIColor + var mergeable: Bool + var mergeFactor: CGFloat - init(id: StorageUsageScreenComponent.Category, displayValue: Double, value: Double, color: UIColor) { + init(id: StorageUsageScreenComponent.Category, displayValue: Double, value: Double, color: UIColor, mergeable: Bool, mergeFactor: CGFloat) { self.id = id self.displayValue = displayValue self.value = value self.color = color + self.mergeable = mergeable + self.mergeFactor = mergeFactor } } @@ -179,12 +184,12 @@ final class PieChartComponent: Component { private final class ChartDataView: UIView { private(set) var theme: PresentationTheme? private(set) var data: ChartData? - private(set) var selectedKey: StorageUsageScreenComponent.Category? + private(set) var selectedKey: AnyHashable? private var currentAnimation: (start: ChartData, end: ChartData, current: ChartData, progress: CGFloat)? private var animator: DisplayLinkAnimator? - private var labels: [StorageUsageScreenComponent.Category: ChartLabel] = [:] + private var labels: [AnyHashable: ChartLabel] = [:] override init(frame: CGRect) { super.init(frame: frame) @@ -201,7 +206,7 @@ final class PieChartComponent: Component { self.animator?.invalidate() } - func setItems(theme: PresentationTheme, data: ChartData, selectedKey: StorageUsageScreenComponent.Category?, animated: Bool) { + func setItems(theme: PresentationTheme, data: ChartData, selectedKey: AnyHashable?, animated: Bool) { let data = processChartData(data: data) if self.theme !== theme || self.data != data || self.selectedKey != selectedKey { @@ -253,7 +258,6 @@ final class PieChartComponent: Component { let innerDiameter: CGFloat = 100.0 let spacing: CGFloat = 2.0 let innerAngleSpacing: CGFloat = spacing / (innerDiameter * 0.5) - //let minAngle: CGFloat = innerAngleSpacing * 2.0 + 2.0 / (innerDiameter * 0.5) var angles: [Double] = [] for i in 0 ..< data.items.count { @@ -265,13 +269,23 @@ final class PieChartComponent: Component { let diameter: CGFloat = 200.0 let reducedDiameter: CGFloat = 170.0 + let shapeLayerFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: diameter, height: diameter)) + + struct ItemAngleData { + var angleValue: CGFloat + var startAngle: CGFloat + var endAngle: CGFloat + } + + var anglesData: [ItemAngleData] = [] + var startAngle: CGFloat = 0.0 for i in 0 ..< data.items.count { let item = data.items[i] let itemOuterDiameter: CGFloat if let selectedKey = self.selectedKey { - if selectedKey == item.id { + if selectedKey == AnyHashable(item.id) { itemOuterDiameter = diameter } else { itemOuterDiameter = reducedDiameter @@ -280,24 +294,54 @@ final class PieChartComponent: Component { itemOuterDiameter = diameter } - let shapeLayerFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: diameter, height: diameter)) - let angleSpacing: CGFloat = spacing / (itemOuterDiameter * 0.5) 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 = data.items[data.items.count - 1] + } else { + previousItem = data.items[i - 1] + } + + let nextItem: ChartData.Item + if i == data.items.count - 1 { + nextItem = data.items[0] + } else { + nextItem = data.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 innerStartAngle = startAngle + innerAngleSpacing * 0.5 + let arcInnerStartAngle = startAngle + innerAngleSpacing * 0.5 * beforeSpacingFraction + var innerEndAngle = startAngle + angleValue - innerAngleSpacing * 0.5 innerEndAngle = max(innerEndAngle, innerStartAngle) + var arcInnerEndAngle = startAngle + angleValue - innerAngleSpacing * 0.5 * afterSpacingFraction + arcInnerEndAngle = max(arcInnerEndAngle, arcInnerStartAngle) let outerStartAngle = startAngle + angleSpacing * 0.5 + let arcOuterStartAngle = startAngle + angleSpacing * 0.5 * beforeSpacingFraction var outerEndAngle = startAngle + angleValue - angleSpacing * 0.5 outerEndAngle = max(outerEndAngle, outerStartAngle) + var arcOuterEndAngle = startAngle + angleValue - angleSpacing * 0.5 * afterSpacingFraction + arcOuterEndAngle = max(arcOuterEndAngle, arcOuterStartAngle) let path = CGMutablePath() - path.addArc(center: CGPoint(x: diameter * 0.5, y: diameter * 0.5), radius: innerDiameter * 0.5, startAngle: innerEndAngle, endAngle: innerStartAngle, clockwise: true) - path.addArc(center: CGPoint(x: diameter * 0.5, y: diameter * 0.5), radius: itemOuterDiameter * 0.5, startAngle: outerStartAngle, endAngle: outerEndAngle, clockwise: false) + path.addArc(center: CGPoint(x: diameter * 0.5, y: diameter * 0.5), radius: innerDiameter * 0.5, startAngle: arcInnerEndAngle, endAngle: arcInnerStartAngle, clockwise: true) + path.addArc(center: CGPoint(x: diameter * 0.5, y: diameter * 0.5), radius: itemOuterDiameter * 0.5, startAngle: arcOuterStartAngle, endAngle: arcOuterEndAngle, clockwise: false) context.addPath(path) context.setFillColor(item.color.cgColor) @@ -305,7 +349,11 @@ final class PieChartComponent: Component { startAngle += angleValue - let fractionValue: Double = floor(item.displayValue * 100.0 * 10.0) / 10.0 + anglesData.append(ItemAngleData(angleValue: angleValue, startAngle: innerStartAngle, endAngle: innerEndAngle)) + } + + func updateItemLabel(id: AnyHashable, displayValue: Double, mergeFactor: CGFloat, angleData: ItemAngleData) { + let fractionValue: Double = floor(displayValue * 100.0 * 10.0) / 10.0 let fractionString: String if fractionValue < 0.1 { fractionString = "<0.1" @@ -316,16 +364,20 @@ final class PieChartComponent: Component { } let label: ChartLabel - if let current = self.labels[item.id] { + if let current = self.labels[id] { label = current } else { label = ChartLabel() - self.labels[item.id] = label + self.labels[id] = label } let labelSize = label.update(text: "\(fractionString)%") var labelFrame: CGRect? + let angleValue = angleData.angleValue + let innerStartAngle = angleData.startAngle + let innerEndAngle = angleData.endAngle + if angleValue >= 0.001 { for step in 0 ... 20 { let stepFraction: CGFloat = CGFloat(step) / 20.0 @@ -472,7 +524,8 @@ final class PieChartComponent: Component { var labelScale = labelFrame.width / labelSize.width - let normalAlpha: CGFloat = labelScale < 0.4 ? 0.0 : 1.0 + var normalAlpha: CGFloat = labelScale < 0.4 ? 0.0 : 1.0 + normalAlpha *= max(0.0, mergeFactor) var relLabelCenter = CGPoint( x: labelFrame.midX - shapeLayerFrame.midX, @@ -481,7 +534,7 @@ final class PieChartComponent: Component { let labelAlpha: CGFloat if let selectedKey = self.selectedKey { - if selectedKey == item.id { + if selectedKey == id { labelAlpha = normalAlpha } else { labelAlpha = 0.0 @@ -499,7 +552,7 @@ final class PieChartComponent: Component { } if labelView.alpha != labelAlpha { let transition: Transition - if animateIn { + if animateIn || "".isEmpty { transition = .immediate } else { transition = Transition(animation: .curve(duration: 0.18, curve: .easeInOut)) @@ -516,6 +569,34 @@ final class PieChartComponent: Component { labelView.transform = CGAffineTransformMakeScale(labelScale, labelScale) } } + + var mergedItem: (displayValue: Double, angleData: ItemAngleData, mergeFactor: CGFloat)? + for i in 0 ..< data.items.count { + let item = data.items[i] + let angleData = anglesData[i] + updateItemLabel(id: item.id, displayValue: item.displayValue, mergeFactor: item.mergeFactor, angleData: angleData) + + if item.mergeable { + if var currentMergedItem = mergedItem { + currentMergedItem.displayValue += item.displayValue + currentMergedItem.angleData.startAngle = min(currentMergedItem.angleData.startAngle, angleData.startAngle) + currentMergedItem.angleData.endAngle = max(currentMergedItem.angleData.endAngle, angleData.endAngle) + mergedItem = currentMergedItem + } else { + let invertedMergeFactor: CGFloat = 1.0 - max(0.0, item.mergeFactor) + mergedItem = (item.displayValue, angleData, invertedMergeFactor) + } + } + } + + if let mergedItem { + updateItemLabel(id: "merged", displayValue: mergedItem.displayValue, mergeFactor: mergedItem.mergeFactor, angleData: mergedItem.angleData) + } else { + if let label = self.labels["merged"] { + self.labels.removeValue(forKey: "merged") + label.removeFromSuperview() + } + } } } diff --git a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsagePanelContainerComponent.swift b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsagePanelContainerComponent.swift index 73b136a70d..f3edec47c6 100644 --- a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsagePanelContainerComponent.swift +++ b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsagePanelContainerComponent.swift @@ -345,19 +345,22 @@ final class StorageUsagePanelContainerComponent: Component { let dateTimeFormat: PresentationDateTimeFormat let insets: UIEdgeInsets let items: [Item] + let currentPanelUpdated: (AnyHashable, Transition) -> Void init( theme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, insets: UIEdgeInsets, - items: [Item] + items: [Item], + currentPanelUpdated: @escaping (AnyHashable, Transition) -> Void ) { self.theme = theme self.strings = strings self.dateTimeFormat = dateTimeFormat self.insets = insets self.items = items + self.currentPanelUpdated = currentPanelUpdated } static func ==(lhs: StorageUsagePanelContainerComponent, rhs: StorageUsagePanelContainerComponent) -> Bool { @@ -497,13 +500,6 @@ final class StorageUsagePanelContainerComponent: Component { } self.transitionFraction = transitionFraction self.state?.updated(transition: .immediate) - - // let nextKey = availablePanes[updatedIndex] - // print(transitionFraction) - //self.paneTransitionPromise.set(transitionFraction) - - //self.update(size: size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, expansionFraction: expansionFraction, presentationData: presentationData, data: data, transition: .immediate) - //self.currentPaneUpdated?(false) case .cancelled, .ended: guard let component = self.component, let currentId = self.currentId else { return @@ -533,7 +529,11 @@ final class StorageUsagePanelContainerComponent: Component { } self.transitionFraction = 0.0 - self.state?.updated(transition: Transition(animation: .curve(duration: 0.35, curve: .spring))) + let transition = Transition(animation: .curve(duration: 0.35, curve: .spring)) + if let currentId = self.currentId { + self.state?.updated(transition: transition) + component.currentPanelUpdated(currentId, transition) + } self.animatingTransition = false //self.currentPaneUpdated?(false) @@ -615,7 +615,9 @@ final class StorageUsagePanelContainerComponent: Component { } if component.items.contains(where: { $0.id == id }) { self.currentId = id - self.state?.updated(transition: Transition(animation: .curve(duration: 0.35, curve: .spring))) + let transition = Transition(animation: .curve(duration: 0.35, curve: .spring)) + self.state?.updated(transition: transition) + component.currentPanelUpdated(id, transition) } } )), diff --git a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsageScreen.swift b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsageScreen.swift index 4babd975f3..2f188c8162 100644 --- a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsageScreen.swift +++ b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsageScreen.swift @@ -175,6 +175,16 @@ final class StorageUsageScreenComponent: Component { let selectedPeers: Set let selectedMessages: Set + var isEmpty: Bool { + if !self.selectedPeers.isEmpty { + return false + } + if !self.selectedMessages.isEmpty { + return false + } + return true + } + init( selectedPeers: Set, selectedMessages: Set @@ -200,17 +210,31 @@ final class StorageUsageScreenComponent: Component { return true } - func togglePeer(id: EnginePeer.Id) -> SelectionState { + func togglePeer(id: EnginePeer.Id, availableMessages: [EngineMessage.Id: Message]) -> SelectionState { var selectedPeers = self.selectedPeers + var selectedMessages = self.selectedMessages + if selectedPeers.contains(id) { selectedPeers.remove(id) + + for (messageId, _) in availableMessages { + if messageId.peerId == id { + selectedMessages.remove(messageId) + } + } } else { selectedPeers.insert(id) + + for (messageId, _) in availableMessages { + if messageId.peerId == id { + selectedMessages.insert(messageId) + } + } } return SelectionState( selectedPeers: selectedPeers, - selectedMessages: Set() + selectedMessages: selectedMessages ) } @@ -223,7 +247,7 @@ final class StorageUsageScreenComponent: Component { } return SelectionState( - selectedPeers: Set(), + selectedPeers: self.selectedPeers, selectedMessages: selectedMessages ) } @@ -287,6 +311,7 @@ final class StorageUsageScreenComponent: Component { private var currentStats: AllStorageUsageStats? private var existingCategories: Set = Set() + private var otherCategories: Set = Set() private var currentMessages: [MessageId: Message] = [:] private var cacheSettings: CacheStorageSettings? @@ -299,6 +324,8 @@ final class StorageUsageScreenComponent: Component { private var selectionState: SelectionState? + private var currentSelectedPanelId: AnyHashable? + private var clearingDisplayTimestamp: Double? private var isClearing: Bool = false { didSet { @@ -501,11 +528,22 @@ final class StorageUsageScreenComponent: Component { animatedTransition.setAlpha(view: self.navigationBackgroundView, alpha: navigationBackgroundAlpha) animatedTransition.setAlpha(layer: self.navigationSeparatorLayerContainer, alpha: navigationBackgroundAlpha) + var buttonsMasterAlpha: CGFloat = 1.0 + if let component = self.component, component.peer != nil { + buttonsMasterAlpha = 0.0 + } else { + if self.currentSelectedPanelId == nil || self.currentSelectedPanelId == AnyHashable("peers") { + buttonsMasterAlpha = 1.0 + } else { + buttonsMasterAlpha = 0.0 + } + } + if let navigationEditButtonView = self.navigationEditButton.view { - animatedTransition.setAlpha(view: navigationEditButtonView, alpha: (self.selectionState == nil ? 1.0 : 0.0) * navigationBackgroundAlpha) + animatedTransition.setAlpha(view: navigationEditButtonView, alpha: (self.selectionState == nil ? 1.0 : 0.0) * buttonsMasterAlpha * navigationBackgroundAlpha) } if let navigationDoneButtonView = self.navigationDoneButton.view { - animatedTransition.setAlpha(view: navigationDoneButtonView, alpha: (self.selectionState == nil ? 0.0 : 1.0) * navigationBackgroundAlpha) + animatedTransition.setAlpha(view: navigationDoneButtonView, alpha: (self.selectionState == nil ? 0.0 : 1.0) * buttonsMasterAlpha * navigationBackgroundAlpha) } let expansionDistance: CGFloat = 32.0 @@ -766,7 +804,7 @@ final class StorageUsageScreenComponent: Component { let sideInset: CGFloat = 16.0 + environment.safeInsets.left var bottomInset: CGFloat = environment.safeInsets.bottom - if let selectionState = self.selectionState { + if let selectionState = self.selectionState, !selectionState.isEmpty { let selectionPanel: ComponentView var selectionPanelTransition = transition if let current = self.selectionPanel { @@ -779,15 +817,6 @@ final class StorageUsageScreenComponent: Component { var selectedSize: Int64 = 0 if let currentStats = self.currentStats { - for peerId in selectionState.selectedPeers { - if let stats = currentStats.peers[peerId] { - let peerSize = stats.stats.categories.values.reduce(0, { - $0 + $1.size - }) - selectedSize += peerSize - } - } - let contextStats: StorageUsageStats if let peer = component.peer { contextStats = currentStats.peers[peer.id]?.stats ?? StorageUsageStats(categories: [:]) @@ -795,10 +824,34 @@ final class StorageUsageScreenComponent: Component { contextStats = currentStats.totalStats } + for peerId in selectionState.selectedPeers { + if let stats = currentStats.peers[peerId] { + let peerSize = stats.stats.categories.values.reduce(0, { + $0 + $1.size + }) + selectedSize += peerSize + + for (messageId, _) in self.currentMessages { + if messageId.peerId == peerId { + if !selectionState.selectedMessages.contains(messageId) { + inner: for (_, category) in contextStats.categories { + if let messageSize = category.messages[messageId] { + selectedSize -= messageSize + break inner + } + } + } + } + } + } + } + for messageId in selectionState.selectedMessages { for (_, category) in contextStats.categories { if let messageSize = category.messages[messageId] { - selectedSize += messageSize + if !selectionState.selectedPeers.contains(messageId.peerId) { + selectedSize += messageSize + } break } } @@ -846,7 +899,7 @@ final class StorageUsageScreenComponent: Component { contentHeight += environment.statusBarHeight + topInset - let chartOrder: [Category] = [ + let allCategories: [Category] = [ .photos, .videos, .files, @@ -869,15 +922,8 @@ final class StorageUsageScreenComponent: Component { self.selectedCategories.removeAll() } - var chartItems: [PieChartComponent.ChartData.Item] = [] var listCategories: [StorageCategoriesComponent.CategoryData] = [] - let otherCategories: [Category] = [ - .stickers, - .avatars, - .misc - ] - var totalSize: Int64 = 0 if let currentStats = self.currentStats { let contextStats: StorageUsageStats @@ -891,7 +937,7 @@ final class StorageUsageScreenComponent: Component { totalSize += value.size } - for category in chartOrder { + for category in allCategories { let mappedCategory: StorageUsageStats.CategoryKey switch category { case .photos: @@ -924,18 +970,6 @@ final class StorageUsageScreenComponent: Component { categoryFraction = Double(categorySize) / Double(totalSize) } - var categoryChartFraction: CGFloat = categoryFraction - if !self.selectedCategories.isEmpty && !self.selectedCategories.contains(category) { - categoryChartFraction = 0.0 - } - - var chartCategoryColor = category.color - if !self.isOtherCategoryExpanded && otherCategories.contains(category) { - chartCategoryColor = Category.misc.color - } - - chartItems.append(PieChartComponent.ChartData.Item(id: category, displayValue: categoryFraction, value: categoryChartFraction, color: chartCategoryColor)) - if categorySize != 0 { listCategories.append(StorageCategoriesComponent.CategoryData( key: category, color: category.color, title: category.title(strings: environment.strings), size: categorySize, sizeFraction: categoryFraction, isSelected: self.selectedCategories.contains(category), subcategories: [])) @@ -943,15 +977,18 @@ final class StorageUsageScreenComponent: Component { } } + listCategories.sort(by: { $0.sizeFraction > $1.sizeFraction }) + var otherListCategories: [StorageCategoriesComponent.CategoryData] = [] - for listCategory in listCategories { - if otherCategories.contains(where: { $0 == listCategory.key }) { - otherListCategories.append(listCategory) + if listCategories.count > 5 { + for i in (4 ..< listCategories.count).reversed() { + if listCategories[i].sizeFraction < 0.04 { + otherListCategories.insert(listCategories[i], at: 0) + listCategories.remove(at: i) + } } } - listCategories = listCategories.filter { item in - return !otherCategories.contains(where: { $0 == item.key }) - } + self.otherCategories = Set(otherListCategories.map(\.key)) if !otherListCategories.isEmpty { var totalOtherSize: Int64 = 0 for listCategory in otherListCategories { @@ -978,27 +1015,28 @@ final class StorageUsageScreenComponent: Component { key: Category.other, color: listColor, title: Category.other.title(strings: environment.strings), size: totalOtherSize, sizeFraction: categoryFraction, isSelected: isSelected, subcategories: otherListCategories)) } - if !self.isOtherCategoryExpanded { - var otherSum: CGFloat = 0.0 - var otherRealSum: CGFloat = 0.0 - for i in 0 ..< chartItems.count { - if otherCategories.contains(chartItems[i].id) { - var itemValue = chartItems[i].value - if itemValue > 0.00001 { - itemValue = max(itemValue, 0.01) - } - otherSum += itemValue - otherRealSum += chartItems[i].displayValue - if case .misc = chartItems[i].id { - } else { - chartItems[i].value = 0.0 - } - } + var chartItems: [PieChartComponent.ChartData.Item] = [] + for listCategory in listCategories { + var categoryChartFraction: CGFloat = listCategory.sizeFraction + if !self.selectedCategories.isEmpty && !self.selectedCategories.contains(listCategory.key) { + categoryChartFraction = 0.0 } - if let index = chartItems.firstIndex(where: { $0.id == .misc }) { - chartItems[index].value = otherSum - chartItems[index].displayValue = otherRealSum + chartItems.append(PieChartComponent.ChartData.Item(id: listCategory.key, displayValue: listCategory.sizeFraction, value: categoryChartFraction, color: listCategory.color, mergeable: false, mergeFactor: 1.0)) + } + for listCategory in otherListCategories { + var categoryChartFraction: CGFloat = listCategory.sizeFraction + if !self.selectedCategories.isEmpty && !self.selectedCategories.contains(listCategory.key) { + categoryChartFraction = 0.0 } + + let visualMergeFactor: CGFloat + if self.isOtherCategoryExpanded { + visualMergeFactor = 1.0 + } else { + visualMergeFactor = 0.0 + } + + chartItems.append(PieChartComponent.ChartData.Item(id: listCategory.key, displayValue: listCategory.sizeFraction, value: categoryChartFraction, color: self.isOtherCategoryExpanded ? listCategory.color : Category.misc.color, mergeable: true, mergeFactor: visualMergeFactor)) } let chartData = PieChartComponent.ChartData(items: chartItems) @@ -1254,8 +1292,7 @@ final class StorageUsageScreenComponent: Component { return } if key == Category.other { - var otherCategories: [Category] = [.stickers, .avatars, .misc] - otherCategories = otherCategories.filter(self.existingCategories.contains) + let otherCategories = self.otherCategories.filter(self.existingCategories.contains) if !otherCategories.isEmpty { if otherCategories.allSatisfy(self.selectedCategories.contains) { for item in otherCategories { @@ -1557,7 +1594,7 @@ final class StorageUsageScreenComponent: Component { return } if let selectionState = self.selectionState { - self.selectionState = selectionState.togglePeer(id: peer.id) + self.selectionState = selectionState.togglePeer(id: peer.id, availableMessages: self.currentMessages) self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring))) } else { self.openPeer(peer: peer) @@ -1624,7 +1661,7 @@ final class StorageUsageScreenComponent: Component { if self.selectionState == nil { self.selectionState = SelectionState() } - self.selectionState = self.selectionState?.togglePeer(id: peer.id) + self.selectionState = self.selectionState?.togglePeer(id: peer.id, availableMessages: self.currentMessages) self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring))) }) )) @@ -1655,20 +1692,19 @@ final class StorageUsageScreenComponent: Component { panel: AnyComponent(StorageMediaGridPanelComponent( context: component.context, items: self.imageItems, - selectionState: self.selectionState, + selectionState: self.selectionState ?? SelectionState(), action: { [weak self] messageId in guard let self else { return } - guard let message = self.currentMessages[messageId] else { + guard let _ = self.currentMessages[messageId] else { return } if self.selectionState == nil { - self.openMessage(message: message) - } else { - self.selectionState = self.selectionState?.toggleMessage(id: messageId) - self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring))) + self.selectionState = SelectionState() } + self.selectionState = self.selectionState?.toggleMessage(id: messageId) + self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring))) }, contextAction: { [weak self] messageId, containerView, sourceRect, gesture in guard let self else { @@ -1686,20 +1722,19 @@ final class StorageUsageScreenComponent: Component { panel: AnyComponent(StorageFileListPanelComponent( context: component.context, items: self.fileItems, - selectionState: self.selectionState, + selectionState: self.selectionState ?? SelectionState(), action: { [weak self] messageId in guard let self else { return } - guard let message = self.currentMessages[messageId] else { + guard let _ = self.currentMessages[messageId] else { return } if self.selectionState == nil { - self.openMessage(message: message) - } else { - self.selectionState = self.selectionState?.toggleMessage(id: messageId) - self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring))) + self.selectionState = SelectionState() } + self.selectionState = self.selectionState?.toggleMessage(id: messageId) + self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring))) }, contextAction: { [weak self] messageId, containerView, gesture in guard let self else { @@ -1717,7 +1752,7 @@ final class StorageUsageScreenComponent: Component { panel: AnyComponent(StorageFileListPanelComponent( context: component.context, items: self.musicItems, - selectionState: self.selectionState, + selectionState: self.selectionState ?? SelectionState(), action: { [weak self] messageId in guard let self else { return @@ -1755,8 +1790,15 @@ final class StorageUsageScreenComponent: Component { strings: environment.strings, dateTimeFormat: environment.dateTimeFormat, insets: UIEdgeInsets(top: 0.0, left: environment.safeInsets.left, bottom: bottomInset, right: environment.safeInsets.right), - items: panelItems) - ), + items: panelItems, + currentPanelUpdated: { [weak self] id, transition in + guard let self else { + return + } + self.currentSelectedPanelId = id + self.state?.updated(transition: transition) + } + )), environment: { StorageUsagePanelContainerEnvironment(isScrollable: wasLockedAtPanels) }, @@ -2467,7 +2509,7 @@ final class StorageUsageScreenComponent: Component { self.isClearing = true self.state?.updated(transition: .immediate) - let _ = (component.context.engine.resources.clearStorage(peerId: peerId, categories: mappedCategories) + let _ = (component.context.engine.resources.clearStorage(peerId: peerId, categories: mappedCategories, excludeMessages: []) |> deliverOnMainQueue).start(completed: { [weak self] in guard let self, let component = self.component, let currentStats = self.currentStats else { return @@ -2516,7 +2558,7 @@ final class StorageUsageScreenComponent: Component { } }) }) - } else if !peers.isEmpty { + } else if !peers.isEmpty || !messages.isEmpty { self.isClearing = true self.state?.updated(transition: .immediate) @@ -2529,7 +2571,22 @@ final class StorageUsageScreenComponent: Component { } } - let _ = (component.context.engine.resources.clearStorage(peerIds: peers) + var includeMessages: [Message] = [] + var excludeMessages: [Message] = [] + + for (id, message) in self.currentMessages { + if peers.contains(id.peerId) { + if !messages.contains(id) { + excludeMessages.append(message) + } + } else { + if messages.contains(id) { + includeMessages.append(message) + } + } + } + + let _ = (component.context.engine.resources.clearStorage(peerIds: peers, includeMessages: includeMessages, excludeMessages: excludeMessages) |> deliverOnMainQueue).start(completed: { [weak self] in guard let self else { return @@ -2539,48 +2596,6 @@ final class StorageUsageScreenComponent: Component { guard let self else { return } - if totalSize != 0 { - self.reportClearedStorage(size: totalSize) - } - }) - }) - } else if !messages.isEmpty { - var messageItems: [Message] = [] - var totalSize: Int64 = 0 - - let contextStats: StorageUsageStats - if let peer = component.peer { - contextStats = self.currentStats?.peers[peer.id]?.stats ?? StorageUsageStats(categories: [:]) - } else { - contextStats = self.currentStats?.totalStats ?? StorageUsageStats(categories: [:]) - } - - for id in messages { - if let message = self.currentMessages[id] { - messageItems.append(message) - - for (_, value) in contextStats.categories { - if let size = value.messages[id] { - totalSize += size - } - } - } - } - - self.isClearing = true - self.state?.updated(transition: .immediate) - - let _ = (component.context.engine.resources.clearStorage(messages: messageItems) - |> deliverOnMainQueue).start(completed: { [weak self] in - guard let self else { - return - } - - self.reloadStats(firstTime: false, completion: { [weak self] in - guard let self else { - return - } - if totalSize != 0 { self.reportClearedStorage(size: totalSize) }