mirror of
https://github.com/Qortal/names.git
synced 2025-06-15 10:41:22 +00:00
1080 lines
30 KiB
TypeScript
1080 lines
30 KiB
TypeScript
import {
|
|
Table,
|
|
TableBody,
|
|
TableCell,
|
|
TableContainer,
|
|
TableHead,
|
|
TableRow,
|
|
Paper,
|
|
Button,
|
|
Box,
|
|
Tooltip,
|
|
Dialog,
|
|
DialogTitle,
|
|
DialogContent,
|
|
DialogContentText,
|
|
DialogActions,
|
|
TextField,
|
|
useTheme,
|
|
Typography,
|
|
CircularProgress,
|
|
Avatar,
|
|
} from '@mui/material';
|
|
import { useAtom, useSetAtom } from 'jotai';
|
|
import { forwardRef, useCallback, useEffect, useMemo, useState } from 'react';
|
|
import { TableVirtuoso, TableComponents } from 'react-virtuoso';
|
|
import {
|
|
forceRefreshAtom,
|
|
forSaleAtom,
|
|
isNamePendingTx,
|
|
Names,
|
|
namesAtom,
|
|
NamesForSale,
|
|
pendingTxsAtom,
|
|
PendingTxsState,
|
|
refreshAtom,
|
|
} from '../../state/global/names';
|
|
import PersonIcon from '@mui/icons-material/Person';
|
|
import {
|
|
ModalFunctions,
|
|
ModalFunctionsAvatar,
|
|
ModalFunctionsSellName,
|
|
useModal,
|
|
} from '../../hooks/useModal';
|
|
import {
|
|
dismissToast,
|
|
ImagePicker,
|
|
RequestQueueWithPromise,
|
|
showError,
|
|
showLoading,
|
|
showSuccess,
|
|
Spacer,
|
|
useGlobal,
|
|
} from 'qapp-core';
|
|
import CheckIcon from '@mui/icons-material/Check';
|
|
import ErrorIcon from '@mui/icons-material/Error';
|
|
import { BarSpinner } from '../../common/Spinners/BarSpinner/BarSpinner';
|
|
import { usePendingTxs } from '../../hooks/useHandlePendingTxs';
|
|
import { FetchPrimaryNameType, useFetchNames } from '../../hooks/useFetchNames';
|
|
import { Availability } from '../../interfaces';
|
|
import { SetStateAction } from 'jotai';
|
|
import { useTranslation } from 'react-i18next';
|
|
import { TFunction } from 'i18next';
|
|
interface NameData {
|
|
name: string;
|
|
isSelling?: boolean;
|
|
}
|
|
|
|
const getNameQueue = new RequestQueueWithPromise(2);
|
|
|
|
const VirtuosoTableComponents: TableComponents<NameData> = {
|
|
Scroller: forwardRef<HTMLDivElement>((props, ref) => (
|
|
<TableContainer component={Paper} {...props} ref={ref} />
|
|
)),
|
|
Table: (props) => (
|
|
<Table
|
|
{...props}
|
|
sx={{ borderCollapse: 'separate', tableLayout: 'fixed' }}
|
|
/>
|
|
),
|
|
TableHead: forwardRef<HTMLTableSectionElement>((props, ref) => (
|
|
<TableHead {...props} ref={ref} />
|
|
)),
|
|
TableRow,
|
|
TableBody: forwardRef<HTMLTableSectionElement>((props, ref) => (
|
|
<TableBody {...props} ref={ref} />
|
|
)),
|
|
};
|
|
|
|
function fixedHeaderContent(t: TFunction) {
|
|
return (
|
|
<TableRow
|
|
sx={{
|
|
backgroundColor: 'background.paper',
|
|
}}
|
|
>
|
|
<TableCell>
|
|
{t('core:tables.name', {
|
|
postProcess: 'capitalizeFirstChar',
|
|
})}
|
|
</TableCell>
|
|
<TableCell>
|
|
{t('core:tables.actions', {
|
|
postProcess: 'capitalizeFirstChar',
|
|
})}
|
|
</TableCell>
|
|
</TableRow>
|
|
);
|
|
}
|
|
|
|
interface ManageAvatarProps {
|
|
name: string;
|
|
modalFunctionsAvatar: ModalFunctionsAvatar;
|
|
isNameCurrentlyDoingATx?: boolean;
|
|
}
|
|
|
|
const ManageAvatar = ({
|
|
name,
|
|
modalFunctionsAvatar,
|
|
isNameCurrentlyDoingATx,
|
|
}: ManageAvatarProps) => {
|
|
const { setHasAvatar, getHasAvatar } = usePendingTxs();
|
|
const [refresh] = useAtom(refreshAtom); // just to subscribe
|
|
const [hasAvatarState, setHasAvatarState] = useState<boolean | null>(null);
|
|
const { t } = useTranslation();
|
|
const checkIfAvatarExists = useCallback(
|
|
async (name: string) => {
|
|
try {
|
|
const res = getHasAvatar(name);
|
|
if (res !== null) {
|
|
setHasAvatarState(res);
|
|
return;
|
|
}
|
|
const identifier = `qortal_avatar`;
|
|
const url = `/arbitrary/resources/searchsimple?mode=ALL&service=THUMBNAIL&identifier=${identifier}&limit=1&name=${name}&includemetadata=false&prefix=true`;
|
|
const response = await getNameQueue.enqueue(() =>
|
|
fetch(url, {
|
|
method: 'GET',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
})
|
|
);
|
|
|
|
const responseData = await response.json();
|
|
if (responseData?.length > 0) {
|
|
setHasAvatarState(true);
|
|
setHasAvatar(name, true);
|
|
} else {
|
|
setHasAvatarState(false);
|
|
}
|
|
} catch (error) {
|
|
console.log(error);
|
|
}
|
|
},
|
|
[getHasAvatar, setHasAvatar]
|
|
);
|
|
useEffect(() => {
|
|
if (!name) return;
|
|
checkIfAvatarExists(name);
|
|
}, [name, checkIfAvatarExists, refresh]);
|
|
return (
|
|
<Button
|
|
variant="outlined"
|
|
size="small"
|
|
disabled={hasAvatarState === null || isNameCurrentlyDoingATx}
|
|
onClick={() =>
|
|
modalFunctionsAvatar.show({ name, hasAvatar: Boolean(hasAvatarState) })
|
|
}
|
|
>
|
|
{hasAvatarState === null ? (
|
|
<CircularProgress size={10} />
|
|
) : hasAvatarState ? (
|
|
t('core:actions.update_avatar', {
|
|
postProcess: 'capitalizeFirstChar',
|
|
})
|
|
) : (
|
|
t('core:actions.set_avatar', {
|
|
postProcess: 'capitalizeFirstChar',
|
|
})
|
|
)}
|
|
</Button>
|
|
);
|
|
};
|
|
|
|
type SetPendingTxs = (update: SetStateAction<PendingTxsState>) => void;
|
|
type SetNames = (update: SetStateAction<Names[]>) => void;
|
|
|
|
type SetNamesForSale = (update: SetStateAction<NamesForSale[]>) => void;
|
|
function rowContent(
|
|
_index: number,
|
|
row: NameData,
|
|
primaryName: string,
|
|
address: string,
|
|
fetchPrimaryName: FetchPrimaryNameType,
|
|
numberOfNames: number,
|
|
modalFunctions: ModalFunctions,
|
|
modalFunctionsUpdateName: ReturnType<typeof useModal>,
|
|
modalFunctionsAvatar: ModalFunctionsAvatar,
|
|
modalFunctionsSellName: ReturnType<typeof useModal>,
|
|
setPendingTxs: SetPendingTxs,
|
|
setNames: SetNames,
|
|
setNamesForSale: SetNamesForSale,
|
|
isNameCurrentlyDoingATx: boolean,
|
|
t: TFunction
|
|
) {
|
|
const handleUpdate = async (name: string) => {
|
|
if (name === primaryName && numberOfNames > 1) {
|
|
showError(
|
|
t('core:actions.set_avatar', {
|
|
postProcess: 'capitalizeFirstChar',
|
|
})
|
|
);
|
|
return;
|
|
}
|
|
const loadId = showLoading(
|
|
t('core:update_name.responses.loading', {
|
|
postProcess: 'capitalizeFirstChar',
|
|
})
|
|
);
|
|
|
|
try {
|
|
const response = await modalFunctionsUpdateName.show(undefined);
|
|
if (typeof response !== 'string') throw new Error('Invalid name');
|
|
const res = await qortalRequest({
|
|
action: 'UPDATE_NAME',
|
|
newName: response,
|
|
oldName: name,
|
|
});
|
|
showSuccess(
|
|
t('core:update_name.responses.loading', {
|
|
postProcess: 'capitalizeFirstChar',
|
|
})
|
|
);
|
|
setPendingTxs((prev) => {
|
|
return {
|
|
...prev, // preserve existing categories
|
|
['UPDATE_NAME']: {
|
|
...(prev['UPDATE_NAME'] || {}), // preserve existing transactions in this category
|
|
[res.signature]: {
|
|
...res,
|
|
status: 'PENDING',
|
|
callback: () => {
|
|
setNames((prev) => {
|
|
const copyArray = [...prev];
|
|
const findIndex = copyArray.findIndex(
|
|
(item) => item.name === res.name
|
|
);
|
|
if (findIndex === -1) return copyArray;
|
|
copyArray[findIndex] = {
|
|
name: res.newName,
|
|
owner: res.creatorAddress,
|
|
};
|
|
return copyArray;
|
|
});
|
|
fetchPrimaryName(address);
|
|
},
|
|
}, // add or overwrite this transaction
|
|
},
|
|
};
|
|
});
|
|
} catch (error) {
|
|
if (error instanceof Error) {
|
|
showError(error?.message);
|
|
return;
|
|
}
|
|
showError(
|
|
t('core:update_name.responses.loading', {
|
|
postProcess: 'capitalizeFirstChar',
|
|
})
|
|
);
|
|
console.log('error', error);
|
|
} finally {
|
|
dismissToast(loadId);
|
|
}
|
|
|
|
// Your logic here
|
|
};
|
|
|
|
const handleSell = async (name: string) => {
|
|
if (name === primaryName && numberOfNames > 1) {
|
|
showError(
|
|
t('core:sell_name.responses.error1', {
|
|
postProcess: 'capitalizeFirstChar',
|
|
})
|
|
);
|
|
return;
|
|
}
|
|
const loadId = showLoading(
|
|
t('core:sell_name.responses.loading', {
|
|
postProcess: 'capitalizeFirstChar',
|
|
})
|
|
);
|
|
try {
|
|
if (name === primaryName) {
|
|
await modalFunctions.show({ name });
|
|
}
|
|
const price = await modalFunctionsSellName.show(name);
|
|
if (typeof price !== 'string' && typeof price !== 'number')
|
|
throw new Error(
|
|
t('core:sell_name.responses.error3', {
|
|
postProcess: 'capitalizeFirstChar',
|
|
})
|
|
);
|
|
const res = await qortalRequest({
|
|
action: 'SELL_NAME',
|
|
nameForSale: name,
|
|
salePrice: +price,
|
|
});
|
|
showSuccess(
|
|
t('core:sell_name.responses.success', {
|
|
postProcess: 'capitalizeFirstChar',
|
|
})
|
|
);
|
|
setPendingTxs((prev) => {
|
|
return {
|
|
...prev, // preserve existing categories
|
|
['SELL_NAME']: {
|
|
...(prev['SELL_NAME'] || {}), // preserve existing transactions in this category
|
|
[res.signature]: {
|
|
...res,
|
|
status: 'PENDING',
|
|
callback: () => {
|
|
setNamesForSale((prev) => {
|
|
return [
|
|
{
|
|
name: res.name,
|
|
salePrice: res.amount,
|
|
},
|
|
...prev,
|
|
];
|
|
});
|
|
},
|
|
}, // add or overwrite this transaction
|
|
},
|
|
};
|
|
});
|
|
} catch (error) {
|
|
if (error instanceof Error) {
|
|
showError(error?.message);
|
|
|
|
return;
|
|
}
|
|
showError(
|
|
t('core:sell_name.responses.error2', {
|
|
postProcess: 'capitalizeFirstChar',
|
|
})
|
|
);
|
|
console.log('error', error);
|
|
} finally {
|
|
dismissToast(loadId);
|
|
}
|
|
};
|
|
|
|
const handleCancel = async (name: string) => {
|
|
const loadId = showLoading(
|
|
t('core:cancel_name.responses.error2', {
|
|
postProcess: 'capitalizeFirstChar',
|
|
})
|
|
);
|
|
|
|
try {
|
|
const res = await qortalRequest({
|
|
action: 'CANCEL_SELL_NAME',
|
|
nameForSale: name,
|
|
});
|
|
setPendingTxs((prev) => {
|
|
return {
|
|
...prev, // preserve existing categories
|
|
['CANCEL_SELL_NAME']: {
|
|
...(prev['CANCEL_SELL_NAME'] || {}), // preserve existing transactions in this category
|
|
[res.signature]: {
|
|
...res,
|
|
status: 'PENDING',
|
|
callback: () => {
|
|
setNamesForSale((prev) =>
|
|
prev.filter((item) => item?.name !== res.name)
|
|
);
|
|
},
|
|
},
|
|
},
|
|
};
|
|
});
|
|
showSuccess(
|
|
t('core:cancel_name.responses.error2', {
|
|
postProcess: 'capitalizeFirstChar',
|
|
})
|
|
);
|
|
} catch (error) {
|
|
if (error instanceof Error) {
|
|
showError(error?.message);
|
|
return;
|
|
}
|
|
showError(
|
|
t('core:cancel_name.responses.error2', {
|
|
postProcess: 'capitalizeFirstChar',
|
|
})
|
|
);
|
|
} finally {
|
|
dismissToast(loadId);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<TableCell>
|
|
<Box
|
|
sx={{
|
|
display: 'flex',
|
|
gap: '5px',
|
|
alignItems: 'center',
|
|
}}
|
|
>
|
|
{primaryName === row.name && (
|
|
<Tooltip
|
|
title={t('core:tooltips.primary_name', {
|
|
postProcess: 'capitalizeFirstChar',
|
|
})}
|
|
placement="left"
|
|
arrow
|
|
sx={{ fontSize: '24' }}
|
|
>
|
|
<PersonIcon color="success" />
|
|
</Tooltip>
|
|
)}
|
|
{row.name}
|
|
</Box>
|
|
</TableCell>
|
|
<TableCell>
|
|
<Box
|
|
sx={{
|
|
display: 'flex',
|
|
gap: '5px',
|
|
flexWrap: 'wrap',
|
|
}}
|
|
>
|
|
<Button
|
|
color={primaryName === row.name ? 'warning' : 'primary'}
|
|
variant="outlined"
|
|
size="small"
|
|
disabled={
|
|
(row?.name === primaryName && numberOfNames > 1) ||
|
|
isNameCurrentlyDoingATx
|
|
}
|
|
onClick={() => handleUpdate(row.name)}
|
|
>
|
|
{t('core:actions.update', {
|
|
postProcess: 'capitalizeFirstChar',
|
|
})}
|
|
</Button>
|
|
{!row.isSelling ? (
|
|
<Button
|
|
color={primaryName === row.name ? 'warning' : 'primary'}
|
|
size="small"
|
|
variant="outlined"
|
|
onClick={() => handleSell(row.name)}
|
|
disabled={
|
|
(row?.name === primaryName && numberOfNames > 1) ||
|
|
isNameCurrentlyDoingATx
|
|
}
|
|
>
|
|
{t('core:actions.sell', {
|
|
postProcess: 'capitalizeFirstChar',
|
|
})}
|
|
</Button>
|
|
) : (
|
|
<Button
|
|
color="error"
|
|
size="small"
|
|
onClick={() => handleCancel(row.name)}
|
|
disabled={isNameCurrentlyDoingATx}
|
|
>
|
|
{t('core:actions.cancel_sell', {
|
|
postProcess: 'capitalizeFirstChar',
|
|
})}
|
|
</Button>
|
|
)}
|
|
<ManageAvatar
|
|
name={row.name}
|
|
modalFunctionsAvatar={modalFunctionsAvatar}
|
|
isNameCurrentlyDoingATx={isNameCurrentlyDoingATx}
|
|
/>
|
|
</Box>
|
|
</TableCell>
|
|
</>
|
|
);
|
|
}
|
|
|
|
interface NameTableProps {
|
|
names: Names[];
|
|
primaryName: string;
|
|
}
|
|
export const NameTable = ({ names, primaryName }: NameTableProps) => {
|
|
const setNames = useSetAtom(namesAtom);
|
|
const { auth } = useGlobal();
|
|
const [namesForSale, setNamesForSale] = useAtom(forSaleAtom);
|
|
const [pendingTxs] = useAtom(pendingTxsAtom);
|
|
const { t } = useTranslation(['core']);
|
|
|
|
const modalFunctions = useModal<{ name: string }>();
|
|
const modalFunctionsUpdateName = useModal();
|
|
const modalFunctionsAvatar = useModal<{ name: string; hasAvatar: boolean }>();
|
|
const modalFunctionsSellName = useModal();
|
|
const { fetchPrimaryName } = useFetchNames();
|
|
|
|
const setPendingTxs = useSetAtom(pendingTxsAtom);
|
|
|
|
const namesToDisplay = useMemo(() => {
|
|
const namesForSaleString = namesForSale.map((item) => item.name);
|
|
return names.map((name) => {
|
|
return {
|
|
name: name.name,
|
|
isSelling: namesForSaleString.includes(name.name),
|
|
};
|
|
});
|
|
}, [names, namesForSale]);
|
|
|
|
return (
|
|
<Paper
|
|
sx={{
|
|
height: 'calc(100vh - 64px - 60px)', // Header + footer height
|
|
width: '100%',
|
|
}}
|
|
>
|
|
<TableVirtuoso
|
|
data={namesToDisplay}
|
|
components={VirtuosoTableComponents}
|
|
fixedHeaderContent={() => fixedHeaderContent(t)}
|
|
itemContent={(index, row) => {
|
|
const isNameCurrentlyDoingATx = isNamePendingTx(
|
|
row?.name,
|
|
pendingTxs
|
|
);
|
|
return rowContent(
|
|
index,
|
|
row,
|
|
primaryName,
|
|
auth?.address || '',
|
|
fetchPrimaryName,
|
|
names?.length,
|
|
modalFunctions,
|
|
modalFunctionsUpdateName,
|
|
modalFunctionsAvatar,
|
|
modalFunctionsSellName,
|
|
setPendingTxs,
|
|
setNames,
|
|
setNamesForSale,
|
|
isNameCurrentlyDoingATx,
|
|
t
|
|
);
|
|
}}
|
|
/>
|
|
{modalFunctions?.isShow && (
|
|
<Dialog
|
|
open={modalFunctions?.isShow}
|
|
aria-labelledby="alert-dialog-title"
|
|
aria-describedby="alert-dialog-description"
|
|
>
|
|
<DialogTitle id="alert-dialog-title">
|
|
{t('core:warnings.warning', {
|
|
postProcess: 'capitalizeFirstChar',
|
|
})}
|
|
</DialogTitle>
|
|
<DialogContent>
|
|
<DialogContentText id="alert-dialog-description">
|
|
{t('core:warnings.primary_name_sell_caution', {
|
|
postProcess: 'capitalizeFirstChar',
|
|
})}
|
|
</DialogContentText>
|
|
<Spacer height="20px" />
|
|
<DialogContentText id="alert-dialog-description2">
|
|
{t('core:warnings.primary_name_sell', {
|
|
name: modalFunctions?.data?.name,
|
|
})}
|
|
</DialogContentText>
|
|
</DialogContent>
|
|
<DialogActions>
|
|
<Button
|
|
color="warning"
|
|
variant="contained"
|
|
onClick={() => modalFunctions.onOk(undefined)}
|
|
autoFocus
|
|
>
|
|
{t('core:actions.continue', {
|
|
postProcess: 'capitalizeFirstChar',
|
|
})}
|
|
</Button>
|
|
<Button variant="contained" onClick={modalFunctions.onCancel}>
|
|
{t('core:actions.cancel', {
|
|
postProcess: 'capitalizeFirstChar',
|
|
})}
|
|
</Button>
|
|
</DialogActions>
|
|
</Dialog>
|
|
)}
|
|
{modalFunctionsUpdateName?.isShow && (
|
|
<UpdateNameModal modalFunctionsUpdateName={modalFunctionsUpdateName} />
|
|
)}
|
|
{modalFunctionsAvatar?.isShow && (
|
|
<AvatarModal modalFunctionsAvatar={modalFunctionsAvatar} />
|
|
)}
|
|
{modalFunctionsSellName?.isShow && (
|
|
<SellNameModal modalFunctionsSellName={modalFunctionsSellName} />
|
|
)}
|
|
</Paper>
|
|
);
|
|
};
|
|
|
|
interface PickedAvatar {
|
|
base64: string;
|
|
name: string;
|
|
}
|
|
|
|
interface AvatarModalProps {
|
|
modalFunctionsAvatar: ModalFunctionsAvatar;
|
|
}
|
|
const AvatarModal = ({ modalFunctionsAvatar }: AvatarModalProps) => {
|
|
const { t } = useTranslation();
|
|
const { setHasAvatar } = usePendingTxs();
|
|
const forceRefresh = useSetAtom(forceRefreshAtom);
|
|
|
|
const [pickedAvatar, setPickedAvatar] = useState<null | PickedAvatar>(null);
|
|
const [isLoadingPublish, setIsLoadingPublish] = useState(false);
|
|
|
|
const publishAvatar = async () => {
|
|
const loadId = showLoading(
|
|
t('core:avatar.responses.loading', {
|
|
postProcess: 'capitalizeFirstChar',
|
|
})
|
|
);
|
|
try {
|
|
if (!modalFunctionsAvatar?.data || !pickedAvatar?.base64)
|
|
throw new Error(
|
|
t('core:avatar.responses.error1', {
|
|
postProcess: 'capitalizeFirstChar',
|
|
})
|
|
);
|
|
setIsLoadingPublish(true);
|
|
await qortalRequest({
|
|
action: 'PUBLISH_QDN_RESOURCE',
|
|
base64: pickedAvatar?.base64,
|
|
service: 'THUMBNAIL',
|
|
identifier: 'qortal_avatar',
|
|
name: modalFunctionsAvatar?.data?.name,
|
|
});
|
|
setHasAvatar(modalFunctionsAvatar?.data?.name, true);
|
|
forceRefresh();
|
|
|
|
showSuccess(
|
|
t('core:avatar.responses.success', {
|
|
postProcess: 'capitalizeFirstChar',
|
|
})
|
|
);
|
|
modalFunctionsAvatar.onOk(undefined);
|
|
} catch (error) {
|
|
if (error instanceof Error) {
|
|
showError(error?.message);
|
|
return;
|
|
}
|
|
showError(
|
|
t('core:avatar.responses.loading', {
|
|
postProcess: 'capitalizeFirstChar',
|
|
})
|
|
);
|
|
} finally {
|
|
dismissToast(loadId);
|
|
setIsLoadingPublish(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<Dialog
|
|
open={modalFunctionsAvatar?.isShow}
|
|
aria-labelledby="alert-dialog-title"
|
|
aria-describedby="alert-dialog-description"
|
|
>
|
|
<DialogTitle id="alert-dialog-title">
|
|
{t('core:avatar.title', {
|
|
postProcess: 'capitalizeFirstChar',
|
|
})}
|
|
</DialogTitle>
|
|
<DialogContent
|
|
sx={{
|
|
width: '300px',
|
|
maxWidth: '95vw',
|
|
}}
|
|
>
|
|
<Spacer height="20px" />
|
|
<Box
|
|
sx={{
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
alignItems: 'center',
|
|
width: '100%',
|
|
}}
|
|
>
|
|
{modalFunctionsAvatar?.data?.hasAvatar && !pickedAvatar?.base64 && (
|
|
<Avatar
|
|
sx={{
|
|
height: '138px',
|
|
width: '138px',
|
|
}}
|
|
src={`/arbitrary/THUMBNAIL/${modalFunctionsAvatar.data.name}/qortal_avatar?async=true`}
|
|
alt={modalFunctionsAvatar.data.name}
|
|
>
|
|
<CircularProgress />
|
|
</Avatar>
|
|
)}
|
|
{pickedAvatar?.base64 && (
|
|
<Avatar
|
|
sx={{
|
|
height: '138px',
|
|
width: '138px',
|
|
}}
|
|
src={`data:image/webp;base64,${pickedAvatar?.base64}`}
|
|
alt={modalFunctionsAvatar?.data?.name}
|
|
>
|
|
<CircularProgress />
|
|
</Avatar>
|
|
)}
|
|
|
|
{pickedAvatar?.name && (
|
|
<>
|
|
<Spacer height="10px" />
|
|
<Typography variant="body2">{pickedAvatar?.name}</Typography>
|
|
</>
|
|
)}
|
|
|
|
<Spacer height="20px" />
|
|
<Typography
|
|
sx={{
|
|
fontSize: '12px',
|
|
}}
|
|
>
|
|
(500 KB max. for GIFS){' '}
|
|
</Typography>
|
|
<ImagePicker onPick={(file) => setPickedAvatar(file)} mode="single">
|
|
<Button variant="contained">
|
|
{t('core:actions.choose_image', {
|
|
postProcess: 'capitalizeFirstChar',
|
|
})}
|
|
</Button>
|
|
</ImagePicker>
|
|
</Box>
|
|
</DialogContent>
|
|
<DialogActions>
|
|
<Button
|
|
disabled={!pickedAvatar?.base64 || isLoadingPublish}
|
|
variant="contained"
|
|
onClick={publishAvatar}
|
|
autoFocus
|
|
>
|
|
{t('core:actions.publish', {
|
|
postProcess: 'capitalizeFirstChar',
|
|
})}
|
|
</Button>
|
|
<Button variant="contained" onClick={modalFunctionsAvatar.onCancel}>
|
|
{t('core:actions.cancel', {
|
|
postProcess: 'capitalizeFirstChar',
|
|
})}
|
|
</Button>
|
|
</DialogActions>
|
|
</Dialog>
|
|
);
|
|
};
|
|
|
|
interface UpdateNameModalProps {
|
|
modalFunctionsUpdateName: ReturnType<typeof useModal>;
|
|
}
|
|
|
|
const UpdateNameModal = ({
|
|
modalFunctionsUpdateName,
|
|
}: UpdateNameModalProps) => {
|
|
const [step, setStep] = useState(1);
|
|
const [newName, setNewName] = useState('');
|
|
const [isNameAvailable, setIsNameAvailable] = useState<Availability>(
|
|
Availability.NULL
|
|
);
|
|
const { t } = useTranslation();
|
|
const [nameFee, setNameFee] = useState<null | number>(null);
|
|
const balance = useGlobal().auth.balance;
|
|
|
|
const theme = useTheme();
|
|
|
|
const checkIfNameExisits = async (name: string) => {
|
|
if (!name?.trim()) {
|
|
setIsNameAvailable(Availability.NULL);
|
|
|
|
return;
|
|
}
|
|
setIsNameAvailable(Availability.LOADING);
|
|
try {
|
|
const res = await fetch(`/names/` + name);
|
|
const data = await res.json();
|
|
if (data?.message === 'name unknown') {
|
|
setIsNameAvailable(Availability.AVAILABLE);
|
|
} else {
|
|
setIsNameAvailable(Availability.NOT_AVAILABLE);
|
|
}
|
|
} catch (error) {
|
|
console.error(error);
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
const handler = setTimeout(() => {
|
|
checkIfNameExisits(newName);
|
|
}, 500);
|
|
|
|
// Cleanup timeout if searchValue changes before the timeout completes
|
|
return () => {
|
|
clearTimeout(handler);
|
|
};
|
|
}, [newName]);
|
|
|
|
useEffect(() => {
|
|
const nameRegistrationFee = async () => {
|
|
try {
|
|
const data = await fetch(`/transactions/unitfee?txType=REGISTER_NAME`);
|
|
const fee = await data.text();
|
|
|
|
setNameFee(+(Number(fee) / 1e8).toFixed(8));
|
|
} catch (error) {
|
|
console.error(error);
|
|
}
|
|
};
|
|
nameRegistrationFee();
|
|
}, []);
|
|
|
|
return (
|
|
<Dialog
|
|
open={modalFunctionsUpdateName?.isShow}
|
|
aria-labelledby="alert-dialog-title"
|
|
aria-describedby="alert-dialog-description"
|
|
>
|
|
{step === 1 && (
|
|
<>
|
|
<DialogTitle id="alert-dialog-title">
|
|
{t('core:warnings.warning', {
|
|
postProcess: 'capitalizeFirstChar',
|
|
})}
|
|
</DialogTitle>
|
|
<DialogContent>
|
|
<DialogContentText id="alert-dialog-description">
|
|
{t('core:update_name.title', {
|
|
postProcess: 'capitalizeFirstChar',
|
|
})}
|
|
</DialogContentText>
|
|
<Spacer height="20px" />
|
|
<DialogContentText id="alert-dialog-description2">
|
|
{t('core:warnings.update_name1', {
|
|
postProcess: 'capitalizeFirstChar',
|
|
})}
|
|
</DialogContentText>
|
|
</DialogContent>
|
|
<DialogActions>
|
|
<Button
|
|
color="warning"
|
|
variant="contained"
|
|
onClick={() => setStep(2)}
|
|
autoFocus
|
|
>
|
|
{t('core:actions.continue', {
|
|
postProcess: 'capitalizeFirstChar',
|
|
})}
|
|
</Button>
|
|
<Button
|
|
variant="contained"
|
|
onClick={modalFunctionsUpdateName.onCancel}
|
|
>
|
|
{t('core:actions.cancel', {
|
|
postProcess: 'capitalizeFirstChar',
|
|
})}
|
|
</Button>
|
|
</DialogActions>
|
|
</>
|
|
)}
|
|
{step === 2 && (
|
|
<>
|
|
<DialogTitle id="alert-dialog-title">
|
|
{t('core:warnings.warning', {
|
|
postProcess: 'capitalizeFirstChar',
|
|
})}
|
|
</DialogTitle>
|
|
<DialogContent>
|
|
<DialogContentText id="alert-dialog-description">
|
|
{t('core:update_name.choose_name', {
|
|
postProcess: 'capitalizeFirstChar',
|
|
})}
|
|
</DialogContentText>
|
|
<Spacer height="20px" />
|
|
<TextField
|
|
autoComplete="off"
|
|
autoFocus
|
|
onChange={(e) => setNewName(e.target.value)}
|
|
value={newName}
|
|
placeholder={t('core:new_name.choose_name', {
|
|
postProcess: 'capitalizeFirstChar',
|
|
})}
|
|
/>
|
|
{(!balance || (nameFee && balance && balance < nameFee)) && (
|
|
<>
|
|
<Spacer height="10px" />
|
|
<Box
|
|
sx={{
|
|
display: 'flex',
|
|
gap: '5px',
|
|
alignItems: 'center',
|
|
}}
|
|
>
|
|
<ErrorIcon
|
|
sx={{
|
|
color: theme.palette.text.primary,
|
|
}}
|
|
/>
|
|
<Typography>
|
|
{t('core:update_name.balanceInfo', {
|
|
postProcess: 'capitalizeFirstChar',
|
|
nameFee: nameFee,
|
|
balance: balance ?? 0,
|
|
})}
|
|
</Typography>
|
|
</Box>
|
|
<Spacer height="10px" />
|
|
</>
|
|
)}
|
|
<Spacer height="5px" />
|
|
{isNameAvailable === Availability.AVAILABLE && (
|
|
<Box
|
|
sx={{
|
|
display: 'flex',
|
|
gap: '5px',
|
|
alignItems: 'center',
|
|
}}
|
|
>
|
|
<CheckIcon
|
|
sx={{
|
|
color: theme.palette.text.primary,
|
|
}}
|
|
/>
|
|
<Typography>
|
|
{' '}
|
|
{t('core:update_name.name_available', {
|
|
name: newName,
|
|
})}
|
|
</Typography>
|
|
</Box>
|
|
)}
|
|
{isNameAvailable === Availability.NOT_AVAILABLE && (
|
|
<Box
|
|
sx={{
|
|
display: 'flex',
|
|
gap: '5px',
|
|
alignItems: 'center',
|
|
}}
|
|
>
|
|
<ErrorIcon
|
|
sx={{
|
|
color: theme.palette.text.primary,
|
|
}}
|
|
/>
|
|
<Typography>
|
|
{t('core:update_name.name_unavailable', {
|
|
name: newName,
|
|
})}
|
|
</Typography>
|
|
</Box>
|
|
)}
|
|
{isNameAvailable === Availability.LOADING && (
|
|
<Box
|
|
sx={{
|
|
display: 'flex',
|
|
gap: '5px',
|
|
alignItems: 'center',
|
|
}}
|
|
>
|
|
<BarSpinner width="16px" color={theme.palette.text.primary} />
|
|
<Typography>
|
|
{t('core:new_name.checking', {
|
|
postProcess: 'capitalizeFirstChar',
|
|
})}
|
|
</Typography>
|
|
</Box>
|
|
)}
|
|
</DialogContent>
|
|
<DialogActions>
|
|
<Button
|
|
color="primary"
|
|
variant="contained"
|
|
disabled={Boolean(
|
|
!newName?.trim() ||
|
|
isNameAvailable !== Availability.AVAILABLE ||
|
|
!balance ||
|
|
(balance && nameFee && +balance < +nameFee)
|
|
)}
|
|
onClick={() => modalFunctionsUpdateName.onOk(newName.trim())}
|
|
autoFocus
|
|
>
|
|
{t('core:actions.continue', {
|
|
postProcess: 'capitalizeFirstChar',
|
|
})}
|
|
</Button>
|
|
<Button
|
|
color="secondary"
|
|
variant="contained"
|
|
onClick={modalFunctionsUpdateName.onCancel}
|
|
>
|
|
{t('core:actions.cancel', {
|
|
postProcess: 'capitalizeFirstChar',
|
|
})}
|
|
</Button>
|
|
</DialogActions>
|
|
</>
|
|
)}
|
|
</Dialog>
|
|
);
|
|
};
|
|
|
|
interface SellNameModalProps {
|
|
modalFunctionsSellName: ModalFunctionsSellName;
|
|
}
|
|
|
|
const SellNameModal = ({ modalFunctionsSellName }: SellNameModalProps) => {
|
|
const { t } = useTranslation();
|
|
const [price, setPrice] = useState(0);
|
|
|
|
return (
|
|
<Dialog
|
|
open={modalFunctionsSellName?.isShow}
|
|
aria-labelledby="alert-dialog-title"
|
|
aria-describedby="alert-dialog-description"
|
|
>
|
|
<DialogTitle id="alert-dialog-title">
|
|
{t('core:sell_name.title', {
|
|
postProcess: 'capitalizeFirstChar',
|
|
})}
|
|
</DialogTitle>
|
|
<DialogContent>
|
|
<DialogContentText id="alert-dialog-description">
|
|
{t('core:sell_name.choose_price', {
|
|
postProcess: 'capitalizeFirstChar',
|
|
})}
|
|
</DialogContentText>
|
|
<Spacer height="20px" />
|
|
<TextField
|
|
autoComplete="off"
|
|
autoFocus
|
|
onChange={(e) => setPrice(+e.target.value)}
|
|
value={price}
|
|
type="number"
|
|
placeholder={t('core:new_name.choose_price', {
|
|
postProcess: 'capitalizeFirstChar',
|
|
})}
|
|
/>
|
|
</DialogContent>
|
|
<DialogActions>
|
|
<Button
|
|
color="primary"
|
|
variant="contained"
|
|
disabled={!price}
|
|
onClick={() => modalFunctionsSellName.onOk(price)}
|
|
autoFocus
|
|
>
|
|
{t('core:actions.continue', {
|
|
postProcess: 'capitalizeFirstChar',
|
|
})}
|
|
</Button>
|
|
<Button
|
|
color="secondary"
|
|
variant="contained"
|
|
onClick={modalFunctionsSellName.onCancel}
|
|
>
|
|
{t('core:actions.cancel', {
|
|
postProcess: 'capitalizeFirstChar',
|
|
})}
|
|
</Button>
|
|
</DialogActions>
|
|
</Dialog>
|
|
);
|
|
};
|