From c3fa5cc03e74187514a1bf40d6f724aedee0eb68 Mon Sep 17 00:00:00 2001 From: Afonso Jorge Ramos Date: Thu, 2 Apr 2026 04:29:35 +0200 Subject: [PATCH 1/8] feat: enable strict mode in TypeScript config --- tsconfig.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tsconfig.json b/tsconfig.json index 9d3795184..c15fb822a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,7 +11,7 @@ "outDir": "./build/", "types": ["vitest/globals", "vite/client"], - "strict": false, + "strict": true, "esModuleInterop": true, "moduleResolution": "Node", "ignoreDeprecations": "6.0", From 8c217ea113d65dd607f5d1f4521a1ea1109f5e15 Mon Sep 17 00:00:00 2001 From: Afonso Jorge Ramos Date: Thu, 2 Apr 2026 04:31:49 +0200 Subject: [PATCH 2/8] fix: resolve strict mode errors in main process --- src/main/config.test.ts | 6 +++--- src/main/events.ts | 14 +++++++------- src/main/handlers/app.test.ts | 4 ++-- src/main/handlers/storage.test.ts | 2 +- src/main/handlers/storage.ts | 4 ++-- src/main/handlers/system.test.ts | 4 ++-- src/main/handlers/system.ts | 2 +- src/main/handlers/tray.test.ts | 12 ++++++------ src/main/lifecycle/first-run.ts | 6 +++++- src/main/lifecycle/reset.ts | 4 ++++ src/main/lifecycle/window.test.ts | 10 ++++++---- src/main/lifecycle/window.ts | 16 ++++++++++------ src/main/menu.test.ts | 14 ++++++++------ src/main/updater.ts | 4 ++-- src/main/utils.test.ts | 2 +- src/main/utils.ts | 2 +- 16 files changed, 61 insertions(+), 45 deletions(-) diff --git a/src/main/config.test.ts b/src/main/config.test.ts index f0c4412cd..119eae6e7 100644 --- a/src/main/config.test.ts +++ b/src/main/config.test.ts @@ -33,8 +33,8 @@ describe('main/config.ts', () => { expect(WindowConfig.resizable).toBe(false); expect(WindowConfig.skipTaskbar).toBe(true); expect(WindowConfig.webPreferences).toBeDefined(); - expect(WindowConfig.webPreferences.contextIsolation).toBe(true); - expect(WindowConfig.webPreferences.nodeIntegration).toBe(false); - expect(WindowConfig.webPreferences.backgroundThrottling).toBe(false); + expect(WindowConfig.webPreferences?.contextIsolation).toBe(true); + expect(WindowConfig.webPreferences?.nodeIntegration).toBe(false); + expect(WindowConfig.webPreferences?.backgroundThrottling).toBe(false); }); }); diff --git a/src/main/events.ts b/src/main/events.ts index 9c7901433..b5ab910b8 100644 --- a/src/main/events.ts +++ b/src/main/events.ts @@ -10,11 +10,11 @@ import type { EventData, EventType } from '../shared/events'; * @param event - The IPC channel/event name to listen on. * @param listener - Callback invoked when the event is received. */ -export function onMainEvent( +export function onMainEvent( event: EventType, - listener: (event: Electron.IpcMainEvent, args: EventData) => void, + listener: (event: Electron.IpcMainEvent, args: T) => void, ) { - ipcMain.on(event, listener); + ipcMain.on(event, listener as Parameters[1]); } /** @@ -24,14 +24,14 @@ export function onMainEvent( * @param event - The IPC channel/event name to handle. * @param listener - Callback whose return value is sent back to the renderer. */ -export function handleMainEvent( +export function handleMainEvent( event: EventType, listener: ( event: Electron.IpcMainInvokeEvent, - data: EventData, + data: T, ) => unknown | Promise, ) { - ipcMain.handle(event, listener); + ipcMain.handle(event, listener as Parameters[1]); } /** @@ -46,5 +46,5 @@ export function sendRendererEvent( event: EventType, data?: string, ) { - mb.window.webContents.send(event, data); + mb.window?.webContents.send(event, data); } diff --git a/src/main/handlers/app.test.ts b/src/main/handlers/app.test.ts index 3ac14c617..85b6d577e 100644 --- a/src/main/handlers/app.test.ts +++ b/src/main/handlers/app.test.ts @@ -44,10 +44,10 @@ describe('main/handlers/app.ts', () => { registerAppHandlers(menubar); const registeredHandlers = handleMock.mock.calls.map( - (call: [string]) => call[0], + (call: unknown[]) => call[0], ); const registeredEvents = onMock.mock.calls.map( - (call: [string]) => call[0], + (call: unknown[]) => call[0], ); expect(registeredHandlers).toContain(EVENTS.VERSION); diff --git a/src/main/handlers/storage.test.ts b/src/main/handlers/storage.test.ts index e59ff4102..91f54f2c6 100644 --- a/src/main/handlers/storage.test.ts +++ b/src/main/handlers/storage.test.ts @@ -29,7 +29,7 @@ describe('main/handlers/storage.ts', () => { registerStorageHandlers(); const registeredHandlers = handleMock.mock.calls.map( - (call: [string]) => call[0], + (call: unknown[]) => call[0], ); expect(registeredHandlers).toContain(EVENTS.SAFE_STORAGE_ENCRYPT); diff --git a/src/main/handlers/storage.ts b/src/main/handlers/storage.ts index b87b20ddc..417caf382 100644 --- a/src/main/handlers/storage.ts +++ b/src/main/handlers/storage.ts @@ -22,11 +22,11 @@ export function registerStorageHandlers(): void { handleMainEvent(EVENTS.SAFE_STORAGE_DECRYPT, (_, value: string) => { try { return safeStorage.decryptString(Buffer.from(value, 'base64')); - } catch (err) { + } catch (err: unknown) { logError( 'main:safe-storage-decrypt', 'Failed to decrypt value - data may be from old build', - err, + err as Error, ); throw err; } diff --git a/src/main/handlers/system.test.ts b/src/main/handlers/system.test.ts index 2351c212e..331efc4dc 100644 --- a/src/main/handlers/system.test.ts +++ b/src/main/handlers/system.test.ts @@ -63,9 +63,9 @@ describe('main/handlers/system.ts', () => { it('registers expected system IPC event handlers', () => { registerSystemHandlers(menubar); - const onEvents = onMock.mock.calls.map((call: [string]) => call[0]); + const onEvents = onMock.mock.calls.map((call: unknown[]) => call[0]); const handleEvents = handleMock.mock.calls.map( - (call: [string]) => call[0], + (call: unknown[]) => call[0], ); expect(onEvents).toContain(EVENTS.OPEN_EXTERNAL); diff --git a/src/main/handlers/system.ts b/src/main/handlers/system.ts index 2baabcb59..17b23d265 100644 --- a/src/main/handlers/system.ts +++ b/src/main/handlers/system.ts @@ -24,7 +24,7 @@ export function registerSystemHandlers(mb: Menubar): void { let lastRegisteredAccelerator: string | null = null; const toggleWindow = () => { - if (mb.window.isVisible()) { + if (mb.window?.isVisible()) { mb.hideWindow(); } else { mb.showWindow(); diff --git a/src/main/handlers/tray.test.ts b/src/main/handlers/tray.test.ts index 36c477370..4c4584258 100644 --- a/src/main/handlers/tray.test.ts +++ b/src/main/handlers/tray.test.ts @@ -38,7 +38,7 @@ describe('main/handlers/tray.ts', () => { registerTrayHandlers(menubar); const registeredEvents = onMock.mock.calls.map( - (call: [string]) => call[0], + (call: unknown[]) => call[0], ); expect(registeredEvents).toContain(EVENTS.USE_ALTERNATE_IDLE_ICON); @@ -55,7 +55,7 @@ describe('main/handlers/tray.ts', () => { registerTrayHandlers(menubar); const updateColorHandler = onMock.mock.calls.find( - (call: [string]) => call[0] === EVENTS.UPDATE_ICON_COLOR, + (call: unknown[]) => call[0] === EVENTS.UPDATE_ICON_COLOR, )?.[1]; updateColorHandler?.({}, 5); @@ -66,7 +66,7 @@ describe('main/handlers/tray.ts', () => { registerTrayHandlers(menubar); const updateColorHandler = onMock.mock.calls.find( - (call: [string]) => call[0] === EVENTS.UPDATE_ICON_COLOR, + (call: unknown[]) => call[0] === EVENTS.UPDATE_ICON_COLOR, )?.[1]; updateColorHandler?.({}, 0); @@ -77,7 +77,7 @@ describe('main/handlers/tray.ts', () => { registerTrayHandlers(menubar); const updateColorHandler = onMock.mock.calls.find( - (call: [string]) => call[0] === EVENTS.UPDATE_ICON_COLOR, + (call: unknown[]) => call[0] === EVENTS.UPDATE_ICON_COLOR, )?.[1]; updateColorHandler?.({}, 3); @@ -88,7 +88,7 @@ describe('main/handlers/tray.ts', () => { registerTrayHandlers(menubar); const updateColorHandler = onMock.mock.calls.find( - (call: [string]) => call[0] === EVENTS.UPDATE_ICON_COLOR, + (call: unknown[]) => call[0] === EVENTS.UPDATE_ICON_COLOR, )?.[1]; updateColorHandler?.({}, -1); @@ -99,7 +99,7 @@ describe('main/handlers/tray.ts', () => { registerTrayHandlers(menubar); const updateTitleHandler = onMock.mock.calls.find( - (call: [string]) => call[0] === EVENTS.UPDATE_ICON_TITLE, + (call: unknown[]) => call[0] === EVENTS.UPDATE_ICON_TITLE, )?.[1]; updateTitleHandler?.({}, '5'); diff --git a/src/main/lifecycle/first-run.ts b/src/main/lifecycle/first-run.ts index d6bf36ef0..2204b9c65 100644 --- a/src/main/lifecycle/first-run.ts +++ b/src/main/lifecycle/first-run.ts @@ -71,7 +71,11 @@ function checkAndMarkFirstRun(): boolean { fs.writeFileSync(configPath, ''); } catch (err) { - logError('checkAndMarkFirstRun', 'Unable to write firstRun file', err); + logError( + 'checkAndMarkFirstRun', + 'Unable to write firstRun file', + err as Error, + ); } return true; diff --git a/src/main/lifecycle/reset.ts b/src/main/lifecycle/reset.ts index d353cffbd..7d2b85c47 100644 --- a/src/main/lifecycle/reset.ts +++ b/src/main/lifecycle/reset.ts @@ -13,6 +13,10 @@ import { sendRendererEvent } from '../events'; * @param mb - The menubar instance used for the dialog parent window and quit. */ export function resetApp(mb: Menubar): void { + if (!mb.window) { + return; + } + const cancelButtonId = 0; const resetButtonId = 1; diff --git a/src/main/lifecycle/window.test.ts b/src/main/lifecycle/window.test.ts index 7a64efe7c..7c688d19d 100644 --- a/src/main/lifecycle/window.test.ts +++ b/src/main/lifecycle/window.test.ts @@ -37,21 +37,23 @@ describe('main/lifecycle/window.ts', () => { it('configureWindowEvents returns early if no window', () => { const mbNoWindow = { ...menubar, window: null }; - expect(() => configureWindowEvents(mbNoWindow as Menubar)).not.toThrow(); + expect(() => + configureWindowEvents(mbNoWindow as unknown as Menubar), + ).not.toThrow(); }); it('configureWindowEvents registers webContents event listeners', () => { configureWindowEvents(menubar); - expect(menubar.window.webContents.on).toHaveBeenCalledWith( + expect(menubar.window?.webContents.on).toHaveBeenCalledWith( 'before-input-event', expect.any(Function), ); - expect(menubar.window.webContents.on).toHaveBeenCalledWith( + expect(menubar.window?.webContents.on).toHaveBeenCalledWith( 'devtools-opened', expect.any(Function), ); - expect(menubar.window.webContents.on).toHaveBeenCalledWith( + expect(menubar.window?.webContents.on).toHaveBeenCalledWith( 'devtools-closed', expect.any(Function), ); diff --git a/src/main/lifecycle/window.ts b/src/main/lifecycle/window.ts index a3392b5e5..357de035a 100644 --- a/src/main/lifecycle/window.ts +++ b/src/main/lifecycle/window.ts @@ -26,10 +26,12 @@ export function configureWindowEvents(mb: Menubar): void { * When DevTools is opened, resize and center the window for better visibility and allow resizing. */ mb.window.webContents.on('devtools-opened', () => { - mb.window.setSize(800, 600); - mb.window.center(); - mb.window.resizable = true; - mb.window.setAlwaysOnTop(true); + mb.window?.setSize(800, 600); + mb.window?.center(); + if (mb.window) { + mb.window.resizable = true; + } + mb.window?.setAlwaysOnTop(true); }); /** @@ -37,8 +39,10 @@ export function configureWindowEvents(mb: Menubar): void { */ mb.window.webContents.on('devtools-closed', () => { const trayBounds = mb.tray.getBounds(); - mb.window.setSize(WindowConfig.width, WindowConfig.height); + mb.window?.setSize(WindowConfig.width ?? 500, WindowConfig.height ?? 400); mb.positioner.move('trayCenter', trayBounds); - mb.window.resizable = false; + if (mb.window) { + mb.window.resizable = false; + } }); } diff --git a/src/main/menu.test.ts b/src/main/menu.test.ts index 6a178e34f..10b7ff958 100644 --- a/src/main/menu.test.ts +++ b/src/main/menu.test.ts @@ -200,7 +200,7 @@ describe('main/menu.ts', () => { const cfg = getMenuItemConfigByLabel('Check for updates'); expect(cfg).toBeDefined(); - cfg.click(); + cfg?.click?.(); expect(autoUpdater.checkForUpdatesAndNotify).toHaveBeenCalled(); }); @@ -209,7 +209,7 @@ describe('main/menu.ts', () => { const cfg = getMenuItemConfigByLabel('Restart to install update'); expect(cfg).toBeDefined(); - cfg.click(); + cfg?.click?.(); expect(autoUpdater.quitAndInstall).toHaveBeenCalled(); }); @@ -220,7 +220,7 @@ describe('main/menu.ts', () => { (item) => item?.label === 'Developer', ) as TemplateItem; expect(devEntry).toBeDefined(); - const submenu = devEntry.submenu; + const submenu = devEntry.submenu ?? []; const clickByLabel = (label: string) => submenu.find((i) => i.label === label)?.click?.(); @@ -242,7 +242,7 @@ describe('main/menu.ts', () => { it('website menu item opens external URL', () => { const template = buildAndGetTemplate(); const item = template.find((i) => i.label === 'Visit Website'); - item.click(); + item?.click?.(); expect(shell.openExternal).toHaveBeenCalledWith(APPLICATION.WEBSITE); }); @@ -250,7 +250,7 @@ describe('main/menu.ts', () => { const template = buildAndGetTemplate(); const item = template.find((i) => i.label === `Quit ${APPLICATION.NAME}`); - item.click(); + item?.click?.(); expect(menubar.app.quit).toHaveBeenCalled(); }); @@ -260,7 +260,9 @@ describe('main/menu.ts', () => { const devEntry = template.find( (item) => item?.label === 'Developer', ) as TemplateItem; - const reloadItem = devEntry.submenu.find((i) => i.role === 'reload'); + const reloadItem = (devEntry.submenu ?? []).find( + (i) => i.role === 'reload', + ); expect(reloadItem?.accelerator).toBe('CommandOrControl+R'); }); diff --git a/src/main/updater.ts b/src/main/updater.ts index 941d12586..f726c76a7 100644 --- a/src/main/updater.ts +++ b/src/main/updater.ts @@ -87,7 +87,7 @@ export default class AppUpdater { this.setTooltipWithStatus('A new update is ready to install'); this.menuBuilder.setUpdateAvailableMenuVisibility(false); this.menuBuilder.setUpdateReadyForInstallMenuVisibility(true); - this.showUpdateReadyDialog(event.releaseName); + this.showUpdateReadyDialog(event.releaseName ?? undefined); }); autoUpdater.on('update-not-available', () => { @@ -196,7 +196,7 @@ export default class AppUpdater { * * @param releaseName - The version string shown in the dialog message. */ - private showUpdateReadyDialog(releaseName: string) { + private showUpdateReadyDialog(releaseName?: string) { const dialogOpts: MessageBoxOptions = { type: 'info', buttons: ['Restart', 'Later'], diff --git a/src/main/utils.test.ts b/src/main/utils.test.ts index 941fb132d..cfd17469c 100644 --- a/src/main/utils.test.ts +++ b/src/main/utils.test.ts @@ -88,7 +88,7 @@ describe('main/utils', () => { }); it('logs error when log path is unavailable', () => { - fileGetFileMock.mockReturnValueOnce(null); + fileGetFileMock.mockReturnValueOnce(null as unknown as { path: string }); openLogsDirectory(); expect(logErrorMock).toHaveBeenCalledWith( 'openLogsDirectory', diff --git a/src/main/utils.ts b/src/main/utils.ts index cbd175d2b..bb0c45892 100644 --- a/src/main/utils.ts +++ b/src/main/utils.ts @@ -30,7 +30,7 @@ export function takeScreenshot(mb: Menubar) { `${dateStr}-${APPLICATION.NAME}-screenshot.png`, ); - mb.window.capturePage().then((img) => { + mb.window?.capturePage().then((img) => { fs.writeFile(capturedPicFilePath, img.toPNG(), () => logInfo('takeScreenshot', `Screenshot saved ${capturedPicFilePath}`), ); From 970d4cb993037300da332523a3e9ee44ae91afd9 Mon Sep 17 00:00:00 2001 From: Afonso Jorge Ramos Date: Thu, 2 Apr 2026 04:33:20 +0200 Subject: [PATCH 3/8] fix: resolve strict mode errors in renderer process --- src/renderer/App.tsx | 2 +- src/renderer/__mocks__/notifications-mocks.ts | 6 +-- src/renderer/components/AllRead.test.tsx | 4 +- src/renderer/components/Oops.test.tsx | 10 +++-- .../avatars/AvatarWithFallback.test.tsx | 4 +- src/renderer/components/layout/Centered.tsx | 2 +- .../components/layout/EmojiSplash.test.tsx | 4 +- .../components/layout/EmojiSplash.tsx | 2 +- .../components/metrics/CommentsPill.test.tsx | 2 +- .../components/metrics/MetricGroup.tsx | 2 +- .../components/metrics/MilestonePill.test.tsx | 2 +- .../components/metrics/ReviewsPill.test.tsx | 2 +- .../AccountNotifications.test.tsx | 12 +++--- .../notifications/AccountNotifications.tsx | 6 +-- .../notifications/NotificationFooter.test.tsx | 6 +-- .../notifications/NotificationFooter.tsx | 2 +- .../notifications/NotificationHeader.test.tsx | 2 +- .../notifications/NotificationTitle.tsx | 2 +- .../components/primitives/CustomCounter.tsx | 3 +- .../components/primitives/EmojiText.test.tsx | 2 +- .../components/settings/SettingsFooter.tsx | 2 +- src/renderer/context/App.test.tsx | 2 +- src/renderer/context/App.tsx | 2 +- .../hooks/timers/useInactivityTimer.ts | 5 ++- src/renderer/hooks/timers/useIntervalTimer.ts | 7 +++- src/renderer/hooks/useNotifications.test.ts | 16 ++++---- src/renderer/hooks/useNotifications.ts | 26 +++++++------ src/renderer/index.tsx | 2 +- src/renderer/routes/Accounts.tsx | 22 +++++------ src/renderer/routes/LoginWithDeviceFlow.tsx | 4 +- .../routes/LoginWithOAuthApp.test.tsx | 6 +-- src/renderer/routes/LoginWithOAuthApp.tsx | 2 +- .../LoginWithPersonalAccessToken.test.tsx | 4 +- .../routes/LoginWithPersonalAccessToken.tsx | 2 +- src/renderer/routes/Notifications.test.tsx | 2 +- src/renderer/routes/Notifications.tsx | 2 +- src/renderer/utils/api/client.ts | 12 +++--- src/renderer/utils/api/errors.test.ts | 6 ++- src/renderer/utils/api/errors.ts | 4 +- src/renderer/utils/api/features.test.ts | 4 +- .../utils/api/graphql/MergeQueryBuilder.ts | 13 ++++--- src/renderer/utils/api/graphql/utils.test.ts | 10 ++--- src/renderer/utils/api/transform.ts | 5 ++- src/renderer/utils/api/utils.ts | 2 +- src/renderer/utils/auth/flows.ts | 4 +- src/renderer/utils/auth/utils.test.ts | 8 ++-- src/renderer/utils/auth/utils.ts | 8 ++-- src/renderer/utils/core/errors.test.ts | 6 +-- .../utils/notifications/filters/filter.ts | 4 +- .../notifications/filters/userType.test.ts | 8 ++-- .../utils/notifications/filters/userType.ts | 2 +- .../utils/notifications/formatters.test.ts | 10 +++-- .../utils/notifications/formatters.ts | 2 +- .../notifications/handlers/checkSuite.test.ts | 24 ++++++------ .../notifications/handlers/checkSuite.ts | 12 +++--- .../notifications/handlers/commit.test.ts | 4 +- .../utils/notifications/handlers/commit.ts | 20 +++++----- .../notifications/handlers/default.test.ts | 4 +- .../notifications/handlers/discussion.ts | 24 ++++++++---- .../utils/notifications/handlers/issue.ts | 15 ++++++-- .../handlers/pullRequest.test.ts | 32 ++++++++-------- .../notifications/handlers/pullRequest.ts | 38 ++++++++++++------- .../notifications/handlers/release.test.ts | 2 +- .../utils/notifications/handlers/release.ts | 8 ++-- .../notifications/handlers/utils.test.ts | 4 +- .../utils/notifications/handlers/utils.ts | 4 +- .../handlers/workflowRun.test.ts | 4 +- .../notifications/handlers/workflowRun.ts | 8 ++-- .../utils/notifications/notifications.ts | 18 ++++----- src/renderer/utils/notifications/url.test.ts | 6 +-- src/renderer/utils/notifications/url.ts | 4 +- src/renderer/utils/system/audio.ts | 6 ++- src/renderer/utils/system/comms.test.ts | 2 +- src/renderer/utils/system/links.ts | 2 +- src/renderer/utils/system/native.test.ts | 2 +- src/renderer/utils/system/native.ts | 2 +- src/renderer/utils/ui/zoom.test.ts | 2 +- 77 files changed, 296 insertions(+), 242 deletions(-) diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index 30e67c88d..d7e7a91b5 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -26,7 +26,7 @@ import './App.css'; import { useAppContext } from './hooks/useAppContext'; -function RequireAuth({ children }) { +function RequireAuth({ children }: { children: React.ReactNode }) { const location = useLocation(); const { isLoggedIn } = useAppContext(); diff --git a/src/renderer/__mocks__/notifications-mocks.ts b/src/renderer/__mocks__/notifications-mocks.ts index 201c3802f..abe57cb6a 100644 --- a/src/renderer/__mocks__/notifications-mocks.ts +++ b/src/renderer/__mocks__/notifications-mocks.ts @@ -114,7 +114,7 @@ export const mockGitHubCloudGitifyNotifications: GitifyNotification[] = [ latestCommentUrl: 'https://api.github.com/repos/gitify-app/notifications-test/issues/comments/302885965' as Link, type: 'Issue', - reviews: null, + reviews: undefined, }, repository: mockGitHubRepository, display: { @@ -168,7 +168,7 @@ export const mockGithubEnterpriseGitifyNotifications: GitifyNotification[] = [ latestCommentUrl: 'https://github.gitify.io/api/v3/repos/myorg/notifications-test/releases/3' as Link, type: 'Release', - reviews: null, + reviews: undefined, }, repository: mockEnterpriseRepository, display: { @@ -199,7 +199,7 @@ export const mockGithubEnterpriseGitifyNotifications: GitifyNotification[] = [ latestCommentUrl: 'https://github.gitify.io/api/v3/repos/myorg/notifications-test/issues/comments/21' as Link, type: 'PullRequest', - reviews: null, + reviews: undefined, }, repository: mockEnterpriseRepository, display: { diff --git a/src/renderer/components/AllRead.test.tsx b/src/renderer/components/AllRead.test.tsx index 3b2fc8af6..11e3aec6f 100644 --- a/src/renderer/components/AllRead.test.tsx +++ b/src/renderer/components/AllRead.test.tsx @@ -17,7 +17,7 @@ describe('renderer/components/AllRead.tsx', () => { }); }); - expect(tree.container).toMatchSnapshot(); + expect(tree!.container).toMatchSnapshot(); }); it('should render itself & its children - with filters', async () => { @@ -34,6 +34,6 @@ describe('renderer/components/AllRead.tsx', () => { }); }); - expect(tree.container).toMatchSnapshot(); + expect(tree!.container).toMatchSnapshot(); }); }); diff --git a/src/renderer/components/Oops.test.tsx b/src/renderer/components/Oops.test.tsx index c0c5ca20a..b4f2a0aee 100644 --- a/src/renderer/components/Oops.test.tsx +++ b/src/renderer/components/Oops.test.tsx @@ -5,6 +5,8 @@ import { PersonIcon } from '@primer/octicons-react'; import { navigateMock, renderWithProviders } from '../__helpers__/test-utils'; +import type { GitifyError } from '../types'; + import { Oops } from './Oops'; describe('renderer/components/Oops.tsx', () => { @@ -21,17 +23,19 @@ describe('renderer/components/Oops.tsx', () => { tree = renderWithProviders(); }); - expect(tree.container).toMatchSnapshot(); + expect(tree!.container).toMatchSnapshot(); }); it('should render itself & its children - fallback to unknown error', async () => { let tree: ReturnType | null = null; await act(async () => { - tree = renderWithProviders(); + tree = renderWithProviders( + , + ); }); - expect(tree.container).toMatchSnapshot(); + expect(tree!.container).toMatchSnapshot(); }); it('should render action buttons and navigate on click', async () => { diff --git a/src/renderer/components/avatars/AvatarWithFallback.test.tsx b/src/renderer/components/avatars/AvatarWithFallback.test.tsx index ca34744f5..f8e78c804 100644 --- a/src/renderer/components/avatars/AvatarWithFallback.test.tsx +++ b/src/renderer/components/avatars/AvatarWithFallback.test.tsx @@ -32,7 +32,7 @@ describe('renderer/components/avatars/AvatarWithFallback.tsx', () => { it('renders the fallback icon when no src url - human user', () => { const tree = renderWithProviders( - , + , ); expect(tree.container).toMatchSnapshot(); @@ -40,7 +40,7 @@ describe('renderer/components/avatars/AvatarWithFallback.tsx', () => { it('renders the fallback icon when no src url - non human user', () => { const tree = renderWithProviders( - , + , ); expect(tree.container).toMatchSnapshot(); diff --git a/src/renderer/components/layout/Centered.tsx b/src/renderer/components/layout/Centered.tsx index ae60a023f..f26c1b266 100644 --- a/src/renderer/components/layout/Centered.tsx +++ b/src/renderer/components/layout/Centered.tsx @@ -11,7 +11,7 @@ export const Centered: FC = (props: CenteredProps) => { return ( { }); }); - expect(tree.container).toMatchSnapshot(); + expect(tree!.container).toMatchSnapshot(); }); it('should render itself & its children - heading and sub-heading', async () => { @@ -34,6 +34,6 @@ describe('renderer/components/layout/EmojiSplash.tsx', () => { }); }); - expect(tree.container).toMatchSnapshot(); + expect(tree!.container).toMatchSnapshot(); }); }); diff --git a/src/renderer/components/layout/EmojiSplash.tsx b/src/renderer/components/layout/EmojiSplash.tsx index ee63d87ed..39099aedc 100644 --- a/src/renderer/components/layout/EmojiSplash.tsx +++ b/src/renderer/components/layout/EmojiSplash.tsx @@ -16,7 +16,7 @@ interface EmojiSplashProps { export const EmojiSplash: FC = (props: EmojiSplashProps) => { return ( - + { it('renders with no comments (null)', () => { - const props: CommentsPillProps = null; + const props = {} as CommentsPillProps; const tree = renderWithProviders(); diff --git a/src/renderer/components/metrics/MetricGroup.tsx b/src/renderer/components/metrics/MetricGroup.tsx index 1dde4cdca..99301a9fd 100644 --- a/src/renderer/components/metrics/MetricGroup.tsx +++ b/src/renderer/components/metrics/MetricGroup.tsx @@ -37,7 +37,7 @@ export const MetricGroup: FC = ({ notification }) => { - + diff --git a/src/renderer/components/metrics/MilestonePill.test.tsx b/src/renderer/components/metrics/MilestonePill.test.tsx index fa508d688..ead9994a1 100644 --- a/src/renderer/components/metrics/MilestonePill.test.tsx +++ b/src/renderer/components/metrics/MilestonePill.test.tsx @@ -4,7 +4,7 @@ import { MilestonePill, type MilestonePillProps } from './MilestonePill'; describe('renderer/components/metrics/MilestonePill.tsx', () => { it('renders with no milestone', () => { - const props: MilestonePillProps = { milestone: null }; + const props: MilestonePillProps = { milestone: undefined! }; const tree = renderWithProviders(); diff --git a/src/renderer/components/metrics/ReviewsPill.test.tsx b/src/renderer/components/metrics/ReviewsPill.test.tsx index 10fb9c59b..a6575f8fe 100644 --- a/src/renderer/components/metrics/ReviewsPill.test.tsx +++ b/src/renderer/components/metrics/ReviewsPill.test.tsx @@ -6,7 +6,7 @@ import { ReviewsPill, type ReviewsPillProps } from './ReviewsPill'; describe('renderer/components/metrics/ReviewsPill.tsx', () => { it('renders review pills when reviews exist', () => { const props: ReviewsPillProps = { - reviews: mockGitifyNotification.subject.reviews, + reviews: mockGitifyNotification.subject.reviews ?? [], }; const tree = renderWithProviders(); diff --git a/src/renderer/components/notifications/AccountNotifications.test.tsx b/src/renderer/components/notifications/AccountNotifications.test.tsx index 4e28c5c8f..0a497e598 100644 --- a/src/renderer/components/notifications/AccountNotifications.test.tsx +++ b/src/renderer/components/notifications/AccountNotifications.test.tsx @@ -31,7 +31,7 @@ describe('renderer/components/notifications/AccountNotifications.tsx', () => { settings: { ...mockSettings, groupBy: GroupBy.REPOSITORY }, }); - expect(tree.container).toMatchSnapshot(); + expect(tree!.container).toMatchSnapshot(); }); it('should render itself - group notifications by date', () => { @@ -46,7 +46,7 @@ describe('renderer/components/notifications/AccountNotifications.tsx', () => { settings: { ...mockSettings, groupBy: GroupBy.DATE }, }); - expect(tree.container).toMatchSnapshot(); + expect(tree!.container).toMatchSnapshot(); }); it('should render itself - no notifications', async () => { @@ -63,7 +63,7 @@ describe('renderer/components/notifications/AccountNotifications.tsx', () => { tree = renderWithProviders(); }); - expect(tree.container).toMatchSnapshot(); + expect(tree!.container).toMatchSnapshot(); }); it('should render itself - account error for single account', async () => { @@ -86,7 +86,7 @@ describe('renderer/components/notifications/AccountNotifications.tsx', () => { }); }); - expect(tree.container).toMatchSnapshot(); + expect(tree!.container).toMatchSnapshot(); }); it('should render itself - account error for multiple accounts', async () => { @@ -107,7 +107,7 @@ describe('renderer/components/notifications/AccountNotifications.tsx', () => { tree = renderWithProviders(); }); - expect(tree.container).toMatchSnapshot(); + expect(tree!.container).toMatchSnapshot(); }); it('should open profile when clicked', async () => { @@ -188,6 +188,6 @@ describe('renderer/components/notifications/AccountNotifications.tsx', () => { const tree = renderWithProviders(); - expect(tree.container).toMatchSnapshot(); + expect(tree!.container).toMatchSnapshot(); }); }); diff --git a/src/renderer/components/notifications/AccountNotifications.tsx b/src/renderer/components/notifications/AccountNotifications.tsx index 87fcf7531..850ab0705 100644 --- a/src/renderer/components/notifications/AccountNotifications.tsx +++ b/src/renderer/components/notifications/AccountNotifications.tsx @@ -100,10 +100,10 @@ export const AccountNotifications: FC = ( variant="invisible" > diff --git a/src/renderer/components/notifications/NotificationFooter.test.tsx b/src/renderer/components/notifications/NotificationFooter.test.tsx index 5d8863705..06ffe2b9c 100644 --- a/src/renderer/components/notifications/NotificationFooter.test.tsx +++ b/src/renderer/components/notifications/NotificationFooter.test.tsx @@ -57,7 +57,7 @@ describe('renderer/components/notifications/NotificationFooter.tsx', () => { it('should default to known avatar if no user found', async () => { const mockNotification = mockGitifyNotification; - mockNotification.subject.user = null; + mockNotification.subject.user = undefined; const props: NotificationFooterProps = { notification: mockNotification, @@ -85,7 +85,7 @@ describe('renderer/components/notifications/NotificationFooter.tsx', () => { 'https://avatars.githubusercontent.com/u/583231?v=4' as Link, type: 'User' as GitifyNotificationUser['type'], }, - reviews: null, + reviews: undefined, }, }, }; @@ -96,7 +96,7 @@ describe('renderer/components/notifications/NotificationFooter.tsx', () => { expect(openExternalLinkSpy).toHaveBeenCalledTimes(1); expect(openExternalLinkSpy).toHaveBeenCalledWith( - props.notification.subject.user.htmlUrl, + props.notification.subject.user!.htmlUrl, ); }); }); diff --git a/src/renderer/components/notifications/NotificationFooter.tsx b/src/renderer/components/notifications/NotificationFooter.tsx index e54c40122..783206d89 100644 --- a/src/renderer/components/notifications/NotificationFooter.tsx +++ b/src/renderer/components/notifications/NotificationFooter.tsx @@ -30,7 +30,7 @@ export const NotificationFooter: FC = ({ onClick={(event: MouseEvent) => { // Don't trigger onClick of parent element. event.stopPropagation(); - openUserProfile(notification.subject.user); + openUserProfile(notification.subject.user!); }} title={notification.subject.user.login} type="button" diff --git a/src/renderer/components/notifications/NotificationHeader.test.tsx b/src/renderer/components/notifications/NotificationHeader.test.tsx index 10320c940..956ee326e 100644 --- a/src/renderer/components/notifications/NotificationHeader.test.tsx +++ b/src/renderer/components/notifications/NotificationHeader.test.tsx @@ -59,7 +59,7 @@ describe('renderer/components/notifications/NotificationHeader.tsx', () => { const props: NotificationHeaderProps = { notification: { ...mockGitifyNotification, - subject: { ...mockGitifyNotification.subject, number: null }, + subject: { ...mockGitifyNotification.subject, number: undefined }, }, }; diff --git a/src/renderer/components/notifications/NotificationTitle.tsx b/src/renderer/components/notifications/NotificationTitle.tsx index 6c6028d11..cfcd19a17 100644 --- a/src/renderer/components/notifications/NotificationTitle.tsx +++ b/src/renderer/components/notifications/NotificationTitle.tsx @@ -18,7 +18,7 @@ export const NotificationTitle: FC = ({ const parts = parseInlineCode(title); return ( - + {parts.map((part) => ( = ({ const baseStyles = 'px-2 py-0.25 rounded-full text-[10px] font-medium leading-none min-w-[16px] text-gitify-counter-text'; - const schemeStyles = { + const schemeStyles: Record = { primary: 'bg-gitify-counter-primary', secondary: 'bg-gitify-counter-secondary', + empty: '', }; return {value}; diff --git a/src/renderer/components/primitives/EmojiText.test.tsx b/src/renderer/components/primitives/EmojiText.test.tsx index 7cc893d80..140ad767d 100644 --- a/src/renderer/components/primitives/EmojiText.test.tsx +++ b/src/renderer/components/primitives/EmojiText.test.tsx @@ -16,6 +16,6 @@ describe('renderer/components/primitives/EmojiText.tsx', () => { tree = renderWithProviders(); }); - expect(tree.container).toMatchSnapshot(); + expect(tree!.container).toMatchSnapshot(); }); }); diff --git a/src/renderer/components/settings/SettingsFooter.tsx b/src/renderer/components/settings/SettingsFooter.tsx index 41625fd11..9baf5e145 100644 --- a/src/renderer/components/settings/SettingsFooter.tsx +++ b/src/renderer/components/settings/SettingsFooter.tsx @@ -29,7 +29,7 @@ export const SettingsFooter: FC = () => { diff --git a/src/renderer/context/App.test.tsx b/src/renderer/context/App.test.tsx index 9da1629c0..17dd588ae 100644 --- a/src/renderer/context/App.test.tsx +++ b/src/renderer/context/App.test.tsx @@ -58,7 +58,7 @@ describe('renderer/context/App.tsx', () => { vi.useFakeTimers(); vi.mocked(useNotifications).mockReturnValue({ status: 'success', - globalError: null, + globalError: undefined, notifications: [], notificationCount: 0, hasNotifications: false, diff --git a/src/renderer/context/App.tsx b/src/renderer/context/App.tsx index 116e860f5..dd3630a42 100644 --- a/src/renderer/context/App.tsx +++ b/src/renderer/context/App.tsx @@ -93,7 +93,7 @@ export interface AppContextState { logoutFromAccount: (account: Account) => Promise; status: Status; - globalError: GitifyError; + globalError: GitifyError | undefined; notifications: AccountNotifications[]; notificationCount: number; diff --git a/src/renderer/hooks/timers/useInactivityTimer.ts b/src/renderer/hooks/timers/useInactivityTimer.ts index 161a1f4dd..1565c5fe8 100644 --- a/src/renderer/hooks/timers/useInactivityTimer.ts +++ b/src/renderer/hooks/timers/useInactivityTimer.ts @@ -11,7 +11,10 @@ const events = ['mousedown', 'keypress', 'click']; * @param callback - The function to call once inactivity exceeds `delay`. * @param delay - Inactivity timeout in milliseconds. */ -export const useInactivityTimer = (callback: () => void, delay: number) => { +export const useInactivityTimer = ( + callback: () => void, + delay: number | null, +) => { const savedCallback = useRef<(() => void) | null>(null); const timeoutRef = useRef | null>(null); diff --git a/src/renderer/hooks/timers/useIntervalTimer.ts b/src/renderer/hooks/timers/useIntervalTimer.ts index 1e98a5ee0..420e541e9 100644 --- a/src/renderer/hooks/timers/useIntervalTimer.ts +++ b/src/renderer/hooks/timers/useIntervalTimer.ts @@ -8,7 +8,10 @@ import { useEffect, useRef } from 'react'; * @param callback - Function to call on each interval tick. Always uses the latest reference. * @param delay - Interval duration in milliseconds. Pass `null` to disable. */ -export const useIntervalTimer = (callback: () => void, delay: number) => { +export const useIntervalTimer = ( + callback: () => void, + delay: number | null, +) => { const savedCallback = useRef<(() => void) | null>(null); // Remember the latest callback. @@ -19,7 +22,7 @@ export const useIntervalTimer = (callback: () => void, delay: number) => { // Set up the interval. useEffect(() => { function tick() { - savedCallback.current(); + savedCallback.current?.(); } if (delay !== null) { diff --git a/src/renderer/hooks/useNotifications.test.ts b/src/renderer/hooks/useNotifications.test.ts index d71df2328..4c24e492c 100644 --- a/src/renderer/hooks/useNotifications.test.ts +++ b/src/renderer/hooks/useNotifications.test.ts @@ -168,7 +168,7 @@ describe('renderer/hooks/useNotifications.ts', () => { expect(result.current.status).toBe('error'); }); - expect(result.current.globalError).toBeNull(); + expect(result.current.globalError).toBeUndefined(); }); it('should play sound when new notifications arrive and playSound is enabled', async () => { @@ -577,7 +577,7 @@ describe('renderer/hooks/useNotifications.ts', () => { vi.spyOn( apiClient, 'ignoreNotificationThreadSubscription', - ).mockResolvedValue(undefined); + ).mockResolvedValue(undefined as any); vi.spyOn(apiClient, 'markNotificationThreadAsRead').mockResolvedValue( undefined, @@ -603,7 +603,7 @@ describe('renderer/hooks/useNotifications.ts', () => { vi.spyOn( apiClient, 'ignoreNotificationThreadSubscription', - ).mockResolvedValue(undefined); + ).mockResolvedValue(undefined as any); vi.spyOn(apiClient, 'markNotificationThreadAsDone').mockResolvedValue( undefined, @@ -616,7 +616,7 @@ describe('renderer/hooks/useNotifications.ts', () => { { ...mockState, settings: { - ...mockState.settings, + ...mockState.settings!, markAsDoneOnUnsubscribe: true, }, }, @@ -662,7 +662,7 @@ describe('renderer/hooks/useNotifications.ts', () => { vi.spyOn( apiClient, 'ignoreNotificationThreadSubscription', - ).mockResolvedValue(undefined); + ).mockResolvedValue(undefined as any); vi.spyOn(apiClient, 'markNotificationThreadAsDone').mockResolvedValue( undefined, @@ -675,7 +675,7 @@ describe('renderer/hooks/useNotifications.ts', () => { { ...mockState, settings: { - ...mockState.settings, + ...mockState.settings!, markAsDoneOnUnsubscribe: true, fetchReadNotifications: true, }, @@ -695,7 +695,7 @@ describe('renderer/hooks/useNotifications.ts', () => { vi.spyOn( apiClient, 'ignoreNotificationThreadSubscription', - ).mockResolvedValue(undefined); + ).mockResolvedValue(undefined as any); vi.spyOn(apiClient, 'markNotificationThreadAsRead').mockResolvedValue( undefined, @@ -708,7 +708,7 @@ describe('renderer/hooks/useNotifications.ts', () => { { ...mockState, settings: { - ...mockState.settings, + ...mockState.settings!, markAsDoneOnUnsubscribe: false, fetchReadNotifications: true, }, diff --git a/src/renderer/hooks/useNotifications.ts b/src/renderer/hooks/useNotifications.ts index aff00b272..3e0bc8d42 100644 --- a/src/renderer/hooks/useNotifications.ts +++ b/src/renderer/hooks/useNotifications.ts @@ -33,7 +33,7 @@ import { raiseNativeNotification } from '../utils/system/native'; interface NotificationsState { status: Status; - globalError: GitifyError; + globalError: GitifyError | undefined; notifications: AccountNotifications[]; notificationCount: number; @@ -127,7 +127,9 @@ export const useNotifications = (): NotificationsState => { if (allAccountsHaveErrors) { const accountError = fetchedNotifications[0].error; setStatus('error'); - setGlobalError(allAccountErrorsAreSame ? accountError : null); + setGlobalError( + allAccountErrorsAreSame ? (accountError ?? undefined) : undefined, + ); return; } @@ -137,17 +139,17 @@ export const useNotifications = (): NotificationsState => { ); if (diffNotifications.length > 0) { - if (state.settings.playSound) { - raiseSoundNotification(state.settings.notificationVolume); + if (state.settings!.playSound) { + raiseSoundNotification(state.settings!.notificationVolume); } - if (state.settings.showNotifications) { + if (state.settings!.showNotifications) { raiseNativeNotification(diffNotifications); } } setStatus('success'); - setGlobalError(null); + setGlobalError(undefined); } finally { isFetchingRef.current = false; } @@ -168,7 +170,7 @@ export const useNotifications = (): NotificationsState => { const updatedNotifications = removeNotificationsForAccount( readNotifications[0].account, - state.settings, + state.settings!, readNotifications, notifications, ); @@ -178,7 +180,7 @@ export const useNotifications = (): NotificationsState => { rendererLogError( 'markNotificationsAsRead', 'Error occurred while marking notifications as read', - err, + err as Error, ); } @@ -204,7 +206,7 @@ export const useNotifications = (): NotificationsState => { const updatedNotifications = removeNotificationsForAccount( doneNotifications[0].account, - state.settings, + state.settings!, doneNotifications, notifications, ); @@ -214,7 +216,7 @@ export const useNotifications = (): NotificationsState => { rendererLogError( 'markNotificationsAsDone', 'Error occurred while marking notifications as done', - err, + err as Error, ); } @@ -233,7 +235,7 @@ export const useNotifications = (): NotificationsState => { notification.id, ); - if (state.settings.markAsDoneOnUnsubscribe) { + if (state.settings!.markAsDoneOnUnsubscribe) { await markNotificationsAsDone(state, [notification]); } else { await markNotificationsAsRead(state, [notification]); @@ -242,7 +244,7 @@ export const useNotifications = (): NotificationsState => { rendererLogError( 'unsubscribeNotification', 'Error occurred while unsubscribing from notification thread', - err, + err as Error, notification, ); } diff --git a/src/renderer/index.tsx b/src/renderer/index.tsx index 23722ef71..9b6cb2142 100644 --- a/src/renderer/index.tsx +++ b/src/renderer/index.tsx @@ -3,6 +3,6 @@ import { createRoot } from 'react-dom/client'; import { App } from './App'; const container = document.getElementById('root'); -const root = createRoot(container); +const root = createRoot(container!); root.render(); diff --git a/src/renderer/routes/Accounts.tsx b/src/renderer/routes/Accounts.tsx index b4ac3aa6a..c5466138e 100644 --- a/src/renderer/routes/Accounts.tsx +++ b/src/renderer/routes/Accounts.tsx @@ -85,7 +85,7 @@ export const AccountsRoute: FC = () => { } catch (err) { setRefreshErrorStates((prev) => ({ ...prev, - [accountUUID]: determineFailureType(err), + [accountUUID]: determineFailureType(err as Error), })); } @@ -153,8 +153,8 @@ export const AccountsRoute: FC = () => { {auth.accounts.map((account, i) => { - const AuthMethodIcon = getAuthMethodIcon(account.method); - const PlatformIcon = getPlatformIcon(account.platform); + const AuthMethodIcon = getAuthMethodIcon(account.method)!; + const PlatformIcon = getPlatformIcon(account.platform)!; const accountUUID = getAccountUUID(account); const accountError = getAccountError(account); const hasBadCredentials = accountError === Errors.BAD_CREDENTIALS; @@ -172,10 +172,10 @@ export const AccountsRoute: FC = () => { title="Open account profile ↗" > @@ -191,7 +191,7 @@ export const AccountsRoute: FC = () => { align="center" direction="horizontal" gap="condensed" - hidden={!account.user.name} + hidden={!account.user!.name} > {account.user?.name} @@ -252,7 +252,7 @@ export const AccountsRoute: FC = () => { {!hasBadCredentials && ( ( { {hasBadCredentials && ( handleReAuthenticate(account)} @@ -292,7 +292,7 @@ export const AccountsRoute: FC = () => { )} { /> logoutAccount(account)} diff --git a/src/renderer/routes/LoginWithDeviceFlow.tsx b/src/renderer/routes/LoginWithDeviceFlow.tsx index e6480da07..08c8c2399 100644 --- a/src/renderer/routes/LoginWithDeviceFlow.tsx +++ b/src/renderer/routes/LoginWithDeviceFlow.tsx @@ -83,7 +83,7 @@ export const LoginWithDeviceFlowRoute: FC = () => { rendererLogError( 'LoginWithDeviceFlow', 'Failed to start device flow', - err, + err as Error, ); setError('Failed to start authentication. Please try again.'); } @@ -130,7 +130,7 @@ export const LoginWithDeviceFlowRoute: FC = () => { rendererLogError( 'LoginWithDeviceFlow', 'Failed to poll device flow', - err, + err as Error, ); setError('Authentication failed. Please try again.'); } diff --git a/src/renderer/routes/LoginWithOAuthApp.test.tsx b/src/renderer/routes/LoginWithOAuthApp.test.tsx index e20d2d63b..b3976d1d4 100644 --- a/src/renderer/routes/LoginWithOAuthApp.test.tsx +++ b/src/renderer/routes/LoginWithOAuthApp.test.tsx @@ -38,9 +38,9 @@ describe('renderer/routes/LoginWithOAuthApp.tsx', () => { describe('form validation', () => { it('should validate the form values are not empty', () => { const values: IFormData = { - hostname: null, - clientId: null, - clientSecret: null, + hostname: null as unknown as Hostname, + clientId: null as unknown as ClientID, + clientSecret: null as unknown as ClientSecret, }; expect(validateForm(values).hostname).toBe('Hostname is required'); diff --git a/src/renderer/routes/LoginWithOAuthApp.tsx b/src/renderer/routes/LoginWithOAuthApp.tsx index 8b15f2cc3..fc209b34d 100644 --- a/src/renderer/routes/LoginWithOAuthApp.tsx +++ b/src/renderer/routes/LoginWithOAuthApp.tsx @@ -134,7 +134,7 @@ export const LoginWithOAuthAppRoute: FC = () => { rendererLogError( 'loginWithOAuthApp', 'Failed to login with OAuth App', - err, + err as Error, ); setErrors({ invalidCredentialsForHost: `Failed to validate provided Client ID and Secret against ${data.hostname}`, diff --git a/src/renderer/routes/LoginWithPersonalAccessToken.test.tsx b/src/renderer/routes/LoginWithPersonalAccessToken.test.tsx index 6b570facd..eb95957f4 100644 --- a/src/renderer/routes/LoginWithPersonalAccessToken.test.tsx +++ b/src/renderer/routes/LoginWithPersonalAccessToken.test.tsx @@ -37,8 +37,8 @@ describe('renderer/routes/LoginWithPersonalAccessToken.tsx', () => { describe('form validation', () => { it('should validate the form values are not empty', () => { const values: IFormData = { - hostname: null, - token: null, + hostname: null as unknown as Hostname, + token: null as unknown as Token, }; expect(validateForm(values).hostname).toBe('Hostname is required'); expect(validateForm(values).token).toBe('Token is required'); diff --git a/src/renderer/routes/LoginWithPersonalAccessToken.tsx b/src/renderer/routes/LoginWithPersonalAccessToken.tsx index 3a1b79d0c..b50711c1b 100644 --- a/src/renderer/routes/LoginWithPersonalAccessToken.tsx +++ b/src/renderer/routes/LoginWithPersonalAccessToken.tsx @@ -121,7 +121,7 @@ export const LoginWithPersonalAccessTokenRoute: FC = () => { rendererLogError( 'loginWithPersonalAccessToken', 'Failed to login with PAT', - err, + err as Error, ); setErrors({ invalidCredentialsForHost: `Failed to validate provided token against ${data.hostname}`, diff --git a/src/renderer/routes/Notifications.test.tsx b/src/renderer/routes/Notifications.test.tsx index d30f6346e..f6010353d 100644 --- a/src/renderer/routes/Notifications.test.tsx +++ b/src/renderer/routes/Notifications.test.tsx @@ -79,7 +79,7 @@ describe('renderer/routes/Notifications.tsx', () => { it('default error', () => { const tree = renderWithProviders(, { status: 'error', - globalError: null, + globalError: undefined, }); expect(tree.container).toMatchSnapshot(); diff --git a/src/renderer/routes/Notifications.tsx b/src/renderer/routes/Notifications.tsx index a6f02c95b..ac78f2b27 100644 --- a/src/renderer/routes/Notifications.tsx +++ b/src/renderer/routes/Notifications.tsx @@ -54,7 +54,7 @@ export const NotificationsRoute: FC = () => { ); if (displayState.status === 'error') { - return ; + return ; } if (!displayState.hasNotifications && hasNoAccountErrors) { diff --git a/src/renderer/utils/api/client.ts b/src/renderer/utils/api/client.ts index 957863f55..e4da8a475 100644 --- a/src/renderer/utils/api/client.ts +++ b/src/renderer/utils/api/client.ts @@ -216,7 +216,7 @@ async function followUrl( export async function fetchDiscussionByNumber( notification: GitifyNotification, ): Promise { - const number = getNumberFromUrl(notification.subject.url); + const number = getNumberFromUrl(notification.subject.url!); return performGraphQLRequest( notification.account, @@ -241,7 +241,7 @@ export async function fetchDiscussionByNumber( export async function fetchIssueByNumber( notification: GitifyNotification, ): Promise { - const number = getNumberFromUrl(notification.subject.url); + const number = getNumberFromUrl(notification.subject.url!); return performGraphQLRequest( notification.account, @@ -262,7 +262,7 @@ export async function fetchIssueByNumber( export async function fetchPullByNumber( notification: GitifyNotification, ): Promise { - const number = getNumberFromUrl(notification.subject.url); + const number = getNumberFromUrl(notification.subject.url!); return performGraphQLRequest( notification.account, @@ -313,7 +313,7 @@ export async function fetchNotificationDetailsForList( const alias = builder.addNode({ owner: notification.repository.owner.login, name: notification.repository.name, - number: getNumberFromUrl(notification.subject.url), + number: getNumberFromUrl(notification.subject.url!), isDiscussionNotification: notification.subject.type === 'Discussion', isIssueNotification: notification.subject.type === 'Issue', isPullRequestNotification: notification.subject.type === 'PullRequest', @@ -348,7 +348,9 @@ export async function fetchNotificationDetailsForList( ); for (const [alias, notification] of aliasToNotification) { - const repoData = response[alias] as Record | undefined; + const repoData = (response as Record)[alias] as + | Record + | undefined; if (!repoData) { continue; // Skip if no data for this alias } diff --git a/src/renderer/utils/api/errors.test.ts b/src/renderer/utils/api/errors.test.ts index 2d65941c2..534abedb4 100644 --- a/src/renderer/utils/api/errors.test.ts +++ b/src/renderer/utils/api/errors.test.ts @@ -168,7 +168,7 @@ describe('renderer/utils/api/errors.ts', () => { errors: [ { message: 'Error 1' }, { message: 'Error 2' }, - ] as unknown as GraphqlResponseError['errors'], + ] as unknown as NonNullable['errors']>, }, ); @@ -191,7 +191,9 @@ function createGraphQLResponseError( {}, { data: {}, - errors: [{ message }] as GraphqlResponseError['errors'], + errors: [{ message }] as unknown as NonNullable< + GraphqlResponseError['errors'] + >, }, ); } diff --git a/src/renderer/utils/api/errors.ts b/src/renderer/utils/api/errors.ts index 1aa3e76e9..a942c6559 100644 --- a/src/renderer/utils/api/errors.ts +++ b/src/renderer/utils/api/errors.ts @@ -83,8 +83,8 @@ export function handleGraphQLResponseError( context: string, payload: GraphqlResponseError, ): never { - const errorMessages = payload.errors - .map((graphQLError) => graphQLError.message) + const errorMessages = payload + .errors!.map((graphQLError) => graphQLError.message) .join('; '); const err = new Error( diff --git a/src/renderer/utils/api/features.test.ts b/src/renderer/utils/api/features.test.ts index 541894f92..3fd8b903f 100644 --- a/src/renderer/utils/api/features.test.ts +++ b/src/renderer/utils/api/features.test.ts @@ -35,7 +35,7 @@ describe('renderer/utils/api/features.ts', () => { it('should return false for GitHub Enterprise Server when no version available', () => { const account = { ...mockGitHubEnterpriseServerAccount, - version: null, + version: undefined, }; expect(isMarkAsDoneFeatureSupported(account)).toBe(false); @@ -70,7 +70,7 @@ describe('renderer/utils/api/features.ts', () => { it('should return false for GitHub Enterprise Server when no version available', () => { const account = { ...mockGitHubEnterpriseServerAccount, - version: null, + version: undefined, }; expect(isAnsweredDiscussionFeatureSupported(account)).toBe(false); diff --git a/src/renderer/utils/api/graphql/MergeQueryBuilder.ts b/src/renderer/utils/api/graphql/MergeQueryBuilder.ts index bf82bf05e..4f952ad7f 100644 --- a/src/renderer/utils/api/graphql/MergeQueryBuilder.ts +++ b/src/renderer/utils/api/graphql/MergeQueryBuilder.ts @@ -50,16 +50,17 @@ export class MergeQueryBuilder { private readonly fragments: FragmentInfo[] = []; // Precomputed, invariant template-derived data (computed once per module load) - private static readonly TEMPLATE_FRAGMENTS = - extractNonQueryFragments(TemplateDocument); + private static readonly TEMPLATE_FRAGMENTS = extractNonQueryFragments( + TemplateDocument as any, + ); private static readonly TEMPLATE_QUERY_INNER = (() => { - const queryFrags = extractQueryFragments(TemplateDocument); + const queryFrags = extractQueryFragments(TemplateDocument as any); return queryFrags.length ? queryFrags[0].inner : null; })(); private static readonly TEMPLATE_NON_INDEXED_DEFS = - extractNonIndexedVariableDefinitions(TemplateDocument); + extractNonIndexedVariableDefinitions(TemplateDocument as any); private static readonly TEMPLATE_INDEXED_VAR_DEFS = - extractIndexedVariableDefinitions(TemplateDocument); + extractIndexedVariableDefinitions(TemplateDocument as any); constructor() { // Add precomputed static fragments @@ -132,7 +133,7 @@ export class MergeQueryBuilder { const selection = aliasFieldAndSubstituteIndexedVars( alias, index, - MergeQueryBuilder.TEMPLATE_QUERY_INNER, + MergeQueryBuilder.TEMPLATE_QUERY_INNER!, ); this.addSelection(selection); diff --git a/src/renderer/utils/api/graphql/utils.test.ts b/src/renderer/utils/api/graphql/utils.test.ts index 86b161ed5..d5867f0db 100644 --- a/src/renderer/utils/api/graphql/utils.test.ts +++ b/src/renderer/utils/api/graphql/utils.test.ts @@ -15,7 +15,7 @@ describe('renderer/utils/api/graphql/utils.ts', () => { describe('getQueryFragmentBody', () => { it('should extract query fragments from operation document', () => { const fragments = extractQueryFragments( - FetchMergedDetailsTemplateDocument, + FetchMergedDetailsTemplateDocument as any, ); expect(fragments).not.toBeNull(); @@ -28,7 +28,7 @@ describe('renderer/utils/api/graphql/utils.ts', () => { it('should extract query fragments from fragment document', () => { const fragments = extractQueryFragments( - MergedDetailsQueryTemplateFragmentDoc, + MergedDetailsQueryTemplateFragmentDoc as any, ); expect(fragments).not.toBeNull(); @@ -49,7 +49,7 @@ describe('renderer/utils/api/graphql/utils.ts', () => { describe('extractNonQueryFragments', () => { it('should extract non-query fragments from FetchBatchMergedTemplateDocument', () => { const fragments = extractNonQueryFragments( - FetchMergedDetailsTemplateDocument, + FetchMergedDetailsTemplateDocument as any, ); expect(fragments).not.toBeNull(); @@ -72,7 +72,7 @@ describe('renderer/utils/api/graphql/utils.ts', () => { describe('extractIndexedVariableDefinitions', () => { it('should extract indexed variable definitions from BatchMergedDetailsQueryTemplateFragmentDoc', () => { const varDefs = extractIndexedVariableDefinitions( - FetchMergedDetailsTemplateDocument, + FetchMergedDetailsTemplateDocument as any, ); expect(varDefs).not.toBeNull(); @@ -91,7 +91,7 @@ describe('renderer/utils/api/graphql/utils.ts', () => { describe('extractNonIndexedVariableDefinitions', () => { it('should extract non-indexed variable definitions from extractNonIndexedVariableDefinitions', () => { const varDefs = extractNonIndexedVariableDefinitions( - FetchMergedDetailsTemplateDocument, + FetchMergedDetailsTemplateDocument as any, ); expect(varDefs).not.toBeNull(); diff --git a/src/renderer/utils/api/transform.ts b/src/renderer/utils/api/transform.ts index bbcd97686..4c9f37190 100644 --- a/src/renderer/utils/api/transform.ts +++ b/src/renderer/utils/api/transform.ts @@ -1,6 +1,7 @@ import type { Account, GitifyNotification, + GitifyNotificationDisplay, GitifyOwner, GitifyReason, GitifyRepository, @@ -54,7 +55,7 @@ function transformNotification( repository: transformRepository(raw.repository), account: account, order: 0, // Will be set later in stabilizeNotificationsOrder - display: undefined, // Display fields start as undefined, populated by formatNotification post-enrichment + display: undefined as unknown as GitifyNotificationDisplay, // Display fields start as undefined, populated by formatNotification post-enrichment }; } @@ -64,7 +65,7 @@ function transformReason(raw: RawGitHubNotification['reason']): GitifyReason { return { code: raw as Reason, title: reasonDetails.title, - description: reasonDetails.description, + description: reasonDetails.description ?? '', }; } diff --git a/src/renderer/utils/api/utils.ts b/src/renderer/utils/api/utils.ts index 6fd071a18..a20f676b7 100644 --- a/src/renderer/utils/api/utils.ts +++ b/src/renderer/utils/api/utils.ts @@ -27,5 +27,5 @@ export function getGitHubAPIBaseUrl(hostname: Hostname, type: APIClientType) { } export function getNumberFromUrl(url: string): number { - return Number.parseInt(url.split('/').pop(), 10); + return Number.parseInt(url.split('/').pop()!, 10); } diff --git a/src/renderer/utils/auth/flows.ts b/src/renderer/utils/auth/flows.ts index 25cf40c7c..8976466fe 100644 --- a/src/renderer/utils/auth/flows.ts +++ b/src/renderer/utils/auth/flows.ts @@ -141,7 +141,7 @@ export async function pollGitHubDeviceFlow( return authentication.token as Token; } catch (err) { if (err instanceof RequestError) { - const response = err.response.data as DeviceFlowErrorResponse; + const response = err.response!.data as DeviceFlowErrorResponse; const errorCode = response.error; if (errorCode === 'authorization_pending' || errorCode === 'slow_down') { @@ -152,7 +152,7 @@ export async function pollGitHubDeviceFlow( rendererLogError( 'pollGitHubDeviceFlow', 'Error exchanging device code', - err, + err as Error, ); throw err; diff --git a/src/renderer/utils/auth/utils.test.ts b/src/renderer/utils/auth/utils.test.ts index 2cd041170..865b0353b 100644 --- a/src/renderer/utils/auth/utils.test.ts +++ b/src/renderer/utils/auth/utils.test.ts @@ -78,7 +78,7 @@ describe('renderer/utils/auth/utils.ts', () => { token: 'encrypted' as Token, user: { id: String(mockAuthenticatedResponse.id), - name: mockAuthenticatedResponse.name, + name: mockAuthenticatedResponse.name as string | null, login: mockAuthenticatedResponse.login, avatar: mockAuthenticatedResponse.avatar_url as Link, }, @@ -104,7 +104,7 @@ describe('renderer/utils/auth/utils.ts', () => { token: 'encrypted' as Token, user: { id: String(mockAuthenticatedResponse.id), - name: mockAuthenticatedResponse.name, + name: mockAuthenticatedResponse.name as string | null, login: mockAuthenticatedResponse.login, avatar: mockAuthenticatedResponse.avatar_url as Link, }, @@ -144,7 +144,7 @@ describe('renderer/utils/auth/utils.ts', () => { token: 'encrypted' as Token, user: { id: String(mockAuthenticatedResponse.id), - name: mockAuthenticatedResponse.name, + name: mockAuthenticatedResponse.name as string | null, login: mockAuthenticatedResponse.login, avatar: mockAuthenticatedResponse.avatar_url as Link, }, @@ -170,7 +170,7 @@ describe('renderer/utils/auth/utils.ts', () => { token: 'encrypted' as Token, user: { id: String(mockAuthenticatedResponse.id), - name: mockAuthenticatedResponse.name, + name: mockAuthenticatedResponse.name as string | null, login: mockAuthenticatedResponse.login, avatar: mockAuthenticatedResponse.avatar_url as Link, }, diff --git a/src/renderer/utils/auth/utils.ts b/src/renderer/utils/auth/utils.ts index 561f6ef3c..7558a4344 100644 --- a/src/renderer/utils/auth/utils.ts +++ b/src/renderer/utils/auth/utils.ts @@ -70,7 +70,7 @@ export async function addAccount( // Replace the existing account (e.g. re-authentication with a new token) rendererLogInfo( 'addAccount', - `updating existing account for user ${newAccount.user.login}`, + `updating existing account for user ${newAccount.user!.login}`, ); accountList[existingIndex] = newAccount; } else { @@ -145,7 +145,7 @@ export async function refreshAccount(account: Account): Promise { rendererLogError( 'refreshAccount', `failed to refresh account for user ${account.user?.login ?? account.hostname}`, - err, + err as Error, ); throw err; } @@ -164,7 +164,7 @@ export async function refreshAccount(account: Account): Promise { */ export function extractHostVersion(version: string | null): string { if (version) { - return semver.valid(semver.coerce(version)); + return semver.valid(semver.coerce(version))!; } return 'latest'; @@ -331,7 +331,7 @@ export function isValidToken(token: Token) { */ export function getAccountUUID(account: Account): AccountUUID { return btoa( - `${account.hostname}-${account.user.id}-${account.method}`, + `${account.hostname}-${account.user!.id}-${account.method}`, ) as AccountUUID; } diff --git a/src/renderer/utils/core/errors.test.ts b/src/renderer/utils/core/errors.test.ts index 23c71ba71..1cbe9ec79 100644 --- a/src/renderer/utils/core/errors.test.ts +++ b/src/renderer/utils/core/errors.test.ts @@ -1,6 +1,6 @@ import { mockAccountWithError } from '../../__mocks__/account-mocks'; -import type { AccountNotifications } from '../../types'; +import type { AccountNotifications, GitifyError } from '../../types'; import { areAllAccountErrorsSame, @@ -17,7 +17,7 @@ describe('renderer/utils/errors.ts', () => { it('returns false when some accounts have no error', () => { const items: AccountNotifications[] = [ mockAccountWithError(Errors.NETWORK), - mockAccountWithError(null), + mockAccountWithError(null as unknown as GitifyError), ]; expect(doesAllAccountsHaveErrors(items)).toBe(false); @@ -60,7 +60,7 @@ describe('renderer/utils/errors.ts', () => { it('returns false when one account has null error', () => { const items: AccountNotifications[] = [ mockAccountWithError(Errors.NETWORK), - mockAccountWithError(null), + mockAccountWithError(null as unknown as GitifyError), ]; expect(areAllAccountErrorsSame(items)).toBe(false); diff --git a/src/renderer/utils/notifications/filters/filter.ts b/src/renderer/utils/notifications/filters/filter.ts index 7fef38ccd..77f3ff884 100644 --- a/src/renderer/utils/notifications/filters/filter.ts +++ b/src/renderer/utils/notifications/filters/filter.ts @@ -185,7 +185,9 @@ function passesStateFilter(notification: GitifyNotification): boolean { * @param state - The notification state to check. * @returns `true` if the state is currently filtered out. */ -export function isStateFilteredOut(state: GitifyNotificationState): boolean { +export function isStateFilteredOut( + state: GitifyNotificationState | undefined, +): boolean { const notification = { subject: { state: state } } as GitifyNotification; return !passesStateFilter(notification); diff --git a/src/renderer/utils/notifications/filters/userType.test.ts b/src/renderer/utils/notifications/filters/userType.test.ts index 73a2a1bb6..fe5f2674b 100644 --- a/src/renderer/utils/notifications/filters/userType.test.ts +++ b/src/renderer/utils/notifications/filters/userType.test.ts @@ -22,22 +22,22 @@ describe('renderer/utils/notifications/filters/userType.ts', () => { }, } satisfies DeepPartial as GitifyNotification; - mockPartialNotification.subject.user.type = 'User'; + mockPartialNotification.subject.user!.type = 'User'; expect( userTypeFilter.filterNotification(mockPartialNotification, 'User'), ).toBe(true); - mockPartialNotification.subject.user.type = 'EnterpriseUserAccount'; + mockPartialNotification.subject.user!.type = 'EnterpriseUserAccount'; expect( userTypeFilter.filterNotification(mockPartialNotification, 'User'), ).toBe(true); - mockPartialNotification.subject.user.type = 'Bot'; + mockPartialNotification.subject.user!.type = 'Bot'; expect( userTypeFilter.filterNotification(mockPartialNotification, 'Bot'), ).toBe(true); - mockPartialNotification.subject.user.type = 'Organization'; + mockPartialNotification.subject.user!.type = 'Organization'; expect( userTypeFilter.filterNotification( mockPartialNotification, diff --git a/src/renderer/utils/notifications/filters/userType.ts b/src/renderer/utils/notifications/filters/userType.ts index 3f0224c49..29a4673f1 100644 --- a/src/renderer/utils/notifications/filters/userType.ts +++ b/src/renderer/utils/notifications/filters/userType.ts @@ -67,7 +67,7 @@ export const userTypeFilter: Filter = { const allUserTypes = ['User', 'EnterpriseUserAccount']; if (userType === 'User') { - return allUserTypes.includes(notification.subject?.user?.type); + return allUserTypes.includes(notification.subject?.user?.type ?? ''); } return notification.subject?.user?.type === userType; diff --git a/src/renderer/utils/notifications/formatters.test.ts b/src/renderer/utils/notifications/formatters.test.ts index a728a3737..5cd99d1c3 100644 --- a/src/renderer/utils/notifications/formatters.test.ts +++ b/src/renderer/utils/notifications/formatters.test.ts @@ -11,13 +11,13 @@ import { describe('renderer/utils/notifications/formatters.ts', () => { it('formatProperCase', () => { - expect(formatProperCase(null)).toBe(''); + expect(formatProperCase(null as unknown as string)).toBe(''); expect(formatProperCase('')).toBe(''); expect(formatProperCase('OUTDATED discussion')).toBe('Outdated Discussion'); }); it('formatForDisplay', () => { - expect(formatForDisplay(null)).toBe(''); + expect(formatForDisplay(null as unknown as string[])).toBe(''); expect(formatForDisplay([])).toBe(''); expect(formatForDisplay(['open', 'PullRequest'])).toBe('Open Pull Request'); expect(formatForDisplay(['OUTDATED', 'Discussion'])).toBe( @@ -43,7 +43,7 @@ describe('renderer/utils/notifications/formatters.ts', () => { const notification = mockPartialGitifyNotification({ title: 'Sample', type: 'Issue', - state: null, + state: undefined, }); expect(formatNotificationType(notification)).toBe('Issue'); @@ -98,7 +98,9 @@ describe('renderer/utils/notifications/formatters.ts', () => { describe('formatMetricDescription', () => { it('return empty if no count', () => { - expect(formatMetricDescription(null, 'bee')).toBe(''); + expect(formatMetricDescription(null as unknown as number, 'bee')).toBe( + '', + ); }); it('return singular if count is 1', () => { diff --git a/src/renderer/utils/notifications/formatters.ts b/src/renderer/utils/notifications/formatters.ts index 0b6ba4eda..9c6eecce4 100644 --- a/src/renderer/utils/notifications/formatters.ts +++ b/src/renderer/utils/notifications/formatters.ts @@ -82,7 +82,7 @@ export function formatNotificationType( notification: GitifyNotification, ): string { return formatForDisplay([ - notification.subject.state, + notification.subject.state ?? '', notification.subject.type, ]); } diff --git a/src/renderer/utils/notifications/handlers/checkSuite.test.ts b/src/renderer/utils/notifications/handlers/checkSuite.test.ts index 193129108..f34ce78e7 100644 --- a/src/renderer/utils/notifications/handlers/checkSuite.test.ts +++ b/src/renderer/utils/notifications/handlers/checkSuite.test.ts @@ -25,7 +25,7 @@ describe('renderer/utils/notifications/handlers/checkSuite.ts', () => { expect(result).toEqual({ state: 'CANCELLED', - user: null, + user: undefined, htmlUrl: 'https://github.com/gitify-app/notifications-test/actions?query=workflow%3A%22Demo%22+is%3ACANCELLED+branch%3Amain', }); @@ -44,7 +44,7 @@ describe('renderer/utils/notifications/handlers/checkSuite.ts', () => { expect(result).toEqual({ state: 'FAILURE', - user: null, + user: undefined, htmlUrl: 'https://github.com/gitify-app/notifications-test/actions?query=workflow%3A%22Demo%22+is%3AFAILURE+branch%3Amain', }); @@ -63,7 +63,7 @@ describe('renderer/utils/notifications/handlers/checkSuite.ts', () => { expect(result).toEqual({ state: 'FAILURE', - user: null, + user: undefined, htmlUrl: 'https://github.com/gitify-app/notifications-test/actions?query=workflow%3A%22Demo%22+is%3AFAILURE+branch%3Amain', }); @@ -82,7 +82,7 @@ describe('renderer/utils/notifications/handlers/checkSuite.ts', () => { expect(result).toEqual({ state: 'FAILURE', - user: null, + user: undefined, htmlUrl: 'https://github.com/gitify-app/notifications-test/actions?query=workflow%3A%22Demo%22+is%3AFAILURE+branch%3Amain', }); @@ -101,7 +101,7 @@ describe('renderer/utils/notifications/handlers/checkSuite.ts', () => { expect(result).toEqual({ state: 'SKIPPED', - user: null, + user: undefined, htmlUrl: 'https://github.com/gitify-app/notifications-test/actions?query=workflow%3A%22Demo%22+is%3ASKIPPED+branch%3Amain', }); @@ -120,7 +120,7 @@ describe('renderer/utils/notifications/handlers/checkSuite.ts', () => { expect(result).toEqual({ state: 'SUCCESS', - user: null, + user: undefined, htmlUrl: 'https://github.com/gitify-app/notifications-test/actions?query=workflow%3A%22Demo%22+is%3ASUCCESS+branch%3Amain', }); @@ -246,7 +246,7 @@ describe('renderer/utils/notifications/handlers/checkSuite.ts', () => { expect(result).toEqual({ workflowName: 'Demo', - attemptNumber: null, + attemptNumber: undefined, status: 'CANCELLED', statusDisplayName: 'cancelled', branchName: 'feature/foo', @@ -263,7 +263,7 @@ describe('renderer/utils/notifications/handlers/checkSuite.ts', () => { expect(result).toEqual({ workflowName: 'Demo', - attemptNumber: null, + attemptNumber: undefined, status: 'FAILURE', statusDisplayName: 'failed', branchName: 'main', @@ -297,7 +297,7 @@ describe('renderer/utils/notifications/handlers/checkSuite.ts', () => { expect(result).toEqual({ workflowName: 'Demo', - attemptNumber: null, + attemptNumber: undefined, status: 'SKIPPED', statusDisplayName: 'skipped', branchName: 'main', @@ -314,7 +314,7 @@ describe('renderer/utils/notifications/handlers/checkSuite.ts', () => { expect(result).toEqual({ workflowName: 'Demo', - attemptNumber: null, + attemptNumber: undefined, status: 'SUCCESS', statusDisplayName: 'succeeded', branchName: 'main', @@ -331,8 +331,8 @@ describe('renderer/utils/notifications/handlers/checkSuite.ts', () => { expect(result).toEqual({ workflowName: 'Demo', - attemptNumber: null, - status: null, + attemptNumber: undefined, + status: undefined, statusDisplayName: 'unknown-status', branchName: 'main', }); diff --git a/src/renderer/utils/notifications/handlers/checkSuite.ts b/src/renderer/utils/notifications/handlers/checkSuite.ts index cb2905475..16c433947 100644 --- a/src/renderer/utils/notifications/handlers/checkSuite.ts +++ b/src/renderer/utils/notifications/handlers/checkSuite.ts @@ -25,7 +25,7 @@ export interface CheckSuiteAttributes { workflowName: string; attemptNumber?: number; statusDisplayName: string; - status: GitifyCheckSuiteStatus | null; + status?: GitifyCheckSuiteStatus; branchName: string; } @@ -39,7 +39,7 @@ class CheckSuiteHandler extends DefaultHandler { if (state) { return { state: state, - user: null, + user: undefined, htmlUrl: getCheckSuiteUrl(notification), }; } @@ -101,7 +101,9 @@ export function getCheckSuiteAttributes( return { workflowName, - attemptNumber: attemptNumber ? Number.parseInt(attemptNumber, 10) : null, + attemptNumber: attemptNumber + ? Number.parseInt(attemptNumber, 10) + : undefined, status: getCheckSuiteStatus(statusDisplayName), statusDisplayName, branchName, @@ -110,7 +112,7 @@ export function getCheckSuiteAttributes( function getCheckSuiteStatus( statusDisplayName: string, -): GitifyCheckSuiteStatus { +): GitifyCheckSuiteStatus | undefined { switch (statusDisplayName) { case 'cancelled': return 'CANCELLED'; @@ -122,7 +124,7 @@ function getCheckSuiteStatus( case 'succeeded': return 'SUCCESS'; default: - return null; + return undefined; } } diff --git a/src/renderer/utils/notifications/handlers/commit.test.ts b/src/renderer/utils/notifications/handlers/commit.test.ts index 996207e54..9c5c5fc0f 100644 --- a/src/renderer/utils/notifications/handlers/commit.test.ts +++ b/src/renderer/utils/notifications/handlers/commit.test.ts @@ -41,7 +41,7 @@ describe('renderer/utils/notifications/handlers/commit.ts', () => { const result = await commitHandler.enrich(mockNotification, mockSettings); expect(result).toEqual({ - state: null, + state: undefined, user: { login: mockCommenter.login, htmlUrl: mockCommenter.html_url, @@ -66,7 +66,7 @@ describe('renderer/utils/notifications/handlers/commit.ts', () => { const result = await commitHandler.enrich(mockNotification, mockSettings); expect(result).toEqual({ - state: null, + state: undefined, user: { login: mockAuthor.login, htmlUrl: mockAuthor.html_url, diff --git a/src/renderer/utils/notifications/handlers/commit.ts b/src/renderer/utils/notifications/handlers/commit.ts index ba41c62ec..2bb0fb0e8 100644 --- a/src/renderer/utils/notifications/handlers/commit.ts +++ b/src/renderer/utils/notifications/handlers/commit.ts @@ -22,7 +22,7 @@ class CommitHandler extends DefaultHandler { notification: GitifyNotification, _settings: SettingsState, ): Promise> { - const commitState: GitifyNotificationState = null; // Commit notifications are stateless + const commitState: GitifyNotificationState | undefined = undefined; // Commit notifications are stateless // Return early if this notification would be hidden by filters if (isStateFilteredOut(commitState)) { @@ -38,22 +38,22 @@ class CommitHandler extends DefaultHandler { ); user = { - login: commitComment.user.login, - avatarUrl: commitComment.user.avatar_url as Link, - htmlUrl: commitComment.user.html_url as Link, - type: commitComment.user.type as GitifyNotificationUser['type'], + login: commitComment.user!.login, + avatarUrl: commitComment.user!.avatar_url as Link, + htmlUrl: commitComment.user!.html_url as Link, + type: commitComment.user!.type as GitifyNotificationUser['type'], }; } else { const commit = await getCommit( notification.account, - notification.subject.url, + notification.subject.url!, ); user = { - login: commit.author.login, - avatarUrl: commit.author.avatar_url as Link, - htmlUrl: commit.author.html_url as Link, - type: commit.author.type as GitifyNotificationUser['type'], + login: commit.author!.login, + avatarUrl: commit.author!.avatar_url as Link, + htmlUrl: commit.author!.html_url as Link, + type: commit.author!.type as GitifyNotificationUser['type'], }; } diff --git a/src/renderer/utils/notifications/handlers/default.test.ts b/src/renderer/utils/notifications/handlers/default.test.ts index 0b5d4c310..2a21931dc 100644 --- a/src/renderer/utils/notifications/handlers/default.test.ts +++ b/src/renderer/utils/notifications/handlers/default.test.ts @@ -45,9 +45,9 @@ describe('renderer/utils/notifications/handlers/default.ts', () => { describe('iconColor', () => { it('returns GRAY for any unrecognized state (fallback behavior)', () => { - const states: Array = [ + const states: Array = [ 'unknown' as GitifyNotificationState, - null, + undefined, undefined, ]; diff --git a/src/renderer/utils/notifications/handlers/discussion.ts b/src/renderer/utils/notifications/handlers/discussion.ts index c1f39fad1..e693f42b8 100644 --- a/src/renderer/utils/notifications/handlers/discussion.ts +++ b/src/renderer/utils/notifications/handlers/discussion.ts @@ -13,6 +13,7 @@ import { differenceInMilliseconds } from 'date-fns/differenceInMilliseconds'; import { type GitifyDiscussionState, type GitifyNotification, + type GitifyReactionGroup, type GitifySubject, IconColor, type Link, @@ -40,6 +41,10 @@ class DiscussionHandler extends DefaultHandler { fetchedData ?? (await fetchDiscussionByNumber(notification)).repository?.discussion; + if (!discussion) { + return {}; + } + let discussionState: GitifyDiscussionState = 'OPEN'; if (discussion.isAnswered) { @@ -52,7 +57,8 @@ class DiscussionHandler extends DefaultHandler { const latestDiscussionComment = getClosestDiscussionCommentOrReply( notification, - discussion.comments.nodes, + (discussion.comments.nodes?.filter(Boolean) ?? + []) as DiscussionCommentFieldsFragment[], ); const discussionReactionCount = @@ -70,13 +76,15 @@ class DiscussionHandler extends DefaultHandler { ]), commentCount: discussion.comments.totalCount, labels: - discussion.labels?.nodes.map((label) => ({ - name: label.name, - color: label.color, + discussion.labels?.nodes?.filter(Boolean).map((label) => ({ + name: label!.name, + color: label!.color, })) ?? [], htmlUrl: latestDiscussionComment?.url ?? discussion.url, reactionsCount: discussionReactionCount, - reactionGroups: discussionReactionGroup, + reactionGroups: (discussionReactionGroup ?? undefined) as + | GitifyReactionGroup[] + | undefined, }; } @@ -124,17 +132,17 @@ export function getClosestDiscussionCommentOrReply( const targetTimestamp = notification.updatedAt; const allCommentsAndReplies = comments.flatMap((comment) => [ - ...comment.replies.nodes, + ...(comment.replies?.nodes?.filter(Boolean) ?? []), comment, ]); // Find the closest match using the target timestamp const closestComment = allCommentsAndReplies.reduce((prev, curr) => { const prevDiff = Math.abs( - differenceInMilliseconds(prev.createdAt, targetTimestamp), + differenceInMilliseconds(prev!.createdAt, targetTimestamp), ); const currDiff = Math.abs( - differenceInMilliseconds(curr.createdAt, targetTimestamp), + differenceInMilliseconds(curr!.createdAt, targetTimestamp), ); return currDiff < prevDiff ? curr : prev; }, allCommentsAndReplies[0]); diff --git a/src/renderer/utils/notifications/handlers/issue.ts b/src/renderer/utils/notifications/handlers/issue.ts index 85d14a122..6838806c5 100644 --- a/src/renderer/utils/notifications/handlers/issue.ts +++ b/src/renderer/utils/notifications/handlers/issue.ts @@ -11,6 +11,7 @@ import { import type { GitifyIssueState, GitifyNotification, + GitifyReactionGroup, GitifySubject, Link, SettingsState, @@ -33,6 +34,10 @@ class IssueHandler extends DefaultHandler { const issue = fetchedData ?? (await fetchIssueByNumber(notification)).repository?.issue; + if (!issue) { + return {}; + } + const issueState = issue.stateReason ?? issue.state; const issueComment = issue.comments?.nodes?.[0]; @@ -53,14 +58,16 @@ class IssueHandler extends DefaultHandler { user: issueUser, commentCount: issue.comments.totalCount, labels: - issue.labels?.nodes.map((label) => ({ - name: label.name, - color: label.color, + issue.labels?.nodes?.filter(Boolean).map((label) => ({ + name: label!.name, + color: label!.color, })) ?? [], milestone: issue.milestone ?? undefined, htmlUrl: issueComment?.url ?? issue.url, reactionsCount: issueReactionCount, - reactionGroups: issueReactionGroup, + reactionGroups: (issueReactionGroup ?? undefined) as + | GitifyReactionGroup[] + | undefined, }; } diff --git a/src/renderer/utils/notifications/handlers/pullRequest.test.ts b/src/renderer/utils/notifications/handlers/pullRequest.test.ts index e1095f993..326a1b8ce 100644 --- a/src/renderer/utils/notifications/handlers/pullRequest.test.ts +++ b/src/renderer/utils/notifications/handlers/pullRequest.test.ts @@ -65,11 +65,11 @@ describe('renderer/utils/notifications/handlers/pullRequest.ts', () => { htmlUrl: mockAuthor.htmlUrl, type: mockAuthor.type, }, - reviews: null, + reviews: [], labels: [], linkedIssues: [], commentCount: 0, - milestone: null, + milestone: undefined, htmlUrl: 'https://github.com/gitify-app/notifications-test/pulls/123' as Link, reactionsCount: 0, @@ -103,11 +103,11 @@ describe('renderer/utils/notifications/handlers/pullRequest.ts', () => { htmlUrl: mockAuthor.htmlUrl, type: mockAuthor.type, }, - reviews: null, + reviews: [], labels: [], linkedIssues: [], commentCount: 0, - milestone: null, + milestone: undefined, htmlUrl: 'https://github.com/gitify-app/notifications-test/pulls/123' as Link, reactionsCount: 0, @@ -141,11 +141,11 @@ describe('renderer/utils/notifications/handlers/pullRequest.ts', () => { htmlUrl: mockAuthor.htmlUrl, type: mockAuthor.type, }, - reviews: null, + reviews: [], labels: [], linkedIssues: [], commentCount: 0, - milestone: null, + milestone: undefined, htmlUrl: 'https://github.com/gitify-app/notifications-test/pulls/123' as Link, reactionsCount: 0, @@ -179,11 +179,11 @@ describe('renderer/utils/notifications/handlers/pullRequest.ts', () => { htmlUrl: mockAuthor.htmlUrl, type: mockAuthor.type, }, - reviews: null, + reviews: [], labels: [], linkedIssues: [], commentCount: 0, - milestone: null, + milestone: undefined, htmlUrl: 'https://github.com/gitify-app/notifications-test/pulls/123' as Link, reactionsCount: 0, @@ -228,11 +228,11 @@ describe('renderer/utils/notifications/handlers/pullRequest.ts', () => { htmlUrl: mockCommenter.htmlUrl, type: mockCommenter.type, }, - reviews: null, + reviews: [], labels: [], linkedIssues: [], commentCount: 1, - milestone: null, + milestone: undefined, htmlUrl: 'https://github.com/gitify-app/notifications-test/pulls/123#issuecomment-1234' as Link, reactionsCount: 0, @@ -273,11 +273,11 @@ describe('renderer/utils/notifications/handlers/pullRequest.ts', () => { htmlUrl: mockAuthor.htmlUrl, type: mockAuthor.type, }, - reviews: null, + reviews: [], labels: [{ name: 'enhancement', color: '0e8a16' }], linkedIssues: [], commentCount: 0, - milestone: null, + milestone: undefined, htmlUrl: 'https://github.com/gitify-app/notifications-test/pulls/123' as Link, reactionsCount: 0, @@ -317,11 +317,11 @@ describe('renderer/utils/notifications/handlers/pullRequest.ts', () => { htmlUrl: mockAuthor.htmlUrl, type: mockAuthor.type, }, - reviews: null, + reviews: [], labels: [], linkedIssues: ['#789'], commentCount: 0, - milestone: null, + milestone: undefined, htmlUrl: 'https://github.com/gitify-app/notifications-test/pulls/123' as Link, reactionsCount: 0, @@ -358,7 +358,7 @@ describe('renderer/utils/notifications/handlers/pullRequest.ts', () => { htmlUrl: mockAuthor.htmlUrl, type: mockAuthor.type, }, - reviews: null, + reviews: [], labels: [], linkedIssues: [], commentCount: 0, @@ -473,7 +473,7 @@ describe('renderer/utils/notifications/handlers/pullRequest.ts', () => { it('handles no PR reviews yet', async () => { const result = getLatestReviewForReviewers([]); - expect(result).toBeNull(); + expect(result).toEqual([]); }); }); }); diff --git a/src/renderer/utils/notifications/handlers/pullRequest.ts b/src/renderer/utils/notifications/handlers/pullRequest.ts index aa51290d9..721abdca4 100644 --- a/src/renderer/utils/notifications/handlers/pullRequest.ts +++ b/src/renderer/utils/notifications/handlers/pullRequest.ts @@ -13,6 +13,7 @@ import { type GitifyNotification, type GitifyPullRequestReview, type GitifyPullRequestState, + type GitifyReactionGroup, type GitifySubject, IconColor, type Link, @@ -40,6 +41,10 @@ class PullRequestHandler extends DefaultHandler { fetchedData ?? (await fetchPullByNumber(notification)).repository?.pullRequest; + if (!pr) { + return {}; + } + let prState: GitifyPullRequestState = pr.state; if (pr.isDraft) { prState = 'DRAFT'; @@ -47,11 +52,14 @@ class PullRequestHandler extends DefaultHandler { prState = 'MERGE_QUEUE'; } - const prComment = pr.comments?.nodes[0]; + const prComment = pr.comments?.nodes?.[0]; const prUser = getNotificationAuthor([prComment?.author, pr.author]); - const reviews = getLatestReviewForReviewers(pr.reviews.nodes); + const reviews = getLatestReviewForReviewers( + (pr.reviews?.nodes?.filter(Boolean) ?? + []) as PullRequestReviewFieldsFragment[], + ); const prReactionCount = prComment?.reactions.totalCount ?? pr.reactions.totalCount; @@ -64,17 +72,19 @@ class PullRequestHandler extends DefaultHandler { reviews: reviews, commentCount: pr.comments.totalCount, labels: - pr.labels?.nodes.map((label) => ({ - name: label.name, - color: label.color, + pr.labels?.nodes?.filter(Boolean).map((label) => ({ + name: label!.name, + color: label!.color, })) ?? [], - linkedIssues: pr.closingIssuesReferences?.nodes.map((issue) => - formatGitHubNumber(issue.number), - ), - milestone: pr.milestone, + linkedIssues: pr.closingIssuesReferences?.nodes + ?.filter(Boolean) + .map((issue) => formatGitHubNumber(issue!.number)), + milestone: pr.milestone ?? undefined, htmlUrl: prComment?.url ?? pr.url, reactionsCount: prReactionCount, - reactionGroups: prReactionGroup, + reactionGroups: (prReactionGroup ?? undefined) as + | GitifyReactionGroup[] + | undefined, }; } @@ -121,7 +131,7 @@ export function getLatestReviewForReviewers( reviews: PullRequestReviewFieldsFragment[], ): GitifyPullRequestReview[] { if (!reviews.length) { - return null; + return []; } // Find the most recent review for each reviewer @@ -129,7 +139,7 @@ export function getLatestReviewForReviewers( const sortedReviews = reviews.toReversed(); for (const prReview of sortedReviews) { const reviewerFound = latestReviews.find( - (review) => review.author.login === prReview.author.login, + (review) => review.author?.login === prReview.author?.login, ); if (!reviewerFound) { @@ -145,11 +155,11 @@ export function getLatestReviewForReviewers( ); if (reviewerFound) { - reviewerFound.users.push(prReview.author.login); + reviewerFound.users.push(prReview.author?.login ?? ''); } else { reviewers.push({ state: prReview.state, - users: [prReview.author.login], + users: [prReview.author?.login ?? ''], }); } } diff --git a/src/renderer/utils/notifications/handlers/release.test.ts b/src/renderer/utils/notifications/handlers/release.test.ts index 1ad7667f9..6e16b96e7 100644 --- a/src/renderer/utils/notifications/handlers/release.test.ts +++ b/src/renderer/utils/notifications/handlers/release.test.ts @@ -35,7 +35,7 @@ describe('renderer/utils/notifications/handlers/release.ts', () => { ); expect(result).toEqual({ - state: null, + state: undefined, user: { login: mockAuthor.login, htmlUrl: mockAuthor.html_url, diff --git a/src/renderer/utils/notifications/handlers/release.ts b/src/renderer/utils/notifications/handlers/release.ts index 789d96712..f04cfb05e 100644 --- a/src/renderer/utils/notifications/handlers/release.ts +++ b/src/renderer/utils/notifications/handlers/release.ts @@ -23,7 +23,7 @@ class ReleaseHandler extends DefaultHandler { notification: GitifyNotification, _settings: SettingsState, ): Promise> { - const releaseState: GitifyNotificationState = null; // Release notifications are stateless + const releaseState: GitifyNotificationState | undefined = undefined; // Release notifications are stateless // Return early if this notification would be hidden by filters if (isStateFilteredOut(releaseState)) { @@ -32,17 +32,17 @@ class ReleaseHandler extends DefaultHandler { const release = await getRelease( notification.account, - notification.subject.url, + notification.subject.url!, ); - const user: GitifyNotificationUser = release.author + const user: GitifyNotificationUser | undefined = release.author ? { login: release.author.login, avatarUrl: release.author.avatar_url as Link, htmlUrl: release.author.html_url as Link, type: release.author.type as UserType, } - : null; + : undefined; return { state: releaseState, diff --git a/src/renderer/utils/notifications/handlers/utils.test.ts b/src/renderer/utils/notifications/handlers/utils.test.ts index b4f6989d3..152aa4388 100644 --- a/src/renderer/utils/notifications/handlers/utils.test.ts +++ b/src/renderer/utils/notifications/handlers/utils.test.ts @@ -4,10 +4,10 @@ import { getNotificationAuthor } from './utils'; describe('renderer/utils/notifications/handlers/utils.ts', () => { describe('getNotificationAuthor', () => { - it('returns null when all users are null', () => { + it('returns undefined when all users are null', () => { const result = getNotificationAuthor([null, null]); - expect(result).toBeNull(); + expect(result).toBeUndefined(); }); it('returns first user', () => { diff --git a/src/renderer/utils/notifications/handlers/utils.ts b/src/renderer/utils/notifications/handlers/utils.ts index c8205232c..7c2e7a2cf 100644 --- a/src/renderer/utils/notifications/handlers/utils.ts +++ b/src/renderer/utils/notifications/handlers/utils.ts @@ -16,8 +16,8 @@ type AuthorInput = */ export function getNotificationAuthor( users: AuthorInput[], -): GitifyNotificationUser { - let subjectUser: GitifyNotificationUser = null; +): GitifyNotificationUser | undefined { + let subjectUser: GitifyNotificationUser | undefined; for (const user of users) { if (user) { diff --git a/src/renderer/utils/notifications/handlers/workflowRun.test.ts b/src/renderer/utils/notifications/handlers/workflowRun.test.ts index b9e60e667..969605daa 100644 --- a/src/renderer/utils/notifications/handlers/workflowRun.test.ts +++ b/src/renderer/utils/notifications/handlers/workflowRun.test.ts @@ -20,7 +20,7 @@ describe('renderer/utils/notifications/handlers/workflowRun.ts', () => { expect(result).toEqual({ state: 'WAITING', - user: null, + user: undefined, htmlUrl: 'https://github.com/gitify-app/notifications-test/actions?query=is%3AWAITING', }); @@ -110,7 +110,7 @@ describe('renderer/utils/notifications/handlers/workflowRun.ts', () => { const result = getWorkflowRunAttributes(mockNotification); expect(result).toEqual({ - status: null, + status: undefined, statusDisplayName: 'unknown-state', user: 'some-user', }); diff --git a/src/renderer/utils/notifications/handlers/workflowRun.ts b/src/renderer/utils/notifications/handlers/workflowRun.ts index 44a83f4e7..3ba9cbe63 100644 --- a/src/renderer/utils/notifications/handlers/workflowRun.ts +++ b/src/renderer/utils/notifications/handlers/workflowRun.ts @@ -17,7 +17,7 @@ import { actionsURL } from './utils'; export interface WorkflowRunAttributes { user: string; statusDisplayName: string; - status: GitifyCheckSuiteStatus | null; + status?: GitifyCheckSuiteStatus; } class WorkflowRunHandler extends DefaultHandler { @@ -30,7 +30,7 @@ class WorkflowRunHandler extends DefaultHandler { if (state) { return { state: state, - user: null, + user: undefined, htmlUrl: getWorkflowRunUrl(notification), }; } @@ -76,12 +76,12 @@ export function getWorkflowRunAttributes( function getWorkflowRunStatus( statusDisplayName: string, -): GitifyCheckSuiteStatus { +): GitifyCheckSuiteStatus | undefined { switch (statusDisplayName) { case 'review': return 'WAITING'; default: - return null; + return undefined; } } diff --git a/src/renderer/utils/notifications/notifications.ts b/src/renderer/utils/notifications/notifications.ts index a287ec89b..bdd5c7627 100644 --- a/src/renderer/utils/notifications/notifications.ts +++ b/src/renderer/utils/notifications/notifications.ts @@ -56,12 +56,12 @@ export function getUnreadNotificationCount( } function getNotifications(state: GitifyState) { - return state.auth.accounts.map((account) => { + return state.auth!.accounts.map((account) => { return { account, notifications: listNotificationsForAuthenticatedUser( account, - state.settings, + state.settings!, ), }; }); @@ -101,12 +101,12 @@ export async function getAllNotifications( notifications = await enrichNotifications( notifications, - state.settings, + state.settings!, ); notifications = filterDetailedNotifications( notifications, - state.settings, + state.settings!, ); notifications = notifications.map((notification) => { @@ -122,20 +122,20 @@ export async function getAllNotifications( rendererLogError( 'getAllNotifications', 'error occurred while fetching account notifications', - err, + err as Error, ); return { account: accountNotifications.account, notifications: [], - error: determineFailureType(err), + error: determineFailureType(err as Error), }; } }), ); // Set the order property for the notifications - stabilizeNotificationsOrder(accountNotifications, state.settings); + stabilizeNotificationsOrder(accountNotifications, state.settings!); return accountNotifications; } @@ -211,7 +211,7 @@ async function fetchNotificationDetailsInBatches( rendererLogError( 'fetchNotificationDetailsInBatches', `Failed to fetch merged notification details for batch ${batchIndex}`, - err, + err as Error, ); } } @@ -244,7 +244,7 @@ export async function enrichNotification( rendererLogError( 'enrichNotification', 'failed to enrich notification details for', - err, + err as Error, notification, ); diff --git a/src/renderer/utils/notifications/url.test.ts b/src/renderer/utils/notifications/url.test.ts index 68a55125e..68cb88d02 100644 --- a/src/renderer/utils/notifications/url.test.ts +++ b/src/renderer/utils/notifications/url.test.ts @@ -37,7 +37,7 @@ describe('renderer/utils/notifications/url.ts', () => { latestCommentUrl: mockLatestCommentUrl, type: 'Issue' as SubjectType, htmlUrl: mockSubjectHtmlUrl, - } as GitifySubject; + } as unknown as GitifySubject; const result = await generateGitHubWebUrl({ ...mockGitifyNotification, @@ -61,7 +61,7 @@ describe('renderer/utils/notifications/url.ts', () => { latestCommentUrl: mockLatestCommentUrl, type: 'Issue' as SubjectType, htmlUrl: mockSubjectHtmlUrl, - } as GitifySubject; + } as unknown as GitifySubject; getHtmlUrlSpy.mockResolvedValue({ html_url: mockHtmlUrl, @@ -92,7 +92,7 @@ describe('renderer/utils/notifications/url.ts', () => { latestCommentUrl: mockLatestCommentUrl, type: 'Issue' as SubjectType, htmlUrl: mockSubjectHtmlUrl, - } as GitifySubject; + } as unknown as GitifySubject; getHtmlUrlSpy.mockResolvedValue({ html_url: mockHtmlUrl, diff --git a/src/renderer/utils/notifications/url.ts b/src/renderer/utils/notifications/url.ts index c0337a40a..88808be48 100644 --- a/src/renderer/utils/notifications/url.ts +++ b/src/renderer/utils/notifications/url.ts @@ -7,7 +7,7 @@ import { createNotificationHandler } from './handlers'; export function generateNotificationReferrerId( notification: GitifyNotification, ): string { - const raw = `018:NotificationThread${notification.id}:${notification.account.user.id}`; + const raw = `018:NotificationThread${notification.id}:${notification.account.user!.id}`; return btoa(raw); } @@ -40,7 +40,7 @@ export async function generateGitHubWebUrl( rendererLogError( 'generateGitHubWebUrl', 'Failed to resolve specific notification html url for', - err, + err as Error, notification, ); } diff --git a/src/renderer/utils/system/audio.ts b/src/renderer/utils/system/audio.ts index 059212c31..d82f62c8d 100644 --- a/src/renderer/utils/system/audio.ts +++ b/src/renderer/utils/system/audio.ts @@ -27,7 +27,11 @@ export async function raiseSoundNotification(volume: Percentage) { try { await audio.play(); } catch (err) { - rendererLogError('audio', 'Failed to play notification sound:', err); + rendererLogError( + 'audio', + 'Failed to play notification sound:', + err as Error, + ); cachedAudio = null; } } diff --git a/src/renderer/utils/system/comms.test.ts b/src/renderer/utils/system/comms.test.ts index f3869d4e8..7d3cc9105 100644 --- a/src/renderer/utils/system/comms.test.ts +++ b/src/renderer/utils/system/comms.test.ts @@ -50,7 +50,7 @@ describe('renderer/utils/comms.ts', () => { it('should use default open preference if user settings not found', () => { vi.spyOn(storage, 'loadState').mockReturnValue({ - settings: null, + settings: undefined, }); openExternalLink('https://gitify.io/' as Link); diff --git a/src/renderer/utils/system/links.ts b/src/renderer/utils/system/links.ts index 35aefb221..5358997a8 100644 --- a/src/renderer/utils/system/links.ts +++ b/src/renderer/utils/system/links.ts @@ -41,7 +41,7 @@ export function openGitHubPulls(hostname: Hostname) { export function openAccountProfile(account: Account) { const url = new URL(`https://${account.hostname}`); - url.pathname = account.user.login; + url.pathname = account.user!.login; openExternalLink(url.toString() as Link); } diff --git a/src/renderer/utils/system/native.test.ts b/src/renderer/utils/system/native.test.ts index 5d5158af7..040103294 100644 --- a/src/renderer/utils/system/native.test.ts +++ b/src/renderer/utils/system/native.test.ts @@ -49,7 +49,7 @@ describe('renderer/utils/system/native.ts', () => { expect(window.gitify.raiseNativeNotification).toHaveBeenCalledWith( 'Gitify', 'You have 2 notifications', - null, + '', ); expect(url.generateGitHubWebUrl).toHaveBeenCalledTimes(0); }); diff --git a/src/renderer/utils/system/native.ts b/src/renderer/utils/system/native.ts index 65fcd990f..71a8a5ab8 100644 --- a/src/renderer/utils/system/native.ts +++ b/src/renderer/utils/system/native.ts @@ -18,7 +18,7 @@ export async function raiseNativeNotification( ) { let title: string; let body: string; - let url: string = null; + let url = ''; if (notifications.length === 1) { const notification = notifications[0]; diff --git a/src/renderer/utils/ui/zoom.test.ts b/src/renderer/utils/ui/zoom.test.ts index e602dc6c5..8f71644f3 100644 --- a/src/renderer/utils/ui/zoom.test.ts +++ b/src/renderer/utils/ui/zoom.test.ts @@ -28,7 +28,7 @@ describe('renderer/utils/zoom.ts', () => { expect(zoomLevelToPercentage(-2)).toBe(0); expect(zoomLevelToPercentage(1)).toBe(150); - expect(zoomLevelToPercentage(undefined)).toBe(100); + expect(zoomLevelToPercentage(undefined as unknown as number)).toBe(100); }); it('can decrease zoom percentage', () => { From fd4789aafc85253a6611a1dbc1e6a8e8b3910a76 Mon Sep 17 00:00:00 2001 From: Afonso Jorge Ramos Date: Thu, 2 Apr 2026 19:09:07 +0200 Subject: [PATCH 4/8] refactor: use early return guard for mb.window across src/main --- src/main/events.ts | 6 +++++- src/main/handlers/system.ts | 6 +++++- src/main/lifecycle/window.ts | 22 +++++++++++++--------- src/main/utils.ts | 6 +++++- 4 files changed, 28 insertions(+), 12 deletions(-) diff --git a/src/main/events.ts b/src/main/events.ts index b5ab910b8..38dcb8687 100644 --- a/src/main/events.ts +++ b/src/main/events.ts @@ -46,5 +46,9 @@ export function sendRendererEvent( event: EventType, data?: string, ) { - mb.window?.webContents.send(event, data); + if (!mb.window) { + return; + } + + mb.window.webContents.send(event, data); } diff --git a/src/main/handlers/system.ts b/src/main/handlers/system.ts index 17b23d265..07eca1615 100644 --- a/src/main/handlers/system.ts +++ b/src/main/handlers/system.ts @@ -24,7 +24,11 @@ export function registerSystemHandlers(mb: Menubar): void { let lastRegisteredAccelerator: string | null = null; const toggleWindow = () => { - if (mb.window?.isVisible()) { + if (!mb.window) { + return; + } + + if (mb.window.isVisible()) { mb.hideWindow(); } else { mb.showWindow(); diff --git a/src/main/lifecycle/window.ts b/src/main/lifecycle/window.ts index 357de035a..81d219868 100644 --- a/src/main/lifecycle/window.ts +++ b/src/main/lifecycle/window.ts @@ -26,23 +26,27 @@ export function configureWindowEvents(mb: Menubar): void { * When DevTools is opened, resize and center the window for better visibility and allow resizing. */ mb.window.webContents.on('devtools-opened', () => { - mb.window?.setSize(800, 600); - mb.window?.center(); - if (mb.window) { - mb.window.resizable = true; + if (!mb.window) { + return; } - mb.window?.setAlwaysOnTop(true); + + mb.window.setSize(800, 600); + mb.window.center(); + mb.window.resizable = true; + mb.window.setAlwaysOnTop(true); }); /** * When DevTools is closed, restore the window to its original size and position it centered on the tray icon. */ mb.window.webContents.on('devtools-closed', () => { + if (!mb.window) { + return; + } + const trayBounds = mb.tray.getBounds(); - mb.window?.setSize(WindowConfig.width ?? 500, WindowConfig.height ?? 400); + mb.window.setSize(WindowConfig.width!, WindowConfig.height!); mb.positioner.move('trayCenter', trayBounds); - if (mb.window) { - mb.window.resizable = false; - } + mb.window.resizable = false; }); } diff --git a/src/main/utils.ts b/src/main/utils.ts index bb0c45892..7dd5b69d9 100644 --- a/src/main/utils.ts +++ b/src/main/utils.ts @@ -30,7 +30,11 @@ export function takeScreenshot(mb: Menubar) { `${dateStr}-${APPLICATION.NAME}-screenshot.png`, ); - mb.window?.capturePage().then((img) => { + if (!mb.window) { + return; + } + + mb.window.capturePage().then((img) => { fs.writeFile(capturedPicFilePath, img.toPNG(), () => logInfo('takeScreenshot', `Screenshot saved ${capturedPicFilePath}`), ); From a2adc75a3a57ee4909c99f19ad81a4d70c16600a Mon Sep 17 00:00:00 2001 From: Afonso Jorge Ramos Date: Thu, 2 Apr 2026 19:09:26 +0200 Subject: [PATCH 5/8] fix: restore null semantics for native notification url parameter --- src/renderer/utils/system/native.test.ts | 2 +- src/renderer/utils/system/native.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/renderer/utils/system/native.test.ts b/src/renderer/utils/system/native.test.ts index 040103294..0814d84fb 100644 --- a/src/renderer/utils/system/native.test.ts +++ b/src/renderer/utils/system/native.test.ts @@ -49,7 +49,7 @@ describe('renderer/utils/system/native.ts', () => { expect(window.gitify.raiseNativeNotification).toHaveBeenCalledWith( 'Gitify', 'You have 2 notifications', - '', + undefined, ); expect(url.generateGitHubWebUrl).toHaveBeenCalledTimes(0); }); diff --git a/src/renderer/utils/system/native.ts b/src/renderer/utils/system/native.ts index 71a8a5ab8..d54bc8608 100644 --- a/src/renderer/utils/system/native.ts +++ b/src/renderer/utils/system/native.ts @@ -18,7 +18,7 @@ export async function raiseNativeNotification( ) { let title: string; let body: string; - let url = ''; + let url: string | null = null; if (notifications.length === 1) { const notification = notifications[0]; @@ -32,5 +32,5 @@ export async function raiseNativeNotification( body = `You have ${notifications.length} notifications`; } - return window.gitify.raiseNativeNotification(title, body, url); + return window.gitify.raiseNativeNotification(title, body, url ?? undefined); } From 02e8d14b9f151ea2eff8b9f228419ecd044eca02 Mon Sep 17 00:00:00 2001 From: Afonso Jorge Ramos Date: Fri, 3 Apr 2026 02:39:03 +0200 Subject: [PATCH 6/8] refactor: replace err as Error casts with toError utility --- src/main/handlers/storage.test.ts | 10 +++-- src/main/handlers/storage.ts | 6 +-- src/main/lifecycle/first-run.test.ts | 10 +++-- src/main/lifecycle/first-run.ts | 4 +- src/main/updater.test.ts | 12 ++++-- src/main/updater.ts | 6 +-- src/renderer/hooks/useNotifications.ts | 33 ++++++++++----- src/renderer/routes/Accounts.tsx | 27 +++++++------ src/renderer/routes/LoginWithDeviceFlow.tsx | 6 +-- src/renderer/routes/LoginWithOAuthApp.tsx | 4 +- .../routes/LoginWithPersonalAccessToken.tsx | 4 +- src/renderer/utils/auth/flows.ts | 6 +-- src/renderer/utils/auth/utils.test.ts | 2 +- src/renderer/utils/auth/utils.ts | 11 ++--- src/renderer/utils/core/logger.ts | 4 +- .../utils/notifications/notifications.ts | 40 +++++++++---------- src/renderer/utils/notifications/url.ts | 6 +-- src/renderer/utils/system/audio.ts | 4 +- src/shared/logger.ts | 7 ++++ 19 files changed, 117 insertions(+), 85 deletions(-) diff --git a/src/main/handlers/storage.test.ts b/src/main/handlers/storage.test.ts index 91f54f2c6..88249b822 100644 --- a/src/main/handlers/storage.test.ts +++ b/src/main/handlers/storage.test.ts @@ -15,9 +15,13 @@ vi.mock('electron', () => ({ })); const logErrorMock = vi.fn(); -vi.mock('../../shared/logger', () => ({ - logError: (...args: unknown[]) => logErrorMock(...args), -})); +vi.mock('../../shared/logger', async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + logError: (...args: unknown[]) => logErrorMock(...args), + }; +}); describe('main/handlers/storage.ts', () => { describe('registerStorageHandlers', () => { diff --git a/src/main/handlers/storage.ts b/src/main/handlers/storage.ts index 417caf382..d60ddf02c 100644 --- a/src/main/handlers/storage.ts +++ b/src/main/handlers/storage.ts @@ -1,7 +1,7 @@ import { safeStorage } from 'electron'; import { EVENTS } from '../../shared/events'; -import { logError } from '../../shared/logger'; +import { logError, toError } from '../../shared/logger'; import { handleMainEvent } from '../events'; @@ -22,11 +22,11 @@ export function registerStorageHandlers(): void { handleMainEvent(EVENTS.SAFE_STORAGE_DECRYPT, (_, value: string) => { try { return safeStorage.decryptString(Buffer.from(value, 'base64')); - } catch (err: unknown) { + } catch (err) { logError( 'main:safe-storage-decrypt', 'Failed to decrypt value - data may be from old build', - err as Error, + toError(err), ); throw err; } diff --git a/src/main/lifecycle/first-run.test.ts b/src/main/lifecycle/first-run.test.ts index 4be73ca6f..4a84f955f 100644 --- a/src/main/lifecycle/first-run.test.ts +++ b/src/main/lifecycle/first-run.test.ts @@ -35,9 +35,13 @@ vi.mock('electron', () => ({ vi.mock('../utils', () => ({ isDevMode: () => false })); const logErrorMock = vi.fn(); -vi.mock('../../shared/logger', () => ({ - logError: (...a: unknown[]) => logErrorMock(...a), -})); +vi.mock('../../shared/logger', async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + logError: (...a: unknown[]) => logErrorMock(...a), + }; +}); let mac = true; vi.mock('../../shared/platform', () => ({ isMacOS: () => mac })); diff --git a/src/main/lifecycle/first-run.ts b/src/main/lifecycle/first-run.ts index 2204b9c65..78e0ed327 100644 --- a/src/main/lifecycle/first-run.ts +++ b/src/main/lifecycle/first-run.ts @@ -4,7 +4,7 @@ import path from 'node:path'; import { app, dialog } from 'electron'; import { APPLICATION } from '../../shared/constants'; -import { logError } from '../../shared/logger'; +import { logError, toError } from '../../shared/logger'; import { isMacOS } from '../../shared/platform'; import { isDevMode } from '../utils'; @@ -74,7 +74,7 @@ function checkAndMarkFirstRun(): boolean { logError( 'checkAndMarkFirstRun', 'Unable to write firstRun file', - err as Error, + toError(err), ); } diff --git a/src/main/updater.test.ts b/src/main/updater.test.ts index 3952d395d..237f0f4a2 100644 --- a/src/main/updater.test.ts +++ b/src/main/updater.test.ts @@ -4,10 +4,14 @@ import type { Menubar } from 'menubar'; import { APPLICATION } from '../shared/constants'; import { logError, logInfo } from '../shared/logger'; -vi.mock('../shared/logger', () => ({ - logInfo: vi.fn(), - logError: vi.fn(), -})); +vi.mock('../shared/logger', async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + logInfo: vi.fn(), + logError: vi.fn(), + }; +}); import MenuBuilder from './menu'; import AppUpdater from './updater'; diff --git a/src/main/updater.ts b/src/main/updater.ts index f726c76a7..f303f99ff 100644 --- a/src/main/updater.ts +++ b/src/main/updater.ts @@ -3,7 +3,7 @@ import { autoUpdater } from 'electron-updater'; import type { Menubar } from 'menubar'; import { APPLICATION } from '../shared/constants'; -import { logError, logInfo } from '../shared/logger'; +import { logError, logInfo, toError } from '../shared/logger'; import type MenuBuilder from './menu'; @@ -123,7 +123,7 @@ export default class AppUpdater { logInfo('app updater', 'Checking for updates on application launch'); await autoUpdater.checkForUpdatesAndNotify(); } catch (err) { - logError('auto updater', 'Initial check failed', err as Error); + logError('auto updater', 'Initial check failed', toError(err)); } } @@ -136,7 +136,7 @@ export default class AppUpdater { logInfo('app updater', 'Checking for updates on a periodic schedule'); await autoUpdater.checkForUpdatesAndNotify(); } catch (e) { - logError('auto updater', 'Scheduled check failed', e as Error); + logError('auto updater', 'Scheduled check failed', toError(e)); } }; diff --git a/src/renderer/hooks/useNotifications.ts b/src/renderer/hooks/useNotifications.ts index 3e0bc8d42..3c924c447 100644 --- a/src/renderer/hooks/useNotifications.ts +++ b/src/renderer/hooks/useNotifications.ts @@ -20,7 +20,7 @@ import { areAllAccountErrorsSame, doesAllAccountsHaveErrors, } from '../utils/core/errors'; -import { rendererLogError } from '../utils/core/logger'; +import { rendererLogError, toError } from '../utils/core/logger'; import { getAllNotifications, getNotificationCount, @@ -139,11 +139,11 @@ export const useNotifications = (): NotificationsState => { ); if (diffNotifications.length > 0) { - if (state.settings!.playSound) { - raiseSoundNotification(state.settings!.notificationVolume); + if (state.settings?.playSound) { + raiseSoundNotification(state.settings.notificationVolume); } - if (state.settings!.showNotifications) { + if (state.settings?.showNotifications) { raiseNativeNotification(diffNotifications); } } @@ -159,6 +159,10 @@ export const useNotifications = (): NotificationsState => { const markNotificationsAsRead = useCallback( async (state: GitifyState, readNotifications: GitifyNotification[]) => { + if (!state.settings) { + return; + } + setStatus('loading'); try { @@ -170,7 +174,7 @@ export const useNotifications = (): NotificationsState => { const updatedNotifications = removeNotificationsForAccount( readNotifications[0].account, - state.settings!, + state.settings, readNotifications, notifications, ); @@ -180,7 +184,7 @@ export const useNotifications = (): NotificationsState => { rendererLogError( 'markNotificationsAsRead', 'Error occurred while marking notifications as read', - err as Error, + toError(err), ); } @@ -191,7 +195,10 @@ export const useNotifications = (): NotificationsState => { const markNotificationsAsDone = useCallback( async (state: GitifyState, doneNotifications: GitifyNotification[]) => { - if (!isMarkAsDoneFeatureSupported(doneNotifications[0].account)) { + if ( + !state.settings || + !isMarkAsDoneFeatureSupported(doneNotifications[0].account) + ) { return; } @@ -206,7 +213,7 @@ export const useNotifications = (): NotificationsState => { const updatedNotifications = removeNotificationsForAccount( doneNotifications[0].account, - state.settings!, + state.settings, doneNotifications, notifications, ); @@ -216,7 +223,7 @@ export const useNotifications = (): NotificationsState => { rendererLogError( 'markNotificationsAsDone', 'Error occurred while marking notifications as done', - err as Error, + toError(err), ); } @@ -227,6 +234,10 @@ export const useNotifications = (): NotificationsState => { const unsubscribeNotification = useCallback( async (state: GitifyState, notification: GitifyNotification) => { + if (!state.settings) { + return; + } + setStatus('loading'); try { @@ -235,7 +246,7 @@ export const useNotifications = (): NotificationsState => { notification.id, ); - if (state.settings!.markAsDoneOnUnsubscribe) { + if (state.settings.markAsDoneOnUnsubscribe) { await markNotificationsAsDone(state, [notification]); } else { await markNotificationsAsRead(state, [notification]); @@ -244,7 +255,7 @@ export const useNotifications = (): NotificationsState => { rendererLogError( 'unsubscribeNotification', 'Error occurred while unsubscribing from notification thread', - err as Error, + toError(err), notification, ); } diff --git a/src/renderer/routes/Accounts.tsx b/src/renderer/routes/Accounts.tsx index c5466138e..096f55e49 100644 --- a/src/renderer/routes/Accounts.tsx +++ b/src/renderer/routes/Accounts.tsx @@ -36,6 +36,7 @@ import { determineFailureType } from '../utils/api/errors'; import { hasAlternateScopes, hasRecommendedScopes } from '../utils/auth/scopes'; import { getAccountUUID, refreshAccount } from '../utils/auth/utils'; import { Errors } from '../utils/core/errors'; +import { toError } from '../utils/core/logger'; import { saveState } from '../utils/core/storage'; import { openAccountProfile, @@ -85,7 +86,7 @@ export const AccountsRoute: FC = () => { } catch (err) { setRefreshErrorStates((prev) => ({ ...prev, - [accountUUID]: determineFailureType(err as Error), + [accountUUID]: determineFailureType(toError(err)), })); } @@ -153,8 +154,8 @@ export const AccountsRoute: FC = () => { {auth.accounts.map((account, i) => { - const AuthMethodIcon = getAuthMethodIcon(account.method)!; - const PlatformIcon = getPlatformIcon(account.platform)!; + const AuthMethodIcon = getAuthMethodIcon(account.method); + const PlatformIcon = getPlatformIcon(account.platform); const accountUUID = getAccountUUID(account); const accountError = getAccountError(account); const hasBadCredentials = accountError === Errors.BAD_CREDENTIALS; @@ -172,10 +173,10 @@ export const AccountsRoute: FC = () => { title="Open account profile ↗" > @@ -191,7 +192,7 @@ export const AccountsRoute: FC = () => { align="center" direction="horizontal" gap="condensed" - hidden={!account.user!.name} + hidden={!account.user?.name} > {account.user?.name} @@ -206,7 +207,7 @@ export const AccountsRoute: FC = () => { onClick={() => openHost(account.hostname)} title="Open host ↗" > - + {PlatformIcon && } {account.hostname} @@ -219,7 +220,7 @@ export const AccountsRoute: FC = () => { onClick={() => openDeveloperSettings(account)} title="Open developer settings ↗" > - + {AuthMethodIcon && } {account.method} @@ -252,7 +253,7 @@ export const AccountsRoute: FC = () => { {!hasBadCredentials && ( ( { {hasBadCredentials && ( handleReAuthenticate(account)} @@ -292,7 +293,7 @@ export const AccountsRoute: FC = () => { )} { /> logoutAccount(account)} diff --git a/src/renderer/routes/LoginWithDeviceFlow.tsx b/src/renderer/routes/LoginWithDeviceFlow.tsx index 08c8c2399..cce2b6965 100644 --- a/src/renderer/routes/LoginWithDeviceFlow.tsx +++ b/src/renderer/routes/LoginWithDeviceFlow.tsx @@ -33,7 +33,7 @@ import { getAlternateScopeNames, getRecommendedScopeNames, } from '../utils/auth/scopes'; -import { rendererLogError } from '../utils/core/logger'; +import { rendererLogError, toError } from '../utils/core/logger'; import { copyToClipboard, openExternalLink } from '../utils/system/comms'; import { openDeveloperSettings } from '../utils/system/links'; @@ -83,7 +83,7 @@ export const LoginWithDeviceFlowRoute: FC = () => { rendererLogError( 'LoginWithDeviceFlow', 'Failed to start device flow', - err as Error, + toError(err), ); setError('Failed to start authentication. Please try again.'); } @@ -130,7 +130,7 @@ export const LoginWithDeviceFlowRoute: FC = () => { rendererLogError( 'LoginWithDeviceFlow', 'Failed to poll device flow', - err as Error, + toError(err), ); setError('Authentication failed. Please try again.'); } diff --git a/src/renderer/routes/LoginWithOAuthApp.tsx b/src/renderer/routes/LoginWithOAuthApp.tsx index fc209b34d..79a88cf67 100644 --- a/src/renderer/routes/LoginWithOAuthApp.tsx +++ b/src/renderer/routes/LoginWithOAuthApp.tsx @@ -42,7 +42,7 @@ import { isValidHostname, isValidToken, } from '../utils/auth/utils'; -import { rendererLogError } from '../utils/core/logger'; +import { rendererLogError, toError } from '../utils/core/logger'; import { openExternalLink } from '../utils/system/comms'; interface LocationState { @@ -134,7 +134,7 @@ export const LoginWithOAuthAppRoute: FC = () => { rendererLogError( 'loginWithOAuthApp', 'Failed to login with OAuth App', - err as Error, + toError(err), ); setErrors({ invalidCredentialsForHost: `Failed to validate provided Client ID and Secret against ${data.hostname}`, diff --git a/src/renderer/routes/LoginWithPersonalAccessToken.tsx b/src/renderer/routes/LoginWithPersonalAccessToken.tsx index b50711c1b..6b364ff36 100644 --- a/src/renderer/routes/LoginWithPersonalAccessToken.tsx +++ b/src/renderer/routes/LoginWithPersonalAccessToken.tsx @@ -36,7 +36,7 @@ import { isValidHostname, isValidToken, } from '../utils/auth/utils'; -import { rendererLogError } from '../utils/core/logger'; +import { rendererLogError, toError } from '../utils/core/logger'; import { openExternalLink } from '../utils/system/comms'; interface LocationState { @@ -121,7 +121,7 @@ export const LoginWithPersonalAccessTokenRoute: FC = () => { rendererLogError( 'loginWithPersonalAccessToken', 'Failed to login with PAT', - err as Error, + toError(err), ); setErrors({ invalidCredentialsForHost: `Failed to validate provided token against ${data.hostname}`, diff --git a/src/renderer/utils/auth/flows.ts b/src/renderer/utils/auth/flows.ts index 8976466fe..c5748c36f 100644 --- a/src/renderer/utils/auth/flows.ts +++ b/src/renderer/utils/auth/flows.ts @@ -17,7 +17,7 @@ import type { LoginOAuthWebOptions, } from './types'; -import { rendererLogError, rendererLogInfo } from '../core/logger'; +import { rendererLogError, rendererLogInfo, toError } from '../core/logger'; import { openExternalLink } from '../system/comms'; import { getRecommendedScopeNames } from './scopes'; import { getGitHubAuthBaseUrl } from './utils'; @@ -141,7 +141,7 @@ export async function pollGitHubDeviceFlow( return authentication.token as Token; } catch (err) { if (err instanceof RequestError) { - const response = err.response!.data as DeviceFlowErrorResponse; + const response = err.response?.data as DeviceFlowErrorResponse; const errorCode = response.error; if (errorCode === 'authorization_pending' || errorCode === 'slow_down') { @@ -152,7 +152,7 @@ export async function pollGitHubDeviceFlow( rendererLogError( 'pollGitHubDeviceFlow', 'Error exchanging device code', - err as Error, + toError(err), ); throw err; diff --git a/src/renderer/utils/auth/utils.test.ts b/src/renderer/utils/auth/utils.test.ts index 865b0353b..66316c5ab 100644 --- a/src/renderer/utils/auth/utils.test.ts +++ b/src/renderer/utils/auth/utils.test.ts @@ -206,7 +206,7 @@ describe('renderer/utils/auth/utils.ts', () => { it('extractHostVersion', () => { expect(authUtils.extractHostVersion(null)).toBe('latest'); - expect(authUtils.extractHostVersion('foo')).toBe(null); + expect(authUtils.extractHostVersion('foo')).toBeUndefined(); expect(authUtils.extractHostVersion('3')).toBe('3.0.0'); diff --git a/src/renderer/utils/auth/utils.ts b/src/renderer/utils/auth/utils.ts index 7558a4344..45c471dac 100644 --- a/src/renderer/utils/auth/utils.ts +++ b/src/renderer/utils/auth/utils.ts @@ -22,6 +22,7 @@ import { rendererLogError, rendererLogInfo, rendererLogWarn, + toError, } from '../core/logger'; import { encryptValue } from '../system/comms'; import { getPlatformFromHostname } from './platform'; @@ -70,7 +71,7 @@ export async function addAccount( // Replace the existing account (e.g. re-authentication with a new token) rendererLogInfo( 'addAccount', - `updating existing account for user ${newAccount.user!.login}`, + `updating existing account for user ${newAccount.user?.login}`, ); accountList[existingIndex] = newAccount; } else { @@ -145,7 +146,7 @@ export async function refreshAccount(account: Account): Promise { rendererLogError( 'refreshAccount', `failed to refresh account for user ${account.user?.login ?? account.hostname}`, - err as Error, + toError(err), ); throw err; } @@ -162,9 +163,9 @@ export async function refreshAccount(account: Account): Promise { * @param version - The raw version string from the `x-github-enterprise-version` header. * @returns A normalized semver string, or `"latest"` if unset. */ -export function extractHostVersion(version: string | null): string { +export function extractHostVersion(version: string | null): string | undefined { if (version) { - return semver.valid(semver.coerce(version))!; + return semver.valid(semver.coerce(version)) ?? undefined; } return 'latest'; @@ -331,7 +332,7 @@ export function isValidToken(token: Token) { */ export function getAccountUUID(account: Account): AccountUUID { return btoa( - `${account.hostname}-${account.user!.id}-${account.method}`, + `${account.hostname}-${account.user?.id}-${account.method}`, ) as AccountUUID; } diff --git a/src/renderer/utils/core/logger.ts b/src/renderer/utils/core/logger.ts index 226c751f4..433d12474 100644 --- a/src/renderer/utils/core/logger.ts +++ b/src/renderer/utils/core/logger.ts @@ -1,4 +1,6 @@ -import { logError, logInfo, logWarn } from '../../../shared/logger'; +import { logError, logInfo, logWarn, toError } from '../../../shared/logger'; + +export { toError }; import type { GitifyNotification } from '../../types'; diff --git a/src/renderer/utils/notifications/notifications.ts b/src/renderer/utils/notifications/notifications.ts index bdd5c7627..fb58c910c 100644 --- a/src/renderer/utils/notifications/notifications.ts +++ b/src/renderer/utils/notifications/notifications.ts @@ -2,6 +2,7 @@ import { Constants } from '../../constants'; import type { AccountNotifications, + AuthState, GitifyNotification, GitifyState, GitifySubject, @@ -15,7 +16,7 @@ import { import { determineFailureType } from '../api/errors'; import type { FetchMergedDetailsTemplateQuery } from '../api/graphql/generated/graphql'; import { transformNotifications } from '../api/transform'; -import { rendererLogError, rendererLogWarn } from '../core/logger'; +import { rendererLogError, rendererLogWarn, toError } from '../core/logger'; import { filterBaseNotifications, filterDetailedNotifications, @@ -55,14 +56,11 @@ export function getUnreadNotificationCount( ); } -function getNotifications(state: GitifyState) { - return state.auth!.accounts.map((account) => { +function getNotifications(auth: AuthState, settings: SettingsState) { + return auth.accounts.map((account) => { return { account, - notifications: listNotificationsForAuthenticatedUser( - account, - state.settings!, - ), + notifications: listNotificationsForAuthenticatedUser(account, settings), }; }); } @@ -85,8 +83,14 @@ function getNotifications(state: GitifyState) { export async function getAllNotifications( state: GitifyState, ): Promise { + if (!state.auth || !state.settings) { + return []; + } + + const { auth, settings } = state; + const accountNotifications: AccountNotifications[] = await Promise.all( - getNotifications(state) + getNotifications(auth, settings) .filter((response) => !!response) .map(async (accountNotifications) => { try { @@ -99,15 +103,9 @@ export async function getAllNotifications( notifications = filterBaseNotifications(notifications); - notifications = await enrichNotifications( - notifications, - state.settings!, - ); + notifications = await enrichNotifications(notifications, settings); - notifications = filterDetailedNotifications( - notifications, - state.settings!, - ); + notifications = filterDetailedNotifications(notifications, settings); notifications = notifications.map((notification) => { return formatNotification(notification); @@ -122,20 +120,20 @@ export async function getAllNotifications( rendererLogError( 'getAllNotifications', 'error occurred while fetching account notifications', - err as Error, + toError(err), ); return { account: accountNotifications.account, notifications: [], - error: determineFailureType(err as Error), + error: determineFailureType(toError(err)), }; } }), ); // Set the order property for the notifications - stabilizeNotificationsOrder(accountNotifications, state.settings!); + stabilizeNotificationsOrder(accountNotifications, settings); return accountNotifications; } @@ -211,7 +209,7 @@ async function fetchNotificationDetailsInBatches( rendererLogError( 'fetchNotificationDetailsInBatches', `Failed to fetch merged notification details for batch ${batchIndex}`, - err as Error, + toError(err), ); } } @@ -244,7 +242,7 @@ export async function enrichNotification( rendererLogError( 'enrichNotification', 'failed to enrich notification details for', - err as Error, + toError(err), notification, ); diff --git a/src/renderer/utils/notifications/url.ts b/src/renderer/utils/notifications/url.ts index 88808be48..a6daa2ab5 100644 --- a/src/renderer/utils/notifications/url.ts +++ b/src/renderer/utils/notifications/url.ts @@ -1,13 +1,13 @@ import type { GitifyNotification, Link } from '../../types'; import { getHtmlUrl } from '../api/client'; -import { rendererLogError } from '../core/logger'; +import { rendererLogError, toError } from '../core/logger'; import { createNotificationHandler } from './handlers'; export function generateNotificationReferrerId( notification: GitifyNotification, ): string { - const raw = `018:NotificationThread${notification.id}:${notification.account.user!.id}`; + const raw = `018:NotificationThread${notification.id}:${notification.account.user?.id}`; return btoa(raw); } @@ -40,7 +40,7 @@ export async function generateGitHubWebUrl( rendererLogError( 'generateGitHubWebUrl', 'Failed to resolve specific notification html url for', - err as Error, + toError(err), notification, ); } diff --git a/src/renderer/utils/system/audio.ts b/src/renderer/utils/system/audio.ts index d82f62c8d..8e9cb75a5 100644 --- a/src/renderer/utils/system/audio.ts +++ b/src/renderer/utils/system/audio.ts @@ -1,6 +1,6 @@ import type { Percentage } from '../../types'; -import { rendererLogError } from '../core/logger'; +import { rendererLogError, toError } from '../core/logger'; import { volumePercentageToLevel } from '../ui/volume'; // Cache audio instance to avoid re-creating elements on every notification. @@ -30,7 +30,7 @@ export async function raiseSoundNotification(volume: Percentage) { rendererLogError( 'audio', 'Failed to play notification sound:', - err as Error, + toError(err), ); cachedAudio = null; } diff --git a/src/shared/logger.ts b/src/shared/logger.ts index a913a2d5d..d413f16fe 100644 --- a/src/shared/logger.ts +++ b/src/shared/logger.ts @@ -2,6 +2,13 @@ import log from 'electron-log'; type AllowedLogFunction = typeof log.info | typeof log.warn | typeof log.error; +/** + * Safely coerce an unknown caught value into an Error instance. + */ +export function toError(err: unknown): Error { + return err instanceof Error ? err : new Error(String(err)); +} + /** * Logs an informational message via electron-log. * From 9f8a09f25e5f6cef9d3854c6400a46244615d1f1 Mon Sep 17 00:00:00 2001 From: Adam Setch Date: Thu, 2 Apr 2026 22:26:11 -0400 Subject: [PATCH 7/8] address sonar feedback Signed-off-by: Adam Setch --- src/renderer/components/notifications/NotificationTitle.tsx | 2 +- src/renderer/utils/core/logger.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/renderer/components/notifications/NotificationTitle.tsx b/src/renderer/components/notifications/NotificationTitle.tsx index cfcd19a17..ce9025dbe 100644 --- a/src/renderer/components/notifications/NotificationTitle.tsx +++ b/src/renderer/components/notifications/NotificationTitle.tsx @@ -18,7 +18,7 @@ export const NotificationTitle: FC = ({ const parts = parseInlineCode(title); return ( - + {parts.map((part) => ( Date: Thu, 2 Apr 2026 22:28:40 -0400 Subject: [PATCH 8/8] address sonar feedback Signed-off-by: Adam Setch --- src/renderer/utils/notifications/handlers/utils.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/renderer/utils/notifications/handlers/utils.ts b/src/renderer/utils/notifications/handlers/utils.ts index 7c2e7a2cf..d05bbc971 100644 --- a/src/renderer/utils/notifications/handlers/utils.ts +++ b/src/renderer/utils/notifications/handlers/utils.ts @@ -17,22 +17,18 @@ type AuthorInput = export function getNotificationAuthor( users: AuthorInput[], ): GitifyNotificationUser | undefined { - let subjectUser: GitifyNotificationUser | undefined; - for (const user of users) { if (user) { - subjectUser = { + return { login: user.login, avatarUrl: user.avatarUrl, htmlUrl: user.htmlUrl, type: user.type, }; - - return subjectUser; } } - return subjectUser; + return undefined; } /**