summaryrefslogtreecommitdiffhomepage
path: root/gui/src
diff options
context:
space:
mode:
authorOskar Nyberg <oskar@mullvad.net>2022-05-03 15:29:05 +0200
committerOskar Nyberg <oskar@mullvad.net>2022-05-19 10:04:19 +0200
commit22efd7779d228c904077bfafb98fc575a5c63438 (patch)
tree81a77a0f0a19dac8dddd999b0cf3ec322c63942a /gui/src
parentb9f8960efbf37ad2a93bedf4125c2f92933f2e87 (diff)
downloadmullvadvpn-22efd7779d228c904077bfafb98fc575a5c63438.tar.xz
mullvadvpn-22efd7779d228c904077bfafb98fc575a5c63438.zip
Refactor filter by provider and add title and accordion
Diffstat (limited to 'gui/src')
-rw-r--r--gui/src/renderer/components/Filter.tsx187
1 files changed, 98 insertions, 89 deletions
diff --git a/gui/src/renderer/components/Filter.tsx b/gui/src/renderer/components/Filter.tsx
index 2f450ef7d4..8a5f763a7c 100644
--- a/gui/src/renderer/components/Filter.tsx
+++ b/gui/src/renderer/components/Filter.tsx
@@ -1,12 +1,15 @@
-import { useCallback, useMemo, useState } from 'react';
+import { useCallback, useState } from 'react';
import styled from 'styled-components';
import { colors } from '../../config.json';
import { messages } from '../../shared/gettext';
import { useAppContext } from '../context';
import { useHistory } from '../lib/history';
-import { useSelector } from '../redux/store';
+import { useBoolean } from '../lib/utilityHooks';
+import { IReduxState, useSelector } from '../redux/store';
+import Accordion from './Accordion';
import * as AppButton from './AppButton';
+import * as Cell from './cell';
import { normalText } from './common-styles';
import ImageView from './ImageView';
import { BackAction } from './KeyboardNavigation';
@@ -34,77 +37,24 @@ const StyledFooter = styled.div({
padding: '18px 22px 22px',
});
-enum Selection {
- all,
- some,
- none,
-}
-
-export default function FilterByProvider() {
+export default function Filter() {
const history = useHistory();
const { updateRelaySettings } = useAppContext();
- const serverList = useSelector((state) =>
- state.settings.relayLocations.concat(
- state.settings.bridgeState === 'on' ? state.settings.bridgeLocations : [],
- ),
- );
- const providerConstraint = useSelector((state) => {
- if ('normal' in state.settings.relaySettings) {
- return state.settings.relaySettings.normal.providers;
- } else {
- return [];
- }
- });
-
- const [providers, setProviders] = useState(() => {
- const providers = serverList.flatMap((country) =>
- country.cities.flatMap((city) => city.relays.map((relay) => relay.provider)),
- );
- const uniqueProviders = removeDuplicates(providers).sort((a, b) => a.localeCompare(b));
-
- return Object.fromEntries(
- uniqueProviders.map((provider) => [
- provider,
- providerConstraint.length === 0 || providerConstraint.includes(provider),
- ]),
- );
- });
-
- const selectionStatus = useMemo(() => {
- if (Object.values(providers).every((value) => value)) {
- return Selection.all;
- } else if (Object.values(providers).every((value) => !value)) {
- return Selection.none;
- } else {
- return Selection.some;
- }
- }, [providers]);
-
- const onCheck = useCallback((provider: string) => {
- setProviders((providers) => ({ ...providers, [provider]: !providers[provider] }));
- }, []);
-
- const toggleAll = useCallback(() => {
- setProviders((providers) =>
- Object.fromEntries(
- Object.keys(providers).map((provider) => [provider, selectionStatus !== Selection.all]),
- ),
- );
- }, [selectionStatus]);
+ const initialProviders = useSelector(providersSelector);
+ const [providers, setProviders] = useState<Record<string, boolean>>(initialProviders);
const onApply = useCallback(async () => {
- const selectedProviders =
- selectionStatus === Selection.all
- ? []
- : Object.entries(providers)
- .filter(([, selected]) => selected)
- .map(([provider]) => provider);
-
+ // If all providers are selected it's represented as an empty array.
+ const selectedProviders = Object.values(providers).every((provider) => provider)
+ ? []
+ : Object.entries(providers)
+ .filter(([, selected]) => selected)
+ .map(([name]) => name);
await updateRelaySettings({ normal: { providers: selectedProviders } });
history.pop();
- }, [providers, history, updateRelaySettings, selectionStatus]);
+ }, [providers, history, updateRelaySettings]);
return (
<BackAction action={history.pop}>
@@ -116,30 +66,18 @@ export default function FilterByProvider() {
<TitleBarItem>
{
// TRANSLATORS: Title label in navigation bar
- messages.pgettext('filter-nav', 'Filter by provider')
+ messages.pgettext('filter-nav', 'Filter')
}
</TitleBarItem>
</NavigationItems>
</NavigationBar>
<StyledNavigationScrollbars>
- <ProviderRow
- provider={messages.pgettext('filter-view', 'All providers')}
- bold
- checked={selectionStatus === Selection.all}
- onCheck={toggleAll}
- />
- {Object.entries(providers).map(([provider, checked]) => (
- <ProviderRow
- key={provider}
- provider={provider}
- checked={checked}
- onCheck={onCheck}
- />
- ))}
+ <FilterByProvider providers={providers} setProviders={setProviders} />
+ <FilterByOwnership />
</StyledNavigationScrollbars>
<StyledFooter>
<AppButton.GreenButton
- disabled={selectionStatus === Selection.none}
+ disabled={Object.values(providers).every((provider) => !provider)}
onClick={onApply}>
{messages.gettext('Apply')}
</AppButton.GreenButton>
@@ -151,6 +89,77 @@ export default function FilterByProvider() {
);
}
+function providersSelector(state: IReduxState): Record<string, boolean> {
+ const providerConstraint =
+ 'normal' in state.settings.relaySettings ? state.settings.relaySettings.normal.providers : [];
+
+ const relays = state.settings.relayLocations.concat(
+ state.settings.bridgeState === 'on' ? state.settings.bridgeLocations : [],
+ );
+ const providers = relays.flatMap((country) =>
+ country.cities.flatMap((city) => city.relays.map((relay) => relay.provider)),
+ );
+ const uniqueProviders = removeDuplicates(providers).sort((a, b) => a.localeCompare(b));
+
+ // Empty containt array means that all providers are selected. No selection isn't possible.
+ return Object.fromEntries(
+ uniqueProviders.map((provider) => [
+ provider,
+ providerConstraint.length === 0 || providerConstraint.includes(provider),
+ ]),
+ );
+}
+
+function FilterByOwnership() {
+ return null;
+}
+
+interface IFilterByProviderProps {
+ providers: Record<string, boolean>;
+ setProviders: (providers: (previous: Record<string, boolean>) => Record<string, boolean>) => void;
+}
+
+function FilterByProvider(props: IFilterByProviderProps) {
+ const [expanded, , , toggleExpanded] = useBoolean(false);
+
+ const onToggle = useCallback(
+ (provider: string) =>
+ props.setProviders((providers) => ({ ...providers, [provider]: !providers[provider] })),
+ [props.setProviders],
+ );
+
+ const toggleAll = useCallback(() => {
+ props.setProviders((providers) => {
+ const shouldSelect = !Object.values(providers).every((value) => value);
+ return Object.fromEntries(Object.keys(providers).map((provider) => [provider, shouldSelect]));
+ });
+ }, []);
+
+ return (
+ <>
+ <Cell.CellButton onClick={toggleExpanded}>
+ <Cell.Label>{messages.pgettext('filter-view', 'Providers')}</Cell.Label>
+ <ImageView
+ tintColor={colors.white80}
+ source={expanded ? 'icon-chevron-up' : 'icon-chevron-down'}
+ height={24}
+ />
+ </Cell.CellButton>
+ <Accordion expanded={expanded}>
+ <CheckboxRow
+ label={messages.pgettext('filter-view', 'All providers')}
+ bold
+ checked={Object.values(props.providers).every((value) => value)}
+ onChange={toggleAll}
+ />
+ {Object.entries(props.providers).map(([provider, checked]) => (
+ <CheckboxRow key={provider} label={provider} checked={checked} onChange={onToggle} />
+ ))}
+ </Accordion>
+ </>
+ );
+}
+
interface IStyledRowTitleProps {
bold?: boolean;
}
@@ -180,22 +189,22 @@ const StyledRowTitle = styled.label(normalText, (props: IStyledRowTitleProps) =>
marginLeft: '22px',
}));
-interface IProviderRowProps extends IStyledRowTitleProps {
- provider: string;
+interface ICheckboxRowProps extends IStyledRowTitleProps {
+ label: string;
checked: boolean;
- onCheck: (provider: string) => void;
+ onChange: (provider: string) => void;
}
-function ProviderRow(props: IProviderRowProps) {
- const onCheck = useCallback(() => props.onCheck(props.provider), [props.onCheck, props.provider]);
+function CheckboxRow(props: ICheckboxRowProps) {
+ const onToggle = useCallback(() => props.onChange(props.label), [props.onChange, props.label]);
return (
- <StyledRow onClick={onCheck}>
- <StyledCheckbox role="checkbox" aria-label={props.provider} aria-checked={props.checked}>
+ <StyledRow onClick={onToggle}>
+ <StyledCheckbox role="checkbox" aria-label={props.label} aria-checked={props.checked}>
{props.checked && <ImageView source="icon-tick" width={18} tintColor={colors.green} />}
</StyledCheckbox>
<StyledRowTitle aria-hidden bold={props.bold}>
- {props.provider}
+ {props.label}
</StyledRowTitle>
</StyledRow>
);