diff --git a/src/App.tsx b/src/App.tsx index 15bc189..2f6e05a 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -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(); diff --git a/src/components/common/Comments/CommentEditor.tsx b/src/components/common/Comments/CommentEditor.tsx index 0032b26..a2356d5 100644 --- a/src/components/common/Comments/CommentEditor.tsx +++ b/src/components/common/Comments/CommentEditor.tsx @@ -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) { diff --git a/src/components/layout/Navbar/Navbar.tsx b/src/components/layout/Navbar/Navbar.tsx index c453316..69385fc 100644 --- a/src/components/layout/Navbar/Navbar.tsx +++ b/src/components/layout/Navbar/Navbar.tsx @@ -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 = ({ @@ -48,6 +50,8 @@ const NavBar: React.FC = ({ userAvatar, authenticate, setTheme, + accountNames, + setActiveName, }) => { const windowSize = useWindowSize(); const searchValRef = useRef(""); @@ -303,6 +307,17 @@ const NavBar: React.FC = ({ horizontal: "left", }} > + {accountNames.map(n => ( + { + setActiveName(n.name); + handleCloseUserDropdown(); + }} + > + {n.name || "(nameless address)"} + + ))} { setIsOpenBlockedNamesModal(true); diff --git a/src/constants/PublishFees/VerifyPayment-Functions.ts b/src/constants/PublishFees/VerifyPayment-Functions.ts index 7587f0a..a49dbf6 100644 --- a/src/constants/PublishFees/VerifyPayment-Functions.ts +++ b/src/constants/PublishFees/VerifyPayment-Functions.ts @@ -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); diff --git a/src/constants/PublishFees/VerifyPayment.ts b/src/constants/PublishFees/VerifyPayment.ts index 6096b13..56c6d01 100644 --- a/src/constants/PublishFees/VerifyPayment.ts +++ b/src/constants/PublishFees/VerifyPayment.ts @@ -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; diff --git a/src/hooks/useIframe.tsx b/src/hooks/useIframe.tsx new file mode 100644 index 0000000..09d68c5 --- /dev/null +++ b/src/hooks/useIframe.tsx @@ -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 }; +}; diff --git a/src/pages/Home/IssueListComponentLevel.tsx b/src/pages/Home/IssueListComponentLevel.tsx index ffe1c25..c04b174 100644 --- a/src/pages/Home/IssueListComponentLevel.tsx +++ b/src/pages/Home/IssueListComponentLevel.tsx @@ -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: { diff --git a/src/pages/IssueContent/IssueContent.tsx b/src/pages/IssueContent/IssueContent.tsx index bc88b03..963363a 100644 --- a/src/pages/IssueContent/IssueContent.tsx +++ b/src/pages/IssueContent/IssueContent.tsx @@ -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 = () => { )} + + {ticketNumber && ( + + + Ticket #{ticketNumber} + + + + navigator.clipboard.writeText( + `qortal://APP/Q-Support/issue/${name}/${id}` + ) + } + aria-label="Copy issue URL" + > + + + + + )} {issueData?.created && ( { })) 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, diff --git a/src/wrappers/GlobalWrapper.tsx b/src/wrappers/GlobalWrapper.tsx index 5129ac7..0d4e184 100644 --- a/src/wrappers/GlobalWrapper.tsx +++ b/src/wrappers/GlobalWrapper.tsx @@ -74,12 +74,11 @@ const GlobalWrapper: React.FC = ({ 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 = ({ 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 = ({ 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} />