summaryrefslogtreecommitdiffhomepage
path: root/gui/src
diff options
context:
space:
mode:
Diffstat (limited to 'gui/src')
-rw-r--r--gui/src/renderer/components/Connect.tsx319
-rw-r--r--gui/src/renderer/components/MainView.tsx4
-rw-r--r--gui/src/renderer/containers/ConnectPage.tsx105
3 files changed, 174 insertions, 254 deletions
diff --git a/gui/src/renderer/components/Connect.tsx b/gui/src/renderer/components/Connect.tsx
index 35b13786bb..10ee3f209d 100644
--- a/gui/src/renderer/components/Connect.tsx
+++ b/gui/src/renderer/components/Connect.tsx
@@ -1,31 +1,27 @@
-import * as React from 'react';
+import { useCallback, useMemo } from 'react';
+import { sprintf } from 'sprintf-js';
import styled from 'styled-components';
-import { hasExpired } from '../../shared/account-expiry';
-import { AuthFailureKind, parseAuthFailure } from '../../shared/auth-failure';
-import NotificationArea from '../components/NotificationArea';
-import { LoginState } from '../redux/account/reducers';
-import { IConnectionReduxState } from '../redux/connection/reducers';
+import { messages, relayLocations } from '../../shared/gettext';
+import log from '../../shared/logging';
+import { useAppContext } from '../context';
+import { useHistory } from '../lib/history';
+import { RoutePath } from '../lib/routes';
+import { IRelayLocationRedux, RelaySettingsRedux } from '../redux/settings/reducers';
+import { useSelector } from '../redux/store';
import { calculateHeaderBarStyle, DefaultHeaderBar } from './HeaderBar';
import ImageView from './ImageView';
import { Container, Layout } from './Layout';
import Map, { MarkerStyle, ZoomLevel } from './Map';
+import NotificationArea from './NotificationArea';
import TunnelControl from './TunnelControl';
-interface IProps {
- connection: IConnectionReduxState;
- loginState: LoginState;
- accountExpiry?: string;
- blockWhenDisconnected: boolean;
- selectedRelayName: string;
- onSelectLocation: () => void;
- onConnect: () => void;
- onDisconnect: () => void;
- onReconnect: () => void;
-}
-
type MarkerOrSpinner = 'marker' | 'spinner' | 'none';
+const StyledContainer = styled(Container)({
+ position: 'relative',
+});
+
const StyledMap = styled(Map)({
position: 'absolute',
top: 0,
@@ -35,10 +31,6 @@ const StyledMap = styled(Map)({
zIndex: 0,
});
-const StyledContainer = styled(Container)({
- position: 'relative',
-});
-
const Content = styled.div({
display: 'flex',
flex: 1,
@@ -66,161 +58,194 @@ const StyledMain = styled.main({
flex: 1,
});
-interface IState {
- isAccountExpired: boolean;
-}
+export default function Connect() {
+ const history = useHistory();
+ const { connectTunnel, disconnectTunnel, reconnectTunnel } = useAppContext();
-export default class Connect extends React.Component<IProps, IState> {
- constructor(props: IProps) {
- super(props);
+ const connection = useSelector((state) => state.connection);
+ const blockWhenDisconnected = useSelector((state) => state.settings.blockWhenDisconnected);
+ const relaySettings = useSelector((state) => state.settings.relaySettings);
+ const relayLocations = useSelector((state) => state.settings.relayLocations);
- this.state = {
- isAccountExpired: this.checkAccountExpired(false),
- };
- }
+ const mapCenter = useMemo<[number, number] | undefined>(() => {
+ const { longitude, latitude } = connection;
+ return typeof longitude === 'number' && typeof latitude === 'number'
+ ? [longitude, latitude]
+ : undefined;
+ }, [connection]);
- public componentDidUpdate() {
- this.updateAccountExpired();
- }
+ const showMarkerOrSpinner = useMemo<MarkerOrSpinner>(() => {
+ if (!mapCenter) {
+ return 'none';
+ }
- public render() {
- return (
- <Layout>
- <DefaultHeaderBar barStyle={calculateHeaderBarStyle(this.props.connection.status)} />
- <StyledContainer>{this.renderMap()}</StyledContainer>
- </Layout>
- );
- }
+ switch (connection.status.state) {
+ case 'error':
+ return 'none';
+ case 'connecting':
+ case 'disconnecting':
+ return 'spinner';
+ case 'connected':
+ case 'disconnected':
+ return 'marker';
+ }
+ }, [mapCenter, connection.status.state]);
+
+ const markerStyle = useMemo<MarkerStyle>(() => {
+ switch (connection.status.state) {
+ case 'connecting':
+ case 'connected':
+ return MarkerStyle.secure;
+ case 'error':
+ return !connection.status.details.blockFailure ? MarkerStyle.secure : MarkerStyle.unsecure;
+ case 'disconnected':
+ return MarkerStyle.unsecure;
+ case 'disconnecting':
+ switch (connection.status.details) {
+ case 'block':
+ case 'reconnect':
+ return MarkerStyle.secure;
+ case 'nothing':
+ return MarkerStyle.unsecure;
+ }
+ }
+ }, [connection.status]);
- private updateAccountExpired() {
- const nextAccountExpired = this.checkAccountExpired(this.state.isAccountExpired);
+ const zoomLevel = useMemo<ZoomLevel>(() => {
+ const { longitude, latitude } = connection;
- if (nextAccountExpired !== this.state.isAccountExpired) {
- this.setState({
- isAccountExpired: nextAccountExpired,
- });
+ if (typeof longitude === 'number' && typeof latitude === 'number') {
+ return connection.status.state === 'connected' ? ZoomLevel.high : ZoomLevel.medium;
+ } else {
+ return ZoomLevel.low;
}
- }
+ }, [connection.latitude, connection.longitude, connection.status.state]);
+
+ const mapProps = useMemo<Map['props']>(() => {
+ return {
+ center: mapCenter ?? [0, 0],
+ showMarker: showMarkerOrSpinner === 'marker',
+ markerStyle,
+ zoomLevel,
+ // a magic offset to align marker with spinner
+ offset: [0, mapCenter ? 123 : 0],
+ };
+ }, [mapCenter, showMarkerOrSpinner, markerStyle, zoomLevel]);
+
+ const onSelectLocation = useCallback(() => {
+ history.show(RoutePath.selectLocation);
+ }, [history.show]);
- private checkAccountExpired(prevAccountExpired: boolean): boolean {
- const tunnelState = this.props.connection.status;
+ const selectedRelayName = useMemo(() => getRelayName(relaySettings, relayLocations), [
+ relaySettings,
+ relayLocations,
+ ]);
- // Blocked with auth failure / expired account
- if (
- tunnelState.state === 'error' &&
- tunnelState.details.cause.reason === 'auth_failed' &&
- parseAuthFailure(tunnelState.details.cause.reason).kind === AuthFailureKind.expiredAccount
- ) {
- return true;
+ const onConnect = useCallback(async () => {
+ try {
+ await connectTunnel();
+ } catch (e) {
+ const error = e as Error;
+ log.error(`Failed to connect the tunnel: ${error.message}`);
}
+ }, []);
- // Use the account expiry to deduce the account state
- if (this.props.accountExpiry) {
- return hasExpired(this.props.accountExpiry);
+ const onDisconnect = useCallback(async () => {
+ try {
+ await disconnectTunnel();
+ } catch (e) {
+ const error = e as Error;
+ log.error(`Failed to disconnect the tunnel: ${error.message}`);
}
+ }, []);
- // Do not assume that the account hasn't expired if the expiry is not available at the moment
- // instead return the last known state.
- return prevAccountExpired;
- }
+ const onReconnect = useCallback(async () => {
+ try {
+ await reconnectTunnel();
+ } catch (e) {
+ const error = e as Error;
+ log.error(`Failed to reconnect the tunnel: ${error.message}`);
+ }
+ }, []);
- private renderMap() {
- return (
- <>
- <StyledMap {...this.getMapProps()} />
+ return (
+ <Layout>
+ <DefaultHeaderBar barStyle={calculateHeaderBarStyle(connection.status)} />
+ <StyledContainer>
+ <StyledMap {...mapProps} />
<Content>
<StyledNotificationArea />
<StyledMain>
{/* show spinner when connecting */}
- {this.showMarkerOrSpinner() === 'spinner' ? (
+ {showMarkerOrSpinner === 'spinner' ? (
<StatusIcon source="icon-spinner" height={60} width={60} />
) : null}
<TunnelControl
- tunnelState={this.props.connection.status}
- blockWhenDisconnected={this.props.blockWhenDisconnected}
- selectedRelayName={this.props.selectedRelayName}
- city={this.props.connection.city}
- country={this.props.connection.country}
- onConnect={this.props.onConnect}
- onDisconnect={this.props.onDisconnect}
- onReconnect={this.props.onReconnect}
- onSelectLocation={this.props.onSelectLocation}
+ tunnelState={connection.status}
+ blockWhenDisconnected={blockWhenDisconnected}
+ selectedRelayName={selectedRelayName}
+ city={connection.city}
+ country={connection.country}
+ onConnect={onConnect}
+ onDisconnect={onDisconnect}
+ onReconnect={onReconnect}
+ onSelectLocation={onSelectLocation}
/>
</StyledMain>
</Content>
- </>
- );
- }
-
- private getMapProps(): Map['props'] {
- const mapCenter = this.getMapCenter();
-
- return {
- center: mapCenter ?? [0, 0],
- showMarker: this.showMarkerOrSpinner() === 'marker',
- markerStyle: this.getMarkerStyle(),
- zoomLevel: this.getZoomLevel(),
- // a magic offset to align marker with spinner
- offset: [0, mapCenter ? 123 : 0],
- };
- }
-
- private getMapCenter(): [number, number] | undefined {
- const { longitude, latitude } = this.props.connection;
-
- return typeof longitude === 'number' && typeof latitude === 'number'
- ? [longitude, latitude]
- : undefined;
- }
+ </StyledContainer>
+ </Layout>
+ );
+}
- private getMarkerStyle(): MarkerStyle {
- const { status } = this.props.connection;
+function getRelayName(relaySettings: RelaySettingsRedux, locations: IRelayLocationRedux[]): string {
+ if ('normal' in relaySettings) {
+ const location = relaySettings.normal.location;
- switch (status.state) {
- case 'connecting':
- case 'connected':
- return MarkerStyle.secure;
- case 'error':
- return !status.details.blockFailure ? MarkerStyle.secure : MarkerStyle.unsecure;
- case 'disconnected':
- return MarkerStyle.unsecure;
- case 'disconnecting':
- switch (status.details) {
- case 'block':
- case 'reconnect':
- return MarkerStyle.secure;
- case 'nothing':
- return MarkerStyle.unsecure;
+ if (location === 'any') {
+ return 'Automatic';
+ } else if ('country' in location) {
+ const country = locations.find(({ code }) => code === location.country);
+ if (country) {
+ return relayLocations.gettext(country.name);
+ }
+ } else if ('city' in location) {
+ const [countryCode, cityCode] = location.city;
+ const country = locations.find(({ code }) => code === countryCode);
+ if (country) {
+ const city = country.cities.find(({ code }) => code === cityCode);
+ if (city) {
+ return relayLocations.gettext(city.name);
}
+ }
+ } else if ('hostname' in location) {
+ const [countryCode, cityCode, hostname] = location.hostname;
+ const country = locations.find(({ code }) => code === countryCode);
+ if (country) {
+ const city = country.cities.find(({ code }) => code === cityCode);
+ if (city) {
+ return sprintf(
+ // TRANSLATORS: The selected location label displayed on the main view, when a user selected a specific host to connect to.
+ // TRANSLATORS: Example: Malmö (se-mma-001)
+ // TRANSLATORS: Available placeholders:
+ // TRANSLATORS: %(city)s - a city name
+ // TRANSLATORS: %(hostname)s - a hostname
+ messages.pgettext('connect-container', '%(city)s (%(hostname)s)'),
+ {
+ city: relayLocations.gettext(city.name),
+ hostname,
+ },
+ );
+ }
+ }
}
- }
-
- private showMarkerOrSpinner(): MarkerOrSpinner {
- if (!this.getMapCenter()) {
- return 'none';
- }
-
- switch (this.props.connection.status.state) {
- case 'error':
- return 'none';
- case 'connecting':
- case 'disconnecting':
- return 'spinner';
- case 'connected':
- case 'disconnected':
- return 'marker';
- }
- }
-
- private getZoomLevel(): ZoomLevel {
- const { longitude, latitude, status } = this.props.connection;
- if (typeof longitude === 'number' && typeof latitude === 'number') {
- return status.state === 'connected' ? ZoomLevel.high : ZoomLevel.medium;
- } else {
- return ZoomLevel.low;
- }
+ return 'Unknown';
+ } else if (relaySettings.customTunnelEndpoint) {
+ return 'Custom';
+ } else {
+ throw new Error('Unsupported relay settings.');
}
}
diff --git a/gui/src/renderer/components/MainView.tsx b/gui/src/renderer/components/MainView.tsx
index f59097854e..33f5f92ad5 100644
--- a/gui/src/renderer/components/MainView.tsx
+++ b/gui/src/renderer/components/MainView.tsx
@@ -1,7 +1,7 @@
import { useEffect, useState } from 'react';
import { hasExpired } from '../../shared/account-expiry';
-import ConnectPage from '../containers/ConnectPage';
+import Connect from '../components/Connect';
import ExpiredAccountErrorViewContainer from '../containers/ExpiredAccountErrorViewContainer';
import { useHistory } from '../lib/history';
import { RoutePath } from '../lib/routes';
@@ -39,6 +39,6 @@ export default function MainView() {
if (showAccountExpired.show) {
return <ExpiredAccountErrorViewContainer />;
} else {
- return <ConnectPage />;
+ return <Connect />;
}
}
diff --git a/gui/src/renderer/containers/ConnectPage.tsx b/gui/src/renderer/containers/ConnectPage.tsx
deleted file mode 100644
index aeba16e5d0..0000000000
--- a/gui/src/renderer/containers/ConnectPage.tsx
+++ /dev/null
@@ -1,105 +0,0 @@
-import { connect } from 'react-redux';
-import { sprintf } from 'sprintf-js';
-
-import { messages, relayLocations } from '../../shared/gettext';
-import log from '../../shared/logging';
-import Connect from '../components/Connect';
-import withAppContext, { IAppContext } from '../context';
-import { IHistoryProps, withHistory } from '../lib/history';
-import { RoutePath } from '../lib/routes';
-import { IRelayLocationRedux, RelaySettingsRedux } from '../redux/settings/reducers';
-import { IReduxState, ReduxDispatch } from '../redux/store';
-
-function getRelayName(relaySettings: RelaySettingsRedux, locations: IRelayLocationRedux[]): string {
- if ('normal' in relaySettings) {
- const location = relaySettings.normal.location;
-
- if (location === 'any') {
- return 'Automatic';
- } else if ('country' in location) {
- const country = locations.find(({ code }) => code === location.country);
- if (country) {
- return relayLocations.gettext(country.name);
- }
- } else if ('city' in location) {
- const [countryCode, cityCode] = location.city;
- const country = locations.find(({ code }) => code === countryCode);
- if (country) {
- const city = country.cities.find(({ code }) => code === cityCode);
- if (city) {
- return relayLocations.gettext(city.name);
- }
- }
- } else if ('hostname' in location) {
- const [countryCode, cityCode, hostname] = location.hostname;
- const country = locations.find(({ code }) => code === countryCode);
- if (country) {
- const city = country.cities.find(({ code }) => code === cityCode);
- if (city) {
- return sprintf(
- // TRANSLATORS: The selected location label displayed on the main view, when a user selected a specific host to connect to.
- // TRANSLATORS: Example: Malmö (se-mma-001)
- // TRANSLATORS: Available placeholders:
- // TRANSLATORS: %(city)s - a city name
- // TRANSLATORS: %(hostname)s - a hostname
- messages.pgettext('connect-container', '%(city)s (%(hostname)s)'),
- {
- city: relayLocations.gettext(city.name),
- hostname,
- },
- );
- }
- }
- }
-
- return 'Unknown';
- } else if (relaySettings.customTunnelEndpoint) {
- return 'Custom';
- } else {
- throw new Error('Unsupported relay settings.');
- }
-}
-
-const mapStateToProps = (state: IReduxState) => {
- return {
- accountExpiry: state.account.expiry,
- loginState: state.account.status,
- blockWhenDisconnected: state.settings.blockWhenDisconnected,
- selectedRelayName: getRelayName(state.settings.relaySettings, state.settings.relayLocations),
- connection: state.connection,
- };
-};
-
-const mapDispatchToProps = (_dispatch: ReduxDispatch, props: IHistoryProps & IAppContext) => {
- return {
- onSelectLocation: () => {
- props.history.show(RoutePath.selectLocation);
- },
- onConnect: async () => {
- try {
- await props.app.connectTunnel();
- } catch (e) {
- const error = e as Error;
- log.error(`Failed to connect the tunnel: ${error.message}`);
- }
- },
- onDisconnect: async () => {
- try {
- await props.app.disconnectTunnel();
- } catch (e) {
- const error = e as Error;
- log.error(`Failed to disconnect the tunnel: ${error.message}`);
- }
- },
- onReconnect: async () => {
- try {
- await props.app.reconnectTunnel();
- } catch (e) {
- const error = e as Error;
- log.error(`Failed to reconnect the tunnel: ${error.message}`);
- }
- },
- };
-};
-
-export default withAppContext(withHistory(connect(mapStateToProps, mapDispatchToProps)(Connect)));