diff options
| author | Oskar Nyberg <oskar@mullvad.net> | 2021-08-25 09:11:00 +0200 |
|---|---|---|
| committer | Oskar Nyberg <oskar@mullvad.net> | 2021-08-30 14:28:36 +0200 |
| commit | b720305a57eb1c6d4f7eb4d4ccd58dd2990bd2d6 (patch) | |
| tree | c9ab60f01d9c03813d6675cc621536cee9ed20e9 | |
| parent | 35ad04d1188ac2f618f252bd6edaeb523e0245bc (diff) | |
| download | mullvadvpn-b720305a57eb1c6d4f7eb4d4ccd58dd2990bd2d6.tar.xz mullvadvpn-b720305a57eb1c6d4f7eb4d4ccd58dd2990bd2d6.zip | |
Refactor custom DNS and make editable
| -rw-r--r-- | gui/src/renderer/components/AdvancedSettings.tsx | 243 | ||||
| -rw-r--r-- | gui/src/renderer/components/AdvancedSettingsStyles.tsx | 26 | ||||
| -rw-r--r-- | gui/src/renderer/components/CustomDnsSettings.tsx | 382 | ||||
| -rw-r--r-- | gui/src/renderer/components/CustomDnsSettingsStyles.tsx | 69 | ||||
| -rw-r--r-- | gui/src/renderer/components/cell/Input.tsx | 17 | ||||
| -rw-r--r-- | gui/src/renderer/components/cell/List.tsx | 123 | ||||
| -rw-r--r-- | gui/src/renderer/containers/AdvancedSettingsPage.tsx | 7 | ||||
| -rw-r--r-- | gui/src/renderer/lib/utilityHooks.ts | 12 |
8 files changed, 479 insertions, 400 deletions
diff --git a/gui/src/renderer/components/AdvancedSettings.tsx b/gui/src/renderer/components/AdvancedSettings.tsx index 142477d951..e3d9134979 100644 --- a/gui/src/renderer/components/AdvancedSettings.tsx +++ b/gui/src/renderer/components/AdvancedSettings.tsx @@ -1,9 +1,7 @@ import * as React from 'react'; import { sprintf } from 'sprintf-js'; -import { colors } from '../../config.json'; -import { IDnsOptions, TunnelProtocol } from '../../shared/daemon-rpc-types'; +import { TunnelProtocol } from '../../shared/daemon-rpc-types'; import { messages } from '../../shared/gettext'; -import { IpAddress } from '../lib/ip'; import { WgKeyState } from '../redux/settings/reducers'; import { StyledNavigationScrollbars, @@ -11,15 +9,11 @@ import { StyledNoWireguardKeyErrorContainer, StyledSelectorForFooter, StyledTunnelProtocolContainer, - StyledCustomDnsSwitchContainer, - StyledCustomDnsFooter, - StyledAddCustomDnsLabel, - StyledAddCustomDnsButton, } from './AdvancedSettingsStyles'; import * as AppButton from './AppButton'; import { AriaDescription, AriaInput, AriaInputGroup, AriaLabel } from './AriaGroup'; import * as Cell from './cell'; -import CellList, { ICellListItem } from './cell/List'; +import CustomDnsSettings from './CustomDnsSettings'; import { Layout, SettingsContainer } from './Layout'; import { ModalAlert, ModalAlertType, ModalContainer, ModalMessage } from './Modal'; import { @@ -31,8 +25,6 @@ import { } from './NavigationBar'; import { ISelectorItem } from './cell/Selector'; import SettingsHeader, { HeaderTitle } from './SettingsHeader'; -import Accordion from './Accordion'; -import { formatMarkdown } from '../markdown-formatter'; type OptionalTunnelProtocol = TunnelProtocol | undefined; @@ -41,11 +33,9 @@ interface IProps { blockWhenDisconnected: boolean; tunnelProtocol?: TunnelProtocol; wireguardKeyState: WgKeyState; - dns: IDnsOptions; setEnableIpv6: (value: boolean) => void; setBlockWhenDisconnected: (value: boolean) => void; setTunnelProtocol: (value: OptionalTunnelProtocol) => void; - setDnsOptions: (dns: IDnsOptions) => Promise<void>; onViewWireguardSettings: () => void; onViewOpenVpnSettings: () => void; onViewSplitTunneling: () => void; @@ -54,23 +44,13 @@ interface IProps { interface IState { showConfirmBlockWhenDisconnectedAlert: boolean; - showAddCustomDns: boolean; - invalidDnsIp: boolean; - publicDnsIpToConfirm?: string; } export default class AdvancedSettings extends React.Component<IProps, IState> { public state = { showConfirmBlockWhenDisconnectedAlert: false, - showAddCustomDns: false, - invalidDnsIp: false, - publicDnsIpToConfirm: undefined, }; - private customDnsSwitchRef = React.createRef<HTMLDivElement>(); - private customDnsAddButtonRef = React.createRef<HTMLButtonElement>(); - private customDnsInputContainerRef = React.createRef<HTMLDivElement>(); - public render() { const hasWireguardKey = this.props.wireguardKeyState.type === 'key-set'; @@ -209,74 +189,7 @@ export default class AdvancedSettings extends React.Component<IProps, IState> { </Cell.CellButton> </Cell.CellButtonGroup> - <StyledCustomDnsSwitchContainer disabled={!this.customDnsAvailable()}> - <AriaInputGroup> - <AriaLabel> - <Cell.InputLabel> - {messages.pgettext('advanced-settings-view', 'Use custom DNS server')} - </Cell.InputLabel> - </AriaLabel> - <AriaInput> - <Cell.Switch - ref={this.customDnsSwitchRef} - isOn={this.props.dns.state === 'custom' || this.state.showAddCustomDns} - onChange={this.setCustomDnsEnabled} - /> - </AriaInput> - </AriaInputGroup> - </StyledCustomDnsSwitchContainer> - <Accordion - expanded={ - this.customDnsAvailable() && - (this.props.dns.state === 'custom' || this.state.showAddCustomDns) - }> - <CellList items={this.customDnsItems()} onRemove={this.removeDnsAddress} /> - - {this.state.showAddCustomDns && ( - <div ref={this.customDnsInputContainerRef}> - <Cell.RowInput - placeholder={messages.pgettext('advanced-settings-view', 'Enter IP')} - onSubmit={this.addDnsAddress} - onChange={this.addDnsInputChange} - invalid={this.state.invalidDnsIp} - paddingLeft={32} - onBlur={this.customDnsInputBlur} - autofocus - /> - </div> - )} - - <StyledAddCustomDnsButton - ref={this.customDnsAddButtonRef} - onClick={this.showAddCustomDnsRow} - disabled={this.state.showAddCustomDns} - tabIndex={-1}> - <StyledAddCustomDnsLabel tabIndex={-1}> - {messages.pgettext('advanced-settings-view', 'Add a server')} - </StyledAddCustomDnsLabel> - <Cell.Icon - source="icon-add" - width={22} - height={22} - tintColor={colors.white40} - tintHoverColor={colors.white60} - tabIndex={-1} - /> - </StyledAddCustomDnsButton> - </Accordion> - - <StyledCustomDnsFooter> - <Cell.FooterText> - {this.customDnsAvailable() ? ( - messages.pgettext( - 'advanced-settings-view', - 'Enable to add at least one DNS server.', - ) - ) : ( - <CustomDnsDisabledMessage /> - )} - </Cell.FooterText> - </StyledCustomDnsFooter> + <CustomDnsSettings /> </StyledNavigationScrollbars> </NavigationContainer> </SettingsContainer> @@ -284,114 +197,10 @@ export default class AdvancedSettings extends React.Component<IProps, IState> { {this.state.showConfirmBlockWhenDisconnectedAlert && this.renderConfirmBlockWhenDisconnectedAlert()} - {this.state.publicDnsIpToConfirm && this.renderCustomDnsConfirmationDialog()} </ModalContainer> ); } - private customDnsAvailable(): boolean { - return ( - this.props.dns.state === 'custom' || - (!this.props.dns.defaultOptions.blockAds && !this.props.dns.defaultOptions.blockTrackers) - ); - } - - private setCustomDnsEnabled = async (enabled: boolean) => { - if (this.props.dns.customOptions.addresses.length > 0) { - await this.props.setDnsOptions({ - ...this.props.dns, - state: enabled ? 'custom' : 'default', - }); - } - - if (enabled && this.props.dns.customOptions.addresses.length === 0) { - this.showAddCustomDnsRow(); - } - - if (!enabled) { - this.setState({ showAddCustomDns: false }); - } - }; - - private customDnsItems(): ICellListItem<string>[] { - return this.props.dns.customOptions.addresses.map((address) => ({ - label: address, - value: address, - })); - } - - private showAddCustomDnsRow = () => { - this.setState({ showAddCustomDns: true }); - }; - - // The input field should be hidden when it loses focus unless something on the same row or the - // add-button is the new focused element. - private customDnsInputBlur = (event?: React.FocusEvent<HTMLTextAreaElement>) => { - const relatedTarget = event?.relatedTarget as Node | undefined; - if ( - relatedTarget && - (this.customDnsSwitchRef.current?.contains(relatedTarget) || - this.customDnsAddButtonRef.current?.contains(relatedTarget) || - this.customDnsInputContainerRef.current?.contains(relatedTarget)) - ) { - event?.target.focus(); - } else { - this.hideAddCustomDnsRow(); - } - }; - - private hideAddCustomDnsRow() { - if (!this.state.publicDnsIpToConfirm) { - this.setState({ showAddCustomDns: false }); - } - } - - private addDnsInputChange = (_value: string) => { - this.setState({ invalidDnsIp: false }); - }; - - private hideCustomDnsConfirmationDialog = () => { - this.setState({ publicDnsIpToConfirm: undefined }); - }; - - private confirmPublicDnsAddress = () => { - void this.addDnsAddress(this.state.publicDnsIpToConfirm!, true); - this.hideCustomDnsConfirmationDialog(); - }; - - private addDnsAddress = async (address: string, confirmed?: boolean) => { - try { - const ipAddress = IpAddress.fromString(address); - - if (ipAddress.isLocal() || confirmed) { - await this.props.setDnsOptions({ - ...this.props.dns, - state: - this.props.dns.state === 'custom' || this.state.showAddCustomDns ? 'custom' : 'default', - customOptions: { - addresses: [...this.props.dns.customOptions.addresses, address], - }, - }); - this.hideAddCustomDnsRow(); - } else { - this.setState({ publicDnsIpToConfirm: address }); - } - } catch (e) { - this.setState({ invalidDnsIp: true }); - } - }; - - private removeDnsAddress = (address: string) => { - const addresses = this.props.dns.customOptions.addresses.filter((item) => item !== address); - void this.props.setDnsOptions({ - ...this.props.dns, - state: addresses.length > 0 && this.props.dns.state === 'custom' ? 'custom' : 'default', - customOptions: { - addresses, - }, - }); - }; - private tunnelProtocolItems = ( hasWireguardKey: boolean, ): Array<ISelectorItem<OptionalTunnelProtocol>> => { @@ -417,26 +226,6 @@ export default class AdvancedSettings extends React.Component<IProps, IState> { ]; }; - private renderCustomDnsConfirmationDialog = () => { - return ( - <ModalAlert - type={ModalAlertType.info} - buttons={[ - <AppButton.RedButton key="confirm" onClick={this.confirmPublicDnsAddress}> - {messages.pgettext('advanced-settings-view', 'Add anyway')} - </AppButton.RedButton>, - <AppButton.BlueButton key="back" onClick={this.hideCustomDnsConfirmationDialog}> - {messages.gettext('Back')} - </AppButton.BlueButton>, - ]} - close={this.hideCustomDnsConfirmationDialog} - message={messages.pgettext( - 'advanced-settings-view', - 'The DNS server you want to add is public and will only work with WireGuard. To ensure that it always works, set the "Tunnel protocol" (in Advanced settings) to WireGuard.', - )}></ModalAlert> - ); - }; - private renderConfirmBlockWhenDisconnectedAlert = () => { return ( <ModalAlert @@ -489,29 +278,3 @@ export default class AdvancedSettings extends React.Component<IProps, IState> { this.props.setTunnelProtocol(protocol); }; } - -function CustomDnsDisabledMessage() { - const blockAdsFeatureName = messages.pgettext('preferences-view', 'Block ads'); - const blockTrackersFeatureName = messages.pgettext('preferences-view', 'Block trackers'); - const preferencesPageName = messages.pgettext('preferences-nav', 'Preferences'); - - // TRANSLATORS: This is displayed when either or both of the block ads/trackers settings are - // TRANSLATORS: turned on which makes the custom DNS setting disabled. The text enclosed in "**" - // TRANSLATORS: will appear bold. - // TRANSLATORS: Available placeholders: - // TRANSLATORS: %(blockAdsFeatureName)s - The name displayed next to the "Block ads" toggle. - // TRANSLATORS: %(blockTrackersFeatureName)s - The name displayed next to the "Block trackers" toggle. - // TRANSLATORS: %(preferencesPageName)s - The page title showed on top in the preferences page. - const customDnsDisabledMessage = messages.pgettext( - 'preferences-view', - 'Disable **%(blockAdsFeatureName)s** and **%(blockTrackersFeatureName)s** (under %(preferencesPageName)s) to activate this setting.', - ); - - return formatMarkdown( - sprintf(customDnsDisabledMessage, { - blockAdsFeatureName, - blockTrackersFeatureName, - preferencesPageName, - }), - ); -} diff --git a/gui/src/renderer/components/AdvancedSettingsStyles.tsx b/gui/src/renderer/components/AdvancedSettingsStyles.tsx index cb4ffe3425..c3b9a140d4 100644 --- a/gui/src/renderer/components/AdvancedSettingsStyles.tsx +++ b/gui/src/renderer/components/AdvancedSettingsStyles.tsx @@ -32,29 +32,3 @@ export const StyledNoWireguardKeyError = styled(Cell.FooterText)({ fontWeight: 800, color: colors.red, }); - -export const StyledCustomDnsSwitchContainer = styled(Cell.Container)({ - marginBottom: '1px', -}); - -export const StyledCustomDnsFooter = styled(Cell.Footer)({ - marginBottom: '2px', -}); - -export const StyledAddCustomDnsButton = styled(Cell.CellButton)({ - backgroundColor: colors.blue40, -}); - -export const StyledAddCustomDnsLabel = styled(Cell.Label)( - {}, - (props: { paddingLeft?: number }) => ({ - fontFamily: 'Open Sans', - fontWeight: 'normal', - fontSize: '16px', - paddingLeft: (props.paddingLeft ?? 32) + 'px', - whiteSpace: 'pre-wrap', - overflowWrap: 'break-word', - width: '171px', - marginRight: '25px', - }), -); diff --git a/gui/src/renderer/components/CustomDnsSettings.tsx b/gui/src/renderer/components/CustomDnsSettings.tsx new file mode 100644 index 0000000000..9c8b952ec8 --- /dev/null +++ b/gui/src/renderer/components/CustomDnsSettings.tsx @@ -0,0 +1,382 @@ +import React, { useCallback, useMemo, useRef, useState } from 'react'; +import { sprintf } from 'sprintf-js'; +import { colors } from '../../config.json'; +import { messages } from '../../shared/gettext'; +import { useAppContext } from '../context'; +import { IpAddress } from '../lib/ip'; +import { useBoolean } from '../lib/utilityHooks'; +import { formatMarkdown } from '../markdown-formatter'; +import { useSelector } from '../redux/store'; +import Accordion from './Accordion'; +import * as AppButton from './AppButton'; +import { + AriaDescribed, + AriaDescription, + AriaDescriptionGroup, + AriaInput, + AriaInputGroup, + AriaLabel, +} from './AriaGroup'; +import * as Cell from './cell'; +import { + StyledAddCustomDnsButton, + StyledAddCustomDnsLabel, + StyledButton, + StyledContainer, + StyledCustomDnsFooter, + StyledCustomDnsSwitchContainer, + StyledLabel, + StyledRemoveButton, + StyledRemoveIcon, +} from './CustomDnsSettingsStyles'; +import { ModalAlert, ModalAlertType } from './Modal'; + +export default function CustomDnsSettings() { + const { setDnsOptions } = useAppContext(); + const dns = useSelector((state) => state.settings.dns); + + const [inputVisible, showInput, hideInput] = useBoolean(false); + const [invalid, setInvalid, setValid] = useBoolean(false); + const [confirmAction, setConfirmAction] = useState<() => Promise<void>>(); + const willShowConfirmationDialog = useRef(false); + + const featureAvailable = useMemo( + () => + dns.state === 'custom' || (!dns.defaultOptions.blockAds && !dns.defaultOptions.blockTrackers), + [dns], + ); + + const switchRef = useRef() as React.RefObject<HTMLDivElement>; + const addButtonRef = useRef() as React.RefObject<HTMLButtonElement>; + const inputContainerRef = useRef() as React.RefObject<HTMLDivElement>; + + const confirm = useCallback(() => { + confirmAction?.(); + setConfirmAction(undefined); + }, [confirmAction]); + const abortConfirmation = useCallback(() => { + setConfirmAction(undefined); + }, [confirmAction]); + + const setCustomDnsEnabled = useCallback(async (enabled: boolean) => { + if (dns.customOptions.addresses.length > 0) { + await setDnsOptions({ ...dns, state: enabled ? 'custom' : 'default' }); + } + if (enabled && dns.customOptions.addresses.length === 0) { + showInput(); + } + if (!enabled) { + hideInput(); + } + }, []); + + // The input field should be hidden when it loses focus unless something on the same row or the + // add-button is the new focused element. + const onInputBlur = useCallback( + (event?: React.FocusEvent<HTMLTextAreaElement>) => { + const relatedTarget = event?.relatedTarget as Node | undefined; + if ( + relatedTarget && + (switchRef.current?.contains(relatedTarget) || + addButtonRef.current?.contains(relatedTarget) || + inputContainerRef.current?.contains(relatedTarget)) + ) { + event?.target.focus(); + } else if (!willShowConfirmationDialog.current) { + hideInput(); + } + }, + [confirmAction, willShowConfirmationDialog], + ); + + const onAdd = useCallback( + async (address: string) => { + const add = async () => { + await setDnsOptions({ + ...dns, + state: dns.state === 'custom' || inputVisible ? 'custom' : 'default', + customOptions: { + addresses: [...dns.customOptions.addresses, address], + }, + }); + + hideInput(); + }; + + try { + const ipAddress = IpAddress.fromString(address); + if (ipAddress.isLocal()) { + await add(); + } else { + willShowConfirmationDialog.current = true; + setConfirmAction(() => async () => { + willShowConfirmationDialog.current = false; + await add(); + }); + } + } catch { + setInvalid(); + } + }, + [inputVisible, dns, setDnsOptions], + ); + + const onEdit = useCallback( + (oldAddress: string, newAddress: string) => { + const edit = async () => { + const addresses = dns.customOptions.addresses.map((address) => + oldAddress === address ? newAddress : address, + ); + await setDnsOptions({ + ...dns, + customOptions: { + addresses, + }, + }); + }; + + const ipAddress = IpAddress.fromString(newAddress); + return new Promise<void>((resolve) => { + if (ipAddress.isLocal()) { + void edit().then(resolve); + } else { + willShowConfirmationDialog.current = true; + setConfirmAction(() => async () => { + willShowConfirmationDialog.current = false; + await edit(); + resolve(); + }); + } + }); + }, + [dns, setDnsOptions], + ); + + const onRemove = useCallback( + (address: string) => { + const addresses = dns.customOptions.addresses.filter((item) => item !== address); + void setDnsOptions({ + ...dns, + state: addresses.length > 0 && dns.state === 'custom' ? 'custom' : 'default', + customOptions: { + addresses, + }, + }); + }, + [dns, setDnsOptions], + ); + + return ( + <> + <StyledCustomDnsSwitchContainer disabled={!featureAvailable}> + <AriaInputGroup> + <AriaLabel> + <Cell.InputLabel> + {messages.pgettext('advanced-settings-view', 'Use custom DNS server')} + </Cell.InputLabel> + </AriaLabel> + <AriaInput> + <Cell.Switch + ref={switchRef} + isOn={dns.state === 'custom' || inputVisible} + onChange={setCustomDnsEnabled} + /> + </AriaInput> + </AriaInputGroup> + </StyledCustomDnsSwitchContainer> + <Accordion expanded={featureAvailable && (dns.state === 'custom' || inputVisible)}> + <Cell.Section role="listbox"> + {dns.customOptions.addresses.map((item, i) => { + return ( + <CellListItem + key={i} + onRemove={onRemove} + onChange={onEdit} + willShowConfirmationDialog={willShowConfirmationDialog}> + {item} + </CellListItem> + ); + })} + </Cell.Section> + + {inputVisible && ( + <div ref={inputContainerRef}> + <Cell.RowInput + placeholder={messages.pgettext('advanced-settings-view', 'Enter IP')} + onSubmit={onAdd} + onChange={setValid} + invalid={invalid} + paddingLeft={32} + onBlur={onInputBlur} + autofocus + /> + </div> + )} + + <StyledAddCustomDnsButton + ref={addButtonRef} + onClick={showInput} + disabled={inputVisible} + tabIndex={-1}> + <StyledAddCustomDnsLabel tabIndex={-1}> + {messages.pgettext('advanced-settings-view', 'Add a server')} + </StyledAddCustomDnsLabel> + <Cell.Icon + source="icon-add" + width={22} + height={22} + tintColor={colors.white40} + tintHoverColor={colors.white60} + tabIndex={-1} + /> + </StyledAddCustomDnsButton> + </Accordion> + + <StyledCustomDnsFooter> + <Cell.FooterText> + {featureAvailable ? ( + messages.pgettext('advanced-settings-view', 'Enable to add at least one DNS server.') + ) : ( + <DisabledMessage /> + )} + </Cell.FooterText> + </StyledCustomDnsFooter> + + {confirmAction && <ConfirmationDialog confirm={confirm} abort={abortConfirmation} />} + </> + ); +} + +function DisabledMessage() { + const blockAdsFeatureName = messages.pgettext('preferences-view', 'Block ads'); + const blockTrackersFeatureName = messages.pgettext('preferences-view', 'Block trackers'); + const preferencesPageName = messages.pgettext('preferences-nav', 'Preferences'); + + // TRANSLATORS: This is displayed when either or both of the block ads/trackers settings are + // TRANSLATORS: turned on which makes the custom DNS setting disabled. The text enclosed in "**" + // TRANSLATORS: will appear bold. + // TRANSLATORS: Available placeholders: + // TRANSLATORS: %(blockAdsFeatureName)s - The name displayed next to the "Block ads" toggle. + // TRANSLATORS: %(blockTrackersFeatureName)s - The name displayed next to the "Block trackers" toggle. + // TRANSLATORS: %(preferencesPageName)s - The page title showed on top in the preferences page. + const customDnsDisabledMessage = messages.pgettext( + 'preferences-view', + 'Disable **%(blockAdsFeatureName)s** and **%(blockTrackersFeatureName)s** (under %(preferencesPageName)s) to activate this setting.', + ); + + return formatMarkdown( + sprintf(customDnsDisabledMessage, { + blockAdsFeatureName, + blockTrackersFeatureName, + preferencesPageName, + }), + ); +} + +interface ICellListItemProps { + willShowConfirmationDialog: React.RefObject<boolean>; + onRemove: (application: string) => void; + onChange: (value: string, newValue: string) => Promise<void>; + children: string; +} + +function CellListItem(props: ICellListItemProps) { + const [editing, startEditing, stopEditing] = useBoolean(false); + const [invalid, setInvalid, setValid] = useBoolean(false); + + const inputContainerRef = useRef() as React.RefObject<HTMLDivElement>; + + const onRemove = useCallback(() => props.onRemove(props.children), [ + props.onRemove, + props.children, + ]); + + const onSubmit = useCallback( + async (value: string) => { + if (value === props.children) { + stopEditing(); + } else { + try { + await props.onChange(props.children, value); + stopEditing(); + } catch { + setInvalid(); + } + } + }, + [props.onChange, props.children, invalid], + ); + + const onBlur = useCallback((event?: React.FocusEvent<HTMLTextAreaElement>) => { + const relatedTarget = event?.relatedTarget as Node | undefined; + if (relatedTarget && inputContainerRef.current?.contains(relatedTarget)) { + event?.target.focus(); + } else if (!props.willShowConfirmationDialog.current) { + stopEditing(); + } + }, []); + + return ( + <AriaDescriptionGroup> + {editing ? ( + <div ref={inputContainerRef}> + <Cell.RowInput + initialValue={props.children} + placeholder={messages.pgettext('advanced-settings-view', 'Enter IP')} + onSubmit={onSubmit} + onChange={setValid} + invalid={invalid} + paddingLeft={32} + onBlur={onBlur} + autofocus + /> + </div> + ) : ( + <StyledContainer> + <StyledButton onClick={startEditing}> + <AriaDescription> + <StyledLabel>{props.children}</StyledLabel> + </AriaDescription> + </StyledButton> + <AriaDescribed> + <StyledRemoveButton + onClick={onRemove} + aria-label={messages.pgettext('accessibility', 'Remove item')}> + <StyledRemoveIcon + source="icon-close" + width={22} + height={22} + tintColor={editing ? colors.black : colors.white40} + /> + </StyledRemoveButton> + </AriaDescribed> + </StyledContainer> + )} + </AriaDescriptionGroup> + ); +} + +interface IConfirmationDialogProps { + confirm: () => void; + abort: () => void; +} + +function ConfirmationDialog(props: IConfirmationDialogProps) { + return ( + <ModalAlert + type={ModalAlertType.info} + buttons={[ + <AppButton.RedButton key="confirm" onClick={props.confirm}> + {messages.pgettext('advanced-settings-view', 'Add anyway')} + </AppButton.RedButton>, + <AppButton.BlueButton key="back" onClick={props.abort}> + {messages.gettext('Back')} + </AppButton.BlueButton>, + ]} + close={props.abort} + message={messages.pgettext( + 'advanced-settings-view', + 'The DNS server you want to add is public and will only work with WireGuard. To ensure that it always works, set the "Tunnel protocol" (in Advanced settings) to WireGuard.', + )}></ModalAlert> + ); +} diff --git a/gui/src/renderer/components/CustomDnsSettingsStyles.tsx b/gui/src/renderer/components/CustomDnsSettingsStyles.tsx new file mode 100644 index 0000000000..55526681a7 --- /dev/null +++ b/gui/src/renderer/components/CustomDnsSettingsStyles.tsx @@ -0,0 +1,69 @@ +import styled from 'styled-components'; +import { colors } from '../../config.json'; +import * as Cell from './cell'; +import ImageView from './ImageView'; + +export const StyledCustomDnsSwitchContainer = styled(Cell.Container)({ + marginBottom: '1px', +}); + +export const StyledCustomDnsFooter = styled(Cell.Footer)({ + marginBottom: '2px', +}); + +export const StyledAddCustomDnsButton = styled(Cell.CellButton)({ + backgroundColor: colors.blue40, +}); + +export const StyledAddCustomDnsLabel = styled(Cell.Label)( + {}, + (props: { paddingLeft?: number }) => ({ + fontFamily: 'Open Sans', + fontWeight: 'normal', + fontSize: '16px', + paddingLeft: (props.paddingLeft ?? 32) + 'px', + whiteSpace: 'pre-wrap', + overflowWrap: 'break-word', + width: '171px', + marginRight: '25px', + }), +); + +export const StyledContainer = styled(Cell.Container)({ + display: 'flex', + marginBottom: '1px', + backgroundColor: colors.blue40, +}); + +export const StyledButton = styled.button({ + display: 'flex', + alignItems: 'center', + flex: 1, + border: 'none', + background: 'transparent', + padding: 0, + margin: 0, +}); + +export const StyledLabel = styled(Cell.Label)({ + fontFamily: 'Open Sans', + fontWeight: 'normal', + fontSize: '16px', + paddingLeft: '32px', + whiteSpace: 'pre-wrap', + overflowWrap: 'break-word', + width: '171px', + marginRight: '25px', +}); + +export const StyledRemoveButton = styled.button({ + background: 'transparent', + border: 'none', + padding: 0, +}); + +export const StyledRemoveIcon = styled(ImageView)({ + [StyledRemoveButton + ':hover &']: { + backgroundColor: colors.white80, + }, +}); diff --git a/gui/src/renderer/components/cell/Input.tsx b/gui/src/renderer/components/cell/Input.tsx index 06553be624..1dd240b3ce 100644 --- a/gui/src/renderer/components/cell/Input.tsx +++ b/gui/src/renderer/components/cell/Input.tsx @@ -241,6 +241,7 @@ const StyledInputFiller = styled.div({ }); interface IRowInputProps { + initialValue?: string; onChange?: (value: string) => void; onSubmit: (value: string) => void; onFocus?: (event: React.FocusEvent<HTMLTextAreaElement>) => void; @@ -252,7 +253,7 @@ interface IRowInputProps { } export function RowInput(props: IRowInputProps) { - const [value, setValue] = useState(''); + const [value, setValue] = useState(props.initialValue ?? ''); const textAreaRef = useRef() as React.RefObject<HTMLTextAreaElement>; const submit = useCallback(() => props.onSubmit(value), [props.onSubmit, value]); @@ -284,17 +285,25 @@ export function RowInput(props: IRowInputProps) { [props.onBlur], ); + const focus = useCallback(() => { + const input = textAreaRef.current; + if (input) { + input.focus(); + input.selectionStart = input.selectionEnd = value.length; + } + }, [textAreaRef, value.length]); + useEffect(() => { if (props.autofocus) { - textAreaRef.current?.focus(); + focus(); } }, []); useEffect(() => { if (props.invalid) { - textAreaRef.current?.focus(); + focus(); } - }, [props.invalid]); + }, [props.invalid, focus]); useEffect(() => { document.addEventListener('keydown', globalKeyListener, true); diff --git a/gui/src/renderer/components/cell/List.tsx b/gui/src/renderer/components/cell/List.tsx deleted file mode 100644 index 69c9fff427..0000000000 --- a/gui/src/renderer/components/cell/List.tsx +++ /dev/null @@ -1,123 +0,0 @@ -import React, { useCallback } from 'react'; -import styled from 'styled-components'; -import { colors } from '../../../config.json'; -import { messages } from '../../../shared/gettext'; -import { AriaDescribed, AriaDescription, AriaDescriptionGroup } from '../AriaGroup'; -import ImageView from '../ImageView'; -import * as Cell from '.'; - -export interface ICellListItem<T> { - label: string; - value: T; -} - -interface ICellListProps<T> { - title?: string; - items: Array<ICellListItem<T>>; - onSelect?: (value: T) => void; - onRemove?: (value: T) => void; - className?: string; - paddingLeft?: number; -} - -export default function CellList<T>(props: ICellListProps<T>) { - const paddingLeft = props.paddingLeft ?? 32; - - return ( - <Cell.Section role="listbox" className={props.className}> - {props.title && <Cell.SectionTitle as="label">{props.title}</Cell.SectionTitle>} - {props.items.map((item, i) => { - return ( - <CellListItem - key={`${i}-${item.value}`} - value={item.value} - onSelect={props.onSelect} - onRemove={props.onRemove} - paddingLeft={paddingLeft}> - {item.label} - </CellListItem> - ); - })} - </Cell.Section> - ); -} - -const StyledContainer = styled(Cell.Container)({ - display: 'flex', - marginBottom: '1px', - backgroundColor: colors.blue40, -}); - -const StyledButton = styled.button({ - display: 'flex', - alignItems: 'center', - flex: 1, - border: 'none', - background: 'transparent', - padding: 0, - margin: 0, -}); - -const StyledLabel = styled(Cell.Label)({}, (props: { paddingLeft: number }) => ({ - fontFamily: 'Open Sans', - fontWeight: 'normal', - fontSize: '16px', - paddingLeft: props.paddingLeft + 'px', - whiteSpace: 'pre-wrap', - overflowWrap: 'break-word', - width: '171px', - marginRight: '25px', -})); - -const StyledRemoveButton = styled.button({ - background: 'transparent', - border: 'none', - padding: 0, -}); - -const StyledRemoveIcon = styled(ImageView)({ - [StyledRemoveButton + ':hover &']: { - backgroundColor: colors.white80, - }, -}); - -interface ICellListItemProps<T> { - value: T; - onSelect?: (application: T) => void; - onRemove?: (application: T) => void; - paddingLeft: number; - children: string; -} - -function CellListItem<T>(props: ICellListItemProps<T>) { - const onSelect = useCallback(() => props.onSelect?.(props.value), [props.onSelect, props.value]); - const onRemove = useCallback(() => props.onRemove?.(props.value), [props.onRemove, props.value]); - - return ( - <AriaDescriptionGroup> - <StyledContainer> - <StyledButton - onClick={props.onSelect ? onSelect : undefined} - as={props.onSelect ? 'button' : 'span'}> - <AriaDescription> - <StyledLabel paddingLeft={props.paddingLeft}>{props.children}</StyledLabel> - </AriaDescription> - </StyledButton> - {props.onRemove && ( - <AriaDescribed> - <StyledRemoveButton - onClick={onRemove} - aria-label={messages.pgettext('accessibility', 'Remove item')}> - <StyledRemoveIcon - source="icon-close" - width={22} - height={22} - tintColor={colors.white40} - /> - </StyledRemoveButton> - </AriaDescribed> - )} - </StyledContainer> - </AriaDescriptionGroup> - ); -} diff --git a/gui/src/renderer/containers/AdvancedSettingsPage.tsx b/gui/src/renderer/containers/AdvancedSettingsPage.tsx index afd6134042..8e8acf2ee9 100644 --- a/gui/src/renderer/containers/AdvancedSettingsPage.tsx +++ b/gui/src/renderer/containers/AdvancedSettingsPage.tsx @@ -1,5 +1,5 @@ import { connect } from 'react-redux'; -import { IDnsOptions, TunnelProtocol } from '../../shared/daemon-rpc-types'; +import { TunnelProtocol } from '../../shared/daemon-rpc-types'; import log from '../../shared/logging'; import RelaySettingsBuilder from '../../shared/relay-settings-builder'; import AdvancedSettings from '../components/AdvancedSettings'; @@ -17,7 +17,6 @@ const mapStateToProps = (state: IReduxState) => { enableIpv6: state.settings.enableIpv6, blockWhenDisconnected: state.settings.blockWhenDisconnected, wireguardKeyState: state.settings.wireguardKeyState, - dns: state.settings.dns, tunnelProtocol, }; }; @@ -74,10 +73,6 @@ const mapDispatchToProps = (_dispatch: ReduxDispatch, props: IHistoryProps & IAp } }, - setDnsOptions: (dns: IDnsOptions) => { - return props.app.setDnsOptions(dns); - }, - onViewWireguardSettings: () => props.history.push(RoutePath.wireguardSettings), onViewOpenVpnSettings: () => props.history.push(RoutePath.openVpnSettings), onViewSplitTunneling: () => props.history.push(RoutePath.splitTunneling), diff --git a/gui/src/renderer/lib/utilityHooks.ts b/gui/src/renderer/lib/utilityHooks.ts index d20cb80883..ca99a76f94 100644 --- a/gui/src/renderer/lib/utilityHooks.ts +++ b/gui/src/renderer/lib/utilityHooks.ts @@ -1,4 +1,4 @@ -import React, { useCallback, useEffect, useRef } from 'react'; +import React, { useCallback, useEffect, useRef, useState } from 'react'; export function useMounted() { const mountedRef = useRef(false); @@ -43,3 +43,13 @@ export function useAsyncEffect( }; }, dependencies); } + +export function useBoolean(initialValue: boolean) { + const [value, setValue] = useState(initialValue); + + const setTrue = useCallback(() => setValue(true), []); + const setFalse = useCallback(() => setValue(false), []); + const toggle = useCallback(() => setValue((value) => !value), []); + + return [value, setTrue, setFalse, toggle] as const; +} |
