diff options
| author | Oskar Nyberg <oskar@mullvad.net> | 2022-09-27 11:37:32 +0200 |
|---|---|---|
| committer | Oskar Nyberg <oskar@mullvad.net> | 2022-09-27 11:37:32 +0200 |
| commit | 8c4d03d4f31069ba9648ffb22c056185d9706167 (patch) | |
| tree | 5d61a310d8f70cbb4ccb5708395cc0e254ff81a9 /gui | |
| parent | de9bf32918295037d1616d256afedf52ab7d4b6d (diff) | |
| parent | dcdd4b44efb5f77148d262f4c573dd6aca0a5e03 (diff) | |
| download | mullvadvpn-8c4d03d4f31069ba9648ffb22c056185d9706167.tar.xz mullvadvpn-8c4d03d4f31069ba9648ffb22c056185d9706167.zip | |
Merge branch 'add-custom-port-option'
Diffstat (limited to 'gui')
| -rw-r--r-- | gui/locales/messages.pot | 11 | ||||
| -rw-r--r-- | gui/src/main/daemon-rpc.ts | 29 | ||||
| -rw-r--r-- | gui/src/main/relay-list.ts | 36 | ||||
| -rw-r--r-- | gui/src/renderer/app.tsx | 3 | ||||
| -rw-r--r-- | gui/src/renderer/components/Filter.tsx | 12 | ||||
| -rw-r--r-- | gui/src/renderer/components/OpenVpnSettings.tsx | 74 | ||||
| -rw-r--r-- | gui/src/renderer/components/SelectLanguage.tsx | 6 | ||||
| -rw-r--r-- | gui/src/renderer/components/VpnSettings.tsx | 17 | ||||
| -rw-r--r-- | gui/src/renderer/components/WireguardSettings.tsx | 120 | ||||
| -rw-r--r-- | gui/src/renderer/components/cell/Input.tsx | 183 | ||||
| -rw-r--r-- | gui/src/renderer/components/cell/Selector.tsx | 225 | ||||
| -rw-r--r-- | gui/src/renderer/redux/settings/actions.ts | 23 | ||||
| -rw-r--r-- | gui/src/renderer/redux/settings/reducers.ts | 9 | ||||
| -rw-r--r-- | gui/src/shared/daemon-rpc-types.ts | 12 | ||||
| -rw-r--r-- | gui/src/shared/ipc-schema.ts | 2 | ||||
| -rw-r--r-- | gui/src/shared/string-helpers.ts | 4 | ||||
| -rw-r--r-- | gui/test/e2e/setup/main.ts | 12 |
17 files changed, 510 insertions, 268 deletions
diff --git a/gui/locales/messages.pot b/gui/locales/messages.pot index f9e6ca6e10..489a3a8023 100644 --- a/gui/locales/messages.pot +++ b/gui/locales/messages.pot @@ -114,6 +114,9 @@ msgstr "" msgid "CREATING SECURE CONNECTION" msgstr "" +msgid "Custom" +msgstr "" + msgid "Default" msgstr "" @@ -1512,6 +1515,14 @@ msgctxt "wireguard-settings-view" msgid "The automatic setting will randomly choose from a wide range of ports." msgstr "" +msgctxt "wireguard-settings-view" +msgid "The automatic setting will randomly choose from the valid port ranges shown below." +msgstr "" + +msgctxt "wireguard-settings-view" +msgid "The custom port can be any value inside the valid ranges: %(portRanges)s." +msgstr "" + #. The hint displayed below the WireGuard IP version selector. #. Available placeholders: #. %(wireguard)s - Will be replaced with the string "WireGuard" diff --git a/gui/src/main/daemon-rpc.ts b/gui/src/main/daemon-rpc.ts index 950cf1a4a3..a6136bc6f3 100644 --- a/gui/src/main/daemon-rpc.ts +++ b/gui/src/main/daemon-rpc.ts @@ -31,14 +31,15 @@ import { IObfuscationEndpoint, IOpenVpnConstraints, IProxyEndpoint, - IRelayList, IRelayListCity, IRelayListCountry, IRelayListHostname, + IRelayListWithEndpointData, ISettings, ITunnelOptions, ITunnelStateRelayInfo, IWireguardConstraints, + IWireguardEndpointData, LoggedInDeviceState, LoggedOutDeviceState, ObfuscationSettings, @@ -258,7 +259,7 @@ export class DaemonRpc { } } - public async getRelayLocations(): Promise<IRelayList> { + public async getRelayLocations(): Promise<IRelayListWithEndpointData> { if (this.isConnected) { const response = await this.callEmpty<grpcTypes.RelayList>(this.client.getRelayLocations); return convertFromRelayList(response); @@ -757,13 +758,25 @@ function liftConstraint<T>(constraint: Constraint<T> | undefined): T | undefined return undefined; } -function convertFromRelayList(relayList: grpcTypes.RelayList): IRelayList { +function convertFromRelayList(relayList: grpcTypes.RelayList): IRelayListWithEndpointData { return { - countries: relayList - .getCountriesList() - .map((country: grpcTypes.RelayListCountry) => - convertFromRelayListCountry(country.toObject()), - ), + relayList: { + countries: relayList + .getCountriesList() + .map((country: grpcTypes.RelayListCountry) => + convertFromRelayListCountry(country.toObject()), + ), + }, + wireguardEndpointData: convertWireguardEndpointData(relayList.getWireguard()!), + }; +} + +function convertWireguardEndpointData( + data: grpcTypes.WireguardEndpointData, +): IWireguardEndpointData { + return { + portRanges: data.getPortRangesList().map((range) => [range.getFirst(), range.getLast()]), + udp2tcpPorts: data.getUdp2tcpPortsList(), }; } diff --git a/gui/src/main/relay-list.ts b/gui/src/main/relay-list.ts index b43548e6a1..c7b4a1fff1 100644 --- a/gui/src/main/relay-list.ts +++ b/gui/src/main/relay-list.ts @@ -1,12 +1,26 @@ -import { BridgeState, IRelayList, liftConstraint, RelaySettings } from '../shared/daemon-rpc-types'; +import { + BridgeState, + IRelayList, + IRelayListWithEndpointData, + liftConstraint, + RelaySettings, +} from '../shared/daemon-rpc-types'; import { IRelayListPair } from '../shared/ipc-schema'; import { IpcMainEventChannel } from './ipc-event-channel'; export default class RelayList { - private relays: IRelayList = { countries: [] }; + private relays: IRelayListWithEndpointData = { + relayList: { + countries: [], + }, + wireguardEndpointData: { + portRanges: [], + udp2tcpPorts: [], + }, + }; public setRelays( - newRelayList: IRelayList, + newRelayList: IRelayListWithEndpointData, relaySettings: RelaySettings, bridgeState: BridgeState, ) { @@ -25,14 +39,18 @@ export default class RelayList { } private processRelays( - relayList: IRelayList, + relayList: IRelayListWithEndpointData, relaySettings: RelaySettings, bridgeState: BridgeState, ): IRelayListPair { - const filteredRelays = this.processRelaysForPresentation(relayList, relaySettings); - const filteredBridges = this.processBridgesForPresentation(relayList, bridgeState); + const filteredRelays = this.processRelaysForPresentation(relayList.relayList, relaySettings); + const filteredBridges = this.processBridgesForPresentation(relayList.relayList, bridgeState); - return { relays: filteredRelays, bridges: filteredBridges }; + return { + relays: filteredRelays, + bridges: filteredBridges, + wireguardEndpointData: relayList.wireguardEndpointData, + }; } private processRelaysForPresentation( @@ -75,9 +93,7 @@ export default class RelayList { })) .filter((country) => country.cities.length > 0); - return { - countries: filteredCountries, - }; + return { countries: filteredCountries }; } private processBridgesForPresentation( diff --git a/gui/src/renderer/app.tsx b/gui/src/renderer/app.tsx index 1c222c9299..6bc2c61ff3 100644 --- a/gui/src/renderer/app.tsx +++ b/gui/src/renderer/app.tsx @@ -849,6 +849,9 @@ export default class AppRenderer { this.reduxActions.settings.updateRelayLocations(relays); this.reduxActions.settings.updateBridgeLocations(bridges); + this.reduxActions.settings.updateWireguardEndpointData( + this.relayListPair.wireguardEndpointData, + ); } private setCurrentVersion(versionInfo: ICurrentAppVersionInfo) { diff --git a/gui/src/renderer/components/Filter.tsx b/gui/src/renderer/components/Filter.tsx index 79253ed7b0..5c2e7e0f55 100644 --- a/gui/src/renderer/components/Filter.tsx +++ b/gui/src/renderer/components/Filter.tsx @@ -191,10 +191,6 @@ function FilterByOwnership(props: IFilterByOwnershipProps) { () => [ { - label: messages.gettext('Any'), - value: Ownership.any, - }, - { label: messages.pgettext('filter-view', 'Mullvad owned only'), value: Ownership.mullvadOwned, }, @@ -220,7 +216,13 @@ function FilterByOwnership(props: IFilterByOwnershipProps) { </Cell.CellButton> <Accordion expanded={expanded}> - <StyledSelector values={values} value={props.ownership} onSelect={props.setOwnership} /> + <StyledSelector + items={values} + value={props.ownership} + onSelect={props.setOwnership} + automaticLabel={messages.gettext('Any')} + automaticValue={Ownership.any} + /> </Accordion> </AriaInputGroup> ); diff --git a/gui/src/renderer/components/OpenVpnSettings.tsx b/gui/src/renderer/components/OpenVpnSettings.tsx index fce949c6a9..af4404fd1a 100644 --- a/gui/src/renderer/components/OpenVpnSettings.tsx +++ b/gui/src/renderer/components/OpenVpnSettings.tsx @@ -7,6 +7,7 @@ import { BridgeState, RelayProtocol, TunnelProtocol } from '../../shared/daemon- import { messages } from '../../shared/gettext'; import log from '../../shared/logging'; import RelaySettingsBuilder from '../../shared/relay-settings-builder'; +import { removeNonNumericCharacters } from '../../shared/string-helpers'; import { useAppContext } from '../context'; import { useHistory } from '../lib/history'; import { useBoolean } from '../lib/utilityHooks'; @@ -15,7 +16,7 @@ import { useSelector } from '../redux/store'; import * as AppButton from './AppButton'; import { AriaDescription, AriaInput, AriaInputGroup, AriaLabel } from './AriaGroup'; import * as Cell from './cell'; -import Selector, { ISelectorItem } from './cell/Selector'; +import Selector, { SelectorItem } from './cell/Selector'; import { BackAction } from './KeyboardNavigation'; import { Layout, SettingsContainer } from './Layout'; import { ModalAlert, ModalAlertType } from './Modal'; @@ -33,17 +34,13 @@ const MAX_MSSFIX_VALUE = 1450; const UDP_PORTS = [1194, 1195, 1196, 1197, 1300, 1301, 1302]; const TCP_PORTS = [80, 443]; -type OptionalPort = number | undefined; - -type OptionalRelayProtocol = RelayProtocol | undefined; - export enum BridgeModeAvailability { available, blockedDueToTunnelProtocol, blockedDueToTransportProtocol, } -function mapPortToSelectorItem(value: number): ISelectorItem<number> { +function mapPortToSelectorItem(value: number): SelectorItem<number> { return { label: value.toString(), value }; } @@ -127,19 +124,15 @@ function TransportProtocolSelector() { const bridgeState = useSelector((state) => state.settings.bridgeState); const protocol = useMemo(() => { - const protocol = 'normal' in relaySettings ? relaySettings.normal.openvpn.protocol : undefined; - return protocol === 'any' ? undefined : protocol; + const protocol = 'normal' in relaySettings ? relaySettings.normal.openvpn.protocol : 'any'; + return protocol === 'any' ? null : protocol; }, [relaySettings]); const protocolAndPortUpdater = useProtocolAndPortUpdater(); - const items: ISelectorItem<OptionalRelayProtocol>[] = useMemo( + const items: SelectorItem<RelayProtocol>[] = useMemo( () => [ { - label: messages.gettext('Automatic'), - value: undefined, - }, - { label: messages.gettext('TCP'), value: 'tcp', }, @@ -157,9 +150,10 @@ function TransportProtocolSelector() { <AriaInputGroup> <Selector title={messages.pgettext('openvpn-settings-view', 'Transport protocol')} - values={items} + items={items} value={protocol} onSelect={protocolAndPortUpdater} + automaticValue={null} /> {bridgeState === 'on' && ( <Cell.Footer> @@ -186,7 +180,7 @@ function useProtocolAndPortUpdater() { const { updateRelaySettings } = useAppContext(); const updater = useCallback( - async (protocol?: RelayProtocol, port?: number) => { + async (protocol: RelayProtocol | null, port?: number | null) => { const relayUpdate = RelaySettingsBuilder.normal() .tunnel.openvpn((openvpn) => { if (protocol) { @@ -220,35 +214,30 @@ function PortSelector() { const relaySettings = useSelector((state) => state.settings.relaySettings); const protocol = useMemo(() => { - const protocol = 'normal' in relaySettings ? relaySettings.normal.openvpn.protocol : undefined; - return protocol === 'any' ? undefined : protocol; + const protocol = 'normal' in relaySettings ? relaySettings.normal.openvpn.protocol : 'any'; + return protocol === 'any' ? null : protocol; }, [relaySettings]); const port = useMemo(() => { - const port = 'normal' in relaySettings ? relaySettings.normal.openvpn.port : undefined; - return port === 'any' ? undefined : port; + const port = 'normal' in relaySettings ? relaySettings.normal.openvpn.port : 'any'; + return port === 'any' ? null : port; }, [relaySettings]); const protocolAndPortUpdater = useProtocolAndPortUpdater(); const onSelect = useCallback( - async (port?: number) => { + async (port: number | null) => { await protocolAndPortUpdater(protocol, port); }, [protocolAndPortUpdater, protocol], ); - const automaticPort: ISelectorItem<OptionalPort> = { - label: messages.gettext('Automatic'), - value: undefined, - }; - const portItems = { - udp: [automaticPort].concat(UDP_PORTS.map(mapPortToSelectorItem)), - tcp: [automaticPort].concat(TCP_PORTS.map(mapPortToSelectorItem)), + udp: UDP_PORTS.map(mapPortToSelectorItem), + tcp: TCP_PORTS.map(mapPortToSelectorItem), }; - if (protocol === undefined) { + if (protocol === null) { return null; } @@ -265,9 +254,10 @@ function PortSelector() { portType: protocol.toUpperCase(), }, )} - values={portItems[protocol]} + items={portItems[protocol]} value={port} onSelect={onSelect} + automaticValue={null} /> </AriaInputGroup> </StyledSelectorContainer> @@ -281,22 +271,18 @@ function BridgeModeSelector() { const bridgeState = useSelector((state) => state.settings.bridgeState); const tunnelProtocol = useMemo(() => { - const protocol = 'normal' in relaySettings ? relaySettings.normal.tunnelProtocol : undefined; - return protocol === 'any' ? undefined : protocol; + const protocol = 'normal' in relaySettings ? relaySettings.normal.tunnelProtocol : 'any'; + return protocol === 'any' ? null : protocol; }, [relaySettings]); const transportProtocol = useMemo(() => { - const protocol = 'normal' in relaySettings ? relaySettings.normal.openvpn.protocol : undefined; - return protocol === 'any' ? undefined : protocol; + const protocol = 'normal' in relaySettings ? relaySettings.normal.openvpn.protocol : 'any'; + return protocol === 'any' ? null : protocol; }, [relaySettings]); - const options: ISelectorItem<BridgeState>[] = useMemo( + const options: SelectorItem<BridgeState>[] = useMemo( () => [ { - label: messages.gettext('Automatic'), - value: 'auto', - }, - { label: messages.gettext('On'), value: 'on', disabled: tunnelProtocol !== 'openvpn' || transportProtocol === 'udp', @@ -348,9 +334,10 @@ function BridgeModeSelector() { // TRANSLATORS: The title for the shadowsocks bridge selector section. messages.pgettext('openvpn-settings-view', 'Bridge mode') } - values={options} + items={options} value={bridgeState} onSelect={onSelectBridgeState} + automaticValue={'auto' as const} /> </StyledSelectorContainer> <Cell.Footer> @@ -379,7 +366,10 @@ function BridgeModeSelector() { ); } -function bridgeModeFooterText(tunnelProtocol?: TunnelProtocol, transportProtocol?: RelayProtocol) { +function bridgeModeFooterText( + tunnelProtocol: TunnelProtocol | null, + transportProtocol: RelayProtocol | null, +) { if (tunnelProtocol !== 'openvpn') { return formatMarkdown( sprintf( @@ -433,10 +423,6 @@ function bridgeModeFooterText(tunnelProtocol?: TunnelProtocol, transportProtocol } } -function removeNonNumericCharacters(value: string) { - return value.replace(/[^0-9]/g, ''); -} - function mssfixIsValid(mssfix: string): boolean { const parsedMssFix = mssfix ? parseInt(mssfix) : undefined; return ( diff --git a/gui/src/renderer/components/SelectLanguage.tsx b/gui/src/renderer/components/SelectLanguage.tsx index 419557f700..2b11f0a924 100644 --- a/gui/src/renderer/components/SelectLanguage.tsx +++ b/gui/src/renderer/components/SelectLanguage.tsx @@ -3,7 +3,7 @@ import styled from 'styled-components'; import { messages } from '../../shared/gettext'; import { AriaInputGroup } from './AriaGroup'; -import Selector, { ISelectorItem } from './cell/Selector'; +import Selector, { SelectorItem } from './cell/Selector'; import { CustomScrollbarsRef } from './CustomScrollbars'; import { BackAction } from './KeyboardNavigation'; import { Layout, SettingsContainer } from './Layout'; @@ -24,7 +24,7 @@ interface IProps { } interface IState { - source: Array<ISelectorItem<string>>; + source: Array<SelectorItem<string>>; } const StyledNavigationScrollbars = styled(NavigationScrollbars)({ @@ -79,7 +79,7 @@ export default class SelectLanguage extends React.Component<IProps, IState> { <AriaInputGroup> <StyledSelector title="" - values={this.state.source} + items={this.state.source} value={this.props.preferredLocale} onSelect={this.props.setPreferredLocale} selectedCellRef={this.selectedCellRef} diff --git a/gui/src/renderer/components/VpnSettings.tsx b/gui/src/renderer/components/VpnSettings.tsx index 47b48e4fcd..76d8a782b6 100644 --- a/gui/src/renderer/components/VpnSettings.tsx +++ b/gui/src/renderer/components/VpnSettings.tsx @@ -17,7 +17,7 @@ import { useSelector } from '../redux/store'; import * as AppButton from './AppButton'; import { AriaDescription, AriaDetails, AriaInput, AriaInputGroup, AriaLabel } from './AriaGroup'; import * as Cell from './cell'; -import Selector, { ISelectorItem } from './cell/Selector'; +import Selector, { SelectorItem } from './cell/Selector'; import CustomDnsSettings from './CustomDnsSettings'; import InfoButton, { InfoIcon } from './InfoButton'; import { BackAction } from './KeyboardNavigation'; @@ -612,10 +612,10 @@ function TunnelProtocolSetting() { ); const { updateRelaySettings } = useAppContext(); - const setTunnelProtocol = useCallback(async (tunnelProtocol: TunnelProtocol | undefined) => { + const setTunnelProtocol = useCallback(async (tunnelProtocol: TunnelProtocol | null) => { const relayUpdate = RelaySettingsBuilder.normal() .tunnel.tunnelProtocol((config) => { - if (tunnelProtocol) { + if (tunnelProtocol !== null) { config.tunnelProtocol.exact(tunnelProtocol); } else { config.tunnelProtocol.any(); @@ -630,13 +630,9 @@ function TunnelProtocolSetting() { } }, []); - const tunnelProtocolItems: Array<ISelectorItem<TunnelProtocol | undefined>> = useMemo( + const tunnelProtocolItems: Array<SelectorItem<TunnelProtocol>> = useMemo( () => [ { - label: messages.gettext('Automatic'), - value: undefined, - }, - { label: strings.wireguard, value: 'wireguard', }, @@ -653,9 +649,10 @@ function TunnelProtocolSetting() { <StyledSelectorContainer> <Selector title={messages.pgettext('vpn-settings-view', 'Tunnel protocol')} - values={tunnelProtocolItems} - value={tunnelProtocol} + items={tunnelProtocolItems} + value={tunnelProtocol ?? null} onSelect={setTunnelProtocol} + automaticValue={null} /> </StyledSelectorContainer> </AriaInputGroup> diff --git a/gui/src/renderer/components/WireguardSettings.tsx b/gui/src/renderer/components/WireguardSettings.tsx index 222e845001..9a422cc18e 100644 --- a/gui/src/renderer/components/WireguardSettings.tsx +++ b/gui/src/renderer/components/WireguardSettings.tsx @@ -11,6 +11,7 @@ import { } from '../../shared/daemon-rpc-types'; import { messages } from '../../shared/gettext'; import log from '../../shared/logging'; +import { removeNonNumericCharacters } from '../../shared/string-helpers'; import { useAppContext } from '../context'; import { createWireguardRelayUpdater } from '../lib/constraint-updater'; import { useHistory } from '../lib/history'; @@ -19,7 +20,7 @@ import { useSelector } from '../redux/store'; import * as AppButton from './AppButton'; import { AriaDescription, AriaInput, AriaInputGroup, AriaLabel } from './AriaGroup'; import * as Cell from './cell'; -import Selector, { ISelectorItem } from './cell/Selector'; +import Selector, { SelectorItem, SelectorWithCustomItem } from './cell/Selector'; import { InfoIcon } from './InfoButton'; import { BackAction } from './KeyboardNavigation'; import { Layout, SettingsContainer } from './Layout'; @@ -38,10 +39,7 @@ const MAX_WIREGUARD_MTU_VALUE = 1420; const WIREUGARD_UDP_PORTS = [51820, 53]; const UDP2TCP_PORTS = [80, 443, 5001]; -type OptionalPort = number | undefined; -type OptionalIpVersion = IpVersion | undefined; - -function mapPortToSelectorItem(value: number): ISelectorItem<number> { +function mapPortToSelectorItem(value: number): SelectorItem<number> { return { label: value.toString(), value }; } @@ -131,26 +129,23 @@ export default function WireguardSettings() { function PortSelector() { const relaySettings = useSelector((state) => state.settings.relaySettings); const { updateRelaySettings } = useAppContext(); + const allowedPortRanges = useSelector((state) => state.settings.wireguardEndpointData.portRanges); - const wireguardPortItems = useMemo(() => { - const automaticPort: ISelectorItem<OptionalPort> = { - label: messages.gettext('Automatic'), - value: undefined, - }; - - return [automaticPort].concat(WIREUGARD_UDP_PORTS.map(mapPortToSelectorItem)); - }, []); + const wireguardPortItems = useMemo<Array<SelectorItem<number>>>( + () => WIREUGARD_UDP_PORTS.map(mapPortToSelectorItem), + [], + ); const port = useMemo(() => { - const port = 'normal' in relaySettings ? relaySettings.normal.wireguard.port : undefined; - return port === 'any' ? undefined : port; + const port = 'normal' in relaySettings ? relaySettings.normal.wireguard.port : 'any'; + return port === 'any' ? null : port; }, [relaySettings]); const setWireguardPort = useCallback( - async (port?: number) => { + async (port: number | null) => { const relayUpdate = createWireguardRelayUpdater(relaySettings) .tunnel.wireguard((wireguard) => { - if (port) { + if (port !== null) { wireguard.port.exact(port); } else { wireguard.port.any(); @@ -167,15 +162,56 @@ function PortSelector() { [relaySettings], ); + const setCustomPort = useCallback( + async (port: string) => { + await setWireguardPort(parseInt(port)); + }, + [setWireguardPort], + ); + + const validateValue = useCallback( + (value) => allowedPortRanges.some(([start, end]) => value >= start && value <= end), + [allowedPortRanges], + ); + + const portRangesText = allowedPortRanges + .map(([start, end]) => (start === end ? start : `${start}-${end}`)) + .join(', '); + return ( <AriaInputGroup> <StyledSelectorContainer> - <Selector + <SelectorWithCustomItem // TRANSLATORS: The title for the WireGuard port selector. title={messages.pgettext('wireguard-settings-view', 'Port')} - values={wireguardPortItems} + items={wireguardPortItems} value={port} onSelect={setWireguardPort} + onSelectCustom={setCustomPort} + inputPlaceholder={messages.pgettext('wireguard-settings-view', 'Port')} + automaticValue={null} + modifyValue={removeNonNumericCharacters} + validateValue={validateValue} + maxLength={5} + details={ + <> + <ModalMessage> + {messages.pgettext( + 'wireguard-settings-view', + 'The automatic setting will randomly choose from the valid port ranges shown below.', + )} + </ModalMessage> + <ModalMessage> + {sprintf( + messages.pgettext( + 'wireguard-settings-view', + 'The custom port can be any value inside the valid ranges: %(portRanges)s.', + ), + { portRanges: portRangesText }, + )} + </ModalMessage> + </> + } /> </StyledSelectorContainer> <Cell.Footer> @@ -200,13 +236,9 @@ function ObfuscationSettings() { const obfuscationSettings = useSelector((state) => state.settings.obfuscationSettings); const obfuscationType = obfuscationSettings.selectedObfuscation; - const obfuscationTypeItems: ISelectorItem<ObfuscationType>[] = useMemo( + const obfuscationTypeItems: SelectorItem<ObfuscationType>[] = useMemo( () => [ { - label: messages.gettext('Automatic'), - value: ObfuscationType.auto, - }, - { label: messages.pgettext('wireguard-settings-view', 'On (UDP-over-TCP)'), value: ObfuscationType.udp2tcp, }, @@ -242,9 +274,10 @@ function ObfuscationSettings() { )} </ModalMessage> } - values={obfuscationTypeItems} + items={obfuscationTypeItems} value={obfuscationType} onSelect={selectObfuscationType} + automaticValue={ObfuscationType.auto} /> </StyledSelectorContainer> </AriaInputGroup> @@ -256,14 +289,10 @@ function Udp2tcpPortSetting() { const obfuscationSettings = useSelector((state) => state.settings.obfuscationSettings); const port = liftConstraint(obfuscationSettings.udp2tcpSettings.port); - const portItems: ISelectorItem<LiftedConstraint<number>>[] = useMemo(() => { - const automaticItem: ISelectorItem<LiftedConstraint<number>> = { - label: messages.gettext('Automatic'), - value: 'any', - }; - - return [automaticItem].concat(UDP2TCP_PORTS.map(mapPortToSelectorItem)); - }, []); + const portItems: SelectorItem<number>[] = useMemo( + () => UDP2TCP_PORTS.map(mapPortToSelectorItem), + [], + ); const selectPort = useCallback( async (port: LiftedConstraint<number>) => { @@ -292,12 +321,13 @@ function Udp2tcpPortSetting() { )} </ModalMessage> } - values={portItems} + items={portItems} value={port} onSelect={selectPort} disabled={obfuscationSettings.selectedObfuscation === ObfuscationType.off} expandable thinTitle + automaticValue={'any' as const} /> </StyledSelectorContainer> </AriaInputGroup> @@ -401,18 +431,13 @@ function IpVersionSetting() { const { updateRelaySettings } = useAppContext(); const relaySettings = useSelector((state) => state.settings.relaySettings); const ipVersion = useMemo(() => { - const ipVersion = - 'normal' in relaySettings ? relaySettings.normal.wireguard.ipVersion : undefined; - return ipVersion === 'any' ? undefined : ipVersion; + const ipVersion = 'normal' in relaySettings ? relaySettings.normal.wireguard.ipVersion : 'any'; + return ipVersion === 'any' ? null : ipVersion; }, [relaySettings]); - const ipVersionItems: ISelectorItem<OptionalIpVersion>[] = useMemo( + const ipVersionItems: SelectorItem<IpVersion>[] = useMemo( () => [ { - label: messages.gettext('Automatic'), - value: undefined, - }, - { label: messages.gettext('IPv4'), value: 'ipv4', }, @@ -425,10 +450,10 @@ function IpVersionSetting() { ); const setIpVersion = useCallback( - async (ipVersion?: IpVersion) => { + async (ipVersion: IpVersion | null) => { const relayUpdate = createWireguardRelayUpdater(relaySettings) .tunnel.wireguard((wireguard) => { - if (ipVersion) { + if (ipVersion !== null) { wireguard.ipVersion.exact(ipVersion); } else { wireguard.ipVersion.any(); @@ -451,9 +476,10 @@ function IpVersionSetting() { <Selector // TRANSLATORS: The title for the WireGuard IP version selector. title={messages.pgettext('wireguard-settings-view', 'IP version')} - values={ipVersionItems} + items={ipVersionItems} value={ipVersion} onSelect={setIpVersion} + automaticValue={null} /> </StyledSelectorContainer> <Cell.Footer> @@ -476,10 +502,6 @@ function IpVersionSetting() { ); } -function removeNonNumericCharacters(value: string) { - return value.replace(/[^0-9]/g, ''); -} - function mtuIsValid(mtu: string): boolean { const parsedMtu = mtu ? parseInt(mtu) : undefined; return ( diff --git a/gui/src/renderer/components/cell/Input.tsx b/gui/src/renderer/components/cell/Input.tsx index 81ba648338..e68fac424c 100644 --- a/gui/src/renderer/components/cell/Input.tsx +++ b/gui/src/renderer/components/cell/Input.tsx @@ -2,7 +2,7 @@ import React, { useCallback, useContext, useEffect, useRef, useState } from 'rea import styled from 'styled-components'; import { colors } from '../../../config.json'; -import { useBoolean } from '../../lib/utilityHooks'; +import { useBoolean, useCombinedRefs } from '../../lib/utilityHooks'; import { normalText } from '../common-styles'; import ImageView from '../ImageView'; import { BackAction } from '../KeyboardNavigation'; @@ -45,112 +45,96 @@ interface IInputProps extends React.InputHTMLAttributes<HTMLInputElement> { onChangeValue?: (value: string) => void; } -interface IInputState { - value?: string; - focused: boolean; -} - -export class Input extends React.Component<IInputProps, IInputState> { - public state = { - value: this.props.value ?? '', - focused: false, - }; - - public inputRef = React.createRef<HTMLInputElement>(); - - public componentDidUpdate(prevProps: IInputProps, _prevState: IInputState) { - if ( - !this.state.focused && - prevProps.value !== this.props.value && - this.props.value !== this.state.value - ) { - this.setState( - (_state, props) => ({ - value: props.value, - }), - () => { - this.props.onChangeValue?.(this.state.value); - }, - ); - } - } +function InputWithRef(props: IInputProps, forwardedRef: React.Ref<HTMLInputElement>) { + const { + validateValue, + modifyValue, + submitOnBlur, + onSubmitValue, + onChangeValue, + ...otherProps + } = props; - public render() { - const { - type: _type, - onChange: _onChange, - onFocus: _onFocus, - onBlur: _onBlur, - onKeyPress: _onKeyPress, - value: _value, - modifyValue: _modifyValue, - submitOnBlur: _submitOnBlur, - onChangeValue: _onChangeValue, - onSubmitValue: _onSubmitValue, - validateValue, - ...otherProps - } = this.props; + const [value, setValue] = useState(props.value ?? ''); + const [isFocused, setFocused, setBlurred] = useBoolean(false); - const valid = validateValue?.(this.state.value); + const inputRef = useRef() as React.RefObject<HTMLInputElement>; + const combinedRef = useCombinedRefs(inputRef, forwardedRef); - return ( - <CellDisabledContext.Consumer> - {(disabled) => ( - <StyledInput - ref={this.inputRef} - type="text" - valid={valid} - focused={this.state.focused} - aria-invalid={!valid} - onChange={this.onChange} - onFocus={this.onFocus} - onBlur={this.onBlur} - onKeyPress={this.onKeyPress} - value={this.state.value} - disabled={disabled} - {...otherProps} - /> - )} - </CellDisabledContext.Consumer> - ); - } + const onFocus = useCallback( + (event: React.FocusEvent<HTMLInputElement>) => { + setFocused(); + props.onFocus?.(event); + }, + [props.onFocus], + ); - public blur = () => { - this.inputRef.current?.blur(); - }; + const onBlur = useCallback( + (event: React.FocusEvent<HTMLInputElement>) => { + setBlurred(); + props.onBlur?.(event); - private onChange = (event: React.ChangeEvent<HTMLInputElement>) => { - const value = this.props.modifyValue?.(event.target.value) ?? event.target.value; - this.setState({ value }); - this.props.onChange?.(event); - this.props.onChangeValue?.(value); - }; + if (validateValue?.(value) !== false && submitOnBlur) { + onSubmitValue?.(value); + } + }, + [value, props.onBlur, validateValue, onSubmitValue, submitOnBlur], + ); - private onFocus = (event: React.FocusEvent<HTMLInputElement>) => { - this.setState({ focused: true }); - this.props.onFocus?.(event); - }; + const onChange = useCallback( + (event: React.ChangeEvent<HTMLInputElement>) => { + const value = modifyValue?.(event.target.value) ?? event.target.value; + setValue(value); + props.onChange?.(event); + onChangeValue?.(value); + }, + [value, modifyValue, props.onSubmit, onSubmitValue], + ); - private onBlur = (event: React.FocusEvent<HTMLInputElement>) => { - this.setState({ focused: false }); + const onKeyPress = useCallback( + (event: React.KeyboardEvent<HTMLInputElement>) => { + if (event.key === 'Enter') { + onSubmitValue?.(value); + inputRef.current?.blur(); + } + props.onKeyPress?.(event); + }, + [value, onSubmitValue, inputRef, props.onKeyPress], + ); - this.props.onBlur?.(event); - if (this.props.validateValue?.(this.state.value) !== false && this.props.submitOnBlur) { - this.props.onSubmitValue?.(this.state.value); - } else { - this.setState({ value: this.props.value }); + useEffect(() => { + if (!isFocused && props.value !== undefined && value !== props.value) { + setValue(props.value); + onChangeValue?.(props.value); } - }; + }, [props.value]); - private onKeyPress = (event: React.KeyboardEvent<HTMLInputElement>) => { - if (event.key === 'Enter') { - this.props.onSubmitValue?.(this.state.value); - this.inputRef.current?.blur(); - } - this.props.onKeyPress?.(event); - }; + const valid = validateValue?.(value); + + return ( + <CellDisabledContext.Consumer> + {(disabled) => ( + <StyledInput + {...otherProps} + ref={combinedRef} + type="text" + valid={valid} + focused={isFocused} + aria-invalid={!valid} + onChange={onChange} + onFocus={onFocus} + onBlur={onBlur} + onKeyPress={onKeyPress} + value={value} + disabled={disabled} + /> + )} + </CellDisabledContext.Consumer> + ); } +export const Input = React.memo(React.forwardRef(InputWithRef)); + const InputFrame = styled.div((props: { focused: boolean }) => ({ display: 'flex', flexGrow: 0, @@ -177,12 +161,13 @@ const StyledAutoSizingTextInputWrapper = styled.div({ height: '100%', }); -export function AutoSizingTextInput(props: IInputProps) { +function AutoSizingTextInputWithRef(props: IInputProps, forwardedRef: React.Ref<HTMLInputElement>) { const { onChangeValue, onFocus, onBlur, ...otherProps } = props; const [value, setValue] = useState(otherProps.value ?? ''); const [focused, setFocused, setBlurred] = useBoolean(false); - const inputRef = useRef() as React.RefObject<Input>; + const inputRef = useRef() as React.RefObject<HTMLInputElement>; + const combinedRef = useCombinedRefs(inputRef, forwardedRef); const onChangeValueWrapper = useCallback( (value: string) => { @@ -216,7 +201,7 @@ export function AutoSizingTextInput(props: IInputProps) { <StyledAutoSizingTextInputContainer> <StyledAutoSizingTextInputWrapper> <Input - ref={inputRef} + ref={combinedRef} onChangeValue={onChangeValueWrapper} onBlur={onBlurWrapper} onFocus={onFocusWrapper} @@ -232,6 +217,8 @@ export function AutoSizingTextInput(props: IInputProps) { ); } +export const AutoSizingTextInput = React.memo(React.forwardRef(AutoSizingTextInputWithRef)); + const StyledCellInputRowContainer = styled(Container)({ backgroundColor: 'white', marginBottom: '1px', diff --git a/gui/src/renderer/components/cell/Selector.tsx b/gui/src/renderer/components/cell/Selector.tsx index e815519552..d898e3f51d 100644 --- a/gui/src/renderer/components/cell/Selector.tsx +++ b/gui/src/renderer/components/cell/Selector.tsx @@ -1,7 +1,8 @@ -import * as React from 'react'; +import { useCallback, useEffect, useRef, useState } from 'react'; import styled from 'styled-components'; import { colors } from '../../../config.json'; +import { messages } from '../../../shared/gettext'; import { useBoolean } from '../../lib/utilityHooks'; import Accordion from '../Accordion'; import { AriaDetails, AriaInput, AriaLabel } from '../AriaGroup'; @@ -24,44 +25,69 @@ const StyledChevronButton = styled(ChevronButton)({ marginRight: '16px', }); -export interface ISelectorItem<T> { +export interface SelectorItem<T> { label: string; value: T; disabled?: boolean; } -interface ISelectorProps<T> { +// T represents the available values and U represent the value of "Automatic"/"Any" if there is one. +interface CommonSelectorProps<T, U> { title?: string; - values: Array<ISelectorItem<T>>; - value: T; - onSelect: (value: T) => void; - selectedCellRef?: React.Ref<HTMLButtonElement>; + items: Array<SelectorItem<T>>; + value: T | U; + selectedCellRef?: React.Ref<HTMLElement>; className?: string; details?: React.ReactElement; expandable?: boolean; disabled?: boolean; thinTitle?: boolean; + automaticLabel?: string; + automaticValue?: U; + children?: React.ReactNode | Array<React.ReactNode>; +} + +interface SelectorProps<T, U> extends CommonSelectorProps<T, U> { + onSelect: (value: T | U) => void; } -export default function Selector<T>(props: ISelectorProps<T>) { +export default function Selector<T, U>(props: SelectorProps<T, U>) { const [expanded, , , toggleExpanded] = useBoolean(!props.expandable); - const items = props.values.map((item, i) => { - const selected = item.value === props.value; + const items = props.items.map((item) => { + const selected = props.value === item.value; + const ref = selected ? (props.selectedCellRef as React.Ref<HTMLButtonElement>) : undefined; return ( <SelectorCell - key={i} + key={`value-${item.value}`} value={item.value} - selected={selected} + isSelected={selected} disabled={props.disabled || item.disabled} - forwardedRef={selected ? props.selectedCellRef : undefined} + forwardedRef={ref} onSelect={props.onSelect}> {item.label} </SelectorCell> ); }); + if (props.automaticValue !== undefined) { + const selected = props.value === props.automaticValue; + const ref = selected ? (props.selectedCellRef as React.Ref<HTMLButtonElement>) : undefined; + + items.unshift( + <SelectorCell + key={'automatic'} + value={props.automaticValue} + isSelected={selected} + disabled={props.disabled} + forwardedRef={ref} + onSelect={props.onSelect}> + {props.automaticLabel ?? messages.gettext('Automatic')} + </SelectorCell>, + ); + } + const title = props.title && ( <StyledTitle> <AriaLabel> @@ -78,11 +104,19 @@ export default function Selector<T>(props: ISelectorProps<T>) { </StyledTitle> ); + // Add potential additional items to the list. Used for custom entry. + const children = ( + <> + {items} + {props.children} + </> + ); + return ( <AriaInput> <Cell.Section role="listbox" className={props.className}> {title} - {props.expandable ? <Accordion expanded={expanded}>{items}</Accordion> : items} + {props.expandable ? <Accordion expanded={expanded}>{children}</Accordion> : children} </Cell.Section> </AriaInput> ); @@ -97,40 +131,155 @@ const StyledLabel = styled(Cell.Label)(normalText, { fontWeight: 400, }); -interface ISelectorCellProps<T> { +interface SelectorCellProps<T> { value: T; - selected: boolean; + isSelected: boolean; disabled?: boolean; onSelect: (value: T) => void; - children?: React.ReactText; + children: React.ReactNode | Array<React.ReactNode>; forwardedRef?: React.Ref<HTMLButtonElement>; } -class SelectorCell<T> extends React.Component<ISelectorCellProps<T>> { - public render() { - return ( - <Cell.CellButton - ref={this.props.forwardedRef} - onClick={this.onClick} - selected={this.props.selected} - disabled={this.props.disabled} +function SelectorCell<T>(props: SelectorCellProps<T>) { + const handleClick = useCallback(() => { + if (!props.isSelected) { + props.onSelect(props.value); + } + }, [props.isSelected, props.onSelect, props.value]); + + return ( + <Cell.CellButton + ref={props.forwardedRef} + onClick={handleClick} + selected={props.isSelected} + disabled={props.disabled} + role="option" + aria-selected={props.isSelected} + aria-disabled={props.disabled}> + <StyledCellIcon + visible={props.isSelected} + source="icon-tick" + width={18} + tintColor={colors.white} + /> + <StyledLabel>{props.children}</StyledLabel> + </Cell.CellButton> + ); +} + +interface StyledCustomContainerProps { + selected: boolean; +} + +const StyledCustomContainer = styled(Cell.Container)((props: StyledCustomContainerProps) => ({ + minHeight: '44px', + backgroundColor: props.selected ? colors.green : colors.blue40, + ':hover': { + backgroundColor: props.selected ? colors.green : colors.blue, + }, +})); + +// Adding undefined as possible value of the selector to be able to select nothing. +interface SelectorWithCustomItemProps<T, U> extends CommonSelectorProps<T | undefined, U> { + inputPlaceholder: string; + onSelect: (value: T | U) => void; + onSelectCustom: (value: string) => void; + maxLength?: number; + selectedCellRef?: React.Ref<HTMLDivElement>; + validateValue?: (value: string) => boolean; + modifyValue?: (value: string) => string; +} + +export function SelectorWithCustomItem<T, U>(props: SelectorWithCustomItemProps<T, U>) { + const { + value, + inputPlaceholder, + onSelect, + onSelectCustom, + maxLength, + selectedCellRef, + validateValue, + modifyValue, + ...otherProps + } = props; + + // The component needs to keep track of when the custom item should look selected before it has a + // value. + const [customIsSelectedWithoutValue, setCustomIsSelectedWithoutValue] = useState(false); + const inputRef = useRef() as React.RefObject<HTMLInputElement>; + + const itemIsSelected = + props.items.some((item) => item.value === value) || props.automaticValue === value; + const customIsSelected = !itemIsSelected || customIsSelectedWithoutValue; + + const handleClick = useCallback(() => { + if (!customIsSelected) { + setCustomIsSelectedWithoutValue(true); + inputRef.current?.focus(); + } + }, [customIsSelected, inputRef.current]); + + // Wrap onSelect to be able to catch when a new value is selected during the + // customIsSelectedWithoutValue phase. + const handleSelectValue = useCallback( + (newValue: T | U | undefined) => { + if (customIsSelectedWithoutValue && newValue === value) { + setCustomIsSelectedWithoutValue(false); + } else if (newValue !== undefined) { + onSelect(newValue); + } + }, + [customIsSelected, value, onSelect], + ); + + const handleSubmit = useCallback((value: string) => { + if (validateValue?.(value) !== false) { + onSelectCustom(value); + } + }, []); + + // If props.value changes while customIsSelectedWithoutValue then we want to switch to that value + // instead. + useEffect(() => { + if (customIsSelected) { + setCustomIsSelectedWithoutValue(false); + } + }, [value]); + + return ( + <Selector<T | undefined, U> + {...otherProps} + onSelect={handleSelectValue} + value={customIsSelected ? undefined : value}> + <StyledCustomContainer + ref={customIsSelected ? props.selectedCellRef : undefined} + onClick={handleClick} + selected={customIsSelected} + disabled={props.disabled} role="option" - aria-selected={this.props.selected} - aria-disabled={this.props.disabled}> + aria-selected={customIsSelected} + aria-disabled={props.disabled}> <StyledCellIcon - visible={this.props.selected} + visible={customIsSelected} source="icon-tick" width={18} tintColor={colors.white} /> - <StyledLabel>{this.props.children}</StyledLabel> - </Cell.CellButton> - ); - } - - private onClick = () => { - if (!this.props.selected) { - this.props.onSelect(this.props.value); - } - }; + <StyledLabel>{messages.gettext('Custom')}</StyledLabel> + <AriaInput> + <Cell.AutoSizingTextInput + ref={inputRef} + value={itemIsSelected || customIsSelectedWithoutValue ? '' : `${props.value}`} + placeholder={inputPlaceholder} + inputMode={'numeric'} + maxLength={maxLength ?? 4} + onSubmitValue={handleSubmit} + submitOnBlur={true} + validateValue={validateValue} + modifyValue={modifyValue} + /> + </AriaInput> + </StyledCustomContainer> + </Selector> + ); } diff --git a/gui/src/renderer/redux/settings/actions.ts b/gui/src/renderer/redux/settings/actions.ts index 8126f30205..c6e5aed985 100644 --- a/gui/src/renderer/redux/settings/actions.ts +++ b/gui/src/renderer/redux/settings/actions.ts @@ -1,5 +1,10 @@ import { IWindowsApplication } from '../../../shared/application-types'; -import { BridgeState, IDnsOptions, ObfuscationSettings } from '../../../shared/daemon-rpc-types'; +import { + BridgeState, + IDnsOptions, + IWireguardEndpointData, + ObfuscationSettings, +} from '../../../shared/daemon-rpc-types'; import { IGuiSettingsState } from '../../../shared/gui-settings-state'; import { BridgeSettingsRedux, IRelayLocationRedux, RelaySettingsRedux } from './reducers'; @@ -23,6 +28,11 @@ export interface IUpdateBridgeLocationsAction { bridgeLocations: IRelayLocationRedux[]; } +export interface IUpdateWireguardEndpointData { + type: 'UPDATE_WIREGUARD_ENDPOINT_DATA'; + wireguardEndpointData: IWireguardEndpointData; +} + export interface IUpdateAllowLanAction { type: 'UPDATE_ALLOW_LAN'; allowLan: boolean; @@ -93,6 +103,7 @@ export type SettingsAction = | IUpdateRelayAction | IUpdateRelayLocationsAction | IUpdateBridgeLocationsAction + | IUpdateWireguardEndpointData | IUpdateAllowLanAction | IUpdateEnableIpv6Action | IUpdateBlockWhenDisconnectedAction @@ -137,6 +148,15 @@ function updateBridgeLocations( }; } +function updateWireguardEndpointData( + wireguardEndpointData: IWireguardEndpointData, +): IUpdateWireguardEndpointData { + return { + type: 'UPDATE_WIREGUARD_ENDPOINT_DATA', + wireguardEndpointData, + }; +} + function updateAllowLan(allowLan: boolean): IUpdateAllowLanAction { return { type: 'UPDATE_ALLOW_LAN', @@ -239,6 +259,7 @@ export default { updateRelay, updateRelayLocations, updateBridgeLocations, + updateWireguardEndpointData, updateAllowLan, updateEnableIpv6, updateBlockWhenDisconnected, diff --git a/gui/src/renderer/redux/settings/reducers.ts b/gui/src/renderer/redux/settings/reducers.ts index ee9d81e37f..e4437563ec 100644 --- a/gui/src/renderer/redux/settings/reducers.ts +++ b/gui/src/renderer/redux/settings/reducers.ts @@ -3,6 +3,7 @@ import { BridgeState, IDnsOptions, IpVersion, + IWireguardEndpointData, LiftedConstraint, ObfuscationSettings, ObfuscationType, @@ -82,6 +83,7 @@ export interface ISettingsReduxState { relaySettings: RelaySettingsRedux; relayLocations: IRelayLocationRedux[]; bridgeLocations: IRelayLocationRedux[]; + wireguardEndpointData: IWireguardEndpointData; allowLan: boolean; enableIpv6: boolean; bridgeSettings: BridgeSettingsRedux; @@ -127,6 +129,7 @@ const initialState: ISettingsReduxState = { }, relayLocations: [], bridgeLocations: [], + wireguardEndpointData: { portRanges: [], udp2tcpPorts: [] }, allowLan: false, enableIpv6: true, bridgeSettings: { @@ -191,6 +194,12 @@ export default function ( bridgeLocations: action.bridgeLocations, }; + case 'UPDATE_WIREGUARD_ENDPOINT_DATA': + return { + ...state, + wireguardEndpointData: action.wireguardEndpointData, + }; + case 'UPDATE_ALLOW_LAN': return { ...state, diff --git a/gui/src/shared/daemon-rpc-types.ts b/gui/src/shared/daemon-rpc-types.ts index 5893e4def6..6b75ee7d3c 100644 --- a/gui/src/shared/daemon-rpc-types.ts +++ b/gui/src/shared/daemon-rpc-types.ts @@ -119,7 +119,7 @@ export interface IProxyEndpoint { export type DaemonEvent = | { tunnelState: TunnelState } | { settings: ISettings } - | { relayList: IRelayList } + | { relayList: IRelayListWithEndpointData } | { appVersionInfo: IAppVersionInfo } | { device: DeviceEvent } | { deviceRemoval: Array<IDevice> }; @@ -224,10 +224,20 @@ export type RelaySettingsUpdate = customTunnelEndpoint: IRelaySettingsCustom; }; +export interface IRelayListWithEndpointData { + relayList: IRelayList; + wireguardEndpointData: IWireguardEndpointData; +} + export interface IRelayList { countries: IRelayListCountry[]; } +export interface IWireguardEndpointData { + portRanges: [number, number][]; + udp2tcpPorts: number[]; +} + export interface IRelayListCountry { name: string; code: string; diff --git a/gui/src/shared/ipc-schema.ts b/gui/src/shared/ipc-schema.ts index 33ac82180e..85c7f2cabc 100644 --- a/gui/src/shared/ipc-schema.ts +++ b/gui/src/shared/ipc-schema.ts @@ -15,6 +15,7 @@ import { ILocation, IRelayList, ISettings, + IWireguardEndpointData, ObfuscationSettings, RelaySettingsUpdate, TunnelState, @@ -45,6 +46,7 @@ export interface ITranslations { export interface IRelayListPair { relays: IRelayList; bridges: IRelayList; + wireguardEndpointData: IWireguardEndpointData; } export type LaunchApplicationResult = { success: true } | { error: string }; diff --git a/gui/src/shared/string-helpers.ts b/gui/src/shared/string-helpers.ts index c69ebddfcf..983a8e8796 100644 --- a/gui/src/shared/string-helpers.ts +++ b/gui/src/shared/string-helpers.ts @@ -5,3 +5,7 @@ export function capitalize(inputString: string): string { export function capitalizeEveryWord(inputString: string): string { return inputString.split(' ').map(capitalize).join(' '); } + +export function removeNonNumericCharacters(value: string) { + return value.replace(/[^0-9]/g, ''); +} diff --git a/gui/test/e2e/setup/main.ts b/gui/test/e2e/setup/main.ts index f65169ee44..f89e4a3835 100644 --- a/gui/test/e2e/setup/main.ts +++ b/gui/test/e2e/setup/main.ts @@ -10,6 +10,7 @@ import { IAppVersionInfo, ILocation, IRelayList, + IWireguardEndpointData, } from '../../../src/shared/daemon-rpc-types'; import { messages, relayLocations } from '../../../src/shared/gettext'; import { IGuiSettingsState } from '../../../src/shared/gui-settings-state'; @@ -101,6 +102,11 @@ class ApplicationMain { ], }; + private wireguardEndpointData: IWireguardEndpointData = { + portRanges: [], + udp2tcpPorts: [], + }; + public constructor() { app.enableSandbox(); app.on('ready', this.onReady); @@ -153,7 +159,11 @@ class ApplicationMain { settings: this.settings, isPerformingPostUpgrade: false, deviceState: this.deviceState, - relayListPair: { relays: this.relayList, bridges: this.relayList }, + relayListPair: { + relays: this.relayList, + bridges: this.relayList, + wireguardEndpointData: this.wireguardEndpointData, + }, currentVersion: this.currentVersion, upgradeVersion: this.upgradeVersion, guiSettings: this.guiSettings, |
