diff --git a/nym-vpn-app/src/contexts/node-list-state/context.ts b/nym-vpn-app/src/contexts/node-list-state/context.ts index f99c7728c9..e4847e6dd6 100644 --- a/nym-vpn-app/src/contexts/node-list-state/context.ts +++ b/nym-vpn-app/src/contexts/node-list-state/context.ts @@ -25,7 +25,7 @@ type State = { // country code value: string, ) => void; - setFocused: (hop: Hop, focused: Focused) => void; + setFocused: (hop: Hop, focused: Focused | null) => void; setSearch: (hop: Hop, search: string | null) => void; reset: (hop: Hop | 'all') => void; }; diff --git a/nym-vpn-app/src/contexts/node-list-state/provider.tsx b/nym-vpn-app/src/contexts/node-list-state/provider.tsx index 9bec125717..e24751ee17 100644 --- a/nym-vpn-app/src/contexts/node-list-state/provider.tsx +++ b/nym-vpn-app/src/contexts/node-list-state/provider.tsx @@ -29,15 +29,15 @@ function NodeListPrevStateProvider({ children }: NodeListPrevStateProps) { return; } if (hop === 'entry') { - setEntryExpanded([...entryExpanded, value]); + setEntryExpanded((prev) => [...prev, value]); } else { - setExitExpanded([...exitExpanded, value]); + setExitExpanded((prev) => [...prev, value]); } }, [entryExpanded, exitExpanded], ); - const setFocused = useCallback((hop: Hop, focus: Focused) => { + const setFocused = useCallback((hop: Hop, focus: Focused | null) => { if (hop === 'entry') { setEntryFocused(focus); } else { diff --git a/nym-vpn-app/src/screens/home/Home.tsx b/nym-vpn-app/src/screens/home/Home.tsx index cd92c96ab2..ca3324b4b7 100644 --- a/nym-vpn-app/src/screens/home/Home.tsx +++ b/nym-vpn-app/src/screens/home/Home.tsx @@ -6,11 +6,19 @@ import { useNavigate } from 'react-router'; import clsx from 'clsx'; import { motion } from 'motion/react'; import { + Focused, + useGateways, useMainDispatch, useMainState, useNodeListState, } from '../../contexts'; -import { BackendError, StateDispatch } from '../../types'; +import { + BackendError, + StateDispatch, + isCountry, + isGateway, + isRegion, +} from '../../types'; import { routes } from '../../router'; import { Button } from '../../ui'; import { capFirst } from '../../util'; @@ -20,7 +28,7 @@ import HopSelect from './HopSelect'; import NetworkUpdateDialog from './NetworkUpdateDialog'; import UpdateDialog from './UpdateDialog'; import useStreamingOptimizedLabel from './useStreamingOptimizedLabel'; -import { setStreamOptimizedLabelSeen } from './util'; +import { regionToCountryCode, setStreamOptimizedLabelSeen } from './util'; const updaterEnabled = window._APP.updaterEnabled; const devMode = window._APP.devMode; @@ -42,7 +50,8 @@ function Home() { welcomeChecked, } = useMainState(); const dispatch = useMainDispatch() as StateDispatch; - const { reset: resetNodeList } = useNodeListState(); + const { setFocused, setSearch, setExpanded } = useNodeListState(); + const { lookupGw } = useGateways(); const navigate = useNavigate(); const { t } = useTranslation('home'); const loading = state === 'disconnecting'; @@ -167,11 +176,36 @@ function Home() { }; const goToNodeList = (hop: 'entry' | 'exit') => { + const expanded: string[] = []; + let focused: Focused | null = null; + const node = hop === 'entry' ? entryNode : exitNode; + + if (isCountry(node)) { + focused = { type: 'country', key: node.country.code }; + } else if (isRegion(node)) { + const code = regionToCountryCode(node.region); + if (code) { + expanded.push(code.toUpperCase()); + focused = { type: 'region', key: node.region }; + } + } else if (isGateway(node)) { + focused = { type: 'gateway', key: node.gateway.id }; + const gw = lookupGw(node.gateway.id, hop); + if (gw) { + expanded.push(gw.country.code.toUpperCase()); + if (gw.country.code.toLowerCase() === 'us') { + expanded.push(gw.location.region); + } + } + } + + setExpanded(hop, expanded); + setFocused(hop, focused); + setSearch(hop, null); + if (hop === 'entry') { - resetNodeList('entry'); navigate(routes.entryNodeLocation); } else { - resetNodeList('exit'); navigate(routes.exitNodeLocation); setStreamOptimizedLabelSeen(dispatch); } diff --git a/nym-vpn-app/src/screens/home/HopSelect.tsx b/nym-vpn-app/src/screens/home/HopSelect.tsx index 6b5fac6e27..a0f304933a 100644 --- a/nym-vpn-app/src/screens/home/HopSelect.tsx +++ b/nym-vpn-app/src/screens/home/HopSelect.tsx @@ -1,6 +1,5 @@ import { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; -import { AnimatePresence, motion } from 'motion/react'; import clsx from 'clsx'; import { useNavigate } from 'react-router'; import { Button } from '@headlessui/react'; @@ -12,13 +11,16 @@ import { isGateway, isRegion, } from '../../types'; -import { FlagIcon, MsIcon, countryCode } from '../../ui'; +import { MsIcon, countryCode } from '../../ui'; import { useLang } from '../../hooks'; import { useGateways, useMainState } from '../../contexts'; import { countriesWithRegions } from '../../constants'; -import { QuicTag } from '../node'; import { routes } from '../../router'; import { isBridgeMode, regionToCountryCode, useActionToast } from './util'; +import { + SelectedNodeDisplay, + SelectedNodeDisplayProps, +} from './SelectedNodeDisplay'; type HopSelectProps = { node: SelectedNode; @@ -28,15 +30,6 @@ type HopSelectProps = { disabled?: boolean; }; -type SelectedNodeProps = { - countryCode?: countryCode; - name: string; - subInfo?: string | null; - animate?: boolean; - quic?: boolean; - streamOptimized?: boolean; -}; - export default function HopSelect({ nodeHop, node, @@ -67,22 +60,25 @@ export default function HopSelect({ }; const handleDetailsClick = () => { - if (disabled) { - toast(); - } else if (isGateway(node) && gateway) { - navigate(routes.nodeDetails, { - state: { gateway, hop: nodeHop, resetScroll: true }, - }); - } + if (!gateway) return; + + navigate(routes.nodeDetails, { + state: { gateway, hop: nodeHop, resetScroll: true }, + }); }; - const nodeData = (selected: SelectedNode, gateway: Gateway | null) => { + const nodeData = ( + selected: SelectedNode, + gateway: Gateway | null, + ): SelectedNodeDisplayProps => { if (selected === 'random') { return { name: t('fastest', { ns: 'common' }), animate: false, - quic: gateway?.quic, - streamOptimized: gateway?.asn?.type === 'residential', + showQuic: Boolean(quicTag && gateway?.quic), + showStreamOptimized: + nodeHop === 'exit' && gateway?.asn?.type === 'residential', + showFastest: node === 'random' && !gateway?.country?.code, }; } if (isCountry(selected)) { @@ -103,7 +99,7 @@ export default function HopSelect({ countryCode: string, gateway: Gateway | null, region?: string, - ): SelectedNodeProps => { + ): SelectedNodeDisplayProps => { let location = getCountryName(countryCode) || countryCode; let subInfo = null; if (region && region.length > 0) { @@ -125,15 +121,17 @@ export default function HopSelect({ name: location, subInfo, animate: true, - quic: gateway?.quic, - streamOptimized: gateway?.asn?.type === 'residential', + showQuic: Boolean(quicTag && gateway?.quic), + showStreamOptimized: + nodeHop === 'exit' && gateway?.asn?.type === 'residential', + showFastest: node === 'random' && !gateway?.country?.code, }; }; const getGatewayInfo = ( id: string, gateway: Gateway | null, - ): SelectedNodeProps => { + ): SelectedNodeDisplayProps => { if (!gateway) { return { name: id, @@ -157,77 +155,13 @@ export default function HopSelect({ countryCode: country.code.toLowerCase() as countryCode, name, subInfo: components.join(', '), - quic: gateway?.quic, - streamOptimized: gateway?.asn?.type === 'residential', + showQuic: Boolean(quicTag && gateway?.quic), + showStreamOptimized: + nodeHop === 'exit' && gateway?.asn?.type === 'residential', + showFastest: node === 'random' && !gateway?.country?.code, }; }; - const SelectedNode = ({ - countryCode, - name, - subInfo, - animate, - quic, - streamOptimized, - }: SelectedNodeProps) => { - const showQuic = quicTag && quic; - const showStreamOptimized = nodeHop === 'exit' && streamOptimized; - const showFastest = node === 'random' && !countryCode; - - return ( -