mirror of
https://github.com/etkecc/synapse-admin.git
synced 2024-11-21 07:15:20 +03:00
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:
parent
470f1b5455
commit
a79c3597d6
14 changed files with 259 additions and 83 deletions
5
.watchmanconfig
Normal file
5
.watchmanconfig
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"ignore_dirs": [
|
||||
"testdata"
|
||||
]
|
||||
}
|
|
@ -61,6 +61,7 @@ The following changes are already implemented:
|
|||
* [Restrict actions on specific users](https://github.com/etkecc/synapse-admin/pull/42)
|
||||
* [Add `Contact support` menu item](https://github.com/etkecc/synapse-admin/pull/45)
|
||||
* [Provide options to delete media and redact events on user erase](https://github.com/etkecc/synapse-admin/pull/49)
|
||||
* [Authenticated Media support](https://github.com/etkecc/synapse-admin/pull/51)
|
||||
|
||||
_the list will be updated as new changes are added_
|
||||
|
||||
|
|
|
@ -1,18 +1,43 @@
|
|||
import { render, screen } from "@testing-library/react";
|
||||
import { render, screen, waitFor } from "@testing-library/react";
|
||||
import { RecordContextProvider } from "react-admin";
|
||||
|
||||
import { act } from "react";
|
||||
import AvatarField from "./AvatarField";
|
||||
|
||||
describe("AvatarField", () => {
|
||||
it("shows image", () => {
|
||||
beforeEach(() => {
|
||||
// Mock fetch
|
||||
global.fetch = jest.fn(() =>
|
||||
Promise.resolve({
|
||||
blob: () => Promise.resolve(new Blob(["mock image data"], { type: 'image/jpeg' })),
|
||||
})
|
||||
) as jest.Mock;
|
||||
|
||||
// Mock URL.createObjectURL
|
||||
global.URL.createObjectURL = jest.fn(() => "mock-object-url");
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
it.only("shows image", async () => {
|
||||
const value = {
|
||||
avatar: "foo",
|
||||
avatar: "mxc://serverName/mediaId",
|
||||
};
|
||||
render(
|
||||
<RecordContextProvider value={value}>
|
||||
<AvatarField source="avatar" />
|
||||
</RecordContextProvider>
|
||||
);
|
||||
expect(screen.getByRole("img").getAttribute("src")).toBe("foo");
|
||||
|
||||
await act(async () => {
|
||||
render(
|
||||
<RecordContextProvider value={value}>
|
||||
<AvatarField source="avatar" />
|
||||
</RecordContextProvider>
|
||||
);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
const img = screen.getByRole("img");
|
||||
expect(img.getAttribute("src")).toBe("mock-object-url");
|
||||
});
|
||||
|
||||
expect(global.fetch).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,12 +1,37 @@
|
|||
import { get } from "lodash";
|
||||
|
||||
import { Avatar } from "@mui/material";
|
||||
import { Avatar, AvatarProps } from "@mui/material";
|
||||
import { useRecordContext } from "react-admin";
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { fetchAuthenticatedMedia } from "../utils/fetchMedia";
|
||||
|
||||
const AvatarField = ({ source, ...rest }) => {
|
||||
const record = useRecordContext(rest);
|
||||
const src = get(record, source)?.toString();
|
||||
const AvatarField = ({ source, ...rest }: AvatarProps & { source: string, label?: string }) => {
|
||||
const { alt, classes, sizes, sx, variant } = rest;
|
||||
|
||||
const record = useRecordContext(rest);
|
||||
const mxcURL = get(record, source)?.toString();
|
||||
|
||||
const [src, setSrc] = useState<string>("");
|
||||
|
||||
const fetchAvatar = useCallback(async (mxcURL: string) => {
|
||||
const response = await fetchAuthenticatedMedia(mxcURL, "thumbnail");
|
||||
const blob = await response.blob();
|
||||
const blobURL = URL.createObjectURL(blob);
|
||||
setSrc(blobURL);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (mxcURL) {
|
||||
fetchAvatar(mxcURL);
|
||||
}
|
||||
|
||||
// Cleanup function to revoke the object URL
|
||||
return () => {
|
||||
if (src) {
|
||||
URL.revokeObjectURL(src);
|
||||
}
|
||||
};
|
||||
}, [mxcURL, fetchAvatar]);
|
||||
|
||||
return <Avatar alt={alt} classes={classes} sizes={sizes} src={src} sx={sx} variant={variant} />;
|
||||
};
|
||||
|
||||
|
|
|
@ -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} />;
|
||||
};
|
||||
|
|
|
@ -22,7 +22,7 @@ import {
|
|||
} from "react-admin";
|
||||
|
||||
import { DATE_FORMAT } from "../components/date";
|
||||
import { MXCField } from "../components/media";
|
||||
import { ReportMediaContent } from "../components/media";
|
||||
|
||||
const ReportPagination = () => <Pagination rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} />;
|
||||
|
||||
|
@ -62,7 +62,7 @@ export const ReportShow = (props: ShowProps) => {
|
|||
<TextField source="event_json.content.msgtype" />
|
||||
<TextField source="event_json.content.body" />
|
||||
<TextField source="event_json.content.info.mimetype" />
|
||||
<MXCField source="event_json.content.url" />
|
||||
<ReportMediaContent source="event_json.content.url" />
|
||||
<TextField source="event_json.content.format" />
|
||||
<TextField source="event_json.content.formatted_body" />
|
||||
<TextField source="event_json.content.algorithm" />
|
||||
|
|
|
@ -26,9 +26,9 @@ import {
|
|||
useUnselectAll,
|
||||
} from "react-admin";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
|
||||
import AvatarField from "../components/AvatarField";
|
||||
|
||||
|
||||
const RoomDirectoryPagination = () => <Pagination rowsPerPageOptions={[100, 500, 1000, 2000]} />;
|
||||
|
||||
export const RoomDirectoryUnpublishButton = (props: DeleteButtonProps) => {
|
||||
|
@ -144,7 +144,6 @@ export const RoomDirectoryList = () => (
|
|||
>
|
||||
<AvatarField
|
||||
source="avatar_src"
|
||||
sortable={false}
|
||||
sx={{ height: "40px", width: "40px" }}
|
||||
label="resources.rooms.fields.avatar"
|
||||
/>
|
||||
|
|
|
@ -47,6 +47,7 @@ import {
|
|||
} from "./room_directory";
|
||||
import { DATE_FORMAT } from "../components/date";
|
||||
import DeleteRoomButton from "../components/DeleteRoomButton";
|
||||
import AvatarField from "../components/AvatarField";
|
||||
|
||||
const RoomPagination = () => <Pagination rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} />;
|
||||
|
||||
|
@ -90,6 +91,11 @@ export const RoomShow = (props: ShowProps) => {
|
|||
<Show {...props} actions={<RoomShowActions />} title={<RoomTitle />}>
|
||||
<TabbedShowLayout>
|
||||
<Tab label="synapseadmin.rooms.tabs.basic" icon={<ViewListIcon />}>
|
||||
<AvatarField
|
||||
source="avatar"
|
||||
sx={{ height: "120px", width: "120px" }}
|
||||
label="resources.rooms.fields.avatar"
|
||||
/>
|
||||
<TextField source="room_id" />
|
||||
<TextField source="name" />
|
||||
<TextField source="topic" />
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import EqualizerIcon from "@mui/icons-material/Equalizer";
|
||||
import PermMediaIcon from "@mui/icons-material/PermMedia";
|
||||
import {
|
||||
Datagrid,
|
||||
ExportButton,
|
||||
|
@ -48,7 +48,7 @@ export const UserMediaStatsList = (props: ListProps) => (
|
|||
|
||||
const resource: ResourceProps = {
|
||||
name: "user_media_statistics",
|
||||
icon: EqualizerIcon,
|
||||
icon: PermMediaIcon,
|
||||
list: UserMediaStatsList,
|
||||
};
|
||||
|
||||
|
|
|
@ -56,6 +56,7 @@ import {
|
|||
RaRecord,
|
||||
ImageInput,
|
||||
ImageField,
|
||||
FunctionField,
|
||||
} from "react-admin";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
|
@ -90,11 +91,6 @@ const UserListActions = () => {
|
|||
);
|
||||
};
|
||||
|
||||
UserListActions.defaultProps = {
|
||||
selectedIds: [],
|
||||
onUnselectItems: () => null,
|
||||
};
|
||||
|
||||
const UserPagination = () => <Pagination rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} />;
|
||||
|
||||
const userFilters = [
|
||||
|
@ -165,7 +161,7 @@ export const UserList = (props: ListProps) => (
|
|||
pagination={<UserPagination />}
|
||||
>
|
||||
<Datagrid rowClick={usersRowClick} bulkActionButtons={<UserBulkActionButtons />}>
|
||||
<AvatarField source="avatar_src" sx={{ height: "40px", width: "40px" }} sortBy="avatar_url" />
|
||||
<AvatarField source="avatar_src" sx={{ height: "40px", width: "40px" }} />
|
||||
<TextField source="id" sortBy="name" />
|
||||
<TextField source="displayname" />
|
||||
<BooleanField source="is_guest" />
|
||||
|
@ -323,14 +319,19 @@ export const UserEdit = (props: EditProps) => {
|
|||
<Edit {...props} title={<UserTitle />} actions={<UserEditActions />} mutationMode="pessimistic">
|
||||
<TabbedForm toolbar={<UserEditToolbar />}>
|
||||
<FormTab label={translate("resources.users.name", { smart_count: 1 })} icon={<PersonPinIcon />}>
|
||||
<AvatarField source="avatar_src" sortable={false} sx={{ height: "120px", width: "120px" }} />
|
||||
<AvatarField source="avatar_src" sx={{ height: "120px", width: "120px" }} />
|
||||
<BooleanInput source="avatar_erase" label="resources.users.action.erase_avatar" />
|
||||
<ImageInput
|
||||
source="avatar_file"
|
||||
label="resources.users.fields.avatar"
|
||||
accept={{ "image/*": [".png", ".jpg"] }}
|
||||
>
|
||||
<ImageField source="src" title="Avatar" />
|
||||
<ImageField source="src" title="Avatar" sx={{ '& img': {
|
||||
width: "120px !important",
|
||||
height: "120px !important",
|
||||
objectFit: "cover !important",
|
||||
borderRadius: '50% !important',
|
||||
}}} />
|
||||
</ImageInput>
|
||||
<TextInput source="id" readOnly />
|
||||
<TextInput source="displayname" />
|
||||
|
@ -404,8 +405,8 @@ export const UserEdit = (props: EditProps) => {
|
|||
<DateField source="created_ts" showTime options={DATE_FORMAT} />
|
||||
<DateField source="last_access_ts" showTime options={DATE_FORMAT} />
|
||||
<NumberField source="media_length" />
|
||||
<TextField source="media_type" />
|
||||
<TextField source="upload_name" />
|
||||
<TextField source="media_type" sx={{ display: "block", width: 200, wordBreak: "break-word" }} />
|
||||
<FunctionField source="upload_name" render={record => decodeURIComponent(record.upload_name)} />
|
||||
<TextField source="quarantined_by" />
|
||||
<QuarantineMediaButton label="resources.quarantine_media.action.name" />
|
||||
<ProtectMediaButton label="resources.users_media.fields.safe_from_quarantine" />
|
||||
|
|
|
@ -96,8 +96,13 @@ const authProvider: AuthProvider = {
|
|||
};
|
||||
|
||||
if (typeof access_token === "string") {
|
||||
await fetchUtils.fetchJson(logout_api_url, options);
|
||||
storage.removeItem("access_token");
|
||||
try {
|
||||
await fetchUtils.fetchJson(logout_api_url, options);
|
||||
} catch (err) {
|
||||
console.log("Error logging out", err);
|
||||
} finally {
|
||||
storage.removeItem("access_token");
|
||||
}
|
||||
}
|
||||
},
|
||||
// called when the API returns an error
|
||||
|
|
|
@ -42,17 +42,6 @@ const jsonClient = async (url: string, options: Options = {}) => {
|
|||
}
|
||||
};
|
||||
|
||||
const mxcUrlToHttp = (mxcUrl: string) => {
|
||||
const homeserver = storage.getItem("base_url");
|
||||
const re = /^mxc:\/\/([^/]+)\/(\w+)/;
|
||||
const ret = re.exec(mxcUrl);
|
||||
console.log("mxcClient " + ret);
|
||||
if (ret == null) return null;
|
||||
const serverName = ret[1];
|
||||
const mediaId = ret[2];
|
||||
return `${homeserver}/_matrix/media/r0/thumbnail/${serverName}/${mediaId}?width=24&height=24&method=scale`;
|
||||
};
|
||||
|
||||
const filterUndefined = (obj: Record<string, any>) => {
|
||||
return Object.fromEntries(Object.entries(obj).filter(([key, value]) => value !== undefined));
|
||||
};
|
||||
|
@ -267,10 +256,10 @@ export interface SynapseDataProvider extends DataProvider {
|
|||
const resourceMap = {
|
||||
users: {
|
||||
path: "/_synapse/admin/v2/users",
|
||||
map: (u: User) => ({
|
||||
map: async (u: User) => ({
|
||||
...u,
|
||||
id: returnMXID(u.name),
|
||||
avatar_src: u.avatar_url ? mxcUrlToHttp(u.avatar_url) : undefined,
|
||||
avatar_src: u.avatar_url ? u.avatar_url : undefined,
|
||||
is_guest: !!u.is_guest,
|
||||
admin: !!u.admin,
|
||||
deactivated: !!u.deactivated,
|
||||
|
@ -458,7 +447,7 @@ const resourceMap = {
|
|||
id: rd.room_id,
|
||||
public: !!rd.public,
|
||||
guest_access: !!rd.guest_access,
|
||||
avatar_src: rd.avatar_url ? mxcUrlToHttp(rd.avatar_url) : undefined,
|
||||
avatar_src: rd.avatar_url ? rd.avatar_url : undefined,
|
||||
}),
|
||||
data: "chunk",
|
||||
total: json => json.total_room_count_estimate,
|
||||
|
@ -564,8 +553,10 @@ const baseDataProvider: SynapseDataProvider = {
|
|||
const url = `${endpoint_url}?${new URLSearchParams(filterUndefined(query)).toString()}`;
|
||||
|
||||
const { json } = await jsonClient(url);
|
||||
const formattedData = await json[res.data].map(res.map);
|
||||
|
||||
return {
|
||||
data: json[res.data].map(res.map),
|
||||
data: formattedData,
|
||||
total: res.total(json, from, perPage),
|
||||
};
|
||||
},
|
||||
|
|
|
@ -54,11 +54,6 @@ export const getSupportedLoginFlows = async baseUrl => {
|
|||
return response.json.flows;
|
||||
};
|
||||
|
||||
export const getMediaUrl = media_id => {
|
||||
const baseUrl = storage.getItem("base_url");
|
||||
return `${baseUrl}/_matrix/media/v1/download/${media_id}?allow_redirect=true`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate a random MXID for current homeserver
|
||||
* @returns full MXID as string
|
||||
|
|
43
src/utils/fetchMedia.ts
Normal file
43
src/utils/fetchMedia.ts
Normal file
|
@ -0,0 +1,43 @@
|
|||
import storage from "../storage";
|
||||
|
||||
export const getServerAndMediaIdFromMxcUrl = (mxcUrl: string): { serverName: string, mediaId: string } => {
|
||||
const re = /^mxc:\/\/([^/]+)\/(\w+)/;
|
||||
const ret = re.exec(mxcUrl);
|
||||
console.log("mxcClient " + ret);
|
||||
if (ret == null) {
|
||||
throw new Error("Invalid mxcUrl");
|
||||
}
|
||||
const serverName = ret[1];
|
||||
const mediaId = ret[2];
|
||||
return { serverName, mediaId };
|
||||
};
|
||||
|
||||
export type MediaType = "thumbnail" | "original";
|
||||
|
||||
export const fetchAuthenticatedMedia = async (mxcUrl: string, type: MediaType): Promise<Response> => {
|
||||
const homeserver = storage.getItem("base_url");
|
||||
const accessToken = storage.getItem("access_token");
|
||||
|
||||
const { serverName, mediaId } = getServerAndMediaIdFromMxcUrl(mxcUrl);
|
||||
if (!serverName || !mediaId) {
|
||||
throw new Error("Invalid mxcUrl");
|
||||
}
|
||||
|
||||
let url = "";
|
||||
if (type === "thumbnail") {
|
||||
// ref: https://spec.matrix.org/latest/client-server-api/#thumbnails
|
||||
url = `${homeserver}/_matrix/client/v1/media/thumbnail/${serverName}/${mediaId}?width=320&height=240&method=scale`;
|
||||
} else if (type === "original") {
|
||||
url = `${homeserver}/_matrix/client/v1/media/download/${serverName}/${mediaId}`;
|
||||
} else {
|
||||
throw new Error("Invalid authenticated media type");
|
||||
}
|
||||
|
||||
const response = await fetch(`${url}`, {
|
||||
headers: {
|
||||
authorization: `Bearer ${accessToken}`,
|
||||
},
|
||||
});
|
||||
|
||||
return response;
|
||||
};
|
Loading…
Reference in a new issue