added image embed

This commit is contained in:
PhilReact 2024-11-26 12:02:03 +02:00
parent 4e93b597aa
commit b661bd3869
2 changed files with 485 additions and 63 deletions

View File

@ -12,6 +12,8 @@ import {
Box, Box,
ButtonBase, ButtonBase,
Divider, Divider,
Dialog,
IconButton,
} from "@mui/material"; } from "@mui/material";
import { getNameInfo } from "../Group/Group"; import { getNameInfo } from "../Group/Group";
import { getFee } from "../../background"; import { getFee } from "../../background";
@ -22,7 +24,10 @@ import OpenInNewIcon from "@mui/icons-material/OpenInNew";
import { extractComponents } from "../Chat/MessageDisplay"; import { extractComponents } from "../Chat/MessageDisplay";
import { executeEvent } from "../../utils/events"; import { executeEvent } from "../../utils/events";
import { CustomLoader } from "../../common/CustomLoader"; 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) { function decodeHTMLEntities(str) {
const txt = document.createElement("textarea"); const txt = document.createElement("textarea");
txt.innerHTML = str; txt.innerHTML = str;
@ -30,39 +35,39 @@ function decodeHTMLEntities(str) {
} }
const parseQortalLink = (link) => { const parseQortalLink = (link) => {
const prefix = "qortal://use-embed/"; const prefix = "qortal://use-embed/";
if (!link.startsWith(prefix)) { if (!link.startsWith(prefix)) {
throw new Error("Invalid link format"); throw new Error("Invalid link format");
} }
// Decode any HTML entities in the link // Decode any HTML entities in the link
link = decodeHTMLEntities(link); link = decodeHTMLEntities(link);
// Separate the type and query string // Separate the type and query string
const [typePart, queryPart] = link.slice(prefix.length).split("?"); const [typePart, queryPart] = link.slice(prefix.length).split("?");
// Ensure only the type is parsed // Ensure only the type is parsed
const type = typePart.split("/")[0].toUpperCase(); const type = typePart.split("/")[0].toUpperCase();
const params = {}; const params = {};
if (queryPart) { if (queryPart) {
const queryPairs = queryPart.split("&"); const queryPairs = queryPart.split("&");
queryPairs.forEach((pair) => { queryPairs.forEach((pair) => {
const [key, value] = pair.split("="); const [key, value] = pair.split("=");
if (key && value) { if (key && value) {
const decodedKey = decodeURIComponent(key.trim()); const decodedKey = decodeURIComponent(key.trim());
const decodedValue = value.trim().replace( const decodedValue = value.trim().replace(
/<\/?[^>]+(>|$)/g, /<\/?[^>]+(>|$)/g,
"" // Remove any HTML tags "" // Remove any HTML tags
); );
params[decodedKey] = decodedValue; params[decodedKey] = decodedValue;
} }
}); });
} }
return { type, ...params }; return { type, ...params };
}; };
const getPoll = async (name) => { const getPoll = async (name) => {
const pollName = name; const pollName = name;
const url = `${getBaseApiReact()}/polls/${pollName}`; const url = `${getBaseApiReact()}/polls/${pollName}`;
@ -104,7 +109,8 @@ export const Embed = ({ embedLink }) => {
const [openSnack, setOpenSnack] = useState(false); const [openSnack, setOpenSnack] = useState(false);
const [infoSnack, setInfoSnack] = useState(null); const [infoSnack, setInfoSnack] = useState(null);
const [external, setExternal] = useState(null); const [external, setExternal] = useState(null);
const [imageUrl, setImageUrl] = useState("");
const [parsedData, setParsedData] = useState(null)
const handlePoll = async (parsedData) => { const handlePoll = async (parsedData) => {
try { try {
setIsLoading(true); setIsLoading(true);
@ -116,7 +122,6 @@ export const Embed = ({ embedLink }) => {
setPoll(pollRes); setPoll(pollRes);
if (parsedData?.ref) { if (parsedData?.ref) {
const res = extractComponents(decodeURIComponent(parsedData.ref)); const res = extractComponents(decodeURIComponent(parsedData.ref));
if (res?.service && res?.name) { if (res?.service && res?.name) {
setExternal(res); setExternal(res);
@ -128,10 +133,89 @@ export const Embed = ({ embedLink }) => {
setIsLoading(false); 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 = () => { const handleLink = () => {
try { try {
const parsedData = parseQortalLink(embedLink); const parsedData = parseQortalLink(embedLink);
console.log('parsedData', parsedData) setParsedData(parsedData)
const type = parsedData?.type; const type = parsedData?.type;
switch (type) { switch (type) {
case "POLL": case "POLL":
@ -139,7 +223,10 @@ export const Embed = ({ embedLink }) => {
handlePoll(parsedData); handlePoll(parsedData);
} }
break; break;
case "IMAGE":
setType("IMAGE");
break;
default: default:
break; 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 = () => { const openExternal = () => {
executeEvent("addTab", { data: external }); executeEvent("addTab", { data: external });
executeEvent("open-apps-mode", {}); executeEvent("open-apps-mode", {});
@ -174,6 +270,20 @@ export const Embed = ({ embedLink }) => {
errorMsg={errorMsg} errorMsg={errorMsg}
/> />
)} )}
{type === 'IMAGE' && (
<ImageCard
image={imageUrl}
owner={parsedData?.name}
fetchImage={fetchImage}
refresh={fetchImage}
setInfoSnack={setInfoSnack}
setOpenSnack={setOpenSnack}
external={external}
openExternal={openExternal}
isLoadingParent={isLoading}
errorMsg={errorMsg}
/>
)}
<CustomizedSnackbars <CustomizedSnackbars
duration={2000} duration={2000}
open={openSnack} open={openSnack}
@ -262,14 +372,12 @@ export const PollCard = ({
} }
}, [poll?.info?.owner]); }, [poll?.info?.owner]);
console.log('ownerName', ownerName)
return ( return (
<Card <Card
sx={{ sx={{
backgroundColor: "#1F2023", backgroundColor: "#1F2023",
height: isOpen ? 'auto' : "150px", height: isOpen ? "auto" : "150px",
}} }}
> >
<Box <Box
@ -280,17 +388,19 @@ export const PollCard = ({
padding: "16px 16px 0px 16px", padding: "16px 16px 0px 16px",
}} }}
> >
<Box <Box
sx={{ sx={{
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
gap: "10px", gap: "10px",
}} }}
> >
<PollIcon sx={{ <PollIcon
color: 'white' sx={{
}} /> color: "white",
<Typography>POLL embed</Typography> }}
/>
<Typography>POLL embed</Typography>
</Box> </Box>
<Box <Box
sx={{ sx={{
@ -340,25 +450,24 @@ export const PollCard = ({
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
width: "100%", width: "100%",
alignItems: 'center' alignItems: "center",
}} }}
> >
{!isOpen && !errorMsg && ( {!isOpen && !errorMsg && (
<> <>
<Spacer height="5px" /> <Spacer height="5px" />
<Button <Button
size="small" size="small"
variant="contained" variant="contained"
sx={{ sx={{
backgroundColor: 'var(--green)', backgroundColor: "var(--green)",
}} }}
onClick={() => { onClick={() => {
setIsOpen(true); setIsOpen(true);
}} }}
>
> Show poll
Show poll </Button>
</Button>
</> </>
)} )}
{isLoadingParent && isOpen && ( {isLoadingParent && isOpen && (
@ -385,7 +494,7 @@ export const PollCard = ({
<Typography <Typography
sx={{ sx={{
fontSize: "14px", fontSize: "14px",
color: 'var(--unread)' color: "var(--unread)",
}} }}
> >
{errorMsg} {errorMsg}
@ -551,3 +660,263 @@ const PollResults = ({ votes }) => {
</Box> </Box>
); );
}; };
export const ImageCard = ({
image,
fetchImage,
owner,
setInfoSnack,
setOpenSnack,
refresh,
openExternal,
external,
isLoadingParent,
errorMsg,
}) => {
const [isOpen, setIsOpen] = useState(false);
useEffect(() => {
if (isOpen) {
fetchImage();
}
}, [isOpen]);
return (
<Card
sx={{
backgroundColor: "#1F2023",
height: isOpen ? "auto" : "150px",
}}
>
<Box
sx={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
padding: "16px 16px 0px 16px",
}}
>
<Box
sx={{
display: "flex",
alignItems: "center",
gap: "10px",
}}
>
<ImageIcon
sx={{
color: "white",
}}
/>
<Typography>IMAGE embed</Typography>
</Box>
<Box
sx={{
display: "flex",
alignItems: "center",
gap: "10px",
}}
>
<ButtonBase>
<RefreshIcon
onClick={refresh}
sx={{
fontSize: "24px",
color: "white",
}}
/>
</ButtonBase>
{external && (
<ButtonBase>
<OpenInNewIcon
onClick={openExternal}
sx={{
fontSize: "24px",
color: "white",
}}
/>
</ButtonBase>
)}
</Box>
</Box>
<Box
sx={{
padding: "8px 16px 8px 16px",
}}
>
<Typography
sx={{
fontSize: "12px",
color: 'white'
}}
>
Created by {owner}
</Typography>
<Typography
sx={{
fontSize: "12px",
color: 'cadetblue'
}}
>
Not encrypted
</Typography>
</Box>
<Divider sx={{ borderColor: "rgb(255 255 255 / 10%)" }} />
<Box
sx={{
display: "flex",
flexDirection: "column",
width: "100%",
alignItems: "center",
}}
>
{!isOpen && !errorMsg && (
<>
<Spacer height="5px" />
<Button
size="small"
variant="contained"
sx={{
backgroundColor: "var(--green)",
}}
onClick={() => {
setIsOpen(true);
}}
>
Show image
</Button>
</>
)}
{isLoadingParent && isOpen && (
<Box
sx={{
width: "100%",
display: "flex",
justifyContent: "center",
}}
>
{" "}
<CustomLoader />{" "}
</Box>
)}
{errorMsg && (
<Box
sx={{
width: "100%",
display: "flex",
justifyContent: "center",
}}
>
{" "}
<Typography
sx={{
fontSize: "14px",
color: "var(--unread)",
}}
>
{errorMsg}
</Typography>{" "}
</Box>
)}
</Box>
<Box
sx={{
display: isOpen ? "block" : "none",
}}
>
<CardContent>
<ImageViewer src={image} />
</CardContent>
</Box>
</Card>
);
};
export function ImageViewer({ src, alt = "" }) {
const [isFullscreen, setIsFullscreen] = useState(false);
const handleOpenFullscreen = () => setIsFullscreen(true);
const handleCloseFullscreen = () => setIsFullscreen(false);
return (
<>
{/* Image in container */}
<Box
sx={{
maxWidth: "100%", // Prevent horizontal overflow
display: "flex",
justifyContent: "center",
cursor: "pointer",
}}
onClick={handleOpenFullscreen}
>
<img
src={src}
alt={alt}
style={{
maxWidth: "100%",
maxHeight: "400px", // Adjust max height for small containers
objectFit: "contain", // Preserve aspect ratio
}}
/>
</Box>
{/* Fullscreen Viewer */}
<Dialog
open={isFullscreen}
onClose={handleCloseFullscreen}
maxWidth="lg"
fullWidth
fullScreen
sx={{
"& .MuiDialog-paper": {
margin: 0,
maxWidth: "100%",
width: "100%",
height: "100vh",
overflow: "hidden", // Prevent scrollbars
},
}}
>
<Box
sx={{
position: "relative",
width: "100%",
height: "100%",
display: "flex",
justifyContent: "center",
alignItems: "center",
backgroundColor: "#000", // Optional: dark background for fullscreen mode
}}
>
{/* Close Button */}
<IconButton
onClick={handleCloseFullscreen}
sx={{
position: "absolute",
top: 8,
right: 8,
zIndex: 10,
color: "white",
}}
>
<CloseIcon />
</IconButton>
{/* Fullscreen Image */}
<img
src={src}
alt={alt}
style={{
maxWidth: "100%",
maxHeight: "100%",
objectFit: "contain", // Preserve aspect ratio
}}
/>
</Box>
</Dialog>
</>
);
}

View File

@ -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 const encode = (value) => encodeURIComponent(value.trim()); // Helper to encode values
export const createAndCopyEmbedLink = async (data, isFromExtension) => { export const createAndCopyEmbedLink = async (data, isFromExtension) => {
const requiredFields = [ const requiredFields = [
"type", "type",
"name"
]; ];
const missingFields: string[] = []; const missingFields: string[] = [];
requiredFields.forEach((field) => { requiredFields.forEach((field) => {
@ -3078,6 +3091,11 @@ export const createAndCopyEmbedLink = async (data, isFromExtension) => {
switch (data.type) { switch (data.type) {
case "POLL": { case "POLL": {
missingFieldsFunc(data, [
"type",
"name"
])
const queryParams = [ const queryParams = [
`name=${encode(data.name)}`, `name=${encode(data.name)}`,
data.ref ? `ref=${encode(data.ref)}` : null, // Add only if ref exists data.ref ? `ref=${encode(data.ref)}` : null, // Add only if ref exists
@ -3103,6 +3121,41 @@ export const createAndCopyEmbedLink = async (data, isFromExtension) => {
}); });
return link; 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: default:
throw new Error('Invalid type') throw new Error('Invalid type')
} }