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 | |
| parent | ef20b724201e5c374e6080298974bc8e97818cf2 (diff) | |
| download | mullvadvpn-57f5d8f246fa688a36e3138cbbcd51dd95fdf7ea.tar.xz mullvadvpn-57f5d8f246fa688a36e3138cbbcd51dd95fdf7ea.zip | |
Add bridge selector
Diffstat (limited to 'gui/src/renderer')
| -rw-r--r-- | gui/src/renderer/app.tsx | 47 | ||||
| -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 | ||||
| -rw-r--r-- | gui/src/renderer/containers/SelectLocationPage.tsx | 57 | ||||
| -rw-r--r-- | gui/src/renderer/redux/settings/actions.ts | 32 | ||||
| -rw-r--r-- | gui/src/renderer/redux/settings/reducers.ts | 31 | ||||
| -rw-r--r-- | gui/src/renderer/redux/userinterface/actions.ts | 19 | ||||
| -rw-r--r-- | gui/src/renderer/redux/userinterface/reducers.ts | 10 |
10 files changed, 436 insertions, 228 deletions
diff --git a/gui/src/renderer/app.tsx b/gui/src/renderer/app.tsx index 14bb45ff2e..6f54281f3c 100644 --- a/gui/src/renderer/app.tsx +++ b/gui/src/renderer/app.tsx @@ -16,7 +16,7 @@ import AppRoutes from './routes'; import accountActions from './redux/account/actions'; import connectionActions from './redux/connection/actions'; import settingsActions from './redux/settings/actions'; -import { IWgKey } from './redux/settings/reducers'; +import { IRelayLocationRedux, IWgKey } from './redux/settings/reducers'; import configureStore from './redux/store'; import userInterfaceActions from './redux/userinterface/actions'; import versionActions from './redux/version/actions'; @@ -24,11 +24,12 @@ import versionActions from './redux/version/actions'; import { IAppUpgradeInfo, ICurrentAppVersionInfo } from '../main'; import { cities, countries, loadTranslations, messages, relayLocations } from '../shared/gettext'; import { IGuiSettingsState } from '../shared/gui-settings-state'; -import { IpcRendererEventChannel } from '../shared/ipc-event-channel'; +import { IpcRendererEventChannel, IRelayListPair } from '../shared/ipc-event-channel'; import { getRendererLogFile, setupLogging } from '../shared/logging'; import { AccountToken, + BridgeSettings, BridgeState, IAccountData, ILocation, @@ -37,6 +38,7 @@ import { IWireguardPublicKey, KeygenEvent, liftConstraint, + RelayLocation, RelaySettings, RelaySettingsUpdate, TunnelState, @@ -111,8 +113,9 @@ export default class AppRenderer { this.setLocation(newLocation); }); - IpcRendererEventChannel.relays.listen((newRelays: IRelayList) => { - this.setRelays(newRelays); + IpcRendererEventChannel.relays.listen((relayListPair: IRelayListPair) => { + this.setRelays(relayListPair.relays); + this.setBridges(relayListPair.bridges); }); IpcRendererEventChannel.currentVersion.listen((currentVersion: ICurrentAppVersionInfo) => { @@ -158,6 +161,7 @@ export default class AppRenderer { } this.setRelays(initialState.relays); + this.setBridges(initialState.bridges); this.setCurrentVersion(initialState.currentVersion); this.setUpgradeVersion(initialState.upgradeVersion); this.setGuiSettings(initialState.guiSettings); @@ -249,6 +253,10 @@ export default class AppRenderer { return IpcRendererEventChannel.settings.updateRelaySettings(relaySettings); } + public updateBridgeLocation(bridgeLocation: RelayLocation) { + return IpcRendererEventChannel.settings.updateBridgeLocation(bridgeLocation); + } + public async removeAccountFromHistory(accountToken: AccountToken): Promise<void> { return IpcRendererEventChannel.accountHistory.removeItem(accountToken); } @@ -371,6 +379,22 @@ export default class AppRenderer { } } + private setBridgeSettings(bridgeSettings: BridgeSettings) { + const actions = this.reduxActions; + + if ('normal' in bridgeSettings) { + actions.settings.updateBridgeSettings({ + normal: { + location: liftConstraint(bridgeSettings.normal.location), + }, + }); + } else if ('custom' in bridgeSettings) { + actions.settings.updateBridgeSettings({ + custom: bridgeSettings.custom, + }); + } + } + private async onDaemonConnected() { // Filter out the calls coming from IPC events arriving right after the constructor finished // execution. @@ -474,6 +498,7 @@ export default class AppRenderer { reduxSettings.updateBridgeState(newSettings.bridgeState); this.setRelaySettings(newSettings.relaySettings); + this.setBridgeSettings(newSettings.bridgeSettings); if (newSettings.accountToken) { reduxAccount.updateAccountToken(newSettings.accountToken); @@ -525,8 +550,8 @@ export default class AppRenderer { this.reduxActions.connection.newLocation(location); } - private setRelays(relayList: IRelayList) { - const locations = relayList.countries + private covertRelayListToLocationList(relayList: IRelayList): IRelayLocationRedux[] { + return relayList.countries .map((country) => ({ name: country.name, code: country.code, @@ -543,10 +568,20 @@ export default class AppRenderer { .sort((cityA, cityB) => cityA.name.localeCompare(cityB.name)), })) .sort((countryA, countryB) => countryA.name.localeCompare(countryB.name)); + } + + private setRelays(relayList: IRelayList) { + const locations = this.covertRelayListToLocationList(relayList); this.reduxActions.settings.updateRelayLocations(locations); } + private setBridges(relayList: IRelayList) { + const locations = this.covertRelayListToLocationList(relayList); + + this.reduxActions.settings.updateBridgeLocations(locations); + } + private setCurrentVersion(versionInfo: ICurrentAppVersionInfo) { this.reduxActions.version.updateVersion(versionInfo.gui, versionInfo.isConsistent); } 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({ diff --git a/gui/src/renderer/containers/SelectLocationPage.tsx b/gui/src/renderer/containers/SelectLocationPage.tsx index a517c34b3f..24bae9f24d 100644 --- a/gui/src/renderer/containers/SelectLocationPage.tsx +++ b/gui/src/renderer/containers/SelectLocationPage.tsx @@ -5,19 +5,54 @@ import { bindActionCreators } from 'redux'; import { RelayLocation } from '../../shared/daemon-rpc-types'; import SelectLocation from '../components/SelectLocation'; import RelaySettingsBuilder from '../lib/relay-settings-builder'; +import userInterfaceActions from '../redux/userinterface/actions'; +import { LocationScope } from '../redux/userinterface/reducers'; import { IReduxState, ReduxDispatch } from '../redux/store'; import { ISharedRouteProps } from '../routes'; -const mapStateToProps = (state: IReduxState) => ({ - relaySettings: state.settings.relaySettings, - relayLocations: state.settings.relayLocations, -}); +const mapStateToProps = (state: IReduxState) => { + let selectedExitLocation: RelayLocation | undefined; + let selectedBridgeLocation: RelayLocation | undefined; + + if ('normal' in state.settings.relaySettings) { + const exitLocation = state.settings.relaySettings.normal.location; + if (exitLocation !== 'any') { + selectedExitLocation = exitLocation; + } + } + + if ('normal' in state.settings.bridgeSettings) { + const bridgeLocation = state.settings.bridgeSettings.normal.location; + if (bridgeLocation !== 'any') { + selectedBridgeLocation = bridgeLocation; + } + } + + const allowBridgeSelection = state.settings.bridgeState === 'on'; + const locationScope = allowBridgeSelection + ? state.userInterface.locationScope + : LocationScope.relay; + + return { + selectedExitLocation, + selectedBridgeLocation, + relayLocations: state.settings.relayLocations, + bridgeLocations: state.settings.bridgeLocations, + locationScope, + allowBridgeSelection, + }; +}; const mapDispatchToProps = (dispatch: ReduxDispatch, props: ISharedRouteProps) => { const history = bindActionCreators({ goBack }, dispatch); + const userInterface = bindActionCreators(userInterfaceActions, dispatch); + return { onClose: () => history.goBack(), - onSelect: async (relayLocation: RelayLocation) => { + onChangeLocationScope: (scope: LocationScope) => { + userInterface.setLocationScope(scope); + }, + onSelectExitLocation: async (relayLocation: RelayLocation) => { // dismiss the view first history.goBack(); @@ -29,7 +64,17 @@ const mapDispatchToProps = (dispatch: ReduxDispatch, props: ISharedRouteProps) = await props.app.updateRelaySettings(relayUpdate); await props.app.connectTunnel(); } catch (e) { - log.error(`Failed to select server: ${e.message}`); + log.error(`Failed to select the exit location: ${e.message}`); + } + }, + onSelectBridgeLocation: async (bridgeLocation: RelayLocation) => { + // dismiss the view first + history.goBack(); + + try { + await props.app.updateBridgeLocation(bridgeLocation); + } catch (e) { + log.error(`Failed to select the bridge location: ${e.message}`); } }, }; diff --git a/gui/src/renderer/redux/settings/actions.ts b/gui/src/renderer/redux/settings/actions.ts index a7c766e7fd..6fee072cfd 100644 --- a/gui/src/renderer/redux/settings/actions.ts +++ b/gui/src/renderer/redux/settings/actions.ts @@ -1,6 +1,6 @@ import { BridgeState, IWireguardPublicKey, KeygenEvent } from '../../../shared/daemon-rpc-types'; import { IGuiSettingsState } from '../../../shared/gui-settings-state'; -import { IRelayLocationRedux, IWgKey, RelaySettingsRedux } from './reducers'; +import { BridgeSettingsRedux, IRelayLocationRedux, IWgKey, RelaySettingsRedux } from './reducers'; export interface IUpdateGuiSettingsAction { type: 'UPDATE_GUI_SETTINGS'; @@ -17,6 +17,11 @@ export interface IUpdateRelayLocationsAction { relayLocations: IRelayLocationRedux[]; } +export interface IUpdateBridgeLocationsAction { + type: 'UPDATE_BRIDGE_LOCATIONS'; + bridgeLocations: IRelayLocationRedux[]; +} + export interface IUpdateAllowLanAction { type: 'UPDATE_ALLOW_LAN'; allowLan: boolean; @@ -32,6 +37,11 @@ export interface IUpdateBlockWhenDisconnectedAction { blockWhenDisconnected: boolean; } +export interface IUpdateBridgeSettingsAction { + type: 'UPDATE_BRIDGE_SETTINGS'; + bridgeSettings: BridgeSettingsRedux; +} + export interface IUpdateBridgeStateAction { type: 'UPDATE_BRIDGE_STATE'; bridgeState: BridgeState; @@ -81,9 +91,11 @@ export type SettingsAction = | IUpdateGuiSettingsAction | IUpdateRelayAction | IUpdateRelayLocationsAction + | IUpdateBridgeLocationsAction | IUpdateAllowLanAction | IUpdateEnableIpv6Action | IUpdateBlockWhenDisconnectedAction + | IUpdateBridgeSettingsAction | IUpdateBridgeStateAction | IUpdateOpenVpnMssfixAction | IUpdateAutoStartAction @@ -115,6 +127,15 @@ function updateRelayLocations(relayLocations: IRelayLocationRedux[]): IUpdateRel }; } +function updateBridgeLocations( + bridgeLocations: IRelayLocationRedux[], +): IUpdateBridgeLocationsAction { + return { + type: 'UPDATE_BRIDGE_LOCATIONS', + bridgeLocations, + }; +} + function updateAllowLan(allowLan: boolean): IUpdateAllowLanAction { return { type: 'UPDATE_ALLOW_LAN', @@ -138,6 +159,13 @@ function updateBlockWhenDisconnected( }; } +function updateBridgeSettings(bridgeSettings: BridgeSettingsRedux): IUpdateBridgeSettingsAction { + return { + type: 'UPDATE_BRIDGE_SETTINGS', + bridgeSettings, + }; +} + function updateBridgeState(bridgeState: BridgeState): IUpdateBridgeStateAction { return { type: 'UPDATE_BRIDGE_STATE', @@ -211,9 +239,11 @@ export default { updateGuiSettings, updateRelay, updateRelayLocations, + updateBridgeLocations, updateAllowLan, updateEnableIpv6, updateBlockWhenDisconnected, + updateBridgeSettings, updateBridgeState, updateOpenVpnMssfix, updateAutoStart, diff --git a/gui/src/renderer/redux/settings/reducers.ts b/gui/src/renderer/redux/settings/reducers.ts index ff7d531652..80cc2ee5c9 100644 --- a/gui/src/renderer/redux/settings/reducers.ts +++ b/gui/src/renderer/redux/settings/reducers.ts @@ -3,6 +3,7 @@ import { BridgeState, KeygenEvent, LiftedConstraint, + ProxySettings, RelayLocation, RelayProtocol, TunnelProtocol, @@ -32,6 +33,16 @@ export type RelaySettingsRedux = }; }; +export type BridgeSettingsRedux = + | { + normal: { + location: LiftedConstraint<RelayLocation>; + }; + } + | { + custom: ProxySettings; + }; + export interface IRelayLocationRelayRedux { hostname: string; ipv4AddrIn: string; @@ -108,8 +119,10 @@ export interface ISettingsReduxState { guiSettings: IGuiSettingsState; relaySettings: RelaySettingsRedux; relayLocations: IRelayLocationRedux[]; + bridgeLocations: IRelayLocationRedux[]; allowLan: boolean; enableIpv6: boolean; + bridgeSettings: BridgeSettingsRedux; bridgeState: BridgeState; blockWhenDisconnected: boolean; openVpn: { @@ -138,8 +151,14 @@ const initialState: ISettingsReduxState = { }, }, relayLocations: [], + bridgeLocations: [], allowLan: false, enableIpv6: true, + bridgeSettings: { + normal: { + location: 'any', + }, + }, bridgeState: 'auto', blockWhenDisconnected: false, openVpn: {}, @@ -171,6 +190,12 @@ export default function( relayLocations: action.relayLocations, }; + case 'UPDATE_BRIDGE_LOCATIONS': + return { + ...state, + bridgeLocations: action.bridgeLocations, + }; + case 'UPDATE_ALLOW_LAN': return { ...state, @@ -204,6 +229,12 @@ export default function( autoStart: action.autoStart, }; + case 'UPDATE_BRIDGE_SETTINGS': + return { + ...state, + bridgeSettings: action.bridgeSettings, + }; + case 'UPDATE_BRIDGE_STATE': return { ...state, diff --git a/gui/src/renderer/redux/userinterface/actions.ts b/gui/src/renderer/redux/userinterface/actions.ts index 0af66cd365..f9e2fff444 100644 --- a/gui/src/renderer/redux/userinterface/actions.ts +++ b/gui/src/renderer/redux/userinterface/actions.ts @@ -1,3 +1,5 @@ +import { LocationScope } from './reducers'; + export interface IUpdateWindowArrowPositionAction { type: 'UPDATE_WINDOW_ARROW_POSITION'; arrowPosition: number; @@ -7,9 +9,15 @@ export interface IUpdateConnectionInfoOpenAction { type: 'TOGGLE_CONNECTION_PANEL'; } +export interface ISetLocationScopeAction { + type: 'SET_LOCATION_SCOPE'; + scope: LocationScope; +} + export type UserInterfaceAction = | IUpdateWindowArrowPositionAction - | IUpdateConnectionInfoOpenAction; + | IUpdateConnectionInfoOpenAction + | ISetLocationScopeAction; function updateWindowArrowPosition(arrowPosition: number): IUpdateWindowArrowPositionAction { return { @@ -24,4 +32,11 @@ function toggleConnectionPanel(): IUpdateConnectionInfoOpenAction { }; } -export default { updateWindowArrowPosition, toggleConnectionPanel }; +function setLocationScope(scope: LocationScope): ISetLocationScopeAction { + return { + type: 'SET_LOCATION_SCOPE', + scope, + }; +} + +export default { updateWindowArrowPosition, toggleConnectionPanel, setLocationScope }; diff --git a/gui/src/renderer/redux/userinterface/reducers.ts b/gui/src/renderer/redux/userinterface/reducers.ts index c6e7aafde8..729d5194b6 100644 --- a/gui/src/renderer/redux/userinterface/reducers.ts +++ b/gui/src/renderer/redux/userinterface/reducers.ts @@ -1,12 +1,19 @@ import { ReduxAction } from '../store'; +export enum LocationScope { + bridge = 0, + relay, +} + export interface IUserInterfaceReduxState { arrowPosition?: number; connectionPanelVisible: boolean; + locationScope: LocationScope; } const initialState: IUserInterfaceReduxState = { connectionPanelVisible: false, + locationScope: LocationScope.relay, }; export default function( @@ -20,6 +27,9 @@ export default function( case 'TOGGLE_CONNECTION_PANEL': return { ...state, connectionPanelVisible: !state.connectionPanelVisible }; + case 'SET_LOCATION_SCOPE': + return { ...state, locationScope: action.scope }; + default: return state; } |
