diff options
| author | Andrej Mihajlov <and@mullvad.net> | 2019-09-10 14:35:00 +0200 |
|---|---|---|
| committer | Andrej Mihajlov <and@mullvad.net> | 2019-09-12 12:50:51 +0200 |
| commit | 57f5d8f246fa688a36e3138cbbcd51dd95fdf7ea (patch) | |
| tree | fc85b595edbdda91e5f7e894d7ed536d0ac69ba8 /gui/src/renderer/components | |
| parent | ef20b724201e5c374e6080298974bc8e97818cf2 (diff) | |
| download | mullvadvpn-57f5d8f246fa688a36e3138cbbcd51dd95fdf7ea.tar.xz mullvadvpn-57f5d8f246fa688a36e3138cbbcd51dd95fdf7ea.zip | |
Add bridge selector
Diffstat (limited to 'gui/src/renderer/components')
| -rw-r--r-- | gui/src/renderer/components/CustomScrollbars.tsx | 7 | ||||
| -rw-r--r-- | gui/src/renderer/components/LocationList.tsx | 161 | ||||
| -rw-r--r-- | gui/src/renderer/components/SelectLocation.tsx | 298 | ||||
| -rw-r--r-- | gui/src/renderer/components/SelectLocationStyles.tsx | 2 |
4 files changed, 255 insertions, 213 deletions
diff --git a/gui/src/renderer/components/CustomScrollbars.tsx b/gui/src/renderer/components/CustomScrollbars.tsx index d909dd79dd..8928e9891b 100644 --- a/gui/src/renderer/components/CustomScrollbars.tsx +++ b/gui/src/renderer/components/CustomScrollbars.tsx @@ -56,6 +56,13 @@ export default class CustomScrollbars extends React.Component<IProps, IState> { private thumbRef = React.createRef<HTMLDivElement>(); private autoHideTimer?: NodeJS.Timeout; + public scrollToTop() { + const scrollable = this.scrollableRef.current; + if (scrollable) { + scrollable.scrollTop = 0; + } + } + public scrollTo(x: number, y: number) { const scrollable = this.scrollableRef.current; if (scrollable) { diff --git a/gui/src/renderer/components/LocationList.tsx b/gui/src/renderer/components/LocationList.tsx new file mode 100644 index 0000000000..418da66e94 --- /dev/null +++ b/gui/src/renderer/components/LocationList.tsx @@ -0,0 +1,161 @@ +import * as React from 'react'; +import { Component, View } from 'reactxp'; +import { + compareRelayLocation, + compareRelayLocationLoose, + RelayLocation, + relayLocationComponents, +} from '../../shared/daemon-rpc-types'; +import { countries, relayLocations } from '../../shared/gettext'; +import { IRelayLocationRedux } from '../redux/settings/reducers'; +import CityRow from './CityRow'; +import CountryRow from './CountryRow'; +import RelayRow from './RelayRow'; + +interface IProps { + relayLocations: IRelayLocationRedux[]; + selectedLocation?: RelayLocation; + onSelect: (location: RelayLocation) => void; +} + +interface IState { + selectedLocation?: RelayLocation; + expandedItems: RelayLocation[]; +} + +interface ICommonCellProps<T> { + location: RelayLocation; + selected: boolean; + ref?: React.RefObject<T>; +} + +export default class LocationList extends Component<IProps, IState> { + public selectedCell = React.createRef<React.ReactNode>(); + + constructor(props: IProps) { + super(props); + + this.state = { + expandedItems: props.selectedLocation ? expandRelayLocation(props.selectedLocation) : [], + selectedLocation: props.selectedLocation, + }; + } + + public componentDidUpdate(prevProps: IProps, _prevState: IState) { + if (this.props.selectedLocation !== prevProps.selectedLocation) { + this.setState({ selectedLocation: this.props.selectedLocation }); + } + } + + public render() { + return ( + <View> + {this.props.relayLocations.map((relayCountry) => { + const countryLocation: RelayLocation = { country: relayCountry.code }; + + return ( + <CountryRow + key={getLocationKey(countryLocation)} + name={countries.gettext(relayCountry.name)} + hasActiveRelays={relayCountry.hasActiveRelays} + expanded={this.isExpanded(countryLocation)} + onSelect={this.handleSelection} + onExpand={this.handleExpand} + {...this.getCommonCellProps(countryLocation)}> + {relayCountry.cities.map((relayCity) => { + const cityLocation: RelayLocation = { + city: [relayCountry.code, relayCity.code], + }; + + return ( + <CityRow + key={getLocationKey(cityLocation)} + name={relayLocations.gettext(relayCity.name)} + hasActiveRelays={relayCity.hasActiveRelays} + expanded={this.isExpanded(cityLocation)} + onSelect={this.handleSelection} + onExpand={this.handleExpand} + {...this.getCommonCellProps(cityLocation)}> + {relayCity.relays.map((relay) => { + const relayLocation: RelayLocation = { + hostname: [relayCountry.code, relayCity.code, relay.hostname], + }; + + return ( + <RelayRow + key={getLocationKey(relayLocation)} + hostname={relay.hostname} + onSelect={this.handleSelection} + {...this.getCommonCellProps(relayLocation)} + /> + ); + })} + </CityRow> + ); + })} + </CountryRow> + ); + })} + </View> + ); + } + + private isExpanded(relayLocation: RelayLocation) { + return this.state.expandedItems.some((location) => + compareRelayLocation(location, relayLocation), + ); + } + + private isSelected(relayLocation: RelayLocation) { + return compareRelayLocationLoose(this.state.selectedLocation, relayLocation); + } + + private handleSelection = (location: RelayLocation) => { + if (!compareRelayLocationLoose(this.state.selectedLocation, location)) { + this.setState({ selectedLocation: location }, () => { + this.props.onSelect(location); + }); + } + }; + + private handleExpand = (location: RelayLocation, expand: boolean) => { + this.setState((state) => { + const expandedItems = state.expandedItems.filter( + (item) => !compareRelayLocation(item, location), + ); + + if (expand) { + expandedItems.push(location); + } + + return { + ...state, + expandedItems, + }; + }); + }; + + private getCommonCellProps<T>(location: RelayLocation): ICommonCellProps<T> { + const selected = this.isSelected(location); + const ref = selected ? (this.selectedCell as React.RefObject<T>) : undefined; + + return { ref, selected, location }; + } +} + +function expandRelayLocation(location: RelayLocation): RelayLocation[] { + const expandedItems: RelayLocation[] = []; + + if ('city' in location) { + expandedItems.push({ country: location.city[0] }); + } else if ('hostname' in location) { + expandedItems.push({ country: location.hostname[0] }); + expandedItems.push({ city: [location.hostname[0], location.hostname[1]] }); + } + + return expandedItems; +} + +function getLocationKey(location: RelayLocation): string { + return relayLocationComponents(location).join('-'); +} diff --git a/gui/src/renderer/components/SelectLocation.tsx b/gui/src/renderer/components/SelectLocation.tsx index 73f075764a..2f0a162b51 100644 --- a/gui/src/renderer/components/SelectLocation.tsx +++ b/gui/src/renderer/components/SelectLocation.tsx @@ -1,9 +1,13 @@ import * as React from 'react'; import ReactDOM from 'react-dom'; import { Component, View } from 'reactxp'; -import { countries, messages, relayLocations } from '../../shared/gettext'; +import { RelayLocation } from '../../shared/daemon-rpc-types'; +import { messages } from '../../shared/gettext'; +import { IRelayLocationRedux } from '../redux/settings/reducers'; +import { LocationScope } from '../redux/userinterface/reducers'; import CustomScrollbars from './CustomScrollbars'; import { Container, Layout } from './Layout'; +import LocationList from './LocationList'; import { CloseBarItem, NavigationBar, @@ -18,102 +22,31 @@ import { import styles from './SelectLocationStyles'; import SettingsHeader, { HeaderSubTitle, HeaderTitle } from './SettingsHeader'; -import CityRow from './CityRow'; -import CountryRow from './CountryRow'; -import RelayRow from './RelayRow'; - -import { - compareRelayLocation, - compareRelayLocationLoose, - RelayLocation, -} from '../../shared/daemon-rpc-types'; -import { IRelayLocationRedux, RelaySettingsRedux } from '../redux/settings/reducers'; - interface IProps { - relaySettings: RelaySettingsRedux; + locationScope: LocationScope; + selectedExitLocation?: RelayLocation; + selectedBridgeLocation?: RelayLocation; relayLocations: IRelayLocationRedux[]; + bridgeLocations: IRelayLocationRedux[]; + allowBridgeSelection: boolean; onClose: () => void; - onSelect: (location: RelayLocation) => void; -} - -interface IState { - locationScope: LocationScope; - selectedLocation?: RelayLocation; - expandedItems: RelayLocation[]; -} - -enum LocationScope { - relay = 0, - bridge, + onChangeLocationScope: (location: LocationScope) => void; + onSelectExitLocation: (location: RelayLocation) => void; + onSelectBridgeLocation: (location: RelayLocation) => void; } -export default class SelectLocation extends Component<IProps, IState> { - public state: IState = { - locationScope: LocationScope.relay, - expandedItems: [], - }; - private selectedCellRef = React.createRef<React.ReactNode>(); - private scrollViewRef = React.createRef<CustomScrollbars>(); - - constructor(props: IProps) { - super(props); - - if ('normal' in this.props.relaySettings) { - const location = this.props.relaySettings.normal.location; - - if (typeof location === 'object') { - const expandedItems: RelayLocation[] = []; - - if ('city' in location) { - expandedItems.push({ country: location.city[0] }); - } else if ('hostname' in location) { - expandedItems.push({ country: location.hostname[0] }); - expandedItems.push({ city: [location.hostname[0], location.hostname[1]] }); - } - - this.state = { - ...this.state, - selectedLocation: location, - expandedItems, - }; - } - } - } - - public componentDidUpdate(oldProps: IProps) { - const currentLocation = this.state.selectedLocation; - let newLocation = - 'normal' in this.props.relaySettings ? this.props.relaySettings.normal.location : undefined; - - let oldLocation = - 'normal' in oldProps.relaySettings ? oldProps.relaySettings.normal.location : undefined; - - if (newLocation === 'any') { - newLocation = undefined; - } - - if (oldLocation === 'any') { - oldLocation = undefined; - } +export default class SelectLocation extends Component<IProps> { + private scrollView = React.createRef<CustomScrollbars>(); + private exitLocationList = React.createRef<LocationList>(); + private bridgeLocationList = React.createRef<LocationList>(); - if ( - !compareRelayLocationLoose(oldLocation, newLocation) && - !compareRelayLocationLoose(currentLocation, newLocation) - ) { - this.setState({ selectedLocation: newLocation }); - } + public componentDidMount() { + this.scrollToSelectedCell(); } - public componentDidMount() { - // restore scroll to the selected cell - const cell = this.selectedCellRef.current; - const scrollView = this.scrollViewRef.current; - if (scrollView && cell) { - // TODO: Fix the browser specific code - const cellDOMNode = ReactDOM.findDOMNode(cell as Element); - if (cellDOMNode instanceof HTMLElement) { - scrollView.scrollToElement(cellDOMNode, 'middle'); - } + public componentDidUpdate(prevProps: IProps) { + if (this.props.locationScope !== prevProps.locationScope) { + this.scrollToSelectedCell(); } } @@ -131,81 +64,60 @@ export default class SelectLocation extends Component<IProps, IState> { </TitleBarItem> </NavigationBar> <StickyContentContainer style={styles.container}> - <NavigationScrollbars ref={this.scrollViewRef}> + <NavigationScrollbars ref={this.scrollView}> <View style={styles.content}> - <SettingsHeader style={styles.header}> + <SettingsHeader + style={this.props.allowBridgeSelection ? styles.headerWithScope : undefined}> <HeaderTitle> {messages.pgettext('select-location-view', 'Select location')} </HeaderTitle> <HeaderSubTitle> - {messages.pgettext( - 'select-location-view', - 'While connected, your real location is masked with a private and secure location in the selected region', - )} + {this.props.locationScope === LocationScope.relay + ? messages.pgettext( + 'select-location-view', + 'While connected, your real location is masked with a private and secure location in the selected region', + ) + : 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)', + )} </HeaderSubTitle> </SettingsHeader> - <StickyContentHolder style={styles.stickyHolder}> - <View style={styles.stickyContent}> - <ScopeBar - defaultSelectedIndex={this.state.locationScope} - onChange={this.onChangeScope}> - <ScopeBarItem> - {messages.pgettext('select-location-nav', 'Exit')} - </ScopeBarItem> - <ScopeBarItem> - {messages.pgettext('select-location-nav', 'Entry')} - </ScopeBarItem> - </ScopeBar> - </View> - </StickyContentHolder> - - {this.props.relayLocations.map((relayCountry) => { - const countryLocation: RelayLocation = { country: relayCountry.code }; - - return ( - <CountryRow - key={getLocationKey(countryLocation)} - name={countries.gettext(relayCountry.name)} - hasActiveRelays={relayCountry.hasActiveRelays} - expanded={this.isExpanded(countryLocation)} - onSelect={this.handleSelection} - onExpand={this.handleExpand} - {...this.getCommonCellProps(countryLocation)}> - {relayCountry.cities.map((relayCity) => { - const cityLocation: RelayLocation = { - city: [relayCountry.code, relayCity.code], - }; + {this.props.allowBridgeSelection && ( + <StickyContentHolder style={styles.stickyHolder}> + <View style={styles.stickyContent}> + <ScopeBar + defaultSelectedIndex={this.props.locationScope} + onChange={this.props.onChangeLocationScope}> + <ScopeBarItem> + {messages.pgettext('select-location-nav', 'Entry')} + </ScopeBarItem> + <ScopeBarItem> + {messages.pgettext('select-location-nav', 'Exit')} + </ScopeBarItem> + </ScopeBar> + </View> + </StickyContentHolder> + )} - return ( - <CityRow - key={getLocationKey(cityLocation)} - name={relayLocations.gettext(relayCity.name)} - hasActiveRelays={relayCity.hasActiveRelays} - expanded={this.isExpanded(cityLocation)} - onSelect={this.handleSelection} - onExpand={this.handleExpand} - {...this.getCommonCellProps(cityLocation)}> - {relayCity.relays.map((relay) => { - const relayLocation: RelayLocation = { - hostname: [relayCountry.code, relayCity.code, relay.hostname], - }; - - return ( - <RelayRow - key={getLocationKey(relayLocation)} - hostname={relay.hostname} - onSelect={this.handleSelection} - {...this.getCommonCellProps(relayLocation)} - /> - ); - })} - </CityRow> - ); - })} - </CountryRow> - ); - })} + {this.props.locationScope === LocationScope.relay ? ( + <LocationList + key={'exit-locations'} + ref={this.exitLocationList} + selectedLocation={this.props.selectedExitLocation} + relayLocations={this.props.relayLocations} + onSelect={this.props.onSelectExitLocation} + /> + ) : ( + <LocationList + key={'bridge-locations'} + ref={this.bridgeLocationList} + selectedLocation={this.props.selectedBridgeLocation} + relayLocations={this.props.bridgeLocations} + onSelect={this.props.onSelectBridgeLocation} + /> + )} </View> </NavigationScrollbars> </StickyContentContainer> @@ -216,65 +128,27 @@ export default class SelectLocation extends Component<IProps, IState> { ); } - private onChangeScope = (selectedIndex: number) => { - this.setState({ locationScope: selectedIndex }); - }; - - private isExpanded(relayLocation: RelayLocation) { - return this.state.expandedItems.some((location) => - compareRelayLocation(location, relayLocation), - ); - } - - private isSelected(relayLocation: RelayLocation) { - return compareRelayLocationLoose(this.state.selectedLocation, relayLocation); - } - - private handleSelection = (location: RelayLocation) => { - if (!compareRelayLocationLoose(this.state.selectedLocation, location)) { - this.setState({ selectedLocation: location }, () => { - this.props.onSelect(location); - }); - } - }; + private scrollToSelectedCell() { + const ref = + this.props.locationScope === LocationScope.relay + ? this.exitLocationList + : this.bridgeLocationList; + const locationList = ref.current; - private handleExpand = (location: RelayLocation, expand: boolean) => { - this.setState((state) => { - const expandedItems = state.expandedItems.filter( - (item) => !compareRelayLocation(item, location), - ); + if (locationList) { + const cell = locationList.selectedCell.current; + const scrollView = this.scrollView.current; - if (expand) { - expandedItems.push(location); + if (scrollView) { + if (cell) { + const cellDOMNode = ReactDOM.findDOMNode(cell as Element); + if (cellDOMNode instanceof HTMLElement) { + scrollView.scrollToElement(cellDOMNode, 'middle'); + } + } else { + scrollView.scrollToTop(); + } } - - return { - ...state, - expandedItems, - }; - }); - }; - - private getCommonCellProps<T>( - location: RelayLocation, - ): { location: RelayLocation; selected: boolean; ref?: React.RefObject<T> } { - const selected = this.isSelected(location); - const ref = selected ? (this.selectedCellRef as React.RefObject<T>) : undefined; - - return { ref, selected, location }; - } -} - -function getLocationKey(location: RelayLocation): string { - const components: string[] = []; - - if ('city' in location) { - components.push(...location.city); - } else if ('country' in location) { - components.push(location.country); - } else if ('hostname' in location) { - components.push(...location.hostname); + } } - - return ([] as string[]).concat(components).join('-'); } diff --git a/gui/src/renderer/components/SelectLocationStyles.tsx b/gui/src/renderer/components/SelectLocationStyles.tsx index ef4f01ee4e..bc326bb4a7 100644 --- a/gui/src/renderer/components/SelectLocationStyles.tsx +++ b/gui/src/renderer/components/SelectLocationStyles.tsx @@ -13,7 +13,7 @@ export default { content: Styles.createViewStyle({ overflow: 'visible', }), - header: Styles.createViewStyle({ + headerWithScope: Styles.createViewStyle({ paddingBottom: 4, }), stickyHolder: Styles.createViewStyle({ |
