mirror of
https://github.com/Qortal/Qortal-Hub.git
synced 2025-04-28 05:47:52 +00:00
384 lines
11 KiB
TypeScript
384 lines
11 KiB
TypeScript
import React, { useEffect, useMemo, useRef, useState } from "react";
|
|
import { getBaseApiReact } from "../../App";
|
|
|
|
|
|
import { CustomizedSnackbars } from "../Snackbar/Snackbar";
|
|
|
|
import { extractComponents } from "../Chat/MessageDisplay";
|
|
import { executeEvent } from "../../utils/events";
|
|
|
|
import { base64ToBlobUrl } from "../../utils/fileReading";
|
|
import { useRecoilState, useRecoilValue, useSetRecoilState } from "recoil";
|
|
import { blobControllerAtom, blobKeySelector, resourceKeySelector, selectedGroupIdAtom } from "../../atoms/global";
|
|
import { parseQortalLink } from "./embed-utils";
|
|
import { PollCard } from "./PollEmbed";
|
|
import { ImageCard } from "./ImageEmbed";
|
|
import { AttachmentCard } from "./AttachmentEmbed";
|
|
import { decodeIfEncoded } from "../../utils/decode";
|
|
|
|
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 setBlobs = useSetRecoilState(blobControllerAtom);
|
|
const [selectedGroupId] = useRecoilState(selectedGroupIdAtom)
|
|
const resourceData = useMemo(()=> {
|
|
const parsedDataOnTheFly = parseQortalLink(embedLink);
|
|
if(parsedDataOnTheFly?.service && parsedDataOnTheFly?.name && parsedDataOnTheFly?.identifier){
|
|
return {
|
|
service : parsedDataOnTheFly?.service,
|
|
name: parsedDataOnTheFly?.name,
|
|
identifier: parsedDataOnTheFly?.identifier,
|
|
fileName: parsedDataOnTheFly?.fileName ? decodeURIComponent(parsedDataOnTheFly?.fileName) : null,
|
|
mimeType: parsedDataOnTheFly?.mimeType ? decodeURIComponent(parsedDataOnTheFly?.mimeType) : null,
|
|
key: parsedDataOnTheFly?.key ? decodeURIComponent(parsedDataOnTheFly?.key) : null,
|
|
}
|
|
} else {
|
|
return null
|
|
}
|
|
}, [embedLink])
|
|
|
|
const keyIdentifier = useMemo(()=> {
|
|
|
|
if(resourceData){
|
|
return `${resourceData.service}-${resourceData.name}-${resourceData.identifier}`
|
|
} else {
|
|
return undefined
|
|
}
|
|
}, [resourceData])
|
|
const blobUrl = useRecoilValue(blobKeySelector(keyIdentifier));
|
|
|
|
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);
|
|
|
|
} catch (error) {
|
|
setErrorMsg(error?.message || "Invalid embed link");
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
const getImage = async ({ identifier, name, service }, key, parsedData) => {
|
|
try {
|
|
if(blobUrl?.blobUrl){
|
|
return blobUrl?.blobUrl
|
|
}
|
|
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") {
|
|
if (parsedData?.encryptionType) {
|
|
const urlData = `${getBaseApiReact()}/arbitrary/${service}/${name}/${identifier}?encoding=base64`;
|
|
|
|
const responseData = await fetch(urlData, {
|
|
method: "GET",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
},
|
|
});
|
|
const data = await responseData.text();
|
|
if (data) {
|
|
let decryptedData
|
|
try {
|
|
if(key && encryptionType === 'private'){
|
|
decryptedData = await window.sendMessage(
|
|
"DECRYPT_DATA_WITH_SHARING_KEY",
|
|
|
|
{
|
|
encryptedData: data,
|
|
key: decodeURIComponent(key),
|
|
}
|
|
|
|
);
|
|
}
|
|
if(encryptionType === 'group'){
|
|
|
|
decryptedData = await window.sendMessage(
|
|
"DECRYPT_QORTAL_GROUP_DATA",
|
|
|
|
{
|
|
data64: data,
|
|
groupId: selectedGroupId,
|
|
}
|
|
|
|
);
|
|
|
|
}
|
|
} catch (error) {
|
|
throw new Error('Unable to decrypt')
|
|
}
|
|
|
|
if (!decryptedData || decryptedData?.error) throw new Error("Could not decrypt data");
|
|
imageFinalUrl = base64ToBlobUrl(decryptedData, parsedData?.mimeType ? decodeURIComponent(parsedData?.mimeType) : undefined)
|
|
setBlobs((prev=> {
|
|
return {
|
|
...prev,
|
|
[`${service}-${name}-${identifier}`]: {
|
|
blobUrl: imageFinalUrl,
|
|
timestamp: Date.now()
|
|
}
|
|
}
|
|
}))
|
|
} else {
|
|
throw new Error('No data for image')
|
|
}
|
|
|
|
} else {
|
|
imageFinalUrl = `${getBaseApiReact()}/arbitrary/${service}/${name}/${identifier}?async=true`;
|
|
|
|
// If parsedData is used here, it must be defined somewhere
|
|
|
|
}
|
|
}
|
|
};
|
|
|
|
// 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(
|
|
error?.error || error?.message || "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.");
|
|
let image = await getImage({
|
|
name: parsedData.name,
|
|
service: parsedData.service,
|
|
identifier: parsedData?.identifier,
|
|
}, parsedData?.key, parsedData);
|
|
|
|
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;
|
|
try {
|
|
if (parsedData?.ref) {
|
|
const res = extractComponents(decodeURIComponent(parsedData.ref));
|
|
if (res?.service && res?.name) {
|
|
setExternal(res);
|
|
}
|
|
}
|
|
} catch (error) {
|
|
|
|
}
|
|
switch (type) {
|
|
case "POLL":
|
|
{
|
|
handlePoll(parsedData);
|
|
}
|
|
break;
|
|
case "IMAGE":
|
|
setType("IMAGE");
|
|
|
|
break;
|
|
case "ATTACHMENT":
|
|
setType("ATTACHMENT");
|
|
|
|
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]);
|
|
|
|
|
|
|
|
const resourceDetails = useRecoilValue(resourceKeySelector(keyIdentifier));
|
|
|
|
const { parsedType, encryptionType } = useMemo(() => {
|
|
let parsedType;
|
|
let encryptionType = false;
|
|
try {
|
|
const parsedDataOnTheFly = parseQortalLink(embedLink);
|
|
if (parsedDataOnTheFly?.type) {
|
|
parsedType = parsedDataOnTheFly.type;
|
|
}
|
|
if (parsedDataOnTheFly?.encryptionType) {
|
|
encryptionType = parsedDataOnTheFly?.encryptionType
|
|
}
|
|
} catch (error) {}
|
|
return { parsedType, encryptionType };
|
|
}, [embedLink]);
|
|
|
|
return (
|
|
<div>
|
|
{parsedType === "POLL" && (
|
|
<PollCard
|
|
poll={poll}
|
|
refresh={handleLink}
|
|
setInfoSnack={setInfoSnack}
|
|
setOpenSnack={setOpenSnack}
|
|
external={external}
|
|
openExternal={openExternal}
|
|
isLoadingParent={isLoading}
|
|
errorMsg={errorMsg}
|
|
/>
|
|
)}
|
|
{parsedType === "IMAGE" && (
|
|
<ImageCard
|
|
image={imageUrl}
|
|
owner={parsedData?.name}
|
|
fetchImage={fetchImage}
|
|
refresh={fetchImage}
|
|
setInfoSnack={setInfoSnack}
|
|
setOpenSnack={setOpenSnack}
|
|
external={external}
|
|
openExternal={openExternal}
|
|
isLoadingParent={isLoading}
|
|
errorMsg={errorMsg}
|
|
encryptionType={encryptionType}
|
|
/>
|
|
)}
|
|
{parsedType === 'ATTACHMENT' && (
|
|
<AttachmentCard
|
|
resourceData={resourceData}
|
|
resourceDetails={resourceDetails}
|
|
owner={parsedData?.name}
|
|
refresh={fetchImage}
|
|
setInfoSnack={setInfoSnack}
|
|
setOpenSnack={setOpenSnack}
|
|
external={external}
|
|
openExternal={openExternal}
|
|
isLoadingParent={isLoading}
|
|
errorMsg={errorMsg}
|
|
encryptionType={encryptionType}
|
|
selectedGroupId={selectedGroupId}
|
|
/>
|
|
)}
|
|
<CustomizedSnackbars
|
|
duration={2000}
|
|
open={openSnack}
|
|
setOpen={setOpenSnack}
|
|
info={infoSnack}
|
|
setInfo={setInfoSnack}
|
|
/>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|