diff --git a/src/renderer/components/Sidebar.tsx b/src/renderer/components/Sidebar.tsx index bb47aca2d..eb5d5fccb 100644 --- a/src/renderer/components/Sidebar.tsx +++ b/src/renderer/components/Sidebar.tsx @@ -22,7 +22,7 @@ import { openGitHubNotifications, openGitHubPulls, } from '../utils/links'; -import { getNotificationCount } from '../utils/notifications'; +import { getNotificationCount } from '../utils/notifications/notifications'; import { LogoIcon } from './icons/LogoIcon'; export const Sidebar: FC = () => { diff --git a/src/renderer/context/App.test.tsx b/src/renderer/context/App.test.tsx index 654f0e872..f642a1754 100644 --- a/src/renderer/context/App.test.tsx +++ b/src/renderer/context/App.test.tsx @@ -7,7 +7,7 @@ import { mockSingleNotification } from '../utils/api/__mocks__/response-mocks'; import * as apiRequests from '../utils/api/request'; import * as comms from '../utils/comms'; import { Constants } from '../utils/constants'; -import * as notifications from '../utils/notifications'; +import * as notifications from '../utils/notifications/notifications'; import * as storage from '../utils/storage'; import { AppContext, AppProvider, defaultSettings } from './App'; diff --git a/src/renderer/context/App.tsx b/src/renderer/context/App.tsx index eade5b0da..952f4656c 100644 --- a/src/renderer/context/App.tsx +++ b/src/renderer/context/App.tsx @@ -47,7 +47,7 @@ import { updateTrayTitle, } from '../utils/comms'; import { Constants } from '../utils/constants'; -import { getNotificationCount } from '../utils/notifications'; +import { getNotificationCount } from '../utils/notifications/notifications'; import { clearState, loadState, saveState } from '../utils/storage'; import { getColorModeFromTheme, setTheme } from '../utils/theme'; import { zoomPercentageToLevel } from '../utils/zoom'; diff --git a/src/renderer/hooks/useNotifications.ts b/src/renderer/hooks/useNotifications.ts index d75d0b9b0..e4ecb443a 100644 --- a/src/renderer/hooks/useNotifications.ts +++ b/src/renderer/hooks/useNotifications.ts @@ -15,11 +15,11 @@ import { markNotificationThreadAsRead, } from '../utils/api/client'; import { isMarkAsDoneFeatureSupported } from '../utils/features'; +import { triggerNativeNotifications } from '../utils/notifications/native'; import { getAllNotifications, setTrayIconColor, - triggerNativeNotifications, -} from '../utils/notifications'; +} from '../utils/notifications/notifications'; import { removeNotifications } from '../utils/notifications/remove'; interface NotificationsState { diff --git a/src/renderer/routes/Notifications.tsx b/src/renderer/routes/Notifications.tsx index 9860fe5f6..d9cfb473e 100644 --- a/src/renderer/routes/Notifications.tsx +++ b/src/renderer/routes/Notifications.tsx @@ -6,7 +6,7 @@ import { AccountNotifications } from '../components/notifications/AccountNotific import { AppContext } from '../context/App'; import { getAccountUUID } from '../utils/auth/utils'; import { Errors } from '../utils/errors'; -import { getNotificationCount } from '../utils/notifications'; +import { getNotificationCount } from '../utils/notifications/notifications'; export const NotificationsRoute: FC = () => { const { notifications, status, globalError, settings } = diff --git a/src/renderer/utils/notifications.test.ts b/src/renderer/utils/notifications.test.ts deleted file mode 100644 index f8f7aaa7b..000000000 --- a/src/renderer/utils/notifications.test.ts +++ /dev/null @@ -1,203 +0,0 @@ -import { - mockAccountNotifications, - mockSingleAccountNotifications, -} from '../__mocks__/notifications-mocks'; -import { partialMockNotification } from '../__mocks__/partial-mocks'; -import { mockAuth, mockSettings } from '../__mocks__/state-mocks'; -import { defaultSettings } from '../context/App'; -import type { Link, SettingsState } from '../types'; -import { mockGitHubNotifications } from './api/__mocks__/response-mocks'; -import * as comms from './comms'; -import * as links from './links'; -import * as notificationsHelpers from './notifications'; -import { filterNotifications } from './notifications'; - -describe('renderer/utils/notifications.ts', () => { - afterEach(() => { - jest.clearAllMocks(); - }); - - it('should raise a notification (settings - on)', () => { - const settings: SettingsState = { - ...defaultSettings, - playSound: true, - showNotifications: true, - }; - - jest.spyOn(notificationsHelpers, 'raiseNativeNotification'); - jest.spyOn(notificationsHelpers, 'raiseSoundNotification'); - - notificationsHelpers.triggerNativeNotifications( - [], - mockAccountNotifications, - { - auth: mockAuth, - settings, - }, - ); - - expect(notificationsHelpers.raiseNativeNotification).toHaveBeenCalledTimes( - 1, - ); - expect(notificationsHelpers.raiseSoundNotification).toHaveBeenCalledTimes( - 1, - ); - }); - - it('should not raise a notification (settings - off)', () => { - const settings = { - ...defaultSettings, - playSound: false, - showNotifications: false, - }; - - jest.spyOn(notificationsHelpers, 'raiseNativeNotification'); - jest.spyOn(notificationsHelpers, 'raiseSoundNotification'); - - notificationsHelpers.triggerNativeNotifications( - [], - mockAccountNotifications, - { - auth: mockAuth, - settings, - }, - ); - - expect(notificationsHelpers.raiseNativeNotification).not.toHaveBeenCalled(); - expect(notificationsHelpers.raiseSoundNotification).not.toHaveBeenCalled(); - }); - - it('should not raise a notification or play a sound (no new notifications)', () => { - const settings = { - ...defaultSettings, - playSound: true, - showNotifications: true, - }; - - jest.spyOn(notificationsHelpers, 'raiseNativeNotification'); - jest.spyOn(notificationsHelpers, 'raiseSoundNotification'); - - notificationsHelpers.triggerNativeNotifications( - mockSingleAccountNotifications, - mockSingleAccountNotifications, - { auth: mockAuth, settings }, - ); - - expect(notificationsHelpers.raiseNativeNotification).not.toHaveBeenCalled(); - expect(notificationsHelpers.raiseSoundNotification).not.toHaveBeenCalled(); - }); - - it('should not raise a notification (because of 0(zero) notifications)', () => { - const settings = { - ...defaultSettings, - playSound: true, - showNotifications: true, - }; - - jest.spyOn(notificationsHelpers, 'raiseNativeNotification'); - jest.spyOn(notificationsHelpers, 'raiseSoundNotification'); - - notificationsHelpers.triggerNativeNotifications([], [], { - auth: mockAuth, - settings, - }); - notificationsHelpers.triggerNativeNotifications([], [], { - auth: mockAuth, - settings, - }); - - expect(notificationsHelpers.raiseNativeNotification).not.toHaveBeenCalled(); - expect(notificationsHelpers.raiseSoundNotification).not.toHaveBeenCalled(); - }); - - it('should click on a native notification (with 1 notification)', () => { - const hideWindowMock = jest.spyOn(comms, 'hideWindow'); - jest.spyOn(links, 'openNotification'); - - const nativeNotification: Notification = - notificationsHelpers.raiseNativeNotification([ - mockGitHubNotifications[0], - ]); - nativeNotification.onclick(null); - - expect(links.openNotification).toHaveBeenCalledTimes(1); - expect(links.openNotification).toHaveBeenLastCalledWith( - mockGitHubNotifications[0], - ); - expect(hideWindowMock).toHaveBeenCalledTimes(1); - }); - - it('should click on a native notification (with more than 1 notification)', () => { - const showWindowMock = jest.spyOn(comms, 'showWindow'); - - const nativeNotification = notificationsHelpers.raiseNativeNotification( - mockGitHubNotifications, - ); - nativeNotification.onclick(null); - - expect(showWindowMock).toHaveBeenCalledTimes(1); - }); - - it('should play a sound', () => { - jest.spyOn(window.Audio.prototype, 'play'); - notificationsHelpers.raiseSoundNotification(); - expect(window.Audio.prototype.play).toHaveBeenCalledTimes(1); - }); - - describe('filterNotifications', () => { - const mockNotifications = [ - partialMockNotification({ - title: 'User authored notification', - user: { - login: 'user', - html_url: 'https://github.com/user' as Link, - avatar_url: - 'https://avatars.githubusercontent.com/u/133795385?s=200&v=4' as Link, - type: 'User', - }, - }), - partialMockNotification({ - title: 'Bot authored notification', - user: { - login: 'bot', - html_url: 'https://github.com/bot' as Link, - avatar_url: - 'https://avatars.githubusercontent.com/u/133795385?s=200&v=4' as Link, - type: 'Bot', - }, - }), - ]; - - it('should hide bot notifications when set to true', async () => { - const result = filterNotifications(mockNotifications, { - ...mockSettings, - hideBots: true, - }); - - expect(result.length).toBe(1); - expect(result).toEqual([mockNotifications[0]]); - }); - - it('should show bot notifications when set to false', async () => { - const result = filterNotifications(mockNotifications, { - ...mockSettings, - hideBots: false, - }); - - expect(result.length).toBe(2); - expect(result).toEqual(mockNotifications); - }); - - it('should filter notifications by reasons when provided', async () => { - mockNotifications[0].reason = 'subscribed'; - mockNotifications[1].reason = 'manual'; - const result = filterNotifications(mockNotifications, { - ...mockSettings, - filterReasons: ['manual'], - }); - - expect(result.length).toBe(1); - expect(result).toEqual([mockNotifications[1]]); - }); - }); -}); diff --git a/src/renderer/utils/notifications.ts b/src/renderer/utils/notifications.ts deleted file mode 100644 index 3ff75abd0..000000000 --- a/src/renderer/utils/notifications.ts +++ /dev/null @@ -1,239 +0,0 @@ -import path from 'node:path'; - -import { APPLICATION } from '../../shared/constants'; -import { logError, logWarn } from '../../shared/logger'; -import { isWindows } from '../../shared/platform'; -import type { - AccountNotifications, - GitifyState, - SettingsState, -} from '../types'; -import { type GitifySubject, Notification } from '../typesGitHub'; -import { listNotificationsForAuthenticatedUser } from './api/client'; -import { determineFailureType } from './api/errors'; -import { getAccountUUID } from './auth/utils'; -import { hideWindow, showWindow, updateTrayIcon } from './comms'; -import { Constants } from './constants'; -import { openNotification } from './links'; -import { getGitifySubjectDetails } from './subject'; - -export function setTrayIconColor(notifications: AccountNotifications[]) { - const allNotificationsCount = getNotificationCount(notifications); - - updateTrayIcon(allNotificationsCount); -} - -export function getNotificationCount(notifications: AccountNotifications[]) { - return notifications.reduce( - (memo, acc) => memo + acc.notifications.length, - 0, - ); -} - -export const triggerNativeNotifications = ( - previousNotifications: AccountNotifications[], - newNotifications: AccountNotifications[], - state: GitifyState, -) => { - const diffNotifications = newNotifications - .map((accountNotifications) => { - const accountPreviousNotifications = previousNotifications.find( - (item) => - getAccountUUID(item.account) === - getAccountUUID(accountNotifications.account), - ); - - if (!accountPreviousNotifications) { - return accountNotifications.notifications; - } - - const accountPreviousNotificationsIds = - accountPreviousNotifications.notifications.map((item) => item.id); - - const accountNewNotifications = accountNotifications.notifications.filter( - (item) => { - return !accountPreviousNotificationsIds.includes(`${item.id}`); - }, - ); - - return accountNewNotifications; - }) - .reduce((acc, val) => acc.concat(val), []); - - setTrayIconColor(newNotifications); - - // If there are no new notifications just stop there - if (!diffNotifications.length) { - return; - } - - if (state.settings.playSound) { - raiseSoundNotification(); - } - - if (state.settings.showNotifications) { - raiseNativeNotification(diffNotifications); - } -}; - -export const raiseNativeNotification = (notifications: Notification[]) => { - let title: string; - let body: string; - - if (notifications.length === 1) { - const notification = notifications[0]; - title = isWindows() ? '' : notification.repository.full_name; - body = notification.subject.title; - } else { - title = APPLICATION.NAME; - body = `You have ${notifications.length} notifications.`; - } - - const nativeNotification = new Notification(title, { - body, - silent: true, - }); - - nativeNotification.onclick = () => { - if (notifications.length === 1) { - hideWindow(); - openNotification(notifications[0]); - } else { - showWindow(); - } - }; - - return nativeNotification; -}; - -export const raiseSoundNotification = () => { - const audio = new Audio( - path.join( - __dirname, - '..', - 'assets', - 'sounds', - Constants.NOTIFICATION_SOUND, - ), - ); - audio.volume = 0.2; - audio.play(); -}; - -function getNotifications(state: GitifyState) { - return state.auth.accounts.map((account) => { - return { - account, - notifications: listNotificationsForAuthenticatedUser( - account, - state.settings, - ), - }; - }); -} - -export async function getAllNotifications( - state: GitifyState, -): Promise { - const responses = await Promise.all([...getNotifications(state)]); - - const notifications: AccountNotifications[] = await Promise.all( - responses - .filter((response) => !!response) - .map(async (accountNotifications) => { - try { - let notifications = ( - await accountNotifications.notifications - ).data.map((notification: Notification) => ({ - ...notification, - account: accountNotifications.account, - })); - - notifications = await enrichNotifications(notifications, state); - - notifications = filterNotifications(notifications, state.settings); - - return { - account: accountNotifications.account, - notifications: notifications, - error: null, - }; - } catch (err) { - logError( - 'getAllNotifications', - 'error occurred while fetching account notifications', - err, - ); - - return { - account: accountNotifications.account, - notifications: [], - error: determineFailureType(err), - }; - } - }), - ); - - return notifications; -} - -export async function enrichNotifications( - notifications: Notification[], - state: GitifyState, -): Promise { - if (!state.settings.detailedNotifications) { - return notifications; - } - - const enrichedNotifications = await Promise.all( - notifications.map(async (notification: Notification) => { - let additionalSubjectDetails: GitifySubject = {}; - - try { - additionalSubjectDetails = await getGitifySubjectDetails(notification); - } catch (err) { - logError( - 'enrichNotifications', - 'failed to enrich notification details for', - err, - notification, - ); - - logWarn( - 'enrichNotifications', - 'Continuing with base notification details', - ); - } - - return { - ...notification, - subject: { - ...notification.subject, - ...additionalSubjectDetails, - }, - }; - }), - ); - - return enrichedNotifications; -} - -export function filterNotifications( - notifications: Notification[], - settings: SettingsState, -): Notification[] { - return notifications.filter((notification) => { - if (settings.hideBots && notification.subject?.user?.type === 'Bot') { - return false; - } - - if ( - settings.filterReasons.length > 0 && - !settings.filterReasons.includes(notification.reason) - ) { - return false; - } - - return true; - }); -} diff --git a/src/renderer/utils/notifications/filter.test.ts b/src/renderer/utils/notifications/filter.test.ts new file mode 100644 index 000000000..8ddfc8eff --- /dev/null +++ b/src/renderer/utils/notifications/filter.test.ts @@ -0,0 +1,67 @@ +import { partialMockNotification } from '../../__mocks__/partial-mocks'; +import { mockSettings } from '../../__mocks__/state-mocks'; +import type { Link } from '../../types'; +import { filterNotifications } from './filter'; + +describe('renderer/utils/notifications/filter.ts', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('filterNotifications', () => { + const mockNotifications = [ + partialMockNotification({ + title: 'User authored notification', + user: { + login: 'user', + html_url: 'https://github.com/user' as Link, + avatar_url: + 'https://avatars.githubusercontent.com/u/133795385?s=200&v=4' as Link, + type: 'User', + }, + }), + partialMockNotification({ + title: 'Bot authored notification', + user: { + login: 'bot', + html_url: 'https://github.com/bot' as Link, + avatar_url: + 'https://avatars.githubusercontent.com/u/133795385?s=200&v=4' as Link, + type: 'Bot', + }, + }), + ]; + + it('should hide bot notifications when set to true', async () => { + const result = filterNotifications(mockNotifications, { + ...mockSettings, + hideBots: true, + }); + + expect(result.length).toBe(1); + expect(result).toEqual([mockNotifications[0]]); + }); + + it('should show bot notifications when set to false', async () => { + const result = filterNotifications(mockNotifications, { + ...mockSettings, + hideBots: false, + }); + + expect(result.length).toBe(2); + expect(result).toEqual(mockNotifications); + }); + + it('should filter notifications by reasons when provided', async () => { + mockNotifications[0].reason = 'subscribed'; + mockNotifications[1].reason = 'manual'; + const result = filterNotifications(mockNotifications, { + ...mockSettings, + filterReasons: ['manual'], + }); + + expect(result.length).toBe(1); + expect(result).toEqual([mockNotifications[1]]); + }); + }); +}); diff --git a/src/renderer/utils/notifications/filter.ts b/src/renderer/utils/notifications/filter.ts new file mode 100644 index 000000000..d35ac2879 --- /dev/null +++ b/src/renderer/utils/notifications/filter.ts @@ -0,0 +1,22 @@ +import type { SettingsState } from '../../types'; +import type { Notification } from '../../typesGitHub'; + +export function filterNotifications( + notifications: Notification[], + settings: SettingsState, +): Notification[] { + return notifications.filter((notification) => { + if (settings.hideBots && notification.subject?.user?.type === 'Bot') { + return false; + } + + if ( + settings.filterReasons.length > 0 && + !settings.filterReasons.includes(notification.reason) + ) { + return false; + } + + return true; + }); +} diff --git a/src/renderer/utils/notifications/native.test.ts b/src/renderer/utils/notifications/native.test.ts new file mode 100644 index 000000000..f30cc9c8f --- /dev/null +++ b/src/renderer/utils/notifications/native.test.ts @@ -0,0 +1,137 @@ +import { + mockAccountNotifications, + mockSingleAccountNotifications, +} from '../../__mocks__/notifications-mocks'; +import { mockAuth } from '../../__mocks__/state-mocks'; +import { defaultSettings } from '../../context/App'; +import type { SettingsState } from '../../types'; +import { mockGitHubNotifications } from '../api/__mocks__/response-mocks'; +import * as comms from '../comms'; +import * as links from '../links'; +import * as native from './native'; + +describe('renderer/utils/notifications/native.ts', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('triggerNativeNotifications', () => { + it('should raise a native notification (settings - on)', () => { + const settings: SettingsState = { + ...defaultSettings, + playSound: true, + showNotifications: true, + }; + + jest.spyOn(native, 'raiseNativeNotification'); + jest.spyOn(native, 'raiseSoundNotification'); + + native.triggerNativeNotifications([], mockAccountNotifications, { + auth: mockAuth, + settings, + }); + + expect(native.raiseNativeNotification).toHaveBeenCalledTimes(1); + expect(native.raiseSoundNotification).toHaveBeenCalledTimes(1); + }); + + it('should not raise a native notification (settings - off)', () => { + const settings = { + ...defaultSettings, + playSound: false, + showNotifications: false, + }; + + jest.spyOn(native, 'raiseNativeNotification'); + jest.spyOn(native, 'raiseSoundNotification'); + + native.triggerNativeNotifications([], mockAccountNotifications, { + auth: mockAuth, + settings, + }); + + expect(native.raiseNativeNotification).not.toHaveBeenCalled(); + expect(native.raiseSoundNotification).not.toHaveBeenCalled(); + }); + + it('should not raise a native notification or play a sound (no new notifications)', () => { + const settings = { + ...defaultSettings, + playSound: true, + showNotifications: true, + }; + + jest.spyOn(native, 'raiseNativeNotification'); + jest.spyOn(native, 'raiseSoundNotification'); + + native.triggerNativeNotifications( + mockSingleAccountNotifications, + mockSingleAccountNotifications, + { auth: mockAuth, settings }, + ); + + expect(native.raiseNativeNotification).not.toHaveBeenCalled(); + expect(native.raiseSoundNotification).not.toHaveBeenCalled(); + }); + + it('should not raise a native notification (because of 0(zero) notifications)', () => { + const settings = { + ...defaultSettings, + playSound: true, + showNotifications: true, + }; + + jest.spyOn(native, 'raiseNativeNotification'); + jest.spyOn(native, 'raiseSoundNotification'); + + native.triggerNativeNotifications([], [], { + auth: mockAuth, + settings, + }); + native.triggerNativeNotifications([], [], { + auth: mockAuth, + settings, + }); + + expect(native.raiseNativeNotification).not.toHaveBeenCalled(); + expect(native.raiseSoundNotification).not.toHaveBeenCalled(); + }); + }); + + describe('raiseNativeNotification', () => { + it('should click on a native notification (with 1 notification)', () => { + const hideWindowMock = jest.spyOn(comms, 'hideWindow'); + jest.spyOn(links, 'openNotification'); + + const nativeNotification: Notification = native.raiseNativeNotification([ + mockGitHubNotifications[0], + ]); + nativeNotification.onclick(null); + + expect(links.openNotification).toHaveBeenCalledTimes(1); + expect(links.openNotification).toHaveBeenLastCalledWith( + mockGitHubNotifications[0], + ); + expect(hideWindowMock).toHaveBeenCalledTimes(1); + }); + + it('should click on a native notification (with more than 1 notification)', () => { + const showWindowMock = jest.spyOn(comms, 'showWindow'); + + const nativeNotification = native.raiseNativeNotification( + mockGitHubNotifications, + ); + nativeNotification.onclick(null); + + expect(showWindowMock).toHaveBeenCalledTimes(1); + }); + }); + + describe('raiseSoundNotification', () => { + it('should play a sound', () => { + jest.spyOn(window.Audio.prototype, 'play'); + native.raiseSoundNotification(); + expect(window.Audio.prototype.play).toHaveBeenCalledTimes(1); + }); + }); +}); diff --git a/src/renderer/utils/notifications/native.ts b/src/renderer/utils/notifications/native.ts new file mode 100644 index 000000000..cda7f6367 --- /dev/null +++ b/src/renderer/utils/notifications/native.ts @@ -0,0 +1,101 @@ +import path from 'node:path'; + +import { APPLICATION } from '../../../shared/constants'; +import { isWindows } from '../../../shared/platform'; +import type { AccountNotifications, GitifyState } from '../../types'; +import { Notification } from '../../typesGitHub'; +import { getAccountUUID } from '../auth/utils'; +import { hideWindow, showWindow } from '../comms'; +import { Constants } from '../constants'; +import { openNotification } from '../links'; +import { setTrayIconColor } from './notifications'; + +export const triggerNativeNotifications = ( + previousNotifications: AccountNotifications[], + newNotifications: AccountNotifications[], + state: GitifyState, +) => { + const diffNotifications = newNotifications + .map((accountNotifications) => { + const accountPreviousNotifications = previousNotifications.find( + (item) => + getAccountUUID(item.account) === + getAccountUUID(accountNotifications.account), + ); + + if (!accountPreviousNotifications) { + return accountNotifications.notifications; + } + + const accountPreviousNotificationsIds = + accountPreviousNotifications.notifications.map((item) => item.id); + + const accountNewNotifications = accountNotifications.notifications.filter( + (item) => { + return !accountPreviousNotificationsIds.includes(`${item.id}`); + }, + ); + + return accountNewNotifications; + }) + .reduce((acc, val) => acc.concat(val), []); + + setTrayIconColor(newNotifications); + + // If there are no new notifications just stop there + if (!diffNotifications.length) { + return; + } + + if (state.settings.playSound) { + raiseSoundNotification(); + } + + if (state.settings.showNotifications) { + raiseNativeNotification(diffNotifications); + } +}; + +export const raiseNativeNotification = (notifications: Notification[]) => { + let title: string; + let body: string; + + if (notifications.length === 1) { + const notification = notifications[0]; + title = isWindows() ? '' : notification.repository.full_name; + body = notification.subject.title; + } else { + title = APPLICATION.NAME; + body = `You have ${notifications.length} notifications.`; + } + + const nativeNotification = new Notification(title, { + body, + silent: true, + }); + + nativeNotification.onclick = () => { + if (notifications.length === 1) { + hideWindow(); + openNotification(notifications[0]); + } else { + showWindow(); + } + }; + + return nativeNotification; +}; + +export const raiseSoundNotification = () => { + const audio = new Audio( + path.join( + __dirname, + '..', + 'assets', + 'sounds', + Constants.NOTIFICATION_SOUND, + ), + ); + audio.volume = 0.2; + audio.play(); +}; diff --git a/src/renderer/utils/notifications/notifications.test.ts b/src/renderer/utils/notifications/notifications.test.ts new file mode 100644 index 000000000..9c789c5f6 --- /dev/null +++ b/src/renderer/utils/notifications/notifications.test.ts @@ -0,0 +1,14 @@ +import { mockSingleAccountNotifications } from '../../__mocks__/notifications-mocks'; +import { getNotificationCount } from './notifications'; + +describe('renderer/utils/notifications/notifications.ts', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + it('getNotificationCount', () => { + const result = getNotificationCount(mockSingleAccountNotifications); + + expect(result).toBe(1); + }); +}); diff --git a/src/renderer/utils/notifications/notifications.ts b/src/renderer/utils/notifications/notifications.ts new file mode 100644 index 000000000..559f447f1 --- /dev/null +++ b/src/renderer/utils/notifications/notifications.ts @@ -0,0 +1,119 @@ +import { logError, logWarn } from '../../../shared/logger'; +import type { AccountNotifications, GitifyState } from '../../types'; +import type { GitifySubject, Notification } from '../../typesGitHub'; +import { listNotificationsForAuthenticatedUser } from '../api/client'; +import { determineFailureType } from '../api/errors'; +import { updateTrayIcon } from '../comms'; +import { getGitifySubjectDetails } from '../subject'; +import { filterNotifications } from './filter'; + +export function setTrayIconColor(notifications: AccountNotifications[]) { + const allNotificationsCount = getNotificationCount(notifications); + + updateTrayIcon(allNotificationsCount); +} + +export function getNotificationCount(notifications: AccountNotifications[]) { + return notifications.reduce( + (memo, acc) => memo + acc.notifications.length, + 0, + ); +} + +function getNotifications(state: GitifyState) { + return state.auth.accounts.map((account) => { + return { + account, + notifications: listNotificationsForAuthenticatedUser( + account, + state.settings, + ), + }; + }); +} + +export async function getAllNotifications( + state: GitifyState, +): Promise { + const responses = await Promise.all([...getNotifications(state)]); + + const notifications: AccountNotifications[] = await Promise.all( + responses + .filter((response) => !!response) + .map(async (accountNotifications) => { + try { + let notifications = ( + await accountNotifications.notifications + ).data.map((notification: Notification) => ({ + ...notification, + account: accountNotifications.account, + })); + + notifications = await enrichNotifications(notifications, state); + + notifications = filterNotifications(notifications, state.settings); + + return { + account: accountNotifications.account, + notifications: notifications, + error: null, + }; + } catch (err) { + logError( + 'getAllNotifications', + 'error occurred while fetching account notifications', + err, + ); + + return { + account: accountNotifications.account, + notifications: [], + error: determineFailureType(err), + }; + } + }), + ); + + return notifications; +} + +export async function enrichNotifications( + notifications: Notification[], + state: GitifyState, +): Promise { + if (!state.settings.detailedNotifications) { + return notifications; + } + + const enrichedNotifications = await Promise.all( + notifications.map(async (notification: Notification) => { + let additionalSubjectDetails: GitifySubject = {}; + + try { + additionalSubjectDetails = await getGitifySubjectDetails(notification); + } catch (err) { + logError( + 'enrichNotifications', + 'failed to enrich notification details for', + err, + notification, + ); + + logWarn( + 'enrichNotifications', + 'Continuing with base notification details', + ); + } + + return { + ...notification, + subject: { + ...notification.subject, + ...additionalSubjectDetails, + }, + }; + }), + ); + + return enrichedNotifications; +}