From b661bd3869022c4a47b538acb82f0097fbeb1f2d Mon Sep 17 00:00:00 2001 From: PhilReact Date: Tue, 26 Nov 2024 12:02:03 +0200 Subject: [PATCH] added image embed --- src/components/Embeds/Embed.tsx | 493 ++++++++++++++++++++++++++++---- src/qortalRequests/get.ts | 55 +++- 2 files changed, 485 insertions(+), 63 deletions(-) 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 */} + + {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 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') }