diff options
| author | Oskar Nyberg <oskar@mullvad.net> | 2022-07-25 11:40:24 +0200 |
|---|---|---|
| committer | Oskar Nyberg <oskar@mullvad.net> | 2022-07-25 11:40:24 +0200 |
| commit | d7d89095619d0bc66c2abc8e382c44969ab57bdf (patch) | |
| tree | e4a942d5a17278c47d255bb2eba53b8d662fcdc7 /gui/src | |
| parent | 9fa4063cfba1bd9bf44ad54bce650284522d6ebb (diff) | |
| parent | b570bf5ff968edd915c05747b8711461cd31945a (diff) | |
| download | mullvadvpn-d7d89095619d0bc66c2abc8e382c44969ab57bdf.tar.xz mullvadvpn-d7d89095619d0bc66c2abc8e382c44969ab57bdf.zip | |
Merge branch 'refactor-remaining-settings-views'
Diffstat (limited to 'gui/src')
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), []); |
