add new notification types

This commit is contained in:
Samuel Newman
2026-05-18 19:32:37 +03:00
parent 8e8dc7561f
commit 826a0e6d02
3 changed files with 110 additions and 19 deletions
+49 -1
View File
@@ -44,11 +44,20 @@ class NotificationService: UNNotificationServiceExtension {
if reason == "chat-message" || reason == "chat-reaction" {
mutateWithChatMessage(bestAttempt)
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,11 +96,18 @@ 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,
@@ -157,6 +173,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
}
+18 -8
View File
@@ -22,8 +22,9 @@ import {useAccountSwitcher} from '#/lib/hooks/useAccountSwitcher'
import {useColorSchemeStyle} from '#/lib/hooks/useColorSchemeStyle'
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
import {
type ChatNotificationPayload,
getNotificationPayload,
type NotificationPayload,
isChatNotificationPayload,
notificationToURL,
storePayloadForAccountSwitch,
} from '#/lib/hooks/useNotificationHandler'
@@ -926,14 +927,14 @@ function RoutesContainer({children}: React.PropsWithChildren<{}>) {
const linkingUrl = Linking.useLinkingURL()
/**
* Handle navigation to a conversation, or prepares for account switch.
* Handle navigation to the messages tab, or prepares for account switch.
*
* Non-reactive because we need the latest data from some hooks
* after an async call - sfn
*/
const handleChatMessage = useNonReactiveCallback(
(payload: Extract<NotificationPayload, {reason: 'chat-message'}>) => {
notyLogger.debug(`handleChatMessage`, {payload})
const handleChatNotification = useNonReactiveCallback(
(payload: ChatNotificationPayload) => {
notyLogger.debug(`handleChatNotification`, {payload})
if (payload.recipientDid !== currentAccount?.did) {
// handled in useNotificationHandler after account switch finishes
@@ -946,7 +947,10 @@ function RoutesContainer({children}: React.PropsWithChildren<{}>) {
} else {
setShowLoggedOut(true)
}
} else {
} else if (
payload.reason === 'chat-message' ||
payload.reason === 'chat-reaction'
) {
// @ts-expect-error nested navigators aren't typed -sfn
navigate('MessagesTab', {
screen: 'Messages',
@@ -954,6 +958,12 @@ function RoutesContainer({children}: React.PropsWithChildren<{}>) {
pushToConversation: payload.convoId,
},
})
} else {
// chat-added-to-group, chat-removed-from-group,
// chat-join-request-rejected: open the Messages list without
// targeting a specific conversation
// @ts-expect-error nested navigators aren't typed -sfn
navigate('MessagesTab', {screen: 'Messages'})
}
},
)
@@ -989,8 +999,8 @@ function RoutesContainer({children}: React.PropsWithChildren<{}>) {
causedBoot: true,
})
if (payload.reason === 'chat-message') {
handleChatMessage(payload)
if (isChatNotificationPayload(payload)) {
handleChatNotification(payload)
} else {
const path = notificationToURL(payload)
+43 -10
View File
@@ -30,6 +30,9 @@ export type NotificationReason =
| 'quote'
| 'chat-message'
| 'chat-reaction'
| 'chat-added-to-group'
| 'chat-removed-from-group'
| 'chat-join-request-rejected'
| 'starterpack-joined'
| 'like-via-repost'
| 'repost-via-repost'
@@ -37,6 +40,8 @@ export type NotificationReason =
| 'unverified'
| 'subscribed-post'
type ChatNotificationReason = Extract<NotificationReason, `chat-${string}`>
/**
* Manually overridden type, but retains the possibility of
* `notification.request.trigger.payload` being `undefined`, as specified in
@@ -45,7 +50,7 @@ export type NotificationReason =
export type NotificationPayload =
| undefined
| {
reason: Exclude<NotificationReason, 'chat-message' | 'chat-reaction'>
reason: Exclude<NotificationReason, ChatNotificationReason>
uri: string
subject: string
recipientDid: string
@@ -62,6 +67,25 @@ export type NotificationPayload =
messageId: string
recipientDid: string
}
| {
reason:
| 'chat-added-to-group'
| 'chat-removed-from-group'
| 'chat-join-request-rejected'
convoId: string
recipientDid: string
}
export type ChatNotificationPayload = Extract<
NonNullable<NotificationPayload>,
{reason: ChatNotificationReason}
>
export function isChatNotificationPayload(
payload: NonNullable<NotificationPayload>,
): payload is ChatNotificationPayload {
return payload.reason.startsWith('chat-')
}
const DEFAULT_HANDLER_OPTIONS = {
shouldShowBanner: false,
@@ -199,10 +223,7 @@ export function useNotificationsHandler() {
const handleNotification = (payload?: NotificationPayload) => {
if (!payload) return
if (
payload.reason === 'chat-message' ||
payload.reason === 'chat-reaction'
) {
if (isChatNotificationPayload(payload)) {
logger.debug(`useNotificationsHandler: handling chat notification`, {
payload,
})
@@ -220,7 +241,10 @@ export function useNotificationsHandler() {
} else {
setShowLoggedOut(true)
}
} else {
} else if (
payload.reason === 'chat-message' ||
payload.reason === 'chat-reaction'
) {
navigation.dispatch(state => {
if (state.routes[0].name === 'Messages') {
if (
@@ -253,6 +277,13 @@ export function useNotificationsHandler() {
})
}
})
} else {
// chat-added-to-group, chat-removed-from-group,
// chat-join-request-rejected: open the Messages list without
// targeting a specific conversation
navigation.dispatch(
CommonActions.navigate('MessagesTab', {screen: 'Messages'}),
)
}
} else {
const url = notificationToURL(payload)
@@ -280,8 +311,7 @@ export function useNotificationsHandler() {
logger.debug('useNotificationsHandler: incoming', {e, payload})
if (
(payload.reason === 'chat-message' ||
payload.reason === 'chat-reaction') &&
isChatNotificationPayload(payload) &&
payload.recipientDid === currentAccount?.did
) {
const shouldAlert = payload.convoId !== currentConvoId
@@ -352,8 +382,8 @@ export function useNotificationsHandler() {
// Whenever there's a stored payload, that means we had to switch accounts before handling the notification.
// Whenever currentAccount changes, we should try to handle it again.
if (
(storedAccountSwitchPayload?.reason === 'chat-message' ||
storedAccountSwitchPayload?.reason === 'chat-reaction') &&
storedAccountSwitchPayload &&
isChatNotificationPayload(storedAccountSwitchPayload) &&
currentAccount?.did === storedAccountSwitchPayload.recipientDid
) {
handleNotification(storedAccountSwitchPayload)
@@ -442,6 +472,9 @@ export function notificationToURL(payload: NotificationPayload): string | null {
}
case 'chat-message':
case 'chat-reaction':
case 'chat-added-to-group':
case 'chat-removed-from-group':
case 'chat-join-request-rejected':
// should be handled separately
return null
case 'verified':