From ad31ed007199d4c7607221d04398a03e77fb5fb2 Mon Sep 17 00:00:00 2001
From: Ali <>
Date: Tue, 28 Sep 2021 18:59:40 +0400
Subject: [PATCH] Support notification avatars
---
Telegram/BUILD | 15 +-
Telegram/NotificationService/BUILD | 1 +
.../Sources/NotificationService.swift | 305 ++++++++++++++++--
3 files changed, 302 insertions(+), 19 deletions(-)
diff --git a/Telegram/BUILD b/Telegram/BUILD
index a35ed06147..b631d43350 100644
--- a/Telegram/BUILD
+++ b/Telegram/BUILD
@@ -436,6 +436,11 @@ app_groups_fragment = """
telegram_bundle_id=telegram_bundle_id
)
+communication_notifications_fragment = """
+com.apple.developer.usernotifications.communication
+
+"""
+
plist_fragment(
name = "TelegramEntitlements",
extension = "entitlements",
@@ -447,7 +452,8 @@ plist_fragment(
icloud_fragment,
apple_pay_merchants_fragment,
unrestricted_voip_fragment,
- carplay_fragment
+ carplay_fragment,
+ communication_notifications_fragment,
])
)
@@ -1595,6 +1601,13 @@ plist_fragment(
com.apple.usernotifications.service
NSExtensionPrincipalClass
NotificationService
+ NSExtensionAttributes
+
+ IntentsSupported
+
+ INSendMessageIntent
+
+
""".format(
telegram_bundle_id = telegram_bundle_id,
diff --git a/Telegram/NotificationService/BUILD b/Telegram/NotificationService/BUILD
index b3f95c3da8..44e3bdb66c 100644
--- a/Telegram/NotificationService/BUILD
+++ b/Telegram/NotificationService/BUILD
@@ -20,6 +20,7 @@ swift_library(
"//submodules/WebPBinding:WebPBinding",
"//submodules/rlottie:RLottieBinding",
"//submodules/GZip:GZip",
+ "//submodules/PersistentStringHash:PersistentStringHash",
],
visibility = [
"//visibility:public",
diff --git a/Telegram/NotificationService/Sources/NotificationService.swift b/Telegram/NotificationService/Sources/NotificationService.swift
index 207255e831..c5cc9c8ccf 100644
--- a/Telegram/NotificationService/Sources/NotificationService.swift
+++ b/Telegram/NotificationService/Sources/NotificationService.swift
@@ -10,6 +10,8 @@ import WebPBinding
import RLottieBinding
import GZip
import UIKit
+import Intents
+import PersistentStringHash
private let queue = Queue()
@@ -277,6 +279,166 @@ private func convertLottieImage(data: Data) -> UIImage? {
return context.generateImage()
}
+private func testAvatarImage(size: CGSize) -> UIImage? {
+ UIGraphicsBeginImageContextWithOptions(size, false, 2.0)
+ let context = UIGraphicsGetCurrentContext()!
+
+ context.beginPath()
+ context.addEllipse(in: CGRect(x: 0.0, y: 0.0, width: size.width, height: size.height))
+ context.clip()
+
+ context.setFillColor(UIColor.red.cgColor)
+ context.fill(CGRect(origin: CGPoint(), size: size))
+
+ let image = UIGraphicsGetImageFromCurrentImageContext()
+ UIGraphicsEndImageContext()
+ return image
+}
+
+private func avatarRoundImage(size: CGSize, source: UIImage) -> UIImage? {
+ UIGraphicsBeginImageContextWithOptions(size, false, 0.0)
+ let context = UIGraphicsGetCurrentContext()
+
+ context?.beginPath()
+ context?.addEllipse(in: CGRect(x: 0.0, y: 0.0, width: size.width, height: size.height))
+ context?.clip()
+
+ source.draw(in: CGRect(origin: CGPoint(), size: size))
+
+ let image = UIGraphicsGetImageFromCurrentImageContext()
+ UIGraphicsEndImageContext()
+ return image
+}
+
+private extension UIColor {
+ convenience init(rgb: UInt32) {
+ self.init(red: CGFloat((rgb >> 16) & 0xff) / 255.0, green: CGFloat((rgb >> 8) & 0xff) / 255.0, blue: CGFloat(rgb & 0xff) / 255.0, alpha: 1.0)
+ }
+}
+
+private let gradientColors: [NSArray] = [
+ [UIColor(rgb: 0xff516a).cgColor, UIColor(rgb: 0xff885e).cgColor],
+ [UIColor(rgb: 0xffa85c).cgColor, UIColor(rgb: 0xffcd6a).cgColor],
+ [UIColor(rgb: 0x665fff).cgColor, UIColor(rgb: 0x82b1ff).cgColor],
+ [UIColor(rgb: 0x54cb68).cgColor, UIColor(rgb: 0xa0de7e).cgColor],
+ [UIColor(rgb: 0x4acccd).cgColor, UIColor(rgb: 0x00fcfd).cgColor],
+ [UIColor(rgb: 0x2a9ef1).cgColor, UIColor(rgb: 0x72d5fd).cgColor],
+ [UIColor(rgb: 0xd669ed).cgColor, UIColor(rgb: 0xe0a2f3).cgColor],
+]
+
+private func avatarViewLettersImage(size: CGSize, peerId: Int64, accountPeerId: Int64, letters: [String]) -> UIImage? {
+ UIGraphicsBeginImageContextWithOptions(size, false, 2.0)
+ let context = UIGraphicsGetCurrentContext()
+
+ context?.beginPath()
+ context?.addEllipse(in: CGRect(x: 0.0, y: 0.0, width: size.width, height: size.height))
+ context?.clip()
+
+ let colorIndex = abs(Int(accountPeerId + peerId))
+
+ let colorsArray = gradientColors[colorIndex % gradientColors.count]
+ var locations: [CGFloat] = [1.0, 0.0]
+ let gradient = CGGradient(colorsSpace: deviceColorSpace, colors: colorsArray, locations: &locations)!
+
+ context?.drawLinearGradient(gradient, start: CGPoint(), end: CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions())
+
+ context?.setBlendMode(.normal)
+
+ let string = letters.count == 0 ? "" : (letters[0] + (letters.count == 1 ? "" : letters[1]))
+ let attributedString = NSAttributedString(string: string, attributes: [NSAttributedString.Key.font: UIFont.systemFont(ofSize: 20.0), NSAttributedString.Key.foregroundColor: UIColor.white])
+
+ let line = CTLineCreateWithAttributedString(attributedString)
+ let lineBounds = CTLineGetBoundsWithOptions(line, .useGlyphPathBounds)
+
+ let lineOffset = CGPoint(x: string == "B" ? 1.0 : 0.0, y: 0.0)
+ let lineOrigin = CGPoint(x: floor(-lineBounds.origin.x + (size.width - lineBounds.size.width) / 2.0) + lineOffset.x, y: floor(-lineBounds.origin.y + (size.height - lineBounds.size.height) / 2.0))
+
+ context?.translateBy(x: size.width / 2.0, y: size.height / 2.0)
+ context?.scaleBy(x: 1.0, y: -1.0)
+ context?.translateBy(x: -size.width / 2.0, y: -size.height / 2.0)
+
+ context?.translateBy(x: lineOrigin.x, y: lineOrigin.y)
+ if let context = context {
+ CTLineDraw(line, context)
+ }
+ context?.translateBy(x: -lineOrigin.x, y: -lineOrigin.y)
+
+ let image = UIGraphicsGetImageFromCurrentImageContext()
+ UIGraphicsEndImageContext()
+ return image
+}
+
+private func avatarImage(path: String?, peerId: Int64, accountPeerId: Int64, letters: [String], size: CGSize) -> UIImage {
+ if let path = path, let image = UIImage(contentsOfFile: path), let roundImage = avatarRoundImage(size: size, source: image) {
+ return roundImage
+ } else {
+ return avatarViewLettersImage(size: size, peerId: peerId, accountPeerId: accountPeerId, letters: letters)!
+ }
+}
+
+private func storeTemporaryImage(path: String) -> String {
+ let imagesPath = NSTemporaryDirectory() + "/aps-data"
+ let _ = try? FileManager.default.createDirectory(at: URL(fileURLWithPath: imagesPath), withIntermediateDirectories: true, attributes: nil)
+
+ let tempPath = imagesPath + "\(path.persistentHashValue)"
+ if FileManager.default.fileExists(atPath: tempPath) {
+ return tempPath
+ }
+
+ let _ = try? FileManager.default.copyItem(at: URL(fileURLWithPath: path), to: URL(fileURLWithPath: tempPath))
+
+ return tempPath
+}
+
+@available(iOS 15.0, *)
+private func peerAvatar(mediaBox: MediaBox, accountPeerId: PeerId, peer: Peer) -> INImage? {
+ if let resource = smallestImageRepresentation(peer.profileImageRepresentations)?.resource, let path = mediaBox.completedResourcePath(resource) {
+ let cachedPath = mediaBox.cachedRepresentationPathForId(resource.id.stringRepresentation, representationId: "intents.png", keepDuration: .shortLived)
+ if let _ = fileSize(cachedPath), let data = try? Data(contentsOf: URL(fileURLWithPath: cachedPath), options: .alwaysMapped) {
+ do {
+ return INImage(url: URL(fileURLWithPath: storeTemporaryImage(path: cachedPath)))
+ } catch {
+ return nil
+ }
+ } else {
+ let image = avatarImage(path: path, peerId: peer.id.toInt64(), accountPeerId: accountPeerId.toInt64(), letters: peer.displayLetters, size: CGSize(width: 50.0, height: 50.0))
+ if let data = image.pngData() {
+ let _ = try? data.write(to: URL(fileURLWithPath: cachedPath), options: .atomic)
+ }
+ do {
+ //let data = try Data(contentsOf: URL(fileURLWithPath: cachedPath), options: .alwaysMapped)
+ //return INImage(imageData: data)
+ return INImage(url: URL(fileURLWithPath: storeTemporaryImage(path: cachedPath)))
+ } catch {
+ return nil
+ }
+ }
+ }
+
+ let cachedPath = mediaBox.cachedRepresentationPathForId("lettersAvatar-\(peer.displayLetters.joined(separator: ","))", representationId: "intents.png", keepDuration: .shortLived)
+ if let _ = fileSize(cachedPath) {
+ do {
+ //let data = try Data(contentsOf: URL(fileURLWithPath: cachedPath), options: [])
+ //return INImage(imageData: data)
+ return INImage(url: URL(fileURLWithPath: storeTemporaryImage(path: cachedPath)))
+ } catch {
+ return nil
+ }
+ } else {
+ let image = avatarImage(path: nil, peerId: peer.id.toInt64(), accountPeerId: accountPeerId.toInt64(), letters: peer.displayLetters, size: CGSize(width: 50.0, height: 50.0))
+ if let data = image.pngData() {
+ let _ = try? data.write(to: URL(fileURLWithPath: cachedPath), options: .atomic)
+ }
+ do {
+ //let data = try Data(contentsOf: URL(fileURLWithPath: cachedPath), options: .alwaysMapped)
+ //return INImage(imageData: data)
+ return INImage(url: URL(fileURLWithPath: storeTemporaryImage(path: cachedPath)))
+ } catch {
+ return nil
+ }
+ }
+}
+
@available(iOSApplicationExtension 10.0, iOS 10.0, *)
private struct NotificationContent {
var title: String?
@@ -289,8 +451,33 @@ private struct NotificationContent {
var userInfo: [AnyHashable: Any] = [:]
var attachments: [UNNotificationAttachment] = []
- func asNotificationContent() -> UNNotificationContent {
- let content = UNMutableNotificationContent()
+ var senderPerson: INPerson?
+ var senderImage: INImage?
+
+ mutating func addSenderInfo(mediaBox: MediaBox, accountPeerId: PeerId, peer: Peer) {
+ if #available(iOS 15.0, *) {
+ let image = peerAvatar(mediaBox: mediaBox, accountPeerId: accountPeerId, peer: peer)
+
+ self.senderImage = image
+
+ var personNameComponents = PersonNameComponents()
+ personNameComponents.nickname = peer.debugDisplayTitle
+
+ self.senderPerson = INPerson(
+ personHandle: INPersonHandle(value: "\(peer.id.toInt64())", type: .unknown),
+ nameComponents: personNameComponents,
+ displayName: peer.debugDisplayTitle,
+ image: image,
+ contactIdentifier: nil,
+ customIdentifier: nil,
+ isMe: false,
+ suggestionType: .none
+ )
+ }
+ }
+
+ func generate() -> UNNotificationContent {
+ var content = UNMutableNotificationContent()
if let title = self.title {
content.title = title
@@ -320,6 +507,46 @@ private struct NotificationContent {
content.attachments = self.attachments
}
+ if #available(iOS 15.0, *) {
+ if let senderPerson = self.senderPerson {
+ let mePerson = INPerson(
+ personHandle: INPersonHandle(value: "0", type: .unknown),
+ nameComponents: nil,
+ displayName: nil,
+ image: nil,
+ contactIdentifier: nil,
+ customIdentifier: nil,
+ isMe: true,
+ suggestionType: .none
+ )
+
+ let incomingCommunicationIntent = INSendMessageIntent(
+ recipients: [mePerson],
+ outgoingMessageType: .outgoingMessageText,
+ content: content.body,
+ speakableGroupName: INSpeakableString(spokenPhrase: "Sender Name"),
+ conversationIdentifier: "sampleConversationIdentifier",
+ serviceName: nil,
+ sender: senderPerson,
+ attachments: nil
+ )
+
+ if let senderImage = self.senderImage {
+ incomingCommunicationIntent.setImage(senderImage, forParameterNamed: \.sender)
+ }
+
+ let interaction = INInteraction(intent: incomingCommunicationIntent, response: nil)
+ interaction.direction = .incoming
+ interaction.donate(completion: nil)
+
+ do {
+ content = try content.updating(from: incomingCommunicationIntent) as! UNMutableNotificationContent
+ } catch let e {
+ print("Exception: \(e)")
+ }
+ }
+ }
+
return content
}
}
@@ -334,7 +561,7 @@ private final class NotificationServiceHandler {
private let notificationKeyDisposable = MetaDisposable()
private let pollDisposable = MetaDisposable()
- init?(queue: Queue, updateCurrentContent: @escaping (UNNotificationContent) -> Void, completed: @escaping () -> Void, payload: [AnyHashable: Any]) {
+ init?(queue: Queue, updateCurrentContent: @escaping (NotificationContent) -> Void, completed: @escaping () -> Void, payload: [AnyHashable: Any]) {
self.queue = queue
guard let appBundleIdentifier = Bundle.main.bundleIdentifier, let lastDotRange = appBundleIdentifier.range(of: ".", options: [.backwards]) else {
@@ -434,6 +661,8 @@ private final class NotificationServiceHandler {
var messageId: MessageId.Id?
var mediaAttachment: Media?
+ var interactionAuthorId: PeerId?
+
if let messageIdString = payloadJson["msg_id"] as? String {
messageId = Int32(messageIdString)
}
@@ -510,6 +739,7 @@ private final class NotificationServiceHandler {
if let messageId = messageId {
content.userInfo["msg_id"] = "\(messageId)"
+ interactionAuthorId = peerId
}
if peerId.namespace == Namespaces.Peer.CloudUser {
@@ -579,7 +809,7 @@ private final class NotificationServiceHandler {
action = .poll(peerId: peerId, content: content)
- updateCurrentContent(content.asNotificationContent())
+ updateCurrentContent(content)
}
}
@@ -587,9 +817,11 @@ private final class NotificationServiceHandler {
switch action {
case .logout:
completed()
- case .poll(let peerId, var content):
+ case let .poll(peerId, initialContent):
if let stateManager = strongSelf.stateManager {
- let pollCompletion: () -> Void = {
+ let pollCompletion: (NotificationContent) -> Void = { content in
+ var content = content
+
queue.async {
guard let strongSelf = self, let stateManager = strongSelf.stateManager else {
completed()
@@ -728,7 +960,7 @@ private final class NotificationServiceHandler {
}
}
- updateCurrentContent(content.asNotificationContent())
+ updateCurrentContent(content)
completed()
})
@@ -764,8 +996,31 @@ private final class NotificationServiceHandler {
pollSignal = signal
}
- strongSelf.pollDisposable.set(pollSignal.start(completed: {
- pollCompletion()
+ let pollWithUpdatedContent: Signal
+ if let interactionAuthorId = interactionAuthorId {
+ pollWithUpdatedContent = stateManager.postbox.transaction { transaction -> NotificationContent in
+ var content = initialContent
+
+ if let peer = transaction.getPeer(interactionAuthorId) {
+ content.addSenderInfo(mediaBox: stateManager.postbox.mediaBox, accountPeerId: stateManager.accountPeerId, peer: peer)
+ }
+
+ return content
+ }
+ |> then(
+ pollSignal
+ |> map { _ -> NotificationContent in }
+ )
+ } else {
+ pollWithUpdatedContent = pollSignal
+ |> map { _ -> NotificationContent in }
+ }
+
+ var updatedContent = initialContent
+ strongSelf.pollDisposable.set(pollWithUpdatedContent.start(next: { content in
+ updatedContent = content
+ }, completed: {
+ pollCompletion(updatedContent)
}))
} else {
completed()
@@ -800,7 +1055,7 @@ private final class NotificationServiceHandler {
var content = NotificationContent()
content.badge = Int(value.0)
- updateCurrentContent(content.asNotificationContent())
+ updateCurrentContent(content)
completed()
})
@@ -843,7 +1098,7 @@ private final class NotificationServiceHandler {
var content = NotificationContent()
content.badge = Int(value.0)
- updateCurrentContent(content.asNotificationContent())
+ updateCurrentContent(content)
completed()
})
@@ -862,7 +1117,7 @@ private final class NotificationServiceHandler {
}
} else {
let content = NotificationContent()
- updateCurrentContent(content.asNotificationContent())
+ updateCurrentContent(content)
completed()
}
@@ -891,7 +1146,8 @@ private final class BoxedNotificationServiceHandler {
final class NotificationService: UNNotificationServiceExtension {
private var impl: QueueLocalObject?
- private let content = Atomic(value: nil)
+ private var initialContent: UNNotificationContent?
+ private let content = Atomic(value: nil)
private var contentHandler: ((UNNotificationContent) -> Void)?
override init() {
@@ -899,7 +1155,7 @@ final class NotificationService: UNNotificationServiceExtension {
}
override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
- let _ = self.content.swap(request.content)
+ self.initialContent = request.content
self.contentHandler = contentHandler
self.impl = nil
@@ -917,8 +1173,17 @@ final class NotificationService: UNNotificationServiceExtension {
return
}
strongSelf.impl = nil
- if let content = content.with({ $0 }), let contentHandler = strongSelf.contentHandler {
- contentHandler(content)
+
+ if let contentHandler = strongSelf.contentHandler {
+ if let content = content.with({ $0 }) {
+ /*let request = UNNotificationRequest(identifier: UUID().uuidString, content: content.generate(), trigger: .none)
+ UNUserNotificationCenter.current().add(request)
+ contentHandler(UNMutableNotificationContent())*/
+
+ contentHandler(content.generate())
+ } else if let initialContent = strongSelf.initialContent {
+ contentHandler(initialContent)
+ }
}
},
payload: request.content.userInfo
@@ -927,8 +1192,12 @@ final class NotificationService: UNNotificationServiceExtension {
}
override func serviceExtensionTimeWillExpire() {
- if let content = self.content.with({ $0 }), let contentHandler = self.contentHandler {
- contentHandler(content)
+ if let contentHandler = self.contentHandler {
+ if let content = self.content.with({ $0 }) {
+ contentHandler(content.generate())
+ } else if let initialContent = self.initialContent {
+ contentHandler(initialContent)
+ }
}
}
}