// @flow import moment from 'moment'; import React, { Component } from 'react'; import { If, Then, Else } from 'react-if'; import ReactMapboxGl, { Marker } from 'react-mapbox-gl'; import cheapRuler from 'cheap-ruler'; import { Layout, Container, Header } from './Layout'; import { mapbox as mapboxConfig } from '../config'; import { BackendError } from '../lib/backend'; import ExternalLinkSVG from '../assets/images/icon-extLink.svg'; import type { Coordinate2d } from '../types'; import type { ServerInfo } from '../lib/backend'; import type { HeaderBarStyle } from './HeaderBar'; import type { AccountReduxState } from '../redux/account/reducers'; import type { ConnectionReduxState } from '../redux/connection/reducers'; import type { SettingsReduxState } from '../redux/settings/reducers'; type DisplayLocation = { location: Coordinate2d; country: ?string; city: ?string; }; export type ConnectProps = { account: AccountReduxState, connection: ConnectionReduxState, settings: SettingsReduxState, onSettings: () => void, onSelectLocation: () => void, onConnect: (address: string) => void, onCopyIP: () => void, onDisconnect: () => void, onExternalLink: (type: string) => void, getServerInfo: (identifier: string) => ?ServerInfo }; const ReactMap = ReactMapboxGl({ accessToken: mapboxConfig.accessToken, attributionControl: false, interactive: false }); export default class Connect extends Component { props: ConnectProps; state = { isFirstPass: true, showCopyIPMessage: false }; _copyTimer: ?number; componentDidMount() { this.setState({ isFirstPass: false }); } componentWillUnmount() { if(this._copyTimer) { clearTimeout(this._copyTimer); this._copyTimer = null; } this.setState({ isFirstPass: true, showCopyIPMessage: false }); } render(): React.Element<*> { const error = this.displayError(); const child = error ? this.renderError(error) : this.renderMap(); return (
{ child } ); } renderError(error: BackendError): React.Element<*> { return (
{ error.title }
{ error.message }
); } renderMap(): React.Element<*> { const preferredServer = this.props.settings.preferredServer; const serverInfo = this.props.getServerInfo(preferredServer); if(!serverInfo) { throw new Error('Server info cannot be null.'); } let isConnecting = false; let isConnected = false; let isDisconnected = false; switch(this.props.connection.status) { case 'connecting': isConnecting = true; break; case 'connected': isConnected = true; break; case 'disconnected': isDisconnected = true; break; } const altitude = (isConnecting ? 300 : 100) * 1000; const displayLocation = this.displayLocation(); const mapBounds = this.calculateMapBounds(displayLocation.location, altitude); const mapBoundsOptions = { offset: [0, -113], animate: !this.state.isFirstPass }; const accountLocation = this.convertToMapCoordinate(this.props.account.location || [0, 0]); const serverLocation = this.convertToMapCoordinate(serverInfo.location); const map = process.platform === 'darwin' ? : undefined; return (
{ map }
{ /* show spinner when connecting */ }
{ this.networkSecurityMessage() }
{ /* ********************************** Begin: Location block ********************************** */ } { /* location when connecting */ }
{ 'Fastest' } { 'Nearest' } { /* silly but react-if does not have ElseIf */ } { displayLocation.country }
{ /* location when connected */ }
{ displayLocation.city }
{ displayLocation.country }
{ /* location when disconnected */ }
{ displayLocation.country }
{ /* ********************************** End: Location block ********************************** */ }
{ 'IP copied to clipboard!' } { this.props.connection.clientIp }
{ /* ********************************** Begin: Footer block ********************************** */ } { /* footer when disconnected */ }
Connect to
{ serverInfo.name }
{ /* footer when connecting */ }
{ /* footer when connected */ }
{ /* ********************************** End: Footer block ********************************** */ }
); } // Handlers onConnect() { const { preferredServer } = this.props.settings; const serverInfo = this.props.getServerInfo(preferredServer); if(serverInfo) { this.props.onConnect(serverInfo.address); } } onExternalLink(type: string) { this.props.onExternalLink(type); } onIPAddressClick() { this._copyTimer && clearTimeout(this._copyTimer); this._copyTimer = setTimeout(() => this.setState({ showCopyIPMessage: false }), 3000); this.setState({ showCopyIPMessage: true }); this.props.onCopyIP(); } // Private headerStyle(): HeaderBarStyle { switch(this.props.connection.status) { case 'connecting': case 'disconnected': return 'error'; case 'connected': return 'success'; } throw new Error('Invalid ConnectionState'); } networkSecurityClass(): string { let classes = ['connect__status-security']; if(this.props.connection.status === 'connected') { classes.push('connect__status-security--secure'); } else if(this.props.connection.status === 'disconnected') { classes.push('connect__status-security--unsecured'); } return classes.join(' '); } networkSecurityMessage(): string { switch(this.props.connection.status) { case 'connected': return 'Secure connection'; case 'connecting': return 'Creating secure connection'; default: return 'Unsecured connection'; } } spinnerClass(): string { var classes = ['connect__status-icon']; if(this.props.connection.status !== 'connecting') { classes.push('connect__status-icon--hidden'); } return classes.join(' '); } ipAddressClass(): string { var classes = ['connect__status-ipaddress']; if(this.props.connection.status === 'connecting') { classes.push('connect__status-ipaddress--invisible'); } return classes.join(' '); } displayLocation(): DisplayLocation { // return user location when disconnected if(this.props.connection.status === 'disconnected') { let { location, country, city } = this.props.account; return { location: location || [0, 0], country, city }; } else { // otherwise server location const preferredServer = this.props.settings.preferredServer; const serverInfo = this.props.getServerInfo(preferredServer); if(serverInfo) { const { location, country, city } = serverInfo; return { location, country, city }; } throw new Error('Server location is not available.'); } } displayError(): ?BackendError { // Offline? if(!this.props.connection.isOnline) { return new BackendError('NO_INTERNET'); } // No credit? const { paidUntil } = this.props.account; if(paidUntil && moment(paidUntil).isSameOrBefore(moment())) { return new BackendError('NO_CREDIT'); } return null; } // Geo helpers calculateMapBounds(center: Coordinate2d, altitude: number): [Coordinate2d, Coordinate2d] { const bounds = cheapRuler(center[0], 'meters').bufferPoint(center, altitude); // convert [lat,lng] bounds to [lng,lat] return [ [bounds[1], bounds[0]], [bounds[3], bounds[2]] ]; } convertToMapCoordinate(pos: Coordinate2d): Coordinate2d { // convert [lat,lng] bounds to [lng,lat] return [pos[1], pos[0]]; } }