diff options
| -rw-r--r-- | gui/locales/messages.pot | 71 | ||||
| -rw-r--r-- | gui/src/main/index.ts | 7 | ||||
| -rw-r--r-- | gui/src/renderer/app.tsx | 13 | ||||
| -rw-r--r-- | gui/src/renderer/components/ChevronButton.tsx | 6 | ||||
| -rw-r--r-- | gui/src/renderer/components/CustomDnsSettings.tsx | 18 | ||||
| -rw-r--r-- | gui/src/renderer/components/ExpiredAccountAddTime.tsx | 2 | ||||
| -rw-r--r-- | gui/src/renderer/components/NavigationBar.tsx | 27 | ||||
| -rw-r--r-- | gui/src/renderer/components/SplitTunnelingSettings.tsx | 20 | ||||
| -rw-r--r-- | gui/src/renderer/components/VpnSettings.tsx | 155 | ||||
| -rw-r--r-- | gui/src/renderer/components/WireguardSettings.tsx | 4 | ||||
| -rw-r--r-- | gui/src/renderer/components/cell/Section.tsx | 64 | ||||
| -rw-r--r-- | gui/src/renderer/components/cell/Selector.tsx | 56 | ||||
| -rw-r--r-- | gui/src/renderer/lib/history.tsx | 45 | ||||
| -rw-r--r-- | gui/src/renderer/redux/userinterface/actions.ts | 46 | ||||
| -rw-r--r-- | gui/src/renderer/redux/userinterface/reducers.ts | 17 | ||||
| -rw-r--r-- | gui/src/shared/ipc-schema.ts | 3 | ||||
| -rw-r--r-- | gui/src/shared/ipc-types.ts | 7 |
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; } |
