diff options
| -rw-r--r-- | CHANGELOG.md | 1 | ||||
| -rw-r--r-- | gui/locales/messages.pot | 28 | ||||
| -rw-r--r-- | gui/src/main/daemon-rpc.ts | 21 | ||||
| -rw-r--r-- | gui/src/main/index.ts | 10 | ||||
| -rw-r--r-- | gui/src/renderer/components/AdvancedSettings.tsx | 81 | ||||
| -rw-r--r-- | gui/src/renderer/components/Preferences.tsx | 90 | ||||
| -rw-r--r-- | gui/src/renderer/components/PreferencesStyles.tsx | 6 | ||||
| -rw-r--r-- | gui/src/renderer/containers/PreferencesPage.tsx | 5 | ||||
| -rw-r--r-- | gui/src/renderer/markdown-formatter.tsx | 19 | ||||
| -rw-r--r-- | gui/src/renderer/redux/settings/reducers.ts | 16 | ||||
| -rw-r--r-- | gui/src/shared/daemon-rpc-types.ts | 10 |
11 files changed, 252 insertions, 35 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 8140dcd860..5bdcbdffe4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,7 @@ Line wrap the file at 100 chars. Th - Make app native on Apple Silicon. - Add DNS options for ad and tracker blocking to the CLI. - Support WireGuard multihop using an entry endpoint constraint. +- Add DNS options for ad and tracker blocking to the desktop app. ### Changed - Upgrade OpenVPN from 2.5.0 to 2.5.1. diff --git a/gui/locales/messages.pot b/gui/locales/messages.pot index 4d8fa54510..ab24bf500c 100644 --- a/gui/locales/messages.pot +++ b/gui/locales/messages.pot @@ -714,6 +714,34 @@ msgid "Beta program" msgstr "" msgctxt "preferences-view" +msgid "Block ads" +msgstr "" + +msgctxt "preferences-view" +msgid "Block trackers" +msgstr "" + +#. This is displayed when either or both of the block ads/trackers settings are +#. turned on which makes the custom DNS setting disabled. The text enclosed in "**" +#. will appear bold. +#. Available placeholders: +#. %(blockAdsFeatureName)s - The name displayed next to the "Block ads" toggle. +#. %(blockTrackersFeatureName)s - The name displayed next to the "Block trackers" toggle. +#. %(preferencesPageName)s - The page title showed on top in the preferences page. +msgctxt "preferences-view" +msgid "Disable **%(blockAdsFeatureName)s** and **%(blockTrackersFeatureName)s** (under %(preferencesPageName)s) to activate this setting." +msgstr "" + +#. This is displayed when the custom DNS setting is turned on which makes the block +#. ads/trackers settings disabled. The text enclosed in "**" will appear bold. +#. Advanced settings refer to the name of the page with the title "Advanced". +#. Available placeholders: +#. %(customDnsFeatureName)s - The name displayed next to the custom DNS toggle. +msgctxt "preferences-view" +msgid "Disable **%(customDnsFeatureName)s** (under Advanced settings) to activate these settings." +msgstr "" + +msgctxt "preferences-view" msgid "Enable or disable system notifications. The critical notifications will always be displayed." msgstr "" diff --git a/gui/src/main/daemon-rpc.ts b/gui/src/main/daemon-rpc.ts index 5e28be9380..04332a4447 100644 --- a/gui/src/main/daemon-rpc.ts +++ b/gui/src/main/daemon-rpc.ts @@ -449,15 +449,15 @@ export class DaemonRpc { const dnsOptions = new grpcTypes.DnsOptions(); const defaultOptions = new grpcTypes.DefaultDnsOptions(); - defaultOptions.setBlockAds(false); - defaultOptions.setBlockTrackers(false); + defaultOptions.setBlockAds(dns.defaultOptions.blockAds); + defaultOptions.setBlockTrackers(dns.defaultOptions.blockTrackers); dnsOptions.setDefaultOptions(defaultOptions); const customOptions = new grpcTypes.CustomDnsOptions(); - customOptions.setAddressesList(dns.addresses); + customOptions.setAddressesList(dns.customOptions.addresses); dnsOptions.setCustomOptions(customOptions); - if (dns.custom) { + if (dns.state === 'custom') { dnsOptions.setState(grpcTypes.DnsOptions.DnsState.CUSTOM); } else { dnsOptions.setState(grpcTypes.DnsOptions.DnsState.DEFAULT); @@ -1042,8 +1042,17 @@ function convertFromTunnelOptions(tunnelOptions: grpcTypes.TunnelOptions.AsObjec enableIpv6: tunnelOptions.generic!.enableIpv6, }, dns: { - custom: tunnelOptions.dnsOptions!.state! === grpcTypes.DnsOptions.DnsState.CUSTOM, - addresses: tunnelOptions.dnsOptions?.customOptions?.addressesList ?? [], + state: + tunnelOptions.dnsOptions?.state === grpcTypes.DnsOptions.DnsState.CUSTOM + ? 'custom' + : 'default', + defaultOptions: { + blockAds: tunnelOptions.dnsOptions?.defaultOptions?.blockAds ?? false, + blockTrackers: tunnelOptions.dnsOptions?.defaultOptions?.blockTrackers ?? false, + }, + customOptions: { + addresses: tunnelOptions.dnsOptions?.customOptions?.addressesList ?? [], + }, }, }; } diff --git a/gui/src/main/index.ts b/gui/src/main/index.ts index 6b8f3969ad..f6c3d58f3f 100644 --- a/gui/src/main/index.ts +++ b/gui/src/main/index.ts @@ -152,8 +152,14 @@ class ApplicationMain { mtu: undefined, }, dns: { - custom: false, - addresses: [], + state: 'default', + defaultOptions: { + blockAds: false, + blockTrackers: false, + }, + customOptions: { + addresses: [], + }, }, }, }; diff --git a/gui/src/renderer/components/AdvancedSettings.tsx b/gui/src/renderer/components/AdvancedSettings.tsx index a81981263a..ef59798a46 100644 --- a/gui/src/renderer/components/AdvancedSettings.tsx +++ b/gui/src/renderer/components/AdvancedSettings.tsx @@ -42,6 +42,7 @@ import { import Selector, { ISelectorItem } from './cell/Selector'; import SettingsHeader, { HeaderTitle } from './SettingsHeader'; import Accordion from './Accordion'; +import { formatMarkdown } from '../markdown-formatter'; const MIN_MSSFIX_VALUE = 1000; const MAX_MSSFIX_VALUE = 1450; @@ -448,7 +449,7 @@ export default class AdvancedSettings extends React.Component<IProps, IState> { )} </StyledButtonCellGroup> - <StyledCustomDnsSwitchContainer> + <StyledCustomDnsSwitchContainer disabled={!this.customDnsAvailable()}> <AriaInputGroup> <AriaLabel> <Cell.InputLabel> @@ -458,13 +459,17 @@ export default class AdvancedSettings extends React.Component<IProps, IState> { <AriaInput> <Cell.Switch ref={this.customDnsSwitchRef} - isOn={this.props.dns.custom || this.state.showAddCustomDns} + isOn={this.props.dns.state === 'custom' || this.state.showAddCustomDns} onChange={this.setCustomDnsEnabled} /> </AriaInput> </AriaInputGroup> </StyledCustomDnsSwitchContainer> - <Accordion expanded={this.props.dns.custom || this.state.showAddCustomDns}> + <Accordion + expanded={ + this.customDnsAvailable() && + (this.props.dns.state === 'custom' || this.state.showAddCustomDns) + }> <CellList items={this.customDnsItems()} onRemove={this.removeDnsAddress} /> {this.state.showAddCustomDns && ( @@ -502,9 +507,13 @@ export default class AdvancedSettings extends React.Component<IProps, IState> { <StyledCustomDnsFotter> <Cell.FooterText> - {messages.pgettext( - 'advanced-settings-view', - 'Enable to add at least one DNS server.', + {this.customDnsAvailable() ? ( + messages.pgettext( + 'advanced-settings-view', + 'Enable to add at least one DNS server.', + ) + ) : ( + <CustomDnsDisabledMessage /> )} </Cell.FooterText> </StyledCustomDnsFotter> @@ -520,15 +529,22 @@ export default class AdvancedSettings extends React.Component<IProps, IState> { ); } + 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.addresses.length > 0) { + if (this.props.dns.customOptions.addresses.length > 0) { await this.props.setDnsOptions({ - custom: enabled, - addresses: this.props.dns.addresses, + ...this.props.dns, + state: enabled ? 'custom' : 'default', }); } - if (enabled && this.props.dns.addresses.length === 0) { + if (enabled && this.props.dns.customOptions.addresses.length === 0) { this.showAddCustomDnsRow(); } @@ -538,7 +554,7 @@ export default class AdvancedSettings extends React.Component<IProps, IState> { }; private customDnsItems(): ICellListItem<string>[] { - return this.props.dns.addresses.map((address) => ({ + return this.props.dns.customOptions.addresses.map((address) => ({ label: address, value: address, })); @@ -589,8 +605,12 @@ export default class AdvancedSettings extends React.Component<IProps, IState> { if (ipAddress.isLocal() || confirmed) { await this.props.setDnsOptions({ - custom: this.props.dns.custom || this.state.showAddCustomDns, - addresses: [...this.props.dns.addresses, address], + ...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 { @@ -602,11 +622,14 @@ export default class AdvancedSettings extends React.Component<IProps, IState> { }; private removeDnsAddress = (address: string) => { - const addresses = this.props.dns.addresses.filter((item) => item !== address); + const addresses = this.props.dns.customOptions.addresses.filter((item) => item !== address); consumePromise( this.props.setDnsOptions({ - custom: addresses.length > 0 && this.props.dns.custom, - addresses, + ...this.props.dns, + state: addresses.length > 0 && this.props.dns.state === 'custom' ? 'custom' : 'default', + customOptions: { + addresses, + }, }), ); }; @@ -758,3 +781,29 @@ export default class AdvancedSettings extends React.Component<IProps, IState> { ); } } + +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/Preferences.tsx b/gui/src/renderer/components/Preferences.tsx index d47a7315c6..c91330aee0 100644 --- a/gui/src/renderer/components/Preferences.tsx +++ b/gui/src/renderer/components/Preferences.tsx @@ -1,5 +1,8 @@ import * as React from 'react'; +import { sprintf } from 'sprintf-js'; +import { IDnsOptions } from '../../shared/daemon-rpc-types'; import { messages } from '../../shared/gettext'; +import { formatMarkdown } from '../markdown-formatter'; import { AriaDescription, AriaInput, AriaInputGroup, AriaLabel } from './AriaGroup'; import * as Cell from './cell'; import { Layout } from './Layout'; @@ -24,6 +27,7 @@ export interface IProps { monochromaticIcon: boolean; startMinimized: boolean; unpinnedWindow: boolean; + dns: IDnsOptions; setAutoStart: (autoStart: boolean) => void; setEnableSystemNotifications: (flag: boolean) => void; setAutoConnect: (autoConnect: boolean) => void; @@ -32,6 +36,7 @@ export interface IProps { setStartMinimized: (startMinimized: boolean) => void; setMonochromaticIcon: (monochromaticIcon: boolean) => void; setUnpinnedWindow: (unpinnedWindow: boolean) => void; + setDnsOptions: (dns: IDnsOptions) => Promise<void>; onClose: () => void; } @@ -105,6 +110,47 @@ export default class Preferences extends React.Component<IProps> { </AriaInputGroup> <AriaInputGroup> + <Cell.Container disabled={this.props.dns.state === 'custom'}> + <AriaLabel> + <Cell.InputLabel> + {messages.pgettext('preferences-view', 'Block ads')} + </Cell.InputLabel> + </AriaLabel> + <AriaInput> + <Cell.Switch + isOn={ + this.props.dns.state === 'default' && + this.props.dns.defaultOptions.blockAds + } + onChange={this.setBlockAds} + /> + </AriaInput> + </Cell.Container> + </AriaInputGroup> + <StyledSeparator /> + <AriaInputGroup> + <Cell.Container disabled={this.props.dns.state === 'custom'}> + <AriaLabel> + <Cell.InputLabel> + {messages.pgettext('preferences-view', 'Block trackers')} + </Cell.InputLabel> + </AriaLabel> + <AriaInput> + <Cell.Switch + isOn={ + this.props.dns.state === 'default' && + this.props.dns.defaultOptions.blockTrackers + } + onChange={this.setBlockTrackers} + /> + </AriaInput> + </Cell.Container> + {this.props.dns.state === 'custom' && <CustomDnsEnabledFooter />} + </AriaInputGroup> + + {this.props.dns.state !== 'custom' && <StyledSeparator height={20} />} + + <AriaInputGroup> <Cell.Container> <AriaLabel> <Cell.InputLabel> @@ -275,4 +321,48 @@ export default class Preferences extends React.Component<IProps> { </Layout> ); } + + private setBlockAds = async (enabled: boolean) => { + await this.props.setDnsOptions({ + ...this.props.dns, + defaultOptions: { + ...this.props.dns.defaultOptions, + blockAds: enabled, + }, + }); + }; + + private setBlockTrackers = async (enabled: boolean) => { + await this.props.setDnsOptions({ + ...this.props.dns, + defaultOptions: { + ...this.props.dns.defaultOptions, + blockTrackers: enabled, + }, + }); + }; +} + +function CustomDnsEnabledFooter() { + const customDnsFeatureName = messages.pgettext('advanced-settings-view', 'Use custom DNS server'); + + // TRANSLATORS: This is displayed when the custom DNS setting is turned on which makes the block + // TRANSLATORS: ads/trackers settings disabled. The text enclosed in "**" will appear bold. + // TRANSLATORS: Advanced settings refer to the name of the page with the title "Advanced". + // TRANSLATORS: Available placeholders: + // TRANSLATORS: %(customDnsFeatureName)s - The name displayed next to the custom DNS toggle. + const blockingDisabledText = messages.pgettext( + 'preferences-view', + 'Disable **%(customDnsFeatureName)s** (under Advanced settings) to activate these settings.', + ); + + return ( + <Cell.Footer> + <AriaDescription> + <Cell.FooterText> + {formatMarkdown(sprintf(blockingDisabledText, { customDnsFeatureName }))} + </Cell.FooterText> + </AriaDescription> + </Cell.Footer> + ); } diff --git a/gui/src/renderer/components/PreferencesStyles.tsx b/gui/src/renderer/components/PreferencesStyles.tsx index b9986f2c38..f4fa114469 100644 --- a/gui/src/renderer/components/PreferencesStyles.tsx +++ b/gui/src/renderer/components/PreferencesStyles.tsx @@ -13,6 +13,6 @@ export const StyledContent = styled.div({ marginBottom: '2px', }); -export const StyledSeparator = styled.div({ - height: '1px', -}); +export const StyledSeparator = styled.div((props: { height?: number }) => ({ + height: `${props.height ?? 1}px`, +})); diff --git a/gui/src/renderer/containers/PreferencesPage.tsx b/gui/src/renderer/containers/PreferencesPage.tsx index 571219b528..3bd4a20e44 100644 --- a/gui/src/renderer/containers/PreferencesPage.tsx +++ b/gui/src/renderer/containers/PreferencesPage.tsx @@ -1,5 +1,6 @@ import { connect } from 'react-redux'; import { RouteComponentProps, withRouter } from 'react-router'; +import { IDnsOptions } from '../../shared/daemon-rpc-types'; import log from '../../shared/logging'; import consumePromise from '../../shared/promise'; import Preferences from '../components/Preferences'; @@ -16,6 +17,7 @@ const mapStateToProps = (state: IReduxState) => ({ monochromaticIcon: state.settings.guiSettings.monochromaticIcon, startMinimized: state.settings.guiSettings.startMinimized, unpinnedWindow: state.settings.guiSettings.unpinnedWindow, + dns: state.settings.dns, }); const mapDispatchToProps = (_dispatch: ReduxDispatch, props: RouteComponentProps & IAppContext) => { @@ -51,6 +53,9 @@ const mapDispatchToProps = (_dispatch: ReduxDispatch, props: RouteComponentProps setUnpinnedWindow: (unpinnedWindow: boolean) => { props.app.setUnpinnedWindow(unpinnedWindow); }, + setDnsOptions: (dns: IDnsOptions) => { + return props.app.setDnsOptions(dns); + }, }; }; diff --git a/gui/src/renderer/markdown-formatter.tsx b/gui/src/renderer/markdown-formatter.tsx new file mode 100644 index 0000000000..f455a882c4 --- /dev/null +++ b/gui/src/renderer/markdown-formatter.tsx @@ -0,0 +1,19 @@ +import React from 'react'; +import styled from 'styled-components'; + +const boldSyntax = '**'; +const Bold = styled.span({ fontWeight: 'bold' }); + +export function formatMarkdown(inputString: string): React.ReactElement { + const formattedString = inputString + .split(boldSyntax) + .map((value, index) => + index % 2 === 0 ? ( + <React.Fragment key={index}>{value}</React.Fragment> + ) : ( + <Bold key={index}>{value}</Bold> + ), + ); + + return <>{formattedString}</>; +} diff --git a/gui/src/renderer/redux/settings/reducers.ts b/gui/src/renderer/redux/settings/reducers.ts index 068add8e89..be4287c90f 100644 --- a/gui/src/renderer/redux/settings/reducers.ts +++ b/gui/src/renderer/redux/settings/reducers.ts @@ -6,6 +6,7 @@ import { RelayLocation, RelayProtocol, TunnelProtocol, + IDnsOptions, } from '../../../shared/daemon-rpc-types'; import { IGuiSettingsState } from '../../../shared/gui-settings-state'; import log from '../../../shared/logging'; @@ -133,10 +134,7 @@ export interface ISettingsReduxState { wireguard: { mtu?: number; }; - dns: { - custom: boolean; - addresses: string[]; - }; + dns: IDnsOptions; wireguardKeyState: WgKeyState; } @@ -180,8 +178,14 @@ const initialState: ISettingsReduxState = { type: 'key-not-set', }, dns: { - custom: false, - addresses: [], + state: 'default', + defaultOptions: { + blockAds: false, + blockTrackers: false, + }, + customOptions: { + addresses: [], + }, }, }; diff --git a/gui/src/shared/daemon-rpc-types.ts b/gui/src/shared/daemon-rpc-types.ts index 07a75c07ff..92d08e8d5c 100644 --- a/gui/src/shared/daemon-rpc-types.ts +++ b/gui/src/shared/daemon-rpc-types.ts @@ -261,8 +261,14 @@ export interface ITunnelOptions { } export interface IDnsOptions { - custom: boolean; - addresses: string[]; + state: 'custom' | 'default'; + customOptions: { + addresses: string[]; + }; + defaultOptions: { + blockAds: boolean; + blockTrackers: boolean; + }; } export type ProxySettings = ILocalProxySettings | IRemoteProxySettings | IShadowsocksProxySettings; |
