diff --git a/package.json b/package.json
index d0b5cf7da..9e1863559 100644
--- a/package.json
+++ b/package.json
@@ -138,6 +138,7 @@
"nock": "13.5.4",
"postcss": "8.4.39",
"postcss-loader": "8.1.1",
+ "resize-observer-polyfill": "1.5.1",
"rimraf": "5.0.8",
"style-loader": "4.0.0",
"tailwindcss": "3.4.4",
@@ -150,6 +151,6 @@
"packageManager": "pnpm@9.5.0",
"lint-staged": {
"*.{js,json,ts,tsx}": "biome format --fix",
- "*.{js,ts,tsx}": "pnpm test -- --onlyChanged -u --passWithNoTests"
+ "*.{js,ts,tsx}": "pnpm test -- --findRelatedTests -u --passWithNoTests"
}
}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 662cdb330..dc76d29a2 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -117,6 +117,9 @@ importers:
postcss-loader:
specifier: 8.1.1
version: 8.1.1(postcss@8.4.39)(typescript@5.5.3)(webpack@5.92.1(webpack-cli@5.1.4))
+ resize-observer-polyfill:
+ specifier: 1.5.1
+ version: 1.5.1
rimraf:
specifier: 5.0.8
version: 5.0.8
@@ -2595,6 +2598,9 @@ packages:
requires-port@1.0.0:
resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==}
+ resize-observer-polyfill@1.5.1:
+ resolution: {integrity: sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==}
+
resolve-alpn@1.2.1:
resolution: {integrity: sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==}
@@ -6072,6 +6078,8 @@ snapshots:
requires-port@1.0.0: {}
+ resize-observer-polyfill@1.5.1: {}
+
resolve-alpn@1.2.1: {}
resolve-cwd@3.0.0:
diff --git a/src/__mocks__/electron.js b/src/__mocks__/electron.js
index 3362dc13e..c2d0e68c2 100644
--- a/src/__mocks__/electron.js
+++ b/src/__mocks__/electron.js
@@ -49,4 +49,8 @@ module.exports = {
shell: {
openExternal: jest.fn(),
},
+ webFrame: {
+ setZoomLevel: jest.fn(),
+ getZoomLevel: jest.fn(),
+ },
};
diff --git a/src/__mocks__/state-mocks.ts b/src/__mocks__/state-mocks.ts
index 5226657c3..9885f0b10 100644
--- a/src/__mocks__/state-mocks.ts
+++ b/src/__mocks__/state-mocks.ts
@@ -79,6 +79,7 @@ export const mockSettings: SettingsState = {
showNotificationsCountInTray: false,
openAtStartup: false,
theme: Theme.SYSTEM,
+ zoomPercentage: 100,
detailedNotifications: true,
markAsDoneOnOpen: false,
showAccountHostname: false,
diff --git a/src/components/buttons/Button.tsx b/src/components/buttons/Button.tsx
index fa3c6bd98..c51696b10 100644
--- a/src/components/buttons/Button.tsx
+++ b/src/components/buttons/Button.tsx
@@ -19,7 +19,7 @@ const buttonVariants = cva(
},
size: {
default: 'min-w-20 h-10 px-4 py-1',
- xs: 'h-6 rounded-md px-2',
+ xs: 'h-7 rounded-md px-2 py-1',
sm: 'h-9 rounded-md px-2 py-1',
lg: 'h-11 rounded-md px-8',
icon: 'h-10 w-10',
diff --git a/src/components/settings/AppearanceSettings.test.tsx b/src/components/settings/AppearanceSettings.test.tsx
index d7ea21381..843b39d05 100644
--- a/src/components/settings/AppearanceSettings.test.tsx
+++ b/src/components/settings/AppearanceSettings.test.tsx
@@ -1,11 +1,15 @@
import { act, fireEvent, render, screen } from '@testing-library/react';
+import { webFrame } from 'electron';
import { MemoryRouter } from 'react-router-dom';
import { mockAuth, mockSettings } from '../../__mocks__/state-mocks';
import { AppContext } from '../../context/App';
import { AppearanceSettings } from './AppearanceSettings';
+global.ResizeObserver = require('resize-observer-polyfill');
+
describe('routes/components/settings/AppearanceSettings.tsx', () => {
const updateSetting = jest.fn();
+ const zoomTimeout = () => new Promise((r) => setTimeout(r, 300));
afterEach(() => {
jest.clearAllMocks();
@@ -34,6 +38,88 @@ describe('routes/components/settings/AppearanceSettings.tsx', () => {
expect(updateSetting).toHaveBeenCalledWith('theme', 'LIGHT');
});
+ it('should update the zoom value when using CMD + and CMD -', async () => {
+ webFrame.getZoomLevel = jest.fn().mockReturnValue(-1);
+
+ await act(async () => {
+ render(
+
+
+
+
+ ,
+ );
+ });
+
+ fireEvent(window, new Event('resize'));
+ await zoomTimeout();
+
+ expect(updateSetting).toHaveBeenCalledTimes(1);
+ expect(updateSetting).toHaveBeenCalledWith('zoomPercentage', 50);
+ });
+
+ it('should update the zoom values when using the zoom buttons', async () => {
+ webFrame.getZoomLevel = jest.fn().mockReturnValue(0);
+ webFrame.setZoomLevel = jest.fn().mockImplementation((level) => {
+ webFrame.getZoomLevel = jest.fn().mockReturnValue(level);
+ fireEvent(window, new Event('resize'));
+ });
+
+ await act(async () => {
+ render(
+
+
+
+
+ ,
+ );
+ });
+
+ await act(async () => {
+ fireEvent.click(screen.getByLabelText('Zoom Out'));
+ await zoomTimeout();
+ });
+
+ expect(updateSetting).toHaveBeenCalledTimes(1);
+ expect(updateSetting).toHaveBeenCalledWith('zoomPercentage', 90);
+
+ await act(async () => {
+ fireEvent.click(screen.getByLabelText('Zoom Out'));
+ await zoomTimeout();
+
+ expect(updateSetting).toHaveBeenCalledTimes(2);
+ expect(updateSetting).toHaveBeenNthCalledWith(2, 'zoomPercentage', 80);
+ });
+
+ await act(async () => {
+ fireEvent.click(screen.getByLabelText('Zoom In'));
+ await zoomTimeout();
+
+ expect(updateSetting).toHaveBeenCalledTimes(3);
+ expect(updateSetting).toHaveBeenNthCalledWith(3, 'zoomPercentage', 90);
+ });
+
+ await act(async () => {
+ fireEvent.click(screen.getByLabelText('Reset Zoom'));
+ await zoomTimeout();
+
+ expect(updateSetting).toHaveBeenCalledTimes(4);
+ expect(updateSetting).toHaveBeenNthCalledWith(4, 'zoomPercentage', 100);
+ });
+ });
+
it('should toggle detailed notifications checkbox', async () => {
await act(async () => {
render(
diff --git a/src/components/settings/AppearanceSettings.tsx b/src/components/settings/AppearanceSettings.tsx
index ae0b959b7..f0de6d028 100644
--- a/src/components/settings/AppearanceSettings.tsx
+++ b/src/components/settings/AppearanceSettings.tsx
@@ -7,17 +7,25 @@ import {
PaintbrushIcon,
TagIcon,
} from '@primer/octicons-react';
-import { ipcRenderer } from 'electron';
-import { type FC, useContext, useEffect } from 'react';
+import { ipcRenderer, webFrame } from 'electron';
+import { type FC, useContext, useEffect, useState } from 'react';
import { AppContext } from '../../context/App';
import { Size, Theme } from '../../types';
import { setTheme } from '../../utils/theme';
+import { zoomLevelToPercentage, zoomPercentageToLevel } from '../../utils/zoom';
+import { Button } from '../buttons/Button';
import { Checkbox } from '../fields/Checkbox';
import { RadioGroup } from '../fields/RadioGroup';
import { Legend } from './Legend';
+let timeout: NodeJS.Timeout;
+const DELAY = 200;
+
export const AppearanceSettings: FC = () => {
const { settings, updateSetting } = useContext(AppContext);
+ const [zoomPercentage, setZoomPercentage] = useState(
+ zoomLevelToPercentage(webFrame.getZoomLevel()),
+ );
useEffect(() => {
ipcRenderer.on('gitify:update-theme', (_, updatedTheme: Theme) => {
@@ -27,6 +35,17 @@ export const AppearanceSettings: FC = () => {
});
}, [settings.theme]);
+ window.addEventListener('resize', () => {
+ // clear the timeout
+ clearTimeout(timeout);
+ // start timing for event "completion"
+ timeout = setTimeout(() => {
+ const zoomPercentage = zoomLevelToPercentage(webFrame.getZoomLevel());
+ setZoomPercentage(zoomPercentage);
+ updateSetting('zoomPercentage', zoomPercentage);
+ }, DELAY);
+ });
+
return (