🔔 Add setting to disable update notifications (#5725)

* add setting to disable update notifications

* release notes

* fix dependency array

* Update packages/loot-core/src/server/preferences/app.ts

Co-authored-by: Matt Fiddaman <github@m.fiddaman.uk>

* moving latest version info into app state to prevent needless calls to external api

* fix release note

* Update VRT

---------

Co-authored-by: Matt Fiddaman <github@m.fiddaman.uk>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
This commit is contained in:
Michael Clark
2025-09-15 08:12:24 +00:00
committed by GitHub
parent 9eb0e04c6a
commit 91b838c539
15 changed files with 110 additions and 55 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 73 KiB

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 73 KiB

After

Width:  |  Height:  |  Size: 75 KiB

View File

@@ -13,6 +13,7 @@ import { syncAccounts } from '@desktop-client/accounts/accountsSlice';
import { pushModal } from '@desktop-client/modals/modalsSlice';
import { loadPrefs } from '@desktop-client/prefs/prefsSlice';
import { createAppAsyncThunk } from '@desktop-client/redux';
import { getIsOutdated, getLatestVersion } from '@desktop-client/util/versions';
const sliceName = 'app';
@@ -25,6 +26,10 @@ type AppState = {
} | null;
showUpdateNotification: boolean;
managerHasInitialized: boolean;
versionInfo: {
latestVersion: string;
isOutdated: boolean;
} | null;
};
const initialState: AppState = {
@@ -32,6 +37,7 @@ const initialState: AppState = {
updateInfo: null,
showUpdateNotification: true,
managerHasInitialized: false,
versionInfo: null,
};
export const resetApp = createAction(`${sliceName}/resetApp`);
@@ -104,6 +110,26 @@ export const sync = createAppAsyncThunk(
},
);
export const getLatestAppVersion = createAppAsyncThunk(
`${sliceName}/getLatestAppVersion`,
async (_, { dispatch, getState }) => {
const globalPrefs = getState().prefs.global;
if (globalPrefs && globalPrefs.notifyWhenUpdateIsAvailable) {
const theLatestVersion = await getLatestVersion();
dispatch(
setAppState({
versionInfo: {
latestVersion: theLatestVersion,
isOutdated: getIsOutdated(theLatestVersion),
},
}),
);
}
return;
},
);
type SyncAndDownloadPayload = {
accountId?: AccountEntity['id'] | string;
};
@@ -173,6 +199,7 @@ export const actions = {
resetSync,
sync,
syncAndDownload,
getLatestAppVersion,
};
export const { setAppState } = actions;

View File

