import React, { useContext, useEffect, useMemo, useRef, useState } from "react"; import { MyContext, getBaseApiReact } from "../../App"; import { Card, CardContent, CardHeader, Typography, RadioGroup, Radio, FormControlLabel, Button, Box, ButtonBase, Divider, Dialog, IconButton, } from "@mui/material"; import { getNameInfo } from "../Group/Group"; import { getFee } from "../../background"; import { Spacer } from "../../common/Spacer"; import { CustomizedSnackbars } from "../Snackbar/Snackbar"; import RefreshIcon from "@mui/icons-material/Refresh"; 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 ImageIcon from "@mui/icons-material/Image"; import CloseIcon from "@mui/icons-material/Close"; function decodeHTMLEntities(str) { const txt = document.createElement("textarea"); txt.innerHTML = str; return txt.value; } 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 getPoll = async (name) => { const pollName = name; const url = `${getBaseApiReact()}/polls/${pollName}`; const response = await fetch(url, { method: "GET", headers: { "Content-Type": "application/json", }, }); const responseData = await response.json(); if (responseData?.message?.includes("POLL_NO_EXISTS")) { throw new Error("POLL_NO_EXISTS"); } else if (responseData?.pollName) { const urlVotes = `${getBaseApiReact()}/polls/votes/${pollName}`; const responseVotes = await fetch(urlVotes, { method: "GET", headers: { "Content-Type": "application/json", }, }); const responseDataVotes = await responseVotes.json(); return { info: responseData, votes: responseDataVotes, }; } }; export const Embed = ({ embedLink }) => { const [errorMsg, setErrorMsg] = useState(""); const [isLoading, setIsLoading] = useState(false); const [poll, setPoll] = useState(null); const [type, setType] = useState(""); const hasFetched = useRef(false); 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); setErrorMsg(""); setType("POLL"); if (!parsedData?.name) throw new Error("Invalid poll embed link. Missing name."); const pollRes = await getPoll(parsedData.name); setPoll(pollRes); if (parsedData?.ref) { const res = extractComponents(decodeURIComponent(parsedData.ref)); if (res?.service && res?.name) { setExternal(res); } } } catch (error) { setErrorMsg(error?.message || "Invalid embed link"); } finally { 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": { handlePoll(parsedData); } break; case "IMAGE": setType("IMAGE"); break; default: break; } } catch (error) { setErrorMsg(error?.message || "Invalid embed link"); } }; 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", {}); }; useEffect(() => { if (!embedLink || hasFetched.current) return; handleLink(); hasFetched.current = true; }, [embedLink]); return (
{!type && } {type === "POLL" && ( )} {type === 'IMAGE' && ( )}
); }; export const PollCard = ({ poll, setInfoSnack, setOpenSnack, refresh, openExternal, external, isLoadingParent, errorMsg, }) => { const [selectedOption, setSelectedOption] = useState(""); const [ownerName, setOwnerName] = useState(""); const [showResults, setShowResults] = useState(false); const [isOpen, setIsOpen] = useState(false); const { show, userInfo } = useContext(MyContext); const [isLoadingSubmit, setIsLoadingSubmit] = useState(false); const handleVote = async () => { const fee = await getFee("VOTE_ON_POLL"); await show({ message: `Do you accept this VOTE_ON_POLL transaction? POLLS are public!`, publishFee: fee.fee + " QORT", }); setIsLoadingSubmit(true); window .sendMessage( "voteOnPoll", { pollName: poll?.info?.pollName, optionIndex: +selectedOption, }, 60000 ) .then((response) => { setIsLoadingSubmit(false); if (response.error) { setInfoSnack({ type: "error", message: response?.error || "Unable to vote.", }); setOpenSnack(true); return; } else { setInfoSnack({ type: "success", message: "Successfully voted. Please wait a couple minutes for the network to propogate the changes.", }); setOpenSnack(true); } }) .catch((error) => { setIsLoadingSubmit(false); setInfoSnack({ type: "error", message: error?.message || "Unable to vote.", }); setOpenSnack(true); }); }; const getName = async (owner) => { try { const res = await getNameInfo(owner); if (res) { setOwnerName(res); } } catch (error) {} }; useEffect(() => { if (poll?.info?.owner) { getName(poll.info.owner); } }, [poll?.info?.owner]); return ( POLL embed {external && ( )} Created by {ownerName || poll?.info?.owner} {!isOpen && !errorMsg && ( <> )} {isLoadingParent && isOpen && ( {" "} {" "} )} {errorMsg && ( {" "} {errorMsg} {" "} )} Options setSelectedOption(e.target.value)} > {poll?.info?.pollOptions?.map((option, index) => ( } label={option?.optionName} /> ))} {" "} {`${poll?.votes?.totalVotes} ${ poll?.votes?.totalVotes === 1 ? " vote" : " votes" }`} item?.voterPublicKey === userInfo?.publicKey ) ? "visible" : "hidden", }} > You've already voted. {isLoadingSubmit && ( Is processing transaction, please wait... )} { setShowResults((prev) => !prev); }} > {showResults ? "hide " : "show "} results {showResults && } ); }; const PollResults = ({ votes }) => { const maxVotes = Math.max( ...votes?.voteCounts?.map((option) => option.voteCount) ); const options = votes?.voteCounts; return ( {options .sort((a, b) => b.voteCount - a.voteCount) // Sort options by votes (highest first) .map((option, index) => ( {`${index + 1}. ${option.optionName}`} {option.voteCount} 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} ); }