From 066e0d954e71baa9878e40bfc382a4036c53fda1 Mon Sep 17 00:00:00 2001 From: Warrick Corfe-Tan Date: Fri, 28 May 2021 10:09:35 +1000 Subject: [PATCH] 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. --- _locales/en/messages.json | 12 + js/background.js | 226 +++++++++++++++- stylesheets/_index.scss | 2 +- stylesheets/_modal.scss | 10 +- stylesheets/_session.scss | 58 ++-- stylesheets/themes.scss | 3 + ts/components/EditProfileDialog.tsx | 176 +++++++----- ts/components/OnionStatusDialog.tsx | 256 ++++++------------ ts/components/session/ActionsPanel.tsx | 148 +++++----- ts/components/session/SessionWrapperModal.tsx | 191 +++++-------- ts/components/session/icon/SessionIcon.tsx | 71 ++++- ts/ip2country.d.ts | 2 +- ts/session/onions/index.ts | 8 +- ts/state/ducks/SessionTheme.tsx | 4 + ts/state/ducks/onion.tsx | 16 +- ts/styled.d.ts | 1 + ts/window.d.ts | 8 +- 17 files changed, 702 insertions(+), 490 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 93b696a5b..cd7c9d7d0 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -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" } } diff --git a/js/background.js b/js/background.js index a8506d0a6..12aa4ab98 100644 --- a/js/background.js +++ b/js/background.js @@ -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; diff --git a/stylesheets/_index.scss b/stylesheets/_index.scss index 47864e729..323813331 100644 --- a/stylesheets/_index.scss +++ b/stylesheets/_index.scss @@ -48,7 +48,7 @@ .avatar-center-inner { display: flex; - padding-top: 30px; + // padding-top: 30px; } .upload-btn-background { diff --git a/stylesheets/_modal.scss b/stylesheets/_modal.scss index 4eb7137e8..9356a3808 100644 --- a/stylesheets/_modal.scss +++ b/stylesheets/_modal.scss @@ -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; - } } } diff --git a/stylesheets/_session.scss b/stylesheets/_session.scss index 1521ffeb3..857104625 100644 --- a/stylesheets/_session.scss +++ b/stylesheets/_session.scss @@ -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; +// } +// } // } diff --git a/stylesheets/themes.scss b/stylesheets/themes.scss index 2ff4d278e..bd346b42b 100644 --- a/stylesheets/themes.scss +++ b/stylesheets/themes.scss @@ -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, diff --git a/ts/components/EditProfileDialog.tsx b/ts/components/EditProfileDialog.tsx index c2f623d5d..eeeb2b100 100644 --- a/ts/components/EditProfileDialog.tsx +++ b/ts/components/EditProfileDialog.tsx @@ -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 { 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 { 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 { 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 ( - -
- - {viewQR && this.renderQRView(sessionID)} - {viewDefault && this.renderDefaultView()} - {viewEdit && this.renderEditView()} - -
- -

{sessionID}

