Skip to content

feat: mark repository as done #788

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Feb 19, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 18 additions & 2 deletions src/components/Repository.test.tsx
Original file line number Diff line number Diff line change
@@ -14,6 +14,7 @@ jest.mock('./NotificationRow', () => ({

describe('components/Repository.tsx', () => {
const markRepoNotifications = jest.fn();
const markRepoNotificationsDone = jest.fn();

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

it('should mark a repo as read', function () {
const { getByRole } = render(
const { getByTitle } = render(
<AppContext.Provider value={{ markRepoNotifications }}>
<RepositoryNotifications {...props} />
</AppContext.Provider>,
);

fireEvent.click(getByRole('button'));
fireEvent.click(getByTitle('Mark Repository as Read'));

expect(markRepoNotifications).toHaveBeenCalledWith(
'manosim/notifications-test',
'github.com',
);
});

it('should mark a repo as done', function () {
const { getByTitle } = render(
<AppContext.Provider value={{ markRepoNotificationsDone }}>
<RepositoryNotifications {...props} />
</AppContext.Provider>,
);

fireEvent.click(getByTitle('Mark Repository as Done'));

expect(markRepoNotificationsDone).toHaveBeenCalledWith(
'manosim/notifications-test',
'github.com',
);
});
});
22 changes: 19 additions & 3 deletions src/components/Repository.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, { useCallback, useContext } from 'react';
import { ReadIcon } from '@primer/octicons-react';
import { ReadIcon, CheckIcon } from '@primer/octicons-react';
import { CSSTransition, TransitionGroup } from 'react-transition-group';

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

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

const markRepoAsDone = useCallback(() => {
const repoSlug = repoNotifications[0].repository.full_name;
markRepoNotificationsDone(repoSlug, hostname);
}, [repoNotifications, hostname]);

const avatarUrl = repoNotifications[0].repository.owner.avatar_url;

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

<div className="flex justify-center items-center">
<div className="flex justify-center items-center gap-2">
<button
className="focus:outline-none h-full hover:text-green-500"
title="Mark Repository as Done"
onClick={markRepoAsDone}
>
<CheckIcon size={16} aria-label="Mark Repository as Done" />
</button>

<div className="w-[14px]" />

<button
className="focus:outline-none h-full hover:text-green-500"
title="Mark Repository as Read"
34 changes: 33 additions & 1 deletion src/components/__snapshots__/Repository.test.tsx.snap
Original file line number Diff line number Diff line change
@@ -19,8 +19,40 @@ exports[`components/Repository.tsx should render itself & its children 1`] = `
</span>
</div>
<div
className="flex justify-center items-center"
className="flex justify-center items-center gap-2"
>
<button
className="focus:outline-none h-full hover:text-green-500"
onClick={[Function]}
title="Mark Repository as Done"
>
<svg
aria-hidden="false"
aria-label="Mark Repository as Done"
className="octicon octicon-check"
fill="currentColor"
focusable="false"
height={16}
role="img"
style={
{
"display": "inline-block",
"overflow": "visible",
"userSelect": "none",
"verticalAlign": "text-bottom",
}
}
viewBox="0 0 16 16"
width={16}
>
<path
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"
/>
</svg>
</button>
<div
className="w-[14px]"
/>
<button
className="focus:outline-none h-full hover:text-green-500"
onClick={[Function]}
9 changes: 9 additions & 0 deletions src/context/App.tsx
Original file line number Diff line number Diff line change
@@ -57,6 +57,7 @@ interface AppContextState {
markNotificationDone: (id: string, hostname: string) => Promise<void>;
unsubscribeNotification: (id: string, hostname: string) => Promise<void>;
markRepoNotifications: (id: string, hostname: string) => Promise<void>;
markRepoNotificationsDone: (id: string, hostname: string) => Promise<void>;

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

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

const markRepoNotificationsDoneWithAccounts = useCallback(
async (repoSlug: string, hostname: string) =>
await markRepoNotificationsDone(accounts, repoSlug, hostname),
[accounts, notifications],
);

return (
<AppContext.Provider
value={{
@@ -218,6 +226,7 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
markNotificationDone: markNotificationDoneWithAccounts,
unsubscribeNotification: unsubscribeNotificationWithAccounts,
markRepoNotifications: markRepoNotificationsWithAccounts,
markRepoNotificationsDone: markRepoNotificationsDoneWithAccounts,

settings,
updateSetting,
103 changes: 103 additions & 0 deletions src/hooks/useNotifications.test.ts
Original file line number Diff line number Diff line change
@@ -672,4 +672,107 @@ describe('hooks/useNotifications.ts', () => {
});
});
});

describe('markRepoNotificationsDone', () => {
const repoSlug = 'manosim/gitify';
const id = 'notification-123';

describe('github.com', () => {
const accounts = { ...mockAccounts, enterpriseAccounts: [] };
const hostname = 'github.com';

it("should mark a repository's notifications as done with success - github.com", async () => {
nock('https://api.github.com/')
.delete(`/notifications/threads/${id}`)
.reply(200);

const { result } = renderHook(() => useNotifications(false));

act(() => {
result.current.markRepoNotificationsDone(
accounts,
repoSlug,
hostname,
);
});

await waitFor(() => {
expect(result.current.isFetching).toBe(false);
});

expect(result.current.notifications.length).toBe(0);
});

it("should mark a repository's notifications as done with failure - github.com", async () => {
nock('https://api.github.com/')
.delete(`/notifications/threads/${id}`)
.reply(400);

const { result } = renderHook(() => useNotifications(false));

act(() => {
result.current.markRepoNotificationsDone(
accounts,
repoSlug,
hostname,
);
});

await waitFor(() => {
expect(result.current.isFetching).toBe(false);
});

expect(result.current.notifications.length).toBe(0);
});
});

describe('enterprise', () => {
const accounts = { ...mockAccounts, token: null };
const hostname = 'github.gitify.io';

it("should mark a repository's notifications as done with success - enterprise", async () => {
nock('https://api.github.com/')
.delete(`/notifications/threads/${id}`)
.reply(200);

const { result } = renderHook(() => useNotifications(false));

act(() => {
result.current.markRepoNotificationsDone(
accounts,
repoSlug,
hostname,
);
});

await waitFor(() => {
expect(result.current.isFetching).toBe(false);
});

expect(result.current.notifications.length).toBe(0);
});

it("should mark a repository's notifications as done with failure - enterprise", async () => {
nock('https://api.github.com/')
.delete(`/notifications/threads/${id}`)
.reply(400);

const { result } = renderHook(() => useNotifications(false));

act(() => {
result.current.markRepoNotificationsDone(
accounts,
repoSlug,
hostname,
);
});

await waitFor(() => {
expect(result.current.isFetching).toBe(false);
});

expect(result.current.notifications.length).toBe(0);
});
});
});
});
49 changes: 49 additions & 0 deletions src/hooks/useNotifications.ts
Original file line number Diff line number Diff line change
@@ -45,6 +45,11 @@ interface NotificationsState {
repoSlug: string,
hostname: string,
) => Promise<void>;
markRepoNotificationsDone: (
accounts: AuthState,
repoSlug: string,
hostname: string,
) => Promise<void>;
isFetching: boolean;
requestFailed: boolean;
}
@@ -314,6 +319,49 @@ export const useNotifications = (colors: boolean): NotificationsState => {
[notifications],
);

const markRepoNotificationsDone = useCallback(
async (accounts, repoSlug, hostname) => {
setIsFetching(true);

try {
const accountIndex = notifications.findIndex(
(accountNotifications) => accountNotifications.hostname === hostname,
);

if (accountIndex !== -1) {
const notificationsToRemove = notifications[
accountIndex
].notifications.filter(
(notification) => notification.repository.full_name === repoSlug,
);

await Promise.all(
notificationsToRemove.map((notification) =>
markNotificationDone(
accounts,
notification.id,
notifications[accountIndex].hostname,
),
),
);
}

const updatedNotifications = removeNotifications(
repoSlug,
notifications,
hostname,
);

setNotifications(updatedNotifications);
setTrayIconColor(updatedNotifications);
setIsFetching(false);
} catch (err) {
setIsFetching(false);
}
},
[notifications],
);

const removeNotificationFromState = useCallback(
(id, hostname) => {
const updatedNotifications = removeNotification(
@@ -339,5 +387,6 @@ export const useNotifications = (colors: boolean): NotificationsState => {
markNotificationDone,
unsubscribeNotification,
markRepoNotifications,
markRepoNotificationsDone,
};
};