aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLibravatar Kristóf Marussy <kristof@marussy.com>2022-05-15 14:29:26 +0200
committerLibravatar Kristóf Marussy <kristof@marussy.com>2022-05-16 00:55:04 +0200
commitf32fc1105d43b713f40c3b2819ca14b11e989dc2 (patch)
treeb7924be2c21d4f683a059b8d514a4876230a9e97
parentchore(deps): bump dependencies (diff)
downloadsophie-f32fc1105d43b713f40c3b2819ca14b11e989dc2.tar.gz
sophie-f32fc1105d43b713f40c3b2819ca14b11e989dc2.tar.zst
sophie-f32fc1105d43b713f40c3b2819ca14b11e989dc2.zip
refactor(renderer): remove StoreProvider
Use explicit prop threading to pass the MainStore to components, which makes the data dependencies more explicit and enables better testability. Signed-off-by: Kristóf Marussy <kristof@marussy.com>
-rw-r--r--packages/renderer/src/components/App.tsx17
-rw-r--r--packages/renderer/src/components/ServicePanel.tsx49
-rw-r--r--packages/renderer/src/components/StoreProvider.tsx45
-rw-r--r--packages/renderer/src/components/locationBar/LocationBar.tsx14
-rw-r--r--packages/renderer/src/components/sidebar/ServiceSwitcher.tsx13
-rw-r--r--packages/renderer/src/components/sidebar/Sidebar.tsx90
-rw-r--r--packages/renderer/src/components/sidebar/ToggleDarkModeButton.tsx16
-rw-r--r--packages/renderer/src/components/sidebar/ToggleLocationBarButton.tsx13
-rw-r--r--packages/renderer/src/index.tsx5
9 files changed, 123 insertions, 139 deletions
diff --git a/packages/renderer/src/components/App.tsx b/packages/renderer/src/components/App.tsx
index 2f728e7..b0686a9 100644
--- a/packages/renderer/src/components/App.tsx
+++ b/packages/renderer/src/components/App.tsx
@@ -23,18 +23,25 @@ import { observer } from 'mobx-react-lite';
23import React, { useCallback, useEffect } from 'react'; 23import React, { useCallback, useEffect } from 'react';
24import { useTranslation } from 'react-i18next'; 24import { useTranslation } from 'react-i18next';
25 25
26import type RendererStore from '../stores/RendererStore';
27
26import ServicePanel from './ServicePanel'; 28import ServicePanel from './ServicePanel';
27import { useStore } from './StoreProvider';
28import Sidebar from './sidebar/Sidebar'; 29import Sidebar from './sidebar/Sidebar';
29 30
30function App({ devMode }: { devMode: boolean }): JSX.Element { 31function App({
32 store,
33 devMode,
34}: {
35 store: RendererStore;
36 devMode: boolean;
37}): JSX.Element {
31 const { ready, t } = useTranslation(undefined, { 38 const { ready, t } = useTranslation(undefined, {
32 useSuspense: false, 39 useSuspense: false,
33 }); 40 });
34 const { 41 const {
35 settings: { selectedService }, 42 settings: { selectedService },
36 shared: { services }, 43 shared: { services },
37 } = useStore(); 44 } = store;
38 const { 45 const {
39 settings: { name: serviceName }, 46 settings: { name: serviceName },
40 title: serviceTitle, 47 title: serviceTitle,
@@ -88,7 +95,7 @@ function App({ devMode }: { devMode: boolean }): JSX.Element {
88 width: '100vw', 95 width: '100vw',
89 }} 96 }}
90 > 97 >
91 <Sidebar /> 98 <Sidebar store={store} />
92 <Box 99 <Box
93 sx={{ 100 sx={{
94 flex: 1, 101 flex: 1,
@@ -97,7 +104,7 @@ function App({ devMode }: { devMode: boolean }): JSX.Element {
97 }} 104 }}
98 > 105 >
99 {services.map((service) => ( 106 {services.map((service) => (
100 <ServicePanel key={service.id} service={service} /> 107 <ServicePanel key={service.id} store={store} service={service} />
101 ))} 108 ))}
102 </Box> 109 </Box>
103 </Box> 110 </Box>
diff --git a/packages/renderer/src/components/ServicePanel.tsx b/packages/renderer/src/components/ServicePanel.tsx
index de58d24..2c79d99 100644
--- a/packages/renderer/src/components/ServicePanel.tsx
+++ b/packages/renderer/src/components/ServicePanel.tsx
@@ -19,57 +19,66 @@
19 */ 19 */
20 20
21import Box from '@mui/material/Box'; 21import Box from '@mui/material/Box';
22import { styled } from '@mui/material/styles';
22import { observer } from 'mobx-react-lite'; 23import { observer } from 'mobx-react-lite';
23import React from 'react'; 24import React from 'react';
24 25
26import type RendererStore from '../stores/RendererStore';
25import Service from '../stores/Service'; 27import Service from '../stores/Service';
26 28
27import BrowserViewPlaceholder from './BrowserViewPlaceholder'; 29import BrowserViewPlaceholder from './BrowserViewPlaceholder';
28import { useStore } from './StoreProvider';
29import InsecureConnectionBanner from './banner/InsecureConnectionBanner'; 30import InsecureConnectionBanner from './banner/InsecureConnectionBanner';
30import NewWindowBanner from './banner/NewWindowBanner'; 31import NewWindowBanner from './banner/NewWindowBanner';
31import ErrorPage from './errorPage/ErrorPage'; 32import ErrorPage from './errorPage/ErrorPage';
32import LocationBar from './locationBar/LocationBar'; 33import LocationBar from './locationBar/LocationBar';
33 34
35const ServicePanelRoot = styled(Box, {
36 shouldForwardProp: (prop) => prop !== 'hidden',
37})(({ hidden }) => ({
38 position: 'absolute',
39 top: 0,
40 left: 0,
41 bottom: 0,
42 right: 0,
43 display: 'flex',
44 overflow: 'hidden',
45 visibility: hidden ? 'hidden' : 'visible',
46 flexDirection: 'column',
47 alignItems: 'stretch',
48}));
49
34export function getServicePanelID(service: Service): string { 50export function getServicePanelID(service: Service): string {
35 return `Sophie-${service.id}-ServicePanel`; 51 return `Sophie-${service.id}-ServicePanel`;
36} 52}
37 53
38function ServicePanel({ service }: { service: Service }): JSX.Element { 54function ServicePanel({
55 store,
56 service,
57}: {
58 store: RendererStore;
59 service: Service;
60}): JSX.Element {
39 const { 61 const {
40 settings: { selectedService }, 62 settings: { selectedService },
41 } = useStore(); 63 } = store;
42
43 const { 64 const {
44 settings: { name }, 65 settings: { name },
45 } = service; 66 } = service;
46 const visible = service === selectedService;
47 67
48 return ( 68 return (
49 <Box 69 <ServicePanelRoot
50 id={getServicePanelID(service)} 70 id={getServicePanelID(service)}
51 role="tabpanel" 71 role="tabpanel"
52 aria-label={name} 72 aria-label={name}
53 sx={{ 73 hidden={service !== selectedService}
54 position: 'absolute',
55 top: 0,
56 left: 0,
57 bottom: 0,
58 right: 0,
59 display: 'flex',
60 overflow: 'hidden',
61 visibility: visible ? 'visible' : 'hidden',
62 flexDirection: 'column',
63 alignItems: 'stretch',
64 }}
65 > 74 >
66 <LocationBar service={service} /> 75 <LocationBar store={store} service={service} />
67 <InsecureConnectionBanner service={service} /> 76 <InsecureConnectionBanner service={service} />
68 <NewWindowBanner service={service} /> 77 <NewWindowBanner service={service} />
69 <BrowserViewPlaceholder service={service}> 78 <BrowserViewPlaceholder service={service}>
70 <ErrorPage service={service} /> 79 <ErrorPage service={service} />
71 </BrowserViewPlaceholder> 80 </BrowserViewPlaceholder>
72 </Box> 81 </ServicePanelRoot>
73 ); 82 );
74} 83}
75 84
diff --git a/packages/renderer/src/components/StoreProvider.tsx b/packages/renderer/src/components/StoreProvider.tsx
deleted file mode 100644
index de63083..0000000
--- a/packages/renderer/src/components/StoreProvider.tsx
+++ /dev/null
@@ -1,45 +0,0 @@
1/*
2 * Copyright (C) 2021-2022 Kristóf Marussy <kristof@marussy.com>
3 *
4 * This file is part of Sophie.
5 *
6 * Sophie is free software: you can redistribute it and/or modify
7 * it under the terms of the GNU Affero General Public License as
8 * published by the Free Software Foundation, version 3.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU Affero General Public License for more details.
14 *
15 * You should have received a copy of the GNU Affero General Public License
16 * along with this program. If not, see <https://www.gnu.org/licenses/>.
17 *
18 * SPDX-License-Identifier: AGPL-3.0-only
19 */
20
21import React, { createContext, useContext } from 'react';
22
23import type RendererStore from '../stores/RendererStore';
24
25const StoreContext = createContext<RendererStore | undefined>(undefined);
26
27export function useStore(): RendererStore {
28 const store = useContext(StoreContext);
29 if (store === undefined) {
30 throw new Error('useStore can only be called inside of StoreProvider');
31 }
32 return store;
33}
34
35export default function StoreProvider({
36 children,
37 store,
38}: {
39 children: JSX.Element | JSX.Element[];
40 store: RendererStore;
41}): JSX.Element {
42 return (
43 <StoreContext.Provider value={store}>{children}</StoreContext.Provider>
44 );
45}
diff --git a/packages/renderer/src/components/locationBar/LocationBar.tsx b/packages/renderer/src/components/locationBar/LocationBar.tsx
index c290722..11981e9 100644
--- a/packages/renderer/src/components/locationBar/LocationBar.tsx
+++ b/packages/renderer/src/components/locationBar/LocationBar.tsx
@@ -22,8 +22,8 @@ import { styled } from '@mui/material/styles';
22import { observer } from 'mobx-react-lite'; 22import { observer } from 'mobx-react-lite';
23import React from 'react'; 23import React from 'react';
24 24
25import type RendererStore from '../../stores/RendererStore';
25import type Service from '../../stores/Service'; 26import type Service from '../../stores/Service';
26import { useStore } from '../StoreProvider';
27 27
28import ExtraButtons from './ExtraButtons'; 28import ExtraButtons from './ExtraButtons';
29import LocationTextField from './LocationTextField'; 29import LocationTextField from './LocationTextField';
@@ -44,11 +44,15 @@ const LocationBarRoot = styled('header', {
44 borderBottom: `1px solid ${theme.palette.divider}`, 44 borderBottom: `1px solid ${theme.palette.divider}`,
45})); 45}));
46 46
47function LocationBar({ service }: { service: Service }): JSX.Element { 47function LocationBar({
48 const { 48 store: {
49 settings: { showLocationBar }, 49 settings: { showLocationBar },
50 } = useStore(); 50 },
51 51 service,
52}: {
53 store: RendererStore;
54 service: Service;
55}): JSX.Element {
52 const { alwaysShowLocationBar } = service; 56 const { alwaysShowLocationBar } = service;
53 const locationBarVisible = showLocationBar || alwaysShowLocationBar; 57 const locationBarVisible = showLocationBar || alwaysShowLocationBar;
54 58
diff --git a/packages/renderer/src/components/sidebar/ServiceSwitcher.tsx b/packages/renderer/src/components/sidebar/ServiceSwitcher.tsx
index 24cfd0c..7aa9124 100644
--- a/packages/renderer/src/components/sidebar/ServiceSwitcher.tsx
+++ b/packages/renderer/src/components/sidebar/ServiceSwitcher.tsx
@@ -26,9 +26,9 @@ import { observer } from 'mobx-react-lite';
26import React from 'react'; 26import React from 'react';
27import { useTranslation } from 'react-i18next'; 27import { useTranslation } from 'react-i18next';
28 28
29import type RendererStore from '../../stores/RendererStore';
29import type Service from '../../stores/Service'; 30import type Service from '../../stores/Service';
30import { getServicePanelID } from '../ServicePanel'; 31import { getServicePanelID } from '../ServicePanel';
31import { useStore } from '../StoreProvider';
32 32
33import ServiceIcon from './ServiceIcon'; 33import ServiceIcon from './ServiceIcon';
34 34
@@ -88,14 +88,13 @@ function getServiceTitle(service: Service, t: TFunction) {
88 return t('service.title.nameWithMessages', { name, messages: messagesText }); 88 return t('service.title.nameWithMessages', { name, messages: messagesText });
89} 89}
90 90
91function ServiceSwitcher(): JSX.Element { 91function ServiceSwitcher({
92 // This needs to be here even if we don't use any translations in this component, 92 store: { settings, services },
93 // because the component must stay suspended until the translations are loaded. 93}: {
94 // See: https://github.com/mui/material-ui/issues/14077 94 store: RendererStore;
95 // TODO Try and remove this once mui and mobx-react-lite have updated to react 18. 95}): JSX.Element {
96 const { t } = useTranslation(); 96 const { t } = useTranslation();
97 97
98 const { settings, services } = useStore();
99 const { selectedService } = settings; 98 const { selectedService } = settings;
100 99
101 return ( 100 return (
diff --git a/packages/renderer/src/components/sidebar/Sidebar.tsx b/packages/renderer/src/components/sidebar/Sidebar.tsx
index fc57302..6c802ac 100644
--- a/packages/renderer/src/components/sidebar/Sidebar.tsx
+++ b/packages/renderer/src/components/sidebar/Sidebar.tsx
@@ -19,54 +19,60 @@
19 */ 19 */
20 20
21import Box from '@mui/material/Box'; 21import Box from '@mui/material/Box';
22import { styled } from '@mui/material/styles';
22import React from 'react'; 23import React from 'react';
23 24
25import RendererStore from '../../stores/RendererStore';
26
24import ServiceSwitcher from './ServiceSwitcher'; 27import ServiceSwitcher from './ServiceSwitcher';
25import ToggleDarkModeButton from './ToggleDarkModeButton'; 28import ToggleDarkModeButton from './ToggleDarkModeButton';
26import ToggleLocationBarButton from './ToggleLocationBarButton'; 29import ToggleLocationBarButton from './ToggleLocationBarButton';
27 30
28export default function Sidebar(): JSX.Element { 31const SidebarRoot = styled(Box)(({ theme }) => ({
32 flex: 0,
33 display: 'flex',
34 position: 'relative',
35 overflow: 'hidden',
36 flexDirection: 'column',
37 alignItems: 'center',
38 padding: `${theme.spacing(1)} 0`,
39 gap: theme.spacing(1),
40 backgroundColor:
41 theme.palette.mode === 'dark'
42 ? 'rgba(255, 255, 255, 0.09)'
43 : 'rgba(0, 0, 0, 0.06)',
44 minWidth: `calc(${theme.spacing(4)} + 36px)`,
45 '::after': {
46 content: '" "',
47 position: 'absolute',
48 top: '-20px',
49 bottom: '-20px',
50 right: '-20px',
51 zIndex: 100,
52 width: '20px',
53 boxShadow: theme.shadows[4],
54 },
55}));
56
57const SidebarFill = styled(Box)({
58 flex: 1,
59 display: 'flex',
60 flexDirection: 'column',
61 justifyContent: 'flex-start',
62});
63
64export default function Sidebar({
65 store,
66}: {
67 store: RendererStore;
68}): JSX.Element {
29 return ( 69 return (
30 <Box 70 <SidebarRoot component="aside">
31 component="aside" 71 <ToggleLocationBarButton store={store} />
32 sx={(theme) => ({ 72 <SidebarFill>
33 flex: 0, 73 <ServiceSwitcher store={store} />
34 display: 'flex', 74 </SidebarFill>
35 position: 'relative', 75 <ToggleDarkModeButton store={store} />
36 overflow: 'hidden', 76 </SidebarRoot>
37 flexDirection: 'column',
38 alignItems: 'center',
39 paddingY: 1,
40 gap: 1,
41 backgroundColor:
42 theme.palette.mode === 'dark'
43 ? 'rgba(255, 255, 255, 0.09)'
44 : 'rgba(0, 0, 0, 0.06)',
45 minWidth: `calc(${theme.spacing(4)} + 36px)`,
46 '::after': {
47 content: '" "',
48 position: 'absolute',
49 top: '-20px',
50 bottom: '-20px',
51 right: '-20px',
52 zIndex: 100,
53 width: '20px',
54 boxShadow: theme.shadows[4],
55 },
56 })}
57 >
58 <ToggleLocationBarButton />
59 <Box
60 sx={{
61 flex: 1,
62 display: 'flex',
63 flexDirection: 'column',
64 justifyContent: 'flex-start',
65 }}
66 >
67 <ServiceSwitcher />
68 </Box>
69 <ToggleDarkModeButton />
70 </Box>
71 ); 77 );
72} 78}
diff --git a/packages/renderer/src/components/sidebar/ToggleDarkModeButton.tsx b/packages/renderer/src/components/sidebar/ToggleDarkModeButton.tsx
index 51c3b18..a922389 100644
--- a/packages/renderer/src/components/sidebar/ToggleDarkModeButton.tsx
+++ b/packages/renderer/src/components/sidebar/ToggleDarkModeButton.tsx
@@ -25,19 +25,23 @@ import { observer } from 'mobx-react-lite';
25import React from 'react'; 25import React from 'react';
26import { useTranslation } from 'react-i18next'; 26import { useTranslation } from 'react-i18next';
27 27
28import { useStore } from '../StoreProvider'; 28import type RendererStore from '../../stores/RendererStore';
29 29
30export default observer(() => { 30function ToggleDarkModeButton({
31 store: { shared },
32}: {
33 store: RendererStore;
34}): JSX.Element {
31 const { t } = useTranslation(); 35 const { t } = useTranslation();
32 const { shared } = useStore();
33 const { shouldUseDarkColors } = shared;
34 36
35 return ( 37 return (
36 <IconButton 38 <IconButton
37 aria-label={t('toolbar.toggleDarkMode')} 39 aria-label={t('toolbar.toggleDarkMode')}
38 onClick={() => shared.toggleDarkMode()} 40 onClick={() => shared.toggleDarkMode()}
39 > 41 >
40 {shouldUseDarkColors ? <LightModeIcon /> : <DarkModeIcon />} 42 {shared.shouldUseDarkColors ? <LightModeIcon /> : <DarkModeIcon />}
41 </IconButton> 43 </IconButton>
42 ); 44 );
43}); 45}
46
47export default observer(ToggleDarkModeButton);
diff --git a/packages/renderer/src/components/sidebar/ToggleLocationBarButton.tsx b/packages/renderer/src/components/sidebar/ToggleLocationBarButton.tsx
index c697170..b6644a3 100644
--- a/packages/renderer/src/components/sidebar/ToggleLocationBarButton.tsx
+++ b/packages/renderer/src/components/sidebar/ToggleLocationBarButton.tsx
@@ -27,7 +27,7 @@ import { observer } from 'mobx-react-lite';
27import React from 'react'; 27import React from 'react';
28import { useTranslation } from 'react-i18next'; 28import { useTranslation } from 'react-i18next';
29 29
30import { useStore } from '../StoreProvider'; 30import type RendererStore from '../../stores/RendererStore';
31import { getLocaltionBarID } from '../locationBar/LocationBar'; 31import { getLocaltionBarID } from '../locationBar/LocationBar';
32 32
33function ToggleLocationBarIcon({ 33function ToggleLocationBarIcon({
@@ -45,12 +45,15 @@ function ToggleLocationBarIcon({
45 return left ? <IconChevronLeft /> : <IconChevronRight />; 45 return left ? <IconChevronLeft /> : <IconChevronRight />;
46} 46}
47 47
48function ToggleLocationBarButton(): JSX.Element { 48function ToggleLocationBarButton({
49 const { t } = useTranslation(); 49 store: {
50 const {
51 shared: { locationBarVisible, canToggleLocationBar }, 50 shared: { locationBarVisible, canToggleLocationBar },
52 settings, 51 settings,
53 } = useStore(); 52 },
53}: {
54 store: RendererStore;
55}): JSX.Element {
56 const { t } = useTranslation();
54 const { selectedService } = settings; 57 const { selectedService } = settings;
55 58
56 return ( 59 return (
diff --git a/packages/renderer/src/index.tsx b/packages/renderer/src/index.tsx
index 9971469..09b9b74 100644
--- a/packages/renderer/src/index.tsx
+++ b/packages/renderer/src/index.tsx
@@ -29,7 +29,6 @@ import React, { Suspense, lazy } from 'react';
29import { createRoot } from 'react-dom/client'; 29import { createRoot } from 'react-dom/client';
30 30
31import Loading from './components/Loading'; 31import Loading from './components/Loading';
32import StoreProvider from './components/StoreProvider';
33import ThemeProvider from './components/ThemeProvider'; 32import ThemeProvider from './components/ThemeProvider';
34import { exposeToReduxDevtools, hotReload } from './devTools'; 33import { exposeToReduxDevtools, hotReload } from './devTools';
35import RtlCacheProvider from './i18n/RtlCacheProvider'; 34import RtlCacheProvider from './i18n/RtlCacheProvider';
@@ -78,9 +77,7 @@ function Root(): JSX.Element {
78 <ThemeProvider store={store}> 77 <ThemeProvider store={store}>
79 <CssBaseline enableColorScheme /> 78 <CssBaseline enableColorScheme />
80 <Suspense fallback={<Loading />}> 79 <Suspense fallback={<Loading />}>
81 <StoreProvider store={store}> 80 <App store={store} devMode={isDevelopment} />
82 <App devMode={isDevelopment} />
83 </StoreProvider>
84 </Suspense> 81 </Suspense>
85 </ThemeProvider> 82 </ThemeProvider>
86 </RtlCacheProvider> 83 </RtlCacheProvider>