From 42c2f7d0cdedac178a49ff6b61404d92327a43c3 Mon Sep 17 00:00:00 2001 From: Adam Setch Date: Thu, 2 Oct 2025 19:57:35 -0400 Subject: [PATCH 1/2] fix: icon color when idle Signed-off-by: Adam Setch --- package.json | 4 +- src/main/events.test.ts | 8 ++- src/main/index.ts | 51 ++++++++++--------- src/preload/index.test.ts | 16 +++--- src/preload/index.ts | 16 ++---- src/preload/utils.test.ts | 9 ++-- src/renderer/__helpers__/jest.setup.ts | 2 +- src/renderer/context/App.tsx | 25 +++++---- src/renderer/hooks/useNotifications.ts | 4 +- src/renderer/routes/Accounts.test.tsx | 7 +-- src/renderer/routes/Accounts.tsx | 4 +- src/renderer/utils/comms.test.ts | 18 ++++--- src/renderer/utils/comms.ts | 4 +- .../utils/notifications/notifications.ts | 4 +- src/shared/events.ts | 7 ++- 15 files changed, 89 insertions(+), 90 deletions(-) diff --git a/package.json b/package.json index 352804be8..27e88edf0 100644 --- a/package.json +++ b/package.json @@ -125,7 +125,7 @@ "webpack-cli": "6.0.1", "webpack-merge": "6.0.1" }, - "packageManager": "pnpm@10.18.0", + "packageManager": "pnpm@10.17.0", "pnpm": { "onlyBuiltDependencies": [ "@biomejs/biome", @@ -138,4 +138,4 @@ "*": "biome check --fix --no-errors-on-unmatched", "*.{js,ts,tsx}": "pnpm test --findRelatedTests --passWithNoTests --updateSnapshot" } -} +} \ No newline at end of file diff --git a/src/main/events.test.ts b/src/main/events.test.ts index 7960e8767..afc42f597 100644 --- a/src/main/events.test.ts +++ b/src/main/events.test.ts @@ -45,8 +45,12 @@ describe('main/events', () => { it('sendRendererEvent forwards event to webContents with data', () => { const send = jest.fn(); const mb: MockMenubar = { window: { webContents: { send } } }; - sendRendererEvent(mb as unknown as Menubar, EVENTS.UPDATE_TITLE, 'title'); - expect(send).toHaveBeenCalledWith(EVENTS.UPDATE_TITLE, 'title'); + sendRendererEvent( + mb as unknown as Menubar, + EVENTS.UPDATE_ICON_TITLE, + 'title', + ); + expect(send).toHaveBeenCalledWith(EVENTS.UPDATE_ICON_TITLE, 'title'); }); it('sendRendererEvent forwards event without data', () => { diff --git a/src/main/index.ts b/src/main/index.ts index 98a75806b..f5f63bd22 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -165,33 +165,26 @@ app.whenReady().then(async () => { EVENTS.USE_UNREAD_ACTIVE_ICON, (_, useUnreadActiveIcon: boolean) => { shouldUseUnreadActiveIcon = useUnreadActiveIcon; - - if (shouldUseUnreadActiveIcon) { - setActiveIcon(); - } else { - setIdleIcon(); - } }, ); - onMainEvent(EVENTS.ICON_ERROR, () => { + onMainEvent(EVENTS.UPDATE_ICON_COLOR, (_, notificationsCount: number) => { if (!mb.tray.isDestroyed()) { - mb.tray.setImage(TrayIcons.error); - } - }); + if (notificationsCount < 0) { + setErrorIcon(); + return; + } - onMainEvent(EVENTS.ICON_ACTIVE, () => { - if (!mb.tray.isDestroyed() && shouldUseUnreadActiveIcon) { - } - }); + if (notificationsCount > 0) { + setActiveIcon(); + return; + } - onMainEvent(EVENTS.ICON_IDLE, () => { - if (!mb.tray.isDestroyed()) { setIdleIcon(); } }); - onMainEvent(EVENTS.UPDATE_TITLE, (_, title: string) => { + onMainEvent(EVENTS.UPDATE_ICON_TITLE, (_, title: string) => { if (!mb.tray.isDestroyed()) { mb.tray.setTitle(title); } @@ -256,14 +249,6 @@ const handleURL = (url: string) => { } }; -function setActiveIcon() { - mb.tray.setImage( - menuBuilder.isUpdateAvailable() - ? TrayIcons.activeWithUpdate - : TrayIcons.active, - ); -} - function setIdleIcon() { if (shouldUseAlternateIdleIcon) { mb.tray.setImage( @@ -279,3 +264,19 @@ function setIdleIcon() { ); } } + +function setActiveIcon() { + if (shouldUseUnreadActiveIcon) { + mb.tray.setImage( + menuBuilder.isUpdateAvailable() + ? TrayIcons.activeWithUpdate + : TrayIcons.active, + ); + } else { + setIdleIcon(); + } +} + +function setErrorIcon() { + mb.tray.setImage(TrayIcons.error); +} diff --git a/src/preload/index.test.ts b/src/preload/index.test.ts index 89a01dbe0..8f1a12ec3 100644 --- a/src/preload/index.test.ts +++ b/src/preload/index.test.ts @@ -52,7 +52,7 @@ class MockNotification { MockNotification; interface TestApi { - tray: { updateIcon: (n?: number) => void }; + tray: { updateColor: (n?: number) => void }; openExternalLink: (u: string, f: boolean) => void; app: { version: () => Promise; show?: () => void; hide?: () => void }; onSystemThemeUpdate: (cb: (t: string) => void) => void; @@ -90,15 +90,15 @@ describe('preload/index', () => { expect(api).toHaveProperty('openExternalLink'); }); - it('tray.updateIcon sends correct events', async () => { + it('tray.updateColor sends correct events', async () => { await importPreload(); const api = (window as unknown as { gitify: TestApi }).gitify; // casting only in test boundary - api.tray.updateIcon(-1); - api.tray.updateIcon(5); - api.tray.updateIcon(0); - expect(sendMainEvent).toHaveBeenNthCalledWith(1, EVENTS.ICON_ERROR); - expect(sendMainEvent).toHaveBeenNthCalledWith(2, EVENTS.ICON_ACTIVE); - expect(sendMainEvent).toHaveBeenNthCalledWith(3, EVENTS.ICON_IDLE); + api.tray.updateColor(-1); + expect(sendMainEvent).toHaveBeenNthCalledWith( + 1, + EVENTS.UPDATE_ICON_COLOR, + -1, + ); }); it('openExternalLink sends event with payload', async () => { diff --git a/src/preload/index.ts b/src/preload/index.ts index c91229971..77ab36d8e 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -35,21 +35,11 @@ export const api = { }, tray: { - updateIcon: (notificationsLength = 0) => { - if (notificationsLength < 0) { - sendMainEvent(EVENTS.ICON_ERROR); - return; - } - - if (notificationsLength > 0) { - sendMainEvent(EVENTS.ICON_ACTIVE); - return; - } - - sendMainEvent(EVENTS.ICON_IDLE); + updateColor: (notificationsCount = 0) => { + sendMainEvent(EVENTS.UPDATE_ICON_COLOR, notificationsCount); }, - updateTitle: (title = '') => sendMainEvent(EVENTS.UPDATE_TITLE, title), + updateTitle: (title = '') => sendMainEvent(EVENTS.UPDATE_ICON_TITLE, title), useAlternateIdleIcon: (value: boolean) => sendMainEvent(EVENTS.USE_ALTERNATE_IDLE_ICON, value), diff --git a/src/preload/utils.test.ts b/src/preload/utils.test.ts index 9af3be541..4cc5c91c3 100644 --- a/src/preload/utils.test.ts +++ b/src/preload/utils.test.ts @@ -52,7 +52,7 @@ describe('preload/utils', () => { it('onRendererEvent registers listener and receives emitted data', () => { const handler = jest.fn(); onRendererEvent( - EVENTS.UPDATE_TITLE, + EVENTS.UPDATE_ICON_TITLE, handler as unknown as ( e: Electron.IpcRendererEvent, args: string, @@ -62,8 +62,11 @@ describe('preload/utils', () => { ipcRenderer as unknown as { __emit: (channel: string, ...a: unknown[]) => void; } - ).__emit(EVENTS.UPDATE_TITLE, 'payload'); - expect(ipcRenderer.on).toHaveBeenCalledWith(EVENTS.UPDATE_TITLE, handler); + ).__emit(EVENTS.UPDATE_ICON_TITLE, 'payload'); + expect(ipcRenderer.on).toHaveBeenCalledWith( + EVENTS.UPDATE_ICON_TITLE, + handler, + ); expect(handler).toHaveBeenCalledWith({}, 'payload'); }); }); diff --git a/src/renderer/__helpers__/jest.setup.ts b/src/renderer/__helpers__/jest.setup.ts index 9ceb9b239..bdcd805f9 100644 --- a/src/renderer/__helpers__/jest.setup.ts +++ b/src/renderer/__helpers__/jest.setup.ts @@ -26,7 +26,7 @@ window.gitify = { setLevel: jest.fn(), }, tray: { - updateIcon: jest.fn(), + updateColor: jest.fn(), updateTitle: jest.fn(), useAlternateIdleIcon: jest.fn(), useUnreadActiveIcon: jest.fn(), diff --git a/src/renderer/context/App.tsx b/src/renderer/context/App.tsx index e3c92acfd..325203ad2 100644 --- a/src/renderer/context/App.tsx +++ b/src/renderer/context/App.tsx @@ -45,6 +45,7 @@ import { setKeyboardShortcut, setUseAlternateIdleIcon, setUseUnreadActiveIcon, + updateTrayColor, updateTrayTitle, } from '../utils/comms'; import { getNotificationCount } from '../utils/notifications/notifications'; @@ -154,24 +155,22 @@ export const AppProvider = ({ children }: { children: ReactNode }) => { useEffect(() => { const count = getNotificationCount(notifications); + let title = ''; if (settings.showNotificationsCountInTray && count > 0) { - updateTrayTitle(count.toString()); - } else { - updateTrayTitle(); + title = count.toString(); } - }, [settings.showNotificationsCountInTray, notifications]); - - useEffect(() => { - const count = getNotificationCount(notifications); setUseUnreadActiveIcon(settings.useUnreadActiveIcon); - - updateTrayTitle(count.toString()); - }, [settings.useUnreadActiveIcon, notifications]); - - useEffect(() => { setUseAlternateIdleIcon(settings.useAlternateIdleIcon); - }, [settings.useAlternateIdleIcon]); + + updateTrayColor(count); + updateTrayTitle(title); + }, [ + settings.showNotificationsCountInTray, + settings.useUnreadActiveIcon, + settings.useAlternateIdleIcon, + notifications, + ]); useEffect(() => { setKeyboardShortcut(settings.keyboardShortcut); diff --git a/src/renderer/hooks/useNotifications.ts b/src/renderer/hooks/useNotifications.ts index e6dc1f2f5..bd9b9f4ae 100644 --- a/src/renderer/hooks/useNotifications.ts +++ b/src/renderer/hooks/useNotifications.ts @@ -13,7 +13,7 @@ import { markNotificationThreadAsDone, markNotificationThreadAsRead, } from '../utils/api/client'; -import { updateTrayIcon } from '../utils/comms'; +import { updateTrayColor } from '../utils/comms'; import { isMarkAsDoneFeatureSupported } from '../utils/features'; import { rendererLogError } from '../utils/logger'; import { triggerNativeNotifications } from '../utils/notifications/native'; @@ -93,7 +93,7 @@ export const useNotifications = (): NotificationsState => { if (allAccountsHaveErrors) { setStatus('error'); setGlobalError(accountErrorsAreAllSame ? accountError : null); - updateTrayIcon(-1); + updateTrayColor(-1); return; } diff --git a/src/renderer/routes/Accounts.test.tsx b/src/renderer/routes/Accounts.test.tsx index a30413c06..be3dd9aff 100644 --- a/src/renderer/routes/Accounts.test.tsx +++ b/src/renderer/routes/Accounts.test.tsx @@ -258,7 +258,7 @@ describe('renderer/routes/Accounts.tsx', () => { it('should logout', async () => { const logoutFromAccountMock = jest.fn(); - const updateTrayIconMock = jest.spyOn(comms, 'updateTrayIcon'); + const updateTrayColorMock = jest.spyOn(comms, 'updateTrayColor'); const updateTrayTitleMock = jest.spyOn(comms, 'updateTrayTitle'); await act(async () => { @@ -280,10 +280,11 @@ describe('renderer/routes/Accounts.tsx', () => { await userEvent.click(screen.getByTestId('account-logout')); expect(logoutFromAccountMock).toHaveBeenCalledTimes(1); - expect(updateTrayIconMock).toHaveBeenCalledTimes(1); - expect(updateTrayIconMock).toHaveBeenCalledWith(); + expect(updateTrayColorMock).toHaveBeenCalledTimes(1); + expect(updateTrayColorMock).toHaveBeenCalledWith(); expect(updateTrayTitleMock).toHaveBeenCalledTimes(1); expect(updateTrayTitleMock).toHaveBeenCalledWith(); + expect(mockNavigate).toHaveBeenNthCalledWith(1, -1); }); }); diff --git a/src/renderer/routes/Accounts.tsx b/src/renderer/routes/Accounts.tsx index 37047e6ba..b1bd398e0 100644 --- a/src/renderer/routes/Accounts.tsx +++ b/src/renderer/routes/Accounts.tsx @@ -34,7 +34,7 @@ import { getAccountUUID, refreshAccount, } from '../utils/auth/utils'; -import { updateTrayIcon, updateTrayTitle } from '../utils/comms'; +import { updateTrayColor, updateTrayTitle } from '../utils/comms'; import { getAuthMethodIcon, getPlatformIcon } from '../utils/icons'; import { openAccountProfile, @@ -57,7 +57,7 @@ export const AccountsRoute: FC = () => { (account: Account) => { logoutFromAccount(account); navigate(-1); - updateTrayIcon(); + updateTrayColor(); updateTrayTitle(); }, [logoutFromAccount], diff --git a/src/renderer/utils/comms.test.ts b/src/renderer/utils/comms.test.ts index 4f0181bd3..96f9a6d63 100644 --- a/src/renderer/utils/comms.test.ts +++ b/src/renderer/utils/comms.test.ts @@ -11,7 +11,7 @@ import { setKeyboardShortcut, setUseAlternateIdleIcon, showWindow, - updateTrayIcon, + updateTrayColor, updateTrayTitle, } from './comms'; import * as storage from './storage'; @@ -140,16 +140,17 @@ describe('renderer/utils/comms.ts', () => { }); describe('tray helpers', () => { - it('updates tray icon with count', () => { - updateTrayIcon(5); + it('updates tray icon color with count', () => { + updateTrayColor(5); - expect(window.gitify.tray.updateIcon).toHaveBeenCalledTimes(1); - expect(window.gitify.tray.updateIcon).toHaveBeenCalledWith(5); + expect(window.gitify.tray.updateColor).toHaveBeenCalledTimes(1); + expect(window.gitify.tray.updateColor).toHaveBeenCalledWith(5); }); - it('updates tray icon with default count', () => { - updateTrayIcon(); - expect(window.gitify.tray.updateIcon).toHaveBeenCalledTimes(1); + it('updates tray icon color with default count', () => { + updateTrayColor(); + + expect(window.gitify.tray.updateColor).toHaveBeenCalledTimes(1); }); it('updates tray title with provided value', () => { @@ -161,6 +162,7 @@ describe('renderer/utils/comms.ts', () => { it('updates tray title with default value', () => { updateTrayTitle(); + expect(window.gitify.tray.updateTitle).toHaveBeenCalledTimes(1); }); }); diff --git a/src/renderer/utils/comms.ts b/src/renderer/utils/comms.ts index 0a5e06fc2..e210f4226 100644 --- a/src/renderer/utils/comms.ts +++ b/src/renderer/utils/comms.ts @@ -57,8 +57,8 @@ export function setKeyboardShortcut(keyboardShortcut: boolean): void { window.gitify.setKeyboardShortcut(keyboardShortcut); } -export function updateTrayIcon(notificationsLength = 0): void { - window.gitify.tray.updateIcon(notificationsLength); +export function updateTrayColor(notificationsLength = 0): void { + window.gitify.tray.updateColor(notificationsLength); } export function updateTrayTitle(title = ''): void { diff --git a/src/renderer/utils/notifications/notifications.ts b/src/renderer/utils/notifications/notifications.ts index 69f630c52..67a0ad7d2 100644 --- a/src/renderer/utils/notifications/notifications.ts +++ b/src/renderer/utils/notifications/notifications.ts @@ -6,7 +6,7 @@ import type { import type { GitifySubject, Notification } from '../../typesGitHub'; import { listNotificationsForAuthenticatedUser } from '../api/client'; import { determineFailureType } from '../api/errors'; -import { updateTrayIcon } from '../comms'; +import { updateTrayColor } from '../comms'; import { rendererLogError, rendererLogWarn } from '../logger'; import { filterBaseNotifications, @@ -17,7 +17,7 @@ import { createNotificationHandler } from './handlers'; export function setTrayIconColor(notifications: AccountNotifications[]) { const allNotificationsCount = getNotificationCount(notifications); - updateTrayIcon(allNotificationsCount); + updateTrayColor(allNotificationsCount); } export function getNotificationCount(notifications: AccountNotifications[]) { diff --git a/src/shared/events.ts b/src/shared/events.ts index f13d7112b..7d4ff9390 100644 --- a/src/shared/events.ts +++ b/src/shared/events.ts @@ -4,14 +4,12 @@ const P = APPLICATION.EVENT_PREFIX; export const EVENTS = { AUTH_CALLBACK: `${P}auth-callback`, - ICON_IDLE: `${P}icon-idle`, - ICON_ACTIVE: `${P}icon-active`, - ICON_ERROR: `${P}icon-error`, QUIT: `${P}quit`, WINDOW_SHOW: `${P}window-show`, WINDOW_HIDE: `${P}window-hide`, VERSION: `${P}version`, - UPDATE_TITLE: `${P}update-title`, + UPDATE_ICON_COLOR: `${P}update-icon-color`, + UPDATE_ICON_TITLE: `${P}update-icon-title`, USE_ALTERNATE_IDLE_ICON: `${P}use-alternate-idle-icon`, USE_UNREAD_ACTIVE_ICON: `${P}use-unread-active-icon`, UPDATE_KEYBOARD_SHORTCUT: `${P}update-keyboard-shortcut`, @@ -44,6 +42,7 @@ export interface IOpenExternal { export type EventData = | string + | number | boolean | IKeyboardShortcut | IAutoLaunch From b22281e898fe585e5bf6f5773bb5415aa9b8a055 Mon Sep 17 00:00:00 2001 From: Adam Setch Date: Thu, 2 Oct 2025 19:58:51 -0400 Subject: [PATCH 2/2] fix: icon color when idle Signed-off-by: Adam Setch --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 27e88edf0..7bbfb26ba 100644 --- a/package.json +++ b/package.json @@ -138,4 +138,4 @@ "*": "biome check --fix --no-errors-on-unmatched", "*.{js,ts,tsx}": "pnpm test --findRelatedTests --passWithNoTests --updateSnapshot" } -} \ No newline at end of file +}