From 1c717d5b8c0cd95d62b8333335c6a889c7a52e98 Mon Sep 17 00:00:00 2001 From: Aine Date: Fri, 20 Mar 2026 23:15:26 +0000 Subject: [PATCH 01/28] make dialog windows full screen on mobile --- src/components/BlockRoomButton.tsx | 14 +++++++++++--- src/components/DeleteRoomButton.tsx | 6 +++++- src/components/DeleteUserButton.tsx | 6 +++++- src/components/LoginAsUserButton.tsx | 8 ++++++-- src/components/PurgeHistoryButton.tsx | 6 +++++- src/components/QuarantineAllMediaButton.tsx | 10 ++++++++-- src/components/ResetPasswordButton.tsx | 6 +++++- src/components/ServerNotices.tsx | 6 +++++- src/components/media.tsx | 9 +++++++-- src/resources/reports.tsx | 4 +++- 10 files changed, 60 insertions(+), 15 deletions(-) diff --git a/src/components/BlockRoomButton.tsx b/src/components/BlockRoomButton.tsx index d13715eb..85f1acf4 100644 --- a/src/components/BlockRoomButton.tsx +++ b/src/components/BlockRoomButton.tsx @@ -10,6 +10,8 @@ import { DialogTitle, TextField, } from "@mui/material"; +import { useTheme } from "@mui/material/styles"; +import useMediaQuery from "@mui/material/useMediaQuery"; import { useEffect, useState } from "react"; import { Button, @@ -30,6 +32,8 @@ import { SynapseDataProvider } from "../providers/types"; * Block requires confirmation modal, unblock is direct. */ export const BlockRoomButton = () => { + const theme = useTheme(); + const fullScreen = useMediaQuery(theme.breakpoints.down("sm")); const record = useRecordContext(); const [open, setOpen] = useState(false); const [blocked, setBlocked] = useState(null); @@ -110,7 +114,7 @@ export const BlockRoomButton = () => { - setOpen(false)} maxWidth="sm" fullWidth> + setOpen(false)} maxWidth="sm" fullWidth fullScreen={fullScreen}> {translate("resources.rooms.action.block.title", { room: roomName })} {translate("resources.rooms.action.block.content")} @@ -138,6 +142,8 @@ export const BlockRoomButton = () => { * Bulk block/unblock buttons for room lists (main room list + joined_rooms). */ export const BlockRoomBulkButton = () => { + const theme = useTheme(); + const fullScreen = useMediaQuery(theme.breakpoints.down("sm")); const { selectedIds } = useListContext(); const [open, setOpen] = useState(false); const [loading, setLoading] = useState(false); @@ -170,7 +176,7 @@ export const BlockRoomBulkButton = () => { - setOpen(false)} maxWidth="sm" fullWidth> + setOpen(false)} maxWidth="sm" fullWidth fullScreen={fullScreen}> {translate("resources.rooms.action.block.title_bulk", { smart_count: selectedIds.length })} @@ -234,6 +240,8 @@ export const UnblockRoomBulkButton = () => { * Toolbar button above the main room list to block a room by ID. */ export const BlockRoomByIdButton = () => { + const theme = useTheme(); + const fullScreen = useMediaQuery(theme.breakpoints.down("sm")); const [open, setOpen] = useState(false); const [roomId, setRoomId] = useState(""); const [loading, setLoading] = useState(false); @@ -268,7 +276,7 @@ export const BlockRoomByIdButton = () => { - setOpen(false)} maxWidth="sm" fullWidth> + setOpen(false)} maxWidth="sm" fullWidth fullScreen={fullScreen}> {translate("resources.rooms.action.block.title_by_id")} {translate("resources.rooms.action.block.content")} diff --git a/src/components/DeleteRoomButton.tsx b/src/components/DeleteRoomButton.tsx index 37b691f9..b5f4587b 100644 --- a/src/components/DeleteRoomButton.tsx +++ b/src/components/DeleteRoomButton.tsx @@ -10,6 +10,8 @@ import { DialogContentText, DialogTitle, } from "@mui/material"; +import { useTheme } from "@mui/material/styles"; +import useMediaQuery from "@mui/material/useMediaQuery"; import { Fragment, useCallback, useEffect, useRef, useState } from "react"; import { Button, @@ -37,6 +39,8 @@ interface DeleteRoomButtonProps { const resourceName = "rooms"; const DeleteRoomButton: React.FC = props => { + const theme = useTheme(); + const fullScreen = useMediaQuery(theme.breakpoints.down("sm")); const translate = useTranslate(); const [open, setOpen] = useState(false); const [block, setBlock] = useState(false); @@ -163,7 +167,7 @@ const DeleteRoomButton: React.FC = props => { > - + {translate(props.confirmTitle)} {translate(props.confirmContent)} diff --git a/src/components/DeleteUserButton.tsx b/src/components/DeleteUserButton.tsx index 64eee93a..249f9689 100644 --- a/src/components/DeleteUserButton.tsx +++ b/src/components/DeleteUserButton.tsx @@ -10,6 +10,8 @@ import { DialogContentText, DialogTitle, } from "@mui/material"; +import { useTheme } from "@mui/material/styles"; +import useMediaQuery from "@mui/material/useMediaQuery"; import { Fragment, useCallback, useEffect, useRef, useState } from "react"; import { SimpleForm, @@ -35,6 +37,8 @@ interface DeleteUserButtonProps { const resourceName = "users"; const DeleteUserButton: React.FC = props => { + const theme = useTheme(); + const fullScreen = useMediaQuery(theme.breakpoints.down("sm")); const translate = useTranslate(); const [open, setOpen] = useState(false); const [deleteMedia, setDeleteMedia] = useState(false); @@ -174,7 +178,7 @@ const DeleteUserButton: React.FC = props => { > {translate("ra.action.delete")} - + {translate(props.confirmTitle)} {translate(props.confirmContent)} diff --git a/src/components/LoginAsUserButton.tsx b/src/components/LoginAsUserButton.tsx index 4eec0a96..b5e9139b 100644 --- a/src/components/LoginAsUserButton.tsx +++ b/src/components/LoginAsUserButton.tsx @@ -13,12 +13,16 @@ import { TextField, Typography, } from "@mui/material"; +import { useTheme } from "@mui/material/styles"; +import useMediaQuery from "@mui/material/useMediaQuery"; import { useState } from "react"; import { Button, useDataProvider, useLocale, useNotify, useRecordContext, useTranslate } from "react-admin"; import { SynapseDataProvider } from "../providers/types"; export const LoginAsUserButton = () => { + const theme = useTheme(); + const fullScreen = useMediaQuery(theme.breakpoints.down("sm")); const record = useRecordContext(); const [open, setOpen] = useState(false); const [resultOpen, setResultOpen] = useState(false); @@ -74,7 +78,7 @@ export const LoginAsUserButton = () => { - + {translate("resources.users.action.login_as.title")} @@ -110,7 +114,7 @@ export const LoginAsUserButton = () => { - + {translate("resources.users.action.login_as.result_title", { user: record.id })} { + const theme = useTheme(); + const fullScreen = useMediaQuery(theme.breakpoints.down("sm")); const record = useRecordContext(); const [open, setOpen] = useState(false); const [loading, setLoading] = useState(false); @@ -105,7 +109,7 @@ export const PurgeHistoryButton = () => { - + {translate("resources.rooms.action.purge_history.title", { roomName })} diff --git a/src/components/QuarantineAllMediaButton.tsx b/src/components/QuarantineAllMediaButton.tsx index bffb2ec3..eeeedf5b 100644 --- a/src/components/QuarantineAllMediaButton.tsx +++ b/src/components/QuarantineAllMediaButton.tsx @@ -9,6 +9,8 @@ import { DialogContentText, DialogTitle, } from "@mui/material"; +import { useTheme } from "@mui/material/styles"; +import useMediaQuery from "@mui/material/useMediaQuery"; import { useState } from "react"; import { Button, useDataProvider, useNotify, useRecordContext, useTranslate } from "react-admin"; @@ -19,6 +21,8 @@ import { SynapseDataProvider } from "../providers/types"; * Shows a confirmation dialog before proceeding. */ export const QuarantineRoomMediaButton = () => { + const theme = useTheme(); + const fullScreen = useMediaQuery(theme.breakpoints.down("sm")); const record = useRecordContext(); const [open, setOpen] = useState(false); const [loading, setLoading] = useState(false); @@ -60,7 +64,7 @@ export const QuarantineRoomMediaButton = () => { - setOpen(false)} maxWidth="sm" fullWidth> + setOpen(false)} maxWidth="sm" fullWidth fullScreen={fullScreen}> {translate("resources.rooms.action.quarantine_all.title", { roomName })} {translate("resources.rooms.action.quarantine_all.content")} @@ -88,6 +92,8 @@ export const QuarantineRoomMediaButton = () => { * Shows a confirmation dialog before proceeding. */ export const QuarantineUserMediaButton = () => { + const theme = useTheme(); + const fullScreen = useMediaQuery(theme.breakpoints.down("sm")); const record = useRecordContext(); const [open, setOpen] = useState(false); const [loading, setLoading] = useState(false); @@ -129,7 +135,7 @@ export const QuarantineUserMediaButton = () => { - setOpen(false)} maxWidth="sm" fullWidth> + setOpen(false)} maxWidth="sm" fullWidth fullScreen={fullScreen}> {translate("resources.users.action.quarantine_all.title", { userName })} {translate("resources.users.action.quarantine_all.content")} diff --git a/src/components/ResetPasswordButton.tsx b/src/components/ResetPasswordButton.tsx index faa70b9f..4ca9e082 100644 --- a/src/components/ResetPasswordButton.tsx +++ b/src/components/ResetPasswordButton.tsx @@ -12,6 +12,8 @@ import { FormControlLabel, Switch, } from "@mui/material"; +import { useTheme } from "@mui/material/styles"; +import useMediaQuery from "@mui/material/useMediaQuery"; import { useState } from "react"; import { Button, useDataProvider, useNotify, useRecordContext, useTranslate } from "react-admin"; @@ -19,6 +21,8 @@ import { SynapseDataProvider } from "../providers/types"; import { generateRandomPassword } from "../utils/password"; export const ResetPasswordButton = () => { + const theme = useTheme(); + const fullScreen = useMediaQuery(theme.breakpoints.down("sm")); const record = useRecordContext(); const [open, setOpen] = useState(false); const [password, setPassword] = useState(""); @@ -70,7 +74,7 @@ export const ResetPasswordButton = () => { - + {translate("resources.users.action.reset_password.title")} diff --git a/src/components/ServerNotices.tsx b/src/components/ServerNotices.tsx index b89650db..5e924bf6 100644 --- a/src/components/ServerNotices.tsx +++ b/src/components/ServerNotices.tsx @@ -1,6 +1,8 @@ import IconCancel from "@mui/icons-material/Cancel"; import MessageIcon from "@mui/icons-material/Message"; import { Dialog, DialogContent, DialogContentText, DialogTitle } from "@mui/material"; +import { useTheme } from "@mui/material/styles"; +import useMediaQuery from "@mui/material/useMediaQuery"; import { useMutation } from "@tanstack/react-query"; import { useState } from "react"; import { @@ -22,6 +24,8 @@ import { } from "react-admin"; const ServerNoticeDialog = ({ open, onClose, onSubmit }) => { + const theme = useTheme(); + const fullScreen = useMediaQuery(theme.breakpoints.down("sm")); const translate = useTranslate(); const ServerNoticeToolbar = (props: ToolbarProps & { pristine?: boolean }) => ( @@ -34,7 +38,7 @@ const ServerNoticeDialog = ({ open, onClose, onSubmit }) => { ); return ( - + {translate("resources.servernotices.action.send")} {translate("resources.servernotices.helper.send")} diff --git a/src/components/media.tsx b/src/components/media.tsx index c530f50f..539a811c 100644 --- a/src/components/media.tsx +++ b/src/components/media.tsx @@ -10,6 +10,7 @@ import LockOpenIcon from "@mui/icons-material/LockOpen"; import RemoveCircleOutlineIcon from "@mui/icons-material/RemoveCircleOutline"; import { Box, Dialog, DialogContent, DialogContentText, DialogTitle, Tooltip } from "@mui/material"; import { alpha, useTheme } from "@mui/material/styles"; +import useMediaQuery from "@mui/material/useMediaQuery"; import { useMutation } from "@tanstack/react-query"; import { get } from "lodash"; import { useState } from "react"; @@ -35,6 +36,8 @@ import { decodeURLComponent } from "../utils/safety"; import { fetchAuthenticatedMedia } from "../utils/fetchMedia"; const DeleteMediaDialog = ({ open, onClose, onSubmit }) => { + const theme = useTheme(); + const fullScreen = useMediaQuery(theme.breakpoints.down("sm")); const translate = useTranslate(); const DeleteMediaToolbar = (props: ToolbarProps) => ( @@ -47,7 +50,7 @@ const DeleteMediaDialog = ({ open, onClose, onSubmit }) => { ); return ( - + {translate("delete_media.action.send")} {translate("delete_media.helper.send")} @@ -109,6 +112,8 @@ export const DeleteMediaButton = (props: ButtonProps) => { }; const PurgeRemoteMediaDialog = ({ open, onClose, onSubmit }) => { + const theme = useTheme(); + const fullScreen = useMediaQuery(theme.breakpoints.down("sm")); const translate = useTranslate(); const PurgeRemoteMediaToolbar = (props: ToolbarProps) => ( @@ -121,7 +126,7 @@ const PurgeRemoteMediaDialog = ({ open, onClose, onSubmit }) => { ); return ( - + {translate("purge_remote_media.action.send")} {translate("purge_remote_media.helper.send")} diff --git a/src/resources/reports.tsx b/src/resources/reports.tsx index d91b5df6..fe3b52e2 100644 --- a/src/resources/reports.tsx +++ b/src/resources/reports.tsx @@ -216,6 +216,8 @@ const ReportShowActions = () => { }; const EventLookupButton = () => { + const theme = useTheme(); + const fullScreen = useMediaQuery(theme.breakpoints.down("sm")); const [open, setOpen] = useState(false); const [eventId, setEventId] = useState(""); const [loading, setLoading] = useState(false); @@ -255,7 +257,7 @@ const EventLookupButton = () => { - + {translate("resources.reports.action.event_lookup.title")} Date: Fri, 20 Mar 2026 23:27:24 +0000 Subject: [PATCH 02/28] improve user delete button (show icon-only on mobile); truncate footer text (show app name and version only on mobile); improve user counts chips text color --- src/components/DeleteUserButton.tsx | 21 +++++++----------- src/components/Footer.tsx | 34 +++++++++++++++-------------- src/components/UserCounts.tsx | 2 +- 3 files changed, 27 insertions(+), 30 deletions(-) diff --git a/src/components/DeleteUserButton.tsx b/src/components/DeleteUserButton.tsx index 249f9689..c80b89c5 100644 --- a/src/components/DeleteUserButton.tsx +++ b/src/components/DeleteUserButton.tsx @@ -2,7 +2,7 @@ import ActionCheck from "@mui/icons-material/CheckCircle"; import ActionDelete from "@mui/icons-material/Delete"; import AlertError from "@mui/icons-material/ErrorOutline"; import { - Button, + Button as MuiButton, CircularProgress, Dialog, DialogActions, @@ -14,6 +14,7 @@ import { useTheme } from "@mui/material/styles"; import useMediaQuery from "@mui/material/useMediaQuery"; import { Fragment, useCallback, useEffect, useRef, useState } from "react"; import { + Button, SimpleForm, BooleanInput, useTranslate, @@ -163,20 +164,14 @@ const DeleteUserButton: React.FC = props => { return ( {translate(props.confirmTitle)} @@ -213,10 +208,10 @@ const DeleteUserButton: React.FC = props => { )} - - + diff --git a/src/components/Footer.tsx b/src/components/Footer.tsx index 8a9141fb..2485c0e9 100644 --- a/src/components/Footer.tsx +++ b/src/components/Footer.tsx @@ -34,23 +34,25 @@ const Footer = ({ logoSrc = "./images/logo.webp" }: { logoSrc?: string }) => { {" "} Synapse Admin {version} - {" "} - by{" "} - - etke.cc - {" "} - (originally developed by Awesome Technologies Innovationslabor GmbH).{" "} - - #synapse-admin:etke.cc + + {" "}by{" "} + + etke.cc + {" "} + (originally developed by Awesome Technologies Innovationslabor GmbH).{" "} + + #synapse-admin:etke.cc + + ); }; diff --git a/src/components/UserCounts.tsx b/src/components/UserCounts.tsx index e292aabe..0524dbb6 100644 --- a/src/components/UserCounts.tsx +++ b/src/components/UserCounts.tsx @@ -44,7 +44,7 @@ const UserInfoChips = () => { : null; return ( - + {createdDate && ( } From 388b1bdd844b4a5d25ba5486164d1a72a0073878 Mon Sep 17 00:00:00 2001 From: Aine Date: Fri, 20 Mar 2026 23:30:54 +0000 Subject: [PATCH 03/28] make tabs scrollable --- src/resources/destinations.tsx | 2 +- src/resources/reports.tsx | 2 +- src/resources/rooms.tsx | 2 +- src/resources/users.tsx | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/resources/destinations.tsx b/src/resources/destinations.tsx index eb70e822..b4700e36 100644 --- a/src/resources/destinations.tsx +++ b/src/resources/destinations.tsx @@ -165,7 +165,7 @@ export const DestinationShow = (props: ShowProps) => { const locale = useLocale(); return ( } title={} {...props}> - + }> diff --git a/src/resources/reports.tsx b/src/resources/reports.tsx index fe3b52e2..b4d9f7a2 100644 --- a/src/resources/reports.tsx +++ b/src/resources/reports.tsx @@ -187,7 +187,7 @@ const EventFields = ({ event }: { event: Record }) => ( export const ReportShow = (props: ShowProps) => { return ( } title={}> - + }> diff --git a/src/resources/rooms.tsx b/src/resources/rooms.tsx index 5b0e8b86..6b35894b 100644 --- a/src/resources/rooms.tsx +++ b/src/resources/rooms.tsx @@ -477,7 +477,7 @@ export const RoomShow = (props: ShowProps) => { const locale = useLocale(); return ( } title={}> - + }> diff --git a/src/resources/users.tsx b/src/resources/users.tsx index ffd600e4..e307a5f9 100644 --- a/src/resources/users.tsx +++ b/src/resources/users.tsx @@ -596,7 +596,7 @@ export const UserEdit = (props: EditProps) => { }, }} > - }> + } sx={{ "& .MuiTabs-scroller": { overflowX: "auto !important" } }}> }> Date: Sat, 21 Mar 2026 00:02:27 +0000 Subject: [PATCH 04/28] fix horizontal scroll and container sizing on view/edit pages; center user avatar --- src/resources/destinations.tsx | 2 +- src/resources/registration_tokens.tsx | 2 +- src/resources/reports.tsx | 2 +- src/resources/rooms.tsx | 8 ++++---- src/resources/user_media_statistics.tsx | 2 +- src/resources/users.tsx | 7 ++++--- 6 files changed, 12 insertions(+), 11 deletions(-) diff --git a/src/resources/destinations.tsx b/src/resources/destinations.tsx index b4700e36..1ca4218b 100644 --- a/src/resources/destinations.tsx +++ b/src/resources/destinations.tsx @@ -164,7 +164,7 @@ export const DestinationShow = (props: ShowProps) => { const translate = useTranslate(); const locale = useLocale(); return ( - } title={} {...props}> + } title={} {...props} sx={{ "& .RaShow-card": { maxWidth: { xs: "100vw", sm: "calc(100vw - 32px)" }, overflowX: "auto" } }}> }> diff --git a/src/resources/registration_tokens.tsx b/src/resources/registration_tokens.tsx index 9851ea03..89217174 100644 --- a/src/resources/registration_tokens.tsx +++ b/src/resources/registration_tokens.tsx @@ -196,7 +196,7 @@ export const RegistrationTokenEdit = (props: EditProps) => { useDocTitle(`${translate("ra.action.edit")} ${translate("resources.registration_tokens.name")}`); return ( - + }> diff --git a/src/resources/reports.tsx b/src/resources/reports.tsx index b4d9f7a2..b7216ff8 100644 --- a/src/resources/reports.tsx +++ b/src/resources/reports.tsx @@ -186,7 +186,7 @@ const EventFields = ({ event }: { event: Record }) => ( export const ReportShow = (props: ShowProps) => { return ( - } title={}> + } title={} sx={{ "& .RaShow-card": { maxWidth: { xs: "100vw", sm: "calc(100vw - 32px)" }, overflowX: "auto" } }}> }> diff --git a/src/resources/rooms.tsx b/src/resources/rooms.tsx index 6b35894b..615fb70a 100644 --- a/src/resources/rooms.tsx +++ b/src/resources/rooms.tsx @@ -118,7 +118,7 @@ const RoomShowActions = () => { const publishButton = record?.public ? : ; // FIXME: refresh after (un)publish return ( - + {publishButton} @@ -395,7 +395,7 @@ const RoomOverviewTab = () => { {translate("synapseadmin.rooms.tabs.detail")} - + {translate("resources.rooms.fields.joined_members")} @@ -429,7 +429,7 @@ const RoomOverviewTab = () => { {translate("synapseadmin.rooms.tabs.permission")} - + {translate("resources.rooms.fields.join_rules")} @@ -476,7 +476,7 @@ export const RoomShow = (props: ShowProps) => { const translate = useTranslate(); const locale = useLocale(); return ( - } title={}> + } title={} sx={{ "& .RaShow-card": { maxWidth: { xs: "100vw", sm: "calc(100vw - 32px)" }, overflowX: "auto" } }}> }> diff --git a/src/resources/user_media_statistics.tsx b/src/resources/user_media_statistics.tsx index 2961e82f..f1860e3d 100644 --- a/src/resources/user_media_statistics.tsx +++ b/src/resources/user_media_statistics.tsx @@ -24,7 +24,7 @@ import { DeleteMediaButton, PurgeRemoteMediaButton } from "../components/media"; const ListActions = () => { const { isLoading, total } = useListContext(); return ( - + diff --git a/src/resources/users.tsx b/src/resources/users.tsx index e307a5f9..d06aa130 100644 --- a/src/resources/users.tsx +++ b/src/resources/users.tsx @@ -114,7 +114,7 @@ const choices_type = [ const UserListActions = () => { const { isLoading, total } = useListContext(); return ( - + @@ -295,7 +295,7 @@ const UserEditActions = () => { } return ( - + {!record?.deactivated && } {!record?.deactivated && } {!record?.deactivated && } @@ -590,6 +590,7 @@ export const UserEdit = (props: EditProps) => { title={} actions={} mutationMode="pessimistic" + sx={{ "& .RaEdit-card": { maxWidth: { xs: "100vw", sm: "calc(100vw - 32px)" }, overflowX: "auto" } }} queryOptions={{ meta: { include: ["features"], // Tell your dataProvider to include features @@ -605,7 +606,7 @@ export const UserEdit = (props: EditProps) => { sx={{ display: "flex", flexDirection: "column", - alignItems: { xs: "flex-start", sm: "center" }, + alignItems: "center", minWidth: 140, gap: 2, }} From f05508afbc325bd0bb6718467391e3d6c80b5d36 Mon Sep 17 00:00:00 2001 From: Aine Date: Sat, 21 Mar 2026 00:14:54 +0000 Subject: [PATCH 05/28] replace all plain datagrids with configurable options --- .../recurring/RecurringCommandsList.tsx | 6 +++--- .../scheduled/ScheduledCommandsList.tsx | 6 +++--- src/resources/destinations.tsx | 5 ++--- src/resources/rooms.tsx | 17 +++++++-------- src/resources/users.tsx | 21 +++++++++---------- 5 files changed, 26 insertions(+), 29 deletions(-) diff --git a/src/components/etke.cc/schedules/components/recurring/RecurringCommandsList.tsx b/src/components/etke.cc/schedules/components/recurring/RecurringCommandsList.tsx index a0acc904..37edb045 100644 --- a/src/components/etke.cc/schedules/components/recurring/RecurringCommandsList.tsx +++ b/src/components/etke.cc/schedules/components/recurring/RecurringCommandsList.tsx @@ -2,7 +2,7 @@ import AddIcon from "@mui/icons-material/Add"; import { Paper } from "@mui/material"; import { Loading, Button, useLocale, useTranslate } from "react-admin"; import { DateField, useRecordContext } from "react-admin"; -import { Datagrid } from "react-admin"; +import { DatagridConfigurable } from "react-admin"; import { ListContextProvider, TextField, TopToolbar, Identifier } from "react-admin"; import { ResourceContextProvider, useList } from "react-admin"; import { useNavigate } from "react-router-dom"; @@ -86,7 +86,7 @@ const RecurringCommandsList = () => { - { @@ -107,7 +107,7 @@ const RecurringCommandsList = () => { label={translate("etkecc.actions.table.next_run_at")} locales={locale} /> - + diff --git a/src/components/etke.cc/schedules/components/scheduled/ScheduledCommandsList.tsx b/src/components/etke.cc/schedules/components/scheduled/ScheduledCommandsList.tsx index c66b229a..f598eaab 100644 --- a/src/components/etke.cc/schedules/components/scheduled/ScheduledCommandsList.tsx +++ b/src/components/etke.cc/schedules/components/scheduled/ScheduledCommandsList.tsx @@ -3,7 +3,7 @@ import { Paper } from "@mui/material"; import { Loading, Button, useLocale, useTranslate } from "react-admin"; import { ResourceContextProvider, useList } from "react-admin"; import { ListContextProvider, TextField } from "react-admin"; -import { Datagrid } from "react-admin"; +import { DatagridConfigurable } from "react-admin"; import { BooleanField, DateField, TopToolbar } from "react-admin"; import { Identifier } from "react-admin"; import { useNavigate } from "react-router-dom"; @@ -45,7 +45,7 @@ const ScheduledCommandsList = () => { - { @@ -70,7 +70,7 @@ const ScheduledCommandsList = () => { label={translate("etkecc.actions.table.run_at")} locales={locale} /> - + diff --git a/src/resources/destinations.tsx b/src/resources/destinations.tsx index 1ca4218b..a3440b25 100644 --- a/src/resources/destinations.tsx +++ b/src/resources/destinations.tsx @@ -8,7 +8,6 @@ import { get } from "lodash"; import { MouseEvent } from "react"; import { Button, - Datagrid, DatagridConfigurable, DateField, List, @@ -182,7 +181,7 @@ export const DestinationShow = (props: ShowProps) => { pagination={} perPage={50} > - `/rooms/${id}/show`}> + `/rooms/${id}/show`}> { > - + diff --git a/src/resources/rooms.tsx b/src/resources/rooms.tsx index 615fb70a..a66377e6 100644 --- a/src/resources/rooms.tsx +++ b/src/resources/rooms.tsx @@ -29,7 +29,6 @@ import { BooleanField, DateField, WrapperField, - Datagrid, DatagridConfigurable, ExportButton, FunctionField, @@ -491,7 +490,7 @@ export const RoomShow = (props: ShowProps) => { perPage={10} pagination={} > - "/users/" + id} bulkActionButtons={false}> + "/users/" + id} bulkActionButtons={false}> { > - + @@ -561,10 +560,10 @@ export const RoomShow = (props: ShowProps) => { pagination={} perPage={10} > - + - + @@ -576,7 +575,7 @@ export const RoomShow = (props: ShowProps) => { pagination={} perPage={10} > - + { - + @@ -615,12 +614,12 @@ export const RoomShow = (props: ShowProps) => { pagination={} perPage={10} > - + - + diff --git a/src/resources/users.tsx b/src/resources/users.tsx index d06aa130..e11bd106 100644 --- a/src/resources/users.tsx +++ b/src/resources/users.tsx @@ -20,7 +20,6 @@ import { ArrayInput, ArrayField, Button, - Datagrid, DatagridConfigurable, DateField, Create, @@ -714,11 +713,11 @@ export const UserEdit = (props: EditProps) => { } path="connections"> - + - + @@ -737,7 +736,7 @@ export const UserEdit = (props: EditProps) => { perPage={10} sort={{ field: "created_ts", order: "DESC" }} > - }> + }> @@ -751,7 +750,7 @@ export const UserEdit = (props: EditProps) => { - + @@ -763,7 +762,7 @@ export const UserEdit = (props: EditProps) => { perPage={10} pagination={} > - "/rooms/" + id + "/show"} bulkActionButtons={} @@ -799,7 +798,7 @@ export const UserEdit = (props: EditProps) => { - + @@ -815,7 +814,7 @@ export const UserEdit = (props: EditProps) => { perPage={10} pagination={} > - "/rooms/" + id + "/show"} bulkActionButtons={false}> + "/rooms/" + id + "/show"} bulkActionButtons={false}> @@ -834,7 +833,7 @@ export const UserEdit = (props: EditProps) => { label={translate("resources.users.membership", { smart_count: 1 })} sortable={false} /> - + @@ -850,7 +849,7 @@ export const UserEdit = (props: EditProps) => { pagination={} perPage={10} > - + @@ -859,7 +858,7 @@ export const UserEdit = (props: EditProps) => { - + From 021374fa92922d8561bbaca79e05b886a0bf7f9c Mon Sep 17 00:00:00 2001 From: Aine Date: Sat, 21 Mar 2026 00:34:12 +0000 Subject: [PATCH 06/28] add mobile-friendly user list; add user account status icons to the mobile-friendly list and to the user edit status toggles --- src/components/Footer.tsx | 3 +- src/components/UserCounts.tsx | 4 +- src/resources/destinations.tsx | 7 +- src/resources/registration_tokens.tsx | 5 +- src/resources/reports.tsx | 7 +- src/resources/rooms.tsx | 7 +- src/resources/users.tsx | 181 +++++++++++++++++++------- 7 files changed, 161 insertions(+), 53 deletions(-) diff --git a/src/components/Footer.tsx b/src/components/Footer.tsx index 2485c0e9..b16abf10 100644 --- a/src/components/Footer.tsx +++ b/src/components/Footer.tsx @@ -36,7 +36,8 @@ const Footer = ({ logoSrc = "./images/logo.webp" }: { logoSrc?: string }) => { Synapse Admin {version} - {" "}by{" "} + {" "} + by{" "} { : null; return ( - + {createdDate && ( } diff --git a/src/resources/destinations.tsx b/src/resources/destinations.tsx index a3440b25..66b7405d 100644 --- a/src/resources/destinations.tsx +++ b/src/resources/destinations.tsx @@ -163,7 +163,12 @@ export const DestinationShow = (props: ShowProps) => { const translate = useTranslate(); const locale = useLocale(); return ( - } title={} {...props} sx={{ "& .RaShow-card": { maxWidth: { xs: "100vw", sm: "calc(100vw - 32px)" }, overflowX: "auto" } }}> + } + title={} + {...props} + sx={{ "& .RaShow-card": { maxWidth: { xs: "100vw", sm: "calc(100vw - 32px)" }, overflowX: "auto" } }} + > }> diff --git a/src/resources/registration_tokens.tsx b/src/resources/registration_tokens.tsx index 89217174..f13b3db5 100644 --- a/src/resources/registration_tokens.tsx +++ b/src/resources/registration_tokens.tsx @@ -196,7 +196,10 @@ export const RegistrationTokenEdit = (props: EditProps) => { useDocTitle(`${translate("ra.action.edit")} ${translate("resources.registration_tokens.name")}`); return ( - + }> diff --git a/src/resources/reports.tsx b/src/resources/reports.tsx index b7216ff8..087c2ae2 100644 --- a/src/resources/reports.tsx +++ b/src/resources/reports.tsx @@ -186,7 +186,12 @@ const EventFields = ({ event }: { event: Record }) => ( export const ReportShow = (props: ShowProps) => { return ( - } title={} sx={{ "& .RaShow-card": { maxWidth: { xs: "100vw", sm: "calc(100vw - 32px)" }, overflowX: "auto" } }}> + } + title={} + sx={{ "& .RaShow-card": { maxWidth: { xs: "100vw", sm: "calc(100vw - 32px)" }, overflowX: "auto" } }} + > }> diff --git a/src/resources/rooms.tsx b/src/resources/rooms.tsx index a66377e6..6295d829 100644 --- a/src/resources/rooms.tsx +++ b/src/resources/rooms.tsx @@ -475,7 +475,12 @@ export const RoomShow = (props: ShowProps) => { const translate = useTranslate(); const locale = useLocale(); return ( - } title={} sx={{ "& .RaShow-card": { maxWidth: { xs: "100vw", sm: "calc(100vw - 32px)" }, overflowX: "auto" } }}> + } + title={} + sx={{ "& .RaShow-card": { maxWidth: { xs: "100vw", sm: "calc(100vw - 32px)" }, overflowX: "auto" } }} + > }> diff --git a/src/resources/users.tsx b/src/resources/users.tsx index e11bd106..51440608 100644 --- a/src/resources/users.tsx +++ b/src/resources/users.tsx @@ -12,9 +12,16 @@ import PersonPinIcon from "@mui/icons-material/PersonPin"; import ScienceIcon from "@mui/icons-material/Science"; import SettingsInputComponentIcon from "@mui/icons-material/SettingsInputComponent"; import ViewListIcon from "@mui/icons-material/ViewList"; -import { Alert, Box, Divider, Paper, Typography } from "@mui/material"; +import BlockIcon from "@mui/icons-material/Block"; +import DeleteForeverIcon from "@mui/icons-material/DeleteForever"; +import LockIcon from "@mui/icons-material/Lock"; +import NoAccountsIcon from "@mui/icons-material/NoAccounts"; +import AdminPanelSettingsIcon from "@mui/icons-material/AdminPanelSettings"; +import VisibilityOffIcon from "@mui/icons-material/VisibilityOff"; +import { Alert, Box, Divider, Paper, Tooltip, Typography } from "@mui/material"; import EmptyState from "../components/EmptyState"; import { useTheme } from "@mui/material/styles"; +import useMediaQuery from "@mui/material/useMediaQuery"; import { useEffect, useState } from "react"; import { ArrayInput, @@ -68,6 +75,7 @@ import { useCreate, useRedirect, useLocale, + SimpleList, } from "react-admin"; import { useFormContext } from "react-hook-form"; import { Link } from "react-router-dom"; @@ -217,6 +225,8 @@ const UserBulkActionButtons = () => { export const UserList = (props: ListProps) => { const locale = useLocale(); const translate = useTranslate(); + const theme = useTheme(); + const isSmall = useMediaQuery(theme.breakpoints.down("sm")); useDocTitle(translate("resources.users.name", { smart_count: 2 })); return ( { perPage={50} empty={} > - `/${resource}/${encodeURIComponent(id)}`} - bulkActionButtons={} - > - - - - - - - - - - record.displayname || record.id} + secondaryText={record => record.id} + tertiaryText={record => ( + + {record.admin && ( + + + + )} + {record.locked && ( + + + + )} + {record.suspended && ( + + + + )} + {record.shadow_banned && ( + + + + )} + {record.deactivated && ( + + + + )} + {record.erased && ( + + + + )} + + )} + linkType="edit" + leftAvatar={record => ( + + )} /> - + ) : ( + `/${resource}/${encodeURIComponent(id)}`} + bulkActionButtons={} + > + + + + + + + + + + + + )} ); }; @@ -482,6 +537,7 @@ const UserEditToolbar = () => { }; const UserBooleanInput = props => { + const translate = useTranslate(); const record = useRecordContext(); const ownUserId = localStorage.getItem("user_id"); let ownUserIsSelected = false; @@ -495,9 +551,17 @@ const UserBooleanInput = props => { } } + const { icon, ...rest } = props; + const label = icon ? ( + + {icon} + {translate(rest.label || `resources.users.fields.${rest.source}`)} + + ) : undefined; + return ( - + ); }; @@ -629,12 +693,21 @@ export const UserEdit = (props: EditProps) => { - - + } + /> + } + /> } /> { {translate("synapseadmin.users.danger_zone")} - + } + /> } /> } /> @@ -814,7 +893,11 @@ export const UserEdit = (props: EditProps) => { perPage={10} pagination={} > - "/rooms/" + id + "/show"} bulkActionButtons={false}> + "/rooms/" + id + "/show"} + bulkActionButtons={false} + > @@ -849,7 +932,11 @@ export const UserEdit = (props: EditProps) => { pagination={} perPage={10} > - + From 6a5c80694b8d93e1adee65fd88744ca5beb11522 Mon Sep 17 00:00:00 2001 From: Aine Date: Sat, 21 Mar 2026 00:44:54 +0000 Subject: [PATCH 07/28] hide some filters by default --- src/resources/registration_tokens.tsx | 2 +- src/resources/rooms.tsx | 8 +++++--- src/resources/scheduled_tasks.tsx | 1 - 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/resources/registration_tokens.tsx b/src/resources/registration_tokens.tsx index f13b3db5..96f4a1bc 100644 --- a/src/resources/registration_tokens.tsx +++ b/src/resources/registration_tokens.tsx @@ -43,7 +43,7 @@ const validateToken = [regex(/^[A-Za-z0-9._~-]{0,64}$/)]; const validateUsesAllowed = [number()]; const validateLength = [number(), maxValue(64)]; -const registrationTokenFilters = []; +const registrationTokenFilters = []; export const RegistrationTokenList = (props: ListProps) => { const locale = useLocale(); diff --git a/src/resources/rooms.tsx b/src/resources/rooms.tsx index 6295d829..f0f6edb7 100644 --- a/src/resources/rooms.tsx +++ b/src/resources/rooms.tsx @@ -31,6 +31,7 @@ import { WrapperField, DatagridConfigurable, ExportButton, + FilterButton, FunctionField, List, ListProps, @@ -650,13 +651,14 @@ export const RoomBulkActionButtons = () => { }; const roomFilters = [ - , - , - , + , + , + , ]; const RoomListActions = () => ( + diff --git a/src/resources/scheduled_tasks.tsx b/src/resources/scheduled_tasks.tsx index 9fb47cc6..92f0542a 100644 --- a/src/resources/scheduled_tasks.tsx +++ b/src/resources/scheduled_tasks.tsx @@ -49,7 +49,6 @@ const scheduledTaskFilters = (translate: ReturnType) => [ ({ id: s, name: translate(`resources.scheduled_tasks.status.${s}`), From 9b6aee9f8aba9a791f0b32a6f4c6fe4c4f0ad50d Mon Sep 17 00:00:00 2001 From: Aine Date: Sat, 21 Mar 2026 01:35:40 +0000 Subject: [PATCH 08/28] make etke.cc components mobile-frinedly --- src/components/etke.cc/BillingPage.tsx | 39 +++ .../etke.cc/ServerCommandsPanel.tsx | 224 ++++++++++++------ src/components/etke.cc/ServerStatusPage.tsx | 4 +- src/components/etke.cc/SupportPage.tsx | 41 ++++ src/components/etke.cc/SupportRequestPage.tsx | 11 +- .../recurring/RecurringCommandEdit.tsx | 13 +- .../recurring/RecurringCommandsList.tsx | 17 +- .../recurring/RecurringDeleteButton.tsx | 5 +- .../scheduled/ScheduledCommandEdit.tsx | 13 +- .../scheduled/ScheduledCommandShow.tsx | 24 +- .../scheduled/ScheduledCommandsList.tsx | 16 +- .../scheduled/ScheduledDeleteButton.tsx | 5 +- 12 files changed, 300 insertions(+), 112 deletions(-) diff --git a/src/components/etke.cc/BillingPage.tsx b/src/components/etke.cc/BillingPage.tsx index e6023e85..2cef24bf 100644 --- a/src/components/etke.cc/BillingPage.tsx +++ b/src/components/etke.cc/BillingPage.tsx @@ -4,6 +4,9 @@ import PaymentIcon from "@mui/icons-material/Payment"; import { Box, Alert, + Chip, + List, + ListItem, Typography, Link, Table, @@ -15,7 +18,9 @@ import { Paper, Button, Tooltip, + useMediaQuery, } from "@mui/material"; +import { useTheme } from "@mui/material/styles"; import { Stack } from "@mui/material"; import IconButton from "@mui/material/IconButton"; import { useState, useEffect } from "react"; @@ -48,6 +53,8 @@ const BillingPage = () => { const notify = useNotify(); const locale = useLocale(); const translate = useTranslate(); + const theme = useTheme(); + const isSmall = useMediaQuery(theme.breakpoints.down("sm")); const [paymentsData, setPaymentsData] = useState([]); const [loading, setLoading] = useState(true); const [maintenance, setMaintenance] = useState(false); @@ -220,6 +227,38 @@ const BillingPage = () => { + ) : isSmall ? ( + + {paymentsData.map(payment => ( + + + + {payment.amount.toFixed(2)} {payment.currency} + + + {new Date(payment.paid_at).toLocaleDateString(locale)} + {" · "} + {translate(`etkecc.billing.enums.type.${payment.is_subscription ? "subscription" : "one_time"}`)} + + + + {downloadInvoiceButton(payment)} + + ))} + ) : ( diff --git a/src/components/etke.cc/ServerCommandsPanel.tsx b/src/components/etke.cc/ServerCommandsPanel.tsx index 3f2d748a..d2adaff8 100644 --- a/src/components/etke.cc/ServerCommandsPanel.tsx +++ b/src/components/etke.cc/ServerCommandsPanel.tsx @@ -11,9 +11,13 @@ import { TextField, Autocomplete, Box, + Button as MuiButton, Link, + Stack, Typography, + useMediaQuery, } from "@mui/material"; +import { useTheme } from "@mui/material/styles"; import { useEffect, useState } from "react"; import { Button, Loading, useDataProvider, useCreatePath, useLocale, useStore, useTranslate } from "react-admin"; import { Link as RouterLink } from "react-router-dom"; @@ -28,15 +32,15 @@ import { Icons } from "../../utils/icons"; const renderIcon = (icon: string) => { /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ const IconComponent = Icons[icon] as React.ComponentType | undefined; - return IconComponent ? ( - - ) : null; + return IconComponent ? : null; }; const ServerCommandsPanel = () => { const { etkeccAdmin } = useAppContext(); const createPath = useCreatePath(); const translate = useTranslate(); + const theme = useTheme(); + const isSmall = useMediaQuery(theme.breakpoints.down("sm")); const { isLoading, maintenance, serverCommands, setServerCommands } = useServerCommands(); const { units } = useUnits(); const [serverProcess, setServerProcess] = useStore("serverProcess", { @@ -176,82 +180,150 @@ const ServerCommandsPanel = () => { . - -
- - - {translate("etkecc.actions.table.command")} - - - {translate("etkecc.actions.table.description")} - - - - - - {Object.entries(serverCommands).map(([command, { icon, args, description, additionalArgs }]) => ( - - - - {renderIcon(icon)} - {command} - - - - - - + renderInput={params => ( + + )} + /> + )} + {args && command !== "restart" && ( + { + setCommandAdditionalArgs(command, e.target.value); + }} + value={additionalArgs} + fullWidth + /> + )} + } + fullWidth + onClick={() => { + runCommand(command); + }} + disabled={ + commandIsRunning || (args && typeof additionalArgs === "string" && additionalArgs.length === 0) + } + > + {translate("etkecc.actions.buttons.run")} + + + + ))} + + ) : ( + +
+ + + {translate("etkecc.actions.table.command")} + + {translate("etkecc.actions.table.description")} + - ))} - -
-
+ + + {Object.entries(serverCommands).map(([command, { icon, args, description, additionalArgs }]) => ( + + + + {renderIcon(icon)} + {command} + + + + + + + + ))} + + + + )} {commandResult.length > 0 && ( } severity="success"> diff --git a/src/components/etke.cc/ServerStatusPage.tsx b/src/components/etke.cc/ServerStatusPage.tsx index f377fd5d..6c314120 100644 --- a/src/components/etke.cc/ServerStatusPage.tsx +++ b/src/components/etke.cc/ServerStatusPage.tsx @@ -108,12 +108,12 @@ const ServerStatusPage = () => { <> <Stack spacing={3} mt={3}> - <Stack spacing={1} direction="row" alignItems="center"> + <Stack spacing={1} direction={{ xs: "column", sm: "row" }} alignItems={{ xs: "flex-start", sm: "center" }}> <Box sx={{ display: "flex", alignItems: "center", gap: 1 }}> <Typography variant="h4">{translate("etkecc.status.status")}:</Typography> <StatusChip isOkay={isOkay} command={command} errorLabel={errorLabel} /> </Box> - <Typography variant="h5" color="primary" fontWeight="medium"> + <Typography variant="h5" color="primary" fontWeight="medium" sx={{ wordBreak: "break-all" }}> {host} </Typography> </Stack> diff --git a/src/components/etke.cc/SupportPage.tsx b/src/components/etke.cc/SupportPage.tsx index 3dfbd79c..25ce733a 100644 --- a/src/components/etke.cc/SupportPage.tsx +++ b/src/components/etke.cc/SupportPage.tsx @@ -9,6 +9,9 @@ import { CircularProgress, FormControlLabel, Link, + List, + ListItemButton, + ListItemText, Paper, Stack, Table, @@ -19,7 +22,9 @@ import { TableRow, TextField, Typography, + useMediaQuery, } from "@mui/material"; +import { useTheme } from "@mui/material/styles"; import { useEffect, useState } from "react"; import { Title, useDataProvider, useLocale, useNotify, useTranslate } from "react-admin"; import { useNavigate } from "react-router-dom"; @@ -133,6 +138,8 @@ const SupportPage = () => { const notify = useNotify(); const locale = useLocale(); const translate = useTranslate(); + const theme = useTheme(); + const isSmall = useMediaQuery(theme.breakpoints.down("sm")); const [requests, setRequests] = useState<SupportRequest[]>([]); const [loading, setLoading] = useState(true); const [failure, setFailure] = useState<string | null>(null); @@ -228,6 +235,40 @@ const SupportPage = () => { <Paper elevation={2} sx={{ p: 2 }}> <Typography>{translate("etkecc.support.no_requests")}</Typography> </Paper> + ) : isSmall ? ( + <List disablePadding> + {requests.map(req => ( + <ListItemButton + key={req.id} + onClick={() => navigate(`/support/${req.id}`)} + component={Paper} + elevation={2} + sx={{ mb: 1, flexDirection: "column", alignItems: "flex-start", gap: 0.5 }} + > + <ListItemText + primary={req.subject || req.id} + secondary={ + req.updated_at + ? `${translate("etkecc.support.fields.updated_at")}: ${new Date(req.updated_at).toLocaleString(locale)}` + : undefined + } + /> + {req.status && ( + <Chip + label={translate(`etkecc.support.status.${req.status}`, { _: req.status })} + size="small" + color={ + req.status === "active" || req.status === "open" + ? "success" + : req.status === "closed" + ? "default" + : "info" + } + /> + )} + </ListItemButton> + ))} + </List> ) : ( <TableContainer component={Paper} elevation={2}> <Table> diff --git a/src/components/etke.cc/SupportRequestPage.tsx b/src/components/etke.cc/SupportRequestPage.tsx index 0cfa71e4..9fa9abc3 100644 --- a/src/components/etke.cc/SupportRequestPage.tsx +++ b/src/components/etke.cc/SupportRequestPage.tsx @@ -62,20 +62,21 @@ const MessageRow = ({ borderLeftColor: !isCustomer ? "primary.main" : undefined, }} > - <Stack direction="row"> + <Stack direction={{ xs: "column", sm: "row" }}> <Box onClick={mxid ? () => navigate(`/users/${encodeURIComponent(mxid)}`) : undefined} sx={{ - width: 150, + width: { xs: "100%", sm: 150 }, flexShrink: 0, p: 1.5, - borderRight: "1px solid", + borderRight: { xs: "none", sm: "1px solid" }, + borderBottom: { xs: "1px solid", sm: "none" }, borderColor: "action.selected", bgcolor: "background.default", display: "flex", - flexDirection: "column", + flexDirection: { xs: "row", sm: "column" }, alignItems: "center", - gap: 0.5, + gap: { xs: 1, sm: 0.5 }, cursor: mxid ? "pointer" : undefined, "&:hover": mxid ? { bgcolor: "action.hover" } : undefined, }} diff --git a/src/components/etke.cc/schedules/components/recurring/RecurringCommandEdit.tsx b/src/components/etke.cc/schedules/components/recurring/RecurringCommandEdit.tsx index 28f74a3b..8731e4b4 100644 --- a/src/components/etke.cc/schedules/components/recurring/RecurringCommandEdit.tsx +++ b/src/components/etke.cc/schedules/components/recurring/RecurringCommandEdit.tsx @@ -171,13 +171,14 @@ const RecurringCommandEdit = () => { return ( <> <Title title={pageTitle} /> - <Box sx={{ mt: 2 }}> + <Box sx={{ mt: 2, maxWidth: { xs: "100vw", sm: "calc(100vw - 32px)" }, overflowX: "auto" }}> <Button label={translate("etkecc.actions.buttons.back")} onClick={() => navigate("/server_actions")} - startIcon={<ArrowBackIcon />} sx={{ mb: 2 }} - /> + > + <ArrowBackIcon /> + </Button> <Card> <CardHeader title={pageTitle} /> @@ -187,7 +188,11 @@ const RecurringCommandEdit = () => { <Alert severity="info"> <Typography variant="body1" sx={{ px: 2 }}> {translate("etkecc.actions.command_details_intro")}{" "} - <Link href={`https://etke.cc/help/extras/scheduler/#${command.command}`} target="_blank"> + <Link + href={`https://etke.cc/help/extras/scheduler/#${command.command}`} + target="_blank" + sx={{ wordBreak: "break-all" }} + > {`etke.cc/help/extras/scheduler/#${command.command}`} </Link> . diff --git a/src/components/etke.cc/schedules/components/recurring/RecurringCommandsList.tsx b/src/components/etke.cc/schedules/components/recurring/RecurringCommandsList.tsx index 37edb045..2f4ae731 100644 --- a/src/components/etke.cc/schedules/components/recurring/RecurringCommandsList.tsx +++ b/src/components/etke.cc/schedules/components/recurring/RecurringCommandsList.tsx @@ -1,8 +1,10 @@ import AddIcon from "@mui/icons-material/Add"; import { Paper } from "@mui/material"; +import { useTheme } from "@mui/material/styles"; +import useMediaQuery from "@mui/material/useMediaQuery"; import { Loading, Button, useLocale, useTranslate } from "react-admin"; import { DateField, useRecordContext } from "react-admin"; -import { DatagridConfigurable } from "react-admin"; +import { Datagrid } from "react-admin"; import { ListContextProvider, TextField, TopToolbar, Identifier } from "react-admin"; import { ResourceContextProvider, useList } from "react-admin"; import { useNavigate } from "react-router-dom"; @@ -19,8 +21,9 @@ const ListActions = () => { <Button label={translate("etkecc.actions.buttons.create")} onClick={() => navigate("/server_actions/recurring/create")} - startIcon={<AddIcon />} - /> + > + <AddIcon /> + </Button> </TopToolbar> ); }; @@ -69,6 +72,8 @@ const RecurringTimeField = ({ label: _label }: { label?: string }) => { const RecurringCommandsList = () => { const locale = useLocale(); const translate = useTranslate(); + const theme = useTheme(); + const isSmall = useMediaQuery(theme.breakpoints.down("sm")); const { data, isLoading } = useRecurringCommands(); const listContext = useList({ @@ -86,7 +91,7 @@ const RecurringCommandsList = () => { <ListContextProvider value={listContext}> <ListActions /> <Paper> - <DatagridConfigurable + <Datagrid bulkActionButtons={false} /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ rowClick={(id: Identifier, resource: string, record: any) => { @@ -98,7 +103,7 @@ const RecurringCommandsList = () => { }} > <TextField source="command" label={translate("etkecc.actions.table.command")} /> - <TextField source="args" label={translate("etkecc.actions.table.arguments")} /> + {!isSmall && <TextField source="args" label={translate("etkecc.actions.table.arguments")} />} <RecurringTimeField label={translate("etkecc.actions.table.time_local")} /> <DateField options={DATE_FORMAT} @@ -107,7 +112,7 @@ const RecurringCommandsList = () => { label={translate("etkecc.actions.table.next_run_at")} locales={locale} /> - </DatagridConfigurable> + </Datagrid> </Paper> </ListContextProvider> </ResourceContextProvider> diff --git a/src/components/etke.cc/schedules/components/recurring/RecurringDeleteButton.tsx b/src/components/etke.cc/schedules/components/recurring/RecurringDeleteButton.tsx index 0ac2f19e..9d6522ee 100644 --- a/src/components/etke.cc/schedules/components/recurring/RecurringDeleteButton.tsx +++ b/src/components/etke.cc/schedules/components/recurring/RecurringDeleteButton.tsx @@ -51,8 +51,9 @@ const RecurringDeleteButton = () => { label={translate("etkecc.actions.buttons.delete")} onClick={handleClick} disabled={isDeleting} - startIcon={<DeleteIcon />} - /> + > + <DeleteIcon /> + </Button> <Confirm isOpen={open} title={translate("etkecc.actions.delete_recurring_title")} diff --git a/src/components/etke.cc/schedules/components/scheduled/ScheduledCommandEdit.tsx b/src/components/etke.cc/schedules/components/scheduled/ScheduledCommandEdit.tsx index 3925979d..fddbaea7 100644 --- a/src/components/etke.cc/schedules/components/scheduled/ScheduledCommandEdit.tsx +++ b/src/components/etke.cc/schedules/components/scheduled/ScheduledCommandEdit.tsx @@ -124,13 +124,14 @@ const ScheduledCommandEdit = () => { return ( <> <Title title={pageTitle} /> - <Box sx={{ mt: 2 }}> + <Box sx={{ mt: 2, maxWidth: { xs: "100vw", sm: "calc(100vw - 32px)" }, overflowX: "auto" }}> <Button label={translate("etkecc.actions.buttons.back")} onClick={() => navigate("/server_actions")} - startIcon={<ArrowBackIcon />} sx={{ mb: 2 }} - /> + > + <ArrowBackIcon /> + </Button> <Card> <CardHeader title={pageTitle} /> @@ -138,7 +139,11 @@ const ScheduledCommandEdit = () => { <EtkeAttribution> <Typography variant="body1" sx={{ px: 2 }}> {translate("etkecc.actions.command_details_intro")}{" "} - <Link href={`https://etke.cc/help/extras/scheduler/#${command.command}`} target="_blank"> + <Link + href={`https://etke.cc/help/extras/scheduler/#${command.command}`} + target="_blank" + sx={{ wordBreak: "break-all" }} + > {`etke.cc/help/extras/scheduler/#${command.command}`} </Link> . diff --git a/src/components/etke.cc/schedules/components/scheduled/ScheduledCommandShow.tsx b/src/components/etke.cc/schedules/components/scheduled/ScheduledCommandShow.tsx index 65762ccc..6b78b216 100644 --- a/src/components/etke.cc/schedules/components/scheduled/ScheduledCommandShow.tsx +++ b/src/components/etke.cc/schedules/components/scheduled/ScheduledCommandShow.tsx @@ -1,5 +1,5 @@ import ArrowBackIcon from "@mui/icons-material/ArrowBack"; -import { Alert, Box, Card, CardContent, CardHeader, Typography, Link } from "@mui/material"; +import { Alert, Box, Card, CardContent, CardHeader, TextField as MuiTextField, Typography, Link } from "@mui/material"; import { useState, useEffect } from "react"; import { Loading, @@ -52,13 +52,14 @@ const ScheduledCommandShow = () => { return ( <> <Title title={translate("etkecc.actions.scheduled_details_title")} /> - <Box sx={{ mt: 2 }}> + <Box sx={{ mt: 2, maxWidth: { xs: "100vw", sm: "calc(100vw - 32px)" }, overflowX: "auto" }}> <Button label={translate("etkecc.actions.buttons.back")} onClick={() => navigate("/server_actions")} - startIcon={<ArrowBackIcon />} sx={{ mb: 2 }} - /> + > + <ArrowBackIcon /> + </Button> <RecordContextProvider value={command}> <Card> @@ -69,7 +70,11 @@ const ScheduledCommandShow = () => { <Alert severity="info"> <Typography variant="body1" sx={{ px: 2 }}> {translate("etkecc.actions.command_details_intro")}{" "} - <Link href={`https://etke.cc/help/extras/scheduler/#${command.command}`} target="_blank"> + <Link + href={`https://etke.cc/help/extras/scheduler/#${command.command}`} + target="_blank" + sx={{ wordBreak: "break-all" }} + > {`etke.cc/help/extras/scheduler/#${command.command}`} </Link> . @@ -78,7 +83,14 @@ const ScheduledCommandShow = () => { </EtkeAttribution> )} <SimpleShowLayout> - <TextField source="id" label={translate("etkecc.actions.form.id")} /> + <MuiTextField + value={command.id} + label={translate("etkecc.actions.form.id")} + disabled + fullWidth + variant="filled" + size="small" + /> <TextField source="command" label={translate("etkecc.actions.form.command")} /> {command.args && <TextField source="args" label={translate("etkecc.actions.table.arguments")} />} <BooleanField source="is_recurring" label={translate("etkecc.actions.table.is_recurring")} /> diff --git a/src/components/etke.cc/schedules/components/scheduled/ScheduledCommandsList.tsx b/src/components/etke.cc/schedules/components/scheduled/ScheduledCommandsList.tsx index f598eaab..be2bcd03 100644 --- a/src/components/etke.cc/schedules/components/scheduled/ScheduledCommandsList.tsx +++ b/src/components/etke.cc/schedules/components/scheduled/ScheduledCommandsList.tsx @@ -1,9 +1,11 @@ import AddIcon from "@mui/icons-material/Add"; import { Paper } from "@mui/material"; +import { useTheme } from "@mui/material/styles"; +import useMediaQuery from "@mui/material/useMediaQuery"; import { Loading, Button, useLocale, useTranslate } from "react-admin"; import { ResourceContextProvider, useList } from "react-admin"; import { ListContextProvider, TextField } from "react-admin"; -import { DatagridConfigurable } from "react-admin"; +import { Datagrid } from "react-admin"; import { BooleanField, DateField, TopToolbar } from "react-admin"; import { Identifier } from "react-admin"; import { useNavigate } from "react-router-dom"; @@ -20,7 +22,9 @@ const ListActions = () => { return ( <TopToolbar> - <Button label={translate("etkecc.actions.buttons.create")} onClick={handleCreate} startIcon={<AddIcon />} /> + <Button label={translate("etkecc.actions.buttons.create")} onClick={handleCreate}> + <AddIcon /> + </Button> </TopToolbar> ); }; @@ -28,6 +32,8 @@ const ListActions = () => { const ScheduledCommandsList = () => { const locale = useLocale(); const translate = useTranslate(); + const theme = useTheme(); + const isSmall = useMediaQuery(theme.breakpoints.down("sm")); const { data, isLoading } = useScheduledCommands(); const listContext = useList({ @@ -45,7 +51,7 @@ const ScheduledCommandsList = () => { <ListContextProvider value={listContext}> <ListActions /> <Paper> - <DatagridConfigurable + <Datagrid bulkActionButtons={false} /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ rowClick={(id: Identifier, resource: string, record: any) => { @@ -61,7 +67,7 @@ const ScheduledCommandsList = () => { }} > <TextField source="command" label={translate("etkecc.actions.table.command")} /> - <TextField source="args" label={translate("etkecc.actions.table.arguments")} /> + {!isSmall && <TextField source="args" label={translate("etkecc.actions.table.arguments")} />} <BooleanField source="is_recurring" label={translate("etkecc.actions.table.is_recurring")} /> <DateField options={DATE_FORMAT} @@ -70,7 +76,7 @@ const ScheduledCommandsList = () => { label={translate("etkecc.actions.table.run_at")} locales={locale} /> - </DatagridConfigurable> + </Datagrid> </Paper> </ListContextProvider> </ResourceContextProvider> diff --git a/src/components/etke.cc/schedules/components/scheduled/ScheduledDeleteButton.tsx b/src/components/etke.cc/schedules/components/scheduled/ScheduledDeleteButton.tsx index 44a0911e..2ed09d20 100644 --- a/src/components/etke.cc/schedules/components/scheduled/ScheduledDeleteButton.tsx +++ b/src/components/etke.cc/schedules/components/scheduled/ScheduledDeleteButton.tsx @@ -51,8 +51,9 @@ const ScheduledDeleteButton = () => { label={translate("etkecc.actions.buttons.delete")} onClick={handleClick} disabled={isDeleting} - startIcon={<DeleteIcon />} - /> + > + <DeleteIcon /> + </Button> <Confirm isOpen={open} title={translate("etkecc.actions.delete_scheduled_title")} From 9d92d4c783146d285d9d346a0518c8cb69fa27ec Mon Sep 17 00:00:00 2001 From: Aine <aine@etke.cc> Date: Sat, 21 Mar 2026 10:16:12 +0000 Subject: [PATCH 09/28] make rooms list mobile-friendly --- src/resources/rooms.tsx | 61 +++++++++++++++++++++++++++++++++++------ 1 file changed, 53 insertions(+), 8 deletions(-) diff --git a/src/resources/rooms.tsx b/src/resources/rooms.tsx index f0f6edb7..0dc73bf7 100644 --- a/src/resources/rooms.tsx +++ b/src/resources/rooms.tsx @@ -20,6 +20,7 @@ import CardContent from "@mui/material/CardContent"; import Chip from "@mui/material/Chip"; import Divider from "@mui/material/Divider"; import TextField from "@mui/material/TextField"; +import Tooltip from "@mui/material/Tooltip"; import Typography from "@mui/material/Typography"; import { useTheme } from "@mui/material/styles"; import useMediaQuery from "@mui/material/useMediaQuery"; @@ -42,6 +43,7 @@ import { ResourceProps, SearchInput, SelectColumnsButton, + SimpleList, Show, ShowProps, Tab, @@ -656,18 +658,23 @@ const roomFilters = [ <NullableBooleanInput key="empty_rooms" source="empty_rooms" label="resources.rooms.filter.empty_rooms" />, ]; -const RoomListActions = () => ( - <TopToolbar> - <FilterButton /> - <BlockRoomByIdButton /> - <SelectColumnsButton /> - <ExportButton /> - </TopToolbar> -); +const RoomListActions = () => { + const theme = useTheme(); + const isSmall = useMediaQuery(theme.breakpoints.down("sm")); + return ( + <TopToolbar> + <FilterButton /> + <BlockRoomByIdButton /> + {!isSmall && <SelectColumnsButton />} + <ExportButton /> + </TopToolbar> + ); +}; export const RoomList = (props: ListProps) => { const theme = useTheme(); const translate = useTranslate(); + const isSmall = useMediaQuery(theme.breakpoints.down("sm")); useDocTitle(translate("resources.rooms.name", { smart_count: 2 })); return ( @@ -680,6 +687,43 @@ export const RoomList = (props: ListProps) => { perPage={50} empty={<EmptyState />} > + {isSmall ? ( + <SimpleList + primaryText={record => ( + <Box component="span" sx={{ wordBreak: "break-all" }}> + {record.name || record.canonical_alias || record.id} + </Box> + )} + secondaryText={record => ( + <> + {translate("resources.rooms.fields.joined_members")}: {record.joined_members ?? 0} + {record.creator && ( + <> + <br /> + <Box component="span" sx={{ wordBreak: "break-all" }}> + {translate("resources.rooms.fields.creator")}: {record.creator} + </Box> + </> + )} + </> + )} + tertiaryText={record => ( + <Box sx={{ display: "flex", gap: 0.5 }}> + {record.is_encrypted ? ( + <Tooltip title={translate("resources.rooms.fields.encryption")}> + <HttpsIcon fontSize="small" sx={{ color: theme.palette.success.main }} /> + </Tooltip> + ) : ( + <Tooltip title={translate("resources.rooms.fields.encryption")}> + <NoEncryptionIcon fontSize="small" sx={{ color: theme.palette.error.main }} /> + </Tooltip> + )} + </Box> + )} + linkType="show" + leftAvatar={record => <AvatarField record={record} source="avatar_src" sx={{ height: "40px", width: "40px" }} />} + /> + ) : ( <DatagridConfigurable rowClick="show" bulkActionButtons={<RoomBulkActionButtons />} @@ -731,6 +775,7 @@ export const RoomList = (props: ListProps) => { <MakeAdminBtn /> </WrapperField> </DatagridConfigurable> + )} </List> ); }; From 017bc22a801766c6b7ae59a620376ae1980b7ce1 Mon Sep 17 00:00:00 2001 From: Aine <aine@etke.cc> Date: Sat, 21 Mar 2026 10:44:21 +0000 Subject: [PATCH 10/28] make user media stats list mobile-friendly; add human-readable media size helper --- src/components/media.tsx | 44 +++++++++++-------------- src/resources/user_media_statistics.tsx | 37 +++++++++++++++++++-- src/resources/users.tsx | 6 ++-- src/utils/formatBytes.ts | 8 +++++ 4 files changed, 66 insertions(+), 29 deletions(-) create mode 100644 src/utils/formatBytes.ts diff --git a/src/components/media.tsx b/src/components/media.tsx index 539a811c..000fcb2b 100644 --- a/src/components/media.tsx +++ b/src/components/media.tsx @@ -1,5 +1,4 @@ import BlockIcon from "@mui/icons-material/Block"; -import IconCancel from "@mui/icons-material/Cancel"; import ClearIcon from "@mui/icons-material/Clear"; import DeleteSweepIcon from "@mui/icons-material/DeleteSweep"; import DownloadIcon from "@mui/icons-material/Download"; @@ -8,7 +7,16 @@ import FileOpenIcon from "@mui/icons-material/FileOpen"; import LockIcon from "@mui/icons-material/Lock"; import LockOpenIcon from "@mui/icons-material/LockOpen"; import RemoveCircleOutlineIcon from "@mui/icons-material/RemoveCircleOutline"; -import { Box, Dialog, DialogContent, DialogContentText, DialogTitle, Tooltip } from "@mui/material"; +import { + Box, + Dialog, + DialogActions, + DialogContent, + DialogContentText, + DialogTitle, + Button as MuiButton, + Tooltip, +} from "@mui/material"; import { alpha, useTheme } from "@mui/material/styles"; import useMediaQuery from "@mui/material/useMediaQuery"; import { useMutation } from "@tanstack/react-query"; @@ -22,8 +30,6 @@ import { NumberInput, SaveButton, SimpleForm, - Toolbar, - ToolbarProps, useDataProvider, useNotify, useRecordContext, @@ -40,24 +46,19 @@ const DeleteMediaDialog = ({ open, onClose, onSubmit }) => { const fullScreen = useMediaQuery(theme.breakpoints.down("sm")); const translate = useTranslate(); - const DeleteMediaToolbar = (props: ToolbarProps) => ( - <Toolbar {...props}> - <SaveButton label="delete_media.action.send" icon={<DeleteSweepIcon />} /> - <Button label="ra.action.cancel" onClick={onClose}> - <IconCancel /> - </Button> - </Toolbar> - ); - return ( <Dialog open={open} onClose={onClose} maxWidth="sm" fullWidth fullScreen={fullScreen}> <DialogTitle>{translate("delete_media.action.send")}</DialogTitle> <DialogContent> <DialogContentText>{translate("delete_media.helper.send")}</DialogContentText> - <SimpleForm toolbar={<DeleteMediaToolbar />} onSubmit={onSubmit}> + <SimpleForm toolbar={false} onSubmit={onSubmit}> <DateTimeInput source="before_ts" label="delete_media.fields.before_ts" defaultValue={0} parse={dateParser} /> <NumberInput source="size_gt" label="delete_media.fields.size_gt" defaultValue={0} min={0} step={1024} /> <BooleanInput source="keep_profiles" label="delete_media.fields.keep_profiles" defaultValue={true} /> + <DialogActions sx={{ width: "100%", px: 0 }}> + <MuiButton onClick={onClose}>{translate("ra.action.cancel")}</MuiButton> + <SaveButton label="delete_media.action.send" icon={<DeleteSweepIcon />} /> + </DialogActions> </SimpleForm> </DialogContent> </Dialog> @@ -116,27 +117,22 @@ const PurgeRemoteMediaDialog = ({ open, onClose, onSubmit }) => { const fullScreen = useMediaQuery(theme.breakpoints.down("sm")); const translate = useTranslate(); - const PurgeRemoteMediaToolbar = (props: ToolbarProps) => ( - <Toolbar {...props}> - <SaveButton label="purge_remote_media.action.send" icon={<DeleteSweepIcon />} /> - <Button label="ra.action.cancel" onClick={onClose}> - <IconCancel /> - </Button> - </Toolbar> - ); - return ( <Dialog open={open} onClose={onClose} maxWidth="sm" fullWidth fullScreen={fullScreen}> <DialogTitle>{translate("purge_remote_media.action.send")}</DialogTitle> <DialogContent> <DialogContentText>{translate("purge_remote_media.helper.send")}</DialogContentText> - <SimpleForm toolbar={<PurgeRemoteMediaToolbar />} onSubmit={onSubmit}> + <SimpleForm toolbar={false} onSubmit={onSubmit}> <DateTimeInput source="before_ts" label="purge_remote_media.fields.before_ts" defaultValue={0} parse={dateParser} /> + <DialogActions sx={{ width: "100%", px: 0 }}> + <MuiButton onClick={onClose}>{translate("ra.action.cancel")}</MuiButton> + <SaveButton label="purge_remote_media.action.send" icon={<DeleteSweepIcon />} /> + </DialogActions> </SimpleForm> </DialogContent> </Dialog> diff --git a/src/resources/user_media_statistics.tsx b/src/resources/user_media_statistics.tsx index f1860e3d..d4093df5 100644 --- a/src/resources/user_media_statistics.tsx +++ b/src/resources/user_media_statistics.tsx @@ -1,9 +1,12 @@ import PermMediaIcon from "@mui/icons-material/PermMedia"; +import { Box, useMediaQuery } from "@mui/material"; +import { useTheme } from "@mui/material/styles"; import EmptyState from "../components/EmptyState"; import { BooleanField, DatagridConfigurable, ExportButton, + FunctionField, List, ListProps, NumberField, @@ -11,6 +14,7 @@ import { ReferenceField, ResourceProps, SearchInput, + SimpleList, TextField, TopToolbar, useListContext, @@ -19,12 +23,13 @@ import { import AvatarField from "../components/AvatarField"; import { useDocTitle } from "../components/hooks/useDocTitle"; +import { formatBytes } from "../utils/formatBytes"; import { DeleteMediaButton, PurgeRemoteMediaButton } from "../components/media"; const ListActions = () => { const { isLoading, total } = useListContext(); return ( - <TopToolbar sx={{ flexWrap: "wrap", gap: 0.5, whiteSpace: "normal" }}> + <TopToolbar> <DeleteMediaButton /> <PurgeRemoteMediaButton /> <ExportButton disabled={isLoading || total === 0} /> @@ -38,6 +43,8 @@ const userMediaStatsFilters = [<SearchInput source="search_term" alwaysOn />]; export const UserMediaStatsList = (props: ListProps) => { const translate = useTranslate(); + const theme = useTheme(); + const isSmall = useMediaQuery(theme.breakpoints.down("sm")); useDocTitle(translate("resources.user_media_statistics.name", { smart_count: 2 })); return ( <List @@ -49,6 +56,31 @@ export const UserMediaStatsList = (props: ListProps) => { perPage={50} empty={<EmptyState />} > + {isSmall ? ( + <SimpleList + primaryText={record => ( + <Box component="span" sx={{ wordBreak: "break-all" }}> + {record.displayname || record.user_id} + </Box> + )} + secondaryText={record => ( + <> + {record.displayname && ( + <> + <Box component="span" sx={{ wordBreak: "break-all" }}> + {record.user_id} + </Box> + <br /> + </> + )} + {translate("resources.user_media_statistics.fields.media_count")}: {record.media_count} + {" · "} + {translate("resources.user_media_statistics.fields.media_length")}: {formatBytes(record.media_length)} + </> + )} + rowClick={(id) => "/users/" + id + "/media"} + /> + ) : ( <DatagridConfigurable rowClick={id => "/users/" + id + "/media"} bulkActionButtons={false}> <ReferenceField label="resources.users.fields.avatar" source="id" reference="users" sortable={false} link=""> <AvatarField source="avatar_src" sx={{ height: "40px", width: "40px" }} /> @@ -56,7 +88,7 @@ export const UserMediaStatsList = (props: ListProps) => { <TextField source="user_id" label="resources.users.fields.id" /> <TextField source="displayname" label="resources.users.fields.displayname" /> <NumberField source="media_count" /> - <NumberField source="media_length" /> + <FunctionField source="media_length" render={record => formatBytes(record.media_length)} /> <ReferenceField label="resources.users.fields.is_guest" source="id" reference="users" sortable={false} link=""> <BooleanField source="is_guest" label="resources.users.fields.is_guest" /> </ReferenceField> @@ -76,6 +108,7 @@ export const UserMediaStatsList = (props: ListProps) => { <BooleanField source="erased" sortable={false} label="resources.users.fields.erased" /> </ReferenceField> </DatagridConfigurable> + )} </List> ); }; diff --git a/src/resources/users.tsx b/src/resources/users.tsx index 51440608..8e75870d 100644 --- a/src/resources/users.tsx +++ b/src/resources/users.tsx @@ -64,7 +64,6 @@ import { BulkDeleteButton, TopToolbar, Toolbar, - NumberField, useListContext, useNotify, Identifier, @@ -106,6 +105,7 @@ import { GetConfig } from "../utils/config"; import { DATE_FORMAT } from "../utils/date"; import { decodeURLComponent } from "../utils/safety"; import { isASManaged } from "../utils/mxid"; +import { formatBytes } from "../utils/formatBytes"; import { generateRandomPassword } from "../utils/password"; const choices_medium = [ @@ -121,7 +121,7 @@ const choices_type = [ const UserListActions = () => { const { isLoading, total } = useListContext(); return ( - <TopToolbar sx={{ flexWrap: "wrap", gap: 0.5, whiteSpace: "normal" }}> + <TopToolbar> <FindUserButton /> <CreateButton /> <ExportButton disabled={isLoading || total === 0} maxResults={10000} /> @@ -819,7 +819,7 @@ export const UserEdit = (props: EditProps) => { <MediaIDField source="media_id" /> <DateField source="created_ts" showTime options={DATE_FORMAT} locales={locale} /> <DateField source="last_access_ts" showTime options={DATE_FORMAT} locales={locale} /> - <NumberField source="media_length" /> + <FunctionField source="media_length" render={record => formatBytes(record.media_length)} /> <TextField source="media_type" sx={{ display: "block", width: 200, wordBreak: "break-word" }} /> <FunctionField source="upload_name" diff --git a/src/utils/formatBytes.ts b/src/utils/formatBytes.ts new file mode 100644 index 00000000..7fd605bc --- /dev/null +++ b/src/utils/formatBytes.ts @@ -0,0 +1,8 @@ +const units = ["B", "KB", "MB", "GB", "TB"]; + +export const formatBytes = (bytes: number): string => { + if (bytes === 0) return "0 B"; + const i = Math.floor(Math.log(bytes) / Math.log(1024)); + const index = Math.min(i, units.length - 1); + return `${(bytes / Math.pow(1024, index)).toFixed(index === 0 ? 0 : 1)} ${units[index]}`; +}; From 9637fbd5580f74d782aebbdc94fba0e00f5fd0c9 Mon Sep 17 00:00:00 2001 From: Aine <aine@etke.cc> Date: Sat, 21 Mar 2026 10:49:38 +0000 Subject: [PATCH 11/28] make room directory mobile-friendly --- src/resources/room_directory.tsx | 76 +++++++++++++++++++++++--------- 1 file changed, 55 insertions(+), 21 deletions(-) diff --git a/src/resources/room_directory.tsx b/src/resources/room_directory.tsx index 816281d3..acf5f734 100644 --- a/src/resources/room_directory.tsx +++ b/src/resources/room_directory.tsx @@ -1,4 +1,6 @@ import RoomDirectoryIcon from "@mui/icons-material/FolderShared"; +import { Box, useMediaQuery } from "@mui/material"; +import { useTheme } from "@mui/material/styles"; import EmptyState from "../components/EmptyState"; import { useMutation } from "@tanstack/react-query"; import { @@ -16,6 +18,7 @@ import { Pagination, ResourceProps, SelectColumnsButton, + SimpleList, TextField, TopToolbar, useCreate, @@ -132,15 +135,21 @@ export const RoomDirectoryPublishButton = (props: ButtonProps) => { ); }; -const RoomDirectoryListActions = () => ( - <TopToolbar> - <SelectColumnsButton /> - <ExportButton /> - </TopToolbar> -); +const RoomDirectoryListActions = () => { + const theme = useTheme(); + const isSmall = useMediaQuery(theme.breakpoints.down("sm")); + return ( + <TopToolbar> + {!isSmall && <SelectColumnsButton />} + <ExportButton /> + </TopToolbar> + ); +}; export const RoomDirectoryList = () => { const translate = useTranslate(); + const theme = useTheme(); + const isSmall = useMediaQuery(theme.breakpoints.down("sm")); useDocTitle(translate("resources.room_directory.name", { smart_count: 2 })); return ( <List @@ -149,21 +158,46 @@ export const RoomDirectoryList = () => { actions={<RoomDirectoryListActions />} empty={<EmptyState />} > - <DatagridConfigurable - rowClick={id => "/rooms/" + id + "/show"} - bulkActionButtons={<RoomDirectoryBulkUnpublishButton />} - omit={["room_id", "canonical_alias", "topic"]} - > - <AvatarField source="avatar_src" sx={{ height: "40px", width: "40px" }} label="resources.rooms.fields.avatar" /> - <TextField source="name" sortable={false} label="resources.rooms.fields.name" /> - <TextField source="room_id" sortable={false} label="resources.rooms.fields.room_id" /> - <TextField source="canonical_alias" sortable={false} label="resources.rooms.fields.canonical_alias" /> - <TextField source="topic" sortable={false} label="resources.rooms.fields.topic" /> - <NumberField source="num_joined_members" sortable={false} label="resources.rooms.fields.joined_members" /> - <BooleanField source="world_readable" sortable={false} label="resources.room_directory.fields.world_readable" /> - <BooleanField source="guest_can_join" sortable={false} label="resources.room_directory.fields.guest_can_join" /> - <MakeAdminBtn /> - </DatagridConfigurable> + {isSmall ? ( + <SimpleList + primaryText={record => ( + <Box component="span" sx={{ wordBreak: "break-all" }}> + {record.name || record.canonical_alias || record.room_id} + </Box> + )} + secondaryText={record => ( + <> + {record.canonical_alias && ( + <> + <Box component="span" sx={{ wordBreak: "break-all" }}> + {record.canonical_alias} + </Box> + <br /> + </> + )} + {translate("resources.rooms.fields.joined_members")}: {record.num_joined_members ?? 0} + </> + )} + linkType="show" + leftAvatar={record => <AvatarField record={record} source="avatar_src" sx={{ height: "40px", width: "40px" }} />} + /> + ) : ( + <DatagridConfigurable + rowClick={id => "/rooms/" + id + "/show"} + bulkActionButtons={<RoomDirectoryBulkUnpublishButton />} + omit={["room_id", "canonical_alias", "topic"]} + > + <AvatarField source="avatar_src" sx={{ height: "40px", width: "40px" }} label="resources.rooms.fields.avatar" /> + <TextField source="name" sortable={false} label="resources.rooms.fields.name" /> + <TextField source="room_id" sortable={false} label="resources.rooms.fields.room_id" /> + <TextField source="canonical_alias" sortable={false} label="resources.rooms.fields.canonical_alias" /> + <TextField source="topic" sortable={false} label="resources.rooms.fields.topic" /> + <NumberField source="num_joined_members" sortable={false} label="resources.rooms.fields.joined_members" /> + <BooleanField source="world_readable" sortable={false} label="resources.room_directory.fields.world_readable" /> + <BooleanField source="guest_can_join" sortable={false} label="resources.room_directory.fields.guest_can_join" /> + <MakeAdminBtn /> + </DatagridConfigurable> + )} </List> ); }; From aa31b6bd4f4315f4c1b53f6713d6b5faffc6d4bd Mon Sep 17 00:00:00 2001 From: Aine <aine@etke.cc> Date: Sat, 21 Mar 2026 10:55:50 +0000 Subject: [PATCH 12/28] make destinations table mobile-friendly --- src/resources/destinations.tsx | 77 ++++++++++++++++++++++------------ 1 file changed, 51 insertions(+), 26 deletions(-) diff --git a/src/resources/destinations.tsx b/src/resources/destinations.tsx index 66b7405d..b03d46f5 100644 --- a/src/resources/destinations.tsx +++ b/src/resources/destinations.tsx @@ -4,6 +4,8 @@ import ErrorIcon from "@mui/icons-material/Error"; import FolderSharedIcon from "@mui/icons-material/FolderShared"; import EmptyState from "../components/EmptyState"; import ViewListIcon from "@mui/icons-material/ViewList"; +import { Box, useMediaQuery } from "@mui/material"; +import { useTheme } from "@mui/material/styles"; import { get } from "lodash"; import { MouseEvent } from "react"; import { @@ -20,6 +22,7 @@ import { SearchInput, Show, ShowProps, + SimpleList, Tab, TabbedShowLayout, TextField, @@ -118,6 +121,8 @@ const destinationFieldRender = (record: RaRecord) => { export const DestinationList = (props: ListProps) => { const locale = useLocale(); const translate = useTranslate(); + const theme = useTheme(); + const isSmall = useMediaQuery(theme.breakpoints.down("sm")); useDocTitle(translate("resources.destinations.name", 2)); return ( <List @@ -128,33 +133,53 @@ export const DestinationList = (props: ListProps) => { perPage={50} empty={<EmptyState />} > - <DatagridConfigurable rowClick={id => `${id}/show/rooms`} bulkActionButtons={false}> - <FunctionField - source="destination" - render={destinationFieldRender} - label="resources.destinations.fields.destination" + {isSmall ? ( + <SimpleList + primaryText={record => ( + <Box component="span" sx={{ wordBreak: "break-all" }}> + {record.destination} + </Box> + )} + secondaryText={record => + record.failure_ts ? ( + <Box component="span" sx={{ display: "inline-flex", alignItems: "center", gap: 0.5 }}> + <ErrorIcon fontSize="inherit" color="error" /> + {translate("resources.destinations.fields.failure_ts")}: {new Date(record.failure_ts).toLocaleString(locale)} + </Box> + ) : null + } + tertiaryText={record => (record.failure_ts ? <DestinationReconnectButton /> : null)} + rowClick={id => `${id}/show/rooms`} /> - <DateField - source="failure_ts" - showTime - options={DATE_FORMAT} - label="resources.destinations.fields.failure_ts" - locales={locale} - /> - <RetryDateField - source="retry_last_ts" - showTime - options={DATE_FORMAT} - label="resources.destinations.fields.retry_last_ts" - locales={locale} - /> - <TextField source="retry_interval" label="resources.destinations.fields.retry_interval" /> - <TextField - source="last_successful_stream_ordering" - label="resources.destinations.fields.last_successful_stream_ordering" - /> - <DestinationReconnectButton /> - </DatagridConfigurable> + ) : ( + <DatagridConfigurable rowClick={id => `${id}/show/rooms`} bulkActionButtons={false}> + <FunctionField + source="destination" + render={destinationFieldRender} + label="resources.destinations.fields.destination" + /> + <DateField + source="failure_ts" + showTime + options={DATE_FORMAT} + label="resources.destinations.fields.failure_ts" + locales={locale} + /> + <RetryDateField + source="retry_last_ts" + showTime + options={DATE_FORMAT} + label="resources.destinations.fields.retry_last_ts" + locales={locale} + /> + <TextField source="retry_interval" label="resources.destinations.fields.retry_interval" /> + <TextField + source="last_successful_stream_ordering" + label="resources.destinations.fields.last_successful_stream_ordering" + /> + <DestinationReconnectButton /> + </DatagridConfigurable> + )} </List> ); }; From 30768df84ff76408cbed1e60f0734101ab89e927 Mon Sep 17 00:00:00 2001 From: Aine <aine@etke.cc> Date: Sat, 21 Mar 2026 11:05:39 +0000 Subject: [PATCH 13/28] make registration tokens mobile friendly --- src/resources/registration_tokens.tsx | 34 ++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/src/resources/registration_tokens.tsx b/src/resources/registration_tokens.tsx index 96f4a1bc..97afa16d 100644 --- a/src/resources/registration_tokens.tsx +++ b/src/resources/registration_tokens.tsx @@ -1,6 +1,8 @@ import BlockIcon from "@mui/icons-material/Block"; import RegistrationTokenIcon from "@mui/icons-material/ConfirmationNumber"; import RestoreIcon from "@mui/icons-material/RestoreFromTrash"; +import { Box, useMediaQuery } from "@mui/material"; +import { useTheme } from "@mui/material/styles"; import EmptyState from "../components/EmptyState"; import { useState } from "react"; import { @@ -11,6 +13,7 @@ import { DatagridConfigurable, DateField, DateTimeInput, + DeleteButton, Edit, EditProps, List, @@ -23,6 +26,7 @@ import { ResourceProps, SaveButton, SimpleForm, + SimpleList, TextInput, TextField, Toolbar, @@ -48,6 +52,8 @@ const registrationTokenFilters = [<BooleanInput key="valid" source="valid" />]; export const RegistrationTokenList = (props: ListProps) => { const locale = useLocale(); const translate = useTranslate(); + const theme = useTheme(); + const isSmall = useMediaQuery(theme.breakpoints.down("sm")); const isMAS = useIsMAS(); useDocTitle(translate("resources.registration_tokens.name", { smart_count: 2 })); return ( @@ -59,6 +65,30 @@ export const RegistrationTokenList = (props: ListProps) => { perPage={50} empty={<EmptyState />} > + {isSmall ? ( + <SimpleList + primaryText={record => ( + <Box component="span" sx={{ wordBreak: "break-all" }}> + {record.token} + </Box> + )} + secondaryText={record => ( + <> + {translate("resources.registration_tokens.fields.uses_allowed")}: {record.uses_allowed ?? "∞"} + {" · "} + {translate("resources.registration_tokens.fields.completed")}: {record.completed ?? 0} + {record.expiry_time && ( + <> + <br /> + {translate("resources.registration_tokens.fields.expiry_time")}: {new Date(record.expiry_time).toLocaleString(locale)} + </> + )} + </> + )} + tertiaryText={() => <DeleteButton redirect={false} />} + linkType="edit" + /> + ) : ( <DatagridConfigurable rowClick="edit"> <TextField source="token" sortable={false} label="resources.registration_tokens.fields.token" /> <NumberField source="uses_allowed" sortable={false} label="resources.registration_tokens.fields.uses_allowed" /> @@ -103,6 +133,7 @@ export const RegistrationTokenList = (props: ListProps) => { /> )} </DatagridConfigurable> + )} </List> ); }; @@ -184,9 +215,10 @@ const RevokeTokenButton = () => { }; const RegistrationTokenEditToolbar = () => ( - <Toolbar> + <Toolbar sx={{ justifyContent: "space-between" }}> <SaveButton /> <RevokeTokenButton /> + <DeleteButton redirect="list" /> </Toolbar> ); From b58d50e929064533a50a8e3b8c314e68f7b3676e Mon Sep 17 00:00:00 2001 From: Aine <aine@etke.cc> Date: Sat, 21 Mar 2026 11:14:26 +0000 Subject: [PATCH 14/28] make user devices list mobile-friendly --- src/components/DeviceCreateButton.tsx | 8 ++--- src/resources/users.tsx | 50 +++++++++++++++++++-------- 2 files changed, 40 insertions(+), 18 deletions(-) diff --git a/src/components/DeviceCreateButton.tsx b/src/components/DeviceCreateButton.tsx index d5d20ea0..b234fe55 100644 --- a/src/components/DeviceCreateButton.tsx +++ b/src/components/DeviceCreateButton.tsx @@ -5,7 +5,7 @@ import { Button as MuiButton, Dialog, DialogActions, DialogContent, DialogTitle, import { useTheme } from "@mui/material/styles"; import useMediaQuery from "@mui/material/useMediaQuery"; import { useState } from "react"; -import { Button, useNotify, useRecordContext, useRefresh, useTranslate } from "react-admin"; +import { useNotify, useRecordContext, useRefresh, useTranslate } from "react-admin"; import { jsonClient } from "../providers/httpClients"; import { invalidateManyRefCache } from "../providers/resourceMap"; @@ -50,9 +50,9 @@ const DeviceCreateButton = () => { return ( <> - <Button label="resources.devices.action.create.label" onClick={() => setOpen(true)}> - <AddIcon /> - </Button> + <MuiButton variant="outlined" size="small" startIcon={<AddIcon />} onClick={() => setOpen(true)} fullWidth={fullScreen}> + {translate("resources.devices.action.create.label")} + </MuiButton> <Dialog open={open} onClose={handleClose} maxWidth="sm" fullWidth fullScreen={fullScreen}> <DialogTitle>{translate("resources.devices.action.create.title")}</DialogTitle> <DialogContent> diff --git a/src/resources/users.tsx b/src/resources/users.tsx index 8e75870d..57a3e95e 100644 --- a/src/resources/users.tsx +++ b/src/resources/users.tsx @@ -645,6 +645,7 @@ const ErasedBooleanInput = props => { export const UserEdit = (props: EditProps) => { const translate = useTranslate(); const theme = useTheme(); + const isSmall = useMediaQuery(theme.breakpoints.down("sm")); const locale = useLocale(); return ( @@ -771,20 +772,41 @@ export const UserEdit = (props: EditProps) => { perPage={10} > <Box sx={{ width: "100%" }}> - <DatagridConfigurable - bulkActionButtons={<DeviceBulkRemoveButton />} - omit={["last_seen_user_agent", "dehydrated"]} - > - <TextField source="device_id" sortable={false} /> - <DeviceDisplayNameInput /> - <TextField source="last_seen_ip" sortable={false} /> - <TextField source="last_seen_user_agent" sortable={false} /> - <DateField source="last_seen_ts" showTime options={DATE_FORMAT} sortable={false} locales={locale} /> - <BooleanField source="dehydrated" sortable={false} /> - <WrapperField label="resources.rooms.fields.actions"> - <DeviceRemoveButton /> - </WrapperField> - </DatagridConfigurable> + {isSmall ? ( + <SimpleList + primaryText={record => ( + <Box component="span" sx={{ wordBreak: "break-all" }}> + {record.device_id} + </Box> + )} + secondaryText={record => ( + <> + {record.last_seen_ip && <>{record.last_seen_ip}<br /></>} + {record.last_seen_ts && new Date(record.last_seen_ts).toLocaleString(locale)} + <Box sx={{ mt: 1 }}> + <DeviceDisplayNameInput /> + </Box> + </> + )} + tertiaryText={() => <DeviceRemoveButton />} + linkType={false} + /> + ) : ( + <DatagridConfigurable + bulkActionButtons={<DeviceBulkRemoveButton />} + omit={["last_seen_user_agent", "dehydrated"]} + > + <TextField source="device_id" sortable={false} /> + <DeviceDisplayNameInput /> + <TextField source="last_seen_ip" sortable={false} /> + <TextField source="last_seen_user_agent" sortable={false} /> + <DateField source="last_seen_ts" showTime options={DATE_FORMAT} sortable={false} locales={locale} /> + <BooleanField source="dehydrated" sortable={false} /> + <WrapperField label="resources.rooms.fields.actions"> + <DeviceRemoveButton /> + </WrapperField> + </DatagridConfigurable> + )} </Box> </ReferenceManyField> </FormTab> From 84b46a999c5e1f962b69beca07699379f4030d19 Mon Sep 17 00:00:00 2001 From: Aine <aine@etke.cc> Date: Sat, 21 Mar 2026 11:16:56 +0000 Subject: [PATCH 15/28] make user connections mobile friendly --- src/resources/users.tsx | 30 +++++++++++++++++++++++++----- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/src/resources/users.tsx b/src/resources/users.tsx index 57a3e95e..b9b511f0 100644 --- a/src/resources/users.tsx +++ b/src/resources/users.tsx @@ -814,11 +814,31 @@ export const UserEdit = (props: EditProps) => { <FormTab label="resources.connections.name" icon={<SettingsInputComponentIcon />} path="connections"> <ReferenceField reference="connections" source="id" label={false} link={false}> <ArrayField source="devices[].sessions[0].connections" label="resources.connections.name"> - <DatagridConfigurable sx={{ width: "100%" }} bulkActionButtons={false}> - <TextField source="ip" sortable={false} /> - <DateField source="last_seen" showTime options={DATE_FORMAT} sortable={false} locales={locale} /> - <TextField source="user_agent" sortable={false} style={{ width: "100%" }} /> - </DatagridConfigurable> + {isSmall ? ( + <SimpleList + primaryText={record => record.ip} + secondaryText={record => ( + <> + {record.last_seen && new Date(record.last_seen).toLocaleString(locale)} + {record.user_agent && ( + <> + <br /> + <Box component="span" sx={{ wordBreak: "break-all" }}> + {record.user_agent} + </Box> + </> + )} + </> + )} + linkType={false} + /> + ) : ( + <DatagridConfigurable sx={{ width: "100%" }} bulkActionButtons={false}> + <TextField source="ip" sortable={false} /> + <DateField source="last_seen" showTime options={DATE_FORMAT} sortable={false} locales={locale} /> + <TextField source="user_agent" sortable={false} style={{ width: "100%" }} /> + </DatagridConfigurable> + )} </ArrayField> </ReferenceField> </FormTab> From ea37a1ac49257acbd95ee60fc19bf87d7cfa61f0 Mon Sep 17 00:00:00 2001 From: Aine <aine@etke.cc> Date: Sat, 21 Mar 2026 11:18:46 +0000 Subject: [PATCH 16/28] make user media list mobile-friendly --- src/resources/users.tsx | 54 ++++++++++++++++++++++++++++++----------- 1 file changed, 40 insertions(+), 14 deletions(-) diff --git a/src/resources/users.tsx b/src/resources/users.tsx index b9b511f0..c017a5ca 100644 --- a/src/resources/users.tsx +++ b/src/resources/users.tsx @@ -857,21 +857,47 @@ export const UserEdit = (props: EditProps) => { perPage={10} sort={{ field: "created_ts", order: "DESC" }} > - <DatagridConfigurable sx={{ width: "100%" }} bulkActionButtons={<BulkDeleteButton />}> - <MediaIDField source="media_id" /> - <DateField source="created_ts" showTime options={DATE_FORMAT} locales={locale} /> - <DateField source="last_access_ts" showTime options={DATE_FORMAT} locales={locale} /> - <FunctionField source="media_length" render={record => formatBytes(record.media_length)} /> - <TextField source="media_type" sx={{ display: "block", width: 200, wordBreak: "break-word" }} /> - <FunctionField - source="upload_name" - render={record => (record.upload_name ? decodeURLComponent(record.upload_name) : "")} + {isSmall ? ( + <SimpleList + primaryText={record => ( + <Box component="span" sx={{ wordBreak: "break-all" }}> + {record.upload_name ? decodeURLComponent(record.upload_name) : record.media_id} + </Box> + )} + secondaryText={record => ( + <> + {formatBytes(record.media_length)} + {record.media_type && <> · {record.media_type}</>} + <br /> + {new Date(record.created_ts).toLocaleString(locale)} + </> + )} + tertiaryText={() => ( + <Box sx={{ display: "flex", gap: 0.5 }}> + <QuarantineMediaButton /> + <ProtectMediaButton /> + <DeleteButton mutationMode="pessimistic" redirect={false} /> + </Box> + )} + linkType={false} /> - <TextField source="quarantined_by" /> - <QuarantineMediaButton /> - <ProtectMediaButton /> - <DeleteButton mutationMode="pessimistic" redirect={false} /> - </DatagridConfigurable> + ) : ( + <DatagridConfigurable sx={{ width: "100%" }} bulkActionButtons={<BulkDeleteButton />}> + <MediaIDField source="media_id" /> + <DateField source="created_ts" showTime options={DATE_FORMAT} locales={locale} /> + <DateField source="last_access_ts" showTime options={DATE_FORMAT} locales={locale} /> + <FunctionField source="media_length" render={record => formatBytes(record.media_length)} /> + <TextField source="media_type" sx={{ display: "block", width: 200, wordBreak: "break-word" }} /> + <FunctionField + source="upload_name" + render={record => (record.upload_name ? decodeURLComponent(record.upload_name) : "")} + /> + <TextField source="quarantined_by" /> + <QuarantineMediaButton /> + <ProtectMediaButton /> + <DeleteButton mutationMode="pessimistic" redirect={false} /> + </DatagridConfigurable> + )} </ReferenceManyField> </FormTab> From c048c402af2e0769782b59e9f95c3cfcab7e19fc Mon Sep 17 00:00:00 2001 From: Aine <aine@etke.cc> Date: Sat, 21 Mar 2026 11:36:19 +0000 Subject: [PATCH 17/28] make user joined rooms list mobile-friendly --- src/resources/users.tsx | 123 ++++++++++++++++++++++++++++------------ 1 file changed, 86 insertions(+), 37 deletions(-) diff --git a/src/resources/users.tsx b/src/resources/users.tsx index c017a5ca..5a91b388 100644 --- a/src/resources/users.tsx +++ b/src/resources/users.tsx @@ -18,7 +18,7 @@ import LockIcon from "@mui/icons-material/Lock"; import NoAccountsIcon from "@mui/icons-material/NoAccounts"; import AdminPanelSettingsIcon from "@mui/icons-material/AdminPanelSettings"; import VisibilityOffIcon from "@mui/icons-material/VisibilityOff"; -import { Alert, Box, Divider, Paper, Tooltip, Typography } from "@mui/material"; +import { Alert, Box, Divider, List as MuiList, ListItemButton, Paper, Tooltip, Typography } from "@mui/material"; import EmptyState from "../components/EmptyState"; import { useTheme } from "@mui/material/styles"; import useMediaQuery from "@mui/material/useMediaQuery"; @@ -75,6 +75,7 @@ import { useRedirect, useLocale, SimpleList, + useGetMany, } from "react-admin"; import { useFormContext } from "react-hook-form"; import { Link } from "react-router-dom"; @@ -642,6 +643,50 @@ const ErasedBooleanInput = props => { return <UserBooleanInput disabled={!deactivated} {...props} />; }; +const JoinedRoomsMobileList = () => { + const { data: joinedRooms } = useListContext(); + const translate = useTranslate(); + const ids = (joinedRooms || []).map(r => r.id); + const { data: rooms } = useGetMany("rooms", { ids }, { enabled: ids.length > 0 }); + const roomMap = new Map((rooms || []).map(r => [r.id, r])); + + if (!joinedRooms?.length) return null; + + return ( + <MuiList disablePadding> + {joinedRooms.map(record => { + const room = roomMap.get(record.id); + return ( + <ListItemButton + key={record.id as string} + component={Link} + to={"/rooms/" + record.id + "/show"} + sx={{ gap: 1, alignItems: "center" }} + > + <AvatarField record={room || record} source="avatar" sx={{ height: "40px", width: "40px" }} /> + <Box sx={{ flex: 1, minWidth: 0 }}> + <Typography variant="body1" sx={{ wordBreak: "break-all" }}> + {room?.name || room?.canonical_alias || record.id} + </Typography> + <Typography variant="body2" color="text.secondary"> + {translate("resources.rooms.fields.joined_members")}: {room?.joined_members ?? 0} + {room?.creator && ( + <> + <br /> + <Box component="span" sx={{ wordBreak: "break-all" }}> + {translate("resources.rooms.fields.creator")}: {room.creator} + </Box> + </> + )} + </Typography> + </Box> + </ListItemButton> + ); + })} + </MuiList> + ); +}; + export const UserEdit = (props: EditProps) => { const translate = useTranslate(); const theme = useTheme(); @@ -909,43 +954,47 @@ export const UserEdit = (props: EditProps) => { perPage={10} pagination={<Pagination />} > - <DatagridConfigurable - sx={{ width: "100%" }} - rowClick={id => "/rooms/" + id + "/show"} - bulkActionButtons={<RoomBulkActionButtons />} - > - <ReferenceField reference="rooms" source="id" label={false} link={false} sortable={false}> - <AvatarField source="avatar" sx={{ height: "40px", width: "40px" }} /> - </ReferenceField> - <TextField source="id" label="resources.rooms.fields.room_id" sortable={false} /> - <ReferenceField - reference="rooms" - source="id" - label="resources.rooms.fields.name" - link={false} - sortable={false} - > - <TextField - source="name" - sx={{ - wordBreak: "break-word", - overflowWrap: "break-word", - }} - /> - </ReferenceField> - <ReferenceField - reference="rooms" - source="id" - label="resources.rooms.fields.joined_members" - link={false} - sortable={false} + {isSmall ? ( + <JoinedRoomsMobileList /> + ) : ( + <DatagridConfigurable + sx={{ width: "100%" }} + rowClick={id => "/rooms/" + id + "/show"} + bulkActionButtons={<RoomBulkActionButtons />} > - <TextField source="joined_members" sortable={false} /> - </ReferenceField> - <ReferenceField reference="rooms" source="id" label={false} link={false} sortable={false}> - <MakeAdminBtn /> - </ReferenceField> - </DatagridConfigurable> + <ReferenceField reference="rooms" source="id" label={false} link={false} sortable={false}> + <AvatarField source="avatar" sx={{ height: "40px", width: "40px" }} /> + </ReferenceField> + <TextField source="id" label="resources.rooms.fields.room_id" sortable={false} /> + <ReferenceField + reference="rooms" + source="id" + label="resources.rooms.fields.name" + link={false} + sortable={false} + > + <TextField + source="name" + sx={{ + wordBreak: "break-word", + overflowWrap: "break-word", + }} + /> + </ReferenceField> + <ReferenceField + reference="rooms" + source="id" + label="resources.rooms.fields.joined_members" + link={false} + sortable={false} + > + <TextField source="joined_members" sortable={false} /> + </ReferenceField> + <ReferenceField reference="rooms" source="id" label={false} link={false} sortable={false}> + <MakeAdminBtn /> + </ReferenceField> + </DatagridConfigurable> + )} </ReferenceManyField> </FormTab> From f27f4cbf2f3cb362619075c00eac574ce27f13d7 Mon Sep 17 00:00:00 2001 From: Aine <aine@etke.cc> Date: Sat, 21 Mar 2026 11:38:59 +0000 Subject: [PATCH 18/28] make user memberships mobile-friendly --- src/resources/users.tsx | 86 ++++++++++++++++++++++++++++++----------- 1 file changed, 63 insertions(+), 23 deletions(-) diff --git a/src/resources/users.tsx b/src/resources/users.tsx index 5a91b388..11c8ecc8 100644 --- a/src/resources/users.tsx +++ b/src/resources/users.tsx @@ -687,6 +687,42 @@ const JoinedRoomsMobileList = () => { ); }; +const MembershipsMobileList = () => { + const { data: memberships } = useListContext(); + const translate = useTranslate(); + const ids = (memberships || []).map(r => r.id); + const { data: rooms } = useGetMany("rooms", { ids }, { enabled: ids.length > 0 }); + const roomMap = new Map((rooms || []).map(r => [r.id, r])); + + if (!memberships?.length) return null; + + return ( + <MuiList disablePadding> + {memberships.map(record => { + const room = roomMap.get(record.id); + return ( + <ListItemButton + key={record.id as string} + component={Link} + to={"/rooms/" + record.id + "/show"} + sx={{ gap: 1, alignItems: "center" }} + > + <AvatarField record={room || record} source="avatar" sx={{ height: "40px", width: "40px" }} /> + <Box sx={{ flex: 1, minWidth: 0 }}> + <Typography variant="body1" sx={{ wordBreak: "break-all" }}> + {room?.name || record.id} + </Typography> + <Typography variant="body2" color="text.secondary"> + {translate("resources.users.membership", { smart_count: 1 })}: {record.membership} + </Typography> + </Box> + </ListItemButton> + ); + })} + </MuiList> + ); +}; + export const UserEdit = (props: EditProps) => { const translate = useTranslate(); const theme = useTheme(); @@ -1010,30 +1046,34 @@ export const UserEdit = (props: EditProps) => { perPage={10} pagination={<Pagination />} > - <DatagridConfigurable - sx={{ width: "100%" }} - rowClick={id => "/rooms/" + id + "/show"} - bulkActionButtons={false} - > - <ReferenceField reference="rooms" source="id" label={false} link={false} sortable={false}> - <AvatarField source="avatar" sx={{ height: "40px", width: "40px" }} /> - </ReferenceField> - <TextField source="id" label="resources.rooms.fields.room_id" sortable={false} /> - <ReferenceField - reference="rooms" - source="id" - label="resources.rooms.fields.name" - link={false} - sortable={false} + {isSmall ? ( + <MembershipsMobileList /> + ) : ( + <DatagridConfigurable + sx={{ width: "100%" }} + rowClick={id => "/rooms/" + id + "/show"} + bulkActionButtons={false} > - <TextField source="name" /> - </ReferenceField> - <TextField - source="membership" - label={translate("resources.users.membership", { smart_count: 1 })} - sortable={false} - /> - </DatagridConfigurable> + <ReferenceField reference="rooms" source="id" label={false} link={false} sortable={false}> + <AvatarField source="avatar" sx={{ height: "40px", width: "40px" }} /> + </ReferenceField> + <TextField source="id" label="resources.rooms.fields.room_id" sortable={false} /> + <ReferenceField + reference="rooms" + source="id" + label="resources.rooms.fields.name" + link={false} + sortable={false} + > + <TextField source="name" /> + </ReferenceField> + <TextField + source="membership" + label={translate("resources.users.membership", { smart_count: 1 })} + sortable={false} + /> + </DatagridConfigurable> + )} </ReferenceManyField> </FormTab> From aad81e061cbf63a622f281d3783dddfc7ac7210d Mon Sep 17 00:00:00 2001 From: Aine <aine@etke.cc> Date: Sat, 21 Mar 2026 11:40:10 +0000 Subject: [PATCH 19/28] make user pushers mobile-friendly --- src/resources/users.tsx | 49 +++++++++++++++++++++++++++++------------ 1 file changed, 35 insertions(+), 14 deletions(-) diff --git a/src/resources/users.tsx b/src/resources/users.tsx index 11c8ecc8..6f97f575 100644 --- a/src/resources/users.tsx +++ b/src/resources/users.tsx @@ -1089,20 +1089,41 @@ export const UserEdit = (props: EditProps) => { pagination={<Pagination />} perPage={10} > - <DatagridConfigurable - sx={{ width: "100%" }} - bulkActionButtons={false} - omit={["app_id", "data.url", "profile_tag", "pushkey"]} - > - <TextField source="kind" sortable={false} /> - <TextField source="app_display_name" sortable={false} /> - <TextField source="app_id" sortable={false} /> - <TextField source="data.url" sortable={false} /> - <TextField source="device_display_name" sortable={false} /> - <TextField source="lang" sortable={false} /> - <TextField source="profile_tag" sortable={false} /> - <TextField source="pushkey" sortable={false} /> - </DatagridConfigurable> + {isSmall ? ( + <SimpleList + primaryText={record => record.app_display_name || record.app_id} + secondaryText={record => ( + <> + {record.kind} + {record.device_display_name && <> · {record.device_display_name}</>} + {record.pushkey && ( + <> + <br /> + <Box component="span" sx={{ wordBreak: "break-all" }}> + {record.pushkey} + </Box> + </> + )} + </> + )} + linkType={false} + /> + ) : ( + <DatagridConfigurable + sx={{ width: "100%" }} + bulkActionButtons={false} + omit={["app_id", "data.url", "profile_tag", "pushkey"]} + > + <TextField source="kind" sortable={false} /> + <TextField source="app_display_name" sortable={false} /> + <TextField source="app_id" sortable={false} /> + <TextField source="data.url" sortable={false} /> + <TextField source="device_display_name" sortable={false} /> + <TextField source="lang" sortable={false} /> + <TextField source="profile_tag" sortable={false} /> + <TextField source="pushkey" sortable={false} /> + </DatagridConfigurable> + )} </ReferenceManyField> </FormTab> From b2f9aa03906d547a394851d948b5f0a33dc0db88 Mon Sep 17 00:00:00 2001 From: Aine <aine@etke.cc> Date: Sat, 21 Mar 2026 11:43:37 +0000 Subject: [PATCH 20/28] make user account data mobile-friendly --- src/components/UserAccountData.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/UserAccountData.tsx b/src/components/UserAccountData.tsx index 3c49409a..f9ed1844 100644 --- a/src/components/UserAccountData.tsx +++ b/src/components/UserAccountData.tsx @@ -47,7 +47,7 @@ const UserAccountData = () => { <Typography variant="h6">{translate("resources.users.account_data.global")}</Typography> </AccordionSummary> <AccordionDetails> - <Box sx={{ whiteSpace: "pre-wrap" }}>{JSON.stringify(globalAccountData, null, 4)}</Box> + <Box component="pre" sx={{ whiteSpace: "pre-wrap", wordBreak: "break-all", m: 0, p: 2, fontSize: { xs: "0.75rem", sm: "0.85rem" }, bgcolor: "action.hover", borderRadius: 1, overflow: "auto", maxWidth: "100%" }}>{JSON.stringify(globalAccountData, null, 4)}</Box> </AccordionDetails> </Accordion> <Accordion> @@ -55,7 +55,7 @@ const UserAccountData = () => { <Typography variant="h6">{translate("resources.users.account_data.rooms")}</Typography> </AccordionSummary> <AccordionDetails> - <Box sx={{ whiteSpace: "pre-wrap" }}>{JSON.stringify(roomsAccountData, null, 4)}</Box> + <Box component="pre" sx={{ whiteSpace: "pre-wrap", wordBreak: "break-all", m: 0, p: 2, fontSize: { xs: "0.75rem", sm: "0.85rem" }, bgcolor: "action.hover", borderRadius: 1, overflow: "auto", maxWidth: "100%" }}>{JSON.stringify(roomsAccountData, null, 4)}</Box> </AccordionDetails> </Accordion> </Box> From 65073f3f5ce8bf0d6114b07f07167c995dc7416d Mon Sep 17 00:00:00 2001 From: Aine <aine@etke.cc> Date: Sat, 21 Mar 2026 11:48:28 +0000 Subject: [PATCH 21/28] make room members list configurable --- src/resources/rooms.tsx | 161 ++++++++++++++++++++++++++-------------- 1 file changed, 104 insertions(+), 57 deletions(-) diff --git a/src/resources/rooms.tsx b/src/resources/rooms.tsx index 0dc73bf7..1079afa6 100644 --- a/src/resources/rooms.tsx +++ b/src/resources/rooms.tsx @@ -19,6 +19,8 @@ import Card from "@mui/material/Card"; import CardContent from "@mui/material/CardContent"; import Chip from "@mui/material/Chip"; import Divider from "@mui/material/Divider"; +import MuiList from "@mui/material/List"; +import ListItemButton from "@mui/material/ListItemButton"; import TextField from "@mui/material/TextField"; import Tooltip from "@mui/material/Tooltip"; import Typography from "@mui/material/Typography"; @@ -58,7 +60,9 @@ import { DeleteButton, NullableBooleanInput, useLocale, + useGetMany, } from "react-admin"; +import { Link } from "react-router-dom"; import { useDataProvider } from "react-admin"; import { Confirm } from "react-admin"; @@ -474,9 +478,48 @@ const RoomOverviewTab = () => { ); }; +const RoomMembersMobileList = () => { + const { data: members } = useListContext(); + const ids = (members || []).map(r => r.id); + const { data: users } = useGetMany("users", { ids }, { enabled: ids.length > 0 }); + const userMap = new Map((users || []).map(u => [u.id, u])); + + if (!members?.length) return null; + + return ( + <MuiList disablePadding> + {members.map(record => { + const user = userMap.get(record.id); + return ( + <ListItemButton + key={record.id as string} + component={Link} + to={"/users/" + record.id} + sx={{ gap: 1, alignItems: "center" }} + > + <AvatarField record={user || record} source="avatar_src" sx={{ height: "40px", width: "40px" }} /> + <Box sx={{ flex: 1, minWidth: 0 }}> + <Typography variant="body1" sx={{ wordBreak: "break-all" }}> + {user?.displayname || record.id} + </Typography> + {user?.displayname && ( + <Typography variant="body2" color="text.secondary" sx={{ wordBreak: "break-all" }}> + {record.id} + </Typography> + )} + </Box> + </ListItemButton> + ); + })} + </MuiList> + ); +}; + export const RoomShow = (props: ShowProps) => { const translate = useTranslate(); const locale = useLocale(); + const theme = useTheme(); + const isSmall = useMediaQuery(theme.breakpoints.down("sm")); return ( <Show {...props} @@ -498,63 +541,67 @@ export const RoomShow = (props: ShowProps) => { perPage={10} pagination={<RoomPagination />} > - <DatagridConfigurable sx={{ width: "100%" }} rowClick={id => "/users/" + id} bulkActionButtons={false}> - <ReferenceField - label="resources.users.fields.avatar" - source="id" - reference="users" - sortable={false} - link="" - > - <AvatarField source="avatar_src" sx={{ height: "40px", width: "40px" }} /> - </ReferenceField> - <RaTextField source="id" sortable={false} label="resources.users.fields.id" /> - <ReferenceField - label="resources.users.fields.displayname" - source="id" - reference="users" - sortable={false} - link="" - > - <RaTextField source="displayname" sortable={false} /> - </ReferenceField> - <ReferenceField - label="resources.users.fields.is_guest" - source="id" - reference="users" - sortable={false} - link="" - > - <BooleanField source="is_guest" label="resources.users.fields.is_guest" /> - </ReferenceField> - <ReferenceField - label="resources.users.fields.deactivated" - source="id" - reference="users" - sortable={false} - link="" - > - <BooleanField source="deactivated" label="resources.users.fields.deactivated" /> - </ReferenceField> - <ReferenceField - label="resources.users.fields.locked" - source="id" - reference="users" - sortable={false} - link="" - > - <BooleanField source="locked" label="resources.users.fields.locked" /> - </ReferenceField> - <ReferenceField - label="resources.users.fields.erased" - source="id" - reference="users" - sortable={false} - link="" - > - <BooleanField source="erased" sortable={false} label="resources.users.fields.erased" /> - </ReferenceField> - </DatagridConfigurable> + {isSmall ? ( + <RoomMembersMobileList /> + ) : ( + <DatagridConfigurable sx={{ width: "100%" }} rowClick={id => "/users/" + id} bulkActionButtons={false}> + <ReferenceField + label="resources.users.fields.avatar" + source="id" + reference="users" + sortable={false} + link="" + > + <AvatarField source="avatar_src" sx={{ height: "40px", width: "40px" }} /> + </ReferenceField> + <RaTextField source="id" sortable={false} label="resources.users.fields.id" /> + <ReferenceField + label="resources.users.fields.displayname" + source="id" + reference="users" + sortable={false} + link="" + > + <RaTextField source="displayname" sortable={false} /> + </ReferenceField> + <ReferenceField + label="resources.users.fields.is_guest" + source="id" + reference="users" + sortable={false} + link="" + > + <BooleanField source="is_guest" label="resources.users.fields.is_guest" /> + </ReferenceField> + <ReferenceField + label="resources.users.fields.deactivated" + source="id" + reference="users" + sortable={false} + link="" + > + <BooleanField source="deactivated" label="resources.users.fields.deactivated" /> + </ReferenceField> + <ReferenceField + label="resources.users.fields.locked" + source="id" + reference="users" + sortable={false} + link="" + > + <BooleanField source="locked" label="resources.users.fields.locked" /> + </ReferenceField> + <ReferenceField + label="resources.users.fields.erased" + source="id" + reference="users" + sortable={false} + link="" + > + <BooleanField source="erased" sortable={false} label="resources.users.fields.erased" /> + </ReferenceField> + </DatagridConfigurable> + )} </ReferenceManyField> </Tab> From 3f8374d174ce9cd7ce1cc23a7724c6f44e2bff49 Mon Sep 17 00:00:00 2001 From: Aine <aine@etke.cc> Date: Sat, 21 Mar 2026 11:51:33 +0000 Subject: [PATCH 22/28] make rooms media list mobile-friendly --- src/resources/rooms.tsx | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/src/resources/rooms.tsx b/src/resources/rooms.tsx index 1079afa6..f8f9b5ea 100644 --- a/src/resources/rooms.tsx +++ b/src/resources/rooms.tsx @@ -615,10 +615,22 @@ export const RoomShow = (props: ShowProps) => { pagination={<Pagination />} perPage={10} > - <DatagridConfigurable sx={{ width: "100%" }} bulkActionButtons={false}> - <MediaIDField source="media_id" /> - <DeleteButton mutationMode="pessimistic" redirect={false} /> - </DatagridConfigurable> + {isSmall ? ( + <SimpleList + primaryText={() => ( + <Box sx={{ wordBreak: "break-all" }}> + <MediaIDField source="media_id" /> + </Box> + )} + tertiaryText={() => <DeleteButton mutationMode="pessimistic" redirect={false} />} + linkType={false} + /> + ) : ( + <DatagridConfigurable sx={{ width: "100%" }} bulkActionButtons={false}> + <MediaIDField source="media_id" /> + <DeleteButton mutationMode="pessimistic" redirect={false} /> + </DatagridConfigurable> + )} </ReferenceManyField> </Tab> From 5e0bc8168f5b14b11b01a7414f38ecd0a48e4c62 Mon Sep 17 00:00:00 2001 From: Aine <aine@etke.cc> Date: Sat, 21 Mar 2026 11:53:33 +0000 Subject: [PATCH 23/28] make room state events mobile-friendly --- src/resources/rooms.tsx | 59 +++++++++++++++++++++++++++++++++-------- 1 file changed, 48 insertions(+), 11 deletions(-) diff --git a/src/resources/rooms.tsx b/src/resources/rooms.tsx index f8f9b5ea..f671503a 100644 --- a/src/resources/rooms.tsx +++ b/src/resources/rooms.tsx @@ -642,18 +642,55 @@ export const RoomShow = (props: ShowProps) => { pagination={<Pagination />} perPage={10} > - <DatagridConfigurable sx={{ width: "100%" }} bulkActionButtons={false}> - <RaTextField source="type" sortable={false} /> - <DateField source="origin_server_ts" showTime options={DATE_FORMAT} sortable={false} locales={locale} /> - <FunctionField - source="content" - sortable={false} - render={record => `${JSON.stringify(record.content, null, 2)}`} + {isSmall ? ( + <SimpleList + primaryText={record => record.type} + secondaryText={record => ( + <> + {record.origin_server_ts && new Date(record.origin_server_ts).toLocaleString(locale)} + {record.sender && ( + <> + <br /> + <Box component="span" sx={{ wordBreak: "break-all" }}> + {record.sender} + </Box> + </> + )} + <Box + component="pre" + sx={{ + whiteSpace: "pre-wrap", + wordBreak: "break-all", + m: 0, + mt: 0.5, + p: 1, + fontSize: "0.75rem", + bgcolor: "action.hover", + borderRadius: 1, + overflow: "auto", + maxWidth: "100%", + }} + > + {JSON.stringify(record.content, null, 2)} + </Box> + </> + )} + linkType={false} /> - <ReferenceField source="sender" reference="users" sortable={false}> - <RaTextField source="id" /> - </ReferenceField> - </DatagridConfigurable> + ) : ( + <DatagridConfigurable sx={{ width: "100%" }} bulkActionButtons={false}> + <RaTextField source="type" sortable={false} /> + <DateField source="origin_server_ts" showTime options={DATE_FORMAT} sortable={false} locales={locale} /> + <FunctionField + source="content" + sortable={false} + render={record => `${JSON.stringify(record.content, null, 2)}`} + /> + <ReferenceField source="sender" reference="users" sortable={false}> + <RaTextField source="id" /> + </ReferenceField> + </DatagridConfigurable> + )} </ReferenceManyField> </Tab> From c0794c4614715ffd2afefd9812bcfbabc22b576a Mon Sep 17 00:00:00 2001 From: Aine <aine@etke.cc> Date: Sat, 21 Mar 2026 11:55:04 +0000 Subject: [PATCH 24/28] make rooms forward extremities mobile-friendly --- src/resources/rooms.tsx | 29 +++++++++++++++++++++++------ 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/src/resources/rooms.tsx b/src/resources/rooms.tsx index f671503a..25554097 100644 --- a/src/resources/rooms.tsx +++ b/src/resources/rooms.tsx @@ -718,12 +718,29 @@ export const RoomShow = (props: ShowProps) => { pagination={<Pagination />} perPage={10} > - <DatagridConfigurable sx={{ width: "100%" }} bulkActionButtons={false} omit={["depth", "received_ts"]}> - <RaTextField source="id" sortable={false} /> - <DateField source="received_ts" showTime options={DATE_FORMAT} sortable={false} locales={locale} /> - <NumberField source="depth" sortable={false} /> - <RaTextField source="state_group" sortable={false} /> - </DatagridConfigurable> + {isSmall ? ( + <SimpleList + primaryText={record => ( + <Box component="span" sx={{ wordBreak: "break-all" }}> + {record.id} + </Box> + )} + secondaryText={record => ( + <> + {record.received_ts && new Date(record.received_ts).toLocaleString(locale)} + {record.state_group && <> · {translate("resources.forward_extremities.fields.state_group")}: {record.state_group}</>} + </> + )} + linkType={false} + /> + ) : ( + <DatagridConfigurable sx={{ width: "100%" }} bulkActionButtons={false} omit={["depth", "received_ts"]}> + <RaTextField source="id" sortable={false} /> + <DateField source="received_ts" showTime options={DATE_FORMAT} sortable={false} locales={locale} /> + <NumberField source="depth" sortable={false} /> + <RaTextField source="state_group" sortable={false} /> + </DatagridConfigurable> + )} </ReferenceManyField> </Tab> </TabbedShowLayout> From ba2a69a6bbab5d4deb5d164471b40cc872cebae0 Mon Sep 17 00:00:00 2001 From: Aine <aine@etke.cc> Date: Sat, 21 Mar 2026 12:17:58 +0000 Subject: [PATCH 25/28] improve display of long IDs and names on mobile --- src/resources/destinations.tsx | 2 +- src/resources/reports.tsx | 18 +++++++++++----- src/resources/room_directory.tsx | 4 ++-- src/resources/rooms.tsx | 15 +++++++------ src/resources/scheduled_tasks.tsx | 10 ++++++--- src/resources/user_media_statistics.tsx | 2 +- src/resources/users.tsx | 28 +++++++++++++++++-------- 7 files changed, 50 insertions(+), 29 deletions(-) diff --git a/src/resources/destinations.tsx b/src/resources/destinations.tsx index b03d46f5..60a07cce 100644 --- a/src/resources/destinations.tsx +++ b/src/resources/destinations.tsx @@ -212,7 +212,7 @@ export const DestinationShow = (props: ShowProps) => { perPage={50} > <DatagridConfigurable style={{ width: "100%" }} rowClick={id => `/rooms/${id}/show`}> - <TextField source="room_id" label="resources.rooms.fields.room_id" /> + <TextField source="room_id" label="resources.rooms.fields.room_id" sx={{ wordBreak: "break-all" }} /> <TextField source="stream_ordering" sortable={false} /> <ReferenceField label="resources.rooms.fields.name" diff --git a/src/resources/reports.tsx b/src/resources/reports.tsx index 087c2ae2..6ebefc94 100644 --- a/src/resources/reports.tsx +++ b/src/resources/reports.tsx @@ -60,7 +60,7 @@ const RoomInfoField = () => { const parts = [record.id as string]; if (record.canonical_alias) parts.push(record.canonical_alias as string); if (record.name) parts.push(record.name as string); - return <span>{parts.join(" ")}</span>; + return <span style={{ wordBreak: "break-all" }}>{parts.join(" ")}</span>; }; const LabeledField = ({ label, children }: { label: string; children: React.ReactNode }) => ( @@ -113,14 +113,14 @@ const ReportBasicTab = () => { <LabeledField label={translate("resources.reports.fields.user_id")}> <ReferenceField source="user_id" reference="users" link="show" label={false}> <AvatarField source="avatar_src" sx={{ height: "40px", width: "40px" }} /> - <TextField source="id" /> + <TextField source="id" sx={{ wordBreak: "break-all" }} /> </ReferenceField> </LabeledField> <LabeledField label={translate("resources.reports.fields.sender")}> <ReferenceField source="sender" reference="users" link="show" label={false}> <AvatarField source="avatar_src" sx={{ height: "40px", width: "40px" }} /> - <TextField source="id" /> + <TextField source="id" sx={{ wordBreak: "break-all" }} /> </ReferenceField> </LabeledField> @@ -327,7 +327,11 @@ export const ReportList = (props: ListProps) => { > {isSmall ? ( <SimpleList - primaryText={record => `#${record.id} ${record.name || record.room_id}`} + primaryText={record => ( + <Box component="span" sx={{ wordBreak: "break-all" }}> + #{record.id} {record.name || record.room_id} + </Box> + )} secondaryText={record => { const date = new Date(record.received_ts).toLocaleDateString(locale, DATE_FORMAT); const score = @@ -336,7 +340,11 @@ export const ReportList = (props: ListProps) => { }} tertiaryText={record => { if (!record.user_id) return ""; - return record.user_id; + return ( + <Box component="span" sx={{ wordBreak: "break-all" }}> + {record.user_id} + </Box> + ); }} linkType="show" /> diff --git a/src/resources/room_directory.tsx b/src/resources/room_directory.tsx index acf5f734..dac8ce01 100644 --- a/src/resources/room_directory.tsx +++ b/src/resources/room_directory.tsx @@ -189,8 +189,8 @@ export const RoomDirectoryList = () => { > <AvatarField source="avatar_src" sx={{ height: "40px", width: "40px" }} label="resources.rooms.fields.avatar" /> <TextField source="name" sortable={false} label="resources.rooms.fields.name" /> - <TextField source="room_id" sortable={false} label="resources.rooms.fields.room_id" /> - <TextField source="canonical_alias" sortable={false} label="resources.rooms.fields.canonical_alias" /> + <TextField source="room_id" sortable={false} label="resources.rooms.fields.room_id" sx={{ wordBreak: "break-all" }} /> + <TextField source="canonical_alias" sortable={false} label="resources.rooms.fields.canonical_alias" sx={{ wordBreak: "break-all" }} /> <TextField source="topic" sortable={false} label="resources.rooms.fields.topic" /> <NumberField source="num_joined_members" sortable={false} label="resources.rooms.fields.joined_members" /> <BooleanField source="world_readable" sortable={false} label="resources.room_directory.fields.world_readable" /> diff --git a/src/resources/rooms.tsx b/src/resources/rooms.tsx index 25554097..c354ba4c 100644 --- a/src/resources/rooms.tsx +++ b/src/resources/rooms.tsx @@ -359,7 +359,7 @@ const RoomOverviewTab = () => { {record.room_id} </Typography> {record.canonical_alias && ( - <Typography variant="body2" color="text.secondary" sx={{ mb: 0.5 }}> + <Typography variant="body2" color="text.secondary" sx={{ mb: 0.5, wordBreak: "break-all" }}> {record.canonical_alias} </Typography> )} @@ -467,7 +467,7 @@ const RoomOverviewTab = () => { {translate("resources.rooms.fields.creator")} </Typography> <ReferenceField source="creator" reference="users" link="show"> - <RaTextField source="id" /> + <RaTextField source="id" sx={{ wordBreak: "break-all" }} /> </ReferenceField> </Box> </Box> @@ -554,7 +554,7 @@ export const RoomShow = (props: ShowProps) => { > <AvatarField source="avatar_src" sx={{ height: "40px", width: "40px" }} /> </ReferenceField> - <RaTextField source="id" sortable={false} label="resources.users.fields.id" /> + <RaTextField source="id" sortable={false} label="resources.users.fields.id" sx={{ wordBreak: "break-all" }} /> <ReferenceField label="resources.users.fields.displayname" source="id" @@ -687,7 +687,7 @@ export const RoomShow = (props: ShowProps) => { render={record => `${JSON.stringify(record.content, null, 2)}`} /> <ReferenceField source="sender" reference="users" sortable={false}> - <RaTextField source="id" /> + <RaTextField source="id" sx={{ wordBreak: "break-all" }} /> </ReferenceField> </DatagridConfigurable> )} @@ -735,7 +735,7 @@ export const RoomShow = (props: ShowProps) => { /> ) : ( <DatagridConfigurable sx={{ width: "100%" }} bulkActionButtons={false} omit={["depth", "received_ts"]}> - <RaTextField source="id" sortable={false} /> + <RaTextField source="id" sortable={false} sx={{ wordBreak: "break-all" }} /> <DateField source="received_ts" showTime options={DATE_FORMAT} sortable={false} locales={locale} /> <NumberField source="depth" sortable={false} /> <RaTextField source="state_group" sortable={false} /> @@ -868,8 +868,7 @@ export const RoomList = (props: ListProps) => { <FunctionField source="name" sx={{ - wordBreak: "break-word", - overflowWrap: "break-word", + wordBreak: "break-all", }} render={record => record["name"] || record["canonical_alias"] || record["id"]} label="resources.rooms.fields.name" @@ -880,7 +879,7 @@ export const RoomList = (props: ListProps) => { <RaTextField source="version" label="resources.rooms.fields.version" /> <RaTextField source="join_rules" label="resources.rooms.fields.join_rules" /> <ReferenceField source="creator" reference="users"> - <RaTextField source="id" label="resources.rooms.fields.creator" /> + <RaTextField source="id" label="resources.rooms.fields.creator" sx={{ wordBreak: "break-all" }} /> </ReferenceField> <BooleanField source="federatable" label="resources.rooms.fields.federatable" /> <BooleanField source="public" label="resources.rooms.fields.public" /> diff --git a/src/resources/scheduled_tasks.tsx b/src/resources/scheduled_tasks.tsx index 92f0542a..70a0a514 100644 --- a/src/resources/scheduled_tasks.tsx +++ b/src/resources/scheduled_tasks.tsx @@ -1,5 +1,5 @@ import ScheduleIcon from "@mui/icons-material/Schedule"; -import { Chip } from "@mui/material"; +import { Box, Chip } from "@mui/material"; import { useTheme } from "@mui/material/styles"; import useMediaQuery from "@mui/material/useMediaQuery"; import { @@ -89,7 +89,11 @@ export const ScheduledTaskList = (props: ListProps) => { </> )} secondaryText={record => new Date(record.timestamp_ms).toLocaleDateString(locale, DATE_FORMAT)} - tertiaryText={record => record.resource_id || ""} + tertiaryText={record => record.resource_id ? ( + <Box component="span" sx={{ wordBreak: "break-all" }}> + {record.resource_id} + </Box> + ) : ""} linkType={false} /> ) : ( @@ -110,7 +114,7 @@ export const ScheduledTaskList = (props: ListProps) => { label="resources.scheduled_tasks.fields.timestamp" locales={locale} /> - <TextField source="resource_id" sortable={false} label="resources.scheduled_tasks.fields.resource_id" /> + <TextField source="resource_id" sortable={false} label="resources.scheduled_tasks.fields.resource_id" sx={{ wordBreak: "break-all" }} /> <FunctionField source="result" sortable={false} diff --git a/src/resources/user_media_statistics.tsx b/src/resources/user_media_statistics.tsx index d4093df5..d513fef2 100644 --- a/src/resources/user_media_statistics.tsx +++ b/src/resources/user_media_statistics.tsx @@ -85,7 +85,7 @@ export const UserMediaStatsList = (props: ListProps) => { <ReferenceField label="resources.users.fields.avatar" source="id" reference="users" sortable={false} link=""> <AvatarField source="avatar_src" sx={{ height: "40px", width: "40px" }} /> </ReferenceField> - <TextField source="user_id" label="resources.users.fields.id" /> + <TextField source="user_id" label="resources.users.fields.id" sx={{ wordBreak: "break-all" }} /> <TextField source="displayname" label="resources.users.fields.displayname" /> <NumberField source="media_count" /> <FunctionField source="media_length" render={record => formatBytes(record.media_length)} /> diff --git a/src/resources/users.tsx b/src/resources/users.tsx index 6f97f575..4916c162 100644 --- a/src/resources/users.tsx +++ b/src/resources/users.tsx @@ -242,8 +242,16 @@ export const UserList = (props: ListProps) => { > {isSmall ? ( <SimpleList - primaryText={record => record.displayname || record.id} - secondaryText={record => record.id} + primaryText={record => ( + <Box component="span" sx={{ wordBreak: "break-all" }}> + {record.displayname || record.id} + </Box> + )} + secondaryText={record => ( + <Box component="span" sx={{ wordBreak: "break-all" }}> + {record.id} + </Box> + )} tertiaryText={record => ( <Box sx={{ display: "flex", gap: 0.5 }}> {record.admin && ( @@ -297,8 +305,7 @@ export const UserList = (props: ListProps) => { <TextField source="id" sx={{ - wordBreak: "break-word", - overflowWrap: "break-word", + wordBreak: "break-all", }} sortBy="name" label="resources.users.fields.id" @@ -306,8 +313,7 @@ export const UserList = (props: ListProps) => { <TextField source="displayname" sx={{ - wordBreak: "break-word", - overflowWrap: "break-word", + wordBreak: "break-all", }} label="resources.users.fields.displayname" /> @@ -1001,7 +1007,7 @@ export const UserEdit = (props: EditProps) => { <ReferenceField reference="rooms" source="id" label={false} link={false} sortable={false}> <AvatarField source="avatar" sx={{ height: "40px", width: "40px" }} /> </ReferenceField> - <TextField source="id" label="resources.rooms.fields.room_id" sortable={false} /> + <TextField source="id" label="resources.rooms.fields.room_id" sortable={false} sx={{ wordBreak: "break-all" }} /> <ReferenceField reference="rooms" source="id" @@ -1057,7 +1063,7 @@ export const UserEdit = (props: EditProps) => { <ReferenceField reference="rooms" source="id" label={false} link={false} sortable={false}> <AvatarField source="avatar" sx={{ height: "40px", width: "40px" }} /> </ReferenceField> - <TextField source="id" label="resources.rooms.fields.room_id" sortable={false} /> + <TextField source="id" label="resources.rooms.fields.room_id" sortable={false} sx={{ wordBreak: "break-all" }} /> <ReferenceField reference="rooms" source="id" @@ -1091,7 +1097,11 @@ export const UserEdit = (props: EditProps) => { > {isSmall ? ( <SimpleList - primaryText={record => record.app_display_name || record.app_id} + primaryText={record => ( + <Box component="span" sx={{ wordBreak: "break-all" }}> + {record.app_display_name || record.app_id} + </Box> + )} secondaryText={record => ( <> {record.kind} From d25a088f497d91e867a746752f415e7952bc3454 Mon Sep 17 00:00:00 2001 From: Aine <aine@etke.cc> Date: Sat, 21 Mar 2026 12:25:43 +0000 Subject: [PATCH 26/28] optimize login page for mobile --- src/auth-callback-error.tsx | 4 ++-- src/components/LoginFormBox.tsx | 1 + src/pages/LoginPage.tsx | 14 ++++++-------- 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/src/auth-callback-error.tsx b/src/auth-callback-error.tsx index 0180ca54..0b4d1476 100644 --- a/src/auth-callback-error.tsx +++ b/src/auth-callback-error.tsx @@ -40,7 +40,7 @@ const AuthCallbackErrorView = ({ message, onBack }: { message: string; onBack: ( )} <Card className="card"> <Box className="avatar"> - <Avatar sx={{ width: "120px", height: "120px" }} src={logoUrl} /> + <Avatar sx={{ width: { xs: "80px", sm: "120px" }, height: { xs: "80px", sm: "120px" } }} src={logoUrl} /> </Box> <Box className="hint">{translate("synapseadmin.auth.welcome", { name: welcomeTo })}</Box> <Box className="form"> @@ -52,7 +52,7 @@ const AuthCallbackErrorView = ({ message, onBack }: { message: string; onBack: ( </Typography> </Box> <CardActions className="actions"> - <Button size="small" variant="contained" type="button" color="primary" onClick={onBack} fullWidth> + <Button variant="contained" type="button" color="primary" onClick={onBack} fullWidth> {translate("ra.action.back")} </Button> </CardActions> diff --git a/src/components/LoginFormBox.tsx b/src/components/LoginFormBox.tsx index a2a1c8cc..6d0e86a4 100644 --- a/src/components/LoginFormBox.tsx +++ b/src/components/LoginFormBox.tsx @@ -139,6 +139,7 @@ const LoginFormBox = styled(Box, { [`& .card`]: { width: "100%", marginTop: "0", + marginBottom: "2rem", }, }, [`& .avatar`]: { diff --git a/src/pages/LoginPage.tsx b/src/pages/LoginPage.tsx index b8f673dd..d9be9556 100644 --- a/src/pages/LoginPage.tsx +++ b/src/pages/LoginPage.tsx @@ -416,7 +416,7 @@ const LoginPage = () => { onChange={(_, newValue) => setLoginMethod(newValue as LoginMethod)} indicatorColor="primary" textColor="primary" - centered + variant="fullWidth" > <Tab label={translate("synapseadmin.auth.credentials")} value="credentials" /> <Tab label={translate("synapseadmin.auth.access_token")} value="accessToken" /> @@ -483,8 +483,8 @@ const LoginPage = () => { /> </Box> )} - <Typography className="serverVersion">{serverVersion}</Typography> - <Typography className="matrixVersions">{matrixVersions}</Typography> + <Typography className="serverVersion" sx={{ wordBreak: "break-word" }}>{serverVersion}</Typography> + <Typography className="matrixVersions" sx={{ wordBreak: "break-word" }}>{matrixVersions}</Typography> </> ); }; @@ -504,7 +504,7 @@ const LoginPage = () => { {loading ? ( <CircularProgress size={25} thickness={2} /> ) : ( - <Avatar sx={{ width: "120px", height: "120px" }} src={logoUrl} /> + <Avatar sx={{ width: { xs: "80px", sm: "120px" }, height: { xs: "80px", sm: "120px" } }} src={logoUrl} /> )} </Box> <Box className="hint">{translate("synapseadmin.auth.welcome", { name: welcomeTo })}</Box> @@ -524,15 +524,14 @@ const LoginPage = () => { </Select> <FormDataConsumer>{formDataProps => <UserData {...formDataProps} />}</FormDataConsumer> {loginMethod === "credentials" && ( - <CardActions className="actions"> + <CardActions className="actions" sx={{ flexDirection: "column", gap: 1, "& > :not(:first-of-type)": { ml: 0 } }}> {supportPassAuth && ( - <Button size="small" variant="contained" type="submit" color="primary" disabled={loading} fullWidth> + <Button variant="contained" type="submit" color="primary" disabled={loading} fullWidth> {translate("ra.auth.sign_in")} </Button> )} {ssoBaseUrl !== "" && ( <Button - size="small" variant="contained" color="secondary" onClick={handleSSO} @@ -544,7 +543,6 @@ const LoginPage = () => { )} {(oidcVisible || oidcUrl !== "") && ( <Button - size="small" variant="contained" color="secondary" onClick={handleOIDC} From f074bac1d1c5ac941909babf446361e7bbfeb477 Mon Sep 17 00:00:00 2001 From: Aine <aine@etke.cc> Date: Sat, 21 Mar 2026 12:27:17 +0000 Subject: [PATCH 27/28] fix lint --- src/components/DeviceCreateButton.tsx | 8 +- src/components/UserAccountData.tsx | 34 +++++++- src/pages/LoginPage.tsx | 21 ++--- src/resources/destinations.tsx | 3 +- src/resources/registration_tokens.tsx | 79 +++++++++-------- src/resources/room_directory.tsx | 36 ++++++-- src/resources/rooms.tsx | 110 +++++++++++++----------- src/resources/scheduled_tasks.tsx | 21 +++-- src/resources/user_media_statistics.tsx | 62 +++++++------ src/resources/users.tsx | 21 ++++- 10 files changed, 252 insertions(+), 143 deletions(-) diff --git a/src/components/DeviceCreateButton.tsx b/src/components/DeviceCreateButton.tsx index b234fe55..47973295 100644 --- a/src/components/DeviceCreateButton.tsx +++ b/src/components/DeviceCreateButton.tsx @@ -50,7 +50,13 @@ const DeviceCreateButton = () => { return ( <> - <MuiButton variant="outlined" size="small" startIcon={<AddIcon />} onClick={() => setOpen(true)} fullWidth={fullScreen}> + <MuiButton + variant="outlined" + size="small" + startIcon={<AddIcon />} + onClick={() => setOpen(true)} + fullWidth={fullScreen} + > {translate("resources.devices.action.create.label")} </MuiButton> <Dialog open={open} onClose={handleClose} maxWidth="sm" fullWidth fullScreen={fullScreen}> diff --git a/src/components/UserAccountData.tsx b/src/components/UserAccountData.tsx index f9ed1844..e4d7dbcd 100644 --- a/src/components/UserAccountData.tsx +++ b/src/components/UserAccountData.tsx @@ -47,7 +47,22 @@ const UserAccountData = () => { <Typography variant="h6">{translate("resources.users.account_data.global")}</Typography> </AccordionSummary> <AccordionDetails> - <Box component="pre" sx={{ whiteSpace: "pre-wrap", wordBreak: "break-all", m: 0, p: 2, fontSize: { xs: "0.75rem", sm: "0.85rem" }, bgcolor: "action.hover", borderRadius: 1, overflow: "auto", maxWidth: "100%" }}>{JSON.stringify(globalAccountData, null, 4)}</Box> + <Box + component="pre" + sx={{ + whiteSpace: "pre-wrap", + wordBreak: "break-all", + m: 0, + p: 2, + fontSize: { xs: "0.75rem", sm: "0.85rem" }, + bgcolor: "action.hover", + borderRadius: 1, + overflow: "auto", + maxWidth: "100%", + }} + > + {JSON.stringify(globalAccountData, null, 4)} + </Box> </AccordionDetails> </Accordion> <Accordion> @@ -55,7 +70,22 @@ const UserAccountData = () => { <Typography variant="h6">{translate("resources.users.account_data.rooms")}</Typography> </AccordionSummary> <AccordionDetails> - <Box component="pre" sx={{ whiteSpace: "pre-wrap", wordBreak: "break-all", m: 0, p: 2, fontSize: { xs: "0.75rem", sm: "0.85rem" }, bgcolor: "action.hover", borderRadius: 1, overflow: "auto", maxWidth: "100%" }}>{JSON.stringify(roomsAccountData, null, 4)}</Box> + <Box + component="pre" + sx={{ + whiteSpace: "pre-wrap", + wordBreak: "break-all", + m: 0, + p: 2, + fontSize: { xs: "0.75rem", sm: "0.85rem" }, + bgcolor: "action.hover", + borderRadius: 1, + overflow: "auto", + maxWidth: "100%", + }} + > + {JSON.stringify(roomsAccountData, null, 4)} + </Box> </AccordionDetails> </Accordion> </Box> diff --git a/src/pages/LoginPage.tsx b/src/pages/LoginPage.tsx index d9be9556..47d86ce3 100644 --- a/src/pages/LoginPage.tsx +++ b/src/pages/LoginPage.tsx @@ -483,8 +483,12 @@ const LoginPage = () => { /> </Box> )} - <Typography className="serverVersion" sx={{ wordBreak: "break-word" }}>{serverVersion}</Typography> - <Typography className="matrixVersions" sx={{ wordBreak: "break-word" }}>{matrixVersions}</Typography> + <Typography className="serverVersion" sx={{ wordBreak: "break-word" }}> + {serverVersion} + </Typography> + <Typography className="matrixVersions" sx={{ wordBreak: "break-word" }}> + {matrixVersions} + </Typography> </> ); }; @@ -524,20 +528,17 @@ const LoginPage = () => { </Select> <FormDataConsumer>{formDataProps => <UserData {...formDataProps} />}</FormDataConsumer> {loginMethod === "credentials" && ( - <CardActions className="actions" sx={{ flexDirection: "column", gap: 1, "& > :not(:first-of-type)": { ml: 0 } }}> + <CardActions + className="actions" + sx={{ flexDirection: "column", gap: 1, "& > :not(:first-of-type)": { ml: 0 } }} + > {supportPassAuth && ( <Button variant="contained" type="submit" color="primary" disabled={loading} fullWidth> {translate("ra.auth.sign_in")} </Button> )} {ssoBaseUrl !== "" && ( - <Button - variant="contained" - color="secondary" - onClick={handleSSO} - disabled={loading} - fullWidth - > + <Button variant="contained" color="secondary" onClick={handleSSO} disabled={loading} fullWidth> {translate("synapseadmin.auth.sso_sign_in")} </Button> )} diff --git a/src/resources/destinations.tsx b/src/resources/destinations.tsx index 60a07cce..16ba0484 100644 --- a/src/resources/destinations.tsx +++ b/src/resources/destinations.tsx @@ -144,7 +144,8 @@ export const DestinationList = (props: ListProps) => { record.failure_ts ? ( <Box component="span" sx={{ display: "inline-flex", alignItems: "center", gap: 0.5 }}> <ErrorIcon fontSize="inherit" color="error" /> - {translate("resources.destinations.fields.failure_ts")}: {new Date(record.failure_ts).toLocaleString(locale)} + {translate("resources.destinations.fields.failure_ts")}:{" "} + {new Date(record.failure_ts).toLocaleString(locale)} </Box> ) : null } diff --git a/src/resources/registration_tokens.tsx b/src/resources/registration_tokens.tsx index 97afa16d..c69fbc69 100644 --- a/src/resources/registration_tokens.tsx +++ b/src/resources/registration_tokens.tsx @@ -80,7 +80,8 @@ export const RegistrationTokenList = (props: ListProps) => { {record.expiry_time && ( <> <br /> - {translate("resources.registration_tokens.fields.expiry_time")}: {new Date(record.expiry_time).toLocaleString(locale)} + {translate("resources.registration_tokens.fields.expiry_time")}:{" "} + {new Date(record.expiry_time).toLocaleString(locale)} </> )} </> @@ -89,50 +90,54 @@ export const RegistrationTokenList = (props: ListProps) => { linkType="edit" /> ) : ( - <DatagridConfigurable rowClick="edit"> - <TextField source="token" sortable={false} label="resources.registration_tokens.fields.token" /> - <NumberField source="uses_allowed" sortable={false} label="resources.registration_tokens.fields.uses_allowed" /> - <NumberField source="pending" sortable={false} label="resources.registration_tokens.fields.pending" /> - <NumberField source="completed" sortable={false} label="resources.registration_tokens.fields.completed" /> - <DateField - source="expiry_time" - showTime - options={DATE_FORMAT} - sortable={false} - label="resources.registration_tokens.fields.expiry_time" - locales={locale} - /> - {isMAS && ( - <DateField - source="created_at" - showTime - options={DATE_FORMAT} - sortable={false} - label="resources.registration_tokens.fields.created_at" - locales={locale} - /> - )} - {isMAS && ( - <DateField - source="last_used_at" - showTime - options={DATE_FORMAT} + <DatagridConfigurable rowClick="edit"> + <TextField source="token" sortable={false} label="resources.registration_tokens.fields.token" /> + <NumberField + source="uses_allowed" sortable={false} - label="resources.registration_tokens.fields.last_used_at" - locales={locale} + label="resources.registration_tokens.fields.uses_allowed" /> - )} - {isMAS && ( + <NumberField source="pending" sortable={false} label="resources.registration_tokens.fields.pending" /> + <NumberField source="completed" sortable={false} label="resources.registration_tokens.fields.completed" /> <DateField - source="revoked_at" + source="expiry_time" showTime options={DATE_FORMAT} sortable={false} - label="resources.registration_tokens.fields.revoked_at" + label="resources.registration_tokens.fields.expiry_time" locales={locale} /> - )} - </DatagridConfigurable> + {isMAS && ( + <DateField + source="created_at" + showTime + options={DATE_FORMAT} + sortable={false} + label="resources.registration_tokens.fields.created_at" + locales={locale} + /> + )} + {isMAS && ( + <DateField + source="last_used_at" + showTime + options={DATE_FORMAT} + sortable={false} + label="resources.registration_tokens.fields.last_used_at" + locales={locale} + /> + )} + {isMAS && ( + <DateField + source="revoked_at" + showTime + options={DATE_FORMAT} + sortable={false} + label="resources.registration_tokens.fields.revoked_at" + locales={locale} + /> + )} + </DatagridConfigurable> )} </List> ); diff --git a/src/resources/room_directory.tsx b/src/resources/room_directory.tsx index dac8ce01..1e232225 100644 --- a/src/resources/room_directory.tsx +++ b/src/resources/room_directory.tsx @@ -179,7 +179,9 @@ export const RoomDirectoryList = () => { </> )} linkType="show" - leftAvatar={record => <AvatarField record={record} source="avatar_src" sx={{ height: "40px", width: "40px" }} />} + leftAvatar={record => ( + <AvatarField record={record} source="avatar_src" sx={{ height: "40px", width: "40px" }} /> + )} /> ) : ( <DatagridConfigurable @@ -187,14 +189,36 @@ export const RoomDirectoryList = () => { bulkActionButtons={<RoomDirectoryBulkUnpublishButton />} omit={["room_id", "canonical_alias", "topic"]} > - <AvatarField source="avatar_src" sx={{ height: "40px", width: "40px" }} label="resources.rooms.fields.avatar" /> + <AvatarField + source="avatar_src" + sx={{ height: "40px", width: "40px" }} + label="resources.rooms.fields.avatar" + /> <TextField source="name" sortable={false} label="resources.rooms.fields.name" /> - <TextField source="room_id" sortable={false} label="resources.rooms.fields.room_id" sx={{ wordBreak: "break-all" }} /> - <TextField source="canonical_alias" sortable={false} label="resources.rooms.fields.canonical_alias" sx={{ wordBreak: "break-all" }} /> + <TextField + source="room_id" + sortable={false} + label="resources.rooms.fields.room_id" + sx={{ wordBreak: "break-all" }} + /> + <TextField + source="canonical_alias" + sortable={false} + label="resources.rooms.fields.canonical_alias" + sx={{ wordBreak: "break-all" }} + /> <TextField source="topic" sortable={false} label="resources.rooms.fields.topic" /> <NumberField source="num_joined_members" sortable={false} label="resources.rooms.fields.joined_members" /> - <BooleanField source="world_readable" sortable={false} label="resources.room_directory.fields.world_readable" /> - <BooleanField source="guest_can_join" sortable={false} label="resources.room_directory.fields.guest_can_join" /> + <BooleanField + source="world_readable" + sortable={false} + label="resources.room_directory.fields.world_readable" + /> + <BooleanField + source="guest_can_join" + sortable={false} + label="resources.room_directory.fields.guest_can_join" + /> <MakeAdminBtn /> </DatagridConfigurable> )} diff --git a/src/resources/rooms.tsx b/src/resources/rooms.tsx index c354ba4c..d837416b 100644 --- a/src/resources/rooms.tsx +++ b/src/resources/rooms.tsx @@ -554,7 +554,12 @@ export const RoomShow = (props: ShowProps) => { > <AvatarField source="avatar_src" sx={{ height: "40px", width: "40px" }} /> </ReferenceField> - <RaTextField source="id" sortable={false} label="resources.users.fields.id" sx={{ wordBreak: "break-all" }} /> + <RaTextField + source="id" + sortable={false} + label="resources.users.fields.id" + sx={{ wordBreak: "break-all" }} + /> <ReferenceField label="resources.users.fields.displayname" source="id" @@ -728,7 +733,12 @@ export const RoomShow = (props: ShowProps) => { secondaryText={record => ( <> {record.received_ts && new Date(record.received_ts).toLocaleString(locale)} - {record.state_group && <> · {translate("resources.forward_extremities.fields.state_group")}: {record.state_group}</>} + {record.state_group && ( + <> + {" "} + · {translate("resources.forward_extremities.fields.state_group")}: {record.state_group} + </> + )} </> )} linkType={false} @@ -834,59 +844,61 @@ export const RoomList = (props: ListProps) => { </Box> )} linkType="show" - leftAvatar={record => <AvatarField record={record} source="avatar_src" sx={{ height: "40px", width: "40px" }} />} + leftAvatar={record => ( + <AvatarField record={record} source="avatar_src" sx={{ height: "40px", width: "40px" }} /> + )} /> ) : ( - <DatagridConfigurable - rowClick="show" - bulkActionButtons={<RoomBulkActionButtons />} - omit={["joined_local_members", "state_events", "version", "federatable", "join_rules"]} - > - <ReferenceField - reference="rooms" - source="id" - label="resources.users.fields.avatar" - link={false} - sortable={false} + <DatagridConfigurable + rowClick="show" + bulkActionButtons={<RoomBulkActionButtons />} + omit={["joined_local_members", "state_events", "version", "federatable", "join_rules"]} > - <AvatarField source="avatar" sx={{ height: "40px", width: "40px" }} /> - </ReferenceField> - <RaTextField source="id" label="resources.rooms.fields.room_id" sortable={false} /> - <WrapperField source="encryption" sortBy="encryption" label="resources.rooms.fields.encryption"> - <BooleanField - source="is_encrypted" - sortBy="encryption" - TrueIcon={HttpsIcon} - FalseIcon={NoEncryptionIcon} - label={<HttpsIcon />} + <ReferenceField + reference="rooms" + source="id" + label="resources.users.fields.avatar" + link={false} + sortable={false} + > + <AvatarField source="avatar" sx={{ height: "40px", width: "40px" }} /> + </ReferenceField> + <RaTextField source="id" label="resources.rooms.fields.room_id" sortable={false} /> + <WrapperField source="encryption" sortBy="encryption" label="resources.rooms.fields.encryption"> + <BooleanField + source="is_encrypted" + sortBy="encryption" + TrueIcon={HttpsIcon} + FalseIcon={NoEncryptionIcon} + label={<HttpsIcon />} + sx={{ + [`& [data-testid="true"]`]: { color: theme.palette.success.main }, + [`& [data-testid="false"]`]: { color: theme.palette.error.main }, + }} + /> + </WrapperField> + <FunctionField + source="name" sx={{ - [`& [data-testid="true"]`]: { color: theme.palette.success.main }, - [`& [data-testid="false"]`]: { color: theme.palette.error.main }, + wordBreak: "break-all", }} + render={record => record["name"] || record["canonical_alias"] || record["id"]} + label="resources.rooms.fields.name" /> - </WrapperField> - <FunctionField - source="name" - sx={{ - wordBreak: "break-all", - }} - render={record => record["name"] || record["canonical_alias"] || record["id"]} - label="resources.rooms.fields.name" - /> - <RaTextField source="joined_members" label="resources.rooms.fields.joined_members" /> - <RaTextField source="joined_local_members" label="resources.rooms.fields.joined_local_members" /> - <RaTextField source="state_events" label="resources.rooms.fields.state_events" /> - <RaTextField source="version" label="resources.rooms.fields.version" /> - <RaTextField source="join_rules" label="resources.rooms.fields.join_rules" /> - <ReferenceField source="creator" reference="users"> - <RaTextField source="id" label="resources.rooms.fields.creator" sx={{ wordBreak: "break-all" }} /> - </ReferenceField> - <BooleanField source="federatable" label="resources.rooms.fields.federatable" /> - <BooleanField source="public" label="resources.rooms.fields.public" /> - <WrapperField label="resources.rooms.fields.actions"> - <MakeAdminBtn /> - </WrapperField> - </DatagridConfigurable> + <RaTextField source="joined_members" label="resources.rooms.fields.joined_members" /> + <RaTextField source="joined_local_members" label="resources.rooms.fields.joined_local_members" /> + <RaTextField source="state_events" label="resources.rooms.fields.state_events" /> + <RaTextField source="version" label="resources.rooms.fields.version" /> + <RaTextField source="join_rules" label="resources.rooms.fields.join_rules" /> + <ReferenceField source="creator" reference="users"> + <RaTextField source="id" label="resources.rooms.fields.creator" sx={{ wordBreak: "break-all" }} /> + </ReferenceField> + <BooleanField source="federatable" label="resources.rooms.fields.federatable" /> + <BooleanField source="public" label="resources.rooms.fields.public" /> + <WrapperField label="resources.rooms.fields.actions"> + <MakeAdminBtn /> + </WrapperField> + </DatagridConfigurable> )} </List> ); diff --git a/src/resources/scheduled_tasks.tsx b/src/resources/scheduled_tasks.tsx index 70a0a514..11068353 100644 --- a/src/resources/scheduled_tasks.tsx +++ b/src/resources/scheduled_tasks.tsx @@ -89,11 +89,15 @@ export const ScheduledTaskList = (props: ListProps) => { </> )} secondaryText={record => new Date(record.timestamp_ms).toLocaleDateString(locale, DATE_FORMAT)} - tertiaryText={record => record.resource_id ? ( - <Box component="span" sx={{ wordBreak: "break-all" }}> - {record.resource_id} - </Box> - ) : ""} + tertiaryText={record => + record.resource_id ? ( + <Box component="span" sx={{ wordBreak: "break-all" }}> + {record.resource_id} + </Box> + ) : ( + "" + ) + } linkType={false} /> ) : ( @@ -114,7 +118,12 @@ export const ScheduledTaskList = (props: ListProps) => { label="resources.scheduled_tasks.fields.timestamp" locales={locale} /> - <TextField source="resource_id" sortable={false} label="resources.scheduled_tasks.fields.resource_id" sx={{ wordBreak: "break-all" }} /> + <TextField + source="resource_id" + sortable={false} + label="resources.scheduled_tasks.fields.resource_id" + sx={{ wordBreak: "break-all" }} + /> <FunctionField source="result" sortable={false} diff --git a/src/resources/user_media_statistics.tsx b/src/resources/user_media_statistics.tsx index d513fef2..0e21e721 100644 --- a/src/resources/user_media_statistics.tsx +++ b/src/resources/user_media_statistics.tsx @@ -78,36 +78,42 @@ export const UserMediaStatsList = (props: ListProps) => { {translate("resources.user_media_statistics.fields.media_length")}: {formatBytes(record.media_length)} </> )} - rowClick={(id) => "/users/" + id + "/media"} + rowClick={id => "/users/" + id + "/media"} /> ) : ( - <DatagridConfigurable rowClick={id => "/users/" + id + "/media"} bulkActionButtons={false}> - <ReferenceField label="resources.users.fields.avatar" source="id" reference="users" sortable={false} link=""> - <AvatarField source="avatar_src" sx={{ height: "40px", width: "40px" }} /> - </ReferenceField> - <TextField source="user_id" label="resources.users.fields.id" sx={{ wordBreak: "break-all" }} /> - <TextField source="displayname" label="resources.users.fields.displayname" /> - <NumberField source="media_count" /> - <FunctionField source="media_length" render={record => formatBytes(record.media_length)} /> - <ReferenceField label="resources.users.fields.is_guest" source="id" reference="users" sortable={false} link=""> - <BooleanField source="is_guest" label="resources.users.fields.is_guest" /> - </ReferenceField> - <ReferenceField - label="resources.users.fields.deactivated" - source="id" - reference="users" - sortable={false} - link="" - > - <BooleanField source="deactivated" label="resources.users.fields.deactivated" /> - </ReferenceField> - <ReferenceField label="resources.users.fields.locked" source="id" reference="users" sortable={false} link=""> - <BooleanField source="locked" label="resources.users.fields.locked" /> - </ReferenceField> - <ReferenceField label="resources.users.fields.erased" source="id" reference="users" sortable={false} link=""> - <BooleanField source="erased" sortable={false} label="resources.users.fields.erased" /> - </ReferenceField> - </DatagridConfigurable> + <DatagridConfigurable rowClick={id => "/users/" + id + "/media"} bulkActionButtons={false}> + <ReferenceField label="resources.users.fields.avatar" source="id" reference="users" sortable={false} link=""> + <AvatarField source="avatar_src" sx={{ height: "40px", width: "40px" }} /> + </ReferenceField> + <TextField source="user_id" label="resources.users.fields.id" sx={{ wordBreak: "break-all" }} /> + <TextField source="displayname" label="resources.users.fields.displayname" /> + <NumberField source="media_count" /> + <FunctionField source="media_length" render={record => formatBytes(record.media_length)} /> + <ReferenceField + label="resources.users.fields.is_guest" + source="id" + reference="users" + sortable={false} + link="" + > + <BooleanField source="is_guest" label="resources.users.fields.is_guest" /> + </ReferenceField> + <ReferenceField + label="resources.users.fields.deactivated" + source="id" + reference="users" + sortable={false} + link="" + > + <BooleanField source="deactivated" label="resources.users.fields.deactivated" /> + </ReferenceField> + <ReferenceField label="resources.users.fields.locked" source="id" reference="users" sortable={false} link=""> + <BooleanField source="locked" label="resources.users.fields.locked" /> + </ReferenceField> + <ReferenceField label="resources.users.fields.erased" source="id" reference="users" sortable={false} link=""> + <BooleanField source="erased" sortable={false} label="resources.users.fields.erased" /> + </ReferenceField> + </DatagridConfigurable> )} </List> ); diff --git a/src/resources/users.tsx b/src/resources/users.tsx index 4916c162..7e738d8f 100644 --- a/src/resources/users.tsx +++ b/src/resources/users.tsx @@ -868,7 +868,12 @@ export const UserEdit = (props: EditProps) => { )} secondaryText={record => ( <> - {record.last_seen_ip && <>{record.last_seen_ip}<br /></>} + {record.last_seen_ip && ( + <> + {record.last_seen_ip} + <br /> + </> + )} {record.last_seen_ts && new Date(record.last_seen_ts).toLocaleString(locale)} <Box sx={{ mt: 1 }}> <DeviceDisplayNameInput /> @@ -1007,7 +1012,12 @@ export const UserEdit = (props: EditProps) => { <ReferenceField reference="rooms" source="id" label={false} link={false} sortable={false}> <AvatarField source="avatar" sx={{ height: "40px", width: "40px" }} /> </ReferenceField> - <TextField source="id" label="resources.rooms.fields.room_id" sortable={false} sx={{ wordBreak: "break-all" }} /> + <TextField + source="id" + label="resources.rooms.fields.room_id" + sortable={false} + sx={{ wordBreak: "break-all" }} + /> <ReferenceField reference="rooms" source="id" @@ -1063,7 +1073,12 @@ export const UserEdit = (props: EditProps) => { <ReferenceField reference="rooms" source="id" label={false} link={false} sortable={false}> <AvatarField source="avatar" sx={{ height: "40px", width: "40px" }} /> </ReferenceField> - <TextField source="id" label="resources.rooms.fields.room_id" sortable={false} sx={{ wordBreak: "break-all" }} /> + <TextField + source="id" + label="resources.rooms.fields.room_id" + sortable={false} + sx={{ wordBreak: "break-all" }} + /> <ReferenceField reference="rooms" source="id" From 250740a8d28a556d3c14f76ecf80daff998cfed0 Mon Sep 17 00:00:00 2001 From: Aine <aine@etke.cc> Date: Sat, 21 Mar 2026 12:29:17 +0000 Subject: [PATCH 28/28] update readme --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index df88f8e7..6d89fcb9 100644 --- a/README.md +++ b/README.md @@ -188,6 +188,7 @@ The following changes are already implemented: * 💬 [Add room messages viewer with filters and jump-to-date](https://github.com/etkecc/synapse-admin/commit/60818ca3604aef61e2b5abc0d92e14771bb53637) * 🏗️ [Add Room hierarchy tab](https://github.com/etkecc/synapse-admin/commit/566b7148b5cb3e2fab55078621ba2cb4203b21a1) * ⚙️ [Add control of admin flags for Matrix Client-Server APIs](https://github.com/etkecc/synapse-admin/commit/b272cc11945d8e6adabb3cfe4904ad0ac063549d) +* 📱 [Optimize UI for mobile](https://github.com/etkecc/synapse-admin/pull/1104) #### exclusive for [etke.cc](https://etke.cc) customers