diff --git a/connect-qapp.sh b/connect-qapp.sh
new file mode 100755
index 0000000..e4f50ff
--- /dev/null
+++ b/connect-qapp.sh
@@ -0,0 +1,20 @@
+#!/bin/bash
+
+# CONFIGURATION
+REMOTE_USER=phil
+REMOTE_HOST=devnet-nodes.qortal.link
+QAPP_PORT=22393
+CORE_API_PORT=22391
+EDITOR_PORT=5174
+
+# Start reverse tunnel in background
+echo "Starting SSH tunnel to $REMOTE_HOST..."
+
+ssh -p22221 -o ServerAliveInterval=30 \
+ -L $QAPP_PORT:127.0.0.1:$QAPP_PORT \
+ -L $CORE_API_PORT:127.0.0.1:$CORE_API_PORT \
+ -R $EDITOR_PORT:127.0.0.1:$EDITOR_PORT \
+ $REMOTE_USER@$REMOTE_HOST
+
+
+
diff --git a/src/AppWrapper.tsx b/src/AppWrapper.tsx
index ec551cd..d9d1649 100644
--- a/src/AppWrapper.tsx
+++ b/src/AppWrapper.tsx
@@ -1,6 +1,7 @@
-import { Routes } from "./Routes";
-import { GlobalProvider } from "qapp-core";
-import { publicSalt } from "./qapp-config.ts";
+import { Routes } from './Routes';
+import { GlobalProvider } from 'qapp-core';
+import { publicSalt } from './qapp-config.ts';
+import { PendingTxsProvider } from './state/contexts/PendingTxsProvider.tsx';
export const AppWrapper = () => {
return (
@@ -14,10 +15,12 @@ export const AppWrapper = () => {
authenticateOnMount: true,
},
publicSalt: publicSalt,
- appName: 'names'
+ appName: 'names',
}}
>
-
+
+
+
);
};
diff --git a/src/components/RegisterName.tsx b/src/components/RegisterName.tsx
index 71b01ba..f3d9250 100644
--- a/src/components/RegisterName.tsx
+++ b/src/components/RegisterName.tsx
@@ -5,10 +5,6 @@ import {
DialogActions,
DialogContent,
DialogTitle,
- List,
- ListItem,
- ListItemIcon,
- ListItemText,
styled,
TextField,
Typography,
@@ -23,10 +19,11 @@ import {
useGlobal,
} from 'qapp-core';
import { useEffect, useState } from 'react';
-import RadioButtonCheckedIcon from '@mui/icons-material/RadioButtonChecked';
import { BarSpinner } from '../common/Spinners/BarSpinner/BarSpinner';
import CheckIcon from '@mui/icons-material/Check';
import ErrorIcon from '@mui/icons-material/Error';
+import { useSetAtom } from 'jotai';
+import { namesAtom, pendingTxsAtom } from '../state/global/names';
export enum Availability {
NULL = 'null',
LOADING = 'loading',
@@ -45,21 +42,48 @@ const Label = styled('label')`
const RegisterName = () => {
const [isOpen, setIsOpen] = useState(false);
const balance = useGlobal().auth.balance;
+ const setNames = useSetAtom(namesAtom);
+
+ const address = useGlobal().auth.address;
const [nameValue, setNameValue] = useState('');
const [isNameAvailable, setIsNameAvailable] = useState(
Availability.NULL
);
+ const setPendingTxs = useSetAtom(pendingTxsAtom);
+
const [isLoadingRegisterName, setIsLoadingRegisterName] = useState(false);
const theme = useTheme();
const [nameFee, setNameFee] = useState(null);
const registerNameFunc = async () => {
+ if (!address) return;
const loadId = showLoading('Registering name...please wait');
try {
setIsLoadingRegisterName(true);
- await qortalRequest({
+ const res = await qortalRequest({
action: 'REGISTER_NAME',
name: nameValue,
});
+ setPendingTxs((prev) => {
+ return {
+ ...prev, // preserve existing categories
+ ['REGISTER_NAME']: {
+ ...(prev['REGISTER_NAME'] || {}), // preserve existing transactions in this category
+ [res.signature]: {
+ ...res,
+ status: 'PENDING',
+ callback: () => {
+ setNames((prev) => [
+ ...prev,
+ {
+ name: res.name,
+ owner: res.creatorAddress,
+ },
+ ]);
+ },
+ }, // add or overwrite this transaction
+ },
+ };
+ });
showSuccess('Successfully registered a name');
setNameValue('');
setIsOpen(false);
diff --git a/src/components/Tables/ForSaleTable.tsx b/src/components/Tables/ForSaleTable.tsx
index 9acf2e2..b760028 100644
--- a/src/components/Tables/ForSaleTable.tsx
+++ b/src/components/Tables/ForSaleTable.tsx
@@ -8,12 +8,23 @@ import {
Paper,
Button,
} from '@mui/material';
-import { useAtom } from 'jotai';
+import { useAtom, useSetAtom } from 'jotai';
import { forwardRef, useMemo } from 'react';
import { TableVirtuoso, TableComponents } from 'react-virtuoso';
-import { forSaleAtom, namesAtom } from '../../state/global/names';
+import {
+ forSaleAtom,
+ namesAtom,
+ pendingTxsAtom,
+} from '../../state/global/names';
import ArrowUpwardIcon from '@mui/icons-material/ArrowUpward';
import ArrowDownwardIcon from '@mui/icons-material/ArrowDownward';
+import {
+ dismissToast,
+ showError,
+ showLoading,
+ showSuccess,
+ useGlobal,
+} from 'qapp-core';
interface NameData {
name: string;
@@ -78,21 +89,53 @@ function fixedHeaderContent(
);
}
-function rowContent(_index: number, row: NameData) {
- const handleUpdate = () => {
- console.log('Update:', row.name);
- // Your logic here
- };
-
+function rowContent(
+ _index: number,
+ row: NameData,
+ setPendingTxs,
+ setNames,
+ setNamesForSale,
+ address
+) {
const handleBuy = async (name: string) => {
+ const loadId = showLoading('Attempting to purchase name...please wait');
+
try {
- console.log('hello');
- await qortalRequest({
+ const res = await qortalRequest({
action: 'BUY_NAME',
nameForSale: name,
});
+ showSuccess('Purchased name');
+ setPendingTxs((prev) => {
+ return {
+ ...prev, // preserve existing categories
+ ['BUY_NAME']: {
+ ...(prev['BUY_NAME'] || {}), // preserve existing transactions in this category
+ [res.signature]: {
+ ...res,
+ status: 'PENDING',
+ callback: () => {
+ setNamesForSale((prev) =>
+ prev.filter((item) => item?.name !== res.name)
+ );
+ setNames((prev) => [
+ ...prev,
+ {
+ name: res.name,
+ owner: res.creatorAddress,
+ },
+ ]);
+ },
+ }, // add or overwrite this transaction
+ },
+ };
+ });
} catch (error) {
+ showError(error?.message || 'Unable to purchase name');
+
console.log('error', error);
+ } finally {
+ dismissToast(loadId);
}
};
@@ -119,6 +162,11 @@ export const ForSaleTable = ({
sortBy,
handleSort,
}) => {
+ const address = useGlobal().auth.address;
+ const setNames = useSetAtom(namesAtom);
+ const setNamesForSale = useSetAtom(forSaleAtom);
+ const setPendingTxs = useSetAtom(pendingTxsAtom);
+
return (
fixedHeaderContent(sortBy, sortDirection, handleSort)
}
- itemContent={rowContent}
+ itemContent={(index, row) =>
+ rowContent(
+ index,
+ row,
+ setPendingTxs,
+ setNames,
+ setNamesForSale,
+ address
+ )
+ }
/>
);
diff --git a/src/components/Tables/NameTable.tsx b/src/components/Tables/NameTable.tsx
index f81ab2d..9aedde4 100644
--- a/src/components/Tables/NameTable.tsx
+++ b/src/components/Tables/NameTable.tsx
@@ -20,10 +20,17 @@ import {
CircularProgress,
Avatar,
} from '@mui/material';
-import { useAtom } from 'jotai';
+import { useAtom, useAtomValue, useSetAtom } from 'jotai';
import { forwardRef, useCallback, useEffect, useMemo, useState } from 'react';
import { TableVirtuoso, TableComponents } from 'react-virtuoso';
-import { forSaleAtom, namesAtom } from '../../state/global/names';
+import {
+ forceRefreshAtom,
+ forSaleAtom,
+ namesAtom,
+ pendingTxsAtom,
+ refreshAtom,
+ sortedPendingTxsByCategoryAtom,
+} from '../../state/global/names';
import PersonIcon from '@mui/icons-material/Person';
import { useModal } from '../../hooks/useModal';
import {
@@ -40,6 +47,7 @@ import { Availability } from '../RegisterName';
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';
interface NameData {
name: string;
isSelling?: boolean;
@@ -80,10 +88,17 @@ function fixedHeaderContent() {
}
const ManageAvatar = ({ name, modalFunctionsAvatar }) => {
- const [hasAvatar, setHasAvatar] = useState(null);
+ const { setHasAvatar, getHasAvatar } = usePendingTxs();
+ const [refresh] = useAtom(refreshAtom); // just to subscribe
+ const [hasAvatarState, setHasAvatarState] = useState(null);
const checkIfAvatarExists = useCallback(async (name) => {
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(() =>
@@ -97,9 +112,10 @@ const ManageAvatar = ({ name, modalFunctionsAvatar }) => {
const responseData = await response.json();
if (responseData?.length > 0) {
- setHasAvatar(true);
+ setHasAvatarState(true);
+ setHasAvatar(name, true);
} else {
- setHasAvatar(false);
+ setHasAvatarState(false);
}
} catch (error) {
console.log(error);
@@ -108,17 +124,19 @@ const ManageAvatar = ({ name, modalFunctionsAvatar }) => {
useEffect(() => {
if (!name) return;
checkIfAvatarExists(name);
- }, [name, checkIfAvatarExists]);
+ }, [name, checkIfAvatarExists, refresh]);
return (