Skip to content

Commit 7c3523e

Browse files
authored
feat: add a "mark notification as done" button (#706)
1 parent 20b92e2 commit 7c3523e

9 files changed

+247
-15
lines changed

src/components/NotificationRow.test.tsx

+25
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,31 @@ describe('components/Notification.js', () => {
7979
expect(markNotification).toHaveBeenCalledTimes(1);
8080
});
8181

82+
it('should mark a notification as done', () => {
83+
const markNotificationDone = jest.fn();
84+
85+
const props = {
86+
notification: mockedSingleNotification,
87+
hostname: 'github.com',
88+
};
89+
90+
const { getByTitle } = render(
91+
<AppContext.Provider
92+
value={{
93+
settings: { ...mockSettings },
94+
accounts: mockAccounts,
95+
}}
96+
>
97+
<AppContext.Provider value={{ markNotificationDone }}>
98+
<NotificationRow {...props} />
99+
</AppContext.Provider>
100+
</AppContext.Provider>,
101+
);
102+
103+
fireEvent.click(getByTitle('Mark as Done'));
104+
expect(markNotificationDone).toHaveBeenCalledTimes(1);
105+
});
106+
82107
it('should unsubscribe from a notification thread', () => {
83108
const unsubscribeNotification = jest.fn();
84109

src/components/NotificationRow.tsx

+23-6
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import React, { useCallback, useContext } from 'react';
22
import { formatDistanceToNow, parseISO } from 'date-fns';
3-
import { CheckIcon, MuteIcon } from '@primer/octicons-react';
3+
import { CheckIcon, MuteIcon, ReadIcon } from '@primer/octicons-react';
44

55
import {
66
formatReason,
@@ -20,8 +20,13 @@ export const NotificationRow: React.FC<IProps> = ({
2020
notification,
2121
hostname,
2222
}) => {
23-
const { settings, accounts, markNotification, unsubscribeNotification } =
24-
useContext(AppContext);
23+
const {
24+
settings,
25+
accounts,
26+
markNotification,
27+
markNotificationDone,
28+
unsubscribeNotification,
29+
} = useContext(AppContext);
2530

2631
const pressTitle = useCallback(() => {
2732
openBrowser();
@@ -81,18 +86,30 @@ export const NotificationRow: React.FC<IProps> = ({
8186
</div>
8287
</div>
8388

84-
<div className="flex justify-center items-center w-8">
89+
<div className="flex justify-center items-center gap-2">
8590
<button
8691
className="focus:outline-none"
8792
title="Mark as Read"
8893
onClick={() => markNotification(notification.id, hostname)}
8994
>
90-
<CheckIcon
95+
<ReadIcon
9196
className="hover:text-green-500"
92-
size={20}
97+
size={14}
9398
aria-label="Mark as Read"
9499
/>
95100
</button>
101+
102+
<button
103+
className="focus:outline-none"
104+
title="Mark as Done"
105+
onClick={() => markNotificationDone(notification.id, hostname)}
106+
>
107+
<CheckIcon
108+
className="hover:text-green-500"
109+
size={16}
110+
aria-label="Mark as Done"
111+
/>
112+
</button>
96113
</div>
97114
</div>
98115
);

src/components/Repository.tsx

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
const { shell } = require('electron');
22

33
import React, { useCallback, useContext } from 'react';
4-
import { CheckIcon } from '@primer/octicons-react';
4+
import { ReadIcon } from '@primer/octicons-react';
55
import { CSSTransition, TransitionGroup } from 'react-transition-group';
66

77
import { AppContext } from '../context/App';
@@ -43,9 +43,9 @@ export const RepositoryNotifications: React.FC<IProps> = ({
4343

4444
<div className="flex w-8 justify-center items-center">
4545
<button className="focus:outline-none" onClick={markRepoAsRead}>
46-
<CheckIcon
46+
<ReadIcon
4747
className="hover:text-green-500"
48-
size={20}
48+
size={16}
4949
aria-label="Mark Repository as Read"
5050
/>
5151
</button>

src/components/__snapshots__/NotificationRow.test.tsx.snap

+32-3
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ exports[`components/Notification.js should render itself & its children 1`] = `
8787
</div>
8888
</div>
8989
<div
90-
className="flex justify-center items-center w-8"
90+
className="flex justify-center items-center gap-2"
9191
>
9292
<button
9393
className="focus:outline-none"
@@ -100,7 +100,7 @@ exports[`components/Notification.js should render itself & its children 1`] = `
100100
className="hover:text-green-500"
101101
fill="currentColor"
102102
focusable="false"
103-
height={20}
103+
height={14}
104104
role="img"
105105
style={
106106
{
@@ -111,7 +111,36 @@ exports[`components/Notification.js should render itself & its children 1`] = `
111111
}
112112
}
113113
viewBox="0 0 16 16"
114-
width={20}
114+
width={14}
115+
>
116+
<path
117+
d="M7.115.65a1.752 1.752 0 0 1 1.77 0l6.25 3.663c.536.314.865.889.865 1.51v6.427A1.75 1.75 0 0 1 14.25 14H1.75A1.75 1.75 0 0 1 0 12.25V5.823c0-.621.33-1.196.865-1.51Zm1.011 1.293a.252.252 0 0 0-.252 0l-5.72 3.353L6.468 7.76a2.748 2.748 0 0 1 3.066 0l4.312-2.464-5.719-3.353ZM13.15 12.5 8.772 9.06a1.25 1.25 0 0 0-1.544 0L2.85 12.5Zm1.35-5.85-3.687 2.106 3.687 2.897ZM5.187 8.756 1.5 6.65v5.003Z"
118+
/>
119+
</svg>
120+
</button>
121+
<button
122+
className="focus:outline-none"
123+
onClick={[Function]}
124+
title="Mark as Done"
125+
>
126+
<svg
127+
aria-hidden="false"
128+
aria-label="Mark as Done"
129+
className="hover:text-green-500"
130+
fill="currentColor"
131+
focusable="false"
132+
height={16}
133+
role="img"
134+
style={
135+
{
136+
"display": "inline-block",
137+
"overflow": "visible",
138+
"userSelect": "none",
139+
"verticalAlign": "text-bottom",
140+
}
141+
}
142+
viewBox="0 0 16 16"
143+
width={16}
115144
>
116145
<path
117146
d="M13.78 4.22a.75.75 0 0 1 0 1.06l-7.25 7.25a.75.75 0 0 1-1.06 0L2.22 9.28a.751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018L6 10.94l6.72-6.72a.75.75 0 0 1 1.06 0Z"

src/components/__snapshots__/Repository.test.tsx.snap

+3-3
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ exports[`components/Repository.tsx should render itself & its children 1`] = `
3131
className="hover:text-green-500"
3232
fill="currentColor"
3333
focusable="false"
34-
height={20}
34+
height={16}
3535
role="img"
3636
style={
3737
{
@@ -42,10 +42,10 @@ exports[`components/Repository.tsx should render itself & its children 1`] = `
4242
}
4343
}
4444
viewBox="0 0 16 16"
45-
width={20}
45+
width={16}
4646
>
4747
<path
48-
d="M13.78 4.22a.75.75 0 0 1 0 1.06l-7.25 7.25a.75.75 0 0 1-1.06 0L2.22 9.28a.751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018L6 10.94l6.72-6.72a.75.75 0 0 1 1.06 0Z"
48+
d="M7.115.65a1.752 1.752 0 0 1 1.77 0l6.25 3.663c.536.314.865.889.865 1.51v6.427A1.75 1.75 0 0 1 14.25 14H1.75A1.75 1.75 0 0 1 0 12.25V5.823c0-.621.33-1.196.865-1.51Zm1.011 1.293a.252.252 0 0 0-.252 0l-5.72 3.353L6.468 7.76a2.748 2.748 0 0 1 3.066 0l4.312-2.464-5.719-3.353ZM13.15 12.5 8.772 9.06a1.25 1.25 0 0 0-1.544 0L2.85 12.5Zm1.35-5.85-3.687 2.106 3.687 2.897ZM5.187 8.756 1.5 6.65v5.003Z"
4949
/>
5050
</svg>
5151
</button>

src/context/App.test.tsx

+27
Original file line numberDiff line numberDiff line change
@@ -38,13 +38,15 @@ describe('context/App.tsx', () => {
3838

3939
const fetchNotificationsMock = jest.fn();
4040
const markNotificationMock = jest.fn();
41+
const markNotificationDoneMock = jest.fn();
4142
const unsubscribeNotificationMock = jest.fn();
4243
const markRepoNotificationsMock = jest.fn();
4344

4445
beforeEach(() => {
4546
(useNotifications as jest.Mock).mockReturnValue({
4647
fetchNotifications: fetchNotificationsMock,
4748
markNotification: markNotificationMock,
49+
markNotificationDone: markNotificationDoneMock,
4850
unsubscribeNotification: unsubscribeNotificationMock,
4951
markRepoNotifications: markRepoNotificationsMock,
5052
});
@@ -121,6 +123,31 @@ describe('context/App.tsx', () => {
121123
);
122124
});
123125

126+
it('should call markNotificationDone', async () => {
127+
const TestComponent = () => {
128+
const { markNotificationDone } = useContext(AppContext);
129+
130+
return (
131+
<button onClick={() => markNotificationDone('123-456', 'github.com')}>
132+
Test Case
133+
</button>
134+
);
135+
};
136+
137+
const { getByText } = customRender(<TestComponent />);
138+
139+
markNotificationDoneMock.mockReset();
140+
141+
fireEvent.click(getByText('Test Case'));
142+
143+
expect(markNotificationDoneMock).toHaveBeenCalledTimes(1);
144+
expect(markNotificationDoneMock).toHaveBeenCalledWith(
145+
{ enterpriseAccounts: [], token: null, user: null },
146+
'123-456',
147+
'github.com',
148+
);
149+
});
150+
124151
it('should call unsubscribeNotification', async () => {
125152
const TestComponent = () => {
126153
const { unsubscribeNotification } = useContext(AppContext);

src/context/App.tsx

+9
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ interface AppContextState {
5252
requestFailed: boolean;
5353
fetchNotifications: () => Promise<void>;
5454
markNotification: (id: string, hostname: string) => Promise<void>;
55+
markNotificationDone: (id: string, hostname: string) => Promise<void>;
5556
unsubscribeNotification: (id: string, hostname: string) => Promise<void>;
5657
markRepoNotifications: (id: string, hostname: string) => Promise<void>;
5758

@@ -70,6 +71,7 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
7071
requestFailed,
7172
isFetching,
7273
markNotification,
74+
markNotificationDone,
7375
unsubscribeNotification,
7476
markRepoNotifications,
7577
} = useNotifications(settings.colors);
@@ -176,6 +178,12 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
176178
[accounts, notifications],
177179
);
178180

181+
const markNotificationDoneWithAccounts = useCallback(
182+
async (id: string, hostname: string) =>
183+
await markNotificationDone(accounts, id, hostname),
184+
[accounts, notifications],
185+
);
186+
179187
const unsubscribeNotificationWithAccounts = useCallback(
180188
async (id: string, hostname: string) =>
181189
await unsubscribeNotification(accounts, id, hostname),
@@ -203,6 +211,7 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
203211
requestFailed,
204212
fetchNotifications: fetchNotificationsWithAccounts,
205213
markNotification: markNotificationWithAccounts,
214+
markNotificationDone: markNotificationDoneWithAccounts,
206215
unsubscribeNotification: unsubscribeNotificationWithAccounts,
207216
markRepoNotifications: markRepoNotificationsWithAccounts,
208217

src/hooks/useNotifications.test.ts

+86
Original file line numberDiff line numberDiff line change
@@ -355,6 +355,92 @@ describe('hooks/useNotifications.ts', () => {
355355
});
356356
});
357357

358+
describe('markNotificationDone', () => {
359+
const id = 'notification-123';
360+
361+
describe('github.com', () => {
362+
const accounts = { ...mockAccounts, enterpriseAccounts: [] };
363+
const hostname = 'github.com';
364+
365+
it('should mark a notification as done with success - github.com', async () => {
366+
nock('https://api.github.com/')
367+
.delete(`/notifications/threads/${id}`)
368+
.reply(200);
369+
370+
const { result } = renderHook(() => useNotifications(false));
371+
372+
act(() => {
373+
result.current.markNotificationDone(accounts, id, hostname);
374+
});
375+
376+
await waitFor(() => {
377+
expect(result.current.isFetching).toBe(false);
378+
});
379+
380+
expect(result.current.notifications.length).toBe(0);
381+
});
382+
383+
it('should mark a notification as done with failure - github.com', async () => {
384+
nock('https://api.github.com/')
385+
.delete(`/notifications/threads/${id}`)
386+
.reply(400);
387+
388+
const { result } = renderHook(() => useNotifications(false));
389+
390+
act(() => {
391+
result.current.markNotificationDone(accounts, id, hostname);
392+
});
393+
394+
await waitFor(() => {
395+
expect(result.current.isFetching).toBe(false);
396+
});
397+
398+
expect(result.current.notifications.length).toBe(0);
399+
});
400+
});
401+
402+
describe('enterprise', () => {
403+
const accounts = { ...mockAccounts, token: null };
404+
const hostname = 'github.gitify.io';
405+
406+
it('should mark a notification as done with success - enterprise', async () => {
407+
nock('https://github.gitify.io/')
408+
.delete(`/notifications/threads/${id}`)
409+
.reply(200);
410+
411+
const { result } = renderHook(() => useNotifications(false));
412+
413+
act(() => {
414+
result.current.markNotificationDone(accounts, id, hostname);
415+
});
416+
417+
await waitFor(() => {
418+
expect(result.current.isFetching).toBe(false);
419+
});
420+
421+
expect(result.current.notifications.length).toBe(0);
422+
});
423+
424+
it('should mark a notification as done with failure - enterprise', async () => {
425+
nock('https://github.gitify.io/')
426+
.delete(`/notifications/threads/${id}`)
427+
.reply(400);
428+
429+
const { result } = renderHook(() => useNotifications(false));
430+
431+
act(() => {
432+
result.current.markNotificationDone(accounts, id, hostname);
433+
});
434+
435+
await waitFor(() => {
436+
expect(result.current.isFetching).toBe(false);
437+
});
438+
439+
expect(result.current.notifications.length).toBe(0);
440+
});
441+
});
442+
});
443+
358444
describe('unsubscribeNotification', () => {
359445
const id = 'notification-123';
360446

0 commit comments

Comments
 (0)