added image embed

This commit is contained in:
PhilReact 2024-11-26 12:01:50 +02:00
parent 60141ad390
commit 9f12aa4813
2 changed files with 458 additions and 29 deletions

View File

@ -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' && (
<ImageCard
image={imageUrl}
owner={parsedData?.name}
fetchImage={fetchImage}
refresh={fetchImage}
setInfoSnack={setInfoSnack}
setOpenSnack={setOpenSnack}
external={external}
openExternal={openExternal}
isLoadingParent={isLoading}
errorMsg={errorMsg}
/>
)}
<CustomizedSnackbars
duration={2000}
open={openSnack}
@ -259,11 +372,12 @@ export const PollCard = ({
}
}, [poll?.info?.owner]);
return (
<Card
sx={{
backgroundColor: "#1F2023",
height: isOpen ? 'auto' : "150px",
height: isOpen ? "auto" : "150px",
}}
>
<Box
@ -274,17 +388,19 @@ export const PollCard = ({
padding: "16px 16px 0px 16px",
}}
>
<Box
<Box
sx={{
display: "flex",
alignItems: "center",
gap: "10px",
}}
>
<PollIcon sx={{
color: 'white'
}} />
<Typography>POLL embed</Typography>
<PollIcon
sx={{
color: "white",
}}
/>
<Typography>POLL embed</Typography>
</Box>
<Box
sx={{
@ -334,25 +450,24 @@ export const PollCard = ({
display: "flex",
flexDirection: "column",
width: "100%",
alignItems: 'center'
alignItems: "center",
}}
>
{!isOpen && !errorMsg && (
<>
<Spacer height="5px" />
<Button
size="small"
variant="contained"
sx={{
backgroundColor: 'var(--green)',
}}
onClick={() => {
setIsOpen(true);
}}
>
Show poll
</Button>
<Spacer height="5px" />
<Button
size="small"
variant="contained"
sx={{
backgroundColor: "var(--green)",
}}
onClick={() => {
setIsOpen(true);
}}
>
Show poll
</Button>
</>
)}
{isLoadingParent && isOpen && (
@ -367,7 +482,7 @@ export const PollCard = ({
<CustomLoader />{" "}
</Box>
)}
{errorMsg && (
{errorMsg && (
<Box
sx={{
width: "100%",
@ -379,7 +494,7 @@ export const PollCard = ({
<Typography
sx={{
fontSize: "14px",
color: 'var(--unread)'
color: "var(--unread)",
}}
>
{errorMsg}
@ -545,3 +660,263 @@ const PollResults = ({ votes }) => {
</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

@ -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')
}