diff options
| author | Oskar Nyberg <oskar@mullvad.net> | 2022-11-21 13:15:16 +0100 |
|---|---|---|
| committer | Oskar Nyberg <oskar@mullvad.net> | 2022-11-24 16:26:28 +0100 |
| commit | b3a27fdbdd56fec4255b785c73d702304f1e8180 (patch) | |
| tree | 3d63c408aef04d35939ccf96b180c295cfeb6868 | |
| parent | 4f8f400dd010235a3e52fc9294bebad06166f27e (diff) | |
| download | mullvadvpn-b3a27fdbdd56fec4255b785c73d702304f1e8180.tar.xz mullvadvpn-b3a27fdbdd56fec4255b785c73d702304f1e8180.zip | |
Move relay list logic to RelayListContext and scroll logic to ScrollPositionContext
6 files changed, 391 insertions, 360 deletions
diff --git a/gui/src/renderer/components/select-location/RelayListContext.tsx b/gui/src/renderer/components/select-location/RelayListContext.tsx new file mode 100644 index 0000000000..fcef3c8a99 --- /dev/null +++ b/gui/src/renderer/components/select-location/RelayListContext.tsx @@ -0,0 +1,277 @@ +import React, { useCallback, useContext, useEffect, useMemo, useState } from 'react'; + +import { compareRelayLocation, RelayLocation } from '../../../shared/daemon-rpc-types'; +import { + EndpointType, + filterLocations, + filterLocationsByEndPointType, + getLocationsExpandedBySearch, + searchForLocations, +} from '../../lib/filter-locations'; +import { useNormalBridgeSettings, useNormalRelaySettings } from '../../lib/utilityHooks'; +import { IRelayLocationRedux } from '../../redux/settings/reducers'; +import { useSelector } from '../../redux/store'; +import { useScrollPositionContext } from './ScrollPositionContext'; +import { + defaultExpandedLocations, + formatRowName, + isCityDisabled, + isCountryDisabled, + isExpanded, + isRelayDisabled, + isSelected, +} from './select-location-helpers'; +import { + DisabledReason, + LocationList, + LocationSelectionType, + LocationType, +} from './select-location-types'; +import { useSelectLocationContext } from './SelectLocationContainer'; + +interface RelayListContext { + relayList: LocationList<never>; + expandLocation: (location: RelayLocation) => void; + collapseLocation: (location: RelayLocation) => void; + onBeforeExpand: (locationRect: DOMRect, expandedContentHeight: number) => void; +} + +type ExpandedLocations = Partial<Record<LocationType, Array<RelayLocation>>>; + +const relayListContext = React.createContext<RelayListContext | undefined>(undefined); + +export function useRelayListContext() { + return useContext(relayListContext)!; +} + +interface RelayListContextProviderProps { + children: React.ReactNode; +} + +export function RelayListContextProvider(props: RelayListContextProviderProps) { + const { locationType, searchTerm } = useSelectLocationContext(); + const fullRelayList = useSelector((state) => state.settings.relayLocations); + const relaySettings = useNormalRelaySettings(); + const bridgeSettings = useNormalBridgeSettings(); + + const [expandedLocationsMap, setExpandedLocations] = useState<ExpandedLocations>(() => + defaultExpandedLocations(relaySettings, bridgeSettings), + ); + const { + expandedLocations, + expandLocation, + collapseLocation, + onBeforeExpand, + } = useExpandedLocations(expandedLocationsMap, setExpandedLocations); + + const relayListForEndpointType = useMemo(() => { + const endpointType = + locationType === LocationType.entry ? EndpointType.entry : EndpointType.exit; + return filterLocationsByEndPointType(fullRelayList, endpointType, relaySettings); + }, [fullRelayList, locationType, relaySettings?.tunnelProtocol]); + + const relayListForFilters = useMemo(() => { + return filterLocations( + relayListForEndpointType, + relaySettings?.ownership, + relaySettings?.providers, + ); + }, [relaySettings?.ownership, relaySettings?.providers, relayListForEndpointType]); + + const relayListForSearch = useMemo(() => { + return searchForLocations(relayListForFilters, searchTerm); + }, [relayListForFilters, searchTerm]); + + const relayList = useRelayList(relayListForSearch, expandedLocations); + + const value = useMemo( + () => ({ + relayList, + expandLocation, + collapseLocation, + onBeforeExpand, + }), + [relayList, expandLocation, collapseLocation, onBeforeExpand], + ); + + useEffect(() => { + if (searchTerm === '') { + setExpandedLocations(defaultExpandedLocations(relaySettings, bridgeSettings)); + } else { + setExpandedLocations((expandedLocations) => ({ + ...expandedLocations, + [locationType]: getLocationsExpandedBySearch(relayListForFilters, searchTerm), + })); + } + }, [relayListForFilters, searchTerm, relaySettings?.ownership, relaySettings?.providers]); + + return <relayListContext.Provider value={value}>{props.children}</relayListContext.Provider>; +} + +// Return the final filtered and formatted relay list. This should be the only place in the app +// where processing of the relay list is performed. +function useRelayList( + relayList: Array<IRelayLocationRedux>, + expandedLocations?: Array<RelayLocation>, +): LocationList<never> { + const locale = useSelector((state) => state.userInterface.locale); + const selectedLocation = useSelectedLocation(); + const disabledLocation = useDisabledLocation(); + + return useMemo(() => { + 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 { + ...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)); + }, [locale, expandedLocations, relayList, selectedLocation, disabledLocation]); +} + +// Return all RelayLocations that should be expanded +function useExpandedLocations( + expandedLocationsMap: ExpandedLocations, + setExpandedLocations: ( + arg: ExpandedLocations | ((prev: ExpandedLocations) => ExpandedLocations), + ) => void, +) { + const { locationType } = useSelectLocationContext(); + const { spacePreAllocationViewRef, scrollViewRef } = useScrollPositionContext(); + + const expandLocation = useCallback( + (location: RelayLocation) => { + setExpandedLocations((expandedLocations) => ({ + ...expandedLocations, + [locationType]: [...(expandedLocationsMap[locationType] ?? []), location], + })); + }, + [locationType], + ); + + const collapseLocation = useCallback( + (location: RelayLocation) => { + setExpandedLocations((expandedLocations) => ({ + ...expandedLocations, + [locationType]: expandedLocationsMap[locationType]!.filter( + (item) => !compareRelayLocation(location, item), + ), + })); + }, + [locationType], + ); + + const onBeforeExpand = useCallback((locationRect: DOMRect, expandedContentHeight: number) => { + locationRect.height += expandedContentHeight; + spacePreAllocationViewRef.current?.allocate(expandedContentHeight); + scrollViewRef.current?.scrollIntoView(locationRect); + }, []); + + return { + expandedLocations: expandedLocationsMap[locationType], + expandLocation, + collapseLocation, + onBeforeExpand, + }; +} + +function useDisabledLocation() { + const { locationType } = useSelectLocationContext(); + const relaySettings = useNormalRelaySettings(); + + 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; + }, [ + locationType, + relaySettings?.tunnelProtocol, + relaySettings?.wireguard.useMultihop, + relaySettings?.wireguard.entryLocation, + relaySettings?.location, + ]); +} + +// Returns the selected location for the current tunnel protocol and location type +function useSelectedLocation() { + const { locationType } = useSelectLocationContext(); + const relaySettings = useNormalRelaySettings(); + const bridgeSettings = useNormalBridgeSettings(); + + 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, + ]); +} diff --git a/gui/src/renderer/components/select-location/ScrollPositionContext.tsx b/gui/src/renderer/components/select-location/ScrollPositionContext.tsx new file mode 100644 index 0000000000..63efe06b47 --- /dev/null +++ b/gui/src/renderer/components/select-location/ScrollPositionContext.tsx @@ -0,0 +1,83 @@ +import React, { useCallback, useContext, useEffect, useMemo, useRef } from 'react'; + +import { useNormalRelaySettings } from '../../lib/utilityHooks'; +import { CustomScrollbarsRef } from '../CustomScrollbars'; +import { LocationType } from './select-location-types'; +import { useSelectLocationContext } from './SelectLocationContainer'; +import { SpacePreAllocationView } from './SpacePreAllocationView'; + +interface ScrollPositionContext { + scrollPositions: React.RefObject<Partial<Record<LocationType, ScrollPosition>>>; + selectedLocationRef: React.RefObject<HTMLDivElement>; + scrollViewRef: React.RefObject<CustomScrollbarsRef>; + spacePreAllocationViewRef: React.RefObject<SpacePreAllocationView>; + saveScrollPosition: () => void; + resetScrollPositions: () => void; +} + +type ScrollPosition = [number, number]; + +const scrollPositionContext = React.createContext<ScrollPositionContext | undefined>(undefined); + +export function useScrollPositionContext() { + return useContext(scrollPositionContext)!; +} + +interface ScrollPositionContextProps { + children: React.ReactNode; +} + +export function ScrollPositionContextProvider(props: ScrollPositionContextProps) { + const { locationType, searchTerm } = useSelectLocationContext(); + const relaySettings = useNormalRelaySettings(); + + const scrollViewRef = useRef<CustomScrollbarsRef>(null); + const spacePreAllocationViewRef = useRef() as React.RefObject<SpacePreAllocationView>; + const scrollPositions = useRef<Partial<Record<LocationType, ScrollPosition>>>({}); + const selectedLocationRef = useRef<HTMLDivElement>(null); + + const saveScrollPosition = useCallback(() => { + const scrollPosition = scrollViewRef.current?.getScrollPosition(); + if (scrollPositions.current && scrollPosition) { + scrollPositions.current[locationType] = scrollPosition; + } + }, [locationType]); + + const resetScrollPositions = useCallback(() => { + for (const locationTypeVariant of [LocationType.entry, LocationType.exit]) { + if ( + scrollPositions.current && + (scrollPositions.current[locationTypeVariant] || locationTypeVariant === locationType) + ) { + scrollPositions.current[locationTypeVariant] = [0, 0]; + } + } + }, [locationType]); + + const value = useMemo( + () => ({ + scrollPositions, + selectedLocationRef, + scrollViewRef, + spacePreAllocationViewRef, + saveScrollPosition, + resetScrollPositions, + }), + [], + ); + + useEffect(() => { + const scrollPosition = scrollPositions.current?.[locationType]; + if (scrollPosition) { + scrollViewRef.current?.scrollTo(...scrollPosition); + } else if (selectedLocationRef.current) { + scrollViewRef.current?.scrollToElement(selectedLocationRef.current, 'middle'); + } else { + scrollViewRef.current?.scrollToTop(); + } + }, [locationType, searchTerm, relaySettings?.ownership, relaySettings?.providers]); + + return ( + <scrollPositionContext.Provider value={value}>{props.children}</scrollPositionContext.Provider> + ); +} diff --git a/gui/src/renderer/components/select-location/SelectLocation.tsx b/gui/src/renderer/components/select-location/SelectLocation.tsx index 660bdacafa..3a1538d070 100644 --- a/gui/src/renderer/components/select-location/SelectLocation.tsx +++ b/gui/src/renderer/components/select-location/SelectLocation.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect } from 'react'; +import { useCallback } from 'react'; import { sprintf } from 'sprintf-js'; import { colors } from '../../../config.json'; @@ -20,13 +20,10 @@ import { TitleBarItem, } from '../NavigationBar'; import LocationList from './LocationList'; +import { useRelayListContext } from './RelayListContext'; import { ScopeBar, ScopeBarItem } from './ScopeBar'; -import { - useExpandedLocations, - useOnSelectBridgeLocation, - useOnSelectLocation, - useRelayList, -} from './select-location-hooks'; +import { useScrollPositionContext } from './ScrollPositionContext'; +import { useOnSelectBridgeLocation, useOnSelectLocation } from './select-location-hooks'; import { LocationSelectionType, LocationType, @@ -49,8 +46,13 @@ import { SpacePreAllocationView } from './SpacePreAllocationView'; export default function SelectLocation() { const history = useHistory(); const { updateRelaySettings } = useAppContext(); - const { saveScrollPosition, resetScrollPositions, applyScrollPosition } = useScrollPosition(); - const { scrollViewRef, spacePreAllocationViewRef, locationType, setLocationType, searchTerm, setSearchTerm } = useSelectLocationContext(); + const { + saveScrollPosition, + resetScrollPositions, + scrollViewRef, + spacePreAllocationViewRef, + } = useScrollPositionContext(); + const { locationType, setLocationType, searchTerm, setSearchTerm } = useSelectLocationContext(); const relaySettings = useNormalRelaySettings(); const ownership = relaySettings?.ownership ?? Ownership.any; @@ -91,8 +93,6 @@ export default function SelectLocation() { [resetScrollPositions], ); - useEffect(applyScrollPosition, [applyScrollPosition]); - const showOwnershipFilter = ownership !== Ownership.any; const showProvidersFilter = providers.length > 0; const showFilters = showOwnershipFilter || showProvidersFilter; @@ -207,10 +207,9 @@ function ownershipFilterLabel(ownership: Ownership): string { } function SelectLocationContent() { - const { locationType, selectedLocationRef, spacePreAllocationViewRef } = useSelectLocationContext(); - const relayList = useRelayList(); - const { expandLocation, collapseLocation, updateExpandedLocations } = useExpandedLocations(); - const { onBeforeExpand } = useScrollPosition(); + const { locationType } = useSelectLocationContext(); + const { selectedLocationRef, spacePreAllocationViewRef } = useScrollPositionContext(); + const { relayList, expandLocation, collapseLocation, onBeforeExpand } = useRelayListContext(); const onSelectLocation = useOnSelectLocation(); const onSelectBridgeLocation = useOnSelectBridgeLocation(); @@ -219,8 +218,6 @@ function SelectLocationContent() { const resetHeight = useCallback(() => spacePreAllocationViewRef.current?.reset(), []); - useEffect(updateExpandedLocations, [updateExpandedLocations]); - if (locationType === LocationType.exit) { return ( <LocationList @@ -276,45 +273,3 @@ function SelectLocationContent() { ); } } - -function useScrollPosition() { - const { locationType, scrollPositions, scrollViewRef, spacePreAllocationViewRef, selectedLocationRef, searchTerm } = useSelectLocationContext(); - const relaySettings = useNormalRelaySettings(); - - const saveScrollPosition = useCallback(() => { - const scrollPosition = scrollViewRef.current?.getScrollPosition(); - if (scrollPositions.current && scrollPosition) { - scrollPositions.current[locationType] = scrollPosition; - } - }, [locationType]); - - const resetScrollPositions = useCallback(() => { - for (const locationTypeVariant of [LocationType.entry, LocationType.exit]) { - if ( - scrollPositions.current && - (scrollPositions.current[locationTypeVariant] || locationTypeVariant === locationType) - ) { - scrollPositions.current[locationTypeVariant] = [0, 0]; - } - } - }, [locationType]); - - const applyScrollPosition = useCallback(() => { - const scrollPosition = scrollPositions.current?.[locationType]; - if (scrollPosition) { - scrollViewRef.current?.scrollTo(...scrollPosition); - } else if (selectedLocationRef.current) { - scrollViewRef.current?.scrollToElement(selectedLocationRef.current, 'middle'); - } else { - scrollViewRef.current?.scrollToTop(); - } - }, [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 { 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 4f64aa79f3..99203fa1ad 100644 --- a/gui/src/renderer/components/select-location/SelectLocationContainer.tsx +++ b/gui/src/renderer/components/select-location/SelectLocationContainer.tsx @@ -1,29 +1,16 @@ -import React, { useContext, useMemo, useRef, useState } from 'react'; +import React, { useContext, useMemo, useState } from 'react'; -import { RelayLocation } from '../../../shared/daemon-rpc-types'; -import { useNormalBridgeSettings, useNormalRelaySettings } from '../../lib/utilityHooks'; -import {CustomScrollbarsRef} from '../CustomScrollbars'; -import { defaultExpandedLocations } from './select-location-helpers'; +import { useNormalRelaySettings } from '../../lib/utilityHooks'; +import { RelayListContextProvider } from './RelayListContext'; +import { ScrollPositionContextProvider } from './ScrollPositionContext'; 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]; interface SelectLocationContext { locationType: LocationType; setLocationType: (locationType: LocationType) => void; 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); @@ -34,45 +21,24 @@ export function useSelectLocationContext() { export default function SelectLocationContainer() { const relaySettings = useNormalRelaySettings(); - const bridgeSettings = useNormalBridgeSettings(); 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 [searchTerm, setSearchTerm] = useState(''); - const value = useMemo( - () => ({ - locationType, - setLocationType, - searchTerm, - setSearchTerm, - expandedLocations, - setExpandedLocations, - scrollPositions, - selectedLocationRef, - scrollViewRef, - spacePreAllocationViewRef, - }), - [ - locationType, - relaySettings?.ownership, - relaySettings?.providers, - expandedLocations, - searchTerm, - ], - ); + const value = useMemo(() => ({ locationType, setLocationType, searchTerm, setSearchTerm }), [ + locationType, + relaySettings?.ownership, + relaySettings?.providers, + searchTerm, + ]); return ( <selectLocationContext.Provider value={value}> - <SelectLocation /> + <ScrollPositionContextProvider> + <RelayListContextProvider> + <SelectLocation /> + </RelayListContextProvider> + </ScrollPositionContextProvider> </selectLocationContext.Provider> ); } 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 5bab2a429d..12aa490c68 100644 --- a/gui/src/renderer/components/select-location/select-location-hooks.ts +++ b/gui/src/renderer/components/select-location/select-location-hooks.ts @@ -1,42 +1,14 @@ -import { useCallback, useMemo } from 'react'; +import { useCallback } from 'react'; import BridgeSettingsBuilder from '../../../shared/bridge-settings-builder'; -import { - compareRelayLocation, - RelayLocation, - RelaySettingsUpdate, -} from '../../../shared/daemon-rpc-types'; +import { RelaySettingsUpdate } from '../../../shared/daemon-rpc-types'; 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, - filterLocationsByEndPointType, - getLocationsExpandedBySearch, - searchForLocations, -} from '../../lib/filter-locations'; import { useHistory } from '../../lib/history'; -import { - useNormalBridgeSettings, - useNormalRelaySettings, - useSharedMemo, -} from '../../lib/utilityHooks'; -import { IRelayLocationRedux } from '../../redux/settings/reducers'; import { useSelector } from '../../redux/store'; import { - defaultExpandedLocations, - formatRowName, - isCityDisabled, - isCountryDisabled, - isExpanded, - isRelayDisabled, - isSelected, -} from './select-location-helpers'; -import { - DisabledReason, - LocationList, LocationSelection, LocationSelectionType, LocationType, @@ -44,203 +16,6 @@ import { } from './select-location-types'; import { useSelectLocationContext } from './SelectLocationContainer'; -// Return all locations that matches both the set filters and the search term. -function useFilteredRelays(): Array<IRelayLocationRedux> { - const { locationType, searchTerm } = useSelectLocationContext(); - const relayList = useSelector((state) => state.settings.relayLocations); - const relaySettings = useNormalRelaySettings(); - - 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; -} - -// Return all RelayLocations that should be expanded -export function useExpandedLocations() { - const relaySettings = useNormalRelaySettings(); - const bridgeSettings = useNormalBridgeSettings(); - const { - locationType, - expandedLocations, - setExpandedLocations, - searchTerm, - } = useSelectLocationContext(); - const relayList = useFilteredRelays(); - - const expandLocation = useCallback( - (location: RelayLocation) => { - setExpandedLocations((expandedLocations) => ({ - ...expandedLocations, - [locationType]: [...(expandedLocations[locationType] ?? []), location], - })); - }, - [locationType], - ); - - const collapseLocation = useCallback( - (location: RelayLocation) => { - setExpandedLocations((expandedLocations) => ({ - ...expandedLocations, - [locationType]: expandedLocations[locationType]!.filter( - (item) => !compareRelayLocation(location, item), - ), - })); - }, - [locationType], - ); - - 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: expandedLocations[locationType], - expandLocation, - collapseLocation, - updateExpandedLocations, - }; -} - -// Return the final filtered and formatted relay list. This should be the only place in the app -// where processing of the relay list is performed. -export function useRelayList(): LocationList<never> { - const locale = useSelector((state) => state.userInterface.locale); - const { expandedLocations } = useExpandedLocations(); - const relayList = useFilteredRelays(); - const selectedLocation = useSelectedLocation(); - const disabledLocation = useDisabledLocation(); - - 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 { - ...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)); - }, [locale, expandedLocations, relayList, selectedLocation, disabledLocation]); -} - -function useDisabledLocation() { - const { locationType } = useSelectLocationContext(); - const relaySettings = useNormalRelaySettings(); - - 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; - }, [locationType, relaySettings?.tunnelProtocol, relaySettings?.wireguard.useMultihop, relaySettings?.wireguard.entryLocation, relaySettings?.location]); -} - -// Returns the selected location for the current tunnel protocol and location type -function useSelectedLocation() { - const { locationType } = useSelectLocationContext(); - const relaySettings = useNormalRelaySettings(); - const bridgeSettings = useNormalBridgeSettings(); - - 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() { const history = useHistory(); const { updateRelaySettings } = useAppContext(); diff --git a/gui/src/renderer/lib/utilityHooks.ts b/gui/src/renderer/lib/utilityHooks.ts index 4c66937393..378a6d5ae5 100644 --- a/gui/src/renderer/lib/utilityHooks.ts +++ b/gui/src/renderer/lib/utilityHooks.ts @@ -65,28 +65,3 @@ 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; - } -} |
