diff --git a/Telegram/BUILD b/Telegram/BUILD index 2aa652fad6..ce29f30dd0 100644 --- a/Telegram/BUILD +++ b/Telegram/BUILD @@ -1793,6 +1793,7 @@ plist_fragment( BGTaskSchedulerPermittedIdentifiers {telegram_bundle_id}.refresh + {telegram_bundle_id}.cleanup CFBundleAllowMixedLocalizations @@ -1893,6 +1894,7 @@ plist_fragment( location remote-notification voip + processing UIDeviceFamily diff --git a/Telegram/NotificationService/Sources/NotificationService.swift b/Telegram/NotificationService/Sources/NotificationService.swift index ee0c4185ac..f697af4b28 100644 --- a/Telegram/NotificationService/Sources/NotificationService.swift +++ b/Telegram/NotificationService/Sources/NotificationService.swift @@ -1169,6 +1169,12 @@ private final class NotificationServiceHandler { parameters: MediaResourceFetchParameters( tag: nil, info: resourceFetchInfo(resource: resource), + location: messageId.flatMap { messageId in + return MediaResourceStorageLocation( + peerId: peerId, + messageId: messageId + ) + }, isRandomAccessAllowed: true ), encryptionKey: nil, @@ -1220,6 +1226,7 @@ private final class NotificationServiceHandler { parameters: MediaResourceFetchParameters( tag: nil, info: resourceFetchInfo(resource: resource), + location: nil, isRandomAccessAllowed: true ), encryptionKey: nil, diff --git a/submodules/Components/ReactionImageComponent/Sources/ReactionImageComponent.swift b/submodules/Components/ReactionImageComponent/Sources/ReactionImageComponent.swift index 413f41ec20..21e6f265ef 100644 --- a/submodules/Components/ReactionImageComponent/Sources/ReactionImageComponent.swift +++ b/submodules/Components/ReactionImageComponent/Sources/ReactionImageComponent.swift @@ -29,7 +29,17 @@ public func reactionStaticImage(context: AccountContext, animation: TelegramMedi } else { type = .still } - let fetchFrame = animationCacheFetchFile(context: context, resource: MediaResourceReference.standalone(resource: animation.resource), type: type, keyframeOnly: true) + + var customColor: UIColor? + for attribute in animation.attributes { + if case let .CustomEmoji(_, isSingleColor, _, _) = attribute { + if isSingleColor { + customColor = nil + } + } + } + + let fetchFrame = animationCacheFetchFile(context: context, resource: MediaResourceReference.standalone(resource: animation.resource), type: type, keyframeOnly: true, customColor: customColor) class AnimationCacheItemWriterImpl: AnimationCacheItemWriter { let queue: Queue diff --git a/submodules/ContextUI/Sources/ContextActionsContainerNode.swift b/submodules/ContextUI/Sources/ContextActionsContainerNode.swift index 14365a7fb8..b002bf3e07 100644 --- a/submodules/ContextUI/Sources/ContextActionsContainerNode.swift +++ b/submodules/ContextUI/Sources/ContextActionsContainerNode.swift @@ -586,7 +586,7 @@ final class InnerTextSelectionTipContainerNode: ASDisplayNode { let textRightInset: CGFloat if let _ = self.iconNode.image { - textRightInset = iconSize.width - 8.0 + textRightInset = iconSize.width - 2.0 } else { textRightInset = 0.0 } diff --git a/submodules/Display/Source/TextNode.swift b/submodules/Display/Source/TextNode.swift index f514c9866c..b9621e5798 100644 --- a/submodules/Display/Source/TextNode.swift +++ b/submodules/Display/Source/TextNode.swift @@ -212,11 +212,13 @@ public final class TextNodeLayout: NSObject { public let range: NSRange public let rect: CGRect public let value: AnyHashable + public let textColor: UIColor - public init(range: NSRange, rect: CGRect, value: AnyHashable) { + public init(range: NSRange, rect: CGRect, value: AnyHashable, textColor: UIColor) { self.range = range self.rect = rect self.value = value + self.textColor = textColor } public static func ==(lhs: EmbeddedItem, rhs: EmbeddedItem) -> Bool { @@ -229,6 +231,9 @@ public final class TextNodeLayout: NSObject { if lhs.value != rhs.value { return false } + if lhs.textColor != rhs.textColor { + return false + } return true } } @@ -301,7 +306,18 @@ public final class TextNodeLayout: NSObject { spoilers.append(contentsOf: line.spoilers.map { ( $0.range, $0.frame.offsetBy(dx: lineFrame.minX, dy: lineFrame.minY)) }) spoilerWords.append(contentsOf: line.spoilerWords.map { ( $0.range, $0.frame.offsetBy(dx: lineFrame.minX, dy: lineFrame.minY)) }) for embeddedItem in line.embeddedItems { - embeddedItems.append(TextNodeLayout.EmbeddedItem(range: embeddedItem.range, rect: embeddedItem.frame.offsetBy(dx: lineFrame.minX, dy: lineFrame.minY), value: embeddedItem.item)) + var textColor: UIColor? + if let attributedString = attributedString, embeddedItem.range.location < attributedString.length { + if let color = attributedString.attribute(.foregroundColor, at: embeddedItem.range.location, effectiveRange: nil) as? UIColor { + textColor = color + } + if textColor == nil { + if let color = attributedString.attribute(.foregroundColor, at: 0, effectiveRange: nil) as? UIColor { + textColor = color + } + } + } + embeddedItems.append(TextNodeLayout.EmbeddedItem(range: embeddedItem.range, rect: embeddedItem.frame.offsetBy(dx: lineFrame.minX, dy: lineFrame.minY), value: embeddedItem.item, textColor: textColor ?? .black)) } } self.hasRTL = hasRTL diff --git a/submodules/GalleryData/Sources/GalleryData.swift b/submodules/GalleryData/Sources/GalleryData.swift index ab8410856b..448bc0dbe8 100644 --- a/submodules/GalleryData/Sources/GalleryData.swift +++ b/submodules/GalleryData/Sources/GalleryData.swift @@ -100,7 +100,7 @@ public func chatMessageGalleryControllerData(context: AccountContext, chatLocati var instantPageMedia: (TelegramMediaWebpage, [InstantPageGalleryEntry])? if message.media.isEmpty, let entities = message.textEntitiesAttribute?.entities, entities.count == 1, let firstEntity = entities.first, case let .CustomEmoji(_, fileId) = firstEntity.type, let file = message.associatedMedia[MediaId(namespace: Namespaces.Media.CloudFile, id: fileId)] as? TelegramMediaFile { for attribute in file.attributes { - if case let .CustomEmoji(_, _, reference) = attribute { + if case let .CustomEmoji(_, _, _, reference) = attribute { if let reference = reference { return .stickerPack(reference) } diff --git a/submodules/Postbox/BUILD b/submodules/Postbox/BUILD index b975adf50e..d9ce7c24b3 100644 --- a/submodules/Postbox/BUILD +++ b/submodules/Postbox/BUILD @@ -17,6 +17,7 @@ swift_library( "//submodules/StringTransliteration:StringTransliteration", "//submodules/ManagedFile:ManagedFile", "//submodules/Utils/RangeSet:RangeSet", + "//submodules/CryptoUtils", ], visibility = [ "//visibility:public", diff --git a/submodules/Postbox/Sources/MediaBox.swift b/submodules/Postbox/Sources/MediaBox.swift index 633603c788..f06225fce9 100644 --- a/submodules/Postbox/Sources/MediaBox.swift +++ b/submodules/Postbox/Sources/MediaBox.swift @@ -144,6 +144,8 @@ public final class MediaBox { private let cacheQueue = Queue() private let timeBasedCleanup: TimeBasedCleanup + public let storageBox: StorageBox + private let didRemoveResourcesPipe = ValuePipe() public var didRemoveResources: Signal { return .single(Void()) |> then(self.didRemoveResourcesPipe.signal()) @@ -187,6 +189,10 @@ public final class MediaBox { public init(basePath: String) { self.basePath = basePath + self.storageBox = StorageBox(logger: StorageBox.Logger(impl: { string in + postboxLog(string) + }), basePath: basePath + "/storage") + self.timeBasedCleanup = TimeBasedCleanup(generalPaths: [ self.basePath, self.basePath + "/cache", @@ -204,6 +210,14 @@ public final class MediaBox { self.timeBasedCleanup.setMaxStoreTimes(general: general, shortLived: shortLived, gigabytesLimit: gigabytesLimit) } + private func idForFileName(name: String) -> String { + if name.hasSuffix("_partial") { + return String(name[name.startIndex ..< name.index(name.endIndex, offsetBy: -8)]) + } else { + return name + } + } + private func fileNameForId(_ id: MediaResourceId) -> String { return "\(id.stringRepresentation)" } @@ -580,6 +594,10 @@ public final class MediaBox { return } + if let location = parameters?.location { + self.storageBox.add(reference: StorageBox.Reference(peerId: location.peerId, messageNamespace: UInt8(clamping: location.messageId.namespace), messageId: location.messageId.id), to: resource.id.stringRepresentation.data(using: .utf8)!) + } + guard let (fileContext, releaseContext) = self.fileContext(for: resource.id) else { subscriber.putCompletion() return @@ -742,6 +760,10 @@ public final class MediaBox { self.dataQueue.async { let paths = self.storePathsForId(resource.id) + if let location = parameters?.location { + self.storageBox.add(reference: StorageBox.Reference(peerId: location.peerId, messageNamespace: UInt8(clamping: location.messageId.namespace), messageId: location.messageId.id), to: resource.id.stringRepresentation.data(using: .utf8)!) + } + if let _ = fileSize(paths.complete) { if implNext { subscriber.putNext(.local) @@ -1174,6 +1196,17 @@ public final class MediaBox { } } + public func resourceUsage(id: MediaResourceId) -> Int64 { + let paths = self.storePathsForId(id) + if let size = fileSize(paths.complete) { + return Int64(size) + } else if let size = fileSize(paths.partial, useTotalFileAllocatedSize: true) { + return Int64(size) + } else { + return 0 + } + } + public func collectResourceCacheUsage(_ ids: [MediaResourceId]) -> Signal<[MediaResourceId: Int64], NoError> { return Signal { subscriber in self.dataQueue.async { @@ -1194,6 +1227,99 @@ public final class MediaBox { } } + public func collectAllResourceUsage() -> Signal<[(id: String?, path: String, size: Int64)], NoError> { + return Signal { subscriber in + self.dataQueue.async { + var result: [(id: String?, path: String, size: Int64)] = [] + + var fileIds = Set() + + if let enumerator = FileManager.default.enumerator(at: URL(fileURLWithPath: self.basePath), includingPropertiesForKeys: [.fileSizeKey, .fileResourceIdentifierKey], options: [.skipsHiddenFiles, .skipsSubdirectoryDescendants], errorHandler: nil) { + loop: for url in enumerator { + if let url = url as? URL { + if let fileId = (try? url.resourceValues(forKeys: Set([.fileResourceIdentifierKey])))?.fileResourceIdentifier as? Data { + if fileIds.contains(fileId) { + //paths.append(url.lastPathComponent) + continue loop + } + + if let value = (try? url.resourceValues(forKeys: Set([.fileSizeKey])))?.fileSize, value != 0 { + fileIds.insert(fileId) + result.append((id: self.idForFileName(name: url.lastPathComponent), path: url.lastPathComponent, size: Int64(value))) + //paths.append(url.lastPathComponent) + } + } + } + } + } + + /*var cacheResult: Int64 = 0 + + var excludePrefixes = Set() + for id in excludeIds { + let cachedRepresentationPrefix = self.fileNameForId(id) + + excludePrefixes.insert(cachedRepresentationPrefix) + } + + if let enumerator = FileManager.default.enumerator(at: URL(fileURLWithPath: self.basePath + "/cache"), includingPropertiesForKeys: [.fileSizeKey], options: [.skipsHiddenFiles, .skipsSubdirectoryDescendants], errorHandler: nil) { + loop: for url in enumerator { + if let url = url as? URL { + if let prefix = url.lastPathComponent.components(separatedBy: ":").first, excludePrefixes.contains(prefix) { + continue loop + } + + if let value = (try? url.resourceValues(forKeys: Set([.fileSizeKey])))?.fileSize, value != 0 { + paths.append("cache/" + url.lastPathComponent) + cacheResult += Int64(value) + } + } + } + } + + func processRecursive(directoryPath: String, subdirectoryPath: String) { + if let enumerator = FileManager.default.enumerator(at: URL(fileURLWithPath: directoryPath), includingPropertiesForKeys: [.fileSizeKey, .isDirectoryKey], options: [.skipsHiddenFiles, .skipsSubdirectoryDescendants], errorHandler: nil) { + loop: for url in enumerator { + if let url = url as? URL { + if let prefix = url.lastPathComponent.components(separatedBy: ":").first, excludePrefixes.contains(prefix) { + continue loop + } + + if let isDirectory = (try? url.resourceValues(forKeys: Set([.isDirectoryKey])))?.isDirectory, isDirectory { + processRecursive(directoryPath: url.path, subdirectoryPath: subdirectoryPath + "/\(url.lastPathComponent)") + } else if let value = (try? url.resourceValues(forKeys: Set([.fileSizeKey])))?.fileSize, value != 0 { + paths.append("\(subdirectoryPath)/" + url.lastPathComponent) + cacheResult += Int64(value) + } + } + } + } + } + + processRecursive(directoryPath: self.basePath + "/animation-cache", subdirectoryPath: "animation-cache") + + if let enumerator = FileManager.default.enumerator(at: URL(fileURLWithPath: self.basePath + "/short-cache"), includingPropertiesForKeys: [.fileSizeKey], options: [.skipsHiddenFiles, .skipsSubdirectoryDescendants], errorHandler: nil) { + loop: for url in enumerator { + if let url = url as? URL { + if let prefix = url.lastPathComponent.components(separatedBy: ":").first, excludePrefixes.contains(prefix) { + continue loop + } + + if let value = (try? url.resourceValues(forKeys: Set([.fileSizeKey])))?.fileSize, value != 0 { + paths.append("short-cache/" + url.lastPathComponent) + cacheResult += Int64(value) + } + } + } + }*/ + + subscriber.putNext(result) + subscriber.putCompletion() + } + return EmptyDisposable + } + } + public func collectOtherResourceUsage(excludeIds: Set, combinedExcludeIds: Set) -> Signal<(Int64, [String], Int64), NoError> { return Signal { subscriber in self.dataQueue.async { diff --git a/submodules/Postbox/Sources/MediaResource.swift b/submodules/Postbox/Sources/MediaResource.swift index e4d4f2d773..e72385f7a2 100644 --- a/submodules/Postbox/Sources/MediaResource.swift +++ b/submodules/Postbox/Sources/MediaResource.swift @@ -39,14 +39,26 @@ public protocol MediaResourceFetchTag { public protocol MediaResourceFetchInfo { } +public final class MediaResourceStorageLocation { + public let peerId: PeerId + public let messageId: MessageId + + public init(peerId: PeerId, messageId: MessageId) { + self.peerId = peerId + self.messageId = messageId + } +} + public struct MediaResourceFetchParameters { public let tag: MediaResourceFetchTag? public let info: MediaResourceFetchInfo? + public let location: MediaResourceStorageLocation? public let isRandomAccessAllowed: Bool - public init(tag: MediaResourceFetchTag?, info: MediaResourceFetchInfo?, isRandomAccessAllowed: Bool) { + public init(tag: MediaResourceFetchTag?, info: MediaResourceFetchInfo?, location: MediaResourceStorageLocation?, isRandomAccessAllowed: Bool) { self.tag = tag self.info = info + self.location = location self.isRandomAccessAllowed = isRandomAccessAllowed } } diff --git a/submodules/Postbox/Sources/SqliteValueBox.swift b/submodules/Postbox/Sources/SqliteValueBox.swift index 088f89623d..2c8f462a53 100644 --- a/submodules/Postbox/Sources/SqliteValueBox.swift +++ b/submodules/Postbox/Sources/SqliteValueBox.swift @@ -188,6 +188,8 @@ public final class SqliteValueBox: ValueBox { private var updateStatements: [Int32 : SqlitePreparedStatement] = [:] private var insertOrReplacePrimaryKeyStatements: [Int32 : SqlitePreparedStatement] = [:] private var insertOrReplaceIndexKeyStatements: [Int32 : SqlitePreparedStatement] = [:] + private var insertOrIgnorePrimaryKeyStatements: [Int32 : SqlitePreparedStatement] = [:] + private var insertOrIgnoreIndexKeyStatements: [Int32 : SqlitePreparedStatement] = [:] private var deleteStatements: [Int32 : SqlitePreparedStatement] = [:] private var moveStatements: [Int32 : SqlitePreparedStatement] = [:] private var copyStatements: [TablePairKey : SqlitePreparedStatement] = [:] @@ -1220,6 +1222,59 @@ public final class SqliteValueBox: ValueBox { return resultStatement } + private func insertOrIgnoreStatement(_ table: SqliteValueBoxTable, key: ValueBoxKey, value: MemoryBuffer) -> SqlitePreparedStatement { + precondition(self.queue.isCurrent()) + checkTableKey(table.table, key) + + let resultStatement: SqlitePreparedStatement + + if table.table.keyType == .int64 || table.hasPrimaryKey { + if let statement = self.insertOrIgnorePrimaryKeyStatements[table.table.id] { + resultStatement = statement + } else { + var statement: OpaquePointer? = nil + let status = sqlite3_prepare_v2(self.database.handle, "INSERT INTO t\(table.table.id) (key, value) VALUES(?, ?) ON CONFLICT(key) DO NOTHING", -1, &statement, nil) + if status != SQLITE_OK { + let errorText = self.database.currentError() ?? "Unknown error" + preconditionFailure(errorText) + } + let preparedStatement = SqlitePreparedStatement(statement: statement) + self.insertOrIgnorePrimaryKeyStatements[table.table.id] = preparedStatement + resultStatement = preparedStatement + } + } else { + if let statement = self.insertOrIgnoreIndexKeyStatements[table.table.id] { + resultStatement = statement + } else { + var statement: OpaquePointer? = nil + let status = sqlite3_prepare_v2(self.database.handle, "INSERT INTO t\(table.table.id) (key, value) VALUES(?, ?)", -1, &statement, nil) + if status != SQLITE_OK { + let errorText = self.database.currentError() ?? "Unknown error" + preconditionFailure(errorText) + } + let preparedStatement = SqlitePreparedStatement(statement: statement) + self.insertOrIgnorePrimaryKeyStatements[table.table.id] = preparedStatement + resultStatement = preparedStatement + } + } + + resultStatement.reset() + + switch table.table.keyType { + case .binary: + resultStatement.bind(1, data: key.memory, length: key.length) + case .int64: + resultStatement.bind(1, number: key.getInt64(0)) + } + if value.length == 0 { + resultStatement.bindNull(2) + } else { + resultStatement.bind(2, data: value.memory, length: value.length) + } + + return resultStatement + } + private func deleteStatement(_ table: ValueBoxTable, key: ValueBoxKey) -> SqlitePreparedStatement { precondition(self.queue.isCurrent()) checkTableKey(table, key) @@ -1847,6 +1902,30 @@ public final class SqliteValueBox: ValueBox { } } + public func setOrIgnore(_ table: ValueBoxTable, key: ValueBoxKey, value: MemoryBuffer) { + precondition(self.queue.isCurrent()) + let sqliteTable = self.checkTable(table) + + if sqliteTable.hasPrimaryKey { + let statement = self.insertOrIgnoreStatement(sqliteTable, key: key, value: value) + while statement.step(handle: self.database.handle, pathToRemoveOnError: self.removeDatabaseOnError ? self.databasePath : nil) { + } + statement.reset() + } else { + if self.exists(table, key: key) { + let statement = self.updateStatement(table, key: key, value: value) + while statement.step(handle: self.database.handle, pathToRemoveOnError: self.removeDatabaseOnError ? self.databasePath : nil) { + } + statement.reset() + } else { + let statement = self.insertOrReplaceStatement(sqliteTable, key: key, value: value) + while statement.step(handle: self.database.handle, pathToRemoveOnError: self.removeDatabaseOnError ? self.databasePath : nil) { + } + statement.reset() + } + } + } + public func remove(_ table: ValueBoxTable, key: ValueBoxKey, secure: Bool) { precondition(self.queue.isCurrent()) if let _ = self.tables[table.id] { @@ -2108,6 +2187,16 @@ public final class SqliteValueBox: ValueBox { } self.insertOrReplacePrimaryKeyStatements.removeAll() + for (_, statement) in self.insertOrIgnoreIndexKeyStatements { + statement.destroy() + } + self.insertOrIgnoreIndexKeyStatements.removeAll() + + for (_, statement) in self.insertOrIgnorePrimaryKeyStatements { + statement.destroy() + } + self.insertOrIgnorePrimaryKeyStatements.removeAll() + for (_, statement) in self.deleteStatements { statement.destroy() } diff --git a/submodules/Postbox/Sources/StorageBox/StorageBox.swift b/submodules/Postbox/Sources/StorageBox/StorageBox.swift new file mode 100644 index 0000000000..e3a2a51988 --- /dev/null +++ b/submodules/Postbox/Sources/StorageBox/StorageBox.swift @@ -0,0 +1,205 @@ +import Foundation +import SwiftSignalKit +import CryptoUtils + +public struct HashId: Hashable { + public let data: Data + + public init(data: Data) { + precondition(data.count == 16) + self.data = data + } +} + +private func md5Hash(_ data: Data) -> HashId { + let hashData = data.withUnsafeBytes { bytes -> Data in + return CryptoMD5(bytes.baseAddress!, Int32(bytes.count)) + } + return HashId(data: hashData) +} + +public final class StorageBox { + public struct Reference { + public var peerId: PeerId + public var messageNamespace: UInt8 + public var messageId: Int32 + + public init(peerId: PeerId, messageNamespace: UInt8, messageId: Int32) { + self.peerId = peerId + self.messageNamespace = messageNamespace + self.messageId = messageId + } + } + + public final class Entry { + public let id: Data + public let references: [Reference] + + init(id: Data, references: [Reference]) { + self.id = id + self.references = references + } + } + + public final class Logger { + private let impl: (String) -> Void + + public init(impl: @escaping (String) -> Void) { + self.impl = impl + } + + func log(_ string: @autoclosure () -> String) { + self.impl(string()) + } + } + + private final class Impl { + let queue: Queue + let logger: StorageBox.Logger + let basePath: String + let valueBox: SqliteValueBox + let hashIdToIdTable: ValueBoxTable + let idToReferenceTable: ValueBoxTable + + init(queue: Queue, logger: StorageBox.Logger, basePath: String) { + self.queue = queue + self.logger = logger + self.basePath = basePath + + let databasePath = self.basePath + "/db" + let _ = try? FileManager.default.createDirectory(atPath: databasePath, withIntermediateDirectories: true) + var valueBox = SqliteValueBox(basePath: databasePath, queue: queue, isTemporary: false, isReadOnly: false, useCaches: true, removeDatabaseOnError: true, encryptionParameters: nil, upgradeProgress: { _ in }) + if valueBox == nil { + let _ = try? FileManager.default.removeItem(atPath: databasePath) + valueBox = SqliteValueBox(basePath: databasePath, queue: queue, isTemporary: false, isReadOnly: false, useCaches: true, removeDatabaseOnError: true, encryptionParameters: nil, upgradeProgress: { _ in }) + } + guard let valueBox else { + preconditionFailure("Could not open database") + } + self.valueBox = valueBox + + self.hashIdToIdTable = ValueBoxTable(id: 5, keyType: .binary, compactValuesOnCreation: true) + self.idToReferenceTable = ValueBoxTable(id: 6, keyType: .binary, compactValuesOnCreation: true) + } + + func add(reference: Reference, to id: Data) { + self.valueBox.begin() + + let hashId = md5Hash(id) + + let mainKey = ValueBoxKey(length: hashId.data.count) + self.valueBox.setOrIgnore(self.hashIdToIdTable, key: mainKey, value: MemoryBuffer(data: id)) + + let idKey = ValueBoxKey(length: hashId.data.count + 8 + 1 + 4) + idKey.setData(0, value: hashId.data) + idKey.setInt64(hashId.data.count, value: reference.peerId.toInt64()) + idKey.setUInt8(hashId.data.count + 8, value: reference.messageNamespace) + idKey.setInt32(hashId.data.count + 8 + 1, value: reference.messageId) + self.valueBox.setOrIgnore(self.idToReferenceTable, key: idKey, value: MemoryBuffer()) + + self.valueBox.commit() + } + + func all() -> [Entry] { + var result: [Entry] = [] + + self.valueBox.begin() + + var currentId: Data? + var currentReferences: [Reference] = [] + + self.valueBox.scan(self.idToReferenceTable, keys: { key in + let id = key.getData(0, length: 16) + + let peerId = PeerId(key.getInt64(16)) + let messageNamespace: UInt8 = key.getUInt8(16 + 8) + let messageId = key.getInt32(16 + 8 + 1) + + let reference = Reference(peerId: peerId, messageNamespace: messageNamespace, messageId: messageId) + + if currentId == id { + currentReferences.append(reference) + } else { + if let currentId = currentId, !currentReferences.isEmpty { + result.append(StorageBox.Entry(id: currentId, references: currentReferences)) + currentReferences.removeAll(keepingCapacity: true) + } + currentId = id + currentReferences.append(reference) + } + + return true + }) + + self.valueBox.commit() + + return result + } + + func get(ids: [Data]) -> [Entry] { + var result: [Entry] = [] + + self.valueBox.begin() + + let idKey = ValueBoxKey(length: 16) + + for id in ids { + let hashId = md5Hash(id) + idKey.setData(0, value: hashId.data) + var currentReferences: [Reference] = [] + self.valueBox.range(self.idToReferenceTable, start: idKey, end: idKey.successor, keys: { key in + let peerId = PeerId(key.getInt64(16)) + let messageNamespace: UInt8 = key.getUInt8(16 + 8) + let messageId = key.getInt32(16 + 8 + 1) + + let reference = Reference(peerId: peerId, messageNamespace: messageNamespace, messageId: messageId) + + currentReferences.append(reference) + return true + }, limit: 0) + + if !currentReferences.isEmpty { + result.append(StorageBox.Entry(id: id, references: currentReferences)) + } + } + + self.valueBox.commit() + + return result + } + } + + private let queue: Queue + private let impl: QueueLocalObject + + public init(queue: Queue = Queue(name: "StorageBox"), logger: StorageBox.Logger, basePath: String) { + self.queue = queue + self.impl = QueueLocalObject(queue: queue, generate: { + return Impl(queue: queue, logger: logger, basePath: basePath) + }) + } + + public func add(reference: Reference, to id: Data) { + self.impl.with { impl in + impl.add(reference: reference, to: id) + } + } + + public func all() -> Signal<[Entry], NoError> { + return self.impl.signalWith { impl, subscriber in + subscriber.putNext(impl.all()) + subscriber.putCompletion() + + return EmptyDisposable + } + } + + public func get(ids: [Data]) -> Signal<[Entry], NoError> { + return self.impl.signalWith { impl, subscriber in + subscriber.putNext(impl.get(ids: ids)) + subscriber.putCompletion() + + return EmptyDisposable + } + } +} diff --git a/submodules/Postbox/Sources/ValueBoxKey.swift b/submodules/Postbox/Sources/ValueBoxKey.swift index e73b21ddf7..f192bde0f6 100644 --- a/submodules/Postbox/Sources/ValueBoxKey.swift +++ b/submodules/Postbox/Sources/ValueBoxKey.swift @@ -83,6 +83,15 @@ public struct ValueBoxKey: Equatable, Hashable, CustomStringConvertible, Compara memcpy(self.memory + offset, &varValue, 2) } + public func getData(_ offset: Int, length: Int) -> Data { + assert(offset >= 0 && offset + length <= self.length) + var value = Data(count: length) + let _ = value.withUnsafeMutableBytes { bytes in + memcpy(bytes.baseAddress!.assumingMemoryBound(to: UInt8.self), self.memory + offset, length) + } + return value + } + public func getInt32(_ offset: Int) -> Int32 { assert(offset >= 0 && offset + 4 <= self.length) var value: Int32 = 0 diff --git a/submodules/PremiumUI/Sources/PremiumIntroScreen.swift b/submodules/PremiumUI/Sources/PremiumIntroScreen.swift index 855f46ccee..c607410e30 100644 --- a/submodules/PremiumUI/Sources/PremiumIntroScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumIntroScreen.swift @@ -2285,7 +2285,7 @@ private final class PremiumIntroScreenComponent: CombinedComponent { var packReference: StickerPackReference? if let file = file { for attribute in file.attributes { - if case let .CustomEmoji(_, _, reference) = attribute { + if case let .CustomEmoji(_, _, _, reference) = attribute { packReference = reference } } @@ -2357,7 +2357,7 @@ private final class PremiumIntroScreenComponent: CombinedComponent { tapAction: { [weak state, weak environment] _, _ in if let emojiFile = state?.emojiFile, let controller = environment?.controller() as? PremiumIntroScreen, let navigationController = controller.navigationController as? NavigationController { for attribute in emojiFile.attributes { - if case let .CustomEmoji(_, _, packReference) = attribute, let packReference = packReference { + if case let .CustomEmoji(_, _, _, packReference) = attribute, let packReference = packReference { let controller = accountContext.sharedContext.makeStickerPackScreen(context: accountContext, updatedPresentationData: nil, mainStickerPack: packReference, stickerPacks: [packReference], loadedStickerPacks: loadedEmojiPack.flatMap { [$0] } ?? [], parentNavigationController: navigationController, sendSticker: { _, _, _ in return false }) diff --git a/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift b/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift index 482556ec01..9d704462be 100644 --- a/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift +++ b/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift @@ -425,7 +425,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { for item in featuredEmojiPack.topItems { for attribute in item.file.attributes { switch attribute { - case let .CustomEmoji(_, alt, _): + case let .CustomEmoji(_, _, alt, _): if filterList.contains(alt) { filteredFiles.append(item.file) } @@ -1395,7 +1395,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { } for attribute in item.file.attributes { switch attribute { - case let .CustomEmoji(_, alt, _): + case let .CustomEmoji(_, _, alt, _): if !item.file.isPremiumEmoji || hasPremium { if !alt.isEmpty, let keyword = allEmoticons[alt] { result.append((alt, item.file, keyword)) @@ -1424,7 +1424,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { content: .animation(animationData), itemFile: itemFile, subgroupId: nil, icon: .none, - accentTint: false + tintMode: animationData.isTemplate ? .primary : .none ) items.append(item) } @@ -1742,7 +1742,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { if additionalAnimation == nil && itemNode.item.isCustom { outer: for attribute in itemNode.item.stillAnimation.attributes { - if case let .CustomEmoji(_, alt, _) = attribute { + if case let .CustomEmoji(_, _, alt, _) = attribute { if let availableReactions = self.availableReactions { for availableReaction in availableReactions.reactions { if availableReaction.value == .builtin(alt) { diff --git a/submodules/StickerPackPreviewUI/Sources/StickerPackEmojisItem.swift b/submodules/StickerPackPreviewUI/Sources/StickerPackEmojisItem.swift index 98eca08ff2..60c0471896 100644 --- a/submodules/StickerPackPreviewUI/Sources/StickerPackEmojisItem.swift +++ b/submodules/StickerPackPreviewUI/Sources/StickerPackEmojisItem.swift @@ -203,7 +203,7 @@ final class StickerPackEmojisItemNode: GridItemNode { var emojiAttribute: ChatTextInputTextCustomEmojiAttribute? loop: for attribute in file.attributes { switch attribute { - case let .CustomEmoji(_, displayText, _): + case let .CustomEmoji(_, _, displayText, _): text = displayText emojiAttribute = ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: file.fileId.id, file: file) break loop @@ -229,7 +229,7 @@ final class StickerPackEmojisItemNode: GridItemNode { var emojiAttribute: ChatTextInputTextCustomEmojiAttribute? loop: for attribute in file.attributes { switch attribute { - case let .CustomEmoji(_, displayText, _): + case let .CustomEmoji(_, _, displayText, _): text = displayText emojiAttribute = ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: file.fileId.id, file: file) break loop @@ -354,7 +354,7 @@ final class StickerPackEmojisItemNode: GridItemNode { } else { updateItemLayerPlaceholder = true itemTransition = .immediate - + let animationData = EntityKeyboardAnimationData(file: item.file) itemLayer = EmojiPagerContentComponent.View.ItemLayer( item: EmojiPagerContentComponent.Item( @@ -363,7 +363,7 @@ final class StickerPackEmojisItemNode: GridItemNode { itemFile: item.file, subgroupId: nil, icon: .none, - accentTint: false + tintMode: animationData.isTemplate ? .primary : .none ), context: context, attemptSynchronousLoad: attemptSynchronousLoads, @@ -425,6 +425,15 @@ final class StickerPackEmojisItemNode: GridItemNode { self.visibleItemLayers[itemId] = itemLayer } + switch itemLayer.item.tintMode { + case .none: + break + case .accent: + itemLayer.layerTintColor = theme.list.itemAccentColor.cgColor + case .primary: + itemLayer.layerTintColor = theme.list.itemPrimaryTextColor.cgColor + } + var itemFrame = itemLayout.frame(itemIndex: index) itemFrame.origin.x += floor((itemFrame.width - itemVisibleFitSize.width) / 2.0) diff --git a/submodules/StickerPackPreviewUI/Sources/StickerPackScreen.swift b/submodules/StickerPackPreviewUI/Sources/StickerPackScreen.swift index 0808de4c7d..934129b7f5 100644 --- a/submodules/StickerPackPreviewUI/Sources/StickerPackScreen.swift +++ b/submodules/StickerPackPreviewUI/Sources/StickerPackScreen.swift @@ -66,7 +66,7 @@ private struct StickerPackPreviewGridTransaction { let scrollToItem: GridNodeScrollToItem? init(previousList: [StickerPackPreviewGridEntry], list: [StickerPackPreviewGridEntry], context: AccountContext, interaction: StickerPackPreviewInteraction, theme: PresentationTheme, strings: PresentationStrings, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, scrollToItem: GridNodeScrollToItem?) { - let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: previousList, rightList: list) + let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: previousList, rightList: list) self.deletions = deleteIndices self.insertions = indicesAndItems.map { GridNodeInsertItem(index: $0.0, item: $0.1.item(context: context, interaction: interaction, theme: theme, strings: strings, animationCache: animationCache, animationRenderer: animationRenderer), previousIndex: $0.2) } diff --git a/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaFile.swift b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaFile.swift index eab7c442fe..99f83f6aba 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaFile.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaFile.swift @@ -112,8 +112,8 @@ func telegramMediaFileAttributesFromApiAttributes(_ attributes: [Api.DocumentAtt result.append(.Audio(isVoice: isVoice, duration: Int(duration), title: title, performer: performer, waveform: waveformBuffer)) case let .documentAttributeCustomEmoji(flags, alt, stickerSet): let isFree = (flags & (1 << 0)) != 0 - let paintToText = (flags & (1 << 1)) != 0 - result.append(.CustomEmoji(isPremium: !isFree, paintToText: paintToText, alt: alt, packReference: StickerPackReference(apiInputSet: stickerSet))) + let isSingleColor = (flags & (1 << 1)) != 0 + result.append(.CustomEmoji(isPremium: !isFree, isSingleColor: isSingleColor, alt: alt, packReference: StickerPackReference(apiInputSet: stickerSet))) } } return result diff --git a/submodules/TelegramCore/Sources/Network/FetchedMediaResource.swift b/submodules/TelegramCore/Sources/Network/FetchedMediaResource.swift index b1dff14b48..b15de96770 100644 --- a/submodules/TelegramCore/Sources/Network/FetchedMediaResource.swift +++ b/submodules/TelegramCore/Sources/Network/FetchedMediaResource.swift @@ -29,6 +29,26 @@ public func fetchedMediaResource(mediaBox: MediaBox, reference: MediaResourceRef return fetchedMediaResource(mediaBox: mediaBox, reference: reference, ranges: range.flatMap({ [$0] }), statsCategory: statsCategory, reportResultStatus: reportResultStatus, preferBackgroundReferenceRevalidation: preferBackgroundReferenceRevalidation, continueInBackground: continueInBackground) } +public extension MediaResourceStorageLocation { + convenience init?(reference: MediaResourceReference) { + switch reference { + case let .media(media, _): + switch media { + case let .message(message, _): + if let id = message.id { + self.init(peerId: id.peerId, messageId: id) + } else { + return nil + } + default: + return nil + } + default: + return nil + } + } +} + public func fetchedMediaResource(mediaBox: MediaBox, reference: MediaResourceReference, ranges: [(Range, MediaBoxFetchPriority)]?, statsCategory: MediaResourceStatsCategory = .generic, reportResultStatus: Bool = false, preferBackgroundReferenceRevalidation: Bool = false, continueInBackground: Bool = false) -> Signal { var isRandomAccessAllowed = true switch reference { @@ -44,14 +64,24 @@ public func fetchedMediaResource(mediaBox: MediaBox, reference: MediaResourceRef if let ranges = ranges { let signals = ranges.map { (range, priority) -> Signal in - return mediaBox.fetchedResourceData(reference.resource, in: range, priority: priority, parameters: MediaResourceFetchParameters(tag: TelegramMediaResourceFetchTag(statsCategory: statsCategory), info: TelegramCloudMediaResourceFetchInfo(reference: reference, preferBackgroundReferenceRevalidation: preferBackgroundReferenceRevalidation, continueInBackground: continueInBackground), isRandomAccessAllowed: isRandomAccessAllowed)) + return mediaBox.fetchedResourceData(reference.resource, in: range, priority: priority, parameters: MediaResourceFetchParameters( + tag: TelegramMediaResourceFetchTag(statsCategory: statsCategory), + info: TelegramCloudMediaResourceFetchInfo(reference: reference, preferBackgroundReferenceRevalidation: preferBackgroundReferenceRevalidation, continueInBackground: continueInBackground), + location: MediaResourceStorageLocation(reference: reference), + isRandomAccessAllowed: isRandomAccessAllowed + )) } return combineLatest(signals) |> ignoreValues |> map { _ -> FetchResourceSourceType in } |> then(.single(.local)) } else { - return mediaBox.fetchedResource(reference.resource, parameters: MediaResourceFetchParameters(tag: TelegramMediaResourceFetchTag(statsCategory: statsCategory), info: TelegramCloudMediaResourceFetchInfo(reference: reference, preferBackgroundReferenceRevalidation: preferBackgroundReferenceRevalidation, continueInBackground: continueInBackground), isRandomAccessAllowed: isRandomAccessAllowed), implNext: reportResultStatus) + return mediaBox.fetchedResource(reference.resource, parameters: MediaResourceFetchParameters( + tag: TelegramMediaResourceFetchTag(statsCategory: statsCategory), + info: TelegramCloudMediaResourceFetchInfo(reference: reference, preferBackgroundReferenceRevalidation: preferBackgroundReferenceRevalidation, continueInBackground: continueInBackground), + location: MediaResourceStorageLocation(reference: reference), + isRandomAccessAllowed: isRandomAccessAllowed + ), implNext: reportResultStatus) } } diff --git a/submodules/TelegramCore/Sources/State/Fetch.swift b/submodules/TelegramCore/Sources/State/Fetch.swift index b6bced6a66..7ec872f6b6 100644 --- a/submodules/TelegramCore/Sources/State/Fetch.swift +++ b/submodules/TelegramCore/Sources/State/Fetch.swift @@ -75,7 +75,12 @@ func fetchResource(account: Account, resource: MediaResource, intervals: Signal< return .fail(.generic) } return .single(.dataPart(resourceOffset: 0, data: Data(), range: 0 ..< 0, complete: false)) - |> then(fetchCloudMediaLocation(account: account, resource: cloudResource, datacenterId: cloudResource.datacenterId, size: resource.size == 0 ? nil : resource.size, intervals: intervals, parameters: MediaResourceFetchParameters(tag: nil, info: TelegramCloudMediaResourceFetchInfo(reference: .standalone(resource: file.file.resource), preferBackgroundReferenceRevalidation: false, continueInBackground: false), isRandomAccessAllowed: true))) + |> then(fetchCloudMediaLocation(account: account, resource: cloudResource, datacenterId: cloudResource.datacenterId, size: resource.size == 0 ? nil : resource.size, intervals: intervals, parameters: MediaResourceFetchParameters( + tag: nil, + info: TelegramCloudMediaResourceFetchInfo(reference: .standalone(resource: file.file.resource), preferBackgroundReferenceRevalidation: false, continueInBackground: false), + location: nil, + isRandomAccessAllowed: true + ))) } } return nil diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaFile.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaFile.swift index dce8c7ffc7..3f831c4da7 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaFile.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaFile.swift @@ -190,7 +190,7 @@ public struct TelegramMediaVideoFlags: OptionSet { public static let supportsStreaming = TelegramMediaVideoFlags(rawValue: 1 << 1) } -public struct StickerMaskCoords: PostboxCoding { +public struct StickerMaskCoords: PostboxCoding, Equatable { public let n: Int32 public let x: Double public let y: Double @@ -218,7 +218,7 @@ public struct StickerMaskCoords: PostboxCoding { } } -public enum TelegramMediaFileAttribute: PostboxCoding { +public enum TelegramMediaFileAttribute: PostboxCoding, Equatable { case FileName(fileName: String) case Sticker(displayText: String, packReference: StickerPackReference?, maskData: StickerMaskCoords?) case ImageSize(size: PixelDimensions) @@ -229,7 +229,7 @@ public enum TelegramMediaFileAttribute: PostboxCoding { case hintFileIsLarge case hintIsValidated case NoPremium - case CustomEmoji(isPremium: Bool, paintToText: Bool, alt: String, packReference: StickerPackReference?) + case CustomEmoji(isPremium: Bool, isSingleColor: Bool, alt: String, packReference: StickerPackReference?) public init(decoder: PostboxDecoder) { let type: Int32 = decoder.decodeInt32ForKey("t", orElse: 0) @@ -260,7 +260,7 @@ public enum TelegramMediaFileAttribute: PostboxCoding { case typeNoPremium: self = .NoPremium case typeCustomEmoji: - self = .CustomEmoji(isPremium: decoder.decodeBoolForKey("ip", orElse: true), paintToText: decoder.decodeBoolForKey("ptt", orElse: false), alt: decoder.decodeStringForKey("dt", orElse: ""), packReference: decoder.decodeObjectForKey("pr", decoder: { StickerPackReference(decoder: $0) }) as? StickerPackReference) + self = .CustomEmoji(isPremium: decoder.decodeBoolForKey("ip", orElse: true), isSingleColor: decoder.decodeBoolForKey("sc", orElse: false), alt: decoder.decodeStringForKey("dt", orElse: ""), packReference: decoder.decodeObjectForKey("pr", decoder: { StickerPackReference(decoder: $0) }) as? StickerPackReference) default: preconditionFailure() } @@ -317,10 +317,10 @@ public enum TelegramMediaFileAttribute: PostboxCoding { encoder.encodeInt32(typeHintIsValidated, forKey: "t") case .NoPremium: encoder.encodeInt32(typeNoPremium, forKey: "t") - case let .CustomEmoji(isPremium, paintToText, alt, packReference): + case let .CustomEmoji(isPremium, isSingleColor, alt, packReference): encoder.encodeInt32(typeCustomEmoji, forKey: "t") encoder.encodeBool(isPremium, forKey: "ip") - encoder.encodeBool(paintToText, forKey: "ptt") + encoder.encodeBool(isSingleColor, forKey: "sc") encoder.encodeString(alt, forKey: "dt") if let packReference = packReference { encoder.encodeObject(packReference, forKey: "pr") @@ -738,6 +738,10 @@ public final class TelegramMediaFile: Media, Equatable, Codable { return false } + if self.attributes != other.attributes { + return false + } + return true } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Resources/CollectCacheUsageStats.swift b/submodules/TelegramCore/Sources/TelegramEngine/Resources/CollectCacheUsageStats.swift index cc1e9fe5fc..8a48c5f4a7 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Resources/CollectCacheUsageStats.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Resources/CollectCacheUsageStats.swift @@ -53,6 +53,203 @@ private final class CacheUsageStatsState { } func _internal_collectCacheUsageStats(account: Account, peerId: PeerId? = nil, additionalCachePaths: [String] = [], logFilesPath: String? = nil) -> Signal { + if "".isEmpty { + return account.postbox.mediaBox.collectAllResourceUsage() + |> mapToSignal { resourceList -> Signal in + return account.postbox.mediaBox.storageBox.get(ids: resourceList.compactMap { item -> Data? in + return item.id?.data(using: .utf8) + }) + |> mapToSignal { entries -> Signal in + return account.postbox.transaction { transaction -> CacheUsageStatsResult in + var media: [PeerId: [PeerCacheUsageCategory: [MediaId: Int64]]] = [:] + var mediaResourceIds: [MediaId: [MediaResourceId]] = [:] + + media.removeAll() + mediaResourceIds.removeAll() + + let mediaBox = account.postbox.mediaBox + + var totalSize: Int64 = 0 + var mediaSize: Int64 = 0 + + var processedResourceIds = Set() + + for entry in entries { + let resourceId = MediaResourceId(String(data: entry.id, encoding: .utf8)!) + let resourceSize = mediaBox.resourceUsage(id: resourceId) + if resourceSize != 0 { + totalSize += resourceSize + + for reference in entry.references { + if let message = transaction.getMessage(MessageId(peerId: reference.peerId, namespace: MessageId.Namespace(reference.messageNamespace), id: reference.messageId)) { + for mediaItem in message.media { + guard let mediaId = mediaItem.id else { + continue + } + var category: PeerCacheUsageCategory? + if let _ = mediaItem as? TelegramMediaImage { + category = .image + } else if let mediaItem = mediaItem as? TelegramMediaFile { + if mediaItem.isMusic || mediaItem.isVoice { + category = .audio + } else if mediaItem.isVideo { + category = .video + } else { + category = .file + } + } + if let category = category { + mediaSize += resourceSize + processedResourceIds.insert(resourceId.stringRepresentation) + + media[reference.peerId, default: [:]][category, default: [:]][mediaId, default: 0] += resourceSize + if let index = mediaResourceIds.index(forKey: mediaId) { + if !mediaResourceIds[index].value.contains(resourceId) { + mediaResourceIds[mediaId]?.append(resourceId) + } + } else { + mediaResourceIds[mediaId] = [resourceId] + } + } + } + } + } + } + } + + var peers: [PeerId: Peer] = [:] + for peerId in media.keys { + if let peer = transaction.getPeer(peerId) { + peers[peer.id] = peer + } + } + + var tempPaths: [String] = [] + var tempSize: Int64 = 0 + #if os(iOS) + if let enumerator = FileManager.default.enumerator(at: URL(fileURLWithPath: NSTemporaryDirectory()), includingPropertiesForKeys: [.isDirectoryKey, .fileAllocatedSizeKey, .isSymbolicLinkKey]) { + for url in enumerator { + if let url = url as? URL { + if let isDirectoryValue = (try? url.resourceValues(forKeys: Set([.isDirectoryKey])))?.isDirectory, isDirectoryValue { + tempPaths.append(url.path) + } else if let fileSizeValue = (try? url.resourceValues(forKeys: Set([.fileAllocatedSizeKey])))?.fileAllocatedSize { + tempPaths.append(url.path) + + if let isSymbolicLinkValue = (try? url.resourceValues(forKeys: Set([.isSymbolicLinkKey])))?.isSymbolicLink, isSymbolicLinkValue { + } else { + tempSize += Int64(fileSizeValue) + } + } + } + } + } + #endif + + var immutableSize: Int64 = 0 + if let files = try? FileManager.default.contentsOfDirectory(at: URL(fileURLWithPath: account.basePath + "/postbox/db"), includingPropertiesForKeys: [URLResourceKey.fileSizeKey], options: []) { + for url in files { + if let fileSize = (try? url.resourceValues(forKeys: Set([.fileSizeKey])))?.fileSize { + immutableSize += Int64(fileSize) + } + } + } + if let logFilesPath = logFilesPath, let files = try? FileManager.default.contentsOfDirectory(at: URL(fileURLWithPath: logFilesPath), includingPropertiesForKeys: [URLResourceKey.fileSizeKey], options: []) { + for url in files { + if let fileSize = (try? url.resourceValues(forKeys: Set([.fileSizeKey])))?.fileSize { + immutableSize += Int64(fileSize) + } + } + } + + for additionalPath in additionalCachePaths { + if let enumerator = FileManager.default.enumerator(at: URL(fileURLWithPath: additionalPath), includingPropertiesForKeys: [.isDirectoryKey, .fileAllocatedSizeKey, .isSymbolicLinkKey]) { + for url in enumerator { + if let url = url as? URL { + if let isDirectoryValue = (try? url.resourceValues(forKeys: Set([.isDirectoryKey])))?.isDirectory, isDirectoryValue { + } else if let fileSizeValue = (try? url.resourceValues(forKeys: Set([.fileAllocatedSizeKey])))?.fileAllocatedSize { + tempPaths.append(url.path) + + if let isSymbolicLinkValue = (try? url.resourceValues(forKeys: Set([.isSymbolicLinkKey])))?.isSymbolicLink, isSymbolicLinkValue { + } else { + tempSize += Int64(fileSizeValue) + } + } + } + } + } + } + + var otherSize: Int64 = 0 + var otherPaths: [String] = [] + + for (id, name, size) in resourceList { + if size == 0 { + continue + } + if let id = id, processedResourceIds.contains(id) { + continue + } + otherSize += size + otherPaths.append(name) + } + + var cacheSize: Int64 = 0 + let basePath = account.postbox.mediaBox.basePath + if let enumerator = FileManager.default.enumerator(at: URL(fileURLWithPath: basePath + "/cache"), includingPropertiesForKeys: [.fileSizeKey], options: [.skipsHiddenFiles, .skipsSubdirectoryDescendants], errorHandler: nil) { + loop: for url in enumerator { + if let url = url as? URL { + if let value = (try? url.resourceValues(forKeys: Set([.fileSizeKey])))?.fileSize, value != 0 { + otherPaths.append("cache/" + url.lastPathComponent) + cacheSize += 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 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 { + otherPaths.append("\(subdirectoryPath)/" + url.lastPathComponent) + cacheSize += Int64(value) + } + } + } + } + } + + processRecursive(directoryPath: basePath + "/animation-cache", subdirectoryPath: "animation-cache") + + if let enumerator = FileManager.default.enumerator(at: URL(fileURLWithPath: basePath + "/short-cache"), includingPropertiesForKeys: [.fileSizeKey], options: [.skipsHiddenFiles, .skipsSubdirectoryDescendants], errorHandler: nil) { + loop: for url in enumerator { + if let url = url as? URL { + if let value = (try? url.resourceValues(forKeys: Set([.fileSizeKey])))?.fileSize, value != 0 { + otherPaths.append("short-cache/" + url.lastPathComponent) + cacheSize += Int64(value) + } + } + } + } + + return .result(CacheUsageStats( + media: media, + mediaResourceIds: mediaResourceIds, + peers: peers, + otherSize: otherSize, + otherPaths: otherPaths, + cacheSize: 0, + tempPaths: tempPaths, + tempSize: tempSize, + immutableSize: immutableSize + )) + } + } + } + } + let initialState = CacheUsageStatsState() if let peerId = peerId { initialState.lowerBound = MessageIndex.lowerBound(peerId: peerId) @@ -277,7 +474,8 @@ func _internal_collectCacheUsageStats(account: Account, peerId: PeerId? = nil, a let signal = (fetch |> mapToSignal { mediaByPeer, mediaRefs, updatedLowerBound -> Signal in return process(mediaByPeer, mediaRefs, updatedLowerBound) - }) |> restart + }) + |> restart return signal |> `catch` { error in switch error { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Resources/TelegramEngineResources.swift b/submodules/TelegramCore/Sources/TelegramEngine/Resources/TelegramEngineResources.swift index 82e7fd11e1..dbf3f6febf 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Resources/TelegramEngineResources.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Resources/TelegramEngineResources.swift @@ -293,6 +293,7 @@ public extension TelegramEngine { preferBackgroundReferenceRevalidation: false, continueInBackground: false ), + location: nil, isRandomAccessAllowed: true )) |> map { result -> EngineMediaResource.Fetch.Result in diff --git a/submodules/TelegramUI/Components/EmojiStatusComponent/Sources/EmojiStatusComponent.swift b/submodules/TelegramUI/Components/EmojiStatusComponent/Sources/EmojiStatusComponent.swift index 1f962744bf..bf2fbcb135 100644 --- a/submodules/TelegramUI/Components/EmojiStatusComponent/Sources/EmojiStatusComponent.swift +++ b/submodules/TelegramUI/Components/EmojiStatusComponent/Sources/EmojiStatusComponent.swift @@ -442,7 +442,10 @@ public final class EmojiStatusComponent: Component { var accentTint = false if let _ = emojiThemeColor { for attribute in emojiFile.attributes { - if case let .CustomEmoji(_, _, packReference) = attribute { + if case let .CustomEmoji(_, isSingleColor, _, packReference) = attribute { + if isSingleColor { + accentTint = true + } switch packReference { case let .id(id, _): if id == 773947703670341676 || id == 2964141614563343 { @@ -456,8 +459,10 @@ public final class EmojiStatusComponent: Component { } if accentTint { animationLayer.contentTintColor = emojiThemeColor + animationLayer.dynamicColor = emojiThemeColor } else { animationLayer.contentTintColor = nil + animationLayer.dynamicColor = nil } animationLayer.frame = CGRect(origin: CGPoint(), size: size) diff --git a/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/Sources/EmojiStatusSelectionComponent.swift b/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/Sources/EmojiStatusSelectionComponent.swift index dc34157515..d20bbd009d 100644 --- a/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/Sources/EmojiStatusSelectionComponent.swift +++ b/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/Sources/EmojiStatusSelectionComponent.swift @@ -329,7 +329,7 @@ public final class EmojiStatusSelectionController: ViewController { for item in featuredEmojiPack.topItems { for attribute in item.file.attributes { switch attribute { - case let .CustomEmoji(_, alt, _): + case let .CustomEmoji(_, _, alt, _): if filterList.contains(alt) { filteredFiles.append(item.file) } @@ -487,7 +487,7 @@ public final class EmojiStatusSelectionController: ViewController { } for attribute in item.file.attributes { switch attribute { - case let .CustomEmoji(_, alt, _): + case let .CustomEmoji(_, _, alt, _): if !item.file.isPremiumEmoji || hasPremium { if !alt.isEmpty, let keyword = allEmoticons[alt] { result.append((alt, item.file, keyword)) @@ -516,7 +516,7 @@ public final class EmojiStatusSelectionController: ViewController { content: .animation(animationData), itemFile: itemFile, subgroupId: nil, icon: .none, - accentTint: false + tintMode: animationData.isTemplate ? .primary : .none ) items.append(item) } @@ -645,7 +645,10 @@ public final class EmojiStatusSelectionController: ViewController { } else if let itemFile = item.itemFile { var useCleanEffect = false for attribute in itemFile.attributes { - if case let .CustomEmoji(_, _, packReference) = attribute { + if case let .CustomEmoji(_, isSingleColor, _, packReference) = attribute { + if isSingleColor { + useCleanEffect = true + } switch packReference { case let .id(id, _): if id == 773947703670341676 || id == 2964141614563343 { @@ -692,8 +695,13 @@ public final class EmojiStatusSelectionController: ViewController { placeholderColor: UIColor(white: 0.0, alpha: 0.0), pointSize: CGSize(width: 32.0, height: 32.0) ) - if item.accentTint { + switch item.tintMode { + case .accent: baseItemLayer.contentTintColor = self.presentationData.theme.list.itemAccentColor + case .primary: + baseItemLayer.contentTintColor = self.presentationData.theme.list.itemPrimaryTextColor + case .none: + break } if let sublayers = animationLayer.sublayers { @@ -1001,7 +1009,7 @@ public final class EmojiStatusSelectionController: ViewController { if let itemFile = previewItem.item.itemFile { attributeLoop: for attribute in itemFile.attributes { switch attribute { - case let .CustomEmoji(_, alt, _): + case let .CustomEmoji(_, _, alt, _): emojiString = alt break attributeLoop default: @@ -1165,7 +1173,7 @@ public final class EmojiStatusSelectionController: ViewController { if let itemFile = item.itemFile { attributeLoop: for attribute in itemFile.attributes { switch attribute { - case let .CustomEmoji(_, alt, _): + case let .CustomEmoji(_, _, alt, _): emojiString = alt break attributeLoop default: diff --git a/submodules/TelegramUI/Components/EmojiSuggestionsComponent/Sources/EmojiSuggestionsComponent.swift b/submodules/TelegramUI/Components/EmojiSuggestionsComponent/Sources/EmojiSuggestionsComponent.swift index 7f928927f1..f79dadb7a6 100644 --- a/submodules/TelegramUI/Components/EmojiSuggestionsComponent/Sources/EmojiSuggestionsComponent.swift +++ b/submodules/TelegramUI/Components/EmojiSuggestionsComponent/Sources/EmojiSuggestionsComponent.swift @@ -59,7 +59,7 @@ public final class EmojiSuggestionsComponent: Component { } for attribute in item.file.attributes { switch attribute { - case let .CustomEmoji(_, alt, _): + case let .CustomEmoji(_, _, alt, _): if alt == query || (!normalizedQuery.isEmpty && alt == normalizedQuery) { if !item.file.isPremiumEmoji || hasPremium { if !existingIds.contains(item.file.fileId) { @@ -78,7 +78,7 @@ public final class EmojiSuggestionsComponent: Component { for item in featuredPack.topItems { for attribute in item.file.attributes { switch attribute { - case let .CustomEmoji(_, alt, _): + case let .CustomEmoji(_, _, alt, _): if alt == query || (!normalizedQuery.isEmpty && alt == normalizedQuery) { if !item.file.isPremiumEmoji || hasPremium { if !existingIds.contains(item.file.fileId) { diff --git a/submodules/TelegramUI/Components/EmojiTextAttachmentView/Sources/EmojiTextAttachmentView.swift b/submodules/TelegramUI/Components/EmojiTextAttachmentView/Sources/EmojiTextAttachmentView.swift index adcf94e737..4a3215b924 100644 --- a/submodules/TelegramUI/Components/EmojiTextAttachmentView/Sources/EmojiTextAttachmentView.swift +++ b/submodules/TelegramUI/Components/EmojiTextAttachmentView/Sources/EmojiTextAttachmentView.swift @@ -96,7 +96,7 @@ public extension AnimationCacheAnimationType { } } -public func animationCacheFetchFile(context: AccountContext, resource: MediaResourceReference, type: AnimationCacheAnimationType, keyframeOnly: Bool) -> (AnimationCacheFetchOptions) -> Disposable { +public func animationCacheFetchFile(context: AccountContext, resource: MediaResourceReference, type: AnimationCacheAnimationType, keyframeOnly: Bool, customColor: UIColor?) -> (AnimationCacheFetchOptions) -> Disposable { return { options in let source = AnimatedStickerResourceSource(account: context.account, resource: resource.resource, fitzModifier: nil, isVideo: false) @@ -107,15 +107,15 @@ public func animationCacheFetchFile(context: AccountContext, resource: MediaReso switch type { case .video: - cacheVideoAnimation(path: result, width: Int(options.size.width), height: Int(options.size.height), writer: options.writer, firstFrameOnly: options.firstFrameOnly) + cacheVideoAnimation(path: result, width: Int(options.size.width), height: Int(options.size.height), writer: options.writer, firstFrameOnly: options.firstFrameOnly, customColor: customColor) case .lottie: guard let data = try? Data(contentsOf: URL(fileURLWithPath: result)) else { options.writer.finish() return } - cacheLottieAnimation(data: data, width: Int(options.size.width), height: Int(options.size.height), keyframeOnly: keyframeOnly, writer: options.writer, firstFrameOnly: options.firstFrameOnly) + cacheLottieAnimation(data: data, width: Int(options.size.width), height: Int(options.size.height), keyframeOnly: keyframeOnly, writer: options.writer, firstFrameOnly: options.firstFrameOnly, customColor: customColor) case .still: - cacheStillSticker(path: result, width: Int(options.size.width), height: Int(options.size.height), writer: options.writer) + cacheStillSticker(path: result, width: Int(options.size.width), height: Int(options.size.height), writer: options.writer, customColor: customColor) } }) @@ -153,6 +153,7 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget { private let pixelSize: CGSize private var isDisplayingPlaceholder: Bool = false + private var didProcessTintColor: Bool = false public private(set) var file: TelegramMediaFile? private var infoDisposable: Disposable? @@ -168,6 +169,14 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget { } } + public var dynamicColor: UIColor? { + didSet { + if self.dynamicColor != oldValue { + self.updateTintColor() + } + } + } + private var currentLoopCount: Int = 0 private var isInHierarchyValue: Bool = false @@ -179,13 +188,14 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget { } } - public init(context: AccountContext, attemptSynchronousLoad: Bool, emoji: ChatTextInputTextCustomEmojiAttribute, file: TelegramMediaFile?, cache: AnimationCache, renderer: MultiAnimationRenderer, unique: Bool = false, placeholderColor: UIColor, pointSize: CGSize, loopCount: Int? = nil) { + public init(context: AccountContext, attemptSynchronousLoad: Bool, emoji: ChatTextInputTextCustomEmojiAttribute, file: TelegramMediaFile?, cache: AnimationCache, renderer: MultiAnimationRenderer, unique: Bool = false, placeholderColor: UIColor, pointSize: CGSize, dynamicColor: UIColor? = nil, loopCount: Int? = nil) { self.context = context self.emoji = emoji self.cache = cache self.renderer = renderer self.unique = unique self.placeholderColor = placeholderColor + self.dynamicColor = dynamicColor self.loopCount = loopCount let scale = min(2.0, UIScreenScale) @@ -239,7 +249,18 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget { private func updateTintColor() { if !self.isDisplayingPlaceholder { - self.layerTintColor = self.contentTintColor?.cgColor + var customColor = self.contentTintColor + if let file = self.file { + for attribute in file.attributes { + if case let .CustomEmoji(_, isSingleColor, _, _) = attribute { + if isSingleColor { + customColor = self.dynamicColor + } + } + } + } + + self.layerTintColor = customColor?.cgColor } else { self.layerTintColor = nil } @@ -311,10 +332,17 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget { self.loadAnimation() } else { + var isTemplate = false + for attribute in file.attributes { + if case let .CustomEmoji(_, isSingleColor, _, _) = attribute { + isTemplate = isSingleColor + } + } + let pointSize = self.pointSize let placeholderColor = self.placeholderColor let isThumbnailCancelled = Atomic(value: false) - self.loadDisposable = self.renderer.loadFirstFrame(target: self, cache: self.cache, itemId: file.resource.id.stringRepresentation, size: self.pixelSize, fetch: animationCacheFetchFile(context: self.context, resource: .media(media: .standalone(media: file), resource: file.resource), type: AnimationCacheAnimationType(file: file), keyframeOnly: true), completion: { [weak self] result, isFinal in + self.loadDisposable = self.renderer.loadFirstFrame(target: self, cache: self.cache, itemId: file.resource.id.stringRepresentation, size: self.pixelSize, fetch: animationCacheFetchFile(context: self.context, resource: .media(media: .standalone(media: file), resource: file.resource), type: AnimationCacheAnimationType(file: file), keyframeOnly: true, customColor: isTemplate ? .white : nil), completion: { [weak self] result, isFinal in if !result { MultiAnimationRendererImpl.firstFrameQueue.async { let image = generateStickerPlaceholderImage(data: file.immediateThumbnailData, size: pointSize, scale: min(2.0, UIScreenScale), imageSize: file.dimensions?.cgSize ?? CGSize(width: 512.0, height: 512.0), backgroundColor: nil, foregroundColor: placeholderColor) @@ -350,11 +378,18 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget { return } + var isTemplate = false + for attribute in file.attributes { + if case let .CustomEmoji(_, isSingleColor, _, _) = attribute { + isTemplate = isSingleColor + } + } + let context = self.context if file.isAnimatedSticker || file.isVideoEmoji { let keyframeOnly = self.pixelSize.width >= 120.0 - self.disposable = renderer.add(target: self, cache: self.cache, itemId: file.resource.id.stringRepresentation, unique: self.unique, size: self.pixelSize, fetch: animationCacheFetchFile(context: context, resource: .media(media: .standalone(media: file), resource: file.resource), type: AnimationCacheAnimationType(file: file), keyframeOnly: keyframeOnly)) + self.disposable = renderer.add(target: self, cache: self.cache, itemId: file.resource.id.stringRepresentation, unique: self.unique, size: self.pixelSize, fetch: animationCacheFetchFile(context: context, resource: .media(media: .standalone(media: file), resource: file.resource), type: AnimationCacheAnimationType(file: file), keyframeOnly: keyframeOnly, customColor: isTemplate ? .white : nil)) } else { self.disposable = renderer.add(target: self, cache: self.cache, itemId: file.resource.id.stringRepresentation, unique: self.unique, size: self.pixelSize, fetch: { options in let dataDisposable = context.account.postbox.mediaBox.resourceData(file.resource).start(next: { result in @@ -362,7 +397,7 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget { return } - cacheStillSticker(path: result.path, width: Int(options.size.width), height: Int(options.size.height), writer: options.writer) + cacheStillSticker(path: result.path, width: Int(options.size.width), height: Int(options.size.height), writer: options.writer, customColor: isTemplate ? .white : nil) }) let fetchDisposable = freeMediaFileResourceInteractiveFetched(account: context.account, fileReference: .customEmoji(media: file), resource: file.resource).start() @@ -405,6 +440,10 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget { self.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) } } else { + if !self.didProcessTintColor { + //self.didProcessTintColor = true + self.updateTintColor() + } self.contents = contents } @@ -433,6 +472,10 @@ public final class EmojiTextAttachmentView: UIView { fatalError("init(coder:) has not been implemented") } + public func updateTextColor(_ textColor: UIColor) { + self.contentLayer.dynamicColor = textColor + } + override public func layoutSubviews() { super.layoutSubviews() diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift index dde1f64f95..6421a421b7 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift @@ -222,14 +222,16 @@ public final class EntityKeyboardAnimationData: Equatable { public let dimensions: CGSize public let immediateThumbnailData: Data? public let isReaction: Bool + public let isTemplate: Bool - public init(id: Id, type: ItemType, resource: MediaResourceReference, dimensions: CGSize, immediateThumbnailData: Data?, isReaction: Bool) { + public init(id: Id, type: ItemType, resource: MediaResourceReference, dimensions: CGSize, immediateThumbnailData: Data?, isReaction: Bool, isTemplate: Bool) { self.id = id self.type = type self.resource = resource self.dimensions = dimensions self.immediateThumbnailData = immediateThumbnailData self.isReaction = isReaction + self.isTemplate = isTemplate } public convenience init(file: TelegramMediaFile, isReaction: Bool = false) { @@ -241,7 +243,13 @@ public final class EntityKeyboardAnimationData: Equatable { } else { type = .still } - self.init(id: .file(file.fileId), type: type, resource: .standalone(resource: file.resource), dimensions: file.dimensions?.cgSize ?? CGSize(width: 512.0, height: 512.0), immediateThumbnailData: file.immediateThumbnailData, isReaction: isReaction) + var isTemplate = false + for attribute in file.attributes { + if case let .CustomEmoji(_, isSingleColor, _, _) = attribute { + isTemplate = isSingleColor + } + } + self.init(id: .file(file.fileId), type: type, resource: .standalone(resource: file.resource), dimensions: file.dimensions?.cgSize ?? CGSize(width: 512.0, height: 512.0), immediateThumbnailData: file.immediateThumbnailData, isReaction: isReaction, isTemplate: isTemplate) } public static func ==(lhs: EntityKeyboardAnimationData, rhs: EntityKeyboardAnimationData) -> Bool { @@ -2201,12 +2209,18 @@ public final class EmojiPagerContentComponent: Component { case premium } + public enum TintMode { + case none + case accent + case primary + } + public let animationData: EntityKeyboardAnimationData? public let content: ItemContent public let itemFile: TelegramMediaFile? public let subgroupId: Int32? public let icon: Icon - public let accentTint: Bool + public let tintMode: TintMode public init( animationData: EntityKeyboardAnimationData?, @@ -2214,14 +2228,14 @@ public final class EmojiPagerContentComponent: Component { itemFile: TelegramMediaFile?, subgroupId: Int32?, icon: Icon, - accentTint: Bool + tintMode: TintMode ) { self.animationData = animationData self.content = content self.itemFile = itemFile self.subgroupId = subgroupId self.icon = icon - self.accentTint = accentTint + self.tintMode = tintMode } public static func ==(lhs: Item, rhs: Item) -> Bool { @@ -2243,7 +2257,7 @@ public final class EmojiPagerContentComponent: Component { if lhs.icon != rhs.icon { return false } - if lhs.accentTint != rhs.accentTint { + if lhs.tintMode != rhs.tintMode { return false } @@ -2894,14 +2908,14 @@ public final class EmojiPagerContentComponent: Component { return } - strongSelf.disposable = renderer.add(target: strongSelf, cache: cache, itemId: animationData.resource.resource.id.stringRepresentation, unique: false, size: pixelSize, fetch: animationCacheFetchFile(context: context, resource: animationData.resource, type: animationData.type.animationCacheAnimationType, keyframeOnly: pixelSize.width >= 120.0)) + strongSelf.disposable = renderer.add(target: strongSelf, cache: cache, itemId: animationData.resource.resource.id.stringRepresentation, unique: false, size: pixelSize, fetch: animationCacheFetchFile(context: context, resource: animationData.resource, type: animationData.type.animationCacheAnimationType, keyframeOnly: pixelSize.width >= 120.0, customColor: animationData.isTemplate ? .white : nil)) } if attemptSynchronousLoad { if !renderer.loadFirstFrameSynchronously(target: self, cache: cache, itemId: animationData.resource.resource.id.stringRepresentation, size: pixelSize) { self.updateDisplayPlaceholder(displayPlaceholder: true) - self.fetchDisposable = renderer.loadFirstFrame(target: self, cache: cache, itemId: animationData.resource.resource.id.stringRepresentation, size: pixelSize, fetch: animationCacheFetchFile(context: context, resource: animationData.resource, type: animationData.type.animationCacheAnimationType, keyframeOnly: true), completion: { [weak self] success, isFinal in + self.fetchDisposable = renderer.loadFirstFrame(target: self, cache: cache, itemId: animationData.resource.resource.id.stringRepresentation, size: pixelSize, fetch: animationCacheFetchFile(context: context, resource: animationData.resource, type: animationData.type.animationCacheAnimationType, keyframeOnly: true, customColor: animationData.isTemplate ? .white : nil), completion: { [weak self] success, isFinal in if !isFinal { if !success { Queue.mainQueue().async { @@ -2931,7 +2945,7 @@ public final class EmojiPagerContentComponent: Component { loadAnimation() } } else { - self.fetchDisposable = renderer.loadFirstFrame(target: self, cache: cache, itemId: animationData.resource.resource.id.stringRepresentation, size: pixelSize, fetch: animationCacheFetchFile(context: context, resource: animationData.resource, type: animationData.type.animationCacheAnimationType, keyframeOnly: true), completion: { [weak self] success, isFinal in + self.fetchDisposable = renderer.loadFirstFrame(target: self, cache: cache, itemId: animationData.resource.resource.id.stringRepresentation, size: pixelSize, fetch: animationCacheFetchFile(context: context, resource: animationData.resource, type: animationData.type.animationCacheAnimationType, keyframeOnly: true, customColor: animationData.isTemplate ? .white : nil), completion: { [weak self] success, isFinal in if !isFinal { if !success { Queue.mainQueue().async { @@ -5337,9 +5351,12 @@ public final class EmojiPagerContentComponent: Component { itemLayer.update(transition: transition, size: itemFrame.size, badge: badge, blurredBadgeColor: UIColor(white: 0.0, alpha: 0.1), blurredBadgeBackgroundColor: keyboardChildEnvironment.theme.list.plainBackgroundColor) - if item.accentTint { + switch item.tintMode { + case .accent: itemLayer.layerTintColor = keyboardChildEnvironment.theme.list.itemAccentColor.cgColor - } else { + case .primary: + itemLayer.layerTintColor = keyboardChildEnvironment.theme.list.itemPrimaryTextColor.cgColor + case .none: itemLayer.layerTintColor = nil } @@ -5373,7 +5390,7 @@ public final class EmojiPagerContentComponent: Component { self.visibleItemSelectionLayers[itemId] = itemSelectionLayer } - if item.accentTint { + if case .accent = item.tintMode { itemSelectionLayer.backgroundColor = keyboardChildEnvironment.theme.list.itemAccentColor.withMultipliedAlpha(0.1).cgColor itemSelectionLayer.tintContainerLayer.backgroundColor = UIColor.clear.cgColor } else { @@ -6392,7 +6409,7 @@ public final class EmojiPagerContentComponent: Component { itemFile: nil, subgroupId: nil, icon: .none, - accentTint: false + tintMode: .none ) let groupId = "recent" @@ -6411,13 +6428,16 @@ public final class EmojiPagerContentComponent: Component { } existingIds.insert(file.fileId) - var accentTint = false + var tintMode: Item.TintMode = .none for attribute in file.attributes { - if case let .CustomEmoji(_, _, packReference) = attribute { + if case let .CustomEmoji(_, isSingleColor, _, packReference) = attribute { + if isSingleColor { + tintMode = .accent + } switch packReference { case let .id(id, _): if id == 773947703670341676 || id == 2964141614563343 { - accentTint = true + tintMode = .accent } default: break @@ -6434,7 +6454,7 @@ public final class EmojiPagerContentComponent: Component { itemFile: file, subgroupId: nil, icon: .none, - accentTint: accentTint + tintMode: tintMode ) if let groupIndex = itemGroupIndexById[groupId] { @@ -6448,7 +6468,7 @@ public final class EmojiPagerContentComponent: Component { itemFile: nil, subgroupId: nil, icon: .none, - accentTint: false + tintMode: .none ) let groupId = "recent" @@ -6467,13 +6487,16 @@ public final class EmojiPagerContentComponent: Component { } existingIds.insert(file.fileId) - var accentTint = false + var tintMode: Item.TintMode = .none for attribute in file.attributes { - if case let .CustomEmoji(_, _, packReference) = attribute { + if case let .CustomEmoji(_, isSingleColor, _, packReference) = attribute { + if isSingleColor { + tintMode = .accent + } switch packReference { case let .id(id, _): if id == 773947703670341676 || id == 2964141614563343 { - accentTint = true + tintMode = .accent } default: break @@ -6490,7 +6513,7 @@ public final class EmojiPagerContentComponent: Component { itemFile: file, subgroupId: nil, icon: .none, - accentTint: accentTint + tintMode: tintMode ) if let groupIndex = itemGroupIndexById[groupId] { @@ -6510,13 +6533,16 @@ public final class EmojiPagerContentComponent: Component { } existingIds.insert(file.fileId) - var accentTint = false + var tintMode: Item.TintMode = .none for attribute in file.attributes { - if case let .CustomEmoji(_, _, packReference) = attribute { + if case let .CustomEmoji(_, isSingleColor, _, packReference) = attribute { + if isSingleColor { + tintMode = .accent + } switch packReference { case let .id(id, _): if id == 773947703670341676 || id == 2964141614563343 { - accentTint = true + tintMode = .accent } default: break @@ -6533,7 +6559,7 @@ public final class EmojiPagerContentComponent: Component { itemFile: file, subgroupId: nil, icon: .none, - accentTint: accentTint + tintMode: tintMode ) if let groupIndex = itemGroupIndexById[groupId] { @@ -6559,13 +6585,16 @@ public final class EmojiPagerContentComponent: Component { let resultItem: EmojiPagerContentComponent.Item - var accentTint = false + var tintMode: Item.TintMode = .none for attribute in file.attributes { - if case let .CustomEmoji(_, _, packReference) = attribute { + if case let .CustomEmoji(_, isSingleColor, _, packReference) = attribute { + if isSingleColor { + tintMode = .accent + } switch packReference { case let .id(id, _): if id == 773947703670341676 || id == 2964141614563343 { - accentTint = true + tintMode = .accent } default: break @@ -6580,7 +6609,7 @@ public final class EmojiPagerContentComponent: Component { itemFile: file, subgroupId: nil, icon: .none, - accentTint: accentTint + tintMode: tintMode ) if let groupIndex = itemGroupIndexById[groupId] { @@ -6637,6 +6666,15 @@ public final class EmojiPagerContentComponent: Component { icon = .none } + var tintMode: Item.TintMode = .none + for attribute in reactionItem.file.attributes { + if case let .CustomEmoji(_, isSingleColor, _, _) = attribute { + if isSingleColor { + tintMode = .primary + } + } + } + let animationFile = reactionItem.file let animationData = EntityKeyboardAnimationData(file: animationFile, isReaction: true) let resultItem = EmojiPagerContentComponent.Item( @@ -6645,7 +6683,7 @@ public final class EmojiPagerContentComponent: Component { itemFile: animationFile, subgroupId: nil, icon: icon, - accentTint: false + tintMode: tintMode ) let groupId = "recent" @@ -6692,6 +6730,15 @@ public final class EmojiPagerContentComponent: Component { icon = .none } + var tintMode: Item.TintMode = .none + for attribute in reactionItem.selectAnimation.attributes { + if case let .CustomEmoji(_, isSingleColor, _, _) = attribute { + if isSingleColor { + tintMode = .primary + } + } + } + let animationFile = reactionItem.selectAnimation let animationData = EntityKeyboardAnimationData(file: animationFile, isReaction: true) let resultItem = EmojiPagerContentComponent.Item( @@ -6700,7 +6747,7 @@ public final class EmojiPagerContentComponent: Component { itemFile: animationFile, subgroupId: nil, icon: icon, - accentTint: false + tintMode: tintMode ) if hasPremium { @@ -6768,6 +6815,15 @@ public final class EmojiPagerContentComponent: Component { } } + var tintMode: Item.TintMode = .none + for attribute in animationFile.attributes { + if case let .CustomEmoji(_, isSingleColor, _, _) = attribute { + if isSingleColor { + tintMode = .primary + } + } + } + let animationData = EntityKeyboardAnimationData(file: animationFile, isReaction: true) let resultItem = EmojiPagerContentComponent.Item( animationData: animationData, @@ -6775,7 +6831,7 @@ public final class EmojiPagerContentComponent: Component { itemFile: animationFile, subgroupId: nil, icon: icon, - accentTint: false + tintMode: tintMode ) let groupId = "popular" @@ -6811,6 +6867,15 @@ public final class EmojiPagerContentComponent: Component { let resultItem: EmojiPagerContentComponent.Item switch item.content { case let .file(file): + var tintMode: Item.TintMode = .none + for attribute in file.attributes { + if case let .CustomEmoji(_, isSingleColor, _, _) = attribute { + if isSingleColor { + tintMode = .primary + } + } + } + let animationData = EntityKeyboardAnimationData(file: file) resultItem = EmojiPagerContentComponent.Item( animationData: animationData, @@ -6818,7 +6883,7 @@ public final class EmojiPagerContentComponent: Component { itemFile: file, subgroupId: nil, icon: .none, - accentTint: false + tintMode: tintMode ) case let .text(text): resultItem = EmojiPagerContentComponent.Item( @@ -6827,7 +6892,7 @@ public final class EmojiPagerContentComponent: Component { itemFile: nil, subgroupId: nil, icon: .none, - accentTint: false + tintMode: .none ) } @@ -6857,6 +6922,19 @@ public final class EmojiPagerContentComponent: Component { icon = .locked } + var tintMode: Item.TintMode = .none + for attribute in item.file.attributes { + if case let .CustomEmoji(_, isSingleColor, _, _) = attribute { + if isSingleColor { + if isStatusSelection { + tintMode = .accent + } else { + tintMode = .primary + } + } + } + } + let animationData = EntityKeyboardAnimationData(file: item.file) let resultItem = EmojiPagerContentComponent.Item( animationData: animationData, @@ -6864,7 +6942,7 @@ public final class EmojiPagerContentComponent: Component { itemFile: item.file, subgroupId: nil, icon: icon, - accentTint: false + tintMode: tintMode ) let supergroupId = entry.index.collectionId @@ -6900,7 +6978,8 @@ public final class EmojiPagerContentComponent: Component { resource: .stickerPackThumbnail(stickerPack: .id(id: info.id.id, accessHash: info.accessHash), resource: thumbnail.resource), dimensions: thumbnail.dimensions.cgSize, immediateThumbnailData: info.immediateThumbnailData, - isReaction: false + isReaction: false, + isTemplate: false ) } @@ -6918,6 +6997,19 @@ public final class EmojiPagerContentComponent: Component { } for item in featuredEmojiPack.topItems { + var tintMode: Item.TintMode = .none + for attribute in item.file.attributes { + if case let .CustomEmoji(_, isSingleColor, _, _) = attribute { + if isSingleColor { + if isStatusSelection { + tintMode = .accent + } else { + tintMode = .primary + } + } + } + } + let animationData = EntityKeyboardAnimationData(file: item.file) let resultItem = EmojiPagerContentComponent.Item( animationData: animationData, @@ -6925,7 +7017,7 @@ public final class EmojiPagerContentComponent: Component { itemFile: item.file, subgroupId: nil, icon: .none, - accentTint: false + tintMode: tintMode ) let supergroupId = featuredEmojiPack.info.id @@ -6959,7 +7051,8 @@ public final class EmojiPagerContentComponent: Component { resource: .stickerPackThumbnail(stickerPack: .id(id: info.id.id, accessHash: info.accessHash), resource: thumbnail.resource), dimensions: thumbnail.dimensions.cgSize, immediateThumbnailData: info.immediateThumbnailData, - isReaction: false + isReaction: false, + isTemplate: false ) } @@ -6980,7 +7073,7 @@ public final class EmojiPagerContentComponent: Component { itemFile: nil, subgroupId: subgroupId.rawValue, icon: .none, - accentTint: false + tintMode: .none ) if let groupIndex = itemGroupIndexById[groupId] { @@ -7190,19 +7283,29 @@ public final class EmojiPagerContentComponent: Component { resource: .stickerPackThumbnail(stickerPack: .id(id: featuredStickerPack.info.id.id, accessHash: featuredStickerPack.info.accessHash), resource: thumbnail.resource), dimensions: thumbnail.dimensions.cgSize, immediateThumbnailData: featuredStickerPack.info.immediateThumbnailData, - isReaction: false + isReaction: false, + isTemplate: false ) } else { animationData = EntityKeyboardAnimationData(file: item.file) } + var tintMode: Item.TintMode = .none + for attribute in item.file.attributes { + if case let .CustomEmoji(_, isSingleColor, _, _) = attribute { + if isSingleColor { + tintMode = .primary + } + } + } + let resultItem = EmojiPagerContentComponent.Item( animationData: animationData, content: .animation(animationData), itemFile: item.file, subgroupId: nil, icon: .none, - accentTint: false + tintMode: tintMode ) let supergroupId = "featuredTop" @@ -7233,6 +7336,15 @@ public final class EmojiPagerContentComponent: Component { continue } + var tintMode: Item.TintMode = .none + for attribute in item.file.attributes { + if case let .CustomEmoji(_, isSingleColor, _, _) = attribute { + if isSingleColor { + tintMode = .primary + } + } + } + let animationData = EntityKeyboardAnimationData(file: item.file) let resultItem = EmojiPagerContentComponent.Item( animationData: animationData, @@ -7240,7 +7352,7 @@ public final class EmojiPagerContentComponent: Component { itemFile: item.file, subgroupId: nil, icon: .none, - accentTint: false + tintMode: tintMode ) let groupId = "saved" @@ -7262,6 +7374,15 @@ public final class EmojiPagerContentComponent: Component { continue } + var tintMode: Item.TintMode = .none + for attribute in item.media.attributes { + if case let .CustomEmoji(_, isSingleColor, _, _) = attribute { + if isSingleColor { + tintMode = .primary + } + } + } + let animationData = EntityKeyboardAnimationData(file: item.media) let resultItem = EmojiPagerContentComponent.Item( animationData: animationData, @@ -7269,7 +7390,7 @@ public final class EmojiPagerContentComponent: Component { itemFile: item.media, subgroupId: nil, icon: .none, - accentTint: false + tintMode: tintMode ) let groupId = "recent" @@ -7314,6 +7435,15 @@ public final class EmojiPagerContentComponent: Component { } processedIds.insert(item.file.fileId) + var tintMode: Item.TintMode = .none + for attribute in item.file.attributes { + if case let .CustomEmoji(_, isSingleColor, _, _) = attribute { + if isSingleColor { + tintMode = .primary + } + } + } + let animationData = EntityKeyboardAnimationData(file: item.file) let resultItem = EmojiPagerContentComponent.Item( animationData: animationData, @@ -7321,7 +7451,7 @@ public final class EmojiPagerContentComponent: Component { itemFile: item.file, subgroupId: nil, icon: .none, - accentTint: false + tintMode: tintMode ) let groupId = "premium" @@ -7348,6 +7478,15 @@ public final class EmojiPagerContentComponent: Component { } processedIds.insert(item.file.fileId) + var tintMode: Item.TintMode = .none + for attribute in item.file.attributes { + if case let .CustomEmoji(_, isSingleColor, _, _) = attribute { + if isSingleColor { + tintMode = .primary + } + } + } + let animationData = EntityKeyboardAnimationData(file: item.file) let resultItem = EmojiPagerContentComponent.Item( animationData: animationData, @@ -7355,7 +7494,7 @@ public final class EmojiPagerContentComponent: Component { itemFile: item.file, subgroupId: nil, icon: .none, - accentTint: false + tintMode: tintMode ) let groupId = "peerSpecific" @@ -7372,6 +7511,16 @@ public final class EmojiPagerContentComponent: Component { guard let item = entry.item as? StickerPackItem else { continue } + + var tintMode: Item.TintMode = .none + for attribute in item.file.attributes { + if case let .CustomEmoji(_, isSingleColor, _, _) = attribute { + if isSingleColor { + tintMode = .primary + } + } + } + let animationData = EntityKeyboardAnimationData(file: item.file) let resultItem = EmojiPagerContentComponent.Item( animationData: animationData, @@ -7379,7 +7528,7 @@ public final class EmojiPagerContentComponent: Component { itemFile: item.file, subgroupId: nil, icon: .none, - accentTint: false + tintMode: tintMode ) let groupId = entry.index.collectionId if let groupIndex = itemGroupIndexById[groupId] { @@ -7409,7 +7558,8 @@ public final class EmojiPagerContentComponent: Component { resource: .stickerPackThumbnail(stickerPack: .id(id: info.id.id, accessHash: info.accessHash), resource: thumbnail.resource), dimensions: thumbnail.dimensions.cgSize, immediateThumbnailData: info.immediateThumbnailData, - isReaction: false + isReaction: false, + isTemplate: false ) } @@ -7426,6 +7576,15 @@ public final class EmojiPagerContentComponent: Component { } for item in featuredStickerPack.topItems { + var tintMode: Item.TintMode = .none + for attribute in item.file.attributes { + if case let .CustomEmoji(_, isSingleColor, _, _) = attribute { + if isSingleColor { + tintMode = .primary + } + } + } + let animationData = EntityKeyboardAnimationData(file: item.file) let resultItem = EmojiPagerContentComponent.Item( animationData: animationData, @@ -7433,7 +7592,7 @@ public final class EmojiPagerContentComponent: Component { itemFile: item.file, subgroupId: nil, icon: .none, - accentTint: false + tintMode: tintMode ) let supergroupId = featuredStickerPack.info.id @@ -7469,7 +7628,8 @@ public final class EmojiPagerContentComponent: Component { resource: .stickerPackThumbnail(stickerPack: .id(id: info.id.id, accessHash: info.accessHash), resource: thumbnail.resource), dimensions: thumbnail.dimensions.cgSize, immediateThumbnailData: info.immediateThumbnailData, - isReaction: false + isReaction: false, + isTemplate: false ) } diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardTopPanelComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardTopPanelComponent.swift index 2a96109768..4f5cdf4ca0 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardTopPanelComponent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardTopPanelComponent.swift @@ -116,7 +116,7 @@ final class EntityKeyboardAnimationTopPanelComponent: Component { itemFile: nil, subgroupId: nil, icon: .none, - accentTint: false + tintMode: component.item.isTemplate ? .primary : .none ), context: component.context, attemptSynchronousLoad: false, @@ -158,6 +158,15 @@ final class EntityKeyboardAnimationTopPanelComponent: Component { } itemLayer.update(transition: transition, size: iconFrame.size, badge: badge, blurredBadgeColor: UIColor(white: 0.0, alpha: 0.1), blurredBadgeBackgroundColor: component.theme.list.plainBackgroundColor) + switch itemLayer.item.tintMode { + case .none: + break + case .primary: + itemLayer.layerTintColor = component.theme.list.itemPrimaryTextColor.cgColor + case .accent: + itemLayer.layerTintColor = component.theme.list.itemAccentColor.cgColor + } + itemLayer.isVisibleForAnimations = true } diff --git a/submodules/TelegramUI/Components/LottieAnimationCache/Sources/LottieAnimationCache.swift b/submodules/TelegramUI/Components/LottieAnimationCache/Sources/LottieAnimationCache.swift index 96b56e41c0..21265ad2fa 100644 --- a/submodules/TelegramUI/Components/LottieAnimationCache/Sources/LottieAnimationCache.swift +++ b/submodules/TelegramUI/Components/LottieAnimationCache/Sources/LottieAnimationCache.swift @@ -6,7 +6,7 @@ import RLottieBinding import GZip import WebPBinding -public func cacheLottieAnimation(data: Data, width: Int, height: Int, keyframeOnly: Bool, writer: AnimationCacheItemWriter, firstFrameOnly: Bool) { +public func cacheLottieAnimation(data: Data, width: Int, height: Int, keyframeOnly: Bool, writer: AnimationCacheItemWriter, firstFrameOnly: Bool, customColor: UIColor?) { let work: () -> Void = { let decompressedData = TGGUnzipData(data, 2 * 1024 * 1024) ?? data guard let animation = LottieInstance(data: decompressedData, fitzModifier: .none, colorReplacements: nil, cacheKey: "") else { @@ -32,6 +32,19 @@ public func cacheLottieAnimation(data: Data, width: Int, height: Int, keyframeOn } writer.add(with: { surface in animation.renderFrame(with: i, into: surface.argb, width: Int32(surface.width), height: Int32(surface.height), bytesPerRow: Int32(surface.bytesPerRow)) + if customColor != nil { + for y in 0 ..< surface.height { + for x in 0 ..< surface.width { + let pixel = surface.argb.advanced(by: y * surface.bytesPerRow + x * 4) + let a = pixel.advanced(by: 3).pointee + + pixel.advanced(by: 0).pointee = a + pixel.advanced(by: 1).pointee = a + pixel.advanced(by: 2).pointee = a + pixel.advanced(by: 3).pointee = a + } + } + } return frameDuration }, proposedWidth: width, proposedHeight: height, insertKeyframe: i == 0 || keyframeOnly) @@ -46,7 +59,7 @@ public func cacheLottieAnimation(data: Data, width: Int, height: Int, keyframeOn writer.queue.async(work) } -public func cacheStillSticker(path: String, width: Int, height: Int, writer: AnimationCacheItemWriter) { +public func cacheStillSticker(path: String, width: Int, height: Int, writer: AnimationCacheItemWriter, customColor: UIColor?) { let work: () -> Void = { if let data = try? Data(contentsOf: URL(fileURLWithPath: path)), let image = WebP.convert(fromWebP: data) { writer.add(with: { surface in @@ -55,6 +68,12 @@ public func cacheStillSticker(path: String, width: Int, height: Int, writer: Ani } context.withFlippedContext { c in UIGraphicsPushContext(c) + + if let customColor = customColor { + c.setFillColor(customColor.cgColor) + c.setBlendMode(.sourceIn) + } + c.draw(image.cgImage!, in: CGRect(origin: CGPoint(), size: context.size)) UIGraphicsPopContext() } diff --git a/submodules/TelegramUI/Components/TextNodeWithEntities/Sources/TextNodeWithEntities.swift b/submodules/TelegramUI/Components/TextNodeWithEntities/Sources/TextNodeWithEntities.swift index 178e56efa0..0a6b61facc 100644 --- a/submodules/TelegramUI/Components/TextNodeWithEntities/Sources/TextNodeWithEntities.swift +++ b/submodules/TelegramUI/Components/TextNodeWithEntities/Sources/TextNodeWithEntities.swift @@ -243,9 +243,10 @@ public final class TextNodeWithEntities { let itemLayer: InlineStickerItemLayer if let current = self.inlineStickerItemLayers[id] { itemLayer = current + itemLayer.dynamicColor = item.textColor } else { let pointSize = floor(itemSize * 1.3) - itemLayer = InlineStickerItemLayer(context: context, attemptSynchronousLoad: attemptSynchronousLoad, emoji: stickerItem.emoji, file: stickerItem.file, cache: cache, renderer: renderer, placeholderColor: placeholderColor, pointSize: CGSize(width: pointSize, height: pointSize)) + itemLayer = InlineStickerItemLayer(context: context, attemptSynchronousLoad: attemptSynchronousLoad, emoji: stickerItem.emoji, file: stickerItem.file, cache: cache, renderer: renderer, placeholderColor: placeholderColor, pointSize: CGSize(width: pointSize, height: pointSize), dynamicColor: item.textColor) self.inlineStickerItemLayers[id] = itemLayer self.textNode.layer.addSublayer(itemLayer) @@ -407,9 +408,10 @@ public class ImmediateTextNodeWithEntities: TextNode { let itemLayer: InlineStickerItemLayer if let current = self.inlineStickerItemLayers[id] { itemLayer = current + itemLayer.dynamicColor = item.textColor } else { let pointSize = floor(itemSize * 1.3) - itemLayer = InlineStickerItemLayer(context: context, attemptSynchronousLoad: false, emoji: stickerItem.emoji, file: stickerItem.file, cache: cache, renderer: renderer, placeholderColor: placeholderColor, pointSize: CGSize(width: pointSize, height: pointSize)) + itemLayer = InlineStickerItemLayer(context: context, attemptSynchronousLoad: false, emoji: stickerItem.emoji, file: stickerItem.file, cache: cache, renderer: renderer, placeholderColor: placeholderColor, pointSize: CGSize(width: pointSize, height: pointSize), dynamicColor: item.textColor) self.inlineStickerItemLayers[id] = itemLayer self.layer.addSublayer(itemLayer) diff --git a/submodules/TelegramUI/Components/VideoAnimationCache/Sources/VideoAnimationCache.swift b/submodules/TelegramUI/Components/VideoAnimationCache/Sources/VideoAnimationCache.swift index d4cbd23f25..77a642802d 100644 --- a/submodules/TelegramUI/Components/VideoAnimationCache/Sources/VideoAnimationCache.swift +++ b/submodules/TelegramUI/Components/VideoAnimationCache/Sources/VideoAnimationCache.swift @@ -18,7 +18,7 @@ private func roundUp(_ numToRound: Int, multiple: Int) -> Int { return numToRound + multiple - remainder } -public func cacheVideoAnimation(path: String, width: Int, height: Int, writer: AnimationCacheItemWriter, firstFrameOnly: Bool) { +public func cacheVideoAnimation(path: String, width: Int, height: Int, writer: AnimationCacheItemWriter, firstFrameOnly: Bool, customColor: UIColor?) { let work: () -> Void = { guard let frameSource = makeVideoStickerDirectFrameSource(queue: writer.queue, path: path, width: roundUp(width, multiple: 16), height: roundUp(height, multiple: 16), cachePathPrefix: nil, unpremultiplyAlpha: false) else { return diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index e2d8d5f482..156613b17a 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -1195,7 +1195,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G var existingIds = Set() for (_, file) in files { loop: for attribute in file.attributes { - if case let .CustomEmoji(_, _, packReference) = attribute, let packReference = packReference { + if case let .CustomEmoji(_, _, _, packReference) = attribute, let packReference = packReference { if case let .id(id, _) = packReference, !existingIds.contains(id) { packReferences.append(packReference) existingIds.insert(id) @@ -1469,7 +1469,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G var existingIds = Set() for (_, file) in customEmoji { loop: for attribute in file.attributes { - if case let .CustomEmoji(_, _, packReference) = attribute, let packReference = packReference { + if case let .CustomEmoji(_, _, _, packReference) = attribute, let packReference = packReference { if case let .id(id, _) = packReference, !existingIds.contains(id) { packReferences.append(packReference) existingIds.insert(id) @@ -13694,7 +13694,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G var stickerPackReference: StickerPackReference? for attribute in file.attributes { - if case let .CustomEmoji(_, _, packReference) = attribute { + if case let .CustomEmoji(_, _, _, packReference) = attribute { stickerPackReference = packReference break } diff --git a/submodules/TelegramUI/Sources/ChatEntityKeyboardInputNode.swift b/submodules/TelegramUI/Sources/ChatEntityKeyboardInputNode.swift index 62de96da12..efeee55908 100644 --- a/submodules/TelegramUI/Sources/ChatEntityKeyboardInputNode.swift +++ b/submodules/TelegramUI/Sources/ChatEntityKeyboardInputNode.swift @@ -526,7 +526,7 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { var emojiAttribute: ChatTextInputTextCustomEmojiAttribute? loop: for attribute in file.attributes { switch attribute { - case let .CustomEmoji(_, displayText, _): + case let .CustomEmoji(_, _, displayText, _): text = displayText var packId: ItemCollectionId? @@ -781,7 +781,7 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { } for attribute in item.file.attributes { switch attribute { - case let .CustomEmoji(_, alt, _): + case let .CustomEmoji(_, _, alt, _): if !item.file.isPremiumEmoji || hasPremium { if !alt.isEmpty, let keyword = allEmoticons[alt] { result.append((alt, item.file, keyword)) @@ -811,7 +811,7 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { itemFile: itemFile, subgroupId: nil, icon: .none, - accentTint: false + tintMode: animationData.isTemplate ? .primary : .none ) items.append(item) } @@ -824,8 +824,8 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { itemFile: nil, subgroupId: nil, icon: .none, - accentTint: false) - ) + tintMode: .none + )) } return [EmojiPagerContentComponent.ItemGroup( @@ -1731,7 +1731,7 @@ final class EntityInputView: UIView, AttachmentTextInputPanelInputView, UIInputV var emojiAttribute: ChatTextInputTextCustomEmojiAttribute? loop: for attribute in file.attributes { switch attribute { - case let .CustomEmoji(_, displayText, _): + case let .CustomEmoji(_, _, displayText, _): text = displayText var packId: ItemCollectionId? if let id = groupId.base as? ItemCollectionId { @@ -2072,7 +2072,7 @@ private final class EmojiContentPeekBehaviorImpl: EmojiContentPeekBehavior { loop: for attribute in file.attributes { switch attribute { - case let .CustomEmoji(_, _, packReference), let .Sticker(_, packReference, _): + case let .CustomEmoji(_, _, _, packReference), let .Sticker(_, packReference, _): if let packReference = packReference { let controller = strongSelf.context.sharedContext.makeStickerPackScreen(context: context, updatedPresentationData: nil, mainStickerPack: packReference, stickerPacks: [packReference], loadedStickerPacks: [], parentNavigationController: strongSelf.controllerInteraction.navigationController(), sendSticker: { file, sourceView, sourceRect in sendSticker(file, false, false, nil, false, sourceView, sourceRect, nil) diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift index 496a1a4957..7cf50fa436 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift @@ -1165,7 +1165,7 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState } } - if resourceAvailable, !message.containsSecretMedia { + if resourceAvailable, !message.containsSecretMedia, !chatPresentationInterfaceState.copyProtectionEnabled, !message.isCopyProtected() { var mediaReference: AnyMediaReference? var isVideo = false for media in message.media { @@ -2413,7 +2413,7 @@ private final class ChatReadReportContextItemNode: ASDisplayNode, ContextMenuCus var firstCustomEmojiReaction: TelegramMediaFile? for (_, file) in customEmoji { loop: for attribute in file.attributes { - if case let .CustomEmoji(_, _, packReference) = attribute, let packReference = packReference { + if case let .CustomEmoji(_, _, _, packReference) = attribute, let packReference = packReference { if case let .id(id, _) = packReference, !existingIds.contains(id) { if firstCustomEmojiReaction == nil { firstCustomEmojiReaction = file diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateContextQueries.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateContextQueries.swift index dbde4459fc..5b82b8325e 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateContextQueries.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateContextQueries.swift @@ -351,7 +351,7 @@ private func updatedContextQueryResultStateForQuery(context: AccountContext, pee } for attribute in item.file.attributes { switch attribute { - case let .CustomEmoji(_, alt, _): + case let .CustomEmoji(_, _, alt, _): if alt == query { if !item.file.isPremiumEmoji || hasPremium { result.append((alt, item.file, alt)) @@ -415,7 +415,7 @@ private func updatedContextQueryResultStateForQuery(context: AccountContext, pee } for attribute in item.file.attributes { switch attribute { - case let .CustomEmoji(_, alt, _): + case let .CustomEmoji(_, _, alt, _): if !alt.isEmpty, let keyword = allEmoticons[alt] { if !item.file.isPremiumEmoji || hasPremium { result.append((alt, item.file, keyword)) diff --git a/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift index 99837e02d7..3350e302f1 100644 --- a/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift @@ -1069,7 +1069,8 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { let maximumContentWidth = floor(tmpWidth - layoutConstants.bubble.edgeInset - layoutConstants.bubble.edgeInset - layoutConstants.bubble.contentInsets.left - layoutConstants.bubble.contentInsets.right - avatarInset) let font = Font.regular(fontSizeForEmojiString(item.message.text)) - let attributedText = stringWithAppliedEntities(item.message.text, entities: item.message.textEntitiesAttribute?.entities ?? [], baseColor: .black, linkColor: .black, baseFont: font, linkFont: font, boldFont: font, italicFont: font, boldItalicFont: font, fixedFont: font, blockQuoteFont: font, message: item.message) + let textColor = item.presentationData.theme.theme.list.itemPrimaryTextColor + let attributedText = stringWithAppliedEntities(item.message.text, entities: item.message.textEntitiesAttribute?.entities ?? [], baseColor: textColor, linkColor: textColor, baseFont: font, linkFont: font, boldFont: font, italicFont: font, boldItalicFont: font, fixedFont: font, blockQuoteFont: font, message: item.message) textLayoutAndApply = textLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: maximumContentWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural)) imageSize = CGSize(width: textLayoutAndApply!.0.size.width, height: textLayoutAndApply!.0.size.height) diff --git a/submodules/TelegramUI/Sources/ChatMessageTextBubbleContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageTextBubbleContentNode.swift index d305d027ae..159951cb98 100644 --- a/submodules/TelegramUI/Sources/ChatMessageTextBubbleContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageTextBubbleContentNode.swift @@ -334,7 +334,7 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { let currentDict = updatedString.attributes(at: range.lowerBound, effectiveRange: nil) var updatedAttributes: [NSAttributedString.Key: Any] = currentDict - updatedAttributes[NSAttributedString.Key.foregroundColor] = UIColor.clear.cgColor + //updatedAttributes[NSAttributedString.Key.foregroundColor] = UIColor.clear.cgColor updatedAttributes[ChatTextInputAttributes.customEmoji] = ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: fileId, file: item.message.associatedMedia[MediaId(namespace: Namespaces.Media.CloudFile, id: fileId)] as? TelegramMediaFile) let insertString = NSAttributedString(string: updatedString.attributedSubstring(from: range).string, attributes: updatedAttributes) diff --git a/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift b/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift index 755b518892..874a7f1293 100644 --- a/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift @@ -473,7 +473,7 @@ final class CustomEmojiContainerView: UIView { preconditionFailure() } - func update(fontSize: CGFloat, emojiRects: [(CGRect, ChatTextInputTextCustomEmojiAttribute)]) { + func update(fontSize: CGFloat, textColor: UIColor, emojiRects: [(CGRect, ChatTextInputTextCustomEmojiAttribute)]) { var nextIndexById: [Int64: Int] = [:] var validKeys = Set() @@ -499,6 +499,10 @@ final class CustomEmojiContainerView: UIView { continue } + if let view = view as? EmojiTextAttachmentView { + view.updateTextColor(textColor) + } + let itemSize: CGFloat = floor(24.0 * fontSize / 17.0) let size = CGSize(width: itemSize, height: itemSize) @@ -2352,7 +2356,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { self.customEmojiContainerView = customEmojiContainerView } - customEmojiContainerView.update(fontSize: fontSize, emojiRects: customEmojiRects) + customEmojiContainerView.update(fontSize: fontSize, textColor: textColor, emojiRects: customEmojiRects) } else if let customEmojiContainerView = self.customEmojiContainerView { customEmojiContainerView.removeFromSuperview() self.customEmojiContainerView = nil @@ -2599,7 +2603,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { var emojiAttribute: ChatTextInputTextCustomEmojiAttribute? loop: for attribute in file.attributes { switch attribute { - case let .CustomEmoji(_, displayText, _): + case let .CustomEmoji(_, _, displayText, _): text = displayText emojiAttribute = ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: file.fileId.id, file: file) break loop diff --git a/submodules/TelegramUI/Sources/DocumentPreviewController.swift b/submodules/TelegramUI/Sources/DocumentPreviewController.swift index fc2adef437..21944e57b8 100644 --- a/submodules/TelegramUI/Sources/DocumentPreviewController.swift +++ b/submodules/TelegramUI/Sources/DocumentPreviewController.swift @@ -200,22 +200,32 @@ final class CompactDocumentPreviewController: QLPreviewController, QLPreviewCont } private func tick() { - self.navigationItem.rightBarButtonItems = [UIBarButtonItem()] - self.navigationItem.setRightBarButton(UIBarButtonItem(), animated: false) - - self.navigationController?.toolbar.isHidden = true - let (navigationBars, toolbars) = navigationAndToolbarsInSubviews(forView: self.view) self.navigationBars = navigationBars self.toolbars = toolbars - - for navigationBar in self.navigationBars { - navigationBar.topItem?.rightBarButtonItem = UIBarButtonItem() - navigationBar.topItem?.rightBarButtonItems = [UIBarButtonItem()] - } - for toolbar in self.toolbars { - toolbar.isHidden = true + if #available(iOS 16.0, *) { + if let navigationController = self.children.first as? UINavigationController, let topController = navigationController.topViewController { + topController.navigationItem.titleMenuProvider = nil + } + + for toolbar in self.toolbars { + toolbar.isHidden = true + } + } else { + self.navigationItem.rightBarButtonItems = [UIBarButtonItem()] + self.navigationItem.setRightBarButton(UIBarButtonItem(), animated: false) + + self.navigationController?.toolbar.isHidden = true + + for navigationBar in self.navigationBars { + navigationBar.topItem?.rightBarButtonItem = UIBarButtonItem() + navigationBar.topItem?.rightBarButtonItems = [UIBarButtonItem()] + } + + for toolbar in self.toolbars { + toolbar.isHidden = true + } } } diff --git a/submodules/TelegramUI/Sources/EmojisChatInputContextPanelNode.swift b/submodules/TelegramUI/Sources/EmojisChatInputContextPanelNode.swift index 782187ba89..d8039260c7 100644 --- a/submodules/TelegramUI/Sources/EmojisChatInputContextPanelNode.swift +++ b/submodules/TelegramUI/Sources/EmojisChatInputContextPanelNode.swift @@ -206,7 +206,7 @@ final class EmojisChatInputContextPanelNode: ChatInputContextPanelNode { if let file = file { loop: for attribute in file.attributes { switch attribute { - case let .CustomEmoji(_, displayText, _): + case let .CustomEmoji(_, _, displayText, _): text = displayText emojiAttribute = ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: file.fileId.id, file: file) break loop diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoHeaderNode.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoHeaderNode.swift index 376b650543..3ddb660643 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoHeaderNode.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoHeaderNode.swift @@ -2505,7 +2505,7 @@ final class PeerInfoHeaderNode: ASDisplayNode { strongSelf.emojiStatusFileAndPackTitle.set(.never()) for attribute in emojiFile.attributes { - if case let .CustomEmoji(_, _, packReference) = attribute, let packReference = packReference { + if case let .CustomEmoji(_, _, _, packReference) = attribute, let packReference = packReference { strongSelf.emojiStatusPackDisposable.set((strongSelf.context.engine.stickers.loadedStickerPack(reference: packReference, forceActualized: false) |> filter { result in if case .result = result { diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift index 52512980db..56e68c189d 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift @@ -3514,7 +3514,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate if let file = files.first?.value { var stickerPackReference: StickerPackReference? for attribute in file.attributes { - if case let .CustomEmoji(_, _, packReference) = attribute { + if case let .CustomEmoji(_, _, _, packReference) = attribute { stickerPackReference = packReference break } diff --git a/submodules/TelegramVoip/Sources/GroupCallContext.swift b/submodules/TelegramVoip/Sources/GroupCallContext.swift index f71bd2aec1..9c61f22b10 100644 --- a/submodules/TelegramVoip/Sources/GroupCallContext.swift +++ b/submodules/TelegramVoip/Sources/GroupCallContext.swift @@ -415,9 +415,7 @@ public final class OngoingGroupCallContext { private final class Impl { let queue: Queue let context: GroupCallThreadLocalContext -#if os(iOS) let audioDevice: SharedCallAudioDevice? -#endif let sessionId = UInt32.random(in: 0 ..< UInt32(Int32.max)) let joinPayload = Promise<(String, UInt32)>() @@ -435,14 +433,8 @@ public final class OngoingGroupCallContext { init(queue: Queue, inputDeviceId: String, outputDeviceId: String, audioSessionActive: Signal, video: OngoingCallVideoCapturer?, requestMediaChannelDescriptions: @escaping (Set, @escaping ([MediaChannelDescription]) -> Void) -> Disposable, rejoinNeeded: @escaping () -> Void, outgoingAudioBitrateKbit: Int32?, videoContentType: VideoContentType, enableNoiseSuppression: Bool, disableAudioInput: Bool, preferX264: Bool, logPath: String) { self.queue = queue - #if os(iOS) self.audioDevice = nil - #endif - /*#if DEBUG - self.audioDevice = SharedCallAudioDevice(disableRecording: disableAudioInput) - #else - self.audioDevice = nil - #endif*/ + let audioDevice = self.audioDevice var networkStateUpdatedImpl: ((GroupCallNetworkState) -> Void)? var audioLevelsUpdatedImpl: (([NSNumber]) -> Void)? @@ -549,7 +541,8 @@ public final class OngoingGroupCallContext { enableNoiseSuppression: enableNoiseSuppression, disableAudioInput: disableAudioInput, preferX264: preferX264, - logPath: logPath + logPath: logPath, + audioDevice: audioDevice ) let queue = self.queue @@ -602,8 +595,8 @@ public final class OngoingGroupCallContext { guard let `self` = self else { return } - #if os(iOS) self.audioDevice?.setManualAudioSessionIsActive(isActive) + #if os(iOS) self.context.setManualAudioSessionIsActive(isActive) #endif }))