import * as React from 'react'; import ReactDOM from 'react-dom'; import { Component, View } from 'reactxp'; 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 { Container, Layout } from './Layout'; import LocationList, { LocationSelection, LocationSelectionType } from './LocationList'; import { CloseBarItem, NavigationBar, NavigationContainer, NavigationItems, NavigationScrollbars, ScopeBar, ScopeBarItem, TitleBarItem, } from './NavigationBar'; import styles 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 Component { private scrollView = 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: {}, 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 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) { const cellDOMNode = ReactDOM.findDOMNode(ref); if (cellDOMNode instanceof HTMLElement) { scrollView.scrollToElement(cellDOMNode, '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(); } }; }