@@ -29,16 +29,16 @@ import { FloatableSidebar } from './sidebar';
import { ManageTagsPage } from './tags/ManageTagsPage';
import { Titlebar } from './Titlebar';
import { sync } from '@desktop-client/app/appSlice';
import { getLatestAppVersion, sync } from '@desktop-client/app/appSlice';
import { ProtectedRoute } from '@desktop-client/auth/ProtectedRoute';
import { Permissions } from '@desktop-client/auth/types';
import { useAccounts } from '@desktop-client/hooks/useAccounts';
import { useGlobalPref } from '@desktop-client/hooks/useGlobalPref';
import { useLocalPref } from '@desktop-client/hooks/useLocalPref';
import { useMetaThemeColor } from '@desktop-client/hooks/useMetaThemeColor';
import { useNavigate } from '@desktop-client/hooks/useNavigate';
import { addNotification } from '@desktop-client/notifications/notificationsSlice';
import { useSelector, useDispatch } from '@desktop-client/redux';
import { getIsOutdated, getLatestVersion } from '@desktop-client/util/versions';
function NarrowNotSupported({
redirectTo = '/budget',
@@ -88,6 +88,10 @@ export function FinancesApp() {
const accounts = useAccounts();
const isAccountsLoaded = useSelector(state => state.account.isAccountsLoaded);
const versionInfo = useSelector(state => state.app.versionInfo);
const [notifyWhenUpdateIsAvailable] = useGlobalPref(
'notifyWhenUpdateIsAvailable',
);
const [lastUsedVersion, setLastUsedVersion] = useLocalPref(
'flags.updateNotificationShownForVersion',
);
@@ -104,7 +108,7 @@ export function FinancesApp() {
useEffect(() => {
async function run() {
await global.Actual.waitForUpdateReadyForDownload();
await global.Actual.waitForUpdateReadyForDownload(); // This will only resolve when an update is ready
dispatch(
addNotification({
notification: {
@@ -130,11 +134,15 @@ export function FinancesApp() {
}, []);
useEffect(() => {
async function run() {
const latestVersion = await getLatestVersion();
const isOutdated = await getIsOutdated(latestVersion);
dispatch(getLatestAppVersion());
}, [dispatch]);
if (isOutdated && lastUsedVersion !== latestVersion) {
useEffect(() => {
if (notifyWhenUpdateIsAvailable && versionInfo) {
if (
versionInfo.isOutdated &&
lastUsedVersion !== versionInfo.latestVersion
) {
dispatch(
addNotification({
notification: {
@@ -148,7 +156,7 @@ export function FinancesApp() {
)
: t(
'Version {{latestVersion}} of Actual was recently released.',
{ latestVersion },
{ latestVersion: versionInfo.latestVersion },
),
sticky: true,
id: 'update-notification',
@@ -159,16 +167,21 @@ export function FinancesApp() {
},
},
onClose: () => {
setLastUsedVersion(latestVersion);
setLastUsedVersion(versionInfo.latestVersion);
},
},
}),
);
}
}
run();
}, [lastUsedVersion, setLastUsedVersion]);
}, [
dispatch,
lastUsedVersion,
notifyWhenUpdateIsAvailable,
setLastUsedVersion,
t,
versionInfo,
]);
const scrollableRef = useRef<HTMLDivElement>(null);

View File

@@ -27,27 +27,32 @@ import { ResetCache, ResetSync } from './Reset';
import { ThemeSettings } from './Themes';
import { AdvancedToggle, Setting } from './UI';
import { getLatestAppVersion } from '@desktop-client/app/appSlice';
import { closeBudget } from '@desktop-client/budgetfiles/budgetfilesSlice';
import { Link } from '@desktop-client/components/common/Link';
import { FormField, FormLabel } from '@desktop-client/components/forms';
import {
Checkbox,
FormField,
FormLabel,
} from '@desktop-client/components/forms';
import { MOBILE_NAV_HEIGHT } from '@desktop-client/components/mobile/MobileNavTabs';
import { Page } from '@desktop-client/components/Page';
import { useServerVersion } from '@desktop-client/components/ServerContext';
import { useFeatureFlag } from '@desktop-client/hooks/useFeatureFlag';
import { useGlobalPref } from '@desktop-client/hooks/useGlobalPref';
import {
useIsOutdated,
useLatestVersion,
} from '@desktop-client/hooks/useLatestVersion';
import { useMetadataPref } from '@desktop-client/hooks/useMetadataPref';
import { useSyncedPref } from '@desktop-client/hooks/useSyncedPref';
import { loadPrefs } from '@desktop-client/prefs/prefsSlice';
import { useDispatch } from '@desktop-client/redux';
import { useSelector, useDispatch } from '@desktop-client/redux';
function About() {
const version = useServerVersion();
const latestVersion = useLatestVersion();
const isOutdated = useIsOutdated();
const versionInfo = useSelector(state => state.app.versionInfo);
const [notifyWhenUpdateIsAvailable, setNotifyWhenUpdateIsAvailablePref] =
useGlobalPref('notifyWhenUpdateIsAvailable', () => {
dispatch(getLatestAppVersion());
});
const dispatch = useDispatch();
return (
<Setting>
@@ -81,17 +86,20 @@ function About() {
<Text>
<Trans>Server version: {{ version }}</Trans>
</Text>
{isOutdated ? (
{notifyWhenUpdateIsAvailable && versionInfo?.isOutdated ? (
<Link
variant="external"
to="https://actualbudget.org/docs/releases"
linkColor="purple"
>
<Trans>New version available: {{ latestVersion }}</Trans>
<Trans>New version available: {versionInfo.latestVersion}</Trans>
</Link>
) : (
<Text style={{ color: theme.noticeText, fontWeight: 600 }}>
<Trans>Youre up to date!</Trans>
{notifyWhenUpdateIsAvailable ? (
<Trans>Youre up to date!</Trans>
) : null}
</Text>
)}
<Text>
@@ -104,6 +112,20 @@ function About() {
</Link>
</Text>
</View>
<View>
<Text style={{ display: 'flex' }}>
<Checkbox
id="settings-notifyWhenUpdateIsAvailable"
checked={notifyWhenUpdateIsAvailable}
onChange={e =>
setNotifyWhenUpdateIsAvailablePref(e.currentTarget.checked)
}
/>
<label htmlFor="settings-notifyWhenUpdateIsAvailable">
<Trans>Display a notification when updates are available</Trans>
</label>
</Text>
</View>
</Setting>
);
}

View File

@@ -1,27 +0,0 @@
import { useState, useEffect } from 'react';
import { getIsOutdated, getLatestVersion } from '@desktop-client/util/versions';
export function useIsOutdated(): boolean {
const [isOutdated, setIsOutdated] = useState(false);
const latestVersion = useLatestVersion();
useEffect(() => {
(async () => {
setIsOutdated(await getIsOutdated(latestVersion));
})();
}, [latestVersion]);
return isOutdated;
}
export function useLatestVersion(): string {
const [latestVersion, setLatestVersion] = useState('');
useEffect(() => {
(async () => {
setLatestVersion(await getLatestVersion());
})();
}, []);
return latestVersion;
}

View File

@@ -35,10 +35,10 @@ export async function getLatestVersion(): Promise<string | 'unknown'> {
}
}
export async function getIsOutdated(latestVersion: string): Promise<boolean> {
export function getIsOutdated(latestVersion: string): boolean {
const clientVersion = window.Actual.ACTUAL_VERSION;
if (latestVersion === 'unknown') {
return Promise.resolve(false);
return false;
}
return cmpSemanticVersion(clientVersion, latestVersion) < 0;
}

View File

@@ -102,6 +102,12 @@ async function saveGlobalPrefs(prefs: GlobalPrefs) {
if (prefs.syncServerConfig !== undefined) {
await asyncStorage.setItem('syncServerConfig', prefs.syncServerConfig);
}
if (prefs.notifyWhenUpdateIsAvailable !== undefined) {
await asyncStorage.setItem(
'notifyWhenUpdateIsAvailable',
prefs.notifyWhenUpdateIsAvailable,
);
}
return 'ok';
}
@@ -117,6 +123,7 @@ async function loadGlobalPrefs(): Promise<GlobalPrefs> {
'preferred-dark-theme': preferredDarkTheme,
'server-self-signed-cert': serverSelfSignedCert,
syncServerConfig,
notifyWhenUpdateIsAvailable,
} = await asyncStorage.multiGet([
'floating-sidebar',
'category-expanded-state',
@@ -128,6 +135,7 @@ async function loadGlobalPrefs(): Promise<GlobalPrefs> {
'preferred-dark-theme',
'server-self-signed-cert',
'syncServerConfig',
'notifyWhenUpdateIsAvailable',
] as const);
return {
floatingSidebar: floatingSidebar === 'true',
@@ -150,6 +158,10 @@ async function loadGlobalPrefs(): Promise<GlobalPrefs> {
: 'dark',
serverSelfSignedCert: serverSelfSignedCert || undefined,
syncServerConfig: syncServerConfig || undefined,
notifyWhenUpdateIsAvailable:
notifyWhenUpdateIsAvailable === undefined
? true
: notifyWhenUpdateIsAvailable, // default to true
};
}

View File

@@ -99,6 +99,7 @@ export type GlobalPrefs = Partial<{
autoStart?: boolean;
port?: number;
};
notifyWhenUpdateIsAvailable: boolean;
}>;
// GlobalPrefsJson represents what's saved in the global-store.json file
@@ -121,6 +122,7 @@ export type GlobalPrefsJson = Partial<{
'preferred-dark-theme'?: GlobalPrefs['preferredDarkTheme'];
'server-self-signed-cert'?: GlobalPrefs['serverSelfSignedCert'];
syncServerConfig?: GlobalPrefs['syncServerConfig'];
notifyWhenUpdateIsAvailable?: GlobalPrefs['notifyWhenUpdateIsAvailable'];
}>;
export type AuthMethods = 'password' | 'openid';

View File

@@ -1,6 +1,6 @@
---
category: Bugfix
authors: [itsbekas]
category: Maintenance
authors: [MikesGlitch]
---
Fix range calculator on the MonthPicker component
Update Electron to the latest stable version

View File

@@ -0,0 +1,6 @@
---
category: Features
authors: [MikesGlitch]
---
Add a setting to disable update notifications