summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--app/assets/images/icon-chevron-down.svg1
-rw-r--r--app/assets/images/icon-chevron-up.svg1
-rw-r--r--app/components/SelectLocation.css75
-rw-r--r--app/components/SelectLocation.js215
-rw-r--r--app/lib/ipc-facade.js6
-rw-r--r--test/components/SelectLocation.spec.js43
6 files changed, 260 insertions, 81 deletions
diff --git a/app/assets/images/icon-chevron-down.svg b/app/assets/images/icon-chevron-down.svg
new file mode 100644
index 0000000000..7d7419a75d
--- /dev/null
+++ b/app/assets/images/icon-chevron-down.svg
@@ -0,0 +1 @@
+<svg width="24" height="24" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><title>icon-cheveron-down</title><desc>Created with Sketch.</desc><defs><path d="M11.9497475,15.9497475 C11.6938252,15.9497475 11.4379028,15.8521164 11.2426407,15.6568542 L6.29289322,10.7071068 C5.90236893,10.3165825 5.90236893,9.68341751 6.29289322,9.29289322 C6.68341751,8.90236893 7.31658249,8.90236893 7.70710678,9.29289322 L11.9497475,13.5355339 L16.1923882,9.29289322 C16.5829124,8.90236893 17.2160774,8.90236893 17.6066017,9.29289322 C17.997126,9.68341751 17.997126,10.3165825 17.6066017,10.7071068 L12.6568542,15.6568542 C12.4615921,15.8521164 12.2056698,15.9497475 11.9497475,15.9497475 Z" id="path-1"/></defs><g id="Icons/chevron-down" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"><mask id="mask-2" fill="#fff"><use xlink:href="#path-1"/></mask><use id="Icon-shape" fill="#294D73" xlink:href="#path-1"/></g></svg> \ No newline at end of file
diff --git a/app/assets/images/icon-chevron-up.svg b/app/assets/images/icon-chevron-up.svg
new file mode 100644
index 0000000000..fa97da6e26
--- /dev/null
+++ b/app/assets/images/icon-chevron-up.svg
@@ -0,0 +1 @@
+<svg width="24" height="24" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><title>icon-cheveron-up</title><desc>Created with Sketch.</desc><defs><path d="M11.9497475,15.9497475 C11.6938252,15.9497475 11.4379028,15.8521164 11.2426407,15.6568542 L6.29289322,10.7071068 C5.90236893,10.3165825 5.90236893,9.68341751 6.29289322,9.29289322 C6.68341751,8.90236893 7.31658249,8.90236893 7.70710678,9.29289322 L11.9497475,13.5355339 L16.1923882,9.29289322 C16.5829124,8.90236893 17.2160774,8.90236893 17.6066017,9.29289322 C17.997126,9.68341751 17.997126,10.3165825 17.6066017,10.7071068 L12.6568542,15.6568542 C12.4615921,15.8521164 12.2056698,15.9497475 11.9497475,15.9497475 Z" id="path-1"/></defs><g id="Icons/chevron-up" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"><mask id="mask-2" fill="#fff"><use xlink:href="#path-1"/></mask><use id="Icon-shape" fill="#294D73" transform="translate(11.949747, 12.474874) scale(1, -1) translate(-11.949747, -12.474874)" xlink:href="#path-1"/></g></svg> \ No newline at end of file
diff --git a/app/components/SelectLocation.css b/app/components/SelectLocation.css
index cb63f61115..2bf7ec8adc 100644
--- a/app/components/SelectLocation.css
+++ b/app/components/SelectLocation.css
@@ -48,14 +48,17 @@
padding: 0 24px 24px;
}
-.select-location__separator {
- height: 24px;
-}
-
.select-location__cell {
background-color: rgba(41,71,115,1);
+ display: flex;
+ flex-direction: row;
+ align-items: stretch;
+}
+
+.select-location__cell-content {
padding: 15px 24px;
display: flex;
+ flex: 1 1 auto;
flex-direction: row;
align-items: center;
}
@@ -69,7 +72,7 @@
background-color: #44AD4D;
}
-.select-location__cell + .select-location__cell {
+.select-location__country + .select-location__country {
margin-top: 1px;
}
@@ -86,21 +89,61 @@
height: 24px;
flex: 0 0 auto;
margin-right: 8px;
+ align-items: center;
+ justify-content: center;
+ display: flex;
}
-.select-location__cell-value {
- flex: 0 0 auto;
+.select-location__cities {
+ margin-top: 1px;
}
-.select-location__cell-accessory {
- margin-left: auto;
+.select-location__collapse-button {
+ border: 0;
+ background: transparent;
+ padding: 0;
+ margin: 0 0 0 auto;
+ display: flex;
+ align-items: stretch;
+ padding: 12px;
}
-.select-location__cell-footer {
- padding: 8px 24px 24px;
- font-family: "Open Sans";
- font-size: 13px;
- font-weight: 600;
- line-height: 20px;
- color: rgba(255,255,255,0.8);
+.select-location__collapse-icon path {
+ fill: #fff;
}
+
+
+.select-location__sub-cell {
+ background-color: rgb(36, 57, 84);
+ padding: 15px 24px 15px 40px;
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+}
+
+.select-location__sub-cell:hover {
+ background-color: rgba(41,71,115,0.9);
+}
+
+.select-location__sub-cell + .select-location__sub-cell {
+ margin-top: 1px;
+}
+
+.select-location__sub-cell--selected,
+.select-location__sub-cell--selected:hover {
+ background-color: #44AD4D;
+}
+
+.select-location-relay-status {
+ width: 16px;
+ height: 16px;
+ border-radius: 8px;
+}
+
+.select-location-relay-status--inactive {
+ background: rgba(208, 2, 27, 0.95);
+}
+
+.select-location-relay-status--active {
+ background: rgba(68, 173, 77, 0.9);
+} \ No newline at end of file
diff --git a/app/components/SelectLocation.js b/app/components/SelectLocation.js
index e0f50a12d4..952d3882ea 100644
--- a/app/components/SelectLocation.js
+++ b/app/components/SelectLocation.js
@@ -2,11 +2,12 @@
import React, { Component } from 'react';
import { Layout, Container, Header } from './Layout';
import CustomScrollbars from './CustomScrollbars';
-import { servers } from '../config';
-import type { ServerInfo } from '../lib/backend';
+import ChevronDownSVG from '../assets/images/icon-chevron-down.svg';
+import ChevronUpSVG from '../assets/images/icon-chevron-up.svg';
+
import type { SettingsReduxState } from '../redux/settings/reducers';
-import type { RelayLocation } from '../lib/ipc-facade';
+import type { RelayLocation, RelayListCity, RelayListCountry } from '../lib/ipc-facade';
export type SelectLocationProps = {
settings: SettingsReduxState,
@@ -18,54 +19,25 @@ export default class SelectLocation extends Component {
props: SelectLocationProps;
_selectedCell: ?HTMLElement;
- _onSelect(location: RelayLocation) {
- if (!this._isSelected(location)) {
- this.props.onSelect(location);
- }
- }
+ state = {
+ expanded: ([]: Array<string>),
+ };
- _isSelected(selectedLocation: RelayLocation) {
- const { relaySettings } = this.props.settings;
- if(relaySettings.normal) {
- const otherLocation = relaySettings.normal.location;
+ constructor(props: SelectLocationProps, context?: any) {
+ super(props, context);
- if(selectedLocation.country && otherLocation.country &&
- selectedLocation.country === otherLocation.country) {
- return true;
- }
-
- if(Array.isArray(selectedLocation.city) && Array.isArray(otherLocation.city)) {
- const selectedCity = selectedLocation.city;
- const otherCity = otherLocation.city;
-
- return selectedCity.length === otherCity.length &&
- selectedCity.every((v, i) => v === otherCity[i]);
+ // set initially expanded country based on relaySettings
+ const relaySettings = this.props.settings.relaySettings;
+ if(relaySettings.normal) {
+ const { location } = relaySettings.normal;
+ if(location === 'any') {
+ // no-op
+ } else if(location.country) {
+ this.state.expanded.push(location.country);
+ } else if(location.city) {
+ this.state.expanded.push(location.city[0]);
}
}
- return false;
- }
-
- drawCell(key: string, name: string, selected: bool, icon: ?string, onClick: (e: Event) => void): React.Element<*> {
- const classes = ['select-location__cell'];
- if(selected) {
- classes.push('select-location__cell--selected');
- }
- const cellClass = classes.join(' ');
- const onRef = selected ? (element) => {
- this._selectedCell = element;
- } : undefined;
-
- return (
- <div key={ key } className={ cellClass } onClick={ onClick } ref={ onRef }>
-
- { icon && <img className="select-location__cell-icon" src={ icon } />}
-
- <div className="select-location__cell-label">{ name }</div>
-
- { selected && <img className="select-location__cell-accessory" src="./assets/images/icon-tick.svg" /> }
-
- </div>
- );
}
componentDidMount() {
@@ -81,7 +53,7 @@ export default class SelectLocation extends Component {
}
}
- render(): React.Element<*> {
+ render() {
return (
<Layout>
<Header hidden={ true } style={ 'defaultDark' } />
@@ -99,16 +71,8 @@ export default class SelectLocation extends Component {
While connected, your real location is masked with a private and secure location in the selected region
</div>
- <div className="select-location__separator"></div>
-
- { (servers: Array<ServerInfo>).map((server) => {
- const { address, name, country_code, city_code } = server;
- const relayLocation = {
- city: [ country_code, city_code ]
- };
- const selected = this._isSelected(relayLocation);
- const clickHandler = () => this._onSelect(relayLocation);
- return this.drawCell(address, name, selected, null, clickHandler);
+ { this.props.settings.relayLocations.countries.map((relayCountry) => {
+ return this._renderCountry(relayCountry);
}) }
</div>
@@ -119,4 +83,139 @@ export default class SelectLocation extends Component {
</Layout>
);
}
+
+ _onSelect(location: RelayLocation) {
+ if (!this._isSelected(location)) {
+ this.props.onSelect(location);
+ }
+ }
+
+ _isSelected(selectedLocation: RelayLocation) {
+ const { relaySettings } = this.props.settings;
+ if(relaySettings.normal) {
+ const otherLocation = relaySettings.normal.location;
+
+ if(selectedLocation.country && otherLocation.country &&
+ selectedLocation.country === otherLocation.country) {
+ return true;
+ }
+
+ if(Array.isArray(selectedLocation.city) && Array.isArray(otherLocation.city)) {
+ const selectedCity = selectedLocation.city;
+ const otherCity = otherLocation.city;
+
+ return selectedCity.length === otherCity.length &&
+ selectedCity.every((v, i) => v === otherCity[i]);
+ }
+ }
+ return false;
+ }
+
+ _toggleCollapse = (countryCode: string) => {
+ this.setState((state) => {
+ const expanded = state.expanded.slice();
+ const index = expanded.indexOf(countryCode);
+ if(index === -1) {
+ expanded.push(countryCode);
+ } else {
+ expanded.splice(index, 1);
+ }
+ return { expanded };
+ });
+ }
+
+ _relayStatusIndicator(active: boolean) {
+ const statusClass = active ? 'select-location-relay-status--active' : 'select-location-relay-status--inactive';
+
+ return (<div className={ 'select-location-relay-status ' + statusClass }></div>);
+ }
+
+ _renderCountry(relayCountry: RelayListCountry) {
+ const countryHasActiveRelays = relayCountry.cities.some((relayCity) => {
+ return relayCity.has_active_relays;
+ });
+
+ const isSelected = this._isSelected({ country: relayCountry.code });
+
+ // either expanded by user or when the city selected within the country
+ const isExpanded = this.state.expanded.includes(relayCountry.code);
+
+ const handleSelect = () => this._onSelect({ country: relayCountry.code });
+ const handleCollapse = (e) => {
+ this._toggleCollapse(relayCountry.code);
+ e.stopPropagation();
+ };
+
+ const countryClass = 'select-location__cell ' +
+ (isSelected ? 'select-location__cell--selected' : '');
+
+ const onRef = isSelected ? (element) => {
+ this._selectedCell = element;
+ } : undefined;
+
+ return (
+ <div key={ relayCountry.code } className="select-location__country">
+ <div className={ countryClass }
+ onClick={ handleSelect }
+ ref={ onRef }>
+ <div className="select-location__cell-content">
+
+ <div className="select-location__cell-icon">
+ { isSelected ?
+ <img src="./assets/images/icon-tick.svg" /> :
+ this._relayStatusIndicator(countryHasActiveRelays) }
+ </div>
+
+ <div className="select-location__cell-label">{ relayCountry.name }</div>
+ </div>
+
+ { countryHasActiveRelays && <button type="button" className="select-location__collapse-button" onClick={ handleCollapse }>
+ { isExpanded ?
+ <ChevronUpSVG className="select-location__collapse-icon" /> :
+ <ChevronDownSVG className="select-location__collapse-icon" /> }
+ </button> }
+
+ </div>
+
+ { isExpanded && countryHasActiveRelays && relayCountry.cities.length > 0 &&
+ (<div className="select-location__cities">
+ { relayCountry.cities.map((relayCity) => this._renderCity(relayCountry.code, relayCity)) }
+ </div>)
+ }
+ </div>
+ );
+ }
+
+ _renderCity(countryCode: string, relayCity: RelayListCity) {
+ const relayLocation: RelayLocation = { city: [countryCode, relayCity.code] };
+
+ const handleSelect = () => this._onSelect(relayLocation);
+
+ const isSelected = this._isSelected(relayLocation);
+ const key = countryCode + '_' + relayCity.code;
+
+ const cityClass = 'select-location__sub-cell ' +
+ (isSelected ? 'select-location__sub-cell--selected' : '');
+
+ const onRef = isSelected ? (element) => {
+ this._selectedCell = element;
+ } : undefined;
+
+ return (
+ <div key={ key }
+ className={ cityClass }
+ onClick={ handleSelect }
+ ref={ onRef }>
+
+ <div className="select-location__cell-icon">
+ { isSelected ?
+ <img src="./assets/images/icon-tick.svg" /> :
+ this._relayStatusIndicator(relayCity.has_active_relays) }
+ </div>
+
+ <div className="select-location__cell-label">{ relayCity.name }</div>
+ </div>
+ );
+ }
+
}
diff --git a/app/lib/ipc-facade.js b/app/lib/ipc-facade.js
index 2fa4212622..567a6ab8f1 100644
--- a/app/lib/ipc-facade.js
+++ b/app/lib/ipc-facade.js
@@ -1,7 +1,7 @@
// @flow
import JsonRpcWs, { InvalidReply } from './jsonrpc-ws-ipc';
-import { object, string, arrayOf, number, enumeration, oneOf } from 'validated/schema';
+import { object, string, number, boolean, enumeration, arrayOf, oneOf } from 'validated/schema';
import { validate } from 'validated/object';
import type { Coordinate2d } from '../types';
@@ -121,7 +121,8 @@ export type RelayListCountry = {
export type RelayListCity = {
name: string,
code: string,
- position: [Number, Number],
+ position: [number, number],
+ has_active_relays: boolean,
};
const RelayListSchema = object({
@@ -132,6 +133,7 @@ const RelayListSchema = object({
name: string,
code: string,
position: arrayOf(number),
+ has_active_relays: boolean,
})),
})),
});
diff --git a/test/components/SelectLocation.spec.js b/test/components/SelectLocation.spec.js
index 099594ca66..3266fe7aa9 100644
--- a/test/components/SelectLocation.spec.js
+++ b/test/components/SelectLocation.spec.js
@@ -18,7 +18,16 @@ describe('components/SelectLocation', () => {
}
},
relayLocations: {
- countries: [],
+ countries: [{
+ name: 'Sweden',
+ code: 'se',
+ cities: [{
+ name: 'Malmö',
+ code: 'mma',
+ position: [0, 0],
+ has_active_relays: true,
+ }],
+ }],
},
};
@@ -45,13 +54,37 @@ describe('components/SelectLocation', () => {
Simulate.click(domNode);
});
- it('should call select callback', (done) => {
+ it('should call select callback for country', (done) => {
const props = makeProps(state, {
- onSelect: (_server) => done()
+ onSelect: (location) => {
+ done(new Error('Fix me! ' + JSON.stringify(location)));
+ }
});
const elements = ReactTestUtils.scryRenderedDOMComponentsWithClass(render(props), 'select-location__cell');
- expect(elements).to.have.length.greaterThan(1);
- Simulate.click(elements[1]);
+ expect(elements).to.have.length(1);
+ Simulate.click(elements[0]);
+ });
+
+ it('should call select callback for city', (done) => {
+ const testState = {
+ ...state,
+ relaySettings: {
+ normal: {
+ // should auto-expand Sweden
+ location: { city: ['se', 'mma'] },
+ protocol: 'any',
+ port: 'any',
+ }
+ }
+ };
+ const props = makeProps(testState, {
+ onSelect: (location) => {
+ done(new Error('Fix me! ' + JSON.stringify(location)));
+ }
+ });
+ const elements = ReactTestUtils.scryRenderedDOMComponentsWithClass(render(props), 'select-location__sub-cell');
+ expect(elements).to.have.length(1);
+ Simulate.click(elements[0]);
});
});