summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorOskar Nyberg <oskar@mullvad.net>2022-11-07 11:06:02 +0100
committerOskar Nyberg <oskar@mullvad.net>2022-11-07 11:06:02 +0100
commit3b8e28cd91aaa59e236a4c4875d9cb43e823042f (patch)
treefd70a5d3702b45466b917446c037328e1e68f1c4
parent2968800c8da2877d7270bef512e543c4dee84223 (diff)
parentd20b3826f313c91da881c31c1fd3eb17a81ff044 (diff)
downloadmullvadvpn-3b8e28cd91aaa59e236a4c4875d9cb43e823042f.tar.xz
mullvadvpn-3b8e28cd91aaa59e236a4c4875d9cb43e823042f.zip
Merge branch 'make-dns-blockers-collapsible'
-rw-r--r--gui/locales/messages.pot71
-rw-r--r--gui/src/main/index.ts7
-rw-r--r--gui/src/renderer/app.tsx13
-rw-r--r--gui/src/renderer/components/ChevronButton.tsx6
-rw-r--r--gui/src/renderer/components/CustomDnsSettings.tsx18
-rw-r--r--gui/src/renderer/components/ExpiredAccountAddTime.tsx2
-rw-r--r--gui/src/renderer/components/NavigationBar.tsx27
-rw-r--r--gui/src/renderer/components/SplitTunnelingSettings.tsx20
-rw-r--r--gui/src/renderer/components/VpnSettings.tsx155
-rw-r--r--gui/src/renderer/components/WireguardSettings.tsx4
-rw-r--r--gui/src/renderer/components/cell/Section.tsx64
-rw-r--r--gui/src/renderer/components/cell/Selector.tsx56
-rw-r--r--gui/src/renderer/lib/history.tsx45
-rw-r--r--gui/src/renderer/redux/userinterface/actions.ts46
-rw-r--r--gui/src/renderer/redux/userinterface/reducers.ts17
-rw-r--r--gui/src/shared/ipc-schema.ts3
-rw-r--r--gui/src/shared/ipc-types.ts7
17 files changed, 268 insertions, 293 deletions
diff --git a/gui/locales/messages.pot b/gui/locales/messages.pot
index c5f1b073b4..377684c041 100644
--- a/gui/locales/messages.pot
+++ b/gui/locales/messages.pot
@@ -1294,40 +1294,30 @@ msgctxt "vpn-settings-view"
msgid "Add anyway"
msgstr ""
+#. Label for settings that enables ad blocking.
msgctxt "vpn-settings-view"
-msgid "Allows access to other devices on the same network for sharing, printing etc."
-msgstr ""
-
-msgctxt "vpn-settings-view"
-msgid "Attention: enabling this will always require a Mullvad VPN connection in order to reach the internet."
-msgstr ""
-
-msgctxt "vpn-settings-view"
-msgid "Auto-connect"
-msgstr ""
-
-msgctxt "vpn-settings-view"
-msgid "Automatically connect to a server when the app launches."
+msgid "Ads"
msgstr ""
+#. Label for settings that enables block of adult content.
msgctxt "vpn-settings-view"
-msgid "Block ads"
+msgid "Adult content"
msgstr ""
msgctxt "vpn-settings-view"
-msgid "Block adult content"
+msgid "Allows access to other devices on the same network for sharing, printing etc."
msgstr ""
msgctxt "vpn-settings-view"
-msgid "Block gambling"
+msgid "Attention: enabling this will always require a Mullvad VPN connection in order to reach the internet."
msgstr ""
msgctxt "vpn-settings-view"
-msgid "Block malware"
+msgid "Auto-connect"
msgstr ""
msgctxt "vpn-settings-view"
-msgid "Block trackers"
+msgid "Automatically connect to a server when the app launches."
msgstr ""
#. This is displayed when the custom DNS setting is turned on which makes the block
@@ -1343,7 +1333,11 @@ msgstr ""
#. Available placeholders:
#. %(preferencesPageName)s - The page title showed on top in the preferences page.
msgctxt "vpn-settings-view"
-msgid "Disable all content blockers to activate this setting."
+msgid "Disable all <b>DNS content blockers</b> above to activate this setting."
+msgstr ""
+
+msgctxt "vpn-settings-view"
+msgid "DNS content blockers"
msgstr ""
msgctxt "vpn-settings-view"
@@ -1367,6 +1361,11 @@ msgctxt "vpn-settings-view"
msgid "Enter IP"
msgstr ""
+#. Label for settings that enables block of gamling related websites.
+msgctxt "vpn-settings-view"
+msgid "Gambling"
+msgstr ""
+
#. Description for multihop settings toggle.
#. Available placeholders:
#. %(wireguard)s - Will be replaced with the string "WireGuard"
@@ -1390,6 +1389,11 @@ msgctxt "vpn-settings-view"
msgid "Lockdown mode"
msgstr ""
+#. Label for settings that enables malware blocking.
+msgctxt "vpn-settings-view"
+msgid "Malware"
+msgstr ""
+
msgctxt "vpn-settings-view"
msgid "The app’s built-in kill switch is always on. This setting will additionally block the internet if clicking Disconnect or Quit."
msgstr ""
@@ -1418,44 +1422,33 @@ msgid "This built-in feature prevents your traffic from leaking outside of the V
msgstr ""
msgctxt "vpn-settings-view"
-msgid "Tunnel protocol"
+msgid "This might cause issues on certain websites, services, and programs."
msgstr ""
+#. Label for settings that enables tracker blocking.
msgctxt "vpn-settings-view"
-msgid "Use custom DNS server"
-msgstr ""
-
-#. Title label in navigation bar
-msgctxt "vpn-settings-view"
-msgid "VPN settings"
+msgid "Trackers"
msgstr ""
msgctxt "vpn-settings-view"
-msgid "Warning: This is not an anti-virus and should not be treated as such, this is just an extra layer of protection."
-msgstr ""
-
-msgctxt "vpn-settings-view"
-msgid "Warning: This might cause issues on certain websites, services, and programs."
-msgstr ""
-
-msgctxt "vpn-settings-view"
-msgid "When enabled, this feature stops the device from contacting certain domains known to host malware."
+msgid "Tunnel protocol"
msgstr ""
msgctxt "vpn-settings-view"
-msgid "When enabled, this feature stops the device from contacting certain domains known to track users."
+msgid "Use custom DNS server"
msgstr ""
+#. Title label in navigation bar
msgctxt "vpn-settings-view"
-msgid "When enabled, this feature stops the device from contacting certain known ad domains."
+msgid "VPN settings"
msgstr ""
msgctxt "vpn-settings-view"
-msgid "When enabled, this feature stops the device from contacting certain websites and services known to host adult content."
+msgid "Warning: The malware blocker is not an anti-virus and should not be treated as such, this is just an extra layer of protection."
msgstr ""
msgctxt "vpn-settings-view"
-msgid "When enabled, this feature stops the device from contacting certain websites and services known to host gambling content."
+msgid "When this feature is enabled it stops the device from contacting certain domains or websites known for distributing ads, malware, trackers and more."
msgstr ""
msgctxt "vpn-settings-view"
diff --git a/gui/src/main/index.ts b/gui/src/main/index.ts
index e6336b1d18..4649cac8a7 100644
--- a/gui/src/main/index.ts
+++ b/gui/src/main/index.ts
@@ -11,7 +11,7 @@ import { DaemonEvent, DeviceEvent, ISettings, TunnelState } from '../shared/daem
import { messages, relayLocations } from '../shared/gettext';
import { SYSTEM_PREFERRED_LOCALE_KEY } from '../shared/gui-settings-state';
import { ITranslations, MacOsScrollbarVisibility } from '../shared/ipc-schema';
-import { IChangelog, IHistoryObject, ScrollPositions } from '../shared/ipc-types';
+import { IChangelog, IHistoryObject } from '../shared/ipc-types';
import log, { ConsoleOutput, Logger } from '../shared/logging';
import { LogLevel } from '../shared/logging-types';
import { SystemNotification } from '../shared/notifications/notification';
@@ -105,7 +105,6 @@ class ApplicationMain
private changelog?: IChangelog;
private navigationHistory?: IHistoryObject;
- private scrollPositions: ScrollPositions = {};
public run() {
// Remove window animations to combat window flickering when opening window. Can be removed when
@@ -699,7 +698,6 @@ class ApplicationMain
changelog: this.changelog ?? [],
forceShowChanges: SHOULD_SHOW_CHANGES,
navigationHistory: this.navigationHistory,
- scrollPositions: this.scrollPositions,
}));
IpcMainEventChannel.location.handleGet(() => this.daemonRpc.getLocation());
@@ -760,9 +758,6 @@ class ApplicationMain
IpcMainEventChannel.navigation.handleSetHistory((history) => {
this.navigationHistory = history;
});
- IpcMainEventChannel.navigation.handleSetScrollPositions((scrollPositions) => {
- this.scrollPositions = scrollPositions;
- });
problemReport.registerIpcListeners();
this.userInterface!.registerIpcListeners();
diff --git a/gui/src/renderer/app.tsx b/gui/src/renderer/app.tsx
index ca349b073b..80e969a920 100644
--- a/gui/src/renderer/app.tsx
+++ b/gui/src/renderer/app.tsx
@@ -25,12 +25,7 @@ import {
import { messages, relayLocations } from '../shared/gettext';
import { IGuiSettingsState, SYSTEM_PREFERRED_LOCALE_KEY } from '../shared/gui-settings-state';
import { IRelayListPair } from '../shared/ipc-schema';
-import {
- IChangelog,
- ICurrentAppVersionInfo,
- IHistoryObject,
- ScrollPositions,
-} from '../shared/ipc-types';
+import { IChangelog, ICurrentAppVersionInfo, IHistoryObject } from '../shared/ipc-types';
import log, { ConsoleOutput } from '../shared/logging';
import { LogLevel } from '../shared/logging-types';
import { Scheduler } from '../shared/scheduler';
@@ -252,8 +247,6 @@ export default class AppRenderer {
void this.updateLocation();
- this.reduxActions.userInterface.setScrollPositions(initialState.scrollPositions);
-
if (initialState.navigationHistory) {
// Set last action to POP to trigger automatic scrolling to saved coordinates.
initialState.navigationHistory.lastAction = 'POP';
@@ -518,10 +511,6 @@ export default class AppRenderer {
IpcRendererEventChannel.navigation.setHistory(history);
}
- public setScrollPositions(scrollPositions: ScrollPositions) {
- IpcRendererEventChannel.navigation.setScrollPositions(scrollPositions);
- }
-
private isLoggedIn(): boolean {
return this.deviceState?.type === 'logged in';
}
diff --git a/gui/src/renderer/components/ChevronButton.tsx b/gui/src/renderer/components/ChevronButton.tsx
index 0485c22caf..6e16fdf6db 100644
--- a/gui/src/renderer/components/ChevronButton.tsx
+++ b/gui/src/renderer/components/ChevronButton.tsx
@@ -2,7 +2,7 @@ import * as React from 'react';
import styled from 'styled-components';
import { colors } from '../../config.json';
-import * as Cell from './cell';
+import { Icon } from './cell/Label';
interface IProps extends React.HTMLAttributes<HTMLButtonElement> {
up: boolean;
@@ -13,7 +13,7 @@ const Button = styled.button({
background: 'none',
});
-const Icon = styled(Cell.Icon)({
+const StyledIcon = styled(Icon)({
flex: 0,
alignSelf: 'stretch',
justifyContent: 'center',
@@ -24,7 +24,7 @@ export default function ChevronButton(props: IProps) {
return (
<Button {...otherProps}>
- <Icon
+ <StyledIcon
tintColor={colors.white80}
tintHoverColor={colors.white}
source={up ? 'icon-chevron-up' : 'icon-chevron-down'}
diff --git a/gui/src/renderer/components/CustomDnsSettings.tsx b/gui/src/renderer/components/CustomDnsSettings.tsx
index bda3fbb864..fbf7d3dce0 100644
--- a/gui/src/renderer/components/CustomDnsSettings.tsx
+++ b/gui/src/renderer/components/CustomDnsSettings.tsx
@@ -4,6 +4,7 @@ import { sprintf } from 'sprintf-js';
import { colors, strings } from '../../config.json';
import { messages } from '../../shared/gettext';
import { useAppContext } from '../context';
+import { formatHtml } from '../lib/html-formatter';
import { IpAddress } from '../lib/ip';
import { useBoolean, useMounted } from '../lib/utilityHooks';
import { useSelector } from '../redux/store';
@@ -286,14 +287,15 @@ export default function CustomDnsSettings() {
<Cell.CellFooterText>
{featureAvailable
? messages.pgettext('vpn-settings-view', 'Enable to add at least one DNS server.')
- : // This line makes sure that the next one isn't prefixed by the color.
- // 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.
- // TRANSLATORS: Available placeholders:
- // TRANSLATORS: %(preferencesPageName)s - The page title showed on top in the preferences page.
- messages.pgettext(
- 'vpn-settings-view',
- 'Disable all content blockers to activate this setting.',
+ : formatHtml(
+ // 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.
+ // TRANSLATORS: Available placeholders:
+ // TRANSLATORS: %(preferencesPageName)s - The page title showed on top in the preferences page.
+ messages.pgettext(
+ 'vpn-settings-view',
+ 'Disable all <b>DNS content blockers</b> above to activate this setting.',
+ ),
)}
</Cell.CellFooterText>
</StyledCustomDnsFooter>
diff --git a/gui/src/renderer/components/ExpiredAccountAddTime.tsx b/gui/src/renderer/components/ExpiredAccountAddTime.tsx
index 14a2de74c4..54d27f75d8 100644
--- a/gui/src/renderer/components/ExpiredAccountAddTime.tsx
+++ b/gui/src/renderer/components/ExpiredAccountAddTime.tsx
@@ -280,7 +280,7 @@ function useFinishedCallback() {
accountSetupFinished();
}
- history.reset(RoutePath.main, undefined, transitions.push);
+ history.reset(RoutePath.main, transitions.push);
}, [isNewAccount, accountSetupFinished, history]);
return callback;
diff --git a/gui/src/renderer/components/NavigationBar.tsx b/gui/src/renderer/components/NavigationBar.tsx
index 4c27665b65..317a9bd26e 100644
--- a/gui/src/renderer/components/NavigationBar.tsx
+++ b/gui/src/renderer/components/NavigationBar.tsx
@@ -3,11 +3,8 @@ import React, { useCallback, useContext, useEffect, useLayoutEffect, useRef } fr
import { colors } from '../../config.json';
import { messages } from '../../shared/gettext';
import { useAppContext } from '../context';
-import useActions from '../lib/actionsHook';
import { useHistory } from '../lib/history';
import { useCombinedRefs } from '../lib/utilityHooks';
-import { useSelector } from '../redux/store';
-import userInterface from '../redux/userinterface/actions';
import CustomScrollbars, { CustomScrollbarsRef, IScrollEvent } from './CustomScrollbars';
import { BackActionContext } from './KeyboardNavigation';
import {
@@ -92,41 +89,35 @@ export const NavigationScrollbars = React.forwardRef(function NavigationScrollba
forwardedRef?: React.Ref<CustomScrollbarsRef>,
) {
const history = useHistory();
+ const { setNavigationHistory } = useAppContext();
const { onScroll } = useContext(NavigationScrollContext);
- const { setScrollPositions } = useAppContext();
const ref = useRef<CustomScrollbarsRef>();
const combinedRefs = useCombinedRefs(forwardedRef, ref);
- const { addScrollPosition, removeScrollPosition } = useActions(userInterface);
- const scrollPositions = useSelector((state) => state.userInterface.scrollPosition);
-
useEffect(() => {
- const path = history.location.pathname;
const beforeunload = () => {
if (ref.current) {
- const scrollPosition = ref.current.getScrollPosition();
- setScrollPositions({ ...scrollPositions, [path]: scrollPosition });
+ history.location.state.scrollPosition = ref.current.getScrollPosition();
+ setNavigationHistory(history.asObject);
}
};
window.addEventListener('beforeunload', beforeunload);
return () => window.removeEventListener('beforeunload', beforeunload);
- }, [scrollPositions]);
+ }, []);
useLayoutEffect(() => {
- const path = history.location.pathname;
-
- const scrollPosition = scrollPositions[history.location.pathname];
- if (history.action === 'POP' && scrollPosition) {
- ref.current?.scrollTo(...scrollPosition);
- removeScrollPosition(path);
+ const location = history.location;
+ if (history.action === 'POP') {
+ ref.current?.scrollTo(...location.state.scrollPosition);
}
return () => {
if (history.action === 'PUSH' && ref.current) {
- addScrollPosition(path, ref.current.getScrollPosition());
+ location.state.scrollPosition = ref.current.getScrollPosition();
+ setNavigationHistory(history.asObject);
}
};
}, []);
diff --git a/gui/src/renderer/components/SplitTunnelingSettings.tsx b/gui/src/renderer/components/SplitTunnelingSettings.tsx
index 4cad24f445..f68be19589 100644
--- a/gui/src/renderer/components/SplitTunnelingSettings.tsx
+++ b/gui/src/renderer/components/SplitTunnelingSettings.tsx
@@ -410,6 +410,16 @@ export function WindowsSplitTunnelingSettings(props: IPlatformSplitTunnelingSett
splitTunnelingEnabled &&
(!filteredNonSplitApplications || filteredNonSplitApplications.length > 0);
+ const excludedTitle = (
+ <Cell.SectionTitle>
+ {messages.pgettext('split-tunneling-view', 'Excluded apps')}
+ </Cell.SectionTitle>
+ );
+
+ const allTitle = (
+ <Cell.SectionTitle>{messages.pgettext('split-tunneling-view', 'All apps')}</Cell.SectionTitle>
+ );
+
return (
<>
<SettingsHeader>
@@ -428,10 +438,7 @@ export function WindowsSplitTunnelingSettings(props: IPlatformSplitTunnelingSett
{splitTunnelingEnabled && <SearchBar searchTerm={searchTerm} onSearch={setSearchTerm} />}
<Accordion expanded={showSplitSection}>
- <Cell.Section>
- <Cell.SectionTitle>
- {messages.pgettext('split-tunneling-view', 'Excluded apps')}
- </Cell.SectionTitle>
+ <Cell.Section sectionTitle={excludedTitle}>
<ApplicationList
applications={filteredSplitApplications}
rowRenderer={excludedRowRenderer}
@@ -440,10 +447,7 @@ export function WindowsSplitTunnelingSettings(props: IPlatformSplitTunnelingSett
</Accordion>
<Accordion expanded={showNonSplitSection}>
- <Cell.Section>
- <Cell.SectionTitle>
- {messages.pgettext('split-tunneling-view', 'All apps')}
- </Cell.SectionTitle>
+ <Cell.Section sectionTitle={allTitle}>
<ApplicationList
applications={filteredNonSplitApplications}
rowRenderer={includedRowRenderer}
diff --git a/gui/src/renderer/components/VpnSettings.tsx b/gui/src/renderer/components/VpnSettings.tsx
index ad5d3439b1..1213703f61 100644
--- a/gui/src/renderer/components/VpnSettings.tsx
+++ b/gui/src/renderer/components/VpnSettings.tsx
@@ -2,7 +2,7 @@ import { useCallback, useMemo } from 'react';
import { sprintf } from 'sprintf-js';
import styled from 'styled-components';
-import { strings } from '../../config.json';
+import { colors, strings } from '../../config.json';
import { IDnsOptions, TunnelProtocol } from '../../shared/daemon-rpc-types';
import { messages } from '../../shared/gettext';
import log from '../../shared/logging';
@@ -47,6 +47,14 @@ const StyledSelectorContainer = styled.div({
flex: 0,
});
+const StyledTitleLabel = styled(Cell.SectionTitle)({
+ flex: 1,
+});
+
+const StyledSectionItem = styled(Cell.Container)({
+ backgroundColor: colors.blue40,
+});
+
export default function VpnSettings() {
const { pop } = useHistory();
@@ -82,11 +90,7 @@ export default function VpnSettings() {
</Cell.Group>
<Cell.Group>
- <BlockAds />
- <BlockTrackers />
- <BlockMalware />
- <BlockGambling />
- <BlockAdultContent />
+ <DnsBlockers />
</Cell.Group>
<Cell.Group>
@@ -230,38 +234,63 @@ function useDns(setting: keyof IDnsOptions['defaultOptions']) {
return [dns, updateBlockSetting] as const;
}
+function DnsBlockers() {
+ const dns = useSelector((state) => state.settings.dns);
+
+ const title = (
+ <>
+ <StyledTitleLabel as="label" disabled={dns.state === 'custom'}>
+ {messages.pgettext('vpn-settings-view', 'DNS content blockers')}
+ </StyledTitleLabel>
+ <InfoButton>
+ <ModalMessage>
+ {messages.pgettext(
+ 'vpn-settings-view',
+ 'When this feature is enabled it stops the device from contacting certain domains or websites known for distributing ads, malware, trackers and more.',
+ )}
+ </ModalMessage>
+ <ModalMessage>
+ {messages.pgettext(
+ 'vpn-settings-view',
+ 'This might cause issues on certain websites, services, and programs.',
+ )}
+ </ModalMessage>
+ </InfoButton>
+ </>
+ );
+
+ return (
+ <Cell.ExpandableSection sectionTitle={title} expandableId="dns-blockers">
+ <BlockAds />
+ <BlockTrackers />
+ <BlockMalware />
+ <BlockGambling />
+ <BlockAdultContent />
+ </Cell.ExpandableSection>
+ );
+}
+
function BlockAds() {
const [dns, setBlockAds] = useDns('blockAds');
return (
<AriaInputGroup>
- <Cell.Container disabled={dns.state === 'custom'}>
+ <StyledSectionItem disabled={dns.state === 'custom'}>
<AriaLabel>
- <Cell.InputLabel>{messages.pgettext('vpn-settings-view', 'Block ads')}</Cell.InputLabel>
+ <Cell.InputLabel>
+ {
+ // TRANSLATORS: Label for settings that enables ad blocking.
+ messages.pgettext('vpn-settings-view', 'Ads')
+ }
+ </Cell.InputLabel>
</AriaLabel>
- <AriaDetails>
- <InfoButton>
- <ModalMessage>
- {messages.pgettext(
- 'vpn-settings-view',
- 'When enabled, this feature stops the device from contacting certain known ad domains.',
- )}
- </ModalMessage>
- <ModalMessage>
- {messages.pgettext(
- 'vpn-settings-view',
- 'Warning: This might cause issues on certain websites, services, and programs.',
- )}
- </ModalMessage>
- </InfoButton>
- </AriaDetails>
<AriaInput>
<Cell.Switch
isOn={dns.state === 'default' && dns.defaultOptions.blockAds}
onChange={setBlockAds}
/>
</AriaInput>
- </Cell.Container>
+ </StyledSectionItem>
</AriaInputGroup>
);
}
@@ -271,35 +300,22 @@ function BlockTrackers() {
return (
<AriaInputGroup>
- <Cell.Container disabled={dns.state === 'custom'}>
+ <StyledSectionItem disabled={dns.state === 'custom'}>
<AriaLabel>
<Cell.InputLabel>
- {messages.pgettext('vpn-settings-view', 'Block trackers')}
+ {
+ // TRANSLATORS: Label for settings that enables tracker blocking.
+ messages.pgettext('vpn-settings-view', 'Trackers')
+ }
</Cell.InputLabel>
</AriaLabel>
- <AriaDetails>
- <InfoButton>
- <ModalMessage>
- {messages.pgettext(
- 'vpn-settings-view',
- 'When enabled, this feature stops the device from contacting certain domains known to track users.',
- )}
- </ModalMessage>
- <ModalMessage>
- {messages.pgettext(
- 'vpn-settings-view',
- 'Warning: This might cause issues on certain websites, services, and programs.',
- )}
- </ModalMessage>
- </InfoButton>
- </AriaDetails>
<AriaInput>
<Cell.Switch
isOn={dns.state === 'default' && dns.defaultOptions.blockTrackers}
onChange={setBlockTrackers}
/>
</AriaInput>
- </Cell.Container>
+ </StyledSectionItem>
</AriaInputGroup>
);
}
@@ -309,10 +325,13 @@ function BlockMalware() {
return (
<AriaInputGroup>
- <Cell.Container disabled={dns.state === 'custom'}>
+ <StyledSectionItem disabled={dns.state === 'custom'}>
<AriaLabel>
<Cell.InputLabel>
- {messages.pgettext('vpn-settings-view', 'Block malware')}
+ {
+ // TRANSLATORS: Label for settings that enables malware blocking.
+ messages.pgettext('vpn-settings-view', 'Malware')
+ }
</Cell.InputLabel>
</AriaLabel>
<AriaDetails>
@@ -320,13 +339,7 @@ function BlockMalware() {
<ModalMessage>
{messages.pgettext(
'vpn-settings-view',
- 'When enabled, this feature stops the device from contacting certain domains known to host malware.',
- )}
- </ModalMessage>
- <ModalMessage>
- {messages.pgettext(
- 'vpn-settings-view',
- 'Warning: This is not an anti-virus and should not be treated as such, this is just an extra layer of protection.',
+ 'Warning: The malware blocker is not an anti-virus and should not be treated as such, this is just an extra layer of protection.',
)}
</ModalMessage>
</InfoButton>
@@ -337,7 +350,7 @@ function BlockMalware() {
onChange={setBlockMalware}
/>
</AriaInput>
- </Cell.Container>
+ </StyledSectionItem>
</AriaInputGroup>
);
}
@@ -347,27 +360,22 @@ function BlockGambling() {
return (
<AriaInputGroup>
- <Cell.Container disabled={dns.state === 'custom'}>
+ <StyledSectionItem disabled={dns.state === 'custom'}>
<AriaLabel>
<Cell.InputLabel>
- {messages.pgettext('vpn-settings-view', 'Block gambling')}
+ {
+ // TRANSLATORS: Label for settings that enables block of gamling related websites.
+ messages.pgettext('vpn-settings-view', 'Gambling')
+ }
</Cell.InputLabel>
</AriaLabel>
- <AriaDetails>
- <InfoButton
- message={messages.pgettext(
- 'vpn-settings-view',
- 'When enabled, this feature stops the device from contacting certain websites and services known to host gambling content.',
- )}
- />
- </AriaDetails>
<AriaInput>
<Cell.Switch
isOn={dns.state === 'default' && dns.defaultOptions.blockGambling}
onChange={setBlockGambling}
/>
</AriaInput>
- </Cell.Container>
+ </StyledSectionItem>
</AriaInputGroup>
);
}
@@ -377,27 +385,22 @@ function BlockAdultContent() {
return (
<AriaInputGroup>
- <Cell.Container disabled={dns.state === 'custom'}>
+ <StyledSectionItem disabled={dns.state === 'custom'}>
<AriaLabel>
<Cell.InputLabel>
- {messages.pgettext('vpn-settings-view', 'Block adult content')}
+ {
+ // TRANSLATORS: Label for settings that enables block of adult content.
+ messages.pgettext('vpn-settings-view', 'Adult content')
+ }
</Cell.InputLabel>
</AriaLabel>
- <AriaDetails>
- <InfoButton
- message={messages.pgettext(
- 'vpn-settings-view',
- 'When enabled, this feature stops the device from contacting certain websites and services known to host adult content.',
- )}
- />
- </AriaDetails>
<AriaInput>
<Cell.Switch
isOn={dns.state === 'default' && dns.defaultOptions.blockAdultContent}
onChange={setBlockAdultContent}
/>
</AriaInput>
- </Cell.Container>
+ </StyledSectionItem>
{dns.state === 'custom' && <CustomDnsEnabledFooter />}
</AriaInputGroup>
);
diff --git a/gui/src/renderer/components/WireguardSettings.tsx b/gui/src/renderer/components/WireguardSettings.tsx
index dea8c6dbc2..18c39a7396 100644
--- a/gui/src/renderer/components/WireguardSettings.tsx
+++ b/gui/src/renderer/components/WireguardSettings.tsx
@@ -276,6 +276,8 @@ function Udp2tcpPortSetting() {
[],
);
+ const expandableProps = useMemo(() => ({ expandable: true, id: 'udp2tcp-port' }), []);
+
const selectPort = useCallback(
async (port: LiftedConstraint<number>) => {
await setObfuscationSettings({
@@ -307,7 +309,7 @@ function Udp2tcpPortSetting() {
value={port}
onSelect={selectPort}
disabled={obfuscationSettings.selectedObfuscation === ObfuscationType.off}
- expandable
+ expandable={expandableProps}
thinTitle
automaticValue={'any' as const}
/>
diff --git a/gui/src/renderer/components/cell/Section.tsx b/gui/src/renderer/components/cell/Section.tsx
index 38df17ebdb..8ba5975476 100644
--- a/gui/src/renderer/components/cell/Section.tsx
+++ b/gui/src/renderer/components/cell/Section.tsx
@@ -1,8 +1,14 @@
-import React from 'react';
+import React, { useEffect } from 'react';
import styled from 'styled-components';
import { colors } from '../../../config.json';
+import { useAppContext } from '../../context';
+import { useHistory } from '../../lib/history';
+import { useBoolean } from '../../lib/utilityHooks';
+import Accordion from '../Accordion';
+import ChevronButton from '../ChevronButton';
import { buttonText, openSans, sourceSansPro } from '../common-styles';
+import { Container } from './Container';
import { Row } from './Row';
const StyledSection = styled.div({
@@ -25,11 +31,61 @@ export const SectionTitle = styled(Row)(buttonText, (props: SectionTitleProps) =
export const CellSectionContext = React.createContext<boolean>(false);
-export function Section(props: React.HTMLAttributes<HTMLDivElement>) {
- const { children, ...otherProps } = props;
+interface SectionProps extends React.HTMLAttributes<HTMLDivElement> {
+ sectionTitle?: React.ReactElement;
+}
+
+export function Section(props: SectionProps) {
+ const { children, sectionTitle, ...otherProps } = props;
return (
<StyledSection {...otherProps}>
- <CellSectionContext.Provider value={true}>{children}</CellSectionContext.Provider>
+ <CellSectionContext.Provider value={true}>
+ {sectionTitle && <StyledTitleContainer>{sectionTitle}</StyledTitleContainer>}
+ {children}
+ </CellSectionContext.Provider>
</StyledSection>
);
}
+
+const StyledChevronButton = styled(ChevronButton)({
+ padding: 0,
+ marginRight: '16px',
+});
+
+const StyledTitleContainer = styled(Container)({
+ display: 'flex',
+ padding: 0,
+});
+
+interface ExpandableSectionProps extends SectionProps {
+ expandableId: string;
+ expandedInitially?: boolean;
+}
+
+export function ExpandableSection(props: ExpandableSectionProps) {
+ const { expandableId, expandedInitially, sectionTitle, ...otherProps } = props;
+
+ const history = useHistory();
+ const { setNavigationHistory } = useAppContext();
+ const expandedValue =
+ history.location.state.expandedSections[props.expandableId] ?? !!expandedInitially;
+ const [expanded, , , toggleExpanded] = useBoolean(expandedValue);
+
+ useEffect(() => {
+ history.location.state.expandedSections[props.expandableId] = expanded;
+ setNavigationHistory(history.asObject);
+ }, [expanded]);
+
+ const title = (
+ <>
+ {sectionTitle}
+ <StyledChevronButton up={expanded} onClick={toggleExpanded} />
+ </>
+ );
+
+ return (
+ <Section className={props.className} sectionTitle={title} {...otherProps}>
+ <Accordion expanded={expanded}>{props.children}</Accordion>
+ </Section>
+ );
+}
diff --git a/gui/src/renderer/components/cell/Selector.tsx b/gui/src/renderer/components/cell/Selector.tsx
index cb0d6344b6..ed49444da1 100644
--- a/gui/src/renderer/components/cell/Selector.tsx
+++ b/gui/src/renderer/components/cell/Selector.tsx
@@ -3,28 +3,15 @@ import styled from 'styled-components';
import { colors } from '../../../config.json';
import { messages } from '../../../shared/gettext';
-import { useBoolean } from '../../lib/utilityHooks';
-import Accordion from '../Accordion';
import { AriaDetails, AriaInput, AriaLabel } from '../AriaGroup';
-import ChevronButton from '../ChevronButton';
import { normalText } from '../common-styles';
import InfoButton from '../InfoButton';
import * as Cell from '.';
-const StyledTitle = styled(Cell.Container)({
- display: 'flex',
- padding: 0,
-});
-
const StyledTitleLabel = styled(Cell.SectionTitle)({
flex: 1,
});
-const StyledChevronButton = styled(ChevronButton)({
- padding: 0,
- marginRight: '16px',
-});
-
export interface SelectorItem<T> {
label: string;
value: T;
@@ -39,7 +26,7 @@ interface CommonSelectorProps<T, U> {
selectedCellRef?: React.Ref<HTMLElement>;
className?: string;
details?: React.ReactElement;
- expandable?: boolean;
+ expandable?: { expandable: boolean; id: string };
disabled?: boolean;
thinTitle?: boolean;
automaticLabel?: string;
@@ -52,8 +39,6 @@ interface SelectorProps<T, U> extends CommonSelectorProps<T, U> {
}
export default function Selector<T, U>(props: SelectorProps<T, U>) {
- const [expanded, , , toggleExpanded] = useBoolean(!props.expandable);
-
const items = props.items.map((item) => {
const selected = props.value === item.value;
const ref = selected ? (props.selectedCellRef as React.Ref<HTMLButtonElement>) : undefined;
@@ -88,8 +73,8 @@ export default function Selector<T, U>(props: SelectorProps<T, U>) {
);
}
- const title = props.title && (
- <StyledTitle>
+ const title = props.title ? (
+ <>
<AriaLabel>
<StyledTitleLabel as="label" disabled={props.disabled} thin={props.thinTitle}>
{props.title}
@@ -100,8 +85,9 @@ export default function Selector<T, U>(props: SelectorProps<T, U>) {
<InfoButton>{props.details}</InfoButton>
</AriaDetails>
)}
- {props.expandable && <StyledChevronButton up={expanded} onClick={toggleExpanded} />}
- </StyledTitle>
+ </>
+ ) : (
+ <></>
);
// Add potential additional items to the list. Used for custom entry.
@@ -112,14 +98,28 @@ export default function Selector<T, U>(props: SelectorProps<T, U>) {
</Cell.Group>
);
- return (
- <AriaInput>
- <Cell.Section role="listbox" className={props.className}>
- {title}
- {props.expandable ? <Accordion expanded={expanded}>{children}</Accordion> : children}
- </Cell.Section>
- </AriaInput>
- );
+ if (props.expandable?.expandable) {
+ return (
+ <AriaInput>
+ <Cell.ExpandableSection
+ role="listbox"
+ expandedInitially={false}
+ className={props.className}
+ sectionTitle={title}
+ expandableId={props.expandable.id}>
+ {children}
+ </Cell.ExpandableSection>
+ </AriaInput>
+ );
+ } else {
+ return (
+ <AriaInput>
+ <Cell.Section role="listbox" className={props.className} sectionTitle={title}>
+ {children}
+ </Cell.Section>
+ </AriaInput>
+ );
+ }
}
const StyledCellIcon = styled(Cell.Icon)((props: { visible: boolean }) => ({
diff --git a/gui/src/renderer/lib/history.tsx b/gui/src/renderer/lib/history.tsx
index 486093cca0..b81aef50fe 100644
--- a/gui/src/renderer/lib/history.tsx
+++ b/gui/src/renderer/lib/history.tsx
@@ -1,7 +1,7 @@
import { Action, History as OriginalHistory, Location, LocationDescriptorObject } from 'history';
import { useHistory as useReactRouterHistory } from 'react-router';
-import { IHistoryObject } from '../../shared/ipc-types';
+import { IHistoryObject, LocationState } from '../../shared/ipc-types';
import { GeneratedRoutePath, RoutePath } from './routes';
export interface ITransitionSpecification {
@@ -39,25 +39,21 @@ export const transitions: ITransitionMap = {
},
};
-type LocationDescriptor<S> = RoutePath | GeneratedRoutePath | LocationDescriptorObject<S>;
+type LocationDescriptor = RoutePath | GeneratedRoutePath | LocationDescriptorObject<LocationState>;
-type LocationListener<S = unknown> = (
- location: Location<S>,
+type LocationListener = (
+ location: Location<LocationState>,
action: Action,
transition: ITransitionSpecification,
) => void;
-// It currently isn't possible to implement this correctly with support for a generic state. State
-// can be added as a generic type (<S = unknown>) after this issue has been resolved:
-// https://github.com/DefinitelyTyped/DefinitelyTyped/issues/49060
-type S = unknown;
export default class History {
- private listeners: LocationListener<S>[] = [];
- private entries: Location<S>[];
+ private listeners: LocationListener[] = [];
+ private entries: Location<LocationState>[];
private index = 0;
private lastAction: Action = 'POP';
- public constructor(location: LocationDescriptor<S>, state?: S) {
+ public constructor(location: LocationDescriptor, state?: LocationState) {
this.entries = [this.createLocation(location, state)];
}
@@ -70,7 +66,7 @@ export default class History {
return history;
}
- public get location(): Location<S> {
+ public get location(): Location<LocationState> {
return this.entries[this.index];
}
@@ -82,7 +78,7 @@ export default class History {
return this.lastAction;
}
- public push = (nextLocation: LocationDescriptor<S>, nextState?: S) => {
+ public push = (nextLocation: LocationDescriptor, nextState?: LocationState) => {
this.pushImpl(nextLocation, nextState);
this.notify(transitions.push);
};
@@ -93,7 +89,7 @@ export default class History {
}
};
- public show = (nextLocation: LocationDescriptor<S>, nextState?: S) => {
+ public show = (nextLocation: LocationDescriptor, nextState?: LocationState) => {
this.pushImpl(nextLocation, nextState);
this.notify(transitions.show);
};
@@ -105,9 +101,9 @@ export default class History {
};
public reset = (
- nextLocation: LocationDescriptor<S>,
+ nextLocation: LocationDescriptor,
transition?: ITransitionSpecification,
- nextState?: S,
+ nextState?: LocationState,
) => {
const location = this.createLocation(nextLocation, nextState);
this.lastAction = 'REPLACE';
@@ -117,7 +113,7 @@ export default class History {
this.notify(transition ?? transitions.none);
};
- public listen(callback: LocationListener<S>) {
+ public listen(callback: LocationListener) {
this.listeners.push(callback);
return () => (this.listeners = this.listeners.filter((listener) => listener !== callback));
}
@@ -162,7 +158,7 @@ export default class History {
throw Error('Not implemented');
}
- private pushImpl(nextLocation: LocationDescriptor<S>, nextState?: S) {
+ private pushImpl(nextLocation: LocationDescriptor, nextState?: LocationState) {
const location = this.createLocation(nextLocation, nextState);
this.lastAction = 'PUSH';
this.index += 1;
@@ -185,7 +181,10 @@ export default class History {
this.listeners.forEach((listener) => listener(this.location, this.action, transition));
}
- private createLocation(location: LocationDescriptor<S>, state?: S): Location<S> {
+ private createLocation(
+ location: LocationDescriptor,
+ state?: LocationState,
+ ): Location<LocationState> {
if (typeof location === 'string') {
return this.createLocationFromString(location, state);
} else if ('routePath' in location) {
@@ -195,18 +194,18 @@ export default class History {
pathname: location.pathname ?? this.location.pathname,
search: location.search ?? '',
hash: location.hash ?? '',
- state: location.state,
+ state: location.state ?? { scrollPosition: [0, 0], expandedSections: {} },
key: location.key ?? this.getRandomKey(),
};
}
}
- private createLocationFromString(path: string, state?: S): Location<S> {
+ private createLocationFromString(path: string, state?: LocationState): Location<LocationState> {
return {
pathname: path,
search: '',
hash: '',
- state,
+ state: state ?? { scrollPosition: [0, 0], expandedSections: {} },
key: this.getRandomKey(),
};
}
@@ -217,5 +216,5 @@ export default class History {
}
export function useHistory(): History {
- return useReactRouterHistory() as History;
+ return useReactRouterHistory<LocationState>() as History;
}
diff --git a/gui/src/renderer/redux/userinterface/actions.ts b/gui/src/renderer/redux/userinterface/actions.ts
index 19ae7380bc..46da6bdc3d 100644
--- a/gui/src/renderer/redux/userinterface/actions.ts
+++ b/gui/src/renderer/redux/userinterface/actions.ts
@@ -1,5 +1,5 @@
import { MacOsScrollbarVisibility } from '../../../shared/ipc-schema';
-import { IChangelog, ScrollPositions } from '../../../shared/ipc-types';
+import { IChangelog } from '../../../shared/ipc-types';
export interface IUpdateLocaleAction {
type: 'UPDATE_LOCALE';
@@ -20,22 +20,6 @@ export interface ISetWindowFocusedAction {
focused: boolean;
}
-export interface ISetScrollPositions {
- type: 'SET_SCROLL_POSITIONS';
- scrollPositions: ScrollPositions;
-}
-
-export interface IAddScrollPosition {
- type: 'ADD_SCROLL_POSITION';
- path: string;
- scrollPosition: [number, number];
-}
-
-export interface IRemoveScrollPosition {
- type: 'REMOVE_SCROLL_POSITION';
- path: string;
-}
-
export interface ISetMacOsScrollbarVisibility {
type: 'SET_MACOS_SCROLLBAR_VISIBILITY';
visibility: MacOsScrollbarVisibility;
@@ -66,9 +50,6 @@ export type UserInterfaceAction =
| IUpdateWindowArrowPositionAction
| IUpdateConnectionInfoOpenAction
| ISetWindowFocusedAction
- | ISetScrollPositions
- | IAddScrollPosition
- | IRemoveScrollPosition
| ISetMacOsScrollbarVisibility
| ISetConnectedToDaemon
| ISetChangelog
@@ -102,28 +83,6 @@ function setWindowFocused(focused: boolean): ISetWindowFocusedAction {
};
}
-function setScrollPositions(scrollPositions: ScrollPositions): ISetScrollPositions {
- return {
- type: 'SET_SCROLL_POSITIONS',
- scrollPositions,
- };
-}
-
-function addScrollPosition(path: string, scrollPosition: [number, number]): IAddScrollPosition {
- return {
- type: 'ADD_SCROLL_POSITION',
- path,
- scrollPosition,
- };
-}
-
-function removeScrollPosition(path: string): IRemoveScrollPosition {
- return {
- type: 'REMOVE_SCROLL_POSITION',
- path,
- };
-}
-
function setMacOsScrollbarVisibility(
visibility: MacOsScrollbarVisibility,
): ISetMacOsScrollbarVisibility {
@@ -166,9 +125,6 @@ export default {
updateWindowArrowPosition,
toggleConnectionPanel,
setWindowFocused,
- setScrollPositions,
- addScrollPosition,
- removeScrollPosition,
setMacOsScrollbarVisibility,
setConnectedToDaemon,
setChangelog,
diff --git a/gui/src/renderer/redux/userinterface/reducers.ts b/gui/src/renderer/redux/userinterface/reducers.ts
index 2ab684b377..96d8bc03e6 100644
--- a/gui/src/renderer/redux/userinterface/reducers.ts
+++ b/gui/src/renderer/redux/userinterface/reducers.ts
@@ -7,7 +7,6 @@ export interface IUserInterfaceReduxState {
arrowPosition?: number;
connectionPanelVisible: boolean;
windowFocused: boolean;
- scrollPosition: Record<string, [number, number]>;
macOsScrollbarVisibility?: MacOsScrollbarVisibility;
connectedToDaemon: boolean;
changelog: IChangelog;
@@ -19,7 +18,6 @@ const initialState: IUserInterfaceReduxState = {
locale: 'en',
connectionPanelVisible: false,
windowFocused: false,
- scrollPosition: {},
macOsScrollbarVisibility: undefined,
connectedToDaemon: false,
changelog: [],
@@ -44,21 +42,6 @@ export default function (
case 'SET_WINDOW_FOCUSED':
return { ...state, windowFocused: action.focused };
- case 'SET_SCROLL_POSITIONS':
- return { ...state, scrollPosition: action.scrollPositions };
-
- case 'ADD_SCROLL_POSITION':
- return {
- ...state,
- scrollPosition: { ...state.scrollPosition, [action.path]: action.scrollPosition },
- };
-
- case 'REMOVE_SCROLL_POSITION': {
- const scrollPosition = { ...state.scrollPosition };
- delete scrollPosition[action.path];
- return { ...state, scrollPosition };
- }
-
case 'SET_MACOS_SCROLLBAR_VISIBILITY':
return { ...state, macOsScrollbarVisibility: action.visibility };
diff --git a/gui/src/shared/ipc-schema.ts b/gui/src/shared/ipc-schema.ts
index 85c7f2cabc..210e85e007 100644
--- a/gui/src/shared/ipc-schema.ts
+++ b/gui/src/shared/ipc-schema.ts
@@ -34,7 +34,6 @@ import {
ICurrentAppVersionInfo,
IHistoryObject,
IWindowShapeParameters,
- ScrollPositions,
} from './ipc-types';
export interface ITranslations {
@@ -76,7 +75,6 @@ export interface IAppStateSnapshot {
changelog: IChangelog;
forceShowChanges: boolean;
navigationHistory?: IHistoryObject;
- scrollPositions: ScrollPositions;
}
// The different types of requests are:
@@ -130,7 +128,6 @@ export const ipcSchema = {
navigation: {
reset: notifyRenderer<void>(),
setHistory: send<IHistoryObject>(),
- setScrollPositions: send<ScrollPositions>(),
},
daemon: {
isPerformingPostUpgrade: notifyRenderer<boolean>(),
diff --git a/gui/src/shared/ipc-types.ts b/gui/src/shared/ipc-types.ts
index 2b42f42b0e..81ca28b1ac 100644
--- a/gui/src/shared/ipc-types.ts
+++ b/gui/src/shared/ipc-types.ts
@@ -13,8 +13,13 @@ export interface IWindowShapeParameters {
export type IChangelog = Array<string>;
+export interface LocationState {
+ scrollPosition: [number, number];
+ expandedSections: Record<string, boolean>;
+}
+
export interface IHistoryObject {
- entries: Location<unknown>[];
+ entries: Location<LocationState>[];
index: number;
lastAction: Action;
}