diff options
Diffstat (limited to 'gui/src/renderer/components')
| -rw-r--r-- | gui/src/renderer/components/AdvancedSettings.tsx | 2 | ||||
| -rw-r--r-- | gui/src/renderer/components/BridgeLocations.tsx | 2 | ||||
| -rw-r--r-- | gui/src/renderer/components/ConnectionPanel.tsx | 32 | ||||
| -rw-r--r-- | gui/src/renderer/components/ConnectionPanelDisclosure.tsx | 6 | ||||
| -rw-r--r-- | gui/src/renderer/components/LocationList.tsx | 240 | ||||
| -rw-r--r-- | gui/src/renderer/components/LocationRow.tsx | 6 | ||||
| -rw-r--r-- | gui/src/renderer/components/Locations.tsx (renamed from gui/src/renderer/components/ExitLocations.tsx) | 17 | ||||
| -rw-r--r-- | gui/src/renderer/components/OpenVPNSettings.tsx | 133 | ||||
| -rw-r--r-- | gui/src/renderer/components/SelectLocation.tsx | 216 | ||||
| -rw-r--r-- | gui/src/renderer/components/WireguardSettings.tsx | 353 |
10 files changed, 752 insertions, 255 deletions
diff --git a/gui/src/renderer/components/AdvancedSettings.tsx b/gui/src/renderer/components/AdvancedSettings.tsx index f511265464..369f5169a2 100644 --- a/gui/src/renderer/components/AdvancedSettings.tsx +++ b/gui/src/renderer/components/AdvancedSettings.tsx @@ -236,7 +236,7 @@ export default class AdvancedSettings extends React.Component<IProps, IState> { type={ModalAlertType.caution} buttons={[ <AppButton.RedButton key="confirm" onClick={this.confirmEnableBlockWhenDisconnected}> - {messages.pgettext('advanced-settings-view', 'Enable anyway')} + {messages.gettext('Enable anyway')} </AppButton.RedButton>, <AppButton.BlueButton key="back" onClick={this.hideConfirmBlockWhenDisconnectedAlert}> {messages.gettext('Back')} diff --git a/gui/src/renderer/components/BridgeLocations.tsx b/gui/src/renderer/components/BridgeLocations.tsx index 802797eb77..a62368e166 100644 --- a/gui/src/renderer/components/BridgeLocations.tsx +++ b/gui/src/renderer/components/BridgeLocations.tsx @@ -17,6 +17,7 @@ export enum SpecialBridgeLocationType { interface IBridgeLocationsProps { source: IRelayLocationRedux[]; + locale: string; defaultExpandedLocations?: RelayLocation[]; selectedValue?: LiftedConstraint<RelayLocation>; selectedElementRef?: React.Ref<React.ReactInstance>; @@ -53,6 +54,7 @@ const BridgeLocations = React.forwardRef(function BridgeLocationsT( </SpecialLocations> <RelayLocations source={props.source} + locale={props.locale} onWillExpand={props.onWillExpand} onTransitionEnd={props.onTransitionEnd} /> diff --git a/gui/src/renderer/components/ConnectionPanel.tsx b/gui/src/renderer/components/ConnectionPanel.tsx index d0b0098d50..65a47ea1b3 100644 --- a/gui/src/renderer/components/ConnectionPanel.tsx +++ b/gui/src/renderer/components/ConnectionPanel.tsx @@ -11,6 +11,7 @@ import { } from '../../shared/daemon-rpc-types'; import { messages } from '../../shared/gettext'; import { default as ConnectionPanelDisclosure } from '../components/ConnectionPanelDisclosure'; +import Marquee from './Marquee'; export interface IEndpoint { ip: string; @@ -35,7 +36,9 @@ interface IProps { isOpen: boolean; hostname?: string; bridgeHostname?: string; + entryHostname?: string; inAddress?: IInAddress; + entryLocationInAddress?: IInAddress; bridgeInfo?: IBridgeData; outAddress?: IOutAddress; onToggle: () => void; @@ -72,15 +75,15 @@ const Header = styled.div({ export default class ConnectionPanel extends React.Component<IProps> { public render() { - const { inAddress, outAddress, bridgeInfo } = this.props; - const entryPoint = bridgeInfo && inAddress ? bridgeInfo : inAddress; + const { outAddress } = this.props; + const entryPoint = this.getEntryPoint(); return ( <div className={this.props.className}> {this.props.hostname && ( <Header> <ConnectionPanelDisclosure pointsUp={this.props.isOpen} onToggle={this.props.onToggle}> - {this.hostnameLine()} + <Marquee>{this.hostnameLine()}</Marquee> </ConnectionPanelDisclosure> </Header> )} @@ -117,17 +120,34 @@ export default class ConnectionPanel extends React.Component<IProps> { ); } + private getEntryPoint() { + const { inAddress, entryLocationInAddress, bridgeInfo } = this.props; + + if (entryLocationInAddress && inAddress) { + return entryLocationInAddress; + } else if (bridgeInfo && inAddress) { + return bridgeInfo; + } else { + return inAddress; + } + } + private hostnameLine() { if (this.props.hostname && this.props.bridgeHostname) { + return sprintf(messages.pgettext('connection-info', '%(relay)s via %(entry)s'), { + relay: this.props.hostname, + entry: this.props.bridgeHostname, + }); + } else if (this.props.hostname && this.props.entryHostname) { return sprintf( // TRANSLATORS: The hostname line displayed below the country on the main screen // TRANSLATORS: Available placeholders: // TRANSLATORS: %(relay)s - the relay hostname - // TRANSLATORS: %(bridge)s - the bridge hostname - messages.pgettext('connection-info', '%(relay)s via %(bridge)s'), + // TRANSLATORS: %(entry)s - the entry relay hostname + messages.pgettext('connection-info', '%(relay)s via %(entry)s'), { relay: this.props.hostname, - bridge: this.props.bridgeHostname, + entry: this.props.entryHostname, }, ); } else { diff --git a/gui/src/renderer/components/ConnectionPanelDisclosure.tsx b/gui/src/renderer/components/ConnectionPanelDisclosure.tsx index ad7c092f9c..d0727ddb6f 100644 --- a/gui/src/renderer/components/ConnectionPanelDisclosure.tsx +++ b/gui/src/renderer/components/ConnectionPanelDisclosure.tsx @@ -6,13 +6,15 @@ import ImageView from './ImageView'; const Container = styled.div({ display: 'flex', alignItems: 'center', + width: '100%', }); -const Caption = styled.span((props: { open: boolean }) => ({ +const Caption = styled.span({}, (props: { open: boolean }) => ({ fontFamily: 'Open Sans', fontSize: '15px', fontWeight: 600, lineHeight: '20px', + minWidth: '0px', color: props.open ? colors.white : colors.white40, [Container + ':hover &']: { color: colors.white, @@ -28,7 +30,7 @@ const Chevron = styled(ImageView)({ interface IProps { pointsUp: boolean; onToggle?: () => void; - children: React.ReactText; + children: React.ReactNode; className?: string; } diff --git a/gui/src/renderer/components/LocationList.tsx b/gui/src/renderer/components/LocationList.tsx index 6389784ecd..855efdc066 100644 --- a/gui/src/renderer/components/LocationList.tsx +++ b/gui/src/renderer/components/LocationList.tsx @@ -1,4 +1,5 @@ import * as React from 'react'; +import { sprintf } from 'sprintf-js'; import styled from 'styled-components'; import { colors } from '../../config.json'; import { @@ -7,8 +8,12 @@ import { RelayLocation, relayLocationComponents, } from '../../shared/daemon-rpc-types'; -import { relayLocations } from '../../shared/gettext'; -import { IRelayLocationRedux } from '../redux/settings/reducers'; +import { messages, relayLocations } from '../../shared/gettext'; +import { + IRelayLocationRedux, + IRelayLocationCityRedux, + IRelayLocationRelayRedux, +} from '../redux/settings/reducers'; import * as Cell from './cell'; import LocationRow from './LocationRow'; @@ -257,35 +262,82 @@ export class SpecialLocation<T> extends React.Component<ISpecialLocationProps<T> }; } +export enum DisabledReason { + entry, + exit, + inactive, +} + interface IRelayLocationsProps { source: IRelayLocationRedux[]; + locale: string; selectedLocation?: RelayLocation; selectedElementRef?: React.Ref<React.ReactInstance>; expandedItems?: RelayLocation[]; + disabledLocation?: { location: RelayLocation; reason: DisabledReason }; onSelect?: (location: RelayLocation) => void; onExpand?: (location: RelayLocation, expand: boolean) => void; onWillExpand?: (locationRect: DOMRect, expandedContentHeight: number) => void; onTransitionEnd?: () => void; } +interface Relay extends IRelayLocationRelayRedux { + label: string; + disabled: boolean; +} + +interface City extends Omit<IRelayLocationCityRedux, 'relays'> { + label: string; + active: boolean; + disabled: boolean; + relays: Array<Relay>; +} + +interface Country extends Omit<IRelayLocationRedux, 'cities'> { + label: string; + active: boolean; + disabled: boolean; + cities: Array<City>; +} + +type CountryList = Array<Country>; + +interface IRelayLocationsState { + countries: CountryList; +} + interface ICommonCellProps { location: RelayLocation; selected: boolean; ref?: React.Ref<HTMLDivElement>; } -export class RelayLocations extends React.PureComponent<IRelayLocationsProps> { +export class RelayLocations extends React.PureComponent< + IRelayLocationsProps, + IRelayLocationsState +> { + public state = { + countries: this.prepareRelaysForPresentation(this.props.source), + }; + + public componentDidUpdate(prevProps: IRelayLocationsProps) { + if (this.props.source !== prevProps.source) { + this.setState({ countries: this.prepareRelaysForPresentation(this.props.source) }); + } + } + public render() { return ( <> - {this.props.source.map((relayCountry) => { + {this.state.countries.map((relayCountry) => { const countryLocation: RelayLocation = { country: relayCountry.code }; return ( <LocationRow key={getLocationKey(countryLocation)} - name={relayLocations.gettext(relayCountry.name)} - active={relayCountry.hasActiveRelays} + name={relayCountry.label} + active={relayCountry.active} + disabled={relayCountry.disabled} expanded={this.isExpanded(countryLocation)} onSelect={this.handleSelection} onExpand={this.handleExpand} @@ -300,8 +352,9 @@ export class RelayLocations extends React.PureComponent<IRelayLocationsProps> { return ( <LocationRow key={getLocationKey(cityLocation)} - name={relayLocations.gettext(relayCity.name)} - active={relayCity.hasActiveRelays} + name={relayCity.label} + active={relayCity.active} + disabled={relayCity.disabled} expanded={this.isExpanded(cityLocation)} onSelect={this.handleSelection} onExpand={this.handleExpand} @@ -316,8 +369,9 @@ export class RelayLocations extends React.PureComponent<IRelayLocationsProps> { return ( <LocationRow key={getLocationKey(relayLocation)} - name={relay.hostname} + name={relay.label} active={relay.active} + disabled={relay.disabled} onSelect={this.handleSelection} {...this.getCommonCellProps(relayLocation)} /> @@ -333,6 +387,174 @@ export class RelayLocations extends React.PureComponent<IRelayLocationsProps> { ); } + private prepareRelaysForPresentation(relayList: IRelayLocationRedux[]): CountryList { + return relayList + .map((country) => { + const countryDisabled = this.isCountryDisabled(country, country.code); + const countryLocation = { country: country.code }; + + return { + ...country, + label: this.formatRowName(country.name, countryLocation, countryDisabled), + active: countryDisabled !== DisabledReason.inactive, + disabled: countryDisabled !== undefined, + cities: country.cities + .map((city) => { + const cityDisabled = + countryDisabled ?? this.isCityDisabled(city, [country.code, city.code]); + const cityLocation: RelayLocation = { city: [country.code, city.code] }; + + return { + ...city, + label: this.formatRowName(city.name, cityLocation, cityDisabled), + active: cityDisabled !== DisabledReason.inactive, + disabled: cityDisabled !== undefined, + relays: city.relays + .map((relay) => { + const relayDisabled = + countryDisabled ?? + cityDisabled ?? + this.isRelayDisabled(relay, [country.code, city.code, relay.hostname]); + const relayLocation: RelayLocation = { + hostname: [country.code, city.code, relay.hostname], + }; + + return { + ...relay, + label: this.formatRowName(relay.hostname, relayLocation, relayDisabled), + disabled: relayDisabled !== undefined, + }; + }) + .sort((a, b) => + a.hostname.localeCompare(b.hostname, this.props.locale, { numeric: true }), + ), + }; + }) + .sort((a, b) => a.name.localeCompare(b.name, this.props.locale)), + }; + }) + .sort((a, b) => a.name.localeCompare(b.name, this.props.locale)); + } + + private formatRowName( + name: string, + location: RelayLocation, + disabledReason?: DisabledReason, + ): string { + const translatedName = 'hostname' in location ? name : relayLocations.gettext(name); + const disabledLocation = this.props.disabledLocation; + const matchDisabledLocation = compareRelayLocationLoose(location, disabledLocation?.location); + + let info: string | undefined; + if ( + disabledReason === DisabledReason.entry || + (matchDisabledLocation && disabledLocation?.reason === DisabledReason.entry) + ) { + info = messages.pgettext('select-location-view', 'Entry'); + } else if ( + disabledReason === DisabledReason.exit || + (matchDisabledLocation && disabledLocation?.reason === DisabledReason.exit) + ) { + info = messages.pgettext('select-location-view', 'Exit'); + } + + return info !== undefined + ? sprintf( + // TRANSLATORS: This is used for appending information about a location. + // TRANSLATORS: E.g. "Gothenburg (Entry)" if Gothenburg has been selected as the entrypoint. + // TRANSLATORS: Available placeholders: + // TRANSLATORS: %(location)s - Translated location name + // TRANSLATORS: %(info)s - Information about the location + messages.pgettext('select-location-view', '%(location)s (%(info)s)'), + { + location: translatedName, + info, + }, + ) + : translatedName; + } + + private isRelayDisabled( + relay: IRelayLocationRelayRedux, + location: [string, string, string], + ): DisabledReason | undefined { + if (!relay.active) { + return DisabledReason.inactive; + } else if ( + this.props.disabledLocation && + compareRelayLocation({ hostname: location }, this.props.disabledLocation.location) + ) { + return this.props.disabledLocation.reason; + } else { + return undefined; + } + } + + private isCityDisabled( + city: IRelayLocationCityRedux, + location: [string, string], + ): DisabledReason | undefined { + const relaysDisabled = city.relays.map((relay) => + this.isRelayDisabled(relay, [...location, relay.hostname]), + ); + if (relaysDisabled.every((status) => status === DisabledReason.inactive)) { + return DisabledReason.inactive; + } + + const disabledDueToSelection = relaysDisabled.find( + (status) => status === DisabledReason.entry || status === DisabledReason.exit, + ); + + if ( + relaysDisabled.every((status) => status !== undefined) && + disabledDueToSelection !== undefined + ) { + return disabledDueToSelection; + } + + if ( + this.props.disabledLocation && + compareRelayLocation({ city: location }, this.props.disabledLocation.location) && + city.relays.filter((relay) => relay.active).length <= 1 + ) { + return this.props.disabledLocation.reason; + } + + return undefined; + } + + private isCountryDisabled( + country: IRelayLocationRedux, + location: string, + ): DisabledReason | undefined { + const citiesDisabled = country.cities.map((city) => + this.isCityDisabled(city, [location, city.code]), + ); + if (citiesDisabled.every((status) => status === DisabledReason.inactive)) { + return DisabledReason.inactive; + } + + const disabledDueToSelection = citiesDisabled.find( + (status) => status === DisabledReason.entry || status === DisabledReason.exit, + ); + if ( + citiesDisabled.every((status) => status !== undefined) && + disabledDueToSelection !== undefined + ) { + return disabledDueToSelection; + } + + if ( + this.props.disabledLocation && + compareRelayLocation({ country: location }, this.props.disabledLocation.location) && + country.cities.flatMap((city) => city.relays).filter((relay) => relay.active).length <= 1 + ) { + return this.props.disabledLocation.reason; + } + + return undefined; + } + private isExpanded(relayLocation: RelayLocation) { return (this.props.expandedItems || []).some((location) => compareRelayLocation(location, relayLocation), diff --git a/gui/src/renderer/components/LocationRow.tsx b/gui/src/renderer/components/LocationRow.tsx index cf7bc5942c..980ffaae3e 100644 --- a/gui/src/renderer/components/LocationRow.tsx +++ b/gui/src/renderer/components/LocationRow.tsx @@ -67,6 +67,7 @@ const Label = styled(Cell.Label)({ interface IProps { name: string; active: boolean; + disabled: boolean; location: RelayLocation; selected: boolean; expanded?: boolean; @@ -105,13 +106,13 @@ function LocationRow(props: IProps, ref: React.Ref<HTMLDivElement>) { <Container ref={ref} selected={props.selected} - disabled={!props.active} + disabled={props.disabled} location={props.location}> <Button ref={buttonRef} onClick={handleClick} location={props.location} - disabled={!props.active}> + disabled={props.disabled}> <RelayStatusIndicator active={props.active} selected={props.selected} /> <Label>{props.name}</Label> </Button> @@ -149,6 +150,7 @@ function compareProps(oldProps: IProps, nextProps: IProps): boolean { React.Children.count(oldProps.children) === React.Children.count(nextProps.children) && oldProps.name === nextProps.name && oldProps.active === nextProps.active && + oldProps.disabled === nextProps.disabled && oldProps.selected === nextProps.selected && oldProps.expanded === nextProps.expanded && oldProps.onSelect === nextProps.onSelect && diff --git a/gui/src/renderer/components/ExitLocations.tsx b/gui/src/renderer/components/Locations.tsx index e4174a0e7c..901d27c9ec 100644 --- a/gui/src/renderer/components/ExitLocations.tsx +++ b/gui/src/renderer/components/Locations.tsx @@ -2,25 +2,25 @@ import * as React from 'react'; import { RelayLocation } from '../../shared/daemon-rpc-types'; import { IRelayLocationRedux } from '../redux/settings/reducers'; import LocationList, { + DisabledReason, LocationSelection, LocationSelectionType, RelayLocations, } from './LocationList'; -interface IExitLocationsProps { +interface ILocationsProps { source: IRelayLocationRedux[]; + locale: string; defaultExpandedLocations?: RelayLocation[]; selectedValue?: RelayLocation; + disabledLocation?: { location: RelayLocation; reason: DisabledReason }; selectedElementRef?: React.Ref<React.ReactInstance>; onSelect?: (value: LocationSelection<never>) => void; onWillExpand?: (locationRect: DOMRect, expandedContentHeight: number) => void; onTransitionEnd?: () => void; } -const ExitLocations = React.forwardRef(function ExitLocationsT( - props: IExitLocationsProps, - ref: React.Ref<LocationList<never>>, -) { +function Locations(props: ILocationsProps, ref: React.Ref<LocationList<never>>) { const selectedValue: LocationSelection<never> | undefined = props.selectedValue ? { type: LocationSelectionType.relay, value: props.selectedValue } : undefined; @@ -34,11 +34,14 @@ const ExitLocations = React.forwardRef(function ExitLocationsT( onSelect={props.onSelect}> <RelayLocations source={props.source} + locale={props.locale} + disabledLocation={props.disabledLocation} onWillExpand={props.onWillExpand} onTransitionEnd={props.onTransitionEnd} /> </LocationList> ); -}); +} -export default ExitLocations; +export const ExitLocations = React.forwardRef(Locations); +export const EntryLocations = React.forwardRef(Locations); diff --git a/gui/src/renderer/components/OpenVPNSettings.tsx b/gui/src/renderer/components/OpenVPNSettings.tsx index 72e672b681..1696f45052 100644 --- a/gui/src/renderer/components/OpenVPNSettings.tsx +++ b/gui/src/renderer/components/OpenVPNSettings.tsx @@ -3,10 +3,11 @@ import { sprintf } from 'sprintf-js'; import styled from 'styled-components'; import { BridgeState, RelayProtocol } from '../../shared/daemon-rpc-types'; import { messages } from '../../shared/gettext'; +import * as AppButton from './AppButton'; import { AriaDescription, AriaInput, AriaInputGroup, AriaLabel } from './AriaGroup'; import * as Cell from './cell'; import { Layout, SettingsContainer } from './Layout'; -import { ModalContainer } from './Modal'; +import { ModalAlert, ModalAlertType, ModalContainer } from './Modal'; import { BackBarItem, NavigationBar, @@ -17,6 +18,7 @@ import { } from './NavigationBar'; import Selector, { ISelectorItem } from './cell/Selector'; import SettingsHeader, { HeaderTitle } from './SettingsHeader'; +import { formatMarkdown } from '../markdown-formatter'; const MIN_MSSFIX_VALUE = 1000; const MAX_MSSFIX_VALUE = 1450; @@ -43,7 +45,12 @@ export const StyledInputFrame = styled(Cell.InputFrame)({ flex: 0, }); +export const StyledSelectorForFooter = (styled(Selector)({ + marginBottom: 0, +}) as unknown) as new <T>() => Selector<T>; + interface IProps { + tunnelProtocolIsOpenVpn: boolean; openvpn: { protocol?: RelayProtocol; port?: number; @@ -56,10 +63,15 @@ interface IProps { onClose: () => void; } -export default class OpenVpnSettings extends React.Component<IProps> { +interface IState { + showBridgeStateConfirmationDialog: boolean; +} + +export default class OpenVpnSettings extends React.Component<IProps, IState> { + public state = { showBridgeStateConfirmationDialog: false }; + private portItems: { [key in RelayProtocol]: Array<ISelectorItem<OptionalPort>> }; private protocolItems: Array<ISelectorItem<OptionalRelayProtocol>>; - private bridgeStateItems: Array<ISelectorItem<BridgeState>>; constructor(props: IProps) { super(props); @@ -88,21 +100,6 @@ export default class OpenVpnSettings extends React.Component<IProps> { value: 'udp', }, ]; - - this.bridgeStateItems = [ - { - label: messages.gettext('Automatic'), - value: 'auto', - }, - { - label: messages.gettext('On'), - value: 'on', - }, - { - label: messages.gettext('Off'), - value: 'off', - }, - ]; } public render() { @@ -164,15 +161,39 @@ export default class OpenVpnSettings extends React.Component<IProps> { </AriaInputGroup> <AriaInputGroup> - <Selector - title={ - // TRANSLATORS: The title for the shadowsocks bridge selector section. - messages.pgettext('openvpn-settings-view', 'Bridge mode') - } - values={this.bridgeStateItems} - value={this.props.bridgeState} - onSelect={this.onSelectBridgeState} - /> + <StyledSelectorContainer> + <StyledSelectorForFooter + title={ + // TRANSLATORS: The title for the shadowsocks bridge selector section. + messages.pgettext('openvpn-settings-view', 'Bridge mode') + } + values={this.bridgeStateItems(this.props.tunnelProtocolIsOpenVpn)} + value={this.props.bridgeState} + onSelect={this.onSelectBridgeState} + /> + </StyledSelectorContainer> + <Cell.Footer> + <AriaDescription> + <Cell.FooterText> + {this.props.tunnelProtocolIsOpenVpn + ? // This line is here to prevent prettier from moving up the next line. + // TRANSLATORS: This is used as a description for the bridge mode + // TRANSLATORS: setting. + messages.pgettext( + 'openvpn-settings-view', + 'Helps circumvent censorship, by routing your traffic through a bridge server before reaching an OpenVPN server. Obfuscation is added to make fingerprinting harder.', + ) + : formatMarkdown( + // TRANSLATORS: This is used to instruct users how to make the bridge + // TRANSLATORS: mode setting available. + messages.pgettext( + 'openvpn-settings-view', + 'To activate Bridge mode, go back and change **Tunnel protocol** to **OpenVPN**.', + ), + )} + </Cell.FooterText> + </AriaDescription> + </Cell.Footer> </AriaInputGroup> <AriaInputGroup> @@ -222,10 +243,30 @@ export default class OpenVpnSettings extends React.Component<IProps> { </NavigationContainer> </SettingsContainer> </Layout> + + {this.state.showBridgeStateConfirmationDialog && this.renderBridgeStateConfirmation()} </ModalContainer> ); } + 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', + }, + ]; + } + private onSelectOpenvpnProtocol = (protocol?: RelayProtocol) => { this.props.setOpenVpnRelayProtocolAndPort(protocol); }; @@ -234,10 +275,6 @@ export default class OpenVpnSettings extends React.Component<IProps> { this.props.setOpenVpnRelayProtocolAndPort(this.props.openvpn.protocol, port); }; - private onSelectBridgeState = (bridgeState: BridgeState) => { - this.props.setBridgeState(bridgeState); - }; - private onMssfixSubmit = (value: string) => { const parsedValue = value === '' ? undefined : parseInt(value, 10); if (OpenVpnSettings.mssfixIsValid(value)) { @@ -256,4 +293,38 @@ export default class OpenVpnSettings extends React.Component<IProps> { (parsedMssFix >= MIN_MSSFIX_VALUE && parsedMssFix <= MAX_MSSFIX_VALUE) ); } + + private renderBridgeStateConfirmation = () => { + return ( + <ModalAlert + type={ModalAlertType.info} + message={messages.gettext('This setting increases latency. Use only if needed.')} + buttons={[ + <AppButton.RedButton key="confirm" onClick={this.confirmBridgeState}> + {messages.gettext('Enable anyway')} + </AppButton.RedButton>, + <AppButton.BlueButton key="back" onClick={this.hideBridgeStateConfirmationDialog}> + {messages.gettext('Back')} + </AppButton.BlueButton>, + ]} + close={this.hideBridgeStateConfirmationDialog}></ModalAlert> + ); + }; + + private onSelectBridgeState = (newValue: BridgeState) => { + if (newValue === 'on') { + this.setState({ showBridgeStateConfirmationDialog: true }); + } else { + this.props.setBridgeState(newValue); + } + }; + + private hideBridgeStateConfirmationDialog = () => { + this.setState({ showBridgeStateConfirmationDialog: false }); + }; + + private confirmBridgeState = () => { + this.setState({ showBridgeStateConfirmationDialog: false }); + this.props.setBridgeState('on'); + }; } diff --git a/gui/src/renderer/components/SelectLocation.tsx b/gui/src/renderer/components/SelectLocation.tsx index 528e8129ef..b6698fcbc7 100644 --- a/gui/src/renderer/components/SelectLocation.tsx +++ b/gui/src/renderer/components/SelectLocation.tsx @@ -1,16 +1,19 @@ import React from 'react'; import { sprintf } from 'sprintf-js'; import { colors } from '../../config.json'; -import { LiftedConstraint, RelayLocation } from '../../shared/daemon-rpc-types'; +import { LiftedConstraint, RelayLocation, TunnelProtocol } from '../../shared/daemon-rpc-types'; import { messages } from '../../shared/gettext'; import { IRelayLocationRedux } from '../redux/settings/reducers'; -import { LocationScope } from '../redux/userinterface/reducers'; import BridgeLocations, { SpecialBridgeLocationType } from './BridgeLocations'; import { CustomScrollbarsRef } from './CustomScrollbars'; -import ExitLocations from './ExitLocations'; +import { EntryLocations, ExitLocations } from './Locations'; import ImageView from './ImageView'; import { Layout } from './Layout'; -import LocationList, { LocationSelection, LocationSelectionType } from './LocationList'; +import LocationList, { + DisabledReason, + LocationSelection, + LocationSelectionType, +} from './LocationList'; import { CloseBarItem, NavigationBar, @@ -37,25 +40,33 @@ import { import { HeaderSubTitle, HeaderTitle } from './SettingsHeader'; interface IProps { - locationScope: LocationScope; + locale: string; selectedExitLocation?: RelayLocation; + selectedEntryLocation?: RelayLocation; selectedBridgeLocation?: LiftedConstraint<RelayLocation>; relayLocations: IRelayLocationRedux[]; bridgeLocations: IRelayLocationRedux[]; - allowBridgeSelection: boolean; + allowEntrySelection: boolean; + tunnelProtocol: LiftedConstraint<TunnelProtocol>; providers: string[]; onClose: () => void; onViewFilterByProvider: () => void; - onChangeLocationScope: (location: LocationScope) => void; onSelectExitLocation: (location: RelayLocation) => void; + onSelectEntryLocation: (location: RelayLocation) => void; onSelectBridgeLocation: (location: RelayLocation) => void; onSelectClosestToExit: () => void; onClearProviders: () => void; } +enum LocationScope { + entry = 0, + exit, +} + interface IState { showFilterMenu: boolean; headingHeight: number; + locationScope: LocationScope; } interface ISelectLocationSnapshot { @@ -64,17 +75,19 @@ interface ISelectLocationSnapshot { } export default class SelectLocation extends React.Component<IProps, IState> { - public state = { showFilterMenu: false, headingHeight: 0 }; + public state = { showFilterMenu: false, headingHeight: 0, locationScope: LocationScope.exit }; private scrollView = React.createRef<CustomScrollbarsRef>(); private spacePreAllocationViewRef = React.createRef<SpacePreAllocationView>(); private selectedExitLocationRef = React.createRef<React.ReactInstance>(); + private selectedEntryLocationRef = React.createRef<React.ReactInstance>(); private selectedBridgeLocationRef = React.createRef<React.ReactInstance>(); private exitLocationList = React.createRef<LocationList<never>>(); + private entryLocationList = React.createRef<LocationList<never>>(); private bridgeLocationList = React.createRef<LocationList<SpecialBridgeLocationType>>(); - private snapshotByScope: { [index: number]: ISelectLocationSnapshot } = {}; + private snapshotByScope: Partial<Record<LocationScope, ISelectLocationSnapshot>> = {}; private filterButtonRef = React.createRef<HTMLDivElement>(); private headerRef = React.createRef<HTMLHeadingElement>(); @@ -87,25 +100,25 @@ export default class SelectLocation extends React.Component<IProps, IState> { } public componentDidUpdate( - prevProps: IProps, - _prevState: unknown, + _prevProps: IProps, + prevState: IState, snapshot?: ISelectLocationSnapshot, ) { - if (this.props.locationScope !== prevProps.locationScope) { - this.restoreScrollPosition(this.props.locationScope); + if (this.state.locationScope !== prevState.locationScope) { + this.restoreScrollPosition(this.state.locationScope); if (snapshot) { - this.snapshotByScope[prevProps.locationScope] = snapshot; + this.snapshotByScope[prevState.locationScope] = snapshot; } } } - public getSnapshotBeforeUpdate(prevProps: IProps): ISelectLocationSnapshot | undefined { + public getSnapshotBeforeUpdate( + prevProps: IProps, + prevState: IState, + ): ISelectLocationSnapshot | undefined { const scrollView = this.scrollView.current; - const locationList = - prevProps.locationScope === LocationScope.relay - ? this.exitLocationList.current - : this.bridgeLocationList.current; + const locationList = this.getLocationListRef(prevProps, prevState); if (scrollView && locationList) { return { @@ -164,13 +177,7 @@ export default class SelectLocation extends React.Component<IProps, IState> { messages.pgettext('select-location-view', 'Select location') } </HeaderTitle> - <HeaderSubTitle> - {this.props.allowBridgeSelection && - messages.pgettext( - 'select-location-view', - 'While connected, your traffic will be routed through two secure locations, the entry point (a bridge server) and the exit point (a VPN server).', - )} - </HeaderSubTitle> + {this.renderHeaderSubtitle()} </StyledSettingsHeader> {this.props.providers.length > 0 && ( @@ -200,45 +207,21 @@ export default class SelectLocation extends React.Component<IProps, IState> { </StyledProvidersCount> </StyledProviderCountRow> )} - {this.props.allowBridgeSelection && ( + {this.props.allowEntrySelection && ( <StyledScopeBar - defaultSelectedIndex={this.props.locationScope} - onChange={this.props.onChangeLocationScope}> + defaultSelectedIndex={this.state.locationScope} + onChange={this.onChangeLocationScope}> <ScopeBarItem> - {messages.pgettext('select-location-nav', 'Entry')} + {messages.pgettext('select-location-view', 'Entry')} </ScopeBarItem> <ScopeBarItem> - {messages.pgettext('select-location-nav', 'Exit')} + {messages.pgettext('select-location-view', 'Exit')} </ScopeBarItem> </StyledScopeBar> )} </StyledNavigationBarAttachment> - <StyledContent> - {this.props.locationScope === LocationScope.relay ? ( - <ExitLocations - ref={this.exitLocationList} - source={this.props.relayLocations} - defaultExpandedLocations={this.getExpandedLocationsFromSnapshot()} - selectedValue={this.props.selectedExitLocation} - selectedElementRef={this.selectedExitLocationRef} - onSelect={this.onSelectExitLocation} - onWillExpand={this.onWillExpand} - onTransitionEnd={this.resetHeight} - /> - ) : ( - <BridgeLocations - ref={this.bridgeLocationList} - source={this.props.bridgeLocations} - defaultExpandedLocations={this.getExpandedLocationsFromSnapshot()} - selectedValue={this.props.selectedBridgeLocation} - selectedElementRef={this.selectedBridgeLocationRef} - onSelect={this.onSelectBridgeLocation} - onWillExpand={this.onWillExpand} - onTransitionEnd={this.resetHeight} - /> - )} - </StyledContent> + <StyledContent>{this.renderLocationList()}</StyledContent> </SpacePreAllocationView> </NavigationScrollbars> </NavigationContainer> @@ -257,12 +240,118 @@ export default class SelectLocation extends React.Component<IProps, IState> { } } + private getLocationListRef(prevProps: IProps, prevState: IState) { + if (prevState.locationScope === LocationScope.exit) { + return this.exitLocationList.current; + } else if (prevProps.tunnelProtocol === 'wireguard') { + return this.entryLocationList.current; + } else { + return this.bridgeLocationList.current; + } + } + + private getSelectedLocationRef() { + if (this.state.locationScope === LocationScope.exit) { + return this.selectedExitLocationRef.current; + } else if (this.props.tunnelProtocol === 'wireguard') { + return this.selectedEntryLocationRef.current; + } else { + return this.selectedBridgeLocationRef.current; + } + } + + private renderHeaderSubtitle() { + if (this.props.allowEntrySelection) { + if (this.props.tunnelProtocol === 'openvpn') { + return ( + <HeaderSubTitle> + {messages.pgettext( + 'select-location-view', + 'While connected, your traffic will be routed through two secure locations, the entry point (a bridge server) and the exit point (a VPN server).', + )} + </HeaderSubTitle> + ); + } else { + return ( + <HeaderSubTitle> + {messages.pgettext( + 'select-location-view', + 'While connected, your traffic will be routed through two secure locations, the entry point and the exit point (needs to be two different VPN servers).', + )} + </HeaderSubTitle> + ); + } + } else { + return null; + } + } + + private renderLocationList() { + if (this.state.locationScope === LocationScope.exit) { + const disabledLocation = this.props.selectedEntryLocation + ? { + location: this.props.selectedEntryLocation, + reason: DisabledReason.entry, + } + : undefined; + return ( + <ExitLocations + ref={this.exitLocationList} + source={this.props.relayLocations} + locale={this.props.locale} + defaultExpandedLocations={this.getExpandedLocationsFromSnapshot()} + selectedValue={this.props.selectedExitLocation} + selectedElementRef={this.selectedExitLocationRef} + disabledLocation={disabledLocation} + onSelect={this.onSelectExitLocation} + onWillExpand={this.onWillExpand} + onTransitionEnd={this.resetHeight} + /> + ); + } else if (this.props.tunnelProtocol === 'any' || this.props.tunnelProtocol === 'wireguard') { + const disabledLocation = this.props.selectedExitLocation + ? { + location: this.props.selectedExitLocation, + reason: DisabledReason.exit, + } + : undefined; + return ( + <EntryLocations + ref={this.entryLocationList} + source={this.props.relayLocations} + locale={this.props.locale} + defaultExpandedLocations={this.getExpandedLocationsFromSnapshot()} + selectedValue={this.props.selectedEntryLocation} + selectedElementRef={this.selectedEntryLocationRef} + disabledLocation={disabledLocation} + onSelect={this.onSelectEntryLocation} + onWillExpand={this.onWillExpand} + onTransitionEnd={this.resetHeight} + /> + ); + } else { + return ( + <BridgeLocations + ref={this.bridgeLocationList} + source={this.props.bridgeLocations} + locale={this.props.locale} + defaultExpandedLocations={this.getExpandedLocationsFromSnapshot()} + selectedValue={this.props.selectedBridgeLocation} + selectedElementRef={this.selectedBridgeLocationRef} + onSelect={this.onSelectBridgeLocation} + onWillExpand={this.onWillExpand} + onTransitionEnd={this.resetHeight} + /> + ); + } + } + private resetHeight = () => { this.spacePreAllocationViewRef.current?.reset(); }; private getExpandedLocationsFromSnapshot(): RelayLocation[] | undefined { - const snapshot = this.snapshotByScope[this.props.locationScope]; + const snapshot = this.snapshotByScope[this.state.locationScope]; if (snapshot) { return snapshot.expandedLocations; } else { @@ -278,10 +367,7 @@ export default class SelectLocation extends React.Component<IProps, IState> { } private scrollToSelectedCell() { - const ref = - this.props.locationScope === LocationScope.relay - ? this.selectedExitLocationRef.current - : this.selectedBridgeLocationRef.current; + const ref = this.getSelectedLocationRef(); const scrollView = this.scrollView.current; if (scrollView) { @@ -295,12 +381,20 @@ export default class SelectLocation extends React.Component<IProps, IState> { } } + private onChangeLocationScope = (locationScope: LocationScope) => { + this.setState({ locationScope }); + }; + private onSelectExitLocation = (location: LocationSelection<never>) => { if (location.type === LocationSelectionType.relay) { this.props.onSelectExitLocation(location.value); } }; + private onSelectEntryLocation = (location: LocationSelection<never>) => { + this.props.onSelectEntryLocation(location.value); + }; + private onSelectBridgeLocation = (location: LocationSelection<SpecialBridgeLocationType>) => { if (location.type === LocationSelectionType.relay) { this.props.onSelectBridgeLocation(location.value); diff --git a/gui/src/renderer/components/WireguardSettings.tsx b/gui/src/renderer/components/WireguardSettings.tsx index 8bd379d7d4..d41d647266 100644 --- a/gui/src/renderer/components/WireguardSettings.tsx +++ b/gui/src/renderer/components/WireguardSettings.tsx @@ -3,9 +3,11 @@ import { sprintf } from 'sprintf-js'; import styled from 'styled-components'; import { IpVersion } from '../../shared/daemon-rpc-types'; import { messages } from '../../shared/gettext'; +import * as AppButton from './AppButton'; import { AriaDescription, AriaInput, AriaInputGroup, AriaLabel } from './AriaGroup'; import * as Cell from './cell'; import { Layout, SettingsContainer } from './Layout'; +import { ModalAlert, ModalAlertType, ModalContainer } from './Modal'; import { BackBarItem, NavigationBar, @@ -16,6 +18,7 @@ import { } from './NavigationBar'; import Selector, { ISelectorItem } from './cell/Selector'; import SettingsHeader, { HeaderTitle } from './SettingsHeader'; +import Switch from './Switch'; const MIN_WIREGUARD_MTU_VALUE = 1280; const MAX_WIREGUARD_MTU_VALUE = 1420; @@ -47,13 +50,24 @@ export const StyledInputFrame = styled(Cell.InputFrame)({ interface IProps { wireguard: { port?: number; ipVersion?: IpVersion }; wireguardMtu?: number; + wireguardMultihop: boolean; setWireguardMtu: (value: number | undefined) => void; - setWireguardRelayPortAndIpVersion: (port?: number, ipVersion?: IpVersion) => void; + setWireguardMultihop: (value: boolean) => void; + setWireguardPort: (port?: number) => void; + setWireguardIpVersion: (ipVersion?: IpVersion) => void; onViewWireguardKeys: () => void; onClose: () => void; } -export default class WireguardSettings extends React.Component<IProps> { +interface IState { + showMultihopConfirmationDialog: boolean; +} + +export default class WireguardSettings extends React.Component<IProps, IState> { + public state = { showMultihopConfirmationDialog: false }; + + private multihopRef = React.createRef<Switch>(); + private wireguardPortItems: Array<ISelectorItem<OptionalPort>>; private wireguardIpVersionItems: Array<ISelectorItem<OptionalIpVersion>>; @@ -87,149 +101,178 @@ export default class WireguardSettings extends React.Component<IProps> { public render() { return ( - <Layout> - <SettingsContainer> - <NavigationContainer> - <NavigationBar> - <NavigationItems> - <BackBarItem action={this.props.onClose}> - { - // TRANSLATORS: Back button in navigation bar - messages.pgettext('navigation-bar', 'Advanced') - } - </BackBarItem> - <TitleBarItem> - { - // TRANSLATORS: Title label in navigation bar - messages.pgettext('wireguard-settings-nav', 'WireGuard settings') - } - </TitleBarItem> - </NavigationItems> - </NavigationBar> + <ModalContainer> + <Layout> + <SettingsContainer> + <NavigationContainer> + <NavigationBar> + <NavigationItems> + <BackBarItem action={this.props.onClose}> + { + // TRANSLATORS: Back button in navigation bar + messages.pgettext('navigation-bar', 'Advanced') + } + </BackBarItem> + <TitleBarItem> + { + // TRANSLATORS: Title label in navigation bar + messages.pgettext('wireguard-settings-nav', 'WireGuard settings') + } + </TitleBarItem> + </NavigationItems> + </NavigationBar> - <StyledNavigationScrollbars> - <SettingsHeader> - <HeaderTitle> - {messages.pgettext('wireguard-settings-view', 'WireGuard settings')} - </HeaderTitle> - </SettingsHeader> + <StyledNavigationScrollbars> + <SettingsHeader> + <HeaderTitle> + {messages.pgettext('wireguard-settings-view', 'WireGuard settings')} + </HeaderTitle> + </SettingsHeader> - <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.onSelectWireguardPort} - /> - </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> - - <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.onSelectWireguardIpVersion} - /> - </StyledSelectorContainer> - <Cell.Footer> - <AriaDescription> - <Cell.FooterText> - { - // TRANSLATORS: The hint displayed below the WireGuard IP version selector. - messages.pgettext( - 'wireguard-settings-view', - 'This allows access to WireGuard for devices that only support IPv6.', - ) - } - </Cell.FooterText> - </AriaDescription> - </Cell.Footer> - </AriaInputGroup> - - <Cell.CellButtonGroup> - <Cell.CellButton onClick={this.props.onViewWireguardKeys}> - <Cell.Label> - {messages.pgettext('wireguard-settings-view', 'WireGuard key')} - </Cell.Label> - <Cell.Icon height={12} width={7} source="icon-chevron" /> - </Cell.CellButton> - </Cell.CellButtonGroup> + <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> - <AriaInputGroup> - <Cell.Container> - <AriaLabel> - <Cell.InputLabel> - {messages.pgettext('wireguard-settings-view', 'MTU')} - </Cell.InputLabel> - </AriaLabel> - <StyledInputFrame> + <AriaInputGroup> + <Cell.Container> + <AriaLabel> + <Cell.InputLabel> + { + // TRANSLATORS: The label next to the multihop settings toggle. + messages.pgettext('advanced-settings-view', 'Enable multihop') + } + </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} + <Cell.Switch + ref={this.multihopRef} + isOn={this.props.wireguardMultihop} + onChange={this.setWireguardMultihop} /> </AriaInput> - </StyledInputFrame> - </Cell.Container> - <Cell.Footer> - <AriaDescription> - <Cell.FooterText> - {sprintf( - // TRANSLATORS: The hint displayed below the WireGuard MTU input field. - // TRANSLATORS: Available placeholders: - // 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 MTU value. Valid range: %(min)d - %(max)d.', - ), + </Cell.Container> + <Cell.Footer> + <AriaDescription> + <Cell.FooterText> { - min: MIN_WIREGUARD_MTU_VALUE, - max: MAX_WIREGUARD_MTU_VALUE, - }, - )} - </Cell.FooterText> - </AriaDescription> - </Cell.Footer> - </AriaInputGroup> - </StyledNavigationScrollbars> - </NavigationContainer> - </SettingsContainer> - </Layout> - ); - } + // TRANSLATORS: Description for multihop settings toggle. + messages.pgettext( + 'advanced-settings-view', + 'Increases anonymity by routing your traffic into one WireGuard server and out another, making it harder to trace.', + ) + } + </Cell.FooterText> + </AriaDescription> + </Cell.Footer> + </AriaInputGroup> - private onSelectWireguardPort = (port?: number) => { - this.props.setWireguardRelayPortAndIpVersion(port, this.props.wireguard.ipVersion); - }; + <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> + { + // TRANSLATORS: The hint displayed below the WireGuard IP version selector. + messages.pgettext( + 'wireguard-settings-view', + 'This allows access to WireGuard for devices that only support IPv6.', + ) + } + </Cell.FooterText> + </AriaDescription> + </Cell.Footer> + </AriaInputGroup> - private onSelectWireguardIpVersion = (ipVersion?: IpVersion) => { - this.props.setWireguardRelayPortAndIpVersion(this.props.wireguard.port, ipVersion); - }; + <Cell.CellButtonGroup> + <Cell.CellButton onClick={this.props.onViewWireguardKeys}> + <Cell.Label> + {messages.pgettext('wireguard-settings-view', 'WireGuard key')} + </Cell.Label> + <Cell.Icon height={12} width={7} source="icon-chevron" /> + </Cell.CellButton> + </Cell.CellButtonGroup> + + <AriaInputGroup> + <Cell.Container> + <AriaLabel> + <Cell.InputLabel> + {messages.pgettext('wireguard-settings-view', 'MTU')} + </Cell.InputLabel> + </AriaLabel> + <StyledInputFrame> + <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> + </StyledInputFrame> + </Cell.Container> + <Cell.Footer> + <AriaDescription> + <Cell.FooterText> + {sprintf( + // TRANSLATORS: The hint displayed below the WireGuard MTU input field. + // TRANSLATORS: Available placeholders: + // 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 MTU value. Valid range: %(min)d - %(max)d.', + ), + { + min: MIN_WIREGUARD_MTU_VALUE, + max: MAX_WIREGUARD_MTU_VALUE, + }, + )} + </Cell.FooterText> + </AriaDescription> + </Cell.Footer> + </AriaInputGroup> + </StyledNavigationScrollbars> + </NavigationContainer> + </SettingsContainer> + </Layout> + + {this.state.showMultihopConfirmationDialog && this.renderMultihopConfirmation()} + </ModalContainer> + ); + } private static removeNonNumericCharacters(value: string) { return value.replace(/[^0-9]/g, ''); @@ -249,4 +292,42 @@ export default class WireguardSettings extends React.Component<IProps> { (parsedMtu >= MIN_WIREGUARD_MTU_VALUE && parsedMtu <= MAX_WIREGUARD_MTU_VALUE) ); } + + private renderMultihopConfirmation = () => { + return ( + <ModalAlert + 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}> + {messages.gettext('Enable anyway')} + </AppButton.RedButton>, + <AppButton.BlueButton key="back" onClick={this.hideWireguardMultihopConfirmationDialog}> + {messages.gettext('Back')} + </AppButton.BlueButton>, + ]} + close={this.hideWireguardMultihopConfirmationDialog}></ModalAlert> + ); + }; + + private setWireguardMultihop = (newValue: boolean) => { + if (newValue) { + this.setState({ showMultihopConfirmationDialog: true }); + } else { + this.props.setWireguardMultihop(false); + } + }; + + private hideWireguardMultihopConfirmationDialog = () => { + this.setState({ showMultihopConfirmationDialog: false }); + this.multihopRef.current?.setOn(this.props.wireguardMultihop); + }; + + private confirmWireguardMultihop = () => { + this.setState({ showMultihopConfirmationDialog: false }); + this.props.setWireguardMultihop(true); + }; } |
