Skip to content

Commit 88c8b24

Browse files
authored
feat: zoom buttons (#1333)
1 parent 44bcb03 commit 88c8b24

File tree

16 files changed

+256
-8
lines changed

16 files changed

+256
-8
lines changed

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,7 @@
138138
"nock": "13.5.4",
139139
"postcss": "8.4.39",
140140
"postcss-loader": "8.1.1",
141+
"resize-observer-polyfill": "1.5.1",
141142
"rimraf": "5.0.8",
142143
"style-loader": "4.0.0",
143144
"tailwindcss": "3.4.4",
@@ -150,6 +151,6 @@
150151
"packageManager": "[email protected]",
151152
"lint-staged": {
152153
"*.{js,json,ts,tsx}": "biome format --fix",
153-
"*.{js,ts,tsx}": "pnpm test -- --onlyChanged -u --passWithNoTests"
154+
"*.{js,ts,tsx}": "pnpm test -- --findRelatedTests -u --passWithNoTests"
154155
}
155156
}

pnpm-lock.yaml

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/__mocks__/electron.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,4 +49,8 @@ module.exports = {
4949
shell: {
5050
openExternal: jest.fn(),
5151
},
52+
webFrame: {
53+
setZoomLevel: jest.fn(),
54+
getZoomLevel: jest.fn(),
55+
},
5256
};