- -
- - - {viewDefault || viewQR ? ( - { - this.copySessionID(sessionID); - }} - /> - ) : ( - !this.state.loading && ( + // +
+ + +
+ + {viewQR && this.renderQRView(sessionID)} + {viewDefault && this.renderDefaultView()} + {viewEdit && this.renderEditView()} + +
+ +

{sessionID}

+ +
+ + + {viewDefault || viewQR ? ( { + this.copySessionID(sessionID); + }} /> - ) - )} - -
-
- + ) : ( + !this.state.loading && ( + + ) + )} + +
+
+ {/* */} + +
); } @@ -280,9 +329,9 @@ export class EditProfileDialog extends React.Component { 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 { loading: true, }, async () => { - await this.props.onOk(newName, avatar); + // await this.props.onOk(newName, avatar); + await window.commitProfileEdits(newName, avatar); this.setState({ loading: false, diff --git a/ts/components/OnionStatusDialog.tsx b/ts/components/OnionStatusDialog.tsx index 3daf9f4f8..4bb4ec19e 100644 --- a/ts/components/OnionStatusDialog.tsx +++ b/ts/components/OnionStatusDialog.tsx @@ -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; -// onClose: any; -// } - -// export interface IPathNode { -// ip?: string; -// label: string; -// } - -// export const OnionPath = (props: { nodes: IPathNode[]; hasPath: boolean }) => { -// const { nodes, hasPath } = props; - -// return ( -//
-// {nodes.map(node => { -// return OnionPathNode({ hasPath, node }); -// })} -//
-// ); -// }; - -// export const OnionStatusDialog = (props: OnionStatusDialogProps) => { -// const { theme, onClose } = props; - -// const [onionPathAddresses, setOnionPathAddresses] = useState([]); -// const [pathNodes, setPathNodes] = useState([]); -// const [hasPath, setHasPath] = useState(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 ( -// -//
-//
-//

{window.i18n('onionPathIndicatorDescription')}

-//
- -// - -// -// -// ); -// }; +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 (
- {onionPath.path.map((snode: Snode, index: number) => { + {nodes.map((snode: Snode | any, index: number) => { return ( <> - + ); })} - {/* TODO: Destination node maybe pass in light colour maybe changes based on if 3 nodes are connected similar to the action panel light? */} -
); }; +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 (
- - {labelText ? + + {labelText ? ( <>
{labelText}
- : - null - } + ) : null}
- ) -} - -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 ( <> - - - {showModal ? ( - - - - ) : null} ); }; + +export const OnionPathModal = (props: OnionPathModalType) => { + const onConfirm = () => { + shell.openExternal('https://getsession.org/faq/#onion-routing'); + }; + return ( + + + + ); +}; diff --git a/ts/components/session/ActionsPanel.tsx b/ts/components/session/ActionsPanel.tsx index ee53e66f1..0cf7c0416 100644 --- a/ts/components/session/ActionsPanel.tsx +++ b/ts/components/session/ActionsPanel.tsx @@ -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( setModal(null)}>); + setModal(); } 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(); } 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 ; - } 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 ; - } - return ( - + <> + + ); }; @@ -243,6 +248,8 @@ export const ActionsPanel = () => { const [hasOnionPath, setHasOnionPath] = useState(false); const ourPrimaryConversation = useSelector(getOurPrimaryConversation); + const [modal, setModal] = useState(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() - // } - - // dispatch(updateOnionPaths(testNode)); - - // console.log('Is Onion Path found -', hasOnionPath); setHasOnionPath(hasOnionPath); }; @@ -324,15 +295,20 @@ export const ActionsPanel = () => { return (
-
-
-
-
+
+
+
+
+ {modal ? modal : null} -
-
+
+
); }; diff --git a/ts/components/session/SessionWrapperModal.tsx b/ts/components/session/SessionWrapperModal.tsx index 7f00c71d8..623f8c592 100644 --- a/ts/components/session/SessionWrapperModal.tsx +++ b/ts/components/session/SessionWrapperModal.tsx @@ -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 (
-
- {/* Onion Nodes / Generic Title {title} */} - {title} +
+ {showExitIcon ? ( + + ) : null} +
+
{title}
+
+ {headerIconButtons + ? headerIconButtons.map((iconItem: any) => { + return ( + + ); + }) + : null}
{props.children}
- Close + {onConfirm ? ( + + {confirmText || window.i18n('ok')} + + ) : null} + {onClose && showClose ? ( + + {cancelText || window.i18n('close')} + + ) : null}
@@ -74,110 +126,3 @@ export const SessionWrapperModal = (props: any) => {
); }; - -// export class SessionModal extends React.PureComponent { -// 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 ? ( -//
(this.node = node)} className={'session-modal'}> -// {showHeader ? ( -// <> -//
-//
-// {showExitIcon ? ( -// -// ) : null} -//
-//
{title}
-//
-// {headerIconButtons -// ? headerIconButtons.map((iconItem: any) => { -// return ( -// -// ); -// }) -// : null} -//
-//
-// -// ) : null} - -//
{this.props.children}
-//
-// ) : 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: -// } -// } -// } diff --git a/ts/components/session/icon/SessionIcon.tsx b/ts/components/session/icon/SessionIcon.tsx index 85afa1961..8a02123c7 100644 --- a/ts/components/session/icon/SessionIcon.tsx +++ b/ts/components/session/icon/SessionIcon.tsx @@ -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` 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 ( + {/* { props.glowDuration ? + + + + + + : + null + } */} {pathArray.map((path, index) => { return ; })} @@ -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} diff --git a/ts/ip2country.d.ts b/ts/ip2country.d.ts index 97eb6b44a..6c4567906 100644 --- a/ts/ip2country.d.ts +++ b/ts/ip2country.d.ts @@ -1 +1 @@ -declare module 'ip2country'; \ No newline at end of file +declare module 'ip2country'; diff --git a/ts/session/onions/index.ts b/ts/session/onions/index.ts index 7c82b5401..96d5603fe 100644 --- a/ts/session/onions/index.ts +++ b/ts/session/onions/index.ts @@ -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(), 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; } diff --git a/ts/state/ducks/SessionTheme.tsx b/ts/state/ducks/SessionTheme.tsx index f045df4ea..3432efc28 100644 --- a/ts/state/ducks/SessionTheme.tsx +++ b/ts/state/ducks/SessionTheme.tsx @@ -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', diff --git a/ts/state/ducks/onion.tsx b/ts/state/ducks/onion.tsx index 4db436752..0c3a99a3b 100644 --- a/ts/state/ducks/onion.tsx +++ b/ts/state/ducks/onion.tsx @@ -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; @@ -15,9 +16,9 @@ export type OnionState = { const initialState = { snodePath: { path: new Array(), - 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) { updateOnionPaths(state, action: PayloadAction) { - 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; }, }, }); diff --git a/ts/styled.d.ts b/ts/styled.d.ts index afd7d30bb..905375e34 100644 --- a/ts/styled.d.ts +++ b/ts/styled.d.ts @@ -26,6 +26,7 @@ declare module 'styled-components' { colors: { accent: string; accentButton: string; + warning: string; destructive: string; cellBackground: string; modalBackground: string; diff --git a/ts/window.d.ts b/ts/window.d.ts index 5eb9706cf..b7c282656 100644 --- a/ts/window.d.ts +++ b/ts/window.d.ts @@ -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; + + commitProfileEdits: (string, string) => void; } }