diff --git a/src/components/NotificationRow.test.tsx b/src/components/NotificationRow.test.tsx index cb66ae0ca..0359f0eef 100644 --- a/src/components/NotificationRow.test.tsx +++ b/src/components/NotificationRow.test.tsx @@ -79,6 +79,31 @@ describe('components/Notification.js', () => { expect(markNotification).toHaveBeenCalledTimes(1); }); + it('should mark a notification as done', () => { + const markNotificationDone = jest.fn(); + + const props = { + notification: mockedSingleNotification, + hostname: 'github.com', + }; + + const { getByTitle } = render( + + + + + , + ); + + fireEvent.click(getByTitle('Mark as Done')); + expect(markNotificationDone).toHaveBeenCalledTimes(1); + }); + it('should unsubscribe from a notification thread', () => { const unsubscribeNotification = jest.fn(); diff --git a/src/components/NotificationRow.tsx b/src/components/NotificationRow.tsx index 612a3bbb6..d8b58c72a 100644 --- a/src/components/NotificationRow.tsx +++ b/src/components/NotificationRow.tsx @@ -1,6 +1,6 @@ import React, { useCallback, useContext } from 'react'; import { formatDistanceToNow, parseISO } from 'date-fns'; -import { CheckIcon, MuteIcon } from '@primer/octicons-react'; +import { CheckIcon, MuteIcon, ReadIcon } from '@primer/octicons-react'; import { formatReason, @@ -20,8 +20,13 @@ export const NotificationRow: React.FC = ({ notification, hostname, }) => { - const { settings, accounts, markNotification, unsubscribeNotification } = - useContext(AppContext); + const { + settings, + accounts, + markNotification, + markNotificationDone, + unsubscribeNotification, + } = useContext(AppContext); const pressTitle = useCallback(() => { openBrowser(); @@ -81,18 +86,30 @@ export const NotificationRow: React.FC = ({ -
+
+ +
); diff --git a/src/components/Repository.tsx b/src/components/Repository.tsx index 14ee53c2b..ddf1ef4a1 100644 --- a/src/components/Repository.tsx +++ b/src/components/Repository.tsx @@ -1,7 +1,7 @@ const { shell } = require('electron'); import React, { useCallback, useContext } from 'react'; -import { CheckIcon } from '@primer/octicons-react'; +import { ReadIcon } from '@primer/octicons-react'; import { CSSTransition, TransitionGroup } from 'react-transition-group'; import { AppContext } from '../context/App'; @@ -43,9 +43,9 @@ export const RepositoryNotifications: React.FC = ({
diff --git a/src/components/__snapshots__/NotificationRow.test.tsx.snap b/src/components/__snapshots__/NotificationRow.test.tsx.snap index 8a922c507..9d9ed5042 100644 --- a/src/components/__snapshots__/NotificationRow.test.tsx.snap +++ b/src/components/__snapshots__/NotificationRow.test.tsx.snap @@ -87,7 +87,7 @@ exports[`components/Notification.js should render itself & its children 1`] = `
+ diff --git a/src/context/App.test.tsx b/src/context/App.test.tsx index d0b45e637..aea2d742c 100644 --- a/src/context/App.test.tsx +++ b/src/context/App.test.tsx @@ -38,6 +38,7 @@ describe('context/App.tsx', () => { const fetchNotificationsMock = jest.fn(); const markNotificationMock = jest.fn(); + const markNotificationDoneMock = jest.fn(); const unsubscribeNotificationMock = jest.fn(); const markRepoNotificationsMock = jest.fn(); @@ -45,6 +46,7 @@ describe('context/App.tsx', () => { (useNotifications as jest.Mock).mockReturnValue({ fetchNotifications: fetchNotificationsMock, markNotification: markNotificationMock, + markNotificationDone: markNotificationDoneMock, unsubscribeNotification: unsubscribeNotificationMock, markRepoNotifications: markRepoNotificationsMock, }); @@ -121,6 +123,31 @@ describe('context/App.tsx', () => { ); }); + it('should call markNotificationDone', async () => { + const TestComponent = () => { + const { markNotificationDone } = useContext(AppContext); + + return ( + + ); + }; + + const { getByText } = customRender(); + + markNotificationDoneMock.mockReset(); + + fireEvent.click(getByText('Test Case')); + + expect(markNotificationDoneMock).toHaveBeenCalledTimes(1); + expect(markNotificationDoneMock).toHaveBeenCalledWith( + { enterpriseAccounts: [], token: null, user: null }, + '123-456', + 'github.com', + ); + }); + it('should call unsubscribeNotification', async () => { const TestComponent = () => { const { unsubscribeNotification } = useContext(AppContext); diff --git a/src/context/App.tsx b/src/context/App.tsx index 60197bca5..dbc788cd1 100644 --- a/src/context/App.tsx +++ b/src/context/App.tsx @@ -52,6 +52,7 @@ interface AppContextState { requestFailed: boolean; fetchNotifications: () => Promise; markNotification: (id: string, hostname: string) => Promise; + markNotificationDone: (id: string, hostname: string) => Promise; unsubscribeNotification: (id: string, hostname: string) => Promise; markRepoNotifications: (id: string, hostname: string) => Promise; @@ -70,6 +71,7 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => { requestFailed, isFetching, markNotification, + markNotificationDone, unsubscribeNotification, markRepoNotifications, } = useNotifications(settings.colors); @@ -176,6 +178,12 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => { [accounts, notifications], ); + const markNotificationDoneWithAccounts = useCallback( + async (id: string, hostname: string) => + await markNotificationDone(accounts, id, hostname), + [accounts, notifications], + ); + const unsubscribeNotificationWithAccounts = useCallback( async (id: string, hostname: string) => await unsubscribeNotification(accounts, id, hostname), @@ -203,6 +211,7 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => { requestFailed, fetchNotifications: fetchNotificationsWithAccounts, markNotification: markNotificationWithAccounts, + markNotificationDone: markNotificationDoneWithAccounts, unsubscribeNotification: unsubscribeNotificationWithAccounts, markRepoNotifications: markRepoNotificationsWithAccounts, diff --git a/src/hooks/useNotifications.test.ts b/src/hooks/useNotifications.test.ts index 66e40f26a..c6f2b753d 100644 --- a/src/hooks/useNotifications.test.ts +++ b/src/hooks/useNotifications.test.ts @@ -355,6 +355,92 @@ describe('hooks/useNotifications.ts', () => { }); }); + describe('markNotificationDone', () => { + const id = 'notification-123'; + + describe('github.com', () => { + const accounts = { ...mockAccounts, enterpriseAccounts: [] }; + const hostname = 'github.com'; + + it('should mark a notification 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.markNotificationDone(accounts, id, hostname); + }); + + await waitFor(() => { + expect(result.current.isFetching).toBe(false); + }); + + expect(result.current.notifications.length).toBe(0); + }); + + it('should mark a notification 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.markNotificationDone(accounts, id, 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 notification as done with success - enterprise', async () => { + nock('https://github.gitify.io/') + .delete(`/notifications/threads/${id}`) + .reply(200); + + const { result } = renderHook(() => useNotifications(false)); + + act(() => { + result.current.markNotificationDone(accounts, id, hostname); + }); + + await waitFor(() => { + expect(result.current.isFetching).toBe(false); + }); + + expect(result.current.notifications.length).toBe(0); + }); + + it('should mark a notification as done with failure - enterprise', async () => { + nock('https://github.gitify.io/') + .delete(`/notifications/threads/${id}`) + .reply(400); + + const { result } = renderHook(() => useNotifications(false)); + + act(() => { + result.current.markNotificationDone(accounts, id, hostname); + }); + + await waitFor(() => { + expect(result.current.isFetching).toBe(false); + }); + + expect(result.current.notifications.length).toBe(0); + }); + }); + }); + describe('unsubscribeNotification', () => { const id = 'notification-123'; diff --git a/src/hooks/useNotifications.ts b/src/hooks/useNotifications.ts index 50cf161b1..41875fb8b 100644 --- a/src/hooks/useNotifications.ts +++ b/src/hooks/useNotifications.ts @@ -28,6 +28,11 @@ interface NotificationsState { id: string, hostname: string, ) => Promise; + markNotificationDone: ( + accounts: AuthState, + id: string, + hostname: string, + ) => Promise; unsubscribeNotification: ( accounts: AuthState, id: string, @@ -215,6 +220,39 @@ export const useNotifications = (colors: boolean): NotificationsState => { [notifications], ); + const markNotificationDone = useCallback( + async (accounts, id, hostname) => { + setIsFetching(true); + + const isEnterprise = hostname !== Constants.DEFAULT_AUTH_OPTIONS.hostname; + const token = isEnterprise + ? getEnterpriseAccountToken(hostname, accounts.enterpriseAccounts) + : accounts.token; + + try { + await apiRequestAuth( + `${generateGitHubAPIUrl(hostname)}notifications/threads/${id}`, + 'DELETE', + token, + {}, + ); + + const updatedNotifications = removeNotification( + id, + notifications, + hostname, + ); + + setNotifications(updatedNotifications); + setTrayIconColor(updatedNotifications); + setIsFetching(false); + } catch (err) { + setIsFetching(false); + } + }, + [notifications], + ); + const unsubscribeNotification = useCallback( async (accounts, id, hostname) => { setIsFetching(true); @@ -281,6 +319,7 @@ export const useNotifications = (colors: boolean): NotificationsState => { fetchNotifications, markNotification, + markNotificationDone, unsubscribeNotification, markRepoNotifications, };