diff options
| author | Oskar <oskar@mullvad.net> | 2024-11-05 07:57:08 +0100 |
|---|---|---|
| committer | Oskar <oskar@mullvad.net> | 2024-11-14 16:43:18 +0100 |
| commit | 84f14d79c4f0dde73337820ec94ba8ff928a3797 (patch) | |
| tree | ce468658e5ba7b0a74950c7ad1b09b3a4d00520b /gui/src/renderer/components/main-view | |
| parent | e3ce0eb5cd0610dbff6ec98cb8cb388415c74bf6 (diff) | |
| download | mullvadvpn-84f14d79c4f0dde73337820ec94ba8ff928a3797.tar.xz mullvadvpn-84f14d79c4f0dde73337820ec94ba8ff928a3797.zip | |
Move gui directory to desktop/packages/mullvad-vpn
Diffstat (limited to 'gui/src/renderer/components/main-view')
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', -}); |
