Storage usage improvements

This commit is contained in:
Ali 2023-01-03 17:44:14 +04:00
parent a526ba7843
commit da7b04a592
17 changed files with 679 additions and 196 deletions

View File

@ -1341,12 +1341,20 @@ public final class ChatListNode: ListView {
} }
let storageInfo: Signal<Double?, NoError> let storageInfo: Signal<Double?, NoError>
if "".isEmpty, case .chatList(groupId: .root) = location, chatListFilter == nil { if case .chatList(groupId: .root) = location, chatListFilter == nil {
let storageBox = context.account.postbox.mediaBox.storageBox let totalSizeSignal = combineLatest(context.account.postbox.mediaBox.storageBox.totalSize(), context.account.postbox.mediaBox.cacheStorageBox.totalSize())
storageInfo = storageBox.totalSize() |> map { a, b -> Int64 in
return a + b
}
storageInfo = totalSizeSignal
|> take(1) |> take(1)
|> mapToSignal { initialSize -> Signal<Double?, NoError> in |> mapToSignal { initialSize -> Signal<Double?, NoError> in
#if DEBUG
let fractionLimit: Double = 0.0001
#else
let fractionLimit: Double = 0.3 let fractionLimit: Double = 0.3
#endif
let systemAttributes = try? FileManager.default.attributesOfFileSystem(forPath: NSHomeDirectory() as String) let systemAttributes = try? FileManager.default.attributesOfFileSystem(forPath: NSHomeDirectory() as String)
let deviceFreeSpace = (systemAttributes?[FileAttributeKey.systemFreeSize] as? NSNumber)?.int64Value ?? 0 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 state = Atomic(value: ReportState(lastSize: initialSize))
let updatedReportSize: Signal<Double?, NoError> = Signal { subscriber in let updatedReportSize: Signal<Double?, NoError> = 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 let updatedSize = state.with { state -> Int64 in
if abs(initialSize - size) > 50 * 1024 * 1024 { if abs(initialSize - size) > 50 * 1024 * 1024 {
state.lastSize = size state.lastSize = size

View File

@ -92,6 +92,8 @@ class ChatListStorageInfoItemNode: ListViewItemNode {
self.addSubnode(self.titleNode) self.addSubnode(self.titleNode)
self.addSubnode(self.textNode) self.addSubnode(self.textNode)
self.addSubnode(self.arrowNode) self.addSubnode(self.arrowNode)
self.zPosition = 1.0
} }
override func didLoad() { override func didLoad() {

View File

@ -71,7 +71,16 @@ private func generateBlurredThumbnail(image: UIImage, adjustSaturation: Bool = f
return thumbnailContext.generateImage() 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 { if context.size.width <= 70.0 && context.size.height <= 70.0 {
guard let file = ManagedFile(queue: nil, path: path, mode: .readwrite) else { guard let file = ManagedFile(queue: nil, path: path, mode: .readwrite) else {
return nil return nil
@ -103,6 +112,9 @@ private func storeImage(context: DrawingContext, to path: String) -> UIImage? {
vImageConvert_BGRA8888toRGB565(&source, &target, vImage_Flags(kvImageDoNotTile)) vImageConvert_BGRA8888toRGB565(&source, &target, vImage_Flags(kvImageDoNotTile))
let _ = file.write(targetData, count: targetLength) 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() return context.generateImage()
} else { } else {
@ -110,6 +122,9 @@ private func storeImage(context: DrawingContext, to path: String) -> UIImage? {
return nil return nil
} }
let _ = try? resultData.write(to: URL(fileURLWithPath: path)) 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 return image
} }
} }
@ -212,7 +227,7 @@ public final class DirectMediaImageCache {
} }
} }
private enum ImageType { fileprivate enum ImageType {
case blurredThumbnail case blurredThumbnail
case square(width: Int) 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<UIImage?, NoError>? { private func getLoadSignal(width: Int, userLocation: MediaResourceUserLocation, userContentType: MediaResourceUserContentType, resource: MediaResourceReference, resourceSizeLimit: Int64) -> Signal<UIImage?, NoError>? {
return Signal { subscriber in return Signal { subscriber in
let cachePath = self.getCachePath(resourceId: resource.resource.id, imageType: .square(width: width))
let fetch = fetchedMediaResource( let fetch = fetchedMediaResource(
mediaBox: self.account.postbox.mediaBox, mediaBox: self.account.postbox.mediaBox,
userLocation: userLocation, userLocation: userLocation,
@ -281,7 +294,7 @@ public final class DirectMediaImageCache {
context.draw(image.cgImage!, in: imageRect) 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.putNext(scaledImage)
subscriber.putCompletion() subscriber.putCompletion()
} }

View File

@ -145,6 +145,7 @@ public final class MediaBox {
private let timeBasedCleanup: TimeBasedCleanup private let timeBasedCleanup: TimeBasedCleanup
public let storageBox: StorageBox public let storageBox: StorageBox
public let cacheStorageBox: StorageBox
private let didRemoveResourcesPipe = ValuePipe<Void>() private let didRemoveResourcesPipe = ValuePipe<Void>()
public var didRemoveResources: Signal<Void, NoError> { public var didRemoveResources: Signal<Void, NoError> {
@ -192,6 +193,9 @@ public final class MediaBox {
self.storageBox = StorageBox(logger: StorageBox.Logger(impl: { string in self.storageBox = StorageBox(logger: StorageBox.Logger(impl: { string in
postboxLog(string) postboxLog(string)
}), basePath: basePath + "/storage") }), 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.timeBasedCleanup = TimeBasedCleanup(storageBox: self.storageBox, generalPaths: [
self.basePath + "/cache", self.basePath + "/cache",
@ -878,6 +882,9 @@ public final class MediaBox {
public func storeCachedResourceRepresentation(_ resource: MediaResource, representation: CachedMediaResourceRepresentation, data: Data) { public func storeCachedResourceRepresentation(_ resource: MediaResource, representation: CachedMediaResourceRepresentation, data: Data) {
self.dataQueue.async { self.dataQueue.async {
let path = self.cachedRepresentationPathsForId(resource.id.stringRepresentation, representationId: representation.uniqueId, keepDuration: representation.keepDuration).complete 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)) 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 }) { public func storeCachedResourceRepresentation(_ resource: MediaResource, representationId: String, keepDuration: CachedMediaRepresentationKeepDuration, data: Data, completion: @escaping (String) -> Void = { _ in }) {
self.dataQueue.async { self.dataQueue.async {
let path = self.cachedRepresentationPathsForId(resource.id.stringRepresentation, representationId: representationId, keepDuration: keepDuration).complete 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)) let _ = try? data.write(to: URL(fileURLWithPath: path))
completion(path) completion(path)
} }
@ -894,6 +904,9 @@ public final class MediaBox {
self.dataQueue.async { self.dataQueue.async {
let path = self.cachedRepresentationPathsForId(resourceId, representationId: representationId, keepDuration: keepDuration).complete let path = self.cachedRepresentationPathsForId(resourceId, representationId: representationId, keepDuration: keepDuration).complete
let _ = try? data.write(to: URL(fileURLWithPath: path)) 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) completion(path)
} }
} }
@ -902,6 +915,9 @@ public final class MediaBox {
self.dataQueue.async { self.dataQueue.async {
let path = self.cachedRepresentationPathsForId(resourceId, representationId: representationId, keepDuration: keepDuration).complete let path = self.cachedRepresentationPathsForId(resourceId, representationId: representationId, keepDuration: keepDuration).complete
let _ = try? FileManager.default.moveItem(atPath: tempFile.path, toPath: path) 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) completion(path)
} }
} }
@ -983,6 +999,7 @@ public final class MediaBox {
if !context.initialized { if !context.initialized {
context.initialized = true context.initialized = true
let cacheStorageBox = self.cacheStorageBox
let signal = self.wrappedFetchCachedResourceRepresentation.get() let signal = self.wrappedFetchCachedResourceRepresentation.get()
|> take(1) |> take(1)
|> mapToSignal { fetch in |> mapToSignal { fetch in
@ -999,15 +1016,24 @@ public final class MediaBox {
switch next { switch next {
case let .temporaryPath(temporaryPath): case let .temporaryPath(temporaryPath):
rename(temporaryPath, paths.complete) 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 isDone = true
case let .tempFile(tempFile): case let .tempFile(tempFile):
rename(tempFile.path, paths.complete) rename(tempFile.path, paths.complete)
TempBox.shared.dispose(tempFile) 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 isDone = true
case .reset: case .reset:
let file = ManagedFile(queue: strongSelf.dataQueue, path: paths.partial, mode: .readwrite) let file = ManagedFile(queue: strongSelf.dataQueue, path: paths.partial, mode: .readwrite)
file?.truncate(count: 0) file?.truncate(count: 0)
unlink(paths.complete) unlink(paths.complete)
if let pathData = paths.complete.data(using: .utf8) {
cacheStorageBox.update(id: pathData, size: 0)
}
case let .data(dataPart): case let .data(dataPart):
let file = ManagedFile(queue: strongSelf.dataQueue, path: paths.partial, mode: .append) let file = ManagedFile(queue: strongSelf.dataQueue, path: paths.partial, mode: .append)
let dataCount = dataPart.count let dataCount = dataPart.count
@ -1015,8 +1041,14 @@ public final class MediaBox {
let bytes = rawBytes.baseAddress!.assumingMemoryBound(to: UInt8.self) let bytes = rawBytes.baseAddress!.assumingMemoryBound(to: UInt8.self)
let _ = file?.write(bytes, count: dataCount) 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: case .done:
link(paths.partial, paths.complete) 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 isDone = true
} }
@ -1155,6 +1187,7 @@ public final class MediaBox {
if !context.initialized { if !context.initialized {
context.initialized = true context.initialized = true
let cacheStorageBox = self.cacheStorageBox
let signal = fetch() let signal = fetch()
|> deliverOn(self.dataQueue) |> deliverOn(self.dataQueue)
context.disposable.set(signal.start(next: { [weak self, weak context] next in context.disposable.set(signal.start(next: { [weak self, weak context] next in
@ -1165,15 +1198,24 @@ public final class MediaBox {
switch next { switch next {
case let .temporaryPath(temporaryPath): case let .temporaryPath(temporaryPath):
rename(temporaryPath, paths.complete) 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 isDone = true
case let .tempFile(tempFile): case let .tempFile(tempFile):
rename(tempFile.path, paths.complete) rename(tempFile.path, paths.complete)
TempBox.shared.dispose(tempFile) 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 isDone = true
case .reset: case .reset:
let file = ManagedFile(queue: strongSelf.dataQueue, path: paths.partial, mode: .readwrite) let file = ManagedFile(queue: strongSelf.dataQueue, path: paths.partial, mode: .readwrite)
file?.truncate(count: 0) file?.truncate(count: 0)
unlink(paths.complete) unlink(paths.complete)
if let pathData = paths.complete.data(using: .utf8) {
cacheStorageBox.update(id: pathData, size: 0)
}
case let .data(dataPart): case let .data(dataPart):
let file = ManagedFile(queue: strongSelf.dataQueue, path: paths.partial, mode: .append) let file = ManagedFile(queue: strongSelf.dataQueue, path: paths.partial, mode: .append)
let dataCount = dataPart.count let dataCount = dataPart.count
@ -1181,9 +1223,15 @@ public final class MediaBox {
let bytes = rawBytes.baseAddress!.assumingMemoryBound(to: UInt8.self) let bytes = rawBytes.baseAddress!.assumingMemoryBound(to: UInt8.self)
let _ = file?.write(bytes, count: dataCount) 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: case .done:
link(paths.partial, paths.complete) link(paths.partial, paths.complete)
isDone = true 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 { 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 basePath = self.basePath
let storageBox = self.storageBox 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> { public func collectAllResourceUsage() -> Signal<[(id: String?, path: String, size: Int64)], NoError> {
return Signal { subscriber in return Signal { subscriber in
self.dataQueue.async { self.dataQueue.async {
@ -1396,66 +1542,6 @@ public final class MediaBox {
} }
} }
/*var cacheResult: Int64 = 0
var excludePrefixes = Set<String>()
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.putNext(result)
subscriber.putCompletion() subscriber.putCompletion()
} }

View File

@ -472,6 +472,8 @@ public final class StorageBox {
for peerId in self.peerIdsReferencing(hashId: hashId) { for peerId in self.peerIdsReferencing(hashId: hashId) {
self.internalAddSize(peerId: peerId, contentType: info.contentType, delta: sizeDelta) 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() self.valueBox.commit()

View File

@ -449,10 +449,27 @@ private func cleanupAccount(networkArguments: NetworkInitializationArguments, ac
break 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 { if tokens.count > 20 {
tokens.removeLast(tokens.count - 20) tokens.removeLast(tokens.count - 20)
} }
NSUbiquitousKeyValueStore.default.set(tokens.map { $0.base64EncodedString() }, forKey: "T_SLTokens")
NSUbiquitousKeyValueStore.default.synchronize()
transaction.setStoredLoginTokens(tokens) transaction.setStoredLoginTokens(tokens)
}).start() }).start()
account.shouldBeServiceTaskMaster.set(.single(.never)) account.shouldBeServiceTaskMaster.set(.single(.never))

View File

@ -72,11 +72,27 @@ private func ~=<T: RegularExpressionMatchable>(pattern: Regex, matchable: T) ->
} }
public func sendAuthorizationCode(accountManager: AccountManager<TelegramAccountManagerTypes>, account: UnauthorizedAccount, phoneNumber: String, apiId: Int32, apiHash: String, syncContacts: Bool) -> Signal<UnauthorizedAccount, AuthorizationCodeRequestError> { public func sendAuthorizationCode(accountManager: AccountManager<TelegramAccountManagerTypes>, account: UnauthorizedAccount, phoneNumber: String, apiId: Int32, apiHash: String, syncContacts: Bool) -> Signal<UnauthorizedAccount, AuthorizationCodeRequestError> {
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 accountManager.transaction { transaction -> [Data] in
return transaction.getStoredLoginTokens() return transaction.getStoredLoginTokens()
} }
|> castError(AuthorizationCodeRequestError.self) |> castError(AuthorizationCodeRequestError.self)
|> mapToSignal { authTokens -> Signal<UnauthorizedAccount, AuthorizationCodeRequestError> in |> mapToSignal { localAuthTokens -> Signal<UnauthorizedAccount, AuthorizationCodeRequestError> in
var authTokens = localAuthTokens
for data in cloudValue {
if !authTokens.contains(data) {
authTokens.insert(data, at: 0)
}
}
var flags: Int32 = 0 var flags: Int32 = 0
flags |= 1 << 5 //allowMissedCall flags |= 1 << 5 //allowMissedCall
flags |= 1 << 6 //tokens flags |= 1 << 6 //tokens

View File

@ -123,10 +123,19 @@ private extension StorageUsageStats {
mappedCategory = .avatars mappedCategory = .avatars
case MediaResourceUserContentType.sticker.rawValue: case MediaResourceUserContentType.sticker.rawValue:
mappedCategory = .stickers mappedCategory = .stickers
case MediaResourceUserContentType.other.rawValue:
mappedCategory = .misc
case MediaResourceUserContentType.audioVideoMessage.rawValue:
mappedCategory = .misc
default: default:
mappedCategory = .misc 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) self.init(categories: mappedCategories)
@ -134,7 +143,7 @@ private extension StorageUsageStats {
} }
func _internal_collectStorageUsageStats(account: Account) -> Signal<AllStorageUsageStats, NoError> { func _internal_collectStorageUsageStats(account: Account) -> Signal<AllStorageUsageStats, NoError> {
let additionalStats = Signal<Int64, NoError> { subscriber in /*let additionalStats = Signal<Int64, NoError> { subscriber in
DispatchQueue.global().async { DispatchQueue.global().async {
var totalSize: Int64 = 0 var totalSize: Int64 = 0
@ -207,7 +216,9 @@ func _internal_collectStorageUsageStats(account: Account) -> Signal<AllStorageUs
} }
return EmptyDisposable return EmptyDisposable
} }*/
let additionalStats = account.postbox.mediaBox.cacheStorageBox.totalSize() |> take(1)
return combineLatest( return combineLatest(
additionalStats, additionalStats,
@ -264,6 +275,7 @@ func _internal_collectStorageUsageStats(account: Account) -> Signal<AllStorageUs
func _internal_renderStorageUsageStatsMessages(account: Account, stats: StorageUsageStats, categories: [StorageUsageStats.CategoryKey], existingMessages: [EngineMessage.Id: Message]) -> Signal<[EngineMessage.Id: Message], NoError> { func _internal_renderStorageUsageStatsMessages(account: Account, stats: StorageUsageStats, categories: [StorageUsageStats.CategoryKey], existingMessages: [EngineMessage.Id: Message]) -> Signal<[EngineMessage.Id: Message], NoError> {
return account.postbox.transaction { transaction -> [EngineMessage.Id: Message] in return account.postbox.transaction { transaction -> [EngineMessage.Id: Message] in
var result: [EngineMessage.Id: Message] = [:] var result: [EngineMessage.Id: Message] = [:]
var peerInChatList: [EnginePeer.Id: Bool] = [:]
for (category, value) in stats.categories { for (category, value) in stats.categories {
if !categories.contains(category) { if !categories.contains(category) {
continue continue
@ -273,8 +285,23 @@ func _internal_renderStorageUsageStatsMessages(account: Account, stats: StorageU
if result[id] == nil { if result[id] == nil {
if let message = existingMessages[id] { if let message = existingMessages[id] {
result[id] = message result[id] = message
} else if let message = transaction.getMessage(id) { } else {
result[id] = message 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: case .misc:
mappedContentTypes.append(MediaResourceUserContentType.other.rawValue) mappedContentTypes.append(MediaResourceUserContentType.other.rawValue)
mappedContentTypes.append(MediaResourceUserContentType.audioVideoMessage.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() subscriber.putCompletion()
} else { } else {
subscriber.putCompletion() subscriber.putCompletion()

View File

@ -165,6 +165,7 @@ private enum ApplicationSpecificGlobalNotice: Int32 {
case forcedPasswordSetup = 31 case forcedPasswordSetup = 31
case emojiTooltip = 32 case emojiTooltip = 32
case audioTranscriptionSuggestion = 33 case audioTranscriptionSuggestion = 33
case clearStorageDismissedTipSize = 34
var key: ValueBoxKey { var key: ValueBoxKey {
let v = ValueBoxKey(length: 4) let v = ValueBoxKey(length: 4)
@ -349,6 +350,10 @@ private struct ApplicationSpecificNoticeKeys {
static func audioTranscriptionSuggestion() -> NoticeEntryKey { static func audioTranscriptionSuggestion() -> NoticeEntryKey {
return NoticeEntryKey(namespace: noticeNamespace(namespace: globalNamespace), key: ApplicationSpecificGlobalNotice.audioTranscriptionSuggestion.key) 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 { public struct ApplicationSpecificNotice {
@ -1087,6 +1092,25 @@ public struct ApplicationSpecificNotice {
} }
} }
public static func getClearStorageDismissedTipSize(accountManager: AccountManager<TelegramAccountManagerTypes>) -> Signal<Int32, NoError> {
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<TelegramAccountManagerTypes>, value: Int32) -> Signal<Never, NoError> {
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<TelegramAccountManagerTypes>) -> Signal<(Int32, Int32), NoError> { public static func getInteractiveEmojiSyncTip(accountManager: AccountManager<TelegramAccountManagerTypes>) -> Signal<(Int32, Int32), NoError> {
return accountManager.transaction { transaction -> (Int32, Int32) in return accountManager.transaction { transaction -> (Int32, Int32) in
if let value = transaction.getNotice(ApplicationSpecificNoticeKeys.interactiveEmojiSyncTip())?.get(ApplicationSpecificTimestampAndCounterNotice.self) { if let value = transaction.getNotice(ApplicationSpecificNoticeKeys.interactiveEmojiSyncTip())?.get(ApplicationSpecificTimestampAndCounterNotice.self) {

View File

@ -14,6 +14,26 @@ private func alignUp(size: Int, align: Int) -> Int {
return (size + alignmentMask) & ~alignmentMask 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 final class AnimationCacheItemFrame {
public enum RequestedFormat { public enum RequestedFormat {
case rgba 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 { guard let higherResolutionItem = try? loadItem(path: higherResolutionPath) else {
return nil 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 { guard let _ = try? FileManager.default.moveItem(atPath: result.animationPath, toPath: itemPath) else {
return nil return nil
} }
if let size = fileSize(itemPath) {
updateStorageStats(itemPath, size)
}
guard let item = try? loadItem(path: itemPath) else { guard let item = try? loadItem(path: itemPath) else {
return nil 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 { guard let animationItem = try? loadItem(path: animationItemPath) else {
return false 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 { guard let _ = try? FileManager.default.moveItem(atPath: result.animationPath, toPath: itemPath) else {
return false return false
} }
if let size = fileSize(itemPath) {
updateStorageStats(itemPath, size)
}
return true return true
} catch { } catch {
return false return false
@ -1408,13 +1435,14 @@ public final class AnimationCacheImpl: AnimationCache {
private let queue: Queue private let queue: Queue
private let basePath: String private let basePath: String
private let allocateTempFile: () -> String private let allocateTempFile: () -> String
private let updateStorageStats: (String, Int64) -> Void
private let fetchQueues: [Queue] private let fetchQueues: [Queue]
private var nextFetchQueueIndex: Int = 0 private var nextFetchQueueIndex: Int = 0
private var itemContexts: [ItemKey: ItemContext] = [:] 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 self.queue = queue
let fetchQueueCount: Int 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.fetchQueues = (0 ..< fetchQueueCount).map { i in Queue(name: "AnimationCacheImpl-Fetch\(i)", qos: .default) }
self.basePath = basePath self.basePath = basePath
self.allocateTempFile = allocateTempFile self.allocateTempFile = allocateTempFile
self.updateStorageStats = updateStorageStats
} }
deinit { deinit {
@ -1464,6 +1493,7 @@ public final class AnimationCacheImpl: AnimationCache {
let fetchQueueIndex = self.nextFetchQueueIndex let fetchQueueIndex = self.nextFetchQueueIndex
self.nextFetchQueueIndex += 1 self.nextFetchQueueIndex += 1
let allocateTempFile = self.allocateTempFile 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 guard let writer = AnimationCacheItemWriterImpl(queue: self.fetchQueues[fetchQueueIndex % self.fetchQueues.count], allocateTempFile: self.allocateTempFile, completion: { [weak self, weak itemContext] result in
queue.async { queue.async {
guard let strongSelf = self, let itemContext = itemContext, itemContext === strongSelf.itemContexts[key] else { 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 { guard let _ = try? FileManager.default.moveItem(atPath: result.animationPath, toPath: itemPath) else {
return 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() { for f in itemContext.subscribers.copyItems() {
guard let item = try? loadItem(path: itemPath) else { 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 hashString = md5Hash(sourceId)
let sourceIdPath = itemSubpath(hashString: hashString, width: Int(size.width), height: Int(size.height)) let sourceIdPath = itemSubpath(hashString: hashString, width: Int(size.width), height: Int(size.height))
let itemDirectoryPath = "\(basePath)/\(sourceIdPath.directory)" 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 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 return adaptedItem
} }
} }
@ -1543,7 +1576,7 @@ public final class AnimationCacheImpl: AnimationCache {
return nil 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 hashString = md5Hash(sourceId)
let sourceIdPath = itemSubpath(hashString: hashString, width: Int(size.width), height: Int(size.height)) let sourceIdPath = itemSubpath(hashString: hashString, width: Int(size.width), height: Int(size.height))
let itemDirectoryPath = "\(basePath)/\(sourceIdPath.directory)" 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 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)) completion(AnimationCacheItemResult(item: adaptedItem, isFinal: true))
return EmptyDisposable return EmptyDisposable
} }
@ -1579,6 +1612,9 @@ public final class AnimationCacheImpl: AnimationCache {
completion(AnimationCacheItemResult(item: nil, isFinal: true)) completion(AnimationCacheItemResult(item: nil, isFinal: true))
return return
} }
if let size = fileSize(itemFirstFramePath) {
updateStorageStats(itemFirstFramePath, size)
}
guard let item = try? loadItem(path: itemFirstFramePath) else { guard let item = try? loadItem(path: itemFirstFramePath) else {
completion(AnimationCacheItemResult(item: nil, isFinal: true)) completion(AnimationCacheItemResult(item: nil, isFinal: true))
return return
@ -1604,14 +1640,16 @@ public final class AnimationCacheImpl: AnimationCache {
private let basePath: String private let basePath: String
private let impl: QueueLocalObject<Impl> private let impl: QueueLocalObject<Impl>
private let allocateTempFile: () -> String 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() let queue = Queue()
self.queue = queue self.queue = queue
self.basePath = basePath self.basePath = basePath
self.allocateTempFile = allocateTempFile self.allocateTempFile = allocateTempFile
self.updateStorageStats = updateStorageStats
self.impl = QueueLocalObject(queue: queue, generate: { 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? { 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 { 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 basePath = self.basePath
let allocateTempFile = self.allocateTempFile let allocateTempFile = self.allocateTempFile
let updateStorageStats = self.updateStorageStats
queue.async { 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 return disposable

View File

@ -332,15 +332,18 @@ final class PieChartComponent: Component {
private struct CalculatedLayout { private struct CalculatedLayout {
var size: CGSize var size: CGSize
var sections: [CalculatedSection] var sections: [CalculatedSection]
var isEmpty: Bool
init(size: CGSize, sections: [CalculatedSection]) { init(size: CGSize, sections: [CalculatedSection]) {
self.size = size self.size = size
self.sections = sections self.sections = sections
self.isEmpty = sections.isEmpty
} }
init(interpolating start: CalculatedLayout, to end: CalculatedLayout, progress: CGFloat, size: CGSize) { init(interpolating start: CalculatedLayout, to end: CalculatedLayout, progress: CGFloat, size: CGSize) {
self.size = size self.size = size
self.sections = [] self.sections = []
self.isEmpty = end.isEmpty
for i in 0 ..< end.sections.count { for i in 0 ..< end.sections.count {
let right = end.sections[i] 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.size = size
self.sections = [] self.sections = []
self.isEmpty = isEmpty
if items.isEmpty { if items.isEmpty {
return return
} }
let innerDiameter: CGFloat = 100.0 let innerDiameter: CGFloat = isEmpty ? 90.0 : 100.0
let spacing: CGFloat = 2.0 let spacing: CGFloat = isEmpty ? -0.5 : 2.0
let innerAngleSpacing: CGFloat = spacing / (innerDiameter * 0.5) let innerAngleSpacing: CGFloat = spacing / (innerDiameter * 0.5)
var angles: [Double] = [] var angles: [Double] = []
@ -389,8 +393,8 @@ final class PieChartComponent: Component {
angles.append(angle) angles.append(angle)
} }
let diameter: CGFloat = 200.0 let diameter: CGFloat = isEmpty ? (innerDiameter + 6.0 * 2.0) : 200.0
let reducedDiameter: CGFloat = 170.0 let reducedDiameter: CGFloat = floor(0.85 * diameter)
var anglesData: [ItemAngleData] = [] var anglesData: [ItemAngleData] = []
@ -413,30 +417,8 @@ final class PieChartComponent: Component {
let angleValue: CGFloat = angles[i] let angleValue: CGFloat = angles[i]
var beforeSpacingFraction: CGFloat = 1.0 let beforeSpacingFraction: CGFloat = 1.0
var afterSpacingFraction: CGFloat = 1.0 let 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 innerStartAngle = startAngle + innerAngleSpacing * 0.5 let innerStartAngle = startAngle + innerAngleSpacing * 0.5
let arcInnerStartAngle = startAngle + innerAngleSpacing * 0.5 * beforeSpacingFraction let arcInnerStartAngle = startAngle + innerAngleSpacing * 0.5 * beforeSpacingFraction
@ -453,9 +435,11 @@ final class PieChartComponent: Component {
var arcOuterEndAngle = startAngle + angleValue - angleSpacing * 0.5 * afterSpacingFraction var arcOuterEndAngle = startAngle + angleValue - angleSpacing * 0.5 * afterSpacingFraction
arcOuterEndAngle = max(arcOuterEndAngle, arcOuterStartAngle) arcOuterEndAngle = max(arcOuterEndAngle, arcOuterStartAngle)
let itemColor: UIColor = isEmpty ? UIColor(rgb: 0x34C759) : item.color
self.sections.append(CalculatedSection( self.sections.append(CalculatedSection(
id: item.id, id: item.id,
color: item.color, color: itemColor,
innerAngle: arcInnerStartAngle ..< arcInnerEndAngle, innerAngle: arcInnerStartAngle ..< arcInnerEndAngle,
outerAngle: arcOuterStartAngle ..< arcOuterEndAngle, outerAngle: arcOuterStartAngle ..< arcOuterEndAngle,
innerRadius: innerDiameter * 0.5, innerRadius: innerDiameter * 0.5,
@ -705,10 +689,15 @@ final class PieChartComponent: Component {
} }
private final class ParticleSet { private final class ParticleSet {
private let innerRadius: CGFloat
private let maxRadius: CGFloat
private(set) var particles: [Particle] = [] private(set) var particles: [Particle] = []
init() { init(innerRadius: CGFloat, maxRadius: CGFloat, preAdvance: Bool) {
self.generateParticles(preAdvance: true) self.innerRadius = innerRadius
self.maxRadius = maxRadius
self.generateParticles(preAdvance: preAdvance)
} }
private func generateParticles(preAdvance: Bool) { private func generateParticles(preAdvance: Bool) {
@ -768,12 +757,13 @@ final class PieChartComponent: Component {
func update(deltaTime: CGFloat) { func update(deltaTime: CGFloat) {
let size = CGSize(width: 200.0, height: 200.0) 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() { for i in (0 ..< self.particles.count).reversed() {
self.particles[i].update(deltaTime: deltaTime) self.particles[i].update(deltaTime: deltaTime)
let position = self.particles[i].position 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) 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 { guard let particleImage = self.particleImage else {
return return
} }
@ -922,7 +912,93 @@ final class PieChartComponent: Component {
particleLayer.position = particle.position particleLayer.position = particle.position
particleLayer.transform = CATransform3DMakeScale(particle.scale, particle.scale, 1.0) 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 { if particleSet.particles.count < self.particleLayers.count {
for i in 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 var sectionLayers: [AnyHashable: SectionLayer] = [:]
private let particleSet: ParticleSet private let particleSet: ParticleSet
private var labels: [AnyHashable: ChartLabel] = [:] private var doneLayer: DoneLayer?
override init(frame: CGRect) { override init(frame: CGRect) {
self.particleSet = ParticleSet() self.particleSet = ParticleSet(innerRadius: 50.0, maxRadius: 100.0, preAdvance: true)
super.init(frame: frame) super.init(frame: frame)
@ -1008,6 +1084,7 @@ final class PieChartComponent: Component {
if self.theme !== theme || self.data != data || self.selectedKey != selectedKey { if self.theme !== theme || self.data != data || self.selectedKey != selectedKey {
self.theme = theme self.theme = theme
self.selectedKey = selectedKey self.selectedKey = selectedKey
let previousData = self.data
if animated, let previous = self.currentLayout { if animated, let previous = self.currentLayout {
var initialState = previous var initialState = previous
@ -1016,19 +1093,42 @@ final class PieChartComponent: Component {
let mappedProgress = listViewAnimationCurveSystem(CGFloat(currentProgress)) let mappedProgress = listViewAnimationCurveSystem(CGFloat(currentProgress))
initialState = CalculatedLayout(interpolating: currentAnimation.start, to: previous, progress: mappedProgress, size: previous.size) initialState = CalculatedLayout(interpolating: currentAnimation.start, to: previous, progress: mappedProgress, size: previous.size)
} }
let targetLayout = CalculatedLayout(
size: CGSize(width: 200.0, height: 200.0), let targetLayout: CalculatedLayout
items: data.items, if let previousData = previousData, data.items.isEmpty {
selectedKey: self.selectedKey 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.currentLayout = targetLayout
self.currentAnimation = (initialState, CACurrentMediaTime(), 0.4) self.currentAnimation = (initialState, CACurrentMediaTime(), 0.4)
} else { } else {
self.currentLayout = CalculatedLayout( if data.items.isEmpty {
size: CGSize(width: 200.0, height: 200.0), self.currentLayout = CalculatedLayout(
items: data.items, size: CGSize(width: 200.0, height: 200.0),
selectedKey: self.selectedKey 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 self.data = data
@ -1043,15 +1143,71 @@ final class PieChartComponent: Component {
var validIds: [AnyHashable] = [] var validIds: [AnyHashable] = []
if let currentLayout = self.currentLayout { if let currentLayout = self.currentLayout {
var effectiveLayout = 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 { if let currentAnimation = self.currentAnimation {
let currentProgress: Double = max(0.0, min(1.0, (CACurrentMediaTime() - currentAnimation.startTime) / currentAnimation.duration)) let currentProgress: Double = max(0.0, min(1.0, (CACurrentMediaTime() - currentAnimation.startTime) / currentAnimation.duration))
let mappedProgress = listViewAnimationCurveSystem(CGFloat(currentProgress)) let mappedProgress = listViewAnimationCurveSystem(CGFloat(currentProgress))
effectiveLayout = CalculatedLayout(interpolating: currentAnimation.start, to: currentLayout, progress: mappedProgress, size: currentLayout.size) 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 { if currentProgress >= 1.0 - CGFloat.ulpOfOne {
self.currentAnimation = nil 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 { for section in effectiveLayout.sections {
@ -1066,9 +1222,12 @@ final class PieChartComponent: Component {
self.layer.addSublayer(sectionLayer) 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.update(size: sectionLayer.bounds.size, section: section)
sectionLayer.updateParticles(particleSet: self.particleSet) sectionLayer.updateParticles(particleSet: self.particleSet, alpha: particleAlpha)
} }
} }

View File

@ -289,7 +289,11 @@ private final class PeerListItemComponent: Component {
} else { } else {
clipStyle = .round 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( let labelSize = self.label.update(
@ -582,13 +586,20 @@ final class StoragePeerListPanelComponent: Component {
itemSelectionState = .none 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( let _ = itemView.update(
transition: itemTransition, transition: itemTransition,
component: AnyComponent(PeerListItemComponent( component: AnyComponent(PeerListItemComponent(
context: component.context, context: component.context,
theme: environment.theme, theme: environment.theme,
sideInset: environment.containerInsets.left, sideInset: environment.containerInsets.left,
title: item.peer.displayTitle(strings: environment.strings, displayOrder: .firstLast), title: itemTitle,
peer: item.peer, peer: item.peer,
label: dataSizeString(item.size, formatting: dataSizeFormatting), label: dataSizeString(item.size, formatting: dataSizeFormatting),
selectionState: itemSelectionState, selectionState: itemSelectionState,

View File

@ -727,6 +727,8 @@ final class StorageUsageScreenComponent: Component {
private var doneStatusCircle: SimpleShapeLayer? private var doneStatusCircle: SimpleShapeLayer?
private var doneStatusNode: RadialStatusNode? private var doneStatusNode: RadialStatusNode?
private let scrollContainerView: UIView
private let pieChartView = ComponentView<Empty>() private let pieChartView = ComponentView<Empty>()
private let chartTotalLabel = ComponentView<Empty>() private let chartTotalLabel = ComponentView<Empty>()
private let categoriesView = ComponentView<Empty>() private let categoriesView = ComponentView<Empty>()
@ -777,6 +779,8 @@ final class StorageUsageScreenComponent: Component {
self.navigationSeparatorLayerContainer = SimpleLayer() self.navigationSeparatorLayerContainer = SimpleLayer()
self.navigationSeparatorLayerContainer.opacity = 0.0 self.navigationSeparatorLayerContainer.opacity = 0.0
self.scrollContainerView = UIView()
self.scrollView = ScrollViewImpl() self.scrollView = ScrollViewImpl()
self.keepDurationSectionContainerView = UIView() self.keepDurationSectionContainerView = UIView()
@ -805,7 +809,9 @@ final class StorageUsageScreenComponent: Component {
self.scrollView.clipsToBounds = true self.scrollView.clipsToBounds = true
self.addSubview(self.scrollView) 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.headerProgressBackgroundLayer)
self.scrollView.layer.addSublayer(self.headerProgressForegroundLayer) 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.scrollView, alpha: self.aggregatedData != nil ? 1.0 : 0.0)
alphaTransition.setAlpha(view: self.headerOffsetContainer, 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 { } else if case .clearedItems = animationHint.value {
if let snapshotView = self.snapshotView(afterScreenUpdates: false) { if let snapshotView = self.scrollContainerView.snapshotView(afterScreenUpdates: false) {
snapshotView.frame = self.bounds snapshotView.frame = self.scrollContainerView.frame
self.addSubview(snapshotView) 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.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { [weak snapshotView] _ in
snapshotView?.removeFromSuperview() snapshotView?.removeFromSuperview()
}) })
@ -1347,12 +1354,20 @@ final class StorageUsageScreenComponent: Component {
chartItems.append(chartItem) 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) let chartData = PieChartComponent.ChartData(items: chartItems)
self.pieChartView.parentState = state 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( let pieChartSize = self.pieChartView.update(
transition: transition, transition: pieChartTransition,
component: AnyComponent(PieChartComponent( component: AnyComponent(PieChartComponent(
theme: environment.theme, theme: environment.theme,
strings: environment.strings, strings: environment.strings,
@ -1367,8 +1382,8 @@ final class StorageUsageScreenComponent: Component {
self.scrollView.addSubview(pieChartComponentView) self.scrollView.addSubview(pieChartComponentView)
} }
transition.setFrame(view: pieChartComponentView, frame: pieChartFrame) pieChartTransition.setFrame(view: pieChartComponentView, frame: pieChartFrame)
transition.setAlpha(view: pieChartComponentView, alpha: listCategories.isEmpty ? 0.0 : 1.0) //transition.setAlpha(view: pieChartComponentView, alpha: listCategories.isEmpty ? 0.0 : 1.0)
} }
if let _ = self.aggregatedData, listCategories.isEmpty { if let _ = self.aggregatedData, listCategories.isEmpty {
let checkColor = UIColor(rgb: 0x34C759) let checkColor = UIColor(rgb: 0x34C759)
@ -1392,7 +1407,7 @@ final class StorageUsageScreenComponent: Component {
} else { } else {
doneStatusCircle = SimpleShapeLayer() doneStatusCircle = SimpleShapeLayer()
self.doneStatusCircle = doneStatusCircle self.doneStatusCircle = doneStatusCircle
self.scrollView.layer.addSublayer(doneStatusCircle) //self.scrollView.layer.addSublayer(doneStatusCircle)
doneStatusCircle.opacity = 0.0 doneStatusCircle.opacity = 0.0
} }
@ -1431,7 +1446,11 @@ final class StorageUsageScreenComponent: Component {
if listCategories.isEmpty { if listCategories.isEmpty {
headerText = environment.strings.StorageManagement_TitleCleared headerText = environment.strings.StorageManagement_TitleCleared
} else if let peer = component.peer { } 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 { } else {
headerText = environment.strings.StorageManagement_Title 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) let headerDescriptionFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - headerDescriptionSize.width) / 2.0), y: contentHeight), size: headerDescriptionSize)
if let headerDescriptionComponentView = self.headerDescriptionView.view { if let headerDescriptionComponentView = self.headerDescriptionView.view {
if headerDescriptionComponentView.superview == nil { if headerDescriptionComponentView.superview == nil {
self.scrollView.addSubview(headerDescriptionComponentView) self.scrollContainerView.addSubview(headerDescriptionComponentView)
} }
transition.setFrame(view: headerDescriptionComponentView, frame: headerDescriptionFrame) transition.setFrame(view: headerDescriptionComponentView, frame: headerDescriptionFrame)
} }
@ -1562,14 +1581,14 @@ final class StorageUsageScreenComponent: Component {
} else { } else {
chartAvatarNode = AvatarNode(font: avatarPlaceholderFont(size: 17.0)) chartAvatarNode = AvatarNode(font: avatarPlaceholderFont(size: 17.0))
self.chartAvatarNode = chartAvatarNode self.chartAvatarNode = chartAvatarNode
if let pieChartComponentView = self.pieChartView.view { self.scrollContainerView.addSubview(chartAvatarNode.view)
self.scrollView.insertSubview(chartAvatarNode.view, belowSubview: pieChartComponentView)
} else {
self.scrollView.addSubview(chartAvatarNode.view)
}
chartAvatarNode.frame = avatarFrame 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) transition.setAlpha(view: chartAvatarNode.view, alpha: listCategories.isEmpty ? 0.0 : 1.0)
} else { } else {
@ -1606,11 +1625,7 @@ final class StorageUsageScreenComponent: Component {
) )
if let chartTotalLabelView = self.chartTotalLabel.view { if let chartTotalLabelView = self.chartTotalLabel.view {
if chartTotalLabelView.superview == nil { if chartTotalLabelView.superview == nil {
if let pieChartComponentView = self.pieChartView.view { self.scrollContainerView.addSubview(chartTotalLabelView)
self.scrollView.insertSubview(chartTotalLabelView, belowSubview: pieChartComponentView)
} else {
self.scrollView.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) 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) transition.setFrame(view: chartTotalLabelView, frame: totalLabelFrame)
@ -1674,7 +1689,7 @@ final class StorageUsageScreenComponent: Component {
) )
if let categoriesComponentView = self.categoriesView.view { if let categoriesComponentView = self.categoriesView.view {
if categoriesComponentView.superview == nil { 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)) 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) let categoriesDescriptionFrame = CGRect(origin: CGPoint(x: sideInset + 15.0, y: contentHeight), size: categoriesDescriptionSize)
if let categoriesDescriptionComponentView = self.categoriesDescriptionView.view { if let categoriesDescriptionComponentView = self.categoriesDescriptionView.view {
if categoriesDescriptionComponentView.superview == nil { if categoriesDescriptionComponentView.superview == nil {
self.scrollView.addSubview(categoriesDescriptionComponentView) self.scrollContainerView.addSubview(categoriesDescriptionComponentView)
} }
transition.setFrame(view: categoriesDescriptionComponentView, frame: categoriesDescriptionFrame) 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) let keepDurationTitleFrame = CGRect(origin: CGPoint(x: sideInset + 15.0, y: contentHeight), size: keepDurationTitleSize)
if let keepDurationTitleComponentView = self.keepDurationTitleView.view { if let keepDurationTitleComponentView = self.keepDurationTitleView.view {
if keepDurationTitleComponentView.superview == nil { if keepDurationTitleComponentView.superview == nil {
self.scrollView.addSubview(keepDurationTitleComponentView) self.scrollContainerView.addSubview(keepDurationTitleComponentView)
} }
transition.setFrame(view: keepDurationTitleComponentView, frame: keepDurationTitleFrame) 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) let keepDurationDescriptionFrame = CGRect(origin: CGPoint(x: sideInset + 15.0, y: contentHeight), size: keepDurationDescriptionSize)
if let keepDurationDescriptionComponentView = self.keepDurationDescriptionView.view { if let keepDurationDescriptionComponentView = self.keepDurationDescriptionView.view {
if keepDurationDescriptionComponentView.superview == nil { if keepDurationDescriptionComponentView.superview == nil {
self.scrollView.addSubview(keepDurationDescriptionComponentView) self.scrollContainerView.addSubview(keepDurationDescriptionComponentView)
} }
transition.setFrame(view: keepDurationDescriptionComponentView, frame: keepDurationDescriptionFrame) 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) let keepSizeTitleFrame = CGRect(origin: CGPoint(x: sideInset + 15.0, y: contentHeight), size: keepSizeTitleSize)
if let keepSizeTitleComponentView = self.keepSizeTitleView.view { if let keepSizeTitleComponentView = self.keepSizeTitleView.view {
if keepSizeTitleComponentView.superview == nil { if keepSizeTitleComponentView.superview == nil {
self.scrollView.addSubview(keepSizeTitleComponentView) self.scrollContainerView.addSubview(keepSizeTitleComponentView)
} }
transition.setFrame(view: keepSizeTitleComponentView, frame: keepSizeTitleFrame) 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) let keepSizeFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: keepSizeSize)
if let keepSizeComponentView = self.keepSizeView.view { if let keepSizeComponentView = self.keepSizeView.view {
if keepSizeComponentView.superview == nil { if keepSizeComponentView.superview == nil {
self.scrollView.addSubview(keepSizeComponentView) self.scrollContainerView.addSubview(keepSizeComponentView)
} }
transition.setFrame(view: keepSizeComponentView, frame: keepSizeFrame) 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) let keepSizeDescriptionFrame = CGRect(origin: CGPoint(x: sideInset + 15.0, y: contentHeight), size: keepSizeDescriptionSize)
if let keepSizeDescriptionComponentView = self.keepSizeDescriptionView.view { if let keepSizeDescriptionComponentView = self.keepSizeDescriptionView.view {
if keepSizeDescriptionComponentView.superview == nil { if keepSizeDescriptionComponentView.superview == nil {
self.scrollView.addSubview(keepSizeDescriptionComponentView) self.scrollContainerView.addSubview(keepSizeDescriptionComponentView)
} }
transition.setFrame(view: keepSizeDescriptionComponentView, frame: keepSizeDescriptionFrame) transition.setFrame(view: keepSizeDescriptionComponentView, frame: keepSizeDescriptionFrame)
} }
@ -2129,7 +2144,7 @@ final class StorageUsageScreenComponent: Component {
) )
if let panelContainerView = self.panelContainer.view { if let panelContainerView = self.panelContainer.view {
if panelContainerView.superview == nil { 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)) 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 { if self.scrollView.contentSize != contentSize {
self.scrollView.contentSize = contentSize self.scrollView.contentSize = contentSize
} }
transition.setFrame(view: self.scrollContainerView, frame: CGRect(origin: CGPoint(), size: contentSize))
var scrollViewBounds = self.scrollView.bounds var scrollViewBounds = self.scrollView.bounds
scrollViewBounds.size = availableSize scrollViewBounds.size = availableSize
@ -2181,7 +2197,7 @@ final class StorageUsageScreenComponent: Component {
clearingNode.updateLayout(size: clearingSize, transition: .immediate) clearingNode.updateLayout(size: clearingSize, transition: .immediate)
if animateIn { 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 { } else {
if let clearingNode = self.clearingNode { if let clearingNode = self.clearingNode {
@ -2190,10 +2206,10 @@ final class StorageUsageScreenComponent: Component {
var delay: Double = 0.0 var delay: Double = 0.0
if let clearingDisplayTimestamp = self.clearingDisplayTimestamp { if let clearingDisplayTimestamp = self.clearingDisplayTimestamp {
let timeDelta = CFAbsoluteTimeGetCurrent() - clearingDisplayTimestamp let timeDelta = CFAbsoluteTimeGetCurrent() - clearingDisplayTimestamp
if timeDelta < 0.12 { if timeDelta < 0.4 {
delay = 0.0 delay = 0.0
} else if timeDelta < 0.4 { } else if timeDelta < 1.0 {
delay = 0.4 delay = 1.0
} }
} }
@ -2414,7 +2430,9 @@ final class StorageUsageScreenComponent: Component {
self.isClearing = false 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() completion()
}) })
@ -2468,8 +2486,21 @@ final class StorageUsageScreenComponent: Component {
var items: [ContextMenuItem] = [] 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( 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) }, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Expand"), color: theme.contextMenu.primaryColor) },
action: { [weak self] c, _ in action: { [weak self] c, _ in
c.dismiss(completion: { [weak self] in c.dismiss(completion: { [weak self] in

View File

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

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="72px" height="72px" viewBox="0 0 72 72" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>Icon / Video Copy 13</title>
<g id="Icon-/-Video-Copy-13" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<path d="M28.9214873,41.7868781 L14.2382898,37.778549 C13.2560244,37.5104028 12.6771163,36.4967439 12.9452625,35.5144784 C13.1170949,34.8850266 13.6088381,34.3932834 14.2382898,34.221451 L28.9214873,30.2131219 C29.5510238,30.0412663 30.0428079,29.5494165 30.2145795,28.9198571 L34.2213862,14.2345211 C34.4894013,13.2522198 35.5029829,12.6731764 36.4852842,12.9411915 C37.1149283,13.1129862 37.6068191,13.604877 37.7786138,14.2345211 L41.7854205,28.9198571 C41.9571921,29.5494165 42.4489762,30.0412663 43.0785127,30.2131219 L57.7617102,34.221451 C58.7439756,34.4895972 59.3228837,35.5032561 59.0547375,36.4855216 C58.8829051,37.1149734 58.3911619,37.6067166 57.7617102,37.778549 L43.0785127,41.7868781 C42.4489762,41.9587337 41.9571921,42.4505835 41.7854205,43.0801429 L37.7786138,57.7654789 C37.5105987,58.7477802 36.4970171,59.3268236 35.5147158,59.0588085 C34.8850717,58.8870138 34.3931809,58.395123 34.2213862,57.7654789 L30.2145795,43.0801429 C30.0428079,42.4505835 29.5510238,41.9587337 28.9214873,41.7868781 Z" id="Star-Copy-3" fill="#FFFFFF"></path>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -217,8 +217,13 @@ public final class AccountContextImpl: AccountContext {
self.cachedGroupCallContexts = AccountGroupCallContextCacheImpl() self.cachedGroupCallContexts = AccountGroupCallContextCacheImpl()
self.meshAnimationCache = MeshAnimationCache(mediaBox: account.postbox.mediaBox) 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: { self.animationCache = AnimationCacheImpl(basePath: self.account.postbox.mediaBox.basePath + "/animation-cache", allocateTempFile: {
return TempBox.shared.tempFile(fileName: "file").path 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() self.animationRenderer = MultiAnimationRendererImpl()

View File

@ -1306,26 +1306,10 @@ private func extractAccountManagerState(records: AccountRecordsView<TelegramAcco
BGTaskScheduler.shared.register(forTaskWithIdentifier: taskId, using: DispatchQueue.main) { task in BGTaskScheduler.shared.register(forTaskWithIdentifier: taskId, using: DispatchQueue.main) { task in
Logger.shared.log("App \(self.episodeId)", "Executing cleanup task") Logger.shared.log("App \(self.episodeId)", "Executing cleanup task")
let disposable = MetaDisposable() let disposable = self.runCacheReindexTasks(lowImpact: true, completion: {
Logger.shared.log("App \(self.episodeId)", "Completed cleanup task")
let _ = (self.sharedContextPromise.get()
|> take(1) task.setTaskCompleted(success: true)
|> deliverOnMainQueue).start(next: { sharedApplicationContext in
let _ = (sharedApplicationContext.sharedContext.activeAccountContexts
|> take(1)
|> deliverOnMainQueue).start(next: { activeAccounts in
var signals: Signal<Never, NoError> = .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)
}))
})
}) })
task.expirationHandler = { task.expirationHandler = {
@ -1351,8 +1335,43 @@ private func extractAccountManagerState(records: AccountRecordsView<TelegramAcco
}) })
} }
let timestamp = Int(CFAbsoluteTimeGetCurrent())
let minReindexTimestamp = timestamp - 2 * 24 * 60 * 60
if let indexTimestamp = UserDefaults.standard.object(forKey: "TelegramCacheIndexTimestamp") as? NSNumber, indexTimestamp.intValue >= 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 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<Never, NoError> = .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() { private func resetBadge() {
var resetOnce = true var resetOnce = true