🔔 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>
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 76 KiB |
|
Before Width: | Height: | Size: 73 KiB After Width: | Height: | Size: 75 KiB |
|
Before Width: | Height: | Size: 73 KiB After Width: | Height: | Size: 75 KiB |
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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>You’re up to date!</Trans>
|
||||
{notifyWhenUpdateIsAvailable ? (
|
||||
<Trans>You’re 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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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
|
||||
|
||||
6
upcoming-release-notes/5725.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
category: Features
|
||||
authors: [MikesGlitch]
|
||||
---
|
||||
|
||||
Add a setting to disable update notifications
|
||||