summaryrefslogtreecommitdiffhomepage
path: root/gui/src/renderer/components
diff options
context:
space:
mode:
authorAndrej Mihajlov <and@mullvad.net>2019-09-10 14:35:00 +0200
committerAndrej Mihajlov <and@mullvad.net>2019-09-12 12:50:51 +0200
commit57f5d8f246fa688a36e3138cbbcd51dd95fdf7ea (patch)
treefc85b595edbdda91e5f7e894d7ed536d0ac69ba8 /gui/src/renderer/components
parentef20b724201e5c374e6080298974bc8e97818cf2 (diff)
downloadmullvadvpn-57f5d8f246fa688a36e3138cbbcd51dd95fdf7ea.tar.xz
mullvadvpn-57f5d8f246fa688a36e3138cbbcd51dd95fdf7ea.zip
Add bridge selector
Diffstat (limited to 'gui/src/renderer/components')
-rw-r--r--gui/src/renderer/components/CustomScrollbars.tsx7
-rw-r--r--gui/src/renderer/components/LocationList.tsx161
-rw-r--r--gui/src/renderer/components/SelectLocation.tsx298
-rw-r--r--gui/src/renderer/components/SelectLocationStyles.tsx2
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({