From d7b22e13e180966ea28428cf29932cb8c8c0b2d7 Mon Sep 17 00:00:00 2001 From: Warrick Corfe-Tan Date: Thu, 20 May 2021 11:27:21 +1000 Subject: [PATCH] WIP: Onion path modal using redux state and refactored modals. --- _locales/en/messages.json | 2 +- js/background.js | 2 +- js/views/app_view.js | 2 +- js/views/onion_status_dialog_view.js | 67 +++--- stylesheets/_modal.scss | 39 +++- stylesheets/_session.scss | 42 ++++ ts/components/OnionStatusDialog.tsx | 208 +++++++++++++----- ts/components/session/ActionsPanel.tsx | 77 +++++-- ts/components/session/SessionWrapperModal.tsx | 164 ++++++++++++++ ts/session/constants.ts | 3 + ts/session/onions/index.ts | 2 + ts/session/onions/onionSend.ts | 2 +- ts/state/ducks/onion.tsx | 67 ++++++ ts/state/reducer.ts | 6 + 14 files changed, 581 insertions(+), 102 deletions(-) create mode 100644 ts/components/session/SessionWrapperModal.tsx create mode 100644 ts/state/ducks/onion.tsx diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 2789f2b40..93b696a5b 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -1867,6 +1867,6 @@ "message": "Session hides your IP by bouncing your messages through several Service Nodes in Session's decentralized network. These are the countries your connection is currently being bounced through:" }, "learnMore": { - "message" : "Learn More" + "message": "Learn More" } } diff --git a/js/background.js b/js/background.js index 3b6e3ac27..a8506d0a6 100644 --- a/js/background.js +++ b/js/background.js @@ -529,7 +529,7 @@ window.showOnionStatusDialog = () => { appView.showOnionStatusDialog(); - } + }; // Set user's launch count. const prevLaunchCount = window.getSettingValue('launch-count'); diff --git a/js/views/app_view.js b/js/views/app_view.js index 404141564..bee6d23ce 100644 --- a/js/views/app_view.js +++ b/js/views/app_view.js @@ -129,7 +129,7 @@ showOnionStatusDialog() { // eslint-disable-next-line no-param-reassign const theme = this.getThemeObject(); - const dialog = new Whisper.OnionStatusDialogView({theme}); + const dialog = new Whisper.OnionStatusDialogView({ theme }); this.el.prepend(dialog.el); }, showResetSessionIdDialog() { diff --git a/js/views/onion_status_dialog_view.js b/js/views/onion_status_dialog_view.js index 057715b4d..f7f1a843f 100644 --- a/js/views/onion_status_dialog_view.js +++ b/js/views/onion_status_dialog_view.js @@ -2,37 +2,36 @@ // eslint-disable-next-line func-names (function() { - 'use strict'; - - window.Whisper = window.Whisper || {}; - - Whisper.OnionStatusDialogView = Whisper.View.extend({ - className: 'loki-dialog modal', - initialize({ theme }) { - this.close = this.close.bind(this); - - this.theme = theme; - - this.$el.focus(); - this.render(); - }, - render() { - this.dialogView = new Whisper.ReactWrapperView({ - className: 'onion-status-dialog', - Component: window.Signal.Components.OnionStatusDialog, - props: { - onClose: this.close, - i18n, - theme: this.theme, - }, - }); - - this.$el.append(this.dialogView.el); - return this; - }, - close() { - this.remove(); - }, - }); - })(); - \ No newline at end of file + 'use strict'; + + window.Whisper = window.Whisper || {}; + + Whisper.OnionStatusDialogView = Whisper.View.extend({ + className: 'loki-dialog modal', + initialize({ theme }) { + this.close = this.close.bind(this); + + this.theme = theme; + + this.$el.focus(); + this.render(); + }, + render() { + this.dialogView = new Whisper.ReactWrapperView({ + className: 'onion-status-dialog', + Component: window.Signal.Components.OnionStatusDialog, + props: { + onClose: this.close, + i18n, + theme: this.theme, + }, + }); + + this.$el.append(this.dialogView.el); + return this; + }, + close() { + this.remove(); + }, + }); +})(); diff --git a/stylesheets/_modal.scss b/stylesheets/_modal.scss index 62587b5b5..4eb7137e8 100644 --- a/stylesheets/_modal.scss +++ b/stylesheets/_modal.scss @@ -306,6 +306,43 @@ } } +.modal { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 10000; + padding: 2rem; + background-color: rgba(0, 0, 0, 0.55); + + .modal-content { + background-color: white; + max-height: 80%; + height: 80%; + + .modal-title { + padding: 0 2.25rem; + height: 5rem; + max-height: 5rem; + text-transform: uppercase; + font-size: 1.8rem; + } + + .modal-body { + height: calc(100% - 16rem); + overflow-y: scroll; + padding: 0 2rem; + } + + .modal-footer { + height: 5rem; + max-height: 5rem; + padding: 0 2.25rem; + } + } +} + .onion-status-dialog { .session-modal__header__title { font-size: $session-font-lg; @@ -365,7 +402,7 @@ } .red { - background-color: #FF453A; + background-color: #ff453a; } } } diff --git a/stylesheets/_session.scss b/stylesheets/_session.scss index d3ec7dde5..7fe697f9e 100644 --- a/stylesheets/_session.scss +++ b/stylesheets/_session.scss @@ -1198,6 +1198,48 @@ input { } } +// .session-onion-path-wrapper { + + .onion-node-list { + display: flex; + flex-direction: column; + margin: $session-margin-sm; + + // .onion-node__vertical-line { + // border-left: 3px solid green; + // left: 50%; + // position: absolute; + // margin-left: -3px; + // top: 0; + // height: 100% + // } + + .onion__node { + display: flex; + flex-grow: 1; + align-items: center; + margin: $session-margin-xs; + + // unsure + position: relative; + + .line { + width: 50%; + height: 100%; + position: absolute; + left: 25%; + top: 40%; + border-left: 3px solid green; + } + + * { + margin: $session-margin-sm; + } + } + } + +// } + .session-nickname-wrapper { position: absolute; height: 100%; diff --git a/ts/components/OnionStatusDialog.tsx b/ts/components/OnionStatusDialog.tsx index 49a670447..83462a248 100644 --- a/ts/components/OnionStatusDialog.tsx +++ b/ts/components/OnionStatusDialog.tsx @@ -6,83 +6,80 @@ import { getPathNodesIPAddresses } from '../session/onions/onionSend'; import { useInterval } from '../hooks/useInterval'; import classNames from 'classnames'; +import _ from 'lodash'; + +import { getTheme } from '../state/selectors/theme'; + import electron from 'electron'; +import { useSelector } from 'react-redux'; +import { StateType } from '../state/reducer'; +import { OnionPathNodeType } from '../state/ducks/onion'; +import { SessionIconButton, SessionIconSize, SessionIconType } from './session/icon'; +import { Constants } from '../session'; const { shell } = electron; +import { SessionWrapperModal } from '../components/session/SessionWrapperModal'; + interface OnionStatusDialogProps { theme: DefaultTheme; nodes?: Array; onClose: any; } -interface IPathNode { +export interface IPathNode { ip?: string; label: string; } -const OnionPath = (props: { nodes: IPathNode[], hasPath: boolean }) => { +export const OnionPath = (props: { nodes: IPathNode[]; hasPath: boolean }) => { const { nodes, hasPath } = props; return ( -
- { - nodes.map((node) => { - return ( -
-
-

- {node.label} - {node.ip && ( - <> -
- {node.ip} - - )} -

-
- ) - }) - } +
+ {nodes.map(node => { + // return OnionPathNode(hasPath, 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 [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) - } + console.log('Current Onion Path - ', onionPathAddresses); + setOnionPathAddresses(onionPathAddresses); + }; const buildOnionPath = () => { // TODO: Add i18n to onion path // Default path values let path = [ { - label: 'You' + label: 'You', }, { ip: 'Connecting...', - label: 'Entry Node' + label: 'Entry Node', }, { ip: 'Connecting...', - label: 'Service Node' + label: 'Service Node', }, { ip: 'Connecting...', - label: 'Service Node' + label: 'Service Node', }, { - label: 'Destination' + label: 'Destination', }, - ] + ]; // FIXME call function to check if an onion path exists setHasPath(onionPathAddresses.length !== 0); @@ -92,32 +89,28 @@ export const OnionStatusDialog = (props: OnionStatusDialogProps) => { onionPathAddresses.forEach((ipAddress, index) => { const pathIndex = index + 1; path[pathIndex].ip = ipAddress; - }) + }); } setPathNodes(path); - } + }; useInterval(() => { - getOnionPathAddresses() - }, 1000) + getOnionPathAddresses(); + }, 1000); useEffect(() => { - buildOnionPath() - }, [onionPathAddresses]) + buildOnionPath(); + }, [onionPathAddresses]); const openFAQPage = () => { - console.log("Opening FAQ Page") + console.log('Opening FAQ Page'); shell.openExternal('https://getsession.org/faq/#onion-routing'); - } + }; return ( - +
-
+

{window.i18n('onionPathIndicatorDescription')}

@@ -131,4 +124,119 @@ export const OnionStatusDialog = (props: OnionStatusDialogProps) => { /> ); -} +}; + +// export const OnionPathNode = (hasPath: boolean, node: IPathNode): JSX.Element => { +// export const OnionPathNode = (hasPath: boolean, node: any): JSX.Element => { +// export const OnionPathNode = (hasPath: boolean, node: any) => { +// export const OnionPathNode = (hasPath: any, node: any) => { +export const OnionPathNode = (props: any) => { + const { hasPath, node } = props; + + const theme = useSelector(getTheme); + console.log('@@@ onionpathnode theme', theme); + + const onionPaths = useSelector((state: StateType) => state.onionPaths); + console.log('@@@ state onion path node', onionPaths); + + // if (!(node && node.label && node.ip)) { + // return
{'Onion' + JSON.stringify(onionPaths)}
; + // } + + // let connectedNodesCounts = onionPaths.nodes.reduce() + let connectedNodesCount = _.sumBy(onionPaths.nodes, (node: OnionPathNodeType) => { + return node.isConnected ? 1 : 0; + }); + + if (true) { + console.log('@@@', connectedNodesCount); + return ( + // +
+ ); + } + + return ( +
+
+

+ {node && node.label ? {node.label} : null} + {node.ip && ( + <> +
+ {node.ip} + + )} +

+
+ ); +}; + +const OnionPathModalInner = (props: any) => { + const onionPaths = useSelector((state: StateType) => state.onionPaths); + + // let connectedNodesCount = _.sumBy(onionPaths.nodes, (node: OnionPathNodeType) => { + // return node.isConnected ? 1 : 0; + // }) + + + return ( +
+ {/*
*/} + {onionPaths.nodes.map((node: OnionPathNodeType, index: number) => { + let nodeStatusColor = node.isConnected + ? Constants.UI.COLORS.GREEN + : node.isAttemptingConnect + ? Constants.UI.COLORS.WARNING + : Constants.UI.COLORS.DANGER; + return ( + <> +
+ {index <= onionPaths.nodes.map.length ? +
+ : + null + } + + {node.ip ? +
country
+ : + null + } +
+ + ); + })} +
+ ); +}; + +export const StatusLight = (props: any) => { + const [showModal, toggleShowModal] = useState(false); + const { isSelected, color } = props; + const theme = useSelector(getTheme); + const onClick = () => { + toggleShowModal(!showModal); + }; + + return ( + <> + + + {showModal ? ( + + + + ) : null} + + ); +}; diff --git a/ts/components/session/ActionsPanel.tsx b/ts/components/session/ActionsPanel.tsx index 488295fe1..416e99988 100644 --- a/ts/components/session/ActionsPanel.tsx +++ b/ts/components/session/ActionsPanel.tsx @@ -36,6 +36,11 @@ 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 { IPathNode, OnionPathNode, StatusLight } from '../OnionStatusDialog'; +import { OnionUpdate, updateOnionPaths, OnionPathNodeType } from '../../state/ducks/onion'; +import { StateType } from '../../state/reducer'; +import _ from 'lodash'; +import { constants } from 'original-fs'; // tslint:disable-next-line: no-import-side-effect no-submodule-imports @@ -73,8 +78,7 @@ const Section = (props: { type: SectionType; avatarPath?: string; hasOnionPath?: } else if (type === SectionType.PathIndicator) { // Show Path Indicator Modal window.showOnionStatusDialog(); - } - else { + } else { dispatch(clearSearch()); dispatch(showLeftPaneSection(type)); } @@ -99,10 +103,12 @@ 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 - console.log("Status Indicator Color", iconColor) + iconColor = hasOnionPath ? Constants.UI.COLORS.GREEN : Constants.UI.COLORS.DANGER; + console.log('Status Indicator Color', iconColor); } + const unreadToShow = type === SectionType.Message ? unreadMessageCount : undefined; + let iconType: SessionIconType; switch (type) { case SectionType.Message: @@ -124,7 +130,28 @@ const Section = (props: { type: SectionType; avatarPath?: string; hasOnionPath?: iconType = SessionIconType.Moon; } - const unreadToShow = type === SectionType.Message ? unreadMessageCount : undefined; + // calculate light status. + // TODO: Refactor this so this logic is reusable elsewhere. + if (type === SectionType.PathIndicator) { + const onionPaths = useSelector((state: StateType) => state.onionPaths); + console.log('@@@ state onion path node', onionPaths); + let connectedNodesCount = _.sumBy(onionPaths.nodes, (node: OnionPathNodeType) => { + return node.isConnected ? 1 : 0; + }); + + console.log('@@@@@ is connected count: ', connectedNodesCount); + + const statusColor = + connectedNodesCount > 2 + ? Constants.UI.COLORS.GREEN + : connectedNodesCount > 1 + ? Constants.UI.COLORS.WARNING + : Constants.UI.COLORS.DANGER; + + console.log('@@@@@ is connected color: ', statusColor); + + return ; + } return ( { } }; - - /** * This function is called only once: on app startup with a logged in user */ @@ -211,7 +236,7 @@ const doAppStartUp = (dispatch: Dispatch) => { export const ActionsPanel = () => { const dispatch = useDispatch(); const [startCleanUpMedia, setStartCleanUpMedia] = useState(false); - const [hasOnionPath, setHasOnionPath] = useState(false) + const [hasOnionPath, setHasOnionPath] = useState(false); const ourPrimaryConversation = useSelector(getOurPrimaryConversation); // this maxi useEffect is called only once: when the component is mounted. @@ -228,11 +253,37 @@ export const ActionsPanel = () => { return () => global.clearTimeout(timeout); }, []); - const getOnionPathIndicator = () => { - const hasOnionPath = getOnionPathStatus(); - console.log("Is Onion Path found -", hasOnionPath) - setHasOnionPath(hasOnionPath) - } + 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)); + + console.log('Is Onion Path found -', hasOnionPath); + setHasOnionPath(hasOnionPath); + }; useInterval(() => { getOnionPathIndicator(); diff --git a/ts/components/session/SessionWrapperModal.tsx b/ts/components/session/SessionWrapperModal.tsx new file mode 100644 index 000000000..94b2d3c1b --- /dev/null +++ b/ts/components/session/SessionWrapperModal.tsx @@ -0,0 +1,164 @@ +import React from 'react'; +import classNames from 'classnames'; + +import { SessionIconButton, SessionIconSize, SessionIconType } from './icon/'; +import { SessionButton, SessionButtonColor, SessionButtonType } from './SessionButton'; +import { DefaultTheme } from 'styled-components'; + +interface Props { + title: string; + onClose: any; + showExitIcon?: boolean; + showHeader?: boolean; + headerReverse?: boolean; + //Maximum of two icons or buttons in header + headerIconButtons?: Array<{ + iconType: SessionIconType; + iconRotation: number; + onClick?: any; + }>; + headerButtons?: Array<{ + buttonType: SessionButtonType; + buttonColor: SessionButtonColor; + text: string; + onClick?: any; + }>; + theme: DefaultTheme; +} + +interface State { + isVisible: boolean; +} + +export const SessionWrapperModal = (props: any) => { + const { onclick, showModal } = props; + + return ( +
+
+
+
+
+ Onion Nodes / Generic Title +
+
+
+
+ {props.children} +
+ Close +
+
+
+
+
+
+ ); +}; + +// 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/session/constants.ts b/ts/session/constants.ts index 0a04440f6..24b32c855 100644 --- a/ts/session/constants.ts +++ b/ts/session/constants.ts @@ -41,6 +41,9 @@ export const UI = { WHITE_PALE: '#AFAFAF', GREEN: '#00F782', + // CAUTION + WARNING: '#FFC02E', + // SEMANTIC COLORS DANGER: '#FF453A', DANGER_ALT: '#FF4538', diff --git a/ts/session/onions/index.ts b/ts/session/onions/index.ts index 2c9918cb8..fa2dc7909 100644 --- a/ts/session/onions/index.ts +++ b/ts/session/onions/index.ts @@ -198,6 +198,8 @@ export class OnionPaths { let guardNodes: Array = []; + console.log('@@@@ guardNodes: ', guardNodes); + // The use of await inside while is intentional: // we only want to repeat if the await fails // eslint-disable-next-line-no-await-in-loop diff --git a/ts/session/onions/onionSend.ts b/ts/session/onions/onionSend.ts index 5fdc558f8..ad47a9460 100644 --- a/ts/session/onions/onionSend.ts +++ b/ts/session/onions/onionSend.ts @@ -133,7 +133,7 @@ export const getOnionPathStatus = () => { export const getPathNodesIPAddresses = () => { let pathNodes: Array = []; - let displayNode: Array = [] + let displayNode: Array = []; try { pathNodes = OnionPaths.getInstance().getOnionPathNoRebuild(); displayNode = pathNodes.map(node => node.ip); diff --git a/ts/state/ducks/onion.tsx b/ts/state/ducks/onion.tsx new file mode 100644 index 000000000..b705b50e1 --- /dev/null +++ b/ts/state/ducks/onion.tsx @@ -0,0 +1,67 @@ +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; +// import { OpenGroupV2InfoJoinable } from '../../opengroup/opengroupV2/ApiUtil'; + +// export type DefaultRoomsState = Array; + +export type OnionState = { + nodes: Array; + // ip?: string; + // label?: string; + // isConnected?: boolean; + // isAttemptingConnect?: boolean; +}; + +const initialState: OnionState = { + nodes: new Array(), +}; + +// const initialState: OnionState = { +// ip: '', +// label: '', +// isConnected: false, +// isAttemptingConnect: false +// }; + +/** + * Payload to dispatch to update the base64 data of a default room + */ +export type Base64Update = { + base64Data: string; + roomId: string; +}; + +/** + * Type for a singular onion node to be used in the onion redux state. + */ +export type OnionPathNodeType = { + ip?: string; + label?: string; + isConnected?: boolean; + isAttemptingConnect?: boolean; +}; + +/** + * Payload to dispatch an update of the onion node paths + */ +export type OnionUpdate = { + nodes: Array; +}; + +/** + * This slice is the one holding the default joinable rooms fetched once in a while from the default opengroup v2 server. + */ +const onionSlice = createSlice({ + name: 'onionPaths', + initialState, + reducers: { + updateOnionPaths(state, action: PayloadAction) { + window.log.warn('updating default rooms', action.payload); + return action.payload as OnionState; + }, + }, +}); + +// destructures +const { actions, reducer } = onionSlice; +export const { updateOnionPaths } = actions; +export const defaultOnionReducer = reducer; diff --git a/ts/state/reducer.ts b/ts/state/reducer.ts index cb99c331d..7aecd3f55 100644 --- a/ts/state/reducer.ts +++ b/ts/state/reducer.ts @@ -7,6 +7,8 @@ import { reducer as theme, ThemeStateType } from './ducks/theme'; import { reducer as section, SectionStateType } from './ducks/section'; import { defaultRoomReducer as defaultRooms, DefaultRoomsState } from './ducks/defaultRooms'; +import { defaultOnionReducer as onionPaths, OnionState } from './ducks/onion'; + export type StateType = { search: SearchStateType; // messages: MessagesStateType; @@ -15,6 +17,8 @@ export type StateType = { theme: ThemeStateType; section: SectionStateType; defaultRooms: DefaultRoomsState; + + onionPaths: OnionState; }; export const reducers = { @@ -27,6 +31,8 @@ export const reducers = { theme, section, defaultRooms, + + onionPaths, }; // Making this work would require that our reducer signature supported AnyAction, not