Skip to content

Commit 1bffe84

Browse files
authored
feat: mark repository as done (#788)
1 parent 7efe0d9 commit 1bffe84

File tree

6 files changed

+231
-6
lines changed

6 files changed

+231
-6
lines changed

src/components/Repository.test.tsx

+18-2
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ jest.mock('./NotificationRow', () => ({
1414

1515
describe('components/Repository.tsx', () => {
1616
const markRepoNotifications = jest.fn();
17+
const markRepoNotificationsDone = jest.fn();
1718

1819
const props = {
1920
hostname: 'github.com',
@@ -52,17 +53,32 @@ describe('components/Repository.tsx', () => {
5253
});
5354

5455
it('should mark a repo as read', function () {
55-
const { getByRole } = render(
56+
const { getByTitle } = render(
5657
<AppContext.Provider value={{ markRepoNotifications }}>
5758
<RepositoryNotifications {...props} />
5859
</AppContext.Provider>,
5960
);
6061

61-
fireEvent.click(getByRole('button'));
62+
fireEvent.click(getByTitle('Mark Repository as Read'));
6263

6364
expect(markRepoNotifications).toHaveBeenCalledWith(
6465
'manosim/notifications-test',
6566
'github.com',
6667
);
6768
});
69+
70+
it('should mark a repo as done', function () {
71+
const { getByTitle } = render(
72+
<AppContext.Provider value={{ markRepoNotificationsDone }}>
73+
<RepositoryNotifications {...props} />
74+
</AppContext.Provider>,
75+
);
76+
77+
fireEvent.click(getByTitle('Mark Repository as Done'));
78+
79+
expect(markRepoNotificationsDone).toHaveBeenCalledWith(
80+
'manosim/notifications-test',
81+
'github.com',
82+
);
83+
});
6884
});

src/components/Repository.tsx

+19-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import React, { useCallback, useContext } from 'react';
2-
import { ReadIcon } from '@primer/octicons-react';
2+
import { ReadIcon, CheckIcon } from '@primer/octicons-react';
33
import { CSSTransition, TransitionGroup } from 'react-transition-group';
44

55
import { AppContext } from '../context/App';
@@ -18,7 +18,8 @@ export const RepositoryNotifications: React.FC<IProps> = ({
1818
repoNotifications,
1919
hostname,
2020
}) => {
21-
const { markRepoNotifications } = useContext(AppContext);
21+
const { markRepoNotifications, markRepoNotificationsDone } =
22+
useContext(AppContext);
2223

2324
const openBrowser = useCallback(() => {
2425
const url = repoNotifications[0].repository.html_url;
@@ -30,6 +31,11 @@ export const RepositoryNotifications: React.FC<IProps> = ({
3031
markRepoNotifications(repoSlug, hostname);
3132
}, [repoNotifications, hostname]);
3233

34+
const markRepoAsDone = useCallback(() => {
35+
const repoSlug = repoNotifications[0].repository.full_name;
36+
markRepoNotificationsDone(repoSlug, hostname);
37+
}, [repoNotifications, hostname]);
38+
3339
const avatarUrl = repoNotifications[0].repository.owner.avatar_url;
3440

3541
return (
@@ -40,7 +46,17 @@ export const RepositoryNotifications: React.FC<IProps> = ({
4046
<span onClick={openBrowser}>{repoName}</span>
4147
</div>
4248

43-
<div className="flex justify-center items-center">
49+
<div className="flex justify-center items-center gap-2">
50+
<button
51+
className="focus:outline-none h-full hover:text-green-500"
52+
title="Mark Repository as Done"
53+
onClick={markRepoAsDone}
54+
>
55+
<CheckIcon size={16} aria-label="Mark Repository as Done" />
56+
</button>
57+
58+
<div className="w-[14px]" />
59+
4460
<button
4561
className="focus:outline-none h-full hover:text-green-500"
4662
title="Mark Repository as Read"

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

+33-1
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,40 @@ exports[`components/Repository.tsx should render itself & its children 1`] = `
1919
</span>
2020
</div>
2121
<div
22-
className="flex justify-center items-center"
22+
className="flex justify-center items-center gap-2"
2323
>
24+
<button
25+
className="focus:outline-none h-full hover:text-green-500"
26+
onClick={[Function]}
27+
title="Mark Repository as Done"
28+
>
29+
<svg
30+
aria-hidden="false"
31+
aria-label="Mark Repository as Done"
32+
className="octicon octicon-check"
33+
fill="currentColor"
34+
focusable="false"
35+
height={16}
36+
role="img"
37+
style={
38+
{
39+
"display": "inline-block",
40+
"overflow": "visible",
41+
"userSelect": "none",
42+
"verticalAlign": "text-bottom",
43+
}
44+
}
45+
viewBox="0 0 16 16"
46+
width={16}
47+
>
48+
<path
49+
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"
50+
/>
51+
</svg>
52+
</button>
53+
<div
54+
className="w-[14px]"
55+
/>
2456
<button
2557
className="focus:outline-none h-full hover:text-green-500"
2658
onClick={[Function]}

src/context/App.tsx

+9
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ interface AppContextState {
5757
markNotificationDone: (id: string, hostname: string) => Promise<void>;
5858
unsubscribeNotification: (id: string, hostname: string) => Promise<void>;
5959
markRepoNotifications: (id: string, hostname: string) => Promise<void>;
60+
markRepoNotificationsDone: (id: string, hostname: string) => Promise<void>;
6061

6162
settings: SettingsState;
6263
updateSetting: (name: keyof SettingsState, value: any) => void;
@@ -77,6 +78,7 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
7778
markNotificationDone,
7879
unsubscribeNotification,
7980
markRepoNotifications,
81+
markRepoNotificationsDone,
8082
} = useNotifications(settings.colors);
8183

8284
useEffect(() => {
@@ -199,6 +201,12 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
199201
[accounts, notifications],
200202
);
201203

204+
const markRepoNotificationsDoneWithAccounts = useCallback(
205+
async (repoSlug: string, hostname: string) =>
206+
await markRepoNotificationsDone(accounts, repoSlug, hostname),
207+
[accounts, notifications],
208+
);
209+
202210
return (
203211
<AppContext.Provider
204212
value={{
@@ -218,6 +226,7 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
218226
markNotificationDone: markNotificationDoneWithAccounts,
219227
unsubscribeNotification: unsubscribeNotificationWithAccounts,
220228
markRepoNotifications: markRepoNotificationsWithAccounts,
229+
markRepoNotificationsDone: markRepoNotificationsDoneWithAccounts,
221230

222231
settings,
223232
updateSetting,

src/hooks/useNotifications.test.ts

+103
Original file line numberDiff line numberDiff line change
@@ -672,4 +672,107 @@ describe('hooks/useNotifications.ts', () => {
672672
});
673673
});
674674
});
675+
676+
describe('markRepoNotificationsDone', () => {
677+
const repoSlug = 'manosim/gitify';
678+
const id = 'notification-123';
679+
680+
describe('github.com', () => {
681+
const accounts = { ...mockAccounts, enterpriseAccounts: [] };
682+
const hostname = 'github.com';
683+
684+
it("should mark a repository's notifications as done with success - github.com", async () => {
685+
nock('https://api.github.com/')
686+
.delete(`/notifications/threads/${id}`)
687+
.reply(200);
688+
689+
const { result } = renderHook(() => useNotifications(false));
690+
691+
act(() => {
692+
result.current.markRepoNotificationsDone(
693+
accounts,
694+
repoSlug,
695+
hostname,
696+
);
697+
});
698+
699+
await waitFor(() => {
700+
expect(result.current.isFetching).toBe(false);
701+
});
702+
703+
expect(result.current.notifications.length).toBe(0);
704+
});
705+
706+
it("should mark a repository's notifications as done with failure - github.com", async () => {
707+
nock('https://api.github.com/')
708+
.delete(`/notifications/threads/${id}`)
709+
.reply(400);
710+
711+
const { result } = renderHook(() => useNotifications(false));
712+
713+
act(() => {
714+
result.current.markRepoNotificationsDone(
715+
accounts,
716+
repoSlug,
717+
hostname,
718+
);
719+
});
720+
721+
await waitFor(() => {
722+
expect(result.current.isFetching).toBe(false);
723+
});
724+
725+
expect(result.current.notifications.length).toBe(0);
726+
});
727+
});
728+
729+
describe('enterprise', () => {
730+
const accounts = { ...mockAccounts, token: null };
731+
const hostname = 'github.gitify.io';
732+
733+
it("should mark a repository's notifications as done with success - enterprise", async () => {
734+
nock('https://api.github.com/')
735+
.delete(`/notifications/threads/${id}`)
736+
.reply(200);
737+
738+
const { result } = renderHook(() => useNotifications(false));
739+
740+
act(() => {
741+
result.current.markRepoNotificationsDone(
742+
accounts,
743+
repoSlug,
744+
hostname,
745+
);
746+
});
747+
748+
await waitFor(() => {
749+
expect(result.current.isFetching).toBe(false);
750+
});
751+
752+
expect(result.current.notifications.length).toBe(0);
753+
});
754+
755+
it("should mark a repository's notifications as done with failure - enterprise", async () => {
756+
nock('https://api.github.com/')
757+
.delete(`/notifications/threads/${id}`)
758+
.reply(400);
759+
760+
const { result } = renderHook(() => useNotifications(false));
761+
762+
act(() => {
763+
result.current.markRepoNotificationsDone(
764+
accounts,
765+
repoSlug,
766+
hostname,
767+
);
768+
});
769+
770+
await waitFor(() => {
771+
expect(result.current.isFetching).toBe(false);
772+
});
773+
774+
expect(result.current.notifications.length).toBe(0);
775+
});
776+
});
777+
});
675778
});

src/hooks/useNotifications.ts

+49
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,11 @@ interface NotificationsState {
4545
repoSlug: string,
4646
hostname: string,
4747
) => Promise<void>;
48+
markRepoNotificationsDone: (
49+
accounts: AuthState,
50+
repoSlug: string,
51+
hostname: string,
52+
) => Promise<void>;
4853
isFetching: boolean;
4954
requestFailed: boolean;
5055
}
@@ -314,6 +319,49 @@ export const useNotifications = (colors: boolean): NotificationsState => {
314319
[notifications],
315320
);
316321

322+
const markRepoNotificationsDone = useCallback(
323+
async (accounts, repoSlug, hostname) => {
324+
setIsFetching(true);
325+
326+
try {
327+
const accountIndex = notifications.findIndex(
328+
(accountNotifications) => accountNotifications.hostname === hostname,
329+
);
330+
331+
if (accountIndex !== -1) {
332+
const notificationsToRemove = notifications[
333+
accountIndex
334+
].notifications.filter(
335+
(notification) => notification.repository.full_name === repoSlug,
336+
);
337+
338+
await Promise.all(
339+
notificationsToRemove.map((notification) =>
340+
markNotificationDone(
341+
accounts,
342+
notification.id,
343+
notifications[accountIndex].hostname,
344+
),
345+
),
346+
);
347+
}
348+
349+
const updatedNotifications = removeNotifications(
350+
repoSlug,
351+
notifications,
352+
hostname,
353+
);
354+
355+
setNotifications(updatedNotifications);
356+
setTrayIconColor(updatedNotifications);
357+
setIsFetching(false);
358+
} catch (err) {
359+
setIsFetching(false);
360+
}
361+
},
362+
[notifications],
363+
);
364+
317365
const removeNotificationFromState = useCallback(
318366
(id, hostname) => {
319367
const updatedNotifications = removeNotification(
@@ -339,5 +387,6 @@ export const useNotifications = (colors: boolean): NotificationsState => {
339387
markNotificationDone,
340388
unsubscribeNotification,
341389
markRepoNotifications,
390+
markRepoNotificationsDone,
342391
};
343392
};

0 commit comments

Comments
 (0)