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 */} + + {alt} + + + {/* Fullscreen Viewer */} + + + {/* Close Button */} + + + + + {/* Fullscreen Image */} + {alt} + + + + ); + } \ 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') }