import Foundation import UserNotifications import SwiftSignalKit import Postbox import TelegramCore import BuildConfig import OpenSSLEncryptionProvider import TelegramUIPreferences import WebPBinding import RLottieBinding import GZip import UIKit private let queue = Queue() private var installedSharedLogger = false private func setupSharedLogger(rootPath: String, path: String) { if !installedSharedLogger { installedSharedLogger = true Logger.setSharedLogger(Logger(rootPath: rootPath, basePath: path)) } } private let accountAuxiliaryMethods = AccountAuxiliaryMethods(fetchResource: { account, resource, ranges, _ in return nil }, fetchResourceMediaReferenceHash: { resource in return .single(nil) }, prepareSecretThumbnailData: { _ in return nil }) private func rootPathForBasePath(_ appGroupPath: String) -> String { return appGroupPath + "/telegram-data" } private let deviceColorSpace: CGColorSpace = { if #available(iOSApplicationExtension 9.3, iOS 9.3, *) { if let colorSpace = CGColorSpace(name: CGColorSpace.displayP3) { return colorSpace } else { return CGColorSpaceCreateDeviceRGB() } } else { return CGColorSpaceCreateDeviceRGB() } }() private func getSharedDevideGraphicsContextSettings() -> DeviceGraphicsContextSettings { struct OpaqueSettings { let rowAlignment: Int let bitsPerPixel: Int let bitsPerComponent: Int let opaqueBitmapInfo: CGBitmapInfo let colorSpace: CGColorSpace init(context: CGContext) { self.rowAlignment = context.bytesPerRow self.bitsPerPixel = context.bitsPerPixel self.bitsPerComponent = context.bitsPerComponent self.opaqueBitmapInfo = context.bitmapInfo if #available(iOS 10.0, *) { if UIScreen.main.traitCollection.displayGamut == .P3 { self.colorSpace = CGColorSpace(name: CGColorSpace.displayP3) ?? context.colorSpace! } else { self.colorSpace = context.colorSpace! } } else { self.colorSpace = context.colorSpace! } assert(self.rowAlignment == 32) assert(self.bitsPerPixel == 32) assert(self.bitsPerComponent == 8) } } struct TransparentSettings { let transparentBitmapInfo: CGBitmapInfo init(context: CGContext) { self.transparentBitmapInfo = context.bitmapInfo } } var opaqueSettings: OpaqueSettings? var transparentSettings: TransparentSettings? if #available(iOS 10.0, *) { let opaqueFormat = UIGraphicsImageRendererFormat() let transparentFormat = UIGraphicsImageRendererFormat() if #available(iOS 12.0, *) { opaqueFormat.preferredRange = .standard transparentFormat.preferredRange = .standard } opaqueFormat.opaque = true transparentFormat.opaque = false let opaqueRenderer = UIGraphicsImageRenderer(bounds: CGRect(origin: CGPoint(), size: CGSize(width: 1.0, height: 1.0)), format: opaqueFormat) let _ = opaqueRenderer.image(actions: { context in opaqueSettings = OpaqueSettings(context: context.cgContext) }) let transparentRenderer = UIGraphicsImageRenderer(bounds: CGRect(origin: CGPoint(), size: CGSize(width: 1.0, height: 1.0)), format: transparentFormat) let _ = transparentRenderer.image(actions: { context in transparentSettings = TransparentSettings(context: context.cgContext) }) } else { UIGraphicsBeginImageContextWithOptions(CGSize(width: 1.0, height: 1.0), true, 1.0) let refContext = UIGraphicsGetCurrentContext()! opaqueSettings = OpaqueSettings(context: refContext) UIGraphicsEndImageContext() UIGraphicsBeginImageContextWithOptions(CGSize(width: 1.0, height: 1.0), false, 1.0) let refCtxTransparent = UIGraphicsGetCurrentContext()! transparentSettings = TransparentSettings(context: refCtxTransparent) UIGraphicsEndImageContext() } return DeviceGraphicsContextSettings( rowAlignment: opaqueSettings!.rowAlignment, bitsPerPixel: opaqueSettings!.bitsPerPixel, bitsPerComponent: opaqueSettings!.bitsPerComponent, opaqueBitmapInfo: opaqueSettings!.opaqueBitmapInfo, transparentBitmapInfo: transparentSettings!.transparentBitmapInfo, colorSpace: opaqueSettings!.colorSpace ) } public struct DeviceGraphicsContextSettings { public static let shared: DeviceGraphicsContextSettings = getSharedDevideGraphicsContextSettings() public let rowAlignment: Int public let bitsPerPixel: Int public let bitsPerComponent: Int public let opaqueBitmapInfo: CGBitmapInfo public let transparentBitmapInfo: CGBitmapInfo public let colorSpace: CGColorSpace public func bytesPerRow(forWidth width: Int) -> Int { let baseValue = self.bitsPerPixel * width / 8 return (baseValue + 31) & ~0x1F } } private final class DrawingContext { let size: CGSize let scale: CGFloat let scaledSize: CGSize let bytesPerRow: Int private let bitmapInfo: CGBitmapInfo let length: Int let bytes: UnsafeMutableRawPointer private let data: Data private let context: CGContext private var hasGeneratedImage = false func withContext(_ f: (CGContext) -> ()) { let context = self.context context.translateBy(x: self.size.width / 2.0, y: self.size.height / 2.0) context.scaleBy(x: 1.0, y: -1.0) context.translateBy(x: -self.size.width / 2.0, y: -self.size.height / 2.0) f(context) context.translateBy(x: self.size.width / 2.0, y: self.size.height / 2.0) context.scaleBy(x: 1.0, y: -1.0) context.translateBy(x: -self.size.width / 2.0, y: -self.size.height / 2.0) } func withFlippedContext(_ f: (CGContext) -> ()) { f(self.context) } init(size: CGSize, scale: CGFloat = 1.0, opaque: Bool = false, clear: Bool = false) { assert(!size.width.isZero && !size.height.isZero) let size: CGSize = CGSize(width: max(1.0, size.width), height: max(1.0, size.height)) let actualScale: CGFloat if scale.isZero { actualScale = 1.0 } else { actualScale = scale } self.size = size self.scale = actualScale self.scaledSize = CGSize(width: size.width * actualScale, height: size.height * actualScale) self.bytesPerRow = DeviceGraphicsContextSettings.shared.bytesPerRow(forWidth: Int(self.scaledSize.width)) self.length = self.bytesPerRow * Int(self.scaledSize.height) self.bytes = malloc(self.length) self.data = Data(bytesNoCopy: self.bytes, count: self.length, deallocator: .custom({ bytes, _ in free(bytes) })) if opaque { self.bitmapInfo = DeviceGraphicsContextSettings.shared.opaqueBitmapInfo } else { self.bitmapInfo = DeviceGraphicsContextSettings.shared.transparentBitmapInfo } self.context = CGContext( data: self.bytes, width: Int(self.scaledSize.width), height: Int(self.scaledSize.height), bitsPerComponent: 8, bytesPerRow: self.bytesPerRow, space: deviceColorSpace, bitmapInfo: self.bitmapInfo.rawValue, releaseCallback: nil, releaseInfo: nil )! self.context.scaleBy(x: self.scale, y: self.scale) if clear { memset(self.bytes, 0, self.length) } } func generateImage() -> UIImage? { if self.scaledSize.width.isZero || self.scaledSize.height.isZero { return nil } if self.hasGeneratedImage { preconditionFailure() } self.hasGeneratedImage = true guard let dataProvider = CGDataProvider(data: self.data as CFData) else { return nil } if let image = CGImage( width: Int(self.scaledSize.width), height: Int(self.scaledSize.height), bitsPerComponent: self.context.bitsPerComponent, bitsPerPixel: self.context.bitsPerPixel, bytesPerRow: self.context.bytesPerRow, space: DeviceGraphicsContextSettings.shared.colorSpace, bitmapInfo: self.context.bitmapInfo, provider: dataProvider, decode: nil, shouldInterpolate: true, intent: .defaultIntent ) { return UIImage(cgImage: image, scale: self.scale, orientation: .up) } else { return nil } } } private extension CGSize { func fitted(_ size: CGSize) -> CGSize { var fittedSize = self if fittedSize.width > size.width { fittedSize = CGSize(width: size.width, height: floor((fittedSize.height * size.width / max(fittedSize.width, 1.0)))) } if fittedSize.height > size.height { fittedSize = CGSize(width: floor((fittedSize.width * size.height / max(fittedSize.height, 1.0))), height: size.height) } return fittedSize } } private func convertLottieImage(data: Data) -> UIImage? { let decompressedData = TGGUnzipData(data, 512 * 1024) ?? data guard let animation = LottieInstance(data: decompressedData, cacheKey: "") else { return nil } let size = animation.dimensions.fitted(CGSize(width: 200.0, height: 200.0)) let context = DrawingContext(size: size, scale: 1.0, opaque: false, clear: true) animation.renderFrame(with: 0, into: context.bytes.assumingMemoryBound(to: UInt8.self), width: Int32(context.scaledSize.width), height: Int32(context.scaledSize.height), bytesPerRow: Int32(context.bytesPerRow)) return context.generateImage() } @available(iOSApplicationExtension 10.0, iOS 10.0, *) private struct NotificationContent { var title: String? var subtitle: String? var body: String? var threadId: String? var sound: String? var badge: Int? var category: String? var userInfo: [AnyHashable: Any] = [:] var attachments: [UNNotificationAttachment] = [] func asNotificationContent() -> UNNotificationContent { let content = UNMutableNotificationContent() if let title = self.title { content.title = title } if let subtitle = self.subtitle { content.subtitle = subtitle } if let body = self.body { content.body = body } if let threadId = self.threadId { content.threadIdentifier = threadId } if let sound = self.sound { content.sound = UNNotificationSound(named: UNNotificationSoundName(rawValue: sound)) } if let badge = self.badge { content.badge = badge as NSNumber } if let category = self.category { content.categoryIdentifier = category } if !self.userInfo.isEmpty { content.userInfo = self.userInfo } if !self.attachments.isEmpty { content.attachments = self.attachments } return content } } @available(iOSApplicationExtension 10.0, iOS 10.0, *) private final class NotificationServiceHandler { private let queue: Queue private let accountManager: AccountManager private let encryptionParameters: ValueBoxEncryptionParameters private var stateManager: AccountStateManager? private let notificationKeyDisposable = MetaDisposable() private let pollDisposable = MetaDisposable() init?(queue: Queue, updateCurrentContent: @escaping (UNNotificationContent) -> Void, completed: @escaping () -> Void, payload: [AnyHashable: Any]) { self.queue = queue guard let appBundleIdentifier = Bundle.main.bundleIdentifier, let lastDotRange = appBundleIdentifier.range(of: ".", options: [.backwards]) else { return nil } let baseAppBundleId = String(appBundleIdentifier[..(basePath: rootPath + "/accounts-metadata", isTemporary: true, isReadOnly: false, useCaches: false) let deviceSpecificEncryptionParameters = BuildConfig.deviceSpecificEncryptionParameters(rootPath, baseAppBundleId: baseAppBundleId) self.encryptionParameters = ValueBoxEncryptionParameters(forceEncryptionIfNoSet: false, key: ValueBoxEncryptionParameters.Key(data: deviceSpecificEncryptionParameters.key)!, salt: ValueBoxEncryptionParameters.Salt(data: deviceSpecificEncryptionParameters.salt)!) let networkArguments = NetworkInitializationArguments(apiId: apiId, apiHash: apiHash, languagesCategory: languagesCategory, appVersion: appVersion, voipMaxLayer: 0, voipVersions: [], appData: .single(buildConfig.bundleData(withAppToken: nil, signatureDict: nil)), autolockDeadine: .single(nil), encryptionProvider: OpenSSLEncryptionProvider()) guard var encryptedPayload = payload["p"] as? String else { return nil } encryptedPayload = encryptedPayload.replacingOccurrences(of: "-", with: "+") encryptedPayload = encryptedPayload.replacingOccurrences(of: "_", with: "/") while encryptedPayload.count % 4 != 0 { encryptedPayload.append("=") } guard let payloadData = Data(base64Encoded: encryptedPayload) else { return nil } let _ = (self.accountManager.currentAccountRecord(allocateIfNotExists: false) |> take(1) |> deliverOn(self.queue)).start(next: { [weak self] records in guard let strongSelf = self, let record = records else { return } let _ = (standaloneStateManager( accountManager: strongSelf.accountManager, networkArguments: networkArguments, id: record.0, encryptionParameters: strongSelf.encryptionParameters, rootPath: rootPath, auxiliaryMethods: accountAuxiliaryMethods ) |> deliverOn(strongSelf.queue)).start(next: { stateManager in guard let strongSelf = self else { return } guard let stateManager = stateManager else { completed() return } strongSelf.stateManager = stateManager strongSelf.notificationKeyDisposable.set((existingMasterNotificationsKey(postbox: stateManager.postbox) |> deliverOn(strongSelf.queue)).start(next: { notificationsKey in guard let strongSelf = self else { return } guard let notificationsKey = notificationsKey else { completed() return } guard let decryptedPayload = decryptedNotificationPayload(key: notificationsKey, data: payloadData) else { completed() return } guard let payloadJson = try? JSONSerialization.jsonObject(with: decryptedPayload, options: []) as? [String: Any] else { completed() return } var peerId: PeerId? var messageId: MessageId.Id? var mediaAttachment: Media? if let messageIdString = payloadJson["msg_id"] as? String { messageId = Int32(messageIdString) } if let fromIdString = payloadJson["from_id"] as? String { if let userIdValue = Int64(fromIdString) { peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userIdValue)) } } else if let chatIdString = payloadJson["chat_id"] as? String { if let chatIdValue = Int64(chatIdString) { peerId = PeerId(namespace: Namespaces.Peer.CloudGroup, id: PeerId.Id._internalFromInt64Value(chatIdValue)) } } else if let channelIdString = payloadJson["channel_id"] as? String { if let channelIdValue = Int64(channelIdString) { peerId = PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(channelIdValue)) } } enum Action { case logout case poll(peerId: PeerId, content: NotificationContent) case deleteMessage([MessageId]) case readMessage(MessageId) } var action: Action? if let locKey = payloadJson["loc-key"] as? String { switch locKey { case "SESSION_REVOKE": action = .logout case "MESSAGE_MUTED": if let peerId = peerId { action = .poll(peerId: peerId, content: NotificationContent()) } case "MESSAGE_DELETED": if let peerId = peerId { if let messageId = messageId { action = .deleteMessage([MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: messageId)]) } else if let messageIds = payloadJson["messages"] as? String { var messagesDeleted: [MessageId] = [] for messageId in messageIds.split(separator: ",") { if let messageIdValue = Int32(messageId) { messagesDeleted.append(MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: messageIdValue)) } } action = .deleteMessage(messagesDeleted) } } case "READ_HISTORY": if let peerId = peerId { if let messageIdString = payloadJson["max_id"] as? String { if let maxId = Int32(messageIdString) { action = .readMessage(MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: maxId)) } } } default: break } } else { if let aps = payloadJson["aps"] as? [String: Any], let peerId = peerId { var content: NotificationContent = NotificationContent() if let alert = aps["alert"] as? [String: Any] { content.title = alert["title"] as? String content.subtitle = alert["subtitle"] as? String content.body = alert["body"] as? String } else if let alert = aps["alert"] as? String { content.body = alert } else { completed() return } if let messageId = messageId { content.userInfo["msg_id"] = "\(messageId)" } if peerId.namespace == Namespaces.Peer.CloudUser { content.userInfo["from_id"] = "\(peerId.id._internalGetInt64Value())" } else if peerId.namespace == Namespaces.Peer.CloudGroup { content.userInfo["chat_id"] = "\(peerId.id._internalGetInt64Value())" } else if peerId.namespace == Namespaces.Peer.CloudChannel { content.userInfo["channel_id"] = "\(peerId.id._internalGetInt64Value())" } content.userInfo["peerId"] = "\(peerId.toInt64())" content.userInfo["accountId"] = "\(record.0.int64)" if let silentString = payloadJson["silent"] as? String { if let silentValue = Int(silentString), silentValue != 0 { if let title = content.title { content.title = "\(title) 🔕" } } } if var attachmentDataString = payloadJson["attachb64"] as? String { attachmentDataString = attachmentDataString.replacingOccurrences(of: "-", with: "+") attachmentDataString = attachmentDataString.replacingOccurrences(of: "_", with: "/") while attachmentDataString.count % 4 != 0 { attachmentDataString.append("=") } if let attachmentData = Data(base64Encoded: attachmentDataString) { mediaAttachment = _internal_parseMediaAttachment(data: attachmentData) } } if let threadId = aps["thread-id"] as? String { content.threadId = threadId } if let sound = aps["sound"] as? String { content.sound = sound } if let category = aps["category"] as? String { content.category = category let _ = messageId /*if (peerId != 0 && messageId != 0 && parsedAttachment != nil && attachmentData != nil) { userInfo[@"peerId"] = @(peerId); userInfo[@"messageId.namespace"] = @(0); userInfo[@"messageId.id"] = @(messageId); userInfo[@"media"] = [attachmentData base64EncodedStringWithOptions:0]; if (isExpandableMedia) { if ([categoryString isEqualToString:@"r"]) { _bestAttemptContent.categoryIdentifier = @"withReplyMedia"; } else if ([categoryString isEqualToString:@"m"]) { _bestAttemptContent.categoryIdentifier = @"withMuteMedia"; } } }*/ } /*if (accountInfos.accounts.count > 1) { if (_bestAttemptContent.title.length != 0 && account.peerName.length != 0) { _bestAttemptContent.title = [NSString stringWithFormat:@"%@ → %@", _bestAttemptContent.title, account.peerName]; } }*/ action = .poll(peerId: peerId, content: content) updateCurrentContent(content.asNotificationContent()) } } if let action = action { switch action { case .logout: completed() case .poll(let peerId, var content): if let stateManager = strongSelf.stateManager { let pollCompletion: () -> Void = { queue.async { guard let strongSelf = self, let stateManager = strongSelf.stateManager else { completed() return } var fetchMediaSignal: Signal = .single(nil) if let mediaAttachment = mediaAttachment { var fetchResource: TelegramMultipartFetchableResource? if let image = mediaAttachment as? TelegramMediaImage, let representation = largestImageRepresentation(image.representations), let resource = representation.resource as? TelegramMultipartFetchableResource { fetchResource = resource } else if let file = mediaAttachment as? TelegramMediaFile { if file.isSticker { fetchResource = file.resource as? TelegramMultipartFetchableResource } else if file.isVideo { fetchResource = file.previewRepresentations.first?.resource as? TelegramMultipartFetchableResource } } if let resource = fetchResource { if let _ = strongSelf.stateManager?.postbox.mediaBox.completedResourcePath(resource) { } else { let intervals: Signal<[(Range, MediaBoxFetchPriority)], NoError> = .single([(0 ..< Int(Int32.max), MediaBoxFetchPriority.maximum)]) fetchMediaSignal = Signal { subscriber in let collectedData = Atomic(value: Data()) return standaloneMultipartFetch( postbox: stateManager.postbox, network: stateManager.network, resource: resource, datacenterId: resource.datacenterId, size: nil, intervals: intervals, parameters: MediaResourceFetchParameters( tag: nil, info: resourceFetchInfo(resource: resource), isRandomAccessAllowed: true ), encryptionKey: nil, decryptedSize: nil, continueInBackground: false, useMainConnection: true ).start(next: { result in switch result { case let .dataPart(_, data, _, _): let _ = collectedData.modify { current in var current = current current.append(data) return current } default: break } }, error: { _ in subscriber.putNext(nil) subscriber.putCompletion() }, completed: { subscriber.putNext(collectedData.with({ $0 })) subscriber.putCompletion() }) } } } } let _ = (fetchMediaSignal |> timeout(10.0, queue: queue, alternate: .single(nil)) |> deliverOn(queue)).start(next: { mediaData in guard let strongSelf = self, let stateManager = strongSelf.stateManager else { completed() return } let _ = (getCurrentRenderedTotalUnreadCount( accountManager: strongSelf.accountManager, postbox: stateManager.postbox ) |> deliverOn(strongSelf.queue)).start(next: { value in guard let strongSelf = self, let stateManager = strongSelf.stateManager else { completed() return } content.badge = Int(value.0) if let image = mediaAttachment as? TelegramMediaImage, let resource = largestImageRepresentation(image.representations)?.resource { if let mediaData = mediaData { stateManager.postbox.mediaBox.storeResourceData(resource.id, data: mediaData, synchronous: true) } if let storedPath = stateManager.postbox.mediaBox.completedResourcePath(resource, pathExtension: "jpg") { if let attachment = try? UNNotificationAttachment(identifier: "image", url: URL(fileURLWithPath: storedPath), options: nil) { content.attachments.append(attachment) } } } else if let file = mediaAttachment as? TelegramMediaFile { if file.isStaticSticker { let resource = file.resource if let mediaData = mediaData { stateManager.postbox.mediaBox.storeResourceData(resource.id, data: mediaData, synchronous: true) } if let storedPath = stateManager.postbox.mediaBox.completedResourcePath(resource) { if let data = try? Data(contentsOf: URL(fileURLWithPath: storedPath)), let image = WebP.convert(fromWebP: data) { let tempFile = TempBox.shared.tempFile(fileName: "image.png") let _ = try? image.pngData()?.write(to: URL(fileURLWithPath: tempFile.path)) if let attachment = try? UNNotificationAttachment(identifier: "image", url: URL(fileURLWithPath: tempFile.path), options: nil) { content.attachments.append(attachment) } } } } else if file.isAnimatedSticker { let resource = file.resource if let mediaData = mediaData { stateManager.postbox.mediaBox.storeResourceData(resource.id, data: mediaData, synchronous: true) } if let storedPath = stateManager.postbox.mediaBox.completedResourcePath(resource) { if let data = try? Data(contentsOf: URL(fileURLWithPath: storedPath)), let image = convertLottieImage(data: data) { let tempFile = TempBox.shared.tempFile(fileName: "image.png") let _ = try? image.pngData()?.write(to: URL(fileURLWithPath: tempFile.path)) if let attachment = try? UNNotificationAttachment(identifier: "image", url: URL(fileURLWithPath: tempFile.path), options: nil) { content.attachments.append(attachment) } } } } else if file.isVideo, let representation = file.previewRepresentations.first { let resource = representation.resource if let mediaData = mediaData { stateManager.postbox.mediaBox.storeResourceData(resource.id, data: mediaData, synchronous: true) } if let storedPath = stateManager.postbox.mediaBox.completedResourcePath(resource, pathExtension: "jpg") { if let attachment = try? UNNotificationAttachment(identifier: "image", url: URL(fileURLWithPath: storedPath), options: nil) { content.attachments.append(attachment) } } } } updateCurrentContent(content.asNotificationContent()) completed() }) }) } } let pollSignal: Signal stateManager.network.shouldKeepConnection.set(.single(true)) if peerId.namespace == Namespaces.Peer.CloudChannel { pollSignal = standalonePollChannelOnce( postbox: stateManager.postbox, network: stateManager.network, peerId: peerId, stateManager: stateManager ) } else { enum ControlError { case restart } let signal = stateManager.standalonePollDifference() |> castError(ControlError.self) |> mapToSignal { result -> Signal in if result { return .complete() } else { return .fail(.restart) } } |> restartIfError pollSignal = signal } strongSelf.pollDisposable.set(pollSignal.start(completed: { pollCompletion() })) } else { completed() } case let .deleteMessage(ids): let mediaBox = stateManager.postbox.mediaBox let _ = (stateManager.postbox.transaction { transaction -> Void in _internal_deleteMessages(transaction: transaction, mediaBox: mediaBox, ids: ids, deleteMedia: true) } |> deliverOn(strongSelf.queue)).start(completed: { UNUserNotificationCenter.current().getDeliveredNotifications(completionHandler: { notifications in var removeIdentifiers: [String] = [] for notification in notifications { if let peerIdString = notification.request.content.userInfo["peerId"] as? String, let peerIdValue = Int64(peerIdString), let messageIdString = notification.request.content.userInfo["msg_id"] as? String, let messageIdValue = Int32(messageIdString) { for id in ids { if PeerId(peerIdValue) == id.peerId && messageIdValue == id.id { removeIdentifiers.append(notification.request.identifier) } } } } let completeRemoval: () -> Void = { guard let strongSelf = self else { return } let _ = (getCurrentRenderedTotalUnreadCount( accountManager: strongSelf.accountManager, postbox: stateManager.postbox ) |> deliverOn(strongSelf.queue)).start(next: { value in var content = NotificationContent() content.badge = Int(value.0) updateCurrentContent(content.asNotificationContent()) completed() }) } if !removeIdentifiers.isEmpty { UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: removeIdentifiers) queue.after(1.0, { completeRemoval() }) } else { completeRemoval() } }) }) case let .readMessage(id): let _ = (stateManager.postbox.transaction { transaction -> Void in transaction.applyIncomingReadMaxId(id) } |> deliverOn(strongSelf.queue)).start(completed: { UNUserNotificationCenter.current().getDeliveredNotifications(completionHandler: { notifications in var removeIdentifiers: [String] = [] for notification in notifications { if let peerIdString = notification.request.content.userInfo["peerId"] as? String, let peerIdValue = Int64(peerIdString), let messageIdString = notification.request.content.userInfo["msg_id"] as? String, let messageIdValue = Int32(messageIdString) { if PeerId(peerIdValue) == id.peerId && messageIdValue <= id.id { removeIdentifiers.append(notification.request.identifier) } } } let completeRemoval: () -> Void = { guard let strongSelf = self else { return } let _ = (getCurrentRenderedTotalUnreadCount( accountManager: strongSelf.accountManager, postbox: stateManager.postbox ) |> deliverOn(strongSelf.queue)).start(next: { value in var content = NotificationContent() content.badge = Int(value.0) updateCurrentContent(content.asNotificationContent()) completed() }) } if !removeIdentifiers.isEmpty { UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: removeIdentifiers) queue.after(1.0, { completeRemoval() }) } else { completeRemoval() } }) }) } } else { let content = NotificationContent() updateCurrentContent(content.asNotificationContent()) completed() } })) }) }) } deinit { self.pollDisposable.dispose() self.stateManager?.network.shouldKeepConnection.set(.single(false)) } } @available(iOSApplicationExtension 10.0, iOS 10.0, *) private final class BoxedNotificationServiceHandler { let value: NotificationServiceHandler? init(value: NotificationServiceHandler?) { self.value = value } } @available(iOSApplicationExtension 10.0, iOS 10.0, *) @objc(NotificationService) final class NotificationService: UNNotificationServiceExtension { private var impl: QueueLocalObject? private let content = Atomic(value: nil) private var contentHandler: ((UNNotificationContent) -> Void)? override init() { super.init() } override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) { let _ = self.content.swap(request.content) self.contentHandler = contentHandler self.impl = nil let content = self.content self.impl = QueueLocalObject(queue: queue, generate: { [weak self] in return BoxedNotificationServiceHandler(value: NotificationServiceHandler( queue: queue, updateCurrentContent: { value in let _ = content.swap(value) }, completed: { guard let strongSelf = self else { return } strongSelf.impl = nil if let content = content.with({ $0 }), let contentHandler = strongSelf.contentHandler { contentHandler(content) } }, payload: request.content.userInfo )) }) } override func serviceExtensionTimeWillExpire() { if let content = self.content.with({ $0 }), let contentHandler = self.contentHandler { contentHandler(content) } } }