diff --git a/src/renderer/__mocks__/state-mocks.ts b/src/renderer/__mocks__/state-mocks.ts index c3a5964df..f8df09750 100644 --- a/src/renderer/__mocks__/state-mocks.ts +++ b/src/renderer/__mocks__/state-mocks.ts @@ -89,6 +89,7 @@ const mockAppearanceSettings: AppearanceSettingsState = { const mockNotificationSettings: NotificationSettingsState = { groupBy: GroupBy.REPOSITORY, fetchType: FetchType.INTERVAL, + fetchInterval: Constants.DEFAULT_FETCH_NOTIFICATIONS_INTERVAL_MS, fetchAllNotifications: true, detailedNotifications: true, showPills: true, diff --git a/src/renderer/components/settings/NotificationSettings.test.tsx b/src/renderer/components/settings/NotificationSettings.test.tsx index bc99033bf..c28152685 100644 --- a/src/renderer/components/settings/NotificationSettings.test.tsx +++ b/src/renderer/components/settings/NotificationSettings.test.tsx @@ -2,6 +2,7 @@ import { act, render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { mockAuth, mockSettings } from '../../__mocks__/state-mocks'; +import { Constants } from '../../constants'; import { AppContext } from '../../context/App'; import * as comms from '../../utils/comms'; import { NotificationSettings } from './NotificationSettings'; @@ -55,6 +56,161 @@ describe('renderer/components/settings/NotificationSettings.tsx', () => { expect(updateSetting).toHaveBeenCalledWith('fetchType', 'INACTIVITY'); }); + describe('fetch interval settings', () => { + it('should update the fetch interval values when using the buttons', async () => { + await act(async () => { + render( + + + , + ); + }); + + // Increase fetch interval + await act(async () => { + await userEvent.click( + screen.getByTestId('settings-fetch-interval-increase'), + ); + + expect(updateSetting).toHaveBeenCalledTimes(1); + expect(updateSetting).toHaveBeenCalledWith('fetchInterval', 120000); + }); + + await act(async () => { + await userEvent.click( + screen.getByTestId('settings-fetch-interval-increase'), + ); + + expect(updateSetting).toHaveBeenCalledTimes(2); + expect(updateSetting).toHaveBeenNthCalledWith( + 2, + 'fetchInterval', + 180000, + ); + }); + + // Decrease fetch interval + await act(async () => { + await userEvent.click( + screen.getByTestId('settings-fetch-interval-decrease'), + ); + + expect(updateSetting).toHaveBeenCalledTimes(3); + expect(updateSetting).toHaveBeenNthCalledWith( + 3, + 'fetchInterval', + 120000, + ); + }); + + // Fetch interval reset + await act(async () => { + await userEvent.click( + screen.getByTestId('settings-fetch-interval-reset'), + ); + + expect(updateSetting).toHaveBeenCalledTimes(4); + expect(updateSetting).toHaveBeenNthCalledWith( + 4, + 'fetchInterval', + 60000, + ); + }); + }); + + it('should prevent going lower than minimum interval', async () => { + await act(async () => { + render( + + + , + ); + }); + + await act(async () => { + await userEvent.click( + screen.getByTestId('settings-fetch-interval-decrease'), + ); + + expect(updateSetting).toHaveBeenCalledTimes(1); + expect(updateSetting).toHaveBeenNthCalledWith( + 1, + 'fetchInterval', + 60000, + ); + }); + + // Attempt to go below the minimum interval, update settings should not be called + await act(async () => { + await userEvent.click( + screen.getByTestId('settings-fetch-interval-decrease'), + ); + + expect(updateSetting).toHaveBeenCalledTimes(1); + }); + }); + + it('should prevent going above maximum interval', async () => { + await act(async () => { + render( + + + , + ); + }); + + await act(async () => { + await userEvent.click( + screen.getByTestId('settings-fetch-interval-increase'), + ); + + expect(updateSetting).toHaveBeenCalledTimes(1); + expect(updateSetting).toHaveBeenNthCalledWith( + 1, + 'fetchInterval', + 3600000, + ); + }); + + // Attempt to go above the maximum interval, update settings should not be called + await act(async () => { + await userEvent.click( + screen.getByTestId('settings-fetch-interval-increase'), + ); + + expect(updateSetting).toHaveBeenCalledTimes(1); + }); + }); + }); + it('should toggle the fetchAllNotifications checkbox', async () => { await act(async () => { render( diff --git a/src/renderer/components/settings/NotificationSettings.tsx b/src/renderer/components/settings/NotificationSettings.tsx index f82c9ad17..b720a3c96 100644 --- a/src/renderer/components/settings/NotificationSettings.tsx +++ b/src/renderer/components/settings/NotificationSettings.tsx @@ -1,27 +1,47 @@ -import { type FC, type MouseEvent, useContext } from 'react'; +import { + type FC, + type MouseEvent, + useContext, + useEffect, + useState, +} from 'react'; import { BellIcon, CheckIcon, CommentIcon, + DashIcon, GitPullRequestIcon, IssueOpenedIcon, MilestoneIcon, + PlusIcon, + SyncIcon, TagIcon, } from '@primer/octicons-react'; -import { Stack, Text } from '@primer/react'; +import { Button, ButtonGroup, IconButton, Stack, Text } from '@primer/react'; + +import { formatDuration, millisecondsToMinutes } from 'date-fns'; import { APPLICATION } from '../../../shared/constants'; +import { Constants } from '../../constants'; import { AppContext } from '../../context/App'; import { FetchType, GroupBy, Size } from '../../types'; import { openGitHubParticipatingDocs } from '../../utils/links'; import { Checkbox } from '../fields/Checkbox'; +import { FieldLabel } from '../fields/FieldLabel'; import { RadioGroup } from '../fields/RadioGroup'; import { Title } from '../primitives/Title'; export const NotificationSettings: FC = () => { const { settings, updateSetting } = useContext(AppContext); + const [fetchInterval, setFetchInterval] = useState( + settings.fetchInterval, + ); + + useEffect(() => { + setFetchInterval(settings.fetchInterval); + }, [settings.fetchInterval]); return ( @@ -68,6 +88,81 @@ export const NotificationSettings: FC = () => { value={settings.fetchType} /> + + + + + { + const newInterval = Math.max( + fetchInterval - + Constants.FETCH_NOTIFICATIONS_INTERVAL_STEP_MS, + Constants.MIN_FETCH_NOTIFICATIONS_INTERVAL_MS, + ); + + if (newInterval !== fetchInterval) { + setFetchInterval(newInterval); + updateSetting('fetchInterval', newInterval); + } + }} + size="small" + unsafeDisableTooltip={true} + /> + + + {formatDuration({ + minutes: millisecondsToMinutes(fetchInterval), + })} + + + { + const newInterval = Math.min( + fetchInterval + + Constants.FETCH_NOTIFICATIONS_INTERVAL_STEP_MS, + Constants.MAX_FETCH_NOTIFICATIONS_INTERVAL_MS, + ); + + if (newInterval !== fetchInterval) { + setFetchInterval(newInterval); + updateSetting('fetchInterval', newInterval); + } + }} + size="small" + unsafeDisableTooltip={true} + /> + + { + setFetchInterval( + Constants.DEFAULT_FETCH_NOTIFICATIONS_INTERVAL_MS, + ); + updateSetting( + 'fetchInterval', + Constants.DEFAULT_FETCH_NOTIFICATIONS_INTERVAL_MS, + ); + }} + size="small" + unsafeDisableTooltip={true} + variant="danger" + /> + + + { ); act(() => { - jest.advanceTimersByTime(Constants.FETCH_NOTIFICATIONS_INTERVAL_MS); + jest.advanceTimersByTime( + Constants.DEFAULT_FETCH_NOTIFICATIONS_INTERVAL_MS, + ); return; }); expect(fetchNotificationsMock).toHaveBeenCalledTimes(2); act(() => { - jest.advanceTimersByTime(Constants.FETCH_NOTIFICATIONS_INTERVAL_MS); + jest.advanceTimersByTime( + Constants.DEFAULT_FETCH_NOTIFICATIONS_INTERVAL_MS, + ); return; }); expect(fetchNotificationsMock).toHaveBeenCalledTimes(3); act(() => { - jest.advanceTimersByTime(Constants.FETCH_NOTIFICATIONS_INTERVAL_MS); + jest.advanceTimersByTime( + Constants.DEFAULT_FETCH_NOTIFICATIONS_INTERVAL_MS, + ); return; }); expect(fetchNotificationsMock).toHaveBeenCalledTimes(4); diff --git a/src/renderer/context/App.tsx b/src/renderer/context/App.tsx index 7533ba95e..c001167fb 100644 --- a/src/renderer/context/App.tsx +++ b/src/renderer/context/App.tsx @@ -148,18 +148,14 @@ export const AppProvider = ({ children }: { children: ReactNode }) => { () => { fetchNotifications({ auth, settings }); }, - settings.fetchType === FetchType.INTERVAL - ? Constants.FETCH_NOTIFICATIONS_INTERVAL_MS - : null, + settings.fetchType === FetchType.INTERVAL ? settings.fetchInterval : null, ); useInactivityTimer( () => { fetchNotifications({ auth, settings }); }, - settings.fetchType === FetchType.INACTIVITY - ? Constants.FETCH_NOTIFICATIONS_INTERVAL_MS - : null, + settings.fetchType === FetchType.INACTIVITY ? settings.fetchInterval : null, ); useIntervalTimer(() => { diff --git a/src/renderer/context/defaults.ts b/src/renderer/context/defaults.ts index decf204de..38ead715a 100644 --- a/src/renderer/context/defaults.ts +++ b/src/renderer/context/defaults.ts @@ -1,3 +1,4 @@ +import { Constants } from '../constants'; import { type AppearanceSettingsState, type AuthState, @@ -27,6 +28,7 @@ const defaultAppearanceSettings: AppearanceSettingsState = { const defaultNotificationSettings: NotificationSettingsState = { groupBy: GroupBy.REPOSITORY, fetchType: FetchType.INTERVAL, + fetchInterval: Constants.DEFAULT_FETCH_NOTIFICATIONS_INTERVAL_MS, fetchAllNotifications: true, detailedNotifications: true, showPills: true, diff --git a/src/renderer/routes/__snapshots__/Settings.test.tsx.snap b/src/renderer/routes/__snapshots__/Settings.test.tsx.snap index 31fa3517c..91000589a 100644 --- a/src/renderer/routes/__snapshots__/Settings.test.tsx.snap +++ b/src/renderer/routes/__snapshots__/Settings.test.tsx.snap @@ -681,6 +681,145 @@ exports[`renderer/routes/Settings.tsx should render itself & its children 1`] = + + + Fetch interval: + + + + + + + + + + + + + + 1 minute + + + + + + + + + + + + + + + + + + + + @@ -1711,7 +1850,7 @@ exports[`renderer/routes/Settings.tsx should render itself & its children 1`] = data-wrap="nowrap" > Accounts diff --git a/src/renderer/types.ts b/src/renderer/types.ts index 587a28126..33964953b 100644 --- a/src/renderer/types.ts +++ b/src/renderer/types.ts @@ -79,6 +79,7 @@ export interface AppearanceSettingsState { export interface NotificationSettingsState { groupBy: GroupBy; fetchType: FetchType; + fetchInterval: number; fetchAllNotifications: boolean; detailedNotifications: boolean; showPills: boolean;