diff --git a/README.md b/README.md index d98310c..e75ed84 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,7 @@ * [Fork differences](#fork-differences) * [Changes](#changes) + * [exclusive for etke.cc customers](#exclusive-for-etkecc-customers) * [Development](#development) * [Support](#support) * [Configuration](#configuration) @@ -58,6 +59,8 @@ The full list is described below in the [Changes](#changes) section. ### Changes +_the list will be updated as new changes are added_ + The following changes are already implemented: * 🛑 [Prevent admins from deleting themselves](https://github.com/etkecc/synapse-admin/pull/1) @@ -101,10 +104,14 @@ The following changes are already implemented: * 🖼️ [Add rooms' avatars](https://github.com/etkecc/synapse-admin/pull/158) * 🤖 [User Badges](https://github.com/etkecc/synapse-admin/pull/160) * 🔑 [Allow prefilling any fields on the login form via GET params](https://github.com/etkecc/synapse-admin/pull/181) -* _(for [etke.cc](https://etke.cc) customers only)_ [Server Status indicator and page](https://github.com/etkecc/synapse-admin/pull/182) +* 🖼️ [Add "Media" tab for rooms](https://github.com/etkecc/synapse-admin/pull/196) +#### exclusive for [etke.cc](https://etke.cc) customers -_the list will be updated as new changes are added_ +We at [etke.cc](https://etke.cc) attempting to develop everything open-source, but some things are too specific to be used by anyone else. +The following list contains such features - they are only available for [etke.cc](https://etke.cc) customers. + +* 📊 [Server Status indicator and page](https://github.com/etkecc/synapse-admin/pull/182) ### Development diff --git a/src/App.tsx b/src/App.tsx index ab661bf..f24bf1c 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -2,7 +2,7 @@ import { merge } from "lodash"; import polyglotI18nProvider from "ra-i18n-polyglot"; import { Admin, CustomRoutes, Resource, resolveBrowserLocale } from "react-admin"; -import { createContext, useContext } from "react"; + import { Route } from "react-router-dom"; import AdminLayout from "./components/AdminLayout"; @@ -22,9 +22,8 @@ import rooms from "./resources/rooms"; import userMediaStats from "./resources/user_media_statistics"; import users from "./resources/users"; import authProvider from "./synapse/authProvider"; -import dataProvider, { ServerStatusResponse } from "./synapse/dataProvider"; +import dataProvider from "./synapse/dataProvider"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; -import { Config } from "./utils/config"; import ServerStatusPage from "./components/etke.cc/ServerStatusPage"; // TODO: Can we use lazy loading together with browser locale? @@ -88,8 +87,4 @@ export const App = () => ( ); -export const AppContext = createContext({}); - -export const useAppContext = () => useContext(AppContext) as Config; - export default App; diff --git a/src/Context.tsx b/src/Context.tsx new file mode 100644 index 0000000..8d1c837 --- /dev/null +++ b/src/Context.tsx @@ -0,0 +1,7 @@ +import { createContext, useContext } from "react"; + +import { Config } from "./utils/config"; + +export const AppContext = createContext({} as Config); + +export const useAppContext = () => useContext(AppContext) as Config; \ No newline at end of file diff --git a/src/components/etke.cc/ServerStatusBadge.tsx b/src/components/etke.cc/ServerStatusBadge.tsx index 2c65d82..4d0a461 100644 --- a/src/components/etke.cc/ServerStatusBadge.tsx +++ b/src/components/etke.cc/ServerStatusBadge.tsx @@ -1,6 +1,6 @@ import { Avatar, Badge, Theme, Tooltip } from "@mui/material"; import { useEffect } from "react"; -import { useAppContext } from "../../App"; +import { useAppContext } from "../../Context"; import { Button, useDataProvider, useStore } from "react-admin"; import { styled } from '@mui/material/styles'; import MonitorHeartIcon from '@mui/icons-material/MonitorHeart'; diff --git a/src/components/media.tsx b/src/components/media.tsx index 27943e5..63b729c 100644 --- a/src/components/media.tsx +++ b/src/components/media.tsx @@ -314,6 +314,7 @@ export const QuarantineMediaButton = (props: ButtonProps) => { export const ViewMediaButton = ({ mxcURL, label, uploadName, mimetype }) => { const translate = useTranslate(); const [loading, setLoading] = useState(false); + const notify = useNotify(); const isImage = mimetype && mimetype.startsWith("image/"); const openFileInNewTab = (blobURL: string) => { @@ -340,12 +341,25 @@ export const ViewMediaButton = ({ mxcURL, label, uploadName, mimetype }) => { const handleFile = async (preview: boolean) => { setLoading(true); const response = await fetchAuthenticatedMedia(mxcURL, "original"); - const blob = await response.blob(); - const blobURL = URL.createObjectURL(blob); - if (preview) { - openFileInNewTab(blobURL); + + if (response.ok) { + const blob = await response.blob(); + const blobURL = URL.createObjectURL(blob); + if (preview) { + openFileInNewTab(blobURL); + } else { + downloadFile(blobURL); + } } else { - downloadFile(blobURL); + const body = await response.json(); + notify("resources.room_media.action.error", { + messageArgs: { + errcode: body.errcode, + errstatus: response.status, + error: body.error, + }, + type: "error", + }); } setLoading(false); }; @@ -391,8 +405,16 @@ export const MediaIDField = ({ source }) => { return null; } - const uploadName = decodeURIComponent(get(record, "upload_name")?.toString()); - const mxcURL = `mxc://${homeserver}/${mediaID}`; + let uploadName = mediaID; + if (get(record, "upload_name")) { + uploadName = decodeURIComponent(get(record, "upload_name")?.toString()); + } + + let mxcURL = mediaID; + if (!mediaID.startsWith(`mxc://${homeserver}`)) { + // this is user's media, where mediaID doesn't have the mxc://home_server/ prefix as it has in the rooms + mxcURL = `mxc://${homeserver}/${mediaID}`; + } return ; }; diff --git a/src/i18n/de.ts b/src/i18n/de.ts index e26b190..e72f8b1 100644 --- a/src/i18n/de.ts +++ b/src/i18n/de.ts @@ -64,6 +64,7 @@ const de: SynapseTranslationMessages = { members: "Mitglieder", detail: "Details", permission: "Berechtigungen", + media: "Medien", }, }, reports: { tabs: { basic: "Allgemein", detail: "Details" } }, @@ -432,6 +433,18 @@ const de: SynapseTranslationMessages = { sender: "Absender", }, }, + room_media: { + name: "Medien", + fields: { + media_id: "Medien ID", + }, + helper: { + info: "Dies ist eine Liste der Medien, die in den Raum hochgeladen wurden. Es ist nicht möglich, Medien zu löschen, die in externen Medien-Repositories hochgeladen wurden.", + }, + action: { + error: "%{errcode} (%{errstatus}) %{error}" + } + }, room_directory: { name: "Raumverzeichnis", fields: { diff --git a/src/i18n/en.ts b/src/i18n/en.ts index 9dc08c5..f355712 100644 --- a/src/i18n/en.ts +++ b/src/i18n/en.ts @@ -38,6 +38,7 @@ const en: SynapseTranslationMessages = { members: "Members", detail: "Details", permission: "Permissions", + media: "Media", } }, reports: { tabs: { basic: "Basic", detail: "Details" } }, @@ -404,6 +405,18 @@ const en: SynapseTranslationMessages = { sender: "Sender", }, }, + room_media: { + name: "Media", + fields: { + media_id: "Media ID", + }, + helper: { + info: "This is a list of media that has been uploaded to the room. It is not possible to delete media that has been uploaded to external media repositories.", + }, + action: { + error: "%{errcode} (%{errstatus}) %{error}", + }, + }, room_directory: { name: "Room directory", fields: { diff --git a/src/i18n/fa.ts b/src/i18n/fa.ts index 0308b9c..5dc1ff3 100644 --- a/src/i18n/fa.ts +++ b/src/i18n/fa.ts @@ -32,6 +32,7 @@ const fa: SynapseTranslationMessages = { members: "اعضا", detail: "جزئیات", permission: "مجوزها", + media: "رسانه ها", }, }, reports: { tabs: { basic: "اصلی", detail: "جزئیات" } }, @@ -384,6 +385,18 @@ const fa: SynapseTranslationMessages = { sender: "فرستنده", }, }, + room_media: { + name: "رسانه ها", + fields: { + media_id: "شناسه رسانه", + }, + helper: { + info: "این یک لیست از رسانه ها است که در اتاق بارگذاری شده است. نمی توان رسانه ها را حذف کرد که در اتاق های خارجی بارگذاری شده اند.", + }, + action: { + error: "%{errcode} (%{errstatus}) %{error}" + } + }, room_directory: { name: "راهنمای اتاق", fields: { diff --git a/src/i18n/fr.ts b/src/i18n/fr.ts index 12993fe..218e2b5 100644 --- a/src/i18n/fr.ts +++ b/src/i18n/fr.ts @@ -32,6 +32,7 @@ const fr: SynapseTranslationMessages = { members: "Membres", detail: "Détails", permission: "Permissions", + media: "Médias", }, }, reports: { tabs: { basic: "Informations de base", detail: "Détails" } }, @@ -386,6 +387,18 @@ const fr: SynapseTranslationMessages = { sender: "Expéditeur", }, }, + room_media: { + name: "Médias", + fields: { + media_id: "Identifiant du média", + }, + helper: { + info: "Cette liste contient les médias qui ont été téléchargés dans le salon. Il n'est pas possible de supprimer les médias qui ont été téléversés dans des dépôts de médias externes.", + }, + action: { + error: "%{errcode} (%{errstatus}) %{error}" + } + }, room_directory: { name: "Répertoire des salons", fields: { diff --git a/src/i18n/index.d.ts b/src/i18n/index.d.ts index c69b945..f124718 100644 --- a/src/i18n/index.d.ts +++ b/src/i18n/index.d.ts @@ -31,6 +31,7 @@ interface SynapseTranslationMessages extends TranslationMessages { members: string; detail: string; permission: string; + media: string; }; }; reports: { tabs: { basic: string; detail: string } }; @@ -396,6 +397,18 @@ interface SynapseTranslationMessages extends TranslationMessages { sender: string; }; }; + room_media?: { + name: string; + fields: { + media_id: string; + }; + helper: { + info: string; + }; + action: { + error: string; + }; + }; room_directory?: { name: string; fields: { diff --git a/src/i18n/it.ts b/src/i18n/it.ts index 8a66b6d..abd8b0d 100644 --- a/src/i18n/it.ts +++ b/src/i18n/it.ts @@ -32,6 +32,7 @@ const it: SynapseTranslationMessages = { members: "Membro", detail: "Dettagli", permission: "Permessi", + media: "Media", }, }, reports: { tabs: { basic: "Semplice", detail: "Dettagli" } }, @@ -379,6 +380,18 @@ const it: SynapseTranslationMessages = { sender: "Mittente", }, }, + room_media: { + name: "Media", + fields: { + media_id: "ID Media", + }, + helper: { + info: "Questo è un elenco dei media caricati nella stanza. Non è possibile eliminare i media caricati su repository esterni.", + }, + action: { + error: "%{errcode} (%{errstatus}) %{error}" + } + }, room_directory: { name: "Elenco delle stanze", fields: { diff --git a/src/i18n/ru.ts b/src/i18n/ru.ts index 5dcdd8c..e669c1f 100644 --- a/src/i18n/ru.ts +++ b/src/i18n/ru.ts @@ -59,6 +59,7 @@ const ru: SynapseTranslationMessages = { members: "Участники", detail: "Подробности", permission: "Права доступа", + media: "Медиа", }, }, reports: { tabs: { basic: "Основные", detail: "Подробности" } }, @@ -437,6 +438,18 @@ const ru: SynapseTranslationMessages = { sender: "Отправитель", }, }, + room_media: { + name: "Медиа", + fields: { + media_id: "ID медиа", + }, + helper: { + info: "Это список медиа, которые были загружены в комнату. Невозможно удалить медиа, которые были загружены в внешние медиа-репозитории.", + }, + action: { + error: "%{errcode} (%{errstatus}) %{error}" + } + }, room_directory: { name: "Каталог комнат", fields: { diff --git a/src/i18n/zh.ts b/src/i18n/zh.ts index 4f3b19a..a9d6e09 100644 --- a/src/i18n/zh.ts +++ b/src/i18n/zh.ts @@ -60,6 +60,7 @@ const zh: SynapseTranslationMessages = { members: "成员", detail: "细节", permission: "权限", + media: "媒体", }, }, reports: { tabs: { basic: "基本", detail: "细节" } }, @@ -348,6 +349,18 @@ const zh: SynapseTranslationMessages = { media_length: "媒体文件长度", }, }, + room_media: { + name: "媒体", + fields: { + media_id: "媒体ID", + }, + helper: { + info: "这是上传到房间的媒体列表。无法删除上传到外部媒体存储库的媒体。", + }, + action: { + error: "%{errcode} (%{errstatus}) %{error}" + } + }, }, }; export default zh; diff --git a/src/index.tsx b/src/index.tsx index b620cd4..f19c1c5 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -2,7 +2,8 @@ import React from "react"; import { createRoot } from "react-dom/client"; -import {App, AppContext } from "./App"; +import { App } from "./App"; +import { AppContext } from "./Context"; import { FetchConfig, GetConfig } from "./utils/config"; await FetchConfig(); diff --git a/src/pages/LoginPage.test.tsx b/src/pages/LoginPage.test.tsx index 0adca70..c17cd53 100644 --- a/src/pages/LoginPage.test.tsx +++ b/src/pages/LoginPage.test.tsx @@ -4,7 +4,7 @@ import { render, screen } from "@testing-library/react"; import { AdminContext } from "react-admin"; import LoginPage from "./LoginPage"; -import { AppContext } from "../App"; +import { AppContext } from "../Context"; import englishMessages from "../i18n/en"; const i18nProvider = polyglotI18nProvider(() => englishMessages, "en", [{ locale: "en", name: "English" }]); @@ -33,7 +33,9 @@ describe("LoginForm", () => { it("renders with single restricted homeserver", () => { render( - + @@ -56,6 +58,8 @@ describe("LoginForm", () => { diff --git a/src/pages/LoginPage.tsx b/src/pages/LoginPage.tsx index 7da9855..febb038 100644 --- a/src/pages/LoginPage.tsx +++ b/src/pages/LoginPage.tsx @@ -16,7 +16,7 @@ import { } from "react-admin"; import { useFormContext } from "react-hook-form"; import LoginFormBox from "../components/LoginFormBox"; -import { useAppContext } from "../App"; +import { useAppContext } from "../Context"; import { getServerVersion, getSupportedFeatures, diff --git a/src/resources/rooms.tsx b/src/resources/rooms.tsx index 4cf53e9..2c7ae85 100644 --- a/src/resources/rooms.tsx +++ b/src/resources/rooms.tsx @@ -7,12 +7,12 @@ import PageviewIcon from "@mui/icons-material/Pageview"; import ViewListIcon from "@mui/icons-material/ViewList"; import RoomIcon from "@mui/icons-material/ViewList"; import VisibilityIcon from "@mui/icons-material/Visibility"; +import PermMediaIcon from "@mui/icons-material/PermMedia"; import Box from "@mui/material/Box"; import { useTheme } from "@mui/material/styles"; import { BooleanField, DateField, - EditButton, WrapperField, Datagrid, DatagridConfigurable, @@ -38,6 +38,7 @@ import { useTranslate, useListContext, useNotify, + DeleteButton, } from "react-admin"; import TextField from "@mui/material/TextField"; @@ -49,6 +50,7 @@ import { } from "./room_directory"; import { DATE_FORMAT } from "../utils/date"; import DeleteRoomButton from "../components/DeleteRoomButton"; +import { MediaIDField } from "../components/media"; import AvatarField from "../components/AvatarField"; import { Room } from "../synapse/dataProvider"; import { useMutation } from "@tanstack/react-query"; @@ -58,6 +60,8 @@ import { useState } from "react"; import Button from "@mui/material/Button"; import PersonIcon from '@mui/icons-material/Person'; import Typography from "@mui/material/Typography"; +import Alert from "@mui/material/Alert"; + const RoomPagination = () => ; const RoomTitle = () => { @@ -231,6 +235,16 @@ export const RoomShow = (props: ShowProps) => { + } path="media"> + {translate("resources.room_media.helper.info")} + + + + + + + + } path="permission"> diff --git a/src/synapse/dataProvider.ts b/src/synapse/dataProvider.ts index 345012b..898c628 100644 --- a/src/synapse/dataProvider.ts +++ b/src/synapse/dataProvider.ts @@ -385,6 +385,20 @@ const resourceMap = { data: "members", total: json => json.total, }, + room_media: { + map: (mediaId: string) => ({ + id: mediaId.replace("mxc://" + localStorage.getItem("home_server") + "/", ""), + media_id: mediaId.replace("mxc://" + localStorage.getItem("home_server") + "/", ""), + }), + reference: (id: Identifier) => ({ + endpoint: `/_synapse/admin/v1/room/${id}/media`, + }), + total: json => json.total, + data: "local", + delete: (params: DeleteParams) => ({ + endpoint: `/_synapse/admin/v1/media/${localStorage.getItem("home_server")}/${params.id}`, + }), + }, room_state: { map: (rs: RoomState) => ({ ...rs,