mirror of
https://github.com/Qortal/qortal-mobile.git
synced 2025-03-15 04:12:32 +00:00
added image embed
This commit is contained in:
parent
60141ad390
commit
9f12aa4813
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
@ -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')
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user