Authenticated Media Support (#51)

* Fix AvatarField to work with authenticated media

* Fix ViewMediaButton to work for user's media tab and reported events

* remove console.log

* cleanup AvatarField

* use correct thumbnail size

* fix AvatarField.test

* ignore postgres data for watchman

* fix new avatar preview

* watchman should ignore testdata completely, instead of specific subdirs

* update README

* change user's media icon in sidebar - use the same icon as the media tab

* Add preview for user media files if mimeType is image/*

* Add new line in user media Dialog
This commit is contained in:
Borislav Pantaleev
2024-10-03 00:38:35 +03:00
committed by GitHub
parent 470f1b5455
commit a79c3597d6
14 changed files with 259 additions and 83 deletions

View File

@@ -1,6 +1,7 @@
import { get } from "lodash";
import { useState } from "react";
import Typography from "@mui/material/Typography";
import BlockIcon from "@mui/icons-material/Block";
import IconCancel from "@mui/icons-material/Cancel";
import ClearIcon from "@mui/icons-material/Clear";
@@ -8,7 +9,10 @@ import DeleteSweepIcon from "@mui/icons-material/DeleteSweep";
import FileOpenIcon from "@mui/icons-material/FileOpen";
import LockIcon from "@mui/icons-material/Lock";
import LockOpenIcon from "@mui/icons-material/LockOpen";
import { Box, Dialog, DialogContent, DialogContentText, DialogTitle, Tooltip } from "@mui/material";
import IconButton from '@mui/material/IconButton';
import CloseIcon from '@mui/icons-material/Close';
import ZoomInIcon from '@mui/icons-material/ZoomIn';
import { Box, Dialog, DialogContent, DialogContentText, DialogTitle, Tooltip, Link } from "@mui/material";
import { alpha, useTheme } from "@mui/material/styles";
import {
BooleanInput,
@@ -29,12 +33,11 @@ import {
useTranslate,
} from "react-admin";
import { useMutation } from "@tanstack/react-query";
import { Link } from "react-router-dom";
import { dateParser } from "./date";
import { DeleteMediaParams, SynapseDataProvider } from "../synapse/dataProvider";
import { getMediaUrl } from "../synapse/synapse";
import storage from "../storage";
import { fetchAuthenticatedMedia } from "../utils/fetchMedia";
const DeleteMediaDialog = ({ open, onClose, onSubmit }) => {
const translate = useTranslate();
@@ -311,48 +314,125 @@ export const QuarantineMediaButton = (props: ButtonProps) => {
);
};
export const ViewMediaButton = ({ media_id, label }) => {
export const ViewMediaButton = ({ mxcURL, uploadName, label }) => {
const translate = useTranslate();
const url = getMediaUrl(media_id);
const [open, setOpen] = useState(false);
const [blobURL, setBlobURL] = useState("");
const handleOpen = () => setOpen(true);
const handleClose = () => {
setOpen(false);
if (blobURL) {
URL.revokeObjectURL(blobURL);
}
};
const forceDownload = (url: string, filename: string) => {
const anchorElement = document.createElement("a");
anchorElement.href = url;
anchorElement.download = filename;
document.body.appendChild(anchorElement);
anchorElement.click();
document.body.removeChild(anchorElement);
URL.revokeObjectURL(blobURL);
};
const handleFile = async () => {
const response = await fetchAuthenticatedMedia(mxcURL, "original");
const blob = await response.blob();
const blobURL = URL.createObjectURL(blob);
setBlobURL(blobURL);
const mimeType = blob.type;
if (!mimeType.startsWith("image/")) {
forceDownload(blobURL, uploadName);
} else {
handleOpen();
}
};
return (
<Box style={{ whiteSpace: "pre" }}>
<Tooltip title={translate("resources.users_media.action.open")}>
<span>
<>
<Box style={{ whiteSpace: "pre" }}>
<Tooltip title={translate("resources.users_media.action.open")}>
<span>
<Button
component={Link}
to={url}
target="_blank"
rel="noopener"
style={{ minWidth: 0, paddingLeft: 0, paddingRight: 0 }}
onClick={() => handleFile()}
style={{ minWidth: 0, paddingLeft: 0, paddingRight: 0 }}
>
<FileOpenIcon />
</Button>
</span>
</Tooltip>
{label}
</Box>
<Dialog
open={open}
onClose={handleClose}
aria-labelledby="image-modal-title"
aria-describedby="image-modal-description"
style={{ maxWidth: "100%", maxHeight: "100%" }}
>
<DialogTitle id="image-modal-title">
<Typography>{uploadName}</Typography>
<IconButton
aria-label="close"
onClick={handleClose}
sx={(theme) => ({
position: 'absolute',
right: 8,
top: 8,
color: theme.palette.grey[500],
})}
>
<FileOpenIcon />
</Button>
</span>
</Tooltip>
{label}
</Box>
<CloseIcon />
</IconButton>
</DialogTitle>
<DialogContent>
<Link href={blobURL} target="_blank">
<img src={blobURL} alt={uploadName}
style={{ maxWidth: "100%", maxHeight: "/calc(100vh - 64px)", objectFit: "contain" }}
/>
<br />
<ZoomInIcon />
</Link>
</DialogContent>
</Dialog>
</>
);
};
export const MediaIDField = ({ source }) => {
const record = useRecordContext();
if (!record) {
return null;
}
const homeserver = storage.getItem("home_server");
const record = useRecordContext();
if (!record) return null;
const src = get(record, source)?.toString();
if (!src) return null;
const mediaID = get(record, source)?.toString();
if (!mediaID) {
return null;
}
return <ViewMediaButton media_id={`${homeserver}/${src}`} label={src} />;
const mxcURL = `mxc://${homeserver}/${mediaID}`;
const uploadName = decodeURIComponent(get(record, "upload_name")?.toString());
return <ViewMediaButton mxcURL={mxcURL} uploadName={uploadName} label={mediaID} />;
};
export const MXCField = ({ source }) => {
export const ReportMediaContent = ({ source }) => {
const record = useRecordContext();
if (!record) return null;
if (!record) {
return null;
}
const src = get(record, source)?.toString();
if (!src) return null;
const mxcURL = get(record, source)?.toString();
if (!mxcURL) {
return null;
}
const media_id = src.replace("mxc://", "");
const uploadName = decodeURIComponent(record.event_json.content.body);
return <ViewMediaButton media_id={media_id} label={src} />;
return <ViewMediaButton mxcURL={mxcURL} uploadName={uploadName} label={mxcURL} />;
};