Skip to content

Commit 3c77479

Browse files
authored
fix: orphaned notification groupings (#2337)
Signed-off-by: Adam Setch <adam.setch@outlook.com>
1 parent cd9d3c8 commit 3c77479

File tree

4 files changed

+44
-25
lines changed

4 files changed

+44
-25
lines changed

src/renderer/components/notifications/AccountNotifications.tsx

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ export const AccountNotifications: FC<IAccountNotifications> = (
5252
{ account, error, notifications: sortedNotifications },
5353
]);
5454

55-
return Array.from(map.values());
55+
return Array.from(map.entries());
5656
}, [account, error, sortedNotifications]);
5757

5858
const hasNotifications = useMemo(
@@ -131,17 +131,13 @@ export const AccountNotifications: FC<IAccountNotifications> = (
131131
{props.error && <Oops error={props.error} fullHeight={false} />}
132132
{!hasNotifications && !props.error && <AllRead fullHeight={false} />}
133133
{isGroupByRepository(settings)
134-
? groupedNotifications.map((repoNotifications) => {
135-
const repoSlug = repoNotifications[0].repository.full_name;
136-
137-
return (
138-
<RepositoryNotifications
139-
key={repoSlug}
140-
repoName={repoSlug}
141-
repoNotifications={repoNotifications}
142-
/>
143-
);
144-
})
134+
? groupedNotifications.map(([repoSlug, repoNotifications]) => (
135+
<RepositoryNotifications
136+
key={repoSlug}
137+
repoName={repoSlug}
138+
repoNotifications={repoNotifications}
139+
/>
140+
))
145141
: sortedNotifications.map((notification) => (
146142
<NotificationRow
147143
key={notification.id}

src/renderer/hooks/useNotifications.test.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -370,7 +370,6 @@ describe('renderer/hooks/useNotifications.ts', () => {
370370
});
371371

372372
expect(result.current.notifications.length).toBe(0);
373-
expect(mockSingleNotification.unread).toBeFalsy();
374373
});
375374

376375
it('should mark notifications as read with failure', async () => {
@@ -414,7 +413,6 @@ describe('renderer/hooks/useNotifications.ts', () => {
414413
});
415414

416415
expect(result.current.notifications.length).toBe(0);
417-
expect(mockSingleNotification.unread).toBeFalsy();
418416
});
419417

420418
it('should mark notifications as done with failure', async () => {

src/renderer/hooks/useNotifications.ts

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,25 @@ import {
2424
import { removeNotifications } from '../utils/notifications/remove';
2525

2626
/**
27-
* Apply optimistic local updates for read state. This helps with some
28-
* rendering edge cases between fetch notification intervals.
27+
* Apply read state updates to all notifications, replacing the target notifications
28+
* with new objects that have unread: false. This is used for optimistic UI updates.
2929
*/
30-
function markNotificationsAsReadLocally(targetNotifications: Notification[]) {
31-
for (const n of targetNotifications) {
32-
n.unread = false;
33-
}
30+
function applyReadStateToNotifications(
31+
allNotifications: AccountNotifications[],
32+
readNotifications: Notification[],
33+
): AccountNotifications[] {
34+
const readNotificationIDs = new Set<string>(
35+
readNotifications.map((notification) => notification.id),
36+
);
37+
38+
return allNotifications.map((accountNotifications) => ({
39+
...accountNotifications,
40+
notifications: accountNotifications.notifications.map((notification) =>
41+
readNotificationIDs.has(notification.id)
42+
? { ...notification, unread: false }
43+
: notification,
44+
),
45+
}));
3446
}
3547

3648
interface NotificationsState {
@@ -129,13 +141,16 @@ export const useNotifications = (): NotificationsState => {
129141
),
130142
);
131143

132-
const updatedNotifications = removeNotifications(
144+
let updatedNotifications = removeNotifications(
133145
state.settings,
134146
readNotifications,
135147
notifications,
136148
);
137149

138-
markNotificationsAsReadLocally(readNotifications);
150+
updatedNotifications = applyReadStateToNotifications(
151+
updatedNotifications,
152+
readNotifications,
153+
);
139154

140155
setNotifications(updatedNotifications);
141156
setTrayIconColor(updatedNotifications);
@@ -171,13 +186,16 @@ export const useNotifications = (): NotificationsState => {
171186
),
172187
);
173188

174-
const updatedNotifications = removeNotifications(
189+
let updatedNotifications = removeNotifications(
175190
state.settings,
176191
doneNotifications,
177192
notifications,
178193
);
179194

180-
markNotificationsAsReadLocally(doneNotifications);
195+
updatedNotifications = applyReadStateToNotifications(
196+
updatedNotifications,
197+
doneNotifications,
198+
);
181199

182200
setNotifications(updatedNotifications);
183201
setTrayIconColor(updatedNotifications);

src/renderer/utils/notifications/group.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export function isGroupByRepository(settings: SettingsState) {
1111
/**
1212
* Group notifications by repository.full_name preserving first-seen repo order.
1313
* Returns a Map where keys are repo full_names and values are arrays of notifications.
14+
* Skips notifications without valid repository data.
1415
*/
1516
export function groupNotificationsByRepository(
1617
accounts: AccountNotifications[],
@@ -19,7 +20,13 @@ export function groupNotificationsByRepository(
1920

2021
for (const account of accounts) {
2122
for (const notification of account.notifications) {
22-
const repo = notification.repository?.full_name ?? '';
23+
const repo = notification.repository?.full_name;
24+
25+
// Skip notifications without valid repository data
26+
if (!repo) {
27+
continue;
28+
}
29+
2330
const group = repoGroups.get(repo);
2431

2532
if (group) {

0 commit comments

Comments
 (0)