diff --git a/submodules/TelegramUI/Sources/AccountContext.swift b/submodules/TelegramUI/Sources/AccountContext.swift index 24be73bed9..fb828fb0a3 100644 --- a/submodules/TelegramUI/Sources/AccountContext.swift +++ b/submodules/TelegramUI/Sources/AccountContext.swift @@ -175,6 +175,72 @@ public final class AccountContextImpl: AccountContext { private var animatedEmojiStickersDisposable: Disposable? public private(set) var animatedEmojiStickers: [String: [StickerPackItem]] = [:] + private let animatedEmojiStickersValue = Promise<[String: [StickerPackItem]]>() + public var animatedEmojiStickersSignal: Signal<[String: [StickerPackItem]], NoError> { + return self.animatedEmojiStickersValue.get() + } + + private var additionalAnimatedEmojiStickersValue: Promise<[String: [Int: StickerPackItem]]>? + public var additionalAnimatedEmojiStickers: Signal<[String: [Int: StickerPackItem]], NoError> { + let additionalAnimatedEmojiStickersValue: Promise<[String: [Int: StickerPackItem]]> + if let current = self.additionalAnimatedEmojiStickersValue { + additionalAnimatedEmojiStickersValue = current + } else { + additionalAnimatedEmojiStickersValue = Promise<[String: [Int: StickerPackItem]]>() + self.additionalAnimatedEmojiStickersValue = additionalAnimatedEmojiStickersValue + additionalAnimatedEmojiStickersValue.set(self.engine.stickers.loadedStickerPack(reference: .animatedEmojiAnimations, forceActualized: false) + |> map { animatedEmoji -> [String: [Int: StickerPackItem]] in + let sequence = "0️⃣1️⃣2️⃣3️⃣4️⃣5️⃣6️⃣7️⃣8️⃣9️⃣".strippedEmoji + var animatedEmojiStickers: [String: [Int: StickerPackItem]] = [:] + switch animatedEmoji { + case let .result(_, items, _): + for item in items { + let indexKeys = item.getStringRepresentationsOfIndexKeys() + if indexKeys.count > 1, let first = indexKeys.first, let last = indexKeys.last { + let emoji: String? + let indexEmoji: String? + if sequence.contains(first.strippedEmoji) { + emoji = last + indexEmoji = first + } else if sequence.contains(last.strippedEmoji) { + emoji = first + indexEmoji = last + } else { + emoji = nil + indexEmoji = nil + } + + if let emoji = emoji?.strippedEmoji, let indexEmoji = indexEmoji?.strippedEmoji.first, let strIndex = sequence.firstIndex(of: indexEmoji) { + let index = sequence.distance(from: sequence.startIndex, to: strIndex) + if animatedEmojiStickers[emoji] != nil { + animatedEmojiStickers[emoji]![index] = item + } else { + animatedEmojiStickers[emoji] = [index: item] + } + } + } + } + default: + break + } + return animatedEmojiStickers + }) + } + return additionalAnimatedEmojiStickersValue.get() + } + + private var availableReactionsValue: Promise? + public var availableReactions: Signal { + let availableReactionsValue: Promise + if let current = self.availableReactionsValue { + availableReactionsValue = current + } else { + availableReactionsValue = Promise() + self.availableReactionsValue = availableReactionsValue + availableReactionsValue.set(self.engine.stickers.availableReactions()) + } + return availableReactionsValue.get() + } private var userLimitsConfigurationDisposable: Disposable? public private(set) var userLimits: EngineConfiguration.UserLimits @@ -311,6 +377,7 @@ public final class AccountContextImpl: AccountContext { return } strongSelf.animatedEmojiStickers = stickers + strongSelf.animatedEmojiStickersValue.set(.single(stickers)) }) self.userLimitsConfigurationDisposable = (self.account.postbox.peerView(id: self.account.peerId) diff --git a/submodules/TelegramUI/Sources/AppDelegate.swift b/submodules/TelegramUI/Sources/AppDelegate.swift index 9ae5443d09..299b5803de 100644 --- a/submodules/TelegramUI/Sources/AppDelegate.swift +++ b/submodules/TelegramUI/Sources/AppDelegate.swift @@ -36,6 +36,7 @@ import UIKitRuntimeUtils import StoreKit import PhoneNumberFormat import AuthorizationUI +import ManagedFile #if canImport(AppCenter) import AppCenter @@ -210,7 +211,7 @@ private func extractAccountManagerState(records: AccountRecordsView([:]) + private var urlSessions: [URLSession] = [] + private func urlSession(identifier: String) -> URLSession { + if let existingSession = self.urlSessions.first(where: { $0.configuration.identifier == identifier }) { + return existingSession + } + + let baseAppBundleId = Bundle.main.bundleIdentifier! + let appGroupName = "group.\(baseAppBundleId)" + + let configuration = URLSessionConfiguration.background(withIdentifier: identifier) + configuration.sharedContainerIdentifier = appGroupName + configuration.isDiscretionary = false + let session = URLSession(configuration: configuration, delegate: self, delegateQueue: .main) + self.urlSessions.append(session) + return session + } + + private var pendingUrlSessionBackgroundEventsCompletion: (() -> Void)? + private var notificationTokenPromise: Promise { if let current = self._notificationTokenPromise { return current @@ -834,7 +854,7 @@ private func extractAccountManagerState(records: AccountRecordsView Void>] = [:] + + func uploadInBackround(postbox: Postbox, resource: MediaResource) -> Signal { + let baseAppBundleId = Bundle.main.bundleIdentifier! + let session = self.urlSession(identifier: "\(baseAppBundleId).backroundSession") + + let signal = Signal { subscriber in + let disposable = MetaDisposable() + + let _ = session.getAllTasks(completionHandler: { tasks in + var alreadyExists = false + for task in tasks { + if let originalRequest = task.originalRequest { + if let requestResourceId = originalRequest.value(forHTTPHeaderField: "tresource") { + if resource.id.stringRepresentation == requestResourceId { + alreadyExists = true + break + } + } + } + } + + if !alreadyExists, self.backgroundSessionSourceDataDisposables[resource.id.stringRepresentation] == nil { + self.backgroundSessionSourceDataDisposables[resource.id.stringRepresentation] = (Signal { subscriber in + let dataDisposable = (postbox.mediaBox.resourceData(resource) + |> deliverOnMainQueue).start(next: { data in + if data.complete { + self.addBackgroundUploadTask(id: resource.id.stringRepresentation, path: data.path) + } + }) + let fetchDisposable = postbox.mediaBox.fetchedResource(resource, parameters: nil).start() + + return ActionDisposable { + dataDisposable.dispose() + fetchDisposable.dispose() + } + }).start() + } + }) + + return disposable + } + |> runOn(.mainQueue()) + + return Signal { subscriber in + let bag: Bag<(String?) -> Void> + if let current = self.backgroundUploadResultSubscribers[resource.id.stringRepresentation] { + bag = current + } else { + bag = Bag() + self.backgroundUploadResultSubscribers[resource.id.stringRepresentation] = bag + } + let index = bag.add { result in + subscriber.putNext(result) + subscriber.putCompletion() + } + + let workDisposable = signal.start() + + return ActionDisposable { + workDisposable.dispose() + + Queue.mainQueue().async { + if let bag = self.backgroundUploadResultSubscribers[resource.id.stringRepresentation] { + bag.remove(index) + if bag.isEmpty { + //TODO:cancel tasks + } + } + } + } + } + |> runOn(.mainQueue()) + } + + private func addBackgroundUploadTask(id: String, path: String) { + let baseAppBundleId = Bundle.main.bundleIdentifier! + let session = self.urlSession(identifier: "\(baseAppBundleId).backroundSession") + + let fileName = "upload-\(UInt32.random(in: 0 ... UInt32.max))" + let uploadFilePath = NSTemporaryDirectory() + "/" + fileName + guard let sourceFile = ManagedFile(queue: nil, path: uploadFilePath, mode: .readwrite) else { + return + } + guard let inFile = ManagedFile(queue: nil, path: path, mode: .read) else { + return + } + + let boundary = UUID().uuidString + + var headerData = Data() + headerData.append("\r\n--\(boundary)\r\n".data(using: .utf8)!) + headerData.append("Content-Disposition: form-data; name=\"file\"; filename=\"\(fileName)\"\r\n".data(using: .utf8)!) + headerData.append("Content-Type: image/png\r\n\r\n".data(using: .utf8)!) + + var footerData = Data() + footerData.append("\r\n--\(boundary)--\r\n".data(using: .utf8)!) + + let _ = sourceFile.write(headerData) + + let bufferSize = 512 * 1024 + let buffer = malloc(bufferSize)! + defer { + free(buffer) + } + + while true { + let readBytes = inFile.read(buffer, bufferSize) + if readBytes <= 0 { + break + } else { + let _ = sourceFile.write(buffer, count: readBytes) + } + } + + let _ = sourceFile.write(footerData) + + sourceFile._unsafeClose() + inFile._unsafeClose() + + var request = URLRequest(url: URL(string: "http://localhost:25478/upload?token=f9403fc5f537b4ab332d")!) + request.httpMethod = "POST" + + request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type") + request.setValue(id, forHTTPHeaderField: "tresource") + + let task = session.uploadTask(with: request, fromFile: URL(fileURLWithPath: uploadFilePath)) + task.resume() + } + + func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { + if let response = task.response as? HTTPURLResponse { + if let originalRequest = task.originalRequest { + if let requestResourceId = originalRequest.value(forHTTPHeaderField: "tresource") { + if let bag = self.backgroundUploadResultSubscribers[requestResourceId] { + for item in bag.copyItems() { + item("http server: \(response.allHeaderFields)") + } + } + } + } + } + } + private func runCacheReindexTasks(lowImpact: Bool, completion: @escaping () -> Void) -> Disposable { let disposable = MetaDisposable() diff --git a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift index 1c25de874d..508a8e7d72 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift @@ -954,63 +954,9 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { } |> distinctUntilChanged - let animatedEmojiStickers = context.engine.stickers.loadedStickerPack(reference: .animatedEmoji, forceActualized: false) - |> map { animatedEmoji -> [String: [StickerPackItem]] in - var animatedEmojiStickers: [String: [StickerPackItem]] = [:] - switch animatedEmoji { - case let .result(_, items, _): - for item in items { - if let emoji = item.getStringRepresentationsOfIndexKeys().first { - animatedEmojiStickers[emoji.basicEmoji.0] = [item] - let strippedEmoji = emoji.basicEmoji.0.strippedEmoji - if animatedEmojiStickers[strippedEmoji] == nil { - animatedEmojiStickers[strippedEmoji] = [item] - } - } - } - default: - break - } - return animatedEmojiStickers - } + let animatedEmojiStickers: Signal<[String: [StickerPackItem]], NoError> = (context as! AccountContextImpl).animatedEmojiStickersSignal - let additionalAnimatedEmojiStickers = context.engine.stickers.loadedStickerPack(reference: .animatedEmojiAnimations, forceActualized: false) - |> map { animatedEmoji -> [String: [Int: StickerPackItem]] in - let sequence = "0️⃣1️⃣2️⃣3️⃣4️⃣5️⃣6️⃣7️⃣8️⃣9️⃣".strippedEmoji - var animatedEmojiStickers: [String: [Int: StickerPackItem]] = [:] - switch animatedEmoji { - case let .result(_, items, _): - for item in items { - let indexKeys = item.getStringRepresentationsOfIndexKeys() - if indexKeys.count > 1, let first = indexKeys.first, let last = indexKeys.last { - let emoji: String? - let indexEmoji: String? - if sequence.contains(first.strippedEmoji) { - emoji = last - indexEmoji = first - } else if sequence.contains(last.strippedEmoji) { - emoji = first - indexEmoji = last - } else { - emoji = nil - indexEmoji = nil - } - - if let emoji = emoji?.strippedEmoji, let indexEmoji = indexEmoji?.strippedEmoji.first, let strIndex = sequence.firstIndex(of: indexEmoji) { - let index = sequence.distance(from: sequence.startIndex, to: strIndex) - if animatedEmojiStickers[emoji] != nil { - animatedEmojiStickers[emoji]![index] = item - } else { - animatedEmojiStickers[emoji] = [index: item] - } - } - } - } - default: - break - } - return animatedEmojiStickers - } + let additionalAnimatedEmojiStickers = (context as! AccountContextImpl).additionalAnimatedEmojiStickers let previousHistoryAppearsCleared = Atomic(value: nil) @@ -1077,7 +1023,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { customThreadOutgoingReadState = .single(nil) } - let availableReactions = context.engine.stickers.availableReactions() + let availableReactions: Signal = (context as! AccountContextImpl).availableReactions let defaultReaction = combineLatest( context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)),