forked from Qortal/Q-Nodecontrol
Add follow and block lists from Q-Follow #1
+1
-1
@@ -6,7 +6,7 @@ yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
*.zip
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" type="image/png" href="/qnc.png" />
|
||||
<meta name="viewport" content="initial-scale=1, width=device-width" />
|
||||
<title>Qortal Nodecontrol</title>
|
||||
<title>Q-Node & Q-Follow</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
+145
-33
@@ -1,7 +1,6 @@
|
||||
import * as React from 'react';
|
||||
import "@fontsource/lato";
|
||||
import { CssBaseline } from '@mui/material';
|
||||
import { ThemeProvider, createTheme } from '@mui/material/styles';
|
||||
import { ThemeProvider, createTheme, useTheme } from '@mui/material/styles';
|
||||
import { styled } from "@mui/system";
|
||||
import {
|
||||
Alert,
|
||||
@@ -11,6 +10,7 @@ import {
|
||||
Button,
|
||||
CircularProgress,
|
||||
Container,
|
||||
CssBaseline,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
@@ -19,6 +19,8 @@ import {
|
||||
Divider,
|
||||
Grid,
|
||||
IconButton,
|
||||
Menu,
|
||||
MenuItem,
|
||||
Paper,
|
||||
Table,
|
||||
TableBody,
|
||||
@@ -44,7 +46,9 @@ import {
|
||||
RemoveCircleOutline,
|
||||
RestartAlt,
|
||||
Storage,
|
||||
SyncLock
|
||||
SyncLock,
|
||||
ExpandLess,
|
||||
ExpandMore
|
||||
} from '@mui/icons-material';
|
||||
import Snackbar, { SnackbarCloseReason } from '@mui/material/Snackbar';
|
||||
import Slide, { SlideProps } from '@mui/material/Slide';
|
||||
@@ -61,9 +65,9 @@ import appLogo from './assets/q-nodecontrol.png';
|
||||
import noAvatar from "./assets/noavatar.png";
|
||||
import NagistralBold from './fonts/Magistral-Bold.woff2';
|
||||
import NodeWidget from './components/NodeWidget';
|
||||
import FollowBlockPanel, { FollowBlockPanelHandles } from './FollowBlockPanel';
|
||||
import { useIframe } from './main';
|
||||
import { DefaultTheme } from '@mui/private-theming';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
|
||||
function secondsToDhms(seconds: number) {
|
||||
seconds = Number(seconds);
|
||||
@@ -209,8 +213,48 @@ function App() {
|
||||
const [mintingAccountKey, setMintingAccountKey] = React.useState('');
|
||||
const [openPeerDialog, setOpenPeerDialog] = React.useState(false);
|
||||
const [newPeerAddress, setNewPeerAddress] = React.useState('');
|
||||
|
||||
const [openMinting, setOpenMinting] = React.useState(true);
|
||||
const [openPeers, setOpenPeers] = React.useState(true);
|
||||
const [openFollowBlock, setOpenFollowBlock] = React.useState(true);
|
||||
const followBlockRef = React.useRef<FollowBlockPanelHandles>(null);
|
||||
const [userAddr, setUserAddr] = React.useState<string | null>(null);
|
||||
const [userNames, setUserNames] = React.useState<string[]>([]);
|
||||
const [authAnchorEl, setAuthAnchorEl] = React.useState<null | HTMLElement>(null);
|
||||
const isAuthed = Boolean(userAddr);
|
||||
const emptyRows = page > 0 ? Math.max(0, (1 + page) * rowsPerPage - connectedPeers.length) : 0;
|
||||
const handleAuthClose = () => setAuthAnchorEl(null);
|
||||
const handleReloadAndToggle = () => {
|
||||
followBlockRef.current?.reload?.();
|
||||
if (!openFollowBlock) {
|
||||
setOpenFollowBlock(!openFollowBlock);
|
||||
};
|
||||
};
|
||||
|
||||
const handleAuthClick = async (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
if (!isAuthed) {
|
||||
await authenticate();
|
||||
} else {
|
||||
setAuthAnchorEl(event.currentTarget);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
async function authenticate() {
|
||||
try {
|
||||
const account = await qortalRequest({ action: "GET_USER_ACCOUNT" });
|
||||
setUserAddr(account?.address ?? null);
|
||||
if (account?.address) {
|
||||
const nameRes = await qortalRequest({
|
||||
action: "GET_ACCOUNT_NAMES",
|
||||
address: account.address,
|
||||
limit: 50, offset: 0, reverse: true,
|
||||
});
|
||||
setUserNames((nameRes || []).map((n: any) => n.name));
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
let newTheme: Partial<DefaultTheme>;
|
||||
|
||||
@@ -696,16 +740,16 @@ function App() {
|
||||
|
||||
const mintingAccountsHeader = () => {
|
||||
return (
|
||||
<div style={{
|
||||
width: "100%",
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between'
|
||||
}}>
|
||||
<Typography variant="h6">
|
||||
Minting Account(s)
|
||||
</Typography>
|
||||
<Box
|
||||
sx={{ width: '100%', display: 'flex', alignItems: 'center', justifyContent: 'space-between',
|
||||
cursor: 'pointer' }}
|
||||
>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: .5 }}
|
||||
onClick={() => setOpenMinting(!openMinting)}
|
||||
>
|
||||
<Typography variant="h6">Minting Account(s)</Typography>
|
||||
{openMinting ? <ExpandLess/> : <ExpandMore/>}
|
||||
</Box>
|
||||
<Button
|
||||
disabled={isUsingGateway}
|
||||
size="small"
|
||||
@@ -716,22 +760,22 @@ function App() {
|
||||
>
|
||||
Add Minting Account
|
||||
</Button>
|
||||
</div>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
const connectedPeersHeader = () => {
|
||||
return (
|
||||
<div style={{
|
||||
width: "100%",
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between'
|
||||
}}>
|
||||
<Typography variant="h6">
|
||||
Peers connected to this Node
|
||||
</Typography>
|
||||
<Box
|
||||
sx={{ width: '100%', display: 'flex', alignItems: 'center', justifyContent: 'space-between',
|
||||
cursor: 'pointer' }}
|
||||
>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: .5 }}
|
||||
onClick={() => setOpenPeers(!openPeers)}
|
||||
>
|
||||
<Typography variant="h6">Connected Peers</Typography>
|
||||
{openPeers ? <ExpandLess/> : <ExpandMore/>}
|
||||
</Box>
|
||||
<Button
|
||||
disabled={isUsingGateway}
|
||||
size="small"
|
||||
@@ -742,7 +786,7 @@ function App() {
|
||||
>
|
||||
Add new peer
|
||||
</Button>
|
||||
</div>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1073,7 +1117,7 @@ function App() {
|
||||
textDecoration: 'none',
|
||||
}}
|
||||
>
|
||||
<span style={{ color: '#05a2e4' }}>Qortal </span>Nodecontrol
|
||||
<span style={{ color: '#05a2e4' }}>Q-</span>Node & <span style={{ color: '#05a2e4' }}>Q-</span>Follow
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="h6"
|
||||
@@ -1162,15 +1206,83 @@ function App() {
|
||||
{mintingAccountsHeader()}
|
||||
</Box>
|
||||
<Divider sx={{ marginTop: '5px' }} />
|
||||
<Box maxWidth="xl" marginTop={2}>
|
||||
{loadingMintingAccountsTable ? tableLoaderMintingAccounts() : tableMintingAccounts()}
|
||||
</Box>
|
||||
{openMinting && (
|
||||
<Box maxWidth="xl" marginTop={2}>
|
||||
{loadingMintingAccountsTable
|
||||
? tableLoaderMintingAccounts()
|
||||
: tableMintingAccounts()}
|
||||
</Box>
|
||||
)}
|
||||
<Box maxWidth="xl" marginTop={4}>
|
||||
{connectedPeersHeader()}
|
||||
</Box>
|
||||
<Divider sx={{ marginTop: '5px' }} />
|
||||
<Box maxWidth="xl" marginTop={2}>
|
||||
{tableConnectedPeers()}
|
||||
{openPeers && (
|
||||
<Box maxWidth="xl" marginTop={2}>
|
||||
{tableConnectedPeers()}
|
||||
</Box>
|
||||
)}
|
||||
<Box maxWidth="xl" marginTop={4}>
|
||||
<Box
|
||||
sx={{ width: "100%", display: "flex", alignItems: "center",
|
||||
justifyContent: "space-between", cursor: "pointer" }}
|
||||
>
|
||||
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}
|
||||
onClick={() => setOpenFollowBlock(!openFollowBlock)}
|
||||
>
|
||||
<Typography
|
||||
variant="h6"
|
||||
>Follow / Block Lists</Typography>
|
||||
{openFollowBlock ?
|
||||
<ExpandLess /> :
|
||||
<ExpandMore />}
|
||||
<Tooltip title="Reload lists">
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={(e) => { e.stopPropagation(); handleReloadAndToggle(); }}>
|
||||
<RestartAlt fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
<Box onClick={(e) => e.stopPropagation()}>
|
||||
<Button
|
||||
size="small"
|
||||
variant="outlined"
|
||||
onClick={handleAuthClick}
|
||||
endIcon={isAuthed ? <ExpandMore fontSize="small" /> : undefined}
|
||||
sx={{ borderRadius: 50 }}
|
||||
>
|
||||
{isAuthed ? `${userNames.length} name${userNames.length === 1 ? "" : "s"}` : "Authenticate"}
|
||||
</Button>
|
||||
<Menu
|
||||
anchorEl={authAnchorEl}
|
||||
open={Boolean(authAnchorEl)}
|
||||
onClose={handleAuthClose}
|
||||
>
|
||||
{userNames.map((nm) => (
|
||||
<MenuItem
|
||||
key={nm}
|
||||
disableRipple
|
||||
sx={{
|
||||
"&:hover": { backgroundColor: "transparent" },
|
||||
"&.Mui-focusVisible": { backgroundColor: "transparent" },
|
||||
"&.Mui-selected": { backgroundColor: "transparent" }
|
||||
}}
|
||||
>
|
||||
{nm}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Menu>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
<Divider sx={{ marginTop: '5px' }} />
|
||||
<Box
|
||||
maxWidth="xl"
|
||||
marginTop={2}
|
||||
sx={{ display: openFollowBlock ? "block" : "none" }}
|
||||
>
|
||||
<FollowBlockPanel ref={followBlockRef} userAddr={userAddr} userNames={userNames}/>
|
||||
</Box>
|
||||
</Container>
|
||||
</ThemeProvider>
|
||||
|
||||
@@ -0,0 +1,359 @@
|
||||
import React, { forwardRef, useImperativeHandle, useMemo } from "react";
|
||||
import {
|
||||
Alert, Box, Paper, Typography, Table, TableHead, TableRow, TableCell,
|
||||
TableBody, Button, CircularProgress, TableContainer, TextField,
|
||||
Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle
|
||||
} from "@mui/material";
|
||||
import { tableCellClasses } from '@mui/material/TableCell';
|
||||
import { styled } from "@mui/system";
|
||||
import { RemoveCircleOutline, AddBoxOutlined } from "@mui/icons-material";
|
||||
|
||||
type ListName = "followedNames" | "blockedNames" | "blockedAddresses";
|
||||
|
||||
/** expose a `reload()` method to the parent via ref */
|
||||
export interface FollowBlockPanelHandles { reload: () => void; }
|
||||
|
||||
interface FollowBlockPanelProps {
|
||||
onExpand?: () => void;
|
||||
userAddr?: string | null;
|
||||
userNames?: string[];
|
||||
}
|
||||
|
||||
|
||||
|
||||
export const StyledTableCell = styled(TableCell)(({ theme }) => ({
|
||||
[`&.${tableCellClasses.head}`]: {
|
||||
backgroundColor: "#02648d",
|
||||
color: theme.palette.common.white,
|
||||
fontSize: 14,
|
||||
},
|
||||
[`&.${tableCellClasses.body}`]: { fontSize: 13 },
|
||||
}));
|
||||
|
||||
export const StyledTableRow = styled(TableRow)(({ theme }) => ({
|
||||
"&:nth-of-type(odd)": { backgroundColor: theme.palette.action.hover },
|
||||
"&:last-child td, &:last-child th": { border: 0 },
|
||||
}));
|
||||
|
||||
export const DialogGeneral = styled(Dialog)(({ theme }) => ({
|
||||
"& .MuiDialogContent-root": { padding: theme.spacing(3) },
|
||||
"& .MuiDialogActions-root": { padding: theme.spacing(1) },
|
||||
"& .MuiDialog-paper": { borderRadius: 15 },
|
||||
"& .MuiTextField-root": { width: "50ch" },
|
||||
}));
|
||||
|
||||
const FollowBlockPanel = forwardRef<FollowBlockPanelHandles, FollowBlockPanelProps>((props, ref) => {
|
||||
const [followed, setFollowed] = React.useState<string[]>([]);
|
||||
const [blockedNames, setBlockedNames] = React.useState<string[]>([]);
|
||||
const [blockedAddresses, setBlockedAddrs] = React.useState<string[]>([]);
|
||||
const [loading, setLoading] = React.useState(false);
|
||||
const [listsLoaded, setListsLoaded] = React.useState(false);
|
||||
const { userAddr = null, userNames = [] } = props;
|
||||
const [notices, setNotices] = React.useState<string[]>([]);
|
||||
const [openDialog, setOpenDialog] = React.useState<Record<ListName, boolean>>({
|
||||
followedNames: false,
|
||||
blockedNames: false,
|
||||
blockedAddresses: false,
|
||||
});
|
||||
const [inputVals, setInputVals] = React.useState<Record<ListName, string>>({
|
||||
followedNames: "",
|
||||
blockedNames: "",
|
||||
blockedAddresses: "",
|
||||
});
|
||||
|
||||
const loadList = async (list: ListName) => {
|
||||
const res = await qortalRequest({
|
||||
action: "GET_LIST_ITEMS",
|
||||
list_name: list,
|
||||
});
|
||||
const sorted = (res || []).sort();
|
||||
if (list === "followedNames") setFollowed(sorted);
|
||||
if (list === "blockedNames") setBlockedNames(sorted);
|
||||
if (list === "blockedAddresses") setBlockedAddrs(sorted);
|
||||
};
|
||||
|
||||
const loadLists = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
await loadList("followedNames");
|
||||
await loadList("blockedNames");
|
||||
await loadList("blockedAddresses");
|
||||
setListsLoaded(true);
|
||||
props.onExpand?.();
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const [followOwnerMap, setFollowOwnerMap] =
|
||||
React.useState<Record<string, string>>({});
|
||||
|
||||
React.useEffect(() => {
|
||||
setNotices(calcNotices(
|
||||
userAddr, userNames, blockedNames, blockedAddresses, followed, followOwnerMap,
|
||||
));
|
||||
}, [userAddr, userNames, blockedNames, blockedAddresses, followed, followOwnerMap]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!followed.length) { setFollowOwnerMap({}); return; }
|
||||
(async () => {
|
||||
const m: Record<string, string> = {};
|
||||
await Promise.all(
|
||||
followed.map(async (n) => {
|
||||
const d = await qortalRequest({ action: "GET_NAME_DATA", name: n });
|
||||
if (d?.owner) m[n] = d.owner;
|
||||
})
|
||||
);
|
||||
setFollowOwnerMap(m);
|
||||
})();
|
||||
}, [followed]);
|
||||
|
||||
const calcNotices = (
|
||||
addr: string | null,
|
||||
names: string[],
|
||||
blockedN: string[],
|
||||
blockedA: string[],
|
||||
followedN: string[],
|
||||
followOwner: Record<string,string> = {},
|
||||
) => {
|
||||
const msgs: string[] = [];
|
||||
if (addr && blockedA.includes(addr))
|
||||
msgs.push("⚠️ Your address is blocked on this node.");
|
||||
if (names.some((n) => blockedN.includes(n)))
|
||||
msgs.push("⚠️ One of your names is blocked on this node.");
|
||||
const dupe = followedN.filter((n) => blockedN.includes(n));
|
||||
if (dupe.length)
|
||||
msgs.push(`⚠️ Conflict: ${dupe.join(", ")} in both Follow & Block lists.`);
|
||||
const ownedConflict = Object.entries(followOwner)
|
||||
.filter(([, owner]) => blockedA.includes(owner))
|
||||
.map(([name]) => name);
|
||||
if (ownedConflict.length)
|
||||
msgs.push(`⚠️ Conflict: ${ownedConflict.join(", ")} owned by a blocked address.`);
|
||||
return msgs;
|
||||
};
|
||||
|
||||
useImperativeHandle(ref, () => ({ reload: loadLists }), []);
|
||||
|
||||
const remove = async (list: ListName, value: string) => {
|
||||
if (list === "blockedNames") {
|
||||
const d = await qortalRequest({ action: "GET_NAME_DATA", name: value });
|
||||
const owner = d?.owner;
|
||||
if (owner && blockedAddresses.includes(owner)) {
|
||||
if (window.confirm(`Also remove owner address ${owner} from Blocked Addresses?`)) {
|
||||
await qortalRequest({
|
||||
action: "DELETE_LIST_ITEM",
|
||||
list_name: "blockedAddresses",
|
||||
item: owner,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (list === "blockedAddresses") {
|
||||
const nameRes = await qortalRequest({
|
||||
action: "GET_ACCOUNT_NAMES",
|
||||
address: value, limit: 50, offset: 0, reverse: true,
|
||||
});
|
||||
const namesToRemove = (nameRes || [])
|
||||
.map((n: any) => n.name)
|
||||
.filter((n: string) => blockedNames.includes(n));
|
||||
if (namesToRemove.length &&
|
||||
window.confirm(
|
||||
`Also remove these names from Blocked Names?\n${namesToRemove.join(", ")}`
|
||||
)
|
||||
) {
|
||||
for (const nm of namesToRemove) {
|
||||
await qortalRequest({
|
||||
action: "DELETE_LIST_ITEM",
|
||||
list_name: "blockedNames",
|
||||
item: nm,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await qortalRequest({
|
||||
action: "DELETE_LIST_ITEM",
|
||||
list_name: list,
|
||||
item: value,
|
||||
});
|
||||
await loadLists();
|
||||
};
|
||||
|
||||
const saveItem = async (list: ListName, kind: "name" | "address") => {
|
||||
const value = inputVals[list].trim();
|
||||
if (!value) return;
|
||||
await qortalRequest({ action: "ADD_LIST_ITEMS", list_name: list, items: [value] });
|
||||
|
||||
if (list === "blockedAddresses") {
|
||||
const nameRes = await qortalRequest({
|
||||
action: "GET_ACCOUNT_NAMES", address: value, limit: 50, offset: 0, reverse: true,
|
||||
});
|
||||
const names = (nameRes || []).map((n: any) => n.name)
|
||||
.filter((n: string) => !blockedNames.includes(n));
|
||||
if (names.length &&
|
||||
window.confirm(`Block these names too?\n${names.join(", ")}`)) {
|
||||
await qortalRequest({ action: "ADD_LIST_ITEMS", list_name: "blockedNames", items: names });
|
||||
}
|
||||
}
|
||||
|
||||
if (list === "blockedNames") {
|
||||
const nameData = await qortalRequest({ action: "GET_NAME_DATA", name: value });
|
||||
if (nameData?.owner &&
|
||||
!blockedAddresses.includes(nameData.owner) &&
|
||||
window.confirm(`Block the owner address ${nameData.owner}?`)) {
|
||||
await qortalRequest({
|
||||
action: "ADD_LIST_ITEMS", list_name: "blockedAddresses", items: [nameData.owner],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (list === "followedNames" && blockedNames.includes(value))
|
||||
window.alert(`${value} is currently blocked – choose to unblock or unfollow.`);
|
||||
if (list === "blockedNames" && followed.includes(value))
|
||||
window.alert(`${value} is currently followed – choose to block or unfollow.`);
|
||||
|
||||
await loadLists();
|
||||
setInputVals({ ...inputVals, [list]: "" });
|
||||
setOpenDialog({ ...openDialog, [list]: false });
|
||||
};
|
||||
|
||||
const openAddDialog = (list: ListName) =>
|
||||
setOpenDialog({ ...openDialog, [list]: true });
|
||||
|
||||
const closeAddDialog = (list: ListName) => {
|
||||
setOpenDialog({ ...openDialog, [list]: false });
|
||||
setInputVals({ ...inputVals, [list]: "" });
|
||||
};
|
||||
|
||||
const renderTable = (
|
||||
title: string,
|
||||
rows: string[],
|
||||
list: ListName,
|
||||
kind: "name" | "address",
|
||||
) => (
|
||||
<Box sx={{ mb: 3 }}>
|
||||
<Box
|
||||
sx={{ display: "flex", justifyContent: "space-between", alignItems: "center", mb: .5 }}
|
||||
>
|
||||
<Typography variant="subtitle1" sx={{ fontWeight: 700 }}>
|
||||
{title} ({rows.length})
|
||||
</Typography>
|
||||
<Button
|
||||
size="small"
|
||||
variant="outlined"
|
||||
startIcon={<AddBoxOutlined />}
|
||||
onClick={() => openAddDialog(list)}
|
||||
style={{ borderRadius: 50 }}
|
||||
>
|
||||
{kind === "address" ? "Add Address" : "Add Name"}
|
||||
</Button>
|
||||
</Box>
|
||||
{!listsLoaded
|
||||
? <Typography variant="body2" color="text.secondary">No lists loaded</Typography>
|
||||
: rows.length === 0
|
||||
? <Typography variant="body2" color="text.secondary">No list items</Typography>
|
||||
: (
|
||||
<TableContainer component={Paper}>
|
||||
<Table stickyHeader size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<StyledTableCell>Item</StyledTableCell>
|
||||
<StyledTableCell sx={{ width: 120 }}>Action</StyledTableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{rows.map((val) => (
|
||||
<StyledTableRow key={val}>
|
||||
<StyledTableCell>{val}</StyledTableCell>
|
||||
<StyledTableCell>
|
||||
<Button
|
||||
size="small"
|
||||
color="error"
|
||||
startIcon={<RemoveCircleOutline/>}
|
||||
onClick={() => remove(list, val)}
|
||||
>
|
||||
Remove
|
||||
</Button>
|
||||
</StyledTableCell>
|
||||
</StyledTableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
|
||||
const listMeta: Record<ListName, { title: string; helper: string; label: string; kind: "name"|"address"; }> = {
|
||||
followedNames: {
|
||||
title: "Add name to Follow list",
|
||||
helper: "Following a name downloads and rehosts QDN data published by that name – videos, shops, etc.",
|
||||
label: "NAME",
|
||||
kind: "name",
|
||||
},
|
||||
blockedNames: {
|
||||
title: "Add name to Block list",
|
||||
helper: "Blocking a name blocks QDN data published by that name – videos, shops, etc.",
|
||||
label: "NAME",
|
||||
kind: "name",
|
||||
},
|
||||
blockedAddresses: {
|
||||
title: "Add address to Block list",
|
||||
helper: "Blocking an address blocks ALL transactions from that address – trades, chats, etc.",
|
||||
label: "ADDRESS",
|
||||
kind: "address",
|
||||
},
|
||||
};
|
||||
|
||||
const addDialogs = (list: ListName) => (
|
||||
<DialogGeneral
|
||||
key={list}
|
||||
maxWidth="md"
|
||||
open={openDialog[list]}
|
||||
aria-labelledby={`add-${list}`}
|
||||
keepMounted={false}
|
||||
>
|
||||
<DialogTitle>{listMeta[list].title}</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText>{listMeta[list].helper}</DialogContentText>
|
||||
<TextField
|
||||
required
|
||||
margin="normal"
|
||||
label={listMeta[list].label}
|
||||
value={inputVals[list]}
|
||||
onChange={(e) =>
|
||||
setInputVals({ ...inputVals, [list]: e.target.value })
|
||||
}
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button color="error" onClick={() => closeAddDialog(list)}>Cancel</Button>
|
||||
<Button color="success" onClick={() => saveItem(list, listMeta[list].kind)}>Add</Button>
|
||||
</DialogActions>
|
||||
</DialogGeneral>
|
||||
);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Box sx={{ display:'flex', alignItems:'center', gap:1 }}>
|
||||
<CircularProgress size={20}/> <Typography>Loading lists…</Typography>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
{notices.map((msg) => (
|
||||
<Alert key={msg} severity="warning" sx={{ mb: 2 }}>{msg}</Alert>
|
||||
))}
|
||||
{renderTable("Followed Names", followed, "followedNames", "name")}
|
||||
{renderTable("Blocked Names", blockedNames, "blockedNames", "name")}
|
||||
{renderTable("Blocked Addresses", blockedAddresses,"blockedAddresses","address")}
|
||||
{["followedNames", "blockedNames", "blockedAddresses"].map(
|
||||
(list) => addDialogs(list as ListName)
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
});
|
||||
|
||||
export default FollowBlockPanel;
|
||||
Reference in New Issue
Block a user