aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLibravatar Kristóf Marussy <kristof@marussy.com>2022-04-20 00:28:38 +0200
committerLibravatar Kristóf Marussy <kristof@marussy.com>2022-05-16 00:55:01 +0200
commit9462cec9a7dc5b6722df464559721cd2445636e6 (patch)
tree08a266cc2c6d65e55ffe76b279f310d1cf517bdd
parentfix(renderer): Improve appearance in small windows (diff)
downloadsophie-9462cec9a7dc5b6722df464559721cd2445636e6.tar.gz
sophie-9462cec9a7dc5b6722df464559721cd2445636e6.tar.zst
sophie-9462cec9a7dc5b6722df464559721cd2445636e6.zip
feat: Always show location bar on error
The location bar should be visible on error page to let people see and change the URL if needed. Additionally, it must be visible on insecure connections to show people that the connection if not secure. In the future, the location bar should also be shown if the loaded website is uncommon in the context of the current service, i.e., it is not explcitly allowlisted by the recipe. Signed-off-by: Kristóf Marussy <kristof@marussy.com>
-rw-r--r--locales/en/translation.json4
-rw-r--r--packages/renderer/src/components/locationBar/LocationBar.tsx5
-rw-r--r--packages/renderer/src/components/locationBar/LocationTextField.tsx13
-rw-r--r--packages/renderer/src/components/locationBar/SecurityLabel.tsx79
-rw-r--r--packages/renderer/src/components/locationBar/UrlOverlay.tsx61
-rw-r--r--packages/renderer/src/components/locationBar/splitUrl.ts62
-rw-r--r--packages/renderer/src/components/sidebar/ToggleLocationBarButton.tsx12
-rw-r--r--packages/shared/src/index.ts1
-rw-r--r--packages/shared/src/stores/ServiceBase.ts55
-rw-r--r--packages/shared/src/stores/SharedStoreBase.ts39
10 files changed, 190 insertions, 141 deletions
diff --git a/locales/en/translation.json b/locales/en/translation.json
index d04f1f3..74320f3 100644
--- a/locales/en/translation.json
+++ b/locales/en/translation.json
@@ -31,9 +31,11 @@
31 } 31 }
32 }, 32 },
33 "securityLabel": { 33 "securityLabel": {
34 "unspecific": "Unknown error",
34 "notSecureConnection": "Not secure", 35 "notSecureConnection": "Not secure",
35 "secureConnection": "Secure connection", 36 "secureConnection": "Secure connection",
36 "unknownSite": "Unknown site" 37 "certificateError": "Certificate error",
38 "invalidURL": "Invalid URL"
37 }, 39 },
38 "service": { 40 "service": {
39 "title": { 41 "title": {
diff --git a/packages/renderer/src/components/locationBar/LocationBar.tsx b/packages/renderer/src/components/locationBar/LocationBar.tsx
index fc9c147..54ead8e 100644
--- a/packages/renderer/src/components/locationBar/LocationBar.tsx
+++ b/packages/renderer/src/components/locationBar/LocationBar.tsx
@@ -43,11 +43,12 @@ const LocationBarRoot = styled('header', {
43 43
44function LocationBar(): JSX.Element { 44function LocationBar(): JSX.Element {
45 const { 45 const {
46 settings: { selectedService, showLocationBar }, 46 shared: { locationBarVisible },
47 settings: { selectedService },
47 } = useStore(); 48 } = useStore();
48 49
49 return ( 50 return (
50 <LocationBarRoot id={LOCATION_BAR_ID} hidden={!showLocationBar}> 51 <LocationBarRoot id={LOCATION_BAR_ID} hidden={!locationBarVisible}>
51 <NavigationButtons service={selectedService} /> 52 <NavigationButtons service={selectedService} />
52 <LocationTextField service={selectedService} /> 53 <LocationTextField service={selectedService} />
53 <ExtraButtons service={selectedService} /> 54 <ExtraButtons service={selectedService} />
diff --git a/packages/renderer/src/components/locationBar/LocationTextField.tsx b/packages/renderer/src/components/locationBar/LocationTextField.tsx
index 30953de..85cf794 100644
--- a/packages/renderer/src/components/locationBar/LocationTextField.tsx
+++ b/packages/renderer/src/components/locationBar/LocationTextField.tsx
@@ -20,6 +20,7 @@
20 20
21import FilledInput from '@mui/material/FilledInput'; 21import FilledInput from '@mui/material/FilledInput';
22import { styled } from '@mui/material/styles'; 22import { styled } from '@mui/material/styles';
23import { SecurityLabelKind } from '@sophie/shared';
23import { autorun } from 'mobx'; 24import { autorun } from 'mobx';
24import { observer } from 'mobx-react-lite'; 25import { observer } from 'mobx-react-lite';
25import React, { useCallback, useEffect, useState } from 'react'; 26import React, { useCallback, useEffect, useState } from 'react';
@@ -30,7 +31,6 @@ import GoButton from './GoButton';
30import LocationOverlayInput from './LocationOverlayInput'; 31import LocationOverlayInput from './LocationOverlayInput';
31import SecurityLabel from './SecurityLabel'; 32import SecurityLabel from './SecurityLabel';
32import UrlOverlay from './UrlOverlay'; 33import UrlOverlay from './UrlOverlay';
33import splitUrl from './splitUrl';
34 34
35const LocationTextFieldRoot = styled(FilledInput, { 35const LocationTextFieldRoot = styled(FilledInput, {
36 name: 'LocationTextField', 36 name: 'LocationTextField',
@@ -75,8 +75,6 @@ function LocationTextField({
75 [setInputFocused], 75 [setInputFocused],
76 ); 76 );
77 77
78 const splitResult = splitUrl(service?.currentUrl);
79
80 return ( 78 return (
81 <LocationTextFieldRoot 79 <LocationTextFieldRoot
82 inputComponent={LocationOverlayInput} 80 inputComponent={LocationOverlayInput}
@@ -84,7 +82,12 @@ function LocationTextField({
84 'aria-label': 'Location', 82 'aria-label': 'Location',
85 spellCheck: false, 83 spellCheck: false,
86 overlayVisible: !inputFocused && !changed, 84 overlayVisible: !inputFocused && !changed,
87 overlay: <UrlOverlay splitResult={splitResult} />, 85 overlay: (
86 <UrlOverlay
87 url={service?.currentUrl ?? ''}
88 alert={service?.hasSecurityLabelWarning ?? false}
89 />
90 ),
88 }} 91 }}
89 inputRef={inputRefCallback} 92 inputRef={inputRefCallback}
90 onFocus={() => setInputFocused(true)} 93 onFocus={() => setInputFocused(true)}
@@ -114,8 +117,8 @@ function LocationTextField({
114 disableUnderline 117 disableUnderline
115 startAdornment={ 118 startAdornment={
116 <SecurityLabel 119 <SecurityLabel
120 kind={service?.securityLabel ?? SecurityLabelKind.Empty}
117 changed={changed} 121 changed={changed}
118 splitResult={splitResult}
119 position="start" 122 position="start"
120 /> 123 />
121 } 124 }
diff --git a/packages/renderer/src/components/locationBar/SecurityLabel.tsx b/packages/renderer/src/components/locationBar/SecurityLabel.tsx
index 0017f89..ac51cff 100644
--- a/packages/renderer/src/components/locationBar/SecurityLabel.tsx
+++ b/packages/renderer/src/components/locationBar/SecurityLabel.tsx
@@ -18,17 +18,17 @@
18 * SPDX-License-Identifier: AGPL-3.0-only 18 * SPDX-License-Identifier: AGPL-3.0-only
19 */ 19 */
20 20
21import IconHttps from '@mui/icons-material/HttpsOutlined'; 21import HttpsOutliedIcon from '@mui/icons-material/HttpsOutlined';
22import IconHttp from '@mui/icons-material/NoEncryption'; 22import NoEncrpytionIcon from '@mui/icons-material/NoEncryption';
23import IconGlobe from '@mui/icons-material/Public'; 23import PublicIcon from '@mui/icons-material/Public';
24import IconWarning from '@mui/icons-material/Warning'; 24import WarningIcon from '@mui/icons-material/Warning';
25import { styled } from '@mui/material/styles'; 25import { styled } from '@mui/material/styles';
26import { SecurityLabelKind } from '@sophie/shared';
26import React from 'react'; 27import React from 'react';
27import { useTranslation } from 'react-i18next'; 28import { useTranslation } from 'react-i18next';
28 29
29import LocationInputAdornment from './LocationInputAdornment'; 30import LocationInputAdornment from './LocationInputAdornment';
30import getAlertColor from './getAlertColor'; 31import getAlertColor from './getAlertColor';
31import type { SplitResult } from './splitUrl';
32 32
33const SecurityLabelRoot = styled(LocationInputAdornment, { 33const SecurityLabelRoot = styled(LocationInputAdornment, {
34 name: 'SecurityLabel', 34 name: 'SecurityLabel',
@@ -53,55 +53,48 @@ const SecurityLabelText = styled('span', {
53})); 53}));
54 54
55export default function SecurityLabel({ 55export default function SecurityLabel({
56 splitResult, 56 kind,
57 changed, 57 changed,
58 position, 58 position,
59}: { 59}: {
60 splitResult: SplitResult; 60 kind: SecurityLabelKind;
61 changed: boolean; 61 changed: boolean;
62 position: 'start' | 'end'; 62 position: 'start' | 'end';
63}): JSX.Element { 63}): JSX.Element {
64 const { t } = useTranslation(undefined, { 64 const { t } = useTranslation();
65 keyPrefix: 'securityLabel',
66 });
67 65
68 const { type } = splitResult; 66 if (changed || kind === SecurityLabelKind.Empty) {
69 if (changed || type === 'empty') {
70 return ( 67 return (
71 <SecurityLabelRoot alert={false} position={position} aria-hidden> 68 <SecurityLabelRoot alert={false} position={position} aria-hidden>
72 <IconGlobe fontSize="small" /> 69 <PublicIcon fontSize="small" />
73 </SecurityLabelRoot> 70 </SecurityLabelRoot>
74 ); 71 );
75 } 72 }
76 switch (type) { 73 if (kind === SecurityLabelKind.SecureConnection) {
77 case 'valid': { 74 return (
78 const { secure } = splitResult; 75 <SecurityLabelRoot
79 return secure ? ( 76 alert={false}
80 <SecurityLabelRoot 77 position={position}
81 alert={false} 78 aria-label={t('securityLabel.secureConnection')}
82 position={position} 79 >
83 aria-label={t('secureConnection')} 80 <HttpsOutliedIcon fontSize="small" />
84 > 81 </SecurityLabelRoot>
85 <IconHttps fontSize="small" /> 82 );
86 </SecurityLabelRoot>
87 ) : (
88 <SecurityLabelRoot alert position={position}>
89 <IconHttp fontSize="small" />
90 <SecurityLabelText>{t('notSecureConnection')}</SecurityLabelText>
91 </SecurityLabelRoot>
92 );
93 }
94 case 'invalid':
95 return (
96 <SecurityLabelRoot alert position={position}>
97 <IconWarning fontSize="small" />
98 <SecurityLabelText>{t('unknownSite')}</SecurityLabelText>
99 </SecurityLabelRoot>
100 );
101 default:
102 /* eslint-disable-next-line @typescript-eslint/restrict-template-expressions --
103 Error handling for impossible case.
104 */
105 throw new Error(`Unexpected SplitResult: ${type}`);
106 } 83 }
84 const tslError =
85 kind === SecurityLabelKind.NotSecureConnection ||
86 kind === SecurityLabelKind.CertificateError;
87 const icon = tslError ? (
88 <NoEncrpytionIcon fontSize="small" />
89 ) : (
90 <WarningIcon fontSize="small" />
91 );
92 return (
93 <SecurityLabelRoot alert position={position}>
94 {icon}
95 <SecurityLabelText>
96 {t([`securityLabel.${kind}`, 'securityLabel.unspecific'])}
97 </SecurityLabelText>
98 </SecurityLabelRoot>
99 );
107} 100}
diff --git a/packages/renderer/src/components/locationBar/UrlOverlay.tsx b/packages/renderer/src/components/locationBar/UrlOverlay.tsx
index d590709..a71fa4e 100644
--- a/packages/renderer/src/components/locationBar/UrlOverlay.tsx
+++ b/packages/renderer/src/components/locationBar/UrlOverlay.tsx
@@ -22,7 +22,47 @@ import { styled } from '@mui/material/styles';
22import React from 'react'; 22import React from 'react';
23 23
24import getAlertColor from './getAlertColor'; 24import getAlertColor from './getAlertColor';
25import type { SplitResult } from './splitUrl'; 25
26export type SplitResult =
27 | {
28 type: 'hostOnly';
29 prefix: string;
30 host: string;
31 suffix: string;
32 }
33 | {
34 type: 'wholeString';
35 value: string;
36 }
37 | {
38 type: 'empty';
39 };
40
41function splitUrl(urlString: string | undefined): SplitResult {
42 if (urlString === undefined || urlString === '') {
43 return { type: 'empty' };
44 }
45 let url: URL;
46 try {
47 url = new URL(urlString);
48 } catch {
49 return { type: 'wholeString', value: urlString };
50 }
51 const { protocol, host, username, password, pathname, search, hash } = url;
52 if (host !== '') {
53 return {
54 type: 'hostOnly',
55 prefix: `${protocol}//${
56 username === ''
57 ? ''
58 : `${username}${password === '' ? '' : `:${password}`}@`
59 }`,
60 host,
61 suffix: `${pathname}${search}${hash}`,
62 };
63 }
64 return { type: 'wholeString', value: urlString };
65}
26 66
27const PrimaryFragment = styled('span', { 67const PrimaryFragment = styled('span', {
28 name: 'LocationOverlayInput', 68 name: 'LocationOverlayInput',
@@ -40,28 +80,31 @@ const SecondaryFragment = styled('span', {
40})); 80}));
41 81
42export default function UrlOverlay({ 82export default function UrlOverlay({
43 splitResult, 83 url,
84 alert,
44}: { 85}: {
45 splitResult: SplitResult; 86 url: string | undefined;
87 alert: boolean;
46}): JSX.Element { 88}): JSX.Element {
89 const splitResult = splitUrl(url);
47 const { type } = splitResult; 90 const { type } = splitResult;
48 switch (type) { 91 switch (type) {
49 case 'valid': { 92 case 'hostOnly': {
50 const { secure, prefix, host, suffix } = splitResult; 93 const { prefix, host, suffix } = splitResult;
51 return ( 94 return (
52 <> 95 <>
53 <SecondaryFragment>{prefix}</SecondaryFragment> 96 <SecondaryFragment>{prefix}</SecondaryFragment>
54 <PrimaryFragment alert={!secure}>{host}</PrimaryFragment> 97 <PrimaryFragment alert={alert}>{host}</PrimaryFragment>
55 <SecondaryFragment>{suffix}</SecondaryFragment> 98 <SecondaryFragment>{suffix}</SecondaryFragment>
56 </> 99 </>
57 ); 100 );
58 } 101 }
59 case 'invalid': { 102 case 'wholeString': {
60 const { value } = splitResult; 103 const { value } = splitResult;
61 return <PrimaryFragment alert>{value}</PrimaryFragment>; 104 return <PrimaryFragment alert={alert}>{value}</PrimaryFragment>;
62 } 105 }
63 case 'empty': 106 case 'empty':
64 return <PrimaryFragment alert={false} />; 107 return <PrimaryFragment alert={alert} />;
65 default: 108 default:
66 /* eslint-disable-next-line @typescript-eslint/restrict-template-expressions -- 109 /* eslint-disable-next-line @typescript-eslint/restrict-template-expressions --
67 Error handling for impossible case. 110 Error handling for impossible case.
diff --git a/packages/renderer/src/components/locationBar/splitUrl.ts b/packages/renderer/src/components/locationBar/splitUrl.ts
deleted file mode 100644
index 6e95472..0000000
--- a/packages/renderer/src/components/locationBar/splitUrl.ts
+++ /dev/null
@@ -1,62 +0,0 @@
1/*
2 * Copyright (C) 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
21export type SplitResult =
22 | {
23 type: 'valid';
24 secure: boolean;
25 prefix: string;
26 host: string;
27 suffix: string;
28 }
29 | {
30 type: 'invalid';
31 value: string;
32 }
33 | {
34 type: 'empty';
35 };
36
37export default function splitUrl(urlString: string | undefined): SplitResult {
38 if (urlString === undefined || urlString === '') {
39 return { type: 'empty' };
40 }
41 let url: URL;
42 try {
43 url = new URL(urlString);
44 } catch {
45 return { type: 'invalid', value: urlString };
46 }
47 const { protocol, host, username, password, pathname, search, hash } = url;
48 if (protocol === 'http:' || protocol === 'https:') {
49 return {
50 type: 'valid',
51 secure: protocol === 'https:',
52 prefix: `${protocol}//${
53 username === ''
54 ? ''
55 : `${username}${password === '' ? '' : `:${password}`}@`
56 }`,
57 host,
58 suffix: `${pathname}${search}${hash}`,
59 };
60 }
61 return { type: 'invalid', value: urlString };
62}
diff --git a/packages/renderer/src/components/sidebar/ToggleLocationBarButton.tsx b/packages/renderer/src/components/sidebar/ToggleLocationBarButton.tsx
index 325160e..7fc559d 100644
--- a/packages/renderer/src/components/sidebar/ToggleLocationBarButton.tsx
+++ b/packages/renderer/src/components/sidebar/ToggleLocationBarButton.tsx
@@ -47,19 +47,23 @@ function ToggleLocationBarIcon({
47 47
48function ToggleLocationBarButton(): JSX.Element { 48function ToggleLocationBarButton(): JSX.Element {
49 const { t } = useTranslation(); 49 const { t } = useTranslation();
50 const { settings } = useStore(); 50 const {
51 const { selectedService, showLocationBar } = settings; 51 shared: { locationBarVisible, canToggleLocationBar },
52 settings,
53 } = useStore();
54 const { selectedService } = settings;
52 55
53 return ( 56 return (
54 <IconButton 57 <IconButton
55 aria-pressed={showLocationBar} 58 disabled={!canToggleLocationBar}
59 aria-pressed={locationBarVisible}
56 aria-controls={LOCATION_BAR_ID} 60 aria-controls={LOCATION_BAR_ID}
57 aria-label={t('toolbar.toggleLocationBar')} 61 aria-label={t('toolbar.toggleLocationBar')}
58 onClick={() => settings.toggleLocationBar()} 62 onClick={() => settings.toggleLocationBar()}
59 > 63 >
60 <ToggleLocationBarIcon 64 <ToggleLocationBarIcon
61 loading={selectedService?.loading ?? false} 65 loading={selectedService?.loading ?? false}
62 show={showLocationBar} 66 show={locationBarVisible}
63 /> 67 />
64 </IconButton> 68 </IconButton>
65 ); 69 );
diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts
index 95af73a..e3da192 100644
--- a/packages/shared/src/index.ts
+++ b/packages/shared/src/index.ts
@@ -56,6 +56,7 @@ export { default as ProfileSettings } from './stores/ProfileSettings';
56export { 56export {
57 default as ServiceBase, 57 default as ServiceBase,
58 defineServiceModel, 58 defineServiceModel,
59 SecurityLabelKind,
59} from './stores/ServiceBase'; 60} from './stores/ServiceBase';
60 61
61export type { 62export type {
diff --git a/packages/shared/src/stores/ServiceBase.ts b/packages/shared/src/stores/ServiceBase.ts
index a19f59e..0e4be97 100644
--- a/packages/shared/src/stores/ServiceBase.ts
+++ b/packages/shared/src/stores/ServiceBase.ts
@@ -25,6 +25,14 @@ import type Profile from './Profile';
25import ServiceSettingsBase from './ServiceSettingsBase'; 25import ServiceSettingsBase from './ServiceSettingsBase';
26import ServiceState from './ServiceState'; 26import ServiceState from './ServiceState';
27 27
28export enum SecurityLabelKind {
29 Empty = 'empty',
30 SecureConnection = 'secureConnection',
31 NotSecureConnection = 'notSecureConnection',
32 CertificateError = 'certificateError',
33 InvalidURL = 'invalidURL',
34}
35
28export function defineServiceModel<TS extends IAnyModelType>(settings: TS) { 36export function defineServiceModel<TS extends IAnyModelType>(settings: TS) {
29 return types 37 return types
30 .model('Service', { 38 .model('Service', {
@@ -48,6 +56,38 @@ export function defineServiceModel<TS extends IAnyModelType>(settings: TS) {
48 get crashed(): boolean { 56 get crashed(): boolean {
49 return self.state.type === 'crashed'; 57 return self.state.type === 'crashed';
50 }, 58 },
59 isCertificateTemporarilyTrusted(
60 certificate: CertificateSnapshotIn,
61 ): boolean {
62 return (
63 self.settings.profile as Profile
64 ).isCertificateTemporarilyTrusted(certificate);
65 },
66 get securityLabel(): SecurityLabelKind {
67 const {
68 state: { type: stateType },
69 currentUrl,
70 } = self;
71 if (stateType === 'certificateError') {
72 return SecurityLabelKind.CertificateError;
73 }
74 if (currentUrl === undefined || currentUrl === '') {
75 return SecurityLabelKind.Empty;
76 }
77 try {
78 const parsedUrl = new URL(currentUrl);
79 switch (parsedUrl.protocol) {
80 case 'https:':
81 return SecurityLabelKind.SecureConnection;
82 case 'http:':
83 return SecurityLabelKind.NotSecureConnection;
84 default:
85 return SecurityLabelKind.InvalidURL;
86 }
87 } catch {
88 return SecurityLabelKind.InvalidURL;
89 }
90 },
51 })) 91 }))
52 .views((self) => ({ 92 .views((self) => ({
53 get hasError(): boolean { 93 get hasError(): boolean {
@@ -57,12 +97,17 @@ export function defineServiceModel<TS extends IAnyModelType>(settings: TS) {
57 self.state.type === 'certificateError' 97 self.state.type === 'certificateError'
58 ); 98 );
59 }, 99 },
60 isCertificateTemporarilyTrusted( 100 get hasSecurityLabelWarning(): boolean {
61 certificate: CertificateSnapshotIn, 101 const { securityLabel } = self;
62 ): boolean {
63 return ( 102 return (
64 self.settings.profile as Profile 103 securityLabel !== SecurityLabelKind.Empty &&
65 ).isCertificateTemporarilyTrusted(certificate); 104 securityLabel !== SecurityLabelKind.SecureConnection
105 );
106 },
107 }))
108 .views((self) => ({
109 get alwaysShowLocationBar(): boolean {
110 return self.hasError || self.hasSecurityLabelWarning;
66 }, 111 },
67 })); 112 }));
68} 113}
diff --git a/packages/shared/src/stores/SharedStoreBase.ts b/packages/shared/src/stores/SharedStoreBase.ts
index bd71cea..e4b3a38 100644
--- a/packages/shared/src/stores/SharedStoreBase.ts
+++ b/packages/shared/src/stores/SharedStoreBase.ts
@@ -39,16 +39,35 @@ export function defineSharedStoreModel<
39 TP extends IAnyModelType, 39 TP extends IAnyModelType,
40 TS extends IAnyModelType, 40 TS extends IAnyModelType,
41>(globalSettings: TG, profile: TP, service: TS) { 41>(globalSettings: TG, profile: TP, service: TS) {
42 return types.model('SharedStore', { 42 return types
43 settings: types.optional(globalSettings, {}), 43 .model('SharedStore', {
44 profilesById: types.map(profile), 44 settings: types.optional(globalSettings, {}),
45 profiles: types.array(types.reference(profile)), 45 profilesById: types.map(profile),
46 servicesById: types.map(service), 46 profiles: types.array(types.reference(profile)),
47 services: types.array(types.reference(service)), 47 servicesById: types.map(service),
48 shouldUseDarkColors: false, 48 services: types.array(types.reference(service)),
49 language: FALLBACK_LOCALE, 49 shouldUseDarkColors: false,
50 writingDirection: types.optional(WritingDirection, 'ltr'), 50 language: FALLBACK_LOCALE,
51 }); 51 writingDirection: types.optional(WritingDirection, 'ltr'),
52 })
53 .views((self) => ({
54 get alwaysShowLocationBar(): boolean {
55 const settings = self.settings as GlobalSettingsBase;
56 const selectedService = settings.selectedService as
57 | ServiceBase
58 | undefined;
59 return selectedService?.alwaysShowLocationBar ?? false;
60 },
61 }))
62 .views((self) => ({
63 get locationBarVisible(): boolean {
64 const settings = self.settings as GlobalSettingsBase;
65 return settings.showLocationBar || self.alwaysShowLocationBar;
66 },
67 get canToggleLocationBar(): boolean {
68 return !self.alwaysShowLocationBar;
69 },
70 }));
52} 71}
53 72
54const SharedStoreBase = /* @__PURE__ */ (() => 73const SharedStoreBase = /* @__PURE__ */ (() =>