diff --git a/src/components/Embeds/Embed.tsx b/src/components/Embeds/Embed.tsx
index 63fa4b3..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,10 @@ 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");
txt.innerHTML = str;
@@ -30,39 +35,39 @@ function decodeHTMLEntities(str) {
}
const parseQortalLink = (link) => {
- const prefix = "qortal://use-embed/";
- if (!link.startsWith(prefix)) {
- throw new Error("Invalid link format");
- }
-
- // Decode any HTML entities in the link
- link = decodeHTMLEntities(link);
-
- // Separate the type and query string
- const [typePart, queryPart] = link.slice(prefix.length).split("?");
-
- // Ensure only the type is parsed
- const type = typePart.split("/")[0].toUpperCase();
-
- const params = {};
- if (queryPart) {
- const queryPairs = queryPart.split("&");
-
- queryPairs.forEach((pair) => {
- const [key, value] = pair.split("=");
- if (key && value) {
- const decodedKey = decodeURIComponent(key.trim());
- const decodedValue = value.trim().replace(
- /<\/?[^>]+(>|$)/g,
- "" // Remove any HTML tags
- );
- params[decodedKey] = decodedValue;
- }
- });
- }
-
- return { type, ...params };
- };
+ const prefix = "qortal://use-embed/";
+ if (!link.startsWith(prefix)) {
+ throw new Error("Invalid link format");
+ }
+
+ // Decode any HTML entities in the link
+ link = decodeHTMLEntities(link);
+
+ // Separate the type and query string
+ const [typePart, queryPart] = link.slice(prefix.length).split("?");
+
+ // Ensure only the type is parsed
+ const type = typePart.split("/")[0].toUpperCase();
+
+ const params = {};
+ if (queryPart) {
+ const queryPairs = queryPart.split("&");
+
+ queryPairs.forEach((pair) => {
+ const [key, value] = pair.split("=");
+ if (key && value) {
+ const decodedKey = decodeURIComponent(key.trim());
+ const decodedValue = value.trim().replace(
+ /<\/?[^>]+(>|$)/g,
+ "" // Remove any HTML tags
+ );
+ params[decodedKey] = decodedValue;
+ }
+ });
+ }
+
+ return { type, ...params };
+};
const getPoll = async (name) => {
const pollName = name;
const url = `${getBaseApiReact()}/polls/${pollName}`;
@@ -104,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);
@@ -116,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);
@@ -128,10 +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);
- console.log('parsedData', parsedData)
+ setParsedData(parsedData)
const type = parsedData?.type;
switch (type) {
case "POLL":
@@ -139,7 +223,10 @@ export const Embed = ({ embedLink }) => {
handlePoll(parsedData);
}
break;
+ case "IMAGE":
+ setType("IMAGE");
+ break;
default:
break;
}
@@ -148,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", {});
@@ -174,6 +270,20 @@ export const Embed = ({ embedLink }) => {
errorMsg={errorMsg}
/>
)}
+ {type === 'IMAGE' && (
+
+ )}
-
-
- POLL embed
+
+ POLL embed
{!isOpen && !errorMsg && (
- <>
-
-
+ <>
+
+
>
)}
{isLoadingParent && isOpen && (
@@ -385,7 +494,7 @@ export const PollCard = ({
{errorMsg}
@@ -551,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 60b434b..25654f4 100644
--- a/src/qortalRequests/get.ts
+++ b/src/qortalRequests/get.ts
@@ -3056,12 +3056,25 @@ export const signTransaction = 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) => {
@@ -3078,6 +3091,11 @@ 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
@@ -3103,6 +3121,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')
}