diff options
| author | Oliver <oliver@mohlin.dev> | 2025-09-02 11:54:22 +0200 |
|---|---|---|
| committer | Tobias Järvelöv <tobias.jarvelov@mullvad.net> | 2025-09-22 12:35:43 +0200 |
| commit | 469028cf96a750b22f7e252fbd7b8c9aa501d77e (patch) | |
| tree | abe3b1611f6b0bbd5c76bcebd2bc0d1e1cb18ae8 | |
| parent | d692e28a4dd4025ba2867c19ad482889f2decb3e (diff) | |
| download | mullvadvpn-469028cf96a750b22f7e252fbd7b8c9aa501d77e.tar.xz mullvadvpn-469028cf96a750b22f7e252fbd7b8c9aa501d77e.zip | |
Link feature indicators to settings in open vpn settings
5 files changed, 234 insertions, 178 deletions
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/views/main/components/connection-panel/components/feature-indicators/hooks/use-get-feature-indicator/useGetFeatureIndicator.ts b/desktop/packages/mullvad-vpn/src/renderer/components/views/main/components/connection-panel/components/feature-indicators/hooks/use-get-feature-indicator/useGetFeatureIndicator.ts index 0bc0d9d336..5e09a00d54 100644 --- a/desktop/packages/mullvad-vpn/src/renderer/components/views/main/components/connection-panel/components/feature-indicators/hooks/use-get-feature-indicator/useGetFeatureIndicator.ts +++ b/desktop/packages/mullvad-vpn/src/renderer/components/views/main/components/connection-panel/components/feature-indicators/hooks/use-get-feature-indicator/useGetFeatureIndicator.ts @@ -124,6 +124,30 @@ export const useGetFeatureIndicator = () => { }); }, [history]); + const gotoBridgeMode = React.useCallback(() => { + history.push(RoutePath.openVpnSettings, { + transition: TransitionType.show, + options: [ + { + type: 'scroll-to-anchor', + id: 'bridge-mode-setting', + }, + ], + }); + }, [history]); + + const goToMssFix = React.useCallback(() => { + history.push(RoutePath.openVpnSettings, { + transition: TransitionType.show, + options: [ + { + type: 'scroll-to-anchor', + id: 'mss-fix-setting', + }, + ], + }); + }, [history]); + const featureMap: Record<FeatureIndicator, { label: string; onClick?: () => void }> = { [FeatureIndicator.daita]: { label: strings.daita, onClick: gotoDaitaFeature }, [FeatureIndicator.daitaMultihop]: { @@ -171,6 +195,7 @@ export const useGetFeatureIndicator = () => { }, [FeatureIndicator.bridgeMode]: { label: messages.pgettext('openvpn-settings-view', 'Bridge mode'), + onClick: gotoBridgeMode, }, [FeatureIndicator.lanSharing]: { label: messages.pgettext('vpn-settings-view', 'Local network sharing'), @@ -178,6 +203,7 @@ export const useGetFeatureIndicator = () => { }, [FeatureIndicator.customMssFix]: { label: messages.pgettext('openvpn-settings-view', 'Mssfix'), + onClick: goToMssFix, }, [FeatureIndicator.lockdownMode]: { label: messages.pgettext('vpn-settings-view', 'Lockdown mode'), diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/views/open-vpn-settings/components/bridge-mode-setting/BridgeModeSetting.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/views/open-vpn-settings/components/bridge-mode-setting/BridgeModeSetting.tsx index 3ad7ae02c3..eaa246d2cb 100644 --- a/desktop/packages/mullvad-vpn/src/renderer/components/views/open-vpn-settings/components/bridge-mode-setting/BridgeModeSetting.tsx +++ b/desktop/packages/mullvad-vpn/src/renderer/components/views/open-vpn-settings/components/bridge-mode-setting/BridgeModeSetting.tsx @@ -1,6 +1,5 @@ -import { useCallback, useMemo } from 'react'; +import React, { useCallback, useMemo } from 'react'; import { sprintf } from 'sprintf-js'; -import styled from 'styled-components'; import { strings } from '../../../../../../shared/constants'; import { @@ -11,23 +10,24 @@ import { import { messages } from '../../../../../../shared/gettext'; import log from '../../../../../../shared/logging'; import { useAppContext } from '../../../../../context'; +import { useScrollToListItem } from '../../../../../hooks'; +import { Listbox } from '../../../../../lib/components/listbox/Listbox'; import { formatHtml } from '../../../../../lib/html-formatter'; import { useSelector } from '../../../../../redux/store'; -import { AriaDescription, AriaInputGroup } from '../../../../AriaGroup'; -import * as Cell from '../../../../cell'; -import Selector, { SelectorItem } from '../../../../cell/Selector'; +import { DefaultListboxOption } from '../../../../default-listbox-option'; +import InfoButton from '../../../../InfoButton'; import { ModalMessage } from '../../../../Modal'; -const StyledSelectorContainer = styled.div({ - flex: 0, -}); - export function BridgeModeSetting() { const { setBridgeState: setBridgeStateImpl } = useAppContext(); const relaySettings = useSelector((state) => state.settings.relaySettings); const bridgeState = useSelector((state) => state.settings.bridgeState); + const id = 'bridge-mode-setting'; + const ref = React.useRef<HTMLDivElement>(null); + const scrollToAnchor = useScrollToListItem(ref, id); + const tunnelProtocol = useMemo(() => { const protocol = 'normal' in relaySettings ? relaySettings.normal.tunnelProtocol : 'any'; return protocol === 'any' ? null : protocol; @@ -38,22 +38,6 @@ export function BridgeModeSetting() { return protocol === 'any' ? null : protocol; }, [relaySettings]); - const options: SelectorItem<BridgeState>[] = useMemo( - () => [ - { - label: messages.gettext('On'), - value: 'on', - disabled: tunnelProtocol !== 'openvpn' || transportProtocol === 'udp', - 'data-testid': 'bridge-mode-on', - }, - { - label: messages.gettext('Off'), - value: 'off', - }, - ], - [tunnelProtocol, transportProtocol], - ); - const setBridgeState = useCallback( async (bridgeState: BridgeState) => { try { @@ -76,50 +60,56 @@ export function BridgeModeSetting() { const footerText = bridgeModeFooterText(bridgeState === 'on', tunnelProtocol, transportProtocol); return ( - <> - <AriaInputGroup> - <StyledSelectorContainer> - <Selector - title={ + <Listbox + value={bridgeState} + onValueChange={onSelectBridgeState} + animation={scrollToAnchor?.animation}> + <Listbox.Item ref={ref}> + <Listbox.Content> + <Listbox.Label> + { // TRANSLATORS: The title for the shadowsocks bridge selector section. messages.pgettext('openvpn-settings-view', 'Bridge mode') } - infoTitle={messages.pgettext('openvpn-settings-view', 'Bridge mode')} - details={ - <> - <ModalMessage> - {sprintf( - // TRANSLATORS: This is used as a description for the bridge mode - // TRANSLATORS: setting. - // TRANSLATORS: Available placeholders: - // TRANSLATORS: %(openvpn)s - will be replaced with OpenVPN - messages.pgettext( - 'openvpn-settings-view', - 'Helps circumvent censorship, by routing your traffic through a bridge server before reaching an %(openvpn)s server. Obfuscation is added to make fingerprinting harder.', - ), - { openvpn: strings.openvpn }, - )} - </ModalMessage> - <ModalMessage> - {messages.gettext('This setting increases latency. Use only if needed.')} - </ModalMessage> - </> - } - items={options} - value={bridgeState} - onSelect={onSelectBridgeState} - automaticValue={'auto' as const} - /> - </StyledSelectorContainer> - {footerText !== undefined && ( - <Cell.CellFooter> - <AriaDescription> - <Cell.CellFooterText>{footerText}</Cell.CellFooterText> - </AriaDescription> - </Cell.CellFooter> - )} - </AriaInputGroup> - </> + </Listbox.Label> + <InfoButton> + <> + <ModalMessage> + {sprintf( + // TRANSLATORS: This is used as a description for the bridge mode + // TRANSLATORS: setting. + // TRANSLATORS: Available placeholders: + // TRANSLATORS: %(openvpn)s - will be replaced with OpenVPN + messages.pgettext( + 'openvpn-settings-view', + 'Helps circumvent censorship, by routing your traffic through a bridge server before reaching an %(openvpn)s server. Obfuscation is added to make fingerprinting harder.', + ), + { openvpn: strings.openvpn }, + )} + </ModalMessage> + <ModalMessage> + {messages.gettext('This setting increases latency. Use only if needed.')} + </ModalMessage> + </> + </InfoButton> + </Listbox.Content> + </Listbox.Item> + <Listbox.Options> + <DefaultListboxOption value={'auto'}>{messages.gettext('Automatic')}</DefaultListboxOption> + <DefaultListboxOption + value={'on'} + disabled={tunnelProtocol !== 'openvpn' || transportProtocol === 'udp'} + data-testid="bridge-mode-on"> + {messages.gettext('On')} + </DefaultListboxOption> + <DefaultListboxOption value={'off'}>{messages.gettext('Off')}</DefaultListboxOption> + </Listbox.Options> + {footerText !== undefined && ( + <Listbox.Footer> + <Listbox.Text>{footerText}</Listbox.Text> + </Listbox.Footer> + )} + </Listbox> ); } diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/views/open-vpn-settings/components/mss-fix-setting/MssFixSetting.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/views/open-vpn-settings/components/mss-fix-setting/MssFixSetting.tsx index d9f727addf..0107faca44 100644 --- a/desktop/packages/mullvad-vpn/src/renderer/components/views/open-vpn-settings/components/mss-fix-setting/MssFixSetting.tsx +++ b/desktop/packages/mullvad-vpn/src/renderer/components/views/open-vpn-settings/components/mss-fix-setting/MssFixSetting.tsx @@ -1,4 +1,4 @@ -import { useCallback } from 'react'; +import React, { useCallback } from 'react'; import { sprintf } from 'sprintf-js'; import { strings } from '../../../../../../shared/constants'; @@ -6,9 +6,10 @@ import { messages } from '../../../../../../shared/gettext'; import log from '../../../../../../shared/logging'; import { removeNonNumericCharacters } from '../../../../../../shared/string-helpers'; import { useAppContext } from '../../../../../context'; +import { useScrollToListItem } from '../../../../../hooks'; +import { ListItem } from '../../../../../lib/components/list-item'; +import { useTextField } from '../../../../../lib/components/text-field'; import { useSelector } from '../../../../../redux/store'; -import { AriaDescription, AriaInput, AriaInputGroup, AriaLabel } from '../../../../AriaGroup'; -import * as Cell from '../../../../cell'; const MIN_MSSFIX_VALUE = 1000; const MAX_MSSFIX_VALUE = 1450; @@ -17,6 +18,14 @@ export function MssFixSetting() { const { setOpenVpnMssfix: setOpenVpnMssfixImpl } = useAppContext(); const mssfix = useSelector((state) => state.settings.openVpn.mssfix); + const id = 'mss-fix-setting'; + const ref = React.useRef<HTMLDivElement>(null); + const scrollToAnchor = useScrollToListItem(ref, id); + + const inputRef = React.useRef<HTMLInputElement>(null); + const labelId = React.useId(); + const descriptionId = React.useId(); + const setOpenVpnMssfix = useCallback( async (mssfix?: number) => { try { @@ -39,48 +48,76 @@ export function MssFixSetting() { [setOpenVpnMssfix], ); + const { value, handleChange, invalid, dirty, blur, reset } = useTextField({ + inputRef, + defaultValue: mssfix ? mssfix.toString() : '', + format: removeNonNumericCharacters, + validate: mssfixIsValid, + }); + + const handleBlur = React.useCallback(async () => { + if (!invalid && dirty) { + await onMssfixSubmit(value); + } + if (invalid) { + reset(); + } + }, [dirty, invalid, onMssfixSubmit, reset, value]); + + const handleSubmit = React.useCallback( + async (event: React.FormEvent) => { + event.preventDefault(); + if (!invalid) { + await onMssfixSubmit(value); + blur(); + } + }, + [blur, invalid, onMssfixSubmit, value], + ); + return ( - <AriaInputGroup> - <Cell.Container> - <AriaLabel> - <Cell.InputLabel>{messages.pgettext('openvpn-settings-view', 'Mssfix')}</Cell.InputLabel> - </AriaLabel> - <AriaInput> - <Cell.AutoSizingTextInput - initialValue={mssfix ? mssfix.toString() : ''} - inputMode={'numeric'} - maxLength={4} - placeholder={messages.gettext('Default')} - onSubmitValue={onMssfixSubmit} - validateValue={mssfixIsValid} - submitOnBlur={true} - modifyValue={removeNonNumericCharacters} - /> - </AriaInput> - </Cell.Container> - <Cell.CellFooter> - <AriaDescription> - <Cell.CellFooterText> - {sprintf( - // TRANSLATORS: The hint displayed below the Mssfix input field. - // TRANSLATORS: Available placeholders: - // TRANSLATORS: %(openvpn)s - will be replaced with "OpenVPN" - // TRANSLATORS: %(max)d - the maximum possible mssfix value - // TRANSLATORS: %(min)d - the minimum possible mssfix value - messages.pgettext( - 'openvpn-settings-view', - 'Set %(openvpn)s MSS value. Valid range: %(min)d - %(max)d.', - ), - { - openvpn: strings.openvpn, - min: MIN_MSSFIX_VALUE, - max: MAX_MSSFIX_VALUE, - }, - )} - </Cell.CellFooterText> - </AriaDescription> - </Cell.CellFooter> - </AriaInputGroup> + <ListItem animation={scrollToAnchor?.animation}> + <ListItem.Item ref={ref}> + <ListItem.Content> + <ListItem.Label id={labelId}> + {messages.pgettext('openvpn-settings-view', 'Mssfix')} + </ListItem.Label> + <ListItem.TextField invalid={invalid} onSubmit={handleSubmit}> + <ListItem.TextField.Input + ref={inputRef} + value={value} + placeholder={messages.gettext('Default')} + inputMode="numeric" + maxLength={4} + aria-labelledby={labelId} + aria-describedby={descriptionId} + onBlur={handleBlur} + onChange={handleChange} + /> + </ListItem.TextField> + </ListItem.Content> + </ListItem.Item> + <ListItem.Footer> + <ListItem.Text id={descriptionId}> + {sprintf( + // TRANSLATORS: The hint displayed below the Mssfix input field. + // TRANSLATORS: Available placeholders: + // TRANSLATORS: %(openvpn)s - will be replaced with "OpenVPN" + // TRANSLATORS: %(max)d - the maximum possible mssfix value + // TRANSLATORS: %(min)d - the minimum possible mssfix value + messages.pgettext( + 'openvpn-settings-view', + 'Set %(openvpn)s MSS value. Valid range: %(min)d - %(max)d.', + ), + { + openvpn: strings.openvpn, + min: MIN_MSSFIX_VALUE, + max: MAX_MSSFIX_VALUE, + }, + )} + </ListItem.Text> + </ListItem.Footer> + </ListItem> ); } diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/views/open-vpn-settings/components/open-vpn-port-setting/OpenVpnPortSetting.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/views/open-vpn-settings/components/open-vpn-port-setting/OpenVpnPortSetting.tsx index 1c7df82270..40fd659b45 100644 --- a/desktop/packages/mullvad-vpn/src/renderer/components/views/open-vpn-settings/components/open-vpn-port-setting/OpenVpnPortSetting.tsx +++ b/desktop/packages/mullvad-vpn/src/renderer/components/views/open-vpn-settings/components/open-vpn-port-setting/OpenVpnPortSetting.tsx @@ -4,10 +4,11 @@ import styled from 'styled-components'; import { wrapConstraint } from '../../../../../../shared/daemon-rpc-types'; import { messages } from '../../../../../../shared/gettext'; +import { Listbox } from '../../../../../lib/components/listbox/Listbox'; import { useRelaySettingsUpdater } from '../../../../../lib/constraint-updater'; import { useSelector } from '../../../../../redux/store'; -import { AriaInputGroup } from '../../../../AriaGroup'; -import Selector, { SelectorItem } from '../../../../cell/Selector'; +import { SelectorItem } from '../../../../cell/Selector'; +import { DefaultListboxOption } from '../../../../default-listbox-option'; const UDP_PORTS = [1194, 1195, 1196, 1197, 1300, 1301, 1302]; const TCP_PORTS = [80, 443]; @@ -54,24 +55,30 @@ export function OpenVpnPortSetting() { } return ( - <StyledSelectorContainer> - <AriaInputGroup> - <Selector - title={sprintf( - // TRANSLATORS: The title for the port selector section. - // TRANSLATORS: Available placeholders: - // TRANSLATORS: %(portType)s - a selected protocol (either TCP or UDP) - messages.pgettext('openvpn-settings-view', '%(portType)s port'), - { - portType: protocol.toUpperCase(), - }, - )} - items={portItems[protocol]} - value={port} - onSelect={onSelect} - automaticValue={null} - /> - </AriaInputGroup> - </StyledSelectorContainer> + <Listbox value={port} onValueChange={onSelect}> + <Listbox.Item> + <Listbox.Content> + <Listbox.Label> + {sprintf( + // TRANSLATORS: The title for the port selector section. + // TRANSLATORS: Available placeholders: + // TRANSLATORS: %(portType)s - a selected protocol (either TCP or UDP) + messages.pgettext('openvpn-settings-view', '%(portType)s port'), + { + portType: protocol.toUpperCase(), + }, + )} + </Listbox.Label> + </Listbox.Content> + </Listbox.Item> + <Listbox.Options> + <DefaultListboxOption value={null}>{messages.gettext('Automatic')}</DefaultListboxOption> + {portItems[protocol].map((item) => ( + <DefaultListboxOption key={item.value} value={item.value}> + {item.label} + </DefaultListboxOption> + ))} + </Listbox.Options> + </Listbox> ); } diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/views/open-vpn-settings/components/transport-protocol-setting/TransportProtocolSetting.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/views/open-vpn-settings/components/transport-protocol-setting/TransportProtocolSetting.tsx index 5b89da0833..89abfa35a8 100644 --- a/desktop/packages/mullvad-vpn/src/renderer/components/views/open-vpn-settings/components/transport-protocol-setting/TransportProtocolSetting.tsx +++ b/desktop/packages/mullvad-vpn/src/renderer/components/views/open-vpn-settings/components/transport-protocol-setting/TransportProtocolSetting.tsx @@ -1,20 +1,25 @@ -import { useCallback, useMemo } from 'react'; +import React, { useCallback, useMemo } from 'react'; import { RelayProtocol, wrapConstraint } from '../../../../../../shared/daemon-rpc-types'; import { messages } from '../../../../../../shared/gettext'; +import { useScrollToListItem } from '../../../../../hooks'; +import { Listbox } from '../../../../../lib/components/listbox/Listbox'; import { useRelaySettingsUpdater } from '../../../../../lib/constraint-updater'; import { formatHtml } from '../../../../../lib/html-formatter'; import { useSelector } from '../../../../../redux/store'; -import { AriaDescription, AriaInputGroup } from '../../../../AriaGroup'; -import * as Cell from '../../../../cell'; -import Selector, { SelectorItem } from '../../../../cell/Selector'; -import { StyledSelectorContainer } from '../../OpenVpnSettingsView'; +import { DefaultListboxOption } from '../../../../default-listbox-option'; export function TransportProtocolSetting() { const relaySettingsUpdater = useRelaySettingsUpdater(); const relaySettings = useSelector((state) => state.settings.relaySettings); const bridgeState = useSelector((state) => state.settings.bridgeState); + const id = 'transport-protocol-setting'; + const ref = React.useRef<HTMLDivElement>(null); + const scrollToListItem = useScrollToListItem(ref, id); + + const descriptionId = React.useId(); + const protocol = useMemo(() => { const protocol = 'normal' in relaySettings ? relaySettings.normal.openvpn.protocol : 'any'; return protocol === 'any' ? null : protocol; @@ -31,48 +36,39 @@ export function TransportProtocolSetting() { [relaySettingsUpdater], ); - const items: SelectorItem<RelayProtocol>[] = useMemo( - () => [ - { - label: messages.gettext('TCP'), - value: 'tcp', - }, - { - label: messages.gettext('UDP'), - value: 'udp', - disabled: bridgeState === 'on', - }, - ], - [bridgeState], - ); - return ( - <StyledSelectorContainer> - <AriaInputGroup> - <Selector - title={messages.pgettext('openvpn-settings-view', 'Transport protocol')} - items={items} - value={protocol} - onSelect={onSelect} - automaticValue={null} - /> - {bridgeState === 'on' && ( - <Cell.CellFooter> - <AriaDescription> - <Cell.CellFooterText> - {formatHtml( - // TRANSLATORS: This is used to instruct users how to make UDP mode - // TRANSLATORS: available. - messages.pgettext( - 'openvpn-settings-view', - 'To activate UDP, change <b>Bridge mode</b> to <b>Automatic</b> or <b>Off</b>.', - ), - )} - </Cell.CellFooterText> - </AriaDescription> - </Cell.CellFooter> - )} - </AriaInputGroup> - </StyledSelectorContainer> + <Listbox animation={scrollToListItem?.animation} value={protocol} onValueChange={onSelect}> + <Listbox.Item ref={ref}> + <Listbox.Content> + <Listbox.Label> + {messages.pgettext('openvpn-settings-view', 'Transport protocol')} + </Listbox.Label> + </Listbox.Content> + </Listbox.Item> + <Listbox.Options> + <DefaultListboxOption value={null}>{messages.gettext('Automatic')}</DefaultListboxOption> + <DefaultListboxOption value={'tcp'}>{messages.gettext('TCP')}</DefaultListboxOption> + <DefaultListboxOption + value={'udp'} + disabled={bridgeState === 'on'} + aria-describedby={bridgeState === 'on' ? descriptionId : undefined}> + {messages.gettext('UDP')} + </DefaultListboxOption> + </Listbox.Options> + {bridgeState === 'on' && ( + <Listbox.Footer> + <Listbox.Text id={descriptionId}> + {formatHtml( + // TRANSLATORS: This is used to instruct users how to make UDP mode + // TRANSLATORS: available. + messages.pgettext( + 'openvpn-settings-view', + 'To activate UDP, change <b>Bridge mode</b> to <b>Automatic</b> or <b>Off</b>.', + ), + )} + </Listbox.Text> + </Listbox.Footer> + )} + </Listbox> ); } |
