import { ipcRenderer, shell } from 'electron'; import { useState, SessionDataTestId } from 'react'; import { useDispatch } from 'react-redux'; import useHover from 'react-use/lib/useHover'; import styled from 'styled-components'; import { isEmpty, isTypedArray } from 'lodash'; import { CityResponse, Reader } from 'maxmind'; import useMount from 'react-use/lib/useMount'; import { onionPathModal } from '../../state/ducks/modalDialog'; import { useFirstOnionPath, useFirstOnionPathLength, useIsOnline, useOnionPathsCount, } from '../../state/selectors/onions'; import { Flex } from '../basic/Flex'; import { Snode } from '../../data/types'; import { THEME_GLOBALS } from '../../themes/globals'; import { SessionWrapperModal } from '../SessionWrapperModal'; import { SessionIcon, SessionIconButton } from '../icon'; import { SessionSpinner } from '../loading'; import { getCrowdinLocale } from '../../util/i18n/shared'; export type StatusLightType = { glowStartDelay: number; glowDuration: number; color?: string; dataTestId?: SessionDataTestId; }; const StyledCountry = styled.div` margin: var(--margins-sm); min-width: 150px; `; const StyledOnionNodeList = styled.div` display: flex; flex-direction: column; margin: var(--margins-sm); align-items: center; min-width: 10vw; position: relative; `; const StyledOnionDescription = styled.p` min-width: 400px; width: 0; line-height: 1.3333; `; const StyledVerticalLine = styled.div` background: var(--border-color); position: absolute; height: calc(100% - 2 * 15px); margin: 15px calc(100% / 2 - 1px); width: 1px; `; const StyledLightsContainer = styled.div` position: relative; `; const StyledGrowingIcon = styled.div` flex-grow: 1; display: flex; align-items: center; `; const OnionCountryDisplay = ({ labelText, snodeIp }: { snodeIp?: string; labelText: string }) => { const element = (hovered: boolean) => ( {hovered && snodeIp ? snodeIp : labelText} ); const [hoverable] = useHover(element); return hoverable; }; let reader: Reader | null; const OnionPathModalInner = () => { const onionPath = useFirstOnionPath(); // eslint-disable-next-line @typescript-eslint/no-unused-vars const [_dataLoaded, setDataLoaded] = useState(false); const isOnline = useIsOnline(); const glowDuration = onionPath.length + 2; useMount(() => { ipcRenderer.once('load-maxmind-data-complete', (_event, content) => { const asArrayBuffer = content as Uint8Array; if (asArrayBuffer && isTypedArray(asArrayBuffer) && !isEmpty(asArrayBuffer)) { reader = new Reader(Buffer.from(asArrayBuffer.buffer)); setDataLoaded(true); // retrigger a rerender } }); ipcRenderer.send('load-maxmind-data'); }); if (!isOnline || !onionPath || onionPath.length === 0) { return ; } const nodes = [ { label: window.i18n('you'), }, ...onionPath, { label: window.i18n('onionRoutingPathDestination'), }, ]; return ( <> {window.i18n('onionRoutingPathDescription')} {nodes.map((_snode: Snode | any, index: number) => { return ( ); })} {nodes.map((snode: Snode | any) => { const country = reader?.get(snode.ip || '0.0.0.0')?.country; const locale = getCrowdinLocale(); // typescript complains that the [] operator cannot be used with the 'string' coming from getCrowdinLocale() const countryNamesAsAny = country?.names as any; const countryName = snode.label || // to take care of the "Device" case countryNamesAsAny?.[locale] || // try to find the country name based on the user local first // eslint-disable-next-line dot-notation countryNamesAsAny?.['en'] || // if not found, fallback to the country in english window.i18n('onionRoutingPathUnknownCountry'); return ( ); })} ); }; export type OnionNodeStatusLightType = { glowStartDelay: number; glowDuration: number; dataTestId?: SessionDataTestId; }; /** * Component containing a coloured status light. */ export const OnionNodeStatusLight = (props: OnionNodeStatusLightType): JSX.Element => { const { glowStartDelay, glowDuration, dataTestId } = props; return ( ); }; /** * An icon with a pulsating glow emission. */ export const ModalStatusLight = (props: StatusLightType) => { const { glowStartDelay, glowDuration, color } = props; return ( ); }; /** * A status light specifically for the action panel. Color is based on aggregate node states instead of individual onion node state */ export const ActionPanelOnionStatusLight = (props: { isSelected: boolean; handleClick: () => void; id: string; }) => { const { isSelected, handleClick, id } = props; const onionPathsCount = useOnionPathsCount(); const firstPathLength = useFirstOnionPathLength(); const isOnline = useIsOnline(); const glowDuration = Number(THEME_GLOBALS['--duration-onion-status-glow']); // 10 seconds // Set icon color based on result const errorColor = 'var(--button-path-error-color)'; const defaultColor = 'var(--button-path-default-color)'; const connectingColor = 'var(--button-path-connecting-color)'; // start with red let iconColor = errorColor; // if we are not online or the first path is not valid, we keep red as color if (isOnline && firstPathLength > 1) { iconColor = onionPathsCount >= 2 ? defaultColor : onionPathsCount >= 1 ? connectingColor : errorColor; } return ( ); }; export const OnionPathModal = () => { const onConfirm = () => { void shell.openExternal('https://getsession.org/faq/#onion-routing'); }; const dispatch = useDispatch(); return ( dispatch(onionPathModal(null))} showExitIcon={true} > ); };