mirror of
https://github.com/RoboSats/robosats.git
synced 2024-09-11 14:05:51 +03:00
Add Nav Bar, Settings Page, large refactor (#308)
commita5b63aed93
Author: Reckless_Satoshi <reckless.satoshi@protonmail.com> Date: Sun Oct 30 10:46:05 2022 -0700 Small fixes commitd64adfc2bf
Author: Reckless_Satoshi <reckless.satoshi@protonmail.com> Date: Sun Oct 30 06:02:06 2022 -0700 wip work on federation settings commitca35d6b3d2
Author: Reckless_Satoshi <reckless.satoshi@protonmail.com> Date: Sun Oct 30 04:05:33 2022 -0700 Refactor confirmation Dialogs commitc660a5b0d1
Author: Reckless_Satoshi <reckless.satoshi@protonmail.com> Date: Sat Oct 29 13:36:59 2022 -0700 refactor login (clean separation robot/info. Style navbar. commitb9dc7f7c95
Author: Reckless_Satoshi <reckless.satoshi@protonmail.com> Date: Fri Oct 28 09:54:38 2022 -0700 Add size slider and settings widget commit20b2b3dcd6
Author: Reckless_Satoshi <reckless.satoshi@protonmail.com> Date: Fri Oct 28 05:41:48 2022 -0700 Add show more and Dialogs commitda8b70091b
Author: Reckless_Satoshi <reckless.satoshi@protonmail.com> Date: Thu Oct 27 16:26:07 2022 -0700 Add sliding pages commit6dd90aa118
Author: Reckless_Satoshi <reckless.satoshi@protonmail.com> Date: Thu Oct 27 06:34:58 2022 -0700 Add settings forms commitd3d0f3ee1a
Author: Reckless_Satoshi <reckless.satoshi@protonmail.com> Date: Wed Oct 26 04:16:06 2022 -0700 Refactor utils
This commit is contained in:
parent
5e6f7165d7
commit
227610c84a
@ -534,22 +534,23 @@ class UserGenSerializer(serializers.Serializer):
|
||||
required=True,
|
||||
help_text="SHA256 of user secret",
|
||||
)
|
||||
# Optional fields
|
||||
# (PGP keys are mandatory for new users, but optional for logins)
|
||||
public_key = serializers.CharField(
|
||||
max_length=2000,
|
||||
allow_null=False,
|
||||
allow_blank=False,
|
||||
required=True,
|
||||
required=False,
|
||||
help_text="Armored ASCII PGP public key block",
|
||||
)
|
||||
encrypted_private_key = serializers.CharField(
|
||||
max_length=2000,
|
||||
allow_null=False,
|
||||
allow_blank=False,
|
||||
required=True,
|
||||
required=False,
|
||||
help_text="Armored ASCII PGP encrypted private key block",
|
||||
)
|
||||
|
||||
# Optional fields
|
||||
ref_code = serializers.CharField(
|
||||
max_length=30,
|
||||
allow_null=True,
|
||||
|
72
api/views.py
72
api/views.py
@ -632,20 +632,22 @@ class UserView(APIView):
|
||||
context = {"bad_request": "Invalid serializer"}
|
||||
return Response(context, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# Deprecated
|
||||
#
|
||||
# If an existing user opens the main page by mistake, we do not want it to create a new nickname/profile for him
|
||||
if request.user.is_authenticated:
|
||||
context = {"nickname": request.user.username}
|
||||
not_participant, _, order = Logics.validate_already_maker_or_taker(
|
||||
request.user
|
||||
)
|
||||
# if request.user.is_authenticated:
|
||||
# context = {"nickname": request.user.username}
|
||||
# not_participant, _, order = Logics.validate_already_maker_or_taker(
|
||||
# request.user
|
||||
# )
|
||||
|
||||
# Does not allow this 'mistake' if an active order
|
||||
if not not_participant:
|
||||
context["active_order_id"] = order.id
|
||||
context[
|
||||
"bad_request"
|
||||
] = f"You are already logged in as {request.user} and have an active order"
|
||||
return Response(context, status.HTTP_400_BAD_REQUEST)
|
||||
# # Does not allow this 'mistake' if an active order
|
||||
# if not not_participant:
|
||||
# context["active_order_id"] = order.id
|
||||
# context[
|
||||
# "bad_request"
|
||||
# ] = f"You are already logged in as {request.user} and have an active order"
|
||||
# return Response(context, status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# The new way. The token is never sent. Only its SHA256
|
||||
token_sha256 = serializer.data.get("token_sha256")
|
||||
@ -653,19 +655,6 @@ class UserView(APIView):
|
||||
encrypted_private_key = serializer.data.get("encrypted_private_key")
|
||||
ref_code = serializer.data.get("ref_code")
|
||||
|
||||
if not public_key or not encrypted_private_key:
|
||||
context["bad_request"] = "Must provide valid 'pub' and 'enc_priv' PGP keys"
|
||||
return Response(context, status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
(
|
||||
valid,
|
||||
bad_keys_context,
|
||||
public_key,
|
||||
encrypted_private_key,
|
||||
) = Logics.validate_pgp_keys(public_key, encrypted_private_key)
|
||||
if not valid:
|
||||
return Response(bad_keys_context, status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# Now the server only receives a hash of the token. So server trusts the client
|
||||
# with computing length, counts and unique_values to confirm the high entropy of the token
|
||||
# In any case, it is up to the client if they want to create a bad high entropy token.
|
||||
@ -712,6 +701,20 @@ class UserView(APIView):
|
||||
|
||||
# Create new credentials and login if nickname is new
|
||||
if len(User.objects.filter(username=nickname)) == 0:
|
||||
if not public_key or not encrypted_private_key:
|
||||
context[
|
||||
"bad_request"
|
||||
] = "Must provide valid 'pub' and 'enc_priv' PGP keys"
|
||||
return Response(context, status.HTTP_400_BAD_REQUEST)
|
||||
(
|
||||
valid,
|
||||
bad_keys_context,
|
||||
public_key,
|
||||
encrypted_private_key,
|
||||
) = Logics.validate_pgp_keys(public_key, encrypted_private_key)
|
||||
if not valid:
|
||||
return Response(bad_keys_context, status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
User.objects.create_user(
|
||||
username=nickname, password=token_sha256, is_staff=False
|
||||
)
|
||||
@ -931,25 +934,6 @@ class InfoView(ListAPIView):
|
||||
BalanceLog.objects.latest("time")
|
||||
)
|
||||
|
||||
if request.user.is_authenticated:
|
||||
context["nickname"] = request.user.username
|
||||
context["referral_code"] = str(request.user.profile.referral_code)
|
||||
context["earned_rewards"] = request.user.profile.earned_rewards
|
||||
context["wants_stealth"] = request.user.profile.wants_stealth
|
||||
# Adds/generate telegram token and whether it is enabled
|
||||
context = {**context, **Telegram.get_context(request.user)}
|
||||
has_no_active_order, _, order = Logics.validate_already_maker_or_taker(
|
||||
request.user
|
||||
)
|
||||
if not has_no_active_order:
|
||||
context["active_order_id"] = order.id
|
||||
else:
|
||||
last_order = Order.objects.filter(
|
||||
Q(maker=request.user) | Q(taker=request.user)
|
||||
).last()
|
||||
if last_order:
|
||||
context["last_order_id"] = last_order.id
|
||||
|
||||
return Response(context, status.HTTP_200_OK)
|
||||
|
||||
|
||||
|
@ -42,7 +42,11 @@ const App = (): JSX.Element => {
|
||||
|
||||
useEffect(() => {
|
||||
updateTheme();
|
||||
}, [settings]);
|
||||
}, [settings.fontSize, settings.mode]);
|
||||
|
||||
useEffect(() => {
|
||||
i18n.changeLanguage(settings.language);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Suspense fallback='loading language'>
|
||||
|
@ -1,16 +1,17 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button, Typography, Grid, ButtonGroup, Dialog, Box } from '@mui/material';
|
||||
import { Button, Grid, ButtonGroup, Dialog, Box } from '@mui/material';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import currencyDict from '../../../static/assets/currencies.json';
|
||||
import DepthChart from '../../components/Charts/DepthChart';
|
||||
|
||||
import { NoRobotDialog } from '../../components/Dialogs';
|
||||
import MakerForm from '../../components/MakerForm';
|
||||
import BookTable from '../../components/BookTable';
|
||||
import { Page } from '../NavBar';
|
||||
import { Book, Favorites, LimitList, Maker } from '../../models';
|
||||
|
||||
// Icons
|
||||
import { BarChart, FormatListBulleted } from '@mui/icons-material';
|
||||
import MakerForm from '../../components/MakerForm';
|
||||
import BookTable from '../../components/BookTable';
|
||||
|
||||
interface BookPageProps {
|
||||
book: Book;
|
||||
@ -23,6 +24,9 @@ interface BookPageProps {
|
||||
lastDayPremium: number;
|
||||
maker: Maker;
|
||||
setMaker: (state: Maker) => void;
|
||||
hasRobot: boolean;
|
||||
setPage: (state: Page) => void;
|
||||
setOrder: (state: number) => void;
|
||||
}
|
||||
|
||||
const BookPage = ({
|
||||
@ -36,36 +40,21 @@ const BookPage = ({
|
||||
maker,
|
||||
setMaker,
|
||||
windowSize,
|
||||
hasRobot = false,
|
||||
setPage = () => null,
|
||||
setOrder = () => null,
|
||||
}: BookPageProps): JSX.Element => {
|
||||
const { t } = useTranslation();
|
||||
const history = useHistory();
|
||||
const [view, setView] = useState<'list' | 'depth'>('list');
|
||||
const [openMaker, setOpenMaker] = useState<boolean>(false);
|
||||
const [openNoRobot, setOpenNoRobot] = useState<boolean>(false);
|
||||
|
||||
const doubleView = windowSize.width > 115;
|
||||
const width = windowSize.width * 0.9;
|
||||
const maxBookTableWidth = 85;
|
||||
const chartWidthEm = width - maxBookTableWidth;
|
||||
|
||||
const defaultMaker: Maker = {
|
||||
isExplicit: false,
|
||||
amount: '',
|
||||
paymentMethods: [],
|
||||
paymentMethodsText: 'not specified',
|
||||
badPaymentMethod: false,
|
||||
premium: '',
|
||||
satoshis: '',
|
||||
publicExpiryTime: new Date(0, 0, 0, 23, 59),
|
||||
publicDuration: 86340,
|
||||
escrowExpiryTime: new Date(0, 0, 0, 3, 0),
|
||||
escrowDuration: 10800,
|
||||
bondSize: 3,
|
||||
minAmount: '',
|
||||
maxAmount: '',
|
||||
badPremiumText: '',
|
||||
badSatoshisText: '',
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (book.orders.length < 1) {
|
||||
fetchBook(true, false);
|
||||
@ -83,11 +72,21 @@ const BookPage = ({
|
||||
setFav({ ...fav, type: val });
|
||||
};
|
||||
|
||||
const onOrderClicked = function (id: number) {
|
||||
if (hasRobot) {
|
||||
history.push('/order/' + id);
|
||||
setPage('order');
|
||||
setOrder(id);
|
||||
} else {
|
||||
setOpenNoRobot(true);
|
||||
}
|
||||
};
|
||||
|
||||
const NavButtons = function () {
|
||||
return (
|
||||
<ButtonGroup variant='contained' color='inherit'>
|
||||
<Button color='primary' onClick={() => setOpenMaker(true)}>
|
||||
{t('Create Order')}
|
||||
{t('Create')}
|
||||
</Button>
|
||||
{doubleView ? (
|
||||
<></>
|
||||
@ -108,14 +107,12 @@ const BookPage = ({
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
<Button color='secondary' onClick={() => history.push('/')}>
|
||||
{t('Back')}
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
);
|
||||
};
|
||||
return (
|
||||
<Grid container direction='column' alignItems='center' spacing={1} sx={{ minWidth: 400 }}>
|
||||
<NoRobotDialog open={openNoRobot} onClose={() => setOpenNoRobot(false)} setPage={setPage} />
|
||||
{openMaker ? (
|
||||
<Dialog open={openMaker} onClose={() => setOpenMaker(false)}>
|
||||
<Box sx={{ maxWidth: '18em', padding: '0.5em' }}>
|
||||
@ -126,6 +123,8 @@ const BookPage = ({
|
||||
setMaker={setMaker}
|
||||
fav={fav}
|
||||
setFav={setFav}
|
||||
setPage={setPage}
|
||||
hasRobot={hasRobot}
|
||||
/>
|
||||
</Box>
|
||||
</Dialog>
|
||||
@ -153,6 +152,7 @@ const BookPage = ({
|
||||
defaultFullscreen={false}
|
||||
onCurrencyChange={handleCurrencyChange}
|
||||
onTypeChange={handleTypeChange}
|
||||
onOrderClicked={onOrderClicked}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
@ -187,6 +187,7 @@ const BookPage = ({
|
||||
defaultFullscreen={false}
|
||||
onCurrencyChange={handleCurrencyChange}
|
||||
onTypeChange={handleTypeChange}
|
||||
onOrderClicked={onOrderClicked}
|
||||
/>
|
||||
)}
|
||||
</Grid>
|
||||
|
@ -1,642 +0,0 @@
|
||||
import React, { Component } from 'react';
|
||||
import { withTranslation } from 'react-i18next';
|
||||
import {
|
||||
Badge,
|
||||
Tooltip,
|
||||
ListItemAvatar,
|
||||
Paper,
|
||||
Grid,
|
||||
IconButton,
|
||||
Select,
|
||||
MenuItem,
|
||||
ListItemText,
|
||||
ListItem,
|
||||
ListItemIcon,
|
||||
ListItemButton,
|
||||
} from '@mui/material';
|
||||
import MediaQuery from 'react-responsive';
|
||||
import Flags from 'country-flag-icons/react/3x2';
|
||||
import { Link as LinkRouter } from 'react-router-dom';
|
||||
import { apiClient } from '../services/api';
|
||||
import { systemClient } from '../services/System';
|
||||
import RobotAvatar from '../components/RobotAvatar';
|
||||
|
||||
// Icons
|
||||
import BarChartIcon from '@mui/icons-material/BarChart';
|
||||
import PeopleIcon from '@mui/icons-material/People';
|
||||
import InventoryIcon from '@mui/icons-material/Inventory';
|
||||
import SellIcon from '@mui/icons-material/Sell';
|
||||
import SmartToyIcon from '@mui/icons-material/SmartToy';
|
||||
import PercentIcon from '@mui/icons-material/Percent';
|
||||
import PriceChangeIcon from '@mui/icons-material/PriceChange';
|
||||
|
||||
// Missing flags
|
||||
import { CataloniaFlag, BasqueCountryFlag } from '../components/Icons';
|
||||
|
||||
import {
|
||||
CommunityDialog,
|
||||
ExchangeSummaryDialog,
|
||||
ProfileDialog,
|
||||
StatsDialog,
|
||||
UpdateClientDialog,
|
||||
} from '../components/Dialogs';
|
||||
|
||||
class BottomBar extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
profileShown: false,
|
||||
openStatsForNerds: false,
|
||||
openCommunity: false,
|
||||
openExchangeSummary: false,
|
||||
openClaimRewards: false,
|
||||
openProfile: false,
|
||||
showRewards: false,
|
||||
rewardInvoice: null,
|
||||
badInvoice: false,
|
||||
showRewardsSpinner: false,
|
||||
withdrawn: false,
|
||||
};
|
||||
}
|
||||
|
||||
handleClickOpenStatsForNerds = () => {
|
||||
this.setState({ openStatsForNerds: true });
|
||||
};
|
||||
|
||||
handleClickCloseStatsForNerds = () => {
|
||||
this.setState({ openStatsForNerds: false });
|
||||
};
|
||||
|
||||
handleClickOpenCommunity = () => {
|
||||
this.setState({ openCommunity: true });
|
||||
};
|
||||
|
||||
handleClickCloseCommunity = () => {
|
||||
this.setState({ openCommunity: false });
|
||||
};
|
||||
|
||||
handleClickOpenProfile = () => {
|
||||
this.setState({ openProfile: true, profileShown: true });
|
||||
};
|
||||
|
||||
handleClickCloseProfile = () => {
|
||||
this.setState({ openProfile: false });
|
||||
};
|
||||
|
||||
handleClickOpenExchangeSummary = () => {
|
||||
this.setState({ openExchangeSummary: true });
|
||||
};
|
||||
|
||||
handleClickCloseExchangeSummary = () => {
|
||||
this.setState({ openExchangeSummary: false });
|
||||
};
|
||||
|
||||
handleSubmitInvoiceClicked = (e, rewardInvoice) => {
|
||||
this.setState({ badInvoice: false, showRewardsSpinner: true });
|
||||
|
||||
apiClient
|
||||
.post('/api/reward/', {
|
||||
invoice: rewardInvoice,
|
||||
})
|
||||
.then((data) => {
|
||||
this.setState({ badInvoice: data.bad_invoice, showRewardsSpinner: false });
|
||||
this.props.setInfo({
|
||||
...this.props.info,
|
||||
badInvoice: data.bad_invoice,
|
||||
openClaimRewards: !data.successful_withdrawal,
|
||||
withdrawn: !!data.successful_withdrawal,
|
||||
showRewardsSpinner: false,
|
||||
});
|
||||
this.props.setRobot({
|
||||
...this.props.robot,
|
||||
earnedRewards: data.successful_withdrawal ? 0 : this.props.robot.earnedRewards,
|
||||
});
|
||||
});
|
||||
e.preventDefault();
|
||||
};
|
||||
|
||||
handleSetStealthInvoice = (wantsStealth) => {
|
||||
apiClient
|
||||
.put('/api/stealth/', { wantsStealth })
|
||||
.then((data) =>
|
||||
this.props.setRobot({ ...this.props.robot, stealthInvoices: data?.wantsStealth }),
|
||||
);
|
||||
};
|
||||
|
||||
getHost() {
|
||||
const url =
|
||||
window.location != window.parent.location
|
||||
? this.getHost(document.referrer)
|
||||
: document.location.href;
|
||||
return url.split('/')[2];
|
||||
}
|
||||
|
||||
showProfileButton = () => {
|
||||
return (
|
||||
this.props.robot.avatarLoaded &&
|
||||
(this.props.robot.token
|
||||
? systemClient.getCookie('robot_token') === this.props.robot.token
|
||||
: true) &&
|
||||
systemClient.getCookie('sessionid')
|
||||
);
|
||||
};
|
||||
|
||||
bottomBarDesktop = () => {
|
||||
const { t } = this.props;
|
||||
const hasRewards = this.props.robot.earnedRewards > 0;
|
||||
const hasOrder = !!(
|
||||
(this.props.robot.activeOrderId > 0) &
|
||||
!this.state.profileShown &
|
||||
this.props.robot.avatarLoaded
|
||||
);
|
||||
const fontSize = this.props.theme.typography.fontSize;
|
||||
const fontSizeFactor = fontSize / 14; // default fontSize is 14
|
||||
const typographyProps = {
|
||||
primaryTypographyProps: { fontSize },
|
||||
secondaryTypographyProps: { fontSize: (fontSize * 12) / 14 },
|
||||
};
|
||||
return (
|
||||
<Paper
|
||||
elevation={6}
|
||||
style={{ height: '2.5em', width: `${(this.props.windowSize.width / 16) * 14}em` }}
|
||||
>
|
||||
<Grid container>
|
||||
<Grid item xs={1.9}>
|
||||
<div style={{ display: this.showProfileButton() ? '' : 'none' }}>
|
||||
<ListItemButton onClick={this.handleClickOpenProfile}>
|
||||
<Tooltip
|
||||
open={(hasRewards || hasOrder) && this.showProfileButton()}
|
||||
title={
|
||||
(hasRewards ? t('You can claim satoshis!') + ' ' : '') +
|
||||
(hasOrder ? t('You have an active order') : '')
|
||||
}
|
||||
>
|
||||
<ListItemAvatar sx={{ width: 30 * fontSizeFactor, height: 30 * fontSizeFactor }}>
|
||||
<RobotAvatar
|
||||
style={{ marginTop: -13 }}
|
||||
statusColor={
|
||||
(this.props.robot.activeOrderId > 0) & !this.state.profileShown
|
||||
? 'primary'
|
||||
: undefined
|
||||
}
|
||||
nickname={this.props.robot.nickname}
|
||||
onLoad={() =>
|
||||
this.props.setRobot({ ...this.props.robot, avatarLoaded: true })
|
||||
}
|
||||
/>
|
||||
</ListItemAvatar>
|
||||
</Tooltip>
|
||||
<ListItemText primary={this.props.robot.nickname} />
|
||||
</ListItemButton>
|
||||
</div>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={1.9}>
|
||||
<ListItem className='bottomItem'>
|
||||
<ListItemIcon size='small'>
|
||||
<IconButton
|
||||
disabled={!this.showProfileButton()}
|
||||
color='primary'
|
||||
to={`/book/`}
|
||||
component={LinkRouter}
|
||||
>
|
||||
<InventoryIcon />
|
||||
</IconButton>
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
{...typographyProps}
|
||||
primary={this.props.info.num_public_buy_orders}
|
||||
secondary={t('Public Buy Orders')}
|
||||
/>
|
||||
</ListItem>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={1.9}>
|
||||
<ListItem className='bottomItem'>
|
||||
<ListItemIcon size='small'>
|
||||
<IconButton
|
||||
disabled={!this.showProfileButton()}
|
||||
color='primary'
|
||||
to={`/book/`}
|
||||
component={LinkRouter}
|
||||
>
|
||||
<SellIcon />
|
||||
</IconButton>
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
{...typographyProps}
|
||||
primary={this.props.info.num_public_sell_orders}
|
||||
secondary={t('Public Sell Orders')}
|
||||
/>
|
||||
</ListItem>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={1.9}>
|
||||
<ListItem className='bottomItem'>
|
||||
<ListItemIcon size='small'>
|
||||
<IconButton
|
||||
disabled={!this.showProfileButton()}
|
||||
color='primary'
|
||||
to={`/`}
|
||||
component={LinkRouter}
|
||||
>
|
||||
<SmartToyIcon />
|
||||
</IconButton>
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
{...typographyProps}
|
||||
primary={this.props.info.active_robots_today}
|
||||
secondary={t('Today Active Robots')}
|
||||
/>
|
||||
</ListItem>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={1.9}>
|
||||
<ListItem className='bottomItem'>
|
||||
<ListItemIcon size='small'>
|
||||
<IconButton color='primary' onClick={this.handleClickOpenExchangeSummary}>
|
||||
<PriceChangeIcon />
|
||||
</IconButton>
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
{...typographyProps}
|
||||
primary={this.props.info.last_day_nonkyc_btc_premium + '%'}
|
||||
secondary={t('24h Avg Premium')}
|
||||
/>
|
||||
</ListItem>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={1.5}>
|
||||
<ListItem className='bottomItem'>
|
||||
<ListItemIcon size='small'>
|
||||
<IconButton color='primary' onClick={this.handleClickOpenExchangeSummary}>
|
||||
<PercentIcon />
|
||||
</IconButton>
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
{...typographyProps}
|
||||
primary={(this.props.info.maker_fee + this.props.info.taker_fee) * 100}
|
||||
secondary={t('Trade Fee')}
|
||||
/>
|
||||
</ListItem>
|
||||
</Grid>
|
||||
|
||||
<Grid container item xs={1}>
|
||||
<Grid item xs={6}>
|
||||
{this.LangSelect()}
|
||||
</Grid>
|
||||
<Grid item xs={3}>
|
||||
<Tooltip enterTouchDelay={250} title={t('Show community and support links')}>
|
||||
<IconButton
|
||||
color='primary'
|
||||
aria-label='Community'
|
||||
onClick={this.handleClickOpenCommunity}
|
||||
>
|
||||
<PeopleIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Grid>
|
||||
<Grid item xs={3}>
|
||||
<Tooltip enterTouchDelay={250} title={t('Show stats for nerds')}>
|
||||
<IconButton
|
||||
color='primary'
|
||||
aria-label='Stats for Nerds'
|
||||
onClick={this.handleClickOpenStatsForNerds}
|
||||
>
|
||||
<BarChartIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Paper>
|
||||
);
|
||||
};
|
||||
|
||||
handleChangeLang = (e) => {
|
||||
const { i18n } = this.props;
|
||||
i18n.changeLanguage(e.target.value);
|
||||
};
|
||||
|
||||
LangSelect = () => {
|
||||
const { i18n } = this.props;
|
||||
const lang = i18n.resolvedLanguage == null ? 'en' : i18n.resolvedLanguage.substring(0, 2);
|
||||
const flagProps = {
|
||||
width: 20,
|
||||
height: 20,
|
||||
};
|
||||
|
||||
return (
|
||||
<Select
|
||||
size='small'
|
||||
value={lang}
|
||||
inputProps={{
|
||||
style: { textAlign: 'center' },
|
||||
}}
|
||||
renderValue={(value) => value.toUpperCase()}
|
||||
onChange={this.handleChangeLang}
|
||||
>
|
||||
<MenuItem value={'en'}>
|
||||
<div style={{ width: 24, position: 'relative', top: 3 }}>
|
||||
<Flags.US {...flagProps} />
|
||||
</div>
|
||||
EN
|
||||
</MenuItem>
|
||||
<MenuItem value={'es'}>
|
||||
<div style={{ width: 24, position: 'relative', top: 3 }}>
|
||||
<Flags.ES {...flagProps} />
|
||||
</div>
|
||||
ES
|
||||
</MenuItem>
|
||||
<MenuItem value={'de'}>
|
||||
<div style={{ width: 24, position: 'relative', top: 3 }}>
|
||||
<Flags.DE {...flagProps} />
|
||||
</div>
|
||||
DE
|
||||
</MenuItem>
|
||||
<MenuItem value={'pl'}>
|
||||
<div style={{ width: 24, position: 'relative', top: 3 }}>
|
||||
<Flags.PL {...flagProps} />
|
||||
</div>
|
||||
PL
|
||||
</MenuItem>
|
||||
<MenuItem value={'fr'}>
|
||||
<div style={{ width: 24, position: 'relative', top: 3 }}>
|
||||
<Flags.FR {...flagProps} />
|
||||
</div>
|
||||
FR
|
||||
</MenuItem>
|
||||
<MenuItem value={'ru'}>
|
||||
<div style={{ width: 24, position: 'relative', top: 3 }}>
|
||||
<Flags.RU {...flagProps} />
|
||||
</div>
|
||||
RU
|
||||
</MenuItem>
|
||||
<MenuItem value={'it'}>
|
||||
<div style={{ width: 24, position: 'relative', top: 3 }}>
|
||||
<Flags.IT {...flagProps} />
|
||||
</div>
|
||||
IT
|
||||
</MenuItem>
|
||||
<MenuItem value={'pt'}>
|
||||
<div style={{ width: 24, position: 'relative', top: 3 }}>
|
||||
<Flags.BR {...flagProps} />
|
||||
</div>
|
||||
PT
|
||||
</MenuItem>
|
||||
<MenuItem value={'zh-si'}>
|
||||
<div style={{ width: 24, position: 'relative', top: 3 }}>
|
||||
<Flags.CN {...flagProps} />
|
||||
</div>
|
||||
简体
|
||||
</MenuItem>
|
||||
<MenuItem value={'zh-tr'}>
|
||||
<div style={{ width: 24, position: 'relative', top: 3 }}>
|
||||
<Flags.CN {...flagProps} />
|
||||
</div>
|
||||
繁體
|
||||
</MenuItem>
|
||||
<MenuItem value={'sv'}>
|
||||
<div style={{ width: 24, position: 'relative', top: 3 }}>
|
||||
<Flags.SE {...flagProps} />
|
||||
</div>
|
||||
SV
|
||||
</MenuItem>
|
||||
<MenuItem value={'cs'}>
|
||||
<div style={{ width: 24, position: 'relative', top: 3 }}>
|
||||
<Flags.CZ {...flagProps} />
|
||||
</div>
|
||||
CS
|
||||
</MenuItem>
|
||||
<MenuItem value={'th'}>
|
||||
<div style={{ width: 24, position: 'relative', top: 3 }}>
|
||||
<Flags.TH {...flagProps} />
|
||||
</div>
|
||||
TH
|
||||
</MenuItem>
|
||||
<MenuItem value={'ca'}>
|
||||
<div style={{ width: 24, position: 'relative', top: 3 }}>
|
||||
<CataloniaFlag {...flagProps} />
|
||||
</div>
|
||||
CA
|
||||
</MenuItem>
|
||||
<MenuItem value={'eu'}>
|
||||
<div style={{ width: 24, position: 'relative', top: 3 }}>
|
||||
<BasqueCountryFlag {...flagProps} />
|
||||
</div>
|
||||
EU
|
||||
</MenuItem>
|
||||
</Select>
|
||||
);
|
||||
};
|
||||
|
||||
bottomBarPhone = () => {
|
||||
const { t } = this.props;
|
||||
const hasRewards = this.props.robot.earnedRewards > 0;
|
||||
const hasOrder = !!(
|
||||
(this.props.info.active_order_id > 0) &
|
||||
!this.state.profileShown &
|
||||
this.props.robot.avatarLoaded
|
||||
);
|
||||
return (
|
||||
<Paper
|
||||
elevation={6}
|
||||
style={{ height: '2.85em', width: `${(this.props.windowSize.width / 16) * 14}em` }}
|
||||
>
|
||||
<Grid container>
|
||||
<Grid item xs={1.6}>
|
||||
<div style={{ display: this.showProfileButton() ? '' : 'none' }}>
|
||||
<Tooltip
|
||||
open={(hasRewards || hasOrder) && this.showProfileButton()}
|
||||
title={
|
||||
(hasRewards ? t('You can claim satoshis!') + ' ' : '') +
|
||||
(hasOrder ? t('You have an active order') : '')
|
||||
}
|
||||
>
|
||||
<IconButton
|
||||
onClick={this.handleClickOpenProfile}
|
||||
sx={{ margin: 0, bottom: 17, right: 8 }}
|
||||
>
|
||||
<RobotAvatar
|
||||
style={{ width: 55, height: 55 }}
|
||||
avatarClass='phoneFlippedSmallAvatar'
|
||||
statusColor={
|
||||
(this.props.activeOrderId > 0) & !this.state.profileShown
|
||||
? 'primary'
|
||||
: undefined
|
||||
}
|
||||
nickname={this.props.robot.nickname}
|
||||
onLoad={() => this.props.setRobot({ ...this.props.robot, avatarLoaded: true })}
|
||||
/>
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={1.6} align='center'>
|
||||
<Tooltip enterTouchDelay={300} title={t('Number of public BUY orders')}>
|
||||
<IconButton
|
||||
disabled={!this.showProfileButton()}
|
||||
color='primary'
|
||||
to={`/book/`}
|
||||
component={LinkRouter}
|
||||
>
|
||||
<Badge badgeContent={this.props.info.num_public_buy_orders} color='action'>
|
||||
<InventoryIcon />
|
||||
</Badge>
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={1.6} align='center'>
|
||||
<Tooltip enterTouchDelay={300} title={t('Number of public SELL orders')}>
|
||||
<IconButton
|
||||
disabled={!this.showProfileButton()}
|
||||
color='primary'
|
||||
to={`/book/`}
|
||||
component={LinkRouter}
|
||||
>
|
||||
<Badge badgeContent={this.props.info.num_public_sell_orders} color='action'>
|
||||
<SellIcon />
|
||||
</Badge>
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={1.6} align='center'>
|
||||
<Tooltip enterTouchDelay={300} title={t('Today active robots')}>
|
||||
<IconButton
|
||||
disabled={!this.showProfileButton()}
|
||||
color='primary'
|
||||
to={`/`}
|
||||
component={LinkRouter}
|
||||
>
|
||||
<Badge badgeContent={this.props.info.active_robots_today} color='action'>
|
||||
<SmartToyIcon />
|
||||
</Badge>
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={1.8} align='center'>
|
||||
<Tooltip enterTouchDelay={300} title={t('24h non-KYC bitcoin premium')}>
|
||||
<IconButton color='primary' onClick={this.handleClickOpenExchangeSummary}>
|
||||
<Badge
|
||||
badgeContent={this.props.info.last_day_nonkyc_btc_premium + '%'}
|
||||
color='action'
|
||||
>
|
||||
<PriceChangeIcon />
|
||||
</Badge>
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Grid>
|
||||
|
||||
<Grid container item xs={3.8}>
|
||||
<Grid item xs={6}>
|
||||
{this.LangSelect()}
|
||||
</Grid>
|
||||
<Grid item xs={3}>
|
||||
<Tooltip enterTouchDelay={250} title={t('Show community and support links')}>
|
||||
<IconButton
|
||||
color='primary'
|
||||
aria-label='Community'
|
||||
onClick={this.handleClickOpenCommunity}
|
||||
>
|
||||
<PeopleIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Grid>
|
||||
<Grid item xs={3}>
|
||||
<Tooltip enterTouchDelay={250} title={t('Show stats for nerds')}>
|
||||
<IconButton
|
||||
color='primary'
|
||||
aria-label='Stats for Nerds'
|
||||
onClick={() => this.props.fetchInfo()}
|
||||
onClick={this.handleClickOpenStatsForNerds}
|
||||
>
|
||||
<BarChartIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Paper>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<CommunityDialog
|
||||
open={this.state.openCommunity}
|
||||
handleClickCloseCommunity={this.handleClickCloseCommunity}
|
||||
/>
|
||||
|
||||
<UpdateClientDialog
|
||||
open={this.state.openUpdateClient}
|
||||
coordinatorVersion={this.props.info.coordinatorVersion}
|
||||
clientVersion={this.props.info.clientVersion}
|
||||
handleClickClose={() =>
|
||||
this.props.setInfo({ ...this.props.info, openUpdateClient: false })
|
||||
}
|
||||
/>
|
||||
|
||||
<ExchangeSummaryDialog
|
||||
open={this.state.openExchangeSummary}
|
||||
handleClickCloseExchangeSummary={this.handleClickCloseExchangeSummary}
|
||||
numPublicBuyOrders={this.props.info.num_public_buy_orders}
|
||||
numPublicSellOrders={this.props.info.num_public_sell_orders}
|
||||
bookLiquidity={this.props.info.book_liquidity}
|
||||
activeRobotsToday={this.props.info.active_robots_today}
|
||||
lastDayNonkycBtcPremium={this.props.info.last_day_nonkyc_btc_premium}
|
||||
makerFee={this.props.info.maker_fee}
|
||||
takerFee={this.props.info.taker_fee}
|
||||
swapFeeRate={this.props.info.current_swap_fee_rate}
|
||||
/>
|
||||
|
||||
<ProfileDialog
|
||||
open={this.state.openProfile}
|
||||
handleClickCloseProfile={this.handleClickCloseProfile}
|
||||
nickname={this.props.robot.nickname}
|
||||
activeOrderId={this.props.robot.activeOrderId}
|
||||
lastOrderId={this.props.robot.lastOrderId}
|
||||
referralCode={this.props.robot.referralCode}
|
||||
tgEnabled={this.props.robot.tgEnabled}
|
||||
tgBotName={this.props.robot.tgBotName}
|
||||
tgToken={this.props.robot.tgToken}
|
||||
handleSubmitInvoiceClicked={this.handleSubmitInvoiceClicked}
|
||||
host={this.getHost()}
|
||||
showRewardsSpinner={this.state.showRewardsSpinner}
|
||||
withdrawn={this.props.info.withdrawn}
|
||||
badInvoice={this.props.info.badInvoice}
|
||||
earnedRewards={this.props.robot.earnedRewards}
|
||||
updateRobot={(newParam) => this.props.setRobot({ ...robot, ...newParam })}
|
||||
stealthInvoices={this.props.robot.stealthInvoices}
|
||||
handleSetStealthInvoice={this.handleSetStealthInvoice}
|
||||
/>
|
||||
|
||||
<StatsDialog
|
||||
open={this.state.openStatsForNerds}
|
||||
handleClickCloseStatsForNerds={this.handleClickCloseStatsForNerds}
|
||||
coordinatorVersion={this.props.info.coordinatorVersion}
|
||||
clientVersion={this.props.info.clientVersion}
|
||||
lndVersion={this.props.info.lnd_version}
|
||||
network={this.props.info.network}
|
||||
nodeAlias={this.props.info.node_alias}
|
||||
nodeId={this.props.info.node_id}
|
||||
alternativeName={this.props.info.alternative_name}
|
||||
alternativeSite={this.props.info.alternative_site}
|
||||
commitHash={this.props.info.robosats_running_commit_hash}
|
||||
lastDayVolume={this.props.info.last_day_volume}
|
||||
lifetimeVolume={this.props.info.lifetime_volume}
|
||||
/>
|
||||
|
||||
<MediaQuery minWidth={1200}>{this.bottomBarDesktop()}</MediaQuery>
|
||||
|
||||
<MediaQuery maxWidth={1199}>{this.bottomBarPhone()}</MediaQuery>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withTranslation()(BottomBar);
|
@ -1,17 +1,16 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { HashRouter, BrowserRouter, Switch, Route, useHistory } from 'react-router-dom';
|
||||
import { useTheme, IconButton } from '@mui/material';
|
||||
import { useTheme, Box, Slide } from '@mui/material';
|
||||
|
||||
import UserGenPage from './UserGenPage';
|
||||
import MakerPage from './MakerPage';
|
||||
import BookPage from './BookPage';
|
||||
import OrderPage from './OrderPage';
|
||||
import BottomBar from './BottomBar';
|
||||
import { LearnDialog } from '../components/Dialogs';
|
||||
|
||||
import { apiClient } from '../services/api';
|
||||
import checkVer from '../utils/checkVer';
|
||||
import SettingsPage from './SettingsPage';
|
||||
import NavBar, { Page } from './NavBar';
|
||||
import MainDialogs, { OpenDialogs } from './MainDialogs';
|
||||
|
||||
import RobotAvatar from '../components/RobotAvatar';
|
||||
import {
|
||||
Book,
|
||||
LimitList,
|
||||
@ -23,12 +22,14 @@ import {
|
||||
defaultMaker,
|
||||
defaultRobot,
|
||||
defaultInfo,
|
||||
Coordinator,
|
||||
} from '../models';
|
||||
|
||||
// Icons
|
||||
import DarkModeIcon from '@mui/icons-material/DarkMode';
|
||||
import LightModeIcon from '@mui/icons-material/LightMode';
|
||||
import SchoolIcon from '@mui/icons-material/School';
|
||||
import { apiClient } from '../services/api';
|
||||
import { checkVer } from '../utils';
|
||||
import { sha256 } from 'js-sha256';
|
||||
|
||||
import defaultCoordinators from '../../static/federation.json';
|
||||
|
||||
const getWindowSize = function (fontSize: number) {
|
||||
// returns window size in EM units
|
||||
@ -38,19 +39,17 @@ const getWindowSize = function (fontSize: number) {
|
||||
};
|
||||
};
|
||||
|
||||
interface SlideDirection {
|
||||
in: 'left' | 'right' | undefined;
|
||||
out: 'left' | 'right' | undefined;
|
||||
}
|
||||
|
||||
interface MainProps {
|
||||
updateTheme: () => void;
|
||||
settings: Settings;
|
||||
setSettings: (state: Settings) => void;
|
||||
}
|
||||
|
||||
const Main = ({ settings, setSettings }: MainProps): JSX.Element => {
|
||||
const theme = useTheme();
|
||||
const history = useHistory();
|
||||
const Router = window.NativeRobosats != null ? HashRouter : BrowserRouter;
|
||||
const basename = window.NativeRobosats != null ? window.location.pathname : '';
|
||||
const [openLearn, setOpenLearn] = useState<boolean>(false);
|
||||
|
||||
// All app data structured
|
||||
const [book, setBook] = useState<Book>({ orders: [], loading: true });
|
||||
const [limits, setLimits] = useState<{ list: LimitList; loading: boolean }>({
|
||||
@ -60,8 +59,38 @@ const Main = ({ settings, setSettings }: MainProps): JSX.Element => {
|
||||
const [robot, setRobot] = useState<Robot>(defaultRobot);
|
||||
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 theme = useTheme();
|
||||
const history = useHistory();
|
||||
|
||||
const Router = window.NativeRobosats != null ? HashRouter : BrowserRouter;
|
||||
const basename = window.NativeRobosats != null ? window.location.pathname : '';
|
||||
const [page, setPage] = useState<Page>(
|
||||
window.location.pathname.split('/')[1] == ''
|
||||
? 'offers'
|
||||
: window.location.pathname.split('/')[1],
|
||||
);
|
||||
const [slideDirection, setSlideDirection] = useState<SlideDirection>({
|
||||
in: undefined,
|
||||
out: undefined,
|
||||
});
|
||||
const [order, setOrder] = useState<number | null>(null);
|
||||
|
||||
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),
|
||||
);
|
||||
@ -72,7 +101,6 @@ const Main = ({ settings, setSettings }: MainProps): JSX.Element => {
|
||||
}
|
||||
fetchBook();
|
||||
fetchLimits();
|
||||
fetchInfo();
|
||||
return () => {
|
||||
if (typeof window !== undefined) {
|
||||
window.removeEventListener('resize', onResize);
|
||||
@ -80,6 +108,10 @@ const Main = ({ settings, setSettings }: MainProps): JSX.Element => {
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setWindowSize(getWindowSize(theme.typography.fontSize));
|
||||
}, [theme.typography.fontSize]);
|
||||
|
||||
const onResize = function () {
|
||||
setWindowSize(getWindowSize(theme.typography.fontSize));
|
||||
};
|
||||
@ -104,7 +136,7 @@ const Main = ({ settings, setSettings }: MainProps): JSX.Element => {
|
||||
};
|
||||
|
||||
const fetchInfo = function () {
|
||||
apiClient.get('/api/info/').then((data: any) => {
|
||||
apiClient.get('/api/info/').then((data: Info) => {
|
||||
const versionInfo: any = checkVer(data.version.major, data.version.minor, data.version.patch);
|
||||
setInfo({
|
||||
...data,
|
||||
@ -112,116 +144,202 @@ const Main = ({ settings, setSettings }: MainProps): JSX.Element => {
|
||||
coordinatorVersion: versionInfo.coordinatorVersion,
|
||||
clientVersion: versionInfo.clientVersion,
|
||||
});
|
||||
if (!robot.nickname && data.nickname && !robot.loading) {
|
||||
setRobot({
|
||||
...robot,
|
||||
nickname: data.nickname,
|
||||
loading: false,
|
||||
activeOrderId: data.active_order_id ?? null,
|
||||
lastOrderId: data.last_order_id ?? null,
|
||||
referralCode: data.referral_code,
|
||||
tgEnabled: data.tg_enabled,
|
||||
tgBotName: data.tg_bot_name,
|
||||
tgToken: data.tg_token,
|
||||
earnedRewards: data.earned_rewards ?? 0,
|
||||
stealthInvoices: data.wants_stealth,
|
||||
});
|
||||
}
|
||||
setSettings({
|
||||
...settings,
|
||||
network: data.network,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchInfo();
|
||||
}, [open.stats, open.coordinator]);
|
||||
|
||||
const fetchRobot = function () {
|
||||
const requestBody = {
|
||||
token_sha256: sha256(robot.token),
|
||||
};
|
||||
|
||||
apiClient.post('/api/user/', requestBody).then((data: any) => {
|
||||
setOrder(
|
||||
data.active_order_id
|
||||
? data.active_order_id
|
||||
: data.last_order_id
|
||||
? data.last_order_id
|
||||
: order,
|
||||
);
|
||||
setRobot({
|
||||
...robot,
|
||||
nickname: data.nickname,
|
||||
token: robot.token,
|
||||
loading: false,
|
||||
avatarLoaded: 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,
|
||||
pub_key: data.public_key,
|
||||
enc_priv_key: data.encrypted_private_key,
|
||||
copiedToken: data.found ? true : robot.copiedToken,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (robot.token && robot.nickname === null) {
|
||||
fetchRobot();
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Router basename={basename}>
|
||||
<div className='temporaryUpperIcons'>
|
||||
<LearnDialog open={openLearn} onClose={() => setOpenLearn(false)} />
|
||||
<IconButton
|
||||
color='inherit'
|
||||
sx={{ position: 'fixed', right: '34px', color: 'text.secondary' }}
|
||||
onClick={() => setOpenLearn(true)}
|
||||
>
|
||||
<SchoolIcon />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
color='inherit'
|
||||
sx={{ position: 'fixed', right: '0px', color: 'text.secondary' }}
|
||||
onClick={() =>
|
||||
setSettings({ ...settings, mode: settings.mode === 'dark' ? 'light' : 'dark' })
|
||||
}
|
||||
>
|
||||
{theme.palette.mode === 'dark' ? <LightModeIcon /> : <DarkModeIcon />}
|
||||
</IconButton>
|
||||
</div>
|
||||
<div className='appCenter'>
|
||||
<Switch>
|
||||
<Route
|
||||
exact
|
||||
path='/'
|
||||
render={(props: any) => (
|
||||
<UserGenPage match={props.match} theme={theme} robot={robot} setRobot={setRobot} />
|
||||
)}
|
||||
/>
|
||||
<Route
|
||||
path='/ref/:refCode'
|
||||
render={(props: any) => (
|
||||
<UserGenPage match={props.match} theme={theme} robot={robot} setRobot={setRobot} />
|
||||
)}
|
||||
/>
|
||||
<Route
|
||||
path='/make'
|
||||
render={() => (
|
||||
<MakerPage
|
||||
book={book}
|
||||
limits={limits}
|
||||
fetchLimits={fetchLimits}
|
||||
maker={maker}
|
||||
setMaker={setMaker}
|
||||
fav={fav}
|
||||
setFav={setFav}
|
||||
windowSize={windowSize}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Route
|
||||
path='/book'
|
||||
render={() => (
|
||||
<BookPage
|
||||
book={book}
|
||||
fetchBook={fetchBook}
|
||||
limits={limits}
|
||||
fetchLimits={fetchLimits}
|
||||
fav={fav}
|
||||
setFav={setFav}
|
||||
maker={maker}
|
||||
setMaker={setMaker}
|
||||
lastDayPremium={info.last_day_nonkyc_btc_premium}
|
||||
windowSize={windowSize}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Route
|
||||
path='/order/:orderId'
|
||||
render={(props: any) => <OrderPage theme={theme} history={history} {...props} />}
|
||||
/>
|
||||
</Switch>
|
||||
</div>
|
||||
<div
|
||||
{/* load robot avatar image, set avatarLoaded: true */}
|
||||
<RobotAvatar
|
||||
style={{ display: 'none' }}
|
||||
nickname={robot.nickname}
|
||||
onLoad={() => setRobot({ ...robot, avatarLoaded: true })}
|
||||
/>
|
||||
<Box
|
||||
style={{
|
||||
height: '2.5em',
|
||||
position: 'fixed',
|
||||
bottom: 0,
|
||||
position: 'absolute',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: `translate(-50%, -50%) translate(0, -${navbarHeight / 2}em`,
|
||||
}}
|
||||
>
|
||||
<BottomBar
|
||||
theme={theme}
|
||||
windowSize={windowSize}
|
||||
redirectTo={(location: string) => history.push(location)}
|
||||
robot={robot}
|
||||
setRobot={setRobot}
|
||||
info={info}
|
||||
setInfo={setInfo}
|
||||
fetchInfo={fetchInfo}
|
||||
/>
|
||||
</div>
|
||||
<Switch>
|
||||
<Route
|
||||
path='/robot/:refCode?'
|
||||
render={(props: any) => (
|
||||
<Slide
|
||||
direction={page === 'robot' ? slideDirection.in : slideDirection.out}
|
||||
in={page === 'robot'}
|
||||
appear={slideDirection.in != undefined}
|
||||
>
|
||||
<div>
|
||||
<UserGenPage
|
||||
setPage={setPage}
|
||||
order={order}
|
||||
setOrder={setOrder}
|
||||
match={props.match}
|
||||
theme={theme}
|
||||
robot={robot}
|
||||
setRobot={setRobot}
|
||||
/>
|
||||
</div>
|
||||
</Slide>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Route exact path={['/offers', '/']}>
|
||||
<Slide
|
||||
direction={page === 'offers' ? slideDirection.in : slideDirection.out}
|
||||
in={page === 'offers'}
|
||||
appear={slideDirection.in != undefined}
|
||||
>
|
||||
<div>
|
||||
<BookPage
|
||||
book={book}
|
||||
fetchBook={fetchBook}
|
||||
limits={limits}
|
||||
fetchLimits={fetchLimits}
|
||||
fav={fav}
|
||||
setFav={setFav}
|
||||
maker={maker}
|
||||
setMaker={setMaker}
|
||||
lastDayPremium={info.last_day_nonkyc_btc_premium}
|
||||
windowSize={windowSize}
|
||||
hasRobot={robot.avatarLoaded}
|
||||
setPage={setPage}
|
||||
setOrder={setOrder}
|
||||
/>
|
||||
</div>
|
||||
</Slide>
|
||||
</Route>
|
||||
|
||||
<Route path='/create'>
|
||||
<Slide
|
||||
direction={page === 'create' ? slideDirection.in : slideDirection.out}
|
||||
in={page === 'create'}
|
||||
appear={slideDirection.in != undefined}
|
||||
>
|
||||
<div>
|
||||
<MakerPage
|
||||
book={book}
|
||||
limits={limits}
|
||||
fetchLimits={fetchLimits}
|
||||
maker={maker}
|
||||
setMaker={setMaker}
|
||||
setPage={setPage}
|
||||
setOrder={setOrder}
|
||||
fav={fav}
|
||||
setFav={setFav}
|
||||
windowSize={{ ...windowSize, height: windowSize.height - navbarHeight }}
|
||||
hasRobot={robot.avatarLoaded}
|
||||
/>
|
||||
</div>
|
||||
</Slide>
|
||||
</Route>
|
||||
|
||||
<Route
|
||||
path='/order/:orderId'
|
||||
render={(props: any) => (
|
||||
<Slide
|
||||
direction={page === 'order' ? slideDirection.in : slideDirection.out}
|
||||
in={page === 'order'}
|
||||
appear={slideDirection.in != undefined}
|
||||
>
|
||||
<div>
|
||||
<OrderPage theme={theme} history={history} {...props} />
|
||||
</div>
|
||||
</Slide>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Route path='/settings'>
|
||||
<Slide
|
||||
direction={page === 'settings' ? slideDirection.in : slideDirection.out}
|
||||
in={page === 'settings'}
|
||||
appear={slideDirection.in != undefined}
|
||||
>
|
||||
<div>
|
||||
<SettingsPage
|
||||
settings={settings}
|
||||
setSettings={setSettings}
|
||||
windowSize={{ ...windowSize, height: windowSize.height - navbarHeight }}
|
||||
/>
|
||||
</div>
|
||||
</Slide>
|
||||
</Route>
|
||||
</Switch>
|
||||
</Box>
|
||||
<NavBar
|
||||
nickname={robot.avatarLoaded ? robot.nickname : null}
|
||||
width={windowSize.width}
|
||||
height={navbarHeight}
|
||||
page={page}
|
||||
setPage={setPage}
|
||||
open={open}
|
||||
setOpen={setOpen}
|
||||
closeAll={closeAll}
|
||||
setSlideDirection={setSlideDirection}
|
||||
order={order}
|
||||
hasRobot={robot.avatarLoaded}
|
||||
/>
|
||||
<MainDialogs
|
||||
open={open}
|
||||
setOpen={setOpen}
|
||||
setRobot={setRobot}
|
||||
info={info}
|
||||
robot={robot}
|
||||
closeAll={closeAll}
|
||||
/>
|
||||
</Router>
|
||||
);
|
||||
};
|
||||
|
102
frontend/src/basic/MainDialogs/index.tsx
Normal file
102
frontend/src/basic/MainDialogs/index.tsx
Normal file
@ -0,0 +1,102 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { Info, Robot } from '../../models';
|
||||
import {
|
||||
CommunityDialog,
|
||||
CoordinatorSummaryDialog,
|
||||
InfoDialog,
|
||||
LearnDialog,
|
||||
ProfileDialog,
|
||||
StatsDialog,
|
||||
UpdateClientDialog,
|
||||
} from '../../components/Dialogs';
|
||||
|
||||
export interface OpenDialogs {
|
||||
more: boolean;
|
||||
learn: boolean;
|
||||
community: boolean;
|
||||
info: boolean;
|
||||
coordinator: boolean;
|
||||
stats: boolean;
|
||||
update: boolean;
|
||||
profile: boolean; // temporary until new Robot Page is ready
|
||||
}
|
||||
|
||||
interface MainDialogsProps {
|
||||
open: OpenDialogs;
|
||||
setOpen: (state: OpenDialogs) => void;
|
||||
info: Info;
|
||||
robot: Robot;
|
||||
setRobot: (state: Robot) => void;
|
||||
closeAll: OpenDialogs;
|
||||
}
|
||||
|
||||
const MainDialogs = ({
|
||||
open,
|
||||
setOpen,
|
||||
info,
|
||||
closeAll,
|
||||
robot,
|
||||
setRobot,
|
||||
}: MainDialogsProps): JSX.Element => {
|
||||
useEffect(() => {
|
||||
if (info.openUpdateClient) {
|
||||
setOpen({ ...closeAll, update: true });
|
||||
}
|
||||
}, [info]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<UpdateClientDialog
|
||||
open={open.update}
|
||||
coordinatorVersion={info.coordinatorVersion}
|
||||
clientVersion={info.clientVersion}
|
||||
onClose={() => setOpen({ ...open, update: false })}
|
||||
/>
|
||||
<InfoDialog
|
||||
open={open.info}
|
||||
maxAmount='4,000,000'
|
||||
onClose={() => setOpen({ ...open, info: false })}
|
||||
/>
|
||||
<LearnDialog open={open.learn} onClose={() => setOpen({ ...open, learn: false })} />
|
||||
<CommunityDialog
|
||||
open={open.community}
|
||||
onClose={() => setOpen({ ...open, community: false })}
|
||||
/>
|
||||
<CoordinatorSummaryDialog
|
||||
open={open.coordinator}
|
||||
onClose={() => setOpen({ ...open, coordinator: false })}
|
||||
numPublicBuyOrders={info.num_public_buy_orders}
|
||||
numPublicSellOrders={info.num_public_sell_orders}
|
||||
bookLiquidity={info.book_liquidity}
|
||||
activeRobotsToday={info.active_robots_today}
|
||||
lastDayNonkycBtcPremium={info.last_day_nonkyc_btc_premium}
|
||||
makerFee={info.maker_fee}
|
||||
takerFee={info.taker_fee}
|
||||
swapFeeRate={info.current_swap_fee_rate}
|
||||
/>
|
||||
<StatsDialog
|
||||
open={open.stats}
|
||||
onClose={() => setOpen({ ...open, stats: false })}
|
||||
coordinatorVersion={info.coordinatorVersion}
|
||||
clientVersion={info.clientVersion}
|
||||
lndVersion={info.lnd_version}
|
||||
network={info.network}
|
||||
nodeAlias={info.node_alias}
|
||||
nodeId={info.node_id}
|
||||
alternativeName={info.alternative_name}
|
||||
alternativeSite={info.alternative_site}
|
||||
commitHash={info.robosats_running_commit_hash}
|
||||
lastDayVolume={info.last_day_volume}
|
||||
lifetimeVolume={info.lifetime_volume}
|
||||
/>
|
||||
<ProfileDialog
|
||||
open={open.profile}
|
||||
onClose={() => setOpen({ ...open, profile: false })}
|
||||
robot={robot}
|
||||
setRobot={setRobot}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default MainDialogs;
|
@ -1,15 +1,16 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { Button, Grid, Paper, Collapse, Typography } from '@mui/material';
|
||||
import { Grid, Paper, Collapse, Typography } from '@mui/material';
|
||||
|
||||
import { LimitList, Maker, Book, Favorites } from '../../models';
|
||||
|
||||
import filterOrders from '../../utils/filterOrders';
|
||||
import { filterOrders } from '../../utils';
|
||||
|
||||
import MakerForm from '../../components/MakerForm';
|
||||
import BookTable from '../../components/BookTable';
|
||||
|
||||
import { Page } from '../NavBar';
|
||||
|
||||
interface MakerPageProps {
|
||||
limits: { list: LimitList; loading: boolean };
|
||||
fetchLimits: () => void;
|
||||
@ -19,6 +20,9 @@ interface MakerPageProps {
|
||||
setFav: (state: Favorites) => void;
|
||||
setMaker: (state: Maker) => void;
|
||||
windowSize: { width: number; height: number };
|
||||
hasRobot: boolean;
|
||||
setOrder: (state: number) => void;
|
||||
setPage: (state: Page) => void;
|
||||
}
|
||||
|
||||
const MakerPage = ({
|
||||
@ -30,11 +34,13 @@ const MakerPage = ({
|
||||
setFav,
|
||||
setMaker,
|
||||
windowSize,
|
||||
setOrder,
|
||||
setPage,
|
||||
hasRobot = false,
|
||||
}: MakerPageProps): JSX.Element => {
|
||||
const { t } = useTranslation();
|
||||
const history = useHistory();
|
||||
|
||||
const maxHeight = windowSize.height * 0.85 - 7;
|
||||
const maxHeight = windowSize.height * 0.85 - 3;
|
||||
const [showMatches, setShowMatches] = useState<boolean>(false);
|
||||
|
||||
const matches = filterOrders({
|
||||
@ -74,7 +80,12 @@ const MakerPage = ({
|
||||
<Grid item>
|
||||
<Paper
|
||||
elevation={12}
|
||||
style={{ padding: 8, width: '17.25em', maxHeight: `${maxHeight}em`, overflow: 'auto' }}
|
||||
style={{
|
||||
padding: '0.6em',
|
||||
width: '17.25em',
|
||||
maxHeight: `${maxHeight}em`,
|
||||
overflow: 'auto',
|
||||
}}
|
||||
>
|
||||
<MakerForm
|
||||
limits={limits}
|
||||
@ -83,19 +94,20 @@ const MakerPage = ({
|
||||
setFav={setFav}
|
||||
maker={maker}
|
||||
setMaker={setMaker}
|
||||
onOrderCreated={(id) => {
|
||||
setOrder(id);
|
||||
setPage('order');
|
||||
}}
|
||||
hasRobot={hasRobot}
|
||||
disableRequest={matches.length > 0 && !showMatches}
|
||||
collapseAll={showMatches}
|
||||
onSubmit={() => setShowMatches(matches.length > 0)}
|
||||
onReset={() => setShowMatches(false)}
|
||||
submitButtonLabel={matches.length > 0 && !showMatches ? 'Submit' : 'Create order'}
|
||||
setPage={setPage}
|
||||
/>
|
||||
</Paper>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Button color='secondary' variant='contained' onClick={() => history.push('/')}>
|
||||
{t('Back')}
|
||||
</Button>
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
128
frontend/src/basic/NavBar/MoreTooltip.tsx
Normal file
128
frontend/src/basic/NavBar/MoreTooltip.tsx
Normal file
@ -0,0 +1,128 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useTheme, styled, Grid, IconButton } from '@mui/material';
|
||||
import Tooltip, { TooltipProps, tooltipClasses } from '@mui/material/Tooltip';
|
||||
|
||||
import { OpenDialogs } from '../MainDialogs';
|
||||
|
||||
import { BubbleChart, Info, People, PriceChange, School } from '@mui/icons-material';
|
||||
|
||||
const StyledTooltip = styled(({ className, ...props }: TooltipProps) => (
|
||||
<Tooltip {...props} classes={{ popper: className }} />
|
||||
))(({ theme }) => ({
|
||||
[`& .${tooltipClasses.tooltip}`]: {
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
color: theme.palette.text.primary,
|
||||
boxShadow: theme.shadows[1],
|
||||
fontSize: theme.typography.fontSize,
|
||||
borderRadius: '2em',
|
||||
},
|
||||
}));
|
||||
|
||||
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 { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<StyledTooltip
|
||||
open={open.more}
|
||||
title={
|
||||
<Grid
|
||||
container
|
||||
direction='column'
|
||||
padding={0}
|
||||
sx={{ width: '2em', padding: '0em' }}
|
||||
justifyContent='center'
|
||||
>
|
||||
<Grid item sx={{ position: 'relative', right: '0.4em' }}>
|
||||
<Tooltip enterTouchDelay={250} placement='left' title={t('RoboSats information')}>
|
||||
<IconButton
|
||||
sx={{
|
||||
color: open.info ? theme.palette.primary.main : theme.palette.text.secondary,
|
||||
}}
|
||||
onClick={() => setOpen({ ...closeAll, info: !open.info })}
|
||||
>
|
||||
<Info />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Grid>
|
||||
|
||||
<Grid item sx={{ position: 'relative', right: '0.4em' }}>
|
||||
<Tooltip enterTouchDelay={250} placement='left' title={t('Learn RoboSats')}>
|
||||
<IconButton
|
||||
sx={{
|
||||
color: open.learn ? theme.palette.primary.main : theme.palette.text.secondary,
|
||||
}}
|
||||
onClick={() => setOpen({ ...closeAll, learn: !open.learn })}
|
||||
>
|
||||
<School />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Grid>
|
||||
|
||||
<Grid item sx={{ position: 'relative', right: '0.4em' }}>
|
||||
<Tooltip
|
||||
enterTouchDelay={250}
|
||||
placement='left'
|
||||
title={t('Community and public support')}
|
||||
>
|
||||
<IconButton
|
||||
sx={{
|
||||
color: open.community ? theme.palette.primary.main : theme.palette.text.secondary,
|
||||
}}
|
||||
onClick={() => setOpen({ ...closeAll, community: !open.community })}
|
||||
>
|
||||
<People />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Grid>
|
||||
|
||||
<Grid item sx={{ position: 'relative', right: '0.4em' }}>
|
||||
<Tooltip enterTouchDelay={250} placement='left' title={t('Coordinator summary')}>
|
||||
<IconButton
|
||||
sx={{
|
||||
color: open.coordinator
|
||||
? theme.palette.primary.main
|
||||
: theme.palette.text.secondary,
|
||||
}}
|
||||
onClick={() => setOpen({ ...closeAll, coordinator: !open.coordinator })}
|
||||
>
|
||||
<PriceChange />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Grid>
|
||||
|
||||
<Grid item sx={{ position: 'relative', right: '0.4em' }}>
|
||||
<Tooltip enterTouchDelay={250} placement='left' title={t('Stats for nerds')}>
|
||||
<IconButton
|
||||
sx={{
|
||||
color: open.stats ? theme.palette.primary.main : theme.palette.text.secondary,
|
||||
}}
|
||||
onClick={() => setOpen({ ...closeAll, stats: !open.stats })}
|
||||
>
|
||||
<BubbleChart />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Grid>
|
||||
</Grid>
|
||||
}
|
||||
>
|
||||
{children}
|
||||
</StyledTooltip>
|
||||
);
|
||||
};
|
||||
|
||||
export default MoreTooltip;
|
647
frontend/src/basic/NavBar/NavBar.tsx
Normal file
647
frontend/src/basic/NavBar/NavBar.tsx
Normal file
@ -0,0 +1,647 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { Tabs, Tab, Paper, useTheme, Tooltip } from '@mui/material';
|
||||
import MoreTooltip from './MoreTooltip';
|
||||
|
||||
import { OpenDialogs } from '../MainDialogs';
|
||||
|
||||
import { Page } from '.';
|
||||
|
||||
import {
|
||||
SettingsApplications,
|
||||
SmartToy,
|
||||
Storefront,
|
||||
AddBox,
|
||||
Assignment,
|
||||
MoreHoriz,
|
||||
} from '@mui/icons-material';
|
||||
import RobotAvatar from '../../components/RobotAvatar';
|
||||
|
||||
type Direction = 'left' | 'right' | undefined;
|
||||
|
||||
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;
|
||||
order: number | null;
|
||||
hasRobot: boolean;
|
||||
}
|
||||
|
||||
const NavBar = ({
|
||||
page,
|
||||
setPage,
|
||||
setSlideDirection,
|
||||
open,
|
||||
nickname = null,
|
||||
setOpen,
|
||||
closeAll,
|
||||
width,
|
||||
height,
|
||||
order,
|
||||
hasRobot = false,
|
||||
}: NavBarProps): JSX.Element => {
|
||||
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: '1em', minWidth: '2em' };
|
||||
const pagesPosition = {
|
||||
robot: 1,
|
||||
offers: 2,
|
||||
create: 3,
|
||||
order: 4,
|
||||
settings: 5,
|
||||
};
|
||||
|
||||
const handleSlideDirection = function (oldPage: Page, newPage: Page) {
|
||||
const oldPos: number = pagesPosition[oldPage];
|
||||
const newPos: number = pagesPosition[newPage];
|
||||
setSlideDirection(
|
||||
newPos > oldPos ? { in: 'left', out: 'right' } : { in: 'right', out: 'left' },
|
||||
);
|
||||
};
|
||||
|
||||
const changePage = function (mouseEvent: any, newPage: Page) {
|
||||
if (newPage === 'none') {
|
||||
return null;
|
||||
} else {
|
||||
handleSlideDirection(page, newPage);
|
||||
setPage(newPage);
|
||||
const param = newPage === 'order' ? order ?? '' : '';
|
||||
setTimeout(
|
||||
() => history.push(`/${newPage}/${param}`),
|
||||
theme.transitions.duration.leavingScreen * 3,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setOpen(closeAll);
|
||||
}, [page]);
|
||||
|
||||
return (
|
||||
<Paper
|
||||
elevation={6}
|
||||
sx={{ height: `${height}em`, width: `${width * 0.9}em`, position: 'fixed', bottom: 0 }}
|
||||
>
|
||||
<Tabs
|
||||
TabIndicatorProps={{ sx: { height: '0.3em', position: 'absolute', top: 0 } }}
|
||||
variant='fullWidth'
|
||||
value={page}
|
||||
onChange={changePage}
|
||||
>
|
||||
<Tab
|
||||
sx={{ ...tabSx, minWidth: '2.5em', width: '2.5em', maxWidth: '4em' }}
|
||||
value='none'
|
||||
disabled={nickname === null}
|
||||
onClick={() => setOpen({ ...closeAll, profile: !open.profile })}
|
||||
icon={
|
||||
nickname ? (
|
||||
<RobotAvatar
|
||||
style={{ width: '2.3em', height: '2.3em', position: 'relative', top: '0.2em' }}
|
||||
avatarClass={theme.palette.mode === 'dark' ? 'navBarAvatarDark' : 'navBarAvatar'}
|
||||
nickname={nickname}
|
||||
/>
|
||||
) : (
|
||||
<></>
|
||||
)
|
||||
}
|
||||
/>
|
||||
|
||||
<Tab
|
||||
label={smallBar ? undefined : t('Robot')}
|
||||
sx={{ ...tabSx, minWidth: '1em' }}
|
||||
value='robot'
|
||||
icon={<SmartToy />}
|
||||
iconPosition='start'
|
||||
/>
|
||||
|
||||
<Tab
|
||||
sx={tabSx}
|
||||
label={smallBar ? undefined : t('Offers')}
|
||||
value='offers'
|
||||
icon={<Storefront />}
|
||||
iconPosition='start'
|
||||
/>
|
||||
<Tab
|
||||
sx={tabSx}
|
||||
label={smallBar ? undefined : t('Create')}
|
||||
value='create'
|
||||
icon={<AddBox />}
|
||||
iconPosition='start'
|
||||
/>
|
||||
<Tab
|
||||
sx={tabSx}
|
||||
label={smallBar ? undefined : t('Order')}
|
||||
value='order'
|
||||
disabled={!hasRobot || order == null}
|
||||
icon={<Assignment />}
|
||||
iconPosition='start'
|
||||
/>
|
||||
<Tab
|
||||
sx={tabSx}
|
||||
label={smallBar ? undefined : t('Settings')}
|
||||
value='settings'
|
||||
icon={<SettingsApplications />}
|
||||
iconPosition='start'
|
||||
/>
|
||||
|
||||
<Tab
|
||||
sx={tabSx}
|
||||
label={smallBar ? undefined : t('More')}
|
||||
value='none'
|
||||
onClick={(e) => {
|
||||
open.more ? null : setOpen({ ...open, more: true });
|
||||
}}
|
||||
icon={
|
||||
<MoreTooltip open={open} nickname={nickname} setOpen={setOpen} closeAll={closeAll}>
|
||||
<MoreHoriz />
|
||||
</MoreTooltip>
|
||||
}
|
||||
iconPosition='start'
|
||||
/>
|
||||
</Tabs>
|
||||
</Paper>
|
||||
);
|
||||
};
|
||||
|
||||
export default NavBar;
|
||||
|
||||
// constructor(props) {
|
||||
// super(props);
|
||||
// this.state = {
|
||||
// profileShown: false,
|
||||
// openStatsForNerds: false,
|
||||
// openCommunity: false,
|
||||
// openExchangeSummary: false,
|
||||
// openClaimRewards: false,
|
||||
// openProfile: false,
|
||||
// showRewards: false,
|
||||
// rewardInvoice: null,
|
||||
// badInvoice: false,
|
||||
// showRewardsSpinner: false,
|
||||
// withdrawn: false,
|
||||
// };
|
||||
// }
|
||||
|
||||
// handleClickOpenStatsForNerds = () => {
|
||||
// this.setState({ openStatsForNerds: true });
|
||||
// };
|
||||
|
||||
// handleClickCloseStatsForNerds = () => {
|
||||
// this.setState({ openStatsForNerds: false });
|
||||
// };
|
||||
|
||||
// handleClickOpenCommunity = () => {
|
||||
// this.setState({ openCommunity: true });
|
||||
// };
|
||||
|
||||
// handleClickCloseCommunity = () => {
|
||||
// this.setState({ openCommunity: false });
|
||||
// };
|
||||
|
||||
// handleClickOpenProfile = () => {
|
||||
// this.setState({ openProfile: true, profileShown: true });
|
||||
// };
|
||||
|
||||
// handleClickCloseProfile = () => {
|
||||
// this.setState({ openProfile: false });
|
||||
// };
|
||||
|
||||
// handleClickOpenExchangeSummary = () => {
|
||||
// this.setState({ openExchangeSummary: true });
|
||||
// };
|
||||
|
||||
// handleClickCloseExchangeSummary = () => {
|
||||
// this.setState({ openExchangeSummary: false });
|
||||
// };
|
||||
|
||||
// handleSubmitInvoiceClicked = (e, rewardInvoice) => {
|
||||
// this.setState({ badInvoice: false, showRewardsSpinner: true });
|
||||
|
||||
// apiClient
|
||||
// .post('/api/reward/', {
|
||||
// invoice: rewardInvoice,
|
||||
// })
|
||||
// .then((data) => {
|
||||
// this.setState({ badInvoice: data.bad_invoice, showRewardsSpinner: false });
|
||||
// this.props.setInfo({
|
||||
// ...this.props.info,
|
||||
// badInvoice: data.bad_invoice,
|
||||
// openClaimRewards: !data.successful_withdrawal,
|
||||
// withdrawn: !!data.successful_withdrawal,
|
||||
// showRewardsSpinner: false,
|
||||
// });
|
||||
// this.props.setRobot({
|
||||
// ...this.props.robot,
|
||||
// earnedRewards: data.successful_withdrawal ? 0 : this.props.robot.earnedRewards,
|
||||
// });
|
||||
// });
|
||||
// e.preventDefault();
|
||||
// };
|
||||
|
||||
// handleSetStealthInvoice = (wantsStealth) => {
|
||||
// apiClient
|
||||
// .put('/api/stealth/', { wantsStealth })
|
||||
// .then((data) =>
|
||||
// this.props.setRobot({ ...this.props.robot, stealthInvoices: data?.wantsStealth }),
|
||||
// );
|
||||
// };
|
||||
|
||||
// showProfileButton = () => {
|
||||
// return (
|
||||
// this.props.robot.avatarLoaded &&
|
||||
// (this.props.robot.token
|
||||
// ? systemClient.getCookie('robot_token') === this.props.robot.token
|
||||
// : true) &&
|
||||
// systemClient.getCookie('sessionid')
|
||||
// );
|
||||
// };
|
||||
|
||||
// bottomBarDesktop = () => {
|
||||
// const { t } = this.props;
|
||||
// const hasRewards = this.props.robot.earnedRewards > 0;
|
||||
// const hasOrder = !!(
|
||||
// (this.props.robot.activeOrderId > 0) &
|
||||
// !this.state.profileShown &
|
||||
// this.props.robot.avatarLoaded
|
||||
// );
|
||||
// const fontSize = this.props.theme.typography.fontSize;
|
||||
// const fontSizeFactor = fontSize / 14; // default fontSize is 14
|
||||
// const typographyProps = {
|
||||
// primaryTypographyProps: { fontSize },
|
||||
// secondaryTypographyProps: { fontSize: (fontSize * 12) / 14 },
|
||||
// };
|
||||
// return (
|
||||
// <Paper
|
||||
// elevation={6}
|
||||
// style={{ height: '2.5em', width: `${(this.props.windowSize.width / 16) * 14}em` }}
|
||||
// >
|
||||
// <Grid container>
|
||||
// <Grid item xs={1.9}>
|
||||
// <div style={{ display: this.showProfileButton() ? '' : 'none' }}>
|
||||
// <ListItemButton onClick={this.handleClickOpenProfile}>
|
||||
// <Tooltip
|
||||
// open={(hasRewards || hasOrder) && this.showProfileButton()}
|
||||
// title={
|
||||
// (hasRewards ? t('You can claim satoshis!') + ' ' : '') +
|
||||
// (hasOrder ? t('You have an active order') : '')
|
||||
// }
|
||||
// >
|
||||
// <ListItemAvatar sx={{ width: 30 * fontSizeFactor, height: 30 * fontSizeFactor }}>
|
||||
// <RobotAvatar
|
||||
// style={{ marginTop: -13 }}
|
||||
// statusColor={
|
||||
// (this.props.robot.activeOrderId > 0) & !this.state.profileShown
|
||||
// ? 'primary'
|
||||
// : undefined
|
||||
// }
|
||||
// nickname={this.props.robot.nickname}
|
||||
// onLoad={() =>
|
||||
// this.props.setRobot({ ...this.props.robot, avatarLoaded: true })
|
||||
// }
|
||||
// />
|
||||
// </ListItemAvatar>
|
||||
// </Tooltip>
|
||||
// <ListItemText primary={this.props.robot.nickname} />
|
||||
// </ListItemButton>
|
||||
// </div>
|
||||
// </Grid>
|
||||
|
||||
// <Grid item xs={1.9}>
|
||||
// <ListItem className='bottomItem'>
|
||||
// <ListItemIcon size='small'>
|
||||
// <IconButton
|
||||
// disabled={!this.showProfileButton()}
|
||||
// color='primary'
|
||||
// to={`/book/`}
|
||||
// component={LinkRouter}
|
||||
// >
|
||||
// <InventoryIcon />
|
||||
// </IconButton>
|
||||
// </ListItemIcon>
|
||||
// <ListItemText
|
||||
// {...typographyProps}
|
||||
// primary={this.props.info.num_public_buy_orders}
|
||||
// secondary={t('Public Buy Orders')}
|
||||
// />
|
||||
// </ListItem>
|
||||
// </Grid>
|
||||
|
||||
// <Grid item xs={1.9}>
|
||||
// <ListItem className='bottomItem'>
|
||||
// <ListItemIcon size='small'>
|
||||
// <IconButton
|
||||
// disabled={!this.showProfileButton()}
|
||||
// color='primary'
|
||||
// to={`/book/`}
|
||||
// component={LinkRouter}
|
||||
// >
|
||||
// <SellIcon />
|
||||
// </IconButton>
|
||||
// </ListItemIcon>
|
||||
// <ListItemText
|
||||
// {...typographyProps}
|
||||
// primary={this.props.info.num_public_sell_orders}
|
||||
// secondary={t('Public Sell Orders')}
|
||||
// />
|
||||
// </ListItem>
|
||||
// </Grid>
|
||||
|
||||
// <Grid item xs={1.9}>
|
||||
// <ListItem className='bottomItem'>
|
||||
// <ListItemIcon size='small'>
|
||||
// <IconButton
|
||||
// disabled={!this.showProfileButton()}
|
||||
// color='primary'
|
||||
// to={`/`}
|
||||
// component={LinkRouter}
|
||||
// >
|
||||
// <SmartToyIcon />
|
||||
// </IconButton>
|
||||
// </ListItemIcon>
|
||||
// <ListItemText
|
||||
// {...typographyProps}
|
||||
// primary={this.props.info.active_robots_today}
|
||||
// secondary={t('Today Active Robots')}
|
||||
// />
|
||||
// </ListItem>
|
||||
// </Grid>
|
||||
|
||||
// <Grid item xs={1.9}>
|
||||
// <ListItem className='bottomItem'>
|
||||
// <ListItemIcon size='small'>
|
||||
// <IconButton color='primary' onClick={this.handleClickOpenExchangeSummary}>
|
||||
// <PriceChangeIcon />
|
||||
// </IconButton>
|
||||
// </ListItemIcon>
|
||||
// <ListItemText
|
||||
// {...typographyProps}
|
||||
// primary={this.props.info.last_day_nonkyc_btc_premium + '%'}
|
||||
// secondary={t('24h Avg Premium')}
|
||||
// />
|
||||
// </ListItem>
|
||||
// </Grid>
|
||||
|
||||
// <Grid item xs={1.5}>
|
||||
// <ListItem className='bottomItem'>
|
||||
// <ListItemIcon size='small'>
|
||||
// <IconButton color='primary' onClick={this.handleClickOpenExchangeSummary}>
|
||||
// <PercentIcon />
|
||||
// </IconButton>
|
||||
// </ListItemIcon>
|
||||
// <ListItemText
|
||||
// {...typographyProps}
|
||||
// primary={(this.props.info.maker_fee + this.props.info.taker_fee) * 100}
|
||||
// secondary={t('Trade Fee')}
|
||||
// />
|
||||
// </ListItem>
|
||||
// </Grid>
|
||||
|
||||
// <Grid container item xs={1}>
|
||||
// <Grid item xs={6}>
|
||||
// {this.LangSelect()}
|
||||
// </Grid>
|
||||
// <Grid item xs={3}>
|
||||
// <Tooltip enterTouchDelay={250} title={t('Show community and support links')}>
|
||||
// <IconButton
|
||||
// color='primary'
|
||||
// aria-label='Community'
|
||||
// onClick={this.handleClickOpenCommunity}
|
||||
// >
|
||||
// <PeopleIcon />
|
||||
// </IconButton>
|
||||
// </Tooltip>
|
||||
// </Grid>
|
||||
// <Grid item xs={3}>
|
||||
// <Tooltip enterTouchDelay={250} title={t('Show stats for nerds')}>
|
||||
// <IconButton
|
||||
// color='primary'
|
||||
// aria-label='Stats for Nerds'
|
||||
// onClick={this.handleClickOpenStatsForNerds}
|
||||
// >
|
||||
// <BarChartIcon />
|
||||
// </IconButton>
|
||||
// </Tooltip>
|
||||
// </Grid>
|
||||
// </Grid>
|
||||
// </Grid>
|
||||
// </Paper>
|
||||
// );
|
||||
// };
|
||||
|
||||
// bottomBarPhone = () => {
|
||||
// const { t } = this.props;
|
||||
// const hasRewards = this.props.robot.earnedRewards > 0;
|
||||
// const hasOrder = !!(
|
||||
// (this.props.info.active_order_id > 0) &
|
||||
// !this.state.profileShown &
|
||||
// this.props.robot.avatarLoaded
|
||||
// );
|
||||
// return (
|
||||
// <Paper
|
||||
// elevation={6}
|
||||
// style={{ height: '2.85em', width: `${(this.props.windowSize.width / 16) * 14}em` }}
|
||||
// >
|
||||
// <Grid container>
|
||||
// <Grid item xs={1.6}>
|
||||
// <div style={{ display: this.showProfileButton() ? '' : 'none' }}>
|
||||
// <Tooltip
|
||||
// open={(hasRewards || hasOrder) && this.showProfileButton()}
|
||||
// title={
|
||||
// (hasRewards ? t('You can claim satoshis!') + ' ' : '') +
|
||||
// (hasOrder ? t('You have an active order') : '')
|
||||
// }
|
||||
// >
|
||||
// <IconButton
|
||||
// onClick={this.handleClickOpenProfile}
|
||||
// sx={{ margin: 0, bottom: 17, right: 8 }}
|
||||
// >
|
||||
// <RobotAvatar
|
||||
// style={{ width: 55, height: 55 }}
|
||||
// avatarClass='phoneFlippedSmallAvatar'
|
||||
// statusColor={
|
||||
// (this.props.activeOrderId > 0) & !this.state.profileShown
|
||||
// ? 'primary'
|
||||
// : undefined
|
||||
// }
|
||||
// nickname={this.props.robot.nickname}
|
||||
// onLoad={() => this.props.setRobot({ ...this.props.robot, avatarLoaded: true })}
|
||||
// />
|
||||
// </IconButton>
|
||||
// </Tooltip>
|
||||
// </div>
|
||||
// </Grid>
|
||||
|
||||
// <Grid item xs={1.6} align='center'>
|
||||
// <Tooltip enterTouchDelay={300} title={t('Number of public BUY orders')}>
|
||||
// <IconButton
|
||||
// disabled={!this.showProfileButton()}
|
||||
// color='primary'
|
||||
// to={`/book/`}
|
||||
// component={LinkRouter}
|
||||
// >
|
||||
// <Badge badgeContent={this.props.info.num_public_buy_orders} color='action'>
|
||||
// <InventoryIcon />
|
||||
// </Badge>
|
||||
// </IconButton>
|
||||
// </Tooltip>
|
||||
// </Grid>
|
||||
|
||||
// <Grid item xs={1.6} align='center'>
|
||||
// <Tooltip enterTouchDelay={300} title={t('Number of public SELL orders')}>
|
||||
// <IconButton
|
||||
// disabled={!this.showProfileButton()}
|
||||
// color='primary'
|
||||
// to={`/book/`}
|
||||
// component={LinkRouter}
|
||||
// >
|
||||
// <Badge badgeContent={this.props.info.num_public_sell_orders} color='action'>
|
||||
// <SellIcon />
|
||||
// </Badge>
|
||||
// </IconButton>
|
||||
// </Tooltip>
|
||||
// </Grid>
|
||||
|
||||
// <Grid item xs={1.6} align='center'>
|
||||
// <Tooltip enterTouchDelay={300} title={t('Today active robots')}>
|
||||
// <IconButton
|
||||
// disabled={!this.showProfileButton()}
|
||||
// color='primary'
|
||||
// to={`/`}
|
||||
// component={LinkRouter}
|
||||
// >
|
||||
// <Badge badgeContent={this.props.info.active_robots_today} color='action'>
|
||||
// <SmartToyIcon />
|
||||
// </Badge>
|
||||
// </IconButton>
|
||||
// </Tooltip>
|
||||
// </Grid>
|
||||
|
||||
// <Grid item xs={1.8} align='center'>
|
||||
// <Tooltip enterTouchDelay={300} title={t('24h non-KYC bitcoin premium')}>
|
||||
// <IconButton color='primary' onClick={this.handleClickOpenExchangeSummary}>
|
||||
// <Badge
|
||||
// badgeContent={this.props.info.last_day_nonkyc_btc_premium + '%'}
|
||||
// color='action'
|
||||
// >
|
||||
// <PriceChangeIcon />
|
||||
// </Badge>
|
||||
// </IconButton>
|
||||
// </Tooltip>
|
||||
// </Grid>
|
||||
|
||||
// <Grid container item xs={3.8}>
|
||||
// <Grid item xs={6}>
|
||||
// {this.LangSelect()}
|
||||
// </Grid>
|
||||
// <Grid item xs={3}>
|
||||
// <Tooltip enterTouchDelay={250} title={t('Show community and support links')}>
|
||||
// <IconButton
|
||||
// color='primary'
|
||||
// aria-label='Community'
|
||||
// onClick={this.handleClickOpenCommunity}
|
||||
// >
|
||||
// <PeopleIcon />
|
||||
// </IconButton>
|
||||
// </Tooltip>
|
||||
// </Grid>
|
||||
// <Grid item xs={3}>
|
||||
// <Tooltip enterTouchDelay={250} title={t('Show stats for nerds')}>
|
||||
// <IconButton
|
||||
// color='primary'
|
||||
// aria-label='Stats for Nerds'
|
||||
// onClick={() => this.props.fetchInfo()}
|
||||
// onClick={this.handleClickOpenStatsForNerds}
|
||||
// >
|
||||
// <BarChartIcon />
|
||||
// </IconButton>
|
||||
// </Tooltip>
|
||||
// </Grid>
|
||||
// </Grid>
|
||||
// </Grid>
|
||||
// </Paper>
|
||||
// );
|
||||
// };
|
||||
|
||||
// render() {
|
||||
// return (
|
||||
// <div>
|
||||
|
||||
// <UpdateClientDialog
|
||||
// open={this.state.openUpdateClient}
|
||||
// coordinatorVersion={this.props.info.coordinatorVersion}
|
||||
// clientVersion={this.props.info.clientVersion}
|
||||
// handleClickClose={() =>
|
||||
// this.props.setInfo({ ...this.props.info, openUpdateClient: false })
|
||||
// }
|
||||
// />
|
||||
|
||||
// <ExchangeSummaryDialog
|
||||
// open={this.state.openExchangeSummary}
|
||||
// handleClickCloseExchangeSummary={this.handleClickCloseExchangeSummary}
|
||||
// numPublicBuyOrders={this.props.info.num_public_buy_orders}
|
||||
// numPublicSellOrders={this.props.info.num_public_sell_orders}
|
||||
// bookLiquidity={this.props.info.book_liquidity}
|
||||
// activeRobotsToday={this.props.info.active_robots_today}
|
||||
// lastDayNonkycBtcPremium={this.props.info.last_day_nonkyc_btc_premium}
|
||||
// makerFee={this.props.info.maker_fee}
|
||||
// takerFee={this.props.info.taker_fee}
|
||||
// swapFeeRate={this.props.info.current_swap_fee_rate}
|
||||
// />
|
||||
|
||||
// <ProfileDialog
|
||||
// open={this.state.openProfile}
|
||||
// handleClickCloseProfile={this.handleClickCloseProfile}
|
||||
// nickname={this.props.robot.nickname}
|
||||
// activeOrderId={this.props.robot.activeOrderId}
|
||||
// lastOrderId={this.props.robot.lastOrderId}
|
||||
// referralCode={this.props.robot.referralCode}
|
||||
// tgEnabled={this.props.robot.tgEnabled}
|
||||
// tgBotName={this.props.robot.tgBotName}
|
||||
// tgToken={this.props.robot.tgToken}
|
||||
// handleSubmitInvoiceClicked={this.handleSubmitInvoiceClicked}
|
||||
// showRewardsSpinner={this.state.showRewardsSpinner}
|
||||
// withdrawn={this.props.info.withdrawn}
|
||||
// badInvoice={this.props.info.badInvoice}
|
||||
// earnedRewards={this.props.robot.earnedRewards}
|
||||
// updateRobot={(newParam) => this.props.setRobot({ ...robot, ...newParam })}
|
||||
// stealthInvoices={this.props.robot.stealthInvoices}
|
||||
// handleSetStealthInvoice={this.handleSetStealthInvoice}
|
||||
// />
|
||||
|
||||
// <StatsDialog
|
||||
// open={this.state.openStatsForNerds}
|
||||
// handleClickCloseStatsForNerds={this.handleClickCloseStatsForNerds}
|
||||
// coordinatorVersion={this.props.info.coordinatorVersion}
|
||||
// clientVersion={this.props.info.clientVersion}
|
||||
// lndVersion={this.props.info.lnd_version}
|
||||
// network={this.props.info.network}
|
||||
// nodeAlias={this.props.info.node_alias}
|
||||
// nodeId={this.props.info.node_id}
|
||||
// alternativeName={this.props.info.alternative_name}
|
||||
// alternativeSite={this.props.info.alternative_site}
|
||||
// commitHash={this.props.info.robosats_running_commit_hash}
|
||||
// lastDayVolume={this.props.info.last_day_volume}
|
||||
// lifetimeVolume={this.props.info.lifetime_volume}
|
||||
// />
|
||||
|
||||
// <MediaQuery minWidth={1200}>{this.bottomBarDesktop()}</MediaQuery>
|
||||
|
||||
// <MediaQuery maxWidth={1199}>{this.bottomBarPhone()}</MediaQuery>
|
||||
// </div>
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
|
||||
// export default NavBar;
|
@ -0,0 +1,4 @@
|
||||
import NavBar from './NavBar';
|
||||
|
||||
export type Page = 'robot' | 'order' | 'create' | 'offers' | 'settings' | 'none';
|
||||
export default NavBar;
|
@ -44,12 +44,10 @@ import ArticleIcon from '@mui/icons-material/Article';
|
||||
import HourglassTopIcon from '@mui/icons-material/HourglassTop';
|
||||
import CheckIcon from '@mui/icons-material/Check';
|
||||
|
||||
import { pn } from '../../utils/prettyNumbers';
|
||||
import { pn, getWebln, statusBadgeColor } from '../../utils';
|
||||
import { systemClient } from '../../services/System';
|
||||
import { getWebln } from '../../utils/webln';
|
||||
import { apiClient } from '../../services/api';
|
||||
import RobotAvatar from '../../components/RobotAvatar';
|
||||
import statusBadgeColor from '../../utils/statusBadgeColor';
|
||||
import { PaymentStringAsIcons } from '../../components/PaymentMethods';
|
||||
|
||||
class OrderPage extends Component {
|
||||
|
@ -0,0 +1,36 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Grid, Paper, useTheme } from '@mui/material';
|
||||
import SettingsForm from '../../components/SettingsForm';
|
||||
import { Settings } from '../../models';
|
||||
|
||||
interface SettingsPageProps {
|
||||
settings: Settings;
|
||||
setSettings: (state: Settings) => void;
|
||||
windowSize: { width: number; height: number };
|
||||
}
|
||||
|
||||
const SettingsPage = ({ settings, setSettings, windowSize }: SettingsPageProps): JSX.Element => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
const maxHeight = windowSize.height * 0.85 - 3;
|
||||
|
||||
return (
|
||||
<Paper
|
||||
elevation={12}
|
||||
sx={{ padding: '0.6em', width: '18em', maxHeight: `${maxHeight}em`, overflow: 'auto' }}
|
||||
>
|
||||
<Grid container>
|
||||
<Grid item>
|
||||
<SettingsForm
|
||||
settings={settings}
|
||||
setSettings={setSettings}
|
||||
networt={window.NativeRobosats}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Paper>
|
||||
);
|
||||
};
|
||||
|
||||
export default SettingsPage;
|
@ -10,8 +10,6 @@ import {
|
||||
CircularProgress,
|
||||
IconButton,
|
||||
} from '@mui/material';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { InfoDialog } from '../components/Dialogs';
|
||||
|
||||
import SmartToyIcon from '@mui/icons-material/SmartToy';
|
||||
import CasinoIcon from '@mui/icons-material/Casino';
|
||||
@ -21,9 +19,8 @@ import DownloadIcon from '@mui/icons-material/Download';
|
||||
import { RoboSatsNoTextIcon } from '../components/Icons';
|
||||
|
||||
import { sha256 } from 'js-sha256';
|
||||
import { genBase62Token, tokenStrength } from '../utils/token';
|
||||
import { genKey } from '../utils/pgp';
|
||||
import { saveAsJson } from '../utils/saveFile';
|
||||
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';
|
||||
@ -32,7 +29,6 @@ class UserGenPage extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
openInfo: false,
|
||||
tokenHasChanged: false,
|
||||
inputToken: '',
|
||||
found: false,
|
||||
@ -46,7 +42,10 @@ class UserGenPage extends Component {
|
||||
// Displays the existing one
|
||||
if (this.props.robot.nickname != null) {
|
||||
this.setState({ inputToken: this.props.robot.token });
|
||||
} else if (this.props.robot.token) {
|
||||
} else if (
|
||||
this.props.robot.token ||
|
||||
(window.NativeRobosats && systemClient.getCookie('robot_token'))
|
||||
) {
|
||||
this.setState({ inputToken: this.props.robot.token });
|
||||
this.getGeneratedUser(this.props.robot.token);
|
||||
} else {
|
||||
@ -77,6 +76,13 @@ class UserGenPage extends Component {
|
||||
requestBody.then((body) =>
|
||||
apiClient.post('/api/user/', body).then((data) => {
|
||||
this.setState({ found: data.found, bad_request: data.bad_request });
|
||||
this.props.setOrder(
|
||||
data.active_order_id
|
||||
? data.active_order_id
|
||||
: data.last_order_id
|
||||
? data.last_order_id
|
||||
: this.props.order,
|
||||
);
|
||||
// Add nick and token to App state (token only if not a bad request)
|
||||
data.bad_request
|
||||
? this.props.setRobot({
|
||||
@ -89,6 +95,9 @@ class UserGenPage extends Component {
|
||||
earnedRewards: data.earned_rewards ?? this.props.eartnedRewards,
|
||||
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,
|
||||
@ -156,14 +165,6 @@ class UserGenPage extends Component {
|
||||
});
|
||||
};
|
||||
|
||||
handleClickOpenInfo = () => {
|
||||
this.setState({ openInfo: true });
|
||||
};
|
||||
|
||||
handleCloseInfo = () => {
|
||||
this.setState({ openInfo: false });
|
||||
};
|
||||
|
||||
createJsonFile = () => {
|
||||
return {
|
||||
token: this.props.robot.token,
|
||||
@ -369,44 +370,6 @@ class UserGenPage extends Component {
|
||||
</Tooltip>
|
||||
)}
|
||||
</Grid>
|
||||
<Grid item xs={12} align='center'>
|
||||
<ButtonGroup variant='contained' aria-label='outlined primary button group'>
|
||||
<Button
|
||||
disabled={
|
||||
this.props.robot.loading ||
|
||||
!(this.props.robot.token
|
||||
? systemClient.getCookie('robot_token') === this.props.robot.token
|
||||
: true)
|
||||
}
|
||||
color='primary'
|
||||
to='/make/'
|
||||
component={Link}
|
||||
>
|
||||
{t('Make Order')}
|
||||
</Button>
|
||||
<Button color='inherit' style={{ color: '#111111' }} onClick={this.handleClickOpenInfo}>
|
||||
{t('Info')}
|
||||
</Button>
|
||||
<InfoDialog
|
||||
open={Boolean(this.state.openInfo)}
|
||||
maxAmount='4,000,000'
|
||||
onClose={this.handleCloseInfo}
|
||||
/>
|
||||
<Button
|
||||
disabled={
|
||||
this.props.robot.loading ||
|
||||
!(this.props.robot.token
|
||||
? systemClient.getCookie('robot_token') == this.props.robot.token
|
||||
: true)
|
||||
}
|
||||
color='secondary'
|
||||
to='/book/'
|
||||
component={Link}
|
||||
>
|
||||
{t('View Book')}
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12} align='center' sx={{ width: '26.43em' }}>
|
||||
<Grid item>
|
||||
|
@ -19,20 +19,17 @@ import {
|
||||
import { DataGrid, GridPagination } from '@mui/x-data-grid';
|
||||
import currencyDict from '../../../static/assets/currencies.json';
|
||||
import { Book, Favorites } from '../../models';
|
||||
import filterOrders from '../../utils/filterOrders';
|
||||
import { filterOrders, hexToRgb, statusBadgeColor, pn, amountToString } from '../../utils';
|
||||
import BookControl from './BookControl';
|
||||
|
||||
import { FlagWithProps } from '../Icons';
|
||||
import { pn, amountToString } from '../../utils/prettyNumbers';
|
||||
import { PaymentStringAsIcons } from '../PaymentMethods';
|
||||
import RobotAvatar from '../RobotAvatar';
|
||||
import hexToRgb from '../../utils/hexToRgb';
|
||||
import statusBadgeColor from '../../utils/statusBadgeColor';
|
||||
|
||||
// Icons
|
||||
import { Fullscreen, FullscreenExit, Refresh } from '@mui/icons-material';
|
||||
|
||||
interface Props {
|
||||
interface BookTableProps {
|
||||
clickRefresh?: () => void;
|
||||
book: Book;
|
||||
fav?: Favorites;
|
||||
@ -40,7 +37,7 @@ interface Props {
|
||||
maxHeight: number;
|
||||
fullWidth?: number;
|
||||
fullHeight?: number;
|
||||
elevation: number;
|
||||
elevation?: number;
|
||||
defaultFullscreen?: boolean;
|
||||
fillContainer?: boolean;
|
||||
showControls?: boolean;
|
||||
@ -48,16 +45,17 @@ interface Props {
|
||||
showNoResults?: boolean;
|
||||
onCurrencyChange?: (e: any) => void;
|
||||
onTypeChange?: (mouseEvent: any, val: number) => void;
|
||||
onOrderClicked?: (id: number) => void;
|
||||
}
|
||||
|
||||
const BookTable = ({
|
||||
clickRefresh,
|
||||
book,
|
||||
fav,
|
||||
maxWidth,
|
||||
maxHeight,
|
||||
fullWidth,
|
||||
fullHeight,
|
||||
fav = { currency: 1, type: 0 },
|
||||
maxWidth = 100,
|
||||
maxHeight = 70,
|
||||
fullWidth = 100,
|
||||
fullHeight = 70,
|
||||
defaultFullscreen = false,
|
||||
elevation = 6,
|
||||
fillContainer = false,
|
||||
@ -66,7 +64,8 @@ const BookTable = ({
|
||||
showNoResults = true,
|
||||
onCurrencyChange,
|
||||
onTypeChange,
|
||||
}: Props): JSX.Element => {
|
||||
onOrderClicked = () => null,
|
||||
}: BookTableProps): JSX.Element => {
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
const history = useHistory();
|
||||
@ -756,7 +755,7 @@ const BookTable = ({
|
||||
setPageSize(newPageSize);
|
||||
setUseDefaultPageSize(false);
|
||||
}}
|
||||
onRowClick={(params: any) => history.push('/order/' + params.row.id)} // Whole row is clickable, but the mouse only looks clickly in some places.
|
||||
onRowClick={(params: any) => onOrderClicked(params.row.id)}
|
||||
/>
|
||||
</Paper>
|
||||
);
|
||||
|
@ -23,12 +23,10 @@ import { useTranslation } from 'react-i18next';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { Order, LimitList } from '../../../models';
|
||||
import RobotAvatar from '../../RobotAvatar';
|
||||
import { amountToString } from '../../../utils/prettyNumbers';
|
||||
import { amountToString, matchMedian, statusBadgeColor } from '../../../utils';
|
||||
import currencyDict from '../../../../static/assets/currencies.json';
|
||||
import { PaymentStringAsIcons } from '../../PaymentMethods';
|
||||
import getNivoScheme from '../NivoScheme';
|
||||
import median from '../../../utils/match';
|
||||
import statusBadgeColor from '../../../utils/statusBadgeColor';
|
||||
|
||||
interface DepthChartProps {
|
||||
orders: Order[];
|
||||
@ -38,7 +36,7 @@ interface DepthChartProps {
|
||||
maxWidth: number;
|
||||
maxHeight: number;
|
||||
fillContainer?: boolean;
|
||||
elevation: number;
|
||||
elevation?: number;
|
||||
}
|
||||
|
||||
const DepthChart: React.FC<DepthChartProps> = ({
|
||||
@ -93,7 +91,7 @@ const DepthChart: React.FC<DepthChartProps> = ({
|
||||
if (xType === 'base_amount') {
|
||||
const prices: number[] = enrichedOrders.map((order) => order?.base_amount || 0);
|
||||
|
||||
const medianValue = ~~median(prices);
|
||||
const medianValue = ~~matchMedian(prices);
|
||||
const maxValue = prices.sort((a, b) => b - a).slice(0, 1)[0] || 1500;
|
||||
const maxRange = maxValue - medianValue;
|
||||
const rangeSteps = maxRange / 10;
|
||||
@ -104,7 +102,7 @@ const DepthChart: React.FC<DepthChartProps> = ({
|
||||
} else {
|
||||
if (lastDayPremium === undefined) {
|
||||
const premiums: number[] = enrichedOrders.map((order) => order?.premium || 0);
|
||||
setCenter(~~median(premiums));
|
||||
setCenter(~~matchMedian(premiums));
|
||||
} else {
|
||||
setCenter(lastDayPremium);
|
||||
}
|
||||
@ -217,62 +215,60 @@ const DepthChart: React.FC<DepthChartProps> = ({
|
||||
/>
|
||||
);
|
||||
|
||||
// Unkown Bug. Temporarily silenced until cause is found.
|
||||
|
||||
// const generateTooltip: React.FunctionComponent<PointTooltipProps> = (
|
||||
// pointTooltip: PointTooltipProps,
|
||||
// ) => {
|
||||
// const order: Order = pointTooltip.point.data.order;
|
||||
// return order ? (
|
||||
// <Paper elevation={12} style={{ padding: 10, width: 250 }}>
|
||||
// <Grid container justifyContent='space-between'>
|
||||
// <Grid item xs={3}>
|
||||
// <Grid container justifyContent='center' alignItems='center'>
|
||||
// <RobotAvatar
|
||||
// nickname={order.maker_nick}
|
||||
// orderType={order.type}
|
||||
// statusColor={statusBadgeColor(order.maker_status)}
|
||||
// tooltip={t(order.maker_status)}
|
||||
// />
|
||||
// </Grid>
|
||||
// </Grid>
|
||||
// <Grid item xs={8}>
|
||||
// <Grid container direction='column' justifyContent='center' alignItems='flex-start'>
|
||||
// <Box>{order.maker_nick}</Box>
|
||||
// <Box>
|
||||
// <Grid
|
||||
// container
|
||||
// direction='column'
|
||||
// justifyContent='flex-start'
|
||||
// alignItems='flex-start'
|
||||
// >
|
||||
// <Grid item xs={12}>
|
||||
// {amountToString(
|
||||
// order.amount,
|
||||
// order.has_range,
|
||||
// order.min_amount,
|
||||
// order.max_amount,
|
||||
// )}{' '}
|
||||
// {currencyDict[order.currency]}
|
||||
// </Grid>
|
||||
// <Grid item xs={12}>
|
||||
// <PaymentStringAsIcons
|
||||
// othersText={t('Others')}
|
||||
// verbose={true}
|
||||
// size={20}
|
||||
// text={order.payment_method}
|
||||
// />
|
||||
// </Grid>
|
||||
// </Grid>
|
||||
// </Box>
|
||||
// </Grid>
|
||||
// </Grid>
|
||||
// </Grid>
|
||||
// </Paper>
|
||||
// ) : (
|
||||
// <></>
|
||||
// );
|
||||
// };
|
||||
const generateTooltip: React.FunctionComponent<PointTooltipProps> = (
|
||||
pointTooltip: PointTooltipProps,
|
||||
) => {
|
||||
const order: Order = pointTooltip.point.data.order;
|
||||
return order ? (
|
||||
<Paper elevation={12} style={{ padding: 10, width: 250 }}>
|
||||
<Grid container justifyContent='space-between'>
|
||||
<Grid item xs={3}>
|
||||
<Grid container justifyContent='center' alignItems='center'>
|
||||
<RobotAvatar
|
||||
nickname={order.maker_nick}
|
||||
orderType={order.type}
|
||||
statusColor={statusBadgeColor(order.maker_status)}
|
||||
tooltip={t(order.maker_status)}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid item xs={8}>
|
||||
<Grid container direction='column' justifyContent='center' alignItems='flex-start'>
|
||||
<Box>{order.maker_nick}</Box>
|
||||
<Box>
|
||||
<Grid
|
||||
container
|
||||
direction='column'
|
||||
justifyContent='flex-start'
|
||||
alignItems='flex-start'
|
||||
>
|
||||
<Grid item xs={12}>
|
||||
{amountToString(
|
||||
order.amount,
|
||||
order.has_range,
|
||||
order.min_amount,
|
||||
order.max_amount,
|
||||
)}{' '}
|
||||
{currencyDict[order.currency]}
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<PaymentStringAsIcons
|
||||
othersText={t('Others')}
|
||||
verbose={true}
|
||||
size={20}
|
||||
text={order.payment_method}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Paper>
|
||||
) : (
|
||||
<></>
|
||||
);
|
||||
};
|
||||
|
||||
const formatAxisX = (value: number): string => {
|
||||
if (xType === 'base_amount') {
|
||||
@ -364,7 +360,7 @@ const DepthChart: React.FC<DepthChartProps> = ({
|
||||
useMesh={true}
|
||||
animate={false}
|
||||
crosshairType='cross'
|
||||
// tooltip={generateTooltip}
|
||||
tooltip={generateTooltip}
|
||||
onClick={handleOnClick}
|
||||
axisLeft={{
|
||||
tickSize: 5,
|
||||
|
@ -14,7 +14,7 @@ import {
|
||||
Link,
|
||||
} from '@mui/material';
|
||||
|
||||
import { saveAsJson } from '../../utils/saveFile';
|
||||
import { saveAsJson } from '../../utils';
|
||||
import { systemClient } from '../../services/System';
|
||||
|
||||
// Icons
|
||||
|
@ -21,10 +21,10 @@ import Flags from 'country-flag-icons/react/3x2';
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
handleClickCloseCommunity: () => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const CommunityDialog = ({ open = false, handleClickCloseCommunity }: Props): JSX.Element => {
|
||||
const CommunityDialog = ({ open = false, onClose }: Props): JSX.Element => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const flagProps = {
|
||||
@ -39,7 +39,7 @@ const CommunityDialog = ({ open = false, handleClickCloseCommunity }: Props): JS
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
onClose={handleClickCloseCommunity}
|
||||
onClose={onClose}
|
||||
aria-labelledby='community-dialog-title'
|
||||
aria-describedby='community-description'
|
||||
>
|
||||
|
32
frontend/src/components/Dialogs/Confirmation.tsx
Normal file
32
frontend/src/components/Dialogs/Confirmation.tsx
Normal file
@ -0,0 +1,32 @@
|
||||
import React from 'react';
|
||||
import { NoRobotDialog, StoreTokenDialog } from '.';
|
||||
import { Page } from '../../basic/NavBar';
|
||||
|
||||
interface ConfirmationDialogProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
setPage: (state: Page) => void;
|
||||
onClickDone: () => void;
|
||||
hasRobot: boolean;
|
||||
}
|
||||
|
||||
const ConfirmationDialog = ({
|
||||
open,
|
||||
onClose,
|
||||
hasRobot,
|
||||
setPage,
|
||||
onClickDone,
|
||||
}: ConfirmationDialogProps): JSX.Element => {
|
||||
return hasRobot ? (
|
||||
<StoreTokenDialog
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
onClickBack={onClose}
|
||||
onClickDone={onClickDone}
|
||||
/>
|
||||
) : (
|
||||
<NoRobotDialog open={open} onClose={onClose} setPage={setPage} />
|
||||
);
|
||||
};
|
||||
|
||||
export default ConfirmationDialog;
|
@ -21,11 +21,11 @@ import PriceChangeIcon from '@mui/icons-material/PriceChange';
|
||||
import BookIcon from '@mui/icons-material/Book';
|
||||
import LinkIcon from '@mui/icons-material/Link';
|
||||
|
||||
import { pn } from '../../utils/prettyNumbers';
|
||||
import { pn } from '../../utils';
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
handleClickCloseExchangeSummary: () => void;
|
||||
onClose: () => void;
|
||||
numPublicBuyOrders: number;
|
||||
numPublicSellOrders: number;
|
||||
bookLiquidity: number;
|
||||
@ -36,9 +36,9 @@ interface Props {
|
||||
swapFeeRate: number;
|
||||
}
|
||||
|
||||
const ExchangeSummaryDialog = ({
|
||||
const CoordinatorSummaryDialog = ({
|
||||
open = false,
|
||||
handleClickCloseExchangeSummary,
|
||||
onClose,
|
||||
numPublicBuyOrders,
|
||||
numPublicSellOrders,
|
||||
bookLiquidity,
|
||||
@ -54,15 +54,10 @@ const ExchangeSummaryDialog = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
onClose={handleClickCloseExchangeSummary}
|
||||
aria-labelledby='exchange-summary-title'
|
||||
aria-describedby='exchange-summary-description'
|
||||
>
|
||||
<Dialog open={open} onClose={onClose}>
|
||||
<DialogContent>
|
||||
<Typography component='h5' variant='h5'>
|
||||
{t('Exchange Summary')}
|
||||
{t('Coordinator Summary')}
|
||||
</Typography>
|
||||
|
||||
<List dense>
|
||||
@ -189,4 +184,4 @@ const ExchangeSummaryDialog = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default ExchangeSummaryDialog;
|
||||
export default CoordinatorSummaryDialog;
|
@ -14,6 +14,7 @@ import {
|
||||
} from '@mui/material';
|
||||
import SmoothImage from 'react-smooth-image';
|
||||
import MediaQuery from 'react-responsive';
|
||||
import { pn } from '../../utils';
|
||||
|
||||
// Icons
|
||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
||||
@ -148,7 +149,7 @@ const InfoDialog = ({ maxAmount, open, onClose }: Props): JSX.Element => {
|
||||
<p>
|
||||
{t(
|
||||
'Maximum single trade size is {{maxAmount}} Satoshis to minimize lightning routing failure. There is no limits to the number of trades per day. A robot can only have one order at a time. However, you can use multiple robots simultaneously in different browsers (remember to back up your robot tokens!).',
|
||||
{ maxAmount },
|
||||
{ maxAmount: pn(maxAmount) },
|
||||
)}{' '}
|
||||
</p>
|
||||
</Typography>
|
||||
|
@ -8,15 +8,24 @@ import {
|
||||
DialogContentText,
|
||||
Button,
|
||||
} from '@mui/material';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { Page } from '../../basic/NavBar';
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
setPage: (state: Page) => void;
|
||||
}
|
||||
|
||||
const NoRobotDialog = ({ open, onClose }: Props): JSX.Element => {
|
||||
const NoRobotDialog = ({ open, onClose, setPage }: Props): JSX.Element => {
|
||||
const { t } = useTranslation();
|
||||
const history = useHistory();
|
||||
|
||||
const handleClickGenerate = function () {
|
||||
onClose();
|
||||
setPage('robot');
|
||||
history.push('/robot');
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onClose={onClose}>
|
||||
@ -24,17 +33,12 @@ const NoRobotDialog = ({ open, onClose }: Props): JSX.Element => {
|
||||
|
||||
<DialogContent>
|
||||
<DialogContentText>
|
||||
{t('You need to generate a robot avatar in order to become an order maker')}
|
||||
{t('Generate a robot avatar first. Then create your own order.')}
|
||||
</DialogContentText>
|
||||
</DialogContent>
|
||||
|
||||
<DialogActions>
|
||||
<Button onClick={onClose} autoFocus>
|
||||
{t('Go back')}
|
||||
</Button>
|
||||
<Button onClick={onClose} to='/' component={Link}>
|
||||
{t('Generate Robot')}
|
||||
</Button>
|
||||
<Button onClick={handleClickGenerate}>{t('Generate Robot')}</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
|
@ -4,7 +4,6 @@ import { useTheme } from '@mui/material/styles';
|
||||
import { Link as LinkRouter } from 'react-router-dom';
|
||||
|
||||
import {
|
||||
Avatar,
|
||||
Badge,
|
||||
Button,
|
||||
CircularProgress,
|
||||
@ -37,55 +36,28 @@ import EmojiEventsIcon from '@mui/icons-material/EmojiEvents';
|
||||
import { UserNinjaIcon, BitcoinIcon } from '../Icons';
|
||||
|
||||
import { systemClient } from '../../services/System';
|
||||
import { getWebln } from '../../utils/webln';
|
||||
import { getHost, getWebln } from '../../utils';
|
||||
import RobotAvatar from '../RobotAvatar';
|
||||
import { apiClient } from '../../services/api';
|
||||
import { Robot } from '../../models';
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
handleClickCloseProfile: () => void;
|
||||
nickname: string;
|
||||
activeOrderId: string | number;
|
||||
lastOrderId: string | number;
|
||||
referralCode: string;
|
||||
tgEnabled: boolean;
|
||||
tgBotName: string;
|
||||
tgToken: string;
|
||||
handleSubmitInvoiceClicked: (e: any, invoice: string) => void;
|
||||
host: string;
|
||||
showRewardsSpinner: boolean;
|
||||
withdrawn: boolean;
|
||||
badInvoice: boolean | string;
|
||||
earnedRewards: number;
|
||||
stealthInvoices: boolean;
|
||||
handleSetStealthInvoice: (wantsStealth: boolean) => void;
|
||||
updateRobot: (state: any) => void; // TODO: move to a ContextProvider
|
||||
onClose: () => void;
|
||||
robot: Robot;
|
||||
setRobot: (state: Robot) => void;
|
||||
}
|
||||
|
||||
const ProfileDialog = ({
|
||||
open = false,
|
||||
handleClickCloseProfile,
|
||||
nickname,
|
||||
activeOrderId,
|
||||
lastOrderId,
|
||||
referralCode,
|
||||
tgEnabled,
|
||||
tgBotName,
|
||||
tgToken,
|
||||
handleSubmitInvoiceClicked,
|
||||
host,
|
||||
showRewardsSpinner,
|
||||
withdrawn,
|
||||
badInvoice,
|
||||
earnedRewards,
|
||||
updateRobot,
|
||||
stealthInvoices,
|
||||
handleSetStealthInvoice,
|
||||
}: Props): JSX.Element => {
|
||||
const ProfileDialog = ({ open = false, onClose, robot, setRobot }: Props): JSX.Element => {
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
const host = getHost();
|
||||
|
||||
const [rewardInvoice, setRewardInvoice] = useState<string>('');
|
||||
const [showRewards, setShowRewards] = useState<boolean>(false);
|
||||
const [showRewardsSpinner, setShowRewardsSpinner] = useState<boolean>(false);
|
||||
const [withdrawn, setWithdrawn] = useState<boolean>(false);
|
||||
const [badInvoice, setBadInvoice] = useState<string>('');
|
||||
const [openClaimRewards, setOpenClaimRewards] = useState<boolean>(false);
|
||||
const [weblnEnabled, setWeblnEnabled] = useState<boolean>(false);
|
||||
const [openEnableTelegram, setOpenEnableTelegram] = useState<boolean>(false);
|
||||
@ -101,19 +73,19 @@ const ProfileDialog = ({
|
||||
|
||||
if (robotToken) {
|
||||
systemClient.copyToClipboard(robotToken);
|
||||
updateRobot({ copiedToken: true });
|
||||
setRobot({ ...robot, copiedToken: true });
|
||||
}
|
||||
};
|
||||
|
||||
const copyReferralCodeHandler = () => {
|
||||
systemClient.copyToClipboard(`http://${host}/ref/${referralCode}`);
|
||||
systemClient.copyToClipboard(`http://${host}/ref/${robot.referralCode}`);
|
||||
};
|
||||
|
||||
const handleWeblnInvoiceClicked = async (e: any) => {
|
||||
e.preventDefault();
|
||||
if (earnedRewards) {
|
||||
if (robot.earnedRewards) {
|
||||
const webln = await getWebln();
|
||||
const invoice = webln.makeInvoice(earnedRewards).then(() => {
|
||||
const invoice = webln.makeInvoice(robot.earnedRewards).then(() => {
|
||||
if (invoice) {
|
||||
handleSubmitInvoiceClicked(e, invoice.paymentRequest);
|
||||
}
|
||||
@ -121,15 +93,39 @@ const ProfileDialog = ({
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmitInvoiceClicked = (e: any, rewardInvoice: string) => {
|
||||
setBadInvoice('');
|
||||
setShowRewardsSpinner(true);
|
||||
|
||||
apiClient
|
||||
.post('/api/reward/', {
|
||||
invoice: rewardInvoice,
|
||||
})
|
||||
.then((data: any) => {
|
||||
setBadInvoice(data.bad_invoice ?? '');
|
||||
setShowRewardsSpinner(false);
|
||||
setWithdrawn(data.successful_withdrawal);
|
||||
setOpenClaimRewards(!data.successful_withdrawal);
|
||||
setRobot({ ...robot, earnedRewards: data.successful_withdrawal ? 0 : robot.earnedRewards });
|
||||
});
|
||||
e.preventDefault();
|
||||
};
|
||||
|
||||
const handleClickEnableTelegram = () => {
|
||||
window.open('https://t.me/' + tgBotName + '?start=' + tgToken, '_blank').focus();
|
||||
window.open('https://t.me/' + robot.tgBotName + '?start=' + robot.tgToken, '_blank').focus();
|
||||
setOpenEnableTelegram(false);
|
||||
};
|
||||
|
||||
const setStealthInvoice = (wantsStealth: boolean) => {
|
||||
apiClient
|
||||
.put('/api/stealth/', { wantsStealth })
|
||||
.then((data) => setRobot({ ...robot, stealthInvoices: data?.wantsStealth }));
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
onClose={handleClickCloseProfile}
|
||||
onClose={onClose}
|
||||
aria-labelledby='profile-title'
|
||||
aria-describedby='profile-description'
|
||||
>
|
||||
@ -144,7 +140,7 @@ const ProfileDialog = ({
|
||||
<ListItem className='profileNickname'>
|
||||
<ListItemText secondary={t('Your robot')}>
|
||||
<Typography component='h6' variant='h6'>
|
||||
{nickname ? (
|
||||
{robot.nickname ? (
|
||||
<div style={{ position: 'relative', left: '-7px' }}>
|
||||
<div
|
||||
style={{
|
||||
@ -157,7 +153,7 @@ const ProfileDialog = ({
|
||||
>
|
||||
<BoltIcon sx={{ color: '#fcba03', height: '28px', width: '24px' }} />
|
||||
|
||||
<a>{nickname}</a>
|
||||
<a>{robot.nickname}</a>
|
||||
|
||||
<BoltIcon sx={{ color: '#fcba03', height: '28px', width: '24px' }} />
|
||||
</div>
|
||||
@ -170,17 +166,17 @@ const ProfileDialog = ({
|
||||
<RobotAvatar
|
||||
avatarClass='profileAvatar'
|
||||
style={{ width: 65, height: 65 }}
|
||||
nickname={nickname}
|
||||
nickname={robot.nickname}
|
||||
/>
|
||||
</ListItemAvatar>
|
||||
</ListItem>
|
||||
|
||||
<Divider />
|
||||
|
||||
{activeOrderId ? (
|
||||
{robot.activeOrderId ? (
|
||||
<ListItemButton
|
||||
onClick={handleClickCloseProfile}
|
||||
to={`/order/${activeOrderId}`}
|
||||
onClick={onClose}
|
||||
to={`/order/${robot.activeOrderId}`}
|
||||
component={LinkRouter}
|
||||
>
|
||||
<ListItemIcon>
|
||||
@ -189,21 +185,21 @@ const ProfileDialog = ({
|
||||
</Badge>
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary={t('One active order #{{orderID}}', { orderID: activeOrderId })}
|
||||
primary={t('One active order #{{orderID}}', { orderID: robot.activeOrderId })}
|
||||
secondary={t('Your current order')}
|
||||
/>
|
||||
</ListItemButton>
|
||||
) : lastOrderId ? (
|
||||
) : robot.lastOrderId ? (
|
||||
<ListItemButton
|
||||
onClick={handleClickCloseProfile}
|
||||
to={`/order/${lastOrderId}`}
|
||||
onClick={onClose}
|
||||
to={`/order/${robot.lastOrderId}`}
|
||||
component={LinkRouter}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<NumbersIcon color='primary' />
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary={t('Your last order #{{orderID}}', { orderID: lastOrderId })}
|
||||
primary={t('Your last order #{{orderID}}', { orderID: robot.lastOrderId })}
|
||||
secondary={t('Inactive order')}
|
||||
/>
|
||||
</ListItemButton>
|
||||
@ -254,8 +250,8 @@ const ProfileDialog = ({
|
||||
<EnableTelegramDialog
|
||||
open={openEnableTelegram}
|
||||
onClose={() => setOpenEnableTelegram(false)}
|
||||
tgBotName={tgBotName}
|
||||
tgToken={tgToken}
|
||||
tgBotName={robot.tgBotName}
|
||||
tgToken={robot.tgToken}
|
||||
onClickEnable={handleClickEnableTelegram}
|
||||
/>
|
||||
|
||||
@ -265,7 +261,7 @@ const ProfileDialog = ({
|
||||
</ListItemIcon>
|
||||
|
||||
<ListItemText>
|
||||
{tgEnabled ? (
|
||||
{robot.tgEnabled ? (
|
||||
<Typography color={theme.palette.success.main}>
|
||||
<b>{t('Telegram enabled')}</b>
|
||||
</Typography>
|
||||
@ -296,8 +292,8 @@ const ProfileDialog = ({
|
||||
label={t('Use stealth invoices')}
|
||||
control={
|
||||
<Switch
|
||||
checked={stealthInvoices}
|
||||
onChange={() => handleSetStealthInvoice(!stealthInvoices)}
|
||||
checked={robot.stealthInvoices}
|
||||
onChange={() => setStealthInvoice(!robot.stealthInvoices)}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
@ -336,7 +332,7 @@ const ProfileDialog = ({
|
||||
<ListItemText secondary={t('Share to earn 100 Sats per trade')}>
|
||||
<TextField
|
||||
label={t('Your referral link')}
|
||||
value={host + '/ref/' + referralCode}
|
||||
value={host + '/ref/' + robot.referralCode}
|
||||
size='small'
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
@ -364,12 +360,12 @@ const ProfileDialog = ({
|
||||
<ListItemText secondary={t('Your earned rewards')}>
|
||||
<Grid container>
|
||||
<Grid item xs={9}>
|
||||
<Typography>{`${earnedRewards} Sats`}</Typography>
|
||||
<Typography>{`${robot.earnedRewards} Sats`}</Typography>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={3}>
|
||||
<Button
|
||||
disabled={earnedRewards === 0}
|
||||
disabled={robot.earnedRewards === 0}
|
||||
onClick={() => setOpenClaimRewards(true)}
|
||||
variant='contained'
|
||||
size='small'
|
||||
@ -387,7 +383,7 @@ const ProfileDialog = ({
|
||||
error={!!badInvoice}
|
||||
helperText={badInvoice || ''}
|
||||
label={t('Invoice for {{amountSats}} Sats', {
|
||||
amountSats: earnedRewards,
|
||||
amountSats: robot.earnedRewards,
|
||||
})}
|
||||
size='small'
|
||||
value={rewardInvoice}
|
||||
|
@ -23,15 +23,15 @@ import EqualizerIcon from '@mui/icons-material/Equalizer';
|
||||
|
||||
import { AmbossIcon, BitcoinSignIcon, RoboSatsNoTextIcon } from '../Icons';
|
||||
|
||||
import { pn } from '../../utils/prettyNumbers';
|
||||
import { pn } from '../../utils';
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
handleClickCloseStatsForNerds: () => void;
|
||||
onClose: () => void;
|
||||
lndVersion: string;
|
||||
coordinatorVersion: string;
|
||||
clientVersion: string;
|
||||
network: string;
|
||||
network: string | undefined;
|
||||
nodeAlias: string;
|
||||
nodeId: string;
|
||||
alternativeName: string;
|
||||
@ -43,7 +43,7 @@ interface Props {
|
||||
|
||||
const StatsDialog = ({
|
||||
open = false,
|
||||
handleClickCloseStatsForNerds,
|
||||
onClose,
|
||||
lndVersion,
|
||||
coordinatorVersion,
|
||||
clientVersion,
|
||||
@ -61,7 +61,7 @@ const StatsDialog = ({
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
onClose={handleClickCloseStatsForNerds}
|
||||
onClose={onClose}
|
||||
aria-labelledby='stats-for-nerds-dialog-title'
|
||||
aria-describedby='stats-for-nerds-description'
|
||||
>
|
||||
|
@ -18,20 +18,11 @@ import ContentCopy from '@mui/icons-material/ContentCopy';
|
||||
interface Props {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
copyIconColor: string;
|
||||
onClickCopy: () => void;
|
||||
onClickBack: () => void;
|
||||
onClickDone: () => void;
|
||||
}
|
||||
|
||||
const StoreTokenDialog = ({
|
||||
open,
|
||||
onClose,
|
||||
copyIconColor,
|
||||
onClickCopy,
|
||||
onClickBack,
|
||||
onClickDone,
|
||||
}: Props): JSX.Element => {
|
||||
const StoreTokenDialog = ({ open, onClose, onClickBack, onClickDone }: Props): JSX.Element => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
@ -45,7 +36,7 @@ const StoreTokenDialog = ({
|
||||
)}
|
||||
</DialogContentText>
|
||||
<br />
|
||||
<Grid align='center'>
|
||||
<Grid container>
|
||||
<TextField
|
||||
sx={{ width: '100%', maxWidth: '550px' }}
|
||||
disabled
|
||||
@ -56,8 +47,12 @@ const StoreTokenDialog = ({
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<Tooltip disableHoverListener enterTouchDelay={0} title={t('Copied!')}>
|
||||
<IconButton onClick={onClickCopy}>
|
||||
<ContentCopy color={copyIconColor} />
|
||||
<IconButton
|
||||
onClick={() =>
|
||||
systemClient.copyToClipboard(systemClient.getCookie('robot_token'))
|
||||
}
|
||||
>
|
||||
<ContentCopy color='primary' />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
),
|
||||
|
@ -9,7 +9,6 @@ import {
|
||||
Divider,
|
||||
List,
|
||||
ListItemText,
|
||||
ListItem,
|
||||
ListItemIcon,
|
||||
ListItemButton,
|
||||
Typography,
|
||||
@ -23,19 +22,19 @@ interface Props {
|
||||
open: boolean;
|
||||
clientVersion: string;
|
||||
coordinatorVersion: string;
|
||||
handleClickClose: () => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const UpdateClientDialog = ({
|
||||
open = false,
|
||||
clientVersion,
|
||||
coordinatorVersion,
|
||||
handleClickClose,
|
||||
onClose,
|
||||
}: Props): JSX.Element => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Dialog open={open} onClose={handleClickClose}>
|
||||
<Dialog open={open} onClose={onClose}>
|
||||
<DialogContent>
|
||||
<Typography component='h5' variant='h5'>
|
||||
{t('Update your RoboSats client')}
|
||||
@ -103,7 +102,7 @@ const UpdateClientDialog = ({
|
||||
</ListItemButton>
|
||||
|
||||
<DialogActions>
|
||||
<Button onClick={handleClickClose}>{t('Go away!')}</Button>
|
||||
<Button onClick={onClose}>{t('Go away!')}</Button>
|
||||
</DialogActions>
|
||||
</List>
|
||||
</DialogContent>
|
||||
|
@ -4,7 +4,8 @@ export { default as InfoDialog } from './Info';
|
||||
export { default as LearnDialog } from './Learn';
|
||||
export { default as NoRobotDialog } from './NoRobot';
|
||||
export { default as StoreTokenDialog } from './StoreToken';
|
||||
export { default as ExchangeSummaryDialog } from './ExchangeSummary';
|
||||
export { default as ConfirmationDialog } from './Confirmation';
|
||||
export { default as CoordinatorSummaryDialog } from './CoordinatorSummary';
|
||||
export { default as ProfileDialog } from './Profile';
|
||||
export { default as StatsDialog } from './Stats';
|
||||
export { default as EnableTelegramDialog } from './EnableTelegram';
|
||||
|
@ -14,7 +14,7 @@ import {
|
||||
import { FlagWithProps } from '../Icons';
|
||||
import RangeSlider from './RangeSlider';
|
||||
import currencyDict from '../../../static/assets/currencies.json';
|
||||
import { pn } from '../../utils/prettyNumbers';
|
||||
import { pn } from '../../utils';
|
||||
|
||||
const RangeThumbComponent = function (props: object) {
|
||||
const { children, ...other } = props;
|
||||
|
@ -29,7 +29,7 @@ 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 { StoreTokenDialog, NoRobotDialog } from '../Dialogs';
|
||||
import { StoreTokenDialog, NoRobotDialog, ConfirmationDialog } from '../Dialogs';
|
||||
import { apiClient } from '../../services/api';
|
||||
import { systemClient } from '../../services/System';
|
||||
|
||||
@ -37,10 +37,11 @@ import { FlagWithProps } from '../Icons';
|
||||
import AutocompletePayments from './AutocompletePayments';
|
||||
import AmountRange from './AmountRange';
|
||||
import currencyDict from '../../../static/assets/currencies.json';
|
||||
import { pn } from '../../utils/prettyNumbers';
|
||||
import { pn } from '../../utils';
|
||||
|
||||
import { SelfImprovement, Lock, HourglassTop, DeleteSweep, Edit } from '@mui/icons-material';
|
||||
import { LoadingButton } from '@mui/lab';
|
||||
import { Page } from '../../basic/NavBar';
|
||||
|
||||
interface MakerFormProps {
|
||||
limits: { list: LimitList; loading: boolean };
|
||||
@ -55,6 +56,9 @@ interface MakerFormProps {
|
||||
onSubmit?: () => void;
|
||||
onReset?: () => void;
|
||||
submitButtonLabel?: string;
|
||||
onOrderCreated?: (id: number) => void;
|
||||
hasRobot?: boolean;
|
||||
setPage?: (state: Page) => void;
|
||||
}
|
||||
|
||||
const MakerForm = ({
|
||||
@ -70,6 +74,9 @@ const MakerForm = ({
|
||||
onSubmit = () => {},
|
||||
onReset = () => {},
|
||||
submitButtonLabel = 'Create Order',
|
||||
onOrderCreated = () => null,
|
||||
hasRobot = true,
|
||||
setPage = () => null,
|
||||
}: MakerFormProps): JSX.Element => {
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
@ -258,7 +265,10 @@ const MakerForm = ({
|
||||
};
|
||||
apiClient.post('/api/make/', body).then((data: object) => {
|
||||
setBadRequest(data.bad_request);
|
||||
data.id ? history.push('/order/' + data.id) : '';
|
||||
if (data.id) {
|
||||
history.push('/order/' + data.id);
|
||||
onOrderCreated(data.id);
|
||||
}
|
||||
setSubmittingRequest(false);
|
||||
});
|
||||
}
|
||||
@ -418,23 +428,15 @@ const MakerForm = ({
|
||||
);
|
||||
};
|
||||
|
||||
const ConfirmationDialogs = function () {
|
||||
return systemClient.getCookie('robot_token') ? (
|
||||
<StoreTokenDialog
|
||||
open={openDialogs}
|
||||
onClose={() => setOpenDialogs(false)}
|
||||
onClickCopy={() => systemClient.copyToClipboard(systemClient.getCookie('robot_token'))}
|
||||
copyIconColor={'primary'}
|
||||
onClickBack={() => setOpenDialogs(false)}
|
||||
onClickDone={handleCreateOrder}
|
||||
/>
|
||||
) : (
|
||||
<NoRobotDialog open={openDialogs} onClose={() => setOpenDialogs(false)} />
|
||||
);
|
||||
};
|
||||
return (
|
||||
<Box>
|
||||
<ConfirmationDialogs />
|
||||
<ConfirmationDialog
|
||||
open={openDialogs}
|
||||
onClose={() => setOpenDialogs(false)}
|
||||
setPage={setPage}
|
||||
onClickDone={handleCreateOrder}
|
||||
hasRobot={hasRobot}
|
||||
/>
|
||||
<Collapse in={limits.list.length == 0}>
|
||||
<div style={{ display: limits.list.length == 0 ? '' : 'none' }}>
|
||||
<LinearProgress />
|
||||
|
@ -7,7 +7,7 @@ import { apiClient } from '../../services/api';
|
||||
import placeholder from './placeholder.json';
|
||||
|
||||
interface Props {
|
||||
nickname: string;
|
||||
nickname: string | null;
|
||||
smooth?: boolean;
|
||||
flipHorizontally?: boolean;
|
||||
style?: object;
|
||||
@ -38,7 +38,7 @@ const RobotAvatar: React.FC<Props> = ({
|
||||
const [avatarSrc, setAvatarSrc] = useState<string>();
|
||||
|
||||
useEffect(() => {
|
||||
if (nickname) {
|
||||
if (nickname != null) {
|
||||
apiClient.fileImageUrl('/static/assets/avatars/' + nickname + '.png').then(setAvatarSrc);
|
||||
}
|
||||
}, [nickname]);
|
||||
@ -92,6 +92,8 @@ const RobotAvatar: React.FC<Props> = ({
|
||||
alt={nickname}
|
||||
src={avatarSrc}
|
||||
imgProps={{
|
||||
sx: { transform: flipHorizontally ? 'scaleX(-1)' : '' },
|
||||
style: { transform: flipHorizontally ? 'scaleX(-1)' : '' },
|
||||
onLoad,
|
||||
}}
|
||||
/>
|
||||
|
71
frontend/src/components/SettingsForm/SelectLanguage.tsx
Normal file
71
frontend/src/components/SettingsForm/SelectLanguage.tsx
Normal file
@ -0,0 +1,71 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Select, MenuItem, useTheme, Grid, Typography } from '@mui/material';
|
||||
import Language from '../../models/Language.model';
|
||||
|
||||
import Flags from 'country-flag-icons/react/3x2';
|
||||
import { CataloniaFlag, BasqueCountryFlag } from '../Icons';
|
||||
|
||||
const menuLanuguages = [
|
||||
{ name: 'English', i18nCode: 'en', flag: Flags['US'] },
|
||||
{ name: 'Español', i18nCode: 'es', flag: Flags['ES'] },
|
||||
{ name: 'Deutsch', i18nCode: 'de', flag: Flags['DE'] },
|
||||
{ name: 'Polski', i18nCode: 'pl', flag: Flags['PL'] },
|
||||
{ name: 'Français', i18nCode: 'fr', flag: Flags['FR'] },
|
||||
{ name: 'Русский', i18nCode: 'ru', flag: Flags['RU'] },
|
||||
{ name: 'Italiano', i18nCode: 'it', flag: Flags['IT'] },
|
||||
{ name: 'Português', i18nCode: 'pt', flag: Flags['BR'] },
|
||||
{ name: '简体', i18nCode: 'zh-si', flag: Flags['CN'] },
|
||||
{ name: '繁體', i18nCode: 'zh-tr', flag: Flags['CN'] },
|
||||
{ name: 'Svenska', i18nCode: 'sv', flag: Flags['SE'] },
|
||||
{ name: 'Čeština', i18nCode: 'cs', flag: Flags['CZ'] },
|
||||
{ name: 'ภาษาไทย', i18nCode: 'th', flag: Flags['TH'] },
|
||||
{ name: 'Català', i18nCode: 'ca', flag: CataloniaFlag },
|
||||
{ name: 'Euskara', i18nCode: 'eu', flag: BasqueCountryFlag },
|
||||
];
|
||||
|
||||
interface SelectLanguageProps {
|
||||
language: Language;
|
||||
setLanguage: (lang: Language) => void;
|
||||
}
|
||||
|
||||
const SelectLanguage = ({ language, setLanguage }: SelectLanguageProps): JSX.Element => {
|
||||
const theme = useTheme();
|
||||
const { t, i18n } = useTranslation();
|
||||
|
||||
const flagProps = {
|
||||
width: 1.5 * theme.typography.fontSize,
|
||||
height: 1.5 * theme.typography.fontSize,
|
||||
};
|
||||
|
||||
const handleChangeLang = function (e: any) {
|
||||
setLanguage(e.target.value);
|
||||
i18n.changeLanguage(e.target.value);
|
||||
};
|
||||
|
||||
return (
|
||||
<Select
|
||||
fullWidth={true}
|
||||
value={language}
|
||||
inputProps={{
|
||||
style: { textAlign: 'center' },
|
||||
}}
|
||||
onChange={handleChangeLang}
|
||||
>
|
||||
{menuLanuguages.map((language, index) => (
|
||||
<MenuItem key={index} value={language.i18nCode}>
|
||||
<Grid container>
|
||||
<Grid item style={{ width: '1.9em', position: 'relative', top: '0.15em' }}>
|
||||
<language.flag {...flagProps} />
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Typography variant='inherit'>{language.name}</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
);
|
||||
};
|
||||
|
||||
export default SelectLanguage;
|
159
frontend/src/components/SettingsForm/index.tsx
Normal file
159
frontend/src/components/SettingsForm/index.tsx
Normal file
@ -0,0 +1,159 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
Grid,
|
||||
Paper,
|
||||
Switch,
|
||||
useTheme,
|
||||
FormControlLabel,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemIcon,
|
||||
Slider,
|
||||
Typography,
|
||||
ToggleButtonGroup,
|
||||
ToggleButton,
|
||||
} from '@mui/material';
|
||||
import { Settings } from '../../models';
|
||||
import SelectLanguage from './SelectLanguage';
|
||||
import {
|
||||
Translate,
|
||||
Palette,
|
||||
LightMode,
|
||||
DarkMode,
|
||||
SettingsOverscan,
|
||||
Link,
|
||||
} from '@mui/icons-material';
|
||||
|
||||
interface SettingsFormProps {
|
||||
dense?: boolean;
|
||||
settings: Settings;
|
||||
setSettings: (state: Settings) => void;
|
||||
network?: boolean;
|
||||
}
|
||||
|
||||
const SettingsForm = ({
|
||||
dense = false,
|
||||
settings,
|
||||
setSettings,
|
||||
network = false,
|
||||
}: SettingsFormProps): JSX.Element => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
const fontSizes = [
|
||||
{ label: 'XS', value: { basic: 12, pro: 10 } },
|
||||
{ label: 'S', value: { basic: 13, pro: 11 } },
|
||||
{ label: 'M', value: { basic: 14, pro: 12 } },
|
||||
{ label: 'L', value: { basic: 15, pro: 13 } },
|
||||
{ label: 'XL', value: { basic: 16, pro: 14 } },
|
||||
];
|
||||
|
||||
return (
|
||||
<Grid container spacing={1}>
|
||||
<Grid item>
|
||||
<List dense={dense}>
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
<Translate />
|
||||
</ListItemIcon>
|
||||
<SelectLanguage
|
||||
language={settings.language}
|
||||
setLanguage={(language) => setSettings({ ...settings, language })}
|
||||
/>
|
||||
</ListItem>
|
||||
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
<Palette />
|
||||
</ListItemIcon>
|
||||
<FormControlLabel
|
||||
labelPlacement='end'
|
||||
label={settings.mode === 'dark' ? t('Dark') : t('Light')}
|
||||
control={
|
||||
<Switch
|
||||
checked={settings.mode === 'dark'}
|
||||
checkedIcon={
|
||||
<Paper
|
||||
elevation={3}
|
||||
sx={{
|
||||
width: '1.2em',
|
||||
height: '1.2em',
|
||||
borderRadius: '0.4em',
|
||||
backgroundColor: 'white',
|
||||
position: 'relative',
|
||||
top: `${7 - 0.5 * theme.typography.fontSize}px`,
|
||||
}}
|
||||
>
|
||||
<DarkMode sx={{ width: '0.8em', height: '0.8em', color: '#666' }} />
|
||||
</Paper>
|
||||
}
|
||||
icon={
|
||||
<Paper
|
||||
elevation={3}
|
||||
sx={{
|
||||
width: '1.2em',
|
||||
height: '1.2em',
|
||||
borderRadius: '0.4em',
|
||||
backgroundColor: 'white',
|
||||
padding: '0.07em',
|
||||
position: 'relative',
|
||||
top: `${7 - 0.5 * theme.typography.fontSize}px`,
|
||||
}}
|
||||
>
|
||||
<LightMode sx={{ width: '0.67em', height: '0.67em', color: '#666' }} />
|
||||
</Paper>
|
||||
}
|
||||
onChange={(e) =>
|
||||
setSettings({ ...settings, mode: e.target.checked ? 'dark' : 'light' })
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</ListItem>
|
||||
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
<SettingsOverscan />
|
||||
</ListItemIcon>
|
||||
<Slider
|
||||
value={settings.fontSize}
|
||||
min={settings.frontend == 'basic' ? 12 : 10}
|
||||
max={settings.frontend == 'basic' ? 16 : 14}
|
||||
step={1}
|
||||
onChange={(e) => setSettings({ ...settings, fontSize: e.target.value })}
|
||||
valueLabelDisplay='off'
|
||||
marks={fontSizes.map(({ label, value }) => ({
|
||||
label: <Typography variant='caption'>{t(label)}</Typography>,
|
||||
value: settings.frontend === 'basic' ? value.basic : value.pro,
|
||||
}))}
|
||||
track={false}
|
||||
/>
|
||||
</ListItem>
|
||||
{network ? (
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
<Link />
|
||||
</ListItemIcon>
|
||||
<ToggleButtonGroup
|
||||
exclusive={true}
|
||||
value={settings.network}
|
||||
onChange={(e, value) => setSettings({ ...settings, network: value })}
|
||||
>
|
||||
<ToggleButton value='mainnet' color='primary'>
|
||||
{t('Mainnet')}
|
||||
</ToggleButton>
|
||||
<ToggleButton value='testnet' color='secondary'>
|
||||
{t('Testnet')}
|
||||
</ToggleButton>
|
||||
</ToggleButtonGroup>
|
||||
</ListItem>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</List>
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
export default SettingsForm;
|
565
frontend/src/components/TradeBox/EncryptedChat.js
Normal file
565
frontend/src/components/TradeBox/EncryptedChat.js
Normal file
@ -0,0 +1,565 @@
|
||||
import React, { Component } from 'react';
|
||||
import { withTranslation } from 'react-i18next';
|
||||
import {
|
||||
Button,
|
||||
IconButton,
|
||||
Badge,
|
||||
Tooltip,
|
||||
TextField,
|
||||
Grid,
|
||||
Container,
|
||||
Card,
|
||||
CardHeader,
|
||||
Paper,
|
||||
Avatar,
|
||||
Typography,
|
||||
} from '@mui/material';
|
||||
import ReconnectingWebSocket from 'reconnecting-websocket';
|
||||
import { encryptMessage, decryptMessage } from '../../pgp';
|
||||
import { saveAsJson } from '../../utils';
|
||||
import { AuditPGPDialog } from '../Dialogs';
|
||||
import RobotAvatar from '../RobotAvatar';
|
||||
import { systemClient } from '../../services/System';
|
||||
import { websocketClient } from '../../services/Websocket';
|
||||
|
||||
// Icons
|
||||
import CheckIcon from '@mui/icons-material/Check';
|
||||
import CloseIcon from '@mui/icons-material/Close';
|
||||
import ContentCopy from '@mui/icons-material/ContentCopy';
|
||||
import VisibilityIcon from '@mui/icons-material/Visibility';
|
||||
import CircularProgress from '@mui/material/CircularProgress';
|
||||
import KeyIcon from '@mui/icons-material/Key';
|
||||
import { ExportIcon } from '../Icons';
|
||||
|
||||
class Chat extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
state = {
|
||||
own_pub_key: systemClient.getCookie('pub_key').split('\\').join('\n'),
|
||||
own_enc_priv_key: systemClient.getCookie('enc_priv_key').split('\\').join('\n'),
|
||||
peer_pub_key: null,
|
||||
token: systemClient.getCookie('robot_token'),
|
||||
messages: [],
|
||||
value: '',
|
||||
connected: false,
|
||||
connection: null,
|
||||
peer_connected: false,
|
||||
audit: false,
|
||||
showPGP: new Array(),
|
||||
waitingEcho: false,
|
||||
lastSent: '---BLANK---',
|
||||
latestIndex: 0,
|
||||
scrollNow: false,
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
websocketClient
|
||||
.open(`ws://${window.location.host}/ws/chat/${this.props.orderId}/`)
|
||||
.then((connection) => {
|
||||
console.log('Connected!');
|
||||
|
||||
connection.send({
|
||||
message: this.state.own_pub_key,
|
||||
nick: this.props.ur_nick,
|
||||
});
|
||||
|
||||
connection.onMessage(this.onMessage);
|
||||
connection.onClose(() => {
|
||||
console.log('Socket is closed. Reconnect will be attempted');
|
||||
this.setState({ connected: false });
|
||||
});
|
||||
connection.onError(() => {
|
||||
console.error('Socket encountered error: Closing socket');
|
||||
this.setState({ connected: false });
|
||||
});
|
||||
|
||||
this.setState({ connected: true, connection });
|
||||
});
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
// Only fire the scroll and audio when the reason for Update is a new message
|
||||
if (this.state.scrollNow) {
|
||||
const audio = new Audio(`/static/assets/sounds/chat-open.mp3`);
|
||||
audio.play();
|
||||
this.scrollToBottom();
|
||||
this.setState({ scrollNow: false });
|
||||
}
|
||||
}
|
||||
|
||||
onMessage = (message) => {
|
||||
const dataFromServer = JSON.parse(message.data);
|
||||
console.log('Got reply!', dataFromServer.type);
|
||||
console.log('PGP message index', dataFromServer.index, ' latestIndex ', this.state.latestIndex);
|
||||
if (dataFromServer) {
|
||||
console.log(dataFromServer);
|
||||
this.setState({ peer_connected: dataFromServer.peer_connected });
|
||||
|
||||
// If we receive our own key on a message
|
||||
if (dataFromServer.message == this.state.own_pub_key) {
|
||||
console.log('OWN PUB KEY RECEIVED!!');
|
||||
}
|
||||
|
||||
// If we receive a public key other than ours (our peer key!)
|
||||
if (
|
||||
dataFromServer.message.substring(0, 36) == `-----BEGIN PGP PUBLIC KEY BLOCK-----` &&
|
||||
dataFromServer.message != this.state.own_pub_key
|
||||
) {
|
||||
if (dataFromServer.message == this.state.peer_pub_key) {
|
||||
console.log('PEER HAS RECONNECTED USING HIS PREVIOUSLY KNOWN PUBKEY');
|
||||
} else if (
|
||||
(dataFromServer.message != this.state.peer_pub_key) &
|
||||
(this.state.peer_pub_key != null)
|
||||
) {
|
||||
console.log('PEER PUBKEY HAS CHANGED');
|
||||
}
|
||||
console.log('PEER PUBKEY RECEIVED!!');
|
||||
this.setState({ peer_pub_key: dataFromServer.message });
|
||||
|
||||
// After receiving the peer pubkey we ask the server for the historic messages if any
|
||||
this.state.connection.send({
|
||||
message: `-----SERVE HISTORY-----`,
|
||||
nick: this.props.ur_nick,
|
||||
});
|
||||
}
|
||||
|
||||
// If we receive an encrypted message
|
||||
else if (
|
||||
dataFromServer.message.substring(0, 27) == `-----BEGIN PGP MESSAGE-----` &&
|
||||
dataFromServer.index > this.state.latestIndex
|
||||
) {
|
||||
decryptMessage(
|
||||
dataFromServer.message.split('\\').join('\n'),
|
||||
dataFromServer.user_nick == this.props.ur_nick
|
||||
? this.state.own_pub_key
|
||||
: this.state.peer_pub_key,
|
||||
this.state.own_enc_priv_key,
|
||||
this.state.token,
|
||||
).then((decryptedData) =>
|
||||
this.setState((state) => ({
|
||||
scrollNow: true,
|
||||
waitingEcho:
|
||||
this.state.waitingEcho == true
|
||||
? decryptedData.decryptedMessage != this.state.lastSent
|
||||
: false,
|
||||
lastSent:
|
||||
decryptedData.decryptedMessage == this.state.lastSent
|
||||
? '----BLANK----'
|
||||
: this.state.lastSent,
|
||||
latestIndex:
|
||||
dataFromServer.index > this.state.latestIndex
|
||||
? dataFromServer.index
|
||||
: this.state.latestIndex,
|
||||
messages: [
|
||||
...state.messages,
|
||||
{
|
||||
index: dataFromServer.index,
|
||||
encryptedMessage: dataFromServer.message.split('\\').join('\n'),
|
||||
plainTextMessage: decryptedData.decryptedMessage,
|
||||
validSignature: decryptedData.validSignature,
|
||||
userNick: dataFromServer.user_nick,
|
||||
time: dataFromServer.time,
|
||||
},
|
||||
].sort(function (a, b) {
|
||||
// order the message array by their index (increasing)
|
||||
return a.index - b.index;
|
||||
}),
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
// We allow plaintext communication. The user must write # to start
|
||||
// If we receive an plaintext message
|
||||
else if (dataFromServer.message.substring(0, 1) == '#') {
|
||||
console.log('Got plaintext message', dataFromServer.message);
|
||||
this.setState((state) => ({
|
||||
scrollNow: true,
|
||||
messages: [
|
||||
...state.messages,
|
||||
{
|
||||
index: this.state.latestIndex + 0.001,
|
||||
encryptedMessage: dataFromServer.message,
|
||||
plainTextMessage: dataFromServer.message,
|
||||
validSignature: false,
|
||||
userNick: dataFromServer.user_nick,
|
||||
time: new Date().toString(),
|
||||
},
|
||||
],
|
||||
}));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
scrollToBottom = () => {
|
||||
this.messagesEnd.scrollIntoView({ behavior: 'smooth' });
|
||||
};
|
||||
|
||||
onButtonClicked = (e) => {
|
||||
// If input string contains token. Do not set message
|
||||
if (this.state.value.indexOf(this.state.token) !== -1) {
|
||||
alert(
|
||||
`Aye! You just sent your own robot token to your peer in chat, that's a catastrophic idea! So bad your message was blocked.`,
|
||||
);
|
||||
this.setState({ value: '' });
|
||||
}
|
||||
|
||||
// If input string contains '#' send unencrypted and unlogged message
|
||||
else if (this.state.value.substring(0, 1) == '#') {
|
||||
this.state.connection.send({
|
||||
message: this.state.value,
|
||||
nick: this.props.ur_nick,
|
||||
});
|
||||
this.setState({ value: '' });
|
||||
}
|
||||
|
||||
// Else if message is not empty send message
|
||||
else if (this.state.value != '') {
|
||||
this.setState({ value: '', waitingEcho: true, lastSent: this.state.value });
|
||||
encryptMessage(
|
||||
this.state.value,
|
||||
this.state.own_pub_key,
|
||||
this.state.peer_pub_key,
|
||||
this.state.own_enc_priv_key,
|
||||
this.state.token,
|
||||
).then(
|
||||
(encryptedMessage) =>
|
||||
console.log('Sending Encrypted MESSAGE', encryptedMessage) &
|
||||
this.state.connection.send({
|
||||
message: encryptedMessage.split('\n').join('\\'),
|
||||
nick: this.props.ur_nick,
|
||||
}),
|
||||
);
|
||||
}
|
||||
e.preventDefault();
|
||||
};
|
||||
|
||||
createJsonFile = () => {
|
||||
return {
|
||||
credentials: {
|
||||
own_public_key: this.state.own_pub_key,
|
||||
peer_public_key: this.state.peer_pub_key,
|
||||
encrypted_private_key: this.state.own_enc_priv_key,
|
||||
passphrase: this.state.token,
|
||||
},
|
||||
messages: this.state.messages,
|
||||
};
|
||||
};
|
||||
|
||||
messageCard = (props) => {
|
||||
const { t } = this.props;
|
||||
return (
|
||||
<Card elevation={5} align='left'>
|
||||
<CardHeader
|
||||
sx={{ color: '#333333' }}
|
||||
avatar={
|
||||
<RobotAvatar
|
||||
statusColor={props.userConnected ? 'success' : 'error'}
|
||||
nickname={props.message.userNick}
|
||||
/>
|
||||
}
|
||||
style={{ backgroundColor: props.cardColor }}
|
||||
title={
|
||||
<Tooltip
|
||||
placement='top'
|
||||
enterTouchDelay={0}
|
||||
enterDelay={500}
|
||||
enterNextDelay={2000}
|
||||
title={t(
|
||||
props.message.validSignature
|
||||
? 'Verified signature by {{nickname}}'
|
||||
: 'Cannot verify signature of {{nickname}}',
|
||||
{ nickname: props.message.userNick },
|
||||
)}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
flexWrap: 'wrap',
|
||||
position: 'relative',
|
||||
left: -5,
|
||||
width: 240,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{ width: 168, display: 'flex', alignItems: 'center', flexWrap: 'wrap' }}
|
||||
>
|
||||
{props.message.userNick}
|
||||
{props.message.validSignature ? (
|
||||
<CheckIcon sx={{ height: 16 }} color='success' />
|
||||
) : (
|
||||
<CloseIcon sx={{ height: 16 }} color='error' />
|
||||
)}
|
||||
</div>
|
||||
<div style={{ width: 20 }}>
|
||||
<IconButton
|
||||
sx={{ height: 18, width: 18 }}
|
||||
onClick={() =>
|
||||
this.setState((prevState) => {
|
||||
const newShowPGP = [...prevState.showPGP];
|
||||
newShowPGP[props.index] = !newShowPGP[props.index];
|
||||
return { showPGP: newShowPGP };
|
||||
})
|
||||
}
|
||||
>
|
||||
<VisibilityIcon
|
||||
color={this.state.showPGP[props.index] ? 'primary' : 'inherit'}
|
||||
sx={{
|
||||
height: 16,
|
||||
width: 16,
|
||||
color: this.state.showPGP[props.index] ? 'primary' : '#333333',
|
||||
}}
|
||||
/>
|
||||
</IconButton>
|
||||
</div>
|
||||
<div style={{ width: 20 }}>
|
||||
<Tooltip disableHoverListener enterTouchDelay={0} title={t('Copied!')}>
|
||||
<IconButton
|
||||
sx={{ height: 18, width: 18 }}
|
||||
onClick={() =>
|
||||
systemClient.copyToClipboard(
|
||||
this.state.showPGP[props.index]
|
||||
? props.message.encryptedMessage
|
||||
: props.message.plainTextMessage,
|
||||
)
|
||||
}
|
||||
>
|
||||
<ContentCopy sx={{ height: 16, width: 16, color: '#333333' }} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</Tooltip>
|
||||
}
|
||||
subheader={
|
||||
this.state.showPGP[props.index] ? (
|
||||
<a>
|
||||
{' '}
|
||||
{props.message.time} <br /> {'Valid signature: ' + props.message.validSignature}{' '}
|
||||
<br /> {props.message.encryptedMessage}{' '}
|
||||
</a>
|
||||
) : (
|
||||
props.message.plainTextMessage
|
||||
)
|
||||
}
|
||||
subheaderTypographyProps={{
|
||||
sx: {
|
||||
wordWrap: 'break-word',
|
||||
width: '200px',
|
||||
color: '#444444',
|
||||
fontSize: this.state.showPGP[props.index] ? 11 : null,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { t } = this.props;
|
||||
return (
|
||||
<Container component='main'>
|
||||
<Grid container spacing={0.5}>
|
||||
<Grid item xs={0.3} />
|
||||
<Grid item xs={5.5}>
|
||||
<Paper
|
||||
elevation={1}
|
||||
style={
|
||||
this.state.connected
|
||||
? { backgroundColor: '#e8ffe6' }
|
||||
: { backgroundColor: '#FFF1C5' }
|
||||
}
|
||||
>
|
||||
<Typography variant='caption' sx={{ color: '#333333' }}>
|
||||
{t('You') + ': '}
|
||||
{this.state.connected ? t('connected') : t('disconnected')}
|
||||
</Typography>
|
||||
</Paper>
|
||||
</Grid>
|
||||
<Grid item xs={0.4} />
|
||||
<Grid item xs={5.5}>
|
||||
<Paper
|
||||
elevation={1}
|
||||
style={
|
||||
this.state.peer_connected
|
||||
? { backgroundColor: '#e8ffe6' }
|
||||
: { backgroundColor: '#FFF1C5' }
|
||||
}
|
||||
>
|
||||
<Typography variant='caption' sx={{ color: '#333333' }}>
|
||||
{t('Peer') + ': '}
|
||||
{this.state.peer_connected ? t('connected') : t('disconnected')}
|
||||
</Typography>
|
||||
</Paper>
|
||||
</Grid>
|
||||
<Grid item xs={0.3} />
|
||||
</Grid>
|
||||
<div style={{ position: 'relative', left: '-2px', margin: '0 auto', width: '285px' }}>
|
||||
<Paper
|
||||
elevation={1}
|
||||
style={{
|
||||
height: '300px',
|
||||
maxHeight: '300px',
|
||||
width: '285px',
|
||||
overflow: 'auto',
|
||||
backgroundColor: '#F7F7F7',
|
||||
}}
|
||||
>
|
||||
{this.state.messages.map((message, index) => (
|
||||
<li style={{ listStyleType: 'none' }} key={index}>
|
||||
{message.userNick == this.props.ur_nick ? (
|
||||
<this.messageCard
|
||||
message={message}
|
||||
index={index}
|
||||
cardColor={'#eeeeee'}
|
||||
userConnected={this.state.connected}
|
||||
/>
|
||||
) : (
|
||||
<this.messageCard
|
||||
message={message}
|
||||
index={index}
|
||||
cardColor={'#fafafa'}
|
||||
userConnected={this.state.peer_connected}
|
||||
/>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
<div
|
||||
style={{ float: 'left', clear: 'both' }}
|
||||
ref={(el) => {
|
||||
this.messagesEnd = el;
|
||||
}}
|
||||
></div>
|
||||
</Paper>
|
||||
<form noValidate onSubmit={this.onButtonClicked}>
|
||||
<Grid alignItems='stretch' style={{ display: 'flex' }}>
|
||||
<Grid item alignItems='stretch' style={{ display: 'flex' }}>
|
||||
<TextField
|
||||
label={t('Type a message')}
|
||||
variant='standard'
|
||||
size='small'
|
||||
helperText={
|
||||
this.state.connected
|
||||
? this.state.peer_pub_key
|
||||
? null
|
||||
: t('Waiting for peer public key...')
|
||||
: t('Connecting...')
|
||||
}
|
||||
value={this.state.value}
|
||||
onChange={(e) => {
|
||||
this.setState({ value: e.target.value });
|
||||
this.value = this.state.value;
|
||||
}}
|
||||
sx={{ width: 219 }}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item alignItems='stretch' style={{ display: 'flex' }}>
|
||||
<Button
|
||||
sx={{ width: 68 }}
|
||||
disabled={
|
||||
!this.state.connected ||
|
||||
this.state.waitingEcho ||
|
||||
this.state.peer_pub_key == null
|
||||
}
|
||||
type='submit'
|
||||
variant='contained'
|
||||
color='primary'
|
||||
>
|
||||
{this.state.waitingEcho ? (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
flexWrap: 'wrap',
|
||||
minWidth: 68,
|
||||
width: 68,
|
||||
position: 'relative',
|
||||
left: 15,
|
||||
}}
|
||||
>
|
||||
<div style={{ width: 20 }}>
|
||||
<KeyIcon sx={{ width: 18 }} />
|
||||
</div>
|
||||
<div style={{ width: 18 }}>
|
||||
<CircularProgress size={16} thickness={5} />
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
t('Send')
|
||||
)}
|
||||
</Button>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div style={{ height: 4 }} />
|
||||
|
||||
<Grid container spacing={0}>
|
||||
<AuditPGPDialog
|
||||
open={this.state.audit}
|
||||
onClose={() => this.setState({ audit: false })}
|
||||
orderId={Number(this.props.orderId)}
|
||||
messages={this.state.messages}
|
||||
own_pub_key={this.state.own_pub_key}
|
||||
own_enc_priv_key={this.state.own_enc_priv_key}
|
||||
peer_pub_key={this.state.peer_pub_key ? this.state.peer_pub_key : 'Not received yet'}
|
||||
passphrase={this.state.token}
|
||||
onClickBack={() => this.setState({ audit: false })}
|
||||
/>
|
||||
|
||||
<Grid item xs={6}>
|
||||
<Tooltip
|
||||
placement='bottom'
|
||||
enterTouchDelay={0}
|
||||
enterDelay={500}
|
||||
enterNextDelay={2000}
|
||||
title={t('Verify your privacy')}
|
||||
>
|
||||
<Button
|
||||
size='small'
|
||||
color='primary'
|
||||
variant='outlined'
|
||||
onClick={() => this.setState({ audit: !this.state.audit })}
|
||||
>
|
||||
<KeyIcon />
|
||||
{t('Audit PGP')}{' '}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={6}>
|
||||
<Tooltip
|
||||
placement='bottom'
|
||||
enterTouchDelay={0}
|
||||
enterDelay={500}
|
||||
enterNextDelay={2000}
|
||||
title={t('Save full log as a JSON file (messages and credentials)')}
|
||||
>
|
||||
<Button
|
||||
size='small'
|
||||
color='primary'
|
||||
variant='outlined'
|
||||
onClick={() =>
|
||||
saveAsJson(
|
||||
'complete_log_chat_' + this.props.orderId + '.json',
|
||||
this.createJsonFile(),
|
||||
)
|
||||
}
|
||||
>
|
||||
<div style={{ width: 28, height: 20 }}>
|
||||
<ExportIcon sx={{ width: 20, height: 20 }} />
|
||||
</div>{' '}
|
||||
{t('Export')}{' '}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withTranslation()(Chat);
|
@ -18,8 +18,7 @@ import {
|
||||
AccordionDetails,
|
||||
Typography,
|
||||
} from '@mui/material';
|
||||
import { pn } from '../../utils/prettyNumbers';
|
||||
import { saveAsJson } from '../../utils/saveFile';
|
||||
import { pn, saveAsJson } from '../../utils';
|
||||
import RobotAvatar from '../RobotAvatar';
|
||||
|
||||
// Icons
|
||||
|
@ -2,6 +2,7 @@ import React, { Component } from 'react';
|
||||
import { withTranslation, Trans } from 'react-i18next';
|
||||
import { Paper, Alert, AlertTitle, Button, Link } from '@mui/material';
|
||||
import MediaQuery from 'react-responsive';
|
||||
import { getHost } from '../utils';
|
||||
|
||||
class UnsafeAlert extends Component {
|
||||
constructor(props) {
|
||||
@ -12,18 +13,10 @@ class UnsafeAlert extends Component {
|
||||
};
|
||||
}
|
||||
|
||||
getHost() {
|
||||
const url =
|
||||
window.location !== window.parent.location
|
||||
? this.getHost(document.referrer)
|
||||
: document.location.href;
|
||||
return url.split('/')[2];
|
||||
}
|
||||
|
||||
isSelfhosted() {
|
||||
const http = new XMLHttpRequest();
|
||||
try {
|
||||
http.open('HEAD', `${location.protocol}//${this.getHost()}/selfhosted`, false);
|
||||
http.open('HEAD', `${location.protocol}//${getHost()}/selfhosted`, false);
|
||||
http.send();
|
||||
return http.status === 200;
|
||||
} catch {
|
||||
@ -72,7 +65,7 @@ class UnsafeAlert extends Component {
|
||||
}
|
||||
|
||||
// Show unsafe alert
|
||||
if (!window.NativeRobosats && !this.safe_urls.includes(this.getHost())) {
|
||||
if (!window.NativeRobosats && !this.safe_urls.includes(getHost())) {
|
||||
return (
|
||||
<div>
|
||||
<MediaQuery minWidth={800}>
|
||||
|
22
frontend/src/models/Coordinator.model.ts
Normal file
22
frontend/src/models/Coordinator.model.ts
Normal file
@ -0,0 +1,22 @@
|
||||
export interface Coordinator {
|
||||
alias: string;
|
||||
description: string | undefined;
|
||||
coverLetter: string | undefined;
|
||||
logo: string;
|
||||
color: string;
|
||||
contact: {
|
||||
email: string | undefined;
|
||||
telegram: string | undefined;
|
||||
matrix: string | undefined;
|
||||
twitter: string | undefined;
|
||||
website: string | undefined;
|
||||
};
|
||||
mainnetOnion: string | undefined;
|
||||
mainnetClearnet: string | undefined;
|
||||
testnetOnion: string | undefined;
|
||||
testnetClearnet: string | undefined;
|
||||
mainnetNodesPubkeys: string[];
|
||||
testnetNodesPubkeys: string[];
|
||||
}
|
||||
|
||||
export default Coordinator;
|
@ -17,11 +17,15 @@ export interface Info {
|
||||
taker_fee: number;
|
||||
bond_size: number;
|
||||
current_swap_fee_rate: number;
|
||||
network: 'mainnet' | 'testnet' | undefined;
|
||||
coordinatorVersion: string;
|
||||
clientVersion: string;
|
||||
openUpdateClient: boolean;
|
||||
}
|
||||
|
||||
import packageJson from '../../package.json';
|
||||
const semver = packageJson.version.split('.');
|
||||
|
||||
export const defaultInfo: Info = {
|
||||
num_public_buy_orders: 0,
|
||||
num_public_sell_orders: 0,
|
||||
@ -41,8 +45,9 @@ export const defaultInfo: Info = {
|
||||
taker_fee: 0,
|
||||
bond_size: 0,
|
||||
current_swap_fee_rate: 0,
|
||||
network: undefined,
|
||||
coordinatorVersion: 'v?.?.?',
|
||||
clientVersion: 'v?.?.?',
|
||||
clientVersion: `v${semver[0]}.${semver[1]}.${semver[2]}`,
|
||||
openUpdateClient: false,
|
||||
};
|
||||
|
||||
|
@ -23,14 +23,8 @@ export interface Robot {
|
||||
export const defaultRobot: Robot = {
|
||||
nickname: null,
|
||||
token: systemClient.getCookie('robot_token') ?? null,
|
||||
pub_key:
|
||||
systemClient.getCookie('pub_key') === undefined
|
||||
? null
|
||||
: systemClient.getCookie('pub_key').split('\\').join('\n'),
|
||||
enc_priv_key:
|
||||
systemClient.getCookie('enc_priv_key') === undefined
|
||||
? null
|
||||
: systemClient.getCookie('enc_priv_key').split('\\').join('\n'),
|
||||
pub_key: systemClient.getCookie('pub_key').split('\\').join('\n'),
|
||||
enc_priv_key: systemClient.getCookie('enc_priv_key').split('\\').join('\n'),
|
||||
bitsEntropy: null,
|
||||
shannonEntropy: null,
|
||||
stealthInvoices: true,
|
||||
|
@ -2,6 +2,7 @@ import { baseSettings, Settings } from './Settings.model';
|
||||
|
||||
export const defaultSettings: Settings = {
|
||||
...baseSettings,
|
||||
frontend: 'basic',
|
||||
};
|
||||
|
||||
export default defaultSettings;
|
||||
|
@ -3,6 +3,7 @@ import { baseSettings, Settings } from './Settings.model';
|
||||
export const defaultSettings: Settings = {
|
||||
...baseSettings,
|
||||
fontSize: 12,
|
||||
frontend: 'pro',
|
||||
};
|
||||
|
||||
export default defaultSettings;
|
||||
|
@ -1,34 +1,45 @@
|
||||
import i18n from '../i18n/Web';
|
||||
import type Coordinator from './Coordinator.model';
|
||||
|
||||
export type Language =
|
||||
| 'en'
|
||||
| 'es'
|
||||
| 'ru'
|
||||
| 'de'
|
||||
| 'pl'
|
||||
| 'fr'
|
||||
| 'ca'
|
||||
| 'it'
|
||||
| 'pt'
|
||||
| 'eu'
|
||||
| 'cs'
|
||||
| 'th'
|
||||
| 'pl'
|
||||
| 'sv'
|
||||
| 'zh-SI'
|
||||
| 'zh-TR';
|
||||
|
||||
export interface Settings {
|
||||
frontend: 'basic' | 'pro';
|
||||
mode: 'light' | 'dark';
|
||||
fontSize: number;
|
||||
language:
|
||||
| 'en'
|
||||
| 'es'
|
||||
| 'ru'
|
||||
| 'de'
|
||||
| 'pl'
|
||||
| 'fr'
|
||||
| 'ca'
|
||||
| 'it'
|
||||
| 'pt'
|
||||
| 'eu'
|
||||
| 'cs'
|
||||
| 'th'
|
||||
| 'pl'
|
||||
| 'sv'
|
||||
| 'zh-SI'
|
||||
| 'zh-TR';
|
||||
language: Language;
|
||||
freezeViewports: boolean;
|
||||
network: 'mainnet' | 'testnet' | undefined;
|
||||
coordinator: Coordinator | undefined;
|
||||
}
|
||||
|
||||
export const baseSettings: Settings = {
|
||||
frontend: 'basic',
|
||||
mode:
|
||||
window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||
? 'dark'
|
||||
: 'light',
|
||||
fontSize: 14,
|
||||
language: 'en',
|
||||
language: i18n.resolvedLanguage == null ? 'en' : i18n.resolvedLanguage.substring(0, 2),
|
||||
freezeViewports: false,
|
||||
network: undefined,
|
||||
coordinator: undefined,
|
||||
};
|
||||
|
||||
export default Settings;
|
||||
|
@ -6,7 +6,9 @@ export type { Book } from './Book.model';
|
||||
export type { Robot } from './Robot.model';
|
||||
export type { Info } from './Info.model';
|
||||
export type { Settings } from './Settings.model';
|
||||
export type { Language } from './Settings.model';
|
||||
export type { Favorites } from './Favorites.model';
|
||||
export type { Coordinator } from './Coordinator.model';
|
||||
|
||||
export { defaultMaker } from './Maker.model';
|
||||
export { defaultRobot } from './Robot.model';
|
||||
|
@ -17,7 +17,13 @@ import {
|
||||
defaultInfo,
|
||||
} from '../models';
|
||||
|
||||
import { PlaceholderWidget, MakerWidget, BookWidget, DepthChartWidget } from '../pro/Widgets';
|
||||
import {
|
||||
PlaceholderWidget,
|
||||
MakerWidget,
|
||||
BookWidget,
|
||||
DepthChartWidget,
|
||||
SettingsWidget,
|
||||
} from '../pro/Widgets';
|
||||
import ToolBar from '../pro/ToolBar';
|
||||
import LandingDialog from '../pro/LandingDialog';
|
||||
|
||||
@ -101,6 +107,10 @@ const Main = ({ settings, setSettings }: MainProps): JSX.Element => {
|
||||
setWindowSize(getWindowSize(em));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setWindowSize(getWindowSize(theme.typography.fontSize));
|
||||
}, [theme.typography.fontSize]);
|
||||
|
||||
const fetchLimits = async () => {
|
||||
setLimits({ ...limits, loading: true });
|
||||
const data = apiClient.get('/api/limits/').then((data) => {
|
||||
@ -188,6 +198,9 @@ const Main = ({ settings, setSettings }: MainProps): JSX.Element => {
|
||||
windowSize={windowSize}
|
||||
/>
|
||||
</div>
|
||||
<div key='Settings'>
|
||||
<SettingsWidget settings={settings} setSettings={setSettings} />
|
||||
</div>
|
||||
<div key='Garage'>
|
||||
<PlaceholderWidget label='Robot Garage' />
|
||||
</div>
|
||||
@ -197,9 +210,6 @@ const Main = ({ settings, setSettings }: MainProps): JSX.Element => {
|
||||
<div key='Trade'>
|
||||
<PlaceholderWidget label='Trade Box' />
|
||||
</div>
|
||||
<div key='Settings'>
|
||||
<PlaceholderWidget label='Settings' />
|
||||
</div>
|
||||
<div key='Other'>
|
||||
<PlaceholderWidget label='Other' />
|
||||
</div>
|
||||
|
44
frontend/src/pro/Widgets/Settings.tsx
Normal file
44
frontend/src/pro/Widgets/Settings.tsx
Normal file
@ -0,0 +1,44 @@
|
||||
import React from 'react';
|
||||
|
||||
import { Settings } from '../../models';
|
||||
import { Paper, useTheme } from '@mui/material';
|
||||
import SettingsForm from '../../components/SettingsForm';
|
||||
|
||||
interface SettingsWidgetProps {
|
||||
settings: Settings;
|
||||
setSettings: (state: Settings) => void;
|
||||
style?: Object;
|
||||
className?: string;
|
||||
onMouseDown?: () => void;
|
||||
onMouseUp?: () => void;
|
||||
onTouchEnd?: () => void;
|
||||
}
|
||||
|
||||
const SettingsWidget = React.forwardRef(
|
||||
(
|
||||
{
|
||||
settings,
|
||||
setSettings,
|
||||
style,
|
||||
className,
|
||||
onMouseDown,
|
||||
onMouseUp,
|
||||
onTouchEnd,
|
||||
}: SettingsWidgetProps,
|
||||
ref,
|
||||
) => {
|
||||
const theme = useTheme();
|
||||
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} />
|
||||
</Paper>
|
||||
);
|
||||
}, [settings]);
|
||||
},
|
||||
);
|
||||
|
||||
export default SettingsWidget;
|
@ -1,4 +1,5 @@
|
||||
export { default as MakerWidget } from './Maker';
|
||||
export { default as BookWidget } from './Book';
|
||||
export { default as DepthChartWidget } from './Depth';
|
||||
export { default as SettingsWidget } from './Settings';
|
||||
export { default as PlaceholderWidget } from './Placeholder';
|
||||
|
7
frontend/src/utils/getHost.ts
Normal file
7
frontend/src/utils/getHost.ts
Normal file
@ -0,0 +1,7 @@
|
||||
const getHost = function () {
|
||||
const url =
|
||||
window.location != window.parent.location ? document.referrer : document.location.href;
|
||||
return url.split('/')[2];
|
||||
};
|
||||
|
||||
export default getHost;
|
11
frontend/src/utils/index.ts
Normal file
11
frontend/src/utils/index.ts
Normal file
@ -0,0 +1,11 @@
|
||||
export { default as checkVer } from './checkVer';
|
||||
export { default as filterOrders } from './filterOrders';
|
||||
export { default as getHost } from './getHost';
|
||||
export { default as hexToRgb } from './hexToRgb';
|
||||
export { default as matchMedian } from './match';
|
||||
export { default as pn } from './prettyNumbers';
|
||||
export { amountToString } from './prettyNumbers';
|
||||
export { default as saveAsJson } from './saveFile';
|
||||
export { default as statusBadgeColor } from './statusBadgeColor';
|
||||
export { genBase62Token, tokenStrength } from './token';
|
||||
export { default as getWebln } from './webln';
|
@ -1,7 +1,7 @@
|
||||
export const median = (arr: number[]) => {
|
||||
export const matchMedian = (arr: number[]) => {
|
||||
const mid = Math.floor(arr.length / 2);
|
||||
const nums = [...arr].sort((a, b) => a - b);
|
||||
return arr.length % 2 !== 0 ? nums[mid] : (nums[mid - 1] + nums[mid]) / 2;
|
||||
};
|
||||
|
||||
export default median;
|
||||
export default matchMedian;
|
||||
|
@ -3,7 +3,8 @@
|
||||
* @param {filename} data -- object to save
|
||||
*/
|
||||
|
||||
export const saveAsJson = (filename, dataObjToWrite) => {
|
||||
const saveAsJson = (filename, dataObjToWrite) => {
|
||||
console.log(filename, dataObjToWrite);
|
||||
const blob = new Blob([JSON.stringify(dataObjToWrite, null, 2)], { type: 'text/json' });
|
||||
const link = document.createElement('a');
|
||||
|
||||
@ -20,3 +21,5 @@ export const saveAsJson = (filename, dataObjToWrite) => {
|
||||
link.dispatchEvent(evt);
|
||||
link.remove();
|
||||
};
|
||||
|
||||
export default saveAsJson;
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { requestProvider, WeblnProvider } from 'webln';
|
||||
|
||||
export const getWebln = async (): Promise<WeblnProvider> => {
|
||||
const getWebln = async (): Promise<WeblnProvider> => {
|
||||
const resultPromise = new Promise<WeblnProvider>(async (resolve, reject) => {
|
||||
try {
|
||||
const webln = await requestProvider();
|
||||
@ -16,3 +16,5 @@ export const getWebln = async (): Promise<WeblnProvider> => {
|
||||
|
||||
return await resultPromise;
|
||||
};
|
||||
|
||||
export default getWebln;
|
||||
|
@ -46,13 +46,6 @@ body {
|
||||
}
|
||||
}
|
||||
|
||||
.appCenter {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%) translate(0, -20px);
|
||||
}
|
||||
|
||||
.alertUnsafe {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
@ -124,14 +117,14 @@ input[type='number'] {
|
||||
filter: drop-shadow(0.5px 0.5px 0.5px #000000);
|
||||
}
|
||||
|
||||
.phoneFlippedSmallAvatar img {
|
||||
.navBarAvatar img {
|
||||
transform: scaleX(-1);
|
||||
border: 1.3px solid #1976d2;
|
||||
-webkit-filter: grayscale(100%);
|
||||
filter: grayscale(100%) brightness(150%) contrast(150%) drop-shadow(0.7px 0.7px 0.7px #000000);
|
||||
}
|
||||
|
||||
.phoneFlippedSmallAvatar:after {
|
||||
.navBarAvatar:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
@ -143,6 +136,25 @@ input[type='number'] {
|
||||
box-shadow: inset 0px 0px 35px rgb(255, 255, 255);
|
||||
}
|
||||
|
||||
.navBarAvatarDark img {
|
||||
transform: scaleX(-1);
|
||||
border: 1.3px solid #90caf9;
|
||||
-webkit-filter: grayscale(100%);
|
||||
filter: grayscale(100%) brightness(100%) contrast(150%) drop-shadow(0.7px 0.7px 0.7px #000000);
|
||||
}
|
||||
|
||||
.navBarAvatarDark:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
border-radius: 50%;
|
||||
border: 2.4px solid #90caf9;
|
||||
box-shadow: inset 0px 0px 35px rgb(255, 255, 255);
|
||||
}
|
||||
|
||||
.MuiButton-textInherit {
|
||||
color: '#111111';
|
||||
}
|
||||
|
@ -1,21 +1,22 @@
|
||||
{
|
||||
"coordinator_1": {
|
||||
"alias": "Maximalist",
|
||||
"description": "Maximalist Robots. P2P for freedom. No trade limits, low fees.",
|
||||
"cover_letter": "Hi! I am Mike. I'm a freedom activist based in TorLand. I have been running LN infrastructure since early 2019, long time FOSS contributor....",
|
||||
[
|
||||
{
|
||||
"alias": "Inception",
|
||||
"description": "RoboSats original and experimental coordinator",
|
||||
"coverLetter": "N/A",
|
||||
"contact_methods": {
|
||||
"email": "maximalist@bitcoin.p2p",
|
||||
"telegram": "maximalist_robot",
|
||||
".....": "...."
|
||||
"email": "robosats@protonmail.com",
|
||||
"telegram": "@robosats",
|
||||
"twitter": "@robosats",
|
||||
"matrix": "#robosats:matrix.org",
|
||||
"website": "learn.robosats.com"
|
||||
},
|
||||
"color": "#FFFFFF",
|
||||
"mainnet_onion": "robomaxim......onion",
|
||||
"testnet_onion": null,
|
||||
"mainnet_ln_nodes_pubkeys": ["03e96as....", "02aaecc...."],
|
||||
"testnet_ln_nodes_pubkeys": ["0284ff2...."],
|
||||
"logo_svg_200x200": "<svg xmlns=\"http://www.w3.org/2000/svg\" version=\"1.1\" width=\"120\" height=\"120\"> <rect x=\"14\" y=\"23\" width=\"200\" height=\"50\" fill=\"lime\" stroke=\"black\" /> </svg>"
|
||||
},
|
||||
"coordinator_2": {
|
||||
"...": "..."
|
||||
"color": "#9C27B0",
|
||||
"mainnetOnion": "robosats6tkf3eva7x2voqso3a5wcorsnw34jveyxfqi2fu7oyheasid.onion",
|
||||
"mainnetClearnet": "unsafe.robosats.com",
|
||||
"testnetOnion": "robotestagw3dcxmd66r4rgksb4nmmr43fh77bzn2ia2eucduyeafnyd.onion",
|
||||
"testnetClearnet": "unsafe.testnet.robosats.com",
|
||||
"mainnetNodesPubkeys": ["0282eb467bc073833a039940392592bf10cf338a830ba4e392c1667d7697654c7e"],
|
||||
"testnetNodesPubkeys": ["03ecb271b3e2e36f2b91c92c65bab665e5165f8cdfdada1b5f46cfdd3248c87fd6"],
|
||||
"logo": "/static/federation/inception.svg"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
@ -3,10 +3,13 @@ from django.urls import path
|
||||
from .views import basic, pro
|
||||
|
||||
urlpatterns = [
|
||||
path("make/", basic),
|
||||
path("book/", basic),
|
||||
path("order/<int:orderId>", basic),
|
||||
path("", basic),
|
||||
path("ref/<refCode>", basic),
|
||||
path("create/", basic),
|
||||
path("robot/", basic),
|
||||
path("robot/<refCode>", basic),
|
||||
path("offers/", basic),
|
||||
path("order/<int:orderId>", basic),
|
||||
path("settings/", basic),
|
||||
path("", basic),
|
||||
path("pro/", pro),
|
||||
]
|
||||
|
Loading…
Reference in New Issue
Block a user