From e3ce6631306d68a0331f02484fed9230750716c8 Mon Sep 17 00:00:00 2001 From: Adam Setch Date: Wed, 29 Oct 2025 21:03:58 -0400 Subject: [PATCH] fix: orphaned notification groupings Signed-off-by: Adam Setch --- .../notifications/AccountNotifications.tsx | 20 ++++------ src/renderer/hooks/useNotifications.test.ts | 2 - src/renderer/hooks/useNotifications.ts | 38 ++++++++++++++----- src/renderer/utils/notifications/group.ts | 9 ++++- 4 files changed, 44 insertions(+), 25 deletions(-) diff --git a/src/renderer/components/notifications/AccountNotifications.tsx b/src/renderer/components/notifications/AccountNotifications.tsx index 5ff75ab7c..5811f505b 100644 --- a/src/renderer/components/notifications/AccountNotifications.tsx +++ b/src/renderer/components/notifications/AccountNotifications.tsx @@ -52,7 +52,7 @@ export const AccountNotifications: FC = ( { account, error, notifications: sortedNotifications }, ]); - return Array.from(map.values()); + return Array.from(map.entries()); }, [account, error, sortedNotifications]); const hasNotifications = useMemo( @@ -131,17 +131,13 @@ export const AccountNotifications: FC = ( {props.error && } {!hasNotifications && !props.error && } {isGroupByRepository(settings) - ? groupedNotifications.map((repoNotifications) => { - const repoSlug = repoNotifications[0].repository.full_name; - - return ( - - ); - }) + ? groupedNotifications.map(([repoSlug, repoNotifications]) => ( + + )) : sortedNotifications.map((notification) => ( { }); expect(result.current.notifications.length).toBe(0); - expect(mockSingleNotification.unread).toBeFalsy(); }); it('should mark notifications as read with failure', async () => { @@ -414,7 +413,6 @@ describe('renderer/hooks/useNotifications.ts', () => { }); expect(result.current.notifications.length).toBe(0); - expect(mockSingleNotification.unread).toBeFalsy(); }); it('should mark notifications as done with failure', async () => { diff --git a/src/renderer/hooks/useNotifications.ts b/src/renderer/hooks/useNotifications.ts index 6540c43db..3eb668701 100644 --- a/src/renderer/hooks/useNotifications.ts +++ b/src/renderer/hooks/useNotifications.ts @@ -24,13 +24,25 @@ import { import { removeNotifications } from '../utils/notifications/remove'; /** - * Apply optimistic local updates for read state. This helps with some - * rendering edge cases between fetch notification intervals. + * Apply read state updates to all notifications, replacing the target notifications + * with new objects that have unread: false. This is used for optimistic UI updates. */ -function markNotificationsAsReadLocally(targetNotifications: Notification[]) { - for (const n of targetNotifications) { - n.unread = false; - } +function applyReadStateToNotifications( + allNotifications: AccountNotifications[], + readNotifications: Notification[], +): AccountNotifications[] { + const readNotificationIDs = new Set( + readNotifications.map((notification) => notification.id), + ); + + return allNotifications.map((accountNotifications) => ({ + ...accountNotifications, + notifications: accountNotifications.notifications.map((notification) => + readNotificationIDs.has(notification.id) + ? { ...notification, unread: false } + : notification, + ), + })); } interface NotificationsState { @@ -129,13 +141,16 @@ export const useNotifications = (): NotificationsState => { ), ); - const updatedNotifications = removeNotifications( + let updatedNotifications = removeNotifications( state.settings, readNotifications, notifications, ); - markNotificationsAsReadLocally(readNotifications); + updatedNotifications = applyReadStateToNotifications( + updatedNotifications, + readNotifications, + ); setNotifications(updatedNotifications); setTrayIconColor(updatedNotifications); @@ -171,13 +186,16 @@ export const useNotifications = (): NotificationsState => { ), ); - const updatedNotifications = removeNotifications( + let updatedNotifications = removeNotifications( state.settings, doneNotifications, notifications, ); - markNotificationsAsReadLocally(doneNotifications); + updatedNotifications = applyReadStateToNotifications( + updatedNotifications, + doneNotifications, + ); setNotifications(updatedNotifications); setTrayIconColor(updatedNotifications); diff --git a/src/renderer/utils/notifications/group.ts b/src/renderer/utils/notifications/group.ts index 0c118071a..68571f89a 100644 --- a/src/renderer/utils/notifications/group.ts +++ b/src/renderer/utils/notifications/group.ts @@ -11,6 +11,7 @@ export function isGroupByRepository(settings: SettingsState) { /** * Group notifications by repository.full_name preserving first-seen repo order. * Returns a Map where keys are repo full_names and values are arrays of notifications. + * Skips notifications without valid repository data. */ export function groupNotificationsByRepository( accounts: AccountNotifications[], @@ -19,7 +20,13 @@ export function groupNotificationsByRepository( for (const account of accounts) { for (const notification of account.notifications) { - const repo = notification.repository?.full_name ?? ''; + const repo = notification.repository?.full_name; + + // Skip notifications without valid repository data + if (!repo) { + continue; + } + const group = repoGroups.get(repo); if (group) {