src/__mocks__/state-mocks.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ export const mockSettings: SettingsState = {
7979
showNotificationsCountInTray: false,
8080
openAtStartup: false,
8181
theme: Theme.SYSTEM,
82+
zoomPercentage: 100,
8283
detailedNotifications: true,
8384
markAsDoneOnOpen: false,
8485
showAccountHostname: false,

src/components/buttons/Button.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ const buttonVariants = cva(
1919
},
2020
size: {
2121
default: 'min-w-20 h-10 px-4 py-1',
22-
xs: 'h-6 rounded-md px-2',
22+
xs: 'h-7 rounded-md px-2 py-1',
2323
sm: 'h-9 rounded-md px-2 py-1',
2424
lg: 'h-11 rounded-md px-8',
2525
icon: 'h-10 w-10',

src/components/settings/AppearanceSettings.test.tsx

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
import { act, fireEvent, render, screen } from '@testing-library/react';
2+
import { webFrame } from 'electron';
23
import { MemoryRouter } from 'react-router-dom';
34
import { mockAuth, mockSettings } from '../../__mocks__/state-mocks';
45
import { AppContext } from '../../context/App';
56
import { AppearanceSettings } from './AppearanceSettings';
67

8+
global.ResizeObserver = require('resize-observer-polyfill');
9+
710
describe('routes/components/settings/AppearanceSettings.tsx', () => {
811
const updateSetting = jest.fn();
12+
const zoomTimeout = () => new Promise((r) => setTimeout(r, 300));
913

1014
afterEach(() => {
1115
jest.clearAllMocks();
@@ -34,6 +38,88 @@ describe('routes/components/settings/AppearanceSettings.tsx', () => {
3438
expect(updateSetting).toHaveBeenCalledWith('theme', 'LIGHT');
3539
});
3640

41+
it('should update the zoom value when using CMD + and CMD -', async () => {
42+
webFrame.getZoomLevel = jest.fn().mockReturnValue(-1);
43+
44+
await act(async () => {
45+
render(
46+
<AppContext.Provider
47+
value={{
48+
auth: mockAuth,
49+
settings: mockSettings,
50+
updateSetting,
51+
}}
52+
>
53+
<MemoryRouter>
54+
<AppearanceSettings />
55+
</MemoryRouter>
56+
</AppContext.Provider>,
57+
);
58+
});
59+
60+
fireEvent(window, new Event('resize'));
61+
await zoomTimeout();
62+
63+
expect(updateSetting).toHaveBeenCalledTimes(1);
64+
expect(updateSetting).toHaveBeenCalledWith('zoomPercentage', 50);
65+
});
66+
67+
it('should update the zoom values when using the zoom buttons', async () => {
68+
webFrame.getZoomLevel = jest.fn().mockReturnValue(0);
69+
webFrame.setZoomLevel = jest.fn().mockImplementation((level) => {
70+
webFrame.getZoomLevel = jest.fn().mockReturnValue(level);
71+
fireEvent(window, new Event('resize'));
72+
});
73+
74+
await act(async () => {
75+
render(
76+
<AppContext.Provider
77+
value={{
78+
auth: mockAuth,
79+
settings: mockSettings,
80+
updateSetting,
81+
}}
82+
>
83+
<MemoryRouter>
84+
<AppearanceSettings />
85+
</MemoryRouter>
86+
</AppContext.Provider>,
87+
);
88+
});
89+
90+
await act(async () => {
91+
fireEvent.click(screen.getByLabelText('Zoom Out'));
92+
await zoomTimeout();
93+
});
94+
95+
expect(updateSetting).toHaveBeenCalledTimes(1);
96+
expect(updateSetting).toHaveBeenCalledWith('zoomPercentage', 90);
97+
98+
await act(async () => {
99+
fireEvent.click(screen.getByLabelText('Zoom Out'));
100+
await zoomTimeout();
101+
102+
expect(updateSetting).toHaveBeenCalledTimes(2);
103+
expect(updateSetting).toHaveBeenNthCalledWith(2, 'zoomPercentage', 80);
104+
});
105+
106+
await act(async () => {
107+
fireEvent.click(screen.getByLabelText('Zoom In'));
108+
await zoomTimeout();
109+
110+
expect(updateSetting).toHaveBeenCalledTimes(3);
111+
expect(updateSetting).toHaveBeenNthCalledWith(3, 'zoomPercentage', 90);
112+
});
113+
114+
await act(async () => {
115+
fireEvent.click(screen.getByLabelText('Reset Zoom'));
116+
await zoomTimeout();
117+
118+
expect(updateSetting).toHaveBeenCalledTimes(4);
119+
expect(updateSetting).toHaveBeenNthCalledWith(4, 'zoomPercentage', 100);
120+
});
121+
});
122+
37123
it('should toggle detailed notifications checkbox', async () => {
38124
await act(async () => {
39125
render(

src/components/settings/AppearanceSettings.tsx

Lines changed: 63 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,25 @@ import {
77
PaintbrushIcon,
88
TagIcon,
99
} from '@primer/octicons-react';
10-
import { ipcRenderer } from 'electron';
11-
import { type FC, useContext, useEffect } from 'react';
10+
import { ipcRenderer, webFrame } from 'electron';
11+
import { type FC, useContext, useEffect, useState } from 'react';
1212
import { AppContext } from '../../context/App';
1313
import { Size, Theme } from '../../types';
1414
import { setTheme } from '../../utils/theme';
15+
import { zoomLevelToPercentage, zoomPercentageToLevel } from '../../utils/zoom';
16+
import { Button } from '../buttons/Button';
1517
import { Checkbox } from '../fields/Checkbox';
1618
import { RadioGroup } from '../fields/RadioGroup';
1719
import { Legend } from './Legend';
1820

21+
let timeout: NodeJS.Timeout;
22+
const DELAY = 200;
23+
1924
export const AppearanceSettings: FC = () => {
2025
const { settings, updateSetting } = useContext(AppContext);
26+
const [zoomPercentage, setZoomPercentage] = useState(
27+
zoomLevelToPercentage(webFrame.getZoomLevel()),
28+
);
2129

2230
useEffect(() => {
2331
ipcRenderer.on('gitify:update-theme', (_, updatedTheme: Theme) => {
@@ -27,6 +35,17 @@ export const AppearanceSettings: FC = () => {
2735
});
2836
}, [settings.theme]);
2937

38+
window.addEventListener('resize', () => {
39+
// clear the timeout
40+
clearTimeout(timeout);
41+
// start timing for event "completion"
42+
timeout = setTimeout(() => {
43+
const zoomPercentage = zoomLevelToPercentage(webFrame.getZoomLevel());
44+
setZoomPercentage(zoomPercentage);
45+
updateSetting('zoomPercentage', zoomPercentage);
46+
}, DELAY);
47+
});
48+
3049
return (
3150
<fieldset>
3251
<Legend icon={PaintbrushIcon}>Appearance</Legend>
@@ -44,6 +63,48 @@ export const AppearanceSettings: FC = () => {
4463
}}
4564
className="mb-0"
4665
/>
66+
<div className="flex">
67+
<label
68+
htmlFor="Zoom"
69+
className="mr-3 content-center font-medium text-sm text-gray-700 dark:text-gray-200"
70+
>
71+
Zoom:
72+
</label>
73+
<Button
74+
label="Zoom Out"
75+
onClick={() =>
76+
zoomPercentage > 0 &&
77+
webFrame.setZoomLevel(zoomPercentageToLevel(zoomPercentage - 10))
78+
}
79+
className="rounded-r-none"
80+
size="xs"
81+
>
82+
-
83+
</Button>
84+
<span className="flex w-16 items-center justify-center rounded-none border border-gray-300 bg-transparent text-sm text-gray-700 dark:text-gray-200">
85+
{zoomPercentage.toFixed(0)}%
86+
</span>
87+
<Button
88+
label="Zoom In"
89+
onClick={() =>
90+
zoomPercentage < 120 &&
91+
webFrame.setZoomLevel(zoomPercentageToLevel(zoomPercentage + 10))
92+
}
93+
className="rounded-none"
94+
size="xs"
95+
>
96+
+
97+
</Button>
98+
<Button
99+
label="Reset Zoom"
100+
onClick={() => webFrame.setZoomLevel(0)}
101+
variant="destructive"
102+
className="rounded-l-none"
103+
size="xs"
104+
>
105+
X
106+
</Button>
107+
</div>
47108
<Checkbox
48109
name="detailedNotifications"
49110
label="Detailed notifications"

src/components/settings/SettingsFooter.test.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ jest.mock('react-router-dom', () => ({
1111
useNavigate: () => mockNavigate,
1212
}));
1313

14+
global.ResizeObserver = require('resize-observer-polyfill');
15+
1416
describe('routes/components/settings/SettingsFooter.tsx', () => {
1517
afterEach(() => {
1618
jest.clearAllMocks();

src/context/App.test.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -383,6 +383,7 @@ describe('context/App.tsx', () => {
383383
keyboardShortcut: true,
384384
groupBy: 'REPOSITORY',
385385
filterReasons: [],
386+
zoomPercentage: 100,
386387
} as SettingsState,
387388
});
388389
});
@@ -438,6 +439,7 @@ describe('context/App.tsx', () => {
438439
keyboardShortcut: true,
439440
groupBy: 'REPOSITORY',
440441
filterReasons: [],
442+
zoomPercentage: 100,
441443
} as SettingsState,
442444
});
443445
});

src/context/App.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { webFrame } from 'electron';
12
import {
23
type ReactNode,
34
createContext,
@@ -42,6 +43,7 @@ import Constants from '../utils/constants';
4243
import { getNotificationCount } from '../utils/notifications';
4344
import { clearState, loadState, saveState } from '../utils/storage';
4445
import { setTheme } from '../utils/theme';
46+
import { zoomPercentageToLevel } from '../utils/zoom';
4547

4648
const defaultAuth: AuthState = {
4749
accounts: [],
@@ -62,6 +64,7 @@ export const defaultSettings: SettingsState = {
6264
showNotificationsCountInTray: false,
6365
openAtStartup: false,
6466
theme: Theme.SYSTEM,
67+
zoomPercentage: 100,
6568
detailedNotifications: true,
6669
markAsDoneOnOpen: false,
6770
showAccountHostname: false,
@@ -244,6 +247,9 @@ export const AppProvider = ({ children }: { children: ReactNode }) => {
244247
if (existing.settings) {
245248
setKeyboardShortcut(existing.settings.keyboardShortcut);
246249
setSettings({ ...defaultSettings, ...existing.settings });
250+
webFrame.setZoomLevel(
251+
zoomPercentageToLevel(existing.settings.zoomPercentage),
252+
);
247253
return existing.settings;
248254
}
249255
}, []);

0 commit comments

Comments
 (0)