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 = { Scroller: forwardRef((props, ref) => ( )), Table: (props) => ( ), TableHead: forwardRef((props, ref) => ( )), TableRow, TableBody: forwardRef((props, ref) => ( )), }; function fixedHeaderContent(t: TFunction) { return ( {t('core:tables.name', { postProcess: 'capitalizeFirstChar', })} {t('core:tables.actions', { postProcess: 'capitalizeFirstChar', })} ); } 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(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 ( ); }; type SetPendingTxs = (update: SetStateAction) => void; type SetNames = (update: SetStateAction) => void; type SetNamesForSale = (update: SetStateAction) => void; function rowContent( _index: number, row: NameData, primaryName: string, address: string, fetchPrimaryName: FetchPrimaryNameType, numberOfNames: number, modalFunctions: ModalFunctions, modalFunctionsUpdateName: ReturnType, modalFunctionsAvatar: ModalFunctionsAvatar, modalFunctionsSellName: ReturnType, 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 ( <> {primaryName === row.name && ( )} {row.name} {!row.isSelling ? ( ) : ( )} ); } 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 ( 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 && ( {t('core:warnings.warning', { postProcess: 'capitalizeFirstChar', })} {t('core:warnings.primary_name_sell_caution', { postProcess: 'capitalizeFirstChar', })} {t('core:warnings.primary_name_sell', { name: modalFunctions?.data?.name, })} )} {modalFunctionsUpdateName?.isShow && ( )} {modalFunctionsAvatar?.isShow && ( )} {modalFunctionsSellName?.isShow && ( )} ); }; 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); 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 ( {t('core:avatar.title', { postProcess: 'capitalizeFirstChar', })} {modalFunctionsAvatar?.data?.hasAvatar && !pickedAvatar?.base64 && ( )} {pickedAvatar?.base64 && ( )} {pickedAvatar?.name && ( <> {pickedAvatar?.name} )} (500 KB max. for GIFS){' '} setPickedAvatar(file)} mode="single"> ); }; interface UpdateNameModalProps { modalFunctionsUpdateName: ReturnType; } const UpdateNameModal = ({ modalFunctionsUpdateName, }: UpdateNameModalProps) => { const [step, setStep] = useState(1); const [newName, setNewName] = useState(''); const [isNameAvailable, setIsNameAvailable] = useState( Availability.NULL ); const { t } = useTranslation(); const [nameFee, setNameFee] = useState(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 ( {step === 1 && ( <> {t('core:warnings.warning', { postProcess: 'capitalizeFirstChar', })} {t('core:update_name.title', { postProcess: 'capitalizeFirstChar', })} {t('core:warnings.update_name1', { postProcess: 'capitalizeFirstChar', })} )} {step === 2 && ( <> {t('core:warnings.warning', { postProcess: 'capitalizeFirstChar', })} {t('core:update_name.choose_name', { postProcess: 'capitalizeFirstChar', })} setNewName(e.target.value)} value={newName} placeholder={t('core:new_name.choose_name', { postProcess: 'capitalizeFirstChar', })} /> {(!balance || (nameFee && balance && balance < nameFee)) && ( <> {t('core:update_name.balanceInfo', { postProcess: 'capitalizeFirstChar', nameFee: nameFee, balance: balance ?? 0, })} )} {isNameAvailable === Availability.AVAILABLE && ( {' '} {t('core:update_name.name_available', { name: newName, })} )} {isNameAvailable === Availability.NOT_AVAILABLE && ( {t('core:update_name.name_unavailable', { name: newName, })} )} {isNameAvailable === Availability.LOADING && ( {t('core:new_name.checking', { postProcess: 'capitalizeFirstChar', })} )} )} ); }; interface SellNameModalProps { modalFunctionsSellName: ModalFunctionsSellName; } const SellNameModal = ({ modalFunctionsSellName }: SellNameModalProps) => { const { t } = useTranslation(); const [price, setPrice] = useState(0); return ( {t('core:sell_name.title', { postProcess: 'capitalizeFirstChar', })} {t('core:sell_name.choose_price', { postProcess: 'capitalizeFirstChar', })} setPrice(+e.target.value)} value={price} type="number" placeholder={t('core:new_name.choose_price', { postProcess: 'capitalizeFirstChar', })} /> ); };