You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
252 lines
6.8 KiB
TypeScript
252 lines
6.8 KiB
TypeScript
import { shell } from 'electron';
|
|
import { readFileSync } from 'fs';
|
|
import path from 'path';
|
|
import React from 'react';
|
|
|
|
import { useDispatch, useSelector } from 'react-redux';
|
|
import useHover from 'react-use/lib/useHover';
|
|
import styled from 'styled-components';
|
|
|
|
import { CityResponse, Reader } from 'maxmind';
|
|
import { Snode } from '../../data/data';
|
|
import { onionPathModal } from '../../state/ducks/modalDialog';
|
|
import {
|
|
getFirstOnionPath,
|
|
getFirstOnionPathLength,
|
|
getIsOnline,
|
|
getOnionPathsCount,
|
|
} from '../../state/selectors/onions';
|
|
import { Flex } from '../basic/Flex';
|
|
|
|
import { SessionSpinner } from '../basic/SessionSpinner';
|
|
import { SessionIcon, SessionIconButton } from '../icon';
|
|
import { SessionWrapperModal } from '../SessionWrapperModal';
|
|
|
|
export type StatusLightType = {
|
|
glowStartDelay: number;
|
|
glowDuration: number;
|
|
color?: string;
|
|
dataTestId?: string;
|
|
};
|
|
|
|
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) => (
|
|
<StyledCountry>{hovered && snodeIp ? snodeIp : labelText}</StyledCountry>
|
|
);
|
|
const [hoverable] = useHover(element);
|
|
|
|
return hoverable;
|
|
};
|
|
|
|
const OnionPathModalInner = () => {
|
|
const onionPath = useSelector(getFirstOnionPath);
|
|
const isOnline = useSelector(getIsOnline);
|
|
const glowDuration = onionPath.length + 2;
|
|
|
|
if (!isOnline || !onionPath || onionPath.length === 0) {
|
|
return <SessionSpinner loading={true} />;
|
|
}
|
|
|
|
const nodes = [
|
|
{
|
|
label: window.i18n('device'),
|
|
},
|
|
...onionPath,
|
|
{
|
|
label: window.i18n('destination'),
|
|
},
|
|
];
|
|
|
|
const binPath = (process.env.NODE_APP_INSTANCE || '').startsWith('devprod')
|
|
? path.resolve(`${__dirname}/../../..`)
|
|
: path.resolve(`${process.resourcesPath}/..`);
|
|
const buffer = readFileSync(`${binPath}/mmdb/GeoLite2-Country.mmdb`);
|
|
const reader = new Reader<CityResponse>(buffer);
|
|
const lang = 'en';
|
|
|
|
return (
|
|
<>
|
|
<StyledOnionDescription>
|
|
{window.i18n('onionPathIndicatorDescription')}
|
|
</StyledOnionDescription>
|
|
<StyledOnionNodeList>
|
|
<Flex container={true}>
|
|
<StyledLightsContainer>
|
|
<StyledVerticalLine />
|
|
<Flex container={true} flexDirection="column" alignItems="center" height="100%">
|
|
{nodes.map((_snode: Snode | any, index: number) => {
|
|
return (
|
|
<OnionNodeStatusLight
|
|
glowDuration={glowDuration}
|
|
glowStartDelay={index}
|
|
key={`light-${index}`}
|
|
/>
|
|
);
|
|
})}
|
|
</Flex>
|
|
</StyledLightsContainer>
|
|
<Flex container={true} flexDirection="column" alignItems="flex-start">
|
|
{nodes.map((snode: Snode | any) => {
|
|
const geoLookup = reader.get(snode.ip || '0.0.0.0');
|
|
const countryName = geoLookup?.country?.names[lang] || window.i18n('unknownCountry');
|
|
const labelText = snode.label || countryName;
|
|
|
|
return (
|
|
<OnionCountryDisplay
|
|
labelText={labelText}
|
|
snodeIp={snode.ip}
|
|
key={`country-${snode.ip}`}
|
|
/>
|
|
);
|
|
})}
|
|
</Flex>
|
|
</Flex>
|
|
</StyledOnionNodeList>
|
|
</>
|
|
);
|
|
};
|
|
|
|
export type OnionNodeStatusLightType = {
|
|
glowStartDelay: number;
|
|
glowDuration: number;
|
|
dataTestId?: string;
|
|
};
|
|
|
|
/**
|
|
* Component containing a coloured status light.
|
|
*/
|
|
export const OnionNodeStatusLight = (props: OnionNodeStatusLightType): JSX.Element => {
|
|
const { glowStartDelay, glowDuration, dataTestId } = props;
|
|
|
|
return (
|
|
<ModalStatusLight
|
|
glowDuration={glowDuration}
|
|
glowStartDelay={glowStartDelay}
|
|
color={'var(--button-path-default-color)'}
|
|
dataTestId={dataTestId}
|
|
/>
|
|
);
|
|
};
|
|
|
|
/**
|
|
* An icon with a pulsating glow emission.
|
|
*/
|
|
export const ModalStatusLight = (props: StatusLightType) => {
|
|
const { glowStartDelay, glowDuration, color } = props;
|
|
|
|
return (
|
|
<StyledGrowingIcon>
|
|
<SessionIcon
|
|
borderRadius={'50px'}
|
|
iconColor={color}
|
|
glowDuration={glowDuration}
|
|
glowStartDelay={glowStartDelay}
|
|
iconType="circle"
|
|
iconSize={'tiny'}
|
|
/>
|
|
</StyledGrowingIcon>
|
|
);
|
|
};
|
|
|
|
/**
|
|
* 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 = useSelector(getOnionPathsCount);
|
|
const firstPathLength = useSelector(getFirstOnionPathLength);
|
|
const isOnline = useSelector(getIsOnline);
|
|
|
|
// 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 (
|
|
<SessionIconButton
|
|
iconSize={'small'}
|
|
iconType="circle"
|
|
iconColor={iconColor}
|
|
onClick={handleClick}
|
|
glowDuration={10}
|
|
glowStartDelay={0}
|
|
noScale={true}
|
|
isSelected={isSelected}
|
|
dataTestId={'path-light-container'}
|
|
dataTestIdIcon={'path-light-svg'}
|
|
id={id}
|
|
/>
|
|
);
|
|
};
|
|
|
|
export const OnionPathModal = () => {
|
|
const onConfirm = () => {
|
|
void shell.openExternal('https://getsession.org/faq/#onion-routing');
|
|
};
|
|
const dispatch = useDispatch();
|
|
return (
|
|
<SessionWrapperModal
|
|
title={window.i18n('onionPathIndicatorTitle')}
|
|
confirmText={window.i18n('learnMore')}
|
|
cancelText={window.i18n('cancel')}
|
|
onConfirm={onConfirm}
|
|
onClose={() => dispatch(onionPathModal(null))}
|
|
showExitIcon={true}
|
|
>
|
|
<OnionPathModalInner />
|
|
</SessionWrapperModal>
|
|
);
|
|
};
|