Add App context (#369)

* Refactor fetchRobot

* Add appcontext

* Robot recovery fixes

* Add usecontext on maker and settings forms

* Add usecontext to booktable

* Add useContext to order page and main dialogs

* Small fixes
This commit is contained in:
Reckless_Satoshi 2023-02-24 19:17:13 +00:00 committed by GitHub
parent d815fb8a8e
commit d88c2a5eff
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
45 changed files with 825 additions and 1016 deletions

View File

@ -1,5 +1,8 @@
## What does this PR do?
Fixes #<PR_NUMBER/>
Fixes #<ISSUE_NUMBER>
This PR introduces/refactors/...
## Checklist before merging
- [ ] Make sure you have installed and initialized [pre-commit](https://pre-commit.com). `pip install pre-commit` and `pre-commit install`
- [ ] If it's a frontend feature, I have ran prettier `cd frontend; npm run format`. If it's a mobile app feature I ran `cd mobile; npm run format`.
- [ ] If I added new phrases to the user interface, I have ran prettier `cd frontend/static/locales; python collect_phrases.py` to collect them for translation.

View File

@ -21,17 +21,15 @@ repos:
- id: check-docstring-first
- repo: local
hooks:
- id: collect-phrases
name: collect-phrases
stages:
- commit
- merge-commit
language: system
files: ^frontend/src/
types_or: [javascript, jsx, ts, tsx] # uses https://github.com/pre-commit/identify
entry: bash -c 'cd frontend/static/locales && python3 collect_phrases.py'
- repo: local
hooks:
# - id: collect-phrases
# name: Collect i18n phrases
# stages:
# - commit
# - merge-commit
# language: system
# files: ^frontend/
# types_or: [javascript, jsx, ts, tsx] # uses https://github.com/pre-commit/identify
# entry: bash -c 'cd frontend/static/locales && python3 collect_phrases.py'
- id: prettier-frontend
name: prettier-frontend
stages:

View File

@ -759,9 +759,8 @@ class UserView(APIView):
if last_order:
context["last_order_id"] = last_order.id
# Sends the welcome back message, only if created +3 mins ago
if request.user.date_joined < (timezone.now() - timedelta(minutes=3)):
context["found"] = "We found your Robot avatar. Welcome back!"
# Sends the welcome back message.
context["found"] = "We found your Robot avatar. Welcome back!"
return Response(context, status=status.HTTP_202_ACCEPTED)
else:
# It is unlikely, but maybe the nickname is taken (1 in 20 Billion chance)

View File

@ -2,6 +2,7 @@ import React, { Suspense, useState, useEffect } from 'react';
import ReactDOM from 'react-dom/client';
import Main from './basic/Main';
import { CssBaseline } from '@mui/material';
import { AppContextProvider } from './contexts/AppContext';
import { ThemeProvider, createTheme, Theme } from '@mui/material/styles';
import UnsafeAlert from './components/UnsafeAlert';
import TorConnectionBadge from './components/TorConnection';
@ -29,14 +30,6 @@ const makeTheme = function (settings: Settings) {
const App = (): JSX.Element => {
const [theme, setTheme] = useState<Theme>(makeTheme(new Settings()));
const [settings, setSettings] = useState<Settings>(new Settings());
const [torStatus, setTorStatus] = useState<string>('NOTINIT');
useEffect(() => {
window.addEventListener('torStatus', (event) => {
// UX improv: delay the "Conencted" status by 10 secs to avoid long waits for first requests
setTimeout(() => setTorStatus(event?.detail), event?.detail === '"Done"' ? 10000 : 0);
});
}, []);
useEffect(() => {
setTheme(makeTheme(settings));
@ -50,13 +43,15 @@ const App = (): JSX.Element => {
<Suspense fallback='loading language'>
<I18nextProvider i18n={i18n}>
<ThemeProvider theme={theme}>
<CssBaseline />
{window.NativeRobosats === undefined ? (
<UnsafeAlert settings={settings} setSettings={setSettings} />
) : (
<TorConnectionBadge torStatus={torStatus} />
)}
<Main settings={settings} setSettings={setSettings} torStatus={torStatus} />
<AppContextProvider settings={settings} setSettings={setSettings}>
<CssBaseline />
{window.NativeRobosats === undefined ? (
<UnsafeAlert settings={settings} setSettings={setSettings} />
) : (
<TorConnectionBadge />
)}
<Main />
</AppContextProvider>
</ThemeProvider>
</I18nextProvider>
</Suspense>

View File

@ -147,56 +147,36 @@ const BookPage = ({
>
<Grid item>
<BookTable
clickRefresh={() => fetchBook()}
book={book}
fav={fav}
setFav={setFav}
maxWidth={maxBookTableWidth} // EM units
maxHeight={windowSize.height * 0.825 - 5} // EM units
fullWidth={windowSize.width} // EM units
fullHeight={windowSize.height} // EM units
defaultFullscreen={false}
onOrderClicked={onOrderClicked}
baseUrl={baseUrl}
/>
</Grid>
<Grid item>
<DepthChart
orders={book.orders}
lastDayPremium={lastDayPremium}
currency={fav.currency}
limits={limits.list}
maxWidth={chartWidthEm} // EM units
maxHeight={windowSize.height * 0.825 - 5} // EM units
onOrderClicked={onOrderClicked}
baseUrl={baseUrl}
/>
</Grid>
</Grid>
) : view === 'depth' ? (
<DepthChart
orders={book.orders}
lastDayPremium={lastDayPremium}
currency={fav.currency}
limits={limits.list}
maxWidth={windowSize.width * 0.8} // EM units
maxHeight={windowSize.height * 0.825 - 5} // EM units
onOrderClicked={onOrderClicked}
baseUrl={baseUrl}
/>
) : (
<BookTable
book={book}
clickRefresh={() => fetchBook()}
fav={fav}
setFav={setFav}
maxWidth={windowSize.width * 0.97} // EM units
maxHeight={windowSize.height * 0.825 - 5} // EM units
fullWidth={windowSize.width} // EM units
fullHeight={windowSize.height} // EM units
defaultFullscreen={false}
onOrderClicked={onOrderClicked}
baseUrl={baseUrl}
/>
)}
</Grid>

View File

@ -1,312 +1,63 @@
import React, { useEffect, useState } from 'react';
import React, { useContext } from 'react';
import { HashRouter, BrowserRouter, Switch, Route } from 'react-router-dom';
import { useTheme, Box, Slide, Typography } from '@mui/material';
import { Box, Slide, Typography } from '@mui/material';
import RobotPage from './RobotPage';
import MakerPage from './MakerPage';
import BookPage from './BookPage';
import OrderPage from './OrderPage';
import SettingsPage from './SettingsPage';
import NavBar, { Page } from './NavBar';
import MainDialogs, { OpenDialogs } from './MainDialogs';
import NavBar from './NavBar';
import MainDialogs from './MainDialogs';
import RobotAvatar from '../components/RobotAvatar';
import {
Book,
LimitList,
Maker,
Robot,
Info,
Settings,
Favorites,
defaultMaker,
defaultInfo,
Coordinator,
Order,
} from '../models';
import { apiClient } from '../services/api';
import { checkVer, getHost } from '../utils';
import { sha256 } from 'js-sha256';
import defaultCoordinators from '../../static/federation.json';
import { useTranslation } from 'react-i18next';
import Notifications from '../components/Notifications';
import { AppContextProps, AppContext } from '../contexts/AppContext';
const getWindowSize = function (fontSize: number) {
// returns window size in EM units
return {
width: window.innerWidth / fontSize,
height: window.innerHeight / fontSize,
};
};
// Refresh delays (ms) according to Order status
const statusToDelay = [
3000, // 'Waiting for maker bond'
35000, // 'Public'
180000, // 'Paused'
3000, // 'Waiting for taker bond'
999999, // 'Cancelled'
999999, // 'Expired'
8000, // 'Waiting for trade collateral and buyer invoice'
8000, // 'Waiting only for seller trade collateral'
8000, // 'Waiting only for buyer invoice'
10000, // 'Sending fiat - In chatroom'
10000, // 'Fiat sent - In chatroom'
100000, // 'In dispute'
999999, // 'Collaboratively cancelled'
10000, // 'Sending satoshis to buyer'
999999, // 'Sucessful trade'
30000, // 'Failed lightning network routing'
300000, // 'Wait for dispute resolution'
300000, // 'Maker lost dispute'
300000, // 'Taker lost dispute'
];
interface SlideDirection {
in: 'left' | 'right' | undefined;
out: 'left' | 'right' | undefined;
}
interface MainProps {
settings: Settings;
torStatus: 'NOTINIT' | 'STARTING' | '"Done"' | 'DONE';
setSettings: (state: Settings) => void;
}
const Main = ({ torStatus, settings, setSettings }: MainProps): JSX.Element => {
const Main = (): JSX.Element => {
const { t } = useTranslation();
const theme = useTheme();
// All app data structured
const [book, setBook] = useState<Book>({ orders: [], loading: true });
const [limits, setLimits] = useState<{ list: LimitList; loading: boolean }>({
list: [],
loading: true,
});
const [robot, setRobot] = useState<Robot>(new Robot());
const [maker, setMaker] = useState<Maker>(defaultMaker);
const [info, setInfo] = useState<Info>(defaultInfo);
const [coordinators, setCoordinators] = useState<Coordinator[]>(defaultCoordinators);
const [baseUrl, setBaseUrl] = useState<string>('');
const [fav, setFav] = useState<Favorites>({ type: null, mode: 'fiat', currency: 0 });
const [delay, setDelay] = useState<number>(60000);
const [timer, setTimer] = useState<NodeJS.Timer | undefined>(setInterval(() => null, delay));
const [order, setOrder] = useState<Order | undefined>(undefined);
const [badOrder, setBadOrder] = useState<string | undefined>(undefined);
const {
book,
fetchBook,
maker,
setMaker,
clearOrder,
torStatus,
settings,
limits,
fetchLimits,
robot,
setRobot,
fetchRobot,
setOrder,
setDelay,
info,
fav,
setFav,
baseUrl,
order,
page,
setPage,
slideDirection,
setSlideDirection,
currentOrder,
setCurrentOrder,
closeAll,
open,
setOpen,
windowSize,
badOrder,
navbarHeight,
setBadOrder,
} = useContext<AppContextProps>(AppContext);
const Router = window.NativeRobosats === undefined ? BrowserRouter : HashRouter;
const basename = window.NativeRobosats === undefined ? '' : window.location.pathname;
const entryPage: Page | '' =
window.NativeRobosats === undefined ? window.location.pathname.split('/')[1] : '';
const [page, setPage] = useState<Page>(entryPage == '' ? 'robot' : entryPage);
const [slideDirection, setSlideDirection] = useState<SlideDirection>({
in: undefined,
out: undefined,
});
const [currentOrder, setCurrentOrder] = useState<number | undefined>(undefined);
const navbarHeight = 2.5;
const closeAll = {
more: false,
learn: false,
community: false,
info: false,
coordinator: false,
stats: false,
update: false,
profile: false,
};
const [open, setOpen] = useState<OpenDialogs>(closeAll);
const [windowSize, setWindowSize] = useState<{ width: number; height: number }>(
getWindowSize(theme.typography.fontSize),
);
useEffect(() => {
if (typeof window !== undefined) {
window.addEventListener('resize', onResize);
}
if (baseUrl != '') {
fetchBook();
fetchLimits();
}
return () => {
if (typeof window !== undefined) {
window.removeEventListener('resize', onResize);
}
};
}, [baseUrl]);
useEffect(() => {
let host = '';
if (window.NativeRobosats === undefined) {
host = getHost();
} else {
host =
settings.network === 'mainnet'
? coordinators[0].mainnetOnion
: coordinators[0].testnetOnion;
}
setBaseUrl(`${location.protocol}//${host}`);
}, [settings.network]);
useEffect(() => {
setWindowSize(getWindowSize(theme.typography.fontSize));
}, [theme.typography.fontSize]);
const onResize = function () {
setWindowSize(getWindowSize(theme.typography.fontSize));
};
const fetchBook = function () {
setBook({ ...book, loading: true });
apiClient.get(baseUrl, '/api/book/').then((data: any) =>
setBook({
loading: false,
orders: data.not_found ? [] : data,
}),
);
};
const fetchLimits = async () => {
setLimits({ ...limits, loading: true });
const data = apiClient.get(baseUrl, '/api/limits/').then((data) => {
setLimits({ list: data ?? [], loading: false });
return data;
});
return await data;
};
const fetchInfo = function () {
setInfo({ ...info, loading: true });
apiClient.get(baseUrl, '/api/info/').then((data: Info) => {
const versionInfo: any = checkVer(data.version.major, data.version.minor, data.version.patch);
setInfo({
...data,
openUpdateClient: versionInfo.updateAvailable,
coordinatorVersion: versionInfo.coordinatorVersion,
clientVersion: versionInfo.clientVersion,
loading: false,
});
});
};
useEffect(() => {
if (open.stats || open.coordinator || info.coordinatorVersion == 'v?.?.?') {
fetchInfo();
}
}, [open.stats, open.coordinator]);
useEffect(() => {
// Sets Setting network from coordinator API param if accessing via web
if (settings.network == undefined && info.network) {
setSettings((settings: Settings) => {
return { ...settings, network: info.network };
});
}
}, [info]);
const fetchRobot = function ({ keys = false }) {
const requestBody = {
token_sha256: sha256(robot.token),
};
if (keys) {
requestBody.pub_key = robot.pubKey;
requestBody.enc_priv_key = robot.encPrivKey;
}
setRobot({ ...robot, loading: true });
apiClient.post(baseUrl, '/api/user/', requestBody).then((data: any) => {
setCurrentOrder(
data.active_order_id
? data.active_order_id
: data.last_order_id
? data.last_order_id
: null,
);
setRobot({
...robot,
nickname: data.nickname,
token: robot.token,
loading: false,
avatarLoaded: robot.nickname === data.nickname,
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 : robot.copiedToken,
});
});
};
useEffect(() => {
if (baseUrl != '' && page != 'robot') {
if (open.profile || (robot.token && robot.nickname === null)) {
fetchRobot({ keys: false }); // fetch existing robot
} else if (robot.token && robot.encPrivKey && robot.pubKey) {
fetchRobot({ keys: true }); // create new robot with existing token and keys (on network and coordinator change)
}
}
}, [open.profile, baseUrl]);
// Fetch current order at load and in a loop
useEffect(() => {
if (currentOrder != undefined && (page == 'order' || (order == badOrder) == undefined)) {
fetchOrder();
}
}, [currentOrder, page]);
useEffect(() => {
clearInterval(timer);
setTimer(setInterval(fetchOrder, delay));
return () => clearInterval(timer);
}, [delay, currentOrder, page, badOrder]);
const orderReceived = function (data: any) {
if (data.bad_request != undefined) {
setBadOrder(data.bad_request);
setDelay(99999999);
setOrder(undefined);
} else {
setDelay(
data.status >= 0 && data.status <= 18
? page == 'order'
? statusToDelay[data.status]
: statusToDelay[data.status] * 5
: 99999999,
);
setOrder(data);
setBadOrder(undefined);
}
};
const fetchOrder = function () {
if (currentOrder != undefined) {
apiClient.get(baseUrl, '/api/order/?order_id=' + currentOrder).then(orderReceived);
}
};
const clearOrder = function () {
setOrder(undefined);
setBadOrder(undefined);
};
return (
<Router basename={basename}>
{/* load robot avatar image, set avatarLoaded: true */}
<RobotAvatar
style={{ display: 'none' }}
nickname={robot.nickname}
@ -353,6 +104,7 @@ const Main = ({ torStatus, settings, setSettings }: MainProps): JSX.Element => {
<RobotPage
setPage={setPage}
torStatus={torStatus}
fetchRobot={fetchRobot}
setCurrentOrder={setCurrentOrder}
windowSize={windowSize}
robot={robot}
@ -403,21 +155,7 @@ const Main = ({ torStatus, settings, setSettings }: MainProps): JSX.Element => {
appear={slideDirection.in != undefined}
>
<div>
<MakerPage
book={book}
limits={limits}
fetchLimits={fetchLimits}
maker={maker}
setMaker={setMaker}
clearOrder={clearOrder}
setPage={setPage}
setCurrentOrder={setCurrentOrder}
fav={fav}
setFav={setFav}
windowSize={{ ...windowSize, height: windowSize.height - navbarHeight }}
hasRobot={robot.avatarLoaded}
baseUrl={baseUrl}
/>
<MakerPage hasRobot={robot.avatarLoaded} />
</div>
</Slide>
</Route>
@ -432,17 +170,8 @@ const Main = ({ torStatus, settings, setSettings }: MainProps): JSX.Element => {
>
<div>
<OrderPage
baseUrl={baseUrl}
order={order}
settings={settings}
setOrder={setOrder}
setCurrentOrder={setCurrentOrder}
badOrder={badOrder}
locationOrderId={props.match.params.orderId}
setBadOrder={setBadOrder}
hasRobot={robot.avatarLoaded}
windowSize={{ ...windowSize, height: windowSize.height - navbarHeight }}
setPage={setPage}
/>
</div>
</Slide>
@ -456,34 +185,14 @@ const Main = ({ torStatus, settings, setSettings }: MainProps): JSX.Element => {
appear={slideDirection.in != undefined}
>
<div>
<SettingsPage
fav={fav}
setFav={setFav}
settings={settings}
setSettings={setSettings}
windowSize={{ ...windowSize, height: windowSize.height - navbarHeight }}
/>
<SettingsPage />
</div>
</Slide>
</Route>
</Switch>
</Box>
<div style={{ alignContent: 'center', display: 'flex' }}>
<NavBar
nickname={robot.avatarLoaded ? robot.nickname : null}
color={settings.network === 'mainnet' ? 'primary' : 'secondary'}
width={windowSize.width}
height={navbarHeight}
page={page}
setPage={setPage}
open={open}
setOpen={setOpen}
closeAll={closeAll}
setSlideDirection={setSlideDirection}
currentOrder={currentOrder}
hasRobot={robot.avatarLoaded}
baseUrl={baseUrl}
/>
<NavBar width={windowSize.width} height={navbarHeight} hasRobot={robot.avatarLoaded} />
</div>
<MainDialogs
open={open}

View File

@ -1,5 +1,4 @@
import React, { useEffect } from 'react';
import { Info, Robot } from '../../models';
import React, { useState, useContext, useEffect } from 'react';
import {
CommunityDialog,
CoordinatorSummaryDialog,
@ -9,7 +8,8 @@ import {
StatsDialog,
UpdateClientDialog,
} from '../../components/Dialogs';
import { Page } from '../NavBar';
import { pn } from '../../utils';
import { AppContext, AppContextProps } from '../../contexts/AppContext';
export interface OpenDialogs {
more: boolean;
@ -19,32 +19,31 @@ export interface OpenDialogs {
coordinator: boolean;
stats: boolean;
update: boolean;
profile: boolean; // temporary until new Robot Page is ready
profile: boolean;
}
interface MainDialogsProps {
open: OpenDialogs;
setOpen: (state: OpenDialogs) => void;
info: Info;
robot: Robot;
setRobot: (state: Robot) => void;
setPage: (state: Page) => void;
setCurrentOrder: (state: number) => void;
closeAll: OpenDialogs;
baseUrl: string;
}
const MainDialogs = (): JSX.Element => {
const {
open,
setOpen,
info,
limits,
closeAll,
robot,
setRobot,
setPage,
setCurrentOrder,
baseUrl,
} = useContext<AppContextProps>(AppContext);
const [maxAmount, setMaxAmount] = useState<string>('...loading...');
useEffect(() => {
if (limits.list[1000]) {
setMaxAmount(pn(limits.list[1000].max_amount * 100000000));
}
}, [limits.list]);
const MainDialogs = ({
open,
setOpen,
info,
closeAll,
robot,
setRobot,
setPage,
setCurrentOrder,
baseUrl,
}: MainDialogsProps): JSX.Element => {
useEffect(() => {
if (info.openUpdateClient) {
setOpen({ ...closeAll, update: true });
@ -61,7 +60,7 @@ const MainDialogs = ({
/>
<InfoDialog
open={open.info}
maxAmount='4,000,000'
maxAmount={maxAmount}
onClose={() => setOpen({ ...open, info: false })}
/>
<LearnDialog open={open.learn} onClose={() => setOpen({ ...open, learn: false })} />

View File

@ -1,52 +1,26 @@
import React, { useState } from 'react';
import React, { useContext, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useHistory } from 'react-router-dom';
import { Grid, Paper, Collapse, Typography } from '@mui/material';
import { LimitList, Maker, Book, Favorites, Order } from '../../models';
import { filterOrders } from '../../utils';
import MakerForm from '../../components/MakerForm';
import BookTable from '../../components/BookTable';
import { Page } from '../NavBar';
import { AppContext, AppContextProps } from '../../contexts/AppContext';
interface MakerPageProps {
limits: { list: LimitList; loading: boolean };
fetchLimits: () => void;
book: Book;
fav: Favorites;
maker: Maker;
setFav: (state: Favorites) => void;
setMaker: (state: Maker) => void;
clearOrder: () => void;
windowSize: { width: number; height: number };
hasRobot: boolean;
setCurrentOrder: (state: number) => void;
setPage: (state: Page) => void;
baseUrl: string;
}
const MakerPage = ({
limits,
fetchLimits,
book,
fav,
maker,
setFav,
setMaker,
clearOrder,
windowSize,
setCurrentOrder,
setPage,
hasRobot = false,
baseUrl,
}: MakerPageProps): JSX.Element => {
const MakerPage = ({ hasRobot = false }: MakerPageProps): JSX.Element => {
const { book, fav, maker, clearOrder, windowSize, setCurrentOrder, navbarHeight, setPage } =
useContext<AppContextProps>(AppContext);
const { t } = useTranslation();
const history = useHistory();
const maxHeight = windowSize.height * 0.85 - 3;
const maxHeight = (windowSize.height - navbarHeight) * 0.85 - 3;
const [showMatches, setShowMatches] = useState<boolean>(false);
const matches = filterOrders({
@ -71,14 +45,13 @@ const MakerPage = ({
</Grid>
<Grid item>
<BookTable
book={{ orders: matches, loading: book.loading }}
orderList={matches}
maxWidth={Math.min(windowSize.width, 60)} // EM units
maxHeight={Math.min(matches.length * 3.25 + 3, 16)} // EM units
defaultFullscreen={false}
showControls={false}
showFooter={false}
showNoResults={false}
baseUrl={baseUrl}
/>
</Grid>
</Grid>
@ -95,12 +68,6 @@ const MakerPage = ({
}}
>
<MakerForm
limits={limits}
fetchLimits={fetchLimits}
fav={fav}
setFav={setFav}
maker={maker}
setMaker={setMaker}
onOrderCreated={(id) => {
clearOrder();
setCurrentOrder(id);
@ -113,8 +80,6 @@ const MakerPage = ({
onSubmit={() => setShowMatches(matches.length > 0)}
onReset={() => setShowMatches(false)}
submitButtonLabel={matches.length > 0 && !showMatches ? 'Submit' : 'Create order'}
setPage={setPage}
baseUrl={baseUrl}
/>
</Paper>
</Grid>

View File

@ -21,19 +21,12 @@ const StyledTooltip = styled(({ className, ...props }: TooltipProps) => (
interface MoreTooltipProps {
open: OpenDialogs;
nickname: string | null;
setOpen: (state: OpenDialogs) => void;
closeAll: OpenDialogs;
children: JSX.Element;
}
const MoreTooltip = ({
open,
setOpen,
closeAll,
nickname,
children,
}: MoreTooltipProps): JSX.Element => {
const MoreTooltip = ({ open, setOpen, closeAll, children }: MoreTooltipProps): JSX.Element => {
const { t } = useTranslation();
const theme = useTheme();
return (

View File

@ -1,11 +1,9 @@
import React, { useEffect } from 'react';
import React, { useContext, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { useHistory } from 'react-router-dom';
import { Tabs, Tab, Paper, useTheme } from '@mui/material';
import MoreTooltip from './MoreTooltip';
import { OpenDialogs } from '../MainDialogs';
import { Page } from '.';
import {
@ -17,47 +15,35 @@ import {
MoreHoriz,
} from '@mui/icons-material';
import RobotAvatar from '../../components/RobotAvatar';
type Direction = 'left' | 'right' | undefined;
import { AppContext, AppContextProps } from '../../contexts/AppContext';
interface NavBarProps {
page: Page;
nickname?: string | null;
setPage: (state: Page) => void;
setSlideDirection: (state: { in: Direction; out: Direction }) => void;
width: number;
height: number;
open: OpenDialogs;
setOpen: (state: OpenDialogs) => void;
closeAll: OpenDialogs;
currentOrder: number | undefined;
hasRobot: boolean;
baseUrl: string;
color: 'primary' | 'secondary';
}
const NavBar = ({
page,
setPage,
setSlideDirection,
open,
nickname = null,
setOpen,
closeAll,
width,
height,
currentOrder,
hasRobot = false,
baseUrl,
color,
}: NavBarProps): JSX.Element => {
const NavBar = ({ width, height, hasRobot = false }: NavBarProps): JSX.Element => {
const {
page,
settings,
setPage,
setSlideDirection,
open,
robot,
setOpen,
closeAll,
currentOrder,
baseUrl,
} = useContext<AppContextProps>(AppContext);
const theme = useTheme();
const { t } = useTranslation();
const history = useHistory();
const smallBar = width < 50;
const tabSx = smallBar
? { position: 'relative', bottom: nickname ? '1em' : '0em', minWidth: '1em' }
? { position: 'relative', bottom: robot.nickname ? '1em' : '0em', minWidth: '1em' }
: { position: 'relative', bottom: '1em', minWidth: '2em' };
const pagesPosition = {
robot: 1,
@ -102,21 +88,21 @@ const NavBar = ({
TabIndicatorProps={{ sx: { height: '0.3em', position: 'absolute', top: 0 } }}
variant='fullWidth'
value={page}
indicatorColor={color}
textColor={color}
indicatorColor={settings.network === 'mainnet' ? 'primary' : 'secondary'}
textColor={settings.network === 'mainnet' ? 'primary' : 'secondary'}
onChange={changePage}
>
<Tab
sx={{ ...tabSx, minWidth: '2.5em', width: '2.5em', maxWidth: '4em' }}
value='none'
disabled={nickname === null}
disabled={robot.nickname === null}
onClick={() => setOpen({ ...closeAll, profile: !open.profile })}
icon={
nickname ? (
robot.nickname && robot.avatarLoaded ? (
<RobotAvatar
style={{ width: '2.3em', height: '2.3em', position: 'relative', top: '0.2em' }}
avatarClass={theme.palette.mode === 'dark' ? 'navBarAvatarDark' : 'navBarAvatar'}
nickname={nickname}
nickname={robot.nickname}
baseUrl={baseUrl}
/>
) : (
@ -171,7 +157,7 @@ const NavBar = ({
open.more ? null : setOpen({ ...open, more: true });
}}
icon={
<MoreTooltip open={open} nickname={nickname} setOpen={setOpen} closeAll={closeAll}>
<MoreTooltip open={open} setOpen={setOpen} closeAll={closeAll}>
<MoreHoriz />
</MoreTooltip>
}

View File

@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react';
import React, { useContext, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Tab, Tabs, Paper, CircularProgress, Grid, Typography, Box } from '@mui/material';
import { useHistory } from 'react-router-dom';
@ -9,40 +9,31 @@ import OrderDetails from '../../components/OrderDetails';
import { Page } from '../NavBar';
import { Order, Settings } from '../../models';
import { apiClient } from '../../services/api';
import { AppContext, AppContextProps } from '../../contexts/AppContext';
interface OrderPageProps {
windowSize: { width: number; height: number };
order: Order;
settings: Settings;
setOrder: (state: Order) => void;
setCurrentOrder: (state: number) => void;
fetchOrder: () => void;
badOrder: string | undefined;
setBadOrder: (state: string | undefined) => void;
hasRobot: boolean;
setPage: (state: Page) => void;
baseUrl: string;
locationOrderId: number;
}
const OrderPage = ({
windowSize,
order,
settings,
setOrder,
setCurrentOrder,
badOrder,
setBadOrder,
setPage,
hasRobot = false,
baseUrl,
locationOrderId,
}: OrderPageProps): JSX.Element => {
const OrderPage = ({ hasRobot = false, locationOrderId }: OrderPageProps): JSX.Element => {
const {
windowSize,
order,
settings,
setOrder,
setCurrentOrder,
badOrder,
setBadOrder,
setPage,
baseUrl,
navbarHeight,
} = useContext<AppContextProps>(AppContext);
const { t } = useTranslation();
const history = useHistory();
const doublePageWidth: number = 50;
const maxHeight: number = windowSize.height * 0.85 - 3;
const maxHeight: number = (windowSize.height - navbarHeight) * 0.85 - 3;
const [tab, setTab] = useState<'order' | 'contract'>('contract');

View File

@ -1,12 +1,7 @@
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 { Button, Grid, Typography, useTheme } from '@mui/material';
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';
@ -17,8 +12,6 @@ interface RecoveryProps {
inputToken: string;
setInputToken: (state: string) => void;
getGenerateRobot: (token: string) => void;
setPage: (state: Page) => void;
baseUrl: string;
}
const Recovery = ({
@ -28,11 +21,8 @@ const Recovery = ({
setView,
setInputToken,
getGenerateRobot,
setPage,
baseUrl,
}: RecoveryProps): JSX.Element => {
const { t } = useTranslation();
const theme = useTheme();
const recoveryDisabled = () => {
return !(inputToken.length > 20);
@ -47,11 +37,14 @@ const Recovery = ({
return (
<Grid container direction='column' alignItems='center' spacing={1} padding={2}>
<Grid item>
<Typography variant='h5' align='center'>
{t('Robot recovery')}
</Typography>
</Grid>
<Grid item>
<Typography align='center'>
{t(
'Please, introduce your robot token to re-build your robot and gain access to its trades.',
)}
{t('Enter your robot token to re-build your robot and gain access to its trades.')}
</Typography>
</Grid>
<Grid item>

View File

@ -7,7 +7,6 @@ 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;
@ -21,7 +20,6 @@ interface RobotProfileProps {
setPage: (state: Page) => void;
baseUrl: string;
badRequest: string;
robotFound: boolean;
width: number;
}
@ -37,7 +35,6 @@ const RobotProfile = ({
setView,
badRequest,
baseUrl,
robotFound,
width,
}: RobotProfileProps): JSX.Element => {
const { t } = useTranslation();
@ -104,15 +101,13 @@ const RobotProfile = ({
/>
</Grid>
{/* {robotFound ? (
<Grid item>
<Typography variant='h6'>
{t('Welcome back!')}
</Typography>
</Grid>
{robot.found ? (
<Typography align='center' variant='h6'>
{t('Welcome back!')}
</Typography>
) : (
<></>
)} */}
)}
{robot.activeOrderId ? (
<Grid item>
@ -184,7 +179,6 @@ const RobotProfile = ({
<Grid item>
<Button
disabled={!(robot.avatarLoaded && robot.nickname)}
size='small'
color='primary'
onClick={() => {

View File

@ -59,7 +59,7 @@ const TokenInput = ({
required={true}
label={label || undefined}
value={inputToken}
autoFocus={autoFocusTarget == 'texfield'}
autoFocus={autoFocusTarget == 'textfield'}
fullWidth={fullWidth}
sx={{ borderColor: 'primary' }}
variant={editable ? 'outlined' : 'filled'}

View File

@ -24,7 +24,7 @@ const Welcome = ({ setView, width, getGenerateRobot }: WelcomeProps): JSX.Elemen
paddingTop={2.2}
padding={0.5}
>
<Grid item>
<Grid item style={{ paddingTop: '2em', paddingBottom: '1.5em' }}>
<svg width={0} height={0}>
<linearGradient id='linearColors' x1={1} y1={0} x2={1} y2={1}>
<stop offset={0} stopColor={theme.palette.primary.main} />

View File

@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react';
import React, { useContext, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import {
Paper,
@ -14,36 +14,19 @@ 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';
import { genKey } from '../../pgp';
import { AppContext, AppContextProps } from '../../contexts/AppContext';
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 RobotPage = (): JSX.Element => {
const { setPage, setCurrentOrder, fetchRobot, torStatus, windowSize, robot, setRobot, baseUrl } =
useContext<AppContextProps>(AppContext);
const { t } = useTranslation();
const params = useParams();
const refCode = params.refCode;
@ -51,7 +34,6 @@ const RobotPage = ({
const maxHeight = windowSize.height * 0.85 - 3;
const theme = useTheme();
const [robotFound, setRobotFound] = useState<boolean>(false);
const [badRequest, setBadRequest] = useState<string | undefined>(undefined);
const [inputToken, setInputToken] = useState<string>('');
const [view, setView] = useState<'welcome' | 'onboarding' | 'recovery' | 'profile'>(
@ -63,82 +45,24 @@ const RobotPage = ({
setInputToken(robot.token);
}
if (robot.nickname == null && robot.token) {
getGenerateRobot(robot.token);
fetchRobot({ action: 'login' });
}
}, []);
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,
};
genKey(token).then(function (key) {
fetchRobot({
action: 'generate',
newKeys: {
pubKey: key.publicKeyArmored,
encPrivKey: key.encryptedPrivateKeyArmored,
},
newToken: token,
refCode,
setBadRequest,
});
});
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 = () => {
@ -148,7 +72,6 @@ const RobotPage = ({
const logoutRobot = () => {
setInputToken('');
setRobotFound(false);
systemClient.deleteCookie('sessionid');
systemClient.deleteItem('robot_token');
systemClient.deleteItem('pub_key');
@ -237,7 +160,6 @@ const RobotPage = ({
<RobotProfile
setView={setView}
robot={robot}
robotFound={robotFound}
setRobot={setRobot}
setCurrentOrder={setCurrentOrder}
badRequest={badRequest}

View File

@ -1,27 +1,14 @@
import React from 'react';
import React, { useContext } from 'react';
import { useTranslation } from 'react-i18next';
import { Grid, Paper, useTheme } from '@mui/material';
import SettingsForm from '../../components/SettingsForm';
import { Settings, Favorites } from '../../models';
import { AppContextProps, AppContext } from '../../contexts/AppContext';
interface SettingsPageProps {
fav: Favorites;
setFav: (state: Favorites) => void;
settings: Settings;
setSettings: (state: Settings) => void;
windowSize: { width: number; height: number };
}
const SettingsPage = ({
fav,
setFav,
settings,
setSettings,
windowSize,
}: SettingsPageProps): JSX.Element => {
const SettingsPage = (): JSX.Element => {
const { windowSize, navbarHeight } = useContext<AppContextProps>(AppContext);
const theme = useTheme();
const { t } = useTranslation();
const maxHeight = windowSize.height * 0.85 - 3;
const maxHeight = (windowSize.height - navbarHeight) * 0.85 - 3;
return (
<Paper
@ -36,13 +23,7 @@ const SettingsPage = ({
>
<Grid container>
<Grid item>
<SettingsForm
fav={fav}
setFav={setFav}
settings={settings}
setSettings={setSettings}
showNetwork={!(window.NativeRobosats === undefined)}
/>
<SettingsForm showNetwork={!(window.NativeRobosats === undefined)} />
</Grid>
</Grid>
</Paper>

View File

@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react';
import React, { useContext, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import {
Box,
@ -17,7 +17,7 @@ import {
} from '@mui/material';
import { DataGrid, GridPagination } from '@mui/x-data-grid';
import currencyDict from '../../../static/assets/currencies.json';
import { Book, Favorites } from '../../models';
import { PublicOrder } from '../../models';
import { filterOrders, hexToRgb, statusBadgeColor, pn, amountToString } from '../../utils';
import BookControl from './BookControl';
@ -27,11 +27,10 @@ import RobotAvatar from '../RobotAvatar';
// Icons
import { Fullscreen, FullscreenExit, Refresh } from '@mui/icons-material';
import { AppContext, AppContextProps } from '../../contexts/AppContext';
interface BookTableProps {
clickRefresh?: () => void;
book: Book;
fav?: Favorites;
orderList?: PublicOrder[];
maxWidth: number;
maxHeight: number;
fullWidth?: number;
@ -42,16 +41,11 @@ interface BookTableProps {
showControls?: boolean;
showFooter?: boolean;
showNoResults?: boolean;
setFav?: (state: Favorites) => void;
onOrderClicked?: (id: number) => void;
baseUrl: string;
}
const BookTable = ({
clickRefresh,
book,
fav = { currency: 1, type: 0, mode: 'fiat' },
setFav,
orderList,
maxWidth = 100,
maxHeight = 70,
fullWidth = 100,
@ -63,10 +57,12 @@ const BookTable = ({
showFooter = true,
showNoResults = true,
onOrderClicked = () => null,
baseUrl,
}: BookTableProps): JSX.Element => {
const { book, fetchBook, fav, setFav, baseUrl } = useContext<AppContextProps>(AppContext);
const { t } = useTranslation();
const theme = useTheme();
const orders = orderList ?? book.orders;
const [pageSize, setPageSize] = useState(0);
const [fullscreen, setFullscreen] = useState(defaultFullscreen);
const [paymentMethods, setPaymentMethods] = useState<string[]>([]);
@ -641,7 +637,7 @@ const BookTable = ({
</IconButton>
</Grid>
<Grid item xs={6}>
<IconButton onClick={clickRefresh}>
<IconButton onClick={() => fetchBook()}>
<Refresh />
</IconButton>
</Grid>
@ -729,11 +725,11 @@ const BookTable = ({
rows={
showControls
? filterOrders({
orders: book.orders,
orders,
baseFilter: fav,
paymentMethods,
})
: book.orders
: orders
}
loading={book.loading}
columns={columns}
@ -748,7 +744,7 @@ const BookTable = ({
setPaymentMethods,
},
}}
pageSize={book.loading && book.orders.length == 0 ? 0 : pageSize}
pageSize={book.loading && orders.length == 0 ? 0 : pageSize}
rowsPerPageOptions={width < 22 ? [] : [0, pageSize, defaultPageSize * 2, 50, 100]}
onPageSizeChange={(newPageSize) => {
setPageSize(newPageSize);
@ -769,11 +765,11 @@ const BookTable = ({
rows={
showControls
? filterOrders({
orders: book.orders,
orders,
baseFilter: fav,
paymentMethods,
})
: book.orders
: orders
}
loading={book.loading}
columns={columns}
@ -788,7 +784,7 @@ const BookTable = ({
setPaymentMethods,
},
}}
pageSize={book.loading && book.orders.length == 0 ? 0 : pageSize}
pageSize={book.loading && orders.length == 0 ? 0 : pageSize}
rowsPerPageOptions={[0, pageSize, defaultPageSize * 2, 50, 100]}
onPageSizeChange={(newPageSize) => {
setPageSize(newPageSize);

View File

@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react';
import React, { useEffect, useState, useContext } from 'react';
import {
ResponsiveLine,
Serie,
@ -26,32 +26,24 @@ import { amountToString, matchMedian, statusBadgeColor } from '../../../utils';
import currencyDict from '../../../../static/assets/currencies.json';
import { PaymentStringAsIcons } from '../../PaymentMethods';
import getNivoScheme from '../NivoScheme';
import { AppContextProps, AppContext } from '../../../contexts/AppContext';
interface DepthChartProps {
orders: PublicOrder[];
lastDayPremium?: number | undefined;
currency: number;
limits: LimitList;
maxWidth: number;
maxHeight: number;
fillContainer?: boolean;
elevation?: number;
onOrderClicked?: (id: number) => void;
baseUrl: string;
}
const DepthChart: React.FC<DepthChartProps> = ({
orders,
lastDayPremium,
currency,
limits,
maxWidth,
maxHeight,
fillContainer = false,
elevation = 6,
onOrderClicked = () => null,
baseUrl,
}) => {
const { book, fav, info, limits, baseUrl } = useContext<AppContextProps>(AppContext);
const { t } = useTranslation();
const theme = useTheme();
const [enrichedOrders, setEnrichedOrders] = useState<Order[]>([]);
@ -66,22 +58,22 @@ const DepthChart: React.FC<DepthChartProps> = ({
const width = maxWidth < 20 ? 20 : maxWidth > 72.8 ? 72.8 : maxWidth;
useEffect(() => {
setCurrencyCode(currency === 0 ? 1 : currency);
}, [currency]);
setCurrencyCode(fav.currency === 0 ? 1 : fav.currency);
}, [fav.currency]);
useEffect(() => {
if (Object.keys(limits).length > 0) {
const enriched = orders.map((order) => {
if (Object.keys(limits.list).length > 0) {
const enriched = book.orders.map((order) => {
// We need to transform all currencies to the same base (ex. USD), we don't have the exchange rate
// for EUR -> USD, but we know the rate of both to BTC, so we get advantage of it and apply a
// simple rule of three
order.base_amount =
(order.price * limits[currencyCode].price) / limits[order.currency].price;
(order.price * limits.list[currencyCode].price) / limits.list[order.currency].price;
return order;
});
setEnrichedOrders(enriched);
}
}, [limits, orders, currencyCode]);
}, [limits.list, book.orders, currencyCode]);
useEffect(() => {
if (enrichedOrders.length > 0) {
@ -102,16 +94,16 @@ const DepthChart: React.FC<DepthChartProps> = ({
setXRange(maxRange);
setRangeSteps(rangeSteps);
} else {
if (lastDayPremium === undefined) {
if (info.last_day_nonkyc_btc_premium === undefined) {
const premiums: number[] = enrichedOrders.map((order) => order?.premium || 0);
setCenter(~~matchMedian(premiums));
} else {
setCenter(lastDayPremium);
setCenter(info.last_day_nonkyc_btc_premium);
}
setXRange(8);
setRangeSteps(0.5);
}
}, [enrichedOrders, xType, lastDayPremium, currencyCode]);
}, [enrichedOrders, xType, info.last_day_nonkyc_btc_premium, currencyCode]);
const generateSeries: () => void = () => {
const sortedOrders: PublicOrder[] =

View File

@ -117,7 +117,7 @@ const CoordinatorSummaryDialog = ({ open = false, onClose, info }: Props): JSX.E
primaryTypographyProps={{ fontSize: '14px' }}
secondaryTypographyProps={{ fontSize: '12px' }}
primary={`${info.last_day_nonkyc_btc_premium}%`}
secondary={t('24h non-KYC bitcoin premium')}
secondary={t('Last 24h mean premium')}
/>
</ListItem>

View File

@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react';
import React, { useContext, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import {
InputAdornment,
@ -29,7 +29,6 @@ import { LimitList, Maker, Favorites, defaultMaker } from '../../models';
import { LocalizationProvider, TimePicker } from '@mui/x-date-pickers';
import DateFnsUtils from '@date-io/date-fns';
import { useHistory } from 'react-router-dom';
import { ConfirmationDialog } from '../Dialogs';
import { apiClient } from '../../services/api';
@ -41,35 +40,21 @@ import { amountToString, pn } from '../../utils';
import { SelfImprovement, Lock, HourglassTop, DeleteSweep, Edit } from '@mui/icons-material';
import { LoadingButton } from '@mui/lab';
import { Page } from '../../basic/NavBar';
import { AppContext, AppContextProps } from '../../contexts/AppContext';
interface MakerFormProps {
limits: { list: LimitList; loading: boolean };
fetchLimits: () => void;
pricingMethods?: boolean;
maker: Maker;
fav: Favorites;
setFav: (state: Favorites) => void;
setMaker: (state: Maker) => void;
disableRequest?: boolean;
pricingMethods?: boolean;
collapseAll?: boolean;
onSubmit?: () => void;
onReset?: () => void;
submitButtonLabel?: string;
onOrderCreated?: (id: number) => void;
hasRobot?: boolean;
setPage?: (state: Page) => void;
baseUrl: string;
}
const MakerForm = ({
limits,
fetchLimits,
pricingMethods = false,
fav,
setFav,
maker,
setMaker,
disableRequest = false,
collapseAll = false,
onSubmit = () => {},
@ -77,12 +62,12 @@ const MakerForm = ({
submitButtonLabel = 'Create Order',
onOrderCreated = () => null,
hasRobot = true,
setPage = () => null,
baseUrl,
}: MakerFormProps): JSX.Element => {
const { fav, setFav, limits, fetchLimits, maker, setMaker, setPage, baseUrl } =
useContext<AppContextProps>(AppContext);
const { t } = useTranslation();
const theme = useTheme();
const history = useHistory();
const [badRequest, setBadRequest] = useState<string | null>(null);
const [amountLimits, setAmountLimits] = useState<number[]>([1, 1000]);
const [satoshisLimits, setSatoshisLimits] = useState<number[]>([20000, 4000000]);

View File

@ -1,5 +1,6 @@
import React from 'react';
import React, { useContext } from 'react';
import { useTranslation } from 'react-i18next';
import { AppContextProps, AppContext } from '../../contexts/AppContext';
import {
Grid,
Paper,
@ -14,7 +15,6 @@ import {
ToggleButtonGroup,
ToggleButton,
} from '@mui/material';
import { Favorites, Settings } from '../../models';
import SelectLanguage from './SelectLanguage';
import {
Translate,
@ -31,21 +31,11 @@ import SwapCalls from '@mui/icons-material/SwapCalls';
interface SettingsFormProps {
dense?: boolean;
fav: Favorites;
setFav: (state: Favorites) => void;
settings: Settings;
setSettings: (state: Settings) => void;
showNetwork?: boolean;
}
const SettingsForm = ({
dense = false,
fav,
setFav,
settings,
setSettings,
showNetwork = false,
}: SettingsFormProps): JSX.Element => {
const SettingsForm = ({ dense = false, showNetwork = false }: SettingsFormProps): JSX.Element => {
const { fav, setFav, settings, setSettings } = useContext<AppContextProps>(AppContext);
const theme = useTheme();
const { t } = useTranslation();
const fontSizes = [

View File

@ -1,7 +1,8 @@
import React, { useEffect, useState } from 'react';
import React, { useContext } from 'react';
import { Box, CircularProgress, Tooltip } from '@mui/material';
import { TorIcon } from './Icons';
import { useTranslation } from 'react-i18next';
import { AppContext, AppContextProps } from '../contexts/AppContext';
interface TorIndicatorProps {
color: 'inherit' | 'error' | 'warning' | 'success' | 'primary' | 'secondary' | 'info' | undefined;
@ -53,11 +54,8 @@ const TorIndicator = ({
);
};
interface TorConnectionBadgeProps {
torStatus: 'NOTINIT' | 'STARTING' | '"Done"' | 'DONE';
}
const TorConnectionBadge = ({ torStatus }: TorConnectionBadgeProps): JSX.Element => {
const TorConnectionBadge = (): JSX.Element => {
const { torStatus } = useContext<AppContextProps>(AppContext);
const { t } = useTranslation();
if (window?.NativeRobosats == null) {

View File

@ -0,0 +1,501 @@
import React, { useEffect, useState } from 'react';
import { Page } from '../basic/NavBar';
import { OpenDialogs } from '../basic/MainDialogs';
import {
Book,
LimitList,
Maker,
Robot,
Info,
Settings,
Favorites,
defaultMaker,
defaultInfo,
Coordinator,
Order,
} from '../models';
import { apiClient } from '../services/api';
import { checkVer, getHost, tokenStrength } from '../utils';
import { sha256 } from 'js-sha256';
import defaultCoordinators from '../../static/federation.json';
import { useTheme } from '@mui/material';
import { systemClient } from '../services/System';
const getWindowSize = function (fontSize: number) {
// returns window size in EM units
return {
width: window.innerWidth / fontSize,
height: window.innerHeight / fontSize,
};
};
// Refresh delays (ms) according to Order status
const statusToDelay = [
3000, // 'Waiting for maker bond'
35000, // 'Public'
180000, // 'Paused'
3000, // 'Waiting for taker bond'
999999, // 'Cancelled'
999999, // 'Expired'
8000, // 'Waiting for trade collateral and buyer invoice'
8000, // 'Waiting only for seller trade collateral'
8000, // 'Waiting only for buyer invoice'
10000, // 'Sending fiat - In chatroom'
10000, // 'Fiat sent - In chatroom'
100000, // 'In dispute'
999999, // 'Collaboratively cancelled'
10000, // 'Sending satoshis to buyer'
999999, // 'Sucessful trade'
30000, // 'Failed lightning network routing'
300000, // 'Wait for dispute resolution'
300000, // 'Maker lost dispute'
300000, // 'Taker lost dispute'
];
export interface SlideDirection {
in: 'left' | 'right' | undefined;
out: 'left' | 'right' | undefined;
}
export interface fetchRobotProps {
action?: 'login' | 'generate';
newKeys?: { encPrivKey: string; pubKey: string } | null;
newToken?: string | null;
refCode?: string | null;
setBadRequest?: (state: string) => void;
}
export type TorStatus = 'NOTINIT' | 'STARTING' | '"Done"' | 'DONE';
export interface AppContextProps {
torStatus: TorStatus;
federation: Coordinator[];
setFederation: (state: Coordinator[]) => void;
settings: Settings;
setSettings: (state: Settings) => void;
book: Book;
info: Info;
setBook: (state: Book) => void;
fetchBook: () => void;
limits: { list: LimitList; loading: boolean };
setLimits: (state: { list: LimitList; loading: boolean }) => void;
fetchLimits: () => void;
maker: Maker;
setMaker: (state: Maker) => void;
clearOrder: () => void;
robot: Robot;
setRobot: (state: Robot) => void;
focusedCoordinator: number;
setFocusedCoordinator: (state: number) => void;
baseUrl: string;
setBaseUrl: (state: string) => void;
fav: Favorites;
setFav: (state: Favorites) => void;
order: Order | undefined;
setOrder: (state: Order | undefined) => void;
badOrder: string;
setBadOrder: (state: string | undefined) => void;
setDelay: (state: number) => void;
page: Page;
setPage: (state: Page) => void;
slideDirection: SlideDirection;
setSlideDirection: (state: SlideDirection) => void;
currentOrder: number | undefined;
setCurrentOrder: (state: number) => void;
navbarHeight: number;
closeAll: OpenDialogs;
open: OpenDialogs;
setOpen: (state: OpenDialogs) => void;
windowSize: { width: number; height: number };
clientVersion: {
semver: Version;
short: string;
long: string;
};
}
const entryPage: Page | '' =
window.NativeRobosats === undefined ? window.location.pathname.split('/')[1] : '';
const closeAll = {
more: false,
learn: false,
community: false,
info: false,
coordinator: false,
exchange: false,
client: false,
update: false,
profile: false,
};
// export const initialState = {
// federation: defaultFederation,
// setFederation: () => null,
// settings: new Settings(),
// setSettings: () => null,
// book: { orders: [], loading: true },
// setBook: () => null,
// fetchBook: () => null,
// limits: {
// list: [],
// loading: true,
// },
// setLimits:() => null,
// fetchLimits: ()=> null,
// maker: defaultMaker,
// setMaker: () => null,
// clearOrder: () => null,
// robot: new Robot(),
// setRobot: () => null,
// info: defaultExchange,
// setExchange: () => null,
// focusedCoordinator: 0,
// setFocusedCoordinator: () => null,
// baseUrl: '',
// setBaseUrl: () => null,
// fav: { type: null, currency: 0 },
// setFav: () => null,
// order: undefined,
// setOrder: () => null,
// badOrder: '',
// setBadOrder: () => null,
// setDelay: () => null,
// page: entryPage == '' ? 'robot' : entryPage,
// setPage: () => null,
// slideDirection: {
// in: undefined,
// out: undefined,
// },
// setSlideDirection: () => null,
// currentOrder: undefined,
// setCurrentOrder: () => null,
// navbarHeight: 2.5,
// closeAll,
// open: closeAll,
// setOpen: () => null,
// windowSize: getWindowSize(14),
// }
export interface AppContextProviderProps {
children: React.ReactNode;
settings: Settings;
setSettings: (state: Settings) => void;
}
export const AppContextProvider = ({
children,
settings,
setSettings,
}: AppContextProviderProps): JSX.Element => {
const theme = useTheme();
// All app data structured
const [torStatus, setTorStatus] = useState<TorStatus>('NOTINIT');
const [book, setBook] = useState<Book>({ orders: [], loading: true });
const [limits, setLimits] = useState<{ list: LimitList; loading: boolean }>({
list: [],
loading: true,
});
const [robot, setRobot] = useState<Robot>(new Robot());
const [maker, setMaker] = useState<Maker>(defaultMaker);
const [info, setInfo] = useState<Info>(defaultInfo);
const [coordinators, setCoordinators] = useState<Coordinator[]>(defaultCoordinators);
const [baseUrl, setBaseUrl] = useState<string>('');
const [fav, setFav] = useState<Favorites>({ type: null, mode: 'fiat', currency: 0 });
const [delay, setDelay] = useState<number>(60000);
const [timer, setTimer] = useState<NodeJS.Timer | undefined>(setInterval(() => null, delay));
const [order, setOrder] = useState<Order | undefined>(undefined);
const [badOrder, setBadOrder] = useState<string | undefined>(undefined);
const entryPage: Page | '' =
window.NativeRobosats === undefined ? window.location.pathname.split('/')[1] : '';
const [page, setPage] = useState<Page>(entryPage == '' ? 'robot' : entryPage);
const [slideDirection, setSlideDirection] = useState<SlideDirection>({
in: undefined,
out: undefined,
});
const [currentOrder, setCurrentOrder] = useState<number | undefined>(undefined);
const navbarHeight = 2.5;
const closeAll = {
more: false,
learn: false,
community: false,
info: false,
coordinator: false,
stats: false,
update: false,
profile: false,
};
const [open, setOpen] = useState<OpenDialogs>(closeAll);
const [windowSize, setWindowSize] = useState<{ width: number; height: number }>(
getWindowSize(theme.typography.fontSize),
);
useEffect(() => {
if (typeof window !== undefined) {
window.addEventListener('resize', onResize);
}
if (baseUrl != '') {
fetchBook();
fetchLimits();
}
return () => {
if (typeof window !== undefined) {
window.removeEventListener('resize', onResize);
}
};
}, [baseUrl]);
useEffect(() => {
let host = '';
if (window.NativeRobosats === undefined) {
host = getHost();
} else {
host =
settings.network === 'mainnet'
? coordinators[0].mainnetOnion
: coordinators[0].testnetOnion;
}
setBaseUrl(`${location.protocol}//${host}`);
}, [settings.network]);
useEffect(() => {
setWindowSize(getWindowSize(theme.typography.fontSize));
}, [theme.typography.fontSize]);
const onResize = function () {
setWindowSize(getWindowSize(theme.typography.fontSize));
};
const fetchBook = function () {
setBook({ ...book, loading: true });
apiClient.get(baseUrl, '/api/book/').then((data: any) =>
setBook({
loading: false,
orders: data.not_found ? [] : data,
}),
);
};
const fetchLimits = async () => {
setLimits({ ...limits, loading: true });
const data = apiClient.get(baseUrl, '/api/limits/').then((data) => {
setLimits({ list: data ?? [], loading: false });
return data;
});
return await data;
};
const fetchInfo = function () {
setInfo({ ...info, loading: true });
apiClient.get(baseUrl, '/api/info/').then((data: Info) => {
const versionInfo: any = checkVer(data.version.major, data.version.minor, data.version.patch);
setInfo({
...data,
openUpdateClient: versionInfo.updateAvailable,
coordinatorVersion: versionInfo.coordinatorVersion,
clientVersion: versionInfo.clientVersion,
loading: false,
});
});
};
useEffect(() => {
if (open.stats || open.coordinator || info.coordinatorVersion == 'v?.?.?') {
fetchInfo();
}
}, [open.stats, open.coordinator]);
useEffect(() => {
// Sets Setting network from coordinator API param if accessing via web
if (settings.network == undefined && info.network) {
setSettings((settings: Settings) => {
return { ...settings, network: info.network };
});
}
}, [info]);
// Fetch current order at load and in a loop
useEffect(() => {
if (currentOrder != undefined && (page == 'order' || (order == badOrder) == undefined)) {
fetchOrder();
}
}, [currentOrder, page]);
useEffect(() => {
clearInterval(timer);
setTimer(setInterval(fetchOrder, delay));
return () => clearInterval(timer);
}, [delay, currentOrder, page, badOrder]);
const orderReceived = function (data: any) {
if (data.bad_request != undefined) {
setBadOrder(data.bad_request);
setDelay(99999999);
setOrder(undefined);
} else {
setDelay(
data.status >= 0 && data.status <= 18
? page == 'order'
? statusToDelay[data.status]
: statusToDelay[data.status] * 5
: 99999999,
);
setOrder(data);
setBadOrder(undefined);
}
};
const fetchOrder = function () {
if (currentOrder != undefined) {
apiClient.get(baseUrl, '/api/order/?order_id=' + currentOrder).then(orderReceived);
}
};
const clearOrder = function () {
setOrder(undefined);
setBadOrder(undefined);
};
const fetchRobot = function ({
action = 'login',
newKeys = null,
newToken = null,
refCode = null,
setBadRequest = () => {},
}: fetchRobotProps) {
setRobot({ ...robot, loading: true, avatarLoaded: false });
setBadRequest('');
let requestBody = {};
if (action == 'login') {
requestBody.token_sha256 = sha256(newToken ?? robot.token);
} else if (action == 'generate' && newToken != null) {
const strength = tokenStrength(newToken);
requestBody.token_sha256 = sha256(newToken);
requestBody.unique_values = strength.uniqueValues;
requestBody.counts = strength.counts;
requestBody.length = newToken.length;
requestBody.ref_code = refCode;
requestBody.public_key = newKeys.pubKey ?? robot.pubkey;
requestBody.encrypted_private_key = newKeys.encPrivKey ?? robot.encPrivKey;
}
apiClient.post(baseUrl, '/api/user/', requestBody).then((data: any) => {
setCurrentOrder(
data.active_order_id
? data.active_order_id
: data.last_order_id
? data.last_order_id
: null,
);
if (data.bad_request) {
setBadRequest(data.bad_request);
setRobot({
...robot,
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,
found: false,
});
} else {
setRobot({
...robot,
nickname: data.nickname,
token: newToken ?? robot.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,
found: data?.found,
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', newToken ?? robot.token);
systemClient.setItem('pub_key', data.public_key.split('\n').join('\\'));
systemClient.setItem('enc_priv_key', data.encrypted_private_key.split('\n').join('\\'));
}
});
};
useEffect(() => {
if (baseUrl != '' && page != 'robot') {
if (open.profile || (robot.token && robot.nickname === null)) {
fetchRobot({ action: 'login' }); // fetch existing robot
} else if (robot.token && robot.encPrivKey && robot.pubKey) {
fetchRobot({ action: 'login' }); // create new robot with existing token and keys (on network and coordinator change)
}
}
}, [open.profile, baseUrl]);
return (
<AppContext.Provider
value={{
torStatus,
settings,
setSettings,
book,
setBook,
fetchBook,
fetchRobot,
limits,
info,
setLimits,
fetchLimits,
maker,
setMaker,
clearOrder,
robot,
setRobot,
baseUrl,
setBaseUrl,
fav,
setFav,
order,
setOrder,
badOrder,
setBadOrder,
setDelay,
page,
setPage,
slideDirection,
setSlideDirection,
currentOrder,
setCurrentOrder,
navbarHeight,
closeAll,
open,
setOpen,
windowSize,
}}
>
{children}
</AppContext.Provider>
);
};
export const AppContext = React.createContext();

View File

@ -22,6 +22,7 @@ class Robot {
public tgBotName: string = 'unknown';
public tgToken: string = 'unknown';
public loading: boolean = false;
public found: boolean = false;
public avatarLoaded: boolean = false;
public copiedToken: boolean = false;
}

View File

@ -1,20 +1,6 @@
import React, { useEffect, useRef, useState } from 'react';
import React, { useEffect, useContext, useState } from 'react';
import GridLayout, { Layout } from 'react-grid-layout';
import { Grid, styled, useTheme } from '@mui/material';
import { apiClient } from '../services/api';
import checkVer from '../utils/checkVer';
import {
Book,
LimitList,
Maker,
Robot,
Info,
Settings,
Favorites,
defaultMaker,
defaultInfo,
} from '../models';
import {
PlaceholderWidget,
@ -26,21 +12,7 @@ import {
import ToolBar from '../pro/ToolBar';
import LandingDialog from '../pro/LandingDialog';
import defaultCoordinators from '../../static/federation.json';
import { getHost } from '../utils';
const getWindowSize = function (fontSize: number) {
// returns window size in EM units
return {
width: window.innerWidth / fontSize,
height: window.innerHeight / fontSize,
};
};
interface MainProps {
settings: Settings;
setSettings: (state: Settings) => void;
}
import { AppContext, AppContextProps } from '../contexts/AppContext';
// To Do. Add dotted grid when layout is not frozen
// ${freeze ?
@ -57,115 +29,58 @@ const StyledRGL = styled(GridLayout)(
`,
);
const Main = ({ settings, setSettings }: MainProps): JSX.Element => {
const defaultLayout: Layout = [
{ i: 'Maker', w: 10, h: 16, x: 67, y: 0, minW: 8, maxW: 22, minH: 10, maxH: 28 },
{ i: 'Book', w: 43, h: 15, x: 34, y: 16, minW: 6, maxW: 70, minH: 9, maxH: 25 },
{ i: 'DepthChart', w: 15, h: 10, x: 19, y: 16, minW: 6, maxW: 22, minH: 9, maxH: 25 },
{ i: 'Garage', w: 52, h: 16, x: 0, y: 0, minW: 15, maxW: 78, minH: 8, maxH: 30 },
{ i: 'History', w: 8, h: 10, x: 11, y: 16, minW: 6, maxW: 22, minH: 9, maxH: 25 },
{ i: 'Trade', w: 15, h: 16, x: 52, y: 0, minW: 6, maxW: 22, minH: 9, maxH: 25 },
{ i: 'Settings', w: 11, h: 15, x: 0, y: 16, minW: 6, maxW: 22, minH: 9, maxH: 25 },
{ i: 'Other', w: 23, h: 5, x: 11, y: 26, minW: 2, maxW: 50, minH: 4, maxH: 25 },
];
const Main = (): JSX.Element => {
const {
book,
fetchBook,
maker,
setMaker,
setSettings,
clearOrder,
torStatus,
settings,
limits,
fetchLimits,
robot,
setRobot,
fetchRobot,
setOrder,
setDelay,
info,
fav,
setFav,
baseUrl,
order,
page,
setPage,
currentOrder,
setCurrentOrder,
open,
setOpen,
windowSize,
badOrder,
setBadOrder,
} = useContext<AppContextProps>(AppContext);
const theme = useTheme();
const em: number = theme.typography.fontSize;
const toolbarHeight: number = 3;
const gridCellSize: number = 2;
const defaultLayout: Layout = [
{ i: 'Maker', w: 10, h: 16, x: 67, y: 0, minW: 8, maxW: 22, minH: 10, maxH: 28 },
{ i: 'Book', w: 43, h: 15, x: 34, y: 16, minW: 6, maxW: 70, minH: 9, maxH: 25 },
{ i: 'DepthChart', w: 15, h: 10, x: 19, y: 16, minW: 6, maxW: 22, minH: 9, maxH: 25 },
{ i: 'Garage', w: 52, h: 16, x: 0, y: 0, minW: 15, maxW: 78, minH: 8, maxH: 30 },
{ i: 'History', w: 10, h: 10, x: 9, y: 16, minW: 6, maxW: 22, minH: 9, maxH: 25 },
{ i: 'Trade', w: 15, h: 16, x: 52, y: 0, minW: 6, maxW: 22, minH: 9, maxH: 25 },
{ i: 'Settings', w: 9, h: 15, x: 0, y: 16, minW: 6, maxW: 22, minH: 9, maxH: 25 },
{ i: 'Other', w: 25, h: 5, x: 9, y: 26, minW: 2, maxW: 50, minH: 4, maxH: 25 },
];
// All app data structured
const [book, setBook] = useState<Book>({ orders: [], loading: true });
const [limits, setLimits] = useState<{ list: LimitList; loading: boolean }>({
list: [],
loading: true,
});
const [robot, setRobot] = useState<Robot>(new Robot());
const [maker, setMaker] = useState<Maker>(defaultMaker);
const [info, setInfo] = useState<Info>(defaultInfo);
const [coordinators, setCoordinators] = useState<Coordinator[]>(defaultCoordinators);
const [fav, setFav] = useState<Favorites>({ type: null, currency: 0 });
const [baseUrl, setBaseUrl] = useState<string>('');
const [openLanding, setOpenLanding] = useState<boolean>(true);
const [layout, setLayout] = useState<Layout>(defaultLayout);
const [openLanding, setOpenLanding] = useState<boolean>(true);
const [windowSize, setWindowSize] = useState<{ width: number; height: number }>(
getWindowSize(em),
);
useEffect(() => {
if (typeof window !== undefined) {
window.addEventListener('resize', onResize);
}
if (baseUrl != '') {
fetchBook();
fetchLimits();
}
return () => {
if (typeof window !== undefined) {
window.removeEventListener('resize', onResize);
}
};
}, [baseUrl]);
useEffect(() => {
let host = '';
if (window.NativeRobosats === undefined) {
host = getHost();
} else {
host =
settings.network === 'mainnet'
? coordinators[0].mainnetOnion
: coordinators[0].testnetOnion;
}
setBaseUrl(`http://${host}`);
}, [settings.network]);
useEffect(() => {
setWindowSize(getWindowSize(theme.typography.fontSize));
}, [theme.typography.fontSize]);
const onResize = function () {
setWindowSize(getWindowSize(em));
};
useEffect(() => {
setWindowSize(getWindowSize(theme.typography.fontSize));
}, [theme.typography.fontSize]);
const fetchLimits = async () => {
setLimits({ ...limits, loading: true });
const data = apiClient.get(baseUrl, '/api/limits/').then((data) => {
setLimits({ list: data ?? [], loading: false });
return data;
});
return await data;
};
const fetchBook = function () {
setBook({ ...book, loading: true });
apiClient.get(baseUrl, '/api/book/').then((data: any) =>
setBook({
loading: false,
orders: data.not_found ? [] : data,
}),
);
};
const fetchInfo = function () {
apiClient.get(baseUrl, '/api/info/').then((data: any) => {
const versionInfo: any = checkVer(data.version.major, data.version.minor, data.version.patch);
setInfo({
...data,
openUpdateClient: versionInfo.updateAvailable,
coordinatorVersion: versionInfo.coordinatorVersion,
clientVersion: versionInfo.clientVersion,
});
});
};
console.log(layout);
return (
<Grid container direction='column' sx={{ width: `${windowSize.width}em` }}>
<Grid item>
@ -191,41 +106,16 @@ const Main = ({ settings, setSettings }: MainProps): JSX.Element => {
onLayoutChange={(layout: Layout) => setLayout(layout)}
>
<div key='Maker'>
<MakerWidget
baseUrl={baseUrl}
limits={limits}
fetchLimits={fetchLimits}
fav={fav}
setFav={setFav}
maker={maker}
setMaker={setMaker}
/>
<MakerWidget />
</div>
<div key='Book'>
<BookWidget
baseUrl={baseUrl}
book={book}
layout={layout[1]}
gridCellSize={gridCellSize}
fetchBook={fetchBook}
fav={fav}
setFav={setFav}
windowSize={windowSize}
/>
<BookWidget layout={layout[1]} gridCellSize={gridCellSize} />
</div>
<div key='DepthChart'>
<DepthChartWidget
baseUrl={baseUrl}
orders={book.orders}
gridCellSize={gridCellSize}
limitList={limits.list}
layout={layout[2]}
currency={fav.currency}
windowSize={windowSize}
/>
<DepthChartWidget gridCellSize={gridCellSize} layout={layout[2]} />
</div>
<div key='Settings'>
<SettingsWidget settings={settings} setSettings={setSettings} />
<SettingsWidget />
</div>
<div key='Garage'>
<PlaceholderWidget label='Robot Garage' />

View File

@ -1,7 +1,8 @@
import React from 'react';
import React, { useContext } from 'react';
import { AppContext, AppContextProps } from '../../contexts/AppContext';
import { Book, Favorites } from '../../models';
import { Paper, useTheme } from '@mui/material';
import { Paper } from '@mui/material';
import BookTable from '../../components/BookTable';
interface BookWidgetProps {
@ -24,13 +25,7 @@ const BookWidget = React.forwardRef(
(
{
layout,
baseUrl,
gridCellSize = 2,
book,
fetchBook,
fav,
setFav,
windowSize,
style,
className,
onMouseDown,
@ -39,24 +34,18 @@ const BookWidget = React.forwardRef(
}: BookWidgetProps,
ref,
) => {
const theme = useTheme();
const { book, windowSize, fav } = useContext<AppContextProps>(AppContext);
return React.useMemo(() => {
return (
<Paper elevation={3} style={{ width: '100%', height: '100%' }}>
<BookTable
baseUrl={baseUrl}
elevation={0}
clickRefresh={() => fetchBook()}
book={book}
fav={fav}
fillContainer={true}
maxWidth={layout.w * gridCellSize} // EM units
maxHeight={layout.h * gridCellSize} // EM units
fullWidth={windowSize.width} // EM units
fullHeight={windowSize.height} // EM units
defaultFullscreen={false}
onCurrencyChange={(e) => setFav({ ...fav, currency: e.target.value })}
onTypeChange={(mouseEvent, val) => setFav({ ...fav, type: val })}
/>
</Paper>
);

View File

@ -1,22 +1,16 @@
import React from 'react';
import { Order, LimitList } from '../../models';
import React, { useContext } from 'react';
import { AppContext, AppContextProps } from '../../contexts/AppContext';
import { Paper, useTheme } from '@mui/material';
import DepthChart from '../../components/Charts/DepthChart';
interface DepthChartWidgetProps {
layout: any;
gridCellSize: number;
orders: PublicOrder[];
currency: number;
limitList: LimitList;
windowSize: { width: number; height: number };
style?: Object;
className?: string;
onMouseDown?: () => void;
onMouseUp?: () => void;
onTouchEnd?: () => void;
baseUrl: string;
}
const DepthChartWidget = React.forwardRef(
@ -24,11 +18,6 @@ const DepthChartWidget = React.forwardRef(
{
layout,
gridCellSize,
limitList,
orders,
baseUrl,
currency,
windowSize,
style,
className,
onMouseDown,
@ -38,22 +27,19 @@ const DepthChartWidget = React.forwardRef(
ref,
) => {
const theme = useTheme();
const { fav, book, limits } = useContext<AppContextProps>(AppContext);
return React.useMemo(() => {
return (
<Paper elevation={3} style={{ width: '100%', height: '100%' }}>
<DepthChart
baseUrl={baseUrl}
elevation={0}
orders={orders}
currency={currency}
limits={limitList}
maxWidth={layout.w * gridCellSize} // EM units
maxHeight={layout.h * gridCellSize} // EM units
fillContainer={true}
/>
</Paper>
);
}, [currency, orders, limitList, layout]);
}, [fav.currency, book, limits, layout]);
},
);

View File

@ -1,17 +1,11 @@
import React from 'react';
import React, { useContext } from 'react';
import { AppContext, AppContextProps } from '../../contexts/AppContext';
import MakerForm from '../../components/MakerForm';
import { LimitList, Maker, Favorites } from '../../models';
import { Paper } from '@mui/material';
interface MakerWidgetProps {
limits: { list: LimitList; loading: boolean };
fetchLimits: () => void;
fav: Favorites;
maker: Maker;
setFav: (state: Favorites) => void;
setMaker: (state: Maker) => void;
baseUrl: string;
style?: Object;
className?: string;
onMouseDown?: () => void;
@ -20,38 +14,15 @@ interface MakerWidgetProps {
}
const MakerWidget = React.forwardRef(
(
{
maker,
setMaker,
limits,
fetchLimits,
fav,
setFav,
baseUrl,
style,
className,
onMouseDown,
onMouseUp,
onTouchEnd,
}: MakerWidgetProps,
ref,
) => {
({ style, className, onMouseDown, onMouseUp, onTouchEnd }: MakerWidgetProps, ref) => {
const { maker, fav, limits } = useContext<AppContextProps>(AppContext);
return React.useMemo(() => {
return (
<Paper
elevation={3}
style={{ padding: 8, overflow: 'auto', width: '100%', height: '100%' }}
>
<MakerForm
baseUrl={baseUrl}
limits={limits}
fetchLimits={fetchLimits}
maker={maker}
setMaker={setMaker}
fav={fav}
setFav={setFav}
/>
<MakerForm />
</Paper>
);
}, [maker, limits, fav]);

View File

@ -1,12 +1,10 @@
import React from 'react';
import React, { useContext } from 'react';
import { AppContextProps, AppContext } from '../../contexts/AppContext';
import { Settings } from '../../models';
import { Paper, useTheme } from '@mui/material';
import { Paper } from '@mui/material';
import SettingsForm from '../../components/SettingsForm';
interface SettingsWidgetProps {
settings: Settings;
setSettings: (state: Settings) => void;
style?: Object;
className?: string;
onMouseDown?: () => void;
@ -15,26 +13,15 @@ interface SettingsWidgetProps {
}
const SettingsWidget = React.forwardRef(
(
{
settings,
setSettings,
style,
className,
onMouseDown,
onMouseUp,
onTouchEnd,
}: SettingsWidgetProps,
ref,
) => {
const theme = useTheme();
({ style, className, onMouseDown, onMouseUp, onTouchEnd }: SettingsWidgetProps, ref) => {
const { settings } = useContext<AppContextProps>(AppContext);
return React.useMemo(() => {
return (
<Paper
elevation={3}
style={{ width: '100%', height: '100%', position: 'relative', top: '0.6em', left: '0em' }}
>
<SettingsForm dense={true} settings={settings} setSettings={setSettings} />
<SettingsForm dense={true} />
</Paper>
);
}, [settings]);

View File

@ -41,7 +41,8 @@
"Connection encrypted and anonymized using TOR.": "Connexió xifrada i anònima mitjançant TOR.",
"This ensures maximum privacy, however you might feel the app behaves slow. If connection is lost, restart the app.": "Això garanteix la màxima privadesa, però és possible que sentis que l'aplicació es comporta lenta. Si es perd la connexió, reinicia l'aplicació.",
"#7": "Phrases in basic/RobotPage/Recovery.tsx",
"Please, introduce your robot token to re-build your robot and gain access to its trades.": "Si us plau, introdueix el teu token per reconstruir el teu robot i accedir a les seves operacions.",
"Robot recovery": "Robot recovery",
"Enter your robot token to re-build your robot and gain access to its trades.": "Introdueix el teu token per reconstruir el teu robot i accedir a les seves operacions.",
"Paste token here": "Enganxa el token aquí",
"Recover": "Recuperar",
"#8": "Phrases in basic/RobotPage/Welcome.tsx",

View File

@ -41,7 +41,8 @@
"Connection encrypted and anonymized using TOR.": "Connection encrypted and anonymized using TOR.",
"This ensures maximum privacy, however you might feel the app behaves slow. If connection is lost, restart the app.": "This ensures maximum privacy, however you might feel the app behaves slow. If connection is lost, restart the app.",
"#7": "Phrases in basic/RobotPage/Recovery.tsx",
"Please, introduce your robot token to re-build your robot and gain access to its trades.": "Please, introduce your robot token to re-build your robot and gain access to its trades.",
"Robot recovery": "Robot recovery",
"Enter your robot token to re-build your robot and gain access to its trades.": "Enter your robot token to re-build your robot and gain access to its trades.",
"Paste token here": "Paste token here",
"Recover": "Recover",
"#8": "Phrases in basic/RobotPage/Welcome.tsx",

View File

@ -41,7 +41,8 @@
"Connection encrypted and anonymized using TOR.": "Connection encrypted and anonymized using TOR.",
"This ensures maximum privacy, however you might feel the app behaves slow. If connection is lost, restart the app.": "This ensures maximum privacy, however you might feel the app behaves slow. If connection is lost, restart the app.",
"#7": "Phrases in basic/RobotPage/Recovery.tsx",
"Please, introduce your robot token to re-build your robot and gain access to its trades.": "Please, introduce your robot token to re-build your robot and gain access to its trades.",
"Robot recovery": "Robot recovery",
"Enter your robot token to re-build your robot and gain access to its trades.": "Enter your robot token to re-build your robot and gain access to its trades.",
"Paste token here": "Paste token here",
"Recover": "Recover",
"#8": "Phrases in basic/RobotPage/Welcome.tsx",

View File

@ -41,7 +41,8 @@
"Connection encrypted and anonymized using TOR.": "Connection encrypted and anonymized using TOR.",
"This ensures maximum privacy, however you might feel the app behaves slow. If connection is lost, restart the app.": "This ensures maximum privacy, however you might feel the app behaves slow. If connection is lost, restart the app.",
"#7": "Phrases in basic/RobotPage/Recovery.tsx",
"Please, introduce your robot token to re-build your robot and gain access to its trades.": "Please, introduce your robot token to re-build your robot and gain access to its trades.",
"Robot recovery": "Robot recovery",
"Enter your robot token to re-build your robot and gain access to its trades.": "Enter your robot token to re-build your robot and gain access to its trades.",
"Paste token here": "Paste token here",
"Recover": "Recover",
"#8": "Phrases in basic/RobotPage/Welcome.tsx",

View File

@ -41,7 +41,8 @@
"Connection encrypted and anonymized using TOR.": "Connection encrypted and anonymized using TOR.",
"This ensures maximum privacy, however you might feel the app behaves slow. If connection is lost, restart the app.": "This ensures maximum privacy, however you might feel the app behaves slow. If connection is lost, restart the app.",
"#7": "Phrases in basic/RobotPage/Recovery.tsx",
"Please, introduce your robot token to re-build your robot and gain access to its trades.": "Please, introduce your robot token to re-build your robot and gain access to its trades.",
"Robot recovery": "Robot recovery",
"Enter your robot token to re-build your robot and gain access to its trades.": "Enter your robot token to re-build your robot and gain access to its trades.",
"Paste token here": "Paste token here",
"Recover": "Recover",
"#8": "Phrases in basic/RobotPage/Welcome.tsx",

View File

@ -41,7 +41,8 @@
"Connection encrypted and anonymized using TOR.": "Connection encrypted and anonymized using TOR.",
"This ensures maximum privacy, however you might feel the app behaves slow. If connection is lost, restart the app.": "This ensures maximum privacy, however you might feel the app behaves slow. If connection is lost, restart the app.",
"#7": "Phrases in basic/RobotPage/Recovery.tsx",
"Please, introduce your robot token to re-build your robot and gain access to its trades.": "Please, introduce your robot token to re-build your robot and gain access to its trades.",
"Robot recovery": "Robot recovery",
"Enter your robot token to re-build your robot and gain access to its trades.": "Enter your robot token to re-build your robot and gain access to its trades.",
"Paste token here": "Paste token here",
"Recover": "Recover",
"#8": "Phrases in basic/RobotPage/Welcome.tsx",

View File

@ -41,7 +41,8 @@
"Connection encrypted and anonymized using TOR.": "Connection encrypted and anonymized using TOR.",
"This ensures maximum privacy, however you might feel the app behaves slow. If connection is lost, restart the app.": "This ensures maximum privacy, however you might feel the app behaves slow. If connection is lost, restart the app.",
"#7": "Phrases in basic/RobotPage/Recovery.tsx",
"Please, introduce your robot token to re-build your robot and gain access to its trades.": "Please, introduce your robot token to re-build your robot and gain access to its trades.",
"Robot recovery": "Robot recovery",
"Enter your robot token to re-build your robot and gain access to its trades.": "Enter your robot token to re-build your robot and gain access to its trades.",
"Paste token here": "Paste token here",
"Recover": "Recover",
"#8": "Phrases in basic/RobotPage/Welcome.tsx",

View File

@ -41,7 +41,8 @@
"Connection encrypted and anonymized using TOR.": "Connection encrypted and anonymized using TOR.",
"This ensures maximum privacy, however you might feel the app behaves slow. If connection is lost, restart the app.": "This ensures maximum privacy, however you might feel the app behaves slow. If connection is lost, restart the app.",
"#7": "Phrases in basic/RobotPage/Recovery.tsx",
"Please, introduce your robot token to re-build your robot and gain access to its trades.": "Please, introduce your robot token to re-build your robot and gain access to its trades.",
"Robot recovery": "Robot recovery",
"Enter your robot token to re-build your robot and gain access to its trades.": "Enter your robot token to re-build your robot and gain access to its trades.",
"Paste token here": "Paste token here",
"Recover": "Recover",
"#8": "Phrases in basic/RobotPage/Welcome.tsx",

View File

@ -41,7 +41,8 @@
"Connection encrypted and anonymized using TOR.": "Connection encrypted and anonymized using TOR.",
"This ensures maximum privacy, however you might feel the app behaves slow. If connection is lost, restart the app.": "This ensures maximum privacy, however you might feel the app behaves slow. If connection is lost, restart the app.",
"#7": "Phrases in basic/RobotPage/Recovery.tsx",
"Please, introduce your robot token to re-build your robot and gain access to its trades.": "Please, introduce your robot token to re-build your robot and gain access to its trades.",
"Robot recovery": "Robot recovery",
"Enter your robot token to re-build your robot and gain access to its trades.": "Enter your robot token to re-build your robot and gain access to its trades.",
"Paste token here": "Paste token here",
"Recover": "Recover",
"#8": "Phrases in basic/RobotPage/Welcome.tsx",

View File

@ -41,7 +41,8 @@
"Connection encrypted and anonymized using TOR.": "Connection encrypted and anonymized using TOR.",
"This ensures maximum privacy, however you might feel the app behaves slow. If connection is lost, restart the app.": "This ensures maximum privacy, however you might feel the app behaves slow. If connection is lost, restart the app.",
"#7": "Phrases in basic/RobotPage/Recovery.tsx",
"Please, introduce your robot token to re-build your robot and gain access to its trades.": "Please, introduce your robot token to re-build your robot and gain access to its trades.",
"Robot recovery": "Robot recovery",
"Enter your robot token to re-build your robot and gain access to its trades.": "Enter your robot token to re-build your robot and gain access to its trades.",
"Paste token here": "Paste token here",
"Recover": "Recover",
"#8": "Phrases in basic/RobotPage/Welcome.tsx",

View File

@ -41,7 +41,8 @@
"Connection encrypted and anonymized using TOR.": "Connection encrypted and anonymized using TOR.",
"This ensures maximum privacy, however you might feel the app behaves slow. If connection is lost, restart the app.": "This ensures maximum privacy, however you might feel the app behaves slow. If connection is lost, restart the app.",
"#7": "Phrases in basic/RobotPage/Recovery.tsx",
"Please, introduce your robot token to re-build your robot and gain access to its trades.": "Please, introduce your robot token to re-build your robot and gain access to its trades.",
"Robot recovery": "Robot recovery",
"Enter your robot token to re-build your robot and gain access to its trades.": "Enter your robot token to re-build your robot and gain access to its trades.",
"Paste token here": "Paste token here",
"Recover": "Recover",
"#8": "Phrases in basic/RobotPage/Welcome.tsx",

View File

@ -41,7 +41,8 @@
"Connection encrypted and anonymized using TOR.": "Connection encrypted and anonymized using TOR.",
"This ensures maximum privacy, however you might feel the app behaves slow. If connection is lost, restart the app.": "This ensures maximum privacy, however you might feel the app behaves slow. If connection is lost, restart the app.",
"#7": "Phrases in basic/RobotPage/Recovery.tsx",
"Please, introduce your robot token to re-build your robot and gain access to its trades.": "Please, introduce your robot token to re-build your robot and gain access to its trades.",
"Robot recovery": "Robot recovery",
"Enter your robot token to re-build your robot and gain access to its trades.": "Enter your robot token to re-build your robot and gain access to its trades.",
"Paste token here": "Paste token here",
"Recover": "Recover",
"#8": "Phrases in basic/RobotPage/Welcome.tsx",

View File

@ -41,7 +41,8 @@
"Connection encrypted and anonymized using TOR.": "Connection encrypted and anonymized using TOR.",
"This ensures maximum privacy, however you might feel the app behaves slow. If connection is lost, restart the app.": "This ensures maximum privacy, however you might feel the app behaves slow. If connection is lost, restart the app.",
"#7": "Phrases in basic/RobotPage/Recovery.tsx",
"Please, introduce your robot token to re-build your robot and gain access to its trades.": "Please, introduce your robot token to re-build your robot and gain access to its trades.",
"Robot recovery": "Robot recovery",
"Enter your robot token to re-build your robot and gain access to its trades.": "Enter your robot token to re-build your robot and gain access to its trades.",
"Paste token here": "Paste token here",
"Recover": "Recover",
"#8": "Phrases in basic/RobotPage/Welcome.tsx",

View File

@ -41,7 +41,8 @@
"Connection encrypted and anonymized using TOR.": "Connection encrypted and anonymized using TOR.",
"This ensures maximum privacy, however you might feel the app behaves slow. If connection is lost, restart the app.": "This ensures maximum privacy, however you might feel the app behaves slow. If connection is lost, restart the app.",
"#7": "Phrases in basic/RobotPage/Recovery.tsx",
"Please, introduce your robot token to re-build your robot and gain access to its trades.": "Please, introduce your robot token to re-build your robot and gain access to its trades.",
"Robot recovery": "Robot recovery",
"Enter your robot token to re-build your robot and gain access to its trades.": "Enter your robot token to re-build your robot and gain access to its trades.",
"Paste token here": "Paste token here",
"Recover": "Recover",
"#8": "Phrases in basic/RobotPage/Welcome.tsx",

View File

@ -41,7 +41,8 @@
"Connection encrypted and anonymized using TOR.": "Connection encrypted and anonymized using TOR.",
"This ensures maximum privacy, however you might feel the app behaves slow. If connection is lost, restart the app.": "This ensures maximum privacy, however you might feel the app behaves slow. If connection is lost, restart the app.",
"#7": "Phrases in basic/RobotPage/Recovery.tsx",
"Please, introduce your robot token to re-build your robot and gain access to its trades.": "Please, introduce your robot token to re-build your robot and gain access to its trades.",
"Robot recovery": "Robot recovery",
"Enter your robot token to re-build your robot and gain access to its trades.": "Enter your robot token to re-build your robot and gain access to its trades.",
"Paste token here": "Paste token here",
"Recover": "Recover",
"#8": "Phrases in basic/RobotPage/Welcome.tsx",