summaryrefslogtreecommitdiffhomepage
path: root/gui/src
diff options
context:
space:
mode:
authorOskar Nyberg <oskar@mullvad.net>2022-07-25 11:40:24 +0200
committerOskar Nyberg <oskar@mullvad.net>2022-07-25 11:40:24 +0200
commitd7d89095619d0bc66c2abc8e382c44969ab57bdf (patch)
treee4a942d5a17278c47d255bb2eba53b8d662fcdc7 /gui/src
parent9fa4063cfba1bd9bf44ad54bce650284522d6ebb (diff)
parentb570bf5ff968edd915c05747b8711461cd31945a (diff)
downloadmullvadvpn-d7d89095619d0bc66c2abc8e382c44969ab57bdf.tar.xz
mullvadvpn-d7d89095619d0bc66c2abc8e382c44969ab57bdf.zip
Merge branch 'refactor-remaining-settings-views'
Diffstat (limited to 'gui/src')
-rw-r--r--gui/src/renderer/app.tsx12
-rw-r--r--gui/src/renderer/components/Account.tsx7
-rw-r--r--gui/src/renderer/components/AccountStyles.tsx6
-rw-r--r--gui/src/renderer/components/AppRouter.tsx8
-rw-r--r--gui/src/renderer/components/Filter.tsx10
-rw-r--r--gui/src/renderer/components/InterfaceSettings.tsx11
-rw-r--r--gui/src/renderer/components/OpenVPNSettings.tsx719
-rw-r--r--gui/src/renderer/components/ProblemReport.tsx7
-rw-r--r--gui/src/renderer/components/ProblemReportStyles.tsx5
-rw-r--r--gui/src/renderer/components/SelectLanguage.tsx11
-rw-r--r--gui/src/renderer/components/SelectLocation.tsx7
-rw-r--r--gui/src/renderer/components/SelectLocationStyles.tsx5
-rw-r--r--gui/src/renderer/components/Settings.tsx7
-rw-r--r--gui/src/renderer/components/SettingsStyles.tsx5
-rw-r--r--gui/src/renderer/components/SplitTunnelingSettings.tsx7
-rw-r--r--gui/src/renderer/components/SplitTunnelingSettingsStyles.tsx5
-rw-r--r--gui/src/renderer/components/Support.tsx12
-rw-r--r--gui/src/renderer/components/VpnSettings.tsx12
-rw-r--r--gui/src/renderer/components/WireguardSettings.tsx621
-rw-r--r--gui/src/renderer/containers/OpenVPNSettingsPage.tsx136
-rw-r--r--gui/src/renderer/containers/WireguardSettingsPage.tsx115
-rw-r--r--gui/src/renderer/lib/utilityHooks.ts2
22 files changed, 821 insertions, 909 deletions
diff --git a/gui/src/renderer/app.tsx b/gui/src/renderer/app.tsx
index 7d269b9bdd..ec69b5d8a7 100644
--- a/gui/src/renderer/app.tsx
+++ b/gui/src/renderer/app.tsx
@@ -436,11 +436,11 @@ export default class AppRenderer {
actions.settings.updateEnableIpv6(enableIpv6);
};
- public async setBridgeState(bridgeState: BridgeState) {
+ public setBridgeState = async (bridgeState: BridgeState) => {
const actions = this.reduxActions;
await IpcRendererEventChannel.settings.setBridgeState(bridgeState);
actions.settings.updateBridgeState(bridgeState);
- }
+ };
public setBlockWhenDisconnected = async (blockWhenDisconnected: boolean) => {
const actions = this.reduxActions;
@@ -448,17 +448,17 @@ export default class AppRenderer {
actions.settings.updateBlockWhenDisconnected(blockWhenDisconnected);
};
- public async setOpenVpnMssfix(mssfix?: number) {
+ public setOpenVpnMssfix = async (mssfix?: number) => {
const actions = this.reduxActions;
actions.settings.updateOpenVpnMssfix(mssfix);
await IpcRendererEventChannel.settings.setOpenVpnMssfix(mssfix);
- }
+ };
- public async setWireguardMtu(mtu?: number) {
+ public setWireguardMtu = async (mtu?: number) => {
const actions = this.reduxActions;
actions.settings.updateWireguardMtu(mtu);
await IpcRendererEventChannel.settings.setWireguardMtu(mtu);
- }
+ };
public setAutoConnect(autoConnect: boolean) {
IpcRendererEventChannel.guiSettings.setAutoConnect(autoConnect);
diff --git a/gui/src/renderer/components/Account.tsx b/gui/src/renderer/components/Account.tsx
index e0596a2b79..eabde95dbb 100644
--- a/gui/src/renderer/components/Account.tsx
+++ b/gui/src/renderer/components/Account.tsx
@@ -13,7 +13,6 @@ import {
AccountRowValue,
DeviceRowValue,
StyledBuyCreditButton,
- StyledContainer,
StyledRedeemVoucherButton,
StyledSpinnerContainer,
} from './AccountStyles';
@@ -22,7 +21,7 @@ import * as AppButton from './AppButton';
import { AriaDescribed, AriaDescription, AriaDescriptionGroup } from './AriaGroup';
import ImageView from './ImageView';
import { BackAction } from './KeyboardNavigation';
-import { Layout } from './Layout';
+import { Layout, SettingsContainer } from './Layout';
import { ModalAlert, ModalAlertType, ModalMessage } from './Modal';
import { NavigationBar, NavigationItems, TitleBarItem } from './NavigationBar';
import SettingsHeader, { HeaderTitle } from './SettingsHeader';
@@ -57,7 +56,7 @@ export default class Account extends React.Component<IProps, IState> {
return (
<BackAction action={this.props.onClose}>
<Layout>
- <StyledContainer>
+ <SettingsContainer>
<NavigationBar>
<NavigationItems>
<TitleBarItem>
@@ -131,7 +130,7 @@ export default class Account extends React.Component<IProps, IState> {
</AppButton.RedButton>
</AccountFooter>
</AccountContainer>
- </StyledContainer>
+ </SettingsContainer>
{this.renderLogoutDialog()}
</Layout>
diff --git a/gui/src/renderer/components/AccountStyles.tsx b/gui/src/renderer/components/AccountStyles.tsx
index cccb1c95af..5e736a1ded 100644
--- a/gui/src/renderer/components/AccountStyles.tsx
+++ b/gui/src/renderer/components/AccountStyles.tsx
@@ -3,14 +3,8 @@ import styled from 'styled-components';
import { colors } from '../../config.json';
import * as AppButton from './AppButton';
import { normalText, tinyText } from './common-styles';
-import { Container } from './Layout';
import { RedeemVoucherButton } from './RedeemVoucher';
-export const StyledContainer = styled(Container)({
- backgroundColor: colors.darkBlue,
- flexDirection: 'column',
-});
-
export const AccountContainer = styled.div({
display: 'flex',
flexDirection: 'column',
diff --git a/gui/src/renderer/components/AppRouter.tsx b/gui/src/renderer/components/AppRouter.tsx
index 75892805e9..0457287744 100644
--- a/gui/src/renderer/components/AppRouter.tsx
+++ b/gui/src/renderer/components/AppRouter.tsx
@@ -4,11 +4,9 @@ import { Route, Switch } from 'react-router';
import AccountPage from '../containers/AccountPage';
import LoginPage from '../containers/LoginPage';
-import OpenVPNSettingsPage from '../containers/OpenVPNSettingsPage';
import ProblemReportPage from '../containers/ProblemReportPage';
import SelectLanguagePage from '../containers/SelectLanguagePage';
import SelectLocationPage from '../containers/SelectLocationPage';
-import WireguardSettingsPage from '../containers/WireguardSettingsPage';
import withAppContext, { IAppContext } from '../context';
import { IHistoryProps, ITransitionSpecification, transitions, withHistory } from '../lib/history';
import { RoutePath } from '../lib/routes';
@@ -24,12 +22,14 @@ import Focus, { IFocusHandle } from './Focus';
import InterfaceSettings from './InterfaceSettings';
import Launch from './Launch';
import MainView from './MainView';
+import OpenVpnSettings from './OpenVpnSettings';
import Settings from './Settings';
import SplitTunnelingSettings from './SplitTunnelingSettings';
import Support from './Support';
import TooManyDevices from './TooManyDevices';
import TransitionContainer, { TransitionView } from './TransitionContainer';
import VpnSettings from './VpnSettings';
+import WireguardSettings from './WireguardSettings';
interface IAppRoutesState {
currentLocation: IHistoryProps['history']['location'];
@@ -92,8 +92,8 @@ class AppRouter extends React.Component<IHistoryProps & IAppContext, IAppRoutesS
<Route exact path={RoutePath.accountSettings} component={AccountPage} />
<Route exact path={RoutePath.interfaceSettings} component={InterfaceSettings} />
<Route exact path={RoutePath.vpnSettings} component={VpnSettings} />
- <Route exact path={RoutePath.wireguardSettings} component={WireguardSettingsPage} />
- <Route exact path={RoutePath.openVpnSettings} component={OpenVPNSettingsPage} />
+ <Route exact path={RoutePath.wireguardSettings} component={WireguardSettings} />
+ <Route exact path={RoutePath.openVpnSettings} component={OpenVpnSettings} />
<Route exact path={RoutePath.splitTunneling} component={SplitTunnelingSettings} />
<Route exact path={RoutePath.support} component={Support} />
<Route exact path={RoutePath.problemReport} component={ProblemReportPage} />
diff --git a/gui/src/renderer/components/Filter.tsx b/gui/src/renderer/components/Filter.tsx
index 1798985070..86e0d2dbb3 100644
--- a/gui/src/renderer/components/Filter.tsx
+++ b/gui/src/renderer/components/Filter.tsx
@@ -18,7 +18,7 @@ import Selector from './cell/Selector';
import { normalText } from './common-styles';
import ImageView from './ImageView';
import { BackAction } from './KeyboardNavigation';
-import { Container, Layout } from './Layout';
+import { Layout, SettingsContainer } from './Layout';
import {
NavigationBar,
NavigationContainer,
@@ -27,10 +27,6 @@ import {
TitleBarItem,
} from './NavigationBar';
-const StyledContainer = styled(Container)({
- backgroundColor: colors.darkBlue,
-});
-
const StyledNavigationScrollbars = styled(NavigationScrollbars)({
backgroundColor: colors.darkBlue,
flex: 1,
@@ -81,7 +77,7 @@ export default function Filter() {
return (
<BackAction action={history.pop}>
<Layout>
- <StyledContainer>
+ <SettingsContainer>
<NavigationContainer>
<NavigationBar alwaysDisplayBarTitle={true}>
<NavigationItems>
@@ -113,7 +109,7 @@ export default function Filter() {
</AppButton.GreenButton>
</StyledFooter>
</NavigationContainer>
- </StyledContainer>
+ </SettingsContainer>
</Layout>
</BackAction>
);
diff --git a/gui/src/renderer/components/InterfaceSettings.tsx b/gui/src/renderer/components/InterfaceSettings.tsx
index bc7f823e7c..4598c1058e 100644
--- a/gui/src/renderer/components/InterfaceSettings.tsx
+++ b/gui/src/renderer/components/InterfaceSettings.tsx
@@ -1,7 +1,6 @@
import { useCallback } from 'react';
import styled from 'styled-components';
-import { colors } from '../../config.json';
import { messages } from '../../shared/gettext';
import { useAppContext } from '../context';
import { useHistory } from '../lib/history';
@@ -10,7 +9,7 @@ import { useSelector } from '../redux/store';
import { AriaDescription, AriaInput, AriaInputGroup, AriaLabel } from './AriaGroup';
import * as Cell from './cell';
import { BackAction } from './KeyboardNavigation';
-import { Container, Layout } from './Layout';
+import { Layout, SettingsContainer } from './Layout';
import {
NavigationBar,
NavigationContainer,
@@ -20,10 +19,6 @@ import {
} from './NavigationBar';
import SettingsHeader, { HeaderTitle } from './SettingsHeader';
-const StyledContainer = styled(Container)({
- backgroundColor: colors.darkBlue,
-});
-
const StyledContent = styled.div({
display: 'flex',
flexDirection: 'column',
@@ -42,7 +37,7 @@ export default function InterfaceSettings() {
return (
<BackAction action={pop}>
<Layout>
- <StyledContainer>
+ <SettingsContainer>
<NavigationContainer>
<NavigationBar>
<NavigationItems>
@@ -89,7 +84,7 @@ export default function InterfaceSettings() {
</StyledContent>
</NavigationScrollbars>
</NavigationContainer>
- </StyledContainer>
+ </SettingsContainer>
</Layout>
</BackAction>
);
diff --git a/gui/src/renderer/components/OpenVPNSettings.tsx b/gui/src/renderer/components/OpenVPNSettings.tsx
index 1b82c9971a..fce949c6a9 100644
--- a/gui/src/renderer/components/OpenVPNSettings.tsx
+++ b/gui/src/renderer/components/OpenVPNSettings.tsx
@@ -1,11 +1,17 @@
-import * as React from 'react';
+import { useCallback, useMemo } from 'react';
import { sprintf } from 'sprintf-js';
import styled from 'styled-components';
import { strings } from '../../config.json';
-import { BridgeState, RelayProtocol } from '../../shared/daemon-rpc-types';
+import { BridgeState, RelayProtocol, TunnelProtocol } from '../../shared/daemon-rpc-types';
import { messages } from '../../shared/gettext';
+import log from '../../shared/logging';
+import RelaySettingsBuilder from '../../shared/relay-settings-builder';
+import { useAppContext } from '../context';
+import { useHistory } from '../lib/history';
+import { useBoolean } from '../lib/utilityHooks';
import { formatMarkdown } from '../markdown-formatter';
+import { useSelector } from '../redux/store';
import * as AppButton from './AppButton';
import { AriaDescription, AriaInput, AriaInputGroup, AriaLabel } from './AriaGroup';
import * as Cell from './cell';
@@ -49,226 +55,86 @@ export const StyledSelectorContainer = styled.div({
flex: 0,
});
-interface IProps {
- bridgeModeAvailablity: BridgeModeAvailability;
- openvpn: {
- protocol?: RelayProtocol;
- port?: number;
- };
- mssfix?: number;
- bridgeState: BridgeState;
- setOpenVpnMssfix: (value: number | undefined) => void;
- setOpenVpnRelayProtocolAndPort: (protocol?: RelayProtocol, port?: number) => void;
- setBridgeState: (value: BridgeState) => void;
- onClose: () => void;
-}
-
-interface IState {
- showBridgeStateConfirmationDialog: boolean;
-}
-
-export default class OpenVpnSettings extends React.Component<IProps, IState> {
- public state = { showBridgeStateConfirmationDialog: false };
+export default function OpenVpnSettings() {
+ const { pop } = useHistory();
- private portItems: { [key in RelayProtocol]: Array<ISelectorItem<OptionalPort>> };
+ const relaySettings = useSelector((state) => state.settings.relaySettings);
- constructor(props: IProps) {
- super(props);
+ const protocol = useMemo(() => {
+ const protocol = 'normal' in relaySettings ? relaySettings.normal.openvpn.protocol : undefined;
+ return protocol === 'any' ? undefined : protocol;
+ }, [relaySettings]);
- const automaticPort: ISelectorItem<OptionalPort> = {
- label: messages.gettext('Automatic'),
- value: undefined,
- };
+ return (
+ <BackAction action={pop}>
+ <Layout>
+ <SettingsContainer>
+ <NavigationContainer>
+ <NavigationBar>
+ <NavigationItems>
+ <TitleBarItem>
+ {sprintf(
+ // TRANSLATORS: Title label in navigation bar
+ // TRANSLATORS: Available placeholders:
+ // TRANSLATORS: %(openvpn)s - Will be replaced with "OpenVPN"
+ messages.pgettext('openvpn-settings-nav', '%(openvpn)s settings'),
+ { openvpn: strings.openvpn },
+ )}
+ </TitleBarItem>
+ </NavigationItems>
+ </NavigationBar>
- this.portItems = {
- udp: [automaticPort].concat(UDP_PORTS.map(mapPortToSelectorItem)),
- tcp: [automaticPort].concat(TCP_PORTS.map(mapPortToSelectorItem)),
- };
- }
+ <NavigationScrollbars>
+ <SettingsHeader>
+ <HeaderTitle>
+ {sprintf(
+ // TRANSLATORS: %(openvpn)s will be replaced with "OpenVPN"
+ messages.pgettext('openvpn-settings-view', '%(openvpn)s settings'),
+ {
+ openvpn: strings.openvpn,
+ },
+ )}
+ </HeaderTitle>
+ </SettingsHeader>
- public render() {
- return (
- <BackAction action={this.props.onClose}>
- <Layout>
- <SettingsContainer>
- <NavigationContainer>
- <NavigationBar>
- <NavigationItems>
- <TitleBarItem>
- {sprintf(
- // TRANSLATORS: Title label in navigation bar
- // TRANSLATORS: Available placeholders:
- // TRANSLATORS: %(openvpn)s - Will be replaced with "OpenVPN"
- messages.pgettext('openvpn-settings-nav', '%(openvpn)s settings'),
- { openvpn: strings.openvpn },
- )}
- </TitleBarItem>
- </NavigationItems>
- </NavigationBar>
-
- <StyledNavigationScrollbars>
- <SettingsHeader>
- <HeaderTitle>
- {sprintf(
- // TRANSLATORS: %(openvpn)s will be replaced with "OpenVPN"
- messages.pgettext('openvpn-settings-view', '%(openvpn)s settings'),
- {
- openvpn: strings.openvpn,
- },
- )}
- </HeaderTitle>
- </SettingsHeader>
+ <Cell.Group>
+ <TransportProtocolSelector />
+ </Cell.Group>
+ {protocol ? (
<Cell.Group>
- <StyledSelectorContainer>
- <AriaInputGroup>
- <Selector
- title={messages.pgettext('openvpn-settings-view', 'Transport protocol')}
- values={this.protocolItems(this.props.bridgeState !== 'on')}
- value={this.props.openvpn.protocol}
- onSelect={this.onSelectOpenvpnProtocol}
- />
- {this.props.bridgeState === 'on' && (
- <Cell.Footer>
- <AriaDescription>
- <Cell.FooterText>
- {formatMarkdown(
- // TRANSLATORS: This is used to instruct users how to make UDP mode
- // TRANSLATORS: available.
- messages.pgettext(
- 'openvpn-settings-view',
- 'To activate UDP, change **Bridge mode** to **Automatic** or **Off**.',
- ),
- )}
- </Cell.FooterText>
- </AriaDescription>
- </Cell.Footer>
- )}
- </AriaInputGroup>
- </StyledSelectorContainer>
+ <PortSelector />
</Cell.Group>
+ ) : undefined}
- {this.props.openvpn.protocol ? (
- <Cell.Group>
- <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: this.props.openvpn.protocol.toUpperCase(),
- },
- )}
- values={this.portItems[this.props.openvpn.protocol]}
- value={this.props.openvpn.port}
- onSelect={this.onSelectOpenVpnPort}
- />
- </AriaInputGroup>
- </StyledSelectorContainer>
- </Cell.Group>
- ) : undefined}
+ <Cell.Group>
+ <BridgeModeSelector />
+ </Cell.Group>
- <Cell.Group>
- <AriaInputGroup>
- <StyledSelectorContainer>
- <Selector
- title={
- // TRANSLATORS: The title for the shadowsocks bridge selector section.
- messages.pgettext('openvpn-settings-view', 'Bridge mode')
- }
- values={this.bridgeStateItems(
- this.props.bridgeModeAvailablity === BridgeModeAvailability.available,
- )}
- value={this.props.bridgeState}
- onSelect={this.onSelectBridgeState}
- />
- </StyledSelectorContainer>
- <Cell.Footer>
- <AriaDescription>
- <Cell.FooterText>{this.bridgeModeFooterText()}</Cell.FooterText>
- </AriaDescription>
- </Cell.Footer>
- </AriaInputGroup>
- </Cell.Group>
+ <Cell.Group>
+ <MssFixSetting />
+ </Cell.Group>
+ </NavigationScrollbars>
+ </NavigationContainer>
+ </SettingsContainer>
+ </Layout>
+ </BackAction>
+ );
+}
- <Cell.Group>
- <AriaInputGroup>
- <Cell.Container>
- <AriaLabel>
- <Cell.InputLabel>
- {messages.pgettext('openvpn-settings-view', 'Mssfix')}
- </Cell.InputLabel>
- </AriaLabel>
- <AriaInput>
- <Cell.AutoSizingTextInput
- value={this.props.mssfix ? this.props.mssfix.toString() : ''}
- inputMode={'numeric'}
- maxLength={4}
- placeholder={messages.gettext('Default')}
- onSubmitValue={this.onMssfixSubmit}
- validateValue={OpenVpnSettings.mssfixIsValid}
- submitOnBlur={true}
- modifyValue={OpenVpnSettings.removeNonNumericCharacters}
- />
- </AriaInput>
- </Cell.Container>
- <Cell.Footer>
- <AriaDescription>
- <Cell.FooterText>
- {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.FooterText>
- </AriaDescription>
- </Cell.Footer>
- </AriaInputGroup>
- </Cell.Group>
- </StyledNavigationScrollbars>
- </NavigationContainer>
- </SettingsContainer>
+function TransportProtocolSelector() {
+ const relaySettings = useSelector((state) => state.settings.relaySettings);
+ const bridgeState = useSelector((state) => state.settings.bridgeState);
- {this.renderBridgeStateConfirmation()}
- </Layout>
- </BackAction>
- );
- }
+ const protocol = useMemo(() => {
+ const protocol = 'normal' in relaySettings ? relaySettings.normal.openvpn.protocol : undefined;
+ return protocol === 'any' ? undefined : protocol;
+ }, [relaySettings]);
- private bridgeStateItems(onAvailable: boolean): Array<ISelectorItem<BridgeState>> {
- return [
- {
- label: messages.gettext('Automatic'),
- value: 'auto',
- },
- {
- label: messages.gettext('On'),
- value: 'on',
- disabled: !onAvailable,
- },
- {
- label: messages.gettext('Off'),
- value: 'off',
- },
- ];
- }
+ const protocolAndPortUpdater = useProtocolAndPortUpdater();
- private protocolItems(udpAvailable: boolean): Array<ISelectorItem<OptionalRelayProtocol>> {
- return [
+ const items: ISelectorItem<OptionalRelayProtocol>[] = useMemo(
+ () => [
{
label: messages.gettext('Automatic'),
value: undefined,
@@ -280,125 +146,372 @@ export default class OpenVpnSettings extends React.Component<IProps, IState> {
{
label: messages.gettext('UDP'),
value: 'udp',
- disabled: !udpAvailable,
+ disabled: bridgeState === 'on',
},
- ];
- }
+ ],
+ [bridgeState],
+ );
- private onSelectOpenvpnProtocol = (protocol?: RelayProtocol) => {
- this.props.setOpenVpnRelayProtocolAndPort(protocol);
- };
+ return (
+ <StyledSelectorContainer>
+ <AriaInputGroup>
+ <Selector
+ title={messages.pgettext('openvpn-settings-view', 'Transport protocol')}
+ values={items}
+ value={protocol}
+ onSelect={protocolAndPortUpdater}
+ />
+ {bridgeState === 'on' && (
+ <Cell.Footer>
+ <AriaDescription>
+ <Cell.FooterText>
+ {formatMarkdown(
+ // TRANSLATORS: This is used to instruct users how to make UDP mode
+ // TRANSLATORS: available.
+ messages.pgettext(
+ 'openvpn-settings-view',
+ 'To activate UDP, change **Bridge mode** to **Automatic** or **Off**.',
+ ),
+ )}
+ </Cell.FooterText>
+ </AriaDescription>
+ </Cell.Footer>
+ )}
+ </AriaInputGroup>
+ </StyledSelectorContainer>
+ );
+}
+
+function useProtocolAndPortUpdater() {
+ const { updateRelaySettings } = useAppContext();
+
+ const updater = useCallback(
+ async (protocol?: RelayProtocol, port?: number) => {
+ const relayUpdate = RelaySettingsBuilder.normal()
+ .tunnel.openvpn((openvpn) => {
+ if (protocol) {
+ openvpn.protocol.exact(protocol);
+ } else {
+ openvpn.protocol.any();
+ }
+
+ if (port) {
+ openvpn.port.exact(port);
+ } else {
+ openvpn.port.any();
+ }
+ })
+ .build();
- private onSelectOpenVpnPort = (port?: number) => {
- this.props.setOpenVpnRelayProtocolAndPort(this.props.openvpn.protocol, port);
+ try {
+ await updateRelaySettings(relayUpdate);
+ } catch (e) {
+ const error = e as Error;
+ log.error('Failed to update relay settings', error.message);
+ }
+ },
+ [updateRelaySettings],
+ );
+
+ return updater;
+}
+
+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;
+ }, [relaySettings]);
+
+ const port = useMemo(() => {
+ const port = 'normal' in relaySettings ? relaySettings.normal.openvpn.port : undefined;
+ return port === 'any' ? undefined : port;
+ }, [relaySettings]);
+
+ const protocolAndPortUpdater = useProtocolAndPortUpdater();
+
+ const onSelect = useCallback(
+ async (port?: number) => {
+ await protocolAndPortUpdater(protocol, port);
+ },
+ [protocolAndPortUpdater, protocol],
+ );
+
+ const automaticPort: ISelectorItem<OptionalPort> = {
+ label: messages.gettext('Automatic'),
+ value: undefined,
};
- private onMssfixSubmit = (value: string) => {
- const parsedValue = value === '' ? undefined : parseInt(value, 10);
- if (OpenVpnSettings.mssfixIsValid(value)) {
- this.props.setOpenVpnMssfix(parsedValue);
- }
+ const portItems = {
+ udp: [automaticPort].concat(UDP_PORTS.map(mapPortToSelectorItem)),
+ tcp: [automaticPort].concat(TCP_PORTS.map(mapPortToSelectorItem)),
};
- private static removeNonNumericCharacters(value: string) {
- return value.replace(/[^0-9]/g, '');
+ if (protocol === undefined) {
+ return null;
}
- private static mssfixIsValid(mssfix: string): boolean {
- const parsedMssFix = mssfix ? parseInt(mssfix) : undefined;
- return (
- parsedMssFix === undefined ||
- (parsedMssFix >= MIN_MSSFIX_VALUE && parsedMssFix <= MAX_MSSFIX_VALUE)
- );
- }
-
- private bridgeModeFooterText() {
- switch (this.props.bridgeModeAvailablity) {
- case BridgeModeAvailability.blockedDueToTunnelProtocol:
- return formatMarkdown(
- sprintf(
- // TRANSLATORS: This is used to instruct users how to make the bridge mode setting
- // TRANSLATORS: available.
- // TRANSLATORS: Available placeholders:
- // TRANSLATORS: %(tunnelProtocol)s - the name of the tunnel protocol setting
- // TRANSLATORS: %(openvpn)s - will be replaced with OpenVPN
- messages.pgettext(
- 'openvpn-settings-view',
- 'To activate Bridge mode, go back and change **%(tunnelProtocol)s** to **%(openvpn)s**.',
- ),
- {
- tunnelProtocol: messages.pgettext('vpn-settings-view', 'Tunnel protocol'),
- openvpn: strings.openvpn,
- },
- ),
- );
- case BridgeModeAvailability.blockedDueToTransportProtocol:
- return formatMarkdown(
- sprintf(
- // TRANSLATORS: This is used to instruct users how to make the bridge mode setting
- // TRANSLATORS: available.
+ return (
+ <StyledSelectorContainer>
+ <AriaInputGroup>
+ <Selector
+ title={sprintf(
+ // TRANSLATORS: The title for the port selector section.
// TRANSLATORS: Available placeholders:
- // TRANSLATORS: %(transportProtocol)s - the name of the transport protocol setting
- // TRANSLATORS: %(automat)s - the translation of "Automatic"
- // TRANSLATORS: %(openvpn)s - will be replaced with OpenVPN
- messages.pgettext(
- 'openvpn-settings-view',
- 'To activate Bridge mode, change **%(transportProtocol)s** to **%(automatic)s** or **%(tcp)s**.',
- ),
+ // TRANSLATORS: %(portType)s - a selected protocol (either TCP or UDP)
+ messages.pgettext('openvpn-settings-view', '%(portType)s port'),
{
- transportProtocol: messages.pgettext('openvpn-settings-view', 'Transport protocol'),
- automatic: messages.gettext('Automatic'),
- tcp: messages.gettext('TCP'),
+ portType: protocol.toUpperCase(),
},
- ),
- );
- case BridgeModeAvailability.available:
- return 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 },
- );
- }
- }
+ )}
+ values={portItems[protocol]}
+ value={port}
+ onSelect={onSelect}
+ />
+ </AriaInputGroup>
+ </StyledSelectorContainer>
+ );
+}
+
+function BridgeModeSelector() {
+ const { setBridgeState: setBridgeStateImpl } = useAppContext();
+ const relaySettings = useSelector((state) => state.settings.relaySettings);
+
+ const bridgeState = useSelector((state) => state.settings.bridgeState);
+
+ const tunnelProtocol = useMemo(() => {
+ const protocol = 'normal' in relaySettings ? relaySettings.normal.tunnelProtocol : undefined;
+ return protocol === 'any' ? undefined : protocol;
+ }, [relaySettings]);
+
+ const transportProtocol = useMemo(() => {
+ const protocol = 'normal' in relaySettings ? relaySettings.normal.openvpn.protocol : undefined;
+ return protocol === 'any' ? undefined : protocol;
+ }, [relaySettings]);
+
+ const options: ISelectorItem<BridgeState>[] = useMemo(
+ () => [
+ {
+ label: messages.gettext('Automatic'),
+ value: 'auto',
+ },
+ {
+ label: messages.gettext('On'),
+ value: 'on',
+ disabled: tunnelProtocol !== 'openvpn' || transportProtocol === 'udp',
+ },
+ {
+ label: messages.gettext('Off'),
+ value: 'off',
+ },
+ ],
+ [tunnelProtocol, transportProtocol],
+ );
+
+ const [confirmationDialogVisible, showConfirmationDialog, hideConfirmationDialog] = useBoolean();
+
+ const setBridgeState = useCallback(
+ async (bridgeState: BridgeState) => {
+ try {
+ await setBridgeStateImpl(bridgeState);
+ } catch (e) {
+ const error = e as Error;
+ log.error(`Failed to update bridge state: ${error.message}`);
+ }
+ },
+ [setBridgeStateImpl],
+ );
+
+ const onSelectBridgeState = useCallback(
+ async (newValue: BridgeState) => {
+ if (newValue === 'on') {
+ showConfirmationDialog();
+ } else {
+ await setBridgeState(newValue);
+ }
+ },
+ [showConfirmationDialog, setBridgeState],
+ );
+
+ const confirmBridgeState = useCallback(async () => {
+ hideConfirmationDialog();
+ await setBridgeState('on');
+ }, [hideConfirmationDialog, setBridgeState]);
- private renderBridgeStateConfirmation = () => {
- return (
+ return (
+ <>
+ <AriaInputGroup>
+ <StyledSelectorContainer>
+ <Selector
+ title={
+ // TRANSLATORS: The title for the shadowsocks bridge selector section.
+ messages.pgettext('openvpn-settings-view', 'Bridge mode')
+ }
+ values={options}
+ value={bridgeState}
+ onSelect={onSelectBridgeState}
+ />
+ </StyledSelectorContainer>
+ <Cell.Footer>
+ <AriaDescription>
+ <Cell.FooterText>
+ {bridgeModeFooterText(tunnelProtocol, transportProtocol)}
+ </Cell.FooterText>
+ </AriaDescription>
+ </Cell.Footer>
+ </AriaInputGroup>
<ModalAlert
- isOpen={this.state.showBridgeStateConfirmationDialog}
+ isOpen={confirmationDialogVisible}
type={ModalAlertType.info}
message={messages.gettext('This setting increases latency. Use only if needed.')}
buttons={[
- <AppButton.RedButton key="confirm" onClick={this.confirmBridgeState}>
+ <AppButton.RedButton key="confirm" onClick={confirmBridgeState}>
{messages.gettext('Enable anyway')}
</AppButton.RedButton>,
- <AppButton.BlueButton key="back" onClick={this.hideBridgeStateConfirmationDialog}>
+ <AppButton.BlueButton key="back" onClick={hideConfirmationDialog}>
{messages.gettext('Back')}
</AppButton.BlueButton>,
]}
- close={this.hideBridgeStateConfirmationDialog}></ModalAlert>
+ close={hideConfirmationDialog}
+ />
+ </>
+ );
+}
+
+function bridgeModeFooterText(tunnelProtocol?: TunnelProtocol, transportProtocol?: RelayProtocol) {
+ if (tunnelProtocol !== 'openvpn') {
+ return formatMarkdown(
+ sprintf(
+ // TRANSLATORS: This is used to instruct users how to make the bridge mode setting
+ // TRANSLATORS: available.
+ // TRANSLATORS: Available placeholders:
+ // TRANSLATORS: %(tunnelProtocol)s - the name of the tunnel protocol setting
+ // TRANSLATORS: %(openvpn)s - will be replaced with OpenVPN
+ messages.pgettext(
+ 'openvpn-settings-view',
+ 'To activate Bridge mode, go back and change **%(tunnelProtocol)s** to **%(openvpn)s**.',
+ ),
+ {
+ tunnelProtocol: messages.pgettext('vpn-settings-view', 'Tunnel protocol'),
+ openvpn: strings.openvpn,
+ },
+ ),
);
- };
+ } else if (transportProtocol === 'udp') {
+ return formatMarkdown(
+ sprintf(
+ // TRANSLATORS: This is used to instruct users how to make the bridge mode setting
+ // TRANSLATORS: available.
+ // TRANSLATORS: Available placeholders:
+ // TRANSLATORS: %(transportProtocol)s - the name of the transport protocol setting
+ // TRANSLATORS: %(automat)s - the translation of "Automatic"
+ // TRANSLATORS: %(openvpn)s - will be replaced with OpenVPN
+ messages.pgettext(
+ 'openvpn-settings-view',
+ 'To activate Bridge mode, change **%(transportProtocol)s** to **%(automatic)s** or **%(tcp)s**.',
+ ),
+ {
+ transportProtocol: messages.pgettext('openvpn-settings-view', 'Transport protocol'),
+ automatic: messages.gettext('Automatic'),
+ tcp: messages.gettext('TCP'),
+ },
+ ),
+ );
+ } else {
+ return 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 },
+ );
+ }
+}
- private onSelectBridgeState = (newValue: BridgeState) => {
- if (newValue === 'on') {
- this.setState({ showBridgeStateConfirmationDialog: true });
- } else {
- this.props.setBridgeState(newValue);
- }
- };
+function removeNonNumericCharacters(value: string) {
+ return value.replace(/[^0-9]/g, '');
+}
- private hideBridgeStateConfirmationDialog = () => {
- this.setState({ showBridgeStateConfirmationDialog: false });
- };
+function mssfixIsValid(mssfix: string): boolean {
+ const parsedMssFix = mssfix ? parseInt(mssfix) : undefined;
+ return (
+ parsedMssFix === undefined ||
+ (parsedMssFix >= MIN_MSSFIX_VALUE && parsedMssFix <= MAX_MSSFIX_VALUE)
+ );
+}
- private confirmBridgeState = () => {
- this.setState({ showBridgeStateConfirmationDialog: false });
- this.props.setBridgeState('on');
- };
+function MssFixSetting() {
+ const { setOpenVpnMssfix: setOpenVpnMssfixImpl } = useAppContext();
+ const mssfix = useSelector((state) => state.settings.openVpn.mssfix);
+
+ const setOpenVpnMssfix = useCallback(
+ async (mssfix?: number) => {
+ try {
+ await setOpenVpnMssfixImpl(mssfix);
+ } catch (e) {
+ const error = e as Error;
+ log.error('Failed to update mssfix value', error.message);
+ }
+ },
+ [setOpenVpnMssfixImpl],
+ );
+
+ const onMssfixSubmit = useCallback(
+ async (value: string) => {
+ const parsedValue = value === '' ? undefined : parseInt(value, 10);
+ if (mssfixIsValid(value)) {
+ await setOpenVpnMssfix(parsedValue);
+ }
+ },
+ [setOpenVpnMssfix],
+ );
+
+ return (
+ <AriaInputGroup>
+ <Cell.Container>
+ <AriaLabel>
+ <Cell.InputLabel>{messages.pgettext('openvpn-settings-view', 'Mssfix')}</Cell.InputLabel>
+ </AriaLabel>
+ <AriaInput>
+ <Cell.AutoSizingTextInput
+ value={mssfix ? mssfix.toString() : ''}
+ inputMode={'numeric'}
+ maxLength={4}
+ placeholder={messages.gettext('Default')}
+ onSubmitValue={onMssfixSubmit}
+ validateValue={mssfixIsValid}
+ submitOnBlur={true}
+ modifyValue={removeNonNumericCharacters}
+ />
+ </AriaInput>
+ </Cell.Container>
+ <Cell.Footer>
+ <AriaDescription>
+ <Cell.FooterText>
+ {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.FooterText>
+ </AriaDescription>
+ </Cell.Footer>
+ </AriaInputGroup>
+ );
}
diff --git a/gui/src/renderer/components/ProblemReport.tsx b/gui/src/renderer/components/ProblemReport.tsx
index d98520640a..e2c3bf4d5b 100644
--- a/gui/src/renderer/components/ProblemReport.tsx
+++ b/gui/src/renderer/components/ProblemReport.tsx
@@ -8,12 +8,11 @@ import * as AppButton from './AppButton';
import { AriaDescribed, AriaDescription, AriaDescriptionGroup } from './AriaGroup';
import ImageView from './ImageView';
import { BackAction } from './KeyboardNavigation';
-import { Layout } from './Layout';
+import { Layout, SettingsContainer } from './Layout';
import { ModalAlert, ModalAlertType } from './Modal';
import { NavigationBar, NavigationItems, TitleBarItem } from './NavigationBar';
import {
StyledBlueButton,
- StyledContainer,
StyledContent,
StyledContentContainer,
StyledEmail,
@@ -156,7 +155,7 @@ export default class ProblemReport extends React.Component<
return (
<BackAction action={this.props.onClose}>
<Layout>
- <StyledContainer>
+ <SettingsContainer>
<NavigationBar>
<NavigationItems>
<TitleBarItem>
@@ -174,7 +173,7 @@ export default class ProblemReport extends React.Component<
{this.renderNoEmailDialog()}
{this.renderOutdateVersionWarningDialog()}
- </StyledContainer>
+ </SettingsContainer>
</Layout>
</BackAction>
);
diff --git a/gui/src/renderer/components/ProblemReportStyles.tsx b/gui/src/renderer/components/ProblemReportStyles.tsx
index bc5166297c..25457c2593 100644
--- a/gui/src/renderer/components/ProblemReportStyles.tsx
+++ b/gui/src/renderer/components/ProblemReportStyles.tsx
@@ -3,16 +3,11 @@ import styled from 'styled-components';
import { colors } from '../../config.json';
import * as AppButton from './AppButton';
import { hugeText, smallText } from './common-styles';
-import { Container } from './Layout';
export const StyledBlueButton = styled(AppButton.BlueButton)({
marginBottom: '18px',
});
-export const StyledContainer = styled(Container)({
- backgroundColor: colors.darkBlue,
-});
-
export const StyledContentContainer = styled.div({
display: 'flex',
flexDirection: 'column',
diff --git a/gui/src/renderer/components/SelectLanguage.tsx b/gui/src/renderer/components/SelectLanguage.tsx
index f0e5ffb624..86d0c8166f 100644
--- a/gui/src/renderer/components/SelectLanguage.tsx
+++ b/gui/src/renderer/components/SelectLanguage.tsx
@@ -1,13 +1,12 @@
import * as React from 'react';
import styled from 'styled-components';
-import { colors } from '../../config.json';
import { messages } from '../../shared/gettext';
import { AriaInputGroup } from './AriaGroup';
import Selector, { ISelectorItem } from './cell/Selector';
import { CustomScrollbarsRef } from './CustomScrollbars';
import { BackAction } from './KeyboardNavigation';
-import { Container, Layout } from './Layout';
+import { Layout, SettingsContainer } from './Layout';
import {
NavigationBar,
NavigationContainer,
@@ -28,10 +27,6 @@ interface IState {
source: Array<ISelectorItem<string>>;
}
-const StyledContainer = styled(Container)({
- backgroundColor: colors.darkBlue,
-});
-
const StyledNavigationScrollbars = styled(NavigationScrollbars)({
flex: 1,
});
@@ -62,7 +57,7 @@ export default class SelectLanguage extends React.Component<IProps, IState> {
return (
<BackAction action={this.props.onClose}>
<Layout>
- <StyledContainer>
+ <SettingsContainer>
<NavigationContainer>
<NavigationBar>
<NavigationItems>
@@ -92,7 +87,7 @@ export default class SelectLanguage extends React.Component<IProps, IState> {
</AriaInputGroup>
</StyledNavigationScrollbars>
</NavigationContainer>
- </StyledContainer>
+ </SettingsContainer>
</Layout>
</BackAction>
);
diff --git a/gui/src/renderer/components/SelectLocation.tsx b/gui/src/renderer/components/SelectLocation.tsx
index 34059f3e85..4c228c9a53 100644
--- a/gui/src/renderer/components/SelectLocation.tsx
+++ b/gui/src/renderer/components/SelectLocation.tsx
@@ -14,7 +14,7 @@ import BridgeLocations, { SpecialBridgeLocationType } from './BridgeLocations';
import { CustomScrollbarsRef } from './CustomScrollbars';
import ImageView from './ImageView';
import { BackAction } from './KeyboardNavigation';
-import { Layout } from './Layout';
+import { Layout, SettingsContainer } from './Layout';
import LocationList, {
DisabledReason,
LocationSelection,
@@ -31,7 +31,6 @@ import {
import { ScopeBarItem } from './ScopeBar';
import {
StyledClearFilterButton,
- StyledContainer,
StyledContent,
StyledFilter,
StyledFilterIconButton,
@@ -140,7 +139,7 @@ export default class SelectLocation extends React.Component<IProps, IState> {
return (
<BackAction icon="close" action={this.props.onClose}>
<Layout>
- <StyledContainer>
+ <SettingsContainer>
<NavigationContainer>
<NavigationBar>
<NavigationItems>
@@ -242,7 +241,7 @@ export default class SelectLocation extends React.Component<IProps, IState> {
</SpacePreAllocationView>
</NavigationScrollbars>
</NavigationContainer>
- </StyledContainer>
+ </SettingsContainer>
</Layout>
</BackAction>
);
diff --git a/gui/src/renderer/components/SelectLocationStyles.tsx b/gui/src/renderer/components/SelectLocationStyles.tsx
index 15211389f6..d4a0450c7c 100644
--- a/gui/src/renderer/components/SelectLocationStyles.tsx
+++ b/gui/src/renderer/components/SelectLocationStyles.tsx
@@ -2,14 +2,9 @@ import styled from 'styled-components';
import { colors } from '../../config.json';
import { tinyText } from './common-styles';
-import { Container } from './Layout';
import { ScopeBar } from './ScopeBar';
import SettingsHeader from './SettingsHeader';
-export const StyledContainer = styled(Container)({
- backgroundColor: colors.darkBlue,
-});
-
export const StyledScopeBar = styled(ScopeBar)({
marginTop: '8px',
});
diff --git a/gui/src/renderer/components/Settings.tsx b/gui/src/renderer/components/Settings.tsx
index 812deef142..56c75f0602 100644
--- a/gui/src/renderer/components/Settings.tsx
+++ b/gui/src/renderer/components/Settings.tsx
@@ -10,12 +10,11 @@ import { useSelector } from '../redux/store';
import { AriaDescribed, AriaDescription, AriaDescriptionGroup } from './AriaGroup';
import * as Cell from './cell';
import { BackAction } from './KeyboardNavigation';
-import { Layout } from './Layout';
+import { Layout, SettingsContainer } from './Layout';
import { NavigationBar, NavigationContainer, NavigationItems, TitleBarItem } from './NavigationBar';
import SettingsHeader, { HeaderTitle } from './SettingsHeader';
import {
StyledCellIcon,
- StyledContainer,
StyledContent,
StyledNavigationScrollbars,
StyledOutOfTimeSubText,
@@ -42,7 +41,7 @@ export default function Support() {
return (
<BackAction icon="close" action={history.dismiss}>
<Layout>
- <StyledContainer>
+ <SettingsContainer>
<NavigationContainer>
<NavigationBar alwaysDisplayBarTitle={!showLargeTitle}>
<NavigationItems>
@@ -90,7 +89,7 @@ export default function Support() {
<QuitButton />
</StyledNavigationScrollbars>
</NavigationContainer>
- </StyledContainer>
+ </SettingsContainer>
</Layout>
</BackAction>
);
diff --git a/gui/src/renderer/components/SettingsStyles.tsx b/gui/src/renderer/components/SettingsStyles.tsx
index 687b448424..612af16dab 100644
--- a/gui/src/renderer/components/SettingsStyles.tsx
+++ b/gui/src/renderer/components/SettingsStyles.tsx
@@ -3,7 +3,6 @@ import styled from 'styled-components';
import { colors } from '../../config.json';
import * as AppButton from './AppButton';
import * as Cell from './cell';
-import { Container } from './Layout';
import { NavigationScrollbars } from './NavigationBar';
export const StyledOutOfTimeSubText = styled(Cell.SubText)((props: { isOutOfTime: boolean }) => ({
@@ -18,10 +17,6 @@ export const StyledNavigationScrollbars = styled(NavigationScrollbars)({
flex: 1,
});
-export const StyledContainer = styled(Container)({
- backgroundColor: colors.darkBlue,
-});
-
export const StyledContent = styled.div({
display: 'flex',
flexDirection: 'column',
diff --git a/gui/src/renderer/components/SplitTunnelingSettings.tsx b/gui/src/renderer/components/SplitTunnelingSettings.tsx
index 77af2b4905..c0fc4e07bf 100644
--- a/gui/src/renderer/components/SplitTunnelingSettings.tsx
+++ b/gui/src/renderer/components/SplitTunnelingSettings.tsx
@@ -20,7 +20,7 @@ import * as Cell from './cell';
import { CustomScrollbarsRef } from './CustomScrollbars';
import ImageView from './ImageView';
import { BackAction } from './KeyboardNavigation';
-import { Layout } from './Layout';
+import { Layout, SettingsContainer } from './Layout';
import List from './List';
import { ModalAlert, ModalAlertType } from './Modal';
import { NavigationBar, NavigationContainer, NavigationItems, TitleBarItem } from './NavigationBar';
@@ -33,7 +33,6 @@ import {
StyledCellWarningIcon,
StyledClearButton,
StyledClearIcon,
- StyledContainer,
StyledContent,
StyledHeaderTitle,
StyledHeaderTitleContainer,
@@ -63,7 +62,7 @@ export default function SplitTunneling() {
<StyledPageCover show={browsing} />
<BackAction action={pop}>
<Layout>
- <StyledContainer>
+ <SettingsContainer>
<NavigationContainer>
<NavigationBar>
<NavigationItems>
@@ -80,7 +79,7 @@ export default function SplitTunneling() {
</StyledContent>
</StyledNavigationScrollbars>
</NavigationContainer>
- </StyledContainer>
+ </SettingsContainer>
</Layout>
</BackAction>
</>
diff --git a/gui/src/renderer/components/SplitTunnelingSettingsStyles.tsx b/gui/src/renderer/components/SplitTunnelingSettingsStyles.tsx
index 1c946d8c72..412d04181f 100644
--- a/gui/src/renderer/components/SplitTunnelingSettingsStyles.tsx
+++ b/gui/src/renderer/components/SplitTunnelingSettingsStyles.tsx
@@ -5,7 +5,6 @@ import * as AppButton from './AppButton';
import * as Cell from './cell';
import { normalText } from './common-styles';
import ImageView from './ImageView';
-import { Container } from './Layout';
import { NavigationScrollbars } from './NavigationBar';
import { HeaderTitle } from './SettingsHeader';
@@ -21,10 +20,6 @@ export const StyledPageCover = styled.div({}, (props: { show: boolean }) => ({
display: props.show ? 'block' : 'none',
}));
-export const StyledContainer = styled(Container)({
- backgroundColor: colors.darkBlue,
-});
-
export const StyledNavigationScrollbars = styled(NavigationScrollbars)({
flex: 1,
});
diff --git a/gui/src/renderer/components/Support.tsx b/gui/src/renderer/components/Support.tsx
index ef2c82dd4e..0579b1d944 100644
--- a/gui/src/renderer/components/Support.tsx
+++ b/gui/src/renderer/components/Support.tsx
@@ -1,7 +1,7 @@
import { useCallback } from 'react';
import styled from 'styled-components';
-import { colors, links } from '../../config.json';
+import { links } from '../../config.json';
import { messages } from '../../shared/gettext';
import { useAppContext } from '../context';
import { useHistory } from '../lib/history';
@@ -17,7 +17,7 @@ import {
} from './AriaGroup';
import * as Cell from './cell';
import { BackAction } from './KeyboardNavigation';
-import { Container, Layout } from './Layout';
+import { Layout, SettingsContainer } from './Layout';
import {
NavigationBar,
NavigationContainer,
@@ -27,10 +27,6 @@ import {
} from './NavigationBar';
import SettingsHeader, { HeaderTitle } from './SettingsHeader';
-const StyledContainer = styled(Container)({
- backgroundColor: colors.darkBlue,
-});
-
const StyledContent = styled.div({
display: 'flex',
flexDirection: 'column',
@@ -44,7 +40,7 @@ export default function Support() {
return (
<BackAction action={pop}>
<Layout>
- <StyledContainer>
+ <SettingsContainer>
<NavigationContainer>
<NavigationBar>
<NavigationItems>
@@ -74,7 +70,7 @@ export default function Support() {
</StyledContent>
</NavigationScrollbars>
</NavigationContainer>
- </StyledContainer>
+ </SettingsContainer>
</Layout>
</BackAction>
);
diff --git a/gui/src/renderer/components/VpnSettings.tsx b/gui/src/renderer/components/VpnSettings.tsx
index 03274c0b2a..47b48e4fcd 100644
--- a/gui/src/renderer/components/VpnSettings.tsx
+++ b/gui/src/renderer/components/VpnSettings.tsx
@@ -2,7 +2,7 @@ import { useCallback, useMemo } from 'react';
import { sprintf } from 'sprintf-js';
import styled from 'styled-components';
-import { colors, strings } from '../../config.json';
+import { strings } from '../../config.json';
import { IDnsOptions, TunnelProtocol } from '../../shared/daemon-rpc-types';
import { messages } from '../../shared/gettext';
import log from '../../shared/logging';
@@ -21,7 +21,7 @@ import Selector, { ISelectorItem } from './cell/Selector';
import CustomDnsSettings from './CustomDnsSettings';
import InfoButton, { InfoIcon } from './InfoButton';
import { BackAction } from './KeyboardNavigation';
-import { Container, Layout } from './Layout';
+import { Layout, SettingsContainer } from './Layout';
import { ModalAlert, ModalAlertType, ModalMessage } from './Modal';
import {
NavigationBar,
@@ -32,10 +32,6 @@ import {
} from './NavigationBar';
import SettingsHeader, { HeaderTitle } from './SettingsHeader';
-const StyledContainer = styled(Container)({
- backgroundColor: colors.darkBlue,
-});
-
const StyledContent = styled.div({
display: 'flex',
flexDirection: 'column',
@@ -57,7 +53,7 @@ export default function VpnSettings() {
return (
<BackAction action={pop}>
<Layout>
- <StyledContainer>
+ <SettingsContainer>
<NavigationContainer>
<NavigationBar>
<NavigationItems>
@@ -117,7 +113,7 @@ export default function VpnSettings() {
</StyledContent>
</NavigationScrollbars>
</NavigationContainer>
- </StyledContainer>
+ </SettingsContainer>
</Layout>
</BackAction>
);
diff --git a/gui/src/renderer/components/WireguardSettings.tsx b/gui/src/renderer/components/WireguardSettings.tsx
index d0a22c7e87..a4c7a11e08 100644
--- a/gui/src/renderer/components/WireguardSettings.tsx
+++ b/gui/src/renderer/components/WireguardSettings.tsx
@@ -1,14 +1,21 @@
-import * as React from 'react';
+import { useCallback, useMemo } from 'react';
import { sprintf } from 'sprintf-js';
import styled from 'styled-components';
import { strings } from '../../config.json';
import { IpVersion } from '../../shared/daemon-rpc-types';
import { messages } from '../../shared/gettext';
+import log from '../../shared/logging';
+import { useAppContext } from '../context';
+import { createWireguardRelayUpdater } 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 Selector, { ISelectorItem } from './cell/Selector';
+import { InfoIcon } from './InfoButton';
import { BackAction } from './KeyboardNavigation';
import { Layout, SettingsContainer } from './Layout';
import { ModalAlert, ModalAlertType } from './Modal';
@@ -20,7 +27,6 @@ import {
TitleBarItem,
} from './NavigationBar';
import SettingsHeader, { HeaderTitle } from './SettingsHeader';
-import Switch from './Switch';
const MIN_WIREGUARD_MTU_VALUE = 1280;
const MAX_WIREGUARD_MTU_VALUE = 1420;
@@ -33,305 +39,402 @@ function mapPortToSelectorItem(value: number): ISelectorItem<number> {
return { label: value.toString(), value };
}
-export const StyledNavigationScrollbars = styled(NavigationScrollbars)({
+export const StyledContent = styled.div({
+ display: 'flex',
+ flexDirection: 'column',
flex: 1,
+ marginBottom: '2px',
+});
+
+export const StyledCellIcon = styled(Cell.UntintedIcon)({
+ marginRight: '8px',
+});
+
+export const StyledInfoIcon = styled(InfoIcon)({
+ marginRight: '16px',
});
export const StyledSelectorContainer = styled.div({
flex: 0,
});
-export const StyledSelectorForFooter = (styled(Selector)({
- marginBottom: 0,
-}) as unknown) as new <T>() => Selector<T>;
+export default function WireguardSettings() {
+ const { pop } = useHistory();
-interface IProps {
- wireguard: { port?: number; ipVersion?: IpVersion };
- wireguardMtu?: number;
- wireguardMultihop: boolean;
- setWireguardMtu: (value: number | undefined) => void;
- setWireguardMultihop: (value: boolean) => void;
- setWireguardPort: (port?: number) => void;
- setWireguardIpVersion: (ipVersion?: IpVersion) => void;
- onClose: () => void;
-}
+ return (
+ <BackAction action={pop}>
+ <Layout>
+ <SettingsContainer>
+ <NavigationContainer>
+ <NavigationBar>
+ <NavigationItems>
+ <TitleBarItem>
+ {sprintf(
+ // TRANSLATORS: Title label in navigation bar
+ // TRANSLATORS: Available placeholders:
+ // TRANSLATORS: %(wireguard)s - Will be replaced with the string "WireGuard"
+ messages.pgettext('wireguard-settings-nav', '%(wireguard)s settings'),
+ { wireguard: strings.wireguard },
+ )}
+ </TitleBarItem>
+ </NavigationItems>
+ </NavigationBar>
-interface IState {
- showMultihopConfirmationDialog: boolean;
-}
+ <NavigationScrollbars>
+ <SettingsHeader>
+ <HeaderTitle>
+ {sprintf(
+ // TRANSLATORS: Available placeholders:
+ // TRANSLATORS: %(wireguard)s - Will be replaced with the string "WireGuard"
+ messages.pgettext('wireguard-settings-view', '%(wireguard)s settings'),
+ { wireguard: strings.wireguard },
+ )}
+ </HeaderTitle>
+ </SettingsHeader>
+
+ <StyledContent>
+ <Cell.Group>
+ <PortSelector />
+ </Cell.Group>
-export default class WireguardSettings extends React.Component<IProps, IState> {
- public state = { showMultihopConfirmationDialog: false };
+ <Cell.Group>
+ <MultihopSetting />
+ </Cell.Group>
- private multihopRef = React.createRef<Switch>();
+ <Cell.Group>
+ <IpVersionSetting />
+ </Cell.Group>
- private wireguardPortItems: Array<ISelectorItem<OptionalPort>>;
- private wireguardIpVersionItems: Array<ISelectorItem<OptionalIpVersion>>;
+ <Cell.Group>
+ <MtuSetting />
+ </Cell.Group>
+ </StyledContent>
+ </NavigationScrollbars>
+ </NavigationContainer>
+ </SettingsContainer>
+ </Layout>
+ </BackAction>
+ );
+}
- constructor(props: IProps) {
- super(props);
+function PortSelector() {
+ const relaySettings = useSelector((state) => state.settings.relaySettings);
+ const { updateRelaySettings } = useAppContext();
+ const wireguardPortItems = useMemo(() => {
const automaticPort: ISelectorItem<OptionalPort> = {
label: messages.gettext('Automatic'),
value: undefined,
};
- this.wireguardPortItems = [automaticPort].concat(
- WIREUGARD_UDP_PORTS.map(mapPortToSelectorItem),
- );
+ return [automaticPort].concat(WIREUGARD_UDP_PORTS.map(mapPortToSelectorItem));
+ }, []);
- this.wireguardIpVersionItems = [
- {
- label: messages.gettext('Automatic'),
- value: undefined,
- },
- {
- label: messages.gettext('IPv4'),
- value: 'ipv4',
- },
- {
- label: messages.gettext('IPv6'),
- value: 'ipv6',
- },
- ];
- }
+ const port = useMemo(() => {
+ const port = 'normal' in relaySettings ? relaySettings.normal.wireguard.port : undefined;
+ return port === 'any' ? undefined : port;
+ }, [relaySettings]);
- public render() {
- return (
- <BackAction action={this.props.onClose}>
- <Layout>
- <SettingsContainer>
- <NavigationContainer>
- <NavigationBar>
- <NavigationItems>
- <TitleBarItem>
- {sprintf(
- // TRANSLATORS: Title label in navigation bar
- // TRANSLATORS: Available placeholders:
- // TRANSLATORS: %(wireguard)s - Will be replaced with the string "WireGuard"
- messages.pgettext('wireguard-settings-nav', '%(wireguard)s settings'),
- { wireguard: strings.wireguard },
- )}
- </TitleBarItem>
- </NavigationItems>
- </NavigationBar>
-
- <StyledNavigationScrollbars>
- <SettingsHeader>
- <HeaderTitle>
- {sprintf(
- // TRANSLATORS: Available placeholders:
- // TRANSLATORS: %(wireguard)s - Will be replaced with the string "WireGuard"
- messages.pgettext('wireguard-settings-view', '%(wireguard)s settings'),
- { wireguard: strings.wireguard },
- )}
- </HeaderTitle>
- </SettingsHeader>
-
- <Cell.Group>
- <AriaInputGroup>
- <StyledSelectorContainer>
- <StyledSelectorForFooter
- // TRANSLATORS: The title for the WireGuard port selector.
- title={messages.pgettext('wireguard-settings-view', 'Port')}
- values={this.wireguardPortItems}
- value={this.props.wireguard.port}
- onSelect={this.props.setWireguardPort}
- />
- </StyledSelectorContainer>
- <Cell.Footer>
- <AriaDescription>
- <Cell.FooterText>
- {
- // TRANSLATORS: The hint displayed below the WireGuard port selector.
- messages.pgettext(
- 'wireguard-settings-view',
- 'The automatic setting will randomly choose from a wide range of ports.',
- )
- }
- </Cell.FooterText>
- </AriaDescription>
- </Cell.Footer>
- </AriaInputGroup>
- </Cell.Group>
+ const setWireguardPort = useCallback(
+ async (port?: number) => {
+ const relayUpdate = createWireguardRelayUpdater(relaySettings)
+ .tunnel.wireguard((wireguard) => {
+ if (port) {
+ wireguard.port.exact(port);
+ } else {
+ wireguard.port.any();
+ }
+ })
+ .build();
+ try {
+ await updateRelaySettings(relayUpdate);
+ } catch (e) {
+ const error = e as Error;
+ log.error('Failed to update relay settings', error.message);
+ }
+ },
+ [relaySettings],
+ );
- <Cell.Group>
- <AriaInputGroup>
- <Cell.Container>
- <AriaLabel>
- <Cell.InputLabel>
- {
- // TRANSLATORS: The label next to the multihop settings toggle.
- messages.pgettext('vpn-settings-view', 'Enable multihop')
- }
- </Cell.InputLabel>
- </AriaLabel>
- <AriaInput>
- <Cell.Switch
- ref={this.multihopRef}
- isOn={this.props.wireguardMultihop}
- onChange={this.setWireguardMultihop}
- />
- </AriaInput>
- </Cell.Container>
- <Cell.Footer>
- <AriaDescription>
- <Cell.FooterText>
- {sprintf(
- // TRANSLATORS: Description for multihop settings toggle.
- // TRANSLATORS: Available placeholders:
- // TRANSLATORS: %(wireguard)s - Will be replaced with the string "WireGuard"
- messages.pgettext(
- 'vpn-settings-view',
- 'Increases anonymity by routing your traffic into one %(wireguard)s server and out another, making it harder to trace.',
- ),
- { wireguard: strings.wireguard },
- )}
- </Cell.FooterText>
- </AriaDescription>
- </Cell.Footer>
- </AriaInputGroup>
- </Cell.Group>
+ return (
+ <AriaInputGroup>
+ <StyledSelectorContainer>
+ <Selector
+ // TRANSLATORS: The title for the WireGuard port selector.
+ title={messages.pgettext('wireguard-settings-view', 'Port')}
+ values={wireguardPortItems}
+ value={port}
+ onSelect={setWireguardPort}
+ />
+ </StyledSelectorContainer>
+ <Cell.Footer>
+ <AriaDescription>
+ <Cell.FooterText>
+ {
+ // TRANSLATORS: The hint displayed below the WireGuard port selector.
+ messages.pgettext(
+ 'wireguard-settings-view',
+ 'The automatic setting will randomly choose from a wide range of ports.',
+ )
+ }
+ </Cell.FooterText>
+ </AriaDescription>
+ </Cell.Footer>
+ </AriaInputGroup>
+ );
+}
- <Cell.Group>
- <AriaInputGroup>
- <StyledSelectorContainer>
- <StyledSelectorForFooter
- // TRANSLATORS: The title for the WireGuard IP version selector.
- title={messages.pgettext('wireguard-settings-view', 'IP version')}
- values={this.wireguardIpVersionItems}
- value={this.props.wireguard.ipVersion}
- onSelect={this.props.setWireguardIpVersion}
- />
- </StyledSelectorContainer>
- <Cell.Footer>
- <AriaDescription>
- <Cell.FooterText>
- {sprintf(
- // TRANSLATORS: The hint displayed below the WireGuard IP version selector.
- // TRANSLATORS: Available placeholders:
- // TRANSLATORS: %(wireguard)s - Will be replaced with the string "WireGuard"
- messages.pgettext(
- 'wireguard-settings-view',
- 'This allows access to %(wireguard)s for devices that only support IPv6.',
- ),
- { wireguard: strings.wireguard },
- )}
- </Cell.FooterText>
- </AriaDescription>
- </Cell.Footer>
- </AriaInputGroup>
- </Cell.Group>
+function MultihopSetting() {
+ const relaySettings = useSelector((state) => state.settings.relaySettings);
+ const { updateRelaySettings } = useAppContext();
- <Cell.Group>
- <AriaInputGroup>
- <Cell.Container>
- <AriaLabel>
- <Cell.InputLabel>
- {messages.pgettext('wireguard-settings-view', 'MTU')}
- </Cell.InputLabel>
- </AriaLabel>
- <AriaInput>
- <Cell.AutoSizingTextInput
- value={this.props.wireguardMtu ? this.props.wireguardMtu.toString() : ''}
- inputMode={'numeric'}
- maxLength={4}
- placeholder={messages.gettext('Default')}
- onSubmitValue={this.onWireguardMtuSubmit}
- validateValue={WireguardSettings.wireguarMtuIsValid}
- submitOnBlur={true}
- modifyValue={WireguardSettings.removeNonNumericCharacters}
- />
- </AriaInput>
- </Cell.Container>
- <Cell.Footer>
- <AriaDescription>
- <Cell.FooterText>
- {sprintf(
- // TRANSLATORS: The hint displayed below the WireGuard MTU input field.
- // TRANSLATORS: Available placeholders:
- // TRANSLATORS: %(wireguard)s - Will be replaced with the string "WireGuard"
- // TRANSLATORS: %(max)d - the maximum possible wireguard mtu value
- // TRANSLATORS: %(min)d - the minimum possible wireguard mtu value
- messages.pgettext(
- 'wireguard-settings-view',
- 'Set %(wireguard)s MTU value. Valid range: %(min)d - %(max)d.',
- ),
- {
- wireguard: strings.wireguard,
- min: MIN_WIREGUARD_MTU_VALUE,
- max: MAX_WIREGUARD_MTU_VALUE,
- },
- )}
- </Cell.FooterText>
- </AriaDescription>
- </Cell.Footer>
- </AriaInputGroup>
- </Cell.Group>
- </StyledNavigationScrollbars>
- </NavigationContainer>
- </SettingsContainer>
+ const multihop = 'normal' in relaySettings ? relaySettings.normal.wireguard.useMultihop : false;
- {this.renderMultihopConfirmation()}
- </Layout>
- </BackAction>
- );
- }
+ const [confirmationDialogVisible, showConfirmationDialog, hideConfirmationDialog] = useBoolean();
- private static removeNonNumericCharacters(value: string) {
- return value.replace(/[^0-9]/g, '');
- }
+ const setMultihopImpl = useCallback(
+ async (enabled: boolean) => {
+ const relayUpdate = createWireguardRelayUpdater(relaySettings)
+ .tunnel.wireguard((wireguard) => wireguard.useMultihop(enabled))
+ .build();
+ try {
+ await updateRelaySettings(relayUpdate);
+ } catch (e) {
+ const error = e as Error;
+ log.error('Failed to update WireGuard multihop settings', error.message);
+ }
+ },
+ [relaySettings, updateRelaySettings],
+ );
- private onWireguardMtuSubmit = (value: string) => {
- const parsedValue = value === '' ? undefined : parseInt(value, 10);
- if (WireguardSettings.wireguarMtuIsValid(value)) {
- this.props.setWireguardMtu(parsedValue);
- }
- };
+ const setMultihop = useCallback(
+ async (newValue: boolean) => {
+ if (newValue) {
+ showConfirmationDialog();
+ } else {
+ await setMultihopImpl(false);
+ }
+ },
+ [setMultihopImpl],
+ );
- private static wireguarMtuIsValid(mtu: string): boolean {
- const parsedMtu = mtu ? parseInt(mtu) : undefined;
- return (
- parsedMtu === undefined ||
- (parsedMtu >= MIN_WIREGUARD_MTU_VALUE && parsedMtu <= MAX_WIREGUARD_MTU_VALUE)
- );
- }
+ const confirmMultihop = useCallback(async () => {
+ await setMultihopImpl(true);
+ hideConfirmationDialog();
+ }, [setMultihopImpl]);
- private renderMultihopConfirmation = () => {
- return (
+ return (
+ <>
+ <AriaInputGroup>
+ <Cell.Container>
+ <AriaLabel>
+ <Cell.InputLabel>
+ {
+ // TRANSLATORS: The label next to the multihop settings toggle.
+ messages.pgettext('vpn-settings-view', 'Enable multihop')
+ }
+ </Cell.InputLabel>
+ </AriaLabel>
+ <AriaInput>
+ <Cell.Switch isOn={multihop} onChange={setMultihop} />
+ </AriaInput>
+ </Cell.Container>
+ <Cell.Footer>
+ <AriaDescription>
+ <Cell.FooterText>
+ {sprintf(
+ // TRANSLATORS: Description for multihop settings toggle.
+ // TRANSLATORS: Available placeholders:
+ // TRANSLATORS: %(wireguard)s - Will be replaced with the string "WireGuard"
+ messages.pgettext(
+ 'vpn-settings-view',
+ 'Increases anonymity by routing your traffic into one %(wireguard)s server and out another, making it harder to trace.',
+ ),
+ { wireguard: strings.wireguard },
+ )}
+ </Cell.FooterText>
+ </AriaDescription>
+ </Cell.Footer>
+ </AriaInputGroup>
<ModalAlert
- isOpen={this.state.showMultihopConfirmationDialog}
+ isOpen={confirmationDialogVisible}
type={ModalAlertType.info}
message={
// TRANSLATORS: Warning text in a dialog that is displayed after a setting is toggled.
messages.gettext('This setting increases latency. Use only if needed.')
}
buttons={[
- <AppButton.RedButton key="confirm" onClick={this.confirmWireguardMultihop}>
+ <AppButton.RedButton key="confirm" onClick={confirmMultihop}>
{messages.gettext('Enable anyway')}
</AppButton.RedButton>,
- <AppButton.BlueButton key="back" onClick={this.hideWireguardMultihopConfirmationDialog}>
+ <AppButton.BlueButton key="back" onClick={hideConfirmationDialog}>
{messages.gettext('Back')}
</AppButton.BlueButton>,
]}
- close={this.hideWireguardMultihopConfirmationDialog}></ModalAlert>
- );
- };
+ close={hideConfirmationDialog}
+ />
+ </>
+ );
+}
+
+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;
+ }, [relaySettings]);
+
+ const ipVersionItems: ISelectorItem<OptionalIpVersion>[] = useMemo(
+ () => [
+ {
+ label: messages.gettext('Automatic'),
+ value: undefined,
+ },
+ {
+ label: messages.gettext('IPv4'),
+ value: 'ipv4',
+ },
+ {
+ label: messages.gettext('IPv6'),
+ value: 'ipv6',
+ },
+ ],
+ [],
+ );
+
+ const setIpVersion = useCallback(
+ async (ipVersion?: IpVersion) => {
+ const relayUpdate = createWireguardRelayUpdater(relaySettings)
+ .tunnel.wireguard((wireguard) => {
+ if (ipVersion) {
+ wireguard.ipVersion.exact(ipVersion);
+ } else {
+ wireguard.ipVersion.any();
+ }
+ })
+ .build();
+ try {
+ await updateRelaySettings(relayUpdate);
+ } catch (e) {
+ const error = e as Error;
+ log.error('Failed to update relay settings', error.message);
+ }
+ },
+ [relaySettings, updateRelaySettings],
+ );
+
+ return (
+ <AriaInputGroup>
+ <StyledSelectorContainer>
+ <Selector
+ // TRANSLATORS: The title for the WireGuard IP version selector.
+ title={messages.pgettext('wireguard-settings-view', 'IP version')}
+ values={ipVersionItems}
+ value={ipVersion}
+ onSelect={setIpVersion}
+ />
+ </StyledSelectorContainer>
+ <Cell.Footer>
+ <AriaDescription>
+ <Cell.FooterText>
+ {sprintf(
+ // TRANSLATORS: The hint displayed below the WireGuard IP version selector.
+ // TRANSLATORS: Available placeholders:
+ // TRANSLATORS: %(wireguard)s - Will be replaced with the string "WireGuard"
+ messages.pgettext(
+ 'wireguard-settings-view',
+ 'This allows access to %(wireguard)s for devices that only support IPv6.',
+ ),
+ { wireguard: strings.wireguard },
+ )}
+ </Cell.FooterText>
+ </AriaDescription>
+ </Cell.Footer>
+ </AriaInputGroup>
+ );
+}
+
+function removeNonNumericCharacters(value: string) {
+ return value.replace(/[^0-9]/g, '');
+}
+
+function mtuIsValid(mtu: string): boolean {
+ const parsedMtu = mtu ? parseInt(mtu) : undefined;
+ return (
+ parsedMtu === undefined ||
+ (parsedMtu >= MIN_WIREGUARD_MTU_VALUE && parsedMtu <= MAX_WIREGUARD_MTU_VALUE)
+ );
+}
+
+function MtuSetting() {
+ const { setWireguardMtu: setWireguardMtuImpl } = useAppContext();
+ const mtu = useSelector((state) => state.settings.wireguard.mtu);
- private setWireguardMultihop = (newValue: boolean) => {
- if (newValue) {
- this.setState({ showMultihopConfirmationDialog: true });
- } else {
- this.props.setWireguardMultihop(false);
- }
- };
+ const setMtu = useCallback(
+ async (mtu?: number) => {
+ try {
+ await setWireguardMtuImpl(mtu);
+ } catch (e) {
+ const error = e as Error;
+ log.error('Failed to update mtu value', error.message);
+ }
+ },
+ [setWireguardMtuImpl],
+ );
- private hideWireguardMultihopConfirmationDialog = () => {
- this.setState({ showMultihopConfirmationDialog: false });
- };
+ const onSubmit = useCallback(
+ async (value: string) => {
+ const parsedValue = value === '' ? undefined : parseInt(value, 10);
+ if (mtuIsValid(value)) {
+ await setMtu(parsedValue);
+ }
+ },
+ [setMtu],
+ );
- private confirmWireguardMultihop = () => {
- this.setState({ showMultihopConfirmationDialog: false });
- this.props.setWireguardMultihop(true);
- };
+ return (
+ <AriaInputGroup>
+ <Cell.Container>
+ <AriaLabel>
+ <Cell.InputLabel>{messages.pgettext('wireguard-settings-view', 'MTU')}</Cell.InputLabel>
+ </AriaLabel>
+ <AriaInput>
+ <Cell.AutoSizingTextInput
+ value={mtu ? mtu.toString() : ''}
+ inputMode={'numeric'}
+ maxLength={4}
+ placeholder={messages.gettext('Default')}
+ onSubmitValue={onSubmit}
+ validateValue={mtuIsValid}
+ submitOnBlur={true}
+ modifyValue={removeNonNumericCharacters}
+ />
+ </AriaInput>
+ </Cell.Container>
+ <Cell.Footer>
+ <AriaDescription>
+ <Cell.FooterText>
+ {sprintf(
+ // TRANSLATORS: The hint displayed below the WireGuard MTU input field.
+ // TRANSLATORS: Available placeholders:
+ // TRANSLATORS: %(wireguard)s - Will be replaced with the string "WireGuard"
+ // TRANSLATORS: %(max)d - the maximum possible wireguard mtu value
+ // TRANSLATORS: %(min)d - the minimum possible wireguard mtu value
+ messages.pgettext(
+ 'wireguard-settings-view',
+ 'Set %(wireguard)s MTU value. Valid range: %(min)d - %(max)d.',
+ ),
+ {
+ wireguard: strings.wireguard,
+ min: MIN_WIREGUARD_MTU_VALUE,
+ max: MAX_WIREGUARD_MTU_VALUE,
+ },
+ )}
+ </Cell.FooterText>
+ </AriaDescription>
+ </Cell.Footer>
+ </AriaInputGroup>
+ );
}
diff --git a/gui/src/renderer/containers/OpenVPNSettingsPage.tsx b/gui/src/renderer/containers/OpenVPNSettingsPage.tsx
deleted file mode 100644
index ab42f4a137..0000000000
--- a/gui/src/renderer/containers/OpenVPNSettingsPage.tsx
+++ /dev/null
@@ -1,136 +0,0 @@
-import { connect } from 'react-redux';
-
-import { BridgeState, RelayProtocol } from '../../shared/daemon-rpc-types';
-import log from '../../shared/logging';
-import RelaySettingsBuilder from '../../shared/relay-settings-builder';
-import OpenVPNSettings, { BridgeModeAvailability } from '../components/OpenVPNSettings';
-import withAppContext, { IAppContext } from '../context';
-import { IHistoryProps, withHistory } from '../lib/history';
-import { RelaySettingsRedux } from '../redux/settings/reducers';
-import { IReduxState, ReduxDispatch } from '../redux/store';
-
-const mapStateToProps = (state: IReduxState) => {
- const protocolAndPort = mapRelaySettingsToProtocolAndPort(state.settings.relaySettings);
-
- let bridgeModeAvailablity = BridgeModeAvailability.available;
- if (mapRelaySettingsToProtocol(state.settings.relaySettings) !== 'openvpn') {
- bridgeModeAvailablity = BridgeModeAvailability.blockedDueToTunnelProtocol;
- } else if (protocolAndPort.openvpn.protocol === 'udp') {
- bridgeModeAvailablity = BridgeModeAvailability.blockedDueToTransportProtocol;
- }
-
- return {
- bridgeModeAvailablity,
- mssfix: state.settings.openVpn.mssfix,
- bridgeState: state.settings.bridgeState,
- ...protocolAndPort,
- };
-};
-
-const mapRelaySettingsToProtocol = (relaySettings: RelaySettingsRedux) => {
- if ('normal' in relaySettings) {
- const { tunnelProtocol } = relaySettings.normal;
- return tunnelProtocol === 'any' ? undefined : tunnelProtocol;
- // since the GUI doesn't display custom settings, just display the default ones.
- // If the user sets any settings, then those will be applied.
- } else if ('customTunnelEndpoint' in relaySettings) {
- return undefined;
- } else {
- throw new Error('Unknown type of relay settings.');
- }
-};
-
-const mapRelaySettingsToProtocolAndPort = (relaySettings: RelaySettingsRedux) => {
- if ('normal' in relaySettings) {
- const { openvpn } = relaySettings.normal;
- return {
- openvpn: {
- protocol: openvpn.protocol === 'any' ? undefined : openvpn.protocol,
- port: openvpn.port === 'any' ? undefined : openvpn.port,
- },
- };
- // since the GUI doesn't display custom settings, just display the default ones.
- // If the user sets any settings, then those will be applied.
- } else if ('customTunnelEndpoint' in relaySettings) {
- return {
- openvpn: {
- protocol: undefined,
- port: undefined,
- },
- };
- } else {
- throw new Error('Unknown type of relay settings.');
- }
-};
-
-const mapDispatchToProps = (_dispatch: ReduxDispatch, props: IHistoryProps & IAppContext) => {
- return {
- onClose: () => {
- props.history.pop();
- },
- setOpenVpnRelayProtocolAndPort: async (protocol?: RelayProtocol, port?: number) => {
- const relayUpdate = RelaySettingsBuilder.normal()
- .tunnel.openvpn((openvpn) => {
- if (protocol) {
- openvpn.protocol.exact(protocol);
- } else {
- openvpn.protocol.any();
- }
-
- if (port) {
- openvpn.port.exact(port);
- } else {
- openvpn.port.any();
- }
- })
- .build();
-
- try {
- await props.app.updateRelaySettings(relayUpdate);
- } catch (e) {
- const error = e as Error;
- log.error('Failed to update relay settings', error.message);
- }
- },
-
- setWireguardRelayPort: async (port?: number) => {
- const relayUpdate = RelaySettingsBuilder.normal()
- .tunnel.wireguard((wireguard) => {
- if (port) {
- wireguard.port.exact(port);
- } else {
- wireguard.port.any();
- }
- })
- .build();
- try {
- await props.app.updateRelaySettings(relayUpdate);
- } catch (e) {
- const error = e as Error;
- log.error('Failed to update relay settings', error.message);
- }
- },
-
- setBridgeState: async (bridgeState: BridgeState) => {
- try {
- await props.app.setBridgeState(bridgeState);
- } catch (e) {
- const error = e as Error;
- log.error(`Failed to update bridge state: ${error.message}`);
- }
- },
-
- setOpenVpnMssfix: async (mssfix?: number) => {
- try {
- await props.app.setOpenVpnMssfix(mssfix);
- } catch (e) {
- const error = e as Error;
- log.error('Failed to update mssfix value', error.message);
- }
- },
- };
-};
-
-export default withAppContext(
- withHistory(connect(mapStateToProps, mapDispatchToProps)(OpenVPNSettings)),
-);
diff --git a/gui/src/renderer/containers/WireguardSettingsPage.tsx b/gui/src/renderer/containers/WireguardSettingsPage.tsx
deleted file mode 100644
index 6217c723d2..0000000000
--- a/gui/src/renderer/containers/WireguardSettingsPage.tsx
+++ /dev/null
@@ -1,115 +0,0 @@
-import { connect } from 'react-redux';
-
-import { IpVersion } from '../../shared/daemon-rpc-types';
-import log from '../../shared/logging';
-import WireguardSettings from '../components/WireguardSettings';
-import withAppContext, { IAppContext } from '../context';
-import { createWireguardRelayUpdater } from '../lib/constraint-updater';
-import { IHistoryProps, withHistory } from '../lib/history';
-import { RelaySettingsRedux } from '../redux/settings/reducers';
-import { IReduxState, ReduxDispatch } from '../redux/store';
-
-const mapStateToProps = (state: IReduxState, props: IAppContext) => {
- const protocolAndPort = mapRelaySettingsToProtocolAndPort(state.settings.relaySettings);
-
- let wireguardMultihop = false;
- if ('normal' in state.settings.relaySettings) {
- wireguardMultihop = state.settings.relaySettings.normal.wireguard.useMultihop;
- }
-
- return {
- wireguardMtu: state.settings.wireguard.mtu,
- wireguardMultihop,
- ...protocolAndPort,
-
- setWireguardPort: async (port?: number) => {
- const relayUpdate = createWireguardRelayUpdater(state.settings.relaySettings)
- .tunnel.wireguard((wireguard) => {
- if (port) {
- wireguard.port.exact(port);
- } else {
- wireguard.port.any();
- }
- })
- .build();
- try {
- await props.app.updateRelaySettings(relayUpdate);
- } catch (e) {
- const error = e as Error;
- log.error('Failed to update relay settings', error.message);
- }
- },
-
- setWireguardIpVersion: async (ipVersion?: IpVersion) => {
- const relayUpdate = createWireguardRelayUpdater(state.settings.relaySettings)
- .tunnel.wireguard((wireguard) => {
- if (ipVersion) {
- wireguard.ipVersion.exact(ipVersion);
- } else {
- wireguard.ipVersion.any();
- }
- })
- .build();
- try {
- await props.app.updateRelaySettings(relayUpdate);
- } catch (e) {
- const error = e as Error;
- log.error('Failed to update relay settings', error.message);
- }
- },
-
- setWireguardMultihop: async (enabled: boolean) => {
- const relayUpdate = createWireguardRelayUpdater(state.settings.relaySettings)
- .tunnel.wireguard((wireguard) => wireguard.useMultihop(enabled))
- .build();
- try {
- await props.app.updateRelaySettings(relayUpdate);
- } catch (e) {
- const error = e as Error;
- log.error('Failed to update WireGuard multihop settings', error.message);
- }
- },
- };
-};
-
-const mapRelaySettingsToProtocolAndPort = (relaySettings: RelaySettingsRedux) => {
- if ('normal' in relaySettings) {
- const port = relaySettings.normal.wireguard.port;
- const ipVersion = relaySettings.normal.wireguard.ipVersion;
- return {
- wireguard: {
- port: port === 'any' ? undefined : port,
- ipVersion: ipVersion === 'any' ? undefined : ipVersion,
- },
- };
- // since the GUI doesn't display custom settings, just display the default ones.
- // If the user sets any settings, then those will be applied.
- } else if ('customTunnelEndpoint' in relaySettings) {
- return {
- wireguard: { port: undefined },
- };
- } else {
- throw new Error('Unknown type of relay settings.');
- }
-};
-
-const mapDispatchToProps = (_dispatch: ReduxDispatch, props: IHistoryProps & IAppContext) => {
- return {
- onClose: () => {
- props.history.pop();
- },
-
- setWireguardMtu: async (mtu?: number) => {
- try {
- await props.app.setWireguardMtu(mtu);
- } catch (e) {
- const error = e as Error;
- log.error('Failed to update mtu value', error.message);
- }
- },
- };
-};
-
-export default withAppContext(
- withHistory(connect(mapStateToProps, mapDispatchToProps)(WireguardSettings)),
-);
diff --git a/gui/src/renderer/lib/utilityHooks.ts b/gui/src/renderer/lib/utilityHooks.ts
index ca99a76f94..59686f1d6d 100644
--- a/gui/src/renderer/lib/utilityHooks.ts
+++ b/gui/src/renderer/lib/utilityHooks.ts
@@ -44,7 +44,7 @@ export function useAsyncEffect(
}, dependencies);
}
-export function useBoolean(initialValue: boolean) {
+export function useBoolean(initialValue = false) {
const [value, setValue] = useState(initialValue);
const setTrue = useCallback(() => setValue(true), []);