Skip to content

Commit

Permalink
feat: zoom buttons (#1333)
Browse files Browse the repository at this point in the history
  • Loading branch information
afonsojramos authored Jul 9, 2024
1 parent 44bcb03 commit 88c8b24
Show file tree
Hide file tree
Showing 16 changed files with 256 additions and 8 deletions.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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"
}
}
8 changes: 8 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions src/__mocks__/electron.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,8 @@ module.exports = {
shell: {
openExternal: jest.fn(),
},
webFrame: {
setZoomLevel: jest.fn(),
getZoomLevel: jest.fn(),
},
};
1 change: 1 addition & 0 deletions src/__mocks__/state-mocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ export const mockSettings: SettingsState = {
showNotificationsCountInTray: false,
openAtStartup: false,
theme: Theme.SYSTEM,
zoomPercentage: 100,
detailedNotifications: true,
markAsDoneOnOpen: false,
showAccountHostname: false,
Expand Down
2 changes: 1 addition & 1 deletion src/components/buttons/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
86 changes: 86 additions & 0 deletions src/components/settings/AppearanceSettings.test.tsx
Original file line number Diff line number Diff line change
@@ -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();
Expand Down Expand Up @@ -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(
<AppContext.Provider
value={{
auth: mockAuth,
settings: mockSettings,
updateSetting,
}}
>
<MemoryRouter>
<AppearanceSettings />
</MemoryRouter>
</AppContext.Provider>,
);
});

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(
<AppContext.Provider
value={{
auth: mockAuth,
settings: mockSettings,
updateSetting,
}}
>
<MemoryRouter>
<AppearanceSettings />
</MemoryRouter>
</AppContext.Provider>,
);
});

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(
Expand Down
65 changes: 63 additions & 2 deletions src/components/settings/AppearanceSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand All @@ -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 (
<fieldset>
<Legend icon={PaintbrushIcon}>Appearance</Legend>
Expand All @@ -44,6 +63,48 @@ export const AppearanceSettings: FC = () => {
}}
className="mb-0"
/>
<div className="flex">
<label
htmlFor="Zoom"
className="mr-3 content-center font-medium text-sm text-gray-700 dark:text-gray-200"
>
Zoom:
</label>
<Button
label="Zoom Out"
onClick={() =>
zoomPercentage > 0 &&
webFrame.setZoomLevel(zoomPercentageToLevel(zoomPercentage - 10))
}
className="rounded-r-none"
size="xs"
>
-
</Button>
<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">
{zoomPercentage.toFixed(0)}%
</span>
<Button
label="Zoom In"
onClick={() =>
zoomPercentage < 120 &&
webFrame.setZoomLevel(zoomPercentageToLevel(zoomPercentage + 10))
}
className="rounded-none"
size="xs"
>
+
</Button>
<Button
label="Reset Zoom"
onClick={() => webFrame.setZoomLevel(0)}
variant="destructive"
className="rounded-l-none"
size="xs"
>
X
</Button>
</div>
<Checkbox
name="detailedNotifications"
label="Detailed notifications"
Expand Down
2 changes: 2 additions & 0 deletions src/components/settings/SettingsFooter.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ jest.mock('react-router-dom', () => ({
useNavigate: () => mockNavigate,
}));

global.ResizeObserver = require('resize-observer-polyfill');

describe('routes/components/settings/SettingsFooter.tsx', () => {
afterEach(() => {
jest.clearAllMocks();
Expand Down
2 changes: 2 additions & 0 deletions src/context/App.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,7 @@ describe('context/App.tsx', () => {
keyboardShortcut: true,
groupBy: 'REPOSITORY',
filterReasons: [],
zoomPercentage: 100,
} as SettingsState,
});
});
Expand Down Expand Up @@ -438,6 +439,7 @@ describe('context/App.tsx', () => {
keyboardShortcut: true,
groupBy: 'REPOSITORY',
filterReasons: [],
zoomPercentage: 100,
} as SettingsState,
});
});
Expand Down
6 changes: 6 additions & 0 deletions src/context/App.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { webFrame } from 'electron';
import {
type ReactNode,
createContext,
Expand Down Expand Up @@ -42,6 +43,7 @@ import Constants from '../utils/constants';
import { getNotificationCount } from '../utils/notifications';
import { clearState, loadState, saveState } from '../utils/storage';
import { setTheme } from '../utils/theme';
import { zoomPercentageToLevel } from '../utils/zoom';

const defaultAuth: AuthState = {
accounts: [],
Expand All @@ -62,6 +64,7 @@ export const defaultSettings: SettingsState = {
showNotificationsCountInTray: false,
openAtStartup: false,
theme: Theme.SYSTEM,
zoomPercentage: 100,
detailedNotifications: true,
markAsDoneOnOpen: false,
showAccountHostname: false,
Expand Down Expand Up @@ -244,6 +247,9 @@ export const AppProvider = ({ children }: { children: ReactNode }) => {
if (existing.settings) {
setKeyboardShortcut(existing.settings.keyboardShortcut);
setSettings({ ...defaultSettings, ...existing.settings });
webFrame.setZoomLevel(
zoomPercentageToLevel(existing.settings.zoomPercentage),
);
return existing.settings;
}
}, []);
Expand Down
3 changes: 0 additions & 3 deletions src/electron/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,9 +71,6 @@ app.whenReady().then(async () => {
mb.tray.popUpContextMenu(contextMenu, { x: bounds.x, y: bounds.y });
});

// Force the window to retrieve its previous zoom factor
mb.window.webContents.setZoomFactor(mb.window.webContents.getZoomFactor());

// Custom key events
mb.window.webContents.on('before-input-event', (event, input) => {
if (input.key === 'Escape') {
Expand Down
2 changes: 2 additions & 0 deletions src/routes/Settings.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { mockPlatform } from '../__mocks__/utils';
import { AppContext } from '../context/App';
import { SettingsRoute } from './Settings';

global.ResizeObserver = require('resize-observer-polyfill');

const mockNavigate = jest.fn();
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
Expand Down
34 changes: 34 additions & 0 deletions src/routes/__snapshots__/Settings.test.tsx.snap

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 88c8b24

Please sign in to comment.