summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorOskar Nyberg <oskar@mullvad.net>2022-11-17 17:57:35 +0100
committerOskar Nyberg <oskar@mullvad.net>2022-11-24 16:26:28 +0100
commit4f8f400dd010235a3e52fc9294bebad06166f27e (patch)
tree33d95e84efe30945b14fdb4ecf5939f3a2ccca3d
parent83e0a8758668a803b3865d926579bb79d7df1415 (diff)
downloadmullvadvpn-4f8f400dd010235a3e52fc9294bebad06166f27e.tar.xz
mullvadvpn-4f8f400dd010235a3e52fc9294bebad06166f27e.zip
Add search field to select location view
-rw-r--r--gui/src/renderer/components/CustomScrollbars.tsx11
-rw-r--r--gui/src/renderer/components/Filter.tsx27
-rw-r--r--gui/src/renderer/components/select-location/LocationRow.tsx5
-rw-r--r--gui/src/renderer/components/select-location/SelectLocation.tsx80
-rw-r--r--gui/src/renderer/components/select-location/SelectLocationContainer.tsx27
-rw-r--r--gui/src/renderer/components/select-location/SelectLocationStyles.tsx3
-rw-r--r--gui/src/renderer/components/select-location/select-location-hooks.ts220
-rw-r--r--gui/src/renderer/lib/filter-locations.ts50
-rw-r--r--gui/src/renderer/lib/utilityHooks.ts25
9 files changed, 272 insertions, 176 deletions
diff --git a/gui/src/renderer/components/CustomScrollbars.tsx b/gui/src/renderer/components/CustomScrollbars.tsx
index a760647cbe..ab95d66511 100644
--- a/gui/src/renderer/components/CustomScrollbars.tsx
+++ b/gui/src/renderer/components/CustomScrollbars.tsx
@@ -137,17 +137,12 @@ class CustomScrollbars extends React.Component<IProps, IState> {
public scrollToTop(smooth = false) {
const scrollable = this.scrollableRef.current;
- if (scrollable) {
- scrollable.scrollTo({ top: 0, behavior: smooth ? 'smooth' : 'auto' });
- }
+ scrollable?.scrollTo({ top: 0, behavior: smooth ? 'smooth' : 'auto' });
}
- public scrollTo(x: number, y: number) {
+ public scrollTo(x: number, y: number, smooth = false) {
const scrollable = this.scrollableRef.current;
- if (scrollable) {
- scrollable.scrollLeft = x;
- scrollable.scrollTop = y;
- }
+ scrollable?.scrollTo({ top: y, left: x, behavior: smooth ? 'smooth' : 'auto' });
}
public scrollToElement(child: HTMLElement, scrollPosition: ScrollPosition) {
diff --git a/gui/src/renderer/components/Filter.tsx b/gui/src/renderer/components/Filter.tsx
index ef3cb84ef2..26ed781396 100644
--- a/gui/src/renderer/components/Filter.tsx
+++ b/gui/src/renderer/components/Filter.tsx
@@ -5,7 +5,11 @@ import { colors } from '../../config.json';
import { Ownership } from '../../shared/daemon-rpc-types';
import { messages } from '../../shared/gettext';
import { useAppContext } from '../context';
-import { EndpointType, filterLocations } from '../lib/filter-locations';
+import {
+ EndpointType,
+ filterLocations,
+ filterLocationsByEndPointType,
+} from '../lib/filter-locations';
import { useHistory } from '../lib/history';
import { useBoolean, useNormalRelaySettings } from '../lib/utilityHooks';
import { IRelayLocationRedux } from '../redux/settings/reducers';
@@ -118,19 +122,24 @@ function useFilteredFilters(providers: string[], ownership: Ownership) {
const endpointType = bridgeState === 'on' ? EndpointType.any : EndpointType.exit;
const availableProviders = useMemo(() => {
- const filteredRelays = filterLocations(locations, endpointType, relaySettings, ownership, []);
- return providersFromRelays(filteredRelays);
+ const relayListForEndpointType = filterLocationsByEndPointType(
+ locations,
+ endpointType,
+ relaySettings,
+ );
+ const relaylistForFilters = filterLocations(relayListForEndpointType, ownership, []);
+ return providersFromRelays(relaylistForFilters);
}, [locations, ownership]);
const availableOwnershipOptions = useMemo(() => {
- const filteredRelays = filterLocations(
+ const relayListForEndpointType = filterLocationsByEndPointType(
locations,
endpointType,
relaySettings,
- Ownership.any,
- providers,
);
- const filteredRelayOwnership = filteredRelays.flatMap((country) =>
+ const relaylistForFilters = filterLocations(relayListForEndpointType, Ownership.any, providers);
+
+ const filteredRelayOwnership = relaylistForFilters.flatMap((country) =>
country.cities.flatMap((city) => city.relays.map((relay) => relay.owned)),
);
@@ -162,12 +171,10 @@ function providersSelector(state: IReduxState): Record<string, boolean> {
const providerConstraint = relaySettings?.providers ?? [];
const endpointType = state.settings.bridgeState === 'on' ? EndpointType.any : EndpointType.exit;
- const relays = filterLocations(
+ const relays = filterLocationsByEndPointType(
state.settings.relayLocations,
endpointType,
relaySettings,
- Ownership.any,
- [],
);
const providers = providersFromRelays(relays);
diff --git a/gui/src/renderer/components/select-location/LocationRow.tsx b/gui/src/renderer/components/select-location/LocationRow.tsx
index 17261d79e4..56a16b84f9 100644
--- a/gui/src/renderer/components/select-location/LocationRow.tsx
+++ b/gui/src/renderer/components/select-location/LocationRow.tsx
@@ -119,10 +119,12 @@ interface IProps<C extends LocationSpecification> {
function LocationRow<C extends LocationSpecification>(props: IProps<C>) {
const hasChildren = React.Children.count(props.children) > 0;
const buttonRef = useRef<HTMLButtonElement>() as React.RefObject<HTMLButtonElement>;
+ const recentClickRef = useRef(false);
const expanded = 'expanded' in props.source ? props.source.expanded : undefined;
const toggleCollapse = useCallback(() => {
if (expanded !== undefined) {
+ recentClickRef.current = true;
const callback = expanded ? props.onCollapse : props.onExpand;
callback(props.source.location);
}
@@ -137,8 +139,9 @@ function LocationRow<C extends LocationSpecification>(props: IProps<C>) {
const onWillExpand = useCallback(
(nextHeight: number) => {
const buttonRect = buttonRef.current?.getBoundingClientRect();
- if (expanded !== undefined && buttonRect) {
+ if (expanded !== undefined && buttonRect && recentClickRef.current) {
props.onWillExpand(buttonRect, nextHeight);
+ recentClickRef.current = false;
}
},
[props.onWillExpand],
diff --git a/gui/src/renderer/components/select-location/SelectLocation.tsx b/gui/src/renderer/components/select-location/SelectLocation.tsx
index 66d70bc174..660bdacafa 100644
--- a/gui/src/renderer/components/select-location/SelectLocation.tsx
+++ b/gui/src/renderer/components/select-location/SelectLocation.tsx
@@ -1,4 +1,4 @@
-import React, { useCallback, useEffect, useRef } from 'react';
+import { useCallback, useEffect } from 'react';
import { sprintf } from 'sprintf-js';
import { colors } from '../../../config.json';
@@ -9,7 +9,6 @@ import { useHistory } from '../../lib/history';
import { RoutePath } from '../../lib/routes';
import { useNormalBridgeSettings, useNormalRelaySettings } from '../../lib/utilityHooks';
import { useSelector } from '../../redux/store';
-import { CustomScrollbarsRef } from '../CustomScrollbars';
import ImageView from '../ImageView';
import { BackAction } from '../KeyboardNavigation';
import { Layout, SettingsContainer } from '../Layout';
@@ -43,16 +42,15 @@ import {
StyledFilterIconButton,
StyledFilterRow,
StyledNavigationBarAttachment,
+ StyledSearchBar,
} from './SelectLocationStyles';
import { SpacePreAllocationView } from './SpacePreAllocationView';
export default function SelectLocation() {
const history = useHistory();
const { updateRelaySettings } = useAppContext();
- const { scrollViewRef, saveScrollPosition, resetScrollPositions } = useScrollPosition();
- const { resetExpandedLocations } = useExpandedLocations();
- const spacePreAllocationViewRef = useRef() as React.RefObject<SpacePreAllocationView>;
- const { locationType, setLocationType } = useSelectLocationContext();
+ const { saveScrollPosition, resetScrollPositions, applyScrollPosition } = useScrollPosition();
+ const { scrollViewRef, spacePreAllocationViewRef, locationType, setLocationType, searchTerm, setSearchTerm } = useSelectLocationContext();
const relaySettings = useNormalRelaySettings();
const ownership = relaySettings?.ownership ?? Ownership.any;
@@ -69,13 +67,11 @@ export default function SelectLocation() {
const onClearProviders = useCallback(async () => {
resetScrollPositions();
- resetExpandedLocations();
await updateRelaySettings({ normal: { providers: [] } });
}, []);
const onClearOwnership = useCallback(async () => {
resetScrollPositions();
- resetExpandedLocations();
await updateRelaySettings({ normal: { ownership: Ownership.any } });
}, []);
@@ -87,6 +83,16 @@ export default function SelectLocation() {
[saveScrollPosition],
);
+ const updateSearchTerm = useCallback(
+ (value: string) => {
+ resetScrollPositions();
+ setSearchTerm(value);
+ },
+ [resetScrollPositions],
+ );
+
+ useEffect(applyScrollPosition, [applyScrollPosition]);
+
const showOwnershipFilter = ownership !== Ownership.any;
const showProvidersFilter = providers.length > 0;
const showFilters = showOwnershipFilter || showProvidersFilter;
@@ -171,15 +177,14 @@ export default function SelectLocation() {
)}
</StyledFilterRow>
)}
+
+ <StyledSearchBar searchTerm={searchTerm} onSearch={updateSearchTerm} />
</StyledNavigationBarAttachment>
<NavigationScrollbars ref={scrollViewRef}>
<SpacePreAllocationView ref={spacePreAllocationViewRef}>
<StyledContent>
- <SelectLocationContent
- spacePreAllocationViewRef={spacePreAllocationViewRef}
- scrollViewRef={scrollViewRef}
- />
+ <SelectLocationContent />
</StyledContent>
</SpacePreAllocationView>
</NavigationScrollbars>
@@ -201,30 +206,20 @@ function ownershipFilterLabel(ownership: Ownership): string {
}
}
-interface SelectLocationContentProps {
- spacePreAllocationViewRef: React.RefObject<SpacePreAllocationView>;
- scrollViewRef: React.RefObject<CustomScrollbarsRef>;
-}
-
-function SelectLocationContent(props: SelectLocationContentProps) {
- const { locationType, selectedLocationRef } = useSelectLocationContext();
+function SelectLocationContent() {
+ const { locationType, selectedLocationRef, spacePreAllocationViewRef } = useSelectLocationContext();
const relayList = useRelayList();
- const { expandLocation, collapseLocation } = useExpandedLocations();
+ const { expandLocation, collapseLocation, updateExpandedLocations } = useExpandedLocations();
+ const { onBeforeExpand } = useScrollPosition();
const onSelectLocation = useOnSelectLocation();
const onSelectBridgeLocation = useOnSelectBridgeLocation();
const relaySettings = useNormalRelaySettings();
const bridgeSettings = useNormalBridgeSettings();
- const onWillExpand = useCallback((locationRect: DOMRect, expandedContentHeight: number) => {
- locationRect.height += expandedContentHeight;
- props.spacePreAllocationViewRef.current?.allocate(expandedContentHeight);
- props.scrollViewRef.current?.scrollIntoView(locationRect);
- }, []);
+ const resetHeight = useCallback(() => spacePreAllocationViewRef.current?.reset(), []);
- const resetHeight = useCallback(() => {
- props.spacePreAllocationViewRef.current?.reset();
- }, []);
+ useEffect(updateExpandedLocations, [updateExpandedLocations]);
if (locationType === LocationType.exit) {
return (
@@ -235,7 +230,7 @@ function SelectLocationContent(props: SelectLocationContentProps) {
onSelect={onSelectLocation}
onExpand={expandLocation}
onCollapse={collapseLocation}
- onWillExpand={onWillExpand}
+ onWillExpand={onBeforeExpand}
onTransitionEnd={resetHeight}
/>
);
@@ -248,7 +243,7 @@ function SelectLocationContent(props: SelectLocationContentProps) {
onSelect={onSelectLocation}
onExpand={expandLocation}
onCollapse={collapseLocation}
- onWillExpand={onWillExpand}
+ onWillExpand={onBeforeExpand}
onTransitionEnd={resetHeight}
/>
);
@@ -275,7 +270,7 @@ function SelectLocationContent(props: SelectLocationContentProps) {
onSelect={onSelectBridgeLocation}
onExpand={expandLocation}
onCollapse={collapseLocation}
- onWillExpand={onWillExpand}
+ onWillExpand={onBeforeExpand}
onTransitionEnd={resetHeight}
/>
);
@@ -283,17 +278,12 @@ function SelectLocationContent(props: SelectLocationContentProps) {
}
function useScrollPosition() {
- const {
- activeFilter,
- locationType,
- scrollPositions,
- selectedLocationRef,
- } = useSelectLocationContext();
- const scrollViewRef = useRef<CustomScrollbarsRef>(null);
+ const { locationType, scrollPositions, scrollViewRef, spacePreAllocationViewRef, selectedLocationRef, searchTerm } = useSelectLocationContext();
+ const relaySettings = useNormalRelaySettings();
const saveScrollPosition = useCallback(() => {
const scrollPosition = scrollViewRef.current?.getScrollPosition();
- if (scrollPositions.current) {
+ if (scrollPositions.current && scrollPosition) {
scrollPositions.current[locationType] = scrollPosition;
}
}, [locationType]);
@@ -309,7 +299,7 @@ function useScrollPosition() {
}
}, [locationType]);
- useEffect(() => {
+ const applyScrollPosition = useCallback(() => {
const scrollPosition = scrollPositions.current?.[locationType];
if (scrollPosition) {
scrollViewRef.current?.scrollTo(...scrollPosition);
@@ -318,7 +308,13 @@ function useScrollPosition() {
} else {
scrollViewRef.current?.scrollToTop();
}
- }, [locationType, activeFilter]);
+ }, [locationType, searchTerm, relaySettings?.ownership, relaySettings?.providers]);
+
+ const onBeforeExpand = useCallback((locationRect: DOMRect, expandedContentHeight: number) => {
+ locationRect.height += expandedContentHeight;
+ spacePreAllocationViewRef.current?.allocate(expandedContentHeight);
+ scrollViewRef.current?.scrollIntoView(locationRect);
+ }, []);
- return { scrollViewRef, saveScrollPosition, resetScrollPositions };
+ return { spacePreAllocationViewRef, saveScrollPosition, resetScrollPositions, applyScrollPosition, onBeforeExpand };
}
diff --git a/gui/src/renderer/components/select-location/SelectLocationContainer.tsx b/gui/src/renderer/components/select-location/SelectLocationContainer.tsx
index 71e898a229..4f64aa79f3 100644
--- a/gui/src/renderer/components/select-location/SelectLocationContainer.tsx
+++ b/gui/src/renderer/components/select-location/SelectLocationContainer.tsx
@@ -1,10 +1,12 @@
import React, { useContext, useMemo, useRef, useState } from 'react';
-import { Ownership, RelayLocation } from '../../../shared/daemon-rpc-types';
+import { RelayLocation } from '../../../shared/daemon-rpc-types';
import { useNormalBridgeSettings, useNormalRelaySettings } from '../../lib/utilityHooks';
+import {CustomScrollbarsRef} from '../CustomScrollbars';
import { defaultExpandedLocations } from './select-location-helpers';
import { LocationType } from './select-location-types';
import SelectLocation from './SelectLocation';
+import {SpacePreAllocationView} from './SpacePreAllocationView';
type ExpandedLocations = Partial<Record<LocationType, Array<RelayLocation>>>;
type ScrollPosition = [number, number];
@@ -12,13 +14,16 @@ type ScrollPosition = [number, number];
interface SelectLocationContext {
locationType: LocationType;
setLocationType: (locationType: LocationType) => void;
- activeFilter: boolean;
+ searchTerm: string;
+ setSearchTerm: (value: string) => void;
expandedLocations: ExpandedLocations;
setExpandedLocations: (
arg: ExpandedLocations | ((prev: ExpandedLocations) => ExpandedLocations),
) => void;
scrollPositions: React.RefObject<Partial<Record<LocationType, ScrollPosition>>>;
selectedLocationRef: React.RefObject<HTMLDivElement>;
+ scrollViewRef: React.RefObject<CustomScrollbarsRef>;
+ spacePreAllocationViewRef: React.RefObject<SpacePreAllocationView>;
}
const selectLocationContext = React.createContext<SelectLocationContext | undefined>(undefined);
@@ -33,26 +38,36 @@ export default function SelectLocationContainer() {
const [locationType, setLocationType] = useState(LocationType.exit);
const selectedLocationRef = useRef<HTMLDivElement>(null);
+ const scrollViewRef = useRef<CustomScrollbarsRef>(null);
+ const spacePreAllocationViewRef = useRef() as React.RefObject<SpacePreAllocationView>;
const [expandedLocations, setExpandedLocations] = useState<
Partial<Record<LocationType, Array<RelayLocation>>>
>(() => defaultExpandedLocations(relaySettings, bridgeSettings));
const scrollPositions = useRef<Partial<Record<LocationType, ScrollPosition>>>({});
- const ownershipActive = relaySettings !== undefined && relaySettings.ownership !== Ownership.any;
- const providersActive = relaySettings !== undefined && relaySettings.providers.length > 0;
+ const [searchTerm, setSearchTerm] = useState('');
const value = useMemo(
() => ({
locationType,
setLocationType,
- activeFilter: ownershipActive || providersActive,
+ searchTerm,
+ setSearchTerm,
expandedLocations,
setExpandedLocations,
scrollPositions,
selectedLocationRef,
+ scrollViewRef,
+ spacePreAllocationViewRef,
}),
- [locationType, relaySettings?.ownership, relaySettings?.providers, expandedLocations],
+ [
+ locationType,
+ relaySettings?.ownership,
+ relaySettings?.providers,
+ expandedLocations,
+ searchTerm,
+ ],
);
return (
diff --git a/gui/src/renderer/components/select-location/SelectLocationStyles.tsx b/gui/src/renderer/components/select-location/SelectLocationStyles.tsx
index ba5c0c5f24..f1702d13dc 100644
--- a/gui/src/renderer/components/select-location/SelectLocationStyles.tsx
+++ b/gui/src/renderer/components/select-location/SelectLocationStyles.tsx
@@ -51,5 +51,6 @@ export const StyledClearFilterButton = styled.div({
});
export const StyledSearchBar = styled(SearchBar)({
- marginBottom: '14px',
+ marginTop: '10px',
+ marginBottom: '4px',
});
diff --git a/gui/src/renderer/components/select-location/select-location-hooks.ts b/gui/src/renderer/components/select-location/select-location-hooks.ts
index c42c5899c7..5bab2a429d 100644
--- a/gui/src/renderer/components/select-location/select-location-hooks.ts
+++ b/gui/src/renderer/components/select-location/select-location-hooks.ts
@@ -10,9 +10,19 @@ import log from '../../../shared/logging';
import RelaySettingsBuilder from '../../../shared/relay-settings-builder';
import { useAppContext } from '../../context';
import { createWireguardRelayUpdater } from '../../lib/constraint-updater';
-import { EndpointType, filterLocations } from '../../lib/filter-locations';
+import {
+ EndpointType,
+ filterLocations,
+ filterLocationsByEndPointType,
+ getLocationsExpandedBySearch,
+ searchForLocations,
+} from '../../lib/filter-locations';
import { useHistory } from '../../lib/history';
-import { useNormalBridgeSettings, useNormalRelaySettings } from '../../lib/utilityHooks';
+import {
+ useNormalBridgeSettings,
+ useNormalRelaySettings,
+ useSharedMemo,
+} from '../../lib/utilityHooks';
import { IRelayLocationRedux } from '../../redux/settings/reducers';
import { useSelector } from '../../redux/store';
import {
@@ -36,14 +46,38 @@ import { useSelectLocationContext } from './SelectLocationContainer';
// Return all locations that matches both the set filters and the search term.
function useFilteredRelays(): Array<IRelayLocationRedux> {
- const { locationType } = useSelectLocationContext();
+ const { locationType, searchTerm } = useSelectLocationContext();
const relayList = useSelector((state) => state.settings.relayLocations);
const relaySettings = useNormalRelaySettings();
- const endpointType = locationType === LocationType.entry ? EndpointType.entry : EndpointType.exit;
- const filteredRelayList = useMemo(
- () => (relaySettings ? filterLocations(relayList, endpointType, relaySettings) : relayList),
- [relaySettings, relayList, endpointType],
+ const relayListForEndpointType = useSharedMemo(
+ 'relay-list-endpoint-type',
+ () => {
+ const endpointType =
+ locationType === LocationType.entry ? EndpointType.entry : EndpointType.exit;
+ return filterLocationsByEndPointType(relayList, endpointType, relaySettings);
+ },
+ [relayList, locationType, relaySettings?.tunnelProtocol],
+ );
+
+ const relayListForFilters = useSharedMemo(
+ 'relay-list-filters',
+ () => {
+ return filterLocations(
+ relayListForEndpointType,
+ relaySettings?.ownership,
+ relaySettings?.providers,
+ );
+ },
+ [relaySettings?.ownership, relaySettings?.providers, relayListForEndpointType],
+ );
+
+ const filteredRelayList = useSharedMemo(
+ 'relay-list-search',
+ () => {
+ return searchForLocations(relayListForFilters, searchTerm);
+ },
+ [relayListForFilters, searchTerm],
);
return filteredRelayList;
@@ -53,12 +87,13 @@ function useFilteredRelays(): Array<IRelayLocationRedux> {
export function useExpandedLocations() {
const relaySettings = useNormalRelaySettings();
const bridgeSettings = useNormalBridgeSettings();
- const { locationType, expandedLocations, setExpandedLocations } = useSelectLocationContext();
-
- const expandedLocationsForType = useMemo(() => expandedLocations[locationType], [
- expandedLocations,
+ const {
locationType,
- ]);
+ expandedLocations,
+ setExpandedLocations,
+ searchTerm,
+ } = useSelectLocationContext();
+ const relayList = useFilteredRelays();
const expandLocation = useCallback(
(location: RelayLocation) => {
@@ -82,15 +117,22 @@ export function useExpandedLocations() {
[locationType],
);
- const resetExpandedLocations = useCallback(() => {
- setExpandedLocations(defaultExpandedLocations(relaySettings, bridgeSettings));
- }, [relaySettings, bridgeSettings]);
+ const updateExpandedLocations = useCallback(() => {
+ if (searchTerm === '') {
+ setExpandedLocations(defaultExpandedLocations(relaySettings, bridgeSettings));
+ } else {
+ setExpandedLocations((expandedLocations) => ({
+ ...expandedLocations,
+ [locationType]: getLocationsExpandedBySearch(relayList, searchTerm),
+ }));
+ }
+ }, [relayList, searchTerm, relaySettings?.ownership, relaySettings?.providers]);
return {
- expandedLocations: expandedLocationsForType,
+ expandedLocations: expandedLocations[locationType],
expandLocation,
collapseLocation,
- resetExpandedLocations,
+ updateExpandedLocations,
};
}
@@ -103,77 +145,81 @@ export function useRelayList(): LocationList<never> {
const selectedLocation = useSelectedLocation();
const disabledLocation = useDisabledLocation();
- return relayList
- .map((country) => {
- const countryLocation = { country: country.code };
- const countryDisabled = isCountryDisabled(country, countryLocation.country, disabledLocation);
+ return useSharedMemo('relay-list-formatted', () => {
+ return relayList
+ .map((country) => {
+ const countryLocation = { country: country.code };
+ const countryDisabled = isCountryDisabled(country, countryLocation.country, disabledLocation);
- return {
- ...country,
- type: LocationSelectionType.relay as const,
- label: formatRowName(country.name, countryLocation, countryDisabled),
- location: countryLocation,
- active: countryDisabled !== DisabledReason.inactive,
- disabled: countryDisabled !== undefined,
- expanded: isExpanded(countryLocation, expandedLocations),
- selected: isSelected(countryLocation, selectedLocation),
- cities: country.cities
- .map((city) => {
- const cityLocation: RelayLocation = { city: [country.code, city.code] };
- const cityDisabled =
- countryDisabled ?? isCityDisabled(city, cityLocation.city, disabledLocation);
+ return {
+ ...country,
+ type: LocationSelectionType.relay as const,
+ label: formatRowName(country.name, countryLocation, countryDisabled),
+ location: countryLocation,
+ active: countryDisabled !== DisabledReason.inactive,
+ disabled: countryDisabled !== undefined,
+ expanded: isExpanded(countryLocation, expandedLocations),
+ selected: isSelected(countryLocation, selectedLocation),
+ cities: country.cities
+ .map((city) => {
+ const cityLocation: RelayLocation = { city: [country.code, city.code] };
+ const cityDisabled =
+ countryDisabled ?? isCityDisabled(city, cityLocation.city, disabledLocation);
- return {
- ...city,
- label: formatRowName(city.name, cityLocation, cityDisabled),
- location: cityLocation,
- active: cityDisabled !== DisabledReason.inactive,
- disabled: cityDisabled !== undefined,
- expanded: isExpanded(cityLocation, expandedLocations),
- selected: isSelected(cityLocation, selectedLocation),
- relays: city.relays
- .map((relay) => {
- const relayLocation: RelayLocation = {
- hostname: [country.code, city.code, relay.hostname],
- };
- const relayDisabled =
- countryDisabled ??
- cityDisabled ??
- isRelayDisabled(relay, relayLocation.hostname, disabledLocation);
+ return {
+ ...city,
+ label: formatRowName(city.name, cityLocation, cityDisabled),
+ location: cityLocation,
+ active: cityDisabled !== DisabledReason.inactive,
+ disabled: cityDisabled !== undefined,
+ expanded: isExpanded(cityLocation, expandedLocations),
+ selected: isSelected(cityLocation, selectedLocation),
+ relays: city.relays
+ .map((relay) => {
+ const relayLocation: RelayLocation = {
+ hostname: [country.code, city.code, relay.hostname],
+ };
+ const relayDisabled =
+ countryDisabled ??
+ cityDisabled ??
+ isRelayDisabled(relay, relayLocation.hostname, disabledLocation);
- return {
- ...relay,
- label: formatRowName(relay.hostname, relayLocation, relayDisabled),
- location: relayLocation,
- disabled: relayDisabled !== undefined,
- selected: isSelected(relayLocation, selectedLocation),
- };
- })
- .sort((a, b) => a.hostname.localeCompare(b.hostname, locale, { numeric: true })),
- };
- })
- .sort((a, b) => a.label.localeCompare(b.label, locale)),
- };
- })
- .sort((a, b) => a.label.localeCompare(b.label, locale));
+ return {
+ ...relay,
+ label: formatRowName(relay.hostname, relayLocation, relayDisabled),
+ location: relayLocation,
+ disabled: relayDisabled !== undefined,
+ selected: isSelected(relayLocation, selectedLocation),
+ };
+ })
+ .sort((a, b) => a.hostname.localeCompare(b.hostname, locale, { numeric: true })),
+ };
+ })
+ .sort((a, b) => a.label.localeCompare(b.label, locale)),
+ };
+ })
+ .sort((a, b) => a.label.localeCompare(b.label, locale));
+ }, [locale, expandedLocations, relayList, selectedLocation, disabledLocation]);
}
function useDisabledLocation() {
const { locationType } = useSelectLocationContext();
const relaySettings = useNormalRelaySettings();
- if (relaySettings?.tunnelProtocol !== 'openvpn' && relaySettings?.wireguard.useMultihop) {
- if (locationType === LocationType.exit && relaySettings?.wireguard.entryLocation !== 'any') {
- return {
- location: relaySettings?.wireguard.entryLocation,
- reason: DisabledReason.entry,
- };
- } else if (locationType === LocationType.entry && relaySettings?.location !== 'any') {
- return { location: relaySettings?.location, reason: DisabledReason.exit };
+ return useMemo(() => {
+ if (relaySettings?.tunnelProtocol !== 'openvpn' && relaySettings?.wireguard.useMultihop) {
+ if (locationType === LocationType.exit && relaySettings?.wireguard.entryLocation !== 'any') {
+ return {
+ location: relaySettings?.wireguard.entryLocation,
+ reason: DisabledReason.entry,
+ };
+ } else if (locationType === LocationType.entry && relaySettings?.location !== 'any') {
+ return { location: relaySettings?.location, reason: DisabledReason.exit };
+ }
}
- }
- return undefined;
+ return undefined;
+ }, [locationType, relaySettings?.tunnelProtocol, relaySettings?.wireguard.useMultihop, relaySettings?.wireguard.entryLocation, relaySettings?.location]);
}
// Returns the selected location for the current tunnel protocol and location type
@@ -182,15 +228,17 @@ function useSelectedLocation() {
const relaySettings = useNormalRelaySettings();
const bridgeSettings = useNormalBridgeSettings();
- if (locationType === LocationType.exit) {
- return relaySettings?.location === 'any' ? undefined : relaySettings?.location;
- } else if (relaySettings?.tunnelProtocol !== 'openvpn') {
- return relaySettings?.wireguard.entryLocation === 'any'
- ? undefined
- : relaySettings?.wireguard.entryLocation;
- } else {
- return bridgeSettings?.location;
- }
+ return useMemo(() => {
+ if (locationType === LocationType.exit) {
+ return relaySettings?.location === 'any' ? undefined : relaySettings?.location;
+ } else if (relaySettings?.tunnelProtocol !== 'openvpn') {
+ return relaySettings?.wireguard.entryLocation === 'any'
+ ? undefined
+ : relaySettings?.wireguard.entryLocation;
+ } else {
+ return bridgeSettings?.location;
+ }
+ }, [locationType, relaySettings?.location, relaySettings?.tunnelProtocol, relaySettings?.wireguard.entryLocation, bridgeSettings?.location]);
}
export function useOnSelectLocation() {
diff --git a/gui/src/renderer/lib/filter-locations.ts b/gui/src/renderer/lib/filter-locations.ts
index c1ea17106f..5352df41bd 100644
--- a/gui/src/renderer/lib/filter-locations.ts
+++ b/gui/src/renderer/lib/filter-locations.ts
@@ -12,25 +12,30 @@ export enum EndpointType {
exit,
}
-export function filterLocations(
+export function filterLocationsByEndPointType(
locations: IRelayLocationRedux[],
endpointType: EndpointType,
relaySettings?: NormalRelaySettingsRedux,
+): IRelayLocationRedux[] {
+ return filterLocationsImpl(locations, getTunnelProtocolFilter(endpointType, relaySettings));
+}
+
+export function filterLocations(
+ locations: IRelayLocationRedux[],
ownership?: Ownership,
providers?: Array<string>,
): IRelayLocationRedux[] {
- const byTunnelProtocol = filterByTunnelProtocol(locations, endpointType, relaySettings);
- const byOwnership = filterByOwnership(byTunnelProtocol, ownership ?? relaySettings?.ownership);
- const byProvider = filterByProvider(byOwnership, providers ?? relaySettings?.providers);
+ const filters = [getOwnershipFilter(ownership), getProviderFilter(providers)];
- return byProvider;
+ return filters.some((filter) => filter !== undefined)
+ ? filterLocationsImpl(locations, (relay) => filters.every((filter) => filter?.(relay) ?? true))
+ : locations;
}
-function filterByTunnelProtocol(
- locations: IRelayLocationRedux[],
+function getTunnelProtocolFilter(
endpointType: EndpointType,
relaySettings?: NormalRelaySettingsRedux,
-) {
+): (relay: IRelayLocationRelayRedux) => boolean {
const tunnelProtocol = relaySettings?.tunnelProtocol ?? 'any';
const endpointTypes: Array<RelayEndpointType> = [];
if (endpointType !== EndpointType.exit && tunnelProtocol === 'openvpn') {
@@ -44,28 +49,26 @@ function filterByTunnelProtocol(
endpointTypes.push(tunnelProtocol);
}
- return filterLocationsImpl(locations, (relay) => endpointTypes.includes(relay.endpointType));
+ return (relay) => endpointTypes.includes(relay.endpointType);
}
-function filterByOwnership(
- locations: IRelayLocationRedux[],
+function getOwnershipFilter(
ownership?: Ownership,
-): IRelayLocationRedux[] {
+): ((relay: IRelayLocationRelayRedux) => boolean) | undefined {
if (ownership === undefined || ownership === Ownership.any) {
- return locations;
+ return undefined;
}
const expectOwned = ownership === Ownership.mullvadOwned;
- return filterLocationsImpl(locations, (relay) => relay.owned === expectOwned);
+ return (relay) => relay.owned === expectOwned;
}
-function filterByProvider(
- locations: IRelayLocationRedux[],
+function getProviderFilter(
providers?: string[],
-): IRelayLocationRedux[] {
+): ((relay: IRelayLocationRelayRedux) => boolean) | undefined {
return providers === undefined || providers.length === 0
- ? locations
- : filterLocationsImpl(locations, (relay) => providers.includes(relay.provider));
+ ? undefined
+ : (relay) => providers.includes(relay.provider);
}
function filterLocationsImpl(
@@ -89,7 +92,7 @@ export function searchForLocations(
return countries.reduce((countries, country) => {
const matchingCities = searchCities(country.cities, searchTerm);
const expanded = matchingCities.length > 0;
- const match = search(country.code, searchTerm) || search(country.name, searchTerm);
+ const match = search(searchTerm, country.code) || search(searchTerm, country.name);
const resultingCities = match ? country.cities : matchingCities;
return expanded || match ? [...countries, { ...country, cities: resultingCities }] : countries;
}, [] as Array<IRelayLocationRedux>);
@@ -102,7 +105,7 @@ function searchCities(
return cities.reduce((cities, city) => {
const matchingRelays = city.relays.filter((relay) => search(searchTerm, relay.hostname));
const expanded = matchingRelays.length > 0;
- const match = search(city.code, searchTerm) || search(city.name, searchTerm);
+ const match = search(searchTerm, city.code) || search(searchTerm, city.name);
const resultingRelays = match ? city.relays : matchingRelays;
return expanded || match ? [...cities, { ...city, relays: resultingRelays }] : cities;
}, [] as Array<IRelayLocationCityRedux>);
@@ -118,8 +121,11 @@ export function getLocationsExpandedBySearch(
country.code,
searchTerm,
);
+ const cityMatches = country.cities.some(
+ (city) => search(searchTerm, city.code) || search(searchTerm, city.name),
+ );
const location = { country: country.code };
- const expanded = cityLocations.length > 0;
+ const expanded = cityMatches || cityLocations.length > 0;
return expanded ? [...locations, ...cityLocations, location] : locations;
}, [] as Array<RelayLocation>);
}
diff --git a/gui/src/renderer/lib/utilityHooks.ts b/gui/src/renderer/lib/utilityHooks.ts
index 378a6d5ae5..4c66937393 100644
--- a/gui/src/renderer/lib/utilityHooks.ts
+++ b/gui/src/renderer/lib/utilityHooks.ts
@@ -65,3 +65,28 @@ export function useNormalBridgeSettings() {
const bridgeSettings = useSelector((state) => state.settings.bridgeSettings);
return 'normal' in bridgeSettings ? bridgeSettings.normal : undefined;
}
+
+const sharedMemoData: Record<
+ string,
+ { value: unknown; dependencies: Array<unknown> | undefined }
+> = {};
+export function useSharedMemo<T>(
+ key: string,
+ factory: () => T,
+ dependencies: Array<unknown> | undefined,
+): T {
+ const data = sharedMemoData[key];
+ if (
+ data === undefined ||
+ data.dependencies === undefined ||
+ dependencies === undefined ||
+ data.dependencies.length !== dependencies.length ||
+ data.dependencies.some((item, i) => item !== dependencies[i])
+ ) {
+ const value = factory();
+ sharedMemoData[key] = { value, dependencies };
+ return value;
+ } else {
+ return data.value as T;
+ }
+}