diff --git a/packages/desktop-client/package.json b/packages/desktop-client/package.json index 3e7375494adb304fbbd227f73c94d6e6a30bdae9..8eaca8ff054a4a365fee2698c4f4ea422914ad0c 100644 --- a/packages/desktop-client/package.json +++ b/packages/desktop-client/package.json @@ -21,6 +21,7 @@ "@swc/plugin-react-remove-properties": "^1.5.108", "@testing-library/react": "14.1.2", "@testing-library/user-event": "14.5.2", + "@types/promise-retry": "^1.1.6", "@types/react": "^18.2.0", "@types/react-dom": "^18.2.1", "@types/react-modal": "^3.16.0", @@ -43,6 +44,7 @@ "mdast-util-newline-to-break": "^2.0.0", "memoize-one": "^6.0.0", "pikaday": "1.8.2", + "promise-retry": "^2.0.1", "react": "18.2.0", "react-aria-components": "^1.1.1", "react-dnd": "^16.0.1", diff --git a/packages/desktop-client/src/components/App.tsx b/packages/desktop-client/src/components/App.tsx index 5a63cdd58b740295ed26147bbdf4ab1ecf67103f..85073350ce9274f7f4cc204fb05dd1fa263cdb6b 100644 --- a/packages/desktop-client/src/components/App.tsx +++ b/packages/desktop-client/src/components/App.tsx @@ -38,24 +38,32 @@ type AppInnerProps = { function AppInner({ budgetId, cloudFileId }: AppInnerProps) { const [initializing, setInitializing] = useState(true); const { showBoundary: showErrorBoundary } = useErrorBoundary(); - const loadingText = useSelector((state: State) => state.app.loadingText); + const stateLoadingText = useSelector((state: State) => state.app.loadingText); + const [loadingText = stateLoadingText, setLoadingText] = useState< + string | null + >(null); const { loadBudget, closeBudget, loadGlobalPrefs } = useActions(); async function init() { const socketName = await global.Actual.getServerSocket(); + setLoadingText('Initializing the connection to the local database...'); await initConnection(socketName); // Load any global prefs + setLoadingText('Loading global preferences...'); await loadGlobalPrefs(); // Open the last opened budget, if any + setLoadingText('Opening last budget...'); const budgetId = await send('get-last-opened-backup'); if (budgetId) { + setLoadingText('Loading the last budget file...'); await loadBudget(budgetId); // Check to see if this file has been remotely deleted (but // don't block on this in case they are offline or something) + setLoadingText('Retrieving remote files...'); send('get-remote-files').then(files => { if (files) { const remoteFile = files.find(f => f.fileId === cloudFileId); @@ -71,6 +79,7 @@ function AppInner({ budgetId, cloudFileId }: AppInnerProps) { async function initAll() { await Promise.all([installPolyfills(), init()]); setInitializing(false); + setLoadingText(null); } initAll().catch(showErrorBoundary); diff --git a/packages/desktop-client/src/components/AppBackground.tsx b/packages/desktop-client/src/components/AppBackground.tsx index dd484ac60c0c922b47839d26e2903d6554d5c3ec..daff02ad650f337c004ff1a71a8cb87488474e0f 100644 --- a/packages/desktop-client/src/components/AppBackground.tsx +++ b/packages/desktop-client/src/components/AppBackground.tsx @@ -1,4 +1,5 @@ import React from 'react'; +import { useTransition, animated } from 'react-spring'; import { css } from 'glamor'; @@ -18,29 +19,39 @@ export function AppBackground({ initializing, loadingText, }: AppBackgroundProps) { + const transitions = useTransition(loadingText, { + from: { opacity: 0, transform: 'translateY(-100px)' }, + enter: { opacity: 1, transform: 'translateY(0)' }, + leave: { opacity: 0, transform: 'translateY(-100px)' }, + unique: true, + }); + return ( <> <Background /> - {(loadingText != null || initializing) && ( - <View - className={`${css({ - position: 'absolute', - top: 0, - left: 0, - right: 0, - padding: 50, - paddingTop: 200, - color: theme.pageText, - alignItems: 'center', - })}`} - > - <Block style={{ marginBottom: 20, fontSize: 18 }}> - {loadingText} - </Block> - <AnimatedLoading width={25} color={theme.pageText} /> - </View> - )} + {(loadingText != null || initializing) && + transitions((style, item) => ( + <animated.div key={item} style={style}> + <View + className={`${css({ + position: 'absolute', + top: 0, + left: 0, + right: 0, + padding: 50, + paddingTop: 200, + color: theme.pageText, + alignItems: 'center', + })}`} + > + <Block style={{ marginBottom: 20, fontSize: 18 }}> + {loadingText} + </Block> + <AnimatedLoading width={25} color={theme.pageText} /> + </View> + </animated.div> + ))} </> ); } diff --git a/packages/desktop-client/src/components/util/LoadComponent.tsx b/packages/desktop-client/src/components/util/LoadComponent.tsx index a0769656e1b41f4eb9eb31a6623443b213e2ad45..9d4275ade509c89aaf13a8794f42fbbceb1cbccc 100644 --- a/packages/desktop-client/src/components/util/LoadComponent.tsx +++ b/packages/desktop-client/src/components/util/LoadComponent.tsx @@ -1,5 +1,7 @@ import { type ComponentType, useEffect, useState } from 'react'; +import promiseRetry from 'promise-retry'; + import { AnimatedLoading } from '../../icons/AnimatedLoading'; import { theme, styles } from '../../style'; import { Block } from '../common/Block'; @@ -23,10 +25,29 @@ function LoadComponentInner<K extends string>({ importer, }: LoadComponentProps<K>) { const [Component, setComponent] = useState<ProplessComponent | null>(null); + const [failedToLoad, setFailedToLoad] = useState(false); + useEffect(() => { - importer().then(module => setComponent(() => module[name])); + setFailedToLoad(false); + + // Load the module; if it fails - retry with exponential backoff + promiseRetry( + retry => + importer() + .then(module => setComponent(() => module[name])) + .catch(retry), + { + retries: 5, + }, + ).catch(() => { + setFailedToLoad(true); + }); }, [name, importer]); + if (failedToLoad) { + throw new Error(`Failed loading the ${name} lazy module.`); + } + if (!Component) { return ( <View @@ -46,8 +67,5 @@ function LoadComponentInner<K extends string>({ ); } - // console.log( - // `rendering <${Component.displayName || Component.name} /> as ${name}`, - // ); return <Component />; } diff --git a/upcoming-release-notes/2639.md b/upcoming-release-notes/2639.md new file mode 100644 index 0000000000000000000000000000000000000000..61c2ba5219ef3ba70de9b8d4e882d9ff3a1dd950 --- /dev/null +++ b/upcoming-release-notes/2639.md @@ -0,0 +1,6 @@ +--- +category: Enhancements +authors: [MatissJanis] +--- + +Added app-loading stage description texts; also added exponential backoff in case a lazy-loaded module fails loading diff --git a/yarn.lock b/yarn.lock index 3016c10bd68d209181e4b58800bb8dcb5584e06e..1373b94432aabd5edaa15d685fa9e0af13264af2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -73,6 +73,7 @@ __metadata: "@swc/plugin-react-remove-properties": "npm:^1.5.108" "@testing-library/react": "npm:14.1.2" "@testing-library/user-event": "npm:14.5.2" + "@types/promise-retry": "npm:^1.1.6" "@types/react": "npm:^18.2.0" "@types/react-dom": "npm:^18.2.1" "@types/react-modal": "npm:^3.16.0" @@ -95,6 +96,7 @@ __metadata: mdast-util-newline-to-break: "npm:^2.0.0" memoize-one: "npm:^6.0.0" pikaday: "npm:1.8.2" + promise-retry: "npm:^2.0.1" react: "npm:18.2.0" react-aria-components: "npm:^1.1.1" react-dnd: "npm:^16.0.1" @@ -5524,6 +5526,15 @@ __metadata: languageName: node linkType: hard +"@types/promise-retry@npm:^1.1.6": + version: 1.1.6 + resolution: "@types/promise-retry@npm:1.1.6" + dependencies: + "@types/retry": "npm:*" + checksum: 8834adbc77399329fafa4e07ada57fc49007802f81af42cd74b2c51e89a3cdd9aa4c09a05e3909aaea9d165bd2ff940ed309db21c21c373104e479b11e24435c + languageName: node + linkType: hard + "@types/prop-types@npm:*, @types/prop-types@npm:^15.0.0": version: 15.7.5 resolution: "@types/prop-types@npm:15.7.5" @@ -5590,6 +5601,13 @@ __metadata: languageName: node linkType: hard +"@types/retry@npm:*": + version: 0.12.5 + resolution: "@types/retry@npm:0.12.5" + checksum: 3fb6bf91835ca0eb2987567d6977585235a7567f8aeb38b34a8bb7bbee57ac050ed6f04b9998cda29701b8c893f5dfe315869bc54ac17e536c9235637fe351a2 + languageName: node + linkType: hard + "@types/scheduler@npm:*": version: 0.16.3 resolution: "@types/scheduler@npm:0.16.3"