import React from 'react'; import { LiftedConstraint, RelayLocation } from '../../shared/daemon-rpc-types'; import { messages } from '../../shared/gettext'; import { IRelayLocationRedux } from '../redux/settings/reducers'; import { LocationScope } from '../redux/userinterface/reducers'; import BridgeLocations, { SpecialBridgeLocationType } from './BridgeLocations'; import CustomScrollbars from './CustomScrollbars'; import ExitLocations from './ExitLocations'; import { Layout } from './Layout'; import LocationList, { LocationSelection, LocationSelectionType } from './LocationList'; import { CloseBarItem, NavigationBar, NavigationContainer, NavigationItems, NavigationScrollbars, TitleBarItem, } from './NavigationBar'; import { ScopeBarItem } from './ScopeBar'; import { StyledContainer, StyledContent, StyledNavigationBarAttachment, StyledScopeBar, } from './SelectLocationStyles'; import { HeaderSubTitle } from './SettingsHeader'; interface IProps { locationScope: LocationScope; selectedExitLocation?: RelayLocation; selectedBridgeLocation?: LiftedConstraint; relayLocations: IRelayLocationRedux[]; bridgeLocations: IRelayLocationRedux[]; allowBridgeSelection: boolean; onClose: () => void; onChangeLocationScope: (location: LocationScope) => void; onSelectExitLocation: (location: RelayLocation) => void; onSelectBridgeLocation: (location: RelayLocation) => void; onSelectClosestToExit: () => void; } interface ISelectLocationSnapshot { scrollPosition: [number, number]; expandedLocations: RelayLocation[]; } export default class SelectLocation extends React.Component { private scrollView = React.createRef(); private spacePreAllocationViewRef = React.createRef(); private selectedExitLocationRef = React.createRef(); private selectedBridgeLocationRef = React.createRef(); private exitLocationList = React.createRef>(); private bridgeLocationList = React.createRef>(); private snapshotByScope: { [index: number]: ISelectLocationSnapshot } = {}; public componentDidMount() { this.scrollToSelectedCell(); } public componentDidUpdate( prevProps: IProps, _prevState: unknown, snapshot?: ISelectLocationSnapshot, ) { if (this.props.locationScope !== prevProps.locationScope) { this.restoreScrollPosition(this.props.locationScope); if (snapshot) { this.snapshotByScope[prevProps.locationScope] = snapshot; } } } public getSnapshotBeforeUpdate(prevProps: IProps): ISelectLocationSnapshot | undefined { const scrollView = this.scrollView.current; const locationList = prevProps.locationScope === LocationScope.relay ? this.exitLocationList.current : this.bridgeLocationList.current; if (scrollView && locationList) { return { scrollPosition: scrollView.getScrollPosition(), expandedLocations: locationList.getExpandedLocations(), }; } else { return undefined; } } public render() { return ( { // TRANSLATORS: Title label in navigation bar messages.pgettext('select-location-nav', 'Select location') } {this.props.allowBridgeSelection ? messages.pgettext( 'select-location-view', 'While connected, your traffic will be routed through two secure locations, the entry point (a bridge server) and the exit point (a VPN server).', ) : messages.pgettext( 'select-location-view', 'While connected, your real location is masked with a private and secure location in the selected region.', )} {this.props.allowBridgeSelection && ( {messages.pgettext('select-location-nav', 'Entry')} {messages.pgettext('select-location-nav', 'Exit')} )} {this.props.locationScope === LocationScope.relay ? ( ) : ( )} ); } public restoreScrollPosition(scope: LocationScope) { const snapshot = this.snapshotByScope[scope]; if (snapshot) { this.scrollToPosition(...snapshot.scrollPosition); } else { this.scrollToSelectedCell(); } } private resetHeight = () => { this.spacePreAllocationViewRef.current?.reset(); }; private getExpandedLocationsFromSnapshot(): RelayLocation[] | undefined { const snapshot = this.snapshotByScope[this.props.locationScope]; if (snapshot) { return snapshot.expandedLocations; } else { return undefined; } } private scrollToPosition(x: number, y: number) { const scrollView = this.scrollView.current; if (scrollView) { scrollView.scrollTo(x, y); } } private scrollToSelectedCell() { const ref = this.props.locationScope === LocationScope.relay ? this.selectedExitLocationRef.current : this.selectedBridgeLocationRef.current; const scrollView = this.scrollView.current; if (scrollView) { if (ref) { if (ref instanceof HTMLElement) { scrollView.scrollToElement(ref, 'middle'); } } else { scrollView.scrollToTop(); } } } private onSelectExitLocation = (location: LocationSelection) => { if (location.type === LocationSelectionType.relay) { this.props.onSelectExitLocation(location.value); } }; private onSelectBridgeLocation = (location: LocationSelection) => { if (location.type === LocationSelectionType.relay) { this.props.onSelectBridgeLocation(location.value); } else if ( location.type === LocationSelectionType.special && location.value === SpecialBridgeLocationType.closestToExit ) { this.props.onSelectClosestToExit(); } }; private onWillExpand = (locationRect: DOMRect, expandedContentHeight: number) => { locationRect.height += expandedContentHeight; this.spacePreAllocationViewRef.current?.allocate(expandedContentHeight); this.scrollView.current?.scrollIntoView(locationRect); }; } interface ISpacePreAllocationView { children?: React.ReactNode; } class SpacePreAllocationView extends React.Component { private ref = React.createRef(); public allocate(height: number) { if (this.ref.current) { this.minHeight = this.ref.current.offsetHeight + height + 'px'; } } public reset = () => { this.minHeight = 'auto'; }; public render() { return
{this.props.children}
; } private set minHeight(value: string) { const element = this.ref.current; if (element) { element.style.minHeight = value; } } }