[Chat] Handle new chat notification reasons (#10530)

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Samuel Newman
2026-05-20 18:20:35 +03:00
committed by GitHub
parent 9cad883ab8
commit c1817d3425
3 changed files with 136 additions and 20 deletions
+64 -1
View File
@@ -44,11 +44,26 @@ class NotificationService: UNNotificationServiceExtension {
if reason == "chat-message" || reason == "chat-reaction" {
mutateWithChatMessage(bestAttempt)
// Only apply the title->body swap for chat-message. For chat-reaction,
// `messageKind` refers to the reacted-to message, so we'd clobber the
// descriptive reaction body with the title for reactions on system
// messages.
if reason == "chat-message" {
mutateChatMessageBody(bestAttempt, userInfo: request.content.userInfo)
}
mutateWithGroupSubtitle(bestAttempt, userInfo: request.content.userInfo)
let finalContent = createCommunicationNotification(
from: bestAttempt,
userInfo: request.content.userInfo
)
contentHandler(finalContent)
} else if reason == "chat-added-to-group"
|| reason == "chat-removed-from-group"
|| reason == "chat-join-request-rejected" {
mutateWithChatMessage(bestAttempt)
mutateWithGroupSubtitle(bestAttempt, userInfo: request.content.userInfo)
mutateWithBadge(bestAttempt)
contentHandler(bestAttempt)
} else {
mutateWithBadge(bestAttempt)
contentHandler(bestAttempt)
@@ -87,17 +102,33 @@ class NotificationService: UNNotificationServiceExtension {
customIdentifier: nil
)
var speakableGroupName: INSpeakableString? = nil
if userInfo["convoKind"] as? String == "group",
let groupName = userInfo["convoGroupName"] as? String,
!groupName.isEmpty {
speakableGroupName = INSpeakableString(spokenPhrase: groupName)
}
let intent = INSendMessageIntent(
recipients: nil,
outgoingMessageType: .outgoingMessageText,
content: content.body,
speakableGroupName: nil,
speakableGroupName: speakableGroupName,
conversationIdentifier: convoId,
serviceName: nil,
sender: sender,
attachments: nil
)
// For group convos, attach the composite group avatar (rendered by the
// ogcard service) to the `speakableGroupName` parameter so iOS shows it
// alongside the sender on the Communication Notification.
if userInfo["convoKind"] as? String == "group",
let convoAvatarUrlString = userInfo["convoAvatarUrl"] as? String,
let groupImage = downloadAvatarImage(from: convoAvatarUrlString) {
intent.setImage(groupImage, forParameterNamed: \.speakableGroupName)
}
let interaction = INInteraction(intent: intent, response: nil)
interaction.direction = .incoming
interaction.donate(completion: nil)
@@ -157,6 +188,38 @@ class NotificationService: UNNotificationServiceExtension {
}
}
// For group convos, surface the group name as the notification subtitle.
// The sender's display name is shown as the title (overridden by
// `INSendMessageIntent` for chat-message/chat-reaction).
func mutateWithGroupSubtitle(
_ content: UNMutableNotificationContent,
userInfo: [AnyHashable: Any]
) {
guard userInfo["convoKind"] as? String == "group",
let groupName = userInfo["convoGroupName"] as? String,
!groupName.isEmpty else {
return
}
content.subtitle = groupName
}
// System messages (`add_member`, `convo_locked`, `edit_group`, etc.) are
// delivered through `chat-message` but have a server-rendered description
// in `title`. iOS overrides the title with the sender name once we apply
// `INSendMessageIntent`, so we move the title text into the body before
// building the intent.
func mutateChatMessageBody(
_ content: UNMutableNotificationContent,
userInfo: [AnyHashable: Any]
) {
guard let messageKind = userInfo["messageKind"] as? String,
messageKind != "message",
!content.title.isEmpty else {
return
}
content.body = content.title
}
func mutateWithDefaultSound(_ content: UNMutableNotificationContent) {
content.sound = UNNotificationSound.default
}