diff --git a/.changeset/fix-notification-room-titles.md b/.changeset/fix-notification-room-titles.md new file mode 100644 index 000000000..a46467735 --- /dev/null +++ b/.changeset/fix-notification-room-titles.md @@ -0,0 +1,5 @@ +--- +default: patch +--- + +Preserve room names and sender details in push notifications. diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index efb186ea0..1dff1de12 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -8325,7 +8325,7 @@ dependencies = [ [[package]] name = "tauri-plugin-notifications" version = "0.5.0" -source = "git+https://github.com/SableClient/tauri-plugin-notifications.git?rev=f2f320f12a5904ece6d648ce551f9d56bb4a9175#f2f320f12a5904ece6d648ce551f9d56bb4a9175" +source = "git+https://github.com/SableClient/tauri-plugin-notifications.git?rev=88c798eaa16fb4a1210fd1c6107e6e93420125eb#88c798eaa16fb4a1210fd1c6107e6e93420125eb" dependencies = [ "log", "notify-rust", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 40d36bdaa..698af6ad8 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -110,12 +110,12 @@ windows = { version = "0.62", features = [ tauri-plugin-single-instance = { version = "2.4.3", features = ["deep-link"] } [target.'cfg(any(windows, target_os = "linux"))'.dependencies] -tauri-plugin-notifications = { git = "https://github.com/SableClient/tauri-plugin-notifications.git", rev = "f2f320f12a5904ece6d648ce551f9d56bb4a9175" } +tauri-plugin-notifications = { git = "https://github.com/SableClient/tauri-plugin-notifications.git", rev = "88c798eaa16fb4a1210fd1c6107e6e93420125eb" } # default-features = false drops notify-rust so macOS uses the native # UNUserNotificationCenter backend (needs a signed .app to deliver). [target.'cfg(target_os = "macos")'.dependencies] -tauri-plugin-notifications = { git = "https://github.com/SableClient/tauri-plugin-notifications.git", rev = "f2f320f12a5904ece6d648ce551f9d56bb4a9175", default-features = false } +tauri-plugin-notifications = { git = "https://github.com/SableClient/tauri-plugin-notifications.git", rev = "88c798eaa16fb4a1210fd1c6107e6e93420125eb", default-features = false } [target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies] tauri-plugin-updater = { version = "2", optional = true } @@ -139,7 +139,7 @@ libloading = "0.9" zbus = "5" [target.'cfg(any(target_os = "android", target_os = "ios"))'.dependencies] -tauri-plugin-notifications = { git = "https://github.com/SableClient/tauri-plugin-notifications.git", rev = "f2f320f12a5904ece6d648ce551f9d56bb4a9175", features = [ +tauri-plugin-notifications = { git = "https://github.com/SableClient/tauri-plugin-notifications.git", rev = "88c798eaa16fb4a1210fd1c6107e6e93420125eb", features = [ "push-notifications", ] } tauri-plugin-edge-to-edge = { git = "https://github.com/SableClient/tauri-plugin-edge-to-edge.git", rev = "33c6116c27be28c06df5a9d02231ecc5fdeb93c5" } diff --git a/src/app/features/settings/notifications/UnifiedPushNotifications.test.ts b/src/app/features/settings/notifications/UnifiedPushNotifications.test.ts index e3fa2f6fe..7946e64eb 100644 --- a/src/app/features/settings/notifications/UnifiedPushNotifications.test.ts +++ b/src/app/features/settings/notifications/UnifiedPushNotifications.test.ts @@ -250,6 +250,28 @@ describe('UnifiedPushNotifications', () => { ); }); + it.each(['m.room.encrypted', 'm.room.message'])( + 'uses the cached room name when a %s push omits it', + async (type) => { + matrixClient.getRoom.mockReturnValue(makeRoom()); + await listenAndPush( + { + ...encryptedPush('$room-title'), + type, + room_name: undefined, + sender_display_name: 'Alice', + }, + makeSettings({ showEncryptedMessageContent: false }) + ); + + await vi.waitFor(() => + expect(notificationsApi.sendNotification).toHaveBeenCalledWith( + expect.objectContaining({ title: 'Room' }) + ) + ); + } + ); + it('posts an encrypted baseline before a hanging local decryption completes', async () => { matrixClient.getRoom.mockReturnValue(makeRoom()); let resolveDecryption!: (content: Record) => void; @@ -282,7 +304,7 @@ describe('UnifiedPushNotifications', () => { await vi.waitFor(() => expect(notificationsApi.sendNotification).toHaveBeenCalledTimes(2)); }); - it('posts message notifications as a conversation on the high-importance channel', async () => { + it('keeps the room header for a named two-member conversation', async () => { matrixClient.getRoom.mockReturnValue(makeRoom()); await listenAndPush({ @@ -298,11 +320,68 @@ describe('UnifiedPushNotifications', () => { await vi.waitFor(() => expect(notificationsApi.sendNotification).toHaveBeenCalledOnce()); expect(notificationsApi.sendNotification.mock.calls[0]?.[0]).toMatchObject({ channelId: 'messages.v2', - groupConversation: false, + groupConversation: true, messages: [{ body: 'hello', senderName: 'Alice', senderKey: '@alice:example.com' }], }); }); + it('keeps the sender identity when a rich push omits its display name', async () => { + matrixClient.getRoom.mockReturnValue(makeRoom()); + await listenAndPush( + { + ...encryptedPush('$sender'), + sender: '@alice:example.com', + }, + makeSettings({ showEncryptedMessageContent: false }) + ); + + await vi.waitFor(() => + expect(notificationsApi.sendNotification).toHaveBeenCalledWith( + expect.objectContaining({ + title: 'Room', + body: 'alice: Encrypted message', + extra: { + user_id: '@user:example.com', + room_id: '!room:example.com', + event_id: '$sender', + }, + messages: [ + expect.objectContaining({ senderName: 'alice', senderKey: '@alice:example.com' }), + ], + }) + ) + ); + }); + + it('keeps room and sender data from an event-ID payload', async () => { + await listenAndPush( + { + room_id: '!minimal:example.com', + event_id: '$minimal', + room_name: 'Project', + sender_display_name: 'Alice', + sender: '@alice:example.com', + }, + makeSettings({ showMessageContent: false }) + ); + + await vi.waitFor(() => + expect(notificationsApi.sendNotification).toHaveBeenCalledWith( + expect.objectContaining({ + title: 'Project', + messages: [ + expect.objectContaining({ senderName: 'Alice', senderKey: '@alice:example.com' }), + ], + extra: { + user_id: '@user:example.com', + room_id: '!minimal:example.com', + event_id: '$minimal', + }, + }) + ) + ); + }); + it('posts invitations on their own channel', async () => { await listenAndPush({ type: 'm.room.member', diff --git a/src/app/features/settings/notifications/UnifiedPushNotifications.ts b/src/app/features/settings/notifications/UnifiedPushNotifications.ts index b77bc1e24..10fb544fa 100644 --- a/src/app/features/settings/notifications/UnifiedPushNotifications.ts +++ b/src/app/features/settings/notifications/UnifiedPushNotifications.ts @@ -673,10 +673,15 @@ async function handleRichPushPayload( }); const roomId: string | undefined = pushData?.room_id; + const currentRoom = roomId ? settings.mx.getRoom(roomId) : undefined; const roomName: string = - pushData?.room_name ?? pushData?.sender_display_name ?? 'Unknown Room'; - const senderName: string | undefined = pushData?.sender_display_name; + pushData?.room_name || currentRoom?.name || pushData?.sender_display_name || 'Unknown Room'; const senderId: string | undefined = pushData?.sender; + const senderName = + pushData?.sender_display_name || + (senderId + ? currentRoom?.getMember(senderId)?.name || getMxIdLocalPart(senderId) || senderId + : undefined); const isSilent = !settings.notificationSoundEnabled; if (!roomId) { @@ -748,10 +753,9 @@ async function handleRichPushPayload( cache.messages = cache.messages.slice(-MAX_MESSAGES); } - const currentRoom = settings.mx.getRoom(roomId); - if (currentRoom) { - cache.isGroupConversation = (currentRoom.getJoinedMemberCount() ?? 0) > 2; - } + cache.isGroupConversation = + Boolean(pushData?.room_name || currentRoom?.name) || + (currentRoom?.getJoinedMemberCount() ?? 0) > 2; try { await postRoomNotification(userId, roomId, cache, isSilent, { @@ -966,11 +970,16 @@ async function handleMinimalPushPayload( } const room = settings.mx.getRoom(roomId); - const roomName = room?.name ?? pushData?.sender_display_name ?? 'Unknown Room'; + const roomName = + room?.name || pushData?.room_name || pushData?.sender_display_name || 'Unknown Room'; const isEncryptedRoom = room ? !!getStateEvent(room, EventType.RoomEncryption) : false; - let senderName: string | undefined; - let senderId: string | undefined; + let senderId = pushData?.sender; + let senderName = + pushData?.sender_display_name || + (senderId + ? room?.getMember(senderId)?.name || getMxIdLocalPart(senderId) || senderId + : undefined); let previewText: string | undefined; let inMemoryStillEncrypted = false; if (room && eventId) { @@ -1023,9 +1032,8 @@ async function handleMinimalPushPayload( cache.messages = cache.messages.slice(-MAX_MESSAGES); } - if (room) { - cache.isGroupConversation = (room.getJoinedMemberCount() ?? 0) > 2; - } + cache.isGroupConversation = + Boolean(pushData?.room_name || room?.name) || (room?.getJoinedMemberCount() ?? 0) > 2; try { await postRoomNotification(userId, roomId, cache, !settings.notificationSoundEnabled, {