diff options
| author | Tobias Järvelöv <tobias.jarvelov@mullvad.net> | 2025-10-15 14:06:54 +0200 |
|---|---|---|
| committer | Tobias Järvelöv <tobias.jarvelov@mullvad.net> | 2025-10-15 14:06:54 +0200 |
| commit | f72fc6ec456abc355d2ef966731851e705e65872 (patch) | |
| tree | 6627b68c970e3ce5965fc8720a42cde29f119cea | |
| parent | 0fd34e680a7489ab8aa70803513417d8f7d89d2d (diff) | |
| parent | f42821860eb61b30ef10c0513b501f076fd69550 (diff) | |
| download | mullvadvpn-f72fc6ec456abc355d2ef966731851e705e65872.tar.xz mullvadvpn-f72fc6ec456abc355d2ef966731851e705e65872.zip | |
Merge branch 'use-new-components-in-user-interface-settings-des-2564'
26 files changed, 475 insertions, 337 deletions
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/AppRouter.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/AppRouter.tsx index 561ab55877..7fc308f6c4 100644 --- a/desktop/packages/mullvad-vpn/src/renderer/components/AppRouter.tsx +++ b/desktop/packages/mullvad-vpn/src/renderer/components/AppRouter.tsx @@ -19,12 +19,10 @@ import ExpiredAccountErrorView from './ExpiredAccountErrorView'; import Filter from './Filter'; import Focus, { IFocusHandle } from './Focus'; import ProblemReport from './ProblemReport'; -import SelectLanguage from './SelectLanguage'; import SettingsImport from './SettingsImport'; import SettingsTextImport from './SettingsTextImport'; import StateTriggeredNavigation from './StateTriggeredNavigation'; import Support from './Support'; -import UserInterfaceSettings from './UserInterfaceSettings'; import { Account, AppInfoView, @@ -37,11 +35,13 @@ import { ManageDevicesView, MultihopSettingsView, OpenVpnSettingsView, + SelectLanguageView, SettingsView, ShadowsocksSettingsView, SplitTunnelingView, TooManyDevicesView, UdpOverTcpSettingsView, + UserInterfaceSettingsView, VpnSettingsView, WireguardSettingsView, } from './views'; @@ -71,8 +71,12 @@ export default function AppRouter() { <Route exact path={RoutePath.setupFinished} component={SetupFinished} /> <Route exact path={RoutePath.account} component={Account} /> <Route exact path={RoutePath.settings} component={SettingsView} /> - <Route exact path={RoutePath.selectLanguage} component={SelectLanguage} /> - <Route exact path={RoutePath.userInterfaceSettings} component={UserInterfaceSettings} /> + <Route exact path={RoutePath.selectLanguage} component={SelectLanguageView} /> + <Route + exact + path={RoutePath.userInterfaceSettings} + component={UserInterfaceSettingsView} + /> <Route exact path={RoutePath.multihopSettings} component={MultihopSettingsView} /> <Route exact path={RoutePath.vpnSettings} component={VpnSettingsView} /> <Route exact path={RoutePath.wireguardSettings} component={WireguardSettingsView} /> diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/UserInterfaceSettings.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/UserInterfaceSettings.tsx deleted file mode 100644 index 5a1e4ad39e..0000000000 --- a/desktop/packages/mullvad-vpn/src/renderer/components/UserInterfaceSettings.tsx +++ /dev/null @@ -1,258 +0,0 @@ -import styled from 'styled-components'; - -import { messages } from '../../shared/gettext'; -import { RoutePath } from '../../shared/routes'; -import { useAppContext } from '../context'; -import { Image } from '../lib/components'; -import { useHistory } from '../lib/history'; -import { useSelector } from '../redux/store'; -import { AppNavigationHeader } from './'; -import { AriaDescription, AriaInput, AriaInputGroup, AriaLabel } from './AriaGroup'; -import * as Cell from './cell'; -import { BackAction } from './KeyboardNavigation'; -import { Layout, SettingsContainer, SettingsContent, SettingsGroup, SettingsStack } from './Layout'; -import { NavigationContainer } from './NavigationContainer'; -import { NavigationScrollbars } from './NavigationScrollbars'; -import { SettingsNavigationListItem } from './settings-navigation-list-item'; -import SettingsHeader, { HeaderTitle } from './SettingsHeader'; - -const StyledAnimateMapCellGroup = styled(SettingsGroup)({ - '@media (prefers-reduced-motion: reduce)': { - display: 'none', - }, -}); - -export default function UserInterfaceSettings() { - const { pop } = useHistory(); - const unpinnedWindow = useSelector((state) => state.settings.guiSettings.unpinnedWindow); - - return ( - <BackAction action={pop}> - <Layout> - <SettingsContainer> - <NavigationContainer> - <AppNavigationHeader - title={ - // TRANSLATORS: Title label in navigation bar - messages.pgettext('user-interface-settings-view', 'User interface settings') - } - /> - - <NavigationScrollbars> - <SettingsHeader> - <HeaderTitle> - {messages.pgettext('user-interface-settings-view', 'User interface settings')} - </HeaderTitle> - </SettingsHeader> - - <SettingsContent> - <SettingsStack> - <SettingsGroup> - <NotificationsSetting /> - </SettingsGroup> - <SettingsGroup> - <MonochromaticTrayIconSetting /> - </SettingsGroup> - - <SettingsGroup> - <LanguageButton /> - </SettingsGroup> - - {(window.env.platform === 'win32' || - (window.env.platform === 'darwin' && window.env.development)) && ( - <SettingsGroup> - <UnpinnedWindowSetting /> - </SettingsGroup> - )} - - {unpinnedWindow && ( - <SettingsGroup> - <StartMinimizedSetting /> - </SettingsGroup> - )} - - <StyledAnimateMapCellGroup> - <AnimateMapSetting /> - </StyledAnimateMapCellGroup> - </SettingsStack> - </SettingsContent> - </NavigationScrollbars> - </NavigationContainer> - </SettingsContainer> - </Layout> - </BackAction> - ); -} - -function NotificationsSetting() { - const enableSystemNotifications = useSelector( - (state) => state.settings.guiSettings.enableSystemNotifications, - ); - const { setEnableSystemNotifications } = useAppContext(); - - return ( - <AriaInputGroup> - <Cell.Container> - <AriaLabel> - <Cell.InputLabel> - {messages.pgettext('user-interface-settings-view', 'Notifications')} - </Cell.InputLabel> - </AriaLabel> - <AriaInput> - <Cell.Switch isOn={enableSystemNotifications} onChange={setEnableSystemNotifications} /> - </AriaInput> - </Cell.Container> - <Cell.CellFooter> - <AriaDescription> - <Cell.CellFooterText> - {messages.pgettext( - 'user-interface-settings-view', - 'Enable or disable system notifications. The critical notifications will always be displayed.', - )} - </Cell.CellFooterText> - </AriaDescription> - </Cell.CellFooter> - </AriaInputGroup> - ); -} - -function MonochromaticTrayIconSetting() { - const monochromaticIcon = useSelector((state) => state.settings.guiSettings.monochromaticIcon); - const { setMonochromaticIcon } = useAppContext(); - - return ( - <AriaInputGroup> - <Cell.Container> - <AriaLabel> - <Cell.InputLabel> - {messages.pgettext('user-interface-settings-view', 'Monochromatic tray icon')} - </Cell.InputLabel> - </AriaLabel> - <AriaInput> - <Cell.Switch isOn={monochromaticIcon} onChange={setMonochromaticIcon} /> - </AriaInput> - </Cell.Container> - <Cell.CellFooter> - <AriaDescription> - <Cell.CellFooterText> - {messages.pgettext( - 'user-interface-settings-view', - 'Use a monochromatic tray icon instead of a colored one.', - )} - </Cell.CellFooterText> - </AriaDescription> - </Cell.CellFooter> - </AriaInputGroup> - ); -} - -function UnpinnedWindowSetting() { - const unpinnedWindow = useSelector((state) => state.settings.guiSettings.unpinnedWindow); - const { setUnpinnedWindow } = useAppContext(); - - return ( - <AriaInputGroup> - <Cell.Container> - <AriaLabel> - <Cell.InputLabel> - {messages.pgettext('user-interface-settings-view', 'Unpin app from taskbar')} - </Cell.InputLabel> - </AriaLabel> - <AriaInput> - <Cell.Switch isOn={unpinnedWindow} onChange={setUnpinnedWindow} /> - </AriaInput> - </Cell.Container> - <Cell.CellFooter> - <AriaDescription> - <Cell.CellFooterText> - {messages.pgettext( - 'user-interface-settings-view', - 'Enable to move the app around as a free-standing window.', - )} - </Cell.CellFooterText> - </AriaDescription> - </Cell.CellFooter> - </AriaInputGroup> - ); -} - -function StartMinimizedSetting() { - const startMinimized = useSelector((state) => state.settings.guiSettings.startMinimized); - const { setStartMinimized } = useAppContext(); - - return ( - <AriaInputGroup> - <Cell.Container> - <AriaLabel> - <Cell.InputLabel> - {messages.pgettext('user-interface-settings-view', 'Start minimized')} - </Cell.InputLabel> - </AriaLabel> - <AriaInput> - <Cell.Switch isOn={startMinimized} onChange={setStartMinimized} /> - </AriaInput> - </Cell.Container> - <Cell.CellFooter> - <AriaDescription> - <Cell.CellFooterText> - {messages.pgettext( - 'user-interface-settings-view', - 'Show only the tray icon when the app starts.', - )} - </Cell.CellFooterText> - </AriaDescription> - </Cell.CellFooter> - </AriaInputGroup> - ); -} - -function AnimateMapSetting() { - const animateMap = useSelector((state) => state.settings.guiSettings.animateMap); - const { setAnimateMap } = useAppContext(); - - return ( - <AriaInputGroup> - <Cell.Container> - <AriaLabel> - <Cell.InputLabel> - {messages.pgettext('user-interface-settings-view', 'Animate map')} - </Cell.InputLabel> - </AriaLabel> - <AriaInput> - <Cell.Switch isOn={animateMap} onChange={setAnimateMap} /> - </AriaInput> - </Cell.Container> - <Cell.CellFooter> - <AriaDescription> - <Cell.CellFooterText> - {messages.pgettext('user-interface-settings-view', 'Animate map movements.')} - </Cell.CellFooterText> - </AriaDescription> - </Cell.CellFooter> - </AriaInputGroup> - ); -} - -function LanguageButton() { - const { getPreferredLocaleDisplayName } = useAppContext(); - const preferredLocale = useSelector((state) => state.settings.guiSettings.preferredLocale); - const localeDisplayName = getPreferredLocaleDisplayName(preferredLocale); - - return ( - <SettingsNavigationListItem to={RoutePath.selectLanguage}> - <SettingsNavigationListItem.Group> - <Image source="icon-language" /> - <SettingsNavigationListItem.Label> - { - // TRANSLATORS: Navigation button to the 'Language' settings view - messages.pgettext('user-interface-settings-view', 'Language') - } - </SettingsNavigationListItem.Label> - </SettingsNavigationListItem.Group> - <SettingsNavigationListItem.Group> - <SettingsNavigationListItem.Text>{localeDisplayName}</SettingsNavigationListItem.Text> - <SettingsNavigationListItem.Icon icon="chevron-right" /> - </SettingsNavigationListItem.Group> - </SettingsNavigationListItem> - ); -} diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/views/app-info/components/beta-list-item/BetaListItem.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/views/app-info/components/beta-list-item/BetaListItem.tsx index 9dbbb0d566..d17c947b67 100644 --- a/desktop/packages/mullvad-vpn/src/renderer/components/views/app-info/components/beta-list-item/BetaListItem.tsx +++ b/desktop/packages/mullvad-vpn/src/renderer/components/views/app-info/components/beta-list-item/BetaListItem.tsx @@ -1,51 +1,36 @@ -import React from 'react'; - import { messages } from '../../../../../../shared/gettext'; -import { ListItem } from '../../../../../lib/components/list-item'; import { useSettingsShowBetaReleases, useVersionIsBeta } from '../../../../../redux/hooks'; -import Switch from '../../../../Switch'; +import { SettingsToggleListItem } from '../../../../settings-toggle-list-item'; export function BetaListItem() { const { isBeta } = useVersionIsBeta(); const { showBetaReleases, setShowBetaReleases } = useSettingsShowBetaReleases(); - const switchId = React.useId(); - const labelId = React.useId(); - const descriptionId = React.useId(); return ( - <ListItem disabled={isBeta}> - <ListItem.Item> - <ListItem.Content> - <ListItem.Label id={labelId} as="label" htmlFor={switchId}> - { - // TRANSLATORS: Label for switch to toggle beta program. - messages.pgettext('app-info-view', 'Beta program') - } - </ListItem.Label> - <Switch - id={switchId} - aria-labelledby={labelId} - aria-describedby={descriptionId} - isOn={showBetaReleases} - onChange={setShowBetaReleases} - /> - </ListItem.Content> - </ListItem.Item> - <ListItem.Footer> - <ListItem.Text id={descriptionId}> - {isBeta - ? // TRANSLATORS: Description for beta program switch when using a beta version. - messages.pgettext( - 'app-info-view', - 'This option is unavailable while using a beta version.', - ) - : // TRANSLATORS: Description for beta program switch. - messages.pgettext( - 'app-info-view', - 'Enable to get notified when new beta versions of the app are released.', - )} - </ListItem.Text> - </ListItem.Footer> - </ListItem> + <SettingsToggleListItem + checked={showBetaReleases} + onCheckedChange={setShowBetaReleases} + disabled={isBeta} + description={ + isBeta + ? // TRANSLATORS: Description for beta program switch when using a beta version. + messages.pgettext( + 'app-info-view', + 'This option is unavailable while using a beta version.', + ) + : // TRANSLATORS: Description for beta program switch. + messages.pgettext( + 'app-info-view', + 'Enable to get notified when new beta versions of the app are released.', + ) + }> + <SettingsToggleListItem.Label> + { + // TRANSLATORS: Label for switch to toggle beta program. + messages.pgettext('app-info-view', 'Beta program') + } + </SettingsToggleListItem.Label> + <SettingsToggleListItem.Switch /> + </SettingsToggleListItem> ); } diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/views/index.ts b/desktop/packages/mullvad-vpn/src/renderer/components/views/index.ts index dbd592ca0f..d6141c4dda 100644 --- a/desktop/packages/mullvad-vpn/src/renderer/components/views/index.ts +++ b/desktop/packages/mullvad-vpn/src/renderer/components/views/index.ts @@ -10,9 +10,11 @@ export * from './login'; export * from './open-vpn-settings'; export * from './changelog'; export * from './settings'; +export * from './select-language'; export * from './shadowsocks-settings'; export * from './split-tunneling'; export * from './too-many-devices'; export * from './udp-over-tcp-settings'; export * from './vpn-settings'; +export * from './user-interface-settings'; export * from './wireguard-settings'; diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/SelectLanguage.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/views/select-language/SelectLanguage.tsx index 91ee78429a..79eda22302 100644 --- a/desktop/packages/mullvad-vpn/src/renderer/components/SelectLanguage.tsx +++ b/desktop/packages/mullvad-vpn/src/renderer/components/views/select-language/SelectLanguage.tsx @@ -1,25 +1,20 @@ import { useCallback, useEffect, useMemo, useRef } from 'react'; -import styled from 'styled-components'; -import { useAppContext } from '../../renderer/context'; -import { messages } from '../../shared/gettext'; -import { useHistory } from '../lib/history'; -import { useSelector } from '../redux/store'; -import { AppNavigationHeader } from './'; -import { AriaInputGroup } from './AriaGroup'; -import Selector, { SelectorItem } from './cell/Selector'; -import { CustomScrollbarsRef } from './CustomScrollbars'; -import { BackAction } from './KeyboardNavigation'; -import { Layout, SettingsContainer } from './Layout'; -import { NavigationContainer } from './NavigationContainer'; -import { NavigationScrollbars } from './NavigationScrollbars'; -import SettingsHeader, { HeaderTitle } from './SettingsHeader'; +import { messages } from '../../../../shared/gettext'; +import { useAppContext } from '../../../context'; +import { Listbox } from '../../../lib/components/listbox'; +import { useHistory } from '../../../lib/history'; +import { useSelector } from '../../../redux/store'; +import { AppNavigationHeader } from '../..'; +import { SelectorItem } from '../../cell/Selector'; +import { CustomScrollbarsRef } from '../../CustomScrollbars'; +import { BackAction } from '../../KeyboardNavigation'; +import { Layout, SettingsContainer } from '../../Layout'; +import { NavigationContainer } from '../../NavigationContainer'; +import { NavigationScrollbars } from '../../NavigationScrollbars'; +import SettingsHeader, { HeaderTitle } from '../../SettingsHeader'; -const StyledSelector = styled(Selector)({ - marginBottom: 0, -}) as typeof Selector; - -export default function SelectLanguage() { +export function SelectLanguageView() { const { pop } = useHistory(); const { preferredLocale, preferredLocalesList, setPreferredLocale } = usePreferredLocale(); const scrollView = useRef<CustomScrollbarsRef>(null); @@ -65,15 +60,24 @@ export default function SelectLanguage() { {messages.pgettext('select-language-nav', 'Select language')} </HeaderTitle> </SettingsHeader> - <AriaInputGroup> - <StyledSelector - title="" - value={preferredLocale} - items={preferredLocalesList} - onSelect={selectLocale} - selectedCellRef={selectedCellRef} - /> - </AriaInputGroup> + <Listbox value={preferredLocale} onValueChange={selectLocale}> + <Listbox.Options> + {preferredLocalesList.map((locale) => ( + <Listbox.Option key={locale.value} level={1} value={locale.value}> + <Listbox.Option.Trigger> + <Listbox.Option.Item> + <Listbox.Option.Content> + <Listbox.Option.Group> + <Listbox.Option.Icon icon="checkmark" /> + <Listbox.Option.Label>{locale.label}</Listbox.Option.Label> + </Listbox.Option.Group> + </Listbox.Option.Content> + </Listbox.Option.Item> + </Listbox.Option.Trigger> + </Listbox.Option> + ))} + </Listbox.Options> + </Listbox> </NavigationScrollbars> </NavigationContainer> </SettingsContainer> diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/views/select-language/index.ts b/desktop/packages/mullvad-vpn/src/renderer/components/views/select-language/index.ts new file mode 100644 index 0000000000..f08f8e9bcb --- /dev/null +++ b/desktop/packages/mullvad-vpn/src/renderer/components/views/select-language/index.ts @@ -0,0 +1 @@ +export * from './SelectLanguage'; diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/views/user-interface-settings/UserInterfaceSettingsView.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/views/user-interface-settings/UserInterfaceSettingsView.tsx new file mode 100644 index 0000000000..1bc6313c2f --- /dev/null +++ b/desktop/packages/mullvad-vpn/src/renderer/components/views/user-interface-settings/UserInterfaceSettingsView.tsx @@ -0,0 +1,93 @@ +import styled from 'styled-components'; + +import { messages } from '../../../../shared/gettext'; +import { useHistory } from '../../../lib/history'; +import { useSelector } from '../../../redux/store'; +import { AppNavigationHeader } from '../..'; +import { BackAction } from '../../KeyboardNavigation'; +import { + Layout, + SettingsContainer, + SettingsContent, + SettingsGroup, + SettingsStack, +} from '../../Layout'; +import { NavigationContainer } from '../../NavigationContainer'; +import { NavigationScrollbars } from '../../NavigationScrollbars'; +import SettingsHeader, { HeaderTitle } from '../../SettingsHeader'; +import { + AnimateMapSetting, + LanguageListItem, + MonochromaticTrayIconSetting, + NotificationsSetting, + StartMinimizedSetting, + UnpinnedWindowSetting, +} from './components'; + +const StyledAnimateMapCellGroup = styled(SettingsGroup)({ + '@media (prefers-reduced-motion: reduce)': { + display: 'none', + }, +}); + +export function UserInterfaceSettingsView() { + const { pop } = useHistory(); + const unpinnedWindow = useSelector((state) => state.settings.guiSettings.unpinnedWindow); + + return ( + <BackAction action={pop}> + <Layout> + <SettingsContainer> + <NavigationContainer> + <AppNavigationHeader + title={ + // TRANSLATORS: Title label in navigation bar + messages.pgettext('user-interface-settings-view', 'User interface settings') + } + /> + + <NavigationScrollbars> + <SettingsHeader> + <HeaderTitle> + {messages.pgettext('user-interface-settings-view', 'User interface settings')} + </HeaderTitle> + </SettingsHeader> + + <SettingsContent> + <SettingsStack> + <SettingsGroup> + <NotificationsSetting /> + </SettingsGroup> + <SettingsGroup> + <MonochromaticTrayIconSetting /> + </SettingsGroup> + + <SettingsGroup> + <LanguageListItem /> + </SettingsGroup> + + {(window.env.platform === 'win32' || + (window.env.platform === 'darwin' && window.env.development)) && ( + <SettingsGroup> + <UnpinnedWindowSetting /> + </SettingsGroup> + )} + + {unpinnedWindow && ( + <SettingsGroup> + <StartMinimizedSetting /> + </SettingsGroup> + )} + + <StyledAnimateMapCellGroup> + <AnimateMapSetting /> + </StyledAnimateMapCellGroup> + </SettingsStack> + </SettingsContent> + </NavigationScrollbars> + </NavigationContainer> + </SettingsContainer> + </Layout> + </BackAction> + ); +} diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/views/user-interface-settings/components/animate-map-setting/AnimateMapSetting.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/views/user-interface-settings/components/animate-map-setting/AnimateMapSetting.tsx new file mode 100644 index 0000000000..ed62e4ad15 --- /dev/null +++ b/desktop/packages/mullvad-vpn/src/renderer/components/views/user-interface-settings/components/animate-map-setting/AnimateMapSetting.tsx @@ -0,0 +1,21 @@ +import { messages } from '../../../../../../shared/gettext'; +import { useAppContext } from '../../../../../context'; +import { useSelector } from '../../../../../redux/store'; +import { SettingsToggleListItem } from '../../../../settings-toggle-list-item'; + +export function AnimateMapSetting() { + const animateMap = useSelector((state) => state.settings.guiSettings.animateMap); + const { setAnimateMap } = useAppContext(); + + return ( + <SettingsToggleListItem + checked={animateMap} + onCheckedChange={setAnimateMap} + description={messages.pgettext('user-interface-settings-view', 'Animate map movements.')}> + <SettingsToggleListItem.Label> + {messages.pgettext('user-interface-settings-view', 'Animate map')} + </SettingsToggleListItem.Label> + <SettingsToggleListItem.Switch /> + </SettingsToggleListItem> + ); +} diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/views/user-interface-settings/components/animate-map-setting/index.ts b/desktop/packages/mullvad-vpn/src/renderer/components/views/user-interface-settings/components/animate-map-setting/index.ts new file mode 100644 index 0000000000..0bd2c4cffc --- /dev/null +++ b/desktop/packages/mullvad-vpn/src/renderer/components/views/user-interface-settings/components/animate-map-setting/index.ts @@ -0,0 +1 @@ +export * from './AnimateMapSetting'; diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/views/user-interface-settings/components/index.ts b/desktop/packages/mullvad-vpn/src/renderer/components/views/user-interface-settings/components/index.ts new file mode 100644 index 0000000000..742d9be904 --- /dev/null +++ b/desktop/packages/mullvad-vpn/src/renderer/components/views/user-interface-settings/components/index.ts @@ -0,0 +1,6 @@ +export * from './animate-map-setting'; +export * from './language-list-item'; +export * from './monochromatic-tray-icon-setting'; +export * from './notifications-setting'; +export * from './start-minimized-setting'; +export * from './unpinned-window-setting'; diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/views/user-interface-settings/components/language-list-item/LanguageListItem.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/views/user-interface-settings/components/language-list-item/LanguageListItem.tsx new file mode 100644 index 0000000000..e28b6733a9 --- /dev/null +++ b/desktop/packages/mullvad-vpn/src/renderer/components/views/user-interface-settings/components/language-list-item/LanguageListItem.tsx @@ -0,0 +1,30 @@ +import { messages } from '../../../../../../shared/gettext'; +import { RoutePath } from '../../../../../../shared/routes'; +import { useAppContext } from '../../../../../context'; +import { Image } from '../../../../../lib/components'; +import { useSelector } from '../../../../../redux/store'; +import { SettingsNavigationListItem } from '../../../../settings-navigation-list-item'; + +export function LanguageListItem() { + const { getPreferredLocaleDisplayName } = useAppContext(); + const preferredLocale = useSelector((state) => state.settings.guiSettings.preferredLocale); + const localeDisplayName = getPreferredLocaleDisplayName(preferredLocale); + + return ( + <SettingsNavigationListItem to={RoutePath.selectLanguage}> + <SettingsNavigationListItem.Group> + <Image source="icon-language" /> + <SettingsNavigationListItem.Label> + { + // TRANSLATORS: Navigation button to the 'Language' settings view + messages.pgettext('user-interface-settings-view', 'Language') + } + </SettingsNavigationListItem.Label> + </SettingsNavigationListItem.Group> + <SettingsNavigationListItem.Group> + <SettingsNavigationListItem.Text>{localeDisplayName}</SettingsNavigationListItem.Text> + <SettingsNavigationListItem.Icon icon="chevron-right" /> + </SettingsNavigationListItem.Group> + </SettingsNavigationListItem> + ); +} diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/views/user-interface-settings/components/language-list-item/index.ts b/desktop/packages/mullvad-vpn/src/renderer/components/views/user-interface-settings/components/language-list-item/index.ts new file mode 100644 index 0000000000..9a6128b43d --- /dev/null +++ b/desktop/packages/mullvad-vpn/src/renderer/components/views/user-interface-settings/components/language-list-item/index.ts @@ -0,0 +1 @@ +export * from './LanguageListItem'; diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/views/user-interface-settings/components/monochromatic-tray-icon-setting/MonochromaticTrayIconSetting.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/views/user-interface-settings/components/monochromatic-tray-icon-setting/MonochromaticTrayIconSetting.tsx new file mode 100644 index 0000000000..7187105c6c --- /dev/null +++ b/desktop/packages/mullvad-vpn/src/renderer/components/views/user-interface-settings/components/monochromatic-tray-icon-setting/MonochromaticTrayIconSetting.tsx @@ -0,0 +1,24 @@ +import { messages } from '../../../../../../shared/gettext'; +import { useAppContext } from '../../../../../context'; +import { useSelector } from '../../../../../redux/store'; +import { SettingsToggleListItem } from '../../../../settings-toggle-list-item'; + +export function MonochromaticTrayIconSetting() { + const monochromaticIcon = useSelector((state) => state.settings.guiSettings.monochromaticIcon); + const { setMonochromaticIcon } = useAppContext(); + + return ( + <SettingsToggleListItem + checked={monochromaticIcon} + onCheckedChange={setMonochromaticIcon} + description={messages.pgettext( + 'user-interface-settings-view', + 'Use a monochromatic tray icon instead of a colored one.', + )}> + <SettingsToggleListItem.Label> + {messages.pgettext('user-interface-settings-view', 'Monochromatic tray icon')} + </SettingsToggleListItem.Label> + <SettingsToggleListItem.Switch /> + </SettingsToggleListItem> + ); +} diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/views/user-interface-settings/components/monochromatic-tray-icon-setting/index.ts b/desktop/packages/mullvad-vpn/src/renderer/components/views/user-interface-settings/components/monochromatic-tray-icon-setting/index.ts new file mode 100644 index 0000000000..38e9f4ae91 --- /dev/null +++ b/desktop/packages/mullvad-vpn/src/renderer/components/views/user-interface-settings/components/monochromatic-tray-icon-setting/index.ts @@ -0,0 +1 @@ +export * from './MonochromaticTrayIconSetting'; diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/views/user-interface-settings/components/notifications-setting/NotificationsSetting.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/views/user-interface-settings/components/notifications-setting/NotificationsSetting.tsx new file mode 100644 index 0000000000..d9445939ae --- /dev/null +++ b/desktop/packages/mullvad-vpn/src/renderer/components/views/user-interface-settings/components/notifications-setting/NotificationsSetting.tsx @@ -0,0 +1,26 @@ +import { messages } from '../../../../../../shared/gettext'; +import { useAppContext } from '../../../../../context'; +import { useSelector } from '../../../../../redux/store'; +import { SettingsToggleListItem } from '../../../../settings-toggle-list-item'; + +export function NotificationsSetting() { + const enableSystemNotifications = useSelector( + (state) => state.settings.guiSettings.enableSystemNotifications, + ); + const { setEnableSystemNotifications } = useAppContext(); + + return ( + <SettingsToggleListItem + checked={enableSystemNotifications} + onCheckedChange={setEnableSystemNotifications} + description={messages.pgettext( + 'user-interface-settings-view', + 'Enable or disable system notifications. The critical notifications will always be displayed.', + )}> + <SettingsToggleListItem.Label> + {messages.pgettext('user-interface-settings-view', 'Notifications')} + </SettingsToggleListItem.Label> + <SettingsToggleListItem.Switch /> + </SettingsToggleListItem> + ); +} diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/views/user-interface-settings/components/notifications-setting/index.ts b/desktop/packages/mullvad-vpn/src/renderer/components/views/user-interface-settings/components/notifications-setting/index.ts new file mode 100644 index 0000000000..2f05ed81ca --- /dev/null +++ b/desktop/packages/mullvad-vpn/src/renderer/components/views/user-interface-settings/components/notifications-setting/index.ts @@ -0,0 +1 @@ +export * from './NotificationsSetting'; diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/views/user-interface-settings/components/start-minimized-setting/StartMinimizedSetting.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/views/user-interface-settings/components/start-minimized-setting/StartMinimizedSetting.tsx new file mode 100644 index 0000000000..19418f9ff0 --- /dev/null +++ b/desktop/packages/mullvad-vpn/src/renderer/components/views/user-interface-settings/components/start-minimized-setting/StartMinimizedSetting.tsx @@ -0,0 +1,24 @@ +import { messages } from '../../../../../../shared/gettext'; +import { useAppContext } from '../../../../../context'; +import { useSelector } from '../../../../../redux/store'; +import { SettingsToggleListItem } from '../../../../settings-toggle-list-item'; + +export function StartMinimizedSetting() { + const startMinimized = useSelector((state) => state.settings.guiSettings.startMinimized); + const { setStartMinimized } = useAppContext(); + + return ( + <SettingsToggleListItem + checked={startMinimized} + onCheckedChange={setStartMinimized} + description={messages.pgettext( + 'user-interface-settings-view', + 'Show only the tray icon when the app starts.', + )}> + <SettingsToggleListItem.Label> + {messages.pgettext('user-interface-settings-view', 'Start minimized')} + </SettingsToggleListItem.Label> + <SettingsToggleListItem.Switch /> + </SettingsToggleListItem> + ); +} diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/views/user-interface-settings/components/start-minimized-setting/index.ts b/desktop/packages/mullvad-vpn/src/renderer/components/views/user-interface-settings/components/start-minimized-setting/index.ts new file mode 100644 index 0000000000..39e6bdb574 --- /dev/null +++ b/desktop/packages/mullvad-vpn/src/renderer/components/views/user-interface-settings/components/start-minimized-setting/index.ts @@ -0,0 +1 @@ +export * from './StartMinimizedSetting'; diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/views/user-interface-settings/components/unpinned-window-setting/UnpinnedWindowSetting.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/views/user-interface-settings/components/unpinned-window-setting/UnpinnedWindowSetting.tsx new file mode 100644 index 0000000000..dc370582ce --- /dev/null +++ b/desktop/packages/mullvad-vpn/src/renderer/components/views/user-interface-settings/components/unpinned-window-setting/UnpinnedWindowSetting.tsx @@ -0,0 +1,24 @@ +import { messages } from '../../../../../../shared/gettext'; +import { useAppContext } from '../../../../../context'; +import { useSelector } from '../../../../../redux/store'; +import { SettingsToggleListItem } from '../../../../settings-toggle-list-item'; + +export function UnpinnedWindowSetting() { + const unpinnedWindow = useSelector((state) => state.settings.guiSettings.unpinnedWindow); + const { setUnpinnedWindow } = useAppContext(); + + return ( + <SettingsToggleListItem + checked={unpinnedWindow} + onCheckedChange={setUnpinnedWindow} + description={messages.pgettext( + 'user-interface-settings-view', + 'Enable to move the app around as a free-standing window.', + )}> + <SettingsToggleListItem.Label> + {messages.pgettext('user-interface-settings-view', 'Unpin app from taskbar')} + </SettingsToggleListItem.Label> + <SettingsToggleListItem.Switch /> + </SettingsToggleListItem> + ); +} diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/views/user-interface-settings/components/unpinned-window-setting/index.ts b/desktop/packages/mullvad-vpn/src/renderer/components/views/user-interface-settings/components/unpinned-window-setting/index.ts new file mode 100644 index 0000000000..8f3f823fb1 --- /dev/null +++ b/desktop/packages/mullvad-vpn/src/renderer/components/views/user-interface-settings/components/unpinned-window-setting/index.ts @@ -0,0 +1 @@ +export * from './UnpinnedWindowSetting'; diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/views/user-interface-settings/index.ts b/desktop/packages/mullvad-vpn/src/renderer/components/views/user-interface-settings/index.ts new file mode 100644 index 0000000000..80c9019ce3 --- /dev/null +++ b/desktop/packages/mullvad-vpn/src/renderer/components/views/user-interface-settings/index.ts @@ -0,0 +1 @@ +export * from './UserInterfaceSettingsView'; diff --git a/desktop/packages/mullvad-vpn/test/e2e/mocked/forced-motion.spec.ts b/desktop/packages/mullvad-vpn/test/e2e/mocked/forced-motion.spec.ts index 103f61653b..2307483b2b 100644 --- a/desktop/packages/mullvad-vpn/test/e2e/mocked/forced-motion.spec.ts +++ b/desktop/packages/mullvad-vpn/test/e2e/mocked/forced-motion.spec.ts @@ -13,8 +13,8 @@ test.describe('Transitions and animations', () => { test.beforeAll(async () => { ({ page, util } = await startMockedApp()); - await page.emulateMedia({ reducedMotion: null }); routes = new RoutesObjectModel(page, util); + await util.setReducedMotion('no-preference'); await routes.main.waitForRoute(); }); diff --git a/desktop/packages/mullvad-vpn/test/e2e/mocked/user-interface-settings/user-interface-settings.spec.ts b/desktop/packages/mullvad-vpn/test/e2e/mocked/user-interface-settings/user-interface-settings.spec.ts index b7c571507b..4e1686d097 100644 --- a/desktop/packages/mullvad-vpn/test/e2e/mocked/user-interface-settings/user-interface-settings.spec.ts +++ b/desktop/packages/mullvad-vpn/test/e2e/mocked/user-interface-settings/user-interface-settings.spec.ts @@ -1,6 +1,7 @@ import { expect, test } from '@playwright/test'; import { Page } from 'playwright'; +import { IGuiSettingsState } from '../../../../src/shared/gui-settings-state'; import { RoutesObjectModel } from '../../route-object-models'; import { MockedTestUtils, startMockedApp } from '../mocked-utils'; @@ -19,6 +20,26 @@ test.describe('User interface settings', () => { await routes.settings.gotoUserInterfaceSettings(); }; + const setGuiSettings = async (state: Partial<IGuiSettingsState> = {}) => { + const baseState: IGuiSettingsState = { + enableSystemNotifications: false, + monochromaticIcon: false, + startMinimized: false, + animateMap: false, + autoConnect: false, + unpinnedWindow: false, + browsedForSplitTunnelingApplications: [], + changelogDisplayedForVersion: '', + preferredLocale: 'en', + updateDismissedForVersion: '', + }; + + await util.ipc.guiSettings[''].notify({ + ...baseState, + ...state, + }); + }; + test.beforeAll(async () => { await startup(); }); @@ -27,6 +48,117 @@ test.describe('User interface settings', () => { await util?.closePage(); }); + test.beforeEach(async () => { + await setGuiSettings(); + }); + + test.describe('Notification settings', () => { + test('Should toggle notification setting', async () => { + const autoStartSwitch = routes.userInterfaceSettings.selectors.autoStartSwitch(); + await expect(autoStartSwitch).toBeVisible(); + await expect(autoStartSwitch).not.toBeChecked(); + + await Promise.all([ + autoStartSwitch.click(), + util.ipc.guiSettings.setEnableSystemNotifications.expect(), + ]); + + await setGuiSettings({ enableSystemNotifications: true }); + await expect(autoStartSwitch).toBeChecked(); + }); + }); + + test.describe('Monochromatic tray icon settings', () => { + test('Should toggle monochromatic tray icon setting', async () => { + const monochromaticTrayIconSwitch = + routes.userInterfaceSettings.selectors.monochromaticTrayIconSwitch(); + await expect(monochromaticTrayIconSwitch).toBeVisible(); + await expect(monochromaticTrayIconSwitch).not.toBeChecked(); + + await Promise.all([ + monochromaticTrayIconSwitch.click(), + util.ipc.guiSettings.setMonochromaticIcon.expect(), + ]); + + await setGuiSettings({ monochromaticIcon: true }); + await expect(monochromaticTrayIconSwitch).toBeChecked(); + }); + }); + + test.describe('Unpinned window setting', () => { + test.skip(() => process.platform !== 'win32'); + + test('Should toggle unpinned window setting', async () => { + const unpinnedWindowSwitch = routes.userInterfaceSettings.selectors.unpinnedWindowSwitch(); + await expect(unpinnedWindowSwitch).toBeVisible(); + await expect(unpinnedWindowSwitch).not.toBeChecked(); + + await Promise.all([ + unpinnedWindowSwitch.click(), + util.ipc.guiSettings.setUnpinnedWindow.expect(), + ]); + + await setGuiSettings({ unpinnedWindow: true }); + await expect(unpinnedWindowSwitch).toBeChecked(); + }); + }); + + test.describe('Start minimized setting', () => { + test.skip(() => process.platform !== 'win32'); + + test('Should toggle start minimized setting', async () => { + await setGuiSettings({ unpinnedWindow: true }); + + const startMinimizedSwitch = routes.userInterfaceSettings.selectors.startMinimizedSwitch(); + await expect(startMinimizedSwitch).toBeVisible(); + await expect(startMinimizedSwitch).not.toBeChecked(); + + await Promise.all([ + startMinimizedSwitch.click(), + util.ipc.guiSettings.setStartMinimized.expect(), + ]); + + await setGuiSettings({ unpinnedWindow: true, startMinimized: true }); + await expect(startMinimizedSwitch).toBeChecked(); + }); + }); + + test.describe('Animate map setting', () => { + test.describe('With reduced motion', () => { + test.beforeEach(async () => { + await util.setReducedMotion('reduce'); + }); + + test('Should not display animate map setting', async () => { + const animateMapSwitch = routes.userInterfaceSettings.selectors.animateMapSwitch(); + await expect(animateMapSwitch).not.toBeVisible(); + }); + }); + + test.describe('Without reduced motion', () => { + test.beforeEach(async () => { + await util.setReducedMotion('no-preference'); + }); + + test('Should display animate map setting', async () => { + const animateMapSwitch = routes.userInterfaceSettings.selectors.animateMapSwitch(); + + await expect(animateMapSwitch).toBeVisible(); + }); + + test('Should toggle animate map setting', async () => { + const animateMapSwitch = routes.userInterfaceSettings.selectors.animateMapSwitch(); + await expect(animateMapSwitch).toBeVisible(); + await expect(animateMapSwitch).not.toBeChecked(); + + await Promise.all([animateMapSwitch.click(), util.ipc.guiSettings.setAnimateMap.expect()]); + + await setGuiSettings({ animateMap: true }); + await expect(animateMapSwitch).toBeChecked(); + }); + }); + }); + test.describe('Select language', () => { ['Svenska', 'Deutsch', 'English', 'System default'].forEach((language) => { test(`Should change language to ${language}`, async () => { diff --git a/desktop/packages/mullvad-vpn/test/e2e/route-object-models/select-language/selectors.ts b/desktop/packages/mullvad-vpn/test/e2e/route-object-models/select-language/selectors.ts index eb9e86f36a..b4dbe6fa5e 100644 --- a/desktop/packages/mullvad-vpn/test/e2e/route-object-models/select-language/selectors.ts +++ b/desktop/packages/mullvad-vpn/test/e2e/route-object-models/select-language/selectors.ts @@ -1,8 +1,5 @@ import { Page } from 'playwright'; export const createSelectors = (page: Page) => ({ - languageOption: (language: string) => - page.locator('button', { - has: page.locator('div', { hasText: language }), - }), + languageOption: (language: string) => page.getByRole('option', { name: language }), }); diff --git a/desktop/packages/mullvad-vpn/test/e2e/route-object-models/user-interface-settings/selectors.ts b/desktop/packages/mullvad-vpn/test/e2e/route-object-models/user-interface-settings/selectors.ts index d718d188dd..52bfdf6d8e 100644 --- a/desktop/packages/mullvad-vpn/test/e2e/route-object-models/user-interface-settings/selectors.ts +++ b/desktop/packages/mullvad-vpn/test/e2e/route-object-models/user-interface-settings/selectors.ts @@ -9,4 +9,9 @@ export const createSelectors = (page: Page) => ({ page.locator('button', { hasText: label, }), + autoStartSwitch: () => page.getByRole('switch', { name: 'Notifications' }), + monochromaticTrayIconSwitch: () => page.getByRole('switch', { name: 'Monochromatic tray icon' }), + unpinnedWindowSwitch: () => page.getByRole('switch', { name: 'Unpin app from taskbar' }), + startMinimizedSwitch: () => page.getByRole('switch', { name: 'Start minimized' }), + animateMapSwitch: () => page.getByRole('switch', { name: 'Animate map' }), }); diff --git a/desktop/packages/mullvad-vpn/test/e2e/utils.ts b/desktop/packages/mullvad-vpn/test/e2e/utils.ts index ff851bd2bd..89cfebe4f2 100644 --- a/desktop/packages/mullvad-vpn/test/e2e/utils.ts +++ b/desktop/packages/mullvad-vpn/test/e2e/utils.ts @@ -19,16 +19,19 @@ export interface TestUtils { getCurrentRoute: () => Promise<string | null>; expectRoute: (route: string) => Promise<void>; expectRouteChange: (trigger: TriggerFn) => Promise<void>; + setReducedMotion: (value: ReducedMotionValue) => Promise<void>; } type LaunchOptions = NonNullable<Parameters<typeof electron.launch>[0]>; +type ReducedMotionValue = 'no-preference' | 'reduce'; + export const startApp = async (options: LaunchOptions): Promise<StartAppResponse> => { const app = await launch(options); const page = await app.firstWindow(); if (!forceMotion) { - await page.emulateMedia({ reducedMotion: 'reduce' }); + await setReducedMotion(page, 'reduce'); } await promiseTimeout(page.waitForEvent('load')); @@ -41,6 +44,7 @@ export const startApp = async (options: LaunchOptions): Promise<StartAppResponse getCurrentRoute: () => getCurrentRoute(page), expectRoute: (route: string) => expectRoute(page, route), expectRouteChange: (trigger: TriggerFn) => expectRouteChange(page, trigger), + setReducedMotion: (value: ReducedMotionValue) => setReducedMotion(page, value), }; return { app, page, util }; @@ -83,6 +87,13 @@ async function expectRouteChange(page: Page, trigger: TriggerFn) { await expect.poll(() => getCurrentRoute(page)).not.toMatchPath(initialRoute); } +async function setReducedMotion(page: Page, value: ReducedMotionValue) { + await page.emulateMedia({ reducedMotion: value }); + + const query = `(prefers-reduced-motion: ${value})`; + await page.evaluate((q) => window.matchMedia(q).matches, query); +} + const getStyleProperty = (locator: Locator, property: string) => { return locator.evaluate( (el, { property }) => { |
