aboutsummaryrefslogtreecommitdiffstats
path: root/packages
diff options
context:
space:
mode:
Diffstat (limited to 'packages')
-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
9 files changed, 187 insertions, 140 deletions
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__ */ (() =>