Skip to content
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

feat: mark repository as done #788

Merged
merged 4 commits into from
Feb 19, 2024
Merged
Show file tree
Hide file tree
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
Expand Up @@ -14,6 +14,7 @@ jest.mock('./NotificationRow', () => ({

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

const props = {
hostname: 'github.com',
Expand Down Expand Up @@ -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';
Expand All @@ -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;
Expand All @@ -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 (
Expand All @@ -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"
Expand Down
34 changes: 33 additions & 1 deletion src/components/__snapshots__/Repository.test.tsx.snap
Original file line number Diff line number Diff line change
Expand Up @@ -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]}
Expand Down
9 changes: 9 additions & 0 deletions src/context/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -77,6 +78,7 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
markNotificationDone,
unsubscribeNotification,
markRepoNotifications,
markRepoNotificationsDone,
} = useNotifications(settings.colors);

useEffect(() => {
Expand Down Expand Up @@ -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={{
Expand All @@ -218,6 +226,7 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
markNotificationDone: markNotificationDoneWithAccounts,
unsubscribeNotification: unsubscribeNotificationWithAccounts,
markRepoNotifications: markRepoNotificationsWithAccounts,
markRepoNotificationsDone: markRepoNotificationsDoneWithAccounts,

settings,
updateSetting,
Expand Down
103 changes: 103 additions & 0 deletions src/hooks/useNotifications.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Up @@ -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;
}
Expand Down Expand Up @@ -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(
Expand All @@ -339,5 +387,6 @@ export const useNotifications = (colors: boolean): NotificationsState => {
markNotificationDone,
unsubscribeNotification,
markRepoNotifications,
markRepoNotificationsDone,
};
};