diff options
| author | Oskar Nyberg <oskar@mullvad.net> | 2022-05-19 10:06:32 +0200 |
|---|---|---|
| committer | Oskar Nyberg <oskar@mullvad.net> | 2022-05-19 10:06:32 +0200 |
| commit | 9cca46231f623247fac50694a66fb5eedada15d8 (patch) | |
| tree | a62db50609fe2679d1f1105558a4712f8300693d /gui | |
| parent | 0211a43e2f9e1b7beccaefd9a2a09672f1877fd8 (diff) | |
| parent | a39213f52d76ddf08ed0a637a0fe831b3581466e (diff) | |
| download | mullvadvpn-9cca46231f623247fac50694a66fb5eedada15d8.tar.xz mullvadvpn-9cca46231f623247fac50694a66fb5eedada15d8.zip | |
Merge branch 'update-filters'
Diffstat (limited to 'gui')
34 files changed, 566 insertions, 400 deletions
diff --git a/gui/locales/da/messages.po b/gui/locales/da/messages.po index fa85d5d701..46b2faec33 100644 --- a/gui/locales/da/messages.po +++ b/gui/locales/da/messages.po @@ -499,16 +499,16 @@ msgid "Something went wrong. Please contact us at %(email)s" msgstr "Noget gik galt. Kontakt os på %(email)s" #. Title label in navigation bar -msgctxt "filter-by-provider-nav" +msgctxt "filter-nav" msgid "Filter by provider" msgstr "Filtrer efter udbyder" #. Back button in navigation bar -msgctxt "filter-by-provider-nav" +msgctxt "filter-nav" msgid "Select location" msgstr "Vælg placering" -msgctxt "filter-by-provider-view" +msgctxt "filter-view" msgid "All providers" msgstr "Alle udbydere" diff --git a/gui/locales/de/messages.po b/gui/locales/de/messages.po index 25c2408eb6..154874e5f8 100644 --- a/gui/locales/de/messages.po +++ b/gui/locales/de/messages.po @@ -499,16 +499,16 @@ msgid "Something went wrong. Please contact us at %(email)s" msgstr "Es ist etwas schiefgegangen. Bitte kontaktieren Sie uns unter %(email)s" #. Title label in navigation bar -msgctxt "filter-by-provider-nav" +msgctxt "filter-nav" msgid "Filter by provider" msgstr "Nach Provider filtern" #. Back button in navigation bar -msgctxt "filter-by-provider-nav" +msgctxt "filter-nav" msgid "Select location" msgstr "Ort auswählen" -msgctxt "filter-by-provider-view" +msgctxt "filter-view" msgid "All providers" msgstr "Alle Provider" diff --git a/gui/locales/es/messages.po b/gui/locales/es/messages.po index ff06235966..cf03916e2c 100644 --- a/gui/locales/es/messages.po +++ b/gui/locales/es/messages.po @@ -499,16 +499,16 @@ msgid "Something went wrong. Please contact us at %(email)s" msgstr "Hubo un problema. Envíenos un mensaje a %(email)s" #. Title label in navigation bar -msgctxt "filter-by-provider-nav" +msgctxt "filter-nav" msgid "Filter by provider" msgstr "Filtrar por proveedor" #. Back button in navigation bar -msgctxt "filter-by-provider-nav" +msgctxt "filter-nav" msgid "Select location" msgstr "Seleccionar ubicación" -msgctxt "filter-by-provider-view" +msgctxt "filter-view" msgid "All providers" msgstr "Todos los proveedores" diff --git a/gui/locales/fi/messages.po b/gui/locales/fi/messages.po index 039ee081d7..6315427592 100644 --- a/gui/locales/fi/messages.po +++ b/gui/locales/fi/messages.po @@ -499,16 +499,16 @@ msgid "Something went wrong. Please contact us at %(email)s" msgstr "Jokin meni vikaan. Ota meihin yhteyttä osoitteeseen %(email)s" #. Title label in navigation bar -msgctxt "filter-by-provider-nav" +msgctxt "filter-nav" msgid "Filter by provider" msgstr "Suodata palveluntarjoajan mukaan" #. Back button in navigation bar -msgctxt "filter-by-provider-nav" +msgctxt "filter-nav" msgid "Select location" msgstr "Valitse sijainti" -msgctxt "filter-by-provider-view" +msgctxt "filter-view" msgid "All providers" msgstr "Kaikki palveluntarjoajat" diff --git a/gui/locales/fr/messages.po b/gui/locales/fr/messages.po index 57c128fc86..b978183041 100644 --- a/gui/locales/fr/messages.po +++ b/gui/locales/fr/messages.po @@ -499,16 +499,16 @@ msgid "Something went wrong. Please contact us at %(email)s" msgstr "Un problème est survenu. Veuillez nous contacter à %(email)s" #. Title label in navigation bar -msgctxt "filter-by-provider-nav" +msgctxt "filter-nav" msgid "Filter by provider" msgstr "Filtrer par fournisseur" #. Back button in navigation bar -msgctxt "filter-by-provider-nav" +msgctxt "filter-nav" msgid "Select location" msgstr "Sélectionner une localisation" -msgctxt "filter-by-provider-view" +msgctxt "filter-view" msgid "All providers" msgstr "Tous les fournisseurs" diff --git a/gui/locales/it/messages.po b/gui/locales/it/messages.po index 4291dc6862..6073107f25 100644 --- a/gui/locales/it/messages.po +++ b/gui/locales/it/messages.po @@ -499,16 +499,16 @@ msgid "Something went wrong. Please contact us at %(email)s" msgstr "Si è verificato un errore. Contattaci all'indirizzo %(email)s" #. Title label in navigation bar -msgctxt "filter-by-provider-nav" +msgctxt "filter-nav" msgid "Filter by provider" msgstr "Filtra per fornitore" #. Back button in navigation bar -msgctxt "filter-by-provider-nav" +msgctxt "filter-nav" msgid "Select location" msgstr "Seleziona posizione" -msgctxt "filter-by-provider-view" +msgctxt "filter-view" msgid "All providers" msgstr "Tutti i fornitori" diff --git a/gui/locales/ja/messages.po b/gui/locales/ja/messages.po index f4058edd33..697670243d 100644 --- a/gui/locales/ja/messages.po +++ b/gui/locales/ja/messages.po @@ -488,16 +488,16 @@ msgid "Something went wrong. Please contact us at %(email)s" msgstr "何か問題が生じたようです。 %(email)s までご連絡ください。" #. Title label in navigation bar -msgctxt "filter-by-provider-nav" +msgctxt "filter-nav" msgid "Filter by provider" msgstr "プロバイダで絞り込む" #. Back button in navigation bar -msgctxt "filter-by-provider-nav" +msgctxt "filter-nav" msgid "Select location" msgstr "場所を選択" -msgctxt "filter-by-provider-view" +msgctxt "filter-view" msgid "All providers" msgstr "すべてのプロバイダ" diff --git a/gui/locales/ko/messages.po b/gui/locales/ko/messages.po index d1801dbb60..8aa63e6e59 100644 --- a/gui/locales/ko/messages.po +++ b/gui/locales/ko/messages.po @@ -488,16 +488,16 @@ msgid "Something went wrong. Please contact us at %(email)s" msgstr "문제가 발생했습니다. %(email)s(으)로 연락주세요." #. Title label in navigation bar -msgctxt "filter-by-provider-nav" +msgctxt "filter-nav" msgid "Filter by provider" msgstr "제공업체별 필터링" #. Back button in navigation bar -msgctxt "filter-by-provider-nav" +msgctxt "filter-nav" msgid "Select location" msgstr "위치 선택" -msgctxt "filter-by-provider-view" +msgctxt "filter-view" msgid "All providers" msgstr "모든 제공업체" diff --git a/gui/locales/messages.pot b/gui/locales/messages.pot index d741ac8e37..b204f818c8 100644 --- a/gui/locales/messages.pot +++ b/gui/locales/messages.pot @@ -63,6 +63,9 @@ msgid_plural "%d hours ago" msgstr[0] "" msgstr[1] "" +msgid "Any" +msgstr "" + msgid "Apply" msgstr "" @@ -585,14 +588,38 @@ msgid "Something went wrong. Please contact us at %(email)s" msgstr "" #. Title label in navigation bar -msgctxt "filter-by-provider-nav" -msgid "Filter by provider" +msgctxt "filter-nav" +msgid "Filter" msgstr "" -msgctxt "filter-by-provider-view" +msgctxt "filter-view" msgid "All providers" msgstr "" +msgctxt "filter-view" +msgid "Mullvad owned only" +msgstr "" + +msgctxt "filter-view" +msgid "Owned" +msgstr "" + +msgctxt "filter-view" +msgid "Ownership" +msgstr "" + +msgctxt "filter-view" +msgid "Providers" +msgstr "" + +msgctxt "filter-view" +msgid "Rented" +msgstr "" + +msgctxt "filter-view" +msgid "Rented only" +msgstr "" + msgctxt "in-app-notifications" msgid "\"Always require VPN\" is enabled." msgstr "" @@ -1158,10 +1185,6 @@ msgid "Exit" msgstr "" msgctxt "select-location-view" -msgid "Filter by provider" -msgstr "" - -msgctxt "select-location-view" msgid "Filtered:" msgstr "" diff --git a/gui/locales/my/messages.po b/gui/locales/my/messages.po index 7831e98452..7ce494def3 100644 --- a/gui/locales/my/messages.po +++ b/gui/locales/my/messages.po @@ -488,16 +488,16 @@ msgid "Something went wrong. Please contact us at %(email)s" msgstr "တစ်စုံတစ်ခု မှားနေပါသည်။ %(email)s မှတစ်ဆင့် ကျွန်ုပ်တို့ထံ ဆက်သွယ်ပေးပါ" #. Title label in navigation bar -msgctxt "filter-by-provider-nav" +msgctxt "filter-nav" msgid "Filter by provider" msgstr "ပံ့ပိုးသူအလိုက် စစ်ထုတ်ရန်" #. Back button in navigation bar -msgctxt "filter-by-provider-nav" +msgctxt "filter-nav" msgid "Select location" msgstr "တည်နေရာ ရွေးရန်" -msgctxt "filter-by-provider-view" +msgctxt "filter-view" msgid "All providers" msgstr "ပံ့ပိုးသူအားလုံး" diff --git a/gui/locales/nb/messages.po b/gui/locales/nb/messages.po index df63a37967..239e3104ac 100644 --- a/gui/locales/nb/messages.po +++ b/gui/locales/nb/messages.po @@ -499,16 +499,16 @@ msgid "Something went wrong. Please contact us at %(email)s" msgstr "Det oppstod en feil. Kontakt oss på %(email)s" #. Title label in navigation bar -msgctxt "filter-by-provider-nav" +msgctxt "filter-nav" msgid "Filter by provider" msgstr "Filtrer etter leverandør" #. Back button in navigation bar -msgctxt "filter-by-provider-nav" +msgctxt "filter-nav" msgid "Select location" msgstr "Velg plassering" -msgctxt "filter-by-provider-view" +msgctxt "filter-view" msgid "All providers" msgstr "Alle leverandører" diff --git a/gui/locales/nl/messages.po b/gui/locales/nl/messages.po index 224fc595f2..11fcabbb07 100644 --- a/gui/locales/nl/messages.po +++ b/gui/locales/nl/messages.po @@ -499,16 +499,16 @@ msgid "Something went wrong. Please contact us at %(email)s" msgstr "Er is iets misgelopen. Neem contact met ons op via %(email)s" #. Title label in navigation bar -msgctxt "filter-by-provider-nav" +msgctxt "filter-nav" msgid "Filter by provider" msgstr "Filteren op provider" #. Back button in navigation bar -msgctxt "filter-by-provider-nav" +msgctxt "filter-nav" msgid "Select location" msgstr "Locatie selecteren" -msgctxt "filter-by-provider-view" +msgctxt "filter-view" msgid "All providers" msgstr "Alle providers" diff --git a/gui/locales/pl/messages.po b/gui/locales/pl/messages.po index 02e08e8000..c59782ccd8 100644 --- a/gui/locales/pl/messages.po +++ b/gui/locales/pl/messages.po @@ -521,16 +521,16 @@ msgid "Something went wrong. Please contact us at %(email)s" msgstr "Coś poszło nie tak. Skontaktuj się z nami pod adresem %(email)s" #. Title label in navigation bar -msgctxt "filter-by-provider-nav" +msgctxt "filter-nav" msgid "Filter by provider" msgstr "Filtruj wg dostawcy" #. Back button in navigation bar -msgctxt "filter-by-provider-nav" +msgctxt "filter-nav" msgid "Select location" msgstr "Wybierz lokalizację" -msgctxt "filter-by-provider-view" +msgctxt "filter-view" msgid "All providers" msgstr "Wszyscy dostawcy" diff --git a/gui/locales/pt/messages.po b/gui/locales/pt/messages.po index a8acbec52d..a3667aedcf 100644 --- a/gui/locales/pt/messages.po +++ b/gui/locales/pt/messages.po @@ -499,16 +499,16 @@ msgid "Something went wrong. Please contact us at %(email)s" msgstr "Ocorreu um erro. Por favor contate-nos através de %(email)s" #. Title label in navigation bar -msgctxt "filter-by-provider-nav" +msgctxt "filter-nav" msgid "Filter by provider" msgstr "Filtrar por fornecedor" #. Back button in navigation bar -msgctxt "filter-by-provider-nav" +msgctxt "filter-nav" msgid "Select location" msgstr "Selecionar local" -msgctxt "filter-by-provider-view" +msgctxt "filter-view" msgid "All providers" msgstr "Todos os fornecedores" diff --git a/gui/locales/ru/messages.po b/gui/locales/ru/messages.po index 93ac3e2e54..514a3c9bfb 100644 --- a/gui/locales/ru/messages.po +++ b/gui/locales/ru/messages.po @@ -521,16 +521,16 @@ msgid "Something went wrong. Please contact us at %(email)s" msgstr "Ошибка. Свяжитесь с нами по адресу %(email)s" #. Title label in navigation bar -msgctxt "filter-by-provider-nav" +msgctxt "filter-nav" msgid "Filter by provider" msgstr "Фильтр по провайдеру" #. Back button in navigation bar -msgctxt "filter-by-provider-nav" +msgctxt "filter-nav" msgid "Select location" msgstr "Выбрать местоположение" -msgctxt "filter-by-provider-view" +msgctxt "filter-view" msgid "All providers" msgstr "Все провайдеры" diff --git a/gui/locales/sv/messages.po b/gui/locales/sv/messages.po index 9f3e41dec5..9dfdc8be81 100644 --- a/gui/locales/sv/messages.po +++ b/gui/locales/sv/messages.po @@ -499,16 +499,16 @@ msgid "Something went wrong. Please contact us at %(email)s" msgstr "Ett fel har inträffat. Kontakta oss på %(email)s" #. Title label in navigation bar -msgctxt "filter-by-provider-nav" +msgctxt "filter-nav" msgid "Filter by provider" msgstr "Filtrera efter leverantör" #. Back button in navigation bar -msgctxt "filter-by-provider-nav" +msgctxt "filter-nav" msgid "Select location" msgstr "Välj plats" -msgctxt "filter-by-provider-view" +msgctxt "filter-view" msgid "All providers" msgstr "Alla leverantörer" diff --git a/gui/locales/th/messages.po b/gui/locales/th/messages.po index 1ae7c57d05..e4abc68619 100644 --- a/gui/locales/th/messages.po +++ b/gui/locales/th/messages.po @@ -488,16 +488,16 @@ msgid "Something went wrong. Please contact us at %(email)s" msgstr "เกิดข้อผิดพลาดขึ้น โปรดติดต่อเราที่ %(email)s" #. Title label in navigation bar -msgctxt "filter-by-provider-nav" +msgctxt "filter-nav" msgid "Filter by provider" msgstr "กรองตามผู้ให้บริการ" #. Back button in navigation bar -msgctxt "filter-by-provider-nav" +msgctxt "filter-nav" msgid "Select location" msgstr "เลือกตำแหน่งที่ตั้ง" -msgctxt "filter-by-provider-view" +msgctxt "filter-view" msgid "All providers" msgstr "ผู้ให้บริการทั้งหมด" diff --git a/gui/locales/tr/messages.po b/gui/locales/tr/messages.po index 343daeba4b..18b45b23cf 100644 --- a/gui/locales/tr/messages.po +++ b/gui/locales/tr/messages.po @@ -499,16 +499,16 @@ msgid "Something went wrong. Please contact us at %(email)s" msgstr "Bir sorun oluştu. Lütfen bizimle %(email)s adresinden iletişime geçin" #. Title label in navigation bar -msgctxt "filter-by-provider-nav" +msgctxt "filter-nav" msgid "Filter by provider" msgstr "Hizmet sağlayıcıya göre filtrele" #. Back button in navigation bar -msgctxt "filter-by-provider-nav" +msgctxt "filter-nav" msgid "Select location" msgstr "Konum seçin" -msgctxt "filter-by-provider-view" +msgctxt "filter-view" msgid "All providers" msgstr "Tüm hizmet sağlayıcılar" diff --git a/gui/locales/zh-CN/messages.po b/gui/locales/zh-CN/messages.po index a3b3080a85..26bb48717b 100644 --- a/gui/locales/zh-CN/messages.po +++ b/gui/locales/zh-CN/messages.po @@ -488,16 +488,16 @@ msgid "Something went wrong. Please contact us at %(email)s" msgstr "出错了。请发送电子邮件到 %(email)s,与我们联系" #. Title label in navigation bar -msgctxt "filter-by-provider-nav" +msgctxt "filter-nav" msgid "Filter by provider" msgstr "按提供商筛选" #. Back button in navigation bar -msgctxt "filter-by-provider-nav" +msgctxt "filter-nav" msgid "Select location" msgstr "选择位置" -msgctxt "filter-by-provider-view" +msgctxt "filter-view" msgid "All providers" msgstr "所有提供商" diff --git a/gui/locales/zh-TW/messages.po b/gui/locales/zh-TW/messages.po index 2d4c180918..e1409ae4b6 100644 --- a/gui/locales/zh-TW/messages.po +++ b/gui/locales/zh-TW/messages.po @@ -488,16 +488,16 @@ msgid "Something went wrong. Please contact us at %(email)s" msgstr "出現了問題。請與我們聯絡:%(email)s" #. Title label in navigation bar -msgctxt "filter-by-provider-nav" +msgctxt "filter-nav" msgid "Filter by provider" msgstr "按供應商篩選" #. Back button in navigation bar -msgctxt "filter-by-provider-nav" +msgctxt "filter-nav" msgid "Select location" msgstr "選取位置" -msgctxt "filter-by-provider-view" +msgctxt "filter-view" msgid "All providers" msgstr "所有供應商" diff --git a/gui/src/main/daemon-rpc.ts b/gui/src/main/daemon-rpc.ts index c40e40b864..166a48c9cb 100644 --- a/gui/src/main/daemon-rpc.ts +++ b/gui/src/main/daemon-rpc.ts @@ -44,6 +44,7 @@ import { LoggedInDeviceState, LoggedOutDeviceState, ObfuscationType, + Ownership, ProxySettings, ProxyType, RelayLocation, @@ -326,6 +327,12 @@ export class DaemonRpc { normalUpdate.setProviders(providerUpdate); } + if (settingsUpdate.ownership !== undefined) { + const ownershipUpdate = new grpcTypes.OwnershipUpdate(); + ownershipUpdate.setOwnership(convertToOwnership(settingsUpdate.ownership)); + normalUpdate.setOwnership(ownershipUpdate); + } + grpcRelaySettings.setNormal(normalUpdate); await this.call<grpcTypes.RelaySettingsUpdate, Empty>( this.client.updateRelaySettings, @@ -1014,6 +1021,7 @@ function convertFromRelaySettings( : 'any'; const tunnelProtocol = convertFromTunnelTypeConstraint(normal.getTunnelType()!); const providers = normal.getProvidersList(); + const ownership = convertFromOwnership(normal.getOwnership()); const openvpnConstraints = convertFromOpenVpnConstraints(normal.getOpenvpnConstraints()!); const wireguardConstraints = convertFromWireguardConstraints( normal.getWireguardConstraints()!, @@ -1024,6 +1032,7 @@ function convertFromRelaySettings( location, tunnelProtocol, providers, + ownership, wireguardConstraints, openvpnConstraints, }, @@ -1043,10 +1052,12 @@ function convertFromBridgeSettings( const grpcLocation = normalSettings.location; const location = grpcLocation ? { only: convertFromLocation(grpcLocation) } : 'any'; const providers = normalSettings.providersList; + const ownership = convertFromOwnership(normalSettings.ownership); return { normal: { location, providers, + ownership, }, }; } @@ -1210,6 +1221,28 @@ function convertFromDaemonEvent(data: grpcTypes.DaemonEvent): DaemonEvent { throw new Error(`Unknown daemon event received containing ${keys}`); } +function convertFromOwnership(ownership: grpcTypes.Ownership): Ownership { + switch (ownership) { + case grpcTypes.Ownership.ANY: + return Ownership.any; + case grpcTypes.Ownership.MULLVAD_OWNED: + return Ownership.mullvadOwned; + case grpcTypes.Ownership.RENTED: + return Ownership.rented; + } +} + +function convertToOwnership(ownership: Ownership): grpcTypes.Ownership { + switch (ownership) { + case Ownership.any: + return grpcTypes.Ownership.ANY; + case Ownership.mullvadOwned: + return grpcTypes.Ownership.MULLVAD_OWNED; + case Ownership.rented: + return grpcTypes.Ownership.RENTED; + } +} + function convertFromOpenVpnConstraints( constraints: grpcTypes.OpenvpnConstraints, ): IOpenVpnConstraints { diff --git a/gui/src/main/index.ts b/gui/src/main/index.ts index 4d0f915bb9..622194fbd7 100644 --- a/gui/src/main/index.ts +++ b/gui/src/main/index.ts @@ -36,6 +36,7 @@ import { IRelayList, ISettings, liftConstraint, + Ownership, RelaySettings, RelaySettingsUpdate, TunnelState, @@ -162,6 +163,7 @@ class ApplicationMain { location: 'any', tunnelProtocol: 'any', providers: [], + ownership: Ownership.any, openvpnConstraints: { port: 'any', protocol: 'any', @@ -178,6 +180,7 @@ class ApplicationMain { normal: { location: 'any', providers: [], + ownership: Ownership.any, }, }, bridgeState: 'auto', diff --git a/gui/src/renderer/app.tsx b/gui/src/renderer/app.tsx index 23abcd8627..f4e711f3ff 100644 --- a/gui/src/renderer/app.tsx +++ b/gui/src/renderer/app.tsx @@ -636,12 +636,14 @@ export default class AppRenderer { wireguardConstraints, tunnelProtocol, providers, + ownership, } = relaySettings.normal; actions.settings.updateRelay({ normal: { location: liftConstraint(location), providers, + ownership, 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 278ad0aafc..db69cbb80f 100644 --- a/gui/src/renderer/components/AppRouter.tsx +++ b/gui/src/renderer/components/AppRouter.tsx @@ -22,7 +22,7 @@ import { VoucherInput, VoucherVerificationSuccess, } from './ExpiredAccountAddTime'; -import FilterByProvider from './FilterByProvider'; +import Filter from './Filter'; import Focus, { IFocusHandle } from './Focus'; import Launch from './Launch'; import MainView from './MainView'; @@ -96,7 +96,7 @@ class AppRouter extends React.Component<IHistoryProps & IAppContext, IAppRoutesS <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} /> + <Route exact path={RoutePath.filter} component={Filter} /> </Switch> </TransitionView> </TransitionContainer> diff --git a/gui/src/renderer/components/Filter.tsx b/gui/src/renderer/components/Filter.tsx new file mode 100644 index 0000000000..a7b8272831 --- /dev/null +++ b/gui/src/renderer/components/Filter.tsx @@ -0,0 +1,274 @@ +import { useCallback, useMemo, useState } from 'react'; +import styled from 'styled-components'; + +import { colors } from '../../config.json'; +import { Ownership } from '../../shared/daemon-rpc-types'; +import { messages } from '../../shared/gettext'; +import { useAppContext } from '../context'; +import { useHistory } from '../lib/history'; +import { useBoolean } from '../lib/utilityHooks'; +import { IReduxState, useSelector } from '../redux/store'; +import Accordion from './Accordion'; +import * as AppButton from './AppButton'; +import { AriaInputGroup, AriaLabel } from './AriaGroup'; +import * as Cell from './cell'; +import Selector from './cell/Selector'; +import { normalText } from './common-styles'; +import ImageView from './ImageView'; +import { BackAction } from './KeyboardNavigation'; +import { Container, Layout } from './Layout'; +import { + NavigationBar, + NavigationContainer, + NavigationItems, + NavigationScrollbars, + TitleBarItem, +} from './NavigationBar'; + +const StyledContainer = styled(Container)({ + backgroundColor: colors.darkBlue, +}); + +const StyledNavigationScrollbars = styled(NavigationScrollbars)({ + backgroundColor: colors.darkBlue, + flex: 1, +}); + +const StyledFooter = styled.div({ + display: 'flex', + flexDirection: 'column', + padding: '18px 22px 22px', +}); + +export default function Filter() { + const history = useHistory(); + const { updateRelaySettings } = useAppContext(); + + const initialProviders = useSelector(providersSelector); + const [providers, setProviders] = useState<Record<string, boolean>>(initialProviders); + + const initialOwnership = useSelector((state) => + 'normal' in state.settings.relaySettings + ? state.settings.relaySettings.normal.ownership + : Ownership.any, + ); + const [ownership, setOwnership] = useState<Ownership>(initialOwnership); + + const onApply = useCallback(async () => { + // 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, ownership } }); + history.pop(); + }, [providers, ownership, history, updateRelaySettings]); + + return ( + <BackAction action={history.pop}> + <Layout> + <StyledContainer> + <NavigationContainer> + <NavigationBar alwaysDisplayBarTitle={true}> + <NavigationItems> + <TitleBarItem> + { + // TRANSLATORS: Title label in navigation bar + messages.pgettext('filter-nav', 'Filter') + } + </TitleBarItem> + </NavigationItems> + </NavigationBar> + <StyledNavigationScrollbars> + <FilterByOwnership ownership={ownership} setOwnership={setOwnership} /> + <FilterByProvider providers={providers} setProviders={setProviders} /> + </StyledNavigationScrollbars> + <StyledFooter> + <AppButton.GreenButton + disabled={Object.values(providers).every((provider) => !provider)} + onClick={onApply}> + {messages.gettext('Apply')} + </AppButton.GreenButton> + </StyledFooter> + </NavigationContainer> + </StyledContainer> + </Layout> + </BackAction> + ); +} + +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), + ]), + ); +} + +const StyledSelector = (styled(Selector)({ + marginBottom: 0, +}) as unknown) as new <T>() => Selector<T>; + +interface IFilterByOwnershipProps { + ownership: Ownership; + setOwnership: (ownership: Ownership) => void; +} + +function FilterByOwnership(props: IFilterByOwnershipProps) { + const [expanded, , , toggleExpanded] = useBoolean(false); + + const values = useMemo( + () => [ + { + label: messages.gettext('Any'), + value: Ownership.any, + }, + { + label: messages.pgettext('filter-view', 'Mullvad owned only'), + value: Ownership.mullvadOwned, + }, + { + label: messages.pgettext('filter-view', 'Rented only'), + value: Ownership.rented, + }, + ], + [], + ); + + return ( + <AriaInputGroup> + <Cell.CellButton onClick={toggleExpanded}> + <AriaLabel> + <Cell.Label>{messages.pgettext('filter-view', 'Ownership')}</Cell.Label> + </AriaLabel> + <ImageView + tintColor={colors.white80} + source={expanded ? 'icon-chevron-up' : 'icon-chevron-down'} + height={24} + /> + </Cell.CellButton> + + <Accordion expanded={expanded}> + <StyledSelector values={values} value={props.ownership} onSelect={props.setOwnership} /> + </Accordion> + </AriaInputGroup> + ); +} + +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; +} + +const StyledRow = styled.div({ + display: 'flex', + height: '44px', + alignItems: 'center', + padding: '0 22px', + marginBottom: '1px', + backgroundColor: colors.blue, +}); + +const StyledCheckbox = styled.div({ + width: '24px', + height: '24px', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + backgroundColor: colors.white, + borderRadius: '4px', +}); + +const StyledRowTitle = styled.label(normalText, (props: IStyledRowTitleProps) => ({ + fontWeight: props.bold ? 600 : 400, + color: colors.white, + marginLeft: '22px', +})); + +interface ICheckboxRowProps extends IStyledRowTitleProps { + label: string; + checked: boolean; + onChange: (provider: string) => void; +} + +function CheckboxRow(props: ICheckboxRowProps) { + const onToggle = useCallback(() => props.onChange(props.label), [props.onChange, props.label]); + + return ( + <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.label} + </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/components/FilterByProvider.tsx b/gui/src/renderer/components/FilterByProvider.tsx deleted file mode 100644 index 335d8597d8..0000000000 --- a/gui/src/renderer/components/FilterByProvider.tsx +++ /dev/null @@ -1,209 +0,0 @@ -import { 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 { normalText } from './common-styles'; -import ImageView from './ImageView'; -import { BackAction } from './KeyboardNavigation'; -import { Container, Layout } from './Layout'; -import { - NavigationBar, - NavigationContainer, - NavigationItems, - NavigationScrollbars, - TitleBarItem, -} from './NavigationBar'; - -const StyledContainer = styled(Container)({ - backgroundColor: colors.darkBlue, -}); - -const StyledNavigationScrollbars = styled(NavigationScrollbars)({ - backgroundColor: colors.darkBlue, - 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 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 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 ( - <BackAction action={history.pop}> - <Layout> - <StyledContainer> - <NavigationContainer> - <NavigationBar alwaysDisplayBarTitle={true}> - <NavigationItems> - <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> - </BackAction> - ); -} - -interface IStyledRowTitleProps { - bold?: boolean; -} - -const StyledRow = styled.div({ - display: 'flex', - height: '44px', - alignItems: 'center', - padding: '0 22px', - marginBottom: '1px', - backgroundColor: colors.blue, -}); - -const StyledCheckbox = styled.div({ - width: '24px', - height: '24px', - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - backgroundColor: colors.white, - borderRadius: '4px', -}); - -const StyledRowTitle = styled.label(normalText, (props: IStyledRowTitleProps) => ({ - fontWeight: props.bold ? 600 : 400, - 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 role="checkbox" aria-label={props.provider} aria-checked={props.checked}> - {props.checked && <ImageView source="icon-tick" width={18} tintColor={colors.green} />} - </StyledCheckbox> - <StyledRowTitle aria-hidden 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/components/SelectLocation.tsx b/gui/src/renderer/components/SelectLocation.tsx index 3d64906a78..34059f3e85 100644 --- a/gui/src/renderer/components/SelectLocation.tsx +++ b/gui/src/renderer/components/SelectLocation.tsx @@ -2,7 +2,12 @@ import React from 'react'; import { sprintf } from 'sprintf-js'; import { colors } from '../../config.json'; -import { LiftedConstraint, RelayLocation, TunnelProtocol } from '../../shared/daemon-rpc-types'; +import { + LiftedConstraint, + Ownership, + RelayLocation, + TunnelProtocol, +} from '../../shared/daemon-rpc-types'; import { messages } from '../../shared/gettext'; import { IRelayLocationRedux } from '../redux/settings/reducers'; import BridgeLocations, { SpecialBridgeLocationType } from './BridgeLocations'; @@ -25,16 +30,13 @@ import { } from './NavigationBar'; import { ScopeBarItem } from './ScopeBar'; import { - StyledClearProvidersButton, + StyledClearFilterButton, StyledContainer, StyledContent, - StyledFilterByProviderButton, - StyledFilterContainer, + StyledFilter, StyledFilterIconButton, - StyledFilterMenu, + StyledFilterRow, StyledNavigationBarAttachment, - StyledProviderCountRow, - StyledProvidersCount, StyledScopeBar, StyledSettingsHeader, } from './SelectLocationStyles'; @@ -50,13 +52,15 @@ interface IProps { allowEntrySelection: boolean; tunnelProtocol: LiftedConstraint<TunnelProtocol>; providers: string[]; + ownership: Ownership; onClose: () => void; - onViewFilterByProvider: () => void; + onViewFilter: () => void; onSelectExitLocation: (location: RelayLocation) => void; onSelectEntryLocation: (location: RelayLocation) => void; onSelectBridgeLocation: (location: RelayLocation) => void; onSelectClosestToExit: () => void; onClearProviders: () => void; + onClearOwnership: () => void; } enum LocationScope { @@ -65,7 +69,6 @@ enum LocationScope { } interface IState { - showFilterMenu: boolean; headingHeight: number; locationScope: LocationScope; } @@ -76,7 +79,7 @@ interface ISelectLocationSnapshot { } export default class SelectLocation extends React.Component<IProps, IState> { - public state = { showFilterMenu: false, headingHeight: 0, locationScope: LocationScope.exit }; + public state = { headingHeight: 0, locationScope: LocationScope.exit }; private scrollView = React.createRef<CustomScrollbarsRef>(); private spacePreAllocationViewRef = React.createRef<SpacePreAllocationView>(); @@ -90,7 +93,6 @@ export default class SelectLocation extends React.Component<IProps, IState> { private snapshotByScope: Partial<Record<LocationScope, ISelectLocationSnapshot>> = {}; - private filterButtonRef = React.createRef<HTMLDivElement>(); private headerRef = React.createRef<HTMLHeadingElement>(); public componentDidMount() { @@ -132,9 +134,12 @@ export default class SelectLocation extends React.Component<IProps, IState> { } public render() { + const showOwnershipFilter = this.props.ownership !== Ownership.any; + const showProvidersFilter = this.props.providers.length > 0; + const showFilters = showOwnershipFilter || showProvidersFilter; return ( <BackAction icon="close" action={this.props.onClose}> - <Layout onClick={this.onClickAnywhere}> + <Layout> <StyledContainer> <NavigationContainer> <NavigationBar> @@ -146,26 +151,17 @@ export default class SelectLocation extends React.Component<IProps, IState> { } </TitleBarItem> - <StyledFilterContainer ref={this.filterButtonRef}> - <StyledFilterIconButton - onClick={this.toggleFilterMenu} - aria-label={messages.gettext('Filter')}> - <ImageView - source="icon-filter-round" - tintColor={colors.white40} - tintHoverColor={colors.white60} - height={24} - width={24} - /> - </StyledFilterIconButton> - {this.state.showFilterMenu && ( - <StyledFilterMenu> - <StyledFilterByProviderButton onClick={this.props.onViewFilterByProvider}> - {messages.pgettext('select-location-view', 'Filter by provider')} - </StyledFilterByProviderButton> - </StyledFilterMenu> - )} - </StyledFilterContainer> + <StyledFilterIconButton + onClick={this.props.onViewFilter} + aria-label={messages.gettext('Filter')}> + <ImageView + source="icon-filter-round" + tintColor={colors.white40} + tintHoverColor={colors.white60} + height={24} + width={24} + /> + </StyledFilterIconButton> </NavigationItems> </NavigationBar> <NavigationScrollbars ref={this.scrollView}> @@ -181,32 +177,52 @@ export default class SelectLocation extends React.Component<IProps, IState> { {this.renderHeaderSubtitle()} </StyledSettingsHeader> - {this.props.providers.length > 0 && ( - <StyledProviderCountRow> + {showFilters && ( + <StyledFilterRow> {messages.pgettext('select-location-view', 'Filtered:')} - <StyledProvidersCount> - {sprintf( - messages.pgettext( - 'select-location-view', - 'Providers: %(numberOfProviders)d', - ), - { - numberOfProviders: this.props.providers.length, - }, - )} - <StyledClearProvidersButton - aria-label={messages.gettext('Clear')} - onClick={this.props.onClearProviders}> - <ImageView - height={16} - width={16} - source="icon-close" - tintColor={colors.white60} - tintHoverColor={colors.white80} - /> - </StyledClearProvidersButton> - </StyledProvidersCount> - </StyledProviderCountRow> + + {showOwnershipFilter && ( + <StyledFilter> + {this.ownershipFilterLabel()} + <StyledClearFilterButton + aria-label={messages.gettext('Clear')} + onClick={this.props.onClearOwnership}> + <ImageView + height={16} + width={16} + source="icon-close" + tintColor={colors.white60} + tintHoverColor={colors.white80} + /> + </StyledClearFilterButton> + </StyledFilter> + )} + + {showProvidersFilter && ( + <StyledFilter> + {sprintf( + messages.pgettext( + 'select-location-view', + 'Providers: %(numberOfProviders)d', + ), + { + numberOfProviders: this.props.providers.length, + }, + )} + <StyledClearFilterButton + aria-label={messages.gettext('Clear')} + onClick={this.props.onClearProviders}> + <ImageView + height={16} + width={16} + source="icon-close" + tintColor={colors.white60} + tintHoverColor={colors.white80} + /> + </StyledClearFilterButton> + </StyledFilter> + )} + </StyledFilterRow> )} {this.props.allowEntrySelection && ( <StyledScopeBar @@ -242,6 +258,17 @@ export default class SelectLocation extends React.Component<IProps, IState> { } } + private ownershipFilterLabel(): string { + switch (this.props.ownership) { + case Ownership.mullvadOwned: + return messages.pgettext('filter-view', 'Owned'); + case Ownership.rented: + return messages.pgettext('filter-view', 'Rented'); + default: + throw new Error('Only owned and rented should make label visible'); + } + } + private getLocationListRef(prevProps: IProps, prevState: IState) { if (prevState.locationScope === LocationScope.exit) { return this.exitLocationList.current; @@ -413,21 +440,6 @@ export default class SelectLocation extends React.Component<IProps, IState> { this.spacePreAllocationViewRef.current?.allocate(expandedContentHeight); this.scrollView.current?.scrollIntoView(locationRect); }; - - private toggleFilterMenu = () => { - this.setState((state) => ({ - showFilterMenu: !state.showFilterMenu, - })); - }; - - private onClickAnywhere = (event: React.MouseEvent<HTMLDivElement>) => { - if ( - this.state.showFilterMenu && - !this.filterButtonRef.current?.contains(event.target as HTMLElement) - ) { - this.setState({ showFilterMenu: false }); - } - }; } interface ISpacePreAllocationView { diff --git a/gui/src/renderer/components/SelectLocationStyles.tsx b/gui/src/renderer/components/SelectLocationStyles.tsx index 4ace3b15cc..15211389f6 100644 --- a/gui/src/renderer/components/SelectLocationStyles.tsx +++ b/gui/src/renderer/components/SelectLocationStyles.tsx @@ -29,13 +29,8 @@ export const StyledNavigationBarAttachment = styled.div({}, (props: { top: numbe zIndex: 1, })); -export const StyledFilterContainer = styled.div({ - display: 'flex', - position: 'relative', - justifySelf: 'end', -}); - export const StyledFilterIconButton = styled.button({ + justifySelf: 'end', borderWidth: 0, padding: 0, margin: 0, @@ -43,43 +38,19 @@ export const StyledFilterIconButton = styled.button({ backgroundColor: 'transparent', }); -export const StyledFilterMenu = styled.div({ - position: 'absolute', - top: 'calc(100% + 4px)', - right: '0', - borderRadius: '4px', - backgroundColor: colors.darkBlue, - overflow: 'hidden', - zIndex: 2, -}); - -export const StyledFilterByProviderButton = styled.button(tinyText, { - borderWidth: 0, - margin: 0, - cursor: 'default', - color: colors.white, - padding: '7px 15px', - whiteSpace: 'nowrap', - borderRadius: 0, - backgroundColor: colors.blue, - ':hover': { - backgroundColor: colors.blue80, - }, -}); - export const StyledSettingsHeader = styled(SettingsHeader)({ paddingLeft: '6px', paddingBottom: '11px', }); -export const StyledProviderCountRow = styled.div({ +export const StyledFilterRow = styled.div({ ...tinyText, color: colors.white, marginLeft: '6px', marginBottom: '8px', }); -export const StyledProvidersCount = styled.div({ +export const StyledFilter = styled.div({ ...tinyText, display: 'inline-flex', alignItems: 'center', @@ -90,7 +61,7 @@ export const StyledProvidersCount = styled.div({ color: colors.white, }); -export const StyledClearProvidersButton = styled.div({ +export const StyledClearFilterButton = styled.div({ display: 'inline-block', borderWidth: 0, padding: 0, diff --git a/gui/src/renderer/containers/SelectLocationPage.tsx b/gui/src/renderer/containers/SelectLocationPage.tsx index d14323457e..8d928421f4 100644 --- a/gui/src/renderer/containers/SelectLocationPage.tsx +++ b/gui/src/renderer/containers/SelectLocationPage.tsx @@ -1,7 +1,7 @@ import { connect } from 'react-redux'; import BridgeSettingsBuilder from '../../shared/bridge-settings-builder'; -import { LiftedConstraint, RelayLocation } from '../../shared/daemon-rpc-types'; +import { LiftedConstraint, Ownership, RelayLocation } from '../../shared/daemon-rpc-types'; import log from '../../shared/logging'; import RelaySettingsBuilder from '../../shared/relay-settings-builder'; import SelectLocation from '../components/SelectLocation'; @@ -44,17 +44,19 @@ const mapStateToProps = (state: IReduxState, props: IHistoryProps & IAppContext) ((tunnelProtocol === 'any' || tunnelProtocol === 'wireguard') && multihopEnabled); const providers = 'normal' in relaySettings ? relaySettings.normal.providers : []; + const ownership = 'normal' in relaySettings ? relaySettings.normal.ownership : Ownership.any; return { locale: state.userInterface.locale, selectedExitLocation, selectedEntryLocation, selectedBridgeLocation, - relayLocations: filterLocationsByProvider(state.settings.relayLocations, providers), - bridgeLocations: filterLocationsByProvider(state.settings.bridgeLocations, providers), + relayLocations: filterLocations(state.settings.relayLocations, providers, ownership), + bridgeLocations: filterLocations(state.settings.bridgeLocations, providers, ownership), allowEntrySelection, tunnelProtocol, providers, + ownership, onSelectEntryLocation: async (entryLocation: RelayLocation) => { // dismiss the view first @@ -76,7 +78,7 @@ const mapStateToProps = (state: IReduxState, props: IHistoryProps & IAppContext) const mapDispatchToProps = (_dispatch: ReduxDispatch, props: IHistoryProps & IAppContext) => { return { onClose: () => props.history.dismiss(), - onViewFilterByProvider: () => props.history.push(RoutePath.filterByProvider), + onViewFilter: () => props.history.push(RoutePath.filter), onSelectExitLocation: async (relayLocation: RelayLocation) => { // dismiss the view first props.history.dismiss(); @@ -118,26 +120,67 @@ const mapDispatchToProps = (_dispatch: ReduxDispatch, props: IHistoryProps & IAp onClearProviders: async () => { await props.app.updateRelaySettings({ normal: { providers: [] } }); }, + onClearOwnership: async () => { + await props.app.updateRelaySettings({ normal: { ownership: Ownership.any } }); + }, }; }; +function filterLocations( + locations: IRelayLocationRedux[], + providers: string[], + ownership: Ownership, +): IRelayLocationRedux[] { + const locationsFilteredByOwnership = filterLocationsByOwnership(locations, ownership); + const locationsFilteredByProvider = filterLocationsByProvider( + locationsFilteredByOwnership, + providers, + ); + + return locationsFilteredByProvider; +} + +function filterLocationsByOwnership( + locations: IRelayLocationRedux[], + ownership: Ownership, +): IRelayLocationRedux[] { + if (ownership === Ownership.any) { + return locations; + } + + const expectOwned = ownership === Ownership.mullvadOwned; + return locations + .map((country) => ({ + ...country, + cities: country.cities + .map((city) => ({ + ...city, + relays: city.relays.filter((relay) => relay.owned === expectOwned), + })) + .filter((city) => city.relays.length > 0), + })) + .filter((country) => country.cities.length > 0); +} + function filterLocationsByProvider( locations: IRelayLocationRedux[], providers: string[], ): IRelayLocationRedux[] { - return providers.length === 0 - ? locations - : locations - .map((country) => ({ - ...country, - cities: country.cities - .map((city) => ({ - ...city, - relays: city.relays.filter((relay) => providers.includes(relay.provider)), - })) - .filter((city) => city.relays.length > 0), + if (providers.length === 0) { + return locations; + } + + return locations + .map((country) => ({ + ...country, + cities: country.cities + .map((city) => ({ + ...city, + relays: city.relays.filter((relay) => providers.includes(relay.provider)), })) - .filter((country) => country.cities.length > 0); + .filter((city) => city.relays.length > 0), + })) + .filter((country) => country.cities.length > 0); } export default withAppContext( diff --git a/gui/src/renderer/lib/routes.ts b/gui/src/renderer/lib/routes.ts index 994a9f6124..641b48781d 100644 --- a/gui/src/renderer/lib/routes.ts +++ b/gui/src/renderer/lib/routes.ts @@ -22,7 +22,7 @@ export enum RoutePath { splitTunneling = '/settings/advanced/split-tunneling', support = '/settings/support', selectLocation = '/select-location', - filterByProvider = '/select-location/filter-by-provider', + filter = '/select-location/filter', } export const disableDismissForRoutes = [ diff --git a/gui/src/renderer/redux/settings/reducers.ts b/gui/src/renderer/redux/settings/reducers.ts index 1c759e1a52..8e4c922192 100644 --- a/gui/src/renderer/redux/settings/reducers.ts +++ b/gui/src/renderer/redux/settings/reducers.ts @@ -4,6 +4,7 @@ import { IDnsOptions, IpVersion, LiftedConstraint, + Ownership, ProxySettings, RelayLocation, RelayProtocol, @@ -18,6 +19,7 @@ export type RelaySettingsRedux = tunnelProtocol: LiftedConstraint<TunnelProtocol>; location: LiftedConstraint<RelayLocation>; providers: string[]; + ownership: Ownership; openvpn: { port: LiftedConstraint<number>; protocol: LiftedConstraint<RelayProtocol>; @@ -54,6 +56,7 @@ export interface IRelayLocationRelayRedux { ipv4AddrIn: string; includeInCountry: boolean; active: boolean; + owned: boolean; weight: number; } @@ -111,6 +114,7 @@ const initialState: ISettingsReduxState = { location: 'any', tunnelProtocol: 'any', providers: [], + ownership: Ownership.any, wireguard: { port: 'any', ipVersion: 'any', useMultihop: false, entryLocation: 'any' }, openvpn: { port: 'any', diff --git a/gui/src/shared/bridge-settings-builder.ts b/gui/src/shared/bridge-settings-builder.ts index fc4eeaa682..858bea055d 100644 --- a/gui/src/shared/bridge-settings-builder.ts +++ b/gui/src/shared/bridge-settings-builder.ts @@ -1,4 +1,4 @@ -import { BridgeSettings, IBridgeConstraints } from './daemon-rpc-types'; +import { BridgeSettings, IBridgeConstraints, Ownership } from './daemon-rpc-types'; import makeLocationBuilder, { ILocationBuilder } from './relay-location-builder'; export default class BridgeSettingsBuilder { @@ -10,6 +10,7 @@ export default class BridgeSettingsBuilder { normal: { location: this.payload.location, providers: this.payload.providers ?? [], + ownership: this.payload.ownership ?? Ownership.any, }, }; } else { diff --git a/gui/src/shared/daemon-rpc-types.ts b/gui/src/shared/daemon-rpc-types.ts index aad35a01f4..6e70c3f9b6 100644 --- a/gui/src/shared/daemon-rpc-types.ts +++ b/gui/src/shared/daemon-rpc-types.ts @@ -82,6 +82,12 @@ export function proxyTypeToString(proxy: ProxyType): string { } } +export enum Ownership { + any, + mullvadOwned, + rented, +} + export interface ITunnelEndpoint { address: string; protocol: RelayProtocol; @@ -159,6 +165,7 @@ interface IRelaySettingsNormal<OpenVpn, Wireguard> { location: Constraint<RelayLocation>; tunnelProtocol: Constraint<TunnelProtocol>; providers: string[]; + ownership: Ownership; openvpnConstraints: OpenVpn; wireguardConstraints: Wireguard; } @@ -241,6 +248,7 @@ export interface IRelayListHostname { includeInCountry: boolean; active: boolean; weight: number; + owned: boolean; tunnels?: IRelayTunnels; bridges?: IRelayBridges; } @@ -380,6 +388,7 @@ export type SplitTunnelSettings = { export interface IBridgeConstraints { location: Constraint<RelayLocation>; providers: string[]; + ownership: Ownership; } export type BridgeSettings = { normal: IBridgeConstraints } | { custom: ProxySettings }; diff --git a/gui/src/shared/localization-contexts.ts b/gui/src/shared/localization-contexts.ts index 5396d7ccc2..e03c679b61 100644 --- a/gui/src/shared/localization-contexts.ts +++ b/gui/src/shared/localization-contexts.ts @@ -15,8 +15,8 @@ export type LocalizationContexts = | 'account-expiry' | 'select-location-view' | 'select-location-nav' - | 'filter-by-provider-view' - | 'filter-by-provider-nav' + | 'filter-view' + | 'filter-nav' | 'settings-view' | 'navigation-bar' | 'account-view' |
