diff --git a/src/__mocks__/mock-state.ts b/src/__mocks__/mock-state.ts index ac5072e48..8175a7f1d 100644 --- a/src/__mocks__/mock-state.ts +++ b/src/__mocks__/mock-state.ts @@ -11,6 +11,7 @@ export const mockSettings: SettingsState = { participating: false, playSound: true, showNotifications: true, + showBots: true, openAtStartup: false, appearance: Appearance.SYSTEM, colors: false, diff --git a/src/__mocks__/mockedData.ts b/src/__mocks__/mockedData.ts index 987b26c71..7e05086fe 100644 --- a/src/__mocks__/mockedData.ts +++ b/src/__mocks__/mockedData.ts @@ -299,6 +299,7 @@ export const mockedGraphQLResponse: GraphQLSearch = { createdAt: '2022-02-20T18:33:39Z', author: { login: 'comment-user', + type: 'User', }, replies: { nodes: [], @@ -309,6 +310,7 @@ export const mockedGraphQLResponse: GraphQLSearch = { createdAt: '2022-02-21T03:30:42Z', author: { login: 'comment-user', + type: 'User', }, replies: { nodes: [], @@ -319,6 +321,7 @@ export const mockedGraphQLResponse: GraphQLSearch = { createdAt: '2022-02-21T18:26:27Z', author: { login: 'comment-user', + type: 'User', }, replies: { nodes: [ @@ -327,6 +330,7 @@ export const mockedGraphQLResponse: GraphQLSearch = { createdAt: '2022-02-23T00:57:58Z', author: { login: 'reply-user', + type: 'User', }, }, ], @@ -337,6 +341,7 @@ export const mockedGraphQLResponse: GraphQLSearch = { createdAt: '2022-02-23T00:57:49Z', author: { login: 'comment-user', + type: 'User', }, replies: { nodes: [], @@ -347,6 +352,7 @@ export const mockedGraphQLResponse: GraphQLSearch = { createdAt: '2022-02-27T01:22:20Z', author: { login: 'comment-user', + type: 'User', }, replies: { nodes: [ @@ -355,6 +361,7 @@ export const mockedGraphQLResponse: GraphQLSearch = { createdAt: '2022-03-05T17:43:52Z', author: { login: 'reply-user', + type: 'User', }, }, ], @@ -365,6 +372,7 @@ export const mockedGraphQLResponse: GraphQLSearch = { createdAt: '2022-03-04T20:39:44Z', author: { login: 'comment-user', + type: 'User', }, replies: { nodes: [ @@ -373,6 +381,7 @@ export const mockedGraphQLResponse: GraphQLSearch = { createdAt: '2022-03-05T17:41:04Z', author: { login: 'reply-user', + type: 'User', }, }, ], @@ -383,6 +392,7 @@ export const mockedGraphQLResponse: GraphQLSearch = { createdAt: '2022-03-05T11:05:42Z', author: { login: 'comment-user', + type: 'User', }, replies: { nodes: [ @@ -391,6 +401,7 @@ export const mockedGraphQLResponse: GraphQLSearch = { createdAt: '2022-03-05T17:41:44Z', author: { login: 'reply-user', + type: 'User', }, }, ], @@ -412,6 +423,7 @@ export const mockedGraphQLResponse: GraphQLSearch = { createdAt: '2022-02-20T18:33:39Z', author: { login: 'comment-user', + type: 'User', }, replies: { nodes: [], @@ -422,6 +434,7 @@ export const mockedGraphQLResponse: GraphQLSearch = { createdAt: '2022-02-21T03:30:42Z', author: { login: 'comment-user', + type: 'User', }, replies: { nodes: [], @@ -432,6 +445,7 @@ export const mockedGraphQLResponse: GraphQLSearch = { createdAt: '2022-02-21T18:26:27Z', author: { login: 'comment-user', + type: 'User', }, replies: { nodes: [ @@ -440,6 +454,7 @@ export const mockedGraphQLResponse: GraphQLSearch = { createdAt: '2022-02-23T00:57:58Z', author: { login: 'reply-user', + type: 'User', }, }, ], @@ -450,6 +465,7 @@ export const mockedGraphQLResponse: GraphQLSearch = { createdAt: '2022-02-23T00:57:49Z', author: { login: 'comment-user', + type: 'User', }, replies: { nodes: [], @@ -460,6 +476,7 @@ export const mockedGraphQLResponse: GraphQLSearch = { createdAt: '2022-02-27T01:22:20Z', author: { login: 'comment-user', + type: 'User', }, replies: { nodes: [ @@ -468,6 +485,7 @@ export const mockedGraphQLResponse: GraphQLSearch = { createdAt: '2022-03-05T17:43:52Z', author: { login: 'reply-user', + type: 'User', }, }, ], @@ -478,6 +496,7 @@ export const mockedGraphQLResponse: GraphQLSearch = { createdAt: '2022-03-04T20:39:44Z', author: { login: 'comment-user', + type: 'User', }, replies: { nodes: [ @@ -486,6 +505,7 @@ export const mockedGraphQLResponse: GraphQLSearch = { createdAt: '2022-03-05T17:41:04Z', author: { login: 'reply-user', + type: 'User', }, }, ], @@ -496,6 +516,7 @@ export const mockedGraphQLResponse: GraphQLSearch = { createdAt: '2022-03-05T11:05:42Z', author: { login: 'comment-user', + type: 'User', }, replies: { nodes: [ @@ -504,6 +525,7 @@ export const mockedGraphQLResponse: GraphQLSearch = { createdAt: '2022-03-05T17:41:44Z', author: { login: 'reply-user', + type: 'User', }, }, ], diff --git a/src/context/App.test.tsx b/src/context/App.test.tsx index 6f704f9f1..63d0e8bfa 100644 --- a/src/context/App.test.tsx +++ b/src/context/App.test.tsx @@ -290,6 +290,7 @@ describe('context/App.tsx', () => { participating: true, playSound: true, showNotifications: true, + showBots: true, colors: null, markAsDoneOnOpen: false, }, @@ -328,6 +329,7 @@ describe('context/App.tsx', () => { participating: false, playSound: true, showNotifications: true, + showBots: true, colors: null, markAsDoneOnOpen: false, }, diff --git a/src/context/App.tsx b/src/context/App.tsx index 2f7b8487e..3da7ba375 100644 --- a/src/context/App.tsx +++ b/src/context/App.tsx @@ -34,6 +34,7 @@ export const defaultSettings: SettingsState = { participating: false, playSound: true, showNotifications: true, + showBots: true, openAtStartup: false, appearance: Appearance.SYSTEM, colors: null, @@ -91,7 +92,7 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => { useEffect(() => { fetchNotifications(accounts, settings); - }, [settings.participating]); + }, [settings.participating, settings.showBots]); useEffect(() => { fetchNotifications(accounts, settings); diff --git a/src/hooks/useNotifications.test.ts b/src/hooks/useNotifications.test.ts index bfa23ffe0..549861c74 100644 --- a/src/hooks/useNotifications.test.ts +++ b/src/hooks/useNotifications.test.ts @@ -303,16 +303,30 @@ describe('hooks/useNotifications.ts', () => { nock('https://api.github.com') .get('/3') - .reply(200, { state: 'closed', merged: true }); + .reply(200, { + state: 'closed', + merged: true, + user: { + login: 'some-user', + type: 'User', + }, + }); nock('https://api.github.com') .get('/3/comments') - .reply(200, { user: { login: 'some-user' } }); + .reply(200, { user: { login: 'some-commenter', type: 'User' } }); nock('https://api.github.com') .get('/4') - .reply(200, { state: 'closed', merged: false }); + .reply(200, { + state: 'closed', + merged: false, + user: { + login: 'some-user', + type: 'User', + }, + }); nock('https://api.github.com') .get('/4/comments') - .reply(200, { user: { login: 'some-user' } }); + .reply(200, { user: { login: 'some-commenter', type: 'User' } }); const { result } = renderHook(() => useNotifications(true)); @@ -332,6 +346,84 @@ describe('hooks/useNotifications.ts', () => { expect(result.current.notifications[0].notifications.length).toBe(6); }); }); + + describe('showBots', () => { + it('should hide bot notifications when set to false', async () => { + const accounts: AuthState = { + ...mockAccounts, + enterpriseAccounts: [], + user: mockedUser, + }; + + const notifications = [ + { + id: 1, + subject: { + title: 'This is an Issue.', + type: 'Issue', + url: 'https://api.github.com/1', + latest_comment_url: null, + }, + repository: { + full_name: 'some/repo', + }, + }, + { + id: 2, + subject: { + title: 'This is a Pull Request.', + type: 'PullRequest', + url: 'https://api.github.com/2', + latest_comment_url: null, + }, + repository: { + full_name: 'some/repo', + }, + }, + ]; + + nock('https://api.github.com') + .get('/notifications?participating=false') + .reply(200, notifications); + nock('https://api.github.com') + .get('/1') + .reply(200, { + state: 'closed', + merged: true, + user: { + login: 'some-user', + type: 'User', + }, + }); + nock('https://api.github.com') + .get('/2') + .reply(200, { + state: 'closed', + merged: false, + user: { + login: 'some-bot', + type: 'Bot', + }, + }); + + const { result } = renderHook(() => useNotifications(true)); + + act(() => { + result.current.fetchNotifications(accounts, { + ...mockSettings, + showBots: false, + }); + }); + + expect(result.current.isFetching).toBe(true); + + await waitFor(() => { + expect(result.current.notifications[0].hostname).toBe('github.com'); + }); + + expect(result.current.notifications[0].notifications.length).toBe(1); + }); + }); }); describe('removeNotificationFromState', () => { diff --git a/src/hooks/useNotifications.ts b/src/hooks/useNotifications.ts index 5da2b46d5..2bb7f15b5 100644 --- a/src/hooks/useNotifications.ts +++ b/src/hooks/useNotifications.ts @@ -128,32 +128,48 @@ export const useNotifications = (colors: boolean): NotificationsState => { data.map(async (accountNotifications) => { return { hostname: accountNotifications.hostname, - notifications: await axios.all( - accountNotifications.notifications.map( - async (notification: Notification) => { - const isEnterprise = isEnterpriseHost( - accountNotifications.hostname, - ); - const token = isEnterprise - ? getEnterpriseAccountToken( - accountNotifications.hostname, - accounts.enterpriseAccounts, - ) - : accounts.token; - - const additionalSubjectDetails = - await getGitifySubjectDetails(notification, token); - - return { - ...notification, - subject: { - ...notification.subject, - ...additionalSubjectDetails, - }, - }; - }, - ), - ), + notifications: await axios + .all( + accountNotifications.notifications.map( + async (notification: Notification) => { + const isEnterprise = isEnterpriseHost( + accountNotifications.hostname, + ); + const token = isEnterprise + ? getEnterpriseAccountToken( + accountNotifications.hostname, + accounts.enterpriseAccounts, + ) + : accounts.token; + + const additionalSubjectDetails = + await getGitifySubjectDetails( + notification, + token, + ); + + return { + ...notification, + subject: { + ...notification.subject, + ...additionalSubjectDetails, + }, + }; + }, + ), + ) + .then((notifications) => { + return notifications.filter((notification) => { + if ( + !settings.showBots && + notification.subject?.user.type === 'Bot' + ) { + return false; + } + + return true; + }); + }), }; }), ) diff --git a/src/routes/Settings.test.tsx b/src/routes/Settings.test.tsx index d0b71382b..3e36c9da1 100644 --- a/src/routes/Settings.test.tsx +++ b/src/routes/Settings.test.tsx @@ -127,6 +127,34 @@ describe('routes/Settings.tsx', () => { expect(updateSetting).toHaveBeenCalledWith('participating', false); }); + it('should toggle the showBots checkbox', async () => { + let getByLabelText; + + await act(async () => { + const { getByLabelText: getByLabelTextLocal } = render( + + + + + , + ); + getByLabelText = getByLabelTextLocal; + }); + + fireEvent.click(getByLabelText('Show notifications from Bot accounts'), { + target: { checked: true }, + }); + + expect(updateSetting).toHaveBeenCalledTimes(1); + expect(updateSetting).toHaveBeenCalledWith('showBots', false); + }); + it('should toggle the playSound checkbox', async () => { let getByLabelText; diff --git a/src/routes/Settings.tsx b/src/routes/Settings.tsx index eaff01b8b..66201d2c9 100644 --- a/src/routes/Settings.tsx +++ b/src/routes/Settings.tsx @@ -146,6 +146,12 @@ export const SettingsRoute: React.FC = () => { updateSetting('showNotifications', evt.target.checked) } /> + updateSetting('showBots', evt.target.checked)} + /> +
+
+ +
+
+ +
+
diff --git a/src/types.ts b/src/types.ts index 5e0eee68b..a3147c0f7 100644 --- a/src/types.ts +++ b/src/types.ts @@ -10,6 +10,7 @@ export interface SettingsState { participating: boolean; playSound: boolean; showNotifications: boolean; + showBots: boolean; openAtStartup: boolean; appearance: Appearance; colors: boolean | null; diff --git a/src/typesGithub.ts b/src/typesGithub.ts index 2c640ef86..01afa62cb 100644 --- a/src/typesGithub.ts +++ b/src/typesGithub.ts @@ -137,10 +137,12 @@ export interface User { export interface SubjectUser { login: string; + type: string; } export interface DiscussionAuthor { login: string; + type: string; } export interface Repository { diff --git a/src/utils/helpers.ts b/src/utils/helpers.ts index 91d1dd9f3..f78324288 100644 --- a/src/utils/helpers.ts +++ b/src/utils/helpers.ts @@ -160,6 +160,7 @@ export async function fetchDiscussion( createdAt author { login + type } } diff --git a/src/utils/subject.ts b/src/utils/subject.ts index e651c1c14..8fb565b59 100644 --- a/src/utils/subject.ts +++ b/src/utils/subject.ts @@ -10,6 +10,7 @@ import { PullRequest, PullRequestStateType, ReleaseComments, + SubjectUser, User, WorkflowRunAttributes, } from '../typesGithub'; @@ -110,9 +111,12 @@ async function getGitifySubjectForDiscussion( const latestDiscussionComment = getLatestDiscussionComment( discussion.comments.nodes, ); - let discussionUser = null; + let discussionUser: SubjectUser = null; if (latestDiscussionComment) { - discussionUser = latestDiscussionComment.author; + discussionUser = { + login: latestDiscussionComment.author.login, + type: latestDiscussionComment.author.type, + }; } return { @@ -135,6 +139,7 @@ async function getGitifySubjectForIssue( state: issue.state_reason ?? issue.state, user: { login: issueCommentUser?.login ?? issue.user.login, + type: issueCommentUser?.type ?? issue.user.type, }, }; } @@ -160,6 +165,7 @@ async function getGitifySubjectForPullRequest( state: prState, user: { login: prCommentUser?.login ?? pr.user.login, + type: prCommentUser?.type ?? pr.user.type, }, }; } @@ -174,6 +180,7 @@ async function getGitifySubjectForRelease( state: null, user: { login: releaseCommentUser.login, + type: releaseCommentUser.type, }, }; }