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

View File

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

View File

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

View File

@ -145,6 +145,7 @@ public final class MediaBox {
private let timeBasedCleanup: TimeBasedCleanup
public let storageBox: StorageBox
public let cacheStorageBox: StorageBox
private let didRemoveResourcesPipe = ValuePipe<Void>()
public var didRemoveResources: Signal<Void, NoError> {
@ -192,6 +193,9 @@ public final class MediaBox {
self.storageBox = StorageBox(logger: StorageBox.Logger(impl: { string in
postboxLog(string)
}), basePath: basePath + "/storage")
self.cacheStorageBox = StorageBox(logger: StorageBox.Logger(impl: { string in
postboxLog(string)
}), basePath: basePath + "/cache-storage")
self.timeBasedCleanup = TimeBasedCleanup(storageBox: self.storageBox, generalPaths: [
self.basePath + "/cache",
@ -878,6 +882,9 @@ public final class MediaBox {
public func storeCachedResourceRepresentation(_ resource: MediaResource, representation: CachedMediaResourceRepresentation, data: Data) {
self.dataQueue.async {
let path = self.cachedRepresentationPathsForId(resource.id.stringRepresentation, representationId: representation.uniqueId, keepDuration: representation.keepDuration).complete
if let pathData = path.data(using: .utf8) {
self.cacheStorageBox.update(id: pathData, size: Int64(data.count))
}
let _ = try? data.write(to: URL(fileURLWithPath: path))
}
}
@ -885,6 +892,9 @@ public final class MediaBox {
public func storeCachedResourceRepresentation(_ resource: MediaResource, representationId: String, keepDuration: CachedMediaRepresentationKeepDuration, data: Data, completion: @escaping (String) -> Void = { _ in }) {
self.dataQueue.async {
let path = self.cachedRepresentationPathsForId(resource.id.stringRepresentation, representationId: representationId, keepDuration: keepDuration).complete
if let pathData = path.data(using: .utf8) {
self.cacheStorageBox.update(id: pathData, size: Int64(data.count))
}
let _ = try? data.write(to: URL(fileURLWithPath: path))
completion(path)
}
@ -894,6 +904,9 @@ public final class MediaBox {
self.dataQueue.async {
let path = self.cachedRepresentationPathsForId(resourceId, representationId: representationId, keepDuration: keepDuration).complete
let _ = try? data.write(to: URL(fileURLWithPath: path))
if let pathData = path.data(using: .utf8) {
self.cacheStorageBox.update(id: pathData, size: Int64(data.count))
}
completion(path)
}
}
@ -902,6 +915,9 @@ public final class MediaBox {
self.dataQueue.async {
let path = self.cachedRepresentationPathsForId(resourceId, representationId: representationId, keepDuration: keepDuration).complete
let _ = try? FileManager.default.moveItem(atPath: tempFile.path, toPath: path)
if let fileSize = fileSize(path), fileSize != 0, let pathData = path.data(using: .utf8) {
self.cacheStorageBox.update(id: pathData, size: fileSize)
}
completion(path)
}
}
@ -983,6 +999,7 @@ public final class MediaBox {
if !context.initialized {
context.initialized = true
let cacheStorageBox = self.cacheStorageBox
let signal = self.wrappedFetchCachedResourceRepresentation.get()
|> take(1)
|> mapToSignal { fetch in
@ -999,15 +1016,24 @@ public final class MediaBox {
switch next {
case let .temporaryPath(temporaryPath):
rename(temporaryPath, paths.complete)
if let size = fileSize(paths.complete), let pathData = paths.complete.data(using: .utf8) {
cacheStorageBox.update(id: pathData, size: size)
}
isDone = true
case let .tempFile(tempFile):
rename(tempFile.path, paths.complete)
TempBox.shared.dispose(tempFile)
if let size = fileSize(paths.complete), let pathData = paths.complete.data(using: .utf8) {
cacheStorageBox.update(id: pathData, size: size)
}
isDone = true
case .reset:
let file = ManagedFile(queue: strongSelf.dataQueue, path: paths.partial, mode: .readwrite)
file?.truncate(count: 0)
unlink(paths.complete)
if let pathData = paths.complete.data(using: .utf8) {
cacheStorageBox.update(id: pathData, size: 0)
}
case let .data(dataPart):
let file = ManagedFile(queue: strongSelf.dataQueue, path: paths.partial, mode: .append)
let dataCount = dataPart.count
@ -1015,8 +1041,14 @@ public final class MediaBox {
let bytes = rawBytes.baseAddress!.assumingMemoryBound(to: UInt8.self)
let _ = file?.write(bytes, count: dataCount)
}
if let file = file, let size = file.getSize(), let pathData = paths.complete.data(using: .utf8) {
cacheStorageBox.update(id: pathData, size: size)
}
case .done:
link(paths.partial, paths.complete)
if let size = fileSize(paths.complete), let pathData = paths.complete.data(using: .utf8) {
cacheStorageBox.update(id: pathData, size: size)
}
isDone = true
}
@ -1155,6 +1187,7 @@ public final class MediaBox {
if !context.initialized {
context.initialized = true
let cacheStorageBox = self.cacheStorageBox
let signal = fetch()
|> deliverOn(self.dataQueue)
context.disposable.set(signal.start(next: { [weak self, weak context] next in
@ -1165,15 +1198,24 @@ public final class MediaBox {
switch next {
case let .temporaryPath(temporaryPath):
rename(temporaryPath, paths.complete)
if let size = fileSize(paths.complete), let pathData = paths.complete.data(using: .utf8) {
cacheStorageBox.update(id: pathData, size: size)
}
isDone = true
case let .tempFile(tempFile):
rename(tempFile.path, paths.complete)
TempBox.shared.dispose(tempFile)
if let size = fileSize(paths.complete), let pathData = paths.complete.data(using: .utf8) {
cacheStorageBox.update(id: pathData, size: size)
}
isDone = true
case .reset:
let file = ManagedFile(queue: strongSelf.dataQueue, path: paths.partial, mode: .readwrite)
file?.truncate(count: 0)
unlink(paths.complete)
if let pathData = paths.complete.data(using: .utf8) {
cacheStorageBox.update(id: pathData, size: 0)
}
case let .data(dataPart):
let file = ManagedFile(queue: strongSelf.dataQueue, path: paths.partial, mode: .append)
let dataCount = dataPart.count
@ -1181,9 +1223,15 @@ public final class MediaBox {
let bytes = rawBytes.baseAddress!.assumingMemoryBound(to: UInt8.self)
let _ = file?.write(bytes, count: dataCount)
}
if let file = file, let size = file.getSize(), let pathData = paths.complete.data(using: .utf8) {
cacheStorageBox.update(id: pathData, size: size)
}
case .done:
link(paths.partial, paths.complete)
isDone = true
if let size = fileSize(paths.complete), let pathData = paths.complete.data(using: .utf8) {
cacheStorageBox.update(id: pathData, size: size)
}
}
if let strongSelf = self, let currentContext = strongSelf.cachedRepresentationContexts[key], currentContext === context {
@ -1277,7 +1325,7 @@ public final class MediaBox {
}
}
public func updateResourceIndex(lowImpact: Bool, completion: @escaping () -> Void) -> Disposable {
private func updateGeneralResourceIndex(lowImpact: Bool, completion: @escaping () -> Void) -> Disposable {
let basePath = self.basePath
let storageBox = self.storageBox
@ -1370,6 +1418,104 @@ public final class MediaBox {
}
}
/*private func updateCacheResourceIndex(pathPrefix: String, lowImpact: Bool, completion: @escaping () -> Void) -> Disposable {
let cacheStorageBox = self.cacheStorageBox
var isCancelled: Bool = false
let processQueue = Queue(name: "UpdateResourceIndex", qos: .background)
processQueue.async {
if isCancelled {
return
}
let scanContext = ScanFilesContext(path: pathPrefix)
func processStale(nextId: Data?) {
let _ = (storageBox.enumerateItems(startingWith: nextId, limit: 1000)
|> deliverOn(processQueue)).start(next: { ids, realNextId in
var staleIds: [Data] = []
for id in ids {
if let name = String(data: id, encoding: .utf8) {
if self.resourceUsage(id: MediaResourceId(name)) == 0 {
staleIds.append(id)
}
} else {
staleIds.append(id)
}
}
if !staleIds.isEmpty {
storageBox.remove(ids: staleIds)
}
if realNextId == nil {
completion()
} else {
if lowImpact {
processQueue.after(0.4, {
processStale(nextId: realNextId)
})
} else {
processStale(nextId: realNextId)
}
}
})
}
func processNext() {
processQueue.async {
if isCancelled {
return
}
let results = scanContext.nextBatch(count: 32000)
if results.isEmpty {
processStale(nextId: nil)
return
}
storageBox.addEmptyReferencesIfNotReferenced(ids: results.map { name -> (id: Data, size: Int64) in
let resourceId = MediaBox.idForFileName(name: name)
let paths = self.storePathsForId(MediaResourceId(resourceId))
var size: Int64 = 0
if let value = fileSize(paths.complete) {
size = value
} else if let value = fileSize(paths.partial) {
size = value
}
return (resourceId.data(using: .utf8)!, size)
}, contentType: MediaResourceUserContentType.other.rawValue, completion: { addedCount in
if addedCount != 0 {
postboxLog("UpdateResourceIndex: added \(addedCount) unreferenced ids")
}
if lowImpact {
processQueue.after(0.4, {
processNext()
})
} else {
processNext()
}
})
}
}
processNext()
}
return ActionDisposable {
isCancelled = true
}
}*/
public func updateResourceIndex(lowImpact: Bool, completion: @escaping () -> Void) -> Disposable {
return self.updateGeneralResourceIndex(lowImpact: lowImpact, completion: {
completion()
})
}
public func collectAllResourceUsage() -> Signal<[(id: String?, path: String, size: Int64)], NoError> {
return Signal { subscriber in
self.dataQueue.async {
@ -1396,66 +1542,6 @@ public final class MediaBox {
}
}
/*var cacheResult: Int64 = 0
var excludePrefixes = Set<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.putCompletion()
}

View File

@ -472,6 +472,8 @@ public final class StorageBox {
for peerId in self.peerIdsReferencing(hashId: hashId) {
self.internalAddSize(peerId: peerId, contentType: info.contentType, delta: sizeDelta)
}
} else {
self.internalAdd(reference: StorageBox.Reference(peerId: 0, messageNamespace: 0, messageId: 0), to: id, contentType: 0, size: size)
}
self.valueBox.commit()

View File

@ -449,10 +449,27 @@ private func cleanupAccount(networkArguments: NetworkInitializationArguments, ac
break
}
var cloudValue: [Data] = []
if let list = NSUbiquitousKeyValueStore.default.object(forKey: "T_SLTokens") as? [String] {
cloudValue = list.compactMap { string -> Data? in
guard let stringData = string.data(using: .utf8) else {
return nil
}
return Data(base64Encoded: stringData)
}
}
for data in cloudValue {
if !tokens.contains(data) {
tokens.insert(data, at: 0)
}
}
if tokens.count > 20 {
tokens.removeLast(tokens.count - 20)
}
NSUbiquitousKeyValueStore.default.set(tokens.map { $0.base64EncodedString() }, forKey: "T_SLTokens")
NSUbiquitousKeyValueStore.default.synchronize()
transaction.setStoredLoginTokens(tokens)
}).start()
account.shouldBeServiceTaskMaster.set(.single(.never))

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> {
var cloudValue: [Data] = []
if let list = NSUbiquitousKeyValueStore.default.object(forKey: "T_SLTokens") as? [String] {
cloudValue = list.compactMap { string -> Data? in
guard let stringData = string.data(using: .utf8) else {
return nil
}
return Data(base64Encoded: stringData)
}
}
return accountManager.transaction { transaction -> [Data] in
return transaction.getStoredLoginTokens()
}
|> castError(AuthorizationCodeRequestError.self)
|> mapToSignal { authTokens -> Signal<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
flags |= 1 << 5 //allowMissedCall
flags |= 1 << 6 //tokens

View File

@ -123,10 +123,19 @@ private extension StorageUsageStats {
mappedCategory = .avatars
case MediaResourceUserContentType.sticker.rawValue:
mappedCategory = .stickers
case MediaResourceUserContentType.other.rawValue:
mappedCategory = .misc
case MediaResourceUserContentType.audioVideoMessage.rawValue:
mappedCategory = .misc
default:
mappedCategory = .misc
}
if mappedCategories[mappedCategory] == nil {
mappedCategories[mappedCategory] = StorageUsageStats.CategoryData(size: value.size, messages: value.messages)
} else {
mappedCategories[mappedCategory]?.size += value.size
mappedCategories[mappedCategory]?.messages.merge(value.messages, uniquingKeysWith: { lhs, _ in lhs})
}
}
self.init(categories: mappedCategories)
@ -134,7 +143,7 @@ private extension StorageUsageStats {
}
func _internal_collectStorageUsageStats(account: Account) -> Signal<AllStorageUsageStats, NoError> {
let additionalStats = Signal<Int64, NoError> { subscriber in
/*let additionalStats = Signal<Int64, NoError> { subscriber in
DispatchQueue.global().async {
var totalSize: Int64 = 0
@ -207,7 +216,9 @@ func _internal_collectStorageUsageStats(account: Account) -> Signal<AllStorageUs
}
return EmptyDisposable
}
}*/
let additionalStats = account.postbox.mediaBox.cacheStorageBox.totalSize() |> take(1)
return combineLatest(
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> {
return account.postbox.transaction { transaction -> [EngineMessage.Id: Message] in
var result: [EngineMessage.Id: Message] = [:]
var peerInChatList: [EnginePeer.Id: Bool] = [:]
for (category, value) in stats.categories {
if !categories.contains(category) {
continue
@ -273,12 +285,27 @@ func _internal_renderStorageUsageStatsMessages(account: Account, stats: StorageU
if result[id] == nil {
if let message = existingMessages[id] {
result[id] = message
} else if let message = transaction.getMessage(id) {
} else {
var matches = false
if let peerInChatListValue = peerInChatList[id.peerId] {
if peerInChatListValue {
matches = true
}
} else {
let peerInChatListValue = transaction.getPeerChatListIndex(id.peerId) != nil
peerInChatList[id.peerId] = peerInChatListValue
if peerInChatListValue {
matches = true
}
}
if matches, let message = transaction.getMessage(id) {
result[id] = message
}
}
}
}
}
return result
}
@ -327,6 +354,9 @@ func _internal_clearStorage(account: Account, peerId: EnginePeer.Id?, categories
case .misc:
mappedContentTypes.append(MediaResourceUserContentType.other.rawValue)
mappedContentTypes.append(MediaResourceUserContentType.audioVideoMessage.rawValue)
// Legacy value for Gif
mappedContentTypes.append(5)
}
}
@ -357,6 +387,8 @@ func _internal_clearStorage(account: Account, peerId: EnginePeer.Id?, categories
}
}
mediaBox.cacheStorageBox.reset()
subscriber.putCompletion()
} else {
subscriber.putCompletion()

View File

@ -165,6 +165,7 @@ private enum ApplicationSpecificGlobalNotice: Int32 {
case forcedPasswordSetup = 31
case emojiTooltip = 32
case audioTranscriptionSuggestion = 33
case clearStorageDismissedTipSize = 34
var key: ValueBoxKey {
let v = ValueBoxKey(length: 4)
@ -349,6 +350,10 @@ private struct ApplicationSpecificNoticeKeys {
static func audioTranscriptionSuggestion() -> NoticeEntryKey {
return NoticeEntryKey(namespace: noticeNamespace(namespace: globalNamespace), key: ApplicationSpecificGlobalNotice.audioTranscriptionSuggestion.key)
}
static func clearStorageDismissedTipSize() -> NoticeEntryKey {
return NoticeEntryKey(namespace: noticeNamespace(namespace: globalNamespace), key: ApplicationSpecificGlobalNotice.clearStorageDismissedTipSize.key)
}
}
public struct ApplicationSpecificNotice {
@ -1087,6 +1092,25 @@ public struct ApplicationSpecificNotice {
}
}
public static func getClearStorageDismissedTipSize(accountManager: AccountManager<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> {
return accountManager.transaction { transaction -> (Int32, Int32) in
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
}
private func fileSize(_ path: String, useTotalFileAllocatedSize: Bool = false) -> Int64? {
if useTotalFileAllocatedSize {
let url = URL(fileURLWithPath: path)
if let values = (try? url.resourceValues(forKeys: Set([.isRegularFileKey, .totalFileAllocatedSizeKey]))) {
if values.isRegularFile ?? false {
if let fileSize = values.totalFileAllocatedSize {
return Int64(fileSize)
}
}
}
}
var value = stat()
if stat(path, &value) == 0 {
return value.st_size
} else {
return nil
}
}
public final class AnimationCacheItemFrame {
public enum RequestedFormat {
case rgba
@ -1246,7 +1266,7 @@ private func loadItem(path: String) throws -> AnimationCacheItem {
})
}
private func adaptItemFromHigherResolution(currentQueue: Queue, itemPath: String, width: Int, height: Int, itemDirectoryPath: String, higherResolutionPath: String, allocateTempFile: @escaping () -> String) -> AnimationCacheItem? {
private func adaptItemFromHigherResolution(currentQueue: Queue, itemPath: String, width: Int, height: Int, itemDirectoryPath: String, higherResolutionPath: String, allocateTempFile: @escaping () -> String, updateStorageStats: @escaping (String, Int64) -> Void) -> AnimationCacheItem? {
guard let higherResolutionItem = try? loadItem(path: higherResolutionPath) else {
return nil
}
@ -1286,6 +1306,10 @@ private func adaptItemFromHigherResolution(currentQueue: Queue, itemPath: String
guard let _ = try? FileManager.default.moveItem(atPath: result.animationPath, toPath: itemPath) else {
return nil
}
if let size = fileSize(itemPath) {
updateStorageStats(itemPath, size)
}
guard let item = try? loadItem(path: itemPath) else {
return nil
}
@ -1295,7 +1319,7 @@ private func adaptItemFromHigherResolution(currentQueue: Queue, itemPath: String
}
}
private func generateFirstFrameFromItem(currentQueue: Queue, itemPath: String, animationItemPath: String, allocateTempFile: @escaping () -> String) -> Bool {
private func generateFirstFrameFromItem(currentQueue: Queue, itemPath: String, animationItemPath: String, allocateTempFile: @escaping () -> String, updateStorageStats: @escaping (String, Int64) -> Void) -> Bool {
guard let animationItem = try? loadItem(path: animationItemPath) else {
return false
}
@ -1337,6 +1361,9 @@ private func generateFirstFrameFromItem(currentQueue: Queue, itemPath: String, a
guard let _ = try? FileManager.default.moveItem(atPath: result.animationPath, toPath: itemPath) else {
return false
}
if let size = fileSize(itemPath) {
updateStorageStats(itemPath, size)
}
return true
} catch {
return false
@ -1408,13 +1435,14 @@ public final class AnimationCacheImpl: AnimationCache {
private let queue: Queue
private let basePath: String
private let allocateTempFile: () -> String
private let updateStorageStats: (String, Int64) -> Void
private let fetchQueues: [Queue]
private var nextFetchQueueIndex: Int = 0
private var itemContexts: [ItemKey: ItemContext] = [:]
init(queue: Queue, basePath: String, allocateTempFile: @escaping () -> String) {
init(queue: Queue, basePath: String, allocateTempFile: @escaping () -> String, updateStorageStats: @escaping (String, Int64) -> Void) {
self.queue = queue
let fetchQueueCount: Int
@ -1427,6 +1455,7 @@ public final class AnimationCacheImpl: AnimationCache {
self.fetchQueues = (0 ..< fetchQueueCount).map { i in Queue(name: "AnimationCacheImpl-Fetch\(i)", qos: .default) }
self.basePath = basePath
self.allocateTempFile = allocateTempFile
self.updateStorageStats = updateStorageStats
}
deinit {
@ -1464,6 +1493,7 @@ public final class AnimationCacheImpl: AnimationCache {
let fetchQueueIndex = self.nextFetchQueueIndex
self.nextFetchQueueIndex += 1
let allocateTempFile = self.allocateTempFile
let updateStorageStats = self.updateStorageStats
guard let writer = AnimationCacheItemWriterImpl(queue: self.fetchQueues[fetchQueueIndex % self.fetchQueues.count], allocateTempFile: self.allocateTempFile, completion: { [weak self, weak itemContext] result in
queue.async {
guard let strongSelf = self, let itemContext = itemContext, itemContext === strongSelf.itemContexts[key] else {
@ -1482,8 +1512,11 @@ public final class AnimationCacheImpl: AnimationCache {
guard let _ = try? FileManager.default.moveItem(atPath: result.animationPath, toPath: itemPath) else {
return
}
if let size = fileSize(itemPath) {
updateStorageStats(itemPath, size)
}
let _ = generateFirstFrameFromItem(currentQueue: queue, itemPath: itemFirstFramePath, animationItemPath: itemPath, allocateTempFile: allocateTempFile)
let _ = generateFirstFrameFromItem(currentQueue: queue, itemPath: itemFirstFramePath, animationItemPath: itemPath, allocateTempFile: allocateTempFile, updateStorageStats: updateStorageStats)
for f in itemContext.subscribers.copyItems() {
guard let item = try? loadItem(path: itemPath) else {
@ -1522,7 +1555,7 @@ public final class AnimationCacheImpl: AnimationCache {
}
}
static func getFirstFrameSynchronously(basePath: String, sourceId: String, size: CGSize, allocateTempFile: @escaping () -> String) -> AnimationCacheItem? {
static func getFirstFrameSynchronously(basePath: String, sourceId: String, size: CGSize, allocateTempFile: @escaping () -> String, updateStorageStats: @escaping (String, Int64) -> Void) -> AnimationCacheItem? {
let hashString = md5Hash(sourceId)
let sourceIdPath = itemSubpath(hashString: hashString, width: Int(size.width), height: Int(size.height))
let itemDirectoryPath = "\(basePath)/\(sourceIdPath.directory)"
@ -1535,7 +1568,7 @@ public final class AnimationCacheImpl: AnimationCache {
}
if let adaptationItemPath = findHigherResolutionFileForAdaptation(itemDirectoryPath: itemDirectoryPath, baseName: "\(hashString)_", baseSuffix: "-f", width: Int(size.width), height: Int(size.height)) {
if let adaptedItem = adaptItemFromHigherResolution(currentQueue: .mainQueue(), itemPath: itemFirstFramePath, width: Int(size.width), height: Int(size.height), itemDirectoryPath: itemDirectoryPath, higherResolutionPath: adaptationItemPath, allocateTempFile: allocateTempFile) {
if let adaptedItem = adaptItemFromHigherResolution(currentQueue: .mainQueue(), itemPath: itemFirstFramePath, width: Int(size.width), height: Int(size.height), itemDirectoryPath: itemDirectoryPath, higherResolutionPath: adaptationItemPath, allocateTempFile: allocateTempFile, updateStorageStats: updateStorageStats) {
return adaptedItem
}
}
@ -1543,7 +1576,7 @@ public final class AnimationCacheImpl: AnimationCache {
return nil
}
static func getFirstFrame(queue: Queue, basePath: String, sourceId: String, size: CGSize, allocateTempFile: @escaping () -> String, fetch: ((AnimationCacheFetchOptions) -> Disposable)?, completion: @escaping (AnimationCacheItemResult) -> Void) -> Disposable {
static func getFirstFrame(queue: Queue, basePath: String, sourceId: String, size: CGSize, allocateTempFile: @escaping () -> String, updateStorageStats: @escaping (String, Int64) -> Void, fetch: ((AnimationCacheFetchOptions) -> Disposable)?, completion: @escaping (AnimationCacheItemResult) -> Void) -> Disposable {
let hashString = md5Hash(sourceId)
let sourceIdPath = itemSubpath(hashString: hashString, width: Int(size.width), height: Int(size.height))
let itemDirectoryPath = "\(basePath)/\(sourceIdPath.directory)"
@ -1555,7 +1588,7 @@ public final class AnimationCacheImpl: AnimationCache {
}
if let adaptationItemPath = findHigherResolutionFileForAdaptation(itemDirectoryPath: itemDirectoryPath, baseName: "\(hashString)_", baseSuffix: "-f", width: Int(size.width), height: Int(size.height)) {
if let adaptedItem = adaptItemFromHigherResolution(currentQueue: .mainQueue(), itemPath: itemFirstFramePath, width: Int(size.width), height: Int(size.height), itemDirectoryPath: itemDirectoryPath, higherResolutionPath: adaptationItemPath, allocateTempFile: allocateTempFile) {
if let adaptedItem = adaptItemFromHigherResolution(currentQueue: .mainQueue(), itemPath: itemFirstFramePath, width: Int(size.width), height: Int(size.height), itemDirectoryPath: itemDirectoryPath, higherResolutionPath: adaptationItemPath, allocateTempFile: allocateTempFile, updateStorageStats: updateStorageStats) {
completion(AnimationCacheItemResult(item: adaptedItem, isFinal: true))
return EmptyDisposable
}
@ -1579,6 +1612,9 @@ public final class AnimationCacheImpl: AnimationCache {
completion(AnimationCacheItemResult(item: nil, isFinal: true))
return
}
if let size = fileSize(itemFirstFramePath) {
updateStorageStats(itemFirstFramePath, size)
}
guard let item = try? loadItem(path: itemFirstFramePath) else {
completion(AnimationCacheItemResult(item: nil, isFinal: true))
return
@ -1604,14 +1640,16 @@ public final class AnimationCacheImpl: AnimationCache {
private let basePath: String
private let impl: QueueLocalObject<Impl>
private let allocateTempFile: () -> String
private let updateStorageStats: (String, Int64) -> Void
public init(basePath: String, allocateTempFile: @escaping () -> String) {
public init(basePath: String, allocateTempFile: @escaping () -> String, updateStorageStats: @escaping (String, Int64) -> Void) {
let queue = Queue()
self.queue = queue
self.basePath = basePath
self.allocateTempFile = allocateTempFile
self.updateStorageStats = updateStorageStats
self.impl = QueueLocalObject(queue: queue, generate: {
return Impl(queue: queue, basePath: basePath, allocateTempFile: allocateTempFile)
return Impl(queue: queue, basePath: basePath, allocateTempFile: allocateTempFile, updateStorageStats: updateStorageStats)
})
}
@ -1634,7 +1672,7 @@ public final class AnimationCacheImpl: AnimationCache {
}
public func getFirstFrameSynchronously(sourceId: String, size: CGSize) -> AnimationCacheItem? {
return Impl.getFirstFrameSynchronously(basePath: self.basePath, sourceId: sourceId, size: size, allocateTempFile: self.allocateTempFile)
return Impl.getFirstFrameSynchronously(basePath: self.basePath, sourceId: sourceId, size: size, allocateTempFile: self.allocateTempFile, updateStorageStats: self.updateStorageStats)
}
public func getFirstFrame(queue: Queue, sourceId: String, size: CGSize, fetch: ((AnimationCacheFetchOptions) -> Disposable)?, completion: @escaping (AnimationCacheItemResult) -> Void) -> Disposable {
@ -1642,8 +1680,9 @@ public final class AnimationCacheImpl: AnimationCache {
let basePath = self.basePath
let allocateTempFile = self.allocateTempFile
let updateStorageStats = self.updateStorageStats
queue.async {
disposable.set(Impl.getFirstFrame(queue: queue, basePath: basePath, sourceId: sourceId, size: size, allocateTempFile: allocateTempFile, fetch: fetch, completion: completion))
disposable.set(Impl.getFirstFrame(queue: queue, basePath: basePath, sourceId: sourceId, size: size, allocateTempFile: allocateTempFile, updateStorageStats: updateStorageStats, fetch: fetch, completion: completion))
}
return disposable

View File

@ -332,15 +332,18 @@ final class PieChartComponent: Component {
private struct CalculatedLayout {
var size: CGSize
var sections: [CalculatedSection]
var isEmpty: Bool
init(size: CGSize, sections: [CalculatedSection]) {
self.size = size
self.sections = sections
self.isEmpty = sections.isEmpty
}
init(interpolating start: CalculatedLayout, to end: CalculatedLayout, progress: CGFloat, size: CGSize) {
self.size = size
self.sections = []
self.isEmpty = end.isEmpty
for i in 0 ..< end.sections.count {
let right = end.sections[i]
@ -370,16 +373,17 @@ final class PieChartComponent: Component {
}
}
init(size: CGSize, items: [ChartData.Item], selectedKey: AnyHashable?) {
init(size: CGSize, items: [ChartData.Item], selectedKey: AnyHashable?, isEmpty: Bool) {
self.size = size
self.sections = []
self.isEmpty = isEmpty
if items.isEmpty {
return
}
let innerDiameter: CGFloat = 100.0
let spacing: CGFloat = 2.0
let innerDiameter: CGFloat = isEmpty ? 90.0 : 100.0
let spacing: CGFloat = isEmpty ? -0.5 : 2.0
let innerAngleSpacing: CGFloat = spacing / (innerDiameter * 0.5)
var angles: [Double] = []
@ -389,8 +393,8 @@ final class PieChartComponent: Component {
angles.append(angle)
}
let diameter: CGFloat = 200.0
let reducedDiameter: CGFloat = 170.0
let diameter: CGFloat = isEmpty ? (innerDiameter + 6.0 * 2.0) : 200.0
let reducedDiameter: CGFloat = floor(0.85 * diameter)
var anglesData: [ItemAngleData] = []
@ -413,30 +417,8 @@ final class PieChartComponent: Component {
let angleValue: CGFloat = angles[i]
var beforeSpacingFraction: CGFloat = 1.0
var afterSpacingFraction: CGFloat = 1.0
if item.mergeable {
let previousItem: ChartData.Item
if i == 0 {
previousItem = items[items.count - 1]
} else {
previousItem = items[i - 1]
}
let nextItem: ChartData.Item
if i == items.count - 1 {
nextItem = items[0]
} else {
nextItem = items[i + 1]
}
if previousItem.mergeable {
beforeSpacingFraction = item.mergeFactor * 1.0 + (1.0 - item.mergeFactor) * (-0.2)
}
if nextItem.mergeable {
afterSpacingFraction = item.mergeFactor * 1.0 + (1.0 - item.mergeFactor) * (-0.2)
}
}
let beforeSpacingFraction: CGFloat = 1.0
let afterSpacingFraction: CGFloat = 1.0
let innerStartAngle = startAngle + innerAngleSpacing * 0.5
let arcInnerStartAngle = startAngle + innerAngleSpacing * 0.5 * beforeSpacingFraction
@ -453,9 +435,11 @@ final class PieChartComponent: Component {
var arcOuterEndAngle = startAngle + angleValue - angleSpacing * 0.5 * afterSpacingFraction
arcOuterEndAngle = max(arcOuterEndAngle, arcOuterStartAngle)
let itemColor: UIColor = isEmpty ? UIColor(rgb: 0x34C759) : item.color
self.sections.append(CalculatedSection(
id: item.id,
color: item.color,
color: itemColor,
innerAngle: arcInnerStartAngle ..< arcInnerEndAngle,
outerAngle: arcOuterStartAngle ..< arcOuterEndAngle,
innerRadius: innerDiameter * 0.5,
@ -705,10 +689,15 @@ final class PieChartComponent: Component {
}
private final class ParticleSet {
private let innerRadius: CGFloat
private let maxRadius: CGFloat
private(set) var particles: [Particle] = []
init() {
self.generateParticles(preAdvance: true)
init(innerRadius: CGFloat, maxRadius: CGFloat, preAdvance: Bool) {
self.innerRadius = innerRadius
self.maxRadius = maxRadius
self.generateParticles(preAdvance: preAdvance)
}
private func generateParticles(preAdvance: Bool) {
@ -768,12 +757,13 @@ final class PieChartComponent: Component {
func update(deltaTime: CGFloat) {
let size = CGSize(width: 200.0, height: 200.0)
let radius2 = pow(size.width * 0.5 + 10.0, 2.0)
let radius = size.width * 0.5 + 10.0
for i in (0 ..< self.particles.count).reversed() {
self.particles[i].update(deltaTime: deltaTime)
let position = self.particles[i].position
if pow(position.x - size.width * 0.5, 2.0) + pow(position.y - size.height * 0.5, 2.0) > radius2 {
let distance = sqrt(pow(position.x - size.width * 0.5, 2.0) + pow(position.y - size.height * 0.5, 2.0))
if distance > radius {
self.particles.remove(at: i)
}
}
@ -901,7 +891,7 @@ final class PieChartComponent: Component {
}
}
func updateParticles(particleSet: ParticleSet) {
func updateParticles(particleSet: ParticleSet, alpha: CGFloat) {
guard let particleImage = self.particleImage else {
return
}
@ -922,7 +912,93 @@ final class PieChartComponent: Component {
particleLayer.position = particle.position
particleLayer.transform = CATransform3DMakeScale(particle.scale, particle.scale, 1.0)
particleLayer.opacity = Float(particle.alpha)
particleLayer.opacity = Float(particle.alpha * alpha)
}
if particleSet.particles.count < self.particleLayers.count {
for i in particleSet.particles.count ..< self.particleLayers.count {
self.particleLayers[i].isHidden = true
}
}
}
}
private final class DoneLayer: SimpleLayer {
private let maskShapeLayer: CAShapeLayer
private var particleImage: UIImage?
private var particleSet: ParticleSet?
private var particleLayers: [SimpleLayer] = []
override init() {
self.maskShapeLayer = CAShapeLayer()
self.maskShapeLayer.fillColor = UIColor.black.cgColor
self.maskShapeLayer.fillRule = .evenOdd
super.init()
self.particleImage = UIImage(bundleImageName: "Settings/Storage/ParticleStar")?.precomposed()
let path = CGMutablePath()
path.addRect(CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: 200.0, height: 200.0)))
path.addEllipse(in: CGRect(origin: CGPoint(x: floor((200.0 - 102.0) * 0.5), y: floor((200.0 - 102.0) * 0.5)), size: CGSize(width: 102.0, height: 102.0)))
self.maskShapeLayer.path = path
self.mask = self.maskShapeLayer
self.particleSet = ParticleSet(innerRadius: 45.0, maxRadius: 100.0, preAdvance: true)
}
override init(layer: Any) {
self.maskShapeLayer = CAShapeLayer()
super.init(layer: layer)
}
required init(coder: NSCoder) {
preconditionFailure()
}
func updateParticles(deltaTime: CGFloat) {
guard let particleSet = self.particleSet else {
return
}
particleSet.update(deltaTime: deltaTime)
let size = CGSize(width: 200.0, height: 200.0)
guard let particleImage = self.particleImage else {
return
}
for i in 0 ..< particleSet.particles.count {
let particle = particleSet.particles[i]
let particleLayer: SimpleLayer
if i < self.particleLayers.count {
particleLayer = self.particleLayers[i]
particleLayer.isHidden = false
} else {
particleLayer = SimpleLayer()
particleLayer.contents = particleImage.cgImage
particleLayer.bounds = CGRect(origin: CGPoint(), size: particleImage.size)
self.particleLayers.append(particleLayer)
self.addSublayer(particleLayer)
particleLayer.layerTintColor = UIColor(rgb: 0x34C759).cgColor
}
particleLayer.position = particle.position
particleLayer.transform = CATransform3DMakeScale(particle.scale * 1.2, particle.scale * 1.2, 1.0)
let distance = sqrt(pow(particle.position.x - size.width * 0.5, 2.0) + pow(particle.position.y - size.height * 0.5, 2.0))
var mulAlpha: CGFloat = 1.0
let outerDistanceNorm: CGFloat = 20.0
if distance > 100.0 - outerDistanceNorm {
let outerDistanceFactor: CGFloat = (100.0 - distance) / outerDistanceNorm
let alphaFactor: CGFloat = max(0.0, min(1.0, outerDistanceFactor))
mulAlpha = alphaFactor
}
particleLayer.opacity = Float(particle.alpha * mulAlpha)
}
if particleSet.particles.count < self.particleLayers.count {
for i in particleSet.particles.count ..< self.particleLayers.count {
@ -945,10 +1021,10 @@ final class PieChartComponent: Component {
private var sectionLayers: [AnyHashable: SectionLayer] = [:]
private let particleSet: ParticleSet
private var labels: [AnyHashable: ChartLabel] = [:]
private var doneLayer: DoneLayer?
override init(frame: CGRect) {
self.particleSet = ParticleSet()
self.particleSet = ParticleSet(innerRadius: 50.0, maxRadius: 100.0, preAdvance: true)
super.init(frame: frame)
@ -1008,6 +1084,7 @@ final class PieChartComponent: Component {
if self.theme !== theme || self.data != data || self.selectedKey != selectedKey {
self.theme = theme
self.selectedKey = selectedKey
let previousData = self.data
if animated, let previous = self.currentLayout {
var initialState = previous
@ -1016,20 +1093,43 @@ final class PieChartComponent: Component {
let mappedProgress = listViewAnimationCurveSystem(CGFloat(currentProgress))
initialState = CalculatedLayout(interpolating: currentAnimation.start, to: previous, progress: mappedProgress, size: previous.size)
}
let targetLayout = CalculatedLayout(
let targetLayout: CalculatedLayout
if let previousData = previousData, data.items.isEmpty {
targetLayout = CalculatedLayout(
size: CGSize(width: 200.0, height: 200.0),
items: previousData.items,
selectedKey: self.selectedKey,
isEmpty: true
)
} else {
targetLayout = CalculatedLayout(
size: CGSize(width: 200.0, height: 200.0),
items: data.items,
selectedKey: self.selectedKey
selectedKey: self.selectedKey,
isEmpty: false
)
}
self.currentLayout = targetLayout
self.currentAnimation = (initialState, CACurrentMediaTime(), 0.4)
} else {
if data.items.isEmpty {
self.currentLayout = CalculatedLayout(
size: CGSize(width: 200.0, height: 200.0),
items: [.init(id: .other, displayValue: 0.0, displaySize: 0, value: 1.0, color: .green, mergeable: false, mergeFactor: 1.0)],
selectedKey: self.selectedKey,
isEmpty: true
)
} else {
self.currentLayout = CalculatedLayout(
size: CGSize(width: 200.0, height: 200.0),
items: data.items,
selectedKey: self.selectedKey
selectedKey: self.selectedKey,
isEmpty: data.items.isEmpty
)
}
}
self.data = data
@ -1043,15 +1143,71 @@ final class PieChartComponent: Component {
var validIds: [AnyHashable] = []
if let currentLayout = self.currentLayout {
var effectiveLayout = currentLayout
var verticalOffset: CGFloat = 0.0
var particleAlpha: CGFloat = 1.0
var rotationAngle: CGFloat = 0.0
let emptyRotationAngle: CGFloat = CGFloat.pi
let emptyVerticalOffset: CGFloat = (92.0 - 200.0) * 0.5
if let currentAnimation = self.currentAnimation {
let currentProgress: Double = max(0.0, min(1.0, (CACurrentMediaTime() - currentAnimation.startTime) / currentAnimation.duration))
let mappedProgress = listViewAnimationCurveSystem(CGFloat(currentProgress))
effectiveLayout = CalculatedLayout(interpolating: currentAnimation.start, to: currentLayout, progress: mappedProgress, size: currentLayout.size)
let fromVerticalOffset: CGFloat
let fromRotationAngle: CGFloat
if currentAnimation.start.isEmpty {
fromVerticalOffset = emptyVerticalOffset
fromRotationAngle = emptyRotationAngle
} else {
fromVerticalOffset = 0.0
fromRotationAngle = 0.0
}
let toVerticalOffset: CGFloat
let toRotationAngle: CGFloat
if currentLayout.isEmpty {
toVerticalOffset = emptyVerticalOffset
toRotationAngle = emptyRotationAngle
} else {
toVerticalOffset = 0.0
toRotationAngle = 0.0
}
verticalOffset = (1.0 - mappedProgress) * fromVerticalOffset + mappedProgress * toVerticalOffset
rotationAngle = (1.0 - mappedProgress) * fromRotationAngle + mappedProgress * toRotationAngle
if currentLayout.isEmpty {
particleAlpha = 1.0 - mappedProgress
}
if currentProgress >= 1.0 - CGFloat.ulpOfOne {
self.currentAnimation = nil
}
} else {
if currentLayout.isEmpty {
verticalOffset = emptyVerticalOffset
particleAlpha = 0.0
rotationAngle = emptyRotationAngle
}
}
if currentLayout.isEmpty {
let doneLayer: DoneLayer
if let current = self.doneLayer {
doneLayer = current
} else {
doneLayer = DoneLayer()
self.doneLayer = doneLayer
self.layer.insertSublayer(doneLayer, at: 0)
}
doneLayer.updateParticles(deltaTime: deltaTime)
doneLayer.frame = CGRect(origin: CGPoint(x: 0.0, y: verticalOffset), size: CGSize(width: 200.0, height: 200.0))
doneLayer.opacity = Float(1.0 - particleAlpha)
} else {
if let doneLayer = self.doneLayer {
self.doneLayer = nil
doneLayer.removeFromSuperlayer()
}
}
for section in effectiveLayout.sections {
@ -1066,9 +1222,12 @@ final class PieChartComponent: Component {
self.layer.addSublayer(sectionLayer)
}
sectionLayer.frame = CGRect(origin: CGPoint(), size: CGSize(width: 200.0, height: 200.0))
let sectionLayerFrame = CGRect(origin: CGPoint(x: 0.0, y: verticalOffset), size: CGSize(width: 200.0, height: 200.0))
sectionLayer.position = sectionLayerFrame.center
sectionLayer.bounds = CGRect(origin: CGPoint(), size: sectionLayerFrame.size)
sectionLayer.transform = CATransform3DMakeRotation(rotationAngle, 0.0, 0.0, 1.0)
sectionLayer.update(size: sectionLayer.bounds.size, section: section)
sectionLayer.updateParticles(particleSet: self.particleSet)
sectionLayer.updateParticles(particleSet: self.particleSet, alpha: particleAlpha)
}
}

View File

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

View File

@ -727,6 +727,8 @@ final class StorageUsageScreenComponent: Component {
private var doneStatusCircle: SimpleShapeLayer?
private var doneStatusNode: RadialStatusNode?
private let scrollContainerView: UIView
private let pieChartView = ComponentView<Empty>()
private let chartTotalLabel = ComponentView<Empty>()
private let categoriesView = ComponentView<Empty>()
@ -777,6 +779,8 @@ final class StorageUsageScreenComponent: Component {
self.navigationSeparatorLayerContainer = SimpleLayer()
self.navigationSeparatorLayerContainer.opacity = 0.0
self.scrollContainerView = UIView()
self.scrollView = ScrollViewImpl()
self.keepDurationSectionContainerView = UIView()
@ -805,7 +809,9 @@ final class StorageUsageScreenComponent: Component {
self.scrollView.clipsToBounds = true
self.addSubview(self.scrollView)
self.scrollView.addSubview(self.keepDurationSectionContainerView)
self.scrollView.addSubview(self.scrollContainerView)
self.scrollContainerView.addSubview(self.keepDurationSectionContainerView)
self.scrollView.layer.addSublayer(self.headerProgressBackgroundLayer)
self.scrollView.layer.addSublayer(self.headerProgressForegroundLayer)
@ -1070,9 +1076,10 @@ final class StorageUsageScreenComponent: Component {
alphaTransition.setAlpha(view: self.scrollView, alpha: self.aggregatedData != nil ? 1.0 : 0.0)
alphaTransition.setAlpha(view: self.headerOffsetContainer, alpha: self.aggregatedData != nil ? 1.0 : 0.0)
} else if case .clearedItems = animationHint.value {
if let snapshotView = self.snapshotView(afterScreenUpdates: false) {
snapshotView.frame = self.bounds
self.addSubview(snapshotView)
if let snapshotView = self.scrollContainerView.snapshotView(afterScreenUpdates: false) {
snapshotView.frame = self.scrollContainerView.frame
self.scrollView.insertSubview(snapshotView, aboveSubview: self.scrollContainerView)
self.scrollContainerView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { [weak snapshotView] _ in
snapshotView?.removeFromSuperview()
})
@ -1347,12 +1354,20 @@ final class StorageUsageScreenComponent: Component {
chartItems.append(chartItem)
}
if !listCategories.isEmpty {
chartItems.append(PieChartComponent.ChartData.Item(id: .other, displayValue: otherRealSum, displaySize: totalOtherSize, value: self.isOtherCategoryExpanded ? 0.0 : otherSum, color: Category.misc.color, mergeable: false, mergeFactor: 1.0))
}
let chartData = PieChartComponent.ChartData(items: chartItems)
self.pieChartView.parentState = state
var pieChartTransition = transition
if transition.animation.isImmediate, let animationHint, case .clearedItems = animationHint.value {
pieChartTransition = Transition(animation: .curve(duration: 0.4, curve: .spring))
}
let pieChartSize = self.pieChartView.update(
transition: transition,
transition: pieChartTransition,
component: AnyComponent(PieChartComponent(
theme: environment.theme,
strings: environment.strings,
@ -1367,8 +1382,8 @@ final class StorageUsageScreenComponent: Component {
self.scrollView.addSubview(pieChartComponentView)
}
transition.setFrame(view: pieChartComponentView, frame: pieChartFrame)
transition.setAlpha(view: pieChartComponentView, alpha: listCategories.isEmpty ? 0.0 : 1.0)
pieChartTransition.setFrame(view: pieChartComponentView, frame: pieChartFrame)
//transition.setAlpha(view: pieChartComponentView, alpha: listCategories.isEmpty ? 0.0 : 1.0)
}
if let _ = self.aggregatedData, listCategories.isEmpty {
let checkColor = UIColor(rgb: 0x34C759)
@ -1392,7 +1407,7 @@ final class StorageUsageScreenComponent: Component {
} else {
doneStatusCircle = SimpleShapeLayer()
self.doneStatusCircle = doneStatusCircle
self.scrollView.layer.addSublayer(doneStatusCircle)
//self.scrollView.layer.addSublayer(doneStatusCircle)
doneStatusCircle.opacity = 0.0
}
@ -1431,7 +1446,11 @@ final class StorageUsageScreenComponent: Component {
if listCategories.isEmpty {
headerText = environment.strings.StorageManagement_TitleCleared
} else if let peer = component.peer {
if peer.id == component.context.account.peerId {
headerText = environment.strings.DialogList_SavedMessages
} else {
headerText = peer.displayTitle(strings: environment.strings, displayOrder: .firstLast)
}
} else {
headerText = environment.strings.StorageManagement_Title
}
@ -1527,7 +1546,7 @@ final class StorageUsageScreenComponent: Component {
let headerDescriptionFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - headerDescriptionSize.width) / 2.0), y: contentHeight), size: headerDescriptionSize)
if let headerDescriptionComponentView = self.headerDescriptionView.view {
if headerDescriptionComponentView.superview == nil {
self.scrollView.addSubview(headerDescriptionComponentView)
self.scrollContainerView.addSubview(headerDescriptionComponentView)
}
transition.setFrame(view: headerDescriptionComponentView, frame: headerDescriptionFrame)
}
@ -1562,15 +1581,15 @@ final class StorageUsageScreenComponent: Component {
} else {
chartAvatarNode = AvatarNode(font: avatarPlaceholderFont(size: 17.0))
self.chartAvatarNode = chartAvatarNode
if let pieChartComponentView = self.pieChartView.view {
self.scrollView.insertSubview(chartAvatarNode.view, belowSubview: pieChartComponentView)
} else {
self.scrollView.addSubview(chartAvatarNode.view)
}
self.scrollContainerView.addSubview(chartAvatarNode.view)
chartAvatarNode.frame = avatarFrame
if peer.id == component.context.account.peerId {
chartAvatarNode.setPeer(context: component.context, theme: environment.theme, peer: peer, overrideImage: .savedMessagesIcon, displayDimensions: avatarSize)
} else {
chartAvatarNode.setPeer(context: component.context, theme: environment.theme, peer: peer, displayDimensions: avatarSize)
}
}
transition.setAlpha(view: chartAvatarNode.view, alpha: listCategories.isEmpty ? 0.0 : 1.0)
} else {
let sizeText = dataSizeString(Int(totalSelectedCategorySize), forceDecimal: true, formatting: DataSizeStringFormatting(strings: environment.strings, decimalSeparator: "."))
@ -1606,11 +1625,7 @@ final class StorageUsageScreenComponent: Component {
)
if let chartTotalLabelView = self.chartTotalLabel.view {
if chartTotalLabelView.superview == nil {
if let pieChartComponentView = self.pieChartView.view {
self.scrollView.insertSubview(chartTotalLabelView, belowSubview: pieChartComponentView)
} else {
self.scrollView.addSubview(chartTotalLabelView)
}
self.scrollContainerView.addSubview(chartTotalLabelView)
}
let totalLabelFrame = CGRect(origin: CGPoint(x: pieChartFrame.minX + floor((pieChartFrame.width - chartTotalLabelSize.width) / 2.0), y: pieChartFrame.minY + floor((pieChartFrame.height - chartTotalLabelSize.height) / 2.0)), size: chartTotalLabelSize)
transition.setFrame(view: chartTotalLabelView, frame: totalLabelFrame)
@ -1674,7 +1689,7 @@ final class StorageUsageScreenComponent: Component {
)
if let categoriesComponentView = self.categoriesView.view {
if categoriesComponentView.superview == nil {
self.scrollView.addSubview(categoriesComponentView)
self.scrollContainerView.addSubview(categoriesComponentView)
}
transition.setFrame(view: categoriesComponentView, frame: CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: categoriesSize))
@ -1697,7 +1712,7 @@ final class StorageUsageScreenComponent: Component {
let categoriesDescriptionFrame = CGRect(origin: CGPoint(x: sideInset + 15.0, y: contentHeight), size: categoriesDescriptionSize)
if let categoriesDescriptionComponentView = self.categoriesDescriptionView.view {
if categoriesDescriptionComponentView.superview == nil {
self.scrollView.addSubview(categoriesDescriptionComponentView)
self.scrollContainerView.addSubview(categoriesDescriptionComponentView)
}
transition.setFrame(view: categoriesDescriptionComponentView, frame: categoriesDescriptionFrame)
}
@ -1728,7 +1743,7 @@ final class StorageUsageScreenComponent: Component {
let keepDurationTitleFrame = CGRect(origin: CGPoint(x: sideInset + 15.0, y: contentHeight), size: keepDurationTitleSize)
if let keepDurationTitleComponentView = self.keepDurationTitleView.view {
if keepDurationTitleComponentView.superview == nil {
self.scrollView.addSubview(keepDurationTitleComponentView)
self.scrollContainerView.addSubview(keepDurationTitleComponentView)
}
transition.setFrame(view: keepDurationTitleComponentView, frame: keepDurationTitleFrame)
}
@ -1830,7 +1845,7 @@ final class StorageUsageScreenComponent: Component {
let keepDurationDescriptionFrame = CGRect(origin: CGPoint(x: sideInset + 15.0, y: contentHeight), size: keepDurationDescriptionSize)
if let keepDurationDescriptionComponentView = self.keepDurationDescriptionView.view {
if keepDurationDescriptionComponentView.superview == nil {
self.scrollView.addSubview(keepDurationDescriptionComponentView)
self.scrollContainerView.addSubview(keepDurationDescriptionComponentView)
}
transition.setFrame(view: keepDurationDescriptionComponentView, frame: keepDurationDescriptionFrame)
}
@ -1856,7 +1871,7 @@ final class StorageUsageScreenComponent: Component {
let keepSizeTitleFrame = CGRect(origin: CGPoint(x: sideInset + 15.0, y: contentHeight), size: keepSizeTitleSize)
if let keepSizeTitleComponentView = self.keepSizeTitleView.view {
if keepSizeTitleComponentView.superview == nil {
self.scrollView.addSubview(keepSizeTitleComponentView)
self.scrollContainerView.addSubview(keepSizeTitleComponentView)
}
transition.setFrame(view: keepSizeTitleComponentView, frame: keepSizeTitleFrame)
}
@ -1887,7 +1902,7 @@ final class StorageUsageScreenComponent: Component {
let keepSizeFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: keepSizeSize)
if let keepSizeComponentView = self.keepSizeView.view {
if keepSizeComponentView.superview == nil {
self.scrollView.addSubview(keepSizeComponentView)
self.scrollContainerView.addSubview(keepSizeComponentView)
}
transition.setFrame(view: keepSizeComponentView, frame: keepSizeFrame)
}
@ -1913,7 +1928,7 @@ final class StorageUsageScreenComponent: Component {
let keepSizeDescriptionFrame = CGRect(origin: CGPoint(x: sideInset + 15.0, y: contentHeight), size: keepSizeDescriptionSize)
if let keepSizeDescriptionComponentView = self.keepSizeDescriptionView.view {
if keepSizeDescriptionComponentView.superview == nil {
self.scrollView.addSubview(keepSizeDescriptionComponentView)
self.scrollContainerView.addSubview(keepSizeDescriptionComponentView)
}
transition.setFrame(view: keepSizeDescriptionComponentView, frame: keepSizeDescriptionFrame)
}
@ -2129,7 +2144,7 @@ final class StorageUsageScreenComponent: Component {
)
if let panelContainerView = self.panelContainer.view {
if panelContainerView.superview == nil {
self.scrollView.addSubview(panelContainerView)
self.scrollContainerView.addSubview(panelContainerView)
}
transition.setFrame(view: panelContainerView, frame: CGRect(origin: CGPoint(x: 0.0, y: contentHeight), size: panelContainerSize))
}
@ -2146,6 +2161,7 @@ final class StorageUsageScreenComponent: Component {
if self.scrollView.contentSize != contentSize {
self.scrollView.contentSize = contentSize
}
transition.setFrame(view: self.scrollContainerView, frame: CGRect(origin: CGPoint(), size: contentSize))
var scrollViewBounds = self.scrollView.bounds
scrollViewBounds.size = availableSize
@ -2181,7 +2197,7 @@ final class StorageUsageScreenComponent: Component {
clearingNode.updateLayout(size: clearingSize, transition: .immediate)
if animateIn {
clearingNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25, delay: 0.15)
clearingNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25, delay: 0.4)
}
} else {
if let clearingNode = self.clearingNode {
@ -2190,10 +2206,10 @@ final class StorageUsageScreenComponent: Component {
var delay: Double = 0.0
if let clearingDisplayTimestamp = self.clearingDisplayTimestamp {
let timeDelta = CFAbsoluteTimeGetCurrent() - clearingDisplayTimestamp
if timeDelta < 0.12 {
if timeDelta < 0.4 {
delay = 0.0
} else if timeDelta < 0.4 {
delay = 0.4
} else if timeDelta < 1.0 {
delay = 1.0
}
}
@ -2414,7 +2430,9 @@ final class StorageUsageScreenComponent: Component {
self.isClearing = false
if !firstTime {
self.state?.updated(transition: Transition(animation: .none).withUserData(AnimationHint(value: .clearedItems)))
}
completion()
})
@ -2468,8 +2486,21 @@ final class StorageUsageScreenComponent: Component {
var items: [ContextMenuItem] = []
var openTitle: String = presentationData.strings.StorageManagement_OpenPhoto
for media in message.media {
if let _ = media as? TelegramMediaImage {
openTitle = presentationData.strings.StorageManagement_OpenPhoto
} else if let file = media as? TelegramMediaFile {
if file.isVideo {
openTitle = presentationData.strings.StorageManagement_OpenVideo
} else {
openTitle = presentationData.strings.StorageManagement_OpenFile
}
}
}
items.append(.action(ContextMenuActionItem(
text: presentationData.strings.StorageManagement_OpenPhoto,
text: openTitle,
icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Expand"), color: theme.contextMenu.primaryColor) },
action: { [weak self] c, _ in
c.dismiss(completion: { [weak self] in

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.meshAnimationCache = MeshAnimationCache(mediaBox: account.postbox.mediaBox)
let cacheStorageBox = self.account.postbox.mediaBox.cacheStorageBox
self.animationCache = AnimationCacheImpl(basePath: self.account.postbox.mediaBox.basePath + "/animation-cache", allocateTempFile: {
return TempBox.shared.tempFile(fileName: "file").path
}, updateStorageStats: { path, size in
if let pathData = path.data(using: .utf8) {
cacheStorageBox.update(id: pathData, size: size)
}
})
self.animationRenderer = MultiAnimationRendererImpl()

View File

@ -1306,26 +1306,10 @@ private func extractAccountManagerState(records: AccountRecordsView<TelegramAcco
BGTaskScheduler.shared.register(forTaskWithIdentifier: taskId, using: DispatchQueue.main) { task in
Logger.shared.log("App \(self.episodeId)", "Executing cleanup task")
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: false))
}
disposable.set(signals.start(completed: {
let disposable = self.runCacheReindexTasks(lowImpact: true, completion: {
Logger.shared.log("App \(self.episodeId)", "Completed cleanup task")
task.setTaskCompleted(success: true)
}))
})
})
task.expirationHandler = {
@ -1351,9 +1335,44 @@ 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
}
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() {
var resetOnce = true
self.badgeDisposable.set((self.context.get()