Files
buzz/mobile/ios/BuzzPushKit/Sources/BuzzPushKit/BuzzCommunicationNotification.swift
Tom Brow 7d6efa9fb5 fix(mobile): show avatars in channel push notifications
Signed-off-by: Tom Brow <tomb@block.xyz>
Co-authored-by: Codex <noreply@openai.com>
Ai-assisted: true
2026-08-24 14:02:32 -07:00

155 lines
5.3 KiB
Swift

import Foundation
/// Verified local values used to specialize an ordinary notification as communication.
public struct BuzzCommunicationNotificationDescriptor: Equatable, Sendable {
public let senderDisplayName: String
public let senderIdentifier: String
public let senderAvatarPNG: Data?
public let messageBody: String
public let conversationIdentifier: String
public let conversationDisplayName: String?
/// Verified recipients represented by the incoming message, excluding its sender.
public let recipientCount: Int
public init(
senderDisplayName: String,
senderIdentifier: String,
senderAvatarPNG: Data?,
messageBody: String,
conversationIdentifier: String,
conversationDisplayName: String?,
recipientCount: Int
) {
self.senderDisplayName = senderDisplayName
self.senderIdentifier = senderIdentifier
self.senderAvatarPNG = senderAvatarPNG
self.messageBody = messageBody
self.conversationIdentifier = conversationIdentifier
self.conversationDisplayName = conversationDisplayName
self.recipientCount = recipientCount
}
public init?(resolution: BuzzPushResolution) {
guard let target = resolution.navigationTarget,
let senderPubkey = resolution.senderPubkey,
!senderPubkey.isEmpty,
let conversationIdentifier = resolution.conversationIdentifier,
!conversationIdentifier.isEmpty,
let recipientCount = resolution.conversationRecipientCount,
recipientCount > 0
else { return nil }
self.init(
senderDisplayName: resolution.title,
senderIdentifier: BuzzPushPresentationIdentity.sender(
communityID: target.communityID,
pubkey: senderPubkey
),
senderAvatarPNG: resolution.senderAvatarPNG,
messageBody: resolution.body,
conversationIdentifier: conversationIdentifier,
conversationDisplayName: resolution.conversationDisplayName,
recipientCount: recipientCount
)
}
}
#if os(iOS)
import Intents
import UserNotifications
/// Donates and applies Apple's supported Communication Notifications intent.
public final class BuzzCommunicationNotificationPresenter {
public typealias Donation = (INInteraction, @escaping (Error?) -> Void) -> Void
public typealias ContentUpdate = (
UNMutableNotificationContent,
INSendMessageIntent
) throws -> UNNotificationContent
private let donate: Donation
private let updateContent: ContentUpdate
public convenience init() {
self.init(
donate: { interaction, completion in
interaction.donate(completion: completion)
},
updateContent: { content, intent in
try content.updating(from: intent)
}
)
}
public init(
donate: @escaping Donation,
updateContent: @escaping ContentUpdate
) {
self.donate = donate
self.updateContent = updateContent
}
public func present(
ordinaryContent: UNMutableNotificationContent,
resolution: BuzzPushResolution,
completion: @escaping (UNNotificationContent) -> Void
) {
guard let descriptor = BuzzCommunicationNotificationDescriptor(resolution: resolution) else {
completion(ordinaryContent)
return
}
let intent = Self.makeIntent(descriptor)
let interaction = INInteraction(intent: intent, response: nil)
interaction.direction = .incoming
donate(interaction) { [updateContent] error in
guard error == nil,
let specialized = try? updateContent(ordinaryContent, intent)
else {
completion(ordinaryContent)
return
}
completion(specialized)
}
}
public static func makeIntent(
_ descriptor: BuzzCommunicationNotificationDescriptor
) -> INSendMessageIntent {
let senderAvatar = descriptor.senderAvatarPNG.map(INImage.init(imageData:))
let sender = INPerson(
personHandle: INPersonHandle(value: descriptor.senderIdentifier, type: .unknown),
nameComponents: nil,
displayName: descriptor.senderDisplayName,
image: senderAvatar,
contactIdentifier: nil,
customIdentifier: descriptor.senderIdentifier,
isMe: false,
suggestionType: .none
)
let intent = INSendMessageIntent(
recipients: nil,
outgoingMessageType: .outgoingMessageText,
content: descriptor.messageBody,
speakableGroupName: descriptor.conversationDisplayName.map {
INSpeakableString(spokenPhrase: $0)
},
conversationIdentifier: descriptor.conversationIdentifier,
serviceName: "Buzz",
sender: sender,
attachments: nil
)
if descriptor.conversationDisplayName != nil {
let donationMetadata = INSendMessageIntentDonationMetadata()
donationMetadata.recipientCount = descriptor.recipientCount
intent.donationMetadata = donationMetadata
if let senderAvatar {
// Communication Notifications render a group conversation's image
// from the speakable-group parameter rather than INPerson.image.
// Buzz channels do not have a separate avatar, so use the verified
// sender thumbnail for the visible incoming-message avatar.
intent.setImage(senderAvatar, forParameterNamed: \.speakableGroupName)
}
}
return intent
}
}
#endif