From b39ae7de419aaaa818a2548a2cbd9c3c881164a6 Mon Sep 17 00:00:00 2001 From: Reckless_Satoshi <90936742+Reckless-Satoshi@users.noreply.github.com> Date: Tue, 21 Feb 2023 17:22:48 +0000 Subject: [PATCH] New Robot page (#357) * Init RobotPage * Add onboarding component and generate token step * Add robot generation step * Add onboarding step 3 * Add Welcome componenent * Minor fixes * Add recovery component * Add robot profile component * Small fixes * Add TOR loading component * Small fixes * Fix tor loading and add highres animations on android * Lint * Fix robot profile order buttons --- frontend/src/App.tsx | 13 +- frontend/src/basic/Main.tsx | 11 +- frontend/src/basic/RobotPage/Onboarding.tsx | 300 +++++++++++++ frontend/src/basic/RobotPage/Recovery.tsx | 84 ++++ frontend/src/basic/RobotPage/RobotProfile.tsx | 203 +++++++++ frontend/src/basic/RobotPage/TokenInput.tsx | 107 +++++ frontend/src/basic/RobotPage/Welcome.tsx | 120 ++++++ frontend/src/basic/RobotPage/index.tsx | 272 ++++++++++++ frontend/src/basic/UserGenPage.js | 395 ------------------ frontend/src/components/Dialogs/Profile.tsx | 2 +- .../src/components/Icons/RoboSatsText.tsx | 2 +- .../src/components/Notifications/index.tsx | 4 +- .../RobotAvatar/generatingCompressed.gif | Bin 0 -> 99543 bytes ...er_Login.gif => generatingPlaceholder.gif} | Bin frontend/src/components/RobotAvatar/index.tsx | 16 +- ...placeholder.gif => loadingPlaceholder.gif} | Bin .../components/RobotAvatar/placeholder.json | 6 +- .../RobotAvatar/placeholder_highres.json | 10 + frontend/src/components/TorConnection.tsx | 25 +- .../TradeBox/Forms/LightningPayout.tsx | 4 +- .../TradeBox/Prompts/Successful.tsx | 3 +- frontend/static/css/index.css | 14 +- frontend/webpack.config.ts | 12 + 23 files changed, 1171 insertions(+), 432 deletions(-) create mode 100644 frontend/src/basic/RobotPage/Onboarding.tsx create mode 100644 frontend/src/basic/RobotPage/Recovery.tsx create mode 100644 frontend/src/basic/RobotPage/RobotProfile.tsx create mode 100644 frontend/src/basic/RobotPage/TokenInput.tsx create mode 100644 frontend/src/basic/RobotPage/Welcome.tsx delete mode 100644 frontend/src/basic/UserGenPage.js create mode 100644 frontend/src/components/RobotAvatar/generatingCompressed.gif rename frontend/src/components/RobotAvatar/{Place_holder_Login.gif => generatingPlaceholder.gif} (100%) rename frontend/src/components/RobotAvatar/{placeholder.gif => loadingPlaceholder.gif} (100%) create mode 100644 frontend/src/components/RobotAvatar/placeholder_highres.json diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 93974b34..0b15fa0c 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -4,7 +4,7 @@ import Main from './basic/Main'; import { CssBaseline } from '@mui/material'; import { ThemeProvider, createTheme, Theme } from '@mui/material/styles'; import UnsafeAlert from './components/UnsafeAlert'; -import TorConnection from './components/TorConnection'; +import TorConnectionBadge from './components/TorConnection'; import { I18nextProvider } from 'react-i18next'; import i18n from './i18n/Web'; @@ -29,6 +29,13 @@ const makeTheme = function (settings: Settings) { const App = (): JSX.Element => { const [theme, setTheme] = useState(makeTheme(new Settings())); const [settings, setSettings] = useState(new Settings()); + const [torStatus, setTorStatus] = useState('NOTINIT'); + + useEffect(() => { + window.addEventListener('torStatus', (event) => { + setTorStatus(event?.detail); + }); + }, []); useEffect(() => { setTheme(makeTheme(settings)); @@ -46,9 +53,9 @@ const App = (): JSX.Element => { {window.NativeRobosats === undefined ? ( ) : ( - + )} -
+
diff --git a/frontend/src/basic/Main.tsx b/frontend/src/basic/Main.tsx index 0e91ef59..f29815e6 100644 --- a/frontend/src/basic/Main.tsx +++ b/frontend/src/basic/Main.tsx @@ -2,7 +2,7 @@ import React, { useEffect, useState } from 'react'; import { HashRouter, BrowserRouter, Switch, Route } from 'react-router-dom'; import { useTheme, Box, Slide, Typography } from '@mui/material'; -import UserGenPage from './UserGenPage'; +import RobotPage from './RobotPage'; import MakerPage from './MakerPage'; import BookPage from './BookPage'; import OrderPage from './OrderPage'; @@ -71,10 +71,11 @@ interface SlideDirection { interface MainProps { settings: Settings; + torStatus: 'NOTINIT' | 'STARTING' | '"Done"' | 'DONE'; setSettings: (state: Settings) => void; } -const Main = ({ settings, setSettings }: MainProps): JSX.Element => { +const Main = ({ torStatus, settings, setSettings }: MainProps): JSX.Element => { const { t } = useTranslation(); const theme = useTheme(); @@ -349,11 +350,11 @@ const Main = ({ settings, setSettings }: MainProps): JSX.Element => { appear={slideDirection.in != undefined} >
- void; + robot: Robot; + setRobot: (state: Robot) => void; + inputToken: string; + setInputToken: (state: string) => void; + getGenerateRobot: (token: string) => void; + badRequest: string | undefined; + setPage: (state: Page) => void; + baseUrl: string; +} + +const Onboarding = ({ + setView, + robot, + inputToken, + setInputToken, + setRobot, + badRequest, + getGenerateRobot, + setPage, + baseUrl, +}: OnboardingProps): JSX.Element => { + const { t } = useTranslation(); + const history = useHistory(); + const theme = useTheme(); + + const [step, setStep] = useState<'1' | '2' | '3'>('1'); + const [generatedToken, setGeneratedToken] = useState(false); + const [showMimickProgress, setShowMimickProgress] = useState(false); + + const generateToken = () => { + setGeneratedToken(true); + setInputToken(genBase62Token(36)); + setShowMimickProgress(true); + setTimeout(() => setShowMimickProgress(false), 1000); + }; + + const changePage = function (newPage: Page) { + setPage(newPage); + history.push(`/${newPage}`); + }; + + return ( + + + + + {t('1. Generate a token')} + + + + + + + {t( + 'This temporary key gives you access to a unique and private robot identity for your trade.', + )} + + + {!generatedToken ? ( + + + + ) : ( + + + + + + {`${t('Store it somewhere safe!')} `} + {t( + `This token is the one and only key to your robot and trade. You will need it later to recover your order or check its status.`, + )} + + + + {showMimickProgress ? ( + + ) : ( + null} + /> + )} + + + + {t('You can also add your own random characters into the token or')} + + + + + + + + + + + )} + + + + + + + + {t('2. Meet your robot identity')} + + + + + + + {robot.avatarLoaded && robot.nickname ? ( + t('This is your trading avatar') + ) : ( + <> + {t('Building your robot!')} + + + )} + + + + + + + + {robot.avatarLoaded && robot.nickname ? ( + + {t('Hi! My name is')} + +
+ + {robot.nickname} + +
+
+
+ ) : null} + + + + + +
+
+
+ + + + + {t('3. Browse or create an order')} + + + + + + + {t( + 'RoboSats is a peer-to-peer marketplace. You can browse the public offers or create a new one.', + )} + + + + + + + + + + + + + {`${t('If you need help on your RoboSats journey join our public support')} `} + + {t('Telegram group')} + + {`, ${t('or visit the robot school for documentation.')} `} + + + + + + + + + + + +
+ ); +}; + +export default Onboarding; diff --git a/frontend/src/basic/RobotPage/Recovery.tsx b/frontend/src/basic/RobotPage/Recovery.tsx new file mode 100644 index 00000000..c4560dd4 --- /dev/null +++ b/frontend/src/basic/RobotPage/Recovery.tsx @@ -0,0 +1,84 @@ +import React, { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Button, Collapse, Grid, Typography, useTheme } from '@mui/material'; +import { useParams } from 'react-router-dom'; + +import { Page } from '../NavBar'; +import { Robot } from '../../models'; +import { Casino, Download, ContentCopy, SmartToy, Bolt } from '@mui/icons-material'; +import RobotAvatar from '../../components/RobotAvatar'; +import TokenInput from './TokenInput'; +import Key from '@mui/icons-material/Key'; + +interface RecoveryProps { + robot: Robot; + setRobot: (state: Robot) => void; + setView: (state: 'welcome' | 'onboarding' | 'recovery' | 'profile') => void; + inputToken: string; + setInputToken: (state: string) => void; + getGenerateRobot: (token: string) => void; + setPage: (state: Page) => void; + baseUrl: string; +} + +const Recovery = ({ + robot, + setRobot, + inputToken, + setView, + setInputToken, + getGenerateRobot, + setPage, + baseUrl, +}: RecoveryProps): JSX.Element => { + const { t } = useTranslation(); + const theme = useTheme(); + + const recoveryDisabled = () => { + return !(inputToken.length > 20); + }; + const onClickRecover = () => { + if (recoveryDisabled()) { + } else { + getGenerateRobot(inputToken); + setView('profile'); + } + }; + + return ( + + + + {t( + 'Please, introduce your robot token to re-build your robot and gain access to its trades.', + )} + + + + + + + + + + ); +}; + +export default Recovery; diff --git a/frontend/src/basic/RobotPage/RobotProfile.tsx b/frontend/src/basic/RobotPage/RobotProfile.tsx new file mode 100644 index 00000000..38d984ef --- /dev/null +++ b/frontend/src/basic/RobotPage/RobotProfile.tsx @@ -0,0 +1,203 @@ +import React, { useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useHistory } from 'react-router-dom'; +import { Button, Link, Grid, LinearProgress, Typography, Alert } from '@mui/material'; +import { Bolt, Logout, Refresh } from '@mui/icons-material'; +import RobotAvatar from '../../components/RobotAvatar'; +import TokenInput from './TokenInput'; +import { Page } from '../NavBar'; +import { Robot } from '../../models'; +import { genBase62Token } from '../../utils'; + +interface RobotProfileProps { + robot: Robot; + setRobot: (state: Robot) => void; + setView: (state: 'welcome' | 'onboarding' | 'recovery' | 'profile') => void; + inputToken: string; + setCurrentOrder: (state: number) => void; + logoutRobot: () => void; + setInputToken: (state: string) => void; + getGenerateRobot: (token: string) => void; + setPage: (state: Page) => void; + baseUrl: string; + badRequest: string; + robotFound: boolean; + width: number; +} + +const RobotProfile = ({ + robot, + setRobot, + inputToken, + setInputToken, + setCurrentOrder, + getGenerateRobot, + logoutRobot, + setPage, + setView, + badRequest, + baseUrl, + robotFound, + width, +}: RobotProfileProps): JSX.Element => { + const { t } = useTranslation(); + const history = useHistory(); + + return ( + + + {robot.avatarLoaded && robot.nickname ? ( + +
+ {width < 19 ? null : ( + + )} + {robot.nickname} + {width < 19 ? null : ( + + )} +
+
+ ) : ( + <> + {t('Building your robot!')} + + + )} +
+ + + + + + {/* {robotFound ? ( + + + {t('Welcome back!')} + + + ) : ( + <> + )} */} + + {robot.activeOrderId ? ( + + + + ) : null} + + {robot.lastOrderId ? ( + + + + + + + + + {t( + 'Reusing trading identity degrades your privacy against other users, coordinators and observers.', + )} + + + + + + + + + ) : null} + + + null} + /> + + + + + +
+ ); +}; + +export default RobotProfile; diff --git a/frontend/src/basic/RobotPage/TokenInput.tsx b/frontend/src/basic/RobotPage/TokenInput.tsx new file mode 100644 index 00000000..3a1d4275 --- /dev/null +++ b/frontend/src/basic/RobotPage/TokenInput.tsx @@ -0,0 +1,107 @@ +import React, { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { IconButton, TextField, Tooltip, useTheme } from '@mui/material'; +import { Robot } from '../../models'; +import { Download, ContentCopy } from '@mui/icons-material'; +import { systemClient } from '../../services/System'; +import { saveAsJson } from '../../utils'; + +interface TokenInputProps { + robot: Robot; + editable?: boolean; + showDownload?: boolean; + fullWidth?: boolean; + setRobot: (state: Robot) => void; + inputToken: string; + autoFocusTarget?: 'textfield' | 'copyButton' | 'none'; + onPressEnter: () => void; + badRequest: string | undefined; + setInputToken: (state: string) => void; + showCopy?: boolean; + label?: string; +} + +const TokenInput = ({ + robot, + editable = true, + showCopy = true, + label, + setRobot, + showDownload = false, + fullWidth = true, + onPressEnter, + autoFocusTarget = 'textfield', + inputToken, + badRequest, + setInputToken, +}: TokenInputProps): JSX.Element => { + const { t } = useTranslation(); + const [showCopied, setShowCopied] = useState(false); + + useEffect(() => { + setShowCopied(false); + }, [inputToken]); + + const createJsonFile = () => { + return { + token: robot.token, + token_shannon_entropy: robot.shannonEntropy, + token_bit_entropy: robot.bitsEntropy, + public_key: robot.pubKey, + encrypted_private_key: robot.encPrivKey, + }; + }; + + return ( + setInputToken(e.target.value)} + onKeyPress={(e) => { + if (e.key === 'Enter') { + onPressEnter(); + } + }} + InputProps={{ + startAdornment: showDownload ? ( + + saveAsJson(robot.nickname + '.json', createJsonFile())} + > + + + + ) : null, + endAdornment: showCopy ? ( + + { + systemClient.copyToClipboard(inputToken); + setShowCopied(true); + setTimeout(() => setShowCopied(false), 1000); + setRobot({ ...robot, copiedToken: true }); + }} + > + + + + ) : null, + }} + /> + ); +}; + +export default TokenInput; diff --git a/frontend/src/basic/RobotPage/Welcome.tsx b/frontend/src/basic/RobotPage/Welcome.tsx new file mode 100644 index 00000000..1d9ff1e0 --- /dev/null +++ b/frontend/src/basic/RobotPage/Welcome.tsx @@ -0,0 +1,120 @@ +import React, { useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Box, Button, Grid, Typography, useTheme } from '@mui/material'; +import { RoboSatsTextIcon } from '../../components/Icons'; +import { FastForward, RocketLaunch, Key } from '@mui/icons-material'; +import { genBase62Token } from '../../utils'; + +interface WelcomeProps { + setView: (state: 'welcome' | 'onboarding' | 'recovery' | 'profile') => void; + getGenerateRobot: (token: string) => void; + width: number; +} + +const Welcome = ({ setView, width, getGenerateRobot }: WelcomeProps): JSX.Element => { + const { t } = useTranslation(); + const theme = useTheme(); + + return ( + + + + + + + + + + + {t('A Simple and Private LN P2P Exchange')} + + + + + + + + + {t('Create a new robot and learn to use RoboSats')} + + + + + + + + + {t('Recover an existing robot using your token')} + + + + + + + + + + + + + ); +}; + +export default Welcome; diff --git a/frontend/src/basic/RobotPage/index.tsx b/frontend/src/basic/RobotPage/index.tsx index e69de29b..56398dd8 100644 --- a/frontend/src/basic/RobotPage/index.tsx +++ b/frontend/src/basic/RobotPage/index.tsx @@ -0,0 +1,272 @@ +import React, { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { + Paper, + Grid, + CircularProgress, + Box, + Alert, + Typography, + useTheme, + AlertTitle, +} from '@mui/material'; +import { useParams } from 'react-router-dom'; + +import { Page } from '../NavBar'; +import { Robot } from '../../models'; +import { tokenStrength } from '../../utils'; +import { systemClient } from '../../services/System'; +import { apiClient } from '../../services/api'; +import { genKey } from '../../pgp'; +import { sha256 } from 'js-sha256'; +import Onboarding from './Onboarding'; +import Welcome from './Welcome'; +import RobotProfile from './RobotProfile'; +import Recovery from './Recovery'; +import { TorIcon } from '../../components/Icons'; + +interface RobotPageProps { + setPage: (state: Page) => void; + setCurrentOrder: (state: number) => void; + torStatus: 'NOTINIT' | 'STARTING' | '"Done"' | 'DONE'; + robot: Robot; + setRobot: (state: Robot) => void; + windowSize: { width: number; height: number }; + baseUrl: string; +} + +const RobotPage = ({ + setPage, + setCurrentOrder, + torStatus, + windowSize, + robot, + setRobot, + baseUrl, +}: RobotPageProps): JSX.Element => { + const { t } = useTranslation(); + const params = useParams(); + const refCode = params.refCode; + const width = Math.min(windowSize.width * 0.8, 28); + const maxHeight = windowSize.height * 0.85 - 3; + const theme = useTheme(); + + const [robotFound, setRobotFound] = useState(false); + const [badRequest, setBadRequest] = useState(undefined); + const [inputToken, setInputToken] = useState(''); + const [view, setView] = useState<'welcome' | 'onboarding' | 'recovery' | 'profile'>( + robot.token ? 'profile' : 'welcome', + ); + + useEffect(() => { + if (robot.token) { + setInputToken(robot.token); + } + if (robot.nickname == null && robot.token) { + getGenerateRobot(robot.token); + } + }, []); + + const getGenerateRobot = (token: string) => { + const strength = tokenStrength(token); + setRobot({ ...robot, loading: true, avatarLoaded: false }); + setInputToken(token); + + const requestBody = genKey(token).then(function (key) { + return { + token_sha256: sha256(token), + public_key: key.publicKeyArmored, + encrypted_private_key: key.encryptedPrivateKeyArmored, + unique_values: strength.uniqueValues, + counts: strength.counts, + length: token.length, + ref_code: refCode, + }; + }); + + requestBody.then( + async (body) => + await apiClient.post(baseUrl, '/api/user/', body).then((data: any) => { + setRobotFound(data?.found); + setBadRequest(data?.bad_request); + setCurrentOrder( + data.active_order_id + ? data.active_order_id + : data.last_order_id + ? data.last_order_id + : null, + ); + // Add nick and token to App state (token only if not a bad request) + data.bad_request + ? setRobot({ + ...robot, + avatarLoaded: true, + loading: false, + nickname: data.nickname ?? robot.nickname, + activeOrderId: data.active_order_id ?? null, + referralCode: data.referral_code ?? robot.referralCode, + earnedRewards: data.earned_rewards ?? robot.earnedRewards, + lastOrderId: data.last_order_id ?? robot.lastOrderId, + stealthInvoices: data.wants_stealth ?? robot.stealthInvoices, + tgEnabled: data.tg_enabled, + tgBotName: data.tg_bot_name, + tgToken: data.tg_token, + }) + : setRobot({ + ...robot, + nickname: data.nickname, + token, + loading: false, + activeOrderId: data.active_order_id ?? null, + lastOrderId: data.last_order_id ?? null, + referralCode: data.referral_code, + earnedRewards: data.earned_rewards ?? 0, + stealthInvoices: data.wants_stealth, + tgEnabled: data.tg_enabled, + tgBotName: data.tg_bot_name, + tgToken: data.tg_token, + bitsEntropy: data.token_bits_entropy, + shannonEntropy: data.token_shannon_entropy, + pubKey: data.public_key, + encPrivKey: data.encrypted_private_key, + copiedToken: data.found ? true : robot.copiedToken, + }) & + systemClient.setItem('robot_token', token) & + systemClient.setItem('pub_key', data.public_key.split('\n').join('\\')) & + systemClient.setItem( + 'enc_priv_key', + data.encrypted_private_key.split('\n').join('\\'), + ); + }), + ); + }; + + const deleteRobot = () => { + apiClient.delete(baseUrl, '/api/user'); + logoutRobot(); + }; + + const logoutRobot = () => { + setInputToken(''); + setRobotFound(false); + systemClient.deleteCookie('sessionid'); + systemClient.deleteItem('robot_token'); + systemClient.deleteItem('pub_key'); + systemClient.deleteItem('enc_priv_key'); + setTimeout(() => setRobot(new Robot()), 10); + }; + + if (!(window.NativeRobosats === undefined) && !(torStatus == 'DONE' || torStatus == '"Done"')) { + return ( + + + + + {t('Connecting to TOR')} + + + + + + + + + + + + + + + + + + + {t('Connection encrypted and anonymized using TOR.')} + {t( + 'This ensures maximum privacy, however you might feel the app behaves slow. If connection is lost, restart the app.', + )} + + + + + ); + } else { + return ( + + {view === 'welcome' ? ( + + ) : null} + + {view === 'onboarding' ? ( + + ) : null} + + {view === 'profile' ? ( + + ) : null} + + {view === 'recovery' ? ( + + ) : null} + + ); + } +}; + +export default RobotPage; diff --git a/frontend/src/basic/UserGenPage.js b/frontend/src/basic/UserGenPage.js deleted file mode 100644 index 6a10f91a..00000000 --- a/frontend/src/basic/UserGenPage.js +++ /dev/null @@ -1,395 +0,0 @@ -import React, { Component } from 'react'; -import { withTranslation } from 'react-i18next'; -import { - Button, - Tooltip, - Grid, - Typography, - TextField, - ButtonGroup, - CircularProgress, - IconButton, -} from '@mui/material'; - -import SmartToyIcon from '@mui/icons-material/SmartToy'; -import CasinoIcon from '@mui/icons-material/Casino'; -import ContentCopy from '@mui/icons-material/ContentCopy'; -import BoltIcon from '@mui/icons-material/Bolt'; -import DownloadIcon from '@mui/icons-material/Download'; -import { RoboSatsNoTextIcon } from '../components/Icons'; - -import { sha256 } from 'js-sha256'; -import { genBase62Token, tokenStrength, saveAsJson } from '../utils'; -import { genKey } from '../pgp'; -import { systemClient } from '../services/System'; -import { apiClient } from '../services/api/index'; -import RobotAvatar from '../components/RobotAvatar'; - -class UserGenPage extends Component { - constructor(props) { - super(props); - this.state = { - tokenHasChanged: false, - inputToken: '', - found: false, - }; - - this.refCode = this.props.match.params.refCode; - } - - componentDidMount() { - // Checks in parent HomePage if there is already a nick and token - // Displays the existing one - if (this.props.robot.nickname != null) { - this.setState({ inputToken: this.props.robot.token }); - } else if (this.props.robot.token) { - this.setState({ inputToken: this.props.robot.token }); - this.getGeneratedUser(this.props.robot.token); - } else { - const newToken = genBase62Token(36); - this.setState({ - inputToken: newToken, - }); - this.getGeneratedUser(newToken); - } - } - - getGeneratedUser = (token) => { - const strength = tokenStrength(token); - const refCode = this.refCode; - this.props.setRobot({ ...this.props.robot, loading: true, avatarLoaded: false }); - - const requestBody = genKey(token).then(function (key) { - return { - token_sha256: sha256(token), - public_key: key.publicKeyArmored, - encrypted_private_key: key.encryptedPrivateKeyArmored, - unique_values: strength.uniqueValues, - counts: strength.counts, - length: token.length, - ref_code: refCode, - }; - }); - requestBody.then((body) => - apiClient.post(this.props.baseUrl, '/api/user/', body).then((data) => { - this.setState({ found: data.found, bad_request: data.bad_request }); - this.props.setCurrentOrder( - data.active_order_id - ? data.active_order_id - : data.last_order_id - ? data.last_order_id - : null, - ); - // Add nick and token to App state (token only if not a bad request) - data.bad_request - ? this.props.setRobot({ - ...this.props.robot, - avatarLoaded: true, - loading: false, - nickname: data.nickname ?? this.props.robot.nickname, - activeOrderId: data.active_order_id ?? null, - referralCode: data.referral_code ?? this.props.referralCode, - earnedRewards: data.earned_rewards ?? this.props.earnedRewards, - lastOrderId: data.last_order_id ?? this.props.lastOrderId, - stealthInvoices: data.wants_stealth ?? this.props.stealthInvoices, - tgEnabled: data.tg_enabled, - tgBotName: data.tg_bot_name, - tgToken: data.tg_token, - }) - : this.props.setRobot({ - ...this.props.robot, - nickname: data.nickname, - token, - loading: false, - activeOrderId: data.active_order_id ? data.active_order_id : null, - lastOrderId: data.last_order_id ? data.last_order_id : null, - referralCode: data.referral_code, - earnedRewards: data.earned_rewards ?? 0, - stealthInvoices: data.wants_stealth, - tgEnabled: data.tg_enabled, - tgBotName: data.tg_bot_name, - tgToken: data.tg_token, - bitsEntropy: data.token_bits_entropy, - shannonEntropy: data.token_shannon_entropy, - pubKey: data.public_key, - encPrivKey: data.encrypted_private_key, - copiedToken: data.found ? true : this.props.robot.copiedToken, - }) & - systemClient.setItem('robot_token', token) & - systemClient.setItem('pub_key', data.public_key.split('\n').join('\\')) & - systemClient.setItem('enc_priv_key', data.encrypted_private_key.split('\n').join('\\')); - }), - ); - }; - - delGeneratedUser() { - apiClient.delete(this.props.baseUrl, '/api/user'); - - systemClient.deleteCookie('sessionid'); - systemClient.deleteItem('robot_token'); - systemClient.deleteItem('pub_key'); - systemClient.deleteItem('enc_priv_key'); - } - - handleClickNewRandomToken = () => { - const inputToken = genBase62Token(36); - this.setState({ - inputToken, - tokenHasChanged: true, - }); - this.props.setRobot({ ...this.props.robot, copiedToken: true }); - }; - - handleChangeToken = (e) => { - this.setState({ - inputToken: e.target.value.split(' ').join(''), - tokenHasChanged: true, - }); - }; - - handleClickSubmitToken = () => { - this.delGeneratedUser(); - this.getGeneratedUser(this.state.inputToken); - this.setState({ tokenHasChanged: false }); - this.props.setRobot({ - ...this.props.robot, - avatarLoaded: false, - nickname: null, - token: null, - copiedToken: false, - lastOrderId: null, - activeOrderId: null, - }); - }; - - createJsonFile = () => { - return { - token: this.props.robot.token, - token_shannon_entropy: this.props.robot.shannonEntropy, - token_bit_entropy: this.props.robot.bitsEntropy, - public_key: this.props.robot.pub_key, - encrypted_private_key: this.props.robot.enc_priv_key, - }; - }; - - render() { - const { t, i18n } = this.props; - const fontSize = this.props.theme.typography.fontSize; - const fontSizeFactor = fontSize / 14; // to scale sizes, default fontSize is 14 - return ( - - -
- - - {this.props.robot.avatarLoaded && this.props.robot.nickname ? ( -
- - - - {this.props.robot.nickname && systemClient.getCookie('sessionid') ? ( - - ) : ( - '' - )} - - - - - -
-
-
- ) : ( - - )} -
- {this.state.found ? ( - - - {this.state.found ? t('A robot avatar was found, welcome back!') : null} -
-
-
- ) : ( - '' - )} - - - { - if (e.key === 'Enter') { - this.handleClickSubmitToken(); - } - }} - InputProps={{ - startAdornment: ( -
- - - - - - saveAsJson( - this.props.robot.nickname + '.json', - this.createJsonFile(), - ) - } - > - - - - - - - - - systemClient.copyToClipboard(systemClient.getItem('robot_token')) & - this.props.setRobot({ ...this.props.robot, copiedToken: true }) - } - > - - - - - -
- ), - endAdornment: ( - - - - - - ), - }} - /> -
-
- - {this.state.tokenHasChanged ? ( - - ) : ( - -
- -
-
- )} -
- - - -
- -
- - - - - {t('Simple and Private LN P2P Exchange')} - - - - - - -
- - - ); - } -} - -export default withTranslation()(UserGenPage); diff --git a/frontend/src/components/Dialogs/Profile.tsx b/frontend/src/components/Dialogs/Profile.tsx index 244d0d97..b242c004 100644 --- a/frontend/src/components/Dialogs/Profile.tsx +++ b/frontend/src/components/Dialogs/Profile.tsx @@ -147,7 +147,7 @@ const ProfileDialog = ({
- {t('Your Profile')} + {t('Your Robot')} diff --git a/frontend/src/components/Icons/RoboSatsText.tsx b/frontend/src/components/Icons/RoboSatsText.tsx index 41256a99..6e9a686f 100644 --- a/frontend/src/components/Icons/RoboSatsText.tsx +++ b/frontend/src/components/Icons/RoboSatsText.tsx @@ -3,7 +3,7 @@ import { SvgIcon } from '@mui/material'; export default function RoboSatsTextIcon(props) { return ( - + = ({ tooltipPosition = 'right', smooth = false, flipHorizontally = false, + placeholderType = 'loading', style = {}, avatarClass = 'flippedSmallAvatar', imageStyle = {}, @@ -39,6 +41,16 @@ const RobotAvatar: React.FC = ({ const theme = useTheme(); const [avatarSrc, setAvatarSrc] = useState(); + const backgroundData = + placeholderType == 'generating' ? placeholder.generating : placeholder.loading; + const backgroundImage = `url(data:${backgroundData.mime};base64,${backgroundData.data})`; + const className = + placeholderType == 'loading' + ? theme.palette.mode === 'dark' + ? 'loadingAvatarDark' + : 'loadingAvatar' + : 'generatingAvatar'; + useEffect(() => { if (nickname != undefined) { if (window.NativeRobosats === undefined) { @@ -76,10 +88,10 @@ const RobotAvatar: React.FC = ({ transform: flipHorizontally ? 'scaleX(-1)' : '', border: '0.3px solid #55555', filter: 'dropShadow(0.5px 0.5px 0.5px #000000)', - backgroundImage: `url(data:${placeholder.image.mime};base64,${placeholder.image.data})`, + backgroundImage, }} > -
+
{ - window.addEventListener('torStatus', (event) => { - setTorStatus(event?.detail); - }); - }, []); +const TorConnectionBadge = ({ torStatus }: TorConnectionBadgeProps): JSX.Element => { + const { t } = useTranslation(); if (window?.NativeRobosats == null) { return <>; @@ -95,4 +96,4 @@ const TorConnection = (): JSX.Element => { } }; -export default TorConnection; +export default TorConnectionBadge; diff --git a/frontend/src/components/TradeBox/Forms/LightningPayout.tsx b/frontend/src/components/TradeBox/Forms/LightningPayout.tsx index 0c8f4dd6..984b8aa4 100644 --- a/frontend/src/components/TradeBox/Forms/LightningPayout.tsx +++ b/frontend/src/components/TradeBox/Forms/LightningPayout.tsx @@ -102,7 +102,7 @@ export const LightningPayoutForm = ({ const validateInvoice = function (invoice: string, targetAmount: number) { try { const decoded = decode(invoice); - const invoiceAmount = Math.floor(decoded['sections'][2]['value'] / 1000); + const invoiceAmount = Math.floor(decoded.sections[2].value / 1000); if (targetAmount != invoiceAmount) { return 'Invalid invoice amount'; } else { @@ -571,7 +571,7 @@ export const LightningPayoutForm = ({ style: { textAlign: 'center', maxHeight: '8em' }, }} variant={lightning.useLnproxy ? 'filled' : 'standard'} - multiline={lightning.useLnproxy ? false : true} + multiline={!lightning.useLnproxy} minRows={3} maxRows={5} onChange={(e) => setLightning({ ...lightning, invoice: e.target.value ?? '' })} diff --git a/frontend/src/components/TradeBox/Prompts/Successful.tsx b/frontend/src/components/TradeBox/Prompts/Successful.tsx index 89473134..b6c077f4 100644 --- a/frontend/src/components/TradeBox/Prompts/Successful.tsx +++ b/frontend/src/components/TradeBox/Prompts/Successful.tsx @@ -1,5 +1,5 @@ import React, { useState } from 'react'; -import { useTranslation } from 'react-i18next'; +import { useTranslation, Trans } from 'react-i18next'; import { Grid, Typography, @@ -16,7 +16,6 @@ import currencies from '../../../../static/assets/currencies.json'; import TradeSummary from '../TradeSummary'; import { Favorite, RocketLaunch, ContentCopy, Refresh } from '@mui/icons-material'; import { LoadingButton } from '@mui/lab'; -import { Trans } from 'react-i18next'; import { Order } from '../../../models'; import { systemClient } from '../../../services/System'; diff --git a/frontend/static/css/index.css b/frontend/static/css/index.css index 3aca9168..276de7ee 100644 --- a/frontend/static/css/index.css +++ b/frontend/static/css/index.css @@ -56,12 +56,6 @@ body { position: fixed; } -.clickTrough { - height: 50px; - pointer-events: none; - z-index: 1; -} - /* No arrows on numeric inputs */ input::-webkit-outer-spin-button, input::-webkit-inner-spin-button { @@ -228,3 +222,11 @@ input[type='number'] { background-repeat: repeat; animation: animatedBackground 5s linear infinite; } + +.generatingAvatar { + background-size: 100%; + border-radius: 50%; + outline: 2px solid #555; + outline-offset: -2px; + filter: dropShadow(1px 1px 1px #000000); +} diff --git a/frontend/webpack.config.ts b/frontend/webpack.config.ts index ff12a85c..b18fecd8 100644 --- a/frontend/webpack.config.ts +++ b/frontend/webpack.config.ts @@ -46,6 +46,18 @@ const configMobile: Configuration = { async: true, }, }, + { + test: path.resolve(__dirname, 'src/components/RobotAvatar/placeholder.json'), + loader: 'file-replace-loader', + options: { + condition: 'if-replacement-exists', + replacement: path.resolve( + __dirname, + 'src/components/RobotAvatar/placeholder_highres.json', + ), + async: true, + }, + }, ], }, plugins: [