diff --git a/packages/desktop-client/src/components/FinancesApp.js b/packages/desktop-client/src/components/FinancesApp.tsx similarity index 87% rename from packages/desktop-client/src/components/FinancesApp.js rename to packages/desktop-client/src/components/FinancesApp.tsx index bc098f9fcae5e7f7feea6bf48832e86fec70c31d..b8804da5dff2da58edd39c51fa4aaf0695a9707d 100644 --- a/packages/desktop-client/src/components/FinancesApp.js +++ b/packages/desktop-client/src/components/FinancesApp.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useMemo } from 'react'; +import React, { type ReactElement, useEffect, useMemo } from 'react'; import { DndProvider } from 'react-dnd'; import { HTML5Backend as Backend } from 'react-dnd-html5-backend'; import { connect } from 'react-redux'; @@ -30,31 +30,28 @@ import { colors, styles } from '../style'; import { ExposeNavigate, StackedRoutes } from '../util/router-tools'; import { getIsOutdated, getLatestVersion } from '../util/versions'; -import Account from './accounts/Account'; -import MobileAccount from './accounts/MobileAccount'; -import MobileAccounts from './accounts/MobileAccounts'; import BankSyncStatus from './BankSyncStatus'; -import Budget from './budget'; import { BudgetMonthCountProvider } from './budget/BudgetMonthCountContext'; -import MobileBudget from './budget/MobileBudget'; import { View } from './common'; import FloatableSidebar, { SidebarProvider } from './FloatableSidebar'; import GlobalKeys from './GlobalKeys'; -import GoCardlessLink from './gocardless/GoCardlessLink'; import { ManageRulesPage } from './ManageRulesPage'; import Modals from './Modals'; import Notifications from './Notifications'; import { ManagePayeesPage } from './payees/ManagePayeesPage'; import Reports from './reports'; -import Schedules from './schedules'; -import DiscoverSchedules from './schedules/DiscoverSchedules'; -import EditSchedule from './schedules/EditSchedule'; -import LinkSchedule from './schedules/LinkSchedule'; +import { NarrowAlternate, WideComponent } from './responsive'; import PostsOfflineNotification from './schedules/PostsOfflineNotification'; import Settings from './settings'; import Titlebar, { TitlebarProvider } from './Titlebar'; -function NarrowNotSupported({ children, redirectTo = '/budget' }) { +function NarrowNotSupported({ + redirectTo = '/budget', + children, +}: { + redirectTo?: string; + children: ReactElement; +}) { const { isNarrowWidth } = useResponsive(); const navigate = useNavigate(); useEffect(() => { @@ -66,7 +63,6 @@ function NarrowNotSupported({ children, redirectTo = '/budget' }) { } function StackedRoutesInner({ location }) { - const { isNarrowWidth } = useResponsive(); return ( <Routes location={location}> <Route path="/" element={<Navigate to="/budget" replace />} /> @@ -75,21 +71,19 @@ function StackedRoutesInner({ location }) { path="/reports/*" element={ <NarrowNotSupported> + {/* Has its own lazy loading logic */} <Reports /> </NarrowNotSupported> } /> - <Route - path="/budget" - element={isNarrowWidth ? <MobileBudget /> : <Budget />} - /> + <Route path="/budget" element={<NarrowAlternate name="Budget" />} /> <Route path="/schedules" element={ <NarrowNotSupported> - <Schedules /> + <WideComponent name="Schedules" /> </NarrowNotSupported> } /> @@ -98,7 +92,7 @@ function StackedRoutesInner({ location }) { path="/schedule/edit" element={ <NarrowNotSupported> - <EditSchedule /> + <WideComponent name="EditSchedule" /> </NarrowNotSupported> } /> @@ -106,7 +100,7 @@ function StackedRoutesInner({ location }) { path="/schedule/edit/:id" element={ <NarrowNotSupported> - <EditSchedule /> + <WideComponent name="EditSchedule" /> </NarrowNotSupported> } /> @@ -114,7 +108,7 @@ function StackedRoutesInner({ location }) { path="/schedule/link" element={ <NarrowNotSupported> - <LinkSchedule /> + <WideComponent name="LinkSchedule" /> </NarrowNotSupported> } /> @@ -122,7 +116,7 @@ function StackedRoutesInner({ location }) { path="/schedule/discover" element={ <NarrowNotSupported> - <DiscoverSchedules /> + <WideComponent name="DiscoverSchedules" /> </NarrowNotSupported> } /> @@ -141,7 +135,7 @@ function StackedRoutesInner({ location }) { path="/nordigen/link" element={ <NarrowNotSupported> - <GoCardlessLink /> + <WideComponent name="GoCardlessLink" /> </NarrowNotSupported> } /> @@ -149,20 +143,17 @@ function StackedRoutesInner({ location }) { path="/gocardless/link" element={ <NarrowNotSupported> - <GoCardlessLink /> + <WideComponent name="GoCardlessLink" /> </NarrowNotSupported> } /> <Route path="/accounts/:id" - element={isNarrowWidth ? <MobileAccount /> : <Account />} + element={<NarrowAlternate name="Account" />} /> - <Route - path="/accounts" - element={isNarrowWidth ? <MobileAccounts /> : <Account />} - /> + <Route path="/accounts" element={<NarrowAlternate name="Accounts" />} /> </Routes> ); } @@ -230,6 +221,8 @@ function RouterBehaviors({ getAccounts }) { useEffect(() => { undo.setUndoState('url', href); }, [href]); + + return null; } function FinancesApp(props) { diff --git a/packages/desktop-client/src/components/Modals.tsx b/packages/desktop-client/src/components/Modals.tsx index 1934a56237c61ccfeec7349c7f339caa8fb3b834..ca5b47a990b28fcd738d7e3d7a390c29418173dc 100644 --- a/packages/desktop-client/src/components/Modals.tsx +++ b/packages/desktop-client/src/components/Modals.tsx @@ -37,7 +37,7 @@ export default function Modals() { const syncServerStatus = useSyncServerStatus(); - return modalStack + let modals = modalStack .map(({ name, options }, idx) => { const modalProps = { onClose: actions.popModal, @@ -227,4 +227,8 @@ export default function Modals() { .map((modal, idx) => ( <React.Fragment key={modalStack[idx].name}>{modal}</React.Fragment> )); + + // fragment needed per TS types + // eslint-disable-next-line react/jsx-no-useless-fragment + return <>{modals}</>; } diff --git a/packages/desktop-client/src/components/Notifications.tsx b/packages/desktop-client/src/components/Notifications.tsx index 67287e2ec1bbf36aa935a704c351f4be60a1017b..69c6450fd2485b532c43d55869a26245ab676ac6 100644 --- a/packages/desktop-client/src/components/Notifications.tsx +++ b/packages/desktop-client/src/components/Notifications.tsx @@ -4,13 +4,13 @@ import React, { useMemo, type SetStateAction, } from 'react'; -import { connect, useSelector } from 'react-redux'; +import { useSelector } from 'react-redux'; -import { bindActionCreators } from 'redux'; +import { type CSSProperties } from 'glamor'; -import * as actions from 'loot-core/src/client/actions'; import type { NotificationWithId } from 'loot-core/src/client/state-types/notifications'; +import { useActions } from '../hooks/useActions'; import Loading from '../icons/AnimatedLoading'; import Delete from '../icons/v0/Delete'; import { styles, colors } from '../style'; @@ -220,7 +220,8 @@ function Notification({ ); } -function Notifications({ removeNotification, style }) { +export default function Notifications({ style }: { style?: CSSProperties }) { + let { removeNotification } = useActions(); let notifications = useSelector(state => state.notifications.notifications); return ( <View @@ -249,7 +250,3 @@ function Notifications({ removeNotification, style }) { </View> ); } - -export default connect(null, dispatch => bindActionCreators(actions, dispatch))( - Notifications, -); diff --git a/packages/desktop-client/src/components/Titlebar.js b/packages/desktop-client/src/components/Titlebar.js index 50eaa6245bc220a0ea3fcfa22f06c4b5f040385c..8a644b39c48dac87f8028ff2754cf825590c1976 100644 --- a/packages/desktop-client/src/components/Titlebar.js +++ b/packages/desktop-client/src/components/Titlebar.js @@ -281,10 +281,7 @@ function Titlebar({ saveGlobalPrefs, savePrefs, localPrefs, - userData, floatingSidebar, - syncError, - setAppState, style, sync, }) { diff --git a/packages/desktop-client/src/components/reports/index.tsx b/packages/desktop-client/src/components/reports/index.tsx index 4d110091bd7c854ebaa36d09de5e9b2a6730fc3e..7d3a886f2d3108942a29aec5dd3c447f93fc775c 100644 --- a/packages/desktop-client/src/components/reports/index.tsx +++ b/packages/desktop-client/src/components/reports/index.tsx @@ -1,44 +1,16 @@ -import { useState, useEffect } from 'react'; - -import AnimatedLoading from '../../icons/AnimatedLoading'; -import { colors, styles } from '../../style'; -import { Block, View } from '../common'; - -import type { ReportRouter } from './ReportRouter'; +import { View } from '../common'; +import { LoadComponent } from '../util/LoadComponent'; export default function Reports() { - let [ReportRouterComponent, setReportRouter] = useState< - typeof ReportRouter | null - >(null); - - useEffect(() => { - import(/* webpackChunkName: 'reports' */ './ReportRouter').then(module => { - setReportRouter(() => module.ReportRouter); - }); - }, []); - return ( <View style={{ flex: 1 }} data-testid="reports-page"> - {ReportRouterComponent ? ( - <ReportRouterComponent /> - ) : ( - <View - style={[ - { - flex: 1, - gap: 20, - justifyContent: 'center', - alignItems: 'center', - }, - styles.delayedFadeIn, - ]} - > - <Block style={{ marginBottom: 20, fontSize: 18 }}> - Loading reports… - </Block> - <AnimatedLoading width={25} color={colors.n1} /> - </View> - )} + <LoadComponent + name="ReportRouter" + message="Loading reports..." + importer={() => + import(/* webpackChunkName: 'reports' */ './ReportRouter') + } + /> </View> ); } diff --git a/packages/desktop-client/src/components/responsive/index.tsx b/packages/desktop-client/src/components/responsive/index.tsx new file mode 100644 index 0000000000000000000000000000000000000000..570e533aabebe75bf7a18b530b630e87eeaa254a --- /dev/null +++ b/packages/desktop-client/src/components/responsive/index.tsx @@ -0,0 +1,27 @@ +import { useResponsive } from '../../ResponsiveProvider'; +import { LoadComponent } from '../util/LoadComponent'; + +import type * as NarrowComponents from './narrow'; +import type * as WideComponents from './wide'; + +let loadNarrow = () => + import(/* webpackChunkName: "narrow-components" */ './narrow'); +let loadWide = () => import(/* webpackChunkName: "wide-components" */ './wide'); + +export function WideComponent({ name }: { name: keyof typeof WideComponents }) { + return <LoadComponent name={name} importer={loadWide} />; +} + +export function NarrowAlternate({ + name, +}: { + name: keyof typeof WideComponents & keyof typeof NarrowComponents; +}) { + const { isNarrowWidth } = useResponsive(); + return ( + <LoadComponent + name={name} + importer={isNarrowWidth ? loadNarrow : loadWide} + /> + ); +} diff --git a/packages/desktop-client/src/components/responsive/narrow.ts b/packages/desktop-client/src/components/responsive/narrow.ts new file mode 100644 index 0000000000000000000000000000000000000000..d3b35e5ef69e2b0d1b8619d15461a01659b2d9dc --- /dev/null +++ b/packages/desktop-client/src/components/responsive/narrow.ts @@ -0,0 +1,4 @@ +export { default as Budget } from '../budget/MobileBudget'; + +export { default as Accounts } from '../accounts/MobileAccounts'; +export { default as Account } from '../accounts/MobileAccount'; diff --git a/packages/desktop-client/src/components/responsive/wide.ts b/packages/desktop-client/src/components/responsive/wide.ts new file mode 100644 index 0000000000000000000000000000000000000000..cca8e725e2a2dc3013478c59c16d2433d95923aa --- /dev/null +++ b/packages/desktop-client/src/components/responsive/wide.ts @@ -0,0 +1,11 @@ +export { default as Budget } from '../budget'; + +export { default as Schedules } from '../schedules'; +export { default as EditSchedule } from '../schedules/EditSchedule'; +export { default as LinkSchedule } from '../schedules/LinkSchedule'; +export { default as DiscoverSchedules } from '../schedules/DiscoverSchedules'; + +export { default as GoCardlessLink } from '../gocardless/GoCardlessLink'; + +export { default as Accounts } from '../accounts/Account'; +export { default as Account } from '../accounts/Account'; diff --git a/packages/desktop-client/src/components/transactions/MobileTransaction.js b/packages/desktop-client/src/components/transactions/MobileTransaction.js index 61c3b533e54cb26b80611c365887b87ef8cbfbdf..032870e190dae27743bc67597a7af13fc3388b9c 100644 --- a/packages/desktop-client/src/components/transactions/MobileTransaction.js +++ b/packages/desktop-client/src/components/transactions/MobileTransaction.js @@ -11,6 +11,7 @@ import { useListBox, useListBoxSection, useOption } from '@react-aria/listbox'; import { mergeProps } from '@react-aria/utils'; import { Item, Section } from '@react-stately/collections'; import { useListState } from '@react-stately/list'; +import { css } from 'glamor'; import memoizeOne from 'memoize-one'; import * as monthUtils from 'loot-core/src/shared/months'; @@ -408,8 +409,7 @@ function ListBoxSection({ section, state }) { {section.rendered && ( <div {...headingProps} - style={{ - ...styles.smallText, + {...css(styles.smallText, { backgroundColor: colors.n10, borderBottom: `1px solid ${colors.n9}`, borderTop: `1px solid ${colors.n9}`, @@ -422,7 +422,7 @@ function ListBoxSection({ section, state }) { top: '0', width: '100%', zIndex: zIndices.SECTION_HEADING, - }} + })} > {section.rendered} </div> diff --git a/packages/desktop-client/src/components/util/LoadComponent.tsx b/packages/desktop-client/src/components/util/LoadComponent.tsx new file mode 100644 index 0000000000000000000000000000000000000000..956be6ba7216beda0804518bc944ee39cf6f562f --- /dev/null +++ b/packages/desktop-client/src/components/util/LoadComponent.tsx @@ -0,0 +1,54 @@ +import { type ComponentType, useEffect, useState } from 'react'; + +import AnimatedLoading from '../../icons/AnimatedLoading'; +import { colors, styles } from '../../style'; +import { Block, View } from '../common'; + +type ProplessComponent = ComponentType<Record<string, never>>; +type LoadComponentProps<K extends string> = { + name: K; + message?: string; + importer: () => Promise<{ [key in K]: ProplessComponent }>; +}; +export function LoadComponent<K extends string>(props: LoadComponentProps<K>) { + // need to set `key` so the component is reloaded when the name changes + // otherwise the old component will be rendered while the new one is being loaded + return <LoadComponentInner key={props.name} {...props} />; +} + +function LoadComponentInner<K extends string>({ + name, + message, + importer, +}: LoadComponentProps<K>) { + let [Component, setComponent] = useState<ProplessComponent | null>(null); + useEffect(() => { + importer().then(module => setComponent(() => module[name])); + }, [name, importer]); + + if (!Component) { + return ( + <View + style={[ + { + flex: 1, + gap: 20, + justifyContent: 'center', + alignItems: 'center', + }, + styles.delayedFadeIn, + ]} + > + {message && ( + <Block style={{ marginBottom: 20, fontSize: 18 }}>{message}</Block> + )} + <AnimatedLoading width={25} color={colors.n1} /> + </View> + ); + } + + // console.log( + // `rendering <${Component.displayName || Component.name} /> as ${name}`, + // ); + return <Component />; +} diff --git a/packages/desktop-client/src/fonts.scss b/packages/desktop-client/src/fonts.scss index b36437aff056bc0462806428ea7859e7c2740dc5..7e1ee6cc8ba2b97626b3c34a1013219521cca603 100644 --- a/packages/desktop-client/src/fonts.scss +++ b/packages/desktop-client/src/fonts.scss @@ -1,2 +1,2 @@ @use '~inter-ui/variable'; -@include variable.all; +@include variable.default; diff --git a/packages/desktop-client/src/style.tsx b/packages/desktop-client/src/style.tsx index 5e3807eee2f4856e45587110e0e63306fc965969..49a7d05a5d07e4470fa275b5bb6dc987624497f9 100644 --- a/packages/desktop-client/src/style.tsx +++ b/packages/desktop-client/src/style.tsx @@ -176,7 +176,7 @@ export const styles = { '0%': { opacity: 0 }, '100%': { opacity: 1 }, }), - animationDuration: '0.2s', + animationDuration: '1s', animationFillMode: 'both', animationDelay: '0.5s', }, diff --git a/packages/desktop-client/src/util/withThemeColor.tsx b/packages/desktop-client/src/util/withThemeColor.tsx index f2615e515c5d86604276d32017e347c09b0666cf..c146b60aae94e69e39a10f4d8409b1ee3ceb6d6b 100644 --- a/packages/desktop-client/src/util/withThemeColor.tsx +++ b/packages/desktop-client/src/util/withThemeColor.tsx @@ -2,6 +2,10 @@ import React, { Component } from 'react'; export const withThemeColor = color => WrappedComponent => { class WithThemeColor extends Component { + static displayName = `withThemeColor(${ + WrappedComponent.displayName || WrappedComponent.name + })`; + componentDidMount() { setThemeColor(color); } diff --git a/upcoming-release-notes/1240.md b/upcoming-release-notes/1240.md new file mode 100644 index 0000000000000000000000000000000000000000..6f76beeb829bf54058d4cc881a31616ec974cebb --- /dev/null +++ b/upcoming-release-notes/1240.md @@ -0,0 +1,6 @@ +--- +category: Enhancements +authors: [j-f1] +--- + +Avoid downloading code for the desktop UI on mobile and vice versa