From 2e6ed4dbc6fa5a5aae6d57dbfedd901f83679f75 Mon Sep 17 00:00:00 2001 From: Oskar Date: Mon, 23 Sep 2024 17:53:41 +0200 Subject: [PATCH 01/10] Move multihop to top level settings along with a dedicated view --- gui/src/renderer/components/AppRouter.tsx | 2 + .../renderer/components/MultihopSettings.tsx | 169 ++++++++++++++++++ gui/src/renderer/components/Settings.tsx | 19 ++ .../renderer/components/WireguardSettings.tsx | 101 +---------- gui/src/renderer/lib/routes.ts | 1 + 5 files changed, 192 insertions(+), 100 deletions(-) create mode 100644 gui/src/renderer/components/MultihopSettings.tsx diff --git a/gui/src/renderer/components/AppRouter.tsx b/gui/src/renderer/components/AppRouter.tsx index 75b8df936c3b..e1a3d2ab0ffc 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() { + diff --git a/gui/src/renderer/components/MultihopSettings.tsx b/gui/src/renderer/components/MultihopSettings.tsx new file mode 100644 index 000000000000..b2290b71d5b3 --- /dev/null +++ b/gui/src/renderer/components/MultihopSettings.tsx @@ -0,0 +1,169 @@ +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 { 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 { BackAction } from './KeyboardNavigation'; +import { Layout, SettingsContainer } from './Layout'; +import { ModalAlert, ModalAlertType } from './Modal'; +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 ( + + + + + + + + {messages.pgettext('wireguard-settings-view', 'Multihop')} + + + + + + + + {messages.pgettext('wireguard-settings-view', 'Multihop')} + + + {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.', + )} + + + + + + + + + + + + + + ); +} + +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 [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 ( + <> + + + + {messages.gettext('Enable')} + + + + + + {unavailable ? ( + + + {featureUnavailableMessage()} + + + ) : null} + + + {messages.gettext('Enable anyway')} + , + + {messages.gettext('Back')} + , + ]} + close={hideConfirmationDialog} + /> + + ); +} + +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/Settings.tsx b/gui/src/renderer/components/Settings.tsx index 98d36fb9fd69..2280998d3ab0 100644 --- a/gui/src/renderer/components/Settings.tsx +++ b/gui/src/renderer/components/Settings.tsx @@ -70,6 +70,7 @@ export default function Support() { <> + @@ -133,6 +134,24 @@ 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 ( + + {messages.pgettext('settings-view', 'Multihop')} + + {multihop && !unavailable ? messages.gettext('On') : messages.gettext('Off')} + + + ); +} + 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 8f2329d77f05..51817c2029c3 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, @@ -104,10 +102,6 @@ export default function WireguardSettings() { - - - - @@ -286,99 +280,6 @@ function formatPortForSubLabel(port: Constraint): 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 ( - <> - - - - - { - // TRANSLATORS: The label next to the multihop settings toggle. - messages.pgettext('vpn-settings-view', 'Enable multihop') - } - - - - - - - - - - {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 }, - )} - - - - - - {messages.gettext('Enable anyway')} - , - - {messages.gettext('Back')} - , - ]} - close={hideConfirmationDialog} - /> - - ); -} - function IpVersionSetting() { const relaySettingsUpdater = useRelaySettingsUpdater(); const relaySettings = useSelector((state) => state.settings.relaySettings); diff --git a/gui/src/renderer/lib/routes.ts b/gui/src/renderer/lib/routes.ts index 5204dc666c61..68e8357b6efd 100644 --- a/gui/src/renderer/lib/routes.ts +++ b/gui/src/renderer/lib/routes.ts @@ -13,6 +13,7 @@ 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', From 591133981646ae720813a762e42cda4d8ee1411e Mon Sep 17 00:00:00 2001 From: Oskar Date: Mon, 23 Sep 2024 17:57:19 +0200 Subject: [PATCH 02/10] Remove multihop confirmation dialog --- .../renderer/components/MultihopSettings.tsx | 40 +------------------ 1 file changed, 1 insertion(+), 39 deletions(-) diff --git a/gui/src/renderer/components/MultihopSettings.tsx b/gui/src/renderer/components/MultihopSettings.tsx index b2290b71d5b3..61ee65c8f888 100644 --- a/gui/src/renderer/components/MultihopSettings.tsx +++ b/gui/src/renderer/components/MultihopSettings.tsx @@ -7,14 +7,11 @@ import { messages } from '../../shared/gettext'; import log from '../../shared/logging'; import { useRelaySettingsUpdater } from '../lib/constraint-updater'; import { useHistory } from '../lib/history'; -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 { BackAction } from './KeyboardNavigation'; import { Layout, SettingsContainer } from './Layout'; -import { ModalAlert, ModalAlertType } from './Modal'; import { NavigationBar, NavigationContainer, @@ -81,9 +78,7 @@ function MultihopSetting() { const unavailable = 'normal' in relaySettings ? relaySettings.normal.tunnelProtocol === 'openvpn' : true; - const [confirmationDialogVisible, showConfirmationDialog, hideConfirmationDialog] = useBoolean(); - - const setMultihopImpl = useCallback( + const setMultihop = useCallback( async (enabled: boolean) => { try { await relaySettingsUpdater((settings) => { @@ -98,22 +93,6 @@ function MultihopSetting() { [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 ( <> @@ -133,23 +112,6 @@ function MultihopSetting() { ) : null} - - {messages.gettext('Enable anyway')} - , - - {messages.gettext('Back')} - , - ]} - close={hideConfirmationDialog} - /> ); } From 52153bfa2cfb9bd902ab09822f29902e687d0d5c Mon Sep 17 00:00:00 2001 From: Oskar Date: Mon, 23 Sep 2024 17:58:40 +0200 Subject: [PATCH 03/10] Move DAITA to top level settings --- gui/src/renderer/components/DaitaSettings.tsx | 22 ++++++++++++++++++- gui/src/renderer/components/Settings.tsx | 19 ++++++++++++++++ .../renderer/components/WireguardSettings.tsx | 17 -------------- gui/src/renderer/lib/routes.ts | 2 +- 4 files changed, 41 insertions(+), 19 deletions(-) diff --git a/gui/src/renderer/components/DaitaSettings.tsx b/gui/src/renderer/components/DaitaSettings.tsx index 3176192ad10d..31d9a747e7ef 100644 --- a/gui/src/renderer/components/DaitaSettings.tsx +++ b/gui/src/renderer/components/DaitaSettings.tsx @@ -119,6 +119,13 @@ function DaitaToggle() { + {unavailable ? ( + + + {featureUnavailableMessage()} + + + ) : null} @@ -138,7 +145,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 +198,16 @@ export function SmartRoutingModalMessage() { ); } + +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/Settings.tsx b/gui/src/renderer/components/Settings.tsx index 2280998d3ab0..d76ebb242e31 100644 --- a/gui/src/renderer/components/Settings.tsx +++ b/gui/src/renderer/components/Settings.tsx @@ -71,6 +71,7 @@ export default function Support() { + @@ -152,6 +153,24 @@ function MultihopButton() { ); } +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 ( + + {strings.daita} + + {daita && !unavailable ? messages.gettext('On') : messages.gettext('Off')} + + + ); +} + 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 51817c2029c3..9a79111ad7cb 100644 --- a/gui/src/renderer/components/WireguardSettings.tsx +++ b/gui/src/renderer/components/WireguardSettings.tsx @@ -94,10 +94,6 @@ export default function WireguardSettings() { - - - - @@ -428,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 ( - - {strings.daita} - {daita ? messages.gettext('On') : messages.gettext('Off')} - - ); -} - 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 68e8357b6efd..89b50c1fb0e0 100644 --- a/gui/src/renderer/lib/routes.ts +++ b/gui/src/renderer/lib/routes.ts @@ -16,7 +16,7 @@ export enum RoutePath { 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', From 29956fce815711e2fedba8cdbbd982704f21ee93 Mon Sep 17 00:00:00 2001 From: Oskar Date: Mon, 23 Sep 2024 20:01:03 +0200 Subject: [PATCH 04/10] Add PageSlider component --- gui/src/renderer/components/PageSlider.tsx | 221 +++++++++++++++++++++ gui/src/shared/utils.ts | 2 + 2 files changed, 223 insertions(+) create mode 100644 gui/src/renderer/components/PageSlider.tsx diff --git a/gui/src/renderer/components/PageSlider.tsx b/gui/src/renderer/components/PageSlider.tsx new file mode 100644 index 000000000000..c6603664fb5c --- /dev/null +++ b/gui/src/renderer/components/PageSlider.tsx @@ -0,0 +1,221 @@ +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'; + +// The amount of scroll required to switch page. This is compared with the `deltaX` value on the +// onWheel event. +const WHEEL_DELTA_THRESHOLD = 30; + +const StyledPageSliderContainer = styled.div({ + display: 'flex', + flexDirection: 'column', +}); + +const StyledPageSlider = styled.div({ + whiteSpace: 'nowrap', + overflow: 'hidden', +}); + +const StyledPage = styled.div({ + display: 'inline-block', + width: '100%', + whiteSpace: 'normal', + verticalAlign: 'top', +}); + +interface PageSliderProps { + content: NonEmptyArray; +} + +export default function PageSlider(props: PageSliderProps) { + const [page, setPage] = useState(0); + const pageContainerRef = useStyledRef(); + + const hasNext = page < props.content.length - 1; + const hasPrev = page > 0; + + const next = useCallback(() => { + setPage((page) => Math.min(props.content.length - 1, page + 1)); + }, [props.content.length]); + + const prev = useCallback(() => { + setPage((page) => Math.max(0, page - 1)); + }, []); + + // Go to next or previous page if the user scrolls horizontally. + const onWheel = useCallback( + (event: React.WheelEvent) => { + if (event.deltaX > WHEEL_DELTA_THRESHOLD) { + next(); + } else if (event.deltaX < -WHEEL_DELTA_THRESHOLD) { + prev(); + } + }, + [next, prev], + ); + + // Scroll to the correct position when the page prop changes. + useEffect(() => { + if (pageContainerRef.current) { + // The page width is the same as the container width. + const width = pageContainerRef.current.offsetWidth; + pageContainerRef.current.scrollTo({ left: width * page, behavior: 'smooth' }); + } + }, [page]); + + // 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], + ); + + useEffect(() => { + document.addEventListener('keydown', handleKeyDown); + return () => document.removeEventListener('keydown', handleKeyDown); + }, [handleKeyDown]); + + return ( + + + {props.content.map((page, i) => ( + {page} + ))} + + + + ); +} + +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 { + page: number; + numberOfPages: number; + hasNext: boolean; + hasPrev: boolean; + next: () => void; + prev: () => void; + goToPage: (page: number) => void; +} + +function Controls(props: ControlsProps) { + return ( + + {/* spacer to make page indicators centered */} + + {[...Array(props.numberOfPages)].map((_, i) => ( + + ))} + + + + + + + + + + + ); +} + +interface PageIndicatorProps { + page: number; + goToPage: (page: number) => void; + current: boolean; +} + +function PageIndicator(props: PageIndicatorProps) { + const onClick = useCallback(() => { + props.goToPage(props.page); + }, [props.goToPage, props.page]); + + return ( + + + + ); +} diff --git a/gui/src/shared/utils.ts b/gui/src/shared/utils.ts index 24984e441215..042c56385af5 100644 --- a/gui/src/shared/utils.ts +++ b/gui/src/shared/utils.ts @@ -1,3 +1,5 @@ +export type NonEmptyArray = [T, ...T[]]; + export function hasValue(value: T): value is NonNullable { return value !== undefined && value !== null; } From 6099621064d8acf562b9b8072e0403eae8be6614 Mon Sep 17 00:00:00 2001 From: Oskar Date: Mon, 23 Sep 2024 20:02:12 +0200 Subject: [PATCH 05/10] Add PageSlider subtitle to daita settings view --- gui/src/renderer/components/DaitaSettings.tsx | 76 +++++++++++++------ 1 file changed, 51 insertions(+), 25 deletions(-) diff --git a/gui/src/renderer/components/DaitaSettings.tsx b/gui/src/renderer/components/DaitaSettings.tsx index 31d9a747e7ef..abd0e8eafcf0 100644 --- a/gui/src/renderer/components/DaitaSettings.tsx +++ b/gui/src/renderer/components/DaitaSettings.tsx @@ -1,4 +1,5 @@ import { useCallback } from 'react'; +import React from 'react'; import { sprintf } from 'sprintf-js'; import styled from 'styled-components'; @@ -17,11 +18,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 +33,18 @@ 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 default function DaitaSettings() { const { pop } = useHistory(); @@ -43,30 +56,39 @@ export default function DaitaSettings() { {strings.daita} - - - - {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 }, - )} - - {strings.daita} - - {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.', - )} - + + {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.', + ), + { daita: strings.daita, daitaFull: strings.daitaFull }, + )} + , + + + {messages.pgettext( + 'wireguard-settings-view', + 'It does this by carefully adding network noise and making all network packets the same size.', + )} + + + {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.', + )} + + , + ]} + /> @@ -84,6 +106,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 +114,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,24 +137,24 @@ function DaitaToggle() { return ( <> - + {messages.gettext('Enable')} - + {unavailable ? ( - + {featureUnavailableMessage()} - + ) : null} - + {messages.gettext('Smart routing')} @@ -136,7 +162,7 @@ function DaitaToggle() { - + From 8a8d81ac9307a960dc0a7fcbd979e853d834a43f Mon Sep 17 00:00:00 2001 From: Oskar Date: Tue, 24 Sep 2024 10:27:28 +0200 Subject: [PATCH 06/10] Add DAITA illustrations --- gui/assets/images/daita-off-illustration.svg | 83 ++++++++++++++++ gui/assets/images/daita-on-illustration.svg | 98 +++++++++++++++++++ gui/src/renderer/components/DaitaSettings.tsx | 34 +++++-- 3 files changed, 205 insertions(+), 10 deletions(-) create mode 100644 gui/assets/images/daita-off-illustration.svg create mode 100644 gui/assets/images/daita-on-illustration.svg diff --git a/gui/assets/images/daita-off-illustration.svg b/gui/assets/images/daita-off-illustration.svg new file mode 100644 index 000000000000..a4686cdf5936 --- /dev/null +++ b/gui/assets/images/daita-off-illustration.svg @@ -0,0 +1,83 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gui/assets/images/daita-on-illustration.svg b/gui/assets/images/daita-on-illustration.svg new file mode 100644 index 000000000000..4321d00cc81f --- /dev/null +++ b/gui/assets/images/daita-on-illustration.svg @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gui/src/renderer/components/DaitaSettings.tsx b/gui/src/renderer/components/DaitaSettings.tsx index abd0e8eafcf0..46f251bf9ecc 100644 --- a/gui/src/renderer/components/DaitaSettings.tsx +++ b/gui/src/renderer/components/DaitaSettings.tsx @@ -1,5 +1,4 @@ -import { useCallback } from 'react'; -import React from 'react'; +import React, { useCallback } from 'react'; import { sprintf } from 'sprintf-js'; import styled from 'styled-components'; @@ -45,6 +44,11 @@ const EnableFooter = styled(Cell.CellFooter)({ paddingBottom: '16px', }); +const StyledIllustration = styled.img({ + width: '100%', + padding: '8px 0 8px', +}); + export default function DaitaSettings() { const { pop } = useHistory(); @@ -64,16 +68,26 @@ export default function DaitaSettings() { {strings.daita} - {sprintf( - messages.pgettext( + + + + {sprintf( + messages.pgettext( + 'wireguard-settings-view', + '%(daita)s (%(daitaFull)s) hides patterns in your encrypted VPN traffic.', + ), + { daita: strings.daita, daitaFull: strings.daitaFull }, + )} + + + {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.', - ), - { daita: strings.daita, daitaFull: strings.daitaFull }, - )} - , + 'If anyone is monitoring your connection, this makes it significantly harder for them to identify what websites you are visiting.', + )} + + , + {messages.pgettext( 'wireguard-settings-view', From 11d3c8c3b4873511cac7494f2e937c4e5055f631 Mon Sep 17 00:00:00 2001 From: Oskar Date: Wed, 25 Sep 2024 09:23:55 +0200 Subject: [PATCH 07/10] Add Multihop illustration --- gui/assets/images/multihop-illustration.svg | 169 ++++++++++++++++++ gui/src/renderer/components/DaitaSettings.tsx | 2 +- .../renderer/components/MultihopSettings.tsx | 2 + 3 files changed, 172 insertions(+), 1 deletion(-) create mode 100644 gui/assets/images/multihop-illustration.svg diff --git a/gui/assets/images/multihop-illustration.svg b/gui/assets/images/multihop-illustration.svg new file mode 100644 index 000000000000..4f6909ea380b --- /dev/null +++ b/gui/assets/images/multihop-illustration.svg @@ -0,0 +1,169 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gui/src/renderer/components/DaitaSettings.tsx b/gui/src/renderer/components/DaitaSettings.tsx index 46f251bf9ecc..b4ee10c94df2 100644 --- a/gui/src/renderer/components/DaitaSettings.tsx +++ b/gui/src/renderer/components/DaitaSettings.tsx @@ -44,7 +44,7 @@ const EnableFooter = styled(Cell.CellFooter)({ paddingBottom: '16px', }); -const StyledIllustration = styled.img({ +export const StyledIllustration = styled.img({ width: '100%', padding: '8px 0 8px', }); diff --git a/gui/src/renderer/components/MultihopSettings.tsx b/gui/src/renderer/components/MultihopSettings.tsx index 61ee65c8f888..3075e1d4ed1b 100644 --- a/gui/src/renderer/components/MultihopSettings.tsx +++ b/gui/src/renderer/components/MultihopSettings.tsx @@ -10,6 +10,7 @@ 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 { @@ -50,6 +51,7 @@ export default function MultihopSettings() { {messages.pgettext('wireguard-settings-view', 'Multihop')} + {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.', From d65d6c01d285aebda3f6607367b8548fc3c19e64 Mon Sep 17 00:00:00 2001 From: Oskar Date: Tue, 24 Sep 2024 15:45:23 +0200 Subject: [PATCH 08/10] Update translations --- gui/locales/messages.pot | 52 +++++++++++++++++++++++++--------------- 1 file changed, 33 insertions(+), 19 deletions(-) diff --git a/gui/locales/messages.pot b/gui/locales/messages.pot index aac7d2de6faa..43a4ebfb811c 100644 --- a/gui/locales/messages.pot +++ b/gui/locales/messages.pot @@ -267,7 +267,6 @@ msgstr "" #. Warning shown in dialog to users when they enable setting that increases #. network latency (decreases performance). -#. Warning text in a dialog that is displayed after a setting is toggled. msgid "This setting increases latency. Use only if needed." msgstr "" @@ -1539,6 +1538,10 @@ msgctxt "settings-view" msgid "Enabling this setting allows traffic to specific Apple-owned networks to go outside of the VPN tunnel, allowing services like iMessage and FaceTime to work whilst using Mullvad." msgstr "" +msgctxt "settings-view" +msgid "Multihop" +msgstr "" + msgctxt "settings-view" msgid "Some Apple services have an issue where the network settings set by Mullvad get ignored, this in turn blocks certain apps." msgstr "" @@ -1936,11 +1939,6 @@ msgctxt "vpn-settings-view" msgid "Enable IPv6" msgstr "" -#. The label next to the multihop settings toggle. -msgctxt "vpn-settings-view" -msgid "Enable multihop" -msgstr "" - msgctxt "vpn-settings-view" msgid "Enable to add at least one DNS server." msgstr "" @@ -1954,21 +1952,10 @@ msgctxt "vpn-settings-view" msgid "Gambling" msgstr "" -#. Description for multihop settings toggle. -#. Available placeholders: -#. %(wireguard)s - Will be replaced with the string "WireGuard" -msgctxt "vpn-settings-view" -msgid "Increases anonymity by routing your traffic into one %(wireguard)s server and out another, making it harder to trace." -msgstr "" - msgctxt "vpn-settings-view" msgid "IPv4 is always enabled and the majority of websites and applications use this protocol. We do not recommend enabling IPv6 unless you know you need it." msgstr "" -msgctxt "vpn-settings-view" -msgid "Is automatically enabled with %(daita)s, makes it possible to use %(daita)s with any server by using multihop. This might increase latency." -msgstr "" - msgctxt "vpn-settings-view" msgid "It does this by allowing network communication outside the tunnel to local multicast and broadcast ranges as well as to and from these private IP ranges:" msgstr "" @@ -1989,6 +1976,10 @@ msgctxt "vpn-settings-view" msgid "Lockdown mode" msgstr "" +msgctxt "vpn-settings-view" +msgid "Makes it possible to use %(daita)s with any server and is automatically enabled." +msgstr "" + #. Label for settings that enables malware blocking. msgctxt "vpn-settings-view" msgid "Malware" @@ -2090,7 +2081,7 @@ msgid "UDP-over-TCP" msgstr "" msgctxt "wireguard-settings-view" -msgid "%(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." +msgid "%(daita)s (%(daitaFull)s) hides patterns in your encrypted VPN traffic." msgstr "" #. Available placeholders: @@ -2100,13 +2091,21 @@ msgid "%(wireguard)s settings" msgstr "" msgctxt "wireguard-settings-view" -msgid "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." +msgid "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." +msgstr "" + +msgctxt "wireguard-settings-view" +msgid "If anyone is monitoring your connection, this makes it significantly harder for them to identify what websites you are visiting." msgstr "" msgctxt "wireguard-settings-view" msgid "IP version" msgstr "" +msgctxt "wireguard-settings-view" +msgid "It does this by carefully adding network noise and making all network packets the same size." +msgstr "" + msgctxt "wireguard-settings-view" msgid "It does this by performing an extra key exchange using a quantum safe algorithm and mixing the result into WireGuard’s regular encryption. This extra step uses approximately 500 kiB of traffic every time a new tunnel is established." msgstr "" @@ -2115,6 +2114,14 @@ msgctxt "wireguard-settings-view" msgid "MTU" msgstr "" +msgctxt "wireguard-settings-view" +msgid "Multihop" +msgstr "" + +msgctxt "wireguard-settings-view" +msgid "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." +msgstr "" + #. Warning text in a dialog that is displayed after a setting is toggled. msgctxt "wireguard-settings-view" msgid "Not all our servers are %(daita)s-enabled. In order to use the internet, you might have to select a new location after disabling, or you can continue using %(daita)s with Smart routing." @@ -2163,6 +2170,10 @@ msgctxt "wireguard-settings-view" msgid "Shadowsocks" msgstr "" +msgctxt "wireguard-settings-view" +msgid "Switch to “%(wireguard)s” or “%(automatic)s” in Settings > %(tunnelProtocol)s to make %(setting)s available." +msgstr "" + msgctxt "wireguard-settings-view" msgid "The automatic setting will randomly choose from the valid port ranges shown below." msgstr "" @@ -2203,6 +2214,9 @@ msgctxt "wireguard-settings-view" msgid "Which TCP port the UDP-over-TCP obfuscation protocol should connect to on the VPN server." msgstr "" +msgid "%s (%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." +msgstr "" + msgid "%s (added)" msgstr "" From 480dc444e06efe98ba41a7cb89c9bff7b1ba4bd2 Mon Sep 17 00:00:00 2001 From: Oskar Date: Wed, 25 Sep 2024 14:22:08 +0200 Subject: [PATCH 09/10] Update changelog for bring core privacy features forward --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8fda44be75df..49dd7a65f7f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,10 @@ Line wrap the file at 100 chars. Th #### Windows - Add experimental support for Windows ARM64. +### Changed +- Move DAITA and multihop to the root settings view along with moving multihop into a dedicated + view with more information. + ## [2024.6-beta1] - 2024-09-26 ### Added From e15e49ca37c375b136b370210d8cac612a980915 Mon Sep 17 00:00:00 2001 From: Oskar Date: Wed, 25 Sep 2024 17:45:51 +0200 Subject: [PATCH 10/10] Switch to using real scroll in the PageSlider component --- gui/src/renderer/components/PageSlider.tsx | 100 ++++++++++++--------- 1 file changed, 60 insertions(+), 40 deletions(-) diff --git a/gui/src/renderer/components/PageSlider.tsx b/gui/src/renderer/components/PageSlider.tsx index c6603664fb5c..56d64954e79e 100644 --- a/gui/src/renderer/components/PageSlider.tsx +++ b/gui/src/renderer/components/PageSlider.tsx @@ -6,9 +6,7 @@ import { NonEmptyArray } from '../../shared/utils'; import { useStyledRef } from '../lib/utilityHooks'; import { Icon } from './cell'; -// The amount of scroll required to switch page. This is compared with the `deltaX` value on the -// onWheel event. -const WHEEL_DELTA_THRESHOLD = 30; +const PAGE_GAP = 16; const StyledPageSliderContainer = styled.div({ display: 'flex', @@ -17,7 +15,13 @@ const StyledPageSliderContainer = styled.div({ const StyledPageSlider = styled.div({ whiteSpace: 'nowrap', - overflow: 'hidden', + overflow: 'scroll hidden', + scrollSnapType: 'x mandatory', + scrollBehavior: 'smooth', + + '&&::-webkit-scrollbar': { + display: 'none', + }, }); const StyledPage = styled.div({ @@ -25,6 +29,11 @@ const StyledPage = styled.div({ width: '100%', whiteSpace: 'normal', verticalAlign: 'top', + scrollSnapAlign: 'start', + + '&&:not(:last-child)': { + marginRight: `${PAGE_GAP}px`, + }, }); interface PageSliderProps { @@ -32,40 +41,42 @@ interface PageSliderProps { } export default function PageSlider(props: PageSliderProps) { - const [page, setPage] = useState(0); + // 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(); - const hasNext = page < props.content.length - 1; - const hasPrev = page > 0; - - const next = useCallback(() => { - setPage((page) => Math.min(props.content.length - 1, page + 1)); - }, [props.content.length]); - - const prev = useCallback(() => { - setPage((page) => Math.max(0, page - 1)); - }, []); - - // Go to next or previous page if the user scrolls horizontally. - const onWheel = useCallback( - (event: React.WheelEvent) => { - if (event.deltaX > WHEEL_DELTA_THRESHOLD) { - next(); - } else if (event.deltaX < -WHEEL_DELTA_THRESHOLD) { - prev(); + // 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 }); } }, - [next, prev], + [pageContainerRef], ); - // Scroll to the correct position when the page prop changes. - useEffect(() => { - if (pageContainerRef.current) { - // The page width is the same as the container width. - const width = pageContainerRef.current.offsetWidth; - pageContainerRef.current.scrollTo({ left: width * page, behavior: 'smooth' }); - } - }, [page]); + 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( @@ -79,6 +90,10 @@ export default function PageSlider(props: PageSliderProps) { [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); @@ -86,18 +101,18 @@ export default function PageSlider(props: PageSliderProps) { return ( - + {props.content.map((page, i) => ( {page} ))} @@ -158,7 +173,7 @@ const StyledLeftArrow = styled(StyledArrow)({ }); interface ControlsProps { - page: number; + pageNumber: number; numberOfPages: number; hasNext: boolean; hasPrev: boolean; @@ -173,7 +188,12 @@ function Controls(props: ControlsProps) { {/* spacer to make page indicators centered */} {[...Array(props.numberOfPages)].map((_, i) => ( - + ))} @@ -203,15 +223,15 @@ function Controls(props: ControlsProps) { } interface PageIndicatorProps { - page: number; + pageNumber: number; goToPage: (page: number) => void; current: boolean; } function PageIndicator(props: PageIndicatorProps) { const onClick = useCallback(() => { - props.goToPage(props.page); - }, [props.goToPage, props.page]); + props.goToPage(props.pageNumber); + }, [props.goToPage, props.pageNumber]); return (