mirror of
https://github.com/Awesome-Technologies/synapse-admin.git
synced 2025-09-03 00:08:22 +02:00
Compare commits
33 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
e15411a04c | ||
|
3902dcd3d1 | ||
|
ef8ae9b38f | ||
|
7a286ad506 | ||
|
fa3f2437a3 | ||
|
8dc5238fcb | ||
|
238350b940 | ||
|
99bf7b1889 | ||
|
d72c91644d | ||
|
e8e28b5df1 | ||
|
d5c10b6e02 | ||
|
3085b9ffa0 | ||
|
b2a3fb0f87 | ||
|
1e8b4cc885 | ||
|
4d1a9cc147 | ||
|
1b8b702270 | ||
|
61c32fb473 | ||
|
ad876bb790 | ||
|
2524848dae | ||
|
669c1f3079 | ||
|
590f673167 | ||
|
307793f000 | ||
|
96f549fe42 | ||
|
3de4332477 | ||
|
9fc005032c | ||
|
dbcb4f92dc | ||
|
035baa786a | ||
|
31fe23d688 | ||
|
d3e623e578 | ||
|
a38bc442cb | ||
|
f88eacee2a | ||
|
77cc936710 | ||
|
eb626a7e9e |
3
.github/workflows/build-test.yml
vendored
3
.github/workflows/build-test.yml
vendored
@ -5,6 +5,9 @@ on:
|
|||||||
branches: ["master"]
|
branches: ["master"]
|
||||||
pull_request:
|
pull_request:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
check:
|
check:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
4
.github/workflows/edge_ghpage.yml
vendored
4
.github/workflows/edge_ghpage.yml
vendored
@ -5,6 +5,8 @@ on:
|
|||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
- master
|
- master
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
jobs:
|
jobs:
|
||||||
build-and-deploy:
|
build-and-deploy:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@ -23,7 +25,7 @@ jobs:
|
|||||||
yarn build --base=/synapse-admin
|
yarn build --base=/synapse-admin
|
||||||
|
|
||||||
- name: Deploy 🚀
|
- name: Deploy 🚀
|
||||||
uses: JamesIves/github-pages-deploy-action@v4.6.3
|
uses: JamesIves/github-pages-deploy-action@v4.7.3
|
||||||
with:
|
with:
|
||||||
branch: gh-pages
|
branch: gh-pages
|
||||||
folder: dist
|
folder: dist
|
||||||
|
2
.github/workflows/github-release.yml
vendored
2
.github/workflows/github-release.yml
vendored
@ -23,7 +23,7 @@ jobs:
|
|||||||
version=`git describe --dirty --tags || echo unknown`
|
version=`git describe --dirty --tags || echo unknown`
|
||||||
cp -r dist synapse-admin-$version
|
cp -r dist synapse-admin-$version
|
||||||
tar chvzf dist/synapse-admin-$version.tar.gz synapse-admin-$version
|
tar chvzf dist/synapse-admin-$version.tar.gz synapse-admin-$version
|
||||||
- uses: softprops/action-gh-release@a74c6b72af54cfa997e81df42d94703d6313a2d0
|
- uses: softprops/action-gh-release@da05d552573ad5aba039eaac05058a918a7bf631
|
||||||
with:
|
with:
|
||||||
files: dist/*.tar.gz
|
files: dist/*.tar.gz
|
||||||
env:
|
env:
|
||||||
|
893
.yarn/releases/yarn-4.1.1.cjs
vendored
893
.yarn/releases/yarn-4.1.1.cjs
vendored
File diff suppressed because one or more lines are too long
925
.yarn/releases/yarn-4.4.1.cjs
vendored
Executable file
925
.yarn/releases/yarn-4.4.1.cjs
vendored
Executable file
File diff suppressed because one or more lines are too long
@ -1 +1 @@
|
|||||||
yarnPath: .yarn/releases/yarn-4.1.1.cjs
|
yarnPath: .yarn/releases/yarn-4.4.1.cjs
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
# Builder
|
# Builder
|
||||||
FROM node:lts as builder
|
FROM node:lts AS builder
|
||||||
LABEL org.opencontainers.image.url=https://github.com/Awesome-Technologies/synapse-admin org.opencontainers.image.source=https://github.com/Awesome-Technologies/synapse-admin
|
LABEL org.opencontainers.image.url=https://github.com/Awesome-Technologies/synapse-admin org.opencontainers.image.source=https://github.com/Awesome-Technologies/synapse-admin
|
||||||
# Base path for synapse admin
|
# Base path for synapse admin
|
||||||
ARG BASE_PATH=./
|
ARG BASE_PATH=./
|
||||||
|
@ -2,7 +2,7 @@ import type { JestConfigWithTsJest } from "ts-jest";
|
|||||||
|
|
||||||
const config: JestConfigWithTsJest = {
|
const config: JestConfigWithTsJest = {
|
||||||
preset: "ts-jest",
|
preset: "ts-jest",
|
||||||
testEnvironment: "jsdom",
|
testEnvironment: "jest-fixed-jsdom",
|
||||||
collectCoverage: true,
|
collectCoverage: true,
|
||||||
coveragePathIgnorePatterns: ["node_modules", "dist"],
|
coveragePathIgnorePatterns: ["node_modules", "dist"],
|
||||||
coverageDirectory: "<rootDir>/coverage/",
|
coverageDirectory: "<rootDir>/coverage/",
|
||||||
|
70
package.json
70
package.json
@ -10,65 +10,69 @@
|
|||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/Awesome-Technologies/synapse-admin"
|
"url": "https://github.com/Awesome-Technologies/synapse-admin"
|
||||||
},
|
},
|
||||||
"packageManager": "yarn@4.1.1",
|
"packageManager": "yarn@4.4.1",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.6.0",
|
"@eslint/js": "^9.7.0",
|
||||||
"@testing-library/dom": "^10.0.0",
|
"@mui/system": "^7.1.0",
|
||||||
|
"@mui/utils": "^7.1.0",
|
||||||
|
"@testing-library/dom": "^10.4.0",
|
||||||
"@testing-library/jest-dom": "^6.0.0",
|
"@testing-library/jest-dom": "^6.0.0",
|
||||||
"@testing-library/react": "^15.0.2",
|
"@testing-library/react": "^16.0.0",
|
||||||
"@testing-library/user-event": "^14.5.2",
|
"@testing-library/user-event": "^14.5.2",
|
||||||
"@types/jest": "^29.5.12",
|
"@types/jest": "^29.5.12",
|
||||||
"@types/lodash": "^4.17.0",
|
"@types/lodash": "^4.17.7",
|
||||||
"@types/node": "^20.14.10",
|
"@types/node": "^20.14.12",
|
||||||
"@types/papaparse": "^5.3.14",
|
"@types/papaparse": "^5.3.14",
|
||||||
"@types/react": "^18.3.1",
|
"@types/react": "^18.3.3",
|
||||||
"@typescript-eslint/eslint-plugin": "^7.7.1",
|
"@typescript-eslint/eslint-plugin": "^7.16.1",
|
||||||
"@typescript-eslint/parser": "^7.15.0",
|
"@typescript-eslint/parser": "^7.16.1",
|
||||||
"@vitejs/plugin-react": "^4.0.0",
|
"@vitejs/plugin-react": "^4.0.0",
|
||||||
"eslint": "^8.57.0",
|
"eslint": "^8.57.0",
|
||||||
"eslint-config-prettier": "^9.1.0",
|
"eslint-config-prettier": "^9.1.0",
|
||||||
"eslint-plugin-import": "^2.29.1",
|
"eslint-plugin-import": "^2.29.1",
|
||||||
"eslint-plugin-jsx-a11y": "^6.9.0",
|
"eslint-plugin-jsx-a11y": "^6.9.0",
|
||||||
"eslint-plugin-prettier": "^5.1.3",
|
"eslint-plugin-prettier": "^5.2.1",
|
||||||
"eslint-plugin-unused-imports": "^3.2.0",
|
"eslint-plugin-unused-imports": "^4.1.4",
|
||||||
"eslint-plugin-yaml": "^0.5.0",
|
"eslint-plugin-yaml": "^1.0.3",
|
||||||
"jest": "^29.7.0",
|
"jest": "^29.7.0",
|
||||||
"jest-environment-jsdom": "^29.7.0",
|
"jest-environment-jsdom": "^29.7.0",
|
||||||
"jest-fetch-mock": "^3.0.3",
|
"jest-fetch-mock": "^3.0.3",
|
||||||
"prettier": "^3.2.5",
|
"jest-fixed-jsdom": "^0.0.9",
|
||||||
|
"prettier": "^3.3.3",
|
||||||
"react-test-renderer": "^18.3.1",
|
"react-test-renderer": "^18.3.1",
|
||||||
"ts-jest": "^29.2.0",
|
"ts-jest": "^29.3.4",
|
||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
"typescript": "^5.4.5",
|
"typescript": "^5.4.5",
|
||||||
"typescript-eslint": "^7.8.0",
|
"typescript-eslint": "^8.32.1",
|
||||||
"vite": "^5.2.12",
|
"vite": "^6.3.5",
|
||||||
"vite-plugin-version-mark": "^0.0.13"
|
"vite-plugin-version-mark": "^0.1.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emotion/react": "^11.4.1",
|
"@emotion/react": "^11.14.0",
|
||||||
"@emotion/styled": "^11.3.0",
|
"@emotion/styled": "^11.14.0",
|
||||||
"@haleos/ra-language-german": "^1.0.0",
|
"@haleos/ra-language-german": "^1.0.0",
|
||||||
"@haxqer/ra-language-chinese": "^4.16.2",
|
"@haxqer/ra-language-chinese": "^4.16.2",
|
||||||
"@mui/icons-material": "^5.15.16",
|
"@mui/icons-material": "^7.1.0",
|
||||||
"@mui/material": "^5.16.0",
|
"@mui/material": "^7.1.0",
|
||||||
"history": "^5.1.0",
|
"@tanstack/react-query": "^5.59.12",
|
||||||
|
"history": "^5.3.0",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"papaparse": "^5.4.1",
|
"papaparse": "^5.4.1",
|
||||||
"query-string": "^7.1.1",
|
"query-string": "^7.1.3",
|
||||||
"ra-core": "^4.16.17",
|
"ra-core": "^5.8.3",
|
||||||
"ra-i18n-polyglot": "^4.16.17",
|
"ra-i18n-polyglot": "^5.8.3",
|
||||||
"ra-language-english": "^4.16.17",
|
"ra-language-english": "^5.8.3",
|
||||||
"ra-language-farsi": "^4.2.0",
|
"ra-language-farsi": "^5.0.0",
|
||||||
"ra-language-french": "^4.16.17",
|
"ra-language-french": "^5.8.3",
|
||||||
"ra-language-italian": "^3.13.1",
|
"ra-language-italian": "^3.13.1",
|
||||||
|
"ra-language-russian": "^4.14.2",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-admin": "^4.16.17",
|
"react-admin": "^5.8.3",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-hook-form": "^7.52.1",
|
"react-hook-form": "^7.52.1",
|
||||||
"react-is": "^18.3.1",
|
"react-is": "^18.3.1",
|
||||||
"react-query": "^3.32.1",
|
"react-router": "^7.6.1",
|
||||||
"react-router": "^6.23.0",
|
"react-router-dom": "^7.6.1"
|
||||||
"react-router-dom": "^6.23.0"
|
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "vite serve",
|
"start": "vite serve",
|
||||||
@ -94,7 +98,7 @@
|
|||||||
"plugin:@typescript-eslint/recommended",
|
"plugin:@typescript-eslint/recommended",
|
||||||
"plugin:@typescript-eslint/stylistic",
|
"plugin:@typescript-eslint/stylistic",
|
||||||
"plugin:import/typescript",
|
"plugin:import/typescript",
|
||||||
"plugin:yaml/recommended"
|
"plugin:yaml/legacy"
|
||||||
],
|
],
|
||||||
"parser": "@typescript-eslint/parser",
|
"parser": "@typescript-eslint/parser",
|
||||||
"parserOptions": {
|
"parserOptions": {
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
import { render, screen } from "@testing-library/react";
|
import { render, screen } from "@testing-library/react";
|
||||||
|
import fetchMock from "jest-fetch-mock";
|
||||||
|
fetchMock.enableMocks();
|
||||||
|
|
||||||
import App from "./App";
|
import App from "./App";
|
||||||
|
|
||||||
|
67
src/App.tsx
67
src/App.tsx
@ -9,6 +9,7 @@ import germanMessages from "./i18n/de";
|
|||||||
import englishMessages from "./i18n/en";
|
import englishMessages from "./i18n/en";
|
||||||
import frenchMessages from "./i18n/fr";
|
import frenchMessages from "./i18n/fr";
|
||||||
import italianMessages from "./i18n/it";
|
import italianMessages from "./i18n/it";
|
||||||
|
import russianMessages from "./i18n/ru";
|
||||||
import chineseMessages from "./i18n/zh";
|
import chineseMessages from "./i18n/zh";
|
||||||
import LoginPage from "./pages/LoginPage";
|
import LoginPage from "./pages/LoginPage";
|
||||||
import destinations from "./resources/destinations";
|
import destinations from "./resources/destinations";
|
||||||
@ -20,6 +21,7 @@ import userMediaStats from "./resources/user_media_statistics";
|
|||||||
import users from "./resources/users";
|
import users from "./resources/users";
|
||||||
import authProvider from "./synapse/authProvider";
|
import authProvider from "./synapse/authProvider";
|
||||||
import dataProvider from "./synapse/dataProvider";
|
import dataProvider from "./synapse/dataProvider";
|
||||||
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||||
|
|
||||||
// TODO: Can we use lazy loading together with browser locale?
|
// TODO: Can we use lazy loading together with browser locale?
|
||||||
const messages = {
|
const messages = {
|
||||||
@ -27,6 +29,7 @@ const messages = {
|
|||||||
en: englishMessages,
|
en: englishMessages,
|
||||||
fr: frenchMessages,
|
fr: frenchMessages,
|
||||||
it: italianMessages,
|
it: italianMessages,
|
||||||
|
ru: russianMessages,
|
||||||
zh: chineseMessages,
|
zh: chineseMessages,
|
||||||
};
|
};
|
||||||
const i18nProvider = polyglotI18nProvider(
|
const i18nProvider = polyglotI18nProvider(
|
||||||
@ -38,41 +41,45 @@ const i18nProvider = polyglotI18nProvider(
|
|||||||
{ locale: "fr", name: "Français" },
|
{ locale: "fr", name: "Français" },
|
||||||
{ locale: "it", name: "Italiano" },
|
{ locale: "it", name: "Italiano" },
|
||||||
{ locale: "fa", name: "Persian(فارسی)" },
|
{ locale: "fa", name: "Persian(فارسی)" },
|
||||||
|
{ locale: "ru", name: "Russian(Русский)" },
|
||||||
{ locale: "zh", name: "简体中文" },
|
{ locale: "zh", name: "简体中文" },
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const queryClient = new QueryClient();
|
||||||
|
|
||||||
const App = () => (
|
const App = () => (
|
||||||
<Admin
|
<QueryClientProvider client={queryClient}>
|
||||||
disableTelemetry
|
<Admin
|
||||||
requireAuth
|
disableTelemetry
|
||||||
loginPage={LoginPage}
|
requireAuth
|
||||||
authProvider={authProvider}
|
loginPage={LoginPage}
|
||||||
dataProvider={dataProvider}
|
authProvider={authProvider}
|
||||||
i18nProvider={i18nProvider}
|
dataProvider={dataProvider}
|
||||||
darkTheme={{ palette: { mode: "dark" } }}
|
i18nProvider={i18nProvider}
|
||||||
>
|
>
|
||||||
<CustomRoutes>
|
<CustomRoutes>
|
||||||
<Route path="/import_users" element={<ImportFeature />} />
|
<Route path="/import_users" element={<ImportFeature />} />
|
||||||
</CustomRoutes>
|
</CustomRoutes>
|
||||||
<Resource {...users} />
|
<Resource {...users} />
|
||||||
<Resource {...rooms} />
|
<Resource {...rooms} />
|
||||||
<Resource {...userMediaStats} />
|
<Resource {...userMediaStats} />
|
||||||
<Resource {...reports} />
|
<Resource {...reports} />
|
||||||
<Resource {...roomDirectory} />
|
<Resource {...roomDirectory} />
|
||||||
<Resource {...destinations} />
|
<Resource {...destinations} />
|
||||||
<Resource {...registrationToken} />
|
<Resource {...registrationToken} />
|
||||||
<Resource name="connections" />
|
<Resource name="connections" />
|
||||||
<Resource name="devices" />
|
<Resource name="devices" />
|
||||||
<Resource name="room_members" />
|
<Resource name="room_members" />
|
||||||
<Resource name="users_media" />
|
<Resource name="users_media" />
|
||||||
<Resource name="joined_rooms" />
|
<Resource name="joined_rooms" />
|
||||||
<Resource name="pushers" />
|
<Resource name="pushers" />
|
||||||
<Resource name="servernotices" />
|
<Resource name="servernotices" />
|
||||||
<Resource name="forward_extremities" />
|
<Resource name="forward_extremities" />
|
||||||
<Resource name="room_state" />
|
<Resource name="room_state" />
|
||||||
<Resource name="destination_rooms" />
|
<Resource name="destination_rooms" />
|
||||||
</Admin>
|
</Admin>
|
||||||
|
</QueryClientProvider>
|
||||||
);
|
);
|
||||||
|
|
||||||
export default App;
|
export default App;
|
||||||
|
@ -121,7 +121,7 @@ const FilePicker = () => {
|
|||||||
|
|
||||||
const verifyCsv = ({ data, meta, errors }: ParseResult<ImportLine>, { setValues, setStats, setError }) => {
|
const verifyCsv = ({ data, meta, errors }: ParseResult<ImportLine>, { setValues, setStats, setError }) => {
|
||||||
/* First, verify the presence of required fields */
|
/* First, verify the presence of required fields */
|
||||||
const missingFields = expectedFields.filter(eF => meta.fields?.find(mF => eF === mF));
|
const missingFields = expectedFields.filter(eF => !meta.fields?.includes(eF));
|
||||||
|
|
||||||
if (missingFields.length > 0) {
|
if (missingFields.length > 0) {
|
||||||
setError(translate("import_users.error.required_field", { field: missingFields[0] }));
|
setError(translate("import_users.error.required_field", { field: missingFields[0] }));
|
||||||
|
@ -20,7 +20,7 @@ import {
|
|||||||
useTranslate,
|
useTranslate,
|
||||||
useUnselectAll,
|
useUnselectAll,
|
||||||
} from "react-admin";
|
} from "react-admin";
|
||||||
import { useMutation } from "react-query";
|
import { useMutation } from "@tanstack/react-query";
|
||||||
|
|
||||||
const ServerNoticeDialog = ({ open, onClose, onSubmit }) => {
|
const ServerNoticeDialog = ({ open, onClose, onSubmit }) => {
|
||||||
const translate = useTranslate();
|
const translate = useTranslate();
|
||||||
@ -43,7 +43,6 @@ const ServerNoticeDialog = ({ open, onClose, onSubmit }) => {
|
|||||||
<TextInput
|
<TextInput
|
||||||
source="body"
|
source="body"
|
||||||
label="resources.servernotices.fields.body"
|
label="resources.servernotices.fields.body"
|
||||||
fullWidth
|
|
||||||
multiline
|
multiline
|
||||||
rows="4"
|
rows="4"
|
||||||
resettable
|
resettable
|
||||||
@ -64,6 +63,10 @@ export const ServerNoticeButton = () => {
|
|||||||
const handleDialogOpen = () => setOpen(true);
|
const handleDialogOpen = () => setOpen(true);
|
||||||
const handleDialogClose = () => setOpen(false);
|
const handleDialogClose = () => setOpen(false);
|
||||||
|
|
||||||
|
if (!record) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const handleSend = (values: Partial<RaRecord>) => {
|
const handleSend = (values: Partial<RaRecord>) => {
|
||||||
create(
|
create(
|
||||||
"servernotices",
|
"servernotices",
|
||||||
@ -100,28 +103,26 @@ export const ServerNoticeBulkButton = () => {
|
|||||||
const unselectAllUsers = useUnselectAll("users");
|
const unselectAllUsers = useUnselectAll("users");
|
||||||
const dataProvider = useDataProvider();
|
const dataProvider = useDataProvider();
|
||||||
|
|
||||||
const { mutate: sendNotices, isLoading } = useMutation(
|
const { mutate: sendNotices, isPending } = useMutation({
|
||||||
data =>
|
mutationFn: (data) =>
|
||||||
dataProvider.createMany("servernotices", {
|
dataProvider.createMany("servernotices", {
|
||||||
ids: selectedIds,
|
ids: selectedIds,
|
||||||
data: data,
|
data: data,
|
||||||
}),
|
}),
|
||||||
{
|
onSuccess: () => {
|
||||||
onSuccess: () => {
|
notify("resources.servernotices.action.send_success");
|
||||||
notify("resources.servernotices.action.send_success");
|
unselectAllUsers();
|
||||||
unselectAllUsers();
|
closeDialog();
|
||||||
closeDialog();
|
},
|
||||||
},
|
onError: () =>
|
||||||
onError: () =>
|
notify("resources.servernotices.action.send_failure", {
|
||||||
notify("resources.servernotices.action.send_failure", {
|
type: "error",
|
||||||
type: "error",
|
}),
|
||||||
}),
|
});
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Button label="resources.servernotices.send" onClick={openDialog} disabled={isLoading}>
|
<Button label="resources.servernotices.send" onClick={openDialog} disabled={isPending}>
|
||||||
<MessageIcon />
|
<MessageIcon />
|
||||||
</Button>
|
</Button>
|
||||||
<ServerNoticeDialog open={open} onClose={closeDialog} onSubmit={sendNotices} />
|
<ServerNoticeDialog open={open} onClose={closeDialog} onSubmit={sendNotices} />
|
||||||
|
@ -28,12 +28,13 @@ import {
|
|||||||
useRefresh,
|
useRefresh,
|
||||||
useTranslate,
|
useTranslate,
|
||||||
} from "react-admin";
|
} from "react-admin";
|
||||||
import { useMutation } from "react-query";
|
import { useMutation } from "@tanstack/react-query";
|
||||||
import { Link } from "react-router-dom";
|
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 { getMediaUrl } from "../synapse/synapse";
|
||||||
|
import storage from "../storage";
|
||||||
|
|
||||||
const DeleteMediaDialog = ({ open, onClose, onSubmit }) => {
|
const DeleteMediaDialog = ({ open, onClose, onSubmit }) => {
|
||||||
const translate = useTranslate();
|
const translate = useTranslate();
|
||||||
@ -54,14 +55,12 @@ const DeleteMediaDialog = ({ open, onClose, onSubmit }) => {
|
|||||||
<DialogContentText>{translate("delete_media.helper.send")}</DialogContentText>
|
<DialogContentText>{translate("delete_media.helper.send")}</DialogContentText>
|
||||||
<SimpleForm toolbar={<DeleteMediaToolbar />} onSubmit={onSubmit}>
|
<SimpleForm toolbar={<DeleteMediaToolbar />} onSubmit={onSubmit}>
|
||||||
<DateTimeInput
|
<DateTimeInput
|
||||||
fullWidth
|
|
||||||
source="before_ts"
|
source="before_ts"
|
||||||
label="delete_media.fields.before_ts"
|
label="delete_media.fields.before_ts"
|
||||||
defaultValue={0}
|
defaultValue={0}
|
||||||
parse={dateParser}
|
parse={dateParser}
|
||||||
/>
|
/>
|
||||||
<NumberInput
|
<NumberInput
|
||||||
fullWidth
|
|
||||||
source="size_gt"
|
source="size_gt"
|
||||||
label="delete_media.fields.size_gt"
|
label="delete_media.fields.size_gt"
|
||||||
defaultValue={0}
|
defaultValue={0}
|
||||||
@ -69,7 +68,6 @@ const DeleteMediaDialog = ({ open, onClose, onSubmit }) => {
|
|||||||
step={1024}
|
step={1024}
|
||||||
/>
|
/>
|
||||||
<BooleanInput
|
<BooleanInput
|
||||||
fullWidth
|
|
||||||
source="keep_profiles"
|
source="keep_profiles"
|
||||||
label="delete_media.fields.keep_profiles"
|
label="delete_media.fields.keep_profiles"
|
||||||
defaultValue={true}
|
defaultValue={true}
|
||||||
@ -85,20 +83,18 @@ export const DeleteMediaButton = (props: ButtonProps) => {
|
|||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const notify = useNotify();
|
const notify = useNotify();
|
||||||
const dataProvider = useDataProvider<SynapseDataProvider>();
|
const dataProvider = useDataProvider<SynapseDataProvider>();
|
||||||
const { mutate: deleteMedia, isLoading } = useMutation(
|
const { mutate: deleteMedia, isPending } = useMutation({
|
||||||
(values: DeleteMediaParams) => dataProvider.deleteMedia(values),
|
mutationFn: (values: DeleteMediaParams) => dataProvider.deleteMedia(values),
|
||||||
{
|
onSuccess: () => {
|
||||||
onSuccess: () => {
|
notify("delete_media.action.send_success");
|
||||||
notify("delete_media.action.send_success");
|
closeDialog();
|
||||||
closeDialog();
|
},
|
||||||
},
|
onError: () => {
|
||||||
onError: () => {
|
notify("delete_media.action.send_failure", {
|
||||||
notify("delete_media.action.send_failure", {
|
type: "error",
|
||||||
type: "error",
|
});
|
||||||
});
|
},
|
||||||
},
|
});
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const openDialog = () => setOpen(true);
|
const openDialog = () => setOpen(true);
|
||||||
const closeDialog = () => setOpen(false);
|
const closeDialog = () => setOpen(false);
|
||||||
@ -109,7 +105,7 @@ export const DeleteMediaButton = (props: ButtonProps) => {
|
|||||||
{...props}
|
{...props}
|
||||||
label="delete_media.action.send"
|
label="delete_media.action.send"
|
||||||
onClick={openDialog}
|
onClick={openDialog}
|
||||||
disabled={isLoading}
|
disabled={isPending}
|
||||||
sx={{
|
sx={{
|
||||||
color: theme.palette.error.main,
|
color: theme.palette.error.main,
|
||||||
"&:hover": {
|
"&:hover": {
|
||||||
@ -339,7 +335,7 @@ export const ViewMediaButton = ({ media_id, label }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const MediaIDField = ({ source }) => {
|
export const MediaIDField = ({ source }) => {
|
||||||
const homeserver = localStorage.getItem("home_server");
|
const homeserver = storage.getItem("home_server");
|
||||||
const record = useRecordContext();
|
const record = useRecordContext();
|
||||||
if (!record) return null;
|
if (!record) return null;
|
||||||
|
|
||||||
|
@ -4,6 +4,14 @@ import { SynapseTranslationMessages } from ".";
|
|||||||
|
|
||||||
const de: SynapseTranslationMessages = {
|
const de: SynapseTranslationMessages = {
|
||||||
...formalGermanMessages,
|
...formalGermanMessages,
|
||||||
|
ra: {
|
||||||
|
...formalGermanMessages.ra,
|
||||||
|
navigation: {
|
||||||
|
...formalGermanMessages.ra.navigation,
|
||||||
|
no_filtered_results: "Keine Ergebnisse",
|
||||||
|
clear_filters: "Alle Filter entfernen",
|
||||||
|
},
|
||||||
|
},
|
||||||
synapseadmin: {
|
synapseadmin: {
|
||||||
auth: {
|
auth: {
|
||||||
base_url: "Heimserver URL",
|
base_url: "Heimserver URL",
|
||||||
|
@ -86,7 +86,7 @@ const en: SynapseTranslationMessages = {
|
|||||||
successful: "%{smart_count} entries successfully imported",
|
successful: "%{smart_count} entries successfully imported",
|
||||||
skipped: "%{smart_count} entries skipped",
|
skipped: "%{smart_count} entries skipped",
|
||||||
download_skipped: "Download skipped records",
|
download_skipped: "Download skipped records",
|
||||||
with_error: "%{smart_count} entry with errors ||| %{smart_count} entries with errors",
|
with_error: "%{smart_count} entry with errors |||| %{smart_count} entries with errors",
|
||||||
simulated_only: "Run was only simulated",
|
simulated_only: "Run was only simulated",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -124,6 +124,7 @@ const en: SynapseTranslationMessages = {
|
|||||||
erased: "Erased",
|
erased: "Erased",
|
||||||
guests: "Show guests",
|
guests: "Show guests",
|
||||||
show_deactivated: "Show deactivated users",
|
show_deactivated: "Show deactivated users",
|
||||||
|
show_locked: "Show locked users",
|
||||||
user_id: "Search user",
|
user_id: "Search user",
|
||||||
displayname: "Displayname",
|
displayname: "Displayname",
|
||||||
password: "Password",
|
password: "Password",
|
||||||
|
1
src/i18n/index.d.ts
vendored
1
src/i18n/index.d.ts
vendored
@ -120,6 +120,7 @@ interface SynapseTranslationMessages extends TranslationMessages {
|
|||||||
erased?: string; // TODO: fa, fr, it, zh
|
erased?: string; // TODO: fa, fr, it, zh
|
||||||
guests: string;
|
guests: string;
|
||||||
show_deactivated: string;
|
show_deactivated: string;
|
||||||
|
show_locked?: string; // TODO: de, fa, fr, it, zh
|
||||||
user_id: string;
|
user_id: string;
|
||||||
displayname: string;
|
displayname: string;
|
||||||
password: string;
|
password: string;
|
||||||
|
414
src/i18n/ru.ts
Normal file
414
src/i18n/ru.ts
Normal file
@ -0,0 +1,414 @@
|
|||||||
|
import russianMessages from "ra-language-russian";
|
||||||
|
|
||||||
|
import { SynapseTranslationMessages } from ".";
|
||||||
|
|
||||||
|
const ru: SynapseTranslationMessages = {
|
||||||
|
...russianMessages,
|
||||||
|
ra: {
|
||||||
|
...russianMessages.ra,
|
||||||
|
navigation: {
|
||||||
|
...russianMessages.ra.navigation,
|
||||||
|
no_filtered_results: "Нет результатов",
|
||||||
|
clear_filters: "Все фильтры сбросить",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
synapseadmin: {
|
||||||
|
auth: {
|
||||||
|
base_url: "Адрес домашнего сервера",
|
||||||
|
welcome: "Добро пожаловать в Synapse-admin",
|
||||||
|
server_version: "Версия Synapse",
|
||||||
|
supports_specs: "поддерживает спецификации Matrix",
|
||||||
|
username_error: "Пожалуйста, укажите полный ID пользователя: '@user:domain'",
|
||||||
|
protocol_error: "Адрес должен начинаться с 'http://' или 'https://'",
|
||||||
|
url_error: "Неверный адрес сервера Matrix",
|
||||||
|
sso_sign_in: "Вход через SSO",
|
||||||
|
},
|
||||||
|
users: {
|
||||||
|
invalid_user_id: "Локальная часть ID пользователя Matrix без адреса домашнего сервера.",
|
||||||
|
tabs: { sso: "SSO" },
|
||||||
|
},
|
||||||
|
rooms: {
|
||||||
|
details: "Данные комнаты",
|
||||||
|
tabs: {
|
||||||
|
basic: "Основные",
|
||||||
|
members: "Участники",
|
||||||
|
detail: "Подробности",
|
||||||
|
permission: "Права доступа",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
reports: { tabs: { basic: "Основные", detail: "Подробности" } },
|
||||||
|
},
|
||||||
|
import_users: {
|
||||||
|
error: {
|
||||||
|
at_entry: "В записи %{entry}: %{message}",
|
||||||
|
error: "Ошибка",
|
||||||
|
required_field: "Отсутствует обязательное поле '%{field}'",
|
||||||
|
invalid_value: "Неверное значение в строке %{row}. Поле '%{field}' может быть либо 'true', либо 'false'",
|
||||||
|
unreasonably_big: "Отказано в загрузке слишком большого файла размером %{size} мегабайт",
|
||||||
|
already_in_progress: "Импорт уже в процессе",
|
||||||
|
id_exits: "ID %{id} уже существует",
|
||||||
|
},
|
||||||
|
title: "Импорт пользователей из CSV",
|
||||||
|
goToPdf: "Перейти к PDF",
|
||||||
|
cards: {
|
||||||
|
importstats: {
|
||||||
|
header: "Импорт пользователей",
|
||||||
|
users_total:
|
||||||
|
"%{smart_count} пользователь в CSV файле |||| %{smart_count} пользователя в CSV файле |||| %{smart_count} пользователей в CSV файле",
|
||||||
|
guest_count: "%{smart_count} гость |||| %{smart_count} гостя |||| %{smart_count} гостей",
|
||||||
|
admin_count:
|
||||||
|
"%{smart_count} администратор |||| %{smart_count} администратора |||| %{smart_count} администраторов",
|
||||||
|
},
|
||||||
|
conflicts: {
|
||||||
|
header: "Стратегия разрешения конфликтов",
|
||||||
|
mode: {
|
||||||
|
stop: "Остановка при конфликте",
|
||||||
|
skip: "Показать ошибку и пропустить при конфликте",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ids: {
|
||||||
|
header: "Идентификаторы",
|
||||||
|
all_ids_present: "Идентификаторы присутствуют в каждой записи",
|
||||||
|
count_ids_present:
|
||||||
|
"%{smart_count} запись с ID |||| %{smart_count} записи с ID |||| %{smart_count} записей с ID",
|
||||||
|
mode: {
|
||||||
|
ignore: "Игнорировать идентификаторы в CSV и создать новые",
|
||||||
|
update: "Обновить существующие записи",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
passwords: {
|
||||||
|
header: "Пароли",
|
||||||
|
all_passwords_present: "Пароли присутствуют в каждой записи",
|
||||||
|
count_passwords_present:
|
||||||
|
"%{smart_count} запись с паролем |||| %{smart_count} записи с паролями |||| %{smart_count} записей с паролями",
|
||||||
|
use_passwords: "Использовать пароли из CSV",
|
||||||
|
},
|
||||||
|
upload: {
|
||||||
|
header: "Загрузить CSV файл",
|
||||||
|
explanation:
|
||||||
|
"Здесь вы можете загрузить файл со значениями, разделёнными запятыми, которые будут использованы для создания или обновления данных пользователей. \
|
||||||
|
В файле должны быть поля 'id' и 'displayname'. Вы можете скачать и изменить файл-образец отсюда: ",
|
||||||
|
},
|
||||||
|
startImport: {
|
||||||
|
simulate_only: "Только симулировать",
|
||||||
|
run_import: "Импорт",
|
||||||
|
},
|
||||||
|
results: {
|
||||||
|
header: "Результаты импорта",
|
||||||
|
total: "%{smart_count} запись всего |||| %{smart_count} записи всего |||| %{smart_count} записей всего",
|
||||||
|
successful:
|
||||||
|
"%{smart_count} запись успешно импортирована |||| %{smart_count} записи успешно импортированы |||| %{smart_count} записей успешно импортированы",
|
||||||
|
skipped:
|
||||||
|
"%{smart_count} запись пропущена |||| %{smart_count} записи пропущены |||| %{smart_count} записей пропущено",
|
||||||
|
download_skipped: "Скачать пропущенные записи",
|
||||||
|
with_error:
|
||||||
|
"%{smart_count} запись с ошибкой |||| %{smart_count} записи с ошибками |||| %{smart_count} записей с ошибками",
|
||||||
|
simulated_only: "Импорт был симулирован",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
delete_media: {
|
||||||
|
name: "Файлы",
|
||||||
|
fields: {
|
||||||
|
before_ts: "Последнее обращение до",
|
||||||
|
size_gt: "Более чем (в байтах)",
|
||||||
|
keep_profiles: "Сохранить аватары",
|
||||||
|
},
|
||||||
|
action: {
|
||||||
|
send: "Удалить файлы",
|
||||||
|
send_success: "Запрос успешно отправлен.",
|
||||||
|
send_failure: "Произошла ошибка.",
|
||||||
|
},
|
||||||
|
helper: {
|
||||||
|
send: "Это API удаляет локальные файлы с вашего собственного сервера, включая локальные миниатюры и копии скачанных файлов. \
|
||||||
|
Данный API не затрагивает файлы, загруженные во внешние хранилища.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
resources: {
|
||||||
|
users: {
|
||||||
|
name: "Пользователь |||| Пользователи",
|
||||||
|
email: "Почта",
|
||||||
|
msisdn: "Телефон",
|
||||||
|
threepid: "Почта / Телефон",
|
||||||
|
fields: {
|
||||||
|
avatar: "Аватар",
|
||||||
|
id: "ID пользователя",
|
||||||
|
name: "Имя",
|
||||||
|
is_guest: "Гость",
|
||||||
|
admin: "Администратор сервера",
|
||||||
|
locked: "Заблокирован",
|
||||||
|
deactivated: "Деактивирован",
|
||||||
|
erased: "Удалён",
|
||||||
|
guests: "Показывать гостей",
|
||||||
|
show_deactivated: "Показывать деактивированных",
|
||||||
|
user_id: "Поиск пользователя",
|
||||||
|
displayname: "Отображаемое имя",
|
||||||
|
password: "Пароль",
|
||||||
|
avatar_url: "Адрес аватары",
|
||||||
|
avatar_src: "Аватар",
|
||||||
|
medium: "Тип",
|
||||||
|
threepids: "3PID'ы",
|
||||||
|
address: "Адрес",
|
||||||
|
creation_ts_ms: "Дата создания",
|
||||||
|
consent_version: "Версия соглашения",
|
||||||
|
auth_provider: "Провайдер",
|
||||||
|
user_type: "Тип пользователя",
|
||||||
|
},
|
||||||
|
helper: {
|
||||||
|
password: "Смена пароля завершит все сессии пользователя.",
|
||||||
|
deactivate: "Вы должны предоставить пароль для реактивации учётной записи.",
|
||||||
|
erase: "Пометить пользователя как удалённого в соответствии с GDPR",
|
||||||
|
},
|
||||||
|
action: {
|
||||||
|
erase: "Удалить данные пользователя",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
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: {
|
||||||
|
forward_extremities:
|
||||||
|
"Оконечности — это события-листья в конце ориентированного ациклического графа (DAG) в комнате, т.е. события без дочерних элементов. \
|
||||||
|
Чем больше их в комнате, тем больше Synapse работает над разрешением состояния (это дорогостоящая операция). \
|
||||||
|
Хотя Synapse старается не допускать существования слишком большого числа таких событий в комнате, из-за ошибок они иногда снова появляются. \
|
||||||
|
Если в комнате >10 оконечностей, стоит найти комнату-виновника и попробовать удалить их с помощью SQL-запросов из #1760.",
|
||||||
|
},
|
||||||
|
enums: {
|
||||||
|
join_rules: {
|
||||||
|
public: "Для всех",
|
||||||
|
knock: "Надо постучать",
|
||||||
|
invite: "По приглашению",
|
||||||
|
private: "Приватная",
|
||||||
|
},
|
||||||
|
guest_access: {
|
||||||
|
can_join: "Гости могут войти",
|
||||||
|
forbidden: "Гости не могут войти",
|
||||||
|
},
|
||||||
|
history_visibility: {
|
||||||
|
invited: "С момента приглашения",
|
||||||
|
joined: "С момента входа",
|
||||||
|
shared: "С момента открытия доступа",
|
||||||
|
world_readable: "Для всех",
|
||||||
|
},
|
||||||
|
unencrypted: "Без шифрования",
|
||||||
|
},
|
||||||
|
action: {
|
||||||
|
erase: {
|
||||||
|
title: "Удалить комнату",
|
||||||
|
content:
|
||||||
|
"Действительно удалить эту комнату? Это действие будет невозможно отменить. Все сообщения и файлы в комнате будут удалены с сервера!",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
reports: {
|
||||||
|
name: "Жалоба |||| Жалобы",
|
||||||
|
fields: {
|
||||||
|
id: "ID",
|
||||||
|
received_ts: "Дата и время жалобы",
|
||||||
|
user_id: "Автор жалобы",
|
||||||
|
name: "Название комнаты",
|
||||||
|
score: "Баллы",
|
||||||
|
reason: "Причина",
|
||||||
|
event_id: "ID события",
|
||||||
|
event_json: {
|
||||||
|
origin: "Исходнный сервер",
|
||||||
|
origin_server_ts: "Дата и время отправки",
|
||||||
|
type: "Тип события",
|
||||||
|
content: {
|
||||||
|
msgtype: "Тип содержимого",
|
||||||
|
body: "Содержимое",
|
||||||
|
format: "Формат",
|
||||||
|
formatted_body: "Форматированное содержимое",
|
||||||
|
algorithm: "Алгоритм",
|
||||||
|
url: "Ссылка",
|
||||||
|
info: {
|
||||||
|
mimetype: "Тип",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
action: {
|
||||||
|
erase: {
|
||||||
|
title: "Удалить жалобу",
|
||||||
|
content: "Действительно удалить жалобу? Это действие будет невозможно отменить.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
connections: {
|
||||||
|
name: "Подключения",
|
||||||
|
fields: {
|
||||||
|
last_seen: "Дата",
|
||||||
|
ip: "IP адрес",
|
||||||
|
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: "Произошла ошибка.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
users_media: {
|
||||||
|
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: {
|
||||||
|
action: {
|
||||||
|
create: "Не защищён, установить защиту",
|
||||||
|
delete: "Защищён, снять защиту",
|
||||||
|
none: "На карантине",
|
||||||
|
send_success: "Статус защиты успешно изменён.",
|
||||||
|
send_failure: "Произошла ошибка.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
quarantine_media: {
|
||||||
|
action: {
|
||||||
|
name: "Карантин",
|
||||||
|
create: "Поместить на карантин",
|
||||||
|
delete: "На карантине, снять карантин",
|
||||||
|
none: "Защищено от карантина",
|
||||||
|
send_success: "Статус карантина успешно изменён.",
|
||||||
|
send_failure: "Произошла ошибка.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
pushers: {
|
||||||
|
name: "Пушер |||| Пушеры",
|
||||||
|
fields: {
|
||||||
|
app: "Приложение",
|
||||||
|
app_display_name: "Название приложения",
|
||||||
|
app_id: "ID приложения",
|
||||||
|
device_display_name: "Название устройства",
|
||||||
|
kind: "Вид",
|
||||||
|
lang: "Язык",
|
||||||
|
profile_tag: "Тег профиля",
|
||||||
|
pushkey: "Ключ",
|
||||||
|
data: { url: "URL" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
servernotices: {
|
||||||
|
name: "Серверные уведомления",
|
||||||
|
send: "Отправить серверные уведомления",
|
||||||
|
fields: {
|
||||||
|
body: "Сообщение",
|
||||||
|
},
|
||||||
|
action: {
|
||||||
|
send: "Отправить",
|
||||||
|
send_success: "Серверное уведомление успешно отправлено.",
|
||||||
|
send_failure: "Произошла ошибка.",
|
||||||
|
},
|
||||||
|
helper: {
|
||||||
|
send: 'Отправить серверное уведомление выбранным пользователям. На сервере должна быть активна функция "Server Notices".',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
user_media_statistics: {
|
||||||
|
name: "Файлы пользователей",
|
||||||
|
fields: {
|
||||||
|
media_count: "Количество файлов",
|
||||||
|
media_length: "Размер файлов",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
forward_extremities: {
|
||||||
|
name: "Оконечности",
|
||||||
|
fields: {
|
||||||
|
id: "ID события",
|
||||||
|
received_ts: "Дата и время",
|
||||||
|
depth: "Глубина",
|
||||||
|
state_group: "Группа состояния",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
room_state: {
|
||||||
|
name: "События состояния",
|
||||||
|
fields: {
|
||||||
|
type: "Тип",
|
||||||
|
content: "Содержимое",
|
||||||
|
origin_server_ts: "Дата отправки",
|
||||||
|
sender: "Отправитель",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
room_directory: {
|
||||||
|
name: "Каталог комнат",
|
||||||
|
fields: {
|
||||||
|
world_readable: "Гости могут просматривать без входа",
|
||||||
|
guest_can_join: "Гости могут войти",
|
||||||
|
},
|
||||||
|
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;
|
@ -4,6 +4,14 @@ import { SynapseTranslationMessages } from ".";
|
|||||||
|
|
||||||
const zh: SynapseTranslationMessages = {
|
const zh: SynapseTranslationMessages = {
|
||||||
...chineseMessages,
|
...chineseMessages,
|
||||||
|
ra: {
|
||||||
|
...chineseMessages.ra,
|
||||||
|
navigation: {
|
||||||
|
...chineseMessages.ra.navigation,
|
||||||
|
no_filtered_results: "没有结果",
|
||||||
|
clear_filters: "清除所有过滤器",
|
||||||
|
},
|
||||||
|
},
|
||||||
synapseadmin: {
|
synapseadmin: {
|
||||||
auth: {
|
auth: {
|
||||||
base_url: "服务器 URL",
|
base_url: "服务器 URL",
|
||||||
|
@ -5,7 +5,13 @@ import { createRoot } from "react-dom/client";
|
|||||||
import App from "./App";
|
import App from "./App";
|
||||||
import { AppContext } from "./AppContext";
|
import { AppContext } from "./AppContext";
|
||||||
|
|
||||||
fetch("config.json")
|
const baseUrl = import.meta.env.BASE_URL;
|
||||||
|
const configJSON = "config.json";
|
||||||
|
// if import.meta.env.BASE_URL have a trailing slash, remove it
|
||||||
|
// load config.json from relative path if import.meta.env.BASE_URL is None or empty
|
||||||
|
const configJSONUrl = baseUrl ? `${baseUrl.replace(/\/$/, "")}/${configJSON}` : configJSON;
|
||||||
|
|
||||||
|
fetch(configJSONUrl)
|
||||||
.then(res => res.json())
|
.then(res => res.json())
|
||||||
.then(props =>
|
.then(props =>
|
||||||
createRoot(document.getElementById("root")).render(
|
createRoot(document.getElementById("root")).render(
|
||||||
|
@ -27,6 +27,7 @@ import {
|
|||||||
isValidBaseUrl,
|
isValidBaseUrl,
|
||||||
splitMxid,
|
splitMxid,
|
||||||
} from "../synapse/synapse";
|
} from "../synapse/synapse";
|
||||||
|
import storage from "../storage";
|
||||||
|
|
||||||
const FormBox = styled(Box)(({ theme }) => ({
|
const FormBox = styled(Box)(({ theme }) => ({
|
||||||
display: "flex",
|
display: "flex",
|
||||||
@ -94,7 +95,7 @@ const LoginPage = () => {
|
|||||||
const [locale, setLocale] = useLocaleState();
|
const [locale, setLocale] = useLocaleState();
|
||||||
const locales = useLocales();
|
const locales = useLocales();
|
||||||
const translate = useTranslate();
|
const translate = useTranslate();
|
||||||
const base_url = allowSingleBaseUrl ? restrictBaseUrl : localStorage.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);
|
||||||
|
|
||||||
@ -103,8 +104,8 @@ const LoginPage = () => {
|
|||||||
console.log("SSO token is", ssoToken);
|
console.log("SSO token is", ssoToken);
|
||||||
// Prevent further requests
|
// Prevent further requests
|
||||||
window.history.replaceState({}, "", window.location.href.replace(loginToken[0], "#").split("#")[0]);
|
window.history.replaceState({}, "", window.location.href.replace(loginToken[0], "#").split("#")[0]);
|
||||||
const baseUrl = localStorage.getItem("sso_base_url");
|
const baseUrl = storage.getItem("sso_base_url");
|
||||||
localStorage.removeItem("sso_base_url");
|
storage.removeItem("sso_base_url");
|
||||||
if (baseUrl) {
|
if (baseUrl) {
|
||||||
const auth = {
|
const auth = {
|
||||||
base_url: baseUrl,
|
base_url: baseUrl,
|
||||||
@ -154,7 +155,7 @@ const LoginPage = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleSSO = () => {
|
const handleSSO = () => {
|
||||||
localStorage.setItem("sso_base_url", ssoBaseUrl);
|
storage.setItem("sso_base_url", ssoBaseUrl);
|
||||||
const ssoFullUrl = `${ssoBaseUrl}/_matrix/client/r0/login/sso/redirect?redirectUrl=${encodeURIComponent(
|
const ssoFullUrl = `${ssoBaseUrl}/_matrix/client/r0/login/sso/redirect?redirectUrl=${encodeURIComponent(
|
||||||
window.location.href
|
window.location.href
|
||||||
)}`;
|
)}`;
|
||||||
@ -216,7 +217,6 @@ const LoginPage = () => {
|
|||||||
disabled={loading || !supportPassAuth}
|
disabled={loading || !supportPassAuth}
|
||||||
onBlur={handleUsernameChange}
|
onBlur={handleUsernameChange}
|
||||||
resettable
|
resettable
|
||||||
fullWidth
|
|
||||||
validate={required()}
|
validate={required()}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
@ -228,7 +228,6 @@ const LoginPage = () => {
|
|||||||
autoComplete="current-password"
|
autoComplete="current-password"
|
||||||
disabled={loading || !supportPassAuth}
|
disabled={loading || !supportPassAuth}
|
||||||
resettable
|
resettable
|
||||||
fullWidth
|
|
||||||
validate={required()}
|
validate={required()}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
@ -241,7 +240,6 @@ const LoginPage = () => {
|
|||||||
disabled={loading}
|
disabled={loading}
|
||||||
readOnly={allowSingleBaseUrl}
|
readOnly={allowSingleBaseUrl}
|
||||||
resettable={allowAnyBaseUrl}
|
resettable={allowAnyBaseUrl}
|
||||||
fullWidth
|
|
||||||
validate={[required(), validateBaseUrl]}
|
validate={[required(), validateBaseUrl]}
|
||||||
>
|
>
|
||||||
{allowMultipleBaseUrls &&
|
{allowMultipleBaseUrls &&
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
|
import { get } from "lodash";
|
||||||
import { MouseEvent } from "react";
|
import { MouseEvent } from "react";
|
||||||
|
|
||||||
import AutorenewIcon from "@mui/icons-material/Autorenew";
|
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 { blue } from "@mui/material/colors";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Datagrid,
|
Datagrid,
|
||||||
@ -27,16 +29,14 @@ import {
|
|||||||
useNotify,
|
useNotify,
|
||||||
useRefresh,
|
useRefresh,
|
||||||
useTranslate,
|
useTranslate,
|
||||||
|
DateFieldProps,
|
||||||
} from "react-admin";
|
} from "react-admin";
|
||||||
|
|
||||||
import { DATE_FORMAT } from "../components/date";
|
import { DATE_FORMAT } from "../components/date";
|
||||||
|
import { lighten, useTheme } from '@mui/material';
|
||||||
|
|
||||||
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 ? "#ffcccc" : "white",
|
|
||||||
});
|
|
||||||
|
|
||||||
const destinationFilters = [<SearchInput source="destination" alwaysOn />];
|
const destinationFilters = [<SearchInput source="destination" alwaysOn />];
|
||||||
|
|
||||||
export const DestinationReconnectButton = () => {
|
export const DestinationReconnectButton = () => {
|
||||||
@ -87,12 +87,30 @@ const DestinationTitle = () => {
|
|||||||
const translate = useTranslate();
|
const translate = useTranslate();
|
||||||
return (
|
return (
|
||||||
<span>
|
<span>
|
||||||
{translate("resources.destinations.name", 1)} {record.destination}
|
{translate("resources.destinations.name", 1)} {record?.destination}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const RetryDateField = (props: DateFieldProps) => {
|
||||||
|
const record = useRecordContext(props);
|
||||||
|
if (props.source && get(record, props.source) === 0) {
|
||||||
|
return <DateField {...props} record={{ ...record, [props.source]: null }} />;
|
||||||
|
}
|
||||||
|
return <DateField {...props} />;
|
||||||
|
};
|
||||||
|
|
||||||
export const DestinationList = (props: ListProps) => {
|
export const DestinationList = (props: ListProps) => {
|
||||||
|
const { palette: { error, mode }, } = useTheme();
|
||||||
|
const destinationRowSx = (record: RaRecord) => ({
|
||||||
|
backgroundColor: record.retry_last_ts > 0 ? lighten(error[mode], 0.5) : undefined,
|
||||||
|
"& > td": mode === 'dark' ? {
|
||||||
|
color: record.retry_last_ts > 0 ? "black" : "white",
|
||||||
|
"& > button": {
|
||||||
|
color: blue[700],
|
||||||
|
},
|
||||||
|
} : undefined,
|
||||||
|
});
|
||||||
return (
|
return (
|
||||||
<List
|
<List
|
||||||
{...props}
|
{...props}
|
||||||
@ -103,7 +121,7 @@ export const DestinationList = (props: ListProps) => {
|
|||||||
<Datagrid rowSx={destinationRowSx} rowClick={id => `${id}/show/rooms`} bulkActionButtons={false}>
|
<Datagrid rowSx={destinationRowSx} rowClick={id => `${id}/show/rooms`} bulkActionButtons={false}>
|
||||||
<TextField source="destination" />
|
<TextField source="destination" />
|
||||||
<DateField source="failure_ts" showTime options={DATE_FORMAT} />
|
<DateField source="failure_ts" showTime options={DATE_FORMAT} />
|
||||||
<DateField 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" />
|
||||||
<TextField source="last_successful_stream_ordering" />
|
<TextField source="last_successful_stream_ordering" />
|
||||||
<DestinationReconnectButton />
|
<DestinationReconnectButton />
|
||||||
|
@ -25,7 +25,7 @@ import {
|
|||||||
useRefresh,
|
useRefresh,
|
||||||
useUnselectAll,
|
useUnselectAll,
|
||||||
} from "react-admin";
|
} from "react-admin";
|
||||||
import { useMutation } from "react-query";
|
import { useMutation } from "@tanstack/react-query";
|
||||||
|
|
||||||
import AvatarField from "../components/AvatarField";
|
import AvatarField from "../components/AvatarField";
|
||||||
|
|
||||||
@ -70,27 +70,25 @@ export const RoomDirectoryBulkPublishButton = (props: ButtonProps) => {
|
|||||||
const refresh = useRefresh();
|
const refresh = useRefresh();
|
||||||
const unselectAllRooms = useUnselectAll("rooms");
|
const unselectAllRooms = useUnselectAll("rooms");
|
||||||
const dataProvider = useDataProvider();
|
const dataProvider = useDataProvider();
|
||||||
const { mutate, isLoading } = useMutation(
|
const { mutate, isPending } = useMutation({
|
||||||
() =>
|
mutationFn: () =>
|
||||||
dataProvider.createMany("room_directory", {
|
dataProvider.createMany("room_directory", {
|
||||||
ids: selectedIds,
|
ids: selectedIds,
|
||||||
data: {},
|
data: {},
|
||||||
}),
|
}),
|
||||||
{
|
onSuccess: () => {
|
||||||
onSuccess: () => {
|
notify("resources.room_directory.action.send_success");
|
||||||
notify("resources.room_directory.action.send_success");
|
unselectAllRooms();
|
||||||
unselectAllRooms();
|
refresh();
|
||||||
refresh();
|
},
|
||||||
},
|
onError: () =>
|
||||||
onError: () =>
|
notify("resources.room_directory.action.send_failure", {
|
||||||
notify("resources.room_directory.action.send_failure", {
|
type: "error",
|
||||||
type: "error",
|
}),
|
||||||
}),
|
});
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button {...props} label="resources.room_directory.action.create" onClick={mutate} disabled={isLoading}>
|
<Button {...props} label="resources.room_directory.action.create" onClick={mutate} disabled={isPending}>
|
||||||
<RoomDirectoryIcon />
|
<RoomDirectoryIcon />
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
@ -102,6 +100,10 @@ export const RoomDirectoryPublishButton = (props: ButtonProps) => {
|
|||||||
const refresh = useRefresh();
|
const refresh = useRefresh();
|
||||||
const [create, { isLoading }] = useCreate();
|
const [create, { isLoading }] = useCreate();
|
||||||
|
|
||||||
|
if (!record) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const handleSend = () => {
|
const handleSend = () => {
|
||||||
create(
|
create(
|
||||||
"room_directory",
|
"room_directory",
|
||||||
|
@ -65,7 +65,7 @@ const RoomTitle = () => {
|
|||||||
|
|
||||||
const RoomShowActions = () => {
|
const RoomShowActions = () => {
|
||||||
const record = useRecordContext();
|
const record = useRecordContext();
|
||||||
const publishButton = record.public ? <RoomDirectoryUnpublishButton /> : <RoomDirectoryPublishButton />;
|
const publishButton = record?.public ? <RoomDirectoryUnpublishButton /> : <RoomDirectoryPublishButton />;
|
||||||
// FIXME: refresh after (un)publish
|
// FIXME: refresh after (un)publish
|
||||||
return (
|
return (
|
||||||
<TopToolbar>
|
<TopToolbar>
|
||||||
|
@ -47,6 +47,7 @@ import {
|
|||||||
TopToolbar,
|
TopToolbar,
|
||||||
NumberField,
|
NumberField,
|
||||||
useListContext,
|
useListContext,
|
||||||
|
Identifier,
|
||||||
} from "react-admin";
|
} from "react-admin";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
|
|
||||||
@ -79,17 +80,13 @@ 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 = [
|
||||||
<SearchInput source="name" alwaysOn />,
|
<SearchInput source="name" alwaysOn />,
|
||||||
<BooleanInput source="guests" alwaysOn />,
|
<BooleanInput source="guests" alwaysOn />,
|
||||||
<BooleanInput label="resources.users.fields.show_deactivated" source="deactivated" alwaysOn />,
|
<BooleanInput label="resources.users.fields.show_deactivated" source="deactivated" alwaysOn />,
|
||||||
|
<BooleanInput label="resources.users.fields.show_locked" source="locked" alwaysOn />,
|
||||||
];
|
];
|
||||||
|
|
||||||
const UserBulkActionButtons = () => (
|
const UserBulkActionButtons = () => (
|
||||||
@ -107,12 +104,15 @@ export const UserList = (props: ListProps) => (
|
|||||||
<List
|
<List
|
||||||
{...props}
|
{...props}
|
||||||
filters={userFilters}
|
filters={userFilters}
|
||||||
filterDefaultValues={{ guests: true, deactivated: false }}
|
filterDefaultValues={{ guests: true, deactivated: false, locked: false }}
|
||||||
sort={{ field: "name", order: "ASC" }}
|
sort={{ field: "name", order: "ASC" }}
|
||||||
actions={<UserListActions />}
|
actions={<UserListActions />}
|
||||||
pagination={<UserPagination />}
|
pagination={<UserPagination />}
|
||||||
>
|
>
|
||||||
<Datagrid rowClick="edit" 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" />
|
||||||
@ -128,8 +128,8 @@ export const UserList = (props: ListProps) => (
|
|||||||
|
|
||||||
// https://matrix.org/docs/spec/appendices#user-identifiers
|
// https://matrix.org/docs/spec/appendices#user-identifiers
|
||||||
// here only local part of user_id
|
// here only local part of user_id
|
||||||
// maxLength = 255 - "@" - ":" - localStorage.getItem("home_server").length
|
// maxLength = 255 - "@" - ":" - storage.getItem("home_server").length
|
||||||
// localStorage.getItem("home_server").length is not valid here
|
// storage.getItem("home_server").length is not valid here
|
||||||
const validateUser = [required(), maxLength(253), regex(/^[a-z0-9._=\-/]+$/, "synapseadmin.users.invalid_user_id")];
|
const validateUser = [required(), maxLength(253), regex(/^[a-z0-9._=\-/]+$/, "synapseadmin.users.invalid_user_id")];
|
||||||
|
|
||||||
const validateAddress = [required(), maxLength(255)];
|
const validateAddress = [required(), maxLength(255)];
|
||||||
@ -140,7 +140,7 @@ const UserEditActions = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<TopToolbar>
|
<TopToolbar>
|
||||||
{!record.deactivated && <ServerNoticeButton />}
|
{!record?.deactivated && <ServerNoticeButton />}
|
||||||
<DeleteButton
|
<DeleteButton
|
||||||
label="resources.users.action.erase"
|
label="resources.users.action.erase"
|
||||||
confirmTitle={translate("resources.users.helper.erase", {
|
confirmTitle={translate("resources.users.helper.erase", {
|
||||||
@ -153,7 +153,12 @@ const UserEditActions = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const UserCreate = (props: CreateProps) => (
|
export const UserCreate = (props: CreateProps) => (
|
||||||
<Create {...props}>
|
<Create
|
||||||
|
{...props}
|
||||||
|
redirect={(resource: string | undefined, id: Identifier | undefined) => {
|
||||||
|
return `${resource}/${id}`;
|
||||||
|
}}
|
||||||
|
>
|
||||||
<SimpleForm>
|
<SimpleForm>
|
||||||
<TextInput source="id" autoComplete="off" validate={validateUser} />
|
<TextInput source="id" autoComplete="off" validate={validateUser} />
|
||||||
<TextInput source="displayname" validate={maxLength(256)} />
|
<TextInput source="displayname" validate={maxLength(256)} />
|
||||||
|
3
src/storage.ts
Normal file
3
src/storage.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
const storage = localStorage;
|
||||||
|
|
||||||
|
export default storage;
|
@ -1,13 +1,14 @@
|
|||||||
import fetchMock from "jest-fetch-mock";
|
import fetchMock from "jest-fetch-mock";
|
||||||
|
|
||||||
import authProvider from "./authProvider";
|
import authProvider from "./authProvider";
|
||||||
|
import storage from "../storage";
|
||||||
|
|
||||||
fetchMock.enableMocks();
|
fetchMock.enableMocks();
|
||||||
|
|
||||||
describe("authProvider", () => {
|
describe("authProvider", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
fetchMock.resetMocks();
|
fetchMock.resetMocks();
|
||||||
localStorage.clear();
|
storage.clear();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("login", () => {
|
describe("login", () => {
|
||||||
@ -29,17 +30,27 @@ describe("authProvider", () => {
|
|||||||
|
|
||||||
expect(ret).toBe(undefined);
|
expect(ret).toBe(undefined);
|
||||||
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","user":"@user:example.com","password":"secret"}',
|
body: JSON.stringify({
|
||||||
|
device_id: null,
|
||||||
|
initial_device_display_name: "Synapse Admin",
|
||||||
|
type: "m.login.password",
|
||||||
|
user: "@user:example.com",
|
||||||
|
password: "secret",
|
||||||
|
identifier: {
|
||||||
|
type: "m.id.user",
|
||||||
|
user: "@user:example.com",
|
||||||
|
}
|
||||||
|
}),
|
||||||
headers: new Headers({
|
headers: new Headers({
|
||||||
Accept: "application/json",
|
Accept: "application/json",
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
}),
|
}),
|
||||||
method: "POST",
|
method: "POST",
|
||||||
});
|
});
|
||||||
expect(localStorage.getItem("base_url")).toEqual("http://example.com");
|
expect(storage.getItem("base_url")).toEqual("http://example.com");
|
||||||
expect(localStorage.getItem("user_id")).toEqual("@user:example.com");
|
expect(storage.getItem("user_id")).toEqual("@user:example.com");
|
||||||
expect(localStorage.getItem("access_token")).toEqual("foobar");
|
expect(storage.getItem("access_token")).toEqual("foobar");
|
||||||
expect(localStorage.getItem("device_id")).toEqual("some_device");
|
expect(storage.getItem("device_id")).toEqual("some_device");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -67,16 +78,16 @@ describe("authProvider", () => {
|
|||||||
}),
|
}),
|
||||||
method: "POST",
|
method: "POST",
|
||||||
});
|
});
|
||||||
expect(localStorage.getItem("base_url")).toEqual("https://example.com");
|
expect(storage.getItem("base_url")).toEqual("https://example.com");
|
||||||
expect(localStorage.getItem("user_id")).toEqual("@user:example.com");
|
expect(storage.getItem("user_id")).toEqual("@user:example.com");
|
||||||
expect(localStorage.getItem("access_token")).toEqual("foobar");
|
expect(storage.getItem("access_token")).toEqual("foobar");
|
||||||
expect(localStorage.getItem("device_id")).toEqual("some_device");
|
expect(storage.getItem("device_id")).toEqual("some_device");
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("logout", () => {
|
describe("logout", () => {
|
||||||
it("should remove the access_token from localStorage", async () => {
|
it("should remove the access_token from storage", async () => {
|
||||||
localStorage.setItem("base_url", "example.com");
|
storage.setItem("base_url", "example.com");
|
||||||
localStorage.setItem("access_token", "foo");
|
storage.setItem("access_token", "foo");
|
||||||
fetchMock.mockResponse(JSON.stringify({}));
|
fetchMock.mockResponse(JSON.stringify({}));
|
||||||
|
|
||||||
await authProvider.logout(null);
|
await authProvider.logout(null);
|
||||||
@ -89,7 +100,7 @@ describe("authProvider", () => {
|
|||||||
method: "POST",
|
method: "POST",
|
||||||
user: { authenticated: true, token: "Bearer foo" },
|
user: { authenticated: true, token: "Bearer foo" },
|
||||||
});
|
});
|
||||||
expect(localStorage.getItem("access_token")).toBeNull();
|
expect(storage.getItem("access_token")).toBeNull();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -113,7 +124,7 @@ describe("authProvider", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should resolve when logged in", async () => {
|
it("should resolve when logged in", async () => {
|
||||||
localStorage.setItem("access_token", "foobar");
|
storage.setItem("access_token", "foobar");
|
||||||
|
|
||||||
await expect(authProvider.checkAuth({})).resolves.toBeUndefined();
|
await expect(authProvider.checkAuth({})).resolves.toBeUndefined();
|
||||||
});
|
});
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
import { AuthProvider, Options, fetchUtils } from "react-admin";
|
import { AuthProvider, Options, fetchUtils } from "react-admin";
|
||||||
|
|
||||||
|
import storage from "../storage";
|
||||||
|
|
||||||
const authProvider: AuthProvider = {
|
const authProvider: AuthProvider = {
|
||||||
// called when the user attempts to log in
|
// called when the user attempts to log in
|
||||||
login: async ({
|
login: async ({
|
||||||
@ -19,7 +21,7 @@ const authProvider: AuthProvider = {
|
|||||||
body: JSON.stringify(
|
body: JSON.stringify(
|
||||||
Object.assign(
|
Object.assign(
|
||||||
{
|
{
|
||||||
device_id: localStorage.getItem("device_id"),
|
device_id: storage.getItem("device_id"),
|
||||||
initial_device_display_name: "Synapse Admin",
|
initial_device_display_name: "Synapse Admin",
|
||||||
},
|
},
|
||||||
loginToken
|
loginToken
|
||||||
@ -31,6 +33,10 @@ const authProvider: AuthProvider = {
|
|||||||
type: "m.login.password",
|
type: "m.login.password",
|
||||||
user: username,
|
user: username,
|
||||||
password: password,
|
password: password,
|
||||||
|
identifier: {
|
||||||
|
type: "m.id.user",
|
||||||
|
user: username,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
@ -40,23 +46,23 @@ const authProvider: AuthProvider = {
|
|||||||
// server, since the admin might want to access the admin API via some
|
// server, since the admin might want to access the admin API via some
|
||||||
// private address
|
// private address
|
||||||
base_url = base_url.replace(/\/+$/g, "");
|
base_url = base_url.replace(/\/+$/g, "");
|
||||||
localStorage.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";
|
const login_api_url = decoded_base_url + "/_matrix/client/r0/login";
|
||||||
|
|
||||||
const { json } = await fetchUtils.fetchJson(login_api_url, options);
|
const { json } = await fetchUtils.fetchJson(login_api_url, options);
|
||||||
localStorage.setItem("home_server", json.home_server);
|
storage.setItem("home_server", json.home_server);
|
||||||
localStorage.setItem("user_id", json.user_id);
|
storage.setItem("user_id", json.user_id);
|
||||||
localStorage.setItem("access_token", json.access_token);
|
storage.setItem("access_token", json.access_token);
|
||||||
localStorage.setItem("device_id", json.device_id);
|
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 () => {
|
||||||
console.log("logout");
|
console.log("logout");
|
||||||
|
|
||||||
const logout_api_url = localStorage.getItem("base_url") + "/_matrix/client/r0/logout";
|
const logout_api_url = storage.getItem("base_url") + "/_matrix/client/r0/logout";
|
||||||
const access_token = localStorage.getItem("access_token");
|
const access_token = storage.getItem("access_token");
|
||||||
|
|
||||||
const options: Options = {
|
const options: Options = {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@ -68,7 +74,7 @@ const authProvider: AuthProvider = {
|
|||||||
|
|
||||||
if (typeof access_token === "string") {
|
if (typeof access_token === "string") {
|
||||||
await fetchUtils.fetchJson(logout_api_url, options);
|
await fetchUtils.fetchJson(logout_api_url, options);
|
||||||
localStorage.removeItem("access_token");
|
storage.removeItem("access_token");
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// called when the API returns an error
|
// called when the API returns an error
|
||||||
@ -81,7 +87,7 @@ const authProvider: AuthProvider = {
|
|||||||
},
|
},
|
||||||
// called when the user navigates to a new location, to check for authentication
|
// called when the user navigates to a new location, to check for authentication
|
||||||
checkAuth: () => {
|
checkAuth: () => {
|
||||||
const access_token = localStorage.getItem("access_token");
|
const access_token = storage.getItem("access_token");
|
||||||
console.log("checkAuth " + access_token);
|
console.log("checkAuth " + access_token);
|
||||||
return typeof access_token === "string" ? Promise.resolve() : Promise.reject();
|
return typeof access_token === "string" ? Promise.resolve() : Promise.reject();
|
||||||
},
|
},
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import fetchMock from "jest-fetch-mock";
|
import fetchMock from "jest-fetch-mock";
|
||||||
|
|
||||||
import dataProvider from "./dataProvider";
|
import dataProvider from "./dataProvider";
|
||||||
|
import storage from "../storage";
|
||||||
|
|
||||||
fetchMock.enableMocks();
|
fetchMock.enableMocks();
|
||||||
|
|
||||||
@ -9,8 +10,8 @@ beforeEach(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("dataProvider", () => {
|
describe("dataProvider", () => {
|
||||||
localStorage.setItem("base_url", "http://localhost");
|
storage.setItem("base_url", "http://localhost");
|
||||||
localStorage.setItem("access_token", "access_token");
|
storage.setItem("access_token", "access_token");
|
||||||
|
|
||||||
it("fetches all users", async () => {
|
it("fetches all users", async () => {
|
||||||
fetchMock.mockResponseOnce(
|
fetchMock.mockResponseOnce(
|
||||||
|
@ -1,10 +1,19 @@
|
|||||||
import { stringify } from "query-string";
|
import { stringify } from "query-string";
|
||||||
|
import {
|
||||||
import { DataProvider, DeleteParams, Identifier, Options, RaRecord, fetchUtils } from "react-admin";
|
DataProvider,
|
||||||
|
DeleteParams,
|
||||||
|
Identifier,
|
||||||
|
Options,
|
||||||
|
PaginationPayload,
|
||||||
|
RaRecord,
|
||||||
|
SortPayload,
|
||||||
|
fetchUtils
|
||||||
|
} from "react-admin";
|
||||||
|
import storage from "../storage";
|
||||||
|
|
||||||
// Adds the access token to all requests
|
// Adds the access token to all requests
|
||||||
const jsonClient = (url: string, options: Options = {}) => {
|
const jsonClient = (url: string, options: Options = {}) => {
|
||||||
const token = localStorage.getItem("access_token");
|
const token = storage.getItem("access_token");
|
||||||
console.log("httpClient " + url);
|
console.log("httpClient " + url);
|
||||||
if (token != null) {
|
if (token != null) {
|
||||||
options.user = {
|
options.user = {
|
||||||
@ -16,7 +25,7 @@ const jsonClient = (url: string, options: Options = {}) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const mxcUrlToHttp = (mxcUrl: string) => {
|
const mxcUrlToHttp = (mxcUrl: string) => {
|
||||||
const homeserver = localStorage.getItem("base_url");
|
const homeserver = storage.getItem("base_url");
|
||||||
const re = /^mxc:\/\/([^/]+)\/(\w+)/;
|
const re = /^mxc:\/\/([^/]+)\/(\w+)/;
|
||||||
const ret = re.exec(mxcUrl);
|
const ret = re.exec(mxcUrl);
|
||||||
console.log("mxcClient " + ret);
|
console.log("mxcClient " + ret);
|
||||||
@ -232,7 +241,7 @@ const resourceMap = {
|
|||||||
data: "users",
|
data: "users",
|
||||||
total: json => json.total,
|
total: json => json.total,
|
||||||
create: (data: RaRecord) => ({
|
create: (data: RaRecord) => ({
|
||||||
endpoint: `/_synapse/admin/v2/users/@${encodeURIComponent(data.id)}:${localStorage.getItem("home_server")}`,
|
endpoint: `/_synapse/admin/v2/users/@${encodeURIComponent(data.id)}:${storage.getItem("home_server")}`,
|
||||||
body: data,
|
body: data,
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
}),
|
}),
|
||||||
@ -341,7 +350,7 @@ const resourceMap = {
|
|||||||
data: "media",
|
data: "media",
|
||||||
total: json => json.total,
|
total: json => json.total,
|
||||||
delete: (params: DeleteParams) => ({
|
delete: (params: DeleteParams) => ({
|
||||||
endpoint: `/_synapse/admin/v1/media/${localStorage.getItem("home_server")}/${params.id}`,
|
endpoint: `/_synapse/admin/v1/media/${storage.getItem("home_server")}/${params.id}`,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
protect_media: {
|
protect_media: {
|
||||||
@ -358,11 +367,11 @@ const resourceMap = {
|
|||||||
quarantine_media: {
|
quarantine_media: {
|
||||||
map: (qm: UserMedia) => ({ id: qm.media_id }),
|
map: (qm: UserMedia) => ({ id: qm.media_id }),
|
||||||
create: (params: UserMedia) => ({
|
create: (params: UserMedia) => ({
|
||||||
endpoint: `/_synapse/admin/v1/media/quarantine/${localStorage.getItem("home_server")}/${params.media_id}`,
|
endpoint: `/_synapse/admin/v1/media/quarantine/${storage.getItem("home_server")}/${params.media_id}`,
|
||||||
method: "POST",
|
method: "POST",
|
||||||
}),
|
}),
|
||||||
delete: (params: DeleteParams) => ({
|
delete: (params: DeleteParams) => ({
|
||||||
endpoint: `/_synapse/admin/v1/media/unquarantine/${localStorage.getItem("home_server")}/${params.id}`,
|
endpoint: `/_synapse/admin/v1/media/unquarantine/${storage.getItem("home_server")}/${params.id}`,
|
||||||
method: "POST",
|
method: "POST",
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
@ -489,9 +498,9 @@ function getSearchOrder(order: "ASC" | "DESC") {
|
|||||||
const dataProvider: SynapseDataProvider = {
|
const dataProvider: SynapseDataProvider = {
|
||||||
getList: async (resource, params) => {
|
getList: async (resource, params) => {
|
||||||
console.log("getList " + resource);
|
console.log("getList " + resource);
|
||||||
const { user_id, name, guests, deactivated, search_term, destination, valid } = params.filter;
|
const { user_id, name, guests, deactivated, locked, search_term, destination, valid } = params.filter;
|
||||||
const { page, perPage } = params.pagination;
|
const { page, perPage } = params.pagination as PaginationPayload;
|
||||||
const { field, order } = params.sort;
|
const { field, order } = params.sort as SortPayload;
|
||||||
const from = (page - 1) * perPage;
|
const from = (page - 1) * perPage;
|
||||||
const query = {
|
const query = {
|
||||||
from: from,
|
from: from,
|
||||||
@ -502,11 +511,12 @@ const dataProvider: SynapseDataProvider = {
|
|||||||
destination: destination,
|
destination: destination,
|
||||||
guests: guests,
|
guests: guests,
|
||||||
deactivated: deactivated,
|
deactivated: deactivated,
|
||||||
|
locked: locked,
|
||||||
valid: valid,
|
valid: valid,
|
||||||
order_by: field,
|
order_by: field,
|
||||||
dir: getSearchOrder(order),
|
dir: getSearchOrder(order),
|
||||||
};
|
};
|
||||||
const homeserver = localStorage.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");
|
||||||
|
|
||||||
const res = resourceMap[resource];
|
const res = resourceMap[resource];
|
||||||
@ -523,7 +533,7 @@ const dataProvider: SynapseDataProvider = {
|
|||||||
|
|
||||||
getOne: async (resource, params) => {
|
getOne: async (resource, params) => {
|
||||||
console.log("getOne " + resource);
|
console.log("getOne " + resource);
|
||||||
const homeserver = localStorage.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");
|
||||||
|
|
||||||
const res = resourceMap[resource];
|
const res = resourceMap[resource];
|
||||||
@ -535,7 +545,7 @@ const dataProvider: SynapseDataProvider = {
|
|||||||
|
|
||||||
getMany: async (resource, params) => {
|
getMany: async (resource, params) => {
|
||||||
console.log("getMany " + resource);
|
console.log("getMany " + resource);
|
||||||
const homeserver = localStorage.getItem("base_url");
|
const homeserver = storage.getItem("base_url");
|
||||||
if (!homeserver || !(resource in resourceMap)) throw Error("Homerserver not set");
|
if (!homeserver || !(resource in resourceMap)) throw Error("Homerserver not set");
|
||||||
|
|
||||||
const res = resourceMap[resource];
|
const res = resourceMap[resource];
|
||||||
@ -560,7 +570,7 @@ const dataProvider: SynapseDataProvider = {
|
|||||||
dir: getSearchOrder(order),
|
dir: getSearchOrder(order),
|
||||||
};
|
};
|
||||||
|
|
||||||
const homeserver = localStorage.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");
|
||||||
|
|
||||||
const res = resourceMap[resource];
|
const res = resourceMap[resource];
|
||||||
@ -577,7 +587,7 @@ const dataProvider: SynapseDataProvider = {
|
|||||||
|
|
||||||
update: async (resource, params) => {
|
update: async (resource, params) => {
|
||||||
console.log("update " + resource);
|
console.log("update " + resource);
|
||||||
const homeserver = localStorage.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");
|
||||||
|
|
||||||
const res = resourceMap[resource];
|
const res = resourceMap[resource];
|
||||||
@ -592,7 +602,7 @@ const dataProvider: SynapseDataProvider = {
|
|||||||
|
|
||||||
updateMany: async (resource, params) => {
|
updateMany: async (resource, params) => {
|
||||||
console.log("updateMany " + resource);
|
console.log("updateMany " + resource);
|
||||||
const homeserver = localStorage.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");
|
||||||
|
|
||||||
const res = resourceMap[resource];
|
const res = resourceMap[resource];
|
||||||
@ -609,7 +619,7 @@ const dataProvider: SynapseDataProvider = {
|
|||||||
|
|
||||||
create: async (resource, params) => {
|
create: async (resource, params) => {
|
||||||
console.log("create " + resource);
|
console.log("create " + resource);
|
||||||
const homeserver = localStorage.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");
|
||||||
|
|
||||||
const res = resourceMap[resource];
|
const res = resourceMap[resource];
|
||||||
@ -626,7 +636,7 @@ const dataProvider: SynapseDataProvider = {
|
|||||||
|
|
||||||
createMany: async (resource: string, params: { ids: Identifier[]; data: RaRecord }) => {
|
createMany: async (resource: string, params: { ids: Identifier[]; data: RaRecord }) => {
|
||||||
console.log("createMany " + resource);
|
console.log("createMany " + resource);
|
||||||
const homeserver = localStorage.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");
|
||||||
|
|
||||||
const res = resourceMap[resource];
|
const res = resourceMap[resource];
|
||||||
@ -648,7 +658,7 @@ const dataProvider: SynapseDataProvider = {
|
|||||||
|
|
||||||
delete: async (resource, params) => {
|
delete: async (resource, params) => {
|
||||||
console.log("delete " + resource);
|
console.log("delete " + resource);
|
||||||
const homeserver = localStorage.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");
|
||||||
|
|
||||||
const res = resourceMap[resource];
|
const res = resourceMap[resource];
|
||||||
@ -673,7 +683,7 @@ const dataProvider: SynapseDataProvider = {
|
|||||||
|
|
||||||
deleteMany: async (resource, params) => {
|
deleteMany: async (resource, params) => {
|
||||||
console.log("deleteMany " + resource);
|
console.log("deleteMany " + resource);
|
||||||
const homeserver = localStorage.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");
|
||||||
|
|
||||||
const res = resourceMap[resource];
|
const res = resourceMap[resource];
|
||||||
@ -719,10 +729,10 @@ const dataProvider: SynapseDataProvider = {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
deleteMedia: async ({ before_ts, size_gt = 0, keep_profiles = true }) => {
|
deleteMedia: async ({ before_ts, size_gt = 0, keep_profiles = true }) => {
|
||||||
const homeserver = localStorage.getItem("home_server"); // TODO only required for synapse < 1.78.0
|
const homeserver = storage.getItem("home_server"); // TODO only required for synapse < 1.78.0
|
||||||
const endpoint = `/_synapse/admin/v1/media/${homeserver}/delete?before_ts=${before_ts}&size_gt=${size_gt}&keep_profiles=${keep_profiles}`;
|
const endpoint = `/_synapse/admin/v1/media/${homeserver}/delete?before_ts=${before_ts}&size_gt=${size_gt}&keep_profiles=${keep_profiles}`;
|
||||||
|
|
||||||
const base_url = localStorage.getItem("base_url");
|
const base_url = storage.getItem("base_url");
|
||||||
const endpoint_url = base_url + endpoint;
|
const endpoint_url = base_url + endpoint;
|
||||||
const { json } = await jsonClient(endpoint_url, { method: "POST" });
|
const { json } = await jsonClient(endpoint_url, { method: "POST" });
|
||||||
return json as DeleteMediaResult;
|
return json as DeleteMediaResult;
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
import { fetchUtils } from "react-admin";
|
import { fetchUtils } from "react-admin";
|
||||||
|
|
||||||
|
import storage from "../storage";
|
||||||
|
|
||||||
export const splitMxid = mxid => {
|
export const splitMxid = mxid => {
|
||||||
const re = /^@(?<name>[a-zA-Z0-9._=\-/]+):(?<domain>[a-zA-Z0-9\-.]+\.[a-zA-Z]+)$/;
|
const re = /^@(?<name>[a-zA-Z0-9._=\-/]+):(?<domain>[a-zA-Z0-9\-.]+\.[a-zA-Z]+)$/;
|
||||||
return re.exec(mxid)?.groups;
|
return re.exec(mxid)?.groups;
|
||||||
@ -53,7 +55,7 @@ export const getSupportedLoginFlows = async baseUrl => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const getMediaUrl = media_id => {
|
export const getMediaUrl = media_id => {
|
||||||
const baseUrl = localStorage.getItem("base_url");
|
const baseUrl = storage.getItem("base_url");
|
||||||
return `${baseUrl}/_matrix/media/v1/download/${media_id}?allow_redirect=true`;
|
return `${baseUrl}/_matrix/media/v1/download/${media_id}?allow_redirect=true`;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -62,7 +64,7 @@ export const getMediaUrl = media_id => {
|
|||||||
* @returns full MXID as string
|
* @returns full MXID as string
|
||||||
*/
|
*/
|
||||||
export function generateRandomMxId(): string {
|
export function generateRandomMxId(): string {
|
||||||
const homeserver = localStorage.getItem("home_server");
|
const homeserver = storage.getItem("home_server");
|
||||||
const characters = "0123456789abcdefghijklmnopqrstuvwxyz";
|
const characters = "0123456789abcdefghijklmnopqrstuvwxyz";
|
||||||
const localpart = Array.from(crypto.getRandomValues(new Uint32Array(8)))
|
const localpart = Array.from(crypto.getRandomValues(new Uint32Array(8)))
|
||||||
.map(x => characters[x % characters.length])
|
.map(x => characters[x % characters.length])
|
||||||
|
Loading…
x
Reference in New Issue
Block a user