summaryrefslogtreecommitdiffhomepage
path: root/gui/src/renderer/components/main-view
diff options
context:
space:
mode:
authorOskar <oskar@mullvad.net>2024-11-05 07:57:08 +0100
committerOskar <oskar@mullvad.net>2024-11-14 16:43:18 +0100
commit84f14d79c4f0dde73337820ec94ba8ff928a3797 (patch)
treece468658e5ba7b0a74950c7ad1b09b3a4d00520b /gui/src/renderer/components/main-view
parente3ce0eb5cd0610dbff6ec98cb8cb388415c74bf6 (diff)
downloadmullvadvpn-84f14d79c4f0dde73337820ec94ba8ff928a3797.tar.xz
mullvadvpn-84f14d79c4f0dde73337820ec94ba8ff928a3797.zip
Move gui directory to desktop/packages/mullvad-vpn
Diffstat (limited to 'gui/src/renderer/components/main-view')
-rw-r--r--gui/src/renderer/components/main-view/ConnectionActionButton.tsx63
-rw-r--r--gui/src/renderer/components/main-view/ConnectionDetails.tsx202
-rw-r--r--gui/src/renderer/components/main-view/ConnectionPanel.tsx122
-rw-r--r--gui/src/renderer/components/main-view/ConnectionPanelChevron.tsx40
-rw-r--r--gui/src/renderer/components/main-view/ConnectionStatus.tsx58
-rw-r--r--gui/src/renderer/components/main-view/FeatureIndicators.tsx288
-rw-r--r--gui/src/renderer/components/main-view/Hostname.tsx69
-rw-r--r--gui/src/renderer/components/main-view/Location.tsx41
-rw-r--r--gui/src/renderer/components/main-view/MainView.tsx67
-rw-r--r--gui/src/renderer/components/main-view/SelectLocationButton.tsx143
-rw-r--r--gui/src/renderer/components/main-view/styles.ts7
11 files changed, 0 insertions, 1100 deletions
diff --git a/gui/src/renderer/components/main-view/ConnectionActionButton.tsx b/gui/src/renderer/components/main-view/ConnectionActionButton.tsx
deleted file mode 100644
index 437fa93321..0000000000
--- a/gui/src/renderer/components/main-view/ConnectionActionButton.tsx
+++ /dev/null
@@ -1,63 +0,0 @@
-import { useCallback } from 'react';
-import styled from 'styled-components';
-
-import { messages } from '../../../shared/gettext';
-import log from '../../../shared/logging';
-import { useAppContext } from '../../context';
-import { useSelector } from '../../redux/store';
-import { SmallButton, SmallButtonColor } from '../SmallButton';
-
-const StyledConnectionButton = styled(SmallButton)({
- margin: 0,
-});
-
-export default function ConnectionActionButton() {
- const tunnelState = useSelector((state) => state.connection.status.state);
-
- if (tunnelState === 'disconnected' || tunnelState === 'disconnecting') {
- return <ConnectButton disabled={tunnelState === 'disconnecting'} />;
- } else {
- return <DisconnectButton />;
- }
-}
-
-function ConnectButton(props: Partial<Parameters<typeof SmallButton>[0]>) {
- const { connectTunnel } = useAppContext();
-
- const onConnect = useCallback(async () => {
- try {
- await connectTunnel();
- } catch (e) {
- const error = e as Error;
- log.error(`Failed to connect the tunnel: ${error.message}`);
- }
- }, [connectTunnel]);
-
- return (
- <StyledConnectionButton color={SmallButtonColor.green} onClick={onConnect} {...props}>
- {messages.pgettext('tunnel-control', 'Connect')}
- </StyledConnectionButton>
- );
-}
-
-function DisconnectButton() {
- const { disconnectTunnel } = useAppContext();
- const tunnelState = useSelector((state) => state.connection.status.state);
-
- const onDisconnect = useCallback(async () => {
- try {
- await disconnectTunnel();
- } catch (e) {
- const error = e as Error;
- log.error(`Failed to disconnect the tunnel: ${error.message}`);
- }
- }, [disconnectTunnel]);
-
- const displayAsCancel = tunnelState !== 'connected';
-
- return (
- <StyledConnectionButton color={SmallButtonColor.red} onClick={onDisconnect}>
- {displayAsCancel ? messages.gettext('Cancel') : messages.gettext('Disconnect')}
- </StyledConnectionButton>
- );
-}
diff --git a/gui/src/renderer/components/main-view/ConnectionDetails.tsx b/gui/src/renderer/components/main-view/ConnectionDetails.tsx
deleted file mode 100644
index c6bff32ef3..0000000000
--- a/gui/src/renderer/components/main-view/ConnectionDetails.tsx
+++ /dev/null
@@ -1,202 +0,0 @@
-import { useEffect, useState } from 'react';
-import styled from 'styled-components';
-
-import { colors } from '../../../config.json';
-import {
- EndpointObfuscationType,
- ITunnelEndpoint,
- parseSocketAddress,
- ProxyType,
- RelayProtocol,
- TunnelState,
- TunnelType,
- tunnelTypeToString,
-} from '../../../shared/daemon-rpc-types';
-import { messages } from '../../../shared/gettext';
-import { useSelector } from '../../redux/store';
-import { tinyText } from '../common-styles';
-
-interface Endpoint {
- ip: string;
- port: number;
- protocol: RelayProtocol;
-}
-
-interface InAddress extends Endpoint {
- tunnelType: TunnelType;
-}
-
-interface BridgeData extends Endpoint {
- bridgeType: ProxyType;
-}
-
-interface ObfuscationData extends Endpoint {
- obfuscationType: EndpointObfuscationType;
-}
-
-const StyledConnectionDetailsHeading = styled.h2(tinyText, {
- margin: '0 0 4px',
- fontSize: '10px',
- lineHeight: '15px',
- color: colors.white60,
-});
-
-const StyledConnectionDetailsContainer = styled.div({
- marginTop: '16px',
- marginBottom: '16px',
-});
-
-const StyledIpTable = styled.div({
- display: 'grid',
- gridTemplateColumns: 'minmax(48px, min-content) auto',
-});
-
-const StyledIpLabelContainer = styled.div({
- display: 'flex',
- flexDirection: 'column',
-});
-
-const StyledConnectionDetailsLabel = styled.span(tinyText, {
- display: 'block',
- color: colors.white,
- fontWeight: '400',
- minHeight: '1em',
-});
-
-const StyledConnectionDetailsTitle = styled(StyledConnectionDetailsLabel)({
- color: colors.white60,
- whiteSpace: 'nowrap',
-});
-
-export default function ConnectionDetails() {
- const reduxConnection = useSelector((state) => state.connection);
- const [connection, setConnection] = useState(reduxConnection);
-
- const tunnelState = connection.status;
-
- useEffect(() => {
- if (
- reduxConnection.status.state === 'connected' ||
- reduxConnection.status.state === 'connecting'
- ) {
- setConnection(reduxConnection);
- }
- }, [reduxConnection, tunnelState.state]);
-
- const entry = getEntryPoint(tunnelState);
-
- const showDetails = tunnelState.state === 'connected' || tunnelState.state === 'connecting';
- const hasEntry = showDetails && entry !== undefined;
-
- return (
- <StyledConnectionDetailsContainer>
- <StyledConnectionDetailsHeading>
- {messages.pgettext('connect-view', 'Connection details')}
- </StyledConnectionDetailsHeading>
- <StyledConnectionDetailsLabel data-testid="tunnel-protocol">
- {showDetails &&
- tunnelState.details !== undefined &&
- tunnelTypeToString(tunnelState.details.endpoint.tunnelType)}
- </StyledConnectionDetailsLabel>
- <StyledIpTable>
- <StyledConnectionDetailsTitle>
- {messages.pgettext('connection-info', 'In')}
- </StyledConnectionDetailsTitle>
- <StyledConnectionDetailsLabel data-testid="in-ip">
- {hasEntry ? `${entry.ip}:${entry.port} ${entry.protocol.toUpperCase()}` : ''}
- </StyledConnectionDetailsLabel>
- <StyledConnectionDetailsTitle>
- {messages.pgettext('connection-info', 'Out')}
- </StyledConnectionDetailsTitle>
- <StyledIpLabelContainer>
- {connection.ipv4 && (
- <StyledConnectionDetailsLabel>{connection.ipv4}</StyledConnectionDetailsLabel>
- )}
- {connection.ipv6 && (
- <StyledConnectionDetailsLabel>{connection.ipv6}</StyledConnectionDetailsLabel>
- )}
- </StyledIpLabelContainer>
- </StyledIpTable>
- </StyledConnectionDetailsContainer>
- );
-}
-
-function getEntryPoint(tunnelState: TunnelState): Endpoint | undefined {
- if (
- (tunnelState.state !== 'connected' && tunnelState.state !== 'connecting') ||
- tunnelState.details === undefined
- ) {
- return undefined;
- }
-
- const endpoint = tunnelState.details.endpoint;
- const inAddress = tunnelEndpointToRelayInAddress(endpoint);
- const entryLocationInAddress = tunnelEndpointToEntryLocationInAddress(endpoint);
- const bridgeInfo = tunnelEndpointToBridgeData(endpoint);
- const obfuscationEndpoint = tunnelEndpointToObfuscationEndpoint(endpoint);
-
- if (obfuscationEndpoint) {
- return obfuscationEndpoint;
- } else if (entryLocationInAddress && inAddress) {
- return entryLocationInAddress;
- } else if (bridgeInfo && inAddress) {
- return bridgeInfo;
- } else {
- return inAddress;
- }
-}
-
-function tunnelEndpointToRelayInAddress(tunnelEndpoint: ITunnelEndpoint): InAddress {
- const socketAddr = parseSocketAddress(tunnelEndpoint.address);
- return {
- ip: socketAddr.host,
- port: socketAddr.port,
- protocol: tunnelEndpoint.protocol,
- tunnelType: tunnelEndpoint.tunnelType,
- };
-}
-
-function tunnelEndpointToEntryLocationInAddress(
- tunnelEndpoint: ITunnelEndpoint,
-): InAddress | undefined {
- if (!tunnelEndpoint.entryEndpoint) {
- return undefined;
- }
-
- const socketAddr = parseSocketAddress(tunnelEndpoint.entryEndpoint.address);
- return {
- ip: socketAddr.host,
- port: socketAddr.port,
- protocol: tunnelEndpoint.entryEndpoint.transportProtocol,
- tunnelType: tunnelEndpoint.tunnelType,
- };
-}
-
-function tunnelEndpointToBridgeData(endpoint: ITunnelEndpoint): BridgeData | undefined {
- if (!endpoint.proxy) {
- return undefined;
- }
-
- const socketAddr = parseSocketAddress(endpoint.proxy.address);
- return {
- ip: socketAddr.host,
- port: socketAddr.port,
- protocol: endpoint.proxy.protocol,
- bridgeType: endpoint.proxy.proxyType,
- };
-}
-
-function tunnelEndpointToObfuscationEndpoint(
- endpoint: ITunnelEndpoint,
-): ObfuscationData | undefined {
- if (!endpoint.obfuscationEndpoint) {
- return undefined;
- }
-
- return {
- ip: endpoint.obfuscationEndpoint.address,
- port: endpoint.obfuscationEndpoint.port,
- protocol: endpoint.obfuscationEndpoint.protocol,
- obfuscationType: endpoint.obfuscationEndpoint.obfuscationType,
- };
-}
diff --git a/gui/src/renderer/components/main-view/ConnectionPanel.tsx b/gui/src/renderer/components/main-view/ConnectionPanel.tsx
deleted file mode 100644
index 34e98abeea..0000000000
--- a/gui/src/renderer/components/main-view/ConnectionPanel.tsx
+++ /dev/null
@@ -1,122 +0,0 @@
-import { useCallback, useEffect } from 'react';
-import styled from 'styled-components';
-
-import { useBoolean } from '../../lib/utility-hooks';
-import { useSelector } from '../../redux/store';
-import CustomScrollbars from '../CustomScrollbars';
-import { BackAction } from '../KeyboardNavigation';
-import ConnectionActionButton from './ConnectionActionButton';
-import ConnectionDetails from './ConnectionDetails';
-import ConnectionPanelChevron from './ConnectionPanelChevron';
-import ConnectionStatus from './ConnectionStatus';
-import FeatureIndicators from './FeatureIndicators';
-import Hostname from './Hostname';
-import Location from './Location';
-import SelectLocationButton from './SelectLocationButton';
-import { ConnectionPanelAccordion } from './styles';
-
-const PANEL_MARGIN = '16px';
-
-const StyledAccordion = styled(ConnectionPanelAccordion)({
- flexShrink: 0,
-});
-
-const StyledConnectionPanel = styled.div<{ $expanded: boolean }>((props) => ({
- position: 'relative',
- display: 'flex',
- flexDirection: 'column',
- maxHeight: `calc(100% - 2 * ${PANEL_MARGIN})`,
- margin: `auto ${PANEL_MARGIN} ${PANEL_MARGIN}`,
- padding: '16px',
- justifySelf: 'flex-end',
- borderRadius: '12px',
- backgroundColor: `rgba(16, 24, 35, ${props.$expanded ? 0.8 : 0.4})`,
- backdropFilter: 'blur(6px)',
-
- transition: 'background-color 300ms ease-out',
-}));
-
-const StyledConnectionButtonContainer = styled.div({
- transition: 'margin-top 300ms ease-out',
- display: 'flex',
- flexDirection: 'column',
- gap: '16px',
- marginTop: '16px',
-});
-
-const StyledCustomScrollbars = styled(CustomScrollbars)({
- flexShrink: 1,
-});
-
-const StyledConnectionPanelChevron = styled(ConnectionPanelChevron)({
- position: 'absolute',
- top: '16px',
- right: '16px',
- width: 'fit-content',
-});
-
-const StyledConnectionStatusContainer = styled.div<{
- $expanded: boolean;
- $hasFeatureIndicators: boolean;
-}>((props) => ({
- paddingBottom: props.$hasFeatureIndicators || props.$expanded ? '16px' : 0,
- marginBottom: props.$expanded && props.$hasFeatureIndicators ? '16px' : 0,
- borderBottom: props.$expanded ? '1px rgba(255, 255, 255, 0.2) solid' : 'none',
- transitionProperty: 'margin-bottom, padding-bottom',
- transitionDuration: '300ms',
- transitionTimingFunction: 'ease-out',
-}));
-
-export default function ConnectionPanel() {
- const [expanded, expandImpl, collapse, toggleExpandedImpl] = useBoolean();
- const tunnelState = useSelector((state) => state.connection.status);
-
- const allowExpand = tunnelState.state === 'connected' || tunnelState.state === 'connecting';
-
- const expand = useCallback(() => {
- if (allowExpand) {
- expandImpl();
- }
- }, [allowExpand, expandImpl]);
-
- const toggleExpanded = useCallback(() => {
- if (allowExpand) {
- toggleExpandedImpl();
- }
- }, [allowExpand, toggleExpandedImpl]);
-
- const hasFeatureIndicators =
- allowExpand &&
- tunnelState.featureIndicators !== undefined &&
- tunnelState.featureIndicators.length > 0;
-
- useEffect(collapse, [tunnelState.state, collapse]);
-
- return (
- <BackAction disabled={!expanded} action={collapse}>
- <StyledConnectionPanel $expanded={expanded}>
- {allowExpand && (
- <StyledConnectionPanelChevron pointsUp={!expanded} onToggle={toggleExpanded} />
- )}
- <StyledConnectionStatusContainer
- $expanded={expanded}
- $hasFeatureIndicators={hasFeatureIndicators}
- onClick={toggleExpanded}>
- <ConnectionStatus />
- <Location />
- <Hostname />
- </StyledConnectionStatusContainer>
- <StyledCustomScrollbars>
- <FeatureIndicators expanded={expanded} expandIsland={expand} />
- <StyledAccordion expanded={expanded}>
- <ConnectionDetails />
- </StyledAccordion>
- </StyledCustomScrollbars>
- <StyledConnectionButtonContainer>
- <SelectLocationButton />
- <ConnectionActionButton />
- </StyledConnectionButtonContainer>
- </StyledConnectionPanel>
- </BackAction>
- );
-}
diff --git a/gui/src/renderer/components/main-view/ConnectionPanelChevron.tsx b/gui/src/renderer/components/main-view/ConnectionPanelChevron.tsx
deleted file mode 100644
index a50fada589..0000000000
--- a/gui/src/renderer/components/main-view/ConnectionPanelChevron.tsx
+++ /dev/null
@@ -1,40 +0,0 @@
-import styled from 'styled-components';
-
-import { colors } from '../../../config.json';
-import ImageView from '../ImageView';
-
-const Container = styled.button({
- display: 'flex',
- alignItems: 'center',
- width: '100%',
- background: 'none',
- border: 'none',
-});
-
-const Chevron = styled(ImageView)({
- [Container + ':hover &&']: {
- backgroundColor: colors.white80,
- },
-});
-
-interface IProps {
- pointsUp: boolean;
- onToggle?: () => void;
- className?: string;
-}
-
-export default function ConnectionPanelChevron(props: IProps) {
- return (
- <Container
- data-testid="connection-panel-chevron"
- className={props.className}
- onClick={props.onToggle}>
- <Chevron
- source={props.pointsUp ? 'icon-chevron-up' : 'icon-chevron-down'}
- width={24}
- height={24}
- tintColor={colors.white}
- />
- </Container>
- );
-}
diff --git a/gui/src/renderer/components/main-view/ConnectionStatus.tsx b/gui/src/renderer/components/main-view/ConnectionStatus.tsx
deleted file mode 100644
index 1745fdaf11..0000000000
--- a/gui/src/renderer/components/main-view/ConnectionStatus.tsx
+++ /dev/null
@@ -1,58 +0,0 @@
-import styled from 'styled-components';
-
-import { colors } from '../../../config.json';
-import { TunnelState } from '../../../shared/daemon-rpc-types';
-import { messages } from '../../../shared/gettext';
-import { useSelector } from '../../redux/store';
-import { largeText } from '../common-styles';
-
-const StyledConnectionStatus = styled.span<{ $color: string }>(largeText, (props) => ({
- minHeight: '24px',
- color: props.$color,
- marginBottom: '4px',
-}));
-
-export default function ConnectionStatus() {
- const tunnelState = useSelector((state) => state.connection.status);
- const lockdownMode = useSelector((state) => state.settings.blockWhenDisconnected);
-
- const color = getConnectionSTatusLabelColor(tunnelState, lockdownMode);
- const text = getConnectionStatusLabelText(tunnelState);
-
- return (
- <StyledConnectionStatus role="status" $color={color}>
- {text}
- </StyledConnectionStatus>
- );
-}
-
-function getConnectionSTatusLabelColor(tunnelState: TunnelState, lockdownMode: boolean) {
- switch (tunnelState.state) {
- case 'connected':
- return colors.green;
- case 'connecting':
- case 'disconnecting':
- return colors.white;
- case 'disconnected':
- return lockdownMode ? colors.white : colors.red;
- case 'error':
- return tunnelState.details.blockingError ? colors.red : colors.white;
- }
-}
-
-function getConnectionStatusLabelText(tunnelState: TunnelState) {
- switch (tunnelState.state) {
- case 'connected':
- return messages.gettext('CONNECTED');
- case 'connecting':
- return messages.gettext('CONNECTING...');
- case 'disconnecting':
- return messages.gettext('DISCONNECTING...');
- case 'disconnected':
- return messages.gettext('DISCONNECTED');
- case 'error':
- return tunnelState.details.blockingError
- ? messages.gettext('FAILED TO SECURE CONNECTION')
- : messages.gettext('BLOCKED CONNECTION');
- }
-}
diff --git a/gui/src/renderer/components/main-view/FeatureIndicators.tsx b/gui/src/renderer/components/main-view/FeatureIndicators.tsx
deleted file mode 100644
index 5b7e60ade0..0000000000
--- a/gui/src/renderer/components/main-view/FeatureIndicators.tsx
+++ /dev/null
@@ -1,288 +0,0 @@
-import { useEffect, useRef } from 'react';
-import { sprintf } from 'sprintf-js';
-import styled from 'styled-components';
-
-import { colors, strings } from '../../../config.json';
-import { FeatureIndicator } from '../../../shared/daemon-rpc-types';
-import { messages } from '../../../shared/gettext';
-import { useStyledRef } from '../../lib/utility-hooks';
-import { useSelector } from '../../redux/store';
-import { tinyText } from '../common-styles';
-import { InfoIcon } from '../InfoButton';
-import { ConnectionPanelAccordion } from './styles';
-
-const LINE_HEIGHT = 22;
-const GAP = 8;
-
-const StyledAccordion = styled(ConnectionPanelAccordion)({
- flexShrink: 0,
-});
-
-const StyledFeatureIndicatorsContainer = styled.div<{ $expanded: boolean }>((props) => ({
- marginTop: '0px',
- marginBottom: props.$expanded ? '8px' : 0,
- transition: 'margin-bottom 300ms ease-out',
-}));
-
-const StyledTitle = styled.h2(tinyText, {
- margin: '0 0 2px',
- fontSize: '10px',
- lineHeight: '15px',
- color: colors.white60,
-});
-
-const StyledFeatureIndicators = styled.div({
- position: 'relative',
-});
-
-const StyledFeatureIndicatorsWrapper = styled.div<{ $expanded: boolean }>((props) => ({
- display: 'flex',
- flexWrap: 'wrap',
- gap: `${GAP}px`,
- maxHeight: props.$expanded ? 'fit-content' : '52px',
- overflow: 'hidden',
-}));
-
-const StyledFeatureIndicatorLabel = styled.span(tinyText, (props) => ({
- display: 'flex',
- gap: '4px',
- padding: '1px 7px',
- justifyContent: 'center',
- alignItems: 'center',
- borderRadius: '4px',
- background: colors.darkerBlue,
- color: colors.white,
- fontWeight: 400,
- whiteSpace: 'nowrap',
- visibility: 'hidden',
-
- // Style clickable feature indicators with a border and on-hover effect
- boxSizing: 'border-box', // make border act as padding rather than margin
- border: 'solid 1px',
- borderColor: props.onClick ? colors.blue : colors.darkerBlue,
- transition: 'background ease-in-out 300ms',
- '&&:hover': {
- background: props.onClick ? colors.blue60 : undefined,
- },
-}));
-
-const StyledBaseEllipsis = styled.span<{ $display: boolean }>(tinyText, (props) => ({
- position: 'absolute',
- top: `${LINE_HEIGHT + GAP}px`,
- color: colors.white,
- padding: '2px 8px 2px 16px',
- display: props.$display ? 'inline' : 'none',
-}));
-
-const StyledEllipsisSpacer = styled(StyledBaseEllipsis)({
- right: 0,
- opacity: 0,
-});
-
-const StyledEllipsis = styled(StyledBaseEllipsis)({
- visibility: 'hidden',
-});
-
-interface FeatureIndicatorsProps {
- expanded: boolean;
- expandIsland: () => void;
-}
-
-// This component needs to render a maximum of two lines of feature indicators and then ellipsis
-// with the text "N more...". This poses two challenges:
-// 1. We can't know the size of the content beforehand or how many indicators should be hidden
-// 2. The ellipsis string doesn't have a fixed width, the amount can change.
-//
-// To solve this the indicators are first rendered hidden along with a invisible "placeholder"
-// ellipsis at the end of the second row. Then after render, all indicators that either is placed
-// after the second row or overlaps with the invisible ellipsis text will be set to invisible. Then
-// we can count those and add another ellipsis element which is visible and place it after the last
-// visible indicator.
-export default function FeatureIndicators(props: FeatureIndicatorsProps) {
- const tunnelState = useSelector((state) => state.connection.status);
- const ellipsisRef = useStyledRef<HTMLSpanElement>();
- const ellipsisSpacerRef = useStyledRef<HTMLSpanElement>();
- const featureIndicatorsContainerRef = useStyledRef<HTMLDivElement>();
-
- const featureIndicatorsVisible =
- tunnelState.state === 'connected' || tunnelState.state === 'connecting';
-
- const featureIndicators = useRef(
- featureIndicatorsVisible ? (tunnelState.featureIndicators ?? []) : [],
- );
-
- if (featureIndicatorsVisible && tunnelState.featureIndicators) {
- featureIndicators.current = tunnelState.featureIndicators;
- }
-
- const ellipsis = messages.gettext('%(amount)d more...');
-
- // Returns an optional callback for clickable feature indicators, or undefined.
- const getFeatureIndicatorOnClick = (indicator: FeatureIndicator) => {
- // NOTE: With the "smart routing" feature indicator removed, this function now does nothing, should it be removed?
- switch (indicator) {
- default:
- return undefined;
- }
- };
-
- useEffect(() => {
- // We need to defer the visibility logic one painting cycle to make sure the elements are
- // rendered and available.
- setTimeout(() => {
- if (
- featureIndicatorsContainerRef.current &&
- ellipsisSpacerRef.current &&
- ellipsisRef.current
- ) {
- // Get all feature indicator elements.
- const indicatorElements = Array.from(
- featureIndicatorsContainerRef.current.getElementsByTagName('span'),
- );
-
- let lastVisibleIndex = 0;
- let hasHidden = false;
- indicatorElements.forEach((indicatorElement, i) => {
- if (
- indicatorShouldBeVisible(
- props.expanded,
- featureIndicatorsContainerRef.current!,
- indicatorElement,
- ellipsisSpacerRef.current!,
- )
- ) {
- // If an indicator should be visible we set its visibility and increment the variable
- // containing the last visible index.
- indicatorElement.style.visibility = 'visible';
- lastVisibleIndex = i;
- } else {
- indicatorElement.style.visibility = 'hidden';
- // If it should be visible we store that there exists hidden indicators.
- hasHidden = true;
- }
- });
-
- if (hasHidden) {
- const lastVisibleIndicatorRect =
- indicatorElements[lastVisibleIndex].getBoundingClientRect();
- const containerRect = featureIndicatorsContainerRef.current.getBoundingClientRect();
-
- // Place the ellipsis at the end of the last visible indicator.
- const left = lastVisibleIndicatorRect.right - containerRect.left;
- // eslint-disable-next-line react-compiler/react-compiler
- ellipsisRef.current.style.left = `${left}px`;
- ellipsisRef.current.style.visibility = 'visible';
-
- // Add the ellipsis text to the ellipsis.
- ellipsisRef.current.textContent = sprintf(ellipsis, {
- amount: indicatorElements.length - (lastVisibleIndex + 1),
- });
- } else {
- ellipsisRef.current.style.visibility = 'hidden';
- }
- }
- }, 0);
- });
-
- const sortedIndicators = [...featureIndicators.current].sort((a, b) => a - b);
-
- return (
- <StyledAccordion expanded={featureIndicatorsVisible && featureIndicators.current.length > 0}>
- <StyledFeatureIndicatorsContainer $expanded={props.expanded}>
- <StyledAccordion expanded={props.expanded}>
- <StyledTitle>{messages.pgettext('connect-view', 'Active features')}</StyledTitle>
- </StyledAccordion>
- <StyledFeatureIndicators>
- <StyledFeatureIndicatorsWrapper
- ref={featureIndicatorsContainerRef}
- $expanded={props.expanded}>
- {sortedIndicators.map((indicator) => {
- const onClick = getFeatureIndicatorOnClick(indicator);
- return (
- <StyledFeatureIndicatorLabel
- key={indicator.toString()}
- data-testid="feature-indicator"
- onClick={onClick}>
- {getFeatureIndicatorLabel(indicator)}
- {onClick ? <InfoIcon size={10} /> : null}
- </StyledFeatureIndicatorLabel>
- );
- })}
- </StyledFeatureIndicatorsWrapper>
- <StyledEllipsisSpacer $display={!props.expanded} ref={ellipsisSpacerRef}>
- {
- // Mock amount for the spacer ellipsis. This needs to be wider than the real
- // ellipsis will ever be.
- sprintf(ellipsis, { amount: 222 })
- }
- </StyledEllipsisSpacer>
- <StyledEllipsis
- onClick={props.expandIsland}
- $display={!props.expanded}
- ref={ellipsisRef}
- />
- </StyledFeatureIndicators>
- </StyledFeatureIndicatorsContainer>
- </StyledAccordion>
- );
-}
-
-function indicatorShouldBeVisible(
- expanded: boolean,
- container: HTMLElement,
- indicator: HTMLElement,
- ellipsisSpacer: HTMLElement,
-): boolean {
- if (expanded) {
- return true;
- }
-
- const indicatorRect = indicator.getBoundingClientRect();
- const ellipsisSpacerRect = ellipsisSpacer.getBoundingClientRect();
- const containerRect = container.getBoundingClientRect();
-
- // Calculate which line the indicator is positioned on.
- const lineIndex = Math.round((indicatorRect.top - containerRect.top) / (LINE_HEIGHT + GAP));
-
- // An indicator should be visible if it's on the first line or if it is on the second line and
- // doesn't overlap with the ellipsis.
- return lineIndex === 0 || (lineIndex === 1 && indicatorRect.right < ellipsisSpacerRect.left);
-}
-
-function getFeatureIndicatorLabel(indicator: FeatureIndicator) {
- switch (indicator) {
- case FeatureIndicator.daita:
- return strings.daita;
- case FeatureIndicator.udp2tcp:
- case FeatureIndicator.shadowsocks:
- return messages.pgettext('wireguard-settings-view', 'Obfuscation');
- case FeatureIndicator.multihop:
- // TRANSLATORS: This refers to the multihop setting in the VPN settings view. This is
- // TRANSLATORS: displayed when the feature is on.
- return messages.gettext('Multihop');
- case FeatureIndicator.customDns:
- // TRANSLATORS: This refers to the Custom DNS setting in the VPN settings view. This is
- // TRANSLATORS: displayed when the feature is on.
- return messages.gettext('Custom DNS');
- case FeatureIndicator.customMtu:
- return messages.pgettext('wireguard-settings-view', 'MTU');
- case FeatureIndicator.bridgeMode:
- return messages.pgettext('openvpn-settings-view', 'Bridge mode');
- case FeatureIndicator.lanSharing:
- return messages.pgettext('vpn-settings-view', 'Local network sharing');
- case FeatureIndicator.customMssFix:
- return messages.pgettext('openvpn-settings-view', 'Mssfix');
- case FeatureIndicator.lockdownMode:
- return messages.pgettext('vpn-settings-view', 'Lockdown mode');
- case FeatureIndicator.splitTunneling:
- return strings.splitTunneling;
- case FeatureIndicator.serverIpOverride:
- return messages.pgettext('settings-import', 'Server IP override');
- case FeatureIndicator.quantumResistance:
- // TRANSLATORS: This refers to the quantum resistance setting in the WireGuard settings view.
- // TRANSLATORS: This is displayed when the feature is on.
- return messages.gettext('Quantum resistance');
- case FeatureIndicator.dnsContentBlockers:
- return messages.pgettext('vpn-settings-view', 'DNS content blockers');
- }
-}
diff --git a/gui/src/renderer/components/main-view/Hostname.tsx b/gui/src/renderer/components/main-view/Hostname.tsx
deleted file mode 100644
index 097e3b291b..0000000000
--- a/gui/src/renderer/components/main-view/Hostname.tsx
+++ /dev/null
@@ -1,69 +0,0 @@
-import { sprintf } from 'sprintf-js';
-import styled from 'styled-components';
-
-import { colors } from '../../../config.json';
-import { messages } from '../../../shared/gettext';
-import { IConnectionReduxState } from '../../redux/connection/reducers';
-import { useSelector } from '../../redux/store';
-import { smallText } from '../common-styles';
-import Marquee from '../Marquee';
-import { ConnectionPanelAccordion } from './styles';
-
-const StyledAccordion = styled(ConnectionPanelAccordion)({
- flexShrink: 0,
-});
-
-const StyledHostname = styled.span(smallText, {
- color: colors.white60,
- fontWeight: '400',
- flexShrink: 0,
- minHeight: '1em',
-});
-
-export default function Hostname() {
- const tunnelState = useSelector((state) => state.connection.status.state);
- const connection = useSelector((state) => state.connection);
- const text = getHostnameText(connection);
-
- return (
- <StyledAccordion expanded={tunnelState === 'connecting' || tunnelState === 'connected'}>
- <StyledHostname data-testid="hostname-line">
- <Marquee>{text}</Marquee>
- </StyledHostname>
- </StyledAccordion>
- );
-}
-
-function getHostnameText(connection: IConnectionReduxState) {
- let hostname = '';
-
- if (connection.hostname && connection.bridgeHostname) {
- hostname = sprintf(messages.pgettext('connection-info', '%(relay)s via %(entry)s'), {
- relay: connection.hostname,
- entry: connection.bridgeHostname,
- });
- } else if (connection.hostname && connection.entryHostname) {
- hostname = sprintf(
- // TRANSLATORS: The hostname line displayed below the country on the main screen
- // TRANSLATORS: Available placeholders:
- // TRANSLATORS: %(relay)s - the relay hostname
- // TRANSLATORS: %(entry)s - the entry relay hostname
- messages.pgettext('connection-info', '%(relay)s via %(entry)s'),
- {
- relay: connection.hostname,
- entry: connection.entryHostname,
- },
- );
- } else if (
- (connection.status.state === 'connecting' || connection.status.state === 'connected') &&
- connection.status.details?.endpoint.proxy !== undefined
- ) {
- hostname = sprintf(messages.pgettext('connection-info', '%(relay)s via Custom bridge'), {
- relay: connection.hostname,
- });
- } else if (connection.hostname) {
- hostname = connection.hostname;
- }
-
- return hostname;
-}
diff --git a/gui/src/renderer/components/main-view/Location.tsx b/gui/src/renderer/components/main-view/Location.tsx
deleted file mode 100644
index 2685394158..0000000000
--- a/gui/src/renderer/components/main-view/Location.tsx
+++ /dev/null
@@ -1,41 +0,0 @@
-import styled from 'styled-components';
-
-import { colors } from '../../../config.json';
-import { TunnelState } from '../../../shared/daemon-rpc-types';
-import { useSelector } from '../../redux/store';
-import { largeText } from '../common-styles';
-import Marquee from '../Marquee';
-import { ConnectionPanelAccordion } from './styles';
-
-const StyledLocation = styled.span(largeText, {
- color: colors.white,
- flexShrink: 0,
-});
-
-export default function Location() {
- const connection = useSelector((state) => state.connection);
- const text = getLocationText(connection.status, connection.country, connection.city);
-
- return (
- <ConnectionPanelAccordion expanded={connection.status.state !== 'error'}>
- <StyledLocation>
- <Marquee>{text}</Marquee>
- </StyledLocation>
- </ConnectionPanelAccordion>
- );
-}
-
-function getLocationText(tunnelState: TunnelState, country?: string, city?: string): string {
- country = country ?? '';
-
- switch (tunnelState.state) {
- case 'connected':
- case 'connecting':
- return city ? `${country}, ${city}` : country;
- case 'disconnecting':
- case 'disconnected':
- return country;
- case 'error':
- return '';
- }
-}
diff --git a/gui/src/renderer/components/main-view/MainView.tsx b/gui/src/renderer/components/main-view/MainView.tsx
deleted file mode 100644
index 9327094a51..0000000000
--- a/gui/src/renderer/components/main-view/MainView.tsx
+++ /dev/null
@@ -1,67 +0,0 @@
-import styled from 'styled-components';
-
-import { useSelector } from '../../redux/store';
-import { calculateHeaderBarStyle, DefaultHeaderBar } from '../HeaderBar';
-import ImageView from '../ImageView';
-import { Container, Layout } from '../Layout';
-import Map from '../Map';
-import NotificationArea from '../NotificationArea';
-import ConnectionPanel from './ConnectionPanel';
-
-const StyledContainer = styled(Container)({
- position: 'relative',
-});
-
-const Content = styled.div({
- display: 'flex',
- flex: 1,
- flexDirection: 'column',
- position: 'relative', // need this for z-index to work to cover the map
- zIndex: 1,
- maxHeight: '100%',
-});
-
-const StatusIcon = styled(ImageView)({
- position: 'absolute',
- alignSelf: 'center',
- marginTop: 94,
-});
-
-const StyledNotificationArea = styled(NotificationArea)({
- position: 'absolute',
- left: 0,
- top: 0,
- right: 0,
-});
-
-const StyledMain = styled.main({
- display: 'flex',
- flexDirection: 'column',
- flex: 1,
- maxHeight: '100%',
-});
-
-export default function MainView() {
- const connection = useSelector((state) => state.connection);
-
- const showSpinner =
- connection.status.state === 'connecting' || connection.status.state === 'disconnecting';
-
- return (
- <Layout>
- <DefaultHeaderBar barStyle={calculateHeaderBarStyle(connection.status)} />
- <StyledContainer>
- <Map />
- <Content>
- <StyledNotificationArea />
-
- <StyledMain>
- {showSpinner ? <StatusIcon source="icon-spinner" height={60} width={60} /> : null}
-
- <ConnectionPanel />
- </StyledMain>
- </Content>
- </StyledContainer>
- </Layout>
- );
-}
diff --git a/gui/src/renderer/components/main-view/SelectLocationButton.tsx b/gui/src/renderer/components/main-view/SelectLocationButton.tsx
deleted file mode 100644
index 50508a3192..0000000000
--- a/gui/src/renderer/components/main-view/SelectLocationButton.tsx
+++ /dev/null
@@ -1,143 +0,0 @@
-import { useCallback, useMemo } from 'react';
-import { sprintf } from 'sprintf-js';
-import styled from 'styled-components';
-
-import { ICustomList } from '../../../shared/daemon-rpc-types';
-import { messages, relayLocations } from '../../../shared/gettext';
-import log from '../../../shared/logging';
-import { useAppContext } from '../../context';
-import { transitions, useHistory } from '../../lib/history';
-import { RoutePath } from '../../lib/routes';
-import { IRelayLocationCountryRedux, RelaySettingsRedux } from '../../redux/settings/reducers';
-import { useSelector } from '../../redux/store';
-import ImageView from '../ImageView';
-import { MultiButton, MultiButtonCompatibleProps } from '../MultiButton';
-import { SmallButton, SmallButtonColor } from '../SmallButton';
-
-const StyledSmallButton = styled(SmallButton)({
- margin: 0,
-});
-
-const StyledReconnectButton = styled(StyledSmallButton)({
- padding: '4px 8px 4px 8px',
-});
-
-export default function SelectLocationButtons() {
- const tunnelState = useSelector((state) => state.connection.status.state);
-
- if (tunnelState === 'connecting' || tunnelState === 'connected') {
- return <MultiButton mainButton={SelectLocationButton} sideButton={ReconnectButton} />;
- } else {
- return <SelectLocationButton />;
- }
-}
-
-function SelectLocationButton(props: MultiButtonCompatibleProps) {
- const { push } = useHistory();
-
- const tunnelState = useSelector((state) => state.connection.status.state);
- const relaySettings = useSelector((state) => state.settings.relaySettings);
- const relayLocations = useSelector((state) => state.settings.relayLocations);
- const customLists = useSelector((state) => state.settings.customLists);
-
- const selectedRelayName = useMemo(
- () => getRelayName(relaySettings, customLists, relayLocations),
- [relaySettings, customLists, relayLocations],
- );
-
- const onSelectLocation = useCallback(() => {
- push(RoutePath.selectLocation, { transition: transitions.show });
- }, [push]);
-
- return (
- <StyledSmallButton
- color={SmallButtonColor.blue}
- onClick={onSelectLocation}
- aria-label={sprintf(
- messages.pgettext('accessibility', 'Select location. Current location is %(location)s'),
- { location: selectedRelayName },
- )}
- {...props}>
- {tunnelState === 'disconnected'
- ? selectedRelayName
- : messages.pgettext('tunnel-control', 'Switch location')}
- </StyledSmallButton>
- );
-}
-
-function getRelayName(
- relaySettings: RelaySettingsRedux,
- customLists: Array<ICustomList>,
- locations: IRelayLocationCountryRedux[],
-): string {
- if ('normal' in relaySettings) {
- const location = relaySettings.normal.location;
-
- if (location === 'any') {
- return 'Automatic';
- } else if ('customList' in location) {
- return customLists.find((list) => list.id === location.customList)?.name ?? 'Unknown';
- } else if ('hostname' in location) {
- const country = locations.find(({ code }) => code === location.country);
- if (country) {
- const city = country.cities.find(({ code }) => code === location.city);
- 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: location.hostname,
- },
- );
- }
- }
- } else if ('city' in location) {
- const country = locations.find(({ code }) => code === location.country);
- if (country) {
- const city = country.cities.find(({ code }) => code === location.city);
- if (city) {
- return relayLocations.gettext(city.name);
- }
- }
- } else if ('country' in location) {
- const country = locations.find(({ code }) => code === location.country);
- if (country) {
- return relayLocations.gettext(country.name);
- }
- }
-
- return 'Unknown';
- } else if (relaySettings.customTunnelEndpoint) {
- return 'Custom';
- } else {
- throw new Error('Unsupported relay settings.');
- }
-}
-
-function ReconnectButton(props: MultiButtonCompatibleProps) {
- const { reconnectTunnel } = useAppContext();
-
- const onReconnect = useCallback(async () => {
- try {
- await reconnectTunnel();
- } catch (e) {
- const error = e as Error;
- log.error(`Failed to reconnect the tunnel: ${error.message}`);
- }
- }, [reconnectTunnel]);
-
- return (
- <StyledReconnectButton
- color={SmallButtonColor.blue}
- onClick={onReconnect}
- aria-label={messages.gettext('Reconnect')}
- {...props}>
- <ImageView height={24} width={24} source="icon-reload" tintColor="white" />
- </StyledReconnectButton>
- );
-}
diff --git a/gui/src/renderer/components/main-view/styles.ts b/gui/src/renderer/components/main-view/styles.ts
deleted file mode 100644
index 58c6e85eb2..0000000000
--- a/gui/src/renderer/components/main-view/styles.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-import styled from 'styled-components';
-
-import Accordion from '../Accordion';
-
-export const ConnectionPanelAccordion = styled(Accordion)({
- transition: 'height 300ms ease-out',
-});