diff --git a/src/components/Embeds/Embed.tsx b/src/components/Embeds/Embed.tsx
index 4fef82e..c893554 100644
--- a/src/components/Embeds/Embed.tsx
+++ b/src/components/Embeds/Embed.tsx
@@ -12,6 +12,8 @@ import {
Box,
ButtonBase,
Divider,
+ Dialog,
+ IconButton,
} from "@mui/material";
import { getNameInfo } from "../Group/Group";
import { getFee } from "../../background";
@@ -22,7 +24,9 @@ import OpenInNewIcon from "@mui/icons-material/OpenInNew";
import { extractComponents } from "../Chat/MessageDisplay";
import { executeEvent } from "../../utils/events";
import { CustomLoader } from "../../common/CustomLoader";
-import PollIcon from '@mui/icons-material/Poll';
+import PollIcon from "@mui/icons-material/Poll";
+import ImageIcon from "@mui/icons-material/Image";
+import CloseIcon from "@mui/icons-material/Close";
function decodeHTMLEntities(str) {
const txt = document.createElement("textarea");
@@ -39,8 +43,11 @@ const parseQortalLink = (link) => {
// Decode any HTML entities in the link
link = decodeHTMLEntities(link);
+ // Separate the type and query string
const [typePart, queryPart] = link.slice(prefix.length).split("?");
- const type = typePart.toUpperCase();
+
+ // Ensure only the type is parsed
+ const type = typePart.split("/")[0].toUpperCase();
const params = {};
if (queryPart) {
@@ -102,7 +109,8 @@ export const Embed = ({ embedLink }) => {
const [openSnack, setOpenSnack] = useState(false);
const [infoSnack, setInfoSnack] = useState(null);
const [external, setExternal] = useState(null);
-
+ const [imageUrl, setImageUrl] = useState("");
+ const [parsedData, setParsedData] = useState(null)
const handlePoll = async (parsedData) => {
try {
setIsLoading(true);
@@ -114,7 +122,6 @@ export const Embed = ({ embedLink }) => {
setPoll(pollRes);
if (parsedData?.ref) {
const res = extractComponents(decodeURIComponent(parsedData.ref));
-
if (res?.service && res?.name) {
setExternal(res);
@@ -126,9 +133,89 @@ export const Embed = ({ embedLink }) => {
setIsLoading(false);
}
};
+
+ const getImage = async ({ identifier, name, service }) => {
+ try {
+ let numberOfTries = 0;
+ let imageFinalUrl = null;
+
+ const tryToGetImageStatus = async () => {
+ const urlStatus = `${getBaseApiReact()}/arbitrary/resource/status/${service}/${name}/${identifier}?build=true`;
+
+ const responseStatus = await fetch(urlStatus, {
+ method: "GET",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ });
+
+ const responseData = await responseStatus.json();
+ if (responseData?.status === "READY") {
+ imageFinalUrl = `${getBaseApiReact()}/arbitrary/${service}/${name}/${identifier}?async=true`;
+
+ // If parsedData is used here, it must be defined somewhere
+ if (parsedData?.ref) {
+ const res = extractComponents(decodeURIComponent(parsedData.ref));
+ if (res?.service && res?.name) {
+ setExternal(res);
+ }
+ }
+ }
+ };
+
+ // Retry logic
+ while (!imageFinalUrl && numberOfTries < 3) {
+ await tryToGetImageStatus();
+ if (!imageFinalUrl) {
+ numberOfTries++;
+ await new Promise((res)=> {
+ setTimeout(()=> {
+ res(null)
+ }, 5000)
+ })
+ }
+ }
+
+ if (imageFinalUrl) {
+ return imageFinalUrl;
+ } else {
+ setErrorMsg(
+ "Unable to download IMAGE. Please try again later by clicking the refresh button"
+ );
+ return null;
+ }
+ } catch (error) {
+ console.error("Error fetching image:", error);
+ setErrorMsg(
+ "An unexpected error occurred while trying to download the image"
+ );
+ return null;
+ }
+ };
+
+
+ const handleImage = async (parsedData) => {
+ try {
+ setIsLoading(true);
+ setErrorMsg("");
+ if (!parsedData?.name || !parsedData?.service || !parsedData?.identifier)
+ throw new Error("Invalid image embed link. Missing param.");
+ const image = await getImage({
+ name: parsedData.name,
+ service: parsedData.service,
+ identifier: parsedData?.identifier,
+ });
+ setImageUrl(image);
+ } catch (error) {
+ setErrorMsg(error?.message || "Invalid embed link");
+ } finally {
+ setIsLoading(false);
+ }
+ };
const handleLink = () => {
try {
const parsedData = parseQortalLink(embedLink);
+ setParsedData(parsedData)
const type = parsedData?.type;
switch (type) {
case "POLL":
@@ -136,7 +223,10 @@ export const Embed = ({ embedLink }) => {
handlePoll(parsedData);
}
break;
+ case "IMAGE":
+ setType("IMAGE");
+ break;
default:
break;
}
@@ -145,6 +235,15 @@ export const Embed = ({ embedLink }) => {
}
};
+ const fetchImage = () => {
+ try {
+ const parsedData = parseQortalLink(embedLink);
+ handleImage(parsedData);
+ } catch (error) {
+ setErrorMsg(error?.message || "Invalid embed link");
+ }
+ };
+
const openExternal = () => {
executeEvent("addTab", { data: external });
executeEvent("open-apps-mode", {});
@@ -171,6 +270,20 @@ export const Embed = ({ embedLink }) => {
errorMsg={errorMsg}
/>
)}
+ {type === 'IMAGE' && (
+
+ )}
-
-
- POLL embed
+
+ POLL embed
{!isOpen && !errorMsg && (
<>
-
-
+
+
>
)}
{isLoadingParent && isOpen && (
@@ -367,7 +482,7 @@ export const PollCard = ({
{" "}
)}
- {errorMsg && (
+ {errorMsg && (
{errorMsg}
@@ -545,3 +660,263 @@ const PollResults = ({ votes }) => {
);
};
+
+export const ImageCard = ({
+ image,
+ fetchImage,
+ owner,
+ setInfoSnack,
+ setOpenSnack,
+ refresh,
+ openExternal,
+ external,
+ isLoadingParent,
+ errorMsg,
+}) => {
+ const [isOpen, setIsOpen] = useState(false);
+
+ useEffect(() => {
+ if (isOpen) {
+ fetchImage();
+ }
+ }, [isOpen]);
+
+ return (
+
+
+
+
+ IMAGE embed
+
+
+
+
+
+ {external && (
+
+
+
+ )}
+
+
+
+
+ Created by {owner}
+
+
+ Not encrypted
+
+
+
+
+ {!isOpen && !errorMsg && (
+ <>
+
+
+ >
+ )}
+ {isLoadingParent && isOpen && (
+
+ {" "}
+ {" "}
+
+ )}
+ {errorMsg && (
+
+ {" "}
+
+ {errorMsg}
+ {" "}
+
+ )}
+
+
+
+
+
+
+
+
+ );
+};
+
+
+export function ImageViewer({ src, alt = "" }) {
+ const [isFullscreen, setIsFullscreen] = useState(false);
+
+ const handleOpenFullscreen = () => setIsFullscreen(true);
+ const handleCloseFullscreen = () => setIsFullscreen(false);
+
+ return (
+ <>
+ {/* Image in container */}
+
+
+
+
+ {/* Fullscreen Viewer */}
+
+ >
+ );
+ }
\ No newline at end of file
diff --git a/src/qortalRequests/get.ts b/src/qortalRequests/get.ts
index 83e0950..7617e3a 100644
--- a/src/qortalRequests/get.ts
+++ b/src/qortalRequests/get.ts
@@ -2882,12 +2882,25 @@ export const openNewTab = async (data, isFromExtension) => {
};
+const missingFieldsFunc = (data, requiredFields)=> {
+ const missingFields: string[] = [];
+ requiredFields.forEach((field) => {
+ if (!data[field]) {
+ missingFields.push(field);
+ }
+ });
+ if (missingFields.length > 0) {
+ const missingFieldsString = missingFields.join(", ");
+ const errorMsg = `Missing fields: ${missingFieldsString}`;
+ throw new Error(errorMsg);
+ }
+}
+
const encode = (value) => encodeURIComponent(value.trim()); // Helper to encode values
export const createAndCopyEmbedLink = async (data, isFromExtension) => {
const requiredFields = [
"type",
- "name"
];
const missingFields: string[] = [];
requiredFields.forEach((field) => {
@@ -2904,13 +2917,19 @@ export const createAndCopyEmbedLink = async (data, isFromExtension) => {
switch (data.type) {
case "POLL": {
+ missingFieldsFunc(data, [
+ "type",
+ "name"
+ ])
+
const queryParams = [
`name=${encode(data.name)}`,
data.ref ? `ref=${encode(data.ref)}` : null, // Add only if ref exists
]
.filter(Boolean) // Remove null values
.join("&"); // Join with `&`
- const link = `qortal://use-embed/POLL?${queryParams}`
+ const link = `qortal://use-embed/POLL?${queryParams}`
+
navigator.clipboard.writeText(link)
.then(() => {
executeEvent('openGlobalSnackBar', {
@@ -2928,6 +2947,41 @@ export const createAndCopyEmbedLink = async (data, isFromExtension) => {
});
return link;
}
+ case "IMAGE": {
+ missingFieldsFunc(data, [
+ "type",
+ "name",
+ "service",
+ "identifier"
+ ])
+ const queryParams = [
+ `name=${encode(data.name)}`,
+ `service=${encode(data.service)}`,
+ `identifier=${encode(data.identifier)}`,
+ data.ref ? `ref=${encode(data.ref)}` : null, // Add only if ref exists
+ ]
+ .filter(Boolean) // Remove null values
+ .join("&"); // Join with `&`
+
+ const link = `qortal://use-embed/IMAGE?${queryParams}`;
+
+ try {
+ await navigator.clipboard.writeText(link);
+ executeEvent("openGlobalSnackBar", {
+ message: "Copied link to clipboard",
+ type: "info",
+ });
+ } catch (error) {
+ executeEvent("openGlobalSnackBar", {
+ message: "Failed to copy to clipboard",
+ type: "error",
+ });
+ }
+
+ return link;
+ }
+
+
default:
throw new Error('Invalid type')
}