summaryrefslogtreecommitdiffhomepage
path: root/gui
diff options
context:
space:
mode:
Diffstat (limited to 'gui')
-rw-r--r--gui/src/renderer/app.tsx2
-rw-r--r--gui/src/renderer/components/AppRouter.tsx2
-rw-r--r--gui/src/renderer/components/FilterByProvider.tsx198
-rw-r--r--gui/src/renderer/lib/routes.ts1
-rw-r--r--gui/src/renderer/redux/settings/reducers.ts3
-rw-r--r--gui/src/shared/localization-contexts.ts2
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'