lights glowing but not yet in order.

WIP glow cycle.

Glow animation working.

Glow animation working, modal no longer interferring with left menu margins.

Cleaning up code. Adding PR suggestions.

Adding typing.

Refactor edit profile modal dialog to no longer use the signal code.
pull/1665/head
Warrick Corfe-Tan 4 years ago
parent 98fe0a87d8
commit 066e0d954e

@ -1868,5 +1868,17 @@
},
"learnMore": {
"message": "Learn More"
},
"unknownCountry": {
"message": "Unknown Country",
"description": "When an onion path node is unable to find the country for an IP address."
},
"device": {
"message": "Device",
"description": "The device of the client running Session"
},
"destination": {
"message": "Destination",
"description": "The destination that the message is being sent to"
}
}

@ -13,7 +13,7 @@
*/
// eslint-disable-next-line func-names
(async function() {
(async function () {
'use strict';
// Globally disable drag and drop
@ -433,7 +433,7 @@
// Ensure that this file is either small enough or is resized to meet our
// requirements for attachments
try {
const withBlob = await window.Signal.Util.AttachmentUtil.autoScale(
const withBlob = await winvadow.Signal.Util.AttachmentUtil.autoScale(
{
contentType: avatar.type,
file: new Blob([data.data], {
@ -527,10 +527,232 @@
}
};
window.onProfileEditOk = async (newName, avatar) => {
let newAvatarPath = '';
let url = null;
let profileKey = null;
if (avatar) {
const data = await readFile({ file: avatar });
// Ensure that this file is either small enough or is resized to meet our
// requirements for attachments
try {
const withBlob = await winvadow.Signal.Util.AttachmentUtil.autoScale(
{
contentType: avatar.type,
file: new Blob([data.data], {
type: avatar.contentType,
}),
},
{
maxSide: 640,
maxSize: 1000 * 1024,
}
);
const dataResized = await window.Signal.Types.Attachment.arrayBufferFromFile(
withBlob.file
);
// For simplicity we use the same attachment pointer that would send to
// others, which means we need to wait for the database response.
// To avoid the wait, we create a temporary url for the local image
// and use it until we the the response from the server
const tempUrl = window.URL.createObjectURL(avatar);
conversation.setLokiProfile({ displayName: newName });
conversation.set('avatar', tempUrl);
// Encrypt with a new key every time
profileKey = libsignal.crypto.getRandomBytes(32);
const encryptedData = await textsecure.crypto.encryptProfile(
dataResized,
profileKey
);
const avatarPointer = await libsession.Utils.AttachmentUtils.uploadAvatarV1({
...dataResized,
data: encryptedData,
size: encryptedData.byteLength,
});
({ url } = avatarPointer);
storage.put('profileKey', profileKey);
conversation.set('avatarPointer', url);
const upgraded = await Signal.Migrations.processNewAttachment({
isRaw: true,
data: data.data,
url,
});
newAvatarPath = upgraded.path;
// Replace our temporary image with the attachment pointer from the server:
conversation.set('avatar', null);
conversation.setLokiProfile({
displayName: newName,
avatar: newAvatarPath,
});
await conversation.commit();
window.libsession.Utils.UserUtils.setLastProfileUpdateTimestamp(Date.now());
await window.libsession.Utils.SyncUtils.forceSyncConfigurationNowIfNeeded(true);
} catch (error) {
window.log.error(
'showEditProfileDialog Error ensuring that image is properly sized:',
error && error.stack ? error.stack : error
);
}
} else {
// do not update the avatar if it did not change
conversation.setLokiProfile({
displayName: newName,
});
// might be good to not trigger a sync if the name did not change
await conversation.commit();
window.libsession.Utils.UserUtils.setLastProfileUpdateTimestamp(Date.now());
await window.libsession.Utils.SyncUtils.forceSyncConfigurationNowIfNeeded(true);
}
// inform all your registered public servers
// could put load on all the servers
// if they just keep changing their names without sending messages
// so we could disable this here
// or least it enable for the quickest response
window.lokiPublicChatAPI.setProfileName(newName);
if (avatar) {
window
.getConversationController()
.getConversations()
.filter(convo => convo.isPublic())
.forEach(convo => convo.trigger('ourAvatarChanged', { url, profileKey }));
}
}
window.showOnionStatusDialog = () => {
appView.showOnionStatusDialog();
};
window.commitProfileEdits = async (newName, avatar) => {
const ourNumber = window.storage.get('primaryDevicePubKey');
const conversation = await window
.getConversationController()
.getOrCreateAndWait(ourNumber, 'private');
const readFile = attachment =>
new Promise((resolve, reject) => {
const fileReader = new FileReader();
fileReader.onload = e => {
const data = e.target.result;
resolve({
...attachment,
data,
size: data.byteLength,
});
};
fileReader.onerror = reject;
fileReader.onabort = reject;
fileReader.readAsArrayBuffer(attachment.file);
});
// const avatarPath = conversation.getAvatarPath();
// const profile = conversation.getLokiProfile();
// const displayName = profile && profile.displayName;
let newAvatarPath = '';
let url = null;
let profileKey = null;
if (avatar) {
const data = await readFile({ file: avatar });
// Ensure that this file is either small enough or is resized to meet our
// requirements for attachments
try {
const withBlob = await window.Signal.Util.AttachmentUtil.autoScale(
{
contentType: avatar.type,
file: new Blob([data.data], {
type: avatar.contentType,
}),
},
{
maxSide: 640,
maxSize: 1000 * 1024,
}
);
const dataResized = await window.Signal.Types.Attachment.arrayBufferFromFile(
withBlob.file
);
// For simplicity we use the same attachment pointer that would send to
// others, which means we need to wait for the database response.
// To avoid the wait, we create a temporary url for the local image
// and use it until we the the response from the server
const tempUrl = window.URL.createObjectURL(avatar);
conversation.setLokiProfile({ displayName: newName });
conversation.set('avatar', tempUrl);
// Encrypt with a new key every time
profileKey = libsignal.crypto.getRandomBytes(32);
const encryptedData = await textsecure.crypto.encryptProfile(dataResized, profileKey);
const avatarPointer = await libsession.Utils.AttachmentUtils.uploadAvatarV1({
...dataResized,
data: encryptedData,
size: encryptedData.byteLength,
});
({ url } = avatarPointer);
storage.put('profileKey', profileKey);
conversation.set('avatarPointer', url);
const upgraded = await Signal.Migrations.processNewAttachment({
isRaw: true,
data: data.data,
url,
});
newAvatarPath = upgraded.path;
// Replace our temporary image with the attachment pointer from the server:
conversation.set('avatar', null);
conversation.setLokiProfile({
displayName: newName,
avatar: newAvatarPath,
});
await conversation.commit();
window.libsession.Utils.UserUtils.setLastProfileUpdateTimestamp(Date.now());
await window.libsession.Utils.SyncUtils.forceSyncConfigurationNowIfNeeded(true);
} catch (error) {
window.log.error(
'showEditProfileDialog Error ensuring that image is properly sized:',
error && error.stack ? error.stack : error
);
}
} else {
// do not update the avatar if it did not change
conversation.setLokiProfile({
displayName: newName,
});
// might be good to not trigger a sync if the name did not change
await conversation.commit();
window.libsession.Utils.UserUtils.setLastProfileUpdateTimestamp(Date.now());
await window.libsession.Utils.SyncUtils.forceSyncConfigurationNowIfNeeded(true);
}
// inform all your registered public servers
// could put load on all the servers
// if they just keep changing their names without sending messages
// so we could disable this here
// or least it enable for the quickest response
window.lokiPublicChatAPI.setProfileName(newName);
if (avatar) {
window
.getConversationController()
.getConversations()
.filter(convo => convo.isPublic())
.forEach(convo => convo.trigger('ourAvatarChanged', { url, profileKey }));
}
};
// Set user's launch count.
const prevLaunchCount = window.getSettingValue('launch-count');
const launchCount = !prevLaunchCount ? 1 : prevLaunchCount + 1;

@ -48,7 +48,7 @@
.avatar-center-inner {
display: flex;
padding-top: 30px;
// padding-top: 30px;
}
.upload-btn-background {

@ -235,6 +235,7 @@
border-radius: 50%;
background-color: $session-color-white;
transition: $session-transition-duration;
margin-top: 30px;
&:hover {
filter: brightness(90%);
@ -395,15 +396,6 @@
border-radius: 50%;
display: inline-block;
}
// TODO: Use colors defined in UI constants
.green {
background-color: $color-loki-green;
}
.red {
background-color: #ff453a;
}
}
}

@ -756,6 +756,7 @@ label {
background-color: rgba($session-color-black, 0.72);
opacity: 0;
transition: $session-transition-duration;
margin: 30px 0px;
&:after {
content: 'Edit';
@ -1200,38 +1201,43 @@ input {
// .session-onion-path-wrapper {
.onion__node-list {
display: flex;
flex-direction: column;
margin: $session-margin-sm;
align-items: flex-start;
.onion__node {
display: flex;
flex-grow: 1;
align-items: center;
margin: $session-margin-xs;
.onion__node-list {
display: flex;
flex-direction: column;
margin: $session-margin-sm;
align-items: flex-start;
* {
margin: $session-margin-sm;
}
.onion__node {
display: flex;
flex-grow: 1;
align-items: center;
margin: $session-margin-xs;
svg {
animation: glow 1s ease-in-out infinite alternate;
}
* {
margin: $session-margin-sm;
}
}
@keyframes glow {
from {
-webkit-filter: drop-shadow( 0px 0px 3px rgba(0,0,0,0));
filter: drop-shadow( 0px 0px 3px rgba(0,0,0,0));
}
to {
-webkit-filter: drop-shadow( 0px 0px 5px rgba(46, 247, 28, 0.7));
filter: drop-shadow( 0px 0px 5px rgba(68, 236, 2, 0.7));
.session-icon-button {
margin: $session-margin-sm !important;
}
// svg {
// animation: glow 1s ease-in-out infinite alternate;
// }
}
}
// @keyframes glow {
// from {
// -webkit-filter: drop-shadow( 0px 0px 3px rgba(0,0,0,0));
// filter: drop-shadow( 0px 0px 3px rgba(0,0,0,0));
// }
// to {
// -webkit-filter: drop-shadow( 0px 0px 5px rgba(46, 247, 28, 0.7));
// filter: drop-shadow( 0px 0px 5px rgba(68, 236, 2, 0.7));
// color: red;
// }
// }
// }

@ -3,6 +3,7 @@
$white: #ffffff;
$black: #000000;
$destructive: #ff453a;
$warning: #e7b100;
$accentLightTheme: #00e97b;
$accentDarkTheme: #00f782;
@ -19,6 +20,7 @@ $themes: (
accent: $accentLightTheme,
accentButton: $black,
cellBackground: #fcfcfc,
warning: $warning,
destructive: $destructive,
modalBackground: #fcfcfc,
fakeChatBubbleBackground: #f5f5f5,
@ -68,6 +70,7 @@ $themes: (
dark: (
accent: $accentDarkTheme,
accentButton: $accentDarkTheme,
warning: $warning,
destructive: $destructive,
cellBackground: #1b1b1b,
modalBackground: #101011,

@ -1,4 +1,4 @@
import React from 'react';
import React, { useEffect, useState } from 'react';
import classNames from 'classnames';
import { QRCode } from 'react-qr-svg';
@ -10,17 +10,24 @@ import { SessionIconButton, SessionIconSize, SessionIconType } from './session/i
import { SessionModal } from './session/SessionModal';
import { PillDivider } from './session/PillDivider';
import { ToastUtils, UserUtils } from '../session/utils';
import { DefaultTheme } from 'styled-components';
import { DefaultTheme, useTheme } from 'styled-components';
import { MAX_USERNAME_LENGTH } from './session/registration/RegistrationTabs';
import { SessionSpinner } from './session/SessionSpinner';
import { ConversationTypeEnum } from '../models/conversation';
import { ConversationController } from '../session/conversations';
import { useSelector } from 'react-redux';
import { getOurNumber } from '../state/selectors/user';
import { SessionWrapperModal } from './session/SessionWrapperModal';
import { times } from 'underscore';
interface Props {
i18n: any;
profileName: string;
avatarPath: string;
pubkey: string;
onClose: any;
onOk: any;
i18n?: any;
profileName?: string;
avatarPath?: string;
pubkey?: string;
onClose?: any;
onOk?: any;
theme: DefaultTheme;
}
@ -46,9 +53,9 @@ export class EditProfileDialog extends React.Component<Props, State> {
this.fireInputEvent = this.fireInputEvent.bind(this);
this.state = {
profileName: this.props.profileName,
setProfileName: this.props.profileName,
avatar: this.props.avatarPath,
profileName: this.props.profileName || '',
setProfileName: this.props.profileName || '',
avatar: this.props.avatarPath || '',
mode: 'default',
loading: false,
};
@ -58,8 +65,44 @@ export class EditProfileDialog extends React.Component<Props, State> {
window.addEventListener('keyup', this.onKeyUp);
}
async componentDidMount() {
const ourNumber = window.storage.get('primaryDevicePubKey');
const conversation = await window
.getConversationController()
.getOrCreateAndWait(ourNumber, ConversationTypeEnum.PRIVATE);
const readFile = (attachment: any) =>
new Promise((resolve, reject) => {
const fileReader = new FileReader();
fileReader.onload = (e: any) => {
const data = e.target.result;
resolve({
...attachment,
data,
size: data.byteLength,
});
};
fileReader.onerror = reject;
fileReader.onabort = reject;
fileReader.readAsArrayBuffer(attachment.file);
});
const avatarPath = conversation.getAvatarPath();
const profile = conversation.getLokiProfile();
const displayName = profile && profile.displayName;
this.setState({
...this.state,
profileName: profile.profileName,
avatar: avatarPath || '',
setProfileName: profile.profileName
})
}
public render() {
const i18n = this.props.i18n;
// const i18n = this.props.i18n;
const i18n = window.i18n;
const viewDefault = this.state.mode === 'default';
const viewEdit = this.state.mode === 'edit';
@ -70,61 +113,67 @@ export class EditProfileDialog extends React.Component<Props, State> {
const backButton =
viewEdit || viewQR
? [
{
iconType: SessionIconType.Chevron,
iconRotation: 90,
onClick: () => {
this.setState({ mode: 'default' });
},
{
iconType: SessionIconType.Chevron,
iconRotation: 90,
onClick: () => {
this.setState({ mode: 'default' });
},
]
},
]
: undefined;
return (
<SessionModal
title={i18n('editProfileModalTitle')}
onClose={this.closeDialog}
headerReverse={viewEdit || viewQR}
headerIconButtons={backButton}
theme={this.props.theme}
>
<div className="spacer-md" />
{viewQR && this.renderQRView(sessionID)}
{viewDefault && this.renderDefaultView()}
{viewEdit && this.renderEditView()}
<div className="session-id-section">
<PillDivider text={window.i18n('yourSessionID')} />
<p className={classNames('text-selectable', 'session-id-section-display')}>{sessionID}</p>
<div className="spacer-lg" />
<SessionSpinner loading={this.state.loading} />
{viewDefault || viewQR ? (
<SessionButton
text={window.i18n('editMenuCopy')}
buttonType={SessionButtonType.BrandOutline}
buttonColor={SessionButtonColor.Green}
onClick={() => {
this.copySessionID(sessionID);
}}
/>
) : (
!this.state.loading && (
// <SessionModal
// title={i18n('editProfileModalTitle')}
// onClose={this.closeDialog}
// headerReverse={viewEdit || viewQR}
// headerIconButtons={backButton}
// theme={this.props.theme}
// >
<div className="edit-profile-dialog">
<SessionWrapperModal
>
<div className="spacer-md" />
{viewQR && this.renderQRView(sessionID)}
{viewDefault && this.renderDefaultView()}
{viewEdit && this.renderEditView()}
<div className="session-id-section">
<PillDivider text={window.i18n('yourSessionID')} />
<p className={classNames('text-selectable', 'session-id-section-display')}>{sessionID}</p>
<div className="spacer-lg" />
<SessionSpinner loading={this.state.loading} />
{viewDefault || viewQR ? (
<SessionButton
text={window.i18n('save')}
text={window.i18n('editMenuCopy')}
buttonType={SessionButtonType.BrandOutline}
buttonColor={SessionButtonColor.Green}
onClick={this.onClickOK}
disabled={this.state.loading}
onClick={() => {
this.copySessionID(sessionID);
}}
/>
)
)}
<div className="spacer-lg" />
</div>
</SessionModal>
) : (
!this.state.loading && (
<SessionButton
text={window.i18n('save')}
buttonType={SessionButtonType.BrandOutline}
buttonColor={SessionButtonColor.Green}
onClick={this.onClickOK}
disabled={this.state.loading}
/>
)
)}
<div className="spacer-lg" />
</div>
{/* </SessionModal> */}
</SessionWrapperModal>
</div>
);
}
@ -280,9 +329,9 @@ export class EditProfileDialog extends React.Component<Props, State> {
const avatar =
this.inputEl &&
this.inputEl.current &&
this.inputEl.current.files &&
this.inputEl.current.files.length > 0
this.inputEl.current &&
this.inputEl.current.files &&
this.inputEl.current.files.length > 0
? this.inputEl.current.files[0]
: null;
@ -291,7 +340,8 @@ export class EditProfileDialog extends React.Component<Props, State> {
loading: true,
},
async () => {
await this.props.onOk(newName, avatar);
// await this.props.onOk(newName, avatar);
await window.commitProfileEdits(newName, avatar);
this.setState({
loading: false,

@ -1,10 +1,4 @@
import React, { useState, useEffect } from 'react';
import { SessionButton, SessionButtonColor, SessionButtonType } from './session/SessionButton';
import { SessionModal } from './session/SessionModal';
import { DefaultTheme } from 'styled-components';
import { getPathNodesIPAddresses } from '../session/onions/onionSend';
import { useInterval } from '../hooks/useInterval';
import classNames from 'classnames';
import React from 'react';
import _ from 'lodash';
@ -13,206 +7,136 @@ import { getTheme } from '../state/selectors/theme';
import electron from 'electron';
import { useSelector } from 'react-redux';
import { StateType } from '../state/reducer';
import { SessionIconButton, SessionIconSize, SessionIconType } from './session/icon';
import { Constants } from '../session';
import { SessionIcon, SessionIconSize, SessionIconType } from './session/icon';
const { shell } = electron;
import { SessionWrapperModal } from '../components/session/SessionWrapperModal';
import { Snode } from '../session/onions';
import ip2country from "ip2country";
import countryLookup from "country-code-lookup";
// import ipLocation = require('ip-location');
// interface OnionStatusDialogProps {
// theme: DefaultTheme;
// nodes?: Array<string>;
// onClose: any;
// }
// export interface IPathNode {
// ip?: string;
// label: string;
// }
// export const OnionPath = (props: { nodes: IPathNode[]; hasPath: boolean }) => {
// const { nodes, hasPath } = props;
// return (
// <div className="onionPath">
// {nodes.map(node => {
// return OnionPathNode({ hasPath, node });
// })}
// </div>
// );
// };
// export const OnionStatusDialog = (props: OnionStatusDialogProps) => {
// const { theme, onClose } = props;
// const [onionPathAddresses, setOnionPathAddresses] = useState<string[]>([]);
// const [pathNodes, setPathNodes] = useState<IPathNode[]>([]);
// const [hasPath, setHasPath] = useState<boolean>(false);
// const getOnionPathAddresses = () => {
// const onionPathAddresses = getPathNodesIPAddresses();
// console.log('Current Onion Path - ', onionPathAddresses);
// setOnionPathAddresses(onionPathAddresses);
// };
// const buildOnionPath = () => {
// // TODO: Add i18n to onion path
// // Default path values
// let path = [
// {
// label: 'You',
// },
// {
// ip: 'Connecting...',
// label: 'Entry Node',
// },
// {
// ip: 'Connecting...',
// label: 'Service Node',
// },
// {
// ip: 'Connecting...',
// label: 'Service Node',
// },
// {
// label: 'Destination',
// },
// ];
// // FIXME call function to check if an onion path exists
// setHasPath(onionPathAddresses.length !== 0);
// // if there is a onion path, update the addresses
// if (onionPathAddresses.length) {
// onionPathAddresses.forEach((ipAddress, index) => {
// const pathIndex = index + 1;
// path[pathIndex].ip = ipAddress;
// });
// }
// setPathNodes(path);
// };
// useInterval(() => {
// getOnionPathAddresses();
// }, 1000);
// useEffect(() => {
// buildOnionPath();
// }, [onionPathAddresses]);
// const openFAQPage = () => {
// console.log('Opening FAQ Page');
// shell.openExternal('https://getsession.org/faq/#onion-routing');
// };
// return (
// <SessionModal title={window.i18n('onionPathIndicatorTitle')} theme={theme} onClose={onClose}>
// <div className="spacer-sm" />
// <div className="onionDescriptionContainer">
// <p>{window.i18n('onionPathIndicatorDescription')}</p>
// </div>
// <OnionPath nodes={pathNodes} hasPath={hasPath} />
// <SessionButton
// text={window.i18n('learnMore')}
// buttonType={SessionButtonType.BrandOutline}
// buttonColor={SessionButtonColor.Green}
// onClick={openFAQPage}
// />
// </SessionModal>
// );
// };
import ip2country from 'ip2country';
import countryLookup from 'country-code-lookup';
import { useTheme } from 'styled-components';
export type OnionPathModalType = {
onConfirm?: () => void;
onClose?: () => void;
confirmText?: string;
cancelText?: string;
title?: string;
};
export type StatusLightType = {
glowStartDelay: number;
glowDuration: number;
color?: string;
};
const OnionPathModalInner = (props: any) => {
const onionPath = useSelector((state: StateType) => state.onionPaths.snodePath);
const onionNodes = useSelector((state: StateType) => state.onionPaths.snodePath);
const onionPath = onionNodes.path;
// including the device and destination in calculation
const glowDuration = onionPath.length + 2;
const nodes = [
{
label: window.i18n('device'),
},
...onionNodes.path,
,
{
label: window.i18n('destination'),
},
];
return (
<div className="onion__node-list">
{onionPath.path.map((snode: Snode, index: number) => {
{nodes.map((snode: Snode | any, index: number) => {
return (
<>
<LabelledStatusLight snode={snode} ></LabelledStatusLight>
<OnionNodeStatusLight
glowDuration={glowDuration}
glowStartDelay={index}
label={snode.label}
snode={snode}
></OnionNodeStatusLight>
</>
);
})}
{/* TODO: Destination node maybe pass in light colour maybe changes based on if 3 nodes are connected similar to the action panel light? */}
<LabelledStatusLight label={'Destination'}></LabelledStatusLight>
</div>
);
};
export type OnionNodeStatusLightType = {
snode: Snode;
label?: string;
glowStartDelay: number;
glowDuration: number;
};
/**
* Component containing a coloured status light and an adjacent country label.
* @param props
* @returns
* @param props
* @returns
*/
export const LabelledStatusLight = (props: any): JSX.Element => {
let { snode, label } = props;
export const OnionNodeStatusLight = (props: OnionNodeStatusLightType): JSX.Element => {
const { snode, label, glowStartDelay, glowDuration } = props;
const theme = useTheme();
let labelText = label ? label : countryLookup.byIso(ip2country(snode.ip))?.country;
console.log('@@@@ country data:: ', labelText);
if (!labelText) {
labelText = `${snode.ip} - Destination unknown`;
console.log(`@@@@ country data failure on code:: ${ip2country(snode.ip)} and ip ${snode.ip}`);
labelText = window.i18n('unknownCountry');
}
return (
<div className="onion__node">
<StatusLight color={Constants.UI.COLORS.GREEN}></StatusLight>
{labelText ?
<StatusLight
glowDuration={glowDuration}
glowStartDelay={glowStartDelay}
color={theme.colors.accent}
></StatusLight>
{labelText ? (
<>
<div className="onion-node__country">{labelText}</div>
</>
:
null
}
) : null}
</div>
)
}
export const OnionNodeLight = (props: any) => {
}
);
};
export const StatusLight = (props: any) => {
const [showModal, toggleShowModal] = useState(false);
const { isSelected, color } = props;
/**
* An icon with a pulsating glow emission.
*/
export const StatusLight = (props: StatusLightType) => {
const { glowStartDelay, glowDuration, color } = props;
const theme = useSelector(getTheme);
const openFAQPage = () => {
console.log('Opening FAQ Page');
shell.openExternal('https://getsession.org/faq/#onion-routing');
};
const onClick = () => {
toggleShowModal(!showModal);
};
return (
<>
<SessionIconButton
iconSize={SessionIconSize.Medium}
iconType={SessionIconType.Circle}
<SessionIcon
borderRadius={50}
iconColor={color}
glowDuration={glowDuration}
glowStartDelay={glowStartDelay}
iconType={SessionIconType.Circle}
iconSize={SessionIconSize.Medium}
theme={theme}
isSelected={isSelected}
onClick={onClick}
/>
{showModal ? (
<SessionWrapperModal title={"Onion Path"} onclick={onClick} showModal={showModal}>
<OnionPathModalInner></OnionPathModalInner>
</SessionWrapperModal>
) : null}
</>
);
};
export const OnionPathModal = (props: OnionPathModalType) => {
const onConfirm = () => {
shell.openExternal('https://getsession.org/faq/#onion-routing');
};
return (
<SessionWrapperModal
title={props.title || window.i18n('onionPathIndicatorTitle')}
confirmText={props.confirmText || window.i18n('learnMore')}
cancelText={props.cancelText || window.i18n('cancel')}
onConfirm={onConfirm}
onClose={props.onClose}
>
<OnionPathModalInner {...props}></OnionPathModalInner>
</SessionWrapperModal>
);
};

@ -36,10 +36,12 @@ import { forceRefreshRandomSnodePool } from '../../session/snode_api/snodePool';
import { SwarmPolling } from '../../session/snode_api/swarmPolling';
import { getOnionPathStatus } from '../../session/onions/onionSend';
import { Constants } from '../../session';
import { StatusLight } from '../OnionStatusDialog';
import { StateType } from '../../state/reducer';
import _ from 'lodash';
import { useNetwork } from '../../hooks/useNetwork';
import { OnionPathModal } from '../OnionStatusDialog';
import { EditProfileDialog } from '../EditProfileDialog';
import { useTheme } from 'styled-components';
// tslint:disable-next-line: no-import-side-effect no-submodule-imports
@ -53,20 +55,33 @@ export enum SectionType {
PathIndicator,
}
const Section = (props: { type: SectionType; avatarPath?: string; hasOnionPath?: boolean }) => {
const Section = (props: {
setModal?: any;
type: SectionType;
avatarPath?: string;
hasOnionPath?: boolean;
}) => {
const ourNumber = useSelector(getOurNumber);
const unreadMessageCount = useSelector(getUnreadMessageCount);
const theme = useSelector(getTheme);
const dispatch = useDispatch();
const { type, avatarPath, hasOnionPath } = props;
const { setModal, type, avatarPath, hasOnionPath } = props;
const focusedSection = useSelector(getFocusedSection);
const isSelected = focusedSection === props.type;
const handleModalClose = () => {
setModal(null);
}
const handleClick = () => {
/* tslint:disable:no-void-expression */
if (type === SectionType.Profile) {
window.showEditProfileDialog();
// window.showEditProfileDialog();
// setModal(<EditProfileDialog2 onClose={() => setModal(null)}></EditProfileDialog2>);
setModal(<EditProfileDialog onClose={handleModalClose} theme={theme} ></EditProfileDialog>);
} else if (type === SectionType.Moon) {
const themeFromSettings = window.Events.getThemeSetting();
const updatedTheme = themeFromSettings === 'dark' ? 'light' : 'dark';
@ -76,7 +91,7 @@ const Section = (props: { type: SectionType; avatarPath?: string; hasOnionPath?:
dispatch(applyTheme(newThemeObject));
} else if (type === SectionType.PathIndicator) {
// Show Path Indicator Modal
window.showOnionStatusDialog();
setModal(<OnionPathModal onClose={handleModalClose}></OnionPathModal>);
} else {
dispatch(clearSearch());
dispatch(showLeftPaneSection(type));
@ -102,7 +117,24 @@ const Section = (props: { type: SectionType; avatarPath?: string; hasOnionPath?:
let iconColor = undefined;
if (type === SectionType.PathIndicator) {
// Set icon color based on result
iconColor = hasOnionPath ? Constants.UI.COLORS.GREEN : Constants.UI.COLORS.DANGER;
const red = theme.colors.destructive;
const green = theme.colors.accent;
const orange = theme.colors.warning;
iconColor = hasOnionPath ? theme.colors.accent : theme.colors.destructive;
const onionState = useSelector((state: StateType) => state.onionPaths);
iconColor = red;
const isOnline = useNetwork();
if (!(onionState && onionState.snodePath) || !isOnline) {
iconColor = Constants.UI.COLORS.DANGER;
} else {
const onionSnodePath = onionState.snodePath;
if (onionState && onionSnodePath && onionSnodePath.path.length > 0) {
let onionNodeCount = onionSnodePath.path.length;
iconColor = onionNodeCount > 2 ? green : onionNodeCount > 1 ? orange : red;
}
}
}
const unreadToShow = type === SectionType.Message ? unreadMessageCount : undefined;
@ -128,45 +160,18 @@ const Section = (props: { type: SectionType; avatarPath?: string; hasOnionPath?:
iconType = SessionIconType.Moon;
}
// calculate light status.
// TODO: Refactor this so this logic is reusable elsewhere.
// TEST:
if (type === SectionType.PathIndicator) {
const onionState = useSelector((state: StateType) => state.onionPaths);
let statusColor = Constants.UI.COLORS.DANGER;
const isOnline = useNetwork();
if (!(onionState && onionState.snodePath) || !isOnline) {
return <StatusLight isSelected={isSelected} color={Constants.UI.COLORS.DANGER}></StatusLight>;
} else {
const onionSnodePath = onionState.snodePath;
if (onionState && onionSnodePath && onionSnodePath.path.length > 0) {
let onionNodeCount = onionSnodePath.path.length;
statusColor =
onionNodeCount > 2
? Constants.UI.COLORS.GREEN
: onionNodeCount > 1
? Constants.UI.COLORS.WARNING
: Constants.UI.COLORS.DANGER;
}
}
return <StatusLight isSelected={isSelected} color={statusColor}></StatusLight>;
}
return (
<SessionIconButton
iconSize={SessionIconSize.Medium}
iconType={iconType}
iconColor={iconColor}
notificationCount={unreadToShow}
onClick={handleClick}
isSelected={isSelected}
theme={theme}
/>
<>
<SessionIconButton
iconSize={SessionIconSize.Medium}
iconType={iconType}
iconColor={iconColor}
notificationCount={unreadToShow}
onClick={handleClick}
isSelected={isSelected}
theme={theme}
/>
</>
);
};
@ -243,6 +248,8 @@ export const ActionsPanel = () => {
const [hasOnionPath, setHasOnionPath] = useState<boolean>(false);
const ourPrimaryConversation = useSelector(getOurPrimaryConversation);
const [modal, setModal] = useState<any>(null);
// this maxi useEffect is called only once: when the component is mounted.
// For the action panel, it means this is called only one per app start/with a user loggedin
useEffect(() => {
@ -259,42 +266,6 @@ export const ActionsPanel = () => {
const getOnionPathIndicator = () => {
const hasOnionPath = getOnionPathStatus();
// const update: OnionUpdate = {
// nodes: [
// {
// ip: 'hi',
// label: 'hi',
// isConnected: Math.random() > 0.5,
// isAttemptingConnect: Math.random() > 0.7,
// },
// {
// ip: 'hi2',
// label: 'hi2',
// isConnected: Math.random() > 0.5,
// isAttemptingConnect: Math.random() > 0.7,
// },
// {
// ip: 'hi3',
// label: 'hi3',
// isConnected: Math.random() > 0.5,
// isAttemptingConnect: Math.random() > 0.7,
// },
// ],
// };
// dispatch(updateOnionPaths(update));
// TEST: Stuff
// let testNode: SnodePath = {
// bad: false,
// path: new Array<Snode>()
// }
// dispatch(updateOnionPaths(testNode));
// console.log('Is Onion Path found -', hasOnionPath);
setHasOnionPath(hasOnionPath);
};
@ -324,15 +295,20 @@ export const ActionsPanel = () => {
return (
<div className="module-left-pane__sections-container">
<Section type={SectionType.Profile} avatarPath={ourPrimaryConversation.avatarPath} />
<Section type={SectionType.Message} />
<Section type={SectionType.Contact} />
<Section type={SectionType.Settings} />
<Section
setModal={setModal}
type={SectionType.Profile}
avatarPath={ourPrimaryConversation.avatarPath}
/>
<Section setModal={setModal} type={SectionType.Message} />
<Section setModal={setModal} type={SectionType.Contact} />
<Section setModal={setModal} type={SectionType.Settings} />
{modal ? modal : null}
<SessionToastContainer />
<Section type={SectionType.PathIndicator} hasOnionPath={hasOnionPath} />
<Section type={SectionType.Moon} />
<Section setModal={setModal} type={SectionType.PathIndicator} hasOnionPath={hasOnionPath} />
<Section setModal={setModal} type={SectionType.Moon} />
</div>
);
};

@ -5,7 +5,7 @@ import { SessionIconButton, SessionIconSize, SessionIconType } from './icon/';
import { SessionButton, SessionButtonColor, SessionButtonType } from './SessionButton';
import { DefaultTheme } from 'styled-components';
import { useKeyPress } from "use-hooks";
import { useKeyPress } from 'use-hooks';
interface Props {
title: string;
@ -28,44 +28,96 @@ interface Props {
theme: DefaultTheme;
}
interface State {
isVisible: boolean;
}
export type SessionWrapperModalType = {
title?: string;
onConfirm?: () => void;
onClose?: () => void;
showClose?: boolean
confirmText?: string;
cancelText?: string;
showExitIcon?: boolean;
theme?: any;
headerIconButtons?: any[];
children: any;
};
export const SessionWrapperModal = (props: any) => {
const { onclick, showModal, title, onConfirm } = props;
export const SessionWrapperModal = (props: SessionWrapperModalType) => {
const {
title,
onConfirm,
onClose,
showClose = false,
confirmText,
cancelText,
showExitIcon,
theme,
headerIconButtons,
} = props;
useEffect(() => {
window.addEventListener('keyup', upHandler);
return () => {
window.removeEventListener('keyup', upHandler);
}
}, [])
};
}, []);
// TODO: warrick: typing
const upHandler = ({key}: any ) => {
const upHandler = ({ key }: any) => {
if (key === 'Escape') {
props.onclick();
if (props.onClose) {
props.onClose();
}
}
}
};
return (
<div className="loki-dialog session-confirm-wrapper modal">
<div className="session-confirm-wrapper">
<div className="session-modal">
<div className="session-modal__header">
<div className="session-modal__header__title">
{/* Onion Nodes / Generic Title {title} */}
{title}
<div className="session-modal__header__close">
{showExitIcon ? (
<SessionIconButton
iconType={SessionIconType.Exit}
iconSize={SessionIconSize.Small}
onClick={props.onClose}
theme={props.theme}
/>
) : null}
</div>
<div className="session-modal__header__title">{title}</div>
<div className="session-modal__header__icons">
{headerIconButtons
? headerIconButtons.map((iconItem: any) => {
return (
<SessionIconButton
key={iconItem.iconType}
iconType={iconItem.iconType}
iconSize={SessionIconSize.Large}
iconRotation={iconItem.iconRotation}
onClick={iconItem.onClick}
theme={props.theme}
/>
);
})
: null}
</div>
</div>
<div className="session-modal__body">
<div className="session-modal__centered">
{props.children}
<div className="session-modal__button-group">
<SessionButton onClick={props.onclick}>Close</SessionButton>
{onConfirm ? (
<SessionButton onClick={props.onConfirm}>
{confirmText || window.i18n('ok')}
</SessionButton>
) : null}
{onClose && showClose ? (
<SessionButton onClick={props.onClose}>
{cancelText || window.i18n('close')}
</SessionButton>
) : null}
</div>
</div>
</div>
@ -74,110 +126,3 @@ export const SessionWrapperModal = (props: any) => {
</div>
);
};
// export class SessionModal extends React.PureComponent<Props, State> {
// public static defaultProps = {
// showExitIcon: true,
// showHeader: true,
// headerReverse: false,
// };
// private node: HTMLDivElement | null;
// constructor(props: any) {
// super(props);
// this.state = {
// isVisible: true,
// };
// this.close = this.close.bind(this);
// this.onKeyUp = this.onKeyUp.bind(this);
// this.node = null;
// window.addEventListener('keyup', this.onKeyUp);
// }
// public componentDidMount() {
// document.addEventListener('mousedown', this.handleClick, false);
// }
// public componentWillUnmount() {
// document.removeEventListener('mousedown', this.handleClick, false);
// }
// public handleClick = (e: any) => {
// if (this.node && this.node.contains(e.target)) {
// return;
// }
// this.close();
// };
// public render() {
// const { title, headerIconButtons, showExitIcon, showHeader, headerReverse } = this.props;
// const { isVisible } = this.state;
// return isVisible ? (
// <div ref={node => (this.node = node)} className={'session-modal'}>
// {showHeader ? (
// <>
// <div className={classNames('session-modal__header', headerReverse && 'reverse')}>
// <div className="session-modal__header__close">
// {showExitIcon ? (
// <SessionIconButton
// iconType={SessionIconType.Exit}
// iconSize={SessionIconSize.Small}
// onClick={this.close}
// theme={this.props.theme}
// />
// ) : null}
// </div>
// <div className="session-modal__header__title">{title}</div>
// <div className="session-modal__header__icons">
// {headerIconButtons
// ? headerIconButtons.map((iconItem: any) => {
// return (
// <SessionIconButton
// key={iconItem.iconType}
// iconType={iconItem.iconType}
// iconSize={SessionIconSize.Large}
// iconRotation={iconItem.iconRotation}
// onClick={iconItem.onClick}
// theme={this.props.theme}
// />
// );
// })
// : null}
// </div>
// </div>
// </>
// ) : null}
// <div className="session-modal__body">{this.props.children}</div>
// </div>
// ) : null;
// }
// public close() {
// this.setState({
// isVisible: false,
// });
// window.removeEventListener('keyup', this.onKeyUp);
// document.removeEventListener('mousedown', this.handleClick, false);
// if (this.props.onClose) {
// this.props.onClose();
// }
// }
// public onKeyUp(event: any) {
// switch (event.key) {
// case 'Esc':
// case 'Escape':
// this.close();
// break;
// default:
// }
// }
// }

@ -2,6 +2,7 @@ import React from 'react';
import { icons, SessionIconSize, SessionIconType } from '../icon';
import styled, { css, DefaultTheme, keyframes } from 'styled-components';
import { drop } from 'lodash';
export type SessionIconProps = {
iconType: SessionIconType;
@ -9,6 +10,9 @@ export type SessionIconProps = {
iconColor?: string;
iconRotation?: number;
rotateDuration?: number;
glowDuration?: number;
borderRadius?: number;
glowStartDelay?: number;
theme: DefaultTheme;
};
@ -40,6 +44,10 @@ type StyledSvgProps = {
height: string | number;
iconRotation: number;
rotateDuration?: number;
borderRadius?: number;
glowDuration?: number;
glowStartDelay?: number;
iconColor?: string;
};
const rotate = keyframes`
@ -51,21 +59,59 @@ const rotate = keyframes`
}
`;
/**
* Creates a glow animation made for multiple element sequentially
* @param color
* @param glowDuration
* @param glowStartDelay
* @returns
*/
const glow = (color: string, glowDuration: number, glowStartDelay: number) => {
let dropShadowType = `drop-shadow(0px 0px 6px ${color}) `;
//increase shadow intensity by 3
let dropShadow = `${dropShadowType.repeat(2)};`;
// TODO: Decrease dropshadow for last frame
// creating keyframe for sequential animations
let kf = '';
for (let i = 0; i <= glowDuration; i++) {
// const percent = (100 / glowDuration) * i;
const percent = (100 / glowDuration) * i;
if (i === glowStartDelay) {
kf += `${percent}% {
filter: ${dropShadow}
}`;
} else {
kf += `${percent}% {
filter: none;
}`;
}
}
return keyframes`${kf}`;
};
const animation = (props: any) => {
if (props.rotateDuration) {
return css`
${rotate} ${props.rotateDuration}s infinite linear;
`;
} else if (props.glowDuration !== undefined && props.glowStartDelay !== undefined) {
return css`
${glow(props.iconColor, props.glowDuration, props.glowStartDelay)} ${2}s ease-in infinite;
`;
} else {
return;
}
};
// ${glow(props.iconColor, props.glowDuration, props.glowStartDelay)} ${props.glowDuration}s ease-in ${ props.glowStartDelay }s infinite alternate;
//tslint:disable no-unnecessary-callback-wrapper
const Svg = styled.svg<StyledSvgProps>`
width: ${props => props.width};
animation: ${props => animation(props)};
transform: ${props => `rotate(${props.iconRotation}deg)`};
border-radius: ${props => props.borderRadius};
`;
//tslint:enable no-unnecessary-callback-wrapper
@ -77,6 +123,9 @@ const SessionSvg = (props: {
iconRotation: number;
iconColor?: string;
rotateDuration?: number;
glowDuration?: number;
glowStartDelay?: number;
borderRadius?: number;
theme: DefaultTheme;
}) => {
const colorSvg = props.iconColor || props?.theme?.colors.textColor;
@ -84,6 +133,15 @@ const SessionSvg = (props: {
return (
<Svg {...props}>
{/* { props.glowDuration ?
<defs>
<filter>
<feDropShadow dx="0.2" dy="0.4" stdDeviation="0.2" />
</filter>
</defs>
:
null
} */}
{pathArray.map((path, index) => {
return <path key={index} fill={colorSvg} d={path} />;
})}
@ -92,7 +150,15 @@ const SessionSvg = (props: {
};
export const SessionIcon = (props: SessionIconProps) => {
const { iconType, iconColor, theme, rotateDuration } = props;
const {
iconType,
iconColor,
theme,
rotateDuration,
glowDuration,
borderRadius,
glowStartDelay,
} = props;
let { iconSize, iconRotation } = props;
iconSize = iconSize || SessionIconSize.Medium;
iconRotation = iconRotation || 0;
@ -111,6 +177,9 @@ export const SessionIcon = (props: SessionIconProps) => {
width={iconDimensions * ratio}
height={iconDimensions}
rotateDuration={rotateDuration}
glowDuration={glowDuration}
glowStartDelay={glowStartDelay}
borderRadius={borderRadius}
iconRotation={iconRotation}
iconColor={iconColor}
theme={theme}

@ -1 +1 @@
declare module 'ip2country';
declare module 'ip2country';

@ -63,7 +63,11 @@ export class OnionPaths {
goodPaths = this.onionPaths.filter(x => !x.bad);
}
window.inboxStore?.dispatch(updateOnionPaths(goodPaths[0]));
if (goodPaths.length <= 0) {
window.inboxStore?.dispatch(updateOnionPaths({ path: new Array<Snode>(), bad: true }));
} else {
window.inboxStore?.dispatch(updateOnionPaths(goodPaths[0]));
}
const paths = _.shuffle(goodPaths);
@ -105,8 +109,6 @@ export class OnionPaths {
log.error('LokiSnodeAPI::getOnionPath - otherPaths no path in', otherPaths[0]);
}
// window.inboxStore?.dispatch(updateOnionPaths(otherPaths[0]));
return otherPaths[0].path;
}

@ -3,9 +3,11 @@ import React from 'react';
// import 'reset-css/reset.css';
import { DefaultTheme, ThemeProvider } from 'styled-components';
import { pushToastWarning } from '../../session/utils/Toast';
const white = '#ffffff';
const black = '#000000';
const warning = '#e7b100';
const destructive = '#ff453a';
const accentLightTheme = '#00e97b';
const accentDarkTheme = '#00f782';
@ -40,6 +42,7 @@ export const lightTheme: DefaultTheme = {
colors: {
accent: accentLightTheme,
accentButton: black,
warning: warning,
destructive: destructive,
cellBackground: '#fcfcfc',
modalBackground: '#fcfcfc',
@ -95,6 +98,7 @@ export const darkTheme = {
colors: {
accent: accentDarkTheme,
accentButton: accentDarkTheme,
warning: warning,
destructive: destructive,
cellBackground: '#1b1b1b',
modalBackground: '#101011',

@ -1,5 +1,6 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { SnodePath, Snode } from "../../session/onions/index";
import _, { forEach } from 'underscore';
import { SnodePath, Snode } from '../../session/onions/index';
export type OnionState = {
// nodes: Array<OnionPathNodeType>;
@ -15,9 +16,9 @@ export type OnionState = {
const initialState = {
snodePath: {
path: new Array<Snode>(),
bad: false
}
}
bad: false,
},
};
/**
* This slice is the one holding the default joinable rooms fetched once in a while from the default opengroup v2 server.
@ -26,11 +27,10 @@ const onionSlice = createSlice({
name: 'onionPaths',
initialState,
reducers: {
// updateOnionPaths(state, action: PayloadAction<OnionUpdate>) {
updateOnionPaths(state, action: PayloadAction<SnodePath>) {
return {
snodePath: action.payload
}
let newPayload = { snodePath: action.payload };
let isEqual = JSON.stringify(state, null, 2) == JSON.stringify(newPayload, null, 2);
return isEqual ? state : newPayload;
},
},
});

1
ts/styled.d.ts vendored

@ -26,6 +26,7 @@ declare module 'styled-components' {
colors: {
accent: string;
accentButton: string;
warning: string;
destructive: string;
cellBackground: string;
modalBackground: string;

8
ts/window.d.ts vendored

@ -15,8 +15,9 @@ import { Store } from 'redux';
import { MessageController } from './session/messages/MessageController';
import { DefaultTheme } from 'styled-components';
import { ConversationCollection } from './models/conversation';
import { ConversationCollection, ConversationModel } from './models/conversation';
import { ConversationType } from './state/ducks/conversations';
import { ConversationController } from './session/conversations';
/*
We declare window stuff here instead of global.d.ts because we are importing other declarations.
@ -99,5 +100,10 @@ declare global {
darkTheme: DefaultTheme;
LokiPushNotificationServer: any;
LokiPushNotificationServerApi: any;
getConversationController: () => ConversationController;
getOrCreateAndWait: () => Promise<ConversationModel>;
commitProfileEdits: (string, string) => void;
}
}

Loading…
Cancel
Save