diff options
Diffstat (limited to 'gui/src')
| -rw-r--r-- | gui/src/renderer/app.tsx | 2 | ||||
| -rw-r--r-- | gui/src/renderer/components/AppRouter.tsx | 2 | ||||
| -rw-r--r-- | gui/src/renderer/components/FilterByProvider.tsx | 198 | ||||
| -rw-r--r-- | gui/src/renderer/lib/routes.ts | 1 | ||||
| -rw-r--r-- | gui/src/renderer/redux/settings/reducers.ts | 3 | ||||
| -rw-r--r-- | gui/src/shared/localization-contexts.ts | 2 |
6 files changed, 208 insertions, 0 deletions
diff --git a/gui/src/renderer/app.tsx b/gui/src/renderer/app.tsx index 5dfdb7bf29..49c4129112 100644 --- a/gui/src/renderer/app.tsx +++ b/gui/src/renderer/app.tsx @@ -595,11 +595,13 @@ export default class AppRenderer { openvpnConstraints, wireguardConstraints, tunnelProtocol, + providers, } = relaySettings.normal; actions.settings.updateRelay({ normal: { location: liftConstraint(location), + providers, openvpn: { port: liftConstraint(openvpnConstraints.port), protocol: liftConstraint(openvpnConstraints.protocol), diff --git a/gui/src/renderer/components/AppRouter.tsx b/gui/src/renderer/components/AppRouter.tsx index 1816541b49..9f049fc791 100644 --- a/gui/src/renderer/components/AppRouter.tsx +++ b/gui/src/renderer/components/AppRouter.tsx @@ -25,6 +25,7 @@ import { VoucherVerificationSuccess, } from './ExpiredAccountAddTime'; import { RoutePath } from '../lib/routes'; +import FilterByProvider from './FilterByProvider'; interface IAppRoutesState { currentLocation: IHistoryProps['history']['location']; @@ -94,6 +95,7 @@ class AppRouter extends React.Component<IHistoryProps, IAppRoutesState> { <Route exact path={RoutePath.splitTunneling} component={SplitTunnelingSettings} /> <Route exact path={RoutePath.support} component={SupportPage} /> <Route exact path={RoutePath.selectLocation} component={SelectLocationPage} /> + <Route exact path={RoutePath.filterByProvider} component={FilterByProvider} /> </Switch> </TransitionView> </TransitionContainer> diff --git a/gui/src/renderer/components/FilterByProvider.tsx b/gui/src/renderer/components/FilterByProvider.tsx new file mode 100644 index 0000000000..a7722cdbe0 --- /dev/null +++ b/gui/src/renderer/components/FilterByProvider.tsx @@ -0,0 +1,198 @@ +import React, { useCallback, useMemo, 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 * as AppButton from './AppButton'; +import { mediumText } from './common-styles'; +import ImageView from './ImageView'; +import { Container, Layout } from './Layout'; +import { + BackBarItem, + NavigationBar, + NavigationContainer, + NavigationItems, + NavigationScrollbars, + TitleBarItem, +} from './NavigationBar'; + +const StyledContainer = styled(Container)({ + backgroundColor: colors.darkBlue, +}); + +const StyledNavigationScrollbars = styled(NavigationScrollbars)({ + backgroundColor: colors.blue, + flex: 1, +}); + +const StyledFooter = styled.div({ + display: 'flex', + flexDirection: 'column', + padding: '18px 22px 22px', +}); + +enum Selection { + all, + some, + none, +} + +export default function FilterByProvider() { + const history = useHistory(); + const { updateRelaySettings } = useAppContext(); + + const relayList = useSelector((state) => state.settings.relayLocations); + const providerConstraint = useSelector((state) => { + if ('normal' in state.settings.relaySettings) { + return state.settings.relaySettings.normal.providers; + } else { + return []; + } + }); + + const [providers, setProviders] = useState(() => { + const providers = relayList.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 onApply = useCallback(async () => { + const selectedProviders = + selectionStatus === Selection.all + ? [] + : Object.entries(providers) + .filter(([, selected]) => selected) + .map(([provider]) => provider); + + await updateRelaySettings({ normal: { providers: selectedProviders } }); + + history.pop(); + }, [providers, history, updateRelaySettings, selectionStatus]); + + return ( + <Layout> + <StyledContainer> + <NavigationContainer> + <NavigationBar alwaysDisplayBarTitle={true}> + <NavigationItems> + <BackBarItem action={history.pop}> + { + // TRANSLATORS: Back button in navigation bar + messages.pgettext('filter-by-provider-nav', 'Select location') + } + </BackBarItem> + <TitleBarItem> + { + // TRANSLATORS: Title label in navigation bar + messages.pgettext('filter-by-provider-nav', 'Filter by provider') + } + </TitleBarItem> + </NavigationItems> + </NavigationBar> + <StyledNavigationScrollbars> + <ProviderRow + provider={messages.pgettext('filter-by-provider-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} /> + ))} + </StyledNavigationScrollbars> + <StyledFooter> + <AppButton.GreenButton disabled={selectionStatus === Selection.none} onClick={onApply}> + {messages.gettext('Apply')} + </AppButton.GreenButton> + </StyledFooter> + </NavigationContainer> + </StyledContainer> + </Layout> + ); +} + +interface IStyledRowTitleProps { + bold?: boolean; +} + +const StyledRow = styled.div({ + display: 'flex', + height: '42px', + padding: '9px 22px', +}); + +const StyledCheckbox = styled.div({ + width: '24px', + height: '24px', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + backgroundColor: colors.white, + borderRadius: '4px', +}); + +const StyledRowTitle = styled.span((props: IStyledRowTitleProps) => ({ + ...mediumText, + fontSize: '16px', + fontWeight: props.bold ? 800 : 600, + color: colors.white, + marginLeft: '22px', +})); + +interface IProviderRowProps extends IStyledRowTitleProps { + provider: string; + checked: boolean; + onCheck: (provider: string) => void; +} + +function ProviderRow(props: IProviderRowProps) { + const onCheck = useCallback(() => props.onCheck(props.provider), [props.onCheck, props.provider]); + + return ( + <StyledRow onClick={onCheck}> + <StyledCheckbox> + {props.checked && <ImageView source="icon-tick" width={16} tintColor={colors.green} />} + </StyledCheckbox> + <StyledRowTitle bold={props.bold}>{props.provider}</StyledRowTitle> + </StyledRow> + ); +} + +function removeDuplicates(list: string[]): string[] { + return list.reduce( + (result, current) => (result.includes(current) ? result : [...result, current]), + [] as string[], + ); +} diff --git a/gui/src/renderer/lib/routes.ts b/gui/src/renderer/lib/routes.ts index bf8e6e1f60..8d8c2ce27b 100644 --- a/gui/src/renderer/lib/routes.ts +++ b/gui/src/renderer/lib/routes.ts @@ -15,4 +15,5 @@ export enum RoutePath { splitTunneling = '/settings/advanced/split-tunneling', support = '/settings/support', selectLocation = '/select-location', + filterByProvider = '/select-location/filter-by-provider', } diff --git a/gui/src/renderer/redux/settings/reducers.ts b/gui/src/renderer/redux/settings/reducers.ts index 8f61721a6e..bbab124d36 100644 --- a/gui/src/renderer/redux/settings/reducers.ts +++ b/gui/src/renderer/redux/settings/reducers.ts @@ -18,6 +18,7 @@ export type RelaySettingsRedux = normal: { tunnelProtocol: LiftedConstraint<TunnelProtocol>; location: LiftedConstraint<RelayLocation>; + providers: string[]; openvpn: { port: LiftedConstraint<number>; protocol: LiftedConstraint<RelayProtocol>; @@ -47,6 +48,7 @@ export type BridgeSettingsRedux = export interface IRelayLocationRelayRedux { hostname: string; + provider: string; ipv4AddrIn: string; includeInCountry: boolean; active: boolean; @@ -156,6 +158,7 @@ const initialState: ISettingsReduxState = { normal: { location: 'any', tunnelProtocol: 'any', + providers: [], wireguard: { port: 'any' }, openvpn: { port: 'any', diff --git a/gui/src/shared/localization-contexts.ts b/gui/src/shared/localization-contexts.ts index 6d86920870..7e80e68a5b 100644 --- a/gui/src/shared/localization-contexts.ts +++ b/gui/src/shared/localization-contexts.ts @@ -14,6 +14,8 @@ export type LocalizationContexts = | 'account-expiry' | 'select-location-view' | 'select-location-nav' + | 'filter-by-provider-view' + | 'filter-by-provider-nav' | 'settings-view' | 'navigation-bar' | 'account-view' |
