eleven / messenger Public

Clone
git clone https://amadan.net/eleven/messenger

Plain git — no account needed to clone.

Download

Download this file

1import Foundation
2import Intents
3import UserNotifications
4
5// Re-presents a message banner as a system "communication notification" — the
6// sender's face as the big icon with the app icon badged on its corner, like
7// Messages/WhatsApp — by describing the message to iOS as an INSendMessageIntent
8// and letting UNNotificationContent.updating(from:) restyle the content. Shared
9// by the iOS notification extension (APNs path) and the Mac's local banners so
10// the two platforms can't drift. Purely cosmetic + best-effort: any failure
11// (no communication entitlement on this build, malformed image) returns nil and
12// the caller ships the plain banner it already built.
13public enum CommunicationNotification {
14 /// The decorated content, or nil to keep the caller's plain banner.
15 /// - senderUID keys the avatar lookup (SharedAvatarCache) and gives the
16 /// system a stable per-person identity; without one the sender still gets
17 /// named, just with a monogram circle.
18 /// - groupName (a group chat's title) renders as the subtitle line; pass
19 /// nil for DMs, where the sender name alone is the title.
20 /// - messageText is the plain body — no "Sender: " prefix; the intent
21 /// carries the sender, so a prefixed body would read twice.
22 public static func decorate(_ content: UNNotificationContent,
23 senderName: String, senderUID: String?,
24 origin: String, conversationID: String,
25 groupName: String?, messageText: String) -> UNNotificationContent? {
26 // The sender's cached photo, else the same initials-on-colour monogram
27 // the app draws for them — never the system's gray silhouette.
28 var image: INImage?
29 if let uid = senderUID, let data = SharedAvatarCache.imageData(origin: origin, uid: uid) {
30 image = INImage(imageData: data)
31 } else if let data = MonogramImage.pngData(name: senderName) {
32 image = INImage(imageData: data)
33 }
34 // The handle/customIdentifier scope the person to their instance —
35 // the same uid on two instances is two different people.
36 let handle = INPersonHandle(value: "\(origin)|\(senderUID ?? senderName)", type: .unknown)
37 let sender = INPerson(personHandle: handle, nameComponents: nil,
38 displayName: senderName, image: image,
39 contactIdentifier: nil,
40 customIdentifier: handle.value)
41 let intent = INSendMessageIntent(recipients: nil,
42 outgoingMessageType: .outgoingMessageText,
43 content: messageText,
44 speakableGroupName: groupName.map { INSpeakableString(spokenPhrase: $0) },
45 conversationIdentifier: conversationID,
46 serviceName: nil,
47 sender: sender,
48 attachments: nil)
49 // The donation is what feeds share-sheet/Siri people suggestions;
50 // the visual restyle below works without it, so ignore failures.
51 let interaction = INInteraction(intent: intent, response: nil)
52 interaction.direction = .incoming
53 interaction.donate(completion: nil)
54
55 guard let mutable = content.mutableCopy() as? UNMutableNotificationContent else { return nil }
56 // updating(from:) takes title/subtitle from the intent's sender/group;
57 // the body stays ours, so strip any sender prefix down to the text.
58 mutable.body = messageText
59 return try? mutable.updating(from: intent)
60 }
61}
62