mirror of
https://github.com/lux-tel/session-desktop.git
synced 2026-10-30 20:33:24 +00:00
357 lines
10 KiB
TypeScript
357 lines
10 KiB
TypeScript
import { ipcRenderer } from 'electron';
|
|
import { useState, SessionDataTestId, type JSX } from 'react';
|
|
|
|
import useUpdate from 'react-use/lib/useUpdate';
|
|
import useHover from 'react-use/lib/useHover';
|
|
import styled from 'styled-components';
|
|
import useInterval from 'react-use/lib/useInterval';
|
|
|
|
import { isEmpty, isTypedArray } from 'lodash';
|
|
import { CityResponse, Reader } from 'maxmind';
|
|
import useMount from 'react-use/lib/useMount';
|
|
import { useSelector } from 'react-redux';
|
|
import { getAppDispatch } from '../../state/dispatch';
|
|
import { onionPathModal, updateOpenUrlModal } from '../../state/ducks/modalDialog';
|
|
import {
|
|
getFirstOnionPathLength,
|
|
getIsOnline,
|
|
getOnionPathsCount,
|
|
useFirstOnionPath,
|
|
} from '../../state/selectors/onions';
|
|
import { Flex } from '../basic/Flex';
|
|
|
|
import { Snode } from '../../data/types';
|
|
import { SessionSpinner } from '../loading';
|
|
import { getCrowdinLocale } from '../../util/i18n/shared';
|
|
import { tr } from '../../localization/localeTools';
|
|
import {
|
|
ModalBasicHeader,
|
|
ModalActionsContainer,
|
|
SessionWrapperModal,
|
|
} from '../SessionWrapperModal';
|
|
import { SessionButton, SessionButtonType } from '../basic/SessionButton';
|
|
import { ModalDescription } from './shared/ModalDescriptionContainer';
|
|
import { ModalFlexContainer } from './shared/ModalFlexContainer';
|
|
import { getDataFeatureFlagMemo } from '../../state/ducks/types/releasedFeaturesReduxTypes';
|
|
import { DURATION } from '../../session/constants';
|
|
|
|
type StatusLightType = {
|
|
glowing?: boolean;
|
|
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 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;
|
|
`;
|
|
|
|
function useOnionPathWithUsAndNetwork() {
|
|
const onionPath = useFirstOnionPath();
|
|
const localDevnet = getDataFeatureFlagMemo('useLocalDevNet');
|
|
|
|
if (onionPath.length === 0) {
|
|
return [];
|
|
}
|
|
|
|
return [
|
|
{
|
|
label: tr('you'),
|
|
},
|
|
...onionPath.map((node, index) => {
|
|
return {
|
|
...node,
|
|
label: localDevnet ? `SeshNet ${index + 1}` : undefined,
|
|
};
|
|
}),
|
|
{
|
|
label: tr('onionRoutingPathDestination'),
|
|
},
|
|
];
|
|
}
|
|
|
|
function useGlowingIndex() {
|
|
const [glowingIndex, setGlowingIndex] = useState(0);
|
|
return { glowingIndex, setGlowingIndex };
|
|
}
|
|
|
|
function GlowingNodes() {
|
|
const onionPath = useOnionPathWithUsAndNetwork();
|
|
const { glowingIndex, setGlowingIndex } = useGlowingIndex();
|
|
|
|
const increment = () => {
|
|
setGlowingIndex(prev => (prev + 1) % onionPath.length);
|
|
};
|
|
|
|
useInterval(increment, 1 * DURATION.SECONDS);
|
|
|
|
return onionPath.map((_, index) => {
|
|
return <OnionNodeStatusLight glowing={index === glowingIndex} key={`light-${index}`} />;
|
|
});
|
|
}
|
|
|
|
const OnionCountryDisplay = ({ labelText, snodeIp }: { snodeIp?: string; labelText: string }) => {
|
|
const element = (hovered: boolean) => (
|
|
<StyledCountry>{hovered && snodeIp ? snodeIp : labelText}</StyledCountry>
|
|
);
|
|
return useHover(element);
|
|
};
|
|
|
|
let reader: Reader<CityResponse> | null;
|
|
|
|
function loadCityData(forceUpdate: () => void) {
|
|
ipcRenderer.once('load-maxmind-data-complete', (_event, content) => {
|
|
const asArrayBuffer = content as Uint8Array;
|
|
if (asArrayBuffer && isTypedArray(asArrayBuffer) && !isEmpty(asArrayBuffer)) {
|
|
reader = new Reader<CityResponse>(Buffer.from(asArrayBuffer.buffer));
|
|
forceUpdate();
|
|
}
|
|
});
|
|
ipcRenderer.send('load-maxmind-data');
|
|
}
|
|
|
|
// NOTE: [react-compiler] this has to live here for the hook to be identified as static
|
|
function useIsOnline() {
|
|
return useSelector(getIsOnline);
|
|
}
|
|
|
|
const OnionPathModalInner = () => {
|
|
const forceUpdate = useUpdate();
|
|
const nodes = useOnionPathWithUsAndNetwork();
|
|
const isOnline = useIsOnline();
|
|
|
|
useMount(() => {
|
|
if (!reader) {
|
|
loadCityData(forceUpdate);
|
|
}
|
|
});
|
|
|
|
if (!isOnline || !nodes || nodes.length <= 0 || !reader) {
|
|
return <SessionSpinner $loading={true} />;
|
|
}
|
|
|
|
return <OnionPathModalLoaded />;
|
|
};
|
|
|
|
const OnionPathModalLoaded = () => {
|
|
const nodes = useOnionPathWithUsAndNetwork();
|
|
return (
|
|
<ModalFlexContainer>
|
|
<ModalDescription
|
|
dataTestId="modal-description"
|
|
localizerProps={{ token: 'onionRoutingPathDescription' }}
|
|
/>
|
|
<StyledOnionNodeList>
|
|
<Flex $container={true}>
|
|
<StyledLightsContainer>
|
|
<StyledVerticalLine />
|
|
<Flex $container={true} $flexDirection="column" $alignItems="center" height="100%">
|
|
<GlowingNodes />
|
|
</Flex>
|
|
</StyledLightsContainer>
|
|
<Flex $container={true} $flexDirection="column" $alignItems="flex-start">
|
|
{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
|
|
tr('onionRoutingPathUnknownCountry');
|
|
|
|
return (
|
|
<OnionCountryDisplay
|
|
labelText={countryName}
|
|
snodeIp={snode.ip}
|
|
key={snode.ip ? `country-${snode.ip}` : countryName}
|
|
/>
|
|
);
|
|
})}
|
|
</Flex>
|
|
</Flex>
|
|
</StyledOnionNodeList>
|
|
</ModalFlexContainer>
|
|
);
|
|
};
|
|
|
|
type OnionNodeStatusLightType = {
|
|
glowing: boolean;
|
|
dataTestId?: SessionDataTestId;
|
|
};
|
|
|
|
/**
|
|
* Component containing a coloured status light.
|
|
*/
|
|
const OnionNodeStatusLight = (props: OnionNodeStatusLightType): JSX.Element => {
|
|
const { glowing, dataTestId } = props;
|
|
|
|
return (
|
|
<ModalStatusLight
|
|
glowing={glowing}
|
|
color={'var(--button-path-default-color)'}
|
|
dataTestId={dataTestId}
|
|
/>
|
|
);
|
|
};
|
|
|
|
/**
|
|
* An icon with a pulsating glow emission.
|
|
*/
|
|
const ModalStatusLight = (props: StatusLightType) => {
|
|
const { glowing, color } = props;
|
|
|
|
return (
|
|
<StyledGrowingIcon>
|
|
<OnionPathDot iconColor={color} glowing={glowing} />
|
|
</StyledGrowingIcon>
|
|
);
|
|
};
|
|
|
|
function OnionPathDot({
|
|
dataTestId,
|
|
iconColor,
|
|
glowing,
|
|
}: {
|
|
dataTestId?: SessionDataTestId;
|
|
iconColor: string;
|
|
glowing?: boolean;
|
|
}) {
|
|
return (
|
|
<svg
|
|
width="12"
|
|
height="12"
|
|
viewBox="0 0 100 100"
|
|
clipRule="nonzero"
|
|
fillRule="nonzero"
|
|
data-testid={dataTestId}
|
|
style={{
|
|
transition: 'all var(--default-duration) ease-in-out',
|
|
|
|
filter: glowing
|
|
? `drop-shadow(0px 0px 4px ${iconColor})`
|
|
: `drop-shadow(0px 0px 6px ${iconColor})`,
|
|
scale: glowing ? '1.3' : '1',
|
|
}}
|
|
>
|
|
<path fill={iconColor} d="M 0, 50a 50,50 0 1,1 100,0a 50,50 0 1,1 -100,0"></path>
|
|
</svg>
|
|
);
|
|
}
|
|
|
|
export const OnionPathModal = () => {
|
|
const dispatch = getAppDispatch();
|
|
return (
|
|
<SessionWrapperModal
|
|
modalId="onionPathModal"
|
|
onClose={() => dispatch(onionPathModal(null))}
|
|
headerChildren={<ModalBasicHeader title={tr('onionRoutingPath')} showExitIcon={true} />}
|
|
buttonChildren={
|
|
<ModalActionsContainer buttonType={SessionButtonType.Simple}>
|
|
<SessionButton
|
|
text={tr('learnMore')}
|
|
buttonType={SessionButtonType.Simple}
|
|
onClick={() => {
|
|
dispatch(
|
|
updateOpenUrlModal({
|
|
urlToOpen: 'https://getsession.org/faq/#onion-routing',
|
|
})
|
|
);
|
|
}}
|
|
/>
|
|
</ModalActionsContainer>
|
|
}
|
|
>
|
|
<OnionPathModalInner />
|
|
</SessionWrapperModal>
|
|
);
|
|
};
|
|
|
|
// 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)';
|
|
|
|
const StyledStatusLightContainer = styled.div<{ $inActionPanel: boolean }>`
|
|
margin-top: ${props => (props.$inActionPanel ? 'auto' : '0')};
|
|
cursor: ${props => (props.$inActionPanel ? 'pointer' : 'inherit')};
|
|
padding: ${props => (props.$inActionPanel ? 'var(--margins-md)' : '0')};
|
|
`;
|
|
|
|
// NOTE: [react-compiler] this has to live here for the hook to be identified as static
|
|
function useOnionPathsCount() {
|
|
return useSelector(getOnionPathsCount);
|
|
}
|
|
|
|
// NOTE: [react-compiler] this has to live here for the hook to be identified as static
|
|
function useFirstOnionPathLength() {
|
|
return useSelector(getFirstOnionPathLength);
|
|
}
|
|
|
|
/**
|
|
* A status light specifically for the action panel. Color is based on aggregate node states instead of individual onion node state
|
|
*/
|
|
export const OnionStatusLight = (
|
|
props:
|
|
| { inActionPanel: true; handleClick: () => void }
|
|
| {
|
|
inActionPanel: false;
|
|
handleClick: undefined;
|
|
}
|
|
) => {
|
|
const { handleClick, inActionPanel } = props;
|
|
|
|
const onionPathsCount = useOnionPathsCount();
|
|
const firstPathLength = useFirstOnionPathLength();
|
|
const isOnline = useIsOnline();
|
|
|
|
// 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 (
|
|
<StyledStatusLightContainer
|
|
data-testid="path-light-container"
|
|
onClick={handleClick}
|
|
$inActionPanel={inActionPanel}
|
|
>
|
|
<OnionPathDot dataTestId="path-light-svg" iconColor={iconColor} />
|
|
</StyledStatusLightContainer>
|
|
);
|
|
};
|