summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorOskar Nyberg <oskar@mullvad.net>2022-05-19 10:06:32 +0200
committerOskar Nyberg <oskar@mullvad.net>2022-05-19 10:06:32 +0200
commit9cca46231f623247fac50694a66fb5eedada15d8 (patch)
treea62db50609fe2679d1f1105558a4712f8300693d
parent0211a43e2f9e1b7beccaefd9a2a09672f1877fd8 (diff)
parenta39213f52d76ddf08ed0a637a0fe831b3581466e (diff)
downloadmullvadvpn-9cca46231f623247fac50694a66fb5eedada15d8.tar.xz
mullvadvpn-9cca46231f623247fac50694a66fb5eedada15d8.zip
Merge branch 'update-filters'
-rw-r--r--CHANGELOG.md3
-rw-r--r--gui/locales/da/messages.po6
-rw-r--r--gui/locales/de/messages.po6
-rw-r--r--gui/locales/es/messages.po6
-rw-r--r--gui/locales/fi/messages.po6
-rw-r--r--gui/locales/fr/messages.po6
-rw-r--r--gui/locales/it/messages.po6
-rw-r--r--gui/locales/ja/messages.po6
-rw-r--r--gui/locales/ko/messages.po6
-rw-r--r--gui/locales/messages.pot37
-rw-r--r--gui/locales/my/messages.po6
-rw-r--r--gui/locales/nb/messages.po6
-rw-r--r--gui/locales/nl/messages.po6
-rw-r--r--gui/locales/pl/messages.po6
-rw-r--r--gui/locales/pt/messages.po6
-rw-r--r--gui/locales/ru/messages.po6
-rw-r--r--gui/locales/sv/messages.po6
-rw-r--r--gui/locales/th/messages.po6
-rw-r--r--gui/locales/tr/messages.po6
-rw-r--r--gui/locales/zh-CN/messages.po6
-rw-r--r--gui/locales/zh-TW/messages.po6
-rw-r--r--gui/src/main/daemon-rpc.ts33
-rw-r--r--gui/src/main/index.ts3
-rw-r--r--gui/src/renderer/app.tsx2
-rw-r--r--gui/src/renderer/components/AppRouter.tsx4
-rw-r--r--gui/src/renderer/components/Filter.tsx274
-rw-r--r--gui/src/renderer/components/FilterByProvider.tsx209
-rw-r--r--gui/src/renderer/components/SelectLocation.tsx156
-rw-r--r--gui/src/renderer/components/SelectLocationStyles.tsx37
-rw-r--r--gui/src/renderer/containers/SelectLocationPage.tsx75
-rw-r--r--gui/src/renderer/lib/routes.ts2
-rw-r--r--gui/src/renderer/redux/settings/reducers.ts4
-rw-r--r--gui/src/shared/bridge-settings-builder.ts3
-rw-r--r--gui/src/shared/daemon-rpc-types.ts9
-rw-r--r--gui/src/shared/localization-contexts.ts4
35 files changed, 569 insertions, 400 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b33f678cd5..f6289720a7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -23,6 +23,9 @@ Line wrap the file at 100 chars. Th
## [Unreleased]
+### Added
+- Add option to filter relays by ownership in the desktop apps.
+
### Changed
#### Android
- Lowered default MTU to 1280 on Android.
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'