Add Nav Bar, Settings Page, large refactor (#308)

commit a5b63aed93
Author: Reckless_Satoshi <reckless.satoshi@protonmail.com>
Date:   Sun Oct 30 10:46:05 2022 -0700

    Small fixes

commit d64adfc2bf
Author: Reckless_Satoshi <reckless.satoshi@protonmail.com>
Date:   Sun Oct 30 06:02:06 2022 -0700

    wip work on federation settings

commit ca35d6b3d2
Author: Reckless_Satoshi <reckless.satoshi@protonmail.com>
Date:   Sun Oct 30 04:05:33 2022 -0700

    Refactor confirmation Dialogs

commit c660a5b0d1
Author: Reckless_Satoshi <reckless.satoshi@protonmail.com>
Date:   Sat Oct 29 13:36:59 2022 -0700

    refactor login (clean separation robot/info. Style navbar.

commit b9dc7f7c95
Author: Reckless_Satoshi <reckless.satoshi@protonmail.com>
Date:   Fri Oct 28 09:54:38 2022 -0700

    Add size slider and settings widget

commit 20b2b3dcd6
Author: Reckless_Satoshi <reckless.satoshi@protonmail.com>
Date:   Fri Oct 28 05:41:48 2022 -0700

    Add show more and Dialogs

commit da8b70091b
Author: Reckless_Satoshi <reckless.satoshi@protonmail.com>
Date:   Thu Oct 27 16:26:07 2022 -0700

    Add sliding pages

commit 6dd90aa118
Author: Reckless_Satoshi <reckless.satoshi@protonmail.com>
Date:   Thu Oct 27 06:34:58 2022 -0700

    Add settings forms

commit d3d0f3ee1a
Author: Reckless_Satoshi <reckless.satoshi@protonmail.com>
Date:   Wed Oct 26 04:16:06 2022 -0700

    Refactor utils
This commit is contained in:
Reckless_Satoshi 2022-10-30 12:13:01 -07:00
parent 5e6f7165d7
commit 227610c84a
No known key found for this signature in database
GPG Key ID: 9C4585B561315571
54 changed files with 2490 additions and 1195 deletions

View File

@ -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,

View File

@ -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)

View File

@ -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'>

View File

@ -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>

View File

@ -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);

View File

@ -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>
);
};

View 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;

View File

@ -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>
);
};

View 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;

View 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;

View File

@ -0,0 +1,4 @@
import NavBar from './NavBar';
export type Page = 'robot' | 'order' | 'create' | 'offers' | 'settings' | 'none';
export default NavBar;

View File

@ -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 {

View File

@ -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;

View File

@ -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>

View File

@ -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>
);

View File

@ -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,

View File

@ -14,7 +14,7 @@ import {
Link,
} from '@mui/material';
import { saveAsJson } from '../../utils/saveFile';
import { saveAsJson } from '../../utils';
import { systemClient } from '../../services/System';
// Icons

View File

@ -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'
>

View 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;

View File

@ -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;

View File

@ -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>

View File

@ -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>
);

View File

@ -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}

View File

@ -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'
>

View File

@ -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>
),

View File

@ -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>

View File

@ -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';

View File

@ -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;

View File

@ -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 />

View File

@ -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,
}}
/>

View 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;

View 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;

View 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);

View File

@ -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

View File

@ -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}>

View 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;

View File

@ -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,
};

View File

@ -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,

View File

@ -2,6 +2,7 @@ import { baseSettings, Settings } from './Settings.model';
export const defaultSettings: Settings = {
...baseSettings,
frontend: 'basic',
};
export default defaultSettings;

View File

@ -3,6 +3,7 @@ import { baseSettings, Settings } from './Settings.model';
export const defaultSettings: Settings = {
...baseSettings,
fontSize: 12,
frontend: 'pro',
};
export default defaultSettings;

View File

@ -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;

View File

@ -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';

View File

@ -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>

View 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;

View File

@ -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';

View 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;

View 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';

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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';
}

View File

@ -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"
}
}
]

View File

@ -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),
]