summaryrefslogtreecommitdiffhomepage
path: root/gui/src
diff options
context:
space:
mode:
authorOskar <oskar@mullvad.net>2024-09-30 10:30:47 +0200
committerOskar <oskar@mullvad.net>2024-09-30 10:30:47 +0200
commit978211cf731bc363781b3eabbe7541cf10f30b98 (patch)
treecbb1de1a4b04fdb96864692666a94e0202998691 /gui/src
parent25ddd914ff6136bc6c2b68855fd2627adab3b410 (diff)
parente15e49ca37c375b136b370210d8cac612a980915 (diff)
downloadmullvadvpn-978211cf731bc363781b3eabbe7541cf10f30b98.tar.xz
mullvadvpn-978211cf731bc363781b3eabbe7541cf10f30b98.zip
Merge branch 'implement-design-for-brought-forward-features-des-1065'
Diffstat (limited to 'gui/src')
-rw-r--r--gui/src/renderer/components/AppRouter.tsx2
-rw-r--r--gui/src/renderer/components/DaitaSettings.tsx110
-rw-r--r--gui/src/renderer/components/MultihopSettings.tsx133
-rw-r--r--gui/src/renderer/components/PageSlider.tsx241
-rw-r--r--gui/src/renderer/components/Settings.tsx38
-rw-r--r--gui/src/renderer/components/WireguardSettings.tsx118
-rw-r--r--gui/src/renderer/lib/routes.ts3
-rw-r--r--gui/src/shared/utils.ts2
8 files changed, 504 insertions, 143 deletions
diff --git a/gui/src/renderer/components/AppRouter.tsx b/gui/src/renderer/components/AppRouter.tsx
index 75b8df936c..e1a3d2ab0f 100644
--- a/gui/src/renderer/components/AppRouter.tsx
+++ b/gui/src/renderer/components/AppRouter.tsx
@@ -24,6 +24,7 @@ import Filter from './Filter';
import Focus, { IFocusHandle } from './Focus';
import Launch from './Launch';
import MainView from './main-view/MainView';
+import MultihopSettings from './MultihopSettings';
import OpenVpnSettings from './OpenVpnSettings';
import ProblemReport from './ProblemReport';
import SelectLanguage from './SelectLanguage';
@@ -84,6 +85,7 @@ export default function AppRouter() {
<Route exact path={RoutePath.settings} component={Settings} />
<Route exact path={RoutePath.selectLanguage} component={SelectLanguage} />
<Route exact path={RoutePath.userInterfaceSettings} component={UserInterfaceSettings} />
+ <Route exact path={RoutePath.multihopSettings} component={MultihopSettings} />
<Route exact path={RoutePath.vpnSettings} component={VpnSettings} />
<Route exact path={RoutePath.wireguardSettings} component={WireguardSettings} />
<Route exact path={RoutePath.daitaSettings} component={DaitaSettings} />
diff --git a/gui/src/renderer/components/DaitaSettings.tsx b/gui/src/renderer/components/DaitaSettings.tsx
index 3176192ad1..b4ee10c94d 100644
--- a/gui/src/renderer/components/DaitaSettings.tsx
+++ b/gui/src/renderer/components/DaitaSettings.tsx
@@ -1,4 +1,4 @@
-import { useCallback } from 'react';
+import React, { useCallback } from 'react';
import { sprintf } from 'sprintf-js';
import styled from 'styled-components';
@@ -17,11 +17,11 @@ import { ModalAlert, ModalAlertType, ModalMessage } from './Modal';
import {
NavigationBar,
NavigationContainer,
- NavigationInfoButton,
NavigationItems,
NavigationScrollbars,
TitleBarItem,
} from './NavigationBar';
+import PageSlider from './PageSlider';
import SettingsHeader, { HeaderSubTitle, HeaderTitle } from './SettingsHeader';
import { SmallButton, SmallButtonColor } from './SmallButton';
@@ -32,6 +32,23 @@ const StyledContent = styled.div({
marginBottom: '2px',
});
+const StyledHeaderSubTitle = styled(HeaderSubTitle)({
+ display: 'inline-block',
+
+ '&&:not(:last-child)': {
+ paddingBottom: '18px',
+ },
+});
+
+const EnableFooter = styled(Cell.CellFooter)({
+ paddingBottom: '16px',
+});
+
+export const StyledIllustration = styled.img({
+ width: '100%',
+ padding: '8px 0 8px',
+});
+
export default function DaitaSettings() {
const { pop } = useHistory();
@@ -43,30 +60,49 @@ export default function DaitaSettings() {
<NavigationBar>
<NavigationItems>
<TitleBarItem>{strings.daita}</TitleBarItem>
-
- <NavigationInfoButton>
- <ModalMessage>
- {sprintf(
- messages.pgettext(
- 'wireguard-settings-view',
- '%(daita)s (%(daitaFull)s) hides patterns in your encrypted VPN traffic. If anyone is monitoring your connection, this makes it significantly harder for them to identify what websites you are visiting. It does this by carefully adding network noise and making all network packets the same size.',
- ),
- { daita: strings.daita, daitaFull: strings.daitaFull },
- )}
- </ModalMessage>
- </NavigationInfoButton>
</NavigationItems>
</NavigationBar>
<NavigationScrollbars>
<SettingsHeader>
<HeaderTitle>{strings.daita}</HeaderTitle>
- <HeaderSubTitle>
- {messages.pgettext(
- 'wireguard-settings-view',
- 'Hides patterns in your encrypted VPN traffic. Since this increases your total network traffic, be cautious if you have a limited data plan. It can also negatively impact your network speed and battery usage.',
- )}
- </HeaderSubTitle>
+ <PageSlider
+ content={[
+ <React.Fragment key="without-daita">
+ <StyledIllustration src="../../assets/images/daita-off-illustration.svg" />
+ <StyledHeaderSubTitle>
+ {sprintf(
+ messages.pgettext(
+ 'wireguard-settings-view',
+ '%(daita)s (%(daitaFull)s) hides patterns in your encrypted VPN traffic.',
+ ),
+ { daita: strings.daita, daitaFull: strings.daitaFull },
+ )}
+ </StyledHeaderSubTitle>
+ <StyledHeaderSubTitle>
+ {messages.pgettext(
+ 'wireguard-settings-view',
+ 'If anyone is monitoring your connection, this makes it significantly harder for them to identify what websites you are visiting.',
+ )}
+ </StyledHeaderSubTitle>
+ </React.Fragment>,
+ <React.Fragment key="with-daita">
+ <StyledIllustration src="../../assets/images/daita-on-illustration.svg" />
+ <StyledHeaderSubTitle>
+ {messages.pgettext(
+ 'wireguard-settings-view',
+ 'It does this by carefully adding network noise and making all network packets the same size.',
+ )}
+ </StyledHeaderSubTitle>
+ <StyledHeaderSubTitle>
+ {messages.pgettext(
+ 'wireguard-settings-view',
+ 'Can only be used with WireGuard. Since this increases your total network traffic, be cautious if you have a limited data plan. It can also negatively impact your network speed.',
+ )}
+ </StyledHeaderSubTitle>
+ </React.Fragment>,
+ ]}
+ />
</SettingsHeader>
<StyledContent>
@@ -84,6 +120,7 @@ export default function DaitaSettings() {
function DaitaToggle() {
const { setEnableDaita, setDaitaSmartRouting } = useAppContext();
+ const relaySettings = useSelector((state) => state.settings.relaySettings);
const daita = useSelector((state) => state.settings.wireguard.daita?.enabled ?? false);
const smartRouting = useSelector(
(state) => state.settings.wireguard.daita?.smartRouting ?? false,
@@ -91,6 +128,9 @@ function DaitaToggle() {
const [confirmationDialogVisible, showConfirmationDialog, hideConfirmationDialog] = useBoolean();
+ const unavailable =
+ 'normal' in relaySettings ? relaySettings.normal.tunnelProtocol === 'openvpn' : true;
+
const setDaita = useCallback((value: boolean) => {
void setEnableDaita(value);
}, []);
@@ -111,17 +151,24 @@ function DaitaToggle() {
return (
<>
<AriaInputGroup>
- <Cell.Container>
+ <Cell.Container disabled={unavailable}>
<AriaLabel>
<Cell.InputLabel>{messages.gettext('Enable')}</Cell.InputLabel>
</AriaLabel>
<AriaInput>
- <Cell.Switch isOn={daita} onChange={setDaita} />
+ <Cell.Switch isOn={daita && !unavailable} onChange={setDaita} />
</AriaInput>
</Cell.Container>
+ {unavailable ? (
+ <EnableFooter>
+ <AriaDescription>
+ <Cell.CellFooterText>{featureUnavailableMessage()}</Cell.CellFooterText>
+ </AriaDescription>
+ </EnableFooter>
+ ) : null}
</AriaInputGroup>
<AriaInputGroup>
- <Cell.Container disabled={!daita}>
+ <Cell.Container disabled={!daita || unavailable}>
<AriaLabel>
<Cell.InputLabel>{messages.gettext('Smart routing')}</Cell.InputLabel>
</AriaLabel>
@@ -129,7 +176,7 @@ function DaitaToggle() {
<SmartRoutingModalMessage />
</InfoButton>
<AriaInput>
- <Cell.Switch isOn={smartRouting} onChange={setSmartRouting} />
+ <Cell.Switch isOn={smartRouting && !unavailable} onChange={setSmartRouting} />
</AriaInput>
</Cell.Container>
<Cell.CellFooter>
@@ -138,7 +185,7 @@ function DaitaToggle() {
{sprintf(
messages.pgettext(
'vpn-settings-view',
- 'Is automatically enabled with %(daita)s, makes it possible to use %(daita)s with any server by using multihop. This might increase latency.',
+ 'Makes it possible to use %(daita)s with any server and is automatically enabled.',
),
{ daita: strings.daita },
)}
@@ -191,3 +238,16 @@ export function SmartRoutingModalMessage() {
</ModalMessage>
);
}
+
+function featureUnavailableMessage() {
+ const automatic = messages.gettext('Automatic');
+ const tunnelProtocol = messages.pgettext('vpn-settings-view', 'Tunnel protocol');
+
+ return sprintf(
+ messages.pgettext(
+ 'wireguard-settings-view',
+ 'Switch to “%(wireguard)s” or “%(automatic)s” in Settings > %(tunnelProtocol)s to make %(setting)s available.',
+ ),
+ { wireguard: strings.wireguard, automatic, tunnelProtocol, setting: strings.daita },
+ );
+}
diff --git a/gui/src/renderer/components/MultihopSettings.tsx b/gui/src/renderer/components/MultihopSettings.tsx
new file mode 100644
index 0000000000..3075e1d4ed
--- /dev/null
+++ b/gui/src/renderer/components/MultihopSettings.tsx
@@ -0,0 +1,133 @@
+import { useCallback } from 'react';
+import { sprintf } from 'sprintf-js';
+import styled from 'styled-components';
+
+import { strings } from '../../config.json';
+import { messages } from '../../shared/gettext';
+import log from '../../shared/logging';
+import { useRelaySettingsUpdater } from '../lib/constraint-updater';
+import { useHistory } from '../lib/history';
+import { useSelector } from '../redux/store';
+import { AriaDescription, AriaInput, AriaInputGroup, AriaLabel } from './AriaGroup';
+import * as Cell from './cell';
+import { StyledIllustration } from './DaitaSettings';
+import { BackAction } from './KeyboardNavigation';
+import { Layout, SettingsContainer } from './Layout';
+import {
+ NavigationBar,
+ NavigationContainer,
+ NavigationItems,
+ NavigationScrollbars,
+ TitleBarItem,
+} from './NavigationBar';
+import SettingsHeader, { HeaderSubTitle, HeaderTitle } from './SettingsHeader';
+
+const StyledContent = styled.div({
+ display: 'flex',
+ flexDirection: 'column',
+ flex: 1,
+ marginBottom: '2px',
+});
+
+export default function MultihopSettings() {
+ const { pop } = useHistory();
+
+ return (
+ <BackAction action={pop}>
+ <Layout>
+ <SettingsContainer>
+ <NavigationContainer>
+ <NavigationBar>
+ <NavigationItems>
+ <TitleBarItem>
+ {messages.pgettext('wireguard-settings-view', 'Multihop')}
+ </TitleBarItem>
+ </NavigationItems>
+ </NavigationBar>
+
+ <NavigationScrollbars>
+ <SettingsHeader>
+ <HeaderTitle>
+ {messages.pgettext('wireguard-settings-view', 'Multihop')}
+ </HeaderTitle>
+ <HeaderSubTitle>
+ <StyledIllustration src="../../assets/images/multihop-illustration.svg" />
+ {messages.pgettext(
+ 'wireguard-settings-view',
+ 'Multihop routes your traffic into one WireGuard server and out another, making it harder to trace. This results in increased latency but increases anonymity online.',
+ )}
+ </HeaderSubTitle>
+ </SettingsHeader>
+
+ <StyledContent>
+ <Cell.Group>
+ <MultihopSetting />
+ </Cell.Group>
+ </StyledContent>
+ </NavigationScrollbars>
+ </NavigationContainer>
+ </SettingsContainer>
+ </Layout>
+ </BackAction>
+ );
+}
+
+function MultihopSetting() {
+ const relaySettings = useSelector((state) => state.settings.relaySettings);
+ const relaySettingsUpdater = useRelaySettingsUpdater();
+
+ const multihop = 'normal' in relaySettings ? relaySettings.normal.wireguard.useMultihop : false;
+ const unavailable =
+ 'normal' in relaySettings ? relaySettings.normal.tunnelProtocol === 'openvpn' : true;
+
+ const setMultihop = useCallback(
+ async (enabled: boolean) => {
+ try {
+ await relaySettingsUpdater((settings) => {
+ settings.wireguardConstraints.useMultihop = enabled;
+ return settings;
+ });
+ } catch (e) {
+ const error = e as Error;
+ log.error('Failed to update WireGuard multihop settings', error.message);
+ }
+ },
+ [relaySettingsUpdater],
+ );
+
+ return (
+ <>
+ <AriaInputGroup>
+ <Cell.Container disabled={unavailable}>
+ <AriaLabel>
+ <Cell.InputLabel>{messages.gettext('Enable')}</Cell.InputLabel>
+ </AriaLabel>
+ <AriaInput>
+ <Cell.Switch isOn={multihop && !unavailable} onChange={setMultihop} />
+ </AriaInput>
+ </Cell.Container>
+ {unavailable ? (
+ <Cell.CellFooter>
+ <AriaDescription>
+ <Cell.CellFooterText>{featureUnavailableMessage()}</Cell.CellFooterText>
+ </AriaDescription>
+ </Cell.CellFooter>
+ ) : null}
+ </AriaInputGroup>
+ </>
+ );
+}
+
+function featureUnavailableMessage() {
+ const automatic = messages.gettext('Automatic');
+ const tunnelProtocol = messages.pgettext('vpn-settings-view', 'Tunnel protocol');
+ const multihop = messages.pgettext('wireguard-settings-view', 'Multihop');
+
+ return sprintf(
+ messages.pgettext(
+ 'wireguard-settings-view',
+ 'Switch to “%(wireguard)s” or “%(automatic)s” in Settings > %(tunnelProtocol)s to make %(setting)s available.',
+ ),
+ { wireguard: strings.wireguard, automatic, tunnelProtocol, setting: multihop },
+ );
+}
diff --git a/gui/src/renderer/components/PageSlider.tsx b/gui/src/renderer/components/PageSlider.tsx
new file mode 100644
index 0000000000..56d64954e7
--- /dev/null
+++ b/gui/src/renderer/components/PageSlider.tsx
@@ -0,0 +1,241 @@
+import { useCallback, useEffect, useState } from 'react';
+import styled from 'styled-components';
+
+import { colors } from '../../config.json';
+import { NonEmptyArray } from '../../shared/utils';
+import { useStyledRef } from '../lib/utilityHooks';
+import { Icon } from './cell';
+
+const PAGE_GAP = 16;
+
+const StyledPageSliderContainer = styled.div({
+ display: 'flex',
+ flexDirection: 'column',
+});
+
+const StyledPageSlider = styled.div({
+ whiteSpace: 'nowrap',
+ overflow: 'scroll hidden',
+ scrollSnapType: 'x mandatory',
+ scrollBehavior: 'smooth',
+
+ '&&::-webkit-scrollbar': {
+ display: 'none',
+ },
+});
+
+const StyledPage = styled.div({
+ display: 'inline-block',
+ width: '100%',
+ whiteSpace: 'normal',
+ verticalAlign: 'top',
+ scrollSnapAlign: 'start',
+
+ '&&:not(:last-child)': {
+ marginRight: `${PAGE_GAP}px`,
+ },
+});
+
+interface PageSliderProps {
+ content: NonEmptyArray<React.ReactNode>;
+}
+
+export default function PageSlider(props: PageSliderProps) {
+ // A state is needed to trigger a rerender. This is needed to update the "disabled" and "$current"
+ // props of the arrows and page indicators.
+ const [, setPageNumberState] = useState(0);
+ const pageContainerRef = useStyledRef<HTMLDivElement>();
+
+ // Calculate the page number based on the scroll position.
+ const getPageNumber = useCallback(() => {
+ if (pageContainerRef.current) {
+ const scrollLeft = pageContainerRef.current.scrollLeft;
+ const pageWidth = pageContainerRef.current.offsetWidth + PAGE_GAP;
+ // Clamp it between 0 and props.content.length-1 to make sure it will correspond to a page.
+ return Math.max(0, Math.min(Math.round(scrollLeft / pageWidth), props.content.length - 1));
+ } else {
+ return 0;
+ }
+ }, [pageContainerRef, props.content.length]);
+
+ // These values are only intended to be used for display purposes. Using them when calculating
+ // next or prev page would increase the risk of race conditions.
+ const pageNumber = getPageNumber();
+ const hasNext = pageNumber < props.content.length - 1;
+ const hasPrev = pageNumber > 0;
+
+ // Scroll to a specific page.
+ const goToPage = useCallback(
+ (page: number) => {
+ if (pageContainerRef.current) {
+ const width = pageContainerRef.current.offsetWidth;
+ pageContainerRef.current.scrollTo({ left: width * page });
+ }
+ },
+ [pageContainerRef],
+ );
+
+ const next = useCallback(() => goToPage(getPageNumber() + 1), [goToPage, getPageNumber]);
+ const prev = useCallback(() => goToPage(getPageNumber() - 1), [goToPage, getPageNumber]);
+
+ // Callback that navigates when left and right arrows are pressed.
+ const handleKeyDown = useCallback(
+ (event: KeyboardEvent) => {
+ if (event.key === 'ArrowLeft') {
+ prev();
+ } else if (event.key === 'ArrowRight') {
+ next();
+ }
+ },
+ [next, prev],
+ );
+
+ // Trigger a rerender when the page number has changed. This needs to be done to update the
+ // states of the arrows and page indicators.
+ const handleScroll = useCallback(() => setPageNumberState(getPageNumber()), []);
+
+ useEffect(() => {
+ document.addEventListener('keydown', handleKeyDown);
+ return () => document.removeEventListener('keydown', handleKeyDown);
+ }, [handleKeyDown]);
+
+ return (
+ <StyledPageSliderContainer>
+ <StyledPageSlider ref={pageContainerRef} onScroll={handleScroll}>
+ {props.content.map((page, i) => (
+ <StyledPage key={`page-${i}`}>{page}</StyledPage>
+ ))}
+ </StyledPageSlider>
+ <Controls
+ goToPage={goToPage}
+ hasNext={hasNext}
+ hasPrev={hasPrev}
+ next={next}
+ prev={prev}
+ pageNumber={pageNumber}
+ numberOfPages={props.content.length}
+ />
+ </StyledPageSliderContainer>
+ );
+}
+
+const StyledControlsContainer = styled.div({
+ display: 'flex',
+ marginTop: '12px',
+ alignItems: 'center',
+});
+
+const StyledControlElement = styled.div({
+ flex: '1 0 60px',
+ display: 'flex',
+});
+
+const StyledArrows = styled(StyledControlElement)({
+ display: 'flex',
+ justifyContent: 'right',
+ gap: '12px',
+});
+
+const StyledPageIndicators = styled(StyledControlElement)({
+ display: 'flex',
+ flexGrow: 2,
+ justifyContent: 'center',
+});
+
+const StyledTransparentButton = styled.button({
+ border: 'none',
+ background: 'transparent',
+ padding: '4px',
+ margin: 0,
+});
+
+const StyledPageIndicator = styled.div<{ $current: boolean }>((props) => ({
+ width: '8px',
+ height: '8px',
+ borderRadius: '50%',
+ backgroundColor: props.$current ? colors.white80 : colors.white40,
+
+ [`${StyledTransparentButton}:hover &&`]: {
+ backgroundColor: colors.white80,
+ },
+}));
+
+const StyledArrow = styled(Icon)((props) => ({
+ backgroundColor: props.disabled ? colors.white20 : props.tintColor,
+
+ [`${StyledTransparentButton}:hover &&`]: {
+ backgroundColor: props.disabled ? colors.white20 : props.tintHoverColor,
+ },
+}));
+
+const StyledLeftArrow = styled(StyledArrow)({
+ transform: 'scaleX(-100%)',
+});
+
+interface ControlsProps {
+ pageNumber: number;
+ numberOfPages: number;
+ hasNext: boolean;
+ hasPrev: boolean;
+ next: () => void;
+ prev: () => void;
+ goToPage: (page: number) => void;
+}
+
+function Controls(props: ControlsProps) {
+ return (
+ <StyledControlsContainer>
+ <StyledControlElement>{/* spacer to make page indicators centered */}</StyledControlElement>
+ <StyledPageIndicators>
+ {[...Array(props.numberOfPages)].map((_, i) => (
+ <PageIndicator
+ key={i}
+ current={i === props.pageNumber}
+ pageNumber={i}
+ goToPage={props.goToPage}
+ />
+ ))}
+ </StyledPageIndicators>
+ <StyledArrows>
+ <StyledTransparentButton onClick={props.prev}>
+ <StyledLeftArrow
+ disabled={!props.hasPrev}
+ height={12}
+ width={7}
+ source="icon-chevron"
+ tintColor={colors.white}
+ tintHoverColor={colors.white60}
+ />
+ </StyledTransparentButton>
+ <StyledTransparentButton onClick={props.next}>
+ <StyledArrow
+ disabled={!props.hasNext}
+ height={12}
+ width={7}
+ source="icon-chevron"
+ tintColor={colors.white}
+ tintHoverColor={colors.white60}
+ />
+ </StyledTransparentButton>
+ </StyledArrows>
+ </StyledControlsContainer>
+ );
+}
+
+interface PageIndicatorProps {
+ pageNumber: number;
+ goToPage: (page: number) => void;
+ current: boolean;
+}
+
+function PageIndicator(props: PageIndicatorProps) {
+ const onClick = useCallback(() => {
+ props.goToPage(props.pageNumber);
+ }, [props.goToPage, props.pageNumber]);
+
+ return (
+ <StyledTransparentButton onClick={onClick}>
+ <StyledPageIndicator $current={props.current} />
+ </StyledTransparentButton>
+ );
+}
diff --git a/gui/src/renderer/components/Settings.tsx b/gui/src/renderer/components/Settings.tsx
index 98d36fb9fd..d76ebb242e 100644
--- a/gui/src/renderer/components/Settings.tsx
+++ b/gui/src/renderer/components/Settings.tsx
@@ -70,6 +70,8 @@ export default function Support() {
<>
<Cell.Group>
<UserInterfaceSettingsButton />
+ <MultihopButton />
+ <DaitaButton />
<VpnSettingsButton />
</Cell.Group>
@@ -133,6 +135,42 @@ function UserInterfaceSettingsButton() {
);
}
+function MultihopButton() {
+ const history = useHistory();
+ const navigate = useCallback(() => history.push(RoutePath.multihopSettings), [history]);
+ const relaySettings = useSelector((state) => state.settings.relaySettings);
+ const multihop = 'normal' in relaySettings ? relaySettings.normal.wireguard.useMultihop : false;
+ const unavailable =
+ 'normal' in relaySettings ? relaySettings.normal.tunnelProtocol === 'openvpn' : true;
+
+ return (
+ <Cell.CellNavigationButton onClick={navigate}>
+ <Cell.Label>{messages.pgettext('settings-view', 'Multihop')}</Cell.Label>
+ <Cell.SubText>
+ {multihop && !unavailable ? messages.gettext('On') : messages.gettext('Off')}
+ </Cell.SubText>
+ </Cell.CellNavigationButton>
+ );
+}
+
+function DaitaButton() {
+ const history = useHistory();
+ const navigate = useCallback(() => history.push(RoutePath.daitaSettings), [history]);
+ const daita = useSelector((state) => state.settings.wireguard.daita?.enabled ?? false);
+ const relaySettings = useSelector((state) => state.settings.relaySettings);
+ const unavailable =
+ 'normal' in relaySettings ? relaySettings.normal.tunnelProtocol === 'openvpn' : true;
+
+ return (
+ <Cell.CellNavigationButton onClick={navigate}>
+ <Cell.Label>{strings.daita}</Cell.Label>
+ <Cell.SubText>
+ {daita && !unavailable ? messages.gettext('On') : messages.gettext('Off')}
+ </Cell.SubText>
+ </Cell.CellNavigationButton>
+ );
+}
+
function VpnSettingsButton() {
const history = useHistory();
const navigate = useCallback(() => history.push(RoutePath.vpnSettings), [history]);
diff --git a/gui/src/renderer/components/WireguardSettings.tsx b/gui/src/renderer/components/WireguardSettings.tsx
index 8f2329d77f..9a79111ad7 100644
--- a/gui/src/renderer/components/WireguardSettings.tsx
+++ b/gui/src/renderer/components/WireguardSettings.tsx
@@ -16,15 +16,13 @@ import { useAppContext } from '../context';
import { useRelaySettingsUpdater } from '../lib/constraint-updater';
import { useHistory } from '../lib/history';
import { RoutePath } from '../lib/routes';
-import { useBoolean } from '../lib/utilityHooks';
import { useSelector } from '../redux/store';
-import * as AppButton from './AppButton';
import { AriaDescription, AriaInput, AriaInputGroup, AriaLabel } from './AriaGroup';
import * as Cell from './cell';
import Selector, { SelectorItem, SelectorWithCustomItem } from './cell/Selector';
import { BackAction } from './KeyboardNavigation';
import { Layout, SettingsContainer } from './Layout';
-import { ModalAlert, ModalAlertType, ModalMessage } from './Modal';
+import { ModalMessage } from './Modal';
import {
NavigationBar,
NavigationContainer,
@@ -97,18 +95,10 @@ export default function WireguardSettings() {
</Cell.Group>
<Cell.Group>
- <DaitaButton />
- </Cell.Group>
-
- <Cell.Group>
<QuantumResistantSetting />
</Cell.Group>
<Cell.Group>
- <MultihopSetting />
- </Cell.Group>
-
- <Cell.Group>
<IpVersionSetting />
</Cell.Group>
@@ -286,99 +276,6 @@ function formatPortForSubLabel(port: Constraint<number>): string {
return port === 'any' ? messages.gettext('Automatic') : `${port.only}`;
}
-function MultihopSetting() {
- const relaySettings = useSelector((state) => state.settings.relaySettings);
- const relaySettingsUpdater = useRelaySettingsUpdater();
-
- const multihop = 'normal' in relaySettings ? relaySettings.normal.wireguard.useMultihop : false;
-
- const [confirmationDialogVisible, showConfirmationDialog, hideConfirmationDialog] = useBoolean();
-
- const setMultihopImpl = useCallback(
- async (enabled: boolean) => {
- try {
- await relaySettingsUpdater((settings) => {
- settings.wireguardConstraints.useMultihop = enabled;
- return settings;
- });
- } catch (e) {
- const error = e as Error;
- log.error('Failed to update WireGuard multihop settings', error.message);
- }
- },
- [relaySettingsUpdater],
- );
-
- const setMultihop = useCallback(
- async (newValue: boolean) => {
- if (newValue) {
- showConfirmationDialog();
- } else {
- await setMultihopImpl(false);
- }
- },
- [setMultihopImpl],
- );
-
- const confirmMultihop = useCallback(async () => {
- await setMultihopImpl(true);
- hideConfirmationDialog();
- }, [setMultihopImpl]);
-
- return (
- <>
- <AriaInputGroup>
- <Cell.Container>
- <AriaLabel>
- <Cell.InputLabel>
- {
- // TRANSLATORS: The label next to the multihop settings toggle.
- messages.pgettext('vpn-settings-view', 'Enable multihop')
- }
- </Cell.InputLabel>
- </AriaLabel>
- <AriaInput>
- <Cell.Switch isOn={multihop} onChange={setMultihop} />
- </AriaInput>
- </Cell.Container>
- <Cell.CellFooter>
- <AriaDescription>
- <Cell.CellFooterText>
- {sprintf(
- // TRANSLATORS: Description for multihop settings toggle.
- // TRANSLATORS: Available placeholders:
- // TRANSLATORS: %(wireguard)s - Will be replaced with the string "WireGuard"
- messages.pgettext(
- 'vpn-settings-view',
- 'Increases anonymity by routing your traffic into one %(wireguard)s server and out another, making it harder to trace.',
- ),
- { wireguard: strings.wireguard },
- )}
- </Cell.CellFooterText>
- </AriaDescription>
- </Cell.CellFooter>
- </AriaInputGroup>
- <ModalAlert
- isOpen={confirmationDialogVisible}
- type={ModalAlertType.caution}
- message={
- // TRANSLATORS: Warning text in a dialog that is displayed after a setting is toggled.
- messages.gettext('This setting increases latency. Use only if needed.')
- }
- buttons={[
- <AppButton.RedButton key="confirm" onClick={confirmMultihop}>
- {messages.gettext('Enable anyway')}
- </AppButton.RedButton>,
- <AppButton.BlueButton key="back" onClick={hideConfirmationDialog}>
- {messages.gettext('Back')}
- </AppButton.BlueButton>,
- ]}
- close={hideConfirmationDialog}
- />
- </>
- );
-}
-
function IpVersionSetting() {
const relaySettingsUpdater = useRelaySettingsUpdater();
const relaySettings = useSelector((state) => state.settings.relaySettings);
@@ -527,19 +424,6 @@ function MtuSetting() {
);
}
-function DaitaButton() {
- const history = useHistory();
- const navigate = useCallback(() => history.push(RoutePath.daitaSettings), [history]);
- const daita = useSelector((state) => state.settings.wireguard.daita?.enabled ?? false);
-
- return (
- <Cell.CellNavigationButton onClick={navigate}>
- <Cell.Label>{strings.daita}</Cell.Label>
- <Cell.SubText>{daita ? messages.gettext('On') : messages.gettext('Off')}</Cell.SubText>
- </Cell.CellNavigationButton>
- );
-}
-
function QuantumResistantSetting() {
const { setWireguardQuantumResistant } = useAppContext();
const quantumResistant = useSelector((state) => state.settings.wireguard.quantumResistant);
diff --git a/gui/src/renderer/lib/routes.ts b/gui/src/renderer/lib/routes.ts
index 5204dc666c..89b50c1fb0 100644
--- a/gui/src/renderer/lib/routes.ts
+++ b/gui/src/renderer/lib/routes.ts
@@ -13,9 +13,10 @@ export enum RoutePath {
selectLanguage = '/settings/language',
account = '/account',
userInterfaceSettings = '/settings/interface',
+ multihopSettings = '/settings/multihop',
vpnSettings = '/settings/vpn',
wireguardSettings = '/settings/advanced/wireguard',
- daitaSettings = '/settings/advanced/wireguard/daita',
+ daitaSettings = '/settings/daita',
udpOverTcp = '/settings/advanced/wireguard/udp-over-tcp',
shadowsocks = '/settings/advanced/shadowsocks',
openVpnSettings = '/settings/advanced/openvpn',
diff --git a/gui/src/shared/utils.ts b/gui/src/shared/utils.ts
index 24984e4412..042c56385a 100644
--- a/gui/src/shared/utils.ts
+++ b/gui/src/shared/utils.ts
@@ -1,3 +1,5 @@
+export type NonEmptyArray<T> = [T, ...T[]];
+
export function hasValue<T>(value: T): value is NonNullable<T> {
return value !== undefined && value !== null;
}