Merge pull request #971 from RoboSats/add-roboidentity-generator

Add robo-identity generator
This commit is contained in:
Reckless_Satoshi 2023-12-15 15:20:26 +00:00 committed by GitHub
commit a2695ca169
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 217 additions and 133 deletions

View File

@ -47,6 +47,7 @@
"react-smooth-image": "^1.1.0", "react-smooth-image": "^1.1.0",
"react-world-flags": "^1.6.0", "react-world-flags": "^1.6.0",
"reconnecting-websocket": "^4.4.0", "reconnecting-websocket": "^4.4.0",
"robo-identities-wasm": "^0.1.0",
"simple-plist": "^1.3.1", "simple-plist": "^1.3.1",
"webln": "^0.3.2", "webln": "^0.3.2",
"websocket": "^1.0.34" "websocket": "^1.0.34"
@ -14282,6 +14283,11 @@
"url": "https://github.com/sponsors/isaacs" "url": "https://github.com/sponsors/isaacs"
} }
}, },
"node_modules/robo-identities-wasm": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/robo-identities-wasm/-/robo-identities-wasm-0.1.0.tgz",
"integrity": "sha512-q6+1Vgq+8d2F5k8Nqm39qwQJYe9uTC7TlR3NbBQ6k2ImBNccdAEoZgb0ikKjN59cK4MvqejlgBV1ybaLXoHbhA=="
},
"node_modules/run-applescript": { "node_modules/run-applescript": {
"version": "5.0.0", "version": "5.0.0",
"resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-5.0.0.tgz", "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-5.0.0.tgz",

View File

@ -86,6 +86,7 @@
"react-smooth-image": "^1.1.0", "react-smooth-image": "^1.1.0",
"react-world-flags": "^1.6.0", "react-world-flags": "^1.6.0",
"reconnecting-websocket": "^4.4.0", "reconnecting-websocket": "^4.4.0",
"robo-identities-wasm": "^0.1.0",
"simple-plist": "^1.3.1", "simple-plist": "^1.3.1",
"webln": "^0.3.2", "webln": "^0.3.2",
"websocket": "^1.0.34" "websocket": "^1.0.34"

View File

@ -31,7 +31,7 @@ const BookPage = (): JSX.Element => {
const chartWidthEm = width - maxBookTableWidth; const chartWidthEm = width - maxBookTableWidth;
const onOrderClicked = function (id: number, shortAlias: string): void { const onOrderClicked = function (id: number, shortAlias: string): void {
if (garage.getSlot()?.avatarLoaded === true) { if (Boolean(garage.getSlot()?.hashId)) {
setDelay(10000); setDelay(10000);
navigate(`/order/${shortAlias}/${id}`); navigate(`/order/${shortAlias}/${id}`);
} else { } else {

View File

@ -1,4 +1,4 @@
import React, { useContext, useEffect, useState } from 'react'; import React, { useContext } from 'react';
import { MemoryRouter, BrowserRouter, Routes, Route } from 'react-router-dom'; import { MemoryRouter, BrowserRouter, Routes, Route } from 'react-router-dom';
import { Box, Slide, Typography, styled } from '@mui/material'; import { Box, Slide, Typography, styled } from '@mui/material';
import { type UseAppStoreType, AppContext, closeAll } from '../contexts/AppContext'; import { type UseAppStoreType, AppContext, closeAll } from '../contexts/AppContext';
@ -8,7 +8,6 @@ import RobotAvatar from '../components/RobotAvatar';
import Notifications from '../components/Notifications'; import Notifications from '../components/Notifications';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { FederationContext, type UseFederationStoreType } from '../contexts/FederationContext';
import { GarageContext, type UseGarageStoreType } from '../contexts/GarageContext'; import { GarageContext, type UseGarageStoreType } from '../contexts/GarageContext';
const Router = window.NativeRobosats === undefined ? BrowserRouter : MemoryRouter; const Router = window.NativeRobosats === undefined ? BrowserRouter : MemoryRouter;
@ -30,28 +29,13 @@ const MainBox = styled(Box)<MainBoxProps>((props) => ({
const Main: React.FC = () => { const Main: React.FC = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const { settings, page, slideDirection, setOpen, windowSize, navbarHeight, hostUrl } = const { settings, page, slideDirection, setOpen, windowSize, navbarHeight } =
useContext<UseAppStoreType>(AppContext); useContext<UseAppStoreType>(AppContext);
const { federation, sortedCoordinators } = useContext<UseFederationStoreType>(FederationContext);
const { garage } = useContext<UseGarageStoreType>(GarageContext); const { garage } = useContext<UseGarageStoreType>(GarageContext);
const [avatarBaseUrl, setAvatarBaseUrl] = useState<string>(hostUrl);
useEffect(() => {
setAvatarBaseUrl(federation.getCoordinator(sortedCoordinators[0]).getBaseUrl());
}, [settings.network, settings.selfhostedClient, federation, sortedCoordinators]);
const onLoad = (): void => {
garage.updateSlot({ avatarLoaded: true });
};
return ( return (
<Router> <Router>
<RobotAvatar <RobotAvatar style={{ display: 'none' }} hashId={garage.getSlot()?.hashId} />
style={{ display: 'none' }}
nickname={garage.getSlot()?.getRobot()?.nickname}
baseUrl={federation.getCoordinator(sortedCoordinators[0]).getBaseUrl()}
onLoad={onLoad}
/>
<Notifications <Notifications
page={page} page={page}
openProfile={() => { openProfile={() => {
@ -81,7 +65,7 @@ const Main: React.FC = () => {
appear={slideDirection.in !== undefined} appear={slideDirection.in !== undefined}
> >
<div> <div>
<RobotPage avatarBaseUrl={avatarBaseUrl} /> <RobotPage />
</div> </div>
</Slide> </Slide>
} }

View File

@ -51,7 +51,7 @@ const MakerPage = (): JSX.Element => {
]); ]);
const onOrderClicked = function (id: number): void { const onOrderClicked = function (id: number): void {
if (garage.getSlot()?.avatarLoaded === true) { if (Boolean(garage.getSlot()?.hashId)) {
navigate(`/order/${id}`); navigate(`/order/${id}`);
} else { } else {
setOpenNoRobot(true); setOpenNoRobot(true);

View File

@ -32,7 +32,7 @@ const NavBar = (): JSX.Element => {
navbarHeight, navbarHeight,
hostUrl, hostUrl,
} = useContext<UseAppStoreType>(AppContext); } = useContext<UseAppStoreType>(AppContext);
const { garage, orderUpdatedAt } = useContext<UseGarageStoreType>(GarageContext); const { garage, orderUpdatedAt, robotUpdatedAt } = useContext<UseGarageStoreType>(GarageContext);
const navigate = useNavigate(); const navigate = useNavigate();
const location = useLocation(); const location = useLocation();
@ -42,7 +42,7 @@ const NavBar = (): JSX.Element => {
const tabSx = smallBar const tabSx = smallBar
? { ? {
position: 'relative', position: 'relative',
bottom: garage.getSlot()?.avatarLoaded === true ? '0.9em' : '0.13em', bottom: Boolean(garage.getSlot()?.hashId) ? '0.9em' : '0.13em',
minWidth: '1em', minWidth: '1em',
} }
: { position: 'relative', bottom: '1em', minWidth: '2em' }; : { position: 'relative', bottom: '1em', minWidth: '2em' };
@ -65,7 +65,7 @@ const NavBar = (): JSX.Element => {
if (isPage(pathPage)) { if (isPage(pathPage)) {
setPage(pathPage); setPage(pathPage);
} }
}, [location, navigate, setPage, orderUpdatedAt]); }, [location, navigate, setPage, orderUpdatedAt, robotUpdatedAt]);
const handleSlideDirection = function (oldPage: Page, newPage: Page): void { const handleSlideDirection = function (oldPage: Page, newPage: Page): void {
const oldPos: number = pagesPosition[oldPage]; const oldPos: number = pagesPosition[oldPage];
@ -121,17 +121,16 @@ const NavBar = (): JSX.Element => {
<Tab <Tab
sx={{ ...tabSx, minWidth: '2.5em', width: '2.5em', maxWidth: '4em' }} sx={{ ...tabSx, minWidth: '2.5em', width: '2.5em', maxWidth: '4em' }}
value='none' value='none'
disabled={slot?.getRobot()?.nickname === null} disabled={slot?.nickname === null}
onClick={() => { onClick={() => {
setOpen({ ...closeAll, profile: !open.profile }); setOpen({ ...closeAll, profile: !open.profile });
}} }}
icon={ icon={
slot?.getRobot()?.nickname != null && slot?.avatarLoaded ? ( slot?.hashId ? (
<RobotAvatar <RobotAvatar
style={{ width: '2.3em', height: '2.3em', position: 'relative', top: '0.2em' }} style={{ width: '2.3em', height: '2.3em', position: 'relative', top: '0.2em' }}
avatarClass={theme.palette.mode === 'dark' ? 'navBarAvatarDark' : 'navBarAvatar'} avatarClass={theme.palette.mode === 'dark' ? 'navBarAvatarDark' : 'navBarAvatar'}
nickname={slot?.getRobot()?.nickname} hashId={slot?.hashId}
baseUrl={hostUrl}
/> />
) : ( ) : (
<></> <></>
@ -166,7 +165,7 @@ const NavBar = (): JSX.Element => {
label={smallBar ? undefined : t('Order')} label={smallBar ? undefined : t('Order')}
value='order' value='order'
disabled={ disabled={
slot?.avatarLoaded === false || !Boolean(slot?.hashId) ||
!(slot?.getRobot(slot?.activeShortAlias ?? '')?.activeOrderId != null) !(slot?.getRobot(slot?.activeShortAlias ?? '')?.activeOrderId != null)
} }
icon={<Assignment />} icon={<Assignment />}

View File

@ -151,7 +151,7 @@ const Onboarding = ({
<Grid container direction='column' alignItems='center' spacing={1}> <Grid container direction='column' alignItems='center' spacing={1}>
<Grid item> <Grid item>
<Typography> <Typography>
{slot?.avatarLoaded === true && Boolean(robot?.nickname) ? ( {Boolean(slot?.hashId) ? (
t('This is your trading avatar') t('This is your trading avatar')
) : ( ) : (
<> <>
@ -164,7 +164,7 @@ const Onboarding = ({
<Grid item sx={{ width: '13.5em' }}> <Grid item sx={{ width: '13.5em' }}>
<RobotAvatar <RobotAvatar
nickname={robot?.nickname} hashId={slot?.hashId}
smooth={true} smooth={true}
style={{ maxWidth: '12.5em', maxHeight: '12.5em' }} style={{ maxWidth: '12.5em', maxHeight: '12.5em' }}
placeholderType='generating' placeholderType='generating'
@ -176,11 +176,10 @@ const Onboarding = ({
width: '12.4em', width: '12.4em',
}} }}
tooltipPosition='top' tooltipPosition='top'
baseUrl={hostUrl}
/> />
</Grid> </Grid>
{slot?.avatarLoaded === true && Boolean(robot?.nickname) ? ( {Boolean(slot?.hashId) ? (
<Grid item> <Grid item>
<Typography align='center'>{t('Hi! My name is')}</Typography> <Typography align='center'>{t('Hi! My name is')}</Typography>
<Typography component='h5' variant='h5'> <Typography component='h5' variant='h5'>
@ -199,7 +198,7 @@ const Onboarding = ({
width: '1.5em', width: '1.5em',
}} }}
/> />
<b>{robot.nickname}</b> <b>{slot?.nickname}</b>
<Bolt <Bolt
sx={{ sx={{
color: '#fcba03', color: '#fcba03',
@ -212,7 +211,7 @@ const Onboarding = ({
</Grid> </Grid>
) : null} ) : null}
<Grid item> <Grid item>
<Collapse in={!!(slot?.avatarLoaded === true && Boolean(robot?.nickname))}> <Collapse in={!!Boolean(slot?.hashId)}>
<Button <Button
onClick={() => { onClick={() => {
setStep('3'); setStep('3');

View File

@ -44,7 +44,7 @@ const RobotProfile = ({
setView, setView,
width, width,
}: RobotProfileProps): JSX.Element => { }: RobotProfileProps): JSX.Element => {
const { windowSize, hostUrl } = useContext<UseAppStoreType>(AppContext); const { windowSize } = useContext<UseAppStoreType>(AppContext);
const { garage, robotUpdatedAt, orderUpdatedAt } = useContext<UseGarageStoreType>(GarageContext); const { garage, robotUpdatedAt, orderUpdatedAt } = useContext<UseGarageStoreType>(GarageContext);
const { sortedCoordinators } = useContext<UseFederationStoreType>(FederationContext); const { sortedCoordinators } = useContext<UseFederationStoreType>(FederationContext);
@ -57,7 +57,7 @@ const RobotProfile = ({
useEffect(() => { useEffect(() => {
const slot = garage.getSlot(); const slot = garage.getSlot();
const robot = slot?.getRobot(sortedCoordinators[0]); const robot = slot?.getRobot(sortedCoordinators[0]);
if (robot?.nickname != null && slot?.avatarLoaded === true) { if (Boolean(slot?.hashId)) {
setLoading(false); setLoading(false);
} }
}, [orderUpdatedAt, robotUpdatedAt, loading]); }, [orderUpdatedAt, robotUpdatedAt, loading]);
@ -86,7 +86,7 @@ const RobotProfile = ({
sx={{ width: '100%' }} sx={{ width: '100%' }}
> >
<Grid item sx={{ height: '2.3em', position: 'relative' }}> <Grid item sx={{ height: '2.3em', position: 'relative' }}>
{slot?.avatarLoaded === true && robot?.nickname != null ? ( {Boolean(slot?.hashId) ? (
<Typography align='center' component='h5' variant='h5'> <Typography align='center' component='h5' variant='h5'>
<div <div
style={{ style={{
@ -105,7 +105,7 @@ const RobotProfile = ({
}} }}
/> />
)} )}
<b>{robot?.nickname}</b> <b>{slot?.nickname}</b>
{width < 19 ? null : ( {width < 19 ? null : (
<Bolt <Bolt
sx={{ sx={{
@ -127,7 +127,7 @@ const RobotProfile = ({
<Grid item sx={{ width: `13.5em` }}> <Grid item sx={{ width: `13.5em` }}>
<RobotAvatar <RobotAvatar
nickname={robot?.nickname} hashId={slot?.hashId}
smooth={true} smooth={true}
style={{ maxWidth: '12.5em', maxHeight: '12.5em' }} style={{ maxWidth: '12.5em', maxHeight: '12.5em' }}
placeholderType='generating' placeholderType='generating'
@ -140,7 +140,6 @@ const RobotProfile = ({
}} }}
tooltip={t('This is your trading avatar')} tooltip={t('This is your trading avatar')}
tooltipPosition='top' tooltipPosition='top'
baseUrl={hostUrl}
/> />
{robot?.found === true && slot?.lastShortAlias != null ? ( {robot?.found === true && slot?.lastShortAlias != null ? (
<Typography align='center' variant='h6'> <Typography align='center' variant='h6'>
@ -151,9 +150,7 @@ const RobotProfile = ({
)} )}
</Grid> </Grid>
{Boolean(robot?.activeOrderId) && {Boolean(robot?.activeOrderId) && Boolean(slot?.hashId) ? (
Boolean(slot?.avatarLoaded) &&
Boolean(robot?.nickname) ? (
<Grid item> <Grid item>
<Button <Button
onClick={() => { onClick={() => {
@ -167,7 +164,7 @@ const RobotProfile = ({
</Grid> </Grid>
) : null} ) : null}
{Boolean(robot?.lastOrderId) && Boolean(slot?.avatarLoaded) && Boolean(robot?.nickname) ? ( {Boolean(robot?.lastOrderId) && Boolean(slot?.hashId) ? (
<Grid item container direction='column' alignItems='center'> <Grid item container direction='column' alignItems='center'>
<Grid item> <Grid item>
<Button <Button
@ -275,17 +272,16 @@ const RobotProfile = ({
> >
<Grid item> <Grid item>
<RobotAvatar <RobotAvatar
nickname={slot?.getRobot()?.nickname} hashId={slot?.hashId}
smooth={true} smooth={true}
style={{ width: '2.6em', height: '2.6em' }} style={{ width: '2.6em', height: '2.6em' }}
placeholderType='loading' placeholderType='loading'
baseUrl={hostUrl}
small={true} small={true}
/> />
</Grid> </Grid>
<Grid item> <Grid item>
<Typography variant={windowSize.width < 26 ? 'caption' : undefined}> <Typography variant={windowSize.width < 26 ? 'caption' : undefined}>
{slot?.getRobot()?.nickname} {slot?.nickname}
</Typography> </Typography>
</Grid> </Grid>
</Grid> </Grid>

View File

@ -23,11 +23,7 @@ import { validateTokenEntropy } from '../../utils';
import { FederationContext, type UseFederationStoreType } from '../../contexts/FederationContext'; import { FederationContext, type UseFederationStoreType } from '../../contexts/FederationContext';
import { GarageContext, type UseGarageStoreType } from '../../contexts/GarageContext'; import { GarageContext, type UseGarageStoreType } from '../../contexts/GarageContext';
interface RobotPageProps { const RobotPage = (): JSX.Element => {
avatarBaseUrl: string;
}
const RobotPage = ({ avatarBaseUrl }: RobotPageProps): JSX.Element => {
const { torStatus, windowSize, settings, page } = useContext<UseAppStoreType>(AppContext); const { torStatus, windowSize, settings, page } = useContext<UseAppStoreType>(AppContext);
const { garage } = useContext<UseGarageStoreType>(GarageContext); const { garage } = useContext<UseGarageStoreType>(GarageContext);
const { federation, sortedCoordinators } = useContext<UseFederationStoreType>(FederationContext); const { federation, sortedCoordinators } = useContext<UseFederationStoreType>(FederationContext);
@ -158,7 +154,6 @@ const RobotPage = ({ avatarBaseUrl }: RobotPageProps): JSX.Element => {
inputToken={inputToken} inputToken={inputToken}
setInputToken={setInputToken} setInputToken={setInputToken}
getGenerateRobot={getGenerateRobot} getGenerateRobot={getGenerateRobot}
avatarBaseUrl={avatarBaseUrl}
/> />
) : null} ) : null}
@ -170,7 +165,6 @@ const RobotPage = ({ avatarBaseUrl }: RobotPageProps): JSX.Element => {
inputToken={inputToken} inputToken={inputToken}
setInputToken={setInputToken} setInputToken={setInputToken}
getGenerateRobot={getGenerateRobot} getGenerateRobot={getGenerateRobot}
avatarBaseUrl={avatarBaseUrl}
/> />
) : null} ) : null}

View File

@ -220,14 +220,13 @@ const BookTable = ({
> >
<ListItemAvatar> <ListItemAvatar>
<RobotAvatar <RobotAvatar
nickname={params.row.maker_nick} hashId={params.row.maker_hash_id}
style={{ width: '3.215em', height: '3.215em' }} style={{ width: '3.215em', height: '3.215em' }}
smooth={true} smooth={true}
flipHorizontally={true} flipHorizontally={true}
orderType={params.row.type} orderType={params.row.type}
statusColor={statusBadgeColor(params.row.maker_status)} statusColor={statusBadgeColor(params.row.maker_status)}
tooltip={t(params.row.maker_status)} tooltip={t(params.row.maker_status)}
baseUrl={url + basePath}
small={true} small={true}
/> />
</ListItemAvatar> </ListItemAvatar>
@ -257,14 +256,13 @@ const BookTable = ({
}} }}
> >
<RobotAvatar <RobotAvatar
nickname={params.row.maker_nick} hashId={params.row.maker_hash_id}
smooth={true} smooth={true}
flipHorizontally={true} flipHorizontally={true}
style={{ width: '3.215em', height: '3.215em' }} style={{ width: '3.215em', height: '3.215em' }}
orderType={params.row.type} orderType={params.row.type}
statusColor={statusBadgeColor(params.row.maker_status)} statusColor={statusBadgeColor(params.row.maker_status)}
tooltip={t(params.row.maker_status)} tooltip={t(params.row.maker_status)}
baseUrl={url + basePath}
/> />
</div> </div>
); );
@ -293,12 +291,10 @@ const BookTable = ({
> >
<ListItemAvatar> <ListItemAvatar>
<RobotAvatar <RobotAvatar
nickname={params.row.coordinatorShortAlias} shortAlias={params.row.coordinatorShortAlias}
coordinator={true}
style={{ width: '3.215em', height: '3.215em' }} style={{ width: '3.215em', height: '3.215em' }}
smooth={true} smooth={true}
flipHorizontally={true} flipHorizontally={true}
baseUrl={hostUrl}
small={true} small={true}
/> />
</ListItemAvatar> </ListItemAvatar>

View File

@ -362,12 +362,10 @@ const CoordinatorDialog = ({ open = false, onClose, network, shortAlias }: Props
<Grid container direction='column' alignItems='center' padding={0}> <Grid container direction='column' alignItems='center' padding={0}>
<Grid item> <Grid item>
<RobotAvatar <RobotAvatar
nickname={coordinator?.shortAlias} shortAlias={coordinator?.shortAlias}
coordinator={true}
style={{ width: '7.5em', height: '7.5em' }} style={{ width: '7.5em', height: '7.5em' }}
smooth={true} smooth={true}
flipHorizontally={false} flipHorizontally={false}
baseUrl={hostUrl}
/> />
</Grid> </Grid>
<Grid item> <Grid item>

View File

@ -33,7 +33,7 @@ const ProfileDialog = ({ open = false, baseUrl, onClose }: Props): JSX.Element =
const [loading, setLoading] = useState<boolean>(true); const [loading, setLoading] = useState<boolean>(true);
useEffect(() => { useEffect(() => {
setLoading(!(garage.getSlot()?.avatarLoaded === true)); setLoading(!Boolean(garage.getSlot()?.hashId));
}, [robotUpdatedAt]); }, [robotUpdatedAt]);
return ( return (
@ -57,7 +57,7 @@ const ProfileDialog = ({ open = false, baseUrl, onClose }: Props): JSX.Element =
<ListItem className='profileNickname'> <ListItem className='profileNickname'>
<ListItemText secondary={t('Your robot')}> <ListItemText secondary={t('Your robot')}>
<Typography component='h6' variant='h6'> <Typography component='h6' variant='h6'>
{garage.getSlot()?.getRobot()?.nickname !== undefined && ( {garage.getSlot()?.nickname !== undefined && (
<div style={{ position: 'relative', left: '-7px' }}> <div style={{ position: 'relative', left: '-7px' }}>
<div <div
style={{ style={{
@ -70,7 +70,7 @@ const ProfileDialog = ({ open = false, baseUrl, onClose }: Props): JSX.Element =
> >
<BoltIcon sx={{ color: '#fcba03', height: '28px', width: '24px' }} /> <BoltIcon sx={{ color: '#fcba03', height: '28px', width: '24px' }} />
<a>{garage.getSlot()?.getRobot()?.nickname}</a> <a>{garage.getSlot()?.nickname}</a>
<BoltIcon sx={{ color: '#fcba03', height: '28px', width: '24px' }} /> <BoltIcon sx={{ color: '#fcba03', height: '28px', width: '24px' }} />
</div> </div>
@ -83,8 +83,7 @@ const ProfileDialog = ({ open = false, baseUrl, onClose }: Props): JSX.Element =
<RobotAvatar <RobotAvatar
avatarClass='profileAvatar' avatarClass='profileAvatar'
style={{ width: 65, height: 65 }} style={{ width: 65, height: 65 }}
nickname={garage.getSlot()?.getRobot()?.nickname} hashId={garage.getSlot()?.hashId}
baseUrl={baseUrl}
/> />
</ListItemAvatar> </ListItemAvatar>
</ListItem> </ListItem>
@ -97,7 +96,7 @@ const ProfileDialog = ({ open = false, baseUrl, onClose }: Props): JSX.Element =
</Typography> </Typography>
{Object.values(federation.coordinators).map((coordinator: Coordinator): JSX.Element => { {Object.values(federation.coordinators).map((coordinator: Coordinator): JSX.Element => {
if (garage.getSlot()?.avatarLoaded === true) { if (Boolean(garage.getSlot()?.hashId)) {
return ( return (
<div key={coordinator.shortAlias}> <div key={coordinator.shortAlias}>
<RobotInfo coordinator={coordinator} onClose={onClose} /> <RobotInfo coordinator={coordinator} onClose={onClose} />

View File

@ -75,12 +75,10 @@ const FederationTable = ({
> >
<Grid item> <Grid item>
<RobotAvatar <RobotAvatar
nickname={params.row.shortAlias} shortAlias={params.row.shortAlias}
coordinator={true}
style={{ width: '3.215em', height: '3.215em' }} style={{ width: '3.215em', height: '3.215em' }}
smooth={true} smooth={true}
flipHorizontally={true} flipHorizontally={true}
baseUrl={hostUrl}
small={true} small={true}
/> />
</Grid> </Grid>

View File

@ -577,7 +577,7 @@ const MakerForm = ({
setOpenDialogs(false); setOpenDialogs(false);
}} }}
onClickDone={handleCreateOrder} onClickDone={handleCreateOrder}
hasRobot={garage.getSlot()?.avatarLoaded ?? false} hasRobot={Boolean(garage.getSlot()?.hashId)}
onClickGenerateRobot={onClickGenerateRobot} onClickGenerateRobot={onClickGenerateRobot}
/> />
<F2fMapDialog <F2fMapDialog

View File

@ -372,7 +372,7 @@ const TakeButton = ({
setLoadingTake(true); setLoadingTake(true);
setOpen(closeAll); setOpen(closeAll);
}} }}
hasRobot={garage.getSlot()?.avatarLoaded ?? false} hasRobot={Boolean(garage.getSlot()?.hashId)}
onClickGenerateRobot={onClickGenerateRobot} onClickGenerateRobot={onClickGenerateRobot}
/> />
<InactiveMakerDialog /> <InactiveMakerDialog />

View File

@ -264,12 +264,7 @@ const OrderDetails = ({
{' '} {' '}
<Grid container direction='row' justifyContent='center' alignItems='center'> <Grid container direction='row' justifyContent='center' alignItems='center'>
<Grid item xs={2}> <Grid item xs={2}>
<RobotAvatar <RobotAvatar shortAlias={coordinator.shortAlias} coordinator={true} small={true} />
nickname={coordinator.shortAlias}
coordinator={true}
baseUrl={hostUrl}
small={true}
/>
</Grid> </Grid>
<Grid item xs={4}> <Grid item xs={4}>
<ListItemText primary={coordinator.longAlias} secondary={t('Order host')} /> <ListItemText primary={coordinator.longAlias} secondary={t('Order host')} />
@ -283,10 +278,9 @@ const OrderDetails = ({
<ListItemAvatar sx={{ width: '4em', height: '4em' }}> <ListItemAvatar sx={{ width: '4em', height: '4em' }}>
<RobotAvatar <RobotAvatar
statusColor={statusBadgeColor(currentOrder?.maker_status ?? '')} statusColor={statusBadgeColor(currentOrder?.maker_status ?? '')}
nickname={currentOrder?.maker_nick} hashId={currentOrder?.maker_hash_id}
tooltip={t(currentOrder?.maker_status ?? '')} tooltip={t(currentOrder?.maker_status ?? '')}
orderType={currentOrder?.type} orderType={currentOrder?.type}
baseUrl={baseUrl}
small={true} small={true}
/> />
</ListItemAvatar> </ListItemAvatar>
@ -315,12 +309,11 @@ const OrderDetails = ({
<RobotAvatar <RobotAvatar
avatarClass='smallAvatar' avatarClass='smallAvatar'
statusColor={statusBadgeColor(currentOrder?.taker_status ?? '')} statusColor={statusBadgeColor(currentOrder?.taker_status ?? '')}
nickname={ hashId={
currentOrder?.taker_nick === 'None' ? undefined : currentOrder?.taker_nick currentOrder?.taker_hash_id === 'None' ? undefined : currentOrder?.taker_hash_id
} }
tooltip={t(currentOrder?.taker_status ?? '')} tooltip={t(currentOrder?.taker_status ?? '')}
orderType={currentOrder?.type === 0 ? 1 : 0} orderType={currentOrder?.type === 0 ? 1 : 0}
baseUrl={baseUrl}
small={true} small={true}
/> />
</ListItemAvatar> </ListItemAvatar>

View File

@ -0,0 +1,73 @@
class RoboGenerator {
private assetsCache: Record<string, string> = {};
private assetsPromises: Record<string, Promise<string>> = {};
private readonly workers: Worker[] = [];
constructor() {
// limit to 8 workers
const numCores = Math.min(navigator.hardwareConcurrency || 1, 8);
for (let i = 0; i < numCores; i++) {
const worker = new Worker(new URL('./robohash.worker.ts', import.meta.url));
this.workers.push(worker);
}
}
public generate: (hash: string, size: 'small' | 'large') => Promise<string> = async (
hash,
size,
) => {
const cacheKey = `${size}px;${hash}`;
if (this.assetsCache[cacheKey]) {
return this.assetsCache[cacheKey];
} else if (cacheKey in this.assetsPromises) {
return await this.assetsPromises[cacheKey];
}
const workerIndex = Object.keys(this.assetsPromises).length % this.workers.length;
const worker = this.workers[workerIndex];
this.assetsPromises[cacheKey] = new Promise<string>((resolve, reject) => {
// const avatarB64 = async_generate_robohash(hash, size == 'small' ? 80 : 256).then((avatarB64)=> resolve(`data:image/png;base64,${avatarB64}`));
// Create a message object with the necessary data
const message = { hash, size, cacheKey, workerIndex };
// Listen for messages from the worker
const handleMessage = (event: MessageEvent) => {
const { cacheKey, imageUrl } = event.data;
// Update the cache and resolve the promise
this.assetsCache[cacheKey] = imageUrl;
delete this.assetsPromises[cacheKey];
resolve(imageUrl);
};
// Add the event listener for messages
worker.addEventListener('message', handleMessage);
// Send the message to the worker
worker.postMessage(message);
// Clean up the event listener after receiving the result
const cleanup = () => {
worker.removeEventListener('message', handleMessage);
};
// Reject the promise if an error occurs
worker.addEventListener('error', (error) => {
cleanup();
reject(error);
});
// Reject the promise if the worker times out
setTimeout(() => {
cleanup();
reject(new Error('Generation timed out'));
}, 5000); // Adjust the timeout duration as needed
});
return await this.assetsPromises[cacheKey];
};
}
export const robohash = new RoboGenerator();

View File

@ -2,15 +2,14 @@ import React, { useCallback, useContext, useEffect, useMemo, useState } from 're
import SmoothImage from 'react-smooth-image'; import SmoothImage from 'react-smooth-image';
import { Avatar, Badge, Tooltip } from '@mui/material'; import { Avatar, Badge, Tooltip } from '@mui/material';
import { SendReceiveIcon } from '../Icons'; import { SendReceiveIcon } from '../Icons';
import { apiClient } from '../../services/api';
import placeholder from './placeholder.json'; import placeholder from './placeholder.json';
import { type UseAppStoreType, AppContext } from '../../contexts/AppContext'; import { robohash } from './RobohashGenerator';
import { type UseFederationStoreType, FederationContext } from '../../contexts/FederationContext'; import { AppContext, UseAppStoreType } from '../../contexts/AppContext';
interface Props { interface Props {
nickname: string | undefined; shortAlias?: string | undefined;
hashId?: string | undefined;
smooth?: boolean; smooth?: boolean;
coordinator?: boolean;
small?: boolean; small?: boolean;
flipHorizontally?: boolean; flipHorizontally?: boolean;
style?: object; style?: object;
@ -22,7 +21,6 @@ interface Props {
tooltipPosition?: string; tooltipPosition?: string;
avatarClass?: string; avatarClass?: string;
onLoad?: () => void; onLoad?: () => void;
baseUrl: string;
} }
interface BackgroundData { interface BackgroundData {
@ -31,7 +29,8 @@ interface BackgroundData {
} }
const RobotAvatar: React.FC<Props> = ({ const RobotAvatar: React.FC<Props> = ({
nickname, shortAlias,
hashId,
orderType, orderType,
statusColor, statusColor,
tooltip, tooltip,
@ -44,14 +43,12 @@ const RobotAvatar: React.FC<Props> = ({
avatarClass = 'flippedSmallAvatar', avatarClass = 'flippedSmallAvatar',
imageStyle = {}, imageStyle = {},
onLoad = () => {}, onLoad = () => {},
coordinator = false,
baseUrl,
}) => { }) => {
const [avatarSrc, setAvatarSrc] = useState<string>(); const [avatarSrc, setAvatarSrc] = useState<string>('');
const [nicknameReady, setNicknameReady] = useState<boolean>(false);
const [activeBackground, setActiveBackground] = useState<boolean>(true); const [activeBackground, setActiveBackground] = useState<boolean>(true);
const { hostUrl } = useContext<UseAppStoreType>(AppContext);
const backgroundFadeTime = 3000;
const path = coordinator ? '/static/federation/avatars/' : '/static/assets/avatars/';
const [backgroundData] = useState<BackgroundData>( const [backgroundData] = useState<BackgroundData>(
placeholderType === 'generating' ? placeholder.generating : placeholder.loading, placeholderType === 'generating' ? placeholder.generating : placeholder.loading,
); );
@ -59,21 +56,42 @@ const RobotAvatar: React.FC<Props> = ({
const className = placeholderType === 'loading' ? 'loadingAvatar' : 'generatingAvatar'; const className = placeholderType === 'loading' ? 'loadingAvatar' : 'generatingAvatar';
useEffect(() => { useEffect(() => {
if (nickname !== undefined) { // TODO: HANDLE ANDROID AVATARS TOO (when window.NativeRobosats !== undefined)
if (hashId !== undefined) {
robohash
.generate(hashId, small ? 'small' : 'large')
.then((avatar) => {
setAvatarSrc(avatar);
})
.catch(() => {
setAvatarSrc('');
});
setTimeout(() => {
setActiveBackground(false);
}, backgroundFadeTime);
}
}, [hashId]);
useEffect(() => {
if (shortAlias !== undefined) {
if (window.NativeRobosats === undefined) { if (window.NativeRobosats === undefined) {
setAvatarSrc(`${baseUrl}${path}${nickname}${small ? '.small' : ''}.webp`); setAvatarSrc(
setNicknameReady(true); `${hostUrl}/static/federation/avatars/${shortAlias}${small ? '.small' : ''}.webp`,
} else if (baseUrl != null && apiClient.fileImageUrl !== undefined) { );
setNicknameReady(true); } else {
void apiClient setAvatarSrc(
.fileImageUrl(baseUrl, `${path}${nickname}${small ? '.small' : ''}.webp`) `file:///android_asset/Web.bundle/assets/federation/avatars/${shortAlias}${
.then(setAvatarSrc); small ? ' .small' : ''
}.webp`,
);
} }
setTimeout(() => {
setActiveBackground(false);
}, backgroundFadeTime);
} else { } else {
setNicknameReady(false);
setActiveBackground(true); setActiveBackground(true);
} }
}, [nickname]); }, [shortAlias]); // TODO: should hashId
const statusBadge = ( const statusBadge = (
<div style={{ position: 'relative', left: '0.428em', top: '0.07em' }}> <div style={{ position: 'relative', left: '0.428em', top: '0.07em' }}>
@ -104,15 +122,12 @@ const RobotAvatar: React.FC<Props> = ({
> >
<div className={className}> <div className={className}>
<SmoothImage <SmoothImage
src={nicknameReady ? avatarSrc : null} src={avatarSrc}
imageStyles={{ imageStyles={{
borderRadius: '50%', borderRadius: '50%',
border: '0.3px solid #55555', border: '0.3px solid #55555',
filter: 'dropShadow(0.5px 0.5px 0.5px #000000)', filter: 'dropShadow(0.5px 0.5px 0.5px #000000)',
...imageStyle, ...imageStyle,
onLoad: setTimeout(() => {
setActiveBackground(false);
}, 5000),
}} }}
/> />
</div> </div>
@ -123,8 +138,8 @@ const RobotAvatar: React.FC<Props> = ({
<Avatar <Avatar
className={avatarClass} className={avatarClass}
style={style} style={style}
alt={nickname} alt={hashId ?? shortAlias ?? 'unknown'}
src={nicknameReady ? avatarSrc : null} src={avatarSrc}
imgProps={{ imgProps={{
sx: { transform: flipHorizontally ? 'scaleX(-1)' : '' }, sx: { transform: flipHorizontally ? 'scaleX(-1)' : '' },
style: { transform: flipHorizontally ? 'scaleX(-1)' : '' }, style: { transform: flipHorizontally ? 'scaleX(-1)' : '' },
@ -133,7 +148,7 @@ const RobotAvatar: React.FC<Props> = ({
/> />
); );
} }
}, [nickname, nicknameReady, avatarSrc, statusColor, tooltip, avatarClass]); }, [hashId, shortAlias, avatarSrc, statusColor, tooltip, avatarClass]);
const getAvatarWithBadges = useCallback(() => { const getAvatarWithBadges = useCallback(() => {
let component = avatar; let component = avatar;

View File

@ -0,0 +1,15 @@
import { async_generate_robohash } from 'robo-identities-wasm';
// Listen for messages from the main thread
self.addEventListener('message', async (event) => {
const { hash, size, cacheKey, workerIndex } = event.data;
// Generate the image using async_image_base
const t0 = performance.now();
const avatarB64 = await async_generate_robohash(hash, size == 'small' ? 80 : 256);
const imageUrl = `data:image/png;base64,${avatarB64}`;
const t1 = performance.now();
console.log(`Worker ${workerIndex} :: Time to generate avatar: ${t1 - t0} ms`);
// Send the result back to the main thread
self.postMessage({ cacheKey, imageUrl });
});

View File

@ -63,7 +63,7 @@ const EncryptedSocketChat: React.FC<Props> = ({
const [error, setError] = useState<string>(''); const [error, setError] = useState<string>('');
useEffect(() => { useEffect(() => {
if (!connected && garage.getSlot()?.avatarLoaded === true) { if (!connected && Boolean(garage.getSlot()?.hashId)) {
connectWebsocket(); connectWebsocket();
} }
}, [connected, robotUpdatedAt]); }, [connected, robotUpdatedAt]);

View File

@ -152,10 +152,9 @@ export const useFederationStore = (): UseFederationStoreType => {
const robot = slot?.getRobot(); const robot = slot?.getRobot();
if (robot != null && garage.currentSlot != null) { if (robot != null && garage.currentSlot != null) {
if (open.profile && slot?.avatarLoaded === true && slot.token != null) { if (open.profile && Boolean(slot?.hashId) && slot?.token) {
void federation.fetchRobot(garage, slot.token); // refresh/update existing robot void federation.fetchRobot(garage, slot?.token); // refresh/update existing robot
} else if ( } else if (
!(slot?.avatarLoaded === true) &&
robot.token !== undefined && robot.token !== undefined &&
robot.encPrivKey !== undefined && robot.encPrivKey !== undefined &&
robot.pubKey !== undefined robot.pubKey !== undefined

View File

@ -20,6 +20,7 @@ export interface PublicOrder {
maker: number; maker: number;
escrow_duration: number; escrow_duration: number;
maker_nick: string; maker_nick: string;
maker_hash_id: string;
price: number; price: number;
maker_status: 'Active' | 'Seen recently' | 'Inactive'; maker_status: 'Active' | 'Seen recently' | 'Inactive';
coordinatorShortAlias?: string; coordinatorShortAlias?: string;

View File

@ -10,6 +10,7 @@ import { apiClient } from '../services/api';
import { validateTokenEntropy } from '../utils'; import { validateTokenEntropy } from '../utils';
import { compareUpdateLimit } from './Limit.model'; import { compareUpdateLimit } from './Limit.model';
import { defaultOrder } from './Order.model'; import { defaultOrder } from './Order.model';
import { robohash } from '../components/RobotAvatar/RobohashGenerator';
export interface Contact { export interface Contact {
nostr?: string | undefined; nostr?: string | undefined;
@ -156,6 +157,12 @@ export class Coordinator {
this.loadInfo(onDataLoad); this.loadInfo(onDataLoad);
}; };
generateAllMakerAvatars = (data: [PublicOrder]) => {
for (const order of data) {
robohash.generate(order.maker_hash_id, 'small');
}
};
loadBook = (onDataLoad: () => void = () => {}): void => { loadBook = (onDataLoad: () => void = () => {}): void => {
if (this.enabled === false) return; if (this.enabled === false) return;
if (this.loadingBook) return; if (this.loadingBook) return;
@ -170,6 +177,7 @@ export class Coordinator {
order.coordinatorShortAlias = this.shortAlias; order.coordinatorShortAlias = this.shortAlias;
return order; return order;
}); });
this.generateAllMakerAvatars(data);
onDataLoad(); onDataLoad();
} }
}) })

View File

@ -98,13 +98,12 @@ class Garage {
} }
}; };
updateSlot: ( updateSlot: (attributes: { copiedToken?: boolean }, token?: string) => Slot | null = (
attributes: { avatarLoaded?: boolean; copiedToken?: boolean }, attributes,
token?: string, token,
) => Slot | null = (attributes, token) => { ) => {
const slot = this.getSlot(token); const slot = this.getSlot(token);
if (attributes != null) { if (attributes != null) {
if (attributes.avatarLoaded !== undefined) slot?.setAvatarLoaded(attributes.avatarLoaded);
if (attributes.copiedToken !== undefined) slot?.setCopiedToken(attributes.copiedToken); if (attributes.copiedToken !== undefined) slot?.setCopiedToken(attributes.copiedToken);
this.triggerHook('onRobotUpdate'); this.triggerHook('onRobotUpdate');
} }

View File

@ -62,7 +62,9 @@ export interface Order {
is_buyer: boolean; is_buyer: boolean;
is_seller: boolean; is_seller: boolean;
maker_nick: string; maker_nick: string;
maker_hash_id: string;
taker_nick: string; taker_nick: string;
taker_hash_id: string;
status_message: string; status_message: string;
is_fiat_sent: boolean; is_fiat_sent: boolean;
is_disputed: boolean; is_disputed: boolean;
@ -150,7 +152,9 @@ export const defaultOrder: Order = {
is_buyer: false, is_buyer: false,
is_seller: false, is_seller: false,
maker_nick: '', maker_nick: '',
maker_hash_id: '',
taker_nick: '', taker_nick: '',
taker_hash_id: '',
status_message: '', status_message: '',
is_fiat_sent: false, is_fiat_sent: false,
is_disputed: false, is_disputed: false,

View File

@ -20,7 +20,6 @@ class Robot {
} }
} }
public nickname?: string;
public token?: string; public token?: string;
public bitsEntropy?: number; public bitsEntropy?: number;
public shannonEntropy?: number; public shannonEntropy?: number;

View File

@ -1,27 +1,34 @@
import { sha256 } from 'js-sha256';
import { Robot, type Order } from '.'; import { Robot, type Order } from '.';
import { robohash } from '../components/RobotAvatar/RobohashGenerator';
import { generate_roboname } from 'robo-identities-wasm';
class Slot { class Slot {
constructor(token: string) { constructor(token: string) {
this.token = token; this.token = token;
this.hashId = sha256(sha256(this.token));
this.nickname = generate_roboname(this.hashId);
// trigger RoboHash avatar generation in webworker and store in RoboHash class cache.
robohash.generate(this.hashId, 'small');
robohash.generate(this.hashId, 'large');
this.robots = {}; this.robots = {};
this.order = null; this.order = null;
this.activeShortAlias = null; this.activeShortAlias = null;
this.lastShortAlias = null; this.lastShortAlias = null;
this.copiedToken = false; this.copiedToken = false;
this.avatarLoaded = false;
} }
token: string | null; token: string | null;
hashId: string | null;
nickname: string | null;
robots: Record<string, Robot>; robots: Record<string, Robot>;
order: Order | null; order: Order | null;
activeShortAlias: string | null; activeShortAlias: string | null;
lastShortAlias: string | null; lastShortAlias: string | null;
copiedToken: boolean; copiedToken: boolean;
avatarLoaded: boolean;
setAvatarLoaded = (avatarLoaded: boolean): void => {
this.avatarLoaded = avatarLoaded;
};
setCopiedToken = (copied: boolean): void => { setCopiedToken = (copied: boolean): void => {
this.copiedToken = copied; this.copiedToken = copied;

View File

@ -18,6 +18,7 @@ const config: Configuration = {
}, },
], ],
}, },
experiments: { asyncWebAssembly: true },
resolve: { resolve: {
extensions: ['.tsx', '.ts', '.jsx', '.js'], extensions: ['.tsx', '.ts', '.jsx', '.js'],
}, },