Compare commits
15 Commits
v0.10.3-et
...
v0.10.3-et
Author | SHA1 | Date | |
---|---|---|---|
![]() |
0d021021df | ||
![]() |
19302466ef | ||
![]() |
0594259ae4 | ||
![]() |
ba485bbb18 | ||
![]() |
9fc005032c | ||
![]() |
f5d6f24b30 | ||
![]() |
a42efe7eda | ||
![]() |
31d3712dbb | ||
![]() |
a15dad4a31 | ||
![]() |
9062a6afb1 | ||
![]() |
a79c3597d6 | ||
![]() |
470f1b5455 | ||
![]() |
46cc0e2fda | ||
![]() |
f3080e9468 | ||
![]() |
f8fe1166e2 |
5
.watchmanconfig
Normal file
5
.watchmanconfig
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"ignore_dirs": [
|
||||||
|
"testdata"
|
||||||
|
]
|
||||||
|
}
|
14
README.md
14
README.md
@@ -7,7 +7,7 @@ This project is built using [react-admin](https://marmelab.com/react-admin/).
|
|||||||
<!-- vim-markdown-toc GFM -->
|
<!-- vim-markdown-toc GFM -->
|
||||||
|
|
||||||
* [Fork differences](#fork-differences)
|
* [Fork differences](#fork-differences)
|
||||||
* [Available via CDN](#available-via-cdn)
|
* [Availability](#availability)
|
||||||
* [Changes](#changes)
|
* [Changes](#changes)
|
||||||
* [Development](#development)
|
* [Development](#development)
|
||||||
* [Configuration](#configuration)
|
* [Configuration](#configuration)
|
||||||
@@ -33,9 +33,11 @@ With [Awesome-Technologies/synapse-admin](https://github.com/Awesome-Technologie
|
|||||||
fork is intended to be a more feature-rich version of the original project. The main goal is to provide a more
|
fork is intended to be a more feature-rich version of the original project. The main goal is to provide a more
|
||||||
user-friendly interface for managing Synapse homeservers.
|
user-friendly interface for managing Synapse homeservers.
|
||||||
|
|
||||||
### Available via CDN
|
### Availability
|
||||||
|
|
||||||
On [admin.etke.cc](https://admin.etke.cc) you can find the latest version of this fork.
|
* As a core/default component on [etke.cc](https://etke.cc/?utm_source=github&utm_medium=readme&utm_campaign=synapse-admin)
|
||||||
|
* Via CDN on [admin.etke.cc](https://admin.etke.cc)
|
||||||
|
* As a component in [Matrix-Docker-Ansible-Deploy Playbook](https://github.com/spantaleev/matrix-docker-ansible-deploy/blob/master/docs/configuring-playbook-synapse-admin.md)
|
||||||
|
|
||||||
### Changes
|
### Changes
|
||||||
|
|
||||||
@@ -60,6 +62,10 @@ The following changes are already implemented:
|
|||||||
* [Upgrade react-admin to v5](https://github.com/etkecc/synapse-admin/pull/40)
|
* [Upgrade react-admin to v5](https://github.com/etkecc/synapse-admin/pull/40)
|
||||||
* [Restrict actions on specific users](https://github.com/etkecc/synapse-admin/pull/42)
|
* [Restrict actions on specific users](https://github.com/etkecc/synapse-admin/pull/42)
|
||||||
* [Add `Contact support` menu item](https://github.com/etkecc/synapse-admin/pull/45)
|
* [Add `Contact support` menu item](https://github.com/etkecc/synapse-admin/pull/45)
|
||||||
|
* [Provide options to delete media and redact events on user erase](https://github.com/etkecc/synapse-admin/pull/49)
|
||||||
|
* [Authenticated Media support](https://github.com/etkecc/synapse-admin/pull/51)
|
||||||
|
* [Better media preview/download](https://github.com/etkecc/synapse-admin/pull/53)
|
||||||
|
* [Login with access token](https://github.com/etkecc/synapse-admin/pull/58)
|
||||||
|
|
||||||
_the list will be updated as new changes are added_
|
_the list will be updated as new changes are added_
|
||||||
|
|
||||||
@@ -136,7 +142,7 @@ Synapse-Admin provides a support link in the main menu - `Contact support`. By d
|
|||||||
|
|
||||||
### Supported Synapse
|
### Supported Synapse
|
||||||
|
|
||||||
It needs at least [Synapse](https://github.com/element-hq/synapse) v1.93.0 for all functions to work as expected!
|
It needs at least [Synapse](https://github.com/element-hq/synapse) v1.116.0 for all functions to work as expected!
|
||||||
|
|
||||||
You get your server version with the request `/_synapse/admin/v1/server_version`.
|
You get your server version with the request `/_synapse/admin/v1/server_version`.
|
||||||
See also [Synapse version API](https://element-hq.github.io/synapse/latest/admin_api/version_api.html).
|
See also [Synapse version API](https://element-hq.github.io/synapse/latest/admin_api/version_api.html).
|
||||||
|
8
justfile
8
justfile
@@ -25,11 +25,15 @@ run-dev:
|
|||||||
stop-dev:
|
stop-dev:
|
||||||
@docker-compose -f docker-compose-dev.yml stop
|
@docker-compose -f docker-compose-dev.yml stop
|
||||||
|
|
||||||
|
# register a user in the dev stack
|
||||||
register-user localpart password *admin:
|
register-user localpart password *admin:
|
||||||
docker-compose exec synapse register_new_matrix_user {{ if admin == "1" {"--admin"} else {"--no-admin"} }} -u {{ localpart }} -p {{ password }} -c /config/homeserver.yaml http://localhost:8008
|
docker-compose exec synapse register_new_matrix_user {{ if admin == "1" {"--admin"} else {"--no-admin"} }} -u {{ localpart }} -p {{ password }} -c /config/homeserver.yaml http://localhost:8008
|
||||||
|
|
||||||
|
# run yarn {fix,lint,test} commands
|
||||||
|
test:
|
||||||
|
@-yarn run fix
|
||||||
|
@-yarn run lint
|
||||||
|
@-yarn run test
|
||||||
|
|
||||||
# run the app in a production mode
|
# run the app in a production mode
|
||||||
run-prod: build
|
run-prod: build
|
||||||
|
@@ -1,26 +1,70 @@
|
|||||||
import { Layout, Menu } from 'react-admin';
|
import { AppBar, Confirm, Layout, Logout, Menu, useLogout, UserMenu } from "react-admin";
|
||||||
import LiveHelpIcon from '@mui/icons-material/LiveHelp';
|
import LiveHelpIcon from "@mui/icons-material/LiveHelp";
|
||||||
|
import { LoginMethod } from "../pages/LoginPage";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
const DEFAULT_SUPPORT_LINK = "https://github.com/etkecc/synapse-admin/issues";
|
const DEFAULT_SUPPORT_LINK = "https://github.com/etkecc/synapse-admin/issues";
|
||||||
const supportLink = (): string => {
|
const supportLink = (): string => {
|
||||||
try {
|
try {
|
||||||
new URL(localStorage.getItem("support_url") || ''); // Check if the URL is valid
|
new URL(localStorage.getItem("support_url") || ""); // Check if the URL is valid
|
||||||
return localStorage.getItem("support_url") || DEFAULT_SUPPORT_LINK;
|
return localStorage.getItem("support_url") || DEFAULT_SUPPORT_LINK;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return DEFAULT_SUPPORT_LINK;
|
return DEFAULT_SUPPORT_LINK;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const AdminUserMenu = () => {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const logout = useLogout();
|
||||||
|
const checkLoginType = (ev: React.MouseEvent<HTMLDivElement>) => {
|
||||||
|
const loginType: LoginMethod = (localStorage.getItem("login_type") || "credentials") as LoginMethod;
|
||||||
|
if (loginType === "accessToken") {
|
||||||
|
ev.stopPropagation();
|
||||||
|
setOpen(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleConfirm = () => {
|
||||||
|
setOpen(false);
|
||||||
|
logout();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDialogClose = () => {
|
||||||
|
setOpen(false);
|
||||||
|
localStorage.removeItem("access_token");
|
||||||
|
localStorage.removeItem("login_type");
|
||||||
|
window.location.reload();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<UserMenu>
|
||||||
|
<div onClickCapture={checkLoginType}>
|
||||||
|
<Logout />
|
||||||
|
</div>
|
||||||
|
<Confirm
|
||||||
|
isOpen={open}
|
||||||
|
title="synapseadmin.auth.logout_acces_token_dialog.title"
|
||||||
|
content="synapseadmin.auth.logout_acces_token_dialog.content"
|
||||||
|
onConfirm={handleConfirm}
|
||||||
|
onClose={handleDialogClose}
|
||||||
|
confirm="synapseadmin.auth.logout_acces_token_dialog.confirm"
|
||||||
|
cancel="synapseadmin.auth.logout_acces_token_dialog.cancel"
|
||||||
|
/>
|
||||||
|
</UserMenu>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const AdminAppBar = () => <AppBar userMenu={<AdminUserMenu />} />;
|
||||||
|
|
||||||
const AdminMenu = () => (
|
const AdminMenu = () => (
|
||||||
<Menu>
|
<Menu>
|
||||||
<Menu.ResourceItems />
|
<Menu.ResourceItems />
|
||||||
<Menu.Item to={supportLink()} target="_blank" primaryText="Contact support" leftIcon={<LiveHelpIcon />} />
|
<Menu.Item to={supportLink()} target="_blank" primaryText="Contact support" leftIcon={<LiveHelpIcon />} />
|
||||||
</Menu>
|
</Menu>
|
||||||
);
|
);
|
||||||
|
|
||||||
export const AdminLayout = ({ children }) => (
|
export const AdminLayout = ({ children }) => (
|
||||||
<Layout menu={AdminMenu}>
|
<Layout appBar={AdminAppBar} menu={AdminMenu}>
|
||||||
{children}
|
{children}
|
||||||
</Layout>
|
</Layout>
|
||||||
);
|
);
|
||||||
|
@@ -1,18 +1,43 @@
|
|||||||
import { render, screen } from "@testing-library/react";
|
import { render, screen, waitFor } from "@testing-library/react";
|
||||||
import { RecordContextProvider } from "react-admin";
|
import { RecordContextProvider } from "react-admin";
|
||||||
|
import { act } from "react";
|
||||||
import AvatarField from "./AvatarField";
|
import AvatarField from "./AvatarField";
|
||||||
|
|
||||||
describe("AvatarField", () => {
|
describe("AvatarField", () => {
|
||||||
it("shows image", () => {
|
beforeEach(() => {
|
||||||
|
// Mock fetch
|
||||||
|
global.fetch = jest.fn(() =>
|
||||||
|
Promise.resolve({
|
||||||
|
blob: () => Promise.resolve(new Blob(["mock image data"], { type: 'image/jpeg' })),
|
||||||
|
})
|
||||||
|
) as jest.Mock;
|
||||||
|
|
||||||
|
// Mock URL.createObjectURL
|
||||||
|
global.URL.createObjectURL = jest.fn(() => "mock-object-url");
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it.only("shows image", async () => {
|
||||||
const value = {
|
const value = {
|
||||||
avatar: "foo",
|
avatar: "mxc://serverName/mediaId",
|
||||||
};
|
};
|
||||||
render(
|
|
||||||
<RecordContextProvider value={value}>
|
await act(async () => {
|
||||||
<AvatarField source="avatar" />
|
render(
|
||||||
</RecordContextProvider>
|
<RecordContextProvider value={value}>
|
||||||
);
|
<AvatarField source="avatar" />
|
||||||
expect(screen.getByRole("img").getAttribute("src")).toBe("foo");
|
</RecordContextProvider>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
const img = screen.getByRole("img");
|
||||||
|
expect(img.getAttribute("src")).toBe("mock-object-url");
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(global.fetch).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -1,12 +1,36 @@
|
|||||||
import { get } from "lodash";
|
import { get } from "lodash";
|
||||||
|
import { Avatar, AvatarProps } from "@mui/material";
|
||||||
|
import { FieldProps, useRecordContext } from "react-admin";
|
||||||
|
import { useState, useEffect, useCallback } from "react";
|
||||||
|
import { fetchAuthenticatedMedia } from "../utils/fetchMedia";
|
||||||
|
|
||||||
import { Avatar } from "@mui/material";
|
const AvatarField = ({ source, ...rest }: AvatarProps & FieldProps) => {
|
||||||
import { useRecordContext } from "react-admin";
|
|
||||||
|
|
||||||
const AvatarField = ({ source, ...rest }) => {
|
|
||||||
const record = useRecordContext(rest);
|
|
||||||
const src = get(record, source)?.toString();
|
|
||||||
const { alt, classes, sizes, sx, variant } = rest;
|
const { alt, classes, sizes, sx, variant } = rest;
|
||||||
|
const record = useRecordContext(rest);
|
||||||
|
const mxcURL = get(record, source)?.toString();
|
||||||
|
|
||||||
|
const [src, setSrc] = useState<string>("");
|
||||||
|
|
||||||
|
const fetchAvatar = useCallback(async (mxcURL: string) => {
|
||||||
|
const response = await fetchAuthenticatedMedia(mxcURL, "thumbnail");
|
||||||
|
const blob = await response.blob();
|
||||||
|
const blobURL = URL.createObjectURL(blob);
|
||||||
|
setSrc(blobURL);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (mxcURL) {
|
||||||
|
fetchAvatar(mxcURL);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cleanup function to revoke the object URL
|
||||||
|
return () => {
|
||||||
|
if (src) {
|
||||||
|
URL.revokeObjectURL(src);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [mxcURL, fetchAvatar]);
|
||||||
|
|
||||||
return <Avatar alt={alt} classes={classes} sizes={sizes} src={src} sx={sx} variant={variant} />;
|
return <Avatar alt={alt} classes={classes} sizes={sizes} src={src} sx={sx} variant={variant} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -16,7 +16,7 @@ const resourceName = "rooms";
|
|||||||
const DeleteRoomButton: React.FC<DeleteRoomButtonProps> = (props) => {
|
const DeleteRoomButton: React.FC<DeleteRoomButtonProps> = (props) => {
|
||||||
const translate = useTranslate();
|
const translate = useTranslate();
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [block, setBlock] = useState(true);
|
const [block, setBlock] = useState(false);
|
||||||
|
|
||||||
const notify = useNotify();
|
const notify = useNotify();
|
||||||
const redirect = useRedirect();
|
const redirect = useRedirect();
|
||||||
@@ -78,7 +78,7 @@ const DeleteRoomButton: React.FC<DeleteRoomButtonProps> = (props) => {
|
|||||||
value={block}
|
value={block}
|
||||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) => setBlock(event.target.checked)}
|
onChange={(event: React.ChangeEvent<HTMLInputElement>) => setBlock(event.target.checked)}
|
||||||
label="resources.rooms.action.erase.fields.block"
|
label="resources.rooms.action.erase.fields.block"
|
||||||
defaultValue={true}
|
defaultValue={false}
|
||||||
/>
|
/>
|
||||||
</SimpleForm>
|
</SimpleForm>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
117
src/components/DeleteUserButton.tsx
Normal file
117
src/components/DeleteUserButton.tsx
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
import { Button, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle } from "@mui/material";
|
||||||
|
import { Fragment, useState } from "react";
|
||||||
|
import { SimpleForm, BooleanInput, useTranslate, RaRecord, useNotify, useRedirect, useDelete, NotificationType, useDeleteMany, Identifier, useUnselectAll } from "react-admin";
|
||||||
|
import ActionDelete from "@mui/icons-material/Delete";
|
||||||
|
import ActionCheck from "@mui/icons-material/CheckCircle";
|
||||||
|
import AlertError from "@mui/icons-material/ErrorOutline";
|
||||||
|
|
||||||
|
interface DeleteUserButtonProps {
|
||||||
|
selectedIds: Identifier[];
|
||||||
|
confirmTitle: string;
|
||||||
|
confirmContent: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const resourceName = "users";
|
||||||
|
|
||||||
|
const DeleteUserButton: React.FC<DeleteUserButtonProps> = (props) => {
|
||||||
|
const translate = useTranslate();
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const [deleteMedia, setDeleteMedia] = useState(false);
|
||||||
|
const [redactEvents, setRedactEvents] = useState(false);
|
||||||
|
|
||||||
|
const notify = useNotify();
|
||||||
|
const redirect = useRedirect();
|
||||||
|
|
||||||
|
const [deleteMany, { isLoading }] = useDeleteMany();
|
||||||
|
const unselectAll = useUnselectAll(resourceName);
|
||||||
|
const recordIds = props.selectedIds;
|
||||||
|
|
||||||
|
const handleDialogOpen = () => setOpen(true);
|
||||||
|
const handleDialogClose = () => setOpen(false);
|
||||||
|
|
||||||
|
const handleDelete = (values: {deleteMedia: boolean, redactEvents: boolean}) => {
|
||||||
|
deleteMany(
|
||||||
|
resourceName,
|
||||||
|
{ ids: recordIds, meta: values },
|
||||||
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
notify("ra.notification.deleted", {
|
||||||
|
messageArgs: {
|
||||||
|
smart_count: recordIds.length,
|
||||||
|
},
|
||||||
|
type: 'info' as NotificationType,
|
||||||
|
});
|
||||||
|
handleDialogClose();
|
||||||
|
unselectAll();
|
||||||
|
redirect("/users");
|
||||||
|
},
|
||||||
|
onError: (error) =>
|
||||||
|
notify("ra.notification.data_provider_error", { type: 'error' as NotificationType }),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleConfirm = () => {
|
||||||
|
setOpen(false);
|
||||||
|
handleDelete({ deleteMedia: deleteMedia, redactEvents: redactEvents });
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
<Button
|
||||||
|
onClick={handleDialogOpen}
|
||||||
|
disabled={isLoading}
|
||||||
|
className={"ra-delete-button"}
|
||||||
|
key="button"
|
||||||
|
size="small"
|
||||||
|
sx={{
|
||||||
|
"&.MuiButton-sizeSmall": {
|
||||||
|
lineHeight: 1.5,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
color={"error"}
|
||||||
|
startIcon={<ActionDelete />}
|
||||||
|
>
|
||||||
|
{translate("ra.action.delete")}
|
||||||
|
</Button>
|
||||||
|
<Dialog open={open} onClose={handleDialogClose}>
|
||||||
|
<DialogTitle>{translate(props.confirmTitle)}</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogContentText>{translate(props.confirmContent)}</DialogContentText>
|
||||||
|
<SimpleForm toolbar={false}>
|
||||||
|
<BooleanInput
|
||||||
|
source="deleteMedia"
|
||||||
|
value={deleteMedia}
|
||||||
|
onChange={(event: React.ChangeEvent<HTMLInputElement>) => setDeleteMedia(event.target.checked)}
|
||||||
|
label="resources.users.action.delete_media"
|
||||||
|
defaultValue={false}
|
||||||
|
/>
|
||||||
|
<BooleanInput
|
||||||
|
source="redactEvents"
|
||||||
|
value={redactEvents}
|
||||||
|
onChange={(event: React.ChangeEvent<HTMLInputElement>) => setRedactEvents(event.target.checked)}
|
||||||
|
label="resources.users.action.redact_events"
|
||||||
|
defaultValue={false}
|
||||||
|
/>
|
||||||
|
</SimpleForm>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button disabled={false} onClick={handleDialogClose} startIcon={<AlertError />}>
|
||||||
|
{translate("ra.action.cancel")}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
disabled={false}
|
||||||
|
onClick={handleConfirm}
|
||||||
|
className={"ra-confirm RaConfirm-confirmPrimary"}
|
||||||
|
autoFocus
|
||||||
|
startIcon={<ActionCheck />}
|
||||||
|
>
|
||||||
|
{translate("ra.action.confirm")}
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DeleteUserButton;
|
58
src/components/LoginFormBox.tsx
Normal file
58
src/components/LoginFormBox.tsx
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import { styled } from "@mui/material/styles";
|
||||||
|
import { Box } from "@mui/material";
|
||||||
|
|
||||||
|
const LoginFormBox = styled(Box)(({ theme }) => ({
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
minHeight: "calc(100vh - 1rem)",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "flex-start",
|
||||||
|
background: "url(./images/floating-cogs.svg)",
|
||||||
|
backgroundColor: "#f9f9f9",
|
||||||
|
backgroundRepeat: "no-repeat",
|
||||||
|
backgroundSize: "cover",
|
||||||
|
|
||||||
|
[`& .card`]: {
|
||||||
|
width: "30rem",
|
||||||
|
marginTop: "6rem",
|
||||||
|
marginBottom: "6rem",
|
||||||
|
},
|
||||||
|
[`& .avatar`]: {
|
||||||
|
margin: "1rem",
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "center",
|
||||||
|
},
|
||||||
|
[`& .icon`]: {
|
||||||
|
backgroundColor: theme.palette.grey[500],
|
||||||
|
},
|
||||||
|
[`& .hint`]: {
|
||||||
|
marginTop: "1em",
|
||||||
|
marginBottom: "1em",
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "center",
|
||||||
|
color: theme.palette.grey[600],
|
||||||
|
},
|
||||||
|
[`& .form`]: {
|
||||||
|
padding: "0 1rem 1rem 1rem",
|
||||||
|
},
|
||||||
|
[`& .select`]: {
|
||||||
|
marginBottom: "2rem",
|
||||||
|
},
|
||||||
|
[`& .actions`]: {
|
||||||
|
padding: "0 1rem 1rem 1rem",
|
||||||
|
},
|
||||||
|
[`& .serverVersion`]: {
|
||||||
|
color: theme.palette.grey[500],
|
||||||
|
fontFamily: "Roboto, Helvetica, Arial, sans-serif",
|
||||||
|
marginLeft: "0.5rem",
|
||||||
|
},
|
||||||
|
[`& .matrixVersions`]: {
|
||||||
|
color: theme.palette.grey[500],
|
||||||
|
fontFamily: "Roboto, Helvetica, Arial, sans-serif",
|
||||||
|
fontSize: "0.8rem",
|
||||||
|
marginBottom: "1rem",
|
||||||
|
marginLeft: "0.5rem",
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
export default LoginFormBox;
|
@@ -8,7 +8,9 @@ import DeleteSweepIcon from "@mui/icons-material/DeleteSweep";
|
|||||||
import FileOpenIcon from "@mui/icons-material/FileOpen";
|
import FileOpenIcon from "@mui/icons-material/FileOpen";
|
||||||
import LockIcon from "@mui/icons-material/Lock";
|
import LockIcon from "@mui/icons-material/Lock";
|
||||||
import LockOpenIcon from "@mui/icons-material/LockOpen";
|
import LockOpenIcon from "@mui/icons-material/LockOpen";
|
||||||
import { Box, Dialog, DialogContent, DialogContentText, DialogTitle, Tooltip } from "@mui/material";
|
import DownloadIcon from '@mui/icons-material/Download';
|
||||||
|
import DownloadingIcon from '@mui/icons-material/Downloading';
|
||||||
|
import { Grid2 as Grid, Box, Dialog, DialogContent, DialogContentText, DialogTitle, Tooltip, Link } from "@mui/material";
|
||||||
import { alpha, useTheme } from "@mui/material/styles";
|
import { alpha, useTheme } from "@mui/material/styles";
|
||||||
import {
|
import {
|
||||||
BooleanInput,
|
BooleanInput,
|
||||||
@@ -29,12 +31,11 @@ import {
|
|||||||
useTranslate,
|
useTranslate,
|
||||||
} from "react-admin";
|
} from "react-admin";
|
||||||
import { useMutation } from "@tanstack/react-query";
|
import { useMutation } from "@tanstack/react-query";
|
||||||
import { Link } from "react-router-dom";
|
|
||||||
|
|
||||||
import { dateParser } from "./date";
|
import { dateParser } from "./date";
|
||||||
import { DeleteMediaParams, SynapseDataProvider } from "../synapse/dataProvider";
|
import { DeleteMediaParams, SynapseDataProvider } from "../synapse/dataProvider";
|
||||||
import { getMediaUrl } from "../synapse/synapse";
|
|
||||||
import storage from "../storage";
|
import storage from "../storage";
|
||||||
|
import { fetchAuthenticatedMedia } from "../utils/fetchMedia";
|
||||||
|
|
||||||
const DeleteMediaDialog = ({ open, onClose, onSubmit }) => {
|
const DeleteMediaDialog = ({ open, onClose, onSubmit }) => {
|
||||||
const translate = useTranslate();
|
const translate = useTranslate();
|
||||||
@@ -311,48 +312,104 @@ export const QuarantineMediaButton = (props: ButtonProps) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ViewMediaButton = ({ media_id, label }) => {
|
export const ViewMediaButton = ({ mxcURL, label, uploadName, mimetype }) => {
|
||||||
const translate = useTranslate();
|
const translate = useTranslate();
|
||||||
const url = getMediaUrl(media_id);
|
const [loading, setLoading] = useState(false);
|
||||||
|
const isImage = mimetype && mimetype.startsWith("image/");
|
||||||
|
|
||||||
|
const openFileInNewTab = (blobURL: string) => {
|
||||||
|
const anchorElement = document.createElement("a");
|
||||||
|
anchorElement.href = blobURL;
|
||||||
|
anchorElement.target = "_blank";
|
||||||
|
document.body.appendChild(anchorElement);
|
||||||
|
anchorElement.click();
|
||||||
|
document.body.removeChild(anchorElement);
|
||||||
|
setTimeout(() => URL.revokeObjectURL(blobURL), 10);
|
||||||
|
};
|
||||||
|
|
||||||
|
const downloadFile = async (blobURL: string) => {
|
||||||
|
console.log("downloadFile", blobURL, uploadName);
|
||||||
|
const anchorElement = document.createElement("a");
|
||||||
|
anchorElement.href = blobURL;
|
||||||
|
anchorElement.download = uploadName;
|
||||||
|
document.body.appendChild(anchorElement);
|
||||||
|
anchorElement.click();
|
||||||
|
document.body.removeChild(anchorElement);
|
||||||
|
setTimeout(() => URL.revokeObjectURL(blobURL), 10);;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFile = async (preview: boolean) => {
|
||||||
|
setLoading(true);
|
||||||
|
const response = await fetchAuthenticatedMedia(mxcURL, "original");
|
||||||
|
const blob = await response.blob();
|
||||||
|
const blobURL = URL.createObjectURL(blob);
|
||||||
|
if (preview) {
|
||||||
|
openFileInNewTab(blobURL);
|
||||||
|
} else {
|
||||||
|
downloadFile(blobURL);
|
||||||
|
}
|
||||||
|
setLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box style={{ whiteSpace: "pre" }}>
|
<>
|
||||||
<Tooltip title={translate("resources.users_media.action.open")}>
|
<Box display="flex" alignItems="center">
|
||||||
<span>
|
<Tooltip title={translate("resources.users_media.action.open")}>
|
||||||
<Button
|
<span>
|
||||||
component={Link}
|
{isImage && (
|
||||||
to={url}
|
<Button
|
||||||
target="_blank"
|
disabled={loading}
|
||||||
rel="noopener"
|
onClick={() => handleFile(true)}
|
||||||
style={{ minWidth: 0, paddingLeft: 0, paddingRight: 0 }}
|
style={{ minWidth: 0, padding: 0, marginRight: 8 }}
|
||||||
>
|
>
|
||||||
<FileOpenIcon />
|
{loading ? <DownloadingIcon /> : <FileOpenIcon />}
|
||||||
</Button>
|
</Button>
|
||||||
</span>
|
)}
|
||||||
</Tooltip>
|
</span>
|
||||||
{label}
|
</Tooltip>
|
||||||
</Box>
|
<Button
|
||||||
|
disabled={loading}
|
||||||
|
onClick={() => handleFile(false)}
|
||||||
|
style={{ minWidth: 0, padding: 0, marginRight: 8 }}
|
||||||
|
>
|
||||||
|
{loading ? <DownloadingIcon /> : <DownloadIcon />}
|
||||||
|
</Button>
|
||||||
|
<span>{label}</span>
|
||||||
|
</Box>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const MediaIDField = ({ source }) => {
|
export const MediaIDField = ({ source }) => {
|
||||||
|
const record = useRecordContext();
|
||||||
|
if (!record) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
const homeserver = storage.getItem("home_server");
|
const homeserver = storage.getItem("home_server");
|
||||||
const record = useRecordContext();
|
|
||||||
if (!record) return null;
|
|
||||||
|
|
||||||
const src = get(record, source)?.toString();
|
const mediaID = get(record, source)?.toString();
|
||||||
if (!src) return null;
|
if (!mediaID) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return <ViewMediaButton media_id={`${homeserver}/${src}`} label={src} />;
|
const uploadName = decodeURIComponent(get(record, "upload_name")?.toString());
|
||||||
|
const mxcURL = `mxc://${homeserver}/${mediaID}`;
|
||||||
|
|
||||||
|
return <ViewMediaButton mxcURL={mxcURL} label={mediaID} uploadName={uploadName} mimetype={record.media_type}/>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const MXCField = ({ source }) => {
|
export const ReportMediaContent = ({ source }) => {
|
||||||
const record = useRecordContext();
|
const record = useRecordContext();
|
||||||
if (!record) return null;
|
if (!record) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const src = get(record, source)?.toString();
|
const mxcURL = get(record, source)?.toString();
|
||||||
if (!src) return null;
|
if (!mxcURL) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const media_id = src.replace("mxc://", "");
|
const uploadName = decodeURIComponent(get(record, "event_json.content.body")?.toString());
|
||||||
|
|
||||||
return <ViewMediaButton media_id={media_id} label={src} />;
|
return <ViewMediaButton mxcURL={mxcURL} label={mxcURL} uploadName={uploadName} mimetype={record.media_type}/>;
|
||||||
};
|
};
|
||||||
|
@@ -22,6 +22,14 @@ const de: SynapseTranslationMessages = {
|
|||||||
protocol_error: "Die URL muss mit 'http://' oder 'https://' beginnen",
|
protocol_error: "Die URL muss mit 'http://' oder 'https://' beginnen",
|
||||||
url_error: "Keine gültige Matrix Server URL",
|
url_error: "Keine gültige Matrix Server URL",
|
||||||
sso_sign_in: "Anmeldung mit SSO",
|
sso_sign_in: "Anmeldung mit SSO",
|
||||||
|
credentials: "Anmeldedaten",
|
||||||
|
access_token: "Zugriffstoken",
|
||||||
|
logout_acces_token_dialog: {
|
||||||
|
title: "Sie verwenden ein bestehendes Matrix-Zugriffstoken.",
|
||||||
|
content: "Möchten Sie diese Sitzung (die anderswo, z.B. in einem Matrix-Client, verwendet werden könnte) beenden oder sich nur vom Admin-Panel abmelden?",
|
||||||
|
confirm: "Sitzung beenden",
|
||||||
|
cancel: "Nur vom Admin-Panel abmelden",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
users: {
|
users: {
|
||||||
invalid_user_id: "Lokaler Anteil der Matrix Benutzer-ID ohne Homeserver.",
|
invalid_user_id: "Lokaler Anteil der Matrix Benutzer-ID ohne Homeserver.",
|
||||||
@@ -151,12 +159,15 @@ const de: SynapseTranslationMessages = {
|
|||||||
password: "Durch die Änderung des Passworts wird der Benutzer von allen Sitzungen abgemeldet.",
|
password: "Durch die Änderung des Passworts wird der Benutzer von allen Sitzungen abgemeldet.",
|
||||||
deactivate: "Sie müssen ein Passwort angeben, um ein Konto wieder zu aktivieren.",
|
deactivate: "Sie müssen ein Passwort angeben, um ein Konto wieder zu aktivieren.",
|
||||||
erase: "DSGVO konformes Löschen der Benutzerdaten.",
|
erase: "DSGVO konformes Löschen der Benutzerdaten.",
|
||||||
|
erase_text: "Das bedeutet, dass die von dem/den Benutzer(n) gesendeten Nachrichten für alle, die zum Zeitpunkt des Sendens im Raum waren, sichtbar bleiben, aber für Benutzer, die dem Raum später beitreten, nicht sichtbar sind.",
|
||||||
erase_admin_error: "Das Löschen des eigenen Benutzers ist nicht erlaubt.",
|
erase_admin_error: "Das Löschen des eigenen Benutzers ist nicht erlaubt.",
|
||||||
modify_managed_user_error: "Das Ändern eines vom System verwalteten Benutzers ist nicht zulässig.",
|
modify_managed_user_error: "Das Ändern eines vom System verwalteten Benutzers ist nicht zulässig.",
|
||||||
},
|
},
|
||||||
action: {
|
action: {
|
||||||
erase: "Lösche Benutzerdaten",
|
erase: "Lösche Benutzerdaten",
|
||||||
erase_avatar: "Avatar löschen"
|
erase_avatar: "Avatar löschen",
|
||||||
|
delete_media: "Alle von dem/den Benutzer(n) hochgeladenen Medien löschen",
|
||||||
|
redact_events: "Schwärzen aller vom Benutzer gesendeten Ereignisse (-s)",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
rooms: {
|
rooms: {
|
||||||
|
@@ -14,6 +14,14 @@ const en: SynapseTranslationMessages = {
|
|||||||
protocol_error: "URL has to start with 'http://' or 'https://'",
|
protocol_error: "URL has to start with 'http://' or 'https://'",
|
||||||
url_error: "Not a valid Matrix server URL",
|
url_error: "Not a valid Matrix server URL",
|
||||||
sso_sign_in: "Sign in with SSO",
|
sso_sign_in: "Sign in with SSO",
|
||||||
|
credentials: "Credentials",
|
||||||
|
access_token: "Access token",
|
||||||
|
logout_acces_token_dialog: {
|
||||||
|
title: "You are using an existing Matrix access token.",
|
||||||
|
content: "Do you want to destroy this session (that could be used elsewhere, e.g. in a Matrix client) or just logout from the admin panel?",
|
||||||
|
confirm: "Destroy session",
|
||||||
|
cancel: "Just logout from admin panel",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
users: {
|
users: {
|
||||||
invalid_user_id: "Localpart of a Matrix user-id without homeserver.",
|
invalid_user_id: "Localpart of a Matrix user-id without homeserver.",
|
||||||
@@ -142,12 +150,15 @@ const en: SynapseTranslationMessages = {
|
|||||||
password: "Changing password will log user out of all sessions.",
|
password: "Changing password will log user out of all sessions.",
|
||||||
deactivate: "You must provide a password to re-activate an account.",
|
deactivate: "You must provide a password to re-activate an account.",
|
||||||
erase: "Mark the user as GDPR-erased",
|
erase: "Mark the user as GDPR-erased",
|
||||||
|
erase_text: "This means messages sent by the user(-s) will still be visible by anyone who was in the room when these messages were sent, but hidden from users joining the room afterward.",
|
||||||
erase_admin_error: "Deleting own user is not allowed.",
|
erase_admin_error: "Deleting own user is not allowed.",
|
||||||
modify_managed_user_error: "Modifying a system-managed user is not allowed.",
|
modify_managed_user_error: "Modifying a system-managed user is not allowed.",
|
||||||
},
|
},
|
||||||
action: {
|
action: {
|
||||||
erase: "Erase user data",
|
erase: "Erase user data",
|
||||||
erase_avatar: "Erase avatar"
|
erase_avatar: "Erase avatar",
|
||||||
|
delete_media: "Delete all media uploaded by the user(-s)",
|
||||||
|
redact_events: "Redact all events sent by the user(-s)",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
rooms: {
|
rooms: {
|
||||||
|
@@ -13,6 +13,14 @@ const fa: SynapseTranslationMessages = {
|
|||||||
protocol_error: "URL باید با 'http://' یا 'https://' شروع شود",
|
protocol_error: "URL باید با 'http://' یا 'https://' شروع شود",
|
||||||
url_error: "آدرس وارد شده یک سرور معتبر نیست",
|
url_error: "آدرس وارد شده یک سرور معتبر نیست",
|
||||||
sso_sign_in: "با SSO وارد شوید",
|
sso_sign_in: "با SSO وارد شوید",
|
||||||
|
credentials: "اعتبارنامه",
|
||||||
|
access_token: "توکن دسترسی",
|
||||||
|
logout_acces_token_dialog: {
|
||||||
|
title: "شما در حال استفاده از یک نشانه دسترسی ماتریکس موجود هستید.",
|
||||||
|
content: "آیا میخواهید این جلسه (که میتواند در جای دیگر، مانند یک کلاینت ماتریکس استفاده شود) را نابود کنید یا فقط از پنل مدیریت خارج شوید؟",
|
||||||
|
confirm: "نابودی جلسه",
|
||||||
|
cancel: "فقط خروج از پنل مدیریت",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
users: {
|
users: {
|
||||||
invalid_user_id: "بخش محلی یک شناسه کاربری ماتریکس بدون سرور خانگی.",
|
invalid_user_id: "بخش محلی یک شناسه کاربری ماتریکس بدون سرور خانگی.",
|
||||||
@@ -138,11 +146,15 @@ const fa: SynapseTranslationMessages = {
|
|||||||
password: "با تغییر رمز عبور کاربر از تمام دستگاه ها خارج می شود.",
|
password: "با تغییر رمز عبور کاربر از تمام دستگاه ها خارج می شود.",
|
||||||
deactivate: "برای فعالسازی مجدد حساب باید رمز عبور وارد کنید.",
|
deactivate: "برای فعالسازی مجدد حساب باید رمز عبور وارد کنید.",
|
||||||
erase: "کاربر را به عنوان GDPR پاک شده علامت گذاری کنید",
|
erase: "کاربر را به عنوان GDPR پاک شده علامت گذاری کنید",
|
||||||
|
erase_text: "وهذا يعني أن الرسائل المرسلة من قبل المستخدم (المستخدمين) ستظل مرئية من قبل أي شخص كان في الغرفة عند إرسال هذه الرسائل، ولكنها مخفية عن المستخدمين الذين ينضمون إلى الغرفة بعد ذلك.",
|
||||||
erase_admin_error: "حذف المستخدم الخاص غير مسموح به.",
|
erase_admin_error: "حذف المستخدم الخاص غير مسموح به.",
|
||||||
modify_managed_user_error: "لا يُسمح بتغيير المستخدم الذي يديره النظام.",
|
modify_managed_user_error: "لا يُسمح بتغيير المستخدم الذي يديره النظام.",
|
||||||
},
|
},
|
||||||
action: {
|
action: {
|
||||||
erase: "پاک کردن اطلاعات کاربر",
|
erase: "پاک کردن اطلاعات کاربر",
|
||||||
|
erase_avatar: "محو الصورة الرمزية",
|
||||||
|
delete_media: "حذف جميع الوسائط التي تم تحميلها بواسطة المستخدم (المستخدمين)",
|
||||||
|
redact_events: "تنقيح جميع الأحداث المرسلة من قبل المستخدم (-s)",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
rooms: {
|
rooms: {
|
||||||
|
@@ -13,6 +13,14 @@ const fr: SynapseTranslationMessages = {
|
|||||||
protocol_error: "L'URL doit commencer par « http:// » ou « https:// »",
|
protocol_error: "L'URL doit commencer par « http:// » ou « https:// »",
|
||||||
url_error: "L'URL du serveur Matrix n'est pas valide",
|
url_error: "L'URL du serveur Matrix n'est pas valide",
|
||||||
sso_sign_in: "Se connecter avec l’authentification unique",
|
sso_sign_in: "Se connecter avec l’authentification unique",
|
||||||
|
credentials: "Identifiants",
|
||||||
|
access_token: "Jeton d'accès",
|
||||||
|
logout_acces_token_dialog: {
|
||||||
|
title: "Vous utilisez un jeton d'accès Matrix existant.",
|
||||||
|
content: "Voulez-vous détruire cette session (qui pourrait être utilisée ailleurs, par exemple dans un client Matrix) ou simplement vous déconnecter du panneau d'administration?",
|
||||||
|
confirm: "Détruire la session",
|
||||||
|
cancel: "Se déconnecter simplement du panneau d'administration",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
users: {
|
users: {
|
||||||
invalid_user_id: "Partie locale d'un identifiant utilisateur Matrix sans le nom du serveur d’accueil.",
|
invalid_user_id: "Partie locale d'un identifiant utilisateur Matrix sans le nom du serveur d’accueil.",
|
||||||
@@ -140,12 +148,15 @@ const fr: SynapseTranslationMessages = {
|
|||||||
helper: {
|
helper: {
|
||||||
deactivate: "Vous devrez fournir un mot de passe pour réactiver le compte.",
|
deactivate: "Vous devrez fournir un mot de passe pour réactiver le compte.",
|
||||||
erase: "Marquer l'utilisateur comme effacé conformément au RGPD",
|
erase: "Marquer l'utilisateur comme effacé conformément au RGPD",
|
||||||
|
erase_text: "Cela signifie que les messages envoyés par le(s) utilisateur(s) seront toujours visibles par toute personne qui se trouvait dans la salle au moment où ces messages ont été envoyés, mais qu'ils seront cachés aux utilisateurs qui rejoindront la salle par la suite.",
|
||||||
erase_admin_error: "La suppression de son propre utilisateur n'est pas autorisée.",
|
erase_admin_error: "La suppression de son propre utilisateur n'est pas autorisée.",
|
||||||
modify_managed_user_error: "La modification d'un utilisateur géré par le système n'est pas autorisée.",
|
modify_managed_user_error: "La modification d'un utilisateur géré par le système n'est pas autorisée.",
|
||||||
},
|
},
|
||||||
action: {
|
action: {
|
||||||
erase: "Effacer les données de l'utilisateur",
|
erase: "Effacer les données de l'utilisateur",
|
||||||
erase_avatar: "Effacer l'avatar",
|
erase_avatar: "Effacer l'avatar",
|
||||||
|
delete_media: "Supprimer tous les médias téléchargés par le(s) utilisateur(s)",
|
||||||
|
redact_events: "Expurger tous les événements envoyés par l'utilisateur(-s)",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
rooms: {
|
rooms: {
|
||||||
@@ -198,9 +209,9 @@ const fr: SynapseTranslationMessages = {
|
|||||||
title: "Supprimer le salon",
|
title: "Supprimer le salon",
|
||||||
content:
|
content:
|
||||||
"Voulez-vous vraiment supprimer le salon ? Cette opération ne peut être annulée. Tous les messages et médias partagés du salon seront supprimés du serveur !",
|
"Voulez-vous vraiment supprimer le salon ? Cette opération ne peut être annulée. Tous les messages et médias partagés du salon seront supprimés du serveur !",
|
||||||
fields: {
|
fields: {
|
||||||
block: "Bloquer et empêcher les utilisateurs de rejoindre la salle",
|
block: "Bloquer et empêcher les utilisateurs de rejoindre la salle",
|
||||||
},
|
},
|
||||||
success: "Salle/s supprimées avec succès.",
|
success: "Salle/s supprimées avec succès.",
|
||||||
failure: "La/les salle/s n'ont pas pu être supprimées.",
|
failure: "La/les salle/s n'ont pas pu être supprimées.",
|
||||||
},
|
},
|
||||||
|
11
src/i18n/index.d.ts
vendored
11
src/i18n/index.d.ts
vendored
@@ -11,6 +11,14 @@ interface SynapseTranslationMessages extends TranslationMessages {
|
|||||||
protocol_error: string;
|
protocol_error: string;
|
||||||
url_error: string;
|
url_error: string;
|
||||||
sso_sign_in: string;
|
sso_sign_in: string;
|
||||||
|
credentials: string;
|
||||||
|
access_token: string;
|
||||||
|
logout_acces_token_dialog: {
|
||||||
|
title: string;
|
||||||
|
content: string;
|
||||||
|
confirm: string;
|
||||||
|
cancel: string;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
users: {
|
users: {
|
||||||
invalid_user_id: string;
|
invalid_user_id: string;
|
||||||
@@ -138,12 +146,15 @@ interface SynapseTranslationMessages extends TranslationMessages {
|
|||||||
password?: string;
|
password?: string;
|
||||||
deactivate: string;
|
deactivate: string;
|
||||||
erase: string;
|
erase: string;
|
||||||
|
erase_text: string;
|
||||||
erase_admin_error: string;
|
erase_admin_error: string;
|
||||||
modify_managed_user_error: string;
|
modify_managed_user_error: string;
|
||||||
};
|
};
|
||||||
action: {
|
action: {
|
||||||
erase: string;
|
erase: string;
|
||||||
erase_avatar: string;
|
erase_avatar: string;
|
||||||
|
delete_media: string;
|
||||||
|
redact_events: string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
rooms: {
|
rooms: {
|
||||||
|
404
src/i18n/it.ts
404
src/i18n/it.ts
@@ -13,6 +13,14 @@ const it: SynapseTranslationMessages = {
|
|||||||
protocol_error: "L'URL deve iniziare per 'http://' o 'https://'",
|
protocol_error: "L'URL deve iniziare per 'http://' o 'https://'",
|
||||||
url_error: "URL del server Matrix non valido",
|
url_error: "URL del server Matrix non valido",
|
||||||
sso_sign_in: "Accedi con SSO",
|
sso_sign_in: "Accedi con SSO",
|
||||||
|
credentials: "Credenziali",
|
||||||
|
access_token: "Token di accesso",
|
||||||
|
logout_acces_token_dialog: {
|
||||||
|
title: "Stai utilizzando un token di accesso Matrix esistente.",
|
||||||
|
content: "Vuoi distruggere questa sessione (che potrebbe essere utilizzata altrove, ad esempio in un client Matrix) o semplicemente disconnetterti dal pannello di amministrazione?",
|
||||||
|
confirm: "Distruggi sessione",
|
||||||
|
cancel: "Disconnetti solo dal pannello di amministrazione",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
users: {
|
users: {
|
||||||
invalid_user_id: "ID utente non valido su questo homeserver.",
|
invalid_user_id: "ID utente non valido su questo homeserver.",
|
||||||
@@ -139,11 +147,15 @@ const it: SynapseTranslationMessages = {
|
|||||||
password: "Cambiando la password l'utente verrà disconnesso da tutte le sessioni attive.",
|
password: "Cambiando la password l'utente verrà disconnesso da tutte le sessioni attive.",
|
||||||
deactivate: "Devi fornire una password per riattivare l'account.",
|
deactivate: "Devi fornire una password per riattivare l'account.",
|
||||||
erase: "Constrassegna l'utente come cancellato dal GDPR",
|
erase: "Constrassegna l'utente come cancellato dal GDPR",
|
||||||
|
erase_text: "Ciò significa che i messaggi inviati dall'utente (o dagli utenti) saranno ancora visibili da chiunque si trovasse nella stanza al momento dell'invio, ma saranno nascosti agli utenti che si uniranno alla stanza in seguito.",
|
||||||
|
erase_admin_error: "Non è consentito eliminare il proprio utente.",
|
||||||
|
modify_managed_user_error: "La modifica di un utente gestito dal sistema non è consentita.",
|
||||||
},
|
},
|
||||||
action: {
|
action: {
|
||||||
erase: "Cancella i dati dell'utente",
|
erase: "Cancella i dati dell'utente",
|
||||||
erase_admin_error: "Non è consentito eliminare il proprio utente.",
|
erase_avatar: "Cancella l'avatar dell'utente",
|
||||||
modify_managed_user_error: "La modifica di un utente gestito dal sistema non è consentita.",
|
delete_media: "Elimina tutti i media caricati dall'utente(-s)",
|
||||||
|
redact_events: "Ridurre tutti gli eventi inviati dall'utente(-s)",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
rooms: {
|
rooms: {
|
||||||
@@ -170,211 +182,211 @@ const it: SynapseTranslationMessages = {
|
|||||||
},
|
},
|
||||||
helper: {
|
helper: {
|
||||||
/* forward_extremities:
|
/* forward_extremities:
|
||||||
"Forward extremities are the leaf events at the end of a Directed acyclic graph (DAG) in a room, aka events that have no children. The more exist in a room, the more state resolution that Synapse needs to perform (hint: it's an expensive operation). While Synapse has code to prevent too many of these existing at one time in a room, bugs can sometimes make them crop up again. If a room has >10 forward extremities, it's worth checking which room is the culprit and potentially removing them using the SQL queries mentioned in #1760.", */
|
"Forward extremities are the leaf events at the end of a Directed acyclic graph (DAG) in a room, aka events that have no children. The more exist in a room, the more state resolution that Synapse needs to perform (hint: it's an expensive operation). While Synapse has code to prevent too many of these existing at one time in a room, bugs can sometimes make them crop up again. If a room has >10 forward extremities, it's worth checking which room is the culprit and potentially removing them using the SQL queries mentioned in #1760.", */
|
||||||
},
|
},
|
||||||
enums: {
|
enums: {
|
||||||
join_rules: {
|
join_rules: {
|
||||||
public: "Pubblica",
|
public: "Pubblica",
|
||||||
knock: "Bussa",
|
knock: "Bussa",
|
||||||
invite: "Invita",
|
invite: "Invita",
|
||||||
private: "Privata",
|
private: "Privata",
|
||||||
},
|
|
||||||
guest_access: {
|
|
||||||
can_join: "Gli utenti ospiti possono entrare",
|
|
||||||
forbidden: "Gli utenti ospiti non possono entrare",
|
|
||||||
},
|
|
||||||
history_visibility: {
|
|
||||||
invited: "Dall'invito",
|
|
||||||
joined: "Dall'entrata",
|
|
||||||
shared: "Dalla condivisione",
|
|
||||||
world_readable: "Chiunque",
|
|
||||||
},
|
|
||||||
unencrypted: "Non criptata",
|
|
||||||
},
|
},
|
||||||
action: {
|
guest_access: {
|
||||||
erase: {
|
can_join: "Gli utenti ospiti possono entrare",
|
||||||
title: "Cancella stanza",
|
forbidden: "Gli utenti ospiti non possono entrare",
|
||||||
content:
|
|
||||||
"Sei sicuro di voler eliminare questa stanza? Questa azione è definitiva. Tutti i messaggi e i media condivisi in questa stanza verranno eliminati dal server!",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
history_visibility: {
|
||||||
|
invited: "Dall'invito",
|
||||||
|
joined: "Dall'entrata",
|
||||||
|
shared: "Dalla condivisione",
|
||||||
|
world_readable: "Chiunque",
|
||||||
|
},
|
||||||
|
unencrypted: "Non criptata",
|
||||||
},
|
},
|
||||||
reports: {
|
action: {
|
||||||
name: "Evento segnalato |||| Eventi segnalati",
|
erase: {
|
||||||
fields: {
|
title: "Cancella stanza",
|
||||||
id: "ID",
|
|
||||||
received_ts: "Orario del report",
|
|
||||||
user_id: "richiedente",
|
|
||||||
name: "nome della stanza",
|
|
||||||
score: "punteggio",
|
|
||||||
reason: "ragione",
|
|
||||||
event_id: "ID dell'evento",
|
|
||||||
event_json: {
|
|
||||||
origin: "server di origine",
|
|
||||||
origin_server_ts: "ora dell'invio",
|
|
||||||
type: "tipo di evento",
|
|
||||||
content: {
|
|
||||||
msgtype: "tipo di contenuto",
|
|
||||||
body: "contenuto",
|
|
||||||
format: "formato",
|
|
||||||
formatted_body: "contenuto formattato",
|
|
||||||
algorithm: "algoritmo",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
connections: {
|
|
||||||
name: "Connessioni",
|
|
||||||
fields: {
|
|
||||||
last_seen: "Data",
|
|
||||||
ip: "Indirizzo IP",
|
|
||||||
user_agent: "agente utente",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
devices: {
|
|
||||||
name: "Dispositivo |||| Dispositivi",
|
|
||||||
fields: {
|
|
||||||
device_id: "ID del dispositivo",
|
|
||||||
display_name: "Nome del dispositivo",
|
|
||||||
last_seen_ts: "Timestamp",
|
|
||||||
last_seen_ip: "Indirizzo IP",
|
|
||||||
},
|
|
||||||
action: {
|
|
||||||
erase: {
|
|
||||||
title: "Rimozione del dispositivo %{id}",
|
|
||||||
content: 'Sei sicuro di voler rimuovere il dispositivo "%{name}"?',
|
|
||||||
success: "Dispositivo rimosso con successo.",
|
|
||||||
failure: "C'è stato un errore.",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
users_media: {
|
|
||||||
name: "Media",
|
|
||||||
fields: {
|
|
||||||
media_id: "ID del media",
|
|
||||||
media_length: "Peso del file (in Byte)",
|
|
||||||
media_type: "Tipo",
|
|
||||||
upload_name: "Nome del file",
|
|
||||||
quarantined_by: "In quarantena da",
|
|
||||||
safe_from_quarantine: "Protetto dalla quarantena",
|
|
||||||
created_ts: "Creato",
|
|
||||||
last_access_ts: "Ultimo accesso",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
protect_media: {
|
|
||||||
action: {
|
|
||||||
create: "Non protetto, proteggi",
|
|
||||||
delete: "Protetto, rimuovi protezione",
|
|
||||||
none: "In quarantena",
|
|
||||||
send_success: "Stato della protezione cambiato con successo.",
|
|
||||||
send_failure: "C'è stato un errore.",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
quarantine_media: {
|
|
||||||
action: {
|
|
||||||
name: "Quarantina",
|
|
||||||
create: "Aggiungi alla quarantena",
|
|
||||||
delete: "In quarantena, rimuovi dalla quarantena",
|
|
||||||
none: "Protetto dalla quarantena",
|
|
||||||
send_success: "Stato della quarantena cambiato con successo.",
|
|
||||||
send_failure: "C'è stato un errore.",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
pushers: {
|
|
||||||
name: "Pusher |||| Pusher",
|
|
||||||
fields: {
|
|
||||||
app: "App",
|
|
||||||
app_display_name: "Nome dell'app",
|
|
||||||
app_id: "ID dell'app",
|
|
||||||
device_display_name: "Nome del dispositivo",
|
|
||||||
kind: "Tipo",
|
|
||||||
lang: "Lingua",
|
|
||||||
profile_tag: "Tag del profilo",
|
|
||||||
pushkey: "Pushkey",
|
|
||||||
data: { url: "URL" },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
servernotices: {
|
|
||||||
name: "Avvisi del server",
|
|
||||||
send: "Invia avvisi",
|
|
||||||
fields: {
|
|
||||||
body: "Messaggio",
|
|
||||||
},
|
|
||||||
action: {
|
|
||||||
send: "Invia nota",
|
|
||||||
send_success: "Avviso inviato con successo.",
|
|
||||||
send_failure: "C'è stato un errore.",
|
|
||||||
},
|
|
||||||
helper: {
|
|
||||||
send: 'Invia un avviso dal server agli utenti selezionati. La feature "Avvisi del server" è stata attivata sul server.',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
user_media_statistics: {
|
|
||||||
name: "Media degli utenti",
|
|
||||||
fields: {
|
|
||||||
media_count: "Numero media",
|
|
||||||
media_length: "Lunghezza media",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
forward_extremities: {
|
|
||||||
name: "Invia estremità",
|
|
||||||
fields: {
|
|
||||||
id: "Event ID",
|
|
||||||
received_ts: "Timestamp",
|
|
||||||
depth: "Profondità",
|
|
||||||
state_group: "State group",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
room_state: {
|
|
||||||
name: "Eventi di stato",
|
|
||||||
fields: {
|
|
||||||
type: "Tipo",
|
|
||||||
content: "Contenuto",
|
|
||||||
origin_server_ts: "Ora dell'invio",
|
|
||||||
sender: "Mittente",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
room_directory: {
|
|
||||||
name: "Elenco delle stanze",
|
|
||||||
fields: {
|
|
||||||
world_readable: "gli utenti ospite possono vedere senza entrare",
|
|
||||||
guest_can_join: "gli utenti ospite possono entrare",
|
|
||||||
},
|
|
||||||
action: {
|
|
||||||
title: "Cancella stanza dall'elenco |||| Cancella %{smart_count} stanze dall'elenco",
|
|
||||||
content:
|
content:
|
||||||
"Sei sicuro di voler rimuovere questa stanza dall'elenco? |||| Sei sicuro di voler rimuovere %{smart_count} stanze dall'elenco?",
|
"Sei sicuro di voler eliminare questa stanza? Questa azione è definitiva. Tutti i messaggi e i media condivisi in questa stanza verranno eliminati dal server!",
|
||||||
erase: "Rimuovi dall'elenco",
|
|
||||||
create: "Crea",
|
|
||||||
send_success: "Stanza creata con successo.",
|
|
||||||
send_failure: "C'è stato un errore.",
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
destinations: {
|
},
|
||||||
name: "Federazione",
|
reports: {
|
||||||
fields: {
|
name: "Evento segnalato |||| Eventi segnalati",
|
||||||
destination: "Destinazione",
|
fields: {
|
||||||
failure_ts: "Timestamp dell'errore",
|
id: "ID",
|
||||||
retry_last_ts: "Tentativo ultimo timestamp",
|
received_ts: "Orario del report",
|
||||||
retry_interval: "Intervallo dei tentativi",
|
user_id: "richiedente",
|
||||||
last_successful_stream_ordering: "Ultimo flusso riuscito con successo",
|
name: "nome della stanza",
|
||||||
stream_ordering: "Flusso",
|
score: "punteggio",
|
||||||
|
reason: "ragione",
|
||||||
|
event_id: "ID dell'evento",
|
||||||
|
event_json: {
|
||||||
|
origin: "server di origine",
|
||||||
|
origin_server_ts: "ora dell'invio",
|
||||||
|
type: "tipo di evento",
|
||||||
|
content: {
|
||||||
|
msgtype: "tipo di contenuto",
|
||||||
|
body: "contenuto",
|
||||||
|
format: "formato",
|
||||||
|
formatted_body: "contenuto formattato",
|
||||||
|
algorithm: "algoritmo",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
action: { reconnect: "Riconnetti" },
|
|
||||||
},
|
},
|
||||||
registration_tokens: {
|
},
|
||||||
name: "Token di registrazione",
|
connections: {
|
||||||
fields: {
|
name: "Connessioni",
|
||||||
token: "Token",
|
fields: {
|
||||||
valid: "Token valido",
|
last_seen: "Data",
|
||||||
uses_allowed: "Usi permessi",
|
ip: "Indirizzo IP",
|
||||||
pending: "In attesa",
|
user_agent: "agente utente",
|
||||||
completed: "Completato",
|
},
|
||||||
expiry_time: "Data della scadenza",
|
},
|
||||||
length: "Lunghezza",
|
devices: {
|
||||||
|
name: "Dispositivo |||| Dispositivi",
|
||||||
|
fields: {
|
||||||
|
device_id: "ID del dispositivo",
|
||||||
|
display_name: "Nome del dispositivo",
|
||||||
|
last_seen_ts: "Timestamp",
|
||||||
|
last_seen_ip: "Indirizzo IP",
|
||||||
|
},
|
||||||
|
action: {
|
||||||
|
erase: {
|
||||||
|
title: "Rimozione del dispositivo %{id}",
|
||||||
|
content: 'Sei sicuro di voler rimuovere il dispositivo "%{name}"?',
|
||||||
|
success: "Dispositivo rimosso con successo.",
|
||||||
|
failure: "C'è stato un errore.",
|
||||||
},
|
},
|
||||||
helper: { length: "Lunghezza del token se non viene dato alcun token." },
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
users_media: {
|
||||||
|
name: "Media",
|
||||||
|
fields: {
|
||||||
|
media_id: "ID del media",
|
||||||
|
media_length: "Peso del file (in Byte)",
|
||||||
|
media_type: "Tipo",
|
||||||
|
upload_name: "Nome del file",
|
||||||
|
quarantined_by: "In quarantena da",
|
||||||
|
safe_from_quarantine: "Protetto dalla quarantena",
|
||||||
|
created_ts: "Creato",
|
||||||
|
last_access_ts: "Ultimo accesso",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
protect_media: {
|
||||||
|
action: {
|
||||||
|
create: "Non protetto, proteggi",
|
||||||
|
delete: "Protetto, rimuovi protezione",
|
||||||
|
none: "In quarantena",
|
||||||
|
send_success: "Stato della protezione cambiato con successo.",
|
||||||
|
send_failure: "C'è stato un errore.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
quarantine_media: {
|
||||||
|
action: {
|
||||||
|
name: "Quarantina",
|
||||||
|
create: "Aggiungi alla quarantena",
|
||||||
|
delete: "In quarantena, rimuovi dalla quarantena",
|
||||||
|
none: "Protetto dalla quarantena",
|
||||||
|
send_success: "Stato della quarantena cambiato con successo.",
|
||||||
|
send_failure: "C'è stato un errore.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
pushers: {
|
||||||
|
name: "Pusher |||| Pusher",
|
||||||
|
fields: {
|
||||||
|
app: "App",
|
||||||
|
app_display_name: "Nome dell'app",
|
||||||
|
app_id: "ID dell'app",
|
||||||
|
device_display_name: "Nome del dispositivo",
|
||||||
|
kind: "Tipo",
|
||||||
|
lang: "Lingua",
|
||||||
|
profile_tag: "Tag del profilo",
|
||||||
|
pushkey: "Pushkey",
|
||||||
|
data: { url: "URL" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
servernotices: {
|
||||||
|
name: "Avvisi del server",
|
||||||
|
send: "Invia avvisi",
|
||||||
|
fields: {
|
||||||
|
body: "Messaggio",
|
||||||
|
},
|
||||||
|
action: {
|
||||||
|
send: "Invia nota",
|
||||||
|
send_success: "Avviso inviato con successo.",
|
||||||
|
send_failure: "C'è stato un errore.",
|
||||||
|
},
|
||||||
|
helper: {
|
||||||
|
send: 'Invia un avviso dal server agli utenti selezionati. La feature "Avvisi del server" è stata attivata sul server.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
user_media_statistics: {
|
||||||
|
name: "Media degli utenti",
|
||||||
|
fields: {
|
||||||
|
media_count: "Numero media",
|
||||||
|
media_length: "Lunghezza media",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
forward_extremities: {
|
||||||
|
name: "Invia estremità",
|
||||||
|
fields: {
|
||||||
|
id: "Event ID",
|
||||||
|
received_ts: "Timestamp",
|
||||||
|
depth: "Profondità",
|
||||||
|
state_group: "State group",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
room_state: {
|
||||||
|
name: "Eventi di stato",
|
||||||
|
fields: {
|
||||||
|
type: "Tipo",
|
||||||
|
content: "Contenuto",
|
||||||
|
origin_server_ts: "Ora dell'invio",
|
||||||
|
sender: "Mittente",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
room_directory: {
|
||||||
|
name: "Elenco delle stanze",
|
||||||
|
fields: {
|
||||||
|
world_readable: "gli utenti ospite possono vedere senza entrare",
|
||||||
|
guest_can_join: "gli utenti ospite possono entrare",
|
||||||
|
},
|
||||||
|
action: {
|
||||||
|
title: "Cancella stanza dall'elenco |||| Cancella %{smart_count} stanze dall'elenco",
|
||||||
|
content:
|
||||||
|
"Sei sicuro di voler rimuovere questa stanza dall'elenco? |||| Sei sicuro di voler rimuovere %{smart_count} stanze dall'elenco?",
|
||||||
|
erase: "Rimuovi dall'elenco",
|
||||||
|
create: "Crea",
|
||||||
|
send_success: "Stanza creata con successo.",
|
||||||
|
send_failure: "C'è stato un errore.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
destinations: {
|
||||||
|
name: "Federazione",
|
||||||
|
fields: {
|
||||||
|
destination: "Destinazione",
|
||||||
|
failure_ts: "Timestamp dell'errore",
|
||||||
|
retry_last_ts: "Tentativo ultimo timestamp",
|
||||||
|
retry_interval: "Intervallo dei tentativi",
|
||||||
|
last_successful_stream_ordering: "Ultimo flusso riuscito con successo",
|
||||||
|
stream_ordering: "Flusso",
|
||||||
|
},
|
||||||
|
action: { reconnect: "Riconnetti" },
|
||||||
|
},
|
||||||
|
registration_tokens: {
|
||||||
|
name: "Token di registrazione",
|
||||||
|
fields: {
|
||||||
|
token: "Token",
|
||||||
|
valid: "Token valido",
|
||||||
|
uses_allowed: "Usi permessi",
|
||||||
|
pending: "In attesa",
|
||||||
|
completed: "Completato",
|
||||||
|
expiry_time: "Data della scadenza",
|
||||||
|
length: "Lunghezza",
|
||||||
|
},
|
||||||
|
helper: { length: "Lunghezza del token se non viene dato alcun token." },
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
export default it;
|
export default it;
|
||||||
|
473
src/i18n/ru.ts
473
src/i18n/ru.ts
@@ -22,6 +22,14 @@ const ru: SynapseTranslationMessages = {
|
|||||||
protocol_error: "Адрес должен начинаться с 'http://' или 'https://'",
|
protocol_error: "Адрес должен начинаться с 'http://' или 'https://'",
|
||||||
url_error: "Неверный адрес сервера Matrix",
|
url_error: "Неверный адрес сервера Matrix",
|
||||||
sso_sign_in: "Вход через SSO",
|
sso_sign_in: "Вход через SSO",
|
||||||
|
credentials: "Учетные данные",
|
||||||
|
access_token: "Токен доступа",
|
||||||
|
logout_acces_token_dialog: {
|
||||||
|
title: "Вы используете существующий токен доступа Matrix.",
|
||||||
|
content: "Вы хотите завершить эту сессию (которая может быть использована в другом месте, например, в клиенте Matrix) или просто выйти из панели администрирования?",
|
||||||
|
confirm: "Завершить сессию",
|
||||||
|
cancel: "Просто выйти из панели администрирования",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
users: {
|
users: {
|
||||||
invalid_user_id: "Локальная часть ID пользователя Matrix без адреса домашнего сервера.",
|
invalid_user_id: "Локальная часть ID пользователя Matrix без адреса домашнего сервера.",
|
||||||
@@ -87,7 +95,7 @@ const ru: SynapseTranslationMessages = {
|
|||||||
header: "Загрузить CSV файл",
|
header: "Загрузить CSV файл",
|
||||||
explanation:
|
explanation:
|
||||||
"Здесь вы можете загрузить файл со значениями, разделёнными запятыми, которые будут использованы для создания или обновления данных пользователей. \
|
"Здесь вы можете загрузить файл со значениями, разделёнными запятыми, которые будут использованы для создания или обновления данных пользователей. \
|
||||||
В файле должны быть поля 'id' и 'displayname'. Вы можете скачать и изменить файл-образец отсюда: ",
|
В файле должны быть поля 'id' и 'displayname'. Вы можете скачать и изменить файл-образец отсюда: ",
|
||||||
},
|
},
|
||||||
startImport: {
|
startImport: {
|
||||||
simulate_only: "Только симулировать",
|
simulate_only: "Только симулировать",
|
||||||
@@ -122,10 +130,10 @@ const ru: SynapseTranslationMessages = {
|
|||||||
helper: {
|
helper: {
|
||||||
send: "Это API удаляет локальные файлы с вашего собственного сервера, включая локальные миниатюры и копии скачанных файлов. \
|
send: "Это API удаляет локальные файлы с вашего собственного сервера, включая локальные миниатюры и копии скачанных файлов. \
|
||||||
Данный API не затрагивает файлы, загруженные во внешние хранилища.",
|
Данный API не затрагивает файлы, загруженные во внешние хранилища.",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
resources: {
|
resources: {
|
||||||
users: {
|
users: {
|
||||||
name: "Пользователь |||| Пользователи",
|
name: "Пользователь |||| Пользователи",
|
||||||
email: "Почта",
|
email: "Почта",
|
||||||
msisdn: "Телефон",
|
msisdn: "Телефон",
|
||||||
@@ -159,265 +167,268 @@ const ru: SynapseTranslationMessages = {
|
|||||||
password: "Смена пароля завершит все сессии пользователя.",
|
password: "Смена пароля завершит все сессии пользователя.",
|
||||||
deactivate: "Вы должны предоставить пароль для реактивации учётной записи.",
|
deactivate: "Вы должны предоставить пароль для реактивации учётной записи.",
|
||||||
erase: "Пометить пользователя как удалённого в соответствии с GDPR",
|
erase: "Пометить пользователя как удалённого в соответствии с GDPR",
|
||||||
|
erase_text: "Это означает, что сообщения, отправленные пользователем (-ами), будут по-прежнему видны всем, кто находился в комнате в момент их отправки, но будут скрыты от пользователей, присоединившихся к комнате после этого.",
|
||||||
erase_admin_error: "Удаление собственного пользователя запрещено.",
|
erase_admin_error: "Удаление собственного пользователя запрещено.",
|
||||||
modify_managed_user_error: "Изменение пользователя, управляемого системой, не допускается.",
|
modify_managed_user_error: "Изменение пользователя, управляемого системой, не допускается.",
|
||||||
},
|
},
|
||||||
action: {
|
action: {
|
||||||
erase: "Удалить данные пользователя",
|
erase: "Удалить данные пользователя",
|
||||||
erase_avatar: "Удалить аватар",
|
erase_avatar: "Удалить аватар",
|
||||||
|
delete_media: "Удаление всех медиафайлов, загруженных пользователем (-ами)",
|
||||||
|
redact_events: "Удаление всех событий, отправленных пользователем (-ами)",
|
||||||
},
|
},
|
||||||
},
|
|
||||||
rooms: {
|
|
||||||
name: "Комната |||| Комнаты",
|
|
||||||
fields: {
|
|
||||||
room_id: "ID комнаты",
|
|
||||||
name: "Название",
|
|
||||||
canonical_alias: "Псевдоним",
|
|
||||||
joined_members: "Участники",
|
|
||||||
joined_local_members: "Локальные участники",
|
|
||||||
joined_local_devices: "Локальные устройства",
|
|
||||||
state_events: "События состояния / Сложность",
|
|
||||||
version: "Версия",
|
|
||||||
is_encrypted: "Зашифровано",
|
|
||||||
encryption: "Шифрование",
|
|
||||||
federatable: "Федерация",
|
|
||||||
public: "Отображается в каталоге комнат",
|
|
||||||
creator: "Создатель",
|
|
||||||
join_rules: "Правила входа",
|
|
||||||
guest_access: "Гостевой доступ",
|
|
||||||
history_visibility: "Видимость истории",
|
|
||||||
topic: "Тема",
|
|
||||||
avatar: "Аватар",
|
|
||||||
},
|
},
|
||||||
helper: {
|
rooms: {
|
||||||
forward_extremities:
|
name: "Комната |||| Комнаты",
|
||||||
"Оконечности — это события-листья в конце ориентированного ациклического графа (DAG) в комнате, т.е. события без дочерних элементов. \
|
fields: {
|
||||||
|
room_id: "ID комнаты",
|
||||||
|
name: "Название",
|
||||||
|
canonical_alias: "Псевдоним",
|
||||||
|
joined_members: "Участники",
|
||||||
|
joined_local_members: "Локальные участники",
|
||||||
|
joined_local_devices: "Локальные устройства",
|
||||||
|
state_events: "События состояния / Сложность",
|
||||||
|
version: "Версия",
|
||||||
|
is_encrypted: "Зашифровано",
|
||||||
|
encryption: "Шифрование",
|
||||||
|
federatable: "Федерация",
|
||||||
|
public: "Отображается в каталоге комнат",
|
||||||
|
creator: "Создатель",
|
||||||
|
join_rules: "Правила входа",
|
||||||
|
guest_access: "Гостевой доступ",
|
||||||
|
history_visibility: "Видимость истории",
|
||||||
|
topic: "Тема",
|
||||||
|
avatar: "Аватар",
|
||||||
|
},
|
||||||
|
helper: {
|
||||||
|
forward_extremities:
|
||||||
|
"Оконечности — это события-листья в конце ориентированного ациклического графа (DAG) в комнате, т.е. события без дочерних элементов. \
|
||||||
Чем больше их в комнате, тем больше Synapse работает над разрешением состояния (это дорогостоящая операция). \
|
Чем больше их в комнате, тем больше Synapse работает над разрешением состояния (это дорогостоящая операция). \
|
||||||
Хотя Synapse старается не допускать существования слишком большого числа таких событий в комнате, из-за ошибок они иногда снова появляются. \
|
Хотя Synapse старается не допускать существования слишком большого числа таких событий в комнате, из-за ошибок они иногда снова появляются. \
|
||||||
Если в комнате >10 оконечностей, стоит найти комнату-виновника и попробовать удалить их с помощью SQL-запросов из #1760.",
|
Если в комнате >10 оконечностей, стоит найти комнату-виновника и попробовать удалить их с помощью SQL-запросов из #1760.",
|
||||||
},
|
|
||||||
enums: {
|
|
||||||
join_rules: {
|
|
||||||
public: "Для всех",
|
|
||||||
knock: "Надо постучать",
|
|
||||||
invite: "По приглашению",
|
|
||||||
private: "Приватная",
|
|
||||||
},
|
},
|
||||||
guest_access: {
|
enums: {
|
||||||
can_join: "Гости могут войти",
|
join_rules: {
|
||||||
forbidden: "Гости не могут войти",
|
public: "Для всех",
|
||||||
},
|
knock: "Надо постучать",
|
||||||
history_visibility: {
|
invite: "По приглашению",
|
||||||
invited: "С момента приглашения",
|
private: "Приватная",
|
||||||
joined: "С момента входа",
|
},
|
||||||
shared: "С момента открытия доступа",
|
guest_access: {
|
||||||
world_readable: "Для всех",
|
can_join: "Гости могут войти",
|
||||||
},
|
forbidden: "Гости не могут войти",
|
||||||
unencrypted: "Без шифрования",
|
},
|
||||||
},
|
history_visibility: {
|
||||||
action: {
|
invited: "С момента приглашения",
|
||||||
erase: {
|
joined: "С момента входа",
|
||||||
title: "Удалить комнату",
|
shared: "С момента открытия доступа",
|
||||||
content:
|
world_readable: "Для всех",
|
||||||
"Действительно удалить эту комнату? Это действие будет невозможно отменить. Все сообщения и файлы в комнате будут удалены с сервера!",
|
},
|
||||||
fields: {
|
unencrypted: "Без шифрования",
|
||||||
block: "Заблокировать и запретить пользователям присоединяться к комнате",
|
},
|
||||||
|
action: {
|
||||||
|
erase: {
|
||||||
|
title: "Удалить комнату",
|
||||||
|
content:
|
||||||
|
"Действительно удалить эту комнату? Это действие будет невозможно отменить. Все сообщения и файлы в комнате будут удалены с сервера!",
|
||||||
|
fields: {
|
||||||
|
block: "Заблокировать и запретить пользователям присоединяться к комнате",
|
||||||
|
},
|
||||||
|
success: "Комната/ы успешно удалены",
|
||||||
|
failure: "Комната/ы не могут быть удалены.",
|
||||||
},
|
},
|
||||||
success: "Комната/ы успешно удалены",
|
|
||||||
failure: "Комната/ы не могут быть удалены.",
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
reports: {
|
||||||
reports: {
|
name: "Жалоба |||| Жалобы",
|
||||||
name: "Жалоба |||| Жалобы",
|
fields: {
|
||||||
fields: {
|
id: "ID",
|
||||||
id: "ID",
|
received_ts: "Дата и время жалобы",
|
||||||
received_ts: "Дата и время жалобы",
|
user_id: "Автор жалобы",
|
||||||
user_id: "Автор жалобы",
|
name: "Название комнаты",
|
||||||
name: "Название комнаты",
|
score: "Баллы",
|
||||||
score: "Баллы",
|
reason: "Причина",
|
||||||
reason: "Причина",
|
event_id: "ID события",
|
||||||
event_id: "ID события",
|
event_json: {
|
||||||
event_json: {
|
origin: "Исходнный сервер",
|
||||||
origin: "Исходнный сервер",
|
origin_server_ts: "Дата и время отправки",
|
||||||
origin_server_ts: "Дата и время отправки",
|
type: "Тип события",
|
||||||
type: "Тип события",
|
content: {
|
||||||
content: {
|
msgtype: "Тип содержимого",
|
||||||
msgtype: "Тип содержимого",
|
body: "Содержимое",
|
||||||
body: "Содержимое",
|
format: "Формат",
|
||||||
format: "Формат",
|
formatted_body: "Форматированное содержимое",
|
||||||
formatted_body: "Форматированное содержимое",
|
algorithm: "Алгоритм",
|
||||||
algorithm: "Алгоритм",
|
url: "Ссылка",
|
||||||
url: "Ссылка",
|
info: {
|
||||||
info: {
|
mimetype: "Тип",
|
||||||
mimetype: "Тип",
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
action: {
|
||||||
action: {
|
erase: {
|
||||||
erase: {
|
title: "Удалить жалобу",
|
||||||
title: "Удалить жалобу",
|
content: "Действительно удалить жалобу? Это действие будет невозможно отменить.",
|
||||||
content: "Действительно удалить жалобу? Это действие будет невозможно отменить.",
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
connections: {
|
||||||
connections: {
|
name: "Подключения",
|
||||||
name: "Подключения",
|
fields: {
|
||||||
fields: {
|
last_seen: "Дата",
|
||||||
last_seen: "Дата",
|
ip: "IP адрес",
|
||||||
ip: "IP адрес",
|
user_agent: "Юзер-агент",
|
||||||
user_agent: "Юзер-агент",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
devices: {
|
|
||||||
name: "Устройство |||| Устройства",
|
|
||||||
fields: {
|
|
||||||
device_id: "ID устройства",
|
|
||||||
display_name: "Название",
|
|
||||||
last_seen_ts: "Дата и время",
|
|
||||||
last_seen_ip: "IP адрес",
|
|
||||||
},
|
|
||||||
action: {
|
|
||||||
erase: {
|
|
||||||
title: "Удаление %{id}",
|
|
||||||
content: 'Действительно удалить устройство "%{name}"?',
|
|
||||||
success: "Устройство успешно удалено.",
|
|
||||||
failure: "Произошла ошибка.",
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
devices: {
|
||||||
users_media: {
|
name: "Устройство |||| Устройства",
|
||||||
name: "Файлы",
|
fields: {
|
||||||
fields: {
|
device_id: "ID устройства",
|
||||||
media_id: "ID файла",
|
display_name: "Название",
|
||||||
media_length: "Размер файла (в байтах)",
|
last_seen_ts: "Дата и время",
|
||||||
media_type: "Тип",
|
last_seen_ip: "IP адрес",
|
||||||
upload_name: "Имя файла",
|
},
|
||||||
quarantined_by: "На карантине",
|
action: {
|
||||||
safe_from_quarantine: "Защитить от карантина",
|
erase: {
|
||||||
created_ts: "Создано",
|
title: "Удаление %{id}",
|
||||||
last_access_ts: "Последний доступ",
|
content: 'Действительно удалить устройство "%{name}"?',
|
||||||
|
success: "Устройство успешно удалено.",
|
||||||
|
failure: "Произошла ошибка.",
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
action: {
|
users_media: {
|
||||||
open: "Открыть файл в новом окне",
|
name: "Файлы",
|
||||||
|
fields: {
|
||||||
|
media_id: "ID файла",
|
||||||
|
media_length: "Размер файла (в байтах)",
|
||||||
|
media_type: "Тип",
|
||||||
|
upload_name: "Имя файла",
|
||||||
|
quarantined_by: "На карантине",
|
||||||
|
safe_from_quarantine: "Защитить от карантина",
|
||||||
|
created_ts: "Создано",
|
||||||
|
last_access_ts: "Последний доступ",
|
||||||
|
},
|
||||||
|
action: {
|
||||||
|
open: "Открыть файл в новом окне",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
protect_media: {
|
||||||
protect_media: {
|
action: {
|
||||||
action: {
|
create: "Не защищён, установить защиту",
|
||||||
create: "Не защищён, установить защиту",
|
delete: "Защищён, снять защиту",
|
||||||
delete: "Защищён, снять защиту",
|
none: "На карантине",
|
||||||
none: "На карантине",
|
send_success: "Статус защиты успешно изменён.",
|
||||||
send_success: "Статус защиты успешно изменён.",
|
send_failure: "Произошла ошибка.",
|
||||||
send_failure: "Произошла ошибка.",
|
},
|
||||||
},
|
},
|
||||||
},
|
quarantine_media: {
|
||||||
quarantine_media: {
|
action: {
|
||||||
action: {
|
name: "Карантин",
|
||||||
name: "Карантин",
|
create: "Поместить на карантин",
|
||||||
create: "Поместить на карантин",
|
delete: "На карантине, снять карантин",
|
||||||
delete: "На карантине, снять карантин",
|
none: "Защищено от карантина",
|
||||||
none: "Защищено от карантина",
|
send_success: "Статус карантина успешно изменён.",
|
||||||
send_success: "Статус карантина успешно изменён.",
|
send_failure: "Произошла ошибка.",
|
||||||
send_failure: "Произошла ошибка.",
|
},
|
||||||
},
|
},
|
||||||
},
|
pushers: {
|
||||||
pushers: {
|
name: "Пушер |||| Пушеры",
|
||||||
name: "Пушер |||| Пушеры",
|
fields: {
|
||||||
fields: {
|
app: "Приложение",
|
||||||
app: "Приложение",
|
app_display_name: "Название приложения",
|
||||||
app_display_name: "Название приложения",
|
app_id: "ID приложения",
|
||||||
app_id: "ID приложения",
|
device_display_name: "Название устройства",
|
||||||
device_display_name: "Название устройства",
|
kind: "Вид",
|
||||||
kind: "Вид",
|
lang: "Язык",
|
||||||
lang: "Язык",
|
profile_tag: "Тег профиля",
|
||||||
profile_tag: "Тег профиля",
|
pushkey: "Ключ",
|
||||||
pushkey: "Ключ",
|
data: { url: "URL" },
|
||||||
data: { url: "URL" },
|
},
|
||||||
},
|
},
|
||||||
},
|
servernotices: {
|
||||||
servernotices: {
|
name: "Серверные уведомления",
|
||||||
name: "Серверные уведомления",
|
send: "Отправить серверные уведомления",
|
||||||
send: "Отправить серверные уведомления",
|
fields: {
|
||||||
fields: {
|
body: "Сообщение",
|
||||||
body: "Сообщение",
|
},
|
||||||
|
action: {
|
||||||
|
send: "Отправить",
|
||||||
|
send_success: "Серверное уведомление успешно отправлено.",
|
||||||
|
send_failure: "Произошла ошибка.",
|
||||||
|
},
|
||||||
|
helper: {
|
||||||
|
send: 'Отправить серверное уведомление выбранным пользователям. На сервере должна быть активна функция "Server Notices".',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
action: {
|
user_media_statistics: {
|
||||||
send: "Отправить",
|
name: "Файлы пользователей",
|
||||||
send_success: "Серверное уведомление успешно отправлено.",
|
fields: {
|
||||||
send_failure: "Произошла ошибка.",
|
media_count: "Количество файлов",
|
||||||
|
media_length: "Размер файлов",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
helper: {
|
forward_extremities: {
|
||||||
send: 'Отправить серверное уведомление выбранным пользователям. На сервере должна быть активна функция "Server Notices".',
|
name: "Оконечности",
|
||||||
|
fields: {
|
||||||
|
id: "ID события",
|
||||||
|
received_ts: "Дата и время",
|
||||||
|
depth: "Глубина",
|
||||||
|
state_group: "Группа состояния",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
room_state: {
|
||||||
user_media_statistics: {
|
name: "События состояния",
|
||||||
name: "Файлы пользователей",
|
fields: {
|
||||||
fields: {
|
type: "Тип",
|
||||||
media_count: "Количество файлов",
|
content: "Содержимое",
|
||||||
media_length: "Размер файлов",
|
origin_server_ts: "Дата отправки",
|
||||||
|
sender: "Отправитель",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
room_directory: {
|
||||||
forward_extremities: {
|
name: "Каталог комнат",
|
||||||
name: "Оконечности",
|
fields: {
|
||||||
fields: {
|
world_readable: "Гости могут просматривать без входа",
|
||||||
id: "ID события",
|
guest_can_join: "Гости могут войти",
|
||||||
received_ts: "Дата и время",
|
},
|
||||||
depth: "Глубина",
|
action: {
|
||||||
state_group: "Группа состояния",
|
title:
|
||||||
|
"Удалить комнату из каталога |||| Удалить %{smart_count} комнаты из каталога |||| Удалить %{smart_count} комнат из каталога",
|
||||||
|
content:
|
||||||
|
"Действительно удалить комнату из каталога? |||| Действительно удалить %{smart_count} комнаты из каталога? |||| Действительно удалить %{smart_count} комнат из каталога?",
|
||||||
|
erase: "Удалить из каталога комнат",
|
||||||
|
create: "Опубликовать в каталоге комнат",
|
||||||
|
send_success: "Комната успешно опубликована.",
|
||||||
|
send_failure: "Произошла ошибка.",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
destinations: {
|
||||||
room_state: {
|
name: "Федерация",
|
||||||
name: "События состояния",
|
fields: {
|
||||||
fields: {
|
destination: "Назначение",
|
||||||
type: "Тип",
|
failure_ts: "Дата и время ошибки",
|
||||||
content: "Содержимое",
|
retry_last_ts: "Дата и время последней попытки",
|
||||||
origin_server_ts: "Дата отправки",
|
retry_interval: "Интервал между попытками",
|
||||||
sender: "Отправитель",
|
last_successful_stream_ordering: "Последний успешный поток",
|
||||||
|
stream_ordering: "Поток",
|
||||||
|
},
|
||||||
|
action: { reconnect: "Переподключиться" },
|
||||||
},
|
},
|
||||||
},
|
registration_tokens: {
|
||||||
room_directory: {
|
name: "Токены регистрации",
|
||||||
name: "Каталог комнат",
|
fields: {
|
||||||
fields: {
|
token: "Токен",
|
||||||
world_readable: "Гости могут просматривать без входа",
|
valid: "Рабочий токен",
|
||||||
guest_can_join: "Гости могут войти",
|
uses_allowed: "Количество использований",
|
||||||
|
pending: "Ожидает",
|
||||||
|
completed: "Завершено",
|
||||||
|
expiry_time: "Дата окончания",
|
||||||
|
length: "Длина",
|
||||||
|
},
|
||||||
|
helper: { length: "Длина токена, если токен не задан." },
|
||||||
},
|
},
|
||||||
action: {
|
},
|
||||||
title:
|
|
||||||
"Удалить комнату из каталога |||| Удалить %{smart_count} комнаты из каталога |||| Удалить %{smart_count} комнат из каталога",
|
|
||||||
content:
|
|
||||||
"Действительно удалить комнату из каталога? |||| Действительно удалить %{smart_count} комнаты из каталога? |||| Действительно удалить %{smart_count} комнат из каталога?",
|
|
||||||
erase: "Удалить из каталога комнат",
|
|
||||||
create: "Опубликовать в каталоге комнат",
|
|
||||||
send_success: "Комната успешно опубликована.",
|
|
||||||
send_failure: "Произошла ошибка.",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
destinations: {
|
|
||||||
name: "Федерация",
|
|
||||||
fields: {
|
|
||||||
destination: "Назначение",
|
|
||||||
failure_ts: "Дата и время ошибки",
|
|
||||||
retry_last_ts: "Дата и время последней попытки",
|
|
||||||
retry_interval: "Интервал между попытками",
|
|
||||||
last_successful_stream_ordering: "Последний успешный поток",
|
|
||||||
stream_ordering: "Поток",
|
|
||||||
},
|
|
||||||
action: { reconnect: "Переподключиться" },
|
|
||||||
},
|
|
||||||
registration_tokens: {
|
|
||||||
name: "Токены регистрации",
|
|
||||||
fields: {
|
|
||||||
token: "Токен",
|
|
||||||
valid: "Рабочий токен",
|
|
||||||
uses_allowed: "Количество использований",
|
|
||||||
pending: "Ожидает",
|
|
||||||
completed: "Завершено",
|
|
||||||
expiry_time: "Дата окончания",
|
|
||||||
length: "Длина",
|
|
||||||
},
|
|
||||||
helper: { length: "Длина токена, если токен не задан." },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
export default ru;
|
export default ru;
|
||||||
|
@@ -21,6 +21,14 @@ const zh: SynapseTranslationMessages = {
|
|||||||
protocol_error: "URL 需要以'http://'或'https://'作为起始",
|
protocol_error: "URL 需要以'http://'或'https://'作为起始",
|
||||||
url_error: "不是一个有效的 Matrix 服务器地址",
|
url_error: "不是一个有效的 Matrix 服务器地址",
|
||||||
sso_sign_in: "使用 SSO 登录",
|
sso_sign_in: "使用 SSO 登录",
|
||||||
|
credentials: "凭证",
|
||||||
|
access_token: "访问令牌",
|
||||||
|
logout_acces_token_dialog: {
|
||||||
|
title: "您正在使用现有的 Matrix 访问令牌。",
|
||||||
|
content: "您想销毁此会话(可能在其他地方使用,例如在 Matrix 客户端中)还是仅从管理面板退出?",
|
||||||
|
confirm: "销毁会话",
|
||||||
|
cancel: "仅从管理面板退出",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
users: {
|
users: {
|
||||||
invalid_user_id: "必须要是一个有效的 Matrix 用户 ID ,例如 @user_id:homeserver",
|
invalid_user_id: "必须要是一个有效的 Matrix 用户 ID ,例如 @user_id:homeserver",
|
||||||
@@ -143,12 +151,15 @@ const zh: SynapseTranslationMessages = {
|
|||||||
helper: {
|
helper: {
|
||||||
deactivate: "您必须提供一串密码来激活账户。",
|
deactivate: "您必须提供一串密码来激活账户。",
|
||||||
erase: "将用户标记为根据 GDPR 的要求抹除了",
|
erase: "将用户标记为根据 GDPR 的要求抹除了",
|
||||||
|
erase_text: "这意味着用户发送的信息对于发送信息时在房间内的任何人来说都是可见的,但对于之后加入房间的用户来说则是隐藏的。",
|
||||||
erase_admin_error: "不允许删除自己的用户",
|
erase_admin_error: "不允许删除自己的用户",
|
||||||
modify_managed_user_error: "不允许修改系统管理的用户。",
|
modify_managed_user_error: "不允许修改系统管理的用户。",
|
||||||
},
|
},
|
||||||
action: {
|
action: {
|
||||||
erase: "抹除用户信息",
|
erase: "抹除用户信息",
|
||||||
erase_avatar: "抹掉头像",
|
erase_avatar: "抹掉头像",
|
||||||
|
delete_media: "删除用户上传的所有媒体",
|
||||||
|
redact_events: "重新编辑用户(-s)发送的所有事件",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
rooms: {
|
rooms: {
|
||||||
|
@@ -9,8 +9,12 @@ import storage from "./storage";
|
|||||||
fetch("config.json")
|
fetch("config.json")
|
||||||
.then(res => res.json())
|
.then(res => res.json())
|
||||||
.then(props => {
|
.then(props => {
|
||||||
storage.setItem("as_managed_users", JSON.stringify(props.asManagedUsers));
|
if (props.asManagedUsers) {
|
||||||
storage.setItem("support_url", props.supportURL);
|
storage.setItem("as_managed_users", JSON.stringify(props.asManagedUsers));
|
||||||
|
}
|
||||||
|
if (props.supportURL) {
|
||||||
|
storage.setItem("support_url", props.supportURL);
|
||||||
|
}
|
||||||
return createRoot(document.getElementById("root")).render(
|
return createRoot(document.getElementById("root")).render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<AppContext.Provider value={props}>
|
<AppContext.Provider value={props}>
|
||||||
|
@@ -1,8 +1,7 @@
|
|||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
|
|
||||||
import LockIcon from "@mui/icons-material/Lock";
|
import LockIcon from "@mui/icons-material/Lock";
|
||||||
import { Avatar, Box, Button, Card, CardActions, CircularProgress, MenuItem, Select, Typography } from "@mui/material";
|
import { Avatar, Box, Button, Card, CardActions, CircularProgress, MenuItem, Select, Tab, Tabs, Typography } from "@mui/material";
|
||||||
import { styled } from "@mui/material/styles";
|
|
||||||
import {
|
import {
|
||||||
Form,
|
Form,
|
||||||
FormDataConsumer,
|
FormDataConsumer,
|
||||||
@@ -17,7 +16,7 @@ import {
|
|||||||
useLocales,
|
useLocales,
|
||||||
} from "react-admin";
|
} from "react-admin";
|
||||||
import { useFormContext } from "react-hook-form";
|
import { useFormContext } from "react-hook-form";
|
||||||
|
import LoginFormBox from "../components/LoginFormBox";
|
||||||
import { useAppContext } from "../AppContext";
|
import { useAppContext } from "../AppContext";
|
||||||
import {
|
import {
|
||||||
getServerVersion,
|
getServerVersion,
|
||||||
@@ -29,66 +28,18 @@ import {
|
|||||||
} from "../synapse/synapse";
|
} from "../synapse/synapse";
|
||||||
import storage from "../storage";
|
import storage from "../storage";
|
||||||
|
|
||||||
const FormBox = styled(Box)(({ theme }) => ({
|
export type LoginMethod = "credentials" | "accessToken";
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
minHeight: "calc(100vh - 1rem)",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "flex-start",
|
|
||||||
background: "url(./images/floating-cogs.svg)",
|
|
||||||
backgroundColor: "#f9f9f9",
|
|
||||||
backgroundRepeat: "no-repeat",
|
|
||||||
backgroundSize: "cover",
|
|
||||||
|
|
||||||
[`& .card`]: {
|
|
||||||
width: "30rem",
|
|
||||||
marginTop: "6rem",
|
|
||||||
marginBottom: "6rem",
|
|
||||||
},
|
|
||||||
[`& .avatar`]: {
|
|
||||||
margin: "1rem",
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "center",
|
|
||||||
},
|
|
||||||
[`& .icon`]: {
|
|
||||||
backgroundColor: theme.palette.grey[500],
|
|
||||||
},
|
|
||||||
[`& .hint`]: {
|
|
||||||
marginTop: "1em",
|
|
||||||
marginBottom: "1em",
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "center",
|
|
||||||
color: theme.palette.grey[600],
|
|
||||||
},
|
|
||||||
[`& .form`]: {
|
|
||||||
padding: "0 1rem 1rem 1rem",
|
|
||||||
},
|
|
||||||
[`& .select`]: {
|
|
||||||
marginBottom: "2rem",
|
|
||||||
},
|
|
||||||
[`& .actions`]: {
|
|
||||||
padding: "0 1rem 1rem 1rem",
|
|
||||||
},
|
|
||||||
[`& .serverVersion`]: {
|
|
||||||
color: theme.palette.grey[500],
|
|
||||||
fontFamily: "Roboto, Helvetica, Arial, sans-serif",
|
|
||||||
marginLeft: "0.5rem",
|
|
||||||
},
|
|
||||||
[`& .matrixVersions`]: {
|
|
||||||
color: theme.palette.grey[500],
|
|
||||||
fontFamily: "Roboto, Helvetica, Arial, sans-serif",
|
|
||||||
fontSize: "0.8rem",
|
|
||||||
marginBottom: "1rem",
|
|
||||||
marginLeft: "0.5rem",
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
const LoginPage = () => {
|
const LoginPage = () => {
|
||||||
const login = useLogin();
|
const login = useLogin();
|
||||||
const notify = useNotify();
|
const notify = useNotify();
|
||||||
const { restrictBaseUrl } = useAppContext();
|
const { restrictBaseUrl } = useAppContext();
|
||||||
const allowSingleBaseUrl = typeof restrictBaseUrl === "string";
|
const allowSingleBaseUrl = typeof restrictBaseUrl === "string";
|
||||||
const allowMultipleBaseUrls = Array.isArray(restrictBaseUrl);
|
const allowMultipleBaseUrls =
|
||||||
|
Array.isArray(restrictBaseUrl) &&
|
||||||
|
restrictBaseUrl.length > 0 &&
|
||||||
|
restrictBaseUrl[0] !== "" &&
|
||||||
|
restrictBaseUrl[0] !== null;
|
||||||
const allowAnyBaseUrl = !(allowSingleBaseUrl || allowMultipleBaseUrls);
|
const allowAnyBaseUrl = !(allowSingleBaseUrl || allowMultipleBaseUrls);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [supportPassAuth, setSupportPassAuth] = useState(true);
|
const [supportPassAuth, setSupportPassAuth] = useState(true);
|
||||||
@@ -98,8 +49,13 @@ const LoginPage = () => {
|
|||||||
const base_url = allowSingleBaseUrl ? restrictBaseUrl : storage.getItem("base_url");
|
const base_url = allowSingleBaseUrl ? restrictBaseUrl : storage.getItem("base_url");
|
||||||
const [ssoBaseUrl, setSSOBaseUrl] = useState("");
|
const [ssoBaseUrl, setSSOBaseUrl] = useState("");
|
||||||
const loginToken = /\?loginToken=([a-zA-Z0-9_-]+)/.exec(window.location.href);
|
const loginToken = /\?loginToken=([a-zA-Z0-9_-]+)/.exec(window.location.href);
|
||||||
|
const [loginMethod, setLoginMethod] = useState<LoginMethod>("credentials");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!loginToken) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (loginToken) {
|
|
||||||
const ssoToken = loginToken[1];
|
const ssoToken = loginToken[1];
|
||||||
console.log("SSO token is", ssoToken);
|
console.log("SSO token is", ssoToken);
|
||||||
// Prevent further requests
|
// Prevent further requests
|
||||||
@@ -127,7 +83,7 @@ const LoginPage = () => {
|
|||||||
console.error(error);
|
console.error(error);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}, [loginToken]);
|
||||||
|
|
||||||
const validateBaseUrl = value => {
|
const validateBaseUrl = value => {
|
||||||
if (!value.match(/^(http|https):\/\//)) {
|
if (!value.match(/^(http|https):\/\//)) {
|
||||||
@@ -213,29 +169,53 @@ const LoginPage = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Box>
|
<Tabs
|
||||||
<TextInput
|
value={loginMethod}
|
||||||
autoFocus
|
onChange={(_, newValue) => setLoginMethod(newValue as LoginMethod)}
|
||||||
source="username"
|
indicatorColor="primary"
|
||||||
label="ra.auth.username"
|
textColor="primary"
|
||||||
autoComplete="username"
|
centered
|
||||||
disabled={loading || !supportPassAuth}
|
>
|
||||||
onBlur={handleUsernameChange}
|
<Tab label={translate("synapseadmin.auth.credentials")} value="credentials" />
|
||||||
resettable
|
<Tab label={translate("synapseadmin.auth.access_token")} value="accessToken" />
|
||||||
validate={required()}
|
</Tabs>
|
||||||
/>
|
{loginMethod === "credentials" ? (
|
||||||
</Box>
|
<>
|
||||||
<Box>
|
<Box>
|
||||||
<PasswordInput
|
<TextInput
|
||||||
source="password"
|
autoFocus
|
||||||
label="ra.auth.password"
|
source="username"
|
||||||
type="password"
|
label="ra.auth.username"
|
||||||
autoComplete="current-password"
|
autoComplete="username"
|
||||||
disabled={loading || !supportPassAuth}
|
disabled={loading || !supportPassAuth}
|
||||||
resettable
|
onBlur={handleUsernameChange}
|
||||||
validate={required()}
|
resettable
|
||||||
/>
|
validate={required()}
|
||||||
</Box>
|
/>
|
||||||
|
</Box>
|
||||||
|
<Box>
|
||||||
|
<PasswordInput
|
||||||
|
source="password"
|
||||||
|
label="ra.auth.password"
|
||||||
|
type="password"
|
||||||
|
autoComplete="current-password"
|
||||||
|
disabled={loading || !supportPassAuth}
|
||||||
|
resettable
|
||||||
|
validate={required()}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<Box>
|
||||||
|
<TextInput
|
||||||
|
source="accessToken"
|
||||||
|
label="synapseadmin.auth.access_token"
|
||||||
|
disabled={loading}
|
||||||
|
resettable
|
||||||
|
validate={required()}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
<Box>
|
<Box>
|
||||||
<TextInput
|
<TextInput
|
||||||
source="base_url"
|
source="base_url"
|
||||||
@@ -263,7 +243,7 @@ const LoginPage = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Form defaultValues={{ base_url: base_url }} onSubmit={handleSubmit} mode="onTouched">
|
<Form defaultValues={{ base_url: base_url }} onSubmit={handleSubmit} mode="onTouched">
|
||||||
<FormBox>
|
<LoginFormBox>
|
||||||
<Card className="card">
|
<Card className="card">
|
||||||
<Box className="avatar">
|
<Box className="avatar">
|
||||||
{loading ? (
|
{loading ? (
|
||||||
@@ -312,7 +292,7 @@ const LoginPage = () => {
|
|||||||
</CardActions>
|
</CardActions>
|
||||||
</Box>
|
</Box>
|
||||||
</Card>
|
</Card>
|
||||||
</FormBox>
|
</LoginFormBox>
|
||||||
<Notification />
|
<Notification />
|
||||||
</Form>
|
</Form>
|
||||||
);
|
);
|
||||||
|
@@ -4,6 +4,7 @@ import AutorenewIcon from "@mui/icons-material/Autorenew";
|
|||||||
import DestinationsIcon from "@mui/icons-material/CloudQueue";
|
import DestinationsIcon from "@mui/icons-material/CloudQueue";
|
||||||
import FolderSharedIcon from "@mui/icons-material/FolderShared";
|
import FolderSharedIcon from "@mui/icons-material/FolderShared";
|
||||||
import ViewListIcon from "@mui/icons-material/ViewList";
|
import ViewListIcon from "@mui/icons-material/ViewList";
|
||||||
|
import ErrorIcon from '@mui/icons-material/Error';
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Datagrid,
|
Datagrid,
|
||||||
@@ -21,6 +22,7 @@ import {
|
|||||||
Tab,
|
Tab,
|
||||||
TabbedShowLayout,
|
TabbedShowLayout,
|
||||||
TextField,
|
TextField,
|
||||||
|
FunctionField,
|
||||||
TopToolbar,
|
TopToolbar,
|
||||||
useRecordContext,
|
useRecordContext,
|
||||||
useDelete,
|
useDelete,
|
||||||
@@ -35,13 +37,6 @@ import { get } from "lodash";
|
|||||||
|
|
||||||
const DestinationPagination = () => <Pagination rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} />;
|
const DestinationPagination = () => <Pagination rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} />;
|
||||||
|
|
||||||
const destinationRowSx = (record: RaRecord) => ({
|
|
||||||
backgroundColor: record.retry_last_ts > 0 ? "warning.light" : "primary.contrastText",
|
|
||||||
"& .MuiButtonBase-root": {
|
|
||||||
color: "primary.dark",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const destinationFilters = [<SearchInput source="destination" alwaysOn />];
|
const destinationFilters = [<SearchInput source="destination" alwaysOn />];
|
||||||
|
|
||||||
export const DestinationReconnectButton = () => {
|
export const DestinationReconnectButton = () => {
|
||||||
@@ -105,7 +100,22 @@ const RetryDateField = (props: DateFieldProps) => {
|
|||||||
return <DateField {...props} />;
|
return <DateField {...props} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const destinationFieldRender = (record: RaRecord) => {
|
||||||
|
if (record.retry_last_ts > 0) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ErrorIcon fontSize="inherit" color="error" sx={{verticalAlign: "middle"}}/>
|
||||||
|
|
||||||
|
{record.destination}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return <> {record.destination} </>;
|
||||||
|
}
|
||||||
|
|
||||||
export const DestinationList = (props: ListProps) => {
|
export const DestinationList = (props: ListProps) => {
|
||||||
|
const record = useRecordContext(props);
|
||||||
return (
|
return (
|
||||||
<List
|
<List
|
||||||
{...props}
|
{...props}
|
||||||
@@ -113,8 +123,8 @@ export const DestinationList = (props: ListProps) => {
|
|||||||
pagination={<DestinationPagination />}
|
pagination={<DestinationPagination />}
|
||||||
sort={{ field: "destination", order: "ASC" }}
|
sort={{ field: "destination", order: "ASC" }}
|
||||||
>
|
>
|
||||||
<Datagrid rowSx={destinationRowSx} rowClick={id => `${id}/show/rooms`} bulkActionButtons={false}>
|
<Datagrid rowClick={id => `${id}/show/rooms`} bulkActionButtons={false}>
|
||||||
<TextField source="destination" />
|
<FunctionField source="destination" render={destinationFieldRender} />
|
||||||
<DateField source="failure_ts" showTime options={DATE_FORMAT} />
|
<DateField source="failure_ts" showTime options={DATE_FORMAT} />
|
||||||
<RetryDateField source="retry_last_ts" showTime options={DATE_FORMAT} />
|
<RetryDateField source="retry_last_ts" showTime options={DATE_FORMAT} />
|
||||||
<TextField source="retry_interval" />
|
<TextField source="retry_interval" />
|
||||||
|
@@ -22,7 +22,7 @@ import {
|
|||||||
} from "react-admin";
|
} from "react-admin";
|
||||||
|
|
||||||
import { DATE_FORMAT } from "../components/date";
|
import { DATE_FORMAT } from "../components/date";
|
||||||
import { MXCField } from "../components/media";
|
import { ReportMediaContent } from "../components/media";
|
||||||
|
|
||||||
const ReportPagination = () => <Pagination rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} />;
|
const ReportPagination = () => <Pagination rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} />;
|
||||||
|
|
||||||
@@ -62,7 +62,7 @@ export const ReportShow = (props: ShowProps) => {
|
|||||||
<TextField source="event_json.content.msgtype" />
|
<TextField source="event_json.content.msgtype" />
|
||||||
<TextField source="event_json.content.body" />
|
<TextField source="event_json.content.body" />
|
||||||
<TextField source="event_json.content.info.mimetype" />
|
<TextField source="event_json.content.info.mimetype" />
|
||||||
<MXCField source="event_json.content.url" />
|
<ReportMediaContent source="event_json.content.url" />
|
||||||
<TextField source="event_json.content.format" />
|
<TextField source="event_json.content.format" />
|
||||||
<TextField source="event_json.content.formatted_body" />
|
<TextField source="event_json.content.formatted_body" />
|
||||||
<TextField source="event_json.content.algorithm" />
|
<TextField source="event_json.content.algorithm" />
|
||||||
|
@@ -26,9 +26,9 @@ import {
|
|||||||
useUnselectAll,
|
useUnselectAll,
|
||||||
} from "react-admin";
|
} from "react-admin";
|
||||||
import { useMutation } from "@tanstack/react-query";
|
import { useMutation } from "@tanstack/react-query";
|
||||||
|
|
||||||
import AvatarField from "../components/AvatarField";
|
import AvatarField from "../components/AvatarField";
|
||||||
|
|
||||||
|
|
||||||
const RoomDirectoryPagination = () => <Pagination rowsPerPageOptions={[100, 500, 1000, 2000]} />;
|
const RoomDirectoryPagination = () => <Pagination rowsPerPageOptions={[100, 500, 1000, 2000]} />;
|
||||||
|
|
||||||
export const RoomDirectoryUnpublishButton = (props: DeleteButtonProps) => {
|
export const RoomDirectoryUnpublishButton = (props: DeleteButtonProps) => {
|
||||||
@@ -144,7 +144,6 @@ export const RoomDirectoryList = () => (
|
|||||||
>
|
>
|
||||||
<AvatarField
|
<AvatarField
|
||||||
source="avatar_src"
|
source="avatar_src"
|
||||||
sortable={false}
|
|
||||||
sx={{ height: "40px", width: "40px" }}
|
sx={{ height: "40px", width: "40px" }}
|
||||||
label="resources.rooms.fields.avatar"
|
label="resources.rooms.fields.avatar"
|
||||||
/>
|
/>
|
||||||
|
@@ -47,6 +47,7 @@ import {
|
|||||||
} from "./room_directory";
|
} from "./room_directory";
|
||||||
import { DATE_FORMAT } from "../components/date";
|
import { DATE_FORMAT } from "../components/date";
|
||||||
import DeleteRoomButton from "../components/DeleteRoomButton";
|
import DeleteRoomButton from "../components/DeleteRoomButton";
|
||||||
|
import AvatarField from "../components/AvatarField";
|
||||||
|
|
||||||
const RoomPagination = () => <Pagination rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} />;
|
const RoomPagination = () => <Pagination rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} />;
|
||||||
|
|
||||||
@@ -90,6 +91,11 @@ export const RoomShow = (props: ShowProps) => {
|
|||||||
<Show {...props} actions={<RoomShowActions />} title={<RoomTitle />}>
|
<Show {...props} actions={<RoomShowActions />} title={<RoomTitle />}>
|
||||||
<TabbedShowLayout>
|
<TabbedShowLayout>
|
||||||
<Tab label="synapseadmin.rooms.tabs.basic" icon={<ViewListIcon />}>
|
<Tab label="synapseadmin.rooms.tabs.basic" icon={<ViewListIcon />}>
|
||||||
|
<AvatarField
|
||||||
|
source="avatar"
|
||||||
|
sx={{ height: "120px", width: "120px" }}
|
||||||
|
label="resources.rooms.fields.avatar"
|
||||||
|
/>
|
||||||
<TextField source="room_id" />
|
<TextField source="room_id" />
|
||||||
<TextField source="name" />
|
<TextField source="name" />
|
||||||
<TextField source="topic" />
|
<TextField source="topic" />
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import EqualizerIcon from "@mui/icons-material/Equalizer";
|
import PermMediaIcon from "@mui/icons-material/PermMedia";
|
||||||
import {
|
import {
|
||||||
Datagrid,
|
Datagrid,
|
||||||
ExportButton,
|
ExportButton,
|
||||||
@@ -48,7 +48,7 @@ export const UserMediaStatsList = (props: ListProps) => (
|
|||||||
|
|
||||||
const resource: ResourceProps = {
|
const resource: ResourceProps = {
|
||||||
name: "user_media_statistics",
|
name: "user_media_statistics",
|
||||||
icon: EqualizerIcon,
|
icon: PermMediaIcon,
|
||||||
list: UserMediaStatsList,
|
list: UserMediaStatsList,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -36,7 +36,6 @@ import {
|
|||||||
ResourceProps,
|
ResourceProps,
|
||||||
SearchInput,
|
SearchInput,
|
||||||
SelectInput,
|
SelectInput,
|
||||||
BulkDeleteButton,
|
|
||||||
DeleteButton,
|
DeleteButton,
|
||||||
maxLength,
|
maxLength,
|
||||||
regex,
|
regex,
|
||||||
@@ -52,15 +51,17 @@ import {
|
|||||||
NumberField,
|
NumberField,
|
||||||
useListContext,
|
useListContext,
|
||||||
useNotify,
|
useNotify,
|
||||||
ToolbarClasses,
|
|
||||||
Identifier,
|
Identifier,
|
||||||
|
ToolbarClasses,
|
||||||
RaRecord,
|
RaRecord,
|
||||||
ImageInput,
|
ImageInput,
|
||||||
ImageField,
|
ImageField,
|
||||||
|
FunctionField,
|
||||||
} from "react-admin";
|
} from "react-admin";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
|
|
||||||
import AvatarField from "../components/AvatarField";
|
import AvatarField from "../components/AvatarField";
|
||||||
|
import DeleteUserButton from "../components/DeleteUserButton";
|
||||||
import { isASManaged } from "../components/mxid";
|
import { isASManaged } from "../components/mxid";
|
||||||
import { ServerNoticeButton, ServerNoticeBulkButton } from "../components/ServerNotices";
|
import { ServerNoticeButton, ServerNoticeBulkButton } from "../components/ServerNotices";
|
||||||
import { DATE_FORMAT } from "../components/date";
|
import { DATE_FORMAT } from "../components/date";
|
||||||
@@ -90,11 +91,6 @@ const UserListActions = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
UserListActions.defaultProps = {
|
|
||||||
selectedIds: [],
|
|
||||||
onUnselectItems: () => null,
|
|
||||||
};
|
|
||||||
|
|
||||||
const UserPagination = () => <Pagination rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} />;
|
const UserPagination = () => <Pagination rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} />;
|
||||||
|
|
||||||
const userFilters = [
|
const userFilters = [
|
||||||
@@ -141,20 +137,16 @@ const UserBulkActionButtons = () => {
|
|||||||
<>
|
<>
|
||||||
<ServerNoticeBulkButton />
|
<ServerNoticeBulkButton />
|
||||||
<UserPreventSelfDelete ownUserIsSelected={ownUserIsSelected} asManagedUserIsSelected={asManagedUserIsSelected}>
|
<UserPreventSelfDelete ownUserIsSelected={ownUserIsSelected} asManagedUserIsSelected={asManagedUserIsSelected}>
|
||||||
<BulkDeleteButton
|
<DeleteUserButton
|
||||||
label="resources.users.action.erase"
|
selectedIds={selectedIds}
|
||||||
confirmTitle="resources.users.helper.erase"
|
confirmTitle="resources.users.helper.erase"
|
||||||
mutationMode="pessimistic"
|
confirmContent="resources.users.helper.erase_text"
|
||||||
/>
|
/>
|
||||||
</UserPreventSelfDelete>
|
</UserPreventSelfDelete>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const usersRowClick = (id: Identifier, resource: string, record: RaRecord): string => {
|
|
||||||
return `/users/${id}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const UserList = (props: ListProps) => (
|
export const UserList = (props: ListProps) => (
|
||||||
<List
|
<List
|
||||||
{...props}
|
{...props}
|
||||||
@@ -164,7 +156,10 @@ export const UserList = (props: ListProps) => (
|
|||||||
actions={<UserListActions />}
|
actions={<UserListActions />}
|
||||||
pagination={<UserPagination />}
|
pagination={<UserPagination />}
|
||||||
>
|
>
|
||||||
<Datagrid rowClick={usersRowClick} bulkActionButtons={<UserBulkActionButtons />}>
|
<Datagrid
|
||||||
|
rowClick={(id: Identifier, resource: string) => `/${resource}/${id}`}
|
||||||
|
bulkActionButtons={<UserBulkActionButtons />}
|
||||||
|
>
|
||||||
<AvatarField source="avatar_src" sx={{ height: "40px", width: "40px" }} sortBy="avatar_url" />
|
<AvatarField source="avatar_src" sx={{ height: "40px", width: "40px" }} sortBy="avatar_url" />
|
||||||
<TextField source="id" sortBy="name" />
|
<TextField source="id" sortBy="name" />
|
||||||
<TextField source="displayname" />
|
<TextField source="displayname" />
|
||||||
@@ -200,15 +195,15 @@ const UserEditActions = () => {
|
|||||||
return (
|
return (
|
||||||
<TopToolbar>
|
<TopToolbar>
|
||||||
{!record?.deactivated && <ServerNoticeButton />}
|
{!record?.deactivated && <ServerNoticeButton />}
|
||||||
|
{record && record.id && (
|
||||||
<UserPreventSelfDelete ownUserIsSelected={ownUserIsSelected} asManagedUserIsSelected={asManagedUserIsSelected}>
|
<UserPreventSelfDelete ownUserIsSelected={ownUserIsSelected} asManagedUserIsSelected={asManagedUserIsSelected}>
|
||||||
<DeleteButton
|
<DeleteUserButton
|
||||||
label="resources.users.action.erase"
|
selectedIds={[record?.id]}
|
||||||
confirmTitle={translate("resources.users.helper.erase", {
|
confirmTitle="resources.users.helper.erase"
|
||||||
smart_count: 1,
|
confirmContent="resources.users.helper.erase_text"
|
||||||
})}
|
|
||||||
mutationMode="pessimistic"
|
|
||||||
/>
|
/>
|
||||||
</UserPreventSelfDelete>
|
</UserPreventSelfDelete>
|
||||||
|
)}
|
||||||
</TopToolbar>
|
</TopToolbar>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -216,9 +211,7 @@ const UserEditActions = () => {
|
|||||||
export const UserCreate = (props: CreateProps) => (
|
export const UserCreate = (props: CreateProps) => (
|
||||||
<Create
|
<Create
|
||||||
{...props}
|
{...props}
|
||||||
redirect={(resource, id, data) => {
|
redirect={(resource: string | undefined, id: Identifier | undefined) => `${resource}/${id}`}
|
||||||
return `users/${id}`;
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<SimpleForm>
|
<SimpleForm>
|
||||||
<TextInput source="id" autoComplete="off" validate={validateUser} />
|
<TextInput source="id" autoComplete="off" validate={validateUser} />
|
||||||
@@ -323,14 +316,19 @@ export const UserEdit = (props: EditProps) => {
|
|||||||
<Edit {...props} title={<UserTitle />} actions={<UserEditActions />} mutationMode="pessimistic">
|
<Edit {...props} title={<UserTitle />} actions={<UserEditActions />} mutationMode="pessimistic">
|
||||||
<TabbedForm toolbar={<UserEditToolbar />}>
|
<TabbedForm toolbar={<UserEditToolbar />}>
|
||||||
<FormTab label={translate("resources.users.name", { smart_count: 1 })} icon={<PersonPinIcon />}>
|
<FormTab label={translate("resources.users.name", { smart_count: 1 })} icon={<PersonPinIcon />}>
|
||||||
<AvatarField source="avatar_src" sortable={false} sx={{ height: "120px", width: "120px" }} />
|
<AvatarField source="avatar_src" sx={{ height: "120px", width: "120px" }} />
|
||||||
<BooleanInput source="avatar_erase" label="resources.users.action.erase_avatar" />
|
<BooleanInput source="avatar_erase" label="resources.users.action.erase_avatar" />
|
||||||
<ImageInput
|
<ImageInput
|
||||||
source="avatar_file"
|
source="avatar_file"
|
||||||
label="resources.users.fields.avatar"
|
label="resources.users.fields.avatar"
|
||||||
accept={{ "image/*": [".png", ".jpg"] }}
|
accept={{ "image/*": [".png", ".jpg"] }}
|
||||||
>
|
>
|
||||||
<ImageField source="src" title="Avatar" />
|
<ImageField source="src" title="Avatar" sx={{ '& img': {
|
||||||
|
width: "120px !important",
|
||||||
|
height: "120px !important",
|
||||||
|
objectFit: "cover !important",
|
||||||
|
borderRadius: '50% !important',
|
||||||
|
}}} />
|
||||||
</ImageInput>
|
</ImageInput>
|
||||||
<TextInput source="id" readOnly />
|
<TextInput source="id" readOnly />
|
||||||
<TextInput source="displayname" />
|
<TextInput source="displayname" />
|
||||||
@@ -404,8 +402,8 @@ export const UserEdit = (props: EditProps) => {
|
|||||||
<DateField source="created_ts" showTime options={DATE_FORMAT} />
|
<DateField source="created_ts" showTime options={DATE_FORMAT} />
|
||||||
<DateField source="last_access_ts" showTime options={DATE_FORMAT} />
|
<DateField source="last_access_ts" showTime options={DATE_FORMAT} />
|
||||||
<NumberField source="media_length" />
|
<NumberField source="media_length" />
|
||||||
<TextField source="media_type" />
|
<TextField source="media_type" sx={{ display: "block", width: 200, wordBreak: "break-word" }} />
|
||||||
<TextField source="upload_name" />
|
<FunctionField source="upload_name" render={record => decodeURIComponent(record.upload_name)} />
|
||||||
<TextField source="quarantined_by" />
|
<TextField source="quarantined_by" />
|
||||||
<QuarantineMediaButton label="resources.quarantine_media.action.name" />
|
<QuarantineMediaButton label="resources.quarantine_media.action.name" />
|
||||||
<ProtectMediaButton label="resources.users_media.fields.safe_from_quarantine" />
|
<ProtectMediaButton label="resources.users_media.fields.safe_from_quarantine" />
|
||||||
|
@@ -23,13 +23,13 @@ describe("authProvider", () => {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
const ret: undefined = await authProvider.login({
|
const ret = await authProvider.login({
|
||||||
base_url: "http://example.com",
|
base_url: "http://example.com",
|
||||||
username: "@user:example.com",
|
username: "@user:example.com",
|
||||||
password: "secret",
|
password: "secret",
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(ret).toBe(undefined);
|
expect(ret).toEqual({redirectTo: "/"});
|
||||||
expect(fetch).toBeCalledWith("http://example.com/_matrix/client/r0/login", {
|
expect(fetch).toBeCalledWith("http://example.com/_matrix/client/r0/login", {
|
||||||
body: '{"device_id":null,"initial_device_display_name":"Synapse Admin","type":"m.login.password","identifier":{"type":"m.id.user","user":"@user:example.com"},"password":"secret"}',
|
body: '{"device_id":null,"initial_device_display_name":"Synapse Admin","type":"m.login.password","identifier":{"type":"m.id.user","user":"@user:example.com"},"password":"secret"}',
|
||||||
headers: new Headers({
|
headers: new Headers({
|
||||||
@@ -55,12 +55,12 @@ describe("authProvider", () => {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
const ret: undefined = await authProvider.login({
|
const ret = await authProvider.login({
|
||||||
base_url: "https://example.com/",
|
base_url: "https://example.com/",
|
||||||
loginToken: "login_token",
|
loginToken: "login_token",
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(ret).toBe(undefined);
|
expect(ret).toEqual({redirectTo: "/"});
|
||||||
expect(fetch).toHaveBeenCalledWith("https://example.com/_matrix/client/r0/login", {
|
expect(fetch).toHaveBeenCalledWith("https://example.com/_matrix/client/r0/login", {
|
||||||
body: '{"device_id":null,"initial_device_display_name":"Synapse Admin","type":"m.login.token","token":"login_token"}',
|
body: '{"device_id":null,"initial_device_display_name":"Synapse Admin","type":"m.login.token","token":"login_token"}',
|
||||||
headers: new Headers({
|
headers: new Headers({
|
||||||
|
@@ -10,14 +10,16 @@ const authProvider: AuthProvider = {
|
|||||||
username,
|
username,
|
||||||
password,
|
password,
|
||||||
loginToken,
|
loginToken,
|
||||||
|
accessToken,
|
||||||
}: {
|
}: {
|
||||||
base_url: string;
|
base_url: string;
|
||||||
username: string;
|
username: string;
|
||||||
password: string;
|
password: string;
|
||||||
loginToken: string;
|
loginToken: string;
|
||||||
|
accessToken: string;
|
||||||
}) => {
|
}) => {
|
||||||
console.log("login ");
|
console.log("login ");
|
||||||
const options: Options = {
|
let options: Options = {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify(
|
body: JSON.stringify(
|
||||||
Object.assign(
|
Object.assign(
|
||||||
@@ -55,11 +57,30 @@ const authProvider: AuthProvider = {
|
|||||||
storage.setItem("base_url", base_url);
|
storage.setItem("base_url", base_url);
|
||||||
|
|
||||||
const decoded_base_url = window.decodeURIComponent(base_url);
|
const decoded_base_url = window.decodeURIComponent(base_url);
|
||||||
const login_api_url = decoded_base_url + "/_matrix/client/r0/login";
|
let login_api_url = decoded_base_url + (accessToken ? "/_matrix/client/v3/account/whoami" : "/_matrix/client/r0/login");
|
||||||
|
|
||||||
let response;
|
let response;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
if (accessToken) {
|
||||||
|
// this a login with an already obtained access token, let's just validate it
|
||||||
|
options = {
|
||||||
|
headers: new Headers({
|
||||||
|
Accept: 'application/json',
|
||||||
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
response = await fetchUtils.fetchJson(login_api_url, options);
|
response = await fetchUtils.fetchJson(login_api_url, options);
|
||||||
|
const json = response.json;
|
||||||
|
storage.setItem("home_server", accessToken ? base_url : json.home_server);
|
||||||
|
storage.setItem("user_id", json.user_id);
|
||||||
|
storage.setItem("access_token", accessToken ? accessToken : json.access_token);
|
||||||
|
storage.setItem("device_id", json.device_id);
|
||||||
|
storage.setItem("login_type", accessToken ? "accessToken" : "credentials");
|
||||||
|
|
||||||
|
return Promise.resolve({redirectTo: "/"});
|
||||||
} catch(err) {
|
} catch(err) {
|
||||||
const error = err as HttpError;
|
const error = err as HttpError;
|
||||||
const errorStatus = error.status;
|
const errorStatus = error.status;
|
||||||
@@ -71,14 +92,8 @@ const authProvider: AuthProvider = {
|
|||||||
errMsg,
|
errMsg,
|
||||||
errorStatus,
|
errorStatus,
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const json = response.json;
|
|
||||||
storage.setItem("home_server", json.home_server);
|
|
||||||
storage.setItem("user_id", json.user_id);
|
|
||||||
storage.setItem("access_token", json.access_token);
|
|
||||||
storage.setItem("device_id", json.device_id);
|
|
||||||
},
|
},
|
||||||
// called when the user clicks on the logout button
|
// called when the user clicks on the logout button
|
||||||
logout: async () => {
|
logout: async () => {
|
||||||
@@ -96,8 +111,14 @@ const authProvider: AuthProvider = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (typeof access_token === "string") {
|
if (typeof access_token === "string") {
|
||||||
await fetchUtils.fetchJson(logout_api_url, options);
|
try {
|
||||||
storage.removeItem("access_token");
|
await fetchUtils.fetchJson(logout_api_url, options);
|
||||||
|
} catch (err) {
|
||||||
|
console.log("Error logging out", err);
|
||||||
|
} finally {
|
||||||
|
storage.removeItem("access_token");
|
||||||
|
storage.removeItem("login_type");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// called when the API returns an error
|
// called when the API returns an error
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
DataProvider,
|
DataProvider,
|
||||||
DeleteParams,
|
DeleteParams,
|
||||||
|
DeleteManyParams,
|
||||||
HttpError,
|
HttpError,
|
||||||
Identifier,
|
Identifier,
|
||||||
Options,
|
Options,
|
||||||
@@ -41,21 +42,16 @@ const jsonClient = async (url: string, options: Options = {}) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const mxcUrlToHttp = (mxcUrl: string) => {
|
|
||||||
const homeserver = storage.getItem("base_url");
|
|
||||||
const re = /^mxc:\/\/([^/]+)\/(\w+)/;
|
|
||||||
const ret = re.exec(mxcUrl);
|
|
||||||
console.log("mxcClient " + ret);
|
|
||||||
if (ret == null) return null;
|
|
||||||
const serverName = ret[1];
|
|
||||||
const mediaId = ret[2];
|
|
||||||
return `${homeserver}/_matrix/media/r0/thumbnail/${serverName}/${mediaId}?width=24&height=24&method=scale`;
|
|
||||||
};
|
|
||||||
|
|
||||||
const filterUndefined = (obj: Record<string, any>) => {
|
const filterUndefined = (obj: Record<string, any>) => {
|
||||||
return Object.fromEntries(Object.entries(obj).filter(([key, value]) => value !== undefined));
|
return Object.fromEntries(Object.entries(obj).filter(([key, value]) => value !== undefined));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
interface Action {
|
||||||
|
endpoint: string;
|
||||||
|
method?: string;
|
||||||
|
body?: Record<string, any>;
|
||||||
|
}
|
||||||
|
|
||||||
interface Room {
|
interface Room {
|
||||||
room_id: string;
|
room_id: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
@@ -260,10 +256,10 @@ export interface SynapseDataProvider extends DataProvider {
|
|||||||
const resourceMap = {
|
const resourceMap = {
|
||||||
users: {
|
users: {
|
||||||
path: "/_synapse/admin/v2/users",
|
path: "/_synapse/admin/v2/users",
|
||||||
map: (u: User) => ({
|
map: async (u: User) => ({
|
||||||
...u,
|
...u,
|
||||||
id: returnMXID(u.name),
|
id: returnMXID(u.name),
|
||||||
avatar_src: u.avatar_url ? mxcUrlToHttp(u.avatar_url) : undefined,
|
avatar_src: u.avatar_url ? u.avatar_url : undefined,
|
||||||
is_guest: !!u.is_guest,
|
is_guest: !!u.is_guest,
|
||||||
admin: !!u.admin,
|
admin: !!u.admin,
|
||||||
deactivated: !!u.deactivated,
|
deactivated: !!u.deactivated,
|
||||||
@@ -451,7 +447,7 @@ const resourceMap = {
|
|||||||
id: rd.room_id,
|
id: rd.room_id,
|
||||||
public: !!rd.public,
|
public: !!rd.public,
|
||||||
guest_access: !!rd.guest_access,
|
guest_access: !!rd.guest_access,
|
||||||
avatar_src: rd.avatar_url ? mxcUrlToHttp(rd.avatar_url) : undefined,
|
avatar_src: rd.avatar_url ? rd.avatar_url : undefined,
|
||||||
}),
|
}),
|
||||||
data: "chunk",
|
data: "chunk",
|
||||||
total: json => json.total_room_count_estimate,
|
total: json => json.total_room_count_estimate,
|
||||||
@@ -557,8 +553,10 @@ const baseDataProvider: SynapseDataProvider = {
|
|||||||
const url = `${endpoint_url}?${new URLSearchParams(filterUndefined(query)).toString()}`;
|
const url = `${endpoint_url}?${new URLSearchParams(filterUndefined(query)).toString()}`;
|
||||||
|
|
||||||
const { json } = await jsonClient(url);
|
const { json } = await jsonClient(url);
|
||||||
|
const formattedData = await json[res.data].map(res.map);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
data: json[res.data].map(res.map),
|
data: formattedData,
|
||||||
total: res.total(json, from, perPage),
|
total: res.total(json, from, perPage),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
@@ -714,7 +712,7 @@ const baseDataProvider: SynapseDataProvider = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
deleteMany: async (resource, params) => {
|
deleteMany: async (resource, params) => {
|
||||||
console.log("deleteMany " + resource);
|
console.log("deleteMany " + resource, "params", params);
|
||||||
const homeserver = storage.getItem("base_url");
|
const homeserver = storage.getItem("base_url");
|
||||||
if (!homeserver || !(resource in resourceMap)) throw Error("Homeserver not set");
|
if (!homeserver || !(resource in resourceMap)) throw Error("Homeserver not set");
|
||||||
|
|
||||||
@@ -731,6 +729,7 @@ const baseDataProvider: SynapseDataProvider = {
|
|||||||
});
|
});
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
data: responses.map(({ json }) => json),
|
data: responses.map(({ json }) => json),
|
||||||
};
|
};
|
||||||
@@ -808,6 +807,39 @@ const dataProvider = withLifecycleCallbacks(baseDataProvider, [
|
|||||||
}
|
}
|
||||||
return params;
|
return params;
|
||||||
},
|
},
|
||||||
|
beforeDelete: async (params: DeleteParams<any>, dataProvider: DataProvider) => {
|
||||||
|
if (params.meta?.deleteMedia) {
|
||||||
|
const base_url = storage.getItem("base_url");
|
||||||
|
const endpoint_url = `${base_url}/_synapse/admin/v1/users/${encodeURIComponent(returnMXID(params.id))}/media`;
|
||||||
|
await jsonClient(endpoint_url, { method: "DELETE" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.meta?.redactEvents) {
|
||||||
|
const base_url = storage.getItem("base_url");
|
||||||
|
const endpoint_url = `${base_url}/_synapse/admin/v1/user/${encodeURIComponent(returnMXID(params.id))}/redact`;
|
||||||
|
await jsonClient(endpoint_url, { method: "POST", body: JSON.stringify({ rooms: [] }) });
|
||||||
|
}
|
||||||
|
|
||||||
|
return params;
|
||||||
|
},
|
||||||
|
beforeDeleteMany: async (params: DeleteManyParams<any>, dataProvider: DataProvider) => {
|
||||||
|
await Promise.all(
|
||||||
|
params.ids.map(async id => {
|
||||||
|
if (params.meta?.deleteMedia) {
|
||||||
|
const base_url = storage.getItem("base_url");
|
||||||
|
const endpoint_url = `${base_url}/_synapse/admin/v1/users/${encodeURIComponent(returnMXID(id))}/media`;
|
||||||
|
await jsonClient(endpoint_url, { method: "DELETE" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.meta?.redactEvents) {
|
||||||
|
const base_url = storage.getItem("base_url");
|
||||||
|
const endpoint_url = `${base_url}/_synapse/admin/v1/user/${encodeURIComponent(returnMXID(id))}/redact`;
|
||||||
|
await jsonClient(endpoint_url, { method: "POST", body: JSON.stringify({ rooms: [] }) });
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
return params;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
@@ -54,11 +54,6 @@ export const getSupportedLoginFlows = async baseUrl => {
|
|||||||
return response.json.flows;
|
return response.json.flows;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getMediaUrl = media_id => {
|
|
||||||
const baseUrl = storage.getItem("base_url");
|
|
||||||
return `${baseUrl}/_matrix/media/v1/download/${media_id}?allow_redirect=true`;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate a random MXID for current homeserver
|
* Generate a random MXID for current homeserver
|
||||||
* @returns full MXID as string
|
* @returns full MXID as string
|
||||||
|
43
src/utils/fetchMedia.ts
Normal file
43
src/utils/fetchMedia.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import storage from "../storage";
|
||||||
|
|
||||||
|
export const getServerAndMediaIdFromMxcUrl = (mxcUrl: string): { serverName: string, mediaId: string } => {
|
||||||
|
const re = /^mxc:\/\/([^/]+)\/(\w+)/;
|
||||||
|
const ret = re.exec(mxcUrl);
|
||||||
|
console.log("mxcClient " + ret);
|
||||||
|
if (ret == null) {
|
||||||
|
throw new Error("Invalid mxcUrl");
|
||||||
|
}
|
||||||
|
const serverName = ret[1];
|
||||||
|
const mediaId = ret[2];
|
||||||
|
return { serverName, mediaId };
|
||||||
|
};
|
||||||
|
|
||||||
|
export type MediaType = "thumbnail" | "original";
|
||||||
|
|
||||||
|
export const fetchAuthenticatedMedia = async (mxcUrl: string, type: MediaType): Promise<Response> => {
|
||||||
|
const homeserver = storage.getItem("base_url");
|
||||||
|
const accessToken = storage.getItem("access_token");
|
||||||
|
|
||||||
|
const { serverName, mediaId } = getServerAndMediaIdFromMxcUrl(mxcUrl);
|
||||||
|
if (!serverName || !mediaId) {
|
||||||
|
throw new Error("Invalid mxcUrl");
|
||||||
|
}
|
||||||
|
|
||||||
|
let url = "";
|
||||||
|
if (type === "thumbnail") {
|
||||||
|
// ref: https://spec.matrix.org/latest/client-server-api/#thumbnails
|
||||||
|
url = `${homeserver}/_matrix/client/v1/media/thumbnail/${serverName}/${mediaId}?width=320&height=240&method=scale`;
|
||||||
|
} else if (type === "original") {
|
||||||
|
url = `${homeserver}/_matrix/client/v1/media/download/${serverName}/${mediaId}`;
|
||||||
|
} else {
|
||||||
|
throw new Error("Invalid authenticated media type");
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(`${url}`, {
|
||||||
|
headers: {
|
||||||
|
authorization: `Bearer ${accessToken}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return response;
|
||||||
|
};
|
Reference in New Issue
Block a user