forked from Qortal/q-support
update #1
@@ -1,5 +1,6 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Route, Routes } from "react-router-dom";
|
||||
import { useIframe } from './hooks/useIframe'
|
||||
import { ThemeProvider } from "@mui/material/styles";
|
||||
import { CssBaseline } from "@mui/material";
|
||||
import { darkTheme, lightTheme } from "./styles/theme";
|
||||
@@ -16,6 +17,7 @@ import { fetchFeesRedux } from "./constants/PublishFees/FeePricePublish/FeePrice
|
||||
function App() {
|
||||
// const themeColor = window._qdnTheme
|
||||
const [theme, setTheme] = useState("dark");
|
||||
useIframe()
|
||||
|
||||
useEffect(() => {
|
||||
fetchFeesRedux();
|
||||
|
||||
@@ -130,9 +130,10 @@ export const CommentEditor = ({
|
||||
const address = user?.address;
|
||||
const name = user?.name || "";
|
||||
let errorMsg = "";
|
||||
const safePostName = encodeURIComponent(postName);
|
||||
|
||||
const notificationMessage = `This is an automated Q-Support notification indicating that someone has commented on your issue here:
|
||||
qortal://APP/Q-Support/issue/${postName}/${postId}`;
|
||||
qortal://APP/Q-Support/issue/${safePostName}/${postId}`;
|
||||
|
||||
if (useTestIdentifiers) await sendQchatDM(postName, notificationMessage);
|
||||
if (!address) {
|
||||
|
||||
@@ -40,6 +40,8 @@ interface Props {
|
||||
userAvatar: string;
|
||||
authenticate: () => void;
|
||||
setTheme: (val: string) => void;
|
||||
accountNames: { name: string }[];
|
||||
setActiveName: (name: string) => void;
|
||||
}
|
||||
|
||||
const NavBar: React.FC<Props> = ({
|
||||
@@ -48,6 +50,8 @@ const NavBar: React.FC<Props> = ({
|
||||
userAvatar,
|
||||
authenticate,
|
||||
setTheme,
|
||||
accountNames,
|
||||
setActiveName,
|
||||
}) => {
|
||||
const windowSize = useWindowSize();
|
||||
const searchValRef = useRef("");
|
||||
@@ -303,6 +307,17 @@ const NavBar: React.FC<Props> = ({
|
||||
horizontal: "left",
|
||||
}}
|
||||
>
|
||||
{accountNames.map(n => (
|
||||
<DropdownContainer
|
||||
key={n.name}
|
||||
onClick={() => {
|
||||
setActiveName(n.name);
|
||||
handleCloseUserDropdown();
|
||||
}}
|
||||
>
|
||||
<DropdownText>{n.name || "(nameless address)"}</DropdownText>
|
||||
</DropdownContainer>
|
||||
))}
|
||||
<DropdownContainer
|
||||
onClick={() => {
|
||||
setIsOpenBlockedNamesModal(true);
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { Issue } from "../../state/features/fileSlice.ts";
|
||||
import { getUserAccountNames } from "../../utils/qortalRequests.ts";
|
||||
import { PublishFeeData } from "./SendFeeFunctions.ts";
|
||||
|
||||
export type AccountName = { name: string; owner: string };
|
||||
|
||||
export interface GetRequestData {
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
@@ -24,35 +23,6 @@ export interface getTransactionBySignatureResponse {
|
||||
amount: string;
|
||||
}
|
||||
|
||||
export const stringIsEmpty = (value: string) => {
|
||||
return value === "";
|
||||
};
|
||||
|
||||
export const getAccountNames = async (
|
||||
address: string,
|
||||
params?: GetRequestData
|
||||
) => {
|
||||
const names = (await qortalRequest({
|
||||
action: "GET_ACCOUNT_NAMES",
|
||||
address: address,
|
||||
...params,
|
||||
})) as AccountName[];
|
||||
|
||||
const namelessAddress = { name: "", owner: address };
|
||||
const emptyNamesFilled = names.map(({ name, owner }) => {
|
||||
return stringIsEmpty(name) ? namelessAddress : { name, owner };
|
||||
});
|
||||
|
||||
const returnValue =
|
||||
emptyNamesFilled.length > 0 ? emptyNamesFilled : [namelessAddress];
|
||||
return returnValue as AccountName[];
|
||||
};
|
||||
|
||||
export const getUserAccountNames = async () => {
|
||||
const account = await getUserAccount();
|
||||
return await getAccountNames(account.address);
|
||||
};
|
||||
|
||||
export const userHasName = async (name: string) => {
|
||||
const userAccountNames = await getUserAccountNames();
|
||||
const userNames = userAccountNames.map(userName => userName.name);
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { feeDestinationName, maxFeePublishTimeDiff } from "./FeeData.tsx";
|
||||
import {
|
||||
getAccountNames,
|
||||
getTransactionBySignatureResponse,
|
||||
objectHasNullValues,
|
||||
objectToPublishFeeData,
|
||||
@@ -8,6 +7,7 @@ import {
|
||||
import { verifyFeeAmount } from "./FeePricePublish/FeePricePublish.ts";
|
||||
import { getNameData, PublishFeeData } from "./SendFeeFunctions.ts";
|
||||
import { Issue } from "../../state/features/fileSlice.ts";
|
||||
import { getAccountNames } from "../../utils/qortalRequests.ts";
|
||||
|
||||
const getSignature = async (signature: string) => {
|
||||
const url = "/transactions/signature/" + signature;
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import { useEffect } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
export const useIframe = () => {
|
||||
const navigate = useNavigate();
|
||||
useEffect(() => {
|
||||
function handleNavigation(event: MessageEvent) {
|
||||
if (event.data?.action === "NAVIGATE_TO_PATH" && event.data.path) {
|
||||
console.log("Navigating to path within React app:", event.data.path);
|
||||
navigate(event.data.path); // Navigate directly to the specified path
|
||||
|
||||
// Send a response back to the parent window after navigation is handled
|
||||
window.parent.postMessage(
|
||||
{ action: "NAVIGATION_SUCCESS", path: event.data.path },
|
||||
"*"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener("message", handleNavigation);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("message", handleNavigation);
|
||||
};
|
||||
}, [navigate]);
|
||||
return { navigate };
|
||||
};
|
||||
@@ -50,7 +50,7 @@ export const IssueListComponentLevel = ({ mode }: VideoListProps) => {
|
||||
try {
|
||||
const offset = issues.length;
|
||||
// `/arbitrary/resources/search?mode=ALL&includemetadata=false&reverse=true&excludeblocked=true&exactmatchnames=true&offset=${offset}&limit=${videoLimit}`;
|
||||
const url = `/arbitrary/resources/search?mode=ALL&includemetadata=false&reverse=true&excludeblocked=true&exactmatchnames=true&offset=${offset}&limit=50&service=DOCUMENT&query=${QSUPPORT_FILE_BASE}_&name=${paramName}`;
|
||||
const url = `/arbitrary/resources/search?mode=ALL&includemetadata=false&reverse=true&excludeblocked=true&exactmatchnames=true&offset=${offset}&limit=50&service=DOCUMENT&query=${QSUPPORT_FILE_BASE}&name=${paramName}`;
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import DownloadIcon from "@mui/icons-material/Download";
|
||||
import ContentCopyIcon from "@mui/icons-material/ContentCopy";
|
||||
import { IconButton, Tooltip } from "@mui/material";
|
||||
import { Avatar, Box, Typography, useTheme } from "@mui/material";
|
||||
import React, { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
@@ -110,6 +112,12 @@ export const IssueContent = () => {
|
||||
}, [issueData]);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const ticketNumber = useMemo(() => {
|
||||
if (!id) return "";
|
||||
const match = id.match(/_([^_]+)_metadata$/);
|
||||
return match ? match[1] : "";
|
||||
}, [id]);
|
||||
|
||||
const getIssueData = React.useCallback(async (name: string, id: string) => {
|
||||
try {
|
||||
if (!name || !id) return;
|
||||
@@ -313,6 +321,36 @@ export const IssueContent = () => {
|
||||
<IssueIcon iconSrc={QORTicon} style={{ marginLeft: "10px" }} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
{ticketNumber && (
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "6px",
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="subtitle1"
|
||||
sx={{ fontWeight: "bold", userSelect: "text" }}
|
||||
>
|
||||
Ticket #{ticketNumber}
|
||||
</Typography>
|
||||
<Tooltip title="Copy issue URL">
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() =>
|
||||
navigator.clipboard.writeText(
|
||||
`qortal://APP/Q-Support/issue/${name}/${id}`
|
||||
)
|
||||
}
|
||||
aria-label="Copy issue URL"
|
||||
>
|
||||
<ContentCopyIcon fontSize="inherit" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
)}
|
||||
{issueData?.created && (
|
||||
<Typography
|
||||
variant="h4"
|
||||
|
||||
@@ -1,13 +1,21 @@
|
||||
import { createSlice } from '@reduxjs/toolkit';
|
||||
|
||||
export interface NameRecord {
|
||||
name: string;
|
||||
owner: string;
|
||||
}
|
||||
|
||||
interface User {
|
||||
address: string;
|
||||
publicKey: string;
|
||||
name?: string;
|
||||
names?: NameRecord[];
|
||||
}
|
||||
|
||||
interface AuthState {
|
||||
user: {
|
||||
address: string;
|
||||
publicKey: string;
|
||||
name?: string;
|
||||
} | null;
|
||||
user: User | null;
|
||||
}
|
||||
|
||||
const initialState: AuthState = {
|
||||
user: null
|
||||
};
|
||||
|
||||
@@ -2,8 +2,8 @@ import moment from "moment";
|
||||
import { CoinType } from "../constants/PublishFees/FeePricePublish/FeePricePublish.ts";
|
||||
import { NameData } from "../constants/PublishFees/SendFeeFunctions.ts";
|
||||
import {
|
||||
GetRequestData,
|
||||
getUserAccount,
|
||||
getUserAccountNames,
|
||||
} from "../constants/PublishFees/VerifyPayment-Functions.ts";
|
||||
import { Issue } from "../state/features/fileSlice.ts";
|
||||
import { isNumber } from "./utilFunctions.ts";
|
||||
@@ -15,6 +15,37 @@ export const getNameData = async (name: string) => {
|
||||
})) as NameData;
|
||||
};
|
||||
|
||||
export type AccountName = { name: string; owner: string };
|
||||
|
||||
export const stringIsEmpty = (value: string) => {
|
||||
return value === "";
|
||||
};
|
||||
|
||||
export const getAccountNames = async (
|
||||
address: string,
|
||||
params?: GetRequestData
|
||||
) => {
|
||||
const names = (await qortalRequest({
|
||||
action: "GET_ACCOUNT_NAMES",
|
||||
address: address,
|
||||
...params,
|
||||
})) as AccountName[];
|
||||
|
||||
const namelessAddress = { name: "", owner: address };
|
||||
const emptyNamesFilled = names.map(({ name, owner }) => {
|
||||
return stringIsEmpty(name) ? namelessAddress : { name, owner };
|
||||
});
|
||||
|
||||
const returnValue =
|
||||
emptyNamesFilled.length > 0 ? emptyNamesFilled : [namelessAddress];
|
||||
return returnValue as AccountName[];
|
||||
};
|
||||
|
||||
export const getUserAccountNames = async () => {
|
||||
const account = await getUserAccount();
|
||||
return await getAccountNames(account.address);
|
||||
};
|
||||
|
||||
export const sendQchatDM = async (
|
||||
recipientName: string,
|
||||
message: string,
|
||||
|
||||
@@ -74,12 +74,11 @@ const GlobalWrapper: React.FC<Props> = ({ children, setTheme }) => {
|
||||
|
||||
const { isLoadingGlobal } = useSelector((state: RootState) => state.global);
|
||||
|
||||
async function getNameInfo(address: string) {
|
||||
return await qortalRequest({
|
||||
action: "GET_PRIMARY_NAME",
|
||||
address: address,
|
||||
});
|
||||
}
|
||||
const getAccountNames = async (address: string) =>
|
||||
(await qortalRequest({
|
||||
action: "GET_ACCOUNT_NAMES",
|
||||
address,
|
||||
})) as { name: string; owner: string }[];
|
||||
|
||||
const askForAccountInformation = React.useCallback(async () => {
|
||||
try {
|
||||
@@ -87,8 +86,15 @@ const GlobalWrapper: React.FC<Props> = ({ children, setTheme }) => {
|
||||
action: "GET_USER_ACCOUNT",
|
||||
});
|
||||
|
||||
const name = await getNameInfo(account.address);
|
||||
dispatch(addUser({ ...account, name }));
|
||||
const names = await getAccountNames(account.address);
|
||||
const [primary] = names;
|
||||
dispatch(
|
||||
addUser({
|
||||
...account,
|
||||
name: primary?.name ?? "",
|
||||
names,
|
||||
})
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
@@ -128,6 +134,10 @@ const GlobalWrapper: React.FC<Props> = ({ children, setTheme }) => {
|
||||
setTheme={(val: string) => setTheme(val)}
|
||||
isAuthenticated={!!user?.name}
|
||||
userName={user?.name || ""}
|
||||
accountNames={user?.names || []}
|
||||
setActiveName={(name: string) =>
|
||||
dispatch(addUser({ ...user, name }))
|
||||
}
|
||||
userAvatar={userAvatar}
|
||||
authenticate={askForAccountInformation}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user