diff options
author | Kristóf Marussy <kristof@marussy.com> | 2022-04-20 00:28:38 +0200 |
---|---|---|
committer | Kristóf Marussy <kristof@marussy.com> | 2022-05-16 00:55:01 +0200 |
commit | 9462cec9a7dc5b6722df464559721cd2445636e6 (patch) | |
tree | 08a266cc2c6d65e55ffe76b279f310d1cf517bdd | |
parent | fix(renderer): Improve appearance in small windows (diff) | |
download | sophie-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>
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 | ||
44 | function LocationBar(): JSX.Element { | 44 | function 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 | ||
21 | import FilledInput from '@mui/material/FilledInput'; | 21 | import FilledInput from '@mui/material/FilledInput'; |
22 | import { styled } from '@mui/material/styles'; | 22 | import { styled } from '@mui/material/styles'; |
23 | import { SecurityLabelKind } from '@sophie/shared'; | ||
23 | import { autorun } from 'mobx'; | 24 | import { autorun } from 'mobx'; |
24 | import { observer } from 'mobx-react-lite'; | 25 | import { observer } from 'mobx-react-lite'; |
25 | import React, { useCallback, useEffect, useState } from 'react'; | 26 | import React, { useCallback, useEffect, useState } from 'react'; |
@@ -30,7 +31,6 @@ import GoButton from './GoButton'; | |||
30 | import LocationOverlayInput from './LocationOverlayInput'; | 31 | import LocationOverlayInput from './LocationOverlayInput'; |
31 | import SecurityLabel from './SecurityLabel'; | 32 | import SecurityLabel from './SecurityLabel'; |
32 | import UrlOverlay from './UrlOverlay'; | 33 | import UrlOverlay from './UrlOverlay'; |
33 | import splitUrl from './splitUrl'; | ||
34 | 34 | ||
35 | const LocationTextFieldRoot = styled(FilledInput, { | 35 | const 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 | ||
21 | import IconHttps from '@mui/icons-material/HttpsOutlined'; | 21 | import HttpsOutliedIcon from '@mui/icons-material/HttpsOutlined'; |
22 | import IconHttp from '@mui/icons-material/NoEncryption'; | 22 | import NoEncrpytionIcon from '@mui/icons-material/NoEncryption'; |
23 | import IconGlobe from '@mui/icons-material/Public'; | 23 | import PublicIcon from '@mui/icons-material/Public'; |
24 | import IconWarning from '@mui/icons-material/Warning'; | 24 | import WarningIcon from '@mui/icons-material/Warning'; |
25 | import { styled } from '@mui/material/styles'; | 25 | import { styled } from '@mui/material/styles'; |
26 | import { SecurityLabelKind } from '@sophie/shared'; | ||
26 | import React from 'react'; | 27 | import React from 'react'; |
27 | import { useTranslation } from 'react-i18next'; | 28 | import { useTranslation } from 'react-i18next'; |
28 | 29 | ||
29 | import LocationInputAdornment from './LocationInputAdornment'; | 30 | import LocationInputAdornment from './LocationInputAdornment'; |
30 | import getAlertColor from './getAlertColor'; | 31 | import getAlertColor from './getAlertColor'; |
31 | import type { SplitResult } from './splitUrl'; | ||
32 | 32 | ||
33 | const SecurityLabelRoot = styled(LocationInputAdornment, { | 33 | const SecurityLabelRoot = styled(LocationInputAdornment, { |
34 | name: 'SecurityLabel', | 34 | name: 'SecurityLabel', |
@@ -53,55 +53,48 @@ const SecurityLabelText = styled('span', { | |||
53 | })); | 53 | })); |
54 | 54 | ||
55 | export default function SecurityLabel({ | 55 | export 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'; | |||
22 | import React from 'react'; | 22 | import React from 'react'; |
23 | 23 | ||
24 | import getAlertColor from './getAlertColor'; | 24 | import getAlertColor from './getAlertColor'; |
25 | import type { SplitResult } from './splitUrl'; | 25 | |
26 | export 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 | |||
41 | function 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 | ||
27 | const PrimaryFragment = styled('span', { | 67 | const PrimaryFragment = styled('span', { |
28 | name: 'LocationOverlayInput', | 68 | name: 'LocationOverlayInput', |
@@ -40,28 +80,31 @@ const SecondaryFragment = styled('span', { | |||
40 | })); | 80 | })); |
41 | 81 | ||
42 | export default function UrlOverlay({ | 82 | export 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 | |||
21 | export 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 | |||
37 | export 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 | ||
48 | function ToggleLocationBarButton(): JSX.Element { | 48 | function 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'; | |||
56 | export { | 56 | export { |
57 | default as ServiceBase, | 57 | default as ServiceBase, |
58 | defineServiceModel, | 58 | defineServiceModel, |
59 | SecurityLabelKind, | ||
59 | } from './stores/ServiceBase'; | 60 | } from './stores/ServiceBase'; |
60 | 61 | ||
61 | export type { | 62 | export 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'; | |||
25 | import ServiceSettingsBase from './ServiceSettingsBase'; | 25 | import ServiceSettingsBase from './ServiceSettingsBase'; |
26 | import ServiceState from './ServiceState'; | 26 | import ServiceState from './ServiceState'; |
27 | 27 | ||
28 | export enum SecurityLabelKind { | ||
29 | Empty = 'empty', | ||
30 | SecureConnection = 'secureConnection', | ||
31 | NotSecureConnection = 'notSecureConnection', | ||
32 | CertificateError = 'certificateError', | ||
33 | InvalidURL = 'invalidURL', | ||
34 | } | ||
35 | |||
28 | export function defineServiceModel<TS extends IAnyModelType>(settings: TS) { | 36 | export 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 | ||
54 | const SharedStoreBase = /* @__PURE__ */ (() => | 73 | const SharedStoreBase = /* @__PURE__ */ (() => |