Skip to content

Commit c1ad7a5

Browse files
authored
feat: group notifications by repository or date (#1273)
1 parent da0e94f commit c1ad7a5

File tree

13 files changed

+1899
-26
lines changed

13 files changed

+1899
-26
lines changed

src/__mocks__/state-mocks.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {
33
type AuthState,
44
type GitifyState,
55
type GitifyUser,
6+
GroupBy,
67
type Hostname,
78
type SettingsState,
89
Theme,
@@ -84,6 +85,7 @@ export const mockSettings: SettingsState = {
8485
delayNotificationState: false,
8586
showPills: true,
8687
keyboardShortcut: true,
88+
groupBy: GroupBy.REPOSITORY,
8789
};
8890

8991
export const mockState: GitifyState = {

src/components/AccountNotifications.test.tsx

Lines changed: 48 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { act, fireEvent, render, screen } from '@testing-library/react';
2-
import { mockGitHubCloudAccount } from '../__mocks__/state-mocks';
2+
import { mockGitHubCloudAccount, mockSettings } from '../__mocks__/state-mocks';
3+
import { AppContext } from '../context/App';
4+
import { GroupBy } from '../types';
35
import { mockGitHubNotifications } from '../utils/api/__mocks__/response-mocks';
46
import * as links from '../utils/links';
57
import { AccountNotifications } from './AccountNotifications';
@@ -9,14 +11,37 @@ jest.mock('./RepositoryNotifications', () => ({
911
}));
1012

1113
describe('components/AccountNotifications.tsx', () => {
12-
it('should render itself (github.com with notifications)', () => {
14+
it('should render itself (github.com with notifications) - group by repositories', () => {
1315
const props = {
1416
account: mockGitHubCloudAccount,
1517
notifications: mockGitHubNotifications,
1618
showAccountHostname: true,
1719
};
1820

19-
const tree = render(<AccountNotifications {...props} />);
21+
const tree = render(
22+
<AppContext.Provider
23+
value={{ settings: { ...mockSettings, groupBy: GroupBy.REPOSITORY } }}
24+
>
25+
<AccountNotifications {...props} />
26+
</AppContext.Provider>,
27+
);
28+
expect(tree).toMatchSnapshot();
29+
});
30+
31+
it('should render itself (github.com with notifications) - group by date', () => {
32+
const props = {
33+
account: mockGitHubCloudAccount,
34+
notifications: mockGitHubNotifications,
35+
showAccountHostname: true,
36+
};
37+
38+
const tree = render(
39+
<AppContext.Provider
40+
value={{ settings: { ...mockSettings, groupBy: GroupBy.DATE } }}
41+
>
42+
<AccountNotifications {...props} />
43+
</AppContext.Provider>,
44+
);
2045
expect(tree).toMatchSnapshot();
2146
});
2247

@@ -27,7 +52,11 @@ describe('components/AccountNotifications.tsx', () => {
2752
showAccountHostname: true,
2853
};
2954

30-
const tree = render(<AccountNotifications {...props} />);
55+
const tree = render(
56+
<AppContext.Provider value={{ settings: mockSettings }}>
57+
<AccountNotifications {...props} />
58+
</AppContext.Provider>,
59+
);
3160
expect(tree).toMatchSnapshot();
3261
});
3362

@@ -41,7 +70,11 @@ describe('components/AccountNotifications.tsx', () => {
4170
};
4271

4372
await act(async () => {
44-
render(<AccountNotifications {...props} />);
73+
render(
74+
<AppContext.Provider value={{ settings: mockSettings }}>
75+
<AccountNotifications {...props} />
76+
</AppContext.Provider>,
77+
);
4578
});
4679

4780
fireEvent.click(screen.getByTitle('Open Profile'));
@@ -58,12 +91,20 @@ describe('components/AccountNotifications.tsx', () => {
5891
};
5992

6093
await act(async () => {
61-
render(<AccountNotifications {...props} />);
94+
render(
95+
<AppContext.Provider value={{ settings: mockSettings }}>
96+
<AccountNotifications {...props} />
97+
</AppContext.Provider>,
98+
);
6299
});
63100

64101
fireEvent.click(screen.getByTitle('Hide account notifications'));
65102

66-
const tree = render(<AccountNotifications {...props} />);
103+
const tree = render(
104+
<AppContext.Provider value={{ settings: mockSettings }}>
105+
<AccountNotifications {...props} />
106+
</AppContext.Provider>,
107+
);
67108
expect(tree).toMatchSnapshot();
68109
});
69110
});

src/components/AccountNotifications.tsx

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@ import {
33
ChevronLeftIcon,
44
ChevronUpIcon,
55
} from '@primer/octicons-react';
6-
import { type FC, useState } from 'react';
6+
import { type FC, useContext, useState } from 'react';
7+
import { AppContext } from '../context/App';
78
import type { Account } from '../types';
89
import type { Notification } from '../typesGitHub';
910
import { openAccountProfile } from '../utils/links';
11+
import { NotificationRow } from './NotificationRow';
1012
import { RepositoryNotifications } from './RepositoryNotifications';
1113
import { PlatformIcon } from './icons/PlatformIcon';
1214

@@ -21,6 +23,8 @@ export const AccountNotifications: FC<IAccountNotifications> = (
2123
) => {
2224
const { account, showAccountHostname, notifications } = props;
2325

26+
const { settings } = useContext(AppContext);
27+
2428
const groupedNotifications = Object.values(
2529
notifications.reduce(
2630
(acc: { [key: string]: Notification[] }, notification) => {
@@ -54,6 +58,8 @@ export const AccountNotifications: FC<IAccountNotifications> = (
5458
? 'Hide account notifications'
5559
: 'Show account notifications';
5660

61+
const groupByRepository = settings.groupBy === 'REPOSITORY';
62+
5763
return (
5864
<>
5965
{showAccountHostname && (
@@ -85,18 +91,24 @@ export const AccountNotifications: FC<IAccountNotifications> = (
8591
</div>
8692
)}
8793

88-
{showAccountNotifications &&
89-
Object.values(groupedNotifications).map((repoNotifications) => {
90-
const repoSlug = repoNotifications[0].repository.full_name;
94+
{showAccountNotifications && groupByRepository
95+
? Object.values(groupedNotifications).map((repoNotifications) => {
96+
const repoSlug = repoNotifications[0].repository.full_name;
9197

92-
return (
93-
<RepositoryNotifications
94-
key={repoSlug}
95-
repoName={repoSlug}
96-
repoNotifications={repoNotifications}
98+
return (
99+
<RepositoryNotifications
100+
key={repoSlug}
101+
repoName={repoSlug}
102+
repoNotifications={repoNotifications}
103+
/>
104+
);
105+
})
106+
: notifications.map((notification) => (
107+
<NotificationRow
108+
key={notification.id}
109+
notification={notification}
97110
/>
98-
);
99-
})}
111+
))}
100112
</>
101113
);
102114
};

src/components/NotificationRow.test.tsx

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {
55
mockSettings,
66
} from '../__mocks__/state-mocks';
77
import { AppContext } from '../context/App';
8-
import type { Link } from '../types';
8+
import { GroupBy, type Link } from '../types';
99
import type { Milestone, UserType } from '../typesGitHub';
1010
import { mockSingleNotification } from '../utils/api/__mocks__/response-mocks';
1111
import * as comms from '../utils/comms';
@@ -21,7 +21,7 @@ describe('components/NotificationRow.tsx', () => {
2121
jest.clearAllMocks();
2222
});
2323

24-
it('should render itself & its children', async () => {
24+
it('should render itself & its children - group by date', async () => {
2525
jest
2626
.spyOn(global.Date, 'now')
2727
.mockImplementation(() => new Date('2024').valueOf());
@@ -32,7 +32,29 @@ describe('components/NotificationRow.tsx', () => {
3232
};
3333

3434
const tree = render(
35-
<AppContext.Provider value={{ settings: mockSettings }}>
35+
<AppContext.Provider
36+
value={{ settings: { ...mockSettings, groupBy: GroupBy.DATE } }}
37+
>
38+
<NotificationRow {...props} />
39+
</AppContext.Provider>,
40+
);
41+
expect(tree).toMatchSnapshot();
42+
});
43+
44+
it('should render itself & its children - group by repositories', async () => {
45+
jest
46+
.spyOn(global.Date, 'now')
47+
.mockImplementation(() => new Date('2024').valueOf());
48+
49+
const props = {
50+
notification: mockSingleNotification,
51+
account: mockGitHubCloudAccount,
52+
};
53+
54+
const tree = render(
55+
<AppContext.Provider
56+
value={{ settings: { ...mockSettings, groupBy: GroupBy.REPOSITORY } }}
57+
>
3658
<NotificationRow {...props} />
3759
</AppContext.Provider>,
3860
);

src/components/NotificationRow.tsx

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
CommentIcon,
55
FeedPersonIcon,
66
IssueClosedIcon,
7+
MarkGithubIcon,
78
MilestoneIcon,
89
ReadIcon,
910
TagIcon,
@@ -28,7 +29,11 @@ import {
2829
getNotificationTypeIconColor,
2930
getPullRequestReviewIcon,
3031
} from '../utils/icons';
31-
import { openNotification, openUserProfile } from '../utils/links';
32+
import {
33+
openNotification,
34+
openRepository,
35+
openUserProfile,
36+
} from '../utils/links';
3237
import { formatReason } from '../utils/reason';
3338
import { PillButton } from './buttons/PillButton';
3439
import { AvatarIcon } from './icons/AvatarIcon';
@@ -101,6 +106,11 @@ export const NotificationRow: FC<INotificationRow> = ({
101106
notification.subject.linkedIssues?.length > 1 ? 'issues' : 'issue'
102107
} ${notification.subject?.linkedIssues?.join(', ')}`;
103108

109+
const repoAvatarUrl = notification.repository.owner.avatar_url;
110+
const repoSlug = notification.repository.full_name;
111+
112+
const groupByRepository = settings.groupBy === 'REPOSITORY';
113+
104114
return (
105115
<div
106116
id={notification.id}
@@ -115,13 +125,38 @@ export const NotificationRow: FC<INotificationRow> = ({
115125
className={cn('mr-3 flex w-5 items-center justify-center', iconColor)}
116126
title={notificationTitle}
117127
>
118-
<NotificationIcon size={16} aria-label={notification.subject.type} />
128+
<NotificationIcon
129+
size={groupByRepository ? 16 : 20}
130+
aria-label={notification.subject.type}
131+
/>
119132
</div>
120133

121134
<div
122135
className="flex-1 overflow-hidden overflow-ellipsis whitespace-nowrap"
123136
onClick={() => handleNotification()}
124137
>
138+
{!groupByRepository && (
139+
<div
140+
className="mb-1 flex items-center gap-1 cursor-pointer truncate text-sm font-medium "
141+
title={repoSlug}
142+
>
143+
<span>
144+
<AvatarIcon
145+
title={repoSlug}
146+
url={repoAvatarUrl}
147+
size="medium"
148+
defaultIcon={MarkGithubIcon}
149+
/>
150+
</span>
151+
<span
152+
className="cursor-pointer truncate opacity-90"
153+
onClick={() => openRepository(notification.repository)}
154+
>
155+
{repoSlug}
156+
</span>
157+
</div>
158+
)}
159+
125160
<div
126161
className="mb-1 cursor-pointer truncate text-sm"
127162
role="main"

0 commit comments

Comments
 (0)